diff options
author | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
---|---|---|
committer | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
commit | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/external_dependencies/openmpt-trunk/soundlib | |
parent | 537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff) | |
download | winamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz |
Initial community commit
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/soundlib')
180 files changed, 87398 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.cpp new file mode 100644 index 00000000..d7514e25 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.cpp @@ -0,0 +1,83 @@ +/* + * AudioCriticalSection.cpp + * ----------- + * Purpose: Implementation of OpenMPT's critical section for access to CSoundFile. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "AudioCriticalSection.h" + +#if defined(MODPLUG_TRACKER) +#include "../misc/mptMutex.h" +#endif + +OPENMPT_NAMESPACE_BEGIN + +#if defined(MODPLUG_TRACKER) + +#if MPT_COMPILER_MSVC +_Acquires_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC +CriticalSection::CriticalSection() + : m_refGlobalMutex(Tracker::GetGlobalMutexRef()) + , inSection(false) +{ + Enter(); +} + +CriticalSection::CriticalSection(CriticalSection &&other) noexcept + : m_refGlobalMutex(other.m_refGlobalMutex) + , inSection(other.inSection) +{ + other.inSection = false; +} + +CriticalSection::CriticalSection(InitialState state) + : m_refGlobalMutex(Tracker::GetGlobalMutexRef()) + , inSection(false) +{ + if(state == InitialState::Locked) + { + Enter(); + } +} + +#if MPT_COMPILER_MSVC +_Acquires_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC +void CriticalSection::Enter() +{ + if(!inSection) + { + inSection = true; + m_refGlobalMutex.lock(); + } +} + +#if MPT_COMPILER_MSVC +_Requires_lock_held_(m_refGlobalMutex.mutex) _Releases_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC +void CriticalSection::Leave() +{ + if(inSection) + { + inSection = false; + m_refGlobalMutex.unlock(); + } +} +CriticalSection::~CriticalSection() +{ + Leave(); +} + +#else + +MPT_MSVC_WORKAROUND_LNK4221(AudioCriticalSection) + +#endif + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.h b/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.h new file mode 100644 index 00000000..8b13a33d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/AudioCriticalSection.h @@ -0,0 +1,100 @@ +/* + * AudioCriticalSection.h + * --------- + * Purpose: Implementation of OpenMPT's critical section for access to CSoundFile. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#if defined(MODPLUG_TRACKER) +#include "../misc/mptMutex.h" +#endif + +OPENMPT_NAMESPACE_BEGIN + +#if defined(MODPLUG_TRACKER) + +namespace mpt { +class recursive_mutex_with_lock_count; +} // namespace mpt + +namespace Tracker { // implemented in mptrack/Mptrack.cpp +mpt::recursive_mutex_with_lock_count & GetGlobalMutexRef(); +} // namespace Tracker + +// Critical section handling done in (safe) RAII style. +// Create a CriticalSection object whenever you need exclusive access to CSoundFile. +// One object = one lock / critical section. +// The critical section is automatically left when the object is destroyed, but +// Enter() and Leave() can also be called manually if needed. +class CriticalSection +{ + +private: + + mpt::recursive_mutex_with_lock_count & m_refGlobalMutex; + +protected: + + bool inSection; + +public: + + enum class InitialState + { + Locked = 0, + Unlocked = 1, + }; + +public: + +#if MPT_COMPILER_MSVC + _Acquires_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC + CriticalSection(); + + CriticalSection(CriticalSection &&other) noexcept; + + explicit CriticalSection(InitialState state); + +#if MPT_COMPILER_MSVC + _Acquires_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC + void Enter(); + +#if MPT_COMPILER_MSVC + _Requires_lock_held_(m_refGlobalMutex.mutex) _Releases_lock_(m_refGlobalMutex.mutex) +#endif // MPT_COMPILER_MSVC + void Leave(); + + ~CriticalSection(); + +}; + +#else // !MODPLUG_TRACKER + +class CriticalSection +{ +public: + enum class InitialState + { + Locked = 0, + Unlocked = 1, + }; +public: + CriticalSection() {} + CriticalSection(CriticalSection &&) noexcept {} + explicit CriticalSection(InitialState) {} + void Enter() {} + void Leave() {} + ~CriticalSection() {} +}; + +#endif // MODPLUG_TRACKER + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/AudioReadTarget.h b/Src/external_dependencies/openmpt-trunk/soundlib/AudioReadTarget.h new file mode 100644 index 00000000..76799d91 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/AudioReadTarget.h @@ -0,0 +1,142 @@ +/* + * AudioReadTarget.h + * ----------------- + * Purpose: Callback class implementations for audio data read via CSoundFile::Read. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Sndfile.h" +#include "mpt/audio/span.hpp" +#include "openmpt/soundbase/SampleFormat.hpp" +#include "openmpt/soundbase/CopyMix.hpp" +#include "openmpt/soundbase/Dither.hpp" +#include "MixerLoops.h" +#include "Mixer.h" +#include "../common/Dither.h" + +#include <type_traits> + + +OPENMPT_NAMESPACE_BEGIN + + +template <typename Taudio_span, typename TDithers = DithersOpenMPT> +class AudioTargetBuffer + : public IAudioTarget +{ +private: + std::size_t countRendered; + TDithers &dithers; +protected: + Taudio_span outputBuffer; +public: + AudioTargetBuffer(Taudio_span buf, TDithers &dithers_) + : countRendered(0) + , dithers(dithers_) + , outputBuffer(buf) + { + return; + } + std::size_t GetRenderedCount() const { return countRendered; } +public: + void Process(mpt::audio_span_interleaved<MixSampleInt> buffer) override + { + std::visit( + [&](auto &ditherInstance) + { + ConvertBufferMixInternalFixedToBuffer<MixSampleIntTraits::mix_fractional_bits, false>(mpt::make_audio_span_with_offset(outputBuffer, countRendered), buffer, ditherInstance, buffer.size_channels(), buffer.size_frames()); + }, + dithers.Variant() + ); + countRendered += buffer.size_frames(); + } + void Process(mpt::audio_span_interleaved<MixSampleFloat> buffer) override + { + std::visit( + [&](auto &ditherInstance) + { + ConvertBufferMixInternalToBuffer<false>(mpt::make_audio_span_with_offset(outputBuffer, countRendered), buffer, ditherInstance, buffer.size_channels(), buffer.size_frames()); + }, + dithers.Variant() + ); + countRendered += buffer.size_frames(); + } +}; + + +template <typename Taudio_span, typename TDithers = DithersOpenMPT> +class AudioTargetBufferWithGain + : public AudioTargetBuffer<Taudio_span> +{ +private: + using Tbase = AudioTargetBuffer<Taudio_span>; +private: + const float gainFactor; +public: + AudioTargetBufferWithGain(Taudio_span buf, TDithers &dithers, float gainFactor_) + : Tbase(buf, dithers) + , gainFactor(gainFactor_) + { + return; + } +public: + void Process(mpt::audio_span_interleaved<MixSampleInt> buffer) override + { + const std::size_t countRendered_ = Tbase::GetRenderedCount(); + if constexpr(!std::is_floating_point<typename Taudio_span::sample_type>::value) + { + int32 gainFactor16_16 = mpt::saturate_round<int32>(gainFactor * (1 << 16)); + if(gainFactor16_16 != (1<<16)) + { + // only apply gain when != +/- 0dB + // no clipping prevention is done here + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < buffer.size_channels(); ++channel) + { + buffer(channel, frame) = Util::muldiv(buffer(channel, frame), gainFactor16_16, 1 << 16); + } + } + } + } + Tbase::Process(buffer); + if constexpr(std::is_floating_point<typename Taudio_span::sample_type>::value) + { + if(gainFactor != 1.0f) + { + // only apply gain when != +/- 0dB + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < buffer.size_channels(); ++channel) + { + Tbase::outputBuffer(channel, countRendered_ + frame) *= gainFactor; + } + } + } + } + } + void Process(mpt::audio_span_interleaved<MixSampleFloat> buffer) override + { + if(gainFactor != 1.0f) + { + // only apply gain when != +/- 0dB + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < buffer.size_channels(); ++channel) + { + buffer(channel, frame) *= gainFactor; + } + } + } + Tbase::Process(buffer); + } +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/BitReader.h b/Src/external_dependencies/openmpt-trunk/soundlib/BitReader.h new file mode 100644 index 00000000..30feb8da --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/BitReader.h @@ -0,0 +1,82 @@ +/* + * BitReader.h + * ----------- + * Purpose: An extended FileReader to read bit-oriented rather than byte-oriented streams. + * Notes : The current implementation can only read bit widths up to 32 bits, and it always + * reads bits starting from the least significant bit, as this is all that is + * required by the class users at the moment. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/FileReader.h" +#include <stdexcept> +#include "mpt/io/base.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +class BitReader : private FileReader +{ +protected: + off_t m_bufPos = 0, m_bufSize = 0; + uint32 bitBuf = 0; // Current bit buffer + int m_bitNum = 0; // Currently available number of bits + std::byte buffer[mpt::IO::BUFFERSIZE_TINY]{}; + +public: + + class eof : public std::range_error + { + public: + eof() : std::range_error("Truncated bit buffer") { } + }; + + BitReader() : FileReader() { } + BitReader(mpt::span<const std::byte> bytedata) : FileReader(bytedata) { } + BitReader(const FileCursor &other) : FileReader(other) { } + BitReader(FileCursor &&other) : FileReader(std::move(other)) { } + + off_t GetLength() const + { + return FileReader::GetLength(); + } + + off_t GetPosition() const + { + return FileReader::GetPosition() - m_bufSize + m_bufPos; + } + + uint32 ReadBits(int numBits) + { + while(m_bitNum < numBits) + { + // Fetch more bits + if(m_bufPos >= m_bufSize) + { + m_bufSize = ReadRaw(mpt::as_span(buffer)).size(); + m_bufPos = 0; + if(!m_bufSize) + { + throw eof(); + } + } + bitBuf |= (static_cast<uint32>(buffer[m_bufPos++]) << m_bitNum); + m_bitNum += 8; + } + + uint32 v = bitBuf & ((1 << numBits) - 1); + bitBuf >>= numBits; + m_bitNum -= numBits; + return v; + } +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Container.h b/Src/external_dependencies/openmpt-trunk/soundlib/Container.h new file mode 100644 index 00000000..f93fe4a8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Container.h @@ -0,0 +1,45 @@ +/* + * Container.h + * ----------- + * Purpose: General interface for MDO container and/or packers. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/FileReader.h" + +#include <vector> + +OPENMPT_NAMESPACE_BEGIN + + +struct ContainerItem +{ + mpt::ustring name; + FileReader file; + std::unique_ptr<std::vector<char> > data_cache; // may be empty +}; + + +enum ContainerLoadingFlags +{ + ContainerOnlyVerifyHeader = 0x00, + ContainerUnwrapData = 0x01, +}; + + +#if !defined(MPT_WITH_ANCIENT) +bool UnpackXPK(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags); +bool UnpackPP20(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags); +bool UnpackMMCMP(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags); +#endif // !MPT_WITH_ANCIENT +bool UnpackUMX(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ContainerMMCMP.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerMMCMP.cpp new file mode 100644 index 00000000..0f98338e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerMMCMP.cpp @@ -0,0 +1,391 @@ +/* + * ContainerMMCMP.cpp + * ------------------ + * Purpose: Handling of MMCMP compressed modules + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "../common/FileReader.h" +#include "Container.h" +#include "Sndfile.h" +#include "BitReader.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#if !defined(MPT_WITH_ANCIENT) + + +#ifdef MPT_ALL_LOGGING +#define MMCMP_LOG +#endif + + +struct MMCMPFileHeader +{ + char id[8]; // "ziRCONia" + uint16le hdrsize; // size of all the remaining header data + uint16le version; + uint16le nblocks; + uint32le filesize; + uint32le blktable; + uint8le glb_comp; + uint8le fmt_comp; + + bool Validate() const + { + if(std::memcmp(id, "ziRCONia", 8) != 0) + return false; + if(hdrsize != 14) + return false; + if(nblocks == 0) + return false; + if(filesize == 0) + return false; + if(filesize >= 0x80000000) + return false; + if(blktable < sizeof(MMCMPFileHeader)) + return false; + return true; + } +}; + +MPT_BINARY_STRUCT(MMCMPFileHeader, 24) + +struct MMCMPBlock +{ + uint32le unpk_size; + uint32le pk_size; + uint32le xor_chk; + uint16le sub_blk; + uint16le flags; + uint16le tt_entries; + uint16le num_bits; +}; + +MPT_BINARY_STRUCT(MMCMPBlock, 20) + +struct MMCMPSubBlock +{ + uint32le position; + uint32le size; + + bool Validate(std::vector<char> &unpackedData, const uint32 unpackedSize) const + { + if(position >= unpackedSize) + return false; + if(size > unpackedSize) + return false; + if(size > unpackedSize - position) + return false; + if(size == 0) + return false; + if(unpackedData.size() < position + size) + unpackedData.resize(position + size); + return true; + } +}; + +MPT_BINARY_STRUCT(MMCMPSubBlock, 8) + +enum MMCMPFlags : uint16 +{ + MMCMP_COMP = 0x0001, + MMCMP_DELTA = 0x0002, + MMCMP_16BIT = 0x0004, + MMCMP_STEREO = 0x0100, + MMCMP_ABS16 = 0x0200, + MMCMP_ENDIAN = 0x0400, +}; + +static constexpr uint8 MMCMP8BitCommands[8] = +{ + 0x01, 0x03, 0x07, 0x0F, 0x1E, 0x3C, 0x78, 0xF8 +}; + +static constexpr uint8 MMCMP8BitFetch[8] = +{ + 3, 3, 3, 3, 2, 1, 0, 0 +}; + +static constexpr uint16 MMCMP16BitCommands[16] = +{ + 0x01, 0x03, 0x07, 0x0F, 0x1E, 0x3C, 0x78, 0xF0, + 0x1F0, 0x3F0, 0x7F0, 0xFF0, 0x1FF0, 0x3FF0, 0x7FF0, 0xFFF0 +}; + +static constexpr uint8 MMCMP16BitFetch[16] = +{ + 4, 4, 4, 4, 3, 2, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0 +}; + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMMCMP(MemoryFileReader file, const uint64 *pfilesize) +{ + MMCMPFileHeader mfh; + if(!file.ReadStruct(mfh)) + return ProbeWantMoreData; + if(!mfh.Validate()) + return ProbeFailure; + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool UnpackMMCMP(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags) +{ + file.Rewind(); + containerItems.clear(); + + MMCMPFileHeader mfh; + if(!file.ReadStruct(mfh)) + return false; + if(!mfh.Validate()) + return false; + if(loadFlags == ContainerOnlyVerifyHeader) + return true; + if(!file.LengthIsAtLeast(mfh.blktable)) + return false; + if(!file.LengthIsAtLeast(mfh.blktable + 4 * mfh.nblocks)) + return false; + + containerItems.emplace_back(); + containerItems.back().data_cache = std::make_unique<std::vector<char> >(); + auto &unpackedData = *(containerItems.back().data_cache); + + // Generally it's not so simple to establish an upper limit for the uncompressed data size (blocks can be reused, etc.), + // so we just reserve a realistic amount of memory. + const uint32 unpackedSize = mfh.filesize; + unpackedData.reserve(std::min(unpackedSize, std::min(mpt::saturate_cast<uint32>(file.GetLength()), uint32_max / 20u) * 20u)); + // 8-bit deltas + uint8 ptable[256] = { 0 }; + + std::vector<MMCMPSubBlock> subblks; + for(uint32 nBlock = 0; nBlock < mfh.nblocks; nBlock++) + { + if(!file.Seek(mfh.blktable + 4 * nBlock)) + return false; + if(!file.CanRead(4)) + return false; + uint32 blkPos = file.ReadUint32LE(); + if(!file.Seek(blkPos)) + return false; + MMCMPBlock blk; + if(!file.ReadStruct(blk)) + return false; + if(!file.ReadVector(subblks, blk.sub_blk)) + return false; + const MMCMPSubBlock *psubblk = blk.sub_blk > 0 ? subblks.data() : nullptr; + + if(blkPos + sizeof(MMCMPBlock) + blk.sub_blk * sizeof(MMCMPSubBlock) >= file.GetLength()) + return false; + uint32 memPos = blkPos + sizeof(MMCMPBlock) + blk.sub_blk * sizeof(MMCMPSubBlock); + +#ifdef MMCMP_LOG + MPT_LOG_GLOBAL(LogDebug, "MMCMP", MPT_UFORMAT("block {}: flags={} sub_blocks={}")(nBlock, mpt::ufmt::HEX0<4>(static_cast<uint16>(blk.flags)), static_cast<uint16>(blk.sub_blk))); + MPT_LOG_GLOBAL(LogDebug, "MMCMP", MPT_UFORMAT(" pksize={} unpksize={}")(static_cast<uint32>(blk.pk_size), static_cast<uint32>(blk.unpk_size))); + MPT_LOG_GLOBAL(LogDebug, "MMCMP", MPT_UFORMAT(" tt_entries={} num_bits={}")(static_cast<uint16>(blk.tt_entries), static_cast<uint16>(blk.num_bits))); +#endif + if(!(blk.flags & MMCMP_COMP)) + { + // Data is not packed + for(uint32 i = 0; i < blk.sub_blk; i++) + { + if(!psubblk) + return false; + if(!psubblk->Validate(unpackedData, unpackedSize)) + return false; +#ifdef MMCMP_LOG + MPT_LOG_GLOBAL(LogDebug, "MMCMP", MPT_UFORMAT(" Unpacked sub-block {}: offset {}, size={}")(i, static_cast<uint32>(psubblk->position), static_cast<uint32>(psubblk->size))); +#endif + if(!file.Seek(memPos)) + return false; + if(file.ReadRaw(mpt::span(&(unpackedData[psubblk->position]), psubblk->size)).size() != psubblk->size) + return false; + psubblk++; + } + } else if(blk.flags & MMCMP_16BIT) + { + // Data is 16-bit packed + uint32 subblk = 0; + if(!psubblk) + return false; + if(!psubblk[subblk].Validate(unpackedData, unpackedSize)) + return false; + char *pDest = &(unpackedData[psubblk[subblk].position]); + uint32 dwSize = psubblk[subblk].size & ~1u; + if(!dwSize) + return false; + uint32 dwPos = 0; + uint32 numbits = blk.num_bits; + uint32 oldval = 0; + +#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(!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) }; + + try + { + while (subblk < blk.sub_blk) + { + uint32 newval = 0x10000; + uint32 d = bitFile.ReadBits(numbits + 1); + + uint32 command = MMCMP16BitCommands[numbits & 0x0F]; + if(d >= command) + { + uint32 nFetch = MMCMP16BitFetch[numbits & 0x0F]; + uint32 newbits = bitFile.ReadBits(nFetch) + ((d - command) << nFetch); + if(newbits != numbits) + { + numbits = newbits & 0x0F; + } else if((d = bitFile.ReadBits(4)) == 0x0F) + { + if(bitFile.ReadBits(1)) + break; + newval = 0xFFFF; + } else + { + newval = 0xFFF0 + d; + } + } else + { + newval = d; + } + if(newval < 0x10000) + { + newval = (newval & 1) ? (uint32)(-(int32)((newval + 1) >> 1)) : (uint32)(newval >> 1); + if(blk.flags & MMCMP_DELTA) + { + newval += oldval; + oldval = newval; + } else if(!(blk.flags & MMCMP_ABS16)) + { + newval ^= 0x8000; + } + if(blk.flags & MMCMP_ENDIAN) + { + pDest[dwPos + 0] = static_cast<uint8>(newval >> 8); + pDest[dwPos + 1] = static_cast<uint8>(newval & 0xFF); + } else + { + pDest[dwPos + 0] = static_cast<uint8>(newval & 0xFF); + pDest[dwPos + 1] = static_cast<uint8>(newval >> 8); + } + dwPos += 2; + } + if(dwPos >= dwSize) + { + subblk++; + dwPos = 0; + if(!(subblk < blk.sub_blk)) + break; + if(!psubblk[subblk].Validate(unpackedData, unpackedSize)) + return false; + dwSize = psubblk[subblk].size & ~1u; + if(!dwSize) + return false; + pDest = &(unpackedData[psubblk[subblk].position]); + } + } + } catch(const BitReader::eof &) + { + } + } else + { + // Data is 8-bit packed + uint32 subblk = 0; + if(!psubblk) + return false; + if(!psubblk[subblk].Validate(unpackedData, unpackedSize)) + return false; + char *pDest = &(unpackedData[psubblk[subblk].position]); + uint32 dwSize = psubblk[subblk].size; + uint32 dwPos = 0; + uint32 numbits = blk.num_bits; + uint32 oldval = 0; + if(blk.tt_entries > sizeof(ptable) + || !file.Seek(memPos) + || file.ReadRaw(mpt::span(ptable, blk.tt_entries)).size() < 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) }; + + try + { + while (subblk < blk.sub_blk) + { + uint32 newval = 0x100; + uint32 d = bitFile.ReadBits(numbits + 1); + + uint32 command = MMCMP8BitCommands[numbits & 0x07]; + if(d >= command) + { + uint32 nFetch = MMCMP8BitFetch[numbits & 0x07]; + uint32 newbits = bitFile.ReadBits(nFetch) + ((d - command) << nFetch); + if(newbits != numbits) + { + numbits = newbits & 0x07; + } else if((d = bitFile.ReadBits(3)) == 7) + { + if(bitFile.ReadBits(1)) + break; + newval = 0xFF; + } else + { + newval = 0xF8 + d; + } + } else + { + newval = d; + } + if(newval < sizeof(ptable)) + { + int n = ptable[newval]; + if(blk.flags & MMCMP_DELTA) + { + n += oldval; + oldval = n; + } + pDest[dwPos++] = static_cast<uint8>(n); + } + if(dwPos >= dwSize) + { + subblk++; + dwPos = 0; + if(!(subblk < blk.sub_blk)) + break; + if(!psubblk[subblk].Validate(unpackedData, unpackedSize)) + return false; + dwSize = psubblk[subblk].size; + pDest = &(unpackedData[psubblk[subblk].position]); + } + } + } catch(const BitReader::eof &) + { + } + } + } + + containerItems.back().file = FileReader(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(unpackedData))); + + return true; +} + + +#endif // !MPT_WITH_ANCIENT + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ContainerPP20.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerPP20.cpp new file mode 100644 index 00000000..cec6502e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerPP20.cpp @@ -0,0 +1,217 @@ +/* + * ContainerPP20.cpp + * ----------------- + * Purpose: Handling of PowerPack PP20 compressed modules + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "../common/FileReader.h" +#include "Container.h" +#include "Sndfile.h" + +#include <stdexcept> + + +OPENMPT_NAMESPACE_BEGIN + + +#if !defined(MPT_WITH_ANCIENT) + + +struct PPBITBUFFER +{ + uint32 bitcount = 0; + uint32 bitbuffer = 0; + const uint8 *pStart = nullptr; + const uint8 *pSrc = nullptr; + + uint32 GetBits(uint32 n); +}; + + +uint32 PPBITBUFFER::GetBits(uint32 n) +{ + uint32 result = 0; + + for(uint32 i = 0; i < n; i++) + { + if(!bitcount) + { + bitcount = 8; + if(pSrc != pStart) + pSrc--; + bitbuffer = *pSrc; + } + result = (result << 1) | (bitbuffer & 1); + bitbuffer >>= 1; + bitcount--; + } + return result; +} + + +static bool PP20_DoUnpack(const uint8 *pSrc, uint32 srcLen, uint8 *pDst, uint32 dstLen) +{ + const std::array<uint8, 4> modeTable{pSrc[0], pSrc[1], pSrc[2], pSrc[3]}; + PPBITBUFFER BitBuffer; + BitBuffer.pStart = pSrc; + BitBuffer.pSrc = pSrc + srcLen - 4; + BitBuffer.GetBits(pSrc[srcLen - 1]); + uint32 bytesLeft = dstLen; + while(bytesLeft > 0) + { + if(!BitBuffer.GetBits(1)) + { + uint32 count = 1, countAdd; + do + { + countAdd = BitBuffer.GetBits(2); + count += countAdd; + } while(countAdd == 3); + LimitMax(count, bytesLeft); + for(uint32 i = 0; i < count; i++) + { + pDst[--bytesLeft] = (uint8)BitBuffer.GetBits(8); + } + if(!bytesLeft) + break; + } + { + uint32 modeIndex = BitBuffer.GetBits(2); + MPT_CHECKER_ASSUME(modeIndex < 4); + uint32 count = modeIndex + 2, offset; + if(modeIndex == 3) + { + offset = BitBuffer.GetBits((BitBuffer.GetBits(1)) ? modeTable[modeIndex] : 7); + uint32 countAdd = 7; + do + { + countAdd = BitBuffer.GetBits(3); + count += countAdd; + } while(countAdd == 7); + } else + { + offset = BitBuffer.GetBits(modeTable[modeIndex]); + } + LimitMax(count, bytesLeft); + for(uint32 i = 0; i < count; i++) + { + pDst[bytesLeft - 1] = (bytesLeft + offset < dstLen) ? pDst[bytesLeft + offset] : 0; + --bytesLeft; + } + } + } + return true; +} + + +struct PP20header +{ + char magic[4]; // "PP20" + uint8 efficiency[4]; +}; + +MPT_BINARY_STRUCT(PP20header, 8) + + +static bool ValidateHeader(const PP20header &hdr) +{ + if(std::memcmp(hdr.magic, "PP20", 4) != 0) + { + return false; + } + if(hdr.efficiency[0] < 9 || hdr.efficiency[0] > 15 + || hdr.efficiency[1] < 9 || hdr.efficiency[1] > 15 + || hdr.efficiency[2] < 9 || hdr.efficiency[2] > 15 + || hdr.efficiency[3] < 9 || hdr.efficiency[3] > 15) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPP20(MemoryFileReader file, const uint64 *pfilesize) +{ + PP20header hdr; + if(!file.ReadStruct(hdr)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(hdr)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool UnpackPP20(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags) +{ + file.Rewind(); + containerItems.clear(); + + PP20header hdr; + if(!file.ReadStruct(hdr)) + { + return false; + } + if(!ValidateHeader(hdr)) + { + return false; + } + if(loadFlags == ContainerOnlyVerifyHeader) + { + return true; + } + + if(!file.CanRead(4)) + { + return false; + } + + containerItems.emplace_back(); + containerItems.back().data_cache = std::make_unique<std::vector<char> >(); + std::vector<char> & unpackedData = *(containerItems.back().data_cache); + + FileReader::off_t length = file.GetLength(); + if(!mpt::in_range<uint32>(length)) return false; + // Length word must be aligned + if((length % 2u) != 0) + return false; + + file.Seek(length - 4); + uint32 dstLen = file.ReadUint24BE(); + if(dstLen == 0) + return false; + try + { + unpackedData.resize(dstLen); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + file.Seek(4); + bool result = PP20_DoUnpack(file.GetRawData<uint8>().data(), static_cast<uint32>(length - 4), mpt::byte_cast<uint8 *>(unpackedData.data()), dstLen); + + if(result) + { + containerItems.back().file = FileReader(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(unpackedData))); + } + + return result; +} + + +#endif // !MPT_WITH_ANCIENT + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ContainerUMX.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerUMX.cpp new file mode 100644 index 00000000..828bee6c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerUMX.cpp @@ -0,0 +1,73 @@ +/* + * ContainerUMX.cpp + * ---------------- + * Purpose: UMX (Unreal Music) module ripper + * Notes : Obviously, this code only rips modules from older Unreal Engine games, such as Unreal 1, Unreal Tournament 1 and Deus Ex. + * Authors: OpenMPT Devs (inspired by code from http://wiki.beyondunreal.com/Legacy:Package_File_Format) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "UMXTools.h" +#include "Container.h" +#include "Sndfile.h" + + +OPENMPT_NAMESPACE_BEGIN + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderUMX(MemoryFileReader file, const uint64 *pfilesize) +{ + return UMX::ProbeFileHeader(file, pfilesize, "music"); +} + + +bool UnpackUMX(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags) +{ + file.Rewind(); + containerItems.clear(); + + UMX::FileHeader fileHeader; + if(!file.ReadStruct(fileHeader) || !fileHeader.IsValid()) + return false; + + // Note that this can be a false positive, e.g. Unreal maps will have music and sound + // in their name table because they usually import such files. However, it spares us + // from wildly seeking through the file, as the name table is usually right at the + // start of the file, so it is hopefully a good enough heuristic for our purposes. + if(!UMX::FindNameTableEntry(file, fileHeader, "music")) + return false; + else if(!file.CanRead(fileHeader.GetMinimumAdditionalFileSize())) + return false; + else if(loadFlags == ContainerOnlyVerifyHeader) + return true; + + const std::vector<std::string> names = UMX::ReadNameTable(file, fileHeader); + const std::vector<int32> classes = UMX::ReadImportTable(file, fileHeader, names); + + // Read export table + file.Seek(fileHeader.exportOffset); + for(uint32 i = 0; i < fileHeader.exportCount && file.CanRead(8); i++) + { + auto [fileChunk, objName] = UMX::ReadExportTableEntry(file, fileHeader, classes, names, "music"); + if(!fileChunk.IsValid()) + continue; + + ContainerItem item; + + if(objName >= 0 && static_cast<std::size_t>(objName) < names.size()) + { + item.name = mpt::ToUnicode(mpt::Charset::Windows1252, names[objName]); + } + + item.file = fileChunk; + + containerItems.push_back(std::move(item)); + } + + return !containerItems.empty(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ContainerXPK.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerXPK.cpp new file mode 100644 index 00000000..87b89408 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ContainerXPK.cpp @@ -0,0 +1,429 @@ +/* + * ContainerXPK.cpp + * ---------------- + * Purpose: Handling of XPK compressed modules + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "../common/FileReader.h" +#include "Container.h" +#include "Sndfile.h" + +#include <stdexcept> + + +OPENMPT_NAMESPACE_BEGIN + + +#if !defined(MPT_WITH_ANCIENT) + + +#ifdef MPT_ALL_LOGGING +#define MMCMP_LOG +#endif + + +struct XPKFILEHEADER +{ + char XPKF[4]; + uint32be SrcLen; + char SQSH[4]; + uint32be DstLen; + char Name[16]; + uint32be Reserved; +}; + +MPT_BINARY_STRUCT(XPKFILEHEADER, 36) + + +struct XPK_error : public std::range_error +{ + XPK_error() : std::range_error("invalid XPK data") { } +}; + +struct XPK_BufferBounds +{ + const uint8 *pSrcBeg; + std::size_t SrcSize; + + inline uint8 SrcRead(std::size_t index) + { + if(index >= SrcSize) throw XPK_error(); + return pSrcBeg[index]; + } +}; + +static int32 bfextu(std::size_t p, int32 bo, int32 bc, XPK_BufferBounds &bufs) +{ + uint32 r; + + p += bo / 8; + r = bufs.SrcRead(p); p++; + r <<= 8; + r |= bufs.SrcRead(p); p++; + r <<= 8; + r |= bufs.SrcRead(p); + r <<= bo % 8; + r &= 0xffffff; + r >>= 24 - bc; + + return r; +} + +static int32 bfexts(std::size_t p, int32 bo, int32 bc, XPK_BufferBounds &bufs) +{ + uint32 r; + + p += bo / 8; + r = bufs.SrcRead(p); p++; + r <<= 8; + r |= bufs.SrcRead(p); p++; + r <<= 8; + r |= bufs.SrcRead(p); + r <<= (bo % 8) + 8; + return mpt::rshift_signed(static_cast<int32>(r), 32 - bc); +} + + +static uint8 XPK_ReadTable(int32 index) +{ + static constexpr uint8 xpk_table[] = { + 2,3,4,5,6,7,8,0,3,2,4,5,6,7,8,0,4,3,5,2,6,7,8,0,5,4,6,2,3,7,8,0,6,5,7,2,3,4,8,0,7,6,8,2,3,4,5,0,8,7,6,2,3,4,5,0 + }; + if(index < 0) throw XPK_error(); + if(static_cast<std::size_t>(index) >= std::size(xpk_table)) throw XPK_error(); + return xpk_table[index]; +} + +static bool XPK_DoUnpack(const uint8 *src_, uint32 srcLen, std::vector<char> &unpackedData, int32 len) +{ + if(len <= 0) return false; + int32 d0,d1,d2,d3,d4,d5,d6,a2,a5; + int32 cp, cup1, type; + std::size_t c; + std::size_t src; + std::size_t phist = 0; + + unpackedData.reserve(std::min(static_cast<uint32>(len), std::min(srcLen, uint32_max / 20u) * 20u)); + + XPK_BufferBounds bufs; + bufs.pSrcBeg = src_; + bufs.SrcSize = srcLen; + + src = 0; + c = src; + while(len > 0) + { + type = bufs.SrcRead(c+0); + cp = (bufs.SrcRead(c+4)<<8) | (bufs.SrcRead(c+5)); // packed + cup1 = (bufs.SrcRead(c+6)<<8) | (bufs.SrcRead(c+7)); // unpacked + //Log(" packed=%6d unpacked=%6d bytes left=%d dst=%08X(%d)\n", cp, cup1, len, dst, dst); + c += 8; + src = c+2; + if (type == 0) + { + // RAW chunk + if(cp < 0 || cp > len) throw XPK_error(); + for(int32 i = 0; i < cp; ++i) + { + unpackedData.push_back(bufs.SrcRead(c + i)); + } + c+=cp; + len -= cp; + continue; + } + + if (type != 1) + { + #ifdef MMCMP_LOG + MPT_LOG_GLOBAL(LogDebug, "XPK", MPT_UFORMAT("Invalid XPK type! ({} bytes left)")(len)); + #endif + break; + } + LimitMax(cup1, len); + len -= cup1; + cp = (cp + 3) & 0xfffc; + c += cp; + + d0 = d1 = d2 = a2 = 0; + d3 = bufs.SrcRead(src); src++; + unpackedData.push_back(static_cast<char>(d3)); + cup1--; + + while (cup1 > 0) + { + if (d1 >= 8) goto l6dc; + if (bfextu(src,d0,1,bufs)) goto l75a; + d0 += 1; + d5 = 0; + d6 = 8; + goto l734; + + l6dc: + if (bfextu(src,d0,1,bufs)) goto l726; + d0 += 1; + if (! bfextu(src,d0,1,bufs)) goto l75a; + d0 += 1; + if (bfextu(src,d0,1,bufs)) goto l6f6; + d6 = 2; + goto l708; + + l6f6: + d0 += 1; + if (!bfextu(src,d0,1,bufs)) goto l706; + d6 = bfextu(src,d0,3,bufs); + d0 += 3; + goto l70a; + + l706: + d6 = 3; + l708: + d0 += 1; + l70a: + d6 = XPK_ReadTable((8*a2) + d6 -17); + if (d6 != 8) goto l730; + l718: + if (d2 >= 20) + { + d5 = 1; + goto l732; + } + d5 = 0; + goto l734; + + l726: + d0 += 1; + d6 = 8; + if (d6 == a2) goto l718; + d6 = a2; + l730: + d5 = 4; + l732: + d2 += 8; + l734: + while ((d5 >= 0) && (cup1 > 0)) + { + d4 = bfexts(src,d0,d6,bufs); + d0 += d6; + d3 -= d4; + unpackedData.push_back(static_cast<char>(d3)); + cup1--; + d5--; + } + if (d1 != 31) d1++; + a2 = d6; + l74c: + d6 = d2; + d6 >>= 3; + d2 -= d6; + } + } + return !unpackedData.empty(); + +l75a: + d0 += 1; + if (bfextu(src,d0,1,bufs)) goto l766; + d4 = 2; + goto l79e; + +l766: + d0 += 1; + if (bfextu(src,d0,1,bufs)) goto l772; + d4 = 4; + goto l79e; + +l772: + d0 += 1; + if (bfextu(src,d0,1,bufs)) goto l77e; + d4 = 6; + goto l79e; + +l77e: + d0 += 1; + if (bfextu(src,d0,1,bufs)) goto l792; + d0 += 1; + d6 = bfextu(src,d0,3,bufs); + d0 += 3; + d6 += 8; + goto l7a8; + +l792: + d0 += 1; + d6 = bfextu(src,d0,5,bufs); + d0 += 5; + d4 = 16; + goto l7a6; + +l79e: + d0 += 1; + d6 = bfextu(src,d0,1,bufs); + d0 += 1; +l7a6: + d6 += d4; +l7a8: + if(bfextu(src, d0, 1, bufs)) + { + d5 = 12; + a5 = -0x100; + } else + { + d0 += 1; + if(bfextu(src, d0, 1, bufs)) + { + d5 = 14; + a5 = -0x1100; + } else + { + d5 = 8; + a5 = 0; + } + } + + d0 += 1; + d4 = bfextu(src,d0,d5,bufs); + d0 += d5; + d6 -= 3; + if (d6 >= 0) + { + if (d6 > 0) d1 -= 1; + d1 -= 1; + if (d1 < 0) d1 = 0; + } + d6 += 2; + phist = unpackedData.size() + a5 - d4 - 1; + if(phist >= unpackedData.size()) + throw XPK_error(); + + while ((d6 >= 0) && (cup1 > 0)) + { + d3 = unpackedData[phist]; + phist++; + unpackedData.push_back(static_cast<char>(d3)); + cup1--; + d6--; + } + goto l74c; +} + + +static bool ValidateHeader(const XPKFILEHEADER &header) +{ + if(std::memcmp(header.XPKF, "XPKF", 4) != 0) + { + return false; + } + if(std::memcmp(header.SQSH, "SQSH", 4) != 0) + { + return false; + } + if(header.SrcLen == 0) + { + return false; + } + if(header.DstLen == 0) + { + return false; + } + static_assert(sizeof(XPKFILEHEADER) >= 8); + if(header.SrcLen < (sizeof(XPKFILEHEADER) - 8)) + { + return false; + } + return true; +} + + +static bool ValidateHeaderFileSize(const XPKFILEHEADER &header, uint64 filesize) +{ + if(filesize < header.SrcLen - 8) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderXPK(MemoryFileReader file, const uint64 *pfilesize) +{ + XPKFILEHEADER header; + if(!file.ReadStruct(header)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(header)) + { + return ProbeFailure; + } + if(pfilesize) + { + if(!ValidateHeaderFileSize(header, *pfilesize)) + { + return ProbeFailure; + } + } + return ProbeSuccess; +} + + +bool UnpackXPK(std::vector<ContainerItem> &containerItems, FileReader &file, ContainerLoadingFlags loadFlags) +{ + file.Rewind(); + containerItems.clear(); + + XPKFILEHEADER header; + if(!file.ReadStruct(header)) + { + return false; + } + if(!ValidateHeader(header)) + { + return false; + } + if(loadFlags == ContainerOnlyVerifyHeader) + { + return true; + } + + if(!file.CanRead(header.SrcLen - (sizeof(XPKFILEHEADER) - 8))) + { + return false; + } + + containerItems.emplace_back(); + containerItems.back().data_cache = std::make_unique<std::vector<char> >(); + std::vector<char> & unpackedData = *(containerItems.back().data_cache); + + #ifdef MMCMP_LOG + MPT_LOG_GLOBAL(LogDebug, "XPK", MPT_UFORMAT("XPK detected (SrcLen={} DstLen={}) filesize={}")(static_cast<uint32>(header.SrcLen), static_cast<uint32>(header.DstLen), file.GetLength())); + #endif + bool result = false; + try + { + result = XPK_DoUnpack(file.GetRawData<uint8>().data(), header.SrcLen - (sizeof(XPKFILEHEADER) - 8), unpackedData, header.DstLen); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } catch(const XPK_error &) + { + return false; + } + + if(result) + { + containerItems.back().file = FileReader(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(unpackedData))); + } + return result; +} + + +#endif // !MPT_WITH_ANCIENT + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.cpp new file mode 100644 index 00000000..e5d7e52d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.cpp @@ -0,0 +1,2212 @@ +/* + * DLSBank.cpp + * ----------- + * Purpose: Sound bank loading. + * Notes : Supported sound bank types: DLS (including embedded DLS in MSS & RMI), SF2, SF3 / SF4 (modified SF2 with compressed samples) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/Mptrack.h" +#include "../common/mptFileIO.h" +#endif +#include "Dlsbank.h" +#include "Loaders.h" +#include "SampleCopy.h" +#include "../common/mptStringBuffer.h" +#include "../common/FileReader.h" +#include "openmpt/base/Endian.hpp" +#include "SampleIO.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +OPENMPT_NAMESPACE_BEGIN + +#ifdef MODPLUG_TRACKER + +#ifdef MPT_ALL_LOGGING +#define DLSBANK_LOG +#define DLSINSTR_LOG +#endif + +#define F_RGN_OPTION_SELFNONEXCLUSIVE 0x0001 + +// Region Flags +enum RegionFlags +{ + DLSREGION_KEYGROUPMASK = 0x0F, + DLSREGION_OVERRIDEWSMP = 0x10, + DLSREGION_PINGPONGLOOP = 0x20, + DLSREGION_SAMPLELOOP = 0x40, + DLSREGION_SELFNONEXCLUSIVE = 0x80, + DLSREGION_SUSTAINLOOP = 0x100, +}; + +/////////////////////////////////////////////////////////////////////////// +// Articulation connection graph definitions + +enum ConnectionSource : uint16 +{ + // Generic Sources + CONN_SRC_NONE = 0x0000, + CONN_SRC_LFO = 0x0001, + CONN_SRC_KEYONVELOCITY = 0x0002, + CONN_SRC_KEYNUMBER = 0x0003, + CONN_SRC_EG1 = 0x0004, + CONN_SRC_EG2 = 0x0005, + CONN_SRC_PITCHWHEEL = 0x0006, + + CONN_SRC_POLYPRESSURE = 0x0007, + CONN_SRC_CHANNELPRESSURE = 0x0008, + CONN_SRC_VIBRATO = 0x0009, + + // Midi Controllers 0-127 + CONN_SRC_CC1 = 0x0081, + CONN_SRC_CC7 = 0x0087, + CONN_SRC_CC10 = 0x008a, + CONN_SRC_CC11 = 0x008b, + + CONN_SRC_CC91 = 0x00db, + CONN_SRC_CC93 = 0x00dd, + + CONN_SRC_RPN0 = 0x0100, + CONN_SRC_RPN1 = 0x0101, + CONN_SRC_RPN2 = 0x0102, +}; + +enum ConnectionDestination : uint16 +{ + // Generic Destinations + CONN_DST_NONE = 0x0000, + CONN_DST_ATTENUATION = 0x0001, + CONN_DST_RESERVED = 0x0002, + CONN_DST_PITCH = 0x0003, + CONN_DST_PAN = 0x0004, + + // LFO Destinations + CONN_DST_LFO_FREQUENCY = 0x0104, + CONN_DST_LFO_STARTDELAY = 0x0105, + + CONN_DST_KEYNUMBER = 0x0005, + + // EG1 Destinations + CONN_DST_EG1_ATTACKTIME = 0x0206, + CONN_DST_EG1_DECAYTIME = 0x0207, + CONN_DST_EG1_RESERVED = 0x0208, + CONN_DST_EG1_RELEASETIME = 0x0209, + CONN_DST_EG1_SUSTAINLEVEL = 0x020a, + + CONN_DST_EG1_DELAYTIME = 0x020b, + CONN_DST_EG1_HOLDTIME = 0x020c, + CONN_DST_EG1_SHUTDOWNTIME = 0x020d, + + // EG2 Destinations + CONN_DST_EG2_ATTACKTIME = 0x030a, + CONN_DST_EG2_DECAYTIME = 0x030b, + CONN_DST_EG2_RESERVED = 0x030c, + CONN_DST_EG2_RELEASETIME = 0x030d, + CONN_DST_EG2_SUSTAINLEVEL = 0x030e, + + CONN_DST_EG2_DELAYTIME = 0x030f, + CONN_DST_EG2_HOLDTIME = 0x0310, + + CONN_TRN_NONE = 0x0000, + CONN_TRN_CONCAVE = 0x0001, +}; + + +////////////////////////////////////////////////////////// +// Supported DLS1 Articulations + +// [4-bit transform][12-bit dest][8-bit control][8-bit source] = 32-bit ID +constexpr uint32 DLSArt(uint8 src, uint8 ctl, uint16 dst) +{ + return (dst << 16u) | (ctl << 8u) | src; +} + +enum DLSArt : uint32 +{ + // Vibrato / Tremolo + ART_LFO_FREQUENCY = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_LFO_FREQUENCY), + ART_LFO_STARTDELAY = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_LFO_STARTDELAY), + ART_LFO_ATTENUATION = DLSArt(CONN_SRC_LFO, CONN_SRC_NONE, CONN_DST_ATTENUATION), + ART_LFO_PITCH = DLSArt(CONN_SRC_LFO, CONN_SRC_NONE, CONN_DST_PITCH), + ART_LFO_MODWTOATTN = DLSArt(CONN_SRC_LFO, CONN_SRC_CC1, CONN_DST_ATTENUATION), + ART_LFO_MODWTOPITCH = DLSArt(CONN_SRC_LFO, CONN_SRC_CC1, CONN_DST_PITCH), + + // Volume Envelope + ART_VOL_EG_ATTACKTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_ATTACKTIME), + ART_VOL_EG_DECAYTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_DECAYTIME), + ART_VOL_EG_SUSTAINLEVEL = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_SUSTAINLEVEL), + ART_VOL_EG_RELEASETIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_RELEASETIME), + ART_VOL_EG_DELAYTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_DELAYTIME), + ART_VOL_EG_HOLDTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_HOLDTIME), + ART_VOL_EG_SHUTDOWNTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG1_SHUTDOWNTIME), + ART_VOL_EG_VELTOATTACK = DLSArt(CONN_SRC_KEYONVELOCITY, CONN_SRC_NONE, CONN_DST_EG1_ATTACKTIME), + ART_VOL_EG_KEYTODECAY = DLSArt(CONN_SRC_KEYNUMBER, CONN_SRC_NONE, CONN_DST_EG1_DECAYTIME), + + // Pitch Envelope + ART_PITCH_EG_ATTACKTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG2_ATTACKTIME), + ART_PITCH_EG_DECAYTIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG2_DECAYTIME), + ART_PITCH_EG_SUSTAINLEVEL = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG2_SUSTAINLEVEL), + ART_PITCH_EG_RELEASETIME = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_EG2_RELEASETIME), + ART_PITCH_EG_VELTOATTACK = DLSArt(CONN_SRC_KEYONVELOCITY, CONN_SRC_NONE, CONN_DST_EG2_ATTACKTIME), + ART_PITCH_EG_KEYTODECAY = DLSArt(CONN_SRC_KEYNUMBER, CONN_SRC_NONE, CONN_DST_EG2_DECAYTIME), + + // Default Pan + ART_DEFAULTPAN = DLSArt(CONN_SRC_NONE, CONN_SRC_NONE, CONN_DST_PAN), +}; + +////////////////////////////////////////////////////////// +// DLS IFF Chunk IDs + +enum IFFChunkID : uint32 +{ + // Standard IFF chunks IDs + IFFID_FORM = MagicLE("FORM"), + IFFID_RIFF = MagicLE("RIFF"), + IFFID_LIST = MagicLE("LIST"), + IFFID_INFO = MagicLE("INFO"), + + // IFF Info fields + IFFID_ICOP = MagicLE("ICOP"), + IFFID_INAM = MagicLE("INAM"), + IFFID_ICMT = MagicLE("ICMT"), + IFFID_IENG = MagicLE("IENG"), + IFFID_ISFT = MagicLE("ISFT"), + IFFID_ISBJ = MagicLE("ISBJ"), + + // Wave IFF chunks IDs + IFFID_wave = MagicLE("wave"), + IFFID_wsmp = MagicLE("wsmp"), + + IFFID_XDLS = MagicLE("XDLS"), + IFFID_DLS = MagicLE("DLS "), + IFFID_MLS = MagicLE("MLS "), + IFFID_RMID = MagicLE("RMID"), + IFFID_colh = MagicLE("colh"), + IFFID_ins = MagicLE("ins "), + IFFID_insh = MagicLE("insh"), + IFFID_ptbl = MagicLE("ptbl"), + IFFID_wvpl = MagicLE("wvpl"), + IFFID_rgn = MagicLE("rgn "), + IFFID_rgn2 = MagicLE("rgn2"), + IFFID_rgnh = MagicLE("rgnh"), + IFFID_wlnk = MagicLE("wlnk"), + IFFID_art1 = MagicLE("art1"), + IFFID_art2 = MagicLE("art2"), +}; + +////////////////////////////////////////////////////////// +// DLS Structures definitions + +struct IFFCHUNK +{ + uint32le id; + uint32le len; +}; + +MPT_BINARY_STRUCT(IFFCHUNK, 8) + +struct RIFFChunkID +{ + uint32le id_RIFF; + uint32le riff_len; + uint32le id_DLS; +}; + +MPT_BINARY_STRUCT(RIFFChunkID, 12) + +struct LISTChunk +{ + uint32le id; + uint32le len; + uint32le listid; +}; + +MPT_BINARY_STRUCT(LISTChunk, 12) + +struct DLSRgnRange +{ + uint16le usLow; + uint16le usHigh; +}; + +MPT_BINARY_STRUCT(DLSRgnRange, 4) + +struct VERSChunk +{ + uint32le id; + uint32le len; + uint16le version[4]; +}; + +MPT_BINARY_STRUCT(VERSChunk, 16) + +struct PTBLChunk +{ + uint32le cbSize; + uint32le cCues; +}; + +MPT_BINARY_STRUCT(PTBLChunk, 8) + +struct INSHChunk +{ + uint32le cRegions; + uint32le ulBank; + uint32le ulInstrument; +}; + +MPT_BINARY_STRUCT(INSHChunk, 12) + +struct RGNHChunk +{ + DLSRgnRange RangeKey; + DLSRgnRange RangeVelocity; + uint16le fusOptions; + uint16le usKeyGroup; +}; + +MPT_BINARY_STRUCT(RGNHChunk, 12) + +struct WLNKChunk +{ + uint16le fusOptions; + uint16le usPhaseGroup; + uint32le ulChannel; + uint32le ulTableIndex; +}; + +MPT_BINARY_STRUCT(WLNKChunk, 12) + +struct ART1Chunk +{ + uint32le cbSize; + uint32le cConnectionBlocks; +}; + +MPT_BINARY_STRUCT(ART1Chunk, 8) + +struct ConnectionBlock +{ + uint16le usSource; + uint16le usControl; + uint16le usDestination; + uint16le usTransform; + int32le lScale; +}; + +MPT_BINARY_STRUCT(ConnectionBlock, 12) + +struct WSMPChunk +{ + uint32le cbSize; + uint16le usUnityNote; + int16le sFineTune; + int32le lAttenuation; + uint32le fulOptions; + uint32le cSampleLoops; +}; + +MPT_BINARY_STRUCT(WSMPChunk, 20) + +struct WSMPSampleLoop +{ + uint32le cbSize; + uint32le ulLoopType; + uint32le ulLoopStart; + uint32le ulLoopLength; + +}; + +MPT_BINARY_STRUCT(WSMPSampleLoop, 16) + + +///////////////////////////////////////////////////////////////////// +// SF2 IFF Chunk IDs + +enum SF2ChunkID : uint32 +{ + IFFID_ifil = MagicLE("ifil"), + IFFID_sfbk = MagicLE("sfbk"), + IFFID_sfpk = MagicLE("sfpk"), // SF2Pack compressed soundfont + IFFID_sdta = MagicLE("sdta"), + IFFID_pdta = MagicLE("pdta"), + IFFID_phdr = MagicLE("phdr"), + IFFID_pbag = MagicLE("pbag"), + IFFID_pgen = MagicLE("pgen"), + IFFID_inst = MagicLE("inst"), + IFFID_ibag = MagicLE("ibag"), + IFFID_igen = MagicLE("igen"), + IFFID_shdr = MagicLE("shdr"), +}; + +/////////////////////////////////////////// +// SF2 Generators IDs + +enum SF2Generators : uint16 +{ + SF2_GEN_START_LOOP_FINE = 2, + SF2_GEN_END_LOOP_FINE = 3, + SF2_GEN_MODENVTOFILTERFC = 11, + SF2_GEN_PAN = 17, + SF2_GEN_DECAYMODENV = 28, + SF2_GEN_ATTACKVOLENV = 34, + SF2_GEN_HOLDVOLENV = 34, + SF2_GEN_DECAYVOLENV = 36, + SF2_GEN_SUSTAINVOLENV = 37, + SF2_GEN_RELEASEVOLENV = 38, + SF2_GEN_INSTRUMENT = 41, + SF2_GEN_KEYRANGE = 43, + SF2_GEN_START_LOOP_COARSE = 45, + SF2_GEN_ATTENUATION = 48, + SF2_GEN_END_LOOP_COARSE = 50, + SF2_GEN_COARSETUNE = 51, + SF2_GEN_FINETUNE = 52, + SF2_GEN_SAMPLEID = 53, + SF2_GEN_SAMPLEMODES = 54, + SF2_GEN_SCALE_TUNING = 56, + SF2_GEN_KEYGROUP = 57, + SF2_GEN_UNITYNOTE = 58, +}; + +///////////////////////////////////////////////////////////////////// +// SF2 Structures Definitions + +struct SFPresetHeader +{ + char achPresetName[20]; + uint16le wPreset; + uint16le wBank; + uint16le wPresetBagNdx; + uint32le dwLibrary; + uint32le dwGenre; + uint32le dwMorphology; +}; + +MPT_BINARY_STRUCT(SFPresetHeader, 38) + +struct SFPresetBag +{ + uint16le wGenNdx; + uint16le wModNdx; +}; + +MPT_BINARY_STRUCT(SFPresetBag, 4) + +struct SFGenList +{ + uint16le sfGenOper; + uint16le genAmount; +}; + +MPT_BINARY_STRUCT(SFGenList, 4) + +struct SFInst +{ + char achInstName[20]; + uint16le wInstBagNdx; +}; + +MPT_BINARY_STRUCT(SFInst, 22) + +struct SFInstBag +{ + uint16le wGenNdx; + uint16le wModNdx; +}; + +MPT_BINARY_STRUCT(SFInstBag, 4) + +struct SFInstGenList +{ + uint16le sfGenOper; + uint16le genAmount; +}; + +MPT_BINARY_STRUCT(SFInstGenList, 4) + +struct SFSample +{ + char achSampleName[20]; + uint32le dwStart; + uint32le dwEnd; + uint32le dwStartloop; + uint32le dwEndloop; + uint32le dwSampleRate; + uint8le byOriginalPitch; + int8le chPitchCorrection; + uint16le wSampleLink; + uint16le sfSampleType; +}; + +MPT_BINARY_STRUCT(SFSample, 46) + +// End of structures definitions +///////////////////////////////////////////////////////////////////// + + +struct SF2LoaderInfo +{ + FileReader presetBags; + FileReader presetGens; + FileReader insts; + FileReader instBags; + FileReader instGens; +}; + + +///////////////////////////////////////////////////////////////////// +// Unit conversion + +static uint8 DLSSustainLevelToLinear(int32 sustain) +{ + // 0.1% units + if(sustain >= 0) + { + int32 l = sustain / (1000 * 512); + if(l >= 0 && l <= 128) + return static_cast<uint8>(l); + } + return 128; +} + + +static uint8 SF2SustainLevelToLinear(int32 sustain) +{ + // 0.1% units + int32 l = 128 * (1000 - Clamp(sustain, 0, 1000)) / 1000; + return static_cast<uint8>(l); +} + + +int32 CDLSBank::DLS32BitTimeCentsToMilliseconds(int32 lTimeCents) +{ + // tc = log2(time[secs]) * 1200*65536 + // time[secs] = 2^(tc/(1200*65536)) + if ((uint32)lTimeCents == 0x80000000) return 0; + double fmsecs = 1000.0 * std::pow(2.0, ((double)lTimeCents)/(1200.0*65536.0)); + if (fmsecs < -32767) return -32767; + if (fmsecs > 32767) return 32767; + return (int32)fmsecs; +} + + +// 0dB = 0x10000 +int32 CDLSBank::DLS32BitRelativeGainToLinear(int32 lCentibels) +{ + // v = 10^(cb/(200*65536)) * V + return (int32)(65536.0 * std::pow(10.0, ((double)lCentibels)/(200*65536.0)) ); +} + + +int32 CDLSBank::DLS32BitRelativeLinearToGain(int32 lGain) +{ + // cb = log10(v/V) * 200 * 65536 + if (lGain <= 0) return -960 * 65536; + return (int32)(200 * 65536.0 * std::log10(((double)lGain) / 65536.0)); +} + + +int32 CDLSBank::DLSMidiVolumeToLinear(uint32 nMidiVolume) +{ + return (nMidiVolume * nMidiVolume << 16) / (127*127); +} + + +///////////////////////////////////////////////////////////////////// +// Implementation + +CDLSBank::CDLSBank() +{ + m_nMaxWaveLink = 0; + m_nType = SOUNDBANK_TYPE_INVALID; +} + + +bool CDLSBank::IsDLSBank(const mpt::PathString &filename) +{ + RIFFChunkID riff; + if(filename.empty()) return false; + mpt::ifstream f(filename, std::ios::binary); + if(!f) + { + return false; + } + MemsetZero(riff); + mpt::IO::Read(f, riff); + // Check for embedded DLS sections + if(riff.id_RIFF == IFFID_FORM) + { + // Miles Sound System + do + { + uint32 len = mpt::bit_cast<uint32be>(riff.riff_len); + if (len <= 4) break; + if (riff.id_DLS == IFFID_XDLS) + { + mpt::IO::Read(f, riff); + break; + } + if((len % 2u) != 0) + len++; + if (!mpt::IO::SeekRelative(f, len-4)) break; + } while (mpt::IO::Read(f, riff)); + } else if(riff.id_RIFF == IFFID_RIFF && riff.id_DLS == IFFID_RMID) + { + for (;;) + { + if(!mpt::IO::Read(f, riff)) + break; + if (riff.id_DLS == IFFID_DLS) + break; // found it + int len = riff.riff_len; + if((len % 2u) != 0) + len++; + if ((len <= 4) || !mpt::IO::SeekRelative(f, len-4)) break; + } + } + return ((riff.id_RIFF == IFFID_RIFF) + && ((riff.id_DLS == IFFID_DLS) || (riff.id_DLS == IFFID_MLS) || (riff.id_DLS == IFFID_sfbk)) + && (riff.riff_len >= 256)); +} + + +/////////////////////////////////////////////////////////////// +// Find an instrument based on the given parameters + +const DLSINSTRUMENT *CDLSBank::FindInstrument(bool isDrum, uint32 bank, uint32 program, uint32 key, uint32 *pInsNo) const +{ + // This helps finding the "more correct" instrument if we search for an instrument in any bank, and the higher-bank instruments appear first in the file + // Fixes issues when loading GeneralUser GS into OpenMPT's MIDI library. + std::vector<std::reference_wrapper<const DLSINSTRUMENT>> sortedInstr{m_Instruments.begin(), m_Instruments.end()}; + if(bank >= 0x4000 || program >= 0x80) + { + std::sort(sortedInstr.begin(), sortedInstr.end(), [](const DLSINSTRUMENT &l, const DLSINSTRUMENT &r) + { return std::tie(l.ulBank, l.ulInstrument) < std::tie(r.ulBank, r.ulInstrument); }); + } + + for(const DLSINSTRUMENT &dlsIns : sortedInstr) + { + uint32 insbank = ((dlsIns.ulBank & 0x7F00) >> 1) | (dlsIns.ulBank & 0x7F); + if((bank >= 0x4000) || (insbank == bank)) + { + if(isDrum && (dlsIns.ulBank & F_INSTRUMENT_DRUMS)) + { + if((program >= 0x80) || (program == (dlsIns.ulInstrument & 0x7F))) + { + for(const auto ®ion : dlsIns.Regions) + { + if(region.IsDummy()) + continue; + + if((!key || key >= 0x80) + || (key >= region.uKeyMin && key <= region.uKeyMax)) + { + if(pInsNo) + *pInsNo = static_cast<uint32>(std::distance(m_Instruments.data(), &dlsIns)); + // cppcheck false-positive + // cppcheck-suppress returnDanglingLifetime + return &dlsIns; + } + } + } + } else if(!isDrum && !(dlsIns.ulBank & F_INSTRUMENT_DRUMS)) + { + if((program >= 0x80) || (program == (dlsIns.ulInstrument & 0x7F))) + { + if(pInsNo) + *pInsNo = static_cast<uint32>(std::distance(m_Instruments.data(), &dlsIns)); + // cppcheck false-positive + // cppcheck-suppress returnDanglingLifetime + return &dlsIns; + } + } + } + } + + return nullptr; +} + + +bool CDLSBank::FindAndExtract(CSoundFile &sndFile, const INSTRUMENTINDEX ins, const bool isDrum) const +{ + ModInstrument *pIns = sndFile.Instruments[ins]; + if(pIns == nullptr) + return false; + + uint32 dlsIns = 0, drumRgn = 0; + const uint32 program = (pIns->nMidiProgram != 0) ? pIns->nMidiProgram - 1 : 0; + const uint32 key = isDrum ? (pIns->nMidiDrumKey & 0x7F) : 0xFF; + if(FindInstrument(isDrum, (pIns->wMidiBank - 1) & 0x3FFF, program, key, &dlsIns) + || FindInstrument(isDrum, (pIns->wMidiBank - 1) & 0x3F80, program, key, &dlsIns) + || FindInstrument(isDrum, 0xFFFF, isDrum ? 0xFF : program, key, &dlsIns)) + { + if(key < 0x80) drumRgn = GetRegionFromKey(dlsIns, key); + if(ExtractInstrument(sndFile, ins, dlsIns, drumRgn)) + { + pIns = sndFile.Instruments[ins]; // Reset pointer because ExtractInstrument may delete the previous value. + if((key >= 24) && (key < 24 + std::size(szMidiPercussionNames))) + { + pIns->name = szMidiPercussionNames[key - 24]; + } + return true; + } + } + return false; +} + + +/////////////////////////////////////////////////////////////// +// Update DLS instrument definition from an IFF chunk + +bool CDLSBank::UpdateInstrumentDefinition(DLSINSTRUMENT *pDlsIns, FileReader chunk) +{ + IFFCHUNK header; + chunk.ReadStruct(header); + if(!header.len || !chunk.CanRead(header.len)) + return false; + if(header.id == IFFID_LIST) + { + uint32 listid = chunk.ReadUint32LE(); + while(chunk.CanRead(sizeof(IFFCHUNK))) + { + IFFCHUNK subHeader; + chunk.ReadStruct(subHeader); + chunk.SkipBack(sizeof(IFFCHUNK)); + FileReader subData = chunk.ReadChunk(subHeader.len + sizeof(IFFCHUNK)); + if(subHeader.len & 1) + { + chunk.Skip(1); + } + UpdateInstrumentDefinition(pDlsIns, subData); + } + switch(listid) + { + case IFFID_rgn: // Level 1 region + case IFFID_rgn2: // Level 2 region + pDlsIns->Regions.push_back({}); + break; + } + } else + { + switch(header.id) + { + case IFFID_insh: + { + INSHChunk insh; + chunk.ReadStruct(insh); + pDlsIns->ulBank = insh.ulBank; + pDlsIns->ulInstrument = insh.ulInstrument; + //Log("%3d regions, bank 0x%04X instrument %3d\n", insh.cRegions, pDlsIns->ulBank, pDlsIns->ulInstrument); + break; + } + + case IFFID_rgnh: + if(!pDlsIns->Regions.empty()) + { + RGNHChunk rgnh; + chunk.ReadStruct(rgnh); + DLSREGION ®ion = pDlsIns->Regions.back(); + region.uKeyMin = (uint8)rgnh.RangeKey.usLow; + region.uKeyMax = (uint8)rgnh.RangeKey.usHigh; + region.fuOptions = (uint8)(rgnh.usKeyGroup & DLSREGION_KEYGROUPMASK); + if(rgnh.fusOptions & F_RGN_OPTION_SELFNONEXCLUSIVE) + region.fuOptions |= DLSREGION_SELFNONEXCLUSIVE; + //Log(" Region %d: fusOptions=0x%02X usKeyGroup=0x%04X ", pDlsIns->nRegions, rgnh.fusOptions, rgnh.usKeyGroup); + //Log("KeyRange[%3d,%3d] ", rgnh.RangeKey.usLow, rgnh.RangeKey.usHigh); + } + break; + + case IFFID_wlnk: + if (!pDlsIns->Regions.empty()) + { + WLNKChunk wlnk; + chunk.ReadStruct(wlnk); + DLSREGION ®ion = pDlsIns->Regions.back(); + region.nWaveLink = (uint16)wlnk.ulTableIndex; + if((region.nWaveLink < Util::MaxValueOfType(region.nWaveLink)) && (region.nWaveLink >= m_nMaxWaveLink)) + m_nMaxWaveLink = region.nWaveLink + 1; + //Log(" WaveLink %d: fusOptions=0x%02X usPhaseGroup=0x%04X ", pDlsIns->nRegions, wlnk.fusOptions, wlnk.usPhaseGroup); + //Log("ulChannel=%d ulTableIndex=%4d\n", wlnk.ulChannel, wlnk.ulTableIndex); + } + break; + + case IFFID_wsmp: + if(!pDlsIns->Regions.empty()) + { + DLSREGION ®ion = pDlsIns->Regions.back(); + WSMPChunk wsmp; + chunk.ReadStruct(wsmp); + region.fuOptions |= DLSREGION_OVERRIDEWSMP; + region.uUnityNote = (uint8)wsmp.usUnityNote; + region.sFineTune = wsmp.sFineTune; + int32 lVolume = DLS32BitRelativeGainToLinear(wsmp.lAttenuation) / 256; + if (lVolume > 256) lVolume = 256; + if (lVolume < 4) lVolume = 4; + region.usVolume = (uint16)lVolume; + //Log(" WaveSample %d: usUnityNote=%2d sFineTune=%3d ", pDlsEnv->nRegions, p->usUnityNote, p->sFineTune); + //Log("fulOptions=0x%04X loops=%d\n", p->fulOptions, p->cSampleLoops); + if((wsmp.cSampleLoops) && (wsmp.cbSize + sizeof(WSMPSampleLoop) <= header.len)) + { + WSMPSampleLoop loop; + chunk.Seek(sizeof(IFFCHUNK) + wsmp.cbSize); + chunk.ReadStruct(loop); + //Log("looptype=%2d loopstart=%5d loopend=%5d\n", ploop->ulLoopType, ploop->ulLoopStart, ploop->ulLoopLength); + if(loop.ulLoopLength > 3) + { + region.fuOptions |= DLSREGION_SAMPLELOOP; + //if(loop.ulLoopType) region.fuOptions |= DLSREGION_PINGPONGLOOP; + region.ulLoopStart = loop.ulLoopStart; + region.ulLoopEnd = loop.ulLoopStart + loop.ulLoopLength; + } + } + } + break; + + case IFFID_art1: + case IFFID_art2: + { + ART1Chunk art1; + chunk.ReadStruct(art1); + if(!(pDlsIns->ulBank & F_INSTRUMENT_DRUMS)) + { + pDlsIns->nMelodicEnv = static_cast<uint32>(m_Envelopes.size() + 1); + } + if(art1.cbSize + art1.cConnectionBlocks * sizeof(ConnectionBlock) > header.len) + break; + DLSENVELOPE dlsEnv; + MemsetZero(dlsEnv); + dlsEnv.nDefPan = 128; + dlsEnv.nVolSustainLevel = 128; + //Log(" art1 (%3d bytes): cbSize=%d cConnectionBlocks=%d\n", p->len, p->cbSize, p->cConnectionBlocks); + chunk.Seek(sizeof(IFFCHUNK) + art1.cbSize); + for (uint32 iblk = 0; iblk < art1.cConnectionBlocks; iblk++) + { + ConnectionBlock blk; + chunk.ReadStruct(blk); + // [4-bit transform][12-bit dest][8-bit control][8-bit source] = 32-bit ID + uint32 dwArticulation = blk.usTransform; + dwArticulation = (dwArticulation << 12) | (blk.usDestination & 0x0FFF); + dwArticulation = (dwArticulation << 8) | (blk.usControl & 0x00FF); + dwArticulation = (dwArticulation << 8) | (blk.usSource & 0x00FF); + switch(dwArticulation) + { + case ART_DEFAULTPAN: + { + int32 pan = 128 + blk.lScale / (65536000/128); + dlsEnv.nDefPan = mpt::saturate_cast<uint8>(pan); + } + break; + + case ART_VOL_EG_ATTACKTIME: + // 32-bit time cents units. range = [0s, 20s] + dlsEnv.wVolAttack = 0; + if(blk.lScale > -0x40000000) + { + int32 l = blk.lScale - 78743200; // maximum velocity + if (l > 0) l = 0; + int32 attacktime = DLS32BitTimeCentsToMilliseconds(l); + if (attacktime < 0) attacktime = 0; + if (attacktime > 20000) attacktime = 20000; + if (attacktime >= 20) dlsEnv.wVolAttack = (uint16)(attacktime / 20); + //Log("%3d: Envelope Attack Time set to %d (%d time cents)\n", (uint32)(dlsEnv.ulInstrument & 0x7F)|((dlsEnv.ulBank >> 16) & 0x8000), attacktime, pblk->lScale); + } + break; + + case ART_VOL_EG_DECAYTIME: + // 32-bit time cents units. range = [0s, 20s] + dlsEnv.wVolDecay = 0; + if(blk.lScale > -0x40000000) + { + int32 decaytime = DLS32BitTimeCentsToMilliseconds(blk.lScale); + if (decaytime > 20000) decaytime = 20000; + if (decaytime >= 20) dlsEnv.wVolDecay = (uint16)(decaytime / 20); + //Log("%3d: Envelope Decay Time set to %d (%d time cents)\n", (uint32)(dlsEnv.ulInstrument & 0x7F)|((dlsEnv.ulBank >> 16) & 0x8000), decaytime, pblk->lScale); + } + break; + + case ART_VOL_EG_RELEASETIME: + // 32-bit time cents units. range = [0s, 20s] + dlsEnv.wVolRelease = 0; + if(blk.lScale > -0x40000000) + { + int32 releasetime = DLS32BitTimeCentsToMilliseconds(blk.lScale); + if (releasetime > 20000) releasetime = 20000; + if (releasetime >= 20) dlsEnv.wVolRelease = (uint16)(releasetime / 20); + //Log("%3d: Envelope Release Time set to %d (%d time cents)\n", (uint32)(dlsEnv.ulInstrument & 0x7F)|((dlsEnv.ulBank >> 16) & 0x8000), dlsEnv.wVolRelease, pblk->lScale); + } + break; + + case ART_VOL_EG_SUSTAINLEVEL: + // 0.1% units + if(blk.lScale >= 0) + { + dlsEnv.nVolSustainLevel = DLSSustainLevelToLinear(blk.lScale); + } + break; + + //default: + // Log(" Articulation = 0x%08X value=%d\n", dwArticulation, blk.lScale); + } + } + m_Envelopes.push_back(dlsEnv); + } + break; + + case IFFID_INAM: + chunk.ReadString<mpt::String::spacePadded>(pDlsIns->szName, header.len); + break; + #if 0 + default: + { + char sid[5]; + memcpy(sid, &header.id, 4); + sid[4] = 0; + Log(" \"%s\": %d bytes\n", (uint32)sid, header.len.get()); + } + #endif + } + } + return true; +} + +/////////////////////////////////////////////////////////////// +// Converts SF2 chunks to DLS + +bool CDLSBank::UpdateSF2PresetData(SF2LoaderInfo &sf2info, const IFFCHUNK &header, FileReader &chunk) +{ + if (!chunk.IsValid()) return false; + switch(header.id) + { + case IFFID_phdr: + if(m_Instruments.empty()) + { + uint32 numIns = static_cast<uint32>(chunk.GetLength() / sizeof(SFPresetHeader)); + if(numIns <= 1) + break; + // The terminal sfPresetHeader record should never be accessed, and exists only to provide a terminal wPresetBagNdx with which to determine the number of zones in the last preset. + numIns--; + m_Instruments.resize(numIns); + + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBank", MPT_UFORMAT("phdr: {} instruments")(m_Instruments.size())); + #endif + SFPresetHeader psfh; + chunk.ReadStruct(psfh); + for(auto &dlsIns : m_Instruments) + { + mpt::String::WriteAutoBuf(dlsIns.szName) = mpt::String::ReadAutoBuf(psfh.achPresetName); + dlsIns.ulInstrument = psfh.wPreset & 0x7F; + dlsIns.ulBank = (psfh.wBank >= 128) ? F_INSTRUMENT_DRUMS : (psfh.wBank << 8); + dlsIns.wPresetBagNdx = psfh.wPresetBagNdx; + dlsIns.wPresetBagNum = 1; + chunk.ReadStruct(psfh); + if (psfh.wPresetBagNdx > dlsIns.wPresetBagNdx) dlsIns.wPresetBagNum = static_cast<uint16>(psfh.wPresetBagNdx - dlsIns.wPresetBagNdx); + } + } + break; + + case IFFID_pbag: + if(!m_Instruments.empty() && chunk.CanRead(sizeof(SFPresetBag))) + { + sf2info.presetBags = chunk.GetChunk(chunk.BytesLeft()); + } + #ifdef DLSINSTR_LOG + else MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", U_("pbag: no instruments!")); + #endif + break; + + case IFFID_pgen: + if(!m_Instruments.empty() && chunk.CanRead(sizeof(SFGenList))) + { + sf2info.presetGens = chunk.GetChunk(chunk.BytesLeft()); + } + #ifdef DLSINSTR_LOG + else MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", U_("pgen: no instruments!")); + #endif + break; + + case IFFID_inst: + if(!m_Instruments.empty() && chunk.CanRead(sizeof(SFInst))) + { + sf2info.insts = chunk.GetChunk(chunk.BytesLeft()); + } + break; + + case IFFID_ibag: + if(!m_Instruments.empty() && chunk.CanRead(sizeof(SFInstBag))) + { + sf2info.instBags = chunk.GetChunk(chunk.BytesLeft()); + } + break; + + case IFFID_igen: + if(!m_Instruments.empty() && chunk.CanRead(sizeof(SFInstGenList))) + { + sf2info.instGens = chunk.GetChunk(chunk.BytesLeft()); + } + break; + + case IFFID_shdr: + if (m_SamplesEx.empty()) + { + uint32 numSmp = static_cast<uint32>(chunk.GetLength() / sizeof(SFSample)); + if (numSmp < 1) break; + m_SamplesEx.resize(numSmp); + m_WaveForms.resize(numSmp); + #ifdef DLSINSTR_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT("shdr: {} samples")(m_SamplesEx.size())); + #endif + + for (uint32 i = 0; i < numSmp; i++) + { + SFSample p; + chunk.ReadStruct(p); + DLSSAMPLEEX &dlsSmp = m_SamplesEx[i]; + mpt::String::WriteAutoBuf(dlsSmp.szName) = mpt::String::ReadAutoBuf(p.achSampleName); + dlsSmp.dwLen = 0; + dlsSmp.dwSampleRate = p.dwSampleRate; + dlsSmp.byOriginalPitch = p.byOriginalPitch; + dlsSmp.chPitchCorrection = static_cast<int8>(Util::muldivr(p.chPitchCorrection, 128, 100)); + // cognitone's sf2convert tool doesn't set the correct sample flags (0x01 / 0x02 instead of 0x10/ 0x20). + // For SF3, we ignore this and go by https://github.com/FluidSynth/fluidsynth/wiki/SoundFont3Format instead + // As cognitone's tool is the only tool writing SF4 files, we always assume compressed samples with SF4 files if bits 0/1 are set. + uint16 sampleType = p.sfSampleType; + if(m_sf2version >= 0x4'0000 && m_sf2version <= 0x4'FFFF && (sampleType & 0x03)) + sampleType = (sampleType & 0xFFFC) | 0x10; + + dlsSmp.compressed = (sampleType & 0x10); + if(((sampleType & 0x7FCF) <= 4) && (p.dwEnd >= p.dwStart + 4)) + { + m_WaveForms[i] = p.dwStart; + dlsSmp.dwLen = (p.dwEnd - p.dwStart); + if(!dlsSmp.compressed) + { + m_WaveForms[i] *= 2; + dlsSmp.dwLen *= 2; + if((p.dwEndloop > p.dwStartloop + 7) && (p.dwStartloop >= p.dwStart)) + { + dlsSmp.dwStartloop = p.dwStartloop - p.dwStart; + dlsSmp.dwEndloop = p.dwEndloop - p.dwStart; + } + } else + { + if(p.dwEndloop > p.dwStartloop + 7) + { + dlsSmp.dwStartloop = p.dwStartloop; + dlsSmp.dwEndloop = p.dwEndloop; + } + } + } + } + } + break; + + #ifdef DLSINSTR_LOG + default: + { + char sdbg[5]; + memcpy(sdbg, &header.id, 4); + sdbg[4] = 0; + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT("Unsupported SF2 chunk: {} ({} bytes)")(mpt::ToUnicode(mpt::Charset::ASCII, mpt::String::ReadAutoBuf(sdbg)), header.len.get())); + } + #endif + } + return true; +} + + +static int16 SF2TimeToDLS(int16 amount) +{ + int32 time = CDLSBank::DLS32BitTimeCentsToMilliseconds(static_cast<int32>(amount) << 16); + return static_cast<int16>(Clamp(time, 20, 20000) / 20); +} + + +// Convert all instruments to the DLS format +bool CDLSBank::ConvertSF2ToDLS(SF2LoaderInfo &sf2info) +{ + if (m_Instruments.empty() || m_SamplesEx.empty()) + return false; + + const uint32 numInsts = static_cast<uint32>(sf2info.insts.GetLength() / sizeof(SFInst)); + const uint32 numInstBags = static_cast<uint32>(sf2info.instBags.GetLength() / sizeof(SFInstBag)); + + std::vector<std::pair<uint16, uint16>> instruments; // instrument, key range + std::vector<SFGenList> generators; + std::vector<SFInstGenList> instrGenerators; + for(auto &dlsIns : m_Instruments) + { + instruments.clear(); + DLSENVELOPE dlsEnv; + int32 instrAttenuation = 0; + int16 instrFinetune = 0; + // Default Envelope Values + dlsEnv.wVolAttack = 0; + dlsEnv.wVolDecay = 0; + dlsEnv.wVolRelease = 0; + dlsEnv.nVolSustainLevel = 128; + dlsEnv.nDefPan = 128; + // Load Preset Bags + sf2info.presetBags.Seek(dlsIns.wPresetBagNdx * sizeof(SFPresetBag)); + for(uint32 ipbagcnt = 0; ipbagcnt < dlsIns.wPresetBagNum; ipbagcnt++) + { + // Load generators for each preset bag + SFPresetBag bag[2]; + if(!sf2info.presetBags.ReadArray(bag)) + break; + sf2info.presetBags.SkipBack(sizeof(SFPresetBag)); + + sf2info.presetGens.Seek(bag[0].wGenNdx * sizeof(SFGenList)); + uint16 keyRange = 0xFFFF; + if(!sf2info.presetGens.ReadVector(generators, bag[1].wGenNdx - bag[0].wGenNdx)) + continue; + for(const auto &gen : generators) + { + const int16 value = static_cast<int16>(gen.genAmount); + switch(gen.sfGenOper) + { + case SF2_GEN_ATTACKVOLENV: + dlsEnv.wVolAttack = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_DECAYVOLENV: + dlsEnv.wVolDecay = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_SUSTAINVOLENV: + // 0.1% units + if(gen.genAmount >= 0) + { + dlsEnv.nVolSustainLevel = SF2SustainLevelToLinear(gen.genAmount); + } + break; + case SF2_GEN_RELEASEVOLENV: + dlsEnv.wVolRelease = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_INSTRUMENT: + if(const auto instr = std::make_pair(gen.genAmount.get(), keyRange); !mpt::contains(instruments, instr)) + instruments.push_back(instr); + keyRange = 0xFFFF; + break; + case SF2_GEN_KEYRANGE: + keyRange = gen.genAmount; + break; + case SF2_GEN_ATTENUATION: + instrAttenuation = -value; + break; + case SF2_GEN_COARSETUNE: + instrFinetune += value * 128; + break; + case SF2_GEN_FINETUNE: + instrFinetune += static_cast<int16>(Util::muldiv(static_cast<int8>(value), 128, 100)); + break; +#if 0 + default: + Log("Ins %3d: bag %3d gen %3d: ", nIns, ipbagndx, ipgenndx); + Log("genoper=%d amount=0x%04X ", gen.sfGenOper, gen.genAmount); + Log((pSmp->ulBank & F_INSTRUMENT_DRUMS) ? "(drum)\n" : "\n"); +#endif + } + } + } + // Envelope + if (!(dlsIns.ulBank & F_INSTRUMENT_DRUMS)) + { + m_Envelopes.push_back(dlsEnv); + dlsIns.nMelodicEnv = static_cast<uint32>(m_Envelopes.size()); + } + // Load Instrument Bags + dlsIns.Regions.clear(); + for(const auto & [nInstrNdx, keyRange] : instruments) + { + if(nInstrNdx >= numInsts) + continue; + sf2info.insts.Seek(nInstrNdx * sizeof(SFInst)); + SFInst insts[2]; + sf2info.insts.ReadArray(insts); + const uint32 numRegions = insts[1].wInstBagNdx - insts[0].wInstBagNdx; + dlsIns.Regions.reserve(dlsIns.Regions.size() + numRegions); + //Log("\nIns %3d, %2d regions:\n", nIns, pSmp->nRegions); + DLSREGION globalZone{}; + globalZone.uUnityNote = 0xFF; // 0xFF means undefined -> use sample root note + globalZone.tuning = 100; + globalZone.sFineTune = instrFinetune; + globalZone.nWaveLink = Util::MaxValueOfType(globalZone.nWaveLink); + if(keyRange != 0xFFFF) + { + globalZone.uKeyMin = static_cast<uint8>(keyRange & 0xFF); + globalZone.uKeyMax = static_cast<uint8>(keyRange >> 8); + if(globalZone.uKeyMin > globalZone.uKeyMax) + std::swap(globalZone.uKeyMin, globalZone.uKeyMax); + } else + { + globalZone.uKeyMin = 0; + globalZone.uKeyMax = 127; + } + for(uint32 nRgn = 0; nRgn < numRegions; nRgn++) + { + uint32 ibagcnt = insts[0].wInstBagNdx + nRgn; + if(ibagcnt >= numInstBags) + break; + // Create a new envelope for drums + DLSENVELOPE *pDlsEnv = &dlsEnv; + if(!(dlsIns.ulBank & F_INSTRUMENT_DRUMS) && dlsIns.nMelodicEnv > 0 && dlsIns.nMelodicEnv <= m_Envelopes.size()) + { + pDlsEnv = &m_Envelopes[dlsIns.nMelodicEnv - 1]; + } + + DLSREGION rgn = globalZone; + + // Region Default Values + int32 regionAttn = 0; + // Load Generators + sf2info.instBags.Seek(ibagcnt * sizeof(SFInstBag)); + SFInstBag bags[2]; + sf2info.instBags.ReadArray(bags); + sf2info.instGens.Seek(bags[0].wGenNdx * sizeof(SFInstGenList)); + uint16 lastOp = SF2_GEN_SAMPLEID; + int32 loopStart = 0, loopEnd = 0; + if(!sf2info.instGens.ReadVector(instrGenerators, bags[1].wGenNdx - bags[0].wGenNdx)) + break; + for(const auto &gen : instrGenerators) + { + uint16 value = gen.genAmount; + lastOp = gen.sfGenOper; + + switch(gen.sfGenOper) + { + case SF2_GEN_KEYRANGE: + { + uint8 keyMin = static_cast<uint8>(value & 0xFF); + uint8 keyMax = static_cast<uint8>(value >> 8); + if(keyMin > keyMax) + std::swap(keyMin, keyMax); + rgn.uKeyMin = std::max(rgn.uKeyMin, keyMin); + rgn.uKeyMax = std::min(rgn.uKeyMax, keyMax); + // There was no overlap between instrument region and preset region - skip it + if(rgn.uKeyMin > rgn.uKeyMax) + rgn.uKeyMin = rgn.uKeyMax = 0xFF; + } + break; + case SF2_GEN_UNITYNOTE: + if (value < 128) rgn.uUnityNote = static_cast<uint8>(value); + break; + case SF2_GEN_ATTACKVOLENV: + pDlsEnv->wVolAttack = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_DECAYVOLENV: + pDlsEnv->wVolDecay = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_SUSTAINVOLENV: + // 0.1% units + if(gen.genAmount >= 0) + { + pDlsEnv->nVolSustainLevel = SF2SustainLevelToLinear(gen.genAmount); + } + break; + case SF2_GEN_RELEASEVOLENV: + pDlsEnv->wVolRelease = SF2TimeToDLS(gen.genAmount); + break; + case SF2_GEN_PAN: + { + int32 pan = static_cast<int16>(value); + pan = std::clamp(Util::muldivr(pan + 500, 256, 1000), 0, 256); + rgn.panning = static_cast<int16>(pan); + pDlsEnv->nDefPan = mpt::saturate_cast<uint8>(pan); + } + break; + case SF2_GEN_ATTENUATION: + regionAttn = -static_cast<int16>(value); + break; + case SF2_GEN_SAMPLEID: + if (value < m_SamplesEx.size()) + { + rgn.nWaveLink = value; + rgn.ulLoopStart = mpt::saturate_cast<uint32>(m_SamplesEx[value].dwStartloop + loopStart); + rgn.ulLoopEnd = mpt::saturate_cast<uint32>(m_SamplesEx[value].dwEndloop + loopEnd); + } + break; + case SF2_GEN_SAMPLEMODES: + value &= 3; + rgn.fuOptions &= uint16(~(DLSREGION_SAMPLELOOP|DLSREGION_PINGPONGLOOP|DLSREGION_SUSTAINLOOP)); + if(value == 1) + rgn.fuOptions |= DLSREGION_SAMPLELOOP; + else if(value == 2) + rgn.fuOptions |= DLSREGION_SAMPLELOOP | DLSREGION_PINGPONGLOOP; + else if(value == 3) + rgn.fuOptions |= DLSREGION_SAMPLELOOP | DLSREGION_SUSTAINLOOP; + rgn.fuOptions |= DLSREGION_OVERRIDEWSMP; + break; + case SF2_GEN_KEYGROUP: + rgn.fuOptions |= (value & DLSREGION_KEYGROUPMASK); + break; + case SF2_GEN_COARSETUNE: + rgn.sFineTune += static_cast<int16>(value) * 128; + break; + case SF2_GEN_FINETUNE: + rgn.sFineTune += static_cast<int16>(Util::muldiv(static_cast<int8>(value), 128, 100)); + break; + case SF2_GEN_SCALE_TUNING: + rgn.tuning = mpt::saturate_cast<uint8>(value); + break; + case SF2_GEN_START_LOOP_FINE: + loopStart += static_cast<int16>(value); + break; + case SF2_GEN_END_LOOP_FINE: + loopEnd += static_cast<int16>(value); + break; + case SF2_GEN_START_LOOP_COARSE: + loopStart += static_cast<int16>(value) * 32768; + break; + case SF2_GEN_END_LOOP_COARSE: + loopEnd += static_cast<int16>(value) * 32768; + break; + //default: + // Log(" gen=%d value=%04X\n", pgen->sfGenOper, pgen->genAmount); + } + } + int32 linearVol = DLS32BitRelativeGainToLinear(((instrAttenuation + regionAttn) * 65536) / 10) / 256; + Limit(linearVol, 16, 256); + rgn.usVolume = static_cast<uint16>(linearVol); + + if(lastOp != SF2_GEN_SAMPLEID && nRgn == 0) + globalZone = rgn; + else if(!rgn.IsDummy()) + dlsIns.Regions.push_back(rgn); + //Log("\n"); + } + } + } + return true; +} + + +/////////////////////////////////////////////////////////////// +// Open: opens a DLS bank + +bool CDLSBank::Open(const mpt::PathString &filename) +{ + if(filename.empty()) return false; + m_szFileName = filename; + InputFile f(filename, SettingCacheCompleteFileBeforeLoading()); + if(!f.IsValid()) return false; + return Open(GetFileReader(f)); +} + + +bool CDLSBank::Open(FileReader file) +{ + uint32 nInsDef; + + if(file.GetOptionalFileName()) + m_szFileName = file.GetOptionalFileName().value(); + + file.Rewind(); + size_t dwMemLength = file.GetLength(); + size_t dwMemPos = 0; + if(!file.CanRead(256)) + { + return false; + } + + RIFFChunkID riff; + file.ReadStruct(riff); + // Check DLS sections embedded in RMI midi files + if(riff.id_RIFF == IFFID_RIFF && riff.id_DLS == IFFID_RMID) + { + while(file.ReadStruct(riff)) + { + if(riff.id_RIFF == IFFID_RIFF && riff.id_DLS == IFFID_DLS) + { + file.SkipBack(sizeof(riff)); + break; + } + uint32 len = riff.riff_len; + if((len % 2u) != 0) + len++; + file.SkipBack(4); + file.Skip(len); + } + } + + // Check XDLS sections embedded in big endian IFF files (Miles Sound System) + if (riff.id_RIFF == IFFID_FORM) + { + do + { + if(riff.id_DLS == IFFID_XDLS) + { + file.ReadStruct(riff); + break; + } + uint32 len = mpt::bit_cast<uint32be>(riff.riff_len); + if((len % 2u) != 0) + len++; + file.SkipBack(4); + file.Skip(len); + } while(file.ReadStruct(riff)); + } + if (riff.id_RIFF != IFFID_RIFF + || (riff.id_DLS != IFFID_DLS && riff.id_DLS != IFFID_MLS && riff.id_DLS != IFFID_sfbk) + || !file.CanRead(riff.riff_len - 4)) + { + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", U_("Invalid DLS bank!")); + #endif + return false; + } + SF2LoaderInfo sf2info; + m_nType = (riff.id_DLS == IFFID_sfbk) ? SOUNDBANK_TYPE_SF2 : SOUNDBANK_TYPE_DLS; + m_dwWavePoolOffset = 0; + m_sf2version = 0; + m_Instruments.clear(); + m_WaveForms.clear(); + m_Envelopes.clear(); + nInsDef = 0; + if (dwMemLength > 8 + riff.riff_len + dwMemPos) dwMemLength = 8 + riff.riff_len + dwMemPos; + bool applyPaddingToSampleChunk = true; + while(file.CanRead(sizeof(IFFCHUNK))) + { + IFFCHUNK chunkHeader; + file.ReadStruct(chunkHeader); + dwMemPos = file.GetPosition(); + FileReader chunk = file.ReadChunk(chunkHeader.len); + + bool applyPadding = (chunkHeader.len % 2u) != 0; + + if(!chunk.LengthIsAtLeast(chunkHeader.len)) + break; + + switch(chunkHeader.id) + { + // DLS 1.0: Instruments Collection Header + case IFFID_colh: + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("colh ({} bytes)")(chunkHeader.len.get())); + #endif + if (m_Instruments.empty()) + { + m_Instruments.resize(chunk.ReadUint32LE()); + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT(" {} instruments")(m_Instruments.size())); + #endif + } + break; + + // DLS 1.0: Instruments Pointers Table + case IFFID_ptbl: + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("ptbl ({} bytes)")(chunkHeader.len.get())); + #endif + if (m_WaveForms.empty()) + { + PTBLChunk ptbl; + chunk.ReadStruct(ptbl); + chunk.Skip(ptbl.cbSize - 8); + uint32 cues = std::min(ptbl.cCues.get(), mpt::saturate_cast<uint32>(chunk.BytesLeft() / sizeof(uint32))); + m_WaveForms.reserve(cues); + for(uint32 i = 0; i < cues; i++) + { + m_WaveForms.push_back(chunk.ReadUint32LE()); + } + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT(" {} waveforms")(m_WaveForms.size())); + #endif + } + break; + + // DLS 1.0: LIST section + case IFFID_LIST: + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", U_("LIST")); + #endif + { + uint32 listid = chunk.ReadUint32LE(); + if (((listid == IFFID_wvpl) && (m_nType & SOUNDBANK_TYPE_DLS)) + || ((listid == IFFID_sdta) && (m_nType & SOUNDBANK_TYPE_SF2))) + { + m_dwWavePoolOffset = dwMemPos + 4; + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("Wave Pool offset: {}")(m_dwWavePoolOffset)); + #endif + if(!applyPaddingToSampleChunk) + applyPadding = false; + break; + } + + while (chunk.CanRead(12)) + { + IFFCHUNK listHeader; + chunk.ReadStruct(listHeader); + + if(!chunk.CanRead(listHeader.len)) + break; + + FileReader subData = chunk.GetChunkAt(chunk.GetPosition() - sizeof(IFFCHUNK), listHeader.len + 8); + FileReader listChunk = chunk.ReadChunk(listHeader.len); + if(listHeader.len % 2u) + chunk.Skip(1); + // DLS Instrument Headers + if (listHeader.id == IFFID_LIST && (m_nType & SOUNDBANK_TYPE_DLS)) + { + uint32 subID = listChunk.ReadUint32LE(); + if ((subID == IFFID_ins) && (nInsDef < m_Instruments.size())) + { + DLSINSTRUMENT &dlsIns = m_Instruments[nInsDef]; + //Log("Instrument %d:\n", nInsDef); + dlsIns.Regions.push_back({}); + UpdateInstrumentDefinition(&dlsIns, subData); + nInsDef++; + } + } else + // DLS/SF2 Bank Information + if (listid == IFFID_INFO && listHeader.len) + { + switch(listHeader.id) + { + case IFFID_ifil: + m_sf2version = listChunk.ReadUint16LE() << 16; + m_sf2version |= listChunk.ReadUint16LE(); + if(m_sf2version >= 0x3'0000 && m_sf2version <= 0x4'FFFF) + { + // "SF3" / "SF4" with compressed samples. The padding of the sample chunk is now optional (probably because it was simply forgotten to be added) + applyPaddingToSampleChunk = false; + } + listChunk.Skip(2); + break; + case IFFID_INAM: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szBankName, listChunk.BytesLeft()); + break; + case IFFID_IENG: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szEngineer, listChunk.BytesLeft()); + break; + case IFFID_ICOP: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szCopyRight, listChunk.BytesLeft()); + break; + case IFFID_ICMT: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szComments, listChunk.BytesLeft()); + break; + case IFFID_ISFT: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szSoftware, listChunk.BytesLeft()); + break; + case IFFID_ISBJ: + listChunk.ReadString<mpt::String::maybeNullTerminated>(m_BankInfo.szDescription, listChunk.BytesLeft()); + break; + } + } else + if ((listid == IFFID_pdta) && (m_nType & SOUNDBANK_TYPE_SF2)) + { + UpdateSF2PresetData(sf2info, listHeader, listChunk); + } + } + } + break; + + #ifdef DLSBANK_LOG + default: + { + char sdbg[5]; + memcpy(sdbg, &chunkHeader.id, 4); + sdbg[4] = 0; + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("Unsupported chunk: {} ({} bytes)")(mpt::ToUnicode(mpt::Charset::ASCII, mpt::String::ReadAutoBuf(sdbg)), chunkHeader.len.get())); + } + break; + #endif + } + + if(applyPadding) + file.Skip(1); + } + // Build the ptbl is not present in file + if ((m_WaveForms.empty()) && (m_dwWavePoolOffset) && (m_nType & SOUNDBANK_TYPE_DLS) && (m_nMaxWaveLink > 0)) + { + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("ptbl not present: building table ({} wavelinks)...")(m_nMaxWaveLink)); + #endif + m_WaveForms.reserve(m_nMaxWaveLink); + file.Seek(m_dwWavePoolOffset); + while(m_WaveForms.size() < m_nMaxWaveLink && file.CanRead(sizeof(IFFCHUNK))) + { + IFFCHUNK chunk; + file.ReadStruct(chunk); + if (chunk.id == IFFID_LIST) + m_WaveForms.push_back(file.GetPosition() - m_dwWavePoolOffset - sizeof(IFFCHUNK)); + file.Skip(chunk.len); + } +#ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("Found {} waveforms")(m_WaveForms.size())); +#endif + } + // Convert the SF2 data to DLS + if ((m_nType & SOUNDBANK_TYPE_SF2) && !m_SamplesEx.empty() && !m_Instruments.empty()) + { + ConvertSF2ToDLS(sf2info); + } +#ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", U_("DLS bank closed")); +#endif + return true; +} + +//////////////////////////////////////////////////////////////////////////////////////// +// Extracts the Waveforms from a DLS/SF2 bank + +uint32 CDLSBank::GetRegionFromKey(uint32 nIns, uint32 nKey) const +{ + if(nIns >= m_Instruments.size()) + return 0; + const DLSINSTRUMENT &dlsIns = m_Instruments[nIns]; + for(uint32 rgn = 0; rgn < static_cast<uint32>(dlsIns.Regions.size()); rgn++) + { + const auto ®ion = dlsIns.Regions[rgn]; + if(nKey < region.uKeyMin || nKey > region.uKeyMax) + continue; + if(region.nWaveLink == Util::MaxValueOfType(region.nWaveLink)) + continue; + return rgn; + } + return 0; +} + + +bool CDLSBank::ExtractWaveForm(uint32 nIns, uint32 nRgn, std::vector<uint8> &waveData, uint32 &length) const +{ + waveData.clear(); + length = 0; + + if (nIns >= m_Instruments.size() || !m_dwWavePoolOffset) + { + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("ExtractWaveForm({}) failed: m_Instruments.size()={} m_dwWavePoolOffset={} m_WaveForms.size()={}")(nIns, m_Instruments.size(), m_dwWavePoolOffset, m_WaveForms.size())); + #endif + return false; + } + const DLSINSTRUMENT &dlsIns = m_Instruments[nIns]; + if(nRgn >= dlsIns.Regions.size()) + { + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("invalid waveform region: nIns={} nRgn={} pSmp->nRegions={}")(nIns, nRgn, dlsIns.Regions.size())); + #endif + return false; + } + uint32 nWaveLink = dlsIns.Regions[nRgn].nWaveLink; + if(nWaveLink >= m_WaveForms.size()) + { + #ifdef DLSBANK_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSBANK", MPT_UFORMAT("Invalid wavelink id: nWaveLink={} nWaveForms={}")(nWaveLink, m_WaveForms.size())); + #endif + return false; + } + + mpt::ifstream f(m_szFileName, std::ios::binary); + if(!f) + { + return false; + } + + mpt::IO::Offset sampleOffset = mpt::saturate_cast<mpt::IO::Offset>(m_WaveForms[nWaveLink] + m_dwWavePoolOffset); + if(mpt::IO::SeekAbsolute(f, sampleOffset)) + { + if (m_nType & SOUNDBANK_TYPE_SF2) + { + if (m_SamplesEx[nWaveLink].dwLen) + { + if (mpt::IO::SeekRelative(f, 8)) + { + length = m_SamplesEx[nWaveLink].dwLen; + try + { + waveData.assign(length + 8, 0); + mpt::IO::ReadRaw(f, waveData.data(), length); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + } + } else + { + LISTChunk chunk; + if(mpt::IO::Read(f, chunk)) + { + if((chunk.id == IFFID_LIST) && (chunk.listid == IFFID_wave) && (chunk.len > 4)) + { + length = chunk.len + 8; + try + { + waveData.assign(chunk.len + sizeof(IFFCHUNK), 0); + memcpy(waveData.data(), &chunk, sizeof(chunk)); + mpt::IO::ReadRaw(f, &waveData[sizeof(chunk)], length - sizeof(chunk)); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + } + } + } + return !waveData.empty(); +} + + +bool CDLSBank::ExtractSample(CSoundFile &sndFile, SAMPLEINDEX nSample, uint32 nIns, uint32 nRgn, int transpose) const +{ + std::vector<uint8> pWaveForm; + uint32 dwLen = 0; + bool ok, hasWaveform; + + if(nIns >= m_Instruments.size()) + return false; + const DLSINSTRUMENT &dlsIns = m_Instruments[nIns]; + if(nRgn >= dlsIns.Regions.size()) + return false; + if(!ExtractWaveForm(nIns, nRgn, pWaveForm, dwLen)) + return false; + if(dwLen < 16) + return false; + ok = false; + + FileReader wsmpChunk; + if (m_nType & SOUNDBANK_TYPE_SF2) + { + sndFile.DestroySample(nSample); + uint32 nWaveLink = dlsIns.Regions[nRgn].nWaveLink; + ModSample &sample = sndFile.GetSample(nSample); + if (sndFile.m_nSamples < nSample) sndFile.m_nSamples = nSample; + if (nWaveLink < m_SamplesEx.size()) + { + const DLSSAMPLEEX &p = m_SamplesEx[nWaveLink]; + #ifdef DLSINSTR_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" SF2 WaveLink #{}: {}Hz")(nWaveLink, p.dwSampleRate)); + #endif + sample.Initialize(); + + FileReader chunk{mpt::as_span(pWaveForm.data(), dwLen)}; + if(!p.compressed || !sndFile.ReadSampleFromFile(nSample, chunk, false, false)) + { + sample.nLength = dwLen / 2; + SampleIO( + SampleIO::_16bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(sample, chunk); + } + sample.nLoopStart = dlsIns.Regions[nRgn].ulLoopStart; + sample.nLoopEnd = dlsIns.Regions[nRgn].ulLoopEnd; + sample.nC5Speed = p.dwSampleRate; + sample.RelativeTone = p.byOriginalPitch; + sample.nFineTune = p.chPitchCorrection; + if(p.szName[0]) + sndFile.m_szNames[nSample] = mpt::String::ReadAutoBuf(p.szName); + else if(dlsIns.szName[0]) + sndFile.m_szNames[nSample] = mpt::String::ReadAutoBuf(dlsIns.szName); + } + hasWaveform = sample.HasSampleData(); + } else + { + FileReader file(mpt::as_span(pWaveForm.data(), dwLen)); + hasWaveform = sndFile.ReadWAVSample(nSample, file, false, &wsmpChunk); + if(dlsIns.szName[0]) + sndFile.m_szNames[nSample] = mpt::String::ReadAutoBuf(dlsIns.szName); + } + if (hasWaveform) + { + ModSample &sample = sndFile.GetSample(nSample); + const DLSREGION &rgn = dlsIns.Regions[nRgn]; + sample.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN); + if (rgn.fuOptions & DLSREGION_SAMPLELOOP) sample.uFlags.set(CHN_LOOP); + if (rgn.fuOptions & DLSREGION_SUSTAINLOOP) sample.uFlags.set(CHN_SUSTAINLOOP); + if (rgn.fuOptions & DLSREGION_PINGPONGLOOP) sample.uFlags.set(CHN_PINGPONGLOOP); + if (sample.uFlags[CHN_LOOP | CHN_SUSTAINLOOP]) + { + if (rgn.ulLoopEnd > rgn.ulLoopStart) + { + if (sample.uFlags[CHN_SUSTAINLOOP]) + { + sample.nSustainStart = rgn.ulLoopStart; + sample.nSustainEnd = rgn.ulLoopEnd; + } else + { + sample.nLoopStart = rgn.ulLoopStart; + sample.nLoopEnd = rgn.ulLoopEnd; + } + } else + { + sample.uFlags.reset(CHN_LOOP|CHN_SUSTAINLOOP); + } + } + // WSMP chunk + { + uint32 usUnityNote = rgn.uUnityNote; + int sFineTune = rgn.sFineTune; + int lVolume = rgn.usVolume; + + WSMPChunk wsmp; + if(!(rgn.fuOptions & DLSREGION_OVERRIDEWSMP) && wsmpChunk.IsValid() && wsmpChunk.Skip(sizeof(IFFCHUNK)) && wsmpChunk.ReadStructPartial(wsmp)) + { + usUnityNote = wsmp.usUnityNote; + sFineTune = wsmp.sFineTune; + lVolume = DLS32BitRelativeGainToLinear(wsmp.lAttenuation) / 256; + if(wsmp.cSampleLoops) + { + WSMPSampleLoop loop; + wsmpChunk.Seek(sizeof(IFFCHUNK) + wsmp.cbSize); + wsmpChunk.ReadStruct(loop); + if(loop.ulLoopLength > 3) + { + sample.uFlags.set(CHN_LOOP); + //if (loop.ulLoopType) sample.uFlags |= CHN_PINGPONGLOOP; + sample.nLoopStart = loop.ulLoopStart; + sample.nLoopEnd = loop.ulLoopStart + loop.ulLoopLength; + } + } + } else if (m_nType & SOUNDBANK_TYPE_SF2) + { + usUnityNote = (usUnityNote < 0x80) ? usUnityNote : sample.RelativeTone; + sFineTune += sample.nFineTune; + } + #ifdef DLSINSTR_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT("WSMP: usUnityNote={}.{}, {}Hz (transp={})")(usUnityNote, sFineTune, sample.nC5Speed, transpose)); + #endif + if (usUnityNote > 0x7F) usUnityNote = 60; + int steps = (60 + transpose - usUnityNote) * 128 + sFineTune; + sample.Transpose(steps * (1.0 / (12.0 * 128.0))); + sample.RelativeTone = 0; + + Limit(lVolume, 16, 256); + sample.nGlobalVol = (uint8)(lVolume / 4); // 0-64 + } + sample.nPan = GetPanning(nIns, nRgn); + + sample.Convert(MOD_TYPE_IT, sndFile.GetType()); + sample.PrecomputeLoops(sndFile, false); + ok = true; + } + return ok; +} + + +static uint16 ScaleEnvelope(uint32 time, float tempoScale) +{ + return std::max(mpt::saturate_round<uint16>(time * tempoScale), uint16(1)); +} + + +bool CDLSBank::ExtractInstrument(CSoundFile &sndFile, INSTRUMENTINDEX nInstr, uint32 nIns, uint32 nDrumRgn) const +{ + uint32 minRegion, maxRegion, nEnv; + + if (nIns >= m_Instruments.size()) + return false; + const DLSINSTRUMENT &dlsIns = m_Instruments[nIns]; + const bool isDrum = (dlsIns.ulBank & F_INSTRUMENT_DRUMS) && nDrumRgn != uint32_max; + if(isDrum) + { + if(nDrumRgn >= dlsIns.Regions.size()) + return false; + minRegion = nDrumRgn; + maxRegion = nDrumRgn + 1; + nEnv = dlsIns.Regions[nDrumRgn].uPercEnv; + } else + { + if(dlsIns.Regions.empty()) + return false; + minRegion = 0; + maxRegion = static_cast<uint32>(dlsIns.Regions.size()); + nEnv = dlsIns.nMelodicEnv; + } +#ifdef DLSINSTR_LOG + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT("DLS Instrument #{}: {}")(nIns, mpt::ToUnicode(mpt::Charset::ASCII, mpt::String::ReadAutoBuf(dlsIns.szName)))); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" Bank=0x{} Instrument=0x{}")(mpt::ufmt::HEX0<4>(dlsIns.ulBank), mpt::ufmt::HEX0<4>(dlsIns.ulInstrument))); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" {} regions, nMelodicEnv={}")(dlsIns.Regions.size(), dlsIns.nMelodicEnv)); + for (uint32 iDbg=0; iDbg<dlsIns.Regions.size(); iDbg++) + { + const DLSREGION *prgn = &dlsIns.Regions[iDbg]; + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" Region {}:")(iDbg)); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" WaveLink = {} (loop [{}, {}])")(prgn->nWaveLink, prgn->ulLoopStart, prgn->ulLoopEnd)); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" Key Range: [{}, {}]")(prgn->uKeyMin, prgn->uKeyMax)); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" fuOptions = 0x{}")(mpt::ufmt::HEX0<4>(prgn->fuOptions))); + MPT_LOG_GLOBAL(LogDebug, "DLSINSTR", MPT_UFORMAT(" usVolume = {}, Unity Note = {}")(prgn->usVolume, prgn->uUnityNote)); + } +#endif + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + { + return false; + } + + if(sndFile.Instruments[nInstr]) + { + sndFile.DestroyInstrument(nInstr, deleteAssociatedSamples); + } + // Initializes Instrument + if(isDrum) + { + uint32 key = dlsIns.Regions[nDrumRgn].uKeyMin; + if((key >= 24) && (key <= 84)) + { + std::string s = szMidiPercussionNames[key-24]; + if(!mpt::String::ReadAutoBuf(dlsIns.szName).empty()) + { + s += MPT_AFORMAT(" ({})")(mpt::trim_right<std::string>(mpt::String::ReadAutoBuf(dlsIns.szName))); + } + pIns->name = s; + } else + { + pIns->name = mpt::String::ReadAutoBuf(dlsIns.szName); + } + } else + { + pIns->name = mpt::String::ReadAutoBuf(dlsIns.szName); + } + int transpose = 0; + if(isDrum) + { + for(uint32 iNoteMap = 0; iNoteMap < NOTE_MAX; iNoteMap++) + { + if(sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MID | MOD_TYPE_MPT)) + { + // Format has instrument note mapping + if(dlsIns.Regions[nDrumRgn].tuning == 0) + pIns->NoteMap[iNoteMap] = NOTE_MIDDLEC; + else if (iNoteMap < dlsIns.Regions[nDrumRgn].uKeyMin) + pIns->NoteMap[iNoteMap] = (uint8)(dlsIns.Regions[nDrumRgn].uKeyMin + NOTE_MIN); + else if(iNoteMap > dlsIns.Regions[nDrumRgn].uKeyMax) + pIns->NoteMap[iNoteMap] = (uint8)(dlsIns.Regions[nDrumRgn].uKeyMax + NOTE_MIN); + } else + { + if(iNoteMap == dlsIns.Regions[nDrumRgn].uKeyMin) + { + transpose = (dlsIns.Regions[nDrumRgn].uKeyMin + (dlsIns.Regions[nDrumRgn].uKeyMax - dlsIns.Regions[nDrumRgn].uKeyMin) / 2) - 60; + } + } + } + } + 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->nNNA = NewNoteAction::NoteOff; + pIns->nDCT = DuplicateCheckType::Note; + pIns->nDNA = DuplicateNoteAction::NoteFade; + sndFile.Instruments[nInstr] = pIns; + uint32 nLoadedSmp = 0; + SAMPLEINDEX nextSample = 0; + // Extract Samples + std::vector<SAMPLEINDEX> RgnToSmp(dlsIns.Regions.size()); + std::set<uint16> extractedSamples; + for(uint32 nRgn = minRegion; nRgn < maxRegion; nRgn++) + { + bool duplicateRegion = false; + SAMPLEINDEX nSmp = 0; + const DLSREGION &rgn = dlsIns.Regions[nRgn]; + if(rgn.IsDummy()) + continue; + // Elimitate Duplicate Regions + uint32 dupRegion; + for(dupRegion = minRegion; dupRegion < nRgn; dupRegion++) + { + const DLSREGION &rgn2 = dlsIns.Regions[dupRegion]; + if(RgnToSmp[dupRegion] == 0 || rgn2.IsDummy()) + continue; + // No need to extract the same sample data twice + const bool sameSample = (rgn2.nWaveLink == rgn.nWaveLink) && (rgn2.ulLoopEnd == rgn.ulLoopEnd) && (rgn2.ulLoopStart == rgn.ulLoopStart) && extractedSamples.count(rgn.nWaveLink); + // Candidate for stereo sample creation + const bool sameKeyRange = (rgn2.uKeyMin == rgn.uKeyMin) && (rgn2.uKeyMax == rgn.uKeyMax); + if(sameSample || sameKeyRange) + { + duplicateRegion = true; + if(!sameKeyRange) + nSmp = RgnToSmp[dupRegion]; + break; + } + } + // Create a new sample + if (!duplicateRegion) + { + uint32 nmaxsmp = (m_nType & MOD_TYPE_XM) ? 16 : (NOTE_MAX - NOTE_MIN + 1); + if (nLoadedSmp >= nmaxsmp) + { + nSmp = RgnToSmp[nRgn - 1]; + } else + { + nextSample = sndFile.GetNextFreeSample(nInstr, nextSample + 1); + if (nextSample == SAMPLEINDEX_INVALID) break; + if (nextSample > sndFile.GetNumSamples()) sndFile.m_nSamples = nextSample; + nSmp = nextSample; + nLoadedSmp++; + } + } + + RgnToSmp[nRgn] = nSmp; + // Map all notes to the right sample + if(nSmp) + { + for(uint8 key = 0; key < NOTE_MAX; key++) + { + if(isDrum || (key >= rgn.uKeyMin && key <= rgn.uKeyMax)) + { + pIns->Keyboard[key] = nSmp; + } + } + // Load the sample + if(!duplicateRegion || !sndFile.GetSample(nSmp).HasSampleData()) + { + ExtractSample(sndFile, nSmp, nIns, nRgn, transpose); + extractedSamples.insert(rgn.nWaveLink); + } + } else if(duplicateRegion && sndFile.GetSample(RgnToSmp[dupRegion]).GetNumChannels() == 1) + { + // Try to combine stereo samples + const uint16 pan1 = GetPanning(nIns, nRgn), pan2 = GetPanning(nIns, dupRegion); + if((pan1 < 16 && pan2 >= 240) || (pan2 < 16 && pan1 >= 240)) + { + ModSample &sample = sndFile.GetSample(RgnToSmp[dupRegion]); + ModSample sampleCopy = sample; + sampleCopy.pData.pSample = nullptr; + sampleCopy.uFlags.set(CHN_16BIT | CHN_STEREO); + if(!sampleCopy.AllocateSample()) + continue; + + const uint8 offsetOrig = (pan1 < pan2) ? 1 : 0; + const uint8 offsetNew = (pan1 < pan2) ? 0 : 1; + + std::vector<uint8> pWaveForm; + uint32 dwLen = 0; + if(!ExtractWaveForm(nIns, nRgn, pWaveForm, dwLen)) + continue; + extractedSamples.insert(rgn.nWaveLink); + + // First copy over original channel + auto pDest = sampleCopy.sample16() + offsetOrig; + if(sample.uFlags[CHN_16BIT]) + CopySample<SC::ConversionChain<SC::Convert<int16, int16>, SC::DecodeIdentity<int16>>>(pDest, sample.nLength, 2, sample.sample16(), sample.GetSampleSizeInBytes(), 1); + else + CopySample<SC::ConversionChain<SC::Convert<int16, int8>, SC::DecodeIdentity<int8>>>(pDest, sample.nLength, 2, sample.sample8(), sample.GetSampleSizeInBytes(), 1); + sample.FreeSample(); + + // Now read the other channel + if(m_SamplesEx[m_Instruments[nIns].Regions[nRgn].nWaveLink].compressed) + { + FileReader file{mpt::as_span(pWaveForm)}; + if(sndFile.ReadSampleFromFile(nSmp, file, false, false)) + { + pDest = sampleCopy.sample16() + offsetNew; + const SmpLength copyLength = std::min(sample.nLength, sampleCopy.nLength); + if(sample.uFlags[CHN_16BIT]) + CopySample<SC::ConversionChain<SC::Convert<int16, int16>, SC::DecodeIdentity<int16>>>(pDest, copyLength, 2, sample.sample16(), sample.GetSampleSizeInBytes(), sample.GetNumChannels()); + else + CopySample<SC::ConversionChain<SC::Convert<int16, int8>, SC::DecodeIdentity<int8>>>(pDest, copyLength, 2, sample.sample8(), sample.GetSampleSizeInBytes(), sample.GetNumChannels()); + } + } else + { + SmpLength len = std::min(dwLen / 2u, sampleCopy.nLength); + const int16 *src = reinterpret_cast<int16 *>(pWaveForm.data()); + int16 *dst = sampleCopy.sample16() + offsetNew; + CopySample<SC::ConversionChain<SC::Convert<int16, int16>, SC::DecodeIdentity<int16>>>(dst, len, 2, src, pWaveForm.size(), 1); + } + sample.FreeSample(); + sample = sampleCopy; + } + } + } + + float tempoScale = 1.0f; + if(sndFile.m_nTempoMode == TempoMode::Modern) + { + uint32 ticksPerBeat = sndFile.m_nDefaultRowsPerBeat * sndFile.m_nDefaultSpeed; + if(ticksPerBeat != 0) + tempoScale = ticksPerBeat / 24.0f; + } + + // Initializes Envelope + if ((nEnv) && (nEnv <= m_Envelopes.size())) + { + const DLSENVELOPE &part = m_Envelopes[nEnv - 1]; + // Volume Envelope + if ((part.wVolAttack) || (part.wVolDecay < 20*50) || (part.nVolSustainLevel) || (part.wVolRelease < 20*50)) + { + pIns->VolEnv.dwFlags.set(ENV_ENABLED); + // Delay section + // -> DLS level 2 + // Attack section + pIns->VolEnv.clear(); + if (part.wVolAttack) + { + pIns->VolEnv.push_back(0, (uint8)(ENVELOPE_MAX / (part.wVolAttack / 2 + 2) + 8)); // /----- + pIns->VolEnv.push_back(ScaleEnvelope(part.wVolAttack, tempoScale), ENVELOPE_MAX); // | + } else + { + pIns->VolEnv.push_back(0, ENVELOPE_MAX); + } + // Hold section + // -> DLS Level 2 + // Sustain Level + if (part.nVolSustainLevel > 0) + { + if (part.nVolSustainLevel < 128) + { + uint16 lStartTime = pIns->VolEnv.back().tick; + int32 lSusLevel = - DLS32BitRelativeLinearToGain(part.nVolSustainLevel << 9) / 65536; + int32 lDecayTime = 1; + if (lSusLevel > 0) + { + lDecayTime = (lSusLevel * (int32)part.wVolDecay) / 960; + for (uint32 i=0; i<7; i++) + { + int32 lFactor = 128 - (1 << i); + if (lFactor <= part.nVolSustainLevel) break; + int32 lev = - DLS32BitRelativeLinearToGain(lFactor << 9) / 65536; + if (lev > 0) + { + int32 ltime = (lev * (int32)part.wVolDecay) / 960; + if ((ltime > 1) && (ltime < lDecayTime)) + { + uint16 tick = lStartTime + ScaleEnvelope(ltime, tempoScale); + if(tick > pIns->VolEnv.back().tick) + { + pIns->VolEnv.push_back(tick, (uint8)(lFactor / 2)); + } + } + } + } + } + + uint16 decayEnd = lStartTime + ScaleEnvelope(lDecayTime, tempoScale); + if (decayEnd > pIns->VolEnv.back().tick) + { + pIns->VolEnv.push_back(decayEnd, (uint8)((part.nVolSustainLevel+1) / 2)); + } + } + pIns->VolEnv.dwFlags.set(ENV_SUSTAIN); + } else + { + pIns->VolEnv.dwFlags.set(ENV_SUSTAIN); + pIns->VolEnv.push_back(pIns->VolEnv.back().tick + 1u, pIns->VolEnv.back().value); + } + pIns->VolEnv.nSustainStart = pIns->VolEnv.nSustainEnd = (uint8)(pIns->VolEnv.size() - 1); + // Release section + if ((part.wVolRelease) && (pIns->VolEnv.back().value > 1)) + { + int32 lReleaseTime = part.wVolRelease; + uint16 lStartTime = pIns->VolEnv.back().tick; + int32 lStartFactor = pIns->VolEnv.back().value; + int32 lSusLevel = - DLS32BitRelativeLinearToGain(lStartFactor << 10) / 65536; + int32 lDecayEndTime = (lReleaseTime * lSusLevel) / 960; + lReleaseTime -= lDecayEndTime; + if(pIns->VolEnv.nSustainEnd > 0) + pIns->VolEnv.nReleaseNode = pIns->VolEnv.nSustainEnd; + for (uint32 i=0; i<5; i++) + { + int32 lFactor = 1 + ((lStartFactor * 3) >> (i+2)); + if ((lFactor <= 1) || (lFactor >= lStartFactor)) continue; + int32 lev = - DLS32BitRelativeLinearToGain(lFactor << 10) / 65536; + if (lev > 0) + { + int32 ltime = (((int32)part.wVolRelease * lev) / 960) - lDecayEndTime; + if ((ltime > 1) && (ltime < lReleaseTime)) + { + uint16 tick = lStartTime + ScaleEnvelope(ltime, tempoScale); + if(tick > pIns->VolEnv.back().tick) + { + pIns->VolEnv.push_back(tick, (uint8)lFactor); + } + } + } + } + if (lReleaseTime < 1) lReleaseTime = 1; + auto releaseTicks = ScaleEnvelope(lReleaseTime, tempoScale); + pIns->VolEnv.push_back(lStartTime + releaseTicks, ENVELOPE_MIN); + if(releaseTicks > 0) + { + pIns->nFadeOut = 32768 / releaseTicks; + } + } else + { + pIns->VolEnv.push_back(pIns->VolEnv.back().tick + 1u, ENVELOPE_MIN); + } + } + } + if(isDrum) + { + // Create a default envelope for drums + pIns->VolEnv.dwFlags.reset(ENV_SUSTAIN); + if(!pIns->VolEnv.dwFlags[ENV_ENABLED]) + { + pIns->VolEnv.dwFlags.set(ENV_ENABLED); + pIns->VolEnv.resize(4); + pIns->VolEnv[0] = EnvelopeNode(0, ENVELOPE_MAX); + pIns->VolEnv[1] = EnvelopeNode(ScaleEnvelope(5, tempoScale), ENVELOPE_MAX); + pIns->VolEnv[2] = EnvelopeNode(pIns->VolEnv[1].tick * 2u, ENVELOPE_MID); + pIns->VolEnv[3] = EnvelopeNode(pIns->VolEnv[2].tick * 2u, ENVELOPE_MIN); // 1 second max. for drums + } + } + pIns->Sanitize(MOD_TYPE_MPT); + pIns->Convert(MOD_TYPE_MPT, sndFile.GetType()); + return true; +} + + +const char *CDLSBank::GetRegionName(uint32 nIns, uint32 nRgn) const +{ + if(nIns >= m_Instruments.size()) + return nullptr; + const DLSINSTRUMENT &dlsIns = m_Instruments[nIns]; + if(nRgn >= dlsIns.Regions.size()) + return nullptr; + + if (m_nType & SOUNDBANK_TYPE_SF2) + { + uint32 nWaveLink = dlsIns.Regions[nRgn].nWaveLink; + if (nWaveLink < m_SamplesEx.size()) + { + return m_SamplesEx[nWaveLink].szName; + } + } + return nullptr; +} + + +uint16 CDLSBank::GetPanning(uint32 ins, uint32 region) const +{ + const DLSINSTRUMENT &dlsIns = m_Instruments[ins]; + if(region >= std::size(dlsIns.Regions)) + return 128; + const DLSREGION &rgn = dlsIns.Regions[region]; + if(rgn.panning >= 0) + return static_cast<uint16>(rgn.panning); + + if(dlsIns.ulBank & F_INSTRUMENT_DRUMS) + { + if(rgn.uPercEnv > 0 && rgn.uPercEnv <= m_Envelopes.size()) + { + return m_Envelopes[rgn.uPercEnv - 1].nDefPan; + } + } else + { + if(dlsIns.nMelodicEnv > 0 && dlsIns.nMelodicEnv <= m_Envelopes.size()) + { + return m_Envelopes[dlsIns.nMelodicEnv - 1].nDefPan; + } + } + return 128; +} + + +#else // !MODPLUG_TRACKER + +MPT_MSVC_WORKAROUND_LNK4221(Dlsbank) + +#endif // MODPLUG_TRACKER + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.h b/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.h new file mode 100644 index 00000000..d9c3fde2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Dlsbank.h @@ -0,0 +1,159 @@ +/* + * DLSBank.h + * --------- + * Purpose: Sound bank loading. + * Notes : Supported sound bank types: DLS (including embedded DLS in MSS & RMI), SF2 + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN +class CSoundFile; +OPENMPT_NAMESPACE_END +#include "Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +#ifdef MODPLUG_TRACKER + + +struct DLSREGION +{ + uint32 ulLoopStart; + uint32 ulLoopEnd; + uint16 nWaveLink; + uint16 uPercEnv; + uint16 usVolume; // 0..256 + uint16 fuOptions; // flags + key group + int16 sFineTune; // +128 = +1 semitone + int16 panning = -1; // -1= unset (DLS), otherwise 0...256 + uint8 uKeyMin; + uint8 uKeyMax; + uint8 uUnityNote; + uint8 tuning = 100; + + constexpr bool IsDummy() const noexcept { return uKeyMin == 0xFF || nWaveLink == Util::MaxValueOfType(nWaveLink); } +}; + +struct DLSENVELOPE +{ + // Volume Envelope + uint16 wVolAttack; // Attack Time: 0-1000, 1 = 20ms (1/50s) -> [0-20s] + uint16 wVolDecay; // Decay Time: 0-1000, 1 = 20ms (1/50s) -> [0-20s] + uint16 wVolRelease; // Release Time: 0-1000, 1 = 20ms (1/50s) -> [0-20s] + uint8 nVolSustainLevel; // Sustain Level: 0-128, 128=100% + uint8 nDefPan; // Default Pan +}; + +// Special Bank bits +#define F_INSTRUMENT_DRUMS 0x80000000 + +struct DLSINSTRUMENT +{ + uint32 ulBank = 0, ulInstrument = 0; + uint32 nMelodicEnv = 0; + std::vector<DLSREGION> Regions; + char szName[32]; + // SF2 stuff (DO NOT USE! -> used internally by the SF2 loader) + uint16 wPresetBagNdx = 0, wPresetBagNum = 0; +}; + +struct DLSSAMPLEEX +{ + char szName[20]; + uint32 dwLen; + uint32 dwStartloop; + uint32 dwEndloop; + uint32 dwSampleRate; + uint8 byOriginalPitch; + int8 chPitchCorrection; + bool compressed = false; +}; + + +#define SOUNDBANK_TYPE_INVALID 0 +#define SOUNDBANK_TYPE_DLS 0x01 +#define SOUNDBANK_TYPE_SF2 0x02 + +struct SOUNDBANKINFO +{ + std::string szBankName, + szCopyRight, + szComments, + szEngineer, + szSoftware, // ISFT: Software + szDescription; // ISBJ: Subject +}; + +struct IFFCHUNK; +struct SF2LoaderInfo; + +class CDLSBank +{ +protected: + SOUNDBANKINFO m_BankInfo; + mpt::PathString m_szFileName; + size_t m_dwWavePoolOffset; + uint32 m_nType; + // DLS Information + uint32 m_nMaxWaveLink; + uint32 m_sf2version = 0; + std::vector<size_t> m_WaveForms; + std::vector<DLSINSTRUMENT> m_Instruments; + std::vector<DLSSAMPLEEX> m_SamplesEx; + std::vector<DLSENVELOPE> m_Envelopes; + +public: + CDLSBank(); + + bool operator==(const CDLSBank &other) const noexcept { return !mpt::PathString::CompareNoCase(m_szFileName, other.m_szFileName); } + + static bool IsDLSBank(const mpt::PathString &filename); + static uint32 MakeMelodicCode(uint32 bank, uint32 instr) { return ((bank << 16) | (instr));} + static uint32 MakeDrumCode(uint32 rgn, uint32 instr) { return (0x80000000 | (rgn << 16) | (instr));} + +public: + bool Open(const mpt::PathString &filename); + bool Open(FileReader file); + mpt::PathString GetFileName() const { return m_szFileName; } + uint32 GetBankType() const { return m_nType; } + const SOUNDBANKINFO &GetBankInfo() const { return m_BankInfo; } + +public: + uint32 GetNumInstruments() const { return static_cast<uint32>(m_Instruments.size()); } + uint32 GetNumSamples() const { return static_cast<uint32>(m_WaveForms.size()); } + const DLSINSTRUMENT *GetInstrument(uint32 iIns) const { return iIns < m_Instruments.size() ? &m_Instruments[iIns] : nullptr; } + const DLSINSTRUMENT *FindInstrument(bool isDrum, uint32 bank = 0xFF, uint32 program = 0xFF, uint32 key = 0xFF, uint32 *pInsNo = nullptr) const; + bool FindAndExtract(CSoundFile &sndFile, const INSTRUMENTINDEX ins, const bool isDrum) const; + uint32 GetRegionFromKey(uint32 nIns, uint32 nKey) const; + bool ExtractWaveForm(uint32 nIns, uint32 nRgn, std::vector<uint8> &waveData, uint32 &length) const; + bool ExtractSample(CSoundFile &sndFile, SAMPLEINDEX nSample, uint32 nIns, uint32 nRgn, int transpose = 0) const; + bool ExtractInstrument(CSoundFile &sndFile, INSTRUMENTINDEX nInstr, uint32 nIns, uint32 nDrumRgn) const; + const char *GetRegionName(uint32 nIns, uint32 nRgn) const; + uint16 GetPanning(uint32 ins, uint32 region) const; + +// Internal Loader Functions +protected: + bool UpdateInstrumentDefinition(DLSINSTRUMENT *pDlsIns, FileReader chunk); + bool UpdateSF2PresetData(SF2LoaderInfo &sf2info, const IFFCHUNK &header, FileReader &chunk); + bool ConvertSF2ToDLS(SF2LoaderInfo &sf2info); + +public: + // DLS Unit conversion + static int32 DLS32BitTimeCentsToMilliseconds(int32 lTimeCents); + static int32 DLS32BitRelativeGainToLinear(int32 lCentibels); // 0dB = 0x10000 + static int32 DLS32BitRelativeLinearToGain(int32 lGain); // 0dB = 0x10000 + static int32 DLSMidiVolumeToLinear(uint32 nMidiVolume); // [0-127] -> [0-0x10000] +}; + + +#endif // MODPLUG_TRACKER + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Fastmix.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Fastmix.cpp new file mode 100644 index 00000000..8ae5a064 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Fastmix.cpp @@ -0,0 +1,759 @@ +/* + * Fastmix.cpp + * ----------- + * Purpose: Mixer core for rendering samples, mixing plugins, etc... + * Notes : If this is Fastmix.cpp, where is Slowmix.cpp? :) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +// FIXME: +// - Playing samples backwards should reverse interpolation LUTs for interpolation modes +// with more than two taps since they're not symmetric. We might need separate LUTs +// because otherwise we will add tons of branches. +// - Loop wraparound works pretty well in general, but not at the start of bidi samples. +// - The loop lookahead stuff might still fail for samples with backward loops. + +#include "stdafx.h" +#include "Sndfile.h" +#include "MixerLoops.h" +#include "MixFuncTable.h" +#include "plugins/PlugInterface.h" +#include <cfloat> // For FLT_EPSILON +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////// + +struct MixLoopState +{ + const int8 * samplePointer = nullptr; + const int8 * lookaheadPointer = nullptr; + SmpLength lookaheadStart = 0; + uint32 maxSamples = 0; + const uint8 ITPingPongDiff; + const bool precisePingPongLoops; + + MixLoopState(const CSoundFile &sndFile, const ModChannel &chn) + : ITPingPongDiff{sndFile.m_playBehaviour[kITPingPongMode] ? uint8(1) : uint8(0)} + , precisePingPongLoops{!sndFile.m_playBehaviour[kImprecisePingPongLoops]} + { + if(chn.pCurrentSample == nullptr) + return; + + UpdateLookaheadPointers(chn); + + // For platforms that have no fast 64-bit division, precompute this constant + // as it won't change during the invocation of CreateStereoMix. + SamplePosition increment = chn.increment; + if(increment.IsNegative()) + increment.Negate(); + maxSamples = 16384u / (increment.GetUInt() + 1u); + if(maxSamples < 2) + maxSamples = 2; + } + + // Calculate offset of loop wrap-around buffer for this sample. + void UpdateLookaheadPointers(const ModChannel &chn) + { + samplePointer = static_cast<const int8 *>(chn.pCurrentSample); + lookaheadPointer = nullptr; + if(!samplePointer) + return; + if(chn.nLoopEnd < InterpolationLookaheadBufferSize) + lookaheadStart = chn.nLoopStart; + else + lookaheadStart = std::max(chn.nLoopStart, chn.nLoopEnd - InterpolationLookaheadBufferSize); + // We only need to apply the loop wrap-around logic if the sample is actually looping and if interpolation is applied. + // If there is no interpolation happening, there is no lookahead happening the sample read-out is exact. + if(chn.dwFlags[CHN_LOOP] && chn.resamplingMode != SRCMODE_NEAREST) + { + const bool inSustainLoop = chn.InSustainLoop() && chn.nLoopStart == chn.pModSample->nSustainStart && chn.nLoopEnd == chn.pModSample->nSustainEnd; + + // Do not enable wraparound magic if we're previewing a custom loop! + if(inSustainLoop || chn.nLoopEnd == chn.pModSample->nLoopEnd) + { + SmpLength lookaheadOffset = 3 * InterpolationLookaheadBufferSize + chn.pModSample->nLength - chn.nLoopEnd; + if(inSustainLoop) + { + lookaheadOffset += 4 * InterpolationLookaheadBufferSize; + } + lookaheadPointer = samplePointer + lookaheadOffset * chn.pModSample->GetBytesPerSample(); + } + } + } + + // Returns the buffer length required to render a certain amount of samples, based on the channel's playback speed. + static MPT_FORCEINLINE uint32 DistanceToBufferLength(SamplePosition from, SamplePosition to, SamplePosition inc) + { + return static_cast<uint32>((to - from - SamplePosition(1)) / inc) + 1; + } + + // Check how many samples can be rendered without encountering loop or sample end, and also update loop position / direction + MPT_FORCEINLINE uint32 GetSampleCount(ModChannel &chn, uint32 nSamples) const + { + int32 nLoopStart = chn.dwFlags[CHN_LOOP] ? chn.nLoopStart : 0; + SamplePosition nInc = chn.increment; + + if(nSamples <= 0 || nInc.IsZero() || !chn.nLength || !samplePointer) + return 0; + + // Part 1: Making sure the play position is valid, and if necessary, invert the play direction in case we reached a loop boundary of a ping-pong loop. + chn.pCurrentSample = samplePointer; + + // Under zero ? + if (chn.position.GetInt() < nLoopStart) + { + if (nInc.IsNegative()) + { + // Invert loop direction for bidi loops + chn.position = SamplePosition(nLoopStart + nLoopStart, 0) - chn.position; + if ((chn.position.GetInt() < nLoopStart) || (chn.position.GetUInt() >= (nLoopStart + chn.nLength) / 2)) + { + chn.position.Set(nLoopStart, 0); + } + if(chn.dwFlags[CHN_PINGPONGLOOP]) + { + chn.dwFlags.reset(CHN_PINGPONGFLAG); // go forward + nInc.Negate(); + chn.increment = nInc; + } else + { + chn.position.SetInt(chn.nLength - 1); + } + if(!chn.dwFlags[CHN_LOOP] || chn.position.GetUInt() >= chn.nLength) + { + chn.position.Set(chn.nLength); + return 0; + } + } else + { + // We probably didn't hit the loop end yet (first loop), so we do nothing + if (chn.position.GetInt() < 0) chn.position.SetInt(0); + } + } else if (chn.position.GetUInt() >= chn.nLength) + { + // Past the end + if(!chn.dwFlags[CHN_LOOP]) + return 0; // not looping -> stop this channel + if(chn.dwFlags[CHN_PINGPONGLOOP]) + { + // Invert loop + if (nInc.IsPositive()) + { + nInc.Negate(); + chn.increment = nInc; + } + chn.dwFlags.set(CHN_PINGPONGFLAG); + + // Adjust loop position + if(precisePingPongLoops) + { + // More accurate loop end overshoot calculation. + // Test cases: BidiPrecision.it, BidiPrecision.xm + const auto overshoot = chn.position - SamplePosition(chn.nLength, 0); + const auto loopLength = chn.nLoopEnd - chn.nLoopStart - ITPingPongDiff; + if(overshoot.GetUInt() < loopLength) + chn.position = SamplePosition(chn.nLength - ITPingPongDiff, 0) - overshoot; + else + chn.position = SamplePosition(chn.nLoopStart, 0); + } else + { + SamplePosition invFract = chn.position.GetInvertedFract(); + chn.position = SamplePosition(chn.nLength - (chn.position.GetInt() - chn.nLength) - invFract.GetInt(), invFract.GetFract()); + if(chn.position.GetUInt() <= chn.nLoopStart || chn.position.GetUInt() >= chn.nLength) + { + // Impulse Tracker's software mixer would put a -2 (instead of -1) in the following line (doesn't happen on a GUS) + chn.position.SetInt(chn.nLength - std::min(chn.nLength, static_cast<SmpLength>(ITPingPongDiff + 1))); + } + } + } else + { + if (nInc.IsNegative()) // This is a bug + { + nInc.Negate(); + chn.increment = nInc; + } + // Restart at loop start + chn.position += SamplePosition(nLoopStart - chn.nLength, 0); + MPT_ASSERT(chn.position.GetInt() >= nLoopStart); + // Interpolate correctly after wrapping around + chn.dwFlags.set(CHN_WRAPPED_LOOP); + } + } + + // Part 2: Compute how many samples we can render until we reach the end of sample / loop boundary / etc. + + SamplePosition nPos = chn.position; + const SmpLength nPosInt = nPos.GetUInt(); + if(nPos.GetInt() < nLoopStart) + { + // too big increment, and/or too small loop length + if(nPos.IsNegative() || nInc.IsNegative()) + return 0; + } else + { + // Not testing for equality since we might be going backwards from the very end of the sample + if(nPosInt > chn.nLength) + return 0; + // If going forwards and we're preceisely at the end, there's no point in going further + if(nPosInt == chn.nLength && nInc.IsPositive()) + return 0; + } + uint32 nSmpCount = nSamples; + SamplePosition nInv = nInc; + if (nInc.IsNegative()) + { + nInv.Negate(); + } + LimitMax(nSamples, maxSamples); + SamplePosition incSamples = nInc * (nSamples - 1); + int32 nPosDest = (nPos + incSamples).GetInt(); + + const bool isAtLoopStart = (nPosInt >= chn.nLoopStart && nPosInt < chn.nLoopStart + InterpolationLookaheadBufferSize); + if(!isAtLoopStart) + { + chn.dwFlags.reset(CHN_WRAPPED_LOOP); + } + + // Loop wrap-around magic. + bool checkDest = true; + if(lookaheadPointer != nullptr) + { + if(nPosInt >= lookaheadStart) + { +#if 0 + const uint32 oldCount = nSmpCount; + + // When going backwards - we can only go back up to lookaheadStart. + // When going forwards - read through the whole pre-computed wrap-around buffer if possible. + // TODO: ProTracker sample swapping needs hard cut at sample end. + int32 samplesToRead = nInc.IsNegative() + ? (nPosInt - lookaheadStart) + //: 2 * InterpolationMaxLookahead - (nPosInt - mixLoopState.lookaheadStart); + : (chn.nLoopEnd - nPosInt); + //LimitMax(samplesToRead, chn.nLoopEnd - chn.nLoopStart); + nSmpCount = SamplesToBufferLength(samplesToRead, chn); + Limit(nSmpCount, 1u, oldCount); +#else + if (nInc.IsNegative()) + { + nSmpCount = DistanceToBufferLength(SamplePosition(lookaheadStart, 0), nPos, nInv); + } else + { + nSmpCount = DistanceToBufferLength(nPos, SamplePosition(chn.nLoopEnd, 0), nInv); + } +#endif + chn.pCurrentSample = lookaheadPointer; + checkDest = false; + } else if(chn.dwFlags[CHN_WRAPPED_LOOP] && isAtLoopStart) + { + // We just restarted the loop, so interpolate correctly after wrapping around + nSmpCount = DistanceToBufferLength(nPos, SamplePosition(nLoopStart + InterpolationLookaheadBufferSize, 0), nInv); + chn.pCurrentSample = lookaheadPointer + (chn.nLoopEnd - nLoopStart) * chn.pModSample->GetBytesPerSample(); + checkDest = false; + } else if(nInc.IsPositive() && static_cast<SmpLength>(nPosDest) >= lookaheadStart && nSmpCount > 1) + { + // We shouldn't read that far if we're not using the pre-computed wrap-around buffer. + nSmpCount = DistanceToBufferLength(nPos, SamplePosition(lookaheadStart, 0), nInv); + checkDest = false; + } + } + + if(checkDest) + { + // Fix up sample count if target position is invalid + if (nInc.IsNegative()) + { + if (nPosDest < nLoopStart) + { + nSmpCount = DistanceToBufferLength(SamplePosition(nLoopStart, 0), nPos, nInv); + } + } else + { + if (nPosDest >= (int32)chn.nLength) + { + nSmpCount = DistanceToBufferLength(nPos, SamplePosition(chn.nLength, 0), nInv); + } + } + } + + Limit(nSmpCount, uint32(1u), nSamples); + +#ifdef MPT_BUILD_DEBUG + { + SmpLength posDest = (nPos + nInc * (nSmpCount - 1)).GetUInt(); + if (posDest < 0 || posDest > chn.nLength) + { + // We computed an invalid delta! + MPT_ASSERT_NOTREACHED(); + return 0; + } + } +#endif + + return nSmpCount; + } +}; + + +// Render count * number of channels samples +void CSoundFile::CreateStereoMix(int count) +{ + mixsample_t *pOfsL, *pOfsR; + + if(!count) + return; + + // Resetting sound buffer + StereoFill(MixSoundBuffer, count, m_dryROfsVol, m_dryLOfsVol); + if(m_MixerSettings.gnChannels > 2) + StereoFill(MixRearBuffer, count, m_surroundROfsVol, m_surroundLOfsVol); + + CHANNELINDEX nchmixed = 0; + + for(uint32 nChn = 0; nChn < m_nMixChannels; nChn++) + { + ModChannel &chn = m_PlayState.Chn[m_PlayState.ChnMix[nChn]]; + + if(!chn.pCurrentSample && !chn.nLOfs && !chn.nROfs) + continue; + + pOfsR = &m_dryROfsVol; + pOfsL = &m_dryLOfsVol; + + uint32 functionNdx = MixFuncTable::ResamplingModeToMixFlags(static_cast<ResamplingMode>(chn.resamplingMode)); + if(chn.dwFlags[CHN_16BIT]) functionNdx |= MixFuncTable::ndx16Bit; + if(chn.dwFlags[CHN_STEREO]) functionNdx |= MixFuncTable::ndxStereo; +#ifndef NO_FILTER + if(chn.dwFlags[CHN_FILTER]) functionNdx |= MixFuncTable::ndxFilter; +#endif + + mixsample_t *pbuffer = MixSoundBuffer; +#ifndef NO_REVERB + if(((m_MixerSettings.DSPMask & SNDDSP_REVERB) && !chn.dwFlags[CHN_NOREVERB]) || chn.dwFlags[CHN_REVERB]) + { + m_Reverb.TouchReverbSendBuffer(ReverbSendBuffer, m_RvbROfsVol, m_RvbLOfsVol, count); + pbuffer = ReverbSendBuffer; + pOfsR = &m_RvbROfsVol; + pOfsL = &m_RvbLOfsVol; + } +#endif + if(chn.dwFlags[CHN_SURROUND] && m_MixerSettings.gnChannels > 2) + { + pbuffer = MixRearBuffer; + pOfsR = &m_surroundROfsVol; + pOfsL = &m_surroundLOfsVol; + } + + //Look for plugins associated with this implicit tracker channel. +#ifndef NO_PLUGINS + PLUGINDEX nMixPlugin = GetBestPlugin(m_PlayState, m_PlayState.ChnMix[nChn], PrioritiseInstrument, RespectMutes); + + if ((nMixPlugin > 0) && (nMixPlugin <= MAX_MIXPLUGINS) && m_MixPlugins[nMixPlugin - 1].pMixPlugin != nullptr) + { + // Render into plugin buffer instead of global buffer + SNDMIXPLUGINSTATE &mixState = m_MixPlugins[nMixPlugin - 1].pMixPlugin->m_MixState; + if (mixState.pMixBuffer) + { + pbuffer = mixState.pMixBuffer; + pOfsR = &mixState.nVolDecayR; + pOfsL = &mixState.nVolDecayL; + if (!(mixState.dwFlags & SNDMIXPLUGINSTATE::psfMixReady)) + { + StereoFill(pbuffer, count, *pOfsR, *pOfsL); + mixState.dwFlags |= SNDMIXPLUGINSTATE::psfMixReady; + } + } + } +#endif // NO_PLUGINS + + if(chn.isPaused) + { + EndChannelOfs(chn, pbuffer, count); + *pOfsR += chn.nROfs; + *pOfsL += chn.nLOfs; + chn.nROfs = chn.nLOfs = 0; + continue; + } + + MixLoopState mixLoopState(*this, chn); + + //////////////////////////////////////////////////// + CHANNELINDEX naddmix = 0; + int nsamples = count; + // Keep mixing this sample until the buffer is filled. + do + { + uint32 nrampsamples = nsamples; + int32 nSmpCount; + if(chn.nRampLength > 0) + { + if (nrampsamples > chn.nRampLength) nrampsamples = chn.nRampLength; + } + + if((nSmpCount = mixLoopState.GetSampleCount(chn, nrampsamples)) <= 0) + { + // Stopping the channel + chn.pCurrentSample = nullptr; + chn.nLength = 0; + chn.position.Set(0); + chn.nRampLength = 0; + EndChannelOfs(chn, pbuffer, nsamples); + *pOfsR += chn.nROfs; + *pOfsL += chn.nLOfs; + chn.nROfs = chn.nLOfs = 0; + chn.dwFlags.reset(CHN_PINGPONGFLAG); + break; + } + + // Should we mix this channel ? + if((nchmixed >= m_MixerSettings.m_nMaxMixChannels) // Too many channels + || (!chn.nRampLength && !(chn.leftVol | chn.rightVol))) // Channel is completely silent + { + chn.position += chn.increment * nSmpCount; + chn.nROfs = chn.nLOfs = 0; + pbuffer += nSmpCount * 2; + naddmix = 0; + } +#ifdef MODPLUG_TRACKER + else if(m_SamplePlayLengths != nullptr) + { + // Detecting the longest play time for each sample for optimization + SmpLength pos = chn.position.GetUInt(); + chn.position += chn.increment * nSmpCount; + if(!chn.increment.IsNegative()) + { + pos = chn.position.GetUInt(); + } + size_t smp = std::distance(static_cast<const ModSample*>(static_cast<std::decay<decltype(Samples)>::type>(Samples)), chn.pModSample); + if(smp < m_SamplePlayLengths->size()) + { + (*m_SamplePlayLengths)[smp] = std::max((*m_SamplePlayLengths)[smp], pos); + } + } +#endif + else + { + // Do mixing + mixsample_t *pbufmax = pbuffer + (nSmpCount * 2); + chn.nROfs = -*(pbufmax - 2); + chn.nLOfs = -*(pbufmax - 1); + +#ifdef MPT_BUILD_DEBUG + SamplePosition targetpos = chn.position + chn.increment * nSmpCount; +#endif + MixFuncTable::Functions[functionNdx | (chn.nRampLength ? MixFuncTable::ndxRamp : 0)](chn, m_Resampler, pbuffer, nSmpCount); +#ifdef MPT_BUILD_DEBUG + MPT_ASSERT(chn.position.GetUInt() == targetpos.GetUInt()); +#endif + + chn.nROfs += *(pbufmax - 2); + chn.nLOfs += *(pbufmax - 1); + pbuffer = pbufmax; + naddmix = 1; + } + + nsamples -= nSmpCount; + if (chn.nRampLength) + { + if (chn.nRampLength <= static_cast<uint32>(nSmpCount)) + { + // Ramping is done + chn.nRampLength = 0; + chn.leftVol = chn.newLeftVol; + chn.rightVol = chn.newRightVol; + chn.rightRamp = chn.leftRamp = 0; + if(chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol) + { + chn.nLength = 0; + chn.pCurrentSample = nullptr; + } + } else + { + chn.nRampLength -= nSmpCount; + } + } + + const bool pastLoopEnd = chn.position.GetUInt() >= chn.nLoopEnd && chn.dwFlags[CHN_LOOP]; + const bool pastSampleEnd = chn.position.GetUInt() >= chn.nLength && !chn.dwFlags[CHN_LOOP] && chn.nLength && !chn.nMasterChn; + const bool doSampleSwap = m_playBehaviour[kMODSampleSwap] && chn.nNewIns && chn.nNewIns <= GetNumSamples() && chn.pModSample != &Samples[chn.nNewIns]; + if((pastLoopEnd || pastSampleEnd) && doSampleSwap) + { + // ProTracker compatibility: Instrument changes without a note do not happen instantly, but rather when the sample loop has finished playing. + // Test case: PTInstrSwap.mod, PTSwapNoLoop.mod + const ModSample &smp = Samples[chn.nNewIns]; + chn.pModSample = &smp; + chn.pCurrentSample = smp.samplev(); + chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS) | smp.uFlags; + chn.nLength = smp.uFlags[CHN_LOOP] ? smp.nLoopEnd : 0; // non-looping sample continue in oneshot mode (i.e. they will most probably just play silence) + chn.nLoopStart = smp.nLoopStart; + chn.nLoopEnd = smp.nLoopEnd; + chn.position.SetInt(chn.nLoopStart); + mixLoopState.UpdateLookaheadPointers(chn); + if(!chn.pCurrentSample) + { + break; + } + } else if(pastLoopEnd && !doSampleSwap && m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) + { + // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end) + chn.position.SetInt(0); + chn.nLoopEnd = chn.nLength = chn.pModSample->nLoopEnd; + } + } while(nsamples > 0); + + // Restore sample pointer in case it got changed through loop wrap-around + chn.pCurrentSample = mixLoopState.samplePointer; + nchmixed += naddmix; + +#ifndef NO_PLUGINS + if(naddmix && nMixPlugin > 0 && nMixPlugin <= MAX_MIXPLUGINS && m_MixPlugins[nMixPlugin - 1].pMixPlugin) + { + m_MixPlugins[nMixPlugin - 1].pMixPlugin->ResetSilence(); + } +#endif // NO_PLUGINS + } + m_nMixStat = std::max(m_nMixStat, nchmixed); +} + + +void CSoundFile::ProcessPlugins(uint32 nCount) +{ +#ifndef NO_PLUGINS + // If any sample channels are active or any plugin has some input, possibly suspended master plugins need to be woken up. + bool masterHasInput = (m_nMixStat > 0); + +#ifdef MPT_INTMIXER + const float IntToFloat = m_PlayConfig.getIntToFloat(); + const float FloatToInt = m_PlayConfig.getFloatToInt(); +#endif // MPT_INTMIXER + + // Setup float inputs from samples + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + SNDMIXPLUGIN &plugin = m_MixPlugins[plug]; + if(plugin.pMixPlugin != nullptr + && plugin.pMixPlugin->m_MixState.pMixBuffer != nullptr + && plugin.pMixPlugin->m_mixBuffer.Ok()) + { + IMixPlugin *mixPlug = plugin.pMixPlugin; + SNDMIXPLUGINSTATE &state = mixPlug->m_MixState; + + //We should only ever reach this point if the song is playing. + if (!mixPlug->IsSongPlaying()) + { + //Plugin doesn't know it is in a song that is playing; + //we must have added it during playback. Initialise it! + mixPlug->NotifySongPlaying(true); + mixPlug->Resume(); + } + + + // Setup float input + float *plugInputL = mixPlug->m_mixBuffer.GetInputBuffer(0); + float *plugInputR = mixPlug->m_mixBuffer.GetInputBuffer(1); + if (state.dwFlags & SNDMIXPLUGINSTATE::psfMixReady) + { +#ifdef MPT_INTMIXER + StereoMixToFloat(state.pMixBuffer, plugInputL, plugInputR, nCount, IntToFloat); +#else + DeinterleaveStereo(state.pMixBuffer, plugInputL, plugInputR, nCount); +#endif // MPT_INTMIXER + } else if (state.nVolDecayR || state.nVolDecayL) + { + StereoFill(state.pMixBuffer, nCount, state.nVolDecayR, state.nVolDecayL); +#ifdef MPT_INTMIXER + StereoMixToFloat(state.pMixBuffer, plugInputL, plugInputR, nCount, IntToFloat); +#else + DeinterleaveStereo(state.pMixBuffer, plugInputL, plugInputR, nCount); +#endif // MPT_INTMIXER + } else + { + memset(plugInputL, 0, nCount * sizeof(plugInputL[0])); + memset(plugInputR, 0, nCount * sizeof(plugInputR[0])); + } + state.dwFlags &= ~SNDMIXPLUGINSTATE::psfMixReady; + + if(!plugin.IsMasterEffect() && !(state.dwFlags & SNDMIXPLUGINSTATE::psfSilenceBypass)) + { + masterHasInput = true; + } + } + } + // Convert mix buffer +#ifdef MPT_INTMIXER + StereoMixToFloat(MixSoundBuffer, MixFloatBuffer[0], MixFloatBuffer[1], nCount, IntToFloat); +#else + DeinterleaveStereo(MixSoundBuffer, MixFloatBuffer[0], MixFloatBuffer[1], nCount); +#endif // MPT_INTMIXER + float *pMixL = MixFloatBuffer[0]; + float *pMixR = MixFloatBuffer[1]; + + const bool positionChanged = HasPositionChanged(); + + // Process Plugins + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + SNDMIXPLUGIN &plugin = m_MixPlugins[plug]; + if (plugin.pMixPlugin != nullptr + && plugin.pMixPlugin->m_MixState.pMixBuffer != nullptr + && plugin.pMixPlugin->m_mixBuffer.Ok()) + { + IMixPlugin *pObject = plugin.pMixPlugin; + if(!plugin.IsMasterEffect() && !plugin.pMixPlugin->ShouldProcessSilence() && !(plugin.pMixPlugin->m_MixState.dwFlags & SNDMIXPLUGINSTATE::psfHasInput)) + { + // If plugin has no inputs and isn't a master plugin, we shouldn't let it process silence if possible. + // I have yet to encounter a VST plugin which actually sets this flag. + bool hasInput = false; + for(PLUGINDEX inPlug = 0; inPlug < plug; inPlug++) + { + if(m_MixPlugins[inPlug].GetOutputPlugin() == plug) + { + hasInput = true; + break; + } + } + if(!hasInput) + { + continue; + } + } + + bool isMasterMix = false; + float *plugInputL = pObject->m_mixBuffer.GetInputBuffer(0); + float *plugInputR = pObject->m_mixBuffer.GetInputBuffer(1); + + if (pMixL == plugInputL) + { + isMasterMix = true; + pMixL = MixFloatBuffer[0]; + pMixR = MixFloatBuffer[1]; + } + SNDMIXPLUGINSTATE &state = plugin.pMixPlugin->m_MixState; + float *pOutL = pMixL; + float *pOutR = pMixR; + + if (!plugin.IsOutputToMaster()) + { + PLUGINDEX nOutput = plugin.GetOutputPlugin(); + if(nOutput > plug && nOutput < MAX_MIXPLUGINS + && m_MixPlugins[nOutput].pMixPlugin != nullptr) + { + IMixPlugin *outPlugin = m_MixPlugins[nOutput].pMixPlugin; + if(!(state.dwFlags & SNDMIXPLUGINSTATE::psfSilenceBypass)) outPlugin->ResetSilence(); + + if(outPlugin->m_mixBuffer.Ok()) + { + pOutL = outPlugin->m_mixBuffer.GetInputBuffer(0); + pOutR = outPlugin->m_mixBuffer.GetInputBuffer(1); + } + } + } + + /* + if (plugin.multiRouting) { + int nOutput=0; + for (int nOutput=0; nOutput < plugin.nOutputs / 2; nOutput++) { + destinationPlug = plugin.multiRoutingDestinations[nOutput]; + pOutState = m_MixPlugins[destinationPlug].pMixState; + pOutputs[2 * nOutput] = plugInputL; + pOutputs[2 * (nOutput + 1)] = plugInputR; + } + + }*/ + + if (plugin.IsMasterEffect()) + { + if (!isMasterMix) + { + float *pInL = plugInputL; + float *pInR = plugInputR; + for (uint32 i=0; i<nCount; i++) + { + pInL[i] += pMixL[i]; + pInR[i] += pMixR[i]; + pMixL[i] = 0; + pMixR[i] = 0; + } + } + pMixL = pOutL; + pMixR = pOutR; + + if(masterHasInput) + { + // Samples or plugins are being rendered, so turn off auto-bypass for this master effect. + if(plugin.pMixPlugin != nullptr) plugin.pMixPlugin->ResetSilence(); + SNDMIXPLUGIN *chain = &plugin; + PLUGINDEX out = chain->GetOutputPlugin(), prevOut = plug; + while(out > prevOut && out < MAX_MIXPLUGINS) + { + chain = &m_MixPlugins[out]; + prevOut = out; + out = chain->GetOutputPlugin(); + if(chain->pMixPlugin) + { + chain->pMixPlugin->ResetSilence(); + } + } + } + } + + if(plugin.IsBypassed() || (plugin.IsAutoSuspendable() && (state.dwFlags & SNDMIXPLUGINSTATE::psfSilenceBypass))) + { + const float * const pInL = plugInputL; + const float * const pInR = plugInputR; + for (uint32 i=0; i<nCount; i++) + { + pOutL[i] += pInL[i]; + pOutR[i] += pInR[i]; + } + } else + { + if(positionChanged) + pObject->PositionChanged(); + pObject->Process(pOutL, pOutR, nCount); + + state.inputSilenceCount += nCount; + if(plugin.IsAutoSuspendable() && pObject->GetNumOutputChannels() > 0 && state.inputSilenceCount >= m_MixerSettings.gdwMixingFreq * 4) + { + bool isSilent = true; + for(uint32 i = 0; i < nCount; i++) + { + if(pOutL[i] >= FLT_EPSILON || pOutL[i] <= -FLT_EPSILON + || pOutR[i] >= FLT_EPSILON || pOutR[i] <= -FLT_EPSILON) + { + isSilent = false; + break; + } + } + if(isSilent) + { + state.dwFlags |= SNDMIXPLUGINSTATE::psfSilenceBypass; + } else + { + state.inputSilenceCount = 0; + } + } + } + state.dwFlags &= ~SNDMIXPLUGINSTATE::psfHasInput; + } + } +#ifdef MPT_INTMIXER + FloatToStereoMix(pMixL, pMixR, MixSoundBuffer, nCount, FloatToInt); +#else + InterleaveStereo(pMixL, pMixR, MixSoundBuffer, nCount); +#endif // MPT_INTMIXER + +#else + MPT_UNREFERENCED_PARAMETER(nCount); +#endif // NO_PLUGINS +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/FloatMixer.h b/Src/external_dependencies/openmpt-trunk/soundlib/FloatMixer.h new file mode 100644 index 00000000..d53fbabc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/FloatMixer.h @@ -0,0 +1,308 @@ +/* + * FloatMixer.h + * ------------ + * Purpose: Floating point mixer classes + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "MixerInterface.h" +#include "Resampler.h" + +OPENMPT_NAMESPACE_BEGIN + +template<int channelsOut, int channelsIn, typename out, typename in, int int2float> +struct IntToFloatTraits : public MixerTraits<channelsOut, channelsIn, out, in> +{ + static_assert(std::numeric_limits<input_t>::is_integer, "Input must be integer"); + static_assert(!std::numeric_limits<output_t>::is_integer, "Output must be floating point"); + + static MPT_CONSTEXPRINLINE output_t Convert(const input_t x) + { + return static_cast<output_t>(x) * (static_cast<output_t>(1) / static_cast<output_t>(int2float)); + } +}; + +typedef IntToFloatTraits<2, 1, mixsample_t, int8, -int8_min> Int8MToFloatS; +typedef IntToFloatTraits<2, 1, mixsample_t, int16, -int16_min> Int16MToFloatS; +typedef IntToFloatTraits<2, 2, mixsample_t, int8, -int8_min> Int8SToFloatS; +typedef IntToFloatTraits<2, 2, mixsample_t, int16, -int16_min> Int16SToFloatS; + + +////////////////////////////////////////////////////////////////////////// +// Interpolation templates + +template<class Traits> +struct LinearInterpolation +{ + MPT_FORCEINLINE LinearInterpolation(const ModChannel &, const CResampler &, unsigned int) { } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const typename Traits::output_t fract = posLo / static_cast<typename Traits::output_t>(0x100000000); //CResampler::LinearTablef[posLo >> 24]; + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + typename Traits::output_t srcVol = Traits::Convert(inBuffer[i]); + typename Traits::output_t destVol = Traits::Convert(inBuffer[i + Traits::numChannelsIn]); + + outSample[i] = srcVol + fract * (destVol - srcVol); + } + } +}; + + +template<class Traits> +struct FastSincInterpolation +{ + MPT_FORCEINLINE FastSincInterpolation(const ModChannel &, const CResampler &, unsigned int) { } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const typename Traits::output_t *lut = CResampler::FastSincTablef + ((posLo >> 22) & 0x3FC); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = + lut[0] * Traits::Convert(inBuffer[i - Traits::numChannelsIn]) + + lut[1] * Traits::Convert(inBuffer[i]) + + lut[2] * Traits::Convert(inBuffer[i + Traits::numChannelsIn]) + + lut[3] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn]); + } + } +}; + + +template<class Traits> +struct PolyphaseInterpolation +{ + const typename Traits::output_t *sinc; + + MPT_FORCEINLINE PolyphaseInterpolation(const ModChannel &chn, const CResampler &resampler, unsigned int) + { + sinc = (((chn.increment > SamplePosition(0x130000000ll)) || (chn.increment < -SamplePosition(-0x130000000ll))) ? + (((chn.increment > SamplePosition(0x180000000ll)) || (chn.increment < SamplePosition(-0x180000000ll))) ? resampler.gDownsample2x : resampler.gDownsample13x) : resampler.gKaiserSinc); + } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const typename Traits::output_t *lut = sinc + ((posLo >> (32 - SINC_PHASES_BITS)) & SINC_MASK) * SINC_WIDTH; + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = + lut[0] * Traits::Convert(inBuffer[i - 3 * Traits::numChannelsIn]) + + lut[1] * Traits::Convert(inBuffer[i - 2 * Traits::numChannelsIn]) + + lut[2] * Traits::Convert(inBuffer[i - Traits::numChannelsIn]) + + lut[3] * Traits::Convert(inBuffer[i]) + + lut[4] * Traits::Convert(inBuffer[i + Traits::numChannelsIn]) + + lut[5] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn]) + + lut[6] * Traits::Convert(inBuffer[i + 3 * Traits::numChannelsIn]) + + lut[7] * Traits::Convert(inBuffer[i + 4 * Traits::numChannelsIn]); + } + } +}; + + +template<class Traits> +struct FIRFilterInterpolation +{ + const typename Traits::output_t *WFIRlut; + + MPT_FORCEINLINE FIRFilterInterpolation(const ModChannel &, const CResampler &resampler, unsigned int) + { + WFIRlut = resampler.m_WindowedFIR.lut; + } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const typename Traits::output_t * const lut = WFIRlut + ((((posLo >> 16) + WFIR_FRACHALVE) >> WFIR_FRACSHIFT) & WFIR_FRACMASK); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = + lut[0] * Traits::Convert(inBuffer[i - 3 * Traits::numChannelsIn]) + + lut[1] * Traits::Convert(inBuffer[i - 2 * Traits::numChannelsIn]) + + lut[2] * Traits::Convert(inBuffer[i - Traits::numChannelsIn]) + + lut[3] * Traits::Convert(inBuffer[i]) + + lut[4] * Traits::Convert(inBuffer[i + Traits::numChannelsIn]) + + lut[5] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn]) + + lut[6] * Traits::Convert(inBuffer[i + 3 * Traits::numChannelsIn]) + + lut[7] * Traits::Convert(inBuffer[i + 4 * Traits::numChannelsIn]); + } + } +}; + + +////////////////////////////////////////////////////////////////////////// +// Mixing templates (add sample to stereo mix) + +template<class Traits> +struct NoRamp +{ + typename Traits::output_t lVol, rVol; + + MPT_FORCEINLINE NoRamp(const ModChannel &chn) + { + lVol = static_cast<Traits::output_t>(chn.leftVol) * (1.0f / 4096.0f); + rVol = static_cast<Traits::output_t>(chn.rightVol) * (1.0f / 4096.0f); + } +}; + + +struct Ramp +{ + ModChannel &channel; + int32 lRamp, rRamp; + + MPT_FORCEINLINE Ramp(ModChannel &chn) + : channel{chn} + { + lRamp = chn.rampLeftVol; + rRamp = chn.rampRightVol; + } + + MPT_FORCEINLINE ~Ramp() + { + channel.rampLeftVol = lRamp; channel.leftVol = lRamp >> VOLUMERAMPPRECISION; + channel.rampRightVol = rRamp; channel.rightVol = rRamp >> VOLUMERAMPPRECISION; + } +}; + + +// Legacy optimization: If chn.nLeftVol == chn.nRightVol, save one multiplication instruction +template<class Traits> +struct MixMonoFastNoRamp : public NoRamp<Traits> +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &chn, typename Traits::output_t * const outBuffer) + { + typename Traits::output_t vol = outSample[0] * lVol; + for(int i = 0; i < Traits::numChannelsOut; i++) + { + outBuffer[i] += vol; + } + } +}; + + +template<class Traits> +struct MixMonoNoRamp : public NoRamp<Traits> +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &, typename Traits::output_t * const outBuffer) + { + outBuffer[0] += outSample[0] * lVol; + outBuffer[1] += outSample[0] * rVol; + } +}; + + +template<class Traits> +struct MixMonoRamp : public Ramp +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &chn, typename Traits::output_t * const outBuffer) + { + // TODO volume is not float, can we optimize this? + lRamp += chn.leftRamp; + rRamp += chn.rightRamp; + outBuffer[0] += outSample[0] * (lRamp >> VOLUMERAMPPRECISION) * (1.0f / 4096.0f); + outBuffer[1] += outSample[0] * (rRamp >> VOLUMERAMPPRECISION) * (1.0f / 4096.0f); + } +}; + + +template<class Traits> +struct MixStereoNoRamp : public NoRamp<Traits> +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &, typename Traits::output_t * const outBuffer) + { + outBuffer[0] += outSample[0] * lVol; + outBuffer[1] += outSample[1] * rVol; + } +}; + + +template<class Traits> +struct MixStereoRamp : public Ramp +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &chn, typename Traits::output_t * const outBuffer) + { + // TODO volume is not float, can we optimize this? + lRamp += chn.leftRamp; + rRamp += chn.rightRamp; + outBuffer[0] += outSample[0] * (lRamp >> VOLUMERAMPPRECISION) * (1.0f / 4096.0f); + outBuffer[1] += outSample[1] * (rRamp >> VOLUMERAMPPRECISION) * (1.0f / 4096.0f); + } +}; + + +////////////////////////////////////////////////////////////////////////// +// Filter templates + + +template<class Traits> +struct NoFilter +{ + MPT_FORCEINLINE NoFilter(const ModChannel &) { } + + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &, const ModChannel &) { } +}; + + +// Resonant filter +template<class Traits> +struct ResonantFilter +{ + ModChannel &channel; + // Filter history + typename Traits::output_t fy[Traits::numChannelsIn][2]; + + MPT_FORCEINLINE ResonantFilter(ModChannel &chn) + : channel{chn} + { + for(int i = 0; i < Traits::numChannelsIn; i++) + { + fy[i][0] = chn.nFilter_Y[i][0]; + fy[i][1] = chn.nFilter_Y[i][1]; + } + } + + MPT_FORCEINLINE ~ResonantFilter(ModChannel &chn) + { + for(int i = 0; i < Traits::numChannelsIn; i++) + { + channel.nFilter_Y[i][0] = fy[i][0]; + channel.nFilter_Y[i][1] = fy[i][1]; + } + } + + // Filter values are clipped to double the input range +#define ClipFilter(x) Clamp(x, static_cast<Traits::output_t>(-2.0f), static_cast<Traits::output_t>(2.0f)) + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const ModChannel &chn) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + typename Traits::output_t val = outSample[i] * chn.nFilter_A0 + ClipFilter(fy[i][0]) * chn.nFilter_B0 + ClipFilter(fy[i][1]) * chn.nFilter_B1; + fy[i][1] = fy[i][0]; + fy[i][0] = val - (outSample[i] * chn.nFilter_HP); + outSample[i] = val; + } + } + +#undef ClipFilter +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.cpp new file mode 100644 index 00000000..1d45f0ad --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.cpp @@ -0,0 +1,413 @@ +/* + * ITCompression.cpp + * ----------------- + * Purpose: Code for IT sample compression and decompression. + * Notes : The original Python compression code was written by GreaseMonkey and has been released into the public domain. + * Authors: OpenMPT Devs + * Ben "GreaseMonkey" Russell + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include <ostream> +#include "ITCompression.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/misc_util.h" +#include "ModSample.h" +#include "SampleCopy.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Algorithm parameters for 16-Bit samples +struct IT16BitParams +{ + using sample_t = int16; + static constexpr int16 lowerTab[] = {0, -1, -3, -7, -15, -31, -56, -120, -248, -504, -1016, -2040, -4088, -8184, -16376, -32760, -32768}; + static constexpr int16 upperTab[] = {0, 1, 3, 7, 15, 31, 55, 119, 247, 503, 1015, 2039, 4087, 8183, 16375, 32759, 32767}; + static constexpr int8 fetchA = 4; + static constexpr int8 lowerB = -8; + static constexpr int8 upperB = 7; + static constexpr int8 defWidth = 17; + static constexpr int mask = 0xFFFF; +}; + +// Algorithm parameters for 8-Bit samples +struct IT8BitParams +{ + using sample_t = int8; + static constexpr int8 lowerTab[] = {0, -1, -3, -7, -15, -31, -60, -124, -128}; + static constexpr int8 upperTab[] = {0, 1, 3, 7, 15, 31, 59, 123, 127}; + static constexpr int8 fetchA = 3; + static constexpr int8 lowerB = -4; + static constexpr int8 upperB = 3; + static constexpr int8 defWidth = 9; + static constexpr int mask = 0xFF; +}; + +static constexpr int8 ITWidthChangeSize[] = { 4, 5, 6, 7, 8, 9, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 }; + +////////////////////////////////////////////////////////////////////////////// +// IT 2.14 compression + + +ITCompression::ITCompression(const ModSample &sample, bool it215, std::ostream *f, SmpLength maxLength) + : file(f) + , mptSample(sample) + , is215(it215) +{ + if(mptSample.GetElementarySampleSize() > 1) + Compress<IT16BitParams>(mptSample.sample16(), maxLength); + else + Compress<IT8BitParams>(mptSample.sample8(), maxLength); +} + + +template<typename Properties> +void ITCompression::Compress(const typename Properties::sample_t *mptSampleData, SmpLength maxLength) +{ + packedData.resize(bufferSize); + std::vector<typename Properties::sample_t> sampleData; + sampleData.resize(blockSize / sizeof(typename Properties::sample_t)); + if(maxLength == 0 || maxLength > mptSample.nLength) + maxLength = mptSample.nLength; + for(uint8 chn = 0; chn < mptSample.GetNumChannels(); chn++) + { + SmpLength offset = 0; + SmpLength remain = maxLength; + while(remain > 0) + { + // Initialise output buffer and bit writer positions + packedLength = 2; + bitPos = 0; + remBits = 8; + byteVal = 0; + + CompressBlock<Properties>(mptSampleData + chn, offset, remain, sampleData.data()); + + if(file) mpt::IO::WriteRaw(*file, packedData.data(), packedLength); + packedTotalLength += packedLength; + + offset += baseLength; + remain -= baseLength; + } + } + packedData.resize(0); + packedData.shrink_to_fit(); +} + + +template<typename T> +void ITCompression::CopySample(T *target, const T *source, SmpLength offset, SmpLength length, SmpLength skip) +{ + T *out = target; + const T *in = source + offset * skip; + for(SmpLength i = 0, j = 0; j < length; i += skip, j++) + { + out[j] = in[i]; + } +} + + +// Convert sample to delta values. +template<typename T> +void ITCompression::Deltafy(T *sampleData) +{ + T *p = sampleData; + int oldVal = 0; + for(SmpLength i = 0; i < baseLength; i++) + { + int newVal = p[i]; + p[i] = static_cast<T>(newVal - oldVal); + oldVal = newVal; + } +} + + +template<typename Properties> +void ITCompression::CompressBlock(const typename Properties::sample_t *data, SmpLength offset, SmpLength actualLength, typename Properties::sample_t *sampleData) +{ + baseLength = std::min(actualLength, SmpLength(blockSize / sizeof(typename Properties::sample_t))); + + CopySample<typename Properties::sample_t>(sampleData, data, offset, baseLength, mptSample.GetNumChannels()); + + Deltafy(sampleData); + if(is215) + { + Deltafy(sampleData); + } + + // Initialise bit width table with initial values + bwt.assign(baseLength, Properties::defWidth); + + // Recurse! + SquishRecurse<Properties>(Properties::defWidth, Properties::defWidth, Properties::defWidth, Properties::defWidth - 2, 0, baseLength, sampleData); + + // Write those bits! + const typename Properties::sample_t *p = sampleData; + int8 width = Properties::defWidth; + for(size_t i = 0; i < baseLength; i++) + { + if(bwt[i] != width) + { + if(width <= 6) + { + // Mode A: 1 to 6 bits + MPT_ASSERT(width); + WriteBits(width, (1 << (width - 1))); + WriteBits(Properties::fetchA, ConvertWidth(width, bwt[i])); + } else if(width < Properties::defWidth) + { + // Mode B: 7 to 8 / 16 bits + int xv = (1 << (width - 1)) + Properties::lowerB + ConvertWidth(width, bwt[i]); + WriteBits(width, xv); + } else + { + // Mode C: 9 / 17 bits + MPT_ASSERT((bwt[i] - 1) >= 0); + WriteBits(width, (1 << (width - 1)) + bwt[i] - 1); + } + + width = bwt[i]; + } + WriteBits(width, static_cast<int>(p[i]) & Properties::mask); + } + + // Write last byte and update block length + WriteByte(byteVal); + packedData[0] = static_cast<uint8>((packedLength - 2) & 0xFF); + packedData[1] = static_cast<uint8>((packedLength - 2) >> 8); +} + + +int8 ITCompression::GetWidthChangeSize(int8 w, bool is16) +{ + MPT_ASSERT(w > 0 && static_cast<unsigned int>(w) <= std::size(ITWidthChangeSize)); + int8 wcs = ITWidthChangeSize[w - 1]; + if(w <= 6 && is16) + wcs++; + return wcs; +} + + +template<typename Properties> +void ITCompression::SquishRecurse(int8 sWidth, int8 lWidth, int8 rWidth, int8 width, SmpLength offset, SmpLength length, const typename Properties::sample_t *sampleData) +{ + if(width + 1 < 1) + { + for(SmpLength i = offset; i < offset + length; i++) + bwt[i] = sWidth; + return; + } + + MPT_ASSERT(width >= 0 && static_cast<unsigned int>(width) < std::size(Properties::lowerTab)); + + SmpLength i = offset; + SmpLength end = offset + length; + const typename Properties::sample_t *p = sampleData; + + while(i < end) + { + if(p[i] >= Properties::lowerTab[width] && p[i] <= Properties::upperTab[width]) + { + SmpLength start = i; + // Check for how long we can keep this bit width + while(i < end && p[i] >= Properties::lowerTab[width] && p[i] <= Properties::upperTab[width]) + { + i++; + } + + const SmpLength blockLength = i - start; + const int8 xlwidth = start == offset ? lWidth : sWidth; + const int8 xrwidth = i == end ? rWidth : sWidth; + + const bool is16 = sizeof(typename Properties::sample_t) > 1; + const int8 wcsl = GetWidthChangeSize(xlwidth, is16); + const int8 wcss = GetWidthChangeSize(sWidth, is16); + const int8 wcsw = GetWidthChangeSize(width + 1, is16); + + bool comparison; + if(i == baseLength) + { + SmpLength keepDown = wcsl + (width + 1) * blockLength; + SmpLength levelLeft = wcsl + sWidth * blockLength; + + if(xlwidth == sWidth) + levelLeft -= wcsl; + + comparison = (keepDown <= levelLeft); + } else + { + SmpLength keepDown = wcsl + (width + 1) * blockLength + wcsw; + SmpLength levelLeft = wcsl + sWidth * blockLength + wcss; + + if(xlwidth == sWidth) + levelLeft -= wcsl; + if(xrwidth == sWidth) + levelLeft -= wcss; + + comparison = (keepDown <= levelLeft); + } + SquishRecurse<Properties>(comparison ? (width + 1) : sWidth, xlwidth, xrwidth, width - 1, start, blockLength, sampleData); + } else + { + bwt[i] = sWidth; + i++; + } + } +} + + +int8 ITCompression::ConvertWidth(int8 curWidth, int8 newWidth) +{ + curWidth--; + newWidth--; + MPT_ASSERT(newWidth != curWidth); + if(newWidth > curWidth) + newWidth--; + return newWidth; +} + + +void ITCompression::WriteBits(int8 width, int v) +{ + while(width > remBits) + { + byteVal |= (v << bitPos); + width -= remBits; + v >>= remBits; + bitPos = 0; + remBits = 8; + WriteByte(byteVal); + byteVal = 0; + } + + if(width > 0) + { + byteVal |= (v & ((1 << width) - 1)) << bitPos; + remBits -= width; + bitPos += width; + } +} + + +void ITCompression::WriteByte(uint8 v) +{ + if(packedLength < bufferSize) + { + packedData[packedLength++] = v; + } else + { + // How could this happen, anyway? + MPT_ASSERT_NOTREACHED(); + } +} + + +////////////////////////////////////////////////////////////////////////////// +// IT 2.14 decompression + + +ITDecompression::ITDecompression(FileReader &file, ModSample &sample, bool it215) + : mptSample(sample) + , is215(it215) +{ + for(uint8 chn = 0; chn < mptSample.GetNumChannels(); chn++) + { + writtenSamples = writePos = 0; + while(writtenSamples < sample.nLength && file.CanRead(sizeof(uint16))) + { + uint16 compressedSize = file.ReadUint16LE(); + if(!compressedSize) + continue; // Malformed sample? + bitFile = file.ReadChunk(compressedSize); + + // Initialise bit reader + mem1 = mem2 = 0; + + try + { + if(mptSample.GetElementarySampleSize() > 1) + Uncompress<IT16BitParams>(mptSample.sample16() + chn); + else + Uncompress<IT8BitParams>(mptSample.sample8() + chn); + } catch(const BitReader::eof &) + { + // Data is not sufficient to decode the block + //AddToLog(LogWarning, "Truncated IT sample block"); + } + } + } +} + + +template<typename Properties> +void ITDecompression::Uncompress(typename Properties::sample_t *target) +{ + curLength = std::min(mptSample.nLength - writtenSamples, SmpLength(ITCompression::blockSize / sizeof(typename Properties::sample_t))); + + int width = Properties::defWidth; + while(curLength > 0) + { + if(width > Properties::defWidth) + { + // Error! + return; + } + + int v = bitFile.ReadBits(width); + const int topBit = (1 << (width - 1)); + if(width <= 6) + { + // Mode A: 1 to 6 bits + if(v == topBit) + ChangeWidth(width, bitFile.ReadBits(Properties::fetchA)); + else + Write<Properties>(v, topBit, target); + } else if(width < Properties::defWidth) + { + // Mode B: 7 to 8 / 16 bits + if(v >= topBit + Properties::lowerB && v <= topBit + Properties::upperB) + ChangeWidth(width, v - (topBit + Properties::lowerB)); + else + Write<Properties>(v, topBit, target); + } else + { + // Mode C: 9 / 17 bits + if(v & topBit) + width = (v & ~topBit) + 1; + else + Write<Properties>((v & ~topBit), 0, target); + } + } +} + + +void ITDecompression::ChangeWidth(int &curWidth, int width) +{ + width++; + if(width >= curWidth) + width++; + curWidth = width; +} + + +template<typename Properties> +void ITDecompression::Write(int v, int topBit, typename Properties::sample_t *target) +{ + if(v & topBit) + v -= (topBit << 1); + mem1 += v; + mem2 += mem1; + target[writePos] = static_cast<typename Properties::sample_t>(static_cast<int>(is215 ? mem2 : mem1)); + writtenSamples++; + writePos += mptSample.GetNumChannels(); + curLength--; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.h b/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.h new file mode 100644 index 00000000..d0670b33 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ITCompression.h @@ -0,0 +1,101 @@ +/* + * ITCompression.h + * --------------- + * Purpose: Code for IT sample compression and decompression. + * Notes : The original Python compression code was written by GreaseMonkey and has been released into the public domain. + * Authors: OpenMPT Devs + * Ben "GreaseMonkey" Russell + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <vector> +#include <iosfwd> +#include "Snd_defs.h" +#include "BitReader.h" + + +OPENMPT_NAMESPACE_BEGIN + +struct ModSample; + +class ITCompression +{ +public: + ITCompression(const ModSample &sample, bool it215, std::ostream *f, SmpLength maxLength = 0); + size_t GetCompressedSize() const { return packedTotalLength; } + + static constexpr size_t bufferSize = 2 + 0xFFFF; // Our output buffer can't be longer than this. + static constexpr size_t blockSize = 0x8000; // Block size (in bytes) in which samples are being processed + +protected: + std::vector<int8> bwt; // Bit width table for each sampling point + std::vector<uint8> packedData; // Compressed data for current sample block + std::ostream *file = nullptr; // File to which compressed data will be written (can be nullptr if you only want to find out the sample size) + std::vector<int8> sampleData8; // Pre-processed sample data for currently compressed sample block + std::vector<int16> sampleData16; // Pre-processed sample data for currently compressed sample block + const ModSample &mptSample; // Sample that is being processed + size_t packedLength = 0; // Size of currently compressed sample block + size_t packedTotalLength = 0; // Size of all compressed data so far + SmpLength baseLength = 0; // Length of the currently compressed sample block (in samples) + + // Bit writer + int8 bitPos = 0; // Current bit position in this byte + int8 remBits = 0; // Remaining bits in this byte + uint8 byteVal = 0; // Current byte value to be written + + const bool is215; // Use IT2.15 compression (double deltas) + + template<typename Properties> + void Compress(const typename Properties::sample_t *mptSampleData, SmpLength maxLength); + + template<typename T> + static void CopySample(T *target, const T *source, SmpLength offset, SmpLength length, SmpLength skip); + + template<typename T> + void Deltafy(T *sampleData); + + template<typename Properties> + void CompressBlock(const typename Properties::sample_t *data, SmpLength offset, SmpLength actualLength, typename Properties::sample_t *sampleData); + + static int8 GetWidthChangeSize(int8 w, bool is16); + + template<typename Properties> + void SquishRecurse(int8 sWidth, int8 lWidth, int8 rWidth, int8 width, SmpLength offset, SmpLength length, const typename Properties::sample_t *sampleData); + + static int8 ConvertWidth(int8 curWidth, int8 newWidth); + void WriteBits(int8 width, int v); + + void WriteByte(uint8 v); +}; + + +class ITDecompression +{ +public: + ITDecompression(FileReader &file, ModSample &sample, bool it215); + +protected: + BitReader bitFile; + ModSample &mptSample; // Sample that is being processed + + SmpLength writtenSamples = 0; // Number of samples so far written on this channel + SmpLength writePos = 0; // Absolut write position in sample (for stereo samples) + SmpLength curLength = 0; // Length of currently processed block + unsigned int mem1 = 0, mem2 = 0; // Integrator memory + + const bool is215; // Use IT2.15 compression (double deltas) + + template<typename Properties> + void Uncompress(typename Properties::sample_t *target); + static void ChangeWidth(int &curWidth, int width); + + template<typename Properties> + void Write(int v, int topbit, typename Properties::sample_t *target); +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.cpp new file mode 100644 index 00000000..e72184f9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.cpp @@ -0,0 +1,685 @@ +/* + * ITTools.cpp + * ----------- + * Purpose: Definition of IT file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "ITTools.h" +#include "Tables.h" +#include "../common/mptStringBuffer.h" +#include "../common/version.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Convert OpenMPT's internal envelope format into an IT/MPTM envelope. +void ITEnvelope::ConvertToIT(const InstrumentEnvelope &mptEnv, uint8 envOffset, uint8 envDefault) +{ + // Envelope Flags + if(mptEnv.dwFlags[ENV_ENABLED]) flags |= ITEnvelope::envEnabled; + if(mptEnv.dwFlags[ENV_LOOP]) flags |= ITEnvelope::envLoop; + if(mptEnv.dwFlags[ENV_SUSTAIN]) flags |= ITEnvelope::envSustain; + if(mptEnv.dwFlags[ENV_CARRY]) flags |= ITEnvelope::envCarry; + + // Nodes and Loops + num = (uint8)std::min(mptEnv.size(), uint32(25)); + lpb = (uint8)mptEnv.nLoopStart; + lpe = (uint8)mptEnv.nLoopEnd; + slb = (uint8)mptEnv.nSustainStart; + sle = (uint8)mptEnv.nSustainEnd; + + // Envelope Data + MemsetZero(data); + if(!mptEnv.empty()) + { + // Attention: Full MPTM envelope is stored in extended instrument properties + for(uint32 ev = 0; ev < num; ev++) + { + data[ev].value = static_cast<int8>(mptEnv[ev].value) - envOffset; + data[ev].tick = mptEnv[ev].tick; + } + } else + { + // Fix non-existing envelopes so that they can still be edited in Impulse Tracker. + num = 2; + data[0].value = data[1].value = envDefault - envOffset; + data[1].tick = 10; + } +} + + +// Convert IT/MPTM envelope data into OpenMPT's internal envelope format - To be used by ITInstrToMPT() +void ITEnvelope::ConvertToMPT(InstrumentEnvelope &mptEnv, uint8 envOffset, uint8 maxNodes) const +{ + // Envelope Flags + mptEnv.dwFlags.set(ENV_ENABLED, (flags & ITEnvelope::envEnabled) != 0); + mptEnv.dwFlags.set(ENV_LOOP, (flags & ITEnvelope::envLoop) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & ITEnvelope::envSustain) != 0); + mptEnv.dwFlags.set(ENV_CARRY, (flags & ITEnvelope::envCarry) != 0); + + // Nodes and Loops + mptEnv.resize(std::min(num, maxNodes)); + mptEnv.nLoopStart = std::min(lpb, maxNodes); + mptEnv.nLoopEnd = Clamp(lpe, mptEnv.nLoopStart, maxNodes); + mptEnv.nSustainStart = std::min(slb, maxNodes); + mptEnv.nSustainEnd = Clamp(sle, mptEnv.nSustainStart, maxNodes); + + // Envelope Data + // Attention: Full MPTM envelope is stored in extended instrument properties + for(uint32 ev = 0; ev < std::min(uint8(25), num); ev++) + { + mptEnv[ev].value = Clamp<int8, int8>(data[ev].value + envOffset, 0, 64); + mptEnv[ev].tick = data[ev].tick; + if(ev > 0 && mptEnv[ev].tick < mptEnv[ev - 1].tick && !(mptEnv[ev].tick & 0xFF00)) + { + // Fix broken envelopes... Instruments 2 and 3 in NoGap.it by Werewolf have envelope points where the high byte of envelope nodes is missing. + // NoGap.it was saved with MPT 1.07 - 1.09, which *normally* doesn't do this in IT files. + // However... It turns out that MPT 1.07 omitted the high byte of envelope nodes when saving an XI instrument file, and it looks like + // Instrument 2 and 3 in NoGap.it were loaded from XI files. + mptEnv[ev].tick |= mptEnv[ev - 1].tick & 0xFF00; + if(mptEnv[ev].tick < mptEnv[ev - 1].tick) + mptEnv[ev].tick += 0x100; + } + } +} + + +// Convert an ITOldInstrument to OpenMPT's internal instrument representation. +void ITOldInstrument::ConvertToMPT(ModInstrument &mptIns) const +{ + // Header + if(memcmp(id, "IMPI", 4)) + { + return; + } + + mptIns.name = mpt::String::ReadBuf(mpt::String::spacePadded, name); + mptIns.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + // Volume / Panning + mptIns.nFadeOut = fadeout << 6; + mptIns.nGlobalVol = 64; + mptIns.nPan = 128; + + // NNA Stuff + mptIns.nNNA = static_cast<NewNoteAction>(nna.get()); + mptIns.nDCT = static_cast<DuplicateCheckType>(dnc.get()); + + // Sample Map + for(size_t i = 0; i < 120; i++) + { + uint8 note = keyboard[i * 2]; + SAMPLEINDEX ins = keyboard[i * 2 + 1]; + if(ins < MAX_SAMPLES) + { + mptIns.Keyboard[i] = ins; + } + if(note < 120) + { + mptIns.NoteMap[i] = note + 1u; + } else + { + mptIns.NoteMap[i] = static_cast<uint8>(i + 1); + } + } + + // Volume Envelope Flags + mptIns.VolEnv.dwFlags.set(ENV_ENABLED, (flags & ITOldInstrument::envEnabled) != 0); + mptIns.VolEnv.dwFlags.set(ENV_LOOP, (flags & ITOldInstrument::envLoop) != 0); + mptIns.VolEnv.dwFlags.set(ENV_SUSTAIN, (flags & ITOldInstrument::envSustain) != 0); + + // Volume Envelope Loops + mptIns.VolEnv.nLoopStart = vls; + mptIns.VolEnv.nLoopEnd = vle; + mptIns.VolEnv.nSustainStart = sls; + mptIns.VolEnv.nSustainEnd = sle; + mptIns.VolEnv.resize(25); + + // Volume Envelope Data + for(uint32 i = 0; i < 25; i++) + { + if((mptIns.VolEnv[i].tick = nodes[i * 2]) == 0xFF) + { + mptIns.VolEnv.resize(i); + break; + } + mptIns.VolEnv[i].value = nodes[i * 2 + 1]; + } + + if(std::max(mptIns.VolEnv.nLoopStart, mptIns.VolEnv.nLoopEnd) >= mptIns.VolEnv.size()) mptIns.VolEnv.dwFlags.reset(ENV_LOOP); + if(std::max(mptIns.VolEnv.nSustainStart, mptIns.VolEnv.nSustainEnd) >= mptIns.VolEnv.size()) mptIns.VolEnv.dwFlags.reset(ENV_SUSTAIN); +} + + +// Convert OpenMPT's internal instrument representation to an ITInstrument. +uint32 ITInstrument::ConvertToIT(const ModInstrument &mptIns, bool compatExport, const CSoundFile &sndFile) +{ + MemsetZero(*this); + + // Header + memcpy(id, "IMPI", 4); + trkvers = 0x5000 | static_cast<uint16>(Version::Current().GetRawVersion() >> 16); + + mpt::String::WriteBuf(mpt::String::nullTerminated, filename) = mptIns.filename; + mpt::String::WriteBuf(mpt::String::nullTerminated, name) = mptIns.name; + + // Volume / Panning + fadeout = static_cast<uint16>(std::min(mptIns.nFadeOut >> 5, uint32(256))); + gbv = static_cast<uint8>(std::min(mptIns.nGlobalVol * 2u, uint32(128))); + dfp = static_cast<uint8>(std::min(mptIns.nPan / 4u, uint32(64))); + if(!mptIns.dwFlags[INS_SETPANNING]) dfp |= ITInstrument::ignorePanning; + + // Random Variation + rv = std::min(mptIns.nVolSwing, uint8(100)); + rp = std::min(mptIns.nPanSwing, uint8(64)); + + // NNA Stuff + nna = static_cast<uint8>(mptIns.nNNA); + dct = static_cast<uint8>((mptIns.nDCT < DuplicateCheckType::Plugin || !compatExport) ? mptIns.nDCT : DuplicateCheckType::None); + dca = static_cast<uint8>(mptIns.nDNA); + + // Pitch / Pan Separation + pps = mptIns.nPPS; + ppc = mptIns.nPPC; + + // Filter Stuff + ifc = mptIns.GetCutoff() | (mptIns.IsCutoffEnabled() ? ITInstrument::enableCutoff : 0x00); + ifr = mptIns.GetResonance() | (mptIns.IsResonanceEnabled() ? ITInstrument::enableResonance : 0x00); + + // MIDI Setup + if(mptIns.nMidiProgram > 0) + mpr = mptIns.nMidiProgram - 1u; + else + mpr = 0xFF; + if(mptIns.wMidiBank > 0) + { + mbank[0] = static_cast<uint8>((mptIns.wMidiBank - 1) & 0x7F); + mbank[1] = static_cast<uint8>((mptIns.wMidiBank - 1) >> 7); + } else + { + mbank[0] = 0xFF; + mbank[1] = 0xFF; + } + if(mptIns.nMidiChannel != MidiNoChannel || mptIns.nMixPlug == 0 || mptIns.nMixPlug > 127 || compatExport) + { + // Default. Prefer MIDI channel over mixplug to keep the semantics intact. + mch = mptIns.nMidiChannel; + } else + { + // Keep compatibility with MPT 1.16's instrument format if possible, as XMPlay / BASS also uses this. + mch = mptIns.nMixPlug + 128; + } + + // Sample Map + nos = 0; // Only really relevant for ITI files + std::vector<bool> smpCount(sndFile.GetNumSamples(), false); + for(int i = 0; i < 120; i++) + { + keyboard[i * 2] = (mptIns.NoteMap[i] >= NOTE_MIN && mptIns.NoteMap[i] <= NOTE_MAX) ? (mptIns.NoteMap[i] - NOTE_MIN) : static_cast<uint8>(i); + + const SAMPLEINDEX smp = mptIns.Keyboard[i]; + if(smp < MAX_SAMPLES && smp < 256) + { + keyboard[i * 2 + 1] = static_cast<uint8>(smp); + + if(smp && smp <= sndFile.GetNumSamples() && !smpCount[smp - 1]) + { + // We haven't considered this sample yet. Update number of samples. + smpCount[smp - 1] = true; + nos++; + } + } + } + + // Writing Volume envelope + volenv.ConvertToIT(mptIns.VolEnv, 0, 64); + // Writing Panning envelope + panenv.ConvertToIT(mptIns.PanEnv, 32, 32); + // Writing Pitch Envelope + pitchenv.ConvertToIT(mptIns.PitchEnv, 32, 32); + if(mptIns.PitchEnv.dwFlags[ENV_FILTER]) pitchenv.flags |= ITEnvelope::envFilter; + + return sizeof(ITInstrument); +} + + +// Convert an ITInstrument to OpenMPT's internal instrument representation. Returns size of the instrument data that has been read. +uint32 ITInstrument::ConvertToMPT(ModInstrument &mptIns, MODTYPE modFormat) const +{ + if(memcmp(id, "IMPI", 4)) + { + return 0; + } + + mptIns.name = mpt::String::ReadBuf(mpt::String::spacePadded, name); + mptIns.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + // Volume / Panning + mptIns.nFadeOut = fadeout << 5; + mptIns.nGlobalVol = gbv / 2; + LimitMax(mptIns.nGlobalVol, 64u); + mptIns.nPan = (dfp & 0x7F) * 4; + if(mptIns.nPan > 256) mptIns.nPan = 128; + mptIns.dwFlags.set(INS_SETPANNING, !(dfp & ITInstrument::ignorePanning)); + + // Random Variation + mptIns.nVolSwing = std::min(static_cast<uint8>(rv), uint8(100)); + mptIns.nPanSwing = std::min(static_cast<uint8>(rp), uint8(64)); + + // NNA Stuff + mptIns.nNNA = static_cast<NewNoteAction>(nna.get()); + mptIns.nDCT = static_cast<DuplicateCheckType>(dct.get()); + mptIns.nDNA = static_cast<DuplicateNoteAction>(dca.get()); + + // Pitch / Pan Separation + mptIns.nPPS = pps; + mptIns.nPPC = ppc; + + // Filter Stuff + mptIns.SetCutoff(ifc & 0x7F, (ifc & ITInstrument::enableCutoff) != 0); + mptIns.SetResonance(ifr & 0x7F, (ifr & ITInstrument::enableResonance) != 0); + + // MIDI Setup + + // MPT used to have a slightly different encoding of MIDI program and banks which we are trying to fix here. + // Impulse Tracker / Schism Tracker will set trkvers to 0 in IT files, + // and we won't care about correctly importing MIDI programs and banks in ITI files. + // Chibi Tracker sets trkvers to 0x214, but always writes mpr=mbank=0 anyway. + // Old BeRoTracker versions set trkvers to 0x214 or 0x217. + // <= MPT 1.07 <= MPT 1.16 OpenMPT 1.17-? <= OpenMPT 1.26 definitely not MPT + if((trkvers == 0x0202 || trkvers == 0x0211 || trkvers == 0x0220 || trkvers == 0x0214) && mpr != 0xFF) + { + if(mpr <= 128) + { + mptIns.nMidiProgram = mpr; + } + uint16 bank = mbank[0] | (mbank[1] << 8); + // These versions also ignored the high bank nibble (was only handled correctly in OpenMPT instrument extensions) + if(bank <= 128) + { + mptIns.wMidiBank = bank; + } + } else + { + if(mpr < 128) + { + mptIns.nMidiProgram = mpr + 1; + } + uint16 bank = 0; + if(mbank[0] < 128) + bank = mbank[0] + 1; + if(mbank[1] < 128) + bank += (mbank[1] << 7); + mptIns.wMidiBank = bank; + } + mptIns.nMidiChannel = mch; + if(mptIns.nMidiChannel >= 128) + { + // Handle old format where MIDI channel and Plugin index are stored in the same variable + mptIns.nMixPlug = mptIns.nMidiChannel - 128; + mptIns.nMidiChannel = 0; + } + + // Envelope point count. Limited to 25 in IT format. + const uint8 maxNodes = (modFormat & MOD_TYPE_MPT) ? MAX_ENVPOINTS : 25; + + // Volume Envelope + volenv.ConvertToMPT(mptIns.VolEnv, 0, maxNodes); + // Panning Envelope + panenv.ConvertToMPT(mptIns.PanEnv, 32, maxNodes); + // Pitch Envelope + pitchenv.ConvertToMPT(mptIns.PitchEnv, 32, maxNodes); + mptIns.PitchEnv.dwFlags.set(ENV_FILTER, (pitchenv.flags & ITEnvelope::envFilter) != 0); + + // Sample Map + for(int i = 0; i < 120; i++) + { + uint8 note = keyboard[i * 2]; + SAMPLEINDEX ins = keyboard[i * 2 + 1]; + if(ins < MAX_SAMPLES) + { + mptIns.Keyboard[i] = ins; + } + if(note < 120) + { + mptIns.NoteMap[i] = note + NOTE_MIN; + } else + { + mptIns.NoteMap[i] = static_cast<uint8>(i + NOTE_MIN); + } + } + + return sizeof(ITInstrument); +} + + +// Convert OpenMPT's internal instrument representation to an ITInstrumentEx. Returns amount of bytes that need to be written to file. +uint32 ITInstrumentEx::ConvertToIT(const ModInstrument &mptIns, bool compatExport, const CSoundFile &sndFile) +{ + uint32 instSize = iti.ConvertToIT(mptIns, compatExport, sndFile); + + if(compatExport) + { + return instSize; + } + + // Sample Map + bool usedExtension = false; + iti.nos = 0; + std::vector<bool> smpCount(sndFile.GetNumSamples(), false); + for(int i = 0; i < 120; i++) + { + const SAMPLEINDEX smp = mptIns.Keyboard[i]; + keyboardhi[i] = 0; + if(smp < MAX_SAMPLES) + { + if(smp >= 256) + { + // We need to save the upper byte for this sample index. + iti.keyboard[i * 2 + 1] = static_cast<uint8>(smp & 0xFF); + keyboardhi[i] = static_cast<uint8>(smp >> 8); + usedExtension = true; + } + + if(smp && smp <= sndFile.GetNumSamples() && !smpCount[smp - 1]) + { + // We haven't considered this sample yet. Update number of samples. + smpCount[smp - 1] = true; + iti.nos++; + } + } + } + + if(usedExtension) + { + // If we actually had to extend the sample map, update the magic bytes and instrument size. + memcpy(iti.dummy, "XTPM", 4); + instSize = sizeof(ITInstrumentEx); + } + + return instSize; +} + + +// Convert an ITInstrumentEx to OpenMPT's internal instrument representation. Returns size of the instrument data that has been read. +uint32 ITInstrumentEx::ConvertToMPT(ModInstrument &mptIns, MODTYPE fromType) const +{ + uint32 insSize = iti.ConvertToMPT(mptIns, fromType); + + // Is this actually an extended instrument? + // Note: OpenMPT 1.20 - 1.22 accidentally wrote "MPTX" here (since revision 1203), while previous versions wrote the reversed version, "XTPM". + if(insSize == 0 || (memcmp(iti.dummy, "MPTX", 4) && memcmp(iti.dummy, "XTPM", 4))) + { + return insSize; + } + + // Olivier's MPT Instrument Extension + for(int i = 0; i < 120; i++) + { + mptIns.Keyboard[i] |= ((SAMPLEINDEX)keyboardhi[i] << 8); + } + + return sizeof(ITInstrumentEx); +} + + +// Convert OpenMPT's internal sample representation to an ITSample. +void ITSample::ConvertToIT(const ModSample &mptSmp, MODTYPE fromType, bool compress, bool compressIT215, bool allowExternal) +{ + MemsetZero(*this); + + // Header + memcpy(id, "IMPS", 4); + + mpt::String::WriteBuf(mpt::String::nullTerminated, filename) = mptSmp.filename; + //mpt::String::WriteBuf(mpt::String::nullTerminated, name) = m_szNames[nsmp]; + + // Volume / Panning + gvl = static_cast<uint8>(mptSmp.nGlobalVol); + vol = static_cast<uint8>(mptSmp.nVolume / 4); + dfp = static_cast<uint8>(mptSmp.nPan / 4); + if(mptSmp.uFlags[CHN_PANNING]) dfp |= ITSample::enablePanning; + + // Sample Format / Loop Flags + if(mptSmp.HasSampleData() && !mptSmp.uFlags[CHN_ADLIB]) + { + flags = ITSample::sampleDataPresent; + if(mptSmp.uFlags[CHN_LOOP]) flags |= ITSample::sampleLoop; + if(mptSmp.uFlags[CHN_SUSTAINLOOP]) flags |= ITSample::sampleSustain; + if(mptSmp.uFlags[CHN_PINGPONGLOOP]) flags |= ITSample::sampleBidiLoop; + if(mptSmp.uFlags[CHN_PINGPONGSUSTAIN]) flags |= ITSample::sampleBidiSustain; + + if(mptSmp.uFlags[CHN_STEREO]) + { + flags |= ITSample::sampleStereo; + } + if(mptSmp.uFlags[CHN_16BIT]) + { + flags |= ITSample::sample16Bit; + } + cvt = ITSample::cvtSignedSample; + + if(compress) + { + flags |= ITSample::sampleCompressed; + if(compressIT215) + { + cvt |= ITSample::cvtDelta; + } + } + } else + { + flags = 0x00; + } + + // Frequency + C5Speed = mptSmp.nC5Speed ? mptSmp.nC5Speed : 8363; + + // Size and loops + length = mpt::saturate_cast<uint32>(mptSmp.nLength); + loopbegin = mpt::saturate_cast<uint32>(mptSmp.nLoopStart); + loopend = mpt::saturate_cast<uint32>(mptSmp.nLoopEnd); + susloopbegin = mpt::saturate_cast<uint32>(mptSmp.nSustainStart); + susloopend = mpt::saturate_cast<uint32>(mptSmp.nSustainEnd); + + // Auto Vibrato settings + vit = AutoVibratoXM2IT[mptSmp.nVibType & 7]; + vis = std::min(mptSmp.nVibRate, uint8(64)); + vid = std::min(mptSmp.nVibDepth, uint8(32)); + vir = std::min(mptSmp.nVibSweep, uint8(255)); + + if((vid | vis) != 0 && (fromType & MOD_TYPE_XM)) + { + // Sweep is upside down in XM + if(mptSmp.nVibSweep != 0) + vir = mpt::saturate_cast<decltype(vir)::base_type>(Util::muldivr_unsigned(mptSmp.nVibDepth, 256, mptSmp.nVibSweep)); + else + vir = 255; + } + + if(mptSmp.uFlags[CHN_ADLIB]) + { + length = 12; + flags = ITSample::sampleDataPresent; + cvt = ITSample::cvtOPLInstrument; + } else if(mptSmp.uFlags[SMP_KEEPONDISK]) + { +#ifndef MPT_EXTERNAL_SAMPLES + allowExternal = false; +#endif // MPT_EXTERNAL_SAMPLES + // Save external sample (filename at sample pointer) + if(allowExternal && mptSmp.HasSampleData()) + { + cvt = ITSample::cvtExternalSample; + } else + { + length = loopbegin = loopend = susloopbegin = susloopend = 0; + } + } +} + + +// Convert an ITSample to OpenMPT's internal sample representation. +uint32 ITSample::ConvertToMPT(ModSample &mptSmp) const +{ + if(memcmp(id, "IMPS", 4)) + { + return 0; + } + + mptSmp.Initialize(MOD_TYPE_IT); + mptSmp.SetDefaultCuePoints(); // For old IT/MPTM files + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + // Volume / Panning + mptSmp.nVolume = vol * 4; + LimitMax(mptSmp.nVolume, uint16(256)); + mptSmp.nGlobalVol = gvl; + LimitMax(mptSmp.nGlobalVol, uint16(64)); + mptSmp.nPan = (dfp & 0x7F) * 4; + LimitMax(mptSmp.nPan, uint16(256)); + if(dfp & ITSample::enablePanning) mptSmp.uFlags.set(CHN_PANNING); + + // Loop Flags + if(flags & ITSample::sampleLoop) mptSmp.uFlags.set(CHN_LOOP); + if(flags & ITSample::sampleSustain) mptSmp.uFlags.set(CHN_SUSTAINLOOP); + if(flags & ITSample::sampleBidiLoop) mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & ITSample::sampleBidiSustain) mptSmp.uFlags.set(CHN_PINGPONGSUSTAIN); + + // Frequency + mptSmp.nC5Speed = C5Speed; + if(!mptSmp.nC5Speed) mptSmp.nC5Speed = 8363; + if(mptSmp.nC5Speed < 256) mptSmp.nC5Speed = 256; + + // Size and loops + mptSmp.nLength = length; + mptSmp.nLoopStart = loopbegin; + mptSmp.nLoopEnd = loopend; + mptSmp.nSustainStart = susloopbegin; + mptSmp.nSustainEnd = susloopend; + mptSmp.SanitizeLoops(); + + // Auto Vibrato settings + mptSmp.nVibType = static_cast<VibratoType>(AutoVibratoIT2XM[vit & 7]); + mptSmp.nVibRate = vis; + mptSmp.nVibDepth = vid & 0x7F; + mptSmp.nVibSweep = vir; + + if(cvt == ITSample::cvtOPLInstrument) + { + // FM instrument in MPTM + mptSmp.uFlags.set(CHN_ADLIB); + } else if(cvt == ITSample::cvtExternalSample) + { + // Read external sample (filename at sample pointer) + mptSmp.uFlags.set(SMP_KEEPONDISK); + } + + return samplepointer; +} + + +// Retrieve the internal sample format flags for this instrument. +SampleIO ITSample::GetSampleFormat(uint16 cwtv) const +{ + SampleIO sampleIO( + (flags & ITSample::sample16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (cvt & ITSample::cvtSignedSample) ? SampleIO::signedPCM: SampleIO::unsignedPCM); + + // Some old version of IT didn't clear the stereo flag when importing samples. Luckily, all other trackers are identifying as IT 2.14+, so let's check for old IT versions. + if((flags & ITSample::sampleStereo) && cwtv >= 0x214) + { + sampleIO |= SampleIO::stereoSplit; + } + + if(flags & ITSample::sampleCompressed) + { + // IT 2.14 packed sample + sampleIO |= (cvt & ITSample::cvtDelta) ? SampleIO::IT215 : SampleIO::IT214; + } else + { + // MODPlugin :( + if(!(flags & ITSample::sample16Bit) && cvt == ITSample::cvtADPCMSample) + { + sampleIO |= SampleIO::ADPCM; + } else + { + // ITTECH.TXT says these convert flags are "safe to ignore". IT doesn't ignore them, though, so why should we? :) + if(cvt & ITSample::cvtBigEndian) + { + sampleIO |= SampleIO::bigEndian; + } + if(cvt & ITSample::cvtDelta) + { + sampleIO |= SampleIO::deltaPCM; + } + if((cvt & ITSample::cvtPTM8to16) && (flags & ITSample::sample16Bit)) + { + sampleIO |= SampleIO::PTM8Dto16; + } + } + } + + return sampleIO; +} + + +// Convert an ITHistoryStruct to OpenMPT's internal edit history representation +void ITHistoryStruct::ConvertToMPT(FileHistory &mptHistory) const +{ + // Decode FAT date and time + MemsetZero(mptHistory.loadDate); + if(fatdate != 0 || fattime != 0) + { + mptHistory.loadDate.tm_year = ((fatdate >> 9) & 0x7F) + 80; + mptHistory.loadDate.tm_mon = Clamp((fatdate >> 5) & 0x0F, 1, 12) - 1; + mptHistory.loadDate.tm_mday = Clamp(fatdate & 0x1F, 1, 31); + mptHistory.loadDate.tm_hour = Clamp((fattime >> 11) & 0x1F, 0, 23); + mptHistory.loadDate.tm_min = Clamp((fattime >> 5) & 0x3F, 0, 59); + mptHistory.loadDate.tm_sec = Clamp((fattime & 0x1F) * 2, 0, 59); + } + mptHistory.openTime = static_cast<uint32>(runtime * (HISTORY_TIMER_PRECISION / 18.2)); +} + + +// Convert OpenMPT's internal edit history representation to an ITHistoryStruct +void ITHistoryStruct::ConvertToIT(const FileHistory &mptHistory) +{ + // Create FAT file dates + if(mptHistory.HasValidDate()) + { + fatdate = static_cast<uint16>(mptHistory.loadDate.tm_mday | ((mptHistory.loadDate.tm_mon + 1) << 5) | ((mptHistory.loadDate.tm_year - 80) << 9)); + fattime = static_cast<uint16>((mptHistory.loadDate.tm_sec / 2) | (mptHistory.loadDate.tm_min << 5) | (mptHistory.loadDate.tm_hour << 11)); + } else + { + fatdate = 0; + fattime = 0; + } + runtime = static_cast<uint32>(mptHistory.openTime * (18.2 / HISTORY_TIMER_PRECISION)); +} + + +uint32 DecodeITEditTimer(uint16 cwtv, uint32 editTime) +{ + if((cwtv & 0xFFF) >= 0x0208) + { + editTime ^= 0x4954524B; // 'ITRK' + editTime = mpt::rotr(editTime, 7); + editTime = ~editTime + 1; + editTime = mpt::rotl(editTime, 4); + editTime ^= 0x4A54484C; // 'JTHL' + } + return editTime; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.h b/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.h new file mode 100644 index 00000000..4176df70 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ITTools.h @@ -0,0 +1,323 @@ +/* + * ITTools.h + * --------- + * Purpose: Definition of IT file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/ModInstrument.h" +#include "../soundlib/ModSample.h" +#include "../soundlib/SampleIO.h" + +OPENMPT_NAMESPACE_BEGIN + +struct ITFileHeader +{ + // Header Flags + enum ITHeaderFlags + { + useStereoPlayback = 0x01, + vol0Optimisations = 0x02, + instrumentMode = 0x04, + linearSlides = 0x08, + itOldEffects = 0x10, + itCompatGxx = 0x20, + useMIDIPitchController = 0x40, + reqEmbeddedMIDIConfig = 0x80, + extendedFilterRange = 0x1000, + }; + + // Special Flags + enum ITHeaderSpecialFlags + { + embedSongMessage = 0x01, + embedEditHistory = 0x02, + embedPatternHighlights = 0x04, + embedMIDIConfiguration = 0x08, + }; + + char id[4]; // Magic Bytes (IMPM) + char songname[26]; // Song Name, null-terminated (but may also contain nulls) + uint8le highlight_minor; // Rows per Beat highlight + uint8le highlight_major; // Rows per Measure highlight + uint16le ordnum; // Number of Orders + uint16le insnum; // Number of Instruments + uint16le smpnum; // Number of Samples + uint16le patnum; // Number of Patterns + uint16le cwtv; // "Made With" Tracker + uint16le cmwt; // "Compatible With" Tracker + uint16le flags; // Header Flags + uint16le special; // Special Flags, for embedding extra information + uint8le globalvol; // Global Volume (0...128) + uint8le mv; // Master Volume (0...128), referred to as Sample Volume in OpenMPT + uint8le speed; // Initial Speed (1...255) + uint8le tempo; // Initial Tempo (31...255) + uint8le sep; // Pan Separation (0...128) + uint8le pwd; // Pitch Wheel Depth + uint16le msglength; // Length of Song Message + uint32le msgoffset; // Offset of Song Message in File (IT crops message after first null) + uint32le reserved; // Some IT versions save an edit timer here. ChibiTracker writes "CHBI" here. OpenMPT and Schism Tracker save extended version information here. + uint8le chnpan[64]; // Initial Channel Panning + uint8le chnvol[64]; // Initial Channel Volume +}; + +MPT_BINARY_STRUCT(ITFileHeader, 192) + + +struct ITEnvelope +{ + // Envelope Flags + enum ITEnvelopeFlags + { + envEnabled = 0x01, + envLoop = 0x02, + envSustain = 0x04, + envCarry = 0x08, + envFilter = 0x80, + }; + + struct Node + { + int8le value; + uint16le tick; + }; + + uint8 flags; // Envelope Flags + uint8 num; // Number of Envelope Nodes + uint8 lpb; // Loop Start + uint8 lpe; // Loop End + uint8 slb; // Sustain Start + uint8 sle; // Sustain End + Node data[25]; // Envelope Node Positions / Values + uint8 reserved; // Reserved + + // Convert OpenMPT's internal envelope format to an IT/MPTM envelope. + void ConvertToIT(const InstrumentEnvelope &mptEnv, uint8 envOffset, uint8 envDefault); + // Convert IT/MPTM envelope data into OpenMPT's internal envelope format - To be used by ITInstrToMPT() + void ConvertToMPT(InstrumentEnvelope &mptEnv, uint8 envOffset, uint8 maxNodes) const; +}; + +MPT_BINARY_STRUCT(ITEnvelope::Node, 3) +MPT_BINARY_STRUCT(ITEnvelope, 82) + + +// Old Impulse Instrument Format (cmwt < 0x200) +struct ITOldInstrument +{ + enum ITOldInstrFlags + { + envEnabled = 0x01, + envLoop = 0x02, + envSustain = 0x04, + }; + + char id[4]; // Magic Bytes (IMPI) + char filename[13]; // DOS Filename, null-terminated + uint8le flags; // Volume Envelope Flags + uint8le vls; // Envelope Loop Start + uint8le vle; // Envelope Loop End + uint8le sls; // Envelope Sustain Start + uint8le sle; // Envelope Sustain End + char reserved1[2]; // Reserved + uint16le fadeout; // Instrument Fadeout (0...128) + uint8le nna; // New Note Action + uint8le dnc; // Duplicate Note Check Type + uint16le trkvers; // Tracker ID + uint8le nos; // Number of embedded samples + char reserved2; // Reserved + char name[26]; // Instrument Name, null-terminated (but may also contain nulls) + char reserved3[6]; // Even more reserved bytes + uint8le keyboard[240]; // Sample / Transpose map + uint8le volenv[200]; // This appears to be a pre-computed (interpolated) version of the volume envelope data found below. + uint8le nodes[25 * 2]; // Volume Envelope Node Positions / Values + + // Convert an ITOldInstrument to OpenMPT's internal instrument representation. + void ConvertToMPT(ModInstrument &mptIns) const; +}; + +MPT_BINARY_STRUCT(ITOldInstrument, 554) + + +// Impulse Instrument Format +struct ITInstrument +{ + enum ITInstrumentFlags + { + ignorePanning = 0x80, + enableCutoff = 0x80, + enableResonance = 0x80, + }; + + char id[4]; // Magic Bytes (IMPI) + char filename[13]; // DOS Filename, null-terminated + uint8le nna; // New Note Action + uint8le dct; // Duplicate Note Check Type + uint8le dca; // Duplicate Note Check Action + uint16le fadeout; // Instrument Fadeout (0...256, although values up to 1024 would be sensible. Up to IT2.07, the limit was 0...128) + int8le pps; // Pitch/Pan Separatation + uint8le ppc; // Pitch/Pan Centre + uint8le gbv; // Global Volume + uint8le dfp; // Panning + uint8le rv; // Vol Swing + uint8le rp; // Pan Swing + uint16le trkvers; // Tracker ID + uint8le nos; // Number of embedded samples + char reserved1; // Reserved + char name[26]; // Instrument Name, null-terminated (but may also contain nulls) + uint8le ifc; // Filter Cutoff + uint8le ifr; // Filter Resonance + uint8le mch; // MIDI Channel + uint8le mpr; // MIDI Program + uint8le mbank[2]; // MIDI Bank + uint8le keyboard[240]; // Sample / Transpose map + ITEnvelope volenv; // Volume Envelope + ITEnvelope panenv; // Pan Envelope + ITEnvelope pitchenv; // Pitch / Filter Envelope + char dummy[4]; // IT saves some additional padding bytes to match the size of the old instrument format for simplified loading. We use them for some hacks. + + // Convert OpenMPT's internal instrument representation to an ITInstrument. Returns amount of bytes that need to be written. + uint32 ConvertToIT(const ModInstrument &mptIns, bool compatExport, const CSoundFile &sndFile); + // Convert an ITInstrument to OpenMPT's internal instrument representation. Returns size of the instrument data that has been read. + uint32 ConvertToMPT(ModInstrument &mptIns, MODTYPE fromType) const; +}; + +MPT_BINARY_STRUCT(ITInstrument, 554) + + +// MPT IT Instrument Extension +struct ITInstrumentEx +{ + ITInstrument iti; // Normal IT Instrument + uint8 keyboardhi[120]; // High Byte of Sample map + + // Convert OpenMPT's internal instrument representation to an ITInstrumentEx. Returns amount of bytes that need to be written. + uint32 ConvertToIT(const ModInstrument &mptIns, bool compatExport, const CSoundFile &sndFile); + // Convert an ITInstrumentEx to OpenMPT's internal instrument representation. Returns size of the instrument data that has been read. + uint32 ConvertToMPT(ModInstrument &mptIns, MODTYPE fromType) const; +}; + +MPT_BINARY_STRUCT(ITInstrumentEx, sizeof(ITInstrument) + 120) + + +// IT Sample Format +struct ITSample +{ + // Magic Bytes + enum Magic + { + magic = 0x53504D49, // "IMPS" IT Sample Header Magic Bytes + }; + + enum ITSampleFlags + { + sampleDataPresent = 0x01, + sample16Bit = 0x02, + sampleStereo = 0x04, + sampleCompressed = 0x08, + sampleLoop = 0x10, + sampleSustain = 0x20, + sampleBidiLoop = 0x40, + sampleBidiSustain = 0x80, + + enablePanning = 0x80, + + cvtSignedSample = 0x01, + cvtOPLInstrument = 0x40, // FM instrument in MPTM + cvtExternalSample = 0x80, // Keep MPTM sample on disk + cvtADPCMSample = 0xFF, // MODPlugin :( + + // ITTECH.TXT says these convert flags are "safe to ignore". IT doesn't ignore them, though, so why should we? :) + cvtBigEndian = 0x02, + cvtDelta = 0x04, + cvtPTM8to16 = 0x08, + }; + + char id[4]; // Magic Bytes (IMPS) + char filename[13]; // DOS Filename, null-terminated + uint8le gvl; // Global Volume + uint8le flags; // Sample Flags + uint8le vol; // Default Volume + char name[26]; // Sample Name, null-terminated (but may also contain nulls) + uint8le cvt; // Sample Import Format + uint8le dfp; // Sample Panning + uint32le length; // Sample Length (in samples) + uint32le loopbegin; // Sample Loop Begin (in samples) + uint32le loopend; // Sample Loop End (in samples) + uint32le C5Speed; // C-5 frequency + uint32le susloopbegin; // Sample Sustain Begin (in samples) + uint32le susloopend; // Sample Sustain End (in samples) + uint32le samplepointer; // Pointer to sample data + uint8le vis; // Auto-Vibrato Rate (called Sweep in IT) + uint8le vid; // Auto-Vibrato Depth + uint8le vir; // Auto-Vibrato Sweep (called Rate in IT) + uint8le vit; // Auto-Vibrato Type + + // Convert OpenMPT's internal sample representation to an ITSample. + void ConvertToIT(const ModSample &mptSmp, MODTYPE fromType, bool compress, bool compressIT215, bool allowExternal); + // Convert an ITSample to OpenMPT's internal sample representation. + uint32 ConvertToMPT(ModSample &mptSmp) const; + // Retrieve the internal sample format flags for this instrument. + SampleIO GetSampleFormat(uint16 cwtv = 0x214) const; +}; + +MPT_BINARY_STRUCT(ITSample, 80) + + +struct FileHistory; + +// IT Header extension: Save history +struct ITHistoryStruct +{ + uint16le fatdate; // DOS / FAT date when the file was opened / created in the editor. For details, read https://docs.microsoft.com/de-de/windows/win32/api/winbase/nf-winbase-dosdatetimetofiletime + uint16le fattime; // DOS / FAT time when the file was opened / created in the editor. + uint32le runtime; // The time how long the file was open in the editor, in 1/18.2th seconds. (= ticks of the DOS timer) + + // Convert an ITHistoryStruct to OpenMPT's internal edit history representation + void ConvertToMPT(FileHistory &mptHistory) const; + // Convert OpenMPT's internal edit history representation to an ITHistoryStruct + void ConvertToIT(const FileHistory &mptHistory); + +}; + +MPT_BINARY_STRUCT(ITHistoryStruct, 8) + + +enum IT_ReaderBitMasks +{ + // pattern row parsing, the channel data is read to obtain + // number of channels active in the pattern. These bit masks are + // to blank out sections of the byte of data being read. + + IT_bitmask_patternChanField_c = 0x7f, + IT_bitmask_patternChanMask_c = 0x3f, + IT_bitmask_patternChanEnabled_c = 0x80, + IT_bitmask_patternChanUsed_c = 0x0f +}; + + +// Calculate Schism Tracker version field for IT / S3M header based on specified release date +// Date calculation derived from https://alcor.concordia.ca/~gpkatch/gdate-algorithm.html +template<int32 y, int32 m, int32 d> +struct SchismVersionFromDate +{ +private: + static constexpr int32 mm = (m + 9) % 12; + static constexpr int32 yy = y - mm / 10; + +public: + static constexpr int32 date = yy * 365 + yy / 4 - yy / 100 + yy / 400 + (mm * 306 + 5) / 10 + (d - 1); +}; + +inline constexpr int32 SchismTrackerEpoch = SchismVersionFromDate<2009, 10, 31>::date; + + +uint32 DecodeITEditTimer(uint16 cwtv, uint32 editTime); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/InstrumentExtensions.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/InstrumentExtensions.cpp new file mode 100644 index 00000000..8698e73e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/InstrumentExtensions.cpp @@ -0,0 +1,775 @@ +/* + * InstrumentExtensions.cpp + * ------------------------ + * Purpose: Instrument properties I/O + * Notes : Welcome to the absolutely horrible abominations that are the "extended instrument properties" + * which are some of the earliest additions OpenMPT did to the IT / XM format. They are ugly, + * and the way they work even differs between IT/XM and ITI/XI/ITP. + * Yes, the world would be a better place without this stuff. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#endif + +OPENMPT_NAMESPACE_BEGIN + +/*--------------------------------------------------------------------------------------------- +----------------------------------------------------------------------------------------------- +MODULAR (in/out) ModInstrument : +----------------------------------------------------------------------------------------------- + +* to update: +------------ + +- both following functions need to be updated when adding a new member in ModInstrument : + +void WriteInstrumentHeaderStructOrField(ModInstrument * input, std::ostream &file, uint32 only_this_code, int16 fixedsize); +bool ReadInstrumentHeaderField(ModInstrument * input, uint32 fcode, int16 fsize, FileReader &file); + +- see below for body declaration. + + +* members: +---------- + +- 32bit identification CODE tag (must be unique) +- 16bit content SIZE in byte(s) +- member field + + +* CODE tag naming convention: +----------------------------- + +- have a look below in current tag dictionnary +- take the initial ones of the field name +- 4 caracters code (not more, not less) +- must be filled with '.' caracters if code has less than 4 caracters +- for arrays, must include a '[' caracter following significant caracters ('.' not significant!!!) +- use only caracters used in full member name, ordered as they appear in it +- match caracter attribute (small,capital) + +Example with "PanEnv.nLoopEnd" , "PitchEnv.nLoopEnd" & "VolEnv.Values[MAX_ENVPOINTS]" members : +- use 'PLE.' for PanEnv.nLoopEnd +- use 'PiLE' for PitchEnv.nLoopEnd +- use 'VE[.' for VolEnv.Values[MAX_ENVPOINTS] + + +* In use CODE tag dictionary (alphabetical order): +-------------------------------------------------- + + !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !!! SECTION TO BE UPDATED !!! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + [EXT] means external (not related) to ModInstrument content + +AUTH [EXT] Song artist +C... [EXT] nChannels +ChnS [EXT] IT/MPTM: Channel settings for channels 65-127 if needed (doesn't fit to IT header). +CS.. nCutSwing +CUES [EXT] Sample cue points +CWV. [EXT] dwCreatedWithVersion +DCT. nDCT; +dF.. dwFlags; +DGV. [EXT] nDefaultGlobalVolume +DT.. [EXT] nDefaultTempo; +DTFR [EXT] Fractional part of default tempo +DNA. nDNA; +EBIH [EXT] embeded instrument header tag (ITP file format) +FM.. filterMode; +fn[. filename[12]; +FO.. nFadeOut; +GV.. nGlobalVol; +IFC. nIFC; +IFR. nIFR; +K[. Keyboard[128]; +LSWV [EXT] Last Saved With Version +MB.. wMidiBank; +MC.. nMidiChannel; +MDK. nMidiDrumKey; +MIMA [EXT] MIdi MApping directives +MiP. nMixPlug; +MP.. nMidiProgram; +MPTS [EXT] Extra song info tag +MPTX [EXT] EXTRA INFO tag +MSF. [EXT] Mod(Specific)Flags +n[.. name[32]; +NNA. nNNA; +NM[. NoteMap[128]; +P... nPan; +PE.. PanEnv.nNodes; +PE[. PanEnv.Values[MAX_ENVPOINTS]; +PiE. PitchEnv.nNodes; +PiE[ PitchEnv.Values[MAX_ENVPOINTS]; +PiLE PitchEnv.nLoopEnd; +PiLS PitchEnv.nLoopStart; +PiP[ PitchEnv.Ticks[MAX_ENVPOINTS]; +PiSB PitchEnv.nSustainStart; +PiSE PitchEnv.nSustainEnd; +PLE. PanEnv.nLoopEnd; +PLS. PanEnv.nLoopStart; +PMM. [EXT] nPlugMixMode; +PP[. PanEnv.Ticks[MAX_ENVPOINTS]; +PPC. nPPC; +PPS. nPPS; +PS.. nPanSwing; +PSB. PanEnv.nSustainStart; +PSE. PanEnv.nSustainEnd; +PTTL pitchToTempoLock; +PTTF pitchToTempoLock (fractional part); +PVEH pluginVelocityHandling; +PVOH pluginVolumeHandling; +R... resampling; +RP.. [EXT] nRestartPos; +RPB. [EXT] nRowsPerBeat; +RPM. [EXT] nRowsPerMeasure; +RS.. nResSwing; +RSMP [EXT] Global resampling +SEP@ [EXT] chunk SEPARATOR tag +SPA. [EXT] m_nSamplePreAmp; +TM.. [EXT] nTempoMode; +VE.. VolEnv.nNodes; +VE[. VolEnv.Values[MAX_ENVPOINTS]; +VLE. VolEnv.nLoopEnd; +VLS. VolEnv.nLoopStart; +VP[. VolEnv.Ticks[MAX_ENVPOINTS]; +VR.. nVolRampUp; +VS.. nVolSwing; +VSB. VolEnv.nSustainStart; +VSE. VolEnv.nSustainEnd; +VSTV [EXT] nVSTiVolume; +PERN PitchEnv.nReleaseNode +AERN PanEnv.nReleaseNode +VERN VolEnv.nReleaseNode +PFLG PitchEnv.dwFlag +AFLG PanEnv.dwFlags +VFLG VolEnv.dwFlags +MPWD MIDI Pitch Wheel Depth +----------------------------------------------------------------------------------------------- +---------------------------------------------------------------------------------------------*/ + +#ifndef MODPLUG_NO_FILESAVE + +template<typename T, bool is_signed> struct IsNegativeFunctor { bool operator()(T val) const { return val < 0; } }; +template<typename T> struct IsNegativeFunctor<T, true> { bool operator()(T val) const { return val < 0; } }; +template<typename T> struct IsNegativeFunctor<T, false> { bool operator()(T /*val*/) const { return false; } }; + +template<typename T> +bool IsNegative(const T &val) +{ + return IsNegativeFunctor<T, std::numeric_limits<T>::is_signed>()(val); +} + +// ------------------------------------------------------------------------------------------ +// Convenient macro to help WRITE_HEADER declaration for single type members ONLY (non-array) +// ------------------------------------------------------------------------------------------ +#define WRITE_MPTHEADER_sized_member(name,type,code) \ + static_assert(sizeof(input->name) == sizeof(type), "Instrument property does match specified type!");\ + fcode = code;\ + fsize = sizeof( type );\ + if(writeAll) \ + { \ + mpt::IO::WriteIntLE<uint32>(file, fcode); \ + mpt::IO::WriteIntLE<uint16>(file, fsize); \ + } else if(only_this_code == fcode)\ + { \ + MPT_ASSERT(fixedsize == fsize); \ + } \ + if(only_this_code == fcode || only_this_code == Util::MaxValueOfType(only_this_code)) \ + { \ + type tmp = (type)(input-> name ); \ + mpt::IO::WriteIntLE(file, tmp); \ + } \ +/**/ + +// ----------------------------------------------------------------------------------------------------- +// Convenient macro to help WRITE_HEADER declaration for single type members which are written truncated +// ----------------------------------------------------------------------------------------------------- +#define WRITE_MPTHEADER_trunc_member(name,type,code) \ + static_assert(sizeof(input->name) > sizeof(type), "Instrument property would not be truncated, use WRITE_MPTHEADER_sized_member instead!");\ + fcode = code;\ + fsize = sizeof( type );\ + if(writeAll) \ + { \ + mpt::IO::WriteIntLE<uint32>(file, fcode); \ + mpt::IO::WriteIntLE<uint16>(file, fsize); \ + type tmp = (type)(input-> name ); \ + mpt::IO::WriteIntLE(file, tmp); \ + } else if(only_this_code == fcode)\ + { \ + /* hackish workaround to resolve mismatched size values: */ \ + /* nResampling was a long time declared as uint32 but these macro tables used uint16 and UINT. */ \ + /* This worked fine on little-endian, on big-endian not so much. Thus support writing size-mismatched fields. */ \ + MPT_ASSERT(fixedsize >= fsize); \ + type tmp = (type)(input-> name ); \ + mpt::IO::WriteIntLE(file, tmp); \ + if(fixedsize > fsize) \ + { \ + for(int16 i = 0; i < fixedsize - fsize; ++i) \ + { \ + uint8 fillbyte = !IsNegative(tmp) ? 0 : 0xff; /* sign extend */ \ + mpt::IO::WriteIntLE(file, fillbyte); \ + } \ + } \ + } \ +/**/ + +// ------------------------------------------------------------------------ +// Convenient macro to help WRITE_HEADER declaration for array members ONLY +// ------------------------------------------------------------------------ +#define WRITE_MPTHEADER_array_member(name,type,code,arraysize) \ + static_assert(sizeof(type) == sizeof(input-> name [0])); \ + MPT_ASSERT(sizeof(input->name) >= sizeof(type) * arraysize);\ + fcode = code;\ + fsize = sizeof( type ) * arraysize;\ + if(writeAll) \ + { \ + mpt::IO::WriteIntLE<uint32>(file, fcode); \ + mpt::IO::WriteIntLE<uint16>(file, fsize); \ + } else if(only_this_code == fcode)\ + { \ + /* MPT_ASSERT(fixedsize <= fsize); */ \ + fsize = fixedsize; /* just trust the size we got passed */ \ + } \ + if(only_this_code == fcode || only_this_code == Util::MaxValueOfType(only_this_code)) \ + { \ + for(std::size_t i = 0; i < fsize/sizeof(type); ++i) \ + { \ + type tmp; \ + tmp = input-> name [i]; \ + mpt::IO::WriteIntLE(file, tmp); \ + } \ + } \ +/**/ + +// ------------------------------------------------------------------------ +// Convenient macro to help WRITE_HEADER declaration for envelope members ONLY +// ------------------------------------------------------------------------ +#define WRITE_MPTHEADER_envelope_member(envType,envField,type,code) \ + {\ + const InstrumentEnvelope &env = input->GetEnvelope(envType); \ + static_assert(sizeof(type) == sizeof(env[0]. envField)); \ + fcode = code;\ + fsize = mpt::saturate_cast<int16>(sizeof( type ) * env.size());\ + MPT_ASSERT(size_t(fsize) == sizeof( type ) * env.size()); \ + \ + if(writeAll) \ + { \ + mpt::IO::WriteIntLE<uint32>(file, fcode); \ + mpt::IO::WriteIntLE<uint16>(file, fsize); \ + } else if(only_this_code == fcode)\ + { \ + fsize = fixedsize; /* just trust the size we got passed */ \ + } \ + if(only_this_code == fcode || only_this_code == Util::MaxValueOfType(only_this_code)) \ + { \ + uint32 maxNodes = std::min(static_cast<uint32>(fsize/sizeof(type)), static_cast<uint32>(env.size())); \ + for(uint32 i = 0; i < maxNodes; ++i) \ + { \ + type tmp; \ + tmp = env[i]. envField ; \ + mpt::IO::WriteIntLE(file, tmp); \ + } \ + /* Not every instrument's envelope will be the same length. fill up with zeros. */ \ + for(uint32 i = maxNodes; i < fsize/sizeof(type); ++i) \ + { \ + type tmp = 0; \ + mpt::IO::WriteIntLE(file, tmp); \ + } \ + } \ + }\ +/**/ + + +// Write (in 'file') 'input' ModInstrument with 'code' & 'size' extra field infos for each member +void WriteInstrumentHeaderStructOrField(ModInstrument * input, std::ostream &file, uint32 only_this_code, uint16 fixedsize) +{ + uint32 fcode; + uint16 fsize; + // If true, all extension are written to the file; otherwise only the specified extension is written. + // writeAll is true iff we are saving an instrument (or, hypothetically, the legacy ITP format) + const bool writeAll = only_this_code == Util::MaxValueOfType(only_this_code); + + if(!writeAll) + { + MPT_ASSERT(fixedsize > 0); + } + + // clang-format off + WRITE_MPTHEADER_sized_member( nFadeOut , uint32 , MagicBE("FO..") ) + WRITE_MPTHEADER_sized_member( nPan , uint32 , MagicBE("P...") ) + WRITE_MPTHEADER_sized_member( VolEnv.size() , uint32 , MagicBE("VE..") ) + WRITE_MPTHEADER_sized_member( PanEnv.size() , uint32 , MagicBE("PE..") ) + WRITE_MPTHEADER_sized_member( PitchEnv.size() , uint32 , MagicBE("PiE.") ) + WRITE_MPTHEADER_sized_member( wMidiBank , uint16 , MagicBE("MB..") ) + WRITE_MPTHEADER_sized_member( nMidiProgram , uint8 , MagicBE("MP..") ) + WRITE_MPTHEADER_sized_member( nMidiChannel , uint8 , MagicBE("MC..") ) + WRITE_MPTHEADER_envelope_member( ENV_VOLUME , tick , uint16 , MagicBE("VP[.") ) + WRITE_MPTHEADER_envelope_member( ENV_PANNING , tick , uint16 , MagicBE("PP[.") ) + WRITE_MPTHEADER_envelope_member( ENV_PITCH , tick , uint16 , MagicBE("PiP[") ) + WRITE_MPTHEADER_envelope_member( ENV_VOLUME , value , uint8 , MagicBE("VE[.") ) + WRITE_MPTHEADER_envelope_member( ENV_PANNING , value , uint8 , MagicBE("PE[.") ) + WRITE_MPTHEADER_envelope_member( ENV_PITCH , value , uint8 , MagicBE("PiE[") ) + WRITE_MPTHEADER_sized_member( nMixPlug , uint8 , MagicBE("MiP.") ) + WRITE_MPTHEADER_sized_member( nVolRampUp , uint16 , MagicBE("VR..") ) + WRITE_MPTHEADER_sized_member( resampling , uint8 , MagicBE("R...") ) + WRITE_MPTHEADER_sized_member( nCutSwing , uint8 , MagicBE("CS..") ) + WRITE_MPTHEADER_sized_member( nResSwing , uint8 , MagicBE("RS..") ) + WRITE_MPTHEADER_sized_member( filterMode , uint8 , MagicBE("FM..") ) + WRITE_MPTHEADER_sized_member( pluginVelocityHandling , uint8 , MagicBE("PVEH") ) + WRITE_MPTHEADER_sized_member( pluginVolumeHandling , uint8 , MagicBE("PVOH") ) + WRITE_MPTHEADER_trunc_member( pitchToTempoLock.GetInt() , uint16 , MagicBE("PTTL") ) + WRITE_MPTHEADER_trunc_member( pitchToTempoLock.GetFract() , uint16 , MagicLE("PTTF") ) + WRITE_MPTHEADER_sized_member( PitchEnv.nReleaseNode , uint8 , MagicBE("PERN") ) + WRITE_MPTHEADER_sized_member( PanEnv.nReleaseNode , uint8 , MagicBE("AERN") ) + WRITE_MPTHEADER_sized_member( VolEnv.nReleaseNode , uint8 , MagicBE("VERN") ) + WRITE_MPTHEADER_sized_member( PitchEnv.dwFlags , uint8 , MagicBE("PFLG") ) + WRITE_MPTHEADER_sized_member( PanEnv.dwFlags , uint8 , MagicBE("AFLG") ) + WRITE_MPTHEADER_sized_member( VolEnv.dwFlags , uint8 , MagicBE("VFLG") ) + WRITE_MPTHEADER_sized_member( midiPWD , int8 , MagicBE("MPWD") ) + // clang-format on + +} + + +template<typename TIns, typename PropType> +static bool IsPropertyNeeded(const TIns &Instruments, PropType ModInstrument::*Prop) +{ + const ModInstrument defaultIns; + for(const auto &ins : Instruments) + { + if(ins != nullptr && defaultIns.*Prop != ins->*Prop) + return true; + } + return false; +} + + +template<typename PropType> +static void WritePropertyIfNeeded(const CSoundFile &sndFile, PropType ModInstrument::*Prop, uint32 code, uint16 size, std::ostream &f, INSTRUMENTINDEX numInstruments) +{ + if(IsPropertyNeeded(sndFile.Instruments, Prop)) + { + sndFile.WriteInstrumentPropertyForAllInstruments(code, size, f, numInstruments); + } +} + + +// Used only when saving IT, XM and MPTM. +// ITI, ITP saves using Ericus' macros etc... +// The reason is that ITs and XMs save [code][size][ins1.Value][ins2.Value]... +// whereas ITP saves [code][size][ins1.Value][code][size][ins2.Value]... +// too late to turn back.... +void CSoundFile::SaveExtendedInstrumentProperties(INSTRUMENTINDEX numInstruments, std::ostream &f) const +{ + uint32 code = MagicBE("MPTX"); // write extension header code + mpt::IO::WriteIntLE<uint32>(f, code); + + if (numInstruments == 0) + return; + + WritePropertyIfNeeded(*this, &ModInstrument::nVolRampUp, MagicBE("VR.."), sizeof(ModInstrument::nVolRampUp), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::nMixPlug, MagicBE("MiP."), sizeof(ModInstrument::nMixPlug), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::nMidiChannel, MagicBE("MC.."), sizeof(ModInstrument::nMidiChannel), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::nMidiProgram, MagicBE("MP.."), sizeof(ModInstrument::nMidiProgram), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::wMidiBank, MagicBE("MB.."), sizeof(ModInstrument::wMidiBank), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::resampling, MagicBE("R..."), sizeof(ModInstrument::resampling), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::pluginVelocityHandling, MagicBE("PVEH"), sizeof(ModInstrument::pluginVelocityHandling), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::pluginVolumeHandling, MagicBE("PVOH"), sizeof(ModInstrument::pluginVolumeHandling), f, numInstruments); + + if(!(GetType() & MOD_TYPE_XM)) + { + // XM instrument headers already stores full-precision fade-out + WritePropertyIfNeeded(*this, &ModInstrument::nFadeOut, MagicBE("FO.."), sizeof(ModInstrument::nFadeOut), f, numInstruments); + // XM instrument headers already have support for this + WritePropertyIfNeeded(*this, &ModInstrument::midiPWD, MagicBE("MPWD"), sizeof(ModInstrument::midiPWD), f, numInstruments); + // We never supported these as hacks in XM (luckily!) + WritePropertyIfNeeded(*this, &ModInstrument::nPan, MagicBE("P..."), sizeof(ModInstrument::nPan), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::nCutSwing, MagicBE("CS.."), sizeof(ModInstrument::nCutSwing), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::nResSwing, MagicBE("RS.."), sizeof(ModInstrument::nResSwing), f, numInstruments); + WritePropertyIfNeeded(*this, &ModInstrument::filterMode, MagicBE("FM.."), sizeof(ModInstrument::filterMode), f, numInstruments); + if(IsPropertyNeeded(Instruments, &ModInstrument::pitchToTempoLock)) + { + WriteInstrumentPropertyForAllInstruments(MagicBE("PTTL"), sizeof(uint16), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicLE("PTTF"), sizeof(uint16), f, numInstruments); + } + } + + if(GetType() & MOD_TYPE_MPT) + { + uint32 maxNodes[3] = { 0, 0, 0 }; + bool hasReleaseNode[3] = { false, false, false }; + for(INSTRUMENTINDEX i = 1; i <= numInstruments; i++) if(Instruments[i] != nullptr) + { + maxNodes[0] = std::max(maxNodes[0], Instruments[i]->VolEnv.size()); + maxNodes[1] = std::max(maxNodes[1], Instruments[i]->PanEnv.size()); + maxNodes[2] = std::max(maxNodes[2], Instruments[i]->PitchEnv.size()); + hasReleaseNode[0] |= (Instruments[i]->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET); + hasReleaseNode[1] |= (Instruments[i]->PanEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET); + hasReleaseNode[2] |= (Instruments[i]->PitchEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET); + } + // write full envelope information for MPTM files (more env points) + if(maxNodes[0] > 25) + { + WriteInstrumentPropertyForAllInstruments(MagicBE("VE.."), sizeof(ModInstrument::VolEnv.size()), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("VP[."), static_cast<uint16>(maxNodes[0] * sizeof(EnvelopeNode::tick)), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("VE[."), static_cast<uint16>(maxNodes[0] * sizeof(EnvelopeNode::value)), f, numInstruments); + } + if(maxNodes[1] > 25) + { + WriteInstrumentPropertyForAllInstruments(MagicBE("PE.."), sizeof(ModInstrument::PanEnv.size()), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("PP[."), static_cast<uint16>(maxNodes[1] * sizeof(EnvelopeNode::tick)), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("PE[."), static_cast<uint16>(maxNodes[1] * sizeof(EnvelopeNode::value)), f, numInstruments); + } + if(maxNodes[2] > 25) + { + WriteInstrumentPropertyForAllInstruments(MagicBE("PiE."), sizeof(ModInstrument::PitchEnv.size()), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("PiP["), static_cast<uint16>(maxNodes[2] * sizeof(EnvelopeNode::tick)), f, numInstruments); + WriteInstrumentPropertyForAllInstruments(MagicBE("PiE["), static_cast<uint16>(maxNodes[2] * sizeof(EnvelopeNode::value)), f, numInstruments); + } + if(hasReleaseNode[0]) + WriteInstrumentPropertyForAllInstruments(MagicBE("VERN"), sizeof(ModInstrument::VolEnv.nReleaseNode), f, numInstruments); + if(hasReleaseNode[1]) + WriteInstrumentPropertyForAllInstruments(MagicBE("AERN"), sizeof(ModInstrument::PanEnv.nReleaseNode), f, numInstruments); + if(hasReleaseNode[2]) + WriteInstrumentPropertyForAllInstruments(MagicBE("PERN"), sizeof(ModInstrument::PitchEnv.nReleaseNode), f, numInstruments); + } +} + +void CSoundFile::WriteInstrumentPropertyForAllInstruments(uint32 code, uint16 size, std::ostream &f, INSTRUMENTINDEX nInstruments) const +{ + mpt::IO::WriteIntLE<uint32>(f, code); //write code + mpt::IO::WriteIntLE<uint16>(f, size); //write size + for(INSTRUMENTINDEX i = 1; i <= nInstruments; i++) //for all instruments... + { + if (Instruments[i]) + { + WriteInstrumentHeaderStructOrField(Instruments[i], f, code, size); + } else + { + ModInstrument emptyInstrument; + WriteInstrumentHeaderStructOrField(&emptyInstrument, f, code, size); + } + } +} + + +#endif // !MODPLUG_NO_FILESAVE + + +// -------------------------------------------------------------------------------------------- +// Convenient macro to help GET_HEADER declaration for single type members ONLY (non-array) +// -------------------------------------------------------------------------------------------- +#define GET_MPTHEADER_sized_member(name,type,code) \ + case code: \ + {\ + if( fsize <= sizeof( type ) ) \ + { \ + /* hackish workaround to resolve mismatched size values: */ \ + /* nResampling was a long time declared as uint32 but these macro tables used uint16 and UINT. */ \ + /* This worked fine on little-endian, on big-endian not so much. Thus support reading size-mismatched fields. */ \ + if(file.CanRead(fsize)) \ + { \ + type tmp; \ + tmp = file.ReadTruncatedIntLE<type>(fsize); \ + static_assert(sizeof(tmp) == sizeof(input-> name )); \ + input-> name = decltype(input-> name )(tmp); \ + result = true; \ + } \ + } \ + } break; + +// -------------------------------------------------------------------------------------------- +// Convenient macro to help GET_HEADER declaration for array members ONLY +// -------------------------------------------------------------------------------------------- +#define GET_MPTHEADER_array_member(name,type,code) \ + case code: \ + {\ + if( fsize <= sizeof( type ) * std::size(input-> name) ) \ + { \ + FileReader arrayChunk = file.ReadChunk(fsize); \ + for(std::size_t i = 0; i < std::size(input-> name); ++i) \ + { \ + input-> name [i] = arrayChunk.ReadIntLE<type>(); \ + } \ + result = true; \ + } \ + } break; + +// -------------------------------------------------------------------------------------------- +// Convenient macro to help GET_HEADER declaration for character buffer members ONLY +// -------------------------------------------------------------------------------------------- +#define GET_MPTHEADER_charbuf_member(name,type,code) \ + case code: \ + {\ + if( fsize <= sizeof( type ) * input-> name .static_length() ) \ + { \ + FileReader arrayChunk = file.ReadChunk(fsize); \ + std::string tmp; \ + for(std::size_t i = 0; i < fsize; ++i) \ + { \ + tmp += arrayChunk.ReadChar(); \ + } \ + input-> name = tmp; \ + result = true; \ + } \ + } break; + +// -------------------------------------------------------------------------------------------- +// Convenient macro to help GET_HEADER declaration for envelope tick/value members +// -------------------------------------------------------------------------------------------- +#define GET_MPTHEADER_envelope_member(envType,envField,type,code) \ + case code: \ + {\ + FileReader arrayChunk = file.ReadChunk(fsize); \ + InstrumentEnvelope &env = input->GetEnvelope(envType); \ + for(uint32 i = 0; i < env.size(); i++) \ + { \ + env[i]. envField = arrayChunk.ReadIntLE<type>(); \ + } \ + result = true; \ + } break; + + +// Return a pointer on the wanted field in 'input' ModInstrument given field code & size +bool ReadInstrumentHeaderField(ModInstrument *input, uint32 fcode, uint16 fsize, FileReader &file) +{ + if(input == nullptr) return false; + + bool result = false; + + // Members which can be found in this table but not in the write table are only required in the legacy ITP format. + switch(fcode) + { + // clang-format off + GET_MPTHEADER_sized_member( nFadeOut , uint32 , MagicBE("FO..") ) + GET_MPTHEADER_sized_member( dwFlags , uint8 , MagicBE("dF..") ) + GET_MPTHEADER_sized_member( nGlobalVol , uint32 , MagicBE("GV..") ) + GET_MPTHEADER_sized_member( nPan , uint32 , MagicBE("P...") ) + GET_MPTHEADER_sized_member( VolEnv.nLoopStart , uint8 , MagicBE("VLS.") ) + GET_MPTHEADER_sized_member( VolEnv.nLoopEnd , uint8 , MagicBE("VLE.") ) + GET_MPTHEADER_sized_member( VolEnv.nSustainStart , uint8 , MagicBE("VSB.") ) + GET_MPTHEADER_sized_member( VolEnv.nSustainEnd , uint8 , MagicBE("VSE.") ) + GET_MPTHEADER_sized_member( PanEnv.nLoopStart , uint8 , MagicBE("PLS.") ) + GET_MPTHEADER_sized_member( PanEnv.nLoopEnd , uint8 , MagicBE("PLE.") ) + GET_MPTHEADER_sized_member( PanEnv.nSustainStart , uint8 , MagicBE("PSB.") ) + GET_MPTHEADER_sized_member( PanEnv.nSustainEnd , uint8 , MagicBE("PSE.") ) + GET_MPTHEADER_sized_member( PitchEnv.nLoopStart , uint8 , MagicBE("PiLS") ) + GET_MPTHEADER_sized_member( PitchEnv.nLoopEnd , uint8 , MagicBE("PiLE") ) + GET_MPTHEADER_sized_member( PitchEnv.nSustainStart , uint8 , MagicBE("PiSB") ) + GET_MPTHEADER_sized_member( PitchEnv.nSustainEnd , uint8 , MagicBE("PiSE") ) + GET_MPTHEADER_sized_member( nNNA , uint8 , MagicBE("NNA.") ) + GET_MPTHEADER_sized_member( nDCT , uint8 , MagicBE("DCT.") ) + GET_MPTHEADER_sized_member( nDNA , uint8 , MagicBE("DNA.") ) + GET_MPTHEADER_sized_member( nPanSwing , uint8 , MagicBE("PS..") ) + GET_MPTHEADER_sized_member( nVolSwing , uint8 , MagicBE("VS..") ) + GET_MPTHEADER_sized_member( nIFC , uint8 , MagicBE("IFC.") ) + GET_MPTHEADER_sized_member( nIFR , uint8 , MagicBE("IFR.") ) + GET_MPTHEADER_sized_member( wMidiBank , uint16 , MagicBE("MB..") ) + GET_MPTHEADER_sized_member( nMidiProgram , uint8 , MagicBE("MP..") ) + GET_MPTHEADER_sized_member( nMidiChannel , uint8 , MagicBE("MC..") ) + GET_MPTHEADER_sized_member( nPPS , int8 , MagicBE("PPS.") ) + GET_MPTHEADER_sized_member( nPPC , uint8 , MagicBE("PPC.") ) + GET_MPTHEADER_envelope_member(ENV_VOLUME , tick , uint16 , MagicBE("VP[.") ) + GET_MPTHEADER_envelope_member(ENV_PANNING , tick , uint16 , MagicBE("PP[.") ) + GET_MPTHEADER_envelope_member(ENV_PITCH , tick , uint16 , MagicBE("PiP[") ) + GET_MPTHEADER_envelope_member(ENV_VOLUME , value , uint8 , MagicBE("VE[.") ) + GET_MPTHEADER_envelope_member(ENV_PANNING , value , uint8 , MagicBE("PE[.") ) + GET_MPTHEADER_envelope_member(ENV_PITCH , value , uint8 , MagicBE("PiE[") ) + GET_MPTHEADER_array_member( NoteMap , uint8 , MagicBE("NM[.") ) + GET_MPTHEADER_array_member( Keyboard , uint16 , MagicBE("K[..") ) + GET_MPTHEADER_charbuf_member( name , char , MagicBE("n[..") ) + GET_MPTHEADER_charbuf_member( filename , char , MagicBE("fn[.") ) + GET_MPTHEADER_sized_member( nMixPlug , uint8 , MagicBE("MiP.") ) + GET_MPTHEADER_sized_member( nVolRampUp , uint16 , MagicBE("VR..") ) + GET_MPTHEADER_sized_member( nCutSwing , uint8 , MagicBE("CS..") ) + GET_MPTHEADER_sized_member( nResSwing , uint8 , MagicBE("RS..") ) + GET_MPTHEADER_sized_member( filterMode , uint8 , MagicBE("FM..") ) + GET_MPTHEADER_sized_member( pluginVelocityHandling , uint8 , MagicBE("PVEH") ) + GET_MPTHEADER_sized_member( pluginVolumeHandling , uint8 , MagicBE("PVOH") ) + GET_MPTHEADER_sized_member( PitchEnv.nReleaseNode , uint8 , MagicBE("PERN") ) + GET_MPTHEADER_sized_member( PanEnv.nReleaseNode , uint8 , MagicBE("AERN") ) + GET_MPTHEADER_sized_member( VolEnv.nReleaseNode , uint8 , MagicBE("VERN") ) + GET_MPTHEADER_sized_member( PitchEnv.dwFlags , uint8 , MagicBE("PFLG") ) + GET_MPTHEADER_sized_member( PanEnv.dwFlags , uint8 , MagicBE("AFLG") ) + GET_MPTHEADER_sized_member( VolEnv.dwFlags , uint8 , MagicBE("VFLG") ) + GET_MPTHEADER_sized_member( midiPWD , int8 , MagicBE("MPWD") ) + // clang-format on + case MagicBE("R..."): + { + // Resampling has been written as various sizes including uint16 and uint32 in the past + uint32 tmp = file.ReadSizedIntLE<uint32>(fsize); + if(Resampling::IsKnownMode(tmp)) + input->resampling = static_cast<ResamplingMode>(tmp); + result = true; + } break; + case MagicBE("PTTL"): + { + // Integer part of pitch/tempo lock + uint16 tmp = file.ReadSizedIntLE<uint16>(fsize); + input->pitchToTempoLock.Set(tmp, input->pitchToTempoLock.GetFract()); + result = true; + } break; + case MagicLE("PTTF"): + { + // Fractional part of pitch/tempo lock + uint16 tmp = file.ReadSizedIntLE<uint16>(fsize); + input->pitchToTempoLock.Set(input->pitchToTempoLock.GetInt(), tmp); + result = true; + } break; + case MagicBE("VE.."): + input->VolEnv.resize(std::min(uint32(MAX_ENVPOINTS), file.ReadSizedIntLE<uint32>(fsize))); + result = true; + break; + case MagicBE("PE.."): + input->PanEnv.resize(std::min(uint32(MAX_ENVPOINTS), file.ReadSizedIntLE<uint32>(fsize))); + result = true; + break; + case MagicBE("PiE."): + input->PitchEnv.resize(std::min(uint32(MAX_ENVPOINTS), file.ReadSizedIntLE<uint32>(fsize))); + result = true; + break; + } + + return result; +} + + +// Convert instrument flags which were read from 'dF..' extension to proper internal representation. +static void ConvertReadExtendedFlags(ModInstrument *pIns) +{ + // Flags of 'dF..' datafield in extended instrument properties. + enum + { + dFdd_VOLUME = 0x0001, + dFdd_VOLSUSTAIN = 0x0002, + dFdd_VOLLOOP = 0x0004, + dFdd_PANNING = 0x0008, + dFdd_PANSUSTAIN = 0x0010, + dFdd_PANLOOP = 0x0020, + dFdd_PITCH = 0x0040, + dFdd_PITCHSUSTAIN = 0x0080, + dFdd_PITCHLOOP = 0x0100, + dFdd_SETPANNING = 0x0200, + dFdd_FILTER = 0x0400, + dFdd_VOLCARRY = 0x0800, + dFdd_PANCARRY = 0x1000, + dFdd_PITCHCARRY = 0x2000, + dFdd_MUTE = 0x4000, + }; + + const uint32 dwOldFlags = pIns->dwFlags.GetRaw(); + + pIns->VolEnv.dwFlags.set(ENV_ENABLED, (dwOldFlags & dFdd_VOLUME) != 0); + pIns->VolEnv.dwFlags.set(ENV_SUSTAIN, (dwOldFlags & dFdd_VOLSUSTAIN) != 0); + pIns->VolEnv.dwFlags.set(ENV_LOOP, (dwOldFlags & dFdd_VOLLOOP) != 0); + pIns->VolEnv.dwFlags.set(ENV_CARRY, (dwOldFlags & dFdd_VOLCARRY) != 0); + + pIns->PanEnv.dwFlags.set(ENV_ENABLED, (dwOldFlags & dFdd_PANNING) != 0); + pIns->PanEnv.dwFlags.set(ENV_SUSTAIN, (dwOldFlags & dFdd_PANSUSTAIN) != 0); + pIns->PanEnv.dwFlags.set(ENV_LOOP, (dwOldFlags & dFdd_PANLOOP) != 0); + pIns->PanEnv.dwFlags.set(ENV_CARRY, (dwOldFlags & dFdd_PANCARRY) != 0); + + pIns->PitchEnv.dwFlags.set(ENV_ENABLED, (dwOldFlags & dFdd_PITCH) != 0); + pIns->PitchEnv.dwFlags.set(ENV_SUSTAIN, (dwOldFlags & dFdd_PITCHSUSTAIN) != 0); + pIns->PitchEnv.dwFlags.set(ENV_LOOP, (dwOldFlags & dFdd_PITCHLOOP) != 0); + pIns->PitchEnv.dwFlags.set(ENV_CARRY, (dwOldFlags & dFdd_PITCHCARRY) != 0); + pIns->PitchEnv.dwFlags.set(ENV_FILTER, (dwOldFlags & dFdd_FILTER) != 0); + + pIns->dwFlags.reset(); + pIns->dwFlags.set(INS_SETPANNING, (dwOldFlags & dFdd_SETPANNING) != 0); + pIns->dwFlags.set(INS_MUTE, (dwOldFlags & dFdd_MUTE) != 0); +} + + +void ReadInstrumentExtensionField(ModInstrument* pIns, const uint32 code, const uint16 size, FileReader &file) +{ + if(code == MagicBE("K[..")) + { + // skip keyboard mapping + file.Skip(size); + return; + } + + bool success = ReadInstrumentHeaderField(pIns, code, size, file); + + if(!success) + { + file.Skip(size); + return; + } + + if(code == MagicBE("dF..")) // 'dF..' field requires additional processing. + ConvertReadExtendedFlags(pIns); +} + + +void ReadExtendedInstrumentProperty(ModInstrument* pIns, const uint32 code, FileReader &file) +{ + uint16 size = file.ReadUint16LE(); + if(!file.CanRead(size)) + { + return; + } + ReadInstrumentExtensionField(pIns, code, size, file); +} + + +void ReadExtendedInstrumentProperties(ModInstrument* pIns, FileReader &file) +{ + if(!file.ReadMagic("XTPM")) // 'MPTX' + { + return; + } + + while(file.CanRead(7)) + { + ReadExtendedInstrumentProperty(pIns, file.ReadUint32LE(), file); + } +} + + +bool CSoundFile::LoadExtendedInstrumentProperties(FileReader &file) +{ + if(!file.ReadMagic("XTPM")) // 'MPTX' + { + return false; + } + + while(file.CanRead(6)) + { + uint32 code = file.ReadUint32LE(); + + if(code == MagicBE("MPTS") // Reached song extensions, break out of this loop + || code == MagicLE("228\x04") // Reached MPTM extensions (in case there are no song extensions) + || (code & 0x80808080) || !(code & 0x60606060)) // Non-ASCII chunk ID + { + file.SkipBack(4); + break; + } + + // Read size of this property for *one* instrument + const uint16 size = file.ReadUint16LE(); + + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(Instruments[i]) + { + ReadInstrumentExtensionField(Instruments[i], code, size, file); + } + } + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/IntMixer.h b/Src/external_dependencies/openmpt-trunk/soundlib/IntMixer.h new file mode 100644 index 00000000..e1376219 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/IntMixer.h @@ -0,0 +1,398 @@ +/* + * IntMixer.h + * ---------- + * Purpose: Fixed point mixer classes + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Resampler.h" +#include "MixerInterface.h" +#include "Paula.h" + +OPENMPT_NAMESPACE_BEGIN + +template<int channelsOut, int channelsIn, typename out, typename in, size_t mixPrecision> +struct IntToIntTraits : public MixerTraits<channelsOut, channelsIn, out, in> +{ + typedef MixerTraits<channelsOut, channelsIn, out, in> base_t; + typedef typename base_t::input_t input_t; + typedef typename base_t::output_t output_t; + + static MPT_CONSTEXPRINLINE output_t Convert(const input_t x) + { + static_assert(std::numeric_limits<input_t>::is_integer, "Input must be integer"); + static_assert(std::numeric_limits<output_t>::is_integer, "Output must be integer"); + static_assert(sizeof(out) * 8 >= mixPrecision, "Mix precision is higher than output type can handle"); + static_assert(sizeof(in) * 8 <= mixPrecision, "Mix precision is lower than input type"); + return static_cast<output_t>(x) * (1<<(mixPrecision - sizeof(in) * 8)); + } +}; + +typedef IntToIntTraits<2, 1, mixsample_t, int8, 16> Int8MToIntS; +typedef IntToIntTraits<2, 1, mixsample_t, int16, 16> Int16MToIntS; +typedef IntToIntTraits<2, 2, mixsample_t, int8, 16> Int8SToIntS; +typedef IntToIntTraits<2, 2, mixsample_t, int16, 16> Int16SToIntS; + + +////////////////////////////////////////////////////////////////////////// +// Interpolation templates + + +template<class Traits> +struct AmigaBlepInterpolation +{ + SamplePosition subIncrement; + Paula::State &paula; + const Paula::BlepArray &WinSincIntegral; + const int numSteps; + unsigned int remainingSamples = 0; + + MPT_FORCEINLINE AmigaBlepInterpolation(ModChannel &chn, const CResampler &resampler, unsigned int numSamples) + : paula{chn.paulaState} + , WinSincIntegral{resampler.blepTables.GetAmigaTable(resampler.m_Settings.emulateAmiga, chn.dwFlags[CHN_AMIGAFILTER])} + , numSteps{chn.paulaState.numSteps} + { + if(numSteps) + { + subIncrement = chn.increment / numSteps; + // May we read past the start or end of sample if we do partial sample increments? + // If that's the case, don't apply any sub increments on the source sample if we reached the last output sample + // Note that this should only happen with notes well outside the Amiga note range, e.g. in software-mixed formats like MED + const int32 targetPos = (chn.position + chn.increment * numSamples).GetInt(); + if(static_cast<SmpLength>(targetPos) > chn.nLength) + remainingSamples = numSamples; + } + + } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const MPT_RESTRICT inBuffer, const uint32 posLo) + { + if(--remainingSamples == 0) + subIncrement = {}; + + SamplePosition pos(0, posLo); + // First, process steps of full length (one Amiga clock interval) + for(int step = numSteps; step > 0; step--) + { + typename Traits::output_t inSample = 0; + int32 posInt = pos.GetInt() * Traits::numChannelsIn; + for(int32 i = 0; i < Traits::numChannelsIn; i++) + inSample += Traits::Convert(inBuffer[posInt + i]); + paula.InputSample(static_cast<int16>(inSample / (4 * Traits::numChannelsIn))); + paula.Clock(Paula::MINIMUM_INTERVAL); + pos += subIncrement; + } + paula.remainder += paula.stepRemainder; + + // Now, process any remaining integer clock amount < MINIMUM_INTERVAL + uint32 remainClocks = paula.remainder.GetInt(); + if(remainClocks) + { + typename Traits::output_t inSample = 0; + int32 posInt = pos.GetInt() * Traits::numChannelsIn; + for(int32 i = 0; i < Traits::numChannelsIn; i++) + inSample += Traits::Convert(inBuffer[posInt + i]); + paula.InputSample(static_cast<int16>(inSample / (4 * Traits::numChannelsIn))); + paula.Clock(remainClocks); + paula.remainder.RemoveInt(); + } + + auto out = paula.OutputSample(WinSincIntegral); + for(int i = 0; i < Traits::numChannelsOut; i++) + outSample[i] = out; + } +}; + + +template<class Traits> +struct LinearInterpolation +{ + MPT_FORCEINLINE LinearInterpolation(const ModChannel &, const CResampler &, unsigned int) { } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const MPT_RESTRICT inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const typename Traits::output_t fract = posLo >> 18u; + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + typename Traits::output_t srcVol = Traits::Convert(inBuffer[i]); + typename Traits::output_t destVol = Traits::Convert(inBuffer[i + Traits::numChannelsIn]); + + outSample[i] = srcVol + ((fract * (destVol - srcVol)) / 16384); + } + } +}; + + +template<class Traits> +struct FastSincInterpolation +{ + MPT_FORCEINLINE FastSincInterpolation(const ModChannel &, const CResampler &, unsigned int) { } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const MPT_RESTRICT inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const int16 *lut = CResampler::FastSincTable + ((posLo >> 22) & 0x3FC); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = + (lut[0] * Traits::Convert(inBuffer[i - Traits::numChannelsIn]) + + lut[1] * Traits::Convert(inBuffer[i]) + + lut[2] * Traits::Convert(inBuffer[i + Traits::numChannelsIn]) + + lut[3] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn])) / 16384; + } + } +}; + + +template<class Traits> +struct PolyphaseInterpolation +{ + const SINC_TYPE *sinc; + + MPT_FORCEINLINE PolyphaseInterpolation(const ModChannel &chn, const CResampler &resampler, unsigned int) + { + #ifdef MODPLUG_TRACKER + // Otherwise causes "warning C4100: 'resampler' : unreferenced formal parameter" + // because all 3 tables are static members. + // #pragma warning fails with this templated case for unknown reasons. + MPT_UNREFERENCED_PARAMETER(resampler); + #endif // MODPLUG_TRACKER + sinc = (((chn.increment > SamplePosition(0x130000000ll)) || (chn.increment < SamplePosition(-0x130000000ll))) ? + (((chn.increment > SamplePosition(0x180000000ll)) || (chn.increment < SamplePosition(-0x180000000ll))) ? resampler.gDownsample2x : resampler.gDownsample13x) : resampler.gKaiserSinc); + } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const MPT_RESTRICT inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const SINC_TYPE *lut = sinc + ((posLo >> (32 - SINC_PHASES_BITS)) & SINC_MASK) * SINC_WIDTH; + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = + (lut[0] * Traits::Convert(inBuffer[i - 3 * Traits::numChannelsIn]) + + lut[1] * Traits::Convert(inBuffer[i - 2 * Traits::numChannelsIn]) + + lut[2] * Traits::Convert(inBuffer[i - Traits::numChannelsIn]) + + lut[3] * Traits::Convert(inBuffer[i]) + + lut[4] * Traits::Convert(inBuffer[i + Traits::numChannelsIn]) + + lut[5] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn]) + + lut[6] * Traits::Convert(inBuffer[i + 3 * Traits::numChannelsIn]) + + lut[7] * Traits::Convert(inBuffer[i + 4 * Traits::numChannelsIn])) / (1 << SINC_QUANTSHIFT); + } + } +}; + + +template<class Traits> +struct FIRFilterInterpolation +{ + const int16 *WFIRlut; + + MPT_FORCEINLINE FIRFilterInterpolation(const ModChannel &, const CResampler &resampler, unsigned int) + { + WFIRlut = resampler.m_WindowedFIR.lut; + } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const MPT_RESTRICT inBuffer, const uint32 posLo) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + const int16 * const lut = WFIRlut + ((((posLo >> 16) + WFIR_FRACHALVE) >> WFIR_FRACSHIFT) & WFIR_FRACMASK); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + typename Traits::output_t vol1 = + (lut[0] * Traits::Convert(inBuffer[i - 3 * Traits::numChannelsIn])) + + (lut[1] * Traits::Convert(inBuffer[i - 2 * Traits::numChannelsIn])) + + (lut[2] * Traits::Convert(inBuffer[i - Traits::numChannelsIn])) + + (lut[3] * Traits::Convert(inBuffer[i])); + typename Traits::output_t vol2 = + (lut[4] * Traits::Convert(inBuffer[i + 1 * Traits::numChannelsIn])) + + (lut[5] * Traits::Convert(inBuffer[i + 2 * Traits::numChannelsIn])) + + (lut[6] * Traits::Convert(inBuffer[i + 3 * Traits::numChannelsIn])) + + (lut[7] * Traits::Convert(inBuffer[i + 4 * Traits::numChannelsIn])); + outSample[i] = ((vol1 / 2) + (vol2 / 2)) / (1 << (WFIR_16BITSHIFT - 1)); + } + } +}; + + +////////////////////////////////////////////////////////////////////////// +// Mixing templates (add sample to stereo mix) + +template<class Traits> +struct NoRamp +{ + typename Traits::output_t lVol, rVol; + + MPT_FORCEINLINE NoRamp(const ModChannel &chn) + { + lVol = chn.leftVol; + rVol = chn.rightVol; + } +}; + + +struct Ramp +{ + ModChannel &channel; + int32 lRamp, rRamp; + + MPT_FORCEINLINE Ramp(ModChannel &chn) + : channel{chn} + { + lRamp = chn.rampLeftVol; + rRamp = chn.rampRightVol; + } + + MPT_FORCEINLINE ~Ramp() + { + channel.rampLeftVol = lRamp; channel.leftVol = lRamp >> VOLUMERAMPPRECISION; + channel.rampRightVol = rRamp; channel.rightVol = rRamp >> VOLUMERAMPPRECISION; + } +}; + + +// Legacy optimization: If chn.nLeftVol == chn.nRightVol, save one multiplication instruction +template<class Traits> +struct MixMonoFastNoRamp : public NoRamp<Traits> +{ + typedef NoRamp<Traits> base_t; + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &, typename Traits::output_t * const MPT_RESTRICT outBuffer) + { + typename Traits::output_t vol = outSample[0] * base_t::lVol; + for(int i = 0; i < Traits::numChannelsOut; i++) + { + outBuffer[i] += vol; + } + } +}; + + +template<class Traits> +struct MixMonoNoRamp : public NoRamp<Traits> +{ + typedef NoRamp<Traits> base_t; + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &, typename Traits::output_t * const MPT_RESTRICT outBuffer) + { + outBuffer[0] += outSample[0] * base_t::lVol; + outBuffer[1] += outSample[0] * base_t::rVol; + } +}; + + +template<class Traits> +struct MixMonoRamp : public Ramp +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &chn, typename Traits::output_t * const MPT_RESTRICT outBuffer) + { + lRamp += chn.leftRamp; + rRamp += chn.rightRamp; + outBuffer[0] += outSample[0] * (lRamp >> VOLUMERAMPPRECISION); + outBuffer[1] += outSample[0] * (rRamp >> VOLUMERAMPPRECISION); + } +}; + + +template<class Traits> +struct MixStereoNoRamp : public NoRamp<Traits> +{ + typedef NoRamp<Traits> base_t; + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &, typename Traits::output_t * const MPT_RESTRICT outBuffer) + { + outBuffer[0] += outSample[0] * base_t::lVol; + outBuffer[1] += outSample[1] * base_t::rVol; + } +}; + + +template<class Traits> +struct MixStereoRamp : public Ramp +{ + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &outSample, const ModChannel &chn, typename Traits::output_t * const MPT_RESTRICT outBuffer) + { + lRamp += chn.leftRamp; + rRamp += chn.rightRamp; + outBuffer[0] += outSample[0] * (lRamp >> VOLUMERAMPPRECISION); + outBuffer[1] += outSample[1] * (rRamp >> VOLUMERAMPPRECISION); + } +}; + + +////////////////////////////////////////////////////////////////////////// +// Filter templates + + +template<class Traits> +struct NoFilter +{ + MPT_FORCEINLINE NoFilter(const ModChannel &) { } + + MPT_FORCEINLINE void operator() (const typename Traits::outbuf_t &, const ModChannel &) { } +}; + + +// Resonant filter +template<class Traits> +struct ResonantFilter +{ + ModChannel &channel; + // Filter history + typename Traits::output_t fy[Traits::numChannelsIn][2]; + + MPT_FORCEINLINE ResonantFilter(ModChannel &chn) + : channel{chn} + { + for(int i = 0; i < Traits::numChannelsIn; i++) + { + fy[i][0] = chn.nFilter_Y[i][0]; + fy[i][1] = chn.nFilter_Y[i][1]; + } + } + + MPT_FORCEINLINE ~ResonantFilter() + { + for(int i = 0; i < Traits::numChannelsIn; i++) + { + channel.nFilter_Y[i][0] = fy[i][0]; + channel.nFilter_Y[i][1] = fy[i][1]; + } + } + + // To avoid a precision loss in the state variables especially with quiet samples at low cutoff and high mix rate, we pre-amplify the sample. +#define MIXING_FILTER_PREAMP 256 + // Filter values are clipped to double the input range +#define ClipFilter(x) Clamp<typename Traits::output_t, typename Traits::output_t>(x, int16_min * 2 * MIXING_FILTER_PREAMP, int16_max * 2 * MIXING_FILTER_PREAMP) + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const ModChannel &chn) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + const auto inputAmp = outSample[i] * MIXING_FILTER_PREAMP; + typename Traits::output_t val = static_cast<typename Traits::output_t>(mpt::rshift_signed( + Util::mul32to64(inputAmp, chn.nFilter_A0) + + Util::mul32to64(ClipFilter(fy[i][0]), chn.nFilter_B0) + + Util::mul32to64(ClipFilter(fy[i][1]), chn.nFilter_B1) + + (1 << (MIXING_FILTER_PRECISION - 1)), MIXING_FILTER_PRECISION)); + fy[i][1] = fy[i][0]; + fy[i][0] = val - (inputAmp & chn.nFilter_HP); + outSample[i] = val / MIXING_FILTER_PREAMP; + } + } + +#undef ClipFilter +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_669.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_669.cpp new file mode 100644 index 00000000..ea39fe62 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_669.cpp @@ -0,0 +1,330 @@ +/* + * Load_669.cpp + * ------------ + * Purpose: 669 Composer / UNIS 669 module loader + * Notes : <opinion humble="false">This is better than Schism's 669 loader</opinion> :) + * (some of this code is "heavily inspired" by Storlek's code from Schism Tracker, and improvements have been made where necessary.) + * 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" + +OPENMPT_NAMESPACE_BEGIN + +struct _669FileHeader +{ + char magic[2]; // 'if' (0x6669, ha ha) or 'JN' + char songMessage[108]; // Song Message + uint8 samples; // number of samples (1-64) + uint8 patterns; // number of patterns (1-128) + uint8 restartPos; + uint8 orders[128]; + uint8 tempoList[128]; + uint8 breaks[128]; +}; + +MPT_BINARY_STRUCT(_669FileHeader, 497) + + +struct _669Sample +{ + char filename[13]; + uint32le length; + uint32le loopStart; + uint32le loopEnd; + + // Convert a 669 sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + + mptSmp.nC5Speed = 8363; + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + + if(mptSmp.nLoopEnd > mptSmp.nLength && mptSmp.nLoopStart == 0) + { + mptSmp.nLoopEnd = 0; + } + if(mptSmp.nLoopEnd != 0) + { + mptSmp.uFlags = CHN_LOOP; + mptSmp.SanitizeLoops(); + } + } +}; + +MPT_BINARY_STRUCT(_669Sample, 25) + + +static bool ValidateHeader(const _669FileHeader &fileHeader) +{ + if((std::memcmp(fileHeader.magic, "if", 2) && std::memcmp(fileHeader.magic, "JN", 2)) + || fileHeader.samples > 64 + || fileHeader.restartPos >= 128 + || fileHeader.patterns > 128) + { + return false; + } + for(std::size_t i = 0; i < std::size(fileHeader.breaks); i++) + { + if(fileHeader.orders[i] >= 128 && fileHeader.orders[i] < 0xFE) + return false; + if(fileHeader.orders[i] < 128 && fileHeader.tempoList[i] == 0) + return false; + if(fileHeader.tempoList[i] > 15) + return false; + if(fileHeader.breaks[i] >= 64) + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const _669FileHeader &fileHeader) +{ + return fileHeader.samples * sizeof(_669Sample) + fileHeader.patterns * 1536u; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeader669(MemoryFileReader file, const uint64 *pfilesize) +{ + _669FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::Read669(FileReader &file, ModLoadingFlags loadFlags) +{ + _669FileHeader fileHeader; + + file.Rewind(); + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + + InitializeGlobals(MOD_TYPE_669); + m_nMinPeriod = 28 << 2; + m_nMaxPeriod = 1712 << 3; + m_nDefaultTempo.Set(78); + m_nDefaultSpeed = 4; + m_nChannels = 8; + m_playBehaviour.set(kPeriodsAreHertz); +#ifdef MODPLUG_TRACKER + // 669 uses frequencies rather than periods, so linear slides mode will sound better in the higher octaves. + //m_SongFlags.set(SONG_LINEARSLIDES); +#endif // MODPLUG_TRACKER + + m_modFormat.formatName = U_("Composer 669"); + m_modFormat.type = U_("669"); + m_modFormat.madeWithTracker = !memcmp(fileHeader.magic, "if", 2) ? UL_("Composer 669") : UL_("UNIS 669"); + m_modFormat.charset = mpt::Charset::CP437; + + m_nSamples = fileHeader.samples; + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + _669Sample sampleHeader; + file.ReadStruct(sampleHeader); + // Since 669 files have very unfortunate magic bytes ("if") and can + // hardly be validated, reject any file with far too big samples. + if(sampleHeader.length >= 0x4000000) + return false; + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename); + } + + // Copy first song message line into song title + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songMessage, 36); + // Song Message + m_songMessage.ReadFixedLineLength(mpt::byte_cast<const std::byte*>(fileHeader.songMessage), 108, 36, 0); + + // Reading Orders + ReadOrderFromArray(Order(), fileHeader.orders, std::size(fileHeader.orders), 0xFF, 0xFE); + if(Order()[fileHeader.restartPos] < fileHeader.patterns) + Order().SetRestartPos(fileHeader.restartPos); + + // Set up panning + for(CHANNELINDEX chn = 0; chn < 8; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = (chn & 1) ? 0xD0 : 0x30; + } + + // Reading Patterns + Patterns.ResizeArray(fileHeader.patterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.patterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + file.Skip(64 * 8 * 3); + continue; + } + + static constexpr ModCommand::COMMAND effTrans[] = + { + CMD_PORTAMENTOUP, // Slide up (param * 80) Hz on every tick + CMD_PORTAMENTODOWN, // Slide down (param * 80) Hz on every tick + CMD_TONEPORTAMENTO, // Slide to note by (param * 40) Hz on every tick + CMD_S3MCMDEX, // Add (param * 80) Hz to sample frequency + CMD_VIBRATO, // Add (param * 669) Hz on every other tick + CMD_SPEED, // Set ticks per row + CMD_PANNINGSLIDE, // Extended UNIS 669 effect + CMD_RETRIG, // Extended UNIS 669 effect + }; + + uint8 effect[8] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }; + for(ROWINDEX row = 0; row < 64; row++) + { + PatternRow m = Patterns[pat].GetRow(row); + + for(CHANNELINDEX chn = 0; chn < 8; chn++, m++) + { + const auto [noteInstr, instrVol, effParam] = file.ReadArray<uint8, 3>(); + + uint8 note = noteInstr >> 2; + uint8 instr = ((noteInstr & 0x03) << 4) | (instrVol >> 4); + uint8 vol = instrVol & 0x0F; + if(noteInstr < 0xFE) + { + m->note = note + 36 + NOTE_MIN; + m->instr = instr + 1; + effect[chn] = 0xFF; + } + if(noteInstr <= 0xFE) + { + m->volcmd = VOLCMD_VOLUME; + m->vol = ((vol * 64 + 8) / 15); + } + + if(effParam != 0xFF) + { + effect[chn] = effParam; + } + if((effParam & 0x0F) == 0 && effParam != 0x30) + { + // A param value of 0 resets the effect. + effect[chn] = 0xFF; + } + if(effect[chn] == 0xFF) + { + continue; + } + + m->param = effect[chn] & 0x0F; + + // Weird stuff happening in corehop.669 with effects > 8... they seem to do the same thing as if the high bit wasn't set, but the sample also behaves strangely. + uint8 command = effect[chn] >> 4; + if(command < static_cast<uint8>(std::size(effTrans))) + { + m->command = effTrans[command]; + } else + { + m->command = CMD_NONE; + continue; + } + + // Fix some commands + switch(command) + { + case 3: + // D - frequency adjust +#ifdef MODPLUG_TRACKER + // Since we convert to S3M, the finetune command will not quite do what we intend to do (it can adjust the frequency upwards and downwards), so try to approximate it using a fine slide. + m->command = CMD_PORTAMENTOUP; + m->param |= 0xF0; +#else + m->param |= 0x20; +#endif + effect[chn] = 0xFF; + break; + + case 4: + // E - frequency vibrato - almost like an arpeggio, but does not arpeggiate by a given note but by a frequency amount. +#ifdef MODPLUG_TRACKER + m->command = CMD_ARPEGGIO; +#endif + m->param |= (m->param << 4); + break; + + case 5: + // F - set tempo + // TODO: param 0 is a "super fast tempo" in Unis 669 mode (?) + effect[chn] = 0xFF; + break; + + case 6: + // G - subcommands (extended) + switch(m->param) + { + case 0: + // balance fine slide left + m->param = 0x4F; + break; + case 1: + // balance fine slide right + m->param = 0xF4; + break; + default: + m->command = CMD_NONE; + } + break; + } + } + } + + // Write pattern break + if(fileHeader.breaks[pat] < 63) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(fileHeader.breaks[pat]).RetryNextRow()); + } + // And of course the speed... + Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, fileHeader.tempoList[pat]).RetryNextRow()); + } + + if(loadFlags & loadSampleData) + { + // Reading Samples + const SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM); + + for(SAMPLEINDEX n = 1; n <= m_nSamples; n++) + { + sampleIO.ReadSample(Samples[n], file); + } + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_amf.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_amf.cpp new file mode 100644 index 00000000..a40b3a18 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_amf.cpp @@ -0,0 +1,703 @@ +/* + * Load_amf.cpp + * ------------ + * Purpose: AMF module loader + * Notes : There are two types of AMF files, the ASYLUM Music Format (used in Crusader: No Remorse and Crusader: No Regret) + * and Advanced Music Format (DSMI / Digital Sound And Music Interface, used in various games such as Pinball World). + * Both module types are handled here. + * 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 <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + +// ASYLUM AMF File Header +struct AsylumFileHeader +{ + char signature[32]; + uint8 defaultSpeed; + uint8 defaultTempo; + uint8 numSamples; + uint8 numPatterns; + uint8 numOrders; + uint8 restartPos; +}; + +MPT_BINARY_STRUCT(AsylumFileHeader, 38) + + +// ASYLUM AMF Sample Header +struct AsylumSampleHeader +{ + char name[22]; + uint8le finetune; + uint8le defaultVolume; + int8le transpose; + uint32le length; + uint32le loopStart; + uint32le loopLength; + + // Convert an AMF sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.nFineTune = MOD2XMFineTune(finetune); + mptSmp.nVolume = std::min(defaultVolume.get(), uint8(64)) * 4u; + mptSmp.RelativeTone = transpose; + mptSmp.nLength = length; + + if(loopLength > 2 && loopStart + loopLength <= length) + { + mptSmp.uFlags.set(CHN_LOOP); + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopStart + loopLength; + } + } +}; + +MPT_BINARY_STRUCT(AsylumSampleHeader, 37) + + +static bool ValidateHeader(const AsylumFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.signature, "ASYLUM Music Format V1.0\0", 25) + || fileHeader.numSamples > 64 + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const AsylumFileHeader &fileHeader) +{ + return 256 + 64 * sizeof(AsylumSampleHeader) + 64 * 4 * 8 * fileHeader.numPatterns; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMF_Asylum(MemoryFileReader file, const uint64 *pfilesize) +{ + AsylumFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadAMF_Asylum(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + AsylumFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_AMF0); + InitializeChannels(); + SetupMODPanning(true); + m_nChannels = 8; + m_nDefaultSpeed = fileHeader.defaultSpeed; + m_nDefaultTempo.Set(fileHeader.defaultTempo); + m_nSamples = fileHeader.numSamples; + if(fileHeader.restartPos < fileHeader.numOrders) + { + Order().SetRestartPos(fileHeader.restartPos); + } + + m_modFormat.formatName = U_("ASYLUM Music Format"); + m_modFormat.type = U_("amf"); + m_modFormat.charset = mpt::Charset::CP437; + + uint8 orders[256]; + file.ReadArray(orders); + ReadOrderFromArray(Order(), orders, fileHeader.numOrders); + + // Read Sample Headers + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + AsylumSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); + } + + file.Skip((64 - fileHeader.numSamples) * sizeof(AsylumSampleHeader)); + + // Read Patterns + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + file.Skip(64 * 4 * 8); + continue; + } + + for(auto &m : Patterns[pat]) + { + const auto [note, instr, command, param] = file.ReadArray<uint8, 4>(); + if(note && note + 12 + NOTE_MIN <= NOTE_MAX) + { + m.note = note + 12 + NOTE_MIN; + } + m.instr = instr; + m.command = command; + m.param = param; + ConvertModCommand(m); +#ifdef MODPLUG_TRACKER + if(m.command == CMD_PANNING8) + { + // Convert 7-bit panning to 8-bit + m.param = mpt::saturate_cast<ModCommand::PARAM>(m.param * 2u); + } +#endif + } + } + + if(loadFlags & loadSampleData) + { + // Read Sample Data + const SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + sampleIO.ReadSample(Samples[smp], file); + } + } + + return true; +} + + +// DSMI AMF File Header +struct AMFFileHeader +{ + char amf[3]; + uint8le version; + char title[32]; + uint8le numSamples; + uint8le numOrders; + uint16le numTracks; + uint8le numChannels; +}; + +MPT_BINARY_STRUCT(AMFFileHeader, 41) + + +// DSMI AMF Sample Header (v1-v9) +struct AMFSampleHeaderOld +{ + uint8le type; + char name[32]; + char filename[13]; + uint32le index; + uint16le length; + uint16le sampleRate; + uint8le volume; + uint16le loopStart; + uint16le loopEnd; + + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + mptSmp.nLength = length; + mptSmp.nC5Speed = sampleRate; + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + if(mptSmp.nLoopEnd == uint16_max) + mptSmp.nLoopStart = mptSmp.nLoopEnd = 0; + else if(type != 0 && mptSmp.nLoopEnd > mptSmp.nLoopStart + 2 && mptSmp.nLoopEnd <= mptSmp.nLength) + mptSmp.uFlags.set(CHN_LOOP); + } +}; + +MPT_BINARY_STRUCT(AMFSampleHeaderOld, 59) + + +// DSMI AMF Sample Header (v10+) +struct AMFSampleHeaderNew +{ + uint8le type; + char name[32]; + char filename[13]; + uint32le index; + uint32le length; + uint16le sampleRate; + uint8le volume; + uint32le loopStart; + uint32le loopEnd; + + void ConvertToMPT(ModSample &mptSmp, bool truncated) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + mptSmp.nLength = length; + mptSmp.nC5Speed = sampleRate; + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + if(truncated && mptSmp.nLoopStart > 0) + mptSmp.nLoopEnd = mptSmp.nLength; + if(type != 0 && mptSmp.nLoopEnd > mptSmp.nLoopStart + 2 && mptSmp.nLoopEnd <= mptSmp.nLength) + mptSmp.uFlags.set(CHN_LOOP); + } + + // Check if sample headers might be truncated + bool IsValid(uint8 numSamples) const + { + return type <= 1 && index <= numSamples && length <= 0x100000 && volume <= 64 && loopStart <= length && loopEnd <= length; + } +}; + +MPT_BINARY_STRUCT(AMFSampleHeaderNew, 65) + + +// Read a single AMF track (channel) into a pattern. +static void AMFReadPattern(CPattern &pattern, CHANNELINDEX chn, FileReader &fileChunk) +{ + fileChunk.Rewind(); + while(fileChunk.CanRead(3)) + { + const auto [row, command, value] = fileChunk.ReadArray<uint8, 3>(); + if(row >= pattern.GetNumRows()) + { + break; + } + + ModCommand &m = *pattern.GetpModCommand(row, chn); + if(command < 0x7F) + { + // Note + Volume + if(command == 0 && value == 0) + { + m.note = NOTE_NOTECUT; + } else + { + m.note = command + NOTE_MIN; + if(value != 0xFF) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = value; + } + } + } else if(command == 0x7F) + { + // Instrument without note retrigger in MOD (no need to do anything here, should be preceded by 0x80 command) + } else if(command == 0x80) + { + // Instrument + m.instr = value + 1; + } else + { + // Effect + static constexpr ModCommand::COMMAND effTrans[] = + { + CMD_NONE, CMD_SPEED, CMD_VOLUMESLIDE, CMD_VOLUME, + CMD_PORTAMENTOUP, CMD_NONE, CMD_TONEPORTAMENTO, CMD_TREMOR, + CMD_ARPEGGIO, CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, + CMD_PATTERNBREAK, CMD_POSITIONJUMP, CMD_NONE, CMD_RETRIG, + CMD_OFFSET, CMD_VOLUMESLIDE, CMD_PORTAMENTOUP, CMD_S3MCMDEX, + CMD_S3MCMDEX, CMD_TEMPO, CMD_PORTAMENTOUP, CMD_PANNING8, + }; + + uint8 cmd = (command & 0x7F); + uint8 param = value; + + if(cmd < std::size(effTrans)) + cmd = effTrans[cmd]; + else + cmd = CMD_NONE; + + // Fix some commands... + switch(command & 0x7F) + { + // 02: Volume Slide + // 0A: Tone Porta + Vol Slide + // 0B: Vibrato + Vol Slide + case 0x02: + case 0x0A: + case 0x0B: + if(param & 0x80) + param = (-static_cast<int8>(param)) & 0x0F; + else + param = (param & 0x0F) << 4; + break; + + // 03: Volume + case 0x03: + param = std::min(param, uint8(64)); + if(m.volcmd == VOLCMD_NONE || m.volcmd == VOLCMD_VOLUME) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = param; + cmd = CMD_NONE; + } + break; + + // 04: Porta Up/Down + case 0x04: + if(param & 0x80) + param = (-static_cast<int8>(param)) & 0x7F; + else + cmd = CMD_PORTAMENTODOWN; + break; + + // 11: Fine Volume Slide + case 0x11: + if(param) + { + if(param & 0x80) + param = 0xF0 | ((-static_cast<int8>(param)) & 0x0F); + else + param = 0x0F | ((param & 0x0F) << 4); + } else + { + cmd = CMD_NONE; + } + break; + + // 12: Fine Portamento + // 16: Extra Fine Portamento + case 0x12: + case 0x16: + if(param) + { + cmd = static_cast<uint8>((param & 0x80) ? CMD_PORTAMENTOUP : CMD_PORTAMENTODOWN); + if(param & 0x80) + { + param = ((-static_cast<int8>(param)) & 0x0F); + } + param |= (command == 0x16) ? 0xE0 : 0xF0; + } else + { + cmd = CMD_NONE; + } + break; + + // 13: Note Delay + case 0x13: + param = 0xD0 | (param & 0x0F); + break; + + // 14: Note Cut + case 0x14: + param = 0xC0 | (param & 0x0F); + break; + + // 17: Panning + case 0x17: + if(param == 100) + { + // History lesson intermission: According to Otto Chrons, he remembers that he added support + // for 8A4 / XA4 "surround" panning in DMP for MOD and S3M files before any other trackers did, + // So DSMI / DMP are most likely the original source of these 7-bit panning + surround commands! + param = 0xA4; + } else + { + param = static_cast<uint8>(std::clamp(static_cast<int8>(param) + 64, 0, 128)); + if(m.command != CMD_NONE) + { + // Move to volume column if required + if(m.volcmd == VOLCMD_NONE || m.volcmd == VOLCMD_PANNING) + { + m.volcmd = VOLCMD_PANNING; + m.vol = param / 2; + } + cmd = CMD_NONE; + } + } + break; + } + + if(cmd != CMD_NONE) + { + m.command = cmd; + m.param = param; + } + } + } +} + + +static bool ValidateHeader(const AMFFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.amf, "AMF", 3) + || (fileHeader.version < 8 && fileHeader.version != 1) || fileHeader.version > 14 + || ((fileHeader.numChannels < 1 || fileHeader.numChannels > 32) && fileHeader.version >= 9)) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMF_DSMI(MemoryFileReader file, const uint64 *pfilesize) +{ + AMFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadAMF_DSMI(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + AMFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_AMF); + InitializeChannels(); + + m_modFormat.formatName = MPT_UFORMAT("DSMI v{}")(fileHeader.version); + m_modFormat.type = U_("amf"); + m_modFormat.charset = mpt::Charset::CP437; + + m_nChannels = fileHeader.numChannels; + m_nSamples = fileHeader.numSamples; + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.title); + + if(fileHeader.version < 9) + { + // Old format revisions are fixed to 4 channels + m_nChannels = 4; + file.SkipBack(1); + SetupMODPanning(true); + } + + // Setup Channel Pan Positions + if(fileHeader.version >= 11) + { + const CHANNELINDEX readChannels = fileHeader.version >= 12 ? 32 : 16; + for(CHANNELINDEX chn = 0; chn < readChannels; chn++) + { + int8 pan = file.ReadInt8(); + if(pan == 100) + ChnSettings[chn].dwFlags = CHN_SURROUND; + else + ChnSettings[chn].nPan = static_cast<uint16>(std::clamp((pan + 64) * 2, 0, 256)); + } + } else if(fileHeader.version >= 9) + { + uint8 panPos[16]; + file.ReadArray(panPos); + for(CHANNELINDEX chn = 0; chn < 16; chn++) + { + ChnSettings[chn].nPan = (panPos[chn] & 1) ? 0x40 : 0xC0; + } + } + + // Get Tempo/Speed + if(fileHeader.version >= 13) + { + auto [tempo, speed] = file.ReadArray<uint8, 2>(); + if(tempo < 32) + tempo = 125; + m_nDefaultTempo.Set(tempo); + m_nDefaultSpeed = speed; + } else + { + m_nDefaultTempo.Set(125); + m_nDefaultSpeed = 6; + } + + // Setup Order List + Order().resize(fileHeader.numOrders); + std::vector<uint16> patternLength; + const FileReader::off_t trackStartPos = file.GetPosition() + (fileHeader.version >= 14 ? 2 : 0); + if(fileHeader.version >= 14) + { + patternLength.resize(fileHeader.numOrders); + } + + for(ORDERINDEX ord = 0; ord < fileHeader.numOrders; ord++) + { + Order()[ord] = ord; + if(fileHeader.version >= 14) + { + patternLength[ord] = file.ReadUint16LE(); + } + // Track positions will be read as needed. + file.Skip(m_nChannels * 2); + } + + // Read Sample Headers + bool truncatedSampleHeaders = false; + if(fileHeader.version == 10) + { + // M2AMF 1.3 included with DMP 2.32 wrote new (v10+) sample headers, but using the old struct length. + const auto startPos = file.GetPosition(); + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + AMFSampleHeaderNew sample; + if(file.ReadStruct(sample) && !sample.IsValid(fileHeader.numSamples)) + { + truncatedSampleHeaders = true; + break; + } + } + file.Seek(startPos); + } + + std::vector<uint32> sampleMap(GetNumSamples(), 0); + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + if(fileHeader.version < 10) + { + AMFSampleHeaderOld sample; + file.ReadStruct(sample); + sample.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sample.name); + sampleMap[smp - 1] = sample.index; + } else + { + AMFSampleHeaderNew sample; + file.ReadStructPartial(sample, truncatedSampleHeaders ? sizeof(AMFSampleHeaderOld) : sizeof(AMFSampleHeaderNew)); + sample.ConvertToMPT(Samples[smp], truncatedSampleHeaders); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sample.name); + sampleMap[smp - 1] = sample.index; + } + } + + // Read Track Mapping Table + std::vector<uint16le> trackMap; + if(!file.ReadVector(trackMap, fileHeader.numTracks)) + { + return false; + } + uint16 trackCount = 0; + if(!trackMap.empty()) + trackCount = *std::max_element(trackMap.cbegin(), trackMap.cend()); + + // Read pattern tracks + std::vector<FileReader> trackData(trackCount); + for(uint16 i = 0; i < trackCount; i++) + { + // Track size is a 16-Bit value describing the number of byte triplets in this track, followed by a track type byte. + uint16 numEvents = file.ReadUint16LE(); + file.Skip(1); + if(numEvents) + trackData[i] = file.ReadChunk(numEvents * 3 + (fileHeader.version == 1 ? 3 : 0)); + } + + if(loadFlags & loadSampleData) + { + // Read Sample Data + const SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM); + + // Note: in theory a sample can be reused by several instruments and appear in a different order in the file + // However, M2AMF doesn't take advantage of this and just writes instruments in the order they appear, + // without de-duplicating identical sample data. + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples() && file.CanRead(1); smp++) + { + auto startPos = file.GetPosition(); + for(SAMPLEINDEX target = 0; target < GetNumSamples(); target++) + { + if(sampleMap[target] != smp) + continue; + file.Seek(startPos); + sampleIO.ReadSample(Samples[target + 1], file); + } + } + } + + if(!(loadFlags & loadPatternData)) + { + return true; + } + + // Create the patterns from the list of tracks + Patterns.ResizeArray(fileHeader.numOrders); + for(PATTERNINDEX pat = 0; pat < fileHeader.numOrders; pat++) + { + uint16 patLength = pat < patternLength.size() ? patternLength[pat] : 64; + if(!Patterns.Insert(pat, patLength)) + { + continue; + } + + // Get table with per-channel track assignments + file.Seek(trackStartPos + pat * (GetNumChannels() * 2 + (fileHeader.version >= 14 ? 2 : 0))); + std::vector<uint16le> tracks; + if(!file.ReadVector(tracks, GetNumChannels())) + { + continue; + } + + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + if(tracks[chn] > 0 && tracks[chn] <= fileHeader.numTracks) + { + uint16 realTrack = trackMap[tracks[chn] - 1]; + if(realTrack > 0 && realTrack <= trackCount) + { + realTrack--; + AMFReadPattern(Patterns[pat], chn, trackData[realTrack]); + } + } + } + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_ams.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ams.cpp new file mode 100644 index 00000000..f000cdf1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ams.cpp @@ -0,0 +1,1123 @@ +/* + * Load_ams.cpp + * ------------ + * Purpose: AMS (Extreme's Tracker / Velvet Studio) module loader + * Notes : Extreme was renamed to Velvet Development at some point, + * and thus they also renamed their tracker from + * "Extreme's Tracker" to "Velvet Studio". + * While the two programs look rather similiar, the structure of both + * programs' "AMS" format is significantly different in some places - + * Velvet Studio is a rather advanced tracker in comparison to Extreme's Tracker. + * The source code of Velvet Studio has been released into the + * public domain in 2013: https://github.com/Patosc/VelvetStudio/commits/master + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Read AMS or AMS2 (newVersion = true) pattern. At least this part of the format is more or less identical between the two trackers... +static void ReadAMSPattern(CPattern &pattern, bool newVersion, FileReader &patternChunk) +{ + enum + { + emptyRow = 0xFF, // No commands on row + endOfRowMask = 0x80, // If set, no more commands on this row + noteMask = 0x40, // If set, no note+instr in this command + channelMask = 0x1F, // Mask for extracting channel + + // Note flags + readNextCmd = 0x80, // One more command follows + noteDataMask = 0x7F, // Extract note + + // Command flags + volCommand = 0x40, // Effect is compressed volume command + commandMask = 0x3F, // Command or volume mask + }; + + // Effect translation table for extended (non-Protracker) effects + static constexpr ModCommand::COMMAND effTrans[] = + { + CMD_S3MCMDEX, // Forward / Backward + CMD_PORTAMENTOUP, // Extra fine slide up + CMD_PORTAMENTODOWN, // Extra fine slide up + CMD_RETRIG, // Retrigger + CMD_NONE, + CMD_TONEPORTAVOL, // Toneporta with fine volume slide + CMD_VIBRATOVOL, // Vibrato with fine volume slide + CMD_NONE, + CMD_PANNINGSLIDE, + CMD_NONE, + CMD_VOLUMESLIDE, // Two times finder volume slide than Axx + CMD_NONE, + CMD_CHANNELVOLUME, // Channel volume (0...127) + CMD_PATTERNBREAK, // Long pattern break (in hex) + CMD_S3MCMDEX, // Fine slide commands + CMD_NONE, // Fractional BPM + CMD_KEYOFF, // Key off at tick xx + CMD_PORTAMENTOUP, // Porta up, but uses all octaves (?) + CMD_PORTAMENTODOWN, // Porta down, but uses all octaves (?) + CMD_NONE, + CMD_NONE, + CMD_NONE, + CMD_NONE, + CMD_NONE, + CMD_NONE, + CMD_NONE, + CMD_GLOBALVOLSLIDE, // Global volume slide + CMD_NONE, + CMD_GLOBALVOLUME, // Global volume (0... 127) + }; + + ModCommand dummy; + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + PatternRow baseRow = pattern.GetRow(row); + while(patternChunk.CanRead(1)) + { + const uint8 flags = patternChunk.ReadUint8(); + if(flags == emptyRow) + { + break; + } + + const CHANNELINDEX chn = (flags & channelMask); + ModCommand &m = chn < pattern.GetNumChannels() ? baseRow[chn] : dummy; + bool moreCommands = true; + if(!(flags & noteMask)) + { + // Read note + instr + uint8 note = patternChunk.ReadUint8(); + moreCommands = (note & readNextCmd) != 0; + note &= noteDataMask; + + if(note == 1) + { + m.note = NOTE_KEYOFF; + } else if(note >= 2 && note <= 121 && newVersion) + { + m.note = note - 2 + NOTE_MIN; + } else if(note >= 12 && note <= 108 && !newVersion) + { + m.note = note + 12 + NOTE_MIN; + } + m.instr = patternChunk.ReadUint8(); + } + + while(moreCommands) + { + // Read one more effect command + ModCommand origCmd = m; + const uint8 command = patternChunk.ReadUint8(), effect = (command & commandMask); + moreCommands = (command & readNextCmd) != 0; + + if(command & volCommand) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = effect; + } else + { + m.param = patternChunk.ReadUint8(); + + if(effect < 0x10) + { + // PT commands + m.command = effect; + CSoundFile::ConvertModCommand(m); + + // Post-fix some commands + switch(m.command) + { + case CMD_PANNING8: + // 4-Bit panning + m.command = CMD_PANNING8; + m.param = (m.param & 0x0F) * 0x11; + break; + + case CMD_VOLUME: + m.command = CMD_NONE; + m.volcmd = VOLCMD_VOLUME; + m.vol = static_cast<ModCommand::VOL>(std::min((m.param + 1) / 2, 64)); + break; + + case CMD_MODCMDEX: + if(m.param == 0x80) + { + // Break sample loop (cut after loop) + m.command = CMD_NONE; + } else + { + m.ExtendedMODtoS3MEffect(); + } + break; + } + } else if(effect < 0x10 + mpt::array_size<decltype(effTrans)>::size) + { + // Extended commands + m.command = effTrans[effect - 0x10]; + + // Post-fix some commands + switch(effect) + { + case 0x10: + // Play sample forwards / backwards + if(m.param <= 0x01) + { + m.param |= 0x9E; + } else + { + m.command = CMD_NONE; + } + break; + + case 0x11: + case 0x12: + // Extra fine slides + m.param = static_cast<ModCommand::PARAM>(std::min(uint8(0x0F), m.param) | 0xE0); + break; + + case 0x15: + case 0x16: + // Fine slides + m.param = static_cast<ModCommand::PARAM>((std::min(0x10, m.param + 1) / 2) | 0xF0); + break; + + case 0x1E: + // More fine slides + switch(m.param >> 4) + { + case 0x1: + // Fine porta up + m.command = CMD_PORTAMENTOUP; + m.param |= 0xF0; + break; + case 0x2: + // Fine porta down + m.command = CMD_PORTAMENTODOWN; + m.param |= 0xF0; + break; + case 0xA: + // Extra fine volume slide up + m.command = CMD_VOLUMESLIDE; + m.param = ((((m.param & 0x0F) + 1) / 2) << 4) | 0x0F; + break; + case 0xB: + // Extra fine volume slide down + m.command = CMD_VOLUMESLIDE; + m.param = (((m.param & 0x0F) + 1) / 2) | 0xF0; + break; + default: + m.command = CMD_NONE; + break; + } + break; + + case 0x1C: + // Adjust channel volume range + m.param = static_cast<ModCommand::PARAM>(std::min((m.param + 1) / 2, 64)); + break; + } + } + + // Try merging commands first + ModCommand::CombineEffects(m.command, m.param, origCmd.command, origCmd.param); + + if(ModCommand::GetEffectWeight(origCmd.command) > ModCommand::GetEffectWeight(m.command)) + { + if(m.volcmd == VOLCMD_NONE && ModCommand::ConvertVolEffect(m.command, m.param, true)) + { + // Volume column to the rescue! + m.volcmd = m.command; + m.vol = m.param; + } + + m.command = origCmd.command; + m.param = origCmd.param; + } + } + } + + if(flags & endOfRowMask) + { + // End of row + break; + } + } + } +} + + +///////////////////////////////////////////////////////////////////// +// AMS (Extreme's Tracker) 1.x loader + +// AMS File Header +struct AMSFileHeader +{ + uint8le versionLow; + uint8le versionHigh; + uint8le channelConfig; + uint8le numSamps; + uint16le numPats; + uint16le numOrds; + uint8le midiChannels; + uint16le extraSize; +}; + +MPT_BINARY_STRUCT(AMSFileHeader, 11) + + +// AMS Sample Header +struct AMSSampleHeader +{ + enum SampleFlags + { + smp16BitOld = 0x04, // AMS 1.0 (at least according to docs, I yet have to find such a file) + smp16Bit = 0x80, // AMS 1.1+ + smpPacked = 0x03, + }; + + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint8le panFinetune; // High nibble = pan position, low nibble = finetune value + uint16le sampleRate; + uint8le volume; // 0...127 + uint8le flags; // See SampleFlags + + // Convert sample header to OpenMPT's internal format. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + + mptSmp.nLength = length; + mptSmp.nLoopStart = std::min(loopStart, length); + mptSmp.nLoopEnd = std::min(loopEnd, length); + + mptSmp.nVolume = (std::min(uint8(127), volume.get()) * 256 + 64) / 127; + if(panFinetune & 0xF0) + { + mptSmp.nPan = (panFinetune & 0xF0); + mptSmp.uFlags = CHN_PANNING; + } + + mptSmp.nC5Speed = 2 * sampleRate; + if(sampleRate == 0) + { + mptSmp.nC5Speed = 2 * 8363; + } + + uint32 newC4speed = ModSample::TransposeToFrequency(0, MOD2XMFineTune(panFinetune & 0x0F)); + mptSmp.nC5Speed = (mptSmp.nC5Speed * newC4speed) / 8363; + + if(mptSmp.nLoopStart < mptSmp.nLoopEnd) + { + mptSmp.uFlags.set(CHN_LOOP); + } + + if(flags & (smp16Bit | smp16BitOld)) + { + mptSmp.uFlags.set(CHN_16BIT); + } + } +}; + +MPT_BINARY_STRUCT(AMSSampleHeader, 17) + + +static bool ValidateHeader(const AMSFileHeader &fileHeader) +{ + if(fileHeader.versionHigh != 0x01) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const AMSFileHeader &fileHeader) +{ + return fileHeader.extraSize + 3u + fileHeader.numSamps * (1u + sizeof(AMSSampleHeader)) + fileHeader.numOrds * 2u + fileHeader.numPats * 4u; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMS(MemoryFileReader file, const uint64 *pfilesize) +{ + if(!file.CanRead(7)) + { + return ProbeWantMoreData; + } + if(!file.ReadMagic("Extreme")) + { + return ProbeFailure; + } + AMSFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadAMS(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + if(!file.ReadMagic("Extreme")) + { + return false; + } + AMSFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(!file.Skip(fileHeader.extraSize)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_AMS); + + m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; + m_nChannels = (fileHeader.channelConfig & 0x1F) + 1; + m_nSamples = fileHeader.numSamps; + SetupMODPanning(true); + + m_modFormat.formatName = U_("Extreme's Tracker"); + m_modFormat.type = U_("ams"); + m_modFormat.madeWithTracker = MPT_UFORMAT("Extreme's Tracker {}.{}")(fileHeader.versionHigh, fileHeader.versionLow); + m_modFormat.charset = mpt::Charset::CP437; + + std::vector<bool> packSample(fileHeader.numSamps); + + static_assert(MAX_SAMPLES > 255); + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + AMSSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[smp]); + packSample[smp - 1] = (sampleHeader.flags & AMSSampleHeader::smpPacked) != 0; + } + + // Texts + file.ReadSizedString<uint8le, mpt::String::spacePadded>(m_songName); + + // Read sample names + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + file.ReadSizedString<uint8le, mpt::String::spacePadded>(m_szNames[smp]); + } + + // Read channel names + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + ChnSettings[chn].Reset(); + file.ReadSizedString<uint8le, mpt::String::spacePadded>(ChnSettings[chn].szName); + } + + // Read pattern names and create patterns + Patterns.ResizeArray(fileHeader.numPats); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPats; pat++) + { + char name[11]; + const bool ok = file.ReadSizedString<uint8le, mpt::String::spacePadded>(name); + // Create pattern now, so name won't be reset later. + if(Patterns.Insert(pat, 64) && ok) + { + Patterns[pat].SetName(name); + } + } + + // Read packed song message + const uint16 packedLength = file.ReadUint16LE(); + if(packedLength && file.CanRead(packedLength)) + { + std::vector<uint8> textIn; + file.ReadVector(textIn, packedLength); + std::string textOut; + textOut.reserve(packedLength); + + for(auto c : textIn) + { + if(c & 0x80) + { + textOut.insert(textOut.end(), (c & 0x7F), ' '); + } else + { + textOut.push_back(c); + } + } + + textOut = mpt::ToCharset(mpt::Charset::CP437, mpt::Charset::CP437AMS, textOut); + + // Packed text doesn't include any line breaks! + m_songMessage.ReadFixedLineLength(mpt::byte_cast<const std::byte*>(textOut.c_str()), textOut.length(), 76, 0); + } + + // Read Order List + ReadOrderFromFile<uint16le>(Order(), file, fileHeader.numOrds); + + // Read patterns + for(PATTERNINDEX pat = 0; pat < fileHeader.numPats && file.CanRead(4); pat++) + { + uint32 patLength = file.ReadUint32LE(); + FileReader patternChunk = file.ReadChunk(patLength); + + if((loadFlags & loadPatternData) && Patterns.IsValidPat(pat)) + { + ReadAMSPattern(Patterns[pat], false, patternChunk); + } + } + + if(loadFlags & loadSampleData) + { + // Read Samples + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + SampleIO( + Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + packSample[smp - 1] ? SampleIO::AMS : SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + } + } + + return true; +} + + +///////////////////////////////////////////////////////////////////// +// AMS (Velvet Studio) 2.0 - 2.02 loader + +// AMS2 File Header +struct AMS2FileHeader +{ + enum FileFlags + { + linearSlides = 0x40, + }; + + uint8le versionLow; // Version of format (Hi = MainVer, Low = SubVer e.g. 0202 = 2.02) + uint8le versionHigh; // ditto + uint8le numIns; // Nr of Instruments (0-255) + uint16le numPats; // Nr of Patterns (1-1024) + uint16le numOrds; // Nr of Positions (1-65535) + // Rest of header differs between format revision 2.01 and 2.02 +}; + +MPT_BINARY_STRUCT(AMS2FileHeader, 7) + + +// AMS2 Instument Envelope +struct AMS2Envelope +{ + uint8 speed; // Envelope speed (currently not supported, always the same as current BPM) + uint8 sustainPoint; // Envelope sustain point + uint8 loopStart; // Envelope loop Start + uint8 loopEnd; // Envelope loop End + uint8 numPoints; // Envelope length + + // Read envelope and do partial conversion. + void ConvertToMPT(InstrumentEnvelope &mptEnv, FileReader &file) + { + file.ReadStruct(*this); + + // Read envelope points + uint8 data[64][3]; + file.ReadStructPartial(data, numPoints * 3); + + if(numPoints <= 1) + { + // This is not an envelope. + return; + } + + static_assert(MAX_ENVPOINTS >= std::size(data)); + mptEnv.resize(std::min(numPoints, mpt::saturate_cast<uint8>(std::size(data)))); + mptEnv.nLoopStart = loopStart; + mptEnv.nLoopEnd = loopEnd; + mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint; + + for(uint32 i = 0; i < mptEnv.size(); i++) + { + if(i != 0) + { + mptEnv[i].tick = mptEnv[i - 1].tick + static_cast<uint16>(std::max(1, data[i][0] | ((data[i][1] & 0x01) << 8))); + } + mptEnv[i].value = data[i][2]; + } + } +}; + +MPT_BINARY_STRUCT(AMS2Envelope, 5) + + +// AMS2 Instrument Data +struct AMS2Instrument +{ + enum EnvelopeFlags + { + envLoop = 0x01, + envSustain = 0x02, + envEnabled = 0x04, + + // Flag shift amounts + volEnvShift = 0, + panEnvShift = 1, + vibEnvShift = 2, + + vibAmpMask = 0x3000, + vibAmpShift = 12, + fadeOutMask = 0xFFF, + }; + + uint8le shadowInstr; // Shadow Instrument. If non-zero, the value=the shadowed inst. + uint16le vibampFadeout; // Vib.Amplify + Volume fadeout in one variable! + uint16le envFlags; // See EnvelopeFlags + + void ApplyFlags(InstrumentEnvelope &mptEnv, EnvelopeFlags shift) const + { + const int flags = envFlags >> (shift * 3); + mptEnv.dwFlags.set(ENV_ENABLED, (flags & envEnabled) != 0); + mptEnv.dwFlags.set(ENV_LOOP, (flags & envLoop) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & envSustain) != 0); + + // "Break envelope" should stop the envelope loop when encountering a note-off... We can only use the sustain loop to emulate this behaviour. + if(!(flags & envSustain) && (flags & envLoop) != 0 && (flags & (1 << (9 - shift * 2))) != 0) + { + mptEnv.nSustainStart = mptEnv.nLoopStart; + mptEnv.nSustainEnd = mptEnv.nLoopEnd; + mptEnv.dwFlags.set(ENV_SUSTAIN); + mptEnv.dwFlags.reset(ENV_LOOP); + } + } + +}; + +MPT_BINARY_STRUCT(AMS2Instrument, 5) + + +// AMS2 Sample Header +struct AMS2SampleHeader +{ + enum SampleFlags + { + smpPacked = 0x03, + smp16Bit = 0x04, + smpLoop = 0x08, + smpBidiLoop = 0x10, + smpReverse = 0x40, + }; + + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint16le sampledRate; // Whyyyy? + uint8le panFinetune; // High nibble = pan position, low nibble = finetune value + uint16le c4speed; // Why is all of this so redundant? + int8le relativeTone; // q.e.d. + uint8le volume; // 0...127 + uint8le flags; // See SampleFlags + + // Convert sample header to OpenMPT's internal format. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + + mptSmp.nLength = length; + mptSmp.nLoopStart = std::min(loopStart, length); + mptSmp.nLoopEnd = std::min(loopEnd, length); + + mptSmp.nC5Speed = c4speed * 2; + if(c4speed == 0) + { + mptSmp.nC5Speed = 8363 * 2; + } + // Why, oh why, does this format need a c5speed and transpose/finetune at the same time... + uint32 newC4speed = ModSample::TransposeToFrequency(relativeTone, MOD2XMFineTune(panFinetune & 0x0F)); + mptSmp.nC5Speed = (mptSmp.nC5Speed * newC4speed) / 8363; + + mptSmp.nVolume = (std::min(volume.get(), uint8(127)) * 256 + 64) / 127; + if(panFinetune & 0xF0) + { + mptSmp.nPan = (panFinetune & 0xF0); + mptSmp.uFlags = CHN_PANNING; + } + + if(flags & smp16Bit) mptSmp.uFlags.set(CHN_16BIT); + if((flags & smpLoop) && mptSmp.nLoopStart < mptSmp.nLoopEnd) + { + mptSmp.uFlags.set(CHN_LOOP); + if(flags & smpBidiLoop) mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & smpReverse) mptSmp.uFlags.set(CHN_REVERSE); + } + } +}; + +MPT_BINARY_STRUCT(AMS2SampleHeader, 20) + + +// AMS2 Song Description Header +struct AMS2Description +{ + uint32le packedLen; // Including header + uint32le unpackedLen; + uint8le packRoutine; // 01 + uint8le preProcessing; // None! + uint8le packingMethod; // RLE +}; + +MPT_BINARY_STRUCT(AMS2Description, 11) + + +static bool ValidateHeader(const AMS2FileHeader &fileHeader) +{ + if(fileHeader.versionHigh != 2 || fileHeader.versionLow > 2) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const AMS2FileHeader &fileHeader) +{ + return 36u + sizeof(AMS2Description) + fileHeader.numIns * 2u + fileHeader.numOrds * 2u + fileHeader.numPats * 4u; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMS2(MemoryFileReader file, const uint64 *pfilesize) +{ + if(!file.CanRead(7)) + { + return ProbeWantMoreData; + } + if(!file.ReadMagic("AMShdr\x1A")) + { + return ProbeFailure; + } + if(!file.CanRead(1)) + { + return ProbeWantMoreData; + } + const uint8 songNameLength = file.ReadUint8(); + if(!file.Skip(songNameLength)) + { + return ProbeWantMoreData; + } + AMS2FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadAMS2(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + if(!file.ReadMagic("AMShdr\x1A")) + { + return false; + } + std::string songName; + if(!file.ReadSizedString<uint8le, mpt::String::spacePadded>(songName)) + { + return false; + } + AMS2FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_AMS); + + m_songName = songName; + + m_nInstruments = fileHeader.numIns; + m_nChannels = 32; + SetupMODPanning(true); + + m_modFormat.formatName = U_("Velvet Studio"); + m_modFormat.type = U_("ams"); + m_modFormat.madeWithTracker = MPT_UFORMAT("Velvet Studio {}.{}")(fileHeader.versionHigh.get(), mpt::ufmt::dec0<2>(fileHeader.versionLow.get())); + m_modFormat.charset = mpt::Charset::CP437; + + uint16 headerFlags; + if(fileHeader.versionLow >= 2) + { + uint16 tempo = std::max(uint16(32 << 8), file.ReadUint16LE()); // 8.8 tempo + m_nDefaultTempo.SetRaw((tempo * TEMPO::fractFact) >> 8); + m_nDefaultSpeed = std::max(uint8(1), file.ReadUint8()); + file.Skip(3); // Default values for pattern editor + headerFlags = file.ReadUint16LE(); + } else + { + m_nDefaultTempo.Set(std::max(uint8(32), file.ReadUint8())); + m_nDefaultSpeed = std::max(uint8(1), file.ReadUint8()); + headerFlags = file.ReadUint8(); + } + + m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS | ((headerFlags & AMS2FileHeader::linearSlides) ? SONG_LINEARSLIDES : SongFlags(0)); + + // Instruments + std::vector<SAMPLEINDEX> firstSample; // First sample of instrument + std::vector<uint16> sampleSettings; // Shadow sample map... Lo byte = Instrument, Hi byte, lo nibble = Sample index in instrument, Hi byte, hi nibble = Sample pack status + enum + { + instrIndexMask = 0xFF, // Shadow instrument + sampleIndexMask = 0x7F00, // Sample index in instrument + sampleIndexShift = 8, + packStatusMask = 0x8000, // If bit is set, sample is packed + }; + + static_assert(MAX_INSTRUMENTS > 255); + for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) + { + ModInstrument *instrument = AllocateInstrument(ins); + if(instrument == nullptr + || !file.ReadSizedString<uint8le, mpt::String::spacePadded>(instrument->name)) + { + break; + } + + uint8 numSamples = file.ReadUint8(); + uint8 sampleAssignment[120]; + MemsetZero(sampleAssignment); // Only really needed for v2.0, where the lowest and highest octave aren't cleared. + + if(numSamples == 0 + || (fileHeader.versionLow > 0 && !file.ReadArray(sampleAssignment)) // v2.01+: 120 Notes + || (fileHeader.versionLow == 0 && !file.ReadRaw(mpt::span(sampleAssignment + 12, 96)).size())) // v2.0: 96 Notes + { + continue; + } + + static_assert(mpt::array_size<decltype(instrument->Keyboard)>::size >= std::size(sampleAssignment)); + for(size_t i = 0; i < 120; i++) + { + instrument->Keyboard[i] = sampleAssignment[i] + GetNumSamples() + 1; + } + + AMS2Envelope volEnv, panEnv, vibratoEnv; + volEnv.ConvertToMPT(instrument->VolEnv, file); + panEnv.ConvertToMPT(instrument->PanEnv, file); + vibratoEnv.ConvertToMPT(instrument->PitchEnv, file); + + AMS2Instrument instrHeader; + file.ReadStruct(instrHeader); + instrument->nFadeOut = (instrHeader.vibampFadeout & AMS2Instrument::fadeOutMask); + const int16 vibAmp = 1 << ((instrHeader.vibampFadeout & AMS2Instrument::vibAmpMask) >> AMS2Instrument::vibAmpShift); + + instrHeader.ApplyFlags(instrument->VolEnv, AMS2Instrument::volEnvShift); + instrHeader.ApplyFlags(instrument->PanEnv, AMS2Instrument::panEnvShift); + instrHeader.ApplyFlags(instrument->PitchEnv, AMS2Instrument::vibEnvShift); + + // Scale envelopes to correct range + for(auto &p : instrument->VolEnv) + { + p.value = std::min(uint8(ENVELOPE_MAX), static_cast<uint8>((p.value * ENVELOPE_MAX + 64u) / 127u)); + } + for(auto &p : instrument->PanEnv) + { + p.value = std::min(uint8(ENVELOPE_MAX), static_cast<uint8>((p.value * ENVELOPE_MAX + 128u) / 255u)); + } + for(auto &p : instrument->PitchEnv) + { +#ifdef MODPLUG_TRACKER + p.value = std::min(uint8(ENVELOPE_MAX), static_cast<uint8>(32 + Util::muldivrfloor(static_cast<int8>(p.value - 128), vibAmp, 255))); +#else + // Try to keep as much precision as possible... divide by 8 since that's the highest possible vibAmp factor. + p.value = static_cast<uint8>(128 + Util::muldivrfloor(static_cast<int8>(p.value - 128), vibAmp, 8)); +#endif + } + + // Sample headers - we will have to read them even for shadow samples, and we will have to load them several times, + // as it is possible that shadow samples use different sample settings like base frequency or panning. + const SAMPLEINDEX firstSmp = GetNumSamples() + 1; + for(SAMPLEINDEX smp = 0; smp < numSamples; smp++) + { + if(firstSmp + smp >= MAX_SAMPLES) + { + file.Skip(sizeof(AMS2SampleHeader)); + break; + } + file.ReadSizedString<uint8le, mpt::String::spacePadded>(m_szNames[firstSmp + smp]); + + AMS2SampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[firstSmp + smp]); + + uint16 settings = (instrHeader.shadowInstr & instrIndexMask) + | ((smp << sampleIndexShift) & sampleIndexMask) + | ((sampleHeader.flags & AMS2SampleHeader::smpPacked) ? packStatusMask : 0); + sampleSettings.push_back(settings); + } + + firstSample.push_back(firstSmp); + m_nSamples = static_cast<SAMPLEINDEX>(std::min(MAX_SAMPLES - 1, GetNumSamples() + numSamples)); + } + + // Text + + // Read composer name + if(std::string composer; file.ReadSizedString<uint8le, mpt::String::spacePadded>(composer)) + { + m_songArtist = mpt::ToUnicode(mpt::Charset::CP437AMS2, composer); + } + + // Channel names + for(CHANNELINDEX chn = 0; chn < 32; chn++) + { + ChnSettings[chn].Reset(); + file.ReadSizedString<uint8le, mpt::String::spacePadded>(ChnSettings[chn].szName); + } + + // RLE-Packed description text + AMS2Description descriptionHeader; + if(!file.ReadStruct(descriptionHeader)) + { + return true; + } + if(descriptionHeader.packedLen > sizeof(descriptionHeader) && file.CanRead(descriptionHeader.packedLen - sizeof(descriptionHeader))) + { + const uint32 textLength = descriptionHeader.packedLen - static_cast<uint32>(sizeof(descriptionHeader)); + std::vector<uint8> textIn; + file.ReadVector(textIn, textLength); + // In the best case, every byte triplet can decode to 255 bytes, which is a ratio of exactly 1:85 + const uint32 maxLength = std::min(textLength, Util::MaxValueOfType(textLength) / 85u) * 85u; + std::string textOut; + textOut.reserve(std::min(maxLength, descriptionHeader.unpackedLen.get())); + + size_t readLen = 0; + while(readLen < textLength) + { + uint8 c = textIn[readLen++]; + if(c == 0xFF && textLength - readLen >= 2) + { + c = textIn[readLen++]; + uint32 count = textIn[readLen++]; + textOut.insert(textOut.end(), count, c); + } else + { + textOut.push_back(c); + } + } + textOut = mpt::ToCharset(mpt::Charset::CP437, mpt::Charset::CP437AMS2, textOut); + // Packed text doesn't include any line breaks! + m_songMessage.ReadFixedLineLength(mpt::byte_cast<const std::byte*>(textOut.c_str()), textOut.length(), 74, 0); + } + + // Read Order List + ReadOrderFromFile<uint16le>(Order(), file, fileHeader.numOrds); + + // Read Patterns + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.numPats); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPats && file.CanRead(4); pat++) + { + uint32 patLength = file.ReadUint32LE(); + FileReader patternChunk = file.ReadChunk(patLength); + + if(loadFlags & loadPatternData) + { + const ROWINDEX numRows = patternChunk.ReadUint8() + 1; + // We don't need to know the number of channels or commands. + patternChunk.Skip(1); + + if(!Patterns.Insert(pat, numRows)) + { + continue; + } + + char patternName[11]; + if(patternChunk.ReadSizedString<uint8le, mpt::String::spacePadded>(patternName)) + Patterns[pat].SetName(patternName); + + ReadAMSPattern(Patterns[pat], true, patternChunk); + } + } + + if(!(loadFlags & loadSampleData)) + { + return true; + } + + // Read Samples + for(SAMPLEINDEX smp = 0; smp < GetNumSamples(); smp++) + { + if((sampleSettings[smp] & instrIndexMask) == 0) + { + // Only load samples that aren't part of a shadow instrument + SampleIO( + (Samples[smp + 1].uFlags & CHN_16BIT) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (sampleSettings[smp] & packStatusMask) ? SampleIO::AMS : SampleIO::signedPCM) + .ReadSample(Samples[smp + 1], file); + } + } + + // Copy shadow samples + for(SAMPLEINDEX smp = 0; smp < GetNumSamples(); smp++) + { + INSTRUMENTINDEX sourceInstr = (sampleSettings[smp] & instrIndexMask); + if(sourceInstr == 0 + || --sourceInstr >= firstSample.size()) + { + continue; + } + + SAMPLEINDEX sourceSample = ((sampleSettings[smp] & sampleIndexMask) >> sampleIndexShift) + firstSample[sourceInstr]; + if(sourceSample > GetNumSamples() || !Samples[sourceSample].HasSampleData()) + { + continue; + } + + // Copy over original sample + ModSample &sample = Samples[smp + 1]; + ModSample &source = Samples[sourceSample]; + sample.uFlags.set(CHN_16BIT, source.uFlags[CHN_16BIT]); + sample.nLength = source.nLength; + if(sample.AllocateSample()) + { + memcpy(sample.sampleb(), source.sampleb(), source.GetSampleSizeInBytes()); + } + } + + return true; +} + + +///////////////////////////////////////////////////////////////////// +// AMS Sample unpacking + +void AMSUnpack(const int8 * const source, size_t sourceSize, void * const dest, const size_t destSize, char packCharacter) +{ + std::vector<int8> tempBuf(destSize, 0); + size_t depackSize = destSize; + + // Unpack Loop + { + const int8 *in = source; + int8 *out = tempBuf.data(); + + size_t i = sourceSize, j = destSize; + while(i != 0 && j != 0) + { + int8 ch = *(in++); + if(--i != 0 && ch == packCharacter) + { + uint8 repCount = *(in++); + repCount = static_cast<uint8>(std::min(static_cast<size_t>(repCount), j)); + if(--i != 0 && repCount) + { + ch = *(in++); + i--; + while(repCount-- != 0) + { + *(out++) = ch; + j--; + } + } else + { + *(out++) = packCharacter; + j--; + } + } else + { + *(out++) = ch; + j--; + } + } + // j should only be non-zero for truncated samples + depackSize -= j; + } + + // Bit Unpack Loop + { + int8 *out = tempBuf.data(); + uint16 bitcount = 0x80; + size_t k = 0; + uint8 *dst = static_cast<uint8 *>(dest); + for(size_t i = 0; i < depackSize; i++) + { + uint8 al = *out++; + uint16 dh = 0; + for(uint16 count = 0; count < 8; count++) + { + uint16 bl = al & bitcount; + bl = ((bl | (bl << 8)) >> ((dh + 8 - count) & 7)) & 0xFF; + bitcount = ((bitcount | (bitcount << 8)) >> 1) & 0xFF; + dst[k++] |= bl; + if(k >= destSize) + { + k = 0; + dh++; + } + } + bitcount = ((bitcount | (bitcount << 8)) >> dh) & 0xFF; + } + } + + // Delta Unpack + { + int8 old = 0; + int8 *out = static_cast<int8 *>(dest); + for(size_t i = depackSize; i != 0; i--) + { + int pos = *reinterpret_cast<uint8 *>(out); + if(pos != 128 && (pos & 0x80) != 0) + { + pos = -(pos & 0x7F); + } + old -= static_cast<int8>(pos); + *(out++) = old; + } + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_c67.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_c67.cpp new file mode 100644 index 00000000..8592a72c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_c67.cpp @@ -0,0 +1,268 @@ +/* + * Load_c67.cpp + * ------------ + * Purpose: C67 (CDFM Composer) module loader + * Notes : C67 is the composer format; 670 files can be converted back to C67 using the converter that comes with CDFM. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct C67SampleHeader +{ + uint32le unknown; // Probably placeholder for in-memory address, 0 on disk + uint32le length; + uint32le loopStart; + uint32le loopEnd; +}; + +MPT_BINARY_STRUCT(C67SampleHeader, 16) + + +struct C67FileHeader +{ + uint8 speed; + uint8 restartPos; + char sampleNames[32][13]; + C67SampleHeader samples[32]; + char fmInstrNames[32][13]; + uint8 fmInstr[32][11]; + uint8 orders[256]; +}; + +MPT_BINARY_STRUCT(C67FileHeader, 1954) + + +static bool ValidateHeader(const C67FileHeader &fileHeader) +{ + if(fileHeader.speed < 1 || fileHeader.speed > 15) + return false; + for(auto ord : fileHeader.orders) + { + if(ord >= 128 && ord != 0xFF) + return false; + } + + bool anyNonSilent = false; + for(SAMPLEINDEX smp = 0; smp < 32; smp++) + { + if(fileHeader.sampleNames[smp][12] != 0 + || fileHeader.samples[smp].unknown != 0 + || fileHeader.samples[smp].length > 0xFFFFF + || fileHeader.fmInstrNames[smp][12] != 0 + || (fileHeader.fmInstr[smp][0] & 0xF0) // No OPL3 + || (fileHeader.fmInstr[smp][5] & 0xFC) // No OPL3 + || (fileHeader.fmInstr[smp][10] & 0xFC)) // No OPL3 + { + return false; + } + if(fileHeader.samples[smp].length != 0 && fileHeader.samples[smp].loopEnd < 0xFFFFF) + { + if(fileHeader.samples[smp].loopEnd > fileHeader.samples[smp].length + || fileHeader.samples[smp].loopStart > fileHeader.samples[smp].loopEnd) + { + return false; + } + } + if(!anyNonSilent && (fileHeader.samples[smp].length != 0 || memcmp(fileHeader.fmInstr[smp], "\0\0\0\0\0\0\0\0\0\0\0", 11))) + { + anyNonSilent = true; + } + } + return anyNonSilent; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const C67FileHeader &) +{ + return 1024; // Pattern offsets and lengths +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderC67(MemoryFileReader file, const uint64 *pfilesize) +{ + C67FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +static void TranslateVolume(ModCommand &m, uint8 volume, bool isFM) +{ + // CDFM uses a linear volume scale for FM instruments. + // ScreamTracker, on the other hand, directly uses the OPL chip's logarithmic volume scale. + // Neither FM nor PCM instruments can be fully muted in CDFM. + static constexpr uint8 fmVolume[16] = + { + 0x08, 0x10, 0x18, 0x20, 0x28, 0x2C, 0x30, 0x34, + 0x36, 0x38, 0x3A, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, + }; + + volume &= 0x0F; + m.volcmd = VOLCMD_VOLUME; + m.vol = isFM ? fmVolume[volume] : (4u + volume * 4u); +} + + +bool CSoundFile::ReadC67(FileReader &file, ModLoadingFlags loadFlags) +{ + C67FileHeader fileHeader; + + file.Rewind(); + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + + // Validate pattern offsets and lengths + uint32le patOffsets[128], patLengths[128]; + file.ReadArray(patOffsets); + file.ReadArray(patLengths); + for(PATTERNINDEX pat = 0; pat < 128; pat++) + { + if(patOffsets[pat] > 0xFFFFFF + || patLengths[pat] < 3 // Smallest well-formed pattern consists of command 0x40 followed by command 0x60 + || patLengths[pat] > 0x1000 // Any well-formed pattern is smaller than this + || !file.LengthIsAtLeast(2978 + patOffsets[pat] + patLengths[pat])) + { + return false; + } + } + + InitializeGlobals(MOD_TYPE_S3M); + InitializeChannels(); + + m_modFormat.formatName = U_("CDFM"); + m_modFormat.type = U_("c67"); + m_modFormat.madeWithTracker = U_("Composer 670"); + m_modFormat.charset = mpt::Charset::CP437; + + m_nDefaultSpeed = fileHeader.speed; + m_nDefaultTempo.Set(143); + Order().SetRestartPos(fileHeader.restartPos); + m_nSamples = 64; + m_nChannels = 4 + 9; + m_playBehaviour.set(kOPLBeatingOscillators); + m_SongFlags.set(SONG_IMPORTED); + + // Pan PCM channels only + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + ChnSettings[chn].nPan = (chn & 1) ? 192 : 64; + } + + // PCM instruments + for(SAMPLEINDEX smp = 0; smp < 32; smp++) + { + ModSample &mptSmp = Samples[smp + 1]; + mptSmp.Initialize(MOD_TYPE_S3M); + m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.sampleNames[smp]); + mptSmp.nLength = fileHeader.samples[smp].length; + if(fileHeader.samples[smp].loopEnd <= fileHeader.samples[smp].length) + { + mptSmp.nLoopStart = fileHeader.samples[smp].loopStart; + mptSmp.nLoopEnd = fileHeader.samples[smp].loopEnd; + mptSmp.uFlags = CHN_LOOP; + } + mptSmp.nC5Speed = 8287; + } + // OPL instruments + for(SAMPLEINDEX smp = 0; smp < 32; smp++) + { + ModSample &mptSmp = Samples[smp + 33]; + mptSmp.Initialize(MOD_TYPE_S3M); + m_szNames[smp + 33] = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.fmInstrNames[smp]); + // Reorder OPL patch bytes (interleave modulator and carrier) + const auto &fm = fileHeader.fmInstr[smp]; + OPLPatch patch{{}}; + patch[0] = fm[1]; patch[1] = fm[6]; + patch[2] = fm[2]; patch[3] = fm[7]; + patch[4] = fm[3]; patch[5] = fm[8]; + patch[6] = fm[4]; patch[7] = fm[9]; + patch[8] = fm[5]; patch[9] = fm[10]; + patch[10] = fm[0]; + mptSmp.SetAdlib(true, patch); + } + + ReadOrderFromArray<uint8>(Order(), fileHeader.orders, 256, 0xFF); + Patterns.ResizeArray(128); + for(PATTERNINDEX pat = 0; pat < 128; pat++) + { + file.Seek(2978 + patOffsets[pat]); + FileReader patChunk = file.ReadChunk(patLengths[pat]); + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + continue; + } + CPattern &pattern = Patterns[pat]; + ROWINDEX row = 0; + while(row < 64 && patChunk.CanRead(1)) + { + uint8 cmd = patChunk.ReadUint8(); + if(cmd <= 0x0C) + { + // Note, instrument, volume + ModCommand &m = *pattern.GetpModCommand(row, cmd); + const auto [note, instrVol] = patChunk.ReadArray<uint8, 2>(); + bool fmChn = (cmd >= 4); + m.note = NOTE_MIN + (fmChn ? 12 : 36) + (note & 0x0F) + ((note >> 4) & 0x07) * 12; + m.instr = (fmChn ? 33 : 1) + (instrVol >> 4) + ((note & 0x80) >> 3); + TranslateVolume(m, instrVol, fmChn); + } else if(cmd >= 0x20 && cmd <= 0x2C) + { + // Volume + TranslateVolume(*pattern.GetpModCommand(row, cmd - 0x20), patChunk.ReadUint8(), cmd >= 0x24); + } else if(cmd == 0x40) + { + // Delay (row done) + row += patChunk.ReadUint8(); + } else if(cmd == 0x60) + { + // End of pattern + if(row > 0) + { + pattern.GetpModCommand(row - 1, 0)->command = CMD_PATTERNBREAK; + } + break; + } else + { + return false; + } + } + } + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= 32; smp++) + { + SampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::unsignedPCM).ReadSample(Samples[smp], file); + } + } + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_dbm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dbm.cpp new file mode 100644 index 00000000..1b8fdf9b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dbm.cpp @@ -0,0 +1,711 @@ +/* + * Load_dbm.cpp + * ------------ + * Purpose: DigiBooster Pro module Loader (DBM) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "../common/mptStringBuffer.h" +#ifndef NO_PLUGINS +#include "plugins/DigiBoosterEcho.h" +#endif // NO_PLUGINS + +#ifdef LIBOPENMPT_BUILD +#define MPT_DBM_USE_REAL_SUBSONGS +#endif + +OPENMPT_NAMESPACE_BEGIN + +struct DBMFileHeader +{ + char dbm0[4]; + uint8 trkVerHi; + uint8 trkVerLo; + char reserved[2]; +}; + +MPT_BINARY_STRUCT(DBMFileHeader, 8) + + +// IFF-style Chunk +struct DBMChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idNAME = MagicBE("NAME"), + idINFO = MagicBE("INFO"), + idSONG = MagicBE("SONG"), + idINST = MagicBE("INST"), + idVENV = MagicBE("VENV"), + idPENV = MagicBE("PENV"), + idPATT = MagicBE("PATT"), + idPNAM = MagicBE("PNAM"), + idSMPL = MagicBE("SMPL"), + idDSPE = MagicBE("DSPE"), + idMPEG = MagicBE("MPEG"), + }; + + uint32be id; + uint32be length; + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(DBMChunk, 8) + + +struct DBMInfoChunk +{ + uint16be instruments; + uint16be samples; + uint16be songs; + uint16be patterns; + uint16be channels; +}; + +MPT_BINARY_STRUCT(DBMInfoChunk, 10) + + +// Instrument header +struct DBMInstrument +{ + enum DBMInstrFlags + { + smpLoop = 0x01, + smpPingPongLoop = 0x02, + }; + + char name[30]; + uint16be sample; // Sample reference + uint16be volume; // 0...64 + uint32be sampleRate; + uint32be loopStart; + uint32be loopLength; + int16be panning; // -128...128 + uint16be flags; // See DBMInstrFlags +}; + +MPT_BINARY_STRUCT(DBMInstrument, 50) + + +// Volume or panning envelope +struct DBMEnvelope +{ + enum DBMEnvelopeFlags + { + envEnabled = 0x01, + envSustain = 0x02, + envLoop = 0x04, + }; + + uint16be instrument; + uint8be flags; // See DBMEnvelopeFlags + uint8be numSegments; // Number of envelope points - 1 + uint8be sustain1; + uint8be loopBegin; + uint8be loopEnd; + uint8be sustain2; // Second sustain point + uint16be data[2 * 32]; +}; + +MPT_BINARY_STRUCT(DBMEnvelope, 136) + + +// Note: Unlike in MOD, 1Fx, 2Fx, 5Fx / 5xF, 6Fx / 6xF and AFx / AxF are fine slides. +static constexpr ModCommand::COMMAND dbmEffects[] = +{ + CMD_ARPEGGIO, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO, + CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO, + CMD_PANNING8, CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP, + CMD_VOLUME, CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_TEMPO, + CMD_GLOBALVOLUME, CMD_GLOBALVOLSLIDE, CMD_NONE, CMD_NONE, + CMD_KEYOFF, CMD_SETENVPOSITION, CMD_NONE, CMD_NONE, + CMD_NONE, CMD_PANNINGSLIDE, CMD_NONE, CMD_NONE, + CMD_NONE, CMD_NONE, CMD_NONE, +#ifndef NO_PLUGINS + CMD_DBMECHO, // Toggle DSP + CMD_MIDI, // Wxx Echo Delay + CMD_MIDI, // Xxx Echo Feedback + CMD_MIDI, // Yxx Echo Mix + CMD_MIDI, // Zxx Echo Cross +#endif // NO_PLUGINS +}; + + +static void ConvertDBMEffect(uint8 &command, uint8 ¶m) +{ + uint8 oldCmd = command; + if(command < std::size(dbmEffects)) + command = dbmEffects[command]; + else + command = CMD_NONE; + + switch(command) + { + case CMD_ARPEGGIO: + if(param == 0) + command = CMD_NONE; + break; + + case CMD_PATTERNBREAK: + param = ((param >> 4) * 10) + (param & 0x0F); + break; + +#ifdef MODPLUG_TRACKER + case CMD_VIBRATO: + if(param & 0x0F) + { + // DBM vibrato is half as deep as most other trackers. Convert it to IT fine vibrato range if possible. + uint8 depth = (param & 0x0F) * 2u; + param &= 0xF0; + if(depth < 16) + command = CMD_FINEVIBRATO; + else + depth = (depth + 2u) / 4u; + param |= depth; + } + break; +#endif + + // Volume slide nibble priority - first nibble (slide up) has precedence. + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + if((param & 0xF0) != 0x00 && (param & 0xF0) != 0xF0 && (param & 0x0F) != 0x0F) + param &= 0xF0; + break; + + case CMD_GLOBALVOLUME: + if(param <= 64) + param *= 2; + else + param = 128; + break; + + case CMD_MODCMDEX: + switch(param & 0xF0) + { + case 0x30: // Play backwards + command = CMD_S3MCMDEX; + param = 0x9F; + break; + case 0x40: // Turn off sound in channel (volume / portamento commands after this can't pick up the note anymore) + command = CMD_S3MCMDEX; + param = 0xC0; + break; + case 0x50: // Turn on/off channel + // TODO: Apparently this should also kill the playing note. + if((param & 0x0F) <= 0x01) + { + command = CMD_CHANNELVOLUME; + param = (param == 0x50) ? 0x00 : 0x40; + } + break; + case 0x70: // Coarse offset + command = CMD_S3MCMDEX; + param = 0xA0 | (param & 0x0F); + break; + default: + // Rest will be converted later from CMD_MODCMDEX to CMD_S3MCMDEX. + break; + } + break; + + case CMD_TEMPO: + if(param <= 0x1F) command = CMD_SPEED; + break; + + case CMD_KEYOFF: + if (param == 0) + { + // TODO key off at tick 0 + } + break; + + case CMD_MIDI: + // Encode echo parameters into fixed MIDI macros + param = 128 + (oldCmd - 32) * 32 + param / 8; + } +} + + +// Read a chunk of volume or panning envelopes +static void ReadDBMEnvelopeChunk(FileReader chunk, EnvelopeType envType, CSoundFile &sndFile, bool scaleEnv) +{ + uint16 numEnvs = chunk.ReadUint16BE(); + for(uint16 env = 0; env < numEnvs; env++) + { + DBMEnvelope dbmEnv; + chunk.ReadStruct(dbmEnv); + + uint16 dbmIns = dbmEnv.instrument; + if(dbmIns > 0 && dbmIns <= sndFile.GetNumInstruments() && (sndFile.Instruments[dbmIns] != nullptr)) + { + ModInstrument *mptIns = sndFile.Instruments[dbmIns]; + InstrumentEnvelope &mptEnv = mptIns->GetEnvelope(envType); + + if(dbmEnv.numSegments) + { + if(dbmEnv.flags & DBMEnvelope::envEnabled) mptEnv.dwFlags.set(ENV_ENABLED); + if(dbmEnv.flags & DBMEnvelope::envSustain) mptEnv.dwFlags.set(ENV_SUSTAIN); + if(dbmEnv.flags & DBMEnvelope::envLoop) mptEnv.dwFlags.set(ENV_LOOP); + } + + uint8 numPoints = std::min(dbmEnv.numSegments.get(), uint8(31)) + 1; + mptEnv.resize(numPoints); + + mptEnv.nLoopStart = dbmEnv.loopBegin; + mptEnv.nLoopEnd = dbmEnv.loopEnd; + mptEnv.nSustainStart = mptEnv.nSustainEnd = dbmEnv.sustain1; + + for(uint8 i = 0; i < numPoints; i++) + { + mptEnv[i].tick = dbmEnv.data[i * 2]; + uint16 val = dbmEnv.data[i * 2 + 1]; + if(scaleEnv) + { + // Panning envelopes are -128...128 in DigiBooster Pro 3.x + val = (val + 128) / 4; + } + LimitMax(val, uint16(64)); + mptEnv[i].value = static_cast<uint8>(val); + } + } + } +} + + +static bool ValidateHeader(const DBMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.dbm0, "DBM0", 4) + || fileHeader.trkVerHi > 3) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDBM(MemoryFileReader file, const uint64 *pfilesize) +{ + DBMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadDBM(FileReader &file, ModLoadingFlags loadFlags) +{ + + file.Rewind(); + DBMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + ChunkReader chunkFile(file); + auto chunks = chunkFile.ReadChunks<DBMChunk>(1); + + // Globals + DBMInfoChunk infoData; + if(!chunks.GetChunk(DBMChunk::idINFO).ReadStruct(infoData)) + { + return false; + } + + InitializeGlobals(MOD_TYPE_DBM); + InitializeChannels(); + m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; + m_nChannels = Clamp<uint16, uint16>(infoData.channels, 1, MAX_BASECHANNELS); // note: MAX_BASECHANNELS is currently 127, but DBPro 2 supports up to 128 channels, DBPro 3 apparently up to 254. + m_nInstruments = std::min(static_cast<INSTRUMENTINDEX>(infoData.instruments), static_cast<INSTRUMENTINDEX>(MAX_INSTRUMENTS - 1)); + m_nSamples = std::min(static_cast<SAMPLEINDEX>(infoData.samples), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1)); + m_playBehaviour.set(kSlidesAtSpeed1); + m_playBehaviour.reset(kITVibratoTremoloPanbrello); + m_playBehaviour.reset(kITArpeggio); + + m_modFormat.formatName = U_("DigiBooster Pro"); + m_modFormat.type = U_("dbm"); + m_modFormat.madeWithTracker = MPT_UFORMAT("DigiBooster Pro {}.{}")(mpt::ufmt::hex(fileHeader.trkVerHi), mpt::ufmt::hex(fileHeader.trkVerLo)); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + // Name chunk + FileReader nameChunk = chunks.GetChunk(DBMChunk::idNAME); + nameChunk.ReadString<mpt::String::maybeNullTerminated>(m_songName, nameChunk.GetLength()); + + // Song chunk + FileReader songChunk = chunks.GetChunk(DBMChunk::idSONG); + Order().clear(); + uint16 numSongs = infoData.songs; + for(uint16 i = 0; i < numSongs && songChunk.CanRead(46); i++) + { + char name[44]; + songChunk.ReadString<mpt::String::maybeNullTerminated>(name, 44); + if(m_songName.empty()) + { + m_songName = name; + } + uint16 numOrders = songChunk.ReadUint16BE(); + +#ifdef MPT_DBM_USE_REAL_SUBSONGS + if(!Order().empty()) + { + // Add a new sequence for this song + if(Order.AddSequence() == SEQUENCEINDEX_INVALID) + break; + } + Order().SetName(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, name)); + ReadOrderFromFile<uint16be>(Order(), songChunk, numOrders); +#else + const ORDERINDEX startIndex = Order().GetLength(); + if(startIndex < MAX_ORDERS && songChunk.CanRead(numOrders * 2u)) + { + LimitMax(numOrders, static_cast<ORDERINDEX>(MAX_ORDERS - startIndex - 1)); + Order().resize(startIndex + numOrders + 1); + for(uint16 ord = 0; ord < numOrders; ord++) + { + Order()[startIndex + ord] = static_cast<PATTERNINDEX>(songChunk.ReadUint16BE()); + } + } +#endif // MPT_DBM_USE_REAL_SUBSONGS + } +#ifdef MPT_DBM_USE_REAL_SUBSONGS + Order.SetSequence(0); +#endif // MPT_DBM_USE_REAL_SUBSONGS + + // Read instruments + if(FileReader instChunk = chunks.GetChunk(DBMChunk::idINST)) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + DBMInstrument instrHeader; + instChunk.ReadStruct(instrHeader); + + ModInstrument *mptIns = AllocateInstrument(i, instrHeader.sample); + if(mptIns == nullptr || instrHeader.sample >= MAX_SAMPLES) + { + continue; + } + + mptIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); + m_szNames[instrHeader.sample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); + + mptIns->nFadeOut = 0; + mptIns->nPan = static_cast<uint16>(instrHeader.panning + 128); + LimitMax(mptIns->nPan, uint32(256)); + mptIns->dwFlags.set(INS_SETPANNING); + + // Sample Info + ModSample &mptSmp = Samples[instrHeader.sample]; + mptSmp.Initialize(); + mptSmp.nVolume = std::min(static_cast<uint16>(instrHeader.volume), uint16(64)) * 4u; + mptSmp.nC5Speed = Util::muldivr(instrHeader.sampleRate, 8303, 8363); + + if(instrHeader.loopLength && (instrHeader.flags & (DBMInstrument::smpLoop | DBMInstrument::smpPingPongLoop))) + { + mptSmp.nLoopStart = instrHeader.loopStart; + mptSmp.nLoopEnd = mptSmp.nLoopStart + instrHeader.loopLength; + mptSmp.uFlags.set(CHN_LOOP); + if(instrHeader.flags & DBMInstrument::smpPingPongLoop) + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + } + } + + // Read envelopes + ReadDBMEnvelopeChunk(chunks.GetChunk(DBMChunk::idVENV), ENV_VOLUME, *this, false); + ReadDBMEnvelopeChunk(chunks.GetChunk(DBMChunk::idPENV), ENV_PANNING, *this, fileHeader.trkVerHi > 2); + + // Note-Off cuts samples if there's no envelope. + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(Instruments[i] != nullptr && !Instruments[i]->VolEnv.dwFlags[ENV_ENABLED]) + { + Instruments[i]->nFadeOut = 32767; + } + } + } + + // Patterns + FileReader patternChunk = chunks.GetChunk(DBMChunk::idPATT); +#ifndef NO_PLUGINS + bool hasEchoEnable = false, hasEchoParams = false; +#endif // NO_PLUGINS + if(patternChunk.IsValid() && (loadFlags & loadPatternData)) + { + FileReader patternNameChunk = chunks.GetChunk(DBMChunk::idPNAM); + patternNameChunk.Skip(1); // Encoding, should be UTF-8 or ASCII + + Patterns.ResizeArray(infoData.patterns); + std::vector<std::pair<EffectCommand, ModCommand::PARAM>> lostGlobalCommands; + for(PATTERNINDEX pat = 0; pat < infoData.patterns; pat++) + { + uint16 numRows = patternChunk.ReadUint16BE(); + uint32 packedSize = patternChunk.ReadUint32BE(); + FileReader chunk = patternChunk.ReadChunk(packedSize); + + if(!Patterns.Insert(pat, numRows)) + continue; + + std::string patName; + patternNameChunk.ReadSizedString<uint8be, mpt::String::maybeNullTerminated>(patName); + Patterns[pat].SetName(patName); + + PatternRow patRow = Patterns[pat].GetRow(0); + ROWINDEX row = 0; + lostGlobalCommands.clear(); + while(chunk.CanRead(1)) + { + const uint8 ch = chunk.ReadUint8(); + + if(!ch) + { + // End Of Row + for(const auto &cmd : lostGlobalCommands) + { + Patterns[pat].WriteEffect(EffectWriter(cmd.first, cmd.second).Row(row)); + } + lostGlobalCommands.clear(); + + if(++row >= numRows) + break; + + patRow = Patterns[pat].GetRow(row); + continue; + } + + ModCommand dummy = ModCommand::Empty(); + ModCommand &m = ch <= GetNumChannels() ? patRow[ch - 1] : dummy; + + const uint8 b = chunk.ReadUint8(); + + if(b & 0x01) + { + uint8 note = chunk.ReadUint8(); + + if(note == 0x1F) + m.note = NOTE_KEYOFF; + else if(note > 0 && note < 0xFE) + m.note = ((note >> 4) * 12) + (note & 0x0F) + 13; + } + if(b & 0x02) + { + m.instr = chunk.ReadUint8(); + } + if(b & 0x3C) + { + uint8 cmd1 = 0, cmd2 = 0, param1 = 0, param2 = 0; + if(b & 0x04) cmd2 = chunk.ReadUint8(); + if(b & 0x08) param2 = chunk.ReadUint8(); + if(b & 0x10) cmd1 = chunk.ReadUint8(); + if(b & 0x20) param1 = chunk.ReadUint8(); + ConvertDBMEffect(cmd1, param1); + ConvertDBMEffect(cmd2, param2); + + if (cmd2 == CMD_VOLUME || (cmd2 == CMD_NONE && cmd1 != CMD_VOLUME)) + { + std::swap(cmd1, cmd2); + std::swap(param1, param2); + } + + const auto lostCommand = ModCommand::TwoRegularCommandsToMPT(cmd1, param1, cmd2, param2); + if(ModCommand::IsGlobalCommand(lostCommand.first, lostCommand.second)) + lostGlobalCommands.insert(lostGlobalCommands.begin(), lostCommand); // Insert at front so that the last command of same type "wins" + + m.volcmd = cmd1; + m.vol = param1; + m.command = cmd2; + m.param = param2; +#ifdef MODPLUG_TRACKER + m.ExtendedMODtoS3MEffect(); +#endif // MODPLUG_TRACKER +#ifndef NO_PLUGINS + if(m.command == CMD_DBMECHO) + hasEchoEnable = true; + else if(m.command == CMD_MIDI) + hasEchoParams = true; +#endif // NO_PLUGINS + } + } + } + } + +#ifndef NO_PLUGINS + // Echo DSP + if(loadFlags & loadPluginData) + { + if(hasEchoEnable) + { + // If there are any Vxx effects to dynamically enable / disable echo, use the CHN_NOFX flag. + for(CHANNELINDEX i = 0; i < m_nChannels; i++) + { + ChnSettings[i].nMixPlugin = 1; + ChnSettings[i].dwFlags.set(CHN_NOFX); + } + } + + bool anyEnabled = hasEchoEnable; + // DBP 3 Documentation says that the defaults are 64/128/128/255, but they appear to be 80/150/80/255 in DBP 2.21 + uint8 settings[8] = { 0, 80, 0, 150, 0, 80, 0, 255 }; + + if(FileReader dspChunk = chunks.GetChunk(DBMChunk::idDSPE)) + { + uint16 maskLen = dspChunk.ReadUint16BE(); + for(uint16 i = 0; i < maskLen; i++) + { + bool enabled = (dspChunk.ReadUint8() == 0); + if(i < m_nChannels) + { + if(hasEchoEnable) + { + // If there are any Vxx effects to dynamically enable / disable echo, use the CHN_NOFX flag. + ChnSettings[i].dwFlags.set(CHN_NOFX, !enabled); + } else if(enabled) + { + ChnSettings[i].nMixPlugin = 1; + anyEnabled = true; + } + } + } + dspChunk.ReadArray(settings); + } + + if(anyEnabled) + { + // Note: DigiBooster Pro 3 has a more versatile per-channel echo effect. + // In this case, we'd have to create one plugin per channel. + SNDMIXPLUGIN &plugin = m_MixPlugins[0]; + plugin.Destroy(); + memcpy(&plugin.Info.dwPluginId1, "DBM0", 4); + memcpy(&plugin.Info.dwPluginId2, "Echo", 4); + plugin.Info.routingFlags = SNDMIXPLUGININFO::irAutoSuspend; + plugin.Info.mixMode = 0; + plugin.Info.gain = 10; + plugin.Info.reserved = 0; + plugin.Info.dwOutputRouting = 0; + std::fill(plugin.Info.dwReserved, plugin.Info.dwReserved + std::size(plugin.Info.dwReserved), 0); + plugin.Info.szName = "Echo"; + plugin.Info.szLibraryName = "DigiBooster Pro Echo"; + + plugin.pluginData.resize(sizeof(DigiBoosterEcho::PluginChunk)); + DigiBoosterEcho::PluginChunk chunk = DigiBoosterEcho::PluginChunk::Create(settings[1], settings[3], settings[5], settings[7]); + new (plugin.pluginData.data()) DigiBoosterEcho::PluginChunk(chunk); + } + } + + // Encode echo parameters into fixed MIDI macros + if(hasEchoParams) + { + for(uint32 i = 0; i < 32; i++) + { + uint32 param = (i * 127u) / 32u; + m_MidiCfg.Zxx[i ] = MPT_AFORMAT("F0F080{}")(mpt::afmt::HEX0<2>(param)); + m_MidiCfg.Zxx[i + 32] = MPT_AFORMAT("F0F081{}")(mpt::afmt::HEX0<2>(param)); + m_MidiCfg.Zxx[i + 64] = MPT_AFORMAT("F0F082{}")(mpt::afmt::HEX0<2>(param)); + m_MidiCfg.Zxx[i + 96] = MPT_AFORMAT("F0F083{}")(mpt::afmt::HEX0<2>(param)); + } + } +#endif // NO_PLUGINS + + // Samples + FileReader sampleChunk = chunks.GetChunk(DBMChunk::idSMPL); + if(sampleChunk.IsValid() && (loadFlags & loadSampleData)) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + uint32 sampleFlags = sampleChunk.ReadUint32BE(); + uint32 sampleLength = sampleChunk.ReadUint32BE(); + + if(sampleFlags & 7) + { + ModSample &sample = Samples[smp]; + sample.nLength = sampleLength; + + SampleIO( + (sampleFlags & 4) ? SampleIO::_32bit : ((sampleFlags & 2) ? SampleIO::_16bit : SampleIO::_8bit), + SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM) + .ReadSample(sample, sampleChunk); + } + } + } + +#if defined(MPT_ENABLE_MP3_SAMPLES) && 0 + // Compressed samples - this does not quite work yet... + FileReader mpegChunk = chunks.GetChunk(DBMChunk::idMPEG); + if(mpegChunk.IsValid() && (loadFlags & loadSampleData)) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + Samples[smp].nLength = mpegChunk.ReadUint32BE(); + } + mpegChunk.Skip(2); // 0x00 0x40 + + // Read whole MPEG stream into one sample and then split it up. + FileReader chunk = mpegChunk.GetChunk(mpegChunk.BytesLeft()); + if(ReadMP3Sample(0, chunk, true)) + { + ModSample &srcSample = Samples[0]; + const std::byte *smpData = srcSample.sampleb(); + SmpLength predelay = Util::muldiv_unsigned(20116, srcSample.nC5Speed, 100000); + LimitMax(predelay, srcSample.nLength); + smpData += predelay * srcSample.GetBytesPerSample(); + srcSample.nLength -= predelay; + + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + ModSample &sample = Samples[smp]; + sample.uFlags.set(srcSample.uFlags); + LimitMax(sample.nLength, srcSample.nLength); + if(sample.nLength) + { + sample.AllocateSample(); + memcpy(sample.sampleb(), smpData, sample.GetSampleSizeInBytes()); + smpData += sample.GetSampleSizeInBytes(); + srcSample.nLength -= sample.nLength; + SmpLength gap = Util::muldiv_unsigned(454, srcSample.nC5Speed, 10000); + LimitMax(gap, srcSample.nLength); + smpData += gap * srcSample.GetBytesPerSample(); + srcSample.nLength -= gap; + } + } + srcSample.FreeSample(); + } + } +#endif // MPT_ENABLE_MP3_SAMPLES + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_digi.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_digi.cpp new file mode 100644 index 00000000..a7048d9f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_digi.cpp @@ -0,0 +1,231 @@ +/* + * Load_digi.cpp + * ------------- + * Purpose: Digi Booster module loader + * Notes : Basically these are like ProTracker MODs with a few extra features such as more channels, longer samples and a few more effects. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +// DIGI File Header +struct DIGIFileHeader +{ + char signature[20]; + char versionStr[4]; // Supposed to be "V1.6" or similar, but other values like "TAP!" have been found as well. + uint8be versionInt; // e.g. 0x16 = 1.6 + uint8be numChannels; + uint8be packEnable; + char unknown[19]; + uint8be lastPatIndex; + uint8be lastOrdIndex; + uint8be orders[128]; + uint32be smpLength[31]; + uint32be smpLoopStart[31]; + uint32be smpLoopLength[31]; + uint8be smpVolume[31]; + uint8be smpFinetune[31]; +}; + +MPT_BINARY_STRUCT(DIGIFileHeader, 610) + + +static void ReadDIGIPatternEntry(FileReader &file, ModCommand &m) +{ + CSoundFile::ReadMODPatternEntry(file, m); + CSoundFile::ConvertModCommand(m); + if(m.command == CMD_MODCMDEX) + { + switch(m.param & 0xF0) + { + case 0x30: + // E3x: Play sample backwards (E30 stops sample when it reaches the beginning, any other value plays it from the beginning including regular loop) + // The play direction is also reset if a new note is played on the other channel linked to this channel. + // The behaviour is rather broken when there is no note next to the ommand. + m.command = CMD_DIGIREVERSESAMPLE; + m.param &= 0x0F; + break; + case 0x40: + // E40: Stop playing sample + if(m.param == 0x40) + { + m.note = NOTE_NOTECUT; + m.command = CMD_NONE; + } + break; + case 0x80: + // E8x: High sample offset + m.command = CMD_S3MCMDEX; + m.param = 0xA0 | (m.param & 0x0F); + } + } else if(m.command == CMD_PANNING8) + { + // 8xx "Robot" effect (not supported) + m.command = CMD_NONE; + } +} + + +static bool ValidateHeader(const DIGIFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.signature, "DIGI Booster module\0", 20) + || !fileHeader.numChannels + || fileHeader.numChannels > 8 + || fileHeader.lastOrdIndex > 127) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDIGI(MemoryFileReader file, const uint64 *pfilesize) +{ + DIGIFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadDIGI(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + DIGIFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Globals + InitializeGlobals(MOD_TYPE_DIGI); + InitializeChannels(); + + m_nChannels = fileHeader.numChannels; + m_nSamples = 31; + m_nSamplePreAmp = 256 / m_nChannels; + + m_modFormat.formatName = U_("DigiBooster"); + m_modFormat.type = U_("digi"); + m_modFormat.madeWithTracker = MPT_UFORMAT("Digi Booster {}.{}")(fileHeader.versionInt >> 4, fileHeader.versionInt & 0x0F); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + ReadOrderFromArray(Order(), fileHeader.orders, fileHeader.lastOrdIndex + 1); + + // Read sample headers + for(SAMPLEINDEX smp = 0; smp < 31; smp++) + { + ModSample &sample = Samples[smp + 1]; + sample.Initialize(MOD_TYPE_MOD); + sample.nLength = fileHeader.smpLength[smp]; + sample.nLoopStart = fileHeader.smpLoopStart[smp]; + sample.nLoopEnd = sample.nLoopStart + fileHeader.smpLoopLength[smp]; + if(fileHeader.smpLoopLength[smp]) + { + sample.uFlags.set(CHN_LOOP); + } + sample.SanitizeLoops(); + + sample.nVolume = std::min(fileHeader.smpVolume[smp].get(), uint8(64)) * 4; + sample.nFineTune = MOD2XMFineTune(fileHeader.smpFinetune[smp]); + } + + // Read song + sample names + file.ReadString<mpt::String::maybeNullTerminated>(m_songName, 32); + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + file.ReadString<mpt::String::maybeNullTerminated>(m_szNames[smp], 30); + } + + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.lastPatIndex + 1); + for(PATTERNINDEX pat = 0; pat <= fileHeader.lastPatIndex; pat++) + { + FileReader patternChunk; + if(fileHeader.packEnable) + { + patternChunk = file.ReadChunk(file.ReadUint16BE()); + } else + { + patternChunk = file.ReadChunk(4 * 64 * GetNumChannels()); + } + + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + continue; + } + + if(fileHeader.packEnable) + { + uint8 eventMask[64]; + patternChunk.ReadArray(eventMask); + + // Compressed patterns are stored in row-major order... + for(ROWINDEX row = 0; row < 64; row++) + { + PatternRow patRow = Patterns[pat].GetRow(row); + uint8 bit = 0x80; + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, bit >>= 1) + { + if(eventMask[row] & bit) + { + ModCommand &m = patRow[chn]; + ReadDIGIPatternEntry(patternChunk, m); + } + } + } + } else + { + // ...but uncompressed patterns are stored in column-major order. WTF! + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + for(ROWINDEX row = 0; row < 64; row++) + { + ReadDIGIPatternEntry(patternChunk, *Patterns[pat].GetpModCommand(row, chn)); + } + } + } + } + + if(loadFlags & loadSampleData) + { + // Reading Samples + const SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM); + + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + sampleIO.ReadSample(Samples[smp], file); + } + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_dmf.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dmf.cpp new file mode 100644 index 00000000..78a16f0b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dmf.cpp @@ -0,0 +1,1148 @@ +/* + * load_dmf.cpp + * ------------ + * Purpose: DMF module loader (X-Tracker by D-LUSiON). + * Notes : If it wasn't already outdated when the tracker left beta state, this would be a rather interesting + * and in some parts even sophisticated format - effect columns are separated by effect type, an easy to + * understand BPM tempo mode, effect durations are always divided into a 256th row, vibrato effects are + * specified by period length and the same 8-Bit granularity is used for both volume and panning. + * Unluckily, this format does not offer any envelopes or multi-sample instruments, and bidi sample loops + * are missing as well, so it was already well behind FT2 back then. + * Authors: Johannes Schultz (mostly based on DMF.TXT, DMF_EFFC.TXT, trial and error and some invaluable hints by Zatzen) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "BitReader.h" + +OPENMPT_NAMESPACE_BEGIN + +// DMF header +struct DMFFileHeader +{ + char signature[4]; // "DDMF" + uint8 version; // 1 - 7 are beta versions, 8 is the official thing, 10 is xtracker32 + char tracker[8]; // "XTRACKER" + char songname[30]; + char composer[20]; + uint8 creationDay; + uint8 creationMonth; + uint8 creationYear; +}; + +MPT_BINARY_STRUCT(DMFFileHeader, 66) + +struct DMFChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idCMSG = MagicLE("CMSG"), // Song message + idSEQU = MagicLE("SEQU"), // Order list + idPATT = MagicLE("PATT"), // Patterns + idSMPI = MagicLE("SMPI"), // Sample headers + idSMPD = MagicLE("SMPD"), // Sample data + idSMPJ = MagicLE("SMPJ"), // Sample jump table (XTracker 32 only) + idENDE = MagicLE("ENDE"), // Last four bytes of DMF file + idSETT = MagicLE("SETT"), // Probably contains GUI settings + }; + + uint32le id; + uint32le length; + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(DMFChunk, 8) + +// Pattern header (global) +struct DMFPatterns +{ + uint16le numPatterns; // 1..1024 patterns + uint8le numTracks; // 1..32 channels +}; + +MPT_BINARY_STRUCT(DMFPatterns, 3) + +// Pattern header (for each pattern) +struct DMFPatternHeader +{ + uint8le numTracks; // 1..32 channels + uint8le beat; // [hi|lo] -> hi = rows per beat, lo = reserved + uint16le numRows; + uint32le patternLength; + // patttern data follows here ... +}; + +MPT_BINARY_STRUCT(DMFPatternHeader, 8) + +// Sample header +struct DMFSampleHeader +{ + enum SampleFlags + { + // Sample flags + smpLoop = 0x01, + smp16Bit = 0x02, + smpCompMask = 0x0C, + smpComp1 = 0x04, // Compression type 1 + smpComp2 = 0x08, // Compression type 2 (unused) + smpComp3 = 0x0C, // Compression type 3 (ditto) + smpLibrary = 0x80, // Sample is stored in a library + }; + + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint16le c3freq; // 1000..45000hz + uint8le volume; // 0 = ignore + uint8le flags; + + // Convert an DMFSampleHeader to OpenMPT's internal sample representation. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.nLength = length; + mptSmp.nSustainStart = loopStart; + mptSmp.nSustainEnd = loopEnd; + + mptSmp.nC5Speed = c3freq; + mptSmp.nGlobalVol = 64; + if(volume) + mptSmp.nVolume = volume + 1; + else + mptSmp.nVolume = 256; + mptSmp.uFlags.set(SMP_NODEFAULTVOLUME, volume == 0); + + if((flags & smpLoop) != 0 && mptSmp.nSustainEnd > mptSmp.nSustainStart) + { + mptSmp.uFlags.set(CHN_SUSTAINLOOP); + } + if((flags & smp16Bit) != 0) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nLength /= 2; + mptSmp.nSustainStart /= 2; + mptSmp.nSustainEnd /= 2; + } + } +}; + +MPT_BINARY_STRUCT(DMFSampleHeader, 16) + + +// Pattern translation memory +struct DMFPatternSettings +{ + struct ChannelState + { + ModCommand::NOTE noteBuffer = NOTE_NONE; // Note buffer + ModCommand::NOTE lastNote = NOTE_NONE; // Last played note on channel + uint8 vibratoType = 8; // Last used vibrato type on channel + uint8 tremoloType = 4; // Last used tremolo type on channel + uint8 highOffset = 6; // Last used high offset on channel + bool playDir = false; // Sample play direction... false = forward (default) + }; + + std::vector<ChannelState> channels; // Memory for each channel's state + bool realBPMmode = false; // true = BPM mode + uint8 beat = 0; // Rows per beat + uint8 tempoTicks = 32; // Tick mode param + uint8 tempoBPM = 120; // BPM mode param + uint8 internalTicks = 6; // Ticks per row in final pattern + + DMFPatternSettings(CHANNELINDEX numChannels) + : channels(numChannels) + { } +}; + + +// Convert portamento value (not very accurate due to X-Tracker's higher granularity, to say the least) +static uint8 DMFporta2MPT(uint8 val, const uint8 internalTicks, const bool hasFine) +{ + if(val == 0) + return 0; + else if((val <= 0x0F && hasFine) || internalTicks < 2) + return (val | 0xF0); + else + return std::max(uint8(1), static_cast<uint8>((val / (internalTicks - 1)))); // no porta on first tick! +} + + +// Convert portamento / volume slide value (not very accurate due to X-Tracker's higher granularity, to say the least) +static uint8 DMFslide2MPT(uint8 val, const uint8 internalTicks, const bool up) +{ + val = std::max(uint8(1), static_cast<uint8>(val / 4)); + const bool isFine = (val < 0x0F) || (internalTicks < 2); + if(!isFine) + val = std::max(uint8(1), static_cast<uint8>((val + internalTicks - 2) / (internalTicks - 1))); // no slides on first tick! "+ internalTicks - 2" for rounding precision + + if(up) + return (isFine ? 0x0F : 0x00) | (val << 4); + else + return (isFine ? 0xF0 : 0x00) | (val & 0x0F); + +} + + +// Calculate tremor on/off param +static uint8 DMFtremor2MPT(uint8 val, const uint8 internalTicks) +{ + uint8 ontime = (val >> 4); + uint8 offtime = (val & 0x0F); + ontime = static_cast<uint8>(Clamp(ontime * internalTicks / 15, 1, 15)); + offtime = static_cast<uint8>(Clamp(offtime * internalTicks / 15, 1, 15)); + return (ontime << 4) | offtime; +} + + +// Calculate delay parameter for note cuts / delays +static uint8 DMFdelay2MPT(uint8 val, const uint8 internalTicks) +{ + int newval = (int)val * (int)internalTicks / 255; + Limit(newval, 0, 15); + return (uint8)newval; +} + + +// Convert vibrato-style command parameters +static uint8 DMFvibrato2MPT(uint8 val, const uint8 internalTicks) +{ + // MPT: 1 vibrato period == 64 ticks... we have internalTicks ticks per row. + // X-Tracker: Period length specified in rows! + const int periodInTicks = std::max(1, (val >> 4)) * internalTicks; + const uint8 matchingPeriod = static_cast<uint8>(Clamp((128 / periodInTicks), 1, 15)); + return (matchingPeriod << 4) | std::max(uint8(1), static_cast<uint8>(val & 0x0F)); +} + + +// Try using effect memory (zero paramer) to give the effect swapper some optimization hints. +static void ApplyEffectMemory(const ModCommand *m, ROWINDEX row, CHANNELINDEX numChannels, uint8 effect, uint8 ¶m) +{ + if(effect == CMD_NONE || param == 0) + return; + + const bool isTonePortaEffect = (effect == CMD_PORTAMENTOUP || effect == CMD_PORTAMENTODOWN || effect == CMD_TONEPORTAMENTO); + const bool isVolSlideEffect = (effect == CMD_VOLUMESLIDE || effect == CMD_TONEPORTAVOL || effect == CMD_VIBRATOVOL); + + while(row > 0) + { + m -= numChannels; + row--; + + // First, keep some extra rules in mind for portamento, where effect memory is shared between various commands. + bool isSame = (effect == m->command); + if(isTonePortaEffect && (m->command == CMD_PORTAMENTOUP || m->command == CMD_PORTAMENTODOWN || m->command == CMD_TONEPORTAMENTO)) + { + if(m->param < 0xE0) + { + // Avoid effect param for fine slides, or else we could accidentally put this command in the volume column, where fine slides won't work! + isSame = true; + } else + { + return; + } + } else if(isVolSlideEffect && (m->command == CMD_VOLUMESLIDE || m->command == CMD_TONEPORTAVOL || m->command == CMD_VIBRATOVOL)) + { + isSame = true; + } + if(isTonePortaEffect + && (m->volcmd == VOLCMD_PORTAUP || m->volcmd == VOLCMD_PORTADOWN || m->volcmd == VOLCMD_TONEPORTAMENTO) + && m->vol != 0) + { + // Uuh... Don't even try + return; + } else if(isVolSlideEffect + && (m->volcmd == VOLCMD_FINEVOLUP || m->volcmd == VOLCMD_FINEVOLDOWN || m->volcmd == VOLCMD_VOLSLIDEUP || m->volcmd == VOLCMD_VOLSLIDEDOWN) + && m->vol != 0) + { + // Same! + return; + } + + if(isSame) + { + if(param != m->param && m->param != 0) + { + // No way to optimize this + return; + } else if(param == m->param) + { + // Yay! + param = 0; + return; + } + } + } +} + + +static PATTERNINDEX ConvertDMFPattern(FileReader &file, const uint8 fileVersion, DMFPatternSettings &settings, CSoundFile &sndFile) +{ + // Pattern flags + enum PatternFlags + { + // Global Track + patGlobPack = 0x80, // Pack information for global track follows + patGlobMask = 0x3F, // Mask for global effects + // Note tracks + patCounter = 0x80, // Pack information for current channel follows + patInstr = 0x40, // Instrument number present + patNote = 0x20, // Note present + patVolume = 0x10, // Volume present + patInsEff = 0x08, // Instrument effect present + patNoteEff = 0x04, // Note effect present + patVolEff = 0x02, // Volume effect stored + }; + + file.Rewind(); + + DMFPatternHeader patHead; + if(fileVersion < 3) + { + patHead.numTracks = file.ReadUint8(); + file.Skip(2); // not sure what this is, later X-Tracker versions just skip over it + patHead.numRows = file.ReadUint16LE(); + patHead.patternLength = file.ReadUint32LE(); + } else + { + file.ReadStruct(patHead); + } + if(fileVersion < 6) + patHead.beat = 0; + + const ROWINDEX numRows = Clamp(ROWINDEX(patHead.numRows), ROWINDEX(1), MAX_PATTERN_ROWS); + const PATTERNINDEX pat = sndFile.Patterns.InsertAny(numRows); + if(pat == PATTERNINDEX_INVALID) + { + return pat; + } + + PatternRow m = sndFile.Patterns[pat].GetRow(0); + const CHANNELINDEX numChannels = std::min(static_cast<CHANNELINDEX>(sndFile.GetNumChannels() - 1), static_cast<CHANNELINDEX>(patHead.numTracks)); + + // When breaking to a pattern with less channels that the previous pattern, + // all voices in the now unused channels are killed: + for(CHANNELINDEX chn = numChannels + 1; chn < sndFile.GetNumChannels(); chn++) + { + m[chn].note = NOTE_NOTECUT; + } + + // Initialize tempo stuff + settings.beat = (patHead.beat >> 4); + bool tempoChange = settings.realBPMmode; + uint8 writeDelay = 0; + + // Counters for channel packing (including global track) + std::vector<uint8> channelCounter(numChannels + 1, 0); + + for(ROWINDEX row = 0; row < numRows; row++) + { + // Global track info counter reached 0 => read global track data + if(channelCounter[0] == 0) + { + uint8 globalInfo = file.ReadUint8(); + // 0x80: Packing counter (if not present, counter stays at 0) + if((globalInfo & patGlobPack) != 0) + { + channelCounter[0] = file.ReadUint8(); + } + + globalInfo &= patGlobMask; + + uint8 globalData = 0; + if(globalInfo != 0) + { + globalData = file.ReadUint8(); + } + + switch(globalInfo) + { + case 1: // Set Tick Frame Speed + settings.realBPMmode = false; + settings.tempoTicks = std::max(uint8(1), globalData); // Tempo in 1/4 rows per second + settings.tempoBPM = 0; // Automatically updated by X-Tracker + tempoChange = true; + break; + case 2: // Set BPM Speed (real BPM mode) + if(globalData) // DATA = 0 doesn't do anything + { + settings.realBPMmode = true; + settings.tempoBPM = globalData; // Tempo in real BPM (depends on rows per beat) + if(settings.beat != 0) + { + settings.tempoTicks = (globalData * settings.beat * 15); // Automatically updated by X-Tracker + } + tempoChange = true; + } + break; + case 3: // Set Beat + settings.beat = (globalData >> 4); + if(settings.beat != 0) + { + // Tempo changes only if we're in real BPM mode + tempoChange = settings.realBPMmode; + } else + { + // If beat is 0, change to tick speed mode, but keep current tempo + settings.realBPMmode = false; + } + break; + case 4: // Tick Delay + writeDelay = globalData; + break; + case 5: // Set External Flag + break; + case 6: // Slide Speed Up + if(globalData > 0) + { + uint8 &tempoData = (settings.realBPMmode) ? settings.tempoBPM : settings.tempoTicks; + if(tempoData < 256 - globalData) + { + tempoData += globalData; + } else + { + tempoData = 255; + } + tempoChange = true; + } + break; + case 7: // Slide Speed Down + if(globalData > 0) + { + uint8 &tempoData = (settings.realBPMmode) ? settings.tempoBPM : settings.tempoTicks; + if(tempoData > 1 + globalData) + { + tempoData -= globalData; + } else + { + tempoData = 1; + } + tempoChange = true; + } + break; + } + } else + { + channelCounter[0]--; + } + + // These will eventually be written to the pattern + int speed = 0, tempo = 0; + + if(tempoChange) + { + // Can't do anything if we're in BPM mode and there's no rows per beat set... + if(!settings.realBPMmode || settings.beat) + { + // My approach to convert X-Tracker's "tick speed" (1/4 rows per second): + // Tempo * 6 / Speed = Beats per Minute + // => Tempo * 6 / (Speed * 60) = Beats per Second + // => Tempo * 24 / (Speed * 60) = Rows per Second (4 rows per beat at tempo 6) + // => Tempo = 60 * Rows per Second * Speed / 24 + // For some reason, using settings.tempoTicks + 1 gives more accurate results than just settings.tempoTicks... (same problem in the old libmodplug DMF loader) + // Original unoptimized formula: + //const int tickspeed = (tempoRealBPMmode) ? std::max(1, (tempoData * beat * 4) / 60) : tempoData; + const int tickspeed = (settings.realBPMmode) ? std::max(1, settings.tempoBPM * settings.beat * 2) : ((settings.tempoTicks + 1) * 30); + // Try to find matching speed - try higher speeds first, so that effects like arpeggio and tremor work better. + for(speed = 255; speed >= 1; speed--) + { + // Original unoptimized formula: + // tempo = 30 * tickspeed * speed / 48; + tempo = tickspeed * speed / 48; + if(tempo >= 32 && tempo <= 255) + break; + } + Limit(tempo, 32, 255); + settings.internalTicks = static_cast<uint8>(std::max(1, speed)); + } else + { + tempoChange = false; + } + } + + m = sndFile.Patterns[pat].GetpModCommand(row, 1); // Reserve first channel for global effects + + for(CHANNELINDEX chn = 1; chn <= numChannels; chn++, m++) + { + // Track info counter reached 0 => read track data + if(channelCounter[chn] == 0) + { + const uint8 channelInfo = file.ReadUint8(); + //////////////////////////////////////////////////////////////// + // 0x80: Packing counter (if not present, counter stays at 0) + if((channelInfo & patCounter) != 0) + { + channelCounter[chn] = file.ReadUint8(); + } + + //////////////////////////////////////////////////////////////// + // 0x40: Instrument + bool slideNote = true; // If there is no instrument number next to a note, the note is not retriggered! + if((channelInfo & patInstr) != 0) + { + m->instr = file.ReadUint8(); + if(m->instr != 0) + { + slideNote = false; + } + } + + //////////////////////////////////////////////////////////////// + // 0x20: Note + if((channelInfo & patNote) != 0) + { + m->note = file.ReadUint8(); + if(m->note >= 1 && m->note <= 108) + { + m->note = static_cast<uint8>(Clamp(m->note + 24, NOTE_MIN, NOTE_MAX)); + settings.channels[chn].lastNote = m->note; + } else if(m->note >= 129 && m->note <= 236) + { + // "Buffer notes" for portamento (and other effects?) that are actually not played, but just "queued"... + m->note = static_cast<uint8>(Clamp((m->note & 0x7F) + 24, NOTE_MIN, NOTE_MAX)); + settings.channels[chn].noteBuffer = m->note; + m->note = NOTE_NONE; + } else if(m->note == 255) + { + m->note = NOTE_NOTECUT; + } + } + + // If there's just an instrument number, but no note, retrigger sample. + if(m->note == NOTE_NONE && m->instr > 0) + { + m->note = settings.channels[chn].lastNote; + m->instr = 0; + } + + if(m->IsNote()) + { + settings.channels[chn].playDir = false; + } + + uint8 effect1 = CMD_NONE, effect2 = CMD_NONE, effect3 = CMD_NONE; + uint8 effectParam1 = 0, effectParam2 = 0, effectParam3 = 0; + bool useMem2 = false, useMem3 = false; // Effect can use memory if necessary + + //////////////////////////////////////////////////////////////// + // 0x10: Volume + if((channelInfo & patVolume) != 0) + { + m->volcmd = VOLCMD_VOLUME; + m->vol = (file.ReadUint8() + 2) / 4; // Should be + 3 instead of + 2, but volume 1 is silent in X-Tracker. + } + + //////////////////////////////////////////////////////////////// + // 0x08: Instrument effect + if((channelInfo & patInsEff) != 0) + { + effect1 = file.ReadUint8(); + effectParam1 = file.ReadUint8(); + + switch(effect1) + { + case 1: // Stop Sample + m->note = NOTE_NOTECUT; + effect1 = CMD_NONE; + break; + case 2: // Stop Sample Loop + m->note = NOTE_KEYOFF; + effect1 = CMD_NONE; + break; + case 3: // Instrument Volume Override (aka "Restart") + m->note = settings.channels[chn].lastNote; + settings.channels[chn].playDir = false; + effect1 = CMD_NONE; + break; + case 4: // Sample Delay + effectParam1 = DMFdelay2MPT(effectParam1, settings.internalTicks); + if(effectParam1) + { + effect1 = CMD_S3MCMDEX; + effectParam1 = 0xD0 | (effectParam1); + } else + { + effect1 = CMD_NONE; + } + if(m->note == NOTE_NONE) + { + m->note = settings.channels[chn].lastNote; + settings.channels[chn].playDir = false; + } + break; + case 5: // Tremolo Retrig Sample (who invented those stupid effect names?) + effectParam1 = std::max(uint8(1), DMFdelay2MPT(effectParam1, settings.internalTicks)); + effect1 = CMD_RETRIG; + settings.channels[chn].playDir = false; + break; + case 6: // Offset + case 7: // Offset + 64k + case 8: // Offset + 128k + case 9: // Offset + 192k + // Put high offset on previous row + if(row > 0 && effect1 != settings.channels[chn].highOffset) + { + if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0xA0 | (effect1 - 6))).Row(row - 1).Channel(chn).RetryPreviousRow())) + { + settings.channels[chn].highOffset = effect1; + } + } + effect1 = CMD_OFFSET; + if(m->note == NOTE_NONE) + { + // Offset without note does also work in DMF. + m->note = settings.channels[chn].lastNote; + } + settings.channels[chn].playDir = false; + break; + case 10: // Invert Sample play direction ("Tekkno Invert") + effect1 = CMD_S3MCMDEX; + if(settings.channels[chn].playDir == false) + effectParam1 = 0x9F; + else + effectParam1 = 0x9E; + settings.channels[chn].playDir = !settings.channels[chn].playDir; + break; + default: + effect1 = CMD_NONE; + break; + } + } + + //////////////////////////////////////////////////////////////// + // 0x04: Note effect + if((channelInfo & patNoteEff) != 0) + { + effect2 = file.ReadUint8(); + effectParam2 = file.ReadUint8(); + + switch(effect2) + { + case 1: // Note Finetune (1/16th of a semitone signed 8-bit value, not 1/128th as the interface claims) + { + const auto fine = std::div(static_cast<int8>(effectParam2) * 8, 128); + if(m->IsNote()) + m->note = static_cast<ModCommand::NOTE>(Clamp(m->note + fine.quot, NOTE_MIN, NOTE_MAX)); + effect2 = CMD_FINETUNE; + effectParam2 = static_cast<uint8>(fine.rem) ^ 0x80; + } + break; + case 2: // Note Delay (wtf is the difference to Sample Delay?) + effectParam2 = DMFdelay2MPT(effectParam2, settings.internalTicks); + if(effectParam2) + { + effect2 = CMD_S3MCMDEX; + effectParam2 = 0xD0 | (effectParam2); + } else + { + effect2 = CMD_NONE; + } + useMem2 = true; + break; + case 3: // Arpeggio + effect2 = CMD_ARPEGGIO; + useMem2 = true; + break; + case 4: // Portamento Up + case 5: // Portamento Down + effectParam2 = DMFporta2MPT(effectParam2, settings.internalTicks, true); + effect2 = (effect2 == 4) ? CMD_PORTAMENTOUP : CMD_PORTAMENTODOWN; + useMem2 = true; + break; + case 6: // Portamento to Note + if(m->note == NOTE_NONE) + { + m->note = settings.channels[chn].noteBuffer; + } + effectParam2 = DMFporta2MPT(effectParam2, settings.internalTicks, false); + effect2 = CMD_TONEPORTAMENTO; + useMem2 = true; + break; + case 7: // Scratch to Note (neat! but we don't have such an effect...) + m->note = static_cast<ModCommand::NOTE>(Clamp(effectParam2 + 25, NOTE_MIN, NOTE_MAX)); + effect2 = CMD_TONEPORTAMENTO; + effectParam2 = 0xFF; + useMem2 = true; + break; + case 8: // Vibrato Sine + case 9: // Vibrato Triangle (ramp down should be close enough) + case 10: // Vibrato Square + // Put vibrato type on previous row + if(row > 0 && effect2 != settings.channels[chn].vibratoType) + { + if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0x30 | (effect2 - 8))).Row(row - 1).Channel(chn).RetryPreviousRow())) + { + settings.channels[chn].vibratoType = effect2; + } + } + effect2 = CMD_VIBRATO; + effectParam2 = DMFvibrato2MPT(effectParam2, settings.internalTicks); + useMem2 = true; + break; + case 11: // Note Tremolo + effectParam2 = DMFtremor2MPT(effectParam2, settings.internalTicks); + effect2 = CMD_TREMOR; + useMem2 = true; + break; + case 12: // Note Cut + effectParam2 = DMFdelay2MPT(effectParam2, settings.internalTicks); + if(effectParam2) + { + effect2 = CMD_S3MCMDEX; + effectParam2 = 0xC0 | (effectParam2); + } else + { + effect2 = CMD_NONE; + m->note = NOTE_NOTECUT; + } + useMem2 = true; + break; + default: + effect2 = CMD_NONE; + break; + } + } + + //////////////////////////////////////////////////////////////// + // 0x02: Volume effect + if((channelInfo & patVolEff) != 0) + { + effect3 = file.ReadUint8(); + effectParam3 = file.ReadUint8(); + + switch(effect3) + { + case 1: // Volume Slide Up + case 2: // Volume Slide Down + effectParam3 = DMFslide2MPT(effectParam3, settings.internalTicks, (effect3 == 1)); + effect3 = CMD_VOLUMESLIDE; + useMem3 = true; + break; + case 3: // Volume Tremolo (actually this is Tremor) + effectParam3 = DMFtremor2MPT(effectParam3, settings.internalTicks); + effect3 = CMD_TREMOR; + useMem3 = true; + break; + case 4: // Tremolo Sine + case 5: // Tremolo Triangle (ramp down should be close enough) + case 6: // Tremolo Square + // Put tremolo type on previous row + if(row > 0 && effect3 != settings.channels[chn].tremoloType) + { + if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0x40 | (effect3 - 4))).Row(row - 1).Channel(chn).RetryPreviousRow())) + { + settings.channels[chn].tremoloType = effect3; + } + } + effect3 = CMD_TREMOLO; + effectParam3 = DMFvibrato2MPT(effectParam3, settings.internalTicks); + useMem3 = true; + break; + case 7: // Set Balance + effect3 = CMD_PANNING8; + break; + case 8: // Slide Balance Left + case 9: // Slide Balance Right + effectParam3 = DMFslide2MPT(effectParam3, settings.internalTicks, (effect3 == 8)); + effect3 = CMD_PANNINGSLIDE; + useMem3 = true; + break; + case 10: // Balance Vibrato Left/Right (always sine modulated) + effect3 = CMD_PANBRELLO; + effectParam3 = DMFvibrato2MPT(effectParam3, settings.internalTicks); + useMem3 = true; + break; + default: + effect3 = CMD_NONE; + break; + } + } + + // Let's see if we can help the effect swapper by reducing some effect parameters to "continue" parameters. + if(useMem2) + ApplyEffectMemory(m, row, sndFile.GetNumChannels(), effect2, effectParam2); + if(useMem3) + ApplyEffectMemory(m, row, sndFile.GetNumChannels(), effect3, effectParam3); + + // I guess this is close enough to "not retriggering the note" + if(slideNote && m->IsNote()) + { + if(effect2 == CMD_NONE) + { + effect2 = CMD_TONEPORTAMENTO; + effectParam2 = 0xFF; + } else if(effect3 == CMD_NONE && effect2 != CMD_TONEPORTAMENTO) // Tone portamentos normally go in effect #2 + { + effect3 = CMD_TONEPORTAMENTO; + effectParam3 = 0xFF; + } + } + // If one of the effects is unused, temporarily put volume commands in there + if(m->volcmd == VOLCMD_VOLUME) + { + if(effect2 == CMD_NONE) + { + effect2 = CMD_VOLUME; + effectParam2 = m->vol; + m->volcmd = VOLCMD_NONE; + } else if(effect3 == CMD_NONE) + { + effect3 = CMD_VOLUME; + effectParam3 = m->vol; + m->volcmd = VOLCMD_NONE; + } + } + + ModCommand::TwoRegularCommandsToMPT(effect2, effectParam2, effect3, effectParam3); + + if(m->volcmd == VOLCMD_NONE && effect2 != VOLCMD_NONE) + { + m->volcmd = effect2; + m->vol = effectParam2; + } + // Prefer instrument effects over any other effects + if(effect1 != CMD_NONE) + { + ModCommand::TwoRegularCommandsToMPT(effect3, effectParam3, effect1, effectParam1); + if(m->volcmd == VOLCMD_NONE && effect3 != VOLCMD_NONE) + { + m->volcmd = effect3; + m->vol = effectParam3; + } + m->command = effect1; + m->param = effectParam1; + } else if(effect3 != CMD_NONE) + { + m->command = effect3; + m->param = effectParam3; + } + + } else + { + channelCounter[chn]--; + } + } // End for all channels + + // Now we can try to write tempo information. + if(tempoChange) + { + tempoChange = false; + + sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_TEMPO, static_cast<ModCommand::PARAM>(tempo)).Row(row).Channel(0).RetryNextRow()); + sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, static_cast<ModCommand::PARAM>(speed)).Row(row).RetryNextRow()); + } + // Try to put delay effects somewhere as well + if(writeDelay & 0xF0) + { + sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, 0xE0 | (writeDelay >> 4)).Row(row).AllowMultiple()); + } + if(writeDelay & 0x0F) + { + const uint8 param = (writeDelay & 0x0F) * settings.internalTicks / 15; + sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, 0x60u | Clamp(param, uint8(1), uint8(15))).Row(row).AllowMultiple()); + } + writeDelay = 0; + } // End for all rows + + return pat; +} + + +static bool ValidateHeader(const DMFFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.signature, "DDMF", 4) + || !fileHeader.version || fileHeader.version > 10) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDMF(MemoryFileReader file, const uint64 *pfilesize) +{ + DMFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadDMF(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + DMFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_DMF); + + m_modFormat.formatName = MPT_UFORMAT("X-Tracker v{}")(fileHeader.version); + m_modFormat.type = U_("dmf"); + m_modFormat.charset = mpt::Charset::CP437; + + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songname); + m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.composer)); + + FileHistory mptHistory; + mptHistory.loadDate.tm_mday = Clamp(fileHeader.creationDay, uint8(1), uint8(31)); + mptHistory.loadDate.tm_mon = Clamp(fileHeader.creationMonth, uint8(1), uint8(12)) - 1; + mptHistory.loadDate.tm_year = fileHeader.creationYear; + m_FileHistory.clear(); + m_FileHistory.push_back(mptHistory); + + // Go through all chunks now... cannot use our standard IFF chunk reader here because early X-Tracker versions write some malformed chunk headers... fun code ahead! + ChunkReader::ChunkList<DMFChunk> chunks; + while(file.CanRead(sizeof(DMFChunk))) + { + DMFChunk chunkHeader; + file.Read(chunkHeader); + uint32 chunkLength = chunkHeader.length, chunkSkip = 0; + // When loop start was added to version 3, the chunk size was not updated... + if(fileHeader.version == 3 && chunkHeader.GetID() == DMFChunk::idSEQU) + chunkSkip = 2; + // ...and when the loop end was added to version 4, it was also note updated! Luckily they fixed it in version 5. + else if(fileHeader.version == 4 && chunkHeader.GetID() == DMFChunk::idSEQU) + chunkSkip = 4; + // Earlier X-Tracker versions also write a garbage length for the SMPD chunk if samples are compressed. + // I don't know when exactly this stopped, but I have no version 5-7 files to check (and no X-Tracker version that writes those versions). + // Since this is practically always the last chunk in the file, the following code is safe for those versions, though. + else if(fileHeader.version < 8 && chunkHeader.GetID() == DMFChunk::idSMPD) + chunkLength = uint32_max; + chunks.chunks.push_back(ChunkReader::Item<DMFChunk>{chunkHeader, file.ReadChunk(chunkLength)}); + file.Skip(chunkSkip); + } + FileReader chunk; + + // Read order list + chunk = chunks.GetChunk(DMFChunk::idSEQU); + ORDERINDEX seqLoopStart = 0, seqLoopEnd = ORDERINDEX_MAX; + if(fileHeader.version >= 3) + seqLoopStart = chunk.ReadUint16LE(); + if(fileHeader.version >= 4) + seqLoopEnd = chunk.ReadUint16LE(); + // HIPOMATK.DMF has a loop end of 0, other v4 files have proper loop ends. Later X-Tracker versions import it as-is but it cannot be intentional. + // We just assume that this feature might have been buggy in early v4 versions and ignore the loop end in that case. + if(fileHeader.version == 4 && seqLoopEnd == 0) + seqLoopEnd = ORDERINDEX_MAX; + ReadOrderFromFile<uint16le>(Order(), chunk, chunk.BytesLeft() / 2); + LimitMax(seqLoopStart, Order().GetLastIndex()); + LimitMax(seqLoopEnd, Order().GetLastIndex()); + + // Read patterns + chunk = chunks.GetChunk(DMFChunk::idPATT); + if(chunk.IsValid() && (loadFlags & loadPatternData)) + { + DMFPatterns patHeader; + chunk.ReadStruct(patHeader); + m_nChannels = Clamp<uint8, uint8>(patHeader.numTracks, 1, 32) + 1; // + 1 for global track (used for tempo stuff) + + // First, find out where all of our patterns are... + std::vector<FileReader> patternChunks(patHeader.numPatterns); + for(auto &patternChunk : patternChunks) + { + const uint8 headerSize = fileHeader.version < 3 ? 9 : 8; + chunk.Skip(headerSize - sizeof(uint32le)); + const uint32 patLength = chunk.ReadUint32LE(); + chunk.SkipBack(headerSize); + patternChunk = chunk.ReadChunk(headerSize + patLength); + } + + // Now go through the order list and load them. + DMFPatternSettings settings(GetNumChannels()); + + Patterns.ResizeArray(Order().GetLength()); + for(PATTERNINDEX &pat : Order()) + { + // Create one pattern for each order item, as the same pattern can be played with different settings + if(pat < patternChunks.size()) + { + pat = ConvertDMFPattern(patternChunks[pat], fileHeader.version, settings, *this); + } + } + // Write loop end if necessary + if(Order().IsValidPat(seqLoopEnd) && (seqLoopStart > 0 || seqLoopEnd < Order().GetLastIndex())) + { + PATTERNINDEX pat = Order()[seqLoopEnd]; + Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(seqLoopStart)).Row(Patterns[pat].GetNumRows() - 1).RetryPreviousRow()); + } + } + + // Read song message + chunk = chunks.GetChunk(DMFChunk::idCMSG); + if(chunk.IsValid()) + { + // The song message seems to start at a 1 byte offset. + // The skipped byte seems to always be 0. + // This also matches how XT 1.03 itself displays the song message. + chunk.Skip(1); + m_songMessage.ReadFixedLineLength(chunk, chunk.BytesLeft(), 40, 0); + } + + // Read sample headers + data + FileReader sampleDataChunk = chunks.GetChunk(DMFChunk::idSMPD); + chunk = chunks.GetChunk(DMFChunk::idSMPI); + m_nSamples = chunk.ReadUint8(); + + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + const uint8 nameLength = (fileHeader.version < 2) ? 30 : chunk.ReadUint8(); + chunk.ReadString<mpt::String::spacePadded>(m_szNames[smp], nameLength); + DMFSampleHeader sampleHeader; + ModSample &sample = Samples[smp]; + chunk.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(sample); + + // Read library name in version 8 files + if(fileHeader.version >= 8) + chunk.ReadString<mpt::String::spacePadded>(sample.filename, 8); + + // Filler + CRC + chunk.Skip(fileHeader.version > 1 ? 6 : 2); + + // Now read the sample data from the data chunk + FileReader sampleData = sampleDataChunk.ReadChunk(sampleDataChunk.ReadUint32LE()); + if(sampleData.IsValid() && (loadFlags & loadSampleData)) + { + SampleIO( + sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (sampleHeader.flags & DMFSampleHeader::smpCompMask) == DMFSampleHeader::smpComp1 ? SampleIO::DMF : SampleIO::signedPCM) + .ReadSample(sample, sampleData); + } + } + + InitializeChannels(); + m_SongFlags = SONG_LINEARSLIDES | SONG_ITCOMPATGXX; // this will be converted to IT format by MPT. SONG_ITOLDEFFECTS is not set because of tremor and vibrato. + m_nDefaultSpeed = 6; + m_nDefaultTempo.Set(120); + m_nDefaultGlobalVolume = 256; + m_nSamplePreAmp = m_nVSTiVolume = 48; + m_playBehaviour.set(kApplyOffsetWithoutNote); + + return true; +} + + +/////////////////////////////////////////////////////////////////////// +// DMF Compression + +struct DMFHNode +{ + int16 left, right; + uint8 value; +}; + +struct DMFHTree +{ + BitReader file; + int lastnode = 0, nodecount = 0; + DMFHNode nodes[256]{}; + + DMFHTree(FileReader &file) + : file(file) + { + } + + // + // tree: [8-bit value][12-bit index][12-bit index] = 32-bit + // + + void DMFNewNode() + { + int actnode = nodecount; + if(actnode > 255) return; + nodes[actnode].value = static_cast<uint8>(file.ReadBits(7)); + bool isLeft = file.ReadBits(1) != 0; + bool isRight = file.ReadBits(1) != 0; + actnode = lastnode; + if(actnode > 255) return; + nodecount++; + lastnode = nodecount; + if(isLeft) + { + nodes[actnode].left = (int16)lastnode; + DMFNewNode(); + } else + { + nodes[actnode].left = -1; + } + lastnode = nodecount; + if(isRight) + { + nodes[actnode].right = (int16)lastnode; + DMFNewNode(); + } else + { + nodes[actnode].right = -1; + } + } +}; + + +uintptr_t DMFUnpack(FileReader &file, uint8 *psample, uint32 maxlen) +{ + DMFHTree tree(file); + uint8 value = 0, delta = 0; + + try + { + tree.DMFNewNode(); + if(tree.nodes[0].left < 0 || tree.nodes[0].right < 0) + return tree.file.GetPosition(); + for(uint32 i = 0; i < maxlen; i++) + { + int actnode = 0; + bool sign = tree.file.ReadBits(1) != 0; + do + { + if(tree.file.ReadBits(1)) + actnode = tree.nodes[actnode].right; + else + actnode = tree.nodes[actnode].left; + if(actnode > 255) break; + delta = tree.nodes[actnode].value; + } while((tree.nodes[actnode].left >= 0) && (tree.nodes[actnode].right >= 0)); + if(sign) delta ^= 0xFF; + value += delta; + psample[i] = value; + } + } catch(const BitReader::eof &) + { + //AddToLog(LogWarning, "Truncated DMF sample block"); + } + return tree.file.GetPosition(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsm.cpp new file mode 100644 index 00000000..5b7ee2e9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsm.cpp @@ -0,0 +1,339 @@ +/* + * Load_dsm.cpp + * ------------ + * Purpose: Digisound Interface Kit (DSIK) Internal Format (DSM v2 / RIFF) module loader + * Notes : 1. There is also another fundamentally different DSIK DSM v1 module format, not handled here. + * MilkyTracker can load it, but the only files of this format seen in the wild are also + * available in their original format, so I did not bother implementing it so far. + * + * 2. Using both PLAY.EXE v1.02 and v2.00, commands not supported in MOD do not seem to do + * anything at all. + * In particular commands 0x11-0x13 handled below are ignored, and no files have been spotted + * in the wild using any commands > 0x0F at all. + * S3M-style retrigger does not seem to exist - it is translated to volume slides by CONV.EXE, + * and J00 in S3M files is not converted either. S3M pattern loops (SBx) are not converted + * properly by CONV.EXE and completely ignored by PLAY.EXE. + * Command 8 (set panning) uses 00-80 for regular panning and A4 for surround, probably + * making DSIK one of the first applications to use this particular encoding scheme still + * used in "extended" S3Ms today. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct DSMChunk +{ + char magic[4]; + uint32le size; +}; + +MPT_BINARY_STRUCT(DSMChunk, 8) + + +struct DSMSongHeader +{ + char songName[28]; + uint16le fileVersion; + uint16le flags; + uint16le orderPos; + uint16le restartPos; + uint16le numOrders; + uint16le numSamples; + uint16le numPatterns; + uint16le numChannels; + uint8le globalVol; + uint8le mastervol; + uint8le speed; + uint8le bpm; + uint8le panPos[16]; + uint8le orders[128]; +}; + +MPT_BINARY_STRUCT(DSMSongHeader, 192) + + +struct DSMSampleHeader +{ + char filename[13]; + uint16le flags; + uint8le volume; + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint32le dataPtr; // Interal sample pointer during playback in DSIK + uint32le sampleRate; + char sampleName[28]; + + // Convert a DSM sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + mptSmp.nC5Speed = sampleRate; + mptSmp.uFlags.set(CHN_LOOP, (flags & 1) != 0); + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4; + } + + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat() const + { + SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM); + if(flags & 0x40) + sampleIO |= SampleIO::deltaPCM; // fairlight.dsm by Comrade J + else if(flags & 0x02) + sampleIO |= SampleIO::signedPCM; + if(flags & 0x04) + sampleIO |= SampleIO::_16bit; + return sampleIO; + } +}; + +MPT_BINARY_STRUCT(DSMSampleHeader, 64) + + +struct DSMHeader +{ + char fileMagic0[4]; + char fileMagic1[4]; + char fileMagic2[4]; +}; + +MPT_BINARY_STRUCT(DSMHeader, 12) + + +static bool ValidateHeader(const DSMHeader &fileHeader) +{ + if(!std::memcmp(fileHeader.fileMagic0, "RIFF", 4) + && !std::memcmp(fileHeader.fileMagic2, "DSMF", 4)) + { + // "Normal" DSM files with RIFF header + // <RIFF> <file size> <DSMF> + return true; + } else if(!std::memcmp(fileHeader.fileMagic0, "DSMF", 4)) + { + // DSM files with alternative header + // <DSMF> <4 bytes, usually 4x NUL or RIFF> <file size> <4 bytes, usually DSMF but not always> + return true; + } else + { + return false; + } +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDSM(MemoryFileReader file, const uint64 *pfilesize) +{ + DSMHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + if(std::memcmp(fileHeader.fileMagic0, "DSMF", 4) == 0) + { + if(!file.Skip(4)) + { + return ProbeWantMoreData; + } + } + DSMChunk chunkHeader; + if(!file.ReadStruct(chunkHeader)) + { + return ProbeWantMoreData; + } + if(std::memcmp(chunkHeader.magic, "SONG", 4)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadDSM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + DSMHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(std::memcmp(fileHeader.fileMagic0, "DSMF", 4) == 0) + { + file.Skip(4); + } + DSMChunk chunkHeader; + if(!file.ReadStruct(chunkHeader)) + { + return false; + } + // Technically, the song chunk could be anywhere in the file, but we're going to simplify + // things by not using a chunk header here and just expect it to be right at the beginning. + if(std::memcmp(chunkHeader.magic, "SONG", 4)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + DSMSongHeader songHeader; + file.ReadStructPartial(songHeader, chunkHeader.size); + if(songHeader.numOrders > 128 || songHeader.numChannels > 16 || songHeader.numPatterns > 256 || songHeader.restartPos > 128) + { + return false; + } + + InitializeGlobals(MOD_TYPE_DSM); + + m_modFormat.formatName = U_("DSIK Format"); + m_modFormat.type = U_("dsm"); + m_modFormat.charset = mpt::Charset::CP437; + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, songHeader.songName); + m_nChannels = std::max(songHeader.numChannels.get(), uint16(1)); + m_nDefaultSpeed = songHeader.speed; + m_nDefaultTempo.Set(songHeader.bpm); + m_nDefaultGlobalVolume = std::min(songHeader.globalVol.get(), uint8(64)) * 4u; + if(!m_nDefaultGlobalVolume) m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + if(songHeader.mastervol == 0x80) + { + m_nSamplePreAmp = std::min(256u / m_nChannels, 128u); + } else + { + m_nSamplePreAmp = songHeader.mastervol & 0x7F; + } + + // Read channel panning + for(CHANNELINDEX chn = 0; chn < 16; chn++) + { + ChnSettings[chn].Reset(); + if(songHeader.panPos[chn] <= 0x80) + { + ChnSettings[chn].nPan = songHeader.panPos[chn] * 2; + } + } + + ReadOrderFromArray(Order(), songHeader.orders, songHeader.numOrders, 0xFF, 0xFE); + if(songHeader.restartPos < songHeader.numOrders) + Order().SetRestartPos(songHeader.restartPos); + + // Read pattern and sample chunks + PATTERNINDEX patNum = 0; + while(file.ReadStruct(chunkHeader)) + { + FileReader chunk = file.ReadChunk(chunkHeader.size); + + if(!memcmp(chunkHeader.magic, "PATT", 4) && (loadFlags & loadPatternData)) + { + // Read pattern + if(!Patterns.Insert(patNum, 64)) + { + continue; + } + chunk.Skip(2); + + ModCommand dummy = ModCommand::Empty(); + ROWINDEX row = 0; + while(chunk.CanRead(1) && row < 64) + { + uint8 flag = chunk.ReadUint8(); + if(!flag) + { + row++; + continue; + } + + CHANNELINDEX chn = (flag & 0x0F); + ModCommand &m = (chn < GetNumChannels() ? *Patterns[patNum].GetpModCommand(row, chn) : dummy); + + if(flag & 0x80) + { + uint8 note = chunk.ReadUint8(); + if(note) + { + if(note <= 12 * 9) note += 11 + NOTE_MIN; + m.note = note; + } + } + if(flag & 0x40) + { + m.instr = chunk.ReadUint8(); + } + if (flag & 0x20) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = std::min(chunk.ReadUint8(), uint8(64)); + } + if(flag & 0x10) + { + auto [command, param] = chunk.ReadArray<uint8, 2>(); + switch(command) + { + // Portamentos + case 0x11: + case 0x12: + command &= 0x0F; + break; + // 3D Sound (?) + case 0x13: + command = 'X' - 55; + param = 0x91; + break; + default: + // Volume + Offset (?) + if(command > 0x10) + command = ((command & 0xF0) == 0x20) ? 0x09 : 0xFF; + } + m.command = command; + m.param = param; + ConvertModCommand(m); + } + } + patNum++; + } else if(!memcmp(chunkHeader.magic, "INST", 4) && CanAddMoreSamples()) + { + // Read sample + m_nSamples++; + ModSample &sample = Samples[m_nSamples]; + + DSMSampleHeader sampleHeader; + chunk.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(sample); + + m_szNames[m_nSamples] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.sampleName); + + if(loadFlags & loadSampleData) + { + sampleHeader.GetSampleFormat().ReadSample(sample, chunk); + } + } + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsym.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsym.cpp new file mode 100644 index 00000000..d883def9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dsym.cpp @@ -0,0 +1,613 @@ +/* + * Load_dsym.cpp + * ------------- + * Purpose: Digital Symphony module loader + * Notes : Based on information from the DSym_Info file and sigma-delta decompression code from TimPlayer. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "BitReader.h" + +OPENMPT_NAMESPACE_BEGIN + +struct DSymFileHeader +{ + char magic[8]; + uint8le version; // 0 / 1 + uint8le numChannels; // 1...8 + uint16le numOrders; // 0...4096 + uint16le numTracks; // 0...4096 + uint16le infoLenLo; + uint8le infoLenHi; + + bool Validate() const + { + return !std::memcmp(magic, "\x02\x01\x13\x13\x14\x12\x01\x0B", 8) + && version <= 1 + && numChannels >= 1 && numChannels <= 8 + && numOrders <= 4096 + && numTracks <= 4096; + } + + uint64 GetHeaderMinimumAdditionalSize() const + { + return 72u; + } +}; + +MPT_BINARY_STRUCT(DSymFileHeader, 17) + + +static std::vector<std::byte> DecompressDSymLZW(FileReader &file, uint32 size) +{ + BitReader bitFile(file); + const auto startPos = bitFile.GetPosition(); + + // In the best case, 13 bits decode 8192 bytes, a ratio of approximately 1:5042. + // Too much for reserving memory in case of malformed files, just choose an arbitrary but realistic upper limit. + std::vector<std::byte> output; + output.reserve(std::min(size, std::min(mpt::saturate_cast<uint32>(file.BytesLeft()), Util::MaxValueOfType(size) / 50u) * 50u)); + + static constexpr uint16 lzwBits = 13, MaxNodes = 1 << lzwBits; + static constexpr uint16 ResetDict = 256, EndOfStream = 257; + + struct LZWEntry + { + uint16 prev; + std::byte value; + }; + std::vector<LZWEntry> dictionary(MaxNodes); + std::vector<std::byte> match(MaxNodes); + + // Initialize dictionary + for(int i = 0; i < 256; i++) + { + dictionary[i].prev = MaxNodes; + dictionary[i].value = static_cast<std::byte>(i); + } + uint8 codeSize = 9; + uint16 prevCode = 0; + uint16 nextIndex = 257; + while(true) + { + // Read next code + const auto newCode = static_cast<uint16>(bitFile.ReadBits(codeSize)); + if(newCode == EndOfStream || newCode > nextIndex || output.size() >= size) + break; + + // Reset dictionary + if(newCode == ResetDict) + { + codeSize = 9; + prevCode = 0; + nextIndex = 257; + continue; + } + + // Output + auto code = (newCode < nextIndex) ? newCode : prevCode; + auto writeOffset = MaxNodes; + do + { + match[--writeOffset] = dictionary[code].value; + code = dictionary[code].prev; + } while(code < MaxNodes); + output.insert(output.end(), match.begin() + writeOffset, match.end()); + + // Handling for KwKwK problem + if(newCode == nextIndex) + output.push_back(match[writeOffset]); + + // Add to dictionary + if(nextIndex < MaxNodes) + { + // Special case for FULLEFFECT, NARCOSIS and NEWDANCE, which end with a dictionary size of 512 + // right before the end-of-stream token, but the code size is expected to be 9 + if(output.size() >= size) + continue; + + dictionary[nextIndex].value = match[writeOffset]; + dictionary[nextIndex].prev = prevCode; + + nextIndex++; + if(nextIndex != MaxNodes && nextIndex == (1u << codeSize)) + codeSize++; + } + + prevCode = newCode; + } + MPT_ASSERT(output.size() == size); + + // Align length to 4 bytes + file.Seek(startPos + ((bitFile.GetPosition() - startPos + 3u) & ~FileReader::off_t(3))); + return output; +} + + +static std::vector<std::byte> DecompressDSymSigmaDelta(FileReader &file, uint32 size) +{ + const uint8 maxRunLength = std::max(file.ReadUint8(), uint8(1)); + + BitReader bitFile(file); + const auto startPos = bitFile.GetPosition(); + + // In the best case, sigma-delta compression represents each sample point as one bit. + // As a result, if we have a file length of n, we know that the sample can be at most n*8 sample points long. + LimitMax(size, std::min(mpt::saturate_cast<uint32>(file.BytesLeft()), Util::MaxValueOfType(size) / 8u) * 8u); + std::vector<std::byte> output(size); + + uint32 pos = 0; + uint8 runLength = maxRunLength; + uint8 numBits = 8; + uint8 accum = static_cast<uint8>(bitFile.ReadBits(numBits)); + output[pos++] = mpt::byte_cast<std::byte>(accum); + + while(pos < size) + { + const uint32 value = bitFile.ReadBits(numBits); + // Increase bit width + if(value == 0) + { + if(numBits >= 9) + break; + numBits++; + runLength = maxRunLength; + continue; + } + + if(value & 1) + accum -= static_cast<uint8>(value >> 1); + else + accum += static_cast<uint8>(value >> 1); + output[pos++] = mpt::byte_cast<std::byte>(accum); + + // Reset run length if high bit is set + if((value >> (numBits - 1u)) != 0) + { + runLength = maxRunLength; + continue; + } + // Decrease bit width + if(--runLength == 0) + { + if(numBits > 1) + numBits--; + runLength = maxRunLength; + } + } + + // Align length to 4 bytes + file.Seek(startPos + ((bitFile.GetPosition() - startPos + 3u) & ~FileReader::off_t(3))); + return output; +} + + +static bool ReadDSymChunk(FileReader &file, std::vector<std::byte> &data, uint32 size) +{ + const uint8 packingType = file.ReadUint8(); + if(packingType > 1) + return false; + if(packingType) + { + try + { + data = DecompressDSymLZW(file, size); + } catch(const BitReader::eof &) + { + return false; + } + } else + { + if(!file.CanRead(size)) + return false; + file.ReadVector(data, size); + } + return data.size() >= size; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDSym(MemoryFileReader file, const uint64 *pfilesize) +{ + DSymFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return ProbeWantMoreData; + if(!fileHeader.Validate()) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, fileHeader.GetHeaderMinimumAdditionalSize()); +} + + +bool CSoundFile::ReadDSym(FileReader &file, ModLoadingFlags loadFlags) +{ + DSymFileHeader fileHeader; + + file.Rewind(); + if(!file.ReadStruct(fileHeader) || !fileHeader.Validate()) + return false; + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(fileHeader.GetHeaderMinimumAdditionalSize()))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_MOD); + m_SongFlags.set(SONG_IMPORTED | SONG_AMIGALIMITS); + m_SongFlags.reset(SONG_ISAMIGA); + m_nChannels = fileHeader.numChannels; + m_nSamples = 63; + + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + InitChannel(chn); + ChnSettings[chn].nPan = (((chn & 3) == 1) || ((chn & 3) == 2)) ? 64 : 192; + } + + uint8 sampleNameLength[64] = {}; + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + Samples[smp].Initialize(MOD_TYPE_MOD); + sampleNameLength[smp] = file.ReadUint8(); + if(!(sampleNameLength[smp] & 0x80)) + Samples[smp].nLength = file.ReadUint24LE() << 1; + } + + file.ReadSizedString<uint8le, mpt::String::spacePadded>(m_songName); + + const auto allowedCommands = file.ReadArray<uint8, 8>(); + + std::vector<std::byte> sequenceData; + if(fileHeader.numOrders) + { + const uint32 sequenceSize = fileHeader.numOrders * fileHeader.numChannels * 2u; + if(!ReadDSymChunk(file, sequenceData, sequenceSize)) + return false; + } + const auto sequence = mpt::as_span(reinterpret_cast<uint16le *>(sequenceData.data()), sequenceData.size() / 2u); + + std::vector<std::byte> trackData; + trackData.reserve(fileHeader.numTracks * 256u); + // For some reason, patterns are stored in 512K chunks + for(uint16 offset = 0; offset < fileHeader.numTracks; offset += 2000) + { + const uint32 chunkSize = std::min(fileHeader.numTracks - offset, 2000) * 256; + std::vector<std::byte> chunk; + if(!ReadDSymChunk(file, chunk, chunkSize)) + return false; + trackData.insert(trackData.end(), chunk.begin(), chunk.end()); + } + const auto tracks = mpt::byte_cast<mpt::span<uint8>>(mpt::as_span(trackData)); + + Order().resize(fileHeader.numOrders); + for(ORDERINDEX pat = 0; pat < fileHeader.numOrders; pat++) + { + Order()[pat] = pat; + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + continue; + + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + const uint16 track = sequence[pat * m_nChannels + chn]; + if(track >= fileHeader.numTracks) + continue; + + ModCommand *m = Patterns[pat].GetpModCommand(0, chn); + for(ROWINDEX row = 0; row < 64; row++, m += m_nChannels) + { + const auto data = tracks.subspan(track * 256 + row * 4, 4); + m->note = data[0] & 0x3F; + if(m->note) + m->note += 47 + NOTE_MIN; + else + m->note = NOTE_NONE; + + m->instr = (data[0] >> 6) | ((data[1] & 0x0F) << 2); + const uint8 command = (data[1] >> 6) | ((data[2] & 0x0F) << 2); + const uint16 param = (data[2] >> 4) | (data[3] << 4); + + if(!(allowedCommands[command >> 3u] & (1u << (command & 7u)))) + continue; + if(command == 0 && param == 0) + continue; + + m->command = command; + m->param = static_cast<uint8>(param); + m->vol = static_cast<ModCommand::VOL>(param >> 8); + + switch(command) + { + case 0x00: // 00 xyz Normal play or Arpeggio + Volume Slide Up + case 0x01: // 01 xyy Slide Up + Volume Slide Up + case 0x02: // 01 xyy Slide Up + Volume Slide Up + case 0x20: // 20 xyz Normal play or Arpeggio + Volume Slide Down + case 0x21: // 21 xyy Slide Up + Volume Slide Down + case 0x22: // 22 xyy Slide Down + Volume Slide Down + m->command &= 0x0F; + ConvertModCommand(*m); + if(m->vol) + m->volcmd = (command < 0x20) ? VOLCMD_VOLSLIDEUP : VOLCMD_VOLSLIDEDOWN; + break; + case 0x03: // 03 xyy Tone Portamento + case 0x04: // 04 xyz Vibrato + case 0x05: // 05 xyz Tone Portamento + Volume Slide + case 0x06: // 06 xyz Vibrato + Volume Slide + case 0x07: // 07 xyz Tremolo + case 0x0C: // 0C xyy Set Volume + ConvertModCommand(*m); + break; + case 0x09: // 09 xxx Set Sample Offset + m->command = CMD_OFFSET; + m->param = static_cast<ModCommand::PARAM>(param >> 1); + if(param >= 0x200) + { + m->volcmd = VOLCMD_OFFSET; + m->vol >>= 1; + } + break; + case 0x0A: // 0A xyz Volume Slide + Fine Slide Up + case 0x2A: // 2A xyz Volume Slide + Fine Slide Down + if(param < 0xFF) + { + m->command &= 0x0F; + ConvertModCommand(*m); + } else + { + m->command = CMD_MODCMDEX; + m->param = static_cast<ModCommand::PARAM>(((command < 0x20) ? 0x10 : 0x20) | (param >> 8)); + if(param & 0xF0) + { + m->volcmd = VOLCMD_VOLSLIDEUP; + m->vol = static_cast<ModCommand::VOL>((param >> 4) & 0x0F); + } else + { + m->volcmd = VOLCMD_VOLSLIDEDOWN; + m->vol = static_cast<ModCommand::VOL>(param & 0x0F); + } + } + break; + case 0x0B: // 0B xxx Position Jump + case 0x0F: // 0F xxx Set Speed + m->command = (command == 0x0B) ? CMD_POSITIONJUMP : CMD_SPEED; + m->param = mpt::saturate_cast<ModCommand::PARAM>(param); + break; + case 0x0D: // 0D xyy Pattern Break (not BCD-encoded like in MOD) + m->command = CMD_PATTERNBREAK; + if(m->param > 63) + m->param = 0; + break; + case 0x10: // 10 xxy Filter Control (not implemented in Digital Symphony) + case 0x13: // 13 xxy Glissando Control + case 0x14: // 14 xxy Set Vibrato Waveform + case 0x15: // 15 xxy Set Fine Tune + case 0x17: // 17 xxy Set Tremolo Waveform + case 0x1F: // 1F xxy Invert Loop + m->command = CMD_MODCMDEX; + m->param = (command << 4) | (m->param & 0x0F); + break; + case 0x16: // 16 xxx Jump to Loop + case 0x19: // 19 xxx Retrig Note + case 0x1C: // 1C xxx Note Cut + case 0x1D: // 1D xxx Note Delay + case 0x1E: // 1E xxx Pattern Delay + m->command = CMD_MODCMDEX; + m->param = (command << 4) | static_cast<ModCommand::PARAM>(std::min(param, uint16(0x0F))); + break; + case 0x11: // 11 xyy Fine Slide Up + Fine Volume Slide Up + case 0x12: // 12 xyy Fine Slide Down + Fine Volume Slide Up + case 0x1A: // 1A xyy Fine Slide Up + Fine Volume Slide Down + case 0x1B: // 1B xyy Fine Slide Down + Fine Volume Slide Down + m->command = CMD_MODCMDEX; + if(m->param & 0xFF) + { + m->param = static_cast<ModCommand::PARAM>(((command == 0x11 || command == 0x1A) ? 0x10 : 0x20) | (param & 0x0F)); + if(param & 0xF00) + m->volcmd = (command >= 0x1A) ? VOLCMD_FINEVOLDOWN : VOLCMD_FINEVOLUP; + } else + { + m->param = static_cast<ModCommand::PARAM>(((command >= 0x1A) ? 0xB0 : 0xA0) | (param >> 8)); + } + break; + case 0x2F: // 2F xxx Set Tempo + if(param > 0) + { + m->command = CMD_TEMPO; + m->param = mpt::saturate_cast<ModCommand::PARAM>(std::max(8, param + 4) / 8); +#ifdef MODPLUG_TRACKER + m->param = std::max(m->param, ModCommand::PARAM(0x20)); +#endif + } else + { + m->command = CMD_NONE; + } + break; + case 0x2B: // 2B xyy Line Jump + m->command = CMD_PATTERNBREAK; + for(CHANNELINDEX brkChn = 0; brkChn < m_nChannels; brkChn++) + { + ModCommand &cmd = *(m - chn + brkChn); + if(cmd.command != CMD_NONE) + continue; + cmd.command = CMD_POSITIONJUMP; + cmd.param = mpt::saturate_cast<ModCommand::PARAM>(pat); + } + break; + case 0x30: // 30 xxy Set Stereo + m->command = CMD_PANNING8; + if(param & 7) + { + static constexpr uint8 panning[8] = {0x00, 0x00, 0x2B, 0x56, 0x80, 0xAA, 0xD4, 0xFF}; + m->param = panning[param & 7]; + } else if((param >> 4) != 0x80) + { + m->param = static_cast<ModCommand::PARAM>(param >> 4); + if(m->param < 0x80) + m->param += 0x80; + else + m->param = 0xFF - m->param; + } else + { + m->command = CMD_NONE; + } + break; + case 0x32: // 32 xxx Unset Sample Repeat + m->command = CMD_NONE; + m->param = 0; + if(m->note == NOTE_NONE) + m->note = NOTE_KEYOFF; + else + m->command = CMD_KEYOFF; + break; + case 0x31: // 31 xxx Song Upcall + default: + m->command = CMD_NONE; + break; + } + } + } + } + + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + file.ReadString<mpt::String::maybeNullTerminated>(m_szNames[smp], sampleNameLength[smp] & 0x3F); + + if(sampleNameLength[smp] & 0x80) + continue; + + ModSample &mptSmp = Samples[smp]; + mptSmp.nSustainStart = file.ReadUint24LE() << 1; + if(const auto loopLen = file.ReadUint24LE() << 1; loopLen > 2) + { + mptSmp.nSustainEnd = mptSmp.nSustainStart + loopLen; + mptSmp.uFlags.set(CHN_SUSTAINLOOP); + } + mptSmp.nVolume = std::min(file.ReadUint8(), uint8(64)) * 4u; + mptSmp.nFineTune = MOD2XMFineTune(file.ReadUint8()); + mptSmp.Set16BitCuePoints(); + + if(!mptSmp.nLength) + continue; + + const uint8 packingType = file.ReadUint8(); + switch(packingType) + { + case 0: // Modified u-Law + if(loadFlags & loadSampleData) + { + std::vector<std::byte> sampleData; + if(!file.CanRead(mptSmp.nLength)) + return false; + file.ReadVector(sampleData, mptSmp.nLength); + for(auto &b : sampleData) + { + uint8 v = mpt::byte_cast<uint8>(b); + v = (v << 7) | (static_cast<uint8>(~v) >> 1); + b = mpt::byte_cast<std::byte>(v); + } + + FileReader sampleDataFile = FileReader(mpt::as_span(sampleData)); + SampleIO( + SampleIO::_16bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::uLaw) + .ReadSample(mptSmp, sampleDataFile); + } else + { + file.Skip(mptSmp.nLength); + } + break; + case 1: // 13-bit LZW applied to linear sample data differences + { + std::vector<std::byte> sampleData; + try + { + sampleData = DecompressDSymLZW(file, mptSmp.nLength); + } catch(const BitReader::eof &) + { + return false; + } + if(!(loadFlags & loadSampleData)) + break; + FileReader sampleDataFile = FileReader(mpt::as_span(sampleData)); + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::deltaPCM) + .ReadSample(mptSmp, sampleDataFile); + } + break; + case 2: // 8-bit signed + case 3: // 16-bit signed + if(loadFlags & loadSampleData) + { + SampleIO( + (packingType == 2) ? SampleIO::_8bit : SampleIO::_16bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(mptSmp, file); + } else + { + file.Skip(mptSmp.nLength * (packingType - 1)); + } + break; + case 4: // Sigma-Delta compression applied to linear sample differences + case 5: // Sigma-Delta compression applied to logarithmic sample differences + { + std::vector<std::byte> sampleData; + try + { + sampleData = DecompressDSymSigmaDelta(file, mptSmp.nLength); + } catch(const BitReader::eof &) + { + return false; + } + if(!(loadFlags & loadSampleData)) + break; + if(packingType == 5) + { + static constexpr uint8 xorMask[] = {0x00, 0x7F}; + for(auto &b : sampleData) + { + uint8 v = mpt::byte_cast<uint8>(b); + v ^= xorMask[v >> 7]; + b = mpt::byte_cast<std::byte>(v); + } + } + + FileReader sampleDataFile = FileReader(mpt::as_span(sampleData)); + SampleIO( + (packingType == 5) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (packingType == 5) ? SampleIO::uLaw : SampleIO::unsignedPCM) + .ReadSample(mptSmp, sampleDataFile); + } + break; + default: + return false; + } + } + + if(const uint32 infoLen = fileHeader.infoLenLo | (fileHeader.infoLenHi << 16); infoLen > 0) + { + std::vector<std::byte> infoData; + if(!ReadDSymChunk(file, infoData, infoLen)) + return false; + FileReader infoChunk = FileReader(mpt::as_span(infoData)); + m_songMessage.Read(infoChunk, infoLen, SongMessage::leLF); + } + + m_modFormat.formatName = MPT_UFORMAT("Digital Symphony v{}")(fileHeader.version); + m_modFormat.type = U_("dsym"); // RISC OS doesn't use file extensions but this is a common abbreviation used for this tracker + m_modFormat.madeWithTracker = U_("Digital Symphony"); + m_modFormat.charset = mpt::Charset::RISC_OS; + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_dtm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dtm.cpp new file mode 100644 index 00000000..6b893140 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_dtm.cpp @@ -0,0 +1,607 @@ +/* + * Load_dtm.cpp + * ------------ + * Purpose: Digital Tracker / Digital Home Studio module Loader (DTM) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +enum PatternFormats : uint32 +{ + DTM_PT_PATTERN_FORMAT = 0, + DTM_204_PATTERN_FORMAT = MagicBE("2.04"), + DTM_206_PATTERN_FORMAT = MagicBE("2.06"), +}; + + +struct DTMFileHeader +{ + char magic[4]; + uint32be headerSize; + uint16be type; // 0 = module + uint8be stereoMode; // FF = panoramic stereo, 00 = old stereo + uint8be bitDepth; // Typically 8, sometimes 16, but is not actually used anywhere? + uint16be reserved; // Usually 0, but not in unknown title 1.dtm and unknown title 2.dtm + uint16be speed; + uint16be tempo; + uint32be forcedSampleRate; // Seems to be ignored in newer files +}; + +MPT_BINARY_STRUCT(DTMFileHeader, 22) + + +// IFF-style Chunk +struct DTMChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idS_Q_ = MagicBE("S.Q."), + idPATT = MagicBE("PATT"), + idINST = MagicBE("INST"), + idIENV = MagicBE("IENV"), + idDAPT = MagicBE("DAPT"), + idDAIT = MagicBE("DAIT"), + idTEXT = MagicBE("TEXT"), + idPATN = MagicBE("PATN"), + idTRKN = MagicBE("TRKN"), + idVERS = MagicBE("VERS"), + idSV19 = MagicBE("SV19"), + }; + + uint32be id; + uint32be length; + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(DTMChunk, 8) + + +struct DTMSample +{ + uint32be reserved; // 0x204 for first sample, 0x208 for second, etc... + uint32be length; // in bytes + uint8be finetune; // -8....7 + uint8be volume; // 0...64 + uint32be loopStart; // in bytes + uint32be loopLength; // ditto + char name[22]; + uint8be stereo; + uint8be bitDepth; + uint16be transpose; + uint16be unknown; + uint32be sampleRate; + + void ConvertToMPT(ModSample &mptSmp, uint32 forcedSampleRate, uint32 formatVersion) const + { + mptSmp.Initialize(MOD_TYPE_IT); + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = mptSmp.nLoopStart + loopLength; + // In revolution to come.dtm, the file header says samples rate is 24512 Hz, but samples say it's 50000 Hz + // Digital Home Studio ignores the header setting in 2.04-/2.06-style modules + mptSmp.nC5Speed = (formatVersion == DTM_PT_PATTERN_FORMAT && forcedSampleRate > 0) ? forcedSampleRate : sampleRate; + int32 transposeAmount = MOD2XMFineTune(finetune); + if(formatVersion == DTM_206_PATTERN_FORMAT && transpose > 0 && transpose != 48) + { + // Digital Home Studio applies this unconditionally, but some old songs sound wrong then (delirium.dtm). + // Digital Tracker 2.03 ignores the setting. + // Maybe this should not be applied for "real" Digital Tracker modules? + transposeAmount += (48 - transpose) * 128; + } + mptSmp.Transpose(transposeAmount * (1.0 / (12.0 * 128.0))); + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u; + if(stereo & 1) + { + mptSmp.uFlags.set(CHN_STEREO); + mptSmp.nLength /= 2u; + mptSmp.nLoopStart /= 2u; + mptSmp.nLoopEnd /= 2u; + } + if(bitDepth > 8) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nLength /= 2u; + mptSmp.nLoopStart /= 2u; + mptSmp.nLoopEnd /= 2u; + } + if(mptSmp.nLoopEnd > mptSmp.nLoopStart + 1) + { + mptSmp.uFlags.set(CHN_LOOP); + } else + { + mptSmp.nLoopStart = mptSmp.nLoopEnd = 0; + } + } +}; + +MPT_BINARY_STRUCT(DTMSample, 50) + + +struct DTMInstrument +{ + uint16be insNum; + uint8be unknown1; + uint8be envelope; // 0xFF = none + uint8be sustain; // 0xFF = no sustain point + uint16be fadeout; + uint8be vibRate; + uint8be vibDepth; + uint8be modulationRate; + uint8be modulationDepth; + uint8be breathRate; + uint8be breathDepth; + uint8be volumeRate; + uint8be volumeDepth; +}; + +MPT_BINARY_STRUCT(DTMInstrument, 15) + + +struct DTMEnvelope +{ + struct DTMEnvPoint + { + uint8be value; + uint8be tick; + }; + uint16be numPoints; + DTMEnvPoint points[16]; +}; + +MPT_BINARY_STRUCT(DTMEnvelope::DTMEnvPoint, 2) +MPT_BINARY_STRUCT(DTMEnvelope, 34) + + +struct DTMText +{ + uint16be textType; // 0 = pattern, 1 = free, 2 = song + uint32be textLength; + uint16be tabWidth; + uint16be reserved; + uint16be oddLength; +}; + +MPT_BINARY_STRUCT(DTMText, 12) + + +static bool ValidateHeader(const DTMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "D.T.", 4) + || fileHeader.headerSize < sizeof(fileHeader) - 8u + || fileHeader.headerSize > 256 // Excessively long song title? + || fileHeader.type != 0) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDTM(MemoryFileReader file, const uint64 *pfilesize) +{ + DTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadDTM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + DTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_DTM); + InitializeChannels(); + m_SongFlags.set(SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS); + m_playBehaviour.reset(kITVibratoTremoloPanbrello); + // Various files have a default speed or tempo of 0 + if(fileHeader.tempo) + m_nDefaultTempo.Set(fileHeader.tempo); + if(fileHeader.speed) + m_nDefaultSpeed = fileHeader.speed; + if(fileHeader.stereoMode == 0) + SetupMODPanning(true); + + file.ReadString<mpt::String::maybeNullTerminated>(m_songName, fileHeader.headerSize - (sizeof(fileHeader) - 8u)); + + auto chunks = ChunkReader(file).ReadChunks<DTMChunk>(1); + + // Read order list + if(FileReader chunk = chunks.GetChunk(DTMChunk::idS_Q_)) + { + uint16 ordLen = chunk.ReadUint16BE(); + uint16 restartPos = chunk.ReadUint16BE(); + chunk.Skip(4); // Reserved + ReadOrderFromFile<uint8>(Order(), chunk, ordLen); + Order().SetRestartPos(restartPos); + } else + { + return false; + } + + // Read pattern properties + uint32 patternFormat; + if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATT)) + { + m_nChannels = chunk.ReadUint16BE(); + if(m_nChannels < 1 || m_nChannels > 32) + { + return false; + } + Patterns.ResizeArray(chunk.ReadUint16BE()); // Number of stored patterns, may be lower than highest pattern number + patternFormat = chunk.ReadUint32BE(); + if(patternFormat != DTM_PT_PATTERN_FORMAT && patternFormat != DTM_204_PATTERN_FORMAT && patternFormat != DTM_206_PATTERN_FORMAT) + { + return false; + } + } else + { + return false; + } + + // Read global info + if(FileReader chunk = chunks.GetChunk(DTMChunk::idSV19)) + { + chunk.Skip(2); // Ticks per quarter note, typically 24 + uint32 fractionalTempo = chunk.ReadUint32BE(); + m_nDefaultTempo = TEMPO(m_nDefaultTempo.GetInt() + fractionalTempo / 4294967296.0); + + uint16be panning[32]; + chunk.ReadArray(panning); + for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++) + { + // Panning is in range 0...180, 90 = center + ChnSettings[chn].nPan = static_cast<uint16>(128 + Util::muldivr(std::min(static_cast<int>(panning[chn]), int(180)) - 90, 128, 90)); + } + + chunk.Skip(16); + // Chunk ends here for old DTM modules + if(chunk.CanRead(2)) + { + m_nDefaultGlobalVolume = std::min(chunk.ReadUint16BE(), static_cast<uint16>(MAX_GLOBAL_VOLUME)); + } + chunk.Skip(128); + uint16be volume[32]; + if(chunk.ReadArray(volume)) + { + for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++) + { + // Volume is in range 0...128, 64 = normal + ChnSettings[chn].nVolume = static_cast<uint8>(std::min(static_cast<int>(volume[chn]), int(128)) / 2); + } + m_nSamplePreAmp *= 2; // Compensate for channel volume range + } + } + + // Read song message + if(FileReader chunk = chunks.GetChunk(DTMChunk::idTEXT)) + { + DTMText text; + chunk.ReadStruct(text); + if(text.oddLength == 0xFFFF) + { + chunk.Skip(1); + } + m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF); + } + + // Read sample headers + if(FileReader chunk = chunks.GetChunk(DTMChunk::idINST)) + { + uint16 numSamples = chunk.ReadUint16BE(); + bool newSamples = (numSamples >= 0x8000); + numSamples &= 0x7FFF; + if(numSamples >= MAX_SAMPLES || !chunk.CanRead(numSamples * (sizeof(DTMSample) + (newSamples ? 2u : 0u)))) + { + return false; + } + + m_nSamples = numSamples; + for(SAMPLEINDEX smp = 1; smp <= numSamples; smp++) + { + SAMPLEINDEX realSample = newSamples ? (chunk.ReadUint16BE() + 1u) : smp; + DTMSample dtmSample; + chunk.ReadStruct(dtmSample); + if(realSample < 1 || realSample >= MAX_SAMPLES) + { + continue; + } + m_nSamples = std::max(m_nSamples, realSample); + ModSample &mptSmp = Samples[realSample]; + dtmSample.ConvertToMPT(mptSmp, fileHeader.forcedSampleRate, patternFormat); + m_szNames[realSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, dtmSample.name); + } + + if(chunk.ReadUint16BE() == 0x0004) + { + // Digital Home Studio instruments + m_nInstruments = std::min(static_cast<INSTRUMENTINDEX>(m_nSamples), static_cast<INSTRUMENTINDEX>(MAX_INSTRUMENTS - 1)); + + FileReader envChunk = chunks.GetChunk(DTMChunk::idIENV); + while(chunk.CanRead(sizeof(DTMInstrument))) + { + DTMInstrument instr; + chunk.ReadStruct(instr); + if(instr.insNum < GetNumInstruments()) + { + ModSample &sample = Samples[instr.insNum + 1]; + sample.nVibDepth = instr.vibDepth; + sample.nVibRate = instr.vibRate; + sample.nVibSweep = 255; + + ModInstrument *mptIns = AllocateInstrument(instr.insNum + 1, instr.insNum + 1); + if(mptIns != nullptr) + { + InstrumentEnvelope &mptEnv = mptIns->VolEnv; + mptIns->nFadeOut = std::min(static_cast<uint16>(instr.fadeout), uint16(0xFFF)); + if(instr.envelope != 0xFF && envChunk.Seek(2 + sizeof(DTMEnvelope) * instr.envelope)) + { + DTMEnvelope env; + envChunk.ReadStruct(env); + mptEnv.dwFlags.set(ENV_ENABLED); + mptEnv.resize(std::min({ static_cast<std::size_t>(env.numPoints), std::size(env.points), static_cast<std::size_t>(MAX_ENVPOINTS) })); + for(size_t i = 0; i < mptEnv.size(); i++) + { + mptEnv[i].value = std::min(uint8(64), static_cast<uint8>(env.points[i].value)); + mptEnv[i].tick = env.points[i].tick; + } + + if(instr.sustain != 0xFF) + { + mptEnv.dwFlags.set(ENV_SUSTAIN); + mptEnv.nSustainStart = mptEnv.nSustainEnd = instr.sustain; + } + if(!mptEnv.empty()) + { + mptEnv.dwFlags.set(ENV_LOOP); + mptEnv.nLoopStart = mptEnv.nLoopEnd = static_cast<uint8>(mptEnv.size() - 1); + } + } + } + } + } + } + } + + // Read pattern data + for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAPT)) + { + chunk.Skip(4); // FF FF FF FF + PATTERNINDEX patNum = chunk.ReadUint16BE(); + ROWINDEX numRows = chunk.ReadUint16BE(); + if(patternFormat == DTM_206_PATTERN_FORMAT) + { + // The stored data is actually not row-based, but tick-based. + numRows /= m_nDefaultSpeed; + } + if(!(loadFlags & loadPatternData) || patNum > 255 || !Patterns.Insert(patNum, numRows)) + { + continue; + } + + if(patternFormat == DTM_206_PATTERN_FORMAT) + { + chunk.Skip(4); + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + uint16 length = chunk.ReadUint16BE(); + if(length % 2u) length++; + FileReader rowChunk = chunk.ReadChunk(length); + int tick = 0; + std::div_t position = { 0, 0 }; + while(rowChunk.CanRead(6) && static_cast<ROWINDEX>(position.quot) < numRows) + { + ModCommand *m = Patterns[patNum].GetpModCommand(position.quot, chn); + + const auto [note, volume, instr, command, param, delay] = rowChunk.ReadArray<uint8, 6>(); + if(note > 0 && note <= 96) + { + m->note = note + NOTE_MIN + 12; + if(position.rem) + { + m->command = CMD_MODCMDEX; + m->param = 0xD0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15)); + } + } else if(note & 0x80) + { + // Lower 7 bits contain note, probably intended for MIDI-like note-on/note-off events + if(position.rem) + { + m->command = CMD_MODCMDEX; + m->param = 0xC0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15)); + } else + { + m->note = NOTE_NOTECUT; + } + } + if(volume) + { + m->volcmd = VOLCMD_VOLUME; + m->vol = std::min(volume, uint8(64)); // Volume can go up to 255, but we do not support over-amplification at the moment. + } + if(instr) + { + m->instr = instr; + } + if(command || param) + { + m->command = command; + m->param = param; + ConvertModCommand(*m); +#ifdef MODPLUG_TRACKER + m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this); +#endif + // G is 8-bit volume + // P is tremor (need to disable oldfx) + } + if(delay & 0x80) + tick += (delay & 0x7F) * 0x100 + rowChunk.ReadUint8(); + else + tick += delay; + position = std::div(tick, m_nDefaultSpeed); + } + } + } else + { + ModCommand *m = Patterns[patNum].GetpModCommand(0, 0); + for(ROWINDEX row = 0; row < numRows; row++) + { + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, m++) + { + const auto data = chunk.ReadArray<uint8, 4>(); + if(patternFormat == DTM_204_PATTERN_FORMAT) + { + const auto [note, instrVol, instrCmd, param] = data; + if(note > 0 && note < 0x80) + { + m->note = (note >> 4) * 12 + (note & 0x0F) + NOTE_MIN + 11; + } + uint8 vol = instrVol >> 2; + if(vol) + { + m->volcmd = VOLCMD_VOLUME; + m->vol = vol - 1u; + } + m->instr = ((instrVol & 0x03) << 4) | (instrCmd >> 4); + m->command = instrCmd & 0x0F; + m->param = param; + } else + { + ReadMODPatternEntry(data, *m); + m->instr |= data[0] & 0x30; // Allow more than 31 instruments + } + ConvertModCommand(*m); + // Fix commands without memory and slide nibble precedence + switch(m->command) + { + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + if(!m->param) + { + m->command = CMD_NONE; + } + break; + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + if(m->param & 0xF0) + { + m->param &= 0xF0; + } else if(!m->param) + { + m->command = CMD_NONE; + } + break; + default: + break; + } +#ifdef MODPLUG_TRACKER + m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this); +#endif + } + } + } + } + + // Read pattern names + if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATN)) + { + PATTERNINDEX pat = 0; + std::string name; + while(chunk.CanRead(1) && pat < Patterns.Size()) + { + chunk.ReadNullString(name, 32); + Patterns[pat].SetName(name); + pat++; + } + } + + // Read channel names + if(FileReader chunk = chunks.GetChunk(DTMChunk::idTRKN)) + { + CHANNELINDEX chn = 0; + std::string name; + while(chunk.CanRead(1) && chn < GetNumChannels()) + { + chunk.ReadNullString(name, 32); + ChnSettings[chn].szName = name; + chn++; + } + } + + // Read sample data + for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAIT)) + { + SAMPLEINDEX smp = chunk.ReadUint16BE(); + if(smp >= GetNumSamples() || !(loadFlags & loadSampleData)) + { + continue; + } + ModSample &mptSmp = Samples[smp + 1]; + SampleIO( + mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved: SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM).ReadSample(mptSmp, chunk); + } + + // Is this accurate? + mpt::ustring tracker; + if(patternFormat == DTM_206_PATTERN_FORMAT) + { + tracker = U_("Digital Home Studio"); + } else if(FileReader chunk = chunks.GetChunk(DTMChunk::idVERS)) + { + uint32 version = chunk.ReadUint32BE(); + tracker = MPT_UFORMAT("Digital Tracker {}.{}")(version >> 4, version & 0x0F); + } else + { + tracker = U_("Digital Tracker"); + } + m_modFormat.formatName = U_("Digital Tracker"); + m_modFormat.type = U_("dtm"); + m_modFormat.madeWithTracker = std::move(tracker); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_far.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_far.cpp new file mode 100644 index 00000000..57230d7c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_far.cpp @@ -0,0 +1,344 @@ +/* + * Load_far.cpp + * ------------ + * Purpose: Farandole (FAR) module loader + * Notes : (currently none) + * Authors: OpenMPT Devs (partly inspired by Storlek's FAR loader from Schism Tracker) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + + +OPENMPT_NAMESPACE_BEGIN + +// FAR File Header +struct FARFileHeader +{ + uint8le magic[4]; + char songName[40]; + uint8le eof[3]; + uint16le headerLength; + uint8le version; + uint8le onOff[16]; + uint8le editingState[9]; // Stuff we don't care about + uint8le defaultSpeed; + uint8le chnPanning[16]; + uint8le patternState[4]; // More stuff we don't care about + uint16le messageLength; +}; + +MPT_BINARY_STRUCT(FARFileHeader, 98) + + +struct FAROrderHeader +{ + uint8le orders[256]; + uint8le numPatterns; // supposed to be "number of patterns stored in the file"; apparently that's wrong + uint8le numOrders; + uint8le restartPos; + uint16le patternSize[256]; +}; + +MPT_BINARY_STRUCT(FAROrderHeader, 771) + + +// FAR Sample header +struct FARSampleHeader +{ + // Sample flags + enum SampleFlags + { + smp16Bit = 0x01, + smpLoop = 0x08, + }; + + char name[32]; + uint32le length; + uint8le finetune; + uint8le volume; + uint32le loopStart; + uint32le loopEnd; + uint8le type; + uint8le loop; + + // Convert sample header to OpenMPT's internal format. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nC5Speed = 8363 * 2; + mptSmp.nVolume = volume * 16; + + if(type & smp16Bit) + { + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + + if((loop & 8) && mptSmp.nLoopEnd > mptSmp.nLoopStart) + { + mptSmp.uFlags.set(CHN_LOOP); + } + } + + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat() const + { + return SampleIO( + (type & smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + } +}; + +MPT_BINARY_STRUCT(FARSampleHeader, 48) + + +static bool ValidateHeader(const FARFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "FAR\xFE", 4) != 0 + || std::memcmp(fileHeader.eof, "\x0D\x0A\x1A", 3) + ) + { + return false; + } + if(fileHeader.headerLength < sizeof(FARFileHeader)) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const FARFileHeader &fileHeader) +{ + return fileHeader.headerLength - sizeof(FARFileHeader); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderFAR(MemoryFileReader file, const uint64 *pfilesize) +{ + FARFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadFAR(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + FARFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Globals + InitializeGlobals(MOD_TYPE_FAR); + m_nChannels = 16; + m_nSamplePreAmp = 32; + m_nDefaultSpeed = fileHeader.defaultSpeed; + m_nDefaultTempo.Set(80); + m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + m_SongFlags = SONG_LINEARSLIDES; + m_playBehaviour.set(kPeriodsAreHertz); + + m_modFormat.formatName = U_("Farandole Composer"); + m_modFormat.type = U_("far"); + m_modFormat.charset = mpt::Charset::CP437; + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + + // Read channel settings + for(CHANNELINDEX chn = 0; chn < 16; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].dwFlags = fileHeader.onOff[chn] ? ChannelFlags(0) : CHN_MUTE; + ChnSettings[chn].nPan = ((fileHeader.chnPanning[chn] & 0x0F) << 4) + 8; + } + + // Read song message + if(fileHeader.messageLength != 0) + { + m_songMessage.ReadFixedLineLength(file, fileHeader.messageLength, 132, 0); // 132 characters per line... wow. :) + } + + // Read orders + FAROrderHeader orderHeader; + if(!file.ReadStruct(orderHeader)) + { + return false; + } + ReadOrderFromArray(Order(), orderHeader.orders, orderHeader.numOrders, 0xFF, 0xFE); + Order().SetRestartPos(orderHeader.restartPos); + + file.Seek(fileHeader.headerLength); + + // Pattern effect LUT + static constexpr EffectCommand farEffects[] = + { + CMD_NONE, + CMD_PORTAMENTOUP, + CMD_PORTAMENTODOWN, + CMD_TONEPORTAMENTO, + CMD_RETRIG, + CMD_VIBRATO, // depth + CMD_VIBRATO, // speed + CMD_VOLUMESLIDE, // up + CMD_VOLUMESLIDE, // down + CMD_VIBRATO, // sustained (?) + CMD_NONE, // actually slide-to-volume + CMD_S3MCMDEX, // panning + CMD_S3MCMDEX, // note offset => note delay? + CMD_NONE, // fine tempo down + CMD_NONE, // fine tempo up + CMD_SPEED, + }; + + // Read patterns + for(PATTERNINDEX pat = 0; pat < 256; pat++) + { + if(!orderHeader.patternSize[pat]) + { + continue; + } + + FileReader patternChunk = file.ReadChunk(orderHeader.patternSize[pat]); + + // Calculate pattern length in rows (every event is 4 bytes, and we have 16 channels) + ROWINDEX numRows = (orderHeader.patternSize[pat] - 2) / (16 * 4); + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows)) + { + continue; + } + + // Read break row and unused value (used to be pattern tempo) + ROWINDEX breakRow = patternChunk.ReadUint8(); + patternChunk.Skip(1); + if(breakRow > 0 && breakRow < numRows - 2) + { + breakRow++; + } else + { + breakRow = ROWINDEX_INVALID; + } + + // Read pattern data + for(ROWINDEX row = 0; row < numRows; row++) + { + PatternRow rowBase = Patterns[pat].GetRow(row); + for(CHANNELINDEX chn = 0; chn < 16; chn++) + { + ModCommand &m = rowBase[chn]; + + const auto [note, instr, volume, effect] = patternChunk.ReadArray<uint8, 4>(); + + if(note > 0 && note <= 72) + { + m.note = note + 35 + NOTE_MIN; + m.instr = instr + 1; + } + + if(volume > 0 && volume <= 16) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = (volume - 1u) * 64u / 15u; + } + + m.param = effect & 0x0F; + + switch(effect >> 4) + { + case 0x01: + case 0x02: + m.param |= 0xF0; + break; + case 0x03: // Porta to note (TODO: Parameter is number of rows the portamento should take) + m.param <<= 2; + break; + case 0x04: // Retrig + m.param = 6 / (1 + (m.param & 0xf)) + 1; // ugh? + break; + case 0x06: // Vibrato speed + case 0x07: // Volume slide up + m.param *= 8; + break; + case 0x0A: // Volume-portamento (what!) + m.volcmd = VOLCMD_VOLUME; + m.vol = (m.param << 2) + 4; + break; + case 0x0B: // Panning + m.param |= 0x80; + break; + case 0x0C: // Note offset + m.param = 6 / (1 + m.param) + 1; + m.param |= 0x0D; + } + m.command = farEffects[effect >> 4]; + } + } + + Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(breakRow).RetryNextRow()); + } + + if(!(loadFlags & loadSampleData)) + { + return true; + } + + // Read samples + uint8 sampleMap[8]; // Sample usage bitset + file.ReadArray(sampleMap); + + for(SAMPLEINDEX smp = 0; smp < 64; smp++) + { + if(!(sampleMap[smp >> 3] & (1 << (smp & 7)))) + { + continue; + } + + FARSampleHeader sampleHeader; + if(!file.ReadStruct(sampleHeader)) + { + return true; + } + + m_nSamples = smp + 1; + ModSample &sample = Samples[m_nSamples]; + m_szNames[m_nSamples] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); + sampleHeader.ConvertToMPT(sample); + sampleHeader.GetSampleFormat().ReadSample(sample, file); + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_fmt.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_fmt.cpp new file mode 100644 index 00000000..2b8a83a1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_fmt.cpp @@ -0,0 +1,195 @@ +/* + * Load_fmt.cpp + * ------------ + * Purpose: Davey W Taylor's FM Tracker module loader + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct FMTChannelSetting +{ + char name[8]; + char settings[11]; +}; + +MPT_BINARY_STRUCT(FMTChannelSetting, 19) + + +struct FMTFileHeader +{ + char magic[11]; // Includes format version number for simplicity + char trackerName[20]; + char songName[32]; + + FMTChannelSetting channels[8]; + uint8 lastRow; + uint8 lastOrder; + uint8 lastPattern; +}; + +MPT_BINARY_STRUCT(FMTFileHeader, 218) + + +static uint64 GetHeaderMinimumAdditionalSize(const FMTFileHeader &fileHeader) +{ + // Order list + pattern delays, pattern mapping + at least one byte per channel + return (fileHeader.lastOrder + 1u) * 2u + (fileHeader.lastPattern + 1u) * 9u; +} + + +static bool ValidateHeader(const FMTFileHeader &fileHeader) +{ + if(memcmp(fileHeader.magic, "FMTracker\x01\x01", 11)) + return false; + + for(const auto &channel : fileHeader.channels) + { + // Reject anything that resembles OPL3 + if((channel.settings[8] & 0xFC) || (channel.settings[9] & 0xFC) || (channel.settings[10] & 0xF0)) + return false; + } + + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderFMT(MemoryFileReader file, const uint64 *pfilesize) +{ + FMTFileHeader fileHeader; + if(!file.Read(fileHeader)) + return ProbeWantMoreData; + if(!ValidateHeader(fileHeader)) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadFMT(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + FMTFileHeader fileHeader; + if(!file.Read(fileHeader)) + return false; + if(!ValidateHeader(fileHeader)) + return false; + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_S3M); + InitializeChannels(); + m_nChannels = 8; + m_nSamples = 8; + m_nDefaultTempo = TEMPO(45.5); // 18.2 Hz timer + m_playBehaviour.set(kOPLNoteStopWith0Hz); + m_SongFlags.set(SONG_IMPORTED); + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + + for(CHANNELINDEX chn = 0; chn < 8; chn++) + { + const auto name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.channels[chn].name); + ChnSettings[chn].szName = name; + + ModSample &mptSmp = Samples[chn + 1]; + mptSmp.Initialize(MOD_TYPE_S3M); + OPLPatch patch{{}}; + memcpy(patch.data(), fileHeader.channels[chn].settings, 11); + mptSmp.SetAdlib(true, patch); + mptSmp.nC5Speed = 8215; + m_szNames[chn + 1] = name; + } + + const ORDERINDEX numOrders = fileHeader.lastOrder + 1u; + ReadOrderFromFile<uint8>(Order(), file, numOrders); + + std::vector<uint8> delays; + file.ReadVector(delays, numOrders); + for(uint8 delay : delays) + { + if(delay < 1 || delay > 8) + return false; + } + m_nDefaultSpeed = delays[0]; + + const PATTERNINDEX numPatterns = fileHeader.lastPattern + 1u; + const ROWINDEX numRows = fileHeader.lastRow + 1u; + std::vector<uint8> patternMap; + file.ReadVector(patternMap, numPatterns); + + Patterns.ResizeArray(numPatterns); + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(patternMap[pat], numRows)) + break; + auto &pattern = Patterns[patternMap[pat]]; + for(CHANNELINDEX chn = 0; chn < 8; chn++) + { + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + uint8 data = file.ReadUint8(); + if(data & 0x80) + { + row += data & 0x7F; + } else + { + ModCommand &m = *pattern.GetpModCommand(row, chn); + if(data == 1) + { + m.note = NOTE_NOTECUT; + } else if(data >= 2 && data <= 97) + { + m.note = data + NOTE_MIN + 11u; + m.instr = static_cast<ModCommand::INSTR>(chn + 1u); + } + } + } + } + } + + // Write song speed to patterns... due to a quirk in the original playback routine + // (delays is applied before notes are triggered, not afterwards), a pattern's delay + // already applies to the last row of the previous pattern. + // In case you wonder if anyone would ever notice: My own songs written with this tracker + // actively work around this issue and will sound wrong if tempo is changed on the first row. + for(ORDERINDEX ord = 0; ord < numOrders; ord++) + { + if(!Order().IsValidPat(ord)) + { + if(PATTERNINDEX pat = Patterns.InsertAny(numRows); pat != PATTERNINDEX_INVALID) + Order()[ord] = pat; + else + continue; + } + auto m = Patterns[Order()[ord]].end() - 1; + auto delay = delays[(ord + 1u) % numOrders]; + if(m->param == delay) + continue; + + if(m->command == CMD_SPEED) + { + PATTERNINDEX newPat = Order().EnsureUnique(ord); + if(newPat != PATTERNINDEX_INVALID) + m = Patterns[newPat].end() - 1; + } + m->command = CMD_SPEED; + m->param = delay; + } + + m_modFormat.formatName = U_("FM Tracker"); + m_modFormat.type = U_("fmt"); + m_modFormat.madeWithTracker = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.trackerName)); + m_modFormat.charset = mpt::Charset::CP437; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_gdm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_gdm.cpp new file mode 100644 index 00000000..789727af --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_gdm.cpp @@ -0,0 +1,509 @@ +/* + * Load_gdm.cpp + * ------------ + * Purpose: GDM (BWSB Soundsystem) module loader + * Notes : This code is partly based on zilym's original code / specs (which are utterly wrong :P). + * Thanks to the MenTaLguY for gdm.txt and ajs for gdm2s3m and some hints. + * + * Hint 1: Most (all?) of the unsupported features were not supported in 2GDM / BWSB either. + * Hint 2: Files will be played like their original formats would be played in MPT, so no + * BWSB quirks including crashes and freezes are supported. :-P + * Authors: Johannes Schultz + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// GDM File Header +struct GDMFileHeader +{ + char magic[4]; // ID: 'GDM\xFE' + char songTitle[32]; // Music's title + char songMusician[32]; // Name of music's composer + char dosEOF[3]; // 13, 10, 26 + char magic2[4]; // ID: 'GMFS' + uint8le formatMajorVer; // Format major version + uint8le formatMinorVer; // Format minor version + uint16le trackerID; // Composing Tracker ID code (00 = 2GDM) + uint8le trackerMajorVer; // Tracker's major version + uint8le trackerMinorVer; // Tracker's minor version + uint8le panMap[32]; // 0-Left to 15-Right, 255-N/U + uint8le masterVol; // Range: 0...64 + uint8le tempo; // Initial music tempo (6) + uint8le bpm; // Initial music BPM (125) + uint16le originalFormat; // Original format ID: + // 1-MOD, 2-MTM, 3-S3M, 4-669, 5-FAR, 6-ULT, 7-STM, 8-MED, 9-PSM + // (versions of 2GDM prior to v1.15 won't set this correctly) + // 2GDM v1.17 will only spit out 0-byte files when trying to convert a PSM16 file, + // and fail outright when trying to convert a new PSM file. + + uint32le orderOffset; + uint8le lastOrder; // Number of orders in module - 1 + uint32le patternOffset; + uint8le lastPattern; // Number of patterns in module - 1 + uint32le sampleHeaderOffset; + uint32le sampleDataOffset; + uint8le lastSample; // Number of samples in module - 1 + uint32le messageTextOffset; // Offset of song message + uint32le messageTextLength; + uint32le scrollyScriptOffset; // Offset of scrolly script (huh?) + uint16le scrollyScriptLength; + uint32le textGraphicOffset; // Offset of text graphic (huh?) + uint16le textGraphicLength; +}; + +MPT_BINARY_STRUCT(GDMFileHeader, 157) + + +// GDM Sample Header +struct GDMSampleHeader +{ + enum SampleFlags + { + smpLoop = 0x01, + smp16Bit = 0x02, // 16-Bit samples are not handled correctly by 2GDM (not implemented) + smpVolume = 0x04, // Use default volume + smpPanning = 0x08, + smpLZW = 0x10, // LZW-compressed samples are not implemented in 2GDM + smpStereo = 0x20, // Stereo samples are not handled correctly by 2GDM (not implemented) + }; + + char name[32]; // sample's name + char fileName[12]; // sample's filename + uint8le emsHandle; // useless + uint32le length; // length in bytes + uint32le loopBegin; // loop start in samples + uint32le loopEnd; // loop end in samples + uint8le flags; // see SampleFlags + uint16le c4Hertz; // frequency + uint8le volume; // default volume + uint8le panning; // default pan +}; + +MPT_BINARY_STRUCT(GDMSampleHeader, 62) + + +static constexpr MODTYPE gdmFormatOrigin[] = +{ + MOD_TYPE_NONE, MOD_TYPE_MOD, MOD_TYPE_MTM, MOD_TYPE_S3M, MOD_TYPE_669, MOD_TYPE_FAR, MOD_TYPE_ULT, MOD_TYPE_STM, MOD_TYPE_MED, MOD_TYPE_PSM +}; +static constexpr mpt::uchar gdmFormatOriginType[][4] = +{ + UL_(""), UL_("mod"), UL_("mtm"), UL_("s3m"), UL_("669"), UL_("far"), UL_("ult"), UL_("stm"), UL_("med"), UL_("psm") +}; +static constexpr const mpt::uchar * gdmFormatOriginFormat[] = +{ + UL_(""), + UL_("Generic MOD"), + UL_("MultiTracker"), + UL_("Scream Tracker 3"), + UL_("Composer 669 / UNIS 669"), + UL_("Farandole Composer"), + UL_("UltraTracker"), + UL_("Scream Tracker 2"), + UL_("OctaMED"), + UL_("Epic Megagames MASI") +}; + + +static bool ValidateHeader(const GDMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "GDM\xFE", 4) + || fileHeader.dosEOF[0] != 13 || fileHeader.dosEOF[1] != 10 || fileHeader.dosEOF[2] != 26 + || std::memcmp(fileHeader.magic2, "GMFS", 4) + || fileHeader.formatMajorVer != 1 || fileHeader.formatMinorVer != 0 + || fileHeader.originalFormat >= std::size(gdmFormatOrigin) + || fileHeader.originalFormat == 0) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderGDM(MemoryFileReader file, const uint64 *pfilesize) +{ + GDMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadGDM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + GDMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(gdmFormatOrigin[fileHeader.originalFormat]); + m_SongFlags.set(SONG_IMPORTED); + + m_modFormat.formatName = U_("General Digital Music"); + m_modFormat.type = U_("gdm"); + m_modFormat.madeWithTracker = MPT_UFORMAT("BWSB 2GDM {}.{}")(fileHeader.trackerMajorVer, fileHeader.formatMinorVer); + m_modFormat.originalType = gdmFormatOriginType[fileHeader.originalFormat]; + m_modFormat.originalFormatName = gdmFormatOriginFormat[fileHeader.originalFormat]; + m_modFormat.charset = mpt::Charset::CP437; + + // Song name + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songTitle); + + // Artist name + { + std::string artist = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songMusician); + if(artist != "Unknown") + { + m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, artist); + } + } + + // Read channel pan map... 0...15 = channel panning, 16 = surround channel, 255 = channel does not exist + m_nChannels = 32; + for(CHANNELINDEX i = 0; i < 32; i++) + { + ChnSettings[i].Reset(); + if(fileHeader.panMap[i] < 16) + { + ChnSettings[i].nPan = static_cast<uint16>(std::min((fileHeader.panMap[i] * 16) + 8, 256)); + } else if(fileHeader.panMap[i] == 16) + { + ChnSettings[i].nPan = 128; + ChnSettings[i].dwFlags = CHN_SURROUND; + } else if(fileHeader.panMap[i] == 0xFF) + { + m_nChannels = i; + break; + } + } + if(m_nChannels < 1) + { + return false; + } + + m_nDefaultGlobalVolume = std::min(fileHeader.masterVol * 4u, 256u); + m_nDefaultSpeed = fileHeader.tempo; + m_nDefaultTempo.Set(fileHeader.bpm); + + // Read orders + if(file.Seek(fileHeader.orderOffset)) + { + ReadOrderFromFile<uint8>(Order(), file, fileHeader.lastOrder + 1, 0xFF, 0xFE); + } + + // Read samples + if(!file.Seek(fileHeader.sampleHeaderOffset)) + { + return false; + } + + m_nSamples = fileHeader.lastSample + 1; + + // Sample headers + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + GDMSampleHeader gdmSample; + if(!file.ReadStruct(gdmSample)) + { + break; + } + + ModSample &sample = Samples[smp]; + sample.Initialize(); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, gdmSample.name); + sample.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, gdmSample.fileName); + + sample.nC5Speed = gdmSample.c4Hertz; + if(UseFinetuneAndTranspose()) + { + // Use the same inaccurate table as 2GDM for translating back to finetune, as our own routines + // give slightly different results for the provided sample rates that may result in transpose != 0. + static constexpr uint16 rate2finetune[] = { 8363, 8424, 8485, 8547, 8608, 8671, 8734, 8797, 7894, 7951, 8009, 8067, 8125, 8184, 8244, 8303 }; + for(uint8 i = 0; i < 16; i++) + { + if(sample.nC5Speed == rate2finetune[i]) + { + sample.nFineTune = MOD2XMFineTune(i); + break; + } + } + } + + sample.nGlobalVol = 64; // Not supported in this format + + sample.nLength = gdmSample.length; // in bytes + + // Sample format + if(gdmSample.flags & GDMSampleHeader::smp16Bit) + { + sample.uFlags.set(CHN_16BIT); + sample.nLength /= 2; + } + + sample.nLoopStart = gdmSample.loopBegin; + sample.nLoopEnd = gdmSample.loopEnd - 1; + + if(gdmSample.flags & GDMSampleHeader::smpLoop) + sample.uFlags.set(CHN_LOOP); + + if((gdmSample.flags & GDMSampleHeader::smpVolume) && gdmSample.volume != 0xFF) + sample.nVolume = std::min(static_cast<uint8>(gdmSample.volume), uint8(64)) * 4; + else + sample.uFlags.set(SMP_NODEFAULTVOLUME); + + if(gdmSample.flags & GDMSampleHeader::smpPanning) + { + // Default panning is used + sample.uFlags.set(CHN_PANNING); + // 0...15, 16 = surround (not supported), 255 = no default panning + sample.nPan = static_cast<uint16>((gdmSample.panning > 15) ? 128 : std::min((gdmSample.panning * 16) + 8, 256)); + sample.uFlags.set(CHN_SURROUND, gdmSample.panning == 16); + } else + { + sample.nPan = 128; + } + } + + // Read sample data + if((loadFlags & loadSampleData) && file.Seek(fileHeader.sampleDataOffset)) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + SampleIO( + Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM) + .ReadSample(Samples[smp], file); + } + } + + // Read patterns + Patterns.ResizeArray(fileHeader.lastPattern + 1); + + const CModSpecifications &modSpecs = GetModSpecifications(GetBestSaveFormat()); + bool onlyAmigaNotes = true; + + // We'll start at position patternsOffset and decode all patterns + file.Seek(fileHeader.patternOffset); + for(PATTERNINDEX pat = 0; pat <= fileHeader.lastPattern; pat++) + { + // Read pattern length *including* the two "length" bytes + uint16 patternLength = file.ReadUint16LE(); + + if(patternLength <= 2) + { + // Huh, no pattern data present? + continue; + } + FileReader chunk = file.ReadChunk(patternLength - 2); + + if(!(loadFlags & loadPatternData) || !chunk.IsValid() || !Patterns.Insert(pat, 64)) + { + continue; + } + + enum + { + rowDone = 0x00, // Advance to next row + channelMask = 0x1F, // Mask for retrieving channel information + noteFlag = 0x20, // Note / instrument information present + effectFlag = 0x40, // Effect information present + effectMask = 0x1F, // Mask for retrieving effect command + effectMore = 0x20, // Another effect follows + }; + + for(ROWINDEX row = 0; row < 64; row++) + { + PatternRow rowBase = Patterns[pat].GetRow(row); + + uint8 channelByte; + // If channel byte is zero, advance to next row. + while((channelByte = chunk.ReadUint8()) != rowDone) + { + CHANNELINDEX channel = channelByte & channelMask; + if(channel >= m_nChannels) break; // Better safe than sorry! + + ModCommand &m = rowBase[channel]; + + if(channelByte & noteFlag) + { + // Note and sample follows + auto [note, instr] = chunk.ReadArray<uint8, 2>(); + + if(note) + { + note = (note & 0x7F) - 1; // High bit = no-retrig flag (notes with portamento have this set) + m.note = (note & 0x0F) + 12 * (note >> 4) + 12 + NOTE_MIN; + if(!m.IsAmigaNote()) + { + onlyAmigaNotes = false; + } + } + m.instr = instr; + } + + if(channelByte & effectFlag) + { + // Effect(s) follow(s) + m.command = CMD_NONE; + m.volcmd = VOLCMD_NONE; + + while(chunk.CanRead(2)) + { + // We may want to restore the old command in some cases. + const ModCommand oldCmd = m; + + const auto [effByte, param] = chunk.ReadArray<uint8, 2>(); + m.param = param; + + // Effect translation LUT + static constexpr EffectCommand gdmEffTrans[] = + { + CMD_NONE, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO, + CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO, + CMD_TREMOR, CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP, + CMD_VOLUME, CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_SPEED, + CMD_ARPEGGIO, CMD_NONE /* set internal flag */, CMD_RETRIG, CMD_GLOBALVOLUME, + CMD_FINEVIBRATO, CMD_NONE, CMD_NONE, CMD_NONE, + CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, + CMD_NONE, CMD_NONE, CMD_S3MCMDEX, CMD_TEMPO, + }; + + // Translate effect + uint8 command = effByte & effectMask; + if(command < std::size(gdmEffTrans)) + m.command = gdmEffTrans[command]; + else + m.command = CMD_NONE; + + // Fix some effects + switch(m.command) + { + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + if(m.param >= 0xE0 && m_nType != MOD_TYPE_MOD) + m.param = 0xDF; // Don't spill into fine slide territory + break; + + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + if(m.param & 0xF0) + m.param &= 0xF0; + break; + + case CMD_VOLUME: + m.param = std::min(m.param, uint8(64)); + if(modSpecs.HasVolCommand(VOLCMD_VOLUME)) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + // Don't destroy old command, if there was one. + m.command = oldCmd.command; + m.param = oldCmd.param; + } + break; + + case CMD_MODCMDEX: + switch(m.param >> 4) + { + case 0x8: + m.command = CMD_PORTAMENTOUP; + m.param = 0xE0 | (m.param & 0x0F); + break; + case 0x9: + m.command = CMD_PORTAMENTODOWN; + m.param = 0xE0 | (m.param & 0x0F); + break; + default: + if(!modSpecs.HasCommand(CMD_MODCMDEX)) + m.ExtendedMODtoS3MEffect(); + break; + } + break; + + case CMD_RETRIG: + if(!modSpecs.HasCommand(CMD_RETRIG) && modSpecs.HasCommand(CMD_MODCMDEX)) + { + // Retrig in "MOD style" + m.command = CMD_MODCMDEX; + m.param = 0x90 | (m.param & 0x0F); + } + break; + + case CMD_S3MCMDEX: + // Some really special commands + if(m.param == 0x01) + { + // Surround (implemented in 2GDM but not in BWSB itself) + m.param = 0x91; + } else if((m.param & 0xF0) == 0x80) + { + // 4-Bit Panning + if (!modSpecs.HasCommand(CMD_S3MCMDEX)) + m.command = CMD_MODCMDEX; + } else + { + // All other effects are implemented neither in 2GDM nor in BWSB. + m.command = CMD_NONE; + } + break; + } + + // Move pannings to volume column - should never happen + if(m.command == CMD_S3MCMDEX && ((m.param >> 4) == 0x8) && m.volcmd == VOLCMD_NONE) + { + m.volcmd = VOLCMD_PANNING; + m.vol = ((m.param & 0x0F) * 64 + 8) / 15; + m.command = oldCmd.command; + m.param = oldCmd.param; + } + + if(!(effByte & effectMore)) + break; + } + } + } + } + } + + m_SongFlags.set(SONG_AMIGALIMITS | SONG_ISAMIGA, GetType() == MOD_TYPE_MOD && GetNumChannels() == 4 && onlyAmigaNotes); + + // Read song comments + if(fileHeader.messageTextLength > 0 && file.Seek(fileHeader.messageTextOffset)) + { + m_songMessage.Read(file, fileHeader.messageTextLength, SongMessage::leAutodetect); + } + + return true; + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_imf.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_imf.cpp new file mode 100644 index 00000000..aae97243 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_imf.cpp @@ -0,0 +1,667 @@ +/* + * Load_imf.cpp + * ------------ + * Purpose: IMF (Imago Orpheus) module loader + * Notes : Reverb and Chorus are not supported. + * Authors: Storlek (Original author - http://schismtracker.org/ - code ported with permission) + * Johannes Schultz (OpenMPT Port, tweaks) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct IMFChannel +{ + char name[12]; // Channel name (ASCIIZ-String, max 11 chars) + uint8 chorus; // Default chorus + uint8 reverb; // Default reverb + uint8 panning; // Pan positions 00-FF + uint8 status; // Channel status: 0 = enabled, 1 = mute, 2 = disabled (ignore effects!) +}; + +MPT_BINARY_STRUCT(IMFChannel, 16) + +struct IMFFileHeader +{ + enum SongFlags + { + linearSlides = 0x01, + }; + + char title[32]; // Songname (ASCIIZ-String, max. 31 chars) + uint16le ordNum; // Number of orders saved + uint16le patNum; // Number of patterns saved + uint16le insNum; // Number of instruments saved + uint16le flags; // See SongFlags + uint8le unused1[8]; + uint8le tempo; // Default tempo (Axx, 1...255) + uint8le bpm; // Default beats per minute (BPM) (Txx, 32...255) + uint8le master; // Default master volume (Vxx, 0...64) + uint8le amp; // Amplification factor (mixing volume, 4...127) + uint8le unused2[8]; + char im10[4]; // 'IM10' + IMFChannel channels[32]; // Channel settings +}; + +MPT_BINARY_STRUCT(IMFFileHeader, 576) + +struct IMFEnvelope +{ + enum EnvFlags + { + envEnabled = 0x01, + envSustain = 0x02, + envLoop = 0x04, + }; + + uint8 points; // Number of envelope points + uint8 sustain; // Envelope sustain point + uint8 loopStart; // Envelope loop start point + uint8 loopEnd; // Envelope loop end point + uint8 flags; // See EnvFlags + uint8 unused[3]; +}; + +MPT_BINARY_STRUCT(IMFEnvelope, 8) + +struct IMFEnvNode +{ + uint16le tick; + uint16le value; +}; + +MPT_BINARY_STRUCT(IMFEnvNode, 4) + +struct IMFInstrument +{ + enum EnvTypes + { + volEnv = 0, + panEnv = 1, + filterEnv = 2, + }; + + char name[32]; // Inst. name (ASCIIZ-String, max. 31 chars) + uint8le map[120]; // Multisample settings + uint8le unused[8]; + IMFEnvNode nodes[3][16]; + IMFEnvelope env[3]; + uint16le fadeout; // Fadeout rate (0...0FFFH) + uint16le smpNum; // Number of samples in instrument + char ii10[4]; // 'II10' (not verified by Orpheus) + + void ConvertEnvelope(InstrumentEnvelope &mptEnv, EnvTypes e) const + { + const uint8 shift = (e == volEnv) ? 0 : 2; + const uint8 mirror = (e == filterEnv) ? 0xFF : 0x00; + + mptEnv.dwFlags.set(ENV_ENABLED, (env[e].flags & 1) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (env[e].flags & 2) != 0); + mptEnv.dwFlags.set(ENV_LOOP, (env[e].flags & 4) != 0); + + mptEnv.resize(Clamp(env[e].points, uint8(2), uint8(16))); + mptEnv.nLoopStart = env[e].loopStart; + mptEnv.nLoopEnd = env[e].loopEnd; + mptEnv.nSustainStart = mptEnv.nSustainEnd = env[e].sustain; + + uint16 minTick = 0; // minimum tick value for next node + for(uint32 n = 0; n < mptEnv.size(); n++) + { + mptEnv[n].tick = minTick = std::max(minTick, nodes[e][n].tick.get()); + minTick++; + uint8 value = static_cast<uint8>(nodes[e][n].value ^ mirror) >> shift; + mptEnv[n].value = std::min(value, uint8(ENVELOPE_MAX)); + } + mptEnv.Convert(MOD_TYPE_XM, MOD_TYPE_IT); + } + + // Convert an IMFInstrument to OpenMPT's internal instrument representation. + void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX firstSample) const + { + mptIns.name = mpt::String::ReadBuf(mpt::String::nullTerminated, name); + + if(smpNum) + { + for(size_t note = 0; note < std::min(std::size(map), std::size(mptIns.Keyboard) - 12u); note++) + { + mptIns.Keyboard[note + 12] = firstSample + map[note]; + } + } + + mptIns.nFadeOut = fadeout; + mptIns.midiPWD = 1; // For CMD_FINETUNE + + ConvertEnvelope(mptIns.VolEnv, volEnv); + ConvertEnvelope(mptIns.PanEnv, panEnv); + ConvertEnvelope(mptIns.PitchEnv, filterEnv); + if(mptIns.PitchEnv.dwFlags[ENV_ENABLED]) + mptIns.PitchEnv.dwFlags.set(ENV_FILTER); + + // hack to get === to stop notes + if(!mptIns.VolEnv.dwFlags[ENV_ENABLED] && !mptIns.nFadeOut) + mptIns.nFadeOut = 32767; + } +}; + +MPT_BINARY_STRUCT(IMFInstrument, 384) + +struct IMFSample +{ + enum SampleFlags + { + smpLoop = 0x01, + smpPingPongLoop = 0x02, + smp16Bit = 0x04, + smpPanning = 0x08, + }; + + char filename[13]; // Sample filename (12345678.ABC) */ + uint8le unused1[3]; + uint32le length; // Length (in bytes) + uint32le loopStart; // Loop start (in bytes) + uint32le loopEnd; // Loop end (in bytes) + uint32le c5Speed; // Samplerate + uint8le volume; // Default volume (0...64) + uint8le panning; // Default pan (0...255) + uint8le unused2[14]; + uint8le flags; // Sample flags + uint8le unused3[5]; + uint16le ems; // Reserved for internal usage + uint32le dram; // Reserved for internal usage + char is10[4]; // 'IS10' + + // Convert an IMFSample to OpenMPT's internal sample representation. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nC5Speed = c5Speed; + mptSmp.nVolume = volume * 4; + mptSmp.nPan = panning; + if(flags & smpLoop) + mptSmp.uFlags.set(CHN_LOOP); + if(flags & smpPingPongLoop) + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & smp16Bit) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + if(flags & smpPanning) + mptSmp.uFlags.set(CHN_PANNING); + } +}; + +MPT_BINARY_STRUCT(IMFSample, 64) + + +static constexpr EffectCommand imfEffects[] = +{ + CMD_NONE, + CMD_SPEED, // 0x01 1xx Set Tempo + CMD_TEMPO, // 0x02 2xx Set BPM + CMD_TONEPORTAMENTO, // 0x03 3xx Tone Portamento + CMD_TONEPORTAVOL, // 0x04 4xy Tone Portamento + Volume Slide + CMD_VIBRATO, // 0x05 5xy Vibrato + CMD_VIBRATOVOL, // 0x06 6xy Vibrato + Volume Slide + CMD_FINEVIBRATO, // 0x07 7xy Fine Vibrato + CMD_TREMOLO, // 0x08 8xy Tremolo + CMD_ARPEGGIO, // 0x09 9xy Arpeggio + CMD_PANNING8, // 0x0A Axx Set Pan Position + CMD_PANNINGSLIDE, // 0x0B Bxy Pan Slide + CMD_VOLUME, // 0x0C Cxx Set Volume + CMD_VOLUMESLIDE, // 0x0D Dxy Volume Slide + CMD_VOLUMESLIDE, // 0x0E Exy Fine Volume Slide + CMD_FINETUNE, // 0x0F Fxx Set Finetune + CMD_NOTESLIDEUP, // 0x10 Gxy Note Slide Up + CMD_NOTESLIDEDOWN, // 0x11 Hxy Note Slide Down + CMD_PORTAMENTOUP, // 0x12 Ixx Slide Up + CMD_PORTAMENTODOWN, // 0x13 Jxx Slide Down + CMD_PORTAMENTOUP, // 0x14 Kxx Fine Slide Up + CMD_PORTAMENTODOWN, // 0x15 Lxx Fine Slide Down + CMD_MIDI, // 0x16 Mxx Set Filter Cutoff + CMD_MIDI, // 0x17 Nxy Filter Slide + Resonance + CMD_OFFSET, // 0x18 Oxx Set Sample Offset + CMD_NONE, // 0x19 Pxx Set Fine Sample Offset - XXX + CMD_KEYOFF, // 0x1A Qxx Key Off + CMD_RETRIG, // 0x1B Rxy Retrig + CMD_TREMOR, // 0x1C Sxy Tremor + CMD_POSITIONJUMP, // 0x1D Txx Position Jump + CMD_PATTERNBREAK, // 0x1E Uxx Pattern Break + CMD_GLOBALVOLUME, // 0x1F Vxx Set Mastervolume + CMD_GLOBALVOLSLIDE, // 0x20 Wxy Mastervolume Slide + CMD_S3MCMDEX, // 0x21 Xxx Extended Effect + // X1x Set Filter + // X3x Glissando + // X5x Vibrato Waveform + // X8x Tremolo Waveform + // XAx Pattern Loop + // XBx Pattern Delay + // XCx Note Cut + // XDx Note Delay + // XEx Ignore Envelope + // XFx Invert Loop + CMD_NONE, // 0x22 Yxx Chorus - XXX + CMD_NONE, // 0x23 Zxx Reverb - XXX +}; + +static void ImportIMFEffect(ModCommand &m) +{ + uint8 n; + // fix some of them + switch(m.command) + { + case 0xE: // fine volslide + // hackaround to get almost-right behavior for fine slides (i think!) + if(m.param == 0) + /* nothing */; + else if(m.param == 0xF0) + m.param = 0xEF; + else if(m.param == 0x0F) + m.param = 0xFE; + else if(m.param & 0xF0) + m.param |= 0x0F; + else + m.param |= 0xF0; + break; + case 0xF: // set finetune + m.param ^= 0x80; + break; + case 0x14: // fine slide up + case 0x15: // fine slide down + // this is about as close as we can do... + if(m.param >> 4) + m.param = 0xF0 | (m.param >> 4); + else + m.param |= 0xE0; + break; + case 0x16: // cutoff + m.param = (0xFF - m.param) / 2u; + break; + case 0x17: // cutoff slide + resonance (TODO: cutoff slide is currently not handled) + m.param = 0x80 | (m.param & 0x0F); + break; + case 0x1F: // set global volume + m.param = mpt::saturate_cast<uint8>(m.param * 2); + break; + case 0x21: + n = 0; + switch (m.param >> 4) + { + case 0: + /* undefined, but since S0x does nothing in IT anyway, we won't care. + this is here to allow S00 to pick up the previous value (assuming IMF + even does that -- I haven't actually tried it) */ + break; + default: // undefined + case 0x1: // set filter + case 0xF: // invert loop + m.command = CMD_NONE; + break; + case 0x3: // glissando + n = 0x20; + break; + case 0x5: // vibrato waveform + n = 0x30; + break; + case 0x8: // tremolo waveform + n = 0x40; + break; + case 0xA: // pattern loop + n = 0xB0; + break; + case 0xB: // pattern delay + n = 0xE0; + break; + case 0xC: // note cut + case 0xD: // note delay + // Apparently, Imago Orpheus doesn't cut samples on tick 0. + if(!m.param) + m.command = CMD_NONE; + break; + case 0xE: // ignore envelope + switch(m.param & 0x0F) + { + // All envelopes + // Predicament: we can only disable one envelope at a time. Volume is probably most noticeable, so let's go with that. + case 0: m.param = 0x77; break; + // Volume + case 1: m.param = 0x77; break; + // Panning + case 2: m.param = 0x79; break; + // Filter + case 3: m.param = 0x7B; break; + } + break; + case 0x18: // sample offset + // O00 doesn't pick up the previous value + if(!m.param) + m.command = CMD_NONE; + break; + } + if(n) + m.param = n | (m.param & 0x0F); + break; + } + m.command = (m.command < std::size(imfEffects)) ? imfEffects[m.command] : CMD_NONE; + if(m.command == CMD_VOLUME && m.volcmd == VOLCMD_NONE) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + m.command = CMD_NONE; + m.param = 0; + } +} + + +static bool ValidateHeader(const IMFFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.im10, "IM10", 4) + || fileHeader.ordNum > 256 + || fileHeader.insNum >= MAX_INSTRUMENTS + || fileHeader.bpm < 32 + || fileHeader.master > 64 + || fileHeader.amp < 4 + || fileHeader.amp > 127) + { + return false; + } + bool channelFound = false; + for(const auto &chn : fileHeader.channels) + { + switch(chn.status) + { + case 0: // enabled; don't worry about it + channelFound = true; + break; + case 1: // mute + channelFound = true; + break; + case 2: // disabled + // nothing + break; + default: // uhhhh.... freak out + return false; + } + } + if(!channelFound) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const IMFFileHeader &fileHeader) +{ + return 256 + fileHeader.patNum * 4 + fileHeader.insNum * sizeof(IMFInstrument); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderIMF(MemoryFileReader file, const uint64 *pfilesize) +{ + IMFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadIMF(FileReader &file, ModLoadingFlags loadFlags) +{ + IMFFileHeader fileHeader; + file.Rewind(); + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Read channel configuration + std::bitset<32> ignoreChannels; // bit set for each channel that's completely disabled + uint8 detectedChannels = 0; + for(uint8 chn = 0; chn < 32; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = fileHeader.channels[chn].panning * 256 / 255; + + ChnSettings[chn].szName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.channels[chn].name); + + // TODO: reverb/chorus? + switch(fileHeader.channels[chn].status) + { + case 0: // enabled; don't worry about it + detectedChannels = chn + 1; + break; + case 1: // mute + ChnSettings[chn].dwFlags = CHN_MUTE; + detectedChannels = chn + 1; + break; + case 2: // disabled + ChnSettings[chn].dwFlags = CHN_MUTE; + ignoreChannels[chn] = true; + break; + default: // uhhhh.... freak out + return false; + } + } + + InitializeGlobals(MOD_TYPE_IMF); + m_nChannels = detectedChannels; + + m_modFormat.formatName = U_("Imago Orpheus"); + m_modFormat.type = U_("imf"); + m_modFormat.charset = mpt::Charset::CP437; + + //From mikmod: work around an Orpheus bug + if(fileHeader.channels[0].status == 0) + { + CHANNELINDEX chn; + for(chn = 1; chn < 16; chn++) + if(fileHeader.channels[chn].status != 1) + break; + if(chn == 16) + for(chn = 1; chn < 16; chn++) + ChnSettings[chn].dwFlags.reset(CHN_MUTE); + } + + // Song Name + m_songName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.title); + + m_SongFlags.set(SONG_LINEARSLIDES, fileHeader.flags & IMFFileHeader::linearSlides); + m_nDefaultSpeed = fileHeader.tempo; + m_nDefaultTempo.Set(fileHeader.bpm); + m_nDefaultGlobalVolume = fileHeader.master * 4u; + m_nSamplePreAmp = fileHeader.amp; + + m_nInstruments = fileHeader.insNum; + m_nSamples = 0; // Will be incremented later + + uint8 orders[256]; + file.ReadArray(orders); + ReadOrderFromArray(Order(), orders, fileHeader.ordNum, uint16_max, 0xFF); + + // Read patterns + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.patNum); + for(PATTERNINDEX pat = 0; pat < fileHeader.patNum; pat++) + { + const uint16 length = file.ReadUint16LE(), numRows = file.ReadUint16LE(); + FileReader patternChunk = file.ReadChunk(length - 4); + + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows)) + { + continue; + } + + ModCommand dummy; + ROWINDEX row = 0; + while(row < numRows) + { + uint8 mask = patternChunk.ReadUint8(); + if(mask == 0) + { + row++; + continue; + } + + uint8 channel = mask & 0x1F; + ModCommand &m = (channel < GetNumChannels()) ? *Patterns[pat].GetpModCommand(row, channel) : dummy; + + if(mask & 0x20) + { + // Read note/instrument + const auto [note, instr] = patternChunk.ReadArray<uint8, 2>(); + m.note = note; + m.instr = instr; + + if(m.note == 160) + { + m.note = NOTE_KEYOFF; + } else if(m.note == 255) + { + m.note = NOTE_NONE; + } else + { + m.note = (m.note >> 4) * 12 + (m.note & 0x0F) + 12 + 1; + if(!m.IsNoteOrEmpty()) + { + m.note = NOTE_NONE; + } + } + } + if((mask & 0xC0) == 0xC0) + { + // Read both effects and figure out what to do with them + const auto [e1c, e1d, e2c, e2d] = patternChunk.ReadArray<uint8, 4>(); // Command 1, Data 1, Command 2, Data 2 + + if(e1c == 0x0C) + { + m.vol = std::min(e1d, uint8(0x40)); + m.volcmd = VOLCMD_VOLUME; + m.command = e2c; + m.param = e2d; + } else if(e2c == 0x0C) + { + m.vol = std::min(e2d, uint8(0x40)); + m.volcmd = VOLCMD_VOLUME; + m.command = e1c; + m.param = e1d; + } else if(e1c == 0x0A) + { + m.vol = e1d * 64 / 255; + m.volcmd = VOLCMD_PANNING; + m.command = e2c; + m.param = e2d; + } else if(e2c == 0x0A) + { + m.vol = e2d * 64 / 255; + m.volcmd = VOLCMD_PANNING; + m.command = e1c; + m.param = e1d; + } else + { + /* check if one of the effects is a 'global' effect + -- if so, put it in some unused channel instead. + otherwise pick the most important effect. */ + m.command = e2c; + m.param = e2d; + } + } else if(mask & 0xC0) + { + // There's one effect, just stick it in the effect column + const auto [command, param] = patternChunk.ReadArray<uint8, 2>(); + m.command = command; + m.param = param; + } + if(m.command) + ImportIMFEffect(m); + if(ignoreChannels[channel] && m.IsGlobalCommand()) + m.command = CMD_NONE; + } + } + + SAMPLEINDEX firstSample = 1; // first sample index of the current instrument + + // read instruments + for(INSTRUMENTINDEX ins = 0; ins < GetNumInstruments(); ins++) + { + ModInstrument *instr = AllocateInstrument(ins + 1); + IMFInstrument instrumentHeader; + if(!file.ReadStruct(instrumentHeader) || instr == nullptr) + { + continue; + } + + // Orpheus does not check this! + //if(memcmp(instrumentHeader.ii10, "II10", 4) != 0) + // return false; + instrumentHeader.ConvertToMPT(*instr, firstSample); + + // Read this instrument's samples + for(SAMPLEINDEX smp = 0; smp < instrumentHeader.smpNum; smp++) + { + IMFSample sampleHeader; + file.ReadStruct(sampleHeader); + + const SAMPLEINDEX smpID = firstSample + smp; + if(memcmp(sampleHeader.is10, "IS10", 4) || smpID >= MAX_SAMPLES) + { + continue; + } + + m_nSamples = smpID; + ModSample &sample = Samples[smpID]; + + sampleHeader.ConvertToMPT(sample); + m_szNames[smpID] = sample.filename; + + if(sampleHeader.length) + { + FileReader sampleChunk = file.ReadChunk(sampleHeader.length); + if(loadFlags & loadSampleData) + { + SampleIO( + sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(sample, sampleChunk); + } + } + } + firstSample += instrumentHeader.smpNum; + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_it.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_it.cpp new file mode 100644 index 00000000..734912e2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_it.cpp @@ -0,0 +1,2531 @@ +/* + * 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 <sstream> +#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<uint32>. +*/ + + +#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<CTuning*, uint16> 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<uint16>(tNameToShort_Map.size()); + mpt::IO::WriteIntLE<uint16>(oStrm, tuningMapSize); + for(auto &iter : tNameToShort_Map) + { + if(iter.first) + mpt::IO::WriteSizedStringLE<uint8>(oStrm, mpt::ToCharset(mpt::Charset::UTF8, iter.first->GetName())); + else //Case: Using original IT tuning. + mpt::IO::WriteSizedStringLE<uint8>(oStrm, "->MPT_ORIGINAL_IT<-"); + + mpt::IO::WriteIntLE<uint16>(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<uint16>(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<class TUNNUMTYPE, class STRSIZETYPE> +static bool ReadTuningMapTemplate(std::istream& iStrm, std::map<uint16, mpt::ustring> &shortToTNameMap, mpt::Charset charset, const size_t maxNum = 500) +{ + TUNNUMTYPE numTuning = 0; + mpt::IO::ReadIntLE<TUNNUMTYPE>(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<STRSIZETYPE>(iStrm, temp, 255)) + return true; + + mpt::IO::ReadIntLE<uint16>(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<uint16, mpt::ustring> shortToTNameMap; + if(old) + { + ReadTuningMapTemplate<uint32, uint32>(iStrm, shortToTNameMap, charset); + } else + { + ReadTuningMapTemplate<uint16, uint8>(iStrm, shortToTNameMap, charset); + } + + // Read & set tunings for instruments + std::vector<mpt::ustring> notFoundTunings; + for(INSTRUMENTINDEX i = 1; i<=csf.GetNumInstruments(); i++) + { + uint16 ui = 0; + mpt::IO::ReadIntLE<uint16>(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<CTuning> pNewTuning = std::unique_ptr<CTuning>(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<CTuning> 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<mpt::String::maybeNullTerminated>(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<int32>((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<FileReader::off_t>(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<uint8>(fileHeader.tempo))); + m_nSamplePreAmp = std::min(static_cast<uint8>(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<uint8, uint8>(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<uint32le>(Order(), file, numOrd)) + return false; + } else + { + ReadOrderFromFile<uint8>(Order(), file, fileHeader.ordnum, 0xFF, 0xFE); + } + + // Reading instrument, sample and pattern offsets + std::vector<uint32le> 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<decltype(minPtr)>::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<MIDIMacroConfigData>(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<CHANNELINDEX>(chnNames.GetLength() / MAX_CHANNELNAME)); + m_nChannels = readChns; + + for(CHANNELINDEX i = 0; i < readChns; i++) + { + chnNames.ReadString<mpt::String::maybeNullTerminated>(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<INSTRUMENTINDEX>(fileHeader.insnum), static_cast<INSTRUMENTINDEX>(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<SAMPLEINDEX>(fileHeader.smpnum), static_cast<SAMPLEINDEX>(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<mpt::String::maybeNullTerminated>(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<PATTERNINDEX>(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<uint8> 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<uint8> chnMask(GetNumChannels()); + std::vector<ModCommand> 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<uint8, 2>(); + 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<uint32>(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<std::string>(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<uint16>(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<uint8>(m_nDefaultRowsPerBeat); + itHeader.highlight_major = mpt::saturate_cast<uint8>(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<uint32le> patpos(itHeader.patnum); + std::vector<uint32le> smppos(itHeader.smpnum); + std::vector<uint32le> 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<uint16>((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<uint8>(m_nDefaultGlobalVolume / 2u); + itHeader.mv = static_cast<uint8>(std::min(m_nSamplePreAmp, uint32(128))); + itHeader.speed = mpt::saturate_cast<uint8>(m_nDefaultSpeed); + itHeader.tempo = mpt::saturate_cast<uint8>(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<uint8>(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<uint16>(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<MIDIMacroConfigData &>(m_MidiCfg)); + } + + // Writing pattern names + if(numNamedPats) + { + mpt::IO::WriteRaw(f, "PNAM", 4); + mpt::IO::WriteIntLE<uint32>(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<uint32>(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<uint32>(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<uint32>(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<uint32>(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<uint32>(dwPos); + + // Write pattern header + ROWINDEX writeRows = mpt::saturate_cast<uint16>(Patterns[pat].GetNumRows()); + uint16 writeSize = 0; + uint16le patinfo[4]; + patinfo[0] = 0; + patinfo[1] = static_cast<uint16>(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<ChnState> 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<uint8> 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<uint8>((ch + 1) | IT_bitmask_patternChanEnabled_c); + buf[len++] = b; + } else + { + buf[len++] = static_cast<uint8>(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<uint32>(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<uint32>(f, static_cast<uint32>(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<uint32>(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<uint32>(f, chunkSize); + mpt::IO::Write(f, m_MixPlugins[i].Info); + mpt::IO::WriteIntLE<uint32>(f, plugDataSize); + if(plugDataSize) + { + mpt::IO::WriteRaw(f, m_MixPlugins[i].pluginData.data(), plugDataSize); + } + + mpt::IO::WriteIntLE<uint32>(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<int32>(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<uint32le> 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<uint32>(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<PLUGINDEX>(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<size_t>(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<uint32>(f, MagicBE("MPTS")); + +#define WRITEMODULARHEADER(code, fsize) \ + { \ + mpt::IO::WriteIntLE<uint32>(f, code); \ + MPT_ASSERT(mpt::in_range<uint16>(fsize)); \ + const uint16 _size = fsize; \ + mpt::IO::WriteIntLE<uint16>(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<uint8>(m_nTempoMode); + mpt::IO::WriteIntLE(f, mode); + } + + const int32 tmpMixLevels = static_cast<int32>(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<uint32>(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<uint16>(2 + std::size(sample.cues) * 4)); + mpt::IO::WriteIntLE<uint16>(f, smp); + for(auto cue : sample.cues) + { + mpt::IO::WriteIntLE<uint32>(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<uint16>(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<uint16>(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<uint16>(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<uint16>(objectsize)) + { + AddToLog(LogWarning, U_("Too many MIDI Mapping directives to save; data won't be written.")); + } else + { + WRITEMODULARHEADER(MagicBE("MIMA"), static_cast<uint16>(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<uint8, 4> rgb{static_cast<uint8>(color), static_cast<uint8>(color >> 8), static_cast<uint8>(color >> 16), static_cast<uint8>(color >> 24)}; + mpt::IO::Write(f, rgb); + } + } + } +#endif + +#undef WRITEMODULAR +#undef WRITEMODULARHEADER + return; +} + +#endif // MODPLUG_NO_FILESAVE + + +template<typename T> +void ReadField(FileReader &chunk, std::size_t size, T &field) +{ + field = chunk.ReadSizedIntLE<T>(size); +} + + +template<typename T> +void ReadFieldCast(FileReader &chunk, std::size_t size, T &field) +{ + static_assert(sizeof(T) <= sizeof(int32)); + field = static_cast<T>(chunk.ReadSizedIntLE<int32>(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<CHANNELINDEX>(size / 4u)); + for(CHANNELINDEX i = 0; i < numChannels; i++) + { + auto rgb = chunk.ReadArray<uint8, 4>(); + 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<mpt::String::spacePadded>(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<decltype(ChnSettings)>::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<uint8, 2>(); + 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<std::string>(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 diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_itp.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_itp.cpp new file mode 100644 index 00000000..e6555f68 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_itp.cpp @@ -0,0 +1,432 @@ +/* + * Load_itp.cpp + * ------------ + * Purpose: Impulse Tracker Project (ITP) module loader + * Notes : Despite its name, ITP is not a format supported by Impulse Tracker. + * In fact, it's a format invented by the OpenMPT team to allow people to work + * with the IT format, but keeping the instrument files with big samples separate + * from the pattern data, to keep the work files small and handy. + * The design of the format is quite flawed, though, so it was superseded by + * extra functionality in the MPTM format in OpenMPT 1.24. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "../common/version.h" +#include "Loaders.h" +#include "ITTools.h" +#ifdef MODPLUG_TRACKER +// For loading external instruments +#include "../mptrack/Moddoc.h" +#endif // MODPLUG_TRACKER +#ifdef MPT_EXTERNAL_SAMPLES +#include "../common/mptFileIO.h" +#endif // MPT_EXTERNAL_SAMPLES + +OPENMPT_NAMESPACE_BEGIN + +// Version changelog: +// v1.03: - Relative unicode instrument paths instead of absolute ANSI paths +// - Per-path variable string length +// - Embedded samples are IT-compressed +// (rev. 3249) +// v1.02: Explicitly updated format to use new instrument flags representation (rev. 483) +// v1.01: Added option to embed instrument headers + + +struct ITPModCommand +{ + uint8 note; + uint8 instr; + uint8 volcmd; + uint8 command; + uint8 vol; + uint8 param; + + operator ModCommand() const + { + static constexpr VolumeCommand ITPVolCmds[] = + { + VOLCMD_NONE, VOLCMD_VOLUME, VOLCMD_PANNING, VOLCMD_VOLSLIDEUP, + VOLCMD_VOLSLIDEDOWN, VOLCMD_FINEVOLUP, VOLCMD_FINEVOLDOWN, VOLCMD_VIBRATOSPEED, + VOLCMD_VIBRATODEPTH, VOLCMD_PANSLIDELEFT, VOLCMD_PANSLIDERIGHT, VOLCMD_TONEPORTAMENTO, + VOLCMD_PORTAUP, VOLCMD_PORTADOWN, VOLCMD_PLAYCONTROL, VOLCMD_OFFSET, + }; + static constexpr EffectCommand ITPCommands[] = + { + CMD_NONE, CMD_ARPEGGIO, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, + CMD_TONEPORTAMENTO, CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, + CMD_TREMOLO, CMD_PANNING8, CMD_OFFSET, CMD_VOLUMESLIDE, + CMD_POSITIONJUMP, CMD_VOLUME, CMD_PATTERNBREAK, CMD_RETRIG, + CMD_SPEED, CMD_TEMPO, CMD_TREMOR, CMD_MODCMDEX, + CMD_S3MCMDEX, CMD_CHANNELVOLUME, CMD_CHANNELVOLSLIDE, CMD_GLOBALVOLUME, + CMD_GLOBALVOLSLIDE, CMD_KEYOFF, CMD_FINEVIBRATO, CMD_PANBRELLO, + CMD_XFINEPORTAUPDOWN, CMD_PANNINGSLIDE, CMD_SETENVPOSITION, CMD_MIDI, + CMD_SMOOTHMIDI, CMD_DELAYCUT, CMD_XPARAM, + }; + ModCommand result; + result.note = (ModCommand::IsNote(note) || ModCommand::IsSpecialNote(note)) ? static_cast<ModCommand::NOTE>(note) : static_cast<ModCommand::NOTE>(NOTE_NONE); + result.instr = instr; + result.volcmd = (volcmd < std::size(ITPVolCmds)) ? ITPVolCmds[volcmd] : VOLCMD_NONE; + result.command = (command < std::size(ITPCommands)) ? ITPCommands[command] : CMD_NONE; + result.vol = vol; + result.param = param; + return result; + } +}; + +MPT_BINARY_STRUCT(ITPModCommand, 6) + + +struct ITPHeader +{ + uint32le magic; + uint32le version; +}; + +MPT_BINARY_STRUCT(ITPHeader, 8) + + +static bool ValidateHeader(const ITPHeader &hdr) +{ + if(hdr.magic != MagicBE(".itp")) + { + return false; + } + if(hdr.version < 0x00000100 || hdr.version > 0x00000103) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const ITPHeader &hdr) +{ + return 76 + (hdr.version <= 0x102 ? 4 : 0); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderITP(MemoryFileReader file, const uint64 *pfilesize) +{ + ITPHeader hdr; + if(!file.ReadStruct(hdr)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(hdr)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(hdr)); +} + + +bool CSoundFile::ReadITP(FileReader &file, ModLoadingFlags loadFlags) +{ +#if !defined(MPT_EXTERNAL_SAMPLES) && !defined(MPT_FUZZ_TRACKER) + // Doesn't really make sense to support this format when there's no support for external files... + MPT_UNREFERENCED_PARAMETER(file); + MPT_UNREFERENCED_PARAMETER(loadFlags); + return false; +#else // !MPT_EXTERNAL_SAMPLES && !MPT_FUZZ_TRACKER + + enum ITPSongFlags + { + ITP_EMBEDMIDICFG = 0x00001, // Embed macros in file + ITP_ITOLDEFFECTS = 0x00004, // Old Impulse Tracker effect implementations + ITP_ITCOMPATGXX = 0x00008, // IT "Compatible Gxx" (IT's flag to behave more like other trackers w/r/t portamento effects) + ITP_LINEARSLIDES = 0x00010, // Linear slides vs. Amiga slides + ITP_EXFILTERRANGE = 0x08000, // Cutoff Filter has double frequency range (up to ~10Khz) + ITP_ITPROJECT = 0x20000, // Is a project file + ITP_ITPEMBEDIH = 0x40000, // Embed instrument headers in project file + }; + + file.Rewind(); + + ITPHeader hdr; + if(!file.ReadStruct(hdr)) + { + return false; + } + if(!ValidateHeader(hdr)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(hdr)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + const uint32 version = hdr.version; + + InitializeGlobals(MOD_TYPE_IT); + m_playBehaviour.reset(); + file.ReadSizedString<uint32le, mpt::String::maybeNullTerminated>(m_songName); + + // Song comments + m_songMessage.Read(file, file.ReadUint32LE(), SongMessage::leCR); + + // Song global config + const uint32 songFlags = file.ReadUint32LE(); + if(!(songFlags & ITP_ITPROJECT)) + { + return false; + } + m_SongFlags.set(SONG_IMPORTED); + if(songFlags & ITP_ITOLDEFFECTS) + m_SongFlags.set(SONG_ITOLDEFFECTS); + if(songFlags & ITP_ITCOMPATGXX) + m_SongFlags.set(SONG_ITCOMPATGXX); + if(songFlags & ITP_LINEARSLIDES) + m_SongFlags.set(SONG_LINEARSLIDES); + if(songFlags & ITP_EXFILTERRANGE) + m_SongFlags.set(SONG_EXFILTERRANGE); + + m_nDefaultGlobalVolume = file.ReadUint32LE(); + m_nSamplePreAmp = file.ReadUint32LE(); + m_nDefaultSpeed = std::max(uint32(1), file.ReadUint32LE()); + m_nDefaultTempo.Set(std::max(uint32(32), file.ReadUint32LE())); + m_nChannels = static_cast<CHANNELINDEX>(file.ReadUint32LE()); + if(m_nChannels == 0 || m_nChannels > MAX_BASECHANNELS) + { + return false; + } + + // channel name string length (=MAX_CHANNELNAME) + uint32 size = file.ReadUint32LE(); + + // Channels' data + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].nPan = std::min(static_cast<uint16>(file.ReadUint32LE()), uint16(256)); + ChnSettings[chn].dwFlags.reset(); + uint32 flags = file.ReadUint32LE(); + if(flags & 0x100) ChnSettings[chn].dwFlags.set(CHN_MUTE); + if(flags & 0x800) ChnSettings[chn].dwFlags.set(CHN_SURROUND); + ChnSettings[chn].nVolume = std::min(static_cast<uint16>(file.ReadUint32LE()), uint16(64)); + file.ReadString<mpt::String::maybeNullTerminated>(ChnSettings[chn].szName, size); + } + + // Song mix plugins + { + FileReader plugChunk = file.ReadChunk(file.ReadUint32LE()); + LoadMixPlugins(plugChunk); + } + + // MIDI Macro config + file.ReadStructPartial<MIDIMacroConfigData>(m_MidiCfg, file.ReadUint32LE()); + m_MidiCfg.Sanitize(); + + // Song Instruments + m_nInstruments = static_cast<INSTRUMENTINDEX>(file.ReadUint32LE()); + if(m_nInstruments >= MAX_INSTRUMENTS) + { + m_nInstruments = 0; + return false; + } + + // Instruments' paths + if(version <= 0x102) + { + size = file.ReadUint32LE(); // path string length + } + + std::vector<mpt::PathString> instrPaths(GetNumInstruments()); + for(INSTRUMENTINDEX ins = 0; ins < GetNumInstruments(); ins++) + { + if(version > 0x102) + { + size = file.ReadUint32LE(); // path string length + } + std::string path; + file.ReadString<mpt::String::maybeNullTerminated>(path, size); +#ifdef MODPLUG_TRACKER + if(version <= 0x102) + { + instrPaths[ins] = mpt::PathString::FromLocaleSilent(path); + } else +#endif // MODPLUG_TRACKER + { + instrPaths[ins] = mpt::PathString::FromUTF8(path); + } +#ifdef MODPLUG_TRACKER + if(const auto fileName = file.GetOptionalFileName(); fileName.has_value()) + { + instrPaths[ins] = instrPaths[ins].RelativePathToAbsolute(fileName->GetPath()); + } else if(GetpModDoc() != nullptr) + { + instrPaths[ins] = instrPaths[ins].RelativePathToAbsolute(GetpModDoc()->GetPathNameMpt().GetPath()); + } +#endif // MODPLUG_TRACKER + } + + // Song Orders + size = file.ReadUint32LE(); + ReadOrderFromFile<uint8>(Order(), file, size, 0xFF, 0xFE); + + // Song Patterns + const PATTERNINDEX numPats = static_cast<PATTERNINDEX>(file.ReadUint32LE()); + const PATTERNINDEX numNamedPats = static_cast<PATTERNINDEX>(file.ReadUint32LE()); + size_t patNameLen = file.ReadUint32LE(); // Size of each pattern name + FileReader pattNames = file.ReadChunk(numNamedPats * patNameLen); + + // modcommand data length + size = file.ReadUint32LE(); + if(size != sizeof(ITPModCommand)) + { + return false; + } + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(numPats); + for(PATTERNINDEX pat = 0; pat < numPats; pat++) + { + const ROWINDEX numRows = file.ReadUint32LE(); + FileReader patternChunk = file.ReadChunk(numRows * size * GetNumChannels()); + + // Allocate pattern + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows)) + { + pattNames.Skip(patNameLen); + continue; + } + + if(pat < numNamedPats) + { + char patName[32]; + if(pattNames.ReadString<mpt::String::maybeNullTerminated>(patName, patNameLen)) + Patterns[pat].SetName(patName); + } + + // Pattern data + size_t numCommands = GetNumChannels() * numRows; + + if(patternChunk.CanRead(sizeof(ITPModCommand) * numCommands)) + { + ModCommand *target = Patterns[pat].GetpModCommand(0, 0); + while(numCommands-- != 0) + { + ITPModCommand data; + patternChunk.ReadStruct(data); + *(target++) = data; + } + } + } + + // Load embedded samples + + // Read original number of samples + m_nSamples = static_cast<SAMPLEINDEX>(file.ReadUint32LE()); + LimitMax(m_nSamples, SAMPLEINDEX(MAX_SAMPLES - 1)); + + // Read number of embedded samples - at most as many as there are real samples in a valid file + uint32 embeddedSamples = file.ReadUint32LE(); + if(embeddedSamples > m_nSamples) + { + return false; + } + + // Read samples + for(uint32 smp = 0; smp < embeddedSamples && file.CanRead(8 + sizeof(ITSample)); smp++) + { + uint32 realSample = file.ReadUint32LE(); + ITSample sampleHeader; + file.ReadStruct(sampleHeader); + FileReader sampleData = file.ReadChunk(file.ReadUint32LE()); + + if((loadFlags & loadSampleData) + && realSample >= 1 && realSample <= GetNumSamples() + && Samples[realSample].pData.pSample == nullptr + && !memcmp(sampleHeader.id, "IMPS", 4)) + { + sampleHeader.ConvertToMPT(Samples[realSample]); + m_szNames[realSample] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); + + // Read sample data + sampleHeader.GetSampleFormat().ReadSample(Samples[realSample], sampleData); + } + } + + // Load instruments + for(INSTRUMENTINDEX ins = 0; ins < GetNumInstruments(); ins++) + { + if(instrPaths[ins].empty()) + continue; + +#ifdef MPT_EXTERNAL_SAMPLES + InputFile f(instrPaths[ins], SettingCacheCompleteFileBeforeLoading()); + FileReader instrFile = GetFileReader(f); + if(!ReadInstrumentFromFile(ins + 1, instrFile, true)) + { + AddToLog(LogWarning, U_("Unable to open instrument: ") + instrPaths[ins].ToUnicode()); + } +#else + AddToLog(LogWarning, MPT_UFORMAT("Loading external instrument {} ('{}') failed: External instruments are not supported.")(ins + 1, instrPaths[ins].ToUnicode())); +#endif // MPT_EXTERNAL_SAMPLES + } + + // Extra info data + uint32 code = file.ReadUint32LE(); + + // Embed instruments' header [v1.01] + if(version >= 0x101 && (songFlags & ITP_ITPEMBEDIH) && code == MagicBE("EBIH")) + { + code = file.ReadUint32LE(); + + INSTRUMENTINDEX ins = 1; + while(ins <= GetNumInstruments() && file.CanRead(4)) + { + if(code == MagicBE("MPTS")) + { + break; + } else if(code == MagicBE("SEP@") || code == MagicBE("MPTX")) + { + // jump code - switch to next instrument + ins++; + } else + { + ReadExtendedInstrumentProperty(Instruments[ins], code, file); + } + + code = file.ReadUint32LE(); + } + } + + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + Samples[smp].SetDefaultCuePoints(); + } + + // Song extensions + if(code == MagicBE("MPTS")) + { + file.SkipBack(4); + LoadExtendedSongProperties(file, true); + } + + m_nMaxPeriod = 0xF000; + m_nMinPeriod = 8; + + // Before OpenMPT 1.20.01.09, the MIDI macros were always read from the file, even if the "embed" flag was not set. + if(m_dwLastSavedWithVersion >= MPT_V("1.20.01.09") && !(songFlags & ITP_EMBEDMIDICFG)) + { + m_MidiCfg.Reset(); + } + + m_modFormat.formatName = U_("Impulse Tracker Project"); + m_modFormat.type = U_("itp"); + m_modFormat.madeWithTracker = U_("OpenMPT ") + mpt::ufmt::val(m_dwLastSavedWithVersion); + m_modFormat.charset = mpt::Charset::Windows1252; + + return true; +#endif // MPT_EXTERNAL_SAMPLES +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mdl.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mdl.cpp new file mode 100644 index 00000000..24375620 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mdl.cpp @@ -0,0 +1,823 @@ +/* + * Load_mdl.cpp + * ------------ + * Purpose: Digitrakker (MDL) module loader + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +// MDL file header +struct MDLFileHeader +{ + char id[4]; // "DMDL" + uint8 version; +}; + +MPT_BINARY_STRUCT(MDLFileHeader, 5) + + +// RIFF-style Chunk +struct MDLChunk +{ + // 16-Bit chunk identifiers + enum ChunkIdentifiers + { + idInfo = MagicLE("IN"), + idMessage = MagicLE("ME"), + idPats = MagicLE("PA"), + idPatNames = MagicLE("PN"), + idTracks = MagicLE("TR"), + idInstrs = MagicLE("II"), + idVolEnvs = MagicLE("VE"), + idPanEnvs = MagicLE("PE"), + idFreqEnvs = MagicLE("FE"), + idSampleInfo = MagicLE("IS"), + ifSampleData = MagicLE("SA"), + }; + + uint16le id; + uint32le length; + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(MDLChunk, 6) + + +struct MDLInfoBlock +{ + char title[32]; + char composer[20]; + uint16le numOrders; + uint16le restartPos; + uint8le globalVol; // 1...255 + uint8le speed; // 1...255 + uint8le tempo; // 4...255 + uint8le chnSetup[32]; +}; + +MPT_BINARY_STRUCT(MDLInfoBlock, 91) + + +// Sample header in II block +struct MDLSampleHeader +{ + uint8le smpNum; + uint8le lastNote; + uint8le volume; + uint8le volEnvFlags; // 6 bits env #, 2 bits flags + uint8le panning; + uint8le panEnvFlags; + uint16le fadeout; + uint8le vibSpeed; + uint8le vibDepth; + uint8le vibSweep; + uint8le vibType; + uint8le reserved; // zero + uint8le freqEnvFlags; +}; + +MPT_BINARY_STRUCT(MDLSampleHeader, 14) + + +struct MDLEnvelope +{ + uint8 envNum; + struct + { + uint8 x; // Delta value from last point, 0 means no more points defined + uint8 y; // 0...63 + } nodes[15]; + uint8 flags; + uint8 loop; // Lower 4 bits = start, upper 4 bits = end + + void ConvertToMPT(InstrumentEnvelope &mptEnv) const + { + mptEnv.dwFlags.reset(); + mptEnv.clear(); + mptEnv.reserve(15); + int16 tick = -nodes[0].x; + for(uint8 n = 0; n < 15; n++) + { + if(!nodes[n].x) + break; + tick += nodes[n].x; + mptEnv.push_back(EnvelopeNode(tick, std::min(nodes[n].y, uint8(64)))); // actually 0-63 + } + + mptEnv.nLoopStart = (loop & 0x0F); + mptEnv.nLoopEnd = (loop >> 4); + mptEnv.nSustainStart = mptEnv.nSustainEnd = (flags & 0x0F); + + if(flags & 0x10) mptEnv.dwFlags.set(ENV_SUSTAIN); + if(flags & 0x20) mptEnv.dwFlags.set(ENV_LOOP); + } +}; + +MPT_BINARY_STRUCT(MDLEnvelope, 33) + + +struct MDLPatternHeader +{ + uint8le channels; + uint8le lastRow; + char name[16]; +}; + +MPT_BINARY_STRUCT(MDLPatternHeader, 18) + + +enum +{ + MDLNOTE_NOTE = 1 << 0, + MDLNOTE_SAMPLE = 1 << 1, + MDLNOTE_VOLUME = 1 << 2, + MDLNOTE_EFFECTS = 1 << 3, + MDLNOTE_PARAM1 = 1 << 4, + MDLNOTE_PARAM2 = 1 << 5, +}; + + +static constexpr VibratoType MDLVibratoType[] = { VIB_SINE, VIB_RAMP_DOWN, VIB_SQUARE, VIB_SINE }; + +static constexpr ModCommand::COMMAND MDLEffTrans[] = +{ + /* 0 */ CMD_NONE, + /* 1st column only */ + /* 1 */ CMD_PORTAMENTOUP, + /* 2 */ CMD_PORTAMENTODOWN, + /* 3 */ CMD_TONEPORTAMENTO, + /* 4 */ CMD_VIBRATO, + /* 5 */ CMD_ARPEGGIO, + /* 6 */ CMD_NONE, + /* Either column */ + /* 7 */ CMD_TEMPO, + /* 8 */ CMD_PANNING8, + /* 9 */ CMD_SETENVPOSITION, + /* A */ CMD_NONE, + /* B */ CMD_POSITIONJUMP, + /* C */ CMD_GLOBALVOLUME, + /* D */ CMD_PATTERNBREAK, + /* E */ CMD_S3MCMDEX, + /* F */ CMD_SPEED, + /* 2nd column only */ + /* G */ CMD_VOLUMESLIDE, // up + /* H */ CMD_VOLUMESLIDE, // down + /* I */ CMD_RETRIG, + /* J */ CMD_TREMOLO, + /* K */ CMD_TREMOR, + /* L */ CMD_NONE, +}; + + +// receive an MDL effect, give back a 'normal' one. +static void ConvertMDLCommand(uint8 &cmd, uint8 ¶m) +{ + if(cmd >= std::size(MDLEffTrans)) + return; + + uint8 origCmd = cmd; + cmd = MDLEffTrans[cmd]; + + switch(origCmd) + { +#ifdef MODPLUG_TRACKER + case 0x07: // Tempo + // MDL supports any nonzero tempo value, but OpenMPT doesn't + param = std::max(param, uint8(0x20)); + break; +#endif // MODPLUG_TRACKER + case 0x08: // Panning + param = (param & 0x7F) * 2u; + break; + case 0x0C: // Global volume + param = (param + 1) / 2u; + break; + case 0x0D: // Pattern Break + // Convert from BCD + param = 10 * (param >> 4) + (param & 0x0F); + break; + case 0x0E: // Special + switch(param >> 4) + { + case 0x0: // unused + case 0x3: // unused + case 0x8: // Set Samplestatus (loop type) + cmd = CMD_NONE; + break; + case 0x1: // Pan Slide Left + cmd = CMD_PANNINGSLIDE; + param = (std::min(static_cast<uint8>(param & 0x0F), uint8(0x0E)) << 4) | 0x0F; + break; + case 0x2: // Pan Slide Right + cmd = CMD_PANNINGSLIDE; + param = 0xF0 | std::min(static_cast<uint8>(param & 0x0F), uint8(0x0E)); + break; + case 0x4: // Vibrato Waveform + param = 0x30 | (param & 0x0F); + break; + case 0x5: // Set Finetune + cmd = CMD_FINETUNE; + param = (param << 4) ^ 0x80; + break; + case 0x6: // Pattern Loop + param = 0xB0 | (param & 0x0F); + break; + case 0x7: // Tremolo Waveform + param = 0x40 | (param & 0x0F); + break; + case 0x9: // Retrig + cmd = CMD_RETRIG; + param &= 0x0F; + break; + case 0xA: // Global vol slide up + cmd = CMD_GLOBALVOLSLIDE; + param = 0xF0 & (((param & 0x0F) + 1) << 3); + break; + case 0xB: // Global vol slide down + cmd = CMD_GLOBALVOLSLIDE; + param = ((param & 0x0F) + 1) >> 1; + break; + case 0xC: // Note cut + case 0xD: // Note delay + case 0xE: // Pattern delay + // Nothing to change here + break; + case 0xF: // Offset -- further mangled later. + cmd = CMD_OFFSET; + break; + } + break; + case 0x10: // Volslide up + if(param < 0xE0) + { + // 00...DF regular slide - four times more precise than in XM + param >>= 2; + if(param > 0x0F) + param = 0x0F; + param <<= 4; + } else if(param < 0xF0) + { + // E0...EF extra fine slide (on first tick, 4 times finer) + param = (((param & 0x0F) << 2) | 0x0F); + } else + { + // F0...FF regular fine slide (on first tick) - like in XM + param = ((param << 4) | 0x0F); + } + break; + case 0x11: // Volslide down + if(param < 0xE0) + { + // 00...DF regular slide - four times more precise than in XM + param >>= 2; + if(param > 0x0F) + param = 0x0F; + } else if(param < 0xF0) + { + // E0...EF extra fine slide (on first tick, 4 times finer) + param = (((param & 0x0F) >> 2) | 0xF0); + } else + { + // F0...FF regular fine slide (on first tick) - like in XM + } + break; + } +} + + +// Returns true if command was lost +static bool ImportMDLCommands(ModCommand &m, uint8 vol, uint8 e1, uint8 e2, uint8 p1, uint8 p2) +{ + // Map second effect values 1-6 to effects G-L + if(e2 >= 1 && e2 <= 6) + e2 += 15; + + ConvertMDLCommand(e1, p1); + ConvertMDLCommand(e2, p2); + /* From the Digitrakker documentation: + * EFx -xx - Set Sample Offset + This is a double-command. It starts the + sample at adress xxx*256. + Example: C-5 01 -- EF1 -23 ->starts sample + 01 at address 12300 (in hex). + Kind of screwy, but I guess it's better than the mess required to do it with IT (which effectively + requires 3 rows in order to set the offset past 0xff00). If we had access to the entire track, we + *might* be able to shove the high offset SAy into surrounding rows (or 2x MPTM #xx), but it wouldn't + always be possible, it'd make the loader a lot uglier, and generally would be more trouble than + it'd be worth to implement. + + What's more is, if there's another effect in the second column, it's ALSO processed in addition to the + offset, and the second data byte is shared between the two effects. */ + uint32 offset = uint32_max; + uint8 otherCmd = CMD_NONE; + if(e1 == CMD_OFFSET) + { + // EFy -xx => offset yxx00 + offset = ((p1 & 0x0F) << 8) | p2; + p1 = (p1 & 0x0F) ? 0xFF : p2; + if(e2 == CMD_OFFSET) + e2 = CMD_NONE; + else + otherCmd = e2; + } else if (e2 == CMD_OFFSET) + { + // --- EFy => offset y0000 + offset = (p2 & 0x0F) << 8; + p2 = (p2 & 0x0F) ? 0xFF : 0; + otherCmd = e1; + } + + if(offset != uint32_max && offset > 0xFF && ModCommand::GetEffectWeight(otherCmd) < ModCommand::GetEffectWeight(CMD_OFFSET)) + { + m.command = CMD_OFFSET; + m.param = static_cast<ModCommand::PARAM>(offset & 0xFF); + m.volcmd = VOLCMD_OFFSET; + m.vol = static_cast<ModCommand::VOL>(offset >> 8); + return otherCmd != CMD_NONE || vol != 0; + } + + if(vol) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = (vol + 2) / 4u; + } + + // If we have Dxx + G00, or Dxx + H00, combine them into Lxx/Kxx. + ModCommand::CombineEffects(e1, p1, e2, p2); + + bool lostCommand = false; + // Try to fit the "best" effect into e2. + if(e1 == CMD_NONE) + { + // Easy + } else if(e2 == CMD_NONE) + { + // Almost as easy + e2 = e1; + p2 = p1; + } else if(e1 == e2 && e1 != CMD_S3MCMDEX) + { + // Digitrakker processes the effects left-to-right, so if both effects are the same, the + // second essentially overrides the first. + } else if(!vol) + { + lostCommand |= (ModCommand::TwoRegularCommandsToMPT(e1, p1, e2, p2).first != CMD_NONE); + m.volcmd = e1; + m.vol = p1; + } else + { + if(ModCommand::GetEffectWeight((ModCommand::COMMAND)e1) > ModCommand::GetEffectWeight((ModCommand::COMMAND)e2)) + { + std::swap(e1, e2); + std::swap(p1, p2); + } + lostCommand = true; + } + + m.command = e2; + m.param = p2; + return lostCommand; +} + + +static void MDLReadEnvelopes(FileReader file, std::vector<MDLEnvelope> &envelopes) +{ + if(!file.CanRead(1)) + return; + + envelopes.resize(64); + uint8 numEnvs = file.ReadUint8(); + while(numEnvs--) + { + MDLEnvelope mdlEnv; + if(!file.ReadStruct(mdlEnv) || mdlEnv.envNum > 63) + continue; + envelopes[mdlEnv.envNum] = mdlEnv; + } +} + + +static void CopyEnvelope(InstrumentEnvelope &mptEnv, uint8 flags, std::vector<MDLEnvelope> &envelopes) +{ + uint8 envNum = flags & 0x3F; + if(envNum < envelopes.size()) + envelopes[envNum].ConvertToMPT(mptEnv); + mptEnv.dwFlags.set(ENV_ENABLED, (flags & 0x80) && !mptEnv.empty()); +} + + +static bool ValidateHeader(const MDLFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.id, "DMDL", 4) + || fileHeader.version >= 0x20) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMDL(MemoryFileReader file, const uint64 *pfilesize) +{ + MDLFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadMDL(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + MDLFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + ChunkReader chunkFile(file); + ChunkReader::ChunkList<MDLChunk> chunks = chunkFile.ReadChunks<MDLChunk>(0); + + // Read global info + FileReader chunk = chunks.GetChunk(MDLChunk::idInfo); + MDLInfoBlock info; + if(!chunk.IsValid() || !chunk.ReadStruct(info)) + { + return false; + } + + InitializeGlobals(MOD_TYPE_MDL); + m_SongFlags = SONG_ITCOMPATGXX; + m_playBehaviour.set(kPerChannelGlobalVolSlide); + m_playBehaviour.set(kApplyOffsetWithoutNote); + m_playBehaviour.reset(kITVibratoTremoloPanbrello); + m_playBehaviour.reset(kITSCxStopsSample); // Gate effect in underbeat.mdl + + m_modFormat.formatName = U_("Digitrakker"); + m_modFormat.type = U_("mdl"); + m_modFormat.madeWithTracker = U_("Digitrakker ") + ( + (fileHeader.version == 0x11) ? U_("3") // really could be 2.99b - close enough + : (fileHeader.version == 0x10) ? U_("2.3") + : (fileHeader.version == 0x00) ? U_("2.0 - 2.2b") // there was no 1.x release + : U_("")); + m_modFormat.charset = mpt::Charset::CP437; + + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, info.title); + m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, info.composer)); + + m_nDefaultGlobalVolume = info.globalVol + 1; + m_nDefaultSpeed = Clamp<uint8, uint8>(info.speed, 1, 255); + m_nDefaultTempo.Set(Clamp<uint8, uint8>(info.tempo, 4, 255)); + + ReadOrderFromFile<uint8>(Order(), chunk, info.numOrders); + Order().SetRestartPos(info.restartPos); + + m_nChannels = 0; + for(CHANNELINDEX c = 0; c < 32; c++) + { + ChnSettings[c].Reset(); + ChnSettings[c].nPan = (info.chnSetup[c] & 0x7F) * 2u; + if(ChnSettings[c].nPan == 254) + ChnSettings[c].nPan = 256; + if(info.chnSetup[c] & 0x80) + ChnSettings[c].dwFlags.set(CHN_MUTE); + else + m_nChannels = c + 1; + chunk.ReadString<mpt::String::spacePadded>(ChnSettings[c].szName, 8); + } + + // Read song message + chunk = chunks.GetChunk(MDLChunk::idMessage); + m_songMessage.Read(chunk, chunk.GetLength(), SongMessage::leCR); + + // Read sample info and data + chunk = chunks.GetChunk(MDLChunk::idSampleInfo); + if(chunk.IsValid()) + { + FileReader dataChunk = chunks.GetChunk(MDLChunk::ifSampleData); + + uint8 numSamples = chunk.ReadUint8(); + for(uint8 smp = 0; smp < numSamples; smp++) + { + const SAMPLEINDEX sampleIndex = chunk.ReadUint8(); + if(sampleIndex == 0 || sampleIndex >= MAX_SAMPLES || !chunk.CanRead(32 + 8 + 2 + 12 + 2)) + break; + + if(sampleIndex > GetNumSamples()) + m_nSamples = sampleIndex; + + ModSample &sample = Samples[sampleIndex]; + sample.Initialize(); + sample.Set16BitCuePoints(); + + chunk.ReadString<mpt::String::spacePadded>(m_szNames[sampleIndex], 32); + chunk.ReadString<mpt::String::spacePadded>(sample.filename, 8); + + uint32 c4speed; + if(fileHeader.version < 0x10) + c4speed = chunk.ReadUint16LE(); + else + c4speed = chunk.ReadUint32LE(); + sample.nC5Speed = c4speed * 2u; + sample.nLength = chunk.ReadUint32LE(); + sample.nLoopStart = chunk.ReadUint32LE(); + sample.nLoopEnd = chunk.ReadUint32LE(); + if(sample.nLoopEnd != 0) + { + sample.uFlags.set(CHN_LOOP); + sample.nLoopEnd += sample.nLoopStart; + } + uint8 volume = chunk.ReadUint8(); + if(fileHeader.version < 0x10) + sample.nVolume = volume; + uint8 flags = chunk.ReadUint8(); + + if(flags & 0x01) + { + sample.uFlags.set(CHN_16BIT); + sample.nLength /= 2u; + sample.nLoopStart /= 2u; + sample.nLoopEnd /= 2u; + } + + sample.uFlags.set(CHN_PINGPONGLOOP, (flags & 0x02) != 0); + + SampleIO sampleIO( + (flags & 0x01) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (flags & 0x0C) ? SampleIO::MDL : SampleIO::signedPCM); + + if(loadFlags & loadSampleData) + { + sampleIO.ReadSample(sample, dataChunk); + } + } + } + + chunk = chunks.GetChunk(MDLChunk::idInstrs); + if(chunk.IsValid()) + { + std::vector<MDLEnvelope> volEnvs, panEnvs, pitchEnvs; + MDLReadEnvelopes(chunks.GetChunk(MDLChunk::idVolEnvs), volEnvs); + MDLReadEnvelopes(chunks.GetChunk(MDLChunk::idPanEnvs), panEnvs); + MDLReadEnvelopes(chunks.GetChunk(MDLChunk::idFreqEnvs), pitchEnvs); + + uint8 numInstruments = chunk.ReadUint8(); + for(uint8 i = 0; i < numInstruments; i++) + { + const auto [ins, numSamples] = chunk.ReadArray<uint8, 2>(); + uint8 firstNote = 0; + ModInstrument *mptIns = nullptr; + if(ins == 0 + || !chunk.CanRead(32 + sizeof(MDLSampleHeader) * numSamples) + || (mptIns = AllocateInstrument(ins)) == nullptr) + { + chunk.Skip(32 + sizeof(MDLSampleHeader) * numSamples); + continue; + } + + chunk.ReadString<mpt::String::spacePadded>(mptIns->name, 32); + for(uint8 smp = 0; smp < numSamples; smp++) + { + MDLSampleHeader sampleHeader; + chunk.ReadStruct(sampleHeader); + if(sampleHeader.smpNum == 0 || sampleHeader.smpNum > GetNumSamples()) + continue; + + LimitMax(sampleHeader.lastNote, static_cast<uint8>(std::size(mptIns->Keyboard))); + for(uint8 n = firstNote; n <= sampleHeader.lastNote; n++) + { + mptIns->Keyboard[n] = sampleHeader.smpNum; + } + firstNote = sampleHeader.lastNote + 1; + + CopyEnvelope(mptIns->VolEnv, sampleHeader.volEnvFlags, volEnvs); + CopyEnvelope(mptIns->PanEnv, sampleHeader.panEnvFlags, panEnvs); + CopyEnvelope(mptIns->PitchEnv, sampleHeader.freqEnvFlags, pitchEnvs); + mptIns->nFadeOut = (sampleHeader.fadeout + 1u) / 2u; +#ifdef MODPLUG_TRACKER + if((mptIns->VolEnv.dwFlags & (ENV_ENABLED | ENV_LOOP)) == ENV_ENABLED) + { + // Fade-out is only supposed to happen on key-off, not at the end of a volume envelope. + // Fake it by putting a loop at the end. + mptIns->VolEnv.nLoopStart = mptIns->VolEnv.nLoopEnd = static_cast<uint8>(mptIns->VolEnv.size() - 1); + mptIns->VolEnv.dwFlags.set(ENV_LOOP); + } + for(auto &p : mptIns->PitchEnv) + { + // Scale pitch envelope + p.value = (p.value * 6u) / 16u; + } +#endif // MODPLUG_TRACKER + + // Samples were already initialized above. Let's hope they are not going to be re-used with different volume / panning / vibrato... + ModSample &mptSmp = Samples[sampleHeader.smpNum]; + + // This flag literally enables and disables the default volume of a sample. If you disable this flag, + // the sample volume of a previously sample is re-used, even if you put an instrument number next to the note. + if(sampleHeader.volEnvFlags & 0x40) + mptSmp.nVolume = sampleHeader.volume; + else + mptSmp.uFlags.set(SMP_NODEFAULTVOLUME); + mptSmp.nPan = std::min(static_cast<uint16>(sampleHeader.panning * 2), uint16(254)); + mptSmp.nVibType = MDLVibratoType[sampleHeader.vibType & 3]; + mptSmp.nVibSweep = sampleHeader.vibSweep; + mptSmp.nVibDepth = (sampleHeader.vibDepth + 3u) / 4u; + mptSmp.nVibRate = sampleHeader.vibSpeed; + // Convert to IT-like vibrato sweep + if(mptSmp.nVibSweep != 0) + mptSmp.nVibSweep = mpt::saturate_cast<decltype(mptSmp.nVibSweep)>(Util::muldivr_unsigned(mptSmp.nVibDepth, 256, mptSmp.nVibSweep)); + else + mptSmp.nVibSweep = 255; + if(sampleHeader.panEnvFlags & 0x40) + mptSmp.uFlags.set(CHN_PANNING); + } + } + } + + // Read pattern tracks + std::vector<FileReader> tracks; + if((loadFlags & loadPatternData) && (chunk = chunks.GetChunk(MDLChunk::idTracks)).IsValid()) + { + uint32 numTracks = chunk.ReadUint16LE(); + tracks.resize(numTracks + 1); + for(uint32 i = 1; i <= numTracks; i++) + { + tracks[i] = chunk.ReadChunk(chunk.ReadUint16LE()); + } + } + + // Read actual patterns + if((loadFlags & loadPatternData) && (chunk = chunks.GetChunk(MDLChunk::idPats)).IsValid()) + { + PATTERNINDEX numPats = chunk.ReadUint8(); + + // In case any muted channels contain data, be sure that we import them as well. + for(PATTERNINDEX pat = 0; pat < numPats; pat++) + { + CHANNELINDEX numChans = 32; + if(fileHeader.version >= 0x10) + { + MDLPatternHeader patHead; + chunk.ReadStruct(patHead); + if(patHead.channels > m_nChannels && patHead.channels <= 32) + m_nChannels = patHead.channels; + numChans = patHead.channels; + } + for(CHANNELINDEX chn = 0; chn < numChans; chn++) + { + if(chunk.ReadUint16LE() > 0 && chn >= m_nChannels && chn < 32) + m_nChannels = chn + 1; + } + } + chunk.Seek(1); + + Patterns.ResizeArray(numPats); + for(PATTERNINDEX pat = 0; pat < numPats; pat++) + { + CHANNELINDEX numChans = 32; + ROWINDEX numRows = 64; + std::string name; + if(fileHeader.version >= 0x10) + { + MDLPatternHeader patHead; + chunk.ReadStruct(patHead); + numChans = patHead.channels; + numRows = patHead.lastRow + 1; + name = mpt::String::ReadBuf(mpt::String::spacePadded, patHead.name); + } + + if(!Patterns.Insert(pat, numRows)) + { + chunk.Skip(2 * numChans); + continue; + } + Patterns[pat].SetName(name); + + for(CHANNELINDEX chn = 0; chn < numChans; chn++) + { + uint16 trkNum = chunk.ReadUint16LE(); + if(!trkNum || trkNum >= tracks.size() || chn >= m_nChannels) + continue; + + FileReader &track = tracks[trkNum]; + track.Rewind(); + ROWINDEX row = 0; + while(row < numRows && track.CanRead(1)) + { + ModCommand *m = Patterns[pat].GetpModCommand(row, chn); + uint8 b = track.ReadUint8(); + uint8 x = (b >> 2), y = (b & 3); + switch(y) + { + case 0: + // (x + 1) empty notes follow + row += x + 1; + break; + case 1: + // Repeat previous note (x + 1) times + if(row > 0) + { + ModCommand &orig = *Patterns[pat].GetpModCommand(row - 1, chn); + do + { + *m = orig; + m += m_nChannels; + row++; + } while (row < numRows && x--); + } + break; + case 2: + // Copy note from row x + if(row > x) + { + *m = *Patterns[pat].GetpModCommand(x, chn); + } + row++; + break; + case 3: + // New note data + if(x & MDLNOTE_NOTE) + { + b = track.ReadUint8(); + m->note = (b > 120) ? static_cast<ModCommand::NOTE>(NOTE_KEYOFF) : static_cast<ModCommand::NOTE>(b); + } + if(x & MDLNOTE_SAMPLE) + { + m->instr = track.ReadUint8(); + } + { + uint8 vol = 0, e1 = 0, e2 = 0, p1 = 0, p2 = 0; + if(x & MDLNOTE_VOLUME) + { + vol = track.ReadUint8(); + } + if(x & MDLNOTE_EFFECTS) + { + b = track.ReadUint8(); + e1 = (b & 0x0F); + e2 = (b >> 4); + } + if(x & MDLNOTE_PARAM1) + p1 = track.ReadUint8(); + if(x & MDLNOTE_PARAM2) + p2 = track.ReadUint8(); + ImportMDLCommands(*m, vol, e1, e2, p1, p2); + } + + row++; + break; + } + } + } + } + } + + if((loadFlags & loadPatternData) && (chunk = chunks.GetChunk(MDLChunk::idPatNames)).IsValid()) + { + PATTERNINDEX i = 0; + while(i < Patterns.Size() && chunk.CanRead(16)) + { + char name[17]; + chunk.ReadString<mpt::String::spacePadded>(name, 16); + Patterns[i].SetName(name); + } + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_med.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_med.cpp new file mode 100644 index 00000000..1c4a30d8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_med.cpp @@ -0,0 +1,1443 @@ +/* + * Load_med.cpp + * ------------ + * Purpose: OctaMED / MED Soundstudio module loader + * Notes : Support for synthesized instruments is still missing. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "Loaders.h" + +#ifdef MPT_WITH_VST +#include "../mptrack/Vstplug.h" +#include "plugins/PluginManager.h" +#endif // MPT_WITH_VST +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_span.hpp" +#include "mpt/io/io_stdstream.hpp" + +#include <map> + +OPENMPT_NAMESPACE_BEGIN + +struct MMD0FileHeader +{ + char mmd[3]; // "MMD" for the first song in file, "MCN" for the rest + uint8be version; // '0'-'3' + uint32be modLength; // Size of file + uint32be songOffset; // Position in file for the first song + uint16be playerSettings1[2]; // Internal variables for the play routine + uint32be blockArrOffset; // Position in file for blocks (patterns) + uint8be flags; + uint8be reserved1[3]; + uint32be sampleArrOffset; // Position in file for samples (should be identical between songs) + uint32be reserved2; + uint32be expDataOffset; // Absolute offset in file for ExpData (0 if not present) + uint32be reserved3; + char playerSettings2[11]; // Internal variables for the play routine + uint8be extraSongs; // Number of songs - 1 +}; + +MPT_BINARY_STRUCT(MMD0FileHeader, 52) + + +struct MMD0Sample +{ + uint16be loopStart; + uint16be loopLength; + uint8be midiChannel; + uint8be midiPreset; + uint8be sampleVolume; + int8be sampleTranspose; +}; + +MPT_BINARY_STRUCT(MMD0Sample, 8) + + +// Song header for MMD0/MMD1 +struct MMD0Song +{ + uint8be sequence[256]; +}; + +MPT_BINARY_STRUCT(MMD0Song, 256) + + +// Song header for MMD2/MMD3 +struct MMD2Song +{ + enum Flags3 + { + FLAG3_STEREO = 0x01, // Mixing in stereo + FLAG3_FREEPAN = 0x02, // Mixing flag: free pan + }; + + uint32be playSeqTableOffset; + uint32be sectionTableOffset; + uint32be trackVolsOffset; + uint16be numTracks; + uint16be numPlaySeqs; + uint32be trackPanOffset; // 0: all centered (according to docs, MED Soundstudio uses Amiga hard-panning instead) + uint32be flags3; + uint16be volAdjust; // Volume adjust (%) + uint16be mixChannels; // Mixing channels, 0 means 4 + uint8be mixEchoType; // 0 = nothing, 1 = normal, 2 = cross + uint8be mixEchoDepth; // 1 - 6, 0 = default + uint16be mixEchoLength; // Echo length in milliseconds + int8be mixStereoSep; // Stereo separation + char pad0[223]; +}; + +MPT_BINARY_STRUCT(MMD2Song, 256) + + +// Common song header +struct MMDSong +{ + enum Flags + { + FLAG_FILTERON = 0x01, // The hardware audio filter is on + FLAG_JUMPINGON = 0x02, // Mouse pointer jumping on + FLAG_JUMP8TH = 0x04, // ump every 8th line (not in OctaMED Pro) + FLAG_INSTRSATT = 0x08, // sng+samples indicator (not useful in MMDs) + FLAG_VOLHEX = 0x10, // volumes are HEX + FLAG_STSLIDE = 0x20, // use ST/NT/PT compatible sliding + FLAG_8CHANNEL = 0x40, // this is OctaMED 5-8 channel song + FLAG_SLOWHQ = 0x80, // HQ V2-4 compatibility mode + }; + + enum Flags2 + { + FLAG2_BMASK = 0x1F, // (bits 0-4) BPM beat length (in lines) + FLAG2_BPM = 0x20, // BPM mode on + FLAG2_MIX = 0x80, // Module uses mixing + }; + + uint16be numBlocks; // Number of blocks in current song + uint16be songLength; // MMD0: Number of sequence numbers in the play sequence list, MMD2: Number of sections + char song[256]; + MMD0Song GetMMD0Song() const + { + static_assert(sizeof(MMD0Song) == sizeof(song)); + return mpt::bit_cast<MMD0Song>(song); + } + MMD2Song GetMMD2Song() const + { + static_assert(sizeof(MMD2Song) == sizeof(song)); + return mpt::bit_cast<MMD2Song>(song); + } + uint16be defaultTempo; + int8be playTranspose; // The global play transpose value for current song + uint8be flags; + uint8be flags2; + uint8be tempo2; // Timing pulses per line (ticks) + uint8be trackVol[16]; // 1...64 in MMD0/MMD1, reserved in MMD2 + uint8be masterVol; // 1...64 + uint8be numSamples; +}; + +MPT_BINARY_STRUCT(MMDSong, 284) + + +struct MMD2PlaySeq +{ + char name[32]; + uint32be commandTableOffset; + uint32be reserved; + uint16be length; // Number of entries +}; + +MPT_BINARY_STRUCT(MMD2PlaySeq, 42) + + +struct MMD0PatternHeader +{ + uint8be numTracks; + uint8be numRows; +}; + +MPT_BINARY_STRUCT(MMD0PatternHeader, 2) + + +struct MMD1PatternHeader +{ + uint16be numTracks; + uint16be numRows; + uint32be blockInfoOffset; +}; + +MPT_BINARY_STRUCT(MMD1PatternHeader, 8) + + +struct MMDPlaySeqCommand +{ + enum Command + { + kStop = 1, + kJump = 2, + }; + + uint16be offset; // Offset within current play sequence, 0xFFFF = end of list + uint8be command; // Stop = 1, Jump = 2 + uint8be extraSize; +}; + +MPT_BINARY_STRUCT(MMDPlaySeqCommand, 4) + + +struct MMDBlockInfo +{ + uint32be highlightMaskOffset; + uint32be nameOffset; + uint32be nameLength; + uint32be pageTableOffset; // File offset of command page table + uint32be cmdExtTableOffset; // File offset of command extension table (second parameter) + uint32be reserved[4]; +}; + +MPT_BINARY_STRUCT(MMDBlockInfo, 36) + + +struct MMDInstrHeader +{ + enum Types + { + VSTI = -4, + HIGHLIFE = -3, + HYBRID = -2, + SYNTHETIC = -1, + SAMPLE = 0, // an ordinary 1-octave sample (or MIDI) + IFF5OCT = 1, // 5 octaves + IFF3OCT = 2, // 3 octaves + // The following ones are recognized by OctaMED Pro only + IFF2OCT = 3, // 2 octaves + IFF4OCT = 4, // 4 octaves + IFF6OCT = 5, // 6 octaves + IFF7OCT = 6, // 7 octaves + // OctaMED Pro V5 + later + EXTSAMPLE = 7, // two extra-low octaves + + TYPEMASK = 0x0F, + + S_16 = 0x10, + STEREO = 0x20, + DELTA = 0x40, + PACKED = 0x80, // MMDPackedSampleHeader follows + OBSOLETE_MD16 = 0x18, + }; + + uint32be length; + int16be type; +}; + +MPT_BINARY_STRUCT(MMDInstrHeader, 6) + + +struct MMDPackedSampleHeader +{ + uint16be packType; // Only 1 = ADPCM is supported + uint16be subType; // Packing subtype + // ADPCM subtype + // 1: g723_40 + // 2: g721 + // 3: g723_24 + uint8be commonFlags; // flags common to all packtypes (none defined so far) + uint8be packerFlags; // flags for the specific packtype + uint32be leftChLen; // packed length of left channel in bytes + uint32be rightChLen; // packed length of right channel in bytes (ONLY PRESENT IN STEREO SAMPLES) +}; + +MPT_BINARY_STRUCT(MMDPackedSampleHeader, 14) + + +struct MMDInstrExt +{ + enum + { + SSFLG_LOOP = 0x01, // Loop On / Off + SSFLG_EXTPSET = 0x02, // Ext.Preset + SSFLG_DISABLED = 0x04, // Disabled + SSFLG_PINGPONG = 0x08, // Ping-pong looping + }; + + uint8be hold; // 0...127 + uint8be decay; // 0...127 + uint8be suppressMidiOff; + int8be finetune; + // Below fields saved by >= V5 + uint8be defaultPitch; + uint8be instrFlags; + uint16be longMidiPreset; // Legacy MIDI program mode that doesn't use banks but a combination of two program change commands + // Below fields saved by >= V5.02 + uint8be outputDevice; + uint8be reserved; + // Below fields saved by >= V7 + uint32be loopStart; + uint32be loopLength; + // Not sure which version starts saving those but they are saved by MED Soundstudio for Windows + uint8 volume; // 0...127 + uint8 outputPort; // Index into user-configurable device list (NOT WinAPI port index) + uint16le midiBank; +}; + +MPT_BINARY_STRUCT(MMDInstrExt, 22) + + +struct MMDInstrInfo +{ + char name[40]; +}; + +MPT_BINARY_STRUCT(MMDInstrInfo, 40) + + +struct MMD0Exp +{ + uint32be nextModOffset; + uint32be instrExtOffset; + uint16be instrExtEntries; + uint16be instrExtEntrySize; + uint32be annoText; + uint32be annoLength; + uint32be instrInfoOffset; + uint16be instrInfoEntries; + uint16be instrInfoEntrySize; + uint32be jumpMask; + uint32be rgbTable; + uint8be channelSplit[4]; + uint32be notationInfoOffset; + uint32be songNameOffset; + uint32be songNameLength; + uint32be midiDumpOffset; + uint32be mmdInfoOffset; + uint32be arexxOffset; + uint32be midiCmd3xOffset; + uint32be trackInfoOffset; // Pointer to song->numtracks pointers to tag lists + uint32be effectInfoOffset; // Pointers to group pointers + uint32be tagEnd; +}; + +MPT_BINARY_STRUCT(MMD0Exp, 80) + + +struct MMDTag +{ + enum TagType + { + // Generic MMD tags + MMDTAG_END = 0x00000000, + MMDTAG_PTR = 0x80000000, // Data needs relocation + MMDTAG_MUSTKNOW = 0x40000000, // Loader must fail if this isn't recognized + MMDTAG_MUSTWARN = 0x20000000, // Loader must warn if this isn't recognized + MMDTAG_MASK = 0x1FFFFFFF, + + // ExpData tags + // # of effect groups, including the global group (will override settings in MMDSong struct), default = 1 + MMDTAG_EXP_NUMFXGROUPS = 1, + MMDTAG_TRK_FXGROUP = 3, + + MMDTAG_TRK_NAME = 1, // trackinfo tags + MMDTAG_TRK_NAMELEN = 2, // namelen includes zero term. + + // effectinfo tags + MMDTAG_FX_ECHOTYPE = 1, + MMDTAG_FX_ECHOLEN = 2, + MMDTAG_FX_ECHODEPTH = 3, + MMDTAG_FX_STEREOSEP = 4, + MMDTAG_FX_GROUPNAME = 5, // the Global Effects group shouldn't have name saved! + MMDTAG_FX_GRPNAMELEN = 6, // namelen includes zero term. + }; + + uint32be type; + uint32be data; +}; + +MPT_BINARY_STRUCT(MMDTag, 8) + + +struct MMDDump +{ + uint32be length; + uint32be dataPointer; + uint16be extLength; // If >= 20: name follows as char[20] +}; + +MPT_BINARY_STRUCT(MMDDump, 10) + + +static TEMPO MMDTempoToBPM(uint32 tempo, bool is8Ch, bool bpmMode, uint8 rowsPerBeat) +{ + if(bpmMode && !is8Ch) + { + // You would have thought that we could use modern tempo mode here. + // Alas, the number of ticks per row still influences the tempo. :( + return TEMPO((tempo * rowsPerBeat) / 4.0); + } + if(is8Ch && tempo > 0) + { + LimitMax(tempo, 10u); + // 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) + { + // SoundTracker compatible tempo + return TEMPO((6.0 * 1773447.0 / 14500.0) / tempo); + } + + return TEMPO(tempo / 0.264); +} + + +static void ConvertMEDEffect(ModCommand &m, bool is8ch, bool bpmMode, uint8 rowsPerBeat, bool volHex) +{ + switch(m.command) + { + case 0x04: // Vibrato (twice as deep as in ProTracker) + m.command = CMD_VIBRATO; + m.param = (std::min<uint8>(m.param >> 3, 0x0F) << 4) | std::min<uint8>((m.param & 0x0F) * 2, 0x0F); + break; + case 0x08: // Hold and decay + m.command = CMD_NONE; + break; + case 0x09: // Set secondary speed + if(m.param > 0 && m.param <= 20) + m.command = CMD_SPEED; + else + m.command = CMD_NONE; + break; + case 0x0C: // Set Volume + m.command = CMD_VOLUME; + if(!volHex && m.param < 0x99) + m.param = (m.param >> 4) * 10 + (m.param & 0x0F); + else if(volHex) + m.param = ((m.param & 0x7F) + 1) / 2; + else + m.command = CMD_NONE; + break; + case 0x0D: + m.command = CMD_VOLUMESLIDE; + break; + case 0x0E: // Synth jump + m.command = CMD_NONE; + break; + case 0x0F: // Misc + if(m.param == 0) + { + m.command = CMD_PATTERNBREAK; + } else if(m.param <= 0xF0) + { + m.command = CMD_TEMPO; + if(m.param < 0x03) // This appears to be a bug in OctaMED which is not emulated in MED Soundstudio on Windows. + m.param = 0x70; + else + m.param = mpt::saturate_round<ModCommand::PARAM>(MMDTempoToBPM(m.param, is8ch, bpmMode, rowsPerBeat).ToDouble()); +#ifdef MODPLUG_TRACKER + if(m.param < 0x20) + m.param = 0x20; +#endif // MODPLUG_TRACKER + } else switch(m.command) + { + case 0xF1: // Play note twice + m.command = CMD_MODCMDEX; + m.param = 0x93; + break; + case 0xF2: // Delay note + m.command = CMD_MODCMDEX; + m.param = 0xD3; + break; + case 0xF3: // Play note three times + m.command = CMD_MODCMDEX; + m.param = 0x92; + break; + case 0xF8: // Turn filter off + case 0xF9: // Turn filter on + m.command = CMD_MODCMDEX; + m.param = 0xF9 - m.param; + break; + case 0xFA: // MIDI pedal on + case 0xFB: // MIDI pedal off + case 0xFD: // Set pitch + case 0xFE: // End of song + m.command = CMD_NONE; + break; + case 0xFF: // Turn note off + m.note = NOTE_NOTECUT; + m.command = CMD_NONE; + break; + default: + m.command = CMD_NONE; + break; + } + break; + case 0x10: // MIDI message + m.command = CMD_MIDI; + m.param |= 0x80; + break; + case 0x11: // Slide pitch up + m.command = CMD_MODCMDEX; + m.param = 0x10 | std::min<uint8>(m.param, 0x0F); + break; + case 0x12: // Slide pitch down + m.command = CMD_MODCMDEX; + m.param = 0x20 | std::min<uint8>(m.param, 0x0F); + break; + case 0x14: // Vibrato (ProTracker compatible depth, but faster) + m.command = CMD_VIBRATO; + m.param = (std::min<uint8>((m.param >> 4) + 1, 0x0F) << 4) | (m.param & 0x0F); + break; + case 0x15: // Set finetune + m.command = CMD_MODCMDEX; + m.param = 0x50 | (m.param & 0x0F); + break; + case 0x16: // Loop + m.command = CMD_MODCMDEX; + m.param = 0x60 | std::min<uint8>(m.param, 0x0F); + break; + case 0x18: // Stop note + m.command = CMD_MODCMDEX; + m.param = 0xC0 | std::min<uint8>(m.param, 0x0F); + break; + case 0x19: // Sample Offset + m.command = CMD_OFFSET; + break; + case 0x1A: // Slide volume up once + m.command = CMD_MODCMDEX; + m.param = 0xA0 | std::min<uint8>(m.param, 0x0F); + break; + case 0x1B: // Slide volume down once + m.command = CMD_MODCMDEX; + m.param = 0xB0 | std::min<uint8>(m.param, 0x0F); + break; + case 0x1C: // MIDI program + if(m.param > 0 && m.param <= 128) + { + m.command = CMD_MIDI; + m.param--; + } else + { + m.command = CMD_NONE; + } + break; + case 0x1D: // Pattern break (in hex) + m.command = CMD_PATTERNBREAK; + break; + case 0x1E: // Repeat row + m.command = CMD_MODCMDEX; + m.param = 0xE0 | std::min<uint8>(m.param, 0x0F); + break; + case 0x1F: // Note delay and retrigger + { + if(m.param & 0xF0) + { + m.command = CMD_MODCMDEX; + m.param = 0xD0 | (m.param >> 4); + } else if(m.param & 0x0F) + { + m.command = CMD_MODCMDEX; + m.param = 0x90 | m.param; + } else + { + m.command = CMD_NONE; + } + break; + } + case 0x20: // Reverse sample + skip samples + if(m.param == 0 && m.vol == 0) + { + if(m.IsNote()) + { + m.command = CMD_S3MCMDEX; + m.param = 0x9F; + } + } else + { + // Skip given number of samples + m.command = CMD_NONE; + } + break; + case 0x29: // Relative sample offset + if(m.vol > 0) + { + m.command = CMD_OFFSETPERCENTAGE; + m.param = mpt::saturate_cast<ModCommand::PARAM>(Util::muldiv_unsigned(m.param, 0x100, m.vol)); + } else + { + m.command = CMD_NONE; + } + break; + case 0x2E: // Set panning + if(m.param <= 0x10 || m.param >= 0xF0) + { + m.command = CMD_PANNING8; + m.param = mpt::saturate_cast<ModCommand::PARAM>(((m.param ^ 0x80) - 0x70) * 8); + } else + { + m.command = CMD_NONE; + } + break; + default: + if(m.command < 0x10) + CSoundFile::ConvertModCommand(m); + else + m.command = CMD_NONE; + break; + } +} + +#ifdef MPT_WITH_VST +static std::wstring ReadMEDStringUTF16BE(FileReader &file) +{ + FileReader chunk = file.ReadChunk(file.ReadUint32BE()); + std::wstring s(chunk.GetLength() / 2u, L'\0'); + for(auto &c : s) + { + c = chunk.ReadUint16BE(); + } + return s; +} +#endif // MPT_WITH_VST + + +static void MEDReadNextSong(FileReader &file, MMD0FileHeader &fileHeader, MMD0Exp &expData, MMDSong &songHeader) +{ + file.ReadStruct(fileHeader); + file.Seek(fileHeader.songOffset + 63 * sizeof(MMD0Sample)); + file.ReadStruct(songHeader); + if(fileHeader.expDataOffset && file.Seek(fileHeader.expDataOffset)) + file.ReadStruct(expData); + else + expData = {}; +} + + +static std::pair<CHANNELINDEX, SEQUENCEINDEX> MEDScanNumChannels(FileReader &file, const uint8 version) +{ + MMD0FileHeader fileHeader; + MMD0Exp expData; + MMDSong songHeader; + + file.Rewind(); + uint32 songOffset = 0; + MEDReadNextSong(file, fileHeader, expData, songHeader); + + SEQUENCEINDEX numSongs = std::min(MAX_SEQUENCES, mpt::saturate_cast<SEQUENCEINDEX>(fileHeader.expDataOffset ? fileHeader.extraSongs + 1 : 1)); + CHANNELINDEX numChannels = 4; + // Scan patterns for max number of channels + for(SEQUENCEINDEX song = 0; song < numSongs; song++) + { + const PATTERNINDEX numPatterns = songHeader.numBlocks; + if(songHeader.numSamples > 63 || numPatterns > 0x7FFF) + return {}; + + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + if(!file.Seek(fileHeader.blockArrOffset + pat * 4u) + || !file.Seek(file.ReadUint32BE())) + { + continue; + } + numChannels = std::max(numChannels, static_cast<CHANNELINDEX>(version < 1 ? file.ReadUint8() : file.ReadUint16BE())); + } + + // If song offsets are going backwards, reject the file + if(expData.nextModOffset <= songOffset || !file.Seek(expData.nextModOffset)) + { + numSongs = song + 1; + break; + } + songOffset = expData.nextModOffset; + MEDReadNextSong(file, fileHeader, expData, songHeader); + } + return {numChannels, numSongs}; +} + + +static bool ValidateHeader(const MMD0FileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.mmd, "MMD", 3) + || fileHeader.version < '0' || fileHeader.version > '3' + || fileHeader.songOffset < sizeof(MMD0FileHeader) + || fileHeader.songOffset > uint32_max - 63 * sizeof(MMD0Sample) - sizeof(MMDSong) + || fileHeader.blockArrOffset < sizeof(MMD0FileHeader) + || (fileHeader.sampleArrOffset > 0 && fileHeader.sampleArrOffset < sizeof(MMD0FileHeader)) + || fileHeader.expDataOffset > uint32_max - sizeof(MMD0Exp)) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const MMD0FileHeader &fileHeader) +{ + return std::max<uint64>({ fileHeader.songOffset + 63 * sizeof(MMD0Sample) + sizeof(MMDSong), + fileHeader.blockArrOffset, + fileHeader.sampleArrOffset ? fileHeader.sampleArrOffset : sizeof(MMD0FileHeader), + fileHeader.expDataOffset + sizeof(MMD0Exp) }) - sizeof(MMD0FileHeader); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMED(MemoryFileReader file, const uint64 *pfilesize) +{ + MMD0FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return ProbeWantMoreData; + if(!ValidateHeader(fileHeader)) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadMED(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + MMD0FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return false; + if(!ValidateHeader(fileHeader)) + return false; + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_MED); + InitializeChannels(); + const uint8 version = fileHeader.version - '0'; + + file.Seek(fileHeader.songOffset); + FileReader sampleHeaderChunk = file.ReadChunk(63 * sizeof(MMD0Sample)); + + MMDSong songHeader; + file.ReadStruct(songHeader); + + if(songHeader.numSamples > 63 || songHeader.numBlocks > 0x7FFF) + return false; + + MMD0Exp expData{}; + if(fileHeader.expDataOffset && file.Seek(fileHeader.expDataOffset)) + { + file.ReadStruct(expData); + } + + const auto [numChannels, numSongs] = MEDScanNumChannels(file, version); + if(numChannels < 1 || numChannels > MAX_BASECHANNELS) + return false; + m_nChannels = numChannels; + + // Start with the instruments, as those are shared between songs + + std::vector<uint32be> instrOffsets; + if(fileHeader.sampleArrOffset) + { + file.Seek(fileHeader.sampleArrOffset); + file.ReadVector(instrOffsets, songHeader.numSamples); + } else if(songHeader.numSamples > 0) + { + return false; + } + m_nInstruments = m_nSamples = songHeader.numSamples; + + // In MMD0 / MMD1, octave wrapping is not done for synth instruments + // - It's required e.g. for automatic terminated to.mmd0 and you got to let the music.mmd1 + // - starkelsesirap.mmd0 (synth instruments) on the other hand don't need it + // In MMD2 / MMD3, the mix flag is used instead. + const bool hardwareMixSamples = (version < 2) || (version >= 2 && !(songHeader.flags2 & MMDSong::FLAG2_MIX)); + + bool needInstruments = false; + bool anySynthInstrs = false; +#ifdef MPT_WITH_VST + PLUGINDEX numPlugins = 0; +#endif // MPT_WITH_VST + for(SAMPLEINDEX ins = 1, smp = 1; ins <= m_nInstruments; ins++) + { + if(!AllocateInstrument(ins, smp)) + return false; + ModInstrument &instr = *Instruments[ins]; + + MMDInstrHeader instrHeader{}; + FileReader sampleChunk; + if(instrOffsets[ins - 1] != 0 && file.Seek(instrOffsets[ins - 1])) + { + file.ReadStruct(instrHeader); + uint32 chunkLength = instrHeader.length; + if(instrHeader.type > 0 && (instrHeader.type & MMDInstrHeader::STEREO)) + chunkLength *= 2u; + sampleChunk = file.ReadChunk(chunkLength); + } + const bool isSynth = instrHeader.type < 0; + const size_t maskedType = static_cast<size_t>(instrHeader.type & MMDInstrHeader::TYPEMASK); + +#ifdef MPT_WITH_VST + if(instrHeader.type == MMDInstrHeader::VSTI) + { + needInstruments = true; + sampleChunk.Skip(6); // 00 00 <size of following data> + const std::wstring type = ReadMEDStringUTF16BE(sampleChunk); + const std::wstring name = ReadMEDStringUTF16BE(sampleChunk); + if(type == L"VST") + { + auto &mixPlug = m_MixPlugins[numPlugins]; + mixPlug = {}; + mixPlug.Info.dwPluginId1 = Vst::kEffectMagic; + mixPlug.Info.gain = 10; + mixPlug.Info.szName = mpt::ToCharset(mpt::Charset::Locale, name); + mixPlug.Info.szLibraryName = mpt::ToCharset(mpt::Charset::UTF8, name); + instr.nMixPlug = numPlugins + 1; + instr.nMidiChannel = MidiFirstChannel; + instr.Transpose(-24); + instr.AssignSample(0); + // TODO: Figure out patch and routing data + + numPlugins++; + } + } else +#endif // MPT_WITH_VST + if(isSynth) + { + // TODO: Figure out synth instruments + anySynthInstrs = true; + instr.AssignSample(0); + } + + uint8 numSamples = 1; + static constexpr uint8 SamplesPerType[] = {1, 5, 3, 2, 4, 6, 7}; + if(!isSynth && maskedType < std::size(SamplesPerType)) + numSamples = SamplesPerType[maskedType]; + if(numSamples > 1) + { + static_assert(MAX_SAMPLES > 63 * 9, "Check IFFOCT multisample code"); + m_nSamples += numSamples - 1; + needInstruments = true; + static constexpr uint8 OctSampleMap[][8] = + { + {1, 1, 0, 0, 0, 0, 0, 0}, // 2 + {2, 2, 1, 1, 0, 0, 0, 0}, // 3 + {3, 3, 2, 2, 1, 0, 0, 0}, // 4 + {4, 3, 2, 1, 1, 0, 0, 0}, // 5 + {5, 4, 3, 2, 1, 0, 0, 0}, // 6 + {6, 5, 4, 3, 2, 1, 0, 0}, // 7 + }; + + static constexpr int8 OctTransposeMap[][8] = + { + { 0, 0, -12, -12, -24, -36, -48, -60}, // 2 + { 0, 0, -12, -12, -24, -36, -48, -60}, // 3 + { 0, 0, -12, -12, -24, -36, -48, -60}, // 4 + {12, 0, -12, -24, -24, -36, -48, -60}, // 5 + {12, 0, -12, -24, -36, -48, -48, -60}, // 6 + {12, 0, -12, -24, -36, -48, -60, -72}, // 7 + }; + + // TODO: Move octaves so that they align better (C-4 = lowest, we don't have access to the highest four octaves) + for(int octave = 4; octave < 10; octave++) + { + for(int note = 0; note < 12; note++) + { + instr.Keyboard[12 * octave + note] = smp + OctSampleMap[numSamples - 2][octave - 4]; + instr.NoteMap[12 * octave + note] += OctTransposeMap[numSamples - 2][octave - 4]; + } + } + } else if(maskedType == MMDInstrHeader::EXTSAMPLE) + { + needInstruments = true; + instr.Transpose(-24); + } else if(!isSynth && hardwareMixSamples) + { + for(int octave = 7; octave < 10; octave++) + { + for(int note = 0; note < 12; note++) + { + instr.NoteMap[12 * octave + note] -= static_cast<uint8>((octave - 6) * 12); + } + } + } + + MMD0Sample sampleHeader; + sampleHeaderChunk.ReadStruct(sampleHeader); + + // midiChannel = 0xFF == midi instrument but with invalid channel, midiChannel = 0x00 == sample-based instrument? + if(sampleHeader.midiChannel > 0 && sampleHeader.midiChannel <= 16) + { + instr.nMidiChannel = sampleHeader.midiChannel - 1 + MidiFirstChannel; + needInstruments = true; + +#ifdef MPT_WITH_VST + if(!isSynth) + { + auto &mixPlug = m_MixPlugins[numPlugins]; + mixPlug = {}; + mixPlug.Info.dwPluginId1 = PLUGMAGIC('V', 's', 't', 'P'); + mixPlug.Info.dwPluginId2 = PLUGMAGIC('M', 'M', 'I', 'D'); + mixPlug.Info.gain = 10; + mixPlug.Info.szName = "MIDI Input Output"; + mixPlug.Info.szLibraryName = "MIDI Input Output"; + + instr.nMixPlug = numPlugins + 1; + instr.Transpose(-24); + + numPlugins++; + } +#endif // MPT_WITH_VST + } + if(sampleHeader.midiPreset > 0 && sampleHeader.midiPreset <= 128) + { + instr.nMidiProgram = sampleHeader.midiPreset; + } + + for(SAMPLEINDEX i = 0; i < numSamples; i++) + { + ModSample &mptSmp = Samples[smp + i]; + mptSmp.Initialize(MOD_TYPE_MED); + mptSmp.nVolume = 4u * std::min<uint8>(sampleHeader.sampleVolume, 64u); + mptSmp.RelativeTone = sampleHeader.sampleTranspose; + } + + if(isSynth || !(loadFlags & loadSampleData)) + { + smp += numSamples; + continue; + } + + SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM); + + const bool hasLoop = sampleHeader.loopLength > 1; + SmpLength loopStart = sampleHeader.loopStart * 2; + SmpLength loopEnd = loopStart + sampleHeader.loopLength * 2; + + SmpLength length = mpt::saturate_cast<SmpLength>(sampleChunk.GetLength()); + if(instrHeader.type & MMDInstrHeader::S_16) + { + sampleIO |= SampleIO::_16bit; + length /= 2; + } + if(instrHeader.type & MMDInstrHeader::STEREO) + { + sampleIO |= SampleIO::stereoSplit; + length /= 2; + } + if(instrHeader.type & MMDInstrHeader::DELTA) + { + sampleIO |= SampleIO::deltaPCM; + } + + if(numSamples > 1) + length = length / ((1u << numSamples) - 1); + + for(SAMPLEINDEX i = 0; i < numSamples; i++) + { + ModSample &mptSmp = Samples[smp + i]; + + mptSmp.nLength = length; + sampleIO.ReadSample(mptSmp, sampleChunk); + + if(hasLoop) + { + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.uFlags.set(CHN_LOOP); + } + + length *= 2; + loopStart *= 2; + loopEnd *= 2; + } + + smp += numSamples; + } + + if(expData.instrExtOffset != 0 && expData.instrExtEntries != 0 && file.Seek(expData.instrExtOffset)) + { + const uint16 entries = std::min<uint16>(expData.instrExtEntries, songHeader.numSamples); + const uint16 size = expData.instrExtEntrySize; + for(uint16 i = 0; i < entries; i++) + { + MMDInstrExt instrExt; + file.ReadStructPartial(instrExt, size); + + ModInstrument &ins = *Instruments[i + 1]; + if(instrExt.hold) + { + ins.VolEnv.assign({ + EnvelopeNode{0u, ENVELOPE_MAX}, + EnvelopeNode{static_cast<EnvelopeNode::tick_t>(instrExt.hold - 1), ENVELOPE_MAX}, + EnvelopeNode{static_cast<EnvelopeNode::tick_t>(instrExt.hold + (instrExt.decay ? 64u / instrExt.decay : 0u)), ENVELOPE_MIN}, + }); + if(instrExt.hold == 1) + ins.VolEnv.erase(ins.VolEnv.begin()); + ins.nFadeOut = instrExt.decay ? (instrExt.decay * 512) : 32767; + ins.VolEnv.dwFlags.set(ENV_ENABLED); + needInstruments = true; + } + if(size > offsetof(MMDInstrExt, volume)) + ins.nGlobalVol = (instrExt.volume + 1u) / 2u; + if(size > offsetof(MMDInstrExt, midiBank)) + ins.wMidiBank = instrExt.midiBank; +#ifdef MPT_WITH_VST + if(ins.nMixPlug > 0) + { + PLUGINDEX plug = ins.nMixPlug - 1; + auto &mixPlug = m_MixPlugins[plug]; + if(mixPlug.Info.dwPluginId2 == PLUGMAGIC('M', 'M', 'I', 'D')) + { + float dev = (instrExt.outputDevice + 1) / 65536.0f; // Magic code from MidiInOut.h :( + mixPlug.pluginData.resize(3 * sizeof(uint32)); + auto memFile = std::make_pair(mpt::as_span(mixPlug.pluginData), mpt::IO::Offset(0)); + mpt::IO::WriteIntLE<uint32>(memFile, 0); // Plugin data type + mpt::IO::Write(memFile, IEEE754binary32LE{0}); // Input device + mpt::IO::Write(memFile, IEEE754binary32LE{dev}); // Output device + + // Check if we already have another plugin referencing this output device + for(PLUGINDEX p = 0; p < plug; p++) + { + const auto &otherPlug = m_MixPlugins[p]; + if(otherPlug.Info.dwPluginId1 == mixPlug.Info.dwPluginId1 + && otherPlug.Info.dwPluginId2 == mixPlug.Info.dwPluginId2 + && otherPlug.pluginData == mixPlug.pluginData) + { + ins.nMixPlug = p + 1; + mixPlug = {}; + break; + } + } + } + } +#endif // MPT_WITH_VST + + ModSample &sample = Samples[ins.Keyboard[NOTE_MIDDLEC]]; + sample.nFineTune = MOD2XMFineTune(instrExt.finetune); + + if(size > offsetof(MMDInstrExt, loopLength)) + { + sample.nLoopStart = instrExt.loopStart; + sample.nLoopEnd = instrExt.loopStart + instrExt.loopLength; + } + if(size > offsetof(MMDInstrExt, instrFlags)) + { + sample.uFlags.set(CHN_LOOP, (instrExt.instrFlags & MMDInstrExt::SSFLG_LOOP) != 0); + sample.uFlags.set(CHN_PINGPONGLOOP, (instrExt.instrFlags & MMDInstrExt::SSFLG_PINGPONG) != 0); + if(instrExt.instrFlags & MMDInstrExt::SSFLG_DISABLED) + sample.nGlobalVol = 0; + } + } + } + if(expData.instrInfoOffset != 0 && expData.instrInfoEntries != 0 && file.Seek(expData.instrInfoOffset)) + { + const uint16 entries = std::min<uint16>(expData.instrInfoEntries, songHeader.numSamples); + const uint16 size = expData.instrInfoEntrySize; + for(uint16 i = 0; i < entries; i++) + { + MMDInstrInfo instrInfo; + file.ReadStructPartial(instrInfo, size); + Instruments[i + 1]->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrInfo.name); + for(auto smp : Instruments[i + 1]->GetSamples()) + { + m_szNames[smp] = Instruments[i + 1]->name; + } + } + } + + // Setup a program change macro for command 1C (even if MIDI plugin is disabled, as otherwise these commands may act as filter commands) + m_MidiCfg.ClearZxxMacros(); + m_MidiCfg.SFx[0] = "Cc z"; + + file.Rewind(); + PATTERNINDEX basePattern = 0; + for(SEQUENCEINDEX song = 0; song < numSongs; song++) + { + MEDReadNextSong(file, fileHeader, expData, songHeader); + + if(song != 0) + { + if(Order.AddSequence() == SEQUENCEINDEX_INVALID) + return false; + } + + ModSequence &order = Order(song); + + std::map<ORDERINDEX, ORDERINDEX> jumpTargets; + order.clear(); + uint32 preamp = 32; + if(version < 2) + { + if(songHeader.songLength > 256 || m_nChannels > 16) + return false; + ReadOrderFromArray(order, songHeader.GetMMD0Song().sequence, songHeader.songLength); + for(auto &ord : order) + { + ord += basePattern; + } + + SetupMODPanning(true); + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].nVolume = std::min<uint8>(songHeader.trackVol[chn], 64); + } + } else + { + const MMD2Song header = songHeader.GetMMD2Song(); + if(header.numTracks < 1 || header.numTracks > 64 || m_nChannels > 64) + return false; + + const bool freePan = (header.flags3 & MMD2Song::FLAG3_FREEPAN); + if(header.volAdjust) + preamp = Util::muldivr_unsigned(preamp, std::min<uint16>(header.volAdjust, 800), 100); + if (freePan) + preamp /= 2; + + if(file.Seek(header.trackVolsOffset)) + { + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].nVolume = std::min<uint8>(file.ReadUint8(), 64); + } + } + if(header.trackPanOffset && file.Seek(header.trackPanOffset)) + { + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].nPan = (Clamp<int8, int8>(file.ReadInt8(), -16, 16) + 16) * 8; + } + } else + { + SetupMODPanning(true); + } + + std::vector<uint16be> sections; + if(!file.Seek(header.sectionTableOffset) + || !file.CanRead(songHeader.songLength * 2) + || !file.ReadVector(sections, songHeader.songLength)) + continue; + + for(uint16 section : sections) + { + if(section > header.numPlaySeqs) + continue; + + file.Seek(header.playSeqTableOffset + section * 4); + if(!file.Seek(file.ReadUint32BE()) || !file.CanRead(sizeof(MMD2PlaySeq))) + continue; + + MMD2PlaySeq playSeq; + file.ReadStruct(playSeq); + + if(!order.empty()) + order.push_back(order.GetIgnoreIndex()); + + size_t readOrders = playSeq.length; + if(!file.CanRead(readOrders)) + LimitMax(readOrders, file.BytesLeft()); + LimitMax(readOrders, ORDERINDEX_MAX); + + size_t orderStart = order.size(); + order.reserve(orderStart + readOrders); + for(size_t ord = 0; ord < readOrders; ord++) + { + PATTERNINDEX pat = file.ReadUint16BE(); + if(pat < 0x8000) + { + order.push_back(basePattern + pat); + } + } + if(playSeq.name[0]) + order.SetName(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, mpt::String::ReadAutoBuf(playSeq.name))); + + // Play commands (jump / stop) + if(playSeq.commandTableOffset > 0 && file.Seek(playSeq.commandTableOffset)) + { + MMDPlaySeqCommand command; + while(file.ReadStruct(command)) + { + FileReader chunk = file.ReadChunk(command.extraSize); + ORDERINDEX ord = mpt::saturate_cast<ORDERINDEX>(orderStart + command.offset); + if(command.offset == 0xFFFF || ord >= order.size()) + break; + if(command.command == MMDPlaySeqCommand::kStop) + { + order[ord] = order.GetInvalidPatIndex(); + } else if(command.command == MMDPlaySeqCommand::kJump) + { + jumpTargets[ord] = chunk.ReadUint16BE(); + order[ord] = order.GetIgnoreIndex(); + } + } + } + } + } + + 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 uint8 rowsPerBeat = 1 + (songHeader.flags2 & MMDSong::FLAG2_BMASK); + m_nDefaultTempo = MMDTempoToBPM(songHeader.defaultTempo, is8Ch, bpmMode, rowsPerBeat); + m_nDefaultSpeed = Clamp<uint8, uint8>(songHeader.tempo2, 1, 32); + if(bpmMode) + { + m_nDefaultRowsPerBeat = rowsPerBeat; + m_nDefaultRowsPerMeasure = m_nDefaultRowsPerBeat * 4u; + } + + if(songHeader.masterVol) + m_nDefaultGlobalVolume = std::min<uint8>(songHeader.masterVol, 64) * 4; + m_nSamplePreAmp = m_nVSTiVolume = preamp; + + // For MED, this affects both volume and pitch slides + m_SongFlags.set(SONG_FASTVOLSLIDES, !(songHeader.flags & MMDSong::FLAG_STSLIDE)); + + if(expData.songNameOffset && file.Seek(expData.songNameOffset)) + { + file.ReadString<mpt::String::maybeNullTerminated>(m_songName, expData.songNameLength); + if(numSongs > 1) + order.SetName(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, m_songName)); + } + if(expData.annoLength > 1 && file.Seek(expData.annoText)) + { + m_songMessage.Read(file, expData.annoLength - 1, SongMessage::leAutodetect); + } + +#ifdef MPT_WITH_VST + // Read MIDI messages + if(expData.midiDumpOffset && file.Seek(expData.midiDumpOffset) && file.CanRead(8)) + { + uint16 numDumps = std::min(file.ReadUint16BE(), static_cast<uint16>(m_MidiCfg.Zxx.size())); + file.Skip(6); + if(file.CanRead(numDumps * 4)) + { + std::vector<uint32be> dumpPointers; + file.ReadVector(dumpPointers, numDumps); + for(uint16 dump = 0; dump < numDumps; dump++) + { + if(!file.Seek(dumpPointers[dump]) || !file.CanRead(sizeof(MMDDump))) + continue; + MMDDump dumpHeader; + file.ReadStruct(dumpHeader); + if(!file.Seek(dumpHeader.dataPointer) || !file.CanRead(dumpHeader.length)) + continue; + std::array<char, kMacroLength> macro{}; + auto length = std::min(static_cast<size_t>(dumpHeader.length), macro.size() / 2u); + for(size_t i = 0; i < length; i++) + { + const uint8 byte = file.ReadUint8(), high = byte >> 4, low = byte & 0x0F; + macro[i * 2] = high + (high < 0x0A ? '0' : 'A' - 0x0A); + macro[i * 2 + 1] = low + (low < 0x0A ? '0' : 'A' - 0x0A); + } + m_MidiCfg.Zxx[dump] = std::string_view{macro.data(), length * 2}; + } + } + } +#endif // MPT_WITH_VST + + if(expData.mmdInfoOffset && file.Seek(expData.mmdInfoOffset) && file.CanRead(12)) + { + file.Skip(6); // Next info file (unused) + reserved + if(file.ReadUint16BE() == 1) // ASCII text + { + uint32 length = file.ReadUint32BE(); + if(length && file.CanRead(length)) + { + const auto oldMsg = std::move(m_songMessage); + m_songMessage.Read(file, length, SongMessage::leAutodetect); + if(!oldMsg.empty()) + m_songMessage.SetRaw(oldMsg + std::string(2, SongMessage::InternalLineEnding) + m_songMessage); + } + } + } + + // Track Names + if(version >= 2 && expData.trackInfoOffset) + { + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + if(file.Seek(expData.trackInfoOffset + chn * 4) + && file.Seek(file.ReadUint32BE())) + { + uint32 nameOffset = 0, nameLength = 0; + while(file.CanRead(sizeof(MMDTag))) + { + MMDTag tag; + file.ReadStruct(tag); + if(tag.type == MMDTag::MMDTAG_END) + break; + switch(tag.type & MMDTag::MMDTAG_MASK) + { + case MMDTag::MMDTAG_TRK_NAME: nameOffset = tag.data; break; + case MMDTag::MMDTAG_TRK_NAMELEN: nameLength = tag.data; break; + } + } + if(nameOffset > 0 && nameLength > 0 && file.Seek(nameOffset)) + { + file.ReadString<mpt::String::maybeNullTerminated>(ChnSettings[chn].szName, nameLength); + } + } + } + } + + PATTERNINDEX numPatterns = songHeader.numBlocks; + Patterns.ResizeArray(basePattern + numPatterns); + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) + || !file.Seek(fileHeader.blockArrOffset + pat * 4u) + || !file.Seek(file.ReadUint32BE())) + { + continue; + } + + CHANNELINDEX numTracks; + ROWINDEX numRows; + std::string patName; + int transpose; + FileReader cmdExt; + + if(version < 1) + { + transpose = NOTE_MIN + 47; + MMD0PatternHeader patHeader; + file.ReadStruct(patHeader); + numTracks = patHeader.numTracks; + numRows = patHeader.numRows + 1; + } else + { + transpose = NOTE_MIN + (version <= 2 ? 47 : 23) + songHeader.playTranspose; + MMD1PatternHeader patHeader; + file.ReadStruct(patHeader); + numTracks = patHeader.numTracks; + numRows = patHeader.numRows + 1; + if(patHeader.blockInfoOffset) + { + auto offset = file.GetPosition(); + file.Seek(patHeader.blockInfoOffset); + MMDBlockInfo blockInfo; + file.ReadStruct(blockInfo); + if(file.Seek(blockInfo.nameOffset)) + { + // We have now chased four pointers to get this far... lovely format. + file.ReadString<mpt::String::maybeNullTerminated>(patName, blockInfo.nameLength); + } + if(blockInfo.cmdExtTableOffset + && file.Seek(blockInfo.cmdExtTableOffset) + && file.Seek(file.ReadUint32BE())) + { + cmdExt = file.ReadChunk(numTracks * numRows); + } + + file.Seek(offset); + } + } + + if(!Patterns.Insert(basePattern + pat, numRows)) + continue; + + CPattern &pattern = Patterns[basePattern + pat]; + pattern.SetName(patName); + LimitMax(numTracks, m_nChannels); + + for(ROWINDEX row = 0; row < numRows; row++) + { + ModCommand *m = pattern.GetpModCommand(row, 0); + for(CHANNELINDEX chn = 0; chn < numTracks; chn++, m++) + { + int note = NOTE_NONE; + if(version < 1) + { + const auto [noteInstr, instrCmd, param] = file.ReadArray<uint8, 3>(); + + if(noteInstr & 0x3F) + note = (noteInstr & 0x3F) + transpose; + + m->instr = (instrCmd >> 4) | ((noteInstr & 0x80) >> 3) | ((noteInstr & 0x40) >> 1); + + m->command = instrCmd & 0x0F; + m->param = param; + } else + { + const auto [noteVal, instr, command, param1] = file.ReadArray<uint8, 4>(); + m->vol = cmdExt.ReadUint8(); + + if(noteVal & 0x7F) + note = (noteVal & 0x7F) + transpose; + else if(noteVal == 0x80) + m->note = NOTE_NOTECUT; + + m->instr = instr & 0x3F; + m->command = command; + m->param = param1; + } + // Octave wrapping for 4-channel modules (TODO: this should not be set because of synth instruments) + if(hardwareMixSamples && note >= NOTE_MIDDLEC + 2 * 12) + needInstruments = true; + + if(note >= NOTE_MIN && note <= NOTE_MAX) + m->note = static_cast<ModCommand::NOTE>(note); + ConvertMEDEffect(*m, is8Ch, bpmMode, rowsPerBeat, volHex); + } + } + } + + // Fix jump order commands + for(const auto & [from, to] : jumpTargets) + { + PATTERNINDEX pat; + if(from > 0 && order.IsValidPat(from - 1)) + { + pat = order.EnsureUnique(from - 1); + } else + { + if(to == from + 1) // No action required + continue; + pat = Patterns.InsertAny(1); + if(pat == PATTERNINDEX_INVALID) + continue; + order[from] = pat; + } + Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, mpt::saturate_cast<ModCommand::PARAM>(to)).Row(Patterns[pat].GetNumRows() - 1).RetryPreviousRow()); + if(pat >= numPatterns) + numPatterns = pat + 1; + } + + basePattern += numPatterns; + + if(!expData.nextModOffset || !file.Seek(expData.nextModOffset)) + break; + } + Order.SetSequence(0); + + if(!needInstruments) + { + for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) + { + delete Instruments[ins]; + Instruments[ins] = nullptr; + } + m_nInstruments = 0; + } + + if(anySynthInstrs) + AddToLog(LogWarning, U_("Synthesized MED instruments are not supported.")); + + const mpt::uchar *madeWithTracker = MPT_ULITERAL(""); + switch(version) + { + case 0: madeWithTracker = m_nChannels > 4 ? MPT_ULITERAL("OctaMED v2.10 (MMD0)") : MPT_ULITERAL("MED v2 (MMD0)"); break; + case 1: madeWithTracker = MPT_ULITERAL("OctaMED v4 (MMD1)"); break; + case 2: madeWithTracker = MPT_ULITERAL("OctaMED v5 (MMD2)"); break; + case 3: madeWithTracker = MPT_ULITERAL("OctaMED Soundstudio (MMD3)"); break; + } + + m_modFormat.formatName = MPT_UFORMAT("OctaMED (MMD{})")(version); + m_modFormat.type = MPT_USTRING("med"); + m_modFormat.madeWithTracker = madeWithTracker; + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mid.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mid.cpp new file mode 100644 index 00000000..48baa20f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mid.cpp @@ -0,0 +1,1405 @@ +/* + * Load_mid.cpp + * ------------ + * Purpose: MIDI file loader + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "Dlsbank.h" +#include "MIDIEvents.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#include "../mptrack/Moddoc.h" +#include "../mptrack/Mptrack.h" +#include "../common/mptFileIO.h" +#endif // MODPLUG_TRACKER + +OPENMPT_NAMESPACE_BEGIN + +#if defined(MODPLUG_TRACKER) || defined(MPT_FUZZ_TRACKER) + +#ifdef LIBOPENMPT_BUILD +struct CDLSBank { static int32 DLSMidiVolumeToLinear(uint32) { return 256; } }; +#endif // LIBOPENMPT_BUILD + +#define MIDI_DRUMCHANNEL 10 + +const char *szMidiGroupNames[17] = +{ + "Piano", + "Chromatic Percussion", + "Organ", + "Guitar", + "Bass", + "Strings", + "Ensemble", + "Brass", + "Reed", + "Pipe", + "Synth Lead", + "Synth Pad", + "Synth Effects", + "Ethnic", + "Percussive", + "Sound Effects", + "Percussions" +}; + + +const char *szMidiProgramNames[128] = +{ + // 1-8: Piano + "Acoustic Grand Piano", + "Bright Acoustic Piano", + "Electric Grand Piano", + "Honky-tonk Piano", + "Electric Piano 1", + "Electric Piano 2", + "Harpsichord", + "Clavi", + // 9-16: Chromatic Percussion + "Celesta", + "Glockenspiel", + "Music Box", + "Vibraphone", + "Marimba", + "Xylophone", + "Tubular Bells", + "Dulcimer", + // 17-24: Organ + "Drawbar Organ", + "Percussive Organ", + "Rock Organ", + "Church Organ", + "Reed Organ", + "Accordion", + "Harmonica", + "Tango Accordion", + // 25-32: Guitar + "Acoustic Guitar (nylon)", + "Acoustic Guitar (steel)", + "Electric Guitar (jazz)", + "Electric Guitar (clean)", + "Electric Guitar (muted)", + "Overdriven Guitar", + "Distortion Guitar", + "Guitar harmonics", + // 33-40 Bass + "Acoustic Bass", + "Electric Bass (finger)", + "Electric Bass (pick)", + "Fretless Bass", + "Slap Bass 1", + "Slap Bass 2", + "Synth Bass 1", + "Synth Bass 2", + // 41-48 Strings + "Violin", + "Viola", + "Cello", + "Contrabass", + "Tremolo Strings", + "Pizzicato Strings", + "Orchestral Harp", + "Timpani", + // 49-56 Ensemble + "String Ensemble 1", + "String Ensemble 2", + "SynthStrings 1", + "SynthStrings 2", + "Choir Aahs", + "Voice Oohs", + "Synth Voice", + "Orchestra Hit", + // 57-64 Brass + "Trumpet", + "Trombone", + "Tuba", + "Muted Trumpet", + "French Horn", + "Brass Section", + "SynthBrass 1", + "SynthBrass 2", + // 65-72 Reed + "Soprano Sax", + "Alto Sax", + "Tenor Sax", + "Baritone Sax", + "Oboe", + "English Horn", + "Bassoon", + "Clarinet", + // 73-80 Pipe + "Piccolo", + "Flute", + "Recorder", + "Pan Flute", + "Blown Bottle", + "Shakuhachi", + "Whistle", + "Ocarina", + // 81-88 Synth Lead + "Lead 1 (square)", + "Lead 2 (sawtooth)", + "Lead 3 (calliope)", + "Lead 4 (chiff)", + "Lead 5 (charang)", + "Lead 6 (voice)", + "Lead 7 (fifths)", + "Lead 8 (bass + lead)", + // 89-96 Synth Pad + "Pad 1 (new age)", + "Pad 2 (warm)", + "Pad 3 (polysynth)", + "Pad 4 (choir)", + "Pad 5 (bowed)", + "Pad 6 (metallic)", + "Pad 7 (halo)", + "Pad 8 (sweep)", + // 97-104 Synth Effects + "FX 1 (rain)", + "FX 2 (soundtrack)", + "FX 3 (crystal)", + "FX 4 (atmosphere)", + "FX 5 (brightness)", + "FX 6 (goblins)", + "FX 7 (echoes)", + "FX 8 (sci-fi)", + // 105-112 Ethnic + "Sitar", + "Banjo", + "Shamisen", + "Koto", + "Kalimba", + "Bag pipe", + "Fiddle", + "Shanai", + // 113-120 Percussive + "Tinkle Bell", + "Agogo", + "Steel Drums", + "Woodblock", + "Taiko Drum", + "Melodic Tom", + "Synth Drum", + "Reverse Cymbal", + // 121-128 Sound Effects + "Guitar Fret Noise", + "Breath Noise", + "Seashore", + "Bird Tweet", + "Telephone Ring", + "Helicopter", + "Applause", + "Gunshot" +}; + + +// Notes 25-85 +const char *szMidiPercussionNames[61] = +{ + "Seq Click", + "Brush Tap", + "Brush Swirl", + "Brush Slap", + "Brush Swirl W/Attack", + "Snare Roll", + "Castanet", + "Snare Lo", + "Sticks", + "Bass Drum Lo", + "Open Rim Shot", + "Acoustic Bass Drum", + "Bass Drum 1", + "Side Stick", + "Acoustic Snare", + "Hand Clap", + "Electric Snare", + "Low Floor Tom", + "Closed Hi-Hat", + "High Floor Tom", + "Pedal Hi-Hat", + "Low Tom", + "Open Hi-Hat", + "Low-Mid Tom", + "Hi Mid Tom", + "Crash Cymbal 1", + "High Tom", + "Ride Cymbal 1", + "Chinese Cymbal", + "Ride Bell", + "Tambourine", + "Splash Cymbal", + "Cowbell", + "Crash Cymbal 2", + "Vibraslap", + "Ride Cymbal 2", + "Hi Bongo", + "Low Bongo", + "Mute Hi Conga", + "Open Hi Conga", + "Low Conga", + "High Timbale", + "Low Timbale", + "High Agogo", + "Low Agogo", + "Cabasa", + "Maracas", + "Short Whistle", + "Long Whistle", + "Short Guiro", + "Long Guiro", + "Claves", + "Hi Wood Block", + "Low Wood Block", + "Mute Cuica", + "Open Cuica", + "Mute Triangle", + "Open Triangle", + "Shaker", + "Jingle Bell", + "Bell Tree", +}; + + +//////////////////////////////////////////////////////////////////////////////// +// Maps a midi instrument - returns the instrument number in the file +uint32 CSoundFile::MapMidiInstrument(uint8 program, uint16 bank, uint8 midiChannel, uint8 note, bool isXG, std::bitset<16> drumChns) +{ + ModInstrument *pIns; + program &= 0x7F; + bank &= 0x3FFF; + note &= 0x7F; + + // In XG mode, extra drums are on banks with MSB 7F + const bool isDrum = drumChns[midiChannel - 1] || (bank >= 0x3F80 && isXG); + + for (uint32 i = 1; i <= m_nInstruments; i++) if (Instruments[i]) + { + ModInstrument *p = Instruments[i]; + // Drum Kit? + if (isDrum) + { + if (note == p->nMidiDrumKey && bank + 1 == p->wMidiBank) return i; + } else + // Melodic Instrument + { + if (program + 1 == p->nMidiProgram && bank + 1 == p->wMidiBank && p->nMidiDrumKey == 0) return i; + } + } + if(!CanAddMoreInstruments() || !CanAddMoreSamples()) + return 0; + + pIns = AllocateInstrument(m_nInstruments + 1); + if(pIns == nullptr) + { + return 0; + } + + m_nSamples++; + pIns->wMidiBank = bank + 1; + pIns->nMidiProgram = program + 1; + pIns->nFadeOut = 1024; + pIns->nNNA = NewNoteAction::NoteOff; + pIns->nDCT = isDrum ? DuplicateCheckType::Sample : DuplicateCheckType::Note; + pIns->nDNA = DuplicateNoteAction::NoteFade; + if(isDrum) + { + pIns->nMidiChannel = MIDI_DRUMCHANNEL; + pIns->nMidiDrumKey = note; + for(auto &key : pIns->NoteMap) + { + key = NOTE_MIDDLEC; + } + } + pIns->VolEnv.dwFlags.set(ENV_ENABLED); + if (!isDrum) pIns->VolEnv.dwFlags.set(ENV_SUSTAIN); + pIns->VolEnv.reserve(4); + pIns->VolEnv.push_back(EnvelopeNode(0, ENVELOPE_MAX)); + pIns->VolEnv.push_back(EnvelopeNode(10, ENVELOPE_MAX)); + pIns->VolEnv.push_back(EnvelopeNode(15, (ENVELOPE_MAX + ENVELOPE_MID) / 2)); + pIns->VolEnv.push_back(EnvelopeNode(20, ENVELOPE_MIN)); + pIns->VolEnv.nSustainStart = pIns->VolEnv.nSustainEnd = 1; + // Set GM program / drum name + if (!isDrum) + { + pIns->name = szMidiProgramNames[program]; + } else + { + if (note >= 24 && note <= 84) + pIns->name = szMidiPercussionNames[note - 24]; + else + pIns->name = "Percussions"; + } + return m_nInstruments; +} + + +struct MThd +{ + uint32be headerLength; + uint16be format; // 0 = single-track, 1 = multi-track, 2 = multi-song + uint16be numTracks; // Number of track chunks + uint16be division; // Delta timing value: positive = units/beat; negative = smpte compatible units +}; + +MPT_BINARY_STRUCT(MThd, 10) + + +using tick_t = uint32; + +struct TrackState +{ + FileReader track; + tick_t nextEvent = 0; + uint8 command = 0; + bool finished = false; +}; + +struct ModChannelState +{ + static constexpr uint8 NOMIDI = 0xFF; // No MIDI channel assigned. + + tick_t age = 0; // At which MIDI tick the channel was triggered + int32 porta = 0; // Current portamento position in extra-fine slide units (1/64th of a semitone) + uint8 vol = 100; // MIDI note volume (0...127) + uint8 pan = 128; // MIDI channel panning (0...256) + uint8 midiCh = NOMIDI; // MIDI channel that was last played on this channel + ModCommand::NOTE note = NOTE_NONE; // MIDI note that was last played on this channel + bool sustained = false; // If true, the note was already released by a note-off event, but sustain pedal CC is still active +}; + +struct MidiChannelState +{ + int32 pitchbendMod = 0; // Pre-computed pitchbend in extra-fine slide units (1/64th of a semitone) + int16 pitchbend = MIDIEvents::pitchBendCentre; // 0...16383 + uint16 bank = 0; // 0...16383 + uint8 program = 0; // 0...127 + // -- Controllers ---------------- function ---------- CC# --- range ---- init (midi) --- + uint8 pan = 128; // Channel Panning 10 [0-255] 128 (64) + uint8 expression = 128; // Channel Expression 11 0-128 128 (127) + uint8 volume = 80; // Channel Volume 7 0-128 80 (100) + uint16 rpn = 0x3FFF; // Currently selected RPN 100/101 n/a + uint8 pitchBendRange = 2; // Pitch Bend Range 2 + int8 transpose = 0; // Channel transpose 0 + bool monoMode = false; // Mono/Poly operation 126/127 n/a Poly + bool sustain = false; // Sustain pedal 64 on/off off + + std::array<CHANNELINDEX, 128> noteOn; // Value != CHANNELINDEX_INVALID: Note is active and mapped to mod channel in value + + MidiChannelState() + { + noteOn.fill(CHANNELINDEX_INVALID); + } + + void SetPitchbend(uint16 value) + { + pitchbend = value; + // Convert from arbitrary MIDI pitchbend to 64th of semitone + pitchbendMod = Util::muldiv(pitchbend - MIDIEvents::pitchBendCentre, pitchBendRange * 64, MIDIEvents::pitchBendCentre); + } + + void ResetAllControllers() + { + expression = 128; + pitchBendRange = 2; + SetPitchbend(MIDIEvents::pitchBendCentre); + transpose = 0; + rpn = 0x3FFF; + monoMode = false; + sustain = false; + // Should also reset modulation, pedals (40h-43h), aftertouch + } + + void SetRPN(uint8 value) + { + switch(rpn) + { + case 0: // Pitch Bend Range + pitchBendRange = std::max(value, uint8(1)); + SetPitchbend(pitchbend); + break; + case 2: // Coarse Tune + transpose = static_cast<int8>(value) - 64; + break; + } + } + + void SetRPNRelative(int8 value) + { + switch(rpn) + { + case 0: // Pitch Bend Range + pitchBendRange = static_cast<uint8>(std::clamp(pitchBendRange + value, 1, 0x7F)); + break; + case 2: // Coarse Tune + transpose = mpt::saturate_cast<int8>(transpose + value); + break; + } + } +}; + + +static CHANNELINDEX FindUnusedChannel(uint8 midiCh, ModCommand::NOTE note, const std::vector<ModChannelState> &channels, bool monoMode, PatternRow patRow) +{ + for(size_t i = 0; i < channels.size(); i++) + { + // Check if this note is already playing, or find any note of the same MIDI channel in case of mono mode + if(channels[i].midiCh == midiCh && (channels[i].note == note || (monoMode && channels[i].note != NOTE_NONE))) + { + return static_cast<CHANNELINDEX>(i); + } + } + + CHANNELINDEX anyUnusedChannel = CHANNELINDEX_INVALID; + CHANNELINDEX anyFreeChannel = CHANNELINDEX_INVALID; + + CHANNELINDEX oldsetMidiCh = CHANNELINDEX_INVALID; + tick_t oldestMidiChAge = std::numeric_limits<decltype(oldestMidiChAge)>::max(); + + CHANNELINDEX oldestAnyCh = 0; + tick_t oldestAnyChAge = std::numeric_limits<decltype(oldestAnyChAge)>::max(); + + for(size_t i = 0; i < channels.size(); i++) + { + if(channels[i].note == NOTE_NONE && !patRow[i].IsNote()) + { + // Recycle channel previously used by the same MIDI channel + if(channels[i].midiCh == midiCh) + return static_cast<CHANNELINDEX>(i); + // If we cannot find a channel that was already used for the same MIDI channel, try a completely unused channel next + else if(channels[i].midiCh == ModChannelState::NOMIDI && anyUnusedChannel == CHANNELINDEX_INVALID) + anyUnusedChannel = static_cast<CHANNELINDEX>(i); + // And if that fails, try any channel that currently doesn't play a note. + if(anyFreeChannel == CHANNELINDEX_INVALID) + anyFreeChannel = static_cast<CHANNELINDEX>(i); + } + + // If we can't find any free channels, look for the oldest channels + if(channels[i].midiCh == midiCh && channels[i].age < oldestMidiChAge) + { + // Oldest channel matching this MIDI channel + oldestMidiChAge = channels[i].age; + oldsetMidiCh = static_cast<CHANNELINDEX>(i); + } else if(channels[i].age < oldestAnyChAge) + { + // Any oldest channel + oldestAnyChAge = channels[i].age; + oldestAnyCh = static_cast<CHANNELINDEX>(i); + } + } + if(anyUnusedChannel != CHANNELINDEX_INVALID) + return anyUnusedChannel; + if(anyFreeChannel != CHANNELINDEX_INVALID) + return anyFreeChannel; + if(oldsetMidiCh != CHANNELINDEX_INVALID) + return oldsetMidiCh; + return oldestAnyCh; +} + + +static void MIDINoteOff(MidiChannelState &midiChn, std::vector<ModChannelState> &modChnStatus, uint8 note, uint8 delay, PatternRow patRow, std::bitset<16> drumChns) +{ + CHANNELINDEX chn = midiChn.noteOn[note]; + if(chn == CHANNELINDEX_INVALID) + return; + + if(midiChn.sustain) + { + // Turn this off later + modChnStatus[chn].sustained = true; + return; + } + + uint8 midiCh = modChnStatus[chn].midiCh; + modChnStatus[chn].note = NOTE_NONE; + modChnStatus[chn].sustained = false; + midiChn.noteOn[note] = CHANNELINDEX_INVALID; + ModCommand &m = patRow[chn]; + if(m.note == NOTE_NONE) + { + m.note = NOTE_KEYOFF; + if(delay != 0) + { + m.command = CMD_S3MCMDEX; + m.param = 0xD0 | delay; + } + } else if(m.IsNote() && !drumChns[midiCh]) + { + // Only do note cuts for melodic instruments - they sound weird on drums which should fade out naturally. + if(m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0xD0) + { + // Already have a note delay + m.command = CMD_DELAYCUT; + m.param = (m.param << 4) | (delay - (m.param & 0x0F)); + } else if(m.command == CMD_NONE || m.command == CMD_PANNING8) + { + m.command = CMD_S3MCMDEX; + m.param = 0xC0 | delay; + } + } +} + + +static void EnterMIDIVolume(ModCommand &m, ModChannelState &modChn, const MidiChannelState &midiChn) +{ + m.volcmd = VOLCMD_VOLUME; + + int32 vol = CDLSBank::DLSMidiVolumeToLinear(modChn.vol) >> 8; + vol = (vol * midiChn.volume * midiChn.expression) >> 13; + Limit(vol, 4, 256); + m.vol = static_cast<ModCommand::VOL>(vol / 4); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMID(MemoryFileReader file, const uint64 *pfilesize) +{ + MPT_UNREFERENCED_PARAMETER(pfilesize); + char magic[4]; + file.ReadArray(magic); + if(!memcmp(magic, "MThd", 4)) + return ProbeSuccess; + + if(!memcmp(magic, "RIFF", 4) && file.Skip(4) && file.ReadMagic("RMID")) + return ProbeSuccess; + + return ProbeFailure; +} + + +bool CSoundFile::ReadMID(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + // Microsoft MIDI files + bool isRIFF = false; + if(file.ReadMagic("RIFF")) + { + file.Skip(4); + if(!file.ReadMagic("RMID")) + { + return false; + } else if(loadFlags == onlyVerifyHeader) + { + return true; + } + do + { + char id[4]; + file.ReadArray(id); + uint32 length = file.ReadUint32LE(); + if(memcmp(id, "data", 4)) + { + file.Skip(length); + } else + { + isRIFF = true; + break; + } + } while(file.CanRead(8)); + } + + MThd fileHeader; + if(!file.ReadMagic("MThd") + || !file.ReadStruct(fileHeader) + || fileHeader.numTracks == 0 + || fileHeader.headerLength < 6 + || !file.Skip(fileHeader.headerLength - 6)) + { + return false; + } else if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_MID); + InitializeChannels(); + +#ifdef MODPLUG_TRACKER + const uint32 quantize = Clamp(TrackerSettings::Instance().midiImportQuantize.Get(), 4u, 256u); + const ROWINDEX patternLen = Clamp(TrackerSettings::Instance().midiImportPatternLen.Get(), ROWINDEX(1), MAX_PATTERN_ROWS); + const uint8 ticksPerRow = Clamp(TrackerSettings::Instance().midiImportTicks.Get(), uint8(2), uint8(16)); +#else + const uint32 quantize = 32; // Must be 4 or higher + const ROWINDEX patternLen = 128; + const uint8 ticksPerRow = 16; // Must be in range 2...16 +#endif +#ifdef MPT_FUZZ_TRACKER + // Avoid generating test cases that take overly long to evaluate + const ORDERINDEX MPT_MIDI_IMPORT_MAX_ORDERS = 64; +#else + const ORDERINDEX MPT_MIDI_IMPORT_MAX_ORDERS = MAX_ORDERS; +#endif + + m_songArtist = U_("MIDI Conversion"); + m_modFormat.formatName = U_("Standard MIDI File"); + m_modFormat.type = isRIFF ? UL_("rmi") : UL_("mid"); + m_modFormat.madeWithTracker = U_("Standard MIDI File"); + m_modFormat.charset = mpt::Charset::ISO8859_1; + + SetMixLevels(MixLevels::v1_17RC3); + m_nTempoMode = TempoMode::Modern; + m_SongFlags = SONG_LINEARSLIDES; + m_nDefaultTempo.Set(120); + m_nDefaultSpeed = ticksPerRow; + m_nChannels = MAX_BASECHANNELS; + m_nDefaultRowsPerBeat = quantize / 4; + m_nDefaultRowsPerMeasure = 4 * m_nDefaultRowsPerBeat; + m_nSamplePreAmp = m_nVSTiVolume = 32; + TEMPO tempo = m_nDefaultTempo; + uint16 ppqn = fileHeader.division; + if(ppqn & 0x8000) + { + // SMPTE compatible units (approximation) + int frames = 256 - (ppqn >> 8), subFrames = (ppqn & 0xFF); + ppqn = static_cast<uint16>(frames * subFrames / 2); + } + if(!ppqn) + ppqn = 96; + Order().clear(); + + MidiChannelState midiChnStatus[16]; + const CHANNELINDEX tempoChannel = m_nChannels - 2, globalVolChannel = m_nChannels - 1; + const uint16 numTracks = fileHeader.numTracks; + std::vector<TrackState> tracks(numTracks); + std::vector<ModChannelState> modChnStatus(m_nChannels); + std::bitset<16> drumChns; + drumChns.set(MIDI_DRUMCHANNEL - 1); + + tick_t timeShift = 0; + for(auto &track : tracks) + { + if(!file.ReadMagic("MTrk")) + return false; + track.track = file.ReadChunk(file.ReadUint32BE()); + tick_t delta = 0; + track.track.ReadVarInt(delta); + // Work-around for some MID files that assume that negative deltas exist (they don't according to the standard) + if(delta > int32_max) + timeShift = std::max(static_cast<tick_t>(~delta + 1), timeShift); + track.nextEvent = delta; + } + if(timeShift != 0) + { + for(auto &track : tracks) + { + if(track.nextEvent > int32_max) + track.nextEvent = timeShift - static_cast<tick_t>(~track.nextEvent + 1); + else + track.nextEvent += timeShift; + } + } + + uint16 finishedTracks = 0; + PATTERNINDEX emptyPattern = PATTERNINDEX_INVALID; + ORDERINDEX lastOrd = 0, loopEndOrd = ORDERINDEX_INVALID; + ROWINDEX lastRow = 0, loopEndRow = ROWINDEX_INVALID; + ROWINDEX restartRow = ROWINDEX_INVALID; + int8 masterTranspose = 0; + bool isXG = false; + bool isEMIDI = false; + bool isEMIDILoop = false; + const bool isType2 = (fileHeader.format == 2); + + const auto ModPositionFromTick = [&](const tick_t tick, const tick_t offset = 0) + { + tick_t modTicks = Util::muldivr_unsigned(tick, quantize * ticksPerRow, ppqn * 4u) - offset; + + ORDERINDEX ord = static_cast<ORDERINDEX>((modTicks / ticksPerRow) / patternLen); + ROWINDEX row = (modTicks / ticksPerRow) % patternLen; + uint8 delay = static_cast<uint8>(modTicks % ticksPerRow); + + return std::make_tuple(ord, row, delay); + }; + + while(finishedTracks < numTracks) + { + uint16 t = 0; + tick_t tick = std::numeric_limits<decltype(tick)>::max(); + for(uint16 track = 0; track < numTracks; track++) + { + if(!tracks[track].finished && tracks[track].nextEvent < tick) + { + tick = tracks[track].nextEvent; + t = track; + if(isType2) + break; + } + } + FileReader &track = tracks[t].track; + + const auto [ord, row, delay] = ModPositionFromTick(tick); + + if(ord >= Order().GetLength()) + { + if(ord > MPT_MIDI_IMPORT_MAX_ORDERS) + break; + ORDERINDEX curSize = Order().GetLength(); + // If we need to extend the order list by more than one pattern, this means that we + // will be filling in empty patterns. Just recycle one empty pattern for this job. + // We read events in chronological order, so it is never possible for the loader to + // "jump back" to one of those empty patterns and write into it. + if(ord > curSize && emptyPattern == PATTERNINDEX_INVALID) + { + if((emptyPattern = Patterns.InsertAny(patternLen)) == PATTERNINDEX_INVALID) + break; + } + Order().resize(ord + 1, emptyPattern); + + if((Order()[ord] = Patterns.InsertAny(patternLen)) == PATTERNINDEX_INVALID) + break; + } + + // Keep track of position of last event for resizing the last pattern + if(ord > lastOrd) + { + lastOrd = ord; + lastRow = row; + } else if(ord == lastOrd) + { + lastRow = std::max(lastRow, row); + } + + PATTERNINDEX pat = Order()[ord]; + PatternRow patRow = Patterns[pat].GetRow(row); + + uint8 data1 = track.ReadUint8(); + if(data1 == 0xFF) + { + // Meta events + data1 = track.ReadUint8(); + size_t len = 0; + track.ReadVarInt(len); + FileReader chunk = track.ReadChunk(len); + + switch(data1) + { + case 1: // Text + case 2: // Copyright + m_songMessage.Read(chunk, len, SongMessage::leAutodetect); + break; + case 3: // Track Name + if(len > 0) + { + std::string s; + chunk.ReadString<mpt::String::maybeNullTerminated>(s, len); + if(!m_songMessage.empty()) + m_songMessage.append(1, SongMessage::InternalLineEnding); + m_songMessage += s; + if(m_songName.empty()) + m_songName = s; + } + break; + case 4: // Instrument + case 5: // Lyric + break; + case 6: // Marker + case 7: // Cue point + { + std::string s; + chunk.ReadString<mpt::String::maybeNullTerminated>(s, len); + Patterns[pat].SetName(s); + if(!mpt::CompareNoCaseAscii(s, "loopStart")) + { + Order().SetRestartPos(ord); + restartRow = row; + } else if(!mpt::CompareNoCaseAscii(s, "loopEnd")) + { + std::tie(loopEndOrd, loopEndRow, std::ignore) = ModPositionFromTick(tick, 1); + } + } + break; + case 8: // Patch name + case 9: // Port name + break; + case 0x2F: // End Of Track + tracks[t].finished = true; + break; + case 0x51: // Tempo + { + uint32 tempoInt = chunk.ReadUint24BE(); + if(tempoInt == 0) + break; + TEMPO newTempo(60000000.0 / tempoInt); + if(!tick) + { + m_nDefaultTempo = newTempo; + } else if(newTempo != tempo) + { + patRow[tempoChannel].command = CMD_TEMPO; + patRow[tempoChannel].param = mpt::saturate_round<ModCommand::PARAM>(std::max(32.0, newTempo.ToDouble())); + } + tempo = newTempo; + } + break; + + default: + break; + } + } else + { + uint8 command = tracks[t].command; + if(data1 & 0x80) + { + // Command byte (if not present, use running status for channel messages) + command = data1; + if(data1 < 0xF0) + { + tracks[t].command = data1; + data1 = track.ReadUint8(); + } + } + uint8 midiCh = command & 0x0F; + + switch(command & 0xF0) + { + case 0x80: // Note Off + case 0x90: // Note On + { + data1 &= 0x7F; + ModCommand::NOTE note = static_cast<ModCommand::NOTE>(Clamp(data1 + NOTE_MIN, NOTE_MIN, NOTE_MAX)); + uint8 data2 = track.ReadUint8(); + if(data2 > 0 && (command & 0xF0) == 0x90) + { + // Note On + CHANNELINDEX chn = FindUnusedChannel(midiCh, note, modChnStatus, midiChnStatus[midiCh].monoMode, patRow); + if(chn != CHANNELINDEX_INVALID) + { + modChnStatus[chn].age = tick; + modChnStatus[chn].note = note; + modChnStatus[chn].midiCh = midiCh; + modChnStatus[chn].vol = data2; + modChnStatus[chn].sustained = false; + midiChnStatus[midiCh].noteOn[data1] = chn; + int32 pitchOffset = 0; + if(midiChnStatus[midiCh].pitchbendMod != 0) + { + pitchOffset = (midiChnStatus[midiCh].pitchbendMod + (midiChnStatus[midiCh].pitchbendMod > 0 ? 32 : -32)) / 64; + modChnStatus[chn].porta = pitchOffset * 64; + } else + { + modChnStatus[chn].porta = 0; + } + patRow[chn].note = static_cast<ModCommand::NOTE>(Clamp(note + pitchOffset + midiChnStatus[midiCh].transpose + masterTranspose, NOTE_MIN, NOTE_MAX)); + patRow[chn].instr = mpt::saturate_cast<ModCommand::INSTR>(MapMidiInstrument(midiChnStatus[midiCh].program, midiChnStatus[midiCh].bank, midiCh + 1, data1, isXG, drumChns)); + EnterMIDIVolume(patRow[chn], modChnStatus[chn], midiChnStatus[midiCh]); + + if(patRow[chn].command == CMD_PORTAMENTODOWN || patRow[chn].command == CMD_PORTAMENTOUP) + { + patRow[chn].command = CMD_NONE; + } + if(delay != 0) + { + patRow[chn].command = CMD_S3MCMDEX; + patRow[chn].param = 0xD0 | delay; + } + if(modChnStatus[chn].pan != midiChnStatus[midiCh].pan && patRow[chn].command == CMD_NONE) + { + patRow[chn].command = CMD_PANNING8; + patRow[chn].param = midiChnStatus[midiCh].pan; + modChnStatus[chn].pan = midiChnStatus[midiCh].pan; + } + } + } else + { + // Note Off + MIDINoteOff(midiChnStatus[midiCh], modChnStatus, data1, delay, patRow, drumChns); + } + } + break; + case 0xA0: // Note Aftertouch + { + track.Skip(1); + } + break; + case 0xB0: // Controller + { + uint8 data2 = track.ReadUint8(); + switch(data1) + { + case MIDIEvents::MIDICC_Panposition_Coarse: + midiChnStatus[midiCh].pan = data2 * 2u; + for(auto chn : midiChnStatus[midiCh].noteOn) + { + if(chn != CHANNELINDEX_INVALID && modChnStatus[chn].pan != midiChnStatus[midiCh].pan) + { + if(Patterns[pat].WriteEffect(EffectWriter(CMD_PANNING8, midiChnStatus[midiCh].pan).Channel(chn).Row(row))) + { + modChnStatus[chn].pan = midiChnStatus[midiCh].pan; + } + } + } + break; + + case MIDIEvents::MIDICC_DataEntry_Coarse: + midiChnStatus[midiCh].SetRPN(data2); + break; + + case MIDIEvents::MIDICC_Volume_Coarse: + midiChnStatus[midiCh].volume = (uint8)(CDLSBank::DLSMidiVolumeToLinear(data2) >> 9); + for(auto chn : midiChnStatus[midiCh].noteOn) + { + if(chn != CHANNELINDEX_INVALID) + { + EnterMIDIVolume(patRow[chn], modChnStatus[chn], midiChnStatus[midiCh]); + } + } + break; + + case MIDIEvents::MIDICC_Expression_Coarse: + midiChnStatus[midiCh].expression = (uint8)(CDLSBank::DLSMidiVolumeToLinear(data2) >> 9); + for(auto chn : midiChnStatus[midiCh].noteOn) + { + if(chn != CHANNELINDEX_INVALID) + { + EnterMIDIVolume(patRow[chn], modChnStatus[chn], midiChnStatus[midiCh]); + } + } + break; + + case MIDIEvents::MIDICC_BankSelect_Coarse: + midiChnStatus[midiCh].bank &= 0x7F; + midiChnStatus[midiCh].bank |= (data2 << 7); + break; + + case MIDIEvents::MIDICC_BankSelect_Fine: + midiChnStatus[midiCh].bank &= (0x7F << 7); + midiChnStatus[midiCh].bank |= data2; + break; + + case MIDIEvents::MIDICC_HoldPedal_OnOff: + midiChnStatus[midiCh].sustain = (data2 >= 0x40); + if(data2 < 0x40) + { + // Release notes that are still being held after note-off + for(const auto &chnState : modChnStatus) + { + if(chnState.midiCh == midiCh && chnState.sustained && chnState.note != NOTE_NONE) + { + MIDINoteOff(midiChnStatus[midiCh], modChnStatus, chnState.note - NOTE_MIN, delay, patRow, drumChns); + } + } + } + break; + + case MIDIEvents::MIDICC_DataButtonincrement: + case MIDIEvents::MIDICC_DataButtondecrement: + midiChnStatus[midiCh].SetRPNRelative((data1 == MIDIEvents::MIDICC_DataButtonincrement) ? 1 : -1); + break; + + case MIDIEvents::MIDICC_NonRegisteredParameter_Fine: + case MIDIEvents::MIDICC_NonRegisteredParameter_Coarse: + midiChnStatus[midiCh].rpn = 0x3FFF; + break; + + case MIDIEvents::MIDICC_RegisteredParameter_Fine: + midiChnStatus[midiCh].rpn &= (0x7F << 7); + midiChnStatus[midiCh].rpn |= data2; + break; + case MIDIEvents::MIDICC_RegisteredParameter_Coarse: + midiChnStatus[midiCh].rpn &= 0x7F; + midiChnStatus[midiCh].rpn |= (data2 << 7); + break; + + case 110: + isEMIDI = true; + break; + + case 111: + // Non-standard MIDI loop point. May conflict with Apogee EMIDI CCs (110/111), which is why we also check if CC 110 is ever used. + if(data2 == 0 && !isEMIDI) + { + Order().SetRestartPos(ord); + restartRow = row; + } + break; + + case 118: + // EMIDI Global Loop Start + isEMIDI = true; + isEMIDILoop = false; + Order().SetRestartPos(ord); + restartRow = row; + break; + + case 119: + // EMIDI Global Loop End + if(data2 == 0x7F) + { + isEMIDILoop = true; + isEMIDI = true; + std::tie(loopEndOrd, loopEndRow, std::ignore) = ModPositionFromTick(tick, 1); + } + break; + + case MIDIEvents::MIDICC_AllControllersOff: + midiChnStatus[midiCh].ResetAllControllers(); + break; + + // Bn.78.00: All Sound Off (GS) + // Bn.7B.00: All Notes Off (GM) + case MIDIEvents::MIDICC_AllSoundOff: + case MIDIEvents::MIDICC_AllNotesOff: + // All Notes Off + midiChnStatus[midiCh].sustain = false; + for(uint8 note = 0; note < 128; note++) + { + MIDINoteOff(midiChnStatus[midiCh], modChnStatus, note, delay, patRow, drumChns); + } + break; + case MIDIEvents::MIDICC_MonoOperation: + if(data2 == 0) + { + midiChnStatus[midiCh].monoMode = true; + } + break; + case MIDIEvents::MIDICC_PolyOperation: + if(data2 == 0) + { + midiChnStatus[midiCh].monoMode = false; + } + break; + } + } + break; + case 0xC0: // Program Change + midiChnStatus[midiCh].program = data1 & 0x7F; + break; + case 0xD0: // Channel aftertouch + break; + case 0xE0: // Pitch bend + midiChnStatus[midiCh].SetPitchbend(data1 | (track.ReadUint8() << 7)); + break; + case 0xF0: // General / Immediate + switch(midiCh) + { + case MIDIEvents::sysExStart: // SysEx + case MIDIEvents::sysExEnd: // SysEx (continued) + { + uint32 len; + track.ReadVarInt(len); + FileReader sysex = track.ReadChunk(len); + if(midiCh == MIDIEvents::sysExEnd) + break; + + if(sysex.ReadMagic("\x7F\x7F\x04\x01")) + { + // Master volume + uint8 volumeRaw[2]; + sysex.ReadArray(volumeRaw); + uint16 globalVol = volumeRaw[0] | (volumeRaw[1] << 7); + if(tick == 0) + { + m_nDefaultGlobalVolume = Util::muldivr_unsigned(globalVol, MAX_GLOBAL_VOLUME, 16383); + } else + { + patRow[globalVolChannel].command = CMD_GLOBALVOLUME; + patRow[globalVolChannel].param = static_cast<ModCommand::PARAM>(Util::muldivr_unsigned(globalVol, 128, 16383)); + } + } else + { + uint8 xg[7]; + sysex.ReadArray(xg); + if(!memcmp(xg, "\x43\x10\x4C\x00\x00\x7E\x00", 7)) + { + // XG System On + isXG = true; + } else if(!memcmp(xg, "\x43\x10\x4C\x00\x00\x06", 6)) + { + // XG Master Transpose + masterTranspose = static_cast<int8>(xg[6]) - 64; + } else if(!memcmp(xg, "\x41\x10\x42\x12\x40", 5) && (xg[5] & 0xF0) == 0x10 && xg[6] == 0x15) + { + // GS Drum Kit + uint8 chn = xg[5] & 0x0F; + if(chn == 0) + chn = 9; + else if(chn < 10) + chn--; + drumChns.set(chn, sysex.ReadUint8() != 0); + } + } + } + break; + case MIDIEvents::sysQuarterFrame: + track.Skip(1); + break; + case MIDIEvents::sysPositionPointer: + track.Skip(2); + break; + case MIDIEvents::sysSongSelect: + track.Skip(1); + break; + case MIDIEvents::sysTuneRequest: + case MIDIEvents::sysMIDIClock: + case MIDIEvents::sysMIDITick: + case MIDIEvents::sysStart: + case MIDIEvents::sysContinue: + case MIDIEvents::sysStop: + case MIDIEvents::sysActiveSense: + case MIDIEvents::sysReset: + break; + + default: + break; + } + break; + + default: + break; + } + } + + // Pitch bend any channels that haven't reached their target yet + // TODO: This is currently not called on any rows without events! + for(size_t chn = 0; chn < modChnStatus.size(); chn++) + { + ModChannelState &chnState = modChnStatus[chn]; + ModCommand &m = patRow[chn]; + uint8 midiCh = chnState.midiCh; + if(chnState.note == NOTE_NONE || m.command == CMD_S3MCMDEX || m.command == CMD_DELAYCUT || midiCh == ModChannelState::NOMIDI) + continue; + + int32 diff = midiChnStatus[midiCh].pitchbendMod - chnState.porta; + if(diff == 0) + continue; + + if(m.command == CMD_PORTAMENTODOWN || m.command == CMD_PORTAMENTOUP) + { + // First, undo the effect of an existing portamento command + int32 porta = 0; + if(m.param < 0xE0) + porta = m.param * 4 * (ticksPerRow - 1); + else if(m.param < 0xF0) + porta = (m.param & 0x0F); + else + porta = (m.param & 0x0F) * 4; + + if(m.command == CMD_PORTAMENTODOWN) + porta = -porta; + + diff += porta; + chnState.porta -= porta; + + if(diff == 0) + { + m.command = CMD_NONE; + continue; + } + } + + m.command = static_cast<ModCommand::COMMAND>(diff < 0 ? CMD_PORTAMENTODOWN : CMD_PORTAMENTOUP); + int32 absDiff = std::abs(diff); + int32 realDiff = 0; + if(absDiff < 16) + { + // Extra-fine slides can do this. + m.param = 0xE0 | static_cast<uint8>(absDiff); + realDiff = absDiff; + } else if(absDiff < 64) + { + // Fine slides can do this. + absDiff = std::min((absDiff + 3) / 4, 0x0F); + m.param = 0xF0 | static_cast<uint8>(absDiff); + realDiff = absDiff * 4; + } else + { + // Need a normal slide. + absDiff /= 4 * (ticksPerRow - 1); + LimitMax(absDiff, 0xDF); + m.param = static_cast<uint8>(absDiff); + realDiff = absDiff * 4 * (ticksPerRow - 1); + } + chnState.porta += realDiff * mpt::signum(diff); + } + + tick_t delta = 0; + if(track.ReadVarInt(delta) && track.CanRead(1)) + { + tracks[t].nextEvent += delta; + } else + { + finishedTracks++; + tracks[t].nextEvent = Util::MaxValueOfType(delta); + tracks[t].finished = true; + // Add another sub-song for type-2 files + if(isType2 && finishedTracks < numTracks) + { + if(Order.AddSequence() == SEQUENCEINDEX_INVALID) + break; + Order().clear(); + } + } + } + + if(isEMIDILoop) + isEMIDI = false; + + if(isEMIDI) + { + Order().SetRestartPos(0); + } + + if(loopEndOrd == ORDERINDEX_INVALID) + loopEndOrd = lastOrd; + if(loopEndRow == ROWINDEX_INVALID) + loopEndRow = lastRow; + + if(Order().IsValidPat(loopEndOrd)) + { + PATTERNINDEX lastPat = Order()[loopEndOrd]; + if(loopEndOrd == lastOrd) + Patterns[lastPat].Resize(loopEndRow + 1); + if(restartRow != ROWINDEX_INVALID && !isEMIDI) + { + Patterns[lastPat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, mpt::saturate_cast<ModCommand::PARAM>(restartRow)).Row(loopEndRow)); + if(ORDERINDEX restartPos = Order().GetRestartPos(); loopEndOrd != lastOrd || restartPos <= std::numeric_limits<ModCommand::PARAM>::max()) + Patterns[lastPat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, mpt::saturate_cast<ModCommand::PARAM>(restartPos)).Row(loopEndRow)); + } + } + Order.SetSequence(0); + + std::vector<CHANNELINDEX> channels; + channels.reserve(m_nChannels); + for(CHANNELINDEX i = 0; i < m_nChannels; i++) + { + if(modChnStatus[i].midiCh != ModChannelState::NOMIDI +#ifdef MODPLUG_TRACKER + || (GetpModDoc() != nullptr && !GetpModDoc()->IsChannelUnused(i)) +#endif // MODPLUG_TRACKER + ) + { + channels.push_back(i); + if(modChnStatus[i].midiCh != ModChannelState::NOMIDI) + ChnSettings[i].szName = MPT_AFORMAT("MIDI Ch {}")(1 + modChnStatus[i].midiCh); + else if(i == tempoChannel) + ChnSettings[i].szName = "Tempo"; + else if(i == globalVolChannel) + ChnSettings[i].szName = "Global Volume"; + } + } + if(channels.empty()) + return false; + +#ifdef MODPLUG_TRACKER + if(GetpModDoc() != nullptr) + { + // Keep MIDI channels in patterns neatly grouped + std::sort(channels.begin(), channels.end(), [&modChnStatus] (CHANNELINDEX c1, CHANNELINDEX c2) + { + if(modChnStatus[c1].midiCh == modChnStatus[c2].midiCh) + return c1 < c2; + return modChnStatus[c1].midiCh < modChnStatus[c2].midiCh; + }); + GetpModDoc()->ReArrangeChannels(channels, false); + GetpModDoc()->m_ShowSavedialog = true; + } + + std::unique_ptr<CDLSBank> cachedBank, embeddedBank; + + if(CDLSBank::IsDLSBank(file.GetOptionalFileName().value_or(P_("")))) + { + // Soundfont embedded in MIDI file + embeddedBank = std::make_unique<CDLSBank>(); + embeddedBank->Open(file.GetOptionalFileName().value_or(P_(""))); + } else + { + // Soundfont with same name as MIDI file + for(const auto &ext : { P_(".sf2"), P_(".sf3"), P_(".sf4"), P_(".sbk"), P_(".dls") }) + { + mpt::PathString filename = file.GetOptionalFileName().value_or(P_("")).ReplaceExt(ext); + if(filename.IsFile()) + { + embeddedBank = std::make_unique<CDLSBank>(); + if(embeddedBank->Open(filename)) + break; + } + } + } + ChangeModTypeTo(MOD_TYPE_MPT); + const MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + mpt::PathString cachedBankName; + // Load Instruments + for (INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) if (Instruments[ins]) + { + ModInstrument *pIns = Instruments[ins]; + uint32 midiCode = 0; + if(pIns->nMidiChannel == MIDI_DRUMCHANNEL) + midiCode = 0x80 | (pIns->nMidiDrumKey & 0x7F); + else if(pIns->nMidiProgram) + midiCode = (pIns->nMidiProgram - 1) & 0x7F; + + if(embeddedBank && embeddedBank->FindAndExtract(*this, ins, midiCode >= 0x80)) + { + continue; + } + + const mpt::PathString &midiMapName = midiLib[midiCode]; + if(!midiMapName.empty()) + { + // Load from DLS/SF2 Bank + if(CDLSBank::IsDLSBank(midiMapName)) + { + CDLSBank *dlsBank = nullptr; + if(cachedBank != nullptr && !mpt::PathString::CompareNoCase(cachedBankName, midiMapName)) + { + dlsBank = cachedBank.get(); + } else + { + cachedBank = std::make_unique<CDLSBank>(); + cachedBankName = midiMapName; + if(cachedBank->Open(midiMapName)) dlsBank = cachedBank.get(); + } + if(dlsBank) + { + dlsBank->FindAndExtract(*this, ins, midiCode >= 0x80); + } + } else + { + // Load from Instrument or Sample file + InputFile f(midiMapName, SettingCacheCompleteFileBeforeLoading()); + if(f.IsValid()) + { + FileReader insFile = GetFileReader(f); + if(ReadInstrumentFromFile(ins, insFile, false)) + { + mpt::PathString filename = midiMapName.GetFullFileName(); + pIns = Instruments[ins]; + if(!pIns->filename[0]) pIns->filename = filename.ToLocale(); + if(!pIns->name[0]) + { + if(midiCode < 0x80) + { + pIns->name = szMidiProgramNames[midiCode]; + } else + { + uint32 key = midiCode & 0x7F; + if((key >= 24) && (key < 24 + std::size(szMidiPercussionNames))) + pIns->name = szMidiPercussionNames[key - 24]; + } + } + } + } + } + } + } +#endif // MODPLUG_TRACKER + return true; +} + + +#else // !MODPLUG_TRACKER && !MPT_FUZZ_TRACKER + +bool CSoundFile::ReadMID(FileReader &/*file*/, ModLoadingFlags /*loadFlags*/) +{ + return false; +} + +#endif + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mo3.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mo3.cpp new file mode 100644 index 00000000..574adb0d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mo3.cpp @@ -0,0 +1,1984 @@ +/* + * Load_mo3.cpp + * ------------ + * Purpose: MO3 module loader. + * Notes : (currently none) + * Authors: Johannes Schultz / OpenMPT Devs + * Based on documentation and the decompression routines from the + * open-source UNMO3 project (https://github.com/lclevy/unmo3). + * The modified decompression code has been relicensed to the BSD + * license with permission from Laurent Clévy. + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "../common/ComponentManager.h" + +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#include "Tables.h" +#include "../common/version.h" +#include "mpt/audio/span.hpp" +#include "MPEGFrame.h" +#include "OggStream.h" + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) +#include <sstream> +#endif + +#if defined(MPT_WITH_VORBIS) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/codec.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif + +#if defined(MPT_WITH_VORBISFILE) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/vorbisfile.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#include "openmpt/soundbase/Copy.hpp" +#endif + +#ifdef MPT_WITH_STBVORBIS +#include <stb_vorbis/stb_vorbis.c> +#include "openmpt/soundbase/Copy.hpp" +#endif // MPT_WITH_STBVORBIS + + +OPENMPT_NAMESPACE_BEGIN + + + +struct MO3FileHeader +{ + enum MO3HeaderFlags + { + linearSlides = 0x0001, + isS3M = 0x0002, + s3mFastSlides = 0x0004, + isMTM = 0x0008, // Actually this is simply "not XM". But if none of the S3M, MOD and IT flags are set, it's an MTM. + s3mAmigaLimits = 0x0010, + // 0x20 and 0x40 have been used in old versions for things that can be inferred from the file format anyway. + // The official UNMO3 ignores them. + isMOD = 0x0080, + isIT = 0x0100, + instrumentMode = 0x0200, + itCompatGxx = 0x0400, + itOldFX = 0x0800, + modplugMode = 0x10000, + unknown = 0x20000, // Always set (internal BASS flag to designate modules) + modVBlank = 0x80000, + hasPlugins = 0x100000, + extFilterRange = 0x200000, + }; + + uint8le numChannels; // 1...64 (limited by channel panning and volume) + uint16le numOrders; + uint16le restartPos; + uint16le numPatterns; + uint16le numTracks; + uint16le numInstruments; + uint16le numSamples; + uint8le defaultSpeed; + uint8le defaultTempo; + uint32le flags; // See MO3HeaderFlags + uint8le globalVol; // 0...128 in IT, 0...64 in S3M + uint8le panSeparation; // 0...128 in IT + int8le sampleVolume; // Only used in IT + uint8le chnVolume[64]; // 0...64 + uint8le chnPan[64]; // 0...256, 127 = surround + uint8le sfxMacros[16]; + uint8le fixedMacros[128][2]; +}; + +MPT_BINARY_STRUCT(MO3FileHeader, 422) + + +struct MO3Envelope +{ + enum MO3EnvelopeFlags + { + envEnabled = 0x01, + envSustain = 0x02, + envLoop = 0x04, + envFilter = 0x10, + envCarry = 0x20, + }; + + uint8le flags; // See MO3EnvelopeFlags + uint8le numNodes; + uint8le sustainStart; + uint8le sustainEnd; + uint8le loopStart; + uint8le loopEnd; + int16le points[25][2]; + + // Convert MO3 envelope data into OpenMPT's internal envelope format + void ConvertToMPT(InstrumentEnvelope &mptEnv, uint8 envShift) const + { + if(flags & envEnabled) mptEnv.dwFlags.set(ENV_ENABLED); + if(flags & envSustain) mptEnv.dwFlags.set(ENV_SUSTAIN); + if(flags & envLoop) mptEnv.dwFlags.set(ENV_LOOP); + if(flags & envFilter) mptEnv.dwFlags.set(ENV_FILTER); + if(flags & envCarry) mptEnv.dwFlags.set(ENV_CARRY); + mptEnv.resize(std::min(numNodes.get(), uint8(25))); + mptEnv.nSustainStart = sustainStart; + mptEnv.nSustainEnd = sustainEnd; + mptEnv.nLoopStart = loopStart; + mptEnv.nLoopEnd = loopEnd; + for(uint32 ev = 0; ev < mptEnv.size(); ev++) + { + mptEnv[ev].tick = points[ev][0]; + if(ev > 0 && mptEnv[ev].tick < mptEnv[ev - 1].tick) + mptEnv[ev].tick = mptEnv[ev - 1].tick + 1; + mptEnv[ev].value = static_cast<uint8>(Clamp(points[ev][1] >> envShift, 0, 64)); + } + } +}; + +MPT_BINARY_STRUCT(MO3Envelope, 106) + + +struct MO3Instrument +{ + enum MO3InstrumentFlags + { + playOnMIDI = 0x01, + mute = 0x02, + }; + + uint32le flags; // See MO3InstrumentFlags + uint16le sampleMap[120][2]; + MO3Envelope volEnv; + MO3Envelope panEnv; + MO3Envelope pitchEnv; + struct XMVibratoSettings + { + uint8le type; + uint8le sweep; + uint8le depth; + uint8le rate; + } vibrato; // Applies to all samples of this instrument (XM) + uint16le fadeOut; + uint8le midiChannel; + uint8le midiBank; + uint8le midiPatch; + uint8le midiBend; + uint8le globalVol; // 0...128 + uint16le panning; // 0...256 if enabled, 0xFFFF otherwise + uint8le nna; + uint8le pps; + uint8le ppc; + uint8le dct; + uint8le dca; + uint16le volSwing; // 0...100 + uint16le panSwing; // 0...256 + uint8le cutoff; // 0...127, + 128 if enabled + uint8le resonance; // 0...127, + 128 if enabled + + // Convert MO3 instrument data into OpenMPT's internal instrument format + void ConvertToMPT(ModInstrument &mptIns, MODTYPE type) const + { + if(type == MOD_TYPE_XM) + { + for(size_t i = 0; i < 96; i++) + { + mptIns.Keyboard[i + 12] = sampleMap[i][1] + 1; + } + } else + { + for(size_t i = 0; i < 120; i++) + { + mptIns.NoteMap[i] = static_cast<uint8>(sampleMap[i][0] + NOTE_MIN); + mptIns.Keyboard[i] = sampleMap[i][1] + 1; + } + } + volEnv.ConvertToMPT(mptIns.VolEnv, 0); + panEnv.ConvertToMPT(mptIns.PanEnv, 0); + pitchEnv.ConvertToMPT(mptIns.PitchEnv, 5); + mptIns.nFadeOut = fadeOut; + + if(midiChannel >= 128) + { + // Plugin + mptIns.nMixPlug = midiChannel - 127; + } else if(midiChannel < 17 && (flags & playOnMIDI)) + { + // XM, or IT with recent encoder + mptIns.nMidiChannel = midiChannel + MidiFirstChannel; + } else if(midiChannel > 0 && midiChannel < 17) + { + // IT encoded with MO3 version prior to 2.4.1 (yes, channel 0 is represented the same way as "no channel") + mptIns.nMidiChannel = midiChannel + MidiFirstChannel; + } + if(mptIns.nMidiChannel != MidiNoChannel) + { + if(type == MOD_TYPE_XM) + { + mptIns.nMidiProgram = midiPatch + 1; + } else + { + if(midiBank < 128) + mptIns.wMidiBank = midiBank + 1; + if(midiPatch < 128) + mptIns.nMidiProgram = midiPatch + 1; + } + mptIns.midiPWD = midiBend; + } + + if(type == MOD_TYPE_IT) + mptIns.nGlobalVol = std::min(static_cast<uint8>(globalVol), uint8(128)) / 2u; + if(panning <= 256) + { + mptIns.nPan = panning; + mptIns.dwFlags.set(INS_SETPANNING); + } + mptIns.nNNA = static_cast<NewNoteAction>(nna.get()); + mptIns.nPPS = pps; + mptIns.nPPC = ppc; + mptIns.nDCT = static_cast<DuplicateCheckType>(dct.get()); + mptIns.nDNA = static_cast<DuplicateNoteAction>(dca.get()); + mptIns.nVolSwing = static_cast<uint8>(std::min(volSwing.get(), uint16(100))); + mptIns.nPanSwing = static_cast<uint8>(std::min(panSwing.get(), uint16(256)) / 4u); + mptIns.SetCutoff(cutoff & 0x7F, (cutoff & 0x80) != 0); + mptIns.SetResonance(resonance & 0x7F, (resonance & 0x80) != 0); + } +}; + +MPT_BINARY_STRUCT(MO3Instrument, 826) + + +struct MO3Sample +{ + enum MO3SampleFlags + { + smp16Bit = 0x01, + smpLoop = 0x10, + smpPingPongLoop = 0x20, + smpSustain = 0x100, + smpSustainPingPong = 0x200, + smpStereo = 0x400, + smpCompressionMPEG = 0x1000, // MPEG 1.0 / 2.0 / 2.5 sample + smpCompressionOgg = 0x1000 | 0x2000, // Ogg sample + smpSharedOgg = 0x1000 | 0x2000 | 0x4000, // Ogg sample with shared vorbis header + smpDeltaCompression = 0x2000, // Deltas + compression + smpDeltaPrediction = 0x4000, // Delta prediction + compression + smpOPLInstrument = 0x8000, // OPL patch data + smpCompressionMask = 0x1000 | 0x2000 | 0x4000 | 0x8000 + }; + + uint32le freqFinetune; // Frequency in S3M and IT, finetune (0...255) in MOD, MTM, XM + int8le transpose; + uint8le defaultVolume; // 0...64 + uint16le panning; // 0...256 if enabled, 0xFFFF otherwise + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint16le flags; // See MO3SampleFlags + uint8le vibType; + uint8le vibSweep; + uint8le vibDepth; + uint8le vibRate; + uint8le globalVol; // 0...64 in IT, in XM it represents the instrument number + uint32le sustainStart; + uint32le sustainEnd; + int32le compressedSize; + uint16le encoderDelay; // MP3: Ignore first n bytes of decoded output. Ogg: Shared Ogg header size + + // Convert MO3 sample data into OpenMPT's internal instrument format + void ConvertToMPT(ModSample &mptSmp, MODTYPE type, bool frequencyIsHertz) const + { + mptSmp.Initialize(); + mptSmp.SetDefaultCuePoints(); + if(type & (MOD_TYPE_IT | MOD_TYPE_S3M)) + { + if(frequencyIsHertz) + mptSmp.nC5Speed = freqFinetune; + else + mptSmp.nC5Speed = mpt::saturate_round<uint32>(8363.0 * std::pow(2.0, static_cast<int32>(freqFinetune + 1408) / 1536.0)); + } else + { + mptSmp.nFineTune = static_cast<int8>(freqFinetune); + if(type != MOD_TYPE_MTM) + mptSmp.nFineTune -= 128; + mptSmp.RelativeTone = transpose; + } + mptSmp.nVolume = std::min(defaultVolume.get(), uint8(64)) * 4u; + if(panning <= 256) + { + mptSmp.nPan = panning; + mptSmp.uFlags.set(CHN_PANNING); + } + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + if(flags & smpLoop) + mptSmp.uFlags.set(CHN_LOOP); + if(flags & smpPingPongLoop) + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & smpSustain) + mptSmp.uFlags.set(CHN_SUSTAINLOOP); + if(flags & smpSustainPingPong) + mptSmp.uFlags.set(CHN_PINGPONGSUSTAIN); + + mptSmp.nVibType = static_cast<VibratoType>(AutoVibratoIT2XM[vibType & 7]); + mptSmp.nVibSweep = vibSweep; + mptSmp.nVibDepth = vibDepth; + mptSmp.nVibRate = vibRate; + + if(type == MOD_TYPE_IT) + mptSmp.nGlobalVol = std::min(static_cast<uint8>(globalVol), uint8(64)); + mptSmp.nSustainStart = sustainStart; + mptSmp.nSustainEnd = sustainEnd; + } +}; + +MPT_BINARY_STRUCT(MO3Sample, 41) + + +// We need all this information for Ogg-compressed samples with shared headers: +// A shared header can be taken from a sample that has not been read yet, so +// we first need to read all headers, and then load the Ogg samples afterwards. +struct MO3SampleChunk +{ + FileReader chunk; + uint16 headerSize; + int16 sharedHeader; + MO3SampleChunk(const FileReader &chunk_ = FileReader(), uint16 headerSize_ = 0, int16 sharedHeader_ = 0) + : chunk(chunk_), headerSize(headerSize_), sharedHeader(sharedHeader_) {} +}; + + +// Unpack macros + +// shift control bits until it is empty: +// a 0 bit means literal : the next data byte is copied +// a 1 means compressed data +// then the next 2 bits determines what is the LZ ptr +// ('00' same as previous, else stored in stream) + +#define READ_CTRL_BIT \ + data <<= 1; \ + carry = (data > 0xFF); \ + data &= 0xFF; \ + if(data == 0) \ + { \ + uint8 nextByte; \ + if(!file.Read(nextByte)) \ + break; \ + data = nextByte; \ + data = (data << 1) + 1; \ + carry = (data > 0xFF); \ + data &= 0xFF; \ + } + +// length coded within control stream: +// most significant bit is 1 +// then the first bit of each bits pair (noted n1), +// until second bit is 0 (noted n0) + +#define DECODE_CTRL_BITS \ + { \ + strLen++; \ + do \ + { \ + READ_CTRL_BIT; \ + strLen = mpt::lshift_signed(strLen, 1) + carry; \ + READ_CTRL_BIT; \ + } while(carry); \ + } + + +static bool UnpackMO3Data(FileReader &file, std::vector<uint8> &uncompressed, const uint32 size) +{ + if(!size) + return false; + + uint16 data = 0; + int8 carry = 0; // x86 carry (used to propagate the most significant bit from one byte to another) + int32 strLen = 0; // length of previous string + int32 strOffset; // string offset + uint32 previousPtr = 0; + + // Read first uncompressed byte + uncompressed.push_back(file.ReadUint8()); + uint32 remain = size - 1; + + while(remain > 0) + { + READ_CTRL_BIT; + if(!carry) + { + // a 0 ctrl bit means 'copy', not compressed byte + if(uint8 b; file.Read(b)) + uncompressed.push_back(b); + else + break; + remain--; + } else + { + // a 1 ctrl bit means compressed bytes are following + uint8 lengthAdjust = 0; // length adjustment + DECODE_CTRL_BITS; // read length, and if strLen > 3 (coded using more than 1 bits pair) also part of the offset value + strLen -= 3; + if(strLen < 0) + { + // means LZ ptr with same previous relative LZ ptr (saved one) + strOffset = previousPtr; // restore previous Ptr + strLen++; + } else + { + // LZ ptr in ctrl stream + if(uint8 b; file.Read(b)) + strOffset = mpt::lshift_signed(strLen, 8) | b; // read less significant offset byte from stream + else + break; + strLen = 0; + strOffset = ~strOffset; + if(strOffset < -1280) + lengthAdjust++; + lengthAdjust++; // length is always at least 1 + if(strOffset < -32000) + lengthAdjust++; + previousPtr = strOffset; // save current Ptr + } + + // read the next 2 bits as part of strLen + READ_CTRL_BIT; + strLen = mpt::lshift_signed(strLen, 1) + carry; + READ_CTRL_BIT; + strLen = mpt::lshift_signed(strLen, 1) + carry; + if(strLen == 0) + { + // length does not fit in 2 bits + DECODE_CTRL_BITS; // decode length: 1 is the most significant bit, + strLen += 2; // then first bit of each bits pairs (noted n1), until n0. + } + strLen += lengthAdjust; // length adjustment + + if(remain < static_cast<uint32>(strLen) || strLen <= 0) + break; + if(strOffset >= 0 || -static_cast<ptrdiff_t>(uncompressed.size()) > strOffset) + break; + + // Copy previous string + // Need to do this in two steps as source and destination may overlap (e.g. strOffset = -1, strLen = 2 repeats last character twice) + uncompressed.insert(uncompressed.end(), strLen, 0); + remain -= strLen; + auto src = uncompressed.cend() - strLen + strOffset; + auto dst = uncompressed.end() - strLen; + do + { + strLen--; + *dst++ = *src++; + } while(strLen > 0); + } + } +#ifdef MPT_BUILD_FUZZER + // When using a fuzzer, we should not care if the decompressed buffer has the correct size. + // This makes finding new interesting test cases much easier. + return true; +#else + return remain == 0; +#endif // MPT_BUILD_FUZZER +} + + +struct MO3Delta8BitParams +{ + using sample_t = int8; + using unsigned_t = uint8; + static constexpr int shift = 7; + static constexpr uint8 dhInit = 4; + + static inline void Decode(FileReader &file, int8 &carry, uint16 &data, uint8 & /*dh*/, unsigned_t &val) + { + do + { + READ_CTRL_BIT; + val = (val << 1) + carry; + READ_CTRL_BIT; + } while(carry); + } +}; + +struct MO3Delta16BitParams +{ + using sample_t = int16; + using unsigned_t = uint16; + static constexpr int shift = 15; + static constexpr uint8 dhInit = 8; + + static inline void Decode(FileReader &file, int8 &carry, uint16 &data, uint8 &dh, unsigned_t &val) + { + if(dh < 5) + { + do + { + READ_CTRL_BIT; + val = (val << 1) + carry; + READ_CTRL_BIT; + val = (val << 1) + carry; + READ_CTRL_BIT; + } while(carry); + } else + { + do + { + READ_CTRL_BIT; + val = (val << 1) + carry; + READ_CTRL_BIT; + } while(carry); + } + } +}; + + +template <typename Properties> +static void UnpackMO3DeltaSample(FileReader &file, typename Properties::sample_t *dst, uint32 length, uint8 numChannels) +{ + uint8 dh = Properties::dhInit, cl = 0; + int8 carry = 0; + uint16 data = 0; + typename Properties::unsigned_t val; + typename Properties::sample_t previous = 0; + + for(uint8 chn = 0; chn < numChannels; chn++) + { + typename Properties::sample_t *p = dst + chn; + const typename Properties::sample_t *const pEnd = p + length * numChannels; + while(p < pEnd) + { + val = 0; + Properties::Decode(file, carry, data, dh, val); + cl = dh; + while(cl > 0) + { + READ_CTRL_BIT; + val = (val << 1) + carry; + cl--; + } + cl = 1; + if(val >= 4) + { + cl = Properties::shift; + while(((1 << cl) & val) == 0 && cl > 1) + cl--; + } + dh = dh + cl; + dh >>= 1; // next length in bits of encoded delta second part + carry = val & 1; // sign of delta 1=+, 0=not + val >>= 1; + if(carry == 0) + val = ~val; // negative delta + val += previous; // previous value + delta + *p = val; + p += numChannels; + previous = val; + } + } +} + + +template <typename Properties> +static void UnpackMO3DeltaPredictionSample(FileReader &file, typename Properties::sample_t *dst, uint32 length, uint8 numChannels) +{ + uint8 dh = Properties::dhInit, cl = 0; + int8 carry; + uint16 data = 0; + int32 next = 0; + typename Properties::unsigned_t val = 0; + typename Properties::sample_t sval = 0, delta = 0, previous = 0; + + for(uint8 chn = 0; chn < numChannels; chn++) + { + typename Properties::sample_t *p = dst + chn; + const typename Properties::sample_t *const pEnd = p + length * numChannels; + while(p < pEnd) + { + val = 0; + Properties::Decode(file, carry, data, dh, val); + cl = dh; // length in bits of: delta second part (right most bits of delta) and sign bit + while(cl > 0) + { + READ_CTRL_BIT; + val = (val << 1) + carry; + cl--; + } + cl = 1; + if(val >= 4) + { + cl = Properties::shift; + while(((1 << cl) & val) == 0 && cl > 1) + cl--; + } + dh = dh + cl; + dh >>= 1; // next length in bits of encoded delta second part + carry = val & 1; // sign of delta 1=+, 0=not + val >>= 1; + if(carry == 0) + val = ~val; // negative delta + + delta = static_cast<typename Properties::sample_t>(val); + val = val + static_cast<typename Properties::unsigned_t>(next); // predicted value + delta + *p = val; + p += numChannels; + sval = static_cast<typename Properties::sample_t>(val); + next = (sval * (1 << 1)) + (delta >> 1) - previous; // corrected next value + + Limit(next, std::numeric_limits<typename Properties::sample_t>::min(), std::numeric_limits<typename Properties::sample_t>::max()); + + previous = sval; + } + } +} + + +#undef READ_CTRL_BIT +#undef DECODE_CTRL_BITS + + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) + +static size_t VorbisfileFilereaderRead(void *ptr, size_t size, size_t nmemb, void *datasource) +{ + FileReader &file = *reinterpret_cast<FileReader *>(datasource); + return file.ReadRaw(mpt::span(mpt::void_cast<std::byte *>(ptr), size * nmemb)).size() / size; +} + +static int VorbisfileFilereaderSeek(void *datasource, ogg_int64_t offset, int whence) +{ + FileReader &file = *reinterpret_cast<FileReader *>(datasource); + switch(whence) + { + case SEEK_SET: + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + + case SEEK_CUR: + if(offset < 0) + { + if(offset == std::numeric_limits<ogg_int64_t>::min()) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(0 - offset)) + { + return -1; + } + return file.SkipBack(mpt::saturate_cast<FileReader::off_t>(0 - offset)) ? 0 : -1; + } else + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Skip(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + } + break; + + case SEEK_END: + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(file.GetLength() + offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(file.GetLength() + offset)) ? 0 : -1; + + default: + return -1; + } +} + +static long VorbisfileFilereaderTell(void *datasource) +{ + FileReader &file = *reinterpret_cast<FileReader *>(datasource); + FileReader::off_t result = file.GetPosition(); + if(!mpt::in_range<long>(result)) + { + return -1; + } + return static_cast<long>(result); +} + +#endif // MPT_WITH_VORBIS && MPT_WITH_VORBISFILE + + +struct MO3ContainerHeader +{ + char magic[3]; // MO3 + uint8le version; + uint32le musicSize; +}; + +MPT_BINARY_STRUCT(MO3ContainerHeader, 8) + + +static bool ValidateHeader(const MO3ContainerHeader &containerHeader) +{ + if(std::memcmp(containerHeader.magic, "MO3", 3)) + { + return false; + } + if(containerHeader.musicSize <= sizeof(MO3FileHeader) || containerHeader.musicSize >= uint32_max / 2u) + { + return false; + } + if(containerHeader.version > 5) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMO3(MemoryFileReader file, const uint64 *pfilesize) +{ + MO3ContainerHeader containerHeader; + if(!file.ReadStruct(containerHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(containerHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadMO3(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + MO3ContainerHeader containerHeader; + if(!file.ReadStruct(containerHeader)) + { + return false; + } + if(!ValidateHeader(containerHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + const uint8 version = containerHeader.version; + + uint32 compressedSize = uint32_max, reserveSize = 1024 * 1024; // Generous estimate based on biggest pre-v5 MO3s found in the wild (~350K music data) + if(version >= 5) + { + // Size of compressed music chunk + compressedSize = file.ReadUint32LE(); + if(!file.CanRead(compressedSize)) + return false; + // Generous estimate based on highest real-world compression ratio I found in a module (~20:1) + reserveSize = std::min(Util::MaxValueOfType(reserveSize) / 32u, compressedSize) * 32u; + } + + std::vector<uint8> musicData; + // We don't always reserve the whole uncompressed size as claimed by the module to guard against broken files + // that e.g. claim that the uncompressed size is 1GB while the MO3 file itself is only 100 bytes. + // As the LZ compression used in MO3 doesn't allow for establishing a clear upper bound for the maximum size, + // this is probably the only sensible way we can prevent DoS due to huge allocations. + musicData.reserve(std::min(reserveSize, containerHeader.musicSize.get())); + if(!UnpackMO3Data(file, musicData, containerHeader.musicSize)) + { + return false; + } + if(version >= 5) + { + file.Seek(12 + compressedSize); + } + + InitializeGlobals(); + InitializeChannels(); + + FileReader musicChunk(mpt::as_span(musicData)); + musicChunk.ReadNullString(m_songName); + musicChunk.ReadNullString(m_songMessage); + + MO3FileHeader fileHeader; + if(!musicChunk.ReadStruct(fileHeader) + || fileHeader.numChannels == 0 || fileHeader.numChannels > MAX_BASECHANNELS + || fileHeader.numInstruments >= MAX_INSTRUMENTS + || fileHeader.numSamples >= MAX_SAMPLES) + { + return false; + } + + m_nChannels = fileHeader.numChannels; + Order().SetRestartPos(fileHeader.restartPos); + m_nInstruments = fileHeader.numInstruments; + m_nSamples = fileHeader.numSamples; + m_nDefaultSpeed = fileHeader.defaultSpeed ? fileHeader.defaultSpeed : 6; + m_nDefaultTempo.Set(fileHeader.defaultTempo ? fileHeader.defaultTempo : 125, 0); + + if(fileHeader.flags & MO3FileHeader::isIT) + SetType(MOD_TYPE_IT); + else if(fileHeader.flags & MO3FileHeader::isS3M) + SetType(MOD_TYPE_S3M); + else if(fileHeader.flags & MO3FileHeader::isMOD) + SetType(MOD_TYPE_MOD); + else if(fileHeader.flags & MO3FileHeader::isMTM) + SetType(MOD_TYPE_MTM); + else + SetType(MOD_TYPE_XM); + + m_SongFlags.set(SONG_IMPORTED); + if(fileHeader.flags & MO3FileHeader::linearSlides) + m_SongFlags.set(SONG_LINEARSLIDES); + if((fileHeader.flags & MO3FileHeader::s3mAmigaLimits) && m_nType == MOD_TYPE_S3M) + m_SongFlags.set(SONG_AMIGALIMITS); + if((fileHeader.flags & MO3FileHeader::s3mFastSlides) && m_nType == MOD_TYPE_S3M) + m_SongFlags.set(SONG_FASTVOLSLIDES); + if(!(fileHeader.flags & MO3FileHeader::itOldFX) && m_nType == MOD_TYPE_IT) + m_SongFlags.set(SONG_ITOLDEFFECTS); + if(!(fileHeader.flags & MO3FileHeader::itCompatGxx) && m_nType == MOD_TYPE_IT) + m_SongFlags.set(SONG_ITCOMPATGXX); + if(fileHeader.flags & MO3FileHeader::extFilterRange) + m_SongFlags.set(SONG_EXFILTERRANGE); + if(fileHeader.flags & MO3FileHeader::modVBlank) + m_playBehaviour.set(kMODVBlankTiming); + + if(m_nType == MOD_TYPE_IT) + m_nDefaultGlobalVolume = std::min(fileHeader.globalVol.get(), uint8(128)) * 2; + else if(m_nType == MOD_TYPE_S3M) + m_nDefaultGlobalVolume = std::min(fileHeader.globalVol.get(), uint8(64)) * 4; + + if(fileHeader.sampleVolume < 0) + m_nSamplePreAmp = fileHeader.sampleVolume + 52; + else + m_nSamplePreAmp = static_cast<uint32>(std::exp(fileHeader.sampleVolume * 3.1 / 20.0)) + 51; + + // Header only has room for 64 channels, like in IT + const CHANNELINDEX headerChannels = std::min(m_nChannels, CHANNELINDEX(64)); + for(CHANNELINDEX i = 0; i < headerChannels; i++) + { + if(m_nType == MOD_TYPE_IT) + ChnSettings[i].nVolume = std::min(fileHeader.chnVolume[i].get(), uint8(64)); + if(m_nType != MOD_TYPE_XM) + { + if(fileHeader.chnPan[i] == 127) + ChnSettings[i].dwFlags = CHN_SURROUND; + else if(fileHeader.chnPan[i] == 255) + ChnSettings[i].nPan = 256; + else + ChnSettings[i].nPan = fileHeader.chnPan[i]; + } + } + + bool anyMacros = false; + for(uint32 i = 0; i < 16; i++) + { + if(fileHeader.sfxMacros[i]) + anyMacros = true; + } + for(uint32 i = 0; i < 128; i++) + { + if(fileHeader.fixedMacros[i][1]) + anyMacros = true; + } + + if(anyMacros) + { + for(uint32 i = 0; i < 16; i++) + { + if(fileHeader.sfxMacros[i]) + m_MidiCfg.SFx[i] = MPT_AFORMAT("F0F0{}z")(mpt::afmt::HEX0<2>(fileHeader.sfxMacros[i] - 1)); + else + m_MidiCfg.SFx[i] = ""; + } + for(uint32 i = 0; i < 128; i++) + { + if(fileHeader.fixedMacros[i][1]) + m_MidiCfg.Zxx[i] = MPT_AFORMAT("F0F0{}{}")(mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][1] - 1), mpt::afmt::HEX0<2>(fileHeader.fixedMacros[i][0].get())); + else + m_MidiCfg.Zxx[i] = ""; + } + } + + const bool hasOrderSeparators = !(m_nType & (MOD_TYPE_MOD | MOD_TYPE_XM)); + ReadOrderFromFile<uint8>(Order(), musicChunk, fileHeader.numOrders, hasOrderSeparators ? 0xFF : uint16_max, hasOrderSeparators ? 0xFE : uint16_max); + + // Track assignments for all patterns + FileReader trackChunk = musicChunk.ReadChunk(fileHeader.numPatterns * fileHeader.numChannels * sizeof(uint16)); + FileReader patLengthChunk = musicChunk.ReadChunk(fileHeader.numPatterns * sizeof(uint16)); + std::vector<FileReader> tracks(fileHeader.numTracks); + + for(auto &track : tracks) + { + uint32 len = musicChunk.ReadUint32LE(); + track = musicChunk.ReadChunk(len); + } + + /* + MO3 pattern commands: + 01 = Note + 02 = Instrument + 03 = CMD_ARPEGGIO (IT, XM, S3M, MOD, MTM) + 04 = CMD_PORTAMENTOUP (XM, MOD, MTM) [for formats with separate fine slides] + 05 = CMD_PORTAMENTODOWN (XM, MOD, MTM) [for formats with separate fine slides] + 06 = CMD_TONEPORTAMENTO (IT, XM, S3M, MOD, MTM) / VOLCMD_TONEPORTA (IT, XM) + 07 = CMD_VIBRATO (IT, XM, S3M, MOD, MTM) / VOLCMD_VIBRATODEPTH (IT) + 08 = CMD_TONEPORTAVOL (XM, MOD, MTM) + 09 = CMD_VIBRATOVOL (XM, MOD, MTM) + 0A = CMD_TREMOLO (IT, XM, S3M, MOD, MTM) + 0B = CMD_PANNING8 (IT, XM, S3M, MOD, MTM) / VOLCMD_PANNING (IT, XM) + 0C = CMD_OFFSET (IT, XM, S3M, MOD, MTM) + 0D = CMD_VOLUMESLIDE (XM, MOD, MTM) + 0E = CMD_POSITIONJUMP (IT, XM, S3M, MOD, MTM) + 0F = CMD_VOLUME (XM, MOD, MTM) / VOLCMD_VOLUME (IT, XM, S3M) + 10 = CMD_PATTERNBREAK (IT, XM, MOD, MTM) - BCD-encoded in MOD/XM/S3M/MTM! + 11 = CMD_MODCMDEX (XM, MOD, MTM) + 12 = CMD_TEMPO (XM, MOD, MTM) / CMD_SPEED (XM, MOD, MTM) + 13 = CMD_TREMOR (XM) + 14 = VOLCMD_VOLSLIDEUP x=X0 (XM) / VOLCMD_VOLSLIDEDOWN x=0X (XM) + 15 = VOLCMD_FINEVOLUP x=X0 (XM) / VOLCMD_FINEVOLDOWN x=0X (XM) + 16 = CMD_GLOBALVOLUME (IT, XM, S3M) + 17 = CMD_GLOBALVOLSLIDE (XM) + 18 = CMD_KEYOFF (XM) + 19 = CMD_SETENVPOSITION (XM) + 1A = CMD_PANNINGSLIDE (XM) + 1B = VOLCMD_PANSLIDELEFT x=0X (XM) / VOLCMD_PANSLIDERIGHT x=X0 (XM) + 1C = CMD_RETRIG (XM) + 1D = CMD_XFINEPORTAUPDOWN X1x (XM) + 1E = CMD_XFINEPORTAUPDOWN X2x (XM) + 1F = VOLCMD_VIBRATOSPEED (XM) + 20 = VOLCMD_VIBRATODEPTH (XM) + 21 = CMD_SPEED (IT, S3M) + 22 = CMD_VOLUMESLIDE (IT, S3M) + 23 = CMD_PORTAMENTODOWN (IT, S3M) [for formats without separate fine slides] + 24 = CMD_PORTAMENTOUP (IT, S3M) [for formats without separate fine slides] + 25 = CMD_TREMOR (IT, S3M) + 26 = CMD_RETRIG (IT, S3M) + 27 = CMD_FINEVIBRATO (IT, S3M) + 28 = CMD_CHANNELVOLUME (IT, S3M) + 29 = CMD_CHANNELVOLSLIDE (IT, S3M) + 2A = CMD_PANNINGSLIDE (IT, S3M) + 2B = CMD_S3MCMDEX (IT, S3M) + 2C = CMD_TEMPO (IT, S3M) + 2D = CMD_GLOBALVOLSLIDE (IT, S3M) + 2E = CMD_PANBRELLO (IT, XM, S3M) + 2F = CMD_MIDI (IT, XM, S3M) + 30 = VOLCMD_FINEVOLUP x=0...9 (IT) / VOLCMD_FINEVOLDOWN x=10...19 (IT) / VOLCMD_VOLSLIDEUP x=20...29 (IT) / VOLCMD_VOLSLIDEDOWN x=30...39 (IT) + 31 = VOLCMD_PORTADOWN (IT) + 32 = VOLCMD_PORTAUP (IT) + 33 = Unused XM command "W" (XM) + 34 = Any other IT volume column command to support OpenMPT extensions (IT) + 35 = CMD_XPARAM (IT) + 36 = CMD_SMOOTHMIDI (IT) + 37 = CMD_DELAYCUT (IT) + 38 = CMD_FINETUNE (MPTM) + 39 = CMD_FINETUNE_SMOOTH (MPTM) + + Note: S3M/IT CMD_TONEPORTAVOL / CMD_VIBRATOVOL are encoded as two commands: + K= 07 00 22 x + L= 06 00 22 x + */ + + static constexpr ModCommand::COMMAND effTrans[] = + { + CMD_NONE, CMD_NONE, CMD_NONE, CMD_ARPEGGIO, + CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO, CMD_VIBRATO, + CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO, CMD_PANNING8, + CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP, CMD_VOLUME, + CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_TEMPO, CMD_TREMOR, + VOLCMD_VOLSLIDEUP, VOLCMD_FINEVOLUP, CMD_GLOBALVOLUME, CMD_GLOBALVOLSLIDE, + CMD_KEYOFF, CMD_SETENVPOSITION, CMD_PANNINGSLIDE, VOLCMD_PANSLIDELEFT, + CMD_RETRIG, CMD_XFINEPORTAUPDOWN, CMD_XFINEPORTAUPDOWN, VOLCMD_VIBRATOSPEED, + VOLCMD_VIBRATODEPTH, CMD_SPEED, CMD_VOLUMESLIDE, CMD_PORTAMENTODOWN, + CMD_PORTAMENTOUP, CMD_TREMOR, CMD_RETRIG, CMD_FINEVIBRATO, + CMD_CHANNELVOLUME, CMD_CHANNELVOLSLIDE, CMD_PANNINGSLIDE, CMD_S3MCMDEX, + CMD_TEMPO, CMD_GLOBALVOLSLIDE, CMD_PANBRELLO, CMD_MIDI, + VOLCMD_FINEVOLUP, VOLCMD_PORTADOWN, VOLCMD_PORTAUP, CMD_NONE, + VOLCMD_OFFSET, CMD_XPARAM, CMD_SMOOTHMIDI, CMD_DELAYCUT, + CMD_FINETUNE, CMD_FINETUNE_SMOOTH, + }; + + uint8 noteOffset = NOTE_MIN; + if(m_nType == MOD_TYPE_MTM) + noteOffset = 13 + NOTE_MIN; + else if(m_nType != MOD_TYPE_IT) + noteOffset = 12 + NOTE_MIN; + bool onlyAmigaNotes = true; + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + const ROWINDEX numRows = patLengthChunk.ReadUint16LE(); + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows)) + continue; + + for(CHANNELINDEX chn = 0; chn < fileHeader.numChannels; chn++) + { + uint16 trackIndex = trackChunk.ReadUint16LE(); + if(trackIndex >= tracks.size()) + continue; + FileReader &track = tracks[trackIndex]; + track.Rewind(); + ROWINDEX row = 0; + ModCommand *patData = Patterns[pat].GetpModCommand(0, chn); + while(row < numRows) + { + const uint8 b = track.ReadUint8(); + if(!b) + break; + + const uint8 numCommands = (b & 0x0F), rep = (b >> 4); + ModCommand m = ModCommand::Empty(); + for(uint8 c = 0; c < numCommands; c++) + { + uint8 cmd[2]; + track.ReadArray(cmd); + + // Import pattern commands + switch(cmd[0]) + { + case 0x01: + // Note + m.note = cmd[1]; + if(m.note < 120) + m.note += noteOffset; + else if(m.note == 0xFF) + m.note = NOTE_KEYOFF; + else if(m.note == 0xFE) + m.note = NOTE_NOTECUT; + else + m.note = NOTE_FADE; + if(!m.IsAmigaNote()) + onlyAmigaNotes = false; + break; + case 0x02: + // Instrument + m.instr = cmd[1] + 1; + break; + case 0x06: + // Tone portamento + if(m.volcmd == VOLCMD_NONE && m_nType == MOD_TYPE_XM && !(cmd[1] & 0x0F)) + { + m.volcmd = VOLCMD_TONEPORTAMENTO; + m.vol = cmd[1] >> 4; + break; + } else if(m.volcmd == VOLCMD_NONE && m_nType == MOD_TYPE_IT) + { + for(uint8 i = 0; i < 10; i++) + { + if(ImpulseTrackerPortaVolCmd[i] == cmd[1]) + { + m.volcmd = VOLCMD_TONEPORTAMENTO; + m.vol = i; + break; + } + } + if(m.volcmd != VOLCMD_NONE) + break; + } + m.command = CMD_TONEPORTAMENTO; + m.param = cmd[1]; + break; + case 0x07: + // Vibrato + if(m.volcmd == VOLCMD_NONE && cmd[1] < 10 && m_nType == MOD_TYPE_IT) + { + m.volcmd = VOLCMD_VIBRATODEPTH; + m.vol = cmd[1]; + } else + { + m.command = CMD_VIBRATO; + m.param = cmd[1]; + } + break; + case 0x0B: + // Panning + if(m.volcmd == VOLCMD_NONE) + { + if(m_nType == MOD_TYPE_IT && cmd[1] == 0xFF) + { + m.volcmd = VOLCMD_PANNING; + m.vol = 64; + break; + } + if((m_nType == MOD_TYPE_IT && !(cmd[1] & 0x03)) + || (m_nType == MOD_TYPE_XM && !(cmd[1] & 0x0F))) + { + m.volcmd = VOLCMD_PANNING; + m.vol = cmd[1] / 4; + break; + } + } + m.command = CMD_PANNING8; + m.param = cmd[1]; + break; + case 0x0F: + // Volume + if(m_nType != MOD_TYPE_MOD && m.volcmd == VOLCMD_NONE && cmd[1] <= 64) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = cmd[1]; + } else + { + m.command = CMD_VOLUME; + m.param = cmd[1]; + } + break; + case 0x10: + // Pattern break + m.command = CMD_PATTERNBREAK; + m.param = cmd[1]; + if(m_nType != MOD_TYPE_IT) + m.param = ((m.param >> 4) * 10) + (m.param & 0x0F); + break; + case 0x12: + // Combined Tempo / Speed command + m.param = cmd[1]; + if(m.param < 0x20) + m.command = CMD_SPEED; + else + m.command = CMD_TEMPO; + break; + case 0x14: + case 0x15: + // XM volume column volume slides + if(cmd[1] & 0xF0) + { + m.volcmd = static_cast<ModCommand::VOLCMD>((cmd[0] == 0x14) ? VOLCMD_VOLSLIDEUP : VOLCMD_FINEVOLUP); + m.vol = cmd[1] >> 4; + } else + { + m.volcmd = static_cast<ModCommand::VOLCMD>((cmd[0] == 0x14) ? VOLCMD_VOLSLIDEDOWN : VOLCMD_FINEVOLDOWN); + m.vol = cmd[1] & 0x0F; + } + break; + case 0x1B: + // XM volume column panning slides + if(cmd[1] & 0xF0) + { + m.volcmd = VOLCMD_PANSLIDERIGHT; + m.vol = cmd[1] >> 4; + } else + { + m.volcmd = VOLCMD_PANSLIDELEFT; + m.vol = cmd[1] & 0x0F; + } + break; + case 0x1D: + // XM extra fine porta up + m.command = CMD_XFINEPORTAUPDOWN; + m.param = 0x10 | cmd[1]; + break; + case 0x1E: + // XM extra fine porta down + m.command = CMD_XFINEPORTAUPDOWN; + m.param = 0x20 | cmd[1]; + break; + case 0x1F: + case 0x20: + // XM volume column vibrato + m.volcmd = effTrans[cmd[0]]; + m.vol = cmd[1]; + break; + case 0x22: + // IT / S3M volume slide + if(m.command == CMD_TONEPORTAMENTO) + m.command = CMD_TONEPORTAVOL; + else if(m.command == CMD_VIBRATO) + m.command = CMD_VIBRATOVOL; + else + m.command = CMD_VOLUMESLIDE; + m.param = cmd[1]; + break; + case 0x30: + // IT volume column volume slides + m.vol = cmd[1] % 10; + if(cmd[1] < 10) + m.volcmd = VOLCMD_FINEVOLUP; + else if(cmd[1] < 20) + m.volcmd = VOLCMD_FINEVOLDOWN; + else if(cmd[1] < 30) + m.volcmd = VOLCMD_VOLSLIDEUP; + else if(cmd[1] < 40) + m.volcmd = VOLCMD_VOLSLIDEDOWN; + break; + case 0x31: + case 0x32: + // IT volume column portamento + m.volcmd = effTrans[cmd[0]]; + m.vol = cmd[1]; + break; + case 0x34: + // Any unrecognized IT volume command + if(cmd[1] >= 223 && cmd[1] <= 232) + { + m.volcmd = VOLCMD_OFFSET; + m.vol = cmd[1] - 223; + } + break; + default: + if(cmd[0] < std::size(effTrans)) + { + m.command = effTrans[cmd[0]]; + m.param = cmd[1]; + } + break; + } + } +#ifdef MODPLUG_TRACKER + if(m_nType == MOD_TYPE_MTM) + m.Convert(MOD_TYPE_MTM, MOD_TYPE_S3M, *this); +#endif + ROWINDEX targetRow = std::min(row + rep, numRows); + while(row < targetRow) + { + *patData = m; + patData += fileHeader.numChannels; + row++; + } + } + } + } + + if(GetType() == MOD_TYPE_MOD && GetNumChannels() == 4 && onlyAmigaNotes) + { + m_SongFlags.set(SONG_AMIGALIMITS | SONG_ISAMIGA); + } + + const bool isSampleMode = (m_nType != MOD_TYPE_XM && !(fileHeader.flags & MO3FileHeader::instrumentMode)); + std::vector<MO3Instrument::XMVibratoSettings> instrVibrato(m_nType == MOD_TYPE_XM ? m_nInstruments : 0); + for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) + { + ModInstrument *pIns = nullptr; + if(isSampleMode || (pIns = AllocateInstrument(ins)) == nullptr) + { + // Even in IT sample mode, instrument headers are still stored.... + while(musicChunk.ReadUint8() != 0) + ; + if(version >= 5) + { + while(musicChunk.ReadUint8() != 0) + ; + } + musicChunk.Skip(sizeof(MO3Instrument)); + continue; + } + + std::string name; + musicChunk.ReadNullString(name); + pIns->name = name; + if(version >= 5) + { + musicChunk.ReadNullString(name); + pIns->filename = name; + } + + MO3Instrument insHeader; + if(!musicChunk.ReadStruct(insHeader)) + break; + insHeader.ConvertToMPT(*pIns, m_nType); + + if(m_nType == MOD_TYPE_XM) + instrVibrato[ins - 1] = insHeader.vibrato; + } + if(isSampleMode) + m_nInstruments = 0; + + std::vector<MO3SampleChunk> sampleChunks(m_nSamples); + + const bool frequencyIsHertz = (version >= 5 || !(fileHeader.flags & MO3FileHeader::linearSlides)); + bool unsupportedSamples = false; + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + ModSample &sample = Samples[smp]; + std::string name; + musicChunk.ReadNullString(name); + m_szNames[smp] = name; + if(version >= 5) + { + musicChunk.ReadNullString(name); + sample.filename = name; + } + + MO3Sample smpHeader; + if(!musicChunk.ReadStruct(smpHeader)) + break; + smpHeader.ConvertToMPT(sample, m_nType, frequencyIsHertz); + + int16 sharedOggHeader = 0; + if(version >= 5 && (smpHeader.flags & MO3Sample::smpCompressionMask) == MO3Sample::smpSharedOgg) + { + sharedOggHeader = musicChunk.ReadInt16LE(); + } + + if(!(loadFlags & loadSampleData)) + continue; + + const uint32 compression = (smpHeader.flags & MO3Sample::smpCompressionMask); + if(!compression && smpHeader.compressedSize == 0) + { + // Uncompressed sample + SampleIO( + (smpHeader.flags & MO3Sample::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + (smpHeader.flags & MO3Sample::smpStereo) ? SampleIO::stereoSplit : SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + } else if(smpHeader.compressedSize < 0 && (smp + smpHeader.compressedSize) > 0) + { + // Duplicate sample + sample.CopyWaveform(Samples[smp + smpHeader.compressedSize]); + } else if(smpHeader.compressedSize > 0) + { + if(smpHeader.flags & MO3Sample::smp16Bit) + sample.uFlags.set(CHN_16BIT); + if(smpHeader.flags & MO3Sample::smpStereo) + sample.uFlags.set(CHN_STEREO); + + FileReader sampleData = file.ReadChunk(smpHeader.compressedSize); + const uint8 numChannels = sample.GetNumChannels(); + + if(compression == MO3Sample::smpDeltaCompression || compression == MO3Sample::smpDeltaPrediction) + { + // In the best case, MO3 compression represents each sample point as two bits. + // As a result, if we have a file length of n, we know that the sample can be at most n*4 sample points long. + auto maxLength = sampleData.GetLength(); + uint8 maxSamplesPerByte = 4 / numChannels; + if(Util::MaxValueOfType(maxLength) / maxSamplesPerByte >= maxLength) + maxLength *= maxSamplesPerByte; + else + maxLength = Util::MaxValueOfType(maxLength); + LimitMax(sample.nLength, mpt::saturate_cast<SmpLength>(maxLength)); + } + + if(compression == MO3Sample::smpDeltaCompression) + { + if(sample.AllocateSample()) + { + if(smpHeader.flags & MO3Sample::smp16Bit) + UnpackMO3DeltaSample<MO3Delta16BitParams>(sampleData, sample.sample16(), sample.nLength, numChannels); + else + UnpackMO3DeltaSample<MO3Delta8BitParams>(sampleData, sample.sample8(), sample.nLength, numChannels); + } + } else if(compression == MO3Sample::smpDeltaPrediction) + { + if(sample.AllocateSample()) + { + if(smpHeader.flags & MO3Sample::smp16Bit) + UnpackMO3DeltaPredictionSample<MO3Delta16BitParams>(sampleData, sample.sample16(), sample.nLength, numChannels); + else + UnpackMO3DeltaPredictionSample<MO3Delta8BitParams>(sampleData, sample.sample8(), sample.nLength, numChannels); + } + } else if(compression == MO3Sample::smpCompressionOgg || compression == MO3Sample::smpSharedOgg) + { + // Since shared Ogg headers can stem from a sample that has not been read yet, postpone Ogg import. + sampleChunks[smp - 1] = MO3SampleChunk(sampleData, smpHeader.encoderDelay, sharedOggHeader); + } else if(compression == MO3Sample::smpCompressionMPEG) + { + // Old MO3 encoders didn't remove LAME info frames. This is unfortunate since the encoder delay + // specified in the sample header does not take the gapless information from the LAME info frame + // into account. We should not depend on the MP3 decoder's capabilities to read or ignore such frames: + // - libmpg123 has MPG123_IGNORE_INFOFRAME but that requires API version 31 (mpg123 v1.14) or higher + // - Media Foundation does (currently) not read LAME gapless information at all + // So we just play safe and remove such frames. + FileReader mpegData(sampleData); + MPEGFrame frame(sampleData); + uint16 frameDelay = frame.numSamples * 2; + if(frame.isLAME && smpHeader.encoderDelay >= frameDelay) + { + // The info frame does not produce any output, but still counts towards the encoder delay. + smpHeader.encoderDelay -= frameDelay; + sampleData.Seek(frame.frameSize); + mpegData = sampleData.ReadChunk(sampleData.BytesLeft()); + } + + if(ReadMP3Sample(smp, mpegData, true, true) || ReadMediaFoundationSample(smp, mpegData, true)) + { + if(smpHeader.encoderDelay > 0 && smpHeader.encoderDelay < sample.GetSampleSizeInBytes()) + { + SmpLength delay = smpHeader.encoderDelay / sample.GetBytesPerSample(); + memmove(sample.sampleb(), sample.sampleb() + smpHeader.encoderDelay, sample.GetSampleSizeInBytes() - smpHeader.encoderDelay); + sample.nLength -= delay; + } + LimitMax(sample.nLength, smpHeader.length); + } else + { + unsupportedSamples = true; + } + } else if(compression == MO3Sample::smpOPLInstrument) + { + OPLPatch patch; + if(sampleData.ReadArray(patch)) + { + sample.SetAdlib(true, patch); + } + } else + { + unsupportedSamples = true; + } + } + } + + // Now we can load Ogg samples with shared headers. + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + MO3SampleChunk &sampleChunk = sampleChunks[smp - 1]; + // Is this an Ogg sample? + if(!sampleChunk.chunk.IsValid()) + continue; + + SAMPLEINDEX sharedOggHeader = smp + sampleChunk.sharedHeader; + // Which chunk are we going to read the header from? + // Note: Every Ogg stream has a unique serial number. + // stb_vorbis (currently) ignores this serial number so we can just stitch + // together our sample without adjusting the shared header's serial number. + const bool sharedHeader = sharedOggHeader != smp && sharedOggHeader > 0 && sharedOggHeader <= m_nSamples && sampleChunk.headerSize > 0; + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) + + std::vector<char> mergedData; + if(sharedHeader) + { + // Prepend the shared header to the actual sample data and adjust bitstream serial numbers. + // We do not handle multiple muxed logical streams as they do not exist in practice in mo3. + // We assume sequence numbers are consecutive at the end of the headers. + // Corrupted pages get dropped as required by Ogg spec. We cannot do any further sane parsing on them anyway. + // We do not match up multiple muxed stream properly as this would need parsing of actual packet data to determine or guess the codec. + // Ogg Vorbis files may contain at least an additional Ogg Skeleton stream. It is not clear whether these actually exist in MO3. + // We do not validate packet structure or logical bitstream structure (i.e. sequence numbers and granule positions). + + // TODO: At least handle Skeleton streams here, as they violate our stream ordering assumptions here. + +#if 0 + // This block may still turn out to be useful as it does a more thourough validation of the stream than the optimized version below. + + // We copy the whole data into a single consecutive buffer in order to keep things simple when interfacing libvorbisfile. + // We could in theory only adjust the header and pass 2 chunks to libvorbisfile. + // Another option would be to demux both chunks on our own (or using libogg) and pass the raw packet data to libvorbis directly. + + std::ostringstream mergedStream(std::ios::binary); + mergedStream.imbue(std::locale::classic()); + + sampleChunks[sharedOggHeader - 1].chunk.Rewind(); + FileReader sharedChunk = sampleChunks[sharedOggHeader - 1].chunk.ReadChunk(sampleChunk.headerSize); + sharedChunk.Rewind(); + + std::vector<uint32> streamSerials; + Ogg::PageInfo oggPageInfo; + std::vector<uint8> oggPageData; + + streamSerials.clear(); + while(Ogg::ReadPageAndSkipJunk(sharedChunk, oggPageInfo, oggPageData)) + { + auto it = std::find(streamSerials.begin(), streamSerials.end(), oggPageInfo.header.bitstream_serial_number); + if(it == streamSerials.end()) + { + streamSerials.push_back(oggPageInfo.header.bitstream_serial_number); + it = streamSerials.begin() + (streamSerials.size() - 1); + } + uint32 newSerial = it - streamSerials.begin() + 1; + oggPageInfo.header.bitstream_serial_number = newSerial; + Ogg::UpdatePageCRC(oggPageInfo, oggPageData); + Ogg::WritePage(mergedStream, oggPageInfo, oggPageData); + } + + streamSerials.clear(); + while(Ogg::ReadPageAndSkipJunk(sampleChunk.chunk, oggPageInfo, oggPageData)) + { + auto it = std::find(streamSerials.begin(), streamSerials.end(), oggPageInfo.header.bitstream_serial_number); + if(it == streamSerials.end()) + { + streamSerials.push_back(oggPageInfo.header.bitstream_serial_number); + it = streamSerials.begin() + (streamSerials.size() - 1); + } + uint32 newSerial = it - streamSerials.begin() + 1; + oggPageInfo.header.bitstream_serial_number = newSerial; + Ogg::UpdatePageCRC(oggPageInfo, oggPageData); + Ogg::WritePage(mergedStream, oggPageInfo, oggPageData); + } + + std::string mergedStreamData = mergedStream.str(); + mergedData.insert(mergedData.end(), mergedStreamData.begin(), mergedStreamData.end()); + +#else + + // We assume same ordering of streams in both header and data if + // multiple streams are present. + + std::ostringstream mergedStream(std::ios::binary); + mergedStream.imbue(std::locale::classic()); + + sampleChunks[sharedOggHeader - 1].chunk.Rewind(); + FileReader sharedChunk = sampleChunks[sharedOggHeader - 1].chunk.ReadChunk(sampleChunk.headerSize); + sharedChunk.Rewind(); + + std::vector<uint32> dataStreamSerials; + std::vector<uint32> headStreamSerials; + Ogg::PageInfo oggPageInfo; + std::vector<uint8> oggPageData; + + // Gather bitstream serial numbers form sample data chunk + dataStreamSerials.clear(); + while(Ogg::ReadPageAndSkipJunk(sampleChunk.chunk, oggPageInfo, oggPageData)) + { + if(!mpt::contains(dataStreamSerials, oggPageInfo.header.bitstream_serial_number)) + { + dataStreamSerials.push_back(oggPageInfo.header.bitstream_serial_number); + } + } + + // Apply the data bitstream serial numbers to the header + headStreamSerials.clear(); + while(Ogg::ReadPageAndSkipJunk(sharedChunk, oggPageInfo, oggPageData)) + { + auto it = std::find(headStreamSerials.begin(), headStreamSerials.end(), oggPageInfo.header.bitstream_serial_number); + if(it == headStreamSerials.end()) + { + headStreamSerials.push_back(oggPageInfo.header.bitstream_serial_number); + it = headStreamSerials.begin() + (headStreamSerials.size() - 1); + } + uint32 newSerial = 0; + if(dataStreamSerials.size() >= static_cast<std::size_t>(it - headStreamSerials.begin())) + { + // Found corresponding stream in data chunk. + newSerial = dataStreamSerials[it - headStreamSerials.begin()]; + } else + { + // No corresponding stream in data chunk. Find a free serialno. + std::size_t extraIndex = (it - headStreamSerials.begin()) - dataStreamSerials.size(); + for(newSerial = 1; newSerial < 0xffffffffu; ++newSerial) + { + if(!mpt::contains(dataStreamSerials, newSerial)) + { + extraIndex -= 1; + } + if(extraIndex == 0) + { + break; + } + } + } + oggPageInfo.header.bitstream_serial_number = newSerial; + Ogg::UpdatePageCRC(oggPageInfo, oggPageData); + Ogg::WritePage(mergedStream, oggPageInfo, oggPageData); + } + + if(headStreamSerials.size() > 1) + { + AddToLog(LogWarning, MPT_UFORMAT("Sample {}: Ogg Vorbis data with shared header and multiple logical bitstreams in header chunk found. This may be handled incorrectly.")(smp)); + } else if(dataStreamSerials.size() > 1) + { + AddToLog(LogWarning, MPT_UFORMAT("Sample {}: Ogg Vorbis sample with shared header and multiple logical bitstreams found. This may be handled incorrectly.")(smp)); + } else if((dataStreamSerials.size() == 1) && (headStreamSerials.size() == 1) && (dataStreamSerials[0] != headStreamSerials[0])) + { + AddToLog(LogInformation, MPT_UFORMAT("Sample {}: Ogg Vorbis data with shared header and different logical bitstream serials found.")(smp)); + } + + std::string mergedStreamData = mergedStream.str(); + mergedData.insert(mergedData.end(), mergedStreamData.begin(), mergedStreamData.end()); + + sampleChunk.chunk.Rewind(); + FileReader::PinnedView sampleChunkView = sampleChunk.chunk.GetPinnedView(); + mpt::span<const char> sampleChunkViewSpan = mpt::byte_cast<mpt::span<const char>>(sampleChunkView.span()); + mergedData.insert(mergedData.end(), sampleChunkViewSpan.begin(), sampleChunkViewSpan.end()); + +#endif + } + FileReader mergedDataChunk(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(mergedData))); + + FileReader &sampleData = sharedHeader ? mergedDataChunk : sampleChunk.chunk; + FileReader &headerChunk = sampleData; + +#else // !(MPT_WITH_VORBIS && MPT_WITH_VORBISFILE) + + FileReader &sampleData = sampleChunk.chunk; + FileReader &headerChunk = sharedHeader ? sampleChunks[sharedOggHeader - 1].chunk : sampleData; +#if defined(MPT_WITH_STBVORBIS) + std::size_t initialRead = sharedHeader ? sampleChunk.headerSize : headerChunk.GetLength(); +#endif // MPT_WITH_STBVORBIS + +#endif // MPT_WITH_VORBIS && MPT_WITH_VORBISFILE + + headerChunk.Rewind(); + if(sharedHeader && !headerChunk.CanRead(sampleChunk.headerSize)) + continue; + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) + + ov_callbacks callbacks = { + &VorbisfileFilereaderRead, + &VorbisfileFilereaderSeek, + nullptr, + &VorbisfileFilereaderTell}; + OggVorbis_File vf; + MemsetZero(vf); + if(ov_open_callbacks(&sampleData, &vf, nullptr, 0, callbacks) == 0) + { + if(ov_streams(&vf) == 1) + { // we do not support chained vorbis samples + vorbis_info *vi = ov_info(&vf, -1); + if(vi && vi->rate > 0 && vi->channels > 0) + { + ModSample &sample = Samples[smp]; + sample.AllocateSample(); + SmpLength offset = 0; + int channels = vi->channels; + int current_section = 0; + long decodedSamples = 0; + bool eof = false; + while(!eof && offset < sample.nLength && sample.HasSampleData()) + { + float **output = nullptr; + long ret = ov_read_float(&vf, &output, 1024, ¤t_section); + if(ret == 0) + { + eof = true; + } else if(ret < 0) + { + // stream error, just try to continue + } else + { + decodedSamples = ret; + LimitMax(decodedSamples, mpt::saturate_cast<long>(sample.nLength - offset)); + if(decodedSamples > 0 && channels == sample.GetNumChannels()) + { + if(sample.uFlags[CHN_16BIT]) + { + CopyAudio(mpt::audio_span_interleaved(sample.sample16() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } else + { + CopyAudio(mpt::audio_span_interleaved(sample.sample8() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } + } + offset += decodedSamples; + } + } + } else + { + unsupportedSamples = true; + } + } else + { + AddToLog(LogWarning, MPT_UFORMAT("Sample {}: Unsupported Ogg Vorbis chained stream found.")(smp)); + unsupportedSamples = true; + } + ov_clear(&vf); + } else + { + unsupportedSamples = true; + } + +#elif defined(MPT_WITH_STBVORBIS) + + // NOTE/TODO: stb_vorbis does not handle inferred negative PCM sample + // position at stream start. (See + // <https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-132000A.2>). + // This means that, for remuxed and re-aligned/cutted (at stream start) + // Vorbis files, stb_vorbis will include superfluous samples at the + // beginning. MO3 files with this property are yet to be spotted in the + // wild, thus, this behaviour is currently not problematic. + + int consumed = 0, error = 0; + stb_vorbis *vorb = nullptr; + if(sharedHeader) + { + FileReader::PinnedView headChunkView = headerChunk.GetPinnedView(initialRead); + vorb = stb_vorbis_open_pushdata(mpt::byte_cast<const unsigned char *>(headChunkView.data()), mpt::saturate_cast<int>(headChunkView.size()), &consumed, &error, nullptr); + headerChunk.Skip(consumed); + } + FileReader::PinnedView sampleDataView = sampleData.GetPinnedView(); + const std::byte *data = sampleDataView.data(); + std::size_t dataLeft = sampleDataView.size(); + if(!sharedHeader) + { + vorb = stb_vorbis_open_pushdata(mpt::byte_cast<const unsigned char *>(data), mpt::saturate_cast<int>(dataLeft), &consumed, &error, nullptr); + sampleData.Skip(consumed); + data += consumed; + dataLeft -= consumed; + } + if(vorb) + { + // Header has been read, proceed to reading the sample data + ModSample &sample = Samples[smp]; + sample.AllocateSample(); + SmpLength offset = 0; + while((error == VORBIS__no_error || (error == VORBIS_need_more_data && dataLeft > 0)) + && offset < sample.nLength && sample.HasSampleData()) + { + int channels = 0, decodedSamples = 0; + float **output; + consumed = stb_vorbis_decode_frame_pushdata(vorb, mpt::byte_cast<const unsigned char *>(data), mpt::saturate_cast<int>(dataLeft), &channels, &output, &decodedSamples); + sampleData.Skip(consumed); + data += consumed; + dataLeft -= consumed; + LimitMax(decodedSamples, mpt::saturate_cast<int>(sample.nLength - offset)); + if(decodedSamples > 0 && channels == sample.GetNumChannels()) + { + if(sample.uFlags[CHN_16BIT]) + { + CopyAudio(mpt::audio_span_interleaved(sample.sample16() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } else + { + CopyAudio(mpt::audio_span_interleaved(sample.sample8() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } + } + offset += decodedSamples; + error = stb_vorbis_get_error(vorb); + } + stb_vorbis_close(vorb); + } else + { + unsupportedSamples = true; + } + +#else // !VORBIS + + unsupportedSamples = true; + +#endif // VORBIS + } + } + + if(m_nType == MOD_TYPE_XM) + { + // Transfer XM instrument vibrato to samples + for(INSTRUMENTINDEX ins = 0; ins < m_nInstruments; ins++) + { + PropagateXMAutoVibrato(ins + 1, static_cast<VibratoType>(instrVibrato[ins].type.get()), instrVibrato[ins].sweep, instrVibrato[ins].depth, instrVibrato[ins].rate); + } + } + + if((fileHeader.flags & MO3FileHeader::hasPlugins) && musicChunk.CanRead(1)) + { + // Plugin data + uint8 pluginFlags = musicChunk.ReadUint8(); + if(pluginFlags & 1) + { + // Channel plugins + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].nMixPlugin = static_cast<PLUGINDEX>(musicChunk.ReadUint32LE()); + } + } + while(musicChunk.CanRead(1)) + { + PLUGINDEX plug = musicChunk.ReadUint8(); + if(!plug) + break; + FileReader pluginChunk = musicChunk.ReadChunk(musicChunk.ReadUint32LE()); +#ifndef NO_PLUGINS + if(plug <= MAX_MIXPLUGINS) + { + ReadMixPluginChunk(pluginChunk, m_MixPlugins[plug - 1]); + } +#endif // NO_PLUGINS + } + } + + mpt::ustring madeWithTracker; + uint16 cwtv = 0; + uint16 cmwt = 0; + while(musicChunk.CanRead(8)) + { + uint32 id = musicChunk.ReadUint32LE(); + uint32 len = musicChunk.ReadUint32LE(); + FileReader chunk = musicChunk.ReadChunk(len); + switch(id) + { + case MagicLE("VERS"): + // Tracker magic bytes (depending on format) + switch(m_nType) + { + case MOD_TYPE_IT: + cwtv = chunk.ReadUint16LE(); + cmwt = chunk.ReadUint16LE(); + /*switch(cwtv >> 12) + { + + }*/ + break; + case MOD_TYPE_S3M: + cwtv = chunk.ReadUint16LE(); + break; + case MOD_TYPE_XM: + chunk.ReadString<mpt::String::spacePadded>(madeWithTracker, mpt::Charset::CP437, std::min(FileReader::off_t(32), chunk.GetLength())); + break; + case MOD_TYPE_MTM: + { + uint8 mtmVersion = chunk.ReadUint8(); + madeWithTracker = MPT_UFORMAT("MultiTracker {}.{}")(mtmVersion >> 4, mtmVersion & 0x0F); + } + break; + default: + break; + } + break; + case MagicLE("PRHI"): + m_nDefaultRowsPerBeat = chunk.ReadUint8(); + m_nDefaultRowsPerMeasure = chunk.ReadUint8(); + break; + case MagicLE("MIDI"): + // Full MIDI config + chunk.ReadStruct<MIDIMacroConfigData>(m_MidiCfg); + m_MidiCfg.Sanitize(); + break; + case MagicLE("OMPT"): + // Read pattern names: "PNAM" + if(chunk.ReadMagic("PNAM")) + { + FileReader patterns = chunk.ReadChunk(chunk.ReadUint32LE()); + const PATTERNINDEX namedPats = std::min(static_cast<PATTERNINDEX>(patterns.GetLength() / MAX_PATTERNNAME), Patterns.Size()); + + for(PATTERNINDEX pat = 0; pat < namedPats; pat++) + { + char patName[MAX_PATTERNNAME]; + patterns.ReadString<mpt::String::maybeNullTerminated>(patName, MAX_PATTERNNAME); + Patterns[pat].SetName(patName); + } + } + + // Read channel names: "CNAM" + if(chunk.ReadMagic("CNAM")) + { + FileReader channels = chunk.ReadChunk(chunk.ReadUint32LE()); + const CHANNELINDEX namedChans = std::min(static_cast<CHANNELINDEX>(channels.GetLength() / MAX_CHANNELNAME), GetNumChannels()); + for(CHANNELINDEX chn = 0; chn < namedChans; chn++) + { + channels.ReadString<mpt::String::maybeNullTerminated>(ChnSettings[chn].szName, MAX_CHANNELNAME); + } + } + + LoadExtendedInstrumentProperties(chunk); + LoadExtendedSongProperties(chunk, true); + if(cwtv > 0x0889 && cwtv <= 0x8FF) + { + m_nType = MOD_TYPE_MPT; + LoadMPTMProperties(chunk, cwtv); + } + + if(m_dwLastSavedWithVersion) + { + madeWithTracker = U_("OpenMPT ") + mpt::ufmt::val(m_dwLastSavedWithVersion); + } + break; + } + } + + if((GetType() == MOD_TYPE_IT && cwtv >= 0x0100 && cwtv < 0x0214) + || (GetType() == MOD_TYPE_S3M && cwtv >= 0x3100 && cwtv < 0x3214) + || (GetType() == MOD_TYPE_S3M && cwtv >= 0x1300 && cwtv < 0x1320)) + { + // Ignore MIDI data in files made with IT older than version 2.14 and old ST3 versions. + m_MidiCfg.ClearZxxMacros(); + } + + if(fileHeader.flags & MO3FileHeader::modplugMode) + { + // Apply some old ModPlug (mis-)behaviour + if(!m_dwLastSavedWithVersion) + { + // These fixes are only applied when the OpenMPT version number is not known, as otherwise the song upgrade feature will take care of it. + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(ModInstrument *ins = Instruments[i]) + { + // Fix pitch / filter envelope being shortened by one tick (for files before v1.20) + ins->GetEnvelope(ENV_PITCH).Convert(MOD_TYPE_XM, GetType()); + // Fix excessive pan swing range (for files before v1.26) + ins->nPanSwing = (ins->nPanSwing + 3) / 4u; + } + } + } + if(m_dwLastSavedWithVersion < MPT_V("1.18.00.00")) + { + m_playBehaviour.reset(kITOffset); + m_playBehaviour.reset(kFT2ST3OffsetOutOfRange); + } + if(m_dwLastSavedWithVersion < MPT_V("1.23.00.00")) + m_playBehaviour.reset(kFT2Periods); + if(m_dwLastSavedWithVersion < MPT_V("1.26.00.00")) + m_playBehaviour.reset(kITInstrWithNoteOff); + } + + if(madeWithTracker.empty()) + madeWithTracker = MPT_UFORMAT("MO3 v{}")(version); + else + madeWithTracker = MPT_UFORMAT("MO3 v{} ({})")(version, madeWithTracker); + + m_modFormat.formatName = MPT_UFORMAT("Un4seen MO3 v{}")(version); + m_modFormat.type = U_("mo3"); + + switch(GetType()) + { + case MOD_TYPE_MTM: + m_modFormat.originalType = U_("mtm"); + m_modFormat.originalFormatName = U_("MultiTracker"); + break; + case MOD_TYPE_MOD: + m_modFormat.originalType = U_("mod"); + m_modFormat.originalFormatName = U_("Generic MOD"); + break; + case MOD_TYPE_XM: + m_modFormat.originalType = U_("xm"); + m_modFormat.originalFormatName = U_("FastTracker 2"); + break; + case MOD_TYPE_S3M: + m_modFormat.originalType = U_("s3m"); + m_modFormat.originalFormatName = U_("Scream Tracker 3"); + break; + case MOD_TYPE_IT: + m_modFormat.originalType = U_("it"); + if(cmwt) + m_modFormat.originalFormatName = MPT_UFORMAT("Impulse Tracker {}.{}")(cmwt >> 8, mpt::ufmt::hex0<2>(cmwt & 0xFF)); + else + m_modFormat.originalFormatName = U_("Impulse Tracker"); + break; + case MOD_TYPE_MPT: + m_modFormat.originalType = U_("mptm"); + m_modFormat.originalFormatName = U_("OpenMPT MPTM"); + break; + default: + MPT_ASSERT_NOTREACHED(); + } + m_modFormat.madeWithTracker = std::move(madeWithTracker); + if(m_dwLastSavedWithVersion) + m_modFormat.charset = mpt::Charset::Windows1252; + else if(GetType() == MOD_TYPE_MOD) + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + else + m_modFormat.charset = mpt::Charset::CP437; + + if(unsupportedSamples) + { + AddToLog(LogWarning, U_("Some compressed samples could not be loaded because they use an unsupported codec.")); + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mod.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mod.cpp new file mode 100644 index 00000000..6c36408a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mod.cpp @@ -0,0 +1,2434 @@ +/* + * Load_mod.cpp + * ------------ + * Purpose: MOD / NST (ProTracker / NoiseTracker), M15 / STK (Ultimate Soundtracker / Soundtracker) and ST26 (SoundTracker 2.6 / Ice Tracker) module loader / saver + * Notes : "2000 LOC for processing MOD files?!" you say? Well, this file also contains loaders for some formats that are almost identical to MOD, and extensive + * heuristics for more or less broken MOD files and files saved with tons of different trackers, to allow for the most optimal playback. + * 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 "Tables.h" +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/mptFileIO.h" +#endif +#ifdef MPT_EXTERNAL_SAMPLES +// For loading external data in Startrekker files +#include "../common/mptPathString.h" +#endif // MPT_EXTERNAL_SAMPLES + +OPENMPT_NAMESPACE_BEGIN + +void CSoundFile::ConvertModCommand(ModCommand &m) +{ + switch(m.command) + { + case 0x00: if(m.param) m.command = CMD_ARPEGGIO; break; + case 0x01: m.command = CMD_PORTAMENTOUP; break; + case 0x02: m.command = CMD_PORTAMENTODOWN; break; + case 0x03: m.command = CMD_TONEPORTAMENTO; break; + case 0x04: m.command = CMD_VIBRATO; break; + case 0x05: m.command = CMD_TONEPORTAVOL; break; + case 0x06: m.command = CMD_VIBRATOVOL; break; + case 0x07: m.command = CMD_TREMOLO; break; + case 0x08: m.command = CMD_PANNING8; break; + case 0x09: m.command = CMD_OFFSET; break; + case 0x0A: m.command = CMD_VOLUMESLIDE; break; + case 0x0B: m.command = CMD_POSITIONJUMP; break; + case 0x0C: m.command = CMD_VOLUME; break; + case 0x0D: m.command = CMD_PATTERNBREAK; m.param = ((m.param >> 4) * 10) + (m.param & 0x0F); break; + case 0x0E: m.command = CMD_MODCMDEX; break; + case 0x0F: + // For a very long time, this code imported 0x20 as CMD_SPEED for MOD files, but this seems to contradict + // pretty much the majority of other MOD player out there. + // 0x20 is Speed: Impulse Tracker, Scream Tracker, old ModPlug + // 0x20 is Tempo: ProTracker, XMPlay, Imago Orpheus, Cubic Player, ChibiTracker, BeRoTracker, DigiTrakker, DigiTrekker, Disorder Tracker 2, DMP, Extreme's Tracker, ... + if(m.param < 0x20) + m.command = CMD_SPEED; + else + m.command = CMD_TEMPO; + break; + + // Extension for XM extended effects + case 'G' - 55: m.command = CMD_GLOBALVOLUME; break; //16 + case 'H' - 55: m.command = CMD_GLOBALVOLSLIDE; break; + case 'K' - 55: m.command = CMD_KEYOFF; break; + case 'L' - 55: m.command = CMD_SETENVPOSITION; break; + case 'P' - 55: m.command = CMD_PANNINGSLIDE; break; + case 'R' - 55: m.command = CMD_RETRIG; break; + case 'T' - 55: m.command = CMD_TREMOR; break; + case 'W' - 55: m.command = CMD_DUMMY; break; + case 'X' - 55: m.command = CMD_XFINEPORTAUPDOWN; break; + case 'Y' - 55: m.command = CMD_PANBRELLO; break; // 34 + case 'Z' - 55: m.command = CMD_MIDI; break; // 35 + case '\\' - 56: m.command = CMD_SMOOTHMIDI; break; // 36 - note: this is actually displayed as "-" in FT2, but seems to be doing nothing. + case 37: m.command = CMD_SMOOTHMIDI; break; // BeRoTracker uses this for smooth MIDI macros for some reason; in old OpenMPT versions this was reserved for the unimplemented "velocity" command + case '#' + 3: m.command = CMD_XPARAM; break; // 38 + default: m.command = CMD_NONE; + } +} + +#ifndef MODPLUG_NO_FILESAVE + +void CSoundFile::ModSaveCommand(uint8 &command, uint8 ¶m, bool toXM, bool compatibilityExport) const +{ + switch(command) + { + case CMD_NONE: command = param = 0; break; + case CMD_ARPEGGIO: command = 0; break; + case CMD_PORTAMENTOUP: + if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_STM|MOD_TYPE_MPT)) + { + if ((param & 0xF0) == 0xE0) { command = 0x0E; param = ((param & 0x0F) >> 2) | 0x10; break; } + else if ((param & 0xF0) == 0xF0) { command = 0x0E; param &= 0x0F; param |= 0x10; break; } + } + command = 0x01; + break; + case CMD_PORTAMENTODOWN: + if(GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_STM|MOD_TYPE_MPT)) + { + if ((param & 0xF0) == 0xE0) { command = 0x0E; param= ((param & 0x0F) >> 2) | 0x20; break; } + else if ((param & 0xF0) == 0xF0) { command = 0x0E; param &= 0x0F; param |= 0x20; break; } + } + command = 0x02; + break; + case CMD_TONEPORTAMENTO: command = 0x03; break; + case CMD_VIBRATO: command = 0x04; break; + case CMD_TONEPORTAVOL: command = 0x05; break; + case CMD_VIBRATOVOL: command = 0x06; break; + case CMD_TREMOLO: command = 0x07; break; + case CMD_PANNING8: + command = 0x08; + if(GetType() & MOD_TYPE_S3M) + { + if(param <= 0x80) + { + param = mpt::saturate_cast<uint8>(param * 2); + } + else if(param == 0xA4) // surround + { + if(compatibilityExport || !toXM) + { + command = param = 0; + } + else + { + command = 'X' - 55; + param = 91; + } + } + } + break; + case CMD_OFFSET: command = 0x09; break; + case CMD_VOLUMESLIDE: command = 0x0A; break; + case CMD_POSITIONJUMP: command = 0x0B; break; + case CMD_VOLUME: command = 0x0C; break; + case CMD_PATTERNBREAK: command = 0x0D; param = ((param / 10) << 4) | (param % 10); break; + case CMD_MODCMDEX: command = 0x0E; break; + case CMD_SPEED: command = 0x0F; param = std::min(param, uint8(0x1F)); break; + case CMD_TEMPO: command = 0x0F; param = std::max(param, uint8(0x20)); break; + case CMD_GLOBALVOLUME: command = 'G' - 55; break; + case CMD_GLOBALVOLSLIDE: command = 'H' - 55; break; + case CMD_KEYOFF: command = 'K' - 55; break; + case CMD_SETENVPOSITION: command = 'L' - 55; break; + case CMD_PANNINGSLIDE: command = 'P' - 55; break; + case CMD_RETRIG: command = 'R' - 55; break; + case CMD_TREMOR: command = 'T' - 55; break; + case CMD_DUMMY: command = 'W' - 55; break; + case CMD_XFINEPORTAUPDOWN: command = 'X' - 55; + if(compatibilityExport && param >= 0x30) // X1x and X2x are legit, everything above are MPT extensions, which don't belong here. + param = 0; // Don't set command to 0 to indicate that there *was* some X command here... + break; + case CMD_PANBRELLO: + if(compatibilityExport) + command = param = 0; + else + command = 'Y' - 55; + break; + case CMD_MIDI: + if(compatibilityExport) + command = param = 0; + else + command = 'Z' - 55; + break; + case CMD_SMOOTHMIDI: //rewbs.smoothVST: 36 + if(compatibilityExport) + command = param = 0; + else + command = '\\' - 56; + break; + case CMD_XPARAM: //rewbs.XMfixes - XParam is 38 + if(compatibilityExport) + command = param = 0; + else + command = '#' + 3; + break; + case CMD_S3MCMDEX: + switch(param & 0xF0) + { + case 0x10: command = 0x0E; param = (param & 0x0F) | 0x30; break; + case 0x20: command = 0x0E; param = (param & 0x0F) | 0x50; break; + case 0x30: command = 0x0E; param = (param & 0x0F) | 0x40; break; + case 0x40: command = 0x0E; param = (param & 0x0F) | 0x70; break; + case 0x90: + if(compatibilityExport) + command = param = 0; + else + command = 'X' - 55; + break; + case 0xB0: command = 0x0E; param = (param & 0x0F) | 0x60; break; + case 0xA0: + case 0x50: + case 0x70: + case 0x60: command = param = 0; break; + default: command = 0x0E; break; + } + break; + default: + command = param = 0; + } + + // Don't even think about saving XM effects in MODs... + if(command > 0x0F && !toXM) + { + command = param = 0; + } +} + +#endif // MODPLUG_NO_FILESAVE + + +// File Header +struct MODFileHeader +{ + uint8be numOrders; + uint8be restartPos; // Tempo (early SoundTracker) or restart position (only PC trackers?) + uint8be orderList[128]; +}; + +MPT_BINARY_STRUCT(MODFileHeader, 130) + + +// Sample Header +struct MODSampleHeader +{ + char name[22]; + uint16be length; + uint8be finetune; + uint8be volume; + uint16be loopStart; + uint16be loopLength; + + // Convert an MOD sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp, bool is4Chn) const + { + mptSmp.Initialize(MOD_TYPE_MOD); + mptSmp.nLength = length * 2; + mptSmp.nFineTune = MOD2XMFineTune(finetune & 0x0F); + mptSmp.nVolume = 4u * std::min(volume.get(), uint8(64)); + + SmpLength lStart = loopStart * 2; + SmpLength lLength = loopLength * 2; + // See if loop start is incorrect as words, but correct as bytes (like in Soundtracker modules) + if(lLength > 2 && (lStart + lLength > mptSmp.nLength) + && (lStart / 2 + lLength <= mptSmp.nLength)) + { + lStart /= 2; + } + + if(mptSmp.nLength == 2) + { + mptSmp.nLength = 0; + } + + if(mptSmp.nLength) + { + mptSmp.nLoopStart = lStart; + mptSmp.nLoopEnd = lStart + lLength; + + if(mptSmp.nLoopStart >= mptSmp.nLength) + { + mptSmp.nLoopStart = mptSmp.nLength - 1; + } + if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4) + { + mptSmp.nLoopStart = 0; + mptSmp.nLoopEnd = 0; + } + + // Fix for most likely broken sample loops. This fixes super_sufm_-_new_life.mod (M.K.) which has a long sample which is looped from 0 to 4. + // This module also has notes outside of the Amiga frequency range, so we cannot say that it should be played using ProTracker one-shot loops. + // On the other hand, "Crew Generation" by Necros (6CHN) has a sample with a similar loop, which is supposed to be played. + // To be able to correctly play both modules, we will draw a somewhat arbitrary line here and trust the loop points in MODs with more than + // 4 channels, even if they are tiny and at the very beginning of the sample. + if(mptSmp.nLoopEnd <= 8 && mptSmp.nLoopStart == 0 && mptSmp.nLength > mptSmp.nLoopEnd && is4Chn) + { + mptSmp.nLoopEnd = 0; + } + if(mptSmp.nLoopEnd > mptSmp.nLoopStart) + { + mptSmp.uFlags.set(CHN_LOOP); + } + } + } + + // Convert OpenMPT's internal sample header to a MOD sample header. + SmpLength ConvertToMOD(const ModSample &mptSmp) + { + SmpLength writeLength = mptSmp.HasSampleData() ? mptSmp.nLength : 0; + // If the sample size is odd, we have to add a padding byte, as all sample sizes in MODs are even. + if((writeLength % 2u) != 0) + { + writeLength++; + } + LimitMax(writeLength, SmpLength(0x1FFFE)); + + length = static_cast<uint16>(writeLength / 2u); + + if(mptSmp.RelativeTone < 0) + { + finetune = 0x08; + } else if(mptSmp.RelativeTone > 0) + { + finetune = 0x07; + } else + { + finetune = XM2MODFineTune(mptSmp.nFineTune); + } + volume = static_cast<uint8>(mptSmp.nVolume / 4u); + + loopStart = 0; + loopLength = 1; + if(mptSmp.uFlags[CHN_LOOP] && (mptSmp.nLoopStart + 2u) < writeLength) + { + const SmpLength loopEnd = Clamp(mptSmp.nLoopEnd, (mptSmp.nLoopStart & ~1) + 2u, writeLength) & ~1; + loopStart = static_cast<uint16>(mptSmp.nLoopStart / 2u); + loopLength = static_cast<uint16>((loopEnd - (mptSmp.nLoopStart & ~1)) / 2u); + } + + return writeLength; + } + + // Compute a "rating" of this sample header by counting invalid header data to ultimately reject garbage files. + uint32 GetInvalidByteScore() const + { + return ((volume > 64) ? 1 : 0) + + ((finetune > 15) ? 1 : 0) + + ((loopStart > length * 2) ? 1 : 0); + } + + // Suggested threshold for rejecting invalid files based on cumulated score returned by GetInvalidByteScore + static constexpr uint32 INVALID_BYTE_THRESHOLD = 40; + + // This threshold is used for files where the file magic only gives a + // fragile result which alone would lead to too many false positives. + // In particular, the files from Inconexia demo by Iguana + // (https://www.pouet.net/prod.php?which=830) which have 3 \0 bytes in + // the file magic tend to cause misdetection of random files. + static constexpr uint32 INVALID_BYTE_FRAGILE_THRESHOLD = 1; + + // Retrieve the internal sample format flags for this sample. + static SampleIO GetSampleFormat() + { + return SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM); + } +}; + +MPT_BINARY_STRUCT(MODSampleHeader, 30) + +// Pattern data of a 4-channel MOD file +using MODPatternData = std::array<std::array<std::array<uint8, 4>, 4>, 64>; + +// Synthesized StarTrekker instruments +struct AMInstrument +{ + char am[2]; // "AM" + char zero[4]; + uint16be startLevel; // Start level + uint16be attack1Level; // Attack 1 level + uint16be attack1Speed; // Attack 1 speed + uint16be attack2Level; // Attack 2 level + uint16be attack2Speed; // Attack 2 speed + uint16be sustainLevel; // Sustain level + uint16be decaySpeed; // Decay speed + uint16be sustainTime; // Sustain time + uint16be nt; // ? + uint16be releaseSpeed; // Release speed + uint16be waveform; // Waveform + int16be pitchFall; // Pitch fall + uint16be vibAmp; // Vibrato amplitude + uint16be vibSpeed; // Vibrato speed + uint16be octave; // Base frequency + + void ConvertToMPT(ModSample &sample, ModInstrument &ins, mpt::fast_prng &rng) const + { + sample.nLength = waveform == 3 ? 1024 : 32; + sample.nLoopStart = 0; + sample.nLoopEnd = sample.nLength; + sample.uFlags.set(CHN_LOOP); + sample.nVolume = 256; // prelude.mod has volume 0 in sample header + sample.nVibDepth = mpt::saturate_cast<uint8>(vibAmp * 2); + sample.nVibRate = static_cast<uint8>(vibSpeed); + sample.nVibType = VIB_SINE; + sample.RelativeTone = static_cast<int8>(-12 * octave); + if(sample.AllocateSample()) + { + int8 *p = sample.sample8(); + for(SmpLength i = 0; i < sample.nLength; i++) + { + switch(waveform) + { + default: + case 0: p[i] = ModSinusTable[i * 2]; break; // Sine + case 1: p[i] = static_cast<int8>(-128 + i * 8); break; // Saw + case 2: p[i] = i < 16 ? -128 : 127; break; // Square + case 3: p[i] = mpt::random<int8>(rng); break; // Noise + } + } + } + + InstrumentEnvelope &volEnv = ins.VolEnv; + volEnv.dwFlags.set(ENV_ENABLED); + volEnv.reserve(6); + volEnv.push_back(0, static_cast<EnvelopeNode::value_t>(startLevel / 4)); + + const struct + { + uint16 level, speed; + } points[] = {{startLevel, 0}, {attack1Level, attack1Speed}, {attack2Level, attack2Speed}, {sustainLevel, decaySpeed}, {sustainLevel, sustainTime}, {0, releaseSpeed}}; + + for(uint8 i = 1; i < std::size(points); i++) + { + int duration = std::min(points[i].speed, uint16(256)); + // Sustain time is already in ticks, no need to compute the segment duration. + if(i != 4) + { + if(duration == 0) + { + volEnv.dwFlags.set(ENV_LOOP); + volEnv.nLoopStart = volEnv.nLoopEnd = static_cast<uint8>(volEnv.size() - 1); + break; + } + + // Startrekker increments / decrements the envelope level by the stage speed + // until it reaches the next stage level. + int a, b; + if(points[i].level > points[i - 1].level) + { + a = points[i].level - points[i - 1].level; + b = 256 - points[i - 1].level; + } else + { + a = points[i - 1].level - points[i].level; + b = points[i - 1].level; + } + // Release time is again special. + if(i == 5) + b = 256; + else if(b == 0) + b = 1; + duration = std::max((256 * a) / (duration * b), 1); + } + if(duration > 0) + { + volEnv.push_back(volEnv.back().tick + static_cast<EnvelopeNode::tick_t>(duration), static_cast<EnvelopeNode::value_t>(points[i].level / 4)); + } + } + + if(pitchFall) + { + InstrumentEnvelope &pitchEnv = ins.PitchEnv; + pitchEnv.dwFlags.set(ENV_ENABLED); + pitchEnv.reserve(2); + pitchEnv.push_back(0, ENVELOPE_MID); + // cppcheck false-positive + // cppcheck-suppress zerodiv + pitchEnv.push_back(static_cast<EnvelopeNode::tick_t>(1024 / abs(pitchFall)), pitchFall > 0 ? ENVELOPE_MIN : ENVELOPE_MAX); + } + } +}; + +MPT_BINARY_STRUCT(AMInstrument, 36) + +struct PT36IffChunk +{ + // IFF chunk names + enum ChunkIdentifiers + { + idVERS = MagicBE("VERS"), + idINFO = MagicBE("INFO"), + idCMNT = MagicBE("CMNT"), + idPTDT = MagicBE("PTDT"), + }; + + uint32be signature; // IFF chunk name + uint32be chunksize; // chunk size without header +}; + +MPT_BINARY_STRUCT(PT36IffChunk, 8) + +struct PT36InfoChunk +{ + char name[32]; + uint16be numSamples; + uint16be numOrders; + uint16be numPatterns; + uint16be volume; + uint16be tempo; + uint16be flags; + uint16be dateDay; + uint16be dateMonth; + uint16be dateYear; + uint16be dateHour; + uint16be dateMinute; + uint16be dateSecond; + uint16be playtimeHour; + uint16be playtimeMinute; + uint16be playtimeSecond; + uint16be playtimeMsecond; +}; + +MPT_BINARY_STRUCT(PT36InfoChunk, 64) + + +// Check if header magic equals a given string. +static bool IsMagic(const char *magic1, const char (&magic2)[5]) +{ + return std::memcmp(magic1, magic2, 4) == 0; +} + + +static uint32 ReadSample(FileReader &file, MODSampleHeader &sampleHeader, ModSample &sample, mpt::charbuf<MAX_SAMPLENAME> &sampleName, bool is4Chn) +{ + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(sample, is4Chn); + + sampleName = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + // Get rid of weird characters in sample names. + for(auto &c : sampleName.buf) + { + if(c > 0 && c < ' ') + { + c = ' '; + } + } + // Check for invalid values + return sampleHeader.GetInvalidByteScore(); +} + + +// Count malformed bytes in MOD pattern data +static uint32 CountMalformedMODPatternData(const MODPatternData &patternData, const bool allow31Samples) +{ + const uint8 mask = allow31Samples ? 0xE0 : 0xF0; + uint32 malformedBytes = 0; + for(const auto &row : patternData) + { + for(const auto &data : row) + { + if(data[0] & mask) + malformedBytes++; + } + } + return malformedBytes; +} + + +// Check if number of malformed bytes in MOD pattern data exceeds some threshold +template <typename TFileReader> +static bool ValidateMODPatternData(TFileReader &file, const uint32 threshold, const bool allow31Samples) +{ + MODPatternData patternData; + if(!file.Read(patternData)) + return false; + return CountMalformedMODPatternData(patternData, allow31Samples) <= threshold; +} + + +// Parse the order list to determine how many patterns are used in the file. +static PATTERNINDEX GetNumPatterns(FileReader &file, ModSequence &Order, ORDERINDEX numOrders, SmpLength totalSampleLen, CHANNELINDEX &numChannels, SmpLength wowSampleLen, bool validateHiddenPatterns) +{ + PATTERNINDEX numPatterns = 0; // Total number of patterns in file (determined by going through the whole order list) with pattern number < 128 + PATTERNINDEX officialPatterns = 0; // Number of patterns only found in the "official" part of the order list (i.e. order positions < claimed order length) + PATTERNINDEX numPatternsIllegal = 0; // Total number of patterns in file, also counting in "invalid" pattern indexes >= 128 + + for(ORDERINDEX ord = 0; ord < 128; ord++) + { + PATTERNINDEX pat = Order[ord]; + if(pat < 128 && numPatterns <= pat) + { + numPatterns = pat + 1; + if(ord < numOrders) + { + officialPatterns = numPatterns; + } + } + if(pat >= numPatternsIllegal) + { + numPatternsIllegal = pat + 1; + } + } + + // Remove the garbage patterns past the official order end now that we don't need them anymore. + Order.resize(numOrders); + + const size_t patternStartOffset = file.GetPosition(); + const size_t sizeWithoutPatterns = totalSampleLen + patternStartOffset; + const size_t sizeWithOfficialPatterns = sizeWithoutPatterns + officialPatterns * numChannels * 256; + + if(wowSampleLen && (wowSampleLen + patternStartOffset) + numPatterns * 8 * 256 == (file.GetLength() & ~1)) + { + // 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 + // (e.g. ponylips.mod, MD5 c039af363b1d99a492dafc5b5f9dd949, SHA1 1bee1941c47bc6f913735ce0cf1880b248b8fc93) + file.Seek(patternStartOffset + numPatterns * 4 * 256); + if(ValidateMODPatternData(file, 16, true)) + numChannels = 8; + file.Seek(patternStartOffset); + } else if(numPatterns != officialPatterns && (validateHiddenPatterns || sizeWithOfficialPatterns == file.GetLength())) + { + // 15-sample SoundTracker specifics: + // Fix SoundTracker modules where "hidden" patterns should be ignored. + // razor-1911.mod (MD5 b75f0f471b0ae400185585ca05bf7fe8, SHA1 4de31af234229faec00f1e85e1e8f78f405d454b) + // and captain_fizz.mod (MD5 55bd89fe5a8e345df65438dbfc2df94e, SHA1 9e0e8b7dc67939885435ea8d3ff4be7704207a43) + // seem to have the "correct" file size when only taking the "official" patterns into account, + // but they only play correctly when also loading the inofficial patterns. + // On the other hand, the SoundTracker module + // wolf1.mod (MD5 a4983d7a432d324ce8261b019257f4ed, SHA1 aa6b399d02546bcb6baf9ec56a8081730dea3f44), + // wolf3.mod (MD5 af60840815aa9eef43820a7a04417fa6, SHA1 24d6c2e38894f78f6c5c6a4b693a016af8fa037b) + // and jean_baudlot_-_bad_dudes_vs_dragonninja-dragonf.mod (MD5 fa48e0f805b36bdc1833f6b82d22d936, SHA1 39f2f8319f4847fe928b9d88eee19d79310b9f91) + // only play correctly if we ignore the hidden patterns. + // Hence, we have a peek at the first hidden pattern and check if it contains a lot of illegal data. + // If that is the case, we assume it's part of the sample data and only consider the "official" patterns. + + // 31-sample NoiseTracker / ProTracker specifics: + // Interestingly, (broken) variants of the ProTracker modules + // "killing butterfly" (MD5 bd676358b1dbb40d40f25435e845cf6b, SHA1 9df4ae21214ff753802756b616a0cafaeced8021), + // "quartex" by Reflex (MD5 35526bef0fb21cb96394838d94c14bab, SHA1 116756c68c7b6598dcfbad75a043477fcc54c96c), + // seem to have the "correct" file size when only taking the "official" patterns into account, but they only play + // correctly when also loading the inofficial patterns. + // On the other hand, "Shofixti Ditty.mod" from Star Control 2 (MD5 62b7b0819123400e4d5a7813eef7fc7d, SHA1 8330cd595c61f51c37a3b6f2a8559cf3fcaaa6e8) + // doesn't sound correct when taking the second "inofficial" pattern into account. + file.Seek(patternStartOffset + officialPatterns * numChannels * 256); + if(!ValidateMODPatternData(file, 64, true)) + numPatterns = officialPatterns; + file.Seek(patternStartOffset); + } + + if(numPatternsIllegal > numPatterns && sizeWithoutPatterns + numPatternsIllegal * numChannels * 256 == file.GetLength()) + { + // 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. + numPatterns = numPatternsIllegal; + } else if(numPatternsIllegal >= 0xFF) + { + // Patterns FE and FF are used with S3M semantics (e.g. some MODs written with old OpenMPT versions) + Order.Replace(0xFE, Order.GetIgnoreIndex()); + Order.Replace(0xFF, Order.GetInvalidPatIndex()); + } + + return numPatterns; +} + + +void CSoundFile::ReadMODPatternEntry(FileReader &file, ModCommand &m) +{ + ReadMODPatternEntry(file.ReadArray<uint8, 4>(), m); +} + + +void CSoundFile::ReadMODPatternEntry(const std::array<uint8, 4> data, ModCommand &m) +{ + // Read Period + uint16 period = (((static_cast<uint16>(data[0]) & 0x0F) << 8) | data[1]); + size_t note = NOTE_NONE; + if(period > 0 && period != 0xFFF) + { + note = std::size(ProTrackerPeriodTable) + 23 + NOTE_MIN; + for(size_t i = 0; i < std::size(ProTrackerPeriodTable); i++) + { + if(period >= ProTrackerPeriodTable[i]) + { + if(period != ProTrackerPeriodTable[i] && i != 0) + { + uint16 p1 = ProTrackerPeriodTable[i - 1]; + uint16 p2 = ProTrackerPeriodTable[i]; + if(p1 - period < (period - p2)) + { + note = i + 23 + NOTE_MIN; + break; + } + } + note = i + 24 + NOTE_MIN; + break; + } + } + } + m.note = static_cast<ModCommand::NOTE>(note); + // Read Instrument + m.instr = (data[2] >> 4) | (data[0] & 0x10); + // Read Effect + m.command = data[2] & 0x0F; + m.param = data[3]; +} + + +struct MODMagicResult +{ + const mpt::uchar *madeWithTracker = nullptr; + uint32 invalidByteThreshold = MODSampleHeader::INVALID_BYTE_THRESHOLD; + uint16 patternDataOffset = 1084; + CHANNELINDEX numChannels = 0; + bool isNoiseTracker = false; + bool isStartrekker = false; + bool isGenericMultiChannel = false; + bool setMODVBlankTiming = false; +}; + + +static bool CheckMODMagic(const char magic[4], MODMagicResult &result) +{ + if(IsMagic(magic, "M.K.") // ProTracker and compatible + || IsMagic(magic, "M!K!") // ProTracker (>64 patterns) + || IsMagic(magic, "PATT") // ProTracker 3.6 + || IsMagic(magic, "NSMS") // kingdomofpleasure.mod by bee hunter + || IsMagic(magic, "LARD")) // judgement_day_gvine.mod by 4-mat + { + result.madeWithTracker = UL_("Generic ProTracker or compatible"); + result.numChannels = 4; + } else if(IsMagic(magic, "M&K!") // "His Master's Noise" musicdisk + || IsMagic(magic, "FEST") // "His Master's Noise" musicdisk + || IsMagic(magic, "N.T.")) + { + result.madeWithTracker = UL_("NoiseTracker"); + result.isNoiseTracker = true; + result.numChannels = 4; + } else if(IsMagic(magic, "OKTA") + || IsMagic(magic, "OCTA")) + { + // Oktalyzer + result.madeWithTracker = UL_("Oktalyzer"); + result.numChannels = 8; + } else if(IsMagic(magic, "CD81") + || IsMagic(magic, "CD61")) + { + // Octalyser on Atari STe/Falcon + result.madeWithTracker = UL_("Octalyser (Atari)"); + result.numChannels = magic[2] - '0'; + } else if(IsMagic(magic, "M\0\0\0") || IsMagic(magic, "8\0\0\0")) + { + // Inconexia demo by Iguana, delta samples (https://www.pouet.net/prod.php?which=830) + result.madeWithTracker = UL_("Inconexia demo (delta samples)"); + result.invalidByteThreshold = MODSampleHeader::INVALID_BYTE_FRAGILE_THRESHOLD; + result.numChannels = (magic[0] == '8') ? 8 : 4; + } else if(!memcmp(magic, "FA0", 3) && magic[3] >= '4' && magic[3] <= '8') + { + // Digital Tracker on Atari Falcon + result.madeWithTracker = UL_("Digital Tracker"); + result.numChannels = magic[3] - '0'; + // Digital Tracker MODs contain four bytes (00 40 00 00) right after the magic bytes which don't seem to do anything special. + result.patternDataOffset = 1088; + } else if((!memcmp(magic, "FLT", 3) || !memcmp(magic, "EXO", 3)) && magic[3] >= '4' && magic[3] <= '9') + { + // FLTx / EXOx - Startrekker by Exolon / Fairlight + result.madeWithTracker = UL_("Startrekker"); + result.isStartrekker = true; + result.setMODVBlankTiming = true; + result.numChannels = magic[3] - '0'; + } else if(magic[0] >= '1' && magic[0] <= '9' && !memcmp(magic + 1, "CHN", 3)) + { + // xCHN - Many trackers + result.madeWithTracker = UL_("Generic MOD-compatible Tracker"); + result.isGenericMultiChannel = true; + result.numChannels = magic[0] - '0'; + } else if(magic[0] >= '1' && magic[0] <= '9' && magic[1] >= '0' && magic[1] <= '9' + && (!memcmp(magic + 2, "CH", 2) || !memcmp(magic + 2, "CN", 2))) + { + // xxCN / xxCH - Many trackers + result.madeWithTracker = UL_("Generic MOD-compatible Tracker"); + result.isGenericMultiChannel = true; + result.numChannels = (magic[0] - '0') * 10 + magic[1] - '0'; + } else if(!memcmp(magic, "TDZ", 3) && magic[3] >= '1' && magic[3] <= '9') + { + // TDZx - TakeTracker (only TDZ1-TDZ3 should exist, but historically this code only supported 4-9 channels, so we keep those for the unlikely case that they were actually used for something) + result.madeWithTracker = UL_("TakeTracker"); + result.numChannels = magic[3] - '0'; + } else + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMOD(MemoryFileReader file, const uint64 *pfilesize) +{ + if(!file.LengthIsAtLeast(1080 + 4)) + { + return ProbeWantMoreData; + } + file.Seek(1080); + char magic[4]; + file.ReadArray(magic); + MODMagicResult modMagicResult; + if(!CheckMODMagic(magic, modMagicResult)) + { + return ProbeFailure; + } + + file.Seek(20); + uint32 invalidBytes = 0; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + MODSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + invalidBytes += sampleHeader.GetInvalidByteScore(); + } + if(invalidBytes > modMagicResult.invalidByteThreshold) + { + return ProbeFailure; + } + + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadMOD(FileReader &file, ModLoadingFlags loadFlags) +{ + char magic[4]; + if(!file.Seek(1080) || !file.ReadArray(magic)) + { + return false; + } + + InitializeGlobals(MOD_TYPE_MOD); + + MODMagicResult modMagicResult; + if(!CheckMODMagic(magic, modMagicResult) + || modMagicResult.numChannels < 1 + || modMagicResult.numChannels > MAX_BASECHANNELS) + { + return false; + } + + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + m_nChannels = modMagicResult.numChannels; + + bool isNoiseTracker = modMagicResult.isNoiseTracker; + bool isStartrekker = modMagicResult.isStartrekker; + bool isGenericMultiChannel = modMagicResult.isGenericMultiChannel; + bool isInconexia = IsMagic(magic, "M\0\0\0") || IsMagic(magic, "8\0\0\0"); + // A loop length of zero will freeze ProTracker, so assume that modules having such a value were not meant to be played on Amiga. Fixes LHS_MI.MOD + bool hasRepLen0 = false; + // Empty sample slots typically should have a default volume of 0 in ProTracker + bool hasEmptySampleWithVolume = false; + if(modMagicResult.setMODVBlankTiming) + { + m_playBehaviour.set(kMODVBlankTiming); + } + + // Startrekker 8 channel mod (needs special treatment, see below) + const bool isFLT8 = isStartrekker && m_nChannels == 8; + // Only apply VBlank tests to M.K. (ProTracker) modules. + const bool isMdKd = IsMagic(magic, "M.K."); + // Adjust finetune values for modules saved with "His Master's Noisetracker" + const bool isHMNT = IsMagic(magic, "M&K!") || IsMagic(magic, "FEST"); + bool maybeWOW = isMdKd; + + // Reading song title + file.Seek(0); + file.ReadString<mpt::String::spacePadded>(m_songName, 20); + + // Load Sample Headers + SmpLength totalSampleLen = 0, wowSampleLen = 0; + m_nSamples = 31; + uint32 invalidBytes = 0; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + MODSampleHeader sampleHeader; + invalidBytes += ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], m_nChannels == 4); + totalSampleLen += Samples[smp].nLength; + + if(isHMNT) + Samples[smp].nFineTune = -static_cast<int8>(sampleHeader.finetune << 3); + else if(Samples[smp].nLength > 65535) + isNoiseTracker = false; + + if(sampleHeader.length && !sampleHeader.loopLength) + hasRepLen0 = true; + else if(!sampleHeader.length && sampleHeader.volume == 64) + hasEmptySampleWithVolume = true; + + if(maybeWOW) + { + // Some WOW files rely on sample length 1 being counted as well + wowSampleLen += sampleHeader.length * 2; + // WOW files are converted 669 files, which don't support finetune or default volume + if(sampleHeader.finetune) + maybeWOW = false; + else if(sampleHeader.length > 0 && sampleHeader.volume != 64) + maybeWOW = false; + } + } + // If there is too much binary garbage in the sample headers, reject the file. + if(invalidBytes > modMagicResult.invalidByteThreshold) + { + return false; + } + + // Read order information + MODFileHeader fileHeader; + file.ReadStruct(fileHeader); + + file.Seek(modMagicResult.patternDataOffset); + + if(fileHeader.restartPos > 0) + maybeWOW = false; + if(!maybeWOW) + wowSampleLen = 0; + + ReadOrderFromArray(Order(), fileHeader.orderList); + + ORDERINDEX realOrders = fileHeader.numOrders; + if(realOrders > 128) + { + // beatwave.mod by Sidewinder claims to have 129 orders. (MD5: 8a029ac498d453beb929db9a73c3c6b4, SHA1: f7b76fb9f477b07a2e78eb10d8624f0df262cde7 - the version from ModArchive, not ModLand) + realOrders = 128; + } else if(realOrders == 0) + { + // Is this necessary? + realOrders = 128; + while(realOrders > 1 && Order()[realOrders - 1] == 0) + { + realOrders--; + } + } + + // Get number of patterns (including some order list sanity checks) + PATTERNINDEX numPatterns = GetNumPatterns(file, Order(), realOrders, totalSampleLen, m_nChannels, wowSampleLen, false); + if(maybeWOW && GetNumChannels() == 8) + { + // M.K. with 8 channels = Mod's Grave + modMagicResult.madeWithTracker = UL_("Mod's Grave"); + isGenericMultiChannel = true; + } + + if(isFLT8) + { + // FLT8 has only even order items, so divide by two. + for(auto &pat : Order()) + { + pat /= 2u; + } + } + + // Restart position sanity checks + realOrders--; + Order().SetRestartPos(fileHeader.restartPos); + + // (Ultimate) Soundtracker didn't have a restart position, but instead stored a default tempo in this value. + // The default value for this is 0x78 (120 BPM). This is probably the reason why some M.K. modules + // have this weird restart position. I think I've read somewhere that NoiseTracker actually writes 0x78 there. + // M.K. files that have restart pos == 0x78: action's batman by DJ Uno, VALLEY.MOD, WormsTDC.MOD, ZWARTZ.MOD + // Files that have an order list longer than 0x78 with restart pos = 0x78: my_shoe_is_barking.mod, papermix.mod + // - in both cases it does not appear like the restart position should be used. + MPT_ASSERT(fileHeader.restartPos != 0x78 || fileHeader.restartPos + 1u >= realOrders); + if(fileHeader.restartPos > realOrders || (fileHeader.restartPos == 0x78 && m_nChannels == 4)) + { + Order().SetRestartPos(0); + } + + m_nDefaultSpeed = 6; + m_nDefaultTempo.Set(125); + m_nMinPeriod = 14 * 4; + m_nMaxPeriod = 3424 * 4; + // Prevent clipping based on number of channels... If all channels are playing at full volume, "256 / #channels" + // is the maximum possible sample pre-amp without getting distortion (Compatible mix levels given). + // The more channels we have, the less likely it is that all of them are used at the same time, though, so cap at 32... + m_nSamplePreAmp = Clamp(256 / m_nChannels, 32, 128); + m_SongFlags.reset(); // SONG_ISAMIGA will be set conditionally + + // Setup channel pan positions and volume + SetupMODPanning(); + + // Before loading patterns, apply some heuristics: + // - Scan patterns to check if file could be a NoiseTracker file in disguise. + // In this case, the parameter of Dxx commands needs to be ignored. + // - Use the same code to find notes that would be out-of-range on Amiga. + // - Detect 7-bit panning and whether 8xx / E8x commands should be interpreted as panning at all. + bool onlyAmigaNotes = true; + bool fix7BitPanning = false; + uint8 maxPanning = 0; // For detecting 8xx-as-sync + const uint8 ENABLE_MOD_PANNING_THRESHOLD = 0x30; + if(!isNoiseTracker) + { + bool leftPanning = false, extendedPanning = false; // For detecting 800-880 panning + isNoiseTracker = isMdKd; + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + uint16 patternBreaks = 0; + + for(uint32 i = 0; i < 256; i++) + { + ModCommand m; + ReadMODPatternEntry(file, m); + if(!m.IsAmigaNote()) + { + isNoiseTracker = onlyAmigaNotes = false; + } + if((m.command > 0x06 && m.command < 0x0A) + || (m.command == 0x0E && m.param > 0x01) + || (m.command == 0x0F && m.param > 0x1F) + || (m.command == 0x0D && ++patternBreaks > 1)) + { + isNoiseTracker = false; + } + if(m.command == 0x08) + { + maxPanning = std::max(maxPanning, m.param); + if(m.param < 0x80) + leftPanning = true; + else if(m.param > 0x8F && m.param != 0xA4) + extendedPanning = true; + } else if(m.command == 0x0E && (m.param & 0xF0) == 0x80) + { + maxPanning = std::max(maxPanning, static_cast<uint8>((m.param & 0x0F) << 4)); + } + } + } + fix7BitPanning = leftPanning && !extendedPanning && maxPanning >= ENABLE_MOD_PANNING_THRESHOLD; + } + file.Seek(modMagicResult.patternDataOffset); + + const CHANNELINDEX readChannels = (isFLT8 ? 4 : m_nChannels); // 4 channels per pattern in FLT8 format. + if(isFLT8) numPatterns++; // as one logical pattern consists of two real patterns in FLT8 format, the highest pattern number has to be increased by one. + bool hasTempoCommands = false, definitelyCIA = false; // for detecting VBlank MODs + // Heuristic for rejecting E0x commands that are most likely not intended to actually toggle the Amiga LED filter, like in naen_leijasi_ptk.mod by ilmarque + bool filterState = false; + int filterTransitions = 0; + + // Reading patterns + Patterns.ResizeArray(numPatterns); + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + ModCommand *rowBase = nullptr; + + if(isFLT8) + { + // FLT8: Only create "even" patterns and either write to channel 1 to 4 (even patterns) or 5 to 8 (odd patterns). + PATTERNINDEX actualPattern = pat / 2u; + if((pat % 2u) == 0 && !Patterns.Insert(actualPattern, 64)) + { + break; + } + rowBase = Patterns[actualPattern].GetpModCommand(0, (pat % 2u) == 0 ? 0 : 4); + } else + { + if(!Patterns.Insert(pat, 64)) + { + break; + } + rowBase = Patterns[pat].GetpModCommand(0, 0); + } + + if(rowBase == nullptr || !(loadFlags & loadPatternData)) + { + break; + } + + // For detecting PT1x mode + std::vector<ModCommand::INSTR> lastInstrument(GetNumChannels(), 0); + std::vector<uint8> instrWithoutNoteCount(GetNumChannels(), 0); + + for(ROWINDEX row = 0; row < 64; row++, rowBase += m_nChannels) + { + // If we have more than one Fxx command on this row and one can be interpreted as speed + // and the other as tempo, we can be rather sure that it is not a VBlank mod. + bool hasSpeedOnRow = false, hasTempoOnRow = false; + + for(CHANNELINDEX chn = 0; chn < readChannels; chn++) + { + ModCommand &m = rowBase[chn]; + ReadMODPatternEntry(file, m); + + if(m.command || m.param) + { + if(isStartrekker && m.command == 0x0E) + { + // No support for Startrekker assembly macros + m.command = CMD_NONE; + m.param = 0; + } else if(isStartrekker && m.command == 0x0F && m.param > 0x1F) + { + // Startrekker caps speed at 31 ticks per row + m.param = 0x1F; + } + ConvertModCommand(m); + } + + // Perform some checks for our heuristics... + if(m.command == CMD_TEMPO) + { + hasTempoOnRow = true; + if(m.param < 100) + hasTempoCommands = true; + } else if(m.command == CMD_SPEED) + { + hasSpeedOnRow = true; + } else if(m.command == CMD_PATTERNBREAK && isNoiseTracker) + { + m.param = 0; + } else if(m.command == CMD_PANNING8 && fix7BitPanning) + { + // Fix MODs with 7-bit + surround panning + if(m.param == 0xA4) + { + m.command = CMD_S3MCMDEX; + m.param = 0x91; + } else + { + m.param = mpt::saturate_cast<ModCommand::PARAM>(m.param * 2); + } + } else if(m.command == CMD_MODCMDEX && m.param < 0x10) + { + // Count LED filter transitions + bool newState = !(m.param & 0x01); + if(newState != filterState) + { + filterState = newState; + filterTransitions++; + } + } + if(m.note == NOTE_NONE && m.instr > 0 && !isFLT8) + { + if(lastInstrument[chn] > 0 && lastInstrument[chn] != m.instr) + { + // Arbitrary threshold for enabling sample swapping: 4 consecutive "sample swaps" in one pattern. + if(++instrWithoutNoteCount[chn] >= 4) + { + m_playBehaviour.set(kMODSampleSwap); + } + } + } else if(m.note != NOTE_NONE) + { + instrWithoutNoteCount[chn] = 0; + } + if(m.instr != 0) + { + lastInstrument[chn] = m.instr; + } + } + if(hasSpeedOnRow && hasTempoOnRow) + definitelyCIA = true; + } + } + + if(onlyAmigaNotes && !hasRepLen0 && (IsMagic(magic, "M.K.") || IsMagic(magic, "M!K!") || IsMagic(magic, "PATT"))) + { + // M.K. files that don't exceed the Amiga note limit (fixes mod.mothergoose) + m_SongFlags.set(SONG_AMIGALIMITS); + // Need this for professionaltracker.mod by h0ffman (SHA1: 9a7c52cbad73ed2a198ee3fa18d3704ea9f546ff) + m_SongFlags.set(SONG_PT_MODE); + m_playBehaviour.set(kMODSampleSwap); + m_playBehaviour.set(kMODOutOfRangeNoteDelay); + m_playBehaviour.set(kMODTempoOnSecondTick); + // Arbitrary threshold for deciding that 8xx effects are only used as sync markers + if(maxPanning < ENABLE_MOD_PANNING_THRESHOLD) + { + m_playBehaviour.set(kMODIgnorePanning); + if(fileHeader.restartPos != 0x7F) + { + // Don't enable these hacks for ScreamTracker modules (restart position = 0x7F), to fix e.g. sample 10 in BASIC001.MOD (SHA1: 11298a5620e677beaa50bd4ed00c3710b75c81af) + // Note: restart position = 0x7F can also be found in ProTracker modules, e.g. professionaltracker.mod by h0ffman + m_playBehaviour.set(kMODOneShotLoops); + } + } + } else if(!onlyAmigaNotes && fileHeader.restartPos == 0x7F && isMdKd && fileHeader.restartPos + 1u >= realOrders) + { + modMagicResult.madeWithTracker = UL_("Scream Tracker"); + } + + if(onlyAmigaNotes && !isGenericMultiChannel && filterTransitions < 7) + { + m_SongFlags.set(SONG_ISAMIGA); + } + if(isGenericMultiChannel || isMdKd) + { + m_playBehaviour.set(kFT2MODTremoloRampWaveform); + } + if(isInconexia) + { + m_playBehaviour.set(kMODIgnorePanning); + } + + // Reading samples + if(loadFlags & loadSampleData) + { + file.Seek(modMagicResult.patternDataOffset + (readChannels * 64 * 4) * numPatterns); + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + ModSample &sample = Samples[smp]; + if(sample.nLength) + { + SampleIO::Encoding encoding = SampleIO::signedPCM; + if(isInconexia) + encoding = SampleIO::deltaPCM; + else if(file.ReadMagic("ADPCM")) + encoding = SampleIO::ADPCM; + + SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + encoding); + + // Fix sample 6 in MOD.shorttune2, which has a replen longer than the sample itself. + // ProTracker reads beyond the end of the sample when playing. Normally samples are + // adjacent in PT's memory, so we simply read into the next sample in the file. + // On the other hand, the loop points in Purple Motions's SOUL-O-M.MOD are completely broken and shouldn't be treated like this. + // As it was most likely written in Scream Tracker, it has empty sample slots with a default volume of 64, which we use for + // rejecting this quirk for that file. + FileReader::off_t nextSample = file.GetPosition() + sampleIO.CalculateEncodedSize(sample.nLength); + if(isMdKd && onlyAmigaNotes && !hasEmptySampleWithVolume) + sample.nLength = std::max(sample.nLength, sample.nLoopEnd); + + sampleIO.ReadSample(sample, file); + file.Seek(nextSample); + } + } + } + +#if defined(MPT_EXTERNAL_SAMPLES) || defined(MPT_BUILD_FUZZER) + // Detect Startrekker files with external synth instruments. + // Note: Synthesized AM samples may overwrite existing samples (e.g. sample 1 in fa.worse face.mod), + // hence they are loaded here after all regular samples have been loaded. + if((loadFlags & loadSampleData) && isStartrekker) + { +#ifdef MPT_EXTERNAL_SAMPLES + std::optional<InputFile> amFile; + FileReader amData; + if(file.GetOptionalFileName()) + { + mpt::PathString filename = file.GetOptionalFileName().value(); + // Find instrument definition file + const mpt::PathString exts[] = {P_(".nt"), P_(".NT"), P_(".as"), P_(".AS")}; + for(const auto &ext : exts) + { + mpt::PathString infoName = filename + ext; + char stMagic[16]; + if(infoName.IsFile()) + { + amFile.emplace(infoName, SettingCacheCompleteFileBeforeLoading()); + if(amFile->IsValid() && (amData = GetFileReader(*amFile)).IsValid() && amData.ReadArray(stMagic)) + { + if(!memcmp(stMagic, "ST1.2 ModuleINFO", 16)) + modMagicResult.madeWithTracker = UL_("Startrekker 1.2"); + else if(!memcmp(stMagic, "ST1.3 ModuleINFO", 16)) + modMagicResult.madeWithTracker = UL_("Startrekker 1.3"); + else if(!memcmp(stMagic, "AudioSculpture10", 16)) + modMagicResult.madeWithTracker = UL_("AudioSculpture 1.0"); + else + continue; + + if(amData.Seek(144)) + { + // Looks like a valid instrument definition file! + m_nInstruments = 31; + break; + } + } + } + } + } +#elif defined(MPT_BUILD_FUZZER) + // For fuzzing this part of the code, just take random data from patterns + FileReader amData = file.GetChunkAt(1084, 31 * 120); + m_nInstruments = 31; +#endif + + for(SAMPLEINDEX smp = 1; smp <= m_nInstruments; smp++) + { + // For Startrekker AM synthesis, we need instrument envelopes. + ModInstrument *ins = AllocateInstrument(smp, smp); + if(ins == nullptr) + { + break; + } + ins->name = m_szNames[smp]; + + AMInstrument am; + // Allow partial reads for fa.worse face.mod + if(amData.ReadStructPartial(am) && !memcmp(am.am, "AM", 2) && am.waveform < 4) + { + am.ConvertToMPT(Samples[smp], *ins, AccessPRNG()); + } + + // This extra padding is probably present to have identical block sizes for AM and FM instruments. + amData.Skip(120 - sizeof(AMInstrument)); + } + } +#endif // MPT_EXTERNAL_SAMPLES || MPT_BUILD_FUZZER + + // 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). + // There is no perfect way to do this, since both MOD types look the same, + // but the most reliable way is to simply check for extremely long songs + // (as this would indicate that e.g. a F30 command was really meant to set + // the ticks per row to 48, and not the tempo to 48 BPM). + // In the pattern loader above, a second condition is used: Only tempo commands + // below 100 BPM are taken into account. Furthermore, only M.K. (ProTracker) + // modules are checked. + if(isMdKd && hasTempoCommands && !definitelyCIA) + { + const double songTime = GetLength(eNoAdjust).front().duration; + if(songTime >= 480.0) + { + m_playBehaviour.set(kMODVBlankTiming); + if(GetLength(eNoAdjust, GetLengthTarget(songTime)).front().targetReached) + { + // This just makes things worse, song is at least as long as in CIA mode + // Obviously we should keep using CIA timing then... + m_playBehaviour.reset(kMODVBlankTiming); + } else + { + modMagicResult.madeWithTracker = UL_("ProTracker (VBlank)"); + } + } + } + + std::transform(std::begin(magic), std::end(magic), std::begin(magic), [](unsigned char c) -> unsigned char { return (c < ' ') ? ' ' : c; }); + m_modFormat.formatName = MPT_UFORMAT("ProTracker MOD ({})")(mpt::ToUnicode(mpt::Charset::ASCII, std::string(std::begin(magic), std::end(magic)))); + m_modFormat.type = U_("mod"); + if(modMagicResult.madeWithTracker) + m_modFormat.madeWithTracker = modMagicResult.madeWithTracker; + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + return true; +} + + +// Check if a name string is valid (i.e. doesn't contain binary garbage data) +template <size_t N> +static uint32 CountInvalidChars(const char (&name)[N]) +{ + uint32 invalidChars = 0; + for(int8 c : name) // char can be signed or unsigned + { + // Check for any Extended ASCII and control characters + if(c != 0 && c < ' ') + invalidChars++; + } + return invalidChars; +} + + +// We'll have to do some heuristic checks to find out whether this is an old Ultimate Soundtracker module +// or if it was made with the newer Soundtracker versions. +// Thanks for Fraggie for this information! (https://www.un4seen.com/forum/?topic=14471.msg100829#msg100829) +enum STVersions +{ + UST1_00, // Ultimate Soundtracker 1.0-1.21 (K. Obarski) + UST1_80, // Ultimate Soundtracker 1.8-2.0 (K. Obarski) + ST2_00_Exterminator, // SoundTracker 2.0 (The Exterminator), D.O.C. Sountracker II (Unknown/D.O.C.) + ST_III, // Defjam Soundtracker III (Il Scuro/Defjam), Alpha Flight SoundTracker IV (Alpha Flight), D.O.C. SoundTracker IV (Unknown/D.O.C.), D.O.C. SoundTracker VI (Unknown/D.O.C.) + ST_IX, // D.O.C. SoundTracker IX (Unknown/D.O.C.) + MST1_00, // Master Soundtracker 1.0 (Tip/The New Masters) + ST2_00, // SoundTracker 2.0, 2.1, 2.2 (Unknown/D.O.C.) +}; + + + +struct M15FileHeaders +{ + char songname[20]; + MODSampleHeader sampleHeaders[15]; + MODFileHeader fileHeader; +}; + +MPT_BINARY_STRUCT(M15FileHeaders, 20 + 15 * 30 + 130) + + +static bool ValidateHeader(const M15FileHeaders &fileHeaders) +{ + // In theory, sample and song names should only ever contain printable ASCII chars and null. + // However, there are quite a few SoundTracker modules in the wild with random + // characters. To still be able to distguish them from other formats, we just reject + // files with *too* many bogus characters. Arbitrary threshold: 48 bogus characters in total + // or more than 5 invalid characters just in the title alone. + uint32 invalidChars = CountInvalidChars(fileHeaders.songname); + if(invalidChars > 5) + { + return false; + } + + SmpLength totalSampleLen = 0; + uint8 allVolumes = 0; + + for(SAMPLEINDEX smp = 0; smp < 15; smp++) + { + const MODSampleHeader &sampleHeader = fileHeaders.sampleHeaders[smp]; + + invalidChars += CountInvalidChars(sampleHeader.name); + + // Sanity checks - invalid character count adjusted for ata.mod (MD5 937b79b54026fa73a1a4d3597c26eace, SHA1 3322ca62258adb9e0ae8e9afe6e0c29d39add874) + if(invalidChars > 48 + || sampleHeader.volume > 64 + || sampleHeader.finetune != 0 + || sampleHeader.length > 32768) + { + return false; + } + + totalSampleLen += sampleHeader.length; + allVolumes |= sampleHeader.volume; + } + + // Reject any files with no (or only silent) samples at all, as this might just be a random binary file (e.g. ID3 tags with tons of padding) + if(totalSampleLen == 0 || allVolumes == 0) + { + return false; + } + + // Sanity check: No more than 128 positions. ST's GUI limits tempo to [1, 220]. + // There are some mods with a tempo of 0 (explora3-death.mod) though, so ignore the lower limit. + if(fileHeaders.fileHeader.numOrders > 128 || fileHeaders.fileHeader.restartPos > 220) + { + return false; + } + + uint8 maxPattern = *std::max_element(std::begin(fileHeaders.fileHeader.orderList), std::end(fileHeaders.fileHeader.orderList)); + // Sanity check: 64 patterns max. + if(maxPattern > 63) + { + return false; + } + + // No playable song, and lots of null values => most likely a sparse binary file but not a module + if(fileHeaders.fileHeader.restartPos == 0 && fileHeaders.fileHeader.numOrders == 0 && maxPattern == 0) + { + return false; + } + + return true; +} + + +template <typename TFileReader> +static bool ValidateFirstM15Pattern(TFileReader &file) +{ + // threshold is chosen as: [threshold for all patterns combined] / [max patterns] * [margin, do not reject too much] + return ValidateMODPatternData(file, 512 / 64 * 2, false); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderM15(MemoryFileReader file, const uint64 *pfilesize) +{ + M15FileHeaders fileHeaders; + if(!file.ReadStruct(fileHeaders)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeaders)) + { + return ProbeFailure; + } + if(!file.CanRead(sizeof(MODPatternData))) + { + return ProbeWantMoreData; + } + if(!ValidateFirstM15Pattern(file)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadM15(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + M15FileHeaders fileHeaders; + if(!file.ReadStruct(fileHeaders)) + { + return false; + } + if(!ValidateHeader(fileHeaders)) + { + return false; + } + if(!ValidateFirstM15Pattern(file)) + { + return false; + } + + char songname[20]; + std::memcpy(songname, fileHeaders.songname, 20); + + InitializeGlobals(MOD_TYPE_MOD); + m_playBehaviour.reset(kMODOneShotLoops); + m_playBehaviour.set(kMODIgnorePanning); + m_playBehaviour.set(kMODSampleSwap); // untested + m_nChannels = 4; + + STVersions minVersion = UST1_00; + + bool hasDiskNames = true; + SmpLength totalSampleLen = 0; + m_nSamples = 15; + + file.Seek(20); + for(SAMPLEINDEX smp = 1; smp <= 15; smp++) + { + MODSampleHeader sampleHeader; + ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], true); + + totalSampleLen += Samples[smp].nLength; + + if(m_szNames[smp][0] && ((memcmp(m_szNames[smp].buf, "st-", 3) && memcmp(m_szNames[smp].buf, "ST-", 3)) || m_szNames[smp][5] != ':')) + { + // Ultimate Soundtracker 1.8 and D.O.C. SoundTracker IX always have sample names containing disk names. + hasDiskNames = false; + } + + // Loop start is always in bytes, not words, so don't trust the auto-fix magic in the sample header conversion (fixes loop of "st-01:asia" in mod.drag 10) + if(sampleHeader.loopLength > 1) + { + Samples[smp].nLoopStart = sampleHeader.loopStart; + Samples[smp].nLoopEnd = sampleHeader.loopStart + sampleHeader.loopLength * 2; + Samples[smp].SanitizeLoops(); + } + + // UST only handles samples up to 9999 bytes. Master Soundtracker 1.0 and SoundTracker 2.0 introduce 32KB samples. + if(sampleHeader.length > 4999 || sampleHeader.loopStart > 9999) + minVersion = std::max(minVersion, MST1_00); + } + + MODFileHeader fileHeader; + file.ReadStruct(fileHeader); + + ReadOrderFromArray(Order(), fileHeader.orderList); + PATTERNINDEX numPatterns = GetNumPatterns(file, Order(), fileHeader.numOrders, totalSampleLen, m_nChannels, 0, true); + + // Most likely just a file with lots of NULs at the start + if(fileHeader.restartPos == 0 && fileHeader.numOrders == 0 && numPatterns <= 1) + { + return false; + } + + // Let's see if the file is too small (including some overhead for broken files like sll7.mod or ghostbus.mod) + if(file.BytesLeft() + 65536 < numPatterns * 64u * 4u * 4u + totalSampleLen) + return false; + + if(loadFlags == onlyVerifyHeader) + return true; + + // Now we can be pretty sure that this is a valid Soundtracker file. Set up default song settings. + // explora3-death.mod has a tempo of 0 + if(!fileHeader.restartPos) + fileHeader.restartPos = 0x78; + // jjk55 by Jesper Kyd has a weird tempo set, but it needs to be ignored. + if(!memcmp(songname, "jjk55", 6)) + fileHeader.restartPos = 0x78; + // Sample 7 in echoing.mod won't "loop" correctly if we don't convert the VBlank tempo. + m_nDefaultTempo.Set(125); + if(fileHeader.restartPos != 0x78) + { + // Convert to CIA timing + m_nDefaultTempo = TEMPO((709379.0 * 125.0 / 50.0) / ((240 - fileHeader.restartPos) * 122.0)); + if(minVersion > UST1_80) + { + // D.O.C. SoundTracker IX re-introduced the variable tempo after some other versions dropped it. + minVersion = std::max(minVersion, hasDiskNames ? ST_IX : MST1_00); + } else + { + // Ultimate Soundtracker 1.8 adds variable tempo + minVersion = std::max(minVersion, hasDiskNames ? UST1_80 : ST2_00_Exterminator); + } + } + m_nMinPeriod = 113 * 4; + m_nMaxPeriod = 856 * 4; + m_nSamplePreAmp = 64; + m_SongFlags.set(SONG_PT_MODE); + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, songname); + + // Setup channel pan positions and volume + SetupMODPanning(); + + FileReader::off_t patOffset = file.GetPosition(); + + // Scan patterns to identify Ultimate Soundtracker modules. + uint32 illegalBytes = 0, totalNumDxx = 0; + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + const bool patternInUse = mpt::contains(Order(), pat); + uint8 numDxx = 0; + uint8 emptyCmds = 0; + MODPatternData patternData; + file.ReadArray(patternData); + if(patternInUse) + { + illegalBytes += CountMalformedMODPatternData(patternData, false); + // Reject files that contain a lot of illegal pattern data. + // STK.the final remix (MD5 5ff13cdbd77211d1103be7051a7d89c9, SHA1 e94dba82a5da00a4758ba0c207eb17e3a89c3aa3) + // has one illegal byte, so we only reject after an arbitrary threshold has been passed. + // This also allows to play some rather damaged files like + // crockets.mod (MD5 995ed9f44cab995a0eeb19deb52e2a8b, SHA1 6c79983c3b7d55c9bc110b625eaa07ce9d75f369) + // but naturally we cannot recover the broken data. + + // We only check patterns that are actually being used in the order list, because some bad rips of the + // "operation wolf" soundtrack have 15 patterns for several songs, but the last few patterns are just garbage. + // Apart from those hidden patterns, the files play fine. + // Example: operation wolf - wolf1.mod (MD5 739acdbdacd247fbefcac7bc2d8abe6b, SHA1 e6b4813daacbf95f41ce9ec3b22520a2ae07eed8) + if(illegalBytes > 512) + return false; + } + for(ROWINDEX row = 0; row < 64; row++) + { + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + const auto &data = patternData[row][chn]; + const uint8 eff = data[2] & 0x0F, param = data[3]; + // Check for empty space between the last Dxx command and the beginning of another pattern + if(emptyCmds != 0 && !memcmp(data.data(), "\0\0\0\0", 4)) + { + emptyCmds++; + if(emptyCmds > 32) + { + // Since there is a lot of empty space after the last Dxx command, + // we assume it's supposed to be a pattern break effect. + minVersion = ST2_00; + } + } else + { + emptyCmds = 0; + } + + switch(eff) + { + case 1: + case 2: + if(param > 0x1F && minVersion == UST1_80) + { + // If a 1xx / 2xx effect has a parameter greater than 0x20, it is assumed to be UST. + minVersion = hasDiskNames ? UST1_80 : UST1_00; + } else if(eff == 1 && param > 0 && param < 0x03) + { + // This doesn't look like an arpeggio. + minVersion = std::max(minVersion, ST2_00_Exterminator); + } else if(eff == 1 && (param == 0x37 || param == 0x47) && minVersion <= ST2_00_Exterminator) + { + // This suspiciously looks like an arpeggio. + // Catch sleepwalk.mod by Karsten Obarski, which has a default tempo of 125 rather than 120 in the header, so gets mis-identified as a later tracker version. + minVersion = hasDiskNames ? UST1_80 : UST1_00; + } + break; + case 0x0B: + minVersion = ST2_00; + break; + case 0x0C: + case 0x0D: + case 0x0E: + minVersion = std::max(minVersion, ST2_00_Exterminator); + if(eff == 0x0D) + { + emptyCmds = 1; + if(param == 0 && row == 0) + { + // Fix a possible tracking mistake in Blood Money title - who wants to do a pattern break on the first row anyway? + break; + } + numDxx++; + } + break; + case 0x0F: + minVersion = std::max(minVersion, ST_III); + break; + } + } + } + + if(numDxx > 0 && numDxx < 3) + { + // Not many Dxx commands in one pattern means they were probably pattern breaks + minVersion = ST2_00; + } + totalNumDxx += numDxx; + } + + // If there is a huge number of Dxx commands, this is extremely unlikely to be a SoundTracker 2.0 module + if(totalNumDxx > numPatterns + 32u && minVersion == ST2_00) + minVersion = MST1_00; + + file.Seek(patOffset); + + // Reading patterns + if(loadFlags & loadPatternData) + Patterns.ResizeArray(numPatterns); + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + MODPatternData patternData; + file.ReadArray(patternData); + + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + continue; + } + + uint8 autoSlide[4] = {0, 0, 0, 0}; + for(ROWINDEX row = 0; row < 64; row++) + { + PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0); + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + ModCommand &m = rowBase[chn]; + ReadMODPatternEntry(patternData[row][chn], m); + + if(!m.param || m.command == 0x0E) + { + autoSlide[chn] = 0; + } + if(m.command || m.param) + { + if(autoSlide[chn] != 0) + { + if(autoSlide[chn] & 0xF0) + { + m.volcmd = VOLCMD_VOLSLIDEUP; + m.vol = autoSlide[chn] >> 4; + } else + { + m.volcmd = VOLCMD_VOLSLIDEDOWN; + m.vol = autoSlide[chn] & 0x0F; + } + } + if(m.command == 0x0D) + { + if(minVersion != ST2_00) + { + // Dxy is volume slide in some Soundtracker versions, D00 is a pattern break in the latest versions. + m.command = 0x0A; + } else + { + m.param = 0; + } + } else if(m.command == 0x0C) + { + // Volume is sent as-is to the chip, which ignores the highest bit. + m.param &= 0x7F; + } else if(m.command == 0x0E && (m.param > 0x01 || minVersion < ST_IX)) + { + // Import auto-slides as normal slides and fake them using volume column slides. + m.command = 0x0A; + autoSlide[chn] = m.param; + } else if(m.command == 0x0F) + { + // Only the low nibble is evaluated in Soundtracker. + m.param &= 0x0F; + } + + if(minVersion <= UST1_80) + { + // UST effects + switch(m.command) + { + case 0: + // jackdance.mod by Karsten Obarski has 0xy arpeggios... + if(m.param < 0x03) + { + m.command = CMD_NONE; + } else + { + m.command = CMD_ARPEGGIO; + } + break; + case 1: + m.command = CMD_ARPEGGIO; + break; + case 2: + if(m.param & 0x0F) + { + m.command = CMD_PORTAMENTOUP; + m.param &= 0x0F; + } else if(m.param >> 4) + { + m.command = CMD_PORTAMENTODOWN; + m.param >>= 4; + } + break; + default: + m.command = CMD_NONE; + break; + } + } else + { + ConvertModCommand(m); + } + } else + { + autoSlide[chn] = 0; + } + } + } + } + + const mpt::uchar *madeWithTracker = UL_(""); + switch(minVersion) + { + case UST1_00: + madeWithTracker = UL_("Ultimate Soundtracker 1.0-1.21"); + break; + case UST1_80: + madeWithTracker = UL_("Ultimate Soundtracker 1.8-2.0"); + break; + case ST2_00_Exterminator: + madeWithTracker = UL_("SoundTracker 2.0 / D.O.C. SoundTracker II"); + break; + case ST_III: + madeWithTracker = UL_("Defjam Soundtracker III / Alpha Flight SoundTracker IV / D.O.C. SoundTracker IV / VI"); + break; + case ST_IX: + madeWithTracker = UL_("D.O.C. SoundTracker IX"); + break; + case MST1_00: + madeWithTracker = UL_("Master Soundtracker 1.0"); + break; + case ST2_00: + madeWithTracker = UL_("SoundTracker 2.0 / 2.1 / 2.2"); + break; + } + + m_modFormat.formatName = U_("Soundtracker"); + m_modFormat.type = U_("stk"); + m_modFormat.madeWithTracker = madeWithTracker; + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + // Reading samples + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= 15; smp++) + { + // Looped samples in (Ultimate) Soundtracker seem to ignore all sample data before the actual loop start. + // This avoids the clicks in the first sample of pretend.mod by Karsten Obarski. + file.Skip(Samples[smp].nLoopStart); + Samples[smp].nLength -= Samples[smp].nLoopStart; + Samples[smp].nLoopEnd -= Samples[smp].nLoopStart; + Samples[smp].nLoopStart = 0; + MODSampleHeader::GetSampleFormat().ReadSample(Samples[smp], file); + } + } + + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderICE(MemoryFileReader file, const uint64 *pfilesize) +{ + if(!file.CanRead(1464 + 4)) + { + return ProbeWantMoreData; + } + file.Seek(1464); + char magic[4]; + file.ReadArray(magic); + if(!IsMagic(magic, "MTN\0") && !IsMagic(magic, "IT10")) + { + return ProbeFailure; + } + file.Seek(20); + uint32 invalidBytes = 0; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + MODSampleHeader sampleHeader; + if(!file.ReadStruct(sampleHeader)) + { + return ProbeWantMoreData; + } + invalidBytes += sampleHeader.GetInvalidByteScore(); + } + if(invalidBytes > MODSampleHeader::INVALID_BYTE_THRESHOLD) + { + return ProbeFailure; + } + const auto [numOrders, numTracks] = file.ReadArray<uint8, 2>(); + if(numOrders > 128) + { + return ProbeFailure; + } + uint8 tracks[128 * 4]; + file.ReadArray(tracks); + for(auto track : tracks) + { + if(track > numTracks) + { + return ProbeFailure; + } + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +// SoundTracker 2.6 / Ice Tracker variation of the MOD format +// The only real difference to other SoundTracker formats is the way patterns are stored: +// Every pattern consists of four independent, re-usable tracks. +bool CSoundFile::ReadICE(FileReader &file, ModLoadingFlags loadFlags) +{ + char magic[4]; + if(!file.Seek(1464) || !file.ReadArray(magic)) + { + return false; + } + + InitializeGlobals(MOD_TYPE_MOD); + m_playBehaviour.reset(kMODOneShotLoops); + m_playBehaviour.set(kMODIgnorePanning); + m_playBehaviour.set(kMODSampleSwap); // untested + + if(IsMagic(magic, "MTN\0")) + { + m_modFormat.formatName = U_("MnemoTroN SoundTracker"); + m_modFormat.type = U_("st26"); + m_modFormat.madeWithTracker = U_("SoundTracker 2.6"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + } else if(IsMagic(magic, "IT10")) + { + m_modFormat.formatName = U_("Ice Tracker"); + m_modFormat.type = U_("ice"); + m_modFormat.madeWithTracker = U_("Ice Tracker 1.0 / 1.1"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + } else + { + return false; + } + + // Reading song title + file.Seek(0); + file.ReadString<mpt::String::spacePadded>(m_songName, 20); + + // Load Samples + m_nSamples = 31; + uint32 invalidBytes = 0; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + MODSampleHeader sampleHeader; + invalidBytes += ReadSample(file, sampleHeader, Samples[smp], m_szNames[smp], true); + } + if(invalidBytes > MODSampleHeader::INVALID_BYTE_THRESHOLD) + { + return false; + } + + const auto [numOrders, numTracks] = file.ReadArray<uint8, 2>(); + if(numOrders > 128) + { + return false; + } + + uint8 tracks[128 * 4]; + file.ReadArray(tracks); + for(auto track : tracks) + { + if(track > numTracks) + { + return false; + } + } + + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Now we can be pretty sure that this is a valid MOD file. Set up default song settings. + m_nChannels = 4; + m_nInstruments = 0; + m_nDefaultSpeed = 6; + m_nDefaultTempo.Set(125); + m_nMinPeriod = 14 * 4; + m_nMaxPeriod = 3424 * 4; + m_nSamplePreAmp = 64; + m_SongFlags.set(SONG_PT_MODE | SONG_IMPORTED); + + // Setup channel pan positions and volume + SetupMODPanning(); + + // Reading patterns + Order().resize(numOrders); + uint8 speed[2] = {0, 0}, speedPos = 0; + Patterns.ResizeArray(numOrders); + for(PATTERNINDEX pat = 0; pat < numOrders; pat++) + { + Order()[pat] = pat; + if(!Patterns.Insert(pat, 64)) + continue; + + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + file.Seek(1468 + tracks[pat * 4 + chn] * 64u * 4u); + ModCommand *m = Patterns[pat].GetpModCommand(0, chn); + + for(ROWINDEX row = 0; row < 64; row++, m += 4) + { + ReadMODPatternEntry(file, *m); + + if((m->command || m->param) + && !(m->command == 0x0E && m->param >= 0x10) // Exx only sets filter + && !(m->command >= 0x05 && m->command <= 0x09)) // These don't exist in ST2.6 + { + ConvertModCommand(*m); + } else + { + m->command = CMD_NONE; + } + } + } + + // Handle speed command with both nibbles set - this enables auto-swing (alternates between the two nibbles) + auto m = Patterns[pat].begin(); + for(ROWINDEX row = 0; row < 64; row++) + { + for(CHANNELINDEX chn = 0; chn < 4; chn++, m++) + { + if(m->command == CMD_SPEED || m->command == CMD_TEMPO) + { + m->command = CMD_SPEED; + speedPos = 0; + if(m->param & 0xF0) + { + if((m->param >> 4) != (m->param & 0x0F) && (m->param & 0x0F) != 0) + { + // Both nibbles set + speed[0] = m->param >> 4; + speed[1] = m->param & 0x0F; + speedPos = 1; + } + m->param >>= 4; + } + } + } + if(speedPos) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, speed[speedPos - 1]).Row(row)); + speedPos++; + if(speedPos == 3) + speedPos = 1; + } + } + } + + // Reading samples + if(loadFlags & loadSampleData) + { + file.Seek(1468 + numTracks * 64u * 4u); + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) if(Samples[smp].nLength) + { + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + } + } + + return true; +} + + + +struct PT36Header +{ + char magicFORM[4]; // "FORM" + uint32be size; + char magicMODL[4]; // "MODL" +}; + +MPT_BINARY_STRUCT(PT36Header, 12) + + +static bool ValidateHeader(const PT36Header &fileHeader) +{ + if(std::memcmp(fileHeader.magicFORM, "FORM", 4)) + { + return false; + } + if(std::memcmp(fileHeader.magicMODL, "MODL", 4)) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPT36(MemoryFileReader file, const uint64 *pfilesize) +{ + PT36Header fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +// ProTracker 3.6 version of the MOD format +// Basically just a normal ProTracker mod with different magic, wrapped in an IFF file. +// The "PTDT" chunk is passed to the normal MOD loader. +bool CSoundFile::ReadPT36(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + PT36Header fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + + bool ok = false, infoOk = false; + FileReader commentChunk; + mpt::ustring version; + PT36InfoChunk info; + MemsetZero(info); + + // Go through IFF chunks... + PT36IffChunk iffHead; + if(!file.ReadStruct(iffHead)) + { + return false; + } + // First chunk includes "MODL" magic in size + iffHead.chunksize -= 4; + + do + { + // All chunk sizes include chunk header + iffHead.chunksize -= 8; + if(loadFlags == onlyVerifyHeader && iffHead.signature == PT36IffChunk::idPTDT) + { + return true; + } + + FileReader chunk = file.ReadChunk(iffHead.chunksize); + if(!chunk.IsValid()) + { + break; + } + + switch(iffHead.signature) + { + case PT36IffChunk::idVERS: + chunk.Skip(4); + if(chunk.ReadMagic("PT") && iffHead.chunksize > 6) + { + chunk.ReadString<mpt::String::maybeNullTerminated>(version, mpt::Charset::Amiga_no_C1, iffHead.chunksize - 6); + } + break; + + case PT36IffChunk::idINFO: + infoOk = chunk.ReadStruct(info); + break; + + case PT36IffChunk::idCMNT: + commentChunk = chunk; + break; + + case PT36IffChunk::idPTDT: + ok = ReadMOD(chunk, loadFlags); + break; + } + } while(file.ReadStruct(iffHead)); + + if(version.empty()) + { + version = U_("3.6"); + } + + // both an info chunk and a module are required + if(ok && infoOk) + { + bool vblank = (info.flags & 0x100) == 0; + m_playBehaviour.set(kMODVBlankTiming, vblank); + if(info.volume != 0) + m_nSamplePreAmp = std::min(uint16(64), static_cast<uint16>(info.volume)); + if(info.tempo != 0 && !vblank) + m_nDefaultTempo.Set(info.tempo); + + if(info.name[0]) + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, info.name); + + if(mpt::is_in_range(info.dateMonth, 1, 12) && mpt::is_in_range(info.dateDay, 1, 31) && mpt::is_in_range(info.dateHour, 0, 23) + && mpt::is_in_range(info.dateMinute, 0, 59) && mpt::is_in_range(info.dateSecond, 0, 59)) + { + FileHistory mptHistory; + mptHistory.loadDate.tm_year = info.dateYear; + mptHistory.loadDate.tm_mon = info.dateMonth - 1; + mptHistory.loadDate.tm_mday = info.dateDay; + mptHistory.loadDate.tm_hour = info.dateHour; + mptHistory.loadDate.tm_min = info.dateMinute; + mptHistory.loadDate.tm_sec = info.dateSecond; + m_FileHistory.push_back(mptHistory); + } + } + if(ok) + { + if(commentChunk.IsValid()) + { + std::string author; + commentChunk.ReadString<mpt::String::maybeNullTerminated>(author, 32); + if(author != "UNNAMED AUTHOR") + m_songArtist = mpt::ToUnicode(mpt::Charset::Amiga_no_C1, author); + if(!commentChunk.NoBytesLeft()) + { + m_songMessage.ReadFixedLineLength(commentChunk, commentChunk.BytesLeft(), 40, 0); + } + } + + m_modFormat.madeWithTracker = U_("ProTracker ") + version; + } + m_SongFlags.set(SONG_PT_MODE); + m_playBehaviour.set(kMODIgnorePanning); + m_playBehaviour.set(kMODOneShotLoops); + m_playBehaviour.reset(kMODSampleSwap); + + return ok; +} + + +#ifndef MODPLUG_NO_FILESAVE + +bool CSoundFile::SaveMod(std::ostream &f) const +{ + if(m_nChannels == 0) + { + return false; + } + + // Write song title + { + char name[20]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = m_songName; + mpt::IO::Write(f, name); + } + + std::vector<SmpLength> sampleLength(32, 0); + std::vector<SAMPLEINDEX> sampleSource(32, 0); + + if(GetNumInstruments()) + { + INSTRUMENTINDEX lastIns = std::min(INSTRUMENTINDEX(31), GetNumInstruments()); + for(INSTRUMENTINDEX ins = 1; ins <= lastIns; ins++) if (Instruments[ins]) + { + // Find some valid sample associated with this instrument. + for(auto smp : Instruments[ins]->Keyboard) + { + if(smp > 0 && smp <= GetNumSamples()) + { + sampleSource[ins] = smp; + break; + } + } + } + } else + { + for(SAMPLEINDEX i = 1; i <= 31; i++) + { + sampleSource[i] = i; + } + } + + // Write sample headers + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + MODSampleHeader sampleHeader; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, sampleHeader.name) = m_szNames[sampleSource[smp]]; + sampleLength[smp] = sampleHeader.ConvertToMOD(sampleSource[smp] <= GetNumSamples() ? GetSample(sampleSource[smp]) : ModSample(MOD_TYPE_MOD)); + mpt::IO::Write(f, sampleHeader); + } + + // Write order list + MODFileHeader fileHeader; + MemsetZero(fileHeader); + + PATTERNINDEX writePatterns = 0; + uint8 writtenOrders = 0; + for(ORDERINDEX ord = 0; ord < Order().GetLength() && writtenOrders < 128; ord++) + { + // Ignore +++ and --- patterns in order list, as well as high patterns (MOD officially only supports up to 128 patterns) + if(ord == Order().GetRestartPos()) + { + fileHeader.restartPos = writtenOrders; + } + if(Order()[ord] < 128) + { + fileHeader.orderList[writtenOrders++] = static_cast<uint8>(Order()[ord]); + if(writePatterns <= Order()[ord]) + { + writePatterns = Order()[ord] + 1; + } + } + } + fileHeader.numOrders = writtenOrders; + mpt::IO::Write(f, fileHeader); + + // Write magic bytes + char modMagic[4]; + CHANNELINDEX writeChannels = std::min(CHANNELINDEX(99), GetNumChannels()); + if(writeChannels == 4) + { + // ProTracker may not load files with more than 64 patterns correctly if we do not specify the M!K! magic. + if(writePatterns <= 64) + memcpy(modMagic, "M.K.", 4); + else + memcpy(modMagic, "M!K!", 4); + } else if(writeChannels < 10) + { + memcpy(modMagic, "0CHN", 4); + modMagic[0] += static_cast<char>(writeChannels); + } else + { + memcpy(modMagic, "00CH", 4); + modMagic[0] += static_cast<char>(writeChannels / 10u); + modMagic[1] += static_cast<char>(writeChannels % 10u); + } + mpt::IO::Write(f, modMagic); + + // Write patterns + bool invalidInstruments = false; + std::vector<uint8> events; + for(PATTERNINDEX pat = 0; pat < writePatterns; pat++) + { + if(!Patterns.IsValidPat(pat)) + { + // Invent empty pattern + events.assign(writeChannels * 64 * 4, 0); + mpt::IO::Write(f, events); + continue; + } + + for(ROWINDEX row = 0; row < 64; row++) + { + if(row >= Patterns[pat].GetNumRows()) + { + // Invent empty row + events.assign(writeChannels * 4, 0); + mpt::IO::Write(f, events); + continue; + } + PatternRow rowBase = Patterns[pat].GetRow(row); + + events.resize(writeChannels * 4); + size_t eventByte = 0; + for(CHANNELINDEX chn = 0; chn < writeChannels; chn++, eventByte += 4) + { + const ModCommand &m = rowBase[chn]; + uint8 command = m.command, param = m.param; + ModSaveCommand(command, param, false, true); + + if(m.volcmd == VOLCMD_VOLUME && !command && !param) + { + // Maybe we can save some volume commands... + command = 0x0C; + param = std::min(m.vol, uint8(64)); + } + + uint16 period = 0; + // Convert note to period + if(m.note >= 24 + NOTE_MIN && m.note < std::size(ProTrackerPeriodTable) + 24 + NOTE_MIN) + { + period = ProTrackerPeriodTable[m.note - 24 - NOTE_MIN]; + } + + const uint8 instr = (m.instr > 31) ? 0 : m.instr; + if(m.instr > 31) + invalidInstruments = true; + + events[eventByte + 0] = ((period >> 8) & 0x0F) | (instr & 0x10); + events[eventByte + 1] = period & 0xFF; + events[eventByte + 2] = ((instr & 0x0F) << 4) | (command & 0x0F); + events[eventByte + 3] = param; + } + mpt::IO::WriteRaw(f, mpt::as_span(events)); + } + } + + if(invalidInstruments) + { + AddToLog(LogWarning, U_("Warning: This track references sample slots higher than 31. Such samples cannot be saved in the MOD format, and thus the notes will not sound correct. Use the Cleanup tool to rearrange and remove unused samples.")); + } + + //Check for unsaved patterns + for(PATTERNINDEX pat = writePatterns; pat < Patterns.Size(); pat++) + { + if(Patterns.IsValidPat(pat)) + { + AddToLog(LogWarning, U_("Warning: This track contains at least one pattern after the highest pattern number referred to in the sequence. Such patterns are not saved in the MOD format.")); + break; + } + } + + // Writing samples + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + if(sampleLength[smp] == 0) + { + continue; + } + const ModSample &sample = Samples[sampleSource[smp]]; + + const mpt::IO::Offset sampleStart = mpt::IO::TellWrite(f); + const size_t writtenBytes = MODSampleHeader::GetSampleFormat().WriteSample(f, sample, sampleLength[smp]); + + const int8 silence = 0; + + // Write padding byte if the sample size is odd. + if((writtenBytes % 2u) != 0) + { + mpt::IO::Write(f, silence); + } + + if(!sample.uFlags[CHN_LOOP] && writtenBytes >= 2) + { + // First two bytes of oneshot samples have to be 0 due to PT's one-shot loop + const mpt::IO::Offset sampleEnd = mpt::IO::TellWrite(f); + mpt::IO::SeekAbsolute(f, sampleStart); + mpt::IO::Write(f, silence); + mpt::IO::Write(f, silence); + mpt::IO::SeekAbsolute(f, sampleEnd); + } + } + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mt2.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mt2.cpp new file mode 100644 index 00000000..6be8a513 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mt2.cpp @@ -0,0 +1,1173 @@ +/* + * Load_mt2.cpp + * ------------ + * Purpose: MT2 (MadTracker 2) module loader + * Notes : A couple of things are not handled properly or not at all, such as internal effects and automation envelopes + * 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" +#ifdef MPT_EXTERNAL_SAMPLES +// For loading external samples +#include "../common/mptPathString.h" +#endif // MPT_EXTERNAL_SAMPLES +#ifdef MPT_WITH_VST +#include "../mptrack/Vstplug.h" +#endif // MPT_WITH_VST + +OPENMPT_NAMESPACE_BEGIN + +struct MT2FileHeader +{ + enum MT2HeaderFlags + { + packedPatterns = 0x01, + automation = 0x02, + drumsAutomation = 0x08, + masterAutomation = 0x10, + }; + + char signature[4]; // "MT20" + uint32le userID; + uint16le version; + char trackerName[32]; // "MadTracker 2.0" + char songName[64]; + uint16le numOrders; + uint16le restartPos; + uint16le numPatterns; + uint16le numChannels; + uint16le samplesPerTick; + uint8le ticksPerLine; + uint8le linesPerBeat; + uint32le flags; // See HeaderFlags + uint16le numInstruments; + uint16le numSamples; +}; + +MPT_BINARY_STRUCT(MT2FileHeader, 126) + + +struct MT2DrumsData +{ + uint16le numDrumPatterns; + uint16le DrumSamples[8]; + uint8le DrumPatternOrder[256]; +}; + +MPT_BINARY_STRUCT(MT2DrumsData, 274) + + +struct MT2TrackSettings +{ + uint16le volume; + uint8le trackfx; // Built-in effect type is used + uint8le output; + uint16le fxID; + uint16le trackEffectParam[64][8]; +}; + +MPT_BINARY_STRUCT(MT2TrackSettings, 1030) + + +struct MT2Command +{ + uint8 note; // 0=nothing, 97=note off + uint8 instr; + uint8 vol; + uint8 pan; + uint8 fxcmd; + uint8 fxparam1; + uint8 fxparam2; +}; + +MPT_BINARY_STRUCT(MT2Command, 7) + + +struct MT2EnvPoint +{ + uint16le x; + uint16le y; +}; + +MPT_BINARY_STRUCT(MT2EnvPoint, 4) + + +struct MT2Instrument +{ + enum EnvTypes + { + VolumeEnv = 1, + PanningEnv = 2, + PitchEnv = 4, + FilterEnv = 8, + }; + + uint16le numSamples; + uint8le groupMap[96]; + uint8le vibtype, vibsweep, vibdepth, vibrate; + uint16le fadeout; + uint16le nna; +}; + +MPT_BINARY_STRUCT(MT2Instrument, 106) + + +struct MT2IEnvelope +{ + uint8le flags; + uint8le numPoints; + uint8le sustainPos; + uint8le loopStart; + uint8le loopEnd; + uint8le reserved[3]; + MT2EnvPoint points[16]; +}; + +MPT_BINARY_STRUCT(MT2IEnvelope, 72) + + +// Note: The order of these fields differs a bit in MTIOModule_MT2.cpp - maybe just typos, I'm not sure. +// This struct follows the save format of MadTracker 2.6.1. +struct MT2InstrSynth +{ + uint8le synthID; + uint8le effectID; // 0 = Lowpass filter, 1 = Highpass filter + uint16le cutoff; // 100...11000 Hz + uint8le resonance; // 0...128 + uint8le attack; // 0...128 + uint8le decay; // 0...128 + uint8le midiChannel; // 0...15 + int8le device; // VST slot (positive) or MIDI device (negative) + int8le unknown1; // Missing in MTIOModule_MT2.cpp + uint8le volume; // 0...255 + int8le finetune; // -96...96 + int8le transpose; // -48...48 + uint8le unknown2; // Seems to be equal to instrument number. + uint8le unknown3; + uint8le midiProgram; + uint8le reserved[16]; +}; + +MPT_BINARY_STRUCT(MT2InstrSynth, 32) + + +struct MT2Sample +{ + uint32le length; + uint32le frequency; + uint8le depth; + uint8le channels; + uint8le flags; + uint8le loopType; + uint32le loopStart; + uint32le loopEnd; + uint16le volume; + int8le panning; + int8le note; + int16le spb; +}; + +MPT_BINARY_STRUCT(MT2Sample, 26) + + +struct MT2Group +{ + uint8le sample; + uint8le vol; // 0...128 + int8le pitch; // -128...127 + uint8le reserved[5]; +}; + +MPT_BINARY_STRUCT(MT2Group, 8) + + +struct MT2VST +{ + char dll[64]; + char programName[28]; + uint32le fxID; + uint32le fxVersion; + uint32le programNr; + uint8le useChunks; + uint8le track; + int8le pan; // Not imported - could use pan mix mode for D/W ratio, but this is not implemented for instrument plugins! + char reserved[17]; + uint32le n; +}; + +MPT_BINARY_STRUCT(MT2VST, 128) + + +static bool ConvertMT2Command(CSoundFile *that, ModCommand &m, MT2Command &p) +{ + bool hasLegacyTempo = false; + + // Note + m.note = NOTE_NONE; + if(p.note) m.note = (p.note > 96) ? NOTE_KEYOFF : (p.note + NOTE_MIN + 11); + // Instrument + m.instr = p.instr; + // Volume Column + if(p.vol >= 0x10 && p.vol <= 0x90) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = (p.vol - 0x10) / 2; + } else if(p.vol >= 0xA0 && p.vol <= 0xAF) + { + m.volcmd = VOLCMD_VOLSLIDEDOWN; + m.vol = (p.vol & 0x0F); + } else if(p.vol >= 0xB0 && p.vol <= 0xBF) + { + m.volcmd = VOLCMD_VOLSLIDEUP; + m.vol = (p.vol & 0x0F); + } else if(p.vol >= 0xC0 && p.vol <= 0xCF) + { + m.volcmd = VOLCMD_FINEVOLDOWN; + m.vol = (p.vol & 0x0F); + } else if(p.vol >= 0xD0 && p.vol <= 0xDF) + { + m.volcmd = VOLCMD_FINEVOLUP; + m.vol = (p.vol & 0x0F); + } + + // Effects + if(p.fxcmd || p.fxparam1 || p.fxparam2) + { + switch(p.fxcmd) + { + case 0x00: // FastTracker effect + m.command = p.fxparam2; + m.param = p.fxparam1; + CSoundFile::ConvertModCommand(m); +#ifdef MODPLUG_TRACKER + m.Convert(MOD_TYPE_XM, MOD_TYPE_IT, *that); +#else + MPT_UNREFERENCED_PARAMETER(that); +#endif // MODPLUG_TRACKER + if(p.fxparam2 == 0x0F) + hasLegacyTempo = true; + break; + + case 0x01: // Portamento up (on every tick) + m.command = CMD_PORTAMENTOUP; + m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4)); + break; + + case 0x02: // Portamento down (on every tick) + m.command = CMD_PORTAMENTODOWN; + m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4)); + break; + + case 0x03: // Tone Portamento (on every tick) + m.command = CMD_TONEPORTAMENTO; + m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4)); + break; + + case 0x04: // Vibrato + m.command = CMD_VIBRATO; + m.param = (p.fxparam2 & 0xF0) | (p.fxparam1 >> 4); + break; + + case 0x08: // Panning + Polarity (we can only import panning for now) + if(p.fxparam1) + { + m.command = CMD_PANNING8; + m.param = p.fxparam1; + } else if(p.fxparam2 == 1 || p.fxparam2 == 2) + { + // Invert left or right channel + m.command = CMD_S3MCMDEX; + m.param = 0x91; + } + break; + + case 0x0C: // Set volume (0x80 = 100%) + m.command = CMD_VOLUME; + m.param = p.fxparam2 / 2; + break; + + case 0x0F: // Set tempo, LPB and ticks (we can only import tempo for now) + if(p.fxparam2 != 0) + { + m.command = CMD_TEMPO; + m.param = p.fxparam2; + } else + { + m.command = CMD_SPEED; + m.param = (p.fxparam1 & 0x0F); + } + break; + + case 0x10: // Impulse Tracker effect + m.command = p.fxparam2; + m.param = p.fxparam1; + CSoundFile::S3MConvert(m, true); + if(m.command == CMD_TEMPO || m.command == CMD_SPEED) + hasLegacyTempo = true; + break; + + case 0x1D: // Gapper (like IT Tremor with old FX, i.e. 1D 00 XY = ontime X + 1 ticks, offtime Y + 1 ticks) + m.command = CMD_TREMOR; + m.param = p.fxparam1; + break; + + case 0x20: // Cutoff + Resonance (we can only import cutoff for now) + m.command = CMD_MIDI; + m.param = p.fxparam2 >> 1; + break; + + case 0x22: // Cutoff + Resonance + Attack + Decay (we can only import cutoff for now) + m.command = CMD_MIDI; + m.param = (p.fxparam2 & 0xF0) >> 1; + break; + + case 0x24: // Reverse + m.command = CMD_S3MCMDEX; + m.param = 0x9F; + break; + + case 0x80: // Track volume + m.command = CMD_CHANNELVOLUME; + m.param = p.fxparam2 / 4u; + break; + + case 0x9D: // Offset + delay + m.volcmd = VOLCMD_OFFSET; + m.vol = p.fxparam2 >> 3; + m.command = CMD_S3MCMDEX; + m.param = 0xD0 | std::min(p.fxparam1, uint8(0x0F)); + break; + + case 0xCC: // MIDI CC + //m.command = CMD_MIDI; + break; + + // TODO: More MT2 Effects + } + } + + if(p.pan) + { + if(m.command == CMD_NONE) + { + m.command = CMD_PANNING8; + m.param = p.pan; + } else if(m.volcmd == VOLCMD_NONE) + { + m.volcmd = VOLCMD_PANNING; + m.vol = p.pan / 4; + } + } + + return hasLegacyTempo; +} + + +// This doesn't really do anything but skipping the envelope chunk at the moment. +static void ReadMT2Automation(uint16 version, FileReader &file) +{ + uint32 flags; + uint32 trkfxid; + if(version >= 0x203) + { + flags = file.ReadUint32LE(); + trkfxid = file.ReadUint32LE(); + } else + { + flags = file.ReadUint16LE(); + trkfxid = file.ReadUint16LE(); + } + MPT_UNREFERENCED_PARAMETER(trkfxid); + while(flags != 0) + { + if(flags & 1) + { + file.Skip(4 + sizeof(MT2EnvPoint) * 64); + } + flags >>= 1; + } +} + + +static bool ValidateHeader(const MT2FileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.signature, "MT20", 4) + || fileHeader.version < 0x200 || fileHeader.version >= 0x300 + || fileHeader.numChannels < 1 || fileHeader.numChannels > 64 + || fileHeader.numOrders > 256 + || fileHeader.numInstruments >= MAX_INSTRUMENTS + || fileHeader.numSamples >= MAX_SAMPLES + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const MT2FileHeader &fileHeader) +{ + MPT_UNREFERENCED_PARAMETER(fileHeader); + return 256; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMT2(MemoryFileReader file, const uint64 *pfilesize) +{ + MT2FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadMT2(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + MT2FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_MT2); + InitializeChannels(); + + m_modFormat.formatName = MPT_UFORMAT("MadTracker {}.{}")(fileHeader.version >> 8, mpt::ufmt::hex0<2>(fileHeader.version & 0xFF)); + m_modFormat.type = U_("mt2"); + m_modFormat.madeWithTracker = mpt::ToUnicode(mpt::Charset::Windows1252, mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.trackerName)); + m_modFormat.charset = mpt::Charset::Windows1252; + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + m_nChannels = fileHeader.numChannels; + m_nDefaultSpeed = Clamp<uint8, uint8>(fileHeader.ticksPerLine, 1, 31); + m_nDefaultTempo.Set(125); + m_SongFlags = SONG_LINEARSLIDES | SONG_ITCOMPATGXX | SONG_EXFILTERRANGE; + m_nInstruments = fileHeader.numInstruments; + m_nSamples = fileHeader.numSamples; + m_nDefaultRowsPerBeat = Clamp<uint8, uint8>(fileHeader.linesPerBeat, 1, 32); + m_nDefaultRowsPerMeasure = m_nDefaultRowsPerBeat * 4; + m_nVSTiVolume = 48; + m_nSamplePreAmp = 48 * 2; // Double pre-amp because we will halve the volume of all non-drum instruments, because the volume of drum samples can exceed that of normal samples + + uint8 orders[256]; + file.ReadArray(orders); + ReadOrderFromArray(Order(), orders, fileHeader.numOrders); + Order().SetRestartPos(fileHeader.restartPos); + + // This value is supposed to be the size of the drums data, but in old MT2.0 files it's 8 bytes too small. + // MadTracker itself unconditionally reads 274 bytes here if the value is != 0, so we do the same. + const bool hasDrumChannels = file.ReadUint16LE() != 0; + FileReader drumData = file.ReadChunk(hasDrumChannels ? sizeof(MT2DrumsData) : 0); + FileReader extraData = file.ReadChunk(file.ReadUint32LE()); + + const CHANNELINDEX channelsWithoutDrums = m_nChannels; + static_assert(MAX_BASECHANNELS >= 64 + 8); + if(hasDrumChannels) + { + m_nChannels += 8; + } + + bool hasLegacyTempo = false; + + // Read patterns + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + ROWINDEX numRows = file.ReadUint16LE(); + FileReader chunk = file.ReadChunk((file.ReadUint32LE() + 1) & ~1); + + LimitMax(numRows, MAX_PATTERN_ROWS); + if(!numRows + || !(loadFlags & loadPatternData) + || !Patterns.Insert(pat, numRows)) + { + continue; + } + + if(fileHeader.flags & MT2FileHeader::packedPatterns) + { + ROWINDEX row = 0; + CHANNELINDEX chn = 0; + while(chunk.CanRead(1)) + { + MT2Command cmd; + + uint8 infobyte = chunk.ReadUint8(); + uint8 repeatCount = 0; + if(infobyte == 0xFF) + { + repeatCount = chunk.ReadUint8(); + infobyte = chunk.ReadUint8(); + } + if(infobyte & 0x7F) + { + ModCommand *m = Patterns[pat].GetpModCommand(row, chn); + MemsetZero(cmd); + if(infobyte & 0x01) cmd.note = chunk.ReadUint8(); + if(infobyte & 0x02) cmd.instr = chunk.ReadUint8(); + if(infobyte & 0x04) cmd.vol = chunk.ReadUint8(); + if(infobyte & 0x08) cmd.pan = chunk.ReadUint8(); + if(infobyte & 0x10) cmd.fxcmd = chunk.ReadUint8(); + if(infobyte & 0x20) cmd.fxparam1 = chunk.ReadUint8(); + if(infobyte & 0x40) cmd.fxparam2 = chunk.ReadUint8(); + hasLegacyTempo |= ConvertMT2Command(this, *m, cmd); + const ModCommand &orig = *m; + const ROWINDEX fillRows = std::min((uint32)repeatCount, (uint32)numRows - (row + 1)); + for(ROWINDEX r = 0; r < fillRows; r++) + { + m += GetNumChannels(); + // cppcheck false-positive + // cppcheck-suppress selfAssignment + *m = orig; + } + } + row += repeatCount + 1; + while(row >= numRows) { row -= numRows; chn++; } + if(chn >= channelsWithoutDrums) break; + } + } else + { + for(ROWINDEX row = 0; row < numRows; row++) + { + auto rowData = Patterns[pat].GetRow(row); + for(CHANNELINDEX chn = 0; chn < channelsWithoutDrums; chn++) + { + MT2Command cmd; + chunk.ReadStruct(cmd); + hasLegacyTempo |= ConvertMT2Command(this, rowData[chn], cmd); + } + } + } + } + + if(fileHeader.samplesPerTick > 1 && fileHeader.samplesPerTick < 5000) + { + if(hasLegacyTempo) + { + m_nDefaultTempo.SetRaw(Util::muldivr(110250, TEMPO::fractFact, fileHeader.samplesPerTick)); + m_nTempoMode = TempoMode::Classic; + } else + { + m_nDefaultTempo = TEMPO(44100.0 * 60.0 / (m_nDefaultSpeed * m_nDefaultRowsPerBeat * fileHeader.samplesPerTick)); + m_nTempoMode = TempoMode::Modern; + } + } + + // Read extra data + uint32 numVST = 0; + std::vector<int8> trackRouting(GetNumChannels(), 0); + while(extraData.CanRead(8)) + { + uint32 id = extraData.ReadUint32LE(); + FileReader chunk = extraData.ReadChunk(extraData.ReadUint32LE()); + + switch(id) + { + case MagicLE("BPM+"): + if(!hasLegacyTempo) + { + m_nTempoMode = TempoMode::Modern; + double d = chunk.ReadDoubleLE(); + if(d > 0.00000001) + { + m_nDefaultTempo = TEMPO(44100.0 * 60.0 / (m_nDefaultSpeed * m_nDefaultRowsPerBeat * d)); + } + } + break; + + case MagicLE("TFXM"): + break; + + case MagicLE("TRKO"): + break; + + case MagicLE("TRKS"): + m_nSamplePreAmp = chunk.ReadUint16LE() / 256u; // 131072 is 0dB... I think (that's how MTIOModule_MT2.cpp reads) + // Dirty workaround for modules that use track automation for a fade-in at the song start (e.g. Rock.mt2) + if(!m_nSamplePreAmp) + m_nSamplePreAmp = 48; + m_nVSTiVolume = m_nSamplePreAmp / 2u; + for(CHANNELINDEX c = 0; c < GetNumChannels(); c++) + { + MT2TrackSettings trackSettings; + if(chunk.ReadStruct(trackSettings)) + { + ChnSettings[c].nVolume = static_cast<uint8>(trackSettings.volume >> 10); // 32768 is 0dB + trackRouting[c] = trackSettings.output; + } + } + break; + + case MagicLE("TRKL"): + for(CHANNELINDEX i = 0; i < m_nChannels && chunk.CanRead(1); i++) + { + std::string name; + chunk.ReadNullString(name); + ChnSettings[i].szName = mpt::String::ReadBuf(mpt::String::spacePadded, name.c_str(), name.length()); + } + break; + + case MagicLE("PATN"): + for(PATTERNINDEX i = 0; i < fileHeader.numPatterns && chunk.CanRead(1) && Patterns.IsValidIndex(i); i++) + { + std::string name; + chunk.ReadNullString(name); + Patterns[i].SetName(name); + } + break; + + case MagicLE("MSG\0"): + chunk.Skip(1); // Show message on startup + m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF); + break; + + case MagicLE("PICT"): + break; + + case MagicLE("SUM\0"): + { + uint8 summaryMask[6]; + chunk.ReadArray(summaryMask); + std::string artist; + chunk.ReadNullString(artist); + if(artist != "Unregistered") + { + m_songArtist = mpt::ToUnicode(mpt::Charset::Windows1252, artist); + } + } + break; + + case MagicLE("TMAP"): + break; + case MagicLE("MIDI"): + break; + case MagicLE("TREQ"): + break; + + case MagicLE("VST2"): + numVST = chunk.ReadUint32LE(); +#ifdef MPT_WITH_VST + if(!(loadFlags & loadPluginData)) + { + break; + } + for(uint32 i = 0; i < std::min(numVST, uint32(MAX_MIXPLUGINS)); i++) + { + MT2VST vstHeader; + if(chunk.ReadStruct(vstHeader)) + { + if(fileHeader.version >= 0x0250) + chunk.Skip(16 * 4); // Parameter automation map for 16 parameters + + SNDMIXPLUGIN &mixPlug = m_MixPlugins[i]; + mixPlug.Destroy(); + std::string libraryName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, vstHeader.dll); + mixPlug.Info.szName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, vstHeader.programName); + if(libraryName.length() > 4 && libraryName[libraryName.length() - 4] == '.') + { + // Remove ".dll" from library name + libraryName.resize(libraryName.length() - 4 ); + } + mixPlug.Info.szLibraryName = libraryName; + mixPlug.Info.dwPluginId1 = Vst::kEffectMagic; + mixPlug.Info.dwPluginId2 = vstHeader.fxID; + if(vstHeader.track >= m_nChannels) + { + mixPlug.SetMasterEffect(true); + } else + { + if(!ChnSettings[vstHeader.track].nMixPlugin) + { + ChnSettings[vstHeader.track].nMixPlugin = static_cast<PLUGINDEX>(i + 1); + } else + { + // Channel already has plugin assignment - chain the plugins + PLUGINDEX outPlug = ChnSettings[vstHeader.track].nMixPlugin - 1; + while(true) + { + if(m_MixPlugins[outPlug].GetOutputPlugin() == PLUGINDEX_INVALID) + { + m_MixPlugins[outPlug].SetOutputPlugin(static_cast<PLUGINDEX>(i)); + break; + } + outPlug = m_MixPlugins[outPlug].GetOutputPlugin(); + } + } + } + + // Read plugin settings + uint32 dataSize; + if(vstHeader.useChunks) + { + // MT2 only ever calls effGetChunk for programs, and OpenMPT uses the defaultProgram value to determine + // whether it should use effSetChunk for programs or banks... + mixPlug.defaultProgram = -1; + LimitMax(vstHeader.n, std::numeric_limits<decltype(dataSize)>::max() - 4); + dataSize = vstHeader.n + 4; + } else + { + mixPlug.defaultProgram = vstHeader.programNr; + LimitMax(vstHeader.n, (std::numeric_limits<decltype(dataSize)>::max() / 4u) - 1); + dataSize = vstHeader.n * 4 + 4; + } + mixPlug.pluginData.resize(dataSize); + if(vstHeader.useChunks) + { + std::memcpy(mixPlug.pluginData.data(), "fEvN", 4); // 'NvEf' plugin data type + chunk.ReadRaw(mpt::span(mixPlug.pluginData.data() + 4, vstHeader.n)); + } else + { + auto memFile = std::make_pair(mpt::as_span(mixPlug.pluginData), mpt::IO::Offset(0)); + mpt::IO::WriteIntLE<uint32>(memFile, 0); // Plugin data type + for(uint32 param = 0; param < vstHeader.n; param++) + { + mpt::IO::Write(memFile, IEEE754binary32LE{chunk.ReadFloatLE()}); + } + } + } else + { + break; + } + } +#endif // MPT_WITH_VST + break; + } + } + +#ifndef NO_PLUGINS + // Now that we have both the track settings and plugins, establish the track routing by applying the same plugins to the source track as to the target track: + for(CHANNELINDEX c = 0; c < GetNumChannels(); c++) + { + int8 outTrack = trackRouting[c]; + if(outTrack > c && outTrack < GetNumChannels() && ChnSettings[outTrack].nMixPlugin != 0) + { + if(ChnSettings[c].nMixPlugin == 0) + { + ChnSettings[c].nMixPlugin = ChnSettings[outTrack].nMixPlugin; + } else + { + PLUGINDEX outPlug = ChnSettings[c].nMixPlugin - 1; + for(;;) + { + if(m_MixPlugins[outPlug].GetOutputPlugin() == PLUGINDEX_INVALID) + { + m_MixPlugins[outPlug].SetOutputPlugin(ChnSettings[outTrack].nMixPlugin - 1); + break; + } + outPlug = m_MixPlugins[outPlug].GetOutputPlugin(); + } + } + } + } +#endif // NO_PLUGINS + + // Read drum channels + INSTRUMENTINDEX drumMap[8] = { 0 }; + uint16 drumSample[8] = { 0 }; + if(hasDrumChannels) + { + MT2DrumsData drumHeader; + drumData.ReadStruct(drumHeader); + + // Allocate some instruments to handle the drum samples + for(INSTRUMENTINDEX i = 0; i < 8; i++) + { + drumMap[i] = GetNextFreeInstrument(m_nInstruments + 1); + drumSample[i] = drumHeader.DrumSamples[i]; + if(drumMap[i] != INSTRUMENTINDEX_INVALID) + { + ModInstrument *mptIns = AllocateInstrument(drumMap[i], drumHeader.DrumSamples[i] + 1); + if(mptIns != nullptr) + { + mptIns->name = MPT_AFORMAT("Drum #{}")(i+1); + } + } else + { + drumMap[i] = 0; + } + } + + // Get all the drum pattern chunks + std::vector<FileReader> patternChunks(drumHeader.numDrumPatterns); + for(uint32 pat = 0; pat < drumHeader.numDrumPatterns; pat++) + { + uint16 numRows = file.ReadUint16LE(); + patternChunks[pat] = file.ReadChunk(numRows * 32); + } + + std::vector<PATTERNINDEX> patMapping(fileHeader.numPatterns, PATTERNINDEX_INVALID); + for(uint32 ord = 0; ord < fileHeader.numOrders; ord++) + { + if(drumHeader.DrumPatternOrder[ord] >= drumHeader.numDrumPatterns || Order()[ord] >= fileHeader.numPatterns) + continue; + + // Figure out where to write this drum pattern + PATTERNINDEX writePat = Order()[ord]; + if(patMapping[writePat] == PATTERNINDEX_INVALID) + { + patMapping[writePat] = drumHeader.DrumPatternOrder[ord]; + } else if(patMapping[writePat] != drumHeader.DrumPatternOrder[ord]) + { + // Damn, this pattern has previously used a different drum pattern. Duplicate it... + PATTERNINDEX newPat = Patterns.Duplicate(writePat); + if(newPat != PATTERNINDEX_INVALID) + { + writePat = newPat; + Order()[ord] = writePat; + } + } + if(!Patterns.IsValidPat(writePat)) + continue; + + FileReader &chunk = patternChunks[drumHeader.DrumPatternOrder[ord]]; + chunk.Rewind(); + const ROWINDEX numRows = static_cast<ROWINDEX>(chunk.GetLength() / 32u); + for(ROWINDEX row = 0; row < Patterns[writePat].GetNumRows(); row++) + { + ModCommand *m = Patterns[writePat].GetpModCommand(row, m_nChannels - 8); + for(CHANNELINDEX chn = 0; chn < 8; chn++, m++) + { + *m = ModCommand::Empty(); + if(row >= numRows) + continue; + + uint8 drums[4]; + chunk.ReadArray(drums); + if(drums[0] & 0x80) + { + m->note = NOTE_MIDDLEC; + m->instr = static_cast<ModCommand::INSTR>(drumMap[chn]); + uint8 delay = drums[0] & 0x1F; + if(delay) + { + LimitMax(delay, uint8(0x0F)); + m->command = CMD_S3MCMDEX; + m->param = 0xD0 | delay; + } + m->volcmd = VOLCMD_VOLUME; + // Volume is 0...255, but 128 is equivalent to v64 - we compensate this by halving the global volume of all non-drum instruments + m->vol = static_cast<ModCommand::VOL>((static_cast<uint16>(drums[1]) + 3) / 4u); + } + } + } + } + } + + // Read automation envelopes + if(fileHeader.flags & MT2FileHeader::automation) + { + const uint32 numEnvelopes = ((fileHeader.flags & MT2FileHeader::drumsAutomation) ? m_nChannels : channelsWithoutDrums) + + ((fileHeader.version >= 0x0250) ? numVST : 0) + + ((fileHeader.flags & MT2FileHeader::masterAutomation) ? 1 : 0); + + for(uint32 pat = 0; pat < fileHeader.numPatterns; pat++) + { + for(uint32 env = 0; env < numEnvelopes && file.CanRead(4); env++) + { + // TODO + ReadMT2Automation(fileHeader.version, file); + } + } + } + + // Read instruments + std::vector<FileReader> instrChunks(255); + for(INSTRUMENTINDEX i = 0; i < 255; i++) + { + char instrName[32]; + file.ReadArray(instrName); + uint32 dataLength = file.ReadUint32LE(); + if(dataLength == 32) dataLength += 108 + sizeof(MT2IEnvelope) * 4; + if(fileHeader.version > 0x0201 && dataLength) dataLength += 4; + + FileReader instrChunk = instrChunks[i] = file.ReadChunk(dataLength); + + ModInstrument *mptIns = nullptr; + if(i < fileHeader.numInstruments) + { + // Default sample assignment if there is no data chunk? Fixes e.g. instrument 33 in Destiny - Dream Alone.mt2 + mptIns = AllocateInstrument(i + 1, i + 1); + } + if(mptIns == nullptr) + continue; + + mptIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrName); + + if(!dataLength) + continue; + + MT2Instrument insHeader; + instrChunk.ReadStruct(insHeader); + uint16 flags = 0; + if(fileHeader.version >= 0x0201) flags = instrChunk.ReadUint16LE(); + uint32 envMask = MT2Instrument::VolumeEnv | MT2Instrument::PanningEnv; + if(fileHeader.version >= 0x0202) envMask = instrChunk.ReadUint32LE(); + + mptIns->nFadeOut = insHeader.fadeout; + const NewNoteAction NNA[4] = { NewNoteAction::NoteCut, NewNoteAction::Continue, NewNoteAction::NoteOff, NewNoteAction::NoteFade }; + const DuplicateCheckType DCT[4] = { DuplicateCheckType::None, DuplicateCheckType::Note, DuplicateCheckType::Sample, DuplicateCheckType::Instrument }; + const DuplicateNoteAction DNA[4] = { DuplicateNoteAction::NoteCut, DuplicateNoteAction::NoteFade /* actually continue, but IT doesn't have that */, DuplicateNoteAction::NoteOff, DuplicateNoteAction::NoteFade }; + mptIns->nNNA = NNA[insHeader.nna & 3]; + mptIns->nDCT = DCT[(insHeader.nna >> 8) & 3]; + mptIns->nDNA = DNA[(insHeader.nna >> 12) & 3]; + + // Load envelopes + for(uint32 env = 0; env < 4; env++) + { + if(envMask & 1) + { + MT2IEnvelope mt2Env; + instrChunk.ReadStruct(mt2Env); + + const EnvelopeType envType[4] = { ENV_VOLUME, ENV_PANNING, ENV_PITCH, ENV_PITCH }; + InstrumentEnvelope &mptEnv = mptIns->GetEnvelope(envType[env]); + + mptEnv.dwFlags.set(ENV_FILTER, (env == 3) && (mt2Env.flags & 1) != 0); + mptEnv.dwFlags.set(ENV_ENABLED, (mt2Env.flags & 1) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (mt2Env.flags & 2) != 0); + mptEnv.dwFlags.set(ENV_LOOP, (mt2Env.flags & 4) != 0); + mptEnv.resize(std::min(mt2Env.numPoints.get(), uint8(16))); + mptEnv.nSustainStart = mptEnv.nSustainEnd = mt2Env.sustainPos; + mptEnv.nLoopStart = mt2Env.loopStart; + mptEnv.nLoopEnd = mt2Env.loopEnd; + + for(uint32 p = 0; p < mptEnv.size(); p++) + { + mptEnv[p].tick = mt2Env.points[p].x; + mptEnv[p].value = static_cast<uint8>(Clamp<uint16, uint16>(mt2Env.points[p].y, 0, 64)); + } + } + envMask >>= 1; + } + if(!mptIns->VolEnv.dwFlags[ENV_ENABLED] && mptIns->nNNA != NewNoteAction::NoteFade) + { + mptIns->nFadeOut = int16_max; + } + + mptIns->SetCutoff(0x7F, true); + mptIns->SetResonance(0, true); + + if(flags) + { + MT2InstrSynth synthData; + instrChunk.ReadStruct(synthData); + + if(flags & 2) + { + mptIns->SetCutoff(FrequencyToCutOff(synthData.cutoff), true); + mptIns->SetResonance(synthData.resonance, true); + } + mptIns->filterMode = synthData.effectID == 1 ? FilterMode::HighPass : FilterMode::LowPass; + if(flags & 4) + { + // VSTi / MIDI synth enabled + mptIns->nMidiChannel = synthData.midiChannel + 1; + mptIns->nMixPlug = static_cast<PLUGINDEX>(synthData.device + 1); + if(synthData.device < 0) + { + // TODO: This is a MIDI device - maybe use MIDI I/O plugin to emulate those? + mptIns->nMidiProgram = synthData.midiProgram + 1; // MT2 only seems to use this for MIDI devices, not VSTis! + } + if(synthData.transpose) + { + for(uint32 n = 0; n < std::size(mptIns->NoteMap); n++) + { + int note = NOTE_MIN + n + synthData.transpose; + Limit(note, NOTE_MIN, NOTE_MAX); + mptIns->NoteMap[n] = static_cast<uint8>(note); + } + } + // Instruments with plugin assignments never play samples at the same time! + mptIns->AssignSample(0); + } + } + } + + // Read sample headers + std::bitset<256> sampleNoInterpolation; + std::bitset<256> sampleSynchronized; + for(SAMPLEINDEX i = 0; i < 256; i++) + { + char sampleName[32]; + file.ReadArray(sampleName); + uint32 dataLength = file.ReadUint32LE(); + + FileReader sampleChunk = file.ReadChunk(dataLength); + + if(i < fileHeader.numSamples) + { + m_szNames[i + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleName); + } + + if(dataLength && i < fileHeader.numSamples) + { + ModSample &mptSmp = Samples[i + 1]; + mptSmp.Initialize(MOD_TYPE_IT); + mptSmp.SetDefaultCuePoints(); + MT2Sample sampleHeader; + sampleChunk.ReadStruct(sampleHeader); + + mptSmp.nLength = sampleHeader.length; + mptSmp.nC5Speed = sampleHeader.frequency; + if(sampleHeader.depth > 1) { mptSmp.uFlags.set(CHN_16BIT); mptSmp.nLength /= 2u; } + if(sampleHeader.channels > 1) { mptSmp.uFlags.set(CHN_STEREO); mptSmp.nLength /= 2u; } + if(sampleHeader.loopType == 1) mptSmp.uFlags.set(CHN_LOOP); + else if(sampleHeader.loopType == 2) mptSmp.uFlags.set(CHN_LOOP | CHN_PINGPONGLOOP); + mptSmp.nLoopStart = sampleHeader.loopStart; + mptSmp.nLoopEnd = sampleHeader.loopEnd; + mptSmp.nVolume = sampleHeader.volume >> 7; + if(sampleHeader.panning == -128) + mptSmp.uFlags.set(CHN_SURROUND); + else + mptSmp.nPan = sampleHeader.panning + 128; + mptSmp.uFlags.set(CHN_PANNING); + mptSmp.RelativeTone = sampleHeader.note; + + if(sampleHeader.flags & 2) + { + // Sample is synchronized to beat + // The synchronization part is not supported in OpenMPT, but synchronized samples also always play at the same pitch as C-5, which we CAN do! + sampleSynchronized[i] = true; + //mptSmp.nC5Speed = Util::muldiv(mptSmp.nC5Speed, sampleHeader.spb, 22050); + } + if(sampleHeader.flags & 5) + { + // External sample + mptSmp.uFlags.set(SMP_KEEPONDISK); + } + if(sampleHeader.flags & 8) + { + sampleNoInterpolation[i] = true; + for(INSTRUMENTINDEX drum = 0; drum < 8; drum++) + { + if(drumSample[drum] == i && Instruments[drumMap[drum]] != nullptr) + { + Instruments[drumMap[drum]]->resampling = SRCMODE_NEAREST; + } + } + } + } + } + + // Read sample groups + for(INSTRUMENTINDEX ins = 0; ins < fileHeader.numInstruments; ins++) + { + if(instrChunks[ins].GetLength()) + { + FileReader &chunk = instrChunks[ins]; + MT2Instrument insHeader; + chunk.Rewind(); + chunk.ReadStruct(insHeader); + + std::vector<MT2Group> groups; + file.ReadVector(groups, insHeader.numSamples); + + ModInstrument *mptIns = Instruments[ins + 1]; + // Instruments with plugin assignments never play samples at the same time! + if(mptIns == nullptr || mptIns->nMixPlug != 0) + continue; + + mptIns->nGlobalVol = 32; // Compensate for extended dynamic range of drum instruments + mptIns->AssignSample(0); + for(uint32 note = 0; note < 96; note++) + { + if(insHeader.groupMap[note] < insHeader.numSamples) + { + const MT2Group &group = groups[insHeader.groupMap[note]]; + SAMPLEINDEX sample = group.sample + 1; + mptIns->Keyboard[note + 11 + NOTE_MIN] = sample; + if(sample > 0 && sample <= m_nSamples) + { + ModSample &mptSmp = Samples[sample]; + mptSmp.nVibType = static_cast<VibratoType>(insHeader.vibtype & 3); // In fact, MT2 only implements sine vibrato + mptSmp.nVibSweep = insHeader.vibsweep; + mptSmp.nVibDepth = insHeader.vibdepth; + mptSmp.nVibRate = insHeader.vibrate; + mptSmp.nGlobalVol = uint16(group.vol) * 2; + mptSmp.nFineTune = group.pitch; + if(sampleNoInterpolation[sample - 1]) + { + mptIns->resampling = SRCMODE_NEAREST; + } + if(sampleSynchronized[sample - 1]) + { + mptIns->NoteMap[note + 11 + NOTE_MIN] = NOTE_MIDDLEC; + } + } + // TODO: volume, finetune for duplicated samples + } + } + } + } + + if(!(loadFlags & loadSampleData)) + return true; + + // Read sample data + for(SAMPLEINDEX i = 0; i < m_nSamples; i++) + { + ModSample &mptSmp = Samples[i + 1]; + mptSmp.Transpose(-(mptSmp.RelativeTone - 49 - (mptSmp.nFineTune / 128.0)) / 12.0); + mptSmp.nFineTune = 0; + mptSmp.RelativeTone = 0; + + if(!mptSmp.uFlags[SMP_KEEPONDISK]) + { + SampleIO( + mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoSplit : SampleIO::mono, + SampleIO::littleEndian, + SampleIO::MT2) + .ReadSample(mptSmp, file); + } else + { + // External sample + const uint32 filenameSize = file.ReadUint32LE(); + file.Skip(12); // Reserved + std::string filename; + file.ReadString<mpt::String::maybeNullTerminated>(filename, filenameSize); + mptSmp.filename = filename; + +#if defined(MPT_EXTERNAL_SAMPLES) + if(filename.length() >= 2 + && filename[0] != '\\' // Relative path on same drive + && filename[1] != ':') // Absolute path + { + // Relative path in same folder or sub folder + filename = ".\\" + filename; + } + SetSamplePath(i + 1, mpt::PathString::FromLocaleSilent(filename)); +#elif !defined(LIBOPENMPT_BUILD_TEST) + AddToLog(LogWarning, MPT_UFORMAT("Loading external sample {} ('{}') failed: External samples are not supported.")(i + 1, mpt::ToUnicode(GetCharsetFile(), filename))); +#endif // MPT_EXTERNAL_SAMPLES + } + } + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mtm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mtm.cpp new file mode 100644 index 00000000..dffadb9b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mtm.cpp @@ -0,0 +1,324 @@ +/* + * Load_mtm.cpp + * ------------ + * Purpose: MTM (MultiTracker) module loader + * Notes : (currently none) + * 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" + +OPENMPT_NAMESPACE_BEGIN + +// File Header +struct MTMFileHeader +{ + char id[3]; // MTM file marker + uint8le version; // Tracker version + char songName[20]; // ASCIIZ songname + uint16le numTracks; // Number of tracks saved + uint8le lastPattern; // Last pattern number saved + uint8le lastOrder; // Last order number to play (songlength-1) + uint16le commentSize; // Length of comment field + uint8le numSamples; // Number of samples saved + uint8le attribute; // Attribute byte (unused) + uint8le beatsPerTrack; // Numbers of rows in every pattern (MultiTracker itself does not seem to support values != 64) + uint8le numChannels; // Number of channels used + uint8le panPos[32]; // Channel pan positions +}; + +MPT_BINARY_STRUCT(MTMFileHeader, 66) + + +// Sample Header +struct MTMSampleHeader +{ + char samplename[22]; + uint32le length; + uint32le loopStart; + uint32le loopEnd; + int8le finetune; + uint8le volume; + uint8le attribute; + + // Convert an MTM sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.nVolume = std::min(uint16(volume * 4), uint16(256)); + if(length > 2) + { + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = std::max(loopEnd.get(), uint32(1)) - 1; + LimitMax(mptSmp.nLoopEnd, mptSmp.nLength); + if(mptSmp.nLoopStart + 4 >= mptSmp.nLoopEnd) + mptSmp.nLoopStart = mptSmp.nLoopEnd = 0; + if(mptSmp.nLoopEnd > 2) + mptSmp.uFlags.set(CHN_LOOP); + mptSmp.nFineTune = finetune; // Uses MOD units but allows the full int8 range rather than just -8...+7 so we keep the value as-is and convert it during playback + mptSmp.nC5Speed = ModSample::TransposeToFrequency(0, finetune * 16); + + if(attribute & 0x01) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + } + } +}; + +MPT_BINARY_STRUCT(MTMSampleHeader, 37) + + +static bool ValidateHeader(const MTMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.id, "MTM", 3) + || fileHeader.version >= 0x20 + || fileHeader.lastOrder > 127 + || fileHeader.beatsPerTrack > 64 + || fileHeader.numChannels > 32 + || fileHeader.numChannels == 0 + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const MTMFileHeader &fileHeader) +{ + return sizeof(MTMSampleHeader) * fileHeader.numSamples + 128 + 192 * fileHeader.numTracks + 64 * (fileHeader.lastPattern + 1) + fileHeader.commentSize; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMTM(MemoryFileReader file, const uint64 *pfilesize) +{ + MTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadMTM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + MTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_MTM); + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + m_nSamples = fileHeader.numSamples; + m_nChannels = fileHeader.numChannels; + + m_modFormat.formatName = U_("MultiTracker"); + m_modFormat.type = U_("mtm"); + m_modFormat.madeWithTracker = MPT_UFORMAT("MultiTracker {}.{}")(fileHeader.version >> 4, fileHeader.version & 0x0F); + m_modFormat.charset = mpt::Charset::CP437; + + // Reading instruments + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + MTMSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename); + } + + // Setting Channel Pan Position + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = ((fileHeader.panPos[chn] & 0x0F) << 4) + 8; + } + + // Reading pattern order + uint8 orders[128]; + file.ReadArray(orders); + ReadOrderFromArray(Order(), orders, fileHeader.lastOrder + 1, 0xFF, 0xFE); + + // Reading Patterns + const ROWINDEX rowsPerPat = fileHeader.beatsPerTrack ? fileHeader.beatsPerTrack : 64; + FileReader tracks = file.ReadChunk(192 * fileHeader.numTracks); + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.lastPattern + 1); + + bool hasSpeed = false, hasTempo = false; + for(PATTERNINDEX pat = 0; pat <= fileHeader.lastPattern; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, rowsPerPat)) + { + file.Skip(64); + continue; + } + + for(CHANNELINDEX chn = 0; chn < 32; chn++) + { + uint16 track = file.ReadUint16LE(); + if(track == 0 || track > fileHeader.numTracks || chn >= GetNumChannels()) + { + continue; + } + + tracks.Seek(192 * (track - 1)); + + ModCommand *m = Patterns[pat].GetpModCommand(0, chn); + for(ROWINDEX row = 0; row < rowsPerPat; row++, m += GetNumChannels()) + { + const auto [noteInstr, instrCmd, par] = tracks.ReadArray<uint8, 3>(); + + if(noteInstr & 0xFC) + m->note = (noteInstr >> 2) + 36 + NOTE_MIN; + m->instr = ((noteInstr & 0x03) << 4) | (instrCmd >> 4); + uint8 cmd = instrCmd & 0x0F; + uint8 param = par; + if(cmd == 0x0A) + { + if(param & 0xF0) param &= 0xF0; else param &= 0x0F; + } else if(cmd == 0x08) + { + // No 8xx panning in MultiTracker, only E8x + cmd = param = 0; + } else if(cmd == 0x0E) + { + // MultiTracker does not support these commands + switch(param & 0xF0) + { + case 0x00: + case 0x30: + case 0x40: + case 0x60: + case 0x70: + case 0xF0: + cmd = param = 0; + break; + } + } + if(cmd != 0 || param != 0) + { + m->command = cmd; + m->param = param; + ConvertModCommand(*m); +#ifdef MODPLUG_TRACKER + m->Convert(MOD_TYPE_MTM, MOD_TYPE_S3M, *this); +#endif + if(m->command == CMD_SPEED) + hasSpeed = true; + else if(m->command == CMD_TEMPO) + hasTempo = true; + } + } + } + } + + // Curiously, speed commands reset the tempo to 125 in MultiTracker, and tempo commands reset the speed to 6. + // External players of the time (e.g. DMP) did not implement this quirk and assumed a more ProTracker-like interpretation of speed and tempo. + // Quite a few musicians created MTMs that make use DMP's speed and tempo interpretation, which in return means that they will play too + // fast or too slow in MultiTracker. On the other hand there are also a few MTMs that break when using ProTracker-like speed and tempo. + // As a way to support as many modules of both types as possible, we will assume a ProTracker-like interpretation if both speed and tempo + // commands are found on the same line, and a MultiTracker-like interpretation when they are never found on the same line. + if(hasSpeed && hasTempo) + { + bool hasSpeedAndTempoOnSameRow = false; + for(const auto &pattern : Patterns) + { + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + const auto rowBase = pattern.GetRow(row); + bool hasSpeedOnRow = false, hasTempoOnRow = false; + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + if(rowBase[chn].command == CMD_SPEED) + hasSpeedOnRow = true; + else if(rowBase[chn].command == CMD_TEMPO) + hasTempoOnRow = true; + } + if(hasSpeedOnRow && hasTempoOnRow) + { + hasSpeedAndTempoOnSameRow = true; + break; + } + } + if(hasSpeedAndTempoOnSameRow) + break; + } + + if(!hasSpeedAndTempoOnSameRow) + { + for(auto &pattern : Patterns) + { + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + const auto rowBase = pattern.GetRow(row); + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + if(rowBase[chn].command == CMD_SPEED || rowBase[chn].command == CMD_TEMPO) + { + const bool writeTempo = rowBase[chn].command == CMD_SPEED; + pattern.WriteEffect(EffectWriter(writeTempo ? CMD_TEMPO : CMD_SPEED, writeTempo ? 125 : 6).Row(row)); + break; + } + } + } + } + } + } + + if(fileHeader.commentSize != 0) + { + // Read message with a fixed line length of 40 characters + // (actually the last character is always null, so make that 39 + 1 padding byte) + m_songMessage.ReadFixedLineLength(file, fileHeader.commentSize, 39, 1); + } + + // Reading Samples + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + SampleIO( + Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM) + .ReadSample(Samples[smp], file); + } + } + + m_nMinPeriod = 64; + m_nMaxPeriod = 32767; + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_mus_km.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mus_km.cpp new file mode 100644 index 00000000..da67bb86 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_mus_km.cpp @@ -0,0 +1,385 @@ +/* + * Load_mus_km.cpp + * --------------- + * Purpose: Karl Morton Music Format module loader + * Notes : This is probably not the official name of this format. + * Karl Morton's engine has been used in Psycho Pinball and Micro Machines 2 and also Back To Baghdad + * but the latter game only uses its sound effect format, not the music format. + * So there are only two known games using this music format, and no official tools or documentation are available. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct KMChunkHeader +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idSONG = MagicLE("SONG"), + idSMPL = MagicLE("SMPL"), + }; + + uint32le id; // See ChunkIdentifiers + uint32le length; // Chunk size including header + + size_t GetLength() const + { + return length <= 8 ? 0 : (length - 8); + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } + +}; + +MPT_BINARY_STRUCT(KMChunkHeader, 8) + + +struct KMSampleHeader +{ + char name[32]; + uint32le loopStart; + uint32le size; +}; + +MPT_BINARY_STRUCT(KMSampleHeader, 40) + + +struct KMSampleReference +{ + char name[32]; + uint8 finetune; + uint8 volume; +}; + +MPT_BINARY_STRUCT(KMSampleReference, 34) + + +struct KMSongHeader +{ + char name[32]; + KMSampleReference samples[31]; + + uint16le unknown; // always 0 + uint32le numChannels; + uint32le restartPos; + uint32le musicSize; +}; + +MPT_BINARY_STRUCT(KMSongHeader, 32 + 31 * 34 + 14) + + +struct KMFileHeader +{ + KMChunkHeader chunkHeader; + KMSongHeader songHeader; +}; + +MPT_BINARY_STRUCT(KMFileHeader, sizeof(KMChunkHeader) + sizeof(KMSongHeader)) + + +static uint64 GetHeaderMinimumAdditionalSize(const KMFileHeader &fileHeader) +{ + // Require room for at least one more sample chunk header + return static_cast<uint64>(fileHeader.songHeader.musicSize) + sizeof(KMChunkHeader); +} + + +// Check if string only contains printable characters and doesn't contain any garbage after the required terminating null +static bool IsValidKMString(const char (&str)[32]) +{ + bool nullFound = false; + for(char c : str) + { + if(c > 0x00 && c < 0x20) + return false; + else if(c == 0x00) + nullFound = true; + else if(nullFound) + return false; + } + return nullFound; +} + + +static bool ValidateHeader(const KMFileHeader &fileHeader) +{ + if(fileHeader.chunkHeader.id != KMChunkHeader::idSONG + || fileHeader.chunkHeader.length < sizeof(fileHeader) + || fileHeader.chunkHeader.length - sizeof(fileHeader) != fileHeader.songHeader.musicSize + || fileHeader.chunkHeader.length > 0x40000 // That's enough space for 256 crammed 64-row patterns ;) + || fileHeader.songHeader.unknown != 0 + || fileHeader.songHeader.numChannels < 1 + || fileHeader.songHeader.numChannels > 4 // Engine rejects anything above 32, channels 5 to 32 are simply ignored + || !IsValidKMString(fileHeader.songHeader.name)) + { + return false; + } + + for(const auto &sample : fileHeader.songHeader.samples) + { + if(sample.finetune > 15 || sample.volume > 64 || !IsValidKMString(sample.name)) + return false; + } + + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMUS_KM(MemoryFileReader file, const uint64 *pfilesize) +{ + KMFileHeader fileHeader; + if(!file.Read(fileHeader)) + return ProbeWantMoreData; + if(!ValidateHeader(fileHeader)) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadMUS_KM(FileReader &file, ModLoadingFlags loadFlags) +{ + { + file.Rewind(); + KMFileHeader fileHeader; + if(!file.Read(fileHeader)) + return false; + if(!ValidateHeader(fileHeader)) + return false; + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + } + + file.Rewind(); + + const auto chunks = ChunkReader(file).ReadChunks<KMChunkHeader>(1); + auto songChunks = chunks.GetAllChunks(KMChunkHeader::idSONG); + auto sampleChunks = chunks.GetAllChunks(KMChunkHeader::idSMPL); + + if(songChunks.empty() || sampleChunks.empty()) + return false; + + InitializeGlobals(MOD_TYPE_MOD); + InitializeChannels(); + m_SongFlags = SONG_AMIGALIMITS | SONG_IMPORTED | SONG_ISAMIGA; // Yes, those were not Amiga games but the format fully conforms to Amiga limits, so allow the Amiga Resampler to be used. + m_nChannels = 4; + m_nSamples = 0; + + static constexpr uint16 MUS_SAMPLE_UNUSED = 255; // Sentinel value to check if a sample needs to be duplicated + for(auto &chunk : sampleChunks) + { + if(!CanAddMoreSamples()) + break; + m_nSamples++; + ModSample &mptSample = Samples[m_nSamples]; + mptSample.Initialize(MOD_TYPE_MOD); + + KMSampleHeader sampleHeader; + if(!chunk.Read(sampleHeader) + || !IsValidKMString(sampleHeader.name)) + return false; + + m_szNames[m_nSamples] = sampleHeader.name; + mptSample.nLoopEnd = mptSample.nLength = sampleHeader.size; + mptSample.nLoopStart = sampleHeader.loopStart; + mptSample.uFlags.set(CHN_LOOP); + mptSample.nVolume = MUS_SAMPLE_UNUSED; + + if(!(loadFlags & loadSampleData)) + continue; + + SampleIO(SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(mptSample, chunk); + } + + bool firstSong = true; + for(auto &chunk : songChunks) + { + if(!firstSong && !Order.AddSequence()) + break; + firstSong = false; + Order().clear(); + + KMSongHeader songHeader; + if(!chunk.Read(songHeader) + || songHeader.unknown != 0 + || songHeader.numChannels < 1 + || songHeader.numChannels > 4) + return false; + + Order().SetName(mpt::ToUnicode(mpt::Charset::CP437, songHeader.name)); + + FileReader musicData = (loadFlags & loadPatternData) ? chunk.ReadChunk(songHeader.musicSize) : FileReader{}; + + // Map the samples for this subsong + std::array<SAMPLEINDEX, 32> sampleMap{}; + for(uint8 smp = 1; smp <= 31; smp++) + { + const auto &srcSample = songHeader.samples[smp - 1]; + const auto srcName = mpt::String::ReadAutoBuf(srcSample.name); + if(srcName.empty()) + continue; + if(srcSample.finetune > 15 || srcSample.volume > 64 || !IsValidKMString(srcSample.name)) + return false; + + const auto finetune = MOD2XMFineTune(srcSample.finetune); + const uint16 volume = srcSample.volume * 4u; + + SAMPLEINDEX copyFrom = 0; + for(SAMPLEINDEX srcSmp = 1; srcSmp <= m_nSamples; srcSmp++) + { + if(srcName != m_szNames[srcSmp]) + continue; + + auto &mptSample = Samples[srcSmp]; + sampleMap[smp] = srcSmp; + if(mptSample.nVolume == MUS_SAMPLE_UNUSED + || (mptSample.nFineTune == finetune && mptSample.nVolume == volume)) + { + // Sample was not used yet, or it uses the same finetune and volume + mptSample.nFineTune = finetune; + mptSample.nVolume = volume; + copyFrom = 0; + break; + } else + { + copyFrom = srcSmp; + } + } + if(copyFrom && CanAddMoreSamples()) + { + m_nSamples++; + sampleMap[smp] = m_nSamples; + const auto &smpFrom = Samples[copyFrom]; + auto &newSample = Samples[m_nSamples]; + newSample.FreeSample(); + newSample = smpFrom; + newSample.nFineTune = finetune; + newSample.nVolume = volume; + newSample.CopyWaveform(smpFrom); + m_szNames[m_nSamples] = m_szNames[copyFrom]; + } + } + + struct ChannelState + { + ModCommand prevCommand; + uint8 repeat = 0; + }; + std::array<ChannelState, 4> chnStates{}; + + static constexpr ROWINDEX MUS_PATTERN_LENGTH = 64; + const CHANNELINDEX numChannels = static_cast<CHANNELINDEX>(songHeader.numChannels); + PATTERNINDEX pat = PATTERNINDEX_INVALID; + ROWINDEX row = MUS_PATTERN_LENGTH; + ROWINDEX restartRow = 0; + uint32 repeatsLeft = 0; + while(repeatsLeft || musicData.CanRead(1)) + { + row++; + if(row >= MUS_PATTERN_LENGTH) + { + pat = Patterns.InsertAny(MUS_PATTERN_LENGTH); + if(pat == PATTERNINDEX_INVALID) + break; + + Order().push_back(pat); + row = 0; + } + + ModCommand *m = Patterns[pat].GetpModCommand(row, 0); + for(CHANNELINDEX chn = 0; chn < numChannels; chn++, m++) + { + auto &chnState = chnStates[chn]; + if(chnState.repeat) + { + chnState.repeat--; + repeatsLeft--; + *m = chnState.prevCommand; + continue; + } + + if(!musicData.CanRead(1)) + continue; + + if(musicData.GetPosition() == songHeader.restartPos) + { + Order().SetRestartPos(Order().GetLastIndex()); + restartRow = row; + } + + const uint8 note = musicData.ReadUint8(); + if(note & 0x80) + { + chnState.repeat = note & 0x7F; + repeatsLeft += chnState.repeat; + *m = chnState.prevCommand; + continue; + } + + if(note > 0 && note <= 3 * 12) + m->note = note + NOTE_MIDDLEC - 13; + + const auto instr = musicData.ReadUint8(); + m->instr = static_cast<ModCommand::INSTR>(sampleMap[instr & 0x1F]); + + if(instr & 0x80) + { + m->command = chnState.prevCommand.command; + m->param = chnState.prevCommand.param; + } else + { + static constexpr struct { ModCommand::COMMAND command; uint8 mask; } effTrans[] = + { + {CMD_VOLUME, 0x00}, {CMD_MODCMDEX, 0xA0}, {CMD_MODCMDEX, 0xB0}, {CMD_MODCMDEX, 0x10}, + {CMD_MODCMDEX, 0x20}, {CMD_MODCMDEX, 0x50}, {CMD_OFFSET, 0x00}, {CMD_TONEPORTAMENTO, 0x00}, + {CMD_TONEPORTAVOL, 0x00}, {CMD_VIBRATO, 0x00}, {CMD_VIBRATOVOL, 0x00}, {CMD_ARPEGGIO, 0x00}, + {CMD_PORTAMENTOUP, 0x00}, {CMD_PORTAMENTODOWN, 0x00}, {CMD_VOLUMESLIDE, 0x00}, {CMD_MODCMDEX, 0x90}, + {CMD_TONEPORTAMENTO, 0xFF}, {CMD_MODCMDEX, 0xC0}, {CMD_SPEED, 0x00}, {CMD_TREMOLO, 0x00}, + }; + + const auto [command, param] = musicData.ReadArray<uint8, 2>(); + if(command < std::size(effTrans)) + { + m->command = effTrans[command].command; + m->param = param; + if(m->command == CMD_SPEED && m->param >= 0x20) + m->command = CMD_TEMPO; + else if(effTrans[command].mask) + m->param = effTrans[command].mask | (m->param & 0x0F); + } + } + + chnState.prevCommand = *m; + } + } + + if((restartRow != 0 || row < (MUS_PATTERN_LENGTH - 1u)) && pat != PATTERNINDEX_INVALID) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, static_cast<ModCommand::PARAM>(restartRow)).Row(row).RetryNextRow()); + } + } + + Order.SetSequence(0); + + m_modFormat.formatName = U_("Karl Morton Music Format"); + m_modFormat.type = U_("mus"); + m_modFormat.charset = mpt::Charset::CP437; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_okt.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_okt.cpp new file mode 100644 index 00000000..e0b33ee6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_okt.cpp @@ -0,0 +1,499 @@ +/* + * Load_okt.cpp + * ------------ + * Purpose: OKT (Oktalyzer) module loader + * Notes : (currently none) + * Authors: Storlek (Original author - http://schismtracker.org/ - code ported with permission) + * Johannes Schultz (OpenMPT Port, tweaks) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct OktIffChunk +{ + // IFF chunk names + enum ChunkIdentifiers + { + idCMOD = MagicBE("CMOD"), + idSAMP = MagicBE("SAMP"), + idSPEE = MagicBE("SPEE"), + idSLEN = MagicBE("SLEN"), + idPLEN = MagicBE("PLEN"), + idPATT = MagicBE("PATT"), + idPBOD = MagicBE("PBOD"), + idSBOD = MagicBE("SBOD"), + }; + + uint32be signature; // IFF chunk name + uint32be chunksize; // chunk size without header +}; + +MPT_BINARY_STRUCT(OktIffChunk, 8) + +struct OktSample +{ + char name[20]; + uint32be length; // length in bytes + uint16be loopStart; // *2 for real value + uint16be loopLength; // ditto + uint16be volume; // default volume + uint16be type; // 7-/8-bit sample +}; + +MPT_BINARY_STRUCT(OktSample, 32) + + +// Parse the sample header block +static void ReadOKTSamples(FileReader &chunk, CSoundFile &sndFile) +{ + sndFile.m_nSamples = std::min(static_cast<SAMPLEINDEX>(chunk.BytesLeft() / sizeof(OktSample)), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1)); + + for(SAMPLEINDEX smp = 1; smp <= sndFile.GetNumSamples(); smp++) + { + ModSample &mptSmp = sndFile.GetSample(smp); + OktSample oktSmp; + chunk.ReadStruct(oktSmp); + + mptSmp.Initialize(); + sndFile.m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, oktSmp.name); + + mptSmp.nC5Speed = 8287; + mptSmp.nVolume = std::min(oktSmp.volume.get(), uint16(64)) * 4u; + mptSmp.nLength = oktSmp.length & ~1; + mptSmp.cues[0] = oktSmp.type; // Temporary storage for pattern reader, will be reset later + // Parse loops + const SmpLength loopStart = oktSmp.loopStart * 2; + const SmpLength loopLength = oktSmp.loopLength * 2; + if(loopLength > 2 && loopStart + loopLength <= mptSmp.nLength) + { + mptSmp.uFlags.set(CHN_SUSTAINLOOP); + mptSmp.nSustainStart = loopStart; + mptSmp.nSustainEnd = loopStart + loopLength; + } + } +} + + +// Parse a pattern block +static void ReadOKTPattern(FileReader &chunk, PATTERNINDEX pat, CSoundFile &sndFile, const std::array<int8, 8> pairedChn) +{ + if(!chunk.CanRead(2)) + { + // Invent empty pattern + sndFile.Patterns.Insert(pat, 64); + return; + } + + ROWINDEX rows = Clamp(static_cast<ROWINDEX>(chunk.ReadUint16BE()), ROWINDEX(1), MAX_PATTERN_ROWS); + + if(!sndFile.Patterns.Insert(pat, rows)) + { + return; + } + + const CHANNELINDEX chns = sndFile.GetNumChannels(); + + for(ROWINDEX row = 0; row < rows; row++) + { + auto rowCmd = sndFile.Patterns[pat].GetRow(row); + for(CHANNELINDEX chn = 0; chn < chns; chn++) + { + ModCommand &m = rowCmd[chn]; + const auto [note, instr, effect, param] = chunk.ReadArray<uint8, 4>(); + + if(note > 0 && note <= 36) + { + m.note = note + (NOTE_MIDDLEC - 13); + m.instr = instr + 1; + if(m.instr > 0 && m.instr <= sndFile.GetNumSamples()) + { + const auto &sample = sndFile.GetSample(m.instr); + // Default volume only works on raw Paula channels + if(pairedChn[chn] && sample.nVolume < 256) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = 64; + } + // If channel and sample type don't match, stop this channel (add 100 to the instrument number to make it understandable what happened during import) + if((sample.cues[0] == 1 && pairedChn[chn] != 0) || (sample.cues[0] == 0 && pairedChn[chn] == 0)) + { + m.instr += 100; + } + } + } + + switch(effect) + { + case 0: // Nothing + break; + + case 1: // 1 Portamento Down (Period) + if(param) + { + m.command = CMD_PORTAMENTOUP; + m.param = param; + } + break; + case 2: // 2 Portamento Up (Period) + if(param) + { + m.command = CMD_PORTAMENTODOWN; + m.param = param; + } + break; + +#if 0 + /* these aren't like regular arpeggio: "down" means to *subtract* the offset from the note. + For now I'm going to leave these unimplemented. */ + case 10: // A Arpeggio 1 (down, orig, up) + case 11: // B Arpeggio 2 (orig, up, orig, down) + if(param) + { + m.command = CMD_ARPEGGIO; + m.param = param; + } + break; +#endif + // This one is close enough to "standard" arpeggio -- I think! + case 12: // C Arpeggio 3 (up, up, orig) + if(param) + { + m.command = CMD_ARPEGGIO; + m.param = param; + } + break; + + case 13: // D Slide Down (Notes) + if(param) + { + m.command = CMD_NOTESLIDEDOWN; + m.param = 0x10 | std::min(uint8(0x0F), param); + } + break; + case 30: // U Slide Up (Notes) + if(param) + { + m.command = CMD_NOTESLIDEUP; + m.param = 0x10 | std::min(uint8(0x0F), param); + } + break; + // Fine Slides are only implemented for libopenmpt. For OpenMPT, + // sliding every 5 (non-note) ticks kind of works (at least at + // speed 6), but implementing separate (format-agnostic) fine slide commands would of course be better. + case 21: // L Slide Down Once (Notes) + if(param) + { + m.command = CMD_NOTESLIDEDOWN; + m.param = 0x50 | std::min(uint8(0x0F), param); + } + break; + case 17: // H Slide Up Once (Notes) + if(param) + { + m.command = CMD_NOTESLIDEUP; + m.param = 0x50 | std::min(uint8(0x0F), param); + } + break; + + case 15: // F Set Filter <>00:ON + m.command = CMD_MODCMDEX; + m.param = !!param; + break; + + case 25: // P Pos Jump + m.command = CMD_POSITIONJUMP; + m.param = param; + break; + + case 27: // R Release sample (apparently not listed in the help!) + m.Clear(); + m.note = NOTE_KEYOFF; + break; + + case 28: // S Speed + if(param < 0x20) + { + m.command = CMD_SPEED; + m.param = param; + } + break; + + case 31: // V Volume + // Volume on mixed channels is permanent, on hardware channels it behaves like in regular MODs + if(param & 0x0F) + { + m.command = pairedChn[chn] ? CMD_CHANNELVOLSLIDE : CMD_VOLUMESLIDE; + m.param = param & 0x0F; + } + + switch(param >> 4) + { + case 4: // Normal slide down + if(param != 0x40) + break; + // 0x40 is set volume -- fall through + [[fallthrough]]; + case 0: case 1: case 2: case 3: + if(pairedChn[chn]) + { + m.command = CMD_CHANNELVOLUME; + m.param = param; + } else + { + m.volcmd = VOLCMD_VOLUME; + m.vol = param; + m.command = CMD_NONE; + } + break; + case 5: // Normal slide up + m.param <<= 4; + break; + case 6: // Fine slide down + m.param = 0xF0 | std::min(static_cast<uint8>(m.param), uint8(0x0E)); + break; + case 7: // Fine slide up + m.param = (std::min(static_cast<uint8>(m.param), uint8(0x0E)) << 4) | 0x0F; + break; + default: + // Junk. + m.command = CMD_NONE; + break; + } + + // Volume is shared between two mixed channels, second channel has priority + if(m.command == CMD_CHANNELVOLUME || m.command == CMD_CHANNELVOLSLIDE) + { + ModCommand &other = rowCmd[chn + pairedChn[chn]]; + // Try to preserve effect if there already was one + if(other.ConvertVolEffect(other.command, other.param, true)) + { + other.volcmd = other.command; + other.vol = other.param; + } + other.command = m.command; + other.param = m.param; + } + break; + +#if 0 + case 24: // O Old Volume (???) + m.command = CMD_VOLUMESLIDE; + m.param = 0; + break; +#endif + + default: + m.command = CMD_NONE; + break; + } + } + } +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderOKT(MemoryFileReader file, const uint64 *pfilesize) +{ + if(!file.CanRead(8)) + { + return ProbeWantMoreData; + } + if(!file.ReadMagic("OKTASONG")) + { + return ProbeFailure; + } + OktIffChunk iffHead; + if(!file.ReadStruct(iffHead)) + { + return ProbeWantMoreData; + } + if(iffHead.chunksize == 0) + { + return ProbeFailure; + } + if((iffHead.signature & 0x80808080u) != 0) // ASCII? + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadOKT(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + if(!file.ReadMagic("OKTASONG")) + { + return false; + } + + // prepare some arrays to store offsets etc. + std::vector<FileReader> patternChunks; + std::vector<FileReader> sampleChunks; + std::array<int8, 8> pairedChn{{}}; + ORDERINDEX numOrders = 0; + + InitializeGlobals(MOD_TYPE_OKT); + + m_modFormat.formatName = U_("Oktalyzer"); + m_modFormat.type = U_("okt"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + // Go through IFF chunks... + while(file.CanRead(sizeof(OktIffChunk))) + { + OktIffChunk iffHead; + if(!file.ReadStruct(iffHead)) + { + break; + } + + FileReader chunk = file.ReadChunk(iffHead.chunksize); + if(!chunk.IsValid()) + { + break; + } + + switch(iffHead.signature) + { + case OktIffChunk::idCMOD: + // Channel setup table + if(m_nChannels == 0 && chunk.GetLength() >= 8) + { + const auto chnTable = chunk.ReadArray<uint16be, 4>(); + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + if(chnTable[chn]) + { + pairedChn[m_nChannels] = 1; + pairedChn[m_nChannels + 1] = -1; + ChnSettings[m_nChannels].Reset(); + ChnSettings[m_nChannels++].nPan = (((chn & 3) == 1) || ((chn & 3) == 2)) ? 0xC0 : 0x40; + } + ChnSettings[m_nChannels].Reset(); + ChnSettings[m_nChannels++].nPan = (((chn & 3) == 1) || ((chn & 3) == 2)) ? 0xC0 : 0x40; + } + + if(loadFlags == onlyVerifyHeader) + { + return true; + } + } + break; + + case OktIffChunk::idSAMP: + // Convert sample headers + if(m_nSamples > 0) + { + break; + } + ReadOKTSamples(chunk, *this); + break; + + case OktIffChunk::idSPEE: + // Read default speed + if(chunk.GetLength() >= 2) + { + m_nDefaultSpeed = Clamp(chunk.ReadUint16BE(), uint16(1), uint16(255)); + } + break; + + case OktIffChunk::idSLEN: + // Number of patterns, we don't need this. + break; + + case OktIffChunk::idPLEN: + // Read number of valid orders + if(chunk.GetLength() >= 2) + { + numOrders = chunk.ReadUint16BE(); + } + break; + + case OktIffChunk::idPATT: + // Read the orderlist + ReadOrderFromFile<uint8>(Order(), chunk, chunk.GetLength(), 0xFF, 0xFE); + break; + + case OktIffChunk::idPBOD: + // Don't read patterns for now, as the number of channels might be unknown at this point. + if(patternChunks.size() < 256) + { + patternChunks.push_back(chunk); + } + break; + + case OktIffChunk::idSBOD: + // Sample data - same as with patterns, as we need to know the sample format / length + if(sampleChunks.size() < MAX_SAMPLES - 1 && chunk.GetLength() > 0) + { + sampleChunks.push_back(chunk); + } + break; + + default: + // Non-ASCII chunk ID? + if(iffHead.signature & 0x80808080) + return false; + break; + } + } + + // If there wasn't even a CMOD chunk, we can't really load this. + if(m_nChannels == 0) + return false; + + m_nDefaultTempo.Set(125); + m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + m_nSamplePreAmp = m_nVSTiVolume = 48; + m_nMinPeriod = 113 * 4; + m_nMaxPeriod = 856 * 4; + + // Fix orderlist + Order().resize(numOrders); + + // Read patterns + if(loadFlags & loadPatternData) + { + Patterns.ResizeArray(static_cast<PATTERNINDEX>(patternChunks.size())); + for(PATTERNINDEX pat = 0; pat < patternChunks.size(); pat++) + { + ReadOKTPattern(patternChunks[pat], pat, *this, pairedChn); + } + } + + // Read samples + size_t fileSmp = 0; + for(SAMPLEINDEX smp = 1; smp < m_nSamples; smp++) + { + if(fileSmp >= sampleChunks.size() || !(loadFlags & loadSampleData)) + break; + + ModSample &mptSample = Samples[smp]; + mptSample.SetDefaultCuePoints(); + if(mptSample.nLength == 0) + continue; + + // Weird stuff? + LimitMax(mptSample.nLength, mpt::saturate_cast<SmpLength>(sampleChunks[fileSmp].GetLength())); + + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::bigEndian, + SampleIO::signedPCM) + .ReadSample(mptSample, sampleChunks[fileSmp]); + + fileSmp++; + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_plm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_plm.cpp new file mode 100644 index 00000000..62f5e015 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_plm.cpp @@ -0,0 +1,402 @@ +/* + * Load_plm.cpp + * ------------ + * Purpose: PLM (Disorder Tracker 2) module loader + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + + +OPENMPT_NAMESPACE_BEGIN + +struct PLMFileHeader +{ + char magic[4]; // "PLM\x1A" + uint8le headerSize; // Number of bytes in header, including magic bytes + uint8le version; // version code of file format (0x10) + char songName[48]; + uint8le numChannels; + uint8le flags; // unused? + uint8le maxVol; // Maximum volume for vol slides, normally 0x40 + uint8le amplify; // SoundBlaster amplify, 0x40 = no amplify + uint8le tempo; + uint8le speed; + uint8le panPos[32]; // 0...15 + uint8le numSamples; + uint8le numPatterns; + uint16le numOrders; +}; + +MPT_BINARY_STRUCT(PLMFileHeader, 96) + + +struct PLMSampleHeader +{ + enum SampleFlags + { + smp16Bit = 1, + smpPingPong = 2, + }; + + char magic[4]; // "PLS\x1A" + uint8le headerSize; // Number of bytes in header, including magic bytes + uint8le version; + char name[32]; + char filename[12]; + uint8le panning; // 0...15, 255 = no pan + uint8le volume; // 0...64 + uint8le flags; // See SampleFlags + uint16le sampleRate; + char unused[4]; + uint32le loopStart; + uint32le loopEnd; + uint32le length; +}; + +MPT_BINARY_STRUCT(PLMSampleHeader, 71) + + +struct PLMPatternHeader +{ + uint32le size; + uint8le numRows; + uint8le numChannels; + uint8le color; + char name[25]; +}; + +MPT_BINARY_STRUCT(PLMPatternHeader, 32) + + +struct PLMOrderItem +{ + uint16le x; // Starting position of pattern + uint8le y; // Number of first channel + uint8le pattern; +}; + +MPT_BINARY_STRUCT(PLMOrderItem, 4) + + +static bool ValidateHeader(const PLMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "PLM\x1A", 4) + || fileHeader.version != 0x10 + || fileHeader.numChannels == 0 || fileHeader.numChannels > 32 + || fileHeader.headerSize < sizeof(PLMFileHeader) + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const PLMFileHeader &fileHeader) +{ + return fileHeader.headerSize - sizeof(PLMFileHeader) + 4 * (fileHeader.numOrders + fileHeader.numPatterns + fileHeader.numSamples); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPLM(MemoryFileReader file, const uint64 *pfilesize) +{ + PLMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadPLM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + PLMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + if(!file.Seek(fileHeader.headerSize)) + { + return false; + } + + InitializeGlobals(MOD_TYPE_PLM); + InitializeChannels(); + m_SongFlags = SONG_ITOLDEFFECTS; + m_playBehaviour.set(kApplyOffsetWithoutNote); + + m_modFormat.formatName = U_("Disorder Tracker 2"); + m_modFormat.type = U_("plm"); + m_modFormat.charset = mpt::Charset::CP437; + + // Some PLMs use ASCIIZ, some space-padding strings...weird. Oh, and the file browser stops at 0 bytes in the name, the main GUI doesn't. + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songName); + m_nChannels = fileHeader.numChannels + 1; // Additional channel for writing pattern breaks + m_nSamplePreAmp = fileHeader.amplify; + m_nDefaultTempo.Set(fileHeader.tempo); + m_nDefaultSpeed = fileHeader.speed; + for(CHANNELINDEX chn = 0; chn < fileHeader.numChannels; chn++) + { + ChnSettings[chn].nPan = fileHeader.panPos[chn] * 0x11; + } + m_nSamples = fileHeader.numSamples; + + std::vector<PLMOrderItem> order(fileHeader.numOrders); + file.ReadVector(order, fileHeader.numOrders); + + std::vector<uint32le> patternPos, samplePos; + file.ReadVector(patternPos, fileHeader.numPatterns); + file.ReadVector(samplePos, fileHeader.numSamples); + + for(SAMPLEINDEX smp = 0; smp < fileHeader.numSamples; smp++) + { + ModSample &sample = Samples[smp + 1]; + sample.Initialize(); + + PLMSampleHeader sampleHeader; + if(samplePos[smp] == 0 + || !file.Seek(samplePos[smp]) + || !file.ReadStruct(sampleHeader)) + continue; + + m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); + sample.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename); + if(sampleHeader.panning <= 15) + { + sample.uFlags.set(CHN_PANNING); + sample.nPan = sampleHeader.panning * 0x11; + } + sample.nGlobalVol = std::min(sampleHeader.volume.get(), uint8(64)); + sample.nC5Speed = sampleHeader.sampleRate; + sample.nLoopStart = sampleHeader.loopStart; + sample.nLoopEnd = sampleHeader.loopEnd; + sample.nLength = sampleHeader.length; + if(sampleHeader.flags & PLMSampleHeader::smp16Bit) + { + sample.nLoopStart /= 2; + sample.nLoopEnd /= 2; + sample.nLength /= 2; + } + if(sample.nLoopEnd > sample.nLoopStart) + { + sample.uFlags.set(CHN_LOOP); + if(sampleHeader.flags & PLMSampleHeader::smpPingPong) sample.uFlags.set(CHN_PINGPONGLOOP); + } + sample.SanitizeLoops(); + + if(loadFlags & loadSampleData) + { + file.Seek(samplePos[smp] + sampleHeader.headerSize); + SampleIO( + (sampleHeader.flags & PLMSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::unsignedPCM) + .ReadSample(sample, file); + } + } + + if(!(loadFlags & loadPatternData)) + { + return true; + } + + // PLM is basically one huge continuous pattern, so we split it up into smaller patterns. + const ROWINDEX rowsPerPat = 64; + uint32 maxPos = 0; + + static constexpr ModCommand::COMMAND effTrans[] = + { + CMD_NONE, + CMD_PORTAMENTOUP, + CMD_PORTAMENTODOWN, + CMD_TONEPORTAMENTO, + CMD_VOLUMESLIDE, + CMD_TREMOLO, + CMD_VIBRATO, + CMD_S3MCMDEX, // Tremolo Waveform + CMD_S3MCMDEX, // Vibrato Waveform + CMD_TEMPO, + CMD_SPEED, + CMD_POSITIONJUMP, // Jump to order + CMD_POSITIONJUMP, // Break to end of this order + CMD_OFFSET, + CMD_S3MCMDEX, // GUS Panning + CMD_RETRIG, + CMD_S3MCMDEX, // Note Delay + CMD_S3MCMDEX, // Note Cut + CMD_S3MCMDEX, // Pattern Delay + CMD_FINEVIBRATO, + CMD_VIBRATOVOL, + CMD_TONEPORTAVOL, + CMD_OFFSETPERCENTAGE, + }; + + Order().clear(); + for(const auto &ord : order) + { + if(ord.pattern >= fileHeader.numPatterns + || ord.y > fileHeader.numChannels + || !file.Seek(patternPos[ord.pattern])) continue; + + PLMPatternHeader patHeader; + file.ReadStruct(patHeader); + if(!patHeader.numRows) continue; + + static_assert(ORDERINDEX_MAX >= (std::numeric_limits<decltype(ord.x)>::max() + 255) / rowsPerPat); + ORDERINDEX curOrd = static_cast<ORDERINDEX>(ord.x / rowsPerPat); + ROWINDEX curRow = static_cast<ROWINDEX>(ord.x % rowsPerPat); + const CHANNELINDEX numChannels = std::min(patHeader.numChannels.get(), static_cast<uint8>(fileHeader.numChannels - ord.y)); + const uint32 patternEnd = ord.x + patHeader.numRows; + maxPos = std::max(maxPos, patternEnd); + + ModCommand::NOTE lastNote[32] = { 0 }; + for(ROWINDEX r = 0; r < patHeader.numRows; r++, curRow++) + { + if(curRow >= rowsPerPat) + { + curRow = 0; + curOrd++; + } + if(curOrd >= Order().size()) + { + Order().resize(curOrd + 1); + Order()[curOrd] = Patterns.InsertAny(rowsPerPat); + } + PATTERNINDEX pat = Order()[curOrd]; + if(!Patterns.IsValidPat(pat)) break; + + ModCommand *m = Patterns[pat].GetpModCommand(curRow, ord.y); + for(CHANNELINDEX c = 0; c < numChannels; c++, m++) + { + const auto [note, instr, volume, command, param] = file.ReadArray<uint8, 5>(); + if(note > 0 && note < 0x90) + lastNote[c] = m->note = (note >> 4) * 12 + (note & 0x0F) + 12 + NOTE_MIN; + else + m->note = NOTE_NONE; + m->instr = instr; + m->volcmd = VOLCMD_VOLUME; + if(volume != 0xFF) + m->vol = volume; + else + m->volcmd = VOLCMD_NONE; + + if(command < std::size(effTrans)) + { + m->command = effTrans[command]; + m->param = param; + // Fix some commands + switch(command) + { + case 0x07: // Tremolo waveform + m->param = 0x40 | (m->param & 0x03); + break; + case 0x08: // Vibrato waveform + m->param = 0x30 | (m->param & 0x03); + break; + case 0x0B: // Jump to order + if(m->param < order.size()) + { + uint16 target = order[m->param].x; + m->param = static_cast<ModCommand::PARAM>(target / rowsPerPat); + ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1); + mBreak->command = CMD_PATTERNBREAK; + mBreak->param = static_cast<ModCommand::PARAM>(target % rowsPerPat); + } + break; + case 0x0C: // Jump to end of order + { + m->param = static_cast<ModCommand::PARAM>(patternEnd / rowsPerPat); + ModCommand *mBreak = Patterns[pat].GetpModCommand(curRow, m_nChannels - 1); + mBreak->command = CMD_PATTERNBREAK; + mBreak->param = static_cast<ModCommand::PARAM>(patternEnd % rowsPerPat); + } + break; + case 0x0E: // GUS Panning + m->param = 0x80 | (m->param & 0x0F); + break; + case 0x10: // Delay Note + m->param = 0xD0 | std::min(m->param, ModCommand::PARAM(0x0F)); + break; + case 0x11: // Cut Note + m->param = 0xC0 | std::min(m->param, ModCommand::PARAM(0x0F)); + break; + case 0x12: // Pattern Delay + m->param = 0xE0 | std::min(m->param, ModCommand::PARAM(0x0F)); + break; + case 0x04: // Volume Slide + case 0x14: // Vibrato + Volume Slide + case 0x15: // Tone Portamento + Volume Slide + // If both nibbles of a volume slide are set, act as fine volume slide up + if((m->param & 0xF0) && (m->param & 0x0F) && (m->param & 0xF0) != 0xF0) + { + m->param |= 0x0F; + } + break; + case 0x0D: + case 0x16: + // Offset without note + if(m->note == NOTE_NONE) + { + m->note = lastNote[c]; + } + break; + } + } + } + if(patHeader.numChannels > numChannels) + { + file.Skip(5 * (patHeader.numChannels - numChannels)); + } + } + } + // Module ends with the last row of the last order item + ROWINDEX endPatSize = maxPos % rowsPerPat; + ORDERINDEX endOrder = static_cast<ORDERINDEX>(maxPos / rowsPerPat); + if(endPatSize > 0 && Order().IsValidPat(endOrder)) + { + Patterns[Order()[endOrder]].Resize(endPatSize, false); + } + // If there are still any non-existent patterns in our order list, insert some blank patterns. + PATTERNINDEX blankPat = PATTERNINDEX_INVALID; + for(auto &pat : Order()) + { + if(pat == Order.GetInvalidPatIndex()) + { + if(blankPat == PATTERNINDEX_INVALID) + { + blankPat = Patterns.InsertAny(rowsPerPat); + } + pat = blankPat; + } + } + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_psm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_psm.cpp new file mode 100644 index 00000000..724b2e0d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_psm.cpp @@ -0,0 +1,1421 @@ +/* + * Load_psm.cpp + * ------------ + * Purpose: PSM16 and new PSM (ProTracker Studio / Epic MegaGames MASI) module loader + * Notes : This is partly based on http://www.shikadi.net/moddingwiki/ProTracker_Studio_Module + * and partly reverse-engineered. Also thanks to the author of foo_dumb, the source code gave me a few clues. :) + * Authors: Johannes Schultz + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +#ifdef LIBOPENMPT_BUILD +#define MPT_PSM_USE_REAL_SUBSONGS +#endif + +OPENMPT_NAMESPACE_BEGIN + +//////////////////////////////////////////////////////////// +// +// New PSM support starts here. PSM16 structs are below. +// + +// PSM File Header +struct PSMFileHeader +{ + char formatID[4]; // "PSM " (new format) + uint32le fileSize; // Filesize - 12 + char fileInfoID[4]; // "FILE" +}; + +MPT_BINARY_STRUCT(PSMFileHeader, 12) + +// RIFF-style Chunk +struct PSMChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idTITL = MagicLE("TITL"), + idSDFT = MagicLE("SDFT"), + idPBOD = MagicLE("PBOD"), + idSONG = MagicLE("SONG"), + idDATE = MagicLE("DATE"), + idOPLH = MagicLE("OPLH"), + idPPAN = MagicLE("PPAN"), + idPATT = MagicLE("PATT"), + idDSAM = MagicLE("DSAM"), + idDSMP = MagicLE("DSMP"), + }; + + uint32le id; + uint32le length; + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(PSMChunk, 8) + +// Song Information +struct PSMSongHeader +{ + char songType[9]; // Mostly "MAINSONG " (But not in Extreme Pinball!) + uint8 compression; // 1 - uncompressed + uint8 numChannels; // Number of channels + +}; + +MPT_BINARY_STRUCT(PSMSongHeader, 11) + +// Regular sample header +struct PSMSampleHeader +{ + uint8le flags; + char fileName[8]; // Filename of the original module (without extension) + char sampleID[4]; // Identifier like "INS0" (only last digit of sample ID, i.e. sample 1 and sample 11 are equal) or "I0 " + char sampleName[33]; + uint8le unknown1[6]; // 00 00 00 00 00 FF + uint16le sampleNumber; + uint32le sampleLength; + uint32le loopStart; + uint32le loopEnd; // FF FF FF FF = end of sample + uint8le unknown3; + uint8le finetune; // unused? always 0 + uint8le defaultVolume; + uint32le unknown4; + uint32le c5Freq; // MASI ignores the high 16 bits + char padding[19]; + + // Convert header data to OpenMPT's internal format + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileName); + + mptSmp.nC5Speed = c5Freq; + mptSmp.nLength = sampleLength; + mptSmp.nLoopStart = loopStart; + // Note that we shouldn't add + 1 for MTM conversions here (e.g. the OMF 2097 music), + // but I think there is no way to figure out the original format, and in the case of the OMF 2097 soundtrack + // it doesn't make a huge audible difference anyway (no chip samples are used). + // On the other hand, sample 8 of MUSIC_A.PSM from Extreme Pinball will sound detuned if we don't adjust the loop end here. + if(loopEnd) + mptSmp.nLoopEnd = loopEnd + 1; + mptSmp.nVolume = (defaultVolume + 1) * 2; + mptSmp.uFlags.set(CHN_LOOP, (flags & 0x80) != 0); + LimitMax(mptSmp.nLoopEnd, mptSmp.nLength); + LimitMax(mptSmp.nLoopStart, mptSmp.nLoopEnd); + } +}; + +MPT_BINARY_STRUCT(PSMSampleHeader, 96) + +// Sinaria sample header (and possibly other games) +struct PSMSinariaSampleHeader +{ + uint8le flags; + char fileName[8]; // Filename of the original module (without extension) + char sampleID[8]; // INS0...INS99999 + char sampleName[33]; + uint8le unknown1[6]; // 00 00 00 00 00 FF + uint16le sampleNumber; + uint32le sampleLength; + uint32le loopStart; + uint32le loopEnd; + uint16le unknown3; + uint8le finetune; // Appears to be unused + uint8le defaultVolume; + uint32le unknown4; + uint16le c5Freq; + char padding[16]; + + // Convert header data to OpenMPT's internal format + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileName); + + mptSmp.nC5Speed = c5Freq; + mptSmp.nLength = sampleLength; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nVolume = (defaultVolume + 1) * 2; + mptSmp.uFlags.set(CHN_LOOP, (flags & 0x80) != 0); + LimitMax(mptSmp.nLoopEnd, mptSmp.nLength); + LimitMax(mptSmp.nLoopStart, mptSmp.nLoopEnd); + } +}; + +MPT_BINARY_STRUCT(PSMSinariaSampleHeader, 96) + + +struct PSMSubSong // For internal use (pattern conversion) +{ + std::vector<uint8> channelPanning, channelVolume; + std::vector<bool> channelSurround; + ORDERINDEX startOrder = ORDERINDEX_INVALID, endOrder = ORDERINDEX_INVALID, restartPos = 0; + uint8 defaultTempo = 125, defaultSpeed = 6; + char songName[10] = {}; + + PSMSubSong() + : channelPanning(MAX_BASECHANNELS, 128) + , channelVolume(MAX_BASECHANNELS, 64) + , channelSurround(MAX_BASECHANNELS, false) + { } +}; + + +// Portamento effect conversion (depending on format version) +static uint8 ConvertPSMPorta(uint8 param, bool sinariaFormat) +{ + if(sinariaFormat) + return param; + if(param < 4) + return (param | 0xF0); + else + return (param >> 2); +} + + +// Read a Pattern ID (something like "P0 " or "P13 ", or "PATT0 " in Sinaria) +static PATTERNINDEX ReadPSMPatternIndex(FileReader &file, bool &sinariaFormat) +{ + char patternID[5]; + uint8 offset = 1; + file.ReadString<mpt::String::spacePadded>(patternID, 4); + if(!memcmp(patternID, "PATT", 4)) + { + file.ReadString<mpt::String::spacePadded>(patternID, 4); + sinariaFormat = true; + offset = 0; + } + return ConvertStrTo<uint16>(&patternID[offset]); +} + + +static bool ValidateHeader(const PSMFileHeader &fileHeader) +{ + if(!std::memcmp(fileHeader.formatID, "PSM ", 4) + && !std::memcmp(fileHeader.fileInfoID, "FILE", 4)) + { + return true; + } +#ifdef MPT_PSM_DECRYPT + if(!std::memcmp(fileHeader.formatID, "QUP$", 4) + && !std::memcmp(fileHeader.fileInfoID, "OSWQ", 4)) + { + return true; + } +#endif + return false; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPSM(MemoryFileReader file, const uint64 *pfilesize) +{ + PSMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + PSMChunk chunkHeader; + if(!file.ReadStruct(chunkHeader)) + { + return ProbeWantMoreData; + } + if(chunkHeader.length == 0) + { + return ProbeFailure; + } + if((chunkHeader.id & 0x7F7F7F7Fu) != chunkHeader.id) // ASCII? + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadPSM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + PSMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + +#ifdef MPT_PSM_DECRYPT + // CONVERT.EXE /K - I don't think any game ever used this. + std::vector<std::byte> decrypted; + if(!memcmp(fileHeader.formatID, "QUP$", 4) + && !memcmp(fileHeader.fileInfoID, "OSWQ", 4)) + { + if(loadFlags == onlyVerifyHeader) + return true; + file.Rewind(); + decrypted.resize(file.GetLength()); + file.ReadRaw(decrypted.data(), decrypted.size()); + uint8 i = 0; + for(auto &c : decrypted) + { + c -= ++i; + } + file = FileReader(mpt::as_span(decrypted)); + file.ReadStruct(fileHeader); + } +#endif // MPT_PSM_DECRYPT + + // Check header + if(!ValidateHeader(fileHeader)) + { + return false; + } + + ChunkReader chunkFile(file); + ChunkReader::ChunkList<PSMChunk> chunks; + if(loadFlags == onlyVerifyHeader) + chunks = chunkFile.ReadChunksUntil<PSMChunk>(1, PSMChunk::idSDFT); + else + chunks = chunkFile.ReadChunks<PSMChunk>(1); + + // "SDFT" - Format info (song data starts here) + if(!chunks.GetChunk(PSMChunk::idSDFT).ReadMagic("MAINSONG")) + return false; + else if(loadFlags == onlyVerifyHeader) + return true; + + // Yep, this seems to be a valid file. + InitializeGlobals(MOD_TYPE_PSM); + m_SongFlags = SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX; + + // "TITL" - Song Title + FileReader titleChunk = chunks.GetChunk(PSMChunk::idTITL); + titleChunk.ReadString<mpt::String::spacePadded>(m_songName, titleChunk.GetLength()); + + Order().clear(); + // Subsong setup + std::vector<PSMSubSong> subsongs; + bool subsongPanningDiffers = false; // Do we have subsongs with different panning positions? + bool sinariaFormat = false; // The game "Sinaria" uses a slightly modified PSM structure - in some ways it's more like PSM16 (e.g. effects). + + // "SONG" - Subsong information (channel count etc) + auto songChunks = chunks.GetAllChunks(PSMChunk::idSONG); + for(ChunkReader chunk : songChunks) + { + PSMSongHeader songHeader; + if(!chunk.ReadStruct(songHeader) + || songHeader.compression != 0x01) // No compression for PSM files + { + return false; + } + // Subsongs *might* have different channel count + m_nChannels = Clamp(static_cast<CHANNELINDEX>(songHeader.numChannels), m_nChannels, MAX_BASECHANNELS); + + PSMSubSong subsong; + mpt::String::WriteAutoBuf(subsong.songName) = mpt::String::ReadBuf(mpt::String::nullTerminated, songHeader.songType); + +#ifdef MPT_PSM_USE_REAL_SUBSONGS + if(!Order().empty()) + { + // Add a new sequence for this subsong + if(Order.AddSequence() == SEQUENCEINDEX_INVALID) + break; + } + Order().SetName(mpt::ToUnicode(mpt::Charset::CP437, subsong.songName)); +#endif // MPT_PSM_USE_REAL_SUBSONGS + + // Read "Sub chunks" + auto subChunks = chunk.ReadChunks<PSMChunk>(1); + for(const auto &subChunkIter : subChunks.chunks) + { + FileReader subChunk(subChunkIter.GetData()); + PSMChunk subChunkHead = subChunkIter.GetHeader(); + + switch(subChunkHead.GetID()) + { +#if 0 + case PSMChunk::idDATE: // "DATE" - Conversion date (YYMMDD) + if(subChunkHead.GetLength() == 6) + { + char cversion[7]; + subChunk.ReadString<mpt::String::maybeNullTerminated>(cversion, 6); + uint32 version = ConvertStrTo<uint32>(cversion); + // Sinaria song dates (just to go sure...) + if(version == 800211 || version == 940902 || version == 940903 || + version == 940906 || version == 940914 || version == 941213) + sinariaFormat = true; + } + break; +#endif + + case PSMChunk::idOPLH: // "OPLH" - Order list, channel + module settings + if(subChunkHead.GetLength() >= 9) + { + // First two bytes = Number of chunks that follow + //uint16 totalChunks = subChunk.ReadUint16LE(); + subChunk.Skip(2); + + // Now, the interesting part begins! + uint16 chunkCount = 0, firstOrderChunk = uint16_max; + + // "Sub sub chunks" (grrrr, silly format) + while(subChunk.CanRead(1)) + { + uint8 opcode = subChunk.ReadUint8(); + if(!opcode) + { + // Last chunk was reached. + break; + } + + // Note: This is more like a playlist than a collection of global values. + // In theory, a tempo item inbetween two order items should modify the + // tempo when switching patterns. No module uses this feature in practice + // though, so we can keep our loader simple. + // Unimplemented opcodes do nothing or freeze MASI. + switch(opcode) + { + case 0x01: // Play order list item + { + if(subsong.startOrder == ORDERINDEX_INVALID) + subsong.startOrder = Order().GetLength(); + subsong.endOrder = Order().GetLength(); + PATTERNINDEX pat = ReadPSMPatternIndex(subChunk, sinariaFormat); + if(pat == 0xFF) + pat = Order.GetInvalidPatIndex(); + else if(pat == 0xFE) + pat = Order.GetIgnoreIndex(); + Order().push_back(pat); + // Decide whether this is the first order chunk or not (for finding out the correct restart position) + if(firstOrderChunk == uint16_max) + firstOrderChunk = chunkCount; + } + break; + + // 0x02: Play Range + // 0x03: Jump Loop + + case 0x04: // Jump Line (Restart position) + { + uint16 restartChunk = subChunk.ReadUint16LE(); + if(restartChunk >= firstOrderChunk) + subsong.restartPos = static_cast<ORDERINDEX>(restartChunk - firstOrderChunk); // Close enough - we assume that order list is continuous (like in any real-world PSM) + Order().SetRestartPos(subsong.restartPos); + } + break; + + // 0x05: Channel Flip + // 0x06: Transpose + + case 0x07: // Default Speed + subsong.defaultSpeed = subChunk.ReadUint8(); + break; + + case 0x08: // Default Tempo + subsong.defaultTempo = subChunk.ReadUint8(); + break; + + case 0x0C: // Sample map table + // Never seems to be different, so... + // This is probably a part of the never-implemented "mini programming language" mentioned in the PSM docs. + // Output of PLAY.EXE: "SMapTabl from pos 0 to pos -1 starting at 0 and adding 1 to it each time" + // It appears that this maps e.g. what is "I0" in the file to sample 1. + // If we were being fancy, we could implement this, but in practice it won't matter. + { + uint8 mapTable[6]; + if(!subChunk.ReadArray(mapTable) + || mapTable[0] != 0x00 || mapTable[1] != 0xFF // "0 to -1" (does not seem to do anything) + || mapTable[2] != 0x00 || mapTable[3] != 0x00 // "at 0" (actually this appears to be the adding part - changing this to 0x01 0x00 offsets all samples by 1) + || mapTable[4] != 0x01 || mapTable[5] != 0x00) // "adding 1" (does not seem to do anything) + { + return false; + } + } + break; + + case 0x0D: // Channel panning table - can be set using CONVERT.EXE /E + { + const auto [chn, pan, type] = subChunk.ReadArray<uint8, 3>(); + if(chn < subsong.channelPanning.size()) + { + switch(type) + { + case 0: // use panning + subsong.channelPanning[chn] = pan ^ 128; + subsong.channelSurround[chn] = false; + break; + + case 2: // surround + subsong.channelPanning[chn] = 128; + subsong.channelSurround[chn] = true; + break; + + case 4: // center + subsong.channelPanning[chn] = 128; + subsong.channelSurround[chn] = false; + break; + + } + if(subsongPanningDiffers == false && subsongs.size() > 0) + { + if(subsongs.back().channelPanning[chn] != subsong.channelPanning[chn] + || subsongs.back().channelSurround[chn] != subsong.channelSurround[chn]) + subsongPanningDiffers = true; + } + } + } + break; + + case 0x0E: // Channel volume table (0...255) - can be set using CONVERT.EXE /E, is 255 in all "official" PSMs except for some OMF 2097 tracks + { + const auto [chn, vol] = subChunk.ReadArray<uint8, 2>(); + if(chn < subsong.channelVolume.size()) + { + subsong.channelVolume[chn] = (vol / 4u) + 1; + } + } + break; + + default: + // Should never happen in "real" PSM files. But in this case, we have to quit as we don't know how big the chunk really is. + return false; + + } + chunkCount++; + } + } + break; + + case PSMChunk::idPPAN: // PPAN - Channel panning table (used in Sinaria) + // In some Sinaria tunes, this is actually longer than 2 * channels... + MPT_ASSERT(subChunkHead.GetLength() >= m_nChannels * 2u); + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + if(!subChunk.CanRead(2)) + break; + + const auto [type, pan] = subChunk.ReadArray<uint8, 2>(); + switch(type) + { + case 0: // use panning + subsong.channelPanning[chn] = pan ^ 128; + subsong.channelSurround[chn] = false; + break; + + case 2: // surround + subsong.channelPanning[chn] = 128; + subsong.channelSurround[chn] = true; + break; + + case 4: // center + subsong.channelPanning[chn] = 128; + subsong.channelSurround[chn] = false; + break; + + default: + break; + } + } + break; + + case PSMChunk::idPATT: // PATT - Pattern list + // We don't really need this. + break; + + case PSMChunk::idDSAM: // DSAM - Sample list + // We don't need this either. + break; + + default: + break; + + } + } + + // Attach this subsong to the subsong list - finally, all "sub sub sub ..." chunks are parsed. + if(subsong.startOrder != ORDERINDEX_INVALID && subsong.endOrder != ORDERINDEX_INVALID) + { + // Separate subsongs by "---" patterns + Order().push_back(); + subsongs.push_back(subsong); + } + } + +#ifdef MPT_PSM_USE_REAL_SUBSONGS + Order.SetSequence(0); +#endif // MPT_PSM_USE_REAL_SUBSONGS + + if(subsongs.empty()) + return false; + + // DSMP - Samples + if(loadFlags & loadSampleData) + { + auto sampleChunks = chunks.GetAllChunks(PSMChunk::idDSMP); + for(auto &chunk : sampleChunks) + { + SAMPLEINDEX smp; + if(!sinariaFormat) + { + // Original header + PSMSampleHeader sampleHeader; + if(!chunk.ReadStruct(sampleHeader)) + continue; + + smp = static_cast<SAMPLEINDEX>(sampleHeader.sampleNumber + 1); + if(smp > 0 && smp < MAX_SAMPLES) + { + m_nSamples = std::max(m_nSamples, smp); + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.sampleName); + } + } else + { + // Sinaria uses a slightly different sample header + PSMSinariaSampleHeader sampleHeader; + if(!chunk.ReadStruct(sampleHeader)) + continue; + + smp = static_cast<SAMPLEINDEX>(sampleHeader.sampleNumber + 1); + if(smp > 0 && smp < MAX_SAMPLES) + { + m_nSamples = std::max(m_nSamples, smp); + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.sampleName); + } + } + if(smp > 0 && smp < MAX_SAMPLES) + { + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::deltaPCM).ReadSample(Samples[smp], chunk); + } + } + } + + // Make the default variables of the first subsong global + m_nDefaultSpeed = subsongs[0].defaultSpeed; + m_nDefaultTempo.Set(subsongs[0].defaultTempo); + Order().SetRestartPos(subsongs[0].restartPos); + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nVolume = subsongs[0].channelVolume[chn]; + ChnSettings[chn].nPan = subsongs[0].channelPanning[chn]; + ChnSettings[chn].dwFlags.set(CHN_SURROUND, subsongs[0].channelSurround[chn]); + } + + m_modFormat.formatName = sinariaFormat ? U_("Epic MegaGames MASI (New Version / Sinaria)") : U_("Epic MegaGames MASI (New Version)"); + m_modFormat.type = U_("psm"); + m_modFormat.charset = mpt::Charset::CP437; + + if(!(loadFlags & loadPatternData) || m_nChannels == 0) + { + return true; + } + + // "PBOD" - Pattern data of a single pattern + // Now that we know the number of channels, we can go through all the patterns. + auto pattChunks = chunks.GetAllChunks(PSMChunk::idPBOD); + Patterns.ResizeArray(static_cast<PATTERNINDEX>(pattChunks.size())); + for(auto &chunk : pattChunks) + { + if(chunk.GetLength() != chunk.ReadUint32LE() // Same value twice + || !chunk.LengthIsAtLeast(8)) + { + continue; + } + + PATTERNINDEX pat = ReadPSMPatternIndex(chunk, sinariaFormat); + uint16 numRows = chunk.ReadUint16LE(); + + if(!Patterns.Insert(pat, numRows)) + { + continue; + } + + enum + { + noteFlag = 0x80, + instrFlag = 0x40, + volFlag = 0x20, + effectFlag = 0x10, + }; + + // Read pattern. + for(ROWINDEX row = 0; row < numRows; row++) + { + PatternRow rowBase = Patterns[pat].GetRow(row); + uint16 rowSize = chunk.ReadUint16LE(); + if(rowSize <= 2) + { + continue; + } + + FileReader rowChunk = chunk.ReadChunk(rowSize - 2); + + while(rowChunk.CanRead(3)) + { + const auto [flags, channel] = rowChunk.ReadArray<uint8, 2>(); + // Point to the correct channel + ModCommand &m = rowBase[std::min(static_cast<CHANNELINDEX>(m_nChannels - 1), static_cast<CHANNELINDEX>(channel))]; + + if(flags & noteFlag) + { + // Note present + uint8 note = rowChunk.ReadUint8(); + if(!sinariaFormat) + { + if(note == 0xFF) // Can be found in a few files but is apparently not supported by MASI + note = NOTE_NOTECUT; + else + if(note < 129) note = (note & 0x0F) + 12 * (note >> 4) + 13; + } else + { + if(note < 85) note += 36; + } + m.note = note; + } + + if(flags & instrFlag) + { + // Instrument present + m.instr = rowChunk.ReadUint8() + 1; + } + + if(flags & volFlag) + { + // Volume present + uint8 vol = rowChunk.ReadUint8(); + m.volcmd = VOLCMD_VOLUME; + m.vol = (std::min(vol, uint8(127)) + 1) / 2; + } + + if(flags & effectFlag) + { + // Effect present - convert + const auto [command, param] = rowChunk.ReadArray<uint8, 2>(); + m.param = param; + + // This list is annoyingly similar to PSM16, but not quite identical. + switch(command) + { + // Volslides + case 0x01: // fine volslide up + m.command = CMD_VOLUMESLIDE; + if (sinariaFormat) m.param = (m.param << 4) | 0x0F; + else m.param = ((m.param & 0x1E) << 3) | 0x0F; + break; + case 0x02: // volslide up + m.command = CMD_VOLUMESLIDE; + if (sinariaFormat) m.param = 0xF0 & (m.param << 4); + else m.param = 0xF0 & (m.param << 3); + break; + case 0x03: // fine volslide down + m.command = CMD_VOLUMESLIDE; + if (sinariaFormat) m.param |= 0xF0; + else m.param = 0xF0 | (m.param >> 1); + break; + case 0x04: // volslide down + m.command = CMD_VOLUMESLIDE; + if (sinariaFormat) m.param &= 0x0F; + else if(m.param < 2) m.param |= 0xF0; else m.param = (m.param >> 1) & 0x0F; + break; + + // Portamento + case 0x0B: // fine portamento up + m.command = CMD_PORTAMENTOUP; + m.param = 0xF0 | ConvertPSMPorta(m.param, sinariaFormat); + break; + case 0x0C: // portamento up + m.command = CMD_PORTAMENTOUP; + m.param = ConvertPSMPorta(m.param, sinariaFormat); + break; + case 0x0D: // fine portamento down + m.command = CMD_PORTAMENTODOWN; + m.param = 0xF0 | ConvertPSMPorta(m.param, sinariaFormat); + break; + case 0x0E: // portamento down + m.command = CMD_PORTAMENTODOWN; + m.param = ConvertPSMPorta(m.param, sinariaFormat); + break; + case 0x0F: // tone portamento + m.command = CMD_TONEPORTAMENTO; + if(!sinariaFormat) m.param >>= 2; + break; + case 0x11: // glissando control + m.command = CMD_S3MCMDEX; + m.param = 0x10 | (m.param & 0x01); + break; + case 0x10: // tone portamento + volslide up + m.command = CMD_TONEPORTAVOL; + m.param = m.param & 0xF0; + break; + case 0x12: // tone portamento + volslide down + m.command = CMD_TONEPORTAVOL; + m.param = (m.param >> 4) & 0x0F; + break; + + case 0x13: // ScreamTracker command S - actually hangs / crashes MASI + m.command = CMD_S3MCMDEX; + break; + + // Vibrato + case 0x15: // vibrato + m.command = CMD_VIBRATO; + break; + case 0x16: // vibrato waveform + m.command = CMD_S3MCMDEX; + m.param = 0x30 | (m.param & 0x0F); + break; + case 0x17: // vibrato + volslide up + m.command = CMD_VIBRATOVOL; + m.param = 0xF0 | m.param; + break; + case 0x18: // vibrato + volslide down + m.command = CMD_VIBRATOVOL; + break; + + // Tremolo + case 0x1F: // tremolo + m.command = CMD_TREMOLO; + break; + case 0x20: // tremolo waveform + m.command = CMD_S3MCMDEX; + m.param = 0x40 | (m.param & 0x0F); + break; + + // Sample commands + case 0x29: // 3-byte offset - we only support the middle byte. + m.command = CMD_OFFSET; + m.param = rowChunk.ReadUint8(); + rowChunk.Skip(1); + break; + case 0x2A: // retrigger + m.command = CMD_RETRIG; + break; + case 0x2B: // note cut + m.command = CMD_S3MCMDEX; + m.param = 0xC0 | (m.param & 0x0F); + break; + case 0x2C: // note delay + m.command = CMD_S3MCMDEX; + m.param = 0xD0 | (m.param & 0x0F); + break; + + // Position change + case 0x33: // position jump - MASI seems to ignore this command, and CONVERT.EXE never writes it + m.command = CMD_POSITIONJUMP; + m.param /= 2u; // actually it is probably just an index into the order table + rowChunk.Skip(1); + break; + case 0x34: // pattern break + m.command = CMD_PATTERNBREAK; + // When converting from S3M, the parameter is double-BDC-encoded (wtf!) + // When converting from MOD, it's in binary. + // MASI ignores the parameter entirely, and so do we. + m.param = 0; + break; + case 0x35: // loop pattern + m.command = CMD_S3MCMDEX; + m.param = 0xB0 | (m.param & 0x0F); + break; + case 0x36: // pattern delay + m.command = CMD_S3MCMDEX; + m.param = 0xE0 | (m.param & 0x0F); + break; + + // speed change + case 0x3D: // set speed + m.command = CMD_SPEED; + break; + case 0x3E: // set tempo + m.command = CMD_TEMPO; + break; + + // misc commands + case 0x47: // arpeggio + m.command = CMD_ARPEGGIO; + break; + case 0x48: // set finetune + m.command = CMD_S3MCMDEX; + m.param = 0x20 | (m.param & 0x0F); + break; + case 0x49: // set balance + m.command = CMD_S3MCMDEX; + m.param = 0x80 | (m.param & 0x0F); + break; + + default: + m.command = CMD_NONE; + break; + + } + } + } + } + } + + if(subsongs.size() > 1) + { + // Write subsong "configuration" to patterns (only if there are multiple subsongs) + for(size_t i = 0; i < subsongs.size(); i++) + { +#ifdef MPT_PSM_USE_REAL_SUBSONGS + ModSequence &order = Order(static_cast<SEQUENCEINDEX>(i)); +#else + ModSequence &order = Order(); +#endif // MPT_PSM_USE_REAL_SUBSONGS + const PSMSubSong &subsong = subsongs[i]; + PATTERNINDEX startPattern = order[subsong.startOrder]; + if(Patterns.IsValidPat(startPattern)) + { + startPattern = order.EnsureUnique(subsong.startOrder); + // Subsongs with different panning setup -> write to pattern (MUSIC_C.PSM) + // Don't write channel volume for now, as there is no real-world module which needs it. + if(subsongPanningDiffers) + { + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + if(subsong.channelSurround[chn]) + Patterns[startPattern].WriteEffect(EffectWriter(CMD_S3MCMDEX, 0x91).Row(0).Channel(chn).RetryNextRow()); + else + Patterns[startPattern].WriteEffect(EffectWriter(CMD_PANNING8, subsong.channelPanning[chn]).Row(0).Channel(chn).RetryNextRow()); + } + } + // Write default tempo/speed to pattern + Patterns[startPattern].WriteEffect(EffectWriter(CMD_SPEED, subsong.defaultSpeed).Row(0).RetryNextRow()); + Patterns[startPattern].WriteEffect(EffectWriter(CMD_TEMPO, subsong.defaultTempo).Row(0).RetryNextRow()); + } + +#ifndef MPT_PSM_USE_REAL_SUBSONGS + // Add restart position to the last pattern + PATTERNINDEX endPattern = order[subsong.endOrder]; + if(Patterns.IsValidPat(endPattern)) + { + endPattern = order.EnsureUnique(subsong.endOrder); + ROWINDEX lastRow = Patterns[endPattern].GetNumRows() - 1; + auto m = Patterns[endPattern].cbegin(); + for(uint32 cell = 0; cell < m_nChannels * Patterns[endPattern].GetNumRows(); cell++, m++) + { + if(m->command == CMD_PATTERNBREAK || m->command == CMD_POSITIONJUMP) + { + lastRow = cell / m_nChannels; + break; + } + } + Patterns[endPattern].WriteEffect(EffectWriter(CMD_POSITIONJUMP, mpt::saturate_cast<ModCommand::PARAM>(subsong.startOrder + subsong.restartPos)).Row(lastRow).RetryPreviousRow()); + } + + // Set the subsong name to all pattern names + for(ORDERINDEX ord = subsong.startOrder; ord <= subsong.endOrder; ord++) + { + if(Patterns.IsValidIndex(order[ord])) + Patterns[order[ord]].SetName(subsong.songName); + } +#endif // MPT_PSM_USE_REAL_SUBSONGS + } + } + + return true; +} + +//////////////////////////////// +// +// PSM16 support starts here. +// + +struct PSM16FileHeader +{ + char formatID[4]; // "PSM\xFE" (PSM16) + char songName[59]; // Song title, padded with nulls + uint8le lineEnd; // $1A + uint8le songType; // Song Type bitfield + uint8le formatVersion; // $10 + uint8le patternVersion; // 0 or 1 + uint8le songSpeed; // 1 ... 255 + uint8le songTempo; // 32 ... 255 + uint8le masterVolume; // 0 ... 255 + uint16le songLength; // 0 ... 255 (number of patterns to play in the song) + uint16le songOrders; // 0 ... 255 (same as previous value as no subsongs are present) + uint16le numPatterns; // 1 ... 255 + uint16le numSamples; // 1 ... 255 + uint16le numChannelsPlay; // 0 ... 32 (max. number of channels to play) + uint16le numChannelsReal; // 0 ... 32 (max. number of channels to process) + uint32le orderOffset; // Pointer to order list + uint32le panOffset; // Pointer to pan table + uint32le patOffset; // Pointer to pattern data + uint32le smpOffset; // Pointer to sample headers + uint32le commentsOffset; // Pointer to song comment + uint32le patSize; // Size of all patterns + char filler[40]; +}; + +MPT_BINARY_STRUCT(PSM16FileHeader, 146) + +struct PSM16SampleHeader +{ + enum SampleFlags + { + smpMask = 0x7F, + smp16Bit = 0x04, + smpUnsigned = 0x08, + smpDelta = 0x10, + smpPingPong = 0x20, + smpLoop = 0x80, + }; + + char filename[13]; // null-terminated + char name[24]; // ditto + uint32le offset; // in file + uint32le memoffset; // not used + uint16le sampleNumber; // 1 ... 255 + uint8le flags; // sample flag bitfield + uint32le length; // in bytes + uint32le loopStart; // in samples? + uint32le loopEnd; // in samples? + uint8le finetune; // Low nibble = MOD finetune, high nibble = transpose (7 = center) + uint8le volume; // default volume + uint16le c2freq; // Middle-C frequency, which has to be combined with the finetune and transpose. + + // Convert sample header to OpenMPT's internal format + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename); + + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + // It seems like that finetune and transpose are added to the already given c2freq... That's a double WTF! + // Why on earth would you want to use both systems at the same time? + mptSmp.nC5Speed = c2freq; + mptSmp.Transpose(((finetune ^ 0x08) - 0x78) / (12.0 * 16.0)); + + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u; + + mptSmp.uFlags.reset(); + if(flags & PSM16SampleHeader::smp16Bit) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nLength /= 2u; + } + if(flags & PSM16SampleHeader::smpPingPong) + { + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + } + if(flags & PSM16SampleHeader::smpLoop) + { + mptSmp.uFlags.set(CHN_LOOP); + } + } + + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat() const + { + SampleIO sampleIO( + (flags & PSM16SampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + + if(flags & PSM16SampleHeader::smpUnsigned) + { + sampleIO |= SampleIO::unsignedPCM; + } else if((flags & PSM16SampleHeader::smpDelta) || (flags & PSM16SampleHeader::smpMask) == 0) + { + sampleIO |= SampleIO::deltaPCM; + } + + return sampleIO; + } +}; + +MPT_BINARY_STRUCT(PSM16SampleHeader, 64) + +struct PSM16PatternHeader +{ + uint16le size; // includes header bytes + uint8le numRows; // 1 ... 64 + uint8le numChans; // 1 ... 32 +}; + +MPT_BINARY_STRUCT(PSM16PatternHeader, 4) + + +static bool ValidateHeader(const PSM16FileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.formatID, "PSM\xFE", 4) + || fileHeader.lineEnd != 0x1A + || (fileHeader.formatVersion != 0x10 && fileHeader.formatVersion != 0x01) // why is this sometimes 0x01? + || fileHeader.patternVersion != 0 // 255ch pattern version not supported (did anyone use this?) + || (fileHeader.songType & 3) != 0 + || fileHeader.numChannelsPlay > MAX_BASECHANNELS + || fileHeader.numChannelsReal > MAX_BASECHANNELS + || std::max(fileHeader.numChannelsPlay, fileHeader.numChannelsReal) == 0) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPSM16(MemoryFileReader file, const uint64 *pfilesize) +{ + PSM16FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadPSM16(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + // Is it a valid PSM16 file? + PSM16FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Seems to be valid! + InitializeGlobals(MOD_TYPE_PSM); + + m_modFormat.formatName = U_("Epic MegaGames MASI (Old Version)"); + m_modFormat.type = U_("psm"); + m_modFormat.charset = mpt::Charset::CP437; + + m_nChannels = Clamp(CHANNELINDEX(fileHeader.numChannelsPlay), CHANNELINDEX(fileHeader.numChannelsReal), MAX_BASECHANNELS); + m_nSamplePreAmp = fileHeader.masterVolume; + if(m_nSamplePreAmp == 255) + { + // Most of the time, the master volume value makes sense... Just not when it's 255. + m_nSamplePreAmp = 48; + } + m_nDefaultSpeed = fileHeader.songSpeed; + m_nDefaultTempo.Set(fileHeader.songTempo); + + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songName); + + // Read orders + if(fileHeader.orderOffset > 4 && file.Seek(fileHeader.orderOffset - 4) && file.ReadMagic("PORD")) + { + ReadOrderFromFile<uint8>(Order(), file, fileHeader.songOrders); + } + + // Read pan positions + if(fileHeader.panOffset > 4 && file.Seek(fileHeader.panOffset - 4) && file.ReadMagic("PPAN")) + { + for(CHANNELINDEX i = 0; i < 32; i++) + { + ChnSettings[i].Reset(); + ChnSettings[i].nPan = ((15 - (file.ReadUint8() & 0x0F)) * 256 + 8) / 15; // 15 seems to be left and 0 seems to be right... + // ChnSettings[i].dwFlags = (i >= fileHeader.numChannelsPlay) ? CHN_MUTE : 0; // don't mute channels, as muted channels are completely ignored in S3M + } + } + + // Read samples + if(fileHeader.smpOffset > 4 && file.Seek(fileHeader.smpOffset - 4) && file.ReadMagic("PSAH")) + { + FileReader sampleChunk = file.ReadChunk(fileHeader.numSamples * sizeof(PSM16SampleHeader)); + + for(SAMPLEINDEX fileSample = 0; fileSample < fileHeader.numSamples; fileSample++) + { + PSM16SampleHeader sampleHeader; + if(!sampleChunk.ReadStruct(sampleHeader)) + { + break; + } + + const SAMPLEINDEX smp = sampleHeader.sampleNumber; + if(smp > 0 && smp < MAX_SAMPLES && !Samples[smp].HasSampleData()) + { + m_nSamples = std::max(m_nSamples, smp); + + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); + + if(loadFlags & loadSampleData) + { + file.Seek(sampleHeader.offset); + sampleHeader.GetSampleFormat().ReadSample(Samples[smp], file); + } + } + } + } + + // Read patterns + if(!(loadFlags & loadPatternData)) + { + return true; + } + if(fileHeader.patOffset > 4 && file.Seek(fileHeader.patOffset - 4) && file.ReadMagic("PPAT")) + { + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + PSM16PatternHeader patternHeader; + if(!file.ReadStruct(patternHeader)) + { + break; + } + + if(patternHeader.size < sizeof(PSM16PatternHeader)) + { + continue; + } + + // Patterns are padded to 16 Bytes + FileReader patternChunk = file.ReadChunk(((patternHeader.size + 15) & ~15) - sizeof(PSM16PatternHeader)); + + if(!Patterns.Insert(pat, patternHeader.numRows)) + { + continue; + } + + enum + { + channelMask = 0x1F, + noteFlag = 0x80, + volFlag = 0x40, + effectFlag = 0x20, + }; + + ROWINDEX curRow = 0; + + while(patternChunk.CanRead(1) && curRow < patternHeader.numRows) + { + uint8 chnFlag = patternChunk.ReadUint8(); + if(chnFlag == 0) + { + curRow++; + continue; + } + + ModCommand &m = *Patterns[pat].GetpModCommand(curRow, std::min(static_cast<CHANNELINDEX>(chnFlag & channelMask), static_cast<CHANNELINDEX>(m_nChannels - 1))); + + if(chnFlag & noteFlag) + { + // note + instr present + const auto [note, instr] = patternChunk.ReadArray<uint8, 2>(); + m.note = note + 36; + m.instr = instr; + } + if(chnFlag & volFlag) + { + // volume present + m.volcmd = VOLCMD_VOLUME; + m.vol = std::min(patternChunk.ReadUint8(), uint8(64)); + } + if(chnFlag & effectFlag) + { + // effect present - convert + const auto [command, param] = patternChunk.ReadArray<uint8, 2>(); + m.param = param; + + switch(command) + { + // Volslides + case 0x01: // fine volslide up + m.command = CMD_VOLUMESLIDE; + m.param = (m.param << 4) | 0x0F; + break; + case 0x02: // volslide up + m.command = CMD_VOLUMESLIDE; + m.param = (m.param << 4) & 0xF0; + break; + case 0x03: // fine voslide down + m.command = CMD_VOLUMESLIDE; + m.param = 0xF0 | m.param; + break; + case 0x04: // volslide down + m.command = CMD_VOLUMESLIDE; + m.param = m.param & 0x0F; + break; + + // Portamento + case 0x0A: // fine portamento up + m.command = CMD_PORTAMENTOUP; + m.param |= 0xF0; + break; + case 0x0B: // portamento down + m.command = CMD_PORTAMENTOUP; + break; + case 0x0C: // fine portamento down + m.command = CMD_PORTAMENTODOWN; + m.param |= 0xF0; + break; + case 0x0D: // portamento down + m.command = CMD_PORTAMENTODOWN; + break; + case 0x0E: // tone portamento + m.command = CMD_TONEPORTAMENTO; + break; + case 0x0F: // glissando control + m.command = CMD_S3MCMDEX; + m.param |= 0x10; + break; + case 0x10: // tone portamento + volslide up + m.command = CMD_TONEPORTAVOL; + m.param <<= 4; + break; + case 0x11: // tone portamento + volslide down + m.command = CMD_TONEPORTAVOL; + m.param &= 0x0F; + break; + + // Vibrato + case 0x14: // vibrato + m.command = CMD_VIBRATO; + break; + case 0x15: // vibrato waveform + m.command = CMD_S3MCMDEX; + m.param |= 0x30; + break; + case 0x16: // vibrato + volslide up + m.command = CMD_VIBRATOVOL; + m.param <<= 4; + break; + case 0x17: // vibrato + volslide down + m.command = CMD_VIBRATOVOL; + m.param &= 0x0F; + break; + + // Tremolo + case 0x1E: // tremolo + m.command = CMD_TREMOLO; + break; + case 0x1F: // tremolo waveform + m.command = CMD_S3MCMDEX; + m.param |= 0x40; + break; + + // Sample commands + case 0x28: // 3-byte offset - we only support the middle byte. + m.command = CMD_OFFSET; + m.param = patternChunk.ReadUint8(); + patternChunk.Skip(1); + break; + case 0x29: // retrigger + m.command = CMD_RETRIG; + m.param &= 0x0F; + break; + case 0x2A: // note cut + m.command = CMD_S3MCMDEX; +#ifdef MODPLUG_TRACKER + if(m.param == 0) // in S3M mode, SC0 is ignored, so we convert it to a note cut. + { + if(m.note == NOTE_NONE) + { + m.note = NOTE_NOTECUT; + m.command = CMD_NONE; + } else + { + m.param = 1; + } + } +#endif // MODPLUG_TRACKER + m.param |= 0xC0; + break; + case 0x2B: // note delay + m.command = CMD_S3MCMDEX; + m.param |= 0xD0; + break; + + // Position change + case 0x32: // position jump + m.command = CMD_POSITIONJUMP; + break; + case 0x33: // pattern break + m.command = CMD_PATTERNBREAK; + break; + case 0x34: // loop pattern + m.command = CMD_S3MCMDEX; + m.param |= 0xB0; + break; + case 0x35: // pattern delay + m.command = CMD_S3MCMDEX; + m.param |= 0xE0; + break; + + // speed change + case 0x3C: // set speed + m.command = CMD_SPEED; + break; + case 0x3D: // set tempo + m.command = CMD_TEMPO; + break; + + // misc commands + case 0x46: // arpeggio + m.command = CMD_ARPEGGIO; + break; + case 0x47: // set finetune + m.command = CMD_S3MCMDEX; + m.param = 0x20 | (m.param & 0x0F); + break; + case 0x48: // set balance (panning?) + m.command = CMD_S3MCMDEX; + m.param = 0x80 | (m.param & 0x0F); + break; + + default: + m.command = CMD_NONE; + break; + } + } + } + // Pattern break for short patterns (so saving the modules as S3M won't break it) + if(patternHeader.numRows != 64) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(patternHeader.numRows - 1).RetryNextRow()); + } + } + } + + if(fileHeader.commentsOffset != 0) + { + file.Seek(fileHeader.commentsOffset); + m_songMessage.Read(file, file.ReadUint16LE(), SongMessage::leAutodetect); + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_ptm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ptm.cpp new file mode 100644 index 00000000..0b4acfac --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ptm.cpp @@ -0,0 +1,290 @@ +/* + * Load_ptm.cpp + * ------------ + * Purpose: PTM (PolyTracker) module loader + * Notes : (currently none) + * 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" + +OPENMPT_NAMESPACE_BEGIN + +struct PTMFileHeader +{ + char songname[28]; // Name of song, asciiz string + uint8le dosEOF; // 26 + uint8le versionLo; // 03 version of file, currently 0203h + uint8le versionHi; // 02 + uint8le reserved1; // Reserved, set to 0 + uint16le numOrders; // Number of orders (0..256) + uint16le numSamples; // Number of instruments (1..255) + uint16le numPatterns; // Number of patterns (1..128) + uint16le numChannels; // Number of channels (voices) used (1..32) + uint16le flags; // Set to 0 + uint8le reserved2[2]; // Reserved, set to 0 + char magic[4]; // Song identification, 'PTMF' + uint8le reserved3[16]; // Reserved, set to 0 + uint8le chnPan[32]; // Channel panning settings, 0..15, 0 = left, 7 = middle, 15 = right + uint8le orders[256]; // Order list, valid entries 0..nOrders-1 + uint16le patOffsets[128]; // Pattern offsets (*16) +}; + +MPT_BINARY_STRUCT(PTMFileHeader, 608) + +struct PTMSampleHeader +{ + enum SampleFlags + { + smpTypeMask = 0x03, + smpPCM = 0x01, + + smpLoop = 0x04, + smpPingPong = 0x08, + smp16Bit = 0x10, + }; + + uint8le flags; // Sample type (see SampleFlags) + char filename[12]; // Name of external sample file + uint8le volume; // Default volume + uint16le c4speed; // C-4 speed (yep, not C-5) + uint8le smpSegment[2]; // Sample segment (used internally) + uint32le dataOffset; // Offset of sample data + uint32le length; // Sample size (in bytes) + uint32le loopStart; // Start of loop + uint32le loopEnd; // End of loop + uint8le gusdata[14]; + char samplename[28]; // Name of sample, ASCIIZ + char magic[4]; // Sample identification, 'PTMS' + + // Convert an PTM sample header to OpenMPT's internal sample header. + SampleIO ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(MOD_TYPE_S3M); + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4; + mptSmp.nC5Speed = c4speed * 2; + + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename); + + SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::deltaPCM); + + if((flags & smpTypeMask) == smpPCM) + { + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + if(mptSmp.nLoopEnd > mptSmp.nLoopStart) + mptSmp.nLoopEnd--; + + if(flags & smpLoop) mptSmp.uFlags.set(CHN_LOOP); + if(flags & smpPingPong) mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & smp16Bit) + { + sampleIO |= SampleIO::_16bit; + sampleIO |= SampleIO::PTM8Dto16; + + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + } + + return sampleIO; + } +}; + +MPT_BINARY_STRUCT(PTMSampleHeader, 80) + + +static bool ValidateHeader(const PTMFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "PTMF", 4) + || fileHeader.dosEOF != 26 + || fileHeader.versionHi > 2 + || fileHeader.flags != 0 + || !fileHeader.numChannels + || fileHeader.numChannels > 32 + || !fileHeader.numOrders || fileHeader.numOrders > 256 + || !fileHeader.numSamples || fileHeader.numSamples > 255 + || !fileHeader.numPatterns || fileHeader.numPatterns > 128 + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const PTMFileHeader &fileHeader) +{ + return fileHeader.numSamples * sizeof(PTMSampleHeader); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderPTM(MemoryFileReader file, const uint64 *pfilesize) +{ + PTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadPTM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + PTMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_PTM); + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songname); + + m_modFormat.formatName = U_("PolyTracker"); + m_modFormat.type = U_("ptm"); + m_modFormat.madeWithTracker = MPT_UFORMAT("PolyTracker {}.{}")(fileHeader.versionHi.get(), mpt::ufmt::hex0<2>(fileHeader.versionLo.get())); + m_modFormat.charset = mpt::Charset::CP437; + + m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; + m_nChannels = fileHeader.numChannels; + m_nSamples = std::min(static_cast<SAMPLEINDEX>(fileHeader.numSamples), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1)); + ReadOrderFromArray(Order(), fileHeader.orders, fileHeader.numOrders, 0xFF, 0xFE); + + // Reading channel panning + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = ((fileHeader.chnPan[chn] & 0x0F) << 4) + 4; + } + + // Reading samples + FileReader sampleHeaderChunk = file.ReadChunk(fileHeader.numSamples * sizeof(PTMSampleHeader)); + for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++) + { + PTMSampleHeader sampleHeader; + sampleHeaderChunk.ReadStruct(sampleHeader); + + ModSample &sample = Samples[smp + 1]; + m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.samplename); + SampleIO sampleIO = sampleHeader.ConvertToMPT(sample); + + if((loadFlags & loadSampleData) && sample.nLength && file.Seek(sampleHeader.dataOffset)) + { + sampleIO.ReadSample(sample, file); + } + } + + // Reading Patterns + if(!(loadFlags & loadPatternData)) + { + return true; + } + + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + if(!Patterns.Insert(pat, 64) + || fileHeader.patOffsets[pat] == 0 + || !file.Seek(fileHeader.patOffsets[pat] << 4)) + { + continue; + } + + ModCommand *rowBase = Patterns[pat].GetpModCommand(0, 0); + ROWINDEX row = 0; + while(row < 64 && file.CanRead(1)) + { + uint8 b = file.ReadUint8(); + + if(b == 0) + { + row++; + rowBase += m_nChannels; + continue; + } + CHANNELINDEX chn = (b & 0x1F); + ModCommand dummy = ModCommand(); + ModCommand &m = chn < GetNumChannels() ? rowBase[chn] : dummy; + + if(b & 0x20) + { + const auto [note, instr] = file.ReadArray<uint8, 2>(); + m.note = note; + m.instr = instr; + if(m.note == 254) + m.note = NOTE_NOTECUT; + else if(!m.note || m.note > 120) + m.note = NOTE_NONE; + } + if(b & 0x40) + { + const auto [command, param] = file.ReadArray<uint8, 2>(); + m.command = command; + m.param = param; + + static constexpr EffectCommand effTrans[] = { CMD_GLOBALVOLUME, CMD_RETRIG, CMD_FINEVIBRATO, CMD_NOTESLIDEUP, CMD_NOTESLIDEDOWN, CMD_NOTESLIDEUPRETRIG, CMD_NOTESLIDEDOWNRETRIG, CMD_REVERSEOFFSET }; + if(m.command < 0x10) + { + // Beware: Effect letters are as in MOD, but portamento and volume slides behave like in S3M (i.e. fine slides share the same effect letters) + ConvertModCommand(m); + } else if(m.command < 0x10 + std::size(effTrans)) + { + m.command = effTrans[m.command - 0x10]; + } else + { + m.command = CMD_NONE; + } + switch(m.command) + { + case CMD_PANNING8: + // Don't be surprised about the strange formula, this is directly translated from original disassembly... + m.command = CMD_S3MCMDEX; + m.param = 0x80 | ((std::max<uint8>(m.param >> 3, 1u) - 1u) & 0x0F); + break; + case CMD_GLOBALVOLUME: + m.param = std::min(m.param, uint8(0x40)) * 2u; + break; + } + } + if(b & 0x80) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = file.ReadUint8(); + } + } + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_s3m.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_s3m.cpp new file mode 100644 index 00000000..dea5ecb3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_s3m.cpp @@ -0,0 +1,1090 @@ +/* + * Load_s3m.cpp + * ------------ + * Purpose: S3M (ScreamTracker 3) module loader / saver + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "S3MTools.h" +#include "ITTools.h" +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/mptFileIO.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#endif // MODPLUG_TRACKER +#endif // MODPLUG_NO_FILESAVE +#include "../common/version.h" + + +OPENMPT_NAMESPACE_BEGIN + + +void CSoundFile::S3MConvert(ModCommand &m, bool fromIT) +{ + switch(m.command | 0x40) + { + case '@': m.command = (m.param ? CMD_DUMMY : CMD_NONE); break; + case 'A': m.command = CMD_SPEED; break; + case 'B': m.command = CMD_POSITIONJUMP; break; + case 'C': m.command = CMD_PATTERNBREAK; if (!fromIT) m.param = (m.param >> 4) * 10 + (m.param & 0x0F); break; + case 'D': m.command = CMD_VOLUMESLIDE; break; + case 'E': m.command = CMD_PORTAMENTODOWN; break; + case 'F': m.command = CMD_PORTAMENTOUP; break; + case 'G': m.command = CMD_TONEPORTAMENTO; break; + case 'H': m.command = CMD_VIBRATO; break; + case 'I': m.command = CMD_TREMOR; break; + case 'J': m.command = CMD_ARPEGGIO; break; + case 'K': m.command = CMD_VIBRATOVOL; break; + case 'L': m.command = CMD_TONEPORTAVOL; break; + case 'M': m.command = CMD_CHANNELVOLUME; break; + case 'N': m.command = CMD_CHANNELVOLSLIDE; break; + case 'O': m.command = CMD_OFFSET; break; + case 'P': m.command = CMD_PANNINGSLIDE; break; + case 'Q': m.command = CMD_RETRIG; break; + case 'R': m.command = CMD_TREMOLO; break; + case 'S': m.command = CMD_S3MCMDEX; break; + case 'T': m.command = CMD_TEMPO; break; + case 'U': m.command = CMD_FINEVIBRATO; break; + case 'V': m.command = CMD_GLOBALVOLUME; break; + case 'W': m.command = CMD_GLOBALVOLSLIDE; break; + case 'X': m.command = CMD_PANNING8; break; + case 'Y': m.command = CMD_PANBRELLO; break; + case 'Z': m.command = CMD_MIDI; break; + case '\\': m.command = fromIT ? CMD_SMOOTHMIDI : CMD_MIDI; break; + // Chars under 0x40 don't save properly, so the following commands don't map to their pattern editor representations + case ']': m.command = fromIT ? CMD_DELAYCUT : CMD_NONE; break; + case '[': m.command = fromIT ? CMD_XPARAM : CMD_NONE; break; + case '^': m.command = fromIT ? CMD_FINETUNE : CMD_NONE; break; + case '_': m.command = fromIT ? CMD_FINETUNE_SMOOTH : CMD_NONE; break; + // BeRoTracker extensions + case '1' + 0x41: m.command = fromIT ? CMD_KEYOFF : CMD_NONE; break; + case '2' + 0x41: m.command = fromIT ? CMD_SETENVPOSITION : CMD_NONE; break; + default: m.command = CMD_NONE; + } +} + +#ifndef MODPLUG_NO_FILESAVE + +void CSoundFile::S3MSaveConvert(uint8 &command, uint8 ¶m, bool toIT, bool compatibilityExport) const +{ + const bool extendedIT = !compatibilityExport && toIT; + switch(command) + { + case CMD_DUMMY: command = (param ? '@' : 0); break; + case CMD_SPEED: command = 'A'; break; + case CMD_POSITIONJUMP: command = 'B'; break; + case CMD_PATTERNBREAK: command = 'C'; if(!toIT) param = ((param / 10) << 4) + (param % 10); break; + case CMD_VOLUMESLIDE: command = 'D'; break; + case CMD_PORTAMENTODOWN: command = 'E'; if (param >= 0xE0 && (GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) param = 0xDF; break; + case CMD_PORTAMENTOUP: command = 'F'; if (param >= 0xE0 && (GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) param = 0xDF; break; + case CMD_TONEPORTAMENTO: command = 'G'; break; + case CMD_VIBRATO: command = 'H'; break; + case CMD_TREMOR: command = 'I'; break; + case CMD_ARPEGGIO: command = 'J'; break; + case CMD_VIBRATOVOL: command = 'K'; break; + case CMD_TONEPORTAVOL: command = 'L'; break; + case CMD_CHANNELVOLUME: command = 'M'; break; + case CMD_CHANNELVOLSLIDE: command = 'N'; break; + case CMD_OFFSETPERCENTAGE: + case CMD_OFFSET: command = 'O'; break; + case CMD_PANNINGSLIDE: command = 'P'; break; + case CMD_RETRIG: command = 'Q'; break; + case CMD_TREMOLO: command = 'R'; break; + case CMD_S3MCMDEX: command = 'S'; break; + case CMD_TEMPO: command = 'T'; break; + case CMD_FINEVIBRATO: command = 'U'; break; + case CMD_GLOBALVOLUME: command = 'V'; break; + case CMD_GLOBALVOLSLIDE: command = 'W'; break; + case CMD_PANNING8: + command = 'X'; + if(toIT && !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM | MOD_TYPE_MOD))) + { + if (param == 0xA4) { command = 'S'; param = 0x91; } + else if (param == 0x80) { param = 0xFF; } + else if (param < 0x80) { param <<= 1; } + else command = 0; + } else if (!toIT && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM | MOD_TYPE_MOD))) + { + param >>= 1; + } + break; + case CMD_PANBRELLO: command = 'Y'; break; + case CMD_MIDI: command = 'Z'; break; + case CMD_SMOOTHMIDI: + if(extendedIT) + command = '\\'; + else + command = 'Z'; + break; + case CMD_XFINEPORTAUPDOWN: + switch(param & 0xF0) + { + case 0x10: command = 'F'; param = (param & 0x0F) | 0xE0; break; + case 0x20: command = 'E'; param = (param & 0x0F) | 0xE0; break; + case 0x90: command = 'S'; break; + default: command = 0; + } + break; + case CMD_MODCMDEX: + { + ModCommand m; + m.command = CMD_MODCMDEX; + m.param = param; + m.ExtendedMODtoS3MEffect(); + command = m.command; + param = m.param; + S3MSaveConvert(command, param, toIT, compatibilityExport); + } + return; + // Chars under 0x40 don't save properly, so map : to ] and # to [. + case CMD_DELAYCUT: + command = extendedIT ? ']' : 0; + break; + case CMD_XPARAM: + command = extendedIT ? '[' : 0; + break; + case CMD_FINETUNE: + command = extendedIT ? '^' : 0; + break; + case CMD_FINETUNE_SMOOTH: + command = extendedIT ? '_' : 0; + break; + default: + command = 0; + } + if(command == 0) + { + param = 0; + } + + command &= ~0x40; +} + +#endif // MODPLUG_NO_FILESAVE + +static bool ValidateHeader(const S3MFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "SCRM", 4) + || fileHeader.fileType != S3MFileHeader::idS3MType + || (fileHeader.formatVersion != S3MFileHeader::oldVersion && fileHeader.formatVersion != S3MFileHeader::newVersion) + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const S3MFileHeader &fileHeader) +{ + return fileHeader.ordNum + (fileHeader.smpNum + fileHeader.patNum) * 2; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderS3M(MemoryFileReader file, const uint64 *pfilesize) +{ + S3MFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + // Is it a valid S3M file? + S3MFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_S3M); + m_nMinPeriod = 64; + m_nMaxPeriod = 32767; + + // ST3 ignored Zxx commands, so if we find that a file was made with ST3, we should erase all MIDI macros. + bool keepMidiMacros = false; + + mpt::ustring madeWithTracker; + bool formatTrackerStr = false; + bool nonCompatTracker = false; + bool isST3 = false; + bool isSchism = false; + const int32 schismDateVersion = SchismTrackerEpoch + ((fileHeader.cwtv == 0x4FFF) ? fileHeader.reserved2 : (fileHeader.cwtv - 0x4050)); + switch(fileHeader.cwtv & S3MFileHeader::trackerMask) + { + case S3MFileHeader::trkAkord & S3MFileHeader::trackerMask: + if(fileHeader.cwtv == S3MFileHeader::trkAkord) + madeWithTracker = U_("Akord"); + break; + case S3MFileHeader::trkScreamTracker: + if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && (fileHeader.ordNum & 0x0F) == 0 && fileHeader.ultraClicks == 0 && (fileHeader.flags & ~0x50) == 0) + { + // 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.00.00"); + madeWithTracker = U_("ModPlug Tracker / OpenMPT 1.17"); + } else + { + // MPT 1.0 alpha5 doesn't set the stereo flag, but MPT 1.0 beta1 does. + m_dwLastSavedWithVersion = MPT_V("1.00.00.00"); + madeWithTracker = U_("ModPlug Tracker 1.0 alpha"); + } + keepMidiMacros = true; + nonCompatTracker = true; + m_playBehaviour.set(kST3LimitPeriod); + } else if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && fileHeader.ultraClicks == 0 && fileHeader.flags == 0 && fileHeader.usePanningTable == 0) + { + madeWithTracker = U_("Velvet Studio"); + } else + { + // ST3.20 should only ever write ultra-click values 16, 24 and 32 (corresponding to 8, 12 and 16 in the GUI), ST3.01/3.03 should only write 0, + // though several ST3.01/3.03 files with ultra-click values of 16 have been found as well. + // However, we won't fingerprint these values here as it's unlikely that there is any other tracker out there disguising as ST3 and using a strange ultra-click value. + // Also, re-saving a file with a strange ultra-click value in ST3 doesn't fix this value unless the user manually changes it, or if it's below 16. + madeWithTracker = U_("Scream Tracker"); + formatTrackerStr = true; + isST3 = true; + } + break; + case S3MFileHeader::trkImagoOrpheus: + madeWithTracker = U_("Imago Orpheus"); + formatTrackerStr = true; + nonCompatTracker = true; + break; + case S3MFileHeader::trkImpulseTracker: + if(fileHeader.cwtv <= S3MFileHeader::trkIT2_14) + { + madeWithTracker = U_("Impulse Tracker"); + formatTrackerStr = true; + } else + { + madeWithTracker = MPT_UFORMAT("Impulse Tracker 2.14p{}")(fileHeader.cwtv - S3MFileHeader::trkIT2_14); + } + if(fileHeader.cwtv >= S3MFileHeader::trkIT2_07 && fileHeader.reserved3 != 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.reserved3); + + FileHistory hist; + hist.openTime = static_cast<uint32>(editTime * (HISTORY_TIMER_PRECISION / 18.2)); + m_FileHistory.push_back(hist); + } + nonCompatTracker = true; + m_playBehaviour.set(kPeriodsAreHertz); + m_playBehaviour.set(kITRetrigger); + m_playBehaviour.set(kITShortSampleRetrig); + m_playBehaviour.set(kST3SampleSwap); // Not exactly like ST3, but close enough + m_nMinPeriod = 1; + break; + case S3MFileHeader::trkSchismTracker: + if(fileHeader.cwtv == S3MFileHeader::trkBeRoTrackerOld) + { + madeWithTracker = U_("BeRoTracker"); + m_playBehaviour.set(kST3LimitPeriod); + } else + { + madeWithTracker = GetSchismTrackerVersion(fileHeader.cwtv, fileHeader.reserved2); + m_nMinPeriod = 1; + isSchism = true; + if(schismDateVersion >= SchismVersionFromDate<2021, 05, 02>::date) + m_playBehaviour.set(kPeriodsAreHertz); + if(schismDateVersion >= SchismVersionFromDate<2016, 05, 13>::date) + m_playBehaviour.set(kITShortSampleRetrig); + } + nonCompatTracker = true; + break; + case S3MFileHeader::trkOpenMPT: + { + uint32 mptVersion = (fileHeader.cwtv & S3MFileHeader::versionMask) << 16; + if(mptVersion >= 0x01'29'00'00) + mptVersion |= fileHeader.reserved2; + m_dwLastSavedWithVersion = Version(mptVersion); + madeWithTracker = U_("OpenMPT ") + mpt::ufmt::val(m_dwLastSavedWithVersion); + } + break; + case S3MFileHeader::trkBeRoTracker: + madeWithTracker = U_("BeRoTracker"); + m_playBehaviour.set(kST3LimitPeriod); + break; + case S3MFileHeader::trkCreamTracker: + madeWithTracker = U_("CreamTracker"); + break; + default: + if(fileHeader.cwtv == S3MFileHeader::trkCamoto) + madeWithTracker = U_("Camoto"); + break; + } + if(formatTrackerStr) + { + madeWithTracker = MPT_UFORMAT("{} {}.{}")(madeWithTracker, (fileHeader.cwtv & 0xF00) >> 8, mpt::ufmt::hex0<2>(fileHeader.cwtv & 0xFF)); + } + + m_modFormat.formatName = U_("Scream Tracker 3"); + m_modFormat.type = U_("s3m"); + m_modFormat.madeWithTracker = std::move(madeWithTracker); + m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437; + + if(nonCompatTracker) + { + m_playBehaviour.reset(kST3NoMutedChannels); + m_playBehaviour.reset(kST3EffectMemory); + m_playBehaviour.reset(kST3PortaSampleChange); + m_playBehaviour.reset(kST3VibratoMemory); + m_playBehaviour.reset(KST3PortaAfterArpeggio); + m_playBehaviour.reset(kST3OffsetWithoutInstrument); + m_playBehaviour.reset(kApplyUpperPeriodLimit); + } + + if((fileHeader.cwtv & S3MFileHeader::trackerMask) > S3MFileHeader::trkScreamTracker) + { + if((fileHeader.cwtv & S3MFileHeader::trackerMask) != S3MFileHeader::trkImpulseTracker || fileHeader.cwtv >= S3MFileHeader::trkIT2_14) + { + // Keep MIDI macros if this is not an old IT version (BABYLON.S3M by Necros has Zxx commands and was saved with IT 2.05) + keepMidiMacros = true; + } + } + + m_MidiCfg.Reset(); + if(!keepMidiMacros) + { + // Remove macros so they don't interfere with tunes made in trackers that don't support Zxx + m_MidiCfg.ClearZxxMacros(); + } + + m_songName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.name); + + if(fileHeader.flags & S3MFileHeader::amigaLimits) m_SongFlags.set(SONG_AMIGALIMITS); + if(fileHeader.flags & S3MFileHeader::st2Vibrato) m_SongFlags.set(SONG_S3MOLDVIBRATO); + + if(fileHeader.cwtv == S3MFileHeader::trkST3_00 || (fileHeader.flags & S3MFileHeader::fastVolumeSlides) != 0) + { + m_SongFlags.set(SONG_FASTVOLSLIDES); + } + + // Speed + m_nDefaultSpeed = fileHeader.speed; + if(m_nDefaultSpeed == 0 || (m_nDefaultSpeed == 255 && isST3)) + { + // Even though ST3 accepts the command AFF as expected, it mysteriously fails to load a default speed of 255... + m_nDefaultSpeed = 6; + } + + // Tempo + m_nDefaultTempo.Set(fileHeader.tempo); + if(fileHeader.tempo < 33) + { + // ST3 also fails to load an otherwise valid default tempo of 32... + m_nDefaultTempo.Set(isST3 ? 125 : 32); + } + + // Global Volume + m_nDefaultGlobalVolume = std::min(fileHeader.globalVol.get(), uint8(64)) * 4u; + // The following check is probably not very reliable, but it fixes a few tunes, e.g. + // DARKNESS.S3M by Purple Motion (ST 3.00) and "Image of Variance" by C.C.Catch (ST 3.01): + if(m_nDefaultGlobalVolume == 0 && fileHeader.cwtv < S3MFileHeader::trkST3_20) + { + m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + } + + if(fileHeader.formatVersion == S3MFileHeader::oldVersion && fileHeader.masterVolume < 8) + m_nSamplePreAmp = std::min((fileHeader.masterVolume + 1) * 0x10, 0x7F); + // These changes were probably only supposed to be done for older format revisions, where supposedly 0x10 was the stereo flag. + // However, this version check is missing in ST3, so any mono file with a master volume of 18 will be converted to a stereo file with master volume 32. + else if(fileHeader.masterVolume == 2 || fileHeader.masterVolume == (2 | 0x10)) + m_nSamplePreAmp = 0x20; + else if(!(fileHeader.masterVolume & 0x7F)) + m_nSamplePreAmp = 48; + else + m_nSamplePreAmp = std::max(fileHeader.masterVolume & 0x7F, 0x10); // Bit 7 = Stereo (we always use stereo) + + const bool isStereo = (fileHeader.masterVolume & 0x80) != 0 || m_dwLastSavedWithVersion; + if(!isStereo) + m_nSamplePreAmp = Util::muldivr_unsigned(m_nSamplePreAmp, 8, 11); + + // Approximately as loud as in DOSBox and a real SoundBlaster 16 + m_nVSTiVolume = 36; + if(isSchism && schismDateVersion < SchismVersionFromDate<2018, 11, 12>::date) + m_nVSTiVolume = 64; + + // Channel setup + m_nChannels = 4; + std::bitset<32> isAdlibChannel; + for(CHANNELINDEX i = 0; i < 32; i++) + { + ChnSettings[i].Reset(); + + uint8 ctype = fileHeader.channels[i] & ~0x80; + if(fileHeader.channels[i] != 0xFF) + { + m_nChannels = i + 1; + if(isStereo) + ChnSettings[i].nPan = (ctype & 8) ? 0xCC : 0x33; // 200 : 56 + } + if(fileHeader.channels[i] & 0x80) + { + ChnSettings[i].dwFlags = CHN_MUTE; + } + if(ctype >= 16 && ctype <= 29) + { + // Adlib channel - except for OpenMPT 1.19 and older, which would write wrong channel types for PCM channels 16-32. + // However, MPT/OpenMPT always wrote the extra panning table, so there is no need to consider this here. + ChnSettings[i].nPan = 128; + isAdlibChannel[i] = true; + } + } + if(m_nChannels < 1) + { + m_nChannels = 1; + } + + ReadOrderFromFile<uint8>(Order(), file, fileHeader.ordNum, 0xFF, 0xFE); + + // Read sample header offsets + std::vector<uint16le> sampleOffsets; + file.ReadVector(sampleOffsets, fileHeader.smpNum); + // Read pattern offsets + std::vector<uint16le> patternOffsets; + file.ReadVector(patternOffsets, fileHeader.patNum); + + // Read extended channel panning + if(fileHeader.usePanningTable == S3MFileHeader::idPanning) + { + uint8 pan[32]; + file.ReadArray(pan); + for(CHANNELINDEX i = 0; i < 32; i++) + { + if((pan[i] & 0x20) != 0 && (!isST3 || !isAdlibChannel[i])) + { + ChnSettings[i].nPan = (static_cast<uint16>(pan[i] & 0x0F) * 256 + 8) / 15; + } + } + } + + // Reading sample headers + m_nSamples = std::min(static_cast<SAMPLEINDEX>(fileHeader.smpNum), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1)); + bool anySamples = false; + uint16 gusAddresses = 0; + for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++) + { + S3MSampleHeader sampleHeader; + + if(!file.Seek(sampleOffsets[smp] * 16) || !file.ReadStruct(sampleHeader)) + { + continue; + } + + sampleHeader.ConvertToMPT(Samples[smp + 1], isST3); + m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); + + if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel) + { + const uint32 sampleOffset = sampleHeader.GetSampleOffset(); + if((loadFlags & loadSampleData) && sampleHeader.length != 0 && file.Seek(sampleOffset)) + { + sampleHeader.GetSampleFormat((fileHeader.formatVersion == S3MFileHeader::oldVersion)).ReadSample(Samples[smp + 1], file); + anySamples = true; + } + gusAddresses |= sampleHeader.gusAddress; + } + } + + if(isST3 && anySamples && !gusAddresses && fileHeader.cwtv != S3MFileHeader::trkST3_00) + { + // All Scream Tracker versions except for some probably early revisions of Scream Tracker 3.00 write GUS addresses. GUS support might not have existed at that point (1992). + // Hence if a file claims to be written with ST3 (but not ST3.00), but has no GUS addresses, we deduce that it must be written by some other software (e.g. some PSM -> S3M conversions) + isST3 = false; + MPT_UNUSED(isST3); + m_modFormat.madeWithTracker = U_("Unknown"); + } else if(isST3) + { + // Saving an S3M file in ST3 with the Gravis Ultrasound driver loaded will write a unique GUS memory address for each non-empty sample slot (and 0 for unused slots). + // Re-saving that file in ST3 with the SoundBlaster driver loaded will reset the GUS address for all samples to 0 (unused) or 1 (used). + // The first used sample will also have an address of 1 with the GUS driver. + // So this is a safe way of telling if the file was last saved with the GUS driver loaded or not if there's more than one sample. + const bool useGUS = gusAddresses > 1; + m_playBehaviour.set(kST3PortaSampleChange, useGUS); + m_playBehaviour.set(kST3SampleSwap, !useGUS); + m_playBehaviour.set(kITShortSampleRetrig, !useGUS); // Only half the truth but close enough for now + m_modFormat.madeWithTracker += useGUS ? UL_(" (GUS)") : UL_(" (SB)"); + // ST3's GUS driver doesn't use this value. Ignoring it fixes the balance between FM and PCM samples (e.g. in Rotagilla by Manwe) + if(useGUS) + m_nSamplePreAmp = 48; + } + + // Try to find out if Zxx commands are supposed to be panning commands (PixPlay). + // Actually I am only aware of one module that uses this panning style, namely "Crawling Despair" by $volkraq + // and I have no idea what PixPlay is, so this code is solely based on the sample text of that module. + // We won't convert if there are not enough Zxx commands, too "high" Zxx commands + // or there are only "left" or "right" pannings (we assume that stereo should be somewhat balanced), + // and modules not made with an old version of ST3 were probably made in a tracker that supports panning anyway. + bool pixPlayPanning = (fileHeader.cwtv < S3MFileHeader::trkST3_20); + int zxxCountRight = 0, zxxCountLeft = 0; + + // Reading patterns + if(!(loadFlags & loadPatternData)) + { + return true; + } + // Order list cannot contain pattern indices > 255, so do not even try to load higher patterns + const PATTERNINDEX readPatterns = std::min(static_cast<PATTERNINDEX>(fileHeader.patNum), static_cast<PATTERNINDEX>(uint8_max)); + Patterns.ResizeArray(readPatterns); + for(PATTERNINDEX pat = 0; pat < readPatterns; pat++) + { + // A zero parapointer indicates an empty pattern. + if(!Patterns.Insert(pat, 64) || patternOffsets[pat] == 0 || !file.Seek(patternOffsets[pat] * 16)) + { + continue; + } + + // Skip pattern length indication. + // Some modules, for example http://aminet.net/mods/8voic/s3m_hunt.lha seem to have a wrong pattern length - + // If you strictly adhere the pattern length, you won't read some patterns (e.g. 17) correctly in that module. + // It's most likely a broken copy because there are other versions of the track which don't have this issue. + // Still, we don't really need this information, so we just ignore it. + file.Skip(2); + + // Read pattern data + ROWINDEX row = 0; + PatternRow rowBase = Patterns[pat].GetRow(0); + + while(row < 64) + { + uint8 info = file.ReadUint8(); + + if(info == s3mEndOfRow) + { + // End of row + if(++row < 64) + { + rowBase = Patterns[pat].GetRow(row); + } + continue; + } + + CHANNELINDEX channel = (info & s3mChannelMask); + ModCommand dummy; + ModCommand &m = (channel < GetNumChannels()) ? rowBase[channel] : dummy; + + if(info & s3mNotePresent) + { + const auto [note, instr] = file.ReadArray<uint8, 2>(); + if(note < 0xF0) + m.note = static_cast<ModCommand::NOTE>(Clamp((note & 0x0F) + 12 * (note >> 4) + 12 + NOTE_MIN, NOTE_MIN, NOTE_MAX)); + else if(note == s3mNoteOff) + m.note = NOTE_NOTECUT; + else if(note == s3mNoteNone) + m.note = NOTE_NONE; + m.instr = instr; + } + + if(info & s3mVolumePresent) + { + uint8 volume = file.ReadUint8(); + if(volume >= 128 && volume <= 192) + { + m.volcmd = VOLCMD_PANNING; + m.vol = volume - 128; + } else + { + m.volcmd = VOLCMD_VOLUME; + m.vol = std::min(volume, uint8(64)); + } + } + + if(info & s3mEffectPresent) + { + const auto [command, param] = file.ReadArray<uint8, 2>(); + m.command = command; + m.param = param; + S3MConvert(m, false); + + if(m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0xA0 && fileHeader.cwtv < S3MFileHeader::trkST3_20) + { + // Convert old SAx panning to S8x (should only be found in PANIC.S3M by Purple Motion) + m.param = 0x80 | ((m.param & 0x0F) ^ 8); + } else if(m.command == CMD_MIDI) + { + // PixPlay panning test + if(m.param > 0x0F) + { + // PixPlay has Z00 to Z0F panning, so we ignore this. + pixPlayPanning = false; + } else + { + if(m.param < 0x08) + zxxCountLeft++; + else if(m.param > 0x08) + zxxCountRight++; + } + } else if(m.command == CMD_OFFSET && m.param == 0 && fileHeader.cwtv <= S3MFileHeader::trkST3_01) + { + // Offset command didn't have effect memory in ST3.01; fixed in ST3.03 + m.command = CMD_DUMMY; + } + } + } + } + + if(pixPlayPanning && zxxCountLeft + zxxCountRight >= m_nChannels && (-zxxCountLeft + zxxCountRight) < static_cast<int>(m_nChannels)) + { + // There are enough Zxx commands, so let's assume this was made to be played with PixPlay + Patterns.ForEachModCommand([](ModCommand &m) + { + if(m.command == CMD_MIDI) + { + m.command = CMD_S3MCMDEX; + m.param |= 0x80; + } + }); + } + + return true; +} + + +#ifndef MODPLUG_NO_FILESAVE + +bool CSoundFile::SaveS3M(std::ostream &f) const +{ + static constexpr uint8 filler[16] = + { + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, + }; + + if(m_nChannels == 0) + { + return false; + } + + const bool saveMuteStatus = +#ifdef MODPLUG_TRACKER + TrackerSettings::Instance().MiscSaveChannelMuteStatus; +#else + true; +#endif + + S3MFileHeader fileHeader; + MemsetZero(fileHeader); + + mpt::String::WriteBuf(mpt::String::nullTerminated, fileHeader.name) = m_songName; + fileHeader.dosEof = S3MFileHeader::idEOF; + fileHeader.fileType = S3MFileHeader::idS3MType; + + // Orders + ORDERINDEX writeOrders = Order().GetLengthTailTrimmed(); + if(writeOrders < 2) + { + writeOrders = 2; + } else if((writeOrders % 2u) != 0) + { + // Number of orders should be even + writeOrders++; + } + LimitMax(writeOrders, static_cast<ORDERINDEX>(256)); + fileHeader.ordNum = static_cast<uint16>(writeOrders); + + // Samples + SAMPLEINDEX writeSamples = static_cast<SAMPLEINDEX>(GetNumInstruments()); + if(writeSamples == 0) + { + writeSamples = GetNumSamples(); + } + writeSamples = Clamp(writeSamples, static_cast<SAMPLEINDEX>(1), static_cast<SAMPLEINDEX>(99)); + fileHeader.smpNum = static_cast<uint16>(writeSamples); + + // Patterns + PATTERNINDEX writePatterns = std::min(Patterns.GetNumPatterns(), PATTERNINDEX(100)); + fileHeader.patNum = static_cast<uint16>(writePatterns); + + // Flags + if(m_SongFlags[SONG_FASTVOLSLIDES]) + { + fileHeader.flags |= S3MFileHeader::fastVolumeSlides; + } + if(m_nMaxPeriod < 20000 || m_SongFlags[SONG_AMIGALIMITS]) + { + fileHeader.flags |= S3MFileHeader::amigaLimits; + } + if(m_SongFlags[SONG_S3MOLDVIBRATO]) + { + fileHeader.flags |= S3MFileHeader::st2Vibrato; + } + + // Version info following: ST3.20 = 0x1320 + // Most significant nibble = Tracker ID, see S3MFileHeader::S3MTrackerVersions + // Following: One nibble = Major version, one byte = Minor version (hex) + const uint32 mptVersion = Version::Current().GetRawVersion(); + fileHeader.cwtv = S3MFileHeader::trkOpenMPT | static_cast<uint16>((mptVersion >> 16) & S3MFileHeader::versionMask); + fileHeader.reserved2 = static_cast<uint16>(mptVersion); + fileHeader.formatVersion = S3MFileHeader::newVersion; + memcpy(fileHeader.magic, "SCRM", 4); + + // Song Variables + fileHeader.globalVol = static_cast<uint8>(std::min(m_nDefaultGlobalVolume / 4u, uint32(64))); + fileHeader.speed = static_cast<uint8>(Clamp(m_nDefaultSpeed, 1u, 254u)); + fileHeader.tempo = static_cast<uint8>(Clamp(m_nDefaultTempo.GetInt(), 33u, 255u)); + fileHeader.masterVolume = static_cast<uint8>(Clamp(m_nSamplePreAmp, 16u, 127u) | 0x80); + fileHeader.ultraClicks = 16; + fileHeader.usePanningTable = S3MFileHeader::idPanning; + + mpt::IO::Write(f, fileHeader); + Order().WriteAsByte(f, writeOrders); + + // Comment about parapointers stolen from Schism Tracker: + // The sample data parapointers are 24+4 bits, whereas pattern data and sample headers are only 16+4 + // bits -- so while the sample data can be written up to 268 MB within the file (starting at 0xffffff0), + // the pattern data and sample headers are restricted to the first 1 MB (starting at 0xffff0). In effect, + // this practically requires the sample data to be written last in the file, as it is entirely possible + // (and quite easy, even) to write more than 1 MB of sample data in a file. + // The "practical standard order" listed in TECH.DOC is sample headers, patterns, then sample data. + + // Calculate offset of first sample header... + mpt::IO::Offset sampleHeaderOffset = mpt::IO::TellWrite(f) + (writeSamples + writePatterns) * 2 + 32; + // ...which must be a multiple of 16, because parapointers omit the lowest 4 bits. + sampleHeaderOffset = (sampleHeaderOffset + 15) & ~15; + + std::vector<uint16le> sampleOffsets(writeSamples); + for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++) + { + static_assert((sizeof(S3MSampleHeader) % 16) == 0); + sampleOffsets[smp] = static_cast<uint16>((sampleHeaderOffset + smp * sizeof(S3MSampleHeader)) / 16); + } + mpt::IO::Write(f, sampleOffsets); + + mpt::IO::Offset patternPointerOffset = mpt::IO::TellWrite(f); + mpt::IO::Offset firstPatternOffset = sampleHeaderOffset + writeSamples * sizeof(S3MSampleHeader); + std::vector<uint16le> patternOffsets(writePatterns); + + // Need to calculate the real offsets later. + mpt::IO::Write(f, patternOffsets); + + // Write channel panning + uint8 chnPan[32]; + for(CHANNELINDEX chn = 0; chn < 32; chn++) + { + if(chn < GetNumChannels()) + chnPan[chn] = static_cast<uint8>(((ChnSettings[chn].nPan * 15 + 128) / 256) | 0x20); + else + chnPan[chn] = 0x08; + } + mpt::IO::Write(f, chnPan); + + // Do we need to fill up the file with some padding bytes for 16-Byte alignment? + mpt::IO::Offset curPos = mpt::IO::TellWrite(f); + if(curPos < sampleHeaderOffset) + { + MPT_ASSERT(sampleHeaderOffset - curPos < 16); + mpt::IO::WriteRaw(f, filler, static_cast<std::size_t>(sampleHeaderOffset - curPos)); + } + + // Don't write sample headers for now, we are lacking the sample offset data. + mpt::IO::SeekAbsolute(f, firstPatternOffset); + + // Write patterns + enum class S3MChannelType : uint8 { kUnused = 0, kPCM = 1, kAdlib = 2 }; + FlagSet<S3MChannelType> channelType[32] = { S3MChannelType::kUnused }; + bool globalCmdOnMutedChn = false; + for(PATTERNINDEX pat = 0; pat < writePatterns; pat++) + { + if(Patterns.IsPatternEmpty(pat)) + { + patternOffsets[pat] = 0; + continue; + } + + mpt::IO::Offset patOffset = mpt::IO::TellWrite(f); + if(patOffset > 0xFFFF0) + { + AddToLog(LogError, MPT_UFORMAT("Too much pattern data! Writing patterns failed starting from pattern {}.")(pat)); + break; + } + MPT_ASSERT((patOffset % 16) == 0); + patternOffsets[pat] = static_cast<uint16>(patOffset / 16); + + std::vector<uint8> buffer; + buffer.reserve(5 * 1024); + // Reserve space for length bytes + buffer.resize(2, 0); + + if(Patterns.IsValidPat(pat)) + { + for(ROWINDEX row = 0; row < 64; row++) + { + if(row >= Patterns[pat].GetNumRows()) + { + // Invent empty row + buffer.push_back(s3mEndOfRow); + continue; + } + + const PatternRow rowBase = Patterns[pat].GetRow(row); + + CHANNELINDEX writeChannels = std::min(CHANNELINDEX(32), GetNumChannels()); + for(CHANNELINDEX chn = 0; chn < writeChannels; chn++) + { + const ModCommand &m = rowBase[chn]; + + uint8 info = static_cast<uint8>(chn); + uint8 note = m.note; + ModCommand::VOLCMD volcmd = m.volcmd; + uint8 vol = m.vol; + uint8 command = m.command; + uint8 param = m.param; + + if(note != NOTE_NONE || m.instr != 0) + { + info |= s3mNotePresent; + + if(note == NOTE_NONE) + { + note = s3mNoteNone; + } else if(ModCommand::IsSpecialNote(note)) + { + // Note Cut + note = s3mNoteOff; + } else if(note < 12 + NOTE_MIN) + { + // Too low + note = 0; + } else if(note <= NOTE_MAX) + { + note -= (12 + NOTE_MIN); + note = (note % 12) + ((note / 12) << 4); + } + + if(m.instr > 0 && m.instr <= GetNumSamples()) + { + const ModSample &smp = Samples[m.instr]; + if(smp.uFlags[CHN_ADLIB]) + channelType[chn].set(S3MChannelType::kAdlib); + else if(smp.HasSampleData()) + channelType[chn].set(S3MChannelType::kPCM); + } + } + + if(command == CMD_VOLUME) + { + command = CMD_NONE; + volcmd = VOLCMD_VOLUME; + vol = std::min(param, uint8(64)); + } + + if(volcmd == VOLCMD_VOLUME) + { + info |= s3mVolumePresent; + } else if(volcmd == VOLCMD_PANNING) + { + info |= s3mVolumePresent; + vol |= 0x80; + } + + if(command != CMD_NONE) + { + S3MSaveConvert(command, param, false, true); + if(command || param) + { + info |= s3mEffectPresent; + if(saveMuteStatus && ChnSettings[chn].dwFlags[CHN_MUTE] && m.IsGlobalCommand()) + { + globalCmdOnMutedChn = true; + } + } + } + + if(info & s3mAnyPresent) + { + buffer.push_back(info); + if(info & s3mNotePresent) + { + buffer.push_back(note); + buffer.push_back(m.instr); + } + if(info & s3mVolumePresent) + { + buffer.push_back(vol); + } + if(info & s3mEffectPresent) + { + buffer.push_back(command); + buffer.push_back(param); + } + } + } + + buffer.push_back(s3mEndOfRow); + } + } else + { + // Invent empty pattern + buffer.insert(buffer.end(), 64, s3mEndOfRow); + } + + uint16 length = mpt::saturate_cast<uint16>(buffer.size()); + buffer[0] = static_cast<uint8>(length & 0xFF); + buffer[1] = static_cast<uint8>((length >> 8) & 0xFF); + + if((buffer.size() % 16u) != 0) + { + // Add padding bytes + buffer.insert(buffer.end(), 16 - (buffer.size() % 16u), 0); + } + + mpt::IO::Write(f, buffer); + } + if(globalCmdOnMutedChn) + { + //AddToLog(LogWarning, U_("Global commands on muted channels are interpreted only by some S3M players.")); + } + + mpt::IO::Offset sampleDataOffset = mpt::IO::TellWrite(f); + + // Write samples + std::vector<S3MSampleHeader> sampleHeader(writeSamples); + + for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++) + { + SAMPLEINDEX realSmp = smp + 1; + if(GetNumInstruments() != 0 && Instruments[smp] != nullptr) + { + // Find some valid sample associated with this instrument. + for(SAMPLEINDEX keySmp : Instruments[smp]->Keyboard) + { + if(keySmp > 0 && keySmp <= GetNumSamples()) + { + realSmp = keySmp; + break; + } + } + } + + if(realSmp > GetNumSamples()) + { + continue; + } + + const SmpLength smpLength = sampleHeader[smp].ConvertToS3M(Samples[realSmp]); + mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader[smp].name) = m_szNames[realSmp]; + + if(smpLength != 0) + { + // Write sample data + if(sampleDataOffset > 0xFFFFFF0) + { + AddToLog(LogError, MPT_UFORMAT("Too much sample data! Writing samples failed starting from sample {}.")(realSmp)); + break; + } + + sampleHeader[smp].dataPointer[1] = static_cast<uint8>((sampleDataOffset >> 4) & 0xFF); + sampleHeader[smp].dataPointer[2] = static_cast<uint8>((sampleDataOffset >> 12) & 0xFF); + sampleHeader[smp].dataPointer[0] = static_cast<uint8>((sampleDataOffset >> 20) & 0xFF); + + size_t writtenLength = sampleHeader[smp].GetSampleFormat(false).WriteSample(f, Samples[realSmp], smpLength); + sampleDataOffset += writtenLength; + if((writtenLength % 16u) != 0) + { + size_t fillSize = 16 - (writtenLength % 16u); + mpt::IO::WriteRaw(f, filler, fillSize); + sampleDataOffset += fillSize; + } + } + } + + // Channel Table + uint8 sampleCh = 0, adlibCh = 0; + for(CHANNELINDEX chn = 0; chn < 32; chn++) + { + if(chn < GetNumChannels()) + { + if(channelType[chn][S3MChannelType::kPCM] && channelType[chn][S3MChannelType::kAdlib]) + { + AddToLog(LogWarning, MPT_UFORMAT("Pattern channel {} constains both samples and OPL instruments, which is not supported by Scream Tracker 3.")(chn + 1)); + } + // ST3 only supports 16 PCM channels, so if channels 17-32 are used, + // they must be mapped to the same "internal channels" as channels 1-16. + // The channel indices determine in which order channels are evaluated in ST3. + // First, the "left" channels (0...7) are evaluated, then the "right" channels (8...15). + // Previously, an alternating LRLR scheme was written, which would lead to a different + // effect processing in ST3 than LLL...RRR, but since OpenMPT doesn't care about the + // channel order and always parses them left to right as they appear in the pattern, + // we should just write in the LLL...RRR manner. + uint8 ch = sampleCh % 16u; // If there are neither PCM nor AdLib instruments on this channel, just fall back a regular sample-based channel for maximum compatibility. + if(channelType[chn][S3MChannelType::kPCM]) + ch = (sampleCh++) % 16u; + else if(channelType[chn][S3MChannelType::kAdlib]) + ch = 16 + ((adlibCh++) % 9u); + + if(saveMuteStatus && ChnSettings[chn].dwFlags[CHN_MUTE]) + { + ch |= 0x80; + } + fileHeader.channels[chn] = ch; + } else + { + fileHeader.channels[chn] = 0xFF; + } + } + if(sampleCh > 16) + { + AddToLog(LogWarning, MPT_UFORMAT("This module has more than 16 ({}) sample channels, which is not supported by Scream Tracker 3.")(sampleCh)); + } + if(adlibCh > 9) + { + AddToLog(LogWarning, MPT_UFORMAT("This module has more than 9 ({}) OPL channels, which is not supported by Scream Tracker 3.")(adlibCh)); + } + + mpt::IO::SeekAbsolute(f, 0); + mpt::IO::Write(f, fileHeader); + + // Now we know where the patterns are. + if(writePatterns != 0) + { + mpt::IO::SeekAbsolute(f, patternPointerOffset); + mpt::IO::Write(f, patternOffsets); + } + + // And we can finally write the sample headers. + if(writeSamples != 0) + { + mpt::IO::SeekAbsolute(f, sampleHeaderOffset); + mpt::IO::Write(f, sampleHeader); + } + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_sfx.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_sfx.cpp new file mode 100644 index 00000000..8afe6778 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_sfx.cpp @@ -0,0 +1,483 @@ +/* + * Load_sfx.cpp + * ------------ + * Purpose: SFX / MMS (SoundFX / MultiMedia Sound) module loader + * Notes : Mostly based on the Soundtracker loader, some effect behavior is based on Flod's implementation. + * Authors: Devin Acker + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "Loaders.h" +#include "Tables.h" + +OPENMPT_NAMESPACE_BEGIN + +// File Header +struct SFXFileHeader +{ + uint8be numOrders; + uint8be restartPos; + uint8be orderList[128]; +}; + +MPT_BINARY_STRUCT(SFXFileHeader, 130) + +// Sample Header +struct SFXSampleHeader +{ + char name[22]; + char dummy[2]; // Supposedly sample length, but almost always incorrect + uint8be finetune; + uint8be volume; + uint16be loopStart; + uint16be loopLength; + + // Convert an MOD sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp, uint32 length) const + { + mptSmp.Initialize(MOD_TYPE_MOD); + mptSmp.nLength = length; + mptSmp.nFineTune = MOD2XMFineTune(finetune); + mptSmp.nVolume = 4u * std::min(volume.get(), uint8(64)); + + SmpLength lStart = loopStart; + SmpLength lLength = loopLength * 2u; + + if(mptSmp.nLength) + { + mptSmp.nLoopStart = lStart; + mptSmp.nLoopEnd = lStart + lLength; + + if(mptSmp.nLoopStart >= mptSmp.nLength) + { + mptSmp.nLoopStart = mptSmp.nLength - 1; + } + if(mptSmp.nLoopEnd > mptSmp.nLength) + { + mptSmp.nLoopEnd = mptSmp.nLength; + } + if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4) + { + mptSmp.nLoopStart = 0; + mptSmp.nLoopEnd = 0; + } + + if(mptSmp.nLoopEnd > mptSmp.nLoopStart) + { + mptSmp.uFlags.set(CHN_LOOP); + } + } + } +}; + +MPT_BINARY_STRUCT(SFXSampleHeader, 30) + +static uint8 ClampSlideParam(uint8 value, uint8 lowNote, uint8 highNote) +{ + uint16 lowPeriod, highPeriod; + + if(lowNote < highNote && + lowNote >= 24 + NOTE_MIN && + highNote >= 24 + NOTE_MIN && + lowNote < std::size(ProTrackerPeriodTable) + 24 + NOTE_MIN && + highNote < std::size(ProTrackerPeriodTable) + 24 + NOTE_MIN) + { + lowPeriod = ProTrackerPeriodTable[lowNote - 24 - NOTE_MIN]; + highPeriod = ProTrackerPeriodTable[highNote - 24 - NOTE_MIN]; + + // with a fixed speed of 6 ticks/row, and excluding the first row, + // 1xx/2xx param has a max value of (low-high)/5 to avoid sliding too far + return std::min(value, static_cast<uint8>((lowPeriod - highPeriod) / 5)); + } + + return 0; +} + + +static bool ValidateHeader(const SFXFileHeader &fileHeader) +{ + if(fileHeader.numOrders > 128) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSFX(MemoryFileReader file, const uint64 *pfilesize) +{ + SAMPLEINDEX numSamples = 0; + if(numSamples == 0) + { + file.Rewind(); + if(!file.CanRead(0x40)) + { + return ProbeWantMoreData; + } + if(file.Seek(0x3c) && file.ReadMagic("SONG")) + { + numSamples = 15; + } + } + if(numSamples == 0) + { + file.Rewind(); + if(!file.CanRead(0x80)) + { + return ProbeWantMoreData; + } + if(file.Seek(0x7C) && file.ReadMagic("SO31")) + { + numSamples = 31; + } + } + if(numSamples == 0) + { + return ProbeFailure; + } + file.Rewind(); + for(SAMPLEINDEX smp = 0; smp < numSamples; smp++) + { + if(file.ReadUint32BE() > 131072) + { + return ProbeFailure; + } + } + file.Skip(4); + if(!file.CanRead(2)) + { + return ProbeWantMoreData; + } + uint16 speed = file.ReadUint16BE(); + if(speed < 178) + { + return ProbeFailure; + } + if(!file.CanRead(sizeof(SFXSampleHeader) * numSamples)) + { + return ProbeWantMoreData; + } + file.Skip(sizeof(SFXSampleHeader) * numSamples); + SFXFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadSFX(FileReader &file, ModLoadingFlags loadFlags) +{ + if(file.Seek(0x3C), file.ReadMagic("SONG")) + { + InitializeGlobals(MOD_TYPE_SFX); + m_nSamples = 15; + } else if(file.Seek(0x7C), file.ReadMagic("SO31")) + { + InitializeGlobals(MOD_TYPE_SFX); + m_nSamples = 31; + } else + { + return false; + } + + uint32 sampleLen[31]; + + file.Rewind(); + for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++) + { + sampleLen[smp] = file.ReadUint32BE(); + if(sampleLen[smp] > 131072) + return false; + } + + m_nChannels = 4; + m_nInstruments = 0; + m_nDefaultSpeed = 6; + m_nMinPeriod = 14 * 4; + m_nMaxPeriod = 3424 * 4; + m_nSamplePreAmp = 64; + + // Setup channel pan positions and volume + SetupMODPanning(true); + + file.Skip(4); + uint16 speed = file.ReadUint16BE(); + if(speed < 178) + return false; + m_nDefaultTempo = TEMPO((14565.0 * 122.0) / speed); + + file.Skip(14); + + uint32 invalidChars = 0; + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) + { + SFXSampleHeader sampleHeader; + + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[smp], sampleLen[smp - 1]); + + // Get rid of weird characters in sample names. + for(char &c : sampleHeader.name) + { + if(c > 0 && c < ' ') + { + c = ' '; + invalidChars++; + } + } + if(invalidChars >= 128) + return false; + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + } + + // Broken conversions of the "Operation Stealth" soundtrack (BOND23 / BOND32) + // There is a converter that shifts all note values except FFFD (empty note) to the left by 1 bit, + // but it should not do that for FFFE (STP) notes - as a consequence, they turn into pattern breaks (FFFC). + const bool fixPatternBreaks = (m_szNames[1] == "BASSE2.AMI") || (m_szNames[1] == "PRA1.AMI"); + + SFXFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + PATTERNINDEX numPatterns = 0; + for(ORDERINDEX ord = 0; ord < fileHeader.numOrders; ord++) + { + numPatterns = std::max(numPatterns, static_cast<PATTERNINDEX>(fileHeader.orderList[ord] + 1)); + } + + if(fileHeader.restartPos < fileHeader.numOrders) + Order().SetRestartPos(fileHeader.restartPos); + else + Order().SetRestartPos(0); + + ReadOrderFromArray(Order(), fileHeader.orderList, fileHeader.numOrders); + + // SFX v2 / MMS modules have 4 extra bytes here for some reason + if(m_nSamples == 31) + file.Skip(4); + + uint8 lastNote[4] = {0}; + uint8 slideTo[4] = {0}; + uint8 slideRate[4] = {0}; + uint8 version = 0; + + // Reading patterns + if(loadFlags & loadPatternData) + Patterns.ResizeArray(numPatterns); + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + file.Skip(64 * 4 * 4); + continue; + } + + for(ROWINDEX row = 0; row < 64; row++) + { + PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0); + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + ModCommand &m = rowBase[chn]; + auto data = file.ReadArray<uint8, 4>(); + + if(data[0] == 0xFF) + { + lastNote[chn] = slideRate[chn] = 0; + + if(fixPatternBreaks && data[1] == 0xFC) + data[1] = 0xFE; + + switch(data[1]) + { + case 0xFE: // STP (note cut) + m.command = CMD_VOLUME; + continue; + case 0xFD: // PIC (null) + continue; + case 0xFC: // BRK (pattern break) + m.command = CMD_PATTERNBREAK; + version = 9; + continue; + } + } + + ReadMODPatternEntry(data, m); + if(m.note != NOTE_NONE) + { + lastNote[chn] = m.note; + slideRate[chn] = 0; + if(m.note < NOTE_MIDDLEC - 12) + { + version = std::max(version, uint8(8)); + } + } + + if(m.command || m.param) + { + switch(m.command) + { + case 0x1: // Arpeggio + m.command = CMD_ARPEGGIO; + break; + + case 0x2: // Portamento (like Ultimate Soundtracker) + if(m.param & 0xF0) + { + m.command = CMD_PORTAMENTODOWN; + m.param >>= 4; + } else if(m.param & 0xF) + { + m.command = CMD_PORTAMENTOUP; + m.param &= 0x0F; + } else + { + m.command = m.param = 0; + } + break; + + case 0x3: // Enable LED filter + // Give precedence to 7xy/8xy slides + if(slideRate[chn]) + { + m.command = m.param = 0; + break; + } + m.command = CMD_MODCMDEX; + m.param = 0; + break; + + case 0x4: // Disable LED filter + // Give precedence to 7xy/8xy slides + if(slideRate[chn]) + { + m.command = m.param = 0; + break; + } + m.command = CMD_MODCMDEX; + m.param = 1; + break; + + case 0x5: // Increase volume + if(m.instr) + { + m.command = CMD_VOLUME; + m.param = std::min(ModCommand::PARAM(0x3F), static_cast<ModCommand::PARAM>((Samples[m.instr].nVolume / 4u) + m.param)); + + // Give precedence to 7xy/8xy slides (and move this to the volume column) + if(slideRate[chn]) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + m.command = m.param = 0; + break; + } + } else + { + m.command = m.param = 0; + } + break; + + case 0x6: // Decrease volume + if(m.instr) + { + m.command = CMD_VOLUME; + if((Samples[m.instr].nVolume / 4u) >= m.param) + m.param = static_cast<ModCommand::PARAM>(Samples[m.instr].nVolume / 4u) - m.param; + else + m.param = 0; + + // Give precedence to 7xy/8xy slides (and move this to the volume column) + if(slideRate[chn]) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + m.command = m.param = 0; + break; + } + } else + { + m.command = m.param = 0; + } + break; + + case 0x7: // 7xy: Slide down x semitones at speed y + slideTo[chn] = lastNote[chn] - (m.param >> 4); + + m.command = CMD_PORTAMENTODOWN; + slideRate[chn] = m.param & 0xF; + m.param = ClampSlideParam(slideRate[chn], slideTo[chn], lastNote[chn]); + break; + + case 0x8: // 8xy: Slide up x semitones at speed y + slideTo[chn] = lastNote[chn] + (m.param >> 4); + + m.command = CMD_PORTAMENTOUP; + slideRate[chn] = m.param & 0xF; + m.param = ClampSlideParam(slideRate[chn], lastNote[chn], slideTo[chn]); + break; + + case 0x9: // 9xy: Auto slide + version = std::max(version, uint8(8)); + [[fallthrough]]; + default: + m.command = CMD_NONE; + break; + } + } + + // Continue 7xy/8xy slides if needed + if(m.command == CMD_NONE && slideRate[chn]) + { + if(slideTo[chn]) + { + m.note = lastNote[chn] = slideTo[chn]; + m.param = slideRate[chn]; + slideTo[chn] = 0; + } + m.command = CMD_TONEPORTAMENTO; + } + } + } + } + + // Reading samples + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) if(Samples[smp].nLength) + { + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + } + } + + m_modFormat.formatName = m_nSamples == 15 ? MPT_UFORMAT("SoundFX 1.{}")(version) : U_("SoundFX 2.0 / MultiMedia Sound"); + m_modFormat.type = m_nSamples == 15 ? UL_("sfx") : UL_("sfx2"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_stm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_stm.cpp new file mode 100644 index 00000000..0c040da8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_stm.cpp @@ -0,0 +1,618 @@ +/* + * Load_stm.cpp + * ------------ + * Purpose: STM (Scream Tracker 2) and STX (Scream Tracker Music Interface Kit - a mixture of STM and S3M) module loaders + * Notes : (currently none) + * 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 "S3MTools.h" + +OPENMPT_NAMESPACE_BEGIN + +// STM sample header struct +struct STMSampleHeader +{ + char filename[12]; // Can't have long comments - just filename comments :) + uint8le zero; + uint8le disk; // A blast from the past + uint16le offset; // 20-bit offset in file (lower 4 bits are zero) + uint16le length; // Sample length + uint16le loopStart; // Loop start point + uint16le loopEnd; // Loop end point + uint8le volume; // Volume + uint8le reserved2; + uint16le sampleRate; + uint8le reserved3[6]; + + // Convert an STM sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename); + + mptSmp.nC5Speed = sampleRate; + mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4; + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + + if(mptSmp.nLength < 2) mptSmp.nLength = 0; + + if(mptSmp.nLoopStart < mptSmp.nLength + && mptSmp.nLoopEnd > mptSmp.nLoopStart + && mptSmp.nLoopEnd != 0xFFFF) + { + mptSmp.uFlags = CHN_LOOP; + mptSmp.nLoopEnd = std::min(mptSmp.nLoopEnd, mptSmp.nLength); + } + } +}; + +MPT_BINARY_STRUCT(STMSampleHeader, 32) + + +// STM file header +struct STMFileHeader +{ + char songname[20]; + char trackerName[8]; // !Scream! for ST 2.xx + uint8 dosEof; // 0x1A + uint8 filetype; // 1=song, 2=module (only 2 is supported, of course) :) + uint8 verMajor; + uint8 verMinor; + uint8 initTempo; + uint8 numPatterns; + uint8 globalVolume; + uint8 reserved[13]; + + bool Validate() const + { + if(filetype != 2 + || (dosEof != 0x1A && dosEof != 2) // ST2 ignores this, ST3 doesn't. Broken versions of putup10.stm / putup11.stm have dosEof = 2. + || verMajor != 2 + || (verMinor != 0 && verMinor != 10 && verMinor != 20 && verMinor != 21) + || numPatterns > 64 + || (globalVolume > 64 && globalVolume != 0x58)) // 0x58 may be a placeholder value in earlier ST2 versions. + { + return false; + } + return ValidateTrackerName(trackerName); + } + + static bool ValidateTrackerName(const char (&trackerName)[8]) + { + // Tracker string can be anything really (ST2 and ST3 won't check it), + // but we do not want to generate too many false positives here, as + // STM already has very few magic bytes anyway. + // Magic bytes that have been found in the wild are !Scream!, BMOD2STM, WUZAMOD! and SWavePro. + for(uint8 c : trackerName) + { + if(c < 0x20 || c >= 0x7F) + return false; + } + return true; + } + + uint64 GetHeaderMinimumAdditionalSize() const + { + return 31 * sizeof(STMSampleHeader) + (verMinor == 0 ? 64 : 128) + numPatterns * 64 * 4; + } +}; + +MPT_BINARY_STRUCT(STMFileHeader, 48) + + +static bool ValidateSTMOrderList(ModSequence &order) +{ + for(auto &pat : order) + { + if(pat == 99 || pat == 255) // 99 is regular, sometimes a single 255 entry can be found too + pat = order.GetInvalidPatIndex(); + else if(pat > 63) + return false; + } + return true; +} + + +static void ConvertSTMCommand(ModCommand &m, const ROWINDEX row, const uint8 fileVerMinor, uint8 &newTempo, ORDERINDEX &breakPos, ROWINDEX &breakRow) +{ + static constexpr EffectCommand stmEffects[] = + { + CMD_NONE, CMD_SPEED, CMD_POSITIONJUMP, CMD_PATTERNBREAK, // .ABC + CMD_VOLUMESLIDE, CMD_PORTAMENTODOWN, CMD_PORTAMENTOUP, CMD_TONEPORTAMENTO, // DEFG + CMD_VIBRATO, CMD_TREMOR, CMD_ARPEGGIO, CMD_NONE, // HIJK + CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, // LMNO + // KLMNO can be entered in the editor but don't do anything + }; + + m.command = stmEffects[m.command & 0x0F]; + + switch(m.command) + { + case CMD_VOLUMESLIDE: + // Lower nibble always has precedence, and there are no fine slides. + if(m.param & 0x0F) + m.param &= 0x0F; + else + m.param &= 0xF0; + break; + + case CMD_PATTERNBREAK: + m.param = (m.param & 0xF0) * 10 + (m.param & 0x0F); + if(breakPos != ORDERINDEX_INVALID && m.param == 0) + { + // Merge Bxx + C00 into just Bxx + m.command = CMD_POSITIONJUMP; + m.param = static_cast<ModCommand::PARAM>(breakPos); + breakPos = ORDERINDEX_INVALID; + } + LimitMax(breakRow, row); + break; + + case CMD_POSITIONJUMP: + // This effect is also very weird. + // Bxx doesn't appear to cause an immediate break -- it merely + // sets the next order for when the pattern ends (either by + // playing it all the way through, or via Cxx effect) + breakPos = m.param; + breakRow = 63; + m.command = CMD_NONE; + break; + + case CMD_TREMOR: + // this actually does something with zero values, and has no + // effect memory. which makes SENSE for old-effects tremor, + // but ST3 went and screwed it all up by adding an effect + // memory and IT followed that, and those are much more popular + // than STM so we kind of have to live with this effect being + // broken... oh well. not a big loss. + break; + + case CMD_SPEED: + if(fileVerMinor < 21) + m.param = ((m.param / 10u) << 4u) + m.param % 10u; + + if(!m.param) + { + m.command = CMD_NONE; + break; + } + +#ifdef MODPLUG_TRACKER + // ST2 has a very weird tempo mode where the length of a tick depends both + // on the ticks per row and a scaling factor. Try to write the tempo into a separate command. + newTempo = m.param; + m.param >>= 4; +#else + MPT_UNUSED_VARIABLE(newTempo); +#endif // MODPLUG_TRACKER + break; + + default: + // Anything not listed above is a no-op if there's no value, as ST2 doesn't have effect memory. + if(!m.param) + m.command = CMD_NONE; + break; + } +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSTM(MemoryFileReader file, const uint64 *pfilesize) +{ + STMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return ProbeWantMoreData; + if(!fileHeader.Validate()) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, fileHeader.GetHeaderMinimumAdditionalSize()); +} + + +bool CSoundFile::ReadSTM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + STMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return false; + if(!fileHeader.Validate()) + return false; + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(fileHeader.GetHeaderMinimumAdditionalSize()))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_STM); + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songname); + + m_modFormat.formatName = U_("Scream Tracker 2"); + m_modFormat.type = U_("stm"); + m_modFormat.madeWithTracker = MPT_UFORMAT("Scream Tracker {}.{}")(fileHeader.verMajor, mpt::ufmt::dec0<2>(fileHeader.verMinor)); + m_modFormat.charset = mpt::Charset::CP437; + + m_playBehaviour.set(kST3SampleSwap); + + m_nSamples = 31; + m_nChannels = 4; + m_nMinPeriod = 64; + m_nMaxPeriod = 0x7FFF; + + m_playBehaviour.set(kST3SampleSwap); + + uint8 initTempo = fileHeader.initTempo; + if(fileHeader.verMinor < 21) + initTempo = ((initTempo / 10u) << 4u) + initTempo % 10u; + if(initTempo == 0) + initTempo = 0x60; + + m_nDefaultTempo = ConvertST2Tempo(initTempo); + m_nDefaultSpeed = initTempo >> 4; + if(fileHeader.verMinor > 10) + m_nDefaultGlobalVolume = std::min(fileHeader.globalVolume, uint8(64)) * 4u; + + // Setting up channels + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = (chn & 1) ? 0x40 : 0xC0; + } + + // Read samples + uint16 sampleOffsets[31]; + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + STMSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + if(sampleHeader.zero != 0 && sampleHeader.zero != 46) // putup10.stm has zero = 46 + return false; + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename); + sampleOffsets[smp - 1] = sampleHeader.offset; + } + + // Read order list + ReadOrderFromFile<uint8>(Order(), file, fileHeader.verMinor == 0 ? 64 : 128); + if(!ValidateSTMOrderList(Order())) + return false; + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + { + for(int i = 0; i < 64 * 4; i++) + { + uint8 note = file.ReadUint8(); + if(note < 0xFB || note > 0xFD) + file.Skip(3); + } + continue; + } + + auto m = Patterns[pat].begin(); + ORDERINDEX breakPos = ORDERINDEX_INVALID; + ROWINDEX breakRow = 63; // Candidate row for inserting pattern break + + for(ROWINDEX row = 0; row < 64; row++) + { + uint8 newTempo = 0; + for(CHANNELINDEX chn = 0; chn < 4; chn++, m++) + { + uint8 note = file.ReadUint8(), insVol, volCmd, cmdInf; + switch(note) + { + case 0xFB: + note = insVol = volCmd = cmdInf = 0x00; + break; + case 0xFC: + continue; + case 0xFD: + m->note = NOTE_NOTECUT; + continue; + default: + { + const auto patData = file.ReadArray<uint8, 3>(); + insVol = patData[0]; + volCmd = patData[1]; + cmdInf = patData[2]; + } + break; + } + + if(note == 0xFE) + m->note = NOTE_NOTECUT; + else if(note < 0x60) + m->note = (note >> 4) * 12 + (note & 0x0F) + 36 + NOTE_MIN; + + m->instr = insVol >> 3; + if(m->instr > 31) + { + m->instr = 0; + } + + uint8 vol = (insVol & 0x07) | ((volCmd & 0xF0) >> 1); + if(vol <= 64) + { + m->volcmd = VOLCMD_VOLUME; + m->vol = vol; + } + + m->command = volCmd & 0x0F; + m->param = cmdInf; + ConvertSTMCommand(*m, row, fileHeader.verMinor, newTempo, breakPos, breakRow); + } + if(newTempo != 0) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_TEMPO, mpt::saturate_round<ModCommand::PARAM>(ConvertST2Tempo(newTempo).ToDouble())).Row(row).RetryPreviousRow()); + } + } + + if(breakPos != ORDERINDEX_INVALID) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(breakPos)).Row(breakRow).RetryPreviousRow()); + } + } + + // Reading Samples + if(loadFlags & loadSampleData) + { + const SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + + for(SAMPLEINDEX smp = 1; smp <= 31; smp++) + { + ModSample &sample = Samples[smp]; + // ST2 just plays random noise for samples with a default volume of 0 + if(sample.nLength && sample.nVolume > 0) + { + FileReader::off_t sampleOffset = sampleOffsets[smp - 1] << 4; + // acidlamb.stm has some bogus samples with sample offsets past EOF + if(sampleOffset > sizeof(STMFileHeader) && file.Seek(sampleOffset)) + { + sampleIO.ReadSample(sample, file); + } + } + } + } + + return true; +} + + +// STX file header +struct STXFileHeader +{ + char songName[20]; + char trackerName[8]; // Typically !Scream! but mustn't be relied upon, like for STM + uint16le patternSize; // or EOF in newer file version (except for future brain.stx?!) + uint16le unknown1; + uint16le patTableOffset; + uint16le smpTableOffset; + uint16le chnTableOffset; + uint32le unknown2; + uint8 globalVolume; + uint8 initTempo; + uint32le unknown3; + uint16le numPatterns; + uint16le numSamples; + uint16le numOrders; + char unknown4[6]; + char magic[4]; + + bool Validate() const + { + if(std::memcmp(magic, "SCRM", 4) + || (patternSize < 64 && patternSize != 0x1A) + || patternSize > 0x840 + || (globalVolume > 64 && globalVolume != 0x58) // 0x58 may be a placeholder value in earlier ST2 versions. + || numPatterns > 64 + || numSamples > 96 // Some STX files have more sample slots than their STM counterpart for mysterious reasons + || (numOrders > 0x81 && numOrders != 0x101) + || unknown1 != 0 || unknown2 != 0 || unknown3 != 1) + { + return false; + } + return STMFileHeader::ValidateTrackerName(trackerName); + } + + uint64 GetHeaderMinimumAdditionalSize() const + { + return std::max({(patTableOffset << 4) + numPatterns * 2, (smpTableOffset << 4) + numSamples * 2, (chnTableOffset << 4) + 32 + numOrders * 5 }); + } +}; + +MPT_BINARY_STRUCT(STXFileHeader, 64) + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSTX(MemoryFileReader file, const uint64 *pfilesize) +{ + STXFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return ProbeWantMoreData; + if(!fileHeader.Validate()) + return ProbeFailure; + return ProbeAdditionalSize(file, pfilesize, fileHeader.GetHeaderMinimumAdditionalSize()); +} + + +bool CSoundFile::ReadSTX(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + STXFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return false; + if(!fileHeader.Validate()) + return false; + if (!file.CanRead(mpt::saturate_cast<FileReader::off_t>(fileHeader.GetHeaderMinimumAdditionalSize()))) + return false; + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_STM); + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + + m_nSamples = fileHeader.numSamples; + m_nChannels = 4; + m_nMinPeriod = 64; + m_nMaxPeriod = 0x7FFF; + + m_playBehaviour.set(kST3SampleSwap); + + uint8 initTempo = fileHeader.initTempo; + if(initTempo == 0) + initTempo = 0x60; + + m_nDefaultTempo = ConvertST2Tempo(initTempo); + m_nDefaultSpeed = initTempo >> 4; + m_nDefaultGlobalVolume = std::min(fileHeader.globalVolume, uint8(64)) * 4u; + + // Setting up channels + for(CHANNELINDEX chn = 0; chn < 4; chn++) + { + ChnSettings[chn].Reset(); + ChnSettings[chn].nPan = (chn & 1) ? 0x40 : 0xC0; + } + + std::vector<uint16le> patternOffsets, sampleOffsets; + file.Seek(fileHeader.patTableOffset << 4); + file.ReadVector(patternOffsets, fileHeader.numPatterns); + file.Seek(fileHeader.smpTableOffset << 4); + file.ReadVector(sampleOffsets, fileHeader.numSamples); + + // Read order list + file.Seek((fileHeader.chnTableOffset << 4) + 32); + Order().resize(fileHeader.numOrders); + for(auto &pat : Order()) + { + pat = file.ReadUint8(); + file.Skip(4); + } + if(!ValidateSTMOrderList(Order())) + return false; + + // Read samples + for(SAMPLEINDEX smp = 1; smp <= fileHeader.numSamples; smp++) + { + if(!file.Seek(sampleOffsets[smp - 1] << 4)) + return false; + S3MSampleHeader sampleHeader; + file.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.filename); + const uint32 sampleOffset = sampleHeader.GetSampleOffset(); + if((loadFlags & loadSampleData) && sampleHeader.length != 0 && file.Seek(sampleOffset)) + { + sampleHeader.GetSampleFormat(true).ReadSample(Samples[smp], file); + } + } + + // Read patterns + uint8 formatVersion = 1; + if(!patternOffsets.empty() && fileHeader.patternSize != 0x1A) + { + if(!file.Seek(patternOffsets.front() << 4)) + return false; + // First two bytes describe pattern size, like in S3M + if(file.ReadUint16LE() == fileHeader.patternSize) + formatVersion = 0; + } + + if(loadFlags & loadPatternData) + Patterns.ResizeArray(fileHeader.numPatterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++) + { + if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64)) + break; + if(!file.Seek(patternOffsets[pat] << 4)) + return false; + if(formatVersion == 0 && file.ReadUint16LE() > 0x840) + return false; + + ORDERINDEX breakPos = ORDERINDEX_INVALID; + ROWINDEX breakRow = 63; // Candidate row for inserting pattern break + + auto rowBase = Patterns[pat].GetRow(0); + ROWINDEX row = 0; + uint8 newTempo = 0; + while(row < 64) + { + uint8 info = file.ReadUint8(); + + if(info == s3mEndOfRow) + { + // End of row + if(newTempo != 0) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_TEMPO, mpt::saturate_round<ModCommand::PARAM>(ConvertST2Tempo(newTempo).ToDouble())).Row(row).RetryPreviousRow()); + newTempo = 0; + } + + if(++row < 64) + { + rowBase = Patterns[pat].GetRow(row); + } + continue; + } + + CHANNELINDEX channel = (info & s3mChannelMask); + ModCommand dummy; + ModCommand &m = (channel < GetNumChannels()) ? rowBase[channel] : dummy; + + if(info & s3mNotePresent) + { + const auto [note, instr] = file.ReadArray<uint8, 2>(); + if(note < 0xF0) + m.note = static_cast<ModCommand::NOTE>(Clamp((note & 0x0F) + 12 * (note >> 4) + 36 + NOTE_MIN, NOTE_MIN, NOTE_MAX)); + else if(note == s3mNoteOff) + m.note = NOTE_NOTECUT; + else if(note == s3mNoteNone) + m.note = NOTE_NONE; + m.instr = instr; + } + + if(info & s3mVolumePresent) + { + uint8 volume = file.ReadUint8(); + m.volcmd = VOLCMD_VOLUME; + m.vol = std::min(volume, uint8(64)); + } + + if(info & s3mEffectPresent) + { + const auto [command, param] = file.ReadArray<uint8, 2>(); + m.command = command; + m.param = param; + ConvertSTMCommand(m, row, 0xFF, newTempo, breakPos, breakRow); + } + } + + if(breakPos != ORDERINDEX_INVALID) + { + Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(breakPos)).Row(breakRow).RetryPreviousRow()); + } + } + + m_modFormat.formatName = U_("Scream Tracker Music Interface Kit"); + m_modFormat.type = U_("stx"); + m_modFormat.charset = mpt::Charset::CP437; + m_modFormat.madeWithTracker = MPT_UFORMAT("STM2STX 1.{}")(formatVersion); + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_stp.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_stp.cpp new file mode 100644 index 00000000..61c3886e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_stp.cpp @@ -0,0 +1,892 @@ +/* + * Load_stp.cpp + * ------------ + * Purpose: STP (Soundtracker Pro II) module loader + * Notes : A few exotic effects aren't supported. + * Multiple sample loops are supported, but only the first 10 can be used as cue points + * (with 16xx and 18xx). + * Fractional speed values and combined auto effects are handled whenever possible, + * but some effects may be omitted (and there may be tempo accuracy issues). + * Authors: Devin Acker + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + * + * Wisdom from the Soundtracker Pro II manual: + * "To create shorter patterns, simply create shorter patterns." + */ + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +// File header +struct STPFileHeader +{ + char magic[4]; + uint16be version; + uint8be numOrders; + uint8be patternLength; + uint8be orderList[128]; + uint16be speed; + uint16be speedFrac; + uint16be timerCount; + uint16be flags; + uint32be reserved; + uint16be midiCount; // always 50 + uint8be midi[50]; + uint16be numSamples; + uint16be sampleStructSize; +}; + +MPT_BINARY_STRUCT(STPFileHeader, 204) + + +// Sample header (common part between all versions) +struct STPSampleHeader +{ + uint32be length; + uint8be volume; + uint8be reserved1; + uint32be loopStart; + uint32be loopLength; + uint16be defaultCommand; // Default command to put next to note when editing patterns; not relevant for playback + // The following 4 bytes are reserved in version 0 and 1. + uint16be defaultPeriod; + uint8be finetune; + uint8be reserved2; + + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.nLength = length; + mptSmp.nVolume = 4u * std::min(volume.get(), uint8(64)); + + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopStart + loopLength; + + if(mptSmp.nLoopStart >= mptSmp.nLength) + { + mptSmp.nLoopStart = mptSmp.nLength - 1; + } + if(mptSmp.nLoopEnd > mptSmp.nLength) + { + mptSmp.nLoopEnd = mptSmp.nLength; + } + + if(mptSmp.nLoopStart > mptSmp.nLoopEnd) + { + mptSmp.nLoopStart = 0; + mptSmp.nLoopEnd = 0; + } else if(mptSmp.nLoopEnd > mptSmp.nLoopStart) + { + mptSmp.uFlags.set(CHN_LOOP); + mptSmp.cues[0] = mptSmp.nLoopStart; + } + } +}; + +MPT_BINARY_STRUCT(STPSampleHeader, 20) + + +struct STPLoopInfo +{ + SmpLength loopStart; + SmpLength loopLength; + SAMPLEINDEX looped; + SAMPLEINDEX nonLooped; +}; + +typedef std::vector<STPLoopInfo> STPLoopList; + + +static TEMPO ConvertTempo(uint16 ciaSpeed) +{ + // 3546 is the resulting CIA timer value when using 4F7D (tempo 125 bpm) command in STProII + return TEMPO((125.0 * 3546.0) / ciaSpeed); +} + + +static void ConvertLoopSlice(ModSample &src, ModSample &dest, SmpLength start, SmpLength len, bool loop) +{ + if(!src.HasSampleData() + || start >= src.nLength + || src.nLength - start < len) + { + return; + } + + dest.FreeSample(); + dest = src; + dest.nLength = len; + dest.pData.pSample = nullptr; + + if(!dest.AllocateSample()) + { + return; + } + + // only preserve cue points if the target sample length is the same + if(len != src.nLength) + MemsetZero(dest.cues); + + std::memcpy(dest.sampleb(), src.sampleb() + start, len); + dest.uFlags.set(CHN_LOOP, loop); + if(loop) + { + dest.nLoopStart = 0; + dest.nLoopEnd = len; + } else + { + dest.nLoopStart = 0; + dest.nLoopEnd = 0; + } +} + +static void ConvertLoopSequence(ModSample &smp, STPLoopList &loopList) +{ + // This should only modify a sample if it has more than one loop + // (otherwise, it behaves like a normal sample loop) + if(!smp.HasSampleData() || loopList.size() < 2) return; + + ModSample newSmp = smp; + newSmp.nLength = 0; + newSmp.pData.pSample = nullptr; + + size_t numLoops = loopList.size(); + + // Get the total length of the sample after combining all looped sections + for(size_t i = 0; i < numLoops; i++) + { + STPLoopInfo &info = loopList[i]; + + // If adding this loop would cause the sample length to exceed maximum, + // then limit and bail out + if(info.loopStart >= smp.nLength + || smp.nLength - info.loopStart < info.loopLength + || newSmp.nLength > MAX_SAMPLE_LENGTH - info.loopLength) + { + numLoops = i; + break; + } + + newSmp.nLength += info.loopLength; + } + + if(!newSmp.AllocateSample()) + { + return; + } + + // start copying the looped sample data parts + SmpLength start = 0; + + for(size_t i = 0; i < numLoops; i++) + { + STPLoopInfo &info = loopList[i]; + + memcpy(newSmp.sampleb() + start, smp.sampleb() + info.loopStart, info.loopLength); + + // update loop info based on position in edited sample + info.loopStart = start; + if(i > 0 && i <= std::size(newSmp.cues)) + { + newSmp.cues[i - 1] = start; + } + start += info.loopLength; + } + + // replace old sample with new one + smp.FreeSample(); + smp = newSmp; + + smp.nLoopStart = 0; + smp.nLoopEnd = smp.nLength; + smp.uFlags.set(CHN_LOOP); +} + + +static bool ValidateHeader(const STPFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.magic, "STP3", 4) + || fileHeader.version > 2 + || fileHeader.numOrders > 128 + || fileHeader.numSamples >= MAX_SAMPLES + || fileHeader.timerCount == 0 + || fileHeader.midiCount != 50) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSTP(MemoryFileReader file, const uint64 *pfilesize) +{ + STPFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadSTP(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + STPFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_STP); + + m_modFormat.formatName = MPT_UFORMAT("Soundtracker Pro II v{}")(fileHeader.version); + m_modFormat.type = U_("stp"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + m_nChannels = 4; + m_nSamples = 0; + + m_nDefaultSpeed = fileHeader.speed; + m_nDefaultTempo = ConvertTempo(fileHeader.timerCount); + + m_nMinPeriod = 14 * 4; + m_nMaxPeriod = 3424 * 4; + + ReadOrderFromArray(Order(), fileHeader.orderList, fileHeader.numOrders); + + std::vector<STPLoopList> loopInfo; + // Non-looped versions of samples with loops (when needed) + std::vector<SAMPLEINDEX> nonLooped; + + // Load sample headers + SAMPLEINDEX samplesInFile = 0; + + for(SAMPLEINDEX smp = 0; smp < fileHeader.numSamples; smp++) + { + SAMPLEINDEX actualSmp = file.ReadUint16BE(); + if(actualSmp == 0 || actualSmp >= MAX_SAMPLES) + return false; + uint32 chunkSize = fileHeader.sampleStructSize; + if(fileHeader.version == 2) + chunkSize = file.ReadUint32BE() - 2; + FileReader chunk = file.ReadChunk(chunkSize); + + samplesInFile = m_nSamples = std::max(m_nSamples, actualSmp); + + ModSample &mptSmp = Samples[actualSmp]; + mptSmp.Initialize(MOD_TYPE_MOD); + + if(fileHeader.version < 2) + { + // Read path + chunk.ReadString<mpt::String::maybeNullTerminated>(mptSmp.filename, 31); + // Ignore flags, they are all not relevant for us + chunk.Skip(1); + // Read filename / sample text + chunk.ReadString<mpt::String::maybeNullTerminated>(m_szNames[actualSmp], 30); + } else + { + std::string str; + // Read path + chunk.ReadNullString(str, 257); + mptSmp.filename = str; + // Ignore flags, they are all not relevant for us + chunk.Skip(1); + // Read filename / sample text + chunk.ReadNullString(str, 31); + m_szNames[actualSmp] = str; + // Seek to even boundary + if(chunk.GetPosition() % 2u) + chunk.Skip(1); + } + + STPSampleHeader sampleHeader; + chunk.ReadStruct(sampleHeader); + sampleHeader.ConvertToMPT(mptSmp); + + if(fileHeader.version == 2) + { + mptSmp.nFineTune = static_cast<int8>(sampleHeader.finetune << 3); + } + + if(fileHeader.version >= 1) + { + nonLooped.resize(samplesInFile); + loopInfo.resize(samplesInFile); + STPLoopList &loopList = loopInfo[actualSmp - 1]; + loopList.clear(); + + const uint16 numLoops = file.ReadUint16BE(); + if(!file.CanRead(numLoops * 8u)) + return false; + loopList.reserve(numLoops); + + STPLoopInfo loop; + loop.looped = loop.nonLooped = 0; + + if(numLoops == 0 && mptSmp.uFlags[CHN_LOOP]) + { + loop.loopStart = mptSmp.nLoopStart; + loop.loopLength = mptSmp.nLoopEnd - mptSmp.nLoopStart; + loopList.push_back(loop); + } else for(uint16 i = 0; i < numLoops; i++) + { + loop.loopStart = file.ReadUint32BE(); + loop.loopLength = file.ReadUint32BE(); + loopList.push_back(loop); + } + } + } + + // Load patterns + uint16 numPatterns = 128; + if(fileHeader.version == 0) + numPatterns = file.ReadUint16BE(); + + uint16 patternLength = fileHeader.patternLength; + CHANNELINDEX channels = 4; + if(fileHeader.version > 0) + { + // Scan for total number of channels + FileReader::off_t patOffset = file.GetPosition(); + for(uint16 pat = 0; pat < numPatterns; pat++) + { + PATTERNINDEX actualPat = file.ReadUint16BE(); + if(actualPat == 0xFFFF) + break; + + patternLength = file.ReadUint16BE(); + channels = file.ReadUint16BE(); + if(channels > MAX_BASECHANNELS) + return false; + m_nChannels = std::max(m_nChannels, channels); + + file.Skip(channels * patternLength * 4u); + } + file.Seek(patOffset); + } + + struct ChannelMemory + { + uint8 autoFinePorta, autoPortaUp, autoPortaDown, autoVolSlide, autoVibrato; + uint8 vibratoMem, autoTremolo, autoTonePorta, tonePortaMem; + }; + std::vector<ChannelMemory> channelMemory(m_nChannels); + uint8 globalVolSlide = 0; + uint8 speedFrac = static_cast<uint8>(fileHeader.speedFrac); + + for(uint16 pat = 0; pat < numPatterns; pat++) + { + PATTERNINDEX actualPat = pat; + + if(fileHeader.version > 0) + { + actualPat = file.ReadUint16BE(); + if(actualPat == 0xFFFF) + break; + + patternLength = file.ReadUint16BE(); + channels = file.ReadUint16BE(); + } + + if(!file.CanRead(channels * patternLength * 4u)) + break; + + if(!(loadFlags & loadPatternData) || !Patterns.Insert(actualPat, patternLength)) + { + file.Skip(channels * patternLength * 4u); + continue; + } + + for(ROWINDEX row = 0; row < patternLength; row++) + { + auto rowBase = Patterns[actualPat].GetRow(row); + + bool didGlobalVolSlide = false; + + // if a fractional speed value is in use then determine if we should stick a fine pattern delay somewhere + bool shouldDelay; + switch(speedFrac & 3) + { + default: shouldDelay = false; break; + // 1/4 + case 1: shouldDelay = (row & 3) == 0; break; + // 1/2 + case 2: shouldDelay = (row & 1) == 0; break; + // 3/4 + case 3: shouldDelay = (row & 3) != 3; break; + } + + for(CHANNELINDEX chn = 0; chn < channels; chn++) + { + ChannelMemory &chnMem = channelMemory[chn]; + ModCommand &m = rowBase[chn]; + const auto [instr, note, command, param] = file.ReadArray<uint8, 4>(); + + m.instr = instr; + m.note = note; + m.param = param; + + if(m.note) + { + m.note += 24 + NOTE_MIN; + chnMem = ChannelMemory(); + } + + // this is a nibble-swapped param value used for auto fine volside + // and auto global fine volside + uint8 swapped = (m.param >> 4) | (m.param << 4); + + if((command & 0xF0) == 0xF0) + { + // 12-bit CIA tempo + uint16 ciaTempo = (static_cast<uint16>(command & 0x0F) << 8) | m.param; + if(ciaTempo) + { + m.param = mpt::saturate_round<ModCommand::PARAM>(ConvertTempo(ciaTempo).ToDouble()); + m.command = CMD_TEMPO; + } else + { + m.command = CMD_NONE; + } + } else switch(command) + { + case 0x00: // arpeggio + if(m.param) + m.command = CMD_ARPEGGIO; + else + m.command = CMD_NONE; + break; + + case 0x01: // portamento up + m.command = CMD_PORTAMENTOUP; + break; + + case 0x02: // portamento down + m.command = CMD_PORTAMENTODOWN; + break; + + case 0x03: // auto fine portamento up + chnMem.autoFinePorta = 0x10 | std::min(m.param, ModCommand::PARAM(15)); + chnMem.autoPortaUp = 0; + chnMem.autoPortaDown = 0; + chnMem.autoTonePorta = 0; + + m.command = CMD_NONE; + break; + + case 0x04: // auto fine portamento down + chnMem.autoFinePorta = 0x20 | std::min(m.param, ModCommand::PARAM(15)); + chnMem.autoPortaUp = 0; + chnMem.autoPortaDown = 0; + chnMem.autoTonePorta = 0; + + m.command = CMD_NONE; + break; + + case 0x05: // auto portamento up + chnMem.autoFinePorta = 0; + chnMem.autoPortaUp = m.param; + chnMem.autoPortaDown = 0; + chnMem.autoTonePorta = 0; + + m.command = CMD_NONE; + break; + + case 0x06: // auto portamento down + chnMem.autoFinePorta = 0; + chnMem.autoPortaUp = 0; + chnMem.autoPortaDown = m.param; + chnMem.autoTonePorta = 0; + + m.command = CMD_NONE; + break; + + case 0x07: // set global volume + m.command = CMD_GLOBALVOLUME; + globalVolSlide = 0; + break; + + case 0x08: // auto global fine volume slide + globalVolSlide = swapped; + m.command = CMD_NONE; + break; + + case 0x09: // fine portamento up + m.command = CMD_MODCMDEX; + m.param = 0x10 | std::min(m.param, ModCommand::PARAM(15)); + break; + + case 0x0A: // fine portamento down + m.command = CMD_MODCMDEX; + m.param = 0x20 | std::min(m.param, ModCommand::PARAM(15)); + break; + + case 0x0B: // auto fine volume slide + chnMem.autoVolSlide = swapped; + m.command = CMD_NONE; + break; + + case 0x0C: // set volume + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + chnMem.autoVolSlide = 0; + m.command = CMD_NONE; + break; + + case 0x0D: // volume slide (param is swapped compared to .mod) + if(m.param & 0xF0) + { + m.volcmd = VOLCMD_VOLSLIDEDOWN; + m.vol = m.param >> 4; + } else if(m.param & 0x0F) + { + m.volcmd = VOLCMD_VOLSLIDEUP; + m.vol = m.param & 0xF; + } + chnMem.autoVolSlide = 0; + m.command = CMD_NONE; + break; + + case 0x0E: // set filter (also uses opposite value compared to .mod) + m.command = CMD_MODCMDEX; + m.param = 1 ^ (m.param ? 1 : 0); + break; + + case 0x0F: // set speed + m.command = CMD_SPEED; + speedFrac = m.param & 0x0F; + m.param >>= 4; + break; + + case 0x10: // auto vibrato + chnMem.autoVibrato = m.param; + chnMem.vibratoMem = 0; + m.command = CMD_NONE; + break; + + case 0x11: // auto tremolo + if(m.param & 0xF) + chnMem.autoTremolo = m.param; + else + chnMem.autoTremolo = 0; + m.command = CMD_NONE; + break; + + case 0x12: // pattern break + m.command = CMD_PATTERNBREAK; + break; + + case 0x13: // auto tone portamento + chnMem.autoFinePorta = 0; + chnMem.autoPortaUp = 0; + chnMem.autoPortaDown = 0; + chnMem.autoTonePorta = m.param; + + chnMem.tonePortaMem = 0; + m.command = CMD_NONE; + break; + + case 0x14: // position jump + m.command = CMD_POSITIONJUMP; + break; + + case 0x16: // start loop sequence + if(m.instr && m.instr <= loopInfo.size()) + { + STPLoopList &loopList = loopInfo[m.instr - 1]; + + m.param--; + if(m.param < std::min(std::size(ModSample().cues), loopList.size())) + { + m.volcmd = VOLCMD_OFFSET; + m.vol = m.param; + } + } + + m.command = CMD_NONE; + break; + + case 0x17: // play only loop nn + if(m.instr && m.instr <= loopInfo.size()) + { + STPLoopList &loopList = loopInfo[m.instr - 1]; + + m.param--; + if(m.param < loopList.size()) + { + if(!loopList[m.param].looped && CanAddMoreSamples()) + loopList[m.param].looped = ++m_nSamples; + m.instr = static_cast<ModCommand::INSTR>(loopList[m.param].looped); + } + } + + m.command = CMD_NONE; + break; + + case 0x18: // play sequence without loop + if(m.instr && m.instr <= loopInfo.size()) + { + STPLoopList &loopList = loopInfo[m.instr - 1]; + + m.param--; + if(m.param < std::min(std::size(ModSample().cues), loopList.size())) + { + m.volcmd = VOLCMD_OFFSET; + m.vol = m.param; + } + // switch to non-looped version of sample and create it if needed + if(!nonLooped[m.instr - 1] && CanAddMoreSamples()) + nonLooped[m.instr - 1] = ++m_nSamples; + m.instr = static_cast<ModCommand::INSTR>(nonLooped[m.instr - 1]); + } + + m.command = CMD_NONE; + break; + + case 0x19: // play only loop nn without loop + if(m.instr && m.instr <= loopInfo.size()) + { + STPLoopList &loopList = loopInfo[m.instr - 1]; + + m.param--; + if(m.param < loopList.size()) + { + if(!loopList[m.param].nonLooped && CanAddMoreSamples()) + loopList[m.param].nonLooped = ++m_nSamples; + m.instr = static_cast<ModCommand::INSTR>(loopList[m.param].nonLooped); + } + } + + m.command = CMD_NONE; + break; + + case 0x1D: // fine volume slide (nibble order also swapped) + m.command = CMD_VOLUMESLIDE; + m.param = swapped; + if(m.param & 0xF0) // slide down + m.param |= 0x0F; + else if(m.param & 0x0F) + m.param |= 0xF0; + break; + + case 0x20: // "delayed fade" + // just behave like either a normal fade or a notecut + // depending on the speed + if(m.param & 0xF0) + { + chnMem.autoVolSlide = m.param >> 4; + m.command = CMD_NONE; + } else + { + m.command = CMD_MODCMDEX; + m.param = 0xC0 | (m.param & 0xF); + } + break; + + case 0x21: // note delay + m.command = CMD_MODCMDEX; + m.param = 0xD0 | std::min(m.param, ModCommand::PARAM(15)); + break; + + case 0x22: // retrigger note + m.command = CMD_MODCMDEX; + m.param = 0x90 | std::min(m.param, ModCommand::PARAM(15)); + break; + + case 0x49: // set sample offset + m.command = CMD_OFFSET; + break; + + case 0x4E: // other protracker commands (pattern loop / delay) + if((m.param & 0xF0) == 0x60 || (m.param & 0xF0) == 0xE0) + m.command = CMD_MODCMDEX; + else + m.command = CMD_NONE; + break; + + case 0x4F: // set speed/tempo + if(m.param < 0x20) + { + m.command = CMD_SPEED; + speedFrac = 0; + } else + { + m.command = CMD_TEMPO; + } + break; + + default: + m.command = CMD_NONE; + break; + } + + bool didVolSlide = false; + + // try to put volume slide in volume command + if(chnMem.autoVolSlide && m.volcmd == VOLCMD_NONE) + { + if(chnMem.autoVolSlide & 0xF0) + { + m.volcmd = VOLCMD_FINEVOLUP; + m.vol = chnMem.autoVolSlide >> 4; + } else + { + m.volcmd = VOLCMD_FINEVOLDOWN; + m.vol = chnMem.autoVolSlide & 0xF; + } + didVolSlide = true; + } + + // try to place/combine all remaining running effects. + if(m.command == CMD_NONE) + { + if(chnMem.autoPortaUp) + { + m.command = CMD_PORTAMENTOUP; + m.param = chnMem.autoPortaUp; + + } else if(chnMem.autoPortaDown) + { + m.command = CMD_PORTAMENTODOWN; + m.param = chnMem.autoPortaDown; + } else if(chnMem.autoFinePorta) + { + m.command = CMD_MODCMDEX; + m.param = chnMem.autoFinePorta; + + } else if(chnMem.autoTonePorta) + { + m.command = CMD_TONEPORTAMENTO; + m.param = chnMem.tonePortaMem = chnMem.autoTonePorta; + + } else if(chnMem.autoVibrato) + { + m.command = CMD_VIBRATO; + m.param = chnMem.vibratoMem = chnMem.autoVibrato; + + } else if(!didVolSlide && chnMem.autoVolSlide) + { + m.command = CMD_VOLUMESLIDE; + m.param = chnMem.autoVolSlide; + // convert to a "fine" value by setting the other nibble to 0xF + if(m.param & 0x0F) + m.param |= 0xF0; + else if(m.param & 0xF0) + m.param |= 0x0F; + didVolSlide = true; + MPT_UNUSED(didVolSlide); + + } else if(chnMem.autoTremolo) + { + m.command = CMD_TREMOLO; + m.param = chnMem.autoTremolo; + + } else if(shouldDelay) + { + // insert a fine pattern delay here + m.command = CMD_S3MCMDEX; + m.param = 0x61; + shouldDelay = false; + + } else if(!didGlobalVolSlide && globalVolSlide) + { + m.command = CMD_GLOBALVOLSLIDE; + m.param = globalVolSlide; + // convert to a "fine" value by setting the other nibble to 0xF + if(m.param & 0x0F) + m.param |= 0xF0; + else if(m.param & 0xF0) + m.param |= 0x0F; + + didGlobalVolSlide = true; + } + } + } + + // TODO: create/use extra channels for global volslide/delay if needed + } + } + + // after we know how many channels there really are... + m_nSamplePreAmp = 256 / m_nChannels; + // Setup channel pan positions and volume + SetupMODPanning(true); + + // Skip over scripts and drumpad info + if(fileHeader.version > 0) + { + while(file.CanRead(2)) + { + uint16 scriptNum = file.ReadUint16BE(); + if(scriptNum == 0xFFFF) + break; + + file.Skip(2); + uint32 length = file.ReadUint32BE(); + file.Skip(length); + } + + // Skip drumpad stuff + file.Skip(17 * 2); + } + + // Reading samples + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= samplesInFile; smp++) if(Samples[smp].nLength) + { + SampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + + if(smp > loopInfo.size()) + continue; + + ConvertLoopSequence(Samples[smp], loopInfo[smp - 1]); + + // make a non-looping duplicate of this sample if needed + if(nonLooped[smp - 1]) + { + ConvertLoopSlice(Samples[smp], Samples[nonLooped[smp - 1]], 0, Samples[smp].nLength, false); + } + + for(const auto &info : loopInfo[smp - 1]) + { + // make duplicate samples for this individual section if needed + if(info.looped) + { + ConvertLoopSlice(Samples[smp], Samples[info.looped], info.loopStart, info.loopLength, true); + } + if(info.nonLooped) + { + ConvertLoopSlice(Samples[smp], Samples[info.nonLooped], info.loopStart, info.loopLength, false); + } + } + } + } + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_symmod.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_symmod.cpp new file mode 100644 index 00000000..25084acf --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_symmod.cpp @@ -0,0 +1,1947 @@ +/* + * Load_symmod.cpp + * --------------- + * Purpose: SymMOD (Symphonie / Symphonie Pro) module loader + * Notes : Based in part on Patrick Meng's Java-based Symphonie player and its source. + * Some effect behaviour and other things are based on the original Amiga assembly source. + * Symphonie is an interesting beast, with a surprising combination of features and lack thereof. + * It offers advanced DSPs (for its time) but has a fixed track tempo. It can handle stereo samples + * but free panning support was only added in one of the very last versions. Still, a good number + * of high-quality modules were made with it despite (or because of) its lack of features. + * Authors: Devin Acker + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "Loaders.h" +#include "Mixer.h" +#include "MixFuncTable.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleConvertFixedPoint.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "SampleCopy.h" +#ifdef MPT_EXTERNAL_SAMPLES +#include "../common/mptPathString.h" +#endif // MPT_EXTERNAL_SAMPLES +#include "mpt/base/numbers.hpp" + +#include <map> + +OPENMPT_NAMESPACE_BEGIN + +struct SymFileHeader +{ + char magic[4]; // "SymM" + uint32be version; + + bool Validate() const + { + return !std::memcmp(magic, "SymM", 4) && version == 1; + } +}; + +MPT_BINARY_STRUCT(SymFileHeader, 8) + + +struct SymEvent +{ + enum Command : uint8 + { + KeyOn = 0, + VolSlideUp, + VolSlideDown, + PitchSlideUp, + PitchSlideDown, + ReplayFrom, + FromAndPitch, + SetFromAdd, + FromAdd, + SetSpeed, + AddPitch, + AddVolume, + Tremolo, + Vibrato, + SampleVib, + PitchSlideTo, + Retrig, + Emphasis, + AddHalfTone, + CV, + CVAdd, + + Filter = 23, + DSPEcho, + DSPDelay, + }; + + enum Volume : uint8 + { + VolCommand = 200, + StopSample = 254, + ContSample = 253, + StartSample = 252, // unused + KeyOff = 251, + SpeedDown = 250, + SpeedUp = 249, + SetPitch = 248, + PitchUp = 247, + PitchDown = 246, + PitchUp2 = 245, + PitchDown2 = 244, + PitchUp3 = 243, + PitchDown3 = 242 + }; + + uint8be command; // See Command enum + int8be note; + uint8be param; // Volume if <= 100, see Volume enum otherwise + uint8be inst; + + bool IsGlobal() const + { + if(command == SymEvent::SetSpeed || command == SymEvent::DSPEcho || command == SymEvent::DSPDelay) + return true; + if(command == SymEvent::KeyOn && (param == SymEvent::SpeedUp || param == SymEvent::SpeedDown)) + return true; + return false; + } + + // used to compare DSP events for mapping them to MIDI macro numbers + bool operator<(const SymEvent &other) const + { + return std::tie(command, note, param, inst) < std::tie(other.command, other.note, other.param, other.inst); + } +}; + +MPT_BINARY_STRUCT(SymEvent, 4) + + +struct SymVirtualHeader +{ + char id[4]; // "ViRT" + uint8be zero; + uint8be filler1; + uint16be version; // 0 = regular, 1 = transwave + uint16be mixInfo; // unused, but not 0 in all modules + uint16be filler2; + uint16be eos; // 0 + + uint16be numEvents; + uint16be maxEvents; // always 20 + uint16be eventSize; // 4 for virtual instruments, 10 for transwave instruments (number of cycles, not used) + + bool IsValid() const + { + return !memcmp(id, "ViRT", 4) && zero == 0 && version <= 1 && eos == 0 && maxEvents == 20; + } + + bool IsVirtual() const + { + return IsValid() && version == 0 && numEvents <= 20 && eventSize == sizeof(SymEvent); + } + + bool IsTranswave() const + { + return IsValid() && version == 1 && numEvents == 2 && eventSize == 10; + } +}; + +MPT_BINARY_STRUCT(SymVirtualHeader, 20) + + +// Virtual instrument info +// This allows instruments to be created based on a mix of other instruments. +// The sample mixing is done at load time. +struct SymVirtualInst +{ + SymVirtualHeader header; + SymEvent noteEvents[20]; + char padding[28]; + + bool Render(CSoundFile &sndFile, const bool asQueue, ModSample &target, uint16 sampleBoost) const + { + if(header.numEvents < 1 || header.numEvents > std::size(noteEvents) || noteEvents[0].inst >= sndFile.GetNumSamples()) + return false; + + target.Initialize(MOD_TYPE_IT); + target.uFlags = CHN_16BIT; + + const auto events = mpt::as_span(noteEvents).subspan(0, header.numEvents); + const double rateFactor = 1.0 / std::max(sndFile.GetSample(events[0].inst + 1).nC5Speed, uint32(1)); + + for(const auto &event : events.subspan(0, asQueue ? events.size() : 1u)) + { + if(event.inst >= sndFile.GetNumSamples() || event.note < 0) + continue; + const ModSample &sourceSmp = sndFile.GetSample(event.inst + 1); + const double length = sourceSmp.nLength * std::pow(2.0, (event.note - events[0].note) / -12.0) * sourceSmp.nC5Speed * rateFactor; + target.nLength += mpt::saturate_round<SmpLength>(length); + } + if(!target.AllocateSample()) + return false; + + std::vector<ModChannel> channels(events.size()); + SmpLength lastSampleOffset = 0; + for(size_t ev = 0; ev < events.size(); ev++) + { + const SymEvent &event = events[ev]; + ModChannel &chn = channels[ev]; + + if(event.inst >= sndFile.GetNumSamples() || event.note < 0) + continue; + + int8 finetune = 0; + if(event.param >= SymEvent::PitchDown3 && event.param <= SymEvent::PitchUp) + { + static constexpr int8 PitchTable[] = {-4, 4, -2, 2, -1, 1}; + static_assert(mpt::array_size<decltype(PitchTable)>::size == SymEvent::PitchUp - SymEvent::PitchDown3 + 1); + finetune = PitchTable[event.param - SymEvent::PitchDown3]; + } + + const ModSample &sourceSmp = sndFile.GetSample(event.inst + 1); + const double increment = std::pow(2.0, (event.note - events[0].note) / 12.0 + finetune / 96.0) * sourceSmp.nC5Speed * rateFactor; + if(increment <= 0) + continue; + + chn.increment = SamplePosition::FromDouble(increment); + chn.pCurrentSample = sourceSmp.samplev(); + chn.nLength = sourceSmp.nLength; + chn.dwFlags = sourceSmp.uFlags & CHN_SAMPLEFLAGS; + if(asQueue) + { + // This determines when the queued sample will be played + chn.oldOffset = lastSampleOffset; + lastSampleOffset += mpt::saturate_round<SmpLength>(chn.nLength / chn.increment.ToDouble()); + } + int32 volume = 4096 * sampleBoost / 10000; // avoid clipping the filters if the virtual sample is later also filtered (see e.g. 303 emulator.symmod) + if(!asQueue) + volume /= header.numEvents; + chn.leftVol = chn.rightVol = volume; + } + + SmpLength writeOffset = 0; + while(writeOffset < target.nLength) + { + std::array<mixsample_t, MIXBUFFERSIZE * 2> buffer{}; + const SmpLength writeCount = std::min(static_cast<SmpLength>(MIXBUFFERSIZE), target.nLength - writeOffset); + + for(auto &chn : channels) + { + if(!chn.pCurrentSample) + continue; + // Should queued sample be played yet? + if(chn.oldOffset >= writeCount) + { + chn.oldOffset -= writeCount; + continue; + } + + uint32 functionNdx = MixFuncTable::ndxLinear; + if(chn.dwFlags[CHN_16BIT]) + functionNdx |= MixFuncTable::ndx16Bit; + if(chn.dwFlags[CHN_STEREO]) + functionNdx |= MixFuncTable::ndxStereo; + + const SmpLength procCount = std::min(writeCount - chn.oldOffset, mpt::saturate_round<SmpLength>((chn.nLength - chn.position.ToDouble()) / chn.increment.ToDouble())); + MixFuncTable::Functions[functionNdx](chn, sndFile.m_Resampler, buffer.data() + chn.oldOffset * 2, procCount); + chn.oldOffset = 0; + if(chn.position.GetUInt() >= chn.nLength) + chn.pCurrentSample = nullptr; + } + CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int16, mixsample_t, 27>, SC::DecodeIdentity<mixsample_t>>>(target.sample16() + writeOffset, writeCount, 1, buffer.data(), sizeof(buffer), 2); + writeOffset += writeCount; + } + + return true; + } +}; + +MPT_BINARY_STRUCT(SymVirtualInst, 128) + + +// Transwave instrument info +// Similar to virtual instruments, allows blending between two sample loops +struct SymTranswaveInst +{ + struct Transwave + { + uint16be sourceIns; + uint16be volume; // According to source label - but appears to be unused + uint32be loopStart; + uint32be loopLen; + uint32be padding; + + std::pair<SmpLength, SmpLength> ConvertLoop(const ModSample &mptSmp) const + { + const double loopScale = static_cast<double>(mptSmp.nLength) / (100 << 16); + const SmpLength start = mpt::saturate_cast<SmpLength>(loopScale * std::min(uint32(100 << 16), loopStart.get())); + const SmpLength length = mpt::saturate_cast<SmpLength>(loopScale * std::min(uint32(100 << 16), loopLen.get())); + return {start, std::min(mptSmp.nLength - start, length)}; + } + }; + + SymVirtualHeader header; + Transwave points[2]; + char padding[76]; + + // Morph between two sample loops + bool Render(const ModSample &smp1, const ModSample &smp2, ModSample &target) const + { + target.Initialize(MOD_TYPE_IT); + + const auto [loop1Start, loop1Len] = points[0].ConvertLoop(smp1); + const auto [loop2Start, loop2Len] = points[1].ConvertLoop(smp2); + + if(loop1Len < 1 || loop1Len > MAX_SAMPLE_LENGTH / (4u * 80u)) + return false; + + const SmpLength cycleLength = loop1Len * 4u; + const double cycleFactor1 = loop1Len / static_cast<double>(cycleLength); + const double cycleFactor2 = loop2Len / static_cast<double>(cycleLength); + + target.uFlags = CHN_16BIT; + target.nLength = cycleLength * 80u; + if(!target.AllocateSample()) + return false; + + const double ampFactor = 1.0 / target.nLength; + for(SmpLength i = 0; i < cycleLength; i++) + { + const double v1 = TranswaveInterpolate(smp1, loop1Start + i * cycleFactor1); + const double v2 = TranswaveInterpolate(smp2, loop2Start + i * cycleFactor2); + SmpLength writeOffset = i; + for(int cycle = 0; cycle < 80; cycle++, writeOffset += cycleLength) + { + const double amp = writeOffset * ampFactor; + target.sample16()[writeOffset] = mpt::saturate_round<int16>(v1 * (1.0 - amp) + v2 * amp); + } + } + + return true; + } + + static MPT_FORCEINLINE double TranswaveInterpolate(const ModSample &smp, double offset) + { + if(!smp.HasSampleData()) + return 0.0; + + SmpLength intOffset = static_cast<SmpLength>(offset); + const double fractOffset = offset - intOffset; + const uint8 numChannels = smp.GetNumChannels(); + intOffset *= numChannels; + + int16 v1, v2; + if(smp.uFlags[CHN_16BIT]) + { + v1 = smp.sample16()[intOffset]; + v2 = smp.sample16()[intOffset + numChannels]; + } else + { + v1 = smp.sample8()[intOffset] * 256; + v2 = smp.sample8()[intOffset + numChannels] * 256; + } + return (v1 * (1.0 - fractOffset) + v2 * fractOffset); + } +}; + +MPT_BINARY_STRUCT(SymTranswaveInst, 128) + + +// Instrument definition +struct SymInstrument +{ + using SymInstrumentName = std::array<char, 128>; + + SymVirtualInst virt; // or SymInstrumentName, or SymTranswaveInst + + enum Type : int8 + { + Silent = -8, + Kill = -4, + Normal = 0, + Loop = 4, + Sustain = 8 + }; + + enum Channel : uint8 + { + Mono, + StereoL, + StereoR, + LineSrc // virtual mix instrument + }; + + enum SampleFlags : uint8 + { + PlayReverse = 1, // reverse sample + AsQueue = 2, // "queue" virtual instrument (rendereds samples one after another rather than simultaneously) + MirrorX = 4, // invert sample phase + Is16Bit = 8, // not used, we already know the bit depth of the samples + NewLoopSystem = 16, // use fine loop start/len values + + MakeNewSample = (PlayReverse | MirrorX) + }; + + enum InstFlags : uint8 + { + NoTranspose = 1, // don't apply sequence/position transpose + NoDSP = 2, // don't apply DSP effects + SyncPlay = 4 // play a stereo instrument pair (or two copies of the same mono instrument) on consecutive channels + }; + + int8be type; // see Type enum + uint8be loopStartHigh; + uint8be loopLenHigh; + uint8be numRepetitions; // for "sustain" instruments + uint8be channel; // see Channel enum + uint8be dummy1; // called "automaximize" (normalize?) in Amiga source, but unused + uint8be volume; // 0-199 + uint8be dummy2[3]; // info about "parent/child" and sample format + int8be finetune; // -128..127 ~= 2 semitones + int8be transpose; + uint8be sampleFlags; // see SampleFlags enum + int8be filter; // negative: highpass, positive: lowpass + uint8be instFlags; // see InstFlags enum + uint8be downsample; // downsample factor; affects sample tuning + uint8be dummy3[2]; // resonance, "loadflags" (both unused) + uint8be info; // bit 0 should indicate that rangeStart/rangeLen are valid, but they appear to be unused + uint8be rangeStart; // ditto + uint8be rangeLen; // ditto + uint8be dummy4; + uint16be loopStartFine; + uint16be loopLenFine; + uint8be dummy5[6]; + + uint8be filterFlags; // bit 0 = enable, bit 1 = highpass + uint8be numFilterPoints; // # of filter envelope points (up to 4, possibly only 1-2 ever actually used) + struct SymFilterSetting + { + uint8be cutoff; + uint8be resonance; + } filterPoint[4]; + + uint8be volFadeFlag; + uint8be volFadeFrom; + uint8be volFadeTo; + + uint8be padding[83]; + + bool IsVirtual() const + { + return virt.header.IsValid(); + } + + // Valid instrument either is virtual or has a name + bool IsEmpty() const + { + return virt.header.id[0] == 0 || type < 0; + } + + std::string GetName() const + { + return mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mpt::bit_cast<SymInstrumentName>(virt)); + } + + SymTranswaveInst GetTranswave() const + { + return mpt::bit_cast<SymTranswaveInst>(virt); + } + + void ConvertToMPT(ModInstrument &mptIns, ModSample &mptSmp, CSoundFile &sndFile) const + { + if(!IsVirtual()) + mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mpt::bit_cast<SymInstrumentName>(virt)); + + mptSmp.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_SUSTAINLOOP | CHN_PANNING); // Avoid these coming in from sample files + + const auto [loopStart, loopLen] = GetSampleLoop(mptSmp); + if(type == Loop && loopLen > 0) + { + mptSmp.uFlags.set(CHN_LOOP); + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopStart + loopLen; + } + + // volume (0-199, default 100) + // Symphonie actually compresses the sample data if the volume is above 100 (see end of function) + // We spread the volume between sample and instrument global volume if it's below 100 for the best possible resolution. + // This can be simplified if instrument volume ever gets adjusted to 0...128 range like in IT. + uint8 effectiveVolume = (volume > 0 && volume < 200) ? static_cast<uint8>(std::min(volume.get(), uint8(100)) * 128u / 100) : 128; + mptSmp.nGlobalVol = std::max(effectiveVolume, uint8(64)) / 2u; + mptIns.nGlobalVol = std::min(effectiveVolume, uint8(64)); + + // Tuning info (we'll let our own mixer take care of the downsampling instead of doing it at load time) + mptSmp.nC5Speed = 40460; + mptSmp.Transpose(-downsample + (transpose / 12.0) + (finetune / (128.0 * 12.0))); + + // DSP settings + mptIns.nMixPlug = (instFlags & NoDSP) ? 2 : 1; + if(instFlags & NoDSP) + { + // This is not 100% correct: An instrument playing after this one should pick up previous filter settings. + mptIns.SetCutoff(127, true); + mptIns.SetResonance(0, true); + } + + // Various sample processing follows + if(!mptSmp.HasSampleData()) + return; + + if(sampleFlags & PlayReverse) + ctrlSmp::ReverseSample(mptSmp, 0, 0, sndFile); + if(sampleFlags & MirrorX) + ctrlSmp::InvertSample(mptSmp, 0, 0, sndFile); + + // Always use 16-bit data to help with heavily filtered 8-bit samples (like in Future_Dream.SymMOD) + const bool doVolFade = (volFadeFlag == 2) && (volFadeFrom <= 100) && (volFadeTo <= 100); + if(!mptSmp.uFlags[CHN_16BIT] && (filterFlags || doVolFade || filter)) + { + int16 *newSample = static_cast<int16 *>(ModSample::AllocateSample(mptSmp.nLength, 2 * mptSmp.GetNumChannels())); + if(!newSample) + return; + CopySample<SC::ConversionChain<SC::Convert<int16, int8>, SC::DecodeIdentity<int8>>>(newSample, mptSmp.nLength * mptSmp.GetNumChannels(), 1, mptSmp.sample8(), mptSmp.GetSampleSizeInBytes(), 1); + mptSmp.uFlags.set(CHN_16BIT); + ctrlSmp::ReplaceSample(mptSmp, newSample, mptSmp.nLength, sndFile); + } + + // Highpass + if(filter < 0) + { + auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()); + for(int i = 0; i < -filter; i++) + { + int32 mix = sampleData[0]; + for(auto &sample : sampleData) + { + mix = mpt::rshift_signed(sample - mpt::rshift_signed(mix, 1), 1); + sample = static_cast<int16>(mix); + } + } + } + + // Volume Fade + if(doVolFade) + { + auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()); + int32 amp = volFadeFrom << 24, inc = Util::muldivr(volFadeTo - volFadeFrom, 1 << 24, static_cast<SmpLength>(sampleData.size())); + for(auto &sample : sampleData) + { + sample = static_cast<int16>(Util::muldivr(sample, amp, 100 << 24)); + amp += inc; + } + } + + // Resonant Filter Sweep + if(filterFlags != 0) + { + auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()); + int32 cutoff = filterPoint[0].cutoff << 23, resonance = filterPoint[0].resonance << 23; + const int32 cutoffStep = numFilterPoints > 1 ? Util::muldivr(filterPoint[1].cutoff - filterPoint[0].cutoff, 1 << 23, static_cast<SmpLength>(sampleData.size())) : 0; + const int32 resoStep = numFilterPoints > 1 ? Util::muldivr(filterPoint[1].resonance - filterPoint[0].resonance, 1 << 23, static_cast<SmpLength>(sampleData.size())) : 0; + const uint8 highpass = filterFlags & 2; + + int32 filterState[3]{}; + for(auto &sample : sampleData) + { + const int32 currentCutoff = cutoff / (1 << 23), currentReso = resonance / (1 << 23); + cutoff += cutoffStep; + resonance += resoStep; + + filterState[2] = mpt::rshift_signed(sample, 1) - filterState[0]; + filterState[1] += mpt::rshift_signed(currentCutoff * filterState[2], 8); + filterState[0] += mpt::rshift_signed(currentCutoff * filterState[1], 6); + filterState[0] += mpt::rshift_signed(currentReso * filterState[0], 6); + filterState[0] = mpt::rshift_signed(filterState[0], 2); + sample = mpt::saturate_cast<int16>(filterState[highpass]); + } + } + + // Lowpass + if(filter > 0) + { + auto sampleData = mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()); + for(int i = 0; i < filter; i++) + { + int32 mix = sampleData[0]; + for(auto &sample : sampleData) + { + mix = (sample + sample + mix) / 3; + sample = static_cast<int16>(mix); + } + } + } + + // Symphonie normalizes samples at load time (it normalizes them to the sample boost value - but we will use the full 16-bit range) + // Indeed, the left and right channel instruments are normalized separately. + const auto Normalize = [](auto sampleData) + { + const auto scale = Util::MaxValueOfType(sampleData[0]); + const auto [minElem, maxElem] = std::minmax_element(sampleData.begin(), sampleData.end()); + const int max = std::max(-*minElem, +*maxElem); + if(max >= scale || max == 0) + return; + + for(auto &v : sampleData) + { + v = static_cast<typename std::remove_reference<decltype(v)>::type>(static_cast<int>(v) * scale / max); + } + }; + if(mptSmp.uFlags[CHN_16BIT]) + Normalize(mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels())); + else + Normalize(mpt::as_span(mptSmp.sample8(), mptSmp.nLength * mptSmp.GetNumChannels())); + + // "Non-destructive" over-amplification with hard knee compression + if(volume > 100 && volume < 200) + { + const auto Amplify = [](auto sampleData, const uint8 gain) + { + const int32 knee = 16384 * (200 - gain) / 100, kneeInv = 32768 - knee; + constexpr int32 scale = 1 << (16 - (sizeof(sampleData[0]) * 8)); + for(auto &sample : sampleData) + { + int32 v = sample * scale; + if(v > knee) + v = (v - knee) * knee / kneeInv + kneeInv; + else if(v < -knee) + v = (v + knee) * knee / kneeInv - kneeInv; + else + v = v * kneeInv / knee; + sample = mpt::saturate_cast<typename std::remove_reference<decltype(sample)>::type>(v / scale); + } + }; + + const auto length = mptSmp.nLength * mptSmp.GetNumChannels(); + if(mptSmp.uFlags[CHN_16BIT]) + Amplify(mpt::span(mptSmp.sample16(), mptSmp.sample16() + length), volume); + else + Amplify(mpt::span(mptSmp.sample8(), mptSmp.sample8() + length), volume); + } + + // This must be applied last because some sample processors are time-dependent and Symphonie would be doing this during playback instead + mptSmp.RemoveAllCuePoints(); + if(type == Sustain && numRepetitions > 0 && loopLen > 0) + { + mptSmp.cues[0] = loopStart + loopLen * (numRepetitions + 1u); + mptSmp.nSustainStart = loopStart; // This is of purely informative value and not used for playback + mptSmp.nSustainEnd = loopStart + loopLen; + + if(MAX_SAMPLE_LENGTH / numRepetitions < loopLen) + return; + if(MAX_SAMPLE_LENGTH - numRepetitions * loopLen < mptSmp.nLength) + return; + + const uint8 bps = mptSmp.GetBytesPerSample(); + SmpLength loopEnd = loopStart + loopLen * (numRepetitions + 1); + SmpLength newLength = mptSmp.nLength + loopLen * numRepetitions; + std::byte *newSample = static_cast<std::byte *>(ModSample::AllocateSample(newLength, bps)); + if(!newSample) + return; + + mptSmp.nLength = newLength; + std::memcpy(newSample, mptSmp.sampleb(), (loopStart + loopLen) * bps); + for(uint8 i = 0; i < numRepetitions; i++) + { + std::memcpy(newSample + (loopStart + loopLen * (i + 1)) * bps, mptSmp.sampleb() + loopStart * bps, loopLen * bps); + } + std::memcpy(newSample + loopEnd * bps, mptSmp.sampleb() + (loopStart + loopLen) * bps, (newLength - loopEnd) * bps); + + ctrlSmp::ReplaceSample(mptSmp, newSample, mptSmp.nLength, sndFile); + } + } + + std::pair<SmpLength, SmpLength> GetSampleLoop(const ModSample &mptSmp) const + { + if(type != Loop && type != Sustain) + return {0, 0}; + + SmpLength loopStart = static_cast<SmpLength>(std::min(loopStartHigh.get(), uint8(100))); + SmpLength loopLen = static_cast<SmpLength>(std::min(loopLenHigh.get(), uint8(100))); + if(sampleFlags & NewLoopSystem) + { + loopStart = (loopStart << 16) + loopStartFine; + loopLen = (loopLen << 16) + loopLenFine; + + const double loopScale = static_cast<double>(mptSmp.nLength) / (100 << 16); + loopStart = mpt::saturate_cast<SmpLength>(loopStart * loopScale); + loopLen = std::min(mptSmp.nLength - loopStart, mpt::saturate_cast<SmpLength>(loopLen * loopScale)); + } else if(mptSmp.HasSampleData()) + { + // The order of operations here may seem weird as it reduces precision, but it's taken directly from the original assembly source (UpdateRecalcLoop) + loopStart = ((loopStart << 7) / 100u) * (mptSmp.nLength >> 7); + loopLen = std::min(mptSmp.nLength - loopStart, ((loopLen << 7) / 100u) * (mptSmp.nLength >> 7)); + + const auto FindLoopEnd = [](auto sampleData, const uint8 numChannels, SmpLength loopStart, SmpLength loopLen, const int threshold) + { + const auto valAtStart = sampleData.data()[loopStart * numChannels]; + auto *endPtr = sampleData.data() + (loopStart + loopLen) * numChannels; + while(loopLen) + { + if(std::abs(*endPtr - valAtStart) < threshold) + return loopLen; + endPtr -= numChannels; + loopLen--; + } + return loopLen; + }; + if(mptSmp.uFlags[CHN_16BIT]) + loopLen = FindLoopEnd(mpt::as_span(mptSmp.sample16(), mptSmp.nLength * mptSmp.GetNumChannels()), mptSmp.GetNumChannels(), loopStart, loopLen, 6 * 256); + else + loopLen = FindLoopEnd(mpt::as_span(mptSmp.sample8(), mptSmp.nLength * mptSmp.GetNumChannels()), mptSmp.GetNumChannels(), loopStart, loopLen, 6); + } + + return {loopStart, loopLen}; + } +}; + +MPT_BINARY_STRUCT(SymInstrument, 256) + + +struct SymSequence +{ + uint16be start; + uint16be length; + uint16be loop; + int16be info; + int16be transpose; + + uint8be padding[6]; +}; + +MPT_BINARY_STRUCT(SymSequence, 16) + + +struct SymPosition +{ + uint8be dummy[4]; + uint16be loopNum; + uint16be loopCount; // Only used during playback + uint16be pattern; + uint16be start; + uint16be length; + uint16be speed; + int16be transpose; + uint16be eventsPerLine; // Unused + + uint8be padding[12]; + + // Used to compare position entries for mapping them to OpenMPT patterns + bool operator<(const SymPosition &other) const + { + return std::tie(pattern, start, length, transpose, speed) < std::tie(other.pattern, other.start, other.length, other.transpose, other.speed); + } +}; + +MPT_BINARY_STRUCT(SymPosition, 32) + + +static std::vector<std::byte> DecodeSymChunk(FileReader &file) +{ + std::vector<std::byte> data; + const uint32 packedLength = file.ReadUint32BE(); + if(!file.CanRead(packedLength)) + { + file.Skip(file.BytesLeft()); + return data; + } + + FileReader chunk = file.ReadChunk(packedLength); + if(packedLength >= 10 && chunk.ReadMagic("PACK\xFF\xFF")) + { + // RLE-compressed chunk + uint32 unpackedLength = chunk.ReadUint32BE(); + // The best compression ratio can be achieved with type 1, where six bytes turn into up to 255*4 bytes, a ratio of 1:170. + uint32 maxLength = packedLength - 10; + if(Util::MaxValueOfType(maxLength) / 170 >= maxLength) + maxLength *= 170; + else + maxLength = Util::MaxValueOfType(maxLength); + LimitMax(unpackedLength, maxLength); + data.resize(unpackedLength); + + bool done = false; + uint32 offset = 0, remain = unpackedLength; + + while(!done && !chunk.EndOfFile()) + { + uint8 len; + std::array<std::byte, 4> dword; + + const int8 type = chunk.ReadInt8(); + switch(type) + { + case 0: + // Copy raw bytes + len = chunk.ReadUint8(); + if(remain >= len && chunk.CanRead(len)) + { + chunk.ReadRaw(mpt::as_span(data).subspan(offset, len)); + offset += len; + remain -= len; + } else + { + done = true; + } + break; + + case 1: + // Copy a dword multiple times + len = chunk.ReadUint8(); + if(remain >= (len * 4u) && chunk.ReadArray(dword)) + { + remain -= len * 4u; + while(len--) + { + std::copy(dword.begin(), dword.end(), data.begin() + offset); + offset += 4; + } + } else + { + done = true; + } + break; + + case 2: + // Copy a dword twice + if(remain >= 8 && chunk.ReadArray(dword)) + { + std::copy(dword.begin(), dword.end(), data.begin() + offset); + std::copy(dword.begin(), dword.end(), data.begin() + offset + 4); + offset += 8; + remain -= 8; + } else + { + done = true; + } + break; + + case 3: + // Zero bytes + len = chunk.ReadUint8(); + if(remain >= len) + { + // vector is already initialized to zero + offset += len; + remain -= len; + } else + { + done = true; + } + break; + + case -1: + done = true; + break; + + default: + // error + done = true; + break; + } + } + +#ifndef MPT_BUILD_FUZZER + // When using a fuzzer, we should not care if the decompressed buffer has the correct size. + // This makes finding new interesting test cases much easier. + if(remain) + std::vector<std::byte>{}.swap(data); +#endif + } else + { + // Uncompressed chunk + chunk.ReadVector(data, packedLength); + } + + return data; +} + + +template<typename T> +static std::vector<T> DecodeSymArray(FileReader &file) +{ + const auto data = DecodeSymChunk(file); + FileReader chunk(mpt::as_span(data)); + std::vector<T> retVal; + chunk.ReadVector(retVal, data.size() / sizeof(T)); + return retVal; +} + + +static bool ReadRawSymSample(ModSample &sample, FileReader &file) +{ + SampleIO sampleIO(SampleIO::_16bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM); + SmpLength nullBytes = 0; + sample.Initialize(); + + file.Rewind(); + if(file.ReadMagic("MAESTRO")) + { + file.Seek(12); + if(file.ReadUint32BE() == 0) + sampleIO |= SampleIO::stereoInterleaved; + file.Seek(24); + } else if(file.ReadMagic("16BT")) + { + file.Rewind(); + nullBytes = 4; // In Symphonie, the anti-click would take care of those... + } else + { + sampleIO |= SampleIO::_8bit; + } + + sample.nLength = mpt::saturate_cast<SmpLength>(file.BytesLeft() / (sampleIO.GetNumChannels() * sampleIO.GetBitDepth() / 8u)); + const bool ok = sampleIO.ReadSample(sample, file) > 0; + + if(ok && nullBytes) + std::memset(sample.samplev(), 0, std::min(nullBytes, sample.GetSampleSizeInBytes())); + + return ok; +} + + +static std::vector<std::byte> DecodeSample8(FileReader &file) +{ + auto data = DecodeSymChunk(file); + uint8 lastVal = 0; + for(auto &val : data) + { + lastVal += mpt::byte_cast<uint8>(val); + val = mpt::byte_cast<std::byte>(lastVal); + } + + return data; +} + + +static std::vector<std::byte> DecodeSample16(FileReader &file) +{ + auto data = DecodeSymChunk(file); + std::array<std::byte, 4096> buf; + constexpr size_t blockSize = buf.size() / 2; // Size of block in 16-bit samples + + for(size_t block = 0; block < data.size() / buf.size(); block++) + { + const size_t offset = block * sizeof(buf); + uint8 lastVal = 0; + + // Decode LSBs + for(size_t i = 0; i < blockSize; i++) + { + lastVal += mpt::byte_cast<uint8>(data[offset + i]); + buf[i * 2 + 1] = mpt::byte_cast<std::byte>(lastVal); + } + // Decode MSBs + for(size_t i = 0; i < blockSize; i++) + { + lastVal += mpt::byte_cast<uint8>(data[offset + i + blockSize]); + buf[i * 2] = mpt::byte_cast<std::byte>(lastVal); + } + + std::copy(buf.begin(), buf.end(), data.begin() + offset); + } + + return data; +} + + +static bool ConvertDSP(const SymEvent event, MIDIMacroConfigData::Macro ¯o, const CSoundFile &sndFile) +{ + if(event.command == SymEvent::Filter) + { + // Symphonie practically uses the same filter for this as for the sample processing. + // The cutoff and resonance are an approximation. + const uint8 type = event.note % 5u; + const uint8 cutoff = sndFile.FrequencyToCutOff(event.param * 10000.0 / 240.0); + const uint8 reso = static_cast<uint8>(std::min(127, event.inst * 127 / 185)); + + if(type == 1) // lowpass filter + macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00200")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso)); + else if(type == 2) // highpass filter + macro = MPT_AFORMAT("F0F000{} F0F001{} F0F00210")(mpt::afmt::HEX0<2>(cutoff), mpt::afmt::HEX0<2>(reso)); + else // no filter or unsupported filter type + macro = "F0F0007F F0F00100"; + return true; + } else if(event.command == SymEvent::DSPEcho) + { + const uint8 type = (event.note < 5) ? event.note : 0; + const uint8 length = (event.param < 128) ? event.param : 127; + const uint8 feedback = (event.inst < 128) ? event.inst : 127; + macro = MPT_AFORMAT("F0F080{} F0F081{} F0F082{}")(mpt::afmt::HEX0<2>(type), mpt::afmt::HEX0<2>(length), mpt::afmt::HEX0<2>(feedback)); + return true; + } else if(event.command == SymEvent::DSPDelay) + { + // DSP first has to be turned on from the Symphonie GUI before it can be used in a track (unlike Echo), + // so it's not implemented for now. + return false; + } + return false; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSymMOD(MemoryFileReader file, const uint64 *pfilesize) +{ + MPT_UNREFERENCED_PARAMETER(pfilesize); + SymFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + return ProbeWantMoreData; + if(!fileHeader.Validate()) + return ProbeFailure; + if(!file.CanRead(sizeof(uint32be))) + return ProbeWantMoreData; + if(file.ReadInt32BE() >= 0) + return ProbeFailure; + return ProbeSuccess; +} + + +bool CSoundFile::ReadSymMOD(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + SymFileHeader fileHeader; + if(!file.ReadStruct(fileHeader) || !fileHeader.Validate()) + return false; + if(file.ReadInt32BE() >= 0) + return false; + else if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_MPT); + + m_SongFlags.set(SONG_LINEARSLIDES | SONG_EXFILTERRANGE | SONG_IMPORTED); + m_playBehaviour = GetDefaultPlaybackBehaviour(MOD_TYPE_IT); + m_playBehaviour.reset(kITShortSampleRetrig); + + enum class ChunkType : int32 + { + NumChannels = -1, + TrackLength = -2, + PatternSize = -3, + NumInstruments = -4, + EventSize = -5, + Tempo = -6, + ExternalSamples = -7, + PositionList = -10, + SampleFile = -11, + EmptySample = -12, + PatternEvents = -13, + InstrumentList = -14, + Sequences = -15, + InfoText = -16, + SamplePacked = -17, + SamplePacked16 = -18, + InfoType = -19, + InfoBinary = -20, + InfoString = -21, + + SampleBoost = 10, // All samples will be normalized to this value + StereoDetune = 11, // Note: Not affected by no-DSP flag in instrument! So this would need to have its own plugin... + StereoPhase = 12, + }; + + uint32 trackLen = 0; + uint16 sampleBoost = 2500; + bool isSymphoniePro = false; + bool externalSamples = false; + std::vector<SymPosition> positions; + std::vector<SymSequence> sequences; + std::vector<SymEvent> patternData; + std::vector<SymInstrument> instruments; + + file.SkipBack(sizeof(int32)); + while(file.CanRead(sizeof(int32))) + { + const ChunkType chunkType = static_cast<ChunkType>(file.ReadInt32BE()); + switch(chunkType) + { + // Simple values + case ChunkType::NumChannels: + if(auto numChannels = static_cast<CHANNELINDEX>(file.ReadUint32BE()); !m_nChannels && numChannels > 0 && numChannels <= MAX_BASECHANNELS) + { + m_nChannels = numChannels; + m_nSamplePreAmp = Clamp(512 / m_nChannels, 16, 128); + } + break; + + case ChunkType::TrackLength: + trackLen = file.ReadUint32BE(); + if(trackLen > 1024) + return false; + break; + + case ChunkType::EventSize: + if(auto eventSize = (file.ReadUint32BE() & 0xFFFF); eventSize != sizeof(SymEvent)) + return false; + break; + + case ChunkType::Tempo: + m_nDefaultTempo = TEMPO(1.24 * std::min(file.ReadUint32BE(), uint32(800))); + break; + + // Unused values + case ChunkType::NumInstruments: // determined from # of instrument headers instead + case ChunkType::PatternSize: + file.Skip(4); + break; + + case ChunkType::SampleBoost: + sampleBoost = static_cast<uint16>(Clamp(file.ReadUint32BE(), 0u, 10000u)); + isSymphoniePro = true; + break; + + case ChunkType::StereoDetune: + case ChunkType::StereoPhase: + isSymphoniePro = true; + if(uint32 val = file.ReadUint32BE(); val != 0) + AddToLog(LogWarning, U_("Stereo Detune / Stereo Phase is not supported")); + break; + + case ChunkType::ExternalSamples: + file.Skip(4); + if(!m_nSamples) + externalSamples = true; + break; + + // Binary chunk types + case ChunkType::PositionList: + if((loadFlags & loadPatternData) && positions.empty()) + positions = DecodeSymArray<SymPosition>(file); + else + file.Skip(file.ReadUint32BE()); + break; + + case ChunkType::SampleFile: + case ChunkType::SamplePacked: + case ChunkType::SamplePacked16: + if(m_nSamples >= instruments.size()) + break; + if(!externalSamples && (loadFlags & loadSampleData) && CanAddMoreSamples()) + { + const SAMPLEINDEX sample = ++m_nSamples; + + std::vector<std::byte> unpackedSample; + FileReader chunk; + if(chunkType == ChunkType::SampleFile) + { + chunk = file.ReadChunk(file.ReadUint32BE()); + } else if(chunkType == ChunkType::SamplePacked) + { + unpackedSample = DecodeSample8(file); + chunk = FileReader(mpt::as_span(unpackedSample)); + } else // SamplePacked16 + { + unpackedSample = DecodeSample16(file); + chunk = FileReader(mpt::as_span(unpackedSample)); + } + + if(!ReadIFFSample(sample, chunk) + && !ReadWAVSample(sample, chunk) + && !ReadAIFFSample(sample, chunk) + && !ReadRawSymSample(Samples[sample], chunk)) + { + AddToLog(LogWarning, U_("Unknown sample format.")); + } + + // Symphonie represents stereo instruments as two consecutive mono instruments which are + // automatically played at the same time. If this one uses a stereo sample, split it + // and map two OpenMPT instruments to the stereo halves to ensure correct playback + if(Samples[sample].uFlags[CHN_STEREO] && CanAddMoreSamples()) + { + const SAMPLEINDEX sampleL = ++m_nSamples; + ctrlSmp::SplitStereo(Samples[sample], Samples[sampleL], Samples[sample], *this); + Samples[sampleL].filename = "Left"; + Samples[sample].filename = "Right"; + } else if(sample < instruments.size() && instruments[sample].channel == SymInstrument::StereoR && CanAddMoreSamples()) + { + // Prevent misalignment of samples in exit.symmod (see condition in MoveNextMonoInstrument in Symphonie source) + m_nSamples++; + } + } else + { + // Skip sample + file.Skip(file.ReadUint32BE()); + } + break; + + case ChunkType::EmptySample: + if(CanAddMoreSamples()) + m_nSamples++; + break; + + case ChunkType::PatternEvents: + if((loadFlags & loadPatternData) && patternData.empty()) + patternData = DecodeSymArray<SymEvent>(file); + else + file.Skip(file.ReadUint32BE()); + break; + + case ChunkType::InstrumentList: + if(instruments.empty()) + instruments = DecodeSymArray<SymInstrument>(file); + else + file.Skip(file.ReadUint32BE()); + break; + + case ChunkType::Sequences: + if((loadFlags & loadPatternData) && sequences.empty()) + sequences = DecodeSymArray<SymSequence>(file); + else + file.Skip(file.ReadUint32BE()); + break; + + case ChunkType::InfoText: + if(const auto text = DecodeSymChunk(file); !text.empty()) + m_songMessage.Read(text.data(), text.size(), SongMessage::leLF); + break; + + // Unused binary chunks + case ChunkType::InfoType: + case ChunkType::InfoBinary: + case ChunkType::InfoString: + file.Skip(file.ReadUint32BE()); + break; + + // Unrecognized chunk/value type + default: + return false; + } + } + + if(!m_nChannels || !trackLen || instruments.empty()) + return false; + if((loadFlags & loadPatternData) && (positions.empty() || patternData.empty() || sequences.empty())) + return false; + + // Let's hope noone is going to use the 256th instrument ;) + if(instruments.size() >= MAX_INSTRUMENTS) + instruments.resize(MAX_INSTRUMENTS - 1u); + m_nInstruments = static_cast<INSTRUMENTINDEX>(instruments.size()); + static_assert(MAX_SAMPLES >= MAX_INSTRUMENTS); + m_nSamples = std::max(m_nSamples, m_nInstruments); + + // Supporting this is probably rather useless, as the paths will always be full Amiga paths. We just take the filename without path for now. + if(externalSamples) + { +#ifdef MPT_EXTERNAL_SAMPLES + m_nSamples = m_nInstruments; + for(SAMPLEINDEX sample = 1; sample <= m_nSamples; sample++) + { + const SymInstrument &symInst = instruments[sample - 1]; + if(symInst.IsEmpty() || symInst.IsVirtual()) + continue; + + auto filename = mpt::PathString::FromUnicode(mpt::ToUnicode(mpt::Charset::Amiga_no_C1, symInst.GetName())); + if(file.GetOptionalFileName()) + filename = file.GetOptionalFileName()->GetPath() + filename.GetFullFileName(); + + if(!LoadExternalSample(sample, filename)) + AddToLog(LogError, MPT_UFORMAT("Unable to load sample {}: {}")(sample, filename)); + else + ResetSamplePath(sample); + + if(Samples[sample].uFlags[CHN_STEREO] && sample < m_nSamples) + { + const SAMPLEINDEX sampleL = sample + 1; + ctrlSmp::SplitStereo(Samples[sample], Samples[sampleL], Samples[sample], *this); + Samples[sampleL].filename = "Left"; + Samples[sample].filename = "Right"; + sample++; + } + } +#else + AddToLog(LogWarning, U_("External samples are not supported.")); +#endif // MPT_EXTERNAL_SAMPLES + } + + // Convert instruments + for(int pass = 0; pass < 2; pass++) + { + for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) + { + SymInstrument &symInst = instruments[ins - 1]; + if(symInst.IsEmpty()) + continue; + // First load all regular instruments, and when we have the required information, render the virtual ones + if(symInst.IsVirtual() != (pass == 1)) + continue; + + SAMPLEINDEX sample = ins; + if(symInst.virt.header.IsVirtual()) + { + const uint8 firstSource = symInst.virt.noteEvents[0].inst; + ModSample &target = Samples[sample]; + if(symInst.virt.Render(*this, symInst.sampleFlags & SymInstrument::AsQueue, target, sampleBoost)) + { + m_szNames[sample] = "Virtual"; + if(firstSource < instruments.size()) + symInst.downsample += instruments[firstSource].downsample; + } else + { + sample = firstSource + 1; + } + } else if(symInst.virt.header.IsTranswave()) + { + const SymTranswaveInst transwaveInst = symInst.GetTranswave(); + const auto &trans1 = transwaveInst.points[0], &trans2 = transwaveInst.points[1]; + if(trans1.sourceIns < m_nSamples) + { + const ModSample emptySample; + const ModSample &smp1 = Samples[trans1.sourceIns + 1]; + const ModSample &smp2 = trans2.sourceIns < m_nSamples ? Samples[trans2.sourceIns + 1] : emptySample; + ModSample &target = Samples[sample]; + if(transwaveInst.Render(smp1, smp2, target)) + { + m_szNames[sample] = "Transwave"; + // Transwave instruments play an octave lower than the original source sample, but are 4x oversampled, + // so effectively they play an octave higher + symInst.transpose += 12; + } + } + } + + if(ModInstrument *instr = AllocateInstrument(ins, sample); instr != nullptr && sample <= m_nSamples) + symInst.ConvertToMPT(*instr, Samples[sample], *this); + } + } + + // Convert patterns + // map Symphonie positions to converted patterns + std::map<SymPosition, PATTERNINDEX> patternMap; + // map DSP commands to MIDI macro numbers + std::map<SymEvent, uint8> macroMap; + + bool useDSP = false; + const uint32 patternSize = m_nChannels * trackLen; + const PATTERNINDEX numPatterns = mpt::saturate_cast<PATTERNINDEX>(patternData.size() / patternSize); + + Patterns.ResizeArray(numPatterns); + Order().clear(); + + struct ChnState + { + float curVolSlide = 0; // Current volume slide factor of a channel + float curVolSlideAmt = 0; // Cumulative volume slide amount + float curPitchSlide = 0; // Current pitch slide factor of a channel + float curPitchSlideAmt = 0; // Cumulative pitch slide amount + bool stopped = false; // Sample paused or not (affects volume and pitch slides) + uint8 lastNote = 0; // Last note played on a channel + uint8 lastInst = 0; // Last instrument played on a channel + uint8 lastVol = 64; // Last specified volume of a channel (to avoid excessive Mxx commands) + uint8 channelVol = 100; // Volume multiplier, 0...100 + uint8 calculatedVol = 64; // Final channel volume + uint8 fromAdd = 0; // Base sample offset for FROM and FR&P effects + uint8 curVibrato = 0; + uint8 curTremolo = 0; + uint8 sampleVibSpeed = 0; + uint8 sampleVibDepth = 0; + uint8 tonePortaAmt = 0; + uint16 sampleVibPhase = 0; + uint16 retriggerRemain = 0; + uint16 tonePortaRemain = 0; + }; + std::vector<ChnState> chnStates(m_nChannels); + + // In Symphonie, sequences represent the structure of a song, and not separate songs like in OpenMPT. Hence they will all be loaded into the same ModSequence. + for(SymSequence &seq : sequences) + { + if(seq.info == 1) + continue; + if(seq.info == -1) + break; + + if(seq.start >= positions.size() + || seq.length > positions.size() + || seq.length == 0 + || positions.size() - seq.length < seq.start) + continue; + auto seqPositions = mpt::as_span(positions).subspan(seq.start, seq.length); + + // Sequences are all part of the same song, just add a skip index as a divider + ModSequence &order = Order(); + if(!order.empty()) + order.push_back(ModSequence::GetIgnoreIndex()); + + for(auto &pos : seqPositions) + { + // before checking the map, apply the sequence transpose value + pos.transpose += seq.transpose; + + // pattern already converted? + PATTERNINDEX patternIndex = 0; + if(patternMap.count(pos)) + { + patternIndex = patternMap[pos]; + } else if(loadFlags & loadPatternData) + { + // Convert pattern now + patternIndex = Patterns.InsertAny(pos.length); + if(patternIndex == PATTERNINDEX_INVALID) + break; + + patternMap[pos] = patternIndex; + + if(pos.pattern >= numPatterns || pos.start >= trackLen) + continue; + + uint8 patternSpeed = static_cast<uint8>(pos.speed); + + // This may intentionally read into the next pattern + auto srcEvent = patternData.cbegin() + (pos.pattern * patternSize) + (pos.start * m_nChannels); + const SymEvent emptyEvent{}; + ModCommand syncPlayCommand; + for(ROWINDEX row = 0; row < pos.length; row++) + { + ModCommand *rowBase = Patterns[patternIndex].GetpModCommand(row, 0); + bool applySyncPlay = false; + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ModCommand &m = rowBase[chn]; + const SymEvent &event = (srcEvent != patternData.cend()) ? *srcEvent : emptyEvent; + if(srcEvent != patternData.cend()) + srcEvent++; + + int8 note = (event.note >= 0 && event.note <= 84) ? event.note + 25 : -1; + uint8 origInst = event.inst; + uint8 mappedInst = 0; + if(origInst < instruments.size()) + { + mappedInst = static_cast<uint8>(origInst + 1); + if(!(instruments[origInst].instFlags & SymInstrument::NoTranspose) && note >= 0) + note = Clamp(static_cast<int8>(note + pos.transpose), NOTE_MIN, NOTE_MAX); + } + + // If we duplicated a stereo channel to this cell but the event is non-empty, remove it again. + if(m.note != NOTE_NONE && (event.command != SymEvent::KeyOn || event.note != -1 || event.inst != 0 || event.param != 0) + && m.instr > 0 && m.instr <= instruments.size() && instruments[m.instr - 1].channel == SymInstrument::StereoR) + { + m.Clear(); + } + + auto &chnState = chnStates[chn]; + + if(applySyncPlay) + { + applySyncPlay = false; + m = syncPlayCommand; + if(m.command == CMD_NONE && chnState.calculatedVol != chnStates[chn - 1].calculatedVol) + { + m.command = CMD_CHANNELVOLUME; + m.param = chnState.calculatedVol = chnStates[chn - 1].calculatedVol; + } + if(!event.IsGlobal()) + continue; + } + + bool applyVolume = false; + switch(static_cast<SymEvent::Command>(event.command.get())) + { + case SymEvent::KeyOn: + if(event.param > SymEvent::VolCommand) + { + switch(event.param) + { + case SymEvent::StopSample: + m.volcmd = VOLCMD_PLAYCONTROL; + m.vol = 0; + chnState.stopped = true; + break; + + case SymEvent::ContSample: + m.volcmd = VOLCMD_PLAYCONTROL; + m.vol = 1; + chnState.stopped = false; + break; + + case SymEvent::KeyOff: + if(m.note == NOTE_NONE) + m.note = chnState.lastNote; + m.volcmd = VOLCMD_OFFSET; + m.vol = 1; + break; + + case SymEvent::SpeedDown: + if(patternSpeed > 1) + { + m.command = CMD_SPEED; + m.param = --patternSpeed; + } + break; + + case SymEvent::SpeedUp: + if(patternSpeed < 0xFF) + { + m.command = CMD_SPEED; + m.param = ++patternSpeed; + } + break; + + case SymEvent::SetPitch: + chnState.lastNote = note; + if(mappedInst != chnState.lastInst) + break; + m.note = note; + m.command = CMD_TONEPORTAMENTO; + m.param = 0xFF; + chnState.curPitchSlide = 0; + chnState.tonePortaRemain = 0; + break; + + // fine portamentos with range up to half a semitone + case SymEvent::PitchUp: + m.command = CMD_PORTAMENTOUP; + m.param = 0xF2; + break; + case SymEvent::PitchDown: + m.command = CMD_PORTAMENTODOWN; + m.param = 0xF2; + break; + case SymEvent::PitchUp2: + m.command = CMD_PORTAMENTOUP; + m.param = 0xF4; + break; + case SymEvent::PitchDown2: + m.command = CMD_PORTAMENTODOWN; + m.param = 0xF4; + break; + case SymEvent::PitchUp3: + m.command = CMD_PORTAMENTOUP; + m.param = 0xF8; + break; + case SymEvent::PitchDown3: + m.command = CMD_PORTAMENTODOWN; + m.param = 0xF8; + break; + } + } else + { + if(event.note >= 0 || event.param < 100) + { + if(event.note >= 0) + { + m.note = chnState.lastNote = note; + m.instr = chnState.lastInst = mappedInst; + chnState.curPitchSlide = 0; + chnState.tonePortaRemain = 0; + } + + if(event.param > 0) + { + chnState.lastVol = mpt::saturate_round<uint8>(event.param * 0.64); + if(chnState.curVolSlide != 0) + applyVolume = true; + chnState.curVolSlide = 0; + } + } + } + + if(const uint8 newVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100)); + applyVolume || chnState.calculatedVol != newVol) + { + chnState.calculatedVol = newVol; + m.command = CMD_CHANNELVOLUME; + m.param = newVol; + } + + // Key-On commands with stereo instruments are played on both channels - unless there's already some sort of event + if(event.note > 0 && (chn < m_nChannels - 1) && !(chn % 2u) + && origInst < instruments.size() && instruments[origInst].channel == SymInstrument::StereoL) + { + ModCommand &next = rowBase[chn + 1]; + next = m; + next.instr++; + + chnStates[chn + 1].lastVol = chnState.lastVol; + chnStates[chn + 1].curVolSlide = chnState.curVolSlide; + chnStates[chn + 1].curVolSlideAmt = chnState.curVolSlideAmt; + chnStates[chn + 1].curPitchSlide = chnState.curPitchSlide; + chnStates[chn + 1].curPitchSlideAmt = chnState.curPitchSlideAmt; + chnStates[chn + 1].retriggerRemain = chnState.retriggerRemain; + } + + break; + + // volume effects + // Symphonie has very fine fractional volume slides which are applied at the output sample rate, + // rather than per tick or per row, so instead let's simulate it based on the pattern speed + // by keeping track of the volume and using normal volume commands + // the math here is an approximation which works fine for most songs + case SymEvent::VolSlideUp: + chnState.curVolSlideAmt = 0; + chnState.curVolSlide = event.param * 0.0333f; + break; + case SymEvent::VolSlideDown: + chnState.curVolSlideAmt = 0; + chnState.curVolSlide = event.param * -0.0333f; + break; + + case SymEvent::AddVolume: + m.command = m.param = 0; + break; + case SymEvent::Tremolo: + { + // both tremolo speed and depth can go much higher than OpenMPT supports, + // but modules will probably use pretty sane, supportable values anyway + // TODO: handle very small nonzero params + uint8 speed = std::min<uint8>(15, event.inst >> 3); + uint8 depth = std::min<uint8>(15, event.param >> 3); + chnState.curTremolo = (speed << 4) | depth; + } + break; + + // pitch effects + // Pitch slides have a similar granularity to volume slides, and are approximated + // the same way here based on a rough comparison against Exx/Fxx slides + case SymEvent::PitchSlideUp: + chnState.curPitchSlideAmt = 0; + chnState.curPitchSlide = event.param * 0.0333f; + chnState.tonePortaRemain = 0; + break; + case SymEvent::PitchSlideDown: + chnState.curPitchSlideAmt = 0; + chnState.curPitchSlide = event.param * -0.0333f; + chnState.tonePortaRemain = 0; + break; + + case SymEvent::PitchSlideTo: + if(note >= 0 && event.param > 0) + { + const int distance = std::abs((note - chnState.lastNote) * 32); + chnState.curPitchSlide = 0; + m.note = chnState.lastNote = note; + m.command = CMD_TONEPORTAMENTO; + chnState.tonePortaAmt = m.param = mpt::saturate_cast<ModCommand::PARAM>(distance / (2 * event.param)); + chnState.tonePortaRemain = static_cast<uint16>(distance - std::min(distance, chnState.tonePortaAmt * (patternSpeed - 1))); + } + break; + case SymEvent::AddPitch: + // "The range (-128...127) is about 4 half notes." + m.command = m.param = 0; + break; + case SymEvent::Vibrato: + { + // both vibrato speed and depth can go much higher than OpenMPT supports, + // but modules will probably use pretty sane, supportable values anyway + // TODO: handle very small nonzero params + uint8 speed = std::min<uint8>(15, event.inst >> 3); + uint8 depth = std::min<uint8>(15, event.param); + chnState.curVibrato = (speed << 4) | depth; + } + break; + case SymEvent::AddHalfTone: + m.note = chnState.lastNote = Clamp(static_cast<uint8>(chnState.lastNote + event.param), NOTE_MIN, NOTE_MAX); + m.command = CMD_TONEPORTAMENTO; + m.param = 0xFF; + chnState.tonePortaRemain = 0; + break; + + // DSP effects + case SymEvent::Filter: +#ifndef NO_PLUGINS + case SymEvent::DSPEcho: + case SymEvent::DSPDelay: +#endif + if(macroMap.count(event)) + { + m.command = CMD_MIDI; + m.param = macroMap[event]; + } else if(macroMap.size() < m_MidiCfg.Zxx.size()) + { + uint8 param = static_cast<uint8>(macroMap.size()); + if(ConvertDSP(event, m_MidiCfg.Zxx[param], *this)) + { + m.command = CMD_MIDI; + m.param = macroMap[event] = 0x80 | param; + + if(event.command == SymEvent::DSPEcho || event.command == SymEvent::DSPDelay) + useDSP = true; + } + } + break; + + // other effects + case SymEvent::Retrig: + // This plays the note <param> times every <inst>+1 ticks. + // The effect continues on the following rows until the correct amount is reached. + if(event.param < 1) + break; + m.command = CMD_RETRIG; + m.param = static_cast<uint8>(std::min(15, event.inst + 1)); + chnState.retriggerRemain = event.param * (event.inst + 1u); + break; + + case SymEvent::SetSpeed: + m.command = CMD_SPEED; + m.param = patternSpeed = event.param ? event.param : 4u; + break; + + // TODO this applies a fade on the sample level + case SymEvent::Emphasis: + m.command = CMD_NONE; + break; + case SymEvent::CV: + if(event.note == 0 || event.note == 4) + { + uint8 pan = (event.note == 4) ? event.inst : 128; + uint8 vol = std::min<uint8>(event.param, 100); + uint8 volL = static_cast<uint8>(vol * std::min(128, 256 - pan) / 128); + uint8 volR = static_cast<uint8>(vol * std::min(uint8(128), pan) / 128); + + if(volL != chnState.channelVol) + { + chnState.channelVol = volL; + + m.command = CMD_CHANNELVOLUME; + m.param = chnState.calculatedVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100)); + } + if(event.note == 4 && chn < (m_nChannels - 1) && chnStates[chn + 1].channelVol != volR) + { + chnStates[chn + 1].channelVol = volR; + + ModCommand &next = rowBase[chn + 1]; + next.command = CMD_CHANNELVOLUME; + next.param = chnState.calculatedVol = static_cast<uint8>(Util::muldivr_unsigned(chnState.lastVol, chnState.channelVol, 100)); + } + } + break; + case SymEvent::CVAdd: + // Effect doesn't seem to exist in UI and code looks like a no-op + m.command = CMD_NONE; + break; + + case SymEvent::SetFromAdd: + chnState.fromAdd = event.param; + chnState.sampleVibSpeed = 0; + chnState.sampleVibDepth = 0; + break; + case SymEvent::FromAdd: + // TODO need to verify how signedness of this value is treated + // C = -128...+127 + //FORMEL: Neuer FADD := alter FADD + C* Samplelaenge/16384 + chnState.fromAdd += event.param; + break; + + case SymEvent::SampleVib: + chnState.sampleVibSpeed = event.inst; + chnState.sampleVibDepth = event.param; + break; + + // sample effects + case SymEvent::FromAndPitch: + chnState.lastNote = note; + m.instr = chnState.lastInst = mappedInst; + [[fallthrough]]; + case SymEvent::ReplayFrom: + m.note = chnState.lastNote; + if(note >= 0) + m.instr = chnState.lastInst = mappedInst; + if(event.command == SymEvent::ReplayFrom) + { + m.volcmd = VOLCMD_TONEPORTAMENTO; + m.vol = 1; + } + // don't always add the command, because often FromAndPitch is used with offset 0 + // to act as a key-on which doesn't cancel volume slides, etc + if(event.param || chnState.fromAdd || chnState.sampleVibDepth) + { + double sampleVib = 0.0; + if(chnState.sampleVibDepth) + sampleVib = chnState.sampleVibDepth * (std::sin(chnState.sampleVibPhase * (mpt::numbers::pi * 2.0 / 1024.0) + 1.5 * mpt::numbers::pi) - 1.0) / 4.0; + m.command = CMD_OFFSETPERCENTAGE; + m.param = mpt::saturate_round<ModCommand::PARAM>(event.param + chnState.fromAdd + sampleVib); + } + chnState.tonePortaRemain = 0; + break; + } + + // Any event which plays a note should re-enable continuous effects + if(m.note != NOTE_NONE) + chnState.stopped = false; + else if(chnState.stopped) + continue; + + if(chnState.retriggerRemain) + { + chnState.retriggerRemain = std::max(chnState.retriggerRemain, static_cast<uint16>(patternSpeed)) - patternSpeed; + if(m.command == CMD_NONE) + { + m.command = CMD_RETRIG; + m.param = 0; + } + } + + // Handle fractional volume slides + if(chnState.curVolSlide != 0) + { + chnState.curVolSlideAmt += chnState.curVolSlide * patternSpeed; + if(m.command == CMD_NONE) + { + if(patternSpeed > 1 && chnState.curVolSlideAmt >= (patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curVolSlideAmt / (patternSpeed - 1))); + chnState.curVolSlideAmt -= slideAmt * (patternSpeed - 1); + // normal slide up + m.command = CMD_CHANNELVOLSLIDE; + m.param = slideAmt << 4; + } else if(chnState.curVolSlideAmt >= 1.0f) + { + uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curVolSlideAmt)); + chnState.curVolSlideAmt -= slideAmt; + // fine slide up + m.command = CMD_CHANNELVOLSLIDE; + m.param = (slideAmt << 4) | 0x0F; + } else if(patternSpeed > 1 && chnState.curVolSlideAmt <= -(patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(-chnState.curVolSlideAmt / (patternSpeed - 1))); + chnState.curVolSlideAmt += slideAmt * (patternSpeed - 1); + // normal slide down + m.command = CMD_CHANNELVOLSLIDE; + m.param = slideAmt; + } else if(chnState.curVolSlideAmt <= -1.0f) + { + uint8 slideAmt = std::min<uint8>(14, mpt::saturate_round<uint8>(-chnState.curVolSlideAmt)); + chnState.curVolSlideAmt += slideAmt; + // fine slide down + m.command = CMD_CHANNELVOLSLIDE; + m.param = slideAmt | 0xF0; + } + } + } + // Handle fractional pitch slides + if(chnState.curPitchSlide != 0) + { + chnState.curPitchSlideAmt += chnState.curPitchSlide * patternSpeed; + if(m.command == CMD_NONE) + { + if(patternSpeed > 1 && chnState.curPitchSlideAmt >= (patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(0xDF, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt / (patternSpeed - 1))); + chnState.curPitchSlideAmt -= slideAmt * (patternSpeed - 1); + // normal slide up + m.command = CMD_PORTAMENTOUP; + m.param = slideAmt; + } else if(chnState.curPitchSlideAmt >= 1.0f) + { + uint8 slideAmt = std::min<uint8>(15, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt)); + chnState.curPitchSlideAmt -= slideAmt; + // fine slide up + m.command = CMD_PORTAMENTOUP; + m.param = slideAmt | 0xF0; + } else if(patternSpeed > 1 && chnState.curPitchSlideAmt <= -(patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(0xDF, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt / (patternSpeed - 1))); + chnState.curPitchSlideAmt += slideAmt * (patternSpeed - 1); + // normal slide down + m.command = CMD_PORTAMENTODOWN; + m.param = slideAmt; + } else if(chnState.curPitchSlideAmt <= -1.0f) + { + uint8 slideAmt = std::min<uint8>(14, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt)); + chnState.curPitchSlideAmt += slideAmt; + // fine slide down + m.command = CMD_PORTAMENTODOWN; + m.param = slideAmt | 0xF0; + } + } + // TODO: use volume column if effect column is occupied + else if(m.volcmd == VOLCMD_NONE) + { + if(patternSpeed > 1 && chnState.curPitchSlideAmt / 4 >= (patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(9, mpt::saturate_round<uint8>(chnState.curPitchSlideAmt / (patternSpeed - 1)) / 4); + chnState.curPitchSlideAmt -= slideAmt * (patternSpeed - 1) * 4; + m.volcmd = VOLCMD_PORTAUP; + m.vol = slideAmt; + } else if(patternSpeed > 1 && chnState.curPitchSlideAmt / 4 <= -(patternSpeed - 1)) + { + uint8 slideAmt = std::min<uint8>(9, mpt::saturate_round<uint8>(-chnState.curPitchSlideAmt / (patternSpeed - 1)) / 4); + chnState.curPitchSlideAmt += slideAmt * (patternSpeed - 1) * 4; + m.volcmd = VOLCMD_PORTADOWN; + m.vol = slideAmt; + } + } + } + // Vibrato and Tremolo + if(m.command == CMD_NONE && chnState.curVibrato != 0) + { + m.command = CMD_VIBRATO; + m.param = chnState.curVibrato; + } + if(m.command == CMD_NONE && chnState.curTremolo != 0) + { + m.command = CMD_TREMOLO; + m.param = chnState.curTremolo; + } + // Tone Portamento + if(m.command != CMD_TONEPORTAMENTO && chnState.tonePortaRemain) + { + if(m.command == CMD_NONE) + m.command = CMD_TONEPORTAMENTO; + else + m.volcmd = VOLCMD_TONEPORTAMENTO; + chnState.tonePortaRemain -= std::min(chnState.tonePortaRemain, static_cast<uint16>(chnState.tonePortaAmt * (patternSpeed - 1))); + } + + chnState.sampleVibPhase = (chnState.sampleVibPhase + chnState.sampleVibSpeed * patternSpeed) & 1023; + + if(!(chn % 2u) && chnState.lastInst && chnState.lastInst <= instruments.size() + && (instruments[chnState.lastInst - 1].instFlags & SymInstrument::SyncPlay)) + { + syncPlayCommand = m; + applySyncPlay = true; + if(syncPlayCommand.instr && instruments[chnState.lastInst - 1].channel == SymInstrument::StereoL) + syncPlayCommand.instr++; + } + } + } + + Patterns[patternIndex].WriteEffect(EffectWriter(CMD_SPEED, static_cast<uint8>(pos.speed)).Row(0).RetryNextRow()); + } + order.insert(order.GetLength(), std::max(pos.loopNum.get(), uint16(1)), patternIndex); + // Undo transpose tweak + pos.transpose -= seq.transpose; + } + } + +#ifndef NO_PLUGINS + if(useDSP) + { + SNDMIXPLUGIN &plugin = m_MixPlugins[0]; + plugin.Destroy(); + memcpy(&plugin.Info.dwPluginId1, "SymM", 4); + memcpy(&plugin.Info.dwPluginId2, "Echo", 4); + plugin.Info.routingFlags = SNDMIXPLUGININFO::irAutoSuspend; + plugin.Info.mixMode = 0; + plugin.Info.gain = 10; + plugin.Info.reserved = 0; + plugin.Info.dwOutputRouting = 0; + std::fill(plugin.Info.dwReserved, plugin.Info.dwReserved + std::size(plugin.Info.dwReserved), 0); + plugin.Info.szName = "Echo"; + plugin.Info.szLibraryName = "SymMOD Echo"; + + m_MixPlugins[1].Info.szName = "No Echo"; + } +#endif // NO_PLUGINS + + // Channel panning + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + InitChannel(chn); + ChnSettings[chn].nPan = (chn & 1) ? 256 : 0; + ChnSettings[chn].nMixPlugin = useDSP ? 1 : 0; // For MIDI macros controlling the echo DSP + } + + m_modFormat.formatName = U_("Symphonie"); + m_modFormat.type = U_("symmod"); + if(!isSymphoniePro) + m_modFormat.madeWithTracker = U_("Symphonie"); // or Symphonie Jr + else if(instruments.size() <= 128) + m_modFormat.madeWithTracker = U_("Symphonie Pro"); + else + m_modFormat.madeWithTracker = U_("Symphonie Pro 256"); + m_modFormat.charset = mpt::Charset::Amiga_no_C1; + + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_uax.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_uax.cpp new file mode 100644 index 00000000..758eda91 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_uax.cpp @@ -0,0 +1,87 @@ +/* + * Load_uax.cpp + * ------------ + * Purpose: UAX (Unreal Sounds) module ripper + * Notes : The sounds are read into module sample slots. + * Authors: Johannes Schultz (inspired by code from http://wiki.beyondunreal.com/Legacy:Package_File_Format) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "UMXTools.h" + + +OPENMPT_NAMESPACE_BEGIN + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderUAX(MemoryFileReader file, const uint64 *pfilesize) +{ + return UMX::ProbeFileHeader(file, pfilesize, "sound"); +} + + +bool CSoundFile::ReadUAX(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + UMX::FileHeader fileHeader; + if(!file.ReadStruct(fileHeader) || !fileHeader.IsValid()) + return false; + + // Note that this can be a false positive, e.g. Unreal maps will have music and sound + // in their name table because they usually import such files. However, it spares us + // from wildly seeking through the file, as the name table is usually right at the + // start of the file, so it is hopefully a good enough heuristic for our purposes. + if(!UMX::FindNameTableEntry(file, fileHeader, "sound")) + return false; + else if(!file.CanRead(fileHeader.GetMinimumAdditionalFileSize())) + return false; + else if(loadFlags == onlyVerifyHeader) + return true; + + const std::vector<std::string> names = UMX::ReadNameTable(file, fileHeader); + const std::vector<int32> classes = UMX::ReadImportTable(file, fileHeader, names); + + InitializeGlobals(); + m_modFormat.formatName = MPT_UFORMAT("Unreal Package v{}")(fileHeader.packageVersion); + m_modFormat.type = U_("uax"); + m_modFormat.charset = mpt::Charset::Windows1252; + + // Read export table + file.Seek(fileHeader.exportOffset); + for(uint32 i = 0; i < fileHeader.exportCount && file.CanRead(8); i++) + { + auto [fileChunk, objName] = UMX::ReadExportTableEntry(file, fileHeader, classes, names, "sound"); + if(!fileChunk.IsValid()) + continue; + + if(CanAddMoreSamples()) + { + // Read as sample + if(ReadSampleFromFile(GetNumSamples() + 1, fileChunk, true)) + { + if(objName > 0 && static_cast<size_t>(objName) < names.size()) + { + m_szNames[GetNumSamples()] = names[objName]; + } + } + } + } + + if(m_nSamples != 0) + { + InitializeChannels(); + SetType(MOD_TYPE_MPT); + m_ContainerType = MOD_CONTAINERTYPE_UAX; + m_nChannels = 4; + Patterns.Insert(0, 64); + Order().assign(1, 0); + return true; + } else + { + return false; + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_ult.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ult.cpp new file mode 100644 index 00000000..f40ae6a5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_ult.cpp @@ -0,0 +1,515 @@ +/* + * Load_ult.cpp + * ------------ + * Purpose: ULT (UltraTracker) module loader + * Notes : (currently none) + * Authors: Storlek (Original author - http://schismtracker.org/ - code ported with permission) + * Johannes Schultz (OpenMPT Port, tweaks) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +OPENMPT_NAMESPACE_BEGIN + +struct UltFileHeader +{ + char signature[14]; // "MAS_UTrack_V00" + uint8 version; // '1'...'4' + char songName[32]; // Song Name, not guaranteed to be null-terminated + uint8 messageLength; // Number of Lines +}; + +MPT_BINARY_STRUCT(UltFileHeader, 48) + + +struct UltSample +{ + enum UltSampleFlags + { + ULT_16BIT = 4, + ULT_LOOP = 8, + ULT_PINGPONGLOOP = 16, + }; + + char name[32]; + char filename[12]; + uint32le loopStart; + uint32le loopEnd; + uint32le sizeStart; + uint32le sizeEnd; + uint8le volume; // 0-255, apparently prior to 1.4 this was logarithmic? + uint8le flags; // above + uint16le speed; // only exists for 1.4+ + int16le finetune; + + // Convert an ULT sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.Set16BitCuePoints(); + + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename); + + if(sizeEnd <= sizeStart) + { + return; + } + + mptSmp.nLength = sizeEnd - sizeStart; + mptSmp.nSustainStart = loopStart; + mptSmp.nSustainEnd = std::min(static_cast<SmpLength>(loopEnd), mptSmp.nLength); + mptSmp.nVolume = volume; + + mptSmp.nC5Speed = speed; + if(finetune) + { + mptSmp.Transpose(finetune / (12.0 * 32768.0)); + } + + if(flags & ULT_LOOP) + mptSmp.uFlags.set(CHN_SUSTAINLOOP); + if(flags & ULT_PINGPONGLOOP) + mptSmp.uFlags.set(CHN_PINGPONGSUSTAIN); + if(flags & ULT_16BIT) + { + mptSmp.uFlags.set(CHN_16BIT); + mptSmp.nSustainStart /= 2; + mptSmp.nSustainEnd /= 2; + } + + } +}; + +MPT_BINARY_STRUCT(UltSample, 66) + + +/* Unhandled effects: +5x1 - do not loop sample (x is unused) +E0x - set vibrato strength (2 is normal) + +The logarithmic volume scale used in older format versions here, or pretty +much anywhere for that matter. I don't even think Ultra Tracker tries to +convert them. */ + + +static void TranslateULTCommands(uint8 &effect, uint8 ¶m, uint8 version) +{ + + static constexpr uint8 ultEffTrans[] = + { + CMD_ARPEGGIO, + CMD_PORTAMENTOUP, + CMD_PORTAMENTODOWN, + CMD_TONEPORTAMENTO, + CMD_VIBRATO, + CMD_NONE, + CMD_NONE, + CMD_TREMOLO, + CMD_NONE, + CMD_OFFSET, + CMD_VOLUMESLIDE, + CMD_PANNING8, + CMD_VOLUME, + CMD_PATTERNBREAK, + CMD_NONE, // extended effects, processed separately + CMD_SPEED, + }; + + + uint8 e = effect & 0x0F; + effect = ultEffTrans[e]; + + switch(e) + { + case 0x00: + if(!param || version < '3') + effect = CMD_NONE; + break; + case 0x05: + // play backwards + if((param & 0x0F) == 0x02 || (param & 0xF0) == 0x20) + { + effect = CMD_S3MCMDEX; + param = 0x9F; + } + if(((param & 0x0F) == 0x0C || (param & 0xF0) == 0xC0) && version >= '3') + { + effect = CMD_KEYOFF; + param = 0; + } + break; + case 0x07: + if(version < '4') + effect = CMD_NONE; + break; + case 0x0A: + if(param & 0xF0) + param &= 0xF0; + break; + case 0x0B: + param = (param & 0x0F) * 0x11; + break; + case 0x0C: // volume + param /= 4u; + break; + case 0x0D: // pattern break + param = 10 * (param >> 4) + (param & 0x0F); + break; + case 0x0E: // special + switch(param >> 4) + { + case 0x01: + effect = CMD_PORTAMENTOUP; + param = 0xF0 | (param & 0x0F); + break; + case 0x02: + effect = CMD_PORTAMENTODOWN; + param = 0xF0 | (param & 0x0F); + break; + case 0x08: + if(version >= '4') + { + effect = CMD_S3MCMDEX; + param = 0x60 | (param & 0x0F); + } + break; + case 0x09: + effect = CMD_RETRIG; + param &= 0x0F; + break; + case 0x0A: + effect = CMD_VOLUMESLIDE; + param = ((param & 0x0F) << 4) | 0x0F; + break; + case 0x0B: + effect = CMD_VOLUMESLIDE; + param = 0xF0 | (param & 0x0F); + break; + case 0x0C: case 0x0D: + effect = CMD_S3MCMDEX; + break; + } + break; + case 0x0F: + if(param > 0x2F) + effect = CMD_TEMPO; + break; + } +} + + +static int ReadULTEvent(ModCommand &m, FileReader &file, uint8 version) +{ + uint8 repeat = 1; + uint8 b = file.ReadUint8(); + if(b == 0xFC) // repeat event + { + repeat = file.ReadUint8(); + b = file.ReadUint8(); + } + + m.note = (b > 0 && b < 61) ? (b + 35 + NOTE_MIN) : NOTE_NONE; + + const auto [instr, cmd, para1, para2] = file.ReadArray<uint8, 4>(); + + m.instr = instr; + uint8 cmd1 = cmd & 0x0F; + uint8 cmd2 = cmd >> 4; + uint8 param1 = para1; + uint8 param2 = para2; + TranslateULTCommands(cmd1, param1, version); + TranslateULTCommands(cmd2, param2, version); + + // sample offset -- this is even more special than digitrakker's + if(cmd1 == CMD_OFFSET && cmd2 == CMD_OFFSET) + { + uint32 offset = ((param2 << 8) | param1) >> 6; + m.command = CMD_OFFSET; + m.param = static_cast<ModCommand::PARAM>(offset); + if(offset > 0xFF) + { + m.volcmd = VOLCMD_OFFSET; + m.vol = static_cast<ModCommand::VOL>(offset >> 8); + } + return repeat; + } else if(cmd1 == CMD_OFFSET) + { + uint32 offset = param1 * 4; + param1 = mpt::saturate_cast<uint8>(offset); + if(offset > 0xFF && ModCommand::GetEffectWeight(cmd2) < ModCommand::GetEffectType(CMD_OFFSET)) + { + m.command = CMD_OFFSET; + m.param = static_cast<ModCommand::PARAM>(offset); + m.volcmd = VOLCMD_OFFSET; + m.vol = static_cast<ModCommand::VOL>(offset >> 8); + return repeat; + } + } else if(cmd2 == CMD_OFFSET) + { + uint32 offset = param2 * 4; + param2 = mpt::saturate_cast<uint8>(offset); + if(offset > 0xFF && ModCommand::GetEffectWeight(cmd1) < ModCommand::GetEffectType(CMD_OFFSET)) + { + m.command = CMD_OFFSET; + m.param = static_cast<ModCommand::PARAM>(offset); + m.volcmd = VOLCMD_OFFSET; + m.vol = static_cast<ModCommand::VOL>(offset >> 8); + return repeat; + } + } else if(cmd1 == cmd2) + { + // don't try to figure out how ultratracker does this, it's quite random + cmd2 = CMD_NONE; + } + if(cmd2 == CMD_VOLUME || (cmd2 == CMD_NONE && cmd1 != CMD_VOLUME)) + { + // swap commands + std::swap(cmd1, cmd2); + std::swap(param1, param2); + } + + // Combine slide commands, if possible + ModCommand::CombineEffects(cmd2, param2, cmd1, param1); + ModCommand::TwoRegularCommandsToMPT(cmd1, param1, cmd2, param2); + + m.volcmd = cmd1; + m.vol = param1; + m.command = cmd2; + m.param = param2; + + return repeat; +} + + +// Functor for postfixing ULT patterns (this is easier than just remembering everything WHILE we're reading the pattern events) +struct PostFixUltCommands +{ + PostFixUltCommands(CHANNELINDEX numChannels) + { + this->numChannels = numChannels; + curChannel = 0; + writeT125 = false; + isPortaActive.resize(numChannels, false); + } + + void operator()(ModCommand &m) + { + // Attempt to fix portamentos. + // UltraTracker will slide until the destination note is reached or 300 is encountered. + + // Stop porta? + if(m.command == CMD_TONEPORTAMENTO && m.param == 0) + { + isPortaActive[curChannel] = false; + m.command = CMD_NONE; + } + if(m.volcmd == VOLCMD_TONEPORTAMENTO && m.vol == 0) + { + isPortaActive[curChannel] = false; + m.volcmd = VOLCMD_NONE; + } + + // Apply porta? + if(m.note == NOTE_NONE && isPortaActive[curChannel]) + { + if(m.command == CMD_NONE && m.volcmd != VOLCMD_TONEPORTAMENTO) + { + m.command = CMD_TONEPORTAMENTO; + m.param = 0; + } else if(m.volcmd == VOLCMD_NONE && m.command != CMD_TONEPORTAMENTO) + { + m.volcmd = VOLCMD_TONEPORTAMENTO; + m.vol = 0; + } + } else // new note -> stop porta (or initialize again) + { + isPortaActive[curChannel] = (m.command == CMD_TONEPORTAMENTO || m.volcmd == VOLCMD_TONEPORTAMENTO); + } + + // attempt to fix F00 (reset to tempo 125, speed 6) + if(writeT125 && m.command == CMD_NONE) + { + m.command = CMD_TEMPO; + m.param = 125; + } + if(m.command == CMD_SPEED && m.param == 0) + { + m.param = 6; + writeT125 = true; + } + if(m.command == CMD_TEMPO) // don't try to fix this anymore if the tempo has already changed. + { + writeT125 = false; + } + curChannel = (curChannel + 1) % numChannels; + } + + std::vector<bool> isPortaActive; + CHANNELINDEX numChannels, curChannel; + bool writeT125; +}; + + +static bool ValidateHeader(const UltFileHeader &fileHeader) +{ + if(fileHeader.version < '1' + || fileHeader.version > '4' + || std::memcmp(fileHeader.signature, "MAS_UTrack_V00", sizeof(fileHeader.signature)) + ) + { + return false; + } + return true; +} + +static uint64 GetHeaderMinimumAdditionalSize(const UltFileHeader &fileHeader) +{ + return fileHeader.messageLength * 32u + 3u + 256u; +} + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderULT(MemoryFileReader file, const uint64 *pfilesize) +{ + UltFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +bool CSoundFile::ReadULT(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + UltFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } + + InitializeGlobals(MOD_TYPE_ULT); + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName); + + const mpt::uchar *versions[] = {UL_("<1.4"), UL_("1.4"), UL_("1.5"), UL_("1.6")}; + m_modFormat.formatName = U_("UltraTracker"); + m_modFormat.type = U_("ult"); + m_modFormat.madeWithTracker = U_("UltraTracker ") + versions[fileHeader.version - '1']; + m_modFormat.charset = mpt::Charset::CP437; + + m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; // this will be converted to IT format by MPT. + + // Read "messageLength" lines, each containing 32 characters. + m_songMessage.ReadFixedLineLength(file, fileHeader.messageLength * 32, 32, 0); + + if(SAMPLEINDEX numSamples = file.ReadUint8(); numSamples < MAX_SAMPLES) + m_nSamples = numSamples; + else + return false; + + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + UltSample sampleHeader; + + // Annoying: v4 added a field before the end of the struct + if(fileHeader.version >= '4') + { + file.ReadStruct(sampleHeader); + } else + { + file.ReadStructPartial(sampleHeader, 64); + sampleHeader.finetune = sampleHeader.speed; + sampleHeader.speed = 8363; + } + + sampleHeader.ConvertToMPT(Samples[smp]); + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); + } + + ReadOrderFromFile<uint8>(Order(), file, 256, 0xFF, 0xFE); + + if(CHANNELINDEX numChannels = file.ReadUint8() + 1u; numChannels <= MAX_BASECHANNELS) + m_nChannels = numChannels; + else + return false; + + PATTERNINDEX numPats = file.ReadUint8() + 1; + + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + ChnSettings[chn].Reset(); + if(fileHeader.version >= '3') + ChnSettings[chn].nPan = ((file.ReadUint8() & 0x0F) << 4) + 8; + else + ChnSettings[chn].nPan = (chn & 1) ? 192 : 64; + } + + Patterns.ResizeArray(numPats); + for(PATTERNINDEX pat = 0; pat < numPats; pat++) + { + if(!Patterns.Insert(pat, 64)) + return false; + } + + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + ModCommand evnote; + for(PATTERNINDEX pat = 0; pat < numPats && file.CanRead(5); pat++) + { + ModCommand *note = Patterns[pat].GetpModCommand(0, chn); + ROWINDEX row = 0; + while(row < 64) + { + int repeat = ReadULTEvent(evnote, file, fileHeader.version); + if(repeat + row > 64) + repeat = 64 - row; + if(repeat == 0) break; + while(repeat--) + { + *note = evnote; + note += GetNumChannels(); + row++; + } + } + } + } + + // Post-fix some effects. + Patterns.ForEachModCommand(PostFixUltCommands(GetNumChannels())); + + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + SampleIO( + Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .ReadSample(Samples[smp], file); + } + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_wav.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_wav.cpp new file mode 100644 index 00000000..aeb5c386 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_wav.cpp @@ -0,0 +1,204 @@ +/* + * Load_wav.cpp + * ------------ + * Purpose: WAV importer + * Notes : This loader converts each WAV channel into a separate mono sample. + * 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 "WAVTools.h" +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "SampleCopy.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////// +// WAV file support + + +template <typename SampleConversion> +static bool CopyWavChannel(ModSample &sample, const FileReader &file, size_t channelIndex, size_t numChannels, SampleConversion conv = SampleConversion()) +{ + MPT_ASSERT(sample.GetNumChannels() == 1); + MPT_ASSERT(sample.GetElementarySampleSize() == sizeof(typename SampleConversion::output_t)); + + const size_t offset = channelIndex * sizeof(typename SampleConversion::input_t) * SampleConversion::input_inc; + + if(sample.AllocateSample() == 0 || !file.CanRead(offset)) + { + return false; + } + + const std::byte *inBuf = file.GetRawData<std::byte>().data(); + CopySample<SampleConversion>(reinterpret_cast<typename SampleConversion::output_t*>(sample.samplev()), sample.nLength, 1, inBuf + offset, file.BytesLeft() - offset, numChannels, conv); + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderWAV(MemoryFileReader file, const uint64 *pfilesize) +{ + RIFFHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if((fileHeader.magic != RIFFHeader::idRIFF && fileHeader.magic != RIFFHeader::idLIST) + || (fileHeader.type != RIFFHeader::idWAVE && fileHeader.type != RIFFHeader::idwave)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadWAV(FileReader &file, ModLoadingFlags loadFlags) +{ + WAVReader wavFile(file); + + if(!wavFile.IsValid() + || wavFile.GetNumChannels() == 0 + || wavFile.GetNumChannels() > MAX_BASECHANNELS + || wavFile.GetNumChannels() >= MAX_SAMPLES + || wavFile.GetBitsPerSample() == 0 + || wavFile.GetBitsPerSample() > 64 + || (wavFile.GetBitsPerSample() < 32 && wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat) + || (wavFile.GetSampleFormat() != WAVFormatChunk::fmtPCM && wavFile.GetSampleFormat() != WAVFormatChunk::fmtFloat)) + { + return false; + } else if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_MPT); + m_ContainerType = MOD_CONTAINERTYPE_WAV; + m_nChannels = std::max(wavFile.GetNumChannels(), uint16(2)); + Patterns.ResizeArray(2); + if(!Patterns.Insert(0, 64) || !Patterns.Insert(1, 64)) + { + return false; + } + + m_modFormat.formatName = U_("RIFF WAVE"); + m_modFormat.type = U_("wav"); + m_modFormat.charset = mpt::Charset::Windows1252; + + const SmpLength sampleLength = wavFile.GetSampleLength(); + + // Setting up module length + // Calculate sample length in ticks at tempo 125 + const uint32 sampleRate = std::max(uint32(1), wavFile.GetSampleRate()); + const uint32 sampleTicks = mpt::saturate_cast<uint32>(((sampleLength * 50) / sampleRate) + 1); + uint32 ticksPerRow = std::max((sampleTicks + 63u) / 63u, uint32(1)); + + Order().assign(1, 0); + ORDERINDEX numOrders = 1; + while(ticksPerRow >= 32 && numOrders < MAX_ORDERS) + { + numOrders++; + ticksPerRow = (sampleTicks + (64 * numOrders - 1)) / (64 * numOrders); + } + Order().resize(numOrders, 1); + + m_nSamples = wavFile.GetNumChannels(); + m_nInstruments = 0; + m_nDefaultSpeed = ticksPerRow; + m_nDefaultTempo.Set(125); + m_SongFlags = SONG_LINEARSLIDES; + + for(CHANNELINDEX channel = 0; channel < m_nChannels; channel++) + { + ChnSettings[channel].Reset(); + ChnSettings[channel].nPan = (channel % 2u) ? 256 : 0; + } + + // Setting up pattern + PatternRow pattern = Patterns[0].GetRow(0); + pattern[0].note = pattern[1].note = NOTE_MIDDLEC; + pattern[0].instr = pattern[1].instr = 1; + + const FileReader sampleChunk = wavFile.GetSampleData(); + + // Read every channel into its own sample lot. + for(SAMPLEINDEX channel = 0; channel < GetNumSamples(); channel++) + { + pattern[channel].note = pattern[0].note; + pattern[channel].instr = static_cast<ModCommand::INSTR>(channel + 1); + + ModSample &sample = Samples[channel + 1]; + sample.Initialize(); + sample.uFlags = CHN_PANNING; + sample.nLength = sampleLength; + sample.nC5Speed = wavFile.GetSampleRate(); + m_szNames[channel + 1] = ""; + wavFile.ApplySampleSettings(sample, GetCharsetInternal(), m_szNames[channel + 1]); + + if(wavFile.GetNumChannels() > 1) + { + // Pan all samples appropriately + switch(channel) + { + case 0: + sample.nPan = 0; + break; + case 1: + sample.nPan = 256; + break; + case 2: + sample.nPan = (wavFile.GetNumChannels() == 3 ? 128u : 64u); + pattern[channel].command = CMD_S3MCMDEX; + pattern[channel].param = 0x91; + break; + case 3: + sample.nPan = 192; + pattern[channel].command = CMD_S3MCMDEX; + pattern[channel].param = 0x91; + break; + default: + sample.nPan = 128; + break; + } + } + + if(wavFile.GetBitsPerSample() > 8) + { + sample.uFlags.set(CHN_16BIT); + } + + if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat) + { + if(wavFile.GetBitsPerSample() <= 32) + CopyWavChannel<SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeFloat32<littleEndian32>>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + else + CopyWavChannel<SC::ConversionChain<SC::Convert<int16, float64>, SC::DecodeFloat64<littleEndian64>>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + } else + { + if(wavFile.GetBitsPerSample() <= 8) + CopyWavChannel<SC::DecodeUint8>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + else if(wavFile.GetBitsPerSample() <= 16) + CopyWavChannel<SC::DecodeInt16<0, littleEndian16>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + else if(wavFile.GetBitsPerSample() <= 24) + CopyWavChannel<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24>>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + else if(wavFile.GetBitsPerSample() <= 32) + CopyWavChannel<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32>>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + else if(wavFile.GetBitsPerSample() <= 64) + CopyWavChannel<SC::ConversionChain<SC::Convert<int16, int64>, SC::DecodeInt64<0, littleEndian64>>>(sample, sampleChunk, channel, wavFile.GetNumChannels()); + } + sample.PrecomputeLoops(*this, false); + + } + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Load_xm.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Load_xm.cpp new file mode 100644 index 00000000..06ab4e1c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Load_xm.cpp @@ -0,0 +1,1433 @@ +/* + * Load_xm.cpp + * ----------- + * Purpose: XM (FastTracker II) module loader / saver + * Notes : (currently none) + * 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 "../common/version.h" +#include "XMTools.h" +#include "mod_specifications.h" +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/mptFileIO.h" +#endif +#include "OggStream.h" +#include <algorithm> +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" // For super smooth ramping option +#endif // MODPLUG_TRACKER +#include "mpt/audio/span.hpp" + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) +#include <sstream> +#endif + +#if defined(MPT_WITH_VORBIS) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/codec.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif + +#if defined(MPT_WITH_VORBISFILE) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/vorbisfile.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#include "openmpt/soundbase/Copy.hpp" +#endif + +#ifdef MPT_WITH_STBVORBIS +#include <stb_vorbis/stb_vorbis.c> +#include "openmpt/soundbase/Copy.hpp" +#endif // MPT_WITH_STBVORBIS + + +OPENMPT_NAMESPACE_BEGIN + + + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) + +static size_t VorbisfileFilereaderRead(void *ptr, size_t size, size_t nmemb, void *datasource) +{ + FileReader &file = *reinterpret_cast<FileReader*>(datasource); + return file.ReadRaw(mpt::span(mpt::void_cast<std::byte*>(ptr), size * nmemb)).size() / size; +} + +static int VorbisfileFilereaderSeek(void *datasource, ogg_int64_t offset, int whence) +{ + FileReader &file = *reinterpret_cast<FileReader*>(datasource); + switch(whence) + { + case SEEK_SET: + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + } + break; + case SEEK_CUR: + { + if(offset < 0) + { + if(offset == std::numeric_limits<ogg_int64_t>::min()) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(0-offset)) + { + return -1; + } + return file.SkipBack(mpt::saturate_cast<FileReader::off_t>(0 - offset)) ? 0 : -1; + } else + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Skip(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + } + } + break; + case SEEK_END: + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(file.GetLength() + offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(file.GetLength() + offset)) ? 0 : -1; + } + break; + default: + return -1; + } +} + +static long VorbisfileFilereaderTell(void *datasource) +{ + FileReader &file = *reinterpret_cast<FileReader*>(datasource); + FileReader::off_t result = file.GetPosition(); + if(!mpt::in_range<long>(result)) + { + return -1; + } + return static_cast<long>(result); +} + +#endif // MPT_WITH_VORBIS && MPT_WITH_VORBISFILE + + +// Allocate samples for an instrument +static std::vector<SAMPLEINDEX> AllocateXMSamples(CSoundFile &sndFile, SAMPLEINDEX numSamples) +{ + LimitMax(numSamples, SAMPLEINDEX(32)); + + std::vector<SAMPLEINDEX> foundSlots; + foundSlots.reserve(numSamples); + + for(SAMPLEINDEX i = 0; i < numSamples; i++) + { + SAMPLEINDEX candidateSlot = sndFile.GetNumSamples() + 1; + + if(candidateSlot >= MAX_SAMPLES) + { + // If too many sample slots are needed, try to fill some empty slots first. + for(SAMPLEINDEX j = 1; j <= sndFile.GetNumSamples(); j++) + { + if(sndFile.GetSample(j).HasSampleData()) + { + continue; + } + + if(!mpt::contains(foundSlots, j)) + { + // Empty sample slot that is not occupied by the current instrument. Yay! + candidateSlot = j; + + // Remove unused sample from instrument sample assignments + for(INSTRUMENTINDEX ins = 1; ins <= sndFile.GetNumInstruments(); ins++) + { + if(sndFile.Instruments[ins] == nullptr) + { + continue; + } + for(auto &sample : sndFile.Instruments[ins]->Keyboard) + { + if(sample == candidateSlot) + { + sample = 0; + } + } + } + break; + } + } + } + + if(candidateSlot >= MAX_SAMPLES) + { + // Still couldn't find any empty sample slots, so look out for existing but unused samples. + std::vector<bool> usedSamples; + SAMPLEINDEX unusedSampleCount = sndFile.DetectUnusedSamples(usedSamples); + + if(unusedSampleCount > 0) + { + sndFile.RemoveSelectedSamples(usedSamples); + // Remove unused samples from instrument sample assignments + for(INSTRUMENTINDEX ins = 1; ins <= sndFile.GetNumInstruments(); ins++) + { + if(sndFile.Instruments[ins] == nullptr) + { + continue; + } + for(auto &sample : sndFile.Instruments[ins]->Keyboard) + { + if(sample < usedSamples.size() && !usedSamples[sample]) + { + sample = 0; + } + } + } + + // New candidate slot is first unused sample slot. + candidateSlot = static_cast<SAMPLEINDEX>(std::find(usedSamples.begin() + 1, usedSamples.end(), false) - usedSamples.begin()); + } else + { + // No unused sampel slots: Give up :( + break; + } + } + + if(candidateSlot < MAX_SAMPLES) + { + foundSlots.push_back(candidateSlot); + if(candidateSlot > sndFile.GetNumSamples()) + { + sndFile.m_nSamples = candidateSlot; + } + } + } + + return foundSlots; +} + + +// Read .XM patterns +static void ReadXMPatterns(FileReader &file, const XMFileHeader &fileHeader, CSoundFile &sndFile) +{ + // Reading patterns + sndFile.Patterns.ResizeArray(fileHeader.patterns); + for(PATTERNINDEX pat = 0; pat < fileHeader.patterns; pat++) + { + FileReader::off_t curPos = file.GetPosition(); + uint32 headerSize = file.ReadUint32LE(); + file.Skip(1); // Pack method (= 0) + + ROWINDEX numRows = 64; + + if(fileHeader.version == 0x0102) + { + numRows = file.ReadUint8() + 1; + } else + { + numRows = file.ReadUint16LE(); + } + + // A packed size of 0 indicates a completely empty pattern. + const uint16 packedSize = file.ReadUint16LE(); + + if(numRows == 0) + numRows = 64; + else if(numRows > MAX_PATTERN_ROWS) + numRows = MAX_PATTERN_ROWS; + + file.Seek(curPos + headerSize); + FileReader patternChunk = file.ReadChunk(packedSize); + + if(!sndFile.Patterns.Insert(pat, numRows) || packedSize == 0) + { + continue; + } + + enum PatternFlags + { + isPackByte = 0x80, + allFlags = 0xFF, + + notePresent = 0x01, + instrPresent = 0x02, + volPresent = 0x04, + commandPresent = 0x08, + paramPresent = 0x10, + }; + + for(auto &m : sndFile.Patterns[pat]) + { + uint8 info = patternChunk.ReadUint8(); + + uint8 vol = 0; + if(info & isPackByte) + { + // Interpret byte as flag set. + if(info & notePresent) m.note = patternChunk.ReadUint8(); + } else + { + // Interpret byte as note, read all other pattern fields as well. + m.note = info; + info = allFlags; + } + + if(info & instrPresent) m.instr = patternChunk.ReadUint8(); + if(info & volPresent) vol = patternChunk.ReadUint8(); + if(info & commandPresent) m.command = patternChunk.ReadUint8(); + if(info & paramPresent) m.param = patternChunk.ReadUint8(); + + if(m.note == 97) + { + m.note = NOTE_KEYOFF; + } else if(m.note > 0 && m.note < 97) + { + m.note += 12; + } else + { + m.note = NOTE_NONE; + } + + if(m.command | m.param) + { + CSoundFile::ConvertModCommand(m); + } else + { + m.command = CMD_NONE; + } + + if(m.instr == 0xFF) + { + m.instr = 0; + } + + if(vol >= 0x10 && vol <= 0x50) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = vol - 0x10; + } else if (vol >= 0x60) + { + // Volume commands 6-F translation. + static constexpr ModCommand::VOLCMD volEffTrans[] = + { + VOLCMD_VOLSLIDEDOWN, VOLCMD_VOLSLIDEUP, VOLCMD_FINEVOLDOWN, VOLCMD_FINEVOLUP, + VOLCMD_VIBRATOSPEED, VOLCMD_VIBRATODEPTH, VOLCMD_PANNING, VOLCMD_PANSLIDELEFT, + VOLCMD_PANSLIDERIGHT, VOLCMD_TONEPORTAMENTO, + }; + + m.volcmd = volEffTrans[(vol - 0x60) >> 4]; + m.vol = vol & 0x0F; + + if(m.volcmd == VOLCMD_PANNING) + { + m.vol *= 4; // FT2 does indeed not scale panning symmetrically. + } + } + } + } +} + + +enum TrackerVersions +{ + verUnknown = 0x00, // Probably not made with MPT + verOldModPlug = 0x01, // Made with MPT Alpha / Beta + verNewModPlug = 0x02, // Made with MPT (not Alpha / Beta) + verModPlug1_09 = 0x04, // Made with MPT 1.09 or possibly other version + verOpenMPT = 0x08, // Made with OpenMPT + verConfirmed = 0x10, // We are very sure that we found the correct tracker version. + + verFT2Generic = 0x20, // "FastTracker v2.00", but FastTracker has NOT been ruled out + verOther = 0x40, // Something we don't know, testing for DigiTrakker. + verFT2Clone = 0x80, // NOT FT2: itype changed between instruments, or \0 found in song title + verDigiTrakker = 0x100, // Probably DigiTrakker + verUNMO3 = 0x200, // TODO: UNMO3-ed XMs are detected as MPT 1.16 + verEmptyOrders = 0x400, // Allow empty order list like in OpenMPT (FT2 just plays pattern 0 if the order list is empty according to the header) +}; +DECLARE_FLAGSET(TrackerVersions) + + +static bool ValidateHeader(const XMFileHeader &fileHeader) +{ + if(fileHeader.channels == 0 + || fileHeader.channels > MAX_BASECHANNELS + || std::memcmp(fileHeader.signature, "Extended Module: ", 17) + ) + { + return false; + } + return true; +} + + +static uint64 GetHeaderMinimumAdditionalSize(const XMFileHeader &fileHeader) +{ + return fileHeader.orders + 4 * (fileHeader.patterns + fileHeader.instruments); +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderXM(MemoryFileReader file, const uint64 *pfilesize) +{ + XMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); +} + + +static bool ReadSampleData(ModSample &sample, SampleIO sampleFlags, FileReader &sampleChunk, bool &isOXM) +{ + bool unsupportedSample = false; + + bool isOGG = false; + if(sampleChunk.CanRead(8)) + { + isOGG = true; + sampleChunk.Skip(4); + // In order to avoid false-detecting PCM as OggVorbis as much as possible, + // we parse and verify the complete sample data and only assume OggVorbis, + // if all Ogg checksums are correct a no single byte of non-Ogg data exists. + // The fast-path for regular PCM will only check "OggS" magic and do no other work after failing that check. + while(!sampleChunk.EndOfFile()) + { + if(!Ogg::ReadPage(sampleChunk)) + { + isOGG = false; + break; + } + } + } + isOXM = isOXM || isOGG; + sampleChunk.Rewind(); + if(isOGG) + { + uint32 originalSize = sampleChunk.ReadInt32LE(); + FileReader sampleData = sampleChunk.ReadChunk(sampleChunk.BytesLeft()); + + sample.uFlags.set(CHN_16BIT, sampleFlags.GetBitDepth() >= 16); + sample.uFlags.set(CHN_STEREO, sampleFlags.GetChannelFormat() != SampleIO::mono); + sample.nLength = originalSize / (sample.uFlags[CHN_16BIT] ? 2 : 1) / (sample.uFlags[CHN_STEREO] ? 2 : 1); + +#if defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISFILE) + + ov_callbacks callbacks = { + &VorbisfileFilereaderRead, + &VorbisfileFilereaderSeek, + NULL, + &VorbisfileFilereaderTell + }; + OggVorbis_File vf; + MemsetZero(vf); + if(ov_open_callbacks(&sampleData, &vf, nullptr, 0, callbacks) == 0) + { + if(ov_streams(&vf) == 1) + { // we do not support chained vorbis samples + vorbis_info *vi = ov_info(&vf, -1); + if(vi && vi->rate > 0 && vi->channels > 0) + { + sample.AllocateSample(); + SmpLength offset = 0; + int channels = vi->channels; + int current_section = 0; + long decodedSamples = 0; + bool eof = false; + while(!eof && offset < sample.nLength && sample.HasSampleData()) + { + float **output = nullptr; + long ret = ov_read_float(&vf, &output, 1024, ¤t_section); + if(ret == 0) + { + eof = true; + } else if(ret < 0) + { + // stream error, just try to continue + } else + { + decodedSamples = ret; + LimitMax(decodedSamples, mpt::saturate_cast<long>(sample.nLength - offset)); + if(decodedSamples > 0 && channels == sample.GetNumChannels()) + { + if(sample.uFlags[CHN_16BIT]) + { + CopyAudio(mpt::audio_span_interleaved(sample.sample16() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } else + { + CopyAudio(mpt::audio_span_interleaved(sample.sample8() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } + } + offset += decodedSamples; + } + } + } else + { + unsupportedSample = true; + } + } else + { + unsupportedSample = true; + } + ov_clear(&vf); + } else + { + unsupportedSample = true; + } + +#elif defined(MPT_WITH_STBVORBIS) + + // NOTE/TODO: stb_vorbis does not handle inferred negative PCM sample + // position at stream start. (See + // <https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-132000A.2>). + // This means that, for remuxed and re-aligned/cutted (at stream start) + // Vorbis files, stb_vorbis will include superfluous samples at the + // beginning. OXM files with this property are yet to be spotted in the + // wild, thus, this behaviour is currently not problematic. + + int consumed = 0, error = 0; + stb_vorbis *vorb = nullptr; + FileReader::PinnedView sampleDataView = sampleData.GetPinnedView(); + const std::byte* data = sampleDataView.data(); + std::size_t dataLeft = sampleDataView.size(); + vorb = stb_vorbis_open_pushdata(mpt::byte_cast<const unsigned char*>(data), mpt::saturate_cast<int>(dataLeft), &consumed, &error, nullptr); + sampleData.Skip(consumed); + data += consumed; + dataLeft -= consumed; + if(vorb) + { + // Header has been read, proceed to reading the sample data + sample.AllocateSample(); + SmpLength offset = 0; + while((error == VORBIS__no_error || (error == VORBIS_need_more_data && dataLeft > 0)) + && offset < sample.nLength && sample.HasSampleData()) + { + int channels = 0, decodedSamples = 0; + float **output; + consumed = stb_vorbis_decode_frame_pushdata(vorb, mpt::byte_cast<const unsigned char*>(data), mpt::saturate_cast<int>(dataLeft), &channels, &output, &decodedSamples); + sampleData.Skip(consumed); + data += consumed; + dataLeft -= consumed; + LimitMax(decodedSamples, mpt::saturate_cast<int>(sample.nLength - offset)); + if(decodedSamples > 0 && channels == sample.GetNumChannels()) + { + if(sample.uFlags[CHN_16BIT]) + { + CopyAudio(mpt::audio_span_interleaved(sample.sample16() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } else + { + CopyAudio(mpt::audio_span_interleaved(sample.sample8() + (offset * sample.GetNumChannels()), sample.GetNumChannels(), decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + } + } + offset += decodedSamples; + error = stb_vorbis_get_error(vorb); + } + stb_vorbis_close(vorb); + } else + { + unsupportedSample = true; + } + +#else // !VORBIS + + unsupportedSample = true; + +#endif // VORBIS + + } else + { + sampleFlags.ReadSample(sample, sampleChunk); + } + + return !unsupportedSample; +} + + +bool CSoundFile::ReadXM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + + XMFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader)))) + { + return false; + } else if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_XM); + InitializeChannels(); + m_nMixLevels = MixLevels::Compatible; + + FlagSet<TrackerVersions> madeWith(verUnknown); + mpt::ustring madeWithTracker; + bool isMadTracker = false; + + if(!memcmp(fileHeader.trackerName, "FastTracker v2.00 ", 20) && fileHeader.size == 276) + { + if(fileHeader.version < 0x0104) + madeWith = verFT2Generic | verConfirmed; + else if(memchr(fileHeader.songName, '\0', 20) != nullptr) + // FT2 pads the song title with spaces, some other trackers use null chars + madeWith = verFT2Clone | verNewModPlug | verEmptyOrders; + else + madeWith = verFT2Generic | verNewModPlug; + } else if(!memcmp(fileHeader.trackerName, "FastTracker v 2.00 ", 20)) + { + // MPT 1.0 (exact version to be determined later) + madeWith = verOldModPlug; + } else + { + // Something else! + madeWith = verUnknown | verConfirmed; + + madeWithTracker = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.trackerName)); + + if(!memcmp(fileHeader.trackerName, "OpenMPT ", 8)) + { + madeWith = verOpenMPT | verConfirmed | verEmptyOrders; + } else if(!memcmp(fileHeader.trackerName, "MilkyTracker ", 12)) + { + // MilkyTracker prior to version 0.90.87 doesn't set a version string. + // Luckily, starting with v0.90.87, MilkyTracker also implements the FT2 panning scheme. + if(memcmp(fileHeader.trackerName + 12, " ", 8)) + { + m_nMixLevels = MixLevels::CompatibleFT2; + } + } else if(!memcmp(fileHeader.trackerName, "Fasttracker II clone", 20)) + { + // 8bitbubsy's FT2 clone should be treated exactly like FT2 + madeWith = verFT2Generic | verConfirmed; + } else if(!memcmp(fileHeader.trackerName, "MadTracker 2.0\0", 15)) + { + // Fix channel 2 in m3_cha.xm + m_playBehaviour.reset(kFT2PortaNoNote); + // Fix arpeggios in kragle_-_happy_day.xm + m_playBehaviour.reset(kFT2Arpeggio); + isMadTracker = true; + } else if(!memcmp(fileHeader.trackerName, "Skale Tracker\0", 14) || !memcmp(fileHeader.trackerName, "Sk@le Tracker\0", 14)) + { + m_playBehaviour.reset(kFT2ST3OffsetOutOfRange); + // Fix arpeggios in KAPTENFL.XM + m_playBehaviour.reset(kFT2Arpeggio); + } else if(!memcmp(fileHeader.trackerName, "*Converted ", 11)) + { + madeWith = verDigiTrakker; + } + } + + m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songName); + + m_nMinPeriod = 1; + m_nMaxPeriod = 31999; + + Order().SetRestartPos(fileHeader.restartPos); + m_nChannels = fileHeader.channels; + m_nInstruments = std::min(static_cast<uint16>(fileHeader.instruments), static_cast<uint16>(MAX_INSTRUMENTS - 1)); + if(fileHeader.speed) + m_nDefaultSpeed = fileHeader.speed; + if(fileHeader.tempo) + m_nDefaultTempo = Clamp(TEMPO(fileHeader.tempo, 0), ModSpecs::xmEx.GetTempoMin(), ModSpecs::xmEx.GetTempoMax()); + + m_SongFlags.reset(); + m_SongFlags.set(SONG_LINEARSLIDES, (fileHeader.flags & XMFileHeader::linearSlides) != 0); + m_SongFlags.set(SONG_EXFILTERRANGE, (fileHeader.flags & XMFileHeader::extendedFilterRange) != 0); + if(m_SongFlags[SONG_EXFILTERRANGE] && madeWith == (verFT2Generic | verNewModPlug)) + { + madeWith = verFT2Clone | verNewModPlug | verConfirmed; + } + + ReadOrderFromFile<uint8>(Order(), file, fileHeader.orders); + if(fileHeader.orders == 0 && !madeWith[verEmptyOrders]) + { + // Fix lamb_-_dark_lighthouse.xm, which only contains one pattern and an empty order list + Order().assign(1, 0); + } + file.Seek(fileHeader.size + 60); + + if(fileHeader.version >= 0x0104) + { + ReadXMPatterns(file, fileHeader, *this); + } + + bool isOXM = false; + + // In case of XM versions < 1.04, we need to memorize the sample flags for all samples, as they are not stored immediately after the sample headers. + std::vector<SampleIO> sampleFlags; + uint8 sampleReserved = 0; + int instrType = -1; + bool unsupportedSamples = false; + + // Reading instruments + for(INSTRUMENTINDEX instr = 1; instr <= m_nInstruments; instr++) + { + // First, try to read instrument header length... + uint32 headerSize = file.ReadUint32LE(); + if(headerSize == 0) + { + headerSize = sizeof(XMInstrumentHeader); + } + + // Now, read the complete struct. + file.SkipBack(4); + XMInstrumentHeader instrHeader; + file.ReadStructPartial(instrHeader, headerSize); + + // Time for some version detection stuff. + if(madeWith == verOldModPlug) + { + madeWith.set(verConfirmed); + if(instrHeader.size == 245) + { + // ModPlug Tracker Alpha + m_dwLastSavedWithVersion = MPT_V("1.00.00.A5"); + madeWithTracker = U_("ModPlug Tracker 1.0 alpha"); + } else if(instrHeader.size == 263) + { + // ModPlug Tracker Beta (Beta 1 still behaves like Alpha, but Beta 3.3 does it this way) + m_dwLastSavedWithVersion = MPT_V("1.00.00.B3"); + madeWithTracker = U_("ModPlug Tracker 1.0 beta"); + } else + { + // WTF? + madeWith = (verUnknown | verConfirmed); + } + } else if(instrHeader.numSamples == 0) + { + // Empty instruments make tracker identification pretty easy! + if(instrHeader.size == 263 && instrHeader.sampleHeaderSize == 0 && madeWith[verNewModPlug]) + madeWith.set(verConfirmed); + else if(instrHeader.size != 29 && madeWith[verDigiTrakker]) + madeWith.reset(verDigiTrakker); + else if(madeWith[verFT2Clone | verFT2Generic] && instrHeader.size != 33) + { + // Sure isn't FT2. + // Note: FT2 NORMALLY writes shdr=40 for all samples, but sometimes it + // just happens to write random garbage there instead. Surprise! + // Note: 4-mat's eternity.xm has an instrument header size of 29. + madeWith = verUnknown; + } + } + + if(AllocateInstrument(instr) == nullptr) + { + continue; + } + + instrHeader.ConvertToMPT(*Instruments[instr]); + + if(instrType == -1) + { + instrType = instrHeader.type; + } else if(instrType != instrHeader.type && madeWith[verFT2Generic]) + { + // FT2 writes some random junk for the instrument type field, + // but it's always the SAME junk for every instrument saved. + madeWith.reset(verFT2Generic); + madeWith.set(verFT2Clone); + } + + if(instrHeader.numSamples > 0) + { + // Yep, there are some samples associated with this instrument. + if((instrHeader.instrument.midiEnabled | instrHeader.instrument.midiChannel | instrHeader.instrument.midiProgram | instrHeader.instrument.muteComputer) != 0) + { + // Definitely not an old MPT. + madeWith.reset(verOldModPlug | verNewModPlug); + } + + // Read sample headers + std::vector<SAMPLEINDEX> sampleSlots = AllocateXMSamples(*this, instrHeader.numSamples); + + // Update sample assignment map + for(size_t k = 0 + 12; k < 96 + 12; k++) + { + if(Instruments[instr]->Keyboard[k] < sampleSlots.size()) + { + Instruments[instr]->Keyboard[k] = sampleSlots[Instruments[instr]->Keyboard[k]]; + } + } + + if(fileHeader.version >= 0x0104) + { + sampleFlags.clear(); + } + // Need to memorize those if we're going to skip any samples... + std::vector<uint32> sampleSize(instrHeader.numSamples); + + // Early versions of Sk@le Tracker set instrHeader.sampleHeaderSize = 0 (IFULOVE.XM) + // cybernostra weekend has instrHeader.sampleHeaderSize = 0x12, which would leave out the sample name, but FT2 still reads the name. + MPT_ASSERT(instrHeader.sampleHeaderSize == 0 || instrHeader.sampleHeaderSize == sizeof(XMSample)); + + for(SAMPLEINDEX sample = 0; sample < instrHeader.numSamples; sample++) + { + XMSample sampleHeader; + file.ReadStruct(sampleHeader); + + sampleFlags.push_back(sampleHeader.GetSampleFormat()); + sampleSize[sample] = sampleHeader.length; + sampleReserved |= sampleHeader.reserved; + + if(sample < sampleSlots.size()) + { + SAMPLEINDEX mptSample = sampleSlots[sample]; + + sampleHeader.ConvertToMPT(Samples[mptSample]); + instrHeader.instrument.ApplyAutoVibratoToMPT(Samples[mptSample]); + + m_szNames[mptSample] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + + if((sampleHeader.flags & 3) == 3 && madeWith[verNewModPlug]) + { + // MPT 1.09 and maybe newer / older versions set both loop flags for bidi loops. + madeWith.set(verModPlug1_09); + } + } + } + + // Read samples + if(fileHeader.version >= 0x0104) + { + for(SAMPLEINDEX sample = 0; sample < instrHeader.numSamples; sample++) + { + // Sample 15 in dirtysex.xm by J/M/T/M is a 16-bit sample with an odd size of 0x18B according to the header, while the real sample size would be 0x18A. + // Always read as many bytes as specified in the header, even if the sample reader would probably read less bytes. + FileReader sampleChunk = file.ReadChunk(sampleFlags[sample].GetEncoding() != SampleIO::ADPCM ? sampleSize[sample] : (16 + (sampleSize[sample] + 1) / 2)); + if(sample < sampleSlots.size() && (loadFlags & loadSampleData)) + { + if(!ReadSampleData(Samples[sampleSlots[sample]], sampleFlags[sample], sampleChunk, isOXM)) + { + unsupportedSamples = true; + } + } + } + } + } + } + + if(sampleReserved == 0 && madeWith[verNewModPlug] && memchr(fileHeader.songName, '\0', sizeof(fileHeader.songName)) != nullptr) + { + // Null-terminated song name: Quite possibly MPT. (could really be an MPT-made file resaved in FT2, though) + madeWith.set(verConfirmed); + } + + if(fileHeader.version < 0x0104) + { + // Load Patterns and Samples (Version 1.02 and 1.03) + if(loadFlags & (loadPatternData | loadSampleData)) + { + ReadXMPatterns(file, fileHeader, *this); + } + + if(loadFlags & loadSampleData) + { + for(SAMPLEINDEX sample = 1; sample <= GetNumSamples(); sample++) + { + sampleFlags[sample - 1].ReadSample(Samples[sample], file); + } + } + } + + if(unsupportedSamples) + { + AddToLog(LogWarning, U_("Some compressed samples could not be loaded because they use an unsupported codec.")); + } + + // Read song comments: "text" + if(file.ReadMagic("text")) + { + m_songMessage.Read(file, file.ReadUint32LE(), SongMessage::leCR); + madeWith.set(verConfirmed); + } + + // Read midi config: "MIDI" + bool hasMidiConfig = false; + if(file.ReadMagic("MIDI")) + { + file.ReadStructPartial<MIDIMacroConfigData>(m_MidiCfg, file.ReadUint32LE()); + m_MidiCfg.Sanitize(); + hasMidiConfig = true; + madeWith.set(verConfirmed); + } + + // Read pattern names: "PNAM" + if(file.ReadMagic("PNAM")) + { + const PATTERNINDEX namedPats = std::min(static_cast<PATTERNINDEX>(file.ReadUint32LE() / MAX_PATTERNNAME), Patterns.Size()); + + for(PATTERNINDEX pat = 0; pat < namedPats; pat++) + { + char patName[MAX_PATTERNNAME]; + file.ReadString<mpt::String::maybeNullTerminated>(patName, MAX_PATTERNNAME); + Patterns[pat].SetName(patName); + } + madeWith.set(verConfirmed); + } + + // Read channel names: "CNAM" + if(file.ReadMagic("CNAM")) + { + const CHANNELINDEX namedChans = std::min(static_cast<CHANNELINDEX>(file.ReadUint32LE() / MAX_CHANNELNAME), GetNumChannels()); + for(CHANNELINDEX chn = 0; chn < namedChans; chn++) + { + file.ReadString<mpt::String::maybeNullTerminated>(ChnSettings[chn].szName, MAX_CHANNELNAME); + } + madeWith.set(verConfirmed); + } + + // Read mix plugins information + if(file.CanRead(8)) + { + FileReader::off_t oldPos = file.GetPosition(); + LoadMixPlugins(file); + if(file.GetPosition() != oldPos) + { + madeWith.set(verConfirmed); + } + } + + if(madeWith[verConfirmed]) + { + if(madeWith[verModPlug1_09]) + { + m_dwLastSavedWithVersion = MPT_V("1.09.00.00"); + madeWithTracker = U_("ModPlug Tracker 1.09"); + } else if(madeWith[verNewModPlug]) + { + m_dwLastSavedWithVersion = MPT_V("1.16.00.00"); + madeWithTracker = U_("ModPlug Tracker 1.10 - 1.16"); + } + } + + if(!memcmp(fileHeader.trackerName, "OpenMPT ", 8)) + { + // Hey, I know this tracker! + std::string mptVersion(fileHeader.trackerName + 8, 12); + m_dwLastSavedWithVersion = Version::Parse(mpt::ToUnicode(mpt::Charset::ASCII, mptVersion)); + madeWith = verOpenMPT | verConfirmed; + + if(m_dwLastSavedWithVersion < MPT_V("1.22.07.19")) + m_nMixLevels = MixLevels::Compatible; + else + m_nMixLevels = MixLevels::CompatibleFT2; + } + + if(m_dwLastSavedWithVersion && !madeWith[verOpenMPT]) + { + m_nMixLevels = MixLevels::Original; + m_playBehaviour.reset(); + } + + if(madeWith[verFT2Generic]) + { + m_nMixLevels = MixLevels::CompatibleFT2; + + if(!hasMidiConfig) + { + // FT2 allows typing in arbitrary unsupported effect letters such as Zxx. + // Prevent these commands from being interpreted as filter commands by erasing the default MIDI Config. + m_MidiCfg.ClearZxxMacros(); + } + + if(fileHeader.version >= 0x0104 // Old versions of FT2 didn't have (smooth) ramping. Disable it for those versions where we can be sure that there should be no ramping. +#ifdef MODPLUG_TRACKER + && TrackerSettings::Instance().autoApplySmoothFT2Ramping +#endif // MODPLUG_TRACKER + ) + { + // apply FT2-style super-soft volume ramping + m_playBehaviour.set(kFT2VolumeRamping); + } + } + + if(madeWithTracker.empty()) + { + if(madeWith[verDigiTrakker] && sampleReserved == 0 && (instrType ? instrType : -1) == -1) + { + madeWithTracker = U_("DigiTrakker"); + } else if(madeWith[verFT2Generic]) + { + madeWithTracker = U_("FastTracker 2 or compatible"); + } else + { + madeWithTracker = U_("Unknown"); + } + } + + bool isOpenMPTMade = false; // specific for OpenMPT 1.17+ + if(GetNumInstruments()) + { + isOpenMPTMade = LoadExtendedInstrumentProperties(file); + } + + LoadExtendedSongProperties(file, true, &isOpenMPTMade); + + if(isOpenMPTMade && m_dwLastSavedWithVersion < MPT_V("1.17.00.00")) + { + // 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 >= MPT_V("1.17.00.00")) + { + madeWithTracker = U_("OpenMPT ") + m_dwLastSavedWithVersion.ToUString(); + } + + // We no longer allow any --- or +++ items in the order list now. + if(m_dwLastSavedWithVersion && m_dwLastSavedWithVersion < MPT_V("1.22.02.02")) + { + if(!Patterns.IsValidPat(0xFE)) + Order().RemovePattern(0xFE); + if(!Patterns.IsValidPat(0xFF)) + Order().Replace(0xFF, Order.GetInvalidPatIndex()); + } + + m_modFormat.formatName = MPT_UFORMAT("FastTracker 2 v{}.{}")(fileHeader.version >> 8, mpt::ufmt::hex0<2>(fileHeader.version & 0xFF)); + m_modFormat.madeWithTracker = std::move(madeWithTracker); + m_modFormat.charset = (m_dwLastSavedWithVersion || isMadTracker) ? mpt::Charset::Windows1252 : mpt::Charset::CP437; + if(isOXM) + { + m_modFormat.originalFormatName = std::move(m_modFormat.formatName); + m_modFormat.formatName = U_("OggMod FastTracker 2"); + m_modFormat.type = U_("oxm"); + m_modFormat.originalType = U_("xm"); + } else + { + m_modFormat.type = U_("xm"); + } + + return true; +} + + +#ifndef MODPLUG_NO_FILESAVE + + +bool CSoundFile::SaveXM(std::ostream &f, bool compatibilityExport) +{ + + bool addChannel = false; // avoid odd channel count for FT2 compatibility + + XMFileHeader fileHeader; + MemsetZero(fileHeader); + + memcpy(fileHeader.signature, "Extended Module: ", 17); + mpt::String::WriteBuf(mpt::String::spacePadded, fileHeader.songName) = m_songName; + fileHeader.eof = 0x1A; + const std::string openMptTrackerName = mpt::ToCharset(GetCharsetFile(), Version::Current().GetOpenMPTVersionString()); + mpt::String::WriteBuf(mpt::String::spacePadded, fileHeader.trackerName) = openMptTrackerName; + + // Writing song header + fileHeader.version = 0x0104; // XM Format v1.04 + fileHeader.size = sizeof(XMFileHeader) - 60; // minus everything before this field + fileHeader.restartPos = Order().GetRestartPos(); + + fileHeader.channels = m_nChannels; + if((m_nChannels % 2u) && m_nChannels < 32) + { + // Avoid odd channel count for FT2 compatibility + fileHeader.channels++; + addChannel = true; + } else if(compatibilityExport && fileHeader.channels > 32) + { + fileHeader.channels = 32; + } + + // Find out number of orders and patterns used. + // +++ and --- patterns are not taken into consideration as FastTracker does not support them. + + const ORDERINDEX trimmedLength = Order().GetLengthTailTrimmed(); + std::vector<uint8> orderList(trimmedLength); + const ORDERINDEX orderLimit = compatibilityExport ? 256 : uint16_max; + ORDERINDEX numOrders = 0; + PATTERNINDEX numPatterns = Patterns.GetNumPatterns(); + bool changeOrderList = false; + for(ORDERINDEX ord = 0; ord < trimmedLength; ord++) + { + PATTERNINDEX pat = Order()[ord]; + if(pat == Order.GetIgnoreIndex() || pat == Order.GetInvalidPatIndex() || pat > uint8_max) + { + changeOrderList = true; + } else if(numOrders < orderLimit) + { + orderList[numOrders++] = static_cast<uint8>(pat); + if(pat >= numPatterns) + numPatterns = pat + 1; + } + } + if(changeOrderList) + { + AddToLog(LogWarning, U_("Skip and stop order list items (+++ and ---) are not saved in XM files.")); + } + orderList.resize(compatibilityExport ? 256 : numOrders); + + fileHeader.orders = numOrders; + fileHeader.patterns = numPatterns; + fileHeader.size += static_cast<uint32>(orderList.size()); + + uint16 writeInstruments; + if(m_nInstruments > 0) + fileHeader.instruments = writeInstruments = m_nInstruments; + else + fileHeader.instruments = writeInstruments = m_nSamples; + + if(m_SongFlags[SONG_LINEARSLIDES]) fileHeader.flags |= XMFileHeader::linearSlides; + if(m_SongFlags[SONG_EXFILTERRANGE] && !compatibilityExport) fileHeader.flags |= XMFileHeader::extendedFilterRange; + fileHeader.flags = fileHeader.flags; + + // Fasttracker 2 will happily accept any tempo faster than 255 BPM. XMPlay does also support this, great! + fileHeader.tempo = mpt::saturate_cast<uint16>(m_nDefaultTempo.GetInt()); + fileHeader.speed = static_cast<uint16>(Clamp(m_nDefaultSpeed, 1u, 31u)); + + mpt::IO::Write(f, fileHeader); + + // Write processed order list + mpt::IO::Write(f, orderList); + + // Writing patterns + +#define ASSERT_CAN_WRITE(x) \ + if(len > s.size() - x) /*Buffer running out? Make it larger.*/ \ + s.resize(s.size() + 10 * 1024, 0); + std::vector<uint8> s(64 * 64 * 5, 0); + + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + uint8 patHead[9] = { 0 }; + patHead[0] = 9; + + if(!Patterns.IsValidPat(pat)) + { + // There's nothing to write... chicken out. + patHead[5] = 64; + mpt::IO::Write(f, patHead); + continue; + } + + const uint16 numRows = mpt::saturate_cast<uint16>(Patterns[pat].GetNumRows()); + patHead[5] = static_cast<uint8>(numRows & 0xFF); + patHead[6] = static_cast<uint8>(numRows >> 8); + + auto p = Patterns[pat].cbegin(); + size_t len = 0; + // Empty patterns are always loaded as 64-row patterns in FT2, regardless of their real size... + bool emptyPattern = true; + + for(size_t j = m_nChannels * numRows; j > 0; j--, p++) + { + // Don't write more than 32 channels + if(compatibilityExport && m_nChannels - ((j - 1) % m_nChannels) > 32) continue; + + uint8 note = p->note; + uint8 command = p->command, param = p->param; + ModSaveCommand(command, param, true, compatibilityExport); + + if (note >= NOTE_MIN_SPECIAL) note = 97; else + if ((note <= 12) || (note > 96+12)) note = 0; else + note -= 12; + uint8 vol = 0; + if (p->volcmd != VOLCMD_NONE) + { + switch(p->volcmd) + { + case VOLCMD_VOLUME: vol = 0x10 + p->vol; break; + case VOLCMD_VOLSLIDEDOWN: vol = 0x60 + (p->vol & 0x0F); break; + case VOLCMD_VOLSLIDEUP: vol = 0x70 + (p->vol & 0x0F); break; + case VOLCMD_FINEVOLDOWN: vol = 0x80 + (p->vol & 0x0F); break; + case VOLCMD_FINEVOLUP: vol = 0x90 + (p->vol & 0x0F); break; + case VOLCMD_VIBRATOSPEED: vol = 0xA0 + (p->vol & 0x0F); break; + case VOLCMD_VIBRATODEPTH: vol = 0xB0 + (p->vol & 0x0F); break; + case VOLCMD_PANNING: vol = 0xC0 + (p->vol / 4); if (vol > 0xCF) vol = 0xCF; break; + case VOLCMD_PANSLIDELEFT: vol = 0xD0 + (p->vol & 0x0F); break; + case VOLCMD_PANSLIDERIGHT: vol = 0xE0 + (p->vol & 0x0F); break; + case VOLCMD_TONEPORTAMENTO: vol = 0xF0 + (p->vol & 0x0F); break; + } + // Those values are ignored in FT2. Don't save them, also to avoid possible problems with other trackers (or MPT itself) + if(compatibilityExport && p->vol == 0) + { + switch(p->volcmd) + { + case VOLCMD_VOLUME: + case VOLCMD_PANNING: + case VOLCMD_VIBRATODEPTH: + case VOLCMD_TONEPORTAMENTO: + case VOLCMD_PANSLIDELEFT: // Doesn't have memory, but does weird things with zero param. + break; + default: + // no memory here. + vol = 0; + } + } + } + + // no need to fix non-empty patterns + if(!p->IsEmpty()) + emptyPattern = false; + + // Apparently, completely empty patterns are loaded as empty 64-row patterns in FT2, regardless of their original size. + // We have to avoid this, so we add a "break to row 0" command in the last row. + if(j == 1 && emptyPattern && numRows != 64) + { + command = 0x0D; + param = 0; + } + + if ((note) && (p->instr) && (vol > 0x0F) && (command) && (param)) + { + s[len++] = note; + s[len++] = p->instr; + s[len++] = vol; + s[len++] = command; + s[len++] = param; + } else + { + uint8 b = 0x80; + if (note) b |= 0x01; + if (p->instr) b |= 0x02; + if (vol >= 0x10) b |= 0x04; + if (command) b |= 0x08; + if (param) b |= 0x10; + s[len++] = b; + if (b & 1) s[len++] = note; + if (b & 2) s[len++] = p->instr; + if (b & 4) s[len++] = vol; + if (b & 8) s[len++] = command; + if (b & 16) s[len++] = param; + } + + if(addChannel && (j % m_nChannels == 1 || m_nChannels == 1)) + { + ASSERT_CAN_WRITE(1); + s[len++] = 0x80; + } + + ASSERT_CAN_WRITE(5); + } + + if(emptyPattern && numRows == 64) + { + // Be smart when saving empty patterns! + len = 0; + } + + // Reaching the limits of file format? + if(len > uint16_max) + { + AddToLog(LogWarning, MPT_UFORMAT("Warning: File format limit was reached. Some pattern data may not get written to file. (pattern {})")(pat)); + len = uint16_max; + } + + patHead[7] = static_cast<uint8>(len & 0xFF); + patHead[8] = static_cast<uint8>(len >> 8); + mpt::IO::Write(f, patHead); + if(len) mpt::IO::WriteRaw(f, s.data(), len); + } + +#undef ASSERT_CAN_WRITE + + // Check which samples are referenced by which instruments (for assigning unreferenced samples to instruments) + std::vector<bool> sampleAssigned(GetNumSamples() + 1, false); + for(INSTRUMENTINDEX ins = 1; ins <= GetNumInstruments(); ins++) + { + if(Instruments[ins] != nullptr) + { + Instruments[ins]->GetSamples(sampleAssigned); + } + } + + // Writing instruments + for(INSTRUMENTINDEX ins = 1; ins <= writeInstruments; ins++) + { + XMInstrumentHeader insHeader; + std::vector<SAMPLEINDEX> samples; + + if(GetNumInstruments()) + { + if(Instruments[ins] != nullptr) + { + // Convert instrument + insHeader.ConvertToXM(*Instruments[ins], compatibilityExport); + + samples = insHeader.instrument.GetSampleList(*Instruments[ins], compatibilityExport); + if(samples.size() > 0 && samples[0] <= GetNumSamples()) + { + // Copy over auto-vibrato settings of first sample + insHeader.instrument.ApplyAutoVibratoToXM(Samples[samples[0]], GetType()); + } + + std::vector<SAMPLEINDEX> additionalSamples; + + // Try to save "instrument-less" samples as well by adding those after the "normal" samples of our sample. + // We look for unassigned samples directly after the samples assigned to our current instrument, so if + // e.g. sample 1 is assigned to instrument 1 and samples 2 to 10 aren't assigned to any instrument, + // we will assign those to sample 1. Any samples before the first referenced sample are going to be lost, + // but hey, I wrote this mostly for preserving instrument texts in existing modules, where we shouldn't encounter this situation... + for(auto smp : samples) + { + while(++smp <= GetNumSamples() + && !sampleAssigned[smp] + && insHeader.numSamples < (compatibilityExport ? 16 : 32)) + { + sampleAssigned[smp] = true; // Don't want to add this sample again. + additionalSamples.push_back(smp); + insHeader.numSamples++; + } + } + + samples.insert(samples.end(), additionalSamples.begin(), additionalSamples.end()); + } else + { + MemsetZero(insHeader); + } + } else + { + // Convert samples to instruments + MemsetZero(insHeader); + insHeader.numSamples = 1; + insHeader.instrument.ApplyAutoVibratoToXM(Samples[ins], GetType()); + samples.push_back(ins); + } + + insHeader.Finalise(); + size_t insHeaderSize = insHeader.size; + mpt::IO::WritePartial(f, insHeader, insHeaderSize); + + std::vector<SampleIO> sampleFlags(samples.size()); + + // Write Sample Headers + for(SAMPLEINDEX smp = 0; smp < samples.size(); smp++) + { + XMSample xmSample; + if(samples[smp] <= GetNumSamples()) + { + xmSample.ConvertToXM(Samples[samples[smp]], GetType(), compatibilityExport); + } else + { + MemsetZero(xmSample); + } + sampleFlags[smp] = xmSample.GetSampleFormat(); + + mpt::String::WriteBuf(mpt::String::spacePadded, xmSample.name) = m_szNames[samples[smp]]; + + mpt::IO::Write(f, xmSample); + } + + // Write Sample Data + for(SAMPLEINDEX smp = 0; smp < samples.size(); smp++) + { + if(samples[smp] <= GetNumSamples()) + { + sampleFlags[smp].WriteSample(f, Samples[samples[smp]]); + } + } + } + + if(!compatibilityExport) + { + // Writing song comments + if(!m_songMessage.empty()) + { + uint32 size = mpt::saturate_cast<uint32>(m_songMessage.length()); + mpt::IO::WriteRaw(f, "text", 4); + mpt::IO::WriteIntLE<uint32>(f, size); + mpt::IO::WriteRaw(f, m_songMessage.c_str(), size); + } + // Writing midi cfg + if(!m_MidiCfg.IsMacroDefaultSetupUsed()) + { + mpt::IO::WriteRaw(f, "MIDI", 4); + mpt::IO::WriteIntLE<uint32>(f, sizeof(MIDIMacroConfigData)); + mpt::IO::Write(f, static_cast<MIDIMacroConfigData &>(m_MidiCfg)); + } + // Writing Pattern Names + const PATTERNINDEX numNamedPats = Patterns.GetNumNamedPatterns(); + if(numNamedPats > 0) + { + mpt::IO::WriteRaw(f, "PNAM", 4); + mpt::IO::WriteIntLE<uint32>(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 + { + CHANNELINDEX numNamedChannels = 0; + for(CHANNELINDEX chn = 0; chn < m_nChannels; chn++) + { + if (ChnSettings[chn].szName[0]) numNamedChannels = chn + 1; + } + // Do it! + if(numNamedChannels) + { + mpt::IO::WriteRaw(f, "CNAM", 4); + mpt::IO::WriteIntLE<uint32>(f, numNamedChannels * MAX_CHANNELNAME); + for(CHANNELINDEX chn = 0; chn < numNamedChannels; chn++) + { + char name[MAX_CHANNELNAME]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = ChnSettings[chn].szName; + mpt::IO::Write(f, name); + } + } + } + + //Save hacked-on extra info + SaveMixPlugins(&f); + if(GetNumInstruments()) + { + SaveExtendedInstrumentProperties(writeInstruments, f); + } + SaveExtendedSongProperties(f); + } + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Loaders.h b/Src/external_dependencies/openmpt-trunk/soundlib/Loaders.h new file mode 100644 index 00000000..4b9b6504 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Loaders.h @@ -0,0 +1,90 @@ +/* + * Loaders.h + * --------- + * Purpose: Common functions for module loaders + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/misc_util.h" +#include "../common/FileReader.h" +#include "Sndfile.h" +#include "SampleIO.h" + +OPENMPT_NAMESPACE_BEGIN + +// Functions to create 4-byte and 2-byte magic byte identifiers in little-endian format +// Use this together with uint32le/uint16le file members. +constexpr uint32 MagicLE(const char(&id)[5]) +{ + return static_cast<uint32>((static_cast<uint8>(id[3]) << 24) | (static_cast<uint8>(id[2]) << 16) | (static_cast<uint8>(id[1]) << 8) | static_cast<uint8>(id[0])); +} +constexpr uint16 MagicLE(const char(&id)[3]) +{ + return static_cast<uint16>((static_cast<uint8>(id[1]) << 8) | static_cast<uint8>(id[0])); +} +// Functions to create 4-byte and 2-byte magic byte identifiers in big-endian format +// Use this together with uint32be/uint16be file members. +// Note: Historically, some magic bytes in MPT-specific fields are reversed (due to the use of multi-char literals). +// Such fields turned up reversed in files, so MagicBE is used to keep them readable in the code. +constexpr uint32 MagicBE(const char(&id)[5]) +{ + return static_cast<uint32>((static_cast<uint8>(id[0]) << 24) | (static_cast<uint8>(id[1]) << 16) | (static_cast<uint8>(id[2]) << 8) | static_cast<uint8>(id[3])); +} +constexpr uint16 MagicBE(const char(&id)[3]) +{ + return static_cast<uint16>((static_cast<uint8>(id[0]) << 8) | static_cast<uint8>(id[1])); +} + + +// Read 'howMany' order items from an array. +// 'stopIndex' is treated as '---', 'ignoreIndex' is treated as '+++'. If the format doesn't support such indices, just pass uint16_max. +template<typename T, size_t arraySize> +bool ReadOrderFromArray(ModSequence &order, const T(&orders)[arraySize], size_t howMany = arraySize, uint16 stopIndex = uint16_max, uint16 ignoreIndex = uint16_max) +{ + static_assert(mpt::is_binary_safe<T>::value); + LimitMax(howMany, arraySize); + LimitMax(howMany, MAX_ORDERS); + ORDERINDEX readEntries = static_cast<ORDERINDEX>(howMany); + + order.resize(readEntries); + for(int i = 0; i < readEntries; i++) + { + PATTERNINDEX pat = static_cast<PATTERNINDEX>(orders[i]); + if(pat == stopIndex) pat = order.GetInvalidPatIndex(); + else if(pat == ignoreIndex) pat = order.GetIgnoreIndex(); + order.at(i) = pat; + } + return true; +} + + +// Read 'howMany' order items as integers with defined endianness from a file. +// 'stopIndex' is treated as '---', 'ignoreIndex' is treated as '+++'. If the format doesn't support such indices, just pass uint16_max. +template<typename T> +bool ReadOrderFromFile(ModSequence &order, FileReader &file, size_t howMany, uint16 stopIndex = uint16_max, uint16 ignoreIndex = uint16_max) +{ + static_assert(mpt::is_binary_safe<T>::value); + if(!file.CanRead(howMany * sizeof(T))) + return false; + LimitMax(howMany, MAX_ORDERS); + ORDERINDEX readEntries = static_cast<ORDERINDEX>(howMany); + + order.resize(readEntries); + T patF; + for(auto &pat : order) + { + file.ReadStruct(patF); + pat = static_cast<PATTERNINDEX>(patF); + if(pat == stopIndex) pat = order.GetInvalidPatIndex(); + else if(pat == ignoreIndex) pat = order.GetIgnoreIndex(); + } + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.cpp new file mode 100644 index 00000000..6ef8b819 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.cpp @@ -0,0 +1,263 @@ +/* + * MIDIEvents.cpp + * -------------- + * Purpose: MIDI event handling, event lists, ... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "MIDIEvents.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace MIDIEvents +{ + +// Build a generic MIDI event +uint32 Event(EventType eventType, uint8 midiChannel, uint8 dataByte1, uint8 dataByte2) +{ + return (eventType << 4) | (midiChannel & 0x0F) | (dataByte1 << 8) | (dataByte2 << 16); +} + + +// Build a MIDI CC event +uint32 CC(MidiCC midiCC, uint8 midiChannel, uint8 param) +{ + return Event(evControllerChange, midiChannel, static_cast<uint8>(midiCC), param); +} + + +// Build a MIDI Pitchbend event +uint32 PitchBend(uint8 midiChannel, uint16 bendAmount) +{ + return Event(evPitchBend, midiChannel, static_cast<uint8>(bendAmount & 0x7F), static_cast<uint8>(bendAmount >> 7)); +} + + +// Build a MIDI Program Change event +uint32 ProgramChange(uint8 midiChannel, uint8 program) +{ + return Event(evProgramChange, midiChannel, program, 0); +} + + +// Build a MIDI Note Off event +uint32 NoteOff(uint8 midiChannel, uint8 note, uint8 velocity) +{ + return Event(evNoteOff, midiChannel, note, velocity); +} + + +// Build a MIDI Note On event +uint32 NoteOn(uint8 midiChannel, uint8 note, uint8 velocity) +{ + return Event(evNoteOn, midiChannel, note, velocity); +} + + +// Build a MIDI System Event +uint8 System(SystemEvent eventType) +{ + return static_cast<uint8>((evSystem << 4) | eventType); +} + + +// Get MIDI channel from a MIDI event +uint8 GetChannelFromEvent(uint32 midiMsg) +{ + return static_cast<uint8>((midiMsg & 0xF)); +} + + +// Get MIDI Event type from a MIDI event +EventType GetTypeFromEvent(uint32 midiMsg) +{ + return static_cast<EventType>(((midiMsg >> 4) & 0xF)); +} + + +// Get first data byte from a MIDI event +uint8 GetDataByte1FromEvent(uint32 midiMsg) +{ + return static_cast<uint8>(((midiMsg >> 8) & 0xFF)); +} + + +// Get second data byte from a MIDI event +uint8 GetDataByte2FromEvent(uint32 midiMsg) +{ + return static_cast<uint8>(((midiMsg >> 16) & 0xFF)); +} + + +// Get the length of a MIDI event in bytes +uint8 GetEventLength(uint8 firstByte) +{ + uint8 msgSize = 3; + switch(firstByte & 0xF0) + { + case 0xC0: + case 0xD0: + msgSize = 2; + break; + case 0xF0: + switch(firstByte) + { + case 0xF1: + case 0xF3: + msgSize = 2; + break; + case 0xF2: + msgSize = 3; + break; + default: + msgSize = 1; + break; + } + break; + } + return msgSize; +} + + +// MIDI CC Names +const char* const MidiCCNames[MIDICC_end + 1] = +{ + "BankSelect [Coarse]", //0 + "ModulationWheel [Coarse]", //1 + "Breathcontroller [Coarse]", //2 + "", //3 + "FootPedal [Coarse]", //4 + "PortamentoTime [Coarse]", //5 + "DataEntry [Coarse]", //6 + "Volume [Coarse]", //7 + "Balance [Coarse]", //8 + "", //9 + "Panposition [Coarse]", //10 + "Expression [Coarse]", //11 + "EffectControl1 [Coarse]", //12 + "EffectControl2 [Coarse]", //13 + "", //14 + "", //15 + "GeneralPurposeSlider1", //16 + "GeneralPurposeSlider2", //17 + "GeneralPurposeSlider3", //18 + "GeneralPurposeSlider4", //19 + "", //20 + "", //21 + "", //22 + "", //23 + "", //24 + "", //25 + "", //26 + "", //27 + "", //28 + "", //29 + "", //30 + "", //31 + "BankSelect [Fine]", //32 + "ModulationWheel [Fine]", //33 + "Breathcontroller [Fine]", //34 + "", //35 + "FootPedal [Fine]", //36 + "PortamentoTime [Fine]", //37 + "DataEntry [Fine]", //38 + "Volume [Fine]", //39 + "Balance [Fine]", //40 + "", //41 + "Panposition [Fine]", //42 + "Expression [Fine]", //43 + "EffectControl1 [Fine]", //44 + "EffectControl2 [Fine]", //45 + "", //46 + "", //47 + "", //48 + "", //49 + "", //50 + "", //51 + "", //52 + "", //53 + "", //54 + "", //55 + "", //56 + "", //57 + "", //58 + "", //59 + "", //60 + "", //61 + "", //62 + "", //63 + "HoldPedal [OnOff]", //64 + "Portamento [OnOff]", //65 + "SustenutoPedal [OnOff]", //66 + "SoftPedal [OnOff]", //67 + "LegatoPedal [OnOff]", //68 + "Hold2Pedal [OnOff]", //69 + "SoundVariation", //70 + "SoundTimbre", //71 + "SoundReleaseTime", //72 + "SoundAttackTime", //73 + "SoundBrightness", //74 + "SoundControl6", //75 + "SoundControl7", //76 + "SoundControl8", //77 + "SoundControl9", //78 + "SoundControl10", //79 + "GeneralPurposeButton1 [OnOff]",//80 + "GeneralPurposeButton2 [OnOff]",//81 + "GeneralPurposeButton3 [OnOff]",//82 + "GeneralPurposeButton4 [OnOff]",//83 + "", //84 + "", //85 + "", //86 + "", //87 + "", //88 + "", //89 + "", //90 + "EffectsLevel", //91 + "TremoloLevel", //92 + "ChorusLevel", //93 + "CelesteLevel", //94 + "PhaserLevel", //95 + "DataButtonIncrement", //96 + "DataButtonDecrement", //97 + "NonRegisteredParameter [Fine]",//98 + "NonRegisteredParameter [Coarse]",//99 + "RegisteredParameter [Fine]", //100 + "RegisteredParameter [Coarse]", //101 + "", //102 + "", //103 + "", //104 + "", //105 + "", //106 + "", //107 + "", //108 + "", //109 + "", //110 + "", //111 + "", //112 + "", //113 + "", //114 + "", //115 + "", //116 + "", //117 + "", //118 + "", //119 + "AllSoundOff", //120 + "AllControllersOff", //121 + "LocalKeyboard [OnOff]", //122 + "AllNotesOff", //123 + "OmniModeOff", //124 + "OmniModeOn", //125 + "MonoOperation", //126 + "PolyOperation", //127 +}; + + +} // End namespace + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.h b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.h new file mode 100644 index 00000000..1b0dc8ea --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIEvents.h @@ -0,0 +1,170 @@ +/* + * MIDIEvents.h + * ------------ + * Purpose: MIDI event handling, event lists, ... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// MIDI related enums and helper functions +namespace MIDIEvents +{ + + // MIDI Event Types + enum EventType + { + evNoteOff = 0x8, // Note Off event + evNoteOn = 0x9, // Note On event + evPolyAftertouch = 0xA, // Poly Aftertouch / Poly Pressure event + evControllerChange = 0xB, // Controller Change (see MidiCC enum) + evProgramChange = 0xC, // Program Change + evChannelAftertouch = 0xD, // Channel Aftertouch + evPitchBend = 0xE, // Pitchbend event (see PitchBend enum) + evSystem = 0xF, // System event (see SystemEvent enum) + }; + + // System Events (Fx ...) + enum SystemEvent + { + sysExStart = 0x0, // Begin of System Exclusive message + sysQuarterFrame = 0x1, // Quarter Frame Message + sysPositionPointer = 0x2, // Song Position Pointer + sysSongSelect = 0x3, // Song Select + sysTuneRequest = 0x6, // Tune Request + sysExEnd = 0x7, // End of System Exclusive message + sysMIDIClock = 0x8, // MIDI Clock event + sysMIDITick = 0x9, // MIDI Tick event + sysStart = 0xA, // Start Song + sysContinue = 0xB, // Continue Song + sysStop = 0xC, // Stop Song + sysActiveSense = 0xE, // Active Sense Message + sysReset = 0xF, // Reset Device + }; + + // MIDI Pitchbend Constants + enum PitchBend + { + pitchBendMin = 0x00, + pitchBendCentre = 0x2000, + pitchBendMax = 0x3FFF + }; + + // MIDI Continuous Controller Codes + // http://home.roadrunner.com/~jgglatt/tech/midispec/ctllist.htm + enum MidiCC + { + MIDICC_start = 0, + MIDICC_BankSelect_Coarse = MIDICC_start, + MIDICC_ModulationWheel_Coarse = 1, + MIDICC_Breathcontroller_Coarse = 2, + MIDICC_FootPedal_Coarse = 4, + MIDICC_PortamentoTime_Coarse = 5, + MIDICC_DataEntry_Coarse = 6, + MIDICC_Volume_Coarse = 7, + MIDICC_Balance_Coarse = 8, + MIDICC_Panposition_Coarse = 10, + MIDICC_Expression_Coarse = 11, + MIDICC_EffectControl1_Coarse = 12, + MIDICC_EffectControl2_Coarse = 13, + MIDICC_GeneralPurposeSlider1 = 16, + MIDICC_GeneralPurposeSlider2 = 17, + MIDICC_GeneralPurposeSlider3 = 18, + MIDICC_GeneralPurposeSlider4 = 19, + MIDICC_BankSelect_Fine = 32, + MIDICC_ModulationWheel_Fine = 33, + MIDICC_Breathcontroller_Fine = 34, + MIDICC_FootPedal_Fine = 36, + MIDICC_PortamentoTime_Fine = 37, + MIDICC_DataEntry_Fine = 38, + MIDICC_Volume_Fine = 39, + MIDICC_Balance_Fine = 40, + MIDICC_Panposition_Fine = 42, + MIDICC_Expression_Fine = 43, + MIDICC_EffectControl1_Fine = 44, + MIDICC_EffectControl2_Fine = 45, + MIDICC_HoldPedal_OnOff = 64, + MIDICC_Portamento_OnOff = 65, + MIDICC_SustenutoPedal_OnOff = 66, + MIDICC_SoftPedal_OnOff = 67, + MIDICC_LegatoPedal_OnOff = 68, + MIDICC_Hold2Pedal_OnOff = 69, + MIDICC_SoundVariation = 70, + MIDICC_SoundTimbre = 71, + MIDICC_SoundReleaseTime = 72, + MIDICC_SoundAttackTime = 73, + MIDICC_SoundBrightness = 74, + MIDICC_SoundControl6 = 75, + MIDICC_SoundControl7 = 76, + MIDICC_SoundControl8 = 77, + MIDICC_SoundControl9 = 78, + MIDICC_SoundControl10 = 79, + MIDICC_GeneralPurposeButton1_OnOff = 80, + MIDICC_GeneralPurposeButton2_OnOff = 81, + MIDICC_GeneralPurposeButton3_OnOff = 82, + MIDICC_GeneralPurposeButton4_OnOff = 83, + MIDICC_EffectsLevel = 91, + MIDICC_TremoloLevel = 92, + MIDICC_ChorusLevel = 93, + MIDICC_CelesteLevel = 94, + MIDICC_PhaserLevel = 95, + MIDICC_DataButtonincrement = 96, + MIDICC_DataButtondecrement = 97, + MIDICC_NonRegisteredParameter_Fine = 98, + MIDICC_NonRegisteredParameter_Coarse = 99, + MIDICC_RegisteredParameter_Fine = 100, + MIDICC_RegisteredParameter_Coarse = 101, + MIDICC_AllSoundOff = 120, + MIDICC_AllControllersOff = 121, + MIDICC_LocalKeyboard_OnOff = 122, + MIDICC_AllNotesOff = 123, + MIDICC_OmniModeOff = 124, + MIDICC_OmniModeOn = 125, + MIDICC_MonoOperation = 126, + MIDICC_PolyOperation = 127, + MIDICC_end = MIDICC_PolyOperation, + }; + + // MIDI CC Names + extern const char* const MidiCCNames[MIDICC_end + 1]; // Charset::UTF8 + + // Build a generic MIDI event + uint32 Event(EventType eventType, uint8 midiChannel, uint8 dataByte1, uint8 dataByte2); + // Build a MIDI CC event + uint32 CC(MidiCC midiCC, uint8 midiChannel, uint8 param); + // Build a MIDI Pitchbend event + uint32 PitchBend(uint8 midiChannel, uint16 bendAmount); + // Build a MIDI Program Change event + uint32 ProgramChange(uint8 midiChannel, uint8 program); + // Build a MIDI Note Off event + uint32 NoteOff(uint8 midiChannel, uint8 note, uint8 velocity); + // Build a MIDI Note On event + uint32 NoteOn(uint8 midiChannel, uint8 note, uint8 velocity); + // Build a MIDI System Event + uint8 System(SystemEvent eventType); + + // Get MIDI channel from a MIDI event + uint8 GetChannelFromEvent(uint32 midiMsg); + // Get MIDI Event type from a MIDI event + EventType GetTypeFromEvent(uint32 midiMsg); + // Get first data byte from a MIDI event + uint8 GetDataByte1FromEvent(uint32 midiMsg); + // Get second data byte from a MIDI event + uint8 GetDataByte2FromEvent(uint32 midiMsg); + + // Get the length of a MIDI event in bytes + uint8 GetEventLength(uint8 firstByte); + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.cpp new file mode 100644 index 00000000..150e1416 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.cpp @@ -0,0 +1,397 @@ +/* + * MIDIMacros.cpp + * -------------- + * Purpose: Helper functions / classes for MIDI Macro functionality. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "MIDIMacros.h" +#include "../soundlib/MIDIEvents.h" + +#ifdef MODPLUG_TRACKER +#include "Sndfile.h" +#include "plugins/PlugInterface.h" +#endif // MODPLUG_TRACKER + +OPENMPT_NAMESPACE_BEGIN + +ParameteredMacro MIDIMacroConfig::GetParameteredMacroType(uint32 macroIndex) const +{ + const std::string macro = SFx[macroIndex].NormalizedString(); + + for(uint32 i = 0; i < kSFxMax; i++) + { + ParameteredMacro sfx = static_cast<ParameteredMacro>(i); + if(sfx != kSFxCustom) + { + if(macro == CreateParameteredMacro(sfx)) + return sfx; + } + } + + // Special macros with additional "parameter": + if(macro.size() == 5 && macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_start)) >= 0 && macro.compare(CreateParameteredMacro(kSFxCC, MIDIEvents::MIDICC_end)) <= 0) + return kSFxCC; + if(macro.size() == 7 && macro.compare(CreateParameteredMacro(kSFxPlugParam, 0)) >= 0 && macro.compare(CreateParameteredMacro(kSFxPlugParam, 0x17F)) <= 0) + return kSFxPlugParam; + + return kSFxCustom; // custom / unknown +} + + +// Retrieve Zxx (Z80-ZFF) type from current macro configuration +FixedMacro MIDIMacroConfig::GetFixedMacroType() const +{ + // Compare with all possible preset patterns + for(uint32 i = 0; i < kZxxMax; i++) + { + FixedMacro zxx = static_cast<FixedMacro>(i); + if(zxx != kZxxCustom) + { + // Prepare macro pattern to compare + decltype(Zxx) fixedMacros{}; + CreateFixedMacro(fixedMacros, zxx); + if(fixedMacros == Zxx) + return zxx; + } + } + return kZxxCustom; // Custom setup +} + + +void MIDIMacroConfig::CreateParameteredMacro(Macro ¶meteredMacro, ParameteredMacro macroType, int subType) const +{ + switch(macroType) + { + case kSFxUnused: parameteredMacro = ""; break; + case kSFxCutoff: parameteredMacro = "F0F000z"; break; + case kSFxReso: parameteredMacro = "F0F001z"; break; + case kSFxFltMode: parameteredMacro = "F0F002z"; break; + case kSFxDryWet: parameteredMacro = "F0F003z"; break; + case kSFxCC: parameteredMacro = MPT_AFORMAT("Bc{}z")(mpt::afmt::HEX0<2>(subType & 0x7F)); break; + case kSFxPlugParam: parameteredMacro = MPT_AFORMAT("F0F{}z")(mpt::afmt::HEX0<3>(std::min(subType, 0x17F) + 0x80)); break; + case kSFxChannelAT: parameteredMacro = "Dcz"; break; + case kSFxPolyAT: parameteredMacro = "Acnz"; break; + case kSFxPitch: parameteredMacro = "Ec00z"; break; + case kSFxProgChange: parameteredMacro = "Ccz"; break; + case kSFxCustom: + default: + MPT_ASSERT_NOTREACHED(); + break; + } +} + + +std::string MIDIMacroConfig::CreateParameteredMacro(ParameteredMacro macroType, int subType) const +{ + Macro parameteredMacro{}; + CreateParameteredMacro(parameteredMacro, macroType, subType); + return parameteredMacro; +} + + +// Create Zxx (Z80 - ZFF) from preset +void MIDIMacroConfig::CreateFixedMacro(std::array<Macro, kZxxMacros> &fixedMacros, FixedMacro macroType) const +{ + for(uint32 i = 0; i < kZxxMacros; i++) + { + uint32 param = i; + switch(macroType) + { + case kZxxUnused: + fixedMacros[i] = ""; + break; + case kZxxReso4Bit: + param = i * 8; + if(i < 16) + fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param)); + else + fixedMacros[i] = ""; + break; + case kZxxReso7Bit: + fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxCutoff: + fixedMacros[i] = MPT_AFORMAT("F0F000{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxFltMode: + fixedMacros[i] = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxResoFltMode: + param = (i & 0x0F) * 8; + if(i < 16) + fixedMacros[i] = MPT_AFORMAT("F0F001{}")(mpt::afmt::HEX0<2>(param)); + else if(i < 32) + fixedMacros[i] = MPT_AFORMAT("F0F002{}")(mpt::afmt::HEX0<2>(param)); + else + fixedMacros[i] = ""; + break; + case kZxxChannelAT: + fixedMacros[i] = MPT_AFORMAT("Dc{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxPolyAT: + fixedMacros[i] = MPT_AFORMAT("Acn{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxPitch: + fixedMacros[i] = MPT_AFORMAT("Ec00{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxProgChange: + fixedMacros[i] = MPT_AFORMAT("Cc{}")(mpt::afmt::HEX0<2>(param)); + break; + case kZxxCustom: + default: + MPT_ASSERT_NOTREACHED(); + continue; + } + } +} + + +bool MIDIMacroConfig::operator== (const MIDIMacroConfig &other) const +{ + return std::equal(begin(), end(), other.begin()); +} + + +#ifdef MODPLUG_TRACKER + +// Returns macro description including plugin parameter / MIDI CC information +CString MIDIMacroConfig::GetParameteredMacroName(uint32 macroIndex, IMixPlugin *plugin) const +{ + const ParameteredMacro macroType = GetParameteredMacroType(macroIndex); + + switch(macroType) + { + case kSFxPlugParam: + { + const int param = MacroToPlugParam(macroIndex); + CString formattedName; + formattedName.Format(_T("Param %d"), param); +#ifndef NO_PLUGINS + if(plugin != nullptr) + { + CString paramName = plugin->GetParamName(param); + if(!paramName.IsEmpty()) + { + formattedName += _T(" (") + paramName + _T(")"); + } + } else +#else + MPT_UNREFERENCED_PARAMETER(plugin); +#endif // NO_PLUGINS + { + formattedName += _T(" (N/A)"); + } + return formattedName; + } + + case kSFxCC: + { + CString formattedCC; + formattedCC.Format(_T("MIDI CC %d"), MacroToMidiCC(macroIndex)); + return formattedCC; + } + + default: + return GetParameteredMacroName(macroType); + } +} + + +// Returns generic macro description. +CString MIDIMacroConfig::GetParameteredMacroName(ParameteredMacro macroType) const +{ + switch(macroType) + { + case kSFxUnused: return _T("Unused"); + case kSFxCutoff: return _T("Set Filter Cutoff"); + case kSFxReso: return _T("Set Filter Resonance"); + case kSFxFltMode: return _T("Set Filter Mode"); + case kSFxDryWet: return _T("Set Plugin Dry/Wet Ratio"); + case kSFxPlugParam: return _T("Control Plugin Parameter..."); + case kSFxCC: return _T("MIDI CC..."); + case kSFxChannelAT: return _T("Channel Aftertouch"); + case kSFxPolyAT: return _T("Polyphonic Aftertouch"); + case kSFxPitch: return _T("Pitch Bend"); + case kSFxProgChange: return _T("MIDI Program Change"); + case kSFxCustom: + default: return _T("Custom"); + } +} + + +// Returns generic macro description. +CString MIDIMacroConfig::GetFixedMacroName(FixedMacro macroType) const +{ + switch(macroType) + { + case kZxxUnused: return _T("Unused"); + case kZxxReso4Bit: return _T("Z80 - Z8F controls Resonant Filter Resonance"); + case kZxxReso7Bit: return _T("Z80 - ZFF controls Resonant Filter Resonance"); + case kZxxCutoff: return _T("Z80 - ZFF controls Resonant Filter Cutoff"); + case kZxxFltMode: return _T("Z80 - ZFF controls Resonant Filter Mode"); + case kZxxResoFltMode: return _T("Z80 - Z9F controls Resonance + Filter Mode"); + case kZxxChannelAT: return _T("Z80 - ZFF controls Channel Aftertouch"); + case kZxxPolyAT: return _T("Z80 - ZFF controls Polyphonic Aftertouch"); + case kZxxPitch: return _T("Z80 - ZFF controls Pitch Bend"); + case kZxxProgChange: return _T("Z80 - ZFF controls MIDI Program Change"); + case kZxxCustom: + default: return _T("Custom"); + } +} + + +PlugParamIndex MIDIMacroConfig::MacroToPlugParam(uint32 macroIndex) const +{ + const std::string macro = SFx[macroIndex].NormalizedString(); + + PlugParamIndex code = 0; + const char *param = macro.c_str(); + param += 4; + if ((param[0] >= '0') && (param[0] <= '9')) code = (param[0] - '0') << 4; else + if ((param[0] >= 'A') && (param[0] <= 'F')) code = (param[0] - 'A' + 0x0A) << 4; + if ((param[1] >= '0') && (param[1] <= '9')) code += (param[1] - '0'); else + if ((param[1] >= 'A') && (param[1] <= 'F')) code += (param[1] - 'A' + 0x0A); + + if (macro.size() >= 4 && macro[3] == '0') + return (code - 128); + else + return (code + 128); +} + + +int MIDIMacroConfig::MacroToMidiCC(uint32 macroIndex) const +{ + const std::string macro = SFx[macroIndex].NormalizedString(); + + int code = 0; + const char *param = macro.c_str(); + param += 2; + if ((param[0] >= '0') && (param[0] <= '9')) code = (param[0] - '0') << 4; else + if ((param[0] >= 'A') && (param[0] <= 'F')) code = (param[0] - 'A' + 0x0A) << 4; + if ((param[1] >= '0') && (param[1] <= '9')) code += (param[1] - '0'); else + if ((param[1] >= 'A') && (param[1] <= 'F')) code += (param[1] - 'A' + 0x0A); + + return code; +} + + +int MIDIMacroConfig::FindMacroForParam(PlugParamIndex param) const +{ + for(int macroIndex = 0; macroIndex < kSFxMacros; macroIndex++) + { + if(GetParameteredMacroType(macroIndex) == kSFxPlugParam && MacroToPlugParam(macroIndex) == param) + { + return macroIndex; + } + } + return -1; +} + +#endif // MODPLUG_TRACKER + + +// Check if the MIDI Macro configuration used is the default one, +// i.e. the configuration that is assumed when loading a file that has no macros embedded. +bool MIDIMacroConfig::IsMacroDefaultSetupUsed() const +{ + return *this == MIDIMacroConfig{}; +} + + +// Reset MIDI macro config to default values. +void MIDIMacroConfig::Reset() +{ + std::fill(begin(), end(), Macro{}); + + Global[MIDIOUT_START] = "FF"; + Global[MIDIOUT_STOP] = "FC"; + Global[MIDIOUT_NOTEON] = "9c n v"; + Global[MIDIOUT_NOTEOFF] = "9c n 0"; + Global[MIDIOUT_PROGRAM] = "Cc p"; + // SF0: Z00-Z7F controls cutoff + CreateParameteredMacro(0, kSFxCutoff); + // Z80-Z8F controls resonance + CreateFixedMacro(kZxxReso4Bit); +} + + +// Clear all Zxx macros so that they do nothing. +void MIDIMacroConfig::ClearZxxMacros() +{ + std::fill(SFx.begin(), SFx.end(), Macro{}); + std::fill(Zxx.begin(), Zxx.end(), Macro{}); +} + + +// Sanitize all macro config strings. +void MIDIMacroConfig::Sanitize() +{ + for(auto ¯o : *this) + { + macro.Sanitize(); + } +} + + +// Fix old-format (not conforming to IT's MIDI macro definitions) MIDI config strings. +void MIDIMacroConfig::UpgradeMacros() +{ + for(auto ¯o : *this) + { + macro.UpgradeLegacyMacro(); + } +} + + +// Normalize by removing blanks and other unwanted characters from macro strings for internal usage. +std::string MIDIMacroConfig::Macro::NormalizedString() const +{ + std::string sanitizedMacro = *this; + + std::string::size_type pos; + while((pos = sanitizedMacro.find_first_not_of("0123456789ABCDEFabchmnopsuvxyz")) != std::string::npos) + { + sanitizedMacro.erase(pos, 1); + } + + return sanitizedMacro; +} + + +void MIDIMacroConfig::Macro::Sanitize() noexcept +{ + m_data.back() = '\0'; + const auto length = Length(); + std::fill(m_data.begin() + length, m_data.end(), '\0'); + for(size_t i = 0; i < length; i++) + { + if(m_data[i] < 32 || m_data[i] >= 127) + m_data[i] = ' '; + } +} + + +void MIDIMacroConfig::Macro::UpgradeLegacyMacro() noexcept +{ + for(auto &c : m_data) + { + if(c >= 'a' && c <= 'f') // Both A-F and a-f were treated as hex constants + { + c = c - 'a' + 'A'; + } else if(c == 'K' || c == 'k') // Channel was K or k + { + c = 'c'; + } else if(c == 'X' || c == 'x' || c == 'Y' || c == 'y') // Those were pointless + { + c = 'z'; + } + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.h b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.h new file mode 100644 index 00000000..935660a9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MIDIMacros.h @@ -0,0 +1,228 @@ +/* + * MIDIMacros.h + * ------------ + * Purpose: Helper functions / classes for MIDI Macro functionality. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/base/Endian.hpp" + +OPENMPT_NAMESPACE_BEGIN + +enum +{ + kGlobalMacros = 9, // Number of global macros + kSFxMacros = 16, // Number of parametered macros + kZxxMacros = 128, // Number of fixed macros + kMacroLength = 32, // Max number of chars per macro +}; + +OPENMPT_NAMESPACE_END + +#ifdef MODPLUG_TRACKER +#include "plugins/PluginStructs.h" +#endif // MODPLUG_TRACKER + +OPENMPT_NAMESPACE_BEGIN + +// Parametered macro presets +enum ParameteredMacro +{ + kSFxUnused = 0, + kSFxCutoff, // Z00 - Z7F controls resonant filter cutoff + kSFxReso, // Z00 - Z7F controls resonant filter resonance + kSFxFltMode, // Z00 - Z7F controls resonant filter mode (lowpass / highpass) + kSFxDryWet, // Z00 - Z7F controls plugin Dry / Wet ratio + kSFxPlugParam, // Z00 - Z7F controls a plugin parameter + kSFxCC, // Z00 - Z7F controls MIDI CC + kSFxChannelAT, // Z00 - Z7F controls Channel Aftertouch + kSFxPolyAT, // Z00 - Z7F controls Poly Aftertouch + kSFxPitch, // Z00 - Z7F controls Pitch Bend + kSFxProgChange, // Z00 - Z7F controls MIDI Program Change + kSFxCustom, + + kSFxMax +}; + + +// Fixed macro presets +enum FixedMacro +{ + kZxxUnused = 0, + kZxxReso4Bit, // Z80 - Z8F controls resonant filter resonance + kZxxReso7Bit, // Z80 - ZFF controls resonant filter resonance + kZxxCutoff, // Z80 - ZFF controls resonant filter cutoff + kZxxFltMode, // Z80 - ZFF controls resonant filter mode (lowpass / highpass) + kZxxResoFltMode, // Z80 - Z9F controls resonance + filter mode + kZxxChannelAT, // Z80 - ZFF controls Channel Aftertouch + kZxxPolyAT, // Z80 - ZFF controls Poly Aftertouch + kZxxPitch, // Z80 - ZFF controls Pitch Bend + kZxxProgChange, // Z80 - ZFF controls MIDI Program Change + kZxxCustom, + + kZxxMax +}; + + +// Global macro types +enum GlobalMacro +{ + MIDIOUT_START = 0, + MIDIOUT_STOP, + MIDIOUT_TICK, + MIDIOUT_NOTEON, + MIDIOUT_NOTEOFF, + MIDIOUT_VOLUME, + MIDIOUT_PAN, + MIDIOUT_BANKSEL, + MIDIOUT_PROGRAM, +}; + + +struct MIDIMacroConfigData +{ + struct Macro + { + public: + Macro &operator=(const Macro &other) = default; + Macro &operator=(const std::string_view other) noexcept + { + const size_t copyLength = std::min({m_data.size() - 1u, other.size(), other.find('\0')}); + std::copy(other.begin(), other.begin() + copyLength, m_data.begin()); + m_data[copyLength] = '\0'; + Sanitize(); + return *this; + } + + bool operator==(const Macro &other) const noexcept + { + return m_data == other.m_data; // Don't care about data past null-terminator as operator= and Sanitize() ensure there is no data behind it. + } + bool operator!=(const Macro &other) const noexcept + { + return !(*this == other); + } + + operator mpt::span<const char>() const noexcept + { + return {m_data.data(), Length()}; + } + operator std::string_view() const noexcept + { + return {m_data.data(), Length()}; + } + operator std::string() const + { + return {m_data.data(), Length()}; + } + + MPT_CONSTEXPR20_FUN size_t Length() const noexcept + { + return static_cast<size_t>(std::distance(m_data.begin(), std::find(m_data.begin(), m_data.end(), '\0'))); + } + + MPT_CONSTEXPR20_FUN void Clear() noexcept + { + m_data.fill('\0'); + } + + // Remove blanks and other unwanted characters from macro strings for internal usage. + std::string NormalizedString() const; + + void Sanitize() noexcept; + void UpgradeLegacyMacro() noexcept; + + private: + std::array<char, kMacroLength> m_data; + }; + + std::array<Macro, kGlobalMacros> Global; + std::array<Macro, kSFxMacros> SFx; // Parametered macros for Z00...Z7F + std::array<Macro, kZxxMacros> Zxx; // Fixed macros Z80...ZFF + + constexpr Macro *begin() noexcept {return Global.data(); } + constexpr const Macro *begin() const noexcept { return Global.data(); } + constexpr Macro *end() noexcept { return Zxx.data() + Zxx.size(); } + constexpr const Macro *end() const noexcept { return Zxx.data() + Zxx.size(); } +}; + +// This is directly written to files, so the size must be correct! +MPT_BINARY_STRUCT(MIDIMacroConfigData::Macro, 32) +MPT_BINARY_STRUCT(MIDIMacroConfigData, 4896) + +class MIDIMacroConfig : public MIDIMacroConfigData +{ + +public: + + MIDIMacroConfig() { Reset(); } + + // Get macro type from a macro string + ParameteredMacro GetParameteredMacroType(uint32 macroIndex) const; + FixedMacro GetFixedMacroType() const; + + // Create a new macro +protected: + void CreateParameteredMacro(Macro ¶meteredMacro, ParameteredMacro macroType, int subType) const; +public: + void CreateParameteredMacro(uint32 macroIndex, ParameteredMacro macroType, int subType = 0) + { + if(macroIndex < std::size(SFx)) + CreateParameteredMacro(SFx[macroIndex], macroType, subType); + } + std::string CreateParameteredMacro(ParameteredMacro macroType, int subType = 0) const; + +protected: + void CreateFixedMacro(std::array<Macro, kZxxMacros> &fixedMacros, FixedMacro macroType) const; +public: + void CreateFixedMacro(FixedMacro macroType) + { + CreateFixedMacro(Zxx, macroType); + } + + bool operator==(const MIDIMacroConfig &other) const; + bool operator!=(const MIDIMacroConfig &other) const { return !(*this == other); } + +#ifdef MODPLUG_TRACKER + + // Translate macro type or macro string to macro name + CString GetParameteredMacroName(uint32 macroIndex, IMixPlugin *plugin = nullptr) const; + CString GetParameteredMacroName(ParameteredMacro macroType) const; + CString GetFixedMacroName(FixedMacro macroType) const; + + // Extract information from a parametered macro string. + PlugParamIndex MacroToPlugParam(uint32 macroIndex) const; + int MacroToMidiCC(uint32 macroIndex) const; + + // Check if any macro can automate a given plugin parameter + int FindMacroForParam(PlugParamIndex param) const; + +#endif // MODPLUG_TRACKER + + // Check if a given set of macros is the default IT macro set. + bool IsMacroDefaultSetupUsed() const; + + // Reset MIDI macro config to default values. + void Reset(); + + // Clear all Zxx macros so that they do nothing. + void ClearZxxMacros(); + + // Sanitize all macro config strings. + void Sanitize(); + + // Fix old-format (not conforming to IT's MIDI macro definitions) MIDI config strings. + void UpgradeMacros(); +}; + +static_assert(sizeof(MIDIMacroConfig) == sizeof(MIDIMacroConfigData)); // this is directly written to files, so the size must be correct! + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.cpp new file mode 100644 index 00000000..16547539 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.cpp @@ -0,0 +1,113 @@ +/* + * MPEGFrame.cpp + * ------------- + * Purpose: Basic MPEG frame parsing functionality + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "MPEGFrame.h" +#include "../common/FileReader.h" + +OPENMPT_NAMESPACE_BEGIN + +// Samples per frame - for each MPEG version and all three layers +static constexpr uint16 samplesPerFrame[2][3] = +{ + { 384, 1152, 1152 }, // MPEG 1 + { 384, 1152, 576 } // MPEG 2 / 2.5 +}; +// Bit rates for each MPEG version and all three layers +static constexpr uint16 bitRates[2][3][15] = +{ + // MPEG 1 + { + { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448 }, // Layer 1 + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384 }, // Layer 2 + { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 } // Layer 3 + }, + // MPEG 2 / 2.5 + { + { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256 }, // Layer 1 + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 }, // Layer 2 + { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160 } // Layer 3 + } +}; +// Sampling rates for each MPEG version and all three layers +static constexpr uint16 samplingRates[4][3] = +{ + { 11025, 12000, 8000 }, // MPEG 2.5 + { 0, 0, 0 }, // Invalid + { 22050, 24000, 16000 }, // MPEG 2 + { 44100, 48000, 32000 } // MPEG 1 +}; +// Samples per Frame / 8 +static constexpr uint8 mpegCoefficients[2][3] = +{ + { 12, 144, 144 }, // MPEG 1 + { 12, 144, 72 } // MPEG 2 / 2.5 +}; +// Side info size = Offset in frame where Xing/Info magic starts +static constexpr uint8 sideInfoSize[2][2] = +{ + { 17, 32 }, // MPEG 1 + { 9, 17 } // MPEG 2 / 2.5 +}; + + +bool MPEGFrame::IsMPEGHeader(const uint8 (&header)[3]) +{ + return header[0] == 0xFF && (header[1] & 0xE0) == 0xE0 // Sync + && (header[1] & 0x18) != 0x08 // Invalid MPEG version + && (header[1] & 0x06) != 0x00 // Invalid MPEG layer + && (header[2] & 0x0C) != 0x0C // Invalid frequency + && (header[2] & 0xF0) != 0xF0; // Invalid bitrate +} + + +MPEGFrame::MPEGFrame(FileReader &file) + : frameSize(0) + , numSamples(0) + , isValid(false) + , isLAME(false) +{ + uint8 header[4]; + file.ReadArray(header); + + if(!IsMPEGHeader(reinterpret_cast<const uint8(&)[3]>(header))) + return; + + uint8 version = (header[1] & 0x18) >> 3; + uint8 mpeg1 = (version == 3) ? 0 : 1; + uint8 layer = 3 - ((header[1] & 0x06) >> 1); + uint8 bitRate = (header[2] & 0xF0) >> 4; + uint8 sampleRate = (header[2] & 0x0C) >> 2; + uint8 padding = (header[2] & 0x02) >> 1; + bool stereo = ((header[3] & 0xC0) >> 6) != 3; + + isValid = true; + frameSize = (((mpegCoefficients[mpeg1][layer] * (bitRates[mpeg1][layer][bitRate] * 1000) / samplingRates[version][sampleRate]) + padding)) * (layer == 0 ? 4 : 1); + numSamples = samplesPerFrame[mpeg1][layer]; + if(stereo) numSamples *= 2u; + + uint32 lameOffset = sideInfoSize[mpeg1][stereo ? 1 : 0]; + if(frameSize < lameOffset + 8) + return; + + uint8 frame[36]; + file.ReadStructPartial(frame, lameOffset + 4); + // Don't check first two bytes, might be CRC + for(uint32 i = 2; i < lameOffset; i++) + { + if(frame[i] != 0) + return; + } + + // This is all we really need to know for our purposes in the MO3 decoder. + isLAME = !memcmp(frame + lameOffset, "Info", 4) || !memcmp(frame + lameOffset, "Xing", 4); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.h b/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.h new file mode 100644 index 00000000..25c57b0e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MPEGFrame.h @@ -0,0 +1,31 @@ +/* + * MPEGFrame.h + * ----------- + * Purpose: Basic MPEG frame parsing functionality + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/FileReaderFwd.h" + +OPENMPT_NAMESPACE_BEGIN + +class MPEGFrame +{ +public: + uint16 frameSize; // Complete frame size in bytes + uint16 numSamples; // Number of samples in this frame (multiplied by number of channels) + bool isValid; // Is a valid frame at all + bool isLAME; // Has Xing/LAME header + + MPEGFrame(FileReader &file); + static bool IsMPEGHeader(const uint8 (&header)[3]); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Message.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Message.cpp new file mode 100644 index 00000000..c0660732 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Message.cpp @@ -0,0 +1,231 @@ +/* + * Message.cpp + * ----------- + * Purpose: Various functions for processing song messages (allocating, reading from file...) + * Notes : Those functions should offer a rather high level of abstraction compared to + * previous ways of reading the song messages. There are still many things to do, + * though. Future versions of ReadMessage() could e.g. offer charset conversion + * and the code is not yet ready for unicode. + * Some functions for preparing the message text to be written to a file would + * also be handy. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Message.h" +#include "../common/FileReader.h" + +OPENMPT_NAMESPACE_BEGIN + +// Read song message from a mapped file. +// [in] data: pointer to the data in memory that is going to be read +// [in] length: number of characters that should be read. Trailing null characters are automatically removed. +// [in] lineEnding: line ending formatting of the text in memory. +// [out] returns true on success. +bool SongMessage::Read(const std::byte *data, size_t length, LineEnding lineEnding) +{ + const char *str = mpt::byte_cast<const char *>(data); + while(length != 0 && str[length - 1] == '\0') + { + // Ignore trailing null character. + length--; + } + + // Simple line-ending detection algorithm. VERY simple. + if(lineEnding == leAutodetect) + { + size_t nCR = 0, nLF = 0, nCRLF = 0; + // find CRs, LFs and CRLFs + for(size_t i = 0; i < length; i++) + { + char c = str[i]; + + if(c == '\r') + nCR++; + else if(c == '\n') + nLF++; + + if(i && str[i - 1] == '\r' && c == '\n') + nCRLF++; + } + // evaluate findings + if(nCR == nLF && nCR == nCRLF) + lineEnding = leCRLF; + else if(nCR && !nLF) + lineEnding = leCR; + else if(!nCR && nLF) + lineEnding = leLF; + else + lineEnding = leMixed; + } + + size_t finalLength = 0; + // calculate the final amount of characters to be allocated. + for(size_t i = 0; i < length; i++) + { + finalLength++; + if(str[i] == '\r' && lineEnding == leCRLF) + i++; // skip the LF + } + + clear(); + reserve(finalLength); + + for(size_t i = 0; i < length; i++) + { + char c = str[i]; + + switch(c) + { + case '\r': + if(lineEnding != leLF) + c = InternalLineEnding; + else + c = ' '; + if(lineEnding == leCRLF) + i++; // skip the LF + break; + case '\n': + if(lineEnding != leCR && lineEnding != leCRLF) + c = InternalLineEnding; + else + c = ' '; + break; + case '\0': + c = ' '; + break; + default: + break; + } + push_back(c); + } + + return true; +} + + +bool SongMessage::Read(FileReader &file, const size_t length, LineEnding lineEnding) +{ + FileReader::off_t readLength = std::min(static_cast<FileReader::off_t>(length), file.BytesLeft()); + FileReader::PinnedView fileView = file.ReadPinnedView(readLength); + bool success = Read(fileView.data(), fileView.size(), lineEnding); + return success; +} + + +// Read comments with fixed line length from a mapped file. +// [in] data: pointer to the data in memory that is going to be read +// [in] length: number of characters that should be read, not including a possible trailing null terminator (it is automatically appended). +// [in] lineLength: The fixed length of a line. +// [in] lineEndingLength: The padding space between two fixed lines. (there could for example be a null char after every line) +// [out] returns true on success. +bool SongMessage::ReadFixedLineLength(const std::byte *data, const size_t length, const size_t lineLength, const size_t lineEndingLength) +{ + if(lineLength == 0) + return false; + clear(); + reserve(length); + + size_t readPos = 0, writePos = 0; + while(readPos < length) + { + size_t thisLineLength = std::min(lineLength, length - readPos); + append(mpt::byte_cast<const char *>(data) + readPos, thisLineLength); + append(1, InternalLineEnding); + + // Fix weird chars + for(size_t pos = writePos; pos < writePos + thisLineLength; pos++) + { + switch(operator[](pos)) + { + case '\0': + case '\n': + case '\r': + operator[](pos) = ' '; + break; + } + + } + + readPos += thisLineLength + std::min(lineEndingLength, length - readPos - thisLineLength); + writePos += thisLineLength + 1; + } + return true; +} + + +bool SongMessage::ReadFixedLineLength(FileReader &file, const size_t length, const size_t lineLength, const size_t lineEndingLength) +{ + FileReader::off_t readLength = std::min(static_cast<FileReader::off_t>(length), file.BytesLeft()); + FileReader::PinnedView fileView = file.ReadPinnedView(readLength); + bool success = ReadFixedLineLength(fileView.data(), fileView.size(), lineLength, lineEndingLength); + return success; +} + + +// Retrieve song message. +// [in] lineEnding: line ending formatting of the text in memory. +// [out] returns formatted song message. +std::string SongMessage::GetFormatted(const LineEnding lineEnding) const +{ + std::string comments; + comments.reserve(length()); + for(auto c : *this) + { + if(c == InternalLineEnding) + { + switch(lineEnding) + { + case leCR: + comments.push_back('\r'); + break; + case leCRLF: + comments.push_back('\r'); + comments.push_back('\n'); + break; + case leLF: + comments.push_back('\n'); + break; + default: + comments.push_back('\r'); + break; + } + } else + { + comments.push_back(c); + } + } + return comments; +} + + +bool SongMessage::SetFormatted(std::string message, LineEnding lineEnding) +{ + MPT_ASSERT(lineEnding == leLF || lineEnding == leCR || lineEnding == leCRLF); + switch(lineEnding) + { + case leLF: + message = mpt::replace(message, std::string("\n"), std::string(1, InternalLineEnding)); + break; + case leCR: + message = mpt::replace(message, std::string("\r"), std::string(1, InternalLineEnding)); + break; + case leCRLF: + message = mpt::replace(message, std::string("\r\n"), std::string(1, InternalLineEnding)); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + if(message == *this) + { + return false; + } + assign(std::move(message)); + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Message.h b/Src/external_dependencies/openmpt-trunk/soundlib/Message.h new file mode 100644 index 00000000..76c98db0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Message.h @@ -0,0 +1,71 @@ +/* + * Message.h + * --------- + * Purpose: Various functions for processing song messages (allocating, reading from file...) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <string> + +#include "../common/FileReaderFwd.h" + +OPENMPT_NAMESPACE_BEGIN + +class SongMessage : public std::string +{ +public: + + // Line ending types (for reading song messages from module files) + enum LineEnding + { + leCR, // Carriage Return (0x0D, \r) + leLF, // Line Feed (0x0A \n) + leCRLF, // Carriage Return, Line Feed (0x0D0A, \r\n) + leMixed, // It is not defined whether Carriage Return or Line Feed is the actual line ending. Both are accepted. + leAutodetect, // Detect suitable line ending + }; + + enum + { + InternalLineEnding = '\r', // The character that represents line endings internally + }; + + // Read song message from a mapped file. + // [in] data: pointer to the data in memory that is going to be read + // [in] length: number of characters that should be read, not including a possible trailing null terminator (it is automatically appended). + // [in] lineEnding: line ending formatting of the text in memory. + // [out] returns true on success. + bool Read(const std::byte *data, const size_t length, LineEnding lineEnding); + bool Read(FileReader &file, const size_t length, LineEnding lineEnding); + + // Read comments with fixed line length from a mapped file. + // [in] data: pointer to the data in memory that is going to be read + // [in] length: number of characters that should be read, not including a possible trailing null terminator (it is automatically appended). + // [in] lineLength: The fixed length of a line. + // [in] lineEndingLength: The padding space between two fixed lines. (there could for example be a null char after every line) + // [out] returns true on success. + bool ReadFixedLineLength(const std::byte *data, const size_t length, const size_t lineLength, const size_t lineEndingLength); + bool ReadFixedLineLength(FileReader &file, const size_t length, const size_t lineLength, const size_t lineEndingLength); + + // Retrieve song message. + // [in] lineEnding: line ending formatting of the text in memory. + // [out] returns formatted song message. + std::string GetFormatted(const LineEnding lineEnding) const; + + // Set song message. + // [in] lineEnding: line ending formatting of the text in memory. Must be leCR or leLF or leCRLF, + // [out] returns true if the message has been changed. + bool SetFormatted(std::string message, LineEnding lineEnding); + + // Sets the song message. Expects the provided string to already use the internal line ending character. + void SetRaw(std::string message) noexcept { assign(std::move(message)); } +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.cpp new file mode 100644 index 00000000..585f57c7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.cpp @@ -0,0 +1,92 @@ +/* + * MixFuncTable.cpp + * ---------------- + * Purpose: Table containing all mixer functions. + * Notes : The Visual Studio project settings for this file have been adjusted + * to force function inlining, so that the mixer has a somewhat acceptable + * performance in debug mode. If you need to debug anything here, be sure + * to disable those optimizations if needed. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "Mixer.h" +#include "Snd_defs.h" +#include "ModChannel.h" +#include "MixFuncTable.h" + +#ifdef MPT_INTMIXER +#include "IntMixer.h" +#else +#include "FloatMixer.h" +#endif // MPT_INTMIXER + +OPENMPT_NAMESPACE_BEGIN + +namespace MixFuncTable +{ +#ifdef MPT_INTMIXER +using I8M = Int8MToIntS; +using I16M = Int16MToIntS; +using I8S = Int8SToIntS; +using I16S = Int16SToIntS; +#else +using I8M = Int8MToFloatS; +using I16M = Int16MToFloatS; +using I8S = Int8SToFloatS; +using I16S = Int16SToFloatS; +#endif // MPT_INTMIXER + +// Build mix function table for given resampling, filter and ramping settings: One function each for 8-Bit / 16-Bit Mono / Stereo +#define BuildMixFuncTableRamp(resampling, filter, ramp) \ + SampleLoop<I8M, resampling<I8M>, filter<I8M>, MixMono ## ramp<I8M> >, \ + SampleLoop<I16M, resampling<I16M>, filter<I16M>, MixMono ## ramp<I16M> >, \ + SampleLoop<I8S, resampling<I8S>, filter<I8S>, MixStereo ## ramp<I8S> >, \ + SampleLoop<I16S, resampling<I16S>, filter<I16S>, MixStereo ## ramp<I16S> > + +// Build mix function table for given resampling, filter settings: With and without ramping +#define BuildMixFuncTableFilter(resampling, filter) \ + BuildMixFuncTableRamp(resampling, filter, NoRamp), \ + BuildMixFuncTableRamp(resampling, filter, Ramp) + +// Build mix function table for given resampling settings: With and without filter +#define BuildMixFuncTable(resampling) \ + BuildMixFuncTableFilter(resampling, NoFilter), \ + BuildMixFuncTableFilter(resampling, ResonantFilter) + +const MixFuncInterface Functions[6 * 16] = +{ + BuildMixFuncTable(NoInterpolation), // No SRC + BuildMixFuncTable(LinearInterpolation), // Linear SRC + BuildMixFuncTable(FastSincInterpolation), // Fast Sinc (Cubic Spline) SRC + BuildMixFuncTable(PolyphaseInterpolation), // Kaiser SRC + BuildMixFuncTable(FIRFilterInterpolation), // FIR SRC + BuildMixFuncTable(AmigaBlepInterpolation), // Amiga emulation +}; + +#undef BuildMixFuncTableRamp +#undef BuildMixFuncTableFilter +#undef BuildMixFuncTable + + +ResamplingIndex ResamplingModeToMixFlags(ResamplingMode resamplingMode) +{ + switch(resamplingMode) + { + case SRCMODE_NEAREST: return ndxNoInterpolation; + case SRCMODE_LINEAR: return ndxLinear; + case SRCMODE_CUBIC: return ndxFastSinc; + case SRCMODE_SINC8LP: return ndxKaiser; + case SRCMODE_SINC8: return ndxFIRFilter; + case SRCMODE_AMIGA: return ndxAmigaBlep; + default: MPT_ASSERT_NOTREACHED(); + } + return ndxNoInterpolation; +} + +} // namespace MixFuncTable + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.h b/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.h new file mode 100644 index 00000000..dddb676b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixFuncTable.h @@ -0,0 +1,52 @@ +/* + * MixFuncTable.h + * -------------- + * Purpose: Table containing all mixer functions. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "MixerInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace MixFuncTable +{ + // Table index bits: + // [b1-b0] format (8-bit-mono, 16-bit-mono, 8-bit-stereo, 16-bit-stereo) + // [b2] ramp + // [b3] filter + // [b6-b4] src type + + // Sample type / processing type index + enum FunctionIndex + { + ndx16Bit = 0x01, + ndxStereo = 0x02, + ndxRamp = 0x04, + ndxFilter = 0x08, + }; + + // SRC index + enum ResamplingIndex + { + ndxNoInterpolation = 0x00, + ndxLinear = 0x10, + ndxFastSinc = 0x20, + ndxKaiser = 0x30, + ndxFIRFilter = 0x40, + ndxAmigaBlep = 0x50, + }; + + extern const MixFuncInterface Functions[6 * 16]; + + ResamplingIndex ResamplingModeToMixFlags(ResamplingMode resamplingMode); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Mixer.h b/Src/external_dependencies/openmpt-trunk/soundlib/Mixer.h new file mode 100644 index 00000000..c18043d9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Mixer.h @@ -0,0 +1,63 @@ +/* + * Mixer.h + * ------- + * Purpose: Basic mixer constants + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/soundbase/MixSample.hpp" + +OPENMPT_NAMESPACE_BEGIN + +#define MPT_INTMIXER + +#ifdef MPT_INTMIXER +using mixsample_t = MixSampleIntTraits::sample_type; +enum { MIXING_FILTER_PRECISION = MixSampleIntTraits::filter_precision_bits }; // Fixed point resonant filter bits +#else +using mixsample_t = MixSampleFloat; +#endif +enum { MIXING_ATTENUATION = MixSampleIntTraits::mix_headroom_bits }; +enum { MIXING_FRACTIONAL_BITS = MixSampleIntTraits::mix_fractional_bits }; + +inline constexpr float MIXING_SCALEF = MixSampleIntTraits::mix_scale<float>; + +#ifdef MPT_INTMIXER +static_assert(sizeof(mixsample_t) == 4); +static_assert(MIXING_FILTER_PRECISION == 24); +static_assert(MIXING_ATTENUATION == 4); +static_assert(MIXING_FRACTIONAL_BITS == 27); +static_assert(MixSampleIntTraits::mix_clip_max == int32(0x7FFFFFF)); +static_assert(MixSampleIntTraits::mix_clip_min == (0 - int32(0x7FFFFFF))); +static_assert(MIXING_SCALEF == 134217728.0f); +#else +static_assert(sizeof(mixsample_t) == 4); +#endif + +#define MIXBUFFERSIZE 512 +#define NUMMIXINPUTBUFFERS 4 + +#define VOLUMERAMPPRECISION 12 // Fractional bits in volume ramp variables + +// The absolute maximum number of sampling points any interpolation algorithm is going to look at in any direction from the current sampling point +// Currently, the maximum is 4 sampling points forwards and 3 sampling points backwards (Polyphase / FIR algorithms). +// Hence, this value must be at least 4. +inline constexpr uint8 InterpolationMaxLookahead = 4; +// While we could directly use the previous value in various places such as the interpolation wrap-around handling at loop points, +// choosing a higher value (e.g. 16) will reduce CPU usage when using many extremely short (length < 16) samples. +inline constexpr uint8 InterpolationLookaheadBufferSize = 16; + +static_assert(InterpolationLookaheadBufferSize >= InterpolationMaxLookahead); + +// Maximum size of a sampling point of a sample, in bytes. +// The biggest sampling point size is currently 16-bit stereo = 2 * 2 bytes. +inline constexpr uint8 MaxSamplingPointSize = 4; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixerInterface.h b/Src/external_dependencies/openmpt-trunk/soundlib/MixerInterface.h new file mode 100644 index 00000000..43d2f677 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixerInterface.h @@ -0,0 +1,99 @@ +/* + * MixerInterface.h + * ---------------- + * Purpose: The basic mixer interface and main mixer loop, completely agnostic of the actual sample input / output formats. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" +#include "ModChannel.h" + +OPENMPT_NAMESPACE_BEGIN + +class CResampler; + +////////////////////////////////////////////////////////////////////////// +// Sample conversion traits + +template<int channelsOut, int channelsIn, typename out, typename in> +struct MixerTraits +{ + enum : int { numChannelsIn = channelsIn }; // Number of channels in sample + enum : int { numChannelsOut = channelsOut }; // Number of mixer output channels + typedef out output_t; // Output buffer sample type + typedef in input_t; // Input buffer sample type + typedef out outbuf_t[channelsOut]; // Output buffer sampling point type + // To perform sample conversion, add a function with the following signature to your derived classes: + // static MPT_CONSTEXPRINLINE output_t Convert(const input_t x) +}; + + +////////////////////////////////////////////////////////////////////////// +// Interpolation templates + +template<class Traits> +struct NoInterpolation +{ + MPT_FORCEINLINE NoInterpolation(const ModChannel &, const CResampler &, unsigned int) { } + + MPT_FORCEINLINE void operator() (typename Traits::outbuf_t &outSample, const typename Traits::input_t * const inBuffer, const int32) + { + static_assert(static_cast<int>(Traits::numChannelsIn) <= static_cast<int>(Traits::numChannelsOut), "Too many input channels"); + + for(int i = 0; i < Traits::numChannelsIn; i++) + { + outSample[i] = Traits::Convert(inBuffer[i]); + } + } +}; + +// Other interpolation algorithms depend on the input format type (integer / float) and can thus be found in FloatMixer.h and IntMixer.h + + +////////////////////////////////////////////////////////////////////////// +// Main sample render loop template + +// Template parameters: +// Traits: A class derived from MixerTraits that defines the number of channels, sample buffer types, etc.. +// InterpolationFunc: Functor for reading the sample data and doing the SRC +// FilterFunc: Functor for applying the resonant filter +// MixFunc: Functor for mixing the computed sample data into the output buffer +template<class Traits, class InterpolationFunc, class FilterFunc, class MixFunc> +static void SampleLoop(ModChannel &chn, const CResampler &resampler, typename Traits::output_t * MPT_RESTRICT outBuffer, unsigned int numSamples) +{ + ModChannel &c = chn; + const typename Traits::input_t * MPT_RESTRICT inSample = static_cast<const typename Traits::input_t *>(c.pCurrentSample); + + InterpolationFunc interpolate{c, resampler, numSamples}; + FilterFunc filter{c}; + MixFunc mix{c}; + + unsigned int samples = numSamples; + SamplePosition smpPos = c.position; // Fixed-point sample position + const SamplePosition increment = c.increment; // Fixed-point sample increment + + while(samples--) + { + typename Traits::outbuf_t outSample; + interpolate(outSample, inSample + smpPos.GetInt() * Traits::numChannelsIn, smpPos.GetFract()); + filter(outSample, c); + mix(outSample, c, outBuffer); + outBuffer += Traits::numChannelsOut; + + smpPos += increment; + } + + c.position = smpPos; +} + +// Type of the SampleLoop function above +typedef void (*MixFuncInterface)(ModChannel &, const CResampler &, mixsample_t *, unsigned int); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.cpp new file mode 100644 index 00000000..e0dc098a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.cpp @@ -0,0 +1,183 @@ +/* + * MixerLoops.cpp + * -------------- + * Purpose: Utility inner loops for mixer-related functionality. + * Notes : This file contains performance-critical loops. + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "MixerLoops.h" +#include "Snd_defs.h" +#include "ModChannel.h" + + +OPENMPT_NAMESPACE_BEGIN + + + +void FloatToStereoMix(const float *pIn1, const float *pIn2, int32 *pOut, uint32 nCount, const float _f2ic) +{ + for(uint32 i=0; i<nCount; ++i) + { + *pOut++ = (int)(*pIn1++ * _f2ic); + *pOut++ = (int)(*pIn2++ * _f2ic); + } +} + + +void StereoMixToFloat(const int32 *pSrc, float *pOut1, float *pOut2, uint32 nCount, const float _i2fc) +{ + for(uint32 i=0; i<nCount; ++i) + { + *pOut1++ = *pSrc++ * _i2fc; + *pOut2++ = *pSrc++ * _i2fc; + } +} + + +void FloatToMonoMix(const float *pIn, int32 *pOut, uint32 nCount, const float _f2ic) +{ + for(uint32 i=0; i<nCount; ++i) + { + *pOut++ = (int)(*pIn++ * _f2ic); + } +} + + +void MonoMixToFloat(const int32 *pSrc, float *pOut, uint32 nCount, const float _i2fc) +{ + for(uint32 i=0; i<nCount; ++i) + { + *pOut++ = *pSrc++ * _i2fc; + } +} + + + +void InitMixBuffer(mixsample_t *pBuffer, uint32 nSamples) +{ + std::memset(pBuffer, 0, nSamples * sizeof(mixsample_t)); +} + + + +void InterleaveFrontRear(mixsample_t *pFrontBuf, mixsample_t *pRearBuf, uint32 nFrames) +{ + // copy backwards as we are writing back into FrontBuf + for(int i=nFrames-1; i>=0; i--) + { + pFrontBuf[i*4+3] = pRearBuf[i*2+1]; + pFrontBuf[i*4+2] = pRearBuf[i*2+0]; + pFrontBuf[i*4+1] = pFrontBuf[i*2+1]; + pFrontBuf[i*4+0] = pFrontBuf[i*2+0]; + } +} + + + +void MonoFromStereo(mixsample_t *pMixBuf, uint32 nSamples) +{ + for(uint32 i=0; i<nSamples; ++i) + { + pMixBuf[i] = (pMixBuf[i*2] + pMixBuf[i*2+1]) / 2; + } +} + + + +#define OFSDECAYSHIFT 8 +#define OFSDECAYMASK 0xFF +#define OFSTHRESHOLD static_cast<mixsample_t>(1.0 / (1 << 20)) // Decay threshold for floating point mixer + + +void StereoFill(mixsample_t *pBuffer, uint32 nSamples, mixsample_t &rofs, mixsample_t &lofs) +{ + if((!rofs) && (!lofs)) + { + InitMixBuffer(pBuffer, nSamples*2); + return; + } + for(uint32 i=0; i<nSamples; i++) + { +#ifdef MPT_INTMIXER + // Equivalent to int x_r = (rofs + (rofs > 0 ? 255 : -255)) / 256; + const mixsample_t x_r = mpt::rshift_signed(rofs + (mpt::rshift_signed(-rofs, sizeof(mixsample_t) * 8 - 1) & OFSDECAYMASK), OFSDECAYSHIFT); + const mixsample_t x_l = mpt::rshift_signed(lofs + (mpt::rshift_signed(-lofs, sizeof(mixsample_t) * 8 - 1) & OFSDECAYMASK), OFSDECAYSHIFT); +#else + const mixsample_t x_r = rofs * (1.0f / (1 << OFSDECAYSHIFT)); + const mixsample_t x_l = lofs * (1.0f / (1 << OFSDECAYSHIFT)); +#endif + rofs -= x_r; + lofs -= x_l; + pBuffer[i*2] = rofs; + pBuffer[i*2+1] = lofs; + } + +#ifndef MPT_INTMIXER + if(fabs(rofs) < OFSTHRESHOLD) rofs = 0; + if(fabs(lofs) < OFSTHRESHOLD) lofs = 0; +#endif +} + + +void EndChannelOfs(ModChannel &chn, mixsample_t *pBuffer, uint32 nSamples) +{ + + mixsample_t rofs = chn.nROfs; + mixsample_t lofs = chn.nLOfs; + + if((!rofs) && (!lofs)) + { + return; + } + for(uint32 i=0; i<nSamples; i++) + { +#ifdef MPT_INTMIXER + const mixsample_t x_r = mpt::rshift_signed(rofs + (mpt::rshift_signed(-rofs, sizeof(mixsample_t) * 8 - 1) & OFSDECAYMASK), OFSDECAYSHIFT); + const mixsample_t x_l = mpt::rshift_signed(lofs + (mpt::rshift_signed(-lofs, sizeof(mixsample_t) * 8 - 1) & OFSDECAYMASK), OFSDECAYSHIFT); +#else + const mixsample_t x_r = rofs * (1.0f / (1 << OFSDECAYSHIFT)); + const mixsample_t x_l = lofs * (1.0f / (1 << OFSDECAYSHIFT)); +#endif + rofs -= x_r; + lofs -= x_l; + pBuffer[i*2] += rofs; + pBuffer[i*2+1] += lofs; + } +#ifndef MPT_INTMIXER + if(std::abs(rofs) < OFSTHRESHOLD) rofs = 0; + if(std::abs(lofs) < OFSTHRESHOLD) lofs = 0; +#endif + + chn.nROfs = rofs; + chn.nLOfs = lofs; +} + + + +void InterleaveStereo(const mixsample_t * MPT_RESTRICT inputL, const mixsample_t * MPT_RESTRICT inputR, mixsample_t * MPT_RESTRICT output, size_t numSamples) +{ + while(numSamples--) + { + *(output++) = *(inputL++); + *(output++) = *(inputR++); + } +} + + +void DeinterleaveStereo(const mixsample_t * MPT_RESTRICT input, mixsample_t * MPT_RESTRICT outputL, mixsample_t * MPT_RESTRICT outputR, size_t numSamples) +{ + while(numSamples--) + { + *(outputL++) = *(input++); + *(outputR++) = *(input++); + } +} + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.h b/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.h new file mode 100644 index 00000000..35099746 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixerLoops.h @@ -0,0 +1,36 @@ +/* + * MixerLoops.h + * ------------ + * Purpose: Utility inner loops for mixer-related functionality. + * Notes : none. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Mixer.h" + +OPENMPT_NAMESPACE_BEGIN + +struct ModChannel; + +void StereoMixToFloat(const int32 *pSrc, float *pOut1, float *pOut2, uint32 nCount, const float _i2fc); +void FloatToStereoMix(const float *pIn1, const float *pIn2, int32 *pOut, uint32 uint32, const float _f2ic); +void MonoMixToFloat(const int32 *pSrc, float *pOut, uint32 uint32, const float _i2fc); +void FloatToMonoMix(const float *pIn, int32 *pOut, uint32 uint32, const float _f2ic); + +void InitMixBuffer(mixsample_t *pBuffer, uint32 nSamples); +void InterleaveFrontRear(mixsample_t *pFrontBuf, mixsample_t *pRearBuf, uint32 nFrames); +void MonoFromStereo(mixsample_t *pMixBuf, uint32 nSamples); + +void InterleaveStereo(const mixsample_t *inputL, const mixsample_t *inputR, mixsample_t *output, size_t numSamples); +void DeinterleaveStereo(const mixsample_t *input, mixsample_t *outputL, mixsample_t *outputR, size_t numSamples); + +void EndChannelOfs(ModChannel &chn, mixsample_t *pBuffer, uint32 nSamples); +void StereoFill(mixsample_t *pBuffer, uint32 nSamples, mixsample_t &rofs, mixsample_t &lofs); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.cpp new file mode 100644 index 00000000..7a105725 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.cpp @@ -0,0 +1,59 @@ +/* + * MixerSettings.cpp + * ----------------- + * Purpose: A struct containing settings for the mixer of soundlib. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "MixerSettings.h" +#include "Snd_defs.h" +#include "../common/misc_util.h" + +OPENMPT_NAMESPACE_BEGIN + +MixerSettings::MixerSettings() +{ + + // SNDMIX: These are global flags for playback control + m_nStereoSeparation = 128; + m_nMaxMixChannels = MAX_CHANNELS; + + DSPMask = 0; + MixerFlags = 0; + + // Mixing Configuration + gnChannels = 2; + gdwMixingFreq = 48000; + + m_nPreAmp = 128; + + VolumeRampUpMicroseconds = 363; // 16 @44100 + VolumeRampDownMicroseconds = 952; // 42 @44100 + + NumInputChannels = 0; + +} + +int32 MixerSettings::GetVolumeRampUpSamples() const +{ + return Util::muldivr(VolumeRampUpMicroseconds, gdwMixingFreq, 1000000); +} +int32 MixerSettings::GetVolumeRampDownSamples() const +{ + return Util::muldivr(VolumeRampDownMicroseconds, gdwMixingFreq, 1000000); +} + +void MixerSettings::SetVolumeRampUpSamples(int32 rampUpSamples) +{ + VolumeRampUpMicroseconds = Util::muldivr(rampUpSamples, 1000000, gdwMixingFreq); +} +void MixerSettings::SetVolumeRampDownSamples(int32 rampDownSamples) +{ + VolumeRampDownMicroseconds = Util::muldivr(rampDownSamples, 1000000, gdwMixingFreq); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.h b/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.h new file mode 100644 index 00000000..ac18ce65 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/MixerSettings.h @@ -0,0 +1,55 @@ +/* + * MixerSettings.h + * --------------- + * Purpose: A struct containing settings for the mixer of soundlib. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +struct MixerSettings +{ + + int32 m_nStereoSeparation; + enum : int32 { StereoSeparationScale = 128 }; + + uint32 m_nMaxMixChannels; + uint32 DSPMask; + uint32 MixerFlags; + uint32 gdwMixingFreq; + uint32 gnChannels; + uint32 m_nPreAmp; + std::size_t NumInputChannels; + + int32 VolumeRampUpMicroseconds; + int32 VolumeRampDownMicroseconds; + int32 GetVolumeRampUpMicroseconds() const { return VolumeRampUpMicroseconds; } + int32 GetVolumeRampDownMicroseconds() const { return VolumeRampDownMicroseconds; } + void SetVolumeRampUpMicroseconds(int32 rampUpMicroseconds) { VolumeRampUpMicroseconds = rampUpMicroseconds; } + void SetVolumeRampDownMicroseconds(int32 rampDownMicroseconds) { VolumeRampDownMicroseconds = rampDownMicroseconds; } + + int32 GetVolumeRampUpSamples() const; + int32 GetVolumeRampDownSamples() const; + + void SetVolumeRampUpSamples(int32 rampUpSamples); + void SetVolumeRampDownSamples(int32 rampDownSamples); + + bool IsValid() const + { + return (gdwMixingFreq > 0) && (gnChannels == 1 || gnChannels == 2 || gnChannels == 4) && (NumInputChannels == 0 || NumInputChannels == 1 || NumInputChannels == 2 || NumInputChannels == 4); + } + + MixerSettings(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.cpp new file mode 100644 index 00000000..4d6550a4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.cpp @@ -0,0 +1,222 @@ +/* + * ModChannel.cpp + * -------------- + * Purpose: Module Channel header class and helpers + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "ModChannel.h" +#include "tuning.h" + +OPENMPT_NAMESPACE_BEGIN + +void ModChannel::Reset(ResetFlags resetMask, const CSoundFile &sndFile, CHANNELINDEX sourceChannel, ChannelFlags muteFlag) +{ + if(resetMask & resetSetPosBasic) + { + nNote = nNewNote = NOTE_NONE; + nNewIns = nOldIns = 0; + pModSample = nullptr; + pModInstrument = nullptr; + nPortamentoDest = 0; + nCommand = CMD_NONE; + nPatternLoopCount = 0; + nPatternLoop = 0; + nFadeOutVol = 0; + dwFlags.set(CHN_KEYOFF | CHN_NOTEFADE); + dwOldFlags.reset(); + //IT compatibility 15. Retrigger + if(sndFile.m_playBehaviour[kITRetrigger]) + { + nRetrigParam = 1; + nRetrigCount = 0; + } + microTuning = 0; + nTremorCount = 0; + nEFxSpeed = 0; + prevNoteOffset = 0; + lastZxxParam = 0xFF; + isFirstTick = false; + triggerNote = false; + isPreviewNote = false; + isPaused = false; + portaTargetReached = false; + rowCommand.Clear(); + } + + if(resetMask & resetSetPosAdvanced) + { + increment = SamplePosition(0); + nPeriod = 0; + position.Set(0); + nLength = 0; + nLoopStart = 0; + nLoopEnd = 0; + nROfs = nLOfs = 0; + pModSample = nullptr; + pModInstrument = nullptr; + nCutOff = 0x7F; + nResonance = 0; + nFilterMode = FilterMode::LowPass; + rightVol = leftVol = 0; + newRightVol = newLeftVol = 0; + rightRamp = leftRamp = 0; + nVolume = 0; // Needs to be 0 for SMP_NODEFAULTVOLUME flag + nVibratoPos = nTremoloPos = nPanbrelloPos = 0; + nOldHiOffset = 0; + nLeftVU = nRightVU = 0; + + // Custom tuning related + m_ReCalculateFreqOnFirstTick = false; + m_CalculateFreq = false; + m_PortamentoFineSteps = 0; + m_PortamentoTickSlide = 0; + } + + if(resetMask & resetChannelSettings) + { + if(sourceChannel < MAX_BASECHANNELS) + { + dwFlags = sndFile.ChnSettings[sourceChannel].dwFlags; + nPan = sndFile.ChnSettings[sourceChannel].nPan; + nGlobalVol = sndFile.ChnSettings[sourceChannel].nVolume; + if(dwFlags[CHN_MUTE]) + { + dwFlags.reset(CHN_MUTE); + dwFlags.set(muteFlag); + } + } else + { + dwFlags.reset(); + nPan = 128; + nGlobalVol = 64; + } + nRestorePanOnNewNote = 0; + nRestoreCutoffOnNewNote = 0; + nRestoreResonanceOnNewNote = 0; + } +} + + +void ModChannel::Stop() +{ + nPeriod = 0; + increment.Set(0); + position.Set(0); + nLeftVU = nRightVU = 0; + nVolume = 0; + pCurrentSample = nullptr; +} + + +void ModChannel::UpdateInstrumentVolume(const ModSample *smp, const ModInstrument *ins) +{ + nInsVol = 64; + if(smp != nullptr) + nInsVol = smp->nGlobalVol; + if(ins != nullptr) + nInsVol = (nInsVol * ins->nGlobalVol) / 64; +} + + +ModCommand::NOTE ModChannel::GetPluginNote(bool realNoteMapping) const +{ + if(nArpeggioLastNote != NOTE_NONE) + { + // If an arpeggio is playing, this definitely the last playing note, which may be different from the arpeggio base note stored in nNote. + return nArpeggioLastNote; + } + ModCommand::NOTE plugNote = mpt::saturate_cast<ModCommand::NOTE>(nNote - nTranspose); + // Caution: When in compatible mode, ModChannel::nNote stores the "real" note, not the mapped note! + if(realNoteMapping && pModInstrument != nullptr && plugNote >= NOTE_MIN && plugNote < (std::size(pModInstrument->NoteMap) + NOTE_MIN)) + { + plugNote = pModInstrument->NoteMap[plugNote - NOTE_MIN]; + } + return plugNote; +} + + +void ModChannel::SetInstrumentPan(int32 pan, const CSoundFile &sndFile) +{ + // IT compatibility: Instrument and sample panning does not override channel panning + // Test case: PanResetInstr.it + if(sndFile.m_playBehaviour[kITDoNotOverrideChannelPan]) + { + nRestorePanOnNewNote = static_cast<uint16>(nPan + 1); + if(dwFlags[CHN_SURROUND]) + nRestorePanOnNewNote |= 0x8000; + } + nPan = pan; +} + + +void ModChannel::RestorePanAndFilter() +{ + if(nRestorePanOnNewNote > 0) + { + nPan = (nRestorePanOnNewNote & 0x7FFF) - 1; + if(nRestorePanOnNewNote & 0x8000) + dwFlags.set(CHN_SURROUND); + nRestorePanOnNewNote = 0; + } + if(nRestoreResonanceOnNewNote > 0) + { + nResonance = nRestoreResonanceOnNewNote - 1; + nRestoreResonanceOnNewNote = 0; + } + if(nRestoreCutoffOnNewNote > 0) + { + nCutOff = nRestoreCutoffOnNewNote - 1; + nRestoreCutoffOnNewNote = 0; + } +} + + +void ModChannel::RecalcTuningFreq(Tuning::RATIOTYPE vibratoFactor, Tuning::NOTEINDEXTYPE arpeggioSteps, const CSoundFile &sndFile) +{ + if(!HasCustomTuning()) + return; + + ModCommand::NOTE note = ModCommand::IsNote(nNote) ? nNote : nLastNote; + + if(sndFile.m_playBehaviour[kITRealNoteMapping] && note >= NOTE_MIN && note <= NOTE_MAX) + note = pModInstrument->NoteMap[note - NOTE_MIN]; + + nPeriod = mpt::saturate_round<uint32>(nC5Speed * vibratoFactor * pModInstrument->pTuning->GetRatio(note - NOTE_MIDDLEC + arpeggioSteps, nFineTune + m_PortamentoFineSteps) * (1 << FREQ_FRACBITS)); +} + + +// IT command S73-S7E +void ModChannel::InstrumentControl(uint8 param, const CSoundFile &sndFile) +{ + param &= 0x0F; + switch(param) + { + case 0x3: nNNA = NewNoteAction::NoteCut; break; + case 0x4: nNNA = NewNoteAction::Continue; break; + case 0x5: nNNA = NewNoteAction::NoteOff; break; + case 0x6: nNNA = NewNoteAction::NoteFade; break; + case 0x7: VolEnv.flags.reset(ENV_ENABLED); break; + case 0x8: VolEnv.flags.set(ENV_ENABLED); break; + case 0x9: PanEnv.flags.reset(ENV_ENABLED); break; + case 0xA: PanEnv.flags.set(ENV_ENABLED); break; + case 0xB: PitchEnv.flags.reset(ENV_ENABLED); break; + case 0xC: PitchEnv.flags.set(ENV_ENABLED); break; + case 0xD: // S7D: Enable pitch envelope, force to play as pitch envelope + case 0xE: // S7E: Enable pitch envelope, force to play as filter envelope + if(sndFile.GetType() == MOD_TYPE_MPT) + { + PitchEnv.flags.set(ENV_ENABLED); + PitchEnv.flags.set(ENV_FILTER, param != 0xD); + } + break; + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.h b/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.h new file mode 100644 index 00000000..b8293959 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModChannel.h @@ -0,0 +1,234 @@ +/* + * ModChannel.h + * ------------ + * Purpose: Module Channel header class and helpers + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "ModSample.h" +#include "ModInstrument.h" +#include "modcommand.h" +#include "Paula.h" +#include "tuningbase.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +// Mix Channel Struct +struct ModChannel +{ + // Envelope playback info + struct EnvInfo + { + uint32 nEnvPosition = 0; + int16 nEnvValueAtReleaseJump = NOT_YET_RELEASED; + FlagSet<EnvelopeFlags> flags; + + void Reset() + { + nEnvPosition = 0; + nEnvValueAtReleaseJump = NOT_YET_RELEASED; + } + }; + + // Information used in the mixer (should be kept tight for better caching) + SamplePosition position; // Current play position (fixed point) + SamplePosition increment; // Sample speed relative to mixing frequency (fixed point) + const void *pCurrentSample; // Currently playing sample (nullptr if no sample is playing) + int32 leftVol; // 0...4096 (12 bits, since 16 bits + 12 bits = 28 bits = 0dB in integer mixer, see MIXING_ATTENUATION) + int32 rightVol; // Ditto + int32 leftRamp; // Ramping delta, 20.12 fixed point (see VOLUMERAMPPRECISION) + int32 rightRamp; // Ditto + int32 rampLeftVol; // Current ramping volume, 20.12 fixed point (see VOLUMERAMPPRECISION) + int32 rampRightVol; // Ditto + mixsample_t nFilter_Y[2][2]; // Filter memory - two history items per sample channel + mixsample_t nFilter_A0, nFilter_B0, nFilter_B1; // Filter coeffs + mixsample_t nFilter_HP; + + SmpLength nLength; + SmpLength nLoopStart; + SmpLength nLoopEnd; + FlagSet<ChannelFlags> dwFlags; + mixsample_t nROfs, nLOfs; + uint32 nRampLength; + + const ModSample *pModSample; // Currently assigned sample slot (may already be stopped) + Paula::State paulaState; + + // Information not used in the mixer + const ModInstrument *pModInstrument; // Currently assigned instrument slot + SmpLength prevNoteOffset; // Offset for instrument-less notes for ProTracker/ScreamTracker + SmpLength oldOffset; // Offset command memory + FlagSet<ChannelFlags> dwOldFlags; // Flags from previous tick + int32 newLeftVol, newRightVol; + int32 nRealVolume, nRealPan; + int32 nVolume, nPan, nFadeOutVol; + int32 nPeriod; // Frequency in Hz if CSoundFile::PeriodsAreFrequencies() or using custom tuning, 4x Amiga periods otherwise + int32 nC5Speed, nPortamentoDest; + int32 cachedPeriod, glissandoPeriod; + int32 nCalcVolume; // Calculated channel volume, 14-Bit (without global volume, pre-amp etc applied) - for MIDI macros + EnvInfo VolEnv, PanEnv, PitchEnv; // Envelope playback info + int32 nGlobalVol; // Channel volume (CV in ITTECH.TXT) 0...64 + int32 nInsVol; // Sample / Instrument volume (SV * IV in ITTECH.TXT) 0...64 + int32 nAutoVibDepth; + uint32 nEFxOffset; // Offset memory for Invert Loop (EFx, .MOD only) + ROWINDEX nPatternLoop; + uint16 portamentoSlide; + int16 nTranspose; + int16 nFineTune; + int16 microTuning; // Micro-tuning / MIDI pitch wheel command + int16 nVolSwing, nPanSwing; + int16 nCutSwing, nResSwing; + uint16 nRestorePanOnNewNote; // If > 0, nPan should be set to nRestorePanOnNewNote - 1 on new note. Used to recover from pan swing and IT sample / instrument panning. High bit set = surround + CHANNELINDEX nMasterChn; + ModCommand rowCommand; + // 8-bit members + ResamplingMode resamplingMode; + uint8 nRestoreResonanceOnNewNote; // See nRestorePanOnNewNote + uint8 nRestoreCutoffOnNewNote; // ditto + uint8 nNote; + NewNoteAction nNNA; + uint8 nLastNote; // Last note, ignoring note offs and cuts - for MIDI macros + uint8 nArpeggioLastNote, nArpeggioBaseNote; // For plugin arpeggio + uint8 nNewNote, nNewIns, nOldIns, nCommand, nArpeggio; + uint8 nRetrigParam, nRetrigCount; + uint8 nOldVolumeSlide, nOldFineVolUpDown; + uint8 nOldPortaUp, nOldPortaDown, nOldFinePortaUpDown, nOldExtraFinePortaUpDown; + uint8 nOldPanSlide, nOldChnVolSlide; + uint8 nOldGlobalVolSlide; + uint8 nAutoVibPos, nVibratoPos, nTremoloPos, nPanbrelloPos; + uint8 nVibratoType, nVibratoSpeed, nVibratoDepth; + uint8 nTremoloType, nTremoloSpeed, nTremoloDepth; + uint8 nPanbrelloType, nPanbrelloSpeed, nPanbrelloDepth; + int8 nPanbrelloOffset, nPanbrelloRandomMemory; + uint8 nOldCmdEx, nOldVolParam, nOldTempo; + uint8 nOldHiOffset; + uint8 nCutOff, nResonance; + uint8 nTremorCount, nTremorParam; + uint8 nPatternLoopCount; + uint8 nLeftVU, nRightVU; + uint8 nActiveMacro; + FilterMode nFilterMode; + uint8 nEFxSpeed, nEFxDelay; // memory for Invert Loop (EFx, .MOD only) + uint8 noteSlideParam, noteSlideCounter; // IMF / PTM Note Slide + uint8 lastZxxParam; // Memory for \xx slides + bool isFirstTick : 1; // Execute tick-0 effects on this channel? (condition differs between formats due to Pattern Delay commands) + bool triggerNote : 1; // Trigger note on this tick on this channel if there is one? + bool isPreviewNote : 1; // Notes preview in editor + bool isPaused : 1; // Don't mix or increment channel position, but keep the note alive + bool portaTargetReached : 1; // Tone portamento is finished + + //-->Variables used to make user-definable tuning modes work with pattern effects. + //If true, freq should be recalculated in ReadNote() on first tick. + //Currently used only for vibrato things - using in other context might be + //problematic. + bool m_ReCalculateFreqOnFirstTick : 1; + + //To tell whether to calculate frequency. + bool m_CalculateFreq : 1; + + int32 m_PortamentoFineSteps, m_PortamentoTickSlide; + + //NOTE_PCs memory. + float m_plugParamValueStep, m_plugParamTargetValue; + uint16 m_RowPlugParam; + PLUGINDEX m_RowPlug; + + void ClearRowCmd() { rowCommand = ModCommand(); } + + // Get a reference to a specific envelope of this channel + const EnvInfo &GetEnvelope(EnvelopeType envType) const + { + switch(envType) + { + case ENV_VOLUME: + default: + return VolEnv; + case ENV_PANNING: + return PanEnv; + case ENV_PITCH: + return PitchEnv; + } + } + + EnvInfo &GetEnvelope(EnvelopeType envType) + { + return const_cast<EnvInfo &>(static_cast<const ModChannel *>(this)->GetEnvelope(envType)); + } + + void ResetEnvelopes() + { + VolEnv.Reset(); + PanEnv.Reset(); + PitchEnv.Reset(); + } + + enum ResetFlags + { + resetChannelSettings = 1, // Reload initial channel settings + resetSetPosBasic = 2, // Reset basic runtime channel attributes + resetSetPosAdvanced = 4, // Reset more runtime channel attributes + resetSetPosFull = resetSetPosBasic | resetSetPosAdvanced | resetChannelSettings, // Reset all runtime channel attributes + resetTotal = resetSetPosFull, + }; + + void Reset(ResetFlags resetMask, const CSoundFile &sndFile, CHANNELINDEX sourceChannel, ChannelFlags muteFlag); + void Stop(); + + bool IsSamplePlaying() const noexcept { return !increment.IsZero(); } + + uint32 GetVSTVolume() const noexcept { return (pModInstrument) ? pModInstrument->nGlobalVol * 4 : nVolume; } + + ModCommand::NOTE GetPluginNote(bool realNoteMapping) const; + + // Check if the channel has a valid MIDI output. A return value of true implies that pModInstrument != nullptr. + bool HasMIDIOutput() const noexcept { return pModInstrument != nullptr && pModInstrument->HasValidMIDIChannel(); } + // Check if the channel uses custom tuning. A return value of true implies that pModInstrument != nullptr. + bool HasCustomTuning() const noexcept { return pModInstrument != nullptr && pModInstrument->pTuning != nullptr; } + + // Check if currently processed loop is a sustain loop. pModSample is not checked for validity! + bool InSustainLoop() const noexcept { return (dwFlags & (CHN_LOOP | CHN_KEYOFF)) == CHN_LOOP && pModSample->uFlags[CHN_SUSTAINLOOP]; } + + void UpdateInstrumentVolume(const ModSample *smp, const ModInstrument *ins); + + void SetInstrumentPan(int32 pan, const CSoundFile &sndFile); + void RestorePanAndFilter(); + + void RecalcTuningFreq(Tuning::RATIOTYPE vibratoFactor, Tuning::NOTEINDEXTYPE arpeggioSteps, const CSoundFile &sndFile); + + // IT command S73-S7E + void InstrumentControl(uint8 param, const CSoundFile &sndFile); + + int32 GetMIDIPitchBend() const noexcept { return (static_cast<int32>(microTuning) + 0x8000) / 4; } +}; + + +// Default pattern channel settings +struct ModChannelSettings +{ +#ifdef MODPLUG_TRACKER + static constexpr uint32 INVALID_COLOR = 0xFFFFFFFF; + uint32 color = INVALID_COLOR; // For pattern editor +#endif // MODPLUG_TRACKER + FlagSet<ChannelFlags> dwFlags; // Channel flags + uint16 nPan = 128; // Initial pan (0...256) + uint16 nVolume = 64; // Initial channel volume (0...64) + PLUGINDEX nMixPlugin = 0; // Assigned plugin + + mpt::charbuf<MAX_CHANNELNAME> szName; // Channel name + + void Reset() + { + *this = {}; + } +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.cpp new file mode 100644 index 00000000..f55aaa15 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.cpp @@ -0,0 +1,349 @@ +/* + * ModInstrument.cpp + * ----------------- + * Purpose: Helper functions for Module Instrument handling + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "ModInstrument.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Convert envelope data between various formats. +void InstrumentEnvelope::Convert(MODTYPE fromType, MODTYPE toType) +{ + if(!(fromType & MOD_TYPE_XM) && (toType & MOD_TYPE_XM)) + { + // IT / MPTM -> XM: Expand loop by one tick, convert sustain loops to sustain points, remove carry flag. + nSustainStart = nSustainEnd; + dwFlags.reset(ENV_CARRY); + + if(nLoopEnd > nLoopStart && dwFlags[ENV_LOOP]) + { + for(uint32 node = nLoopEnd; node < size(); node++) + { + at(node).tick++; + } + } + } else if((fromType & MOD_TYPE_XM) && !(toType & MOD_TYPE_XM)) + { + if(nSustainStart > nLoopEnd && dwFlags[ENV_LOOP]) + { + // In the IT format, the sustain loop is always considered before the envelope loop. + // In the XM format, whichever of the two is encountered first is considered. + // So we have to disable the sustain loop if it was behind the normal loop. + dwFlags.reset(ENV_SUSTAIN); + } + + // XM -> IT / MPTM: Shorten loop by one tick by inserting bogus point + if(nLoopEnd > nLoopStart && dwFlags[ENV_LOOP] && nLoopEnd < size()) + { + if(at(nLoopEnd).tick - 1 > at(nLoopEnd - 1).tick) + { + // Insert an interpolated point just before the loop point. + EnvelopeNode::tick_t tick = at(nLoopEnd).tick - 1u; + auto interpolatedValue = static_cast<EnvelopeNode::value_t>(GetValueFromPosition(tick, 64)); + insert(begin() + nLoopEnd, EnvelopeNode(tick, interpolatedValue)); + } else + { + // There is already a point before the loop point: Use it as new loop end. + nLoopEnd--; + } + } + } + + if(toType != MOD_TYPE_MPT) + { + nReleaseNode = ENV_RELEASE_NODE_UNSET; + } +} + + +// Get envelope value at a given tick. Assumes that the envelope data is in rage [0, rangeIn], +// returns value in range [0, rangeOut]. +int32 InstrumentEnvelope::GetValueFromPosition(int position, int32 rangeOut, int32 rangeIn) const +{ + uint32 pt = size() - 1u; + const int32 ENV_PRECISION = 1 << 16; + + // Checking where current 'tick' is relative to the envelope points. + for(uint32 i = 0; i < size() - 1u; i++) + { + if (position <= at(i).tick) + { + pt = i; + break; + } + } + + int x2 = at(pt).tick; + int32 value = 0; + + if(position >= x2) + { + // Case: current 'tick' is on a envelope point. + value = at(pt).value * ENV_PRECISION / rangeIn; + } else + { + // Case: current 'tick' is between two envelope points. + int x1 = 0; + + if(pt) + { + // Get previous node's value and tick. + value = at(pt - 1).value * ENV_PRECISION / rangeIn; + x1 = at(pt - 1).tick; + } + + if(x2 > x1 && position > x1) + { + // Linear approximation between the points; + // f(x + d) ~ f(x) + f'(x) * d, where f'(x) = (y2 - y1) / (x2 - x1) + value += Util::muldiv(position - x1, (at(pt).value * ENV_PRECISION / rangeIn - value), x2 - x1); + } + } + + Limit(value, int32(0), ENV_PRECISION); + return (value * rangeOut + ENV_PRECISION / 2) / ENV_PRECISION; +} + + +void InstrumentEnvelope::Sanitize(uint8 maxValue) +{ + if(!empty()) + { + front().tick = 0; + LimitMax(front().value, maxValue); + for(iterator it = begin() + 1; it != end(); it++) + { + it->tick = std::max(it->tick, (it - 1)->tick); + LimitMax(it->value, maxValue); + } + } + LimitMax(nLoopEnd, static_cast<decltype(nLoopEnd)>(size() - 1)); + LimitMax(nLoopStart, nLoopEnd); + LimitMax(nSustainEnd, static_cast<decltype(nSustainEnd)>(size() - 1)); + LimitMax(nSustainStart, nSustainEnd); + if(nReleaseNode != ENV_RELEASE_NODE_UNSET) + LimitMax(nReleaseNode, static_cast<decltype(nReleaseNode)>(size() - 1)); +} + + +ModInstrument::ModInstrument(SAMPLEINDEX sample) +{ + SetCutoff(0, false); + SetResonance(0, false); + + pitchToTempoLock.Set(0); + + pTuning = CSoundFile::GetDefaultTuning(); + + AssignSample(sample); + ResetNoteMap(); +} + + +// Translate instrument properties between two given formats. +void ModInstrument::Convert(MODTYPE fromType, MODTYPE toType) +{ + MPT_UNREFERENCED_PARAMETER(fromType); + + if(toType & MOD_TYPE_XM) + { + ResetNoteMap(); + + PitchEnv.dwFlags.reset(ENV_ENABLED | ENV_FILTER); + + dwFlags.reset(INS_SETPANNING); + SetCutoff(GetCutoff(), false); + SetResonance(GetResonance(), false); + filterMode = FilterMode::Unchanged; + + nCutSwing = nPanSwing = nResSwing = nVolSwing = 0; + + nPPC = NOTE_MIDDLEC - 1; + nPPS = 0; + + nNNA = NewNoteAction::NoteCut; + nDCT = DuplicateCheckType::None; + nDNA = DuplicateNoteAction::NoteCut; + + if(nMidiChannel == MidiMappedChannel) + { + nMidiChannel = MidiFirstChannel; + } + + // FT2 only has unsigned Pitch Wheel Depth, and it's limited to 0...36 (in the GUI, at least. As you would expect it from FT2, this value is actually not sanitized on load). + midiPWD = static_cast<int8>(std::abs(midiPWD)); + Limit(midiPWD, int8(0), int8(36)); + + nGlobalVol = 64; + nPan = 128; + + LimitMax(nFadeOut, 32767u); + } + + VolEnv.Convert(fromType, toType); + PanEnv.Convert(fromType, toType); + PitchEnv.Convert(fromType, toType); + + if(fromType == MOD_TYPE_XM && (toType & (MOD_TYPE_IT | MOD_TYPE_MPT))) + { + if(!VolEnv.dwFlags[ENV_ENABLED]) + { + // Note-Off with no envelope cuts the note immediately in XM + VolEnv.resize(2); + VolEnv[0].tick = 0; + VolEnv[0].value = ENVELOPE_MAX; + VolEnv[1].tick = 1; + VolEnv[1].value = ENVELOPE_MIN; + VolEnv.dwFlags.set(ENV_ENABLED | ENV_SUSTAIN); + VolEnv.dwFlags.reset(ENV_LOOP); + VolEnv.nSustainStart = VolEnv.nSustainEnd = 0; + } + } + + // Limit fadeout length for IT + if(toType & MOD_TYPE_IT) + { + LimitMax(nFadeOut, 8192u); + } + + // MPT-specific features - remove instrument tunings, Pitch/Tempo Lock, cutoff / resonance swing and filter mode for other formats + if(!(toType & MOD_TYPE_MPT)) + { + SetTuning(nullptr); + pitchToTempoLock.Set(0); + nCutSwing = nResSwing = 0; + filterMode = FilterMode::Unchanged; + nVolRampUp = 0; + } +} + + +// Get a set of all samples referenced by this instrument +std::set<SAMPLEINDEX> ModInstrument::GetSamples() const +{ + std::set<SAMPLEINDEX> referencedSamples; + + for(const auto sample : Keyboard) + { + if(sample) + { + referencedSamples.insert(sample); + } + } + + return referencedSamples; +} + + +// Write sample references into a bool vector. If a sample is referenced by this instrument, true is written. +// The caller has to initialize the vector. +void ModInstrument::GetSamples(std::vector<bool> &referencedSamples) const +{ + for(const auto sample : Keyboard) + { + if(sample != 0 && sample < referencedSamples.size()) + { + referencedSamples[sample] = true; + } + } +} + + +void ModInstrument::Sanitize(MODTYPE modType) +{ + LimitMax(nFadeOut, 65536u); + LimitMax(nGlobalVol, 64u); + LimitMax(nPan, 256u); + + LimitMax(wMidiBank, uint16(16384)); + LimitMax(nMidiProgram, uint8(128)); + LimitMax(nMidiChannel, uint8(17)); + + if(nNNA > NewNoteAction::NoteFade) nNNA = NewNoteAction::NoteCut; + if(nDCT > DuplicateCheckType::Plugin) nDCT = DuplicateCheckType::None; + if(nDNA > DuplicateNoteAction::NoteFade) nDNA = DuplicateNoteAction::NoteCut; + + LimitMax(nPanSwing, uint8(64)); + LimitMax(nVolSwing, uint8(100)); + + Limit(nPPS, int8(-32), int8(32)); + + LimitMax(nCutSwing, uint8(64)); + LimitMax(nResSwing, uint8(64)); + +#ifdef MODPLUG_TRACKER + MPT_UNREFERENCED_PARAMETER(modType); + const uint8 range = ENVELOPE_MAX; +#else + const uint8 range = modType == MOD_TYPE_AMS ? uint8_max : ENVELOPE_MAX; +#endif + VolEnv.Sanitize(); + PanEnv.Sanitize(); + PitchEnv.Sanitize(range); + + for(size_t i = 0; i < std::size(NoteMap); i++) + { + if(NoteMap[i] < NOTE_MIN || NoteMap[i] > NOTE_MAX) + NoteMap[i] = static_cast<uint8>(i + NOTE_MIN); + } + + if(!Resampling::IsKnownMode(resampling)) + resampling = SRCMODE_DEFAULT; + + if(nMixPlug > MAX_MIXPLUGINS) + nMixPlug = 0; +} + + +std::map<SAMPLEINDEX, int8> ModInstrument::CanConvertToDefaultNoteMap() const +{ + std::map<SAMPLEINDEX, int8> transposeMap; + for(size_t i = 0; i < std::size(NoteMap); i++) + { + if(Keyboard[i] == 0) + continue; + if(!NoteMap[i] || NoteMap[i] == (i + 1)) + continue; + + const int8 relativeNote = static_cast<int8>(NoteMap[i] - (i + NOTE_MIN)); + if(transposeMap.count(Keyboard[i]) && transposeMap[Keyboard[i]] != relativeNote) + return {}; + transposeMap[Keyboard[i]] = relativeNote; + } + return transposeMap; +} + + +void ModInstrument::Transpose(int8 amount) +{ + for(auto ¬e : NoteMap) + { + note = static_cast<uint8>(Clamp(note + amount, NOTE_MIN, NOTE_MAX)); + } +} + + +uint8 ModInstrument::GetMIDIChannel(const ModChannel &channel, CHANNELINDEX chn) const +{ + // For mapped channels, return their pattern channel, modulo 16 (because there are only 16 MIDI channels) + if(nMidiChannel == MidiMappedChannel) + return static_cast<uint8>((channel.nMasterChn ? (channel.nMasterChn - 1u) : chn) % 16u); + else if(HasValidMIDIChannel()) + return (nMidiChannel - MidiFirstChannel) % 16u; + else + return 0; + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.h b/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.h new file mode 100644 index 00000000..2c04fa76 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModInstrument.h @@ -0,0 +1,197 @@ +/* + * ModInstrument.h + * --------------- + * Purpose: Module Instrument header class and helpers + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "modcommand.h" +#include "tuningbase.h" +#include "Snd_defs.h" +#include "openmpt/base/FlagSet.hpp" +#include "../common/misc_util.h" +#include <map> +#include <set> + +OPENMPT_NAMESPACE_BEGIN + +struct ModChannel; + +// Instrument Nodes +struct EnvelopeNode +{ + using tick_t = uint16; + using value_t = uint8; + + tick_t tick = 0; // Envelope node position (x axis) + value_t value = 0; // Envelope node value (y axis) + + EnvelopeNode() { } + EnvelopeNode(tick_t tick, value_t value) : tick(tick), value(value) { } + + bool operator== (const EnvelopeNode &other) const { return tick == other.tick && value == other.value; } +}; + +// Instrument Envelopes +struct InstrumentEnvelope : public std::vector<EnvelopeNode> +{ + FlagSet<EnvelopeFlags> dwFlags; // Envelope flags + uint8 nLoopStart = 0; // Loop start node + uint8 nLoopEnd = 0; // Loop end node + uint8 nSustainStart = 0; // Sustain start node + uint8 nSustainEnd = 0; // Sustain end node + uint8 nReleaseNode = ENV_RELEASE_NODE_UNSET; // Release node + + // Convert envelope data between various formats. + void Convert(MODTYPE fromType, MODTYPE toType); + + // Get envelope value at a given tick. Assumes that the envelope data is in rage [0, rangeIn], + // returns value in range [0, rangeOut]. + int32 GetValueFromPosition(int position, int32 rangeOut, int32 rangeIn = ENVELOPE_MAX) const; + + // Ensure that ticks are ordered in increasing order and values are within the allowed range. + void Sanitize(uint8 maxValue = ENVELOPE_MAX); + + uint32 size() const { return static_cast<uint32>(std::vector<EnvelopeNode>::size()); } + + using std::vector<EnvelopeNode>::push_back; + void push_back(EnvelopeNode::tick_t tick, EnvelopeNode::value_t value) { emplace_back(tick, value); } +}; + +// Instrument Struct +struct ModInstrument +{ + uint32 nFadeOut = 256; // Instrument fadeout speed + uint32 nGlobalVol = 64; // Global volume (0...64, all sample volumes are multiplied with this - TODO: This is 0...128 in Impulse Tracker) + uint32 nPan = 32 * 4; // Default pan (0...256), if the appropriate flag is set. Sample panning overrides instrument panning. + + uint16 nVolRampUp = 0; // Default sample ramping up, 0 = use global default + + ResamplingMode resampling = SRCMODE_DEFAULT; // Resampling mode + + FlagSet<InstrumentFlags> dwFlags; // Instrument flags + NewNoteAction nNNA = NewNoteAction::NoteCut; // New note action + DuplicateCheckType nDCT = DuplicateCheckType::None; // Duplicate check type (i.e. which condition will trigger the duplicate note action) + DuplicateNoteAction nDNA = DuplicateNoteAction::NoteCut; // Duplicate note action + + uint8 nPanSwing = 0; // Random panning factor (0...64) + uint8 nVolSwing = 0; // Random volume factor (0...100) + + uint8 nIFC = 0; // Default filter cutoff (0...127). Used if the high bit is set + uint8 nIFR = 0; // Default filter resonance (0...127). Used if the high bit is set + uint8 nCutSwing = 0; // Random cutoff factor (0...64) + uint8 nResSwing = 0; // Random resonance factor (0...64) + FilterMode filterMode = FilterMode::Unchanged; // Default filter mode + + int8 nPPS = 0; // Pitch/Pan separation (i.e. how wide the panning spreads, -32...32) + uint8 nPPC = NOTE_MIDDLEC - NOTE_MIN; // Pitch/Pan centre (zero-based) + + uint16 wMidiBank = 0; // MIDI Bank (1...16384). 0 = Don't send. + uint8 nMidiProgram = 0; // MIDI Program (1...128). 0 = Don't send. + uint8 nMidiChannel = 0; // MIDI Channel (1...16). 0 = Don't send. 17 = Mapped (Send to tracker channel modulo 16). + uint8 nMidiDrumKey = 0; // Drum set note mapping (currently only used by the .MID loader) + int8 midiPWD = 2; // MIDI Pitch Wheel Depth and CMD_FINETUNE depth in semitones + PLUGINDEX nMixPlug = 0; // Plugin assigned to this instrument (0 = no plugin, 1 = first plugin) + + PlugVelocityHandling pluginVelocityHandling = PLUGIN_VELOCITYHANDLING_CHANNEL; // How to deal with plugin velocity + PlugVolumeHandling pluginVolumeHandling = PLUGIN_VOLUMEHANDLING_IGNORE; // How to deal with plugin volume + + TEMPO pitchToTempoLock; // BPM at which the samples assigned to this instrument loop correctly (0 = unset) + CTuning *pTuning = nullptr; // sample tuning assigned to this instrument + + InstrumentEnvelope VolEnv; // Volume envelope data + InstrumentEnvelope PanEnv; // Panning envelope data + InstrumentEnvelope PitchEnv; // Pitch / filter envelope data + + std::array<uint8, 128> NoteMap; // Note mapping, e.g. C-5 => D-5 + std::array<SAMPLEINDEX, 128> Keyboard; // Sample mapping, e.g. C-5 => Sample 1 + + mpt::charbuf<MAX_INSTRUMENTNAME> name; + mpt::charbuf<MAX_INSTRUMENTFILENAME> filename; + + std::string GetName() const { return name; } + std::string GetFilename() const { return filename; } + + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + // WHEN adding new members here, ALSO update InstrumentExtensions.cpp + // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + + ModInstrument(SAMPLEINDEX sample = 0); + + // Assign all notes to a given sample. + void AssignSample(SAMPLEINDEX sample) + { + Keyboard.fill(sample); + } + + // Reset note mapping (i.e. every note is mapped to itself) + void ResetNoteMap() + { + std::iota(NoteMap.begin(), NoteMap.end(), static_cast<uint8>(NOTE_MIN)); + } + + // If the instrument has a non-default note mapping and can be simplified to use the default note mapping by transposing samples, + // the list of samples that would need to be transposed and the corresponding transpose values are returned - otherwise an empty map. + std::map<SAMPLEINDEX, int8> CanConvertToDefaultNoteMap() const; + + // Transpose entire note mapping by given number of semitones + void Transpose(int8 amount); + + bool IsCutoffEnabled() const { return (nIFC & 0x80) != 0; } + bool IsResonanceEnabled() const { return (nIFR & 0x80) != 0; } + uint8 GetCutoff() const { return (nIFC & 0x7F); } + uint8 GetResonance() const { return (nIFR & 0x7F); } + void SetCutoff(uint8 cutoff, bool enable) { nIFC = std::min(cutoff, uint8(0x7F)) | (enable ? 0x80 : 0x00); } + void SetResonance(uint8 resonance, bool enable) { nIFR = std::min(resonance, uint8(0x7F)) | (enable ? 0x80 : 0x00); } + + bool HasValidMIDIChannel() const { return (nMidiChannel >= 1 && nMidiChannel <= 17); } + uint8 GetMIDIChannel(const ModChannel &channel, CHANNELINDEX chn) const; + + void SetTuning(CTuning *pT) + { + pTuning = pT; + } + + // Get a reference to a specific envelope of this instrument + const InstrumentEnvelope &GetEnvelope(EnvelopeType envType) const + { + switch(envType) + { + case ENV_VOLUME: + default: + return VolEnv; + case ENV_PANNING: + return PanEnv; + case ENV_PITCH: + return PitchEnv; + } + } + + InstrumentEnvelope &GetEnvelope(EnvelopeType envType) + { + return const_cast<InstrumentEnvelope &>(static_cast<const ModInstrument &>(*this).GetEnvelope(envType)); + } + + // Get a set of all samples referenced by this instrument + std::set<SAMPLEINDEX> GetSamples() const; + + // Write sample references into a bool vector. If a sample is referenced by this instrument, true is written. + // The caller has to initialize the vector. + void GetSamples(std::vector<bool> &referencedSamples) const; + + // Translate instrument properties between two given formats. + void Convert(MODTYPE fromType, MODTYPE toType); + + // Sanitize all instrument data. + void Sanitize(MODTYPE modType); + +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.cpp new file mode 100644 index 00000000..9e9597ca --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.cpp @@ -0,0 +1,579 @@ +/* + * ModSample.h + * ----------- + * Purpose: Module Sample header class and helpers + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "ModSample.h" +#include "modsmp_ctrl.h" +#include "mpt/base/numbers.hpp" + +#include <cmath> + + +OPENMPT_NAMESPACE_BEGIN + + +// Translate sample properties between two given formats. +void ModSample::Convert(MODTYPE fromType, MODTYPE toType) +{ + // Convert between frequency and transpose values if necessary. + if((!(toType & (MOD_TYPE_MOD | MOD_TYPE_XM))) && (fromType & (MOD_TYPE_MOD | MOD_TYPE_XM))) + { + TransposeToFrequency(); + RelativeTone = 0; + nFineTune = 0; + // TransposeToFrequency assumes NTSC middle-C frequency like FT2, but we play MODs with PAL middle-C! + if(fromType == MOD_TYPE_MOD) + nC5Speed = Util::muldivr_unsigned(nC5Speed, 8272, 8363); + } else if((toType & (MOD_TYPE_MOD | MOD_TYPE_XM)) && (!(fromType & (MOD_TYPE_MOD | MOD_TYPE_XM)))) + { + // FrequencyToTranspose assumes NTSC middle-C frequency like FT2, but we play MODs with PAL middle-C! + if(toType == MOD_TYPE_MOD) + nC5Speed = Util::muldivr_unsigned(nC5Speed, 8363, 8272); + FrequencyToTranspose(); + } + + // No ping-pong loop, panning and auto-vibrato for MOD / S3M samples + if(toType & (MOD_TYPE_MOD | MOD_TYPE_S3M)) + { + uFlags.reset(CHN_PINGPONGLOOP | CHN_PANNING); + + nVibDepth = 0; + nVibRate = 0; + nVibSweep = 0; + nVibType = VIB_SINE; + + RelativeTone = 0; + } + + // No global volume / sustain loops for MOD/S3M/XM + if(toType & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_S3M)) + { + nGlobalVol = 64; + // Sustain loops - convert to normal loops + if(uFlags[CHN_SUSTAINLOOP]) + { + // We probably overwrite a normal loop here, but since sustain loops are evaluated before normal loops, this is just correct. + nLoopStart = nSustainStart; + nLoopEnd = nSustainEnd; + uFlags.set(CHN_LOOP); + uFlags.set(CHN_PINGPONGLOOP, uFlags[CHN_PINGPONGSUSTAIN]); + } + nSustainStart = nSustainEnd = 0; + uFlags.reset(CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN); + } + + // All XM samples have default panning, and XM's autovibrato settings are rather limited. + if(toType & MOD_TYPE_XM) + { + if(!uFlags[CHN_PANNING]) + { + uFlags.set(CHN_PANNING); + nPan = 128; + } + + LimitMax(nVibDepth, uint8(15)); + 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)))) + { + if(nVibRate != 0 && nVibDepth != 0) + { + if(nVibSweep != 0) + nVibSweep = mpt::saturate_cast<decltype(nVibSweep)>(Util::muldivr_unsigned(nVibDepth, 256, nVibSweep)); + else + nVibSweep = 255; + } + } + // Convert incompatible autovibrato types + if(toType == MOD_TYPE_IT && nVibType == VIB_RAMP_UP) + { + nVibType = VIB_RAMP_DOWN; + } else if(toType == MOD_TYPE_XM && nVibType == VIB_RANDOM) + { + nVibType = VIB_SINE; + } + + // No external samples in formats other than MPTM. + if(toType != MOD_TYPE_MPT) + { + uFlags.reset(SMP_KEEPONDISK); + } + + // No Adlib instruments in formats that can't handle it. + if(!CSoundFile::SupportsOPL(toType) && uFlags[CHN_ADLIB]) + { + SetAdlib(false); + } else if(toType == MOD_TYPE_S3M && uFlags[CHN_ADLIB]) + { + // No support for OPL3 waveforms in S3M + adlib[8] &= 0x03; + adlib[9] &= 0x03; + } +} + + +// Initialize sample slot with default values. +void ModSample::Initialize(MODTYPE type) +{ + FreeSample(); + nLength = 0; + nLoopStart = nLoopEnd = 0; + nSustainStart = nSustainEnd = 0; + nC5Speed = 8363; + nPan = 128; + nVolume = 256; + nGlobalVol = 64; + uFlags.reset(CHN_PANNING | CHN_SUSTAINLOOP | CHN_LOOP | CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN | CHN_ADLIB | SMP_MODIFIED | SMP_KEEPONDISK); + if(type == MOD_TYPE_XM) + { + uFlags.set(CHN_PANNING); + } + RelativeTone = 0; + nFineTune = 0; + nVibType = VIB_SINE; + nVibSweep = 0; + nVibDepth = 0; + nVibRate = 0; + rootNote = 0; + filename = ""; + + RemoveAllCuePoints(); +} + + +// Returns sample rate of the sample. +uint32 ModSample::GetSampleRate(const MODTYPE type) const +{ + uint32 rate; + if(CSoundFile::UseFinetuneAndTranspose(type)) + rate = TransposeToFrequency(RelativeTone, nFineTune); + else + rate = nC5Speed; + // TransposeToFrequency assumes NTSC middle-C frequency like FT2, but we play MODs with PAL middle-C! + if(type == MOD_TYPE_MOD) + rate = Util::muldivr_unsigned(rate, 8272, 8363); + return (rate > 0) ? rate : 8363; +} + + +// Copies sample data from another sample slot and ensures that the 16-bit/stereo flags are set accordingly. +bool ModSample::CopyWaveform(const ModSample &smpFrom) +{ + if(!smpFrom.HasSampleData()) + return false; + // If we duplicate a sample slot, avoid deleting the sample we just copy from + if(smpFrom.sampleb() == sampleb()) + pData.pSample = nullptr; + LimitMax(nLength, smpFrom.nLength); + uFlags.set(CHN_16BIT, smpFrom.uFlags[CHN_16BIT]); + uFlags.set(CHN_STEREO, smpFrom.uFlags[CHN_STEREO]); + if(AllocateSample()) + { + memcpy(sampleb(), smpFrom.sampleb(), GetSampleSizeInBytes()); + return true; + } + return false; + +} + + +// Allocate sample based on a ModSample's properties. +// Returns number of bytes allocated, 0 on failure. +size_t ModSample::AllocateSample() +{ + FreeSample(); + + if((pData.pSample = AllocateSample(nLength, GetBytesPerSample())) == nullptr) + { + return 0; + } else + { + return GetSampleSizeInBytes(); + } +} + + +// Allocate sample memory. On success, a pointer to the silenced sample buffer is returned. On failure, nullptr is returned. +// numFrames must contain the sample length, bytesPerSample the size of a sampling point multiplied with the number of channels. +void *ModSample::AllocateSample(SmpLength numFrames, size_t bytesPerSample) +{ + const size_t allocSize = GetRealSampleBufferSize(numFrames, bytesPerSample); + + if(allocSize != 0) + { + char *p = new(std::nothrow) char[allocSize]; + if(p != nullptr) + { + memset(p, 0, allocSize); + return p + (InterpolationLookaheadBufferSize * MaxSamplingPointSize); + } + } + return nullptr; +} + + +// Compute sample buffer size in bytes, including any overhead introduced by pre-computed loops and such. Returns 0 if sample is too big. +size_t ModSample::GetRealSampleBufferSize(SmpLength numSamples, size_t bytesPerSample) +{ + // Number of required lookahead samples: + // * 1x InterpolationMaxLookahead samples before the actual sample start. This is set to MaxSamplingPointSize due to the way AllocateSample/FreeSample currently work. + // * 1x InterpolationMaxLookahead samples of silence after the sample end (if normal loop end == sample end, this can be optimized out). + // * 2x InterpolationMaxLookahead before the loop point (because we start at InterpolationMaxLookahead before the loop point and will look backwards from there as well) + // * 2x InterpolationMaxLookahead after the loop point (for wrap-around) + // * 4x InterpolationMaxLookahead for the sustain loop (same as the two points above) + + const SmpLength maxSize = Util::MaxValueOfType(numSamples); + const SmpLength lookaheadBufferSize = (MaxSamplingPointSize + 1 + 4 + 4) * InterpolationLookaheadBufferSize; + + if(numSamples == 0 || numSamples > MAX_SAMPLE_LENGTH || lookaheadBufferSize > maxSize - numSamples) + { + return 0; + } + numSamples += lookaheadBufferSize; + + if(maxSize / bytesPerSample < numSamples) + { + return 0; + } + + return numSamples * bytesPerSample; +} + + +void ModSample::FreeSample() +{ + FreeSample(pData.pSample); + pData.pSample = nullptr; +} + + +void ModSample::FreeSample(void *samplePtr) +{ + if(samplePtr) + { + delete[](((char *)samplePtr) - (InterpolationLookaheadBufferSize * MaxSamplingPointSize)); + } +} + + +// Set loop points and update loop wrap-around buffer +void ModSample::SetLoop(SmpLength start, SmpLength end, bool enable, bool pingpong, CSoundFile &sndFile) +{ + nLoopStart = start; + nLoopEnd = end; + LimitMax(nLoopEnd, nLength); + if(nLoopStart < nLoopEnd) + { + uFlags.set(CHN_LOOP, enable); + uFlags.set(CHN_PINGPONGLOOP, pingpong && enable); + } else + { + nLoopStart = nLoopEnd = 0; + uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP); + } + PrecomputeLoops(sndFile, true); +} + + +// Set sustain loop points and update loop wrap-around buffer +void ModSample::SetSustainLoop(SmpLength start, SmpLength end, bool enable, bool pingpong, CSoundFile &sndFile) +{ + nSustainStart = start; + nSustainEnd = end; + LimitMax(nLoopEnd, nLength); + if(nSustainStart < nSustainEnd) + { + uFlags.set(CHN_SUSTAINLOOP, enable); + uFlags.set(CHN_PINGPONGSUSTAIN, pingpong && enable); + } else + { + nSustainStart = nSustainEnd = 0; + uFlags.reset(CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN); + } + PrecomputeLoops(sndFile, true); +} + + +namespace // Unnamed namespace for local implementation functions. +{ + +template <typename T> +class PrecomputeLoop +{ +protected: + T *target; + const T *sampleData; + SmpLength loopEnd; + int numChannels; + bool pingpong; + bool ITPingPongMode; + +public: + PrecomputeLoop(T *target, const T *sampleData, SmpLength loopEnd, int numChannels, bool pingpong, bool ITPingPongMode) + : target(target), sampleData(sampleData), loopEnd(loopEnd), numChannels(numChannels), pingpong(pingpong), ITPingPongMode(ITPingPongMode) + { + if(loopEnd > 0) + { + CopyLoop(true); + CopyLoop(false); + } + } + + void CopyLoop(bool direction) const + { + // Direction: true = start reading and writing forward, false = start reading and writing backward (write direction never changes) + const int numSamples = 2 * InterpolationLookaheadBufferSize + (direction ? 1 : 0); // Loop point is included in forward loop expansion + T *dest = target + numChannels * (2 * InterpolationLookaheadBufferSize - 1); // Write buffer offset + SmpLength readPosition = loopEnd - 1; + const int writeIncrement = direction ? 1 : -1; + int readIncrement = writeIncrement; + + for(int i = 0; i < numSamples; i++) + { + // Copy sample over to lookahead buffer + for(int c = 0; c < numChannels; c++) + { + dest[c] = sampleData[readPosition * numChannels + c]; + } + dest += writeIncrement * numChannels; + + if(readPosition == loopEnd - 1 && readIncrement > 0) + { + // Reached end of loop while going forward + if(pingpong) + { + readIncrement = -1; + if(ITPingPongMode && readPosition > 0) + { + readPosition--; + } + } else + { + readPosition = 0; + } + } else if(readPosition == 0 && readIncrement < 0) + { + // Reached start of loop while going backward + if(pingpong) + { + readIncrement = 1; + } else + { + readPosition = loopEnd - 1; + } + } else + { + readPosition += readIncrement; + } + } + } +}; + + +template <typename T> +void PrecomputeLoopsImpl(ModSample &smp, const CSoundFile &sndFile) +{ + const int numChannels = smp.GetNumChannels(); + const int copySamples = numChannels * InterpolationLookaheadBufferSize; + + T *sampleData = static_cast<T *>(smp.samplev()); + T *afterSampleStart = sampleData + smp.nLength * numChannels; + T *loopLookAheadStart = afterSampleStart + copySamples; + T *sustainLookAheadStart = loopLookAheadStart + 4 * copySamples; + + // Hold sample on the same level as the last sampling point at the end to prevent extra pops with interpolation. + // Do the same at the sample start, too. + for(int i = 0; i < (int)InterpolationLookaheadBufferSize; i++) + { + for(int c = 0; c < numChannels; c++) + { + afterSampleStart[i * numChannels + c] = afterSampleStart[-numChannels + c]; + sampleData[-(i + 1) * numChannels + c] = sampleData[c]; + } + } + + if(smp.uFlags[CHN_LOOP]) + { + PrecomputeLoop<T>(loopLookAheadStart, + sampleData + smp.nLoopStart * numChannels, + smp.nLoopEnd - smp.nLoopStart, + numChannels, + smp.uFlags[CHN_PINGPONGLOOP], + sndFile.m_playBehaviour[kITPingPongMode]); + } + if(smp.uFlags[CHN_SUSTAINLOOP]) + { + PrecomputeLoop<T>(sustainLookAheadStart, + sampleData + smp.nSustainStart * numChannels, + smp.nSustainEnd - smp.nSustainStart, + numChannels, + smp.uFlags[CHN_PINGPONGSUSTAIN], + sndFile.m_playBehaviour[kITPingPongMode]); + } +} + +} // unnamed namespace + + +void ModSample::PrecomputeLoops(CSoundFile &sndFile, bool updateChannels) +{ + if(!HasSampleData()) + return; + + SanitizeLoops(); + + // Update channels with possibly changed loop values + if(updateChannels) + { + ctrlSmp::UpdateLoopPoints(*this, sndFile); + } + + if(GetElementarySampleSize() == 2) + PrecomputeLoopsImpl<int16>(*this, sndFile); + else if(GetElementarySampleSize() == 1) + PrecomputeLoopsImpl<int8>(*this, sndFile); +} + + +// Remove loop points if they're invalid. +void ModSample::SanitizeLoops() +{ + LimitMax(nSustainEnd, nLength); + LimitMax(nLoopEnd, nLength); + if(nSustainStart >= nSustainEnd) + { + nSustainStart = nSustainEnd = 0; + uFlags.reset(CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN); + } + if(nLoopStart >= nLoopEnd) + { + nLoopStart = nLoopEnd = 0; + uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP); + } +} + + +///////////////////////////////////////////////////////////// +// Transpose <-> Frequency conversions + +uint32 ModSample::TransposeToFrequency(int transpose, int finetune) +{ + return mpt::saturate_round<uint32>(std::pow(2.0, (transpose * 128.0 + finetune) * (1.0 / (12.0 * 128.0))) * 8363.0); +} + + +void ModSample::TransposeToFrequency() +{ + nC5Speed = TransposeToFrequency(RelativeTone, nFineTune); +} + + +// Return a pair of {transpose, finetune} +std::pair<int8, int8> ModSample::FrequencyToTranspose(uint32 freq) +{ + if(!freq) + return {}; + + const auto f2t = mpt::saturate_round<int32>(std::log(freq * (1.0 / 8363.0)) * (12.0 * 128.0 * (1.0 / mpt::numbers::ln2))); + const auto fine = std::div(Clamp(f2t, -16384, 16383), int32(128)); + return {static_cast<int8>(fine.quot), static_cast<int8>(fine.rem)}; +} + + +void ModSample::FrequencyToTranspose() +{ + std::tie(RelativeTone, nFineTune) = FrequencyToTranspose(nC5Speed); +} + + +// Transpose the sample by amount specified in octaves (i.e. amount=1 transposes one octave up) +void ModSample::Transpose(double amount) +{ + nC5Speed = mpt::saturate_round<uint32>(nC5Speed * std::pow(2.0, amount)); +} + + +// Check if the sample has any valid cue points +bool ModSample::HasAnyCuePoints() const +{ + if(uFlags[CHN_ADLIB]) + return false; + for(auto pt : cues) + { + if(pt < nLength) + return true; + } + return false; +} + + +// Check if the sample's cue points are the default cue point set. +bool ModSample::HasCustomCuePoints() const +{ + if(uFlags[CHN_ADLIB]) + return false; + for(SmpLength i = 0; i < std::size(cues); i++) + { + if(cues[i] != (i + 1) << 11) + return true; + } + return false; +} + + +void ModSample::SetDefaultCuePoints() +{ + // Default cues compatible with old-style volume column offset + for(int i = 0; i < 9; i++) + { + cues[i] = (i + 1) << 11; + } +} + + +void ModSample::Set16BitCuePoints() +{ + // Cue points that are useful for extending regular offset command + for(int i = 0; i < 9; i++) + { + cues[i] = (i + 1) << 16; + } +} + + +void ModSample::RemoveAllCuePoints() +{ + if(!uFlags[CHN_ADLIB]) + cues.fill(MAX_SAMPLE_LENGTH); +} + + +void ModSample::SetAdlib(bool enable, OPLPatch patch) +{ + if(!enable && uFlags[CHN_ADLIB]) + { + SetDefaultCuePoints(); + } + uFlags.set(CHN_ADLIB, enable); + if(enable) + { + // Bogus sample to make playback work + uFlags.reset(CHN_16BIT | CHN_STEREO); + nLength = 4; + AllocateSample(); + adlib = patch; + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.h b/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.h new file mode 100644 index 00000000..07a2e1f4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModSample.h @@ -0,0 +1,175 @@ +/* + * ModSample.h + * ----------- + * Purpose: Module Sample header class and helpers + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +// Sample Struct +struct ModSample +{ + SmpLength nLength; // In frames + SmpLength nLoopStart, nLoopEnd; // Ditto + SmpLength nSustainStart, nSustainEnd; // Ditto + union + { + void *pSample; // Pointer to sample data + int8 *pSample8; // Pointer to 8-bit sample data + int16 *pSample16; // Pointer to 16-bit sample data + } pData; + uint32 nC5Speed; // Frequency of middle-C, in Hz (for IT/S3M/MPTM) + uint16 nPan; // Default sample panning (if pan flag is set), 0...256 + uint16 nVolume; // Default volume, 0...256 (ignored if uFlags[SMP_NODEFAULTVOLUME] is set) + uint16 nGlobalVol; // Global volume (sample volume is multiplied by this), 0...64 + SampleFlags uFlags; // Sample flags (see ChannelFlags enum) + int8 RelativeTone; // Relative note to middle c (for MOD/XM) + int8 nFineTune; // Finetune period (for MOD/XM), -128...127, unit is 1/128th of a semitone + VibratoType nVibType; // Auto vibrato type + uint8 nVibSweep; // Auto vibrato sweep (i.e. how long it takes until the vibrato effect reaches its full depth) + uint8 nVibDepth; // Auto vibrato depth + uint8 nVibRate; // Auto vibrato rate (speed) + uint8 rootNote; // For multisample import + + //char name[MAX_SAMPLENAME]; // Maybe it would be nicer to have sample names here, but that would require some refactoring. + mpt::charbuf<MAX_SAMPLEFILENAME> filename; + std::string GetFilename() const { return filename; } + + union + { + std::array<SmpLength, 9> cues; + OPLPatch adlib; + }; + + ModSample(MODTYPE type = MOD_TYPE_NONE) + { + pData.pSample = nullptr; + Initialize(type); + } + + bool HasSampleData() const noexcept + { + MPT_ASSERT(!pData.pSample || (pData.pSample && nLength > 0)); // having sample pointer implies non-zero sample length + return pData.pSample != nullptr && nLength != 0; + } + + MPT_FORCEINLINE const void *samplev() const noexcept + { + return pData.pSample; + } + MPT_FORCEINLINE void *samplev() noexcept + { + return pData.pSample; + } + MPT_FORCEINLINE const std::byte *sampleb() const noexcept + { + return mpt::void_cast<const std::byte*>(pData.pSample); + } + MPT_FORCEINLINE std::byte *sampleb() noexcept + { + return mpt::void_cast<std::byte*>(pData.pSample); + } + MPT_FORCEINLINE const int8 *sample8() const noexcept + { + MPT_ASSERT(GetElementarySampleSize() == sizeof(int8)); + return pData.pSample8; + } + MPT_FORCEINLINE int8 *sample8() noexcept + { + MPT_ASSERT(GetElementarySampleSize() == sizeof(int8)); + return pData.pSample8; + } + MPT_FORCEINLINE const int16 *sample16() const noexcept + { + MPT_ASSERT(GetElementarySampleSize() == sizeof(int16)); + return pData.pSample16; + } + MPT_FORCEINLINE int16 *sample16() noexcept + { + MPT_ASSERT(GetElementarySampleSize() == sizeof(int16)); + return pData.pSample16; + } + + // Return the size of one (elementary) sample in bytes. + uint8 GetElementarySampleSize() const noexcept { return (uFlags & CHN_16BIT) ? 2 : 1; } + + // Return the number of channels in the sample. + uint8 GetNumChannels() const noexcept { return (uFlags & CHN_STEREO) ? 2 : 1; } + + // Return the number of bytes per frame (Channels * Elementary Sample Size) + uint8 GetBytesPerSample() const noexcept { return GetElementarySampleSize() * GetNumChannels(); } + + // Return the size which pSample is at least. + SmpLength GetSampleSizeInBytes() const noexcept { return nLength * GetBytesPerSample(); } + + // Returns sample rate of the sample. The argument is needed because + // the sample rate is obtained differently for different module types. + uint32 GetSampleRate(const MODTYPE type) const; + + // Translate sample properties between two given formats. + void Convert(MODTYPE fromType, MODTYPE toType); + + // Initialize sample slot with default values. + void Initialize(MODTYPE type = MOD_TYPE_NONE); + + // Copies sample data from another sample slot and ensures that the 16-bit/stereo flags are set accordingly. + bool CopyWaveform(const ModSample &smpFrom); + + // Allocate sample based on a ModSample's properties. + // Returns number of bytes allocated, 0 on failure. + size_t AllocateSample(); + // Allocate sample memory. On sucess, a pointer to the silenced sample buffer is returned. On failure, nullptr is returned. + static void *AllocateSample(SmpLength numFrames, size_t bytesPerSample); + // Compute sample buffer size in bytes, including any overhead introduced by pre-computed loops and such. Returns 0 if sample is too big. + static size_t GetRealSampleBufferSize(SmpLength numSamples, size_t bytesPerSample); + + void FreeSample(); + static void FreeSample(void *samplePtr); + + // Set loop points and update loop wrap-around buffer + void SetLoop(SmpLength start, SmpLength end, bool enable, bool pingpong, CSoundFile &sndFile); + // Set sustain loop points and update loop wrap-around buffer + void SetSustainLoop(SmpLength start, SmpLength end, bool enable, bool pingpong, CSoundFile &sndFile); + // Update loop wrap-around buffer + void PrecomputeLoops(CSoundFile &sndFile, bool updateChannels = true); + + constexpr bool HasLoop() const noexcept { return uFlags[CHN_LOOP] && nLoopEnd > nLoopStart; } + constexpr bool HasSustainLoop() const noexcept { return uFlags[CHN_SUSTAINLOOP] && nSustainEnd > nSustainStart; } + constexpr bool HasPingPongLoop() const noexcept { return uFlags.test_all(CHN_LOOP | CHN_PINGPONGLOOP) && nLoopEnd > nLoopStart; } + constexpr bool HasPingPongSustainLoop() const noexcept { return uFlags.test_all(CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN) && nSustainEnd > nSustainStart; } + + // Remove loop points if they're invalid. + void SanitizeLoops(); + + // Transpose <-> Frequency conversions + static uint32 TransposeToFrequency(int transpose, int finetune = 0); + void TransposeToFrequency(); + static std::pair<int8, int8> FrequencyToTranspose(uint32 freq); + void FrequencyToTranspose(); + + // Transpose the sample by amount specified in octaves (i.e. amount=1 transposes one octave up) + void Transpose(double amount); + + // Check if the sample has any valid cue points + bool HasAnyCuePoints() const; + // Check if the sample's cue points are the default cue point set. + bool HasCustomCuePoints() const; + void SetDefaultCuePoints(); + // Set cue points so that they are suitable for regular offset command extension + void Set16BitCuePoints(); + void RemoveAllCuePoints(); + + void SetAdlib(bool enable, OPLPatch patch = OPLPatch{{}}); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModSampleCopy.h b/Src/external_dependencies/openmpt-trunk/soundlib/ModSampleCopy.h new file mode 100644 index 00000000..0eda92e5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModSampleCopy.h @@ -0,0 +1,157 @@ +/* + * ModSampleCopy.h + * --------------- + * Purpose: Functions for copying ModSample data. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "openmpt/soundbase/SampleDecode.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +struct ModSample; + +// Copy a mono sample data buffer. +template <typename SampleConversion, typename Tbyte> +size_t CopyMonoSample(ModSample &sample, const Tbyte *sourceBuffer, size_t sourceSize, SampleConversion conv = SampleConversion()) +{ + MPT_ASSERT(sample.GetNumChannels() == 1); + MPT_ASSERT(sample.GetElementarySampleSize() == sizeof(typename SampleConversion::output_t)); + + const size_t frameSize = SampleConversion::input_inc; + const size_t countFrames = std::min(sourceSize / frameSize, static_cast<std::size_t>(sample.nLength)); + size_t numFrames = countFrames; + SampleConversion sampleConv(conv); + const std::byte * MPT_RESTRICT inBuf = mpt::byte_cast<const std::byte*>(sourceBuffer); + typename SampleConversion::output_t * MPT_RESTRICT outBuf = static_cast<typename SampleConversion::output_t *>(sample.samplev()); + while(numFrames--) + { + *outBuf = sampleConv(inBuf); + inBuf += SampleConversion::input_inc; + outBuf++; + } + return frameSize * countFrames; +} + + +// Copy a stereo interleaved sample data buffer. +template <typename SampleConversion, typename Tbyte> +size_t CopyStereoInterleavedSample(ModSample &sample, const Tbyte *sourceBuffer, size_t sourceSize, SampleConversion conv = SampleConversion()) +{ + MPT_ASSERT(sample.GetNumChannels() == 2); + MPT_ASSERT(sample.GetElementarySampleSize() == sizeof(typename SampleConversion::output_t)); + + const size_t frameSize = 2 * SampleConversion::input_inc; + const size_t countFrames = std::min(sourceSize / frameSize, static_cast<std::size_t>(sample.nLength)); + size_t numFrames = countFrames; + SampleConversion sampleConvLeft(conv); + SampleConversion sampleConvRight(conv); + const std::byte * MPT_RESTRICT inBuf = mpt::byte_cast<const std::byte*>(sourceBuffer); + typename SampleConversion::output_t * MPT_RESTRICT outBuf = static_cast<typename SampleConversion::output_t *>(sample.samplev()); + while(numFrames--) + { + *outBuf = sampleConvLeft(inBuf); + inBuf += SampleConversion::input_inc; + outBuf++; + *outBuf = sampleConvRight(inBuf); + inBuf += SampleConversion::input_inc; + outBuf++; + } + return frameSize * countFrames; +} + + +// Copy a stereo split sample data buffer. +template <typename SampleConversion, typename Tbyte> +size_t CopyStereoSplitSample(ModSample &sample, const Tbyte *sourceBuffer, size_t sourceSize, SampleConversion conv = SampleConversion()) +{ + MPT_ASSERT(sample.GetNumChannels() == 2); + MPT_ASSERT(sample.GetElementarySampleSize() == sizeof(typename SampleConversion::output_t)); + + const size_t sampleSize = SampleConversion::input_inc; + const size_t sourceSizeLeft = std::min(static_cast<std::size_t>(sample.nLength) * SampleConversion::input_inc, sourceSize); + const size_t sourceSizeRight = std::min(static_cast<std::size_t>(sample.nLength) * SampleConversion::input_inc, sourceSize - sourceSizeLeft); + const size_t countSamplesLeft = sourceSizeLeft / sampleSize; + const size_t countSamplesRight = sourceSizeRight / sampleSize; + + size_t numSamplesLeft = countSamplesLeft; + SampleConversion sampleConvLeft(conv); + const std::byte * MPT_RESTRICT inBufLeft = mpt::byte_cast<const std::byte*>(sourceBuffer); + typename SampleConversion::output_t * MPT_RESTRICT outBufLeft = static_cast<typename SampleConversion::output_t *>(sample.samplev()); + while(numSamplesLeft--) + { + *outBufLeft = sampleConvLeft(inBufLeft); + inBufLeft += SampleConversion::input_inc; + outBufLeft += 2; + } + + size_t numSamplesRight = countSamplesRight; + SampleConversion sampleConvRight(conv); + const std::byte * MPT_RESTRICT inBufRight = mpt::byte_cast<const std::byte*>(sourceBuffer) + sample.nLength * SampleConversion::input_inc; + typename SampleConversion::output_t * MPT_RESTRICT outBufRight = static_cast<typename SampleConversion::output_t *>(sample.samplev()) + 1; + while(numSamplesRight--) + { + *outBufRight = sampleConvRight(inBufRight); + inBufRight += SampleConversion::input_inc; + outBufRight += 2; + } + + return (countSamplesLeft + countSamplesRight) * sampleSize; +} + + +// Copy a sample data buffer and normalize it. Requires slightly advanced sample conversion functor. +template <typename SampleConversion, typename Tbyte> +size_t CopyAndNormalizeSample(ModSample &sample, const Tbyte *sourceBuffer, size_t sourceSize, typename SampleConversion::peak_t *srcPeak = nullptr, SampleConversion conv = SampleConversion()) +{ + const size_t sampleSize = SampleConversion::input_inc; + + MPT_ASSERT(sample.GetElementarySampleSize() == sizeof(typename SampleConversion::output_t)); + + size_t numSamples = sample.nLength * sample.GetNumChannels(); + LimitMax(numSamples, sourceSize / sampleSize); + + const std::byte * inBuf = mpt::byte_cast<const std::byte*>(sourceBuffer); + // Finding max value + SampleConversion sampleConv(conv); + for(size_t i = numSamples; i != 0; i--) + { + sampleConv.FindMax(inBuf); + inBuf += SampleConversion::input_inc; + } + + // If buffer is silent (maximum is 0), don't bother normalizing the sample - just keep the already silent buffer. + if(!sampleConv.IsSilent()) + { + inBuf = sourceBuffer; + // Copying buffer. + typename SampleConversion::output_t *outBuf = static_cast<typename SampleConversion::output_t *>(sample.samplev()); + + for(size_t i = numSamples; i != 0; i--) + { + *outBuf = sampleConv(inBuf); + outBuf++; + inBuf += SampleConversion::input_inc; + } + } + + if(srcPeak) + { + *srcPeak = sampleConv.GetSrcPeak(); + } + + return numSamples * sampleSize; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.cpp new file mode 100644 index 00000000..b37eac13 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.cpp @@ -0,0 +1,673 @@ +/* + * ModSequence.cpp + * --------------- + * Purpose: Order and sequence handling. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "ModSequence.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#include "../common/version.h" +#include "../common/serialization_utils.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +OPENMPT_NAMESPACE_BEGIN + + +ModSequence::ModSequence(CSoundFile &sndFile) + : m_sndFile(sndFile) +{ +} + + +ModSequence& ModSequence::operator=(const ModSequence &other) +{ + MPT_ASSERT(&other.m_sndFile == &m_sndFile); + if(&other == this) + return *this; + std::vector<PATTERNINDEX>::assign(other.begin(), other.end()); + m_name = other.m_name; + m_restartPos = other.m_restartPos; + return *this; +} + + +bool ModSequence::operator== (const ModSequence &other) const +{ + return static_cast<const std::vector<PATTERNINDEX> &>(*this) == other + && m_name == other.m_name + && m_restartPos == other.m_restartPos; +} + + +bool ModSequence::NeedsExtraDatafield() const +{ + return (m_sndFile.GetType() == MOD_TYPE_MPT && m_sndFile.Patterns.GetNumPatterns() > 0xFD); +} + + +void ModSequence::AdjustToNewModType(const MODTYPE oldtype) +{ + auto &specs = m_sndFile.GetModSpecifications(); + + if(oldtype != MOD_TYPE_NONE) + { + // If not supported, remove "+++" separator order items. + if(!specs.hasIgnoreIndex) + { + RemovePattern(GetIgnoreIndex()); + } + // If not supported, remove "---" items between patterns. + if(!specs.hasStopIndex) + { + RemovePattern(GetInvalidPatIndex()); + } + } + + //Resize orderlist if needed. + if(specs.ordersMax < size()) + { + // Order list too long? Remove "unnecessary" order items first. + if(oldtype != MOD_TYPE_NONE && specs.ordersMax < GetLengthTailTrimmed()) + { + erase(std::remove_if(begin(), end(), [&] (PATTERNINDEX pat) { return !m_sndFile.Patterns.IsValidPat(pat); }), end()); + if(GetLengthTailTrimmed() > specs.ordersMax) + { + m_sndFile.AddToLog(LogWarning, U_("WARNING: Order list has been trimmed!")); + } + } + resize(specs.ordersMax); + } +} + + +ORDERINDEX ModSequence::GetLengthTailTrimmed() const +{ + if(empty()) + return 0; + auto last = std::find_if(rbegin(), rend(), [] (PATTERNINDEX pat) { return pat != GetInvalidPatIndex(); }); + return static_cast<ORDERINDEX>(std::distance(begin(), last.base())); +} + + +ORDERINDEX ModSequence::GetLengthFirstEmpty() const +{ + return static_cast<ORDERINDEX>(std::distance(begin(), std::find(begin(), end(), GetInvalidPatIndex()))); +} + + +ORDERINDEX ModSequence::GetNextOrderIgnoringSkips(const ORDERINDEX start) const +{ + if(empty()) + return 0; + auto length = GetLength(); + ORDERINDEX next = std::min(ORDERINDEX(length - 1), ORDERINDEX(start + 1)); + while(next + 1 < length && at(next) == GetIgnoreIndex()) next++; + return next; +} + + +ORDERINDEX ModSequence::GetPreviousOrderIgnoringSkips(const ORDERINDEX start) const +{ + const ORDERINDEX last = GetLastIndex(); + if(start == 0 || last == 0) return 0; + ORDERINDEX prev = std::min(ORDERINDEX(start - 1), last); + while(prev > 0 && at(prev) == GetIgnoreIndex()) prev--; + return prev; +} + + +void ModSequence::Remove(ORDERINDEX posBegin, ORDERINDEX posEnd) +{ + if(posEnd < posBegin || posEnd >= size()) + return; + erase(begin() + posBegin, begin() + posEnd + 1); +} + + +// Remove all references to a given pattern index from the order list. Jump commands are updated accordingly. +void ModSequence::RemovePattern(PATTERNINDEX pat) +{ + // First, calculate the offset that needs to be applied to jump commands + const ORDERINDEX orderLength = GetLengthTailTrimmed(); + std::vector<ORDERINDEX> newPosition(orderLength); + ORDERINDEX maxJump = 0; + for(ORDERINDEX i = 0; i < orderLength; i++) + { + newPosition[i] = i - maxJump; + if(at(i) == pat) + { + maxJump++; + } + } + if(!maxJump) + { + return; + } + + erase(std::remove(begin(), end(), pat), end()); + + // Only apply to patterns actually found in this sequence + for(auto p : *this) if(m_sndFile.Patterns.IsValidPat(p)) + { + for(auto &m : m_sndFile.Patterns[p]) + { + if(m.command == CMD_POSITIONJUMP && m.param < newPosition.size()) + { + m.param = static_cast<ModCommand::PARAM>(newPosition[m.param]); + } + } + } + if(m_restartPos < newPosition.size()) + { + m_restartPos = newPosition[m_restartPos]; + } +} + + +void ModSequence::assign(ORDERINDEX newSize, PATTERNINDEX pat) +{ + LimitMax(newSize, m_sndFile.GetModSpecifications().ordersMax); + std::vector<PATTERNINDEX>::assign(newSize, pat); +} + + +ORDERINDEX ModSequence::insert(ORDERINDEX pos, ORDERINDEX count, PATTERNINDEX fill) +{ + const auto ordersMax = m_sndFile.GetModSpecifications().ordersMax; + if(pos >= ordersMax || GetLengthTailTrimmed() >= ordersMax || count == 0) + return 0; + // Limit number of orders to be inserted so that we don't exceed the format limit. + LimitMax(count, static_cast<ORDERINDEX>(ordersMax - pos)); + reserve(std::max(pos, GetLength()) + count); + // Inserting past the end of the container? + if(pos > size()) + resize(pos); + std::vector<PATTERNINDEX>::insert(begin() + pos, count, fill); + // Did we overgrow? Remove patterns at end. + if(size() > ordersMax) + resize(ordersMax); + return count; +} + + +bool ModSequence::IsValidPat(ORDERINDEX ord) const +{ + if(ord < size()) + return m_sndFile.Patterns.IsValidPat(at(ord)); + return false; +} + + +CPattern *ModSequence::PatternAt(ORDERINDEX ord) const +{ + if(!IsValidPat(ord)) + return nullptr; + return &m_sndFile.Patterns[at(ord)]; +} + + +ORDERINDEX ModSequence::FindOrder(PATTERNINDEX pat, ORDERINDEX startSearchAt, bool searchForward) const +{ + const ORDERINDEX length = GetLength(); + if(startSearchAt >= length) + return ORDERINDEX_INVALID; + ORDERINDEX ord = startSearchAt; + for(ORDERINDEX p = 0; p < length; p++) + { + if(at(ord) == pat) + { + return ord; + } + if(searchForward) + { + if(++ord >= length) + ord = 0; + } else + { + if(ord-- == 0) + ord = length - 1; + } + } + return ORDERINDEX_INVALID; +} + + +PATTERNINDEX ModSequence::EnsureUnique(ORDERINDEX ord) +{ + PATTERNINDEX pat = at(ord); + if(!IsValidPat(ord)) + return pat; + + for(const auto &sequence : m_sndFile.Order) + { + ORDERINDEX ords = sequence.GetLength(); + for(ORDERINDEX o = 0; o < ords; o++) + { + if(sequence[o] == pat && (o != ord || &sequence != this)) + { + // Found duplicate usage. + PATTERNINDEX newPat = m_sndFile.Patterns.Duplicate(pat); + if(newPat != PATTERNINDEX_INVALID) + { + at(ord) = newPat; + return newPat; + } + } + } + } + return pat; +} + + +///////////////////////////////////// +// ModSequenceSet +///////////////////////////////////// + + +ModSequenceSet::ModSequenceSet(CSoundFile &sndFile) + : m_sndFile(sndFile) +{ + Initialize(); +} + + +void ModSequenceSet::Initialize() +{ + m_currentSeq = 0; + m_Sequences.assign(1, ModSequence(m_sndFile)); +} + + +void ModSequenceSet::SetSequence(SEQUENCEINDEX n) +{ + if(n < m_Sequences.size()) + m_currentSeq = n; +} + + +SEQUENCEINDEX ModSequenceSet::AddSequence() +{ + if(GetNumSequences() >= MAX_SEQUENCES) + return SEQUENCEINDEX_INVALID; + m_Sequences.push_back(ModSequence{m_sndFile}); + SetSequence(GetNumSequences() - 1); + return GetNumSequences() - 1; +} + + +void ModSequenceSet::RemoveSequence(SEQUENCEINDEX i) +{ + // Do nothing if index is invalid or if there's only one sequence left. + if(i >= m_Sequences.size() || m_Sequences.size() <= 1) + return; + m_Sequences.erase(m_Sequences.begin() + i); + if(i < m_currentSeq || m_currentSeq >= GetNumSequences()) + m_currentSeq--; +} + + +#ifdef MODPLUG_TRACKER + +bool ModSequenceSet::Rearrange(const std::vector<SEQUENCEINDEX> &newOrder) +{ + if(newOrder.empty() || newOrder.size() > MAX_SEQUENCES) + return false; + + const auto oldSequences = std::move(m_Sequences); + m_Sequences.assign(newOrder.size(), ModSequence{m_sndFile}); + for(size_t i = 0; i < newOrder.size(); i++) + { + if(newOrder[i] < oldSequences.size()) + m_Sequences[i] = oldSequences[newOrder[i]]; + } + + if(m_currentSeq > m_Sequences.size()) + m_currentSeq = GetNumSequences() - 1u; + return true; +} + + +void ModSequenceSet::OnModTypeChanged(MODTYPE oldType) +{ + for(auto &seq : m_Sequences) + { + seq.AdjustToNewModType(oldType); + } + if(m_sndFile.GetModSpecifications(oldType).sequencesMax > 1 && m_sndFile.GetModSpecifications().sequencesMax <= 1) + MergeSequences(); +} + + +bool ModSequenceSet::CanSplitSubsongs() const +{ + return GetNumSequences() == 1 && m_sndFile.GetModSpecifications().sequencesMax > 1 && m_Sequences[0].HasSubsongs(); +} + + +bool ModSequenceSet::SplitSubsongsToMultipleSequences() +{ + if(!CanSplitSubsongs()) + return false; + + bool modified = false; + const ORDERINDEX length = m_Sequences[0].GetLengthTailTrimmed(); + + for(ORDERINDEX ord = 0; ord < length; ord++) + { + // End of subsong? + if(!m_Sequences[0].IsValidPat(ord) && m_Sequences[0][ord] != GetIgnoreIndex()) + { + // Remove all separator patterns between current and next subsong first + while(ord < length && !m_sndFile.Patterns.IsValidPat(m_Sequences[0][ord])) + { + m_Sequences[0][ord] = GetInvalidPatIndex(); + ord++; + modified = true; + } + if(ord >= length) + break; + + const SEQUENCEINDEX newSeq = AddSequence(); + if(newSeq == SEQUENCEINDEX_INVALID) + break; + + const ORDERINDEX startOrd = ord; + m_Sequences[newSeq].reserve(length - startOrd); + modified = true; + + // Now, move all following orders to the new sequence + while(ord < length && m_Sequences[0][ord] != GetInvalidPatIndex()) + { + PATTERNINDEX copyPat = m_Sequences[0][ord]; + m_Sequences[newSeq].push_back(copyPat); + m_Sequences[0][ord] = GetInvalidPatIndex(); + ord++; + + // Is this a valid pattern? adjust pattern jump commands, if necessary. + if(m_sndFile.Patterns.IsValidPat(copyPat)) + { + for(auto &m : m_sndFile.Patterns[copyPat]) + { + if(m.command == CMD_POSITIONJUMP && m.param >= startOrd) + { + m.param = static_cast<ModCommand::PARAM>(m.param - startOrd); + } + } + } + } + ord--; + } + } + SetSequence(0); + return modified; +} + + +// Convert the sequence's restart position information to a pattern command. +bool ModSequenceSet::RestartPosToPattern(SEQUENCEINDEX seq) +{ + bool result = false; + auto length = m_sndFile.GetLength(eNoAdjust, GetLengthTarget(true).StartPos(seq, 0, 0)); + ModSequence &order = m_Sequences[seq]; + for(const auto &subSong : length) + { + if(subSong.endOrder != ORDERINDEX_INVALID && subSong.endRow != ROWINDEX_INVALID) + { + if(mpt::in_range<ModCommand::PARAM>(order.GetRestartPos())) + { + PATTERNINDEX writePat = order.EnsureUnique(subSong.endOrder); + result = m_sndFile.Patterns[writePat].WriteEffect( + EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(order.GetRestartPos())).Row(subSong.endRow).RetryNextRow()); + } else + { + result = false; + } + } + } + order.SetRestartPos(0); + return result; +} + + +bool ModSequenceSet::MergeSequences() +{ + if(GetNumSequences() <= 1) + return false; + + ModSequence &firstSeq = m_Sequences[0]; + firstSeq.resize(firstSeq.GetLengthTailTrimmed()); + std::vector<SEQUENCEINDEX> patternsFixed(m_sndFile.Patterns.Size(), SEQUENCEINDEX_INVALID); // pattern fixed by other sequence already? + // Mark patterns handled in first sequence + for(auto pat : firstSeq) + { + if(m_sndFile.Patterns.IsValidPat(pat)) + patternsFixed[pat] = 0; + } + + for(SEQUENCEINDEX seqNum = 1; seqNum < GetNumSequences(); seqNum++) + { + ModSequence &seq = m_Sequences[seqNum]; + const ORDERINDEX firstOrder = firstSeq.GetLength() + 1; // +1 for separator item + const ORDERINDEX lengthTrimmed = seq.GetLengthTailTrimmed(); + if(firstOrder + lengthTrimmed > m_sndFile.GetModSpecifications().ordersMax) + { + m_sndFile.AddToLog(LogWarning, MPT_UFORMAT("WARNING: Cannot merge Sequence {} (too long!)")(seqNum + 1)); + continue; + } + firstSeq.reserve(firstOrder + lengthTrimmed); + firstSeq.push_back(); // Separator item + RestartPosToPattern(seqNum); + for(ORDERINDEX ord = 0; ord < lengthTrimmed; ord++) + { + PATTERNINDEX pat = seq[ord]; + firstSeq.push_back(pat); + + // Try to fix pattern jump commands + if(!m_sndFile.Patterns.IsValidPat(pat)) continue; + + auto m = m_sndFile.Patterns[pat].begin(); + for(size_t len = 0; len < m_sndFile.Patterns[pat].GetNumRows() * m_sndFile.m_nChannels; m++, len++) + { + if(m->command == CMD_POSITIONJUMP) + { + if(patternsFixed[pat] != SEQUENCEINDEX_INVALID && patternsFixed[pat] != seqNum) + { + // Oops, some other sequence uses this pattern already. + const PATTERNINDEX newPat = m_sndFile.Patterns.Duplicate(pat, true); + if(newPat != PATTERNINDEX_INVALID) + { + // Could create new pattern - copy data over and continue from here. + firstSeq[firstOrder + ord] = newPat; + m = m_sndFile.Patterns[newPat].begin() + len; + if(newPat >= patternsFixed.size()) + patternsFixed.resize(newPat + 1, SEQUENCEINDEX_INVALID); + pat = newPat; + } else + { + // Cannot create new pattern: notify the user + m_sndFile.AddToLog(LogWarning, MPT_UFORMAT("CONFLICT: Pattern break commands in Pattern {} might be broken since it has been used in several sequences!")(pat)); + } + } + m->param = static_cast<ModCommand::PARAM>(m->param + firstOrder); + patternsFixed[pat] = seqNum; + } + } + } + } + m_Sequences.erase(m_Sequences.begin() + 1, m_Sequences.end()); + return true; +} + + +// Check if a playback position is currently locked (inaccessible) +bool ModSequence::IsPositionLocked(ORDERINDEX position) const +{ + return(m_sndFile.m_lockOrderStart != ORDERINDEX_INVALID + && (position < m_sndFile.m_lockOrderStart || position > m_sndFile.m_lockOrderEnd)); +} + + +bool ModSequence::HasSubsongs() const +{ + const auto endPat = begin() + GetLengthTailTrimmed(); + return std::find_if(begin(), endPat, + [&](PATTERNINDEX pat) { return pat != GetIgnoreIndex() && !m_sndFile.Patterns.IsValidPat(pat); }) != endPat; +} +#endif // MODPLUG_TRACKER + + +///////////////////////////////////// +// Read/Write +///////////////////////////////////// + + +#ifndef MODPLUG_NO_FILESAVE +size_t ModSequence::WriteAsByte(std::ostream &f, const ORDERINDEX count, uint8 stopIndex, uint8 ignoreIndex) const +{ + const size_t limit = std::min(count, GetLength()); + + for(size_t i = 0; i < limit; i++) + { + const PATTERNINDEX pat = at(i); + uint8 temp = static_cast<uint8>(pat); + + if(pat == GetInvalidPatIndex()) temp = stopIndex; + else if(pat == GetIgnoreIndex() || pat > 0xFF) temp = ignoreIndex; + mpt::IO::WriteIntLE<uint8>(f, temp); + } + // Fill non-existing order items with stop indices + for(size_t i = limit; i < count; i++) + { + mpt::IO::WriteIntLE<uint8>(f, stopIndex); + } + return count; //Returns the number of bytes written. +} +#endif // MODPLUG_NO_FILESAVE + + +void ReadModSequenceOld(std::istream& iStrm, ModSequenceSet& seq, const size_t) +{ + uint16 size; + mpt::IO::ReadIntLE<uint16>(iStrm, size); + if(size > ModSpecs::mptm.ordersMax) + { + seq.m_sndFile.AddToLog(LogWarning, MPT_UFORMAT("Module has sequence of length {}; it will be truncated to maximum supported length, {}.")(size, ModSpecs::mptm.ordersMax)); + size = ModSpecs::mptm.ordersMax; + } + seq(0).resize(size); + for(auto &pat : seq(0)) + { + uint16 temp; + mpt::IO::ReadIntLE<uint16>(iStrm, temp); + pat = temp; + } +} + + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequenceOld(std::ostream& oStrm, const ModSequenceSet& seq) +{ + const uint16 size = seq().GetLength(); + mpt::IO::WriteIntLE<uint16>(oStrm, size); + for(auto pat : seq()) + { + mpt::IO::WriteIntLE<uint16>(oStrm, static_cast<uint16>(pat)); + } +} +#endif // MODPLUG_NO_FILESAVE + + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequence(std::ostream& oStrm, const ModSequence& seq) +{ + srlztn::SsbWrite ssb(oStrm); + ssb.BeginWrite(FileIdSequence, Version::Current().GetRawVersion()); + int8 useUTF8 = 1; + ssb.WriteItem(useUTF8, "u"); + ssb.WriteItem(mpt::ToCharset(mpt::Charset::UTF8, seq.GetName()), "n"); + const uint16 length = seq.GetLengthTailTrimmed(); + ssb.WriteItem<uint16>(length, "l"); + ssb.WriteItem(seq, "a", srlztn::VectorWriter<uint16>(length)); + if(seq.GetRestartPos() > 0) + ssb.WriteItem<uint16>(seq.GetRestartPos(), "r"); + ssb.FinishWrite(); +} +#endif // MODPLUG_NO_FILESAVE + + +void ReadModSequence(std::istream& iStrm, ModSequence& seq, const size_t, mpt::Charset defaultCharset) +{ + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead(FileIdSequence, Version::Current().GetRawVersion()); + if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) + return; + int8 useUTF8 = 0; + ssb.ReadItem(useUTF8, "u"); + std::string str; + ssb.ReadItem(str, "n"); + seq.SetName(mpt::ToUnicode(useUTF8 ? mpt::Charset::UTF8 : defaultCharset, str)); + ORDERINDEX nSize = 0; + ssb.ReadItem(nSize, "l"); + LimitMax(nSize, ModSpecs::mptm.ordersMax); + ssb.ReadItem(seq, "a", srlztn::VectorReader<uint16>(nSize)); + + ORDERINDEX restartPos = ORDERINDEX_INVALID; + if(ssb.ReadItem(restartPos, "r") != srlztn::SsbRead::EntryNotFound && restartPos < nSize) + seq.SetRestartPos(restartPos); +} + + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequences(std::ostream& oStrm, const ModSequenceSet& seq) +{ + srlztn::SsbWrite ssb(oStrm); + ssb.BeginWrite(FileIdSequences, Version::Current().GetRawVersion()); + const uint8 nSeqs = seq.GetNumSequences(); + const uint8 nCurrent = seq.GetCurrentSequenceIndex(); + ssb.WriteItem(nSeqs, "n"); + ssb.WriteItem(nCurrent, "c"); + for(uint8 i = 0; i < nSeqs; i++) + { + ssb.WriteItem(seq(i), srlztn::ID::FromInt<uint8>(i), &WriteModSequence); + } + ssb.FinishWrite(); +} +#endif // MODPLUG_NO_FILESAVE + + +void ReadModSequences(std::istream& iStrm, ModSequenceSet& seq, const size_t, mpt::Charset defaultCharset) +{ + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead(FileIdSequences, Version::Current().GetRawVersion()); + if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) + return; + SEQUENCEINDEX seqs = 0; + uint8 currentSeq = 0; + ssb.ReadItem(seqs, "n"); + if (seqs == 0) + return; + LimitMax(seqs, MAX_SEQUENCES); + ssb.ReadItem(currentSeq, "c"); + if (seq.GetNumSequences() < seqs) + seq.m_Sequences.resize(seqs, ModSequence(seq.m_sndFile)); + + // There used to be only one restart position for all sequences + ORDERINDEX legacyRestartPos = seq(0).GetRestartPos(); + + for(SEQUENCEINDEX i = 0; i < seqs; i++) + { + seq(i).SetRestartPos(legacyRestartPos); + ssb.ReadItem(seq(i), srlztn::ID::FromInt<uint8>(i), [defaultCharset](std::istream &iStrm, ModSequence &seq, std::size_t dummy) { return ReadModSequence(iStrm, seq, dummy, defaultCharset); }); + } + seq.m_currentSeq = (currentSeq < seq.GetNumSequences()) ? currentSeq : 0; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.h b/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.h new file mode 100644 index 00000000..12d1952d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/ModSequence.h @@ -0,0 +1,219 @@ +/* + * ModSequence.h + * ------------- + * Purpose: Order and sequence handling. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" + +#include <algorithm> +#include <vector> + +OPENMPT_NAMESPACE_BEGIN + +class CPattern; +class CSoundFile; +class ModSequenceSet; + +class ModSequence: public std::vector<PATTERNINDEX> +{ + friend class ModSequenceSet; + +protected: + mpt::ustring m_name; // Sequence name + CSoundFile &m_sndFile; // Associated CSoundFile + ORDERINDEX m_restartPos = 0; // Restart position when playback of this order ended + +public: + ModSequence(CSoundFile &sndFile); + ModSequence(ModSequence &&) noexcept = default; + ModSequence(const ModSequence &) = default; + ModSequence& operator=(const ModSequence &other); + + bool operator==(const ModSequence &other) const; + bool operator!=(const ModSequence &other) const { return !(*this == other); } + + ORDERINDEX GetLength() const { return mpt::saturate_cast<ORDERINDEX>(size()); } + // Returns last accessible index, i.e. GetLength() - 1, or 0 if the order list is empty. + ORDERINDEX GetLastIndex() const { return std::max(ORDERINDEX(1), GetLength()) - 1u; } + // Returns length of sequence without counting trailing '---' items. + ORDERINDEX GetLengthTailTrimmed() const; + // Returns length of sequence stopping counting on first '---' (or at the end of sequence). + ORDERINDEX GetLengthFirstEmpty() const; + + // Replaces order list with 'newSize' copies of 'pat'. + void assign(ORDERINDEX newSize, PATTERNINDEX pat); + + // Inserts 'count' orders starting from 'pos' using 'fill' as the pattern index for all inserted orders. + // Sequence will automatically grow if needed and if it can't grow enough, some tail orders will be discarded. + // Return: Number of orders inserted (up to 'count' many). + ORDERINDEX insert(ORDERINDEX pos, ORDERINDEX count) { return insert(pos, count, GetInvalidPatIndex()); } + ORDERINDEX insert(ORDERINDEX pos, ORDERINDEX count, PATTERNINDEX fill); + + void push_back() { push_back(GetInvalidPatIndex()); } + void push_back(PATTERNINDEX pat) { if(GetLength() < MAX_ORDERS) std::vector<PATTERNINDEX>::push_back(pat); } + + void resize(ORDERINDEX newSize) { resize(newSize, GetInvalidPatIndex()); } + void resize(ORDERINDEX newSize, PATTERNINDEX pat) { std::vector<PATTERNINDEX>::resize(std::min(MAX_ORDERS, newSize), pat); } + + // Removes orders from range [posBegin, posEnd]. + void Remove(ORDERINDEX posBegin, ORDERINDEX posEnd); + + // Remove all references to a given pattern index from the order list. Jump commands are updated accordingly. + void RemovePattern(PATTERNINDEX pat); + + // Replaces all occurences of oldPat with newPat. + void Replace(PATTERNINDEX oldPat, PATTERNINDEX newPat) { if(oldPat != newPat) std::replace(begin(), end(), oldPat, newPat); } + + // Removes any "---" patterns at the end of the list. + void Shrink() { resize(GetLengthTailTrimmed()); } + + // Check if pattern at sequence position ord is valid. + bool IsValidPat(ORDERINDEX ord) const; + + CPattern *PatternAt(ORDERINDEX ord) const; + + void AdjustToNewModType(const MODTYPE oldtype); + + // Returns the internal representation of a stop '---' index + static constexpr PATTERNINDEX GetInvalidPatIndex() { return uint16_max; } + // Returns the internal representation of an ignore '+++' index + static constexpr PATTERNINDEX GetIgnoreIndex() { return uint16_max - 1; } + + // Returns the previous/next order ignoring skip indices (+++). + // If no previous/next order exists, return first/last order, and zero + // when orderlist is empty. + ORDERINDEX GetPreviousOrderIgnoringSkips(const ORDERINDEX start) const; + ORDERINDEX GetNextOrderIgnoringSkips(const ORDERINDEX start) const; + + // Find an order item that contains a given pattern number. + ORDERINDEX FindOrder(PATTERNINDEX pat, ORDERINDEX startSearchAt = 0, bool searchForward = true) const; + + // Ensures that the pattern at the specified order position is used only once (across all sequences). + // If another usage is found, the pattern is replaced by a copy and the new index is returned. + PATTERNINDEX EnsureUnique(ORDERINDEX ord); + +#ifndef MODPLUG_NO_FILESAVE + // Write order items as bytes. '---' is written as stopIndex, '+++' is written as ignoreIndex + size_t WriteAsByte(std::ostream &f, const ORDERINDEX count, uint8 stopIndex = 0xFF, uint8 ignoreIndex = 0xFE) const; +#endif // MODPLUG_NO_FILESAVE + + // Returns true if the IT orderlist datafield is not sufficient to store orderlist information. + bool NeedsExtraDatafield() const; + +#ifdef MODPLUG_TRACKER + // Check if a playback position is currently locked (inaccessible) + bool IsPositionLocked(ORDERINDEX position) const; + // Check if this sequence has subsongs separated by invalid ("---" or non-existing) patterns + bool HasSubsongs() const; +#endif // MODPLUG_TRACKER + + // Sequence name setter / getter + inline void SetName(const mpt::ustring &newName) { m_name = newName;} + inline mpt::ustring GetName() const { return m_name; } + + // Restart position setter / getter + inline void SetRestartPos(ORDERINDEX restartPos) noexcept { m_restartPos = restartPos; } + inline ORDERINDEX GetRestartPos() const noexcept { return m_restartPos; } +}; + + +class ModSequenceSet +{ + friend void ReadModSequenceOld(std::istream& iStrm, ModSequenceSet& seq, const size_t); + friend void ReadModSequences(std::istream& iStrm, ModSequenceSet& seq, const size_t, mpt::Charset defaultCharset); + +protected: + std::vector<ModSequence> m_Sequences; // Array of sequences + CSoundFile &m_sndFile; + SEQUENCEINDEX m_currentSeq = 0; // Index of current sequence. + +public: + ModSequenceSet(CSoundFile &sndFile); + ModSequenceSet(ModSequenceSet &&) noexcept = default; + + // Remove all sequences and initialize default sequence + void Initialize(); + + // Get the working sequence + ModSequence& operator() () { return m_Sequences[m_currentSeq]; } + const ModSequence& operator() () const { return m_Sequences[m_currentSeq]; } + // Get an arbitrary sequence + ModSequence& operator() (SEQUENCEINDEX seq) { return m_Sequences[seq]; } + const ModSequence& operator() (SEQUENCEINDEX seq) const { return m_Sequences[seq]; } + + SEQUENCEINDEX GetNumSequences() const { return static_cast<SEQUENCEINDEX>(m_Sequences.size()); } + SEQUENCEINDEX GetCurrentSequenceIndex() const { return m_currentSeq; } + + // Sets working sequence. + void SetSequence(SEQUENCEINDEX); + // Add new empty sequence. + // Returns the ID of the new sequence, or SEQUENCEINDEX_INVALID on failure. + SEQUENCEINDEX AddSequence(); + // Removes given sequence. + void RemoveSequence(SEQUENCEINDEX); + + // Returns the internal representation of a stop '---' index + static constexpr PATTERNINDEX GetInvalidPatIndex() { return ModSequence::GetInvalidPatIndex(); } + // Returns the internal representation of an ignore '+++' index + static constexpr PATTERNINDEX GetIgnoreIndex() { return ModSequence::GetIgnoreIndex(); } + +#ifdef MODPLUG_TRACKER + // Assigns a new set of sequences. The vector contents indicate which existing sequences to keep / duplicate or if a new sequences should be inserted (SEQUENCEINDEX_INVALID) + // The function fails if the vector is empty or contains too many sequences. + bool Rearrange(const std::vector<SEQUENCEINDEX> &newOrder); + + // Adjust sequence when converting between module formats + void OnModTypeChanged(MODTYPE oldType); + // Check if there is a single sequences that qualifies for subsong splitting + bool CanSplitSubsongs() const; + // If there are subsongs (separated by "---" patterns) in the module, + // asks user whether to convert these into multiple sequences (given that the + // modformat supports multiple sequences). + // Returns true if sequences were modified, false otherwise. + bool SplitSubsongsToMultipleSequences(); + + // Convert the sequence's restart position information to a pattern command. + bool RestartPosToPattern(SEQUENCEINDEX seq); + // Merges multiple sequences into one and destroys all other sequences. + // Returns false if there were no sequences to merge, true otherwise. + bool MergeSequences(); +#endif // MODPLUG_TRACKER + + std::vector<ModSequence>::iterator begin() { return m_Sequences.begin(); } + std::vector<ModSequence>::const_iterator begin() const { return m_Sequences.begin(); } + std::vector<ModSequence>::const_iterator cbegin() const { return m_Sequences.cbegin(); } + std::vector<ModSequence>::iterator end() { return m_Sequences.end(); } + std::vector<ModSequence>::const_iterator end() const { return m_Sequences.end(); } + std::vector<ModSequence>::const_iterator cend() const { return m_Sequences.cend(); } +}; + + +const char FileIdSequences[] = "mptSeqC"; +const char FileIdSequence[] = "mptSeq"; + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequences(std::ostream& oStrm, const ModSequenceSet& seq); +#endif // MODPLUG_NO_FILESAVE +void ReadModSequences(std::istream& iStrm, ModSequenceSet& seq, const size_t nSize, mpt::Charset defaultCharset); + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequence(std::ostream& oStrm, const ModSequence& seq); +#endif // MODPLUG_NO_FILESAVE +void ReadModSequence(std::istream& iStrm, ModSequence& seq, const size_t, mpt::Charset defaultCharset); + +#ifndef MODPLUG_NO_FILESAVE +void WriteModSequenceOld(std::ostream& oStrm, const ModSequenceSet& seq); +#endif // MODPLUG_NO_FILESAVE +void ReadModSequenceOld(std::istream& iStrm, ModSequenceSet& seq, const size_t); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/OPL.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/OPL.cpp new file mode 100644 index 00000000..faa205eb --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/OPL.cpp @@ -0,0 +1,345 @@ +/* + * OPL.cpp + * ------- + * Purpose: Translate data coming from OpenMPT's mixer into OPL commands to be sent to the Opal emulator. + * Notes : (currently none) + * Authors: OpenMPT Devs + * Schism Tracker contributors (bisqwit, JosepMa, Malvineous, code relicensed from GPL to BSD with permission) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "../common/misc_util.h" +#include "OPL.h" +#include "opal.h" + +OPENMPT_NAMESPACE_BEGIN + +OPL::OPL(uint32 samplerate) +{ + Initialize(samplerate); +} + + +OPL::OPL(IRegisterLogger &logger) + : m_logger{&logger} +{ + Initialize(OPL_BASERATE); +} + + +OPL::~OPL() +{ + // This destructor is put here so that we can forward-declare the Opal emulator class. +} + + +void OPL::Initialize(uint32 samplerate) +{ + if(m_opl == nullptr) + m_opl = std::make_unique<Opal>(samplerate); + else + m_opl->SetSampleRate(samplerate); + Reset(); +} + + +void OPL::Mix(int32 *target, size_t count, uint32 volumeFactorQ16) +{ + if(!m_isActive) + return; + + // This factor causes a sample voice to be more or less as loud as an OPL voice + const int32 factor = Util::muldiv_unsigned(volumeFactorQ16, 6169, (1 << 16)); + while(count--) + { + int16 l, r; + m_opl->Sample(&l, &r); + target[0] += l * factor; + target[1] += r * factor; + target += 2; + } +} + + +uint16 OPL::ChannelToRegister(uint8 oplCh) +{ + if(oplCh < 9) + return oplCh; + else + return (oplCh - 9) | 0x100; +} + + +// Translate a channel's first operator address into a register +uint16 OPL::OperatorToRegister(uint8 oplCh) +{ + static constexpr uint8 OPLChannelToOperator[] = { 0, 1, 2, 8, 9, 10, 16, 17, 18 }; + if(oplCh < 9) + return OPLChannelToOperator[oplCh]; + else + return OPLChannelToOperator[oplCh - 9] | 0x100; +} + + +uint8 OPL::GetVoice(CHANNELINDEX c) const +{ + if((m_ChanToOPL[c] & OPL_CHANNEL_CUT) || m_ChanToOPL[c] == OPL_CHANNEL_INVALID) + return OPL_CHANNEL_INVALID; + return m_ChanToOPL[c] & OPL_CHANNEL_MASK; +} + + +uint8 OPL::AllocateVoice(CHANNELINDEX c) +{ + // Can we re-use a previous channel? + if(auto oplCh = m_ChanToOPL[c]; oplCh != OPL_CHANNEL_INVALID) + { + if(!(m_ChanToOPL[c] & OPL_CHANNEL_CUT)) + return oplCh; + // Check re-use hint + oplCh &= OPL_CHANNEL_MASK; + if(m_OPLtoChan[oplCh] == CHANNELINDEX_INVALID || m_OPLtoChan[oplCh] == c) + { + m_OPLtoChan[oplCh] = c; + m_ChanToOPL[c] = oplCh; + return oplCh; + } + } + // Search for unused channel or channel with released note + uint8 releasedChn = OPL_CHANNEL_INVALID, releasedCutChn = OPL_CHANNEL_INVALID; + for(uint8 oplCh = 0; oplCh < OPL_CHANNELS; oplCh++) + { + if(m_OPLtoChan[oplCh] == CHANNELINDEX_INVALID) + { + m_OPLtoChan[oplCh] = c; + m_ChanToOPL[c] = oplCh; + return oplCh; + } else if(!(m_KeyOnBlock[oplCh] & KEYON_BIT)) + { + releasedChn = oplCh; + if(m_ChanToOPL[m_OPLtoChan[oplCh]] & OPL_CHANNEL_CUT) + releasedCutChn = oplCh; + } + } + if(releasedChn != OPL_CHANNEL_INVALID) + { + // Prefer channel that has been marked as cut over channel that has just been released + if(releasedCutChn != OPL_CHANNEL_INVALID) + releasedChn = releasedCutChn; + m_ChanToOPL[m_OPLtoChan[releasedChn]] = OPL_CHANNEL_INVALID; + m_OPLtoChan[releasedChn] = c; + m_ChanToOPL[c] = releasedChn; + } + return GetVoice(c); +} + + +void OPL::MoveChannel(CHANNELINDEX from, CHANNELINDEX to) +{ + uint8 oplCh = GetVoice(from); + if(oplCh == OPL_CHANNEL_INVALID) + return; + m_OPLtoChan[oplCh] = to; + m_ChanToOPL[from] = OPL_CHANNEL_INVALID; + m_ChanToOPL[to] = oplCh; +} + + +void OPL::NoteOff(CHANNELINDEX c) +{ + uint8 oplCh = GetVoice(c); + if(oplCh == OPL_CHANNEL_INVALID || m_opl == nullptr) + return; + m_KeyOnBlock[oplCh] &= ~KEYON_BIT; + Port(c, KEYON_BLOCK | ChannelToRegister(oplCh), m_KeyOnBlock[oplCh]); +} + + +void OPL::NoteCut(CHANNELINDEX c, bool unassign) +{ + uint8 oplCh = GetVoice(c); + if(oplCh == OPL_CHANNEL_INVALID) + return; + NoteOff(c); + Volume(c, 0, false); // Note that a volume of 0 is not complete silence; the release portion of the sound will still be heard at -48dB + if(unassign) + { + m_OPLtoChan[oplCh] = CHANNELINDEX_INVALID; + m_ChanToOPL[c] |= OPL_CHANNEL_CUT; + } +} + + +void OPL::Frequency(CHANNELINDEX c, uint32 milliHertz, bool keyOff, bool beatingOscillators) +{ + uint8 oplCh = GetVoice(c); + if(oplCh == OPL_CHANNEL_INVALID || m_opl == nullptr) + return; + + uint16 fnum = 1023; + uint8 block = 7; + if(milliHertz <= 6208431) + { + if(milliHertz > 3104215) block = 7; + else if(milliHertz > 1552107) block = 6; + else if(milliHertz > 776053) block = 5; + else if(milliHertz > 388026) block = 4; + else if(milliHertz > 194013) block = 3; + else if(milliHertz > 97006) block = 2; + else if(milliHertz > 48503) block = 1; + else block = 0; + + fnum = static_cast<uint16>(Util::muldivr_unsigned(milliHertz, 1 << (20 - block), OPL_BASERATE * 1000)); + MPT_ASSERT(fnum < 1024); + } + + // Evil CDFM hack! Composer 670 slightly detunes each note based on the OPL channel number modulo 4. + // We allocate our OPL channels dynamically, which would result in slightly different beating characteristics, + // but we can just take the pattern channel number instead, as the pattern channel layout is always identical. + if(beatingOscillators) + fnum = std::min(static_cast<uint16>(fnum + (c & 3)), uint16(1023)); + + fnum |= (block << 10); + + uint16 channel = ChannelToRegister(oplCh); + m_KeyOnBlock[oplCh] = (keyOff ? 0 : KEYON_BIT) | (fnum >> 8); // Key on bit + Octave (block) + F-number high 2 bits + Port(c, FNUM_LOW | channel, fnum & 0xFF); // F-Number low 8 bits + Port(c, KEYON_BLOCK | channel, m_KeyOnBlock[oplCh]); + + m_isActive = true; +} + + +uint8 OPL::CalcVolume(uint8 trackerVol, uint8 kslVolume) +{ + if(trackerVol >= 63u) + return kslVolume; + if(trackerVol > 0) + trackerVol++; + return (kslVolume & KSL_MASK) | (63u - ((63u - (kslVolume & TOTAL_LEVEL_MASK)) * trackerVol) / 64u); +} + + +void OPL::Volume(CHANNELINDEX c, uint8 vol, bool applyToModulator) +{ + uint8 oplCh = GetVoice(c); + if(oplCh == OPL_CHANNEL_INVALID || m_opl == nullptr) + return; + + const auto &patch = m_Patches[oplCh]; + const uint16 modulator = OperatorToRegister(oplCh), carrier = modulator + 3; + if((patch[10] & CONNECTION_BIT) || applyToModulator) + { + // Set volume of both operators in additive mode + Port(c, KSL_LEVEL + modulator, CalcVolume(vol, patch[2])); + } + if(!applyToModulator) + { + Port(c, KSL_LEVEL + carrier, CalcVolume(vol, patch[3])); + } +} + + +int8 OPL::Pan(CHANNELINDEX c, int32 pan) +{ + uint8 oplCh = GetVoice(c); + if(oplCh == OPL_CHANNEL_INVALID || m_opl == nullptr) + return 0; + + const auto &patch = m_Patches[oplCh]; + uint8 fbConn = patch[10] & ~STEREO_BITS; + // OPL3 only knows hard left, center and right, so we need to translate our + // continuous panning range into one of those three states. + // 0...84 = left, 85...170 = center, 171...256 = right + if(pan <= 170) + fbConn |= VOICE_TO_LEFT; + if(pan >= 85) + fbConn |= VOICE_TO_RIGHT; + + Port(c, FEEDBACK_CONNECTION | ChannelToRegister(oplCh), fbConn); + return ((fbConn & VOICE_TO_LEFT) ? -1 : 0) + ((fbConn & VOICE_TO_RIGHT) ? 1 : 0); +} + + +void OPL::Patch(CHANNELINDEX c, const OPLPatch &patch) +{ + uint8 oplCh = AllocateVoice(c); + if(oplCh == OPL_CHANNEL_INVALID || m_opl == nullptr) + return; + + m_Patches[oplCh] = patch; + + const uint16 modulator = OperatorToRegister(oplCh), carrier = modulator + 3; + for(uint8 op = 0; op < 2; op++) + { + const auto opReg = op ? carrier : modulator; + Port(c, AM_VIB | opReg, patch[0 + op]); + Port(c, KSL_LEVEL | opReg, patch[2 + op]); + Port(c, ATTACK_DECAY | opReg, patch[4 + op]); + Port(c, SUSTAIN_RELEASE | opReg, patch[6 + op]); + Port(c, WAVE_SELECT | opReg, patch[8 + op]); + } + + Port(c, FEEDBACK_CONNECTION | ChannelToRegister(oplCh), patch[10]); +} + + +void OPL::Reset() +{ + if(m_isActive) + { + for(CHANNELINDEX chn = 0; chn < MAX_CHANNELS; chn++) + { + NoteCut(chn); + } + m_isActive = false; + } + + m_KeyOnBlock.fill(0); + m_OPLtoChan.fill(CHANNELINDEX_INVALID); + m_ChanToOPL.fill(OPL_CHANNEL_INVALID); + + Port(CHANNELINDEX_INVALID, 0x105, 1); // Enable OPL3 + Port(CHANNELINDEX_INVALID, 0x104, 0); // No 4-op voices +} + + +void OPL::Port(CHANNELINDEX c, uint16 reg, uint8 value) +{ + if(!m_logger) + m_opl->Port(reg, value); + else + m_logger->Port(c, reg, value); +} + + +std::vector<uint16> OPL::AllVoiceRegisters() +{ + static constexpr uint8 opRegisters[] = {OPL::AM_VIB, OPL::KSL_LEVEL, OPL::ATTACK_DECAY, OPL::SUSTAIN_RELEASE, OPL::WAVE_SELECT}; + static constexpr uint8 chnRegisters[] = {OPL::FNUM_LOW, OPL::KEYON_BLOCK, OPL::FEEDBACK_CONNECTION}; + std::vector<uint16> result; + result.reserve(234); + for(uint16 chip = 0; chip < 2; chip++) + { + for(uint8 opReg : opRegisters) + { + for(uint8 op = 0; op < 22; op++) + { + if((op & 7) < 6) + result.push_back((chip << 8) | opReg | op); + } + } + for(uint8 chnReg : chnRegisters) + { + for(uint8 chn = 0; chn < 9; chn++) + { + result.push_back((chip << 8) | chnReg | chn); + } + } + } + return result; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/OPL.h b/Src/external_dependencies/openmpt-trunk/soundlib/OPL.h new file mode 100644 index 00000000..e8b56bc2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/OPL.h @@ -0,0 +1,125 @@ +/* + * OPL.h + * ----- + * Purpose: Translate data coming from OpenMPT's mixer into OPL commands to be sent to the Opal emulator. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" + +class Opal; + +OPENMPT_NAMESPACE_BEGIN + +class OPL +{ +public: + enum OPLRegisters : uint8 + { + // Operators (combine with result of OperatorToRegister) + AM_VIB = 0x20, // AM / VIB / EG / KSR / Multiple (0x20 to 0x35) + KSL_LEVEL = 0x40, // KSL / Total level (0x40 to 0x55) + ATTACK_DECAY = 0x60, // Attack rate / Decay rate (0x60 to 0x75) + SUSTAIN_RELEASE = 0x80, // Sustain level / Release rate (0x80 to 0x95) + WAVE_SELECT = 0xE0, // Wave select (0xE0 to 0xF5) + + // Channels (combine with result of ChannelToRegister) + FNUM_LOW = 0xA0, // F-number low bits (0xA0 to 0xA8) + KEYON_BLOCK = 0xB0, // F-number high bits / Key on / Block (octave) (0xB0 to 0xB8) + FEEDBACK_CONNECTION = 0xC0, // Feedback / Connection (0xC0 to 0xC8) + }; + + enum OPLValues : uint8 + { + // AM_VIB + TREMOLO_ON = 0x80, + VIBRATO_ON = 0x40, + SUSTAIN_ON = 0x20, + KSR = 0x10, // Key scaling rate + MULTIPLE_MASK = 0x0F, // Frequency multiplier + + // KSL_LEVEL + KSL_MASK = 0xC0, // Envelope scaling bits + TOTAL_LEVEL_MASK = 0x3F, // Strength (volume) of OP + + // ATTACK_DECAY + ATTACK_MASK = 0xF0, + DECAY_MASK = 0x0F, + + // SUSTAIN_RELEASE + SUSTAIN_MASK = 0xF0, + RELEASE_MASK = 0x0F, + + // KEYON_BLOCK + KEYON_BIT = 0x20, + + // FEEDBACK_CONNECTION + FEEDBACK_MASK = 0x0E, // Valid just for first OP of a voice + CONNECTION_BIT = 0x01, + VOICE_TO_LEFT = 0x10, + VOICE_TO_RIGHT = 0x20, + STEREO_BITS = VOICE_TO_LEFT | VOICE_TO_RIGHT, + }; + + class IRegisterLogger + { + public: + virtual void Port(CHANNELINDEX c, uint16 reg, uint8 value) = 0; + virtual ~IRegisterLogger() {} + }; + + OPL(uint32 samplerate); + OPL(IRegisterLogger &logger); + ~OPL(); + + void Initialize(uint32 samplerate); + void Mix(int32 *buffer, size_t count, uint32 volumeFactorQ16); + + void NoteOff(CHANNELINDEX c); + void NoteCut(CHANNELINDEX c, bool unassign = true); + void Frequency(CHANNELINDEX c, uint32 milliHertz, bool keyOff, bool beatingOscillators); + void Volume(CHANNELINDEX c, uint8 vol, bool applyToModulator); + int8 Pan(CHANNELINDEX c, int32 pan); + void Patch(CHANNELINDEX c, const OPLPatch &patch); + bool IsActive(CHANNELINDEX c) const { return GetVoice(c) != OPL_CHANNEL_INVALID; } + void MoveChannel(CHANNELINDEX from, CHANNELINDEX to); + void Reset(); + + // A list of all registers for channels and operators + static std::vector<uint16> AllVoiceRegisters(); + +protected: + static uint16 ChannelToRegister(uint8 oplCh); + static uint16 OperatorToRegister(uint8 oplCh); + static uint8 CalcVolume(uint8 trackerVol, uint8 kslVolume); + uint8 GetVoice(CHANNELINDEX c) const; + uint8 AllocateVoice(CHANNELINDEX c); + void Port(CHANNELINDEX c, uint16 reg, uint8 value); + + enum + { + OPL_CHANNELS = 18, // 9 for OPL2 or 18 for OPL3 + OPL_CHANNEL_CUT = 0x80, // Indicates that the channel has been cut and used as a hint to re-use the channel for the same tracker channel if possible + OPL_CHANNEL_MASK = 0x7F, + OPL_CHANNEL_INVALID = 0xFF, + OPL_BASERATE = 49716, + }; + + std::unique_ptr<Opal> m_opl; + IRegisterLogger *m_logger = nullptr; + + std::array<uint8, OPL_CHANNELS> m_KeyOnBlock; + std::array<CHANNELINDEX, OPL_CHANNELS> m_OPLtoChan; + std::array<uint8, MAX_CHANNELS> m_ChanToOPL; + std::array<OPLPatch, OPL_CHANNELS> m_Patches; + + bool m_isActive = false; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.cpp new file mode 100644 index 00000000..848b47d3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.cpp @@ -0,0 +1,213 @@ +/* + * OggStream.cpp + * ------------- + * Purpose: Basic Ogg stream parsing functionality + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "OggStream.h" +#include "mpt/crc/crc.hpp" +#include "../common/FileReader.h" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Ogg +{ + + +uint16 PageInfo::GetPagePhysicalSize() const +{ + uint16 size = 0; + size += sizeof(PageHeader); + size += header.page_segments; + for(uint8 segment = 0; segment < header.page_segments; ++segment) + { + size += segment_table[segment]; + } + return size; +} + + +uint16 PageInfo::GetPageHeaderSize() const +{ + uint16 size = 0; + size += sizeof(PageHeader); + size += header.page_segments; + return size; +} + + +uint16 PageInfo::GetPageDataSize() const +{ + uint16 size = 0; + for(uint8 segment = 0; segment < header.page_segments; ++segment) + { + size += segment_table[segment]; + } + return size; +} + + +bool AdvanceToPageMagic(FileReader &file) +{ +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant +#endif // MPT_COMPILER_MSVC + while(true) +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif // MPT_COMPILER_MSVC + { + if(!file.CanRead(4)) + { + return false; + } + if(file.ReadMagic("OggS")) + { + file.SkipBack(4); + return true; + } + file.Skip(1); + } +} + + +bool ReadPage(FileReader &file, PageInfo &pageInfo, std::vector<uint8> *pageData) +{ + pageInfo = PageInfo(); + if(pageData) + { + (*pageData).clear(); + } + if(!file.ReadMagic("OggS")) + { + return false; + } + file.SkipBack(4); + FileReader filePageReader = file; // do not modify original file read position + if(!filePageReader.ReadStruct(pageInfo.header)) + { + return false; + } + if(!filePageReader.CanRead(pageInfo.header.page_segments)) + { + return false; + } + uint16 pageDataSize = 0; + for(uint8 segment = 0; segment < pageInfo.header.page_segments; ++segment) + { + pageInfo.segment_table[segment] = filePageReader.ReadIntLE<uint8>(); + pageDataSize += pageInfo.segment_table[segment]; + } + if(!filePageReader.CanRead(pageDataSize)) + { + return false; + } + if(pageData) + { + filePageReader.ReadVector(*pageData, pageDataSize); + } else + { + filePageReader.Skip(pageDataSize); + } + filePageReader.SkipBack(pageInfo.GetPagePhysicalSize()); + { + mpt::crc32_ogg calculatedCRC; + uint8 rawHeader[sizeof(PageHeader)]; + MemsetZero(rawHeader); + filePageReader.ReadArray(rawHeader); + std::memset(rawHeader + 22, 0, 4); // clear out old crc + calculatedCRC.process(rawHeader, rawHeader + sizeof(rawHeader)); + filePageReader.Skip(pageInfo.header.page_segments); + calculatedCRC.process(pageInfo.segment_table, pageInfo.segment_table + pageInfo.header.page_segments); + if(pageData) + { + filePageReader.Skip(pageDataSize); + calculatedCRC.process(*pageData); + } else + { + FileReader pageDataReader = filePageReader.ReadChunk(pageDataSize); + auto pageDataView = pageDataReader.GetPinnedView(); + calculatedCRC.process(pageDataView.GetSpan()); + } + if(calculatedCRC != pageInfo.header.CRC_checksum) + { + return false; + } + } + file.Skip(pageInfo.GetPagePhysicalSize()); + return true; +} + + +bool ReadPage(FileReader &file, PageInfo &pageInfo, std::vector<uint8> &pageData) +{ + return ReadPage(file, pageInfo, &pageData); +} + + +bool ReadPage(FileReader &file) +{ + PageInfo pageInfo; + return ReadPage(file, pageInfo); +} + + +bool ReadPageAndSkipJunk(FileReader &file, PageInfo &pageInfo, std::vector<uint8> &pageData) +{ + pageInfo = PageInfo(); + pageData.clear(); +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant +#endif // MPT_COMPILER_MSVC + while(true) +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif // MPT_COMPILER_MSVC + { + if(!AdvanceToPageMagic(file)) + { + return false; + } + if(ReadPage(file, pageInfo, pageData)) + { + return true; + } else + { + pageInfo = PageInfo(); + pageData.clear(); + } + file.Skip(1); + } +} + + +bool UpdatePageCRC(PageInfo &pageInfo, const std::vector<uint8> &pageData) +{ + if(pageData.size() != pageInfo.GetPageDataSize()) + { + return false; + } + mpt::crc32_ogg crc; + pageInfo.header.CRC_checksum = 0; + char rawHeader[sizeof(PageHeader)]; + std::memcpy(rawHeader, &pageInfo.header, sizeof(PageHeader)); + crc.process(rawHeader, rawHeader + sizeof(PageHeader)); + crc.process(pageInfo.segment_table, pageInfo.segment_table + pageInfo.header.page_segments); + crc.process(pageData); + pageInfo.header.CRC_checksum = crc; + return true; +} + + +} // namespace Ogg + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.h b/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.h new file mode 100644 index 00000000..a8165527 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/OggStream.h @@ -0,0 +1,92 @@ +/* + * OggStream.h + * ----------- + * Purpose: Basic Ogg stream parsing functionality + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/base/Endian.hpp" +#include "mpt/io/io.hpp" + +#include "../common/FileReaderFwd.h" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Ogg +{ + +struct PageHeader +{ + char capture_pattern[4]; // "OggS" + uint8le version; + uint8le header_type; + uint64le granule_position; + uint32le bitstream_serial_number; + uint32le page_sequence_number; + uint32le CRC_checksum; + uint8le page_segments; +}; + +MPT_BINARY_STRUCT(PageHeader, 27) + + +struct PageInfo +{ + PageHeader header; + uint8 segment_table[255]; + PageInfo() + { + MemsetZero(header); + MemsetZero(segment_table); + } + uint16 GetPagePhysicalSize() const; + uint16 GetPageHeaderSize() const; + uint16 GetPageDataSize() const; +}; + + +// returns false on EOF +bool AdvanceToPageMagic(FileReader &file); + +bool ReadPage(FileReader &file, PageInfo &pageInfo, std::vector<uint8> *pageData = nullptr); +bool ReadPage(FileReader &file, PageInfo &pageInfo, std::vector<uint8> &pageData); +bool ReadPage(FileReader &file); + +bool ReadPageAndSkipJunk(FileReader &file, PageInfo &pageInfo, std::vector<uint8> &pageData); + + +bool UpdatePageCRC(PageInfo &pageInfo, const std::vector<uint8> &pageData); + + +template <typename Tfile> +bool WritePage(Tfile & f, const PageInfo &pageInfo, const std::vector<uint8> &pageData) +{ + if(!mpt::IO::Write(f, pageInfo.header)) + { + return false; + } + if(!mpt::IO::WriteRaw(f, pageInfo.segment_table, pageInfo.header.page_segments)) + { + return false; + } + if(!mpt::IO::WriteRaw(f, pageData.data(), pageData.size())) + { + return false; + } + return true; +} + + +} // namespace Ogg + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Paula.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Paula.cpp new file mode 100644 index 00000000..f3291d5d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Paula.cpp @@ -0,0 +1,332 @@ +/* +* Paula.cpp +* --------- +* Purpose: Emulating the Amiga's sound chip, Paula, by implementing resampling using band-limited steps (BLEPs) +* Notes : The BLEP table generator code is a translation of Antti S. Lankila's original Python code. +* Authors: OpenMPT Devs +* Antti S. Lankila +* The OpenMPT source code is released under the BSD license. Read LICENSE for more details. +*/ + +#include "stdafx.h" +#include "Paula.h" +#include "TinyFFT.h" +#include "Tables.h" +#include "mpt/base/numbers.hpp" + +#include <complex> +#include <numeric> + +OPENMPT_NAMESPACE_BEGIN + +namespace Paula +{ + +namespace +{ + +MPT_NOINLINE std::vector<double> KaiserFIR(int numTaps, double cutoff, double beta) +{ + const double izeroBeta = Izero(beta); + const double kPi = 4.0 * std::atan(1.0) * cutoff; + const double xDiv = 1.0 / ((numTaps / 2) * (numTaps / 2)); + const int numTapsDiv2 = numTaps / 2; + std::vector<double> result(numTaps); + for(int i = 0; i < numTaps; i++) + { + double fsinc; + if(i == numTapsDiv2) + { + fsinc = 1.0; + } else + { + const double x = i - numTapsDiv2; + const double xPi = x * kPi; + // - sinc - - Kaiser window - -sinc- + fsinc = std::sin(xPi) * Izero(beta * std::sqrt(1 - x * x * xDiv)) / (izeroBeta * xPi); + } + + result[i] = fsinc * cutoff; + } + return result; +} + + +MPT_NOINLINE void FIR_MinPhase(std::vector<double> &table, const TinyFFT &fft) +{ + std::vector<std::complex<double>> cepstrum(fft.Size()); + MPT_ASSERT(cepstrum.size() >= table.size()); + for(size_t i = 0; i < table.size(); i++) + cepstrum[i] = table[i]; + // Compute the real cepstrum: fft -> abs + ln -> ifft -> real + fft.FFT(cepstrum); + for(auto &v : cepstrum) + v = std::log(std::abs(v)); + fft.IFFT(cepstrum); + fft.Normalize(cepstrum); + + // Window the cepstrum in such a way that anticausal components become rejected + for(size_t i = 1; i < cepstrum.size() / 2; i++) + { + cepstrum[i] *= 2; + cepstrum[i + cepstrum.size() / 2] *= 0; + } + + // Now cancel the previous steps: fft -> exp -> ifft -> real + fft.FFT(cepstrum); + for(auto &v : cepstrum) + v = std::exp(v); + fft.IFFT(cepstrum); + fft.Normalize(cepstrum); + for(size_t i = 0; i < table.size(); i++) + table[i] = cepstrum[i].real(); +} + + +class BiquadFilter +{ + double b0, b1, b2, a1, a2, x1 = 0.0, x2 = 0.0, y1 = 0.0, y2 = 0.0; + + double Filter(double x0) + { + double y0 = b0 * x0 + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + x2 = x1; + x1 = x0; + y2 = y1; + y1 = y0; + return y0; + } + +public: + BiquadFilter(double b0_, double b1_, double b2_, double a1_, double a2_) + : b0(b0_), b1(b1_), b2(b2_), a1(a1_), a2(a2_) + { } + + std::vector<double> Run(std::vector<double> table) + { + x1 = 0.0; + x2 = 0.0; + y1 = 0.0; + y2 = 0.0; + + // Initialize filter to stable state + for(int i = 0; i < 10000; i++) + Filter(table[0]); + // Now run the filter + for(auto &v : table) + v = Filter(v); + + return table; + } +}; + + +// Observe: a and b are reversed here. To be absolutely clear: +// a is the nominator and b is the denominator. :-/ +BiquadFilter ZTransform(double a0, double a1, double a2, double b0, double b1, double b2, double fc, double fs) +{ + // Prewarp s - domain coefficients + const double wp = 2.0 * fs * std::tan(mpt::numbers::pi * fc / fs); + a2 /= wp * wp; + a1 /= wp; + b2 /= wp * wp; + b1 /= wp; + + // Compute bilinear transform and return it + const double bd = 4 * b2 * fs * fs + 2 * b1 * fs + b0; + return BiquadFilter( + (4 * a2 * fs * fs + 2 * a1 * fs + a0) / bd, + (2 * a0 - 8 * a2 * fs * fs) / bd, + (4 * a2 * fs * fs - 2 * a1 * fs + a0) / bd, + (2 * b0 - 8 * b2 * fs * fs) / bd, + (4 * b2 * fs * fs - 2 * b1 * fs + b0) / bd); +} + + +BiquadFilter MakeRCLowpass(double sampleRate, double freq) +{ + const double omega = (2.0 * mpt::numbers::pi) * freq / sampleRate; + const double term = 1 + 1 / omega; + return BiquadFilter(1 / term, 0.0, 0.0, -1.0 + 1.0 / term, 0.0); +} + + +BiquadFilter MakeButterworth(double fs, double fc, double res_dB = 0) +{ + // 2nd-order Butterworth s-domain coefficients are: + // + // b0 = 1.0 b1 = 0 b2 = 0 + // a0 = 1 a1 = sqrt(2) a2 = 1 + // + // by tweaking the a1 parameter, some resonance can be produced. + + const double res = std::pow(10.0, (-res_dB / 10.0 / 2.0)); + return ZTransform(1, 0, 0, 1, std::sqrt(2) * res, 1, fc, fs); +} + + +MPT_NOINLINE void Integrate(std::vector<double> &table) +{ + const double total = std::accumulate(table.begin(), table.end(), 0.0); + double startVal = -total; + + for(auto &v : table) + { + startVal += v; + v = startVal; + } +} + + +MPT_NOINLINE void Quantize(const std::vector<double> &in, Paula::BlepArray &quantized) +{ + MPT_ASSERT(in.size() == Paula::BLEP_SIZE); + constexpr int fact = 1 << Paula::BLEP_SCALE; + const double cv = fact / (in.back() - in.front()); + + for(int i = 0; i < Paula::BLEP_SIZE; i++) + { + double val = in[i] * cv; +#ifdef MPT_INTMIXER + val = mpt::round(val); +#endif + quantized[i] = static_cast<mixsample_t>(-val); + } +} + +} // namespace + +void BlepTables::InitTables() +{ + constexpr double sampleRate = Paula::PAULA_HZ; + + // Because Amiga only has 84 dB SNR, the noise floor is low enough with -90 dB. + // A500 model uses slightly lower-quality kaiser window to obtain slightly + // steeper stopband attenuation. The fixed filters attenuates the sidelobes by + // 12 dB, compensating for the worse performance of the kaiser window. + + // 21 kHz stopband is not fully attenuated by 22 kHz. If the sampling frequency + // is 44.1 kHz, all frequencies above 22 kHz will alias over 20 kHz, thus inaudible. + // The output should be aliasingless for 48 kHz sampling frequency. + auto unfilteredA500 = KaiserFIR(Paula::BLEP_SIZE, 21000.0 / sampleRate * 2.0, 8.0); + auto unfilteredA1200 = KaiserFIR(Paula::BLEP_SIZE, 21000.0 / sampleRate * 2.0, 9.0); + // Move filtering effects to start to allow IIRs more time to settle + constexpr size_t padSize = 8; + constexpr int fftSize = static_cast<int>(mpt::bit_width(size_t(Paula::BLEP_SIZE)) + mpt::bit_width(padSize) - 2); + const TinyFFT fft(fftSize); + FIR_MinPhase(unfilteredA500, fft); + FIR_MinPhase(unfilteredA1200, fft); + + // Make digital models for the filters on Amiga 500 and 1200. + auto filterFixed5kHz = MakeRCLowpass(sampleRate, 4900.0); + // The leakage filter seems to reduce treble in both models a bit + // The A500 filter seems to be well modelled only with a 4.9 kHz + // filter although the component values would suggest 5 kHz filter. + auto filterLeakage = MakeRCLowpass(sampleRate, 32000.0); + auto filterLED = MakeButterworth(sampleRate, 3275.0, -0.70); + + // Apply fixed filter to A500 + auto amiga500Off = filterFixed5kHz.Run(unfilteredA500); + // Produce the filtered outputs + auto amiga1200Off = filterLeakage.Run(unfilteredA1200); + + // Produce LED filters + auto amiga500On = filterLED.Run(amiga500Off); + auto amiga1200On = filterLED.Run(amiga1200Off); + + // Integrate to produce blep + Integrate(amiga500Off); + Integrate(amiga500On); + Integrate(amiga1200Off); + Integrate(amiga1200On); + Integrate(unfilteredA1200); + + // Quantize and scale + Quantize(amiga500Off, WinSincIntegral[A500Off]); + Quantize(amiga500On, WinSincIntegral[A500On]); + Quantize(amiga1200Off, WinSincIntegral[A1200Off]); + Quantize(amiga1200On, WinSincIntegral[A1200On]); + Quantize(unfilteredA1200, WinSincIntegral[Unfiltered]); +} + + +const Paula::BlepArray &BlepTables::GetAmigaTable(Resampling::AmigaFilter amigaType, bool enableFilter) const +{ + if(amigaType == Resampling::AmigaFilter::A500) + return enableFilter ? WinSincIntegral[A500On] : WinSincIntegral[A500Off]; + if(amigaType == Resampling::AmigaFilter::A1200) + return enableFilter ? WinSincIntegral[A1200On] : WinSincIntegral[A1200Off]; + return WinSincIntegral[Unfiltered]; +} + + +// we do not initialize blepState here +// cppcheck-suppress uninitMemberVar +State::State(uint32 sampleRate) +{ + double amigaClocksPerSample = static_cast<double>(PAULA_HZ) / sampleRate; + numSteps = static_cast<int>(amigaClocksPerSample / MINIMUM_INTERVAL); + stepRemainder = SamplePosition::FromDouble(amigaClocksPerSample - numSteps * MINIMUM_INTERVAL); + remainder = SamplePosition(0); +} + + +void State::Reset() +{ + remainder = SamplePosition(0); + activeBleps = 0; + firstBlep = MAX_BLEPS / 2u; + globalOutputLevel = 0; +} + + +void State::InputSample(int16 sample) +{ + if(sample != globalOutputLevel) + { + // Start a new blep: level is the difference, age (or phase) is 0 clocks. + firstBlep = (firstBlep - 1u) % MAX_BLEPS; + if(activeBleps < std::size(blepState)) + activeBleps++; + blepState[firstBlep].age = 0; + blepState[firstBlep].level = sample - globalOutputLevel; + globalOutputLevel = sample; + } +} + + +// Return output simulated as series of bleps +int State::OutputSample(const BlepArray &WinSincIntegral) +{ + int output = globalOutputLevel * (1 << Paula::BLEP_SCALE); + uint32 lastBlep = firstBlep + activeBleps; + for(uint32 i = firstBlep; i != lastBlep; i++) + { + const auto &blep = blepState[i % MAX_BLEPS]; + output -= WinSincIntegral[blep.age] * blep.level; + } + output /= (1 << (Paula::BLEP_SCALE - 2)); // - 2 to compensate for the fact that we reduced the input sample bit depth + + return output; +} + + +// Advance the simulation by given number of clock ticks +void State::Clock(int cycles) +{ + uint32 lastBlep = firstBlep + activeBleps; + for(uint32 i = firstBlep; i != lastBlep; i++) + { + auto &blep = blepState[i % MAX_BLEPS]; + blep.age += static_cast<uint16>(cycles); + if(blep.age >= Paula::BLEP_SIZE) + { + activeBleps = static_cast<uint16>(i - firstBlep); + return; + } + } +} + +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Paula.h b/Src/external_dependencies/openmpt-trunk/soundlib/Paula.h new file mode 100644 index 00000000..527ea9e4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Paula.h @@ -0,0 +1,86 @@ +/* +* Paula.h +* ------- +* Purpose: Emulating the Amiga's sound chip, Paula, by implementing resampling using band-limited steps (BLEPs) +* Notes : (currently none) +* Authors: OpenMPT Devs +* Antti S. Lankila +* The OpenMPT source code is released under the BSD license. Read LICENSE for more details. +*/ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" +#include "Mixer.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace Paula +{ + +inline constexpr int PAULA_HZ = 3546895; +inline constexpr int MINIMUM_INTERVAL = 4; // Tradeoff between quality and speed (lower = less aliasing) +inline constexpr int BLEP_SCALE = 17; // TODO: Should be 1 for float mixer +inline constexpr int BLEP_SIZE = 2048; + +using BlepArray = std::array<mixsample_t, BLEP_SIZE>; + + +class BlepTables +{ + enum AmigaFilter + { + A500Off = 0, + A500On, + A1200Off, + A1200On, + Unfiltered, + NumFilterTypes + }; + + std::array<Paula::BlepArray, AmigaFilter::NumFilterTypes> WinSincIntegral; + +public: + void InitTables(); + const Paula::BlepArray &GetAmigaTable(Resampling::AmigaFilter amigaType, bool enableFilter) const; +}; + + +class State +{ + // MAX_BLEPS configures how many BLEPs (volume steps) are being kept track of per channel, + // and thus directly influences how much memory this feature wastes per virtual channel. + // Paula::BLEP_SIZE / Paula::MINIMUM_INTERVAL would be a safe maximum, + // but even a sample (alternating at +1/-1, thus causing a step on every frame) played at 200 kHz, + // which is way out of spec for the Amiga, will only get close to 128 active BLEPs with Paula::MINIMUM_INTERVAL == 4. + // Hence 128 is chosen as a tradeoff between quality and memory consumption. + static constexpr uint16 MAX_BLEPS = 128; + + struct Blep + { + int16 level; + uint16 age; + }; + +public: + SamplePosition remainder, stepRemainder; + int numSteps; // Number of full-length steps +private: + uint16 activeBleps = 0, firstBlep = 0; // Count of simultaneous bleps to keep track of + int16 globalOutputLevel = 0; // The instantenous value of Paula output + Blep blepState[MAX_BLEPS]; + +public: + State(uint32 sampleRate = 48000); + + void Reset(); + void InputSample(int16 sample); + int OutputSample(const BlepArray &WinSincIntegral); + void Clock(int cycles); +}; + +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Resampler.h b/Src/external_dependencies/openmpt-trunk/soundlib/Resampler.h new file mode 100644 index 00000000..1f617c77 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Resampler.h @@ -0,0 +1,143 @@ +/* + * Resampler.h + * ----------- + * Purpose: Holds the tables for all available resamplers. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "WindowedFIR.h" +#include "Mixer.h" +#include "MixerSettings.h" +#include "Paula.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef LIBOPENMPT_BUILD +// All these optimizations are not applicable to the tracker +// because cutoff and firtype are configurable there. + +// Cache resampler tables across resampler object creation. +// A C++11-style function-static singleton is holding the cached values. +#define MPT_RESAMPLER_TABLES_CACHED + +// Prime the tables cache when the library is loaded. +// Caching gets triggered via a global object that primes the cache during +// construction. +// This is only really useful with MPT_RESAMPLER_TABLES_CACHED. +//#define MPT_RESAMPLER_TABLES_CACHED_ONSTARTUP + +#endif // LIBOPENMPT_BUILD + + +#define SINC_WIDTH 8 + +#define SINC_PHASES_BITS 12 +#define SINC_PHASES (1<<SINC_PHASES_BITS) + +#ifdef MPT_INTMIXER +typedef int16 SINC_TYPE; +#define SINC_QUANTSHIFT 15 +#else +typedef mixsample_t SINC_TYPE; +#endif // MPT_INTMIXER + +#define SINC_MASK (SINC_PHASES-1) +static_assert((SINC_MASK & 0xffff) == SINC_MASK); // exceeding fractional freq + + +class CResamplerSettings +{ +public: + ResamplingMode SrcMode = Resampling::Default(); + double gdWFIRCutoff = 0.97; + uint8 gbWFIRType = WFIR_KAISER4T; + Resampling::AmigaFilter emulateAmiga = Resampling::AmigaFilter::Off; +public: + constexpr CResamplerSettings() = default; + bool operator == (const CResamplerSettings &cmp) const + { +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wfloat-equal" +#endif // MPT_COMPILER_CLANG + return SrcMode == cmp.SrcMode && gdWFIRCutoff == cmp.gdWFIRCutoff && gbWFIRType == cmp.gbWFIRType && emulateAmiga == cmp.emulateAmiga; +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG + } + bool operator != (const CResamplerSettings &cmp) const { return !(*this == cmp); } +}; + + +class CResampler +{ +public: + CResamplerSettings m_Settings; + CWindowedFIR m_WindowedFIR; + static const int16 FastSincTable[256 * 4]; + +#ifdef MODPLUG_TRACKER + static bool StaticTablesInitialized; + #define RESAMPLER_TABLE static +#else + // no global data which has to be initialized by hand in the library + #define RESAMPLER_TABLE +#endif // MODPLUG_TRACKER + + RESAMPLER_TABLE SINC_TYPE gKaiserSinc[SINC_PHASES * 8]; // Upsampling + RESAMPLER_TABLE SINC_TYPE gDownsample13x[SINC_PHASES * 8]; // Downsample 1.333x + RESAMPLER_TABLE SINC_TYPE gDownsample2x[SINC_PHASES * 8]; // Downsample 2x + RESAMPLER_TABLE Paula::BlepTables blepTables; // Amiga BLEP resampler + +#ifndef MPT_INTMIXER + RESAMPLER_TABLE mixsample_t FastSincTablef[256 * 4]; // Cubic spline LUT + RESAMPLER_TABLE mixsample_t LinearTablef[256]; // Linear interpolation LUT +#endif // !defined(MPT_INTMIXER) + +#undef RESAMPLER_TABLE + +private: + CResamplerSettings m_OldSettings; +public: + CResampler(bool fresh_generate=false) + { + if(fresh_generate) + { + InitializeTablesFromScratch(true); + } else + { + InitializeTables(); + } + } + void InitializeTables() + { + #if defined(MPT_RESAMPLER_TABLES_CACHED) + InitializeTablesFromCache(); + #else + InitializeTablesFromScratch(true); + #endif + } + void UpdateTables() + { + InitializeTablesFromScratch(false); + } + +private: + void InitFloatmixerTables(); + void InitializeTablesFromScratch(bool force=false); +#ifdef MPT_RESAMPLER_TABLES_CACHED + void InitializeTablesFromCache(); +#endif +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.cpp new file mode 100644 index 00000000..4e27bc53 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.cpp @@ -0,0 +1,267 @@ +/* + * RowVisitor.cpp + * -------------- + * Purpose: Class for recording which rows of a song has already been visited, used for detecting when a module starts to loop. + * Notes : The class keeps track of rows that have been visited by the player before. + * This way, we can tell when the module starts to loop, i.e. we can determine the song length, + * or find out that a given point of the module can never be reached. + * + * In some module formats, infinite loops can be achieved through pattern loops (e.g. E60 / E61 / E61 in one channel of a ProTracker MOD). + * To detect such loops, we store a set of loop counts across all channels encountered for each row. + * As soon as a set of loop counts is encountered twice for a specific row, we know that the track ends up in an infinite loop. + * As a result of this design, it is safe to evaluate pattern loops in CSoundFile::GetLength. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "RowVisitor.h" +#include "Sndfile.h" + +OPENMPT_NAMESPACE_BEGIN + +RowVisitor::LoopState::LoopState(const ChannelStates &chnState, const bool ignoreRow) +{ + // Rather than storing the exact loop count vector, we compute a FNV-1a 64-bit hash of it. + // This means we can store the loop state in a small and fixed amount of memory. + // In theory there is the possibility of hash collisions for different loop states, but in practice, + // the relevant inputs for the hashing algorithm are extremely unlikely to produce collisions. + // There may be better hashing algorithms, but many of them are much more complex and cannot be applied easily in an incremental way. + uint64 hash = FNV1a_BASIS; + if(ignoreRow) + { + hash = (hash ^ 0xFFu) * FNV1a_PRIME; +#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE + m_counts.emplace_back(uint8(0xFF), uint8(0xFF)); +#endif + } + + for(size_t chn = 0; chn < chnState.size(); chn++) + { + if(chnState[chn].nPatternLoopCount) + { + static_assert(MAX_BASECHANNELS <= 256, "Channel index cannot be used as byte input for hash generator"); + static_assert(sizeof(chnState[0].nPatternLoopCount) <= sizeof(uint8), "Loop count cannot be used as byte input for hash generator"); + hash = (hash ^ chn) * FNV1a_PRIME; + hash = (hash ^ chnState[chn].nPatternLoopCount) * FNV1a_PRIME; +#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE + m_counts.emplace_back(static_cast<uint8>(chn), chnState[chn].nPatternLoopCount); +#endif + } + } + m_hash = hash; +} + + +RowVisitor::RowVisitor(const CSoundFile &sndFile, SEQUENCEINDEX sequence) + : m_sndFile(sndFile) + , m_sequence(sequence) +{ + Initialize(true); +} + + +void RowVisitor::MoveVisitedRowsFrom(RowVisitor &other) noexcept +{ + m_visitedRows = std::move(other.m_visitedRows); + m_visitedLoopStates = std::move(other.m_visitedLoopStates); +} + + +const ModSequence &RowVisitor::Order() const +{ + if(m_sequence >= m_sndFile.Order.GetNumSequences()) + return m_sndFile.Order(); + else + return m_sndFile.Order(m_sequence); +} + + +// Resize / clear the row vector. +// If reset is true, the vector is not only resized to the required dimensions, but also completely cleared (i.e. all visited rows are reset). +void RowVisitor::Initialize(bool reset) +{ + auto &order = Order(); + const ORDERINDEX endOrder = order.GetLengthTailTrimmed(); + m_visitedRows.resize(endOrder); + if(reset) + { + m_visitedLoopStates.clear(); + m_rowsSpentInLoops = 0; + } + + std::vector<uint8> loopCount; + std::vector<ORDERINDEX> visitedPatterns(m_sndFile.Patterns.GetNumPatterns(), ORDERINDEX_INVALID); + for(ORDERINDEX ord = 0; ord < endOrder; ord++) + { + const PATTERNINDEX pat = order[ord]; + const ROWINDEX numRows = VisitedRowsVectorSize(pat); + auto &visitedRows = m_visitedRows[ord]; + + if(reset) + visitedRows.assign(numRows, false); + else + visitedRows.resize(numRows, false); + + if(!order.IsValidPat(ord)) + continue; + + const ROWINDEX startRow = std::min(static_cast<ROWINDEX>(reset ? 0 : visitedRows.size()), numRows); + auto insertionHint = m_visitedLoopStates.end(); + + if(visitedPatterns[pat] != ORDERINDEX_INVALID) + { + // We visited this pattern before, copy over the results + const auto begin = m_visitedLoopStates.lower_bound({visitedPatterns[pat], startRow}); + const auto end = (begin != m_visitedLoopStates.end()) ? m_visitedLoopStates.lower_bound({visitedPatterns[pat], numRows}) : m_visitedLoopStates.end(); + for(auto pos = begin; pos != end; ++pos) + { + LoopStateSet loopStates; + loopStates.reserve(pos->second.capacity()); + insertionHint = ++m_visitedLoopStates.insert_or_assign(insertionHint, {ord, pos->first.second}, std::move(loopStates)); + } + continue; + } + + // Pre-allocate loop count state + const auto &pattern = m_sndFile.Patterns[pat]; + loopCount.assign(pattern.GetNumChannels(), 0); + for(ROWINDEX i = numRows; i != startRow; i--) + { + const ROWINDEX row = i - 1; + uint32 maxLoopStates = 1; + auto m = pattern.GetRow(row); + // Break condition: If it's more than 16, it's probably wrong :) exact loop count depends on how loops overlap. + for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels() && maxLoopStates < 16; chn++, m++) + { + auto count = loopCount[chn]; + if((m->command == CMD_S3MCMDEX && (m->param & 0xF0) == 0xB0) || (m->command == CMD_MODCMDEX && (m->param & 0xF0) == 0x60)) + { + loopCount[chn] = (m->param & 0x0F); + if(loopCount[chn]) + count = loopCount[chn]; + } + if(count) + maxLoopStates *= (count + 1); + } + if(maxLoopStates > 1) + { + LoopStateSet loopStates; + loopStates.reserve(maxLoopStates); + insertionHint = m_visitedLoopStates.insert_or_assign(insertionHint, {ord, row}, std::move(loopStates)); + } + } + // Only use this order as a blueprint for other orders using the same pattern if we fully parsed the pattern. + if(startRow == 0) + visitedPatterns[pat] = ord; + } +} + + +// Mark an order/row combination as visited and returns true if it was visited before. +bool RowVisitor::Visit(ORDERINDEX ord, ROWINDEX row, const ChannelStates &chnState, bool ignoreRow) +{ + auto &order = Order(); + if(ord >= order.size() || row >= VisitedRowsVectorSize(order[ord])) + return false; + + // The module might have been edited in the meantime - so we have to extend this a bit. + if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size()) + { + Initialize(false); + // If it's still past the end of the vector, this means that ord >= order.GetLengthTailTrimmed(), i.e. we are trying to play an empty order. + if(ord >= m_visitedRows.size()) + return false; + } + + MPT_ASSERT(chnState.size() >= m_sndFile.GetNumChannels()); + LoopState newState{chnState.first(m_sndFile.GetNumChannels()), ignoreRow}; + const auto rowLoopState = m_visitedLoopStates.find({ord, row}); + const bool oldHadLoops = (rowLoopState != m_visitedLoopStates.end() && !rowLoopState->second.empty()); + const bool newHasLoops = newState.HasLoops(); + const bool wasVisited = m_visitedRows[ord][row]; + + // Check if new state is part of row state already. If so, we visited this row already and thus the module must be looping + if(!oldHadLoops && !newHasLoops && wasVisited) + return true; + if(oldHadLoops && mpt::contains(rowLoopState->second, newState)) + return true; + + if(newHasLoops) + m_rowsSpentInLoops++; + + if(oldHadLoops || newHasLoops) + { + // Convert to set representation if it isn't already + if(!oldHadLoops && wasVisited) + m_visitedLoopStates[{ord, row}].emplace_back(); + m_visitedLoopStates[{ord, row}].emplace_back(std::move(newState)); + } + m_visitedRows[ord][row] = true; + return false; +} + + +// Get the needed vector size for a given pattern. +ROWINDEX RowVisitor::VisitedRowsVectorSize(PATTERNINDEX pattern) const noexcept +{ + if(m_sndFile.Patterns.IsValidPat(pattern)) + return m_sndFile.Patterns[pattern].GetNumRows(); + else + return 1; // Non-existing patterns consist of a "fake" row. +} + + +// Find the first row that has not been played yet. +// The order and row is stored in the order and row variables on success, on failure they contain invalid values. +// If onlyUnplayedPatterns is true (default), only completely unplayed patterns are considered, otherwise a song can start on any unplayed row. +// Function returns true on success. +bool RowVisitor::GetFirstUnvisitedRow(ORDERINDEX &ord, ROWINDEX &row, bool onlyUnplayedPatterns) const +{ + const auto &order = Order(); + const ORDERINDEX endOrder = order.GetLengthTailTrimmed(); + for(ord = 0; ord < endOrder; ord++) + { + if(!order.IsValidPat(ord)) + continue; + + if(ord >= m_visitedRows.size()) + { + // Not yet initialized => unvisited + row = 0; + return true; + } + + const auto &visitedRows = m_visitedRows[ord]; + const auto firstUnplayedRow = std::find(visitedRows.begin(), visitedRows.end(), onlyUnplayedPatterns); + if(onlyUnplayedPatterns && firstUnplayedRow == visitedRows.end()) + { + // No row of this pattern has been played yet. + row = 0; + return true; + } else if(!onlyUnplayedPatterns) + { + // Return the first unplayed row in this pattern + if(firstUnplayedRow != visitedRows.end()) + { + row = static_cast<ROWINDEX>(std::distance(visitedRows.begin(), firstUnplayedRow)); + return true; + } + if(visitedRows.size() < m_sndFile.Patterns[order[ord]].GetNumRows()) + { + // History is not fully initialized + row = static_cast<ROWINDEX>(visitedRows.size()); + return true; + } + } + } + + // Didn't find anything :( + ord = ORDERINDEX_INVALID; + row = ROWINDEX_INVALID; + return false; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.h b/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.h new file mode 100644 index 00000000..c24d9b53 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/RowVisitor.h @@ -0,0 +1,112 @@ +/* + * RowVisitor.h + * ------------ + * Purpose: Class for recording which rows of a song has already been visited, used for detecting when a module starts to loop. + * Notes : See implementation file. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/base/span.hpp" +#include "Snd_defs.h" + +#include <map> + +OPENMPT_NAMESPACE_BEGIN + +#if defined(MPT_BUILD_DEBUG) || defined(MPT_BUILD_FUZZER) +#define MPT_VERIFY_ROWVISITOR_LOOPSTATE +#endif // MPT_BUILD_DEBUG || MPT_BUILD_FUZZER + +class CSoundFile; +class ModSequence; +struct ModChannel; + +class RowVisitor +{ +protected: + using ChannelStates = mpt::span<const ModChannel>; + + class LoopState + { + static constexpr uint64 FNV1a_BASIS = 14695981039346656037ull; + static constexpr uint64 FNV1a_PRIME = 1099511628211ull; + uint64 m_hash = FNV1a_BASIS; +#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE + std::vector<std::pair<uint8, uint8>> m_counts; // Actual loop counts to verify equality of hash-based implementation +#endif + + public: + LoopState() = default; + LoopState(const ChannelStates &chnState, const bool ignoreRow); + LoopState(const LoopState &) = default; + LoopState(LoopState&&) = default; + LoopState &operator=(const LoopState &) = default; + LoopState &operator=(LoopState&&) = default; + + [[nodiscard]] bool operator==(const LoopState &other) const noexcept + { +#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE + if((m_counts == other.m_counts) != (m_hash == other.m_hash)) + std::abort(); +#endif + return m_hash == other.m_hash; + } + + [[nodiscard]] bool HasLoops() const noexcept + { +#ifdef MPT_VERIFY_ROWVISITOR_LOOPSTATE + if(m_counts.empty() != (m_hash == FNV1a_BASIS)) + std::abort(); +#endif + return m_hash != FNV1a_BASIS; + } + }; + + using LoopStateSet = std::vector<LoopState>; + + // Stores for every (order, row) combination in the sequence if it has been visited or not. + std::vector<std::vector<bool>> m_visitedRows; + // Map for each row that's part of a pattern loop which loop states have been visited. Held in a separate data structure because it is sparse data in typical modules. + std::map<std::pair<ORDERINDEX, ROWINDEX>, LoopStateSet> m_visitedLoopStates; + + const CSoundFile &m_sndFile; + ROWINDEX m_rowsSpentInLoops = 0; + const SEQUENCEINDEX m_sequence; + +public: + RowVisitor(const CSoundFile &sndFile, SEQUENCEINDEX sequence = SEQUENCEINDEX_INVALID); + + void MoveVisitedRowsFrom(RowVisitor &other) noexcept; + + // Resize / Clear the row vector. + // If reset is true, the vector is not only resized to the required dimensions, but also completely cleared (i.e. all visited rows are unset). + void Initialize(bool reset); + + // Mark an order/row combination as visited and returns true if it was visited before. + bool Visit(ORDERINDEX ord, ROWINDEX row, const ChannelStates &chnState, bool ignoreRow); + + // Find the first row that has not been played yet. + // The order and row is stored in the order and row variables on success, on failure they contain invalid values. + // If onlyUnplayedPatterns is true (default), only completely unplayed patterns are considered, otherwise a song can start anywhere. + // Function returns true on success. + [[nodiscard]] bool GetFirstUnvisitedRow(ORDERINDEX &order, ROWINDEX &row, bool onlyUnplayedPatterns) const; + + // Pattern loops can stack up exponentially, which can cause an effectively infinite amount of time to be spent on evaluating them. + // If this function returns true, module evaluation should be aborted because the pattern loops appear to be too complex. + [[nodiscard]] bool ModuleTooComplex(ROWINDEX threshold) const noexcept { return m_rowsSpentInLoops >= threshold; } + void ResetComplexity() { m_rowsSpentInLoops = 0; } + +protected: + // Get the needed vector size for a given pattern. + [[nodiscard]] ROWINDEX VisitedRowsVectorSize(PATTERNINDEX pattern) const noexcept; + + [[nodiscard]] const ModSequence &Order() const; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.cpp new file mode 100644 index 00000000..27be5086 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.cpp @@ -0,0 +1,147 @@ +/* + * S3MTools.cpp + * ------------ + * Purpose: Definition of S3M file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "S3MTools.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + +// Convert an S3M sample header to OpenMPT's internal sample header. +void S3MSampleHeader::ConvertToMPT(ModSample &mptSmp, bool isST3) const +{ + mptSmp.Initialize(MOD_TYPE_S3M); + mptSmp.filename = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, filename); + + if(sampleType == typePCM || sampleType == typeNone) + { + // Sample Length and Loops + if(sampleType == typePCM) + { + mptSmp.nLength = length; + mptSmp.nLoopStart = std::min(static_cast<SmpLength>(loopStart), mptSmp.nLength - 1); + mptSmp.nLoopEnd = std::min(static_cast<SmpLength>(loopEnd), mptSmp.nLength); + mptSmp.uFlags.set(CHN_LOOP, (flags & smpLoop) != 0); + } + + if(mptSmp.nLoopEnd < 2 || mptSmp.nLoopStart >= mptSmp.nLoopEnd || mptSmp.nLoopEnd - mptSmp.nLoopStart < 1) + { + mptSmp.nLoopStart = mptSmp.nLoopEnd = 0; + mptSmp.uFlags.reset(); + } + } else if(sampleType == typeAdMel) + { + OPLPatch patch; + std::memcpy(patch.data() + 0, mpt::as_raw_memory(length).data(), 4); + std::memcpy(patch.data() + 4, mpt::as_raw_memory(loopStart).data(), 4); + std::memcpy(patch.data() + 8, mpt::as_raw_memory(loopEnd).data(), 4); + mptSmp.SetAdlib(true, patch); + } + + // Volume / Panning + mptSmp.nVolume = std::min(defaultVolume.get(), uint8(64)) * 4; + + // C-5 frequency + mptSmp.nC5Speed = c5speed; + if(isST3) + { + // ST3 ignores or clamps the high 16 bits depending on the instrument type + if(sampleType == typeAdMel) + mptSmp.nC5Speed &= 0xFFFF; + else + LimitMax(mptSmp.nC5Speed, uint16_max); + } + + if(mptSmp.nC5Speed == 0) + mptSmp.nC5Speed = 8363; + else if(mptSmp.nC5Speed < 1024) + mptSmp.nC5Speed = 1024; + +} + + +// Convert OpenMPT's internal sample header to an S3M sample header. +SmpLength S3MSampleHeader::ConvertToS3M(const ModSample &mptSmp) +{ + SmpLength smpLength = 0; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, filename) = mptSmp.filename; + memcpy(magic, "SCRS", 4); + + if(mptSmp.uFlags[CHN_ADLIB]) + { + memcpy(magic, "SCRI", 4); + sampleType = typeAdMel; + std::memcpy(mpt::as_raw_memory(length ).data(), mptSmp.adlib.data() + 0, 4); + std::memcpy(mpt::as_raw_memory(loopStart).data(), mptSmp.adlib.data() + 4, 4); + std::memcpy(mpt::as_raw_memory(loopEnd ).data(), mptSmp.adlib.data() + 8, 4); + } else if(mptSmp.HasSampleData()) + { + sampleType = typePCM; + length = mpt::saturate_cast<uint32>(mptSmp.nLength); + loopStart = mpt::saturate_cast<uint32>(mptSmp.nLoopStart); + loopEnd = mpt::saturate_cast<uint32>(mptSmp.nLoopEnd); + + smpLength = length; + + flags = (mptSmp.uFlags[CHN_LOOP] ? smpLoop : 0); + if(mptSmp.uFlags[CHN_16BIT]) + { + flags |= smp16Bit; + } + if(mptSmp.uFlags[CHN_STEREO]) + { + flags |= smpStereo; + } + } else + { + sampleType = typeNone; + } + + defaultVolume = static_cast<uint8>(std::min(static_cast<uint16>(mptSmp.nVolume / 4), uint16(64))); + if(mptSmp.nC5Speed != 0) + { + c5speed = mptSmp.nC5Speed; + } else + { + c5speed = ModSample::TransposeToFrequency(mptSmp.RelativeTone, mptSmp.nFineTune); + } + + return smpLength; +} + + +// Retrieve the internal sample format flags for this sample. +SampleIO S3MSampleHeader::GetSampleFormat(bool signedSamples) const +{ + if(pack == S3MSampleHeader::pADPCM && !(flags & S3MSampleHeader::smp16Bit) && !(flags & S3MSampleHeader::smpStereo)) + { + // MODPlugin :( + return SampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::ADPCM); + } else + { + return SampleIO( + (flags & S3MSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + (flags & S3MSampleHeader::smpStereo) ? SampleIO::stereoSplit : SampleIO::mono, + SampleIO::littleEndian, + signedSamples ? SampleIO::signedPCM : SampleIO::unsignedPCM); + } +} + + +// Calculate the sample position in file +uint32 S3MSampleHeader::GetSampleOffset() const +{ + return (dataPointer[1] << 4) | (dataPointer[2] << 12) | (dataPointer[0] << 20); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.h b/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.h new file mode 100644 index 00000000..9517dfbe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/S3MTools.h @@ -0,0 +1,169 @@ +/* + * S3MTools.h + * ---------- + * Purpose: Definition of S3M file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/ModSample.h" +#include "../soundlib/SampleIO.h" + + +OPENMPT_NAMESPACE_BEGIN + +// S3M File Header +struct S3MFileHeader +{ + // Magic Bytes + enum S3MMagic + { + idEOF = 0x1A, + idS3MType = 0x10, + idPanning = 0xFC, + }; + + // Tracker Versions in the cwtv field + enum S3MTrackerVersions + { + trackerMask = 0xF000, + versionMask = 0x0FFF, + + trkScreamTracker = 0x1000, + trkImagoOrpheus = 0x2000, + trkImpulseTracker = 0x3000, + trkSchismTracker = 0x4000, + trkOpenMPT = 0x5000, + trkBeRoTracker = 0x6000, + trkCreamTracker = 0x7000, + + trkAkord = 0x0208, + trkST3_00 = 0x1300, + trkST3_20 = 0x1320, + trkST3_01 = 0x1301, + trkIT2_07 = 0x3207, + trkIT2_14 = 0x3214, + trkBeRoTrackerOld = 0x4100, // Used from 2004 to 2012 + trkCamoto = 0xCA00, + }; + + // Flags + enum S3MHeaderFlags + { + st2Vibrato = 0x01, // Vibrato is twice as deep. Cannot be enabled from UI. + zeroVolOptim = 0x08, // Volume 0 optimisations + amigaLimits = 0x10, // Enforce Amiga limits + fastVolumeSlides = 0x40, // Fast volume slides (like in ST3.00) + }; + + // S3M Format Versions + enum S3MFormatVersion + { + oldVersion = 0x01, // Old Version, signed samples + newVersion = 0x02, // New Version, unsigned samples + }; + + char name[28]; // Song Title + uint8le dosEof; // Supposed to be 0x1A, but even ST3 seems to ignore this sometimes (see STRSHINE.S3M by Purple Motion) + uint8le fileType; // File Type, 0x10 = ST3 module + char reserved1[2]; // Reserved + uint16le ordNum; // Number of order items + uint16le smpNum; // Number of sample parapointers + uint16le patNum; // Number of pattern parapointers + uint16le flags; // Flags, see S3MHeaderFlags + uint16le cwtv; // "Made With" Tracker ID, see S3MTrackerVersions + uint16le formatVersion; // Format Version, see S3MFormatVersion + char magic[4]; // "SCRM" magic bytes + uint8le globalVol; // Default Global Volume (0...64) + uint8le speed; // Default Speed (1...254) + uint8le tempo; // Default Tempo (33...255) + uint8le masterVolume; // Sample Volume (0...127, stereo if high bit is set) + uint8le ultraClicks; // Number of channels used for ultra click removal + uint8le usePanningTable; // 0xFC => read extended panning table + uint16le reserved2; // Schism Tracker and OpenMPT use this for their extended version information + uint32le reserved3; // Impulse Tracker hides its edit timer here + uint16le reserved4; + uint16le special; // Pointer to special custom data (unused) + uint8le channels[32]; // Channel setup +}; + +MPT_BINARY_STRUCT(S3MFileHeader, 96) + + +// S3M Sample Header +struct S3MSampleHeader +{ + enum SampleType + { + typeNone = 0, + typePCM = 1, + typeAdMel = 2, + }; + + enum SampleFlags + { + smpLoop = 0x01, + smpStereo = 0x02, + smp16Bit = 0x04, + }; + + enum SamplePacking + { + pUnpacked = 0x00, // PCM + pDP30ADPCM = 0x01, // Unused packing type + pADPCM = 0x04, // MODPlugin ADPCM :( + }; + + uint8le sampleType; // Sample type, see SampleType + char filename[12]; // Sample filename + uint8le dataPointer[3]; // Pointer to sample data (divided by 16) + uint32le length; // Sample length, in samples + uint32le loopStart; // Loop start, in samples + uint32le loopEnd; // Loop end, in samples + uint8le defaultVolume; // Default volume (0...64) + char reserved1; // Reserved + uint8le pack; // Packing algorithm, SamplePacking + uint8le flags; // Sample flags + uint32le c5speed; // Middle-C frequency + char reserved2[4]; // Reserved + uint16le gusAddress; // Sample address in GUS memory (used for fingerprinting) + uint16le sb512; // SoundBlaster loop expansion stuff + uint32le lastUsedPos; // More SoundBlaster stuff + char name[28]; // Sample name + char magic[4]; // "SCRS" magic bytes ("SCRI" for Adlib instruments) + + // Convert an S3M sample header to OpenMPT's internal sample header. + void ConvertToMPT(ModSample &mptSmp, bool isST3 = false) const; + // Convert OpenMPT's internal sample header to an S3M sample header. + SmpLength ConvertToS3M(const ModSample &mptSmp); + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat(bool signedSamples) const; + // Calculate the sample position in file + uint32 GetSampleOffset() const; +}; + +MPT_BINARY_STRUCT(S3MSampleHeader, 80) + + +// Pattern decoding flags +enum S3MPattern +{ + s3mEndOfRow = 0x00, + s3mChannelMask = 0x1F, + s3mNotePresent = 0x20, + s3mVolumePresent = 0x40, + s3mEffectPresent = 0x80, + s3mAnyPresent = 0xE0, + + s3mNoteOff = 0xFE, + s3mNoteNone = 0xFF, +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleCopy.h b/Src/external_dependencies/openmpt-trunk/soundlib/SampleCopy.h new file mode 100644 index 00000000..8e6fd782 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleCopy.h @@ -0,0 +1,51 @@ +/* + * SampleCopy.h + * ------------ + * Purpose: Functions for copying sample data. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// Copy a sample data buffer. +// targetBuffer: Buffer in which the sample should be copied into. +// numSamples: Number of samples of size T that should be copied. targetBuffer is expected to be able to hold "numSamples * incTarget" samples. +// incTarget: Number of samples by which the target data pointer is increased each time. +// sourceBuffer: Buffer from which the samples should be read. +// sourceSize: Size of source buffer, in bytes. +// incSource: Number of samples by which the source data pointer is increased each time. +// +// Template arguments: +// SampleConversion: Functor of type SampleConversionFunctor to apply sample conversion (see above for existing functors). +template <typename SampleConversion> +size_t CopySample(typename SampleConversion::output_t *MPT_RESTRICT outBuf, size_t numSamples, size_t incTarget, const typename SampleConversion::input_t *MPT_RESTRICT inBuf, size_t sourceSize, size_t incSource, SampleConversion conv = SampleConversion()) +{ + const size_t sampleSize = incSource * SampleConversion::input_inc * sizeof(typename SampleConversion::input_t); + LimitMax(numSamples, sourceSize / sampleSize); + const size_t copySize = numSamples * sampleSize; + + SampleConversion sampleConv(conv); + while(numSamples--) + { + *outBuf = sampleConv(inBuf); + outBuf += incTarget; + inBuf += incSource * SampleConversion::input_inc; + } + + return copySize; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatBRR.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatBRR.cpp new file mode 100644 index 00000000..3f7042a6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatBRR.cpp @@ -0,0 +1,134 @@ +/* + * SampleFormatBRR.cpp + * ------------------- + * Purpose: BRR (SNES Bit Rate Reduction) sample format import. + * Notes : This format has no magic bytes, so frame headers are thoroughly validated. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "../common/FileReader.h" + + +OPENMPT_NAMESPACE_BEGIN + + +static void ProcessBRRSample(int32 sample, int16 *output, uint8 range, uint8 filter) +{ + if(sample >= 8) + sample -= 16; + + if(range <= 12) + sample = mpt::rshift_signed(mpt::lshift_signed(sample, range), 1); + else + sample = (sample < 0) ? -2048 : 0; // Implementations do not fully agree on what to do in this case. This is what bsnes does. + + // Apply prediction filter + // Note 1: It is okay that we may access data before the first sample point because this memory is reserved for interpolation + // Note 2: The use of signed shift arithmetic is crucial for some samples (e.g. killer lead.brr, Mac2.brr) + // Note 3: Divisors are twice of what is written in the respective comments, as all sample data is divided by 2 (again crucial for accuracy) + static_assert(InterpolationLookaheadBufferSize >= 2); + switch(filter) + { + case 1: // y(n) = x(n) + x(n-1) * 15/16 + sample += mpt::rshift_signed(output[-1] * 15, 5); + break; + case 2: // y(n) = x(n) + x(n-1) * 61/32 - x(n-2) * 15/16 + sample += mpt::rshift_signed(output[-1] * 61, 6) + mpt::rshift_signed(output[-2] * -15, 5); + break; + case 3: // y(n) = x(n) + x(n-1) * 115/64 - x(n-2) * 13/16 + sample += mpt::rshift_signed(output[-1] * 115, 7) + mpt::rshift_signed(output[-2] * -13, 5); + break; + } + + sample = std::clamp(sample, int32(-32768), int32(32767)) * 2; + if(sample > 32767) + sample -= 65536; + else if(sample < -32768) + sample += 65536; + output[0] = static_cast<int16>(sample); +} + + +bool CSoundFile::ReadBRRSample(SAMPLEINDEX sample, FileReader &file) +{ + const auto fileSize = file.GetLength(); + if(fileSize < 9 || fileSize > uint16_max) + return false; + const bool hasLoopInfo = (fileSize % 9) == 2; + if((fileSize % 9) != 0 && !hasLoopInfo) + return false; + + file.Rewind(); + + SmpLength loopStart = 0; + if(hasLoopInfo) + { + loopStart = file.ReadUint16LE(); + if(loopStart >= fileSize) + return false; + if((loopStart % 9) != 0) + return false; + } + + // First scan the file for validity and consistency + // Note: There are some files with loop start set but ultimately the loop is never enabled. Cannot use this as a consistency check. + // Very few files also have a filter set on the first block, so we cannot reject those either. + bool enableLoop = false, first = true; + while(!file.EndOfFile()) + { + const auto block = file.ReadArray<uint8, 9>(); + const bool isLast = (block[0] & 0x01) != 0; + const bool isLoop = (block[0] & 0x02) != 0; + const uint8 range = block[0] >> 4u; + if(isLast != file.EndOfFile()) + return false; + if(!first && enableLoop != isLoop) + return false; + // While a range of 13 is technically invalid as well, it can be found in the wild. + if(range > 13) + return false; + enableLoop = isLoop; + first = false; + } + + file.Seek(hasLoopInfo ? 2 : 0); + + DestroySampleThreadsafe(sample); + ModSample &mptSmp = Samples[sample]; + mptSmp.Initialize(); + mptSmp.uFlags = CHN_16BIT; + mptSmp.nLength = mpt::saturate_cast<SmpLength>((fileSize - (hasLoopInfo ? 2 : 0)) * 16 / 9); + if(enableLoop) + mptSmp.SetLoop(loopStart * 16 / 9, mptSmp.nLength, true, false, *this); + mptSmp.nC5Speed = 32000; + m_szNames[sample] = ""; + + if(!mptSmp.AllocateSample()) + return false; + + int16 *output = mptSmp.sample16(); + while(!file.EndOfFile()) + { + const auto block = file.ReadArray<uint8, 9>(); + const uint8 range = block[0] >> 4u; + const uint8 filter = (block[0] >> 2) & 0x03; + + for(int i = 0; i < 8; i++) + { + ProcessBRRSample(block[i + 1] >> 4u, output, range, filter); + ProcessBRRSample(block[i + 1] & 0x0F, output + 1, range, filter); + output += 2; + } + } + mptSmp.Convert(MOD_TYPE_IT, GetType()); + mptSmp.PrecomputeLoops(*this, false); + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatFLAC.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatFLAC.cpp new file mode 100644 index 00000000..21265fe5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatFLAC.cpp @@ -0,0 +1,729 @@ +/* + * SampleFormatFLAC.cpp + * -------------------- + * Purpose: FLAC sample import. + * Notes : + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#endif //MODPLUG_TRACKER +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif +#include "../common/misc_util.h" +#include "Tagging.h" +#include "Loaders.h" +#include "WAVTools.h" +#include "../common/FileReader.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/Copy.hpp" +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "../soundlib/SampleCopy.h" +#include "../soundlib/ModSampleCopy.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +//#include "mpt/crc/crc.hpp" +#include "OggStream.h" +#ifdef MPT_WITH_OGG +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <ogg/ogg.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_OGG +#ifdef MPT_WITH_FLAC +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <FLAC/stream_decoder.h> +#include <FLAC/stream_encoder.h> +#include <FLAC/metadata.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_FLAC + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// FLAC Samples + +#ifdef MPT_WITH_FLAC + +struct FLACDecoder +{ + FileReader &file; + CSoundFile &sndFile; + SAMPLEINDEX sample; + bool ready; + + FLACDecoder(FileReader &f, CSoundFile &sf, SAMPLEINDEX smp) : file(f), sndFile(sf), sample(smp), ready(false) { } + + static FLAC__StreamDecoderReadStatus read_cb(const FLAC__StreamDecoder *, FLAC__byte buffer[], size_t *bytes, void *client_data) + { + FileReader &file = static_cast<FLACDecoder *>(client_data)->file; + if(*bytes > 0) + { + FileReader::off_t readBytes = *bytes; + LimitMax(readBytes, file.BytesLeft()); + file.ReadRaw(mpt::byte_cast<mpt::byte_span>(mpt::span(buffer, readBytes))); + *bytes = readBytes; + if(*bytes == 0) + return FLAC__STREAM_DECODER_READ_STATUS_END_OF_STREAM; + else + return FLAC__STREAM_DECODER_READ_STATUS_CONTINUE; + } else + { + return FLAC__STREAM_DECODER_READ_STATUS_ABORT; + } + } + + static FLAC__StreamDecoderSeekStatus seek_cb(const FLAC__StreamDecoder *, FLAC__uint64 absolute_byte_offset, void *client_data) + { + FileReader &file = static_cast<FLACDecoder *>(client_data)->file; + if(!file.Seek(static_cast<FileReader::off_t>(absolute_byte_offset))) + return FLAC__STREAM_DECODER_SEEK_STATUS_ERROR; + else + return FLAC__STREAM_DECODER_SEEK_STATUS_OK; + } + + static FLAC__StreamDecoderTellStatus tell_cb(const FLAC__StreamDecoder *, FLAC__uint64 *absolute_byte_offset, void *client_data) + { + FileReader &file = static_cast<FLACDecoder *>(client_data)->file; + *absolute_byte_offset = file.GetPosition(); + return FLAC__STREAM_DECODER_TELL_STATUS_OK; + } + + static FLAC__StreamDecoderLengthStatus length_cb(const FLAC__StreamDecoder *, FLAC__uint64 *stream_length, void *client_data) + { + FileReader &file = static_cast<FLACDecoder *>(client_data)->file; + *stream_length = file.GetLength(); + return FLAC__STREAM_DECODER_LENGTH_STATUS_OK; + } + + static FLAC__bool eof_cb(const FLAC__StreamDecoder *, void *client_data) + { + FileReader &file = static_cast<FLACDecoder *>(client_data)->file; + return file.NoBytesLeft(); + } + + static FLAC__StreamDecoderWriteStatus write_cb(const FLAC__StreamDecoder *decoder, const FLAC__Frame *frame, const FLAC__int32 *const buffer[], void *client_data) + { + FLACDecoder &client = *static_cast<FLACDecoder *>(client_data); + ModSample &sample = client.sndFile.GetSample(client.sample); + + if(frame->header.number.sample_number >= sample.nLength || !client.ready) + { + // We're reading beyond the sample size already, or we aren't even ready to decode yet! + return FLAC__STREAM_DECODER_WRITE_STATUS_ABORT; + } + + // Number of samples to be copied in this call + const SmpLength copySamples = std::min(static_cast<SmpLength>(frame->header.blocksize), static_cast<SmpLength>(sample.nLength - frame->header.number.sample_number)); + // Number of target channels + const uint8 modChannels = sample.GetNumChannels(); + // Offset (in samples) into target data + const size_t offset = static_cast<size_t>(frame->header.number.sample_number) * modChannels; + // Source size in bytes + const size_t srcSize = frame->header.blocksize * 4; + // Source bit depth + const unsigned int bps = frame->header.bits_per_sample; + + MPT_ASSERT((bps <= 8 && sample.GetElementarySampleSize() == 1) || (bps > 8 && sample.GetElementarySampleSize() == 2)); + MPT_ASSERT(modChannels <= FLAC__stream_decoder_get_channels(decoder)); + MPT_ASSERT(bps == FLAC__stream_decoder_get_bits_per_sample(decoder)); + MPT_UNREFERENCED_PARAMETER(decoder); // decoder is unused if ASSERTs are compiled out + + // Do the sample conversion + for(uint8 chn = 0; chn < modChannels; chn++) + { + if(bps <= 8) + { + int8 *sampleData8 = sample.sample8() + offset; + CopySample<SC::ConversionChain<SC::ConvertShift< int8, int32, 0>, SC::DecodeIdentity<int32> > >(sampleData8 + chn, copySamples, modChannels, buffer[chn], srcSize, 1); + } else if(bps <= 16) + { + int16 *sampleData16 = sample.sample16() + offset; + CopySample<SC::ConversionChain<SC::ConvertShift<int16, int32, 0>, SC::DecodeIdentity<int32> > >(sampleData16 + chn, copySamples, modChannels, buffer[chn], srcSize, 1); + } else if(bps <= 24) + { + int16 *sampleData16 = sample.sample16() + offset; + CopySample<SC::ConversionChain<SC::ConvertShift<int16, int32, 8>, SC::DecodeIdentity<int32> > >(sampleData16 + chn, copySamples, modChannels, buffer[chn], srcSize, 1); + } else if(bps <= 32) + { + int16 *sampleData16 = sample.sample16() + offset; + CopySample<SC::ConversionChain<SC::ConvertShift<int16, int32, 16>, SC::DecodeIdentity<int32> > >(sampleData16 + chn, copySamples, modChannels, buffer[chn], srcSize, 1); + } + } + + return FLAC__STREAM_DECODER_WRITE_STATUS_CONTINUE; + } + + static void metadata_cb(const FLAC__StreamDecoder *, const FLAC__StreamMetadata *metadata, void *client_data) + { + FLACDecoder &client = *static_cast<FLACDecoder *>(client_data); + if(client.sample > client.sndFile.GetNumSamples()) + { + client.sndFile.m_nSamples = client.sample; + } + ModSample &sample = client.sndFile.GetSample(client.sample); + + if(metadata->type == FLAC__METADATA_TYPE_STREAMINFO && metadata->data.stream_info.total_samples != 0) + { + // Init sample information + client.sndFile.DestroySampleThreadsafe(client.sample); + client.sndFile.m_szNames[client.sample] = ""; + sample.Initialize(); + sample.uFlags.set(CHN_16BIT, metadata->data.stream_info.bits_per_sample > 8); + sample.uFlags.set(CHN_STEREO, metadata->data.stream_info.channels > 1); + sample.nLength = mpt::saturate_cast<SmpLength>(metadata->data.stream_info.total_samples); + LimitMax(sample.nLength, MAX_SAMPLE_LENGTH); + sample.nC5Speed = metadata->data.stream_info.sample_rate; + client.ready = (sample.AllocateSample() != 0); + } else if(metadata->type == FLAC__METADATA_TYPE_APPLICATION && !memcmp(metadata->data.application.id, "riff", 4) && client.ready) + { + // Try reading RIFF loop points and other sample information + FileReader data(mpt::as_span(metadata->data.application.data, metadata->length)); + FileReader::ChunkList<RIFFChunk> chunks = data.ReadChunks<RIFFChunk>(2); + + // We're not really going to read a WAV file here because there will be only one RIFF chunk per metadata event, but we can still re-use the code for parsing RIFF metadata... + WAVReader riffReader(data); + riffReader.FindMetadataChunks(chunks); + riffReader.ApplySampleSettings(sample, client.sndFile.GetCharsetInternal(), client.sndFile.m_szNames[client.sample]); + } else if(metadata->type == FLAC__METADATA_TYPE_VORBIS_COMMENT && client.ready) + { + // Try reading Vorbis Comments for sample title, sample rate and loop points + SmpLength loopStart = 0, loopLength = 0; + for(FLAC__uint32 i = 0; i < metadata->data.vorbis_comment.num_comments; i++) + { + const char *tag = mpt::byte_cast<const char *>(metadata->data.vorbis_comment.comments[i].entry); + const FLAC__uint32 length = metadata->data.vorbis_comment.comments[i].length; + if(length > 6 && !mpt::CompareNoCaseAscii(tag, "TITLE=", 6)) + { + client.sndFile.m_szNames[client.sample] = mpt::ToCharset(client.sndFile.GetCharsetInternal(), mpt::Charset::UTF8, mpt::String::ReadBuf(mpt::String::maybeNullTerminated, tag + 6, length - 6)); + } else if(length > 11 && !mpt::CompareNoCaseAscii(tag, "SAMPLERATE=", 11)) + { + uint32 sampleRate = ConvertStrTo<uint32>(tag + 11); + if(sampleRate > 0) sample.nC5Speed = sampleRate; + } else if(length > 10 && !mpt::CompareNoCaseAscii(tag, "LOOPSTART=", 10)) + { + loopStart = ConvertStrTo<SmpLength>(tag + 10); + } else if(length > 11 && !mpt::CompareNoCaseAscii(tag, "LOOPLENGTH=", 11)) + { + loopLength = ConvertStrTo<SmpLength>(tag + 11); + } + } + if(loopLength > 0) + { + sample.nLoopStart = loopStart; + sample.nLoopEnd = loopStart + loopLength; + sample.uFlags.set(CHN_LOOP); + sample.SanitizeLoops(); + } + } + } + + static void error_cb(const FLAC__StreamDecoder *, FLAC__StreamDecoderErrorStatus, void *) + { + } +}; + +#endif // MPT_WITH_FLAC + + +bool CSoundFile::ReadFLACSample(SAMPLEINDEX sample, FileReader &file) +{ +#ifdef MPT_WITH_FLAC + file.Rewind(); + bool isOgg = false; +#ifdef MPT_WITH_OGG + uint32 oggFlacBitstreamSerial = 0; +#endif + // Check whether we are dealing with native FLAC, OggFlac or no FLAC at all. + if(file.ReadMagic("fLaC")) + { // ok + isOgg = false; +#ifdef MPT_WITH_OGG + } else if(file.ReadMagic("OggS")) + { // use libogg to find the first OggFlac stream header + file.Rewind(); + bool oggOK = false; + bool needMoreData = true; + constexpr long bufsize = 65536; + std::size_t readSize = 0; + char *buf = nullptr; + ogg_sync_state oy; + MemsetZero(oy); + ogg_page og; + MemsetZero(og); + std::map<uint32, ogg_stream_state*> oggStreams; + ogg_packet op; + MemsetZero(op); + if(ogg_sync_init(&oy) != 0) + { + return false; + } + while(needMoreData) + { + if(file.NoBytesLeft()) + { // stop at EOF + oggOK = false; + needMoreData = false; + break; + } + buf = ogg_sync_buffer(&oy, bufsize); + if(!buf) + { + oggOK = false; + needMoreData = false; + break; + } + readSize = file.ReadRaw(mpt::span(buf, bufsize)).size(); + if(ogg_sync_wrote(&oy, static_cast<long>(readSize)) != 0) + { + oggOK = false; + needMoreData = false; + break; + } + while(ogg_sync_pageout(&oy, &og) == 1) + { + if(!ogg_page_bos(&og)) + { // we stop scanning when seeing the first noo-begin-of-stream page + oggOK = false; + needMoreData = false; + break; + } + uint32 serial = ogg_page_serialno(&og); + if(!oggStreams[serial]) + { // previously unseen stream serial + oggStreams[serial] = new ogg_stream_state(); + MemsetZero(*(oggStreams[serial])); + if(ogg_stream_init(oggStreams[serial], serial) != 0) + { + delete oggStreams[serial]; + oggStreams.erase(serial); + oggOK = false; + needMoreData = false; + break; + } + } + if(ogg_stream_pagein(oggStreams[serial], &og) != 0) + { // invalid page + oggOK = false; + needMoreData = false; + break; + } + if(ogg_stream_packetout(oggStreams[serial], &op) != 1) + { // partial or broken packet, continue with more data + continue; + } + if(op.packetno != 0) + { // non-begin-of-stream packet. + // This should not appear on first page for any known ogg codec, + // but deal gracefully with badly mused streams in that regard. + continue; + } + FileReader packet(mpt::as_span(op.packet, op.bytes)); + if(packet.ReadIntLE<uint8>() == 0x7f && packet.ReadMagic("FLAC")) + { // looks like OggFlac + oggOK = true; + oggFlacBitstreamSerial = serial; + needMoreData = false; + break; + } + } + } + while(oggStreams.size() > 0) + { + uint32 serial = oggStreams.begin()->first; + ogg_stream_clear(oggStreams[serial]); + delete oggStreams[serial]; + oggStreams.erase(serial); + } + ogg_sync_clear(&oy); + if(!oggOK) + { + return false; + } + isOgg = true; +#else // !MPT_WITH_OGG + } else if(file.CanRead(78) && file.ReadMagic("OggS")) + { // first OggFlac page is exactly 78 bytes long + // only support plain OggFlac here with the FLAC logical bitstream being the first one + uint8 oggPageVersion = file.ReadIntLE<uint8>(); + uint8 oggPageHeaderType = file.ReadIntLE<uint8>(); + uint64 oggPageGranulePosition = file.ReadIntLE<uint64>(); + uint32 oggPageBitstreamSerialNumber = file.ReadIntLE<uint32>(); + uint32 oggPageSequenceNumber = file.ReadIntLE<uint32>(); + uint32 oggPageChecksum = file.ReadIntLE<uint32>(); + uint8 oggPageSegments = file.ReadIntLE<uint8>(); + uint8 oggPageSegmentLength = file.ReadIntLE<uint8>(); + if(oggPageVersion != 0) + { // unknown Ogg version + return false; + } + if(!(oggPageHeaderType & 0x02) || (oggPageHeaderType& 0x01)) + { // not BOS or continuation + return false; + } + if(oggPageGranulePosition != 0) + { // not starting position + return false; + } + if(oggPageSequenceNumber != 0) + { // not first page + return false; + } + // skip CRC check for now + if(oggPageSegments != 1) + { // first OggFlac page must contain exactly 1 segment + return false; + } + if(oggPageSegmentLength != 51) + { // segment length must be 51 bytes in OggFlac mapping + return false; + } + if(file.ReadIntLE<uint8>() != 0x7f) + { // OggFlac mapping demands 0x7f packet type + return false; + } + if(!file.ReadMagic("FLAC")) + { // OggFlac magic + return false; + } + if(file.ReadIntLE<uint8>() != 0x01) + { // OggFlac major version + return false; + } + // by now, we are pretty confident that we are not parsing random junk + isOgg = true; +#endif // MPT_WITH_OGG + } else + { + return false; + } + file.Rewind(); + + FLAC__StreamDecoder *decoder = FLAC__stream_decoder_new(); + if(decoder == nullptr) + { + return false; + } + +#ifdef MPT_WITH_OGG + if(isOgg) + { + // force flac decoding of the logical bitstream that actually is OggFlac + if(!FLAC__stream_decoder_set_ogg_serial_number(decoder, oggFlacBitstreamSerial)) + { + FLAC__stream_decoder_delete(decoder); + return false; + } + } +#endif + + // Give me all the metadata! + FLAC__stream_decoder_set_metadata_respond_all(decoder); + + FLACDecoder client(file, *this, sample); + + // Init decoder + FLAC__StreamDecoderInitStatus initStatus = isOgg ? + FLAC__stream_decoder_init_ogg_stream(decoder, FLACDecoder::read_cb, FLACDecoder::seek_cb, FLACDecoder::tell_cb, FLACDecoder::length_cb, FLACDecoder::eof_cb, FLACDecoder::write_cb, FLACDecoder::metadata_cb, FLACDecoder::error_cb, &client) + : + FLAC__stream_decoder_init_stream(decoder, FLACDecoder::read_cb, FLACDecoder::seek_cb, FLACDecoder::tell_cb, FLACDecoder::length_cb, FLACDecoder::eof_cb, FLACDecoder::write_cb, FLACDecoder::metadata_cb, FLACDecoder::error_cb, &client) + ; + if(initStatus != FLAC__STREAM_DECODER_INIT_STATUS_OK) + { + FLAC__stream_decoder_delete(decoder); + return false; + } + + // Decode file + FLAC__stream_decoder_process_until_end_of_stream(decoder); + FLAC__stream_decoder_finish(decoder); + FLAC__stream_decoder_delete(decoder); + + if(client.ready && Samples[sample].HasSampleData()) + { + Samples[sample].Convert(MOD_TYPE_IT, GetType()); + Samples[sample].PrecomputeLoops(*this, false); + return true; + } +#else + MPT_UNREFERENCED_PARAMETER(sample); + MPT_UNREFERENCED_PARAMETER(file); +#endif // MPT_WITH_FLAC + return false; +} + + +#ifdef MPT_WITH_FLAC + +// RAII-style helper struct for FLAC encoder +struct FLAC__StreamEncoder_RAII +{ + std::ostream &f; + FLAC__StreamEncoder *encoder = nullptr; + + operator FLAC__StreamEncoder *() { return encoder; } + + FLAC__StreamEncoder_RAII(std::ostream &f_) : f(f_), encoder(FLAC__stream_encoder_new()) { } + ~FLAC__StreamEncoder_RAII() + { + FLAC__stream_encoder_delete(encoder); + } + + static FLAC__StreamEncoderWriteStatus StreamEncoderWriteCallback(const FLAC__StreamEncoder *encoder, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame, void *client_data) + { + mpt::ofstream & file = *reinterpret_cast<mpt::ofstream*>(client_data); + MPT_UNUSED_VARIABLE(encoder); + MPT_UNUSED_VARIABLE(samples); + MPT_UNUSED_VARIABLE(current_frame); + if(!mpt::IO::WriteRaw(file, mpt::as_span(buffer, bytes))) + { + return FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; + } + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; + } + static FLAC__StreamEncoderSeekStatus StreamEncoderSeekCallback(const FLAC__StreamEncoder *encoder, FLAC__uint64 absolute_byte_offset, void *client_data) + { + mpt::ofstream & file = *reinterpret_cast<mpt::ofstream*>(client_data); + MPT_UNUSED_VARIABLE(encoder); + if(!mpt::in_range<mpt::IO::Offset>(absolute_byte_offset)) + { + return FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR; + } + if(!mpt::IO::SeekAbsolute(file, static_cast<mpt::IO::Offset>(absolute_byte_offset))) + { + return FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR; + } + return FLAC__STREAM_ENCODER_SEEK_STATUS_OK; + } + static FLAC__StreamEncoderTellStatus StreamEncoderTellCallback(const FLAC__StreamEncoder *encoder, FLAC__uint64 *absolute_byte_offset, void *client_data) + { + mpt::ofstream & file = *reinterpret_cast<mpt::ofstream*>(client_data); + MPT_UNUSED_VARIABLE(encoder); + mpt::IO::Offset pos = mpt::IO::TellWrite(file); + if(pos < 0) + { + return FLAC__STREAM_ENCODER_TELL_STATUS_ERROR; + } + if(!mpt::in_range<FLAC__uint64>(pos)) + { + return FLAC__STREAM_ENCODER_TELL_STATUS_ERROR; + } + *absolute_byte_offset = static_cast<FLAC__uint64>(pos); + return FLAC__STREAM_ENCODER_TELL_STATUS_OK; + } + +}; + +class FLAC__StreamMetadata_RAII : public std::vector<FLAC__StreamMetadata *> +{ +public: + FLAC__StreamMetadata_RAII(std::initializer_list<FLAC__StreamMetadata *> init) + : std::vector<FLAC__StreamMetadata *>(init) + { } + + ~FLAC__StreamMetadata_RAII() + { + for(auto m : *this) + { + FLAC__metadata_object_delete(m); + } + } +}; + +#endif + + +#ifndef MODPLUG_NO_FILESAVE +bool CSoundFile::SaveFLACSample(SAMPLEINDEX nSample, std::ostream &f) const +{ +#ifdef MPT_WITH_FLAC + const ModSample &sample = Samples[nSample]; + if(sample.uFlags[CHN_ADLIB]) + return false; + + FLAC__StreamEncoder_RAII encoder(f); + if(encoder == nullptr) + return false; + + uint32 sampleRate = sample.GetSampleRate(GetType()); + + // First off, set up all the metadata... + FLAC__StreamMetadata_RAII metadata = + { + FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT), + FLAC__metadata_object_new(FLAC__METADATA_TYPE_APPLICATION), // MPT sample information + FLAC__metadata_object_new(FLAC__METADATA_TYPE_APPLICATION), // Loop points + FLAC__metadata_object_new(FLAC__METADATA_TYPE_APPLICATION), // Cue points + }; + + unsigned numBlocks = 2; + if(metadata[0]) + { + // Store sample name + FLAC__StreamMetadata_VorbisComment_Entry entry; + FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "TITLE", mpt::ToCharset(mpt::Charset::UTF8, GetCharsetInternal(), m_szNames[nSample]).c_str()); + FLAC__metadata_object_vorbiscomment_append_comment(metadata[0], entry, false); + FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "ENCODER", mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()).c_str()); + FLAC__metadata_object_vorbiscomment_append_comment(metadata[0], entry, false); + if(sampleRate > FLAC__MAX_SAMPLE_RATE) + { + // FLAC only supports a sample rate of up to 655350 Hz. + // Store the real sample rate in a custom Vorbis comment. + FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, "SAMPLERATE", mpt::afmt::val(sampleRate).c_str()); + FLAC__metadata_object_vorbiscomment_append_comment(metadata[0], entry, false); + } + } + if(metadata[1]) + { + // Write MPT sample information + memcpy(metadata[1]->data.application.id, "riff", 4); + + struct + { + RIFFChunk header; + WAVExtraChunk mptInfo; + } chunk; + + chunk.header.id = RIFFChunk::idxtra; + chunk.header.length = sizeof(WAVExtraChunk); + + chunk.mptInfo.ConvertToWAV(sample, GetType()); + + const uint32 length = sizeof(RIFFChunk) + sizeof(WAVExtraChunk); + + FLAC__metadata_object_application_set_data(metadata[1], reinterpret_cast<FLAC__byte *>(&chunk), length, true); + } + if(metadata[numBlocks] && (sample.uFlags[CHN_LOOP | CHN_SUSTAINLOOP] || ModCommand::IsNote(sample.rootNote))) + { + // Store loop points / root note information + memcpy(metadata[numBlocks]->data.application.id, "riff", 4); + + struct + { + RIFFChunk header; + WAVSampleInfoChunk info; + WAVSampleLoop loops[2]; + } chunk; + + chunk.header.id = RIFFChunk::idsmpl; + chunk.header.length = sizeof(WAVSampleInfoChunk); + + chunk.info.ConvertToWAV(sample.GetSampleRate(GetType()), sample.rootNote); + + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + chunk.loops[chunk.info.numLoops++].ConvertToWAV(sample.nSustainStart, sample.nSustainEnd, sample.uFlags[CHN_PINGPONGSUSTAIN]); + chunk.header.length += sizeof(WAVSampleLoop); + } + if(sample.uFlags[CHN_LOOP]) + { + chunk.loops[chunk.info.numLoops++].ConvertToWAV(sample.nLoopStart, sample.nLoopEnd, sample.uFlags[CHN_PINGPONGLOOP]); + chunk.header.length += sizeof(WAVSampleLoop); + } + + const uint32 length = sizeof(RIFFChunk) + chunk.header.length; + + FLAC__metadata_object_application_set_data(metadata[numBlocks], reinterpret_cast<FLAC__byte *>(&chunk), length, true); + numBlocks++; + } + if(metadata[numBlocks] && sample.HasCustomCuePoints()) + { + // Store cue points + memcpy(metadata[numBlocks]->data.application.id, "riff", 4); + + struct + { + RIFFChunk header; + uint32le numPoints; + WAVCuePoint cues[mpt::array_size<decltype(sample.cues)>::size]; + } chunk{}; + + chunk.header.id = RIFFChunk::idcue_; + chunk.header.length = 4 + sizeof(chunk.cues); + chunk.numPoints = mpt::saturate_cast<uint32>(std::size(sample.cues)); + + for(uint32 i = 0; i < std::size(sample.cues); i++) + { + chunk.cues[i].ConvertToWAV(i, sample.cues[i]); + } + + const uint32 length = sizeof(RIFFChunk) + chunk.header.length; + + FLAC__metadata_object_application_set_data(metadata[numBlocks], reinterpret_cast<FLAC__byte *>(&chunk), length, true); + numBlocks++; + } + + // FLAC allows a maximum sample rate of 655350 Hz. + // If the real rate is higher, we store it in a Vorbis comment above. + LimitMax(sampleRate, FLAC__MAX_SAMPLE_RATE); + if(!FLAC__format_sample_rate_is_subset(sampleRate)) + { + // FLAC only supports 10 Hz granularity for frequencies above 65535 Hz if the streamable subset is chosen. + FLAC__stream_encoder_set_streamable_subset(encoder, false); + } + FLAC__stream_encoder_set_channels(encoder, sample.GetNumChannels()); + FLAC__stream_encoder_set_bits_per_sample(encoder, sample.GetElementarySampleSize() * 8); + FLAC__stream_encoder_set_sample_rate(encoder, sampleRate); + FLAC__stream_encoder_set_total_samples_estimate(encoder, sample.nLength); + FLAC__stream_encoder_set_metadata(encoder, metadata.data(), numBlocks); +#ifdef MODPLUG_TRACKER + FLAC__stream_encoder_set_compression_level(encoder, TrackerSettings::Instance().m_FLACCompressionLevel); +#endif // MODPLUG_TRACKER + + bool success = FLAC__stream_encoder_init_stream(encoder, &FLAC__StreamEncoder_RAII::StreamEncoderWriteCallback, &FLAC__StreamEncoder_RAII::StreamEncoderSeekCallback, &FLAC__StreamEncoder_RAII::StreamEncoderTellCallback, nullptr, &encoder.f) == FLAC__STREAM_ENCODER_INIT_STATUS_OK; + + // Convert and encode sample data + SmpLength framesRemain = sample.nLength, framesRead = 0; + const uint8 numChannels = sample.GetNumChannels(); + FLAC__int32 buffer[mpt::IO::BUFFERSIZE_TINY]; + while(framesRemain && success) + { + const SmpLength copyFrames = std::min(framesRemain, mpt::saturate_cast<SmpLength>(std::size(buffer) / numChannels)); + + // First, convert to a 32-bit integer buffer + switch(sample.GetElementarySampleSize()) + { + case 1: std::copy(sample.sample8() + framesRead * numChannels, sample.sample8() + (framesRead + copyFrames) * numChannels, std::begin(buffer)); break; + case 2: std::copy(sample.sample16() + framesRead * numChannels, sample.sample16() + (framesRead + copyFrames) * numChannels, std::begin(buffer)); break; + default: MPT_ASSERT_NOTREACHED(); + } + + // Now do the actual encoding + success = FLAC__stream_encoder_process_interleaved(encoder, buffer, copyFrames) != static_cast<FLAC__bool>(false); + + framesRead += copyFrames; + framesRemain -= copyFrames; + } + + FLAC__stream_encoder_finish(encoder); + + return success; +#else + MPT_UNREFERENCED_PARAMETER(nSample); + MPT_UNREFERENCED_PARAMETER(f); + return false; +#endif // MPT_WITH_FLAC +} +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMP3.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMP3.cpp new file mode 100644 index 00000000..a290a1ea --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMP3.cpp @@ -0,0 +1,726 @@ +/* + * SampleFormatMP3.cpp + * ------------------- + * Purpose: MP3 sample import. + * Notes : + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif +#include "../common/misc_util.h" +#include "Tagging.h" +#include "Loaders.h" +#include "../common/FileReader.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/Copy.hpp" +#include "../soundlib/ModSampleCopy.h" +#include "../common/ComponentManager.h" +#ifdef MPT_ENABLE_MP3_SAMPLES +#include "MPEGFrame.h" +#endif // MPT_ENABLE_MP3_SAMPLES +#if defined(MPT_WITH_MINIMP3) +#include "mpt/base/alloc.hpp" +#endif // MPT_WITH_MINIMP3 + +#if defined(MPT_WITH_MINIMP3) +#include <minimp3/minimp3.h> +#endif // MPT_WITH_MINIMP3 + +// mpg123 must be last because of mpg123 large file support insanity +#if defined(MPT_WITH_MPG123) + +#include <stddef.h> +#include <stdlib.h> +#include <sys/types.h> + +#if MPT_OS_OPENBSD +// This is kind-of a hack. +// See <https://sourceforge.net/p/mpg123/bugs/330/>. +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif +#ifdef _FILE_OFFSET_BITS +#undef _FILE_OFFSET_BITS +#endif +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif +#endif +#include <mpg123.h> + +#endif + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// MP3 Samples + +#if defined(MPT_WITH_MPG123) + +typedef off_t mpg123_off_t; + +typedef size_t mpg123_size_t; + +// Check for exactly _MSC_VER as libmpg123 does, in order to also catch clang-cl. +#ifdef _MSC_VER +// ssize_t definition in libmpg123.h.in should never have existed at all. +// It got removed from libmpg23.h.in after 1.28.0 and before 1.28.1. +typedef ptrdiff_t mpg123_ssize_t; +#else +typedef ssize_t mpg123_ssize_t; +#endif + +class ComponentMPG123 + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentMPG123, "") + +public: + + static mpg123_ssize_t FileReaderRead(void *fp, void *buf, mpg123_size_t count) + { + FileReader &file = *static_cast<FileReader *>(fp); + std::size_t readBytes = std::min(count, static_cast<size_t>(file.BytesLeft())); + file.ReadRaw(mpt::span(mpt::void_cast<std::byte*>(buf), readBytes)); + return readBytes; + } + static mpg123_off_t FileReaderLSeek(void *fp, mpg123_off_t offset, int whence) + { + FileReader &file = *static_cast<FileReader *>(fp); + FileReader::off_t oldpos = file.GetPosition(); + if(whence == SEEK_CUR) file.Seek(file.GetPosition() + offset); + else if(whence == SEEK_END) file.Seek(file.GetLength() + offset); + else file.Seek(offset); + MPT_MAYBE_CONSTANT_IF(!mpt::in_range<mpg123_off_t>(file.GetPosition())) + { + file.Seek(oldpos); + return static_cast<mpg123_off_t>(-1); + } + return static_cast<mpg123_off_t>(file.GetPosition()); + } + +public: + ComponentMPG123() + : ComponentBuiltin() + { + return; + } + bool DoInitialize() override + { + if(mpg123_init() != 0) + { + return false; + } + return true; + } + virtual ~ComponentMPG123() + { + if(IsAvailable()) + { + mpg123_exit(); + } + } +}; + + +static mpt::ustring ReadMPG123String(const mpg123_string &str) +{ + mpt::ustring result; + if(!str.p) + { + return result; + } + if(str.fill < 1) + { + return result; + } + result = mpt::ToUnicode(mpt::Charset::UTF8, std::string(str.p, str.p + str.fill - 1)); + return result; +} + +static mpt::ustring ReadMPG123String(const mpg123_string *str) +{ + mpt::ustring result; + if(!str) + { + return result; + } + result = ReadMPG123String(*str); + return result; +} + +template <std::size_t N> +static mpt::ustring ReadMPG123String(const char (&str)[N]) +{ + return mpt::ToUnicode(mpt::Charset::ISO8859_1, mpt::String::ReadBuf(mpt::String::spacePadded, str)); +} + +#endif // MPT_WITH_MPG123 + + +#if defined(MPT_WITH_MINIMP3) +#if MPT_COMPILER_GCC +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wframe-larger-than=16000" +#endif // MPT_COMPILER_GCC +#if MPT_CLANG_AT_LEAST(13,0,0) +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wframe-larger-than" +#endif // MPT_COMPILER_CLANG +static MPT_NOINLINE int mp3dec_decode_frame_no_inline(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + return mp3dec_decode_frame(dec, mp3, mp3_bytes, pcm, info); +} +#if MPT_CLANG_AT_LEAST(13,0,0) +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#if MPT_COMPILER_GCC +#pragma GCC diagnostic pop +#endif // MPT_COMPILER_GCC +#endif // MPT_WITH_MINIMP3 + + +bool CSoundFile::ReadMP3Sample(SAMPLEINDEX sample, FileReader &file, bool raw, bool mo3Decode) +{ +#if defined(MPT_WITH_MPG123) || defined(MPT_WITH_MINIMP3) + + // Check file for validity, or else mpg123 will happily munch many files that start looking vaguely resemble an MPEG stream mid-file. + file.Rewind(); + while(file.CanRead(4)) + { + uint8 magic[3]; + file.ReadArray(magic); + + if(!memcmp(magic, "ID3", 3)) + { + // Skip ID3 tags + uint8 header[7]; + file.ReadArray(header); + + uint32 size = 0; + for(int i = 3; i < 7; i++) + { + if(header[i] & 0x80) + return false; + size = (size << 7) | header[i]; + } + file.Skip(size); + } else if(!memcmp(magic, "APE", 3) && file.ReadMagic("TAGEX")) + { + // Skip APE tags + uint32 size = file.ReadUint32LE(); + file.Skip(16 + size); + } else if(!memcmp(magic, "\x00\x00\x00", 3) || !memcmp(magic, "\xFF\x00\x00", 3)) + { + // Some MP3 files are padded with zeroes... + } else if(magic[0] == 0) + { + // This might be some padding, followed by an MPEG header, so try again. + file.SkipBack(2); + } else if(MPEGFrame::IsMPEGHeader(magic)) + { + // This is what we want! + break; + } else + { + // This, on the other hand, isn't. + return false; + } + } + +#endif // MPT_WITH_MPG123 || MPT_WITH_MINIMP3 + +#if defined(MPT_WITH_MPG123) + + ComponentHandle<ComponentMPG123> mpg123; + if(!IsComponentAvailable(mpg123)) + { + return false; + } + + struct MPG123Handle + { + mpg123_handle *mh; + MPG123Handle() : mh(mpg123_new(0, nullptr)) { } + ~MPG123Handle() { mpg123_delete(mh); } + operator mpg123_handle *() { return mh; } + }; + + bool hasLameXingVbriHeader = false; + + if(!raw) + { + + mpg123_off_t length_raw = 0; + mpg123_off_t length_hdr = 0; + + // libmpg123 provides no way to determine whether it parsed ID3V2 or VBR tags. + // Thus, we use a pre-scan with those disabled and compare the resulting length. + // We ignore ID3V2 stream length here, althrough we parse the ID3V2 header. + // libmpg123 only accounts for the VBR info frame if gapless &&!ignore_infoframe, + // thus we switch both of those for comparison. + { + MPG123Handle mh; + if(!mh) + { + return false; + } + file.Rewind(); + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_QUIET, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_AUTO_RESAMPLE, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_REMOVE_FLAGS, MPG123_GAPLESS, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_IGNORE_INFOFRAME, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_REMOVE_FLAGS, MPG123_SKIP_ID3V2, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_IGNORE_STREAMLENGTH, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_INDEX_SIZE, -1000, 0.0)) // auto-grow + { + return false; + } + if(mpg123_replace_reader_handle(mh, ComponentMPG123::FileReaderRead, ComponentMPG123::FileReaderLSeek, 0)) + { + return false; + } + if(mpg123_open_handle(mh, &file)) + { + return false; + } + if(mpg123_scan(mh)) + { + return false; + } + long rate = 0; + int channels = 0; + int encoding = 0; + if(mpg123_getformat(mh, &rate, &channels, &encoding)) + { + return false; + } + if((channels != 1 && channels != 2) || (encoding & (MPG123_ENC_16 | MPG123_ENC_SIGNED)) != (MPG123_ENC_16 | MPG123_ENC_SIGNED)) + { + return false; + } + mpg123_frameinfo frameinfo; + MemsetZero(frameinfo); + if(mpg123_info(mh, &frameinfo)) + { + return false; + } + if(frameinfo.layer < 1 || frameinfo.layer > 3) + { + return false; + } + if(mpg123_param(mh, MPG123_FORCE_RATE, rate, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, (channels > 1) ? MPG123_FORCE_STEREO : MPG123_FORCE_MONO, 0.0)) + { + return false; + } + length_raw = mpg123_length(mh); + } + + { + MPG123Handle mh; + if(!mh) + { + return false; + } + file.Rewind(); + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_QUIET, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_AUTO_RESAMPLE, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_GAPLESS, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_REMOVE_FLAGS, MPG123_IGNORE_INFOFRAME, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_REMOVE_FLAGS, MPG123_SKIP_ID3V2, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_IGNORE_STREAMLENGTH, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_INDEX_SIZE, -1000, 0.0)) // auto-grow + { + return false; + } + if(mpg123_replace_reader_handle(mh, ComponentMPG123::FileReaderRead, ComponentMPG123::FileReaderLSeek, 0)) + { + return false; + } + if(mpg123_open_handle(mh, &file)) + { + return false; + } + if(mpg123_scan(mh)) + { + return false; + } + long rate = 0; + int channels = 0; + int encoding = 0; + if(mpg123_getformat(mh, &rate, &channels, &encoding)) + { + return false; + } + if((channels != 1 && channels != 2) || (encoding & (MPG123_ENC_16 | MPG123_ENC_SIGNED)) != (MPG123_ENC_16 | MPG123_ENC_SIGNED)) + { + return false; + } + mpg123_frameinfo frameinfo; + MemsetZero(frameinfo); + if(mpg123_info(mh, &frameinfo)) + { + return false; + } + if(frameinfo.layer < 1 || frameinfo.layer > 3) + { + return false; + } + if(mpg123_param(mh, MPG123_FORCE_RATE, rate, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, (channels > 1) ? MPG123_FORCE_STEREO : MPG123_FORCE_MONO, 0.0)) + { + return false; + } + length_hdr = mpg123_length(mh); + } + + hasLameXingVbriHeader = (length_raw != length_hdr); + + } + + // Set up decoder... + MPG123Handle mh; + if(!mh) + { + return false; + } + file.Rewind(); + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_QUIET, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, MPG123_AUTO_RESAMPLE, 0.0)) + { + return false; + } + if(mpg123_param(mh, raw ? MPG123_REMOVE_FLAGS : MPG123_ADD_FLAGS, MPG123_GAPLESS, 0.0)) + { + return false; + } + if(mpg123_param(mh, raw ? MPG123_ADD_FLAGS : MPG123_REMOVE_FLAGS, MPG123_IGNORE_INFOFRAME, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_REMOVE_FLAGS, MPG123_SKIP_ID3V2, 0.0)) + { + return false; + } + if(mpg123_param(mh, raw ? MPG123_ADD_FLAGS : MPG123_REMOVE_FLAGS, MPG123_IGNORE_STREAMLENGTH, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_INDEX_SIZE, -1000, 0.0)) // auto-grow + { + return false; + } + if(mpg123_replace_reader_handle(mh, ComponentMPG123::FileReaderRead, ComponentMPG123::FileReaderLSeek, 0)) + { + return false; + } + if(mpg123_open_handle(mh, &file)) + { + return false; + } + if(mpg123_scan(mh)) + { + return false; + } + long rate = 0; + int channels = 0; + int encoding = 0; + if(mpg123_getformat(mh, &rate, &channels, &encoding)) + { + return false; + } + if((channels != 1 && channels != 2) || (encoding & (MPG123_ENC_16 | MPG123_ENC_SIGNED)) != (MPG123_ENC_16 | MPG123_ENC_SIGNED)) + { + return false; + } + mpg123_frameinfo frameinfo; + MemsetZero(frameinfo); + if(mpg123_info(mh, &frameinfo)) + { + return false; + } + if(frameinfo.layer < 1 || frameinfo.layer > 3) + { + return false; + } + // We force samplerate, channels and sampleformat, which in + // combination with auto-resample (set above) will cause libmpg123 + // to stay with the given format even for completely confused + // MPG123_FRANKENSTEIN streams. + // Note that we cannot rely on mpg123_length() for the way we + // decode the mpeg streams because it depends on the actual frame + // sample rate instead of the returned sample rate. + if(mpg123_param(mh, MPG123_FORCE_RATE, rate, 0.0)) + { + return false; + } + if(mpg123_param(mh, MPG123_ADD_FLAGS, (channels > 1) ? MPG123_FORCE_STEREO : MPG123_FORCE_MONO, 0.0)) + { + return false; + } + + std::vector<int16> data; + + // decoder delay + std::size_t data_skip_frames = 0; + if(!raw && !hasLameXingVbriHeader) + { + if(frameinfo.layer == 1) + { + data_skip_frames = 240 + 1; + } else if(frameinfo.layer == 2) + { + data_skip_frames = 240 + 1; + } else if(frameinfo.layer == 3) + { + data_skip_frames = 528 + 1; + } + } + + std::vector<std::byte> buf_bytes; + std::vector<int16> buf_samples; + bool decode_error = false; + bool decode_done = false; + while(!decode_error && !decode_done) + { + buf_bytes.resize(mpg123_outblock(mh)); + buf_samples.resize(buf_bytes.size() / sizeof(int16)); + mpg123_size_t buf_bytes_decoded = 0; + int mpg123_read_result = mpg123_read(mh, mpt::byte_cast<unsigned char*>(buf_bytes.data()), buf_bytes.size(), &buf_bytes_decoded); + std::memcpy(buf_samples.data(), buf_bytes.data(), buf_bytes_decoded); + mpt::append(data, buf_samples.data(), buf_samples.data() + buf_bytes_decoded / sizeof(int16)); + if((data.size() / channels) > MAX_SAMPLE_LENGTH) + { + break; + } + if(mpg123_read_result == MPG123_OK) + { + // continue + } else if(mpg123_read_result == MPG123_NEW_FORMAT) + { + // continue + } else if(mpg123_read_result == MPG123_DONE) + { + decode_done = true; + } else + { + decode_error = true; + } + } + + if((data.size() / channels) > MAX_SAMPLE_LENGTH) + { + return false; + } + + FileTags tags; + mpg123_id3v1 *id3v1 = nullptr; + mpg123_id3v2 *id3v2 = nullptr; + if(mpg123_id3(mh, &id3v1, &id3v2) == MPG123_OK) + { + if(id3v2) + { + if(tags.title.empty()) tags.title = ReadMPG123String(id3v2->title); + if(tags.artist.empty()) tags.artist = ReadMPG123String(id3v2->artist); + if(tags.album.empty()) tags.album = ReadMPG123String(id3v2->album); + if(tags.year.empty()) tags.year = ReadMPG123String(id3v2->year); + if(tags.genre.empty()) tags.genre = ReadMPG123String(id3v2->genre); + if(tags.comments.empty()) tags.comments = ReadMPG123String(id3v2->comment); + } + if(id3v1) + { + if(tags.title.empty()) tags.title = ReadMPG123String(id3v1->title); + if(tags.artist.empty()) tags.artist = ReadMPG123String(id3v1->artist); + if(tags.album.empty()) tags.album = ReadMPG123String(id3v1->album); + if(tags.year.empty()) tags.year = ReadMPG123String(id3v1->year); + if(tags.comments.empty()) tags.comments = ReadMPG123String(id3v1->comment); + } + } + mpt::ustring sampleName = GetSampleNameFromTags(tags); + + DestroySampleThreadsafe(sample); + if(!mo3Decode) + { + m_szNames[sample] = mpt::ToCharset(GetCharsetInternal(), sampleName); + Samples[sample].Initialize(); + Samples[sample].nC5Speed = rate; + } + Samples[sample].nLength = mpt::saturate_cast<SmpLength>((data.size() / channels) - data_skip_frames); + + Samples[sample].uFlags.set(CHN_16BIT); + Samples[sample].uFlags.set(CHN_STEREO, channels == 2); + Samples[sample].AllocateSample(); + + if(Samples[sample].HasSampleData()) + { + std::memcpy(Samples[sample].sampleb(), data.data() + (data_skip_frames * channels), (data.size() - (data_skip_frames * channels)) * sizeof(int16)); + } + + if(!mo3Decode) + { + Samples[sample].Convert(MOD_TYPE_IT, GetType()); + Samples[sample].PrecomputeLoops(*this, false); + } + return Samples[sample].HasSampleData(); + +#elif defined(MPT_WITH_MINIMP3) + + MPT_UNREFERENCED_PARAMETER(raw); + + file.Rewind(); + FileReader::PinnedView rawDataView = file.GetPinnedView(); + int64 bytes_left = rawDataView.size(); + const uint8 *stream_pos = mpt::byte_cast<const uint8 *>(rawDataView.data()); + + std::vector<int16> raw_sample_data; + + mpt::heap_value<mp3dec_t> mp3; + std::memset(&*mp3, 0, sizeof(mp3dec_t)); + mp3dec_init(&*mp3); + + int rate = 0; + int channels = 0; + + mp3dec_frame_info_t info; + std::memset(&info, 0, sizeof(mp3dec_frame_info_t)); + std::vector<int16> sample_buf(MINIMP3_MAX_SAMPLES_PER_FRAME); + do + { + int frame_samples = mp3dec_decode_frame_no_inline(&*mp3, stream_pos, mpt::saturate_cast<int>(bytes_left), sample_buf.data(), &info); + if(frame_samples < 0 || info.frame_bytes < 0) break; // internal error in minimp3 + if(frame_samples > 0 && info.frame_bytes == 0) break; // internal error in minimp3 + if(frame_samples == 0 && info.frame_bytes == 0) break; // end of stream, no progress + if(frame_samples == 0 && info.frame_bytes > 0) do { } while(0); // decoder skipped non-mp3 data + if(frame_samples > 0 && info.frame_bytes > 0) do { } while(0); // normal + if(info.frame_bytes > 0) + { + if(rate != 0 && rate != info.hz) break; // inconsistent stream + if(channels != 0 && channels != info.channels) break; // inconsistent stream + rate = info.hz; + channels = info.channels; + if(rate <= 0) break; // broken stream + if(channels != 1 && channels != 2) break; // broken stream + stream_pos += std::clamp(info.frame_bytes, 0, mpt::saturate_cast<int>(bytes_left)); + bytes_left -= std::clamp(info.frame_bytes, 0, mpt::saturate_cast<int>(bytes_left)); + if(frame_samples > 0) + { + try + { + mpt::append(raw_sample_data, sample_buf.data(), sample_buf.data() + frame_samples * channels); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + break; + } + } + } + if((raw_sample_data.size() / channels) > MAX_SAMPLE_LENGTH) + { + break; + } + } while(bytes_left > 0); + + if(rate == 0 || channels == 0 || raw_sample_data.empty()) + { + return false; + } + + if((raw_sample_data.size() / channels) > MAX_SAMPLE_LENGTH) + { + return false; + } + + DestroySampleThreadsafe(sample); + if(!mo3Decode) + { + m_szNames[sample] = ""; + Samples[sample].Initialize(); + Samples[sample].nC5Speed = rate; + } + Samples[sample].nLength = mpt::saturate_cast<SmpLength>(raw_sample_data.size() / channels); + + Samples[sample].uFlags.set(CHN_16BIT); + Samples[sample].uFlags.set(CHN_STEREO, channels == 2); + Samples[sample].AllocateSample(); + + if(Samples[sample].HasSampleData()) + { + std::copy(raw_sample_data.begin(), raw_sample_data.end(), Samples[sample].sample16()); + } + + if(!mo3Decode) + { + Samples[sample].Convert(MOD_TYPE_IT, GetType()); + Samples[sample].PrecomputeLoops(*this, false); + } + return Samples[sample].HasSampleData(); + +#else + + MPT_UNREFERENCED_PARAMETER(sample); + MPT_UNREFERENCED_PARAMETER(file); + MPT_UNREFERENCED_PARAMETER(raw); + MPT_UNREFERENCED_PARAMETER(mo3Decode); + +#endif // MPT_WITH_MPG123 || MPT_WITH_MINIMP3 + + return false; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMediaFoundation.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMediaFoundation.cpp new file mode 100644 index 00000000..07348ec0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatMediaFoundation.cpp @@ -0,0 +1,457 @@ +/* + * SampleFormatMediaSoundation.cpp + * ------------------------------- + * Purpose: MediaFoundation sample import. + * Notes : + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif +#include "../common/misc_util.h" +#include "Tagging.h" +#include "Loaders.h" +#include "../common/FileReader.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/Copy.hpp" +#include "../soundlib/ModSampleCopy.h" +#include "../common/ComponentManager.h" +#if defined(MPT_WITH_MEDIAFOUNDATION) +#include <windows.h> +#include <atlbase.h> +#include <mfapi.h> +#include <mfidl.h> +#include <mfreadwrite.h> +#include <mferror.h> +#include <Propvarutil.h> +#endif // MPT_WITH_MEDIAFOUNDATION + + +OPENMPT_NAMESPACE_BEGIN + + +#if defined(MPT_WITH_MEDIAFOUNDATION) + +struct PropVariant : PROPVARIANT +{ + PropVariant() { PropVariantInit(this); } + ~PropVariant() { PropVariantClear(this); } +}; + +// Implementing IMFByteStream is apparently not enough to stream raw bytes +// data to MediaFoundation. +// Additionally, one has to also implement a custom IMFAsyncResult for the +// BeginRead/EndRead interface which allows transferring the number of read +// bytes around. +// To make things even worse, MediaFoundation fails to detect some AAC and MPEG +// files if a non-file-based or read-only stream is used for opening. +// The only sane option which remains if we do not have an on-disk filename +// available: +// 1 - write a temporary file +// 2 - close it +// 3 - open it using MediaFoundation. +// We use FILE_ATTRIBUTE_TEMPORARY which will try to keep the file data in +// memory just like regular allocated memory and reduce the overhead basically +// to memcpy. + +static FileTags ReadMFMetadata(IMFMediaSource *mediaSource) +{ + + FileTags tags; + + CComPtr<IMFPresentationDescriptor> presentationDescriptor; + if(!SUCCEEDED(mediaSource->CreatePresentationDescriptor(&presentationDescriptor))) + { + return tags; + } + DWORD streams = 0; + if(!SUCCEEDED(presentationDescriptor->GetStreamDescriptorCount(&streams))) + { + return tags; + } + CComPtr<IMFMetadataProvider> metadataProvider; + if(!SUCCEEDED(MFGetService(mediaSource, MF_METADATA_PROVIDER_SERVICE, IID_IMFMetadataProvider, (void**)&metadataProvider))) + { + return tags; + } + CComPtr<IMFMetadata> metadata; + if(!SUCCEEDED(metadataProvider->GetMFMetadata(presentationDescriptor, 0, 0, &metadata))) + { + return tags; + } + + PropVariant varPropNames; + if(!SUCCEEDED(metadata->GetAllPropertyNames(&varPropNames))) + { + return tags; + } + for(DWORD propIndex = 0; propIndex < varPropNames.calpwstr.cElems; ++propIndex) + { + PropVariant propVal; + LPWSTR propName = varPropNames.calpwstr.pElems[propIndex]; + if(S_OK != metadata->GetProperty(propName, &propVal)) + { + break; + } + std::wstring stringVal; +#if !MPT_OS_WINDOWS_WINRT + // WTF, no PropVariantToString() in WinRT + std::vector<WCHAR> wcharVal(256); + for(;;) + { + HRESULT hrToString = PropVariantToString(propVal, wcharVal.data(), mpt::saturate_cast<UINT>(wcharVal.size())); + if(hrToString == S_OK) + { + stringVal = wcharVal.data(); + break; + } else if(hrToString == ERROR_INSUFFICIENT_BUFFER) + { + wcharVal.resize(mpt::exponential_grow(wcharVal.size())); + } else + { + break; + } + } +#endif // !MPT_OS_WINDOWS_WINRT + if(stringVal.length() > 0) + { + if(propName == std::wstring(L"Author")) tags.artist = mpt::ToUnicode(stringVal); + if(propName == std::wstring(L"Title")) tags.title = mpt::ToUnicode(stringVal); + if(propName == std::wstring(L"WM/AlbumTitle")) tags.album = mpt::ToUnicode(stringVal); + if(propName == std::wstring(L"WM/Track")) tags.trackno = mpt::ToUnicode(stringVal); + if(propName == std::wstring(L"WM/Year")) tags.year = mpt::ToUnicode(stringVal); + if(propName == std::wstring(L"WM/Genre")) tags.genre = mpt::ToUnicode(stringVal); + } + } + + return tags; + +} + + +class ComponentMediaFoundation : public ComponentLibrary +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentMediaFoundation, "MediaFoundation") +public: + ComponentMediaFoundation() + : ComponentLibrary(ComponentTypeSystem) + { + return; + } + bool DoInitialize() override + { +#if !MPT_OS_WINDOWS_WINRT + if(!(true + && AddLibrary("mf", mpt::LibraryPath::System(P_("mf"))) + && AddLibrary("mfplat", mpt::LibraryPath::System(P_("mfplat"))) + && AddLibrary("mfreadwrite", mpt::LibraryPath::System(P_("mfreadwrite"))) + && AddLibrary("propsys", mpt::LibraryPath::System(P_("propsys"))) + )) + { + return false; + } +#endif // !MPT_OS_WINDOWS_WINRT + if(!SUCCEEDED(MFStartup(MF_VERSION))) + { + return false; + } + return true; + } + virtual ~ComponentMediaFoundation() + { + if(IsAvailable()) + { + MFShutdown(); + } + } +}; + +#endif // MPT_WITH_MEDIAFOUNDATION + + +#ifdef MODPLUG_TRACKER +std::vector<FileType> CSoundFile::GetMediaFoundationFileTypes() +{ + std::vector<FileType> result; + +#if defined(MPT_WITH_MEDIAFOUNDATION) + + ComponentHandle<ComponentMediaFoundation> mf; + if(!IsComponentAvailable(mf)) + { + return result; + } + + std::map<std::wstring, FileType> guidMap; + + HKEY hkHandlers = NULL; + LSTATUS regResult = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Microsoft\\Windows Media Foundation\\ByteStreamHandlers", 0, KEY_READ, &hkHandlers); + if(regResult != ERROR_SUCCESS) + { + return result; + } + + for(DWORD handlerIndex = 0; ; ++handlerIndex) + { + + WCHAR handlerTypeBuf[256]; + MemsetZero(handlerTypeBuf); + regResult = RegEnumKeyW(hkHandlers, handlerIndex, handlerTypeBuf, 256); + if(regResult != ERROR_SUCCESS) + { + break; + } + + std::wstring handlerType = handlerTypeBuf; + + if(handlerType.length() < 1) + { + continue; + } + + HKEY hkHandler = NULL; + regResult = RegOpenKeyExW(hkHandlers, handlerTypeBuf, 0, KEY_READ, &hkHandler); + if(regResult != ERROR_SUCCESS) + { + continue; + } + + std::vector<WCHAR> valueNameBuf(16384); + std::vector<BYTE> valueData(16384); + for(DWORD valueIndex = 0; ; ++valueIndex) + { + std::fill(valueNameBuf.begin(), valueNameBuf.end(), WCHAR{0}); + DWORD valueNameBufLen = 16384; + DWORD valueType = 0; + std::fill(valueData.begin(), valueData.end(), BYTE{0}); + DWORD valueDataLen = 16384; + regResult = RegEnumValueW(hkHandler, valueIndex, valueNameBuf.data(), &valueNameBufLen, NULL, &valueType, valueData.data(), &valueDataLen); + if(regResult != ERROR_SUCCESS) + { + break; + } + if(valueNameBufLen <= 0 || valueType != REG_SZ || valueDataLen <= 0) + { + continue; + } + + std::wstring guid = std::wstring(valueNameBuf.data()); + + mpt::ustring description = mpt::ToUnicode(ParseMaybeNullTerminatedStringFromBufferWithSizeInBytes<std::wstring>(valueData.data(), valueDataLen)); + description = mpt::replace(description, U_("Byte Stream Handler"), U_("Files")); + description = mpt::replace(description, U_("ByteStreamHandler"), U_("Files")); + + guidMap[guid] + .ShortName(U_("mf")) + .Description(description) + ; + + if(handlerType[0] == L'.') + { + guidMap[guid].AddExtension(mpt::PathString::FromWide(handlerType.substr(1))); + } else + { + guidMap[guid].AddMimeType(mpt::ToCharset(mpt::Charset::ASCII, handlerType)); + } + + } + + RegCloseKey(hkHandler); + hkHandler = NULL; + + } + + RegCloseKey(hkHandlers); + hkHandlers = NULL; + + for(const auto &it : guidMap) + { + result.push_back(it.second); + } + +#endif // MPT_WITH_MEDIAFOUNDATION + + return result; +} +#endif // MODPLUG_TRACKER + + +bool CSoundFile::ReadMediaFoundationSample(SAMPLEINDEX sample, FileReader &file, bool mo3Decode) +{ + +#if !defined(MPT_WITH_MEDIAFOUNDATION) + + MPT_UNREFERENCED_PARAMETER(sample); + MPT_UNREFERENCED_PARAMETER(file); + MPT_UNREFERENCED_PARAMETER(mo3Decode); + return false; + +#else + + ComponentHandle<ComponentMediaFoundation> mf; + if(!IsComponentAvailable(mf)) + { + return false; + } + + file.Rewind(); + // When using MF to decode MP3 samples in MO3 files, we need the mp3 file extension + // for some of them or otherwise MF refuses to recognize them. + mpt::PathString tmpfileExtension = (mo3Decode ? P_("mp3") : P_("tmp")); + OnDiskFileWrapper diskfile(file, tmpfileExtension); + if(!diskfile.IsValid()) + { + return false; + } + + #define MPT_MF_CHECKED(x) do { \ + HRESULT hr = (x); \ + if(!SUCCEEDED(hr)) \ + { \ + return false; \ + } \ + } while(0) + + CComPtr<IMFSourceResolver> sourceResolver; + MPT_MF_CHECKED(MFCreateSourceResolver(&sourceResolver)); + MF_OBJECT_TYPE objectType = MF_OBJECT_INVALID; + CComPtr<IUnknown> unknownMediaSource; + MPT_MF_CHECKED(sourceResolver->CreateObjectFromURL(diskfile.GetFilename().ToWide().c_str(), MF_RESOLUTION_MEDIASOURCE | MF_RESOLUTION_CONTENT_DOES_NOT_HAVE_TO_MATCH_EXTENSION_OR_MIME_TYPE | MF_RESOLUTION_READ, NULL, &objectType, &unknownMediaSource)); + if(objectType != MF_OBJECT_MEDIASOURCE) + { + return false; + } + CComPtr<IMFMediaSource> mediaSource; + MPT_MF_CHECKED(unknownMediaSource->QueryInterface(&mediaSource)); + + FileTags tags = ReadMFMetadata(mediaSource); + + CComPtr<IMFSourceReader> sourceReader; + MPT_MF_CHECKED(MFCreateSourceReaderFromMediaSource(mediaSource, NULL, &sourceReader)); + CComPtr<IMFMediaType> partialType; + MPT_MF_CHECKED(MFCreateMediaType(&partialType)); + MPT_MF_CHECKED(partialType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio)); + MPT_MF_CHECKED(partialType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_PCM)); + MPT_MF_CHECKED(sourceReader->SetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, NULL, partialType)); + CComPtr<IMFMediaType> uncompressedAudioType; + MPT_MF_CHECKED(sourceReader->GetCurrentMediaType((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, &uncompressedAudioType)); + MPT_MF_CHECKED(sourceReader->SetStreamSelection((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, TRUE)); + UINT32 numChannels = 0; + MPT_MF_CHECKED(uncompressedAudioType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &numChannels)); + UINT32 samplesPerSecond = 0; + MPT_MF_CHECKED(uncompressedAudioType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &samplesPerSecond)); + UINT32 bitsPerSample = 0; + MPT_MF_CHECKED(uncompressedAudioType->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, &bitsPerSample)); + if(numChannels <= 0 || numChannels > 2) + { + return false; + } + if(samplesPerSecond <= 0) + { + return false; + } + if(bitsPerSample != 8 && bitsPerSample != 16 && bitsPerSample != 24 && bitsPerSample != 32) + { + return false; + } + + std::vector<char> rawData; + for(;;) + { + CComPtr<IMFSample> mfSample; + DWORD mfSampleFlags = 0; + CComPtr<IMFMediaBuffer> buffer; + MPT_MF_CHECKED(sourceReader->ReadSample((DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, 0, NULL, &mfSampleFlags, NULL, &mfSample)); + if(mfSampleFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED) + { + break; + } + if(mfSampleFlags & MF_SOURCE_READERF_ENDOFSTREAM) + { + break; + } + MPT_MF_CHECKED(mfSample->ConvertToContiguousBuffer(&buffer)); + { + BYTE *data = NULL; + DWORD dataSize = 0; + MPT_MF_CHECKED(buffer->Lock(&data, NULL, &dataSize)); + mpt::append(rawData, mpt::byte_cast<char*>(data), mpt::byte_cast<char*>(data + dataSize)); + MPT_MF_CHECKED(buffer->Unlock()); + if(rawData.size() / numChannels / (bitsPerSample / 8) > MAX_SAMPLE_LENGTH) + { + break; + } + } + } + + std::string sampleName = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags)); + + if(rawData.size() / numChannels / (bitsPerSample / 8) > MAX_SAMPLE_LENGTH) + { + return false; + } + + SmpLength length = mpt::saturate_cast<SmpLength>(rawData.size() / numChannels / (bitsPerSample/8)); + + DestroySampleThreadsafe(sample); + if(!mo3Decode) + { + m_szNames[sample] = sampleName; + Samples[sample].Initialize(); + Samples[sample].nC5Speed = samplesPerSecond; + } + Samples[sample].nLength = length; + Samples[sample].uFlags.set(CHN_16BIT, bitsPerSample >= 16); + Samples[sample].uFlags.set(CHN_STEREO, numChannels == 2); + Samples[sample].AllocateSample(); + if(!Samples[sample].HasSampleData()) + { + return false; + } + + if(bitsPerSample == 24) + { + if(numChannels == 2) + { + CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24> > >(Samples[sample], rawData.data(), rawData.size()); + } else + { + CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24> > >(Samples[sample], rawData.data(), rawData.size()); + } + } else if(bitsPerSample == 32) + { + if(numChannels == 2) + { + CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32> > >(Samples[sample], rawData.data(), rawData.size()); + } else + { + CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32> > >(Samples[sample], rawData.data(), rawData.size()); + } + } else + { + // just copy + std::copy(rawData.data(), rawData.data() + rawData.size(), mpt::byte_cast<char*>(Samples[sample].sampleb())); + } + + #undef MPT_MF_CHECKED + + if(!mo3Decode) + { + Samples[sample].Convert(MOD_TYPE_IT, GetType()); + Samples[sample].PrecomputeLoops(*this, false); + } + + return true; + +#endif + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatOpus.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatOpus.cpp new file mode 100644 index 00000000..9a6f3f92 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatOpus.cpp @@ -0,0 +1,201 @@ +/* + * SampleFormatOpus.cpp + * -------------------- + * Purpose: Opus sample import. + * Notes : + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif +#include "../common/misc_util.h" +#include "Tagging.h" +#include "Loaders.h" +#include "../common/FileReader.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/Copy.hpp" +#include "../soundlib/ModSampleCopy.h" +//#include "mpt/crc/crc.hpp" +#include "OggStream.h" +#ifdef MPT_WITH_OGG +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <ogg/ogg.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_OGG +#if defined(MPT_WITH_OPUSFILE) +#include <opusfile.h> +#endif // MPT_WITH_OPUSFILE + + +OPENMPT_NAMESPACE_BEGIN + + +//////////////////////////////////////////////////////////////////////////////// +// Opus + +#if defined(MPT_WITH_OPUSFILE) + +static mpt::ustring UStringFromOpus(const char *str) +{ + return str ? mpt::ToUnicode(mpt::Charset::UTF8, str) : mpt::ustring(); +} + +static FileTags GetOpusFileTags(OggOpusFile *of) +{ + FileTags tags; + const OpusTags *ot = op_tags(of, -1); + if(!ot) + { + return tags; + } + tags.encoder = UStringFromOpus(opus_tags_query(ot, "ENCODER", 0)); + tags.title = UStringFromOpus(opus_tags_query(ot, "TITLE", 0)); + tags.comments = UStringFromOpus(opus_tags_query(ot, "DESCRIPTION", 0)); + tags.bpm = UStringFromOpus(opus_tags_query(ot, "BPM", 0)); // non-standard + tags.artist = UStringFromOpus(opus_tags_query(ot, "ARTIST", 0)); + tags.album = UStringFromOpus(opus_tags_query(ot, "ALBUM", 0)); + tags.trackno = UStringFromOpus(opus_tags_query(ot, "TRACKNUMBER", 0)); + tags.year = UStringFromOpus(opus_tags_query(ot, "DATE", 0)); + tags.url = UStringFromOpus(opus_tags_query(ot, "CONTACT", 0)); + tags.genre = UStringFromOpus(opus_tags_query(ot, "GENRE", 0)); + return tags; +} + +#endif // MPT_WITH_OPUSFILE + +bool CSoundFile::ReadOpusSample(SAMPLEINDEX sample, FileReader &file) +{ + file.Rewind(); + +#if defined(MPT_WITH_OPUSFILE) + + int rate = 0; + int channels = 0; + std::vector<int16> raw_sample_data; + + std::string sampleName; + + FileReader initial = file.GetChunk(65536); // 512 is recommended by libopusfile + if(op_test(NULL, initial.GetRawData<unsigned char>().data(), initial.GetLength()) != 0) + { + return false; + } + + OggOpusFile *of = op_open_memory(file.GetRawData<unsigned char>().data(), file.GetLength(), NULL); + if(!of) + { + return false; + } + + rate = 48000; + channels = op_channel_count(of, -1); + if(rate <= 0 || channels <= 0) + { + op_free(of); + of = NULL; + return false; + } + if(channels > 2 || op_link_count(of) != 1) + { + // We downmix multichannel to stereo as recommended by Opus specification in + // case we are not able to handle > 2 channels. + // We also decode chained files as stereo even if they start with a mono + // stream, which simplifies handling of link boundaries for us. + channels = 2; + } + + sampleName = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(GetOpusFileTags(of))); + + if(auto length = op_pcm_total(of, 0); length != OP_EINVAL) + raw_sample_data.reserve(std::min(MAX_SAMPLE_LENGTH, mpt::saturate_cast<SmpLength>(length)) * channels); + + std::vector<int16> decodeBuf(120 * 48000 / 1000); // 120ms (max Opus packet), 48kHz + bool eof = false; + while(!eof) + { + int framesRead = 0; + if(channels == 2) + { + framesRead = op_read_stereo(of, &(decodeBuf[0]), static_cast<int>(decodeBuf.size())); + } else if(channels == 1) + { + framesRead = op_read(of, &(decodeBuf[0]), static_cast<int>(decodeBuf.size()), NULL); + } + if(framesRead > 0) + { + mpt::append(raw_sample_data, decodeBuf.begin(), decodeBuf.begin() + (framesRead * channels)); + } else if(framesRead == 0) + { + eof = true; + } else if(framesRead == OP_HOLE) + { + // continue + } else + { + // other errors are fatal, stop decoding + eof = true; + } + if((raw_sample_data.size() / channels) > MAX_SAMPLE_LENGTH) + { + break; + } + } + + op_free(of); + of = NULL; + + if(raw_sample_data.empty()) + { + return false; + } + + DestroySampleThreadsafe(sample); + ModSample &mptSample = Samples[sample]; + mptSample.Initialize(); + mptSample.nC5Speed = rate; + mptSample.nLength = std::min(MAX_SAMPLE_LENGTH, mpt::saturate_cast<SmpLength>(raw_sample_data.size() / channels)); + + mptSample.uFlags.set(CHN_16BIT); + mptSample.uFlags.set(CHN_STEREO, channels == 2); + + if(!mptSample.AllocateSample()) + { + return false; + } + + if(raw_sample_data.size() / channels > MAX_SAMPLE_LENGTH) + { + AddToLog(LogWarning, U_("Sample has been truncated!")); + } + + std::copy(raw_sample_data.begin(), raw_sample_data.begin() + mptSample.nLength * channels, mptSample.sample16()); + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + m_szNames[sample] = sampleName; + + return true; + +#else // !MPT_WITH_OPUSFILE + + MPT_UNREFERENCED_PARAMETER(sample); + MPT_UNREFERENCED_PARAMETER(file); + + return false; + +#endif // MPT_WITH_OPUSFILE + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatSFZ.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatSFZ.cpp new file mode 100644 index 00000000..a59b1931 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatSFZ.cpp @@ -0,0 +1,1270 @@ +/* + * SampleFormatSFZ.cpp + * ------------------- + * Purpose: Loading and saving SFZ instruments. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#endif // MODPLUG_TRACKER +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif // !MODPLUG_NO_FILESAVE +#include "modsmp_ctrl.h" +#include "mpt/base/numbers.hpp" + +#include <functional> + +OPENMPT_NAMESPACE_BEGIN + +#ifdef MPT_EXTERNAL_SAMPLES + +template<size_t N> +static bool SFZStartsWith(const std::string_view &l, const char(&r)[N]) +{ + return l.substr(0, N - 1) == r; +} + +template <size_t N> +static bool SFZEndsWith(const std::string_view &l, const char (&r)[N]) +{ + return l.size() >= (N - 1) && l.substr(l.size() - (N - 1), N - 1) == r; +} + +static bool SFZIsNumeric(const std::string_view &str) +{ + return std::find_if(str.begin(), str.end(), [](char c) { return c < '0' || c > '9'; }) == str.end(); +} + +struct SFZControl +{ + std::string defaultPath; + int8 octaveOffset = 0, noteOffset = 0; + + void Parse(const std::string_view key, const std::string &value) + { + if(key == "default_path") + defaultPath = value; + else if(key == "octave_offset") + octaveOffset = ConvertStrTo<int8>(value); + else if(key == "note_offset") + noteOffset = ConvertStrTo<int8>(value); + } +}; + +struct SFZFlexEG +{ + using PointIndex = decltype(InstrumentEnvelope().nLoopStart); + + std::vector<std::pair<double, double>> points; + double amplitude = 0; // percentage (100 = full volume range) + double pan = 0; // percentage (100 = full pan range) + double pitch = 0; // in cents + double cutoff = 0; // in cents + PointIndex sustain = 0; + + void Parse(std::string_view key, const std::string &value) + { + key = key.substr(key.find('_') + 1); + const double v = ConvertStrTo<double>(value); + + const bool isTime = SFZStartsWith(key, "time"), isLevel = SFZStartsWith(key, "level"); + std::string_view pointStr; + if(isTime) + pointStr = key.substr(4); + else if(isLevel) + pointStr = key.substr(5); + + if(!pointStr.empty() && SFZIsNumeric(pointStr)) + { + PointIndex point = ConvertStrTo<PointIndex>(std::string(pointStr)); + if(point >= points.size() && point < MAX_ENVPOINTS) + points.resize(point + 1); + + if(point < points.size()) + { + if(isTime) + points[point].first = v; + else + points[point].second = v; + } + return; + } + + if(key == "points") + points.resize(std::min(static_cast<PointIndex>(v), static_cast<PointIndex>(MAX_ENVPOINTS))); + else if(key == "sustain") + sustain = mpt::saturate_round<PointIndex>(v); + else if(key == "amplitude" || key == "ampeg") + amplitude = v; + else if(key == "pan") + pan = v; + else if(key == "pitch") + pitch = v; + else if(key == "cutoff") + cutoff = v; + } + + void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile) const + { + if(amplitude) + ConvertToMPT(ins, sndFile, ENV_VOLUME, amplitude / 100.0, 0.0, 1.0); + if(pan) + ConvertToMPT(ins, sndFile, ENV_PANNING, pan / 100.0, -1.0, 1.0); + if(pitch) + ConvertToMPT(ins, sndFile, ENV_PITCH, pitch / 1600.0, -1.0, 1.0); + if(cutoff) + ConvertToMPT(ins, sndFile, ENV_PITCH, cutoff, 0.0, 1.0, true); + } + + void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile, EnvelopeType envType, double scale, double minVal, double maxVal, bool forceFilter = false) const + { + const double tickDuration = sndFile.m_PlayState.m_nSamplesPerTick / static_cast<double>(sndFile.GetSampleRate()); + if(tickDuration <= 0 || points.empty() || scale == 0.0) + return; + + auto &env = ins->GetEnvelope(envType); + std::function<double(double)> conversionFunc = Identity; + if(forceFilter && envType == ENV_PITCH) + { + env.dwFlags.set(ENV_FILTER); + conversionFunc = FilterConversionFunc(*ins, sndFile); + } + + env.clear(); + env.reserve(points.size()); + + const auto ToValue = std::bind(SFZFlexEG::ToValue, std::placeholders::_1, scale, minVal, maxVal, conversionFunc); + + int32 prevTick = -1; + // If the first envelope point's time is greater than 0, we fade in from a neutral value + if(points.front().first > 0) + { + env.push_back({0, ToValue(0.0)}); + prevTick = 0; + } + + for(const auto &point : points) + { + const auto tick = mpt::saturate_cast<EnvelopeNode::tick_t>(prevTick + ToTicks(point.first, tickDuration)); + const auto value = ToValue(point.second); + env.push_back({tick, value}); + prevTick = tick; + if(tick == Util::MaxValueOfType(tick)) + break; + } + + if(sustain < env.size()) + { + env.nSustainStart = env.nSustainEnd = sustain; + env.dwFlags.set(ENV_SUSTAIN); + } else + { + env.dwFlags.reset(ENV_SUSTAIN); + } + env.dwFlags.set(ENV_ENABLED); + + if(envType == ENV_VOLUME && env.nSustainEnd > 0) + env.nReleaseNode = env.nSustainEnd; + } + +protected: + static EnvelopeNode::tick_t ToTicks(double duration, double tickDuration) + { + return std::max(EnvelopeNode::tick_t(1), mpt::saturate_round<EnvelopeNode::tick_t>(duration / tickDuration)); + } + + static EnvelopeNode::value_t ToValue(double value, double scale, double minVal, double maxVal, const std::function<double(double)> &conversionFunc) + { + value = conversionFunc((value * scale - minVal) / (maxVal - minVal)) * ENVELOPE_MAX + ENVELOPE_MIN; + Limit<double, double>(value, ENVELOPE_MIN, ENVELOPE_MAX); + return mpt::saturate_round<EnvelopeNode::value_t>(value); + } + + static double Identity(double v) noexcept { return v; } + + static double CentsToFilterCutoff(double v, const CSoundFile &sndFile, int envBaseCutoff, uint32 envBaseFreq) + { + const auto freq = envBaseFreq * std::pow(2.0, v / 1200.0); + return Util::muldivr(sndFile.FrequencyToCutOff(freq), 127, envBaseCutoff) / 127.0; + } + + static std::function<double(double)> FilterConversionFunc(const ModInstrument &ins, const CSoundFile &sndFile) + { + const auto envBaseCutoff = ins.IsCutoffEnabled() ? ins.GetCutoff() : 127; + const auto envBaseFreq = sndFile.CutOffToFrequency(envBaseCutoff); + return std::bind(CentsToFilterCutoff, std::placeholders::_1, std::cref(sndFile), envBaseCutoff, envBaseFreq); + } +}; + +struct SFZEnvelope +{ + double startLevel = 0, delay = 0, attack = 0, hold = 0; + double decay = 0, sustainLevel = 100, release = 0, depth = 0; + + void Parse(std::string_view key, const std::string &value) + { + key = key.substr(key.find('_') + 1); + double v = ConvertStrTo<double>(value); + if(key == "depth") + Limit(v, -12000.0, 12000.0); + else if(key == "start" || key == "sustain") + Limit(v, -100.0, 100.0); + else + Limit(v, 0.0, 100.0); + + if(key == "start") + startLevel = v; + else if(key == "delay") + delay = v; + else if(key == "attack") + attack = v; + else if(key == "hold") + hold = v; + else if(key == "decay") + decay = v; + else if(key == "sustain") + sustainLevel = v; + else if(key == "release") + release = v; + else if(key == "depth") + depth = v; + } + + void ConvertToMPT(ModInstrument *ins, const CSoundFile &sndFile, EnvelopeType envType, bool forceFilter = false) const + { + SFZFlexEG eg; + if(envType == ENV_VOLUME) + eg.amplitude = 1.0; + else if(envType == ENV_PITCH && !forceFilter) + eg.pitch = depth / 100.0; + else if(envType == ENV_PITCH && forceFilter) + eg.cutoff = depth / 100.0; + + auto &env = eg.points; + if(attack > 0 || delay > 0) + { + env.push_back({0.0, startLevel}); + if(delay > 0) + env.push_back({delay, env.back().second}); + env.push_back({attack, 100.0}); + } + if(hold > 0) + { + if(env.empty()) + env.push_back({0.0, 100.0}); + env.push_back({hold, env.back().second}); + } + if(env.empty()) + env.push_back({0.0, 100.0}); + if(env.back().second != sustainLevel) + env.push_back({decay, sustainLevel}); + if(sustainLevel != 0) + { + eg.sustain = static_cast<SFZFlexEG::PointIndex>(env.size() - 1); + env.push_back({release, 0.0}); + } else + { + eg.sustain = std::numeric_limits<SFZFlexEG::PointIndex>::max(); + } + + eg.ConvertToMPT(ins, sndFile); + } +}; + + +struct SFZRegion +{ + enum class LoopMode + { + kUnspecified, + kContinuous, + kOneShot, + kSustain, + kNoLoop + }; + + enum class LoopType + { + kUnspecified, + kForward, + kBackward, + kAlternate, + }; + + size_t filenameOffset = 0; + std::string filename, name; + SFZEnvelope ampEnv, pitchEnv, filterEnv; + std::vector<SFZFlexEG> flexEGs; + SmpLength loopStart = 0, loopEnd = 0; + SmpLength end = MAX_SAMPLE_LENGTH, offset = 0; + LoopMode loopMode = LoopMode::kUnspecified; + LoopType loopType = LoopType::kUnspecified; + double loopCrossfade = 0.0; + double cutoff = 0; // in Hz + double resonance = 0; // 0...40dB + double filterRandom = 0; // 0...9600 cents + double volume = 0; // -144dB...+6dB + double amplitude = 100.0; // 0...100 + double pitchBend = 200; // -9600...9600 cents + double pitchLfoFade = 0; // 0...100 seconds + double pitchLfoDepth = 0; // -1200...12000 + double pitchLfoFreq = 0; // 0...20 Hz + double panning = -128; // -100...+100 + double finetune = 0; // in cents + int8 transpose = 0; + uint8 keyLo = 0, keyHi = 127, keyRoot = 60; + FilterMode filterType = FilterMode::Unchanged; + uint8 polyphony = 255; + bool useSampleKeyRoot = false; + bool invertPhase = false; + + template<typename T, typename Tc> + static void Read(const std::string &valueStr, T &value, Tc valueMin = std::numeric_limits<T>::min(), Tc valueMax = std::numeric_limits<T>::max()) + { + double valueF = ConvertStrTo<double>(valueStr); + if constexpr(std::numeric_limits<T>::is_integer) + { + valueF = mpt::round(valueF); + } + Limit(valueF, static_cast<double>(valueMin), static_cast<double>(valueMax)); + value = static_cast<T>(valueF); + } + + static uint8 ReadKey(const std::string &value, const SFZControl &control) + { + if(value.empty()) + return 0; + + int key = 0; + if(value[0] >= '0' && value[0] <= '9') + { + // MIDI key + key = ConvertStrTo<uint8>(value); + } else if(value.length() < 2) + { + return 0; + } else + { + // Scientific pitch + static constexpr int8 keys[] = { 9, 11, 0, 2, 4, 5, 7 }; + static_assert(std::size(keys) == 'g' - 'a' + 1); + auto keyC = value[0]; + if(keyC >= 'A' && keyC <= 'G') + key = keys[keyC - 'A']; + if(keyC >= 'a' && keyC <= 'g') + key = keys[keyC - 'a']; + else + return 0; + + uint8 octaveOffset = 1; + if(value[1] == '#') + { + key++; + octaveOffset = 2; + } else if(value[1] == 'b' || value[1] == 'B') + { + key--; + octaveOffset = 2; + } + if(octaveOffset >= value.length()) + return 0; + + int8 octave = ConvertStrTo<int8>(value.c_str() + octaveOffset); + key += (octave + 1) * 12; + } + key += control.octaveOffset * 12 + control.noteOffset; + return static_cast<uint8>(Clamp(key, 0, 127)); + } + + void Parse(const std::string_view key, const std::string &value, const SFZControl &control) + { + if(key == "sample") + { + filename = control.defaultPath + value; + filenameOffset = control.defaultPath.size(); + } + else if(key == "region_label") + name = value; + else if(key == "lokey") + keyLo = ReadKey(value, control); + else if(key == "hikey") + keyHi = ReadKey(value, control); + else if(key == "pitch_keycenter") + { + keyRoot = ReadKey(value, control); + useSampleKeyRoot = (value == "sample"); + } + else if(key == "key") + { + keyLo = keyHi = keyRoot = ReadKey(value, control); + useSampleKeyRoot = false; + } + else if(key == "bend_up" || key == "bendup") + Read(value, pitchBend, -9600.0, 9600.0); + else if(key == "pitchlfo_fade") + Read(value, pitchLfoFade, 0.0, 100.0); + else if(key == "pitchlfo_depth") + Read(value, pitchLfoDepth, -12000.0, 12000.0); + else if(key == "pitchlfo_freq") + Read(value, pitchLfoFreq, 0.0, 20.0); + else if(key == "volume") + Read(value, volume, -144.0, 6.0); + else if(key == "amplitude") + Read(value, amplitude, 0.0, 100.0); + else if(key == "pan") + Read(value, panning, -100.0, 100.0); + else if(key == "transpose") + Read(value, transpose, -127, 127); + else if(key == "tune") + Read(value, finetune, -100.0, 100.0); + else if(key == "end") + Read(value, end, SmpLength(0), MAX_SAMPLE_LENGTH); + else if(key == "offset") + Read(value, offset, SmpLength(0), MAX_SAMPLE_LENGTH); + else if(key == "loop_start" || key == "loopstart") + Read(value, loopStart, SmpLength(0), MAX_SAMPLE_LENGTH); + else if(key == "loop_end" || key == "loopend") + Read(value, loopEnd, SmpLength(0), MAX_SAMPLE_LENGTH); + else if(key == "loop_crossfade" || key == "loopcrossfade") + Read(value, loopCrossfade, 0.0, DBL_MAX); + else if(key == "loop_mode" || key == "loopmode") + { + if(value == "loop_continuous") + loopMode = LoopMode::kContinuous; + else if(value == "one_shot") + loopMode = LoopMode::kOneShot; + else if(value == "loop_sustain") + loopMode = LoopMode::kSustain; + else if(value == "no_loop") + loopMode = LoopMode::kNoLoop; + } + else if(key == "loop_type" || key == "looptype") + { + if(value == "forward") + loopType = LoopType::kForward; + else if(value == "backward") + loopType = LoopType::kBackward; + else if(value == "alternate") + loopType = LoopType::kAlternate; + } + else if(key == "cutoff") + Read(value, cutoff, 0.0, 96000.0); + else if(key == "fil_random") + Read(value, filterRandom, 0.0, 9600.0); + else if(key == "resonance") + Read(value, resonance, 0.0, 40.0); + else if(key == "polyphony") + Read(value, polyphony, 0, 255); + else if(key == "phase") + invertPhase = (value == "invert"); + else if(key == "fil_type" || key == "filtype") + { + if(value == "lpf_1p" || value == "lpf_2p" || value == "lpf_4p" || value == "lpf_6p") + filterType = FilterMode::LowPass; + else if(value == "hpf_1p" || value == "hpf_2p" || value == "hpf_4p" || value == "hpf_6p") + filterType = FilterMode::HighPass; + // Alternatives: bpf_2p, brf_2p + } + else if(SFZStartsWith(key, "ampeg_")) + ampEnv.Parse(key, value); + else if(SFZStartsWith(key, "fileg_")) + filterEnv.Parse(key, value); + else if(SFZStartsWith(key, "pitcheg_")) + pitchEnv.Parse(key, value); + else if(SFZStartsWith(key, "eg") && SFZIsNumeric(key.substr(2, 2)) && key.substr(4, 1) == "_") + { + uint8 eg = ConvertStrTo<uint8>(std::string(key.substr(2, 2))); + if(eg >= flexEGs.size()) + flexEGs.resize(eg + 1); + flexEGs[eg].Parse(key, value); + } + } +}; + +struct SFZInputFile +{ + FileReader file; + std::unique_ptr<InputFile> inputFile; // FileReader has pointers into this so its address must not change + std::string remain; + + SFZInputFile(FileReader f = {}, std::unique_ptr<InputFile> i = {}, std::string r = {}) + : file{std::move(f)}, inputFile{std::move(i)}, remain{std::move(r)} {} + SFZInputFile(SFZInputFile &&) = default; +}; + +bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX nInstr, FileReader &file) +{ + file.Rewind(); + + enum { kNone, kGlobal, kMaster, kGroup, kRegion, kControl, kCurve, kEffect, kUnknown } section = kNone; + bool inMultiLineComment = false; + SFZControl control; + SFZRegion group, master, globals; + std::vector<SFZRegion> regions; + std::map<std::string, std::string> macros; + std::vector<SFZInputFile> files; + files.emplace_back(file); + + std::string s; + while(!files.empty()) + { + if(!files.back().file.ReadLine(s, 1024)) + { + // Finished reading file, so back to remaining characters of the #include line from the previous file + s = std::move(files.back().remain); + files.pop_back(); + } + + if(inMultiLineComment) + { + if(auto commentEnd = s.find("*/"); commentEnd != std::string::npos) + { + s.erase(0, commentEnd + 2); + inMultiLineComment = false; + } else + { + continue; + } + } + + // First, terminate line at the start of a comment block + if(auto commentPos = s.find("//"); commentPos != std::string::npos) + { + s.resize(commentPos); + } + + // Now, read the tokens. + // This format is so funky that no general tokenizer approach seems to work here... + // Consider this jolly good example found at https://stackoverflow.com/questions/5923895/tokenizing-a-custom-text-file-format-file-using-c-sharp + // <region>sample=piano C3.wav key=48 ampeg_release=0.7 // a comment here + // <region>key = 49 sample = piano Db3.wav + // <region> + // group=1 + // key = 48 + // sample = piano D3.ogg + // The original sfz specification claims that spaces around = are not allowed, but a quick look into the real world tells us otherwise. + + while(!s.empty()) + { + s.erase(0, s.find_first_not_of(" \t")); + + const bool isDefine = SFZStartsWith(s, "#define ") || SFZStartsWith(s, "#define\t"); + + // Replace macros (unless this is a #define statement, to allow for macro re-definition) + if(!isDefine) + { + for(const auto &[oldStr, newStr] : macros) + { + std::string::size_type pos = 0; + while((pos = s.find(oldStr, pos)) != std::string::npos) + { + s.replace(pos, oldStr.length(), newStr); + pos += newStr.length(); + } + } + } + + if(s.empty()) + break; + + std::string::size_type charsRead = 0; + + if(s[0] == '<' && (charsRead = s.find('>')) != std::string::npos) + { + // Section header + const auto sec = std::string_view(s).substr(1, charsRead - 1); + section = kUnknown; + if(sec == "global") + { + section = kGlobal; + // Reset global parameters + globals = SFZRegion(); + } else if(sec == "master") + { + section = kMaster; + // Reset master parameters + master = globals; + } else if(sec == "group") + { + section = kGroup; + // Reset group parameters + group = master; + } else if(sec == "region") + { + section = kRegion; + regions.push_back(group); + } else if(sec == "control") + { + section = kControl; + } else if(sec == "curve") + { + section = kCurve; + } else if(sec == "effect") + { + section = kEffect; + } + charsRead++; + } else if(isDefine) + { + // Macro definition + charsRead += 8; + auto keyStart = s.find_first_not_of(" \t", 8); + auto keyEnd = s.find_first_of(" \t", keyStart); + auto valueStart = s.find_first_not_of(" \t", keyEnd); + if(keyStart != std::string::npos && valueStart != std::string::npos) + { + charsRead = s.find_first_of(" \t", valueStart); + const auto key = s.substr(keyStart, keyEnd - keyStart); + if(key.length() > 1 && key[0] == '$') + macros[std::move(key)] = s.substr(valueStart, charsRead - valueStart); + } else + { + break; + } + } else if(SFZStartsWith(s, "#include ") || SFZStartsWith(s, "#include\t")) + { + // Include other sfz file + auto fileStart = s.find("\"", 9); // Yes, there can be arbitrary characters before the opening quote, at least that's how sforzando does it. + auto fileEnd = s.find("\"", fileStart + 1); + if(fileStart != std::string::npos && fileEnd != std::string::npos) + { + charsRead = fileEnd + 1; + fileStart++; + } else + { + break; + } + + std::string filenameU8 = s.substr(fileStart, fileEnd - fileStart); + mpt::PathString filename = mpt::PathString::FromUTF8(filenameU8); + if(!filename.empty()) + { + if(filenameU8.find(':') == std::string::npos) + filename = file.GetOptionalFileName().value_or(P_("")).GetPath() + filename; + filename = filename.Simplify(); + // Avoid recursive #include + if(std::find_if(files.begin(), files.end(), [&filename](const SFZInputFile &f) { return f.file.GetOptionalFileName().value_or(P_("")) == filename; }) == files.end()) + { + auto f = std::make_unique<InputFile>(filename); + if(f->IsValid()) + { + s.erase(0, charsRead); + files.emplace_back(GetFileReader(*f), std::move(f), std::move(s)); + break; + } else + { + AddToLog(LogWarning, U_("Unable to load include file: ") + filename.ToUnicode()); + } + } else + { + AddToLog(LogWarning, U_("Recursive include file ignored: ") + filename.ToUnicode()); + } + } + } else if(SFZStartsWith(s, "/*")) + { + // Multi-line comment + if(auto commentEnd = s.find("*/", charsRead + 2); commentEnd != std::string::npos) + { + charsRead = commentEnd; + } else + { + inMultiLineComment = true; + charsRead = s.length(); + } + } else if(section == kNone) + { + // Garbage before any section, probably not an sfz file + return false; + } else if(s.find('=') != std::string::npos) + { + // Read key=value pair + auto keyEnd = s.find_first_of(" \t="); + auto valueStart = s.find_first_not_of(" \t=", keyEnd); + if(valueStart == std::string::npos) + { + break; + } + const std::string key = mpt::ToLowerCaseAscii(s.substr(0, keyEnd)); + // Currently defined *_label opcodes are global_label, group_label, master_label, region_label, sw_label + if(key == "sample" || key == "default_path" || SFZStartsWith(key, "label_cc") || SFZStartsWith(key, "label_key") || SFZEndsWith(key, "_label")) + { + // Sample / CC name may contain spaces... + charsRead = s.find_first_of("=\t<", valueStart); + if(charsRead != std::string::npos && s[charsRead] == '=') + { + // Backtrack to end of key + while(charsRead > valueStart && s[charsRead] == ' ') + charsRead--; + // Backtrack to start of key + while(charsRead > valueStart && s[charsRead] != ' ') + charsRead--; + } + } else + { + charsRead = s.find_first_of(" \t<", valueStart); + } + const std::string value = s.substr(valueStart, charsRead - valueStart); + + switch(section) + { + case kGlobal: + globals.Parse(key, value, control); + [[fallthrough]]; + case kMaster: + master.Parse(key, value, control); + [[fallthrough]]; + case kGroup: + group.Parse(key, value, control); + break; + case kRegion: + regions.back().Parse(key, value, control); + break; + case kControl: + control.Parse(key, value); + break; + } + } else + { + // Garbage, probably not an sfz file + return false; + } + + // Remove the token(s) we just read + s.erase(0, charsRead); + } + } + + if(regions.empty()) + return false; + + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + return false; + + RecalculateSamplesPerTick(); + DestroyInstrument(nInstr, deleteAssociatedSamples); + if(nInstr > m_nInstruments) m_nInstruments = nInstr; + Instruments[nInstr] = pIns; + + SAMPLEINDEX prevSmp = 0; + for(auto ®ion : regions) + { + uint8 keyLo = region.keyLo, keyHi = region.keyHi; + if(keyLo > keyHi) + continue; + Clamp<uint8, uint8>(keyLo, 0, NOTE_MAX - NOTE_MIN); + Clamp<uint8, uint8>(keyHi, 0, NOTE_MAX - NOTE_MIN); + SAMPLEINDEX smp = GetNextFreeSample(nInstr, prevSmp + 1); + if(smp == SAMPLEINDEX_INVALID) + break; + prevSmp = smp; + + ModSample &sample = Samples[smp]; + sample.Initialize(MOD_TYPE_MPT); + if(const auto synthSample = std::string_view(region.filename).substr(region.filenameOffset); SFZStartsWith(synthSample, "*")) + { + sample.nLength = 256; + sample.nC5Speed = mpt::saturate_round<uint32>(sample.nLength * 261.6255653); + sample.uFlags.set(CHN_16BIT); + std::function<uint16(int32)> generator; + if(synthSample == "*sine") + generator = [](int32 i) { return mpt::saturate_round<int16>(std::sin(i * ((2.0 * mpt::numbers::pi) / 256.0)) * int16_max); }; + else if(synthSample == "*square") + generator = [](int32 i) { return i < 128 ? int16_max : int16_min; }; + else if(synthSample == "*triangle" || synthSample == "*tri") + generator = [](int32 i) { return static_cast<int16>(i < 128 ? ((63 - i) * 512) : ((i - 192) * 512)); }; + else if(synthSample == "*saw") + generator = [](int32 i) { return static_cast<int16>((i - 128) * 256); }; + else if(synthSample == "*silence") + generator = [](int32) { return int16(0); }; + else if(synthSample == "*noise") + { + sample.nLength = sample.nC5Speed; + generator = [this](int32) { return mpt::random<int16>(AccessPRNG()); }; + } else + { + AddToLog(LogWarning, U_("Unknown sample type: ") + mpt::ToUnicode(mpt::Charset::UTF8, std::string(synthSample))); + prevSmp--; + continue; + } + if(sample.AllocateSample()) + { + for(SmpLength i = 0; i < sample.nLength; i++) + { + sample.sample16()[i] = generator(static_cast<int32>(i)); + } + if(smp > m_nSamples) + m_nSamples = smp; + region.offset = 0; + region.loopMode = SFZRegion::LoopMode::kContinuous; + region.loopStart = 0; + region.loopEnd = sample.nLength - 1; + region.loopCrossfade = 0; + region.keyRoot = 60; + } + } else if(auto filename = mpt::PathString::FromUTF8(region.filename); !filename.empty()) + { + if(region.filename.find(':') == std::string::npos) + { + filename = file.GetOptionalFileName().value_or(P_("")).GetPath() + filename; + } + filename = filename.Simplify(); + SetSamplePath(smp, filename); + InputFile f(filename, SettingCacheCompleteFileBeforeLoading()); + FileReader smpFile = GetFileReader(f); + if(!ReadSampleFromFile(smp, smpFile, false)) + { + AddToLog(LogWarning, U_("Unable to load sample: ") + filename.ToUnicode()); + prevSmp--; + continue; + } + + if(UseFinetuneAndTranspose()) + sample.TransposeToFrequency(); + + sample.uFlags.set(SMP_KEEPONDISK, sample.HasSampleData()); + } + + if(!region.name.empty()) + m_szNames[smp] = mpt::ToCharset(GetCharsetInternal(), mpt::Charset::UTF8, region.name); + if(!m_szNames[smp][0]) + m_szNames[smp] = mpt::ToCharset(GetCharsetInternal(), mpt::PathString::FromUTF8(region.filename).GetFileName().ToUnicode()); + + if(region.useSampleKeyRoot) + { + if(sample.rootNote != NOTE_NONE) + region.keyRoot = sample.rootNote - NOTE_MIN; + else + region.keyRoot = 60; + } + + const auto origSampleRate = sample.GetSampleRate(GetType()); + int8 transp = region.transpose + (60 - region.keyRoot); + for(uint8 i = keyLo; i <= keyHi; i++) + { + pIns->Keyboard[i] = smp; + if(GetType() != MOD_TYPE_XM) + pIns->NoteMap[i] = NOTE_MIN + i + transp; + } + if(GetType() == MOD_TYPE_XM) + sample.Transpose(transp / 12.0); + + pIns->filterMode = region.filterType; + if(region.cutoff != 0) + pIns->SetCutoff(FrequencyToCutOff(region.cutoff), true); + if(region.resonance != 0) + pIns->SetResonance(mpt::saturate_round<uint8>(region.resonance * 128.0 / 24.0), true); + pIns->nCutSwing = mpt::saturate_round<uint8>(region.filterRandom * (m_SongFlags[SONG_EXFILTERRANGE] ? 20 : 24) / 1200.0); + pIns->midiPWD = mpt::saturate_round<int8>(region.pitchBend / 100.0); + + pIns->nNNA = NewNoteAction::NoteOff; + if(region.polyphony == 1) + { + pIns->nDNA = DuplicateNoteAction::NoteCut; + pIns->nDCT = DuplicateCheckType::Sample; + } + region.ampEnv.ConvertToMPT(pIns, *this, ENV_VOLUME); + if(region.pitchEnv.depth) + region.pitchEnv.ConvertToMPT(pIns, *this, ENV_PITCH); + else if(region.filterEnv.depth) + region.filterEnv.ConvertToMPT(pIns, *this, ENV_PITCH, true); + + for(const auto &flexEG : region.flexEGs) + { + flexEG.ConvertToMPT(pIns, *this); + } + + if(region.ampEnv.release > 0) + { + const double tickDuration = m_PlayState.m_nSamplesPerTick / static_cast<double>(GetSampleRate()); + pIns->nFadeOut = std::min(mpt::saturate_cast<uint32>(32768.0 * tickDuration / region.ampEnv.release), uint32(32767)); + if(GetType() == MOD_TYPE_IT) + pIns->nFadeOut = std::min((pIns->nFadeOut + 16u) & ~31u, uint32(8192)); + } + + sample.rootNote = region.keyRoot + NOTE_MIN; + sample.nGlobalVol = mpt::saturate_round<decltype(sample.nGlobalVol)>(64.0 * Clamp(std::pow(10.0, region.volume / 20.0) * region.amplitude / 100.0, 0.0, 1.0)); + if(region.panning != -128) + { + sample.nPan = mpt::saturate_round<decltype(sample.nPan)>((region.panning + 100) * 256.0 / 200.0); + sample.uFlags.set(CHN_PANNING); + } + sample.Transpose(region.finetune / 1200.0); + + if(region.pitchLfoDepth && region.pitchLfoFreq) + { + sample.nVibSweep = 255; + if(region.pitchLfoFade > 0) + sample.nVibSweep = mpt::saturate_round<uint8>(255.0 / region.pitchLfoFade); + sample.nVibDepth = mpt::saturate_round<uint8>(region.pitchLfoDepth * 32.0 / 100.0); + sample.nVibRate = mpt::saturate_round<uint8>(region.pitchLfoFreq * 4.0); + } + + if(region.loopMode != SFZRegion::LoopMode::kUnspecified) + { + switch(region.loopMode) + { + case SFZRegion::LoopMode::kContinuous: + sample.uFlags.set(CHN_LOOP); + break; + case SFZRegion::LoopMode::kSustain: + sample.uFlags.set(CHN_SUSTAINLOOP); + break; + case SFZRegion::LoopMode::kNoLoop: + case SFZRegion::LoopMode::kOneShot: + sample.uFlags.reset(CHN_LOOP | CHN_SUSTAINLOOP); + } + } + if(region.loopEnd > region.loopStart) + { + // Loop may also be defined in file, in which case loopStart and loopEnd are unset. + if(region.loopMode == SFZRegion::LoopMode::kSustain) + { + sample.nSustainStart = region.loopStart; + sample.nSustainEnd = region.loopEnd + 1; + } else if(region.loopMode == SFZRegion::LoopMode::kContinuous || region.loopMode == SFZRegion::LoopMode::kOneShot) + { + sample.nLoopStart = region.loopStart; + sample.nLoopEnd = region.loopEnd + 1; + } + } else if(sample.nLoopEnd <= sample.nLoopStart && region.loopMode != SFZRegion::LoopMode::kUnspecified && region.loopMode != SFZRegion::LoopMode::kNoLoop) + { + sample.nLoopEnd = sample.nLength; + } + switch(region.loopType) + { + case SFZRegion::LoopType::kUnspecified: + break; + case SFZRegion::LoopType::kForward: + sample.uFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN | CHN_REVERSE); + break; + case SFZRegion::LoopType::kBackward: + sample.uFlags.set(CHN_REVERSE); + break; + case SFZRegion::LoopType::kAlternate: + sample.uFlags.set(CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN); + break; + default: + break; + } + if(sample.nSustainEnd <= sample.nSustainStart && sample.nLoopEnd > sample.nLoopStart && region.loopMode == SFZRegion::LoopMode::kSustain) + { + // Turn normal loop (imported from sample) into sustain loop + std::swap(sample.nSustainStart, sample.nLoopStart); + std::swap(sample.nSustainEnd, sample.nLoopEnd); + sample.uFlags.set(CHN_SUSTAINLOOP); + sample.uFlags.set(CHN_PINGPONGSUSTAIN, sample.uFlags[CHN_PINGPONGLOOP]); + sample.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP); + } + + mpt::PathString filenameModifier; + + // Loop cross-fade + SmpLength fadeSamples = mpt::saturate_round<SmpLength>(region.loopCrossfade * origSampleRate); + LimitMax(fadeSamples, sample.uFlags[CHN_SUSTAINLOOP] ? sample.nSustainStart : sample.nLoopStart); + if(fadeSamples > 0) + { + ctrlSmp::XFadeSample(sample, fadeSamples, 50000, true, sample.uFlags[CHN_SUSTAINLOOP], *this); + sample.uFlags.set(SMP_MODIFIED); + filenameModifier += P_(" (cross-fade)"); + } + + // Sample offset + if(region.offset && region.offset < sample.nLength) + { + auto offset = region.offset * sample.GetBytesPerSample(); + memmove(sample.sampleb(), sample.sampleb() + offset, sample.nLength * sample.GetBytesPerSample() - offset); + if(region.end > region.offset) + region.end -= region.offset; + sample.nLength -= region.offset; + sample.nLoopStart -= region.offset; + sample.nLoopEnd -= region.offset; + sample.uFlags.set(SMP_MODIFIED); + filenameModifier += P_(" (offset)"); + } + LimitMax(sample.nLength, region.end); + + if(region.invertPhase) + { + ctrlSmp::InvertSample(sample, 0, sample.nLength, *this); + sample.uFlags.set(SMP_MODIFIED); + filenameModifier += P_(" (inverted)"); + } + + if(sample.uFlags.test_all(SMP_KEEPONDISK | SMP_MODIFIED)) + { + // Avoid ruining the original samples + if(auto filename = GetSamplePath(smp); !filename.empty()) + { + filename = filename.GetPath() + filename.GetFileName() + filenameModifier + filename.GetFileExt(); + SetSamplePath(smp, filename); + } + } + + sample.PrecomputeLoops(*this, false); + sample.Convert(MOD_TYPE_MPT, GetType()); + } + + pIns->Sanitize(MOD_TYPE_MPT); + pIns->Convert(MOD_TYPE_MPT, GetType()); + return true; +} + +#ifndef MODPLUG_NO_FILESAVE + +static double SFZLinear2dB(double volume) +{ + return (volume > 0.0 ? 20.0 * std::log10(volume) : -144.0); +} + +static void WriteSFZEnvelope(std::ostream &f, double tickDuration, int index, const InstrumentEnvelope &env, const char *type, double scale, std::function<double(int32)> convFunc) +{ + if(!env.dwFlags[ENV_ENABLED] || env.empty()) + return; + + const bool sustainAtEnd = (!env.dwFlags[ENV_SUSTAIN] || env.nSustainStart == (env.size() - 1)) && convFunc(env.back().value) != 0.0; + + const auto prefix = MPT_AFORMAT("\neg{}_")(mpt::afmt::dec0<2>(index)); + f << "\n" << prefix << type << "=" << scale; + f << prefix << "points=" << (env.size() + (sustainAtEnd ? 1 : 0)); + EnvelopeNode::tick_t lastTick = 0; + int nodeIndex = 0; + for(const auto &node : env) + { + const double time = (node.tick - lastTick) * tickDuration; + lastTick = node.tick; + f << prefix << "time" << nodeIndex << "=" << time; + f << prefix << "level" << nodeIndex << "=" << convFunc(node.value); + nodeIndex++; + } + if(sustainAtEnd) + { + // Prevent envelope from going back to neutral + f << prefix << "time" << nodeIndex << "=0"; + f << prefix << "level" << nodeIndex << "=" << convFunc(env.back().value); + } + // We always must write a sustain point, or the envelope will be sustained on the first point of the envelope + f << prefix << "sustain=" << (env.dwFlags[ENV_SUSTAIN] ? env.nSustainStart : (env.size() - 1)); + + if(env.dwFlags[ENV_LOOP]) + f << "\n// Loop: " << static_cast<uint32>(env.nLoopStart) << "-" << static_cast<uint32>(env.nLoopEnd); + if(env.dwFlags[ENV_SUSTAIN] && env.nSustainEnd > env.nSustainStart) + f << "\n// Sustain Loop: " << static_cast<uint32>(env.nSustainStart) << "-" << static_cast<uint32>(env.nSustainEnd); + if(env.nReleaseNode != ENV_RELEASE_NODE_UNSET) + f << "\n// Release Node: " << static_cast<uint32>(env.nReleaseNode); +} + +bool CSoundFile::SaveSFZInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool useFLACsamples) const +{ +#ifdef MODPLUG_TRACKER + const mpt::FlushMode flushMode = mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave); +#else + const mpt::FlushMode flushMode = mpt::FlushMode::Full; +#endif + const ModInstrument *ins = Instruments[nInstr]; + if(ins == nullptr) + return false; + + // Creating directory names with trailing spaces or dots is a bad idea, as they are difficult to remove in Windows. + const mpt::RawPathString whitespaceDirName = PL_(" \n\r\t."); + const mpt::PathString sampleBaseName = mpt::PathString::FromNative(mpt::trim(filename.GetFileName().AsNative(), whitespaceDirName)); + const mpt::PathString sampleDirName = (sampleBaseName.empty() ? P_("Samples") : sampleBaseName) + P_("/"); + const mpt::PathString sampleBasePath = filename.GetPath() + sampleDirName; + if(!sampleBasePath.IsDirectory() && !::CreateDirectory(sampleBasePath.AsNative().c_str(), nullptr)) + return false; + + const double tickDuration = m_PlayState.m_nSamplesPerTick / static_cast<double>(m_MixerSettings.gdwMixingFreq); + + f << std::setprecision(10); + if(!ins->name.empty()) + { + f << "// Name: " << mpt::ToCharset(mpt::Charset::UTF8, GetCharsetInternal(), ins->name) << "\n"; + } + f << "// Created with " << mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()) << "\n"; + f << "// Envelope tempo base: tempo " << m_PlayState.m_nMusicTempo.ToDouble(); + switch(m_nTempoMode) + { + case TempoMode::Classic: + f << " (classic tempo mode)"; + break; + case TempoMode::Alternative: + f << " (alternative tempo mode)"; + break; + case TempoMode::Modern: + f << ", " << m_PlayState.m_nMusicSpeed << " ticks per row, " << m_PlayState.m_nCurrentRowsPerBeat << " rows per beat (modern tempo mode)"; + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + + f << "\n\n<control>\ndefault_path=" << sampleDirName.ToUTF8() << "\n\n"; + f << "<group>"; + f << "\nbend_up=" << ins->midiPWD * 100; + f << "\nbend_down=" << -ins->midiPWD * 100; + const uint32 cutoff = ins->IsCutoffEnabled() ? ins->GetCutoff() : 127; + // If filter envelope is active but cutoff is not set, we still need to set the base cutoff frequency to be modulated by the envelope. + if(ins->IsCutoffEnabled() || ins->PitchEnv.dwFlags[ENV_FILTER]) + f << "\ncutoff=" << CSoundFile::CutOffToFrequency(cutoff) << " // " << cutoff; + if(ins->IsResonanceEnabled()) + f << "\nresonance=" << Util::muldivr_unsigned(ins->GetResonance(), 24, 128) << " // " << static_cast<int>(ins->GetResonance()); + if(ins->IsCutoffEnabled() || ins->IsResonanceEnabled()) + f << "\nfil_type=" << (ins->filterMode == FilterMode::HighPass ? "hpf_2p" : "lpf_2p"); + if(ins->dwFlags[INS_SETPANNING]) + f << "\npan=" << (Util::muldivr_unsigned(ins->nPan, 200, 256) - 100) << " // " << ins->nPan; + if(ins->nGlobalVol != 64) + f << "\nvolume=" << SFZLinear2dB(ins->nGlobalVol / 64.0) << " // " << ins->nGlobalVol; + if(ins->nFadeOut) + { + f << "\nampeg_release=" << (32768.0 * tickDuration / ins->nFadeOut) << " // " << ins->nFadeOut; + f << "\nampeg_release_shape=0"; + } + + if(ins->nDNA == DuplicateNoteAction::NoteCut && ins->nDCT != DuplicateCheckType::None) + f << "\npolyphony=1"; + + WriteSFZEnvelope(f, tickDuration, 1, ins->VolEnv, "amplitude", 100.0, [](int32 val) { return val / static_cast<double>(ENVELOPE_MAX); }); + WriteSFZEnvelope(f, tickDuration, 2, ins->PanEnv, "pan", 100.0, [](int32 val) { return 2.0 * (val - ENVELOPE_MID) / (ENVELOPE_MAX - ENVELOPE_MIN); }); + if(ins->PitchEnv.dwFlags[ENV_FILTER]) + { + const auto envScale = 1200.0 * std::log(CutOffToFrequency(127, 256) / static_cast<double>(CutOffToFrequency(0, -256))) / mpt::numbers::ln2; + const auto cutoffNormal = CutOffToFrequency(cutoff); + WriteSFZEnvelope(f, tickDuration, 3, ins->PitchEnv, "cutoff", envScale, [this, cutoff, cutoffNormal, envScale](int32 val) { + // Convert interval between center frequency and envelope into cents + const auto freq = CutOffToFrequency(cutoff, (val - ENVELOPE_MID) * 256 / (ENVELOPE_MAX - ENVELOPE_MID)); + return 1200.0 * std::log(freq / static_cast<double>(cutoffNormal)) / mpt::numbers::ln2 / envScale; + }); + } else + { + WriteSFZEnvelope(f, tickDuration, 3, ins->PitchEnv, "pitch", 1600.0, [](int32 val) { return 2.0 * (val - ENVELOPE_MID) / (ENVELOPE_MAX - ENVELOPE_MIN); }); + } + + size_t numSamples = 0; + for(size_t i = 0; i < std::size(ins->Keyboard); i++) + { + if(ins->Keyboard[i] < 1 || ins->Keyboard[i] > GetNumSamples()) + continue; + + size_t endOfRegion = i + 1; + while(endOfRegion < std::size(ins->Keyboard)) + { + if(ins->Keyboard[endOfRegion] != ins->Keyboard[i] || ins->NoteMap[endOfRegion] != (ins->NoteMap[i] + endOfRegion - i)) + break; + endOfRegion++; + } + endOfRegion--; + + const ModSample &sample = Samples[ins->Keyboard[i]]; + const bool isAdlib = sample.uFlags[CHN_ADLIB]; + + if(!sample.HasSampleData()) + { + i = endOfRegion; + continue; + } + + numSamples++; + mpt::PathString sampleName = sampleBasePath + (sampleBaseName.empty() ? P_("Sample") : sampleBaseName) + P_(" ") + mpt::PathString::FromUnicode(mpt::ufmt::val(numSamples)); + if(isAdlib) + sampleName += P_(".s3i"); + else if(useFLACsamples) + sampleName += P_(".flac"); + else + sampleName += P_(".wav"); + + bool success = false; + try + { + mpt::SafeOutputFile sfSmp(sampleName, std::ios::binary, flushMode); + if(sfSmp) + { + mpt::ofstream &fSmp = sfSmp; + fSmp.exceptions(fSmp.exceptions() | std::ios::badbit | std::ios::failbit); + + if(isAdlib) + success = SaveS3ISample(ins->Keyboard[i], fSmp); + else if(useFLACsamples) + success = SaveFLACSample(ins->Keyboard[i], fSmp); + else + success = SaveWAVSample(ins->Keyboard[i], fSmp); + } + } catch(const std::exception &) + { + success = false; + } + if(!success) + { + AddToLog(LogError, MPT_USTRING("Unable to save sample: ") + sampleName.ToUnicode()); + } + + + f << "\n\n<region>"; + if(!m_szNames[ins->Keyboard[i]].empty()) + { + f << "\nregion_label=" << mpt::ToCharset(mpt::Charset::UTF8, GetCharsetInternal(), m_szNames[ins->Keyboard[i]]); + } + f << "\nsample=" << sampleName.GetFullFileName().ToUTF8(); + f << "\nlokey=" << i; + f << "\nhikey=" << endOfRegion; + if(sample.rootNote != NOTE_NONE) + f << "\npitch_keycenter=" << sample.rootNote - NOTE_MIN; + else + f << "\npitch_keycenter=" << NOTE_MIDDLEC + i - ins->NoteMap[i]; + if(sample.uFlags[CHN_PANNING]) + f << "\npan=" << (Util::muldivr_unsigned(sample.nPan, 200, 256) - 100) << " // " << sample.nPan; + if(sample.nGlobalVol != 64) + f << "\nvolume=" << SFZLinear2dB((ins->nGlobalVol * sample.nGlobalVol) / 4096.0) << " // " << sample.nGlobalVol; + const char *loopMode = "no_loop", *loopType = "forward"; + SmpLength loopStart = 0, loopEnd = 0; + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + loopMode = "loop_sustain"; + loopStart = sample.nSustainStart; + loopEnd = sample.nSustainEnd; + if(sample.uFlags[CHN_PINGPONGSUSTAIN]) + loopType = "alternate"; + } else if(sample.uFlags[CHN_LOOP]) + { + loopMode = "loop_continuous"; + loopStart = sample.nLoopStart; + loopEnd = sample.nLoopEnd; + if(sample.uFlags[CHN_PINGPONGLOOP]) + loopType = "alternate"; + else if(sample.uFlags[CHN_REVERSE]) + loopType = "backward"; + } + f << "\nloop_mode=" << loopMode; + if(loopStart < loopEnd) + { + f << "\nloop_start=" << loopStart; + f << "\nloop_end=" << (loopEnd - 1); + f << "\nloop_type=" << loopType; + } + if(sample.uFlags.test_all(CHN_SUSTAINLOOP | CHN_LOOP)) + { + f << "\n// Warning: Only sustain loop was exported!"; + } + i = endOfRegion; + } + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + +#else +bool CSoundFile::ReadSFZInstrument(INSTRUMENTINDEX, FileReader &) +{ + return false; +} +#endif // MPT_EXTERNAL_SAMPLES + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatVorbis.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatVorbis.cpp new file mode 100644 index 00000000..74a3cb1d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormatVorbis.cpp @@ -0,0 +1,364 @@ +/* + * SampleFormatVorbis.cpp + * ---------------------- + * Purpose: Vorbis sample import + * Notes : + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#endif +#include "../common/misc_util.h" +#include "Tagging.h" +#include "Loaders.h" +#include "../common/FileReader.h" +#include "modsmp_ctrl.h" +#include "openmpt/soundbase/Copy.hpp" +#include "mpt/audio/span.hpp" +#include "../soundlib/ModSampleCopy.h" +//#include "mpt/crc/crc.hpp" +#include "OggStream.h" +#ifdef MPT_WITH_OGG +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <ogg/ogg.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_OGG +#if defined(MPT_WITH_VORBIS) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/codec.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_VORBIS +#if defined(MPT_WITH_VORBISFILE) +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wreserved-id-macro" +#endif // MPT_COMPILER_CLANG +#include <vorbis/vorbisfile.h> +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG +#endif // MPT_WITH_VORBISFILE +#ifdef MPT_WITH_STBVORBIS +#include <stb_vorbis/stb_vorbis.c> +#endif // MPT_WITH_STBVORBIS + + +OPENMPT_NAMESPACE_BEGIN + + +//////////////////////////////////////////////////////////////////////////////// +// Vorbis + +#if defined(MPT_WITH_VORBISFILE) + +static size_t VorbisfileFilereaderRead(void *ptr, size_t size, size_t nmemb, void *datasource) +{ + FileReader &file = *static_cast<FileReader*>(datasource); + return file.ReadRaw(mpt::span(mpt::void_cast<std::byte*>(ptr), size * nmemb)).size() / size; +} + +static int VorbisfileFilereaderSeek(void *datasource, ogg_int64_t offset, int whence) +{ + FileReader &file = *static_cast<FileReader*>(datasource); + switch(whence) + { + case SEEK_SET: + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + } + break; + case SEEK_CUR: + { + if(offset < 0) + { + if(offset == std::numeric_limits<ogg_int64_t>::min()) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(0-offset)) + { + return -1; + } + return file.SkipBack(mpt::saturate_cast<FileReader::off_t>(0 - offset)) ? 0 : -1; + } else + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + return file.Skip(mpt::saturate_cast<FileReader::off_t>(offset)) ? 0 : -1; + } + } + break; + case SEEK_END: + { + if(!mpt::in_range<FileReader::off_t>(offset)) + { + return -1; + } + if(!mpt::in_range<FileReader::off_t>(file.GetLength() + offset)) + { + return -1; + } + return file.Seek(mpt::saturate_cast<FileReader::off_t>(file.GetLength() + offset)) ? 0 : -1; + } + break; + default: + return -1; + } +} + +static long VorbisfileFilereaderTell(void *datasource) +{ + FileReader &file = *static_cast<FileReader*>(datasource); + MPT_MAYBE_CONSTANT_IF(!mpt::in_range<long>(file.GetPosition())) + { + return -1; + } + return static_cast<long>(file.GetPosition()); +} + +#if defined(MPT_WITH_VORBIS) +static mpt::ustring UStringFromVorbis(const char *str) +{ + return str ? mpt::ToUnicode(mpt::Charset::UTF8, str) : mpt::ustring(); +} +#endif // MPT_WITH_VORBIS + +static FileTags GetVorbisFileTags(OggVorbis_File &vf) +{ + FileTags tags; + #if defined(MPT_WITH_VORBIS) + vorbis_comment *vc = ov_comment(&vf, -1); + if(!vc) + { + return tags; + } + tags.encoder = UStringFromVorbis(vorbis_comment_query(vc, "ENCODER", 0)); + tags.title = UStringFromVorbis(vorbis_comment_query(vc, "TITLE", 0)); + tags.comments = UStringFromVorbis(vorbis_comment_query(vc, "DESCRIPTION", 0)); + tags.bpm = UStringFromVorbis(vorbis_comment_query(vc, "BPM", 0)); // non-standard + tags.artist = UStringFromVorbis(vorbis_comment_query(vc, "ARTIST", 0)); + tags.album = UStringFromVorbis(vorbis_comment_query(vc, "ALBUM", 0)); + tags.trackno = UStringFromVorbis(vorbis_comment_query(vc, "TRACKNUMBER", 0)); + tags.year = UStringFromVorbis(vorbis_comment_query(vc, "DATE", 0)); + tags.url = UStringFromVorbis(vorbis_comment_query(vc, "CONTACT", 0)); + tags.genre = UStringFromVorbis(vorbis_comment_query(vc, "GENRE", 0)); + #else // !MPT_WITH_VORBIS + MPT_UNREFERENCED_PARAMETER(vf); + #endif // MPT_WITH_VORBIS + return tags; +} + +#endif // MPT_WITH_VORBISFILE + +bool CSoundFile::ReadVorbisSample(SAMPLEINDEX sample, FileReader &file) +{ + +#if defined(MPT_WITH_VORBISFILE) || defined(MPT_WITH_STBVORBIS) + + file.Rewind(); + + int rate = 0; + int channels = 0; + std::vector<int16> raw_sample_data; + + std::string sampleName; + +#endif // VORBIS + +#if defined(MPT_WITH_VORBISFILE) + + bool unsupportedSample = false; + + ov_callbacks callbacks = { + &VorbisfileFilereaderRead, + &VorbisfileFilereaderSeek, + NULL, + &VorbisfileFilereaderTell + }; + OggVorbis_File vf; + MemsetZero(vf); + if(ov_open_callbacks(&file, &vf, NULL, 0, callbacks) == 0) + { + if(ov_streams(&vf) == 1) + { // we do not support chained vorbis samples + vorbis_info *vi = ov_info(&vf, -1); + if(vi && vi->rate > 0 && vi->channels > 0) + { + sampleName = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(GetVorbisFileTags(vf))); + rate = vi->rate; + channels = vi->channels; + std::size_t offset = 0; + int current_section = 0; + long decodedSamples = 0; + bool eof = false; + + if(auto length = ov_pcm_total(&vf, 0); length != OV_EINVAL) + raw_sample_data.reserve(std::min(MAX_SAMPLE_LENGTH, mpt::saturate_cast<SmpLength>(length)) * std::clamp(channels, 1, 2)); + + while(!eof) + { + float **output = nullptr; + long ret = ov_read_float(&vf, &output, 1024, ¤t_section); + if(ret == 0) + { + eof = true; + } else if(ret < 0) + { + // stream error, just try to continue + } else + { + decodedSamples = ret; + if(decodedSamples > 0 && (channels == 1 || channels == 2)) + { + raw_sample_data.resize(raw_sample_data.size() + (channels * decodedSamples)); + CopyAudio(mpt::audio_span_interleaved(raw_sample_data.data() + (offset * channels), channels, decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + offset += decodedSamples; + if((raw_sample_data.size() / channels) > MAX_SAMPLE_LENGTH) + { + break; + } + } + } + } + } else + { + unsupportedSample = true; + } + } else + { + unsupportedSample = true; + } + ov_clear(&vf); + } else + { + unsupportedSample = true; + } + + if(unsupportedSample) + { + return false; + } + +#elif defined(MPT_WITH_STBVORBIS) + + // NOTE/TODO: stb_vorbis does not handle inferred negative PCM sample position + // at stream start. (See + // <https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-132000A.2>). This + // means that, for remuxed and re-aligned/cutted (at stream start) Vorbis + // files, stb_vorbis will include superfluous samples at the beginning. + + FileReader::PinnedView fileView = file.GetPinnedView(); + const std::byte* data = fileView.data(); + std::size_t dataLeft = fileView.size(); + + std::size_t offset = 0; + int consumed = 0; + int error = 0; + stb_vorbis *vorb = stb_vorbis_open_pushdata(mpt::byte_cast<const unsigned char*>(data), mpt::saturate_cast<int>(dataLeft), &consumed, &error, nullptr); + file.Skip(consumed); + data += consumed; + dataLeft -= consumed; + if(!vorb) + { + return false; + } + rate = stb_vorbis_get_info(vorb).sample_rate; + channels = stb_vorbis_get_info(vorb).channels; + if(rate <= 0 || channels <= 0) + { + return false; + } + while((error == VORBIS__no_error || (error == VORBIS_need_more_data && dataLeft > 0))) + { + int frame_channels = 0; + int decodedSamples = 0; + float **output = nullptr; + consumed = stb_vorbis_decode_frame_pushdata(vorb, mpt::byte_cast<const unsigned char*>(data), mpt::saturate_cast<int>(dataLeft), &frame_channels, &output, &decodedSamples); + file.Skip(consumed); + data += consumed; + dataLeft -= consumed; + LimitMax(frame_channels, channels); + if(decodedSamples > 0 && (frame_channels == 1 || frame_channels == 2)) + { + raw_sample_data.resize(raw_sample_data.size() + (channels * decodedSamples)); + CopyAudio(mpt::audio_span_interleaved(raw_sample_data.data() + (offset * channels), channels, decodedSamples), mpt::audio_span_planar(output, channels, decodedSamples)); + offset += decodedSamples; + if((raw_sample_data.size() / channels) > MAX_SAMPLE_LENGTH) + { + break; + } + } + error = stb_vorbis_get_error(vorb); + } + stb_vorbis_close(vorb); + +#endif // VORBIS + +#if defined(MPT_WITH_VORBISFILE) || defined(MPT_WITH_STBVORBIS) + + if(rate <= 0 || channels <= 0 || raw_sample_data.empty()) + { + return false; + } + + DestroySampleThreadsafe(sample); + ModSample &mptSample = Samples[sample]; + mptSample.Initialize(); + mptSample.nC5Speed = rate; + mptSample.nLength = std::min(MAX_SAMPLE_LENGTH, mpt::saturate_cast<SmpLength>(raw_sample_data.size() / channels)); + + mptSample.uFlags.set(CHN_16BIT); + mptSample.uFlags.set(CHN_STEREO, channels == 2); + + if(!mptSample.AllocateSample()) + { + return false; + } + + if(raw_sample_data.size() / channels > MAX_SAMPLE_LENGTH) + { + AddToLog(LogWarning, U_("Sample has been truncated!")); + } + + std::copy(raw_sample_data.begin(), raw_sample_data.begin() + mptSample.nLength * channels, mptSample.sample16()); + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + m_szNames[sample] = sampleName; + + return true; + +#else // !VORBIS + + MPT_UNREFERENCED_PARAMETER(sample); + MPT_UNREFERENCED_PARAMETER(file); + + return false; + +#endif // VORBIS + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormats.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormats.cpp new file mode 100644 index 00000000..bf19d849 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleFormats.cpp @@ -0,0 +1,2652 @@ +/* + * SampleFormats.cpp + * ----------------- + * Purpose: Code for loading various more or less common sample and instrument formats. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/Moddoc.h" +#include "Dlsbank.h" +#endif // MODPLUG_TRACKER +#include "../soundlib/AudioCriticalSection.h" +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/mptFileIO.h" +#endif // !MODPLUG_NO_FILESAVE +#include "../common/misc_util.h" +#include "openmpt/base/Endian.hpp" +#include "Tagging.h" +#include "ITTools.h" +#include "XMTools.h" +#include "S3MTools.h" +#include "WAVTools.h" +#include "../common/version.h" +#include "Loaders.h" +#include "../common/FileReader.h" +#include "../soundlib/ModSampleCopy.h" +#include <functional> +#include <map> + + +OPENMPT_NAMESPACE_BEGIN + + +using namespace mpt::uuid_literals; + + +bool CSoundFile::ReadSampleFromFile(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize, bool includeInstrumentFormats) +{ + if(!nSample || nSample >= MAX_SAMPLES) return false; + if(!ReadWAVSample(nSample, file, mayNormalize) + && !(includeInstrumentFormats && ReadXISample(nSample, file)) + && !(includeInstrumentFormats && ReadITISample(nSample, file)) + && !ReadW64Sample(nSample, file) + && !ReadCAFSample(nSample, file) + && !ReadAIFFSample(nSample, file, mayNormalize) + && !ReadITSSample(nSample, file) + && !(includeInstrumentFormats && ReadPATSample(nSample, file)) + && !ReadIFFSample(nSample, file) + && !ReadS3ISample(nSample, file) + && !ReadSBISample(nSample, file) + && !ReadAUSample(nSample, file, mayNormalize) + && !ReadBRRSample(nSample, file) + && !ReadFLACSample(nSample, file) + && !ReadOpusSample(nSample, file) + && !ReadVorbisSample(nSample, file) + && !ReadMP3Sample(nSample, file, false) + && !ReadMediaFoundationSample(nSample, file) + ) + { + return false; + } + + if(nSample > GetNumSamples()) + { + m_nSamples = nSample; + } + if(Samples[nSample].uFlags[CHN_ADLIB]) + { + InitOPL(); + } + return true; +} + + +bool CSoundFile::ReadInstrumentFromFile(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize) +{ + if ((!nInstr) || (nInstr >= MAX_INSTRUMENTS)) return false; + if(!ReadITIInstrument(nInstr, file) + && !ReadXIInstrument(nInstr, file) + && !ReadPATInstrument(nInstr, file) + && !ReadSFZInstrument(nInstr, file) + // Generic read + && !ReadSampleAsInstrument(nInstr, file, mayNormalize)) + { + bool ok = false; +#ifdef MODPLUG_TRACKER + CDLSBank bank; + if(bank.Open(file)) + { + ok = bank.ExtractInstrument(*this, nInstr, 0, 0); + } +#endif // MODPLUG_TRACKER + if(!ok) return false; + } + + if(nInstr > GetNumInstruments()) m_nInstruments = nInstr; + return true; +} + + +bool CSoundFile::ReadSampleAsInstrument(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize) +{ + // Scanning free sample + SAMPLEINDEX nSample = GetNextFreeSample(nInstr); // may also return samples which are only referenced by the current instrument + if(nSample == SAMPLEINDEX_INVALID) + { + return false; + } + + // Loading Instrument + ModInstrument *pIns = new (std::nothrow) ModInstrument(nSample); + if(pIns == nullptr) + { + return false; + } + if(!ReadSampleFromFile(nSample, file, mayNormalize, false)) + { + delete pIns; + return false; + } + + // Remove all samples which are only referenced by the old instrument, except for the one we just loaded our new sample into. + RemoveInstrumentSamples(nInstr, nSample); + + // Replace the instrument + DestroyInstrument(nInstr, doNoDeleteAssociatedSamples); + Instruments[nInstr] = pIns; + +#if defined(MPT_ENABLE_FILEIO) && defined(MPT_EXTERNAL_SAMPLES) + SetSamplePath(nSample, file.GetOptionalFileName().value_or(P_(""))); +#endif + + return true; +} + + +bool CSoundFile::DestroyInstrument(INSTRUMENTINDEX nInstr, deleteInstrumentSamples removeSamples) +{ + if(nInstr == 0 || nInstr >= MAX_INSTRUMENTS || !Instruments[nInstr]) return true; + + if(removeSamples == deleteAssociatedSamples) + { + RemoveInstrumentSamples(nInstr); + } + + CriticalSection cs; + + ModInstrument *pIns = Instruments[nInstr]; + Instruments[nInstr] = nullptr; + for(auto &chn : m_PlayState.Chn) + { + if(chn.pModInstrument == pIns) + chn.pModInstrument = nullptr; + } + delete pIns; + return true; +} + + +// Remove all unused samples from the given nInstr and keep keepSample if provided +bool CSoundFile::RemoveInstrumentSamples(INSTRUMENTINDEX nInstr, SAMPLEINDEX keepSample) +{ + if(Instruments[nInstr] == nullptr) + { + return false; + } + + std::vector<bool> keepSamples(GetNumSamples() + 1, true); + + // Check which samples are used by the instrument we are going to nuke. + auto referencedSamples = Instruments[nInstr]->GetSamples(); + for(auto sample : referencedSamples) + { + if(sample <= GetNumSamples()) + { + keepSamples[sample] = false; + } + } + + // If we want to keep a specific sample, do so. + if(keepSample != SAMPLEINDEX_INVALID) + { + if(keepSample <= GetNumSamples()) + { + keepSamples[keepSample] = true; + } + } + + // Check if any of those samples are referenced by other instruments as well, in which case we want to keep them of course. + for(INSTRUMENTINDEX nIns = 1; nIns <= GetNumInstruments(); nIns++) if (Instruments[nIns] != nullptr && nIns != nInstr) + { + Instruments[nIns]->GetSamples(keepSamples); + } + + // Now nuke the selected samples. + RemoveSelectedSamples(keepSamples); + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +// +// I/O From another song +// + +bool CSoundFile::ReadInstrumentFromSong(INSTRUMENTINDEX targetInstr, const CSoundFile &srcSong, INSTRUMENTINDEX sourceInstr) +{ + if ((!sourceInstr) || (sourceInstr > srcSong.GetNumInstruments()) + || (targetInstr >= MAX_INSTRUMENTS) || (!srcSong.Instruments[sourceInstr])) + { + return false; + } + if (m_nInstruments < targetInstr) m_nInstruments = targetInstr; + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + { + return false; + } + + DestroyInstrument(targetInstr, deleteAssociatedSamples); + + Instruments[targetInstr] = pIns; + *pIns = *srcSong.Instruments[sourceInstr]; + + std::vector<SAMPLEINDEX> sourceSample; // Sample index in source song + std::vector<SAMPLEINDEX> targetSample; // Sample index in target song + SAMPLEINDEX targetIndex = 0; // Next index for inserting sample + + for(auto &sample : pIns->Keyboard) + { + const SAMPLEINDEX sourceIndex = sample; + if(sourceIndex > 0 && sourceIndex <= srcSong.GetNumSamples()) + { + const auto entry = std::find(sourceSample.cbegin(), sourceSample.cend(), sourceIndex); + if(entry == sourceSample.end()) + { + // Didn't consider this sample yet, so add it to our map. + targetIndex = GetNextFreeSample(targetInstr, targetIndex + 1); + if(targetIndex <= GetModSpecifications().samplesMax) + { + sourceSample.push_back(sourceIndex); + targetSample.push_back(targetIndex); + sample = targetIndex; + } else + { + sample = 0; + } + } else + { + // Sample reference has already been created, so only need to update the sample map. + sample = *(entry - sourceSample.begin() + targetSample.begin()); + } + } else + { + // Invalid or no source sample + sample = 0; + } + } + +#ifdef MODPLUG_TRACKER + if(pIns->filename.empty() && srcSong.GetpModDoc() != nullptr && &srcSong != this) + { + pIns->filename = srcSong.GetpModDoc()->GetPathNameMpt().GetFullFileName().ToLocale(); + } +#endif + pIns->Convert(srcSong.GetType(), GetType()); + + // Copy all referenced samples over + for(size_t i = 0; i < targetSample.size(); i++) + { + ReadSampleFromSong(targetSample[i], srcSong, sourceSample[i]); + } + + return true; +} + + +bool CSoundFile::ReadSampleFromSong(SAMPLEINDEX targetSample, const CSoundFile &srcSong, SAMPLEINDEX sourceSample) +{ + if(!sourceSample + || sourceSample > srcSong.GetNumSamples() + || (targetSample >= GetModSpecifications().samplesMax && targetSample > GetNumSamples())) + { + return false; + } + + DestroySampleThreadsafe(targetSample); + + const ModSample &sourceSmp = srcSong.GetSample(sourceSample); + ModSample &targetSmp = GetSample(targetSample); + + if(GetNumSamples() < targetSample) m_nSamples = targetSample; + targetSmp = sourceSmp; + m_szNames[targetSample] = srcSong.m_szNames[sourceSample]; + + if(sourceSmp.HasSampleData()) + { + if(targetSmp.CopyWaveform(sourceSmp)) + targetSmp.PrecomputeLoops(*this, false); + // Remember on-disk path (for MPTM files), but don't implicitely enable on-disk storage + // (we really don't want this for e.g. duplicating samples or splitting stereo samples) +#ifdef MPT_EXTERNAL_SAMPLES + SetSamplePath(targetSample, srcSong.GetSamplePath(sourceSample)); +#endif + targetSmp.uFlags.reset(SMP_KEEPONDISK); + } + +#ifdef MODPLUG_TRACKER + if((targetSmp.filename.empty()) && srcSong.GetpModDoc() != nullptr && &srcSong != this) + { + targetSmp.filename = mpt::ToCharset(GetCharsetInternal(), srcSong.GetpModDoc()->GetTitle()); + } +#endif + + if(targetSmp.uFlags[CHN_ADLIB] && !SupportsOPL()) + { + AddToLog(LogInformation, U_("OPL instruments are not supported by this format.")); + } + targetSmp.Convert(srcSong.GetType(), GetType()); + if(targetSmp.uFlags[CHN_ADLIB]) + { + InitOPL(); + } + return true; +} + + +//////////////////////////////////////////////////////////////////////// +// IMA ADPCM Support for WAV files + + +static bool IMAADPCMUnpack16(int16 *target, SmpLength sampleLen, FileReader file, uint16 blockAlign, uint32 numChannels) +{ + static constexpr int8 IMAIndexTab[8] = { -1, -1, -1, -1, 2, 4, 6, 8 }; + static constexpr int16 IMAUnpackTable[90] = + { + 7, 8, 9, 10, 11, 12, 13, 14, + 16, 17, 19, 21, 23, 25, 28, 31, + 34, 37, 41, 45, 50, 55, 60, 66, + 73, 80, 88, 97, 107, 118, 130, 143, + 157, 173, 190, 209, 230, 253, 279, 307, + 337, 371, 408, 449, 494, 544, 598, 658, + 724, 796, 876, 963, 1060, 1166, 1282, 1411, + 1552, 1707, 1878, 2066, 2272, 2499, 2749, 3024, + 3327, 3660, 4026, 4428, 4871, 5358, 5894, 6484, + 7132, 7845, 8630, 9493, 10442, 11487, 12635, 13899, + 15289, 16818, 18500, 20350, 22385, 24623, 27086, 29794, + 32767, 0 + }; + + if(target == nullptr || blockAlign < 4u * numChannels) + return false; + + SmpLength samplePos = 0; + sampleLen *= numChannels; + while(file.CanRead(4u * numChannels) && samplePos < sampleLen) + { + FileReader block = file.ReadChunk(blockAlign); + FileReader::PinnedView blockView = block.GetPinnedView(); + const std::byte *data = blockView.data(); + const uint32 blockSize = static_cast<uint32>(blockView.size()); + + for(uint32 chn = 0; chn < numChannels; chn++) + { + // Block header + int32 value = block.ReadInt16LE(); + int32 nIndex = block.ReadUint8(); + Limit(nIndex, 0, 89); + block.Skip(1); + + SmpLength smpPos = samplePos + chn; + uint32 dataPos = (numChannels + chn) * 4; + // Block data + while(smpPos <= (sampleLen - 8) && dataPos <= (blockSize - 4)) + { + for(uint32 i = 0; i < 8; i++) + { + uint8 delta = mpt::byte_cast<uint8>(data[dataPos]); + if(i & 1) + { + delta >>= 4; + dataPos++; + } else + { + delta &= 0x0F; + } + int32 v = IMAUnpackTable[nIndex] >> 3; + if (delta & 1) v += IMAUnpackTable[nIndex] >> 2; + if (delta & 2) v += IMAUnpackTable[nIndex] >> 1; + if (delta & 4) v += IMAUnpackTable[nIndex]; + if (delta & 8) value -= v; else value += v; + nIndex += IMAIndexTab[delta & 7]; + Limit(nIndex, 0, 88); + Limit(value, -32768, 32767); + target[smpPos] = static_cast<int16>(value); + smpPos += numChannels; + } + dataPos += (numChannels - 1) * 4u; + } + } + samplePos += ((blockSize - (numChannels * 4u)) * 2u); + } + + return true; +} + + +//////////////////////////////////////////////////////////////////////////////// +// WAV Open + +bool CSoundFile::ReadWAVSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize, FileReader *wsmpChunk) +{ + WAVReader wavFile(file); + + static constexpr WAVFormatChunk::SampleFormats SupportedFormats[] = {WAVFormatChunk::fmtPCM, WAVFormatChunk::fmtFloat, WAVFormatChunk::fmtIMA_ADPCM, WAVFormatChunk::fmtMP3, WAVFormatChunk::fmtALaw, WAVFormatChunk::fmtULaw}; + if(!wavFile.IsValid() + || wavFile.GetNumChannels() == 0 + || wavFile.GetNumChannels() > 2 + || (wavFile.GetBitsPerSample() == 0 && wavFile.GetSampleFormat() != WAVFormatChunk::fmtMP3) + || (wavFile.GetBitsPerSample() < 32 && wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat) + || (wavFile.GetBitsPerSample() > 64) + || !mpt::contains(SupportedFormats, wavFile.GetSampleFormat())) + { + return false; + } + + DestroySampleThreadsafe(nSample); + m_szNames[nSample] = ""; + ModSample &sample = Samples[nSample]; + sample.Initialize(); + sample.nLength = wavFile.GetSampleLength(); + sample.nC5Speed = wavFile.GetSampleRate(); + wavFile.ApplySampleSettings(sample, GetCharsetInternal(), m_szNames[nSample]); + + FileReader sampleChunk = wavFile.GetSampleData(); + + SampleIO sampleIO( + SampleIO::_8bit, + (wavFile.GetNumChannels() > 1) ? SampleIO::stereoInterleaved : SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + + if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtIMA_ADPCM && wavFile.GetNumChannels() <= 2) + { + // IMA ADPCM 4:1 + LimitMax(sample.nLength, MAX_SAMPLE_LENGTH); + sample.uFlags.set(CHN_16BIT); + sample.uFlags.set(CHN_STEREO, wavFile.GetNumChannels() == 2); + if(!sample.AllocateSample()) + { + return false; + } + IMAADPCMUnpack16(sample.sample16(), sample.nLength, sampleChunk, wavFile.GetBlockAlign(), wavFile.GetNumChannels()); + sample.PrecomputeLoops(*this, false); + } else if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtMP3) + { + // MP3 in WAV + bool loadedMP3 = ReadMP3Sample(nSample, sampleChunk, false, true) || ReadMediaFoundationSample(nSample, sampleChunk, true); + if(!loadedMP3) + { + return false; + } + } else if(!wavFile.IsExtensibleFormat() && wavFile.MayBeCoolEdit16_8() && wavFile.GetSampleFormat() == WAVFormatChunk::fmtPCM && wavFile.GetBitsPerSample() == 32 && wavFile.GetBlockAlign() == wavFile.GetNumChannels() * 4) + { + // Syntrillium Cool Edit hack to store IEEE 32bit floating point + // Format is described as 32bit integer PCM contained in 32bit blocks and an WAVEFORMATEX extension size of 2 which contains a single 16 bit little endian value of 1. + // (This is parsed in WAVTools.cpp and returned via MayBeCoolEdit16_8()). + // The data actually stored in this case is little endian 32bit floating point PCM with 2**15 full scale. + // Cool Edit calls this format "16.8 float". + sampleIO |= SampleIO::_32bit; + sampleIO |= SampleIO::floatPCM15; + sampleIO.ReadSample(sample, sampleChunk); + } else if(!wavFile.IsExtensibleFormat() && wavFile.GetSampleFormat() == WAVFormatChunk::fmtPCM && wavFile.GetBitsPerSample() == 24 && wavFile.GetBlockAlign() == wavFile.GetNumChannels() * 4) + { + // Syntrillium Cool Edit hack to store IEEE 32bit floating point + // Format is described as 24bit integer PCM contained in 32bit blocks. + // The data actually stored in this case is little endian 32bit floating point PCM with 2**23 full scale. + // Cool Edit calls this format "24.0 float". + sampleIO |= SampleIO::_32bit; + sampleIO |= SampleIO::floatPCM23; + sampleIO.ReadSample(sample, sampleChunk); + } else if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtALaw || wavFile.GetSampleFormat() == WAVFormatChunk::fmtULaw) + { + // a-law / u-law + sampleIO |= SampleIO::_16bit; + sampleIO |= wavFile.GetSampleFormat() == WAVFormatChunk::fmtALaw ? SampleIO::aLaw : SampleIO::uLaw; + sampleIO.ReadSample(sample, sampleChunk); + } else + { + // PCM / Float + SampleIO::Bitdepth bitDepth; + switch((wavFile.GetBitsPerSample() - 1) / 8u) + { + default: + case 0: bitDepth = SampleIO::_8bit; break; + case 1: bitDepth = SampleIO::_16bit; break; + case 2: bitDepth = SampleIO::_24bit; break; + case 3: bitDepth = SampleIO::_32bit; break; + case 7: bitDepth = SampleIO::_64bit; break; + } + + sampleIO |= bitDepth; + if(wavFile.GetBitsPerSample() <= 8) + sampleIO |= SampleIO::unsignedPCM; + + if(wavFile.GetSampleFormat() == WAVFormatChunk::fmtFloat) + sampleIO |= SampleIO::floatPCM; + + if(mayNormalize) + sampleIO.MayNormalize(); + + sampleIO.ReadSample(sample, sampleChunk); + } + + if(wsmpChunk != nullptr) + { + // DLS WSMP chunk + *wsmpChunk = wavFile.GetWsmpChunk(); + } + + sample.Convert(MOD_TYPE_IT, GetType()); + sample.PrecomputeLoops(*this, false); + + return true; +} + + +/////////////////////////////////////////////////////////////// +// Save WAV + + +#ifndef MODPLUG_NO_FILESAVE +bool CSoundFile::SaveWAVSample(SAMPLEINDEX nSample, std::ostream &f) const +{ + const ModSample &sample = Samples[nSample]; + if(sample.uFlags[CHN_ADLIB]) + return false; + + mpt::IO::OFile<std::ostream> ff(f); + WAVWriter file(ff); + + file.WriteFormat(sample.GetSampleRate(GetType()), sample.GetElementarySampleSize() * 8, sample.GetNumChannels(), WAVFormatChunk::fmtPCM); + + // Write sample data + file.StartChunk(RIFFChunk::iddata); + file.Skip(SampleIO( + sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + sample.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved : SampleIO::mono, + SampleIO::littleEndian, + sample.uFlags[CHN_16BIT] ? SampleIO::signedPCM : SampleIO::unsignedPCM) + .WriteSample(f, sample)); + + file.WriteLoopInformation(sample); + file.WriteExtraInformation(sample, GetType()); + if(sample.HasCustomCuePoints()) + { + file.WriteCueInformation(sample); + } + + FileTags tags; + tags.SetEncoder(); + tags.title = mpt::ToUnicode(GetCharsetInternal(), m_szNames[nSample]); + file.WriteMetatags(tags); + file.Finalize(); + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + + +///////////////// +// Sony Wave64 // + + +struct Wave64FileHeader +{ + mpt::GUIDms GuidRIFF; + uint64le FileSize; + mpt::GUIDms GuidWAVE; +}; + +MPT_BINARY_STRUCT(Wave64FileHeader, 40) + + +struct Wave64ChunkHeader +{ + mpt::GUIDms GuidChunk; + uint64le Size; +}; + +MPT_BINARY_STRUCT(Wave64ChunkHeader, 24) + + +struct Wave64Chunk +{ + Wave64ChunkHeader header; + + FileReader::off_t GetLength() const + { + uint64 length = header.Size; + if(length < sizeof(Wave64ChunkHeader)) + { + length = 0; + } else + { + length -= sizeof(Wave64ChunkHeader); + } + return mpt::saturate_cast<FileReader::off_t>(length); + } + + mpt::UUID GetID() const + { + return mpt::UUID(header.GuidChunk); + } +}; + +MPT_BINARY_STRUCT(Wave64Chunk, 24) + + +static void Wave64TagFromLISTINFO(mpt::ustring & dst, uint16 codePage, const FileReader::ChunkList<RIFFChunk> & infoChunk, RIFFChunk::ChunkIdentifiers id) +{ + if(!infoChunk.ChunkExists(id)) + { + return; + } + FileReader textChunk = infoChunk.GetChunk(id); + if(!textChunk.IsValid()) + { + return; + } + std::string str; + textChunk.ReadString<mpt::String::maybeNullTerminated>(str, textChunk.GetLength()); + str = mpt::replace(str, std::string("\r\n"), std::string("\n")); + str = mpt::replace(str, std::string("\r"), std::string("\n")); + dst = mpt::ToUnicode(codePage, mpt::Charset::Windows1252, str); +} + + +bool CSoundFile::ReadW64Sample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) +{ + file.Rewind(); + + constexpr mpt::UUID guidRIFF = "66666972-912E-11CF-A5D6-28DB04C10000"_uuid; + constexpr mpt::UUID guidWAVE = "65766177-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + + constexpr mpt::UUID guidLIST = "7473696C-912F-11CF-A5D6-28DB04C10000"_uuid; + constexpr mpt::UUID guidFMT = "20746D66-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + //constexpr mpt::UUID guidFACT = "74636166-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + constexpr mpt::UUID guidDATA = "61746164-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + //constexpr mpt::UUID guidLEVL = "6C76656C-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + //constexpr mpt::UUID guidJUNK = "6b6E756A-ACF3-11D3-8CD1-00C04f8EDB8A"_uuid; + //constexpr mpt::UUID guidBEXT = "74786562-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + //constexpr mpt::UUID guiMARKER = "ABF76256-392D-11D2-86C7-00C04F8EDB8A"_uuid; + //constexpr mpt::UUID guiSUMMARYLIST = "925F94BC-525A-11D2-86DC-00C04F8EDB8A"_uuid; + + constexpr mpt::UUID guidCSET = "54455343-ACF3-11D3-8CD1-00C04F8EDB8A"_uuid; + + Wave64FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(mpt::UUID(fileHeader.GuidRIFF) != guidRIFF) + { + return false; + } + if(mpt::UUID(fileHeader.GuidWAVE) != guidWAVE) + { + return false; + } + if(fileHeader.FileSize != file.GetLength()) + { + return false; + } + + FileReader chunkFile = file; + auto chunkList = chunkFile.ReadChunks<Wave64Chunk>(8); + + if(!chunkList.ChunkExists(guidFMT)) + { + return false; + } + FileReader formatChunk = chunkList.GetChunk(guidFMT); + WAVFormatChunk format; + if(!formatChunk.ReadStruct(format)) + { + return false; + } + uint16 sampleFormat = format.format; + if(format.format == WAVFormatChunk::fmtExtensible) + { + WAVFormatChunkExtension formatExt; + if(!formatChunk.ReadStruct(formatExt)) + { + return false; + } + sampleFormat = static_cast<uint16>(mpt::UUID(formatExt.subFormat).GetData1()); + } + if(format.sampleRate == 0) + { + return false; + } + if(format.numChannels == 0) + { + return false; + } + if(format.numChannels > 2) + { + return false; + } + if(sampleFormat != WAVFormatChunk::fmtPCM && sampleFormat != WAVFormatChunk::fmtFloat) + { + return false; + } + if(sampleFormat == WAVFormatChunk::fmtFloat && format.bitsPerSample != 32 && format.bitsPerSample != 64) + { + return false; + } + if(sampleFormat == WAVFormatChunk::fmtPCM && format.bitsPerSample > 64) + { + return false; + } + + SampleIO::Bitdepth bitDepth; + switch((format.bitsPerSample - 1) / 8u) + { + default: + case 0: bitDepth = SampleIO::_8bit ; break; + case 1: bitDepth = SampleIO::_16bit; break; + case 2: bitDepth = SampleIO::_24bit; break; + case 3: bitDepth = SampleIO::_32bit; break; + case 7: bitDepth = SampleIO::_64bit; break; + } + SampleIO sampleIO( + bitDepth, + (format.numChannels > 1) ? SampleIO::stereoInterleaved : SampleIO::mono, + SampleIO::littleEndian, + (sampleFormat == WAVFormatChunk::fmtFloat) ? SampleIO::floatPCM : SampleIO::signedPCM); + if(format.bitsPerSample <= 8) + { + sampleIO |= SampleIO::unsignedPCM; + } + if(mayNormalize) + { + sampleIO.MayNormalize(); + } + + FileTags tags; + + uint16 codePage = 28591; // mpt::Charset::ISO8859_1 + FileReader csetChunk = chunkList.GetChunk(guidCSET); + if(csetChunk.IsValid()) + { + if(csetChunk.CanRead(2)) + { + codePage = csetChunk.ReadUint16LE(); + } + } + + if(chunkList.ChunkExists(guidLIST)) + { + FileReader listChunk = chunkList.GetChunk(guidLIST); + if(listChunk.ReadMagic("INFO")) + { + auto infoChunk = listChunk.ReadChunks<RIFFChunk>(2); + Wave64TagFromLISTINFO(tags.title, codePage, infoChunk, RIFFChunk::idINAM); + Wave64TagFromLISTINFO(tags.encoder, codePage, infoChunk, RIFFChunk::idISFT); + //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idICOP); + Wave64TagFromLISTINFO(tags.artist, codePage, infoChunk, RIFFChunk::idIART); + Wave64TagFromLISTINFO(tags.album, codePage, infoChunk, RIFFChunk::idIPRD); + Wave64TagFromLISTINFO(tags.comments, codePage, infoChunk, RIFFChunk::idICMT); + //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idIENG); + //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idISBJ); + Wave64TagFromLISTINFO(tags.genre, codePage, infoChunk, RIFFChunk::idIGNR); + //Wave64TagFromLISTINFO(void, codePage, infoChunk, RIFFChunk::idICRD); + Wave64TagFromLISTINFO(tags.year, codePage, infoChunk, RIFFChunk::idYEAR); + Wave64TagFromLISTINFO(tags.trackno, codePage, infoChunk, RIFFChunk::idTRCK); + Wave64TagFromLISTINFO(tags.url, codePage, infoChunk, RIFFChunk::idTURL); + //Wave64TagFromLISTINFO(tags.bpm, codePage, infoChunk, void); + } + } + + if(!chunkList.ChunkExists(guidDATA)) + { + return false; + } + FileReader audioData = chunkList.GetChunk(guidDATA); + + SmpLength length = mpt::saturate_cast<SmpLength>(audioData.GetLength() / (sampleIO.GetEncodedBitsPerSample()/8)); + + ModSample &mptSample = Samples[nSample]; + DestroySampleThreadsafe(nSample); + mptSample.Initialize(); + mptSample.nLength = length; + mptSample.nC5Speed = format.sampleRate; + + sampleIO.ReadSample(mptSample, audioData); + + m_szNames[nSample] = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags)); + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + + return true; + +} + + + +#ifndef MODPLUG_NO_FILESAVE + +/////////////////////////////////////////////////////////////// +// Save RAW + +bool CSoundFile::SaveRAWSample(SAMPLEINDEX nSample, std::ostream &f) const +{ + const ModSample &sample = Samples[nSample]; + SampleIO( + sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, + sample.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved : SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM) + .WriteSample(f, sample); + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + +///////////////////////////////////////////////////////////// +// GUS Patches + +struct GF1PatchFileHeader +{ + char magic[8]; // "GF1PATCH" + char version[4]; // "100", or "110" + char id[10]; // "ID#000002" + char copyright[60]; // Copyright + uint8le numInstr; // Number of instruments in patch + uint8le voices; // Number of voices, usually 14 + uint8le channels; // Number of wav channels that can be played concurently to the patch + uint16le numSamples; // Total number of waveforms for all the .PAT + uint16le volume; // Master volume + uint32le dataSize; + char reserved2[36]; +}; + +MPT_BINARY_STRUCT(GF1PatchFileHeader, 129) + + +struct GF1Instrument +{ + uint16le id; // Instrument id: 0-65535 + char name[16]; // Name of instrument. Gravis doesn't seem to use it + uint32le size; // Number of bytes for the instrument with header. (To skip to next instrument) + uint8 layers; // Number of layers in instrument: 1-4 + char reserved[40]; +}; + +MPT_BINARY_STRUCT(GF1Instrument, 63) + + +struct GF1SampleHeader +{ + char name[7]; // null terminated string. name of the wave. + uint8le fractions; // Start loop point fraction in 4 bits + End loop point fraction in the 4 other bits. + uint32le length; // total size of wavesample. limited to 65535 now by the drivers, not the card. + uint32le loopstart; // start loop position in the wavesample + uint32le loopend; // end loop position in the wavesample + uint16le freq; // Rate at which the wavesample has been sampled + uint32le low_freq; // check note.h for the correspondance. + uint32le high_freq; // check note.h for the correspondance. + uint32le root_freq; // check note.h for the correspondance. + int16le finetune; // fine tune. -512 to +512, EXCLUDING 0 cause it is a multiplier. 512 is one octave off, and 1 is a neutral value + uint8le balance; // Balance: 0-15. 0=full left, 15 = full right + uint8le env_rate[6]; // attack rates + uint8le env_volume[6]; // attack volumes + uint8le tremolo_sweep, tremolo_rate, tremolo_depth; + uint8le vibrato_sweep, vibrato_rate, vibrato_depth; + uint8le flags; + int16le scale_frequency; // Note + uint16le scale_factor; // 0...2048 (1024 is normal) or 0...2 + char reserved[36]; +}; + +MPT_BINARY_STRUCT(GF1SampleHeader, 96) + +// -- GF1 Envelopes -- +// +// It can be represented like this (the envelope is totally bogus, it is +// just to show the concept): +// +// | +// | /----` | | +// | /------/ `\ | | | | | +// | / \ | | | | | +// | / \ | | | | | +// |/ \ | | | | | +// ---------------------------- | | | | | | +// <---> attack rate 0 0 1 2 3 4 5 amplitudes +// <----> attack rate 1 +// <> attack rate 2 +// <--> attack rate 3 +// <> attack rate 4 +// <-----> attack rate 5 +// +// -- GF1 Flags -- +// +// bit 0: 8/16 bit +// bit 1: Signed/Unsigned +// bit 2: off/on looping +// bit 3: off/on bidirectionnal looping +// bit 4: off/on backward looping +// bit 5: off/on sustaining (3rd point in env.) +// bit 6: off/on envelopes +// bit 7: off/on clamped release (6th point, env) + + +struct GF1Layer +{ + uint8le previous; // If !=0 the wavesample to use is from the previous layer. The waveheader is still needed + uint8le id; // Layer id: 0-3 + uint32le size; // data size in bytes in the layer, without the header. to skip to next layer for example: + uint8le samples; // number of wavesamples + char reserved[40]; +}; + +MPT_BINARY_STRUCT(GF1Layer, 47) + + +static double PatchFreqToNote(uint32 nFreq) +{ + return std::log(nFreq / 2044.0) * (12.0 * 1.44269504088896340736); // 1.0/std::log(2.0) +} + + +static int32 PatchFreqToNoteInt(uint32 nFreq) +{ + return mpt::saturate_round<int32>(PatchFreqToNote(nFreq)); +} + + +static void PatchToSample(CSoundFile *that, SAMPLEINDEX nSample, GF1SampleHeader &sampleHeader, FileReader &file) +{ + ModSample &sample = that->GetSample(nSample); + + file.ReadStruct(sampleHeader); + + sample.Initialize(); + if(sampleHeader.flags & 4) sample.uFlags.set(CHN_LOOP); + if(sampleHeader.flags & 8) sample.uFlags.set(CHN_PINGPONGLOOP); + if(sampleHeader.flags & 16) sample.uFlags.set(CHN_REVERSE); + sample.nLength = sampleHeader.length; + sample.nLoopStart = sampleHeader.loopstart; + sample.nLoopEnd = sampleHeader.loopend; + sample.nC5Speed = sampleHeader.freq; + sample.nPan = (sampleHeader.balance * 256 + 8) / 15; + if(sample.nPan > 256) sample.nPan = 128; + else sample.uFlags.set(CHN_PANNING); + sample.nVibType = VIB_SINE; + sample.nVibSweep = sampleHeader.vibrato_sweep; + sample.nVibDepth = sampleHeader.vibrato_depth; + sample.nVibRate = sampleHeader.vibrato_rate / 4; + if(sampleHeader.scale_factor) + { + sample.Transpose((84.0 - PatchFreqToNote(sampleHeader.root_freq)) / 12.0); + } + + SampleIO sampleIO( + SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + (sampleHeader.flags & 2) ? SampleIO::unsignedPCM : SampleIO::signedPCM); + + if(sampleHeader.flags & 1) + { + sampleIO |= SampleIO::_16bit; + sample.nLength /= 2; + sample.nLoopStart /= 2; + sample.nLoopEnd /= 2; + } + sampleIO.ReadSample(sample, file); + sample.Convert(MOD_TYPE_IT, that->GetType()); + sample.PrecomputeLoops(*that, false); + + that->m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); +} + + +bool CSoundFile::ReadPATSample(SAMPLEINDEX nSample, FileReader &file) +{ + file.Rewind(); + GF1PatchFileHeader fileHeader; + GF1Instrument instrHeader; // We only support one instrument + GF1Layer layerHeader; + if(!file.ReadStruct(fileHeader) + || memcmp(fileHeader.magic, "GF1PATCH", 8) + || (memcmp(fileHeader.version, "110\0", 4) && memcmp(fileHeader.version, "100\0", 4)) + || memcmp(fileHeader.id, "ID#000002\0", 10) + || !fileHeader.numInstr || !fileHeader.numSamples + || !file.ReadStruct(instrHeader) + //|| !instrHeader.layers // DOO.PAT has 0 layers + || !file.ReadStruct(layerHeader) + || !layerHeader.samples) + { + return false; + } + + DestroySampleThreadsafe(nSample); + GF1SampleHeader sampleHeader; + PatchToSample(this, nSample, sampleHeader, file); + + if(instrHeader.name[0] > ' ') + { + m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); + } + return true; +} + + +// PAT Instrument +bool CSoundFile::ReadPATInstrument(INSTRUMENTINDEX nInstr, FileReader &file) +{ + file.Rewind(); + GF1PatchFileHeader fileHeader; + GF1Instrument instrHeader; // We only support one instrument + GF1Layer layerHeader; + if(!file.ReadStruct(fileHeader) + || memcmp(fileHeader.magic, "GF1PATCH", 8) + || (memcmp(fileHeader.version, "110\0", 4) && memcmp(fileHeader.version, "100\0", 4)) + || memcmp(fileHeader.id, "ID#000002\0", 10) + || !fileHeader.numInstr || !fileHeader.numSamples + || !file.ReadStruct(instrHeader) + //|| !instrHeader.layers // DOO.PAT has 0 layers + || !file.ReadStruct(layerHeader) + || !layerHeader.samples) + { + return false; + } + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + { + return false; + } + + DestroyInstrument(nInstr, deleteAssociatedSamples); + if (nInstr > m_nInstruments) m_nInstruments = nInstr; + Instruments[nInstr] = pIns; + + pIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrHeader.name); + pIns->nFadeOut = 2048; + if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + pIns->nNNA = NewNoteAction::NoteOff; + pIns->nDNA = DuplicateNoteAction::NoteFade; + } + + SAMPLEINDEX nextSample = 0; + int32 nMinSmpNote = 0xFF; + SAMPLEINDEX nMinSmp = 0; + for(uint8 smp = 0; smp < layerHeader.samples; smp++) + { + // Find a free sample + nextSample = GetNextFreeSample(nInstr, nextSample + 1); + if(nextSample == SAMPLEINDEX_INVALID) break; + if(m_nSamples < nextSample) m_nSamples = nextSample; + if(!nMinSmp) nMinSmp = nextSample; + // Load it + GF1SampleHeader sampleHeader; + PatchToSample(this, nextSample, sampleHeader, file); + int32 nMinNote = (sampleHeader.low_freq > 100) ? PatchFreqToNoteInt(sampleHeader.low_freq) : 0; + int32 nMaxNote = (sampleHeader.high_freq > 100) ? PatchFreqToNoteInt(sampleHeader.high_freq) : static_cast<uint8>(NOTE_MAX); + int32 nBaseNote = (sampleHeader.root_freq > 100) ? PatchFreqToNoteInt(sampleHeader.root_freq) : -1; + if(!sampleHeader.scale_factor && layerHeader.samples == 1) { nMinNote = 0; nMaxNote = NOTE_MAX; } + // Fill Note Map + for(int32 k = 0; k < NOTE_MAX; k++) + { + if(k == nBaseNote || (!pIns->Keyboard[k] && k >= nMinNote && k <= nMaxNote)) + { + if(!sampleHeader.scale_factor) + pIns->NoteMap[k] = NOTE_MIDDLEC; + + pIns->Keyboard[k] = nextSample; + if(k < nMinSmpNote) + { + nMinSmpNote = k; + nMinSmp = nextSample; + } + } + } + } + if(nMinSmp) + { + // Fill note map and missing samples + for(uint8 k = 0; k < NOTE_MAX; k++) + { + if(!pIns->NoteMap[k]) pIns->NoteMap[k] = k + 1; + if(!pIns->Keyboard[k]) + { + pIns->Keyboard[k] = nMinSmp; + } else + { + nMinSmp = pIns->Keyboard[k]; + } + } + } + + pIns->Sanitize(MOD_TYPE_IT); + pIns->Convert(MOD_TYPE_IT, GetType()); + return true; +} + + +///////////////////////////////////////////////////////////// +// S3I Samples + + +bool CSoundFile::ReadS3ISample(SAMPLEINDEX nSample, FileReader &file) +{ + file.Rewind(); + + S3MSampleHeader sampleHeader; + if(!file.ReadStruct(sampleHeader) + || (sampleHeader.sampleType != S3MSampleHeader::typePCM && sampleHeader.sampleType != S3MSampleHeader::typeAdMel) + || (memcmp(sampleHeader.magic, "SCRS", 4) && memcmp(sampleHeader.magic, "SCRI", 4)) + || !file.Seek(sampleHeader.GetSampleOffset())) + { + return false; + } + + DestroySampleThreadsafe(nSample); + + ModSample &sample = Samples[nSample]; + sampleHeader.ConvertToMPT(sample); + m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); + + if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel) + sampleHeader.GetSampleFormat(false).ReadSample(sample, file); + else if(SupportsOPL()) + InitOPL(); + else + AddToLog(LogInformation, U_("OPL instruments are not supported by this format.")); + + sample.Convert(MOD_TYPE_S3M, GetType()); + sample.PrecomputeLoops(*this, false); + return true; +} + +#ifndef MODPLUG_NO_FILESAVE + +bool CSoundFile::SaveS3ISample(SAMPLEINDEX smp, std::ostream &f) const +{ + const ModSample &sample = Samples[smp]; + S3MSampleHeader sampleHeader{}; + SmpLength length = sampleHeader.ConvertToS3M(sample); + mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader.name) = m_szNames[smp]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, sampleHeader.reserved2) = mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()); + if(length) + sampleHeader.dataPointer[1] = sizeof(S3MSampleHeader) >> 4; + mpt::IO::Write(f, sampleHeader); + if(length) + sampleHeader.GetSampleFormat(false).WriteSample(f, sample, length); + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +///////////////////////////////////////////////////////////// +// SBI OPL patch files + +bool CSoundFile::ReadSBISample(SAMPLEINDEX sample, FileReader &file) +{ + file.Rewind(); + const auto magic = file.ReadArray<char, 4>(); + if((memcmp(magic.data(), "SBI\x1A", 4) && memcmp(magic.data(), "SBI\x1D", 4)) // 1D = broken JuceOPLVSTi files + || !file.CanRead(32 + sizeof(OPLPatch)) + || file.CanRead(64)) // Arbitrary threshold to reject files that are unlikely to be SBI files + return false; + + if(!SupportsOPL()) + { + AddToLog(LogInformation, U_("OPL instruments are not supported by this format.")); + return true; + } + + DestroySampleThreadsafe(sample); + InitOPL(); + + ModSample &mptSmp = Samples[sample]; + mptSmp.Initialize(MOD_TYPE_S3M); + file.ReadString<mpt::String::nullTerminated>(m_szNames[sample], 32); + OPLPatch patch; + file.ReadArray(patch); + mptSmp.SetAdlib(true, patch); + + mptSmp.Convert(MOD_TYPE_S3M, GetType()); + return true; +} + + + +///////////////////////////////////////////////////////////// +// XI Instruments + + +bool CSoundFile::ReadXIInstrument(INSTRUMENTINDEX nInstr, FileReader &file) +{ + file.Rewind(); + + XIInstrumentHeader fileHeader; + if(!file.ReadStruct(fileHeader) + || memcmp(fileHeader.signature, "Extended Instrument: ", 21) + || fileHeader.version != XIInstrumentHeader::fileVersion + || fileHeader.eof != 0x1A) + { + return false; + } + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + { + return false; + } + + DestroyInstrument(nInstr, deleteAssociatedSamples); + if(nInstr > m_nInstruments) + { + m_nInstruments = nInstr; + } + Instruments[nInstr] = pIns; + + fileHeader.ConvertToMPT(*pIns); + + // Translate sample map and find available sample slots + std::vector<SAMPLEINDEX> sampleMap(fileHeader.numSamples); + SAMPLEINDEX maxSmp = 0; + + for(size_t i = 0 + 12; i < 96 + 12; i++) + { + if(pIns->Keyboard[i] >= fileHeader.numSamples) + { + continue; + } + + if(sampleMap[pIns->Keyboard[i]] == 0) + { + // Find slot for this sample + maxSmp = GetNextFreeSample(nInstr, maxSmp + 1); + if(maxSmp != SAMPLEINDEX_INVALID) + { + sampleMap[pIns->Keyboard[i]] = maxSmp; + } + } + pIns->Keyboard[i] = sampleMap[pIns->Keyboard[i]]; + } + + if(m_nSamples < maxSmp) + { + m_nSamples = maxSmp; + } + + std::vector<SampleIO> sampleFlags(fileHeader.numSamples); + + // Read sample headers + for(SAMPLEINDEX i = 0; i < fileHeader.numSamples; i++) + { + XMSample sampleHeader; + if(!file.ReadStruct(sampleHeader) + || !sampleMap[i]) + { + continue; + } + + ModSample &mptSample = Samples[sampleMap[i]]; + sampleHeader.ConvertToMPT(mptSample); + fileHeader.instrument.ApplyAutoVibratoToMPT(mptSample); + mptSample.Convert(MOD_TYPE_XM, GetType()); + if(GetType() != MOD_TYPE_XM && fileHeader.numSamples == 1) + { + // No need to pan that single sample, thank you... + mptSample.uFlags &= ~CHN_PANNING; + } + + mptSample.filename = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + m_szNames[sampleMap[i]] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + + sampleFlags[i] = sampleHeader.GetSampleFormat(); + } + + // Read sample data + for(SAMPLEINDEX i = 0; i < fileHeader.numSamples; i++) + { + if(sampleMap[i]) + { + sampleFlags[i].ReadSample(Samples[sampleMap[i]], file); + Samples[sampleMap[i]].PrecomputeLoops(*this, false); + } + } + + // Read MPT crap + ReadExtendedInstrumentProperties(pIns, file); + pIns->Convert(MOD_TYPE_XM, GetType()); + pIns->Sanitize(GetType()); + return true; +} + + +#ifndef MODPLUG_NO_FILESAVE + +bool CSoundFile::SaveXIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f) const +{ + ModInstrument *pIns = Instruments[nInstr]; + if(pIns == nullptr) + { + return false; + } + + // Create file header + XIInstrumentHeader header; + header.ConvertToXM(*pIns, false); + + const std::vector<SAMPLEINDEX> samples = header.instrument.GetSampleList(*pIns, false); + if(samples.size() > 0 && samples[0] <= GetNumSamples()) + { + // Copy over auto-vibrato settings of first sample + header.instrument.ApplyAutoVibratoToXM(Samples[samples[0]], GetType()); + } + + mpt::IO::Write(f, header); + + std::vector<SampleIO> sampleFlags(samples.size()); + + // XI Sample Headers + for(SAMPLEINDEX i = 0; i < samples.size(); i++) + { + XMSample xmSample; + if(samples[i] <= GetNumSamples()) + { + xmSample.ConvertToXM(Samples[samples[i]], GetType(), false); + } else + { + MemsetZero(xmSample); + } + sampleFlags[i] = xmSample.GetSampleFormat(); + + mpt::String::WriteBuf(mpt::String::spacePadded, xmSample.name) = m_szNames[samples[i]]; + + mpt::IO::Write(f, xmSample); + } + + // XI Sample Data + for(SAMPLEINDEX i = 0; i < samples.size(); i++) + { + if(samples[i] <= GetNumSamples()) + { + sampleFlags[i].WriteSample(f, Samples[samples[i]]); + } + } + + // Write 'MPTX' extension tag + mpt::IO::WriteText(f, "XTPM"); + WriteInstrumentHeaderStructOrField(pIns, f); // Write full extended header. + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +// Read first sample from XI file into a sample slot +bool CSoundFile::ReadXISample(SAMPLEINDEX nSample, FileReader &file) +{ + file.Rewind(); + + XIInstrumentHeader fileHeader; + if(!file.ReadStruct(fileHeader) + || !file.CanRead(sizeof(XMSample)) + || memcmp(fileHeader.signature, "Extended Instrument: ", 21) + || fileHeader.version != XIInstrumentHeader::fileVersion + || fileHeader.eof != 0x1A + || fileHeader.numSamples == 0) + { + return false; + } + + if(m_nSamples < nSample) + { + m_nSamples = nSample; + } + + uint16 numSamples = fileHeader.numSamples; + FileReader::off_t samplePos = sizeof(XIInstrumentHeader) + numSamples * sizeof(XMSample); + // Preferrably read the middle-C sample + auto sample = fileHeader.instrument.sampleMap[48]; + if(sample >= fileHeader.numSamples) + sample = 0; + XMSample sampleHeader; + while(sample--) + { + file.ReadStruct(sampleHeader); + samplePos += sampleHeader.length; + } + file.ReadStruct(sampleHeader); + // Gotta skip 'em all! + file.Seek(samplePos); + + DestroySampleThreadsafe(nSample); + + ModSample &mptSample = Samples[nSample]; + sampleHeader.ConvertToMPT(mptSample); + if(GetType() != MOD_TYPE_XM) + { + // No need to pan that single sample, thank you... + mptSample.uFlags.reset(CHN_PANNING); + } + fileHeader.instrument.ApplyAutoVibratoToMPT(mptSample); + mptSample.Convert(MOD_TYPE_XM, GetType()); + + mptSample.filename = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name); + + // Read sample data + sampleHeader.GetSampleFormat().ReadSample(Samples[nSample], file); + Samples[nSample].PrecomputeLoops(*this, false); + + return true; +} + + +/////////////// +// Apple CAF // + + +struct CAFFileHeader +{ + uint32be mFileType; + uint16be mFileVersion; + uint16be mFileFlags; +}; + +MPT_BINARY_STRUCT(CAFFileHeader, 8) + + +struct CAFChunkHeader +{ + uint32be mChunkType; + int64be mChunkSize; +}; + +MPT_BINARY_STRUCT(CAFChunkHeader, 12) + + +struct CAFChunk +{ + enum ChunkIdentifiers + { + iddesc = MagicBE("desc"), + iddata = MagicBE("data"), + idstrg = MagicBE("strg"), + idinfo = MagicBE("info") + }; + + CAFChunkHeader header; + + FileReader::off_t GetLength() const + { + int64 length = header.mChunkSize; + if(length == -1) + { + length = std::numeric_limits<int64>::max(); // spec + } + if(length < 0) + { + length = std::numeric_limits<int64>::max(); // heuristic + } + return mpt::saturate_cast<FileReader::off_t>(length); + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(header.mChunkType.get()); + } +}; + +MPT_BINARY_STRUCT(CAFChunk, 12) + + +enum { + CAFkAudioFormatLinearPCM = MagicBE("lpcm"), + CAFkAudioFormatAppleIMA4 = MagicBE("ima4"), + CAFkAudioFormatMPEG4AAC = MagicBE("aac "), + CAFkAudioFormatMACE3 = MagicBE("MAC3"), + CAFkAudioFormatMACE6 = MagicBE("MAC6"), + CAFkAudioFormatULaw = MagicBE("ulaw"), + CAFkAudioFormatALaw = MagicBE("alaw"), + CAFkAudioFormatMPEGLayer1 = MagicBE(".mp1"), + CAFkAudioFormatMPEGLayer2 = MagicBE(".mp2"), + CAFkAudioFormatMPEGLayer3 = MagicBE(".mp3"), + CAFkAudioFormatAppleLossless = MagicBE("alac") +}; + + +enum { + CAFkCAFLinearPCMFormatFlagIsFloat = (1L << 0), + CAFkCAFLinearPCMFormatFlagIsLittleEndian = (1L << 1) +}; + + +struct CAFAudioFormat +{ + float64be mSampleRate; + uint32be mFormatID; + uint32be mFormatFlags; + uint32be mBytesPerPacket; + uint32be mFramesPerPacket; + uint32be mChannelsPerFrame; + uint32be mBitsPerChannel; +}; + +MPT_BINARY_STRUCT(CAFAudioFormat, 32) + + +static void CAFSetTagFromInfoKey(mpt::ustring & dst, const std::map<std::string,std::string> & infoMap, const std::string & key) +{ + auto item = infoMap.find(key); + if(item == infoMap.end()) + { + return; + } + if(item->second.empty()) + { + return; + } + dst = mpt::ToUnicode(mpt::Charset::UTF8, item->second); +} + + +bool CSoundFile::ReadCAFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) +{ + file.Rewind(); + + CAFFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(fileHeader.mFileType != MagicBE("caff")) + { + return false; + } + if(fileHeader.mFileVersion != 1) + { + return false; + } + + auto chunkList = file.ReadChunks<CAFChunk>(0); + + CAFAudioFormat audioFormat; + if(!chunkList.GetChunk(CAFChunk::iddesc).ReadStruct(audioFormat)) + { + return false; + } + if(audioFormat.mSampleRate <= 0.0) + { + return false; + } + if(audioFormat.mChannelsPerFrame == 0) + { + return false; + } + if(audioFormat.mChannelsPerFrame > 2) + { + return false; + } + + if(!mpt::in_range<uint32>(mpt::saturate_round<int64>(audioFormat.mSampleRate))) + { + return false; + } + uint32 sampleRate = static_cast<uint32>(mpt::saturate_round<int64>(audioFormat.mSampleRate)); + if(sampleRate <= 0) + { + return false; + } + + SampleIO sampleIO; + if(audioFormat.mFormatID == CAFkAudioFormatLinearPCM) + { + if(audioFormat.mFramesPerPacket != 1) + { + return false; + } + if(audioFormat.mBytesPerPacket == 0) + { + return false; + } + if(audioFormat.mBitsPerChannel == 0) + { + return false; + } + if(audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsFloat) + { + if(audioFormat.mBitsPerChannel != 32 && audioFormat.mBitsPerChannel != 64) + { + return false; + } + if(audioFormat.mBytesPerPacket != audioFormat.mChannelsPerFrame * audioFormat.mBitsPerChannel/8) + { + return false; + } + } + if(audioFormat.mBytesPerPacket % audioFormat.mChannelsPerFrame != 0) + { + return false; + } + if(audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 1 + && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 2 + && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 3 + && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 4 + && audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame != 8 + ) + { + return false; + } + SampleIO::Channels channels = (audioFormat.mChannelsPerFrame == 2) ? SampleIO::stereoInterleaved : SampleIO::mono; + SampleIO::Endianness endianness = (audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsLittleEndian) ? SampleIO::littleEndian : SampleIO::bigEndian; + SampleIO::Encoding encoding = (audioFormat.mFormatFlags & CAFkCAFLinearPCMFormatFlagIsFloat) ? SampleIO::floatPCM : SampleIO::signedPCM; + SampleIO::Bitdepth bitdepth = static_cast<SampleIO::Bitdepth>((audioFormat.mBytesPerPacket / audioFormat.mChannelsPerFrame) * 8); + sampleIO = SampleIO(bitdepth, channels, endianness, encoding); + } else + { + return false; + } + + if(mayNormalize) + { + sampleIO.MayNormalize(); + } + + /* + std::map<uint32, std::string> stringMap; // UTF-8 + if(chunkList.ChunkExists(CAFChunk::idstrg)) + { + FileReader stringsChunk = chunkList.GetChunk(CAFChunk::idstrg); + uint32 numEntries = stringsChunk.ReadUint32BE(); + if(stringsChunk.Skip(12 * numEntries)) + { + FileReader stringData = stringsChunk.ReadChunk(stringsChunk.BytesLeft()); + stringsChunk.Seek(4); + for(uint32 entry = 0; entry < numEntries && stringsChunk.CanRead(12); entry++) + { + uint32 stringID = stringsChunk.ReadUint32BE(); + int64 offset = stringsChunk.ReadIntBE<int64>(); + if(offset >= 0 && mpt::in_range<FileReader::off_t>(offset)) + { + stringData.Seek(mpt::saturate_cast<FileReader::off_t>(offset)); + std::string str; + if(stringData.ReadNullString(str)) + { + stringMap[stringID] = str; + } + } + } + } + } + */ + + std::map<std::string, std::string> infoMap; // UTF-8 + if(chunkList.ChunkExists(CAFChunk::idinfo)) + { + FileReader informationChunk = chunkList.GetChunk(CAFChunk::idinfo); + uint32 numEntries = informationChunk.ReadUint32BE(); + for(uint32 entry = 0; entry < numEntries && informationChunk.CanRead(2); entry++) + { + std::string key; + std::string value; + if(!informationChunk.ReadNullString(key)) + { + break; + } + if(!informationChunk.ReadNullString(value)) + { + break; + } + if(!key.empty() && !value.empty()) + { + infoMap[key] = value; + } + } + } + FileTags tags; + CAFSetTagFromInfoKey(tags.bpm, infoMap, "tempo"); + //CAFSetTagFromInfoKey(void, infoMap, "key signature"); + //CAFSetTagFromInfoKey(void, infoMap, "time signature"); + CAFSetTagFromInfoKey(tags.artist, infoMap, "artist"); + CAFSetTagFromInfoKey(tags.album, infoMap, "album"); + CAFSetTagFromInfoKey(tags.trackno, infoMap, "track number"); + CAFSetTagFromInfoKey(tags.year, infoMap, "year"); + //CAFSetTagFromInfoKey(void, infoMap, "composer"); + //CAFSetTagFromInfoKey(void, infoMap, "lyricist"); + CAFSetTagFromInfoKey(tags.genre, infoMap, "genre"); + CAFSetTagFromInfoKey(tags.title, infoMap, "title"); + //CAFSetTagFromInfoKey(void, infoMap, "recorded date"); + CAFSetTagFromInfoKey(tags.comments, infoMap, "comments"); + //CAFSetTagFromInfoKey(void, infoMap, "copyright"); + //CAFSetTagFromInfoKey(void, infoMap, "source encoder"); + CAFSetTagFromInfoKey(tags.encoder, infoMap, "encoding application"); + //CAFSetTagFromInfoKey(void, infoMap, "nominal bit rate"); + //CAFSetTagFromInfoKey(void, infoMap, "channel layout"); + //CAFSetTagFromInfoKey(tags.url, infoMap, void); + + if(!chunkList.ChunkExists(CAFChunk::iddata)) + { + return false; + } + FileReader dataChunk = chunkList.GetChunk(CAFChunk::iddata); + dataChunk.Skip(4); // edit count + FileReader audioData = dataChunk.ReadChunk(dataChunk.BytesLeft()); + + SmpLength length = mpt::saturate_cast<SmpLength>((audioData.GetLength() / audioFormat.mBytesPerPacket) * audioFormat.mFramesPerPacket); + + ModSample &mptSample = Samples[nSample]; + DestroySampleThreadsafe(nSample); + mptSample.Initialize(); + mptSample.nLength = length; + mptSample.nC5Speed = sampleRate; + + sampleIO.ReadSample(mptSample, audioData); + + m_szNames[nSample] = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags)); + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + + return true; + +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// AIFF File I/O + +// AIFF header +struct AIFFHeader +{ + char magic[4]; // FORM + uint32be length; // Size of the file, not including magic and length + char type[4]; // AIFF or AIFC +}; + +MPT_BINARY_STRUCT(AIFFHeader, 12) + + +// General IFF Chunk header +struct AIFFChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idCOMM = MagicBE("COMM"), + idSSND = MagicBE("SSND"), + idINST = MagicBE("INST"), + idMARK = MagicBE("MARK"), + idNAME = MagicBE("NAME"), + }; + + uint32be id; // See ChunkIdentifiers + uint32be length; // Chunk size without header + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(AIFFChunk, 8) + + +// "Common" chunk (in AIFC, a compression ID and compression name follows this header, but apart from that it's identical) +struct AIFFCommonChunk +{ + uint16be numChannels; + uint32be numSampleFrames; + uint16be sampleSize; + uint8be sampleRate[10]; // Sample rate in 80-Bit floating point + + // Convert sample rate to integer + uint32 GetSampleRate() const + { + uint32 mantissa = (sampleRate[2] << 24) | (sampleRate[3] << 16) | (sampleRate[4] << 8) | (sampleRate[5] << 0); + uint32 last = 0; + uint8 exp = 30 - sampleRate[1]; + + while(exp--) + { + last = mantissa; + mantissa >>= 1; + } + if(last & 1) mantissa++; + return mantissa; + } +}; + +MPT_BINARY_STRUCT(AIFFCommonChunk, 18) + + +// Sound chunk +struct AIFFSoundChunk +{ + uint32be offset; + uint32be blockSize; +}; + +MPT_BINARY_STRUCT(AIFFSoundChunk, 8) + + +// Marker +struct AIFFMarker +{ + uint16be id; + uint32be position; // Position in sample + uint8be nameLength; // Not counting eventually existing padding byte in name string +}; + +MPT_BINARY_STRUCT(AIFFMarker, 7) + + +// Instrument loop +struct AIFFInstrumentLoop +{ + enum PlayModes + { + noLoop = 0, + loopNormal = 1, + loopBidi = 2, + }; + + uint16be playMode; + uint16be beginLoop; // Marker index + uint16be endLoop; // Marker index +}; + +MPT_BINARY_STRUCT(AIFFInstrumentLoop, 6) + + +struct AIFFInstrumentChunk +{ + uint8be baseNote; + uint8be detune; + uint8be lowNote; + uint8be highNote; + uint8be lowVelocity; + uint8be highVelocity; + uint16be gain; + AIFFInstrumentLoop sustainLoop; + AIFFInstrumentLoop releaseLoop; +}; + +MPT_BINARY_STRUCT(AIFFInstrumentChunk, 20) + + +bool CSoundFile::ReadAIFFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) +{ + file.Rewind(); + + // Verify header + AIFFHeader fileHeader; + if(!file.ReadStruct(fileHeader) + || memcmp(fileHeader.magic, "FORM", 4) + || (memcmp(fileHeader.type, "AIFF", 4) && memcmp(fileHeader.type, "AIFC", 4))) + { + return false; + } + + auto chunks = file.ReadChunks<AIFFChunk>(2); + + // Read COMM chunk + FileReader commChunk(chunks.GetChunk(AIFFChunk::idCOMM)); + AIFFCommonChunk sampleInfo; + if(!commChunk.ReadStruct(sampleInfo)) + { + return false; + } + + // Is this a proper sample? + if(sampleInfo.numSampleFrames == 0 + || sampleInfo.numChannels < 1 || sampleInfo.numChannels > 2 + || sampleInfo.sampleSize < 1 || sampleInfo.sampleSize > 64) + { + return false; + } + + // Read compression type in AIFF-C files. + uint8 compression[4] = { 'N', 'O', 'N', 'E' }; + SampleIO::Endianness endian = SampleIO::bigEndian; + if(!memcmp(fileHeader.type, "AIFC", 4)) + { + if(!commChunk.ReadArray(compression)) + { + return false; + } + if(!memcmp(compression, "twos", 4)) + { + endian = SampleIO::littleEndian; + } + } + + // Read SSND chunk + FileReader soundChunk(chunks.GetChunk(AIFFChunk::idSSND)); + AIFFSoundChunk sampleHeader; + if(!soundChunk.ReadStruct(sampleHeader)) + { + return false; + } + + SampleIO::Bitdepth bitDepth; + switch((sampleInfo.sampleSize - 1) / 8) + { + default: + case 0: bitDepth = SampleIO::_8bit; break; + case 1: bitDepth = SampleIO::_16bit; break; + case 2: bitDepth = SampleIO::_24bit; break; + case 3: bitDepth = SampleIO::_32bit; break; + case 7: bitDepth = SampleIO::_64bit; break; + } + + SampleIO sampleIO(bitDepth, + (sampleInfo.numChannels == 2) ? SampleIO::stereoInterleaved : SampleIO::mono, + endian, + SampleIO::signedPCM); + + if(!memcmp(compression, "fl32", 4) || !memcmp(compression, "FL32", 4) || !memcmp(compression, "fl64", 4) || !memcmp(compression, "FL64", 4)) + { + sampleIO |= SampleIO::floatPCM; + } else if(!memcmp(compression, "alaw", 4) || !memcmp(compression, "ALAW", 4)) + { + sampleIO |= SampleIO::aLaw; + sampleIO |= SampleIO::_16bit; + } else if(!memcmp(compression, "ulaw", 4) || !memcmp(compression, "ULAW", 4)) + { + sampleIO |= SampleIO::uLaw; + sampleIO |= SampleIO::_16bit; + } else if(!memcmp(compression, "raw ", 4)) + { + sampleIO |= SampleIO::unsignedPCM; + } + + if(mayNormalize) + { + sampleIO.MayNormalize(); + } + + if(soundChunk.CanRead(sampleHeader.offset)) + { + soundChunk.Skip(sampleHeader.offset); + } + + ModSample &mptSample = Samples[nSample]; + DestroySampleThreadsafe(nSample); + mptSample.Initialize(); + mptSample.nLength = sampleInfo.numSampleFrames; + mptSample.nC5Speed = sampleInfo.GetSampleRate(); + + sampleIO.ReadSample(mptSample, soundChunk); + + // Read MARK and INST chunk to extract sample loops + FileReader markerChunk(chunks.GetChunk(AIFFChunk::idMARK)); + AIFFInstrumentChunk instrHeader; + if(markerChunk.IsValid() && chunks.GetChunk(AIFFChunk::idINST).ReadStruct(instrHeader)) + { + uint16 numMarkers = markerChunk.ReadUint16BE(); + + std::vector<AIFFMarker> markers; + markers.reserve(numMarkers); + for(size_t i = 0; i < numMarkers; i++) + { + AIFFMarker marker; + if(!markerChunk.ReadStruct(marker)) + { + break; + } + markers.push_back(marker); + markerChunk.Skip(marker.nameLength + ((marker.nameLength % 2u) == 0 ? 1 : 0)); + } + + if(instrHeader.sustainLoop.playMode != AIFFInstrumentLoop::noLoop) + { + mptSample.uFlags.set(CHN_SUSTAINLOOP); + mptSample.uFlags.set(CHN_PINGPONGSUSTAIN, instrHeader.sustainLoop.playMode == AIFFInstrumentLoop::loopBidi); + } + + if(instrHeader.releaseLoop.playMode != AIFFInstrumentLoop::noLoop) + { + mptSample.uFlags.set(CHN_LOOP); + mptSample.uFlags.set(CHN_PINGPONGLOOP, instrHeader.releaseLoop.playMode == AIFFInstrumentLoop::loopBidi); + } + + // Read markers + for(const auto &m : markers) + { + if(m.id == instrHeader.sustainLoop.beginLoop) + mptSample.nSustainStart = m.position; + if(m.id == instrHeader.sustainLoop.endLoop) + mptSample.nSustainEnd = m.position; + if(m.id == instrHeader.releaseLoop.beginLoop) + mptSample.nLoopStart = m.position; + if(m.id == instrHeader.releaseLoop.endLoop) + mptSample.nLoopEnd = m.position; + } + mptSample.SanitizeLoops(); + } + + // Extract sample name + FileReader nameChunk(chunks.GetChunk(AIFFChunk::idNAME)); + if(nameChunk.IsValid()) + { + nameChunk.ReadString<mpt::String::spacePadded>(m_szNames[nSample], nameChunk.GetLength()); + } else + { + m_szNames[nSample] = ""; + } + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + return true; +} + + +static bool AUIsAnnotationLineWithField(const std::string &line) +{ + std::size_t pos = line.find('='); + if(pos == std::string::npos) + { + return false; + } + if(pos == 0) + { + return false; + } + const auto field = std::string_view(line).substr(0, pos); + // Scan for invalid chars + for(auto c : field) + { + if(!mpt::is_in_range(c, 'a', 'z') && !mpt::is_in_range(c, 'A', 'Z') && !mpt::is_in_range(c, '0', '9') && c != '-' && c != '_') + { + return false; + } + } + return true; +} + +static std::string AUTrimFieldFromAnnotationLine(const std::string &line) +{ + if(!AUIsAnnotationLineWithField(line)) + { + return line; + } + std::size_t pos = line.find('='); + return line.substr(pos + 1); +} + +static std::string AUGetAnnotationFieldFromLine(const std::string &line) +{ + if(!AUIsAnnotationLineWithField(line)) + { + return std::string(); + } + std::size_t pos = line.find('='); + return line.substr(0, pos); +} + +bool CSoundFile::ReadAUSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize) +{ + file.Rewind(); + + // Verify header + const auto magic = file.ReadArray<char, 4>(); + const bool bigEndian = !std::memcmp(magic.data(), ".snd", 4); + const bool littleEndian = !std::memcmp(magic.data(), "dns.", 4); + if(!bigEndian && !littleEndian) + return false; + + auto readUint32 = std::bind(bigEndian ? &FileReader::ReadUint32BE : &FileReader::ReadUint32LE, file); + + uint32 dataOffset = readUint32(); // must be divisible by 8 according to spec, however, there are files that ignore this requirement + uint32 dataSize = readUint32(); + uint32 encoding = readUint32(); + uint32 sampleRate = readUint32(); + uint32 channels = readUint32(); + + // According to spec, a minimum 8 byte annotation field after the header fields is required, + // however, there are files in the wild that violate this requirement. + // Thus, check for 24 instead of 32 here. + if(dataOffset < 24) // data offset points inside header + { + return false; + } + + if(channels < 1 || channels > 2) + return false; + + SampleIO sampleIO(SampleIO::_8bit, channels == 1 ? SampleIO::mono : SampleIO::stereoInterleaved, bigEndian ? SampleIO::bigEndian : SampleIO::littleEndian, SampleIO::signedPCM); + switch(encoding) + { + case 1: sampleIO |= SampleIO::_16bit; // u-law + sampleIO |= SampleIO::uLaw; break; + case 2: break; // 8-bit linear PCM + case 3: sampleIO |= SampleIO::_16bit; break; // 16-bit linear PCM + case 4: sampleIO |= SampleIO::_24bit; break; // 24-bit linear PCM + case 5: sampleIO |= SampleIO::_32bit; break; // 32-bit linear PCM + case 6: sampleIO |= SampleIO::_32bit; // 32-bit IEEE floating point + sampleIO |= SampleIO::floatPCM; + break; + case 7: sampleIO |= SampleIO::_64bit; // 64-bit IEEE floating point + sampleIO |= SampleIO::floatPCM; + break; + case 27: sampleIO |= SampleIO::_16bit; // a-law + sampleIO |= SampleIO::aLaw; break; + default: return false; + } + + if(!file.LengthIsAtLeast(dataOffset)) + { + return false; + } + + FileTags tags; + + // This reads annotation metadata as written by OpenMPT, sox, ffmpeg. + // Additionally, we fall back to just reading the whole field as a single comment. + // We only read up to the first \0 byte. + file.Seek(24); + std::string annotation; + file.ReadString<mpt::String::maybeNullTerminated>(annotation, dataOffset - 24); + annotation = mpt::replace(annotation, std::string("\r\n"), std::string("\n")); + annotation = mpt::replace(annotation, std::string("\r"), std::string("\n")); + mpt::Charset charset = mpt::IsUTF8(annotation) ? mpt::Charset::UTF8 : mpt::Charset::ISO8859_1; + const auto lines = mpt::String::Split<std::string>(annotation, "\n"); + bool hasFields = false; + for(const auto &line : lines) + { + if(AUIsAnnotationLineWithField(line)) + { + hasFields = true; + break; + } + } + if(hasFields) + { + std::map<std::string, std::vector<std::string>> linesPerField; + std::string lastField = "comment"; + for(const auto &line : lines) + { + if(AUIsAnnotationLineWithField(line)) + { + lastField = mpt::ToLowerCaseAscii(mpt::trim(AUGetAnnotationFieldFromLine(line))); + } + linesPerField[lastField].push_back(AUTrimFieldFromAnnotationLine(line)); + } + tags.title = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["title" ], std::string("\n"))); + tags.artist = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["artist" ], std::string("\n"))); + tags.album = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["album" ], std::string("\n"))); + tags.trackno = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["track" ], std::string("\n"))); + tags.genre = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["genre" ], std::string("\n"))); + tags.comments = mpt::ToUnicode(charset, mpt::String::Combine(linesPerField["comment"], std::string("\n"))); + } else + { + // Most applications tend to write their own name here, + // thus there is little use in interpreting the string as a title. + annotation = mpt::trim_right(annotation, std::string("\r\n")); + tags.comments = mpt::ToUnicode(charset, annotation); + } + + file.Seek(dataOffset); + + ModSample &mptSample = Samples[nSample]; + DestroySampleThreadsafe(nSample); + mptSample.Initialize(); + SmpLength length = mpt::saturate_cast<SmpLength>(file.BytesLeft()); + if(dataSize != 0xFFFFFFFF) + LimitMax(length, dataSize); + mptSample.nLength = (length * 8u) / (sampleIO.GetEncodedBitsPerSample() * channels); + mptSample.nC5Speed = sampleRate; + m_szNames[nSample] = mpt::ToCharset(GetCharsetInternal(), GetSampleNameFromTags(tags)); + + if(mayNormalize) + { + sampleIO.MayNormalize(); + } + + sampleIO.ReadSample(mptSample, file); + + mptSample.Convert(MOD_TYPE_IT, GetType()); + mptSample.PrecomputeLoops(*this, false); + return true; +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// ITS Samples + + +bool CSoundFile::ReadITSSample(SAMPLEINDEX nSample, FileReader &file, bool rewind) +{ + if(rewind) + { + file.Rewind(); + } + + ITSample sampleHeader; + if(!file.ReadStruct(sampleHeader) + || memcmp(sampleHeader.id, "IMPS", 4)) + { + return false; + } + DestroySampleThreadsafe(nSample); + + ModSample &sample = Samples[nSample]; + file.Seek(sampleHeader.ConvertToMPT(sample)); + m_szNames[nSample] = mpt::String::ReadBuf(mpt::String::spacePaddedNull, sampleHeader.name); + + if(sample.uFlags[CHN_ADLIB]) + { + OPLPatch patch; + file.ReadArray(patch); + sample.SetAdlib(true, patch); + InitOPL(); + if(!SupportsOPL()) + { + AddToLog(LogInformation, U_("OPL instruments are not supported by this format.")); + } + } else if(!sample.uFlags[SMP_KEEPONDISK]) + { + sampleHeader.GetSampleFormat().ReadSample(sample, file); + } else + { + // External sample + size_t strLen; + file.ReadVarInt(strLen); +#ifdef MPT_EXTERNAL_SAMPLES + std::string filenameU8; + file.ReadString<mpt::String::maybeNullTerminated>(filenameU8, strLen); + mpt::PathString filename = mpt::PathString::FromUTF8(filenameU8); + + if(!filename.empty()) + { + if(file.GetOptionalFileName()) + { + filename = filename.RelativePathToAbsolute(file.GetOptionalFileName()->GetPath()); + } + if(!LoadExternalSample(nSample, filename)) + { + AddToLog(LogWarning, U_("Unable to load sample: ") + filename.ToUnicode()); + } + } else + { + sample.uFlags.reset(SMP_KEEPONDISK); + } +#else + file.Skip(strLen); +#endif // MPT_EXTERNAL_SAMPLES + } + + sample.Convert(MOD_TYPE_IT, GetType()); + sample.PrecomputeLoops(*this, false); + return true; +} + + +bool CSoundFile::ReadITISample(SAMPLEINDEX nSample, FileReader &file) +{ + ITInstrument instrumentHeader; + + file.Rewind(); + if(!file.ReadStruct(instrumentHeader) + || memcmp(instrumentHeader.id, "IMPI", 4)) + { + return false; + } + file.Rewind(); + ModInstrument dummy; + ITInstrToMPT(file, dummy, instrumentHeader.trkvers); + // Old SchismTracker versions set nos=0 + const SAMPLEINDEX nsamples = std::max(static_cast<SAMPLEINDEX>(instrumentHeader.nos), *std::max_element(std::begin(dummy.Keyboard), std::end(dummy.Keyboard))); + if(!nsamples) + return false; + + // Preferrably read the middle-C sample + auto sample = dummy.Keyboard[NOTE_MIDDLEC - NOTE_MIN]; + if(sample > 0) + sample--; + else + sample = 0; + file.Seek(file.GetPosition() + sample * sizeof(ITSample)); + return ReadITSSample(nSample, file, false); +} + + +bool CSoundFile::ReadITIInstrument(INSTRUMENTINDEX nInstr, FileReader &file) +{ + ITInstrument instrumentHeader; + SAMPLEINDEX smp = 0; + + file.Rewind(); + if(!file.ReadStruct(instrumentHeader) + || memcmp(instrumentHeader.id, "IMPI", 4)) + { + return false; + } + if(nInstr > GetNumInstruments()) m_nInstruments = nInstr; + + ModInstrument *pIns = new (std::nothrow) ModInstrument(); + if(pIns == nullptr) + { + return false; + } + + DestroyInstrument(nInstr, deleteAssociatedSamples); + + Instruments[nInstr] = pIns; + file.Rewind(); + ITInstrToMPT(file, *pIns, instrumentHeader.trkvers); + // Old SchismTracker versions set nos=0 + const SAMPLEINDEX nsamples = std::max(static_cast<SAMPLEINDEX>(instrumentHeader.nos), *std::max_element(std::begin(pIns->Keyboard), std::end(pIns->Keyboard))); + + // 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 extraOffset = file.GetPosition(); + + // Reading Samples + std::vector<SAMPLEINDEX> samplemap(nsamples, 0); + for(SAMPLEINDEX i = 0; i < nsamples; i++) + { + smp = GetNextFreeSample(nInstr, smp + 1); + if(smp == SAMPLEINDEX_INVALID) break; + samplemap[i] = smp; + const FileReader::off_t offset = file.GetPosition(); + if(!ReadITSSample(smp, file, false)) + smp--; + extraOffset = std::max(extraOffset, file.GetPosition()); + file.Seek(offset + sizeof(ITSample)); + } + if(GetNumSamples() < smp) m_nSamples = smp; + + // Adjust sample assignment + for(auto &sample : pIns->Keyboard) + { + if(sample > 0 && sample <= nsamples) + { + sample = samplemap[sample - 1]; + } + } + + if(file.Seek(extraOffset)) + { + // Read MPT crap + ReadExtendedInstrumentProperties(pIns, file); + } + + pIns->Convert(MOD_TYPE_IT, GetType()); + pIns->Sanitize(GetType()); + + return true; +} + + +#ifndef MODPLUG_NO_FILESAVE + +bool CSoundFile::SaveITIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool compress, bool allowExternal) const +{ + ITInstrument iti; + ModInstrument *pIns = Instruments[nInstr]; + + if((!pIns) || (filename.empty() && allowExternal)) return false; + + auto instSize = iti.ConvertToIT(*pIns, false, *this); + + // Create sample assignment table + std::vector<SAMPLEINDEX> smptable; + std::vector<uint8> smpmap(GetNumSamples(), 0); + for(size_t i = 0; i < NOTE_MAX; i++) + { + const SAMPLEINDEX smp = pIns->Keyboard[i]; + if(smp && smp <= GetNumSamples()) + { + if(!smpmap[smp - 1]) + { + // We haven't considered this sample yet. + smptable.push_back(smp); + smpmap[smp - 1] = static_cast<uint8>(smptable.size()); + } + iti.keyboard[i * 2 + 1] = smpmap[smp - 1]; + } else + { + iti.keyboard[i * 2 + 1] = 0; + } + } + iti.nos = static_cast<uint8>(smptable.size()); + smpmap.clear(); + + uint32 filePos = instSize; + mpt::IO::WritePartial(f, iti, instSize); + + filePos += mpt::saturate_cast<uint32>(smptable.size() * sizeof(ITSample)); + + // Writing sample headers + data + std::vector<SampleIO> sampleFlags; + for(auto smp : smptable) + { + ITSample itss; + itss.ConvertToIT(Samples[smp], GetType(), compress, compress, allowExternal); + const bool isExternal = itss.cvt == ITSample::cvtExternalSample; + + mpt::String::WriteBuf(mpt::String::nullTerminated, itss.name) = m_szNames[smp]; + + itss.samplepointer = filePos; + mpt::IO::Write(f, itss); + + // Write sample + auto curPos = mpt::IO::TellWrite(f); + mpt::IO::SeekAbsolute(f, filePos); + if(!isExternal) + { + filePos += mpt::saturate_cast<uint32>(itss.GetSampleFormat(0x0214).WriteSample(f, Samples[smp])); + } 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)) + { + filePos += mpt::saturate_cast<uint32>(intBytes + strSize); + mpt::IO::WriteRaw(f, filenameU8.data(), strSize); + } +#endif // MPT_EXTERNAL_SAMPLES + } + mpt::IO::SeekAbsolute(f, curPos); + } + + mpt::IO::SeekEnd(f); + // Write 'MPTX' extension tag + mpt::IO::WriteRaw(f, "XTPM", 4); + WriteInstrumentHeaderStructOrField(pIns, f); // Write full extended header. + + return true; +} + +#endif // MODPLUG_NO_FILESAVE + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// 8SVX / 16SVX / MAUD Samples + +// IFF File Header +struct IFFHeader +{ + char form[4]; // "FORM" + uint32be size; + char magic[4]; // "8SVX", "16SV", "MAUD" +}; + +MPT_BINARY_STRUCT(IFFHeader, 12) + + +// General IFF Chunk header +struct IFFChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + // 8SVX / 16SV + idVHDR = MagicBE("VHDR"), + idBODY = MagicBE("BODY"), + idCHAN = MagicBE("CHAN"), + + // MAUD + idMHDR = MagicBE("MHDR"), + idMDAT = MagicBE("MDAT"), + + idNAME = MagicBE("NAME"), + }; + + uint32be id; // See ChunkIdentifiers + uint32be length; // Chunk size without header + + size_t GetLength() const + { + if(length == 0) // Broken files + return std::numeric_limits<size_t>::max(); + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(IFFChunk, 8) + + +struct IFFSampleHeader +{ + uint32be oneShotHiSamples; // Samples in the high octave 1-shot part + uint32be repeatHiSamples; // Samples in the high octave repeat part + uint32be samplesPerHiCycle; // Samples/cycle in high octave, else 0 + uint16be samplesPerSec; // Data sampling rate + uint8be octave; // Octaves of waveforms + uint8be compression; // Data compression technique used + uint32be volume; +}; + +MPT_BINARY_STRUCT(IFFSampleHeader, 20) + + +bool CSoundFile::ReadIFFSample(SAMPLEINDEX nSample, FileReader &file) +{ + file.Rewind(); + + IFFHeader fileHeader; + if(!file.ReadStruct(fileHeader) + || memcmp(fileHeader.form, "FORM", 4) + || (memcmp(fileHeader.magic, "8SVX", 4) && memcmp(fileHeader.magic, "16SV", 4) && memcmp(fileHeader.magic, "MAUD", 4))) + { + return false; + } + + const auto chunks = file.ReadChunks<IFFChunk>(2); + FileReader sampleData; + + SampleIO sampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM); + uint32 numSamples = 0, sampleRate = 0, loopStart = 0, loopLength = 0, volume = 0; + + if(!memcmp(fileHeader.magic, "MAUD", 4)) + { + FileReader mhdrChunk = chunks.GetChunk(IFFChunk::idMHDR); + sampleData = chunks.GetChunk(IFFChunk::idMDAT); + if(!mhdrChunk.LengthIs(32) + || !sampleData.IsValid()) + { + return false; + } + + numSamples = mhdrChunk.ReadUint32BE(); + const uint16 bitsPerSample = mhdrChunk.ReadUint16BE(); + mhdrChunk.Skip(2); // bits per sample after decompression + sampleRate = mhdrChunk.ReadUint32BE(); + const auto [clockDivide, channelInformation, numChannels, compressionType] = mhdrChunk.ReadArray<uint16be, 4>(); + if(!clockDivide) + return false; + else + sampleRate /= clockDivide; + + if(numChannels != (channelInformation + 1)) + return false; + if(numChannels == 2) + sampleIO |= SampleIO::stereoInterleaved; + + if(bitsPerSample == 8 && compressionType == 0) + sampleIO |= SampleIO::unsignedPCM; + else if(bitsPerSample == 8 && compressionType == 2) + sampleIO |= SampleIO::aLaw; + else if(bitsPerSample == 8 && compressionType == 3) + sampleIO |= SampleIO::uLaw; + else if(bitsPerSample == 16 && compressionType == 0) + sampleIO |= SampleIO::_16bit; + else + return false; + } else + { + FileReader vhdrChunk = chunks.GetChunk(IFFChunk::idVHDR); + FileReader chanChunk = chunks.GetChunk(IFFChunk::idCHAN); + sampleData = chunks.GetChunk(IFFChunk::idBODY); + IFFSampleHeader sampleHeader; + if(!sampleData.IsValid() + || !vhdrChunk.IsValid() + || !vhdrChunk.ReadStruct(sampleHeader)) + { + return false; + } + + const uint8 bytesPerSample = memcmp(fileHeader.magic, "8SVX", 4) ? 2 : 1; + const uint8 numChannels = chanChunk.ReadUint32BE() == 6 ? 2 : 1; + const uint8 bytesPerFrame = bytesPerSample * numChannels; + + // While this is an Amiga format, the 16SV version appears to be only used on PC, and only with little-endian sample data. + if(bytesPerSample == 2) + sampleIO = SampleIO(SampleIO::_16bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM); + if(numChannels == 2) + sampleIO |= SampleIO::stereoSplit; + + loopStart = sampleHeader.oneShotHiSamples / bytesPerFrame; + loopLength = sampleHeader.repeatHiSamples / bytesPerFrame; + sampleRate = sampleHeader.samplesPerSec; + volume = sampleHeader.volume; + numSamples = mpt::saturate_cast<SmpLength>(sampleData.GetLength() / bytesPerFrame); + } + + DestroySampleThreadsafe(nSample); + ModSample &sample = Samples[nSample]; + sample.Initialize(); + sample.nLength = numSamples; + sample.nLoopStart = loopStart; + sample.nLoopEnd = sample.nLoopStart + loopLength; + if((sample.nLoopStart + 4 < sample.nLoopEnd) && (sample.nLoopEnd <= sample.nLength)) + sample.uFlags.set(CHN_LOOP); + + sample.nC5Speed = sampleRate; + if(!sample.nC5Speed) + sample.nC5Speed = 22050; + + sample.nVolume = static_cast<uint16>(volume / 256); + if(!sample.nVolume || sample.nVolume > 256) + sample.nVolume = 256; + + sample.Convert(MOD_TYPE_IT, GetType()); + + FileReader nameChunk = chunks.GetChunk(IFFChunk::idNAME); + if(nameChunk.IsValid()) + nameChunk.ReadString<mpt::String::maybeNullTerminated>(m_szNames[nSample], nameChunk.GetLength()); + else + m_szNames[nSample] = ""; + + sampleIO.ReadSample(sample, sampleData); + sample.PrecomputeLoops(*this, false); + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.cpp new file mode 100644 index 00000000..60e1403b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.cpp @@ -0,0 +1,1030 @@ +/* + * SampleIO.cpp + * ------------ + * Purpose: Central code for reading and writing samples. Create your SampleIO object and have a go at the ReadSample and WriteSample functions! + * Notes : Not all combinations of possible sample format combinations are implemented, especially for WriteSample. + * Using the existing generic functions, it should be quite easy to extend the code, though. + * 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 "SampleIO.h" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "SampleCopy.h" +#include "SampleNormalize.h" +#include "ModSampleCopy.h" +#include "ITCompression.h" +#ifndef MODPLUG_NO_FILESAVE +#include "../common/mptFileIO.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "mpt/io_write/buffer.hpp" +#endif +#include "BitReader.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Read a sample from memory +size_t SampleIO::ReadSample(ModSample &sample, FileReader &file) const +{ + if(!file.IsValid()) + { + return 0; + } + + LimitMax(sample.nLength, MAX_SAMPLE_LENGTH); + + FileReader::off_t bytesRead = 0; // Amount of memory that has been read from file + + FileReader::off_t filePosition = file.GetPosition(); + const std::byte * sourceBuf = nullptr; + FileReader::PinnedView restrictedSampleDataView; + FileReader::off_t fileSize = 0; + if(UsesFileReaderForDecoding()) + { + sourceBuf = nullptr; + fileSize = file.BytesLeft(); + } else if(!IsVariableLengthEncoded()) + { + restrictedSampleDataView = file.GetPinnedView(CalculateEncodedSize(sample.nLength)); + sourceBuf = restrictedSampleDataView.data(); + fileSize = restrictedSampleDataView.size(); + if(sourceBuf == nullptr) + return 0; + } else + { + MPT_ASSERT_NOTREACHED(); + } + + if(!IsVariableLengthEncoded() && sample.nLength > 0x40000) + { + // Limit sample length to available bytes in file to avoid excessive memory allocation. + // However, for ProTracker MODs we need to support samples exceeding the end of file + // (see the comment about MOD.shorttune2 in Load_mod.cpp), so as a semi-arbitrary threshold, + // we do not apply this limit to samples shorter than 256K. + size_t maxLength = fileSize - std::min(GetEncodedHeaderSize(), fileSize); + uint8 bps = GetEncodedBitsPerSample(); + if(bps % 8u != 0) + { + MPT_ASSERT(GetEncoding() == ADPCM && bps == 4); + if(Util::MaxValueOfType(maxLength) / 2u >= maxLength) + maxLength *= 2; + else + maxLength = Util::MaxValueOfType(maxLength); + } else + { + size_t encodedBytesPerSample = GetNumChannels() * GetEncodedBitsPerSample() / 8u; + // Check if we can round up without overflowing + if(Util::MaxValueOfType(maxLength) - maxLength >= (encodedBytesPerSample - 1u)) + maxLength += encodedBytesPerSample - 1u; + else + maxLength = Util::MaxValueOfType(maxLength); + maxLength /= encodedBytesPerSample; + } + LimitMax(sample.nLength, mpt::saturate_cast<SmpLength>(maxLength)); + } else if(GetEncoding() == IT214 || GetEncoding() == IT215 || GetEncoding() == MDL || GetEncoding() == DMF) + { + // In the best case, IT compression represents each sample point as a single bit. + // In practice, there is of course the two-byte header per compressed block and the initial bit width change. + // As a result, if we have a file length of n, we know that the sample can be at most n*8 sample points long. + // For DMF, there are at least two bits per sample, and for MDL at least 5 (so both are worse than IT). + size_t maxLength = fileSize; + uint8 maxSamplesPerByte = 8 / GetNumChannels(); + if(Util::MaxValueOfType(maxLength) / maxSamplesPerByte >= maxLength) + maxLength *= maxSamplesPerByte; + else + maxLength = Util::MaxValueOfType(maxLength); + LimitMax(sample.nLength, mpt::saturate_cast<SmpLength>(maxLength)); + } else if(GetEncoding() == AMS) + { + if(fileSize <= 9) + return 0; + + file.Skip(4); // Target sample size (we already know this) + SmpLength maxLength = std::min(file.ReadUint32LE(), mpt::saturate_cast<uint32>(fileSize)); + file.SkipBack(8); + + // In the best case, every byte triplet can decode to 255 bytes, which is a ratio of exactly 1:85 + if(Util::MaxValueOfType(maxLength) / 85 >= maxLength) + maxLength *= 85; + else + maxLength = Util::MaxValueOfType(maxLength); + LimitMax(sample.nLength, maxLength / (m_bitdepth / 8u)); + } + + if(sample.nLength < 1) + { + return 0; + } + + sample.uFlags.set(CHN_16BIT, GetBitDepth() >= 16); + sample.uFlags.set(CHN_STEREO, GetChannelFormat() != mono); + size_t sampleSize = sample.AllocateSample(); // Target sample size in bytes + + if(sampleSize == 0) + { + sample.nLength = 0; + return 0; + } + + MPT_ASSERT(sampleSize >= sample.GetSampleSizeInBytes()); + + ////////////////////////////////////////////////////// + // Compressed samples + + if(*this == SampleIO(_8bit, mono, littleEndian, ADPCM)) + { + // 4-Bit ADPCM data + int8 compressionTable[16]; // ADPCM Compression LUT + if(file.ReadArray(compressionTable)) + { + size_t readLength = (sample.nLength + 1) / 2; + LimitMax(readLength, file.BytesLeft()); + + const uint8 *inBuf = mpt::byte_cast<const uint8*>(sourceBuf) + sizeof(compressionTable); + int8 *outBuf = sample.sample8(); + int8 delta = 0; + + for(size_t i = readLength; i != 0; i--) + { + delta += compressionTable[*inBuf & 0x0F]; + *(outBuf++) = delta; + delta += compressionTable[(*inBuf >> 4) & 0x0F]; + *(outBuf++) = delta; + inBuf++; + } + bytesRead = sizeof(compressionTable) + readLength; + } + } else if(GetEncoding() == IT214 || GetEncoding() == IT215) + { + // IT 2.14 / 2.15 compressed samples + ITDecompression(file, sample, GetEncoding() == IT215); + bytesRead = file.GetPosition() - filePosition; + } else if(GetEncoding() == AMS && GetChannelFormat() == mono) + { + // AMS compressed samples + file.Skip(4); // Target sample size (we already know this) + uint32 sourceSize = file.ReadUint32LE(); + int8 packCharacter = file.ReadUint8(); + bytesRead += 9; + + FileReader::PinnedView packedDataView = file.ReadPinnedView(sourceSize); + LimitMax(sourceSize, mpt::saturate_cast<uint32>(packedDataView.size())); + bytesRead += sourceSize; + + AMSUnpack(reinterpret_cast<const int8 *>(packedDataView.data()), packedDataView.size(), sample.samplev(), sample.GetSampleSizeInBytes(), packCharacter); + if(sample.uFlags[CHN_16BIT] && !mpt::endian_is_little()) + { + auto p = sample.sample16(); + for(SmpLength length = sample.nLength; length != 0; length--, p++) + { + *p = mpt::bit_cast<int16le>(*p); + } + } + } else if(GetEncoding() == PTM8Dto16 && GetChannelFormat() == mono && GetBitDepth() == 16) + { + // PTM 8-Bit delta to 16-Bit sample + bytesRead = CopyMonoSample<SC::DecodeInt16Delta8>(sample, sourceBuf, fileSize); + } else if(GetEncoding() == MDL && GetChannelFormat() == mono && GetBitDepth() <= 16) + { + // Huffman MDL compressed samples + if(file.CanRead(8) && (fileSize = file.ReadUint32LE()) >= 4) + { + BitReader chunk = file.ReadChunk(fileSize); + bytesRead = chunk.GetLength() + 4; + + uint8 dlt = 0, lowbyte = 0; + const bool is16bit = GetBitDepth() == 16; + try + { + for(SmpLength j = 0; j < sample.nLength; j++) + { + uint8 hibyte; + if(is16bit) + { + lowbyte = static_cast<uint8>(chunk.ReadBits(8)); + } + bool sign = chunk.ReadBits(1) != 0; + if(chunk.ReadBits(1)) + { + hibyte = static_cast<uint8>(chunk.ReadBits(3)); + } else + { + hibyte = 8; + while(!chunk.ReadBits(1)) + { + hibyte += 0x10; + } + hibyte += static_cast<uint8>(chunk.ReadBits(4)); + } + if(sign) + { + hibyte = ~hibyte; + } + dlt += hibyte; + if(!is16bit) + { + sample.sample8()[j] = dlt; + } else + { + sample.sample16()[j] = lowbyte | (dlt << 8); + } + } + } catch(const BitReader::eof &) + { + // Data is not sufficient to decode the whole sample + //AddToLog(LogWarning, "Truncated MDL sample block"); + } + } + } else if(GetEncoding() == DMF && GetChannelFormat() == mono && GetBitDepth() <= 16) + { + // DMF Huffman compression + if(fileSize > 4) + { + bytesRead = DMFUnpack(file, mpt::byte_cast<uint8 *>(sample.sampleb()), sample.GetSampleSizeInBytes()); + } + } else if((GetEncoding() == uLaw || GetEncoding() == aLaw) && GetBitDepth() == 16 && (GetChannelFormat() == mono || GetChannelFormat() == stereoInterleaved)) + { + + SmpLength readLength = sample.nLength * GetNumChannels(); + LimitMax(readLength, mpt::saturate_cast<SmpLength>(fileSize)); + bytesRead = readLength; + + const std::byte *inBuf = sourceBuf; + int16 *outBuf = sample.sample16(); + + if(GetEncoding() == uLaw) + { + SC::DecodeInt16uLaw conv; + while(readLength--) + { + *(outBuf++) = conv(inBuf++); + } + } else + { + SC::DecodeInt16ALaw conv; + while(readLength--) + { + *(outBuf++) = conv(inBuf++); + } + } + } + + + ///////////////////////// + // Uncompressed samples + + ////////////////////////////////////////////////////// + // 8-Bit / Mono / PCM + else if(GetBitDepth() == 8 && GetChannelFormat() == mono) + { + switch(GetEncoding()) + { + case signedPCM: // 8-Bit / Mono / Signed / PCM + bytesRead = CopyMonoSample<SC::DecodeInt8>(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 8-Bit / Mono / Unsigned / PCM + bytesRead = CopyMonoSample<SC::DecodeUint8>(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 8-Bit / Mono / Delta / PCM + case MT2: + bytesRead = CopyMonoSample<SC::DecodeInt8Delta>(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 8-Bit / Stereo Split / PCM + else if(GetBitDepth() == 8 && GetChannelFormat() == stereoSplit) + { + switch(GetEncoding()) + { + case signedPCM: // 8-Bit / Stereo Split / Signed / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt8>(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 8-Bit / Stereo Split / Unsigned / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeUint8>(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 8-Bit / Stereo Split / Delta / PCM + case MT2: // same as deltaPCM, but right channel is stored as a difference from the left channel + bytesRead = CopyStereoSplitSample<SC::DecodeInt8Delta>(sample, sourceBuf, fileSize); + if(GetEncoding() == MT2) + { + for(int8 *p = sample.sample8(), *pEnd = p + sample.nLength * 2; p < pEnd; p += 2) + { + p[1] = static_cast<int8>(static_cast<uint8>(p[0]) + static_cast<uint8>(p[1])); + } + } + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 8-Bit / Stereo Interleaved / PCM + else if(GetBitDepth() == 8 && GetChannelFormat() == stereoInterleaved) + { + switch(GetEncoding()) + { + case signedPCM: // 8-Bit / Stereo Interleaved / Signed / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt8>(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 8-Bit / Stereo Interleaved / Unsigned / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeUint8>(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 8-Bit / Stereo Interleaved / Delta / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt8Delta>(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Mono / Little Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == mono && GetEndianness() == littleEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Stereo Interleaved / Signed / PCM + bytesRead = CopyMonoSample<SC::DecodeInt16<0, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Stereo Interleaved / Unsigned / PCM + bytesRead = CopyMonoSample<SC::DecodeInt16<0x8000u, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Stereo Interleaved / Delta / PCM + case MT2: + bytesRead = CopyMonoSample<SC::DecodeInt16Delta<littleEndian16> >(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Mono / Big Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == mono && GetEndianness() == bigEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Mono / Signed / PCM + bytesRead = CopyMonoSample<SC::DecodeInt16<0, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Mono / Unsigned / PCM + bytesRead = CopyMonoSample<SC::DecodeInt16<0x8000u, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Mono / Delta / PCM + bytesRead = CopyMonoSample<SC::DecodeInt16Delta<bigEndian16> >(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Stereo Split / Little Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoSplit && GetEndianness() == littleEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Stereo Split / Signed / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt16<0, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Stereo Split / Unsigned / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt16<0x8000u, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Stereo Split / Delta / PCM + case MT2: // same as deltaPCM, but right channel is stored as a difference from the left channel + bytesRead = CopyStereoSplitSample<SC::DecodeInt16Delta<littleEndian16> >(sample, sourceBuf, fileSize); + if(GetEncoding() == MT2) + { + for(int16 *p = sample.sample16(), *pEnd = p + sample.nLength * 2; p < pEnd; p += 2) + { + p[1] = static_cast<int16>(static_cast<uint16>(p[0]) + static_cast<uint16>(p[1])); + } + } + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Stereo Split / Big Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoSplit && GetEndianness() == bigEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Stereo Split / Signed / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt16<0, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Stereo Split / Unsigned / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt16<0x8000u, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Stereo Split / Delta / PCM + bytesRead = CopyStereoSplitSample<SC::DecodeInt16Delta<bigEndian16> >(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Stereo Interleaved / Little Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoInterleaved && GetEndianness() == littleEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Stereo Interleaved / Signed / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16<0, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Stereo Interleaved / Unsigned / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16<0x8000u, littleEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Stereo Interleaved / Delta / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16Delta<littleEndian16> >(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 16-Bit / Stereo Interleaved / Big Endian / PCM + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoInterleaved && GetEndianness() == bigEndian) + { + switch(GetEncoding()) + { + case signedPCM: // 16-Bit / Stereo Interleaved / Signed / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16<0, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case unsignedPCM: // 16-Bit / Stereo Interleaved / Unsigned / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16<0x8000u, bigEndian16> >(sample, sourceBuf, fileSize); + break; + case deltaPCM: // 16-Bit / Stereo Interleaved / Delta / PCM + bytesRead = CopyStereoInterleavedSample<SC::DecodeInt16Delta<bigEndian16> >(sample, sourceBuf, fileSize); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + } + + ////////////////////////////////////////////////////// + // 24-Bit / Signed / Mono / PCM + else if(GetBitDepth() == 24 && GetChannelFormat() == mono && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, bigEndian24> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 24-Bit / Signed / Stereo Interleaved / PCM + else if(GetBitDepth() == 24 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, bigEndian24> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Signed / Mono / PCM + else if(GetBitDepth() == 32 && GetChannelFormat() == mono && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, bigEndian32> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Signed / Stereo Interleaved / PCM + else if(GetBitDepth() == 32 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, bigEndian32> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 64-Bit / Signed / Mono / PCM + else if(GetBitDepth() == 64 && GetChannelFormat() == mono && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int64>, SC::DecodeInt64<0, littleEndian64> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, int64>, SC::DecodeInt64<0, bigEndian64> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 64-Bit / Signed / Stereo Interleaved / PCM + else if(GetBitDepth() == 64 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int64>, SC::DecodeInt64<0, littleEndian64> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, int64>, SC::DecodeInt64<0, bigEndian64> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Mono / PCM + else if(GetBitDepth() == 32 && GetChannelFormat() == mono && GetEncoding() == floatPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeFloat32<littleEndian32> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeFloat32<bigEndian32> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Stereo Interleaved / PCM + else if(GetBitDepth() == 32 && GetChannelFormat() == stereoInterleaved && GetEncoding() == floatPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeFloat32<littleEndian32> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeFloat32<bigEndian32> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 64-Bit / Float / Mono / PCM + else if(GetBitDepth() == 64 && GetChannelFormat() == mono && GetEncoding() == floatPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, float64>, SC::DecodeFloat64<littleEndian64> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyMonoSample<SC::ConversionChain<SC::Convert<int16, float64>, SC::DecodeFloat64<bigEndian64> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 64-Bit / Float / Stereo Interleaved / PCM + else if(GetBitDepth() == 64 && GetChannelFormat() == stereoInterleaved && GetEncoding() == floatPCM) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, float64>, SC::DecodeFloat64<littleEndian64> > >(sample, sourceBuf, fileSize); + } else + { + bytesRead = CopyStereoInterleavedSample<SC::ConversionChain<SC::Convert<int16, float64>, SC::DecodeFloat64<bigEndian64> > >(sample, sourceBuf, fileSize); + } + } + + ////////////////////////////////////////////////////// + // 24-Bit / Signed / Mono, Stereo Interleaved / PCM + else if(GetBitDepth() == 24 && (GetChannelFormat() == mono || GetChannelFormat() == stereoInterleaved) && GetEncoding() == signedPCMnormalize) + { + // Normalize to 16-Bit + uint32 srcPeak = uint32(1)<<31; + if(GetEndianness() == littleEndian) + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, littleEndian24> > >(sample, sourceBuf, fileSize, &srcPeak); + } else + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, int32>, SC::DecodeInt24<0, bigEndian24> > >(sample, sourceBuf, fileSize, &srcPeak); + } + if(bytesRead && srcPeak != uint32(1)<<31) + { + // Adjust sample volume so we do not affect relative volume of the sample. Normalizing is only done to increase precision. + sample.nGlobalVol = static_cast<uint16>(Clamp(Util::muldivr_unsigned(sample.nGlobalVol, srcPeak, uint32(1)<<31), uint32(1), uint32(64))); + sample.uFlags.set(SMP_MODIFIED); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Signed / Mono, Stereo Interleaved / PCM + else if(GetBitDepth() == 32 && (GetChannelFormat() == mono || GetChannelFormat() == stereoInterleaved) && GetEncoding() == signedPCMnormalize) + { + // Normalize to 16-Bit + uint32 srcPeak = uint32(1)<<31; + if(GetEndianness() == littleEndian) + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, littleEndian32> > >(sample, sourceBuf, fileSize, &srcPeak); + } else + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, int32>, SC::DecodeInt32<0, bigEndian32> > >(sample, sourceBuf, fileSize, &srcPeak); + } + if(bytesRead && srcPeak != uint32(1)<<31) + { + // Adjust sample volume so we do not affect relative volume of the sample. Normalizing is only done to increase precision. + sample.nGlobalVol = static_cast<uint16>(Clamp(Util::muldivr_unsigned(sample.nGlobalVol, srcPeak, uint32(1)<<31), uint32(1), uint32(64))); + sample.uFlags.set(SMP_MODIFIED); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Mono, Stereo Interleaved / PCM + else if(GetBitDepth() == 32 && (GetChannelFormat() == mono || GetChannelFormat() == stereoInterleaved) && GetEncoding() == floatPCMnormalize) + { + // Normalize to 16-Bit + float32 srcPeak = 1.0f; + if(GetEndianness() == littleEndian) + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, float32>, SC::DecodeFloat32<littleEndian32> > >(sample, sourceBuf, fileSize, &srcPeak); + } else + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, float32>, SC::DecodeFloat32<bigEndian32> > >(sample, sourceBuf, fileSize, &srcPeak); + } + if(bytesRead && srcPeak != 1.0f) + { + // Adjust sample volume so we do not affect relative volume of the sample. Normalizing is only done to increase precision. + sample.nGlobalVol = mpt::saturate_round<uint16>(Clamp(sample.nGlobalVol * srcPeak, 1.0f, 64.0f)); + sample.uFlags.set(SMP_MODIFIED); + } + } + + ////////////////////////////////////////////////////// + // 64-Bit / Float / Mono, Stereo Interleaved / PCM + else if(GetBitDepth() == 64 && (GetChannelFormat() == mono || GetChannelFormat() == stereoInterleaved) && GetEncoding() == floatPCMnormalize) + { + // Normalize to 16-Bit + float64 srcPeak = 1.0; + if(GetEndianness() == littleEndian) + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, float64>, SC::DecodeFloat64<littleEndian64> > >(sample, sourceBuf, fileSize, &srcPeak); + } else + { + bytesRead = CopyAndNormalizeSample<SC::NormalizationChain<SC::Convert<int16, float64>, SC::DecodeFloat64<bigEndian64> > >(sample, sourceBuf, fileSize, &srcPeak); + } + if(bytesRead && srcPeak != 1.0) + { + // Adjust sample volume so we do not affect relative volume of the sample. Normalizing is only done to increase precision. + sample.nGlobalVol = mpt::saturate_round<uint16>(Clamp(sample.nGlobalVol * srcPeak, 1.0, 64.0)); + sample.uFlags.set(SMP_MODIFIED); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Mono / PCM / full scale 2^15 + else if(GetBitDepth() == 32 && GetChannelFormat() == mono && GetEncoding() == floatPCM15) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<littleEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<littleEndian32>(1.0f / static_cast<float>(1<<15))) + ); + } else + { + bytesRead = CopyMonoSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<bigEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<bigEndian32>(1.0f / static_cast<float>(1<<15))) + ); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Stereo Interleaved / PCM / full scale 2^15 + else if(GetBitDepth() == 32 && GetChannelFormat() == stereoInterleaved && GetEncoding() == floatPCM15) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<littleEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<littleEndian32>(1.0f / static_cast<float>(1<<15))) + ); + } else + { + bytesRead = CopyStereoInterleavedSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<bigEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<bigEndian32>(1.0f / static_cast<float>(1<<15))) + ); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Stereo Interleaved / PCM / full scale 2^23 + else if(GetBitDepth() == 32 && GetChannelFormat() == mono && GetEncoding() == floatPCM23) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyMonoSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<littleEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<littleEndian32>(1.0f / static_cast<float>(1<<23))) + ); + } else + { + bytesRead = CopyMonoSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<bigEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<bigEndian32>(1.0f / static_cast<float>(1<<23))) + ); + } + } + + ////////////////////////////////////////////////////// + // 32-Bit / Float / Stereo Interleaved / PCM / full scale 2^23 + else if(GetBitDepth() == 32 && GetChannelFormat() == stereoInterleaved && GetEncoding() == floatPCM23) + { + if(GetEndianness() == littleEndian) + { + bytesRead = CopyStereoInterleavedSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<littleEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<littleEndian32>(1.0f / static_cast<float>(1<<23))) + ); + } else + { + bytesRead = CopyStereoInterleavedSample + (sample, sourceBuf, fileSize, + SC::ConversionChain<SC::Convert<int16, float32>, SC::DecodeScaledFloat32<bigEndian32> > + (SC::Convert<int16, float32>(), SC::DecodeScaledFloat32<bigEndian32>(1.0f / static_cast<float>(1<<23))) + ); + } + } + + //////////////// + // Unsupported + else + { + MPT_ASSERT_NOTREACHED(); + } + + MPT_ASSERT(filePosition + bytesRead <= file.GetLength()); + file.Seek(filePosition + bytesRead); + return bytesRead; +} + + +#ifndef MODPLUG_NO_FILESAVE + + +// Write a sample to file +size_t SampleIO::WriteSample(std::ostream &f, const ModSample &sample, SmpLength maxSamples) const +{ + if(sample.uFlags[CHN_ADLIB]) + { + mpt::IO::Write(f, sample.adlib); + return sizeof(sample.adlib); + } + if(!sample.HasSampleData()) + { + return 0; + } + + std::array<std::byte, mpt::IO::BUFFERSIZE_TINY> writeBuffer; + mpt::IO::WriteBuffer<std::ostream> fb{f, mpt::as_span(writeBuffer)}; + + SmpLength numSamples = sample.nLength; + + if(maxSamples && numSamples > maxSamples) + { + numSamples = maxSamples; + } + + std::size_t len = CalculateEncodedSize(numSamples); + + if(GetBitDepth() == 16 && GetChannelFormat() == mono && GetEndianness() == littleEndian && + (GetEncoding() == signedPCM || GetEncoding() == unsignedPCM || GetEncoding() == deltaPCM)) + { + // 16-bit little-endian mono samples + MPT_ASSERT(len == numSamples * 2); + const int16 *const pSample16 = sample.sample16(); + const int16 *p = pSample16; + int s_old = 0; + const int s_ofs = (GetEncoding() == unsignedPCM) ? 0x8000 : 0; + for(SmpLength j = 0; j < numSamples; j++) + { + int s_new = *p; + p++; + if(sample.uFlags[CHN_STEREO]) + { + // Downmix stereo + s_new = (s_new + (*p) + 1) / 2; + p++; + } + if(GetEncoding() == deltaPCM) + { + mpt::IO::Write(fb, mpt::as_le(static_cast<int16>(s_new - s_old))); + s_old = s_new; + } else + { + mpt::IO::Write(fb, mpt::as_le(static_cast<int16>(s_new + s_ofs))); + } + } + } + + else if(GetBitDepth() == 8 && GetChannelFormat() == stereoSplit && + (GetEncoding() == signedPCM || GetEncoding() == unsignedPCM || GetEncoding() == deltaPCM)) + { + // 8-bit Stereo samples (not interleaved) + MPT_ASSERT(len == numSamples * 2); + const int8 *const pSample8 = sample.sample8(); + const int s_ofs = (GetEncoding() == unsignedPCM) ? 0x80 : 0; + for (uint32 iCh=0; iCh<2; iCh++) + { + const int8 *p = pSample8 + iCh; + int s_old = 0; + for (SmpLength j = 0; j < numSamples; j++) + { + int s_new = *p; + p += 2; + if (GetEncoding() == deltaPCM) + { + mpt::IO::Write(fb, static_cast<int8>(s_new - s_old)); + s_old = s_new; + } else + { + mpt::IO::Write(fb, static_cast<int8>(s_new + s_ofs)); + } + } + } + } + + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoSplit && GetEndianness() == littleEndian && + (GetEncoding() == signedPCM || GetEncoding() == unsignedPCM || GetEncoding() == deltaPCM)) + { + // 16-bit little-endian Stereo samples (not interleaved) + MPT_ASSERT(len == numSamples * 4); + const int16 *const pSample16 = sample.sample16(); + const int s_ofs = (GetEncoding() == unsignedPCM) ? 0x8000 : 0; + for (uint32 iCh=0; iCh<2; iCh++) + { + const int16 *p = pSample16 + iCh; + int s_old = 0; + for (SmpLength j = 0; j < numSamples; j++) + { + int s_new = *p; + p += 2; + if (GetEncoding() == deltaPCM) + { + mpt::IO::Write(fb, mpt::as_le(static_cast<int16>(s_new - s_old))); + s_old = s_new; + } else + { + mpt::IO::Write(fb, mpt::as_le(static_cast<int16>(s_new + s_ofs))); + } + } + } + } + + else if(GetBitDepth() == 8 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM) + { + // Stereo signed interleaved + MPT_ASSERT(len == numSamples * 2); + const int8 *const pSample8 = sample.sample8(); + mpt::IO::WriteRaw(f, reinterpret_cast<const std::byte*>(pSample8), len); + } + + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM && GetEndianness() == littleEndian) + { + // Stereo signed interleaved + MPT_ASSERT(len == numSamples * 4); + const int16 *const pSample16 = sample.sample16(); + const int16 *p = pSample16; + for(SmpLength j = 0; j < numSamples; j++) + { + mpt::IO::Write(fb, mpt::as_le(p[0])); + mpt::IO::Write(fb, mpt::as_le(p[1])); + p += 2; + } + } + + else if(GetBitDepth() == 16 && GetChannelFormat() == stereoInterleaved && GetEncoding() == signedPCM && GetEndianness() == bigEndian) + { + // Stereo signed interleaved + MPT_ASSERT(len == numSamples * 4); + const int16 *const pSample16 = sample.sample16(); + const int16 *p = pSample16; + for(SmpLength j = 0; j < numSamples; j++) + { + mpt::IO::Write(fb, mpt::as_be(p[0])); + mpt::IO::Write(fb, mpt::as_be(p[1])); + p += 2; + } + } + + else if(GetBitDepth() == 8 && GetChannelFormat() == stereoInterleaved && GetEncoding() == unsignedPCM) + { + // Stereo unsigned interleaved + MPT_ASSERT(len == numSamples * 2); + const int8 *const pSample8 = sample.sample8(); + for(SmpLength j = 0; j < numSamples * 2; j++) + { + mpt::IO::Write(fb, static_cast<int8>(static_cast<uint8>(pSample8[j]) + 0x80)); + } + } + + else if(GetEncoding() == IT214 || GetEncoding() == IT215) + { + // IT2.14-encoded samples + ITCompression its(sample, GetEncoding() == IT215, &f, numSamples); + len = its.GetCompressedSize(); + } + + // Default: assume 8-bit PCM data + else + { + MPT_ASSERT(GetBitDepth() == 8); + MPT_ASSERT(len == numSamples); + if(sample.uFlags[CHN_16BIT]) + { + const int16 *p = sample.sample16(); + int s_old = 0; + const int s_ofs = (GetEncoding() == unsignedPCM) ? 0x80 : 0; + for(SmpLength j = 0; j < numSamples; j++) + { + int s_new = mpt::rshift_signed(*p, 8); + p++; + if(sample.uFlags[CHN_STEREO]) + { + s_new = (s_new + mpt::rshift_signed(*p, 8) + 1) / 2; + p++; + } + if(GetEncoding() == deltaPCM) + { + mpt::IO::Write(fb, static_cast<int8>(s_new - s_old)); + s_old = s_new; + } else + { + mpt::IO::Write(fb, static_cast<int8>(s_new + s_ofs)); + } + } + } else + { + const int8 *const pSample8 = sample.sample8(); + const int8 *p = pSample8; + int s_old = 0; + const int s_ofs = (GetEncoding() == unsignedPCM) ? 0x80 : 0; + for(SmpLength j = 0; j < numSamples; j++) + { + int s_new = *p; + p++; + if(sample.uFlags[CHN_STEREO]) + { + s_new = (s_new + (static_cast<int>(*p)) + 1) / 2; + p++; + } + if(GetEncoding() == deltaPCM) + { + mpt::IO::Write(fb, static_cast<int8>(s_new - s_old)); + s_old = s_new; + } else + { + mpt::IO::Write(fb, static_cast<int8>(s_new + s_ofs)); + } + } + } + } + + return len; +} + + +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.h b/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.h new file mode 100644 index 00000000..416a6035 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleIO.h @@ -0,0 +1,256 @@ +/* + * SampleIO.h + * ---------- + * Purpose: Central code for reading and writing samples. Create your SampleIO object and have a go at the ReadSample and WriteSample functions! + * Notes : Not all combinations of possible sample format combinations are implemented, especially for WriteSample. + * Using the existing generic sample conversion functors in SampleFormatConverters.h, it should be quite easy to extend the code, though. + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "../common/FileReaderFwd.h" + + +OPENMPT_NAMESPACE_BEGIN + + +struct ModSample; + +// Sample import / export formats +class SampleIO +{ +public: + // Bits per sample + enum Bitdepth : uint8 + { + _8bit = 8, + _16bit = 16, + _24bit = 24, + _32bit = 32, + _64bit = 64, + }; + + // Number of channels + channel format + enum Channels : uint8 + { + mono = 1, + stereoInterleaved, // LRLRLR... + stereoSplit, // LLL...RRR... + }; + + // Sample byte order + enum Endianness : uint8 + { + littleEndian = 0, + bigEndian = 1, + }; + + // Sample encoding + enum Encoding : uint8 + { + signedPCM = 0, // Integer PCM, signed + unsignedPCM, // Integer PCM, unsigned + deltaPCM, // Integer PCM, delta-encoded + floatPCM, // Floating point PCM + IT214, // Impulse Tracker 2.14 compressed + IT215, // Impulse Tracker 2.15 compressed + AMS, // AMS / Velvet Studio packed + DMF, // DMF Huffman compression + MDL, // MDL Huffman compression + PTM8Dto16, // PTM 8-Bit delta value -> 16-Bit sample + ADPCM, // 4-Bit ADPCM-packed + MT2, // MadTracker 2 stereo delta encoding + floatPCM15, // Floating point PCM with 2^15 full scale + floatPCM23, // Floating point PCM with 2^23 full scale + floatPCMnormalize, // Floating point PCM and data will be normalized while reading + signedPCMnormalize, // Integer PCM and data will be normalized while reading + uLaw, // 8-to-16 bit G.711 u-law compression + aLaw, // 8-to-16 bit G.711 a-law compression + }; + +protected: + Bitdepth m_bitdepth; + Channels m_channels; + Endianness m_endianness; + Encoding m_encoding; + +public: + constexpr SampleIO(Bitdepth bits = _8bit, Channels channels = mono, Endianness endianness = littleEndian, Encoding encoding = signedPCM) + : m_bitdepth(bits), m_channels(channels), m_endianness(endianness), m_encoding(encoding) + { } + + bool operator== (const SampleIO &other) const + { + return memcmp(this, &other, sizeof(*this)) == 0; + } + + bool operator!= (const SampleIO &other) const + { + return memcmp(this, &other, sizeof(*this)) != 0; + } + + void operator|= (Bitdepth bits) + { + m_bitdepth = bits; + } + + void operator|= (Channels channels) + { + m_channels = channels; + } + + void operator|= (Endianness endianness) + { + m_endianness = endianness; + } + + void operator|= (Encoding encoding) + { + m_encoding = encoding; + } + + void MayNormalize() + { + if(GetBitDepth() >= 24) + { + if(GetEncoding() == SampleIO::signedPCM) + { + m_encoding = SampleIO::signedPCMnormalize; + } else if(GetEncoding() == SampleIO::floatPCM) + { + m_encoding = SampleIO::floatPCMnormalize; + } + } + } + + // Return 0 in case of variable-length encoded samples. + MPT_CONSTEXPRINLINE uint8 GetEncodedBitsPerSample() const + { + switch(GetEncoding()) + { + case signedPCM: // Integer PCM, signed + case unsignedPCM: //Integer PCM, unsigned + case deltaPCM: // Integer PCM, delta-encoded + case floatPCM: // Floating point PCM + case MT2: // MadTracker 2 stereo delta encoding + case floatPCM15: // Floating point PCM with 2^15 full scale + case floatPCM23: // Floating point PCM with 2^23 full scale + case floatPCMnormalize: // Floating point PCM and data will be normalized while reading + case signedPCMnormalize: // Integer PCM and data will be normalized while reading + return GetBitDepth(); + + case IT214: // Impulse Tracker 2.14 compressed + case IT215: // Impulse Tracker 2.15 compressed + case AMS: // AMS / Velvet Studio packed + case DMF: // DMF Huffman compression + case MDL: // MDL Huffman compression + return 0; // variable-length compressed + + case PTM8Dto16: // PTM 8-Bit delta value -> 16-Bit sample + return 16; + case ADPCM: // 4-Bit ADPCM-packed + return 4; + case uLaw: // G.711 u-law + return 8; + case aLaw: // G.711 a-law + return 8; + + default: + return 0; + } + } + + // Return the static header size additional to the raw encoded sample data. + MPT_CONSTEXPRINLINE std::size_t GetEncodedHeaderSize() const + { + switch(GetEncoding()) + { + case ADPCM: + return 16; + default: + return 0; + } + } + + // Returns true if the encoded size cannot be calculated apriori from the encoding format and the sample length. + MPT_CONSTEXPRINLINE bool IsVariableLengthEncoded() const + { + return GetEncodedBitsPerSample() == 0; + } + + // Returns true if the decoder for a given format uses FileReader interface and thus do not need to call GetPinnedView() + MPT_CONSTEXPRINLINE bool UsesFileReaderForDecoding() const + { + switch(GetEncoding()) + { + case IT214: + case IT215: + case AMS: + case DMF: + case MDL: + return true; + default: + return false; + } + } + + // Get bits per sample + constexpr uint8 GetBitDepth() const + { + return static_cast<uint8>(m_bitdepth); + } + // Get channel layout + constexpr Channels GetChannelFormat() const + { + return m_channels; + } + // Get number of channels + constexpr uint8 GetNumChannels() const + { + return GetChannelFormat() == mono ? 1u : 2u; + } + // Get sample byte order + constexpr Endianness GetEndianness() const + { + return m_endianness; + } + // Get sample format / encoding + constexpr Encoding GetEncoding() const + { + return m_encoding; + } + + // Returns the encoded size of the sample. In case of variable-length encoding returns 0. + std::size_t CalculateEncodedSize(SmpLength length) const + { + if(IsVariableLengthEncoded()) + { + return 0; + } + uint8 bps = GetEncodedBitsPerSample(); + if(bps % 8u != 0) + { + MPT_ASSERT(GetEncoding() == ADPCM && bps == 4); + return GetEncodedHeaderSize() + (((length + 1) / 2) * GetNumChannels()); // round up + } + return GetEncodedHeaderSize() + (length * (bps / 8) * GetNumChannels()); + } + + // Read a sample from memory + size_t ReadSample(ModSample &sample, FileReader &file) const; + +#ifndef MODPLUG_NO_FILESAVE + // Write a sample to file + size_t WriteSample(std::ostream &f, const ModSample &sample, SmpLength maxSamples = 0) const; +#endif // MODPLUG_NO_FILESAVE +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SampleNormalize.h b/Src/external_dependencies/openmpt-trunk/soundlib/SampleNormalize.h new file mode 100644 index 00000000..954b7704 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SampleNormalize.h @@ -0,0 +1,192 @@ +/* + * SampleNormalize.h + * ----------------- + * Purpose: Functions for normalizing samples. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace SC +{ // SC = _S_ample_C_onversion + + + +template <typename Tsample> +struct Normalize; + +template <> +struct Normalize<int32> +{ + typedef int32 input_t; + typedef int32 output_t; + typedef uint32 peak_t; + uint32 maxVal; + MPT_FORCEINLINE Normalize() + : maxVal(0) {} + MPT_FORCEINLINE void FindMax(input_t val) + { + if(val < 0) + { + if(val == std::numeric_limits<int32>::min()) + { + maxVal = static_cast<uint32>(-static_cast<int64>(std::numeric_limits<int32>::min())); + return; + } + val = -val; + } + if(static_cast<uint32>(val) > maxVal) + { + maxVal = static_cast<uint32>(val); + } + } + MPT_FORCEINLINE bool IsSilent() const + { + return maxVal == 0; + } + MPT_FORCEINLINE output_t operator()(input_t val) + { + return Util::muldivrfloor(val, static_cast<uint32>(1) << 31, maxVal); + } + MPT_FORCEINLINE peak_t GetSrcPeak() const + { + return maxVal; + } +}; + +template <> +struct Normalize<float32> +{ + typedef float32 input_t; + typedef float32 output_t; + typedef float32 peak_t; + float maxVal; + float maxValInv; + MPT_FORCEINLINE Normalize() + : maxVal(0.0f), maxValInv(1.0f) {} + MPT_FORCEINLINE void FindMax(input_t val) + { + float absval = std::fabs(val); + if(absval > maxVal) + { + maxVal = absval; + } + } + MPT_FORCEINLINE bool IsSilent() + { + if(maxVal == 0.0f) + { + maxValInv = 1.0f; + return true; + } else + { + maxValInv = 1.0f / maxVal; + return false; + } + } + MPT_FORCEINLINE output_t operator()(input_t val) + { + return val * maxValInv; + } + MPT_FORCEINLINE peak_t GetSrcPeak() const + { + return maxVal; + } +}; + +template <> +struct Normalize<float64> +{ + typedef float64 input_t; + typedef float64 output_t; + typedef float64 peak_t; + double maxVal; + double maxValInv; + MPT_FORCEINLINE Normalize() + : maxVal(0.0), maxValInv(1.0) {} + MPT_FORCEINLINE void FindMax(input_t val) + { + double absval = std::fabs(val); + if(absval > maxVal) + { + maxVal = absval; + } + } + MPT_FORCEINLINE bool IsSilent() + { + if(maxVal == 0.0) + { + maxValInv = 1.0; + return true; + } else + { + maxValInv = 1.0 / maxVal; + return false; + } + } + MPT_FORCEINLINE output_t operator()(input_t val) + { + return val * maxValInv; + } + MPT_FORCEINLINE peak_t GetSrcPeak() const + { + return maxVal; + } +}; + + +// Reads sample data with Func1, then normalizes the sample data, and then converts it with Func2. +// Func1::output_t and Func2::input_t must be identical. +// Func1 can also be the identity decode (DecodeIdentity<T>). +// Func2 can also be the identity conversion (Convert<T,T>). +template <typename Func2, typename Func1> +struct NormalizationChain +{ + typedef typename Func1::input_t input_t; + typedef typename Func1::output_t normalize_t; + typedef typename Normalize<normalize_t>::peak_t peak_t; + typedef typename Func2::output_t output_t; + static constexpr std::size_t input_inc = Func1::input_inc; + Func1 func1; + Normalize<normalize_t> normalize; + Func2 func2; + MPT_FORCEINLINE void FindMax(const input_t *inBuf) + { + normalize.FindMax(func1(inBuf)); + } + MPT_FORCEINLINE bool IsSilent() + { + return normalize.IsSilent(); + } + MPT_FORCEINLINE output_t operator()(const input_t *inBuf) + { + return func2(normalize(func1(inBuf))); + } + MPT_FORCEINLINE peak_t GetSrcPeak() const + { + return normalize.GetSrcPeak(); + } + MPT_FORCEINLINE NormalizationChain(Func2 f2 = Func2(), Func1 f1 = Func1()) + : func1(f1) + , func2(f2) + { + return; + } +}; + + + +} // namespace SC + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Snd_defs.h b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_defs.h new file mode 100644 index 00000000..7cc00f30 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_defs.h @@ -0,0 +1,693 @@ +/* + * Snd_Defs.h + * ---------- + * Purpose: Basic definitions of data types, enums, etc. for the playback engine core. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/base/FlagSet.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +using ROWINDEX = uint32; +inline constexpr ROWINDEX ROWINDEX_INVALID = uint32_max; +using CHANNELINDEX = uint16; +inline constexpr CHANNELINDEX CHANNELINDEX_INVALID = uint16_max; +using ORDERINDEX = uint16; +inline constexpr ORDERINDEX ORDERINDEX_INVALID = uint16_max; +inline constexpr ORDERINDEX ORDERINDEX_MAX = uint16_max - 1; +using PATTERNINDEX = uint16; +inline constexpr PATTERNINDEX PATTERNINDEX_INVALID = uint16_max; +using PLUGINDEX = uint8; +inline constexpr PLUGINDEX PLUGINDEX_INVALID = uint8_max; +using SAMPLEINDEX = uint16; +inline constexpr SAMPLEINDEX SAMPLEINDEX_INVALID = uint16_max; +using INSTRUMENTINDEX = uint16; +inline constexpr INSTRUMENTINDEX INSTRUMENTINDEX_INVALID = uint16_max; +using SEQUENCEINDEX = uint8; +inline constexpr SEQUENCEINDEX SEQUENCEINDEX_INVALID = uint8_max; + +using SmpLength = uint32; + + +inline constexpr SmpLength MAX_SAMPLE_LENGTH = 0x10000000; // Sample length in frames. Sample size in bytes can be more than this (= 256 MB). + +inline constexpr ROWINDEX MAX_PATTERN_ROWS = 1024; +inline constexpr ROWINDEX MAX_ROWS_PER_BEAT = 65536; +inline constexpr ORDERINDEX MAX_ORDERS = ORDERINDEX_MAX + 1; +inline constexpr PATTERNINDEX MAX_PATTERNS = 4000; +inline constexpr SAMPLEINDEX MAX_SAMPLES = 4000; +inline constexpr INSTRUMENTINDEX MAX_INSTRUMENTS = 256; +inline constexpr PLUGINDEX MAX_MIXPLUGINS = 250; + +inline constexpr SEQUENCEINDEX MAX_SEQUENCES = 50; + +inline constexpr CHANNELINDEX MAX_BASECHANNELS = 127; // Maximum pattern channels. +inline constexpr CHANNELINDEX MAX_CHANNELS = 256; // Maximum number of mixing channels. + +enum { FREQ_FRACBITS = 4 }; // Number of fractional bits in return value of CSoundFile::GetFreqFromPeriod() + +// String lengths (including trailing null char) +enum +{ + MAX_SAMPLENAME = 32, + MAX_SAMPLEFILENAME = 22, + MAX_INSTRUMENTNAME = 32, + MAX_INSTRUMENTFILENAME = 32, + MAX_PATTERNNAME = 32, + MAX_CHANNELNAME = 20, +}; + +enum MODTYPE +{ + MOD_TYPE_NONE = 0x00, + MOD_TYPE_MOD = 0x01, + MOD_TYPE_S3M = 0x02, + MOD_TYPE_XM = 0x04, + MOD_TYPE_MED = 0x08, + MOD_TYPE_MTM = 0x10, + MOD_TYPE_IT = 0x20, + MOD_TYPE_669 = 0x40, + MOD_TYPE_ULT = 0x80, + MOD_TYPE_STM = 0x100, + MOD_TYPE_FAR = 0x200, + MOD_TYPE_DTM = 0x400, + MOD_TYPE_AMF = 0x800, + MOD_TYPE_AMS = 0x1000, + MOD_TYPE_DSM = 0x2000, + MOD_TYPE_MDL = 0x4000, + MOD_TYPE_OKT = 0x8000, + MOD_TYPE_MID = 0x10000, + MOD_TYPE_DMF = 0x20000, + MOD_TYPE_PTM = 0x40000, + MOD_TYPE_DBM = 0x80000, + MOD_TYPE_MT2 = 0x100000, + MOD_TYPE_AMF0 = 0x200000, + MOD_TYPE_PSM = 0x400000, + MOD_TYPE_J2B = 0x800000, + MOD_TYPE_MPT = 0x1000000, + MOD_TYPE_IMF = 0x2000000, + MOD_TYPE_DIGI = 0x4000000, + MOD_TYPE_STP = 0x8000000, + MOD_TYPE_PLM = 0x10000000, + MOD_TYPE_SFX = 0x20000000, +}; +DECLARE_FLAGSET(MODTYPE) + + +enum MODCONTAINERTYPE +{ + MOD_CONTAINERTYPE_NONE = 0x0, + MOD_CONTAINERTYPE_UMX = 0x3, + MOD_CONTAINERTYPE_XPK = 0x4, + MOD_CONTAINERTYPE_PP20 = 0x5, + MOD_CONTAINERTYPE_MMCMP= 0x6, + MOD_CONTAINERTYPE_WAV = 0x7, // WAV as module + MOD_CONTAINERTYPE_UAX = 0x8, // Unreal sample set as module +}; + + +// Module channel / sample flags +enum ChannelFlags +{ + // Sample Flags + CHN_16BIT = 0x01, // 16-bit sample + CHN_LOOP = 0x02, // Looped sample + CHN_PINGPONGLOOP = 0x04, // Bidi-looped sample + CHN_SUSTAINLOOP = 0x08, // Sample with sustain loop + CHN_PINGPONGSUSTAIN = 0x10, // Sample with bidi sustain loop + CHN_PANNING = 0x20, // Sample with forced panning + CHN_STEREO = 0x40, // Stereo sample + CHN_REVERSE = 0x80, // Start sample playback from sample / loop end (Velvet Studio feature) + CHN_SURROUND = 0x100, // Use surround channel + CHN_ADLIB = 0x200, // Adlib / OPL instrument is active on this channel + + // Channel Flags + CHN_PINGPONGFLAG = 0x80, // When flag is on, sample is processed backwards - this is intentionally the same flag as CHN_REVERSE. + CHN_MUTE = 0x400, // Muted channel + CHN_KEYOFF = 0x800, // Exit sustain + CHN_NOTEFADE = 0x1000, // Fade note (instrument mode) + CHN_WRAPPED_LOOP = 0x2000, // Loop just wrapped around to loop start (required for correct interpolation around loop points) + CHN_AMIGAFILTER = 0x4000, // Apply Amiga low-pass filter + CHN_FILTER = 0x8000, // Apply resonant filter on sample + CHN_VOLUMERAMP = 0x10000, // Apply volume ramping + CHN_VIBRATO = 0x20000, // Apply vibrato + CHN_TREMOLO = 0x40000, // Apply tremolo + CHN_PORTAMENTO = 0x80000, // Apply portamento + CHN_GLISSANDO = 0x100000, // Glissando (force portamento to semitones) mode + CHN_FASTVOLRAMP = 0x200000, // Force usage of global ramping settings instead of ramping over the complete render buffer length + CHN_EXTRALOUD = 0x400000, // Force sample to play at 0dB + CHN_REVERB = 0x800000, // Apply reverb on this channel + CHN_NOREVERB = 0x1000000, // Disable reverb on this channel + CHN_SOLO = 0x2000000, // Solo channel + CHN_NOFX = 0x4000000, // Dry channel (no plugins) + CHN_SYNCMUTE = 0x8000000, // Keep sample sync on mute + + // Sample flags (only present in ModSample::uFlags, may overlap with CHN_CHANNELFLAGS) + SMP_MODIFIED = 0x2000, // Sample data has been edited in the tracker + SMP_KEEPONDISK = 0x4000, // Sample is not saved to file, data is restored from original sample file + SMP_NODEFAULTVOLUME = 0x8000, // Ignore default volume setting +}; +DECLARE_FLAGSET(ChannelFlags) + +#define CHN_SAMPLEFLAGS (CHN_16BIT | CHN_LOOP | CHN_PINGPONGLOOP | CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN | CHN_PANNING | CHN_STEREO | CHN_PINGPONGFLAG | CHN_REVERSE | CHN_SURROUND | CHN_ADLIB) +#define CHN_CHANNELFLAGS (~CHN_SAMPLEFLAGS | CHN_SURROUND) + +// Sample flags fit into the first 16 bits, and with the current memory layout, storing them as a 16-bit integer packs struct ModSample nicely. +using SampleFlags = FlagSet<ChannelFlags, uint16>; + + +// Instrument envelope-specific flags +enum EnvelopeFlags : uint8 +{ + ENV_ENABLED = 0x01, // env is enabled + ENV_LOOP = 0x02, // env loop + ENV_SUSTAIN = 0x04, // env sustain + ENV_CARRY = 0x08, // env carry + ENV_FILTER = 0x10, // filter env enabled (this has to be combined with ENV_ENABLED in the pitch envelope's flags) +}; +DECLARE_FLAGSET(EnvelopeFlags) + + +// Envelope value boundaries +#define ENVELOPE_MIN 0 // Vertical min value of a point +#define ENVELOPE_MID 32 // Vertical middle line +#define ENVELOPE_MAX 64 // Vertical max value of a point +#define MAX_ENVPOINTS 240 // Maximum length of each instrument envelope + + +// Instrument-specific flags +enum InstrumentFlags : uint8 +{ + INS_SETPANNING = 0x01, // Panning enabled + INS_MUTE = 0x02, // Instrument is muted +}; +DECLARE_FLAGSET(InstrumentFlags) + + +// envelope types in instrument editor +enum EnvelopeType : uint8 +{ + ENV_VOLUME = 0, + ENV_PANNING, + ENV_PITCH, + + ENV_MAXTYPES +}; + +// Filter Modes +enum class FilterMode : uint8 +{ + Unchanged = 0xFF, + LowPass = 0, + HighPass = 1, +}; + + +// NNA types (New Note Action) +enum class NewNoteAction : uint8 +{ + NoteCut = 0, + Continue = 1, + NoteOff = 2, + NoteFade = 3, +}; + +// DCT types (Duplicate Check Types) +enum class DuplicateCheckType : uint8 +{ + None = 0, + Note = 1, + Sample = 2, + Instrument = 3, + Plugin = 4, +}; + +// DNA types (Duplicate Note Action) +enum class DuplicateNoteAction : uint8 +{ + NoteCut = 0, + NoteOff = 1, + NoteFade = 2, +}; + + +// Module flags - contains both song configuration and playback state... Use SONG_FILE_FLAGS and SONG_PLAY_FLAGS distinguish between the two. +enum SongFlags +{ + SONG_FASTVOLSLIDES = 0x02, // Old Scream Tracker 3.0 volume slides + SONG_ITOLDEFFECTS = 0x04, // Old Impulse Tracker effect implementations + SONG_ITCOMPATGXX = 0x08, // IT "Compatible Gxx" (IT's flag to behave more like other trackers w/r/t portamento effects) + SONG_LINEARSLIDES = 0x10, // Linear slides vs. Amiga slides + SONG_PATTERNLOOP = 0x20, // Loop current pattern (pattern editor) + SONG_STEP = 0x40, // Song is in "step" mode (pattern editor) + SONG_PAUSED = 0x80, // Song is paused (no tick processing, just rendering audio) + SONG_FADINGSONG = 0x0100, // Song is fading out + SONG_ENDREACHED = 0x0200, // Song is finished + SONG_FIRSTTICK = 0x1000, // Is set when the current tick is the first tick of the row + SONG_MPTFILTERMODE = 0x2000, // Local filter mode (reset filter on each note) + SONG_SURROUNDPAN = 0x4000, // Pan in the rear channels + SONG_EXFILTERRANGE = 0x8000, // Cutoff Filter has double frequency range (up to ~10Khz) + SONG_AMIGALIMITS = 0x1'0000, // Enforce amiga frequency limits + SONG_S3MOLDVIBRATO = 0x2'0000, // ScreamTracker 2 vibrato in S3M files + SONG_BREAKTOROW = 0x8'0000, // Break to row command encountered (internal flag, do not touch) + SONG_POSJUMP = 0x10'0000, // Position jump encountered (internal flag, do not touch) + SONG_PT_MODE = 0x20'0000, // ProTracker 1/2 playback mode + SONG_PLAYALLSONGS = 0x40'0000, // Play all subsongs consecutively (libopenmpt) + SONG_ISAMIGA = 0x80'0000, // Is an Amiga module and thus qualifies to be played using the Paula BLEP resampler + SONG_IMPORTED = 0x100'0000, // Song type does not represent actual module format / was imported from a different format (OpenMPT) +}; +DECLARE_FLAGSET(SongFlags) + +#define SONG_FILE_FLAGS (SONG_FASTVOLSLIDES|SONG_ITOLDEFFECTS|SONG_ITCOMPATGXX|SONG_LINEARSLIDES|SONG_EXFILTERRANGE|SONG_AMIGALIMITS|SONG_S3MOLDVIBRATO|SONG_PT_MODE|SONG_ISAMIGA|SONG_IMPORTED) +#define SONG_PLAY_FLAGS (~SONG_FILE_FLAGS) + +// Global Options (Renderer) +#ifndef NO_AGC +#define SNDDSP_AGC 0x40 // Automatic gain control +#endif // ~NO_AGC +#ifndef NO_DSP +#define SNDDSP_MEGABASS 0x02 // Bass expansion +#define SNDDSP_SURROUND 0x08 // Surround mix +#define SNDDSP_BITCRUSH 0x01 +#endif // NO_DSP +#ifndef NO_REVERB +#define SNDDSP_REVERB 0x20 // Apply reverb +#endif // NO_REVERB +#ifndef NO_EQ +#define SNDDSP_EQ 0x80 // Apply EQ +#endif // NO_EQ + +#define SNDMIX_SOFTPANNING 0x10 // Soft panning mode (this is forced with mixmode RC3 and later) + +// Misc Flags (can safely be turned on or off) +#define SNDMIX_MAXDEFAULTPAN 0x80000 // Currently unused (should be used by Amiga MOD loaders) +#define SNDMIX_MUTECHNMODE 0x100000 // Notes are not played on muted channels + + +#define MAX_GLOBAL_VOLUME 256u + +// Resampling modes +enum ResamplingMode : uint8 +{ + // ATTENTION: Do not change ANY of these values, as they get written out to files in per instrument interpolation settings + // and old files have these exact values in them which should not change meaning. + SRCMODE_NEAREST = 0, // 1 tap, no AA + SRCMODE_LINEAR = 1, // 2 tap, no AA + SRCMODE_CUBIC = 2, // 4 tap, no AA + SRCMODE_SINC8 = 4, // 8 tap, no AA (yes, index 4) (XMMS-ModPlug) + SRCMODE_SINC8LP = 3, // 8 tap, with AA (yes, index 3) (Polyphase) + + SRCMODE_DEFAULT = 5, // Only used for instrument settings, not used inside the mixer + + SRCMODE_AMIGA = 0xFF, // Not explicitely user-selectable +}; + +namespace Resampling +{ + +enum class AmigaFilter +{ + Off = 0, + A500 = 1, + A1200 = 2, + Unfiltered = 3, +}; + +inline std::array<ResamplingMode, 5> AllModes() noexcept { return { { SRCMODE_NEAREST, SRCMODE_LINEAR, SRCMODE_CUBIC, SRCMODE_SINC8, SRCMODE_SINC8LP } }; } + +inline std::array<ResamplingMode, 6> AllModesWithDefault() noexcept { return { { SRCMODE_NEAREST, SRCMODE_LINEAR, SRCMODE_CUBIC, SRCMODE_SINC8, SRCMODE_SINC8LP, SRCMODE_DEFAULT } }; } + +constexpr ResamplingMode Default() noexcept { return SRCMODE_SINC8LP; } + +constexpr bool IsKnownMode(int mode) noexcept { return (mode >= 0) && (mode < SRCMODE_DEFAULT); } + +constexpr ResamplingMode ToKnownMode(int mode) noexcept +{ + return Resampling::IsKnownMode(mode) ? static_cast<ResamplingMode>(mode) + : (mode == SRCMODE_AMIGA) ? SRCMODE_LINEAR + : Resampling::Default(); +} + +constexpr int Length(ResamplingMode mode) noexcept +{ + return mode == SRCMODE_NEAREST ? 1 + : mode == SRCMODE_LINEAR ? 2 + : mode == SRCMODE_CUBIC ? 4 + : mode == SRCMODE_SINC8 ? 8 + : mode == SRCMODE_SINC8LP ? 8 + : 0; +} + +constexpr bool HasAA(ResamplingMode mode) noexcept { return (mode == SRCMODE_SINC8LP); } + +constexpr ResamplingMode AddAA(ResamplingMode mode) noexcept { return (mode == SRCMODE_SINC8) ? SRCMODE_SINC8LP : mode; } + +constexpr ResamplingMode RemoveAA(ResamplingMode mode) noexcept { return (mode == SRCMODE_SINC8LP) ? SRCMODE_SINC8 : mode; } + +} + + + +// Release node defines +#define ENV_RELEASE_NODE_UNSET 0xFF +#define NOT_YET_RELEASED (-1) +static_assert(ENV_RELEASE_NODE_UNSET > MAX_ENVPOINTS); + + +enum PluginPriority +{ + ChannelOnly, + InstrumentOnly, + PrioritiseInstrument, + PrioritiseChannel, +}; + +enum PluginMutePriority +{ + EvenIfMuted, + RespectMutes, +}; + +// Plugin velocity handling options +enum PlugVelocityHandling : uint8 +{ + PLUGIN_VELOCITYHANDLING_CHANNEL = 0, + PLUGIN_VELOCITYHANDLING_VOLUME +}; + +// Plugin volumecommand handling options +enum PlugVolumeHandling : uint8 +{ + PLUGIN_VOLUMEHANDLING_MIDI = 0, + PLUGIN_VOLUMEHANDLING_DRYWET, + PLUGIN_VOLUMEHANDLING_IGNORE, + PLUGIN_VOLUMEHANDLING_CUSTOM, + PLUGIN_VOLUMEHANDLING_MAX, +}; + +enum MidiChannel : uint8 +{ + MidiNoChannel = 0, + MidiFirstChannel = 1, + MidiLastChannel = 16, + MidiMappedChannel = 17, +}; + + +// Vibrato Types +enum VibratoType : uint8 +{ + VIB_SINE = 0, + VIB_SQUARE, + VIB_RAMP_UP, + VIB_RAMP_DOWN, + VIB_RANDOM +}; + + +// Tracker-specific playback behaviour +// Note: The index of every flag has to be fixed, so do not remove flags. Always add new flags at the end! +enum PlayBehaviour +{ + MSF_COMPATIBLE_PLAY, // No-op - only used during loading (Old general compatibility flag for IT/MPT/XM) + kMPTOldSwingBehaviour, // MPT 1.16 swing behaviour (IT/MPT, deprecated) + kMIDICCBugEmulation, // Emulate broken volume MIDI CC behaviour (IT/MPT/XM, deprecated) + kOldMIDIPitchBends, // Old VST MIDI pitch bend behaviour (IT/MPT/XM, deprecated) + kFT2VolumeRamping, // Smooth volume ramping like in FT2 (XM) + kMODVBlankTiming, // F21 and above set speed instead of tempo + kSlidesAtSpeed1, // Execute normal slides at speed 1 as if they were fine slides + kPeriodsAreHertz, // Compute note frequency in Hertz rather than periods + kTempoClamp, // Clamp tempo to 32-255 range. + kPerChannelGlobalVolSlide, // Global volume slide memory is per-channel + kPanOverride, // Panning commands override surround and random pan variation + + kITInstrWithoutNote, // Avoid instrument handling if there is no note + kITVolColFinePortamento, // Volume column portamento never does fine portamento + kITArpeggio, // IT arpeggio algorithm + kITOutOfRangeDelay, // Out-of-range delay command behaviour in IT + kITPortaMemoryShare, // Gxx shares memory with Exx and Fxx + kITPatternLoopTargetReset, // After finishing a pattern loop, set the pattern loop target to the next row + kITFT2PatternLoop, // Nested pattern loop behaviour + kITPingPongNoReset, // Don't reset ping pong direction with instrument numbers + kITEnvelopeReset, // IT envelope reset behaviour + kITClearOldNoteAfterCut, // Forget the previous note after cutting it + kITVibratoTremoloPanbrello, // More IT-like Hxx / hx, Rxx, Yxx and autovibrato handling, including more precise LUTs + kITTremor, // Ixx behaves like in IT + kITRetrigger, // Qxx behaves like in IT + kITMultiSampleBehaviour, // Properly update C-5 frequency when changing in multisampled instrument + kITPortaTargetReached, // Clear portamento target after it has been reached + kITPatternLoopBreak, // Don't reset loop count on pattern break. + kITOffset, // IT-style Oxx edge case handling + kITSwingBehaviour, // IT's swing behaviour + kITNNAReset, // NNA is reset on every note change, not every instrument change + kITSCxStopsSample, // SCx really stops the sample and does not just mute it + kITEnvelopePositionHandling, // IT-style envelope position advance + enable/disable behaviour + kITPortamentoInstrument, // No sample changes during portamento with Compatible Gxx enabled, instrument envelope reset with portamento + kITPingPongMode, // Don't repeat last sample point in ping pong loop, like IT's software mixer + kITRealNoteMapping, // Use triggered note rather than translated note for PPS and other effects + kITHighOffsetNoRetrig, // SAx should not apply an offset effect to a note next to it + kITFilterBehaviour, // User IT's filter coefficients (unless extended filter range is used) + kITNoSurroundPan, // Panning and surround are mutually exclusive + kITShortSampleRetrig, // Don't retrigger already stopped channels + kITPortaNoNote, // Don't apply any portamento if no previous note is playing + kITFT2DontResetNoteOffOnPorta, // Only reset note-off status on portamento in IT Compatible Gxx mode + kITVolColMemory, // IT volume column effects share their memory with the effect column + kITPortamentoSwapResetsPos, // Portamento with sample swap plays the new sample from the beginning + kITEmptyNoteMapSlot, // IT ignores instrument note map entries with no note completely + kITFirstTickHandling, // IT-style first tick handling + kITSampleAndHoldPanbrello, // IT-style sample&hold panbrello waveform + kITClearPortaTarget, // New notes reset portamento target in IT + kITPanbrelloHold, // Don't reset panbrello effect until next note or panning effect + kITPanningReset, // Sample and instrument panning is only applied on note change, not instrument change + kITPatternLoopWithJumpsOld, // Bxx on the same row as SBx terminates the loop in IT (old implementation of kITPatternLoopWithJumps) + kITInstrWithNoteOff, // Instrument number with note-off recalls default volume + + kFT2Arpeggio, // FT2 arpeggio algorithm + kFT2Retrigger, // Rxx behaves like in FT2 + kFT2VolColVibrato, // Vibrato depth in volume column does not actually execute the vibrato effect + kFT2PortaNoNote, // Don't play portamento-ed note if no previous note is playing + kFT2KeyOff, // FT2-style Kxx handling + kFT2PanSlide, // Volume-column pan slides should be handled like fine slides + kFT2ST3OffsetOutOfRange, // Offset past sample end stops the note + kFT2RestrictXCommand, // Don't allow MPT extensions to Xxx command in XM + kFT2RetrigWithNoteDelay, // Retrigger envelopes if there is a note delay with no note + kFT2SetPanEnvPos, // Lxx only sets the pan env position if the volume envelope's sustain flag is set + kFT2PortaIgnoreInstr, // Portamento plus instrument number applies the volume settings of the new sample, but not the new sample itself. + kFT2VolColMemory, // No volume column memory in FT2 + kFT2LoopE60Restart, // Next pattern starts on the same row as the last E60 command + kFT2ProcessSilentChannels, // Keep processing silent channels for later 3xx pickup + kFT2ReloadSampleSettings, // Reload sample settings even if a note-off is placed next to an instrument number + kFT2PortaDelay, // Portamento with note delay next to it is ignored in FT2 + kFT2Transpose, // Out-of-range transposed notes in FT2 + kFT2PatternLoopWithJumps, // Bxx or Dxx on the same row as E6x terminates the loop in FT2 + kFT2PortaTargetNoReset, // Portamento target is not reset with new notes in FT2 + kFT2EnvelopeEscape, // FT2 sustain point at end of envelope + kFT2Tremor, // Txx behaves like in FT2 + kFT2OutOfRangeDelay, // Out-of-range delay command behaviour in FT2 + kFT2Periods, // Use FT2's broken period handling + kFT2PanWithDelayedNoteOff, // Pan command with delayed note-off + kFT2VolColDelay, // FT2-style volume column handling if there is a note delay + kFT2FinetunePrecision, // Only take the upper 4 bits of sample finetune. + + kST3NoMutedChannels, // Don't process any effects on muted S3M channels + kST3EffectMemory, // Most effects share the same memory in ST3 + kST3PortaSampleChange, // Portamento plus instrument number applies the volume settings of the new sample, but not the new sample itself (GUS behaviour). + kST3VibratoMemory, // Do not remember vibrato type in effect memory + kST3LimitPeriod, // Cut note instead of limiting final period (ModPlug Tracker style) + KST3PortaAfterArpeggio, // Portamento after arpeggio continues at the note where the arpeggio left off + + kMODOneShotLoops, // Allow ProTracker-like oneshot loops + kMODIgnorePanning, // Do not process any panning commands + kMODSampleSwap, // On-the-fly sample swapping + + kFT2NoteOffFlags, // Set and reset the correct fade/key-off flags with note-off and instrument number after note-off + kITMultiSampleInstrumentNumber, // After portamento to different sample within multi-sampled instrument, lone instrument numbers in patterns always recall the new sample's default settings + kRowDelayWithNoteDelay, // Retrigger note delays on every reptition of a row + kFT2MODTremoloRampWaveform, // FT2-/ProTracker-compatible tremolo ramp down / triangle waveform + kFT2PortaUpDownMemory, // Portamento up and down have separate memory + + kMODOutOfRangeNoteDelay, // ProTracker behaviour for out-of-range note delays + kMODTempoOnSecondTick, // ProTracker sets tempo after the first tick + + kFT2PanSustainRelease, // If the sustain point of a panning envelope is reached before key-off, FT2 does not escape it anymore + kLegacyReleaseNode, // Legacy release node volume processing + kOPLBeatingOscillators, // Emulate beating FM oscillators from CDFM / Composer 670 + kST3OffsetWithoutInstrument, // Note without instrument uses same offset as previous note + kReleaseNodePastSustainBug, // OpenMPT 1.23.01.02 / r4009 broke release nodes past the sustain point, fixed in OpenMPT 1.28 + kFT2NoteDelayWithoutInstr, // Sometime between OpenMPT 1.18.03.00 and 1.19.01.00, delayed instrument-less notes in XM started recalling the default sample volume and panning + kOPLFlexibleNoteOff, // Full control after note-off over OPL voices, ^^^ sends note cut instead of just note-off + kITInstrWithNoteOffOldEffects, // Instrument number with note-off recalls default volume - special cases with Old Effects enabled + kMIDIVolumeOnNoteOffBug, // Update MIDI channel volume on note-off (legacy bug emulation) + kITDoNotOverrideChannelPan, // Sample / instrument pan does not override channel pan for following samples / instruments that are not panned + kITPatternLoopWithJumps, // Bxx right of SBx terminates the loop in IT + kITDCTBehaviour, // DCT="Sample" requires sample instrument, DCT="Note" checks old pattern note against new pattern note (previously was checking old pattern note against new translated note) + kOPLwithNNA, // NNA note-off / fade are applied to OPL channels + kST3RetrigAfterNoteCut, // Qxy does not retrigger note after it has been cut with ^^^ or SCx + kST3SampleSwap, // On-the-fly sample swapping (SoundBlaster behaviour) + kOPLRealRetrig, // Retrigger effect (Qxy) restarts OPL notes + kOPLNoResetAtEnvelopeEnd, // Do not reset OPL channel status at end of envelope (OpenMPT 1.28 inconsistency with samples) + kOPLNoteStopWith0Hz, // Set note frequency to 0 Hz to "stop" OPL notes + kOPLNoteOffOnNoteChange, // Send note-off events for old note on every note change + kFT2PortaResetDirection, // Reset portamento direction when reaching portamento target from below + kApplyUpperPeriodLimit, // Enforce m_nMaxPeriod + kApplyOffsetWithoutNote, // Offset commands even work when there's no note next to them (e.g. DMF, MDL, PLM formats) + kITPitchPanSeparation, // Pitch/Pan Separation can be overridden by panning commands (this also fixes a bug where any "special" notes affect PPS) + kImprecisePingPongLoops, // Use old (less precise) ping-pong overshoot calculation + + // Add new play behaviours here. + + kMaxPlayBehaviours, +}; + + +// Tempo swing determines how much every row in modern tempo mode contributes to a beat. +class TempoSwing : public std::vector<uint32> +{ +public: + static constexpr uint32 Unity = 1u << 24; + // Normalize the tempo swing coefficients so that they add up to exactly the specified tempo again + void Normalize(); + void resize(size_type newSize, value_type val = Unity) { std::vector<uint32>::resize(newSize, val); Normalize(); } + + static void Serialize(std::ostream &oStrm, const TempoSwing &swing); + static void Deserialize(std::istream &iStrm, TempoSwing &swing, const size_t); +}; + + +// Sample position and sample position increment value +struct SamplePosition +{ + using value_t = int64; + using unsigned_value_t = uint64; + +protected: + value_t v = 0; + +public: + static constexpr uint32 fractMax = 0xFFFFFFFFu; + + MPT_CONSTEXPRINLINE SamplePosition() { } + MPT_CONSTEXPRINLINE explicit SamplePosition(value_t pos) : v(pos) { } + MPT_CONSTEXPRINLINE SamplePosition(int32 intPart, uint32 fractPart) : v((static_cast<value_t>(intPart) * (1ll << 32)) | fractPart) { } + static SamplePosition Ratio(uint32 dividend, uint32 divisor) { return SamplePosition((static_cast<int64>(dividend) << 32) / divisor); } + static SamplePosition FromDouble(double pos) { return SamplePosition(static_cast<value_t>(pos * 4294967296.0)); } + double ToDouble() const { return v / 4294967296.0; } + + // Set integer and fractional part + MPT_CONSTEXPRINLINE SamplePosition &Set(int32 intPart, uint32 fractPart = 0) { v = (static_cast<int64>(intPart) << 32) | fractPart; return *this; } + // Set integer part, keep fractional part + MPT_CONSTEXPRINLINE SamplePosition &SetInt(int32 intPart) { v = (static_cast<value_t>(intPart) << 32) | GetFract(); return *this; } + // Get integer part (as sample length / position) + MPT_CONSTEXPRINLINE SmpLength GetUInt() const { return static_cast<SmpLength>(static_cast<unsigned_value_t>(v) >> 32); } + // Get integer part + MPT_CONSTEXPRINLINE int32 GetInt() const { return static_cast<int32>(static_cast<unsigned_value_t>(v) >> 32); } + // Get fractional part + MPT_CONSTEXPRINLINE uint32 GetFract() const { return static_cast<uint32>(v); } + // Get the inverted fractional part + MPT_CONSTEXPRINLINE SamplePosition GetInvertedFract() const { return SamplePosition(0x100000000ll - GetFract()); } + // Get the raw fixed-point value + MPT_CONSTEXPRINLINE int64 GetRaw() const { return v; } + // Negate the current value + MPT_CONSTEXPRINLINE SamplePosition &Negate() { v = -v; return *this; } + // Multiply and divide by given integer scalars + MPT_CONSTEXPRINLINE SamplePosition &MulDiv(uint32 mul, uint32 div) { v = (v * mul) / div; return *this; } + // Removes the integer part, only keeping fractions + MPT_CONSTEXPRINLINE SamplePosition &RemoveInt() { v &= fractMax; return *this; } + // Check if value is 1.0 + MPT_CONSTEXPRINLINE bool IsUnity() const { return v == 0x100000000ll; } + // Check if value is 0 + MPT_CONSTEXPRINLINE bool IsZero() const { return v == 0; } + // Check if value is > 0 + MPT_CONSTEXPRINLINE bool IsPositive() const { return v > 0; } + // Check if value is < 0 + MPT_CONSTEXPRINLINE bool IsNegative() const { return v < 0; } + + // Addition / subtraction of another fixed-point number + SamplePosition operator+ (const SamplePosition &other) const { return SamplePosition(v + other.v); } + SamplePosition operator- (const SamplePosition &other) const { return SamplePosition(v - other.v); } + void operator+= (const SamplePosition &other) { v += other.v; } + void operator-= (const SamplePosition &other) { v -= other.v; } + + // Multiplication with integer scalar + template<typename T> + SamplePosition operator* (T other) const { return SamplePosition(static_cast<value_t>(v * other)); } + template<typename T> + void operator*= (T other) { v = static_cast<value_t>(v *other); } + + // Division by other fractional point number; returns scalar + value_t operator/ (SamplePosition other) const { return v / other.v; } + // Division by scalar; returns fractional point number + SamplePosition operator/ (int div) const { return SamplePosition(v / div); } + + MPT_CONSTEXPRINLINE bool operator==(const SamplePosition &other) const { return v == other.v; } + MPT_CONSTEXPRINLINE bool operator!=(const SamplePosition &other) const { return v != other.v; } + MPT_CONSTEXPRINLINE bool operator<=(const SamplePosition &other) const { return v <= other.v; } + MPT_CONSTEXPRINLINE bool operator>=(const SamplePosition &other) const { return v >= other.v; } + MPT_CONSTEXPRINLINE bool operator<(const SamplePosition &other) const { return v < other.v; } + MPT_CONSTEXPRINLINE bool operator>(const SamplePosition &other) const { return v > other.v; } +}; + + +// Aaaand another fixed-point type, e.g. used for fractional tempos +// Note that this doesn't use classical bit shifting for the fixed point part. +// This is mostly for the clarity of stored values and to be able to represent any value .0000 to .9999 properly. +// For easier debugging, use the Debugger Visualizers available in build/vs/debug/ +// to easily display the stored values. +template<size_t FFact, typename T> +struct FPInt +{ +protected: + T v; + MPT_CONSTEXPRINLINE FPInt(T rawValue) : v(rawValue) { } + +public: + enum : size_t { fractFact = FFact }; + using store_t = T; + + MPT_CONSTEXPRINLINE FPInt() : v(0) { } + MPT_CONSTEXPRINLINE FPInt(T intPart, T fractPart) : v((intPart * fractFact) + (fractPart % fractFact)) { } + explicit MPT_CONSTEXPRINLINE FPInt(float f) : v(mpt::saturate_round<T>(f * float(fractFact))) { } + explicit MPT_CONSTEXPRINLINE FPInt(double f) : v(mpt::saturate_round<T>(f * double(fractFact))) { } + + // Set integer and fractional part + MPT_CONSTEXPRINLINE FPInt<fractFact, T> &Set(T intPart, T fractPart = 0) { v = (intPart * fractFact) + (fractPart % fractFact); return *this; } + // Set raw internal representation directly + MPT_CONSTEXPRINLINE FPInt<fractFact, T> &SetRaw(T value) { v = value; return *this; } + // Retrieve the integer part of the stored value + MPT_CONSTEXPRINLINE T GetInt() const { return v / fractFact; } + // Retrieve the fractional part of the stored value + MPT_CONSTEXPRINLINE T GetFract() const { return v % fractFact; } + // Retrieve the raw internal representation of the stored value + MPT_CONSTEXPRINLINE T GetRaw() const { return v; } + // Formats the stored value as a floating-point value + MPT_CONSTEXPRINLINE double ToDouble() const { return v / double(fractFact); } + + MPT_CONSTEXPRINLINE friend FPInt<fractFact, T> operator+ (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return FPInt<fractFact, T>(a.v + b.v); } + MPT_CONSTEXPRINLINE friend FPInt<fractFact, T> operator- (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return FPInt<fractFact, T>(a.v - b.v); } + MPT_CONSTEXPRINLINE FPInt<fractFact, T> operator+= (const FPInt<fractFact, T> &other) noexcept { v += other.v; return *this; } + MPT_CONSTEXPRINLINE FPInt<fractFact, T> operator-= (const FPInt<fractFact, T> &other) noexcept { v -= other.v; return *this; } + + MPT_CONSTEXPRINLINE friend bool operator== (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v == b.v; } + MPT_CONSTEXPRINLINE friend bool operator!= (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v != b.v; } + MPT_CONSTEXPRINLINE friend bool operator<= (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v <= b.v; } + MPT_CONSTEXPRINLINE friend bool operator>= (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v >= b.v; } + MPT_CONSTEXPRINLINE friend bool operator< (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v < b.v; } + MPT_CONSTEXPRINLINE friend bool operator> (const FPInt<fractFact, T> &a, const FPInt<fractFact, T> &b) noexcept { return a.v > b.v; } +}; + +using TEMPO = FPInt<10000, uint32>; + +using OPLPatch = std::array<uint8, 12>; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Snd_flt.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_flt.cpp new file mode 100644 index 00000000..e5ea5b6e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_flt.cpp @@ -0,0 +1,164 @@ +/* + * Snd_flt.cpp + * ----------- + * Purpose: Calculation of resonant filter coefficients. + * Notes : Extended filter range was introduced in MPT 1.12 and went up to 8652 Hz. + * MPT 1.16 upped this to the current 10670 Hz. + * We have no way of telling whether a file was made with MPT 1.12 or 1.16 though. + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "Tables.h" +#include "../common/misc_util.h" +#include "mpt/base/numbers.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// AWE32: cutoff = reg[0-255] * 31.25 + 100 -> [100Hz-8060Hz] +// EMU10K1 docs: cutoff = reg[0-127]*62+100 + + +uint8 CSoundFile::FrequencyToCutOff(double frequency) const +{ + // IT Cutoff is computed as cutoff = 110 * 2 ^ (0.25 + x/y), where x is the cutoff and y defines the filter range. + // Reversed, this gives us x = (log2(cutoff / 110) - 0.25) * y. + // <==========> Rewrite as x = (log2(cutoff) - log2(110) - 0.25) * y. + // <==========> Rewrite as x = (ln(cutoff) - ln(110) - 0.25*ln(2)) * y/ln(2). + // <4.8737671609324025> + double cutoff = (std::log(frequency) - 4.8737671609324025) * (m_SongFlags[SONG_EXFILTERRANGE] ? (20.0 / mpt::numbers::ln2) : (24.0 / mpt::numbers::ln2)); + Limit(cutoff, 0.0, 127.0); + return mpt::saturate_round<uint8>(cutoff); +} + + +uint32 CSoundFile::CutOffToFrequency(uint32 nCutOff, int envModifier) const +{ + MPT_ASSERT(nCutOff < 128); + float computedCutoff = static_cast<float>(nCutOff * (envModifier + 256)); // 0...127*512 + float Fc; + if(GetType() != MOD_TYPE_IMF) + { + Fc = 110.0f * std::pow(2.0f, 0.25f + computedCutoff / (m_SongFlags[SONG_EXFILTERRANGE] ? 20.0f * 512.0f : 24.0f * 512.0f)); + } else + { + // EMU8000: Documentation says the cutoff is in quarter semitones, with 0x00 being 125 Hz and 0xFF being 8 kHz + // The first half of the sentence contradicts the second, though. + Fc = 125.0f * std::pow(2.0f, computedCutoff * 6.0f / (127.0f * 512.0f)); + } + int freq = mpt::saturate_round<int>(Fc); + Limit(freq, 120, 20000); + if(freq * 2 > (int)m_MixerSettings.gdwMixingFreq) freq = m_MixerSettings.gdwMixingFreq / 2; + return static_cast<uint32>(freq); +} + + +// Simple 2-poles resonant filter. Returns computed cutoff in range [0, 254] or -1 if filter is not applied. +int CSoundFile::SetupChannelFilter(ModChannel &chn, bool bReset, int envModifier) const +{ + int cutoff = static_cast<int>(chn.nCutOff) + chn.nCutSwing; + int resonance = static_cast<int>(chn.nResonance & 0x7F) + chn.nResSwing; + + Limit(cutoff, 0, 127); + Limit(resonance, 0, 127); + + if(!m_playBehaviour[kMPTOldSwingBehaviour]) + { + chn.nCutOff = (uint8)cutoff; + chn.nCutSwing = 0; + chn.nResonance = (uint8)resonance; + chn.nResSwing = 0; + } + + // envModifier is in [-256, 256], so cutoff is in [0, 127 * 2] after this calculation. + const int computedCutoff = cutoff * (envModifier + 256) / 256; + + // Filtering is only ever done in IT if either cutoff is not full or if resonance is set. + if(m_playBehaviour[kITFilterBehaviour] && resonance == 0 && computedCutoff >= 254) + { + if(chn.rowCommand.IsNote() && !chn.rowCommand.IsPortamento() && !chn.nMasterChn && chn.triggerNote) + { + // Z7F next to a note disables the filter, however in other cases this should not happen. + // Test cases: filter-reset.it, filter-reset-carry.it, filter-reset-envelope.it, filter-nna.it, FilterResetPatDelay.it + chn.dwFlags.reset(CHN_FILTER); + } + return -1; + } + + chn.dwFlags.set(CHN_FILTER); + + // 2 * damping factor + const float dmpfac = std::pow(10.0f, -resonance * ((24.0f / 128.0f) / 20.0f)); + const float fc = CutOffToFrequency(cutoff, envModifier) * (2.0f * mpt::numbers::pi_v<float>); + float d, e; + if(m_playBehaviour[kITFilterBehaviour] && !m_SongFlags[SONG_EXFILTERRANGE]) + { + const float r = m_MixerSettings.gdwMixingFreq / fc; + + d = dmpfac * r + dmpfac - 1.0f; + e = r * r; + } else + { + const float r = fc / m_MixerSettings.gdwMixingFreq; + + d = (1.0f - 2.0f * dmpfac) * r; + LimitMax(d, 2.0f); + d = (2.0f * dmpfac - d) / r; + e = 1.0f / (r * r); + } + + float fg = 1.0f / (1.0f + d + e); + float fb0 = (d + e + e) / (1 + d + e); + float fb1 = -e / (1.0f + d + e); + +#if defined(MPT_INTMIXER) +#define MPT_FILTER_CONVERT(x) mpt::saturate_round<mixsample_t>((x) * (1 << MIXING_FILTER_PRECISION)) +#else +#define MPT_FILTER_CONVERT(x) (x) +#endif + + switch(chn.nFilterMode) + { + case FilterMode::HighPass: + chn.nFilter_A0 = MPT_FILTER_CONVERT(1.0f - fg); + chn.nFilter_B0 = MPT_FILTER_CONVERT(fb0); + chn.nFilter_B1 = MPT_FILTER_CONVERT(fb1); +#ifdef MPT_INTMIXER + chn.nFilter_HP = -1; +#else + chn.nFilter_HP = 1.0f; +#endif // MPT_INTMIXER + break; + + default: + chn.nFilter_A0 = MPT_FILTER_CONVERT(fg); + chn.nFilter_B0 = MPT_FILTER_CONVERT(fb0); + chn.nFilter_B1 = MPT_FILTER_CONVERT(fb1); +#ifdef MPT_INTMIXER + if(chn.nFilter_A0 == 0) + chn.nFilter_A0 = 1; // Prevent silence at low filter cutoff and very high sampling rate + chn.nFilter_HP = 0; +#else + chn.nFilter_HP = 0; +#endif // MPT_INTMIXER + break; + } +#undef MPT_FILTER_CONVERT + + if (bReset) + { + chn.nFilter_Y[0][0] = chn.nFilter_Y[0][1] = 0; + chn.nFilter_Y[1][0] = chn.nFilter_Y[1][1] = 0; + } + + return computedCutoff; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Snd_fx.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_fx.cpp new file mode 100644 index 00000000..0b311f65 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Snd_fx.cpp @@ -0,0 +1,6344 @@ +/* + * Snd_fx.cpp + * ----------- + * Purpose: Processing of pattern commands, song length calculation... + * Notes : This needs some heavy refactoring. + * I thought of actually adding an effect interface class. Every pattern effect + * could then be moved into its own class that inherits from the effect interface. + * If effect handling differs severly between module formats, every format would have + * its own class for that effect. Then, a call chain of effect classes could be set up + * for each format, since effects cannot be processed in the same order in all formats. + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/Moddoc.h" +#endif // MODPLUG_TRACKER +#include "tuning.h" +#include "Tables.h" +#include "modsmp_ctrl.h" // For updating the loop wraparound data with the invert loop effect +#include "plugins/PlugInterface.h" +#include "OPL.h" +#include "MIDIEvents.h" + +OPENMPT_NAMESPACE_BEGIN + +// Formats which have 7-bit (0...128) instead of 6-bit (0...64) global volume commands, or which are imported to this range (mostly formats which are converted to IT internally) +#ifdef MODPLUG_TRACKER +static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_MT2; +#else +static constexpr auto GLOBALVOL_7BIT_FORMATS_EXT = MOD_TYPE_NONE; +#endif // MODPLUG_TRACKER +static constexpr auto GLOBALVOL_7BIT_FORMATS = MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM | MOD_TYPE_PTM | MOD_TYPE_MDL | MOD_TYPE_DTM | GLOBALVOL_7BIT_FORMATS_EXT; + + +// Compensate frequency slide LUTs depending on whether we are handling periods or frequency - "up" and "down" in function name are seen from frequency perspective. +static uint32 GetLinearSlideDownTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideDownTable[i] : LinearSlideUpTable[i]; } +static uint32 GetLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(LinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? LinearSlideUpTable[i] : LinearSlideDownTable[i]; } +static uint32 GetFineLinearSlideDownTable(const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideDownTable[i] : FineLinearSlideUpTable[i]; } +static uint32 GetFineLinearSlideUpTable (const CSoundFile *sndFile, uint32 i) { MPT_ASSERT(i < std::size(FineLinearSlideDownTable)); return sndFile->m_playBehaviour[kPeriodsAreHertz] ? FineLinearSlideUpTable[i] : FineLinearSlideDownTable[i]; } + + +//////////////////////////////////////////////////////////// +// Length + + +// Memory class for GetLength() code +class GetLengthMemory +{ +protected: + const CSoundFile &sndFile; + +public: + std::unique_ptr<CSoundFile::PlayState> state; + struct ChnSettings + { + uint32 ticksToRender = 0; // When using sample sync, we still need to render this many ticks + bool incChanged = false; // When using sample sync, note frequency has changed + uint8 vol = 0xFF; + }; + + std::vector<ChnSettings> chnSettings; + double elapsedTime; + static constexpr uint32 IGNORE_CHANNEL = uint32_max; + + GetLengthMemory(const CSoundFile &sf) + : sndFile(sf) + , state(std::make_unique<CSoundFile::PlayState>(sf.m_PlayState)) + { + Reset(); + } + + void Reset() + { + if(state->m_midiMacroEvaluationResults) + state->m_midiMacroEvaluationResults.emplace(); + elapsedTime = 0.0; + state->m_lTotalSampleCount = 0; + state->m_nMusicSpeed = sndFile.m_nDefaultSpeed; + state->m_nMusicTempo = sndFile.m_nDefaultTempo; + state->m_nGlobalVolume = sndFile.m_nDefaultGlobalVolume; + chnSettings.assign(sndFile.GetNumChannels(), ChnSettings()); + const auto muteFlag = CSoundFile::GetChannelMuteFlag(); + for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++) + { + state->Chn[chn].Reset(ModChannel::resetTotal, sndFile, chn, muteFlag); + state->Chn[chn].nOldGlobalVolSlide = 0; + state->Chn[chn].nOldChnVolSlide = 0; + state->Chn[chn].nNote = state->Chn[chn].nNewNote = state->Chn[chn].nLastNote = NOTE_NONE; + } + } + + // Increment playback position of sample and envelopes on a channel + void RenderChannel(CHANNELINDEX channel, uint32 tickDuration, uint32 portaStart = uint32_max) + { + ModChannel &chn = state->Chn[channel]; + uint32 numTicks = chnSettings[channel].ticksToRender; + if(numTicks == IGNORE_CHANNEL || numTicks == 0 || (!chn.IsSamplePlaying() && !chnSettings[channel].incChanged) || chn.pModSample == nullptr) + { + return; + } + + const SamplePosition loopStart(chn.dwFlags[CHN_LOOP] ? chn.nLoopStart : 0u, 0); + const SamplePosition sampleEnd(chn.dwFlags[CHN_LOOP] ? chn.nLoopEnd : chn.nLength, 0); + const SmpLength loopLength = chn.nLoopEnd - chn.nLoopStart; + const bool itEnvMode = sndFile.m_playBehaviour[kITEnvelopePositionHandling]; + const bool updatePitchEnv = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED; + bool stopNote = false; + + SamplePosition inc = chn.increment * tickDuration; + if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate(); + + for(uint32 i = 0; i < numTicks; i++) + { + bool updateInc = (chn.PitchEnv.flags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED; + if(i >= portaStart) + { + chn.isFirstTick = false; + const ModCommand &m = *sndFile.Patterns[state->m_nPattern].GetpModCommand(state->m_nRow, channel); + auto command = m.command; + if(m.volcmd == VOLCMD_TONEPORTAMENTO) + { + const auto [porta, clearEffectCommand] = sndFile.GetVolCmdTonePorta(m, 0); + sndFile.TonePortamento(chn, porta); + if(clearEffectCommand) + command = CMD_NONE; + } + if(command == CMD_TONEPORTAMENTO) + sndFile.TonePortamento(chn, m.param); + else if(command == CMD_TONEPORTAVOL) + sndFile.TonePortamento(chn, 0); + updateInc = true; + } + + int32 period = chn.nPeriod; + if(itEnvMode) sndFile.IncrementEnvelopePositions(chn); + if(updatePitchEnv) + { + sndFile.ProcessPitchFilterEnvelope(chn, period); + updateInc = true; + } + if(!itEnvMode) sndFile.IncrementEnvelopePositions(chn); + int vol = 0; + sndFile.ProcessInstrumentFade(chn, vol); + + if(chn.dwFlags[CHN_ADLIB]) + continue; + + if(updateInc || chnSettings[channel].incChanged) + { + if(chn.m_CalculateFreq || chn.m_ReCalculateFreqOnFirstTick) + { + chn.RecalcTuningFreq(1, 0, sndFile); + if(!chn.m_CalculateFreq) + chn.m_ReCalculateFreqOnFirstTick = false; + else + chn.m_CalculateFreq = false; + } + chn.increment = sndFile.GetChannelIncrement(chn, period, 0).first; + chnSettings[channel].incChanged = false; + inc = chn.increment * tickDuration; + if(chn.dwFlags[CHN_PINGPONGFLAG]) inc.Negate(); + } + + chn.position += inc; + + if(chn.position >= sampleEnd || (chn.position < loopStart && inc.IsNegative())) + { + if(!chn.dwFlags[CHN_LOOP]) + { + // Past sample end. + stopNote = true; + break; + } + // We exceeded the sample loop, go back to loop start. + if(chn.dwFlags[CHN_PINGPONGLOOP]) + { + if(chn.position < loopStart) + { + chn.position = SamplePosition(chn.nLoopStart + chn.nLoopStart, 0) - chn.position; + chn.dwFlags.flip(CHN_PINGPONGFLAG); + inc.Negate(); + } + SmpLength posInt = chn.position.GetUInt() - chn.nLoopStart; + SmpLength pingpongLength = loopLength * 2; + if(sndFile.m_playBehaviour[kITPingPongMode]) pingpongLength--; + posInt %= pingpongLength; + bool forward = (posInt < loopLength); + if(forward) + chn.position.SetInt(chn.nLoopStart + posInt); + else + chn.position.SetInt(chn.nLoopEnd - (posInt - loopLength)); + if(forward == chn.dwFlags[CHN_PINGPONGFLAG]) + { + chn.dwFlags.flip(CHN_PINGPONGFLAG); + inc.Negate(); + } + } else + { + SmpLength posInt = chn.position.GetUInt(); + if(posInt >= chn.nLoopEnd + loopLength) + { + const SmpLength overshoot = posInt - chn.nLoopEnd; + posInt -= (overshoot / loopLength) * loopLength; + } + while(posInt >= chn.nLoopEnd) + { + posInt -= loopLength; + } + chn.position.SetInt(posInt); + } + } + } + + if(stopNote) + { + chn.Stop(); + chn.nPortamentoDest = 0; + } + chnSettings[channel].ticksToRender = 0; + } +}; + + +// Get mod length in various cases. Parameters: +// [in] adjustMode: See enmGetLengthResetMode for possible adjust modes. +// [in] target: Time or position target which should be reached, or no target to get length of the first sub song. Use GetLengthTarget::StartPos to also specify a position from where the seeking should begin. +// [out] See definition of type GetLengthType for the returned values. +std::vector<GetLengthType> CSoundFile::GetLength(enmGetLengthResetMode adjustMode, GetLengthTarget target) +{ + std::vector<GetLengthType> results; + GetLengthType retval; + + // Are we trying to reach a certain pattern position? + const bool hasSearchTarget = target.mode != GetLengthTarget::NoTarget && target.mode != GetLengthTarget::GetAllSubsongs; + const bool adjustSamplePos = (adjustMode & eAdjustSamplePositions) == eAdjustSamplePositions; + + SEQUENCEINDEX sequence = target.sequence; + if(sequence >= Order.GetNumSequences()) sequence = Order.GetCurrentSequenceIndex(); + const ModSequence &orderList = Order(sequence); + + GetLengthMemory memory(*this); + CSoundFile::PlayState &playState = *memory.state; + // Temporary visited rows vector (so that GetLength() won't interfere with the player code if the module is playing at the same time) + RowVisitor visitedRows(*this, sequence); + ROWINDEX allowedPatternLoopComplexity = 32768; + + // If sequence starts with some non-existent patterns, find a better start + while(target.startOrder < orderList.size() && !orderList.IsValidPat(target.startOrder)) + { + target.startOrder++; + target.startRow = 0; + } + retval.startRow = playState.m_nNextRow = playState.m_nRow = target.startRow; + retval.startOrder = playState.m_nNextOrder = playState.m_nCurrentOrder = target.startOrder; + + // Fast LUTs for commands that are too weird / complicated / whatever to emulate in sample position adjust mode. + std::bitset<MAX_EFFECTS> forbiddenCommands; + std::bitset<MAX_VOLCMDS> forbiddenVolCommands; + + if(adjustSamplePos) + { + forbiddenCommands.set(CMD_ARPEGGIO); forbiddenCommands.set(CMD_PORTAMENTOUP); + forbiddenCommands.set(CMD_PORTAMENTODOWN); forbiddenCommands.set(CMD_XFINEPORTAUPDOWN); + forbiddenCommands.set(CMD_NOTESLIDEUP); forbiddenCommands.set(CMD_NOTESLIDEUPRETRIG); + forbiddenCommands.set(CMD_NOTESLIDEDOWN); forbiddenCommands.set(CMD_NOTESLIDEDOWNRETRIG); + forbiddenVolCommands.set(VOLCMD_PORTAUP); forbiddenVolCommands.set(VOLCMD_PORTADOWN); + + if(target.mode == GetLengthTarget::SeekPosition && target.pos.order < orderList.size()) + { + // If we know where to seek, we can directly rule out any channels on which a new note would be triggered right at the start. + const PATTERNINDEX seekPat = orderList[target.pos.order]; + if(Patterns.IsValidPat(seekPat) && Patterns[seekPat].IsValidRow(target.pos.row)) + { + const ModCommand *m = Patterns[seekPat].GetpModCommand(target.pos.row, 0); + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, m++) + { + if(m->note == NOTE_NOTECUT || m->note == NOTE_KEYOFF || (m->note == NOTE_FADE && GetNumInstruments()) + || (m->IsNote() && !m->IsPortamento())) + { + memory.chnSettings[i].ticksToRender = GetLengthMemory::IGNORE_CHANNEL; + } + } + } + } + } + + if(adjustMode & eAdjust) + playState.m_midiMacroEvaluationResults.emplace(); + + // If samples are being synced, force them to resync if tick duration changes + uint32 oldTickDuration = 0; + bool breakToRow = false; + + for (;;) + { + const bool ignoreRow = NextRow(playState, breakToRow).first; + + // Time target reached. + if(target.mode == GetLengthTarget::SeekSeconds && memory.elapsedTime >= target.time) + { + retval.targetReached = true; + break; + } + + // Check if pattern is valid + playState.m_nPattern = playState.m_nCurrentOrder < orderList.size() ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex(); + + if(!Patterns.IsValidPat(playState.m_nPattern) && playState.m_nPattern != orderList.GetInvalidPatIndex() && target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order) + { + // Early test: Target is inside +++ or non-existing pattern + retval.targetReached = true; + break; + } + + while(playState.m_nPattern >= Patterns.Size()) + { + // End of song? + if((playState.m_nPattern == orderList.GetInvalidPatIndex()) || (playState.m_nCurrentOrder >= orderList.size())) + { + if(playState.m_nCurrentOrder == orderList.GetRestartPos()) + break; + else + playState.m_nCurrentOrder = orderList.GetRestartPos(); + } else + { + playState.m_nCurrentOrder++; + } + playState.m_nPattern = (playState.m_nCurrentOrder < orderList.size()) ? orderList[playState.m_nCurrentOrder] : orderList.GetInvalidPatIndex(); + playState.m_nNextOrder = playState.m_nCurrentOrder; + if((!Patterns.IsValidPat(playState.m_nPattern)) && visitedRows.Visit(playState.m_nCurrentOrder, 0, playState.Chn, ignoreRow)) + { + if(!hasSearchTarget) + { + retval.lastOrder = playState.m_nCurrentOrder; + retval.lastRow = 0; + } + if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) + { + // We aren't searching for a specific row, or we couldn't find any more unvisited rows. + break; + } else + { + // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. + retval.duration = memory.elapsedTime; + results.push_back(retval); + retval.startRow = playState.m_nRow; + retval.startOrder = playState.m_nNextOrder; + memory.Reset(); + + playState.m_nCurrentOrder = playState.m_nNextOrder; + playState.m_nPattern = orderList[playState.m_nCurrentOrder]; + playState.m_nNextRow = playState.m_nRow; + break; + } + } + } + if(playState.m_nNextOrder == ORDERINDEX_INVALID) + { + // GetFirstUnvisitedRow failed, so there is nothing more to play + break; + } + + // Skip non-existing patterns + if(!Patterns.IsValidPat(playState.m_nPattern)) + { + // If there isn't even a tune, we should probably stop here. + if(playState.m_nCurrentOrder == orderList.GetRestartPos()) + { + if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) + { + // We aren't searching for a specific row, or we couldn't find any more unvisited rows. + break; + } else + { + // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. + retval.duration = memory.elapsedTime; + results.push_back(retval); + retval.startRow = playState.m_nRow; + retval.startOrder = playState.m_nNextOrder; + memory.Reset(); + playState.m_nNextRow = playState.m_nRow; + continue; + } + } + playState.m_nNextOrder = playState.m_nCurrentOrder + 1; + continue; + } + // Should never happen + if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows()) + playState.m_nRow = 0; + + // Check whether target was reached. + if(target.mode == GetLengthTarget::SeekPosition && playState.m_nCurrentOrder == target.pos.order && playState.m_nRow == target.pos.row) + { + retval.targetReached = true; + break; + } + + // If pattern loops are nested too deeply, they can cause an effectively infinite amount of loop evalations to be generated. + // As we don't want the user to wait forever, we bail out if the pattern loops are too complex. + const bool moduleTooComplex = target.mode != GetLengthTarget::SeekSeconds && visitedRows.ModuleTooComplex(allowedPatternLoopComplexity); + if(moduleTooComplex) + { + memory.elapsedTime = std::numeric_limits<decltype(memory.elapsedTime)>::infinity(); + // Decrease allowed complexity with each subsong, as this seems to be a malicious module + if(allowedPatternLoopComplexity > 256) + allowedPatternLoopComplexity /= 2; + visitedRows.ResetComplexity(); + } + + if(visitedRows.Visit(playState.m_nCurrentOrder, playState.m_nRow, playState.Chn, ignoreRow) || moduleTooComplex) + { + if(!hasSearchTarget) + { + retval.lastOrder = playState.m_nCurrentOrder; + retval.lastRow = playState.m_nRow; + } + if(target.mode == GetLengthTarget::NoTarget || !visitedRows.GetFirstUnvisitedRow(playState.m_nNextOrder, playState.m_nRow, true)) + { + // We aren't searching for a specific row, or we couldn't find any more unvisited rows. + break; + } else + { + // We haven't found the target row yet, but we found some other unplayed row... continue searching from here. + retval.duration = memory.elapsedTime; + results.push_back(retval); + retval.startRow = playState.m_nRow; + retval.startOrder = playState.m_nNextOrder; + memory.Reset(); + playState.m_nNextRow = playState.m_nRow; + continue; + } + } + + retval.endOrder = playState.m_nCurrentOrder; + retval.endRow = playState.m_nRow; + + // Update next position + SetupNextRow(playState, false); + + // Jumped to invalid pattern row? + if(playState.m_nRow >= Patterns[playState.m_nPattern].GetNumRows()) + { + playState.m_nRow = 0; + } + + if(ignoreRow) + continue; + + // For various effects, we need to know first how many ticks there are in this row. + const ModCommand *p = Patterns[playState.m_nPattern].GetpModCommand(playState.m_nRow, 0); + const bool ignoreMutedChn = m_playBehaviour[kST3NoMutedChannels]; + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++, p++) + { + ModChannel &chn = playState.Chn[nChn]; + if(p->IsEmpty() || (ignoreMutedChn && ChnSettings[nChn].dwFlags[CHN_MUTE])) // not even effects are processed on muted S3M channels + { + chn.rowCommand.Clear(); + continue; + } + if(p->IsPcNote()) + { +#ifndef NO_PLUGINS + if(playState.m_midiMacroEvaluationResults && p->instr > 0 && p->instr <= MAX_MIXPLUGINS) + { + playState.m_midiMacroEvaluationResults->pluginParameter[{static_cast<PLUGINDEX>(p->instr - 1), p->GetValueVolCol()}] = p->GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue); + } +#endif // NO_PLUGINS + chn.rowCommand.Clear(); + continue; + } + chn.rowCommand = *p; + switch(p->command) + { + case CMD_SPEED: + SetSpeed(playState, p->param); + break; + + case CMD_TEMPO: + if(m_playBehaviour[kMODVBlankTiming]) + { + // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count. + if(p->param != 0) SetSpeed(playState, p->param); + } + break; + + case CMD_S3MCMDEX: + if(!chn.rowCommand.param && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) + chn.rowCommand.param = chn.nOldCmdEx; + else + chn.nOldCmdEx = static_cast<ModCommand::PARAM>(chn.rowCommand.param); + if((p->param & 0xF0) == 0x60) + { + // Fine Pattern Delay + playState.m_nFrameDelay += (p->param & 0x0F); + } else if((p->param & 0xF0) == 0xE0 && !playState.m_nPatternDelay) + { + // Pattern Delay + if(!(GetType() & MOD_TYPE_S3M) || (p->param & 0x0F) != 0) + { + // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right), + // Scream Tracker 3 simply ignores such commands. + playState.m_nPatternDelay = 1 + (p->param & 0x0F); + } + } + break; + + case CMD_MODCMDEX: + if((p->param & 0xF0) == 0xE0) + { + // Pattern Delay + playState.m_nPatternDelay = 1 + (p->param & 0x0F); + } + break; + } + } + const uint32 numTicks = playState.TicksOnRow(); + const uint32 nonRowTicks = numTicks - std::max(playState.m_nPatternDelay, uint32(1)); + + playState.m_patLoopRow = ROWINDEX_INVALID; + playState.m_breakRow = ROWINDEX_INVALID; + playState.m_posJump = ORDERINDEX_INVALID; + + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) + { + ModChannel &chn = playState.Chn[nChn]; + if(chn.rowCommand.IsEmpty()) + continue; + ModCommand::COMMAND command = chn.rowCommand.command; + ModCommand::PARAM param = chn.rowCommand.param; + ModCommand::NOTE note = chn.rowCommand.note; + + if(adjustMode & eAdjust) + { + if(chn.rowCommand.instr) + { + chn.nNewIns = chn.rowCommand.instr; + chn.nLastNote = NOTE_NONE; + memory.chnSettings[nChn].vol = 0xFF; + } + if(chn.rowCommand.IsNote()) + { + chn.nLastNote = note; + chn.RestorePanAndFilter(); + } + + // Update channel panning + if(chn.rowCommand.IsNote() || chn.rowCommand.instr) + { + ModInstrument *pIns; + if(chn.nNewIns > 0 && chn.nNewIns <= GetNumInstruments() && (pIns = Instruments[chn.nNewIns]) != nullptr) + { + if(pIns->dwFlags[INS_SETPANNING]) + chn.SetInstrumentPan(pIns->nPan, *this); + } + const SAMPLEINDEX smp = GetSampleIndex(note, chn.nNewIns); + if(smp > 0) + { + if(Samples[smp].uFlags[CHN_PANNING]) + chn.SetInstrumentPan(Samples[smp].nPan, *this); + } + } + + switch(chn.rowCommand.volcmd) + { + case VOLCMD_VOLUME: + memory.chnSettings[nChn].vol = chn.rowCommand.vol; + break; + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + if(chn.rowCommand.vol != 0) + chn.nOldVolParam = chn.rowCommand.vol; + break; + case VOLCMD_TONEPORTAMENTO: + if(chn.rowCommand.vol) + { + const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, 0); + chn.portamentoSlide = porta; + if(clearEffectCommand) + command = CMD_NONE; + } + break; + } + } + + switch(command) + { + // Position Jump + case CMD_POSITIONJUMP: + PositionJump(playState, nChn); + break; + + // Pattern Break + case CMD_PATTERNBREAK: + if(ROWINDEX row = PatternBreak(playState, nChn, param); row != ROWINDEX_INVALID) + playState.m_breakRow = row; + break; + + // Set Tempo + case CMD_TEMPO: + if(!m_playBehaviour[kMODVBlankTiming]) + { + TEMPO tempo(CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn), 0); + if ((adjustMode & eAdjust) && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) + { + if (tempo.GetInt()) chn.nOldTempo = static_cast<uint8>(tempo.GetInt()); else tempo.Set(chn.nOldTempo); + } + + if (tempo.GetInt() >= 0x20) playState.m_nMusicTempo = tempo; + else + { + // Tempo Slide + TEMPO tempoDiff((tempo.GetInt() & 0x0F) * nonRowTicks, 0); + if ((tempo.GetInt() & 0xF0) == 0x10) + { + playState.m_nMusicTempo += tempoDiff; + } else + { + if(tempoDiff < playState.m_nMusicTempo) + playState.m_nMusicTempo -= tempoDiff; + else + playState.m_nMusicTempo.Set(0); + } + } + + TEMPO tempoMin = GetModSpecifications().GetTempoMin(), tempoMax = GetModSpecifications().GetTempoMax(); + if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode + { + tempoMax.Set(255); + } + Limit(playState.m_nMusicTempo, tempoMin, tempoMax); + } + break; + + case CMD_S3MCMDEX: + switch(param & 0xF0) + { + case 0x90: + if(param <= 0x91) + chn.dwFlags.set(CHN_SURROUND, param == 0x91); + break; + + case 0xA0: // High sample offset + chn.nOldHiOffset = param & 0x0F; + break; + + case 0xB0: // Pattern Loop + PatternLoop(playState, chn, param & 0x0F); + break; + + case 0xF0: // Active macro + chn.nActiveMacro = param & 0x0F; + break; + } + break; + + case CMD_MODCMDEX: + switch(param & 0xF0) + { + case 0x60: // Pattern Loop + PatternLoop(playState, chn, param & 0x0F); + break; + + case 0xF0: // Active macro + chn.nActiveMacro = param & 0x0F; + break; + } + break; + + case CMD_XFINEPORTAUPDOWN: + // ignore high offset in compatible mode + if(((param & 0xF0) == 0xA0) && !m_playBehaviour[kFT2RestrictXCommand]) + chn.nOldHiOffset = param & 0x0F; + break; + } + + // The following calculations are not interesting if we just want to get the song length. + if(!(adjustMode & eAdjust)) + continue; + switch(command) + { + // Portamento Up/Down + case CMD_PORTAMENTOUP: + if(param) + { + // FT2 compatibility: Separate effect memory for all portamento commands + // Test case: Porta-LinkMem.xm + if(!m_playBehaviour[kFT2PortaUpDownMemory]) + chn.nOldPortaDown = param; + chn.nOldPortaUp = param; + } + break; + case CMD_PORTAMENTODOWN: + if(param) + { + // FT2 compatibility: Separate effect memory for all portamento commands + // Test case: Porta-LinkMem.xm + if(!m_playBehaviour[kFT2PortaUpDownMemory]) + chn.nOldPortaUp = param; + chn.nOldPortaDown = param; + } + break; + // Tone-Portamento + case CMD_TONEPORTAMENTO: + if (param) chn.portamentoSlide = param; + break; + // Offset + case CMD_OFFSET: + if(param) + chn.oldOffset = param << 8; + break; + // Volume Slide + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + if (param) chn.nOldVolumeSlide = param; + break; + // Set Volume + case CMD_VOLUME: + memory.chnSettings[nChn].vol = param; + break; + // Global Volume + case CMD_GLOBALVOLUME: + if(!(GetType() & GLOBALVOL_7BIT_FORMATS) && param < 128) param *= 2; + // IT compatibility 16. ST3 and IT ignore out-of-range values + if(param <= 128) + { + playState.m_nGlobalVolume = param * 2; + } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M))) + { + playState.m_nGlobalVolume = 256; + } + break; + // Global Volume Slide + case CMD_GLOBALVOLSLIDE: + if(m_playBehaviour[kPerChannelGlobalVolSlide]) + { + // IT compatibility 16. Global volume slide params are stored per channel (FT2/IT) + if (param) chn.nOldGlobalVolSlide = param; else param = chn.nOldGlobalVolSlide; + } else + { + if (param) playState.Chn[0].nOldGlobalVolSlide = param; else param = playState.Chn[0].nOldGlobalVolSlide; + } + if (((param & 0x0F) == 0x0F) && (param & 0xF0)) + { + param >>= 4; + if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; + playState.m_nGlobalVolume += param << 1; + } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) + { + param = (param & 0x0F) << 1; + if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; + playState.m_nGlobalVolume -= param; + } else if (param & 0xF0) + { + param >>= 4; + param <<= 1; + if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; + playState.m_nGlobalVolume += param * nonRowTicks; + } else + { + param = (param & 0x0F) << 1; + if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param <<= 1; + playState.m_nGlobalVolume -= param * nonRowTicks; + } + Limit(playState.m_nGlobalVolume, 0, 256); + break; + case CMD_CHANNELVOLUME: + if (param <= 64) chn.nGlobalVol = param; + break; + case CMD_CHANNELVOLSLIDE: + { + if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide; + int32 volume = chn.nGlobalVol; + if((param & 0x0F) == 0x0F && (param & 0xF0)) + volume += (param >> 4); // Fine Up + else if((param & 0xF0) == 0xF0 && (param & 0x0F)) + volume -= (param & 0x0F); // Fine Down + else if(param & 0x0F) // Down + volume -= (param & 0x0F) * nonRowTicks; + else // Up + volume += ((param & 0xF0) >> 4) * nonRowTicks; + Limit(volume, 0, 64); + chn.nGlobalVol = volume; + } + break; + case CMD_PANNING8: + Panning(chn, param, Pan8bit); + break; + case CMD_MODCMDEX: + if(param < 0x10) + { + // LED filter + for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++) + { + playState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1)); + } + } + [[fallthrough]]; + case CMD_S3MCMDEX: + if((param & 0xF0) == 0x80) + { + Panning(chn, (param & 0x0F), Pan4bit); + } + break; + + case CMD_VIBRATOVOL: + if (param) chn.nOldVolumeSlide = param; + param = 0; + [[fallthrough]]; + case CMD_VIBRATO: + Vibrato(chn, param); + break; + case CMD_FINEVIBRATO: + FineVibrato(chn, param); + break; + case CMD_TREMOLO: + Tremolo(chn, param); + break; + case CMD_PANBRELLO: + Panbrello(chn, param); + break; + + case CMD_MIDI: + case CMD_SMOOTHMIDI: + if(param < 0x80) + ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param, 0); + else + ProcessMIDIMacro(playState, nChn, false, m_MidiCfg.Zxx[param & 0x7F], chn.rowCommand.param, 0); + break; + + default: + break; + } + + switch(chn.rowCommand.volcmd) + { + case VOLCMD_PANNING: + Panning(chn, chn.rowCommand.vol, Pan6bit); + break; + + case VOLCMD_VIBRATOSPEED: + // FT2 does not automatically enable vibrato with the "set vibrato speed" command + if(m_playBehaviour[kFT2VolColVibrato]) + chn.nVibratoSpeed = chn.rowCommand.vol & 0x0F; + else + Vibrato(chn, chn.rowCommand.vol << 4); + break; + case VOLCMD_VIBRATODEPTH: + Vibrato(chn, chn.rowCommand.vol); + break; + } + + // Process vibrato / tremolo / panbrello + switch(chn.rowCommand.command) + { + case CMD_VIBRATO: + case CMD_FINEVIBRATO: + case CMD_VIBRATOVOL: + if(adjustMode & eAdjust) + { + uint32 vibTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks; + uint32 inc = chn.nVibratoSpeed * vibTicks; + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + inc *= 4; + chn.nVibratoPos += static_cast<uint8>(inc); + } + break; + + case CMD_TREMOLO: + if(adjustMode & eAdjust) + { + uint32 tremTicks = ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) ? numTicks : nonRowTicks; + uint32 inc = chn.nTremoloSpeed * tremTicks; + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + inc *= 4; + chn.nTremoloPos += static_cast<uint8>(inc); + } + break; + + case CMD_PANBRELLO: + if(adjustMode & eAdjust) + { + // Panbrello effect is permanent in compatible mode, so actually apply panbrello for the last tick of this row + chn.nPanbrelloPos += static_cast<uint8>(chn.nPanbrelloSpeed * (numTicks - 1)); + ProcessPanbrello(chn); + } + break; + } + + if(m_playBehaviour[kST3EffectMemory] && param != 0) + { + UpdateS3MEffectMemory(chn, param); + } + } + + // Interpret F00 effect in XM files as "stop song" + if(GetType() == MOD_TYPE_XM && playState.m_nMusicSpeed == uint16_max) + { + break; + } + + playState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat; + if(Patterns[playState.m_nPattern].GetOverrideSignature()) + { + playState.m_nCurrentRowsPerBeat = Patterns[playState.m_nPattern].GetRowsPerBeat(); + } + + const uint32 tickDuration = GetTickDuration(playState); + const uint32 rowDuration = tickDuration * numTicks; + memory.elapsedTime += static_cast<double>(rowDuration) / static_cast<double>(m_MixerSettings.gdwMixingFreq); + playState.m_lTotalSampleCount += rowDuration; + + if(adjustSamplePos) + { + // Super experimental and dirty sample seeking + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) + { + if(memory.chnSettings[nChn].ticksToRender == GetLengthMemory::IGNORE_CHANNEL) + continue; + + ModChannel &chn = playState.Chn[nChn]; + const ModCommand &m = chn.rowCommand; + if(!chn.nPeriod && m.IsEmpty()) + continue; + + uint32 paramHi = m.param >> 4, paramLo = m.param & 0x0F; + uint32 startTick = 0; + bool porta = m.command == CMD_TONEPORTAMENTO || m.command == CMD_TONEPORTAVOL || m.volcmd == VOLCMD_TONEPORTAMENTO; + bool stopNote = false; + + if(m.instr) chn.prevNoteOffset = 0; + if(m.IsNote()) + { + if(porta && memory.chnSettings[nChn].incChanged) + { + // If there's a portamento, the current channel increment mustn't be 0 in NoteChange() + chn.increment = GetChannelIncrement(chn, chn.nPeriod, 0).first; + } + int32 setPan = chn.nPan; + chn.nNewNote = chn.nLastNote; + if(chn.nNewIns != 0) InstrumentChange(chn, chn.nNewIns, porta); + NoteChange(chn, m.note, porta); + HandleDigiSamplePlayDirection(playState, nChn); + memory.chnSettings[nChn].incChanged = true; + + if((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xD0 && paramLo < numTicks) + { + startTick = paramLo; + } else if(m.command == CMD_DELAYCUT && paramHi < numTicks) + { + startTick = paramHi; + } + if(playState.m_nPatternDelay > 1 && startTick != 0 && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) + { + startTick += (playState.m_nMusicSpeed + playState.m_nFrameDelay) * (playState.m_nPatternDelay - 1); + } + if(!porta) memory.chnSettings[nChn].ticksToRender = 0; + + // Panning commands have to be re-applied after a note change with potential pan change. + if(m.command == CMD_PANNING8 + || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && paramHi == 0x8) + || m.volcmd == VOLCMD_PANNING) + { + chn.nPan = setPan; + } + } + + if(m.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote]) + { + if(m.command == CMD_OFFSET) + { + ProcessSampleOffset(chn, nChn, playState); + } else if(m.command == CMD_OFFSETPERCENTAGE) + { + SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, m.param, 256)); + } else if(m.command == CMD_REVERSEOFFSET && chn.pModSample != nullptr) + { + memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far + ReverseSampleOffset(chn, m.param); + startTick = playState.m_nMusicSpeed - 1; + } else if(m.volcmd == VOLCMD_OFFSET) + { + if(chn.pModSample != nullptr && m.vol <= std::size(chn.pModSample->cues)) + { + SmpLength offset; + if(m.vol == 0) + offset = chn.oldOffset; + else + offset = chn.oldOffset = chn.pModSample->cues[m.vol - 1]; + SampleOffset(chn, offset); + } + } + } + + if(m.note == NOTE_KEYOFF || m.note == NOTE_NOTECUT || (m.note == NOTE_FADE && GetNumInstruments()) + || ((m.command == CMD_MODCMDEX || m.command == CMD_S3MCMDEX) && (m.param & 0xF0) == 0xC0 && paramLo < numTicks) + || (m.command == CMD_DELAYCUT && paramLo != 0 && startTick + paramLo < numTicks) + || m.command == CMD_KEYOFF) + { + stopNote = true; + } + + if(m.command == CMD_VOLUME) + { + chn.nVolume = m.param * 4; + } else if(m.volcmd == VOLCMD_VOLUME) + { + chn.nVolume = m.vol * 4; + } + + if(chn.pModSample && !stopNote) + { + // Check if we don't want to emulate some effect and thus stop processing. + if(m.command < MAX_EFFECTS) + { + if(forbiddenCommands[m.command]) + { + stopNote = true; + } else if(m.command == CMD_MODCMDEX) + { + // Special case: Slides using extended commands + switch(m.param & 0xF0) + { + case 0x10: + case 0x20: + stopNote = true; + } + } + } + + if(m.volcmd < forbiddenVolCommands.size() && forbiddenVolCommands[m.volcmd]) + { + stopNote = true; + } + } + + if(stopNote) + { + chn.Stop(); + memory.chnSettings[nChn].ticksToRender = 0; + } else + { + if(oldTickDuration != tickDuration && oldTickDuration != 0) + { + memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far + } + + switch(m.command) + { + case CMD_TONEPORTAVOL: + case CMD_VOLUMESLIDE: + case CMD_VIBRATOVOL: + if(m.param || (GetType() != MOD_TYPE_MOD)) + { + for(uint32 i = 0; i < numTicks; i++) + { + chn.isFirstTick = (i == 0); + VolumeSlide(chn, m.param); + } + } + break; + + case CMD_MODCMDEX: + if((m.param & 0x0F) || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) + { + chn.isFirstTick = true; + switch(m.param & 0xF0) + { + case 0xA0: FineVolumeUp(chn, m.param & 0x0F, false); break; + case 0xB0: FineVolumeDown(chn, m.param & 0x0F, false); break; + } + } + break; + + case CMD_S3MCMDEX: + if(m.param == 0x9E) + { + // Play forward + memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far + chn.dwFlags.reset(CHN_PINGPONGFLAG); + } else if(m.param == 0x9F) + { + // Reverse + memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far + chn.dwFlags.set(CHN_PINGPONGFLAG); + if(!chn.position.GetInt() && chn.nLength && (m.IsNote() || !chn.dwFlags[CHN_LOOP])) + { + chn.position.Set(chn.nLength - 1, SamplePosition::fractMax); + } + } else if((m.param & 0xF0) == 0x70) + { + if(m.param >= 0x73) + chn.InstrumentControl(m.param, *this); + } + break; + + case CMD_DIGIREVERSESAMPLE: + DigiBoosterSampleReverse(chn, m.param); + break; + + case CMD_FINETUNE: + case CMD_FINETUNE_SMOOTH: + memory.RenderChannel(nChn, oldTickDuration); // Re-sync what we've got so far + SetFinetune(nChn, playState, false); // TODO should render each tick individually for CMD_FINETUNE_SMOOTH for higher sync accuracy + break; + } + chn.isFirstTick = true; + switch(m.volcmd) + { + case VOLCMD_FINEVOLUP: FineVolumeUp(chn, m.vol, m_playBehaviour[kITVolColMemory]); break; + case VOLCMD_FINEVOLDOWN: FineVolumeDown(chn, m.vol, m_playBehaviour[kITVolColMemory]); break; + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + { + // IT Compatibility: Volume column volume slides have their own memory + // Test case: VolColMemory.it + ModCommand::VOL vol = m.vol; + if(vol == 0 && m_playBehaviour[kITVolColMemory]) + { + vol = chn.nOldVolParam; + if(vol == 0) + break; + } + if(m.volcmd == VOLCMD_VOLSLIDEUP) + vol <<= 4; + for(uint32 i = 0; i < numTicks; i++) + { + chn.isFirstTick = (i == 0); + VolumeSlide(chn, vol); + } + } + break; + case VOLCMD_PLAYCONTROL: + if(m.vol <= 1) + chn.isPaused = (m.vol == 0); + break; + } + + if(chn.isPaused) + continue; + if(porta) + { + // Portamento needs immediate syncing, as the pitch changes on each tick + uint32 portaTick = memory.chnSettings[nChn].ticksToRender + startTick + 1; + memory.chnSettings[nChn].ticksToRender += numTicks; + memory.RenderChannel(nChn, tickDuration, portaTick); + } else + { + memory.chnSettings[nChn].ticksToRender += (numTicks - startTick); + } + } + } + } + oldTickDuration = tickDuration; + + breakToRow = HandleNextRow(playState, orderList, false); + } + + // Now advance the sample positions for sample seeking on channels that are still playing + if(adjustSamplePos) + { + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) + { + if(memory.chnSettings[nChn].ticksToRender != GetLengthMemory::IGNORE_CHANNEL) + { + memory.RenderChannel(nChn, oldTickDuration); + } + } + } + + if(retval.targetReached) + { + retval.lastOrder = playState.m_nCurrentOrder; + retval.lastRow = playState.m_nRow; + } + retval.duration = memory.elapsedTime; + results.push_back(retval); + + // Store final variables + if(adjustMode & eAdjust) + { + if(retval.targetReached || target.mode == GetLengthTarget::NoTarget) + { + const auto midiMacroEvaluationResults = std::move(playState.m_midiMacroEvaluationResults); + playState.m_midiMacroEvaluationResults.reset(); + // Target found, or there is no target (i.e. play whole song)... + m_PlayState = std::move(playState); + m_PlayState.ResetGlobalVolumeRamping(); + m_PlayState.m_nNextRow = m_PlayState.m_nRow; + m_PlayState.m_nFrameDelay = m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_bPositionChanged = true; + if(m_opl != nullptr) + m_opl->Reset(); + for(CHANNELINDEX n = 0; n < GetNumChannels(); n++) + { + auto &chn = m_PlayState.Chn[n]; + if(chn.nLastNote != NOTE_NONE) + { + chn.nNewNote = chn.nLastNote; + } + if(memory.chnSettings[n].vol != 0xFF && !adjustSamplePos) + { + chn.nVolume = std::min(memory.chnSettings[n].vol, uint8(64)) * 4; + } + if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl) + { + m_opl->Patch(n, chn.pModSample->adlib); + m_opl->NoteCut(n); + } + chn.pCurrentSample = nullptr; + } + +#ifndef NO_PLUGINS + // If there were any PC events or MIDI macros updating plugin parameters, update plugin parameters to their latest value. + std::bitset<MAX_MIXPLUGINS> plugSetProgram; + for(const auto &[plugParam, value] : midiMacroEvaluationResults->pluginParameter) + { + PLUGINDEX plug = plugParam.first; + IMixPlugin *plugin = m_MixPlugins[plug].pMixPlugin; + if(plugin != nullptr) + { + if(!plugSetProgram[plug]) + { + // Used for bridged plugins to avoid sending out individual messages for each parameter. + plugSetProgram.set(plug); + plugin->BeginSetProgram(); + } + plugin->SetParameter(plugParam.second, value); + } + } + if(plugSetProgram.any()) + { + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + if(plugSetProgram[i]) + { + m_MixPlugins[i].pMixPlugin->EndSetProgram(); + } + } + } + // Do the same for dry/wet ratios + for(const auto &[plug, dryWetRatio] : midiMacroEvaluationResults->pluginDryWetRatio) + { + m_MixPlugins[plug].fDryRatio = dryWetRatio; + } +#endif // NO_PLUGINS + } else if(adjustMode != eAdjustOnSuccess) + { + // Target not found (e.g. when jumping to a hidden sub song), reset global variables... + m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; + m_PlayState.m_nMusicTempo = m_nDefaultTempo; + m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume; + } + // When adjusting the playback status, we will also want to update the visited rows vector according to the current position. + if(sequence != Order.GetCurrentSequenceIndex()) + { + Order.SetSequence(sequence); + } + } + if(adjustMode & (eAdjust | eAdjustOnlyVisitedRows)) + m_visitedRows.MoveVisitedRowsFrom(visitedRows); + + return results; +} + + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Effects + +// Change sample or instrument number. +void CSoundFile::InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta, bool bUpdVol, bool bResetEnv) const +{ + const ModInstrument *pIns = instr <= GetNumInstruments() ? Instruments[instr] : nullptr; + const ModSample *pSmp = &Samples[instr]; + const auto oldInsVol = chn.nInsVol; + ModCommand::NOTE note = chn.nNewNote; + + if(note == NOTE_NONE && m_playBehaviour[kITInstrWithoutNote]) return; + + if(pIns != nullptr && ModCommand::IsNote(note)) + { + // Impulse Tracker ignores empty slots. + // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. + // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it + if(pIns->Keyboard[note - NOTE_MIN] == 0 && m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel()) + { + chn.pModInstrument = pIns; + return; + } + + if(pIns->NoteMap[note - NOTE_MIN] > NOTE_MAX) return; + uint32 n = pIns->Keyboard[note - NOTE_MIN]; + pSmp = ((n) && (n < MAX_SAMPLES)) ? &Samples[n] : nullptr; + } else if(GetNumInstruments()) + { + // No valid instrument, or not a valid note. + if (note >= NOTE_MIN_SPECIAL) return; + if(m_playBehaviour[kITEmptyNoteMapSlot] && (pIns == nullptr || !pIns->HasValidMIDIChannel())) + { + // Impulse Tracker ignores empty slots. + // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. + // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it + chn.pModInstrument = nullptr; + chn.nNewIns = 0; + return; + } + pSmp = nullptr; + } + + bool returnAfterVolumeAdjust = false; + + // instrumentChanged is used for IT carry-on env option + bool instrumentChanged = (pIns != chn.pModInstrument); + const bool sampleChanged = (chn.pModSample != nullptr) && (pSmp != chn.pModSample); + const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns && pIns->pTuning); + + if(!bPorta || instrumentChanged || sampleChanged) + chn.microTuning = 0; + + // Playback behavior change for MPT: With portamento don't change sample if it is in + // the same instrument as previous sample. + if(bPorta && newTuning && pIns == chn.pModInstrument && sampleChanged) + return; + + if(sampleChanged && bPorta) + { + // IT compatibility: No sample change (also within multi-sample instruments) during portamento when using Compatible Gxx. + // Test case: PortaInsNumCompat.it, PortaSampleCompat.it, PortaCutCompat.it + if(m_playBehaviour[kITPortamentoInstrument] && m_SongFlags[SONG_ITCOMPATGXX] && !chn.increment.IsZero()) + { + pSmp = chn.pModSample; + } + + // Special XM hack (also applies to MOD / S3M, except when playing IT-style S3Ms, such as k_vision.s3m) + // Test case: PortaSmpChange.mod, PortaSmpChange.s3m, PortaSwap.s3m + if((!instrumentChanged && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) && pIns) + || (GetType() == MOD_TYPE_PLM) + || (GetType() == MOD_TYPE_MOD && chn.IsSamplePlaying()) + || (m_playBehaviour[kST3PortaSampleChange] && chn.IsSamplePlaying())) + { + // FT2 doesn't change the sample in this case, + // but still uses the sample info from the old one (bug?) + returnAfterVolumeAdjust = true; + } + } + // IT compatibility: A lone instrument number should only reset sample properties to those of the corresponding sample in instrument mode. + // C#5 01 ... <-- sample 1 + // C-5 .. g02 <-- sample 2 + // ... 01 ... <-- still sample 1, but with properties of sample 2 + // In the above example, no sample change happens on the second row. In the third row, sample 1 keeps playing but with the + // volume and panning properties of sample 2. + // Test case: InstrAfterMultisamplePorta.it + if(m_nInstruments && !instrumentChanged && sampleChanged && chn.pCurrentSample != nullptr && m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.rowCommand.IsNote()) + { + returnAfterVolumeAdjust = true; + } + + // IT Compatibility: Envelope pickup after SCx cut (but don't do this when working with plugins, or else envelope carry stops working) + // Test case: cut-carry.it + if(!chn.IsSamplePlaying() && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && (!pIns || !pIns->HasValidMIDIChannel())) + { + instrumentChanged = true; + } + + // FT2 compatibility: new instrument + portamento = ignore new instrument number, but reload old instrument settings (the world of XM is upside down...) + // And this does *not* happen if volume column portamento is used together with note delay... (handled in ProcessEffects(), where all the other note delay stuff is.) + // Test case: porta-delay.xm, SamplePortaInInstrument.xm + if((instrumentChanged || sampleChanged) && bPorta && m_playBehaviour[kFT2PortaIgnoreInstr] && (chn.pModInstrument != nullptr || chn.pModSample != nullptr)) + { + pIns = chn.pModInstrument; + pSmp = chn.pModSample; + instrumentChanged = false; + } else + { + chn.pModInstrument = pIns; + } + + // Update Volume + if (bUpdVol && (!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)) || ((pSmp != nullptr && pSmp->HasSampleData()) || chn.HasMIDIOutput()))) + { + if(pSmp) + { + if(!pSmp->uFlags[SMP_NODEFAULTVOLUME]) + chn.nVolume = pSmp->nVolume; + } else if(pIns && pIns->nMixPlug) + { + chn.nVolume = chn.GetVSTVolume(); + } else + { + chn.nVolume = 0; + } + } + + if(returnAfterVolumeAdjust && sampleChanged && pSmp != nullptr) + { + // ProTracker applies new instrument's finetune but keeps the old sample playing. + // Test case: PortaSwapPT.mod + if(m_playBehaviour[kMODSampleSwap]) + chn.nFineTune = pSmp->nFineTune; + // ST3 does it similarly for middle-C speed. + // Test case: PortaSwap.s3m, SampleSwap.s3m + if(GetType() == MOD_TYPE_S3M && pSmp->HasSampleData()) + chn.nC5Speed = pSmp->nC5Speed; + } + + if(returnAfterVolumeAdjust) return; + + // Instrument adjust + chn.nNewIns = 0; + + // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes s7xinsnum.it). + if (pIns && ((!m_playBehaviour[kITNNAReset] && pSmp) || pIns->nMixPlug || instrumentChanged)) + chn.nNNA = pIns->nNNA; + + // Update volume + chn.UpdateInstrumentVolume(pSmp, pIns); + + // Update panning + // FT2 compatibility: Only reset panning on instrument numbers, not notes (bUpdVol condition) + // Test case: PanMemory.xm + // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change + // Test case: PanReset.it + if((bUpdVol || !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) && !m_playBehaviour[kITPanningReset]) + { + ApplyInstrumentPanning(chn, pIns, pSmp); + } + + // Reset envelopes + if(bResetEnv) + { + // Blurb by Storlek (from the SchismTracker code): + // Conditions experimentally determined to cause envelope reset in Impulse Tracker: + // - no note currently playing (of course) + // - note given, no portamento + // - instrument number given, portamento, compat gxx enabled + // - instrument number given, no portamento, after keyoff, old effects enabled + // If someone can enlighten me to what the logic really is here, I'd appreciate it. + // Seems like it's just a total mess though, probably to get XMs to play right. + + bool reset, resetAlways; + + // IT Compatibility: Envelope reset + // Test case: EnvReset.it + if(m_playBehaviour[kITEnvelopeReset]) + { + const bool insNumber = (instr != 0); + reset = (!chn.nLength + || (insNumber && bPorta && m_SongFlags[SONG_ITCOMPATGXX]) + || (insNumber && !bPorta && chn.dwFlags[CHN_NOTEFADE | CHN_KEYOFF] && m_SongFlags[SONG_ITOLDEFFECTS])); + // NOTE: IT2.14 with SB/GUS/etc. output is different. We are going after IT's WAV writer here. + // For SB/GUS/etc. emulation, envelope carry should only apply when the NNA isn't set to "Note Cut". + // Test case: CarryNNA.it + resetAlways = (!chn.nFadeOutVol || instrumentChanged || chn.dwFlags[CHN_KEYOFF]); + } else + { + reset = (!bPorta || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || m_SongFlags[SONG_ITCOMPATGXX] + || !chn.nLength || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol)); + resetAlways = !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) || instrumentChanged || pIns == nullptr || chn.dwFlags[CHN_KEYOFF | CHN_NOTEFADE]; + } + + if(reset) + { + chn.dwFlags.set(CHN_FASTVOLRAMP); + if(pIns != nullptr) + { + if(resetAlways) + { + chn.ResetEnvelopes(); + } else + { + if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset(); + if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset(); + if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset(); + } + } + + // IT Compatibility: Autovibrato reset + if(!m_playBehaviour[kITVibratoTremoloPanbrello]) + { + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + } + } else if(pIns != nullptr && !pIns->VolEnv.dwFlags[ENV_ENABLED]) + { + if(m_playBehaviour[kITPortamentoInstrument]) + { + chn.VolEnv.Reset(); + } else + { + chn.ResetEnvelopes(); + } + } + } + // Invalid sample ? + if(pSmp == nullptr && (pIns == nullptr || !pIns->HasValidMIDIChannel())) + { + chn.pModSample = nullptr; + chn.nInsVol = 0; + return; + } + + // Tone-Portamento doesn't reset the pingpong direction flag + if(bPorta && pSmp == chn.pModSample && pSmp != nullptr) + { + // If channel length is 0, we cut a previous sample using SCx. In that case, we have to update sample length, loop points, etc... + if(GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT) && chn.nLength != 0) + return; + // FT2 compatibility: Do not reset key-off status on portamento without instrument number + // Test case: Off-Porta.xm + if(GetType() != MOD_TYPE_XM || !m_playBehaviour[kITFT2DontResetNoteOffOnPorta] || chn.rowCommand.instr != 0) + chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE); + chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG)); + } else //if(!instrumentChanged || chn.rowCommand.instr != 0 || !IsCompatibleMode(TRK_FASTTRACKER2)) // SampleChange.xm? + { + chn.dwFlags.reset(CHN_KEYOFF | CHN_NOTEFADE); + + // IT compatibility: Don't change bidi loop direction when no sample nor instrument is changed. + if((m_playBehaviour[kITPingPongNoReset] || !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) && pSmp == chn.pModSample && !instrumentChanged) + chn.dwFlags = (chn.dwFlags & (CHN_CHANNELFLAGS | CHN_PINGPONGFLAG)); + else + chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS); + + if(pIns) + { + // Copy envelope flags (we actually only need the "enabled" and "pitch" flag) + chn.VolEnv.flags = pIns->VolEnv.dwFlags; + chn.PanEnv.flags = pIns->PanEnv.dwFlags; + chn.PitchEnv.flags = pIns->PitchEnv.dwFlags; + + // A cutoff frequency of 0 should not be reset just because the filter envelope is enabled. + // Test case: FilterEnvReset.it + if((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == (ENV_ENABLED | ENV_FILTER) && !m_playBehaviour[kITFilterBehaviour]) + { + if(!chn.nCutOff) chn.nCutOff = 0x7F; + } + + if(pIns->IsCutoffEnabled()) chn.nCutOff = pIns->GetCutoff(); + if(pIns->IsResonanceEnabled()) chn.nResonance = pIns->GetResonance(); + } + } + + if(pSmp == nullptr) + { + chn.pModSample = nullptr; + chn.nLength = 0; + return; + } + + if(bPorta && chn.nLength == 0 && (m_playBehaviour[kFT2PortaNoNote] || m_playBehaviour[kITPortaNoNote])) + { + // IT/FT2 compatibility: If the note just stopped on the previous tick, prevent it from restarting. + // Test cases: PortaJustStoppedNote.xm, PortaJustStoppedNote.it + chn.increment.Set(0); + } + + // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes. + // If the instrument changes, keep playing the previous sample, but load the new instrument's envelopes. + // Test case: ResetEnvNoteOffOldFx.it + if(chn.rowCommand.note == NOTE_KEYOFF && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && sampleChanged) + { + if(chn.pModSample) + { + chn.dwFlags |= (chn.pModSample->uFlags & CHN_SAMPLEFLAGS); + } + chn.nInsVol = oldInsVol; + chn.nVolume = pSmp->nVolume; + if(pSmp->uFlags[CHN_PANNING]) chn.SetInstrumentPan(pSmp->nPan, *this); + return; + } + + chn.pModSample = pSmp; + chn.nLength = pSmp->nLength; + chn.nLoopStart = pSmp->nLoopStart; + chn.nLoopEnd = pSmp->nLoopEnd; + // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end) + if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = pSmp->nLength; + chn.dwFlags |= (pSmp->uFlags & CHN_SAMPLEFLAGS); + + // IT Compatibility: Autovibrato reset + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + { + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + } + + if(newTuning) + { + chn.nC5Speed = pSmp->nC5Speed; + chn.m_CalculateFreq = true; + chn.nFineTune = 0; + } else if(!bPorta || sampleChanged || !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) + { + // Don't reset finetune changed by "set finetune" command. + // Test case: finetune.xm, finetune.mod + // But *do* change the finetune if we switch to a different sample, to fix + // Miranda`s axe by Jamson (jam007.xm). + chn.nC5Speed = pSmp->nC5Speed; + chn.nFineTune = pSmp->nFineTune; + } + + chn.nTranspose = UseFinetuneAndTranspose() ? pSmp->RelativeTone : 0; + + // FT2 compatibility: Don't reset portamento target with new instrument numbers. + // Test case: Porta-Pickup.xm + // ProTracker does the same. + // Test case: PortaTarget.mod + if(!m_playBehaviour[kFT2PortaTargetNoReset] && GetType() != MOD_TYPE_MOD) + { + chn.nPortamentoDest = 0; + } + chn.m_PortamentoFineSteps = 0; + + if(chn.dwFlags[CHN_SUSTAINLOOP]) + { + chn.nLoopStart = pSmp->nSustainStart; + chn.nLoopEnd = pSmp->nSustainEnd; + if(chn.dwFlags[CHN_PINGPONGSUSTAIN]) chn.dwFlags.set(CHN_PINGPONGLOOP); + chn.dwFlags.set(CHN_LOOP); + } + if(chn.dwFlags[CHN_LOOP] && chn.nLoopEnd < chn.nLength) chn.nLength = chn.nLoopEnd; + + // Fix sample position on instrument change. This is needed for IT "on the fly" sample change. + // XXX is this actually called? In ProcessEffects(), a note-on effect is emulated if there's an on the fly sample change! + if(chn.position.GetUInt() >= chn.nLength) + { + if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) + { + chn.position.Set(0); + } + } +} + + +void CSoundFile::NoteChange(ModChannel &chn, int note, bool bPorta, bool bResetEnv, bool bManual, CHANNELINDEX channelHint) const +{ + if(note < NOTE_MIN) + return; + const int origNote = note; + const ModSample *pSmp = chn.pModSample; + const ModInstrument *pIns = chn.pModInstrument; + + const bool newTuning = (GetType() == MOD_TYPE_MPT && pIns != nullptr && pIns->pTuning); + // save the note that's actually used, as it's necessary to properly calculate PPS and stuff + const int realnote = note; + + if((pIns) && (note - NOTE_MIN < (int)std::size(pIns->Keyboard))) + { + uint32 n = pIns->Keyboard[note - NOTE_MIN]; + if((n) && (n < MAX_SAMPLES)) + { + pSmp = &Samples[n]; + } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !chn.HasMIDIOutput()) + { + // Impulse Tracker ignores empty slots. + // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. + // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it + return; + } + note = pIns->NoteMap[note - NOTE_MIN]; + } + // Key Off + if(note > NOTE_MAX) + { + // Key Off (+ Invalid Note for XM - TODO is this correct?) + if(note == NOTE_KEYOFF || !(GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) + { + KeyOff(chn); + // IT compatibility: Note-off + instrument releases sample sustain but does not release envelopes or fade the instrument + // Test case: noteoff3.it, ResetEnvNoteOffOldFx2.it + if(!bPorta && m_playBehaviour[kITInstrWithNoteOffOldEffects] && m_SongFlags[SONG_ITOLDEFFECTS] && chn.rowCommand.instr) + chn.dwFlags.reset(CHN_NOTEFADE | CHN_KEYOFF); + } else // Invalid Note -> Note Fade + { + if(/*note == NOTE_FADE && */ GetNumInstruments()) + chn.dwFlags.set(CHN_NOTEFADE); + } + + // Note Cut + if (note == NOTE_NOTECUT) + { + if(chn.dwFlags[CHN_ADLIB] && GetType() == MOD_TYPE_S3M) + { + // OPL voices are not cut but enter the release portion of their envelope + // In S3M we can still modify the volume after note-off, in legacy MPTM mode we can't + chn.dwFlags.set(CHN_KEYOFF); + } else + { + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + // IT compatibility: Stopping sample playback by setting sample increment to 0 rather than volume + // Test case: NoteOffInstr.it + if ((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) || (m_nInstruments != 0 && !m_playBehaviour[kITInstrWithNoteOff])) chn.nVolume = 0; + if (m_playBehaviour[kITInstrWithNoteOff]) chn.increment.Set(0); + chn.nFadeOutVol = 0; + } + } + + // IT compatibility tentative fix: Clear channel note memory (TRANCE_N.IT by A3F). + if(m_playBehaviour[kITClearOldNoteAfterCut]) + { + chn.nNote = chn.nNewNote = NOTE_NONE; + } + return; + } + + if(newTuning) + { + if(!bPorta || chn.nNote == NOTE_NONE) + chn.nPortamentoDest = 0; + else + { + chn.nPortamentoDest = pIns->pTuning->GetStepDistance(chn.nNote, chn.m_PortamentoFineSteps, static_cast<Tuning::NOTEINDEXTYPE>(note), 0); + //Here chn.nPortamentoDest means 'steps to slide'. + chn.m_PortamentoFineSteps = -chn.nPortamentoDest; + } + } + + if(!bPorta && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MED | MOD_TYPE_MT2))) + { + if(pSmp) + { + chn.nTranspose = pSmp->RelativeTone; + chn.nFineTune = pSmp->nFineTune; + } + } + // IT Compatibility: Update multisample instruments frequency even if instrument is not specified (fixes the guitars in spx-shuttledeparture.it) + // Test case: freqreset-noins.it + if(!bPorta && pSmp && m_playBehaviour[kITMultiSampleBehaviour]) + chn.nC5Speed = pSmp->nC5Speed; + + if(bPorta && !chn.IsSamplePlaying()) + { + if(m_playBehaviour[kFT2PortaNoNote]) + { + // FT2 Compatibility: Ignore notes with portamento if there was no note playing. + // Test case: 3xx-no-old-samp.xm + chn.nPeriod = 0; + return; + } else if(m_playBehaviour[kITPortaNoNote]) + { + // IT Compatibility: Ignore portamento command if no note was playing (e.g. if a previous note has faded out). + // Test case: Fade-Porta.it + bPorta = false; + } + } + + if(UseFinetuneAndTranspose()) + { + note += chn.nTranspose; + // RealNote = PatternNote + RelativeTone; (0..118, 0 = C-0, 118 = A#9) + Limit(note, NOTE_MIN + 11, NOTE_MIN + 130); // 119 possible notes + } else + { + Limit(note, NOTE_MIN, NOTE_MAX); + } + if(m_playBehaviour[kITRealNoteMapping]) + { + // need to memorize the original note for various effects (e.g. PPS) + chn.nNote = static_cast<ModCommand::NOTE>(Clamp(realnote, NOTE_MIN, NOTE_MAX)); + } else + { + chn.nNote = static_cast<ModCommand::NOTE>(note); + } + chn.m_CalculateFreq = true; + chn.isPaused = false; + + if ((!bPorta) || (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))) + chn.nNewIns = 0; + + uint32 period = GetPeriodFromNote(note, chn.nFineTune, chn.nC5Speed); + chn.nPanbrelloOffset = 0; + + // IT compatibility: Sample and instrument panning is only applied on note change, not instrument change + // Test case: PanReset.it + if(m_playBehaviour[kITPanningReset]) + ApplyInstrumentPanning(chn, pIns, pSmp); + + // IT compatibility: Pitch/Pan Separation can be overriden by panning commands, and shouldn't be affected by note-off commands + // Test case: PitchPanReset.it + if(m_playBehaviour[kITPitchPanSeparation] && pIns && pIns->nPPS) + { + if(!chn.nRestorePanOnNewNote) + chn.nRestorePanOnNewNote = static_cast<uint16>(chn.nPan + 1); + ProcessPitchPanSeparation(chn.nPan, origNote, *pIns); + } + + if(bResetEnv && !bPorta) + { + chn.nVolSwing = chn.nPanSwing = 0; + chn.nResSwing = chn.nCutSwing = 0; + if(pIns) + { + // IT Compatiblity: NNA is reset on every note change, not every instrument change (fixes spx-farspacedance.it). + if(m_playBehaviour[kITNNAReset]) chn.nNNA = pIns->nNNA; + + if(!pIns->VolEnv.dwFlags[ENV_CARRY]) chn.VolEnv.Reset(); + if(!pIns->PanEnv.dwFlags[ENV_CARRY]) chn.PanEnv.Reset(); + if(!pIns->PitchEnv.dwFlags[ENV_CARRY]) chn.PitchEnv.Reset(); + + // Volume Swing + if(pIns->nVolSwing) + { + chn.nVolSwing = static_cast<int16>(((mpt::random<int8>(AccessPRNG()) * pIns->nVolSwing) / 64 + 1) * (m_playBehaviour[kITSwingBehaviour] ? chn.nInsVol : ((chn.nVolume + 1) / 2)) / 199); + } + // Pan Swing + if(pIns->nPanSwing) + { + chn.nPanSwing = static_cast<int16>(((mpt::random<int8>(AccessPRNG()) * pIns->nPanSwing * 4) / 128)); + if(!m_playBehaviour[kITSwingBehaviour] && chn.nRestorePanOnNewNote == 0) + { + chn.nRestorePanOnNewNote = static_cast<uint16>(chn.nPan + 1); + } + } + // Cutoff Swing + if(pIns->nCutSwing) + { + int32 d = ((int32)pIns->nCutSwing * (int32)(static_cast<int32>(mpt::random<int8>(AccessPRNG())) + 1)) / 128; + chn.nCutSwing = static_cast<int16>((d * chn.nCutOff + 1) / 128); + chn.nRestoreCutoffOnNewNote = chn.nCutOff + 1; + } + // Resonance Swing + if(pIns->nResSwing) + { + int32 d = ((int32)pIns->nResSwing * (int32)(static_cast<int32>(mpt::random<int8>(AccessPRNG())) + 1)) / 128; + chn.nResSwing = static_cast<int16>((d * chn.nResonance + 1) / 128); + chn.nRestoreResonanceOnNewNote = chn.nResonance + 1; + } + } + } + + if(!pSmp) return; + if(period) + { + if((!bPorta) || (!chn.nPeriod)) chn.nPeriod = period; + if(!newTuning) + { + // FT2 compatibility: Don't reset portamento target with new notes. + // Test case: Porta-Pickup.xm + // ProTracker does the same. + // Test case: PortaTarget.mod + // IT compatibility: Portamento target is completely cleared with new notes. + // Test case: PortaReset.it + if(bPorta || !(m_playBehaviour[kFT2PortaTargetNoReset] || m_playBehaviour[kITClearPortaTarget] || GetType() == MOD_TYPE_MOD)) + { + chn.nPortamentoDest = period; + chn.portaTargetReached = false; + } + } + + if(!bPorta || (!chn.nLength && !(GetType() & MOD_TYPE_S3M))) + { + chn.pModSample = pSmp; + chn.nLength = pSmp->nLength; + chn.nLoopEnd = pSmp->nLength; + chn.nLoopStart = 0; + chn.position.Set(0); + if((m_SongFlags[SONG_PT_MODE] || m_playBehaviour[kST3OffsetWithoutInstrument]) && !chn.rowCommand.instr) + { + chn.position.SetInt(std::min(chn.prevNoteOffset, chn.nLength - SmpLength(1))); + } else + { + chn.prevNoteOffset = 0; + } + chn.dwFlags = (chn.dwFlags & CHN_CHANNELFLAGS) | (pSmp->uFlags & CHN_SAMPLEFLAGS); + chn.dwFlags.reset(CHN_PORTAMENTO); + if(chn.dwFlags[CHN_SUSTAINLOOP]) + { + chn.nLoopStart = pSmp->nSustainStart; + chn.nLoopEnd = pSmp->nSustainEnd; + chn.dwFlags.set(CHN_PINGPONGLOOP, chn.dwFlags[CHN_PINGPONGSUSTAIN]); + chn.dwFlags.set(CHN_LOOP); + if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; + } else if(chn.dwFlags[CHN_LOOP]) + { + chn.nLoopStart = pSmp->nLoopStart; + chn.nLoopEnd = pSmp->nLoopEnd; + if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; + } + // ProTracker "oneshot" loops (if loop start is 0, play the whole sample once and then repeat until loop end) + if(m_playBehaviour[kMODOneShotLoops] && chn.nLoopStart == 0) chn.nLoopEnd = chn.nLength = pSmp->nLength; + + if(chn.dwFlags[CHN_REVERSE] && chn.nLength > 0) + { + chn.dwFlags.set(CHN_PINGPONGFLAG); + chn.position.SetInt(chn.nLength - 1); + } + + // Handle "retrigger" waveform type + if(chn.nVibratoType < 4) + { + // IT Compatibilty: Slightly different waveform offsets (why does MPT have two different offsets here with IT old effects enabled and disabled?) + if(!m_playBehaviour[kITVibratoTremoloPanbrello] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) + chn.nVibratoPos = 0x10; + else if(GetType() == MOD_TYPE_MTM) + chn.nVibratoPos = 0x20; + else if(!(GetType() & (MOD_TYPE_DIGI | MOD_TYPE_DBM))) + chn.nVibratoPos = 0; + } + // IT Compatibility: No "retrigger" waveform here + if(!m_playBehaviour[kITVibratoTremoloPanbrello] && chn.nTremoloType < 4) + { + chn.nTremoloPos = 0; + } + } + if(chn.position.GetUInt() >= chn.nLength) chn.position.SetInt(chn.nLoopStart); + } else + { + bPorta = false; + } + + if (!bPorta + || (!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM))) + || (chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol) + || (m_SongFlags[SONG_ITCOMPATGXX] && chn.rowCommand.instr != 0)) + { + if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_DBM)) && chn.dwFlags[CHN_NOTEFADE] && !chn.nFadeOutVol) + { + chn.ResetEnvelopes(); + // IT Compatibility: Autovibrato reset + if(!m_playBehaviour[kITVibratoTremoloPanbrello]) + { + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + } + chn.dwFlags.reset(CHN_NOTEFADE); + chn.nFadeOutVol = 65536; + } + if ((!bPorta) || (!m_SongFlags[SONG_ITCOMPATGXX]) || (chn.rowCommand.instr)) + { + if ((!(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) || (chn.rowCommand.instr)) + { + chn.dwFlags.reset(CHN_NOTEFADE); + chn.nFadeOutVol = 65536; + } + } + } + + // IT compatibility: Don't reset key-off flag on porta notes unless Compat Gxx is enabled. + // Test case: Off-Porta.it, Off-Porta-CompatGxx.it, Off-Porta.xm + if(m_playBehaviour[kITFT2DontResetNoteOffOnPorta] && bPorta && (!m_SongFlags[SONG_ITCOMPATGXX] || chn.rowCommand.instr == 0)) + chn.dwFlags.reset(CHN_EXTRALOUD); + else + chn.dwFlags.reset(CHN_EXTRALOUD | CHN_KEYOFF); + + // Enable Ramping + if(!bPorta) + { + chn.nLeftVU = chn.nRightVU = 0xFF; + chn.dwFlags.reset(CHN_FILTER); + chn.dwFlags.set(CHN_FASTVOLRAMP); + + // IT compatibility 15. Retrigger is reset in RetrigNote (Tremor doesn't store anything here, so we just don't reset this as well) + if(!m_playBehaviour[kITRetrigger] && !m_playBehaviour[kITTremor]) + { + // FT2 compatibility: Retrigger is reset in RetrigNote, tremor in ProcessEffects + if(!m_playBehaviour[kFT2Retrigger] && !m_playBehaviour[kFT2Tremor]) + { + chn.nRetrigCount = 0; + chn.nTremorCount = 0; + } + } + + if(bResetEnv) + { + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + } + chn.rightVol = chn.leftVol = 0; + bool useFilter = !m_SongFlags[SONG_MPTFILTERMODE]; + // Setup Initial Filter for this note + if(pIns) + { + if(pIns->IsResonanceEnabled()) + { + chn.nResonance = pIns->GetResonance(); + useFilter = true; + } + if(pIns->IsCutoffEnabled()) + { + chn.nCutOff = pIns->GetCutoff(); + useFilter = true; + } + if(useFilter && (pIns->filterMode != FilterMode::Unchanged)) + { + chn.nFilterMode = pIns->filterMode; + } + } else + { + chn.nVolSwing = chn.nPanSwing = 0; + chn.nCutSwing = chn.nResSwing = 0; + } + if((chn.nCutOff < 0x7F || m_playBehaviour[kITFilterBehaviour]) && useFilter) + { + int cutoff = SetupChannelFilter(chn, true); + if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID) + m_opl->Volume(channelHint, chn.nCutOff / 2u, true); + } + + if(chn.dwFlags[CHN_ADLIB] && m_opl && channelHint != CHANNELINDEX_INVALID) + { + // Test case: AdlibZeroVolumeNote.s3m + if(m_playBehaviour[kOPLNoteOffOnNoteChange]) + m_opl->NoteOff(channelHint); + else if(m_playBehaviour[kOPLNoteStopWith0Hz]) + m_opl->Frequency(channelHint, 0, true, false); + } + } + + // Special case for MPT + if (bManual) chn.dwFlags.reset(CHN_MUTE); + if((chn.dwFlags[CHN_MUTE] && (m_MixerSettings.MixerFlags & SNDMIX_MUTECHNMODE)) + || (chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_MUTE] && !bManual) + || (chn.pModInstrument != nullptr && chn.pModInstrument->dwFlags[INS_MUTE] && !bManual)) + { + if (!bManual) chn.nPeriod = 0; + } + + // Reset the Amiga resampler for this channel + if(!bPorta) + { + chn.paulaState.Reset(); + } +} + + +// Apply sample or instrument panning +void CSoundFile::ApplyInstrumentPanning(ModChannel &chn, const ModInstrument *instr, const ModSample *smp) const +{ + int32 newPan = int32_min; + // Default instrument panning + if(instr != nullptr && instr->dwFlags[INS_SETPANNING]) + newPan = instr->nPan; + // Default sample panning + if(smp != nullptr && smp->uFlags[CHN_PANNING]) + newPan = smp->nPan; + + if(newPan != int32_min) + { + chn.SetInstrumentPan(newPan, *this); + // IT compatibility: Sample and instrument panning overrides channel surround status. + // Test case: SmpInsPanSurround.it + if(m_playBehaviour[kPanOverride] && !m_SongFlags[SONG_SURROUNDPAN]) + { + chn.dwFlags.reset(CHN_SURROUND); + } + } +} + + +CHANNELINDEX CSoundFile::GetNNAChannel(CHANNELINDEX nChn) const +{ + // Check for empty channel + for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) + { + const ModChannel &c = m_PlayState.Chn[i]; + // No sample and no plugin playing + if(!c.nLength && !c.HasMIDIOutput()) + return i; + // Plugin channel with already released note + if(!c.nLength && c.dwFlags[CHN_KEYOFF | CHN_NOTEFADE]) + return i; + // Stopped OPL channel + if(c.dwFlags[CHN_ADLIB] && (!m_opl || !m_opl->IsActive(i))) + return i; + } + + uint32 vol = 0x800000; + if(nChn < MAX_CHANNELS) + { + const ModChannel &srcChn = m_PlayState.Chn[nChn]; + if(!srcChn.nFadeOutVol && srcChn.nLength) + return CHANNELINDEX_INVALID; + vol = (srcChn.nRealVolume << 9) | srcChn.nVolume; + } + + // All channels are used: check for lowest volume + CHANNELINDEX result = CHANNELINDEX_INVALID; + uint32 envpos = 0; + for(CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) + { + const ModChannel &c = m_PlayState.Chn[i]; + if(c.nLength && !c.nFadeOutVol) + return i; + // Use a combination of real volume [14 bit] (which includes volume envelopes, but also potentially global volume) and note volume [9 bit]. + // Rationale: We need volume envelopes in case e.g. all NNA channels are playing at full volume but are looping on a 0-volume envelope node. + // But if global volume is not applied to master and the global volume temporarily drops to 0, we would kill arbitrary channels. Hence, add the note volume as well. + uint32 v = (c.nRealVolume << 9) | c.nVolume; + if(c.dwFlags[CHN_LOOP]) + v /= 2; + if((v < vol) || ((v == vol) && (c.VolEnv.nEnvPosition > envpos))) + { + envpos = c.VolEnv.nEnvPosition; + vol = v; + result = i; + } + } + return result; +} + + +CHANNELINDEX CSoundFile::CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, bool forceCut) +{ + ModChannel &srcChn = m_PlayState.Chn[nChn]; + const ModInstrument *pIns = nullptr; + if(!ModCommand::IsNote(static_cast<ModCommand::NOTE>(note))) + return CHANNELINDEX_INVALID; + + // Always NNA cut - using + if((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_MT2)) || !m_nInstruments || forceCut) && !srcChn.HasMIDIOutput()) + { + if(!srcChn.nLength || srcChn.dwFlags[CHN_MUTE] || !(srcChn.rightVol | srcChn.leftVol)) + return CHANNELINDEX_INVALID; + + if(srcChn.dwFlags[CHN_ADLIB] && m_opl) + { + m_opl->NoteCut(nChn, false); + return CHANNELINDEX_INVALID; + } + + const CHANNELINDEX nnaChn = GetNNAChannel(nChn); + if(nnaChn == CHANNELINDEX_INVALID) + return CHANNELINDEX_INVALID; + ModChannel &chn = m_PlayState.Chn[nnaChn]; + // Copy Channel + chn = srcChn; + chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_MUTE | CHN_PORTAMENTO); + chn.nPanbrelloOffset = 0; + chn.nMasterChn = nChn + 1; + chn.nCommand = CMD_NONE; + chn.rowCommand.Clear(); + // Cut the note + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + // Stop this channel + srcChn.nLength = 0; + srcChn.position.Set(0); + srcChn.nROfs = srcChn.nLOfs = 0; + srcChn.rightVol = srcChn.leftVol = 0; + return nnaChn; + } + if(instr > GetNumInstruments()) + instr = 0; + const ModSample *pSample = srcChn.pModSample; + // If no instrument is given, assume previous instrument to still be valid. + // Test case: DNA-NoInstr.it + pIns = instr > 0 ? Instruments[instr] : srcChn.pModInstrument; + auto dnaNote = note; + if(pIns != nullptr) + { + auto smp = pIns->Keyboard[note - NOTE_MIN]; + // IT compatibility: DCT = note uses pattern notes for comparison + // Note: This is not applied in case kITRealNoteMapping is not set to keep playback of legacy modules simple (chn.nNote is translated note in that case) + // 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) + { + pSample = &Samples[smp]; + } else if(m_playBehaviour[kITEmptyNoteMapSlot] && !pIns->HasValidMIDIChannel()) + { + // Impulse Tracker ignores empty slots. + // We won't ignore them if a plugin is assigned to this slot, so that VSTis still work as intended. + // Test case: emptyslot.it, PortaInsNum.it, gxsmp.it, gxsmp2.it + return CHANNELINDEX_INVALID; + } + } + if(srcChn.dwFlags[CHN_MUTE]) + return CHANNELINDEX_INVALID; + + for(CHANNELINDEX i = nChn; i < MAX_CHANNELS; i++) + { + // Only apply to background channels, or the same pattern channel + if(i < m_nChannels && i != nChn) + continue; + + ModChannel &chn = m_PlayState.Chn[i]; + bool applyDNAtoPlug = false; + if((chn.nMasterChn == nChn + 1 || i == nChn) && chn.pModInstrument != nullptr) + { + bool applyDNA = false; + // Duplicate Check Type + switch(chn.pModInstrument->nDCT) + { + case DuplicateCheckType::None: + break; + // Note + case DuplicateCheckType::Note: + if(dnaNote != NOTE_NONE && chn.nNote == dnaNote && pIns == chn.pModInstrument) + applyDNA = true; + if(pIns && pIns->nMixPlug) + applyDNAtoPlug = true; + break; + // Sample + case DuplicateCheckType::Sample: + // IT compatibility: DCT = sample only applies to same instrument + // Test case: dct_smp_note_test.it + if(pSample != nullptr && pSample == chn.pModSample && (pIns == chn.pModInstrument || !m_playBehaviour[kITDCTBehaviour])) + applyDNA = true; + break; + // Instrument + case DuplicateCheckType::Instrument: + if(pIns == chn.pModInstrument) + applyDNA = true; + if(pIns && pIns->nMixPlug) + applyDNAtoPlug = true; + break; + // Plugin + case DuplicateCheckType::Plugin: + if(pIns && (pIns->nMixPlug) && (pIns->nMixPlug == chn.pModInstrument->nMixPlug)) + { + applyDNAtoPlug = true; + applyDNA = true; + } + break; + } + + // Duplicate Note Action + if(applyDNA) + { +#ifndef NO_PLUGINS + if(applyDNAtoPlug && chn.nNote != NOTE_NONE) + { + switch(chn.pModInstrument->nDNA) + { + case DuplicateNoteAction::NoteCut: + case DuplicateNoteAction::NoteOff: + case DuplicateNoteAction::NoteFade: + // Switch off duplicated note played on this plugin + if(const auto oldNote = chn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]); oldNote != NOTE_NONE) + { + SendMIDINote(i, oldNote + NOTE_MAX_SPECIAL, 0); + chn.nArpeggioLastNote = NOTE_NONE; + chn.nNote = NOTE_NONE; + } + break; + } + } +#endif // NO_PLUGINS + + switch(chn.pModInstrument->nDNA) + { + // Cut + case DuplicateNoteAction::NoteCut: + KeyOff(chn); + chn.nVolume = 0; + if(chn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteCut(i); + break; + // Note Off + case DuplicateNoteAction::NoteOff: + KeyOff(chn); + if(chn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteOff(i); + break; + // Note Fade + case DuplicateNoteAction::NoteFade: + chn.dwFlags.set(CHN_NOTEFADE); + if(chn.dwFlags[CHN_ADLIB] && m_opl && !m_playBehaviour[kOPLwithNNA]) + m_opl->NoteOff(i); + break; + } + if(!chn.nVolume) + { + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + } + } + } + } + + // Do we need to apply New/Duplicate Note Action to a VSTi? + bool applyNNAtoPlug = false; +#ifndef NO_PLUGINS + IMixPlugin *pPlugin = nullptr; + if(srcChn.HasMIDIOutput() && ModCommand::IsNote(srcChn.nNote)) // instro sends to a midi chan + { + PLUGINDEX plugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes); + + if(plugin > 0 && plugin <= MAX_MIXPLUGINS) + { + pPlugin = m_MixPlugins[plugin - 1].pMixPlugin; + if(pPlugin) + { + // apply NNA to this plugin iff it is currently playing a note on this tracker channel + // (and if it is playing a note, we know that would be the last note played on this chan). + const auto oldNote = srcChn.GetPluginNote(m_playBehaviour[kITRealNoteMapping]); + applyNNAtoPlug = (oldNote != NOTE_NONE) && pPlugin->IsNotePlaying(oldNote, nChn); + } + } + } +#endif // NO_PLUGINS + + // New Note Action + if(!srcChn.IsSamplePlaying() && !applyNNAtoPlug) + return CHANNELINDEX_INVALID; + +#ifndef NO_PLUGINS + if(applyNNAtoPlug && pPlugin) + { + switch(srcChn.nNNA) + { + case NewNoteAction::NoteOff: + case NewNoteAction::NoteCut: + case NewNoteAction::NoteFade: + // Switch off note played on this plugin, on this tracker channel and midi channel + SendMIDINote(nChn, NOTE_KEYOFF, 0); + srcChn.nArpeggioLastNote = NOTE_NONE; + break; + case NewNoteAction::Continue: + break; + } + } +#endif // NO_PLUGINS + + CHANNELINDEX nnaChn = GetNNAChannel(nChn); + if(nnaChn == CHANNELINDEX_INVALID) + return CHANNELINDEX_INVALID; + + ModChannel &chn = m_PlayState.Chn[nnaChn]; + if(chn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteCut(nnaChn); + // Copy Channel + chn = srcChn; + chn.dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO | CHN_PORTAMENTO); + chn.nPanbrelloOffset = 0; + + chn.nMasterChn = nChn < GetNumChannels() ? nChn + 1 : 0; + chn.nCommand = CMD_NONE; + + // Key Off the note + switch(srcChn.nNNA) + { + case NewNoteAction::NoteOff: + KeyOff(chn); + if(chn.dwFlags[CHN_ADLIB] && m_opl) + { + m_opl->NoteOff(nChn); + if(m_playBehaviour[kOPLwithNNA]) + m_opl->MoveChannel(nChn, nnaChn); + } + break; + case NewNoteAction::NoteCut: + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE); + if(chn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteCut(nChn); + break; + case NewNoteAction::NoteFade: + chn.dwFlags.set(CHN_NOTEFADE); + if(chn.dwFlags[CHN_ADLIB] && m_opl) + { + if(m_playBehaviour[kOPLwithNNA]) + m_opl->MoveChannel(nChn, nnaChn); + else + m_opl->NoteOff(nChn); + } + break; + case NewNoteAction::Continue: + if(chn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->MoveChannel(nChn, nnaChn); + break; + } + if(!chn.nVolume) + { + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + } + // Stop this channel + srcChn.nLength = 0; + srcChn.position.Set(0); + srcChn.nROfs = srcChn.nLOfs = 0; + + return nnaChn; +} + + +bool CSoundFile::ProcessEffects() +{ + m_PlayState.m_breakRow = ROWINDEX_INVALID; // Is changed if a break to row command is encountered + m_PlayState.m_patLoopRow = ROWINDEX_INVALID; // Is changed if a pattern loop jump-back is executed + m_PlayState.m_posJump = ORDERINDEX_INVALID; + + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) + { + ModChannel &chn = m_PlayState.Chn[nChn]; + const uint32 tickCount = m_PlayState.m_nTickCount % (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay); + uint32 instr = chn.rowCommand.instr; + ModCommand::VOLCMD volcmd = chn.rowCommand.volcmd; + uint32 vol = chn.rowCommand.vol; + ModCommand::COMMAND cmd = chn.rowCommand.command; + uint32 param = chn.rowCommand.param; + bool bPorta = chn.rowCommand.IsPortamento(); + + uint32 nStartTick = 0; + chn.isFirstTick = m_SongFlags[SONG_FIRSTTICK]; + + // Process parameter control note. + if(chn.rowCommand.note == NOTE_PC) + { +#ifndef NO_PLUGINS + const PLUGINDEX plug = chn.rowCommand.instr; + const PlugParamIndex plugparam = chn.rowCommand.GetValueVolCol(); + const PlugParamValue value = chn.rowCommand.GetValueEffectCol() / PlugParamValue(ModCommand::maxColumnValue); + + if(plug > 0 && plug <= MAX_MIXPLUGINS && m_MixPlugins[plug - 1].pMixPlugin) + m_MixPlugins[plug-1].pMixPlugin->SetParameter(plugparam, value); +#endif // NO_PLUGINS + } + + // Process continuous parameter control note. + // Row data is cleared after first tick so on following + // ticks using channels m_nPlugParamValueStep to identify + // the need for parameter control. The condition cmd == 0 + // is to make sure that m_nPlugParamValueStep != 0 because + // of NOTE_PCS, not because of macro. + if(chn.rowCommand.note == NOTE_PCS || (cmd == CMD_NONE && chn.m_plugParamValueStep != 0)) + { +#ifndef NO_PLUGINS + const bool isFirstTick = m_SongFlags[SONG_FIRSTTICK]; + if(isFirstTick) + chn.m_RowPlug = chn.rowCommand.instr; + const PLUGINDEX plugin = chn.m_RowPlug; + const bool hasValidPlug = (plugin > 0 && plugin <= MAX_MIXPLUGINS && m_MixPlugins[plugin - 1].pMixPlugin); + if(hasValidPlug) + { + if(isFirstTick) + chn.m_RowPlugParam = ModCommand::GetValueVolCol(chn.rowCommand.volcmd, chn.rowCommand.vol); + const PlugParamIndex plugparam = chn.m_RowPlugParam; + if(isFirstTick) + { + PlugParamValue targetvalue = ModCommand::GetValueEffectCol(chn.rowCommand.command, chn.rowCommand.param) / PlugParamValue(ModCommand::maxColumnValue); + chn.m_plugParamTargetValue = targetvalue; + chn.m_plugParamValueStep = (targetvalue - m_MixPlugins[plugin - 1].pMixPlugin->GetParameter(plugparam)) / PlugParamValue(m_PlayState.TicksOnRow()); + } + if(m_PlayState.m_nTickCount + 1 == m_PlayState.TicksOnRow()) + { // On last tick, set parameter exactly to target value. + m_MixPlugins[plugin - 1].pMixPlugin->SetParameter(plugparam, chn.m_plugParamTargetValue); + } + else + m_MixPlugins[plugin - 1].pMixPlugin->ModifyParameter(plugparam, chn.m_plugParamValueStep); + } +#endif // NO_PLUGINS + } + + // Apart from changing parameters, parameter control notes are intended to be 'invisible'. + // To achieve this, clearing the note data so that rest of the process sees the row as empty row. + if(ModCommand::IsPcNote(chn.rowCommand.note)) + { + chn.ClearRowCmd(); + instr = 0; + volcmd = VOLCMD_NONE; + vol = 0; + cmd = CMD_NONE; + param = 0; + bPorta = false; + } + + // Process Invert Loop (MOD Effect, called every row if it's active) + if(!m_SongFlags[SONG_FIRSTTICK]) + { + InvertLoop(m_PlayState.Chn[nChn]); + } else + { + if(instr) m_PlayState.Chn[nChn].nEFxOffset = 0; + } + + // Process special effects (note delay, pattern delay, pattern loop) + if (cmd == CMD_DELAYCUT) + { + //:xy --> note delay until tick x, note cut at tick x+y + nStartTick = (param & 0xF0) >> 4; + const uint32 cutAtTick = nStartTick + (param & 0x0F); + NoteCut(nChn, cutAtTick, m_playBehaviour[kITSCxStopsSample]); + } else if ((cmd == CMD_MODCMDEX) || (cmd == CMD_S3MCMDEX)) + { + if ((!param) && (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT))) + param = chn.nOldCmdEx; + else + chn.nOldCmdEx = static_cast<ModCommand::PARAM>(param); + + // Note Delay ? + if ((param & 0xF0) == 0xD0) + { + nStartTick = param & 0x0F; + if(nStartTick == 0) + { + //IT compatibility 22. SD0 == SD1 + if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + nStartTick = 1; + //ST3 ignores notes with SD0 completely + else if(GetType() == MOD_TYPE_S3M) + continue; + } else if(nStartTick >= (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay) && m_playBehaviour[kITOutOfRangeDelay]) + { + // IT compatibility 08. Handling of out-of-range delay command. + // Additional test case: tickdelay.it + if(instr) + { + chn.nNewIns = static_cast<ModCommand::INSTR>(instr); + } + continue; + } + } else if(m_SongFlags[SONG_FIRSTTICK]) + { + // Pattern Loop ? + if((param & 0xF0) == 0xE0) + { + // Pattern Delay + // In Scream Tracker 3 / Impulse Tracker, only the first delay command on this row is considered. + // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm + // XXX In Scream Tracker 3, the "left" channels are evaluated before the "right" channels, which is not emulated here! + if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_PlayState.m_nPatternDelay) + { + if(!(GetType() & (MOD_TYPE_S3M)) || (param & 0x0F) != 0) + { + // While Impulse Tracker *does* count S60 as a valid row delay (and thus ignores any other row delay commands on the right), + // Scream Tracker 3 simply ignores such commands. + m_PlayState.m_nPatternDelay = 1 + (param & 0x0F); + } + } + } + } + } + + if(GetType() == MOD_TYPE_MTM && cmd == CMD_MODCMDEX && (param & 0xF0) == 0xD0) + { + // Apparently, retrigger and note delay have the same behaviour in MultiTracker: + // They both restart the note at tick x, and if there is a note on the same row, + // this note is started on the first tick. + nStartTick = 0; + param = 0x90 | (param & 0x0F); + } + + if(nStartTick != 0 && chn.rowCommand.note == NOTE_KEYOFF && chn.rowCommand.volcmd == VOLCMD_PANNING && m_playBehaviour[kFT2PanWithDelayedNoteOff]) + { + // FT2 compatibility: If there's a delayed note off, panning commands are ignored. WTF! + // Test case: PanOff.xm + chn.rowCommand.volcmd = VOLCMD_NONE; + } + + bool triggerNote = (m_PlayState.m_nTickCount == nStartTick); // Can be delayed by a note delay effect + if(m_playBehaviour[kFT2OutOfRangeDelay] && nStartTick >= m_PlayState.m_nMusicSpeed) + { + // FT2 compatibility: Note delays greater than the song speed should be ignored. + // However, EEx pattern delay is *not* considered at all. + // Test case: DelayCombination.xm, PortaDelay.xm + triggerNote = false; + } else if(m_playBehaviour[kRowDelayWithNoteDelay] && nStartTick > 0 && tickCount == nStartTick) + { + // IT compatibility: Delayed notes (using SDx) that are on the same row as a Row Delay effect are retriggered. + // ProTracker / Scream Tracker 3 / FastTracker 2 do the same. + // Test case: PatternDelay-NoteDelay.it, PatternDelay-NoteDelay.xm, PatternDelaysRetrig.mod + triggerNote = true; + } + + // IT compatibility: Tick-0 vs non-tick-0 effect distinction is always based on tick delay. + // Test case: SlideDelay.it + if(m_playBehaviour[kITFirstTickHandling]) + { + chn.isFirstTick = tickCount == nStartTick; + } + chn.triggerNote = triggerNote; + + // FT2 compatibility: Note + portamento + note delay = no portamento + // Test case: PortaDelay.xm + if(m_playBehaviour[kFT2PortaDelay] && nStartTick != 0) + { + bPorta = false; + } + + if(m_SongFlags[SONG_PT_MODE] && instr && !m_PlayState.m_nTickCount) + { + // Instrument number resets the stacked ProTracker offset. + // Test case: ptoffset.mod + chn.prevNoteOffset = 0; + // ProTracker compatibility: Sample properties are always loaded on the first tick, even when there is a note delay. + // Test case: InstrDelay.mod + if(!triggerNote && chn.IsSamplePlaying()) + { + chn.nNewIns = static_cast<ModCommand::INSTR>(instr); + if(instr <= GetNumSamples()) + { + chn.nVolume = Samples[instr].nVolume; + chn.nFineTune = Samples[instr].nFineTune; + } + } + } + + // Handles note/instrument/volume changes + if(triggerNote) + { + ModCommand::NOTE note = chn.rowCommand.note; + if(instr) chn.nNewIns = static_cast<ModCommand::INSTR>(instr); + + if(ModCommand::IsNote(note) && m_playBehaviour[kFT2Transpose]) + { + // Notes that exceed FT2's limit are completely ignored. + // Test case: NoteLimit.xm + int transpose = chn.nTranspose; + if(instr && !bPorta) + { + // Refresh transpose + // Test case: NoteLimit2.xm + const SAMPLEINDEX sample = GetSampleIndex(note, instr); + if(sample > 0) + transpose = GetSample(sample).RelativeTone; + } + + const int computedNote = note + transpose; + if((computedNote < NOTE_MIN + 11 || computedNote > NOTE_MIN + 130)) + { + note = NOTE_NONE; + } + } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B)) && GetNumInstruments() != 0 && ModCommand::IsNoteOrEmpty(static_cast<ModCommand::NOTE>(note))) + { + // IT compatibility: Invalid instrument numbers do nothing, but they are remembered for upcoming notes and do not trigger a note in that case. + // Test case: InstrumentNumberChange.it + INSTRUMENTINDEX instrToCheck = static_cast<INSTRUMENTINDEX>((instr != 0) ? instr : chn.nOldIns); + if(instrToCheck != 0 && (instrToCheck > GetNumInstruments() || Instruments[instrToCheck] == nullptr)) + { + note = NOTE_NONE; + instr = 0; + } + } + + // XM: FT2 ignores a note next to a K00 effect, and a fade-out seems to be done when no volume envelope is present (not exactly the Kxx behaviour) + if(cmd == CMD_KEYOFF && param == 0 && m_playBehaviour[kFT2KeyOff]) + { + note = NOTE_NONE; + instr = 0; + } + + bool retrigEnv = note == NOTE_NONE && instr != 0; + + // Apparently, any note number in a pattern causes instruments to recall their original volume settings - no matter if there's a Note Off next to it or whatever. + // Test cases: keyoff+instr.xm, delay.xm + bool reloadSampleSettings = (m_playBehaviour[kFT2ReloadSampleSettings] && instr != 0); + // ProTracker Compatibility: If a sample was stopped before, lone instrument numbers can retrigger it + // Test case: PTSwapEmpty.mod, PTInstrVolume.mod, SampleSwap.s3m + bool keepInstr = (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + || m_playBehaviour[kST3SampleSwap] + || (m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying() && (chn.pModSample == nullptr || !chn.pModSample->HasSampleData())); + + // Now it's time for some FT2 crap... + if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) + { + // XM: Key-Off + Sample == Note Cut (BUT: Only if no instr number or volume effect is present!) + // Test case: NoteOffVolume.xm + if(note == NOTE_KEYOFF + && ((!instr && volcmd != VOLCMD_VOLUME && cmd != CMD_VOLUME) || !m_playBehaviour[kFT2KeyOff]) + && (chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED])) + { + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.nVolume = 0; + note = NOTE_NONE; + instr = 0; + retrigEnv = false; + // FT2 Compatbility: Start fading the note for notes with no delay. Only relevant when a volume command is encountered after the note-off. + // Test case: NoteOffFadeNoEnv.xm + if(m_SongFlags[SONG_FIRSTTICK] && m_playBehaviour[kFT2NoteOffFlags]) + chn.dwFlags.set(CHN_NOTEFADE); + } else if(m_playBehaviour[kFT2RetrigWithNoteDelay] && !m_SongFlags[SONG_FIRSTTICK]) + { + // FT2 Compatibility: Some special hacks for rogue note delays... (EDx with x > 0) + // Apparently anything that is next to a note delay behaves totally unpredictable in FT2. Swedish tracker logic. :) + + retrigEnv = true; + + // Portamento + Note Delay = No Portamento + // Test case: porta-delay.xm + bPorta = false; + + if(note == NOTE_NONE) + { + // If there's a note delay but no real note, retrig the last note. + // Test case: delay2.xm, delay3.xm + note = static_cast<ModCommand::NOTE>(chn.nNote - chn.nTranspose); + } else if(note >= NOTE_MIN_SPECIAL) + { + // Gah! Even Note Off + Note Delay will cause envelopes to *retrigger*! How stupid is that? + // ... Well, and that is actually all it does if there's an envelope. No fade out, no nothing. *sigh* + // Test case: OffDelay.xm + note = NOTE_NONE; + keepInstr = false; + reloadSampleSettings = true; + } else if(instr || !m_playBehaviour[kFT2NoteDelayWithoutInstr]) + { + // Normal note (only if there is an instrument, test case: DelayVolume.xm) + keepInstr = true; + reloadSampleSettings = true; + } + } + } + + if((retrigEnv && !m_playBehaviour[kFT2ReloadSampleSettings]) || reloadSampleSettings) + { + const ModSample *oldSample = nullptr; + // Reset default volume when retriggering envelopes + + if(GetNumInstruments()) + { + oldSample = chn.pModSample; + } else if (instr <= GetNumSamples()) + { + // Case: Only samples are used; no instruments. + oldSample = &Samples[instr]; + } + + if(oldSample != nullptr) + { + if(!oldSample->uFlags[SMP_NODEFAULTVOLUME] && (GetType() != MOD_TYPE_S3M || oldSample->HasSampleData())) + chn.nVolume = oldSample->nVolume; + if(reloadSampleSettings) + { + // Also reload panning + chn.SetInstrumentPan(oldSample->nPan, *this); + } + } + } + + // FT2 compatibility: Instrument number disables tremor effect + // Test case: TremorInstr.xm, TremoRecover.xm + if(m_playBehaviour[kFT2Tremor] && instr != 0) + { + chn.nTremorCount = 0x20; + } + + // IT compatibility: Envelope retriggering with instrument number based on Old Effects and Compatible Gxx flags: + // OldFX CompatGxx Env Behaviour + // ----- --------- ------------- + // off off never reset + // on off reset on instrument without portamento + // off on reset on instrument with portamento + // on on always reset + // Test case: ins-xx.it, ins-ox.it, ins-oc.it, ins-xc.it, ResetEnvNoteOffOldFx.it, ResetEnvNoteOffOldFx2.it, noteoff3.it + if(GetNumInstruments() && m_playBehaviour[kITInstrWithNoteOffOldEffects] + && instr && !ModCommand::IsNote(note)) + { + if((bPorta && m_SongFlags[SONG_ITCOMPATGXX]) + || (!bPorta && m_SongFlags[SONG_ITOLDEFFECTS])) + { + chn.ResetEnvelopes(); + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.nFadeOutVol = 65536; + } + } + + if(retrigEnv) //Case: instrument with no note data. + { + //IT compatibility: Instrument with no note. + if(m_playBehaviour[kITInstrWithoutNote] || GetType() == MOD_TYPE_PLM) + { + // IT compatibility: Completely retrigger note after sample end to also reset portamento. + // Test case: PortaResetAfterRetrigger.it + bool triggerAfterSmpEnd = m_playBehaviour[kITMultiSampleInstrumentNumber] && !chn.IsSamplePlaying(); + if(GetNumInstruments()) + { + // Instrument mode + if(instr <= GetNumInstruments() && (chn.pModInstrument != Instruments[instr] || triggerAfterSmpEnd)) + note = chn.nNote; + } else + { + // Sample mode + if(instr < MAX_SAMPLES && (chn.pModSample != &Samples[instr] || triggerAfterSmpEnd)) + note = chn.nNote; + } + } + + if(GetNumInstruments() && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED))) + { + chn.ResetEnvelopes(); + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.dwFlags.reset(CHN_NOTEFADE); + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + chn.nFadeOutVol = 65536; + // FT2 Compatbility: Reset key-off status with instrument number + // Test case: NoteOffInstrChange.xm + if(m_playBehaviour[kFT2NoteOffFlags]) + chn.dwFlags.reset(CHN_KEYOFF); + } + if (!keepInstr) instr = 0; + } + + // Note Cut/Off/Fade => ignore instrument + if (note >= NOTE_MIN_SPECIAL) + { + // IT compatibility: Default volume of sample is recalled if instrument number is next to a note-off. + // Test case: NoteOffInstr.it, noteoff2.it + if(m_playBehaviour[kITInstrWithNoteOff] && instr) + { + const SAMPLEINDEX smp = GetSampleIndex(chn.nLastNote, instr); + if(smp > 0 && !Samples[smp].uFlags[SMP_NODEFAULTVOLUME]) + chn.nVolume = Samples[smp].nVolume; + } + // IT compatibility: Note-off with instrument number + Old Effects retriggers envelopes. + // Test case: ResetEnvNoteOffOldFx.it + if(!m_playBehaviour[kITInstrWithNoteOffOldEffects] || !m_SongFlags[SONG_ITOLDEFFECTS]) + instr = 0; + } + + if(ModCommand::IsNote(note)) + { + chn.nNewNote = chn.nLastNote = note; + + // New Note Action ? + if(!bPorta) + { + CheckNNA(nChn, instr, note, false); + } + + chn.RestorePanAndFilter(); + } + + // Instrument Change ? + if(instr) + { + const ModSample *oldSample = chn.pModSample; + //const ModInstrument *oldInstrument = chn.pModInstrument; + + InstrumentChange(chn, instr, bPorta, true); + + if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl) + { + m_opl->Patch(nChn, chn.pModSample->adlib); + } + + // IT compatibility: Keep new instrument number for next instrument-less note even if sample playback is stopped + // Test case: StoppedInstrSwap.it + if(GetType() == MOD_TYPE_MOD) + { + // Test case: PortaSwapPT.mod + if(!bPorta || !m_playBehaviour[kMODSampleSwap]) chn.nNewIns = 0; + } else + { + if(!m_playBehaviour[kITInstrWithNoteOff] || ModCommand::IsNote(note)) chn.nNewIns = 0; + } + + if(m_playBehaviour[kITPortamentoSwapResetsPos]) + { + // Test cases: PortaInsNum.it, PortaSample.it + if(ModCommand::IsNote(note) && oldSample != chn.pModSample) + { + //const bool newInstrument = oldInstrument != chn.pModInstrument && chn.pModInstrument->Keyboard[chn.nNewNote - NOTE_MIN] != 0; + chn.position.Set(0); + } + } else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && oldSample != chn.pModSample && ModCommand::IsNote(note)) + { + // Special IT case: portamento+note causes sample change -> ignore portamento + bPorta = false; + } else if(m_playBehaviour[kST3SampleSwap] && oldSample != chn.pModSample && (bPorta || !ModCommand::IsNote(note)) && chn.position.GetUInt() > chn.nLength) + { + // ST3 with SoundBlaster does sample swapping and continues playing the new sample where the old sample was stopped. + // If the new sample is shorter than that, it is stopped, even if it could be looped. + // This also applies to portamento between different samples. + // Test case: SampleSwap.s3m + chn.nLength = 0; + } else if(m_playBehaviour[kMODSampleSwap] && !chn.IsSamplePlaying()) + { + // If channel was paused and is resurrected by a lone instrument number, reset the sample position. + // Test case: PTSwapEmpty.mod + chn.position.Set(0); + } + } + // New Note ? + if (note != NOTE_NONE) + { + const bool instrChange = (!instr) && (chn.nNewIns) && ModCommand::IsNote(note); + if(instrChange) + { + InstrumentChange(chn, chn.nNewIns, bPorta, chn.pModSample == nullptr && chn.pModInstrument == nullptr, !(GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))); + chn.nNewIns = 0; + } + if(chn.pModSample != nullptr && chn.pModSample->uFlags[CHN_ADLIB] && m_opl && (instrChange || !m_opl->IsActive(nChn))) + { + m_opl->Patch(nChn, chn.pModSample->adlib); + } + + NoteChange(chn, note, bPorta, !(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)), false, nChn); + HandleDigiSamplePlayDirection(m_PlayState, nChn); + if ((bPorta) && (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)) && (instr)) + { + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.ResetEnvelopes(); + chn.nAutoVibDepth = 0; + chn.nAutoVibPos = 0; + } + if(chn.dwFlags[CHN_ADLIB] && m_opl + && ((note == NOTE_NOTECUT || note == NOTE_KEYOFF) || (note == NOTE_FADE && !m_playBehaviour[kOPLFlexibleNoteOff]))) + { + if(m_playBehaviour[kOPLNoteStopWith0Hz]) + m_opl->Frequency(nChn, 0, true, false); + m_opl->NoteOff(nChn); + } + } + // Tick-0 only volume commands + if (volcmd == VOLCMD_VOLUME) + { + if (vol > 64) vol = 64; + chn.nVolume = vol << 2; + chn.dwFlags.set(CHN_FASTVOLRAMP); + } else + if (volcmd == VOLCMD_PANNING) + { + Panning(chn, vol, Pan6bit); + } + +#ifndef NO_PLUGINS + if (m_nInstruments) ProcessMidiOut(nChn); +#endif // NO_PLUGINS + } + + if(m_playBehaviour[kST3NoMutedChannels] && ChnSettings[nChn].dwFlags[CHN_MUTE]) // not even effects are processed on muted S3M channels + continue; + + // Volume Column Effect (except volume & panning) + /* A few notes, paraphrased from ITTECH.TXT by Storlek (creator of schismtracker): + Ex/Fx/Gx are shared with Exx/Fxx/Gxx; Ex/Fx are 4x the 'normal' slide value + Gx is linked with Ex/Fx if Compat Gxx is off, just like Gxx is with Exx/Fxx + Gx values: 1, 4, 8, 16, 32, 64, 96, 128, 255 + Ax/Bx/Cx/Dx values are used directly (i.e. D9 == D09), and are NOT shared with Dxx + (value is stored into nOldVolParam and used by A0/B0/C0/D0) + Hx uses the same value as Hxx and Uxx, and affects the *depth* + so... hxx = (hx | (oldhxx & 0xf0)) ??? + TODO is this done correctly? + */ + bool doVolumeColumn = m_PlayState.m_nTickCount >= nStartTick; + // FT2 compatibility: If there's a note delay, volume column effects are NOT executed + // on the first tick and, if there's an instrument number, on the delayed tick. + // Test case: VolColDelay.xm, PortaDelay.xm + if(m_playBehaviour[kFT2VolColDelay] && nStartTick != 0) + { + doVolumeColumn = m_PlayState.m_nTickCount != 0 && (m_PlayState.m_nTickCount != nStartTick || (chn.rowCommand.instr == 0 && volcmd != VOLCMD_TONEPORTAMENTO)); + } + if(volcmd > VOLCMD_PANNING && doVolumeColumn) + { + if(volcmd == VOLCMD_TONEPORTAMENTO) + { + const auto [porta, clearEffectCommand] = GetVolCmdTonePorta(chn.rowCommand, nStartTick); + if(clearEffectCommand) + cmd = CMD_NONE; + + TonePortamento(chn, porta); + } else + { + // FT2 Compatibility: FT2 ignores some volume commands with parameter = 0. + if(m_playBehaviour[kFT2VolColMemory] && vol == 0) + { + switch(volcmd) + { + case VOLCMD_VOLUME: + case VOLCMD_PANNING: + case VOLCMD_VIBRATODEPTH: + break; + case VOLCMD_PANSLIDELEFT: + // FT2 Compatibility: Pan slide left with zero parameter causes panning to be set to full left on every non-row tick. + // Test case: PanSlideZero.xm + if(!m_SongFlags[SONG_FIRSTTICK]) + { + chn.nPan = 0; + } + [[fallthrough]]; + default: + // no memory here. + volcmd = VOLCMD_NONE; + } + + } else if(!m_playBehaviour[kITVolColMemory]) + { + // IT Compatibility: Effects in the volume column don't have an unified memory. + // Test case: VolColMemory.it + if(vol) chn.nOldVolParam = static_cast<ModCommand::PARAM>(vol); else vol = chn.nOldVolParam; + } + + switch(volcmd) + { + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + // IT Compatibility: Volume column volume slides have their own memory + // Test case: VolColMemory.it + if(vol == 0 && m_playBehaviour[kITVolColMemory]) + { + vol = chn.nOldVolParam; + if(vol == 0) + break; + } else + { + chn.nOldVolParam = static_cast<ModCommand::PARAM>(vol); + } + VolumeSlide(chn, static_cast<ModCommand::PARAM>(volcmd == VOLCMD_VOLSLIDEUP ? (vol << 4) : vol)); + break; + + case VOLCMD_FINEVOLUP: + // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay + // Test case: FineVolColSlide.it + if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory]) + { + // IT Compatibility: Volume column volume slides have their own memory + // Test case: VolColMemory.it + FineVolumeUp(chn, static_cast<ModCommand::PARAM>(vol), m_playBehaviour[kITVolColMemory]); + } + break; + + case VOLCMD_FINEVOLDOWN: + // IT Compatibility: Fine volume slides in the volume column are only executed on the first tick, not on multiples of the first tick in case of pattern delay + // Test case: FineVolColSlide.it + if(m_PlayState.m_nTickCount == nStartTick || !m_playBehaviour[kITVolColMemory]) + { + // IT Compatibility: Volume column volume slides have their own memory + // Test case: VolColMemory.it + FineVolumeDown(chn, static_cast<ModCommand::PARAM>(vol), m_playBehaviour[kITVolColMemory]); + } + break; + + case VOLCMD_VIBRATOSPEED: + // FT2 does not automatically enable vibrato with the "set vibrato speed" command + if(m_playBehaviour[kFT2VolColVibrato]) + chn.nVibratoSpeed = vol & 0x0F; + else + Vibrato(chn, vol << 4); + break; + + case VOLCMD_VIBRATODEPTH: + Vibrato(chn, vol); + break; + + case VOLCMD_PANSLIDELEFT: + PanningSlide(chn, static_cast<ModCommand::PARAM>(vol), !m_playBehaviour[kFT2VolColMemory]); + break; + + case VOLCMD_PANSLIDERIGHT: + PanningSlide(chn, static_cast<ModCommand::PARAM>(vol << 4), !m_playBehaviour[kFT2VolColMemory]); + break; + + case VOLCMD_PORTAUP: + // IT compatibility (one of the first testcases - link effect memory) + PortamentoUp(nChn, static_cast<ModCommand::PARAM>(vol << 2), m_playBehaviour[kITVolColFinePortamento]); + break; + + case VOLCMD_PORTADOWN: + // IT compatibility (one of the first testcases - link effect memory) + PortamentoDown(nChn, static_cast<ModCommand::PARAM>(vol << 2), m_playBehaviour[kITVolColFinePortamento]); + break; + + case VOLCMD_OFFSET: + if(triggerNote && chn.pModSample && vol <= std::size(chn.pModSample->cues)) + { + SmpLength offset; + if(vol == 0) + offset = chn.oldOffset; + else + offset = chn.oldOffset = chn.pModSample->cues[vol - 1]; + SampleOffset(chn, offset); + } + break; + + case VOLCMD_PLAYCONTROL: + if(vol <= 1) + chn.isPaused = (vol == 0); + break; + } + } + } + + // Effects + if(cmd != CMD_NONE) switch (cmd) + { + // Set Volume + case CMD_VOLUME: + if(m_SongFlags[SONG_FIRSTTICK]) + { + chn.nVolume = (param < 64) ? param * 4 : 256; + chn.dwFlags.set(CHN_FASTVOLRAMP); + } + break; + + // Portamento Up + case CMD_PORTAMENTOUP: + if ((!param) && (GetType() & MOD_TYPE_MOD)) break; + PortamentoUp(nChn, static_cast<ModCommand::PARAM>(param)); + break; + + // Portamento Down + case CMD_PORTAMENTODOWN: + if ((!param) && (GetType() & MOD_TYPE_MOD)) break; + PortamentoDown(nChn, static_cast<ModCommand::PARAM>(param)); + break; + + // Volume Slide + case CMD_VOLUMESLIDE: + if (param || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param)); + break; + + // Tone-Portamento + case CMD_TONEPORTAMENTO: + TonePortamento(chn, static_cast<uint16>(param)); + break; + + // Tone-Portamento + Volume Slide + case CMD_TONEPORTAVOL: + if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param)); + TonePortamento(chn, 0); + break; + + // Vibrato + case CMD_VIBRATO: + Vibrato(chn, param); + break; + + // Vibrato + Volume Slide + case CMD_VIBRATOVOL: + if ((param) || (GetType() != MOD_TYPE_MOD)) VolumeSlide(chn, static_cast<ModCommand::PARAM>(param)); + Vibrato(chn, 0); + break; + + // Set Speed + case CMD_SPEED: + if(m_SongFlags[SONG_FIRSTTICK]) + SetSpeed(m_PlayState, param); + break; + + // Set Tempo + case CMD_TEMPO: + if(m_playBehaviour[kMODVBlankTiming]) + { + // ProTracker MODs with VBlank timing: All Fxx parameters set the tick count. + if(m_SongFlags[SONG_FIRSTTICK] && param != 0) SetSpeed(m_PlayState, param); + break; + } + { + param = CalculateXParam(m_PlayState.m_nPattern, m_PlayState.m_nRow, nChn); + if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)) + { + if (param) chn.nOldTempo = static_cast<ModCommand::PARAM>(param); else param = chn.nOldTempo; + } + TEMPO t(param, 0); + LimitMax(t, GetModSpecifications().GetTempoMax()); + SetTempo(t); + } + break; + + // Set Offset + case CMD_OFFSET: + if(triggerNote) + { + // FT2 compatibility: Portamento + Offset = Ignore offset + // Test case: porta-offset.xm + if(bPorta && GetType() == MOD_TYPE_XM) + break; + + ProcessSampleOffset(chn, nChn, m_PlayState); + } + break; + + // Disorder Tracker 2 percentage offset + case CMD_OFFSETPERCENTAGE: + if(triggerNote) + { + SampleOffset(chn, Util::muldiv_unsigned(chn.nLength, param, 256)); + } + break; + + // Arpeggio + case CMD_ARPEGGIO: + // IT compatibility 01. Don't ignore Arpeggio if no note is playing (also valid for ST3) + if(m_PlayState.m_nTickCount) break; + if((!chn.nPeriod || !chn.nNote) + && (chn.pModInstrument == nullptr || !chn.pModInstrument->HasValidMIDIChannel()) // Plugin arpeggio + && !m_playBehaviour[kITArpeggio] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) break; + if (!param && (GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD))) break; // Only important when editing MOD/XM files (000 effects are removed when loading files where this means "no effect") + chn.nCommand = CMD_ARPEGGIO; + if (param) chn.nArpeggio = static_cast<ModCommand::PARAM>(param); + break; + + // Retrig + case CMD_RETRIG: + if (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2)) + { + if (!(param & 0xF0)) param |= chn.nRetrigParam & 0xF0; + if (!(param & 0x0F)) param |= chn.nRetrigParam & 0x0F; + param |= 0x100; // increment retrig count on first row + } + // IT compatibility 15. Retrigger + if(m_playBehaviour[kITRetrigger]) + { + if (param) chn.nRetrigParam = static_cast<uint8>(param & 0xFF); + RetrigNote(nChn, chn.nRetrigParam, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0); + } else + { + // XM Retrig + if (param) chn.nRetrigParam = static_cast<uint8>(param & 0xFF); else param = chn.nRetrigParam; + RetrigNote(nChn, param, (volcmd == VOLCMD_OFFSET) ? vol + 1 : 0); + } + break; + + // Tremor + case CMD_TREMOR: + if(!m_SongFlags[SONG_FIRSTTICK]) + { + break; + } + + // IT compatibility 12. / 13. Tremor (using modified DUMB's Tremor logic here because of old effects - http://dumb.sf.net/) + if(m_playBehaviour[kITTremor]) + { + if(param && !m_SongFlags[SONG_ITOLDEFFECTS]) + { + // Old effects have different length interpretation (+1 for both on and off) + if(param & 0xF0) + param -= 0x10; + if(param & 0x0F) + param -= 0x01; + chn.nTremorParam = static_cast<ModCommand::PARAM>(param); + } + chn.nTremorCount |= 0x80; // set on/off flag + } else if(m_playBehaviour[kFT2Tremor]) + { + // XM Tremor. Logic is being processed in sndmix.cpp + chn.nTremorCount |= 0x80; // set on/off flag + } + + chn.nCommand = CMD_TREMOR; + if(param) + chn.nTremorParam = static_cast<ModCommand::PARAM>(param); + + break; + + // Set Global Volume + case CMD_GLOBALVOLUME: + // IT compatibility: Only apply global volume on first tick (and multiples) + // Test case: GlobalVolFirstTick.it + if(!m_SongFlags[SONG_FIRSTTICK]) + break; + // ST3 applies global volume on tick 1 and does other weird things, but we won't emulate this for now. +// if(((GetType() & MOD_TYPE_S3M) && m_nTickCount != 1) +// || (!(GetType() & MOD_TYPE_S3M) && !m_SongFlags[SONG_FIRSTTICK])) +// { +// break; +// } + + // FT2 compatibility: On channels that are "left" of the global volume command, the new global volume is not applied + // until the second tick of the row. Since we apply global volume on the mix buffer rather than note volumes, this + // cannot be fixed for now. + // Test case: GlobalVolume.xm +// if(IsCompatibleMode(TRK_FASTTRACKER2) && m_SongFlags[SONG_FIRSTTICK] && m_nMusicSpeed > 1) +// { +// break; +// } + + if (!(GetType() & GLOBALVOL_7BIT_FORMATS)) param *= 2; + + // IT compatibility 16. ST3 and IT ignore out-of-range values. + // Test case: globalvol-invalid.it + if(param <= 128) + { + m_PlayState.m_nGlobalVolume = param * 2; + } else if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M))) + { + m_PlayState.m_nGlobalVolume = 256; + } + break; + + // Global Volume Slide + case CMD_GLOBALVOLSLIDE: + //IT compatibility 16. Saving last global volume slide param per channel (FT2/IT) + if(m_playBehaviour[kPerChannelGlobalVolSlide]) + GlobalVolSlide(static_cast<ModCommand::PARAM>(param), chn.nOldGlobalVolSlide); + else + GlobalVolSlide(static_cast<ModCommand::PARAM>(param), m_PlayState.Chn[0].nOldGlobalVolSlide); + break; + + // Set 8-bit Panning + case CMD_PANNING8: + if(m_SongFlags[SONG_FIRSTTICK]) + { + Panning(chn, param, Pan8bit); + } + break; + + // Panning Slide + case CMD_PANNINGSLIDE: + PanningSlide(chn, static_cast<ModCommand::PARAM>(param)); + break; + + // Tremolo + case CMD_TREMOLO: + Tremolo(chn, param); + break; + + // Fine Vibrato + case CMD_FINEVIBRATO: + FineVibrato(chn, param); + break; + + // MOD/XM Exx Extended Commands + case CMD_MODCMDEX: + ExtendedMODCommands(nChn, static_cast<ModCommand::PARAM>(param)); + break; + + // S3M/IT Sxx Extended Commands + case CMD_S3MCMDEX: + ExtendedS3MCommands(nChn, static_cast<ModCommand::PARAM>(param)); + break; + + // Key Off + case CMD_KEYOFF: + // This is how Key Off is supposed to sound... (in FT2 at least) + if(m_playBehaviour[kFT2KeyOff]) + { + if (m_PlayState.m_nTickCount == param) + { + // XM: Key-Off + Sample == Note Cut + if(chn.pModInstrument == nullptr || !chn.pModInstrument->VolEnv.dwFlags[ENV_ENABLED]) + { + if(param == 0 && (chn.rowCommand.instr || chn.rowCommand.volcmd != VOLCMD_NONE)) // FT2 is weird.... + { + chn.dwFlags.set(CHN_NOTEFADE); + } + else + { + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.nVolume = 0; + } + } + KeyOff(chn); + } + } + // This is how it's NOT supposed to sound... + else + { + if(m_SongFlags[SONG_FIRSTTICK]) + KeyOff(chn); + } + break; + + // Extra-fine porta up/down + case CMD_XFINEPORTAUPDOWN: + switch(param & 0xF0) + { + case 0x10: ExtraFinePortamentoUp(chn, param & 0x0F); break; + case 0x20: ExtraFinePortamentoDown(chn, param & 0x0F); break; + // ModPlug XM Extensions (ignore in compatible mode) + case 0x50: + case 0x60: + case 0x70: + case 0x90: + case 0xA0: + if(!m_playBehaviour[kFT2RestrictXCommand]) ExtendedS3MCommands(nChn, static_cast<ModCommand::PARAM>(param)); + break; + } + break; + + case CMD_FINETUNE: + case CMD_FINETUNE_SMOOTH: + if(m_SongFlags[SONG_FIRSTTICK] || cmd == CMD_FINETUNE_SMOOTH) + { + SetFinetune(nChn, m_PlayState, cmd == CMD_FINETUNE_SMOOTH); +#ifndef NO_PLUGINS + if(IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); plugin != nullptr) + plugin->MidiPitchBendRaw(chn.GetMIDIPitchBend(), nChn); +#endif // NO_PLUGINS + } + break; + + // Set Channel Global Volume + case CMD_CHANNELVOLUME: + if(!m_SongFlags[SONG_FIRSTTICK]) break; + if (param <= 64) + { + chn.nGlobalVol = param; + chn.dwFlags.set(CHN_FASTVOLRAMP); + } + break; + + // Channel volume slide + case CMD_CHANNELVOLSLIDE: + ChannelVolSlide(chn, static_cast<ModCommand::PARAM>(param)); + break; + + // Panbrello (IT) + case CMD_PANBRELLO: + Panbrello(chn, param); + break; + + // Set Envelope Position + case CMD_SETENVPOSITION: + if(m_SongFlags[SONG_FIRSTTICK]) + { + chn.VolEnv.nEnvPosition = param; + + // FT2 compatibility: FT2 only sets the position of the panning envelope if the volume envelope's sustain flag is set + // Test case: SetEnvPos.xm + if(!m_playBehaviour[kFT2SetPanEnvPos] || chn.VolEnv.flags[ENV_SUSTAIN]) + { + chn.PanEnv.nEnvPosition = param; + chn.PitchEnv.nEnvPosition = param; + } + + } + break; + + // Position Jump + case CMD_POSITIONJUMP: + PositionJump(m_PlayState, nChn); + break; + + // Pattern Break + case CMD_PATTERNBREAK: + if(ROWINDEX row = PatternBreak(m_PlayState, nChn, static_cast<ModCommand::PARAM>(param)); row != ROWINDEX_INVALID) + { + m_PlayState.m_breakRow = row; + if(m_SongFlags[SONG_PATTERNLOOP]) + { + //If song is set to loop and a pattern break occurs we should stay on the same pattern. + //Use nPosJump to force playback to "jump to this pattern" rather than move to next, as by default. + m_PlayState.m_posJump = m_PlayState.m_nCurrentOrder; + } + } + break; + + // IMF / PTM Note Slides + case CMD_NOTESLIDEUP: + case CMD_NOTESLIDEDOWN: + case CMD_NOTESLIDEUPRETRIG: + case CMD_NOTESLIDEDOWNRETRIG: + // Note that this command seems to be a bit buggy in Polytracker... Luckily, no tune seems to seriously use this + // (Vic uses it e.g. in Spaceman or Perfect Reason to slide effect samples, noone will notice the difference :) + NoteSlide(chn, param, cmd == CMD_NOTESLIDEUP || cmd == CMD_NOTESLIDEUPRETRIG, cmd == CMD_NOTESLIDEUPRETRIG || cmd == CMD_NOTESLIDEDOWNRETRIG); + break; + + // PTM Reverse sample + offset (executed on every tick) + case CMD_REVERSEOFFSET: + ReverseSampleOffset(chn, static_cast<ModCommand::PARAM>(param)); + break; + +#ifndef NO_PLUGINS + // DBM: Toggle DSP Echo + case CMD_DBMECHO: + if(m_PlayState.m_nTickCount == 0) + { + uint32 echoType = (param >> 4), enable = (param & 0x0F); + if(echoType > 2 || enable > 1) + { + break; + } + CHANNELINDEX firstChn = nChn, lastChn = nChn; + if(echoType == 1) + { + firstChn = 0; + lastChn = m_nChannels - 1; + } + for(CHANNELINDEX c = firstChn; c <= lastChn; c++) + { + ChnSettings[c].dwFlags.set(CHN_NOFX, enable == 1); + m_PlayState.Chn[c].dwFlags.set(CHN_NOFX, enable == 1); + } + } + break; +#endif // NO_PLUGINS + + // Digi Booster sample reverse + case CMD_DIGIREVERSESAMPLE: + DigiBoosterSampleReverse(chn, static_cast<ModCommand::PARAM>(param)); + break; + } + + if(m_playBehaviour[kST3EffectMemory] && param != 0) + { + UpdateS3MEffectMemory(chn, static_cast<ModCommand::PARAM>(param)); + } + + if(chn.rowCommand.instr) + { + // Not necessarily consistent with actually playing instrument for IT compatibility + chn.nOldIns = chn.rowCommand.instr; + } + + } // for(...) end + + // Navigation Effects + if(m_SongFlags[SONG_FIRSTTICK]) + { + if(HandleNextRow(m_PlayState, Order(), true)) + m_SongFlags.set(SONG_BREAKTOROW); + } + return true; +} + + +bool CSoundFile::HandleNextRow(PlayState &state, const ModSequence &order, bool honorPatternLoop) const +{ + const bool doPatternLoop = (state.m_patLoopRow != ROWINDEX_INVALID); + const bool doBreakRow = (state.m_breakRow != ROWINDEX_INVALID); + const bool doPosJump = (state.m_posJump != ORDERINDEX_INVALID); + bool breakToRow = false; + + // Pattern Break / Position Jump only if no loop running + // Exception: FastTracker 2 in all cases, Impulse Tracker in case of position jump + // Test case for FT2 exception: PatLoop-Jumps.xm, PatLoop-Various.xm + // Test case for IT: exception: LoopBreak.it, sbx-priority.it + if((doBreakRow || doPosJump) + && (!doPatternLoop + || m_playBehaviour[kFT2PatternLoopWithJumps] + || (m_playBehaviour[kITPatternLoopWithJumps] && doPosJump) + || (m_playBehaviour[kITPatternLoopWithJumpsOld] && doPosJump))) + { + if(!doPosJump) + state.m_posJump = state.m_nCurrentOrder + 1; + if(!doBreakRow) + state.m_breakRow = 0; + breakToRow = true; + + if(state.m_posJump >= order.size()) + state.m_posJump = order.GetRestartPos(); + + // IT / FT2 compatibility: don't reset loop count on pattern break. + // Test case: gm-trippy01.it, PatLoop-Break.xm, PatLoop-Weird.xm, PatLoop-Break.mod + if(state.m_posJump != state.m_nCurrentOrder + && !m_playBehaviour[kITPatternLoopBreak] && !m_playBehaviour[kFT2PatternLoopWithJumps] && GetType() != MOD_TYPE_MOD) + { + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) + { + state.Chn[i].nPatternLoopCount = 0; + } + } + + state.m_nNextRow = state.m_breakRow; + if(!honorPatternLoop || !m_SongFlags[SONG_PATTERNLOOP]) + state.m_nNextOrder = state.m_posJump; + } else if(doPatternLoop) + { + // Pattern Loop + state.m_nNextOrder = state.m_nCurrentOrder; + state.m_nNextRow = state.m_patLoopRow; + // FT2 skips the first row of the pattern loop if there's a pattern delay, ProTracker sometimes does it too (didn't quite figure it out yet). + // But IT and ST3 don't do this. + // Test cases: PatLoopWithDelay.it, PatLoopWithDelay.s3m + if(state.m_nPatternDelay + && (GetType() != MOD_TYPE_IT || !m_playBehaviour[kITPatternLoopWithJumps]) + && GetType() != MOD_TYPE_S3M) + { + state.m_nNextRow++; + } + + // IT Compatibility: If the restart row is past the end of the current pattern + // (e.g. when continued from a previous pattern without explicit SB0 effect), continue the next pattern. + // Test case: LoopStartAfterPatternEnd.it + if(state.m_patLoopRow >= Patterns[state.m_nPattern].GetNumRows()) + { + state.m_nNextOrder++; + state.m_nNextRow = 0; + } + } + + return breakToRow; +} + + +//////////////////////////////////////////////////////////// +// Channels effects + + +// Update the effect memory of all S3M effects that use the last non-zero effect parameter as memory (Dxy, Exx, Fxx, Ixy, Jxy, Kxy, Lxy, Qxy, Rxy, Sxy) +// Test case: ParamMemory.s3m +void CSoundFile::UpdateS3MEffectMemory(ModChannel &chn, ModCommand::PARAM param) const +{ + chn.nOldVolumeSlide = param; // Dxy / Kxy / Lxy + chn.nOldPortaUp = param; // Exx / Fxx + chn.nOldPortaDown = param; // Exx / Fxx + chn.nTremorParam = param; // Ixy + chn.nArpeggio = param; // Jxy + chn.nRetrigParam = param; // Qxy + chn.nTremoloDepth = (param & 0x0F) << 2; // Rxy + chn.nTremoloSpeed = (param >> 4) & 0x0F; // Rxy + chn.nOldCmdEx = param; // Sxy +} + + +// Calculate full parameter for effects that support parameter extension at the given pattern location. +// maxCommands sets the maximum number of XParam commands to look at for this effect +// extendedRows returns how many extended rows are used (i.e. a value of 0 means the command is not extended). +uint32 CSoundFile::CalculateXParam(PATTERNINDEX pat, ROWINDEX row, CHANNELINDEX chn, uint32 *extendedRows) const +{ + if(extendedRows != nullptr) + *extendedRows = 0; + if(!Patterns.IsValidPat(pat)) + { +#ifdef MPT_BUILD_FUZZER + // Ending up in this situation implies a logic error + std::abort(); +#else + return 0; +#endif + } + ROWINDEX maxCommands = 4; + const ModCommand *m = Patterns[pat].GetpModCommand(row, chn); + const auto startCmd = m->command; + uint32 val = m->param; + + switch(m->command) + { + case CMD_OFFSET: + // 24 bit command + maxCommands = 2; + break; + case CMD_TEMPO: + case CMD_PATTERNBREAK: + case CMD_POSITIONJUMP: + case CMD_FINETUNE: + case CMD_FINETUNE_SMOOTH: + // 16 bit command + maxCommands = 1; + break; + default: + return val; + } + + const bool xmTempoFix = m->command == CMD_TEMPO && GetType() == MOD_TYPE_XM; + ROWINDEX numRows = std::min(Patterns[pat].GetNumRows() - row - 1, maxCommands); + uint32 extRows = 0; + while(numRows > 0) + { + m += Patterns[pat].GetNumChannels(); + if(m->command != CMD_XPARAM) + break; + + if(xmTempoFix && val < 256) + { + // With XM, 0x20 is the lowest tempo. Anything below changes ticks per row. + val -= 0x20; + } + val = (val << 8) | m->param; + numRows--; + extRows++; + } + + // Always return a full-precision value for finetune + if((startCmd == CMD_FINETUNE || startCmd == CMD_FINETUNE_SMOOTH) && !extRows) + val <<= 8; + + if(extendedRows != nullptr) + *extendedRows = extRows; + + return val; +} + + +void CSoundFile::PositionJump(PlayState &state, CHANNELINDEX chn) const +{ + state.m_nextPatStartRow = 0; // FT2 E60 bug + state.m_posJump = static_cast<ORDERINDEX>(CalculateXParam(state.m_nPattern, state.m_nRow, chn)); + + // see https://forum.openmpt.org/index.php?topic=2769.0 - FastTracker resets Dxx if Bxx is called _after_ Dxx + // Test case: PatternJump.mod + if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) && state.m_breakRow != ROWINDEX_INVALID) + { + state.m_breakRow = 0; + } +} + + +ROWINDEX CSoundFile::PatternBreak(PlayState &state, CHANNELINDEX chn, uint8 param) const +{ + if(param >= 64 && (GetType() & MOD_TYPE_S3M)) + { + // ST3 ignores invalid pattern breaks. + return ROWINDEX_INVALID; + } + + state.m_nextPatStartRow = 0; // FT2 E60 bug + + return static_cast<ROWINDEX>(CalculateXParam(state.m_nPattern, state.m_nRow, chn)); +} + + +void CSoundFile::PortamentoUp(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + + if(param) + { + // FT2 compatibility: Separate effect memory for all portamento commands + // Test case: Porta-LinkMem.xm + if(!m_playBehaviour[kFT2PortaUpDownMemory]) + chn.nOldPortaDown = param; + chn.nOldPortaUp = param; + } else + { + param = chn.nOldPortaUp; + } + + const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM)); + + // Process MIDI pitch bend for instrument plugins + MidiPortamento(nChn, param, doFineSlides); + + if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning) + { + // Portamento for instruments with custom tuning + if(param >= 0xF0 && !doFinePortamentoAsRegular) + PortamentoFineMPT(chn, param - 0xF0); + else if(param >= 0xE0 && !doFinePortamentoAsRegular) + PortamentoExtraFineMPT(chn, param - 0xE0); + else + PortamentoMPT(chn, param); + return; + } else if(GetType() == MOD_TYPE_PLM) + { + // A normal portamento up or down makes a follow-up tone portamento go the same direction. + chn.nPortamentoDest = 1; + } + + if (doFineSlides && param >= 0xE0) + { + if (param & 0x0F) + { + if ((param & 0xF0) == 0xF0) + { + FinePortamentoUp(chn, param & 0x0F); + return; + } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM) + { + ExtraFinePortamentoUp(chn, param & 0x0F); + return; + } + } + if(GetType() != MOD_TYPE_DBM) + { + // DBM only has fine slides, no extra-fine slides. + return; + } + } + // Regular Slide + if(!chn.isFirstTick + || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) + || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT)) + || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES])) + { + DoFreqSlide(chn, chn.nPeriod, param * 4); + } +} + + +void CSoundFile::PortamentoDown(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + + if(param) + { + // FT2 compatibility: Separate effect memory for all portamento commands + // Test case: Porta-LinkMem.xm + if(!m_playBehaviour[kFT2PortaUpDownMemory]) + chn.nOldPortaUp = param; + chn.nOldPortaDown = param; + } else + { + param = chn.nOldPortaDown; + } + + const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM)); + + // Process MIDI pitch bend for instrument plugins + MidiPortamento(nChn, -static_cast<int>(param), doFineSlides); + + if(GetType() == MOD_TYPE_MPT && chn.pModInstrument && chn.pModInstrument->pTuning) + { + // Portamento for instruments with custom tuning + if(param >= 0xF0 && !doFinePortamentoAsRegular) + PortamentoFineMPT(chn, -static_cast<int>(param - 0xF0)); + else if(param >= 0xE0 && !doFinePortamentoAsRegular) + PortamentoExtraFineMPT(chn, -static_cast<int>(param - 0xE0)); + else + PortamentoMPT(chn, -static_cast<int>(param)); + return; + } else if(GetType() == MOD_TYPE_PLM) + { + // A normal portamento up or down makes a follow-up tone portamento go the same direction. + chn.nPortamentoDest = 65535; + } + + if(doFineSlides && param >= 0xE0) + { + if (param & 0x0F) + { + if ((param & 0xF0) == 0xF0) + { + FinePortamentoDown(chn, param & 0x0F); + return; + } else if ((param & 0xF0) == 0xE0 && GetType() != MOD_TYPE_DBM) + { + ExtraFinePortamentoDown(chn, param & 0x0F); + return; + } + } + if(GetType() != MOD_TYPE_DBM) + { + // DBM only has fine slides, no extra-fine slides. + return; + } + } + + if(!chn.isFirstTick + || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) + || (GetType() & (MOD_TYPE_669 | MOD_TYPE_OKT)) + || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES])) + { + DoFreqSlide(chn, chn.nPeriod, param * -4); + } +} + + +// Send portamento commands to plugins +void CSoundFile::MidiPortamento(CHANNELINDEX nChn, int param, bool doFineSlides) +{ + int actualParam = std::abs(param); + int pitchBend = 0; + + // Old MIDI Pitch Bends: + // - Applied on every tick + // - No fine pitch slides (they are interpreted as normal slides) + // New MIDI Pitch Bends: + // - Behaviour identical to sample pitch bends if the instrument's PWD parameter corresponds to the actual VSTi setting. + + if(doFineSlides && actualParam >= 0xE0 && !m_playBehaviour[kOldMIDIPitchBends]) + { + if(m_PlayState.Chn[nChn].isFirstTick) + { + // Extra fine slide... + pitchBend = (actualParam & 0x0F) * mpt::signum(param); + if(actualParam >= 0xF0) + { + // ... or just a fine slide! + pitchBend *= 4; + } + } + } else if(!m_PlayState.Chn[nChn].isFirstTick || m_playBehaviour[kOldMIDIPitchBends]) + { + // Regular slide + pitchBend = param * 4; + } + + if(pitchBend) + { +#ifndef NO_PLUGINS + IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); + if(plugin != nullptr) + { + int8 pwd = 13; // Early OpenMPT legacy... Actually it's not *exactly* 13, but close enough... + if(m_PlayState.Chn[nChn].pModInstrument != nullptr) + { + pwd = m_PlayState.Chn[nChn].pModInstrument->midiPWD; + } + plugin->MidiPitchBend(pitchBend, pwd, nChn); + } +#endif // NO_PLUGINS + } +} + + +void CSoundFile::FinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const +{ + MPT_ASSERT(!chn.HasCustomTuning()); + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked + // Test case: Porta-LinkMem.xm + if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldFinePortaUpDown >> 4); + } else if(GetType() == MOD_TYPE_MT2) + { + if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; + } + + if(chn.isFirstTick && chn.nPeriod && param) + DoFreqSlide(chn, chn.nPeriod, param * 4); +} + + +void CSoundFile::FinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const +{ + MPT_ASSERT(!chn.HasCustomTuning()); + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked + // Test case: Porta-LinkMem.xm + if(param) chn.nOldFinePortaUpDown = (chn.nOldFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldFinePortaUpDown & 0x0F); + } else if(GetType() == MOD_TYPE_MT2) + { + if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; + } + + if(chn.isFirstTick && chn.nPeriod && param) + { + DoFreqSlide(chn, chn.nPeriod, param * -4); + if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM)) + chn.nPeriod = 0xFFFF; + } +} + + +void CSoundFile::ExtraFinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const +{ + MPT_ASSERT(!chn.HasCustomTuning()); + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked + // Test case: Porta-LinkMem.xm + if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0x0F) | (param << 4); else param = (chn.nOldExtraFinePortaUpDown >> 4); + } else if(GetType() == MOD_TYPE_MT2) + { + if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; + } + + if(chn.isFirstTick && chn.nPeriod && param) + DoFreqSlide(chn, chn.nPeriod, param); +} + + +void CSoundFile::ExtraFinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const +{ + MPT_ASSERT(!chn.HasCustomTuning()); + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: E1x / E2x / X1x / X2x memory is not linked + // Test case: Porta-LinkMem.xm + if(param) chn.nOldExtraFinePortaUpDown = (chn.nOldExtraFinePortaUpDown & 0xF0) | (param & 0x0F); else param = (chn.nOldExtraFinePortaUpDown & 0x0F); + } else if(GetType() == MOD_TYPE_MT2) + { + if(param) chn.nOldFinePortaUpDown = param; else param = chn.nOldFinePortaUpDown; + } + + if(chn.isFirstTick && chn.nPeriod && param) + { + DoFreqSlide(chn, chn.nPeriod, -static_cast<int32>(param)); + if(chn.nPeriod > 0xFFFF && !m_playBehaviour[kPeriodsAreHertz] && (!m_SongFlags[SONG_LINEARSLIDES] || GetType() == MOD_TYPE_XM)) + chn.nPeriod = 0xFFFF; + } +} + + +void CSoundFile::SetFinetune(CHANNELINDEX channel, PlayState &playState, bool isSmooth) const +{ + ModChannel &chn = playState.Chn[channel]; + int16 newTuning = mpt::saturate_cast<int16>(static_cast<int32>(CalculateXParam(playState.m_nPattern, playState.m_nRow, channel, nullptr)) - 0x8000); + + if(isSmooth) + { + const int32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount; + if(ticksLeft > 1) + { + const int32 step = (newTuning - chn.microTuning) / ticksLeft; + newTuning = mpt::saturate_cast<int16>(chn.microTuning + step); + } + } + chn.microTuning = newTuning; +} + + +// Implemented for IMF / PTM / OKT compatibility, can't actually save this in any formats +// Slide up / down every x ticks by y semitones +// Oktalyzer: Slide down on first tick only, or on every tick +void CSoundFile::NoteSlide(ModChannel &chn, uint32 param, bool slideUp, bool retrig) const +{ + if(m_SongFlags[SONG_FIRSTTICK]) + { + if(param & 0xF0) + chn.noteSlideParam = static_cast<uint8>(param & 0xF0) | (chn.noteSlideParam & 0x0F); + if(param & 0x0F) + chn.noteSlideParam = (chn.noteSlideParam & 0xF0) | static_cast<uint8>(param & 0x0F); + chn.noteSlideCounter = (chn.noteSlideParam >> 4); + } + + bool doTrigger = false; + if(GetType() == MOD_TYPE_OKT) + doTrigger = ((chn.noteSlideParam & 0xF0) == 0x10) || m_SongFlags[SONG_FIRSTTICK]; + else + doTrigger = !m_SongFlags[SONG_FIRSTTICK] && (--chn.noteSlideCounter == 0); + + if(doTrigger) + { + const uint8 speed = (chn.noteSlideParam >> 4), steps = (chn.noteSlideParam & 0x0F); + chn.noteSlideCounter = speed; + // update it + const int32 delta = (slideUp ? steps : -steps); + if(chn.HasCustomTuning()) + chn.m_PortamentoFineSteps += delta * chn.pModInstrument->pTuning->GetFineStepCount(); + else + chn.nPeriod = GetPeriodFromNote(delta + GetNoteFromPeriod(chn.nPeriod, chn.nFineTune, chn.nC5Speed), chn.nFineTune, chn.nC5Speed); + + if(retrig) + chn.position.Set(0); + } +} + + +std::pair<uint16, bool> CSoundFile::GetVolCmdTonePorta(const ModCommand &m, uint32 startTick) const +{ + if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_AMS | MOD_TYPE_DMF | MOD_TYPE_DBM | MOD_TYPE_IMF | MOD_TYPE_PSM | MOD_TYPE_J2B | MOD_TYPE_ULT | MOD_TYPE_OKT | MOD_TYPE_MT2 | MOD_TYPE_MDL)) + { + return {ImpulseTrackerPortaVolCmd[m.vol & 0x0F], false}; + } else + { + bool clearEffectColumn = false; + uint16 vol = m.vol; + if(m.command == CMD_TONEPORTAMENTO && GetType() == MOD_TYPE_XM) + { + // Yes, FT2 is *that* weird. If there is a Mx command in the volume column + // and a normal 3xx command, the 3xx command is ignored but the Mx command's + // effectiveness is doubled. + // Test case: TonePortamentoMemory.xm + clearEffectColumn = true; + vol *= 2; + } + + // FT2 compatibility: If there's a portamento and a note delay, execute the portamento, but don't update the parameter + // Test case: PortaDelay.xm + if(m_playBehaviour[kFT2PortaDelay] && startTick != 0) + return {uint16(0), clearEffectColumn}; + else + return {static_cast<uint16>(vol * 16), clearEffectColumn}; + } +} + + +// Portamento Slide +void CSoundFile::TonePortamento(ModChannel &chn, uint16 param) const +{ + chn.dwFlags.set(CHN_PORTAMENTO); + + //IT compatibility 03: Share effect memory with portamento up/down + if((!m_SongFlags[SONG_ITCOMPATGXX] && m_playBehaviour[kITPortaMemoryShare]) || GetType() == MOD_TYPE_PLM) + { + if(param == 0) param = chn.nOldPortaUp; + chn.nOldPortaUp = chn.nOldPortaDown = static_cast<uint8>(param); + } + + if(param) + chn.portamentoSlide = param; + + if(chn.HasCustomTuning()) + { + //Behavior: Param tells number of finesteps(or 'fullsteps'(notes) with glissando) + //to slide per row(not per tick). + if(chn.portamentoSlide == 0) + return; + + const int32 oldPortamentoTickSlide = (m_PlayState.m_nTickCount != 0) ? chn.m_PortamentoTickSlide : 0; + + int32 delta = chn.portamentoSlide; + if(chn.nPortamentoDest < 0) + delta = -delta; + + chn.m_PortamentoTickSlide = static_cast<int32>((m_PlayState.m_nTickCount + 1.0) * delta / m_PlayState.m_nMusicSpeed); + + if(chn.dwFlags[CHN_GLISSANDO]) + { + chn.m_PortamentoTickSlide *= chn.pModInstrument->pTuning->GetFineStepCount() + 1; + //With glissando interpreting param as notes instead of finesteps. + } + + const int32 slide = chn.m_PortamentoTickSlide - oldPortamentoTickSlide; + + if(std::abs(chn.nPortamentoDest) <= std::abs(slide)) + { + if(chn.nPortamentoDest != 0) + { + chn.m_PortamentoFineSteps += chn.nPortamentoDest; + chn.nPortamentoDest = 0; + chn.m_CalculateFreq = true; + } + } else + { + chn.m_PortamentoFineSteps += slide; + chn.nPortamentoDest -= slide; + chn.m_CalculateFreq = true; + } + + return; + } + + bool doPorta = !chn.isFirstTick + || (GetType() & (MOD_TYPE_DBM | MOD_TYPE_669)) + || (m_PlayState.m_nMusicSpeed == 1 && m_playBehaviour[kSlidesAtSpeed1]) + || (GetType() == MOD_TYPE_MED && m_SongFlags[SONG_FASTVOLSLIDES]); + + int32 delta = chn.portamentoSlide; + if(GetType() == MOD_TYPE_PLM && delta >= 0xF0) + { + delta -= 0xF0; + doPorta = chn.isFirstTick; + } + + if(chn.nPeriod && chn.nPortamentoDest && doPorta) + { + delta *= (GetType() == MOD_TYPE_669) ? 2 : 4; + if(!PeriodsAreFrequencies()) + delta = -delta; + if(chn.nPeriod < chn.nPortamentoDest || chn.portaTargetReached) + { + DoFreqSlide(chn, chn.nPeriod, delta, true); + if(chn.nPeriod > chn.nPortamentoDest) + chn.nPeriod = chn.nPortamentoDest; + } else if(chn.nPeriod > chn.nPortamentoDest) + { + DoFreqSlide(chn, chn.nPeriod, -delta, true); + if(chn.nPeriod < chn.nPortamentoDest) + chn.nPeriod = chn.nPortamentoDest; + // FT2 compatibility: Reaching portamento target from below forces subsequent portamentos on the same note to use the logic for reaching the note from above instead. + // Test case: PortaResetDirection.xm + if(chn.nPeriod == chn.nPortamentoDest && m_playBehaviour[kFT2PortaResetDirection]) + chn.portaTargetReached = true; + } + } + + // IT compatibility 23. Portamento with no note + // ProTracker also disables portamento once the target is reached. + // Test case: PortaTarget.mod + if(chn.nPeriod == chn.nPortamentoDest && (m_playBehaviour[kITPortaTargetReached] || GetType() == MOD_TYPE_MOD)) + chn.nPortamentoDest = 0; + +} + + +void CSoundFile::Vibrato(ModChannel &chn, uint32 param) const +{ + if (param & 0x0F) chn.nVibratoDepth = (param & 0x0F) * 4; + if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F; + chn.dwFlags.set(CHN_VIBRATO); +} + + +void CSoundFile::FineVibrato(ModChannel &chn, uint32 param) const +{ + if (param & 0x0F) chn.nVibratoDepth = param & 0x0F; + if (param & 0xF0) chn.nVibratoSpeed = (param >> 4) & 0x0F; + chn.dwFlags.set(CHN_VIBRATO); + // ST3 compatibility: Do not distinguish between vibrato types in effect memory + // Test case: VibratoTypeChange.s3m + if(m_playBehaviour[kST3VibratoMemory] && (param & 0x0F)) + { + chn.nVibratoDepth *= 4u; + } +} + + +void CSoundFile::Panbrello(ModChannel &chn, uint32 param) const +{ + if (param & 0x0F) chn.nPanbrelloDepth = param & 0x0F; + if (param & 0xF0) chn.nPanbrelloSpeed = (param >> 4) & 0x0F; +} + + +void CSoundFile::Panning(ModChannel &chn, uint32 param, PanningType panBits) const +{ + // No panning in ProTracker mode + if(m_playBehaviour[kMODIgnorePanning]) + { + return; + } + // IT Compatibility (and other trackers as well): panning disables surround (unless panning in rear channels is enabled, which is not supported by the original trackers anyway) + if (!m_SongFlags[SONG_SURROUNDPAN] && (panBits == Pan8bit || m_playBehaviour[kPanOverride])) + { + chn.dwFlags.reset(CHN_SURROUND); + } + if(panBits == Pan4bit) + { + // 0...15 panning + chn.nPan = (param * 256 + 8) / 15; + } else if(panBits == Pan6bit) + { + // 0...64 panning + if(param > 64) param = 64; + chn.nPan = param * 4; + } else + { + if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_DSM | MOD_TYPE_AMF0 | MOD_TYPE_AMF | MOD_TYPE_MTM))) + { + // Real 8-bit panning + chn.nPan = param; + } else + { + // 7-bit panning + surround + if(param <= 0x80) + { + chn.nPan = param << 1; + } else if(param == 0xA4) + { + chn.dwFlags.set(CHN_SURROUND); + chn.nPan = 0x80; + } + } + } + + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.nRestorePanOnNewNote = 0; + //IT compatibility 20. Set pan overrides random pan + if(m_playBehaviour[kPanOverride]) + { + chn.nPanSwing = 0; + chn.nPanbrelloOffset = 0; + } +} + + +void CSoundFile::VolumeSlide(ModChannel &chn, ModCommand::PARAM param) const +{ + if (param) + chn.nOldVolumeSlide = param; + else + param = chn.nOldVolumeSlide; + + if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM))) + { + // MOD / XM nibble priority + if((param & 0xF0) != 0) + { + param &= 0xF0; + } else + { + param &= 0x0F; + } + } + + int newVolume = chn.nVolume; + if(!(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_AMF0 | MOD_TYPE_MED | MOD_TYPE_DIGI))) + { + if ((param & 0x0F) == 0x0F) //Fine upslide or slide -15 + { + if (param & 0xF0) //Fine upslide + { + FineVolumeUp(chn, (param >> 4), false); + return; + } else //Slide -15 + { + if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES]) + { + newVolume -= 0x0F * 4; + } + } + } else + if ((param & 0xF0) == 0xF0) //Fine downslide or slide +15 + { + if (param & 0x0F) //Fine downslide + { + FineVolumeDown(chn, (param & 0x0F), false); + return; + } else //Slide +15 + { + if(chn.isFirstTick && !m_SongFlags[SONG_FASTVOLSLIDES]) + { + newVolume += 0x0F * 4; + } + } + } + } + if(!chn.isFirstTick || m_SongFlags[SONG_FASTVOLSLIDES] || (m_PlayState.m_nMusicSpeed == 1 && GetType() == MOD_TYPE_DBM)) + { + // IT compatibility: Ignore slide commands with both nibbles set. + if (param & 0x0F) + { + if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0) + newVolume -= (int)((param & 0x0F) * 4); + } + else + { + newVolume += (int)((param & 0xF0) >> 2); + } + if (GetType() == MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); + } + newVolume = Clamp(newVolume, 0, 256); + + chn.nVolume = newVolume; +} + + +void CSoundFile::PanningSlide(ModChannel &chn, ModCommand::PARAM param, bool memory) const +{ + if(memory) + { + // FT2 compatibility: Use effect memory (lxx and rxx in XM shouldn't use effect memory). + // Test case: PanSlideMem.xm + if(param) + chn.nOldPanSlide = param; + else + param = chn.nOldPanSlide; + } + + if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) + { + // XM nibble priority + if((param & 0xF0) != 0) + { + param &= 0xF0; + } else + { + param &= 0x0F; + } + } + + int32 nPanSlide = 0; + + if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) + { + if (((param & 0x0F) == 0x0F) && (param & 0xF0)) + { + if(m_SongFlags[SONG_FIRSTTICK]) + { + param = (param & 0xF0) / 4u; + nPanSlide = - (int)param; + } + } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) + { + if(m_SongFlags[SONG_FIRSTTICK]) + { + nPanSlide = (param & 0x0F) * 4u; + } + } else if(!m_SongFlags[SONG_FIRSTTICK]) + { + if (param & 0x0F) + { + // IT compatibility: Ignore slide commands with both nibbles set. + if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (param & 0xF0) == 0) + nPanSlide = (int)((param & 0x0F) * 4u); + } else + { + nPanSlide = -(int)((param & 0xF0) / 4u); + } + } + } else + { + if(!m_SongFlags[SONG_FIRSTTICK]) + { + if (param & 0xF0) + { + nPanSlide = (int)((param & 0xF0) / 4u); + } else + { + nPanSlide = -(int)((param & 0x0F) * 4u); + } + // FT2 compatibility: FT2's panning slide is like IT's fine panning slide (not as deep) + if(m_playBehaviour[kFT2PanSlide]) + nPanSlide /= 4; + } + } + if (nPanSlide) + { + nPanSlide += chn.nPan; + nPanSlide = Clamp(nPanSlide, 0, 256); + chn.nPan = nPanSlide; + chn.nRestorePanOnNewNote = 0; + } +} + + +void CSoundFile::FineVolumeUp(ModChannel &chn, ModCommand::PARAM param, bool volCol) const +{ + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: EAx / EBx memory is not linked + // Test case: FineVol-LinkMem.xm + if(param) chn.nOldFineVolUpDown = (param << 4) | (chn.nOldFineVolUpDown & 0x0F); else param = (chn.nOldFineVolUpDown >> 4); + } else if(volCol) + { + if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam; + } else + { + if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown; + } + + if(chn.isFirstTick) + { + chn.nVolume += param * 4; + if(chn.nVolume > 256) chn.nVolume = 256; + if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); + } +} + + +void CSoundFile::FineVolumeDown(ModChannel &chn, ModCommand::PARAM param, bool volCol) const +{ + if(GetType() == MOD_TYPE_XM) + { + // FT2 compatibility: EAx / EBx memory is not linked + // Test case: FineVol-LinkMem.xm + if(param) chn.nOldFineVolUpDown = param | (chn.nOldFineVolUpDown & 0xF0); else param = (chn.nOldFineVolUpDown & 0x0F); + } else if(volCol) + { + if(param) chn.nOldVolParam = param; else param = chn.nOldVolParam; + } else + { + if(param) chn.nOldFineVolUpDown = param; else param = chn.nOldFineVolUpDown; + } + + if(chn.isFirstTick) + { + chn.nVolume -= param * 4; + if(chn.nVolume < 0) chn.nVolume = 0; + if(GetType() & MOD_TYPE_MOD) chn.dwFlags.set(CHN_FASTVOLRAMP); + } +} + + +void CSoundFile::Tremolo(ModChannel &chn, uint32 param) const +{ + if (param & 0x0F) chn.nTremoloDepth = (param & 0x0F) << 2; + if (param & 0xF0) chn.nTremoloSpeed = (param >> 4) & 0x0F; + chn.dwFlags.set(CHN_TREMOLO); +} + + +void CSoundFile::ChannelVolSlide(ModChannel &chn, ModCommand::PARAM param) const +{ + int32 nChnSlide = 0; + if (param) chn.nOldChnVolSlide = param; else param = chn.nOldChnVolSlide; + + if (((param & 0x0F) == 0x0F) && (param & 0xF0)) + { + if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = param >> 4; + } else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) + { + if(m_SongFlags[SONG_FIRSTTICK]) nChnSlide = - (int)(param & 0x0F); + } else + { + if(!m_SongFlags[SONG_FIRSTTICK]) + { + if (param & 0x0F) + { + if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_J2B | MOD_TYPE_DBM)) || (param & 0xF0) == 0) + nChnSlide = -(int)(param & 0x0F); + } else + { + nChnSlide = (int)((param & 0xF0) >> 4); + } + } + } + if (nChnSlide) + { + nChnSlide += chn.nGlobalVol; + nChnSlide = Clamp(nChnSlide, 0, 64); + chn.nGlobalVol = nChnSlide; + } +} + + +void CSoundFile::ExtendedMODCommands(CHANNELINDEX nChn, ModCommand::PARAM param) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + uint8 command = param & 0xF0; + param &= 0x0F; + switch(command) + { + // E0x: Set Filter + case 0x00: + for(CHANNELINDEX channel = 0; channel < GetNumChannels(); channel++) + { + m_PlayState.Chn[channel].dwFlags.set(CHN_AMIGAFILTER, !(param & 1)); + } + break; + // E1x: Fine Portamento Up + case 0x10: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoUp(chn, param); break; + // E2x: Fine Portamento Down + case 0x20: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FinePortamentoDown(chn, param); break; + // E3x: Set Glissando Control + case 0x30: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break; + // E4x: Set Vibrato WaveForm + case 0x40: chn.nVibratoType = param & 0x07; break; + // E5x: Set FineTune + case 0x50: if(!m_SongFlags[SONG_FIRSTTICK]) + break; + if(GetType() & (MOD_TYPE_MOD | MOD_TYPE_DIGI | MOD_TYPE_AMF0 | MOD_TYPE_MED)) + { + chn.nFineTune = MOD2XMFineTune(param); + if(chn.nPeriod && chn.rowCommand.IsNote()) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); + } else if(GetType() == MOD_TYPE_MTM) + { + if(chn.rowCommand.IsNote() && chn.pModSample != nullptr) + { + // Effect is permanent in MultiTracker + const_cast<ModSample *>(chn.pModSample)->nFineTune = param; + chn.nFineTune = param; + if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); + } + } else if(chn.rowCommand.IsNote()) + { + chn.nFineTune = MOD2XMFineTune(param - 8); + if(chn.nPeriod) chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); + } + break; + // E6x: Pattern Loop + case 0x60: + if(m_SongFlags[SONG_FIRSTTICK]) + PatternLoop(m_PlayState, chn, param & 0x0F); + break; + // E7x: Set Tremolo WaveForm + case 0x70: chn.nTremoloType = param & 0x07; break; + // E8x: Set 4-bit Panning + case 0x80: + if(m_SongFlags[SONG_FIRSTTICK]) + { + Panning(chn, param, Pan4bit); + } + break; + // E9x: Retrig + case 0x90: RetrigNote(nChn, param); break; + // EAx: Fine Volume Up + case 0xA0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeUp(chn, param, false); break; + // EBx: Fine Volume Down + case 0xB0: if ((param) || (GetType() & (MOD_TYPE_XM|MOD_TYPE_MT2))) FineVolumeDown(chn, param, false); break; + // ECx: Note Cut + case 0xC0: NoteCut(nChn, param, false); break; + // EDx: Note Delay + // EEx: Pattern Delay + case 0xF0: + if(GetType() == MOD_TYPE_MOD) // MOD: Invert Loop + { + chn.nEFxSpeed = param; + if(m_SongFlags[SONG_FIRSTTICK]) InvertLoop(chn); + } else // XM: Set Active Midi Macro + { + chn.nActiveMacro = param; + } + break; + } +} + + +void CSoundFile::ExtendedS3MCommands(CHANNELINDEX nChn, ModCommand::PARAM param) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + uint8 command = param & 0xF0; + param &= 0x0F; + switch(command) + { + // S0x: Set Filter + // S1x: Set Glissando Control + case 0x10: chn.dwFlags.set(CHN_GLISSANDO, param != 0); break; + // S2x: Set FineTune + case 0x20: if(!m_SongFlags[SONG_FIRSTTICK]) + break; + if(chn.HasCustomTuning()) + { + chn.nFineTune = param - 8; + chn.m_CalculateFreq = true; + } else if(GetType() != MOD_TYPE_669) + { + chn.nC5Speed = S3MFineTuneTable[param]; + chn.nFineTune = MOD2XMFineTune(param); + if(chn.nPeriod) + chn.nPeriod = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); + } else if(chn.pModSample != nullptr) + { + chn.nC5Speed = chn.pModSample->nC5Speed + param * 80; + } + break; + // S3x: Set Vibrato Waveform + case 0x30: if(GetType() == MOD_TYPE_S3M) + { + chn.nVibratoType = param & 0x03; + } else + { + // IT compatibility: Ignore waveform types > 3 + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + chn.nVibratoType = (param < 0x04) ? param : 0; + else + chn.nVibratoType = param & 0x07; + } + break; + // S4x: Set Tremolo Waveform + case 0x40: if(GetType() == MOD_TYPE_S3M) + { + chn.nTremoloType = param & 0x03; + } else + { + // IT compatibility: Ignore waveform types > 3 + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + chn.nTremoloType = (param < 0x04) ? param : 0; + else + chn.nTremoloType = param & 0x07; + } + break; + // S5x: Set Panbrello Waveform + case 0x50: + // IT compatibility: Ignore waveform types > 3 + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + { + chn.nPanbrelloType = (param < 0x04) ? param : 0; + chn.nPanbrelloPos = 0; + } else + { + chn.nPanbrelloType = param & 0x07; + } + break; + // S6x: Pattern Delay for x frames + case 0x60: + if(m_SongFlags[SONG_FIRSTTICK] && m_PlayState.m_nTickCount == 0) + { + // Tick delays are added up. + // Scream Tracker 3 does actually not support this command. + // We'll use the same behaviour as for Impulse Tracker, as we can assume that + // most S3Ms that make use of this command were made with Impulse Tracker. + // MPT added this command to the XM format through the X6x effect, so we will use + // the same behaviour here as well. + // Test cases: PatternDelays.it, PatternDelays.s3m, PatternDelays.xm + m_PlayState.m_nFrameDelay += param; + } + break; + // S7x: Envelope Control / Instrument Control + case 0x70: if(!m_SongFlags[SONG_FIRSTTICK]) break; + switch(param) + { + case 0: + case 1: + case 2: + { + for (CHANNELINDEX i = m_nChannels; i < MAX_CHANNELS; i++) + { + ModChannel &bkChn = m_PlayState.Chn[i]; + if (bkChn.nMasterChn == nChn + 1) + { + if (param == 1) + { + KeyOff(bkChn); + if(bkChn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteOff(i); + } else if (param == 2) + { + bkChn.dwFlags.set(CHN_NOTEFADE); + if(bkChn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteOff(i); + } else + { + bkChn.dwFlags.set(CHN_NOTEFADE); + bkChn.nFadeOutVol = 0; + if(bkChn.dwFlags[CHN_ADLIB] && m_opl) + m_opl->NoteCut(i); + } +#ifndef NO_PLUGINS + const ModInstrument *pIns = bkChn.pModInstrument; + IMixPlugin *pPlugin; + if(pIns != nullptr && pIns->nMixPlug && (pPlugin = m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin) != nullptr) + { + pPlugin->MidiCommand(*pIns, bkChn.nNote + NOTE_MAX_SPECIAL, 0, nChn); + } +#endif // NO_PLUGINS + } + } + } + break; + default: // S73-S7E + chn.InstrumentControl(param, *this); + break; + } + break; + // S8x: Set 4-bit Panning + case 0x80: + if(m_SongFlags[SONG_FIRSTTICK]) + { + Panning(chn, param, Pan4bit); + } + break; + // S9x: Sound Control + case 0x90: ExtendedChannelEffect(chn, param); break; + // SAx: Set 64k Offset + case 0xA0: if(m_SongFlags[SONG_FIRSTTICK]) + { + chn.nOldHiOffset = static_cast<uint8>(param); + if (!m_playBehaviour[kITHighOffsetNoRetrig] && chn.rowCommand.IsNote()) + { + SmpLength pos = param << 16; + if (pos < chn.nLength) chn.position.SetInt(pos); + } + } + break; + // SBx: Pattern Loop + case 0xB0: + if(m_SongFlags[SONG_FIRSTTICK]) + PatternLoop(m_PlayState, chn, param & 0x0F); + break; + // SCx: Note Cut + case 0xC0: + if(param == 0) + { + //IT compatibility 22. SC0 == SC1 + if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + param = 1; + // ST3 doesn't cut notes with SC0 + else if(GetType() == MOD_TYPE_S3M) + return; + } + // S3M/IT compatibility: Note Cut really cuts notes and does not just mute them (so that following volume commands could restore the sample) + // Test case: scx.it + NoteCut(nChn, param, m_playBehaviour[kITSCxStopsSample] || GetType() == MOD_TYPE_S3M); + break; + // SDx: Note Delay + // SEx: Pattern Delay for x rows + // SFx: S3M: Not used, IT: Set Active Midi Macro + case 0xF0: + if(GetType() != MOD_TYPE_S3M) + { + chn.nActiveMacro = static_cast<uint8>(param); + } + break; + } +} + + +void CSoundFile::ExtendedChannelEffect(ModChannel &chn, uint32 param) +{ + // S9x and X9x commands (S3M/XM/IT only) + if(!m_SongFlags[SONG_FIRSTTICK]) return; + switch(param & 0x0F) + { + // S90: Surround Off + case 0x00: chn.dwFlags.reset(CHN_SURROUND); break; + // S91: Surround On + case 0x01: chn.dwFlags.set(CHN_SURROUND); chn.nPan = 128; break; + + //////////////////////////////////////////////////////////// + // ModPlug Extensions + // S98: Reverb Off + case 0x08: + chn.dwFlags.reset(CHN_REVERB); + chn.dwFlags.set(CHN_NOREVERB); + break; + // S99: Reverb On + case 0x09: + chn.dwFlags.reset(CHN_NOREVERB); + chn.dwFlags.set(CHN_REVERB); + break; + // S9A: 2-Channels surround mode + case 0x0A: + m_SongFlags.reset(SONG_SURROUNDPAN); + break; + // S9B: 4-Channels surround mode + case 0x0B: + m_SongFlags.set(SONG_SURROUNDPAN); + break; + // S9C: IT Filter Mode + case 0x0C: + m_SongFlags.reset(SONG_MPTFILTERMODE); + break; + // S9D: MPT Filter Mode + case 0x0D: + m_SongFlags.set(SONG_MPTFILTERMODE); + break; + // S9E: Go forward + case 0x0E: + chn.dwFlags.reset(CHN_PINGPONGFLAG); + break; + // S9F: Go backward (and set playback position to the end if sample just started) + case 0x0F: + if(chn.position.IsZero() && chn.nLength && (chn.rowCommand.IsNote() || !chn.dwFlags[CHN_LOOP])) + { + chn.position.Set(chn.nLength - 1, SamplePosition::fractMax); + } + chn.dwFlags.set(CHN_PINGPONGFLAG); + break; + } +} + + +void CSoundFile::InvertLoop(ModChannel &chn) +{ + // EFx implementation for MOD files (PT 1.1A and up: Invert Loop) + // This effect trashes samples. Thanks to 8bitbubsy for making this work. :) + if(GetType() != MOD_TYPE_MOD || chn.nEFxSpeed == 0) + return; + + ModSample *pModSample = const_cast<ModSample *>(chn.pModSample); + if(pModSample == nullptr || !pModSample->HasSampleData() || !pModSample->uFlags[CHN_LOOP | CHN_SUSTAINLOOP]) + return; + + chn.nEFxDelay += ModEFxTable[chn.nEFxSpeed & 0x0F]; + if(chn.nEFxDelay < 128) + return; + chn.nEFxDelay = 0; + + const SmpLength loopStart = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopStart : pModSample->nSustainStart; + const SmpLength loopEnd = pModSample->uFlags[CHN_LOOP] ? pModSample->nLoopEnd : pModSample->nSustainEnd; + + if(++chn.nEFxOffset >= loopEnd - loopStart) + chn.nEFxOffset = 0; + + // TRASH IT!!! (Yes, the sample!) + const uint8 bps = pModSample->GetBytesPerSample(); + uint8 *begin = mpt::byte_cast<uint8 *>(pModSample->sampleb()) + (loopStart + chn.nEFxOffset) * bps; + for(auto &sample : mpt::as_span(begin, bps)) + { + sample = ~sample; + } + pModSample->PrecomputeLoops(*this, false); +} + + +// Process a MIDI Macro. +// Parameters: +// playState: The playback state to operate on. +// nChn: Mod channel to apply macro on +// isSmooth: If true, internal macros are interpolated between two rows +// macro: MIDI Macro string to process +// param: Parameter for parametric macros (Zxx / \xx parameter) +// plugin: Plugin to send MIDI message to (if not specified but needed, it is autodetected) +void CSoundFile::ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro ¯o, uint8 param, PLUGINDEX plugin) +{ + playState.m_midiMacroScratchSpace.resize(macro.Length() + 1); + auto out = mpt::as_span(playState.m_midiMacroScratchSpace); + + ParseMIDIMacro(playState, nChn, isSmooth, macro, out, param, plugin); + + // Macro string has been parsed and translated, now send the message(s)... + uint32 outSize = static_cast<uint32>(out.size()); + uint32 sendPos = 0; + uint8 runningStatus = 0; + while(sendPos < out.size()) + { + uint32 sendLen = 0; + if(out[sendPos] == 0xF0) + { + // SysEx start + if((outSize - sendPos >= 4) && (out[sendPos + 1] == 0xF0 || out[sendPos + 1] == 0xF1)) + { + // Internal macro (normal (F0F0) or extended (F0F1)), 4 bytes long + sendLen = 4; + } else + { + // SysEx message, find end of message + for(uint32 i = sendPos + 1; i < outSize; i++) + { + if(out[i] == 0xF7) + { + // Found end of SysEx message + sendLen = i - sendPos + 1; + 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)) + { + // Missing status byte? Try inserting running status + if(runningStatus != 0) + { + sendPos--; + out[sendPos] = runningStatus; + } else + { + // No running status to re-use; skip this byte + sendPos++; + } + continue; + } else + { + // Other MIDI messages + sendLen = std::min(static_cast<uint32>(MIDIEvents::GetEventLength(out[sendPos])), outSize - sendPos); + } + + if(sendLen == 0) + break; + + if(out[sendPos] < 0xF0) + { + runningStatus = out[sendPos]; + } + const auto midiMsg = out.subspan(sendPos, sendLen); + SendMIDIData(playState, nChn, isSmooth, midiMsg, plugin); + sendPos += sendLen; + } +} + + +void CSoundFile::ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const char> macro, mpt::span<uint8> &out, uint8 param, PLUGINDEX plugin) const +{ + ModChannel &chn = playState.Chn[nChn]; + const ModInstrument *pIns = chn.pModInstrument; + + const uint8 lastZxxParam = chn.lastZxxParam; // always interpolate based on original value in case z appears multiple times in macro string + uint8 updateZxxParam = 0xFF; // avoid updating lastZxxParam immediately if macro contains both internal and external MIDI message + + bool firstNibble = true; + size_t outPos = 0; // output buffer position, which also equals the number of complete bytes + for(size_t pos = 0; pos < macro.size() && outPos < out.size(); pos++) + { + bool isNibble = false; // did we parse a nibble or a byte value? + uint8 data = 0; // data that has just been parsed + + // Parse next macro byte... See Impulse Tracker's MIDI.TXT for detailed information on each possible character. + if(macro[pos] >= '0' && macro[pos] <= '9') + { + isNibble = true; + data = static_cast<uint8>(macro[pos] - '0'); + } else if(macro[pos] >= 'A' && macro[pos] <= 'F') + { + isNibble = true; + data = static_cast<uint8>(macro[pos] - 'A' + 0x0A); + } else if(macro[pos] == 'c') + { + // MIDI channel + isNibble = true; + data = 0xFF; +#ifndef NO_PLUGINS + const PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); + if(plug > 0 && plug <= MAX_MIXPLUGINS) + { + auto midiPlug = dynamic_cast<const IMidiPlugin *>(m_MixPlugins[plug - 1u].pMixPlugin); + if(midiPlug) + data = midiPlug->GetMidiChannel(playState.Chn[nChn], nChn); + } +#endif // NO_PLUGINS + if(data == 0xFF) + { + // Fallback if no plugin was found + if(pIns) + data = pIns->GetMIDIChannel(playState.Chn[nChn], nChn); + else + data = 0; + } + } else if(macro[pos] == 'n') + { + // Last triggered note + if(ModCommand::IsNote(chn.nLastNote)) + { + data = chn.nLastNote - NOTE_MIN; + } + } else if(macro[pos] == 'v') + { + // 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); + data = static_cast<uint8>(Clamp(vol / 2, 1, 127)); + //data = (unsigned char)std::min((chn.nVolume * chn.nGlobalVol * 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); + data = static_cast<uint8>(Clamp(vol / 2, 1, 127)); + //data = (unsigned char)std::min((chn.nCalcVolume * chn.nGlobalVol * m_nGlobalVolume) >> (7 + 6 + 8), 127); + } else if(macro[pos] == 'x') + { + // Pan set + data = static_cast<uint8>(std::min(static_cast<int>(chn.nPan / 2), 127)); + } else if(macro[pos] == 'y') + { + // Calculated pan + data = static_cast<uint8>(std::min(static_cast<int>(chn.nRealPan / 2), 127)); + } else if(macro[pos] == 'a') + { + // High byte of bank select + if(pIns && pIns->wMidiBank) + { + data = static_cast<uint8>(((pIns->wMidiBank - 1) >> 7) & 0x7F); + } + } else if(macro[pos] == 'b') + { + // Low byte of bank select + if(pIns && pIns->wMidiBank) + { + data = static_cast<uint8>((pIns->wMidiBank - 1) & 0x7F); + } + } else if(macro[pos] == 'o') + { + // Offset (ignoring high offset) + data = static_cast<uint8>((chn.oldOffset >> 8) & 0xFF); + } else if(macro[pos] == 'h') + { + // Host channel number + data = static_cast<uint8>((nChn >= GetNumChannels() ? (chn.nMasterChn - 1) : nChn) & 0x7F); + } else if(macro[pos] == 'm') + { + // Loop direction (judging from the character, it was supposed to be loop type, though) + data = chn.dwFlags[CHN_PINGPONGFLAG] ? 1 : 0; + } else if(macro[pos] == 'p') + { + // Program select + if(pIns && pIns->nMidiProgram) + { + data = static_cast<uint8>((pIns->nMidiProgram - 1) & 0x7F); + } + } else if(macro[pos] == 'z') + { + // Zxx parameter + data = param; + if(isSmooth && chn.lastZxxParam < 0x80 + && (outPos < 3 || out[outPos - 3] != 0xF0 || out[outPos - 2] < 0xF0)) + { + // Interpolation for external MIDI messages - interpolation for internal messages + // is handled separately to allow for more than 7-bit granularity where it's possible + data = static_cast<uint8>(CalculateSmoothParamChange(playState, lastZxxParam, data)); + chn.lastZxxParam = data; + updateZxxParam = 0x80; + } else if(updateZxxParam == 0xFF) + { + updateZxxParam = data; + } + } else if(macro[pos] == 's') + { + // SysEx Checksum (not an original Impulse Tracker macro variable, but added for convenience) + auto startPos = outPos; + while(startPos > 0 && out[--startPos] != 0xF0); + if(outPos - startPos < 5 || out[startPos] != 0xF0) + { + continue; + } + for(auto p = startPos + 5u; p != outPos; p++) + { + data += out[p]; + } + data = (~data + 1) & 0x7F; + } else + { + // Unrecognized byte (e.g. space char) + continue; + } + + // Append parsed data + if(isNibble) // parsed a nibble (constant or 'c' variable) + { + if(firstNibble) + { + out[outPos] = data; + } else + { + out[outPos] = (out[outPos] << 4) | data; + outPos++; + } + firstNibble = !firstNibble; + } else // parsed a byte (variable) + { + if(!firstNibble) // From MIDI.TXT: '9n' is exactly the same as '09 n' or '9 n' -- so finish current byte first + { + outPos++; + } + out[outPos++] = data; + firstNibble = true; + } + } + if(!firstNibble) + { + // Finish current byte + outPos++; + } + if(updateZxxParam < 0x80) + chn.lastZxxParam = updateZxxParam; + + out = out.first(outPos); +} + + +// Calculate smooth MIDI macro slide parameter for current tick. +float CSoundFile::CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param) +{ + MPT_ASSERT(playState.TicksOnRow() > playState.m_nTickCount); + const uint32 ticksLeft = playState.TicksOnRow() - playState.m_nTickCount; + if(ticksLeft > 1) + { + // Slide param + const float step = (param - currentValue) / static_cast<float>(ticksLeft); + return (currentValue + step); + } else + { + // On last tick, set exact value. + return param; + } +} + + +// Process exactly one MIDI message parsed by ProcessMIDIMacro. Returns bytes sent on success, 0 on (parse) failure. +void CSoundFile::SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const uint8> macro, PLUGINDEX plugin) +{ + if(macro.size() < 1) + return; + + // Don't do anything that modifies state outside of the playState itself. + const bool localOnly = playState.m_midiMacroEvaluationResults.has_value(); + + if(macro[0] == 0xFA || macro[0] == 0xFC || macro[0] == 0xFF) + { + // Start Song, Stop Song, MIDI Reset - both interpreted internally and sent to plugins + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + playState.Chn[chn].nCutOff = 0x7F; + playState.Chn[chn].nResonance = 0x00; + } + } + + ModChannel &chn = playState.Chn[nChn]; + if(macro.size() == 4 && macro[0] == 0xF0 && (macro[1] == 0xF0 || macro[1] == 0xF1)) + { + // Internal device. + const bool isExtended = (macro[1] == 0xF1); + const uint8 macroCode = macro[2]; + const uint8 param = macro[3]; + + if(macroCode == 0x00 && !isExtended && param < 0x80) + { + // F0.F0.00.xx: Set CutOff + if(!isSmooth) + chn.nCutOff = param; + else + chn.nCutOff = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nCutOff, param)); + chn.nRestoreCutoffOnNewNote = 0; + int cutoff = SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); + + if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl && !localOnly) + { + // Cutoff doubles as modulator intensity for FM instruments + m_opl->Volume(nChn, static_cast<uint8>(cutoff / 4), true); + } + } else if(macroCode == 0x01 && !isExtended && param < 0x80) + { + // F0.F0.01.xx: Set Resonance + if(!isSmooth) + chn.nResonance = param; + else + chn.nResonance = mpt::saturate_round<uint8>(CalculateSmoothParamChange(playState, chn.nResonance, param)); + chn.nRestoreResonanceOnNewNote = 0; + SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); + } else if(macroCode == 0x02 && !isExtended) + { + // F0.F0.02.xx: Set filter mode (high nibble determines filter mode) + if(param < 0x20) + { + chn.nFilterMode = static_cast<FilterMode>(param >> 4); + SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER]); + } +#ifndef NO_PLUGINS + } else if(macroCode == 0x03 && !isExtended) + { + // F0.F0.03.xx: Set plug dry/wet + PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); + if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80) + { + plug--; + if(IMixPlugin* pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin) + { + const float newRatio = (127 - param) / 127.0f; + if(localOnly) + playState.m_midiMacroEvaluationResults->pluginDryWetRatio[plug] = newRatio; + else if(!isSmooth) + pPlugin->SetDryRatio(newRatio); + else + pPlugin->SetDryRatio(CalculateSmoothParamChange(playState, m_MixPlugins[plug].fDryRatio, newRatio)); + } + } + } else if((macroCode & 0x80) || isExtended) + { + // F0.F0.{80|n}.xx / F0.F1.n.xx: Set VST effect parameter n to xx + PLUGINDEX plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); + if(plug > 0 && plug <= MAX_MIXPLUGINS && param < 0x80) + { + plug--; + if(IMixPlugin *pPlugin = m_MixPlugins[plug].pMixPlugin; pPlugin) + { + const PlugParamIndex plugParam = isExtended ? (0x80 + macroCode) : (macroCode & 0x7F); + const PlugParamValue value = param / 127.0f; + if(localOnly) + playState.m_midiMacroEvaluationResults->pluginParameter[{plug, plugParam}] = value; + else if(!isSmooth) + pPlugin->SetParameter(plugParam, value); + else + pPlugin->SetParameter(plugParam, CalculateSmoothParamChange(playState, pPlugin->GetParameter(plugParam), value)); + } + } +#endif // NO_PLUGINS + } + } else if(!localOnly) + { +#ifndef NO_PLUGINS + // Not an internal device. Pass on to appropriate plugin. + const CHANNELINDEX plugChannel = (nChn < GetNumChannels()) ? nChn + 1 : chn.nMasterChn; + if(plugChannel > 0 && plugChannel <= GetNumChannels()) // XXX do we need this? I guess it might be relevant for previewing notes in the pattern... Or when using this mechanism for volume/panning! + { + PLUGINDEX plug = 0; + if(!chn.dwFlags[CHN_NOFX]) + { + plug = (plugin != 0) ? plugin : GetBestPlugin(playState, nChn, PrioritiseChannel, EvenIfMuted); + } + + if(plug > 0 && plug <= MAX_MIXPLUGINS) + { + if(IMixPlugin *pPlugin = m_MixPlugins[plug - 1].pMixPlugin; pPlugin != nullptr) + { + if(macro[0] == 0xF0) + { + pPlugin->MidiSysexSend(mpt::byte_cast<mpt::const_byte_span>(macro)); + } else + { + size_t len = std::min(static_cast<size_t>(MIDIEvents::GetEventLength(macro[0])), macro.size()); + uint32 curData = 0; + memcpy(&curData, macro.data(), len); + pPlugin->MidiSend(curData); + } + } + } + } +#else + MPT_UNREFERENCED_PARAMETER(plugin); +#endif // NO_PLUGINS + } +} + + +void CSoundFile::SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume) +{ +#ifndef NO_PLUGINS + auto &channel = m_PlayState.Chn[chn]; + const ModInstrument *pIns = channel.pModInstrument; + // instro sends to a midi chan + if (pIns && pIns->HasValidMIDIChannel()) + { + PLUGINDEX plug = pIns->nMixPlug; + if(plug > 0 && plug <= MAX_MIXPLUGINS) + { + IMixPlugin *pPlug = m_MixPlugins[plug - 1].pMixPlugin; + if (pPlug != nullptr) + { + pPlug->MidiCommand(*pIns, note, volume, chn); + if(note < NOTE_MIN_SPECIAL) + channel.nLeftVU = channel.nRightVU = 0xFF; + } + } + } +#endif // NO_PLUGINS +} + + +void CSoundFile::ProcessSampleOffset(ModChannel &chn, CHANNELINDEX nChn, const PlayState &playState) const +{ + const ModCommand &m = chn.rowCommand; + uint32 extendedRows = 0; + SmpLength offset = CalculateXParam(playState.m_nPattern, playState.m_nRow, nChn, &extendedRows), highOffset = 0; + if(!extendedRows) + { + // No X-param (normal behaviour) + const bool isPercentageOffset = (m.volcmd == VOLCMD_OFFSET && m.vol == 0); + offset <<= 8; + if(offset) + chn.oldOffset = offset; + else if(m.volcmd != VOLCMD_OFFSET) + offset = chn.oldOffset; + + if(!isPercentageOffset) + highOffset = static_cast<SmpLength>(chn.nOldHiOffset) << 16; + } + if(m.volcmd == VOLCMD_OFFSET) + { + if(m.vol == 0) + offset = Util::muldivr_unsigned(chn.nLength, offset, 256u << (8u * std::max(uint32(1), extendedRows))); // o00 + Oxx = Percentage Offset + else if(m.vol <= std::size(ModSample().cues) && chn.pModSample != nullptr) + offset += chn.pModSample->cues[m.vol - 1]; // Offset relative to cue point + chn.oldOffset = offset; + } + SampleOffset(chn, offset + highOffset); +} + + +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]) + chn.prevNoteOffset = 0; + + chn.prevNoteOffset += param; + + if(param >= chn.nLoopEnd && (GetType() & (MOD_TYPE_S3M | MOD_TYPE_MTM)) && chn.dwFlags[CHN_LOOP] && chn.nLoopEnd > 0) + { + // Offset wrap-around + // Note that ST3 only does this in GUS mode. SoundBlaster stops the sample entirely instead. + // Test case: OffsetLoopWraparound.s3m + param = (param - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart) + chn.nLoopStart; + } + + if(GetType() == MOD_TYPE_MDL && chn.dwFlags[CHN_16BIT]) + { + // Digitrakker really uses byte offsets, not sample offsets. WTF! + param /= 2u; + } + + if(chn.rowCommand.IsNote() || m_playBehaviour[kApplyOffsetWithoutNote]) + { + // IT compatibility: If this note is not mapped to a sample, ignore it. + // Test case: empty_sample_offset.it + if(chn.pModInstrument != nullptr && chn.rowCommand.IsNote()) + { + SAMPLEINDEX smp = chn.pModInstrument->Keyboard[chn.rowCommand.note - NOTE_MIN]; + if(smp == 0 || smp > GetNumSamples()) + return; + } + + if(m_SongFlags[SONG_PT_MODE]) + { + // ProTracker compatbility: PT1/2-style funky 9xx offset command + // Test case: ptoffset.mod + chn.position.Set(chn.prevNoteOffset); + chn.prevNoteOffset += param; + } else + { + chn.position.Set(param); + } + + if (chn.position.GetUInt() >= chn.nLength || (chn.dwFlags[CHN_LOOP] && chn.position.GetUInt() >= chn.nLoopEnd)) + { + // Offset beyond sample size + if(m_playBehaviour[kFT2ST3OffsetOutOfRange] || GetType() == MOD_TYPE_MTM) + { + // FT2 Compatibility: Don't play note if offset is beyond sample length + // ST3 Compatibility: Don't play note if offset is beyond sample length (non-looped samples only) + // Test cases: 3xx-no-old-samp.xm, OffsetPastSampleEnd.s3m + chn.dwFlags.set(CHN_FASTVOLRAMP); + chn.nPeriod = 0; + } else if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MOD))) + { + // IT Compatibility: Offset + if(m_playBehaviour[kITOffset]) + { + if(m_SongFlags[SONG_ITOLDEFFECTS]) + chn.position.Set(chn.nLength); // Old FX: Clip to end of sample + else + chn.position.Set(0); // Reset to beginning of sample + } else + { + chn.position.Set(chn.nLoopStart); + if(m_SongFlags[SONG_ITOLDEFFECTS] && chn.nLength > 4) + { + chn.position.Set(chn.nLength - 2); + } + } + } else if(GetType() == MOD_TYPE_MOD && chn.dwFlags[CHN_LOOP]) + { + chn.position.Set(chn.nLoopStart); + } + } + } else if ((param < chn.nLength) && (GetType() & (MOD_TYPE_MTM | MOD_TYPE_DMF | MOD_TYPE_MDL | MOD_TYPE_PLM))) + { + // Some trackers can also call offset effects without notes next to them... + chn.position.Set(param); + } +} + + +void CSoundFile::ReverseSampleOffset(ModChannel &chn, ModCommand::PARAM param) const +{ + if(chn.pModSample != nullptr && chn.pModSample->nLength > 0) + { + chn.dwFlags.set(CHN_PINGPONGFLAG); + chn.dwFlags.reset(CHN_LOOP); + chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length. + chn.position.Set((chn.nLength - 1) - std::min(SmpLength(param) << 8, chn.nLength - SmpLength(1)), 0); + } +} + + +void CSoundFile::DigiBoosterSampleReverse(ModChannel &chn, ModCommand::PARAM param) const +{ + if(chn.isFirstTick && chn.pModSample != nullptr && chn.pModSample->nLength > 0) + { + chn.dwFlags.set(CHN_PINGPONGFLAG); + chn.nLength = chn.pModSample->nLength; // If there was a loop, extend sample to whole length. + chn.position.Set(chn.nLength - 1, 0); + chn.dwFlags.set(CHN_LOOP | CHN_PINGPONGLOOP, param > 0); + if(param > 0) + { + chn.nLoopStart = 0; + chn.nLoopEnd = chn.nLength; + // TODO: When the sample starts playing in forward direction again, the loop should be updated to the normal sample loop. + } + } +} + + +void CSoundFile::HandleDigiSamplePlayDirection(PlayState &state, CHANNELINDEX chn) const +{ + // Digi Booster mixes two channels into one Paula channel, and when a note is triggered on one of them it resets the reverse play flag on the other. + if(GetType() == MOD_TYPE_DIGI) + { + state.Chn[chn].dwFlags.reset(CHN_PINGPONGFLAG); + const CHANNELINDEX otherChn = chn ^ 1; + if(otherChn < GetNumChannels()) + state.Chn[otherChn].dwFlags.reset(CHN_PINGPONGFLAG); + } +} + + +void CSoundFile::RetrigNote(CHANNELINDEX nChn, int param, int offset) +{ + // Retrig: bit 8 is set if it's the new XM retrig + ModChannel &chn = m_PlayState.Chn[nChn]; + int retrigSpeed = param & 0x0F; + uint8 retrigCount = chn.nRetrigCount; + bool doRetrig = false; + + // IT compatibility 15. Retrigger + if(m_playBehaviour[kITRetrigger]) + { + if(m_PlayState.m_nTickCount == 0 && chn.rowCommand.note) + { + chn.nRetrigCount = param & 0x0F; + } else if(!chn.nRetrigCount || !--chn.nRetrigCount) + { + chn.nRetrigCount = param & 0x0F; + doRetrig = true; + } + } else if(m_playBehaviour[kFT2Retrigger] && (param & 0x100)) + { + // Buggy-like-hell FT2 Rxy retrig! + // Test case: retrig.xm + if(m_SongFlags[SONG_FIRSTTICK]) + { + // Here are some really stupid things FT2 does on the first tick. + // Test case: RetrigTick0.xm + if(chn.rowCommand.instr > 0 && chn.rowCommand.IsNoteOrEmpty()) + retrigCount = 1; + if(chn.rowCommand.volcmd == VOLCMD_VOLUME && chn.rowCommand.vol != 0) + { + // I guess this condition simply checked if the volume byte was != 0 in FT2. + chn.nRetrigCount = retrigCount; + return; + } + } + if(retrigCount >= retrigSpeed) + { + if(!m_SongFlags[SONG_FIRSTTICK] || !chn.rowCommand.IsNote()) + { + doRetrig = true; + retrigCount = 0; + } + } + } else + { + // old routines + if (GetType() & (MOD_TYPE_S3M|MOD_TYPE_IT|MOD_TYPE_MPT)) + { + if(!retrigSpeed) + retrigSpeed = 1; + if(retrigCount && !(retrigCount % retrigSpeed)) + doRetrig = true; + retrigCount++; + } else if(GetType() == MOD_TYPE_MOD) + { + // ProTracker-style retrigger + // Test case: PTRetrigger.mod + const auto tick = m_PlayState.m_nTickCount % m_PlayState.m_nMusicSpeed; + if(!tick && chn.rowCommand.IsNote()) + return; + if(retrigSpeed && !(tick % retrigSpeed)) + doRetrig = true; + } else if(GetType() == MOD_TYPE_MTM) + { + // In MultiTracker, E9x retriggers the last note at exactly the x-th tick of the row + doRetrig = m_PlayState.m_nTickCount == static_cast<uint32>(param & 0x0F) && retrigSpeed != 0; + } else + { + int realspeed = retrigSpeed; + // FT2 bug: if a retrig (Rxy) occurs together with a volume command, the first retrig interval is increased by one tick + if((param & 0x100) && (chn.rowCommand.volcmd == VOLCMD_VOLUME) && (chn.rowCommand.param & 0xF0)) + realspeed++; + if(!m_SongFlags[SONG_FIRSTTICK] || (param & 0x100)) + { + if(!realspeed) + realspeed = 1; + if(!(param & 0x100) && m_PlayState.m_nMusicSpeed && !(m_PlayState.m_nTickCount % realspeed)) + doRetrig = true; + retrigCount++; + } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) + retrigCount = 0; + if (retrigCount >= realspeed) + { + if(m_PlayState.m_nTickCount || ((param & 0x100) && !chn.rowCommand.note)) + doRetrig = true; + } + if(m_playBehaviour[kFT2Retrigger] && param == 0) + { + // E90 = Retrig instantly, and only once + doRetrig = (m_PlayState.m_nTickCount == 0); + } + } + } + + // IT compatibility: If a sample is shorter than the retrig time (i.e. it stops before the retrig counter hits zero), it is not retriggered. + // Test case: retrig-short.it + if(chn.nLength == 0 && m_playBehaviour[kITShortSampleRetrig] && !chn.HasMIDIOutput()) + return; + // ST3 compatibility: No retrig after Note Cut + // Test case: RetrigAfterNoteCut.s3m + if(m_playBehaviour[kST3RetrigAfterNoteCut] && !chn.nFadeOutVol) + return; + + if(doRetrig) + { + uint32 dv = (param >> 4) & 0x0F; + int vol = chn.nVolume; + if(dv) + { + + // FT2 compatibility: Retrig + volume will not change volume of retrigged notes + if(!m_playBehaviour[kFT2Retrigger] || !(chn.rowCommand.volcmd == VOLCMD_VOLUME)) + { + if(retrigTable1[dv]) + vol = (vol * retrigTable1[dv]) / 16; + else + vol += ((int)retrigTable2[dv]) * 4; + } + Limit(vol, 0, 256); + + chn.dwFlags.set(CHN_FASTVOLRAMP); + } + uint32 note = chn.nNewNote; + int32 oldPeriod = chn.nPeriod; + // ST3 doesn't retrigger OPL notes + // Test case: RetrigSlide.s3m + const bool oplRealRetrig = chn.dwFlags[CHN_ADLIB] && m_playBehaviour[kOPLRealRetrig]; + if(note >= NOTE_MIN && note <= NOTE_MAX && chn.nLength && (GetType() != MOD_TYPE_S3M || oplRealRetrig)) + CheckNNA(nChn, 0, note, true); + bool resetEnv = false; + if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) + { + if(chn.rowCommand.instr && param < 0x100) + { + InstrumentChange(chn, chn.rowCommand.instr, false, false); + resetEnv = true; + } + if(param < 0x100) + resetEnv = true; + } + + 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) + // 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); + NoteChange(chn, note, itS3Mstyle, resetEnv, false, nChn); + if(!chn.rowCommand.instr) + chn.prevNoteOffset = oldPrevNoteOffset; + // XM compatibility: Prevent NoteChange from resetting the fade flag in case an instrument number + note-off is present. + // Test case: RetrigFade.xm + if(fading && GetType() == MOD_TYPE_XM) + chn.dwFlags.set(CHN_NOTEFADE); + chn.nVolume = vol; + if(m_nInstruments) + { + chn.rowCommand.note = static_cast<ModCommand::NOTE>(note); // No retrig without note... +#ifndef NO_PLUGINS + ProcessMidiOut(nChn); //Send retrig to Midi +#endif // NO_PLUGINS + } + if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && chn.rowCommand.note == NOTE_NONE && oldPeriod != 0) + chn.nPeriod = oldPeriod; + if(!(GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT))) + retrigCount = 0; + // IT compatibility: see previous IT compatibility comment =) + if(itS3Mstyle) + chn.position.Set(0); + + offset--; + if(chn.pModSample != nullptr && offset >= 0 && offset <= static_cast<int>(std::size(chn.pModSample->cues))) + { + if(offset == 0) + offset = chn.oldOffset; + else + offset = chn.oldOffset = chn.pModSample->cues[offset - 1]; + SampleOffset(chn, offset); + } + } + + // buggy-like-hell FT2 Rxy retrig! + if(m_playBehaviour[kFT2Retrigger] && (param & 0x100)) + retrigCount++; + + // Now we can also store the retrig value for IT... + if(!m_playBehaviour[kITRetrigger]) + chn.nRetrigCount = retrigCount; +} + + +// Execute a frequency slide on given channel. +// Positive amounts increase the frequency, negative amounts decrease it. +// The period or frequency that is read and written is in the period variable, chn.nPeriod is not touched. +void CSoundFile::DoFreqSlide(ModChannel &chn, int32 &period, int32 amount, bool isTonePorta) const +{ + if(!period || !amount) + return; + MPT_ASSERT(!chn.HasCustomTuning()); + + if(GetType() == MOD_TYPE_669) + { + // Like other oldskool trackers, Composer 669 doesn't have linear slides... + // But the slides are done in Hertz rather than periods, meaning that they + // are more effective in the lower notes (rather than the higher notes). + period += amount * 20; + } else if(GetType() == MOD_TYPE_FAR) + { + period += (amount * 36318 / 1024); + } else if(m_SongFlags[SONG_LINEARSLIDES] && GetType() != MOD_TYPE_XM) + { + // IT Linear slides + const auto oldPeriod = period; + uint32 n = std::abs(amount); + LimitMax(n, 255u * 4u); + + // Note: IT ignores the lower 2 bits when abs(mount) > 16 (it either uses the fine *or* the regular table, not both) + // This means that vibratos are slightly less accurate in this range than they could be. + // Other code paths will *either* have an amount that's a multiple of 4 *or* it's less than 16. + if(amount > 0) + { + if(n < 16) + period = Util::muldivr(period, GetFineLinearSlideUpTable(this, n), 65536); + else + period = Util::muldivr(period, GetLinearSlideUpTable(this, n / 4u), 65536); + } else + { + if(n < 16) + period = Util::muldivr(period, GetFineLinearSlideDownTable(this, n), 65536); + else + period = Util::muldivr(period, GetLinearSlideDownTable(this, n / 4u), 65536); + } + + if(period == oldPeriod) + { + const bool incPeriod = m_playBehaviour[kPeriodsAreHertz] == (amount > 0); + if(incPeriod && period < Util::MaxValueOfType(period)) + period++; + else if(!incPeriod && period > 1) + period--; + } + } else if(!m_SongFlags[SONG_LINEARSLIDES] && m_playBehaviour[kPeriodsAreHertz]) + { + // IT Amiga slides + if(amount < 0) + { + // Go down + period = mpt::saturate_cast<int32>(Util::mul32to64_unsigned(1712 * 8363, period) / (Util::mul32to64_unsigned(period, -amount) + 1712 * 8363)); + } else if(amount > 0) + { + // Go up + const auto periodDiv = 1712 * 8363 - Util::mul32to64(period, amount); + if(periodDiv <= 0) + { + if(isTonePorta) + { + period = int32_max; + return; + } else + { + period = 0; + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + } + return; + } + period = mpt::saturate_cast<int32>(Util::mul32to64_unsigned(1712 * 8363, period) / periodDiv); + } + } else + { + period -= amount; + } + if(period < 1) + { + period = 1; + if(GetType() == MOD_TYPE_S3M && !isTonePorta) + { + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_FASTVOLRAMP); + } + } +} + + +void CSoundFile::NoteCut(CHANNELINDEX nChn, uint32 nTick, bool cutSample) +{ + if (m_PlayState.m_nTickCount == nTick) + { + ModChannel &chn = m_PlayState.Chn[nChn]; + if(cutSample) + { + chn.increment.Set(0); + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE); + } else + { + chn.nVolume = 0; + } + chn.dwFlags.set(CHN_FASTVOLRAMP); + + // instro sends to a midi chan + SendMIDINote(nChn, /*chn.nNote+*/NOTE_MAX_SPECIAL, 0); + + if(chn.dwFlags[CHN_ADLIB] && m_opl) + { + m_opl->NoteCut(nChn, false); + } + } +} + + +void CSoundFile::KeyOff(ModChannel &chn) const +{ + const bool keyIsOn = !chn.dwFlags[CHN_KEYOFF]; + chn.dwFlags.set(CHN_KEYOFF); + if(chn.pModInstrument != nullptr && !chn.VolEnv.flags[ENV_ENABLED]) + { + chn.dwFlags.set(CHN_NOTEFADE); + } + if (!chn.nLength) return; + if (chn.dwFlags[CHN_SUSTAINLOOP] && chn.pModSample && keyIsOn) + { + const ModSample *pSmp = chn.pModSample; + if(pSmp->uFlags[CHN_LOOP]) + { + if (pSmp->uFlags[CHN_PINGPONGLOOP]) + chn.dwFlags.set(CHN_PINGPONGLOOP); + else + chn.dwFlags.reset(CHN_PINGPONGLOOP | CHN_PINGPONGFLAG); + chn.dwFlags.set(CHN_LOOP); + chn.nLength = pSmp->nLength; + chn.nLoopStart = pSmp->nLoopStart; + chn.nLoopEnd = pSmp->nLoopEnd; + if (chn.nLength > chn.nLoopEnd) chn.nLength = chn.nLoopEnd; + if(chn.position.GetUInt() > chn.nLength) + { + // Test case: SusAfterLoop.it + chn.position.Set(chn.nLoopStart + ((chn.position.GetInt() - chn.nLoopStart) % (chn.nLoopEnd - chn.nLoopStart))); + } + } else + { + chn.dwFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_PINGPONGFLAG); + chn.nLength = pSmp->nLength; + } + } + + if (chn.pModInstrument) + { + const ModInstrument *pIns = chn.pModInstrument; + if((pIns->VolEnv.dwFlags[ENV_LOOP] || (GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MDL))) && pIns->nFadeOut != 0) + { + chn.dwFlags.set(CHN_NOTEFADE); + } + + if (pIns->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET && chn.VolEnv.nEnvValueAtReleaseJump == NOT_YET_RELEASED) + { + chn.VolEnv.nEnvValueAtReleaseJump = mpt::saturate_cast<int16>(pIns->VolEnv.GetValueFromPosition(chn.VolEnv.nEnvPosition, 256)); + chn.VolEnv.nEnvPosition = pIns->VolEnv[pIns->VolEnv.nReleaseNode].tick; + } + } +} + + +////////////////////////////////////////////////////////// +// CSoundFile: Global Effects + + +void CSoundFile::SetSpeed(PlayState &playState, uint32 param) const +{ +#ifdef MODPLUG_TRACKER + // FT2 appears to be decrementing the tick count before checking for zero, + // so it effectively counts down 65536 ticks with speed = 0 (song speed is a 16-bit variable in FT2) + if(GetType() == MOD_TYPE_XM && !param) + { + playState.m_nMusicSpeed = uint16_max; + } +#endif // MODPLUG_TRACKER + if(param > 0) playState.m_nMusicSpeed = param; + if(GetType() == MOD_TYPE_STM && param > 0) + { + playState.m_nMusicSpeed = std::max(param >> 4, uint32(1)); + playState.m_nMusicTempo = ConvertST2Tempo(static_cast<uint8>(param)); + } +} + + +// Convert a ST2 tempo byte to classic tempo and speed combination +TEMPO CSoundFile::ConvertST2Tempo(uint8 tempo) +{ + static constexpr uint8 ST2TempoFactor[] = { 140, 50, 25, 15, 10, 7, 6, 4, 3, 3, 2, 2, 2, 2, 1, 1 }; + static constexpr uint32 st2MixingRate = 23863; // Highest possible setting in ST2 + + // This underflows at tempo 06...0F, and the resulting tick lengths depend on the mixing rate. + // Note: ST2.3 uses the constant 50 below, earlier versions use 49 but they also play samples at a different speed. + int32 samplesPerTick = st2MixingRate / (50 - ((ST2TempoFactor[tempo >> 4u] * (tempo & 0x0F)) >> 4u)); + if(samplesPerTick <= 0) + samplesPerTick += 65536; + return TEMPO().SetRaw(Util::muldivrfloor(st2MixingRate, 5 * TEMPO::fractFact, samplesPerTick * 2)); +} + + +void CSoundFile::SetTempo(TEMPO param, bool setFromUI) +{ + const CModSpecifications &specs = GetModSpecifications(); + + // Anything lower than the minimum tempo is considered to be a tempo slide + const TEMPO minTempo = (GetType() & (MOD_TYPE_MDL | MOD_TYPE_MED | MOD_TYPE_MOD)) ? TEMPO(1, 0) : TEMPO(32, 0); + + if(setFromUI) + { + // Set tempo from UI - ignore slide commands and such. + m_PlayState.m_nMusicTempo = Clamp(param, specs.GetTempoMin(), specs.GetTempoMax()); + } else if(param >= minTempo && m_SongFlags[SONG_FIRSTTICK] == !m_playBehaviour[kMODTempoOnSecondTick]) + { + // ProTracker sets the tempo after the first tick. + // Note: The case of one tick per row is handled in ProcessRow() instead. + // Test case: TempoChange.mod + m_PlayState.m_nMusicTempo = std::min(param, specs.GetTempoMax()); + } else if(param < minTempo && !m_SongFlags[SONG_FIRSTTICK]) + { + // Tempo Slide + TEMPO tempDiff(param.GetInt() & 0x0F, 0); + if((param.GetInt() & 0xF0) == 0x10) + m_PlayState.m_nMusicTempo += tempDiff; + else + m_PlayState.m_nMusicTempo -= tempDiff; + + TEMPO tempoMin = specs.GetTempoMin(), tempoMax = specs.GetTempoMax(); + if(m_playBehaviour[kTempoClamp]) // clamp tempo correctly in compatible mode + { + tempoMax.Set(255); + } + Limit(m_PlayState.m_nMusicTempo, tempoMin, tempoMax); + } +} + + +void CSoundFile::PatternLoop(PlayState &state, ModChannel &chn, ModCommand::PARAM param) const +{ + if(m_playBehaviour[kST3NoMutedChannels] && chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) + return; // not even effects are processed on muted S3M channels + + if(!param) + { + // Loop Start + chn.nPatternLoop = state.m_nRow; + return; + } + + // Loop Repeat + if(chn.nPatternLoopCount) + { + // There's a loop left + chn.nPatternLoopCount--; + if(!chn.nPatternLoopCount) + { + // IT compatibility 10. Pattern loops (+ same fix for S3M files) + // When finishing a pattern loop, the next loop without a dedicated SB0 starts on the first row after the previous loop. + if(m_playBehaviour[kITPatternLoopTargetReset] || (GetType() == MOD_TYPE_S3M)) + chn.nPatternLoop = state.m_nRow + 1; + + return; + } + } else + { + // First time we get into the loop => Set loop count. + + // IT compatibility 10. Pattern loops (+ same fix for XM / MOD / S3M files) + if(!m_playBehaviour[kITFT2PatternLoop] && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M))) + { + auto p = std::cbegin(state.Chn); + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++, p++) + { + // Loop on other channel + if(p != &chn && p->nPatternLoopCount) + return; + } + } + chn.nPatternLoopCount = param; + } + state.m_nextPatStartRow = chn.nPatternLoop; // Nasty FT2 E60 bug emulation! + + const auto loopTarget = chn.nPatternLoop; + if(loopTarget != ROWINDEX_INVALID) + { + // FT2 compatibility: E6x overwrites jump targets of Dxx effects that are located left of the E6x effect. + // Test cases: PatLoop-Jumps.xm, PatLoop-Various.xm + if(state.m_breakRow != ROWINDEX_INVALID && m_playBehaviour[kFT2PatternLoopWithJumps]) + state.m_breakRow = loopTarget; + + state.m_patLoopRow = loopTarget; + // IT compatibility: SBx is prioritized over Position Jump (Bxx) effects that are located left of the SBx effect. + // Test case: sbx-priority.it, LoopBreak.it + if(m_playBehaviour[kITPatternLoopWithJumps]) + state.m_posJump = ORDERINDEX_INVALID; + } + + if(GetType() == MOD_TYPE_S3M) + { + // ST3 doesn't have per-channel pattern loop memory, so spam all changes to other channels as well. + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) + { + state.Chn[i].nPatternLoop = chn.nPatternLoop; + state.Chn[i].nPatternLoopCount = chn.nPatternLoopCount; + } + } + +} + + +void CSoundFile::GlobalVolSlide(ModCommand::PARAM param, uint8 &nOldGlobalVolSlide) +{ + int32 nGlbSlide = 0; + if (param) nOldGlobalVolSlide = param; else param = nOldGlobalVolSlide; + + if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) + { + // XM nibble priority + if((param & 0xF0) != 0) + { + param &= 0xF0; + } else + { + param &= 0x0F; + } + } + + if (((param & 0x0F) == 0x0F) && (param & 0xF0)) + { + if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = (param >> 4) * 2; + } else + if (((param & 0xF0) == 0xF0) && (param & 0x0F)) + { + if(m_SongFlags[SONG_FIRSTTICK]) nGlbSlide = - (int)((param & 0x0F) * 2); + } else + { + if(!m_SongFlags[SONG_FIRSTTICK]) + { + if (param & 0xF0) + { + // IT compatibility: Ignore slide commands with both nibbles set. + if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM)) || (param & 0x0F) == 0) + nGlbSlide = (int)((param & 0xF0) >> 4) * 2; + } else + { + nGlbSlide = -(int)((param & 0x0F) * 2); + } + } + } + if (nGlbSlide) + { + if(!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_IMF | MOD_TYPE_J2B | MOD_TYPE_MID | MOD_TYPE_AMS | MOD_TYPE_DBM))) nGlbSlide *= 2; + nGlbSlide += m_PlayState.m_nGlobalVolume; + Limit(nGlbSlide, 0, 256); + m_PlayState.m_nGlobalVolume = nGlbSlide; + } +} + + +////////////////////////////////////////////////////// +// Note/Period/Frequency functions + +// Find lowest note which has same or lower period as a given period (i.e. the note has the same or higher frequency) +uint32 CSoundFile::GetNoteFromPeriod(uint32 period, int32 nFineTune, uint32 nC5Speed) const +{ + if(!period) return 0; + if(m_playBehaviour[kFT2Periods]) + { + // FT2's "RelocateTon" function actually rounds up and down, while GetNoteFromPeriod normally just truncates. + nFineTune += 64; + } + // This essentially implements std::lower_bound, with the difference that we don't need an iterable container. + uint32 minNote = NOTE_MIN, maxNote = NOTE_MAX, count = maxNote - minNote + 1; + const bool periodIsFreq = PeriodsAreFrequencies(); + while(count > 0) + { + const uint32 step = count / 2, midNote = minNote + step; + uint32 n = GetPeriodFromNote(midNote, nFineTune, nC5Speed); + if((n > period && !periodIsFreq) || (n < period && periodIsFreq) || !n) + { + minNote = midNote + 1; + count -= step + 1; + } else + { + count = step; + } + } + return minNote; +} + + +uint32 CSoundFile::GetPeriodFromNote(uint32 note, int32 nFineTune, uint32 nC5Speed) const +{ + if (note == NOTE_NONE || (note >= NOTE_MIN_SPECIAL)) return 0; + note -= NOTE_MIN; + if(!UseFinetuneAndTranspose()) + { + if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM)) + { + // MDL uses non-linear slides, but their effectiveness does not depend on the middle-C frequency. + return (FreqS3MTable[note % 12u] << 4) >> (note / 12); + } + if(!nC5Speed) + nC5Speed = 8363; + if(PeriodsAreFrequencies()) + { + // Compute everything in Hertz rather than periods. + uint32 freq = Util::muldiv_unsigned(nC5Speed, LinearSlideUpTable[(note % 12u) * 16u] << (note / 12u), 65536 << 5); + LimitMax(freq, static_cast<uint32>(int32_max)); + return freq; + } else if(m_SongFlags[SONG_LINEARSLIDES]) + { + return (FreqS3MTable[note % 12u] << 5) >> (note / 12); + } else + { + LimitMax(nC5Speed, uint32_max >> (note / 12u)); + //(a*b)/c + return Util::muldiv_unsigned(8363, (FreqS3MTable[note % 12u] << 5), nC5Speed << (note / 12u)); + //8363 * freq[note%12] / nC5Speed * 2^(5-note/12) + } + } else if(GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM)) + { + if (note < 12) note = 12; + note -= 12; + + if(GetType() == MOD_TYPE_MTM) + { + nFineTune *= 16; + } else if(m_playBehaviour[kFT2FinetunePrecision]) + { + // FT2 Compatibility: The lower three bits of the finetune are truncated. + // Test case: Finetune-Precision.xm + nFineTune &= ~7; + } + + if(m_SongFlags[SONG_LINEARSLIDES]) + { + int l = ((NOTE_MAX - note) << 6) - (nFineTune / 2); + if (l < 1) l = 1; + return static_cast<uint32>(l); + } else + { + int finetune = nFineTune; + uint32 rnote = (note % 12) << 3; + uint32 roct = note / 12; + int rfine = finetune / 16; + int i = rnote + rfine + 8; + Limit(i , 0, 103); + uint32 per1 = XMPeriodTable[i]; + if(finetune < 0) + { + rfine--; + finetune = -finetune; + } else rfine++; + i = rnote+rfine+8; + if (i < 0) i = 0; + if (i >= 104) i = 103; + uint32 per2 = XMPeriodTable[i]; + rfine = finetune & 0x0F; + per1 *= 16-rfine; + per2 *= rfine; + return ((per1 + per2) << 1) >> roct; + } + } else + { + nFineTune = XM2MODFineTune(nFineTune); + if ((nFineTune) || (note < 24) || (note >= 24 + std::size(ProTrackerPeriodTable))) + return (ProTrackerTunedPeriods[nFineTune * 12u + note % 12u] << 5) >> (note / 12u); + else + return (ProTrackerPeriodTable[note - 24] << 2); + } +} + + +// Converts period value to sample frequency. Return value is fixed point, with FREQ_FRACBITS fractional bits. +uint32 CSoundFile::GetFreqFromPeriod(uint32 period, uint32 c5speed, int32 nPeriodFrac) const +{ + if (!period) return 0; + if (GetType() & (MOD_TYPE_XM | MOD_TYPE_MTM)) + { + if(m_playBehaviour[kFT2Periods]) + { + // FT2 compatibility: Period is a 16-bit value in FT2, and it overflows happily. + // Test case: FreqWraparound.xm + period &= 0xFFFF; + } + if(m_SongFlags[SONG_LINEARSLIDES]) + { + uint32 octave; + if(m_playBehaviour[kFT2Periods]) + { + // Under normal circumstances, this calculation returns the same values as the non-compatible one. + // However, once the 12 octaves are exceeded (through portamento slides), the octave shift goes + // crazy in FT2, meaning that the frequency wraps around randomly... + // The entries in FT2's conversion table are four times as big, hence we have to do an additional shift by two bits. + // Test case: FreqWraparound.xm + // 12 octaves * (12 * 64) LUT entries = 9216, add 767 for rounding + uint32 div = ((9216u + 767u - period) / 768); + octave = ((14 - div) & 0x1F); + } else + { + octave = (period / 768) + 2; + } + return (XMLinearTable[period % 768] << (FREQ_FRACBITS + 2)) >> octave; + } else + { + if(!period) period = 1; + return ((8363 * 1712L) << FREQ_FRACBITS) / period; + } + } else if(UseFinetuneAndTranspose()) + { + return ((3546895L * 4) << FREQ_FRACBITS) / period; + } else if(GetType() == MOD_TYPE_669) + { + // We only really use c5speed for the finetune pattern command. All samples in 669 files have the same middle-C speed (imported as 8363 Hz). + return (period + c5speed - 8363) << FREQ_FRACBITS; + } else if(GetType() & (MOD_TYPE_MDL | MOD_TYPE_DTM)) + { + LimitMax(period, Util::MaxValueOfType(period) >> 8); + if (!c5speed) c5speed = 8363; + return Util::muldiv_unsigned(c5speed, (1712L << 7) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); + } else + { + LimitMax(period, Util::MaxValueOfType(period) >> 8); + if(PeriodsAreFrequencies()) + { + // Input is already a frequency in Hertz, not a period. + static_assert(FREQ_FRACBITS <= 8, "Check this shift operator"); + return uint32(((uint64(period) << 8) + nPeriodFrac) >> (8 - FREQ_FRACBITS)); + } else if(m_SongFlags[SONG_LINEARSLIDES]) + { + if(!c5speed) + c5speed = 8363; + return Util::muldiv_unsigned(c5speed, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); + } else + { + return Util::muldiv_unsigned(8363, (1712L << 8) << FREQ_FRACBITS, (period << 8) + nPeriodFrac); + } + } +} + + +PLUGINDEX CSoundFile::GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const +{ + if (nChn >= MAX_CHANNELS) //Check valid channel number + { + return 0; + } + + //Define search source order + PLUGINDEX plugin = 0; + switch (priority) + { + case ChannelOnly: + plugin = GetChannelPlugin(playState, nChn, respectMutes); + break; + case InstrumentOnly: + plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); + break; + case PrioritiseInstrument: + plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); + if(!plugin || plugin > MAX_MIXPLUGINS) + { + plugin = GetChannelPlugin(playState, nChn, respectMutes); + } + break; + case PrioritiseChannel: + plugin = GetChannelPlugin(playState, nChn, respectMutes); + if(!plugin || plugin > MAX_MIXPLUGINS) + { + plugin = GetActiveInstrumentPlugin(playState.Chn[nChn], respectMutes); + } + break; + } + + return plugin; // 0 Means no plugin found. +} + + +PLUGINDEX CSoundFile::GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const +{ + const ModChannel &channel = playState.Chn[nChn]; + + PLUGINDEX plugin; + if((respectMutes == RespectMutes && channel.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) || channel.dwFlags[CHN_NOFX]) + { + plugin = 0; + } else + { + // If it looks like this is an NNA channel, we need to find the master channel. + // This ensures we pick up the right ChnSettings. + if(channel.nMasterChn > 0) + { + nChn = channel.nMasterChn - 1; + } + + if(nChn < MAX_BASECHANNELS) + { + plugin = ChnSettings[nChn].nMixPlugin; + } else + { + plugin = 0; + } + } + return plugin; +} + + +PLUGINDEX CSoundFile::GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes) +{ + // Unlike channel settings, pModInstrument is copied from the original chan to the NNA chan, + // so we don't need to worry about finding the master chan. + + PLUGINDEX plug = 0; + if(chn.pModInstrument != nullptr) + { + // TODO this looks fishy. Shouldn't it check the mute status of the instrument itself?! + if(respectMutes == RespectMutes && chn.pModSample && chn.pModSample->uFlags[CHN_MUTE]) + { + plug = 0; + } else + { + plug = chn.pModInstrument->nMixPlug; + } + } + return plug; +} + + +// Retrieve the plugin that is associated with the channel's current instrument. +// No plugin is returned if the channel is muted or if the instrument doesn't have a MIDI channel set up, +// As this is meant to be used with instrument plugins. +IMixPlugin *CSoundFile::GetChannelInstrumentPlugin(const ModChannel &chn) const +{ +#ifndef NO_PLUGINS + if(chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) + { + // Don't process portamento on muted channels. Note that this might have a side-effect + // on other channels which trigger notes on the same MIDI channel of the same plugin, + // as those won't be pitch-bent anymore. + return nullptr; + } + + if(chn.HasMIDIOutput()) + { + const ModInstrument *pIns = chn.pModInstrument; + // Instrument sends to a MIDI channel + if(pIns->nMixPlug != 0 && pIns->nMixPlug <= MAX_MIXPLUGINS) + { + return m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin; + } + } +#else + MPT_UNREFERENCED_PARAMETER(chn); +#endif // NO_PLUGINS + return nullptr; +} + + +#ifdef MODPLUG_TRACKER +void CSoundFile::HandlePatternTransitionEvents() +{ + // MPT sequence override + if(m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID && m_PlayState.m_nSeqOverride < Order().size()) + { + if(m_SongFlags[SONG_PATTERNLOOP]) + { + m_PlayState.m_nPattern = Order()[m_PlayState.m_nSeqOverride]; + } + m_PlayState.m_nCurrentOrder = m_PlayState.m_nSeqOverride; + m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID; + } + + // Channel mutes + for (CHANNELINDEX chan = 0; chan < GetNumChannels(); chan++) + { + if (m_bChannelMuteTogglePending[chan]) + { + if(GetpModDoc()) + { + GetpModDoc()->MuteChannel(chan, !GetpModDoc()->IsChannelMuted(chan)); + } + m_bChannelMuteTogglePending[chan] = false; + } + } +} +#endif // MODPLUG_TRACKER + + +// Update time signatures (global or pattern-specific). Don't forget to call this when changing the RPB/RPM settings anywhere! +void CSoundFile::UpdateTimeSignature() +{ + if(!Patterns.IsValidIndex(m_PlayState.m_nPattern) || !Patterns[m_PlayState.m_nPattern].GetOverrideSignature()) + { + m_PlayState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat; + m_PlayState.m_nCurrentRowsPerMeasure = m_nDefaultRowsPerMeasure; + } else + { + m_PlayState.m_nCurrentRowsPerBeat = Patterns[m_PlayState.m_nPattern].GetRowsPerBeat(); + m_PlayState.m_nCurrentRowsPerMeasure = Patterns[m_PlayState.m_nPattern].GetRowsPerMeasure(); + } +} + + +void CSoundFile::PortamentoMPT(ModChannel &chn, int param) +{ + //Behavior: Modifies portamento by param-steps on every tick. + //Note that step meaning depends on tuning. + + chn.m_PortamentoFineSteps += param; + chn.m_CalculateFreq = true; +} + + +void CSoundFile::PortamentoFineMPT(ModChannel &chn, int param) +{ + //Behavior: Divides portamento change between ticks/row. For example + //if Ticks/row == 6, and param == +-6, portamento goes up/down by one tuning-dependent + //fine step every tick. + + if(m_PlayState.m_nTickCount == 0) + chn.nOldFinePortaUpDown = 0; + + const int tickParam = static_cast<int>((m_PlayState.m_nTickCount + 1.0) * param / m_PlayState.m_nMusicSpeed); + chn.m_PortamentoFineSteps += (param >= 0) ? tickParam - chn.nOldFinePortaUpDown : tickParam + chn.nOldFinePortaUpDown; + if(m_PlayState.m_nTickCount + 1 == m_PlayState.m_nMusicSpeed) + chn.nOldFinePortaUpDown = static_cast<int8>(std::abs(param)); + else + chn.nOldFinePortaUpDown = static_cast<int8>(std::abs(tickParam)); + + chn.m_CalculateFreq = true; +} + + +void CSoundFile::PortamentoExtraFineMPT(ModChannel &chn, int param) +{ + // This kinda behaves like regular fine portamento. + // It changes the pitch by n finetune steps on the first tick. + + if(chn.isFirstTick) + { + chn.m_PortamentoFineSteps += param; + chn.m_CalculateFreq = true; + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.cpp new file mode 100644 index 00000000..e3c87cd7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.cpp @@ -0,0 +1,2083 @@ +/* + * Sndfile.cpp + * ----------- + * Purpose: Core class of the playback engine. Every song is represented by a CSoundFile object. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/Mptrack.h" // For CTrackApp::OpenURL +#include "../mptrack/TrackerSettings.h" +#include "../mptrack/Moddoc.h" +#include "../mptrack/Reporting.h" +#include "../mptrack/Mainfrm.h" +#endif // MODPLUG_TRACKER +#ifdef MPT_EXTERNAL_SAMPLES +#include "../common/mptFileIO.h" +#endif // MPT_EXTERNAL_SAMPLES +#include "../common/version.h" +#include "../soundlib/AudioCriticalSection.h" +#include "../common/serialization_utils.h" +#include "Sndfile.h" +#include "Tables.h" +#include "mod_specifications.h" +#include "tuningcollection.h" +#include "plugins/PluginManager.h" +#include "plugins/PlugInterface.h" +#include "../common/mptStringBuffer.h" +#include "../common/FileReader.h" +#include "Container.h" +#include "OPL.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#ifndef NO_ARCHIVE_SUPPORT +#include "../unarchiver/unarchiver.h" +#endif // NO_ARCHIVE_SUPPORT + + +OPENMPT_NAMESPACE_BEGIN + + +bool SettingCacheCompleteFileBeforeLoading() +{ + #ifdef MODPLUG_TRACKER + return TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading; + #else + return false; + #endif +} + + +mpt::ustring FileHistory::AsISO8601() const +{ + tm date = loadDate; + if(openTime > 0) + { + // Calculate the date when editing finished. + double openSeconds = static_cast<double>(openTime) / HISTORY_TIMER_PRECISION; + tm tmpLoadDate = loadDate; + int64 loadDateSinceEpoch = mpt::Date::Unix::FromUTC(tmpLoadDate); + int64 saveDateSinceEpoch = loadDateSinceEpoch + mpt::saturate_round<int64>(openSeconds); + date = mpt::Date::Unix(saveDateSinceEpoch).AsUTC(); + } + return mpt::Date::ToShortenedISO8601(date); +} + + +CSoundFile::PlayState::PlayState() +{ + std::fill(std::begin(Chn), std::end(Chn), ModChannel{}); + m_midiMacroScratchSpace.reserve(kMacroLength); // Note: If macros ever become variable-length, the scratch space needs to be at least one byte longer than the longest macro in the file for end-of-SysEx insertion to stay allocation-free in the mixer! +} + + +////////////////////////////////////////////////////////// +// CSoundFile + +#ifdef MODPLUG_TRACKER +const NoteName *CSoundFile::m_NoteNames = NoteNamesFlat; +#endif + +CSoundFile::CSoundFile() : +#ifndef MODPLUG_TRACKER + m_NoteNames(NoteNamesSharp), +#endif + m_pModSpecs(&ModSpecs::itEx), + m_nType(MOD_TYPE_NONE), + Patterns(*this), +#ifdef MODPLUG_TRACKER + m_MIDIMapper(*this), +#endif + Order(*this), + m_PRNG(mpt::make_prng<mpt::fast_prng>(mpt::global_prng())), + m_visitedRows(*this) +{ + MemsetZero(MixSoundBuffer); + MemsetZero(MixRearBuffer); + MemsetZero(MixFloatBuffer); + +#ifdef MODPLUG_TRACKER + m_bChannelMuteTogglePending.reset(); + + m_nDefaultRowsPerBeat = m_PlayState.m_nCurrentRowsPerBeat = (TrackerSettings::Instance().m_nRowHighlightBeats) ? TrackerSettings::Instance().m_nRowHighlightBeats : 4; + m_nDefaultRowsPerMeasure = m_PlayState.m_nCurrentRowsPerMeasure = (TrackerSettings::Instance().m_nRowHighlightMeasures >= m_nDefaultRowsPerBeat) ? TrackerSettings::Instance().m_nRowHighlightMeasures : m_nDefaultRowsPerBeat * 4; +#else + m_nDefaultRowsPerBeat = m_PlayState.m_nCurrentRowsPerBeat = 4; + m_nDefaultRowsPerMeasure = m_PlayState.m_nCurrentRowsPerMeasure = 16; +#endif // MODPLUG_TRACKER + + MemsetZero(Instruments); + Clear(m_szNames); + + m_pTuningsTuneSpecific = new CTuningCollection(); +} + + +CSoundFile::~CSoundFile() +{ + Destroy(); + delete m_pTuningsTuneSpecific; + m_pTuningsTuneSpecific = nullptr; +} + + +void CSoundFile::AddToLog(LogLevel level, const mpt::ustring &text) const +{ + if(m_pCustomLog) + { + m_pCustomLog->AddToLog(level, text); + } else + { + #ifdef MODPLUG_TRACKER + if(GetpModDoc()) GetpModDoc()->AddToLog(level, text); + #else + MPT_LOG_GLOBAL(level, "soundlib", text); + #endif + } +} + + +// Global variable initializer for loader functions +void CSoundFile::InitializeGlobals(MODTYPE type) +{ + // Do not add or change any of these values! And if you do, review each and every loader to check if they require these defaults! + m_nType = type; + + MODTYPE bestType = GetBestSaveFormat(); + m_playBehaviour = GetDefaultPlaybackBehaviour(bestType); + SetModSpecsPointer(m_pModSpecs, bestType); + + // Delete instruments in case some previously called loader already created them. + for(INSTRUMENTINDEX i = 1; i <= m_nInstruments; i++) + { + delete Instruments[i]; + Instruments[i] = nullptr; + } + + m_ContainerType = MOD_CONTAINERTYPE_NONE; + m_nChannels = 0; + m_nInstruments = 0; + m_nSamples = 0; + m_nSamplePreAmp = 48; + m_nVSTiVolume = 48; + m_OPLVolumeFactor = m_OPLVolumeFactorScale; + m_nDefaultSpeed = 6; + m_nDefaultTempo.Set(125); + m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + m_SongFlags.reset(); + m_nMinPeriod = 16; + m_nMaxPeriod = 32767; + m_nResampling = SRCMODE_DEFAULT; + m_dwLastSavedWithVersion = Version(0); + m_dwCreatedWithVersion = Version(0); + + SetMixLevels(MixLevels::Compatible); + + Patterns.ClearPatterns(); + Order.Initialize(); + + m_songName.clear(); + m_songArtist.clear(); + m_songMessage.clear(); + m_modFormat = ModFormatDetails(); + m_FileHistory.clear(); + m_tempoSwing.clear(); +#ifdef MPT_EXTERNAL_SAMPLES + m_samplePaths.clear(); +#endif // MPT_EXTERNAL_SAMPLES + + // Note: we do not use the Amiga resampler for DBM as it's a multichannel format and can make use of higher-quality Amiga soundcards instead of Paula. + if(GetType() & (/*MOD_TYPE_DBM | */MOD_TYPE_DIGI | MOD_TYPE_MED | MOD_TYPE_MOD | MOD_TYPE_OKT | MOD_TYPE_SFX | MOD_TYPE_STP)) + { + m_SongFlags.set(SONG_ISAMIGA); + } +} + + +void CSoundFile::InitializeChannels() +{ + for(CHANNELINDEX nChn = 0; nChn < MAX_BASECHANNELS; nChn++) + { + InitChannel(nChn); + } +} + + +struct FileFormatLoader +{ + decltype(CSoundFile::ProbeFileHeaderXM) *prober; + decltype(&CSoundFile::ReadXM) loader; +}; + +#ifdef MODPLUG_TRACKER +#define MPT_DECLARE_FORMAT(format) { nullptr, &CSoundFile::Read ## format } +#else +#define MPT_DECLARE_FORMAT(format) { CSoundFile::ProbeFileHeader ## format, &CSoundFile::Read ## format } +#endif + +// All module format loaders, in the order they should be executed. +// This order matters, depending on the format, due to some unfortunate +// clashes or lack of magic bytes that can lead to mis-detection of some formats. +// Apart from that, more common formats with sane magic bytes are also found +// at the top of the list to match the most common cases more quickly. +static constexpr FileFormatLoader ModuleFormatLoaders[] = +{ + MPT_DECLARE_FORMAT(XM), + MPT_DECLARE_FORMAT(IT), + MPT_DECLARE_FORMAT(S3M), + MPT_DECLARE_FORMAT(STM), + MPT_DECLARE_FORMAT(MED), + MPT_DECLARE_FORMAT(MTM), + MPT_DECLARE_FORMAT(MDL), + MPT_DECLARE_FORMAT(DBM), + MPT_DECLARE_FORMAT(FAR), + MPT_DECLARE_FORMAT(AMS), + MPT_DECLARE_FORMAT(AMS2), + MPT_DECLARE_FORMAT(OKT), + MPT_DECLARE_FORMAT(PTM), + MPT_DECLARE_FORMAT(ULT), + MPT_DECLARE_FORMAT(DMF), + MPT_DECLARE_FORMAT(DSM), + MPT_DECLARE_FORMAT(AMF_Asylum), + MPT_DECLARE_FORMAT(AMF_DSMI), + MPT_DECLARE_FORMAT(PSM), + MPT_DECLARE_FORMAT(PSM16), + MPT_DECLARE_FORMAT(MT2), + MPT_DECLARE_FORMAT(ITP), +#if defined(MODPLUG_TRACKER) || defined(MPT_FUZZ_TRACKER) + // These make little sense for a module player library + MPT_DECLARE_FORMAT(UAX), + MPT_DECLARE_FORMAT(WAV), + MPT_DECLARE_FORMAT(MID), +#endif // MODPLUG_TRACKER || MPT_FUZZ_TRACKER + MPT_DECLARE_FORMAT(GDM), + MPT_DECLARE_FORMAT(IMF), + MPT_DECLARE_FORMAT(DIGI), + MPT_DECLARE_FORMAT(DTM), + MPT_DECLARE_FORMAT(PLM), + MPT_DECLARE_FORMAT(AM), + MPT_DECLARE_FORMAT(J2B), + MPT_DECLARE_FORMAT(PT36), + MPT_DECLARE_FORMAT(SymMOD), + MPT_DECLARE_FORMAT(MUS_KM), + MPT_DECLARE_FORMAT(FMT), + MPT_DECLARE_FORMAT(SFX), + MPT_DECLARE_FORMAT(STP), + MPT_DECLARE_FORMAT(DSym), + MPT_DECLARE_FORMAT(STX), + MPT_DECLARE_FORMAT(MOD), + MPT_DECLARE_FORMAT(ICE), + MPT_DECLARE_FORMAT(669), + MPT_DECLARE_FORMAT(C67), + MPT_DECLARE_FORMAT(MO3), + MPT_DECLARE_FORMAT(M15), +}; + +#undef MPT_DECLARE_FORMAT + + +CSoundFile::ProbeResult CSoundFile::ProbeAdditionalSize(MemoryFileReader &file, const uint64 *pfilesize, uint64 minimumAdditionalSize) +{ + const uint64 availableFileSize = file.GetLength(); + const uint64 fileSize = (pfilesize ? *pfilesize : file.GetLength()); + //const uint64 validFileSize = std::min(fileSize, static_cast<uint64>(ProbeRecommendedSize)); + const uint64 goalSize = file.GetPosition() + minimumAdditionalSize; + //const uint64 goalMinimumSize = std::min(goalSize, static_cast<uint64>(ProbeRecommendedSize)); + if(pfilesize) + { + if(availableFileSize < std::min(fileSize, static_cast<uint64>(ProbeRecommendedSize))) + { + if(availableFileSize < goalSize) + { + return ProbeWantMoreData; + } + } else + { + if(fileSize < goalSize) + { + return ProbeFailure; + } + } + return ProbeSuccess; + } + return ProbeSuccess; +} + + +#define MPT_DO_PROBE( storedResult , call ) \ + do { \ + ProbeResult lastResult = call ; \ + if(lastResult == ProbeSuccess) { \ + return ProbeSuccess; \ + } else if(lastResult == ProbeWantMoreData) { \ + storedResult = ProbeWantMoreData; \ + } \ + } while(0) \ +/**/ + + +CSoundFile::ProbeResult CSoundFile::Probe(ProbeFlags flags, mpt::span<const std::byte> data, const uint64 *pfilesize) +{ + ProbeResult result = ProbeFailure; + if(pfilesize && (*pfilesize < data.size())) + { + throw std::out_of_range(""); + } + if(!data.data()) + { + throw std::invalid_argument(""); + } + MemoryFileReader file(data); + if(flags & ProbeContainers) + { +#if !defined(MPT_WITH_ANCIENT) + MPT_DO_PROBE(result, ProbeFileHeaderMMCMP(file, pfilesize)); + MPT_DO_PROBE(result, ProbeFileHeaderPP20(file, pfilesize)); + MPT_DO_PROBE(result, ProbeFileHeaderXPK(file, pfilesize)); +#endif // !MPT_WITH_ANCIENT + MPT_DO_PROBE(result, ProbeFileHeaderUMX(file, pfilesize)); + } + if(flags & ProbeModules) + { + for(const auto &format : ModuleFormatLoaders) + { + if(format.prober != nullptr) + { + MPT_DO_PROBE(result, format.prober(file, pfilesize)); + } + } + } + if(pfilesize) + { + if((result == ProbeWantMoreData) && (mpt::saturate_cast<std::size_t>(*pfilesize) <= data.size())) + { + // If the prober wants more data but we already reached EOF, + // probing must fail. + result = ProbeFailure; + } + } else + { + if((result == ProbeWantMoreData) && (data.size() >= ProbeRecommendedSize)) + { + // If the prober wants more daat but we already provided the recommended required maximum, + // just return success as this is the best we can do for the suggestesd probing size. + result = ProbeSuccess; + } + } + return result; +} + + +bool CSoundFile::Create(FileReader file, ModLoadingFlags loadFlags, CModDoc *pModDoc) +{ + m_nMixChannels = 0; +#ifdef MODPLUG_TRACKER + m_pModDoc = pModDoc; +#else + MPT_UNUSED(pModDoc); + m_nFreqFactor = m_nTempoFactor = 65536; +#endif // MODPLUG_TRACKER + + Clear(m_szNames); +#ifndef NO_PLUGINS + std::fill(std::begin(m_MixPlugins), std::end(m_MixPlugins), SNDMIXPLUGIN()); +#endif // NO_PLUGINS + + if(CreateInternal(file, loadFlags)) + return true; + +#ifndef NO_ARCHIVE_SUPPORT + if(!(loadFlags & skipContainer) && file.IsValid()) + { + CUnarchiver unarchiver(file); + if(unarchiver.ExtractBestFile(GetSupportedExtensions(true))) + { + if(CreateInternal(unarchiver.GetOutputFile(), loadFlags)) + { + // Read archive comment if there is no song comment + if(m_songMessage.empty()) + { + m_songMessage.assign(mpt::ToCharset(mpt::Charset::Locale, unarchiver.GetComment())); + } + return true; + } + } + } +#endif + + return false; +} + + +bool CSoundFile::CreateInternal(FileReader file, ModLoadingFlags loadFlags) +{ + if(file.IsValid()) + { + std::vector<ContainerItem> containerItems; + MODCONTAINERTYPE packedContainerType = MOD_CONTAINERTYPE_NONE; + if(!(loadFlags & skipContainer)) + { + ContainerLoadingFlags containerLoadFlags = (loadFlags == onlyVerifyHeader) ? ContainerOnlyVerifyHeader : ContainerUnwrapData; +#if !defined(MPT_WITH_ANCIENT) + if(packedContainerType == MOD_CONTAINERTYPE_NONE && UnpackXPK(containerItems, file, containerLoadFlags)) packedContainerType = MOD_CONTAINERTYPE_XPK; + if(packedContainerType == MOD_CONTAINERTYPE_NONE && UnpackPP20(containerItems, file, containerLoadFlags)) packedContainerType = MOD_CONTAINERTYPE_PP20; + if(packedContainerType == MOD_CONTAINERTYPE_NONE && UnpackMMCMP(containerItems, file, containerLoadFlags)) packedContainerType = MOD_CONTAINERTYPE_MMCMP; +#endif // !MPT_WITH_ANCIENT + if(packedContainerType == MOD_CONTAINERTYPE_NONE && UnpackUMX(containerItems, file, containerLoadFlags)) packedContainerType = MOD_CONTAINERTYPE_UMX; + if(packedContainerType != MOD_CONTAINERTYPE_NONE) + { + if(loadFlags == onlyVerifyHeader) + { + return true; + } + if(!containerItems.empty()) + { + // cppcheck false-positive + // cppcheck-suppress containerOutOfBounds + file = containerItems[0].file; + } + } + } + + if(loadFlags & skipModules) + { + return false; + } + + // Try all module format loaders + bool loaderSuccess = false; + for(const auto &format : ModuleFormatLoaders) + { + loaderSuccess = (this->*(format.loader))(file, loadFlags); + if(loaderSuccess) + break; + } + + if(!loaderSuccess) + { + m_nType = MOD_TYPE_NONE; + m_ContainerType = MOD_CONTAINERTYPE_NONE; + } + if(loadFlags == onlyVerifyHeader) + { + return loaderSuccess; + } + + if(packedContainerType != MOD_CONTAINERTYPE_NONE && m_ContainerType == MOD_CONTAINERTYPE_NONE) + { + m_ContainerType = packedContainerType; + } + + m_visitedRows.Initialize(true); + } else + { + // New song + InitializeGlobals(); + m_visitedRows.Initialize(true); + m_dwCreatedWithVersion = Version::Current(); + } + + // Adjust channels + const auto muteFlag = GetChannelMuteFlag(); + for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++) + { + LimitMax(ChnSettings[chn].nVolume, uint16(64)); + if(ChnSettings[chn].nPan > 256) + ChnSettings[chn].nPan = 128; + if(ChnSettings[chn].nMixPlugin > MAX_MIXPLUGINS) + ChnSettings[chn].nMixPlugin = 0; + m_PlayState.Chn[chn].Reset(ModChannel::resetTotal, *this, chn, muteFlag); + } + + // Checking samples, load external samples + for(SAMPLEINDEX nSmp = 1; nSmp <= m_nSamples; nSmp++) + { + ModSample &sample = Samples[nSmp]; + +#ifdef MPT_EXTERNAL_SAMPLES + if(SampleHasPath(nSmp)) + { + mpt::PathString filename = GetSamplePath(nSmp); + if(file.GetOptionalFileName()) + { + filename = filename.RelativePathToAbsolute(file.GetOptionalFileName()->GetPath()); + } else if(GetpModDoc() != nullptr) + { + filename = filename.RelativePathToAbsolute(GetpModDoc()->GetPathNameMpt().GetPath()); + } + filename = filename.Simplify(); + if(!LoadExternalSample(nSmp, filename)) + { +#ifndef MODPLUG_TRACKER + // OpenMPT has its own way of reporting this error in CModDoc. + AddToLog(LogError, MPT_UFORMAT("Unable to load sample {}: {}")(i, filename.ToUnicode())); +#endif // MODPLUG_TRACKER + } + } else + { + sample.uFlags.reset(SMP_KEEPONDISK); + } +#endif // MPT_EXTERNAL_SAMPLES + + if(sample.HasSampleData()) + { + sample.PrecomputeLoops(*this, false); + } else if(!sample.uFlags[SMP_KEEPONDISK]) + { + sample.nLength = 0; + sample.nLoopStart = 0; + sample.nLoopEnd = 0; + sample.nSustainStart = 0; + sample.nSustainEnd = 0; + sample.uFlags.reset(CHN_LOOP | CHN_PINGPONGLOOP | CHN_SUSTAINLOOP | CHN_PINGPONGSUSTAIN); + } + if(sample.nGlobalVol > 64) sample.nGlobalVol = 64; + if(sample.uFlags[CHN_ADLIB] && m_opl == nullptr) InitOPL(); + } + // Check invalid instruments + INSTRUMENTINDEX maxInstr = 0; + for(INSTRUMENTINDEX i = 0; i <= m_nInstruments; i++) + { + if(Instruments[i] != nullptr) + { + maxInstr = i; + Instruments[i]->Sanitize(GetType()); + } + } + m_nInstruments = maxInstr; + + // Set default play state values + if(!m_nDefaultTempo.GetInt()) + m_nDefaultTempo.Set(125); + else + LimitMax(m_nDefaultTempo, TEMPO(uint16_max, 0)); + if(!m_nDefaultSpeed) + m_nDefaultSpeed = 6; + + if(m_nDefaultRowsPerMeasure < m_nDefaultRowsPerBeat) + m_nDefaultRowsPerMeasure = m_nDefaultRowsPerBeat; + LimitMax(m_nDefaultRowsPerBeat, MAX_ROWS_PER_BEAT); + LimitMax(m_nDefaultRowsPerMeasure, MAX_ROWS_PER_BEAT); + LimitMax(m_nDefaultGlobalVolume, MAX_GLOBAL_VOLUME); + if(!m_tempoSwing.empty()) + m_tempoSwing.resize(m_nDefaultRowsPerBeat); + + m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; + m_PlayState.m_nMusicTempo = m_nDefaultTempo; + m_PlayState.m_nCurrentRowsPerBeat = m_nDefaultRowsPerBeat; + m_PlayState.m_nCurrentRowsPerMeasure = m_nDefaultRowsPerMeasure; + m_PlayState.m_nGlobalVolume = static_cast<int32>(m_nDefaultGlobalVolume); + m_PlayState.ResetGlobalVolumeRamping(); + m_PlayState.m_nNextOrder = 0; + m_PlayState.m_nCurrentOrder = 0; + m_PlayState.m_nPattern = 0; + m_PlayState.m_nBufferCount = 0; + m_PlayState.m_dBufferDiff = 0; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_nNextRow = 0; + m_PlayState.m_nRow = 0; + m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nFrameDelay = 0; + m_PlayState.m_nextPatStartRow = 0; + m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID; + + if(UseFinetuneAndTranspose()) + m_playBehaviour.reset(kPeriodsAreHertz); + + m_nMaxOrderPosition = 0; + + RecalculateSamplesPerTick(); + + for(auto &order : Order) + { + order.Shrink(); + if(order.GetRestartPos() >= order.size()) + { + order.SetRestartPos(0); + } + } + + if(GetType() == MOD_TYPE_NONE) + { + return false; + } + + SetModSpecsPointer(m_pModSpecs, GetBestSaveFormat()); + + // When reading a file made with an older version of MPT, it might be necessary to upgrade some settings automatically. + if(m_dwLastSavedWithVersion) + { + UpgradeModule(); + } + +#ifndef NO_PLUGINS + // Load plugins +#ifdef MODPLUG_TRACKER + mpt::ustring notFoundText; +#endif // MODPLUG_TRACKER + std::vector<const SNDMIXPLUGININFO *> notFoundIDs; + + if((loadFlags & (loadPluginData | loadPluginInstance)) == (loadPluginData | loadPluginInstance)) + { + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + auto &plugin = m_MixPlugins[plug]; + if(plugin.IsValidPlugin()) + { +#ifdef MODPLUG_TRACKER + // Provide some visual feedback + { + mpt::ustring s = MPT_UFORMAT("Loading Plugin FX{}: {} ({})")( + mpt::ufmt::dec0<2>(plug + 1), + mpt::ToUnicode(mpt::Charset::UTF8, plugin.Info.szLibraryName), + mpt::ToUnicode(mpt::Charset::Locale, plugin.Info.szName)); + CMainFrame::GetMainFrame()->SetHelpText(mpt::ToCString(s)); + } +#endif // MODPLUG_TRACKER + CreateMixPluginProc(plugin, *this); + if(plugin.pMixPlugin) + { + // Plugin was found + plugin.pMixPlugin->RestoreAllParameters(plugin.defaultProgram); + } else + { + // Plugin not found - add to list + bool found = std::find_if(notFoundIDs.cbegin(), notFoundIDs.cend(), + [&plugin](const SNDMIXPLUGININFO *info) { return info->dwPluginId2 == plugin.Info.dwPluginId2 && info->dwPluginId1 == plugin.Info.dwPluginId1; }) != notFoundIDs.cend(); + + if(!found) + { + notFoundIDs.push_back(&plugin.Info); +#ifdef MODPLUG_TRACKER + notFoundText.append(plugin.GetLibraryName()); + notFoundText.append(UL_("\n")); +#else + AddToLog(LogWarning, U_("Plugin not found: ") + plugin.GetLibraryName()); +#endif // MODPLUG_TRACKER + } + } + } + } + } + + // Set up mix levels (also recalculates plugin mix levels - must be done after plugins were loaded) + SetMixLevels(m_nMixLevels); + +#ifdef MODPLUG_TRACKER + // Display a nice message so the user sees which plugins are missing + // TODO: Use IDD_MODLOADING_WARNINGS dialog (NON-MODAL!) to display all warnings that are encountered when loading a module. + if(!notFoundIDs.empty()) + { + if(notFoundIDs.size() == 1) + { + notFoundText = UL_("The following plugin has not been found:\n\n") + notFoundText + UL_("\nDo you want to search for it online?"); + } else + { + notFoundText = UL_("The following plugins have not been found:\n\n") + notFoundText + UL_("\nDo you want to search for them online?"); + } + if(Reporting::Confirm(notFoundText, U_("OpenMPT - Plugins missing"), false, true) == cnfYes) + { + mpt::ustring url = U_("https://resources.openmpt.org/plugins/search.php?p="); + for(const auto &id : notFoundIDs) + { + url += mpt::ufmt::HEX0<8>(id->dwPluginId2.get()); + url += mpt::ToUnicode(mpt::Charset::UTF8, id->szLibraryName); + url += UL_("%0a"); + } + CTrackApp::OpenURL(mpt::PathString::FromUnicode(url)); + } + } +#endif // MODPLUG_TRACKER +#endif // NO_PLUGINS + + return true; +} + + +bool CSoundFile::Destroy() +{ + for(auto &chn : m_PlayState.Chn) + { + chn.pModInstrument = nullptr; + chn.pModSample = nullptr; + chn.pCurrentSample = nullptr; + chn.nLength = 0; + } + + Patterns.DestroyPatterns(); + + m_songName.clear(); + m_songArtist.clear(); + m_songMessage.clear(); + m_FileHistory.clear(); +#ifdef MPT_EXTERNAL_SAMPLES + m_samplePaths.clear(); +#endif // MPT_EXTERNAL_SAMPLES + + for(auto &smp : Samples) + { + smp.FreeSample(); + } + for(auto &ins : Instruments) + { + delete ins; + ins = nullptr; + } +#ifndef NO_PLUGINS + for(auto &plug : m_MixPlugins) + { + plug.Destroy(); + } +#endif // NO_PLUGINS + + m_nType = MOD_TYPE_NONE; + m_ContainerType = MOD_CONTAINERTYPE_NONE; + m_nChannels = m_nSamples = m_nInstruments = 0; + return true; +} + + +////////////////////////////////////////////////////////////////////////// +// Misc functions + + +void CSoundFile::SetDspEffects(uint32 DSPMask) +{ + m_MixerSettings.DSPMask = DSPMask; + InitPlayer(false); +} + + +void CSoundFile::SetPreAmp(uint32 nVol) +{ + if (nVol < 1) nVol = 1; + if (nVol > 0x200) nVol = 0x200; // x4 maximum +#ifndef NO_AGC + if ((nVol < m_MixerSettings.m_nPreAmp) && (nVol) && (m_MixerSettings.DSPMask & SNDDSP_AGC)) + { + m_AGC.Adjust(m_MixerSettings.m_nPreAmp, nVol); + } +#endif + m_MixerSettings.m_nPreAmp = nVol; +} + + +double CSoundFile::GetCurrentBPM() const +{ + double bpm; + + if (m_nTempoMode == TempoMode::Modern) + { + // With modern mode, we trust that true bpm is close enough to what user chose. + // This avoids oscillation due to tick-to-tick corrections. + bpm = m_PlayState.m_nMusicTempo.ToDouble(); + } else + { + //with other modes, we calculate it: + double ticksPerBeat = m_PlayState.m_nMusicSpeed * m_PlayState.m_nCurrentRowsPerBeat; //ticks/beat = ticks/row * rows/beat + double samplesPerBeat = m_PlayState.m_nSamplesPerTick * ticksPerBeat; //samps/beat = samps/tick * ticks/beat + bpm = m_MixerSettings.gdwMixingFreq / samplesPerBeat * 60; //beats/sec = samps/sec / samps/beat + } //beats/min = beats/sec * 60 + + return bpm; +} + + +void CSoundFile::ResetPlayPos() +{ + const auto muteFlag = GetChannelMuteFlag(); + for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++) + m_PlayState.Chn[i].Reset(ModChannel::resetSetPosFull, *this, i, muteFlag); + + m_visitedRows.Initialize(true); + m_SongFlags.reset(SONG_FADINGSONG | SONG_ENDREACHED); + + m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume; + m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; + m_PlayState.m_nMusicTempo = m_nDefaultTempo; + + // Do not ramp global volume when starting playback + m_PlayState.ResetGlobalVolumeRamping(); + + m_PlayState.m_nNextOrder = 0; + m_PlayState.m_nNextRow = 0; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_nBufferCount = 0; + m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nFrameDelay = 0; + m_PlayState.m_nextPatStartRow = 0; + m_PlayState.m_lTotalSampleCount = 0; +} + + + +void CSoundFile::SetCurrentOrder(ORDERINDEX nOrder) +{ + while(nOrder < Order().size() && !Order().IsValidPat(nOrder)) + nOrder++; + if(nOrder >= Order().size()) + return; + + for(auto &chn : m_PlayState.Chn) + { + chn.nPeriod = 0; + chn.nNote = NOTE_NONE; + chn.nPortamentoDest = 0; + chn.nCommand = 0; + chn.nPatternLoopCount = 0; + chn.nPatternLoop = 0; + chn.nVibratoPos = chn.nTremoloPos = chn.nPanbrelloPos = 0; + //IT compatibility 15. Retrigger + if(m_playBehaviour[kITRetrigger]) + { + chn.nRetrigCount = 0; + chn.nRetrigParam = 1; + } + chn.nTremorCount = 0; + } + +#ifndef NO_PLUGINS + // Stop hanging notes from VST instruments as well + StopAllVsti(); +#endif // NO_PLUGINS + + if (!nOrder) + { + ResetPlayPos(); + } else + { + m_PlayState.m_nNextOrder = nOrder; + m_PlayState.m_nRow = m_PlayState.m_nNextRow = 0; + m_PlayState.m_nPattern = 0; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_nBufferCount = 0; + m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nFrameDelay = 0; + m_PlayState.m_nextPatStartRow = 0; + } + + m_SongFlags.reset(SONG_FADINGSONG | SONG_ENDREACHED); +} + +void CSoundFile::SuspendPlugins() +{ +#ifndef NO_PLUGINS + for(auto &plug : m_MixPlugins) + { + IMixPlugin *pPlugin = plug.pMixPlugin; + if(pPlugin != nullptr && pPlugin->IsResumed()) + { + pPlugin->NotifySongPlaying(false); + pPlugin->HardAllNotesOff(); + pPlugin->Suspend(); + } + } +#endif // NO_PLUGINS +} + +void CSoundFile::ResumePlugins() +{ +#ifndef NO_PLUGINS + for(auto &plugin : m_MixPlugins) + { + IMixPlugin *pPlugin = plugin.pMixPlugin; + if(pPlugin != nullptr && !pPlugin->IsResumed()) + { + pPlugin->NotifySongPlaying(true); + pPlugin->Resume(); + } + } +#endif // NO_PLUGINS +} + + +void CSoundFile::StopAllVsti() +{ +#ifndef NO_PLUGINS + for(auto &plugin : m_MixPlugins) + { + IMixPlugin *pPlugin = plugin.pMixPlugin; + if(pPlugin != nullptr && pPlugin->IsResumed()) + { + pPlugin->HardAllNotesOff(); + } + } +#endif // NO_PLUGINS +} + + +void CSoundFile::SetMixLevels(MixLevels levels) +{ + m_nMixLevels = levels; + m_PlayConfig.SetMixLevels(m_nMixLevels); + RecalculateGainForAllPlugs(); +} + + +void CSoundFile::RecalculateGainForAllPlugs() +{ +#ifndef NO_PLUGINS + for(auto &plugin : m_MixPlugins) + { + if(plugin.pMixPlugin != nullptr) + plugin.pMixPlugin->RecalculateGain(); + } +#endif // NO_PLUGINS +} + + +void CSoundFile::ResetChannels() +{ + m_SongFlags.reset(SONG_FADINGSONG | SONG_ENDREACHED); + m_PlayState.m_nBufferCount = 0; + for(auto &chn : m_PlayState.Chn) + { + chn.nROfs = chn.nLOfs = 0; + chn.nLength = 0; + if(chn.dwFlags[CHN_ADLIB] && m_opl) + { + CHANNELINDEX c = static_cast<CHANNELINDEX>(std::distance(std::begin(m_PlayState.Chn), &chn)); + m_opl->NoteCut(c); + } + } +} + + +#ifdef MODPLUG_TRACKER + +void CSoundFile::PatternTranstionChnSolo(const CHANNELINDEX chnIndex) +{ + if(chnIndex >= m_nChannels) + return; + + for(CHANNELINDEX i = 0; i < m_nChannels; i++) + { + m_bChannelMuteTogglePending[i] = !ChnSettings[i].dwFlags[CHN_MUTE]; + } + m_bChannelMuteTogglePending[chnIndex] = ChnSettings[chnIndex].dwFlags[CHN_MUTE]; +} + + +void CSoundFile::PatternTransitionChnUnmuteAll() +{ + for(CHANNELINDEX i = 0; i < m_nChannels; i++) + { + m_bChannelMuteTogglePending[i] = ChnSettings[i].dwFlags[CHN_MUTE]; + } +} + +#endif // MODPLUG_TRACKER + + +void CSoundFile::LoopPattern(PATTERNINDEX nPat, ROWINDEX nRow) +{ + if(!Patterns.IsValidPat(nPat)) + { + m_SongFlags.reset(SONG_PATTERNLOOP); + } else + { + if(nRow >= Patterns[nPat].GetNumRows()) nRow = 0; + m_PlayState.m_nPattern = nPat; + m_PlayState.m_nRow = m_PlayState.m_nNextRow = nRow; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nFrameDelay = 0; + m_PlayState.m_nextPatStartRow = 0; + m_SongFlags.set(SONG_PATTERNLOOP); + } + m_PlayState.m_nBufferCount = 0; +} + + +void CSoundFile::DontLoopPattern(PATTERNINDEX nPat, ROWINDEX nRow) +{ + if(!Patterns.IsValidPat(nPat)) nPat = 0; + if(nRow >= Patterns[nPat].GetNumRows()) nRow = 0; + m_PlayState.m_nPattern = nPat; + m_PlayState.m_nRow = m_PlayState.m_nNextRow = nRow; + m_PlayState.m_nTickCount = TICKS_ROW_FINISHED; + m_PlayState.m_nPatternDelay = 0; + m_PlayState.m_nFrameDelay = 0; + m_PlayState.m_nBufferCount = 0; + m_PlayState.m_nextPatStartRow = 0; + m_SongFlags.reset(SONG_PATTERNLOOP); +} + + +void CSoundFile::SetDefaultPlaybackBehaviour(MODTYPE type) +{ + m_playBehaviour = GetDefaultPlaybackBehaviour(type); +} + + +PlayBehaviourSet CSoundFile::GetSupportedPlaybackBehaviour(MODTYPE type) +{ + PlayBehaviourSet playBehaviour; + switch(type) + { + case MOD_TYPE_MPT: + case MOD_TYPE_IT: + playBehaviour.set(MSF_COMPATIBLE_PLAY); + playBehaviour.set(kPeriodsAreHertz); + playBehaviour.set(kTempoClamp); + playBehaviour.set(kPerChannelGlobalVolSlide); + playBehaviour.set(kPanOverride); + playBehaviour.set(kITInstrWithoutNote); + playBehaviour.set(kITVolColFinePortamento); + playBehaviour.set(kITArpeggio); + playBehaviour.set(kITOutOfRangeDelay); + playBehaviour.set(kITPortaMemoryShare); + playBehaviour.set(kITPatternLoopTargetReset); + playBehaviour.set(kITFT2PatternLoop); + playBehaviour.set(kITPingPongNoReset); + playBehaviour.set(kITEnvelopeReset); + playBehaviour.set(kITClearOldNoteAfterCut); + playBehaviour.set(kITVibratoTremoloPanbrello); + playBehaviour.set(kITTremor); + playBehaviour.set(kITRetrigger); + playBehaviour.set(kITMultiSampleBehaviour); + playBehaviour.set(kITPortaTargetReached); + playBehaviour.set(kITPatternLoopBreak); + playBehaviour.set(kITOffset); + playBehaviour.set(kITSwingBehaviour); + playBehaviour.set(kITNNAReset); + playBehaviour.set(kITSCxStopsSample); + playBehaviour.set(kITEnvelopePositionHandling); + playBehaviour.set(kITPortamentoInstrument); + playBehaviour.set(kITPingPongMode); + playBehaviour.set(kITRealNoteMapping); + playBehaviour.set(kITHighOffsetNoRetrig); + playBehaviour.set(kITFilterBehaviour); + playBehaviour.set(kITNoSurroundPan); + playBehaviour.set(kITShortSampleRetrig); + playBehaviour.set(kITPortaNoNote); + playBehaviour.set(kITFT2DontResetNoteOffOnPorta); + playBehaviour.set(kITVolColMemory); + playBehaviour.set(kITPortamentoSwapResetsPos); + playBehaviour.set(kITEmptyNoteMapSlot); + playBehaviour.set(kITFirstTickHandling); + playBehaviour.set(kITSampleAndHoldPanbrello); + playBehaviour.set(kITClearPortaTarget); + playBehaviour.set(kITPanbrelloHold); + playBehaviour.set(kITPanningReset); + playBehaviour.set(kITPatternLoopWithJumps); + playBehaviour.set(kITInstrWithNoteOff); + playBehaviour.set(kITMultiSampleInstrumentNumber); + playBehaviour.set(kRowDelayWithNoteDelay); + playBehaviour.set(kITInstrWithNoteOffOldEffects); + playBehaviour.set(kITDoNotOverrideChannelPan); + playBehaviour.set(kITDCTBehaviour); + playBehaviour.set(kITPitchPanSeparation); + if(type == MOD_TYPE_MPT) + { + playBehaviour.set(kOPLFlexibleNoteOff); + playBehaviour.set(kOPLwithNNA); + playBehaviour.set(kOPLNoteOffOnNoteChange); + } + break; + + case MOD_TYPE_XM: + playBehaviour.set(MSF_COMPATIBLE_PLAY); + playBehaviour.set(kFT2VolumeRamping); + playBehaviour.set(kTempoClamp); + playBehaviour.set(kPerChannelGlobalVolSlide); + playBehaviour.set(kPanOverride); + playBehaviour.set(kITFT2PatternLoop); + playBehaviour.set(kITFT2DontResetNoteOffOnPorta); + playBehaviour.set(kFT2Arpeggio); + playBehaviour.set(kFT2Retrigger); + playBehaviour.set(kFT2VolColVibrato); + playBehaviour.set(kFT2PortaNoNote); + playBehaviour.set(kFT2KeyOff); + playBehaviour.set(kFT2PanSlide); + playBehaviour.set(kFT2ST3OffsetOutOfRange); + playBehaviour.set(kFT2RestrictXCommand); + playBehaviour.set(kFT2RetrigWithNoteDelay); + playBehaviour.set(kFT2SetPanEnvPos); + playBehaviour.set(kFT2PortaIgnoreInstr); + playBehaviour.set(kFT2VolColMemory); + playBehaviour.set(kFT2LoopE60Restart); + playBehaviour.set(kFT2ProcessSilentChannels); + playBehaviour.set(kFT2ReloadSampleSettings); + playBehaviour.set(kFT2PortaDelay); + playBehaviour.set(kFT2Transpose); + playBehaviour.set(kFT2PatternLoopWithJumps); + playBehaviour.set(kFT2PortaTargetNoReset); + playBehaviour.set(kFT2EnvelopeEscape); + playBehaviour.set(kFT2Tremor); + playBehaviour.set(kFT2OutOfRangeDelay); + playBehaviour.set(kFT2Periods); + playBehaviour.set(kFT2PanWithDelayedNoteOff); + playBehaviour.set(kFT2VolColDelay); + playBehaviour.set(kFT2FinetunePrecision); + playBehaviour.set(kFT2NoteOffFlags); + playBehaviour.set(kRowDelayWithNoteDelay); + playBehaviour.set(kFT2MODTremoloRampWaveform); + playBehaviour.set(kFT2PortaUpDownMemory); + playBehaviour.set(kFT2PanSustainRelease); + playBehaviour.set(kFT2NoteDelayWithoutInstr); + playBehaviour.set(kFT2PortaResetDirection); + break; + + case MOD_TYPE_S3M: + playBehaviour.set(MSF_COMPATIBLE_PLAY); + playBehaviour.set(kTempoClamp); + playBehaviour.set(kPanOverride); + playBehaviour.set(kITPanbrelloHold); + playBehaviour.set(kFT2ST3OffsetOutOfRange); + playBehaviour.set(kST3NoMutedChannels); + playBehaviour.set(kST3PortaSampleChange); + playBehaviour.set(kST3EffectMemory); + playBehaviour.set(kST3VibratoMemory); + playBehaviour.set(KST3PortaAfterArpeggio); + playBehaviour.set(kRowDelayWithNoteDelay); + playBehaviour.set(kST3OffsetWithoutInstrument); + playBehaviour.set(kST3RetrigAfterNoteCut); + playBehaviour.set(kST3SampleSwap); + playBehaviour.set(kOPLNoteOffOnNoteChange); + playBehaviour.set(kApplyUpperPeriodLimit); + break; + + case MOD_TYPE_MOD: + playBehaviour.set(kMODVBlankTiming); + playBehaviour.set(kMODOneShotLoops); + playBehaviour.set(kMODIgnorePanning); + playBehaviour.set(kMODSampleSwap); + playBehaviour.set(kMODOutOfRangeNoteDelay); + playBehaviour.set(kMODTempoOnSecondTick); + playBehaviour.set(kRowDelayWithNoteDelay); + playBehaviour.set(kFT2MODTremoloRampWaveform); + break; + + default: + playBehaviour.set(MSF_COMPATIBLE_PLAY); + playBehaviour.set(kPeriodsAreHertz); + playBehaviour.set(kTempoClamp); + playBehaviour.set(kPanOverride); + break; + } + return playBehaviour; +} + + +PlayBehaviourSet CSoundFile::GetDefaultPlaybackBehaviour(MODTYPE type) +{ + PlayBehaviourSet playBehaviour; + switch(type) + { + case MOD_TYPE_MPT: + playBehaviour.set(kPeriodsAreHertz); + playBehaviour.set(kPerChannelGlobalVolSlide); + playBehaviour.set(kPanOverride); + playBehaviour.set(kITArpeggio); + playBehaviour.set(kITPortaMemoryShare); + playBehaviour.set(kITPatternLoopTargetReset); + playBehaviour.set(kITFT2PatternLoop); + playBehaviour.set(kITPingPongNoReset); + playBehaviour.set(kITClearOldNoteAfterCut); + playBehaviour.set(kITVibratoTremoloPanbrello); + playBehaviour.set(kITMultiSampleBehaviour); + playBehaviour.set(kITPortaTargetReached); + playBehaviour.set(kITPatternLoopBreak); + playBehaviour.set(kITSwingBehaviour); + playBehaviour.set(kITSCxStopsSample); + playBehaviour.set(kITEnvelopePositionHandling); + playBehaviour.set(kITPingPongMode); + playBehaviour.set(kITRealNoteMapping); + playBehaviour.set(kITPortaNoNote); + playBehaviour.set(kITVolColMemory); + playBehaviour.set(kITFirstTickHandling); + playBehaviour.set(kITClearPortaTarget); + playBehaviour.set(kITSampleAndHoldPanbrello); + playBehaviour.set(kITPanbrelloHold); + playBehaviour.set(kITPanningReset); + playBehaviour.set(kITInstrWithNoteOff); + playBehaviour.set(kOPLFlexibleNoteOff); + playBehaviour.set(kITDoNotOverrideChannelPan); + playBehaviour.set(kITDCTBehaviour); + playBehaviour.set(kOPLwithNNA); + playBehaviour.set(kITPitchPanSeparation); + break; + + case MOD_TYPE_S3M: + playBehaviour = GetSupportedPlaybackBehaviour(type); + // Default behaviour was chosen to follow GUS, so kST3PortaSampleChange is enabled and kST3SampleSwap is disabled. + // For SoundBlaster behaviour, those two flags would need to be swapped. + playBehaviour.reset(kST3SampleSwap); + break; + + case MOD_TYPE_XM: + playBehaviour = GetSupportedPlaybackBehaviour(type); + // Only set this explicitely for FT2-made XMs. + playBehaviour.reset(kFT2VolumeRamping); + break; + + case MOD_TYPE_MOD: + playBehaviour.set(kRowDelayWithNoteDelay); + break; + + default: + playBehaviour = GetSupportedPlaybackBehaviour(type); + break; + } + return playBehaviour; +} + + +MODTYPE CSoundFile::GetBestSaveFormat() const +{ + switch(GetType()) + { + case MOD_TYPE_MOD: + case MOD_TYPE_S3M: + case MOD_TYPE_XM: + case MOD_TYPE_IT: + case MOD_TYPE_MPT: + return GetType(); + case MOD_TYPE_AMF0: + case MOD_TYPE_DIGI: + case MOD_TYPE_SFX: + case MOD_TYPE_STP: + return MOD_TYPE_MOD; + case MOD_TYPE_MED: + if(!m_nInstruments) + { + for(const auto &pat : Patterns) + { + if(pat.IsValid() && pat.GetNumRows() != 64) + return MOD_TYPE_XM; + } + return MOD_TYPE_MOD; + } + return MOD_TYPE_XM; + case MOD_TYPE_PSM: + if(GetNumChannels() > 16) + return MOD_TYPE_IT; + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) + { + if(ChnSettings[i].dwFlags[CHN_SURROUND] || ChnSettings[i].nVolume != 64) + { + return MOD_TYPE_IT; + break; + } + } + return MOD_TYPE_S3M; + case MOD_TYPE_669: + case MOD_TYPE_FAR: + case MOD_TYPE_STM: + case MOD_TYPE_DSM: + case MOD_TYPE_AMF: + case MOD_TYPE_MTM: + return MOD_TYPE_S3M; + case MOD_TYPE_AMS: + case MOD_TYPE_DMF: + case MOD_TYPE_DBM: + case MOD_TYPE_IMF: + case MOD_TYPE_J2B: + case MOD_TYPE_ULT: + case MOD_TYPE_OKT: + case MOD_TYPE_MT2: + case MOD_TYPE_MDL: + case MOD_TYPE_PTM: + case MOD_TYPE_DTM: + default: + return MOD_TYPE_IT; + case MOD_TYPE_MID: + return MOD_TYPE_MPT; + } +} + + +const char *CSoundFile::GetSampleName(SAMPLEINDEX nSample) const +{ + MPT_ASSERT(nSample <= GetNumSamples()); + if (nSample < MAX_SAMPLES) + { + return m_szNames[nSample].buf; + } else + { + return ""; + } +} + + +const char *CSoundFile::GetInstrumentName(INSTRUMENTINDEX nInstr) const +{ + if((nInstr >= MAX_INSTRUMENTS) || (!Instruments[nInstr])) + return ""; + + MPT_ASSERT(nInstr <= GetNumInstruments()); + return Instruments[nInstr]->name.buf; +} + + +bool CSoundFile::InitChannel(CHANNELINDEX nChn) +{ + if(nChn >= MAX_BASECHANNELS) + return true; + + ChnSettings[nChn].Reset(); + m_PlayState.Chn[nChn].Reset(ModChannel::resetTotal, *this, nChn, GetChannelMuteFlag()); + +#ifdef MODPLUG_TRACKER + if(GetpModDoc() != nullptr) + { + GetpModDoc()->SetChannelRecordGroup(nChn, RecordGroup::NoGroup); + } +#endif // MODPLUG_TRACKER + +#ifdef MODPLUG_TRACKER + m_bChannelMuteTogglePending[nChn] = false; +#endif // MODPLUG_TRACKER + + return false; +} + + +void CSoundFile::InitAmigaResampler() +{ + if(m_SongFlags[SONG_ISAMIGA] && m_Resampler.m_Settings.emulateAmiga != Resampling::AmigaFilter::Off) + { + const Paula::State defaultState(GetSampleRate()); + for(auto &chn : m_PlayState.Chn) + { + chn.paulaState = defaultState; + } + } +} + + +void CSoundFile::InitOPL() +{ + if(!m_opl) + m_opl = std::make_unique<OPL>(m_MixerSettings.gdwMixingFreq); +} + + +// Detect samples that are referenced by an instrument, but actually not used in a song. +// Only works in instrument mode. Unused samples are marked as false in the vector. +SAMPLEINDEX CSoundFile::DetectUnusedSamples(std::vector<bool> &sampleUsed) const +{ + sampleUsed.assign(GetNumSamples() + 1, false); + + if(GetNumInstruments() == 0) + { + return 0; + } + SAMPLEINDEX unused = 0; + std::vector<ModCommand::INSTR> lastIns; + + for(const auto &pat : Patterns) if(pat.IsValid()) + { + lastIns.assign(GetNumChannels(), 0); + auto p = pat.cbegin(); + for(ROWINDEX row = 0; row < pat.GetNumRows(); row++) + { + for(CHANNELINDEX c = 0; c < GetNumChannels(); c++, p++) + { + if(p->IsNote()) + { + ModCommand::INSTR instr = p->instr; + if(!p->instr) + instr = lastIns[c]; + INSTRUMENTINDEX minInstr = 1, maxInstr = GetNumInstruments(); + if(instr > 0) + { + if(instr <= GetNumInstruments()) + { + minInstr = maxInstr = instr; + } + lastIns[c] = instr; + } else + { + // No idea which instrument this note belongs to, so mark it used in any instruments. + } + for(INSTRUMENTINDEX i = minInstr; i <= maxInstr; i++) + { + if(const auto *pIns = Instruments[i]; pIns != nullptr) + { + SAMPLEINDEX n = pIns->Keyboard[p->note - NOTE_MIN]; + if(n <= GetNumSamples()) + sampleUsed[n] = true; + } + } + } + } + } + } + for (SAMPLEINDEX ichk = GetNumSamples(); ichk >= 1; ichk--) + { + if ((!sampleUsed[ichk]) && (Samples[ichk].HasSampleData())) unused++; + } + + return unused; +} + + +// Destroy samples where keepSamples index is false. First sample is keepSamples[1]! +SAMPLEINDEX CSoundFile::RemoveSelectedSamples(const std::vector<bool> &keepSamples) +{ + if(keepSamples.empty()) + { + return 0; + } + + SAMPLEINDEX nRemoved = 0; + for(SAMPLEINDEX nSmp = std::min(GetNumSamples(), static_cast<SAMPLEINDEX>(keepSamples.size() - 1)); nSmp >= 1; nSmp--) + { + if(!keepSamples[nSmp]) + { + CriticalSection cs; + +#ifdef MODPLUG_TRACKER + if(GetpModDoc()) + { + GetpModDoc()->GetSampleUndo().PrepareUndo(nSmp, sundo_replace, "Remove Sample"); + } +#endif // MODPLUG_TRACKER + + if(DestroySample(nSmp)) + { + m_szNames[nSmp] = ""; + nRemoved++; + } + if((nSmp == GetNumSamples()) && (nSmp > 1)) m_nSamples--; + } + } + return nRemoved; +} + + +bool CSoundFile::DestroySample(SAMPLEINDEX nSample) +{ + if(!nSample || nSample >= MAX_SAMPLES) + { + return false; + } + if(!Samples[nSample].HasSampleData()) + { + return true; + } + + ModSample &sample = Samples[nSample]; + + for(auto &chn : m_PlayState.Chn) + { + if(chn.pModSample == &sample) + { + chn.position.Set(0); + chn.nLength = 0; + chn.pCurrentSample = nullptr; + } + } + + sample.FreeSample(); + sample.nLength = 0; + sample.uFlags.reset(CHN_16BIT | CHN_STEREO); + sample.SetAdlib(false); + +#ifdef MODPLUG_TRACKER + ResetSamplePath(nSample); +#endif + return true; +} + + +bool CSoundFile::DestroySampleThreadsafe(SAMPLEINDEX nSample) +{ + CriticalSection cs; + return DestroySample(nSample); +} + + +std::unique_ptr<CTuning> CSoundFile::CreateTuning12TET(const mpt::ustring &name) +{ + std::unique_ptr<CTuning> pT = CTuning::CreateGeometric(name, 12, 2, 15); + for(ModCommand::NOTE note = 0; note < 12; ++note) + { + pT->SetNoteName(note, mpt::ustring(NoteNamesSharp[note])); + } + return pT; +} + + +mpt::ustring CSoundFile::GetNoteName(const ModCommand::NOTE note, const INSTRUMENTINDEX inst) const +{ + // For MPTM instruments with custom tuning, find the appropriate note name. Else, use default note names. + if(ModCommand::IsNote(note) && GetType() == MOD_TYPE_MPT && inst >= 1 && inst <= GetNumInstruments() && Instruments[inst] && Instruments[inst]->pTuning) + { + return Instruments[inst]->pTuning->GetNoteName(note - NOTE_MIDDLEC); + } else + { + return GetNoteName(note); + } +} + + +mpt::ustring CSoundFile::GetNoteName(const ModCommand::NOTE note) const +{ + return GetNoteName(note, m_NoteNames); +} + + +mpt::ustring CSoundFile::GetNoteName(const ModCommand::NOTE note, const NoteName *noteNames) +{ + if(ModCommand::IsSpecialNote(note)) + { + // cppcheck false-positive + // cppcheck-suppress constStatement + const mpt::uchar specialNoteNames[][4] = { UL_("PCs"), UL_("PC "), UL_("~~~"), UL_("^^^"), UL_("===") }; + static_assert(mpt::array_size<decltype(specialNoteNames)>::size == NOTE_MAX_SPECIAL - NOTE_MIN_SPECIAL + 1); + return specialNoteNames[note - NOTE_MIN_SPECIAL]; + } else if(ModCommand::IsNote(note)) + { + return mpt::ustring() + .append(noteNames[(note - NOTE_MIN) % 12]) + .append(1, UC_('0') + (note - NOTE_MIN) / 12) + ; // e.g. "C#" + "5" + } else if(note == NOTE_NONE) + { + return UL_("..."); + } + return UL_("???"); +} + + +#ifdef MODPLUG_TRACKER + +void CSoundFile::SetDefaultNoteNames() +{ + m_NoteNames = TrackerSettings::Instance().accidentalFlats ? NoteNamesFlat : NoteNamesSharp; +} + +const NoteName *CSoundFile::GetDefaultNoteNames() +{ + return m_NoteNames; +} + +#endif // MODPLUG_TRACKER + + +void CSoundFile::SetModSpecsPointer(const CModSpecifications*& pModSpecs, const MODTYPE type) +{ + switch(type) + { + case MOD_TYPE_MPT: + pModSpecs = &ModSpecs::mptm; + break; + + case MOD_TYPE_IT: + pModSpecs = &ModSpecs::itEx; + break; + + case MOD_TYPE_XM: + pModSpecs = &ModSpecs::xmEx; + break; + + case MOD_TYPE_S3M: + pModSpecs = &ModSpecs::s3mEx; + break; + + case MOD_TYPE_MOD: + default: + pModSpecs = &ModSpecs::mod; + break; + } +} + + +void CSoundFile::SetType(MODTYPE type) +{ + m_nType = type; + m_playBehaviour = GetDefaultPlaybackBehaviour(GetBestSaveFormat()); + SetModSpecsPointer(m_pModSpecs, GetBestSaveFormat()); +} + + +#ifdef MODPLUG_TRACKER + +void CSoundFile::ChangeModTypeTo(const MODTYPE newType, bool adjust) +{ + const MODTYPE oldType = GetType(); + m_nType = newType; + SetModSpecsPointer(m_pModSpecs, m_nType); + + if(oldType == newType || !adjust) + return; + + SetupMODPanning(); // Setup LRRL panning scheme if needed + + // Only keep supported play behaviour flags + PlayBehaviourSet oldAllowedFlags = GetSupportedPlaybackBehaviour(oldType); + PlayBehaviourSet newAllowedFlags = GetSupportedPlaybackBehaviour(newType); + PlayBehaviourSet newDefaultFlags = GetDefaultPlaybackBehaviour(newType); + for(size_t i = 0; i < m_playBehaviour.size(); i++) + { + // If a flag is supported in both formats, keep its status + if(m_playBehaviour[i]) m_playBehaviour.set(i, newAllowedFlags[i]); + // Set allowed flags to their defaults if they were not supported in the old format + if(!oldAllowedFlags[i]) m_playBehaviour.set(i, newDefaultFlags[i]); + } + // Special case for OPL behaviour when converting from S3M to MPTM to retain S3M-like note-off behaviour + if(oldType == MOD_TYPE_S3M && newType == MOD_TYPE_MPT && m_opl) + m_playBehaviour.reset(kOPLFlexibleNoteOff); + + Order.OnModTypeChanged(oldType); + Patterns.OnModTypeChanged(oldType); + + m_modFormat.type = mpt::ToUnicode(mpt::Charset::UTF8, GetModSpecifications().fileExtension); +} + +#endif // MODPLUG_TRACKER + + +ModMessageHeuristicOrder CSoundFile::GetMessageHeuristic() const +{ + ModMessageHeuristicOrder result = ModMessageHeuristicOrder::Default; + switch(GetType()) + { + case MOD_TYPE_MPT: + result = ModMessageHeuristicOrder::Samples; + break; + case MOD_TYPE_IT: + result = ModMessageHeuristicOrder::Samples; + break; + case MOD_TYPE_XM: + result = ModMessageHeuristicOrder::InstrumentsSamples; + break; + case MOD_TYPE_MDL: + result = ModMessageHeuristicOrder::InstrumentsSamples; + break; + case MOD_TYPE_IMF: + result = ModMessageHeuristicOrder::InstrumentsSamples; + break; + default: + result = ModMessageHeuristicOrder::Default; + break; + } + return result; +} + + +bool CSoundFile::SetTitle(const std::string &newTitle) +{ + if(m_songName != newTitle) + { + m_songName = newTitle; + return true; + } + return false; +} + + +double CSoundFile::GetPlaybackTimeAt(ORDERINDEX ord, ROWINDEX row, bool updateVars, bool updateSamplePos) +{ + const GetLengthType t = GetLength(updateVars ? (updateSamplePos ? eAdjustSamplePositions : eAdjust) : eNoAdjust, GetLengthTarget(ord, row)).back(); + if(t.targetReached) return t.duration; + else return -1; //Given position not found from play sequence. +} + + +std::vector<SubSong> CSoundFile::GetAllSubSongs() +{ + std::vector<SubSong> subSongs; + for(SEQUENCEINDEX seq = 0; seq < Order.GetNumSequences(); seq++) + { + const auto subSongsSeq = GetLength(eNoAdjust, GetLengthTarget(true).StartPos(seq, 0, 0)); + subSongs.reserve(subSongs.size() + subSongsSeq.size()); + for(const auto &song : subSongsSeq) + { + subSongs.push_back({song.duration, song.startRow, song.endRow, song.lastRow, song.startOrder, song.endOrder, song.lastOrder, seq}); + } + } + return subSongs; +} + + +// Calculate the length of a tick, depending on the tempo mode. +// This differs from GetTickDuration() by not accumulating errors +// because this is not called once per tick but in unrelated +// circumstances. So this should not update error accumulation. +void CSoundFile::RecalculateSamplesPerTick() +{ + switch(m_nTempoMode) + { + case TempoMode::Classic: + default: + m_PlayState.m_nSamplesPerTick = Util::muldiv(m_MixerSettings.gdwMixingFreq, 5 * TEMPO::fractFact, std::max(TEMPO::store_t(1), m_PlayState.m_nMusicTempo.GetRaw() << 1)); + break; + + case TempoMode::Modern: + m_PlayState.m_nSamplesPerTick = static_cast<uint32>((Util::mul32to64_unsigned(m_MixerSettings.gdwMixingFreq, 60 * TEMPO::fractFact) / std::max(uint64(1), Util::mul32to64_unsigned(m_PlayState.m_nMusicSpeed, m_PlayState.m_nCurrentRowsPerBeat) * m_PlayState.m_nMusicTempo.GetRaw()))); + break; + + case TempoMode::Alternative: + m_PlayState.m_nSamplesPerTick = Util::muldiv(m_MixerSettings.gdwMixingFreq, TEMPO::fractFact, std::max(TEMPO::store_t(1), m_PlayState.m_nMusicTempo.GetRaw())); + break; + } +#ifndef MODPLUG_TRACKER + m_PlayState.m_nSamplesPerTick = Util::muldivr(m_PlayState.m_nSamplesPerTick, m_nTempoFactor, 65536); +#endif // !MODPLUG_TRACKER + if(!m_PlayState.m_nSamplesPerTick) + m_PlayState.m_nSamplesPerTick = 1; +} + + +// Get length of a tick in sample, with tick-to-tick tempo correction in modern tempo mode. +// This has to be called exactly once per tick because otherwise the error accumulation +// goes wrong. +uint32 CSoundFile::GetTickDuration(PlayState &playState) const +{ + uint32 retval = 0; + switch(m_nTempoMode) + { + case TempoMode::Classic: + default: + retval = Util::muldiv(m_MixerSettings.gdwMixingFreq, 5 * TEMPO::fractFact, std::max(TEMPO::store_t(1), playState.m_nMusicTempo.GetRaw() << 1)); + break; + + case TempoMode::Alternative: + retval = Util::muldiv(m_MixerSettings.gdwMixingFreq, TEMPO::fractFact, std::max(TEMPO::store_t(1), playState.m_nMusicTempo.GetRaw())); + break; + + case TempoMode::Modern: + { + double accurateBufferCount = static_cast<double>(m_MixerSettings.gdwMixingFreq) * (60.0 / (playState.m_nMusicTempo.ToDouble() * Util::mul32to64_unsigned(playState.m_nMusicSpeed, playState.m_nCurrentRowsPerBeat))); + const TempoSwing &swing = (Patterns.IsValidPat(playState.m_nPattern) && Patterns[playState.m_nPattern].HasTempoSwing()) + ? Patterns[playState.m_nPattern].GetTempoSwing() + : m_tempoSwing; + if(!swing.empty()) + { + // Apply current row's tempo swing factor + TempoSwing::value_type swingFactor = swing[playState.m_nRow % swing.size()]; + accurateBufferCount = accurateBufferCount * swingFactor / double(TempoSwing::Unity); + } + uint32 bufferCount = static_cast<int>(accurateBufferCount); + playState.m_dBufferDiff += accurateBufferCount - bufferCount; + + //tick-to-tick tempo correction: + if(playState.m_dBufferDiff >= 1) + { + bufferCount++; + playState.m_dBufferDiff--; + } else if(m_PlayState.m_dBufferDiff <= -1) + { + bufferCount--; + playState.m_dBufferDiff++; + } + MPT_ASSERT(std::abs(playState.m_dBufferDiff) < 1.0); + retval = bufferCount; + } + break; + } +#ifndef MODPLUG_TRACKER + // when the user modifies the tempo, we do not really care about accurate tempo error accumulation + retval = Util::muldivr_unsigned(retval, m_nTempoFactor, 65536); +#endif // !MODPLUG_TRACKER + if(!retval) + retval = 1; + return retval; +} + + +// Get the duration of a row in milliseconds, based on the current rows per beat and given speed and tempo settings. +double CSoundFile::GetRowDuration(TEMPO tempo, uint32 speed) const +{ + switch(m_nTempoMode) + { + case TempoMode::Classic: + default: + return static_cast<double>(2500 * speed) / tempo.ToDouble(); + + case TempoMode::Modern: + { + // If there are any row delay effects, the row length factor compensates for those. + return 60000.0 / tempo.ToDouble() / static_cast<double>(m_PlayState.m_nCurrentRowsPerBeat); + } + + case TempoMode::Alternative: + return static_cast<double>(1000 * speed) / tempo.ToDouble(); + } +} + + +const CModSpecifications& CSoundFile::GetModSpecifications(const MODTYPE type) +{ + const CModSpecifications* p = nullptr; + SetModSpecsPointer(p, type); + return *p; +} + + +ChannelFlags CSoundFile::GetChannelMuteFlag() +{ +#ifdef MODPLUG_TRACKER + return (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SYNCMUTE) ? CHN_SYNCMUTE : CHN_MUTE; +#else + return CHN_SYNCMUTE; +#endif +} + + +// Resolve note/instrument combination to real sample index. Return value is guaranteed to be in [0, GetNumSamples()]. +SAMPLEINDEX CSoundFile::GetSampleIndex(ModCommand::NOTE note, uint32 instr) const noexcept +{ + SAMPLEINDEX smp = 0; + if(GetNumInstruments()) + { + if(ModCommand::IsNote(note) && instr <= GetNumInstruments() && Instruments[instr] != nullptr) + smp = Instruments[instr]->Keyboard[note - NOTE_MIN]; + } else + { + smp = static_cast<SAMPLEINDEX>(instr); + } + if(smp <= GetNumSamples()) + return smp; + else + return 0; +} + + +// Find an unused sample slot. If it is going to be assigned to an instrument, targetInstrument should be specified. +// SAMPLEINDEX_INVLAID is returned if no free sample slot could be found. +SAMPLEINDEX CSoundFile::GetNextFreeSample(INSTRUMENTINDEX targetInstrument, SAMPLEINDEX start) const +{ + // Find empty slot in two passes - in the first pass, we only search for samples with empty sample names, + // in the second pass we check all samples with non-empty sample names. + for(int passes = 0; passes < 2; passes++) + { + for(SAMPLEINDEX i = start; i <= GetModSpecifications().samplesMax; i++) + { + // Early exit for FM instruments + if(Samples[i].uFlags[CHN_ADLIB] && (targetInstrument == INSTRUMENTINDEX_INVALID || !IsSampleReferencedByInstrument(i, targetInstrument))) + continue; + + // When loading into an instrument, ignore non-empty sample names. Else, only use this slot if the sample name is empty or we're in second pass. + if((i > GetNumSamples() && passes == 1) + || (!Samples[i].HasSampleData() && (!m_szNames[i][0] || passes == 1 || targetInstrument != INSTRUMENTINDEX_INVALID)) + || (targetInstrument != INSTRUMENTINDEX_INVALID && IsSampleReferencedByInstrument(i, targetInstrument))) // Not empty, but already used by this instrument. XXX this should only be done when replacing an instrument with a single sample! Otherwise it will use an inconsistent sample map! + { + // Empty slot, so it's a good candidate already. + + // In instrument mode, check whether any instrument references this sample slot. If that is the case, we won't use it as it could lead to unwanted conflicts. + // If we are loading the sample *into* an instrument, we should also not consider that instrument's sample map, since it might be inconsistent at this time. + bool isReferenced = false; + for(INSTRUMENTINDEX ins = 1; ins <= GetNumInstruments(); ins++) + { + if(ins == targetInstrument) + { + continue; + } + if(IsSampleReferencedByInstrument(i, ins)) + { + isReferenced = true; + break; + } + } + if(!isReferenced) + { + return i; + } + } + } + } + + return SAMPLEINDEX_INVALID; +} + + +// Find an unused instrument slot. +// INSTRUMENTINDEX_INVALID is returned if no free instrument slot could be found. +INSTRUMENTINDEX CSoundFile::GetNextFreeInstrument(INSTRUMENTINDEX start) const +{ + for(INSTRUMENTINDEX i = start; i <= GetModSpecifications().instrumentsMax; i++) + { + if(Instruments[i] == nullptr) + { + return i; + } + } + + return INSTRUMENTINDEX_INVALID; +} + + +// Check whether a given sample is used by a given instrument. +bool CSoundFile::IsSampleReferencedByInstrument(SAMPLEINDEX sample, INSTRUMENTINDEX instr) const +{ + if(instr < 1 || instr > GetNumInstruments()) + return false; + + const ModInstrument *targetIns = Instruments[instr]; + if(targetIns == nullptr) + return false; + + return mpt::contains(mpt::as_span(targetIns->Keyboard).first(NOTE_MAX), sample); +} + + +ModInstrument *CSoundFile::AllocateInstrument(INSTRUMENTINDEX instr, SAMPLEINDEX assignedSample) +{ + if(instr == 0 || instr >= MAX_INSTRUMENTS) + { + return nullptr; + } + + ModInstrument *ins = Instruments[instr]; + if(ins != nullptr) + { + // Re-initialize instrument + *ins = ModInstrument(assignedSample); + } else + { + // Create new instrument + Instruments[instr] = ins = new (std::nothrow) ModInstrument(assignedSample); + } + if(ins != nullptr) + { + m_nInstruments = std::max(m_nInstruments, instr); + } + return ins; +} + + +void CSoundFile::PrecomputeSampleLoops(bool updateChannels) +{ + for(SAMPLEINDEX i = 1; i <= GetNumSamples(); i++) + { + Samples[i].PrecomputeLoops(*this, updateChannels); + } +} + + +#ifdef MPT_EXTERNAL_SAMPLES +// Load external waveform, but keep sample properties like frequency, panning, etc... +// Returns true if the file could be loaded. +bool CSoundFile::LoadExternalSample(SAMPLEINDEX smp, const mpt::PathString &filename) +{ + bool ok = false; + InputFile f(filename, SettingCacheCompleteFileBeforeLoading()); + + if(f.IsValid()) + { + const ModSample origSample = Samples[smp]; + mpt::charbuf<MAX_SAMPLENAME> origName; + origName = m_szNames[smp]; + + FileReader file = GetFileReader(f); + ok = ReadSampleFromFile(smp, file, false); + if(ok) + { + // Copy over old attributes, but keep new sample data + ModSample &sample = GetSample(smp); + SmpLength newLength = sample.nLength; + void *newData = sample.samplev(); + SampleFlags newFlags = sample.uFlags; + + sample = origSample; + sample.nLength = newLength; + sample.pData.pSample = newData; + sample.uFlags.set(CHN_16BIT, newFlags[CHN_16BIT]); + sample.uFlags.set(CHN_STEREO, newFlags[CHN_STEREO]); + sample.uFlags.reset(SMP_MODIFIED); + sample.SanitizeLoops(); + } + m_szNames[smp] = origName; + } + SetSamplePath(smp, filename); + return ok; +} +#endif // MPT_EXTERNAL_SAMPLES + + +// Set up channel panning and volume suitable for MOD + similar files. If the current mod type is not MOD, bForceSetup has to be set to true. +void CSoundFile::SetupMODPanning(bool bForceSetup) +{ + // Setup LRRL panning, max channel volume + if(!(GetType() & MOD_TYPE_MOD) && bForceSetup == false) return; + + for(CHANNELINDEX nChn = 0; nChn < MAX_BASECHANNELS; nChn++) + { + ChnSettings[nChn].nVolume = 64; + ChnSettings[nChn].dwFlags.reset(CHN_SURROUND); + if(m_MixerSettings.MixerFlags & SNDMIX_MAXDEFAULTPAN) + ChnSettings[nChn].nPan = (((nChn & 3) == 1) || ((nChn & 3) == 2)) ? 256 : 0; + else + ChnSettings[nChn].nPan = (((nChn & 3) == 1) || ((nChn & 3) == 2)) ? 0xC0 : 0x40; + } +} + + +void CSoundFile::PropagateXMAutoVibrato(INSTRUMENTINDEX ins, VibratoType type, uint8 sweep, uint8 depth, uint8 rate) +{ + if(ins > m_nInstruments || Instruments[ins] == nullptr) + return; + const std::set<SAMPLEINDEX> referencedSamples = Instruments[ins]->GetSamples(); + + // Propagate changes to all samples that belong to this instrument. + for(auto sample : referencedSamples) + { + if(sample <= m_nSamples) + { + Samples[sample].nVibDepth = depth; + Samples[sample].nVibType = type; + Samples[sample].nVibRate = rate; + Samples[sample].nVibSweep = sweep; + } + } +} + + +// Normalize the tempo swing coefficients so that they add up to exactly the specified tempo again +void TempoSwing::Normalize() +{ + if(empty()) return; + uint64 sum = 0; + for(auto &i : *this) + { + Limit(i, Unity / 4u, Unity * 4u); + sum += i; + } + sum /= size(); + MPT_ASSERT(sum > 0); // clang-analyzer false-positive + int64 remain = Unity * size(); + for(auto &i : *this) + { + i = Util::muldivr_unsigned(i, Unity, static_cast<int32>(sum)); + remain -= i; + } + //MPT_ASSERT(static_cast<uint32>(std::abs(static_cast<int32>(remain))) <= size()); + at(0) += static_cast<int32>(remain); +} + + +void TempoSwing::Serialize(std::ostream &oStrm, const TempoSwing &swing) +{ + mpt::IO::WriteIntLE<uint16>(oStrm, static_cast<uint16>(swing.size())); + for(std::size_t i = 0; i < swing.size(); i++) + { + mpt::IO::WriteIntLE<uint32>(oStrm, swing[i]); + } +} + + +void TempoSwing::Deserialize(std::istream &iStrm, TempoSwing &swing, const size_t) +{ + uint16 numEntries; + mpt::IO::ReadIntLE<uint16>(iStrm, numEntries); + swing.resize(numEntries); + for(uint16 i = 0; i < numEntries; i++) + { + mpt::IO::ReadIntLE<uint32>(iStrm, swing[i]); + } + swing.Normalize(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.h b/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.h new file mode 100644 index 00000000..74d07fc6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Sndfile.h @@ -0,0 +1,1295 @@ +/* + * Sndfile.h + * --------- + * Purpose: Core class of the playback engine. Every song is represented by a CSoundFile object. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "SoundFilePlayConfig.h" +#include "MixerSettings.h" +#include "../common/misc_util.h" +#include "../common/mptRandom.h" +#include "../common/version.h" +#include <vector> +#include <bitset> +#include <set> +#include "Snd_defs.h" +#include "tuningbase.h" +#include "MIDIMacros.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/MIDIMapping.h" +#endif // MODPLUG_TRACKER + +#include "Mixer.h" +#include "Resampler.h" +#ifndef NO_REVERB +#include "../sounddsp/Reverb.h" +#endif +#ifndef NO_AGC +#include "../sounddsp/AGC.h" +#endif +#ifndef NO_DSP +#include "../sounddsp/DSP.h" +#endif +#ifndef NO_EQ +#include "../sounddsp/EQ.h" +#endif + +#include "modcommand.h" +#include "ModSample.h" +#include "ModInstrument.h" +#include "ModChannel.h" +#include "plugins/PluginStructs.h" +#include "RowVisitor.h" +#include "Message.h" +#include "pattern.h" +#include "patternContainer.h" +#include "ModSequence.h" + +#include "mpt/audio/span.hpp" + +#include "../common/FileReaderFwd.h" + + +OPENMPT_NAMESPACE_BEGIN + + +bool SettingCacheCompleteFileBeforeLoading(); + + +// ----------------------------------------------------------------------------- +// MODULAR ModInstrument FIELD ACCESS : body content in InstrumentExtensions.cpp +// ----------------------------------------------------------------------------- +#ifndef MODPLUG_NO_FILESAVE +void WriteInstrumentHeaderStructOrField(ModInstrument * input, std::ostream &file, uint32 only_this_code = -1 /* -1 for all */, uint16 fixedsize = 0); +#endif // !MODPLUG_NO_FILESAVE +bool ReadInstrumentHeaderField(ModInstrument * input, uint32 fcode, uint16 fsize, FileReader &file); +// -------------------------------------------------------------------------------------------- +// -------------------------------------------------------------------------------------------- + + +// Sample decompression routines in format-specific source files +void AMSUnpack(const int8 * const source, size_t sourceSize, void * const dest, const size_t destSize, char packCharacter); +uintptr_t DMFUnpack(FileReader &file, uint8 *psample, uint32 maxlen); + + +#ifdef LIBOPENMPT_BUILD +#ifndef NO_PLUGINS +class CVstPluginManager; +#endif +#endif + + +using PlayBehaviourSet = std::bitset<kMaxPlayBehaviours>; + +#ifdef MODPLUG_TRACKER + +// For WAV export (writing pattern positions to file) +struct PatternCuePoint +{ + uint64 offset; // offset in the file (in samples) + ORDERINDEX order; // which order is this? + bool processed; // has this point been processed by the main WAV render function yet? +}; + +#endif // MODPLUG_TRACKER + + +// Return values for GetLength() +struct GetLengthType +{ + double duration = 0.0; // Total time in seconds + ROWINDEX lastRow = ROWINDEX_INVALID; // Last parsed row (if no target is specified, this is the first row that is parsed twice, i.e. not the *last* played order) + ROWINDEX endRow = ROWINDEX_INVALID; // Last row before module loops (UNDEFINED if a target is specified) + ROWINDEX startRow = 0; // First row of parsed subsong + ORDERINDEX lastOrder = ORDERINDEX_INVALID; // Last parsed order (see lastRow remark) + ORDERINDEX endOrder = ORDERINDEX_INVALID; // Last order before module loops (UNDEFINED if a target is specified) + ORDERINDEX startOrder = 0; // First order of parsed subsong + bool targetReached = false; // True if the specified order/row combination or duration has been reached while going through the module +}; + + +struct SubSong +{ + double duration; + ROWINDEX startRow, endRow, loopStartRow; + ORDERINDEX startOrder, endOrder, loopStartOrder; + SEQUENCEINDEX sequence; +}; + + +// Target seek mode for GetLength() +struct GetLengthTarget +{ + ROWINDEX startRow; + ORDERINDEX startOrder; + SEQUENCEINDEX sequence; + + struct pos_type + { + ROWINDEX row; + ORDERINDEX order; + }; + + union + { + double time; + pos_type pos; + }; + + enum Mode + { + NoTarget, // Don't seek, i.e. return complete length of the first subsong. + GetAllSubsongs, // Same as NoTarget (i.e. get complete length), but returns the length of all sub songs + SeekPosition, // Seek to given pattern position. + SeekSeconds, // Seek to given time. + } mode; + + // Don't seek, i.e. return complete module length. + GetLengthTarget(bool allSongs = false) + { + mode = allSongs ? GetAllSubsongs : NoTarget; + sequence = SEQUENCEINDEX_INVALID; + startOrder = 0; + startRow = 0; + } + + // Seek to given pattern position if position is valid. + GetLengthTarget(ORDERINDEX order, ROWINDEX row) + { + mode = NoTarget; + sequence = SEQUENCEINDEX_INVALID; + startOrder = 0; + startRow = 0; + if(order != ORDERINDEX_INVALID && row != ROWINDEX_INVALID) + { + mode = SeekPosition; + pos.row = row; + pos.order = order; + } + } + + // Seek to given time if t is valid (i.e. not negative). + GetLengthTarget(double t) + { + mode = NoTarget; + sequence = SEQUENCEINDEX_INVALID; + startOrder = 0; + startRow = 0; + if(t >= 0.0) + { + mode = SeekSeconds; + time = t; + } + } + + // Set start position from which seeking should begin. + GetLengthTarget &StartPos(SEQUENCEINDEX seq, ORDERINDEX order, ROWINDEX row) + { + sequence = seq; + startOrder = order; + startRow = row; + return *this; + } +}; + + +// Reset mode for GetLength() +enum enmGetLengthResetMode +{ + // Never adjust global variables / mod parameters + eNoAdjust = 0x00, + // Mod parameters (such as global volume, speed, tempo, etc...) will always be memorized if the target was reached (i.e. they won't be reset to the previous values). If target couldn't be reached, they are reset to their default values. + eAdjust = 0x01, + // Same as above, but global variables will only be memorized if the target could be reached. This does *NOT* influence the visited rows vector - it will *ALWAYS* be adjusted in this mode. + eAdjustOnSuccess = 0x02 | eAdjust, + // Same as previous option, but will also try to emulate sample playback so that voices from previous patterns will sound when continuing playback at the target position. + eAdjustSamplePositions = 0x04 | eAdjustOnSuccess, + // Only adjust the visited rows state + eAdjustOnlyVisitedRows = 0x08, +}; + + +// Delete samples assigned to instrument +enum deleteInstrumentSamples +{ + deleteAssociatedSamples, + doNoDeleteAssociatedSamples, +}; + + +namespace Tuning { +class CTuningCollection; +} // namespace Tuning +using CTuningCollection = Tuning::CTuningCollection; +struct CModSpecifications; +class OPL; +class CModDoc; + + +///////////////////////////////////////////////////////////////////////// +// File edit history + +#define HISTORY_TIMER_PRECISION 18.2 + +struct FileHistory +{ + // Date when the file was loaded in the the tracker or created. + tm loadDate = {}; + // Time the file was open in the editor, in 1/18.2th seconds (frequency of a standard DOS timer, to keep compatibility with Impulse Tracker easy). + uint32 openTime = 0; + // Return the date as a (possibly truncated if not enough precision is available) ISO 8601 formatted date. + mpt::ustring AsISO8601() const; + // Returns true if the date component is valid. Some formats only store edit time, not edit date. + bool HasValidDate() const { return loadDate.tm_mday != 0; } +}; + + +struct TimingInfo +{ + double InputLatency = 0.0; // seconds + double OutputLatency = 0.0; // seconds + int64 StreamFrames = 0; + uint64 SystemTimestamp = 0; // nanoseconds + double Speed = 1.0; +}; + + +enum class ModMessageHeuristicOrder +{ + Instruments, + Samples, + InstrumentsSamples, + SamplesInstruments, + BothInstrumentsSamples, + BothSamplesInstruments, + Default = InstrumentsSamples, +}; + +struct ModFormatDetails +{ + mpt::ustring formatName; // "FastTracker 2" + mpt::ustring type; // "xm" + mpt::ustring madeWithTracker; // "OpenMPT 1.28.01.00" + mpt::ustring originalFormatName; // "FastTracker 2" in the case of converted formats like MO3 or GDM + mpt::ustring originalType; // "xm" in the case of converted formats like MO3 or GDM + mpt::Charset charset = mpt::Charset::UTF8; +}; + + +class IAudioTarget +{ +protected: + virtual ~IAudioTarget() = default; +public: + virtual void Process(mpt::audio_span_interleaved<MixSampleInt> buffer) = 0; + virtual void Process(mpt::audio_span_interleaved<MixSampleFloat> buffer) = 0; +}; + + +class IAudioSource +{ +public: + virtual ~IAudioSource() = default; +public: + virtual void Process(mpt::audio_span_planar<MixSampleInt> buffer) = 0; + virtual void Process(mpt::audio_span_planar<MixSampleFloat> buffer) = 0; +}; + + +class IMonitorInput +{ +public: + virtual ~IMonitorInput() = default; +public: + virtual void Process(mpt::audio_span_planar<const MixSampleInt> buffer) = 0; + virtual void Process(mpt::audio_span_planar<const MixSampleFloat> buffer) = 0; +}; + + +class IMonitorOutput +{ +public: + virtual ~IMonitorOutput() = default; +public: + virtual void Process(mpt::audio_span_interleaved<const MixSampleInt> buffer) = 0; + virtual void Process(mpt::audio_span_interleaved<const MixSampleFloat> buffer) = 0; +}; + + +class AudioSourceNone + : public IAudioSource +{ +public: + void Process(mpt::audio_span_planar<MixSampleInt> buffer) override + { + for(std::size_t channel = 0; channel < buffer.size_channels(); ++channel) + { + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + buffer(channel, frame) = 0; + } + } + } + void Process(mpt::audio_span_planar<MixSampleFloat> buffer) override + { + for(std::size_t channel = 0; channel < buffer.size_channels(); ++channel) + { + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + buffer(channel, frame) = MixSampleFloat(0.0); + } + } + } +}; + + +using NoteName = mpt::uchar[4]; + + +class CSoundFile +{ + friend class GetLengthMemory; + +public: +#ifdef MODPLUG_TRACKER + void ChangeModTypeTo(const MODTYPE newType, bool adjust = true); +#endif // MODPLUG_TRACKER + + // Returns value in seconds. If given position won't be played at all, returns -1. + // If updateVars is true, the state of various playback variables will be updated according to the playback position. + // If updateSamplePos is also true, the sample positions of samples still playing from previous patterns will be kept in sync. + double GetPlaybackTimeAt(ORDERINDEX ord, ROWINDEX row, bool updateVars, bool updateSamplePos); + + std::vector<SubSong> GetAllSubSongs(); + + //Tuning--> +public: + static std::unique_ptr<CTuning> CreateTuning12TET(const mpt::ustring &name); + static CTuning *GetDefaultTuning() {return nullptr;} + CTuningCollection& GetTuneSpecificTunings() {return *m_pTuningsTuneSpecific;} + + mpt::ustring GetNoteName(const ModCommand::NOTE note, const INSTRUMENTINDEX inst) const; + mpt::ustring GetNoteName(const ModCommand::NOTE note) const; + static mpt::ustring GetNoteName(const ModCommand::NOTE note, const NoteName *noteNames); +#ifdef MODPLUG_TRACKER +public: + static void SetDefaultNoteNames(); + static const NoteName *GetDefaultNoteNames(); + static mpt::ustring GetDefaultNoteName(int note) // note = [0..11] + { + return m_NoteNames[note]; + } +private: + static const NoteName *m_NoteNames; +#else +private: + const NoteName *m_NoteNames; +#endif + +private: + CTuningCollection* m_pTuningsTuneSpecific = nullptr; + +#ifdef MODPLUG_TRACKER +public: + CMIDIMapper& GetMIDIMapper() {return m_MIDIMapper;} + const CMIDIMapper& GetMIDIMapper() const {return m_MIDIMapper;} +private: + CMIDIMapper m_MIDIMapper; + +#endif // MODPLUG_TRACKER + +private: //Misc private methods. + static void SetModSpecsPointer(const CModSpecifications* &pModSpecs, const MODTYPE type); + +private: //Misc data + const CModSpecifications *m_pModSpecs; + +private: + // Interleaved Front Mix Buffer (Also room for interleaved rear mix) + mixsample_t MixSoundBuffer[MIXBUFFERSIZE * 4]; + mixsample_t MixRearBuffer[MIXBUFFERSIZE * 2]; + // Non-interleaved plugin processing buffer + float MixFloatBuffer[2][MIXBUFFERSIZE]; + mixsample_t MixInputBuffer[NUMMIXINPUTBUFFERS][MIXBUFFERSIZE]; + + // End-of-sample pop reduction tail level + mixsample_t m_dryLOfsVol = 0, m_dryROfsVol = 0; + mixsample_t m_surroundLOfsVol = 0, m_surroundROfsVol = 0; + +public: + MixerSettings m_MixerSettings; + CResampler m_Resampler; +#ifndef NO_REVERB + mixsample_t ReverbSendBuffer[MIXBUFFERSIZE * 2]; + mixsample_t m_RvbROfsVol = 0, m_RvbLOfsVol = 0; + CReverb m_Reverb; +#endif +#ifndef NO_DSP + CSurround m_Surround; + CMegaBass m_MegaBass; +#endif +#ifndef NO_EQ + CEQ m_EQ; +#endif +#ifndef NO_AGC + CAGC m_AGC; +#endif +#ifndef NO_DSP + BitCrush m_BitCrush; +#endif + + using samplecount_t = uint32; // Number of rendered samples + + static constexpr uint32 TICKS_ROW_FINISHED = uint32_max - 1u; + +public: // for Editing +#ifdef MODPLUG_TRACKER + CModDoc *m_pModDoc = nullptr; // Can be a null pointer for example when previewing samples from the treeview. +#endif // MODPLUG_TRACKER + Enum<MODTYPE> m_nType; +private: + MODCONTAINERTYPE m_ContainerType = MOD_CONTAINERTYPE_NONE; +public: + CHANNELINDEX m_nChannels = 0; + SAMPLEINDEX m_nSamples = 0; + INSTRUMENTINDEX m_nInstruments = 0; + uint32 m_nDefaultSpeed, m_nDefaultGlobalVolume; + TEMPO m_nDefaultTempo; + FlagSet<SongFlags> m_SongFlags; + CHANNELINDEX m_nMixChannels = 0; +private: + CHANNELINDEX m_nMixStat; +public: + ROWINDEX m_nDefaultRowsPerBeat, m_nDefaultRowsPerMeasure; // default rows per beat and measure for this module + TempoMode m_nTempoMode = TempoMode::Classic; + +#ifdef MODPLUG_TRACKER + // Lock playback between two rows. Lock is active if lock start != ROWINDEX_INVALID). + ROWINDEX m_lockRowStart = ROWINDEX_INVALID, m_lockRowEnd = ROWINDEX_INVALID; + // Lock playback between two orders. Lock is active if lock start != ORDERINDEX_INVALID). + ORDERINDEX m_lockOrderStart = ORDERINDEX_INVALID, m_lockOrderEnd = ORDERINDEX_INVALID; +#endif // MODPLUG_TRACKER + + uint32 m_nSamplePreAmp, m_nVSTiVolume; + uint32 m_OPLVolumeFactor; // 16.16 + static constexpr uint32 m_OPLVolumeFactorScale = 1 << 16; + + constexpr bool IsGlobalVolumeUnset() const noexcept { return IsFirstTick(); } +#ifndef MODPLUG_TRACKER + uint32 m_nFreqFactor = 65536; // Pitch shift factor (65536 = no pitch shifting). Only used in libopenmpt (openmpt::ext::interactive::set_pitch_factor) + uint32 m_nTempoFactor = 65536; // Tempo factor (65536 = no tempo adjustment). Only used in libopenmpt (openmpt::ext::interactive::set_tempo_factor) +#endif + + // Row swing factors for modern tempo mode + TempoSwing m_tempoSwing; + + // Min Period = highest possible frequency, Max Period = lowest possible frequency for current format + // Note: Period is an Amiga metric that is inverse to frequency. + // Periods in MPT are 4 times as fine as Amiga periods because of extra fine frequency slides (introduced in the S3M format). + int32 m_nMinPeriod, m_nMaxPeriod; + + ResamplingMode m_nResampling; // Resampling mode (if overriding the globally set resampling) + int32 m_nRepeatCount = 0; // -1 means repeat infinitely. + ORDERINDEX m_nMaxOrderPosition; + ModChannelSettings ChnSettings[MAX_BASECHANNELS]; // Initial channels settings + CPatternContainer Patterns; + ModSequenceSet Order; // Pattern sequences (order lists) +protected: + ModSample Samples[MAX_SAMPLES]; +public: + ModInstrument *Instruments[MAX_INSTRUMENTS]; // Instrument Headers + MIDIMacroConfig m_MidiCfg; // MIDI Macro config table +#ifndef NO_PLUGINS + SNDMIXPLUGIN m_MixPlugins[MAX_MIXPLUGINS]; // Mix plugins + uint32 m_loadedPlugins = 0; // Not a PLUGINDEX because number of loaded plugins may exceed MAX_MIXPLUGINS during MIDI conversion +#endif + mpt::charbuf<MAX_SAMPLENAME> m_szNames[MAX_SAMPLES]; // Sample names + + Version m_dwCreatedWithVersion; + Version m_dwLastSavedWithVersion; + + PlayBehaviourSet m_playBehaviour; + +protected: + + mpt::fast_prng m_PRNG; + inline mpt::fast_prng & AccessPRNG() const { return const_cast<CSoundFile*>(this)->m_PRNG; } + inline mpt::fast_prng & AccessPRNG() { return m_PRNG; } + +protected: + // Mix level stuff + CSoundFilePlayConfig m_PlayConfig; + MixLevels m_nMixLevels; + +public: + struct PlayState + { + friend class CSoundFile; + + public: + samplecount_t m_lTotalSampleCount = 0; // Total number of rendered samples + protected: + samplecount_t m_nBufferCount = 0; // Remaining number samples to render for this tick + double m_dBufferDiff = 0.0; // Modern tempo rounding error compensation + + public: + uint32 m_nTickCount = 0; // Current tick being processed + protected: + uint32 m_nPatternDelay = 0; // Pattern delay (rows) + uint32 m_nFrameDelay = 0; // Fine pattern delay (ticks) + public: + uint32 m_nSamplesPerTick = 0; + ROWINDEX m_nCurrentRowsPerBeat = 0; // Current time signature + ROWINDEX m_nCurrentRowsPerMeasure = 0; // Current time signature + uint32 m_nMusicSpeed = 0; // Current speed + TEMPO m_nMusicTempo; // Current tempo + + // Playback position + ROWINDEX m_nRow = 0; // Current row being processed + ROWINDEX m_nNextRow = 0; // Next row to process + protected: + ROWINDEX m_nextPatStartRow = 0; // For FT2's E60 bug + ROWINDEX m_breakRow = 0; // Candidate target row for pattern break + ROWINDEX m_patLoopRow = 0; // Candidate target row for pattern loop + ORDERINDEX m_posJump = 0; // Candidate target order for position jump + + public: + PATTERNINDEX m_nPattern = 0; // Current pattern being processed + ORDERINDEX m_nCurrentOrder = 0; // Current order being processed + ORDERINDEX m_nNextOrder = 0; // Next order to process + ORDERINDEX m_nSeqOverride = ORDERINDEX_INVALID; // Queued order to be processed next, regardless of what order would normally follow + + // Global volume + public: + int32 m_nGlobalVolume = MAX_GLOBAL_VOLUME; // Current global volume (0...MAX_GLOBAL_VOLUME) + protected: + int32 m_nSamplesToGlobalVolRampDest = 0, m_nGlobalVolumeRampAmount = 0, + m_nGlobalVolumeDestination = 0; // Global volume ramping + int32 m_lHighResRampingGlobalVolume = 0; // Global volume ramping + + public: + bool m_bPositionChanged = true; // Report to plugins that we jumped around in the module + + public: + CHANNELINDEX ChnMix[MAX_CHANNELS]; // Index of channels in Chn to be actually mixed + ModChannel Chn[MAX_CHANNELS]; // Mixing channels... First m_nChannels channels are master channels (i.e. they are never NNA channels)! + + struct MIDIMacroEvaluationResults + { + std::map<PLUGINDEX, float> pluginDryWetRatio; + std::map<std::pair<PLUGINDEX, PlugParamIndex>, PlugParamValue> pluginParameter; + }; + + std::vector<uint8> m_midiMacroScratchSpace; + std::optional<MIDIMacroEvaluationResults> m_midiMacroEvaluationResults; + + public: + PlayState(); + + void ResetGlobalVolumeRamping() + { + m_lHighResRampingGlobalVolume = m_nGlobalVolume << VOLUMERAMPPRECISION; + m_nGlobalVolumeDestination = m_nGlobalVolume; + m_nSamplesToGlobalVolRampDest = 0; + m_nGlobalVolumeRampAmount = 0; + } + + constexpr uint32 TicksOnRow() const noexcept + { + return (m_nMusicSpeed + m_nFrameDelay) * std::max(m_nPatternDelay, uint32(1)); + } + }; + + PlayState m_PlayState; + +protected: + // For handling backwards jumps and stuff to prevent infinite loops when counting the mod length or rendering to wav. + RowVisitor m_visitedRows; + +public: +#ifdef MODPLUG_TRACKER + std::bitset<MAX_BASECHANNELS> m_bChannelMuteTogglePending; + std::bitset<MAX_MIXPLUGINS> m_pluginDryWetRatioChanged; // Dry/Wet ratio was changed by playback code (e.g. through MIDI macro), need to update UI + + std::vector<PatternCuePoint> *m_PatternCuePoints = nullptr; // For WAV export (writing pattern positions to file) + std::vector<SmpLength> *m_SamplePlayLengths = nullptr; // For storing the maximum play length of each sample for automatic sample trimming +#endif // MODPLUG_TRACKER + + std::unique_ptr<OPL> m_opl; + +public: +#ifdef LIBOPENMPT_BUILD +#ifndef NO_PLUGINS + std::unique_ptr<CVstPluginManager> m_PluginManager; +#endif +#endif + +public: + std::string m_songName; + mpt::ustring m_songArtist; + SongMessage m_songMessage; + ModFormatDetails m_modFormat; + +protected: + std::vector<FileHistory> m_FileHistory; // File edit history +public: + std::vector<FileHistory> &GetFileHistory() { return m_FileHistory; } + const std::vector<FileHistory> &GetFileHistory() const { return m_FileHistory; } + +#ifdef MPT_EXTERNAL_SAMPLES + // MPTM external on-disk sample paths +protected: + std::vector<mpt::PathString> m_samplePaths; + +public: + void SetSamplePath(SAMPLEINDEX smp, mpt::PathString filename) { if(m_samplePaths.size() < smp) m_samplePaths.resize(smp); m_samplePaths[smp - 1] = std::move(filename); } + void ResetSamplePath(SAMPLEINDEX smp) { if(m_samplePaths.size() >= smp) m_samplePaths[smp - 1] = mpt::PathString(); Samples[smp].uFlags.reset(SMP_KEEPONDISK | SMP_MODIFIED);} + mpt::PathString GetSamplePath(SAMPLEINDEX smp) const { if(m_samplePaths.size() >= smp) return m_samplePaths[smp - 1]; else return mpt::PathString(); } + bool SampleHasPath(SAMPLEINDEX smp) const { if(m_samplePaths.size() >= smp) return !m_samplePaths[smp - 1].empty(); else return false; } + bool IsExternalSampleMissing(SAMPLEINDEX smp) const { return Samples[smp].uFlags[SMP_KEEPONDISK] && !Samples[smp].HasSampleData(); } + + bool LoadExternalSample(SAMPLEINDEX smp, const mpt::PathString &filename); +#endif // MPT_EXTERNAL_SAMPLES + + bool m_bIsRendering = false; + TimingInfo m_TimingInfo; // only valid if !m_bIsRendering + +private: + // logging + ILog *m_pCustomLog = nullptr; + +public: + CSoundFile(); + CSoundFile(const CSoundFile &) = delete; + CSoundFile & operator=(const CSoundFile &) = delete; + ~CSoundFile(); + +public: + // logging + void SetCustomLog(ILog *pLog) { m_pCustomLog = pLog; } + void AddToLog(LogLevel level, const mpt::ustring &text) const; + +public: + + enum ModLoadingFlags + { + onlyVerifyHeader = 0x00, + loadPatternData = 0x01, // If unset, advise loaders to not process any pattern data (if possible) + loadSampleData = 0x02, // If unset, advise loaders to not process any sample data (if possible) + loadPluginData = 0x04, // If unset, plugin data is not loaded (and as a consequence, plugins are not instanciated). + loadPluginInstance = 0x08, // If unset, plugins are not instanciated. + skipContainer = 0x10, + skipModules = 0x20, + + // Shortcuts + loadCompleteModule = loadSampleData | loadPatternData | loadPluginData | loadPluginInstance, + loadNoPatternOrPluginData = loadSampleData, + loadNoPluginInstance = loadSampleData | loadPatternData | loadPluginData, + }; + + #define PROBE_RECOMMENDED_SIZE 2048u + + static constexpr std::size_t ProbeRecommendedSize = PROBE_RECOMMENDED_SIZE; + + enum ProbeFlags + { + ProbeModules = 0x1, + ProbeContainers = 0x2, + + ProbeFlagsDefault = ProbeModules | ProbeContainers, + ProbeFlagsNone = 0 + }; + + enum ProbeResult + { + ProbeSuccess = 1, + ProbeFailure = 0, + ProbeWantMoreData = -1 + }; + + static ProbeResult ProbeAdditionalSize(MemoryFileReader &file, const uint64 *pfilesize, uint64 minimumAdditionalSize); + + static ProbeResult Probe(ProbeFlags flags, mpt::span<const std::byte> data, const uint64 *pfilesize); + +public: + +#ifdef MODPLUG_TRACKER + // Get parent CModDoc. Can be nullptr if previewing from tree view, and is always nullptr if we're not actually compiling OpenMPT. + CModDoc *GetpModDoc() const noexcept { return m_pModDoc; } +#endif // MODPLUG_TRACKER + + bool Create(FileReader file, ModLoadingFlags loadFlags = loadCompleteModule, CModDoc *pModDoc = nullptr); +private: + bool CreateInternal(FileReader file, ModLoadingFlags loadFlags); + +public: + bool Destroy(); + Enum<MODTYPE> GetType() const noexcept { return m_nType; } + + MODCONTAINERTYPE GetContainerType() const noexcept { return m_ContainerType; } + + // rough heuristic, could be improved + mpt::Charset GetCharsetFile() const // 8bit string encoding of strings in the on-disk file + { + return m_modFormat.charset; + } + mpt::Charset GetCharsetInternal() const // 8bit string encoding of strings internal in CSoundFile + { + #if defined(MODPLUG_TRACKER) + return mpt::Charset::Locale; + #else // MODPLUG_TRACKER + return GetCharsetFile(); + #endif // MODPLUG_TRACKER + } + + ModMessageHeuristicOrder GetMessageHeuristic() const; + + void SetPreAmp(uint32 vol); + uint32 GetPreAmp() const noexcept { return m_MixerSettings.m_nPreAmp; } + + void SetMixLevels(MixLevels levels); + MixLevels GetMixLevels() const noexcept { return m_nMixLevels; } + const CSoundFilePlayConfig &GetPlayConfig() const noexcept { return m_PlayConfig; } + + constexpr INSTRUMENTINDEX GetNumInstruments() const noexcept { return m_nInstruments; } + constexpr SAMPLEINDEX GetNumSamples() const noexcept { return m_nSamples; } + constexpr PATTERNINDEX GetCurrentPattern() const noexcept { return m_PlayState.m_nPattern; } + constexpr ORDERINDEX GetCurrentOrder() const noexcept { return m_PlayState.m_nCurrentOrder; } + constexpr CHANNELINDEX GetNumChannels() const noexcept { return m_nChannels; } + + constexpr bool CanAddMoreSamples(SAMPLEINDEX amount = 1) const noexcept { return (amount < MAX_SAMPLES) && m_nSamples < (MAX_SAMPLES - amount); } + constexpr bool CanAddMoreInstruments(INSTRUMENTINDEX amount = 1) const noexcept { return (amount < MAX_INSTRUMENTS) && m_nInstruments < (MAX_INSTRUMENTS - amount); } + +#ifndef NO_PLUGINS + IMixPlugin* GetInstrumentPlugin(INSTRUMENTINDEX instr) const noexcept; +#endif + const CModSpecifications& GetModSpecifications() const {return *m_pModSpecs;} + static const CModSpecifications& GetModSpecifications(const MODTYPE type); + + static ChannelFlags GetChannelMuteFlag(); + +#ifdef MODPLUG_TRACKER + void PatternTranstionChnSolo(const CHANNELINDEX chnIndex); + void PatternTransitionChnUnmuteAll(); + +protected: + void HandlePatternTransitionEvents(); +#endif // MODPLUG_TRACKER + +public: + double GetCurrentBPM() const; + void DontLoopPattern(PATTERNINDEX nPat, ROWINDEX nRow = 0); + CHANNELINDEX GetMixStat() const { return m_nMixStat; } + void ResetMixStat() { m_nMixStat = 0; } + void ResetPlayPos(); + void SetCurrentOrder(ORDERINDEX nOrder); + std::string GetTitle() const { return m_songName; } + bool SetTitle(const std::string &newTitle); // Return true if title was changed. + const char *GetSampleName(SAMPLEINDEX nSample) const; + const char *GetInstrumentName(INSTRUMENTINDEX nInstr) const; + uint32 GetMusicSpeed() const { return m_PlayState.m_nMusicSpeed; } + TEMPO GetMusicTempo() const { return m_PlayState.m_nMusicTempo; } + constexpr bool IsFirstTick() const noexcept { return (m_PlayState.m_lTotalSampleCount == 0); } + + // Get song duration in various cases: total length, length to specific order & row, etc. + std::vector<GetLengthType> GetLength(enmGetLengthResetMode adjustMode, GetLengthTarget target = GetLengthTarget()); + +public: + void RecalculateSamplesPerTick(); + double GetRowDuration(TEMPO tempo, uint32 speed) const; + uint32 GetTickDuration(PlayState &playState) const; + + // A repeat count value of -1 means infinite loop + void SetRepeatCount(int n) { m_nRepeatCount = n; } + int GetRepeatCount() const { return m_nRepeatCount; } + bool IsPaused() const { return m_SongFlags[SONG_PAUSED | SONG_STEP]; } // Added SONG_STEP as it seems to be desirable in most cases to check for this as well. + void LoopPattern(PATTERNINDEX nPat, ROWINDEX nRow = 0); + + bool InitChannel(CHANNELINDEX nChn); + void InitAmigaResampler(); + + void InitOPL(); + static constexpr bool SupportsOPL(MODTYPE type) noexcept { return type & (MOD_TYPE_S3M | MOD_TYPE_MPT); } + bool SupportsOPL() const noexcept { return SupportsOPL(m_nType); } + +#if !defined(MPT_WITH_ANCIENT) + static ProbeResult ProbeFileHeaderMMCMP(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPP20(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderXPK(MemoryFileReader file, const uint64 *pfilesize); +#endif // !MPT_WITH_ANCIENT + static ProbeResult ProbeFileHeaderUMX(MemoryFileReader file, const uint64* pfilesize); + + static ProbeResult ProbeFileHeader669(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderAM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderAMF_Asylum(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderAMF_DSMI(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderAMS(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderAMS2(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderC67(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDBM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDTM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDIGI(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDMF(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDSM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderDSym(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderFAR(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderFMT(MemoryFileReader file, const uint64* pfilesize); + static ProbeResult ProbeFileHeaderGDM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderICE(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderIMF(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderIT(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderITP(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderJ2B(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMUS_KM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderM15(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMDL(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMED(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMO3(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMOD(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMT2(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderMTM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderOKT(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPLM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPSM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPSM16(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPT36(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderPTM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderS3M(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderSFX(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderSTM(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderSTP(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderSTX(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderSymMOD(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderULT(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderXM(MemoryFileReader file, const uint64 *pfilesize); + + static ProbeResult ProbeFileHeaderMID(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderUAX(MemoryFileReader file, const uint64 *pfilesize); + static ProbeResult ProbeFileHeaderWAV(MemoryFileReader file, const uint64 *pfilesize); + + // Module Loaders + bool Read669(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadAM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadAMF_Asylum(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadAMF_DSMI(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadAMS(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadAMS2(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadC67(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDBM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDIGI(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDMF(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDSM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadDSym(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadFAR(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadFMT(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadGDM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadICE(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadIMF(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadIT(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadITP(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadJ2B(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMUS_KM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadM15(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMDL(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMED(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMO3(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMOD(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMT2(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadMTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadOKT(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadPLM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadPSM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadPSM16(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadPT36(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadPTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadS3M(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSFX(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSTM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSTP(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSTX(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSymMOD(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadULT(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadXM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + + bool ReadMID(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadUAX(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadWAV(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + + static std::vector<const char *> GetSupportedExtensions(bool otherFormats); + static bool IsExtensionSupported(std::string_view ext); // UTF8, casing of ext is ignored + static mpt::ustring ModContainerTypeToString(MODCONTAINERTYPE containertype); + static mpt::ustring ModContainerTypeToTracker(MODCONTAINERTYPE containertype); + + /// <summary> + /// From version: 0.7.0 + /// Hakan DANISIK + /// </summary> + /// <param name="extension"></param> + /// <returns></returns> + static std::string ExtensionToTracker(std::string extension); + + // Repair non-standard stuff in modules saved with previous ModPlug versions + void UpgradeModule(); + + // Save Functions +#ifndef MODPLUG_NO_FILESAVE + bool SaveXM(std::ostream &f, bool compatibilityExport = false); + bool SaveS3M(std::ostream &f) const; + bool SaveMod(std::ostream &f) const; + bool SaveIT(std::ostream &f, const mpt::PathString &filename, bool compatibilityExport = false); + uint32 SaveMixPlugins(std::ostream *file=nullptr, bool bUpdate=true); + void WriteInstrumentPropertyForAllInstruments(uint32 code, uint16 size, std::ostream &f, INSTRUMENTINDEX nInstruments) const; + void SaveExtendedInstrumentProperties(INSTRUMENTINDEX nInstruments, std::ostream &f) const; + void SaveExtendedSongProperties(std::ostream &f) const; +#endif // MODPLUG_NO_FILESAVE + void LoadExtendedSongProperties(FileReader &file, bool ignoreChannelCount, bool* pInterpretMptMade = nullptr); + void LoadMPTMProperties(FileReader &file, uint16 cwtv); + + static mpt::ustring GetSchismTrackerVersion(uint16 cwtv, uint32 reserved); + + // Reads extended instrument properties(XM/IT/MPTM). + // Returns true if extended instrument properties were found. + bool LoadExtendedInstrumentProperties(FileReader &file); + + void SetDefaultPlaybackBehaviour(MODTYPE type); + static PlayBehaviourSet GetSupportedPlaybackBehaviour(MODTYPE type); + static PlayBehaviourSet GetDefaultPlaybackBehaviour(MODTYPE type); + + // MOD Convert function + MODTYPE GetBestSaveFormat() const; + static void ConvertModCommand(ModCommand &m); + static void S3MConvert(ModCommand &m, bool fromIT); + void S3MSaveConvert(uint8 &command, uint8 ¶m, bool toIT, bool compatibilityExport = false) const; + void ModSaveCommand(uint8 &command, uint8 ¶m, const bool toXM, const bool compatibilityExport = false) const; + static void ReadMODPatternEntry(FileReader &file, ModCommand &m); + static void ReadMODPatternEntry(const std::array<uint8, 4> data, ModCommand &m); + + void SetupMODPanning(bool bForceSetup = false); // Setup LRRL panning, max channel volume + +public: + // Real-time sound functions + void SuspendPlugins(); + void ResumePlugins(); + void StopAllVsti(); + void RecalculateGainForAllPlugs(); + void ResetChannels(); + samplecount_t Read(samplecount_t count, IAudioTarget &target) { AudioSourceNone source; return Read(count, target, source); } + samplecount_t Read( + samplecount_t count, + IAudioTarget &target, + IAudioSource &source, + std::optional<std::reference_wrapper<IMonitorOutput>> outputMonitor = std::nullopt, + std::optional<std::reference_wrapper<IMonitorInput>> inputMonitor = std::nullopt + ); + samplecount_t ReadOneTick(); +private: + void CreateStereoMix(int count); +public: + bool FadeSong(uint32 msec); +private: + void ProcessDSP(uint32 countChunk); + void ProcessPlugins(uint32 nCount); + void ProcessInputChannels(IAudioSource &source, std::size_t countChunk); +public: + samplecount_t GetTotalSampleCount() const { return m_PlayState.m_lTotalSampleCount; } + bool HasPositionChanged() { bool b = m_PlayState.m_bPositionChanged; m_PlayState.m_bPositionChanged = false; return b; } + bool IsRenderingToDisc() const { return m_bIsRendering; } + + void PrecomputeSampleLoops(bool updateChannels = false); + +public: + // Mixer Config + void SetMixerSettings(const MixerSettings &mixersettings); + void SetResamplerSettings(const CResamplerSettings &resamplersettings); + void InitPlayer(bool bReset=false); + void SetDspEffects(uint32 DSPMask); + uint32 GetSampleRate() const { return m_MixerSettings.gdwMixingFreq; } +#ifndef NO_EQ + void SetEQGains(const uint32 *pGains, const uint32 *pFreqs, bool bReset = false) { m_EQ.SetEQGains(pGains, pFreqs, bReset, m_MixerSettings.gdwMixingFreq); } // 0=-12dB, 32=+12dB +#endif // NO_EQ +public: + bool ReadNote(); + bool ProcessRow(); + bool ProcessEffects(); + std::pair<bool, bool> NextRow(PlayState &playState, const bool breakRow) const; + void SetupNextRow(PlayState &playState, const bool patternLoop) const; + CHANNELINDEX GetNNAChannel(CHANNELINDEX nChn) const; + CHANNELINDEX CheckNNA(CHANNELINDEX nChn, uint32 instr, int note, bool forceCut); + void NoteChange(ModChannel &chn, int note, bool bPorta = false, bool bResetEnv = true, bool bManual = false, CHANNELINDEX channelHint = CHANNELINDEX_INVALID) const; + void InstrumentChange(ModChannel &chn, uint32 instr, bool bPorta = false, bool bUpdVol = true, bool bResetEnv = true) const; + void ApplyInstrumentPanning(ModChannel &chn, const ModInstrument *instr, const ModSample *smp) const; + uint32 CalculateXParam(PATTERNINDEX pat, ROWINDEX row, CHANNELINDEX chn, uint32 *extendedRows = nullptr) const; + + // Channel Effects + void KeyOff(ModChannel &chn) const; + // Global Effects + void SetTempo(TEMPO param, bool setAsNonModcommand = false); + void SetSpeed(PlayState &playState, uint32 param) const; + static TEMPO ConvertST2Tempo(uint8 tempo); + + void ProcessRamping(ModChannel &chn) const; + +protected: + // Global variable initializer for loader functions + void SetType(MODTYPE type); + void InitializeGlobals(MODTYPE type = MOD_TYPE_NONE); + void InitializeChannels(); + + // Channel effect processing + int GetVibratoDelta(int type, int position) const; + + void ProcessVolumeSwing(ModChannel &chn, int &vol) const; + void ProcessPanningSwing(ModChannel &chn) const; + void ProcessTremolo(ModChannel &chn, int &vol) const; + void ProcessTremor(CHANNELINDEX nChn, int &vol); + + bool IsEnvelopeProcessed(const ModChannel &chn, EnvelopeType env) const; + void ProcessVolumeEnvelope(ModChannel &chn, int &vol) const; + void ProcessPanningEnvelope(ModChannel &chn) const; + int ProcessPitchFilterEnvelope(ModChannel &chn, int32 &period) const; + + void IncrementEnvelopePosition(ModChannel &chn, EnvelopeType envType) const; + void IncrementEnvelopePositions(ModChannel &chn) const; + + void ProcessInstrumentFade(ModChannel &chn, int &vol) const; + + static void ProcessPitchPanSeparation(int32 &pan, int note, const ModInstrument &instr); + void ProcessPanbrello(ModChannel &chn) const; + + void ProcessArpeggio(CHANNELINDEX nChn, int32 &period, Tuning::NOTEINDEXTYPE &arpeggioSteps); + void ProcessVibrato(CHANNELINDEX nChn, int32 &period, Tuning::RATIOTYPE &vibratoFactor); + void ProcessSampleAutoVibrato(ModChannel &chn, int32 &period, Tuning::RATIOTYPE &vibratoFactor, int &nPeriodFrac) const; + + std::pair<SamplePosition, uint32> GetChannelIncrement(const ModChannel &chn, uint32 period, int periodFrac) const; + +protected: + // Type of panning command + enum PanningType + { + Pan4bit = 4, + Pan6bit = 6, + Pan8bit = 8, + }; + // Channel Effects + void UpdateS3MEffectMemory(ModChannel &chn, ModCommand::PARAM param) const; + void PortamentoUp(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular = false); + void PortamentoDown(CHANNELINDEX nChn, ModCommand::PARAM param, const bool doFinePortamentoAsRegular = false); + void MidiPortamento(CHANNELINDEX nChn, int param, bool doFineSlides); + void FinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const; + void FinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const; + void ExtraFinePortamentoUp(ModChannel &chn, ModCommand::PARAM param) const; + void ExtraFinePortamentoDown(ModChannel &chn, ModCommand::PARAM param) const; + void PortamentoMPT(ModChannel &chn, int); + void PortamentoFineMPT(ModChannel &chn, int); + void PortamentoExtraFineMPT(ModChannel &chn, int); + void SetFinetune(CHANNELINDEX channel, PlayState &playState, bool isSmooth) const; + void NoteSlide(ModChannel &chn, uint32 param, bool slideUp, bool retrig) const; + std::pair<uint16, bool> GetVolCmdTonePorta(const ModCommand &m, uint32 startTick) const; + void TonePortamento(ModChannel &chn, uint16 param) const; + void Vibrato(ModChannel &chn, uint32 param) const; + void FineVibrato(ModChannel &chn, uint32 param) const; + void VolumeSlide(ModChannel &chn, ModCommand::PARAM param) const; + void PanningSlide(ModChannel &chn, ModCommand::PARAM param, bool memory = true) const; + void ChannelVolSlide(ModChannel &chn, ModCommand::PARAM param) const; + void FineVolumeUp(ModChannel &chn, ModCommand::PARAM param, bool volCol) const; + void FineVolumeDown(ModChannel &chn, ModCommand::PARAM param, bool volCol) const; + void Tremolo(ModChannel &chn, uint32 param) const; + void Panbrello(ModChannel &chn, uint32 param) const; + void Panning(ModChannel &chn, uint32 param, PanningType panBits) const; + void RetrigNote(CHANNELINDEX nChn, int param, int offset = 0); + void ProcessSampleOffset(ModChannel &chn, CHANNELINDEX nChn, const PlayState &playState) const; + void SampleOffset(ModChannel &chn, SmpLength param) const; + void ReverseSampleOffset(ModChannel &chn, ModCommand::PARAM param) const; + void DigiBoosterSampleReverse(ModChannel &chn, ModCommand::PARAM param) const; + void HandleDigiSamplePlayDirection(PlayState &state, CHANNELINDEX chn) const; + void NoteCut(CHANNELINDEX nChn, uint32 nTick, bool cutSample); + void PatternLoop(PlayState &state, ModChannel &chn, ModCommand::PARAM param) const; + bool HandleNextRow(PlayState &state, const ModSequence &order, bool honorPatternLoop) const; + void ExtendedMODCommands(CHANNELINDEX nChn, ModCommand::PARAM param); + void ExtendedS3MCommands(CHANNELINDEX nChn, ModCommand::PARAM param); + void ExtendedChannelEffect(ModChannel &chn, uint32 param); + void InvertLoop(ModChannel &chn); + void PositionJump(PlayState &state, CHANNELINDEX chn) const; + ROWINDEX PatternBreak(PlayState &state, CHANNELINDEX chn, uint8 param) const; + void GlobalVolSlide(ModCommand::PARAM param, uint8 &nOldGlobalVolSlide); + + void ProcessMacroOnChannel(CHANNELINDEX nChn); + void ProcessMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const MIDIMacroConfigData::Macro ¯o, uint8 param = 0, PLUGINDEX plugin = 0); + void ParseMIDIMacro(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const char> macro, mpt::span<uint8> &out, uint8 param = 0, PLUGINDEX plugin = 0) const; + static float CalculateSmoothParamChange(const PlayState &playState, float currentValue, float param); + void SendMIDIData(PlayState &playState, CHANNELINDEX nChn, bool isSmooth, const mpt::span<const uint8> macro, PLUGINDEX plugin); + void SendMIDINote(CHANNELINDEX chn, uint16 note, uint16 volume); + + int SetupChannelFilter(ModChannel &chn, bool bReset, int envModifier = 256) const; + + // Low-Level effect processing + void DoFreqSlide(ModChannel &chn, int32 &period, int32 amount, bool isTonePorta = false) const; + void UpdateTimeSignature(); + +public: + // Convert frequency to IT cutoff (0...127) + uint8 FrequencyToCutOff(double frequency) const; + // Convert IT cutoff (0...127 + modifier) to frequency + uint32 CutOffToFrequency(uint32 nCutOff, int envModifier = 256) const; // [0-127] => [1-10KHz] + + // Returns true if periods are actually plain frequency values in Hz. + bool PeriodsAreFrequencies() const noexcept + { + return m_playBehaviour[kPeriodsAreHertz] && !UseFinetuneAndTranspose(); + } + + // Returns true if the current format uses transpose+finetune rather than frequency in Hz to specify middle-C. + static constexpr bool UseFinetuneAndTranspose(MODTYPE type) noexcept + { + return (type & (MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_MED | MOD_TYPE_MOD | MOD_TYPE_MTM | MOD_TYPE_OKT | MOD_TYPE_SFX | MOD_TYPE_STP | MOD_TYPE_XM)); + } + bool UseFinetuneAndTranspose() const noexcept + { + return UseFinetuneAndTranspose(GetType()); + } + + bool DestroySample(SAMPLEINDEX nSample); + bool DestroySampleThreadsafe(SAMPLEINDEX nSample); + + // Find an unused sample slot. If it is going to be assigned to an instrument, targetInstrument should be specified. + // SAMPLEINDEX_INVLAID is returned if no free sample slot could be found. + SAMPLEINDEX GetNextFreeSample(INSTRUMENTINDEX targetInstrument = INSTRUMENTINDEX_INVALID, SAMPLEINDEX start = 1) const; + // Find an unused instrument slot. + // INSTRUMENTINDEX_INVALID is returned if no free instrument slot could be found. + INSTRUMENTINDEX GetNextFreeInstrument(INSTRUMENTINDEX start = 1) const; + // Check whether a given sample is used by a given instrument. + bool IsSampleReferencedByInstrument(SAMPLEINDEX sample, INSTRUMENTINDEX instr) const; + + ModInstrument *AllocateInstrument(INSTRUMENTINDEX instr, SAMPLEINDEX assignedSample = 0); + bool DestroyInstrument(INSTRUMENTINDEX nInstr, deleteInstrumentSamples removeSamples); + bool RemoveInstrumentSamples(INSTRUMENTINDEX nInstr, SAMPLEINDEX keepSample = SAMPLEINDEX_INVALID); + SAMPLEINDEX DetectUnusedSamples(std::vector<bool> &sampleUsed) const; + SAMPLEINDEX RemoveSelectedSamples(const std::vector<bool> &keepSamples); + + // Set the autovibrato settings for all samples associated to the given instrument. + void PropagateXMAutoVibrato(INSTRUMENTINDEX ins, VibratoType type, uint8 sweep, uint8 depth, uint8 rate); + + // Samples file I/O + bool ReadSampleFromFile(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false, bool includeInstrumentFormats = true); + bool ReadWAVSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false, FileReader *wsmpChunk = nullptr); +protected: + bool ReadW64Sample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false); + bool ReadPATSample(SAMPLEINDEX nSample, FileReader &file); + bool ReadS3ISample(SAMPLEINDEX nSample, FileReader &file); + bool ReadSBISample(SAMPLEINDEX sample, FileReader &file); + bool ReadCAFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false); + bool ReadAIFFSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false); + bool ReadAUSample(SAMPLEINDEX nSample, FileReader &file, bool mayNormalize = false); + bool ReadXISample(SAMPLEINDEX nSample, FileReader &file); + bool ReadITSSample(SAMPLEINDEX nSample, FileReader &file, bool rewind = true); + bool ReadITISample(SAMPLEINDEX nSample, FileReader &file); + bool ReadIFFSample(SAMPLEINDEX sample, FileReader &file); + bool ReadBRRSample(SAMPLEINDEX sample, FileReader &file); + bool ReadFLACSample(SAMPLEINDEX sample, FileReader &file); + bool ReadOpusSample(SAMPLEINDEX sample, FileReader &file); + bool ReadVorbisSample(SAMPLEINDEX sample, FileReader &file); + bool ReadMP3Sample(SAMPLEINDEX sample, FileReader &file, bool raw = false, bool mo3Decode = false); // raw: ignore all encoder-/decodr-delays, decode just raw frames ; mod3Decode: skip metadata and loop-precompute + bool ReadMediaFoundationSample(SAMPLEINDEX sample, FileReader &file, bool mo3Decode = false); // mod3Decode: skip metadata and loop-precompute +public: +#ifdef MODPLUG_TRACKER + static std::vector<FileType> GetMediaFoundationFileTypes(); +#endif // MODPLUG_TRACKER +#ifndef MODPLUG_NO_FILESAVE + bool SaveWAVSample(SAMPLEINDEX nSample, std::ostream &f) const; + bool SaveRAWSample(SAMPLEINDEX nSample, std::ostream &f) const; + bool SaveFLACSample(SAMPLEINDEX nSample, std::ostream &f) const; + bool SaveS3ISample(SAMPLEINDEX smp, std::ostream &f) const; +#endif + + // Instrument file I/O + bool ReadInstrumentFromFile(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize = false); + bool ReadSampleAsInstrument(INSTRUMENTINDEX nInstr, FileReader &file, bool mayNormalize = false); +protected: + bool ReadXIInstrument(INSTRUMENTINDEX nInstr, FileReader &file); + bool ReadITIInstrument(INSTRUMENTINDEX nInstr, FileReader &file); + bool ReadPATInstrument(INSTRUMENTINDEX nInstr, FileReader &file); + bool ReadSFZInstrument(INSTRUMENTINDEX nInstr, FileReader &file); +public: +#ifndef MODPLUG_NO_FILESAVE + bool SaveXIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f) const; + bool SaveITIInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool compress, bool allowExternal) const; + bool SaveSFZInstrument(INSTRUMENTINDEX nInstr, std::ostream &f, const mpt::PathString &filename, bool useFLACsamples) const; +#endif + + // I/O from another sound file + bool ReadInstrumentFromSong(INSTRUMENTINDEX targetInstr, const CSoundFile &srcSong, INSTRUMENTINDEX sourceInstr); + bool ReadSampleFromSong(SAMPLEINDEX targetSample, const CSoundFile &srcSong, SAMPLEINDEX sourceSample); + + // Period/Note functions + uint32 GetNoteFromPeriod(uint32 period, int32 nFineTune = 0, uint32 nC5Speed = 0) const; + uint32 GetPeriodFromNote(uint32 note, int32 nFineTune, uint32 nC5Speed) const; + uint32 GetFreqFromPeriod(uint32 period, uint32 c5speed, int32 nPeriodFrac = 0) const; + // Misc functions + ModSample &GetSample(SAMPLEINDEX sample) { MPT_ASSERT(sample <= m_nSamples && sample < std::size(Samples)); return Samples[sample]; } + const ModSample &GetSample(SAMPLEINDEX sample) const { MPT_ASSERT(sample <= m_nSamples && sample < std::size(Samples)); return Samples[sample]; } + + // Resolve note/instrument combination to real sample index. Return value is guaranteed to be in [0, GetNumSamples()]. + SAMPLEINDEX GetSampleIndex(ModCommand::NOTE note, uint32 instr) const noexcept; + + uint32 MapMidiInstrument(uint8 program, uint16 bank, uint8 midiChannel, uint8 note, bool isXG, std::bitset<16> drumChns); + size_t ITInstrToMPT(FileReader &file, ModInstrument &ins, uint16 trkvers); + bool LoadMixPlugins(FileReader &file); +#ifndef NO_PLUGINS + static void ReadMixPluginChunk(FileReader &file, SNDMIXPLUGIN &plugin); + void ProcessMidiOut(CHANNELINDEX nChn); +#endif // NO_PLUGINS + + void ProcessGlobalVolume(long countChunk); + void ProcessStereoSeparation(long countChunk); + +private: + PLUGINDEX GetChannelPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginMutePriority respectMutes) const; + static PLUGINDEX GetActiveInstrumentPlugin(const ModChannel &chn, PluginMutePriority respectMutes); + IMixPlugin *GetChannelInstrumentPlugin(const ModChannel &chn) const; + +public: + PLUGINDEX GetBestPlugin(const PlayState &playState, CHANNELINDEX nChn, PluginPriority priority, PluginMutePriority respectMutes) const; + +}; + + +#ifndef NO_PLUGINS +inline IMixPlugin* CSoundFile::GetInstrumentPlugin(INSTRUMENTINDEX instr) const noexcept +{ + if(instr > 0 && instr <= GetNumInstruments() && Instruments[instr] && Instruments[instr]->nMixPlug && Instruments[instr]->nMixPlug <= MAX_MIXPLUGINS) + return m_MixPlugins[Instruments[instr]->nMixPlug - 1].pMixPlugin; + else + return nullptr; +} +#endif // NO_PLUGINS + + +/////////////////////////////////////////////////////////// +// Low-level Mixing functions + +#define FADESONGDELAY 100 + +MPT_CONSTEXPRINLINE int8 MOD2XMFineTune(int v) { return static_cast<int8>(static_cast<uint8>(v) << 4); } +MPT_CONSTEXPRINLINE int8 XM2MODFineTune(int v) { return static_cast<int8>(static_cast<uint8>(v) >> 4); } + +// Read instrument property with 'code' and 'size' from 'file' to instrument 'pIns'. +void ReadInstrumentExtensionField(ModInstrument* pIns, const uint32 code, const uint16 size, FileReader &file); + +// Read instrument property with 'code' from 'file' to instrument 'pIns'. +void ReadExtendedInstrumentProperty(ModInstrument* pIns, const uint32 code, FileReader &file); + +// Read extended instrument properties from 'file' to instrument 'pIns'. +void ReadExtendedInstrumentProperties(ModInstrument* pIns, FileReader &file); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Sndmix.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Sndmix.cpp new file mode 100644 index 00000000..f66586e4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Sndmix.cpp @@ -0,0 +1,2752 @@ +/* + * Sndmix.cpp + * ----------- + * Purpose: Pattern playback, effect processing + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "Sndfile.h" +#include "MixerLoops.h" +#include "MIDIEvents.h" +#include "Tables.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#endif // MODPLUG_TRACKER +#ifndef NO_PLUGINS +#include "plugins/PlugInterface.h" +#endif // NO_PLUGINS +#include "OPL.h" + +OPENMPT_NAMESPACE_BEGIN + +// Log tables for pre-amp +// Pre-amp (or more precisely: Pre-attenuation) depends on the number of channels, +// Which this table takes care of. +static constexpr uint8 PreAmpTable[16] = +{ + 0x60, 0x60, 0x60, 0x70, // 0-7 + 0x80, 0x88, 0x90, 0x98, // 8-15 + 0xA0, 0xA4, 0xA8, 0xAC, // 16-23 + 0xB0, 0xB4, 0xB8, 0xBC, // 24-31 +}; + +#ifndef NO_AGC +static constexpr uint8 PreAmpAGCTable[16] = +{ + 0x60, 0x60, 0x60, 0x64, + 0x68, 0x70, 0x78, 0x80, + 0x84, 0x88, 0x8C, 0x90, + 0x92, 0x94, 0x96, 0x98, +}; +#endif + + +void CSoundFile::SetMixerSettings(const MixerSettings &mixersettings) +{ + SetPreAmp(mixersettings.m_nPreAmp); // adjust agc + bool reset = false; + if( + (mixersettings.gdwMixingFreq != m_MixerSettings.gdwMixingFreq) + || + (mixersettings.gnChannels != m_MixerSettings.gnChannels) + || + (mixersettings.MixerFlags != m_MixerSettings.MixerFlags)) + reset = true; + m_MixerSettings = mixersettings; + InitPlayer(reset); +} + + +void CSoundFile::SetResamplerSettings(const CResamplerSettings &resamplersettings) +{ + m_Resampler.m_Settings = resamplersettings; + m_Resampler.UpdateTables(); + InitAmigaResampler(); +} + + +void CSoundFile::InitPlayer(bool bReset) +{ + if(bReset) + { + ResetMixStat(); + m_dryLOfsVol = m_dryROfsVol = 0; + m_surroundLOfsVol = m_surroundROfsVol = 0; + InitAmigaResampler(); + } + m_Resampler.UpdateTables(); +#ifndef NO_REVERB + m_Reverb.Initialize(bReset, m_RvbROfsVol, m_RvbLOfsVol, m_MixerSettings.gdwMixingFreq); +#endif +#ifndef NO_DSP + m_Surround.Initialize(bReset, m_MixerSettings.gdwMixingFreq); +#endif +#ifndef NO_DSP + m_MegaBass.Initialize(bReset, m_MixerSettings.gdwMixingFreq); +#endif +#ifndef NO_EQ + m_EQ.Initialize(bReset, m_MixerSettings.gdwMixingFreq); +#endif +#ifndef NO_AGC + m_AGC.Initialize(bReset, m_MixerSettings.gdwMixingFreq); +#endif +#ifndef NO_DSP + m_BitCrush.Initialize(bReset, m_MixerSettings.gdwMixingFreq); +#endif + if(m_opl) + { + m_opl->Initialize(m_MixerSettings.gdwMixingFreq); + } +} + + +bool CSoundFile::FadeSong(uint32 msec) +{ + samplecount_t nsamples = Util::muldiv(msec, m_MixerSettings.gdwMixingFreq, 1000); + if (nsamples <= 0) return false; + if (nsamples > 0x100000) nsamples = 0x100000; + m_PlayState.m_nBufferCount = nsamples; + int32 nRampLength = static_cast<int32>(m_PlayState.m_nBufferCount); + // Ramp everything down + for (uint32 noff=0; noff < m_nMixChannels; noff++) + { + ModChannel &pramp = m_PlayState.Chn[m_PlayState.ChnMix[noff]]; + pramp.newRightVol = pramp.newLeftVol = 0; + pramp.leftRamp = -pramp.leftVol * (1 << VOLUMERAMPPRECISION) / nRampLength; + pramp.rightRamp = -pramp.rightVol * (1 << VOLUMERAMPPRECISION) / nRampLength; + pramp.rampLeftVol = pramp.leftVol * (1 << VOLUMERAMPPRECISION); + pramp.rampRightVol = pramp.rightVol * (1 << VOLUMERAMPPRECISION); + pramp.nRampLength = nRampLength; + pramp.dwFlags.set(CHN_VOLUMERAMP); + } + return true; +} + + +// Apply stereo separation factor on an interleaved stereo/quad stream. +// count = Number of stereo sample pairs to process +// separation = -256...256 (negative values = swap L/R, 0 = mono, 128 = normal) +static void ApplyStereoSeparation(mixsample_t *mixBuf, std::size_t count, int32 separation) +{ +#ifdef MPT_INTMIXER + const mixsample_t factor_num = separation; // 128 =^= 1.0f + const mixsample_t factor_den = MixerSettings::StereoSeparationScale; // 128 + const mixsample_t normalize_den = 2; // mid/side pre/post normalization + const mixsample_t mid_den = normalize_den; + const mixsample_t side_num = factor_num; + const mixsample_t side_den = factor_den * normalize_den; +#else + const float normalize_factor = 0.5f; // cumulative mid/side normalization factor (1/sqrt(2))*(1/sqrt(2)) + const float factor = static_cast<float>(separation) / static_cast<float>(MixerSettings::StereoSeparationScale); // sep / 128 + const float mid_factor = normalize_factor; + const float side_factor = factor * normalize_factor; +#endif + for(std::size_t i = 0; i < count; i++) + { + mixsample_t l = mixBuf[0]; + mixsample_t r = mixBuf[1]; + mixsample_t m = l + r; + mixsample_t s = l - r; +#ifdef MPT_INTMIXER + m /= mid_den; + s = Util::muldiv(s, side_num, side_den); +#else + m *= mid_factor; + s *= side_factor; +#endif + l = m + s; + r = m - s; + mixBuf[0] = l; + mixBuf[1] = r; + mixBuf += 2; + } +} + + +static void ApplyStereoSeparation(mixsample_t *SoundFrontBuffer, mixsample_t *SoundRearBuffer, std::size_t channels, std::size_t countChunk, int32 separation) +{ + if(separation == MixerSettings::StereoSeparationScale) + { // identity + return; + } + if(channels >= 2) ApplyStereoSeparation(SoundFrontBuffer, countChunk, separation); + if(channels >= 4) ApplyStereoSeparation(SoundRearBuffer , countChunk, separation); +} + + +void CSoundFile::ProcessInputChannels(IAudioSource &source, std::size_t countChunk) +{ + for(std::size_t channel = 0; channel < NUMMIXINPUTBUFFERS; ++channel) + { + std::fill(&(MixInputBuffer[channel][0]), &(MixInputBuffer[channel][countChunk]), 0); + } + mixsample_t * buffers[NUMMIXINPUTBUFFERS]; + for(std::size_t channel = 0; channel < NUMMIXINPUTBUFFERS; ++channel) + { + buffers[channel] = MixInputBuffer[channel]; + } + source.Process(mpt::audio_span_planar(buffers, m_MixerSettings.NumInputChannels, countChunk)); +} + + +// Read one tick but skip all expensive rendering options +CSoundFile::samplecount_t CSoundFile::ReadOneTick() +{ + const auto origMaxMixChannels = m_MixerSettings.m_nMaxMixChannels; + m_MixerSettings.m_nMaxMixChannels = 0; + while(m_PlayState.m_nBufferCount) + { + auto framesToRender = std::min(m_PlayState.m_nBufferCount, samplecount_t(MIXBUFFERSIZE)); + CreateStereoMix(framesToRender); + m_PlayState.m_nBufferCount -= framesToRender; + m_PlayState.m_lTotalSampleCount += framesToRender; + } + m_MixerSettings.m_nMaxMixChannels = origMaxMixChannels; + if(ReadNote()) + return m_PlayState.m_nBufferCount; + else + return 0; +} + + +CSoundFile::samplecount_t CSoundFile::Read(samplecount_t count, IAudioTarget &target, IAudioSource &source, std::optional<std::reference_wrapper<IMonitorOutput>> outputMonitor, std::optional<std::reference_wrapper<IMonitorInput>> inputMonitor) +{ + MPT_ASSERT_ALWAYS(m_MixerSettings.IsValid()); + + samplecount_t countRendered = 0; + samplecount_t countToRender = count; + + while(!m_SongFlags[SONG_ENDREACHED] && countToRender > 0) + { + + // Update Channel Data + if(!m_PlayState.m_nBufferCount) + { + // Last tick or fade completely processed, find out what to do next + + if(m_SongFlags[SONG_FADINGSONG]) + { + // Song was faded out + m_SongFlags.set(SONG_ENDREACHED); + } else if(ReadNote()) + { + // Render next tick (normal progress) + MPT_ASSERT(m_PlayState.m_nBufferCount > 0); + #ifdef MODPLUG_TRACKER + // Save pattern cue points for WAV rendering here (if we reached a new pattern, that is.) + if(m_PatternCuePoints != nullptr && (m_PatternCuePoints->empty() || m_PlayState.m_nCurrentOrder != m_PatternCuePoints->back().order)) + { + PatternCuePoint cue; + cue.offset = countRendered; + cue.order = m_PlayState.m_nCurrentOrder; + cue.processed = false; // We don't know the base offset in the file here. It has to be added in the main conversion loop. + m_PatternCuePoints->push_back(cue); + } + #endif + } else + { + // No new pattern data + #ifdef MODPLUG_TRACKER + if((m_nMaxOrderPosition) && (m_PlayState.m_nCurrentOrder >= m_nMaxOrderPosition)) + { + m_SongFlags.set(SONG_ENDREACHED); + } + #endif // MODPLUG_TRACKER + if(IsRenderingToDisc()) + { + // Disable song fade when rendering or when requested in libopenmpt. + m_SongFlags.set(SONG_ENDREACHED); + } else + { // end of song reached, fade it out + if(FadeSong(FADESONGDELAY)) // sets m_nBufferCount xor returns false + { // FadeSong sets m_nBufferCount here + MPT_ASSERT(m_PlayState.m_nBufferCount > 0); + m_SongFlags.set(SONG_FADINGSONG); + } else + { + m_SongFlags.set(SONG_ENDREACHED); + } + } + } + + } + + if(m_SongFlags[SONG_ENDREACHED]) + { + // Mix done. + + // If we decide to continue the mix (possible in libopenmpt), the tick count + // is valid right now (0), meaning that no new row data will be processed. + // This would effectively prolong the last played row. + m_PlayState.m_nTickCount = m_PlayState.TicksOnRow(); + break; + } + + MPT_ASSERT(m_PlayState.m_nBufferCount > 0); // assert that we have actually something to do + + const samplecount_t countChunk = std::min({ static_cast<samplecount_t>(MIXBUFFERSIZE), static_cast<samplecount_t>(m_PlayState.m_nBufferCount), static_cast<samplecount_t>(countToRender) }); + + if(m_MixerSettings.NumInputChannels > 0) + { + ProcessInputChannels(source, countChunk); + } + + if(inputMonitor) + { + mixsample_t *buffers[NUMMIXINPUTBUFFERS]; + for(std::size_t channel = 0; channel < NUMMIXINPUTBUFFERS; ++channel) + { + buffers[channel] = MixInputBuffer[channel]; + } + inputMonitor->get().Process(mpt::audio_span_planar<const mixsample_t>(buffers, m_MixerSettings.NumInputChannels, countChunk)); + } + + CreateStereoMix(countChunk); + + if(m_opl) + { + m_opl->Mix(MixSoundBuffer, countChunk, m_OPLVolumeFactor * m_nVSTiVolume / 48); + } + +#ifndef NO_REVERB + m_Reverb.Process(MixSoundBuffer, ReverbSendBuffer, m_RvbROfsVol, m_RvbLOfsVol, countChunk); +#endif // NO_REVERB + +#ifndef NO_PLUGINS + if(m_loadedPlugins) + { + ProcessPlugins(countChunk); + } +#endif // NO_PLUGINS + + if(m_MixerSettings.gnChannels == 1) + { + MonoFromStereo(MixSoundBuffer, countChunk); + } + + if(m_PlayConfig.getGlobalVolumeAppliesToMaster()) + { + ProcessGlobalVolume(countChunk); + } + + if(m_MixerSettings.m_nStereoSeparation != MixerSettings::StereoSeparationScale) + { + ProcessStereoSeparation(countChunk); + } + + if(m_MixerSettings.DSPMask) + { + ProcessDSP(countChunk); + } + + if(m_MixerSettings.gnChannels == 4) + { + InterleaveFrontRear(MixSoundBuffer, MixRearBuffer, countChunk); + } + + if(outputMonitor) + { + outputMonitor->get().Process(mpt::audio_span_interleaved<const mixsample_t>(MixSoundBuffer, m_MixerSettings.gnChannels, countChunk)); + } + + target.Process(mpt::audio_span_interleaved<mixsample_t>(MixSoundBuffer, m_MixerSettings.gnChannels, countChunk)); + + // Buffer ready + countRendered += countChunk; + countToRender -= countChunk; + m_PlayState.m_nBufferCount -= countChunk; + m_PlayState.m_lTotalSampleCount += countChunk; + +#ifdef MODPLUG_TRACKER + if(IsRenderingToDisc()) + { + // Stop playback on F00 if no more voices are active. + // F00 sets the tick count to 65536 in FT2, so it just generates a reaaaally long row. + // Usually this command can be found at the end of a song to effectively stop playback. + // Since we don't want to render hours of silence, we are going to check if there are + // still any channels playing, and if that is no longer the case, we stop playback at + // the end of the next tick. + if(m_PlayState.m_nMusicSpeed == uint16_max && (m_nMixStat == 0 || m_PlayState.m_nGlobalVolume == 0) && GetType() == MOD_TYPE_XM && !m_PlayState.m_nBufferCount) + { + m_SongFlags.set(SONG_ENDREACHED); + } + } +#endif // MODPLUG_TRACKER + } + + // mix done + + return countRendered; + +} + + +void CSoundFile::ProcessDSP(uint32 countChunk) +{ + #ifndef NO_DSP + if(m_MixerSettings.DSPMask & SNDDSP_SURROUND) + { + m_Surround.Process(MixSoundBuffer, MixRearBuffer, countChunk, m_MixerSettings.gnChannels); + } + #endif // NO_DSP + + #ifndef NO_DSP + if(m_MixerSettings.DSPMask & SNDDSP_MEGABASS) + { + m_MegaBass.Process(MixSoundBuffer, MixRearBuffer, countChunk, m_MixerSettings.gnChannels); + } + #endif // NO_DSP + + #ifndef NO_EQ + if(m_MixerSettings.DSPMask & SNDDSP_EQ) + { + m_EQ.Process(MixSoundBuffer, MixRearBuffer, countChunk, m_MixerSettings.gnChannels); + } + #endif // NO_EQ + + #ifndef NO_AGC + if(m_MixerSettings.DSPMask & SNDDSP_AGC) + { + m_AGC.Process(MixSoundBuffer, MixRearBuffer, countChunk, m_MixerSettings.gnChannels); + } + #endif // NO_AGC + + #ifndef NO_DSP + if(m_MixerSettings.DSPMask & SNDDSP_BITCRUSH) + { + m_BitCrush.Process(MixSoundBuffer, MixRearBuffer, countChunk, m_MixerSettings.gnChannels); + } + #endif // NO_DSP + + #if defined(NO_DSP) && defined(NO_EQ) && defined(NO_AGC) + MPT_UNREFERENCED_PARAMETER(countChunk); + #endif +} + + +///////////////////////////////////////////////////////////////////////////// +// Handles navigation/effects + +bool CSoundFile::ProcessRow() +{ + while(++m_PlayState.m_nTickCount >= m_PlayState.TicksOnRow()) + { + const auto [ignoreRow, patternTransition] = NextRow(m_PlayState, m_SongFlags[SONG_BREAKTOROW]); + +#ifdef MODPLUG_TRACKER + if(patternTransition) + { + HandlePatternTransitionEvents(); + } + // "Lock row" editing feature + if(m_lockRowStart != ROWINDEX_INVALID && (m_PlayState.m_nRow < m_lockRowStart || m_PlayState.m_nRow > m_lockRowEnd) && !IsRenderingToDisc()) + { + m_PlayState.m_nRow = m_lockRowStart; + } + // "Lock order" editing feature + if(Order().IsPositionLocked(m_PlayState.m_nCurrentOrder) && !IsRenderingToDisc()) + { + m_PlayState.m_nCurrentOrder = m_lockOrderStart; + } +#else + MPT_UNUSED_VARIABLE(patternTransition); +#endif // MODPLUG_TRACKER + + // Check if pattern is valid + if(!m_SongFlags[SONG_PATTERNLOOP]) + { + m_PlayState.m_nPattern = (m_PlayState.m_nCurrentOrder < Order().size()) ? Order()[m_PlayState.m_nCurrentOrder] : Order.GetInvalidPatIndex(); + if (m_PlayState.m_nPattern < Patterns.Size() && !Patterns[m_PlayState.m_nPattern].IsValid()) m_PlayState.m_nPattern = Order.GetIgnoreIndex(); + while (m_PlayState.m_nPattern >= Patterns.Size()) + { + // End of song? + if ((m_PlayState.m_nPattern == Order.GetInvalidPatIndex()) || (m_PlayState.m_nCurrentOrder >= Order().size())) + { + ORDERINDEX restartPosOverride = Order().GetRestartPos(); + if(restartPosOverride == 0 && m_PlayState.m_nCurrentOrder <= Order().size() && m_PlayState.m_nCurrentOrder > 0) + { + // Subtune detection. Subtunes are separated by "---" order items, so if we're in a + // subtune and there's no restart position, we go to the first order of the subtune + // (i.e. the first order after the previous "---" item) + for(ORDERINDEX ord = m_PlayState.m_nCurrentOrder - 1; ord > 0; ord--) + { + if(Order()[ord] == Order.GetInvalidPatIndex()) + { + // Jump back to first order of this subtune + restartPosOverride = ord + 1; + break; + } + } + } + + // If channel resetting is disabled in MPT, we will emulate a pattern break (and we always do it if we're not in MPT) +#ifdef MODPLUG_TRACKER + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_RESETCHANNELS)) +#endif // MODPLUG_TRACKER + { + m_SongFlags.set(SONG_BREAKTOROW); + } + + if (restartPosOverride == 0 && !m_SongFlags[SONG_BREAKTOROW]) + { + //rewbs.instroVSTi: stop all VSTi at end of song, if looping. + StopAllVsti(); + m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; + m_PlayState.m_nMusicTempo = m_nDefaultTempo; + m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume; + for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++) + { + auto &chn = m_PlayState.Chn[i]; + if(chn.dwFlags[CHN_ADLIB] && m_opl) + { + m_opl->NoteCut(i); + } + chn.dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + chn.nFadeOutVol = 0; + + if(i < m_nChannels) + { + chn.nGlobalVol = ChnSettings[i].nVolume; + chn.nVolume = ChnSettings[i].nVolume; + chn.nPan = ChnSettings[i].nPan; + chn.nPanSwing = chn.nVolSwing = 0; + chn.nCutSwing = chn.nResSwing = 0; + chn.nOldVolParam = 0; + chn.oldOffset = 0; + chn.nOldHiOffset = 0; + chn.nPortamentoDest = 0; + + if(!chn.nLength) + { + chn.dwFlags = ChnSettings[i].dwFlags; + chn.nLoopStart = 0; + chn.nLoopEnd = 0; + chn.pModInstrument = nullptr; + chn.pModSample = nullptr; + } + } + } + } + + //Handle Repeat position + m_PlayState.m_nCurrentOrder = restartPosOverride; + m_SongFlags.reset(SONG_BREAKTOROW); + //If restart pos points to +++, move along + while(m_PlayState.m_nCurrentOrder < Order().size() && Order()[m_PlayState.m_nCurrentOrder] == Order.GetIgnoreIndex()) + { + m_PlayState.m_nCurrentOrder++; + } + //Check for end of song or bad pattern + if (m_PlayState.m_nCurrentOrder >= Order().size() + || !Order().IsValidPat(m_PlayState.m_nCurrentOrder)) + { + m_visitedRows.Initialize(true); + return false; + } + } else + { + m_PlayState.m_nCurrentOrder++; + } + + if (m_PlayState.m_nCurrentOrder < Order().size()) + m_PlayState.m_nPattern = Order()[m_PlayState.m_nCurrentOrder]; + else + m_PlayState.m_nPattern = Order.GetInvalidPatIndex(); + + if (m_PlayState.m_nPattern < Patterns.Size() && !Patterns[m_PlayState.m_nPattern].IsValid()) + m_PlayState.m_nPattern = Order.GetIgnoreIndex(); + } + m_PlayState.m_nNextOrder = m_PlayState.m_nCurrentOrder; + +#ifdef MODPLUG_TRACKER + if ((m_nMaxOrderPosition) && (m_PlayState.m_nCurrentOrder >= m_nMaxOrderPosition)) return false; +#endif // MODPLUG_TRACKER + } + + // Weird stuff? + if (!Patterns.IsValidPat(m_PlayState.m_nPattern)) + return false; + // Did we jump to an invalid row? + if (m_PlayState.m_nRow >= Patterns[m_PlayState.m_nPattern].GetNumRows()) m_PlayState.m_nRow = 0; + + // Has this row been visited before? We might want to stop playback now. + // But: We will not mark the row as modified if the song is not in loop mode but + // the pattern loop (editor flag, not to be confused with the pattern loop effect) + // flag is set - because in that case, the module would stop after the first pattern loop... + const bool overrideLoopCheck = (m_nRepeatCount != -1) && m_SongFlags[SONG_PATTERNLOOP]; + if(!overrideLoopCheck && m_visitedRows.Visit(m_PlayState.m_nCurrentOrder, m_PlayState.m_nRow, m_PlayState.Chn, ignoreRow)) + { + if(m_nRepeatCount) + { + // repeat count == -1 means repeat infinitely. + if(m_nRepeatCount > 0) + { + m_nRepeatCount--; + } + // Forget all but the current row. + m_visitedRows.Initialize(true); + m_visitedRows.Visit(m_PlayState.m_nCurrentOrder, m_PlayState.m_nRow, m_PlayState.Chn, ignoreRow); + } else + { +#ifdef MODPLUG_TRACKER + // Let's check again if this really is the end of the song. + // The visited rows vector might have been screwed up while editing... + // This is of course not possible during rendering to WAV, so we ignore that case. + bool isReallyAtEnd = IsRenderingToDisc(); + if(!isReallyAtEnd) + { + for(const auto &t : GetLength(eNoAdjust, GetLengthTarget(true))) + { + if(t.lastOrder == m_PlayState.m_nCurrentOrder && t.lastRow == m_PlayState.m_nRow) + { + isReallyAtEnd = true; + break; + } + } + } + + if(isReallyAtEnd) + { + // This is really the song's end! + m_visitedRows.Initialize(true); + return false; + } else + { + // Ok, this is really dirty, but we have to update the visited rows vector... + GetLength(eAdjustOnlyVisitedRows, GetLengthTarget(m_PlayState.m_nCurrentOrder, m_PlayState.m_nRow)); + } +#else + if(m_SongFlags[SONG_PLAYALLSONGS]) + { + // When playing all subsongs consecutively, first search for any hidden subsongs... + if(!m_visitedRows.GetFirstUnvisitedRow(m_PlayState.m_nCurrentOrder, m_PlayState.m_nRow, true)) + { + // ...and then try the next sequence. + m_PlayState.m_nNextOrder = m_PlayState.m_nCurrentOrder = 0; + m_PlayState.m_nNextRow = m_PlayState.m_nRow = 0; + if(Order.GetCurrentSequenceIndex() >= Order.GetNumSequences() - 1) + { + Order.SetSequence(0); + m_visitedRows.Initialize(true); + return false; + } + Order.SetSequence(Order.GetCurrentSequenceIndex() + 1); + m_visitedRows.Initialize(true); + } + // When jumping to the next subsong, stop all playing notes from the previous song... + const auto muteFlag = CSoundFile::GetChannelMuteFlag(); + for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++) + m_PlayState.Chn[i].Reset(ModChannel::resetSetPosFull, *this, i, muteFlag); + StopAllVsti(); + // ...and the global playback information. + m_PlayState.m_nMusicSpeed = m_nDefaultSpeed; + m_PlayState.m_nMusicTempo = m_nDefaultTempo; + m_PlayState.m_nGlobalVolume = m_nDefaultGlobalVolume; + + m_PlayState.m_nNextOrder = m_PlayState.m_nCurrentOrder; + m_PlayState.m_nNextRow = m_PlayState.m_nRow; + if(Order().size() > m_PlayState.m_nCurrentOrder) + m_PlayState.m_nPattern = Order()[m_PlayState.m_nCurrentOrder]; + m_visitedRows.Visit(m_PlayState.m_nCurrentOrder, m_PlayState.m_nRow, m_PlayState.Chn, ignoreRow); + if (!Patterns.IsValidPat(m_PlayState.m_nPattern)) + return false; + } else + { + m_visitedRows.Initialize(true); + return false; + } +#endif // MODPLUG_TRACKER + } + } + + SetupNextRow(m_PlayState, m_SongFlags[SONG_PATTERNLOOP]); + + // Reset channel values + ModCommand *m = Patterns[m_PlayState.m_nPattern].GetpModCommand(m_PlayState.m_nRow, 0); + for (ModChannel *pChn = m_PlayState.Chn, *pEnd = pChn + m_nChannels; pChn != pEnd; pChn++, m++) + { + // First, handle some quirks that happen after the last tick of the previous row... + if(m_playBehaviour[KST3PortaAfterArpeggio] + && pChn->nCommand == CMD_ARPEGGIO // Previous row state! + && (m->command == CMD_PORTAMENTOUP || m->command == CMD_PORTAMENTODOWN)) + { + // In ST3, a portamento immediately following an arpeggio continues where the arpeggio left off. + // Test case: PortaAfterArp.s3m + pChn->nPeriod = GetPeriodFromNote(pChn->nArpeggioLastNote, pChn->nFineTune, pChn->nC5Speed); + } + + if(m_playBehaviour[kMODOutOfRangeNoteDelay] + && !m->IsNote() + && pChn->rowCommand.IsNote() + && pChn->rowCommand.command == CMD_MODCMDEX && (pChn->rowCommand.param & 0xF0) == 0xD0 + && (pChn->rowCommand.param & 0x0Fu) >= m_PlayState.m_nMusicSpeed) + { + // In ProTracker, a note triggered by an out-of-range note delay can be heard on the next row + // if there is no new note on that row. + // Test case: NoteDelay-NextRow.mod + pChn->nPeriod = GetPeriodFromNote(pChn->rowCommand.note, pChn->nFineTune, 0); + } + if(m_playBehaviour[kMODTempoOnSecondTick] && !m_playBehaviour[kMODVBlankTiming] && m_PlayState.m_nMusicSpeed == 1 && pChn->rowCommand.command == CMD_TEMPO) + { + // ProTracker sets the tempo after the first tick. This block handles the case of one tick per row. + // Test case: TempoChange.mod + m_PlayState.m_nMusicTempo = TEMPO(std::max(ModCommand::PARAM(1), pChn->rowCommand.param), 0); + } + + pChn->rowCommand = *m; + + pChn->rightVol = pChn->newRightVol; + pChn->leftVol = pChn->newLeftVol; + pChn->dwFlags.reset(CHN_VIBRATO | CHN_TREMOLO); + if(!m_playBehaviour[kITVibratoTremoloPanbrello]) pChn->nPanbrelloOffset = 0; + pChn->nCommand = CMD_NONE; + pChn->m_plugParamValueStep = 0; + } + + // Now that we know which pattern we're on, we can update time signatures (global or pattern-specific) + UpdateTimeSignature(); + + if(ignoreRow) + { + m_PlayState.m_nTickCount = m_PlayState.m_nMusicSpeed; + continue; + } + break; + } + // Should we process tick0 effects? + if (!m_PlayState.m_nMusicSpeed) m_PlayState.m_nMusicSpeed = 1; + + //End of row? stop pattern step (aka "play row"). +#ifdef MODPLUG_TRACKER + if (m_PlayState.m_nTickCount >= m_PlayState.TicksOnRow() - 1) + { + if(m_SongFlags[SONG_STEP]) + { + m_SongFlags.reset(SONG_STEP); + m_SongFlags.set(SONG_PAUSED); + } + } +#endif // MODPLUG_TRACKER + + if (m_PlayState.m_nTickCount) + { + m_SongFlags.reset(SONG_FIRSTTICK); + if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2)) + && (GetType() != MOD_TYPE_MOD || m_SongFlags[SONG_PT_MODE]) // Fix infinite loop in "GamerMan " by MrGamer, which was made with FT2 + && m_PlayState.m_nTickCount < m_PlayState.TicksOnRow()) + { + // Emulate first tick behaviour if Row Delay is set. + // Test cases: PatternDelaysRetrig.it, PatternDelaysRetrig.s3m, PatternDelaysRetrig.xm, PatternDelaysRetrig.mod + if(!(m_PlayState.m_nTickCount % (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay))) + { + m_SongFlags.set(SONG_FIRSTTICK); + } + } + } else + { + m_SongFlags.set(SONG_FIRSTTICK); + m_SongFlags.reset(SONG_BREAKTOROW); + } + + // Update Effects + return ProcessEffects(); +} + + +std::pair<bool, bool> CSoundFile::NextRow(PlayState &playState, const bool breakRow) const +{ + // When having an EEx effect on the same row as a Dxx jump, the target row is not played in ProTracker. + // Test case: DelayBreak.mod (based on condom_corruption by Travolta) + const bool ignoreRow = playState.m_nPatternDelay > 1 && breakRow && GetType() == MOD_TYPE_MOD; + + // Done with the last row of the pattern or jumping somewhere else (could also be a result of pattern loop to row 0, but that doesn't matter here) + const bool patternTransition = playState.m_nNextRow == 0 || breakRow; + if(patternTransition && GetType() == MOD_TYPE_S3M) + { + // Reset pattern loop start + // Test case: LoopReset.s3m + for(CHANNELINDEX i = 0; i < GetNumChannels(); i++) + { + playState.Chn[i].nPatternLoop = 0; + } + } + + playState.m_nPatternDelay = 0; + playState.m_nFrameDelay = 0; + playState.m_nTickCount = 0; + playState.m_nRow = playState.m_nNextRow; + playState.m_nCurrentOrder = playState.m_nNextOrder; + + return {ignoreRow, patternTransition}; +} + + +void CSoundFile::SetupNextRow(PlayState &playState, const bool patternLoop) const +{ + playState.m_nNextRow = playState.m_nRow + 1; + if(playState.m_nNextRow >= Patterns[playState.m_nPattern].GetNumRows()) + { + if(!patternLoop) + playState.m_nNextOrder = playState.m_nCurrentOrder + 1; + playState.m_nNextRow = 0; + + // FT2 idiosyncrasy: When E60 is used on a pattern row x, the following pattern also starts from row x + // instead of the beginning of the pattern, unless there was a Bxx or Dxx effect. + if(m_playBehaviour[kFT2LoopE60Restart]) + { + playState.m_nNextRow = playState.m_nextPatStartRow; + playState.m_nextPatStartRow = 0; + } + } +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Channel effect processing + + +// Calculate delta for Vibrato / Tremolo / Panbrello effect +int CSoundFile::GetVibratoDelta(int type, int position) const +{ + // IT compatibility: IT has its own, more precise tables + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + { + position &= 0xFF; + switch(type & 0x03) + { + case 0: // Sine + default: + return ITSinusTable[position]; + case 1: // Ramp down + return 64 - (position + 1) / 2; + case 2: // Square + return position < 128 ? 64 : 0; + case 3: // Random + return mpt::random<int, 7>(AccessPRNG()) - 0x40; + } + } else if(GetType() & (MOD_TYPE_DIGI | MOD_TYPE_DBM)) + { + // Other waveforms are not supported. + static constexpr int8 DBMSinus[] = + { + 33, 52, 69, 84, 96, 107, 116, 122, 125, 127, 125, 122, 116, 107, 96, 84, + 69, 52, 33, 13, -8, -31, -54, -79, -104,-128, -104, -79, -54, -31, -8, 13, + }; + return DBMSinus[(position / 2u) & 0x1F]; + } else + { + position &= 0x3F; + switch(type & 0x03) + { + case 0: // Sine + default: + return ModSinusTable[position]; + case 1: // Ramp down + return (position < 32 ? 0 : 255) - position * 4; + case 2: // Square + return position < 32 ? 127 : -127; + case 3: // Random + return ModRandomTable[position]; + } + } +} + + +void CSoundFile::ProcessVolumeSwing(ModChannel &chn, int &vol) const +{ + if(m_playBehaviour[kITSwingBehaviour]) + { + vol += chn.nVolSwing; + Limit(vol, 0, 64); + } else if(m_playBehaviour[kMPTOldSwingBehaviour]) + { + vol += chn.nVolSwing; + Limit(vol, 0, 256); + } else + { + chn.nVolume += chn.nVolSwing; + Limit(chn.nVolume, 0, 256); + vol = chn.nVolume; + chn.nVolSwing = 0; + } +} + + +void CSoundFile::ProcessPanningSwing(ModChannel &chn) const +{ + if(m_playBehaviour[kITSwingBehaviour] || m_playBehaviour[kMPTOldSwingBehaviour]) + { + chn.nRealPan = chn.nPan + chn.nPanSwing; + Limit(chn.nRealPan, 0, 256); + } else + { + chn.nPan += chn.nPanSwing; + Limit(chn.nPan, 0, 256); + chn.nPanSwing = 0; + chn.nRealPan = chn.nPan; + } +} + + +void CSoundFile::ProcessTremolo(ModChannel &chn, int &vol) const +{ + if (chn.dwFlags[CHN_TREMOLO]) + { + if(m_SongFlags.test_all(SONG_FIRSTTICK | SONG_PT_MODE)) + { + // ProTracker doesn't apply tremolo nor advance on the first tick. + // Test case: VibratoReset.mod + return; + } + + // IT compatibility: Why would you not want to execute tremolo at volume 0? + if(vol > 0 || m_playBehaviour[kITVibratoTremoloPanbrello]) + { + // IT compatibility: We don't need a different attenuation here because of the different tables we're going to use + const uint8 attenuation = ((GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) || m_playBehaviour[kITVibratoTremoloPanbrello]) ? 5 : 6; + + int delta = GetVibratoDelta(chn.nTremoloType, chn.nTremoloPos); + if((chn.nTremoloType & 0x03) == 1 && m_playBehaviour[kFT2MODTremoloRampWaveform]) + { + // FT2 compatibility: Tremolo ramp down / triangle implementation is weird and affected by vibrato position (copypaste bug) + // Test case: TremoloWaveforms.xm, TremoloVibrato.xm + uint8 ramp = (chn.nTremoloPos * 4u) & 0x7F; + // Volume-colum vibrato gets executed first in FT2, so we may need to advance the vibrato position first + uint32 vibPos = chn.nVibratoPos; + if(!m_SongFlags[SONG_FIRSTTICK] && chn.dwFlags[CHN_VIBRATO]) + vibPos += chn.nVibratoSpeed; + if((vibPos & 0x3F) >= 32) + ramp ^= 0x7F; + if((chn.nTremoloPos & 0x3F) >= 32) + delta = -ramp; + else + delta = ramp; + } + if(GetType() != MOD_TYPE_DMF) + { + vol += (delta * chn.nTremoloDepth) / (1 << attenuation); + } else + { + // Tremolo in DMF always attenuates by a percentage of the current note volume + vol -= (vol * chn.nTremoloDepth * (64 - delta)) / (128 * 64); + } + } + if(!m_SongFlags[SONG_FIRSTTICK] || ((GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS])) + { + // IT compatibility: IT has its own, more precise tables + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + chn.nTremoloPos += 4 * chn.nTremoloSpeed; + else + chn.nTremoloPos += chn.nTremoloSpeed; + } + } +} + + +void CSoundFile::ProcessTremor(CHANNELINDEX nChn, int &vol) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + + if(m_playBehaviour[kFT2Tremor]) + { + // FT2 Compatibility: Weird XM tremor. + // Test case: Tremor.xm + if(chn.nTremorCount & 0x80) + { + if(!m_SongFlags[SONG_FIRSTTICK] && chn.nCommand == CMD_TREMOR) + { + chn.nTremorCount &= ~0x20; + if(chn.nTremorCount == 0x80) + { + // Reached end of off-time + chn.nTremorCount = (chn.nTremorParam >> 4) | 0xC0; + } else if(chn.nTremorCount == 0xC0) + { + // Reached end of on-time + chn.nTremorCount = (chn.nTremorParam & 0x0F) | 0x80; + } else + { + chn.nTremorCount--; + } + + chn.dwFlags.set(CHN_FASTVOLRAMP); + } + + if((chn.nTremorCount & 0xE0) == 0x80) + { + vol = 0; + } + } + } else if(chn.nCommand == CMD_TREMOR) + { + // IT compatibility 12. / 13.: Tremor + if(m_playBehaviour[kITTremor]) + { + if((chn.nTremorCount & 0x80) && chn.nLength) + { + if (chn.nTremorCount == 0x80) + chn.nTremorCount = (chn.nTremorParam >> 4) | 0xC0; + else if (chn.nTremorCount == 0xC0) + chn.nTremorCount = (chn.nTremorParam & 0x0F) | 0x80; + else + chn.nTremorCount--; + } + + if((chn.nTremorCount & 0xC0) == 0x80) + vol = 0; + } else + { + uint8 ontime = chn.nTremorParam >> 4; + uint8 n = ontime + (chn.nTremorParam & 0x0F); // Total tremor cycle time (On + Off) + if ((!(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) || m_SongFlags[SONG_ITOLDEFFECTS]) + { + n += 2; + ontime++; + } + uint8 tremcount = chn.nTremorCount; + if(!(GetType() & MOD_TYPE_XM)) + { + if (tremcount >= n) tremcount = 0; + if (tremcount >= ontime) vol = 0; + chn.nTremorCount = tremcount + 1; + } else + { + if(m_SongFlags[SONG_FIRSTTICK]) + { + // tremcount is only 0 on the first tremor tick after triggering a note. + if(tremcount > 0) + { + tremcount--; + } + } else + { + chn.nTremorCount = tremcount + 1; + } + if (tremcount % n >= ontime) vol = 0; + } + } + chn.dwFlags.set(CHN_FASTVOLRAMP); + } + +#ifndef NO_PLUGINS + // Plugin tremor + if(chn.nCommand == CMD_TREMOR && chn.pModInstrument && chn.pModInstrument->nMixPlug + && !chn.pModInstrument->dwFlags[INS_MUTE] + && !chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE] + && ModCommand::IsNote(chn.nLastNote)) + { + const ModInstrument *pIns = chn.pModInstrument; + IMixPlugin *pPlugin = m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin; + if(pPlugin) + { + const bool isPlaying = pPlugin->IsNotePlaying(chn.nLastNote, nChn); + if(vol == 0 && isPlaying) + pPlugin->MidiCommand(*pIns, chn.nLastNote + NOTE_MAX_SPECIAL, 0, nChn); + else if(vol != 0 && !isPlaying) + pPlugin->MidiCommand(*pIns, chn.nLastNote, static_cast<uint16>(chn.nVolume), nChn); + } + } +#endif // NO_PLUGINS +} + + +bool CSoundFile::IsEnvelopeProcessed(const ModChannel &chn, EnvelopeType env) const +{ + if(chn.pModInstrument == nullptr) + { + return false; + } + const InstrumentEnvelope &insEnv = chn.pModInstrument->GetEnvelope(env); + + // IT Compatibility: S77/S79/S7B do not disable the envelope, they just pause the counter + // Test cases: s77.it, EnvLoops.xm, PanSustainRelease.xm + bool playIfPaused = m_playBehaviour[kITEnvelopePositionHandling] || m_playBehaviour[kFT2PanSustainRelease]; + return ((chn.GetEnvelope(env).flags[ENV_ENABLED] || (insEnv.dwFlags[ENV_ENABLED] && playIfPaused)) + && !insEnv.empty()); +} + + +void CSoundFile::ProcessVolumeEnvelope(ModChannel &chn, int &vol) const +{ + if(IsEnvelopeProcessed(chn, ENV_VOLUME)) + { + const ModInstrument *pIns = chn.pModInstrument; + + if(m_playBehaviour[kITEnvelopePositionHandling] && chn.VolEnv.nEnvPosition == 0) + { + // If the envelope is disabled at the very same moment as it is triggered, we do not process anything. + return; + } + const int envpos = chn.VolEnv.nEnvPosition - (m_playBehaviour[kITEnvelopePositionHandling] ? 1 : 0); + // Get values in [0, 256] + int envval = pIns->VolEnv.GetValueFromPosition(envpos, 256); + + // if we are in the release portion of the envelope, + // rescale envelope factor so that it is proportional to the release point + // and release envelope beginning. + if(pIns->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET + && chn.VolEnv.nEnvValueAtReleaseJump != NOT_YET_RELEASED) + { + int envValueAtReleaseJump = chn.VolEnv.nEnvValueAtReleaseJump; + int envValueAtReleaseNode = pIns->VolEnv[pIns->VolEnv.nReleaseNode].value * 4; + + //If we have just hit the release node, force the current env value + //to be that of the release node. This works around the case where + // we have another node at the same position as the release node. + if(envpos == pIns->VolEnv[pIns->VolEnv.nReleaseNode].tick) + envval = envValueAtReleaseNode; + + if(m_playBehaviour[kLegacyReleaseNode]) + { + // Old, hard to grasp release node behaviour (additive) + int relativeVolumeChange = (envval - envValueAtReleaseNode) * 2; + envval = envValueAtReleaseJump + relativeVolumeChange; + } else + { + // New behaviour, truly relative to release node + if(envValueAtReleaseNode > 0) + envval = envValueAtReleaseJump * envval / envValueAtReleaseNode; + else + envval = 0; + } + } + vol = (vol * Clamp(envval, 0, 512)) / 256; + } + +} + + +void CSoundFile::ProcessPanningEnvelope(ModChannel &chn) const +{ + if(IsEnvelopeProcessed(chn, ENV_PANNING)) + { + const ModInstrument *pIns = chn.pModInstrument; + + if(m_playBehaviour[kITEnvelopePositionHandling] && chn.PanEnv.nEnvPosition == 0) + { + // If the envelope is disabled at the very same moment as it is triggered, we do not process anything. + return; + } + + const int envpos = chn.PanEnv.nEnvPosition - (m_playBehaviour[kITEnvelopePositionHandling] ? 1 : 0); + // Get values in [-32, 32] + const int envval = pIns->PanEnv.GetValueFromPosition(envpos, 64) - 32; + + int pan = chn.nRealPan; + if(pan >= 128) + { + pan += (envval * (256 - pan)) / 32; + } else + { + pan += (envval * (pan)) / 32; + } + chn.nRealPan = Clamp(pan, 0, 256); + + } +} + + +int CSoundFile::ProcessPitchFilterEnvelope(ModChannel &chn, int32 &period) const +{ + if(IsEnvelopeProcessed(chn, ENV_PITCH)) + { + const ModInstrument *pIns = chn.pModInstrument; + + if(m_playBehaviour[kITEnvelopePositionHandling] && chn.PitchEnv.nEnvPosition == 0) + { + // If the envelope is disabled at the very same moment as it is triggered, we do not process anything. + return -1; + } + + const int envpos = chn.PitchEnv.nEnvPosition - (m_playBehaviour[kITEnvelopePositionHandling] ? 1 : 0); + // Get values in [-256, 256] +#ifdef MODPLUG_TRACKER + const int32 range = ENVELOPE_MAX; + const int32 amp = 512; +#else + // TODO: AMS2 envelopes behave differently when linear slides are off - emulate with 15 * (-128...127) >> 6 + // Copy over vibrato behaviour for that? + const int32 range = GetType() == MOD_TYPE_AMS ? uint8_max : ENVELOPE_MAX; + int32 amp; + switch(GetType()) + { + case MOD_TYPE_AMS: amp = 64; break; + case MOD_TYPE_MDL: amp = 192; break; + default: amp = 512; + } +#endif + const int envval = pIns->PitchEnv.GetValueFromPosition(envpos, amp, range) - amp / 2; + + if(chn.PitchEnv.flags[ENV_FILTER]) + { + // Filter Envelope: controls cutoff frequency + return SetupChannelFilter(chn, !chn.dwFlags[CHN_FILTER], envval); + } else + { + // Pitch Envelope + if(chn.HasCustomTuning()) + { + if(chn.nFineTune != envval) + { + chn.nFineTune = mpt::saturate_cast<int16>(envval); + chn.m_CalculateFreq = true; + //Preliminary tests indicated that this behavior + //is very close to original(with 12TET) when finestep count + //is 15. + } + } else //Original behavior + { + const bool useFreq = PeriodsAreFrequencies(); + const uint32 (&upTable)[256] = useFreq ? LinearSlideUpTable : LinearSlideDownTable; + const uint32 (&downTable)[256] = useFreq ? LinearSlideDownTable : LinearSlideUpTable; + + int l = envval; + if(l < 0) + { + l = -l; + LimitMax(l, 255); + period = Util::muldiv(period, downTable[l], 65536); + } else + { + LimitMax(l, 255); + period = Util::muldiv(period, upTable[l], 65536); + } + } //End: Original behavior. + } + } + return -1; +} + + +void CSoundFile::IncrementEnvelopePosition(ModChannel &chn, EnvelopeType envType) const +{ + ModChannel::EnvInfo &chnEnv = chn.GetEnvelope(envType); + + if(chn.pModInstrument == nullptr || !chnEnv.flags[ENV_ENABLED]) + { + return; + } + + // Increase position + uint32 position = chnEnv.nEnvPosition + (m_playBehaviour[kITEnvelopePositionHandling] ? 0 : 1); + + const InstrumentEnvelope &insEnv = chn.pModInstrument->GetEnvelope(envType); + if(insEnv.empty()) + { + return; + } + + bool endReached = false; + + if(!m_playBehaviour[kITEnvelopePositionHandling]) + { + // FT2-style envelope processing. + if(insEnv.dwFlags[ENV_LOOP]) + { + // Normal loop active + uint32 end = insEnv[insEnv.nLoopEnd].tick; + if(!(GetType() & (MOD_TYPE_XM | MOD_TYPE_MT2))) end++; + + // FT2 compatibility: If the sustain point is at the loop end and the sustain loop has been released, don't loop anymore. + // Test case: EnvLoops.xm + const bool escapeLoop = (insEnv.nLoopEnd == insEnv.nSustainEnd && insEnv.dwFlags[ENV_SUSTAIN] && chn.dwFlags[CHN_KEYOFF] && m_playBehaviour[kFT2EnvelopeEscape]); + + if(position == end && !escapeLoop) + { + position = insEnv[insEnv.nLoopStart].tick; + } + } + + if(insEnv.dwFlags[ENV_SUSTAIN] && !chn.dwFlags[CHN_KEYOFF]) + { + // Envelope sustained + if(position == insEnv[insEnv.nSustainEnd].tick + 1u) + { + position = insEnv[insEnv.nSustainStart].tick; + // FT2 compatibility: If the panning envelope reaches its sustain point before key-off, it stays there forever. + // Test case: PanSustainRelease.xm + if(m_playBehaviour[kFT2PanSustainRelease] && envType == ENV_PANNING && !chn.dwFlags[CHN_KEYOFF]) + { + chnEnv.flags.reset(ENV_ENABLED); + } + } + } else + { + // Limit to last envelope point + if(position > insEnv.back().tick) + { + // Env of envelope + position = insEnv.back().tick; + endReached = true; + } + } + } else + { + // IT envelope processing. + // Test case: EnvLoops.it + uint32 start, end; + + // IT compatiblity: OpenMPT processes the key-off flag earlier than IT. Grab the flag from the previous tick instead. + // Test case: EnvOffLength.it + if(insEnv.dwFlags[ENV_SUSTAIN] && !chn.dwOldFlags[CHN_KEYOFF] && (chnEnv.nEnvValueAtReleaseJump == NOT_YET_RELEASED || m_playBehaviour[kReleaseNodePastSustainBug])) + { + // Envelope sustained + start = insEnv[insEnv.nSustainStart].tick; + end = insEnv[insEnv.nSustainEnd].tick + 1; + } else if(insEnv.dwFlags[ENV_LOOP]) + { + // Normal loop active + start = insEnv[insEnv.nLoopStart].tick; + end = insEnv[insEnv.nLoopEnd].tick + 1; + } else + { + // Limit to last envelope point + start = end = insEnv.back().tick; + if(position > end) + { + // Env of envelope + endReached = true; + } + } + + if(position >= end) + { + position = start; + } + } + + if(envType == ENV_VOLUME && endReached) + { + // Special handling for volume envelopes at end of envelope + if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (chn.dwFlags[CHN_KEYOFF] && GetType() != MOD_TYPE_MDL)) + { + chn.dwFlags.set(CHN_NOTEFADE); + } + + if(insEnv.back().value == 0 && (chn.nMasterChn > 0 || (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)))) + { + // Stop channel if the last envelope node is silent anyway. + chn.dwFlags.set(CHN_NOTEFADE); + chn.nFadeOutVol = 0; + chn.nRealVolume = 0; + chn.nCalcVolume = 0; + } + } + + chnEnv.nEnvPosition = position + (m_playBehaviour[kITEnvelopePositionHandling] ? 1 : 0); + +} + + +void CSoundFile::IncrementEnvelopePositions(ModChannel &chn) const +{ + if (chn.isFirstTick && GetType() == MOD_TYPE_MED) + return; + IncrementEnvelopePosition(chn, ENV_VOLUME); + IncrementEnvelopePosition(chn, ENV_PANNING); + IncrementEnvelopePosition(chn, ENV_PITCH); +} + + +void CSoundFile::ProcessInstrumentFade(ModChannel &chn, int &vol) const +{ + // FadeOut volume + if(chn.dwFlags[CHN_NOTEFADE] && chn.pModInstrument != nullptr) + { + const ModInstrument *pIns = chn.pModInstrument; + + uint32 fadeout = pIns->nFadeOut; + if (fadeout) + { + chn.nFadeOutVol -= fadeout * 2; + if (chn.nFadeOutVol <= 0) chn.nFadeOutVol = 0; + vol = (vol * chn.nFadeOutVol) / 65536; + } else if (!chn.nFadeOutVol) + { + vol = 0; + } + } +} + + +void CSoundFile::ProcessPitchPanSeparation(int32 &pan, int note, const ModInstrument &instr) +{ + if(!instr.nPPS || note == NOTE_NONE) + return; + // with PPS = 16 / PPC = C-5, E-6 will pan hard right (and D#6 will not) + int32 delta = (note - instr.nPPC - NOTE_MIN) * instr.nPPS / 2; + pan = Clamp(pan + delta, 0, 256); +} + + +void CSoundFile::ProcessPanbrello(ModChannel &chn) const +{ + int pdelta = chn.nPanbrelloOffset; + if(chn.rowCommand.command == CMD_PANBRELLO) + { + uint32 panpos; + // IT compatibility: IT has its own, more precise tables + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + panpos = chn.nPanbrelloPos; + else + panpos = ((chn.nPanbrelloPos + 0x10) >> 2); + + pdelta = GetVibratoDelta(chn.nPanbrelloType, panpos); + + // IT compatibility: Sample-and-hold style random panbrello (tremolo and vibrato don't use this mechanism in IT) + // Test case: RandomWaveform.it + if(m_playBehaviour[kITSampleAndHoldPanbrello] && chn.nPanbrelloType == 3) + { + if(chn.nPanbrelloPos == 0 || chn.nPanbrelloPos >= chn.nPanbrelloSpeed) + { + chn.nPanbrelloPos = 0; + chn.nPanbrelloRandomMemory = static_cast<int8>(pdelta); + } + chn.nPanbrelloPos++; + pdelta = chn.nPanbrelloRandomMemory; + } else + { + chn.nPanbrelloPos += chn.nPanbrelloSpeed; + } + // IT compatibility: Panbrello effect is active until next note or panning command. + // Test case: PanbrelloHold.it + if(m_playBehaviour[kITPanbrelloHold]) + { + chn.nPanbrelloOffset = static_cast<int8>(pdelta); + } + } + if(pdelta) + { + pdelta = ((pdelta * (int)chn.nPanbrelloDepth) + 2) / 8; + pdelta += chn.nRealPan; + chn.nRealPan = Clamp(pdelta, 0, 256); + } +} + + +void CSoundFile::ProcessArpeggio(CHANNELINDEX nChn, int32 &period, Tuning::NOTEINDEXTYPE &arpeggioSteps) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + +#ifndef NO_PLUGINS + // Plugin arpeggio + if(chn.pModInstrument && chn.pModInstrument->nMixPlug + && !chn.pModInstrument->dwFlags[INS_MUTE] + && !chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE]) + { + const ModInstrument *pIns = chn.pModInstrument; + IMixPlugin *pPlugin = m_MixPlugins[pIns->nMixPlug - 1].pMixPlugin; + if(pPlugin) + { + uint8 step = 0; + const bool arpOnRow = (chn.rowCommand.command == CMD_ARPEGGIO); + const ModCommand::NOTE lastNote = ModCommand::IsNote(chn.nLastNote) ? static_cast<ModCommand::NOTE>(pIns->NoteMap[chn.nLastNote - NOTE_MIN]) : static_cast<ModCommand::NOTE>(NOTE_NONE); + if(arpOnRow) + { + switch(m_PlayState.m_nTickCount % 3) + { + case 1: step = chn.nArpeggio >> 4; break; + case 2: step = chn.nArpeggio & 0x0F; break; + } + chn.nArpeggioBaseNote = lastNote; + } + + // Trigger new note: + // - If there's an arpeggio on this row and + // - the note to trigger is not the same as the previous arpeggio note or + // - a pattern note has just been triggered on this tick + // - If there's no arpeggio + // - but an arpeggio note is still active and + // - there's no note stop or new note that would stop it anyway + if((arpOnRow && chn.nArpeggioLastNote != chn.nArpeggioBaseNote + step && (!m_SongFlags[SONG_FIRSTTICK] || !chn.rowCommand.IsNote())) + || (!arpOnRow && chn.rowCommand.note == NOTE_NONE && chn.nArpeggioLastNote != NOTE_NONE)) + SendMIDINote(nChn, chn.nArpeggioBaseNote + step, static_cast<uint16>(chn.nVolume)); + // Stop note: + // - If some arpeggio note is still registered or + // - When starting an arpeggio on a row with no other note on it, stop some possibly still playing note. + if(chn.nArpeggioLastNote != NOTE_NONE) + SendMIDINote(nChn, chn.nArpeggioLastNote + NOTE_MAX_SPECIAL, 0); + else if(arpOnRow && m_SongFlags[SONG_FIRSTTICK] && !chn.rowCommand.IsNote() && ModCommand::IsNote(lastNote)) + SendMIDINote(nChn, lastNote + NOTE_MAX_SPECIAL, 0); + + if(chn.rowCommand.command == CMD_ARPEGGIO) + chn.nArpeggioLastNote = chn.nArpeggioBaseNote + step; + else + chn.nArpeggioLastNote = NOTE_NONE; + } + } +#endif // NO_PLUGINS + + if(chn.nCommand == CMD_ARPEGGIO) + { + if(chn.HasCustomTuning()) + { + switch(m_PlayState.m_nTickCount % 3) + { + case 0: arpeggioSteps = 0; break; + case 1: arpeggioSteps = chn.nArpeggio >> 4; break; + case 2: arpeggioSteps = chn.nArpeggio & 0x0F; break; + } + chn.m_CalculateFreq = true; + chn.m_ReCalculateFreqOnFirstTick = true; + } else + { + if(GetType() == MOD_TYPE_MT2 && m_SongFlags[SONG_FIRSTTICK]) + { + // MT2 resets any previous portamento when an arpeggio occurs. + chn.nPeriod = period = GetPeriodFromNote(chn.nNote, chn.nFineTune, chn.nC5Speed); + } + + if(m_playBehaviour[kITArpeggio]) + { + //IT playback compatibility 01 & 02 + + // Pattern delay restarts tick counting. Not quite correct yet! + const uint32 tick = m_PlayState.m_nTickCount % (m_PlayState.m_nMusicSpeed + m_PlayState.m_nFrameDelay); + if(chn.nArpeggio != 0) + { + uint32 arpRatio = 65536; + switch(tick % 3) + { + case 1: arpRatio = LinearSlideUpTable[(chn.nArpeggio >> 4) * 16]; break; + case 2: arpRatio = LinearSlideUpTable[(chn.nArpeggio & 0x0F) * 16]; break; + } + if(PeriodsAreFrequencies()) + period = Util::muldivr(period, arpRatio, 65536); + else + period = Util::muldivr(period, 65536, arpRatio); + } + } else if(m_playBehaviour[kFT2Arpeggio]) + { + // FastTracker 2: Swedish tracker logic (TM) arpeggio + if(!m_SongFlags[SONG_FIRSTTICK]) + { + // Arpeggio is added on top of current note, but cannot do it the IT way because of + // the behaviour in ArpeggioClamp.xm. + // Test case: ArpSlide.xm + uint32 note = 0; + + // The fact that arpeggio behaves in a totally fucked up way at 16 ticks/row or more is that the arpeggio offset LUT only has 16 entries in FT2. + // At more than 16 ticks/row, FT2 reads into the vibrato table, which is placed right after the arpeggio table. + // Test case: Arpeggio.xm + int arpPos = m_PlayState.m_nMusicSpeed - (m_PlayState.m_nTickCount % m_PlayState.m_nMusicSpeed); + if(arpPos > 16) + arpPos = 2; + else if(arpPos == 16) + arpPos = 0; + else + arpPos %= 3; + switch(arpPos) + { + case 1: note = (chn.nArpeggio >> 4); break; + case 2: note = (chn.nArpeggio & 0x0F); break; + } + + if(arpPos != 0) + { + // Arpeggio is added on top of current note, but cannot do it the IT way because of + // the behaviour in ArpeggioClamp.xm. + // Test case: ArpSlide.xm + note += GetNoteFromPeriod(period, chn.nFineTune, chn.nC5Speed); + + period = GetPeriodFromNote(note, chn.nFineTune, chn.nC5Speed); + + // FT2 compatibility: FT2 has a different note limit for Arpeggio. + // Test case: ArpeggioClamp.xm + if(note >= 108 + NOTE_MIN) + { + period = std::max(static_cast<uint32>(period), GetPeriodFromNote(108 + NOTE_MIN, 0, chn.nC5Speed)); + } + } + } + } + // Other trackers + else + { + uint32 tick = m_PlayState.m_nTickCount; + + // TODO other likely formats for MOD case: MED, OKT, etc + uint8 note = (GetType() != MOD_TYPE_MOD) ? chn.nNote : static_cast<uint8>(GetNoteFromPeriod(period, chn.nFineTune, chn.nC5Speed)); + if(GetType() & (MOD_TYPE_DBM | MOD_TYPE_DIGI)) + tick += 2; + switch(tick % 3) + { + case 1: note += (chn.nArpeggio >> 4); break; + case 2: note += (chn.nArpeggio & 0x0F); break; + } + if(note != chn.nNote || (GetType() & (MOD_TYPE_DBM | MOD_TYPE_DIGI | MOD_TYPE_STM)) || m_playBehaviour[KST3PortaAfterArpeggio]) + { + if(m_SongFlags[SONG_PT_MODE]) + { + // Weird arpeggio wrap-around in ProTracker. + // Test case: ArpWraparound.mod, and the snare sound in "Jim is dead" by doh. + if(note == NOTE_MIDDLEC + 24) + { + period = int32_max; + return; + } else if(note > NOTE_MIDDLEC + 24) + { + note -= 37; + } + } + period = GetPeriodFromNote(note, chn.nFineTune, chn.nC5Speed); + + if(GetType() & (MOD_TYPE_DBM | MOD_TYPE_DIGI | MOD_TYPE_PSM | MOD_TYPE_STM | MOD_TYPE_OKT)) + { + // The arpeggio note offset remains effective after the end of the current row in ScreamTracker 2. + // This fixes the flute lead in MORPH.STM by Skaven, pattern 27. + // Note that ScreamTracker 2.24 handles arpeggio slightly differently: It only considers the lower + // nibble, and switches to that note halfway through the row. + chn.nPeriod = period; + } else if(m_playBehaviour[KST3PortaAfterArpeggio]) + { + chn.nArpeggioLastNote = note; + } + } + } + } + } +} + + +void CSoundFile::ProcessVibrato(CHANNELINDEX nChn, int32 &period, Tuning::RATIOTYPE &vibratoFactor) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + + if(chn.dwFlags[CHN_VIBRATO]) + { + const bool advancePosition = !m_SongFlags[SONG_FIRSTTICK] || ((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !(m_SongFlags[SONG_ITOLDEFFECTS])); + + if(GetType() == MOD_TYPE_669) + { + if(chn.nVibratoPos % 2u) + { + period += chn.nVibratoDepth * 167; // Already multiplied by 4, and it seems like the real factor here is 669... how original =) + } + chn.nVibratoPos++; + return; + } + + // IT compatibility: IT has its own, more precise tables and pre-increments the vibrato position + if(advancePosition && m_playBehaviour[kITVibratoTremoloPanbrello]) + chn.nVibratoPos += 4 * chn.nVibratoSpeed; + + int vdelta = GetVibratoDelta(chn.nVibratoType, chn.nVibratoPos); + + if(chn.HasCustomTuning()) + { + //Hack implementation: Scaling vibratofactor to [0.95; 1.05] + //using figure from above tables and vibratodepth parameter + vibratoFactor += 0.05f * (vdelta * chn.nVibratoDepth) / (128.0f * 60.0f); + chn.m_CalculateFreq = true; + chn.m_ReCalculateFreqOnFirstTick = false; + + if(m_PlayState.m_nTickCount + 1 == m_PlayState.m_nMusicSpeed) + chn.m_ReCalculateFreqOnFirstTick = true; + } else + { + // Original behaviour + if(m_SongFlags.test_all(SONG_FIRSTTICK | SONG_PT_MODE) || ((GetType() & (MOD_TYPE_DIGI | MOD_TYPE_DBM)) && m_SongFlags[SONG_FIRSTTICK])) + { + // ProTracker doesn't apply vibrato nor advance on the first tick. + // Test case: VibratoReset.mod + return; + } else if((GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) && (chn.nVibratoType & 0x03) == 1) + { + // FT2 compatibility: Vibrato ramp down table is upside down. + // Test case: VibratoWaveforms.xm + vdelta = -vdelta; + } + + uint32 vdepth; + // IT compatibility: correct vibrato depth + if(m_playBehaviour[kITVibratoTremoloPanbrello]) + { + // Yes, vibrato goes backwards with old effects enabled! + if(m_SongFlags[SONG_ITOLDEFFECTS]) + { + // Test case: vibrato-oldfx.it + vdepth = 5; + } else + { + // Test case: vibrato.it + vdepth = 6; + vdelta = -vdelta; + } + } else + { + if(m_SongFlags[SONG_S3MOLDVIBRATO]) + vdepth = 5; + else if(GetType() == MOD_TYPE_DTM) + vdepth = 8; + else if(GetType() & (MOD_TYPE_DBM | MOD_TYPE_MTM)) + vdepth = 7; + else if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && !m_SongFlags[SONG_ITOLDEFFECTS]) + vdepth = 7; + else + vdepth = 6; + + // ST3 compatibility: Do not distinguish between vibrato types in effect memory + // Test case: VibratoTypeChange.s3m + if(m_playBehaviour[kST3VibratoMemory] && chn.rowCommand.command == CMD_FINEVIBRATO) + vdepth += 2; + } + + vdelta = (-vdelta * static_cast<int>(chn.nVibratoDepth)) / (1 << vdepth); + + DoFreqSlide(chn, period, vdelta); + + // Process MIDI vibrato for plugins: +#ifndef NO_PLUGINS + IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); + if(plugin != nullptr) + { + // If the Pitch Wheel Depth is configured correctly (so it's the same as the plugin's PWD), + // MIDI vibrato will sound identical to vibrato with linear slides enabled. + int8 pwd = 2; + if(chn.pModInstrument != nullptr) + { + pwd = chn.pModInstrument->midiPWD; + } + plugin->MidiVibrato(vdelta, pwd, nChn); + } +#endif // NO_PLUGINS + } + + // Advance vibrato position - IT updates on every tick, unless "old effects" are enabled (in this case it only updates on non-first ticks like other trackers) + // IT compatibility: IT has its own, more precise tables and pre-increments the vibrato position + if(advancePosition && !m_playBehaviour[kITVibratoTremoloPanbrello]) + chn.nVibratoPos += chn.nVibratoSpeed; + } else if(chn.dwOldFlags[CHN_VIBRATO]) + { + // Stop MIDI vibrato for plugins: +#ifndef NO_PLUGINS + IMixPlugin *plugin = GetChannelInstrumentPlugin(m_PlayState.Chn[nChn]); + if(plugin != nullptr) + { + plugin->MidiVibrato(0, 0, nChn); + } +#endif // NO_PLUGINS + } +} + + +void CSoundFile::ProcessSampleAutoVibrato(ModChannel &chn, int32 &period, Tuning::RATIOTYPE &vibratoFactor, int &nPeriodFrac) const +{ + // Sample Auto-Vibrato + if(chn.pModSample != nullptr && chn.pModSample->nVibDepth) + { + const ModSample *pSmp = chn.pModSample; + const bool hasTuning = chn.HasCustomTuning(); + + // In IT compatible mode, we use always frequencies, otherwise we use periods, which are upside down. + // In this context, the "up" tables refer to the tables that increase frequency, and the down tables are the ones that decrease frequency. + const bool useFreq = PeriodsAreFrequencies(); + const uint32 (&upTable)[256] = useFreq ? LinearSlideUpTable : LinearSlideDownTable; + const uint32 (&downTable)[256] = useFreq ? LinearSlideDownTable : LinearSlideUpTable; + const uint32 (&fineUpTable)[16] = useFreq ? FineLinearSlideUpTable : FineLinearSlideDownTable; + const uint32 (&fineDownTable)[16] = useFreq ? FineLinearSlideDownTable : FineLinearSlideUpTable; + + // IT compatibility: Autovibrato is so much different in IT that I just put this in a separate code block, to get rid of a dozen IsCompatibilityMode() calls. + if(m_playBehaviour[kITVibratoTremoloPanbrello] && !hasTuning && GetType() != MOD_TYPE_MT2) + { + if(!pSmp->nVibRate) + return; + + // Schism's autovibrato code + + /* + X86 Assembler from ITTECH.TXT: + 1) Mov AX, [SomeVariableNameRelatingToVibrato] + 2) Add AL, Rate + 3) AdC AH, 0 + 4) AH contains the depth of the vibrato as a fine-linear slide. + 5) Mov [SomeVariableNameRelatingToVibrato], AX ; For the next cycle. + */ + const int vibpos = chn.nAutoVibPos & 0xFF; + int adepth = chn.nAutoVibDepth; // (1) + adepth += pSmp->nVibSweep; // (2 & 3) + LimitMax(adepth, static_cast<int>(pSmp->nVibDepth * 256u)); + chn.nAutoVibDepth = adepth; // (5) + adepth /= 256; // (4) + + chn.nAutoVibPos += pSmp->nVibRate; + + int vdelta; + switch(pSmp->nVibType) + { + case VIB_RANDOM: + vdelta = mpt::random<int, 7>(AccessPRNG()) - 0x40; + break; + case VIB_RAMP_DOWN: + vdelta = 64 - (vibpos + 1) / 2; + break; + case VIB_RAMP_UP: + vdelta = ((vibpos + 1) / 2) - 64; + break; + case VIB_SQUARE: + vdelta = vibpos < 128 ? 64 : 0; + break; + case VIB_SINE: + default: + vdelta = ITSinusTable[vibpos]; + break; + } + + vdelta = (vdelta * adepth) / 64; + uint32 l = std::abs(vdelta); + LimitMax(period, Util::MaxValueOfType(period) / 256); + period *= 256; + if(vdelta < 0) + { + vdelta = Util::muldiv(period, downTable[l / 4u], 0x10000) - period; + if (l & 0x03) + { + vdelta += Util::muldiv(period, fineDownTable[l & 0x03], 0x10000) - period; + } + } else + { + vdelta = Util::muldiv(period, upTable[l / 4u], 0x10000) - period; + if (l & 0x03) + { + vdelta += Util::muldiv(period, fineUpTable[l & 0x03], 0x10000) - period; + } + } + period = (period + vdelta) / 256; + nPeriodFrac = vdelta & 0xFF; + } else + { + // MPT's autovibrato code + if (pSmp->nVibSweep == 0 && !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) + { + chn.nAutoVibDepth = pSmp->nVibDepth * 256; + } else + { + // Calculate current autovibrato depth using vibsweep + if (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + chn.nAutoVibDepth += pSmp->nVibSweep * 2u; + } else + { + if(!chn.dwFlags[CHN_KEYOFF]) + { + chn.nAutoVibDepth += (pSmp->nVibDepth * 256u) / pSmp->nVibSweep; + } + } + LimitMax(chn.nAutoVibDepth, static_cast<int>(pSmp->nVibDepth * 256u)); + } + chn.nAutoVibPos += pSmp->nVibRate; + int vdelta; + switch(pSmp->nVibType) + { + case VIB_RANDOM: + vdelta = ModRandomTable[chn.nAutoVibPos & 0x3F]; + chn.nAutoVibPos++; + break; + case VIB_RAMP_DOWN: + vdelta = ((0x40 - (chn.nAutoVibPos / 2u)) & 0x7F) - 0x40; + break; + case VIB_RAMP_UP: + vdelta = ((0x40 + (chn.nAutoVibPos / 2u)) & 0x7F) - 0x40; + break; + case VIB_SQUARE: + vdelta = (chn.nAutoVibPos & 128) ? +64 : -64; + break; + case VIB_SINE: + default: + if(GetType() != MOD_TYPE_MT2) + { + vdelta = -ITSinusTable[chn.nAutoVibPos & 0xFF]; + } else + { + // Fix flat-sounding pads in "another worlds" by Eternal Engine. + // Vibrato starts at the maximum amplitude of the sine wave + // and the vibrato frequency never decreases below the original note's frequency. + vdelta = (-ITSinusTable[(chn.nAutoVibPos + 192) & 0xFF] + 64) / 2; + } + } + int n = (vdelta * chn.nAutoVibDepth) / 256; + + if(hasTuning) + { + //Vib sweep is not taken into account here. + vibratoFactor += 0.05F * pSmp->nVibDepth * vdelta / 4096.0f; //4096 == 64^2 + //See vibrato for explanation. + chn.m_CalculateFreq = true; + /* + Finestep vibrato: + const float autoVibDepth = pSmp->nVibDepth * val / 4096.0f; //4096 == 64^2 + vibratoFineSteps += static_cast<CTuning::FINESTEPTYPE>(chn.pModInstrument->pTuning->GetFineStepCount() * autoVibDepth); + chn.m_CalculateFreq = true; + */ + } + else //Original behavior + { + if (GetType() != MOD_TYPE_XM) + { + int df1, df2; + if (n < 0) + { + n = -n; + uint32 n1 = n / 256; + df1 = downTable[n1]; + df2 = downTable[n1+1]; + } else + { + uint32 n1 = n / 256; + df1 = upTable[n1]; + df2 = upTable[n1+1]; + } + n /= 4; + period = Util::muldiv(period, df1 + ((df2 - df1) * (n & 0x3F) / 64), 256); + nPeriodFrac = period & 0xFF; + period /= 256; + } else + { + period += (n / 64); + } + } //Original MPT behavior + } + } +} + + +void CSoundFile::ProcessRamping(ModChannel &chn) const +{ + chn.leftRamp = chn.rightRamp = 0; + LimitMax(chn.newLeftVol, int32_max >> VOLUMERAMPPRECISION); + LimitMax(chn.newRightVol, int32_max >> VOLUMERAMPPRECISION); + if(chn.dwFlags[CHN_VOLUMERAMP] && (chn.leftVol != chn.newLeftVol || chn.rightVol != chn.newRightVol)) + { + const bool rampUp = (chn.newLeftVol > chn.leftVol) || (chn.newRightVol > chn.rightVol); + int32 rampLength, globalRampLength, instrRampLength = 0; + rampLength = globalRampLength = (rampUp ? m_MixerSettings.GetVolumeRampUpSamples() : m_MixerSettings.GetVolumeRampDownSamples()); + //XXXih: add real support for bidi ramping here + + if(m_playBehaviour[kFT2VolumeRamping] && (GetType() & MOD_TYPE_XM)) + { + // apply FT2-style super-soft volume ramping (5ms), overriding openmpt settings + rampLength = globalRampLength = Util::muldivr(5, m_MixerSettings.gdwMixingFreq, 1000); + } + + if(chn.pModInstrument != nullptr && rampUp) + { + instrRampLength = chn.pModInstrument->nVolRampUp; + rampLength = instrRampLength ? (m_MixerSettings.gdwMixingFreq * instrRampLength / 100000) : globalRampLength; + } + const bool enableCustomRamp = (instrRampLength > 0); + + if(!rampLength) + { + rampLength = 1; + } + + int32 leftDelta = ((chn.newLeftVol - chn.leftVol) * (1 << VOLUMERAMPPRECISION)); + int32 rightDelta = ((chn.newRightVol - chn.rightVol) * (1 << VOLUMERAMPPRECISION)); + if(!enableCustomRamp) + { + // Extra-smooth ramping, unless we're forced to use the default values + if((chn.leftVol | chn.rightVol) && (chn.newLeftVol | chn.newRightVol) && !chn.dwFlags[CHN_FASTVOLRAMP]) + { + rampLength = m_PlayState.m_nBufferCount; + Limit(rampLength, globalRampLength, int32(1 << (VOLUMERAMPPRECISION - 1))); + } + } + + chn.leftRamp = leftDelta / rampLength; + chn.rightRamp = rightDelta / rampLength; + chn.leftVol = chn.newLeftVol - ((chn.leftRamp * rampLength) / (1 << VOLUMERAMPPRECISION)); + chn.rightVol = chn.newRightVol - ((chn.rightRamp * rampLength) / (1 << VOLUMERAMPPRECISION)); + + if (chn.leftRamp|chn.rightRamp) + { + chn.nRampLength = rampLength; + } else + { + chn.dwFlags.reset(CHN_VOLUMERAMP); + chn.leftVol = chn.newLeftVol; + chn.rightVol = chn.newRightVol; + } + } else + { + chn.dwFlags.reset(CHN_VOLUMERAMP); + chn.leftVol = chn.newLeftVol; + chn.rightVol = chn.newRightVol; + } + chn.rampLeftVol = chn.leftVol * (1 << VOLUMERAMPPRECISION); + chn.rampRightVol = chn.rightVol * (1 << VOLUMERAMPPRECISION); + chn.dwFlags.reset(CHN_FASTVOLRAMP); +} + + +// Returns channel increment and frequency with FREQ_FRACBITS fractional bits +std::pair<SamplePosition, uint32> CSoundFile::GetChannelIncrement(const ModChannel &chn, uint32 period, int periodFrac) const +{ + uint32 freq; + if(!chn.HasCustomTuning()) + freq = GetFreqFromPeriod(period, chn.nC5Speed, periodFrac); + else + freq = chn.nPeriod; + + const ModInstrument *ins = chn.pModInstrument; + + if(int32 finetune = chn.microTuning; finetune != 0) + { + if(ins) + finetune *= ins->midiPWD; + if(finetune) + freq = mpt::saturate_round<uint32>(freq * std::pow(2.0, finetune / (12.0 * 256.0 * 128.0))); + } + + // Applying Pitch/Tempo lock + if(ins && ins->pitchToTempoLock.GetRaw()) + { + freq = Util::muldivr(freq, m_PlayState.m_nMusicTempo.GetRaw(), ins->pitchToTempoLock.GetRaw()); + } + + // Avoid increment to overflow and become negative with unrealisticly high frequencies. + LimitMax(freq, uint32(int32_max)); + return {SamplePosition::Ratio(freq, m_MixerSettings.gdwMixingFreq << FREQ_FRACBITS), freq}; +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Handles envelopes & mixer setup + +bool CSoundFile::ReadNote() +{ +#ifdef MODPLUG_TRACKER + // Checking end of row ? + if(m_SongFlags[SONG_PAUSED]) + { + m_PlayState.m_nTickCount = 0; + if (!m_PlayState.m_nMusicSpeed) m_PlayState.m_nMusicSpeed = 6; + if (!m_PlayState.m_nMusicTempo.GetRaw()) m_PlayState.m_nMusicTempo.Set(125); + } else +#endif // MODPLUG_TRACKER + { + if(!ProcessRow()) + return false; + } + //////////////////////////////////////////////////////////////////////////////////// + if (m_PlayState.m_nMusicTempo.GetRaw() == 0) return false; + + m_PlayState.m_nSamplesPerTick = GetTickDuration(m_PlayState); + m_PlayState.m_nBufferCount = m_PlayState.m_nSamplesPerTick; + + // Master Volume + Pre-Amplification / Attenuation setup + uint32 nMasterVol; + { + CHANNELINDEX nchn32 = Clamp(m_nChannels, CHANNELINDEX(1), CHANNELINDEX(31)); + + uint32 mastervol; + + if (m_PlayConfig.getUseGlobalPreAmp()) + { + int realmastervol = m_MixerSettings.m_nPreAmp; + if (realmastervol > 0x80) + { + //Attenuate global pre-amp depending on num channels + realmastervol = 0x80 + ((realmastervol - 0x80) * (nchn32 + 4)) / 16; + } + mastervol = (realmastervol * (m_nSamplePreAmp)) / 64; + } else + { + //Preferred option: don't use global pre-amp at all. + mastervol = m_nSamplePreAmp; + } + + if (m_PlayConfig.getUseGlobalPreAmp()) + { + uint32 attenuation = +#ifndef NO_AGC + (m_MixerSettings.DSPMask & SNDDSP_AGC) ? PreAmpAGCTable[nchn32 / 2u] : +#endif + PreAmpTable[nchn32 / 2u]; + if(attenuation < 1) attenuation = 1; + nMasterVol = (mastervol << 7) / attenuation; + } else + { + nMasterVol = mastervol; + } + } + + //////////////////////////////////////////////////////////////////////////////////// + // Update channels data + m_nMixChannels = 0; + for (CHANNELINDEX nChn = 0; nChn < MAX_CHANNELS; nChn++) + { + ModChannel &chn = m_PlayState.Chn[nChn]; + // FT2 Compatibility: Prevent notes to be stopped after a fadeout. This way, a portamento effect can pick up a faded instrument which is long enough. + // This occurs for example in the bassline (channel 11) of jt_burn.xm. I hope this won't break anything else... + // I also suppose this could decrease mixing performance a bit, but hey, which CPU can't handle 32 muted channels these days... :-) + if(chn.dwFlags[CHN_NOTEFADE] && (!(chn.nFadeOutVol|chn.leftVol|chn.rightVol)) && !m_playBehaviour[kFT2ProcessSilentChannels]) + { + chn.nLength = 0; + chn.nROfs = chn.nLOfs = 0; + } + // Check for unused channel + if(chn.dwFlags[CHN_MUTE] || (nChn >= m_nChannels && !chn.nLength)) + { + if(nChn < m_nChannels) + { + // Process MIDI macros on channels that are currently muted. + ProcessMacroOnChannel(nChn); + } + chn.nLeftVU = chn.nRightVU = 0; + continue; + } + // Reset channel data + chn.increment = SamplePosition(0); + chn.nRealVolume = 0; + chn.nCalcVolume = 0; + + chn.nRampLength = 0; + + //Aux variables + Tuning::RATIOTYPE vibratoFactor = 1; + Tuning::NOTEINDEXTYPE arpeggioSteps = 0; + + const ModInstrument *pIns = chn.pModInstrument; + + // Calc Frequency + int32 period = 0; + + // Also process envelopes etc. when there's a plugin on this channel, for possible fake automation using volume and pan data. + // We only care about master channels, though, since automation only "happens" on them. + const bool samplePlaying = (chn.nPeriod && chn.nLength); + const bool plugAssigned = (nChn < m_nChannels) && (ChnSettings[nChn].nMixPlugin || (chn.pModInstrument != nullptr && chn.pModInstrument->nMixPlug)); + if (samplePlaying || plugAssigned) + { + int vol = chn.nVolume; + int insVol = chn.nInsVol; // This is the "SV * IV" value in ITTECH.TXT + + ProcessVolumeSwing(chn, m_playBehaviour[kITSwingBehaviour] ? insVol : vol); + ProcessPanningSwing(chn); + ProcessTremolo(chn, vol); + ProcessTremor(nChn, vol); + + // Clip volume and multiply (extend to 14 bits) + Limit(vol, 0, 256); + vol <<= 6; + + // Process Envelopes + if (pIns) + { + if(m_playBehaviour[kITEnvelopePositionHandling]) + { + // In IT compatible mode, envelope position indices are shifted by one for proper envelope pausing, + // so we have to update the position before we actually process the envelopes. + // When using MPT behaviour, we get the envelope position for the next tick while we are still calculating the current tick, + // which then results in wrong position information when the envelope is paused on the next row. + // Test cases: s77.it + IncrementEnvelopePositions(chn); + } + ProcessVolumeEnvelope(chn, vol); + ProcessInstrumentFade(chn, vol); + ProcessPanningEnvelope(chn); + + if(!m_playBehaviour[kITPitchPanSeparation] && chn.nNote != NOTE_NONE && chn.pModInstrument && chn.pModInstrument->nPPS != 0) + ProcessPitchPanSeparation(chn.nRealPan, chn.nNote, *chn.pModInstrument); + } else + { + // No Envelope: key off => note cut + if(chn.dwFlags[CHN_NOTEFADE]) // 1.41-: CHN_KEYOFF|CHN_NOTEFADE + { + chn.nFadeOutVol = 0; + vol = 0; + } + } + + if(chn.isPaused) + vol = 0; + + // vol is 14-bits + if (vol) + { + // IMPORTANT: chn.nRealVolume is 14 bits !!! + // -> Util::muldiv( 14+8, 6+6, 18); => RealVolume: 14-bit result (22+12-20) + + if(chn.dwFlags[CHN_SYNCMUTE]) + { + chn.nRealVolume = 0; + } else if (m_PlayConfig.getGlobalVolumeAppliesToMaster()) + { + // Don't let global volume affect level of sample if + // Global volume is going to be applied to master output anyway. + chn.nRealVolume = Util::muldiv(vol * MAX_GLOBAL_VOLUME, chn.nGlobalVol * insVol, 1 << 20); + } else + { + chn.nRealVolume = Util::muldiv(vol * m_PlayState.m_nGlobalVolume, chn.nGlobalVol * insVol, 1 << 20); + } + } + + chn.nCalcVolume = vol; // Update calculated volume for MIDI macros + + // ST3 only clamps the final output period, but never the channel's internal period. + // Test case: PeriodLimit.s3m + if (chn.nPeriod < m_nMinPeriod + && GetType() != MOD_TYPE_S3M + && !PeriodsAreFrequencies()) + { + chn.nPeriod = m_nMinPeriod; + } else if(chn.nPeriod >= m_nMaxPeriod && m_playBehaviour[kApplyUpperPeriodLimit] && !PeriodsAreFrequencies()) + { + // ...but on the other hand, ST3's SoundBlaster driver clamps the maximum channel period. + // Test case: PeriodLimitUpper.s3m + chn.nPeriod = m_nMaxPeriod; + } + if(m_playBehaviour[kFT2Periods]) Clamp(chn.nPeriod, 1, 31999); + period = chn.nPeriod; + + // When glissando mode is set to semitones, clamp to the next halftone. + if((chn.dwFlags & (CHN_GLISSANDO | CHN_PORTAMENTO)) == (CHN_GLISSANDO | CHN_PORTAMENTO) + && (!m_SongFlags[SONG_PT_MODE] || (chn.rowCommand.IsPortamento() && !m_SongFlags[SONG_FIRSTTICK]))) + { + if(period != chn.cachedPeriod) + { + // Only recompute this whole thing in case the base period has changed. + chn.cachedPeriod = period; + chn.glissandoPeriod = GetPeriodFromNote(GetNoteFromPeriod(period, chn.nFineTune, chn.nC5Speed), chn.nFineTune, chn.nC5Speed); + } + period = chn.glissandoPeriod; + } + + ProcessArpeggio(nChn, period, arpeggioSteps); + + // Preserve Amiga freq limits. + // In ST3, the frequency is always clamped to periods 113 to 856, while in ProTracker, + // the limit is variable, depending on the finetune of the sample. + // The int32_max test is for the arpeggio wrap-around in ProcessArpeggio(). + // Test case: AmigaLimits.s3m, AmigaLimitsFinetune.mod + if(m_SongFlags[SONG_AMIGALIMITS | SONG_PT_MODE] && period != int32_max) + { + int limitLow = 113 * 4, limitHigh = 856 * 4; + if(GetType() != MOD_TYPE_S3M) + { + const int tableOffset = XM2MODFineTune(chn.nFineTune) * 12; + limitLow = ProTrackerTunedPeriods[tableOffset + 11] / 2; + limitHigh = ProTrackerTunedPeriods[tableOffset] * 2; + // Amiga cannot actually keep up with lower periods + if(limitLow < 113 * 4) limitLow = 113 * 4; + } + Limit(period, limitLow, limitHigh); + Limit(chn.nPeriod, limitLow, limitHigh); + } + + ProcessPanbrello(chn); + } + + // IT Compatibility: Ensure that there is no pan swing, panbrello, panning envelopes, etc. applied on surround channels. + // Test case: surround-pan.it + if(chn.dwFlags[CHN_SURROUND] && !m_SongFlags[SONG_SURROUNDPAN] && m_playBehaviour[kITNoSurroundPan]) + { + chn.nRealPan = 128; + } + + // Now that all relevant envelopes etc. have been processed, we can parse the MIDI macro data. + ProcessMacroOnChannel(nChn); + + // After MIDI macros have been processed, we can also process the pitch / filter envelope and other pitch-related things. + if(samplePlaying) + { + int cutoff = ProcessPitchFilterEnvelope(chn, period); + if(cutoff >= 0 && chn.dwFlags[CHN_ADLIB] && m_opl) + { + // Cutoff doubles as modulator intensity for FM instruments + m_opl->Volume(nChn, static_cast<uint8>(cutoff / 4), true); + } + } + + if(chn.rowCommand.volcmd == VOLCMD_VIBRATODEPTH && + (chn.rowCommand.command == CMD_VIBRATO || chn.rowCommand.command == CMD_VIBRATOVOL || chn.rowCommand.command == CMD_FINEVIBRATO)) + { + if(GetType() == MOD_TYPE_XM) + { + // XM Compatibility: Vibrato should be advanced twice (but not added up) if both volume-column and effect column vibrato is present. + // Effect column vibrato parameter has precedence if non-zero. + // Test case: VibratoDouble.xm + if(!m_SongFlags[SONG_FIRSTTICK]) + chn.nVibratoPos += chn.nVibratoSpeed; + } else if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + // IT Compatibility: Vibrato should be applied twice if both volume-colum and effect column vibrato is present. + // Volume column vibrato parameter has precedence if non-zero. + // Test case: VibratoDouble.it + Vibrato(chn, chn.rowCommand.vol); + ProcessVibrato(nChn, period, vibratoFactor); + } + } + // Plugins may also receive vibrato + ProcessVibrato(nChn, period, vibratoFactor); + + if(samplePlaying) + { + int nPeriodFrac = 0; + ProcessSampleAutoVibrato(chn, period, vibratoFactor, nPeriodFrac); + + // Final Period + // ST3 only clamps the final output period, but never the channel's internal period. + // Test case: PeriodLimit.s3m + if (period <= m_nMinPeriod) + { + if(m_playBehaviour[kST3LimitPeriod]) chn.nLength = 0; // Pattern 15 in watcha.s3m + period = m_nMinPeriod; + } + + const bool hasTuning = chn.HasCustomTuning(); + if(hasTuning) + { + if(chn.m_CalculateFreq || (chn.m_ReCalculateFreqOnFirstTick && m_PlayState.m_nTickCount == 0)) + { + chn.RecalcTuningFreq(vibratoFactor, arpeggioSteps, *this); + if(!chn.m_CalculateFreq) + chn.m_ReCalculateFreqOnFirstTick = false; + else + chn.m_CalculateFreq = false; + } + } + + auto [ninc, freq] = GetChannelIncrement(chn, period, nPeriodFrac); +#ifndef MODPLUG_TRACKER + ninc.MulDiv(m_nFreqFactor, 65536); +#endif // !MODPLUG_TRACKER + if(ninc.IsZero()) + { + ninc.Set(0, 1); + } + chn.increment = ninc; + + if((chn.dwFlags & (CHN_ADLIB | CHN_MUTE | CHN_SYNCMUTE)) == CHN_ADLIB && m_opl) + { + const bool doProcess = m_playBehaviour[kOPLFlexibleNoteOff] || !chn.dwFlags[CHN_NOTEFADE] || GetType() == MOD_TYPE_S3M; + if(doProcess && !(GetType() == MOD_TYPE_S3M && chn.dwFlags[CHN_KEYOFF])) + { + // In ST3, a sample rate of 8363 Hz is mapped to middle-C, which is 261.625 Hz in a tempered scale at A4 = 440. + // Hence, we have to translate our "sample rate" into pitch. + auto milliHertz = Util::muldivr_unsigned(freq, 261625, 8363 << FREQ_FRACBITS); + + const bool keyOff = chn.dwFlags[CHN_KEYOFF] || (chn.dwFlags[CHN_NOTEFADE] && chn.nFadeOutVol == 0); + if(!m_playBehaviour[kOPLNoteStopWith0Hz] || !keyOff) + m_opl->Frequency(nChn, milliHertz, keyOff, m_playBehaviour[kOPLBeatingOscillators]); + } + if(doProcess) + { + // Scale volume to OPL range (0...63). + m_opl->Volume(nChn, static_cast<uint8>(Util::muldivr_unsigned(chn.nCalcVolume * chn.nGlobalVol * chn.nInsVol, 63, 1 << 26)), false); + chn.nRealPan = m_opl->Pan(nChn, chn.nRealPan) * 128 + 128; + } + + // Deallocate OPL channels for notes that are most definitely never going to play again. + if(const auto *ins = chn.pModInstrument; ins != nullptr + && (ins->VolEnv.dwFlags & (ENV_ENABLED | ENV_LOOP | ENV_SUSTAIN)) == ENV_ENABLED + && !ins->VolEnv.empty() + && chn.GetEnvelope(ENV_VOLUME).nEnvPosition >= ins->VolEnv.back().tick + && ins->VolEnv.back().value == 0) + { + m_opl->NoteCut(nChn); + if(!m_playBehaviour[kOPLNoResetAtEnvelopeEnd]) + chn.dwFlags.reset(CHN_ADLIB); + chn.dwFlags.set(CHN_NOTEFADE); + chn.nFadeOutVol = 0; + } else if(m_playBehaviour[kOPLFlexibleNoteOff] && chn.dwFlags[CHN_NOTEFADE] && chn.nFadeOutVol == 0) + { + m_opl->NoteCut(nChn); + chn.dwFlags.reset(CHN_ADLIB); + } + } + } + + // Increment envelope positions + if(pIns != nullptr && !m_playBehaviour[kITEnvelopePositionHandling]) + { + // In IT and FT2 compatible mode, envelope positions are updated above. + // Test cases: s77.it, EnvLoops.xm + IncrementEnvelopePositions(chn); + } + + // Volume ramping + chn.dwFlags.set(CHN_VOLUMERAMP, (chn.nRealVolume | chn.rightVol | chn.leftVol) != 0 && !chn.dwFlags[CHN_ADLIB]); + + constexpr uint8 VUMETER_DECAY = 4; + chn.nLeftVU = (chn.nLeftVU > VUMETER_DECAY) ? (chn.nLeftVU - VUMETER_DECAY) : 0; + chn.nRightVU = (chn.nRightVU > VUMETER_DECAY) ? (chn.nRightVU - VUMETER_DECAY) : 0; + + chn.newLeftVol = chn.newRightVol = 0; + chn.pCurrentSample = (chn.pModSample && chn.pModSample->HasSampleData() && chn.nLength && chn.IsSamplePlaying()) ? chn.pModSample->samplev() : nullptr; + if(chn.pCurrentSample || (chn.HasMIDIOutput() && !chn.dwFlags[CHN_KEYOFF | CHN_NOTEFADE])) + { + // Update VU-Meter (nRealVolume is 14-bit) + uint32 vul = (chn.nRealVolume * (256-chn.nRealPan)) / (1 << 14); + if (vul > 127) vul = 127; + if (chn.nLeftVU > 127) chn.nLeftVU = (uint8)vul; + vul /= 2; + if (chn.nLeftVU < vul) chn.nLeftVU = (uint8)vul; + uint32 vur = (chn.nRealVolume * chn.nRealPan) / (1 << 14); + if (vur > 127) vur = 127; + if (chn.nRightVU > 127) chn.nRightVU = (uint8)vur; + vur /= 2; + if (chn.nRightVU < vur) chn.nRightVU = (uint8)vur; + } else + { + // Note change but no sample + if (chn.nLeftVU > 128) chn.nLeftVU = 0; + if (chn.nRightVU > 128) chn.nRightVU = 0; + } + + if (chn.pCurrentSample) + { +#ifdef MODPLUG_TRACKER + const uint32 kChnMasterVol = chn.dwFlags[CHN_EXTRALOUD] ? (uint32)m_PlayConfig.getNormalSamplePreAmp() : nMasterVol; +#else + const uint32 kChnMasterVol = nMasterVol; +#endif // MODPLUG_TRACKER + + // Adjusting volumes + { + int32 pan = (m_MixerSettings.gnChannels >= 2) ? Clamp(chn.nRealPan, 0, 256) : 128; + + int32 realvol; + if(m_PlayConfig.getUseGlobalPreAmp()) + { + realvol = (chn.nRealVolume * kChnMasterVol) / 128; + } else + { + // Extra attenuation required here if we're bypassing pre-amp. + realvol = (chn.nRealVolume * kChnMasterVol) / 256; + } + + const PanningMode panningMode = m_PlayConfig.getPanningMode(); + if(panningMode == PanningMode::SoftPanning || (panningMode == PanningMode::Undetermined && (m_MixerSettings.MixerFlags & SNDMIX_SOFTPANNING))) + { + if(pan < 128) + { + chn.newLeftVol = (realvol * 128) / 256; + chn.newRightVol = (realvol * pan) / 256; + } else + { + chn.newLeftVol = (realvol * (256 - pan)) / 256; + chn.newRightVol = (realvol * 128) / 256; + } + } else if(panningMode == PanningMode::FT2Panning) + { + // FT2 uses square root panning. There is a 257-entry LUT for this, + // but FT2's internal panning ranges from 0 to 255 only, meaning that + // you can never truly achieve 100% right panning in FT2, only 100% left. + // Test case: FT2PanLaw.xm + LimitMax(pan, 255); + const int panL = pan > 0 ? XMPanningTable[256 - pan] : 65536; + const int panR = XMPanningTable[pan]; + chn.newLeftVol = (realvol * panL) / 65536; + chn.newRightVol = (realvol * panR) / 65536; + } else + { + chn.newLeftVol = (realvol * (256 - pan)) / 256; + chn.newRightVol = (realvol * pan) / 256; + } + } + // Clipping volumes + //if (chn.nNewRightVol > 0xFFFF) chn.nNewRightVol = 0xFFFF; + //if (chn.nNewLeftVol > 0xFFFF) chn.nNewLeftVol = 0xFFFF; + + if(chn.pModInstrument && Resampling::IsKnownMode(chn.pModInstrument->resampling)) + { + // For defined resampling modes, use per-instrument resampling mode if set + chn.resamplingMode = chn.pModInstrument->resampling; + } else if(Resampling::IsKnownMode(m_nResampling)) + { + chn.resamplingMode = m_nResampling; + } else if(m_SongFlags[SONG_ISAMIGA] && m_Resampler.m_Settings.emulateAmiga != Resampling::AmigaFilter::Off) + { + // Enforce Amiga resampler for Amiga modules + chn.resamplingMode = SRCMODE_AMIGA; + } else + { + // Default to global mixer settings + chn.resamplingMode = m_Resampler.m_Settings.SrcMode; + } + + if(chn.increment.IsUnity() && !(chn.dwFlags[CHN_VIBRATO] || chn.nAutoVibDepth || chn.resamplingMode == SRCMODE_AMIGA)) + { + // Exact sample rate match, do not interpolate at all + // - unless vibrato is applied, because in this case the constant enabling and disabling + // of resampling can introduce clicks (this is easily observable with a sine sample + // played at the mix rate). + chn.resamplingMode = SRCMODE_NEAREST; + } + + const int extraAttenuation = m_PlayConfig.getExtraSampleAttenuation(); + chn.newLeftVol /= (1 << extraAttenuation); + chn.newRightVol /= (1 << extraAttenuation); + + // Dolby Pro-Logic Surround + if(chn.dwFlags[CHN_SURROUND] && m_MixerSettings.gnChannels == 2) chn.newRightVol = -chn.newRightVol; + + // Checking Ping-Pong Loops + if(chn.dwFlags[CHN_PINGPONGFLAG]) chn.increment.Negate(); + + // Setting up volume ramp + ProcessRamping(chn); + + // Adding the channel in the channel list + if(!chn.dwFlags[CHN_ADLIB]) + { + m_PlayState.ChnMix[m_nMixChannels++] = nChn; + } + } else + { + chn.rightVol = chn.leftVol = 0; + chn.nLength = 0; + // Put the channel back into the mixer for end-of-sample pop reduction + if(chn.nLOfs || chn.nROfs) + m_PlayState.ChnMix[m_nMixChannels++] = nChn; + } + + chn.dwOldFlags = chn.dwFlags; + } + + // If there are more channels being mixed than allowed, order them by volume and discard the most quiet ones + if(m_nMixChannels >= m_MixerSettings.m_nMaxMixChannels) + { + std::partial_sort(std::begin(m_PlayState.ChnMix), std::begin(m_PlayState.ChnMix) + m_MixerSettings.m_nMaxMixChannels, std::begin(m_PlayState.ChnMix) + m_nMixChannels, + [this](CHANNELINDEX i, CHANNELINDEX j) { return (m_PlayState.Chn[i].nRealVolume > m_PlayState.Chn[j].nRealVolume); }); + } + return true; +} + + +void CSoundFile::ProcessMacroOnChannel(CHANNELINDEX nChn) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + if(nChn < GetNumChannels()) + { + // TODO evaluate per-plugin macros here + //ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.Global[MIDIOUT_PAN]); + //ProcessMIDIMacro(m_PlayState, nChn, false, m_MidiCfg.Global[MIDIOUT_VOLUME]); + + if((chn.rowCommand.command == CMD_MIDI && m_SongFlags[SONG_FIRSTTICK]) || chn.rowCommand.command == CMD_SMOOTHMIDI) + { + if(chn.rowCommand.param < 0x80) + ProcessMIDIMacro(m_PlayState, nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.SFx[chn.nActiveMacro], chn.rowCommand.param); + else + ProcessMIDIMacro(m_PlayState, nChn, (chn.rowCommand.command == CMD_SMOOTHMIDI), m_MidiCfg.Zxx[chn.rowCommand.param & 0x7F], chn.rowCommand.param); + } + } +} + + +#ifndef NO_PLUGINS + +void CSoundFile::ProcessMidiOut(CHANNELINDEX nChn) +{ + ModChannel &chn = m_PlayState.Chn[nChn]; + + // Do we need to process MIDI? + // For now there is no difference between mute and sync mute with VSTis. + if(chn.dwFlags[CHN_MUTE | CHN_SYNCMUTE] || !chn.HasMIDIOutput()) return; + + // Get instrument info and plugin reference + const ModInstrument *pIns = chn.pModInstrument; // Can't be nullptr at this point, as we have valid MIDI output. + + // No instrument or muted instrument? + if(pIns->dwFlags[INS_MUTE]) + { + return; + } + + // Check instrument plugins + const PLUGINDEX nPlugin = GetBestPlugin(m_PlayState, nChn, PrioritiseInstrument, RespectMutes); + IMixPlugin *pPlugin = nullptr; + if(nPlugin > 0 && nPlugin <= MAX_MIXPLUGINS) + { + pPlugin = m_MixPlugins[nPlugin - 1].pMixPlugin; + } + + // Couldn't find a valid plugin + if(pPlugin == nullptr) return; + + const ModCommand::NOTE note = chn.rowCommand.note; + // Check for volume commands + uint8 vol = 0xFF; + if(chn.rowCommand.volcmd == VOLCMD_VOLUME) + { + vol = std::min(chn.rowCommand.vol, uint8(64)); + } else if(chn.rowCommand.command == CMD_VOLUME) + { + vol = std::min(chn.rowCommand.param, uint8(64)); + } + const bool hasVolCommand = (vol != 0xFF); + + if(m_playBehaviour[kMIDICCBugEmulation]) + { + if(note != NOTE_NONE) + { + ModCommand::NOTE realNote = note; + if(ModCommand::IsNote(note)) + realNote = pIns->NoteMap[note - NOTE_MIN]; + SendMIDINote(nChn, realNote, static_cast<uint16>(chn.nVolume)); + } else if(hasVolCommand) + { + pPlugin->MidiCC(MIDIEvents::MIDICC_Volume_Fine, vol, nChn); + } + return; + } + + const uint32 defaultVolume = pIns->nGlobalVol; + + //If new note, determine notevelocity to use. + if(note != NOTE_NONE) + { + int32 velocity = static_cast<int32>(4 * defaultVolume); + switch(pIns->pluginVelocityHandling) + { + case PLUGIN_VELOCITYHANDLING_CHANNEL: + velocity = chn.nVolume; + break; + default: + break; + } + + int32 swing = chn.nVolSwing; + if(m_playBehaviour[kITSwingBehaviour]) swing *= 4; + velocity += swing; + Limit(velocity, 0, 256); + + ModCommand::NOTE realNote = note; + if(ModCommand::IsNote(note)) + realNote = pIns->NoteMap[note - NOTE_MIN]; + // Experimental VST panning + //ProcessMIDIMacro(nChn, false, m_MidiCfg.Global[MIDIOUT_PAN], 0, nPlugin); + SendMIDINote(nChn, realNote, static_cast<uint16>(velocity)); + } + + const bool processVolumeAlsoOnNote = (pIns->pluginVelocityHandling == PLUGIN_VELOCITYHANDLING_VOLUME); + const bool hasNote = m_playBehaviour[kMIDIVolumeOnNoteOffBug] ? (note != NOTE_NONE) : ModCommand::IsNote(note); + + if((hasVolCommand && !hasNote) || (hasNote && processVolumeAlsoOnNote)) + { + switch(pIns->pluginVolumeHandling) + { + case PLUGIN_VOLUMEHANDLING_DRYWET: + if(hasVolCommand) pPlugin->SetDryRatio(1.0f - (2 * vol) / 127.0f); + else pPlugin->SetDryRatio(1.0f - (2 * defaultVolume) / 127.0f); + break; + case PLUGIN_VOLUMEHANDLING_MIDI: + if(hasVolCommand) pPlugin->MidiCC(MIDIEvents::MIDICC_Volume_Coarse, std::min(uint8(127), static_cast<uint8>(2 * vol)), nChn); + else pPlugin->MidiCC(MIDIEvents::MIDICC_Volume_Coarse, static_cast<uint8>(std::min(uint32(127), static_cast<uint32>(2 * defaultVolume))), nChn); + break; + default: + break; + } + } +} + +#endif // NO_PLUGINS + + +template<int channels> +MPT_FORCEINLINE void ApplyGlobalVolumeWithRamping(int32 *SoundBuffer, int32 *RearBuffer, int32 lCount, int32 m_nGlobalVolume, int32 step, int32 &m_nSamplesToGlobalVolRampDest, int32 &m_lHighResRampingGlobalVolume) +{ + const bool isStereo = (channels >= 2); + const bool hasRear = (channels >= 4); + for(int pos = 0; pos < lCount; ++pos) + { + if(m_nSamplesToGlobalVolRampDest > 0) + { + // Ramping required + m_lHighResRampingGlobalVolume += step; + SoundBuffer[0] = Util::muldiv(SoundBuffer[0], m_lHighResRampingGlobalVolume, MAX_GLOBAL_VOLUME << VOLUMERAMPPRECISION); + if constexpr(isStereo) SoundBuffer[1] = Util::muldiv(SoundBuffer[1], m_lHighResRampingGlobalVolume, MAX_GLOBAL_VOLUME << VOLUMERAMPPRECISION); + if constexpr(hasRear) RearBuffer[0] = Util::muldiv(RearBuffer[0] , m_lHighResRampingGlobalVolume, MAX_GLOBAL_VOLUME << VOLUMERAMPPRECISION); else MPT_UNUSED_VARIABLE(RearBuffer); + if constexpr(hasRear) RearBuffer[1] = Util::muldiv(RearBuffer[1] , m_lHighResRampingGlobalVolume, MAX_GLOBAL_VOLUME << VOLUMERAMPPRECISION); else MPT_UNUSED_VARIABLE(RearBuffer); + m_nSamplesToGlobalVolRampDest--; + } else + { + SoundBuffer[0] = Util::muldiv(SoundBuffer[0], m_nGlobalVolume, MAX_GLOBAL_VOLUME); + if constexpr(isStereo) SoundBuffer[1] = Util::muldiv(SoundBuffer[1], m_nGlobalVolume, MAX_GLOBAL_VOLUME); + if constexpr(hasRear) RearBuffer[0] = Util::muldiv(RearBuffer[0] , m_nGlobalVolume, MAX_GLOBAL_VOLUME); else MPT_UNUSED_VARIABLE(RearBuffer); + if constexpr(hasRear) RearBuffer[1] = Util::muldiv(RearBuffer[1] , m_nGlobalVolume, MAX_GLOBAL_VOLUME); else MPT_UNUSED_VARIABLE(RearBuffer); + m_lHighResRampingGlobalVolume = m_nGlobalVolume << VOLUMERAMPPRECISION; + } + SoundBuffer += isStereo ? 2 : 1; + if constexpr(hasRear) RearBuffer += 2; + } +} + + +void CSoundFile::ProcessGlobalVolume(long lCount) +{ + + // should we ramp? + if(IsGlobalVolumeUnset()) + { + // do not ramp if no global volume was set before (which is the case at song start), to prevent audible glitches when default volume is > 0 and it is set to 0 in the first row + m_PlayState.m_nGlobalVolumeDestination = m_PlayState.m_nGlobalVolume; + m_PlayState.m_nSamplesToGlobalVolRampDest = 0; + m_PlayState.m_nGlobalVolumeRampAmount = 0; + } else if(m_PlayState.m_nGlobalVolumeDestination != m_PlayState.m_nGlobalVolume) + { + // User has provided new global volume + + // m_nGlobalVolume: the last global volume which got set e.g. by a pattern command + // m_nGlobalVolumeDestination: the current target of the ramping algorithm + const bool rampUp = m_PlayState.m_nGlobalVolume > m_PlayState.m_nGlobalVolumeDestination; + + m_PlayState.m_nGlobalVolumeDestination = m_PlayState.m_nGlobalVolume; + m_PlayState.m_nSamplesToGlobalVolRampDest = m_PlayState.m_nGlobalVolumeRampAmount = rampUp ? m_MixerSettings.GetVolumeRampUpSamples() : m_MixerSettings.GetVolumeRampDownSamples(); + } + + // calculate ramping step + int32 step = 0; + if (m_PlayState.m_nSamplesToGlobalVolRampDest > 0) + { + + // Still some ramping left to do. + int32 highResGlobalVolumeDestination = static_cast<int32>(m_PlayState.m_nGlobalVolumeDestination) << VOLUMERAMPPRECISION; + + const long delta = highResGlobalVolumeDestination - m_PlayState.m_lHighResRampingGlobalVolume; + step = delta / static_cast<long>(m_PlayState.m_nSamplesToGlobalVolRampDest); + + if(m_nMixLevels == MixLevels::v1_17RC2) + { + // Define max step size as some factor of user defined ramping value: the lower the value, the more likely the click. + // If step is too big (might cause click), extend ramp length. + // Warning: This increases the volume ramp length by EXTREME amounts (factors of 100 are easily reachable) + // compared to the user-defined setting, so this really should not be used! + int32 maxStep = std::max(int32(50), static_cast<int32>((10000 / (m_PlayState.m_nGlobalVolumeRampAmount + 1)))); + while(std::abs(step) > maxStep) + { + m_PlayState.m_nSamplesToGlobalVolRampDest += m_PlayState.m_nGlobalVolumeRampAmount; + step = delta / static_cast<int32>(m_PlayState.m_nSamplesToGlobalVolRampDest); + } + } + } + + // apply volume and ramping + if(m_MixerSettings.gnChannels == 1) + { + ApplyGlobalVolumeWithRamping<1>(MixSoundBuffer, MixRearBuffer, lCount, m_PlayState.m_nGlobalVolume, step, m_PlayState.m_nSamplesToGlobalVolRampDest, m_PlayState.m_lHighResRampingGlobalVolume); + } else if(m_MixerSettings.gnChannels == 2) + { + ApplyGlobalVolumeWithRamping<2>(MixSoundBuffer, MixRearBuffer, lCount, m_PlayState.m_nGlobalVolume, step, m_PlayState.m_nSamplesToGlobalVolRampDest, m_PlayState.m_lHighResRampingGlobalVolume); + } else if(m_MixerSettings.gnChannels == 4) + { + ApplyGlobalVolumeWithRamping<4>(MixSoundBuffer, MixRearBuffer, lCount, m_PlayState.m_nGlobalVolume, step, m_PlayState.m_nSamplesToGlobalVolRampDest, m_PlayState.m_lHighResRampingGlobalVolume); + } + +} + + +void CSoundFile::ProcessStereoSeparation(long countChunk) +{ + ApplyStereoSeparation(MixSoundBuffer, MixRearBuffer, m_MixerSettings.gnChannels, countChunk, m_MixerSettings.m_nStereoSeparation); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.cpp new file mode 100644 index 00000000..76317542 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.cpp @@ -0,0 +1,116 @@ +/* + * SoundFilePlayConfig.cpp + * ----------------------- + * Purpose: Configuration of sound levels, pan laws, etc... for various mix configurations. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Mixer.h" +#include "SoundFilePlayConfig.h" + +OPENMPT_NAMESPACE_BEGIN + +CSoundFilePlayConfig::CSoundFilePlayConfig() +{ + setVSTiVolume(1.0f); + SetMixLevels(MixLevels::Compatible); +} + +void CSoundFilePlayConfig::SetMixLevels(MixLevels mixLevelType) +{ + switch (mixLevelType) + { + // Olivier's version gives us floats in [-0.5; 0.5] and slightly saturates VSTis. + case MixLevels::Original: + setVSTiAttenuation(1.0f); // no attenuation + setIntToFloat(1.0f/static_cast<float>(1<<28)); + setFloatToInt(static_cast<float>(1<<28)); + setGlobalVolumeAppliesToMaster(false); + setUseGlobalPreAmp(true); + setPanningMode(PanningMode::Undetermined); + setDisplayDBValues(false); + setNormalSamplePreAmp(256.0f); + setNormalVSTiVol(100.0f); + setNormalGlobalVol(128.0f); + setExtraSampleAttenuation(MIXING_ATTENUATION); + break; + + // Ericus' version gives us floats in [-0.06;0.06] and requires attenuation to + // avoid massive VSTi saturation. + case MixLevels::v1_17RC1: + setVSTiAttenuation(32.0f); + setIntToFloat(1.0f/static_cast<float>(0x07FFFFFFF)); + setFloatToInt(static_cast<float>(0x07FFFFFFF)); + setGlobalVolumeAppliesToMaster(false); + setUseGlobalPreAmp(true); + setPanningMode(PanningMode::Undetermined); + setDisplayDBValues(false); + setNormalSamplePreAmp(256.0f); + setNormalVSTiVol(100.0f); + setNormalGlobalVol(128.0f); + setExtraSampleAttenuation(MIXING_ATTENUATION); + break; + + // 1.17RC2 gives us floats in [-1.0; 1.0] and hopefully plays VSTis at + // the right volume... but we attenuate by 2x to approx. match sample volume. + + case MixLevels::v1_17RC2: + setVSTiAttenuation(2.0f); + setIntToFloat(1.0f/MIXING_SCALEF); + setFloatToInt(MIXING_SCALEF); + setGlobalVolumeAppliesToMaster(true); + setUseGlobalPreAmp(true); + setPanningMode(PanningMode::Undetermined); + setDisplayDBValues(false); + setNormalSamplePreAmp(256.0f); + setNormalVSTiVol(100.0f); + setNormalGlobalVol(128.0f); + setExtraSampleAttenuation(MIXING_ATTENUATION); + break; + + // 1.17RC3 ignores the horrible global, system-specific pre-amp, + // treats panning as balance to avoid saturation on loud sample (and because I think it's better :), + // and allows display of attenuation in decibels. + default: + case MixLevels::v1_17RC3: + setVSTiAttenuation(1.0f); + setIntToFloat(1.0f/MIXING_SCALEF); + setFloatToInt(MIXING_SCALEF); + setGlobalVolumeAppliesToMaster(true); + setUseGlobalPreAmp(false); + setPanningMode(PanningMode::SoftPanning); + setDisplayDBValues(true); + setNormalSamplePreAmp(128.0f); + setNormalVSTiVol(128.0f); + setNormalGlobalVol(256.0f); + setExtraSampleAttenuation(0); + break; + + // A mixmode that is intended to be compatible to legacy trackers (IT/FT2/etc). + // This is basically derived from mixmode 1.17 RC3, with panning mode and volume levels changed. + // Sample attenuation is the same as in Schism Tracker (more attenuation than with RC3, thus VSTi attenuation is also higher). + case MixLevels::Compatible: + case MixLevels::CompatibleFT2: + setVSTiAttenuation(0.75f); + setIntToFloat(1.0f/MIXING_SCALEF); + setFloatToInt(MIXING_SCALEF); + setGlobalVolumeAppliesToMaster(true); + setUseGlobalPreAmp(false); + setPanningMode(mixLevelType == MixLevels::Compatible ? PanningMode::NoSoftPanning : PanningMode::FT2Panning); + setDisplayDBValues(true); + setNormalSamplePreAmp(mixLevelType == MixLevels::Compatible ? 256.0f : 192.0f); + setNormalVSTiVol(mixLevelType == MixLevels::Compatible ? 256.0f : 192.0f); + setNormalGlobalVol(256.0f); + setExtraSampleAttenuation(1); + break; + + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.h b/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.h new file mode 100644 index 00000000..ee2c7486 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/SoundFilePlayConfig.h @@ -0,0 +1,109 @@ +/* + * SoundFilePlayConfig.h + * --------------------- + * Purpose: Configuration of sound levels, pan laws, etc... for various mix configurations. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +enum class TempoMode : uint8 +{ + Classic = 0, + Alternative = 1, + Modern = 2, + NumModes +}; + +enum class MixLevels : uint8 +{ + Original = 0, + v1_17RC1 = 1, + v1_17RC2 = 2, + v1_17RC3 = 3, + Compatible = 4, + CompatibleFT2 = 5, + NumMixLevels +}; + +enum class PanningMode : uint8 +{ + Undetermined, + SoftPanning, + NoSoftPanning, + FT2Panning, +}; + +// Class used to store settings for a song file. +class CSoundFilePlayConfig +{ +public: + CSoundFilePlayConfig(); + + void SetMixLevels(MixLevels mixLevelType); + +//getters/setters + bool getGlobalVolumeAppliesToMaster() const { return m_globalVolumeAppliesToMaster; } + void setGlobalVolumeAppliesToMaster(bool inGlobalVolumeAppliesToMaster) { m_globalVolumeAppliesToMaster=inGlobalVolumeAppliesToMaster; } + + // user-controllable VSTi gain factor. + float getVSTiVolume() const { return m_VSTiVolume; } + void setVSTiVolume(float inVSTiVolume) { m_VSTiVolume = inVSTiVolume; } + + // default VSTi gain factor, different depending on the MPT version we're "emulating" + float getVSTiAttenuation() const { return m_VSTiAttenuation; } + void setVSTiAttenuation(float inVSTiAttenuation) { m_VSTiAttenuation = inVSTiAttenuation; } + + float getIntToFloat() const { return m_IntToFloat; } + void setIntToFloat(float inIntToFloat) { m_IntToFloat = inIntToFloat; } + + float getFloatToInt() const { return m_FloatToInt; } + void setFloatToInt(float inFloatToInt) { m_FloatToInt = inFloatToInt; } + + bool getUseGlobalPreAmp() const { return m_ignorePreAmp; } + void setUseGlobalPreAmp(bool inUseGlobalPreAmp) { m_ignorePreAmp = inUseGlobalPreAmp; } + + PanningMode getPanningMode() const { return m_forceSoftPanning; } + void setPanningMode(PanningMode inForceSoftPanning) { m_forceSoftPanning = inForceSoftPanning; } + + bool getDisplayDBValues() const { return m_displayDBValues; } + void setDisplayDBValues(bool in) { m_displayDBValues = in; } + + // Values at which volumes are unchanged + float getNormalSamplePreAmp() const { return m_normalSamplePreAmp; } + void setNormalSamplePreAmp(float in) { m_normalSamplePreAmp = in; } + float getNormalVSTiVol() const { return m_normalVSTiVol; } + void setNormalVSTiVol(float in) { m_normalVSTiVol = in; } + float getNormalGlobalVol() const { return m_normalGlobalVol; } + void setNormalGlobalVol(float in) { m_normalGlobalVol = in; } + + // Extra sample attenuation in bits + int getExtraSampleAttenuation() const { return m_extraAttenuation; } + void setExtraSampleAttenuation(int attn) { m_extraAttenuation = attn; } + +protected: + + float m_IntToFloat; + float m_FloatToInt; + float m_VSTiAttenuation; + float m_VSTiVolume; + + float m_normalSamplePreAmp; + float m_normalVSTiVol; + float m_normalGlobalVol; + + int m_extraAttenuation; + PanningMode m_forceSoftPanning; + bool m_globalVolumeAppliesToMaster; + bool m_ignorePreAmp; + bool m_displayDBValues; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Tables.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Tables.cpp new file mode 100644 index 00000000..000b12b2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Tables.cpp @@ -0,0 +1,885 @@ +/* + * Tables.cpp + * ---------- + * Purpose: Effect, interpolation, data and other pre-calculated tables. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Tables.h" +#include "Sndfile.h" + +#include "Resampler.h" +#include "WindowedFIR.h" +#include <cmath> + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////////// +// Note Name Tables + +const mpt::uchar NoteNamesSharp[12][4] = +{ + UL_("C-"), UL_("C#"), UL_("D-"), UL_("D#"), UL_("E-"), UL_("F-"), + UL_("F#"), UL_("G-"), UL_("G#"), UL_("A-"), UL_("A#"), UL_("B-") +}; + +const mpt::uchar NoteNamesFlat[12][4] = +{ + UL_("C-"), UL_("Db"), UL_("D-"), UL_("Eb"), UL_("E-"), UL_("F-"), + UL_("Gb"), UL_("G-"), UL_("Ab"), UL_("A-"), UL_("Bb"), UL_("B-") +}; + + +/////////////////////////////////////////////////////////// +// File Formats Information (name, extension, etc) + +struct ModFormatInfo +{ + MODTYPE format; // MOD_TYPE_XXXX + const mpt::uchar *name; // "ProTracker" + const char *extension; // "mod" +}; + +// Note: Formats with identical extensions must be grouped together. +static constexpr ModFormatInfo modFormatInfo[] = +{ + { MOD_TYPE_MPT, UL_("OpenMPT"), "mptm" }, + { MOD_TYPE_MOD, UL_("ProTracker"), "mod" }, + { MOD_TYPE_S3M, UL_("Scream Tracker 3"), "s3m" }, + { MOD_TYPE_XM, UL_("FastTracker 2"), "xm" }, + { MOD_TYPE_IT, UL_("Impulse Tracker"), "it" }, + + { MOD_TYPE_669, UL_("Composer 669 / UNIS 669"), "669" }, + { MOD_TYPE_AMF0, UL_("ASYLUM Music Format"), "amf" }, + { MOD_TYPE_AMF, UL_("DSMI Advanced Music Format"), "amf" }, + { MOD_TYPE_AMS, UL_("Extreme's Tracker"), "ams" }, + { MOD_TYPE_AMS, UL_("Velvet Studio"), "ams" }, + { MOD_TYPE_S3M, UL_("CDFM / Composer 670"), "c67" }, + { MOD_TYPE_DBM, UL_("DigiBooster Pro"), "dbm" }, + { MOD_TYPE_DIGI, UL_("DigiBooster"), "digi" }, + { MOD_TYPE_DMF, UL_("X-Tracker"), "dmf" }, + { MOD_TYPE_DSM, UL_("DSIK Format"), "dsm" }, + { MOD_TYPE_MOD, UL_("Digital Symphony"), "dsym" }, + { MOD_TYPE_DTM, UL_("Digital Tracker"), "dtm" }, + { MOD_TYPE_FAR, UL_("Farandole Composer"), "far" }, + { MOD_TYPE_S3M, UL_("FM Tracker"), "fmt" }, + { MOD_TYPE_IMF, UL_("Imago Orpheus"), "imf" }, + { MOD_TYPE_MOD, UL_("Ice Tracker"), "ice" }, +#ifdef MPT_EXTERNAL_SAMPLES + { MOD_TYPE_IT, UL_("Impulse Tracker Project"), "itp" }, +#endif + { MOD_TYPE_J2B, UL_("Galaxy Sound System"), "j2b" }, + { MOD_TYPE_MOD, UL_("Soundtracker"), "m15" }, + { MOD_TYPE_MDL, UL_("Digitrakker"), "mdl" }, + { MOD_TYPE_MED, UL_("OctaMED"), "med" }, + { MOD_TYPE_SFX, UL_("MultiMedia Sound"), "mms" }, + { MOD_TYPE_MT2, UL_("MadTracker 2"), "mt2" }, + { MOD_TYPE_MTM, UL_("MultiTracker"), "mtm" }, + { MOD_TYPE_MOD, UL_("Karl Morton Music Format"), "mus" }, + { MOD_TYPE_MOD, UL_("NoiseTracker"), "nst" }, + { MOD_TYPE_OKT, UL_("Oktalyzer"), "okt" }, + { MOD_TYPE_PLM, UL_("Disorder Tracker 2"), "plm" }, + { MOD_TYPE_PSM, UL_("Epic Megagames MASI"), "psm" }, + { MOD_TYPE_MOD, UL_("ProTracker"), "pt36" }, + { MOD_TYPE_PTM, UL_("PolyTracker"), "ptm" }, + { MOD_TYPE_SFX, UL_("SoundFX"), "sfx" }, + { MOD_TYPE_SFX, UL_("SoundFX"), "sfx2" }, + { MOD_TYPE_MOD, UL_("SoundTracker 2.6"), "st26" }, + { MOD_TYPE_MOD, UL_("Soundtracker"), "stk" }, + { MOD_TYPE_STM, UL_("Scream Tracker 2"), "stm" }, + { MOD_TYPE_STM, UL_("Scream Tracker Music Interface Kit"), "stx" }, + { MOD_TYPE_STP, UL_("Soundtracker Pro II"), "stp" }, + { MOD_TYPE_MPT, UL_("Symphonie"), "symmod"}, + { MOD_TYPE_ULT, UL_("UltraTracker"), "ult" }, + { MOD_TYPE_MOD, UL_("Mod's Grave"), "wow" }, + // converted formats (no MODTYPE) + { MOD_TYPE_NONE, UL_("General Digital Music"), "gdm" }, + { MOD_TYPE_NONE, UL_("Un4seen MO3"), "mo3" }, + { MOD_TYPE_NONE, UL_("OggMod FastTracker 2"), "oxm" }, +//#ifndef NO_ARCHIVE_SUPPORT + // Compressed modules + { MOD_TYPE_MOD, UL_("Compressed ProTracker"), "mdz" }, + { MOD_TYPE_MOD, UL_("Compressed Module"), "mdr" }, + { MOD_TYPE_S3M, UL_("Compressed Scream Tracker 3"), "s3z" }, + { MOD_TYPE_XM, UL_("Compressed FastTracker 2"), "xmz" }, + { MOD_TYPE_IT, UL_("Compressed Impulse Tracker"), "itz" }, + { MOD_TYPE_MPT, UL_("Compressed OpenMPT"), "mptmz" }, +//#endif +}; + + +struct ModContainerInfo +{ + MODCONTAINERTYPE format; // MOD_CONTAINERTYPE_XXXX + const mpt::uchar *name; // "Unreal Music" + const char *extension; // "umx" +}; + +static constexpr ModContainerInfo modContainerInfo[] = +{ + // Container formats + { MOD_CONTAINERTYPE_UMX, UL_("Unreal Music"), "umx" }, + { MOD_CONTAINERTYPE_XPK, UL_("XPK packed"), "xpk" }, + { MOD_CONTAINERTYPE_PP20, UL_("PowerPack PP20"), "ppm" }, + { MOD_CONTAINERTYPE_MMCMP, UL_("Music Module Compressor"), "mmcmp" }, +#ifdef MODPLUG_TRACKER + { MOD_CONTAINERTYPE_WAV, UL_("Wave"), "wav" }, + { MOD_CONTAINERTYPE_UAX, UL_("Unreal Sounds"), "uax" }, +#endif +}; + + +#ifdef MODPLUG_TRACKER +static constexpr ModFormatInfo otherFormatInfo[] = +{ + { MOD_TYPE_MID, UL_("MIDI"), "mid" }, + { MOD_TYPE_MID, UL_("MIDI"), "rmi" }, + { MOD_TYPE_MID, UL_("MIDI"), "smf" } +}; +#endif + + +std::vector<const char *> CSoundFile::GetSupportedExtensions(bool otherFormats) +{ + std::vector<const char *> exts; + for(const auto &formatInfo : modFormatInfo) + { + // Avoid dupes in list + if(exts.empty() || strcmp(formatInfo.extension, exts.back())) + { + exts.push_back(formatInfo.extension); + } + } + for(const auto &containerInfo : modContainerInfo) + { + // Avoid dupes in list + if(exts.empty() || strcmp(containerInfo.extension, exts.back())) + { + exts.push_back(containerInfo.extension); + } + } +#ifdef MODPLUG_TRACKER + if(otherFormats) + { + for(const auto &formatInfo : otherFormatInfo) + { + exts.push_back(formatInfo.extension); + } + } +#else + MPT_UNREFERENCED_PARAMETER(otherFormats); +#endif + return exts; +} +/// <summary> +/// From version: 0.7.0 +/// Hakan DANISIK +/// </summary> +/// <param name="var"></param> +/// <returns></returns> +std::string w2s(const std::wstring &var) +{ + static std::locale loc(""); + auto &facet = std::use_facet<std::codecvt<wchar_t, char, std::mbstate_t>>(loc); + return std::wstring_convert<std::remove_reference<decltype(facet)>::type, wchar_t>(&facet).to_bytes(var); +} + +/// <summary> +/// From version: 0.7.0 +/// Hakan DANISIK +/// </summary> +/// <param name="extension"></param> +/// <returns></returns> +std::string CSoundFile::ExtensionToTracker(std::string extension) +//------------------------------------------------------- +{ + for(size_t i = 0; i < CountOf(modFormatInfo); i++) + { + if(extension == modFormatInfo[i].extension) + { + std::wstring name(modFormatInfo[i].name); + return w2s(name); + } + } + for(size_t i = 0; i < CountOf(modContainerInfo); i++) + { + if(extension == modContainerInfo[i].extension) + { + std::wstring name(modContainerInfo[i].name); + return w2s(name); + } + } + return std::string(); +} + +static bool IsEqualExtension(std::string_view a, std::string_view b) +{ + if(a.length() != b.length()) + { + return false; + } + return mpt::CompareNoCaseAscii(a, b) == 0; +} + + +bool CSoundFile::IsExtensionSupported(std::string_view ext) +{ + if(ext.length() == 0) + { + return false; + } + for(const auto &formatInfo : modFormatInfo) + { + if(IsEqualExtension(ext, formatInfo.extension)) + { + return true; + } + } + for(const auto &containerInfo : modContainerInfo) + { + if(IsEqualExtension(ext, containerInfo.extension)) + { + return true; + } + } + return false; +} + + +mpt::ustring CSoundFile::ModContainerTypeToString(MODCONTAINERTYPE containertype) +{ + for(const auto &containerInfo : modContainerInfo) + { + if(containerInfo.format == containertype) + { + return mpt::ToUnicode(mpt::Charset::UTF8, containerInfo.extension); + } + } + return mpt::ustring(); +} + + +mpt::ustring CSoundFile::ModContainerTypeToTracker(MODCONTAINERTYPE containertype) +{ + std::set<mpt::ustring> retvals; + mpt::ustring retval; + for(const auto &containerInfo : modContainerInfo) + { + if(containerInfo.format == containertype) + { + mpt::ustring name = containerInfo.name; + if(retvals.insert(name).second) + { + if(!retval.empty()) + { + retval += U_(" / "); + } + retval += name; + } + } + } + return retval; +} + + + +/////////////////////////////////////////////////////////////////////// + +const uint8 ImpulseTrackerPortaVolCmd[16] = +{ + 0x00, 0x01, 0x04, 0x08, 0x10, 0x20, 0x40, 0x60, + 0x80, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +}; + +// Period table for ProTracker octaves (1-7 in FastTracker 2, also used for file I/O): +const uint16 ProTrackerPeriodTable[7*12] = +{ + 2*1712,2*1616,2*1524,2*1440,2*1356,2*1280,2*1208,2*1140,2*1076,2*1016,2*960,2*906, + 1712,1616,1524,1440,1356,1280,1208,1140,1076,1016,960,907, + 856,808,762,720,678,640,604,570,538,508,480,453, + 428,404,381,360,339,320,302,285,269,254,240,226, + 214,202,190,180,170,160,151,143,135,127,120,113, + 107,101,95,90,85,80,75,71,67,63,60,56, + 53,50,47,45,42,40,37,35,33,31,30,28 +}; + + +const uint16 ProTrackerTunedPeriods[16*12] = +{ + 1712,1616,1524,1440,1356,1280,1208,1140,1076,1016,960,907, + 1700,1604,1514,1430,1348,1274,1202,1134,1070,1010,954,900, + 1688,1592,1504,1418,1340,1264,1194,1126,1064,1004,948,894, + 1676,1582,1492,1408,1330,1256,1184,1118,1056,996,940,888, + 1664,1570,1482,1398,1320,1246,1176,1110,1048,990,934,882, + 1652,1558,1472,1388,1310,1238,1168,1102,1040,982,926,874, + 1640,1548,1460,1378,1302,1228,1160,1094,1032,974,920,868, + 1628,1536,1450,1368,1292,1220,1150,1086,1026,968,914,862, + 1814,1712,1616,1524,1440,1356,1280,1208,1140,1076,1016,960, + 1800,1700,1604,1514,1430,1350,1272,1202,1134,1070,1010,954, + 1788,1688,1592,1504,1418,1340,1264,1194,1126,1064,1004,948, + 1774,1676,1582,1492,1408,1330,1256,1184,1118,1056,996,940, + 1762,1664,1570,1482,1398,1320,1246,1176,1110,1048,988,934, + 1750,1652,1558,1472,1388,1310,1238,1168,1102,1040,982,926, + 1736,1640,1548,1460,1378,1302,1228,1160,1094,1032,974,920, + 1724,1628,1536,1450,1368,1292,1220,1150,1086,1026,968,914 +}; + +// Table for Invert Loop and Funk Repeat effects (EFx, .MOD only) +const uint8 ModEFxTable[16] = +{ + 0, 5, 6, 7, 8, 10, 11, 13, + 16, 19, 22, 26, 32, 43, 64, 128 +}; + +// S3M C-4 periods +const uint16 FreqS3MTable[12] = +{ + 1712,1616,1524,1440,1356,1280,1208,1140,1076,1016,960,907 +}; + +// S3M FineTune frequencies +const uint16 S3MFineTuneTable[16] = +{ + 7895,7941,7985,8046,8107,8169,8232,8280, + 8363,8413,8463,8529,8581,8651,8723,8757, // 8363*2^((i-8)/(12*8)) +}; + + +// Sinus table +const int8 ModSinusTable[64] = +{ + 0,12,25,37,49,60,71,81,90,98,106,112,117,122,125,126, + 127,126,125,122,117,112,106,98,90,81,71,60,49,37,25,12, + 0,-12,-25,-37,-49,-60,-71,-81,-90,-98,-106,-112,-117,-122,-125,-126, + -127,-126,-125,-122,-117,-112,-106,-98,-90,-81,-71,-60,-49,-37,-25,-12 +}; + +// Random wave table +const int8 ModRandomTable[64] = +{ + 98,-127,-43,88,102,41,-65,-94,125,20,-71,-86,-70,-32,-16,-96, + 17,72,107,-5,116,-69,-62,-40,10,-61,65,109,-18,-38,-13,-76, + -23,88,21,-94,8,106,21,-112,6,109,20,-88,-30,9,-127,118, + 42,-34,89,-4,-51,-72,21,-29,112,123,84,-101,-92,98,-54,-95 +}; + +// Impulse Tracker sinus table (ITTECH.TXT) +const int8 ITSinusTable[256] = +{ + 0, 2, 3, 5, 6, 8, 9, 11, 12, 14, 16, 17, 19, 20, 22, 23, + 24, 26, 27, 29, 30, 32, 33, 34, 36, 37, 38, 39, 41, 42, 43, 44, + 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59, + 59, 60, 60, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 63, 63, 63, 62, 62, 62, 61, 61, 60, 60, + 59, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, + 45, 44, 43, 42, 41, 39, 38, 37, 36, 34, 33, 32, 30, 29, 27, 26, + 24, 23, 22, 20, 19, 17, 16, 14, 12, 11, 9, 8, 6, 5, 3, 2, + 0, -2, -3, -5, -6, -8, -9,-11,-12,-14,-16,-17,-19,-20,-22,-23, + -24,-26,-27,-29,-30,-32,-33,-34,-36,-37,-38,-39,-41,-42,-43,-44, + -45,-46,-47,-48,-49,-50,-51,-52,-53,-54,-55,-56,-56,-57,-58,-59, + -59,-60,-60,-61,-61,-62,-62,-62,-63,-63,-63,-64,-64,-64,-64,-64, + -64,-64,-64,-64,-64,-64,-63,-63,-63,-62,-62,-62,-61,-61,-60,-60, + -59,-59,-58,-57,-56,-56,-55,-54,-53,-52,-51,-50,-49,-48,-47,-46, + -45,-44,-43,-42,-41,-39,-38,-37,-36,-34,-33,-32,-30,-29,-27,-26, + -24,-23,-22,-20,-19,-17,-16,-14,-12,-11, -9, -8, -6, -5, -3, -2, +}; + + +// volume fade tables for Retrig Note: +const int8 retrigTable1[16] = +{ 0, 0, 0, 0, 0, 0, 10, 8, 0, 0, 0, 0, 0, 0, 24, 32 }; + +const int8 retrigTable2[16] = +{ 0, -1, -2, -4, -8, -16, 0, 0, 0, 1, 2, 4, 8, 16, 0, 0 }; + + + + +const uint16 XMPeriodTable[104] = +{ + 907,900,894,887,881,875,868,862,856,850,844,838,832,826,820,814, + 808,802,796,791,785,779,774,768,762,757,752,746,741,736,730,725, + 720,715,709,704,699,694,689,684,678,675,670,665,660,655,651,646, + 640,636,632,628,623,619,614,610,604,601,597,592,588,584,580,575, + 570,567,563,559,555,551,547,543,538,535,532,528,524,520,516,513, + 508,505,502,498,494,491,487,484,480,477,474,470,467,463,460,457, + 453,450,447,443,440,437,434,431 +}; + + +// floor(8363 * 64 * 2**(-n/768)) +// 768 = 64 period steps for 12 notes +// Table is for highest possible octave +const uint32 XMLinearTable[768] = +{ + 535232,534749,534266,533784,533303,532822,532341,531861, + 531381,530902,530423,529944,529466,528988,528511,528034, + 527558,527082,526607,526131,525657,525183,524709,524236, + 523763,523290,522818,522346,521875,521404,520934,520464, + 519994,519525,519057,518588,518121,517653,517186,516720, + 516253,515788,515322,514858,514393,513929,513465,513002, + 512539,512077,511615,511154,510692,510232,509771,509312, + 508852,508393,507934,507476,507018,506561,506104,505647, + 505191,504735,504280,503825,503371,502917,502463,502010, + 501557,501104,500652,500201,499749,499298,498848,498398, + 497948,497499,497050,496602,496154,495706,495259,494812, + 494366,493920,493474,493029,492585,492140,491696,491253, + 490809,490367,489924,489482,489041,488600,488159,487718, + 487278,486839,486400,485961,485522,485084,484647,484210, + 483773,483336,482900,482465,482029,481595,481160,480726, + 480292,479859,479426,478994,478562,478130,477699,477268, + 476837,476407,475977,475548,475119,474690,474262,473834, + 473407,472979,472553,472126,471701,471275,470850,470425, + 470001,469577,469153,468730,468307,467884,467462,467041, + 466619,466198,465778,465358,464938,464518,464099,463681, + 463262,462844,462427,462010,461593,461177,460760,460345, + 459930,459515,459100,458686,458272,457859,457446,457033, + 456621,456209,455797,455386,454975,454565,454155,453745, + 453336,452927,452518,452110,451702,451294,450887,450481, + 450074,449668,449262,448857,448452,448048,447644,447240, + 446836,446433,446030,445628,445226,444824,444423,444022, + 443622,443221,442821,442422,442023,441624,441226,440828, + 440430,440033,439636,439239,438843,438447,438051,437656, + 437261,436867,436473,436079,435686,435293,434900,434508, + 434116,433724,433333,432942,432551,432161,431771,431382, + 430992,430604,430215,429827,429439,429052,428665,428278, + 427892,427506,427120,426735,426350,425965,425581,425197, + 424813,424430,424047,423665,423283,422901,422519,422138, + 421757,421377,420997,420617,420237,419858,419479,419101, + 418723,418345,417968,417591,417214,416838,416462,416086, + 415711,415336,414961,414586,414212,413839,413465,413092, + 412720,412347,411975,411604,411232,410862,410491,410121, + 409751,409381,409012,408643,408274,407906,407538,407170, + 406803,406436,406069,405703,405337,404971,404606,404241, + 403876,403512,403148,402784,402421,402058,401695,401333, + 400970,400609,400247,399886,399525,399165,398805,398445, + 398086,397727,397368,397009,396651,396293,395936,395579, + 395222,394865,394509,394153,393798,393442,393087,392733, + 392378,392024,391671,391317,390964,390612,390259,389907, + 389556,389204,388853,388502,388152,387802,387452,387102, + 386753,386404,386056,385707,385359,385012,384664,384317, + 383971,383624,383278,382932,382587,382242,381897,381552, + 381208,380864,380521,380177,379834,379492,379149,378807, + 378466,378124,377783,377442,377102,376762,376422,376082, + 375743,375404,375065,374727,374389,374051,373714,373377, + 373040,372703,372367,372031,371695,371360,371025,370690, + 370356,370022,369688,369355,369021,368688,368356,368023, + 367691,367360,367028,366697,366366,366036,365706,365376, + 365046,364717,364388,364059,363731,363403,363075,362747, + 362420,362093,361766,361440,361114,360788,360463,360137, + 359813,359488,359164,358840,358516,358193,357869,357547, + 357224,356902,356580,356258,355937,355616,355295,354974, + 354654,354334,354014,353695,353376,353057,352739,352420, + 352103,351785,351468,351150,350834,350517,350201,349885, + 349569,349254,348939,348624,348310,347995,347682,347368, + 347055,346741,346429,346116,345804,345492,345180,344869, + 344558,344247,343936,343626,343316,343006,342697,342388, + 342079,341770,341462,341154,340846,340539,340231,339924, + 339618,339311,339005,338700,338394,338089,337784,337479, + 337175,336870,336566,336263,335959,335656,335354,335051, + 334749,334447,334145,333844,333542,333242,332941,332641, + 332341,332041,331741,331442,331143,330844,330546,330247, + 329950,329652,329355,329057,328761,328464,328168,327872, + 327576,327280,326985,326690,326395,326101,325807,325513, + 325219,324926,324633,324340,324047,323755,323463,323171, + 322879,322588,322297,322006,321716,321426,321136,320846, + 320557,320267,319978,319690,319401,319113,318825,318538, + 318250,317963,317676,317390,317103,316817,316532,316246, + 315961,315676,315391,315106,314822,314538,314254,313971, + 313688,313405,313122,312839,312557,312275,311994,311712, + 311431,311150,310869,310589,310309,310029,309749,309470, + 309190,308911,308633,308354,308076,307798,307521,307243, + 306966,306689,306412,306136,305860,305584,305308,305033, + 304758,304483,304208,303934,303659,303385,303112,302838, + 302565,302292,302019,301747,301475,301203,300931,300660, + 300388,300117,299847,299576,299306,299036,298766,298497, + 298227,297958,297689,297421,297153,296884,296617,296349, + 296082,295815,295548,295281,295015,294749,294483,294217, + 293952,293686,293421,293157,292892,292628,292364,292100, + 291837,291574,291311,291048,290785,290523,290261,289999, + 289737,289476,289215,288954,288693,288433,288173,287913, + 287653,287393,287134,286875,286616,286358,286099,285841, + 285583,285326,285068,284811,284554,284298,284041,283785, + 283529,283273,283017,282762,282507,282252,281998,281743, + 281489,281235,280981,280728,280475,280222,279969,279716, + 279464,279212,278960,278708,278457,278206,277955,277704, + 277453,277203,276953,276703,276453,276204,275955,275706, + 275457,275209,274960,274712,274465,274217,273970,273722, + 273476,273229,272982,272736,272490,272244,271999,271753, + 271508,271263,271018,270774,270530,270286,270042,269798, + 269555,269312,269069,268826,268583,268341,268099,267857 +}; + + +// round(65536 * 2**(n/768)) +// 768 = 64 extra-fine finetune steps for 12 notes +// Table content is in 16.16 format +const uint32 FineLinearSlideUpTable[16] = +{ + 65536, 65595, 65654, 65714, 65773, 65832, 65892, 65951, + 66011, 66071, 66130, 66190, 66250, 66309, 66369, 66429 +}; + + +// round(65536 * 2**(-n/768)) +// 768 = 64 extra-fine finetune steps for 12 notes +// Table content is in 16.16 format +// Note that there are a few errors in this table (typos?), but well, this table comes straight from Impulse Tracker's source... +// Entry 0 (65535) should be 65536 (this value is unused and most likely stored this way so that it fits in a 16-bit integer) +// Entry 11 (64888) should be 64889 - rounding error? +// Entry 15 (64645) should be 64655 - typo? +const uint32 FineLinearSlideDownTable[16] = +{ + 65535, 65477, 65418, 65359, 65300, 65241, 65182, 65123, + 65065, 65006, 64947, 64888, 64830, 64772, 64713, 64645 +}; + + +// round(65536 * 2**(n/192)) +// 192 = 16 finetune steps for 12 notes +// Table content is in 16.16 format +const uint32 LinearSlideUpTable[256] = +{ + 65536, 65773, 66011, 66250, 66489, 66730, 66971, 67213, + 67456, 67700, 67945, 68191, 68438, 68685, 68933, 69183, + 69433, 69684, 69936, 70189, 70443, 70698, 70953, 71210, + 71468, 71726, 71985, 72246, 72507, 72769, 73032, 73297, + 73562, 73828, 74095, 74363, 74632, 74902, 75172, 75444, + 75717, 75991, 76266, 76542, 76819, 77096, 77375, 77655, + 77936, 78218, 78501, 78785, 79069, 79355, 79642, 79930, + 80220, 80510, 80801, 81093, 81386, 81681, 81976, 82273, + 82570, 82869, 83169, 83469, 83771, 84074, 84378, 84683, + 84990, 85297, 85606, 85915, 86226, 86538, 86851, 87165, + 87480, 87796, 88114, 88433, 88752, 89073, 89396, 89719, + 90043, 90369, 90696, 91024, 91353, 91684, 92015, 92348, + 92682, 93017, 93354, 93691, 94030, 94370, 94711, 95054, + 95398, 95743, 96089, 96436, 96785, 97135, 97487, 97839, + 98193, 98548, 98905, 99262, 99621, 99982, 100343, 100706, + 101070, 101436, 101803, 102171, 102540, 102911, 103283, 103657, + 104032, 104408, 104786, 105165, 105545, 105927, 106310, 106694, + 107080, 107468, 107856, 108246, 108638, 109031, 109425, 109821, + 110218, 110617, 111017, 111418, 111821, 112226, 112631, 113039, + 113448, 113858, 114270, 114683, 115098, 115514, 115932, 116351, + 116772, 117194, 117618, 118043, 118470, 118899, 119329, 119760, + 120194, 120628, 121065, 121502, 121942, 122383, 122825, 123270, + 123715, 124163, 124612, 125063, 125515, 125969, 126425, 126882, + 127341, 127801, 128263, 128727, 129193, 129660, 130129, 130600, + 131072, 131546, 132022, 132499, 132978, 133459, 133942, 134427, + 134913, 135401, 135890, 136382, 136875, 137370, 137867, 138366, + 138866, 139368, 139872, 140378, 140886, 141395, 141907, 142420, + 142935, 143452, 143971, 144491, 145014, 145539, 146065, 146593, + 147123, 147655, 148189, 148725, 149263, 149803, 150345, 150889, + 151434, 151982, 152532, 153083, 153637, 154193, 154750, 155310, + 155872, 156435, 157001, 157569, 158139, 158711, 159285, 159861, + 160439, 161019, 161602, 162186, 162773, 163361, 163952, 164545 +}; + + +// round(65536 * 2**(-n/192)) +// 192 = 16 finetune steps for 12 notes +// Table content is in 16.16 format +const uint32 LinearSlideDownTable[256] = +{ + 65536, 65300, 65065, 64830, 64596, 64364, 64132, 63901, + 63670, 63441, 63212, 62984, 62757, 62531, 62306, 62081, + 61858, 61635, 61413, 61191, 60971, 60751, 60532, 60314, + 60097, 59880, 59664, 59449, 59235, 59022, 58809, 58597, + 58386, 58176, 57966, 57757, 57549, 57341, 57135, 56929, + 56724, 56519, 56316, 56113, 55911, 55709, 55508, 55308, + 55109, 54910, 54713, 54515, 54319, 54123, 53928, 53734, + 53540, 53347, 53155, 52963, 52773, 52582, 52393, 52204, + 52016, 51829, 51642, 51456, 51270, 51085, 50901, 50718, + 50535, 50353, 50172, 49991, 49811, 49631, 49452, 49274, + 49097, 48920, 48743, 48568, 48393, 48218, 48044, 47871, + 47699, 47527, 47356, 47185, 47015, 46846, 46677, 46509, + 46341, 46174, 46008, 45842, 45677, 45512, 45348, 45185, + 45022, 44859, 44698, 44537, 44376, 44216, 44057, 43898, + 43740, 43582, 43425, 43269, 43113, 42958, 42803, 42649, + 42495, 42342, 42189, 42037, 41886, 41735, 41584, 41434, + 41285, 41136, 40988, 40840, 40693, 40547, 40400, 40255, + 40110, 39965, 39821, 39678, 39535, 39392, 39250, 39109, + 38968, 38828, 38688, 38548, 38409, 38271, 38133, 37996, + 37859, 37722, 37586, 37451, 37316, 37181, 37047, 36914, + 36781, 36648, 36516, 36385, 36254, 36123, 35993, 35863, + 35734, 35605, 35477, 35349, 35221, 35095, 34968, 34842, + 34716, 34591, 34467, 34343, 34219, 34095, 33973, 33850, + 33728, 33607, 33486, 33365, 33245, 33125, 33005, 32887, + 32768, 32650, 32532, 32415, 32298, 32182, 32066, 31950, + 31835, 31720, 31606, 31492, 31379, 31266, 31153, 31041, + 30929, 30817, 30706, 30596, 30485, 30376, 30266, 30157, + 30048, 29940, 29832, 29725, 29618, 29511, 29405, 29299, + 29193, 29088, 28983, 28879, 28774, 28671, 28567, 28464, + 28362, 28260, 28158, 28056, 27955, 27855, 27754, 27654, + 27554, 27455, 27356, 27258, 27159, 27062, 26964, 26867, + 26770, 26674, 26577, 26482, 26386, 26291, 26196, 26102 +}; + + +// FT2's square root panning law LUT. +// Formula to generate this table: round(65536 * sqrt(n / 256)) +const uint16 XMPanningTable[256] = +{ + 0, 4096, 5793, 7094, 8192, 9159, 10033, 10837, 11585, 12288, 12953, 13585, 14189, 14768, 15326, 15864, + 16384, 16888, 17378, 17854, 18318, 18770, 19212, 19644, 20066, 20480, 20886, 21283, 21674, 22058, 22435, 22806, + 23170, 23530, 23884, 24232, 24576, 24915, 25249, 25580, 25905, 26227, 26545, 26859, 27170, 27477, 27780, 28081, + 28378, 28672, 28963, 29251, 29537, 29819, 30099, 30377, 30652, 30924, 31194, 31462, 31727, 31991, 32252, 32511, + 32768, 33023, 33276, 33527, 33776, 34024, 34270, 34514, 34756, 34996, 35235, 35472, 35708, 35942, 36175, 36406, + 36636, 36864, 37091, 37316, 37540, 37763, 37985, 38205, 38424, 38642, 38858, 39073, 39287, 39500, 39712, 39923, + 40132, 40341, 40548, 40755, 40960, 41164, 41368, 41570, 41771, 41972, 42171, 42369, 42567, 42763, 42959, 43154, + 43348, 43541, 43733, 43925, 44115, 44305, 44494, 44682, 44869, 45056, 45242, 45427, 45611, 45795, 45977, 46160, + 46341, 46522, 46702, 46881, 47059, 47237, 47415, 47591, 47767, 47942, 48117, 48291, 48465, 48637, 48809, 48981, + 49152, 49322, 49492, 49661, 49830, 49998, 50166, 50332, 50499, 50665, 50830, 50995, 51159, 51323, 51486, 51649, + 51811, 51972, 52134, 52294, 52454, 52614, 52773, 52932, 53090, 53248, 53405, 53562, 53719, 53874, 54030, 54185, + 54340, 54494, 54647, 54801, 54954, 55106, 55258, 55410, 55561, 55712, 55862, 56012, 56162, 56311, 56459, 56608, + 56756, 56903, 57051, 57198, 57344, 57490, 57636, 57781, 57926, 58071, 58215, 58359, 58503, 58646, 58789, 58931, + 59073, 59215, 59357, 59498, 59639, 59779, 59919, 60059, 60199, 60338, 60477, 60615, 60753, 60891, 61029, 61166, + 61303, 61440, 61576, 61712, 61848, 61984, 62119, 62254, 62388, 62523, 62657, 62790, 62924, 63057, 63190, 63323, + 63455, 63587, 63719, 63850, 63982, 64113, 64243, 64374, 64504, 64634, 64763, 64893, 65022, 65151, 65279, 65408, +}; + + +// IT Vibrato -> OpenMPT/XM VibratoType +const uint8 AutoVibratoIT2XM[8] = { VIB_SINE, VIB_RAMP_DOWN, VIB_SQUARE, VIB_RANDOM, VIB_RAMP_UP, 0, 0, 0 }; +// OpenMPT/XM VibratoType -> IT Vibrato +const uint8 AutoVibratoXM2IT[8] = { 0, 2, 4, 1, 3, 0, 0, 0 }; + +// Reversed sinc coefficients for 4x256 taps polyphase FIR resampling filter (SchismTracker's lutgen.c should generate a very similar table, but it's more precise) +const int16 CResampler::FastSincTable[256*4] = +{ // Cubic Spline + 0, 16384, 0, 0, -31, 16383, 32, 0, -63, 16381, 65, 0, -93, 16378, 100, -1, + -124, 16374, 135, -1, -153, 16368, 172, -3, -183, 16361, 209, -4, -211, 16353, 247, -5, + -240, 16344, 287, -7, -268, 16334, 327, -9, -295, 16322, 368, -12, -322, 16310, 410, -14, + -348, 16296, 453, -17, -374, 16281, 497, -20, -400, 16265, 541, -23, -425, 16248, 587, -26, + -450, 16230, 634, -30, -474, 16210, 681, -33, -497, 16190, 729, -37, -521, 16168, 778, -41, + -543, 16145, 828, -46, -566, 16121, 878, -50, -588, 16097, 930, -55, -609, 16071, 982, -60, + -630, 16044, 1035, -65, -651, 16016, 1089, -70, -671, 15987, 1144, -75, -691, 15957, 1199, -81, + -710, 15926, 1255, -87, -729, 15894, 1312, -93, -748, 15861, 1370, -99, -766, 15827, 1428, -105, + -784, 15792, 1488, -112, -801, 15756, 1547, -118, -818, 15719, 1608, -125, -834, 15681, 1669, -132, + -850, 15642, 1731, -139, -866, 15602, 1794, -146, -881, 15561, 1857, -153, -896, 15520, 1921, -161, + -911, 15477, 1986, -168, -925, 15434, 2051, -176, -939, 15390, 2117, -184, -952, 15344, 2184, -192, + -965, 15298, 2251, -200, -978, 15251, 2319, -208, -990, 15204, 2387, -216, -1002, 15155, 2456, -225, +-1014, 15106, 2526, -234, -1025, 15055, 2596, -242, -1036, 15004, 2666, -251, -1046, 14952, 2738, -260, +-1056, 14899, 2810, -269, -1066, 14846, 2882, -278, -1075, 14792, 2955, -287, -1084, 14737, 3028, -296, +-1093, 14681, 3102, -306, -1102, 14624, 3177, -315, -1110, 14567, 3252, -325, -1118, 14509, 3327, -334, +-1125, 14450, 3403, -344, -1132, 14390, 3480, -354, -1139, 14330, 3556, -364, -1145, 14269, 3634, -374, +-1152, 14208, 3712, -384, -1157, 14145, 3790, -394, -1163, 14082, 3868, -404, -1168, 14018, 3947, -414, +-1173, 13954, 4027, -424, -1178, 13889, 4107, -434, -1182, 13823, 4187, -445, -1186, 13757, 4268, -455, +-1190, 13690, 4349, -465, -1193, 13623, 4430, -476, -1196, 13555, 4512, -486, -1199, 13486, 4594, -497, +-1202, 13417, 4676, -507, -1204, 13347, 4759, -518, -1206, 13276, 4842, -528, -1208, 13205, 4926, -539, +-1210, 13134, 5010, -550, -1211, 13061, 5094, -560, -1212, 12989, 5178, -571, -1212, 12915, 5262, -581, +-1213, 12842, 5347, -592, -1213, 12767, 5432, -603, -1213, 12693, 5518, -613, -1213, 12617, 5603, -624, +-1212, 12542, 5689, -635, -1211, 12466, 5775, -645, -1210, 12389, 5862, -656, -1209, 12312, 5948, -667, +-1208, 12234, 6035, -677, -1206, 12156, 6122, -688, -1204, 12078, 6209, -698, -1202, 11999, 6296, -709, +-1200, 11920, 6384, -720, -1197, 11840, 6471, -730, -1194, 11760, 6559, -740, -1191, 11679, 6647, -751, +-1188, 11598, 6735, -761, -1184, 11517, 6823, -772, -1181, 11436, 6911, -782, -1177, 11354, 6999, -792, +-1173, 11271, 7088, -802, -1168, 11189, 7176, -812, -1164, 11106, 7265, -822, -1159, 11022, 7354, -832, +-1155, 10939, 7442, -842, -1150, 10855, 7531, -852, -1144, 10771, 7620, -862, -1139, 10686, 7709, -872, +-1134, 10602, 7798, -882, -1128, 10516, 7886, -891, -1122, 10431, 7975, -901, -1116, 10346, 8064, -910, +-1110, 10260, 8153, -919, -1103, 10174, 8242, -929, -1097, 10088, 8331, -938, -1090, 10001, 8420, -947, +-1083, 9915, 8508, -956, -1076, 9828, 8597, -965, -1069, 9741, 8686, -973, -1062, 9654, 8774, -982, +-1054, 9566, 8863, -991, -1047, 9479, 8951, -999, -1039, 9391, 9039, -1007, -1031, 9303, 9127, -1015, +-1024, 9216, 9216, -1024, -1015, 9127, 9303, -1031, -1007, 9039, 9391, -1039, -999, 8951, 9479, -1047, + -991, 8863, 9566, -1054, -982, 8774, 9654, -1062, -973, 8686, 9741, -1069, -965, 8597, 9828, -1076, + -956, 8508, 9915, -1083, -947, 8420, 10001, -1090, -938, 8331, 10088, -1097, -929, 8242, 10174, -1103, + -919, 8153, 10260, -1110, -910, 8064, 10346, -1116, -901, 7975, 10431, -1122, -891, 7886, 10516, -1128, + -882, 7798, 10602, -1134, -872, 7709, 10686, -1139, -862, 7620, 10771, -1144, -852, 7531, 10855, -1150, + -842, 7442, 10939, -1155, -832, 7354, 11022, -1159, -822, 7265, 11106, -1164, -812, 7176, 11189, -1168, + -802, 7088, 11271, -1173, -792, 6999, 11354, -1177, -782, 6911, 11436, -1181, -772, 6823, 11517, -1184, + -761, 6735, 11598, -1188, -751, 6647, 11679, -1191, -740, 6559, 11760, -1194, -730, 6471, 11840, -1197, + -720, 6384, 11920, -1200, -709, 6296, 11999, -1202, -698, 6209, 12078, -1204, -688, 6122, 12156, -1206, + -677, 6035, 12234, -1208, -667, 5948, 12312, -1209, -656, 5862, 12389, -1210, -645, 5775, 12466, -1211, + -635, 5689, 12542, -1212, -624, 5603, 12617, -1213, -613, 5518, 12693, -1213, -603, 5432, 12767, -1213, + -592, 5347, 12842, -1213, -581, 5262, 12915, -1212, -571, 5178, 12989, -1212, -560, 5094, 13061, -1211, + -550, 5010, 13134, -1210, -539, 4926, 13205, -1208, -528, 4842, 13276, -1206, -518, 4759, 13347, -1204, + -507, 4676, 13417, -1202, -497, 4594, 13486, -1199, -486, 4512, 13555, -1196, -476, 4430, 13623, -1193, + -465, 4349, 13690, -1190, -455, 4268, 13757, -1186, -445, 4187, 13823, -1182, -434, 4107, 13889, -1178, + -424, 4027, 13954, -1173, -414, 3947, 14018, -1168, -404, 3868, 14082, -1163, -394, 3790, 14145, -1157, + -384, 3712, 14208, -1152, -374, 3634, 14269, -1145, -364, 3556, 14330, -1139, -354, 3480, 14390, -1132, + -344, 3403, 14450, -1125, -334, 3327, 14509, -1118, -325, 3252, 14567, -1110, -315, 3177, 14624, -1102, + -306, 3102, 14681, -1093, -296, 3028, 14737, -1084, -287, 2955, 14792, -1075, -278, 2882, 14846, -1066, + -269, 2810, 14899, -1056, -260, 2738, 14952, -1046, -251, 2666, 15004, -1036, -242, 2596, 15055, -1025, + -234, 2526, 15106, -1014, -225, 2456, 15155, -1002, -216, 2387, 15204, -990, -208, 2319, 15251, -978, + -200, 2251, 15298, -965, -192, 2184, 15344, -952, -184, 2117, 15390, -939, -176, 2051, 15434, -925, + -168, 1986, 15477, -911, -161, 1921, 15520, -896, -153, 1857, 15561, -881, -146, 1794, 15602, -866, + -139, 1731, 15642, -850, -132, 1669, 15681, -834, -125, 1608, 15719, -818, -118, 1547, 15756, -801, + -112, 1488, 15792, -784, -105, 1428, 15827, -766, -99, 1370, 15861, -748, -93, 1312, 15894, -729, + -87, 1255, 15926, -710, -81, 1199, 15957, -691, -75, 1144, 15987, -671, -70, 1089, 16016, -651, + -65, 1035, 16044, -630, -60, 982, 16071, -609, -55, 930, 16097, -588, -50, 878, 16121, -566, + -46, 828, 16145, -543, -41, 778, 16168, -521, -37, 729, 16190, -497, -33, 681, 16210, -474, + -30, 634, 16230, -450, -26, 587, 16248, -425, -23, 541, 16265, -400, -20, 497, 16281, -374, + -17, 453, 16296, -348, -14, 410, 16310, -322, -12, 368, 16322, -295, -9, 327, 16334, -268, + -7, 287, 16344, -240, -5, 247, 16353, -211, -4, 209, 16361, -183, -3, 172, 16368, -153, + -1, 135, 16374, -124, -1, 100, 16378, -93, 0, 65, 16381, -63, 0, 32, 16383, -31, +}; + + +///////////////////////////////////////////////////////////////////////////////////////////// + + +// Compute Bessel function Izero(y) using a series approximation +double Izero(double y) +{ + double s = 1, ds = 1, d = 0; + do + { + d = d + 2; + ds = ds * (y * y) / (d * d); + s = s + ds; + } while(ds > 1E-7 * s); + return s; +} + + +static void getsinc(SINC_TYPE *psinc, double beta, double cutoff) +{ + if(cutoff >= 0.999) + { + // Avoid mixer overflows. + // 1.0 itself does not make much sense. + cutoff = 0.999; + } + const double izeroBeta = Izero(beta); + const double kPi = 4.0 * std::atan(1.0) * cutoff; + for(int isrc = 0; isrc < 8 * SINC_PHASES; isrc++) + { + double fsinc; + int ix = 7 - (isrc & 7); + ix = (ix * SINC_PHASES) + (isrc >> 3); + if(ix == (4 * SINC_PHASES)) + { + fsinc = 1.0; + } else + { + const double x = (double)(ix - (4 * SINC_PHASES)) * (double)(1.0 / SINC_PHASES); + const double xPi = x * kPi; + fsinc = std::sin(xPi) * Izero(beta * std::sqrt(1 - x * x * (1.0 / 16.0))) / (izeroBeta * xPi); // Kaiser window + } + double coeff = fsinc * cutoff; +#ifdef MPT_INTMIXER + *psinc++ = mpt::saturate_round<SINC_TYPE>(coeff * (1 << SINC_QUANTSHIFT)); +#else + *psinc++ = static_cast<SINC_TYPE>(coeff); +#endif + } +} + + +#ifdef MODPLUG_TRACKER +bool CResampler::StaticTablesInitialized = false; +SINC_TYPE CResampler::gKaiserSinc[SINC_PHASES * 8]; // Upsampling +SINC_TYPE CResampler::gDownsample13x[SINC_PHASES * 8]; // Downsample 1.333x +SINC_TYPE CResampler::gDownsample2x[SINC_PHASES * 8]; // Downsample 2x +Paula::BlepTables CResampler::blepTables; // Amiga BLEP resampler +#ifndef MPT_INTMIXER +mixsample_t CResampler::FastSincTablef[256 * 4]; // Cubic spline LUT +#endif // !defined(MPT_INTMIXER) +#endif // MODPLUG_TRACKER + + +void CResampler::InitFloatmixerTables() +{ +#ifdef MPT_BUILD_FUZZER + // Creating resampling tables can take a little while which we really should not spend + // when fuzzing OpenMPT for crashes and hangs. This content of the tables is not really + // relevant for any kind of possible crashes or hangs. + return; +#endif // MPT_BUILD_FUZZER +#ifndef MPT_INTMIXER + // Prepare fast sinc coefficients for floating point mixer + for(std::size_t i = 0; i < std::size(FastSincTable); i++) + { + FastSincTablef[i] = static_cast<mixsample_t>(FastSincTable[i] * mixsample_t(1.0f / 16384.0f)); + } +#endif // !defined(MPT_INTMIXER) +} + + +void CResampler::InitializeTablesFromScratch(bool force) +{ + bool initParameterIndependentTables = false; + if(force) + { + initParameterIndependentTables = true; + } +#ifdef MODPLUG_TRACKER + initParameterIndependentTables = !StaticTablesInitialized; +#endif // MODPLUG_TRACKER + + MPT_MAYBE_CONSTANT_IF(initParameterIndependentTables) + { + InitFloatmixerTables(); + + blepTables.InitTables(); + + getsinc(gKaiserSinc, 9.6377, 0.97); + getsinc(gDownsample13x, 8.5, 0.5); + getsinc(gDownsample2x, 2.7625, 0.425); + +#ifdef MODPLUG_TRACKER + StaticTablesInitialized = true; +#endif // MODPLUG_TRACKER + } + + if((m_OldSettings == m_Settings) && !force) + { + return; + } + + m_WindowedFIR.InitTable(m_Settings.gdWFIRCutoff, m_Settings.gbWFIRType); + + m_OldSettings = m_Settings; +} + + +#ifdef MPT_RESAMPLER_TABLES_CACHED + +static const CResampler & GetCachedResampler() +{ + static CResampler s_CachedResampler(true); + return s_CachedResampler; +} + + +void CResampler::InitializeTablesFromCache() +{ + const CResampler & s_CachedResampler = GetCachedResampler(); + InitFloatmixerTables(); + std::copy(s_CachedResampler.gKaiserSinc, s_CachedResampler.gKaiserSinc + SINC_PHASES*8, gKaiserSinc); + std::copy(s_CachedResampler.gDownsample13x, s_CachedResampler.gDownsample13x + SINC_PHASES*8, gDownsample13x); + std::copy(s_CachedResampler.gDownsample2x, s_CachedResampler.gDownsample2x + SINC_PHASES*8, gDownsample2x); + std::copy(s_CachedResampler.m_WindowedFIR.lut, s_CachedResampler.m_WindowedFIR.lut + WFIR_LUTLEN*WFIR_WIDTH, m_WindowedFIR.lut); + blepTables = s_CachedResampler.blepTables; +} + +#endif // MPT_RESAMPLER_TABLES_CACHED + + +#ifdef MPT_RESAMPLER_TABLES_CACHED_ONSTARTUP + +struct ResampleCacheInitializer +{ + ResampleCacheInitializer() + { + GetCachedResampler(); + } +}; +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wglobal-constructors" +#endif // MPT_COMPILER_CLANG +static ResampleCacheInitializer g_ResamplerCachePrimer; +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG + +#endif // MPT_RESAMPLER_TABLES_CACHED_ONSTARTUP + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Tables.h b/Src/external_dependencies/openmpt-trunk/soundlib/Tables.h new file mode 100644 index 00000000..d9a8fccd --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Tables.h @@ -0,0 +1,46 @@ +/* + * Tables.h + * -------- + * Purpose: Effect, interpolation, data and other pre-calculated tables. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +// Compute Bessel function Izero(y) using a series approximation +double Izero(double y); + +extern const mpt::uchar NoteNamesSharp[12][4]; +extern const mpt::uchar NoteNamesFlat[12][4]; + +extern const uint8 ImpulseTrackerPortaVolCmd[16]; +extern const uint16 ProTrackerPeriodTable[7*12]; +extern const uint16 ProTrackerTunedPeriods[16*12]; +extern const uint8 ModEFxTable[16]; +extern const uint16 FreqS3MTable[12]; +extern const uint16 S3MFineTuneTable[16]; +extern const int8 ModSinusTable[64]; +extern const int8 ModRandomTable[64]; +extern const int8 ITSinusTable[256]; +extern const int8 retrigTable1[16]; +extern const int8 retrigTable2[16]; +extern const uint16 XMPeriodTable[104]; +extern const uint32 XMLinearTable[768]; +extern const uint32 FineLinearSlideUpTable[16]; +extern const uint32 FineLinearSlideDownTable[16]; +extern const uint32 LinearSlideUpTable[256]; +extern const uint32 LinearSlideDownTable[256]; +extern const uint16 XMPanningTable[256]; + +extern const uint8 AutoVibratoIT2XM[8]; +extern const uint8 AutoVibratoXM2IT[8]; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.cpp new file mode 100644 index 00000000..1fb7b13a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.cpp @@ -0,0 +1,38 @@ +/* + * tagging.cpp + * ----------- + * Purpose: Structure holding a superset of tags for all supported output sample or stream files or types. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Tagging.h" +#include "../common/version.h" + +OPENMPT_NAMESPACE_BEGIN + + +void FileTags::SetEncoder() +{ + encoder = Version::Current().GetOpenMPTVersionString(); +} + + +mpt::ustring GetSampleNameFromTags(const FileTags &tags) +{ + mpt::ustring result; + if(tags.artist.empty()) + { + result = tags.title; + } else + { + result = MPT_UFORMAT("{} (by {})")(tags.title, tags.artist); + } + return result; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.h b/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.h new file mode 100644 index 00000000..272262a9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/Tagging.h @@ -0,0 +1,46 @@ +/* + * Tagging.h + * --------- + * Purpose: Structure holding a superset of tags for all supported output sample or stream files or types. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <string> + +OPENMPT_NAMESPACE_BEGIN + + +struct FileTags +{ + + mpt::ustring encoder; + + mpt::ustring title; + mpt::ustring comments; + + mpt::ustring bpm; + + mpt::ustring artist; + mpt::ustring album; + mpt::ustring trackno; + mpt::ustring year; + mpt::ustring url; + + mpt::ustring genre; + + void SetEncoder(); + +}; + + +mpt::ustring GetSampleNameFromTags(const FileTags &tags); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.cpp new file mode 100644 index 00000000..2aa709da --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.cpp @@ -0,0 +1,154 @@ +/* + * TinyFFT.cpp + * ----------- + * Purpose: A simple FFT implementation for power-of-two FFTs + * Notes : This is a C++ adaption of Ryuhei Mori's BSD 2-clause licensed TinyFFT + * available from https://github.com/ryuhei-mori/tinyfft + * Authors: Ryuhei Mori + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "TinyFFT.h" + +OPENMPT_NAMESPACE_BEGIN + +void TinyFFT::GenerateTwiddleFactors(uint32 i, uint32 b, std::complex<double> z) +{ + if(b == 0) + w[i] = z; + else + { + GenerateTwiddleFactors(i, b >> 1, z); + GenerateTwiddleFactors(i | b, b >> 1, z * w[b]); + } +} + + +TinyFFT::TinyFFT(const uint32 fftSize) + : w(std::size_t(1) << (fftSize - 1)) + , k(fftSize) +{ + const uint32 m = 1 << k; + constexpr double PI2_ = 6.28318530717958647692; + const double arg = -PI2_ / m; + for(uint32 i = 1, j = m / 4; j; i <<= 1, j >>= 1) + { + w[i] = std::exp(I * (arg * j)); + } + GenerateTwiddleFactors(0, m / 4, 1); +} + + +uint32 TinyFFT::Size() const noexcept +{ + return 1 << k; +} + + +// Computes in-place FFT of size 2^k of A, result is in bit-reversed order. +void TinyFFT::FFT(std::vector<std::complex<double>> &A) const +{ + MPT_ASSERT(A.size() == (std::size_t(1) << k)); + const uint32 m = 1 << k; + uint32 u = 1; + uint32 v = m / 4; + if(k & 1) + { + for(uint32 j = 0; j < m / 2; j++) + { + auto Ajv = A[j + (m / 2)]; + A[j + (m / 2)] = A[j] - Ajv; + A[j] += Ajv; + } + u <<= 1; + v >>= 1; + } + for(uint32 i = k & ~1; i > 0; i -= 2) + { + for(uint32 jh = 0; jh < u; jh++) + { + auto wj = w[jh << 1]; + auto wj2 = w[jh]; + auto wj3 = wj2 * wj; + for(uint32 j = jh << i, je = j + v; j < je; j++) + { + auto tmp0 = A[j]; + auto tmp1 = wj * A[j + v]; + auto tmp2 = wj2 * A[j + 2 * v]; + auto tmp3 = wj3 * A[j + 3 * v]; + + auto ttmp0 = tmp0 + tmp2; + auto ttmp2 = tmp0 - tmp2; + auto ttmp1 = tmp1 + tmp3; + auto ttmp3 = -I * (tmp1 - tmp3); + + A[j] = ttmp0 + ttmp1; + A[j + v] = ttmp0 - ttmp1; + A[j + 2 * v] = ttmp2 + ttmp3; + A[j + 3 * v] = ttmp2 - ttmp3; + } + } + u <<= 2; + v >>= 2; + } +} + + +// Computes in-place IFFT of size 2^k of A, input is expected to be in bit-reversed order. +void TinyFFT::IFFT(std::vector<std::complex<double>> &A) const +{ + MPT_ASSERT(A.size() == (std::size_t(1) << k)); + const uint32 m = 1 << k; + uint32 u = m / 4; + uint32 v = 1; + for(uint32 i = 2; i <= k; i += 2) + { + for(uint32 jh = 0; jh < u; jh++) + { + auto wj = std::conj(w[jh << 1]); + auto wj2 = std::conj(w[jh]); + auto wj3 = wj2 * wj; + for(uint32 j = jh << i, je = j + v; j < je; j++) + { + auto tmp0 = A[j]; + auto tmp1 = A[j + v]; + auto tmp2 = A[j + 2 * v]; + auto tmp3 = A[j + 3 * v]; + + auto ttmp0 = tmp0 + tmp1; + auto ttmp1 = tmp0 - tmp1; + auto ttmp2 = tmp2 + tmp3; + auto ttmp3 = I * (tmp2 - tmp3); + + A[j] = ttmp0 + ttmp2; + A[j + v] = wj * (ttmp1 + ttmp3); + A[j + 2 * v] = wj2 * (ttmp0 - ttmp2); + A[j + 3 * v] = wj3 * (ttmp1 - ttmp3); + } + } + u >>= 2; + v <<= 2; + } + if(k & 1) + { + for(uint32 j = 0; j < m / 2; j++) + { + auto Ajv = A[j + (m / 2)]; + A[j + (m / 2)] = A[j] - Ajv; + A[j] += Ajv; + } + } +} + + +void TinyFFT::Normalize(std::vector<std::complex<double>> &data) +{ + const double s = static_cast<double>(data.size()); + for(auto &v : data) + v /= s; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.h b/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.h new file mode 100644 index 00000000..5f0e8f90 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/TinyFFT.h @@ -0,0 +1,42 @@ +/* + * TinyFFT.h + * --------- + * Purpose: A simple FFT implementation for power-of-two FFTs + * Notes : This is a C++ adaption of Ryuhei Mori's BSD 2-clause licensed TinyFFT + * available from https://github.com/ryuhei-mori/tinyfft + * Authors: Ryuhei Mori + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include <complex> + +OPENMPT_NAMESPACE_BEGIN + +class TinyFFT +{ + static constexpr std::complex<double> I{0.0, 1.0}; + std::vector<std::complex<double>> w; // Pre-computed twiddle factors + const uint32 k; // log2 of FFT size + + void GenerateTwiddleFactors(uint32 i, uint32 b, std::complex<double> z); + +public: + TinyFFT(const uint32 fftSize); + + uint32 Size() const noexcept; + + // Computes in-place FFT of size 2^k of A, result is in bit-reversed order. + void FFT(std::vector<std::complex<double>> &A) const; + + // Computes in-place IFFT of size 2^k of A, input is expected to be in bit-reversed order. + void IFFT(std::vector<std::complex<double>> &A) const; + + static void Normalize(std::vector<std::complex<double>> &data); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.cpp new file mode 100644 index 00000000..38366318 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.cpp @@ -0,0 +1,333 @@ +/* +* UMXTools.h +* ------------ +* Purpose: UMX/UAX (Unreal package) helper functions +* Notes : (currently none) +* Authors: OpenMPT Devs (inspired by code from https://wiki.beyondunreal.com/Legacy:Package_File_Format) +* The OpenMPT source code is released under the BSD license. Read LICENSE for more details. +*/ + + +#include "stdafx.h" +#include "Loaders.h" +#include "UMXTools.h" + + +OPENMPT_NAMESPACE_BEGIN + +namespace UMX +{ + +bool FileHeader::IsValid() const +{ + return !std::memcmp(magic, "\xC1\x83\x2A\x9E", 4) + && nameOffset >= sizeof(FileHeader) + && exportOffset >= sizeof(FileHeader) + && importOffset >= sizeof(FileHeader) + && nameCount > 0 && nameCount <= uint32_max / 5u + && exportCount > 0 && exportCount <= uint32_max / 8u + && importCount > 0 && importCount <= uint32_max / 4u + && uint32_max - nameCount * 5u >= nameOffset + && uint32_max - exportCount * 8u >= exportOffset + && uint32_max - importCount * 4u >= importOffset; +} + + +uint32 FileHeader::GetMinimumAdditionalFileSize() const +{ + return std::max({nameOffset + nameCount * 5u, exportOffset + exportCount * 8u, importOffset + importCount * 4u}) - sizeof(FileHeader); +} + + +CSoundFile::ProbeResult ProbeFileHeader(MemoryFileReader file, const uint64 *pfilesize, const char *requiredType) +{ + FileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return CSoundFile::ProbeWantMoreData; + } + if(!fileHeader.IsValid()) + { + return CSoundFile::ProbeFailure; + } + if(requiredType != nullptr && !FindNameTableEntryMemory(file, fileHeader, requiredType)) + { + return CSoundFile::ProbeFailure; + } + return CSoundFile::ProbeAdditionalSize(file, pfilesize, fileHeader.GetMinimumAdditionalFileSize()); +} + + +// Read compressed unreal integers - similar to MIDI integers, but signed values are possible. +template <typename Tfile> +static int32 ReadIndexImpl(Tfile &chunk) +{ + enum + { + signMask = 0x80, // Highest bit of first byte indicates if value is signed + valueMask1 = 0x3F, // Low 6 bits of first byte are actual value + continueMask1 = 0x40, // Second-highest bit of first byte indicates if further bytes follow + valueMask = 0x7F, // Low 7 bits of following bytes are actual value + continueMask = 0x80, // Highest bit of following bytes indicates if further bytes follow + }; + + // Read first byte + uint8 b = chunk.ReadUint8(); + bool isSigned = (b & signMask) != 0; + uint32 result = (b & valueMask1); + int shift = 6; + + if(b & continueMask1) + { + // Read remaining bytes + do + { + b = chunk.ReadUint8(); + uint32 data = static_cast<uint32>(b) & valueMask; + data <<= shift; + result |= data; + shift += 7; + } while((b & continueMask) != 0 && (shift < 32)); + } + + if(isSigned && result <= int32_max) + return -static_cast<int32>(result); + else if(isSigned) + return int32_min; + else + return result; +} + +int32 ReadIndex(FileReader &chunk) +{ + return ReadIndexImpl(chunk); +} + + +// Returns true if the given nme exists in the name table. +template <typename TFile> +static bool FindNameTableEntryImpl(TFile &file, const FileHeader &fileHeader, const char *name) +{ + if(!name) + { + return false; + } + const std::size_t nameLen = std::strlen(name); + if(nameLen == 0) + { + return false; + } + bool result = false; + const FileReader::off_t oldpos = file.GetPosition(); + if(file.Seek(fileHeader.nameOffset)) + { + for(uint32 i = 0; i < fileHeader.nameCount && file.CanRead(5); i++) + { + if(fileHeader.packageVersion >= 64) + { + int32 length = ReadIndexImpl(file); + if(length <= 0) + { + continue; + } + } + bool match = true; + std::size_t pos = 0; + char c = 0; + while((c = file.ReadUint8()) != 0) + { + c = mpt::ToLowerCaseAscii(c); + if(pos < nameLen) + { + match = match && (c == name[pos]); + } + pos++; + } + if(pos != nameLen) + { + match = false; + } + if(match) + { + result = true; + } + file.Skip(4); // Object flags + } + } + file.Seek(oldpos); + return result; +} + +bool FindNameTableEntry(FileReader &file, const FileHeader &fileHeader, const char *name) +{ + return FindNameTableEntryImpl(file, fileHeader, name); +} + +bool FindNameTableEntryMemory(MemoryFileReader &file, const FileHeader &fileHeader, const char *name) +{ + return FindNameTableEntryImpl(file, fileHeader, name); +} + + +// Read an entry from the name table. +std::string ReadNameTableEntry(FileReader &chunk, uint16 packageVersion) +{ + std::string name; + if(packageVersion >= 64) + { + // String length + int32 length = ReadIndex(chunk); + if(length <= 0) + { + return ""; + } + name.reserve(std::min(length, mpt::saturate_cast<int32>(chunk.BytesLeft()))); + } + + // Simple zero-terminated string + uint8 chr; + while((chr = chunk.ReadUint8()) != 0) + { + // Convert string to lower case + if(chr >= 'A' && chr <= 'Z') + { + chr = chr - 'A' + 'a'; + } + name.append(1, static_cast<char>(chr)); + } + + chunk.Skip(4); // Object flags + return name; +} + + +// Read complete name table. +std::vector<std::string> ReadNameTable(FileReader &file, const FileHeader &fileHeader) +{ + file.Seek(fileHeader.nameOffset); // nameOffset and nameCount were validated when parsing header + std::vector<std::string> names; + names.reserve(fileHeader.nameCount); + for(uint32 i = 0; i < fileHeader.nameCount && file.CanRead(5); i++) + { + names.push_back(ReadNameTableEntry(file, fileHeader.packageVersion)); + } + return names; +} + + +// Read an entry from the import table. +int32 ReadImportTableEntry(FileReader &chunk, uint16 packageVersion) +{ + ReadIndex(chunk); // Class package + ReadIndex(chunk); // Class name + if(packageVersion >= 60) + { + chunk.Skip(4); // Package + } else + { + ReadIndex(chunk); // ?? + } + return ReadIndex(chunk); // Object name (offset into the name table) +} + + +// Read import table. +std::vector<int32> ReadImportTable(FileReader &file, const FileHeader &fileHeader, const std::vector<std::string> &names) +{ + file.Seek(fileHeader.importOffset); // importOffset and importCount were validated when parsing header + std::vector<int32> classes; + classes.reserve(fileHeader.importCount); + for(uint32 i = 0; i < fileHeader.importCount && file.CanRead(4); i++) + { + int32 objName = ReadImportTableEntry(file, fileHeader.packageVersion); + if(static_cast<size_t>(objName) < names.size()) + { + classes.push_back(objName); + } + } + return classes; +} + + +// Read an entry from the export table. +std::pair<FileReader, int32> ReadExportTableEntry(FileReader &file, const FileHeader &fileHeader, const std::vector<int32> &classes, const std::vector<std::string> &names, const char *filterType) +{ + const uint32 objClass = ~static_cast<uint32>(ReadIndex(file)); // Object class + if(objClass >= classes.size()) + return {}; + + ReadIndex(file); // Object parent + if(fileHeader.packageVersion >= 60) + { + file.Skip(4); // Internal package / group of the object + } + int32 objName = ReadIndex(file); // Object name (offset into the name table) + file.Skip(4); // Object flags + int32 objSize = ReadIndex(file); + int32 objOffset = ReadIndex(file); + if(objSize <= 0 || objOffset <= static_cast<int32>(sizeof(FileHeader))) + return {}; + + // If filterType is set, reject any objects not of that type + if(filterType != nullptr && names[classes[objClass]] != filterType) + return {}; + + FileReader chunk = file.GetChunkAt(objOffset, objSize); + if(!chunk.IsValid()) + return {}; + + if(fileHeader.packageVersion < 40) + { + chunk.Skip(8); // 00 00 00 00 00 00 00 00 + } + if(fileHeader.packageVersion < 60) + { + chunk.Skip(16); // 81 00 00 00 00 00 FF FF FF FF FF FF FF FF 00 00 + } + // Read object properties +#if 0 + size_t propertyName = static_cast<size_t>(ReadIndex(chunk)); + if(propertyName >= names.size() || names[propertyName] != "none") + { + // Can't bother to implement property reading, as no UMX files I've seen so far use properties for the relevant objects, + // and only the UAX files in the Unreal 1997/98 beta seem to use this and still load just fine when ignoring it. + // If it should be necessary to implement this, check CUnProperty.cpp in http://ut-files.com/index.php?dir=Utilities/&file=utcms_source.zip + MPT_ASSERT_NOTREACHED(); + continue; + } +#else + ReadIndex(chunk); +#endif + + if(fileHeader.packageVersion >= 120) + { + // UT2003 Packages + ReadIndex(chunk); + chunk.Skip(8); + } else if(fileHeader.packageVersion >= 100) + { + // AAO Packages + chunk.Skip(4); + ReadIndex(chunk); + chunk.Skip(4); + } else if(fileHeader.packageVersion >= 62) + { + // UT Packages + // Mech8.umx and a few other UT tunes have packageVersion = 62. + // In CUnSound.cpp, the condition above reads "packageVersion >= 63" but if that is used, those tunes won't load properly. + ReadIndex(chunk); + chunk.Skip(4); + } else + { + // Old Unreal Packagaes + ReadIndex(chunk); + } + + int32 size = ReadIndex(chunk); + return {chunk.ReadChunk(size), objName}; +} + + +} // namespace UMX + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.h b/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.h new file mode 100644 index 00000000..65e24784 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/UMXTools.h @@ -0,0 +1,71 @@ +/* + * UMXTools.h + * ------------ + * Purpose: UMX/UAX (Unreal) helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs (inspired by code from http://wiki.beyondunreal.com/Legacy:Package_File_Format) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + +namespace UMX +{ + +// UMX File Header +struct FileHeader +{ + char magic[4]; // C1 83 2A 9E + uint16le packageVersion; + uint16le licenseMode; + uint32le flags; + uint32le nameCount; + uint32le nameOffset; + uint32le exportCount; + uint32le exportOffset; + uint32le importCount; + uint32le importOffset; + + bool IsValid() const; + uint32 GetMinimumAdditionalFileSize() const; +}; + +MPT_BINARY_STRUCT(FileHeader, 36) + + +// Check validity of file header +CSoundFile::ProbeResult ProbeFileHeader(MemoryFileReader file, const uint64* pfilesize, const char *requiredType); + +// Read compressed unreal integers - similar to MIDI integers, but signed values are possible. +int32 ReadIndex(FileReader &chunk); + +// Returns true if the given nme exists in the name table. +bool FindNameTableEntry(FileReader &file, const FileHeader &fileHeader, const char *name); + +// Returns true if the given nme exists in the name table. +bool FindNameTableEntryMemory(MemoryFileReader &file, const FileHeader &fileHeader, const char *name); + +// Read an entry from the name table. +std::string ReadNameTableEntry(FileReader &chunk, uint16 packageVersion); + +// Read complete name table. +std::vector<std::string> ReadNameTable(FileReader &file, const FileHeader &fileHeader); + +// Read import table. +std::vector<int32> ReadImportTable(FileReader &file, const FileHeader &fileHeader, const std::vector<std::string> &names); + +// Read an entry from the import table. +int32 ReadImportTableEntry(FileReader &chunk, uint16 packageVersion); + +// Read an entry from the export table. +std::pair<FileReader, int32> ReadExportTableEntry(FileReader &file, const FileHeader &fileHeader, const std::vector<int32> &classes, const std::vector<std::string> &names, const char *filterType); + +} // namespace UMX + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/UpgradeModule.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/UpgradeModule.cpp new file mode 100644 index 00000000..4a548f95 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/UpgradeModule.cpp @@ -0,0 +1,750 @@ +/* + * UpdateModule.cpp + * ---------------- + * Purpose: Compensate for playback bugs of previous OpenMPT versions during import + * by rewriting patterns / samples / instruments or enabling / disabling specific compatibility flags + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "plugins/PluginManager.h" +#include "../common/mptStringBuffer.h" +#include "../common/version.h" + + +OPENMPT_NAMESPACE_BEGIN + + +struct UpgradePatternData +{ + UpgradePatternData(CSoundFile &sf) + : sndFile(sf) + , compatPlay(sf.m_playBehaviour[MSF_COMPATIBLE_PLAY]) { } + + void operator() (ModCommand &m) + { + const CHANNELINDEX curChn = chn; + chn++; + if(chn >= sndFile.GetNumChannels()) + { + chn = 0; + } + + if(m.IsPcNote()) + { + return; + } + const auto version = sndFile.m_dwLastSavedWithVersion; + const auto modType = sndFile.GetType(); + + if(modType == MOD_TYPE_S3M) + { + // Out-of-range global volume commands should be ignored in S3M. Fixed in OpenMPT 1.19 (r831). + // So for tracks made with older versions of OpenMPT, we limit invalid global volume commands. + if(version < MPT_V("1.19.00.00") && m.command == CMD_GLOBALVOLUME) + { + LimitMax(m.param, ModCommand::PARAM(64)); + } + } + + else if(modType & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + if(version < MPT_V("1.17.03.02") || + (!compatPlay && version < MPT_V("1.20.00.00"))) + { + if(m.command == CMD_GLOBALVOLUME) + { + // Out-of-range global volume commands should be ignored in IT. + // OpenMPT 1.17.03.02 fixed this in compatible mode, OpenMPT 1.20 fixes it in normal mode as well. + // So for tracks made with older versions than OpenMPT 1.17.03.02 or tracks made with 1.17.03.02 <= version < 1.20, we limit invalid global volume commands. + LimitMax(m.param, ModCommand::PARAM(128)); + } + + // SC0 and SD0 should be interpreted as SC1 and SD1 in IT files. + // OpenMPT 1.17.03.02 fixed this in compatible mode, OpenMPT 1.20 fixes it in normal mode as well. + else if(m.command == CMD_S3MCMDEX) + { + if(m.param == 0xC0) + { + m.command = CMD_NONE; + m.note = NOTE_NOTECUT; + } else if(m.param == 0xD0) + { + m.command = CMD_NONE; + } + } + } + + // In the IT format, slide commands with both nibbles set should be ignored. + // For note volume slides, OpenMPT 1.18 fixes this in compatible mode, OpenMPT 1.20 fixes this in normal mode as well. + const bool noteVolSlide = + (version < MPT_V("1.18.00.00") || + (!compatPlay && version < MPT_V("1.20.00.00"))) + && + (m.command == CMD_VOLUMESLIDE || m.command == CMD_VIBRATOVOL || m.command == CMD_TONEPORTAVOL || m.command == CMD_PANNINGSLIDE); + + // OpenMPT 1.20 also fixes this for global volume and channel volume slides. + const bool chanVolSlide = + (version < MPT_V("1.20.00.00")) + && + (m.command == CMD_GLOBALVOLSLIDE || m.command == CMD_CHANNELVOLSLIDE); + + if(noteVolSlide || chanVolSlide) + { + if((m.param & 0x0F) != 0x00 && (m.param & 0x0F) != 0x0F && (m.param & 0xF0) != 0x00 && (m.param & 0xF0) != 0xF0) + { + if(m.command == CMD_GLOBALVOLSLIDE) + m.param &= 0xF0; + else + m.param &= 0x0F; + } + } + + if(version < MPT_V("1.22.01.04") + && version != MPT_V("1.22.00.00")) // Ignore compatibility export + { + // OpenMPT 1.22.01.04 fixes illegal (out of range) instrument numbers; they should do nothing. In previous versions, they stopped the playing sample. + if(sndFile.GetNumInstruments() && m.instr > sndFile.GetNumInstruments() && !compatPlay) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = 0; + } + } + + // Command I11 accidentally behaved the same as command I00 with compatible IT tremor and old effects disabled + if(m.command == CMD_TREMOR && m.param == 0x11 && version < MPT_V("1.29.12.02") && sndFile.m_playBehaviour[kITTremor] && !sndFile.m_SongFlags[SONG_ITOLDEFFECTS]) + { + m.param = 0; + } + } + + else if(modType == MOD_TYPE_XM) + { + // Something made be believe that out-of-range global volume commands are ignored in XM + // just like they are ignored in IT, but apparently they are not. Aaaaaargh! + if(((version >= MPT_V("1.17.03.02") && compatPlay) || (version >= MPT_V("1.20.00.00"))) + && version < MPT_V("1.24.02.02") + && m.command == CMD_GLOBALVOLUME + && m.param > 64) + { + m.command = CMD_NONE; + } + + if(version < MPT_V("1.19.00.00") + || (!compatPlay && version < MPT_V("1.20.00.00"))) + { + if(m.command == CMD_OFFSET && m.volcmd == VOLCMD_TONEPORTAMENTO) + { + // If there are both a portamento and an offset effect, the portamento should be preferred in XM files. + // OpenMPT 1.19 fixed this in compatible mode, OpenMPT 1.20 fixes it in normal mode as well. + m.command = CMD_NONE; + } + } + + if(version < MPT_V("1.20.01.10") + && m.volcmd == VOLCMD_TONEPORTAMENTO && m.command == CMD_TONEPORTAMENTO + && (m.vol != 0 || compatPlay) && m.param != 0) + { + // Mx and 3xx on the same row does weird things in FT2: 3xx is completely ignored and the Mx parameter is doubled. Fixed in revision 1312 / OpenMPT 1.20.01.10 + // Previously the values were just added up, so let's fix this! + m.volcmd = VOLCMD_NONE; + const uint16 param = static_cast<uint16>(m.param) + static_cast<uint16>(m.vol << 4); + m.param = mpt::saturate_cast<ModCommand::PARAM>(param); + } + + if(version < MPT_V("1.22.07.09") + && m.command == CMD_SPEED && m.param == 0) + { + // OpenMPT can emulate FT2's F00 behaviour now. + m.command = CMD_NONE; + } + } + + if(version < MPT_V("1.20.00.00")) + { + // Pattern Delay fixes + + const bool fixS6x = (m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0x60); + // We also fix X6x commands in hacked XM files, since they are treated identically to the S6x command in IT/S3M files. + // We don't treat them in files made with OpenMPT 1.18+ that have compatible play enabled, though, since they are ignored there anyway. + const bool fixX6x = (m.command == CMD_XFINEPORTAUPDOWN && (m.param & 0xF0) == 0x60 + && (!(compatPlay && modType == MOD_TYPE_XM) || version < MPT_V("1.18.00.00"))); + + if(fixS6x || fixX6x) + { + // OpenMPT 1.20 fixes multiple fine pattern delays on the same row. Previously, only the last command was considered, + // but all commands should be added up. Since Scream Tracker 3 itself doesn't support S6x, we also use Impulse Tracker's behaviour here, + // since we can assume that most S3Ms that make use of S6x were composed with Impulse Tracker. + for(ModCommand *fixCmd = (&m) - curChn; fixCmd < &m; fixCmd++) + { + if((fixCmd->command == CMD_S3MCMDEX || fixCmd->command == CMD_XFINEPORTAUPDOWN) && (fixCmd->param & 0xF0) == 0x60) + { + fixCmd->command = CMD_NONE; + } + } + } + + if(m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0xE0) + { + // OpenMPT 1.20 fixes multiple pattern delays on the same row. Previously, only the *last* command was considered, + // but Scream Tracker 3 and Impulse Tracker only consider the *first* command. + for(ModCommand *fixCmd = (&m) - curChn; fixCmd < &m; fixCmd++) + { + if(fixCmd->command == CMD_S3MCMDEX && (fixCmd->param & 0xF0) == 0xE0) + { + fixCmd->command = CMD_NONE; + } + } + } + } + + if(m.volcmd == VOLCMD_VIBRATODEPTH + && version < MPT_V("1.27.00.37") + && version != MPT_V("1.27.00.00")) + { + // Fix handling of double vibrato commands - previously only one of them was applied at a time + if(m.command == CMD_VIBRATOVOL && m.vol > 0) + { + m.command = CMD_VOLUMESLIDE; + } else if((m.command == CMD_VIBRATO || m.command == CMD_FINEVIBRATO) && (m.param & 0x0F) == 0) + { + m.command = CMD_VIBRATO; + m.param |= (m.vol & 0x0F); + m.volcmd = VOLCMD_NONE; + } else if(m.command == CMD_VIBRATO || m.command == CMD_VIBRATOVOL || m.command == CMD_FINEVIBRATO) + { + m.volcmd = VOLCMD_NONE; + } + } + + // Volume column offset in IT/XM is bad, mkay? + if(modType != MOD_TYPE_MPT && m.volcmd == VOLCMD_OFFSET && m.command == CMD_NONE) + { + m.command = CMD_OFFSET; + m.param = m.vol << 3; + m.volcmd = VOLCMD_NONE; + } + + // Previously CMD_OFFSET simply overrode VOLCMD_OFFSET, now they work together as a combined command + if(m.volcmd == VOLCMD_OFFSET && m.command == CMD_OFFSET && version < MPT_V("1.30.00.14")) + { + if(m.param != 0 || m.vol == 0) + m.volcmd = VOLCMD_NONE; + else + m.command = CMD_NONE; + } + } + + const CSoundFile &sndFile; + CHANNELINDEX chn = 0; + const bool compatPlay; +}; + + +void CSoundFile::UpgradeModule() +{ + if(m_dwLastSavedWithVersion < MPT_V("1.17.02.46") && m_dwLastSavedWithVersion != MPT_V("1.17.00.00")) + { + // Compatible playback mode didn't exist in earlier versions, so definitely disable it. + m_playBehaviour.reset(MSF_COMPATIBLE_PLAY); + } + + const bool compatModeIT = m_playBehaviour[MSF_COMPATIBLE_PLAY] && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)); + const bool compatModeXM = m_playBehaviour[MSF_COMPATIBLE_PLAY] && GetType() == MOD_TYPE_XM; + + if(m_dwLastSavedWithVersion < MPT_V("1.20.00.00")) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) if(Instruments[i] != nullptr) + { + ModInstrument *ins = Instruments[i]; + // Previously, volume swing values ranged from 0 to 64. They should reach from 0 to 100 instead. + ins->nVolSwing = static_cast<uint8>(std::min(static_cast<uint32>(ins->nVolSwing * 100 / 64), uint32(100))); + + if(!compatModeIT || m_dwLastSavedWithVersion < MPT_V("1.18.00.00")) + { + // Previously, Pitch/Pan Separation was only half depth (plot twist: it was actually only quarter depth). + // This was corrected in compatible mode in OpenMPT 1.18, and in OpenMPT 1.20 it is corrected in normal mode as well. + ins->nPPS = (ins->nPPS + (ins->nPPS >= 0 ? 1 : -1)) / 2; + } + + if(!compatModeIT || m_dwLastSavedWithVersion < MPT_V("1.17.03.02")) + { + // IT compatibility 24. Short envelope loops + // Previously, the pitch / filter envelope loop handling was broken, the loop was shortened by a tick (like in XM). + // This was corrected in compatible mode in OpenMPT 1.17.03.02, and in OpenMPT 1.20 it is corrected in normal mode as well. + ins->GetEnvelope(ENV_PITCH).Convert(MOD_TYPE_XM, GetType()); + } + + if(m_dwLastSavedWithVersion >= MPT_V("1.17.00.00") && m_dwLastSavedWithVersion < MPT_V("1.17.02.50")) + { + // If there are any plugins that can receive volume commands, enable volume bug emulation. + if(ins->nMixPlug && ins->HasValidMIDIChannel()) + { + m_playBehaviour.set(kMIDICCBugEmulation); + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.17.02.50") && (ins->nVolSwing | ins->nPanSwing | ins->nCutSwing | ins->nResSwing)) + { + // If there are any instruments with random variation, enable the old random variation behaviour. + m_playBehaviour.set(kMPTOldSwingBehaviour); + break; + } + } + + if((GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && (m_dwLastSavedWithVersion < MPT_V("1.17.03.02") || !compatModeIT)) + { + // In the IT format, a sweep value of 0 shouldn't apply vibrato at all. Previously, a value of 0 was treated as "no sweep". + // In OpenMPT 1.17.03.02, this was corrected in compatible mode, in OpenMPT 1.20 it is corrected in normal mode as well, + // so we have to fix the setting while loading. + for(SAMPLEINDEX i = 1; i <= GetNumSamples(); i++) + { + if(Samples[i].nVibSweep == 0 && (Samples[i].nVibDepth | Samples[i].nVibRate)) + { + Samples[i].nVibSweep = 255; + } + } + } + + // Fix old nasty broken (non-standard) MIDI configs in files. + m_MidiCfg.UpgradeMacros(); + } + + if(m_dwLastSavedWithVersion < MPT_V("1.20.02.10") + && m_dwLastSavedWithVersion != MPT_V("1.20.00.00") + && (GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT))) + { + bool instrPlugs = false; + // Old pitch wheel commands were closest to sample pitch bend commands if the PWD is 13. + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(Instruments[i] != nullptr && Instruments[i]->nMidiChannel != MidiNoChannel) + { + Instruments[i]->midiPWD = 13; + instrPlugs = true; + } + } + if(instrPlugs) + { + m_playBehaviour.set(kOldMIDIPitchBends); + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.22.03.12") + && m_dwLastSavedWithVersion != MPT_V("1.22.00.00") + && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + && (m_playBehaviour[MSF_COMPATIBLE_PLAY] || m_playBehaviour[kMPTOldSwingBehaviour])) + { + // The "correct" pan swing implementation did nothing if the instrument also had a pan envelope. + // If there's a pan envelope, disable pan swing for such modules. + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(Instruments[i] != nullptr && Instruments[i]->nPanSwing != 0 && Instruments[i]->PanEnv.dwFlags[ENV_ENABLED]) + { + Instruments[i]->nPanSwing = 0; + } + } + } + +#ifndef NO_PLUGINS + if(m_dwLastSavedWithVersion < MPT_V("1.22.07.01")) + { + // Convert ANSI plugin path names to UTF-8 (irrelevant in probably 99% of all cases anyway, I think I've never seen a VST plugin with a non-ASCII file name) + for(auto &plugin : m_MixPlugins) + { +#if defined(MODPLUG_TRACKER) + const std::string name = mpt::ToCharset(mpt::Charset::UTF8, mpt::Charset::Locale, plugin.Info.szLibraryName); +#else + const std::string name = mpt::ToCharset(mpt::Charset::UTF8, mpt::Charset::Windows1252, plugin.Info.szLibraryName); +#endif + plugin.Info.szLibraryName = name; + } + } +#endif // NO_PLUGINS + + // Starting from OpenMPT 1.22.07.19, FT2-style panning was applied in compatible mix mode. + // Starting from OpenMPT 1.23.01.04, FT2-style panning has its own mix mode instead. + if(GetType() == MOD_TYPE_XM) + { + if(m_dwLastSavedWithVersion >= MPT_V("1.22.07.19") + && m_dwLastSavedWithVersion < MPT_V("1.23.01.04") + && GetMixLevels() == MixLevels::Compatible) + { + SetMixLevels(MixLevels::CompatibleFT2); + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.25.00.07") && m_dwLastSavedWithVersion != MPT_V("1.25.00.00")) + { + // Instrument plugins can now receive random volume variation. + // For old instruments, disable volume swing in case there was no sample associated. + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(Instruments[i] != nullptr && Instruments[i]->nVolSwing != 0 && Instruments[i]->nMidiChannel != MidiNoChannel) + { + bool hasSample = false; + for(auto smp : Instruments[i]->Keyboard) + { + if(smp != 0) + { + hasSample = true; + break; + } + } + if(!hasSample) + { + Instruments[i]->nVolSwing = 0; + } + } + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.26.00.00")) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) if(Instruments[i] != nullptr) + { + ModInstrument *ins = Instruments[i]; + // Even after fixing it in OpenMPT 1.18, instrument PPS was only half the depth. + ins->nPPS = (ins->nPPS + (ins->nPPS >= 0 ? 1 : -1)) / 2; + + // OpenMPT 1.18 fixed the depth of random pan in compatible mode. + // OpenMPT 1.26 fixes it in normal mode too. + if(!compatModeIT || m_dwLastSavedWithVersion < MPT_V("1.18.00.00")) + { + ins->nPanSwing = (ins->nPanSwing + 3) / 4u; + } + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.28.00.12")) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) if(Instruments[i] != nullptr) + { + if(Instruments[i]->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET) + { + m_playBehaviour.set(kLegacyReleaseNode); + break; + } + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.28.03.04")) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) if (Instruments[i] != nullptr) + { + if(Instruments[i]->pluginVolumeHandling == PLUGIN_VOLUMEHANDLING_MIDI || Instruments[i]->pluginVolumeHandling == PLUGIN_VOLUMEHANDLING_DRYWET) + { + m_playBehaviour.set(kMIDIVolumeOnNoteOffBug); + break; + } + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.30.00.54")) + { + for(SAMPLEINDEX i = 1; i <= GetNumSamples(); i++) + { + if(Samples[i].HasSampleData() && Samples[i].uFlags[CHN_PINGPONGLOOP | CHN_PINGPONGSUSTAIN]) + { + m_playBehaviour.set(kImprecisePingPongLoops); + break; + } + } + } + + Patterns.ForEachModCommand(UpgradePatternData(*this)); + + // Convert compatibility flags + // NOTE: Some of these version numbers are just approximations. + // Sometimes a quirk flag is shared by several code locations which might have been fixed at different times. + // Sometimes the quirk behaviour has been revised over time, in which case the first version that emulated the quirk enables it. + struct PlayBehaviourVersion + { + PlayBehaviour behaviour; + Version version; + }; + + if(compatModeIT && m_dwLastSavedWithVersion < MPT_V("1.26.00.00")) + { + // Pre-1.26: Detailed compatibility flags did not exist. + static constexpr PlayBehaviourVersion behaviours[] = + { + { kTempoClamp, MPT_V("1.17.03.02") }, + { kPerChannelGlobalVolSlide, MPT_V("1.17.03.02") }, + { kPanOverride, MPT_V("1.17.03.02") }, + { kITInstrWithoutNote, MPT_V("1.17.02.46") }, + { kITVolColFinePortamento, MPT_V("1.17.02.49") }, + { kITArpeggio, MPT_V("1.17.02.49") }, + { kITOutOfRangeDelay, MPT_V("1.17.02.49") }, + { kITPortaMemoryShare, MPT_V("1.17.02.49") }, + { kITPatternLoopTargetReset, MPT_V("1.17.02.49") }, + { kITFT2PatternLoop, MPT_V("1.17.02.49") }, + { kITPingPongNoReset, MPT_V("1.17.02.51") }, + { kITEnvelopeReset, MPT_V("1.17.02.51") }, + { kITClearOldNoteAfterCut, MPT_V("1.17.02.52") }, + { kITVibratoTremoloPanbrello, MPT_V("1.17.03.02") }, + { kITTremor, MPT_V("1.17.03.02") }, + { kITRetrigger, MPT_V("1.17.03.02") }, + { kITMultiSampleBehaviour, MPT_V("1.17.03.02") }, + { kITPortaTargetReached, MPT_V("1.17.03.02") }, + { kITPatternLoopBreak, MPT_V("1.17.03.02") }, + { kITOffset, MPT_V("1.17.03.02") }, + { kITSwingBehaviour, MPT_V("1.18.00.00") }, + { kITNNAReset, MPT_V("1.18.00.00") }, + { kITSCxStopsSample, MPT_V("1.18.00.01") }, + { kITEnvelopePositionHandling, MPT_V("1.18.01.00") }, + { kITPortamentoInstrument, MPT_V("1.19.00.01") }, + { kITPingPongMode, MPT_V("1.19.00.21") }, + { kITRealNoteMapping, MPT_V("1.19.00.30") }, + { kITHighOffsetNoRetrig, MPT_V("1.20.00.14") }, + { kITFilterBehaviour, MPT_V("1.20.00.35") }, + { kITNoSurroundPan, MPT_V("1.20.00.53") }, + { kITShortSampleRetrig, MPT_V("1.20.00.54") }, + { kITPortaNoNote, MPT_V("1.20.00.56") }, + { kRowDelayWithNoteDelay, MPT_V("1.20.00.76") }, + { kITFT2DontResetNoteOffOnPorta, MPT_V("1.20.02.06") }, + { kITVolColMemory, MPT_V("1.21.01.16") }, + { kITPortamentoSwapResetsPos, MPT_V("1.21.01.25") }, + { kITEmptyNoteMapSlot, MPT_V("1.21.01.25") }, + { kITFirstTickHandling, MPT_V("1.22.07.09") }, + { kITSampleAndHoldPanbrello, MPT_V("1.22.07.19") }, + { kITClearPortaTarget, MPT_V("1.23.04.03") }, + { kITPanbrelloHold, MPT_V("1.24.01.06") }, + { kITPanningReset, MPT_V("1.24.01.06") }, + { kITPatternLoopWithJumpsOld, MPT_V("1.25.00.19") }, + }; + + for(const auto &b : behaviours) + { + m_playBehaviour.set(b.behaviour, (m_dwLastSavedWithVersion >= b.version || m_dwLastSavedWithVersion == b.version.Masked(0xFFFF0000u))); + } + } else if(compatModeXM && m_dwLastSavedWithVersion < MPT_V("1.26.00.00")) + { + // Pre-1.26: Detailed compatibility flags did not exist. + static constexpr PlayBehaviourVersion behaviours[] = + { + { kTempoClamp, MPT_V("1.17.03.02") }, + { kPerChannelGlobalVolSlide, MPT_V("1.17.03.02") }, + { kPanOverride, MPT_V("1.17.03.02") }, + { kITFT2PatternLoop, MPT_V("1.17.03.02") }, + { kFT2Arpeggio, MPT_V("1.17.03.02") }, + { kFT2Retrigger, MPT_V("1.17.03.02") }, + { kFT2VolColVibrato, MPT_V("1.17.03.02") }, + { kFT2PortaNoNote, MPT_V("1.17.03.02") }, + { kFT2KeyOff, MPT_V("1.17.03.02") }, + { kFT2PanSlide, MPT_V("1.17.03.02") }, + { kFT2ST3OffsetOutOfRange, MPT_V("1.17.03.02") }, + { kFT2RestrictXCommand, MPT_V("1.18.00.00") }, + { kFT2RetrigWithNoteDelay, MPT_V("1.18.00.00") }, + { kFT2SetPanEnvPos, MPT_V("1.18.00.00") }, + { kFT2PortaIgnoreInstr, MPT_V("1.18.00.01") }, + { kFT2VolColMemory, MPT_V("1.18.01.00") }, + { kFT2LoopE60Restart, MPT_V("1.18.02.01") }, + { kFT2ProcessSilentChannels, MPT_V("1.18.02.01") }, + { kFT2ReloadSampleSettings, MPT_V("1.20.00.36") }, + { kFT2PortaDelay, MPT_V("1.20.00.40") }, + { kFT2Transpose, MPT_V("1.20.00.62") }, + { kFT2PatternLoopWithJumps, MPT_V("1.20.00.69") }, + { kFT2PortaTargetNoReset, MPT_V("1.20.00.69") }, + { kFT2EnvelopeEscape, MPT_V("1.20.00.77") }, + { kFT2Tremor, MPT_V("1.20.01.11") }, + { kFT2OutOfRangeDelay, MPT_V("1.20.02.02") }, + { kFT2Periods, MPT_V("1.22.03.01") }, + { kFT2PanWithDelayedNoteOff, MPT_V("1.22.03.02") }, + { kFT2VolColDelay, MPT_V("1.22.07.19") }, + { kFT2FinetunePrecision, MPT_V("1.22.07.19") }, + }; + + for(const auto &b : behaviours) + { + m_playBehaviour.set(b.behaviour, m_dwLastSavedWithVersion >= b.version); + } + } + + if(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + // The following behaviours were added in/after OpenMPT 1.26, so are not affected by the upgrade mechanism above. + static constexpr PlayBehaviourVersion behaviours[] = + { + { kITInstrWithNoteOff, MPT_V("1.26.00.01") }, + { kITMultiSampleInstrumentNumber, MPT_V("1.27.00.27") }, + { kITInstrWithNoteOffOldEffects, MPT_V("1.28.02.06") }, + { kITDoNotOverrideChannelPan, MPT_V("1.29.00.22") }, + { kITPatternLoopWithJumps, MPT_V("1.29.00.32") }, + { kITDCTBehaviour, MPT_V("1.29.00.57") }, + { kITPitchPanSeparation, MPT_V("1.30.00.53") }, + }; + + for(const auto &b : behaviours) + { + if(m_dwLastSavedWithVersion < b.version.Masked(0xFFFF0000u)) + m_playBehaviour.reset(b.behaviour); + // Full version information available, i.e. not compatibility-exported. + else if(m_dwLastSavedWithVersion > b.version.Masked(0xFFFF0000u) && m_dwLastSavedWithVersion < b.version) + m_playBehaviour.reset(b.behaviour); + } + } else if(GetType() == MOD_TYPE_XM) + { + // The following behaviours were added after OpenMPT 1.26, so are not affected by the upgrade mechanism above. + static constexpr PlayBehaviourVersion behaviours[] = + { + { kFT2NoteOffFlags, MPT_V("1.27.00.27") }, + { kRowDelayWithNoteDelay, MPT_V("1.27.00.37") }, + { kFT2MODTremoloRampWaveform, MPT_V("1.27.00.37") }, + { kFT2PortaUpDownMemory, MPT_V("1.27.00.37") }, + { kFT2PanSustainRelease, MPT_V("1.28.00.09") }, + { kFT2NoteDelayWithoutInstr, MPT_V("1.28.00.44") }, + { kITFT2DontResetNoteOffOnPorta, MPT_V("1.29.00.34") }, + { kFT2PortaResetDirection, MPT_V("1.30.00.40") }, + }; + + for(const auto &b : behaviours) + { + if(m_dwLastSavedWithVersion < b.version) + m_playBehaviour.reset(b.behaviour); + } + } else if(GetType() == MOD_TYPE_S3M) + { + // We do not store any of these flags in S3M files. + static constexpr PlayBehaviourVersion behaviours[] = + { + { kST3NoMutedChannels, MPT_V("1.18.00.00") }, + { kST3EffectMemory, MPT_V("1.20.00.00") }, + { kRowDelayWithNoteDelay, MPT_V("1.20.00.00") }, + { kST3PortaSampleChange, MPT_V("1.22.00.00") }, + { kST3VibratoMemory, MPT_V("1.26.00.00") }, + { kITPanbrelloHold, MPT_V("1.26.00.00") }, + { KST3PortaAfterArpeggio, MPT_V("1.27.00.00") }, + { kST3OffsetWithoutInstrument, MPT_V("1.28.00.00") }, + { kST3RetrigAfterNoteCut, MPT_V("1.29.00.00") }, + { kFT2ST3OffsetOutOfRange, MPT_V("1.29.00.00") }, + { kApplyUpperPeriodLimit, MPT_V("1.30.00.45") }, + }; + + for(const auto &b : behaviours) + { + if(m_dwLastSavedWithVersion < b.version) + m_playBehaviour.reset(b.behaviour); + } + } + + if(GetType() == MOD_TYPE_XM && m_dwLastSavedWithVersion < MPT_V("1.19.00.00")) + { + // This bug was introduced sometime between 1.18.03.00 and 1.19.01.00 + m_playBehaviour.set(kFT2NoteDelayWithoutInstr); + } + + if(m_dwLastSavedWithVersion >= MPT_V("1.27.00.27") && m_dwLastSavedWithVersion < MPT_V("1.27.00.49")) + { + // OpenMPT 1.27 inserted some IT/FT2 flags before the S3M flags that are never saved to files anyway, to keep the flag IDs a bit more compact. + // However, it was overlooked that these flags would still be read by OpenMPT 1.26 and thus S3M-specific behaviour would be enabled in IT/XM files. + // Hence, in OpenMPT 1.27.00.49 the flag IDs got remapped to no longer conflict with OpenMPT 1.26. + // Files made with the affected pre-release versions of OpenMPT 1.27 are upgraded here to use the new IDs. + for(int i = 0; i < 5; i++) + { + m_playBehaviour.set(kFT2NoteOffFlags + i, m_playBehaviour[kST3NoMutedChannels + i]); + m_playBehaviour.reset(kST3NoMutedChannels + i); + } + } + + if(m_dwLastSavedWithVersion < MPT_V("1.17.00.00")) + { + // MPT 1.16 has a maximum tempo of 255. + m_playBehaviour.set(kTempoClamp); + } else if(m_dwLastSavedWithVersion >= MPT_V("1.17.00.00") && m_dwLastSavedWithVersion <= MPT_V("1.20.01.03") && m_dwLastSavedWithVersion != MPT_V("1.20.00.00")) + { + // OpenMPT introduced some "fixes" that execute regular portamentos also at speed 1. + m_playBehaviour.set(kSlidesAtSpeed1); + } + + if(m_SongFlags[SONG_LINEARSLIDES]) + { + if(m_dwLastSavedWithVersion < MPT_V("1.24.00.00")) + { + // No frequency slides in Hz before OpenMPT 1.24 + m_playBehaviour.reset(kPeriodsAreHertz); + } else if(m_dwLastSavedWithVersion >= MPT_V("1.24.00.00") && m_dwLastSavedWithVersion < MPT_V("1.26.00.00") && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) + { + // Frequency slides were always in Hz rather than periods in this version range. + m_playBehaviour.set(kPeriodsAreHertz); + } + } else + { + if(m_dwLastSavedWithVersion < MPT_V("1.30.00.36") && m_dwLastSavedWithVersion != MPT_V("1.30.00.00")) + { + // No frequency slides in Hz before OpenMPT 1.30 + m_playBehaviour.reset(kPeriodsAreHertz); + } + } + + if(m_playBehaviour[kITEnvelopePositionHandling] + && m_dwLastSavedWithVersion >= MPT_V("1.23.01.02") && m_dwLastSavedWithVersion < MPT_V("1.28.00.43")) + { + // Bug that effectively clamped the release node to the sustain end + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) if(Instruments[i] != nullptr) + { + if(Instruments[i]->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET + && Instruments[i]->VolEnv.dwFlags[ENV_SUSTAIN] + && Instruments[i]->VolEnv.nReleaseNode > Instruments[i]->VolEnv.nSustainEnd) + { + m_playBehaviour.set(kReleaseNodePastSustainBug); + break; + } + } + } + + if(GetType() & (MOD_TYPE_MPT | MOD_TYPE_S3M)) + { + for(SAMPLEINDEX i = 1; i <= GetNumSamples(); i++) + { + if(Samples[i].uFlags[CHN_ADLIB]) + { + if(GetType() == MOD_TYPE_MPT && GetNumInstruments() && m_dwLastSavedWithVersion >= MPT_V("1.28.00.20") && m_dwLastSavedWithVersion <= MPT_V("1.29.00.55")) + m_playBehaviour.set(kOPLNoResetAtEnvelopeEnd); + if(m_dwLastSavedWithVersion <= MPT_V("1.30.00.34") && m_dwLastSavedWithVersion != MPT_V("1.30")) + m_playBehaviour.reset(kOPLNoteOffOnNoteChange); + if(GetType() == MOD_TYPE_S3M && m_dwLastSavedWithVersion < MPT_V("1.29")) + m_playBehaviour.set(kOPLRealRetrig); + else if(GetType() != MOD_TYPE_S3M) + m_playBehaviour.reset(kOPLRealRetrig); + break; + } + } + } + + if(m_dwLastSavedWithVersion >= MPT_V("1.27.00.42") && m_dwLastSavedWithVersion < MPT_V("1.30.00.46") && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + { + // The Flanger DMO plugin is almost identical to the Chorus... but only almost. + // The effect implementation was the same in OpenMPT 1.27-1.29, now it isn't anymore. + // As the old implementation continues to exist for the Chorus plugin, there is a legacy wrapper for the Flanger plugin. + for(auto &plugin : m_MixPlugins) + { + if(plugin.Info.dwPluginId1 == kDmoMagic && plugin.Info.dwPluginId2 == int32(0xEFCA3D92) && plugin.pluginData.size() == 32) + plugin.Info.szLibraryName = "Flanger (Legacy)"; + } + } + + if(m_dwLastSavedWithVersion >= MPT_V("1.27") && m_dwLastSavedWithVersion < MPT_V("1.30.06.00") && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + { + // Fix off-by-one delay length in older Echo DMO emulation + for(auto &plugin : m_MixPlugins) + { + if(plugin.Info.dwPluginId1 == kDmoMagic && plugin.Info.dwPluginId2 == int32(0xEF3E932C) && plugin.pluginData.size() == 24) + { + float32le leftDelay, rightDelay; + memcpy(&leftDelay, plugin.pluginData.data() + 12, 4); + memcpy(&rightDelay, plugin.pluginData.data() + 16, 4); + leftDelay = float32le{mpt::safe_clamp(((leftDelay * 2000.0f) - 1.0f) / 1999.0f, 0.0f, 1.0f)}; + rightDelay = float32le{mpt::safe_clamp(((rightDelay * 2000.0f) - 1.0f) / 1999.0f, 0.0f, 1.0f)}; + memcpy(plugin.pluginData.data() + 12, &leftDelay, 4); + memcpy(plugin.pluginData.data() + 16, &rightDelay, 4); + } + } + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.cpp new file mode 100644 index 00000000..eebe2c98 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.cpp @@ -0,0 +1,651 @@ +/* + * WAVTools.cpp + * ------------ + * Purpose: Definition of WAV file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "WAVTools.h" +#include "Tagging.h" +#include "../common/version.h" +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/io.hpp" +#include "mpt/io/io_virtual_wrapper.hpp" +#include "../common/mptFileIO.h" +#endif + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////// +// WAV Reading + + +WAVReader::WAVReader(FileReader &inputFile) : file(inputFile) +{ + file.Rewind(); + + RIFFHeader fileHeader; + codePage = 28591; // ISO 8859-1 + isDLS = false; + subFormat = 0; + mayBeCoolEdit16_8 = false; + if(!file.ReadStruct(fileHeader) + || (fileHeader.magic != RIFFHeader::idRIFF && fileHeader.magic != RIFFHeader::idLIST) + || (fileHeader.type != RIFFHeader::idWAVE && fileHeader.type != RIFFHeader::idwave)) + { + return; + } + + isDLS = (fileHeader.magic == RIFFHeader::idLIST); + + auto chunks = file.ReadChunks<RIFFChunk>(2); + + if(chunks.chunks.size() >= 4 + && chunks.chunks[1].GetHeader().GetID() == RIFFChunk::iddata + && chunks.chunks[1].GetHeader().GetLength() % 2u != 0 + && chunks.chunks[2].GetHeader().GetLength() == 0 + && chunks.chunks[3].GetHeader().GetID() == RIFFChunk::id____) + { + // Houston, we have a problem: Old versions of (Open)MPT didn't write RIFF padding bytes. -_- + // Luckily, the only RIFF chunk with an odd size those versions would ever write would be the "data" chunk + // (which contains the sample data), and its size is only odd iff the sample has an odd length and is in + // 8-Bit mono format. In all other cases, the sample size (and thus the chunk size) is even. + + // And we're even more lucky: The versions of (Open)MPT in question will always write a relatively small + // (smaller than 256 bytes) "smpl" chunk after the "data" chunk. This means that after an unpadded sample, + // we will always read "mpl?" (? being the length of the "smpl" chunk) as the next chunk magic. The first two + // 32-Bit members of the "smpl" chunk are always zero in our case, so we are going to read a chunk length of 0 + // next and the next chunk magic, which will always consist of four zero bytes. Hooray! We just checked for those + // four zero bytes and can be pretty confident that we should not have applied padding. + file.Seek(sizeof(RIFFHeader)); + chunks = file.ReadChunks<RIFFChunk>(1); + } + + // Read format chunk + FileReader formatChunk = chunks.GetChunk(RIFFChunk::idfmt_); + if(!formatChunk.ReadStruct(formatInfo)) + { + return; + } + if(formatInfo.format == WAVFormatChunk::fmtPCM && formatChunk.BytesLeft() == 4) + { + uint16 size = formatChunk.ReadIntLE<uint16>(); + uint16 value = formatChunk.ReadIntLE<uint16>(); + if(size == 2 && value == 1) + { + // May be Cool Edit 16.8 format. + // See SampleFormats.cpp for details. + mayBeCoolEdit16_8 = true; + } + } else if(formatInfo.format == WAVFormatChunk::fmtExtensible) + { + WAVFormatChunkExtension extFormat; + if(!formatChunk.ReadStruct(extFormat)) + { + return; + } + subFormat = static_cast<uint16>(mpt::UUID(extFormat.subFormat).GetData1()); + } + + // Read sample data + sampleData = chunks.GetChunk(RIFFChunk::iddata); + + if(!sampleData.IsValid()) + { + // The old IMA ADPCM loader code looked for the "pcm " chunk instead of the "data" chunk... + // Dunno why (Windows XP's audio recorder saves IMA ADPCM files with a "data" chunk), but we will just look for both. + sampleData = chunks.GetChunk(RIFFChunk::idpcm_); + } + + // "fact" chunk should contain sample length of compressed samples. + sampleLength = chunks.GetChunk(RIFFChunk::idfact).ReadUint32LE(); + + if((formatInfo.format != WAVFormatChunk::fmtIMA_ADPCM || sampleLength == 0) && GetSampleSize() != 0) + { + if((GetBlockAlign() == 0) || (GetBlockAlign() / GetNumChannels() >= 2 * GetSampleSize())) + { + // Some samples have an incorrect blockAlign / sample size set (e.g. it's 8 in SQUARE.WAV while it should be 1), so let's better not always trust this value. + // The idea here is, if block align is off by twice or more, it is unlikely to be describing sample padding inside the block. + // Ignore it in this case and calculate the length based on the single sample size and number of channels instead. + sampleLength = sampleData.GetLength() / GetSampleSize(); + } else + { + // Correct case (so that 20bit WAVEFORMATEX files work). + sampleLength = sampleData.GetLength() / GetBlockAlign(); + } + } + + // Determine string encoding + codePage = GetFileCodePage(chunks); + + // Check for loop points, texts, etc... + FindMetadataChunks(chunks); + + // DLS bank chunk + wsmpChunk = chunks.GetChunk(RIFFChunk::idwsmp); +} + + +void WAVReader::FindMetadataChunks(FileReader::ChunkList<RIFFChunk> &chunks) +{ + // Read sample loop points and other sampler information + smplChunk = chunks.GetChunk(RIFFChunk::idsmpl); + instChunk = chunks.GetChunk(RIFFChunk::idinst); + + // Read sample cues + cueChunk = chunks.GetChunk(RIFFChunk::idcue_); + + // Read text chunks + FileReader listChunk = chunks.GetChunk(RIFFChunk::idLIST); + if(listChunk.ReadMagic("INFO")) + { + infoChunk = listChunk.ReadChunks<RIFFChunk>(2); + } + + // Read MPT sample information + xtraChunk = chunks.GetChunk(RIFFChunk::idxtra); +} + + +uint16 WAVReader::GetFileCodePage(FileReader::ChunkList<RIFFChunk> &chunks) +{ + FileReader csetChunk = chunks.GetChunk(RIFFChunk::idCSET); + if(!csetChunk.IsValid()) + { + FileReader iSFT = infoChunk.GetChunk(RIFFChunk::idISFT); + if(iSFT.ReadMagic("OpenMPT")) + { + std::string versionString; + iSFT.ReadString<mpt::String::maybeNullTerminated>(versionString, iSFT.BytesLeft()); + versionString = mpt::trim(versionString); + Version version = Version::Parse(mpt::ToUnicode(mpt::Charset::ISO8859_1, versionString)); + if(version && version < MPT_V("1.28.00.02")) + { + return 1252; // mpt::Charset::Windows1252; // OpenMPT up to and including 1.28.00.01 wrote metadata in windows-1252 encoding + } else + { + return 28591; // mpt::Charset::ISO8859_1; // as per spec + } + } else + { + return 28591; // mpt::Charset::ISO8859_1; // as per spec + } + } + if(!csetChunk.CanRead(2)) + { + // chunk not parsable + return 28591; // mpt::Charset::ISO8859_1; + } + uint16 codepage = csetChunk.ReadUint16LE(); + return codepage; +} + + +void WAVReader::ApplySampleSettings(ModSample &sample, mpt::Charset sampleCharset, mpt::charbuf<MAX_SAMPLENAME> &sampleName) +{ + // Read sample name + FileReader textChunk = infoChunk.GetChunk(RIFFChunk::idINAM); + if(textChunk.IsValid()) + { + std::string sampleNameEncoded; + textChunk.ReadString<mpt::String::nullTerminated>(sampleNameEncoded, textChunk.GetLength()); + sampleName = mpt::ToCharset(sampleCharset, mpt::ToUnicode(codePage, mpt::Charset::Windows1252, sampleNameEncoded)); + } + if(isDLS) + { + // DLS sample -> sample filename + sample.filename = sampleName; + } + + // Read software name + const bool isOldMPT = infoChunk.GetChunk(RIFFChunk::idISFT).ReadMagic("Modplug Tracker"); + + // Convert loops + WAVSampleInfoChunk sampleInfo; + smplChunk.Rewind(); + if(smplChunk.ReadStruct(sampleInfo)) + { + WAVSampleLoop loopData; + if(sampleInfo.numLoops > 1 && smplChunk.ReadStruct(loopData)) + { + // First loop: Sustain loop + loopData.ApplyToSample(sample.nSustainStart, sample.nSustainEnd, sample.nLength, sample.uFlags, CHN_SUSTAINLOOP, CHN_PINGPONGSUSTAIN, isOldMPT); + } + // First loop (if only one loop is present) or second loop (if more than one loop is present): Normal sample loop + if(smplChunk.ReadStruct(loopData)) + { + loopData.ApplyToSample(sample.nLoopStart, sample.nLoopEnd, sample.nLength, sample.uFlags, CHN_LOOP, CHN_PINGPONGLOOP, isOldMPT); + } + //sample.Transpose((60 - sampleInfo.baseNote) / 12.0); + sample.rootNote = static_cast<uint8>(sampleInfo.baseNote); + if(sample.rootNote < 128) + sample.rootNote += NOTE_MIN; + else + sample.rootNote = NOTE_NONE; + sample.SanitizeLoops(); + } + + if(sample.rootNote == NOTE_NONE && instChunk.LengthIsAtLeast(sizeof(WAVInstrumentChunk))) + { + WAVInstrumentChunk inst; + instChunk.Rewind(); + if(instChunk.ReadStruct(inst)) + { + sample.rootNote = inst.unshiftedNote; + if(sample.rootNote < 128) + sample.rootNote += NOTE_MIN; + else + sample.rootNote = NOTE_NONE; + } + } + + // Read cue points + if(cueChunk.IsValid()) + { + uint32 numPoints = cueChunk.ReadUint32LE(); + LimitMax(numPoints, mpt::saturate_cast<uint32>(std::size(sample.cues))); + for(uint32 i = 0; i < numPoints; i++) + { + WAVCuePoint cuePoint; + cueChunk.ReadStruct(cuePoint); + sample.cues[i] = cuePoint.position; + } + std::fill(std::begin(sample.cues) + numPoints, std::end(sample.cues), MAX_SAMPLE_LENGTH); + } + + // Read MPT extra info + WAVExtraChunk mptInfo; + xtraChunk.Rewind(); + if(xtraChunk.ReadStruct(mptInfo)) + { + if(mptInfo.flags & WAVExtraChunk::setPanning) sample.uFlags.set(CHN_PANNING); + + sample.nPan = std::min(static_cast<uint16>(mptInfo.defaultPan), uint16(256)); + sample.nVolume = std::min(static_cast<uint16>(mptInfo.defaultVolume), uint16(256)); + sample.nGlobalVol = std::min(static_cast<uint16>(mptInfo.globalVolume), uint16(64)); + sample.nVibType = static_cast<VibratoType>(mptInfo.vibratoType.get()); + sample.nVibSweep = mptInfo.vibratoSweep; + sample.nVibDepth = mptInfo.vibratoDepth; + sample.nVibRate = mptInfo.vibratoRate; + + if(xtraChunk.CanRead(MAX_SAMPLENAME)) + { + // Name present (clipboard only) + // FIXME: When modules can have individual encoding in OpenMPT or when + // internal metadata gets converted to Unicode, we must adjust this to + // also specify encoding. + xtraChunk.ReadString<mpt::String::nullTerminated>(sampleName, MAX_SAMPLENAME); + xtraChunk.ReadString<mpt::String::nullTerminated>(sample.filename, xtraChunk.BytesLeft()); + } + } +} + + +// Apply WAV loop information to a mod sample. +void WAVSampleLoop::ApplyToSample(SmpLength &start, SmpLength &end, SmpLength sampleLength, SampleFlags &flags, ChannelFlags enableFlag, ChannelFlags bidiFlag, bool mptLoopFix) const +{ + if(loopEnd == 0) + { + // Some WAV files seem to have loops going from 0 to 0... We should ignore those. + return; + } + start = std::min(static_cast<SmpLength>(loopStart), sampleLength); + end = Clamp(static_cast<SmpLength>(loopEnd), start, sampleLength); + if(!mptLoopFix && end < sampleLength) + { + // RIFF loop end points are inclusive - old versions of MPT didn't consider this. + end++; + } + + flags.set(enableFlag); + if(loopType == loopBidi) + { + flags.set(bidiFlag); + } +} + + +// Convert internal loop information into a WAV loop. +void WAVSampleLoop::ConvertToWAV(SmpLength start, SmpLength end, bool bidi) +{ + identifier = 0; + loopType = bidi ? loopBidi : loopForward; + loopStart = mpt::saturate_cast<uint32>(start); + // Loop ends are *inclusive* in the RIFF standard, while they're *exclusive* in OpenMPT. + if(end > start) + { + loopEnd = mpt::saturate_cast<uint32>(end - 1); + } else + { + loopEnd = loopStart; + } + fraction = 0; + playCount = 0; +} + + +#ifndef MODPLUG_NO_FILESAVE + +/////////////////////////////////////////////////////////// +// WAV Writing + + +// Output to stream: Initialize with std::ostream*. +WAVWriter::WAVWriter(mpt::IO::OFileBase &stream) + : s(stream) +{ + // Skip file header for now + Seek(sizeof(RIFFHeader)); +} + + +WAVWriter::~WAVWriter() +{ + MPT_ASSERT(finalized); +} + + +// Finalize the file by closing the last open chunk and updating the file header. Returns total size of file. +std::size_t WAVWriter::Finalize() +{ + FinalizeChunk(); + + RIFFHeader fileHeader; + Clear(fileHeader); + fileHeader.magic = RIFFHeader::idRIFF; + fileHeader.length = static_cast<uint32>(totalSize - 8); + fileHeader.type = RIFFHeader::idWAVE; + + Seek(0); + Write(fileHeader); + finalized = true; + + return totalSize; +} + + +// Write a new chunk header to the file. +void WAVWriter::StartChunk(RIFFChunk::ChunkIdentifiers id) +{ + FinalizeChunk(); + + chunkStartPos = position; + chunkHeader.id = id; + Skip(sizeof(chunkHeader)); +} + + +// End current chunk by updating the chunk header and writing a padding byte if necessary. +void WAVWriter::FinalizeChunk() +{ + if(chunkStartPos != 0) + { + const std::size_t chunkSize = position - (chunkStartPos + sizeof(RIFFChunk)); + chunkHeader.length = mpt::saturate_cast<uint32>(chunkSize); + + std::size_t curPos = position; + Seek(chunkStartPos); + Write(chunkHeader); + + Seek(curPos); + if((chunkSize % 2u) != 0) + { + // Write padding + uint8 padding = 0; + Write(padding); + } + + chunkStartPos = 0; + } +} + + +// Seek to a position in file. +void WAVWriter::Seek(std::size_t pos) +{ + position = pos; + totalSize = std::max(totalSize, position); + mpt::IO::SeekAbsolute(s, pos); +} + + +// Write some data to the file. +void WAVWriter::Write(mpt::const_byte_span data) +{ + MPT_ASSERT(!finalized); + auto success = mpt::IO::WriteRaw(s, data); + MPT_ASSERT(success); // this assertion is useful to catch mis-calculation of required buffer size for pre-allocate in-memory file buffers (like in View_smp.cpp for clipboard) + if(!success) + { + return; + } + position += data.size(); + totalSize = std::max(totalSize, position); +} + + +void WAVWriter::WriteBeforeDirect() +{ + MPT_ASSERT(!finalized); +} + + +void WAVWriter::WriteAfterDirect(bool success, std::size_t count) +{ + MPT_ASSERT(success); // this assertion is useful to catch mis-calculation of required buffer size for pre-allocate in-memory file buffers (like in View_smp.cpp for clipboard) + if (!success) + { + return; + } + position += count; + totalSize = std::max(totalSize, position); +} + + +// Write the WAV format to the file. +void WAVWriter::WriteFormat(uint32 sampleRate, uint16 bitDepth, uint16 numChannels, WAVFormatChunk::SampleFormats encoding) +{ + StartChunk(RIFFChunk::idfmt_); + WAVFormatChunk wavFormat; + Clear(wavFormat); + + bool extensible = (numChannels > 2); + + wavFormat.format = static_cast<uint16>(extensible ? WAVFormatChunk::fmtExtensible : encoding); + wavFormat.numChannels = numChannels; + wavFormat.sampleRate = sampleRate; + wavFormat.blockAlign = (bitDepth * numChannels + 7) / 8; + wavFormat.byteRate = wavFormat.sampleRate * wavFormat.blockAlign; + wavFormat.bitsPerSample = bitDepth; + + Write(wavFormat); + + if(extensible) + { + WAVFormatChunkExtension extFormat; + Clear(extFormat); + extFormat.size = sizeof(WAVFormatChunkExtension) - sizeof(uint16); + extFormat.validBitsPerSample = bitDepth; + switch(numChannels) + { + case 1: + extFormat.channelMask = 0x0004; // FRONT_CENTER + break; + case 2: + extFormat.channelMask = 0x0003; // FRONT_LEFT | FRONT_RIGHT + break; + case 3: + extFormat.channelMask = 0x0103; // FRONT_LEFT | FRONT_RIGHT | BACK_CENTER + break; + case 4: + extFormat.channelMask = 0x0033; // FRONT_LEFT | FRONT_RIGHT | BACK_LEFT | BACK_RIGHT + break; + default: + extFormat.channelMask = 0; + break; + } + extFormat.subFormat = mpt::UUID(static_cast<uint16>(encoding), 0x0000, 0x0010, 0x800000AA00389B71ull); + Write(extFormat); + } +} + + +// Write text tags to the file. +void WAVWriter::WriteMetatags(const FileTags &tags) +{ + StartChunk(RIFFChunk::idCSET); + Write(mpt::as_le(uint16(65001))); // code page (UTF-8) + Write(mpt::as_le(uint16(0))); // country code (unset) + Write(mpt::as_le(uint16(0))); // language (unset) + Write(mpt::as_le(uint16(0))); // dialect (unset) + + StartChunk(RIFFChunk::idLIST); + const char info[] = { 'I', 'N', 'F', 'O' }; + Write(info); + + WriteTag(RIFFChunk::idINAM, tags.title); + WriteTag(RIFFChunk::idIART, tags.artist); + WriteTag(RIFFChunk::idIPRD, tags.album); + WriteTag(RIFFChunk::idICRD, tags.year); + WriteTag(RIFFChunk::idICMT, tags.comments); + WriteTag(RIFFChunk::idIGNR, tags.genre); + WriteTag(RIFFChunk::idTURL, tags.url); + WriteTag(RIFFChunk::idISFT, tags.encoder); + //WriteTag(RIFFChunk:: , tags.bpm); + WriteTag(RIFFChunk::idTRCK, tags.trackno); +} + + +// Write a single tag into a open idLIST chunk +void WAVWriter::WriteTag(RIFFChunk::ChunkIdentifiers id, const mpt::ustring &utext) +{ + std::string text = mpt::ToCharset(mpt::Charset::UTF8, utext); + text = text.substr(0, uint32_max - 1u); + if(!text.empty()) + { + const uint32 length = mpt::saturate_cast<uint32>(text.length() + 1); + + RIFFChunk chunk; + Clear(chunk); + chunk.id = static_cast<uint32>(id); + chunk.length = length; + Write(chunk); + Write(mpt::byte_cast<mpt::const_byte_span>(mpt::span(text.c_str(), length))); + + if((length % 2u) != 0) + { + uint8 padding = 0; + Write(padding); + } + } +} + + +// Write a sample loop information chunk to the file. +void WAVWriter::WriteLoopInformation(const ModSample &sample) +{ + if(!sample.uFlags[CHN_LOOP | CHN_SUSTAINLOOP] && !ModCommand::IsNote(sample.rootNote)) + { + return; + } + + StartChunk(RIFFChunk::idsmpl); + WAVSampleInfoChunk info; + + uint32 sampleRate = sample.nC5Speed; + if(sampleRate == 0) + { + sampleRate = ModSample::TransposeToFrequency(sample.RelativeTone, sample.nFineTune); + } + + info.ConvertToWAV(sampleRate, sample.rootNote); + + // Set up loops + WAVSampleLoop loops[2]; + Clear(loops); + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + loops[info.numLoops++].ConvertToWAV(sample.nSustainStart, sample.nSustainEnd, sample.uFlags[CHN_PINGPONGSUSTAIN]); + } + if(sample.uFlags[CHN_LOOP]) + { + loops[info.numLoops++].ConvertToWAV(sample.nLoopStart, sample.nLoopEnd, sample.uFlags[CHN_PINGPONGLOOP]); + } else if(sample.uFlags[CHN_SUSTAINLOOP]) + { + // Since there are no "loop types" to distinguish between sustain and normal loops, OpenMPT assumes + // that the first loop is a sustain loop if there are two loops. If we only want a sustain loop, + // we will have to write a second bogus loop. + loops[info.numLoops++].ConvertToWAV(0, 0, false); + } + + Write(info); + for(uint32 i = 0; i < info.numLoops; i++) + { + Write(loops[i]); + } +} + + +// Write a sample's cue points to the file. +void WAVWriter::WriteCueInformation(const ModSample &sample) +{ + uint32 numMarkers = 0; + for(const auto cue : sample.cues) + { + if(cue < sample.nLength) + numMarkers++; + } + + StartChunk(RIFFChunk::idcue_); + Write(mpt::as_le(numMarkers)); + uint32 i = 0; + for(const auto cue : sample.cues) + { + if(cue < sample.nLength) + { + WAVCuePoint cuePoint; + cuePoint.ConvertToWAV(i++, cue); + Write(cuePoint); + } + } +} + + +// Write MPT's sample information chunk to the file. +void WAVWriter::WriteExtraInformation(const ModSample &sample, MODTYPE modType, const char *sampleName) +{ + StartChunk(RIFFChunk::idxtra); + WAVExtraChunk mptInfo; + + mptInfo.ConvertToWAV(sample, modType); + Write(mptInfo); + + if(sampleName != nullptr) + { + // Write sample name (clipboard only) + + // FIXME: When modules can have individual encoding in OpenMPT or when + // internal metadata gets converted to Unicode, we must adjust this to + // also specify encoding. + + char name[MAX_SAMPLENAME]; + mpt::String::WriteBuf(mpt::String::nullTerminated, name) = sampleName; + Write(name); + + char filename[MAX_SAMPLEFILENAME]; + mpt::String::WriteBuf(mpt::String::nullTerminated, filename) = sample.filename; + Write(filename); + } +} + +#endif // MODPLUG_NO_FILESAVE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.h b/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.h new file mode 100644 index 00000000..380eaf80 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/WAVTools.h @@ -0,0 +1,406 @@ +/* + * WAVTools.h + * ---------- + * Purpose: Definition of WAV file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/uuid/uuid.hpp" + +#include "../common/FileReader.h" +#include "Loaders.h" + +#ifndef MODPLUG_NO_FILESAVE +#include "mpt/io/io.hpp" +#include "mpt/io/io_virtual_wrapper.hpp" +#endif + +OPENMPT_NAMESPACE_BEGIN + +struct FileTags; + +// RIFF header +struct RIFFHeader +{ + // 32-Bit chunk identifiers + enum RIFFMagic + { + idRIFF = MagicLE("RIFF"), // magic for WAV files + idLIST = MagicLE("LIST"), // magic for samples in DLS banks + idWAVE = MagicLE("WAVE"), // type for WAV files + idwave = MagicLE("wave"), // type for samples in DLS banks + }; + + uint32le magic; // RIFF (in WAV files) or LIST (in DLS banks) + uint32le length; // Size of the file, not including magic and length + uint32le type; // WAVE (in WAV files) or wave (in DLS banks) +}; + +MPT_BINARY_STRUCT(RIFFHeader, 12) + + +// General RIFF Chunk header +struct RIFFChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idfmt_ = MagicLE("fmt "), // Sample format information + iddata = MagicLE("data"), // Sample data + idpcm_ = MagicLE("pcm "), // IMA ADPCM samples + idfact = MagicLE("fact"), // Compressed samples + idsmpl = MagicLE("smpl"), // Sampler and loop information + idinst = MagicLE("inst"), // Instrument information + idLIST = MagicLE("LIST"), // List of chunks + idxtra = MagicLE("xtra"), // OpenMPT extra infomration + idcue_ = MagicLE("cue "), // Cue points + idwsmp = MagicLE("wsmp"), // DLS bank samples + idCSET = MagicLE("CSET"), // Character Set + id____ = 0x00000000, // Found when loading buggy MPT samples + + // Identifiers in "LIST" chunk + idINAM = MagicLE("INAM"), // title + idISFT = MagicLE("ISFT"), // software + idICOP = MagicLE("ICOP"), // copyright + idIART = MagicLE("IART"), // artist + idIPRD = MagicLE("IPRD"), // product (album) + idICMT = MagicLE("ICMT"), // comment + idIENG = MagicLE("IENG"), // engineer + idISBJ = MagicLE("ISBJ"), // subject + idIGNR = MagicLE("IGNR"), // genre + idICRD = MagicLE("ICRD"), // date created + + idYEAR = MagicLE("YEAR"), // year + idTRCK = MagicLE("TRCK"), // track number + idTURL = MagicLE("TURL"), // url + }; + + uint32le id; // See ChunkIdentifiers + uint32le length; // Chunk size without header + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(RIFFChunk, 8) + + +// Format Chunk +struct WAVFormatChunk +{ + // Sample formats + enum SampleFormats + { + fmtPCM = 1, + fmtFloat = 3, + fmtALaw = 6, + fmtULaw = 7, + fmtIMA_ADPCM = 17, + fmtMP3 = 85, + fmtExtensible = 0xFFFE, + }; + + uint16le format; // Sample format, see SampleFormats + uint16le numChannels; // Number of audio channels + uint32le sampleRate; // Sample rate in Hz + uint32le byteRate; // Bytes per second (should be freqHz * blockAlign) + uint16le blockAlign; // Size of a sample, in bytes (do not trust this value, it's incorrect in some files) + uint16le bitsPerSample; // Bits per sample +}; + +MPT_BINARY_STRUCT(WAVFormatChunk, 16) + + +// Extension of the WAVFormatChunk structure, used if format == formatExtensible +struct WAVFormatChunkExtension +{ + uint16le size; + uint16le validBitsPerSample; + uint32le channelMask; + mpt::GUIDms subFormat; +}; + +MPT_BINARY_STRUCT(WAVFormatChunkExtension, 24) + + +// Sample information chunk +struct WAVSampleInfoChunk +{ + uint32le manufacturer; + uint32le product; + uint32le samplePeriod; // 1000000000 / sampleRate + uint32le baseNote; // MIDI base note of sample + uint32le pitchFraction; + uint32le SMPTEFormat; + uint32le SMPTEOffset; + uint32le numLoops; // number of loops + uint32le samplerData; + + // Set up information + void ConvertToWAV(uint32 freq, uint8 rootNote) + { + manufacturer = 0; + product = 0; + samplePeriod = 1000000000 / freq; + if(rootNote != 0) + baseNote = rootNote - NOTE_MIN; + else + baseNote = NOTE_MIDDLEC - NOTE_MIN; + pitchFraction = 0; + SMPTEFormat = 0; + SMPTEOffset = 0; + numLoops = 0; + samplerData = 0; + } +}; + +MPT_BINARY_STRUCT(WAVSampleInfoChunk, 36) + + +// Sample loop information chunk (found after WAVSampleInfoChunk in "smpl" chunk) +struct WAVSampleLoop +{ + // Sample Loop Types + enum LoopType + { + loopForward = 0, + loopBidi = 1, + loopBackward = 2, + }; + + uint32le identifier; + uint32le loopType; // See LoopType + uint32le loopStart; // Loop start in samples + uint32le loopEnd; // Loop end in samples + uint32le fraction; + uint32le playCount; // Loop Count, 0 = infinite + + // Apply WAV loop information to a mod sample. + void ApplyToSample(SmpLength &start, SmpLength &end, SmpLength sampleLength, SampleFlags &flags, ChannelFlags enableFlag, ChannelFlags bidiFlag, bool mptLoopFix) const; + + // Convert internal loop information into a WAV loop. + void ConvertToWAV(SmpLength start, SmpLength end, bool bidi); +}; + +MPT_BINARY_STRUCT(WAVSampleLoop, 24) + + +// Instrument information chunk +struct WAVInstrumentChunk +{ + uint8 unshiftedNote; // Root key of sample, 0...127 + int8 finetune; // Finetune of root key in cents + int8 gain; // in dB + uint8 lowNote; // Note range, 0...127 + uint8 highNote; + uint8 lowVelocity; // Velocity range, 0...127 + uint8 highVelocity; +}; + +MPT_BINARY_STRUCT(WAVInstrumentChunk, 7) + + +// MPT-specific "xtra" chunk +struct WAVExtraChunk +{ + enum Flags + { + setPanning = 0x20, + }; + + uint32le flags; + uint16le defaultPan; + uint16le defaultVolume; + uint16le globalVolume; + uint16le reserved; + uint8le vibratoType; + uint8le vibratoSweep; + uint8le vibratoDepth; + uint8le vibratoRate; + + // Set up sample information + void ConvertToWAV(const ModSample &sample, MODTYPE modType) + { + if(sample.uFlags[CHN_PANNING]) + { + flags = WAVExtraChunk::setPanning; + } else + { + flags = 0; + } + + defaultPan = sample.nPan; + defaultVolume = sample.nVolume; + globalVolume = sample.nGlobalVol; + vibratoType = sample.nVibType; + vibratoSweep = sample.nVibSweep; + vibratoDepth = sample.nVibDepth; + vibratoRate = sample.nVibRate; + + if((modType & MOD_TYPE_XM) && (vibratoDepth | vibratoRate)) + { + // XM vibrato is upside down + vibratoSweep = 255 - vibratoSweep; + } + } +}; + +MPT_BINARY_STRUCT(WAVExtraChunk, 16) + + +// Sample cue point structure for the "cue " chunk +struct WAVCuePoint +{ + uint32le id; // Unique identification value + uint32le position; // Play order position + uint32le riffChunkID; // RIFF ID of corresponding data chunk + uint32le chunkStart; // Byte Offset of Data Chunk + uint32le blockStart; // Byte Offset to sample of First Channel + uint32le offset; // Byte Offset to sample byte of First Channel + + // Set up sample information + void ConvertToWAV(uint32 id_, SmpLength offset_) + { + id = id_; + position = offset_; + riffChunkID = static_cast<uint32>(RIFFChunk::iddata); + chunkStart = 0; // we use no Wave List Chunk (wavl) as we have only one data block, so this should be 0. + blockStart = 0; // ditto + offset = offset_; + } +}; + +MPT_BINARY_STRUCT(WAVCuePoint, 24) + + +class WAVReader +{ +protected: + FileReader file; + FileReader sampleData, smplChunk, instChunk, xtraChunk, wsmpChunk, cueChunk; + FileReader::ChunkList<RIFFChunk> infoChunk; + + FileReader::off_t sampleLength; + WAVFormatChunk formatInfo; + uint16 subFormat; + uint16 codePage; + bool isDLS; + bool mayBeCoolEdit16_8; + + uint16 GetFileCodePage(FileReader::ChunkList<RIFFChunk> &chunks); + +public: + WAVReader(FileReader &inputFile); + + bool IsValid() const { return sampleData.IsValid(); } + + void FindMetadataChunks(FileReader::ChunkList<RIFFChunk> &chunks); + + // Self-explanatory getters. + WAVFormatChunk::SampleFormats GetSampleFormat() const { return IsExtensibleFormat() ? static_cast<WAVFormatChunk::SampleFormats>(subFormat) : static_cast<WAVFormatChunk::SampleFormats>(formatInfo.format.get()); } + uint16 GetNumChannels() const { return formatInfo.numChannels; } + uint16 GetBitsPerSample() const { return formatInfo.bitsPerSample; } + uint32 GetSampleRate() const { return formatInfo.sampleRate; } + uint16 GetBlockAlign() const { return formatInfo.blockAlign; } + FileReader GetSampleData() const { return sampleData; } + FileReader GetWsmpChunk() const { return wsmpChunk; } + bool IsExtensibleFormat() const { return formatInfo.format == WAVFormatChunk::fmtExtensible; } + bool MayBeCoolEdit16_8() const { return mayBeCoolEdit16_8; } + + // Get size of a single sample point, in bytes. + uint16 GetSampleSize() const { return static_cast<uint16>(((static_cast<uint32>(GetNumChannels()) * static_cast<uint32>(GetBitsPerSample())) + 7) / 8); } + + // Get sample length (in samples) + SmpLength GetSampleLength() const { return mpt::saturate_cast<SmpLength>(sampleLength); } + + // Apply sample settings from file (loop points, MPT extra settings, ...) to a sample. + void ApplySampleSettings(ModSample &sample, mpt::Charset sampleCharset, mpt::charbuf<MAX_SAMPLENAME> &sampleName); +}; + + +#ifndef MODPLUG_NO_FILESAVE + +class WAVWriter +{ +protected: + // Output stream + mpt::IO::OFileBase &s; + + // Cursor position + std::size_t position = 0; + // Total number of bytes written to file / memory + std::size_t totalSize = 0; + + // Currently written chunk + std::size_t chunkStartPos = 0; + RIFFChunk chunkHeader; + bool finalized = false; + +public: + // Output to stream + WAVWriter(mpt::IO::OFileBase &stream); + ~WAVWriter(); + + // Finalize the file by closing the last open chunk and updating the file header. Returns total size of file. + std::size_t Finalize(); + // Begin writing a new chunk to the file. + void StartChunk(RIFFChunk::ChunkIdentifiers id); + + // Skip some bytes... For example after writing sample data. + void Skip(size_t numBytes) { Seek(position + numBytes); } + // Get position in file (not counting any changes done to the file from outside this class, i.e. through GetFile()) + std::size_t GetPosition() const { return position; } + + // Write some data to the file. + template<typename T> + void Write(const T &data) + { + Write(mpt::as_raw_memory(data)); + } + + // Write a buffer to the file. + void Write(mpt::const_byte_span data); + + // Use before writing raw data directly to the underlying stream s + void WriteBeforeDirect(); + // Use after writing raw data directly to the underlying stream s + void WriteAfterDirect(bool success, std::size_t count); + + // Write the WAV format to the file. + void WriteFormat(uint32 sampleRate, uint16 bitDepth, uint16 numChannels, WAVFormatChunk::SampleFormats encoding); + // Write text tags to the file. + void WriteMetatags(const FileTags &tags); + // Write a sample loop information chunk to the file. + void WriteLoopInformation(const ModSample &sample); + // Write a sample's cue points to the file. + void WriteCueInformation(const ModSample &sample); + // Write MPT's sample information chunk to the file. + void WriteExtraInformation(const ModSample &sample, MODTYPE modType, const char *sampleName = nullptr); + +protected: + // Seek to a position in file. + void Seek(std::size_t pos); + // End current chunk by updating the chunk header and writing a padding byte if necessary. + void FinalizeChunk(); + + // Write a single tag into a open idLIST chunk + void WriteTag(RIFFChunk::ChunkIdentifiers id, const mpt::ustring &utext); +}; + +#endif // MODPLUG_NO_FILESAVE + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.cpp new file mode 100644 index 00000000..256889ba --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.cpp @@ -0,0 +1,99 @@ +/* + * WindowedFIR.cpp + * --------------- + * Purpose: FIR resampling code + * Notes : Original code from modplug-xmms + * Authors: OpenMPT Devs + * ModPlug-XMMS Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "WindowedFIR.h" +#include "mpt/base/numbers.hpp" +#include <cmath> + +OPENMPT_NAMESPACE_BEGIN + +double CWindowedFIR::coef( int _PCnr, double _POfs, double _PCut, int _PWidth, int _PType ) //float _PPos, float _PFc, int _PLen ) +{ + const double epsilon = 1e-8; + const double _LWidthM1 = _PWidth - 1; + const double _LWidthM1Half = 0.5 * _LWidthM1; + const double _LPosU = (_PCnr - _POfs); + const double _LPIdl = (2.0 * mpt::numbers::pi) / _LWidthM1; + double _LPos = _LPosU - _LWidthM1Half; + double _LWc, _LSi; + if(std::abs(_LPos) < epsilon) + { + _LWc = 1.0; + _LSi = _PCut; + } else + { + switch(_PType) + { + case WFIR_HANN: + _LWc = 0.50 - 0.50 * std::cos(_LPIdl * _LPosU); + break; + case WFIR_HAMMING: + _LWc = 0.54 - 0.46 * std::cos(_LPIdl * _LPosU); + break; + case WFIR_BLACKMANEXACT: + _LWc = 0.42 - 0.50 * std::cos(_LPIdl * _LPosU) + 0.08 * std::cos(2.0 * _LPIdl * _LPosU); + break; + case WFIR_BLACKMAN3T61: + _LWc = 0.44959 - 0.49364 * std::cos(_LPIdl * _LPosU) + 0.05677 * std::cos(2.0 * _LPIdl * _LPosU); + break; + case WFIR_BLACKMAN3T67: + _LWc = 0.42323 - 0.49755 * std::cos(_LPIdl * _LPosU) + 0.07922 * std::cos(2.0 * _LPIdl * _LPosU); + break; + case WFIR_BLACKMAN4T92: // blackman harris + _LWc = 0.35875 - 0.48829 * std::cos(_LPIdl * _LPosU) + 0.14128 * std::cos(2.0 * _LPIdl * _LPosU) - 0.01168 * std::cos(3.0 * _LPIdl * _LPosU); + break; + case WFIR_BLACKMAN4T74: + _LWc = 0.40217 - 0.49703 * std::cos(_LPIdl * _LPosU) + 0.09392 * std::cos(2.0 * _LPIdl * _LPosU) - 0.00183 * std::cos(3.0 * _LPIdl * _LPosU); + break; + case WFIR_KAISER4T: // kaiser-bessel, alpha~7.5 + _LWc = 0.40243 - 0.49804 * std::cos(_LPIdl * _LPosU) + 0.09831 * std::cos(2.0 * _LPIdl * _LPosU) - 0.00122 * std::cos(3.0 * _LPIdl * _LPosU); + break; + default: + _LWc = 1.0; + break; + } + _LPos *= mpt::numbers::pi; + _LSi = std::sin(_PCut * _LPos) / _LPos; + } + return (_LWc * _LSi); +} + +void CWindowedFIR::InitTable(double WFIRCutoff, uint8 WFIRType) +{ + const double _LPcllen = (double)(1 << WFIR_FRACBITS); // number of precalculated lines for 0..1 (-1..0) + const double _LNorm = 1.0 / (2.0 * _LPcllen); + const double _LCut = WFIRCutoff; + for(int _LPcl = 0; _LPcl < WFIR_LUTLEN; _LPcl++) + { + double _LGain = 0.0, _LCoefs[WFIR_WIDTH]; + const double _LOfs = (_LPcl - _LPcllen) * _LNorm; + const int _LIdx = _LPcl << WFIR_LOG2WIDTH; + for(int _LCc = 0; _LCc < WFIR_WIDTH; _LCc++) + { + _LGain += (_LCoefs[_LCc] = coef(_LCc, _LOfs, _LCut, WFIR_WIDTH, WFIRType)); + } + _LGain = 1.0 / _LGain; + for(int _LCc = 0; _LCc < WFIR_WIDTH; _LCc++) + { +#ifdef MPT_INTMIXER + double _LCoef = std::floor(0.5 + WFIR_QUANTSCALE * _LCoefs[_LCc] * _LGain); + lut[_LIdx + _LCc] = (signed short)((_LCoef < -WFIR_QUANTSCALE) ? -WFIR_QUANTSCALE : ((_LCoef > WFIR_QUANTSCALE) ? WFIR_QUANTSCALE : _LCoef)); +#else + double _LCoef = _LCoefs[_LCc] * _LGain; + lut[_LIdx + _LCc] = (float)_LCoef; +#endif // MPT_INTMIXER + } + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.h b/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.h new file mode 100644 index 00000000..ed324a33 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/WindowedFIR.h @@ -0,0 +1,82 @@ +/* + * WindowedFIR.h + * ------------- + * Purpose: FIR resampling code + * Notes : (currently none) + * Authors: OpenMPT Devs + * ModPlug-XMMS Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Mixer.h" + +OPENMPT_NAMESPACE_BEGIN + +/* + ------------------------------------------------------------------------------------------------ + fir interpolation doc, + (derived from "an engineer's guide to fir digital filters", n.j. loy) + + calculate coefficients for ideal lowpass filter (with cutoff = fc in 0..1 (mapped to 0..nyquist)) + c[-N..N] = (i==0) ? fc : sin(fc*pi*i)/(pi*i) + + then apply selected window to coefficients + c[-N..N] *= w(0..N) + with n in 2*N and w(n) being a window function (see loy) + + then calculate gain and scale filter coefs to have unity gain. + ------------------------------------------------------------------------------------------------ +*/ + +#ifdef MPT_INTMIXER +// quantizer scale of window coefs - only required for integer mixing +inline constexpr int WFIR_QUANTBITS = 15; +inline constexpr double WFIR_QUANTSCALE = 1 << WFIR_QUANTBITS; +inline constexpr int WFIR_8SHIFT = (WFIR_QUANTBITS - 8); +inline constexpr int WFIR_16BITSHIFT = (WFIR_QUANTBITS); +using WFIR_TYPE = int16; +#else +using WFIR_TYPE = mixsample_t; +#endif // INTMIXER +// log2(number)-1 of precalculated taps range is [4..12] +inline constexpr int WFIR_FRACBITS = 12; //10 +inline constexpr int WFIR_LUTLEN = ((1 << (WFIR_FRACBITS + 1)) + 1); +// number of samples in window +inline constexpr int WFIR_LOG2WIDTH = 3; +inline constexpr int WFIR_WIDTH = (1 << WFIR_LOG2WIDTH); +// cutoff (1.0 == pi/2) +// wfir type +enum WFIRType +{ + WFIR_HANN = 0, // Hann + WFIR_HAMMING = 1, // Hamming + WFIR_BLACKMANEXACT = 2, // Blackman Exact + WFIR_BLACKMAN3T61 = 3, // Blackman 3-Tap 61 + WFIR_BLACKMAN3T67 = 4, // Blackman 3-Tap 67 + WFIR_BLACKMAN4T92 = 5, // Blackman-Harris + WFIR_BLACKMAN4T74 = 6, // Blackman 4-Tap 74 + WFIR_KAISER4T = 7, // Kaiser a=7.5 +}; + + +// fir interpolation +inline constexpr int WFIR_FRACSHIFT = (16 - (WFIR_FRACBITS + 1 + WFIR_LOG2WIDTH)); +inline constexpr int WFIR_FRACMASK = ((((1 << (17 - WFIR_FRACSHIFT)) - 1) & ~(WFIR_WIDTH - 1))); +inline constexpr int WFIR_FRACHALVE = (1 << (16 - (WFIR_FRACBITS + 2))); + +class CWindowedFIR +{ +private: + double coef(int,double,double,int,int); + +public: + void InitTable(double WFIRCutoff, uint8 WFIRType); + WFIR_TYPE lut[WFIR_LUTLEN * WFIR_WIDTH]; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.cpp new file mode 100644 index 00000000..ddb79653 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.cpp @@ -0,0 +1,431 @@ +/* + * XMTools.cpp + * ----------- + * Purpose: Definition of XM file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" +#include "XMTools.h" +#include "Sndfile.h" +#include "../common/version.h" +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +// Convert OpenMPT's internal envelope representation to XM envelope data. +void XMInstrument::ConvertEnvelopeToXM(const InstrumentEnvelope &mptEnv, uint8le &numPoints, uint8le &flags, uint8le &sustain, uint8le &loopStart, uint8le &loopEnd, EnvType env) +{ + numPoints = static_cast<uint8>(std::min(std::size_t(12), static_cast<std::size_t>(mptEnv.size()))); + + // Envelope Data + for(uint8 i = 0; i < numPoints; i++) + { + switch(env) + { + case EnvTypeVol: + volEnv[i * 2] = std::min(mptEnv[i].tick, uint16_max); + volEnv[i * 2 + 1] = std::min(mptEnv[i].value, uint8(64)); + break; + case EnvTypePan: + panEnv[i * 2] = std::min(mptEnv[i].tick, uint16_max); + panEnv[i * 2 + 1] = std::min(mptEnv[i].value, uint8(63)); + break; + } + } + + // Envelope Flags + if(mptEnv.dwFlags[ENV_ENABLED]) flags |= XMInstrument::envEnabled; + if(mptEnv.dwFlags[ENV_SUSTAIN]) flags |= XMInstrument::envSustain; + if(mptEnv.dwFlags[ENV_LOOP]) flags |= XMInstrument::envLoop; + + // Envelope Loops + sustain = std::min(uint8(12), mptEnv.nSustainStart); + loopStart = std::min(uint8(12), mptEnv.nLoopStart); + loopEnd = std::min(uint8(12), mptEnv.nLoopEnd); + +} + + +// Convert OpenMPT's internal sample representation to an XMInstrument. +uint16 XMInstrument::ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport) +{ + MemsetZero(*this); + + // FFF is maximum in the FT2 GUI, but it can also accept other values. MilkyTracker just allows 0...4095 and 32767 ("cut") + volFade = static_cast<uint16>(std::min(mptIns.nFadeOut, uint32(32767))); + + // Convert envelopes + ConvertEnvelopeToXM(mptIns.VolEnv, volPoints, volFlags, volSustain, volLoopStart, volLoopEnd, EnvTypeVol); + ConvertEnvelopeToXM(mptIns.PanEnv, panPoints, panFlags, panSustain, panLoopStart, panLoopEnd, EnvTypePan); + + // Create sample assignment table + auto sampleList = GetSampleList(mptIns, compatibilityExport); + for(std::size_t i = 0; i < std::size(sampleMap); i++) + { + if(mptIns.Keyboard[i + 12] > 0) + { + auto sample = std::find(sampleList.begin(), sampleList.end(), mptIns.Keyboard[i + 12]); + if(sample != sampleList.end()) + { + // Yep, we want to export this sample. + sampleMap[i] = static_cast<uint8>(sample - sampleList.begin()); + } + } + } + + if(mptIns.nMidiChannel != MidiNoChannel) + { + midiEnabled = 1; + midiChannel = (mptIns.nMidiChannel != MidiMappedChannel ? (mptIns.nMidiChannel - MidiFirstChannel) : 0); + } + midiProgram = (mptIns.nMidiProgram != 0 ? mptIns.nMidiProgram - 1 : 0); + pitchWheelRange = std::min(mptIns.midiPWD, int8(36)); + + return static_cast<uint16>(sampleList.size()); +} + + +// Get a list of samples that should be written to the file. +std::vector<SAMPLEINDEX> XMInstrument::GetSampleList(const ModInstrument &mptIns, bool compatibilityExport) const +{ + std::vector<SAMPLEINDEX> sampleList; // List of samples associated with this instrument + std::vector<bool> addedToList; // Which samples did we already add to the sample list? + + uint8 numSamples = 0; + for(std::size_t i = 0; i < std::size(sampleMap); i++) + { + const SAMPLEINDEX smp = mptIns.Keyboard[i + 12]; + if(smp > 0) + { + if(smp > addedToList.size()) + { + addedToList.resize(smp, false); + } + + if(!addedToList[smp - 1] && numSamples < (compatibilityExport ? 16 : 32)) + { + // We haven't considered this sample yet. + addedToList[smp - 1] = true; + numSamples++; + sampleList.push_back(smp); + } + } + } + return sampleList; +} + + +// Convert XM envelope data to an OpenMPT's internal envelope representation. +void XMInstrument::ConvertEnvelopeToMPT(InstrumentEnvelope &mptEnv, uint8 numPoints, uint8 flags, uint8 sustain, uint8 loopStart, uint8 loopEnd, EnvType env) const +{ + mptEnv.resize(std::min(numPoints, uint8(12))); + + // Envelope Data + for(uint32 i = 0; i < mptEnv.size(); i++) + { + switch(env) + { + case EnvTypeVol: + mptEnv[i].tick = volEnv[i * 2]; + mptEnv[i].value = static_cast<EnvelopeNode::value_t>(volEnv[i * 2 + 1]); + break; + case EnvTypePan: + mptEnv[i].tick = panEnv[i * 2]; + mptEnv[i].value = static_cast<EnvelopeNode::value_t>(panEnv[i * 2 + 1]); + break; + } + + if(i > 0 && mptEnv[i].tick < mptEnv[i - 1].tick && !(mptEnv[i].tick & 0xFF00)) + { + // libmikmod code says: "Some broken XM editing program will only save the low byte of the position + // value. Try to compensate by adding the missing high byte." + // Note: MPT 1.07's XI instrument saver omitted the high byte of envelope nodes. + // This might be the source for some broken envelopes in IT and XM files. + mptEnv[i].tick |= mptEnv[i - 1].tick & 0xFF00; + if(mptEnv[i].tick < mptEnv[i - 1].tick) + mptEnv[i].tick += 0x100; + } + } + + // Envelope Flags + mptEnv.dwFlags.reset(); + if((flags & XMInstrument::envEnabled) != 0 && !mptEnv.empty()) mptEnv.dwFlags.set(ENV_ENABLED); + + // Envelope Loops + if(sustain < 12) + { + if((flags & XMInstrument::envSustain) != 0) mptEnv.dwFlags.set(ENV_SUSTAIN); + mptEnv.nSustainStart = mptEnv.nSustainEnd = sustain; + } + + if(loopEnd < 12 && loopEnd >= loopStart) + { + if((flags & XMInstrument::envLoop) != 0) mptEnv.dwFlags.set(ENV_LOOP); + mptEnv.nLoopStart = loopStart; + mptEnv.nLoopEnd = loopEnd; + } +} + + +// Convert an XMInstrument to OpenMPT's internal instrument representation. +void XMInstrument::ConvertToMPT(ModInstrument &mptIns) const +{ + mptIns.nFadeOut = volFade; + + // Convert envelopes + ConvertEnvelopeToMPT(mptIns.VolEnv, volPoints, volFlags, volSustain, volLoopStart, volLoopEnd, EnvTypeVol); + ConvertEnvelopeToMPT(mptIns.PanEnv, panPoints, panFlags, panSustain, panLoopStart, panLoopEnd, EnvTypePan); + + // Create sample assignment table + for(std::size_t i = 0; i < std::size(sampleMap); i++) + { + mptIns.Keyboard[i + 12] = sampleMap[i]; + } + + if(midiEnabled) + { + mptIns.nMidiChannel = midiChannel + MidiFirstChannel; + Limit(mptIns.nMidiChannel, uint8(MidiFirstChannel), uint8(MidiLastChannel)); + mptIns.nMidiProgram = static_cast<uint8>(std::min(static_cast<uint16>(midiProgram), uint16(127)) + 1); + } + mptIns.midiPWD = static_cast<int8>(pitchWheelRange); +} + + +// Apply auto-vibrato settings from sample to file. +void XMInstrument::ApplyAutoVibratoToXM(const ModSample &mptSmp, MODTYPE fromType) +{ + vibType = mptSmp.nVibType; + vibSweep = mptSmp.nVibSweep; + vibDepth = mptSmp.nVibDepth; + vibRate = mptSmp.nVibRate; + + if((vibDepth | vibRate) != 0 && !(fromType & MOD_TYPE_XM)) + { + if(mptSmp.nVibSweep != 0) + vibSweep = mpt::saturate_cast<decltype(vibSweep)::base_type>(Util::muldivr_unsigned(mptSmp.nVibDepth, 256, mptSmp.nVibSweep)); + else + vibSweep = 255; + } +} + + +// Apply auto-vibrato settings from file to a sample. +void XMInstrument::ApplyAutoVibratoToMPT(ModSample &mptSmp) const +{ + mptSmp.nVibType = static_cast<VibratoType>(vibType.get()); + mptSmp.nVibSweep = vibSweep; + mptSmp.nVibDepth = vibDepth; + mptSmp.nVibRate = vibRate; +} + + +// Write stuff to the header that's always necessary (also for empty instruments) +void XMInstrumentHeader::Finalise() +{ + size = sizeof(XMInstrumentHeader); + if(numSamples > 0) + { + sampleHeaderSize = sizeof(XMSample); + } else + { + // TODO: FT2 completely ignores MIDI settings (and also the less important stuff) if not at least one (empty) sample is assigned to this instrument! + size -= sizeof(XMInstrument); + sampleHeaderSize = 0; + } +} + + +// Convert OpenMPT's internal sample representation to an XMInstrumentHeader. +void XMInstrumentHeader::ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport) +{ + numSamples = instrument.ConvertToXM(mptIns, compatibilityExport); + mpt::String::WriteBuf(mpt::String::spacePadded, name) = mptIns.name; + + type = mptIns.nMidiProgram; // If FT2 writes crap here, we can do so, too! (we probably shouldn't, though. This is just for backwards compatibility with old MPT versions.) +} + + +// Convert an XMInstrumentHeader to OpenMPT's internal instrument representation. +void XMInstrumentHeader::ConvertToMPT(ModInstrument &mptIns) const +{ + instrument.ConvertToMPT(mptIns); + + // Create sample assignment table + for(std::size_t i = 0; i < std::size(instrument.sampleMap); i++) + { + if(instrument.sampleMap[i] < numSamples) + { + mptIns.Keyboard[i + 12] = instrument.sampleMap[i]; + } else + { + mptIns.Keyboard[i + 12] = 0; + } + } + + mptIns.name = mpt::String::ReadBuf(mpt::String::spacePadded, name); + + // Old MPT backwards compatibility + if(!instrument.midiEnabled) + { + mptIns.nMidiProgram = type; + } +} + + +// Convert OpenMPT's internal sample representation to an XIInstrumentHeader. +void XIInstrumentHeader::ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport) +{ + numSamples = instrument.ConvertToXM(mptIns, compatibilityExport); + + memcpy(signature, "Extended Instrument: ", 21); + mpt::String::WriteBuf(mpt::String::spacePadded, name) = mptIns.name; + eof = 0x1A; + + const std::string openMptTrackerName = mpt::ToCharset(mpt::Charset::CP437, Version::Current().GetOpenMPTVersionString()); + mpt::String::WriteBuf(mpt::String::spacePadded, trackerName) = openMptTrackerName; + + version = 0x102; +} + + +// Convert an XIInstrumentHeader to OpenMPT's internal instrument representation. +void XIInstrumentHeader::ConvertToMPT(ModInstrument &mptIns) const +{ + instrument.ConvertToMPT(mptIns); + + // Fix sample assignment table + for(std::size_t i = 12; i < std::size(instrument.sampleMap) + 12; i++) + { + if(mptIns.Keyboard[i] >= numSamples) + { + mptIns.Keyboard[i] = 0; + } + } + + mptIns.name = mpt::String::ReadBuf(mpt::String::spacePadded, name); +} + + +// Convert OpenMPT's internal sample representation to an XMSample. +void XMSample::ConvertToXM(const ModSample &mptSmp, MODTYPE fromType, bool compatibilityExport) +{ + MemsetZero(*this); + + // Volume / Panning + vol = static_cast<uint8>(std::min(mptSmp.nVolume / 4u, 64u)); + pan = static_cast<uint8>(std::min(mptSmp.nPan, uint16(255))); + + // Sample Frequency + if((fromType & (MOD_TYPE_MOD | MOD_TYPE_XM))) + { + finetune = mptSmp.nFineTune; + relnote = mptSmp.RelativeTone; + } else + { + std::tie(relnote, finetune) = ModSample::FrequencyToTranspose(mptSmp.nC5Speed); + } + + flags = 0; + if(mptSmp.uFlags[CHN_PINGPONGLOOP]) + flags |= XMSample::sampleBidiLoop; + else if(mptSmp.uFlags[CHN_LOOP]) + flags |= XMSample::sampleLoop; + + // Sample Length and Loops + length = mpt::saturate_cast<uint32>(mptSmp.nLength); + loopStart = mpt::saturate_cast<uint32>(mptSmp.nLoopStart); + loopLength = mpt::saturate_cast<uint32>(mptSmp.nLoopEnd - mptSmp.nLoopStart); + + if(mptSmp.uFlags[CHN_16BIT]) + { + flags |= XMSample::sample16Bit; + length *= 2; + loopStart *= 2; + loopLength *= 2; + } + + if(mptSmp.uFlags[CHN_STEREO] && !compatibilityExport) + { + flags |= XMSample::sampleStereo; + length *= 2; + loopStart *= 2; + loopLength *= 2; + } +} + + +// Convert an XMSample to OpenMPT's internal sample representation. +void XMSample::ConvertToMPT(ModSample &mptSmp) const +{ + mptSmp.Initialize(MOD_TYPE_XM); + + // Volume + mptSmp.nVolume = vol * 4; + LimitMax(mptSmp.nVolume, uint16(256)); + + // Panning + mptSmp.nPan = pan; + mptSmp.uFlags = CHN_PANNING; + + // Sample Frequency + mptSmp.nFineTune = finetune; + mptSmp.RelativeTone = relnote; + + // Sample Length and Loops + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = mptSmp.nLoopStart + loopLength; + + if((flags & XMSample::sample16Bit)) + { + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + + if((flags & XMSample::sampleStereo)) + { + mptSmp.nLength /= 2; + mptSmp.nLoopStart /= 2; + mptSmp.nLoopEnd /= 2; + } + + if((flags & (XMSample::sampleLoop | XMSample::sampleBidiLoop)) && mptSmp.nLoopEnd > mptSmp.nLoopStart) + { + mptSmp.uFlags.set(CHN_LOOP); + if((flags & XMSample::sampleBidiLoop)) + { + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + } + } + + mptSmp.filename = ""; +} + + +// Retrieve the internal sample format flags for this instrument. +SampleIO XMSample::GetSampleFormat() const +{ + if(reserved == sampleADPCM && !(flags & (XMSample::sample16Bit | XMSample::sampleStereo))) + { + // MODPlugin :( + return SampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::ADPCM); + } + + return SampleIO( + (flags & XMSample::sample16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + (flags & XMSample::sampleStereo) ? SampleIO::stereoSplit : SampleIO::mono, + SampleIO::littleEndian, + SampleIO::deltaPCM); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.h b/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.h new file mode 100644 index 00000000..5369c7a7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/XMTools.h @@ -0,0 +1,191 @@ +/* + * XMTools.h + * --------- + * Purpose: Definition of XM file structures and helper functions + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// XM File Header +struct XMFileHeader +{ + enum XMHeaderFlags + { + linearSlides = 0x01, + extendedFilterRange = 0x1000, + }; + + char signature[17]; // "Extended Module: " + char songName[20]; // Song Name, not null-terminated (any nulls are treated as spaces) + uint8le eof; // DOS EOF Character (0x1A) + char trackerName[20]; // Software that was used to create the XM file + uint16le version; // File version (1.02 - 1.04 are supported) + uint32le size; // Header Size + uint16le orders; // Number of Orders + uint16le restartPos; // Restart Position + uint16le channels; // Number of Channels + uint16le patterns; // Number of Patterns + uint16le instruments; // Number of Unstruments + uint16le flags; // Song Flags + uint16le speed; // Default Speed + uint16le tempo; // Default Tempo +}; + +MPT_BINARY_STRUCT(XMFileHeader, 80) + + +// XM Instrument Data +struct XMInstrument +{ + // Envelope Flags + enum XMEnvelopeFlags + { + envEnabled = 0x01, + envSustain = 0x02, + envLoop = 0x04, + }; + + uint8le sampleMap[96]; // Note -> Sample assignment + uint16le volEnv[24]; // Volume envelope nodes / values (0...64) + uint16le panEnv[24]; // Panning envelope nodes / values (0...63) + uint8le volPoints; // Volume envelope length + uint8le panPoints; // Panning envelope length + uint8le volSustain; // Volume envelope sustain point + uint8le volLoopStart; // Volume envelope loop start point + uint8le volLoopEnd; // Volume envelope loop end point + uint8le panSustain; // Panning envelope sustain point + uint8le panLoopStart; // Panning envelope loop start point + uint8le panLoopEnd; // Panning envelope loop end point + uint8le volFlags; // Volume envelope flags + uint8le panFlags; // Panning envelope flags + uint8le vibType; // Sample Auto-Vibrato Type + uint8le vibSweep; // Sample Auto-Vibrato Sweep + uint8le vibDepth; // Sample Auto-Vibrato Depth + uint8le vibRate; // Sample Auto-Vibrato Rate + uint16le volFade; // Volume Fade-Out + uint8le midiEnabled; // MIDI Out Enabled (0 / 1) + uint8le midiChannel; // MIDI Channel (0...15) + uint16le midiProgram; // MIDI Program (0...127) + uint16le pitchWheelRange; // MIDI Pitch Wheel Range (0...36 halftones) + uint8le muteComputer; // Mute instrument if MIDI is enabled (0 / 1) + uint8le reserved[15]; // Reserved + + enum EnvType + { + EnvTypeVol, + EnvTypePan, + }; + // Convert OpenMPT's internal envelope representation to XM envelope data. + void ConvertEnvelopeToXM(const InstrumentEnvelope &mptEnv, uint8le &numPoints, uint8le &flags, uint8le &sustain, uint8le &loopStart, uint8le &loopEnd, EnvType env); + // Convert XM envelope data to an OpenMPT's internal envelope representation. + void ConvertEnvelopeToMPT(InstrumentEnvelope &mptEnv, uint8 numPoints, uint8 flags, uint8 sustain, uint8 loopStart, uint8 loopEnd, EnvType env) const; + + // Convert OpenMPT's internal sample representation to an XMInstrument. + uint16 ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport); + // Convert an XMInstrument to OpenMPT's internal instrument representation. + void ConvertToMPT(ModInstrument &mptIns) const; + // Apply auto-vibrato settings from sample to file. + void ApplyAutoVibratoToXM(const ModSample &mptSmp, MODTYPE fromType); + // Apply auto-vibrato settings from file to a sample. + void ApplyAutoVibratoToMPT(ModSample &mptSmp) const; + + // Get a list of samples that should be written to the file. + std::vector<SAMPLEINDEX> GetSampleList(const ModInstrument &mptIns, bool compatibilityExport) const; +}; + +MPT_BINARY_STRUCT(XMInstrument, 230) + + +// XM Instrument Header +struct XMInstrumentHeader +{ + uint32le size; // Size of XMInstrumentHeader + XMInstrument + char name[22]; // Instrument Name, not null-terminated (any nulls are treated as spaces) + uint8le type; // Instrument Type (Apparently FT2 writes some crap here, but it's the same crap for all instruments of the same module!) + uint16le numSamples; // Number of Samples associated with instrument + uint32le sampleHeaderSize; // Size of XMSample + XMInstrument instrument; + + // Write stuff to the header that's always necessary (also for empty instruments) + void Finalise(); + + // Convert OpenMPT's internal sample representation to an XMInstrument. + void ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport); + // Convert an XMInstrument to OpenMPT's internal instrument representation. + void ConvertToMPT(ModInstrument &mptIns) const; +}; + +MPT_BINARY_STRUCT(XMInstrumentHeader, 263) + + +// XI Instrument Header +struct XIInstrumentHeader +{ + enum + { + fileVersion = 0x102, + }; + + char signature[21]; // "Extended Instrument: " + char name[22]; // Instrument Name, not null-terminated (any nulls are treated as spaces) + uint8le eof; // DOS EOF Character (0x1A) + char trackerName[20]; // Software that was used to create the XI file + uint16le version; // File Version (1.02) + XMInstrument instrument; + uint16le numSamples; // Number of embedded sample headers + samples + + // Convert OpenMPT's internal sample representation to an XIInstrumentHeader. + void ConvertToXM(const ModInstrument &mptIns, bool compatibilityExport); + // Convert an XIInstrumentHeader to OpenMPT's internal instrument representation. + void ConvertToMPT(ModInstrument &mptIns) const; +}; + +MPT_BINARY_STRUCT(XIInstrumentHeader, 298) + + +// XM Sample Header +struct XMSample +{ + enum XMSampleFlags + { + sampleLoop = 0x01, + sampleBidiLoop = 0x02, + sample16Bit = 0x10, + sampleStereo = 0x20, + + sampleADPCM = 0xAD, // MODPlugin :( + }; + + uint32le length; // Sample Length (in bytes) + uint32le loopStart; // Loop Start (in bytes) + uint32le loopLength; // Loop Length (in bytes) + uint8le vol; // Default Volume + int8le finetune; // Sample Finetune + uint8le flags; // Sample Flags + uint8le pan; // Sample Panning + int8le relnote; // Sample Transpose + uint8le reserved; // Reserved (abused for ModPlug's ADPCM compression) + char name[22]; // Sample Name, not null-terminated (any nulls are treated as spaces) + + // Convert OpenMPT's internal sample representation to an XMSample. + void ConvertToXM(const ModSample &mptSmp, MODTYPE fromType, bool compatibilityExport); + // Convert an XMSample to OpenMPT's internal sample representation. + void ConvertToMPT(ModSample &mptSmp) const; + // Retrieve the internal sample format flags for this instrument. + SampleIO GetSampleFormat() const; +}; + +MPT_BINARY_STRUCT(XMSample, 40) + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/load_j2b.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/load_j2b.cpp new file mode 100644 index 00000000..90ca009e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/load_j2b.cpp @@ -0,0 +1,1076 @@ +/* + * load_j2b.cpp + * ------------ + * Purpose: RIFF AM and RIFF AMFF (Galaxy Sound System) module loader + * Notes : J2B is a compressed variant of RIFF AM and RIFF AMFF files used in Jazz Jackrabbit 2. + * It seems like no other game used the AM(FF) format. + * RIFF AM is the newer version of the format, generally following the RIFF "standard" closely. + * Authors: Johannes Schultz (OpenMPT port, reverse engineering + loader implementation of the instrument format) + * kode54 (foo_dumb - this is almost a complete port of his code, thanks) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Loaders.h" + +#include "mpt/io/base.hpp" + +#if defined(MPT_WITH_ZLIB) +#include <zlib.h> +#elif defined(MPT_WITH_MINIZ) +#include <miniz/miniz.h> +#endif + + +#ifdef MPT_ALL_LOGGING +#define J2B_LOG +#endif + + +OPENMPT_NAMESPACE_BEGIN + + +// First off, a nice vibrato translation LUT. +static constexpr VibratoType j2bAutoVibratoTrans[] = +{ + VIB_SINE, VIB_SQUARE, VIB_RAMP_UP, VIB_RAMP_DOWN, VIB_RANDOM, +}; + + +// header for compressed j2b files +struct J2BFileHeader +{ + // Magic Bytes + // 32-Bit J2B header identifiers + enum : uint32 { + magicDEADBEAF = 0xAFBEADDEu, + magicDEADBABE = 0xBEBAADDEu + }; + + char signature[4]; // MUSE + uint32le deadbeaf; // 0xDEADBEAF (AM) or 0xDEADBABE (AMFF) + uint32le fileLength; // complete filesize + uint32le crc32; // checksum of the compressed data block + uint32le packedLength; // length of the compressed data block + uint32le unpackedLength; // length of the decompressed module +}; + +MPT_BINARY_STRUCT(J2BFileHeader, 24) + + +// AM(FF) stuff + +struct AMFFRiffChunk +{ + // 32-Bit chunk identifiers + enum ChunkIdentifiers + { + idRIFF = MagicLE("RIFF"), + idAMFF = MagicLE("AMFF"), + idAM__ = MagicLE("AM "), + idMAIN = MagicLE("MAIN"), + idINIT = MagicLE("INIT"), + idORDR = MagicLE("ORDR"), + idPATT = MagicLE("PATT"), + idINST = MagicLE("INST"), + idSAMP = MagicLE("SAMP"), + idAI__ = MagicLE("AI "), + idAS__ = MagicLE("AS "), + }; + + uint32le id; // See ChunkIdentifiers + uint32le length; // Chunk size without header + + size_t GetLength() const + { + return length; + } + + ChunkIdentifiers GetID() const + { + return static_cast<ChunkIdentifiers>(id.get()); + } +}; + +MPT_BINARY_STRUCT(AMFFRiffChunk, 8) + + +// This header is used for both AM's "INIT" as well as AMFF's "MAIN" chunk +struct AMFFMainChunk +{ + // Main Chunk flags + enum MainFlags + { + amigaSlides = 0x01, + }; + + char songname[64]; + uint8le flags; + uint8le channels; + uint8le speed; + uint8le tempo; + uint16le minPeriod; // 16x Amiga periods, but we should ignore them - otherwise some high notes in Medivo.j2b won't sound correct. + uint16le maxPeriod; // Ditto + uint8le globalvolume; +}; + +MPT_BINARY_STRUCT(AMFFMainChunk, 73) + + +// AMFF instrument envelope (old format) +struct AMFFEnvelope +{ + // Envelope flags (also used for RIFF AM) + enum EnvelopeFlags + { + envEnabled = 0x01, + envSustain = 0x02, + envLoop = 0x04, + }; + + struct EnvPoint + { + uint16le tick; + uint8le value; // 0...64 + }; + + uint8le envFlags; // high nibble = pan env flags, low nibble = vol env flags (both nibbles work the same way) + uint8le envNumPoints; // high nibble = pan env length, low nibble = vol env length + uint8le envSustainPoints; // you guessed it... high nibble = pan env sustain point, low nibble = vol env sustain point + uint8le envLoopStarts; // I guess you know the pattern now. + uint8le envLoopEnds; // same here. + EnvPoint volEnv[10]; + EnvPoint panEnv[10]; + + // Convert weird envelope data to OpenMPT's internal format. + void ConvertEnvelope(uint8 flags, uint8 numPoints, uint8 sustainPoint, uint8 loopStart, uint8 loopEnd, const EnvPoint (&points)[10], InstrumentEnvelope &mptEnv) const + { + // The buggy mod2j2b converter will actually NOT limit this to 10 points if the envelope is longer. + mptEnv.resize(std::min(numPoints, static_cast<uint8>(10))); + + mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint; + + mptEnv.nLoopStart = loopStart; + mptEnv.nLoopEnd = loopEnd; + + for(uint32 i = 0; i < mptEnv.size(); i++) + { + mptEnv[i].tick = points[i].tick >> 4; + if(i == 0) + mptEnv[0].tick = 0; + else if(mptEnv[i].tick < mptEnv[i - 1].tick) + mptEnv[i].tick = mptEnv[i - 1].tick + 1; + + mptEnv[i].value = Clamp<uint8, uint8>(points[i].value, 0, 64); + } + + mptEnv.dwFlags.set(ENV_ENABLED, (flags & AMFFEnvelope::envEnabled) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & AMFFEnvelope::envSustain) && mptEnv.nSustainStart <= mptEnv.size()); + mptEnv.dwFlags.set(ENV_LOOP, (flags & AMFFEnvelope::envLoop) && mptEnv.nLoopStart <= mptEnv.nLoopEnd && mptEnv.nLoopStart <= mptEnv.size()); + } + + void ConvertToMPT(ModInstrument &mptIns) const + { + // interleaved envelope data... meh. gotta split it up here and decode it separately. + // note: mod2j2b is BUGGY and always writes ($original_num_points & 0x0F) in the header, + // but just has room for 10 envelope points. That means that long (>= 16 points) + // envelopes are cut off, and envelopes have to be trimmed to 10 points, even if + // the header claims that they are longer. + // For XM files the number of points also appears to be off by one, + // but luckily there are no official J2Bs using envelopes anyway. + ConvertEnvelope(envFlags & 0x0F, envNumPoints & 0x0F, envSustainPoints & 0x0F, envLoopStarts & 0x0F, envLoopEnds & 0x0F, volEnv, mptIns.VolEnv); + ConvertEnvelope(envFlags >> 4, envNumPoints >> 4, envSustainPoints >> 4, envLoopStarts >> 4, envLoopEnds >> 4, panEnv, mptIns.PanEnv); + } +}; + +MPT_BINARY_STRUCT(AMFFEnvelope::EnvPoint, 3) +MPT_BINARY_STRUCT(AMFFEnvelope, 65) + + +// AMFF instrument header (old format) +struct AMFFInstrumentHeader +{ + uint8le unknown; // 0x00 + uint8le index; // actual instrument number + char name[28]; + uint8le numSamples; + uint8le sampleMap[120]; + uint8le vibratoType; + uint16le vibratoSweep; + uint16le vibratoDepth; + uint16le vibratoRate; + AMFFEnvelope envelopes; + uint16le fadeout; + + // Convert instrument data to OpenMPT's internal format. + void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX baseSample) + { + mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, name); + + static_assert(mpt::array_size<decltype(sampleMap)>::size <= mpt::array_size<decltype(mptIns.Keyboard)>::size); + for(size_t i = 0; i < std::size(sampleMap); i++) + { + mptIns.Keyboard[i] = sampleMap[i] + baseSample + 1; + } + + mptIns.nFadeOut = fadeout << 5; + envelopes.ConvertToMPT(mptIns); + } + +}; + +MPT_BINARY_STRUCT(AMFFInstrumentHeader, 225) + + +// AMFF sample header (old format) +struct AMFFSampleHeader +{ + // Sample flags (also used for RIFF AM) + enum SampleFlags + { + smp16Bit = 0x04, + smpLoop = 0x08, + smpPingPong = 0x10, + smpPanning = 0x20, + smpExists = 0x80, + // some flags are still missing... what is e.g. 0x8000? + }; + + uint32le id; // "SAMP" + uint32le chunkSize; // header + sample size + char name[28]; + uint8le pan; + uint8le volume; + uint16le flags; + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint32le sampleRate; + uint32le reserved1; + uint32le reserved2; + + // Convert sample header to OpenMPT's internal format. + void ConvertToMPT(AMFFInstrumentHeader &instrHeader, ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.nPan = pan * 4; + mptSmp.nVolume = volume * 4; + mptSmp.nGlobalVol = 64; + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nC5Speed = sampleRate; + + if(instrHeader.vibratoType < std::size(j2bAutoVibratoTrans)) + mptSmp.nVibType = j2bAutoVibratoTrans[instrHeader.vibratoType]; + mptSmp.nVibSweep = static_cast<uint8>(instrHeader.vibratoSweep); + mptSmp.nVibRate = static_cast<uint8>(instrHeader.vibratoRate / 16); + mptSmp.nVibDepth = static_cast<uint8>(instrHeader.vibratoDepth / 4); + if((mptSmp.nVibRate | mptSmp.nVibDepth) != 0) + { + // Convert XM-style vibrato sweep to IT + mptSmp.nVibSweep = 255 - mptSmp.nVibSweep; + } + + if(flags & AMFFSampleHeader::smp16Bit) + mptSmp.uFlags.set(CHN_16BIT); + if(flags & AMFFSampleHeader::smpLoop) + mptSmp.uFlags.set(CHN_LOOP); + if(flags & AMFFSampleHeader::smpPingPong) + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & AMFFSampleHeader::smpPanning) + mptSmp.uFlags.set(CHN_PANNING); + } + + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat() const + { + return SampleIO( + (flags & AMFFSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + } +}; + +MPT_BINARY_STRUCT(AMFFSampleHeader, 64) + + +// AM instrument envelope (new format) +struct AMEnvelope +{ + struct EnvPoint + { + uint16le tick; + int16le value; + }; + + uint16le flags; + uint8le numPoints; // actually, it's num. points - 1, and 0xFF if there is no envelope + uint8le sustainPoint; + uint8le loopStart; + uint8le loopEnd; + EnvPoint values[10]; + uint16le fadeout; // why is this here? it's only needed for the volume envelope... + + // Convert envelope data to OpenMPT's internal format. + void ConvertToMPT(InstrumentEnvelope &mptEnv, EnvelopeType envType) const + { + if(numPoints == 0xFF || numPoints == 0) + return; + + mptEnv.resize(std::min(numPoints + 1, 10)); + + mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint; + + mptEnv.nLoopStart = loopStart; + mptEnv.nLoopEnd = loopEnd; + + int32 scale = 0, offset = 0; + switch(envType) + { + case ENV_VOLUME: // 0....32767 + default: + scale = 32767 / ENVELOPE_MAX; + break; + case ENV_PITCH: // -4096....4096 + scale = 8192 / ENVELOPE_MAX; + offset = 4096; + break; + case ENV_PANNING: // -32768...32767 + scale = 65536 / ENVELOPE_MAX; + offset = 32768; + break; + } + + for(uint32 i = 0; i < mptEnv.size(); i++) + { + mptEnv[i].tick = values[i].tick >> 4; + if(i == 0) + mptEnv[i].tick = 0; + else if(mptEnv[i].tick < mptEnv[i - 1].tick) + mptEnv[i].tick = mptEnv[i - 1].tick + 1; + + int32 val = values[i].value + offset; + val = (val + scale / 2) / scale; + mptEnv[i].value = static_cast<EnvelopeNode::value_t>(std::clamp(val, int32(ENVELOPE_MIN), int32(ENVELOPE_MAX))); + } + + mptEnv.dwFlags.set(ENV_ENABLED, (flags & AMFFEnvelope::envEnabled) != 0); + mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & AMFFEnvelope::envSustain) && mptEnv.nSustainStart <= mptEnv.size()); + mptEnv.dwFlags.set(ENV_LOOP, (flags & AMFFEnvelope::envLoop) && mptEnv.nLoopStart <= mptEnv.nLoopEnd && mptEnv.nLoopStart <= mptEnv.size()); + } +}; + +MPT_BINARY_STRUCT(AMEnvelope::EnvPoint, 4) +MPT_BINARY_STRUCT(AMEnvelope, 48) + + +// AM instrument header (new format) +struct AMInstrumentHeader +{ + uint32le headSize; // Header size (i.e. the size of this struct) + uint8le unknown1; // 0x00 + uint8le index; // Actual instrument number + char name[32]; + uint8le sampleMap[128]; + uint8le vibratoType; + uint16le vibratoSweep; + uint16le vibratoDepth; + uint16le vibratoRate; + uint8le unknown2[7]; + AMEnvelope volEnv; + AMEnvelope pitchEnv; + AMEnvelope panEnv; + uint16le numSamples; + + // Convert instrument data to OpenMPT's internal format. + void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX baseSample) + { + mptIns.name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, name); + + static_assert(mpt::array_size<decltype(sampleMap)>::size <= mpt::array_size<decltype(mptIns.Keyboard)>::size); + for(uint8 i = 0; i < std::size(sampleMap); i++) + { + mptIns.Keyboard[i] = sampleMap[i] + baseSample + 1; + } + + mptIns.nFadeOut = volEnv.fadeout << 5; + + volEnv.ConvertToMPT(mptIns.VolEnv, ENV_VOLUME); + pitchEnv.ConvertToMPT(mptIns.PitchEnv, ENV_PITCH); + panEnv.ConvertToMPT(mptIns.PanEnv, ENV_PANNING); + + if(numSamples == 0) + { + MemsetZero(mptIns.Keyboard); + } + } +}; + +MPT_BINARY_STRUCT(AMInstrumentHeader, 326) + + +// AM sample header (new format) +struct AMSampleHeader +{ + uint32le headSize; // Header size (i.e. the size of this struct), apparently not including headSize. + char name[32]; + uint16le pan; + uint16le volume; + uint16le flags; + uint16le unknown; // 0x0000 / 0x0080? + uint32le length; + uint32le loopStart; + uint32le loopEnd; + uint32le sampleRate; + + // Convert sample header to OpenMPT's internal format. + void ConvertToMPT(AMInstrumentHeader &instrHeader, ModSample &mptSmp) const + { + mptSmp.Initialize(); + mptSmp.nPan = std::min(pan.get(), uint16(32767)) * 256 / 32767; + mptSmp.nVolume = std::min(volume.get(), uint16(32767)) * 256 / 32767; + mptSmp.nGlobalVol = 64; + mptSmp.nLength = length; + mptSmp.nLoopStart = loopStart; + mptSmp.nLoopEnd = loopEnd; + mptSmp.nC5Speed = sampleRate; + + if(instrHeader.vibratoType < std::size(j2bAutoVibratoTrans)) + mptSmp.nVibType = j2bAutoVibratoTrans[instrHeader.vibratoType]; + mptSmp.nVibSweep = static_cast<uint8>(instrHeader.vibratoSweep); + mptSmp.nVibRate = static_cast<uint8>(instrHeader.vibratoRate / 16); + mptSmp.nVibDepth = static_cast<uint8>(instrHeader.vibratoDepth / 4); + if((mptSmp.nVibRate | mptSmp.nVibDepth) != 0) + { + // Convert XM-style vibrato sweep to IT + mptSmp.nVibSweep = 255 - mptSmp.nVibSweep; + } + + if(flags & AMFFSampleHeader::smp16Bit) + mptSmp.uFlags.set(CHN_16BIT); + if(flags & AMFFSampleHeader::smpLoop) + mptSmp.uFlags.set(CHN_LOOP); + if(flags & AMFFSampleHeader::smpPingPong) + mptSmp.uFlags.set(CHN_PINGPONGLOOP); + if(flags & AMFFSampleHeader::smpPanning) + mptSmp.uFlags.set(CHN_PANNING); + } + + // Retrieve the internal sample format flags for this sample. + SampleIO GetSampleFormat() const + { + return SampleIO( + (flags & AMFFSampleHeader::smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit, + SampleIO::mono, + SampleIO::littleEndian, + SampleIO::signedPCM); + } +}; + +MPT_BINARY_STRUCT(AMSampleHeader, 60) + + +// Convert RIFF AM(FF) pattern data to MPT pattern data. +static bool ConvertAMPattern(FileReader chunk, PATTERNINDEX pat, bool isAM, CSoundFile &sndFile) +{ + // Effect translation LUT + static constexpr EffectCommand amEffTrans[] = + { + CMD_ARPEGGIO, CMD_PORTAMENTOUP, CMD_PORTAMENTODOWN, CMD_TONEPORTAMENTO, + CMD_VIBRATO, CMD_TONEPORTAVOL, CMD_VIBRATOVOL, CMD_TREMOLO, + CMD_PANNING8, CMD_OFFSET, CMD_VOLUMESLIDE, CMD_POSITIONJUMP, + CMD_VOLUME, CMD_PATTERNBREAK, CMD_MODCMDEX, CMD_TEMPO, + CMD_GLOBALVOLUME, CMD_GLOBALVOLSLIDE, CMD_KEYOFF, CMD_SETENVPOSITION, + CMD_CHANNELVOLUME, CMD_CHANNELVOLSLIDE, CMD_PANNINGSLIDE, CMD_RETRIG, + CMD_TREMOR, CMD_XFINEPORTAUPDOWN, + }; + + enum + { + rowDone = 0, // Advance to next row + channelMask = 0x1F, // Mask for retrieving channel information + volFlag = 0x20, // Volume effect present + noteFlag = 0x40, // Note + instr present + effectFlag = 0x80, // Effect information present + dataFlag = 0xE0, // Channel data present + }; + + if(chunk.NoBytesLeft()) + { + return false; + } + + ROWINDEX numRows = Clamp(static_cast<ROWINDEX>(chunk.ReadUint8()) + 1, ROWINDEX(1), MAX_PATTERN_ROWS); + + if(!sndFile.Patterns.Insert(pat, numRows)) + return false; + + const CHANNELINDEX channels = sndFile.GetNumChannels(); + if(channels == 0) + return false; + + ROWINDEX row = 0; + + while(row < numRows && chunk.CanRead(1)) + { + const uint8 flags = chunk.ReadUint8(); + + if(flags == rowDone) + { + row++; + continue; + } + + ModCommand &m = *sndFile.Patterns[pat].GetpModCommand(row, std::min(static_cast<CHANNELINDEX>(flags & channelMask), static_cast<CHANNELINDEX>(channels - 1))); + + if(flags & dataFlag) + { + if(flags & effectFlag) // effect + { + m.param = chunk.ReadUint8(); + uint8 command = chunk.ReadUint8(); + + if(command < std::size(amEffTrans)) + { + // command translation + m.command = amEffTrans[command]; + } else + { +#ifdef J2B_LOG + MPT_LOG_GLOBAL(LogDebug, "J2B", MPT_UFORMAT("J2B: Unknown command: 0x{}, param 0x{}")(mpt::ufmt::HEX0<2>(command), mpt::ufmt::HEX0<2>(m.param))); +#endif + m.command = CMD_NONE; + } + + // Handling special commands + switch(m.command) + { + case CMD_ARPEGGIO: + if(m.param == 0) m.command = CMD_NONE; + break; + case CMD_VOLUME: + if(m.volcmd == VOLCMD_NONE) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = Clamp(m.param, uint8(0), uint8(64)); + m.command = CMD_NONE; + m.param = 0; + } + break; + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_VOLUMESLIDE: + case CMD_GLOBALVOLSLIDE: + case CMD_PANNINGSLIDE: + if (m.param & 0xF0) m.param &= 0xF0; + break; + case CMD_PANNING8: + if(m.param <= 0x80) m.param = mpt::saturate_cast<uint8>(m.param * 2); + else if(m.param == 0xA4) {m.command = CMD_S3MCMDEX; m.param = 0x91;} + break; + case CMD_PATTERNBREAK: + m.param = ((m.param >> 4) * 10) + (m.param & 0x0F); + break; + case CMD_MODCMDEX: + m.ExtendedMODtoS3MEffect(); + break; + case CMD_TEMPO: + if(m.param <= 0x1F) m.command = CMD_SPEED; + break; + case CMD_XFINEPORTAUPDOWN: + switch(m.param & 0xF0) + { + case 0x10: + m.command = CMD_PORTAMENTOUP; + break; + case 0x20: + m.command = CMD_PORTAMENTODOWN; + break; + } + m.param = (m.param & 0x0F) | 0xE0; + break; + } + } + + if (flags & noteFlag) // note + ins + { + const auto [instr, note] = chunk.ReadArray<uint8, 2>(); + m.instr = instr; + m.note = note; + if(m.note == 0x80) m.note = NOTE_KEYOFF; + else if(m.note > 0x80) m.note = NOTE_FADE; // I guess the support for IT "note fade" notes was not intended in mod2j2b, but hey, it works! :-D + } + + if (flags & volFlag) // volume + { + m.volcmd = VOLCMD_VOLUME; + m.vol = chunk.ReadUint8(); + if(isAM) + { + m.vol = m.vol * 64 / 127; + } + } + } + } + + return true; +} + + +struct AMFFRiffChunkFormat +{ + uint32le format; +}; + +MPT_BINARY_STRUCT(AMFFRiffChunkFormat, 4) + + +static bool ValidateHeader(const AMFFRiffChunk &fileHeader) +{ + if(fileHeader.id != AMFFRiffChunk::idRIFF) + { + return false; + } + if(fileHeader.GetLength() < 8 + sizeof(AMFFMainChunk)) + { + return false; + } + return true; +} + + +static bool ValidateHeader(const AMFFRiffChunkFormat &formatHeader) +{ + if(formatHeader.format != AMFFRiffChunk::idAMFF && formatHeader.format != AMFFRiffChunk::idAM__) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAM(MemoryFileReader file, const uint64 *pfilesize) +{ + AMFFRiffChunk fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + AMFFRiffChunkFormat formatHeader; + if(!file.ReadStruct(formatHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(formatHeader)) + { + return ProbeFailure; + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadAM(FileReader &file, ModLoadingFlags loadFlags) +{ + file.Rewind(); + AMFFRiffChunk fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + AMFFRiffChunkFormat formatHeader; + if(!file.ReadStruct(formatHeader)) + { + return false; + } + if(!ValidateHeader(formatHeader)) + { + return false; + } + + bool isAM; // false: AMFF, true: AM + + uint32 format = formatHeader.format; + if(format == AMFFRiffChunk::idAMFF) + isAM = false; // "AMFF" + else if(format == AMFFRiffChunk::idAM__) + isAM = true; // "AM " + else + return false; + + ChunkReader chunkFile(file); + + // The main chunk is almost identical in both formats but uses different chunk IDs. + // "MAIN" - Song info (AMFF) + // "INIT" - Song info (AM) + AMFFRiffChunk::ChunkIdentifiers mainChunkID = isAM ? AMFFRiffChunk::idINIT : AMFFRiffChunk::idMAIN; + + // RIFF AM has a padding byte so that all chunks have an even size. + ChunkReader::ChunkList<AMFFRiffChunk> chunks; + if(loadFlags == onlyVerifyHeader) + chunks = chunkFile.ReadChunksUntil<AMFFRiffChunk>(isAM ? 2 : 1, mainChunkID); + else + chunks = chunkFile.ReadChunks<AMFFRiffChunk>(isAM ? 2 : 1); + + FileReader chunkMain(chunks.GetChunk(mainChunkID)); + AMFFMainChunk mainChunk; + if(!chunkMain.IsValid() + || !chunkMain.ReadStruct(mainChunk) + || mainChunk.channels < 1 + || !chunkMain.CanRead(mainChunk.channels)) + { + return false; + } else if(loadFlags == onlyVerifyHeader) + { + return true; + } + + InitializeGlobals(MOD_TYPE_J2B); + m_SongFlags = SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX; + m_SongFlags.set(SONG_LINEARSLIDES, !(mainChunk.flags & AMFFMainChunk::amigaSlides)); + + m_nChannels = std::min(static_cast<CHANNELINDEX>(mainChunk.channels), static_cast<CHANNELINDEX>(MAX_BASECHANNELS)); + m_nDefaultSpeed = mainChunk.speed; + m_nDefaultTempo.Set(mainChunk.tempo); + m_nDefaultGlobalVolume = mainChunk.globalvolume * 2; + + m_modFormat.formatName = isAM ? UL_("Galaxy Sound System (new version)") : UL_("Galaxy Sound System (old version)"); + m_modFormat.type = U_("j2b"); + m_modFormat.charset = mpt::Charset::CP437; + + m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, mainChunk.songname); + + // It seems like there's no way to differentiate between + // Muted and Surround channels (they're all 0xA0) - might + // be a limitation in mod2j2b. + for(CHANNELINDEX nChn = 0; nChn < m_nChannels; nChn++) + { + ChnSettings[nChn].Reset(); + + uint8 pan = chunkMain.ReadUint8(); + if(isAM) + { + if(pan > 128) + ChnSettings[nChn].dwFlags = CHN_MUTE; + else + ChnSettings[nChn].nPan = pan * 2; + } else + { + if(pan >= 128) + ChnSettings[nChn].dwFlags = CHN_MUTE; + else + ChnSettings[nChn].nPan = static_cast<uint16>(std::min(pan * 4, 256)); + } + } + + if(chunks.ChunkExists(AMFFRiffChunk::idORDR)) + { + // "ORDR" - Order list + FileReader chunk(chunks.GetChunk(AMFFRiffChunk::idORDR)); + uint8 numOrders = chunk.ReadUint8() + 1; + ReadOrderFromFile<uint8>(Order(), chunk, numOrders, 0xFF, 0xFE); + } + + // "PATT" - Pattern data for one pattern + if(loadFlags & loadPatternData) + { + PATTERNINDEX maxPattern = 0; + auto pattChunks = chunks.GetAllChunks(AMFFRiffChunk::idPATT); + Patterns.ResizeArray(static_cast<PATTERNINDEX>(pattChunks.size())); + for(auto chunk : pattChunks) + { + PATTERNINDEX pat = chunk.ReadUint8(); + size_t patternSize = chunk.ReadUint32LE(); + ConvertAMPattern(chunk.ReadChunk(patternSize), pat, isAM, *this); + maxPattern = std::max(maxPattern, pat); + } + for(PATTERNINDEX pat = 0; pat < maxPattern; pat++) + { + if(!Patterns.IsValidPat(pat)) + Patterns.Insert(pat, 64); + } + } + + if(!isAM) + { + // "INST" - Instrument (only in RIFF AMFF) + auto instChunks = chunks.GetAllChunks(AMFFRiffChunk::idINST); + for(auto chunk : instChunks) + { + AMFFInstrumentHeader instrHeader; + if(!chunk.ReadStruct(instrHeader)) + { + continue; + } + + const INSTRUMENTINDEX instr = instrHeader.index + 1; + if(instr >= MAX_INSTRUMENTS) + continue; + + ModInstrument *pIns = AllocateInstrument(instr); + if(pIns == nullptr) + { + continue; + } + + instrHeader.ConvertToMPT(*pIns, m_nSamples); + + // read sample sub-chunks - this is a rather "flat" format compared to RIFF AM and has no nested RIFF chunks. + for(size_t samples = 0; samples < instrHeader.numSamples; samples++) + { + AMFFSampleHeader sampleHeader; + + if(!CanAddMoreSamples() || !chunk.ReadStruct(sampleHeader)) + { + continue; + } + + const SAMPLEINDEX smp = ++m_nSamples; + + if(sampleHeader.id != AMFFRiffChunk::idSAMP) + { + continue; + } + + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); + sampleHeader.ConvertToMPT(instrHeader, Samples[smp]); + if(loadFlags & loadSampleData) + sampleHeader.GetSampleFormat().ReadSample(Samples[smp], chunk); + else + chunk.Skip(Samples[smp].GetSampleSizeInBytes()); + } + } + } else + { + // "RIFF" - Instrument (only in RIFF AM) + auto instChunks = chunks.GetAllChunks(AMFFRiffChunk::idRIFF); + for(ChunkReader chunk : instChunks) + { + if(chunk.ReadUint32LE() != AMFFRiffChunk::idAI__) + { + continue; + } + + AMFFRiffChunk instChunk; + if(!chunk.ReadStruct(instChunk) || instChunk.id != AMFFRiffChunk::idINST) + { + continue; + } + + AMInstrumentHeader instrHeader; + if(!chunk.ReadStruct(instrHeader)) + { + continue; + } + MPT_ASSERT(instrHeader.headSize + 4 == sizeof(instrHeader)); + + const INSTRUMENTINDEX instr = instrHeader.index + 1; + if(instr >= MAX_INSTRUMENTS) + continue; + + ModInstrument *pIns = AllocateInstrument(instr); + if(pIns == nullptr) + { + continue; + } + + instrHeader.ConvertToMPT(*pIns, m_nSamples); + + // Read sample sub-chunks (RIFF nesting ftw) + auto sampleChunks = chunk.ReadChunks<AMFFRiffChunk>(2).GetAllChunks(AMFFRiffChunk::idRIFF); + MPT_ASSERT(sampleChunks.size() == instrHeader.numSamples); + + for(auto sampleChunk : sampleChunks) + { + if(sampleChunk.ReadUint32LE() != AMFFRiffChunk::idAS__ || !CanAddMoreSamples()) + { + continue; + } + + // Don't read more samples than the instrument header claims to have. + if((instrHeader.numSamples--) == 0) + { + break; + } + + const SAMPLEINDEX smp = ++m_nSamples; + + // Aaand even more nested chunks! Great, innit? + AMFFRiffChunk sampleHeaderChunk; + if(!sampleChunk.ReadStruct(sampleHeaderChunk) || sampleHeaderChunk.id != AMFFRiffChunk::idSAMP) + { + break; + } + + FileReader sampleFileChunk = sampleChunk.ReadChunk(sampleHeaderChunk.length); + + AMSampleHeader sampleHeader; + if(!sampleFileChunk.ReadStruct(sampleHeader)) + { + break; + } + + m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleHeader.name); + + sampleHeader.ConvertToMPT(instrHeader, Samples[smp]); + + if(loadFlags & loadSampleData) + { + sampleFileChunk.Seek(sampleHeader.headSize + 4); + sampleHeader.GetSampleFormat().ReadSample(Samples[smp], sampleFileChunk); + } + } + + } + } + + return true; +} + + +static bool ValidateHeader(const J2BFileHeader &fileHeader) +{ + if(std::memcmp(fileHeader.signature, "MUSE", 4) + || (fileHeader.deadbeaf != J2BFileHeader::magicDEADBEAF // 0xDEADBEAF (RIFF AM) + && fileHeader.deadbeaf != J2BFileHeader::magicDEADBABE) // 0xDEADBABE (RIFF AMFF) + ) + { + return false; + } + if(fileHeader.packedLength == 0) + { + return false; + } + if(fileHeader.fileLength != fileHeader.packedLength + sizeof(J2BFileHeader)) + { + return false; + } + return true; +} + + +static bool ValidateHeaderFileSize(const J2BFileHeader &fileHeader, uint64 filesize) +{ + if(filesize != fileHeader.fileLength) + { + return false; + } + return true; +} + + +CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderJ2B(MemoryFileReader file, const uint64 *pfilesize) +{ + J2BFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return ProbeWantMoreData; + } + if(!ValidateHeader(fileHeader)) + { + return ProbeFailure; + } + if(pfilesize) + { + if(!ValidateHeaderFileSize(fileHeader, *pfilesize)) + { + return ProbeFailure; + } + } + MPT_UNREFERENCED_PARAMETER(pfilesize); + return ProbeSuccess; +} + + +bool CSoundFile::ReadJ2B(FileReader &file, ModLoadingFlags loadFlags) +{ + +#if !defined(MPT_WITH_ZLIB) && !defined(MPT_WITH_MINIZ) + + MPT_UNREFERENCED_PARAMETER(file); + MPT_UNREFERENCED_PARAMETER(loadFlags); + return false; + +#else + + file.Rewind(); + J2BFileHeader fileHeader; + if(!file.ReadStruct(fileHeader)) + { + return false; + } + if(!ValidateHeader(fileHeader)) + { + return false; + } + if(fileHeader.fileLength != file.GetLength() + || fileHeader.packedLength != file.BytesLeft() + ) + { + return false; + } + if(loadFlags == onlyVerifyHeader) + { + return true; + } + + // Header is valid, now unpack the RIFF AM file using inflate + z_stream strm{}; + if(inflateInit(&strm) != Z_OK) + return false; + + uint32 remainRead = fileHeader.packedLength, remainWrite = fileHeader.unpackedLength, totalWritten = 0; + uint32 crc = 0; + std::vector<Bytef> amFileData(remainWrite); + int retVal = Z_OK; + while(remainRead && remainWrite && retVal != Z_STREAM_END) + { + Bytef buffer[mpt::IO::BUFFERSIZE_TINY]; + uint32 readSize = std::min(static_cast<uint32>(sizeof(buffer)), remainRead); + file.ReadRaw(mpt::span(buffer, readSize)); + crc = crc32(crc, buffer, readSize); + + strm.avail_in = readSize; + strm.next_in = buffer; + do + { + strm.avail_out = remainWrite; + strm.next_out = amFileData.data() + totalWritten; + retVal = inflate(&strm, Z_NO_FLUSH); + uint32 written = remainWrite - strm.avail_out; + totalWritten += written; + remainWrite -= written; + } while(remainWrite && strm.avail_out == 0); + + remainRead -= readSize; + } + inflateEnd(&strm); + + bool result = false; +#ifndef MPT_BUILD_FUZZER + if(fileHeader.crc32 == crc && !remainWrite && retVal == Z_STREAM_END) +#endif + { + // Success, now load the RIFF AM(FF) module. + FileReader amFile(mpt::as_span(amFileData)); + result = ReadAM(amFile, loadFlags); + } + return result; + +#endif + +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.cpp new file mode 100644 index 00000000..60f881fe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.cpp @@ -0,0 +1,494 @@ +/* + * mod_specifications.cpp + * ---------------------- + * Purpose: Mod specifications characterise the features of every editable module format in OpenMPT, such as the number of supported channels, samples, effects, etc... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "mod_specifications.h" +#include "../common/misc_util.h" +#include <algorithm> + +OPENMPT_NAMESPACE_BEGIN + +namespace ModSpecs +{ + + +constexpr CModSpecifications mptm_ = +{ + /* + TODO: Proper, less arbitrarily chosen values here. + NOTE: If changing limits, see whether: + -savefile format and GUI methods can handle new values(might not be a small task :). + */ + MOD_TYPE_MPT, // Internal MODTYPE value + "mptm", // File extension + NOTE_MIN, // Minimum note index + NOTE_MAX, // Maximum note index + 4000, // Pattern max. + 4000, // Order max. + MAX_SEQUENCES, // Sequences max + 1, // Channel min + 127, // Channel max + 32, // Min tempo + 1000, // Max tempo + 1, // Min Speed + 255, // Max Speed + 1, // Min pattern rows + 1024, // Max pattern rows + 25, // Max mod name length + 25, // Max sample name length + 12, // Max sample filename length + 25, // Max instrument name length + 12, // Max instrument filename length + 0, // Max comment line length + 3999, // SamplesMax + 255, // instrumentMax + MixLevels::v1_17RC3, // defaultMixLevels + SONG_LINEARSLIDES | SONG_EXFILTERRANGE | SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX, // Supported song flags + 200, // Max MIDI mapping directives + MAX_ENVPOINTS, // Envelope point count + true, // Has notecut. + true, // Has noteoff. + true, // Has notefade. + true, // Has envelope release node + true, // Has song comments + true, // Has "+++" pattern + true, // Has "---" pattern + true, // Has restart position (order) + true, // Supports plugins + true, // Custom pattern time signatures + true, // Pattern names + true, // Has artist name + true, // Has default resampling + true, // Fixed point tempo + " JFEGHLKRXODB?CQATI?SMNVW?UY?P?Z\\:#+*?????????", // Supported Effects + " vpcdabuh??gfe?o", // Supported Volume Column commands +}; + + + + +constexpr CModSpecifications mod_ = +{ + MOD_TYPE_MOD, // Internal MODTYPE value + "mod", // File extension + 25, // Minimum note index + 108, // Maximum note index + 128, // Pattern max. + 128, // Order max. + 1, // Only one order list + 1, // Channel min + 99, // Channel max + 32, // Min tempo + 255, // Max tempo + 1, // Min Speed + 31, // Max Speed + 64, // Min pattern rows + 64, // Max pattern rows + 20, // Max mod name length + 22, // Max sample name length + 0, // Max sample filename length + 0, // Max instrument name length + 0, // Max instrument filename length + 0, // Max comment line length + 31, // SamplesMax + 0, // instrumentMax + MixLevels::Compatible, // defaultMixLevels + SONG_PT_MODE | SONG_AMIGALIMITS | SONG_ISAMIGA, // Supported song flags + 0, // Max MIDI mapping directives + 0, // No instrument envelopes + false, // No notecut. + false, // No noteoff. + false, // No notefade. + false, // No envelope release node + false, // No song comments + false, // Doesn't have "+++" pattern + false, // Doesn't have "---" pattern + true, // Has restart position (order) + false, // Doesn't support plugins + false, // No custom pattern time signatures + false, // No pattern names + false, // Doesn't have artist name + false, // Doesn't have default resampling + false, // Integer tempo + " 0123456789ABCD?FF?E??????????????????????????", // Supported Effects + " ???????????????", // Supported Volume Column commands +}; + + +constexpr CModSpecifications xm_ = +{ + MOD_TYPE_XM, // Internal MODTYPE value + "xm", // File extension + 13, // Minimum note index + 108, // Maximum note index + 256, // Pattern max. + 255, // Order max. + 1, // Only one order list + 1, // Channel min + 32, // Channel max + 32, // Min tempo + 1000, // Max tempo + 1, // Min Speed + 31, // Max Speed + 1, // Min pattern rows + 256, // Max pattern rows + 20, // Max mod name length + 22, // Max sample name length + 0, // Max sample filename length + 22, // Max instrument name length + 0, // Max instrument filename length + 0, // Max comment line length + 128 * 16, // SamplesMax (actually 16 per instrument) + 128, // instrumentMax + MixLevels::CompatibleFT2, // defaultMixLevels + SONG_LINEARSLIDES, // Supported song flags + 0, // Max MIDI mapping directives + 12, // Envelope point count + false, // No notecut. + true, // Has noteoff. + false, // No notefade. + false, // No envelope release node + false, // No song comments + false, // Doesn't have "+++" pattern + false, // Doesn't have "---" pattern + true, // Has restart position (order) + false, // Doesn't support plugins + false, // No custom pattern time signatures + false, // No pattern names + false, // Doesn't have artist name + false, // Doesn't have default resampling + false, // Integer tempo + " 0123456789ABCDRFFTE???GHK??XPL??????W????????", // Supported Effects + " vpcdabuhlrg????", // Supported Volume Column commands +}; + +// XM with MPT extensions +constexpr CModSpecifications xmEx_ = +{ + MOD_TYPE_XM, // Internal MODTYPE value + "xm", // File extension + 13, // Minimum note index + 108, // Maximum note index + 256, // Pattern max. + 255, // Order max. + 1, // Only one order list + 1, // Channel min + 127, // Channel max + 32, // Min tempo + 1000, // Max tempo + 1, // Min Speed + 31, // Max Speed + 1, // Min pattern rows + 1024, // Max pattern rows + 20, // Max mod name length + 22, // Max sample name length + 0, // Max sample filename length + 22, // Max instrument name length + 0, // Max instrument filename length + 0, // Max comment line length + MAX_SAMPLES - 1, // SamplesMax (actually 32 per instrument(256 * 32 = 8192), but limited to MAX_SAMPLES = 4000) + 255, // instrumentMax + MixLevels::CompatibleFT2, // defaultMixLevels + SONG_LINEARSLIDES | SONG_EXFILTERRANGE, // Supported song flags + 200, // Max MIDI mapping directives + 12, // Envelope point count + false, // No notecut. + true, // Has noteoff. + false, // No notefade. + false, // No envelope release node + true, // Has song comments + false, // Doesn't have "+++" pattern + false, // Doesn't have "---" pattern + true, // Has restart position (order) + true, // Supports plugins + false, // No custom pattern time signatures + true, // Pattern names + true, // Has artist name + false, // Doesn't have default resampling + false, // Integer tempo + " 0123456789ABCDRFFTE???GHK?YXPLZ\\?#??W????????", // Supported Effects + " vpcdabuhlrg????", // Supported Volume Column commands +}; + +constexpr CModSpecifications s3m_ = +{ + MOD_TYPE_S3M, // Internal MODTYPE value + "s3m", // File extension + 13, // Minimum note index + 108, // Maximum note index + 100, // Pattern max. + 255, // Order max. + 1, // Only one order list + 1, // Channel min + 32, // Channel max + 33, // Min tempo + 255, // Max tempo + 1, // Min Speed + 255, // Max Speed + 64, // Min pattern rows + 64, // Max pattern rows + 27, // Max mod name length + 27, // Max sample name length + 12, // Max sample filename length + 0, // Max instrument name length + 0, // Max instrument filename length + 0, // Max comment line length + 99, // SamplesMax + 0, // instrumentMax + MixLevels::Compatible, // defaultMixLevels + SONG_FASTVOLSLIDES | SONG_AMIGALIMITS | SONG_S3MOLDVIBRATO, // Supported song flags + 0, // Max MIDI mapping directives + 0, // No instrument envelopes + true, // Has notecut. + false, // No noteoff. + false, // No notefade. + false, // No envelope release node + false, // No song comments + true, // Has "+++" pattern + true, // Has "---" pattern + false, // Doesn't have restart position (order) + false, // Doesn't support plugins + false, // No custom pattern time signatures + false, // No pattern names + false, // Doesn't have artist name + false, // Doesn't have default resampling + false, // Integer tempo + " JFEGHLKRXODB?CQATI?SMNVW?U?????????? ????????", // Supported Effects + " vp?????????????", // Supported Volume Column commands +}; + +// S3M with MPT extensions +constexpr CModSpecifications s3mEx_ = +{ + MOD_TYPE_S3M, // Internal MODTYPE value + "s3m", // File extension + 13, // Minimum note index + 108, // Maximum note index + 100, // Pattern max. + 255, // Order max. + 1, // Only one order list + 1, // Channel min + 32, // Channel max + 33, // Min tempo + 255, // Max tempo + 1, // Min Speed + 255, // Max Speed + 64, // Min pattern rows + 64, // Max pattern rows + 27, // Max mod name length + 27, // Max sample name length + 12, // Max sample filename length + 0, // Max instrument name length + 0, // Max instrument filename length + 0, // Max comment line length + 99, // SamplesMax + 0, // instrumentMax + MixLevels::Compatible, // defaultMixLevels + SONG_FASTVOLSLIDES | SONG_AMIGALIMITS, // Supported song flags + 0, // Max MIDI mapping directives + 0, // No instrument envelopes + true, // Has notecut. + false, // No noteoff. + false, // No notefade. + false, // No envelope release node + false, // No song comments + true, // Has "+++" pattern + true, // Has "---" pattern + false, // Doesn't have restart position (order) + false, // Doesn't support plugins + false, // No custom pattern time signatures + false, // No pattern names + false, // Doesn't have artist name + false, // Doesn't have default resampling + false, // Integer tempo + " JFEGHLKRXODB?CQATI?SMNVW?UY?P?Z????? ????????", // Supported Effects + " vp?????????????", // Supported Volume Column commands +}; + +constexpr CModSpecifications it_ = +{ + MOD_TYPE_IT, // Internal MODTYPE value + "it", // File extension + 1, // Minimum note index + 120, // Maximum note index + 200, // Pattern max. + 256, // Order max. + 1, // Only one order list + 1, // Channel min + 64, // Channel max + 32, // Min tempo + 255, // Max tempo + 1, // Min Speed + 255, // Max Speed + 1, // Min pattern rows + 200, // Max pattern rows + 25, // Max mod name length + 25, // Max sample name length + 12, // Max sample filename length + 25, // Max instrument name length + 12, // Max instrument filename length + 75, // Max comment line length + 99, // SamplesMax + 99, // instrumentMax + MixLevels::Compatible, // defaultMixLevels + SONG_LINEARSLIDES | SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX, // Supported song flags + 0, // Max MIDI mapping directives + 25, // Envelope point count + true, // Has notecut. + true, // Has noteoff. + true, // Has notefade. + false, // No envelope release node + true, // Has song comments + true, // Has "+++" pattern + true, // Has "--" pattern + false, // Doesn't have restart position (order) + false, // Doesn't support plugins + false, // No custom pattern time signatures + false, // No pattern names + false, // Doesn't have artist name + false, // Doesn't have default resampling + false, // Integer tempo + " JFEGHLKRXODB?CQATI?SMNVW?UY?P?Z????? ????????", // Supported Effects + " vpcdab?h??gfe??", // Supported Volume Column commands +}; + +constexpr CModSpecifications itEx_ = +{ + MOD_TYPE_IT, // Internal MODTYPE value + "it", // File extension + 1, // Minimum note index + 120, // Maximum note index + 240, // Pattern max. + 256, // Order max. + 1, // Only one order list + 1, // Channel min + 127, // Channel max + 32, // Min tempo + 512, // Max tempo + 1, // Min Speed + 255, // Max Speed + 1, // Min pattern rows + 1024, // Max pattern rows + 25, // Max mod name length + 25, // Max sample name length + 12, // Max sample filename length + 25, // Max instrument name length + 12, // Max instrument filename length + 75, // Max comment line length + 3999, // SamplesMax + 255, // instrumentMax + MixLevels::Compatible, // defaultMixLevels + SONG_LINEARSLIDES | SONG_EXFILTERRANGE | SONG_ITOLDEFFECTS | SONG_ITCOMPATGXX, // Supported song flags + 200, // Max MIDI mapping directives + 25, // Envelope point count + true, // Has notecut. + true, // Has noteoff. + true, // Has notefade. + false, // No envelope release node + true, // Has song comments + true, // Has "+++" pattern + true, // Has "---" pattern + false, // Doesn't have restart position (order) + true, // Supports plugins + false, // No custom pattern time signatures + true, // Pattern names + true, // Has artist name + false, // Doesn't have default resampling + false, // Integer tempo + " JFEGHLKRXODB?CQATI?SMNVW?UY?P?Z\\?#?? ????????", // Supported Effects + " vpcdab?h??gfe??", // Supported Volume Column commands +}; + +const std::array<const CModSpecifications *, 8> Collection = { &mptm_, &mod_, &s3m_, &s3mEx_, &xm_, &xmEx_, &it_, &itEx_ }; + +const CModSpecifications &mptm = mptm_; +const CModSpecifications &mod = mod_; +const CModSpecifications &s3m = s3m_; +const CModSpecifications &s3mEx = s3mEx_; +const CModSpecifications &xm = xm_; +const CModSpecifications &xmEx = xmEx_; +const CModSpecifications &it = it_; +const CModSpecifications &itEx = itEx_; + +} // namespace ModSpecs + + +MODTYPE CModSpecifications::ExtensionToType(std::string ext) +{ + if(ext.empty()) + { + return MOD_TYPE_NONE; + } else if(ext[0] == '.') + { + ext.erase(0, 1); + } + ext = mpt::ToLowerCaseAscii(ext); + for(const auto &spec : ModSpecs::Collection) + { + if(ext == spec->fileExtension) + { + return spec->internalType; + } + } + return MOD_TYPE_NONE; +} + + +bool CModSpecifications::HasNote(ModCommand::NOTE note) const +{ + if(note >= noteMin && note <= noteMax) + return true; + else if(ModCommand::IsSpecialNote(note)) + { + if(note == NOTE_NOTECUT) + return hasNoteCut; + else if(note == NOTE_KEYOFF) + return hasNoteOff; + else if(note == NOTE_FADE) + return hasNoteFade; + else + return (internalType == MOD_TYPE_MPT); + } else if(note == NOTE_NONE) + return true; + return false; +} + + +bool CModSpecifications::HasVolCommand(ModCommand::VOLCMD volcmd) const +{ + if(volcmd >= MAX_VOLCMDS) return false; + if(volcommands[volcmd] == '?') return false; + return true; +} + + +bool CModSpecifications::HasCommand(ModCommand::COMMAND cmd) const +{ + if(cmd >= MAX_EFFECTS) return false; + if(commands[cmd] == '?') return false; + return true; +} + + +char CModSpecifications::GetVolEffectLetter(ModCommand::VOLCMD volcmd) const +{ + if(volcmd >= MAX_VOLCMDS) return '?'; + return volcommands[volcmd]; +} + + +char CModSpecifications::GetEffectLetter(ModCommand::COMMAND cmd) const +{ + if(cmd >= MAX_EFFECTS) return '?'; + return commands[cmd]; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.h b/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.h new file mode 100644 index 00000000..bbf2a170 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/mod_specifications.h @@ -0,0 +1,100 @@ +/* + * mod_specifications.h + * -------------------- + * Purpose: Mod specifications characterise the features of every editable module format in OpenMPT, such as the number of supported channels, samples, effects, etc... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" +#include "modcommand.h" // ModCommand:: +#include "../soundlib/SoundFilePlayConfig.h" // mixlevel constants. + + +OPENMPT_NAMESPACE_BEGIN + + +struct CModSpecifications +{ + /// Returns modtype corresponding to given file extension. The extension string + /// may begin with or without dot, e.g. both ".it" and "it" will be handled correctly. + static MODTYPE ExtensionToType(std::string ext); // (encoded in UTF8) + + // Return true if format supports given note. + bool HasNote(ModCommand::NOTE note) const; + bool HasVolCommand(ModCommand::VOLCMD volcmd) const; + bool HasCommand(ModCommand::COMMAND cmd) const; + // Return corresponding effect letter for this format + char GetEffectLetter(ModCommand::COMMAND cmd) const; + char GetVolEffectLetter(ModCommand::VOLCMD cmd) const; + + // NOTE: If changing order, update all initializations in .cpp file. + MODTYPE internalType; // Internal MODTYPE value + const char *fileExtension; // File extension without dot (encoded in UTF8). + ModCommand::NOTE noteMin; // Minimum note index (index starts from 1) + ModCommand::NOTE noteMax; // Maximum note index (index starts from 1) + PATTERNINDEX patternsMax; + ORDERINDEX ordersMax; + SEQUENCEINDEX sequencesMax; + CHANNELINDEX channelsMin; // Minimum number of editable channels in pattern. + CHANNELINDEX channelsMax; // Maximum number of editable channels in pattern. + uint32 tempoMinInt; + uint32 tempoMaxInt; + uint32 speedMin; // Minimum ticks per frame + uint32 speedMax; // Maximum ticks per frame + ROWINDEX patternRowsMin; + ROWINDEX patternRowsMax; + uint16 modNameLengthMax; // Meaning 'usable letters', possible null character is not included. + uint16 sampleNameLengthMax; // Ditto + uint16 sampleFilenameLengthMax; // Ditto + uint16 instrNameLengthMax; // Ditto + uint16 instrFilenameLengthMax; // Ditto + uint16 commentLineLengthMax; // not including line break, 0 for unlimited + SAMPLEINDEX samplesMax; // Max number of samples == Highest possible sample index + INSTRUMENTINDEX instrumentsMax; // Max number of instruments == Highest possible instrument index + MixLevels defaultMixLevels; // Default mix levels that are used when creating a new file in this format + FlagSet<SongFlags> songFlags; // Supported song flags + uint8 MIDIMappingDirectivesMax; // Number of MIDI Mapping directives that the format can store (0 = none) + uint8 envelopePointsMax; // Maximum number of points of each envelope + bool hasNoteCut; // True if format has note cut (^^). + bool hasNoteOff; // True if format has note off (==). + bool hasNoteFade; // True if format has note fade (~~). + bool hasReleaseNode; // Envelope release node + bool hasComments; // True if format has a comments field + bool hasIgnoreIndex; // Does "+++" pattern exist? + bool hasStopIndex; // Does "---" pattern exist? + bool hasRestartPos; // Format has an automatic restart order position + bool supportsPlugins; // Format can store plugins + bool hasPatternSignatures; // Can patterns have a custom time signature? + bool hasPatternNames; // Can patterns have a name? + bool hasArtistName; // Can artist name be stored in file? + bool hasDefaultResampling; // Can default resampling be saved? (if not, it can still be modified in the GUI but won't set the module as modified) + bool hasFractionalTempo; // Are fractional tempos allowed? + const char *commands; // An array holding all commands this format supports; commands that are not supported are marked with "?" + const char *volcommands; // Ditto, but for volume column + MPT_CONSTEXPRINLINE TEMPO GetTempoMin() const { return TEMPO(tempoMinInt, 0); } + MPT_CONSTEXPRINLINE TEMPO GetTempoMax() const { return TEMPO(tempoMaxInt, 0); } +}; + + +namespace ModSpecs +{ + extern const CModSpecifications & mptm; + extern const CModSpecifications & mod; + extern const CModSpecifications & s3m; + extern const CModSpecifications & s3mEx; + extern const CModSpecifications & xm; + extern const CModSpecifications & xmEx; + extern const CModSpecifications & it; + extern const CModSpecifications & itEx; + extern const std::array<const CModSpecifications *, 8> Collection; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.cpp new file mode 100644 index 00000000..5f5f98b2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.cpp @@ -0,0 +1,1279 @@ +/* + * modcommand.cpp + * -------------- + * Purpose: Various functions for writing effects to patterns, converting ModCommands, etc. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#include "Tables.h" + + +OPENMPT_NAMESPACE_BEGIN + + +const EffectType effectTypes[] = +{ + EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, + EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, + EFFECT_TYPE_VOLUME, EFFECT_TYPE_PANNING, EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, + EFFECT_TYPE_GLOBAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, + EFFECT_TYPE_GLOBAL, EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, + EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_GLOBAL, + EFFECT_TYPE_GLOBAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, + EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, + EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, + EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, + EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, + EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, +}; + +static_assert(std::size(effectTypes) == MAX_EFFECTS); + + +const EffectType volumeEffectTypes[] = +{ + EFFECT_TYPE_NORMAL, EFFECT_TYPE_VOLUME, EFFECT_TYPE_PANNING, EFFECT_TYPE_VOLUME, + EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_VOLUME, EFFECT_TYPE_PITCH, + EFFECT_TYPE_PITCH, EFFECT_TYPE_PANNING, EFFECT_TYPE_PANNING, EFFECT_TYPE_PITCH, + EFFECT_TYPE_PITCH, EFFECT_TYPE_PITCH, EFFECT_TYPE_NORMAL, EFFECT_TYPE_NORMAL, +}; + +static_assert(std::size(volumeEffectTypes) == MAX_VOLCMDS); + + +EffectType ModCommand::GetEffectType(COMMAND cmd) +{ + if(cmd < std::size(effectTypes)) + return effectTypes[cmd]; + else + return EFFECT_TYPE_NORMAL; +} + + +EffectType ModCommand::GetVolumeEffectType(VOLCMD volcmd) +{ + if(volcmd < std::size(volumeEffectTypes)) + return volumeEffectTypes[volcmd]; + else + return EFFECT_TYPE_NORMAL; +} + + +// Convert an Exx command (MOD) to Sxx command (S3M) +void ModCommand::ExtendedMODtoS3MEffect() +{ + if(command != CMD_MODCMDEX) + return; + + command = CMD_S3MCMDEX; + switch(param & 0xF0) + { + case 0x00: command = CMD_NONE; break; // No filter control + case 0x10: command = CMD_PORTAMENTOUP; param |= 0xF0; break; + case 0x20: command = CMD_PORTAMENTODOWN; param |= 0xF0; break; + case 0x30: param = (param & 0x0F) | 0x10; break; + case 0x40: param = (param & 0x03) | 0x30; break; + case 0x50: param = (param & 0x0F) | 0x20; break; + case 0x60: param = (param & 0x0F) | 0xB0; break; + case 0x70: param = (param & 0x03) | 0x40; break; + case 0x90: command = CMD_RETRIG; param = (param & 0x0F); break; + case 0xA0: if(param & 0x0F) { command = CMD_VOLUMESLIDE; param = (param << 4) | 0x0F; } else command = CMD_NONE; break; + case 0xB0: if(param & 0x0F) { command = CMD_VOLUMESLIDE; param = 0xF0 | static_cast<PARAM>(std::min(param & 0x0F, 0x0E)); } else command = CMD_NONE; break; + case 0xC0: if(param == 0xC0) { command = CMD_NONE; note = NOTE_NOTECUT; } break; // this does different things in IT and ST3 + case 0xD0: if(param == 0xD0) { command = CMD_NONE; } break; // ditto + // rest are the same or handled elsewhere + } +} + + +// Convert an Sxx command (S3M) to Exx command (MOD) +void ModCommand::ExtendedS3MtoMODEffect() +{ + if(command != CMD_S3MCMDEX) + return; + + command = CMD_MODCMDEX; + switch(param & 0xF0) + { + case 0x10: param = (param & 0x0F) | 0x30; break; + case 0x20: param = (param & 0x0F) | 0x50; break; + case 0x30: param = (param & 0x0F) | 0x40; break; + case 0x40: param = (param & 0x0F) | 0x70; break; + case 0x50: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X5x + case 0x60: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X6x + case 0x80: command = CMD_PANNING8; param = (param & 0x0F) * 0x11; break; // FT2 does actually not support E8x + case 0x90: command = CMD_XFINEPORTAUPDOWN; break; // map to unused X9x + case 0xA0: command = CMD_XFINEPORTAUPDOWN; break; // map to unused XAx + case 0xB0: param = (param & 0x0F) | 0x60; break; + case 0x70: command = CMD_NONE; break; // No NNA / envelope control in MOD/XM format + // rest are the same or handled elsewhere + } +} + + +// Convert a mod command from one format to another. +void ModCommand::Convert(MODTYPE fromType, MODTYPE toType, const CSoundFile &sndFile) +{ + if(fromType == toType) + { + return; + } + + if(fromType == MOD_TYPE_MTM) + { + // Special MTM fixups. + // Retrigger with param 0 + if(command == CMD_MODCMDEX && param == 0x90) + { + command = CMD_NONE; + } else if(command == CMD_VIBRATO) + { + // Vibrato is approximately half as deep compared to MOD/S3M. + uint8 speed = (param & 0xF0); + uint8 depth = (param & 0x0F) >> 1; + param = speed | depth; + } + // Apart from these special fixups, do a regular conversion from MOD. + fromType = MOD_TYPE_MOD; + } + if(command == CMD_DIGIREVERSESAMPLE && toType != MOD_TYPE_DIGI) + { + command = CMD_S3MCMDEX; + param = 0x9F; + } + + // helper variables + const bool oldTypeIsMOD = (fromType == MOD_TYPE_MOD), oldTypeIsXM = (fromType == MOD_TYPE_XM), + oldTypeIsS3M = (fromType == MOD_TYPE_S3M), oldTypeIsIT = (fromType == MOD_TYPE_IT), + oldTypeIsMPT = (fromType == MOD_TYPE_MPT), oldTypeIsMOD_XM = (oldTypeIsMOD || oldTypeIsXM), + oldTypeIsS3M_IT_MPT = (oldTypeIsS3M || oldTypeIsIT || oldTypeIsMPT), + oldTypeIsIT_MPT = (oldTypeIsIT || oldTypeIsMPT); + + const bool newTypeIsMOD = (toType == MOD_TYPE_MOD), newTypeIsXM = (toType == MOD_TYPE_XM), + newTypeIsS3M = (toType == MOD_TYPE_S3M), newTypeIsIT = (toType == MOD_TYPE_IT), + newTypeIsMPT = (toType == MOD_TYPE_MPT), newTypeIsMOD_XM = (newTypeIsMOD || newTypeIsXM), + newTypeIsS3M_IT_MPT = (newTypeIsS3M || newTypeIsIT || newTypeIsMPT), + newTypeIsIT_MPT = (newTypeIsIT || newTypeIsMPT); + + const CModSpecifications &newSpecs = CSoundFile::GetModSpecifications(toType); + + ////////////////////////// + // Convert 8-bit Panning + if(command == CMD_PANNING8) + { + if(newTypeIsS3M) + { + param = (param + 1) >> 1; + } else if(oldTypeIsS3M) + { + if(param == 0xA4) + { + // surround remap + command = static_cast<COMMAND>((toType & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? CMD_S3MCMDEX : CMD_XFINEPORTAUPDOWN); + param = 0x91; + } else + { + param = mpt::saturate_cast<PARAM>(param * 2u); + } + } + } // End if(command == CMD_PANNING8) + + // Re-map \xx to Zxx if the new format only knows the latter command. + if(command == CMD_SMOOTHMIDI && !newSpecs.HasCommand(CMD_SMOOTHMIDI) && newSpecs.HasCommand(CMD_MIDI)) + { + command = CMD_MIDI; + } + + /////////////////////////////////////////////////////////////////////////////////////// + // MPTM to anything: Convert param control, extended envelope control, note delay+cut + if(oldTypeIsMPT) + { + if(IsPcNote()) + { + COMMAND newCmd = static_cast<COMMAND>(note == NOTE_PC ? CMD_MIDI : CMD_SMOOTHMIDI); + if(!newSpecs.HasCommand(newCmd)) + { + newCmd = CMD_MIDI; // assuming that this was CMD_SMOOTHMIDI + if(!newSpecs.HasCommand(newCmd)) + { + newCmd = CMD_NONE; + } + } + + param = static_cast<PARAM>(std::min(static_cast<uint16>(maxColumnValue), GetValueEffectCol()) * 0x7F / maxColumnValue); + command = newCmd; // might be removed later + volcmd = VOLCMD_NONE; + note = NOTE_NONE; + instr = 0; + } + + if((command == CMD_S3MCMDEX) && ((param & 0xF0) == 0x70) && ((param & 0x0F) > 0x0C)) + { + // Extended pitch envelope control commands + param = 0x7C; + } else if(command == CMD_DELAYCUT) + { + command = CMD_S3MCMDEX; // When converting to MOD/XM, this will be converted to CMD_MODCMDEX later + param = 0xD0 | (param >> 4); // Preserve delay nibble + } else if(command == CMD_FINETUNE || command == CMD_FINETUNE_SMOOTH) + { + // Convert finetune from +/-128th of a semitone to (extra-)fine portamento (assumes linear slides, plus we're missing the actual pitch wheel depth of the instrument) + if(param < 0x80) + { + command = CMD_PORTAMENTODOWN; + param = 0x80 - param; + } else if(param > 0x80) + { + command = CMD_PORTAMENTOUP; + param -= 0x80; + } + if(param <= 30) + param = 0xE0 | ((param + 1u) / 2u); + else + param = 0xF0 | std::min(static_cast<PARAM>((param + 7u) / 8u), PARAM(15)); + } + } // End if(oldTypeIsMPT) + + ///////////////////////////////////////// + // Convert MOD / XM to S3M / IT / MPTM + if(oldTypeIsMOD_XM && newTypeIsS3M_IT_MPT) + { + switch(command) + { + case CMD_ARPEGGIO: + if(!param) command = CMD_NONE; // 000 does nothing in MOD/XM + break; + + case CMD_MODCMDEX: + ExtendedMODtoS3MEffect(); + break; + + case CMD_VOLUME: + // Effect column volume command overrides the volume column in XM. + if(volcmd == VOLCMD_NONE || volcmd == VOLCMD_VOLUME) + { + volcmd = VOLCMD_VOLUME; + vol = param; + if(vol > 64) vol = 64; + command = CMD_NONE; + param = 0; + } else if(volcmd == VOLCMD_PANNING) + { + std::swap(vol, param); + volcmd = VOLCMD_VOLUME; + if(vol > 64) vol = 64; + command = CMD_S3MCMDEX; + param = 0x80 | (param / 4); // XM volcol panning is actually 4-Bit, so we can use 4-Bit panning here. + } + break; + + case CMD_PORTAMENTOUP: + if(param > 0xDF) param = 0xDF; + break; + + case CMD_PORTAMENTODOWN: + if(param > 0xDF) param = 0xDF; + break; + + case CMD_XFINEPORTAUPDOWN: + switch(param & 0xF0) + { + case 0x10: command = CMD_PORTAMENTOUP; param = (param & 0x0F) | 0xE0; break; + case 0x20: command = CMD_PORTAMENTODOWN; param = (param & 0x0F) | 0xE0; break; + case 0x50: + case 0x60: + case 0x70: + case 0x90: + case 0xA0: + command = CMD_S3MCMDEX; + // Surround remap (this is the "official" command) + if(toType & MOD_TYPE_S3M && param == 0x91) + { + command = CMD_PANNING8; + param = 0xA4; + } + break; + } + break; + + case CMD_KEYOFF: + if(note == NOTE_NONE) + { + note = newTypeIsS3M ? NOTE_NOTECUT : NOTE_KEYOFF; + command = CMD_S3MCMDEX; + if(param == 0) + instr = 0; + param = 0xD0 | (param & 0x0F); + } + break; + + case CMD_PANNINGSLIDE: + // swap L/R, convert to fine slide + if(param & 0xF0) + { + param = 0xF0 | std::min(PARAM(0x0E), static_cast<PARAM>(param >> 4)); + } else + { + param = 0x0F | (std::min(PARAM(0x0E), static_cast<PARAM>(param & 0x0F)) << 4); + } + break; + + default: + break; + } + } // End if(oldTypeIsMOD_XM && newTypeIsS3M_IT_MPT) + + + ///////////////////////////////////////// + // Convert S3M / IT / MPTM to MOD / XM + else if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) + { + if(note == NOTE_NOTECUT) + { + // convert note cut to C00 if possible or volume command otherwise (MOD/XM has no real way of cutting notes that cannot be "undone" by volume commands) + note = NOTE_NONE; + if(command == CMD_NONE || !newTypeIsXM) + { + command = CMD_VOLUME; + param = 0; + } else + { + volcmd = VOLCMD_VOLUME; + vol = 0; + } + } else if(note == NOTE_FADE) + { + // convert note fade to note off + note = NOTE_KEYOFF; + } + + switch(command) + { + case CMD_S3MCMDEX: + ExtendedS3MtoMODEffect(); + break; + + case CMD_TONEPORTAVOL: // Can't do fine slides and portamento/vibrato at the same time :( + case CMD_VIBRATOVOL: // ditto + if(volcmd == VOLCMD_NONE && (((param & 0xF0) && ((param & 0x0F) == 0x0F)) || ((param & 0x0F) && ((param & 0xF0) == 0xF0)))) + { + // Try to salvage portamento/vibrato + if(command == CMD_TONEPORTAVOL) + volcmd = VOLCMD_TONEPORTAMENTO; + else if(command == CMD_VIBRATOVOL) + volcmd = VOLCMD_VIBRATODEPTH; + vol = 0; + } + + [[fallthrough]]; + case CMD_VOLUMESLIDE: + if((param & 0xF0) && ((param & 0x0F) == 0x0F)) + { + command = CMD_MODCMDEX; + param = (param >> 4) | 0xA0; + } else if((param & 0x0F) && ((param & 0xF0) == 0xF0)) + { + command = CMD_MODCMDEX; + param = (param & 0x0F) | 0xB0; + } + break; + + case CMD_PORTAMENTOUP: + if(param >= 0xF0) + { + command = CMD_MODCMDEX; + param = (param & 0x0F) | 0x10; + } else if(param >= 0xE0) + { + if(newTypeIsXM) + { + command = CMD_XFINEPORTAUPDOWN; + param = 0x10 | (param & 0x0F); + } else + { + command = CMD_MODCMDEX; + param = (((param & 0x0F) + 3) >> 2) | 0x10; + } + } else + { + command = CMD_PORTAMENTOUP; + } + break; + + case CMD_PORTAMENTODOWN: + if(param >= 0xF0) + { + command = CMD_MODCMDEX; + param = (param & 0x0F) | 0x20; + } else if(param >= 0xE0) + { + if(newTypeIsXM) + { + command = CMD_XFINEPORTAUPDOWN; + param = 0x20 | (param & 0x0F); + } else + { + command = CMD_MODCMDEX; + param = (((param & 0x0F) + 3) >> 2) | 0x20; + } + } else + { + command = CMD_PORTAMENTODOWN; + } + break; + + case CMD_TEMPO: + if(param < 0x20) command = CMD_NONE; // no tempo slides + break; + + case CMD_PANNINGSLIDE: + // swap L/R, convert fine slides to normal slides + if((param & 0x0F) == 0x0F && (param & 0xF0)) + { + param = (param >> 4); + } else if((param & 0xF0) == 0xF0 && (param & 0x0F)) + { + param = (param & 0x0F) << 4; + } else if(param & 0x0F) + { + param = 0xF0; + } else if(param & 0xF0) + { + param = 0x0F; + } else + { + param = 0; + } + break; + + case CMD_RETRIG: + // Retrig: Q0y doesn't change volume in IT/S3M, but R0y in XM takes the last x parameter + if(param != 0 && (param & 0xF0) == 0) + { + param |= 0x80; + } + break; + + default: + break; + } + } // End if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) + + + /////////////////////// + // Convert IT to S3M + else if(oldTypeIsIT_MPT && newTypeIsS3M) + { + if(note == NOTE_KEYOFF || note == NOTE_FADE) + note = NOTE_NOTECUT; + + switch(command) + { + case CMD_S3MCMDEX: + switch(param & 0xF0) + { + case 0x70: command = CMD_NONE; break; // No NNA / envelope control in S3M format + case 0x90: + if(param == 0x91) + { + // surround remap (this is the "official" command) + command = CMD_PANNING8; + param = 0xA4; + } else if(param == 0x90) + { + command = CMD_PANNING8; + param = 0x40; + } + break; + } + break; + + case CMD_GLOBALVOLUME: + param = (std::min(PARAM(0x80), param) + 1) / 2u; + break; + + default: + break; + } + } // End if(oldTypeIsIT_MPT && newTypeIsS3M) + + ////////////////////// + // Convert IT to XM + if(oldTypeIsIT_MPT && newTypeIsXM) + { + switch(command) + { + case CMD_VIBRATO: + // With linear slides, strength is roughly doubled. + param = (param & 0xF0) | (((param & 0x0F) + 1) / 2u); + break; + case CMD_GLOBALVOLUME: + param = (std::min(PARAM(0x80), param) + 1) / 2u; + break; + } + } // End if(oldTypeIsIT_MPT && newTypeIsXM) + + ////////////////////// + // Convert XM to IT + if(oldTypeIsXM && newTypeIsIT_MPT) + { + switch(command) + { + case CMD_VIBRATO: + // With linear slides, strength is roughly halved. + param = (param & 0xF0) | std::min(static_cast<PARAM>((param & 0x0F) * 2u), PARAM(15)); + break; + case CMD_GLOBALVOLUME: + param = std::min(PARAM(0x40), param) * 2u; + break; + } + } // End if(oldTypeIsIT_MPT && newTypeIsXM) + + /////////////////////////////////// + // MOD / XM Speed/Tempo limits + if(newTypeIsMOD_XM) + { + switch(command) + { + case CMD_SPEED: + param = std::min(param, PARAM(0x1F)); + break; + break; + case CMD_TEMPO: + param = std::max(param, PARAM(0x20)); + break; + } + } + + /////////////////////////////////////////////////////////////////////// + // Convert MOD to anything - adjust effect memory, remove Invert Loop + if(oldTypeIsMOD) + { + switch(command) + { + case CMD_TONEPORTAVOL: // lacks memory -> 500 is the same as 300 + if(param == 0x00) + command = CMD_TONEPORTAMENTO; + break; + + case CMD_VIBRATOVOL: // lacks memory -> 600 is the same as 400 + if(param == 0x00) + command = CMD_VIBRATO; + break; + + case CMD_PORTAMENTOUP: // lacks memory -> remove + case CMD_PORTAMENTODOWN: + case CMD_VOLUMESLIDE: + if(param == 0x00) + command = CMD_NONE; + break; + + case CMD_MODCMDEX: // This would turn into "Set Active Macro", so let's better remove it + case CMD_S3MCMDEX: + if((param & 0xF0) == 0xF0) + command = CMD_NONE; + break; + } + } // End if(oldTypeIsMOD && newTypeIsXM) + + ///////////////////////////////////////////////////////////////////// + // Convert anything to MOD - remove volume column, remove Set Macro + if(newTypeIsMOD) + { + // convert note off events + if(IsSpecialNote()) + { + note = NOTE_NONE; + // no effect present, so just convert note off to volume 0 + if(command == CMD_NONE) + { + command = CMD_VOLUME; + param = 0; + // EDx effect present, so convert it to ECx + } else if((command == CMD_MODCMDEX) && ((param & 0xF0) == 0xD0)) + { + param = 0xC0 | (param & 0x0F); + } + } + + if(command != CMD_NONE) switch(command) + { + case CMD_RETRIG: // MOD only has E9x + command = CMD_MODCMDEX; + param = 0x90 | (param & 0x0F); + break; + + case CMD_MODCMDEX: // This would turn into "Invert Loop", so let's better remove it + if((param & 0xF0) == 0xF0) command = CMD_NONE; + break; + } + + if(command == CMD_NONE) switch(volcmd) + { + case VOLCMD_VOLUME: + command = CMD_VOLUME; + param = vol; + break; + + case VOLCMD_PANNING: + command = CMD_PANNING8; + param = vol < 64 ? vol << 2 : 255; + break; + + case VOLCMD_VOLSLIDEDOWN: + command = CMD_VOLUMESLIDE; + param = vol; + break; + + case VOLCMD_VOLSLIDEUP: + command = CMD_VOLUMESLIDE; + param = vol << 4; + break; + + case VOLCMD_FINEVOLDOWN: + command = CMD_MODCMDEX; + param = 0xB0 | vol; + break; + + case VOLCMD_FINEVOLUP: + command = CMD_MODCMDEX; + param = 0xA0 | vol; + break; + + case VOLCMD_PORTADOWN: + command = CMD_PORTAMENTODOWN; + param = vol << 2; + break; + + case VOLCMD_PORTAUP: + command = CMD_PORTAMENTOUP; + param = vol << 2; + break; + + case VOLCMD_TONEPORTAMENTO: + command = CMD_TONEPORTAMENTO; + param = vol << 2; + break; + + case VOLCMD_VIBRATODEPTH: + command = CMD_VIBRATO; + param = vol; + break; + + case VOLCMD_VIBRATOSPEED: + command = CMD_VIBRATO; + param = vol << 4; + break; + } + volcmd = VOLCMD_NONE; + } // End if(newTypeIsMOD) + + /////////////////////////////////////////////////// + // Convert anything to S3M - adjust volume column + if(newTypeIsS3M) + { + if(command == CMD_NONE) switch(volcmd) + { + case VOLCMD_VOLSLIDEDOWN: + command = CMD_VOLUMESLIDE; + param = vol; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_VOLSLIDEUP: + command = CMD_VOLUMESLIDE; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_FINEVOLDOWN: + command = CMD_VOLUMESLIDE; + param = 0xF0 | vol; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_FINEVOLUP: + command = CMD_VOLUMESLIDE; + param = (vol << 4) | 0x0F; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PORTADOWN: + command = CMD_PORTAMENTODOWN; + param = vol << 2; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PORTAUP: + command = CMD_PORTAMENTOUP; + param = vol << 2; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_TONEPORTAMENTO: + command = CMD_TONEPORTAMENTO; + param = vol << 2; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_VIBRATODEPTH: + command = CMD_VIBRATO; + param = vol; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_VIBRATOSPEED: + command = CMD_VIBRATO; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PANSLIDELEFT: + command = CMD_PANNINGSLIDE; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PANSLIDERIGHT: + command = CMD_PANNINGSLIDE; + param = vol; + volcmd = VOLCMD_NONE; + break; + } + } // End if(newTypeIsS3M) + + //////////////////////////////////////////////////////////////////////// + // Convert anything to XM - adjust volume column, breaking EDx command + if(newTypeIsXM) + { + // remove EDx if no note is next to it, or it will retrigger the note in FT2 mode + if(command == CMD_MODCMDEX && (param & 0xF0) == 0xD0 && note == NOTE_NONE) + { + command = CMD_NONE; + param = 0; + } + + if(IsSpecialNote()) + { + // Instrument numbers next to Note Off reset instrument settings + instr = 0; + + if(command == CMD_MODCMDEX && (param & 0xF0) == 0xD0) + { + // Note Off + Note Delay does nothing when using envelopes. + note = NOTE_NONE; + command = CMD_KEYOFF; + param &= 0x0F; + } + } + + // Convert some commands which behave differently or don't exist + if(command == CMD_NONE) switch(volcmd) + { + case VOLCMD_PORTADOWN: + command = CMD_PORTAMENTODOWN; + param = vol << 2; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PORTAUP: + command = CMD_PORTAMENTOUP; + param = vol << 2; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_TONEPORTAMENTO: + command = CMD_TONEPORTAMENTO; + param = ImpulseTrackerPortaVolCmd[vol & 0x0F]; + volcmd = VOLCMD_NONE; + break; + } + } // End if(newTypeIsXM) + + /////////////////////////////////////////////////// + // Convert anything to IT - adjust volume column + if(newTypeIsIT_MPT) + { + // Convert some commands which behave differently or don't exist + if(!oldTypeIsIT_MPT && command == CMD_NONE) switch(volcmd) + { + case VOLCMD_PANSLIDELEFT: + command = CMD_PANNINGSLIDE; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_PANSLIDERIGHT: + command = CMD_PANNINGSLIDE; + param = vol; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_VIBRATOSPEED: + command = CMD_VIBRATO; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + + case VOLCMD_TONEPORTAMENTO: + command = CMD_TONEPORTAMENTO; + param = vol << 4; + volcmd = VOLCMD_NONE; + break; + } + + switch(volcmd) + { + case VOLCMD_VOLSLIDEDOWN: + case VOLCMD_VOLSLIDEUP: + case VOLCMD_FINEVOLDOWN: + case VOLCMD_FINEVOLUP: + case VOLCMD_PORTADOWN: + case VOLCMD_PORTAUP: + case VOLCMD_TONEPORTAMENTO: + case VOLCMD_VIBRATODEPTH: + // OpenMPT-specific commands + case VOLCMD_OFFSET: + vol = std::min(vol, VOL(9)); + break; + } + } // End if(newTypeIsIT_MPT) + + // Fix volume column offset for formats that don't have it. + if(volcmd == VOLCMD_OFFSET && !newSpecs.HasVolCommand(VOLCMD_OFFSET) && (command == CMD_NONE || command == CMD_OFFSET || !newSpecs.HasCommand(command))) + { + const ModCommand::PARAM oldOffset = (command == CMD_OFFSET) ? param : 0; + command = CMD_OFFSET; + volcmd = VOLCMD_NONE; + SAMPLEINDEX smp = instr; + if(smp > 0 && smp <= sndFile.GetNumInstruments() && IsNote() && sndFile.Instruments[smp] != nullptr) + smp = sndFile.Instruments[smp]->Keyboard[note - NOTE_MIN]; + + if(smp > 0 && smp <= sndFile.GetNumSamples() && vol <= std::size(ModSample().cues)) + { + const ModSample &sample = sndFile.GetSample(smp); + if(vol == 0) + param = mpt::saturate_cast<ModCommand::PARAM>(Util::muldivr_unsigned(sample.nLength, oldOffset, 65536u)); + else + param = mpt::saturate_cast<ModCommand::PARAM>((sample.cues[vol - 1] + (oldOffset * 256u) + 128u) / 256u); + } else + { + param = vol << 3; + } + } + + if((command == CMD_REVERSEOFFSET || command == CMD_OFFSETPERCENTAGE) && !newSpecs.HasCommand(command)) + { + command = CMD_OFFSET; + } + + if(!newSpecs.HasNote(note)) + note = NOTE_NONE; + + // ensure the commands really exist in this format + if(!newSpecs.HasCommand(command)) + command = CMD_NONE; + if(!newSpecs.HasVolCommand(volcmd)) + volcmd = VOLCMD_NONE; + +} + + +bool ModCommand::IsContinousCommand(const CSoundFile &sndFile) const +{ + switch(command) + { + case CMD_ARPEGGIO: + case CMD_TONEPORTAMENTO: + case CMD_VIBRATO: + case CMD_TREMOLO: + case CMD_RETRIG: + case CMD_TREMOR: + case CMD_FINEVIBRATO: + case CMD_PANBRELLO: + case CMD_SMOOTHMIDI: + case CMD_NOTESLIDEUP: + case CMD_NOTESLIDEDOWN: + case CMD_NOTESLIDEUPRETRIG: + case CMD_NOTESLIDEDOWNRETRIG: + return true; + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + if(!param && sndFile.GetType() == MOD_TYPE_MOD) + return false; + if(sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI | MOD_TYPE_STP | MOD_TYPE_DTM)) + return true; + if(param >= 0xF0) + return false; + if(param >= 0xE0 && sndFile.GetType() != MOD_TYPE_DBM) + return false; + return true; + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + if(!param && sndFile.GetType() == MOD_TYPE_MOD) + return false; + if(sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_AMF0 | MOD_TYPE_MED | MOD_TYPE_DIGI)) + return true; + if((param & 0xF0) == 0xF0 && (param & 0x0F)) + return false; + if((param & 0x0F) == 0x0F && (param & 0xF0)) + return false; + return true; + case CMD_TEMPO: + return (param < 0x20); + default: + return false; + } +} + + +bool ModCommand::IsContinousVolColCommand() const +{ + switch(volcmd) + { + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + case VOLCMD_VIBRATOSPEED: + case VOLCMD_VIBRATODEPTH: + case VOLCMD_PANSLIDELEFT: + case VOLCMD_PANSLIDERIGHT: + case VOLCMD_TONEPORTAMENTO: + case VOLCMD_PORTAUP: + case VOLCMD_PORTADOWN: + return true; + default: + return false; + } +} + + +bool ModCommand::IsSlideUpDownCommand() const +{ + switch(command) + { + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + return true; + default: + return false; + } +} + + +bool ModCommand::IsGlobalCommand(COMMAND command, PARAM param) +{ + switch(command) + { + case CMD_POSITIONJUMP: + case CMD_PATTERNBREAK: + case CMD_SPEED: + case CMD_TEMPO: + case CMD_GLOBALVOLUME: + case CMD_GLOBALVOLSLIDE: + case CMD_MIDI: + case CMD_SMOOTHMIDI: + case CMD_DBMECHO: + return true; + case CMD_MODCMDEX: + switch(param & 0xF0) + { + case 0x00: // LED Filter + case 0x60: // Pattern Loop + case 0xE0: // Row Delay + return true; + default: + return false; + } + case CMD_XFINEPORTAUPDOWN: + case CMD_S3MCMDEX: + switch(param & 0xF0) + { + case 0x60: // Tick Delay + case 0x90: // Sound Control + case 0xB0: // Pattern Loop + case 0xE0: // Row Delay + return true; + default: + return false; + } + + default: + return false; + } +} + +// "Importance" of every FX command. Table is used for importing from formats with multiple effect colums +// and is approximately the same as in SchismTracker. +size_t ModCommand::GetEffectWeight(COMMAND cmd) +{ + // Effect weights, sorted from lowest to highest weight. + static constexpr COMMAND weights[] = + { + CMD_NONE, + CMD_DUMMY, + CMD_XPARAM, + CMD_SETENVPOSITION, + CMD_KEYOFF, + CMD_TREMOLO, + CMD_FINEVIBRATO, + CMD_VIBRATO, + CMD_XFINEPORTAUPDOWN, + CMD_FINETUNE, + CMD_FINETUNE_SMOOTH, + CMD_PANBRELLO, + CMD_S3MCMDEX, + CMD_MODCMDEX, + CMD_DELAYCUT, + CMD_MIDI, + CMD_SMOOTHMIDI, + CMD_PANNINGSLIDE, + CMD_PANNING8, + CMD_NOTESLIDEUPRETRIG, + CMD_NOTESLIDEUP, + CMD_NOTESLIDEDOWNRETRIG, + CMD_NOTESLIDEDOWN, + CMD_PORTAMENTOUP, + CMD_PORTAMENTODOWN, + CMD_VOLUMESLIDE, + CMD_VIBRATOVOL, + CMD_VOLUME, + CMD_DIGIREVERSESAMPLE, + CMD_REVERSEOFFSET, + CMD_OFFSETPERCENTAGE, + CMD_OFFSET, + CMD_TREMOR, + CMD_RETRIG, + CMD_ARPEGGIO, + CMD_TONEPORTAMENTO, + CMD_TONEPORTAVOL, + CMD_DBMECHO, + CMD_GLOBALVOLSLIDE, + CMD_CHANNELVOLUME, + CMD_GLOBALVOLSLIDE, + CMD_GLOBALVOLUME, + CMD_TEMPO, + CMD_SPEED, + CMD_POSITIONJUMP, + CMD_PATTERNBREAK, + }; + static_assert(std::size(weights) == MAX_EFFECTS); + + for(size_t i = 0; i < std::size(weights); i++) + { + if(weights[i] == cmd) + { + return i; + } + } + // Invalid / unknown command. + return 0; +} + + +// Try to convert a fx column command (&effect) into a volume column command. +// Returns true if successful. +// Some commands can only be converted by losing some precision. +// If moving the command into the volume column is more important than accuracy, use force = true. +// (Code translated from SchismTracker and mainly supposed to be used with loaders ported from this tracker) +bool ModCommand::ConvertVolEffect(uint8 &effect, uint8 ¶m, bool force) +{ + switch(effect) + { + case CMD_NONE: + effect = VOLCMD_NONE; + return true; + case CMD_VOLUME: + effect = VOLCMD_VOLUME; + param = std::min(param, PARAM(64)); + break; + case CMD_PORTAMENTOUP: + // if not force, reject when dividing causes loss of data in LSB, or if the final value is too + // large to fit. (volume column Ex/Fx are four times stronger than effect column) + if(!force && ((param & 3) || param >= 0xE0)) + return false; + param /= 4; + effect = VOLCMD_PORTAUP; + break; + case CMD_PORTAMENTODOWN: + if(!force && ((param & 3) || param >= 0xE0)) + return false; + param /= 4; + effect = VOLCMD_PORTADOWN; + break; + case CMD_TONEPORTAMENTO: + if(param >= 0xF0) + { + // hack for people who can't type F twice :) + effect = VOLCMD_TONEPORTAMENTO; + param = 9; + return true; + } + for(uint8 n = 0; n < 10; n++) + { + if(force + ? (param <= ImpulseTrackerPortaVolCmd[n]) + : (param == ImpulseTrackerPortaVolCmd[n])) + { + effect = VOLCMD_TONEPORTAMENTO; + param = n; + return true; + } + } + return false; + case CMD_VIBRATO: + if(force) + param = std::min(static_cast<PARAM>(param & 0x0F), PARAM(9)); + else if((param & 0x0F) > 9 || (param & 0xF0) != 0) + return false; + param &= 0x0F; + effect = VOLCMD_VIBRATODEPTH; + break; + case CMD_FINEVIBRATO: + if(force) + param = 0; + else if(param) + return false; + effect = VOLCMD_VIBRATODEPTH; + break; + case CMD_PANNING8: + if(param == 255) + param = 64; + else + param /= 4; + effect = VOLCMD_PANNING; + break; + case CMD_VOLUMESLIDE: + if(param == 0) + return false; + if((param & 0xF) == 0) // Dx0 / Cx + { + param >>= 4; + effect = VOLCMD_VOLSLIDEUP; + } else if((param & 0xF0) == 0) // D0x / Dx + { + effect = VOLCMD_VOLSLIDEDOWN; + } else if((param & 0xF) == 0xF) // DxF / Ax + { + param >>= 4; + effect = VOLCMD_FINEVOLUP; + } else if((param & 0xF0) == 0xF0) // DFx / Bx + { + param &= 0xF; + effect = VOLCMD_FINEVOLDOWN; + } else // ??? + { + return false; + } + break; + case CMD_S3MCMDEX: + switch (param >> 4) + { + case 8: + effect = VOLCMD_PANNING; + param = ((param & 0xF) << 2) + 2; + return true; + case 0: case 1: case 2: case 0xF: + if(force) + { + effect = param = 0; + return true; + } + break; + default: + break; + } + return false; + default: + return false; + } + return true; +} + +// Try to combine two commands into one. Returns true on success and the combined command is placed in eff1 / param1. +bool ModCommand::CombineEffects(uint8 &eff1, uint8 ¶m1, uint8 &eff2, uint8 ¶m2) +{ + if(eff1 == CMD_VOLUMESLIDE && (eff2 == CMD_VIBRATO || eff2 == CMD_TONEPORTAVOL) && param2 == 0) + { + // Merge commands + if(eff2 == CMD_VIBRATO) + { + eff1 = CMD_VIBRATOVOL; + } else + { + eff1 = CMD_TONEPORTAVOL; + } + eff2 = CMD_NONE; + return true; + } else if(eff2 == CMD_VOLUMESLIDE && (eff1 == CMD_VIBRATO || eff1 == CMD_TONEPORTAVOL) && param1 == 0) + { + // Merge commands + if(eff1 == CMD_VIBRATO) + { + eff1 = CMD_VIBRATOVOL; + } else + { + eff1 = CMD_TONEPORTAVOL; + } + param1 = param2; + eff2 = CMD_NONE; + return true; + } else if(eff1 == CMD_OFFSET && eff2 == CMD_S3MCMDEX && param2 == 0x9F) + { + // Reverse offset + eff1 = CMD_REVERSEOFFSET; + eff2 = CMD_NONE; + return true; + } else if(eff1 == CMD_S3MCMDEX && param1 == 0x9F && eff2 == CMD_OFFSET) + { + // Reverse offset + eff1 = CMD_REVERSEOFFSET; + param1 = param2; + eff2 = CMD_NONE; + return true; + } else + { + return false; + } +} + + +std::pair<EffectCommand, ModCommand::PARAM> ModCommand::TwoRegularCommandsToMPT(uint8 &effect1, uint8 ¶m1, uint8 &effect2, uint8 ¶m2) +{ + for(uint8 n = 0; n < 4; n++) + { + if(ModCommand::ConvertVolEffect(effect1, param1, (n > 1))) + { + return {CMD_NONE, ModCommand::PARAM(0)}; + } + std::swap(effect1, effect2); + std::swap(param1, param2); + } + + // Can only keep one command :( + if(GetEffectWeight(static_cast<COMMAND>(effect1)) > GetEffectWeight(static_cast<COMMAND>(effect2))) + { + std::swap(effect1, effect2); + std::swap(param1, param2); + } + std::pair<EffectCommand, PARAM> lostCommand = {static_cast<EffectCommand>(effect1), param1}; + effect1 = VOLCMD_NONE; + param1 = 0; + return lostCommand; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.h b/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.h new file mode 100644 index 00000000..bf9049a9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/modcommand.h @@ -0,0 +1,235 @@ +/* + * modcommand.h + * ------------ + * Purpose: ModCommand declarations and helpers. One ModCommand corresponds to one pattern cell. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" +#include "../common/misc_util.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +// Note definitions +enum : uint8 // ModCommand::NOTE +{ + NOTE_NONE = 0, // Empty note cell + NOTE_MIN = 1, // Minimum note value + NOTE_MAX = 120, // Maximum note value + NOTE_MIDDLEC = (5 * 12 + NOTE_MIN), + NOTE_KEYOFF = 0xFF, // === (Note Off, releases envelope / fades samples, stops plugin note) + NOTE_NOTECUT = 0xFE, // ^^^ (Cuts sample / stops all plugin notes) + NOTE_FADE = 0xFD, // ~~~ (Fades samples, stops plugin note) + NOTE_PC = 0xFC, // Param Control 'note'. Changes param value on first tick. + NOTE_PCS = 0xFB, // Param Control (Smooth) 'note'. Interpolates param value during the whole row. + NOTE_MIN_SPECIAL = NOTE_PCS, + NOTE_MAX_SPECIAL = NOTE_KEYOFF, +}; + + +// Volume Column commands +enum VolumeCommand : uint8 +{ + VOLCMD_NONE = 0, + VOLCMD_VOLUME = 1, + VOLCMD_PANNING = 2, + VOLCMD_VOLSLIDEUP = 3, + VOLCMD_VOLSLIDEDOWN = 4, + VOLCMD_FINEVOLUP = 5, + VOLCMD_FINEVOLDOWN = 6, + VOLCMD_VIBRATOSPEED = 7, + VOLCMD_VIBRATODEPTH = 8, + VOLCMD_PANSLIDELEFT = 9, + VOLCMD_PANSLIDERIGHT = 10, + VOLCMD_TONEPORTAMENTO = 11, + VOLCMD_PORTAUP = 12, + VOLCMD_PORTADOWN = 13, + VOLCMD_PLAYCONTROL = 14, + VOLCMD_OFFSET = 15, + MAX_VOLCMDS +}; + + +// Effect column commands +enum EffectCommand : uint8 +{ + CMD_NONE = 0, + CMD_ARPEGGIO = 1, + CMD_PORTAMENTOUP = 2, + CMD_PORTAMENTODOWN = 3, + CMD_TONEPORTAMENTO = 4, + CMD_VIBRATO = 5, + CMD_TONEPORTAVOL = 6, + CMD_VIBRATOVOL = 7, + CMD_TREMOLO = 8, + CMD_PANNING8 = 9, + CMD_OFFSET = 10, + CMD_VOLUMESLIDE = 11, + CMD_POSITIONJUMP = 12, + CMD_VOLUME = 13, + CMD_PATTERNBREAK = 14, + CMD_RETRIG = 15, + CMD_SPEED = 16, + CMD_TEMPO = 17, + CMD_TREMOR = 18, + CMD_MODCMDEX = 19, + CMD_S3MCMDEX = 20, + CMD_CHANNELVOLUME = 21, + CMD_CHANNELVOLSLIDE = 22, + CMD_GLOBALVOLUME = 23, + CMD_GLOBALVOLSLIDE = 24, + CMD_KEYOFF = 25, + CMD_FINEVIBRATO = 26, + CMD_PANBRELLO = 27, + CMD_XFINEPORTAUPDOWN = 28, + CMD_PANNINGSLIDE = 29, + CMD_SETENVPOSITION = 30, + CMD_MIDI = 31, + CMD_SMOOTHMIDI = 32, + CMD_DELAYCUT = 33, + CMD_XPARAM = 34, + CMD_FINETUNE = 35, + CMD_FINETUNE_SMOOTH = 36, + CMD_DUMMY = 37, + CMD_NOTESLIDEUP = 38, // IMF Gxy / PTM Jxy (Slide y notes up every x ticks) + CMD_NOTESLIDEDOWN = 39, // IMF Hxy / PTM Kxy (Slide y notes down every x ticks) + CMD_NOTESLIDEUPRETRIG = 40, // PTM Lxy (Slide y notes up every x ticks + retrigger note) + CMD_NOTESLIDEDOWNRETRIG = 41, // PTM Mxy (Slide y notes down every x ticks + retrigger note) + CMD_REVERSEOFFSET = 42, // PTM Nxx Revert sample + offset + CMD_DBMECHO = 43, // DBM enable/disable echo + CMD_OFFSETPERCENTAGE = 44, // PLM Percentage Offset + CMD_DIGIREVERSESAMPLE = 45, // DIGI reverse sample + MAX_EFFECTS +}; + + +enum EffectType : uint8 +{ + EFFECT_TYPE_NORMAL = 0, + EFFECT_TYPE_GLOBAL = 1, + EFFECT_TYPE_VOLUME = 2, + EFFECT_TYPE_PANNING = 3, + EFFECT_TYPE_PITCH = 4, + MAX_EFFECT_TYPE = 5 +}; + + +class ModCommand +{ +public: + using NOTE = uint8; + using INSTR = uint8; + using VOL = uint8; + using VOLCMD = uint8; + using COMMAND = uint8; + using PARAM = uint8; + + // Defines the maximum value for column data when interpreted as 2-byte value + // (for example volcmd and vol). The valid value range is [0, maxColumnValue]. + static constexpr int maxColumnValue = 999; + + // Returns empty modcommand. + static ModCommand Empty() { return ModCommand(); } + + bool operator==(const ModCommand &mc) const + { + return (note == mc.note) + && (instr == mc.instr) + && (volcmd == mc.volcmd) + && (command == mc.command) + && ((volcmd == VOLCMD_NONE && !IsPcNote()) || vol == mc.vol) + && ((command == CMD_NONE && !IsPcNote()) || param == mc.param); + } + bool operator!=(const ModCommand& mc) const { return !(*this == mc); } + + void Set(NOTE n, INSTR ins, uint16 volcol, uint16 effectcol) { note = n; instr = ins; SetValueVolCol(volcol); SetValueEffectCol(effectcol); } + + uint16 GetValueVolCol() const { return GetValueVolCol(volcmd, vol); } + static uint16 GetValueVolCol(uint8 volcmd, uint8 vol) { return (volcmd << 8) + vol; } + void SetValueVolCol(const uint16 val) { volcmd = static_cast<VOLCMD>(val >> 8); vol = static_cast<uint8>(val & 0xFF); } + + uint16 GetValueEffectCol() const { return GetValueEffectCol(command, param); } + static uint16 GetValueEffectCol(uint8 command, uint8 param) { return (command << 8) + param; } + void SetValueEffectCol(const uint16 val) { command = static_cast<COMMAND>(val >> 8); param = static_cast<uint8>(val & 0xFF); } + + // Clears modcommand. + void Clear() { memset(this, 0, sizeof(ModCommand)); } + + // Returns true if modcommand is empty, false otherwise. + bool IsEmpty() const + { + return (note == NOTE_NONE && instr == 0 && volcmd == VOLCMD_NONE && command == CMD_NONE); + } + + // Returns true if instrument column represents plugin index. + bool IsInstrPlug() const { return IsPcNote(); } + + // Returns true if and only if note is NOTE_PC or NOTE_PCS. + bool IsPcNote() const { return IsPcNote(note); } + static bool IsPcNote(NOTE note) { return note == NOTE_PC || note == NOTE_PCS; } + + // Returns true if and only if note is a valid musical note. + bool IsNote() const { return mpt::is_in_range(note, NOTE_MIN, NOTE_MAX); } + static bool IsNote(NOTE note) { return mpt::is_in_range(note, NOTE_MIN, NOTE_MAX); } + // Returns true if and only if note is a valid special note. + bool IsSpecialNote() const { return mpt::is_in_range(note, NOTE_MIN_SPECIAL, NOTE_MAX_SPECIAL); } + static bool IsSpecialNote(NOTE note) { return mpt::is_in_range(note, NOTE_MIN_SPECIAL, NOTE_MAX_SPECIAL); } + // Returns true if and only if note is a valid musical note or the note entry is empty. + bool IsNoteOrEmpty() const { return note == NOTE_NONE || IsNote(); } + static bool IsNoteOrEmpty(NOTE note) { return note == NOTE_NONE || IsNote(note); } + // Returns true if any of the commands in this cell trigger a tone portamento. + bool IsPortamento() const { return command == CMD_TONEPORTAMENTO || command == CMD_TONEPORTAVOL || volcmd == VOLCMD_TONEPORTAMENTO; } + // Returns true if the cell contains a sliding or otherwise continuous effect command. + bool IsContinousCommand(const CSoundFile &sndFile) const; + bool IsContinousVolColCommand() const; + // Returns true if the cell contains a sliding command with separate up/down nibbles. + bool IsSlideUpDownCommand() const; + // Returns true if the cell contains an effect command that may affect the global state of the module. + bool IsGlobalCommand() const { return IsGlobalCommand(command, param); } + static bool IsGlobalCommand(COMMAND command, PARAM param); + + // Returns true if the note is inside the Amiga frequency range + bool IsAmigaNote() const { return IsAmigaNote(note); } + static bool IsAmigaNote(NOTE note) { return !IsNote(note) || (note >= NOTE_MIDDLEC - 12 && note < NOTE_MIDDLEC + 24); } + + static EffectType GetEffectType(COMMAND cmd); + EffectType GetEffectType() const { return GetEffectType(command); } + static EffectType GetVolumeEffectType(VOLCMD volcmd); + EffectType GetVolumeEffectType() const { return GetVolumeEffectType(volcmd); } + + // Convert a complete ModCommand item from one format to another + void Convert(MODTYPE fromType, MODTYPE toType, const CSoundFile &sndFile); + // Convert MOD/XM Exx to S3M/IT Sxx + void ExtendedMODtoS3MEffect(); + // Convert S3M/IT Sxx to MOD/XM Exx + void ExtendedS3MtoMODEffect(); + + // "Importance" of every FX command. Table is used for importing from formats with multiple effect columns + // and is approximately the same as in SchismTracker. + static size_t GetEffectWeight(COMMAND cmd); + // Try to convert a an effect into a volume column effect. Returns true on success. + static bool ConvertVolEffect(uint8 &effect, uint8 ¶m, 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). + static std::pair<EffectCommand, PARAM> TwoRegularCommandsToMPT(uint8 &effect1, uint8 ¶m1, uint8 &effect2, uint8 ¶m2); + // Try to combine two commands into one. Returns true on success and the combined command is placed in eff1 / param1. + static bool CombineEffects(uint8 &eff1, uint8 ¶m1, uint8 &eff2, uint8 ¶m2); + +public: + uint8 note = NOTE_NONE; + uint8 instr = 0; + uint8 volcmd = VOLCMD_NONE; + uint8 command = CMD_NONE; + uint8 vol = 0; + uint8 param = 0; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.cpp new file mode 100644 index 00000000..562d7fb6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.cpp @@ -0,0 +1,430 @@ +/* + * modsmp_ctrl.cpp + * --------------- + * Purpose: Basic sample editing code. + * Notes : This is a legacy namespace. Some of this stuff is not required in libopenmpt (but stuff in soundlib/ still depends on it). The rest could be merged into struct ModSample. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "modsmp_ctrl.h" +#include "AudioCriticalSection.h" +#include "Sndfile.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace ctrlSmp +{ + +void ReplaceSample(ModSample &smp, void *pNewSample, const SmpLength newLength, CSoundFile &sndFile) +{ + void * const pOldSmp = smp.samplev(); + FlagSet<ChannelFlags> setFlags, resetFlags; + + setFlags.set(CHN_16BIT, smp.uFlags[CHN_16BIT]); + resetFlags.set(CHN_16BIT, !smp.uFlags[CHN_16BIT]); + + setFlags.set(CHN_STEREO, smp.uFlags[CHN_STEREO]); + resetFlags.set(CHN_STEREO, !smp.uFlags[CHN_STEREO]); + + CriticalSection cs; + + ctrlChn::ReplaceSample(sndFile, smp, pNewSample, newLength, setFlags, resetFlags); + smp.pData.pSample = pNewSample; + smp.nLength = newLength; + ModSample::FreeSample(pOldSmp); +} + + +// Propagate loop point changes to player +bool UpdateLoopPoints(const ModSample &smp, CSoundFile &sndFile) +{ + if(!smp.HasSampleData()) + return false; + + CriticalSection cs; + + // Update channels with new loop values + for(auto &chn : sndFile.m_PlayState.Chn) if((chn.pModSample == &smp) && chn.nLength != 0) + { + bool looped = false, bidi = false; + + if(smp.nSustainStart < smp.nSustainEnd && smp.nSustainEnd <= smp.nLength && smp.uFlags[CHN_SUSTAINLOOP] && !chn.dwFlags[CHN_KEYOFF]) + { + // Sustain loop is active + chn.nLoopStart = smp.nSustainStart; + chn.nLoopEnd = smp.nSustainEnd; + chn.nLength = smp.nSustainEnd; + looped = true; + bidi = smp.uFlags[CHN_PINGPONGSUSTAIN]; + } else if(smp.nLoopStart < smp.nLoopEnd && smp.nLoopEnd <= smp.nLength && smp.uFlags[CHN_LOOP]) + { + // Normal loop is active + chn.nLoopStart = smp.nLoopStart; + chn.nLoopEnd = smp.nLoopEnd; + chn.nLength = smp.nLoopEnd; + looped = true; + bidi = smp.uFlags[CHN_PINGPONGLOOP]; + } + chn.dwFlags.set(CHN_LOOP, looped); + chn.dwFlags.set(CHN_PINGPONGLOOP, looped && bidi); + + if(chn.position.GetUInt() > chn.nLength) + { + chn.position.Set(chn.nLoopStart); + chn.dwFlags.reset(CHN_PINGPONGFLAG); + } + if(!bidi) + { + chn.dwFlags.reset(CHN_PINGPONGFLAG); + } + if(!looped) + { + chn.nLength = smp.nLength; + } + } + + return true; +} + + +template <class T> +static void ReverseSampleImpl(T *pStart, const SmpLength length) +{ + for(SmpLength i = 0; i < length / 2; i++) + { + std::swap(pStart[i], pStart[length - 1 - i]); + } +} + +// Reverse sample data +bool ReverseSample(ModSample &smp, SmpLength start, SmpLength end, CSoundFile &sndFile) +{ + if(!smp.HasSampleData()) return false; + if(end == 0 || start > smp.nLength || end > smp.nLength) + { + start = 0; + end = smp.nLength; + } + + if(end - start < 2) return false; + + static_assert(MaxSamplingPointSize <= 4); + if(smp.GetBytesPerSample() == 4) // 16 bit stereo + ReverseSampleImpl(static_cast<int32 *>(smp.samplev()) + start, end - start); + else if(smp.GetBytesPerSample() == 2) // 16 bit mono / 8 bit stereo + ReverseSampleImpl(static_cast<int16 *>(smp.samplev()) + start, end - start); + else if(smp.GetBytesPerSample() == 1) // 8 bit mono + ReverseSampleImpl(static_cast<int8 *>(smp.samplev()) + start, end - start); + else + return false; + + smp.PrecomputeLoops(sndFile, false); + return true; +} + + +template <class T> +static void InvertSampleImpl(T *pStart, const SmpLength length) +{ + for(SmpLength i = 0; i < length; i++) + { + pStart[i] = ~pStart[i]; + } +} + +// Invert sample data (flip by 180 degrees) +bool InvertSample(ModSample &smp, SmpLength start, SmpLength end, CSoundFile &sndFile) +{ + if(!smp.HasSampleData()) return false; + if(end == 0 || start > smp.nLength || end > smp.nLength) + { + start = 0; + end = smp.nLength; + } + start *= smp.GetNumChannels(); + end *= smp.GetNumChannels(); + if(smp.GetElementarySampleSize() == 2) + InvertSampleImpl(smp.sample16() + start, end - start); + else if(smp.GetElementarySampleSize() == 1) + InvertSampleImpl(smp.sample8() + start, end - start); + else + return false; + + smp.PrecomputeLoops(sndFile, false); + return true; +} + + +template <class T> +static void XFadeSampleImpl(const T *srcIn, const T *srcOut, T *output, const SmpLength fadeLength, double e) +{ + const double length = 1.0 / static_cast<double>(fadeLength); + for(SmpLength i = 0; i < fadeLength; i++, srcIn++, srcOut++, output++) + { + double fact1 = std::pow(i * length, e); + double fact2 = std::pow((fadeLength - i) * length, e); + int32 val = static_cast<int32>( + static_cast<double>(*srcIn) * fact1 + + static_cast<double>(*srcOut) * fact2); + *output = mpt::saturate_cast<T>(val); + } +} + +// X-Fade sample data to create smooth loop transitions +bool XFadeSample(ModSample &smp, SmpLength fadeLength, int fadeLaw, bool afterloopFade, bool useSustainLoop, CSoundFile &sndFile) +{ + if(!smp.HasSampleData()) return false; + const SmpLength loopStart = useSustainLoop ? smp.nSustainStart : smp.nLoopStart; + const SmpLength loopEnd = useSustainLoop ? smp.nSustainEnd : smp.nLoopEnd; + + if(loopEnd <= loopStart || loopEnd > smp.nLength) return false; + if(loopStart < fadeLength) return false; + + const SmpLength start = (loopStart - fadeLength) * smp.GetNumChannels(); + const SmpLength end = (loopEnd - fadeLength) * smp.GetNumChannels(); + const SmpLength afterloopStart = loopStart * smp.GetNumChannels(); + const SmpLength afterloopEnd = loopEnd * smp.GetNumChannels(); + const SmpLength afterLoopLength = std::min(smp.nLength - loopEnd, fadeLength) * smp.GetNumChannels(); + fadeLength *= smp.GetNumChannels(); + + // e=0.5: constant power crossfade (for uncorrelated samples), e=1.0: constant volume crossfade (for perfectly correlated samples) + const double e = 1.0 - fadeLaw / 200000.0; + + if(smp.GetElementarySampleSize() == 2) + { + XFadeSampleImpl(smp.sample16() + start, smp.sample16() + end, smp.sample16() + end, fadeLength, e); + if(afterloopFade) XFadeSampleImpl(smp.sample16() + afterloopEnd, smp.sample16() + afterloopStart, smp.sample16() + afterloopEnd, afterLoopLength, e); + } else if(smp.GetElementarySampleSize() == 1) + { + XFadeSampleImpl(smp.sample8() + start, smp.sample8() + end, smp.sample8() + end, fadeLength, e); + if(afterloopFade) XFadeSampleImpl(smp.sample8() + afterloopEnd, smp.sample8() + afterloopStart, smp.sample8() + afterloopEnd, afterLoopLength, e); + } else + return false; + + smp.PrecomputeLoops(sndFile, true); + return true; +} + + +template <class T> +static void ConvertStereoToMonoMixImpl(T *pDest, const SmpLength length) +{ + const T *pEnd = pDest + length; + for(T *pSource = pDest; pDest != pEnd; pDest++, pSource += 2) + { + *pDest = static_cast<T>(mpt::rshift_signed(pSource[0] + pSource[1] + 1, 1)); + } +} + + +template <class T> +static void ConvertStereoToMonoOneChannelImpl(T *pDest, const T *pSource, const SmpLength length) +{ + for(const T *pEnd = pDest + length; pDest != pEnd; pDest++, pSource += 2) + { + *pDest = *pSource; + } +} + + +// Convert a multichannel sample to mono (currently only implemented for stereo) +bool ConvertToMono(ModSample &smp, CSoundFile &sndFile, StereoToMonoMode conversionMode) +{ + if(!smp.HasSampleData() || smp.GetNumChannels() != 2) return false; + + // Note: Sample is overwritten in-place! Unused data is not deallocated! + if(conversionMode == mixChannels) + { + if(smp.GetElementarySampleSize() == 2) + ConvertStereoToMonoMixImpl(smp.sample16(), smp.nLength); + else if(smp.GetElementarySampleSize() == 1) + ConvertStereoToMonoMixImpl(smp.sample8(), smp.nLength); + else + return false; + } else + { + if(conversionMode == splitSample) + { + conversionMode = onlyLeft; + } + if(smp.GetElementarySampleSize() == 2) + ConvertStereoToMonoOneChannelImpl(smp.sample16(), smp.sample16() + (conversionMode == onlyLeft ? 0 : 1), smp.nLength); + else if(smp.GetElementarySampleSize() == 1) + ConvertStereoToMonoOneChannelImpl(smp.sample8(), smp.sample8() + (conversionMode == onlyLeft ? 0 : 1), smp.nLength); + else + return false; + } + + CriticalSection cs; + smp.uFlags.reset(CHN_STEREO); + for(auto &chn : sndFile.m_PlayState.Chn) + { + if(chn.pModSample == &smp) + { + chn.dwFlags.reset(CHN_STEREO); + } + } + + smp.PrecomputeLoops(sndFile, false); + return true; +} + + +template <class T> +static void SplitStereoImpl(void *destL, void *destR, const T *source, SmpLength length) +{ + T *l = static_cast<T *>(destL), *r = static_cast<T*>(destR); + while(length--) + { + *(l++) = source[0]; + *(r++) = source[1]; + source += 2; + } +} + + +// Converts a stereo sample into two mono samples. Source sample will not be deleted. +bool SplitStereo(const ModSample &source, ModSample &left, ModSample &right, CSoundFile &sndFile) +{ + if(!source.HasSampleData() || source.GetNumChannels() != 2 || &left == &right) + return false; + const bool sourceIsLeft = &left == &source, sourceIsRight = &right == &source; + if(left.HasSampleData() && !sourceIsLeft) + return false; + if(right.HasSampleData() && !sourceIsRight) + return false; + + void *leftData = sourceIsLeft ? left.samplev() : ModSample::AllocateSample(source.nLength, source.GetElementarySampleSize()); + void *rightData = sourceIsRight ? right.samplev() : ModSample::AllocateSample(source.nLength, source.GetElementarySampleSize()); + if(!leftData || !rightData) + { + if(!sourceIsLeft) + ModSample::FreeSample(leftData); + if(!sourceIsRight) + ModSample::FreeSample(rightData); + return false; + } + + if(source.GetElementarySampleSize() == 2) + SplitStereoImpl(leftData, rightData, source.sample16(), source.nLength); + else if(source.GetElementarySampleSize() == 1) + SplitStereoImpl(leftData, rightData, source.sample8(), source.nLength); + else + MPT_ASSERT_NOTREACHED(); + + CriticalSection cs; + left = source; + left.uFlags.reset(CHN_STEREO); + left.pData.pSample = leftData; + + right = source; + right.uFlags.reset(CHN_STEREO); + right.pData.pSample = rightData; + + for(auto &chn : sndFile.m_PlayState.Chn) + { + if(chn.pModSample == &left || chn.pModSample == &right) + chn.dwFlags.reset(CHN_STEREO); + } + + left.PrecomputeLoops(sndFile, false); + right.PrecomputeLoops(sndFile, false); + return true; +} + + +template <class T> +static void ConvertMonoToStereoImpl(const T *MPT_RESTRICT src, T *MPT_RESTRICT dst, SmpLength length) +{ + while(length--) + { + dst[0] = *src; + dst[1] = *src; + dst += 2; + src++; + } +} + + +// Convert a multichannel sample to mono (currently only implemented for stereo) +bool ConvertToStereo(ModSample &smp, CSoundFile &sndFile) +{ + if(!smp.HasSampleData() || smp.GetNumChannels() != 1) return false; + + void *newSample = ModSample::AllocateSample(smp.nLength, smp.GetBytesPerSample() * 2); + if(newSample == nullptr) + { + return 0; + } + + if(smp.GetElementarySampleSize() == 2) + ConvertMonoToStereoImpl(smp.sample16(), (int16 *)newSample, smp.nLength); + else if(smp.GetElementarySampleSize() == 1) + ConvertMonoToStereoImpl(smp.sample8(), (int8 *)newSample, smp.nLength); + else + return false; + + CriticalSection cs; + smp.uFlags.set(CHN_STEREO); + ReplaceSample(smp, newSample, smp.nLength, sndFile); + + smp.PrecomputeLoops(sndFile, false); + return true; +} + + +} // namespace ctrlSmp + + + +namespace ctrlChn +{ + +void ReplaceSample( CSoundFile &sndFile, + const ModSample &sample, + const void * const pNewSample, + const SmpLength newLength, + FlagSet<ChannelFlags> setFlags, + FlagSet<ChannelFlags> resetFlags) +{ + const bool periodIsFreq = sndFile.PeriodsAreFrequencies(); + for(auto &chn : sndFile.m_PlayState.Chn) + { + if(chn.pModSample == &sample) + { + if(chn.pCurrentSample != nullptr) + chn.pCurrentSample = pNewSample; + if(chn.position.GetUInt() > newLength) + chn.position.Set(0); + if(chn.nLength > 0) + LimitMax(chn.nLength, newLength); + if(chn.InSustainLoop()) + { + chn.nLoopStart = sample.nSustainStart; + chn.nLoopEnd = sample.nSustainEnd; + } else + { + chn.nLoopStart = sample.nLoopStart; + chn.nLoopEnd = sample.nLoopEnd; + } + chn.dwFlags.set(setFlags); + chn.dwFlags.reset(resetFlags); + if(chn.nC5Speed && sample.nC5Speed && !sndFile.UseFinetuneAndTranspose()) + { + if(periodIsFreq) + chn.nPeriod = Util::muldivr_unsigned(chn.nPeriod, sample.nC5Speed, chn.nC5Speed); + else + chn.nPeriod = Util::muldivr_unsigned(chn.nPeriod, chn.nC5Speed, sample.nC5Speed); + } + chn.nC5Speed = sample.nC5Speed; + } + } +} + +} // namespace ctrlChn + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.h b/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.h new file mode 100644 index 00000000..0111b564 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/modsmp_ctrl.h @@ -0,0 +1,75 @@ +/* + * modsmp_ctrl.h + * ------------- + * Purpose: Basic sample editing code + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; +struct ModSample; +struct ModChannel; + +namespace ctrlSmp +{ + +// Replaces sample in 'smp' with given sample and frees the old sample. +void ReplaceSample(ModSample &smp, void *pNewSample, const SmpLength newLength, CSoundFile &sndFile); + +// Propagate loop point changes to player +bool UpdateLoopPoints(const ModSample &smp, CSoundFile &sndFile); + +// Reverse sample data +bool ReverseSample(ModSample &smp, SmpLength start, SmpLength end, CSoundFile &sndFile); + +// Invert sample data (flip by 180 degrees) +bool InvertSample(ModSample &smp, SmpLength start, SmpLength end, CSoundFile &sndFile); + +// Crossfade sample data to create smooth loops +bool XFadeSample(ModSample &smp, SmpLength fadeLength, int fadeLaw, bool afterloopFade, bool useSustainLoop, CSoundFile &sndFile); + +enum StereoToMonoMode +{ + mixChannels, + onlyLeft, + onlyRight, + splitSample, +}; + +// Convert a sample with any number of channels to mono +bool ConvertToMono(ModSample &smp, CSoundFile &sndFile, StereoToMonoMode conversionMode); + +// Converts a stereo sample into two mono samples. Source sample will not be deleted. +// Either of the two target samples may be identical to the source sample. +bool SplitStereo(const ModSample &source, ModSample &left, ModSample &right, CSoundFile &sndFile); + +// Convert a mono sample to stereo +bool ConvertToStereo(ModSample &smp, CSoundFile &sndFile); + +} // Namespace ctrlSmp + +namespace ctrlChn +{ + +// Replaces sample from sound channels by given sample. +void ReplaceSample( CSoundFile &sndFile, + const ModSample &sample, + const void * const pNewSample, + const SmpLength newLength, + FlagSet<ChannelFlags> setFlags, + FlagSet<ChannelFlags> resetFlags); + +} // namespace ctrlChn + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/opal.h b/Src/external_dependencies/openmpt-trunk/soundlib/opal.h new file mode 100644 index 00000000..760cda20 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/opal.h @@ -0,0 +1,1345 @@ +// This is the Opal OPL3 emulator from Reality Adlib Tracker v2.0a (http://www.3eality.com/productions/reality-adlib-tracker). +// It was released by Shayde/Reality into the public domain. +// Minor modifications to silence some warnings and fix a bug in the envelope generator have been applied. +// Additional fixes by JP Cimalando. + +/* + + The Opal OPL3 emulator. + + Note: this is not a complete emulator, just enough for Reality Adlib Tracker tunes. + + Missing features compared to a real OPL3: + + - Timers/interrupts + - OPL3 enable bit (it defaults to always on) + - CSW mode + - Test register + - Percussion mode + +*/ + + + +#include <cstdint> + + + +//================================================================================================== +// Opal class. +//================================================================================================== +class Opal { + + class Channel; + + // Various constants + enum { + OPL3SampleRate = 49716, + NumChannels = 18, + NumOperators = 36, + + EnvOff = -1, + EnvAtt, + EnvDec, + EnvSus, + EnvRel, + }; + + // A single FM operator + class Operator { + + public: + Operator(); + void SetMaster(Opal *opal) { Master = opal; } + void SetChannel(Channel *chan) { Chan = chan; } + + int16_t Output(uint16_t keyscalenum, uint32_t phase_step, int16_t vibrato, int16_t mod = 0, int16_t fbshift = 0); + + void SetKeyOn(bool on); + void SetTremoloEnable(bool on); + void SetVibratoEnable(bool on); + void SetSustainMode(bool on); + void SetEnvelopeScaling(bool on); + void SetFrequencyMultiplier(uint16_t scale); + void SetKeyScale(uint16_t scale); + void SetOutputLevel(uint16_t level); + void SetAttackRate(uint16_t rate); + void SetDecayRate(uint16_t rate); + void SetSustainLevel(uint16_t level); + void SetReleaseRate(uint16_t rate); + void SetWaveform(uint16_t wave); + + void ComputeRates(); + void ComputeKeyScaleLevel(); + + protected: + Opal * Master; // Master object + Channel * Chan; // Owning channel + uint32_t Phase; // The current offset in the selected waveform + uint16_t Waveform; // The waveform id this operator is using + uint16_t FreqMultTimes2; // Frequency multiplier * 2 + int EnvelopeStage; // Which stage the envelope is at (see Env* enums above) + int16_t EnvelopeLevel; // 0 - $1FF, 0 being the loudest + uint16_t OutputLevel; // 0 - $FF + uint16_t AttackRate; + uint16_t DecayRate; + uint16_t SustainLevel; + uint16_t ReleaseRate; + uint16_t AttackShift; + uint16_t AttackMask; + uint16_t AttackAdd; + const uint16_t *AttackTab; + uint16_t DecayShift; + uint16_t DecayMask; + uint16_t DecayAdd; + const uint16_t *DecayTab; + uint16_t ReleaseShift; + uint16_t ReleaseMask; + uint16_t ReleaseAdd; + const uint16_t *ReleaseTab; + uint16_t KeyScaleShift; + uint16_t KeyScaleLevel; + int16_t Out[2]; + bool KeyOn; + bool KeyScaleRate; // Affects envelope rate scaling + bool SustainMode; // Whether to sustain during the sustain phase, or release instead + bool TremoloEnable; + bool VibratoEnable; + }; + + // A single channel, which can contain two or more operators + class Channel { + + public: + Channel(); + void SetMaster(Opal *opal) { Master = opal; } + void SetOperators(Operator *a, Operator *b, Operator *c, Operator *d) { + Op[0] = a; + Op[1] = b; + Op[2] = c; + Op[3] = d; + if (a) + a->SetChannel(this); + if (b) + b->SetChannel(this); + if (c) + c->SetChannel(this); + if (d) + d->SetChannel(this); + } + + void Output(int16_t &left, int16_t &right); + void SetEnable(bool on) { Enable = on; } + void SetChannelPair(Channel *pair) { ChannelPair = pair; } + + void SetFrequencyLow(uint16_t freq); + void SetFrequencyHigh(uint16_t freq); + void SetKeyOn(bool on); + void SetOctave(uint16_t oct); + void SetLeftEnable(bool on); + void SetRightEnable(bool on); + void SetFeedback(uint16_t val); + void SetModulationType(uint16_t type); + + uint16_t GetFreq() const { return Freq; } + uint16_t GetOctave() const { return Octave; } + uint16_t GetKeyScaleNumber() const { return KeyScaleNumber; } + uint16_t GetModulationType() const { return ModulationType; } + Channel * GetChannelPair() const { return ChannelPair; } + + void ComputeKeyScaleNumber(); + + protected: + void ComputePhaseStep(); + + Operator * Op[4]; + + Opal * Master; // Master object + uint16_t Freq; // Frequency; actually it's a phase stepping value + uint16_t Octave; // Also known as "block" in Yamaha parlance + uint32_t PhaseStep; + uint16_t KeyScaleNumber; + uint16_t FeedbackShift; + uint16_t ModulationType; + Channel * ChannelPair; + bool Enable; + bool LeftEnable, RightEnable; + }; + + public: + Opal(int sample_rate); + Opal(const Opal &) = delete; + Opal(Opal &&) = delete; + ~Opal(); + + void SetSampleRate(int sample_rate); + void Port(uint16_t reg_num, uint8_t val); + void Sample(int16_t *left, int16_t *right); + + protected: + void Init(int sample_rate); + void Output(int16_t &left, int16_t &right); + + int32_t SampleRate; + int32_t SampleAccum; + int16_t LastOutput[2], CurrOutput[2]; + Channel Chan[NumChannels]; + Operator Op[NumOperators]; +// uint16_t ExpTable[256]; +// uint16_t LogSinTable[256]; + uint16_t Clock; + uint16_t TremoloClock; + uint16_t TremoloLevel; + uint16_t VibratoTick; + uint16_t VibratoClock; + bool NoteSel; + bool TremoloDepth; + bool VibratoDepth; + + static const uint16_t RateTables[4][8]; + static const uint16_t ExpTable[256]; + static const uint16_t LogSinTable[256]; +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::RateTables[4][8] = { + { 1, 0, 1, 0, 1, 0, 1, 0 }, + { 1, 0, 1, 0, 0, 0, 1, 0 }, + { 1, 0, 0, 0, 1, 0, 0, 0 }, + { 1, 0, 0, 0, 0, 0, 0, 0 }, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::ExpTable[0x100] = { + 1018, 1013, 1007, 1002, 996, 991, 986, 980, 975, 969, 964, 959, 953, 948, 942, 937, + 932, 927, 921, 916, 911, 906, 900, 895, 890, 885, 880, 874, 869, 864, 859, 854, + 849, 844, 839, 834, 829, 824, 819, 814, 809, 804, 799, 794, 789, 784, 779, 774, + 770, 765, 760, 755, 750, 745, 741, 736, 731, 726, 722, 717, 712, 708, 703, 698, + 693, 689, 684, 680, 675, 670, 666, 661, 657, 652, 648, 643, 639, 634, 630, 625, + 621, 616, 612, 607, 603, 599, 594, 590, 585, 581, 577, 572, 568, 564, 560, 555, + 551, 547, 542, 538, 534, 530, 526, 521, 517, 513, 509, 505, 501, 496, 492, 488, + 484, 480, 476, 472, 468, 464, 460, 456, 452, 448, 444, 440, 436, 432, 428, 424, + 420, 416, 412, 409, 405, 401, 397, 393, 389, 385, 382, 378, 374, 370, 367, 363, + 359, 355, 352, 348, 344, 340, 337, 333, 329, 326, 322, 318, 315, 311, 308, 304, + 300, 297, 293, 290, 286, 283, 279, 276, 272, 268, 265, 262, 258, 255, 251, 248, + 244, 241, 237, 234, 231, 227, 224, 220, 217, 214, 210, 207, 204, 200, 197, 194, + 190, 187, 184, 181, 177, 174, 171, 168, 164, 161, 158, 155, 152, 148, 145, 142, + 139, 136, 133, 130, 126, 123, 120, 117, 114, 111, 108, 105, 102, 99, 96, 93, + 90, 87, 84, 81, 78, 75, 72, 69, 66, 63, 60, 57, 54, 51, 48, 45, + 42, 40, 37, 34, 31, 28, 25, 22, 20, 17, 14, 11, 8, 6, 3, 0, +}; +//-------------------------------------------------------------------------------------------------- +const uint16_t Opal::LogSinTable[0x100] = { + 2137, 1731, 1543, 1419, 1326, 1252, 1190, 1137, 1091, 1050, 1013, 979, 949, 920, 894, 869, + 846, 825, 804, 785, 767, 749, 732, 717, 701, 687, 672, 659, 646, 633, 621, 609, + 598, 587, 576, 566, 556, 546, 536, 527, 518, 509, 501, 492, 484, 476, 468, 461, + 453, 446, 439, 432, 425, 418, 411, 405, 399, 392, 386, 380, 375, 369, 363, 358, + 352, 347, 341, 336, 331, 326, 321, 316, 311, 307, 302, 297, 293, 289, 284, 280, + 276, 271, 267, 263, 259, 255, 251, 248, 244, 240, 236, 233, 229, 226, 222, 219, + 215, 212, 209, 205, 202, 199, 196, 193, 190, 187, 184, 181, 178, 175, 172, 169, + 167, 164, 161, 159, 156, 153, 151, 148, 146, 143, 141, 138, 136, 134, 131, 129, + 127, 125, 122, 120, 118, 116, 114, 112, 110, 108, 106, 104, 102, 100, 98, 96, + 94, 92, 91, 89, 87, 85, 83, 82, 80, 78, 77, 75, 74, 72, 70, 69, + 67, 66, 64, 63, 62, 60, 59, 57, 56, 55, 53, 52, 51, 49, 48, 47, + 46, 45, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, + 29, 28, 27, 26, 25, 24, 23, 23, 22, 21, 20, 20, 19, 18, 17, 17, + 16, 15, 15, 14, 13, 13, 12, 12, 11, 10, 10, 9, 9, 8, 8, 7, + 7, 7, 6, 6, 5, 5, 5, 4, 4, 4, 3, 3, 3, 2, 2, 2, + 2, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, +}; + + + +//================================================================================================== +// This is the temporary code for generating the above tables. Maths and data from this nice +// reverse-engineering effort: +// +// https://docs.google.com/document/d/18IGx18NQY_Q1PJVZ-bHywao9bhsDoAqoIn1rIm42nwo/edit +//================================================================================================== +#if 0 +#include <math.h> + +void GenerateTables() { + + // Build the exponentiation table (reversed from the official OPL3 ROM) + FILE *fd = fopen("exptab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } + + // Build the log-sin table + fd = fopen("sintab.txt", "wb"); + if (fd) { + for (int i = 0; i < 0x100; i++) { + int v = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + if (i & 15) + fprintf(fd, " %4d,", v); + else + fprintf(fd, "\n\t%4d,", v); + } + fclose(fd); + } +} +#endif + + + +//================================================================================================== +// Constructor/destructor. +//================================================================================================== +Opal::Opal(int sample_rate) { + + Init(sample_rate); +} +//-------------------------------------------------------------------------------------------------- +Opal::~Opal() { +} + + + +//================================================================================================== +// Initialise the emulation. +//================================================================================================== +void Opal::Init(int sample_rate) { + + Clock = 0; + TremoloClock = 0; + TremoloLevel = 0; + VibratoTick = 0; + VibratoClock = 0; + NoteSel = false; + TremoloDepth = false; + VibratoDepth = false; + +// // Build the exponentiation table (reversed from the official OPL3 ROM) +// for (int i = 0; i < 0x100; i++) +// ExpTable[i] = (pow(2, (0xFF - i) / 256.0) - 1) * 1024 + 0.5; +// +// // Build the log-sin table +// for (int i = 0; i < 0x100; i++) +// LogSinTable[i] = -log(sin((i + 0.5) * 3.1415926535897933 / 256 / 2)) / log(2) * 256 + 0.5; + + // Let sub-objects know where to find us + for (int i = 0; i < NumOperators; i++) + Op[i].SetMaster(this); + + for (int i = 0; i < NumChannels; i++) + Chan[i].SetMaster(this); + + // Add the operators to the channels. Note, some channels can't use all the operators + // FIXME: put this into a separate routine + const int chan_ops[] = { + 0, 1, 2, 6, 7, 8, 12, 13, 14, 18, 19, 20, 24, 25, 26, 30, 31, 32, + }; + + for (int i = 0; i < NumChannels; i++) { + Channel *chan = &Chan[i]; + int op = chan_ops[i]; + if (i < 3 || (i >= 9 && i < 12)) + chan->SetOperators(&Op[op], &Op[op + 3], &Op[op + 6], &Op[op + 9]); + else + chan->SetOperators(&Op[op], &Op[op + 3], 0, 0); + } + + // Initialise the operator rate data. We can't do this in the Operator constructor as it + // relies on referencing the master and channel objects + for (int i = 0; i < NumOperators; i++) + Op[i].ComputeRates(); + + SetSampleRate(sample_rate); +} + + + +//================================================================================================== +// Change the sample rate. +//================================================================================================== +void Opal::SetSampleRate(int sample_rate) { + + // Sanity + if (sample_rate == 0) + sample_rate = OPL3SampleRate; + + SampleRate = sample_rate; + SampleAccum = 0; + LastOutput[0] = LastOutput[1] = 0; + CurrOutput[0] = CurrOutput[1] = 0; +} + + + +//================================================================================================== +// Write a value to an OPL3 register. +//================================================================================================== +void Opal::Port(uint16_t reg_num, uint8_t val) { + + static constexpr int8_t op_lookup[] = { + // 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F + 0, 1, 2, 3, 4, 5, -1, -1, 6, 7, 8, 9, 10, 11, -1, -1, + // 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F + 12, 13, 14, 15, 16, 17, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + }; + + uint16_t type = reg_num & 0xE0; + + // Is it BD, the one-off register stuck in the middle of the register array? + if (reg_num == 0xBD) { + TremoloDepth = (val & 0x80) != 0; + VibratoDepth = (val & 0x40) != 0; + return; + } + + // Global registers + if (type == 0x00) { + + // 4-OP enables + if (reg_num == 0x104) { + + // Enable/disable channels based on which 4-op enables + uint8_t mask = 1; + for (int i = 0; i < 6; i++, mask <<= 1) { + + // The 4-op channels are 0, 1, 2, 9, 10, 11 + uint16_t chan = static_cast<uint16_t>(i < 3 ? i : i + 6); + Channel *primary = &Chan[chan]; + Channel *secondary = &Chan[chan + 3]; + + if (val & mask) { + + // Let primary channel know it's controlling the secondary channel + primary->SetChannelPair(secondary); + + // Turn off the second channel in the pair + secondary->SetEnable(false); + + } else { + + // Let primary channel know it's no longer controlling the secondary channel + primary->SetChannelPair(0); + + // Turn on the second channel in the pair + secondary->SetEnable(true); + } + } + + // CSW / Note-sel + } else if (reg_num == 0x08) { + + NoteSel = (val & 0x40) != 0; + + // Get the channels to recompute the Key Scale No. as this varies based on NoteSel + for (int i = 0; i < NumChannels; i++) + Chan[i].ComputeKeyScaleNumber(); + } + + // Channel registers + } else if (type >= 0xA0 && type <= 0xC0) { + + // Convert to channel number + int chan_num = reg_num & 15; + + // Valid channel? + if (chan_num >= 9) + return; + + // Is it the other bank of channels? + if (reg_num & 0x100) + chan_num += 9; + + Channel &chan = Chan[chan_num]; + + // Registers Ax and Bx affect both channels + Channel *chans[2] = {&chan, chan.GetChannelPair()}; + int numchans = chans[1] ? 2 : 1; + + // Do specific registers + switch (reg_num & 0xF0) { + + // Frequency low + case 0xA0: { + for (int i = 0; i < numchans; i++) { + chans[i]->SetFrequencyLow(val); + } + break; + } + + // Key-on / Octave / Frequency High + case 0xB0: { + for (int i = 0; i < numchans; i++) { + chans[i]->SetKeyOn((val & 0x20) != 0); + chans[i]->SetOctave(val >> 2 & 7); + chans[i]->SetFrequencyHigh(val & 3); + } + break; + } + + // Right Stereo Channel Enable / Left Stereo Channel Enable / Feedback Factor / Modulation Type + case 0xC0: { + chan.SetRightEnable((val & 0x20) != 0); + chan.SetLeftEnable((val & 0x10) != 0); + chan.SetFeedback(val >> 1 & 7); + chan.SetModulationType(val & 1); + break; + } + } + + // Operator registers + } else if ((type >= 0x20 && type <= 0x80) || type == 0xE0) { + + // Convert to operator number + int op_num = op_lookup[reg_num & 0x1F]; + + // Valid register? + if (op_num < 0) + return; + + // Is it the other bank of operators? + if (reg_num & 0x100) + op_num += 18; + + Operator &op = Op[op_num]; + + // Do specific registers + switch (type) { + + // Tremolo Enable / Vibrato Enable / Sustain Mode / Envelope Scaling / Frequency Multiplier + case 0x20: { + op.SetTremoloEnable((val & 0x80) != 0); + op.SetVibratoEnable((val & 0x40) != 0); + op.SetSustainMode((val & 0x20) != 0); + op.SetEnvelopeScaling((val & 0x10) != 0); + op.SetFrequencyMultiplier(val & 15); + break; + } + + // Key Scale / Output Level + case 0x40: { + op.SetKeyScale(val >> 6); + op.SetOutputLevel(val & 0x3F); + break; + } + + // Attack Rate / Decay Rate + case 0x60: { + op.SetAttackRate(val >> 4); + op.SetDecayRate(val & 15); + break; + } + + // Sustain Level / Release Rate + case 0x80: { + op.SetSustainLevel(val >> 4); + op.SetReleaseRate(val & 15); + break; + } + + // Waveform + case 0xE0: { + op.SetWaveform(val & 7); + break; + } + } + } +} + + + +//================================================================================================== +// Generate sample. Every time you call this you will get two signed 16-bit samples (one for each +// stereo channel) which will sound correct when played back at the sample rate given when the +// class was constructed. +//================================================================================================== +void Opal::Sample(int16_t *left, int16_t *right) { + + // If the destination sample rate is higher than the OPL3 sample rate, we need to skip ahead + while (SampleAccum >= SampleRate) { + + LastOutput[0] = CurrOutput[0]; + LastOutput[1] = CurrOutput[1]; + + Output(CurrOutput[0], CurrOutput[1]); + + SampleAccum -= SampleRate; + } + + // Mix with the partial accumulation + int32_t omblend = SampleRate - SampleAccum; + *left = static_cast<uint16_t>((LastOutput[0] * omblend + CurrOutput[0] * SampleAccum) / SampleRate); + *right = static_cast<uint16_t>((LastOutput[1] * omblend + CurrOutput[1] * SampleAccum) / SampleRate); + + SampleAccum += OPL3SampleRate; +} + + + +//================================================================================================== +// Produce final output from the chip. This is at the OPL3 sample-rate. +//================================================================================================== +void Opal::Output(int16_t &left, int16_t &right) { + + int32_t leftmix = 0, rightmix = 0; + + // Sum the output of each channel + for (int i = 0; i < NumChannels; i++) { + + int16_t chanleft, chanright; + Chan[i].Output(chanleft, chanright); + + leftmix += chanleft; + rightmix += chanright; + } + + // Clamp + if (leftmix < -0x8000) + left = -0x8000; + else if (leftmix > 0x7FFF) + left = 0x7FFF; + else + left = static_cast<uint16_t>(leftmix); + + if (rightmix < -0x8000) + right = -0x8000; + else if (rightmix > 0x7FFF) + right = 0x7FFF; + else + right = static_cast<uint16_t>(rightmix); + + Clock++; + + // Tremolo. According to this post, the OPL3 tremolo is a 13,440 sample length triangle wave + // with a peak at 26 and a trough at 0 and is simply added to the logarithmic level accumulator + // http://forums.submarine.org.uk/phpBB/viewtopic.php?f=9&t=1171 + TremoloClock = (TremoloClock + 1) % 13440; + TremoloLevel = ((TremoloClock < 13440 / 2) ? TremoloClock : 13440 - TremoloClock) / 256; + if (!TremoloDepth) + TremoloLevel >>= 2; + + // Vibrato. This appears to be a 8 sample long triangle wave with a magnitude of the three + // high bits of the channel frequency, positive and negative, divided by two if the vibrato + // depth is zero. It is only cycled every 1,024 samples. + VibratoTick++; + if (VibratoTick >= 1024) { + VibratoTick = 0; + VibratoClock = (VibratoClock + 1) & 7; + } +} + + + +//================================================================================================== +// Channel constructor. +//================================================================================================== +Opal::Channel::Channel() { + + Master = 0; + Freq = 0; + Octave = 0; + PhaseStep = 0; + KeyScaleNumber = 0; + FeedbackShift = 0; + ModulationType = 0; + ChannelPair = 0; + Enable = true; + LeftEnable = true; + RightEnable = true; +} + + + +//================================================================================================== +// Produce output from channel. +//================================================================================================== +void Opal::Channel::Output(int16_t &left, int16_t &right) { + + // Has the channel been disabled? This is usually a result of the 4-op enables being used to + // disable the secondary channel in each 4-op pair + if (!Enable) { + left = right = 0; + return; + } + + int16_t vibrato = (Freq >> 7) & 7; + if (!Master->VibratoDepth) + vibrato >>= 1; + + // 0 3 7 3 0 -3 -7 -3 + uint16_t clk = Master->VibratoClock; + if (!(clk & 3)) + vibrato = 0; // Position 0 and 4 is zero + else { + if (clk & 1) + vibrato >>= 1; // Odd positions are half the magnitude + + vibrato <<= Octave; + + if (clk & 4) + vibrato = -vibrato; // The second half positions are negative + } + + // Combine individual operator outputs + int16_t out, acc; + + // Running in 4-op mode? + if (ChannelPair) { + + // Get the secondary channel's modulation type. This is the only thing from the secondary + // channel that is used + if (ChannelPair->GetModulationType() == 0) { + + if (ModulationType == 0) { + + // feedback -> modulator -> modulator -> modulator -> carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + out = Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // (feedback -> carrier) + (modulator -> modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + } + + } else { + + if (ModulationType == 0) { + + // (feedback -> modulator -> carrier) + (modulator -> carrier) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + acc = Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + + } else { + + // (feedback -> carrier) + (modulator -> carrier) + carrier + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + acc = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + out += Op[2]->Output(KeyScaleNumber, PhaseStep, vibrato, acc, 0); + out += Op[3]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, 0); + } + } + + } else { + + // Standard 2-op mode + if (ModulationType == 0) { + + // Frequency modulation (well, phase modulation technically) + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out = Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato, out, 0); + + } else { + + // Additive + out = Op[0]->Output(KeyScaleNumber, PhaseStep, vibrato, 0, FeedbackShift); + out += Op[1]->Output(KeyScaleNumber, PhaseStep, vibrato); + } + } + + left = LeftEnable ? out : 0; + right = RightEnable ? out : 0; +} + + + +//================================================================================================== +// Set phase step for operators using this channel. +//================================================================================================== +void Opal::Channel::SetFrequencyLow(uint16_t freq) { + + Freq = (Freq & 0x300) | (freq & 0xFF); + ComputePhaseStep(); +} +//-------------------------------------------------------------------------------------------------- +void Opal::Channel::SetFrequencyHigh(uint16_t freq) { + + Freq = (Freq & 0xFF) | ((freq & 3) << 8); + ComputePhaseStep(); + + // Only the high bits of Freq affect the Key Scale No. + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Set the octave of the channel (0 to 7). +//================================================================================================== +void Opal::Channel::SetOctave(uint16_t oct) { + + Octave = oct & 7; + ComputePhaseStep(); + ComputeKeyScaleNumber(); +} + + + +//================================================================================================== +// Keys the channel on/off. +//================================================================================================== +void Opal::Channel::SetKeyOn(bool on) { + + Op[0]->SetKeyOn(on); + Op[1]->SetKeyOn(on); +} + + + +//================================================================================================== +// Enable left stereo channel. +//================================================================================================== +void Opal::Channel::SetLeftEnable(bool on) { + + LeftEnable = on; +} + + + +//================================================================================================== +// Enable right stereo channel. +//================================================================================================== +void Opal::Channel::SetRightEnable(bool on) { + + RightEnable = on; +} + + + +//================================================================================================== +// Set the channel feedback amount. +//================================================================================================== +void Opal::Channel::SetFeedback(uint16_t val) { + + FeedbackShift = val ? 9 - val : 0; +} + + + +//================================================================================================== +// Set frequency modulation/additive modulation +//================================================================================================== +void Opal::Channel::SetModulationType(uint16_t type) { + + ModulationType = type; +} + + + +//================================================================================================== +// Compute the stepping factor for the operator waveform phase based on the frequency and octave +// values of the channel. +//================================================================================================== +void Opal::Channel::ComputePhaseStep() { + + PhaseStep = uint32_t(Freq) << Octave; +} + + + +//================================================================================================== +// Compute the key scale number and key scale levels. +// +// From the Yamaha data sheet this is the block/octave number as bits 3-1, with bit 0 coming from +// the MSB of the frequency if NoteSel is 1, and the 2nd MSB if NoteSel is 0. +//================================================================================================== +void Opal::Channel::ComputeKeyScaleNumber() { + + uint16_t lsb = Master->NoteSel ? Freq >> 9 : (Freq >> 8) & 1; + KeyScaleNumber = Octave << 1 | lsb; + + // Get the channel operators to recompute their rates as they're dependent on this number. They + // also need to recompute their key scale level + for (int i = 0; i < 4; i++) { + + if (!Op[i]) + continue; + + Op[i]->ComputeRates(); + Op[i]->ComputeKeyScaleLevel(); + } +} + + + +//================================================================================================== +// Operator constructor. +//================================================================================================== +Opal::Operator::Operator() { + + Master = 0; + Chan = 0; + Phase = 0; + Waveform = 0; + FreqMultTimes2 = 1; + EnvelopeStage = EnvOff; + EnvelopeLevel = 0x1FF; + AttackRate = 0; + DecayRate = 0; + SustainLevel = 0; + ReleaseRate = 0; + KeyScaleShift = 0; + KeyScaleLevel = 0; + Out[0] = Out[1] = 0; + KeyOn = false; + KeyScaleRate = false; + SustainMode = false; + TremoloEnable = false; + VibratoEnable = false; +} + + + +//================================================================================================== +// Produce output from operator. +//================================================================================================== +int16_t Opal::Operator::Output(uint16_t /*keyscalenum*/, uint32_t phase_step, int16_t vibrato, int16_t mod, int16_t fbshift) { + + // Advance wave phase + if (VibratoEnable) + phase_step += vibrato; + Phase += (phase_step * FreqMultTimes2) / 2; + + uint16_t level = (EnvelopeLevel + OutputLevel + KeyScaleLevel + (TremoloEnable ? Master->TremoloLevel : 0)) << 3; + + switch (EnvelopeStage) { + + // Attack stage + case EnvAtt: { + uint16_t add = ((AttackAdd >> AttackTab[Master->Clock >> AttackShift & 7]) * ~EnvelopeLevel) >> 3; + if (AttackRate == 0) + add = 0; + if (AttackMask && (Master->Clock & AttackMask)) + add = 0; + EnvelopeLevel += add; + if (EnvelopeLevel <= 0) { + EnvelopeLevel = 0; + EnvelopeStage = EnvDec; + } + break; + } + + // Decay stage + case EnvDec: { + uint16_t add = DecayAdd >> DecayTab[Master->Clock >> DecayShift & 7]; + if (DecayRate == 0) + add = 0; + if (DecayMask && (Master->Clock & DecayMask)) + add = 0; + EnvelopeLevel += add; + if (EnvelopeLevel >= SustainLevel) { + EnvelopeLevel = SustainLevel; + EnvelopeStage = EnvSus; + } + break; + } + + // Sustain stage + case EnvSus: { + + if (SustainMode) + break; + + // Note: fall-through! + [[fallthrough]]; + } + + // Release stage + case EnvRel: { + uint16_t add = ReleaseAdd >> ReleaseTab[Master->Clock >> ReleaseShift & 7]; + if (ReleaseRate == 0) + add = 0; + if (ReleaseMask && (Master->Clock & ReleaseMask)) + add = 0; + EnvelopeLevel += add; + if (EnvelopeLevel >= 0x1FF) { + EnvelopeLevel = 0x1FF; + EnvelopeStage = EnvOff; + Out[0] = Out[1] = 0; + return 0; + } + break; + } + + // Envelope, and therefore the operator, is not running + default: + Out[0] = Out[1] = 0; + return 0; + } + + // Feedback? In that case we modulate by a blend of the last two samples + if (fbshift) + mod += (Out[0] + Out[1]) >> fbshift; + + uint16_t phase = static_cast<uint16_t>(Phase >> 10) + mod; + uint16_t offset = phase & 0xFF; + uint16_t logsin; + bool negate = false; + + switch (Waveform) { + + //------------------------------------ + // Standard sine wave + //------------------------------------ + case 0: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + negate = (phase & 0x200) != 0; + break; + + //------------------------------------ + // Half sine wave + //------------------------------------ + case 1: + if (phase & 0x200) + offset = 0; + else if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Positive sine wave + //------------------------------------ + case 2: + if (phase & 0x100) + offset ^= 0xFF; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Quarter positive sine wave + //------------------------------------ + case 3: + if (phase & 0x100) + offset = 0; + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed sine wave + //------------------------------------ + case 4: + if (phase & 0x200) + offset = 0; + + else { + + if (phase & 0x80) + offset ^= 0xFF; + + offset = (offset + offset) & 0xFF; + negate = (phase & 0x100) != 0; + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Double-speed positive sine wave + //------------------------------------ + case 5: + if (phase & 0x200) + offset = 0; + + else { + + offset = (offset + offset) & 0xFF; + if (phase & 0x80) + offset ^= 0xFF; + } + + logsin = Master->LogSinTable[offset]; + break; + + //------------------------------------ + // Square wave + //------------------------------------ + case 6: + logsin = 0; + negate = (phase & 0x200) != 0; + break; + + //------------------------------------ + // Exponentiation wave + //------------------------------------ + default: + logsin = phase & 0x1FF; + if (phase & 0x200) { + logsin ^= 0x1FF; + negate = true; + } + logsin <<= 3; + break; + } + + uint16_t mix = logsin + level; + if (mix > 0x1FFF) + mix = 0x1FFF; + + // From the OPLx decapsulated docs: + // "When such a table is used for calculation of the exponential, the table is read at the + // position given by the 8 LSB's of the input. The value + 1024 (the hidden bit) is then the + // significand of the floating point output and the yet unused MSB's of the input are the + // exponent of the floating point output." + int16_t v = (Master->ExpTable[mix & 0xFF] + 1024u) >> (mix >> 8u); + v += v; + if (negate) + v = ~v; + + // Keep last two results for feedback calculation + Out[1] = Out[0]; + Out[0] = v; + + return v; +} + + + +//================================================================================================== +// Trigger operator. +//================================================================================================== +void Opal::Operator::SetKeyOn(bool on) { + + // Already on/off? + if (KeyOn == on) + return; + KeyOn = on; + + if (on) { + + // The highest attack rate is instant; it bypasses the attack phase + if (AttackRate == 15) { + EnvelopeStage = EnvDec; + EnvelopeLevel = 0; + } else + EnvelopeStage = EnvAtt; + + Phase = 0; + + } else { + + // Stopping current sound? + if (EnvelopeStage != EnvOff && EnvelopeStage != EnvRel) + EnvelopeStage = EnvRel; + } +} + + + +//================================================================================================== +// Enable amplitude vibrato. +//================================================================================================== +void Opal::Operator::SetTremoloEnable(bool on) { + + TremoloEnable = on; +} + + + +//================================================================================================== +// Enable frequency vibrato. +//================================================================================================== +void Opal::Operator::SetVibratoEnable(bool on) { + + VibratoEnable = on; +} + + + +//================================================================================================== +// Sets whether we release or sustain during the sustain phase of the envelope. 'true' is to +// sustain, otherwise release. +//================================================================================================== +void Opal::Operator::SetSustainMode(bool on) { + + SustainMode = on; +} + + + +//================================================================================================== +// Key scale rate. Sets how much the Key Scaling Number affects the envelope rates. +//================================================================================================== +void Opal::Operator::SetEnvelopeScaling(bool on) { + + KeyScaleRate = on; + ComputeRates(); +} + + + +//================================================================================================== +// Multiplies the phase frequency. +//================================================================================================== +void Opal::Operator::SetFrequencyMultiplier(uint16_t scale) { + + // Needs to be multiplied by two (and divided by two later when we use it) because the first + // entry is actually .5 + const uint16_t mul_times_2[] = { + 1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 20, 24, 24, 30, 30, + }; + + FreqMultTimes2 = mul_times_2[scale & 15]; +} + + + +//================================================================================================== +// Attenuates output level towards higher pitch. +//================================================================================================== +void Opal::Operator::SetKeyScale(uint16_t scale) { + + static constexpr uint8_t kslShift[4] = { 8, 1, 2, 0 }; + KeyScaleShift = kslShift[scale]; + ComputeKeyScaleLevel(); +} + + + +//================================================================================================== +// Sets the output level (volume) of the operator. +//================================================================================================== +void Opal::Operator::SetOutputLevel(uint16_t level) { + + OutputLevel = level * 4; +} + + + +//================================================================================================== +// Operator attack rate. +//================================================================================================== +void Opal::Operator::SetAttackRate(uint16_t rate) { + + AttackRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator decay rate. +//================================================================================================== +void Opal::Operator::SetDecayRate(uint16_t rate) { + + DecayRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Operator sustain level. +//================================================================================================== +void Opal::Operator::SetSustainLevel(uint16_t level) { + + SustainLevel = level < 15 ? level : 31; + SustainLevel *= 16; +} + + + +//================================================================================================== +// Operator release rate. +//================================================================================================== +void Opal::Operator::SetReleaseRate(uint16_t rate) { + + ReleaseRate = rate; + + ComputeRates(); +} + + + +//================================================================================================== +// Assign the waveform this operator will use. +//================================================================================================== +void Opal::Operator::SetWaveform(uint16_t wave) { + + Waveform = wave & 7; +} + + + +//================================================================================================== +// Compute actual rate from register rate. From the Yamaha data sheet: +// +// Actual rate = Rate value * 4 + Rof, if Rate value = 0, actual rate = 0 +// +// Rof is set as follows depending on the KSR setting: +// +// Key scale 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// KSR = 0 0 0 0 0 1 1 1 1 2 2 2 2 3 3 3 3 +// KSR = 1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 +// +// Note: zero rates are infinite, and are treated separately elsewhere +//================================================================================================== +void Opal::Operator::ComputeRates() { + + int combined_rate = AttackRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + int rate_high = combined_rate >> 2; + int rate_low = combined_rate & 3; + + AttackShift = static_cast<uint16_t>(rate_high < 12 ? 12 - rate_high : 0); + AttackMask = (1 << AttackShift) - 1; + AttackAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + AttackTab = Master->RateTables[rate_low]; + + // Attack rate of 15 is always instant + if (AttackRate == 15) + AttackAdd = 0xFFF; + + combined_rate = DecayRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + DecayShift = static_cast<uint16_t>(rate_high < 12 ? 12 - rate_high : 0); + DecayMask = (1 << DecayShift) - 1; + DecayAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + DecayTab = Master->RateTables[rate_low]; + + combined_rate = ReleaseRate * 4 + (Chan->GetKeyScaleNumber() >> (KeyScaleRate ? 0 : 2)); + rate_high = combined_rate >> 2; + rate_low = combined_rate & 3; + + ReleaseShift = static_cast<uint16_t>(rate_high < 12 ? 12 - rate_high : 0); + ReleaseMask = (1 << ReleaseShift) - 1; + ReleaseAdd = (rate_high < 12) ? 1 : 1 << (rate_high - 12); + ReleaseTab = Master->RateTables[rate_low]; +} + + + +//================================================================================================== +// Compute the operator's key scale level. This changes based on the channel frequency/octave and +// operator key scale value. +//================================================================================================== +void Opal::Operator::ComputeKeyScaleLevel() { + + static constexpr uint8_t levtab[] = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 8, 12, 16, 20, 24, 28, 32, + 0, 0, 0, 0, 0, 12, 20, 28, 32, 40, 44, 48, 52, 56, 60, 64, + 0, 0, 0, 20, 32, 44, 52, 60, 64, 72, 76, 80, 84, 88, 92, 96, + 0, 0, 32, 52, 64, 76, 84, 92, 96, 104, 108, 112, 116, 120, 124, 128, + 0, 32, 64, 84, 96, 108, 116, 124, 128, 136, 140, 144, 148, 152, 156, 160, + 0, 64, 96, 116, 128, 140, 148, 156, 160, 168, 172, 176, 180, 184, 188, 192, + 0, 96, 128, 148, 160, 172, 180, 188, 192, 200, 204, 208, 212, 216, 220, 224, + }; + + // This uses a combined value of the top four bits of frequency with the octave/block + uint16_t i = (Chan->GetOctave() << 4) | (Chan->GetFreq() >> 6); + KeyScaleLevel = levtab[i] >> KeyScaleShift; +} diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/pattern.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/pattern.cpp new file mode 100644 index 00000000..b94b0ea9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/pattern.cpp @@ -0,0 +1,643 @@ +/* + * Pattern.cpp + * ----------- + * Purpose: Module Pattern header class + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "pattern.h" +#include "patternContainer.h" +#include "../common/serialization_utils.h" +#include "../common/version.h" +#include "ITTools.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +CSoundFile& CPattern::GetSoundFile() { return m_rPatternContainer.GetSoundFile(); } +const CSoundFile& CPattern::GetSoundFile() const { return m_rPatternContainer.GetSoundFile(); } + + +CHANNELINDEX CPattern::GetNumChannels() const +{ + return GetSoundFile().GetNumChannels(); +} + + +// Check if there is any note data on a given row. +bool CPattern::IsEmptyRow(ROWINDEX row) const +{ + if(m_ModCommands.empty() || !IsValidRow(row)) + { + return true; + } + + PatternRow data = GetRow(row); + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, data++) + { + if(!data->IsEmpty()) + { + return false; + } + } + return true; +} + + +bool CPattern::SetSignature(const ROWINDEX rowsPerBeat, const ROWINDEX rowsPerMeasure) +{ + if(rowsPerBeat < 1 + || rowsPerBeat > GetSoundFile().GetModSpecifications().patternRowsMax + || rowsPerMeasure < rowsPerBeat + || rowsPerMeasure > GetSoundFile().GetModSpecifications().patternRowsMax) + { + return false; + } + m_RowsPerBeat = rowsPerBeat; + m_RowsPerMeasure = rowsPerMeasure; + return true; +} + + +// Add or remove rows from the pattern. +bool CPattern::Resize(const ROWINDEX newRowCount, bool enforceFormatLimits, bool resizeAtEnd) +{ + CSoundFile &sndFile = GetSoundFile(); + + if(newRowCount == m_Rows || newRowCount < 1 || newRowCount > MAX_PATTERN_ROWS) + { + return false; + } + if(enforceFormatLimits) + { + auto &specs = sndFile.GetModSpecifications(); + if(newRowCount > specs.patternRowsMax || newRowCount < specs.patternRowsMin) return false; + } + + try + { + size_t count = ((newRowCount > m_Rows) ? (newRowCount - m_Rows) : (m_Rows - newRowCount)) * GetNumChannels(); + + if(newRowCount > m_Rows) + m_ModCommands.insert(resizeAtEnd ? m_ModCommands.end() : m_ModCommands.begin(), count, ModCommand::Empty()); + else if(resizeAtEnd) + m_ModCommands.erase(m_ModCommands.end() - count, m_ModCommands.end()); + else + m_ModCommands.erase(m_ModCommands.begin(), m_ModCommands.begin() + count); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + + m_Rows = newRowCount; + return true; +} + + +void CPattern::ClearCommands() +{ + std::fill(m_ModCommands.begin(), m_ModCommands.end(), ModCommand::Empty()); +} + + +bool CPattern::AllocatePattern(ROWINDEX rows) +{ + size_t newSize = GetNumChannels() * rows; + if(rows == 0) + { + return false; + } else if(rows == GetNumRows() && m_ModCommands.size() == newSize) + { + // Re-use allocated memory + ClearCommands(); + return true; + } else + { + // Do this in two steps in order to keep the old pattern data in case of OOM + decltype(m_ModCommands) newPattern(newSize, ModCommand::Empty()); + m_ModCommands = std::move(newPattern); + } + m_Rows = rows; + return true; +} + + +void CPattern::Deallocate() +{ + m_Rows = m_RowsPerBeat = m_RowsPerMeasure = 0; + m_ModCommands.clear(); + m_PatternName.clear(); +} + + +CPattern& CPattern::operator= (const CPattern &pat) +{ + m_ModCommands = pat.m_ModCommands; + m_Rows = pat.m_Rows; + m_RowsPerBeat = pat.m_RowsPerBeat; + m_RowsPerMeasure = pat.m_RowsPerMeasure; + m_tempoSwing = pat.m_tempoSwing; + m_PatternName = pat.m_PatternName; + return *this; +} + + + +bool CPattern::operator== (const CPattern &other) const +{ + return GetNumRows() == other.GetNumRows() + && GetNumChannels() == other.GetNumChannels() + && GetOverrideSignature() == other.GetOverrideSignature() + && GetRowsPerBeat() == other.GetRowsPerBeat() + && GetRowsPerMeasure() == other.GetRowsPerMeasure() + && GetTempoSwing() == other.GetTempoSwing() + && m_ModCommands == other.m_ModCommands; +} + + +#ifdef MODPLUG_TRACKER + +bool CPattern::Expand() +{ + const ROWINDEX newRows = m_Rows * 2; + const CHANNELINDEX nChns = GetNumChannels(); + + if(m_ModCommands.empty() + || newRows > GetSoundFile().GetModSpecifications().patternRowsMax) + { + return false; + } + + decltype(m_ModCommands) newPattern; + try + { + newPattern.assign(m_ModCommands.size() * 2, ModCommand::Empty()); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + + for(auto mSrc = m_ModCommands.begin(), mDst = newPattern.begin(); mSrc != m_ModCommands.end(); mSrc += nChns, mDst += 2 * nChns) + { + std::copy(mSrc, mSrc + nChns, mDst); + } + + m_ModCommands = std::move(newPattern); + m_Rows = newRows; + + return true; +} + + +bool CPattern::Shrink() +{ + if (m_ModCommands.empty() + || m_Rows < GetSoundFile().GetModSpecifications().patternRowsMin * 2) + { + return false; + } + + m_Rows /= 2; + const CHANNELINDEX nChns = GetNumChannels(); + + for(ROWINDEX y = 0; y < m_Rows; y++) + { + const PatternRow srcRow = GetRow(y * 2); + const PatternRow nextSrcRow = GetRow(y * 2 + 1); + PatternRow destRow = GetRow(y); + + for(CHANNELINDEX x = 0; x < nChns; x++) + { + const ModCommand &src = srcRow[x]; + const ModCommand &srcNext = nextSrcRow[x]; + ModCommand &dest = destRow[x]; + dest = src; + + if(dest.note == NOTE_NONE && !dest.instr) + { + // Fill in data from next row if field is empty + dest.note = srcNext.note; + dest.instr = srcNext.instr; + if(srcNext.volcmd != VOLCMD_NONE) + { + dest.volcmd = srcNext.volcmd; + dest.vol = srcNext.vol; + } + if(dest.command == CMD_NONE) + { + dest.command = srcNext.command; + dest.param = srcNext.param; + } + } + } + } + m_ModCommands.resize(m_Rows * nChns); + + return true; +} + + +#endif // MODPLUG_TRACKER + + +bool CPattern::SetName(const std::string &newName) +{ + m_PatternName = newName; + return true; +} + + +bool CPattern::SetName(const char *newName, size_t maxChars) +{ + if(newName == nullptr || maxChars == 0) + { + return false; + } + const auto nameEnd = std::find(newName, newName + maxChars, '\0'); + m_PatternName.assign(newName, nameEnd); + return true; +} + + +// Write some kind of effect data to the pattern. Exact data to be written and write behaviour can be found in the EffectWriter object. +bool CPattern::WriteEffect(EffectWriter &settings) +{ + // First, reject invalid parameters. + if(m_ModCommands.empty() + || settings.m_row >= GetNumRows() + || (settings.m_channel >= GetNumChannels() && settings.m_channel != CHANNELINDEX_INVALID)) + { + return false; + } + + CHANNELINDEX scanChnMin = settings.m_channel, scanChnMax = settings.m_channel; + + // Scan all channels + if(settings.m_channel == CHANNELINDEX_INVALID) + { + scanChnMin = 0; + scanChnMax = GetNumChannels() - 1; + } + + ModCommand * const baseCommand = GetpModCommand(settings.m_row, scanChnMin); + ModCommand *m; + + // Scan channel(s) for same effect type - if an effect of the same type is already present, exit. + if(!settings.m_allowMultiple) + { + m = baseCommand; + for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) + { + if(!settings.m_isVolEffect && m->command == settings.m_command) + return true; + if(settings.m_isVolEffect && m->volcmd == settings.m_volcmd) + return true; + } + } + + // Easy case: check if there's some space left to put the effect somewhere + m = baseCommand; + for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) + { + if(!settings.m_isVolEffect && m->command == CMD_NONE) + { + m->command = settings.m_command; + m->param = settings.m_param; + return true; + } + if(settings.m_isVolEffect && m->volcmd == VOLCMD_NONE) + { + m->volcmd = settings.m_volcmd; + m->vol = settings.m_vol; + return true; + } + } + + // Ok, apparently there's no space. If we haven't tried already, try to map it to the volume column or effect column instead. + if(settings.m_retry) + { + const bool isS3M = (GetSoundFile().GetType() & MOD_TYPE_S3M); + + // Move some effects that also work in the volume column, so there's place for our new effect. + if(!settings.m_isVolEffect) + { + m = baseCommand; + for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) + { + switch(m->command) + { + case CMD_VOLUME: + if(!GetSoundFile().GetModSpecifications().HasVolCommand(VOLCMD_VOLUME)) + { + break; + } + m->volcmd = VOLCMD_VOLUME; + m->vol = m->param; + m->command = settings.m_command; + m->param = settings.m_param; + return true; + + case CMD_PANNING8: + if(isS3M && m->param > 0x80) + { + break; + } + + m->volcmd = VOLCMD_PANNING; + m->command = settings.m_command; + + if(isS3M) + m->vol = (m->param + 1u) / 2u; + else + m->vol = (m->param + 2u) / 4u; + + m->param = settings.m_param; + return true; + + default: + break; + } + } + } + + // Let's try it again by writing into the "other" effect column. + if(settings.m_isVolEffect) + { + // Convert volume effect to normal effect + ModCommand::COMMAND newCommand = CMD_NONE; + ModCommand::PARAM newParam = settings.m_vol; + switch(settings.m_volcmd) + { + case VOLCMD_PANNING: + newCommand = CMD_PANNING8; + newParam = mpt::saturate_cast<ModCommand::PARAM>(settings.m_vol * (isS3M ? 2u : 4u)); + break; + case VOLCMD_VOLUME: + newCommand = CMD_VOLUME; + break; + default: + break; + } + + if(newCommand != CMD_NONE) + { + settings.m_command = static_cast<EffectCommand>(newCommand); + settings.m_param = newParam; + settings.m_retry = false; + } + } else + { + // Convert normal effect to volume effect + ModCommand::VOLCMD newVolCmd = VOLCMD_NONE; + ModCommand::VOL newVol = settings.m_param; + if(settings.m_command == CMD_PANNING8 && isS3M) + { + // This needs some manual fixing. + if(settings.m_param <= 0x80) + { + // Can't have surround in volume column, only normal panning + newVolCmd = VOLCMD_PANNING; + newVol /= 2u; + } + } else + { + newVolCmd = settings.m_command; + if(!ModCommand::ConvertVolEffect(newVolCmd, newVol, true)) + { + // No Success :( + newVolCmd = VOLCMD_NONE; + } + } + + if(newVolCmd != CMD_NONE) + { + settings.m_volcmd = static_cast<VolumeCommand>(newVolCmd); + settings.m_vol = newVol; + settings.m_retry = false; + } + } + + if(!settings.m_retry) + { + settings.m_isVolEffect = !settings.m_isVolEffect; + if(WriteEffect(settings)) + { + return true; + } + } + } + + // Try in the next row if possible (this may also happen if we already retried) + if(settings.m_retryMode == EffectWriter::rmTryNextRow && settings.m_row + 1 < GetNumRows()) + { + settings.m_row++; + settings.m_retry = true; + return WriteEffect(settings); + } else if(settings.m_retryMode == EffectWriter::rmTryPreviousRow && settings.m_row > 0) + { + settings.m_row--; + settings.m_retry = true; + return WriteEffect(settings); + } + + return false; +} + + +//////////////////////////////////////////////////////////////////////// +// +// Pattern serialization functions +// +//////////////////////////////////////////////////////////////////////// + + +enum maskbits +{ + noteBit = (1 << 0), + instrBit = (1 << 1), + volcmdBit = (1 << 2), + volBit = (1 << 3), + commandBit = (1 << 4), + effectParamBit = (1 << 5), + extraData = (1 << 6) +}; + +void WriteData(std::ostream& oStrm, const CPattern& pat); +void ReadData(std::istream& iStrm, CPattern& pat, const size_t nSize = 0); + +void WriteModPattern(std::ostream& oStrm, const CPattern& pat) +{ + srlztn::SsbWrite ssb(oStrm); + ssb.BeginWrite(FileIdPattern, Version::Current().GetRawVersion()); + ssb.WriteItem(pat, "data", &WriteData); + // pattern time signature + if(pat.GetOverrideSignature()) + { + ssb.WriteItem<uint32>(pat.GetRowsPerBeat(), "RPB."); + ssb.WriteItem<uint32>(pat.GetRowsPerMeasure(), "RPM."); + } + if(pat.HasTempoSwing()) + { + ssb.WriteItem<TempoSwing>(pat.GetTempoSwing(), "SWNG", TempoSwing::Serialize); + } + ssb.FinishWrite(); +} + + +void ReadModPattern(std::istream& iStrm, CPattern& pat, const size_t) +{ + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead(FileIdPattern, Version::Current().GetRawVersion()); + if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) + return; + ssb.ReadItem(pat, "data", &ReadData); + // pattern time signature + uint32 rpb = 0, rpm = 0; + ssb.ReadItem<uint32>(rpb, "RPB."); + ssb.ReadItem<uint32>(rpm, "RPM."); + pat.SetSignature(rpb, rpm); + TempoSwing swing; + ssb.ReadItem<TempoSwing>(swing, "SWNG", TempoSwing::Deserialize); + if(!swing.empty()) + swing.resize(pat.GetRowsPerBeat()); + pat.SetTempoSwing(swing); +} + + +static uint8 CreateDiffMask(const ModCommand &chnMC, const ModCommand &newMC) +{ + uint8 mask = 0; + if(chnMC.note != newMC.note) + mask |= noteBit; + if(chnMC.instr != newMC.instr) + mask |= instrBit; + if(chnMC.volcmd != newMC.volcmd) + mask |= volcmdBit; + if(chnMC.vol != newMC.vol) + mask |= volBit; + if(chnMC.command != newMC.command) + mask |= commandBit; + if(chnMC.param != newMC.param) + mask |= effectParamBit; + return mask; +} + + +// Writes pattern data. Adapted from SaveIT. +void WriteData(std::ostream& oStrm, const CPattern& pat) +{ + if(!pat.IsValid()) + return; + + const ROWINDEX rows = pat.GetNumRows(); + const CHANNELINDEX chns = pat.GetNumChannels(); + std::vector<ModCommand> lastChnMC(chns); + + for(ROWINDEX r = 0; r<rows; r++) + { + for(CHANNELINDEX c = 0; c<chns; c++) + { + const ModCommand m = *pat.GetpModCommand(r, c); + // Writing only commands not written in IT-pattern writing: + // For now this means only NOTE_PC and NOTE_PCS. + if(!m.IsPcNote()) + continue; + uint8 diffmask = CreateDiffMask(lastChnMC[c], m); + uint8 chval = static_cast<uint8>(c+1); + if(diffmask != 0) + chval |= IT_bitmask_patternChanEnabled_c; + + mpt::IO::WriteIntLE<uint8>(oStrm, chval); + + if(diffmask) + { + lastChnMC[c] = m; + mpt::IO::WriteIntLE<uint8>(oStrm, diffmask); + if(diffmask & noteBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.note); + if(diffmask & instrBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.instr); + if(diffmask & volcmdBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.volcmd); + if(diffmask & volBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.vol); + if(diffmask & commandBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.command); + if(diffmask & effectParamBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.param); + } + } + mpt::IO::WriteIntLE<uint8>(oStrm, 0); // Write end of row marker. + } +} + + +#define READITEM(itembit,id) \ +if(diffmask & itembit) \ +{ \ + mpt::IO::ReadIntLE<uint8>(iStrm, temp); \ + if(ch < chns) \ + lastChnMC[ch].id = temp; \ +} \ +if(ch < chns) \ + m.id = lastChnMC[ch].id; + + +void ReadData(std::istream& iStrm, CPattern& pat, const size_t) +{ + if (!pat.IsValid()) // Expecting patterns to be allocated and resized properly. + return; + + const CHANNELINDEX chns = pat.GetNumChannels(); + const ROWINDEX rows = pat.GetNumRows(); + + std::vector<ModCommand> lastChnMC(chns); + + ROWINDEX row = 0; + while(row < rows && iStrm.good()) + { + uint8 t = 0; + mpt::IO::ReadIntLE<uint8>(iStrm, t); + if(t == 0) + { + row++; + continue; + } + + CHANNELINDEX ch = (t & IT_bitmask_patternChanField_c); + if(ch > 0) + ch--; + + uint8 diffmask = 0; + if((t & IT_bitmask_patternChanEnabled_c) != 0) + mpt::IO::ReadIntLE<uint8>(iStrm, diffmask); + uint8 temp = 0; + + ModCommand dummy = ModCommand::Empty(); + ModCommand& m = (ch < chns) ? *pat.GetpModCommand(row, ch) : dummy; + + READITEM(noteBit, note); + READITEM(instrBit, instr); + READITEM(volcmdBit, volcmd); + READITEM(volBit, vol); + READITEM(commandBit, command); + READITEM(effectParamBit, param); + if(diffmask & extraData) + { + //Ignore additional data. + uint8 size; + mpt::IO::ReadIntLE<uint8>(iStrm, size); + iStrm.ignore(size); + } + } +} + +#undef READITEM + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/pattern.h b/Src/external_dependencies/openmpt-trunk/soundlib/pattern.h new file mode 100644 index 00000000..c5e98aad --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/pattern.h @@ -0,0 +1,214 @@ +/* + * Pattern.h + * --------- + * Purpose: Module Pattern header class + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <vector> +#include "modcommand.h" +#include "Snd_defs.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class CPatternContainer; +class CSoundFile; +class EffectWriter; + +typedef ModCommand* PatternRow; + + +class CPattern +{ + friend class CPatternContainer; + +public: + CPattern& operator= (const CPattern &pat); + bool operator== (const CPattern &other) const; + bool operator!= (const CPattern &other) const { return !(*this == other); } + +public: + ModCommand* GetpModCommand(const ROWINDEX r, const CHANNELINDEX c) { return &m_ModCommands[r * GetNumChannels() + c]; } + const ModCommand* GetpModCommand(const ROWINDEX r, const CHANNELINDEX c) const { return &m_ModCommands[r * GetNumChannels() + c]; } + + ROWINDEX GetNumRows() const { return m_Rows; } + ROWINDEX GetRowsPerBeat() const { return m_RowsPerBeat; } // pattern-specific rows per beat + ROWINDEX GetRowsPerMeasure() const { return m_RowsPerMeasure; } // pattern-specific rows per measure + bool GetOverrideSignature() const { return (m_RowsPerBeat + m_RowsPerMeasure > 0); } // override song time signature? + + // Returns true if pattern data can be accessed at given row, false otherwise. + bool IsValidRow(const ROWINDEX row) const { return (row < GetNumRows()); } + // Returns true if any pattern data is present. + bool IsValid() const { return !m_ModCommands.empty(); } + + // Return PatternRow object which has operator[] defined so that ModCommand + // at (iRow, iChn) can be accessed with GetRow(iRow)[iChn]. + PatternRow GetRow(const ROWINDEX row) { return GetpModCommand(row, 0); } + PatternRow GetRow(const ROWINDEX row) const { return const_cast<ModCommand *>(GetpModCommand(row, 0)); } + + CHANNELINDEX GetNumChannels() const; + + // Add or remove rows from the pattern. + bool Resize(const ROWINDEX newRowCount, bool enforceFormatLimits = true, bool resizeAtEnd = true); + + // Check if there is any note data on a given row. + bool IsEmptyRow(ROWINDEX row) const; + + // Allocate new pattern memory and replace old pattern data. + bool AllocatePattern(ROWINDEX rows); + // Deallocate pattern data. + void Deallocate(); + + // Removes all modcommands from the pattern. + void ClearCommands(); + + // Returns associated soundfile. + CSoundFile& GetSoundFile(); + const CSoundFile& GetSoundFile() const; + + const std::vector<ModCommand> &GetData() const { return m_ModCommands; } + void SetData(std::vector<ModCommand> &&data) { MPT_ASSERT(data.size() == GetNumRows() * GetNumChannels()); m_ModCommands = std::move(data); } + + // Set pattern signature (rows per beat, rows per measure). Returns true on success. + bool SetSignature(const ROWINDEX rowsPerBeat, const ROWINDEX rowsPerMeasure); + void RemoveSignature() { m_RowsPerBeat = m_RowsPerMeasure = 0; } + + bool HasTempoSwing() const { return !m_tempoSwing.empty(); } + const TempoSwing& GetTempoSwing() const { return m_tempoSwing; } + void SetTempoSwing(const TempoSwing &swing) { m_tempoSwing = swing; m_tempoSwing.Normalize(); } + void RemoveTempoSwing() { m_tempoSwing.clear(); } + + // Pattern name functions - bool functions return true on success. + bool SetName(const std::string &newName); + bool SetName(const char *newName, size_t maxChars); + template<size_t bufferSize> + bool SetName(const char (&buffer)[bufferSize]) + { + return SetName(buffer, bufferSize); + } + + std::string GetName() const { return m_PatternName; } + +#ifdef MODPLUG_TRACKER + // Double number of rows + bool Expand(); + + // Halve number of rows + bool Shrink(); +#endif // MODPLUG_TRACKER + + // Write some kind of effect data to the pattern + bool WriteEffect(EffectWriter &settings); + + typedef std::vector<ModCommand>::iterator iterator; + typedef std::vector<ModCommand>::const_iterator const_iterator; + + iterator begin() { return m_ModCommands.begin(); } + const_iterator begin() const { return m_ModCommands.begin(); } + const_iterator cbegin() const { return m_ModCommands.cbegin(); } + + iterator end() { return m_ModCommands.end(); } + const_iterator end() const { return m_ModCommands.end(); } + const_iterator cend() const { return m_ModCommands.cend(); } + + CPattern(CPatternContainer& patCont) : m_rPatternContainer(patCont) {} + CPattern(const CPattern &) = default; + CPattern(CPattern &&) noexcept = default; + +protected: + ModCommand& GetModCommand(size_t i) { return m_ModCommands[i]; } + //Returns modcommand from (floor[i/channelCount], i%channelCount) + + ModCommand& GetModCommand(ROWINDEX r, CHANNELINDEX c) { return m_ModCommands[r * GetNumChannels() + c]; } + const ModCommand& GetModCommand(ROWINDEX r, CHANNELINDEX c) const { return m_ModCommands[r * GetNumChannels() + c]; } + + +protected: + std::vector<ModCommand> m_ModCommands; + ROWINDEX m_Rows = 0; + ROWINDEX m_RowsPerBeat = 0; // patterns-specific time signature. if != 0, this is implicitely set. + ROWINDEX m_RowsPerMeasure = 0; // ditto + TempoSwing m_tempoSwing; + std::string m_PatternName; + CPatternContainer& m_rPatternContainer; +}; + + +const char FileIdPattern[] = "mptP"; + +void ReadModPattern(std::istream& iStrm, CPattern& patc, const size_t nSize = 0); +void WriteModPattern(std::ostream& oStrm, const CPattern& patc); + + +// Class for conveniently writing an effect to the pattern. + +class EffectWriter +{ + friend class CPattern; + + // Row advance mode + enum RetryMode + { + rmIgnore, // If effect can't be written, abort. + rmTryNextRow, // If effect can't be written, try next row. + rmTryPreviousRow, // If effect can't be written, try previous row. + }; + +public: + // Constructors with effect commands + EffectWriter(EffectCommand cmd, ModCommand::PARAM param) : m_command(cmd), m_param(param), m_isVolEffect(false) { Init(); } + EffectWriter(VolumeCommand cmd, ModCommand::VOL param) : m_volcmd(cmd), m_vol(param), m_isVolEffect(true) { Init(); } + + // Additional constructors: + // Set row in which writing should start + EffectWriter &Row(ROWINDEX row) { m_row = row; return *this; } + // Set channel to which writing should be restricted to + EffectWriter &Channel(CHANNELINDEX chn) { m_channel = chn; return *this; } + // Allow multiple effects of the same kind to be written in the same row. + EffectWriter &AllowMultiple() { m_allowMultiple = true; return *this; } + // Set retry mode. + EffectWriter &RetryNextRow() { m_retryMode = rmTryNextRow; return *this; } + EffectWriter &RetryPreviousRow() { m_retryMode = rmTryPreviousRow; return *this; } + +protected: + RetryMode m_retryMode; + ROWINDEX m_row; + CHANNELINDEX m_channel; + + union + { + EffectCommand m_command; + VolumeCommand m_volcmd; + }; + union + { + ModCommand::PARAM m_param; + ModCommand::VOL m_vol; + }; + + bool m_retry : 1; + bool m_allowMultiple : 1; + bool m_isVolEffect : 1; + + // Common data initialisation + void Init() + { + m_row = 0; + m_channel = CHANNELINDEX_INVALID; // Any channel + m_retryMode = rmIgnore; // If effect couldn't be written, abort. + m_retry = true; + m_allowMultiple = false; // Stop if same type of effect is encountered + } +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.cpp new file mode 100644 index 00000000..ace4406d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.cpp @@ -0,0 +1,201 @@ +/* + * PatternContainer.cpp + * -------------------- + * Purpose: Container class for managing patterns. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "patternContainer.h" +#include "Sndfile.h" +#include "mod_specifications.h" +#include "../common/serialization_utils.h" +#include "../common/version.h" + + +OPENMPT_NAMESPACE_BEGIN + + +void CPatternContainer::ClearPatterns() +{ + DestroyPatterns(); + m_Patterns.assign(m_Patterns.size(), CPattern(*this)); +} + + +void CPatternContainer::DestroyPatterns() +{ + m_Patterns.clear(); +} + + +PATTERNINDEX CPatternContainer::Duplicate(PATTERNINDEX from, bool respectQtyLimits) +{ + if(!IsValidPat(from)) + { + return PATTERNINDEX_INVALID; + } + + PATTERNINDEX newPatIndex = InsertAny(m_Patterns[from].GetNumRows(), respectQtyLimits); + + if(newPatIndex != PATTERNINDEX_INVALID) + { + m_Patterns[newPatIndex] = m_Patterns[from]; + } + return newPatIndex; +} + + +PATTERNINDEX CPatternContainer::InsertAny(const ROWINDEX rows, bool respectQtyLimits) +{ + PATTERNINDEX i = 0; + for(i = 0; i < m_Patterns.size(); i++) + if(!m_Patterns[i].IsValid()) break; + if(respectQtyLimits && i >= m_rSndFile.GetModSpecifications().patternsMax) + return PATTERNINDEX_INVALID; + if(!Insert(i, rows)) + return PATTERNINDEX_INVALID; + else return i; +} + + +bool CPatternContainer::Insert(const PATTERNINDEX index, const ROWINDEX rows) +{ + if(rows > MAX_PATTERN_ROWS || rows == 0 || index >= PATTERNINDEX_INVALID) + return false; + if(IsValidPat(index)) + return false; + + try + { + if(index >= m_Patterns.size()) + { + m_Patterns.resize(index + 1, CPattern(*this)); + } + m_Patterns[index].AllocatePattern(rows); + m_Patterns[index].RemoveSignature(); + m_Patterns[index].SetName(""); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + return m_Patterns[index].IsValid(); +} + + +void CPatternContainer::Remove(const PATTERNINDEX ipat) +{ + if(ipat < m_Patterns.size()) m_Patterns[ipat].Deallocate(); +} + + +bool CPatternContainer::IsPatternEmpty(const PATTERNINDEX nPat) const +{ + if(!IsValidPat(nPat)) + return false; + + for(const auto &m : m_Patterns[nPat].m_ModCommands) + { + if(!m.IsEmpty()) + return false; + } + return true; +} + + +void CPatternContainer::ResizeArray(const PATTERNINDEX newSize) +{ + m_Patterns.resize(newSize, CPattern(*this)); +} + + +void CPatternContainer::OnModTypeChanged(const MODTYPE /*oldtype*/) +{ + const CModSpecifications specs = m_rSndFile.GetModSpecifications(); + //if(specs.patternsMax < Size()) + // ResizeArray(specs.patternsMax); + + // remove pattern time signatures + if(!specs.hasPatternSignatures) + { + for(PATTERNINDEX nPat = 0; nPat < m_Patterns.size(); nPat++) + { + m_Patterns[nPat].RemoveSignature(); + m_Patterns[nPat].RemoveTempoSwing(); + } + } +} + + +PATTERNINDEX CPatternContainer::GetNumPatterns() const +{ + for(PATTERNINDEX pat = Size(); pat > 0; pat--) + { + if(IsValidPat(pat - 1)) + { + return pat; + } + } + return 0; +} + + +PATTERNINDEX CPatternContainer::GetNumNamedPatterns() const +{ + if(Size() == 0) + { + return 0; + } + for(PATTERNINDEX nPat = Size(); nPat > 0; nPat--) + { + if(!m_Patterns[nPat - 1].GetName().empty()) + { + return nPat; + } + } + return 0; +} + + + +void WriteModPatterns(std::ostream& oStrm, const CPatternContainer& patc) +{ + srlztn::SsbWrite ssb(oStrm); + ssb.BeginWrite(FileIdPatterns, Version::Current().GetRawVersion()); + const PATTERNINDEX nPatterns = patc.Size(); + uint16 nCount = 0; + for(uint16 i = 0; i < nPatterns; i++) if (patc[i].IsValid()) + { + ssb.WriteItem(patc[i], srlztn::ID::FromInt<uint16>(i), &WriteModPattern); + nCount = i + 1; + } + ssb.WriteItem<uint16>(nCount, "num"); // Index of last pattern + 1. + ssb.FinishWrite(); +} + + +void ReadModPatterns(std::istream& iStrm, CPatternContainer& patc, const size_t) +{ + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead(FileIdPatterns, Version::Current().GetRawVersion()); + if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) + return; + PATTERNINDEX nPatterns = patc.Size(); + uint16 nCount = uint16_max; + if (ssb.ReadItem(nCount, "num") != srlztn::SsbRead::EntryNotFound) + nPatterns = nCount; + LimitMax(nPatterns, ModSpecs::mptm.patternsMax); + if (nPatterns > patc.Size()) + patc.ResizeArray(nPatterns); + for(uint16 i = 0; i < nPatterns; i++) + { + ssb.ReadItem(patc[i], srlztn::ID::FromInt<uint16>(i), &ReadModPattern); + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.h b/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.h new file mode 100644 index 00000000..c085cedc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/patternContainer.h @@ -0,0 +1,115 @@ +/* + * PatternContainer.h + * ------------------ + * Purpose: Container class for managing patterns. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "pattern.h" + +#include <algorithm> + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +class CPatternContainer +{ +public: + CPattern& operator[](const int pat) { return m_Patterns[pat]; } + const CPattern& operator[](const int pat) const { return m_Patterns[pat]; } + +public: + CPatternContainer(CSoundFile& sndFile) : m_rSndFile(sndFile) { } + + // Empty and initialize all patterns. + void ClearPatterns(); + // Delete all patterns. + void DestroyPatterns(); + + // Insert (default)pattern to given position. If pattern already exists at that position, + // ignoring request. Returns true on success, false otherwise. + bool Insert(const PATTERNINDEX index, const ROWINDEX rows); + + // Insert pattern to position with the lowest index, and return that index, PATTERNINDEX_INVALID on failure. + // If respectQtyLimits is true, inserting patterns will fail if the resulting pattern index would exceed the current format's pattern quantity limits. + PATTERNINDEX InsertAny(const ROWINDEX rows, bool respectQtyLimits = false); + + // Duplicate an existing pattern. Returns new pattern index on success, or PATTERNINDEX_INVALID on failure. + // If respectQtyLimits is true, inserting patterns will fail if the resulting pattern index would exceed the current format's pattern quantity limits. + PATTERNINDEX Duplicate(PATTERNINDEX from, bool respectQtyLimits = false); + + //Remove pattern from given position. Currently it actually makes the pattern + //'invisible' - the pattern data is cleared but the actual pattern object won't get removed. + void Remove(const PATTERNINDEX index); + + // Applies function object for modcommands in patterns in given range. + // Return: Copy of the function object. + template <class Func> + Func ForEachModCommand(PATTERNINDEX nStartPat, PATTERNINDEX nLastPat, Func func); + template <class Func> + Func ForEachModCommand(Func func) { return ForEachModCommand(0, Size() - 1, func); } + + std::vector<CPattern>::iterator begin() { return m_Patterns.begin(); } + std::vector<CPattern>::const_iterator begin() const { return m_Patterns.begin(); } + std::vector<CPattern>::const_iterator cbegin() const { return m_Patterns.cbegin(); } + std::vector<CPattern>::iterator end() { return m_Patterns.end(); } + std::vector<CPattern>::const_iterator end() const { return m_Patterns.end(); } + std::vector<CPattern>::const_iterator cend() const { return m_Patterns.cend(); } + + PATTERNINDEX Size() const { return static_cast<PATTERNINDEX>(m_Patterns.size()); } + + CSoundFile& GetSoundFile() { return m_rSndFile; } + const CSoundFile& GetSoundFile() const { return m_rSndFile; } + + // Return true if pattern can be accessed with operator[](iPat), false otherwise. + bool IsValidIndex(const PATTERNINDEX iPat) const { return (iPat < Size()); } + + // Return true if IsValidIndex() is true and the corresponding pattern has allocated modcommand array, false otherwise. + bool IsValidPat(const PATTERNINDEX iPat) const { return IsValidIndex(iPat) && m_Patterns[iPat].IsValid(); } + + // Returns true if the pattern is empty, i.e. there are no notes/effects in this pattern + bool IsPatternEmpty(const PATTERNINDEX nPat) const; + + void ResizeArray(const PATTERNINDEX newSize); + + void OnModTypeChanged(const MODTYPE oldtype); + + // Returns index of last valid pattern + 1, zero if no such pattern exists. + PATTERNINDEX GetNumPatterns() const; + + // Returns index of highest pattern with pattern named + 1. + PATTERNINDEX GetNumNamedPatterns() const; + + +private: + std::vector<CPattern> m_Patterns; + CSoundFile &m_rSndFile; +}; + + +template <class Func> +Func CPatternContainer::ForEachModCommand(PATTERNINDEX nStartPat, PATTERNINDEX nLastPat, Func func) +{ + if (nStartPat > nLastPat || nLastPat >= Size()) + return func; + for (PATTERNINDEX nPat = nStartPat; nPat <= nLastPat; nPat++) if (m_Patterns[nPat].IsValid()) + std::for_each(m_Patterns[nPat].begin(), m_Patterns[nPat].end(), func); + return func; +} + + +const char FileIdPatterns[] = "mptPc"; + +void ReadModPatterns(std::istream& iStrm, CPatternContainer& patc, const size_t nSize = 0); +void WriteModPatterns(std::ostream& oStrm, const CPatternContainer& patc); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.cpp new file mode 100644 index 00000000..2ef25522 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.cpp @@ -0,0 +1,235 @@ +/* + * DigiBoosterEcho.cpp + * ------------------- + * Purpose: Implementation of the DigiBooster Pro Echo DSP + * Notes : (currently none) + * Authors: OpenMPT Devs, based on original code by Grzegorz Kraszewski (BSD 2-clause) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../Sndfile.h" +#include "DigiBoosterEcho.h" + +OPENMPT_NAMESPACE_BEGIN + +IMixPlugin* DigiBoosterEcho::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) DigiBoosterEcho(factory, sndFile, mixStruct); +} + + +DigiBoosterEcho::DigiBoosterEcho(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_sampleRate(sndFile.GetSampleRate()) + , m_chunk(PluginChunk::Default()) +{ + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void DigiBoosterEcho::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_bufferSize) + return; + const float *srcL = m_mixBuffer.GetInputBuffer(0), *srcR = m_mixBuffer.GetInputBuffer(1); + float *outL = m_mixBuffer.GetOutputBuffer(0), *outR = m_mixBuffer.GetOutputBuffer(1); + + for(uint32 i = numFrames; i != 0; i--) + { + int readPos = m_writePos - m_delayTime; + if(readPos < 0) + readPos += m_bufferSize; + + float l = *srcL++, r = *srcR++; + float lDelay = m_delayLine[readPos * 2], rDelay = m_delayLine[readPos * 2 + 1]; + + // Calculate the delay + float al = l * m_NCrossNBack; + al += r * m_PCrossNBack; + al += lDelay * m_NCrossPBack; + al += rDelay * m_PCrossPBack; + + float ar = r * m_NCrossNBack; + ar += l * m_PCrossNBack; + ar += rDelay * m_NCrossPBack; + ar += lDelay * m_PCrossPBack; + + // Prevent denormals + if(std::abs(al) < 1e-24f) + al = 0.0f; + if(std::abs(ar) < 1e-24f) + ar = 0.0f; + + m_delayLine[m_writePos * 2] = al; + m_delayLine[m_writePos * 2 + 1] = ar; + m_writePos++; + if(m_writePos == m_bufferSize) + m_writePos = 0; + + // Output samples now + *outL++ = (l * m_NMix + lDelay * m_PMix); + *outR++ = (r * m_NMix + rDelay * m_PMix); + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +void DigiBoosterEcho::SaveAllParameters() +{ + m_pMixStruct->defaultProgram = -1; + try + { + m_pMixStruct->pluginData.resize(sizeof(m_chunk)); + memcpy(m_pMixStruct->pluginData.data(), &m_chunk, sizeof(m_chunk)); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_pMixStruct->pluginData.clear(); + } +} + + +void DigiBoosterEcho::RestoreAllParameters(int32 program) +{ + if(m_pMixStruct->pluginData.size() == sizeof(m_chunk) && !memcmp(m_pMixStruct->pluginData.data(), "Echo", 4)) + { + memcpy(&m_chunk, m_pMixStruct->pluginData.data(), sizeof(m_chunk)); + } else + { + IMixPlugin::RestoreAllParameters(program); + } + RecalculateEchoParams(); +} + + +PlugParamValue DigiBoosterEcho::GetParameter(PlugParamIndex index) +{ + if(index < kEchoNumParameters) + { + return m_chunk.param[index] / 255.0f; + } + return 0; +} + + +void DigiBoosterEcho::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kEchoNumParameters) + { + m_chunk.param[index] = mpt::saturate_round<uint8>(mpt::safe_clamp(value, 0.0f, 1.0f) * 255.0f); + RecalculateEchoParams(); + } +} + + +void DigiBoosterEcho::Resume() +{ + m_isResumed = true; + m_sampleRate = m_SndFile.GetSampleRate(); + RecalculateEchoParams(); + PositionChanged(); +} + + +void DigiBoosterEcho::PositionChanged() +{ + m_bufferSize = (m_sampleRate >> 1) + (m_sampleRate >> 6); + try + { + m_delayLine.assign(m_bufferSize * 2, 0); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_bufferSize = 0; + } + m_writePos = 0; +} + + +#ifdef MODPLUG_TRACKER + +CString DigiBoosterEcho::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kEchoDelay: return _T("Delay"); + case kEchoFeedback: return _T("Feedback"); + case kEchoMix: return _T("Wet / Dry Ratio"); + case kEchoCross: return _T("Cross Echo"); + } + return CString(); +} + + +CString DigiBoosterEcho::GetParamLabel(PlugParamIndex param) +{ + if(param == kEchoDelay) + return _T("ms"); + return CString(); +} + + +CString DigiBoosterEcho::GetParamDisplay(PlugParamIndex param) +{ + CString s; + if(param == kEchoMix) + { + int wet = (m_chunk.param[kEchoMix] * 100) / 255; + s.Format(_T("%d%% / %d%%"), wet, 100 - wet); + } else if(param < kEchoNumParameters) + { + int val = m_chunk.param[param]; + if(param == kEchoDelay) + { + if(val == 0) + val = 167; + val *= 2; + } + s.Format(_T("%d"), val); + } + return s; +} + +#endif // MODPLUG_TRACKER + + +IMixPlugin::ChunkData DigiBoosterEcho::GetChunk(bool) +{ + auto data = reinterpret_cast<const std::byte *>(&m_chunk); + return ChunkData(data, sizeof(m_chunk)); +} + + +void DigiBoosterEcho::SetChunk(const ChunkData &chunk, bool) +{ + auto data = chunk.data(); + if(chunk.size() == sizeof(chunk) && !memcmp(data, "Echo", 4)) + { + memcpy(&m_chunk, data, chunk.size()); + RecalculateEchoParams(); + } +} + + +void DigiBoosterEcho::RecalculateEchoParams() +{ + // The fallback value when the delay is 0 was determined experimentally from DBPro 2.21 output. + // The C implementation of libdigibooster3 has no specific handling of this value and thus produces a delay with maximum length. + m_delayTime = ((m_chunk.param[kEchoDelay] ? m_chunk.param[kEchoDelay] : 167u) * m_sampleRate + 250u) / 500u; + m_PMix = (m_chunk.param[kEchoMix]) * (1.0f / 256.0f); + m_NMix = (256 - m_chunk.param[kEchoMix]) * (1.0f / 256.0f); + m_PCrossPBack = (m_chunk.param[kEchoCross] * m_chunk.param[kEchoFeedback]) * (1.0f / 65536.0f); + m_PCrossNBack = (m_chunk.param[kEchoCross] * (256 - m_chunk.param[kEchoFeedback])) * (1.0f / 65536.0f); + m_NCrossPBack = ((m_chunk.param[kEchoCross] - 256) * m_chunk.param[kEchoFeedback]) * (1.0f / 65536.0f); + m_NCrossNBack = ((m_chunk.param[kEchoCross] - 256) * (m_chunk.param[kEchoFeedback] - 256)) * (1.0f / 65536.0f); +} + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.h new file mode 100644 index 00000000..8d0de4d8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/DigiBoosterEcho.h @@ -0,0 +1,126 @@ +/* + * DigiBoosterEcho.h + * ----------------- + * Purpose: Implementation of the DigiBooster Pro Echo DSP + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +class DigiBoosterEcho final : public IMixPlugin +{ +public: + enum Parameters + { + kEchoDelay = 0, + kEchoFeedback, + kEchoMix, + kEchoCross, + kEchoNumParameters + }; + + // Our settings chunk for file I/O, as it will be written to files + struct PluginChunk + { + char id[4]; + uint8 param[kEchoNumParameters]; + + static PluginChunk Create(uint8 delay, uint8 feedback, uint8 mix, uint8 cross) + { + static_assert(sizeof(PluginChunk) == 8); + PluginChunk result; + memcpy(result.id, "Echo", 4); + result.param[kEchoDelay] = delay; + result.param[kEchoFeedback] = feedback; + result.param[kEchoMix] = mix; + result.param[kEchoCross] = cross; + return result; + } + static PluginChunk Default() + { + return Create(80, 150, 80, 255); + } + }; + +protected: + std::vector<float> m_delayLine; // Echo delay line + uint32 m_bufferSize = 0; // Delay line length in frames + uint32 m_writePos = 0; // Current write position in the delay line + uint32 m_delayTime = 0; // In frames + uint32 m_sampleRate = 0; + + // Echo calculation coefficients + float m_PMix, m_NMix; + float m_PCrossPBack, m_PCrossNBack; + float m_NCrossPBack, m_NCrossNBack; + + // Settings chunk for file I/O + PluginChunk m_chunk; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + DigiBoosterEcho(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + void SaveAllParameters() override; + void RestoreAllParameters(int32 program) override; + int32 GetUID() const override { int32le id; memcpy(&id, "Echo", 4); return id; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kEchoNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Echo"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + + bool ProgramsAreChunks() const override { return true; } + ChunkData GetChunk(bool) override; + void SetChunk(const ChunkData &chunk, bool) override; + +protected: + void RecalculateEchoParams(); +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.cpp new file mode 100644 index 00000000..52ca4484 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.cpp @@ -0,0 +1,521 @@ +/* + * LFOPlugin.cpp + * ------------- + * Purpose: Plugin for automating other plugins' parameters + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "LFOPlugin.h" +#include "../Sndfile.h" +#include "../../common/FileReader.h" +#ifdef MODPLUG_TRACKER +#include "../../mptrack/plugins/LFOPluginEditor.h" +#endif // MODPLUG_TRACKER +#include "mpt/base/numbers.hpp" + +OPENMPT_NAMESPACE_BEGIN + +IMixPlugin* LFOPlugin::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) LFOPlugin(factory, sndFile, mixStruct); +} + + +LFOPlugin::LFOPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_PRNG(mpt::make_prng<mpt::fast_prng>(mpt::global_prng())) +{ + RecalculateFrequency(); + RecalculateIncrement(); + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +// Processing (we do not process audio, just send out parameters) +void LFOPlugin::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_bypassed) + { + ResetSilence(); + if(m_tempoSync) + { + double tempo = m_SndFile.GetCurrentBPM(); + if(tempo != m_tempo) + { + m_tempo = tempo; + RecalculateIncrement(); + } + } + + if(m_oneshot) + { + LimitMax(m_phase, 1.0); + } else + { + int intPhase = static_cast<int>(m_phase); + if(intPhase > 0 && (m_waveForm == kSHNoise || m_waveForm == kSmoothNoise)) + { + // Phase wrap-around happened + NextRandom(); + } + m_phase -= intPhase; + } + + double value = 0; + switch(m_waveForm) + { + case kSine: + value = std::sin(m_phase * (2.0 * mpt::numbers::pi)); + break; + case kTriangle: + value = 1.0 - 4.0 * std::abs(m_phase - 0.5); + break; + case kSaw: + value = 2.0 * m_phase - 1.0; + break; + case kSquare: + value = m_phase < 0.5 ? -1.0 : 1.0; + break; + case kSHNoise: + value = m_random; + break; + case kSmoothNoise: + value = m_phase * m_phase * m_phase * (m_phase * (m_phase * 6 - 15) + 10); // Smootherstep + value = m_nextRandom * value + m_random * (1.0 - value); + break; + default: + break; + } + if(m_polarity) + value = -value; + // Transform value from -1...+1 to 0...1 range and apply offset/amplitude + value = value * m_amplitude + m_offset; + Limit(value, 0.0, 1.0); + + IMixPlugin *plugin = GetOutputPlugin(); + if(plugin != nullptr) + { + if(m_outputToCC) + { + plugin->MidiSend(MIDIEvents::CC(static_cast<MIDIEvents::MidiCC>(m_outputParam & 0x7F), static_cast<uint8>((m_outputParam >> 8) & 0x0F), mpt::saturate_round<uint8>(value * 127.0f))); + } else + { + plugin->SetParameter(m_outputParam, static_cast<PlugParamValue>(value)); + } + } + + m_phase += m_increment * numFrames; + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1), numFrames); +} + + +PlugParamValue LFOPlugin::GetParameter(PlugParamIndex index) +{ + switch(index) + { + case kAmplitude: return m_amplitude; + case kOffset: return m_offset; + case kFrequency: return m_frequency; + case kTempoSync: return m_tempoSync ? 1.0f : 0.0f; + case kWaveform: return WaveformToParam(m_waveForm); + case kPolarity: return m_polarity ? 1.0f : 0.0f; + case kBypassed: return m_bypassed ? 1.0f : 0.0f; + case kLoopMode: return m_oneshot ? 1.0f : 0.0f; + default: return 0; + } +} + + +void LFOPlugin::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + ResetSilence(); + value = mpt::safe_clamp(value, 0.0f, 1.0f); + switch(index) + { + case kAmplitude: m_amplitude = value; break; + case kOffset: m_offset = value; break; + case kFrequency: + m_frequency = value; + RecalculateFrequency(); + break; + case kTempoSync: + m_tempoSync = (value >= 0.5f); + RecalculateFrequency(); + break; + case kWaveform: + m_waveForm = ParamToWaveform(value); + break; + case kPolarity: m_polarity = (value >= 0.5f); break; + case kBypassed: m_bypassed = (value >= 0.5f); break; + case kLoopMode: m_oneshot = (value >= 0.5f); break; + case kCurrentPhase: + if(value == 0) + { + // Enforce next random value for random LFOs + NextRandom(); + } + m_phase = value; + return; + + default: return; + } + +#ifdef MODPLUG_TRACKER + if(GetEditor() != nullptr) + { + GetEditor()->PostMessage(WM_PARAM_UDPATE, GetSlot(), index); + } +#endif +} + + +void LFOPlugin::Resume() +{ + m_isResumed = true; + RecalculateIncrement(); + NextRandom(); + PositionChanged(); +} + + +void LFOPlugin::PositionChanged() +{ + // TODO Changing tempo (with tempo sync enabled), parameter automation over time and setting the LFO phase manually is not considered here. + m_phase = m_increment * m_SndFile.GetTotalSampleCount(); + m_phase -= static_cast<int64>(m_phase); +} + + +bool LFOPlugin::MidiSend(uint32 midiCode) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + return plugin->MidiSend(midiCode); + else + return true; +} + + +bool LFOPlugin::MidiSysexSend(mpt::const_byte_span sysex) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + return plugin->MidiSysexSend(sysex); + else + return true; +} + + +void LFOPlugin::MidiCC(MIDIEvents::MidiCC nController, uint8 nParam, CHANNELINDEX trackChannel) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + { + plugin->MidiCC(nController, nParam, trackChannel); + } +} + + +void LFOPlugin::MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackChannel) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + { + plugin->MidiPitchBend(increment, pwd, trackChannel); + } +} + + +void LFOPlugin::MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackChannel) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + { + plugin->MidiVibrato(depth, pwd, trackChannel); + } +} + + +void LFOPlugin::MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) +{ + if(ModCommand::IsNote(static_cast<ModCommand::NOTE>(note)) && vol > 0) + { + SetParameter(kCurrentPhase, 0); + } + if(IMixPlugin *plugin = GetOutputPlugin()) + { + plugin->MidiCommand(instr, note, vol, trackChannel); + } +} + + +void LFOPlugin::HardAllNotesOff() +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + { + plugin->HardAllNotesOff(); + } +} + + +bool LFOPlugin::IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) +{ + if(IMixPlugin *plugin = GetOutputPlugin()) + return plugin->IsNotePlaying(note, trackerChn); + else + return false; +} + + +void LFOPlugin::SaveAllParameters() +{ + auto chunk = GetChunk(false); + if(chunk.empty()) + return; + + m_pMixStruct->defaultProgram = -1; + m_pMixStruct->pluginData.assign(chunk.begin(), chunk.end()); +} + + +void LFOPlugin::RestoreAllParameters(int32 /*program*/) +{ + SetChunk(mpt::as_span(m_pMixStruct->pluginData), false); +} + + +struct PluginData +{ + char magic[4]; + uint32le version; + uint32le amplitude; // float + uint32le offset; // float + uint32le frequency; // float + uint32le waveForm; + uint32le outputParam; + uint8le tempoSync; + uint8le polarity; + uint8le bypassed; + uint8le outputToCC; + uint8le loopMode; +}; + +MPT_BINARY_STRUCT(PluginData, 33) + + +IMixPlugin::ChunkData LFOPlugin::GetChunk(bool) +{ + PluginData chunk; + memcpy(chunk.magic, "LFO ", 4); + chunk.version = 0; + chunk.amplitude = IEEE754binary32LE(m_amplitude).GetInt32(); + chunk.offset = IEEE754binary32LE(m_offset).GetInt32(); + chunk.frequency = IEEE754binary32LE(m_frequency).GetInt32(); + chunk.waveForm = m_waveForm; + chunk.outputParam = m_outputParam; + chunk.tempoSync = m_tempoSync ? 1 : 0; + chunk.polarity = m_polarity ? 1 : 0; + chunk.bypassed = m_bypassed ? 1 : 0; + chunk.outputToCC = m_outputToCC ? 1 : 0; + chunk.loopMode = m_oneshot ? 1 : 0; + + m_chunkData.resize(sizeof(chunk)); + memcpy(m_chunkData.data(), &chunk, sizeof(chunk)); + return mpt::as_span(m_chunkData); +} + + +void LFOPlugin::SetChunk(const ChunkData &chunk, bool) +{ + FileReader file(chunk); + PluginData data; + if(file.ReadStructPartial(data, file.BytesLeft()) + && !memcmp(data.magic, "LFO ", 4) + && data.version == 0) + { + const float amplitude = IEEE754binary32LE().SetInt32(data.amplitude); + m_amplitude = mpt::safe_clamp(amplitude, 0.0f, 1.0f); + const float offset = IEEE754binary32LE().SetInt32(data.offset); + m_offset = mpt::safe_clamp(offset, 0.0f, 1.0f); + const float frequency = IEEE754binary32LE().SetInt32(data.frequency); + m_frequency = mpt::safe_clamp(frequency, 0.0f, 1.0f); + if(data.waveForm < kNumWaveforms) + m_waveForm = static_cast<LFOWaveform>(data.waveForm.get()); + m_outputParam = data.outputParam; + m_tempoSync = data.tempoSync != 0; + m_polarity = data.polarity != 0; + m_bypassed = data.bypassed != 0; + m_outputToCC = data.outputToCC != 0; + m_oneshot = data.loopMode != 0; + RecalculateFrequency(); + } +} + + +#ifdef MODPLUG_TRACKER + +std::pair<PlugParamValue, PlugParamValue> LFOPlugin::GetParamUIRange(PlugParamIndex param) +{ + if(param == kWaveform) + return {0.0f, WaveformToParam(static_cast<LFOWaveform>(kNumWaveforms - 1))}; + else + return {0.0f, 1.0f}; +} + +CString LFOPlugin::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kAmplitude: return _T("Amplitude"); + case kOffset: return _T("Offset"); + case kFrequency: return _T("Frequency"); + case kTempoSync: return _T("Tempo Sync"); + case kWaveform: return _T("Waveform"); + case kPolarity: return _T("Polarity"); + case kBypassed: return _T("Bypassed"); + case kLoopMode: return _T("Loop Mode"); + case kCurrentPhase: return _T("Set LFO Phase"); + } + return CString(); +} + + +CString LFOPlugin::GetParamLabel(PlugParamIndex param) +{ + if(param == kFrequency) + { + if(m_tempoSync && m_computedFrequency > 0.0 && m_computedFrequency < 1.0) + return _T("Beats Per Cycle"); + else if(m_tempoSync) + return _T("Cycles Per Beat"); + else + return _T("Hz"); + } + return CString(); +} + + +CString LFOPlugin::GetParamDisplay(PlugParamIndex param) +{ + CString s; + if(param == kPolarity) + { + return m_polarity ? _T("Inverted") : _T("Normal"); + } else if(param == kTempoSync) + { + return m_tempoSync ? _T("Yes") : _T("No"); + } else if(param == kBypassed) + { + return m_bypassed ? _T("Yes") : _T("No"); + } else if(param == kWaveform) + { + static constexpr const TCHAR * const waveforms[] = { _T("Sine"), _T("Triangle"), _T("Saw"), _T("Square"), _T("Noise"), _T("Smoothed Noise") }; + if(m_waveForm < static_cast<int>(std::size(waveforms))) + return waveforms[m_waveForm]; + } else if(param == kLoopMode) + { + return m_oneshot ? _T("One-Shot") : _T("Looped"); + } else if(param == kCurrentPhase) + { + return _T("Write-Only"); + } else if(param < kLFONumParameters) + { + auto val = GetParameter(param); + if(param == kOffset) + val = 2.0f * val - 1.0f; + if(param == kFrequency) + { + val = static_cast<PlugParamValue>(m_computedFrequency); + if(m_tempoSync && val > 0.0f && val < 1.0f) + val = static_cast<PlugParamValue>(1.0 / m_computedFrequency); + } + s.Format(_T("%.3f"), val); + } + return s; +} + + +CAbstractVstEditor *LFOPlugin::OpenEditor() +{ + try + { + return new LFOPluginEditor(*this); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return nullptr; + } +} + +#endif // MODPLUG_TRACKER + + +void LFOPlugin::NextRandom() +{ + m_random = m_nextRandom; + m_nextRandom = mpt::random<int32>(m_PRNG) / static_cast<float>(int32_min); +} + + +void LFOPlugin::RecalculateFrequency() +{ + m_computedFrequency = 0.25 * std::pow(2.0, m_frequency * 8.0) - 0.25; + if(m_tempoSync) + { + if(m_computedFrequency > 0.00045) + { + double freqLog = std::log(m_computedFrequency) / mpt::numbers::ln2; + double freqFrac = freqLog - std::floor(freqLog); + freqLog -= freqFrac; + + // Lock to powers of two and 1.5 times or 1.333333... times the powers of two + if(freqFrac < 0.20751874963942190927313052802609) + freqFrac = 0.0; + else if(freqFrac < 0.5) + freqFrac = 0.41503749927884381854626105605218; + else if(freqFrac < 0.79248125036057809072686947197391) + freqFrac = 0.58496250072115618145373894394782; + else + freqFrac = 1.0; + + m_computedFrequency = std::pow(2.0, freqLog + freqFrac) * 0.5; + } else + { + m_computedFrequency = 0; + } + } + RecalculateIncrement(); +} + + +void LFOPlugin::RecalculateIncrement() +{ + m_increment = m_computedFrequency / m_SndFile.GetSampleRate(); + if(m_tempoSync) + { + m_increment *= m_tempo / 60.0; + } +} + + +IMixPlugin *LFOPlugin::GetOutputPlugin() const +{ + PLUGINDEX outPlug = m_pMixStruct->GetOutputPlugin(); + if(outPlug > m_nSlot && outPlug < MAX_MIXPLUGINS) + return m_SndFile.m_MixPlugins[outPlug].pMixPlugin; + else + return nullptr; +} + + +OPENMPT_NAMESPACE_END + +#else +MPT_MSVC_WORKAROUND_LNK4221(LFOPlugin) + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.h new file mode 100644 index 00000000..56b70e9a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/LFOPlugin.h @@ -0,0 +1,156 @@ +/* + * LFOPlugin.h + * ----------- + * Purpose: Plugin for automating other plugins' parameters + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "PlugInterface.h" +#include "../../common/mptRandom.h" + +OPENMPT_NAMESPACE_BEGIN + +class LFOPlugin final : public IMixPlugin +{ + friend class LFOPluginEditor; + +protected: + enum Parameters + { + kAmplitude = 0, + kOffset, + kFrequency, + kTempoSync, + kWaveform, + kPolarity, + kBypassed, + kLoopMode, + kCurrentPhase, + kLFONumParameters + }; + + enum LFOWaveform + { + kSine = 0, + kTriangle, + kSaw, + kSquare, + kSHNoise, + kSmoothNoise, + kNumWaveforms + }; + + std::vector<std::byte> m_chunkData; + + static constexpr PlugParamIndex INVALID_OUTPUT_PARAM = uint32_max; + + // LFO parameters + float m_amplitude = 0.5f, m_offset = 0.5f, m_frequency = 0.290241f; + LFOWaveform m_waveForm = kSine; + PlugParamIndex m_outputParam = INVALID_OUTPUT_PARAM; + bool m_tempoSync = false, m_polarity = false, m_bypassed = false, m_outputToCC = false, m_oneshot = false; + + // LFO state + double m_computedFrequency = 0.0; + double m_phase = 0.0, m_increment = 0.0; + double m_random = 0.0, m_nextRandom = 0.0; + double m_tempo = 0.0; + + mpt::fast_prng m_PRNG; + +#ifdef MODPLUG_TRACKER + static constexpr int WM_PARAM_UDPATE = WM_USER + 500; +#endif + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + LFOPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { int32 id; memcpy(&id, "LFO ", 4); return id; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + // MIDI event handling (mostly passing it through to the follow-up plugin) + bool MidiSend(uint32 midiCode) override; + bool MidiSysexSend(mpt::const_byte_span sysex) override; + void MidiCC(MIDIEvents::MidiCC nController, uint8 nParam, CHANNELINDEX trackChannel) override; + void MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackChannel) override; + void MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackChannel) override; + void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override; + void HardAllNotesOff() override; + bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override; + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kLFONumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("LFO"); } + + std::pair<PlugParamValue, PlugParamValue> GetParamUIRange(PlugParamIndex param) override; + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return true; } +protected: + CAbstractVstEditor *OpenEditor() override; +#endif + +public: + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + + bool ProgramsAreChunks() const override { return true; } + // Save parameters for storing them in a module file + void SaveAllParameters() override; + // Restore parameters from module file + void RestoreAllParameters(int32 program) override; + ChunkData GetChunk(bool) override; + void SetChunk(const ChunkData &chunk, bool) override; + +protected: + void NextRandom(); + void RecalculateFrequency(); + void RecalculateIncrement(); + IMixPlugin *GetOutputPlugin() const; + +public: + static LFOWaveform ParamToWaveform(float param) { return static_cast<LFOWaveform>(std::clamp(mpt::saturate_round<int>(param * 32.0f), 0, kNumWaveforms - 1)); } + static float WaveformToParam(LFOWaveform waveform) { return static_cast<int>(waveform) / 32.0f; } +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/OpCodes.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/OpCodes.h new file mode 100644 index 00000000..a8d303bb --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/OpCodes.h @@ -0,0 +1,103 @@ +/* + * OpCodes.h + * --------- + * Purpose: A human-readable list of VST opcodes, for error reporting purposes. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +#ifdef MPT_WITH_VST +inline constexpr const char *VstOpCodes[] = +{ + "effOpen", + "effClose", + "effSetProgram", + "effGetProgram", + "effSetProgramName", + "effGetProgramName", + "effGetParamLabel", + "effGetParamDisplay", + "effGetParamName", + "effGetVu", + "effSetSampleRate", + "effSetBlockSize", + "effMainsChanged", + "effEditGetRect", + "effEditOpen", + "effEditClose", + "effEditDraw", + "effEditMouse", + "effEditKey", + "effEditIdle", + "effEditTop", + "effEditSleep", + "effIdentify", + "effGetChunk", + "effSetChunk", + "effProcessEvents", + "effCanBeAutomated", + "effString2Parameter", + "effGetNumProgramCategories", + "effGetProgramNameIndexed", + "effCopyProgram", + "effConnectInput", + "effConnectOutput", + "effGetInputProperties", + "effGetOutputProperties", + "effGetPlugCategory", + "effGetCurrentPosition", + "effGetDestinationBuffer", + "effOfflineNotify", + "effOfflinePrepare", + "effOfflineRun", + "effProcessVarIo", + "effSetSpeakerArrangement", + "effSetBlockSizeAndSampleRate", + "effSetBypass", + "effGetEffectName", + "effGetErrorText", + "effGetVendorString", + "effGetProductString", + "effGetVendorVersion", + "effVendorSpecific", + "effCanDo", + "effGetTailSize", + "effIdle", + "effGetIcon", + "effSetViewPosition", + "effGetParameterProperties", + "effKeysRequired", + "effGetVstVersion", + "effEditKeyDown", + "effEditKeyUp", + "effSetEditKnobMode", + "effGetMidiProgramName", + "effGetCurrentMidiProgram", + "effGetMidiProgramCategory", + "effHasMidiProgramsChanged", + "effGetMidiKeyName", + "effBeginSetProgram", + "effEndSetProgram", + "effGetSpeakerArrangement", + "effShellGetNextPlugin", + "effStartProcess", + "effStopProcess", + "effSetTotalSampleToProcess", + "effSetPanLaw", + "effBeginLoadBank", + "effBeginLoadProgram", + "effSetProcessPrecision", + "effGetNumMidiInputChannels", + "effGetNumMidiOutputChannels" +}; +#endif + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.cpp new file mode 100644 index 00000000..7e524c7a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.cpp @@ -0,0 +1,1065 @@ +/* + * PlugInterface.cpp + * ----------------- + * Purpose: Default plugin interface implementation + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "../Sndfile.h" +#include "PlugInterface.h" +#include "PluginManager.h" +#include "../../common/FileReader.h" +#ifdef MODPLUG_TRACKER +#include "../../mptrack/Moddoc.h" +#include "../../mptrack/Mainfrm.h" +#include "../../mptrack/InputHandler.h" +#include "../../mptrack/AbstractVstEditor.h" +#include "../../mptrack/DefaultVstEditor.h" +// LoadProgram/SaveProgram +#include "../../mptrack/FileDialog.h" +#include "../../mptrack/VstPresets.h" +#include "../../common/mptFileIO.h" +#include "../mod_specifications.h" +#endif // MODPLUG_TRACKER +#include "mpt/base/aligned_array.hpp" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_span.hpp" + +#include <cmath> + +#ifndef NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MODPLUG_TRACKER +CModDoc *IMixPlugin::GetModDoc() { return m_SndFile.GetpModDoc(); } +const CModDoc *IMixPlugin::GetModDoc() const { return m_SndFile.GetpModDoc(); } +#endif // MODPLUG_TRACKER + + +IMixPlugin::IMixPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : m_Factory(factory) + , m_SndFile(sndFile) + , m_pMixStruct(mixStruct) +{ + m_SndFile.m_loadedPlugins++; + m_MixState.pMixBuffer = mpt::align_bytes<8, MIXBUFFERSIZE * 2>(m_MixBuffer); + while(m_pMixStruct != &(m_SndFile.m_MixPlugins[m_nSlot]) && m_nSlot < MAX_MIXPLUGINS - 1) + { + m_nSlot++; + } +} + + +IMixPlugin::~IMixPlugin() +{ +#ifdef MODPLUG_TRACKER + CloseEditor(); + CriticalSection cs; +#endif // MODPLUG_TRACKER + + // First thing to do, if we don't want to hang in a loop + if (m_Factory.pPluginsList == this) m_Factory.pPluginsList = m_pNext; + if (m_pMixStruct) + { + m_pMixStruct->pMixPlugin = nullptr; + m_pMixStruct = nullptr; + } + + if (m_pNext) m_pNext->m_pPrev = m_pPrev; + if (m_pPrev) m_pPrev->m_pNext = m_pNext; + m_pPrev = nullptr; + m_pNext = nullptr; + m_SndFile.m_loadedPlugins--; +} + + +void IMixPlugin::InsertIntoFactoryList() +{ + m_pMixStruct->pMixPlugin = this; + + m_pNext = m_Factory.pPluginsList; + if(m_Factory.pPluginsList) + { + m_Factory.pPluginsList->m_pPrev = this; + } + m_Factory.pPluginsList = this; +} + + +#ifdef MODPLUG_TRACKER + +void IMixPlugin::SetSlot(PLUGINDEX slot) +{ + m_nSlot = slot; + m_pMixStruct = &m_SndFile.m_MixPlugins[slot]; +} + + +PlugParamValue IMixPlugin::GetScaledUIParam(PlugParamIndex param) +{ + const auto [paramMin, paramMax] = GetParamUIRange(param); + return (std::clamp(GetParameter(param), paramMin, paramMax) - paramMin) / (paramMax - paramMin); +} + + +void IMixPlugin::SetScaledUIParam(PlugParamIndex param, PlugParamValue value) +{ + const auto [paramMin, paramMax] = GetParamUIRange(param); + const auto scaledVal = paramMin + std::clamp(value, 0.0f, 1.0f) * (paramMax - paramMin); + SetParameter(param, scaledVal); +} + + +CString IMixPlugin::GetFormattedParamName(PlugParamIndex param) +{ + CString paramName = GetParamName(param); + CString name; + if(paramName.IsEmpty()) + { + name = MPT_CFORMAT("{}: Parameter {}")(mpt::cfmt::dec0<2>(param), mpt::cfmt::dec0<2>(param)); + } else + { + name = MPT_CFORMAT("{}: {}")(mpt::cfmt::dec0<2>(param), paramName); + } + return name; +} + + +// Get a parameter's current value, represented by the plugin. +CString IMixPlugin::GetFormattedParamValue(PlugParamIndex param) +{ + + CString paramDisplay = GetParamDisplay(param); + CString paramUnits = GetParamLabel(param); + paramDisplay.Trim(); + paramUnits.Trim(); + paramDisplay += _T(" ") + paramUnits; + + return paramDisplay; +} + + +CString IMixPlugin::GetFormattedProgramName(int32 index) +{ + CString rawname = GetProgramName(index); + + // Let's start counting at 1 for the program name (as most MIDI hardware / software does) + index++; + + CString formattedName; + if(rawname[0] >= 0 && rawname[0] < _T(' ')) + formattedName = MPT_CFORMAT("{} - Program {}")(mpt::cfmt::dec0<2>(index), index); + else + formattedName = MPT_CFORMAT("{} - {}")(mpt::cfmt::dec0<2>(index), rawname); + + return formattedName; +} + + +void IMixPlugin::SetEditorPos(int32 x, int32 y) +{ + m_pMixStruct->editorX = x; + m_pMixStruct->editorY = y; +} + + +void IMixPlugin::GetEditorPos(int32 &x, int32 &y) const +{ + x = m_pMixStruct->editorX; + y = m_pMixStruct->editorY; +} + + +#endif // MODPLUG_TRACKER + + +bool IMixPlugin::IsBypassed() const +{ + return m_pMixStruct != nullptr && m_pMixStruct->IsBypassed(); +} + + +void IMixPlugin::RecalculateGain() +{ + float gain = 0.1f * static_cast<float>(m_pMixStruct ? m_pMixStruct->GetGain() : 10); + if(gain < 0.1f) gain = 1.0f; + + if(IsInstrument()) + { + gain /= m_SndFile.GetPlayConfig().getVSTiAttenuation(); + gain = static_cast<float>(gain * (m_SndFile.m_nVSTiVolume / m_SndFile.GetPlayConfig().getNormalVSTiVol())); + } + m_fGain = gain; +} + + +void IMixPlugin::SetDryRatio(float dryRatio) +{ + m_pMixStruct->fDryRatio = std::clamp(dryRatio, 0.0f, 1.0f); +#ifdef MODPLUG_TRACKER + m_SndFile.m_pluginDryWetRatioChanged.set(m_nSlot); +#endif // MODPLUG_TRACKER +} + + +void IMixPlugin::Bypass(bool bypass) +{ + m_pMixStruct->Info.SetBypass(bypass); + +#ifdef MODPLUG_TRACKER + if(m_SndFile.GetpModDoc()) + m_SndFile.GetpModDoc()->UpdateAllViews(nullptr, PluginHint(m_nSlot + 1).Info(), nullptr); +#endif // MODPLUG_TRACKER +} + + +double IMixPlugin::GetOutputLatency() const +{ + if(GetSoundFile().IsRenderingToDisc()) + return 0; + else + return GetSoundFile().m_TimingInfo.OutputLatency; +} + + +void IMixPlugin::ProcessMixOps(float * MPT_RESTRICT pOutL, float * MPT_RESTRICT pOutR, float * MPT_RESTRICT leftPlugOutput, float * MPT_RESTRICT rightPlugOutput, uint32 numFrames) +{ +/* float *leftPlugOutput; + float *rightPlugOutput; + + if(m_Effect.numOutputs == 1) + { + // If there was just the one plugin output we copy it into our 2 outputs + leftPlugOutput = rightPlugOutput = mixBuffer.GetOutputBuffer(0); + } else if(m_Effect.numOutputs > 1) + { + // Otherwise we actually only cater for two outputs max (outputs > 2 have been mixed together already). + leftPlugOutput = mixBuffer.GetOutputBuffer(0); + rightPlugOutput = mixBuffer.GetOutputBuffer(1); + } else + { + return; + }*/ + + // -> mixop == 0 : normal processing + // -> mixop == 1 : MIX += DRY - WET * wetRatio + // -> mixop == 2 : MIX += WET - DRY * dryRatio + // -> mixop == 3 : MIX -= WET - DRY * wetRatio + // -> mixop == 4 : MIX -= middle - WET * wetRatio + middle - DRY + // -> mixop == 5 : MIX_L += wetRatio * (WET_L - DRY_L) + dryRatio * (DRY_R - WET_R) + // MIX_R += dryRatio * (WET_L - DRY_L) + wetRatio * (DRY_R - WET_R) + + MPT_ASSERT(m_pMixStruct != nullptr); + + int mixop; + if(IsInstrument()) + { + // Force normal mix mode for instruments + mixop = 0; + } else + { + mixop = m_pMixStruct->GetMixMode(); + } + + float wetRatio = 1 - m_pMixStruct->fDryRatio; + float dryRatio = IsInstrument() ? 1 : m_pMixStruct->fDryRatio; // Always mix full dry if this is an instrument + + // Wet / Dry range expansion [0,1] -> [-1,1] + if(GetNumInputChannels() > 0 && m_pMixStruct->IsExpandedMix()) + { + wetRatio = 2.0f * wetRatio - 1.0f; + dryRatio = -wetRatio; + } + + wetRatio *= m_fGain; + dryRatio *= m_fGain; + + float * MPT_RESTRICT plugInputL = m_mixBuffer.GetInputBuffer(0); + float * MPT_RESTRICT plugInputR = m_mixBuffer.GetInputBuffer(1); + + // Mix operation + switch(mixop) + { + + // Default mix + case 0: + for(uint32 i = 0; i < numFrames; i++) + { + //rewbs.wetratio - added the factors. [20040123] + pOutL[i] += leftPlugOutput[i] * wetRatio + plugInputL[i] * dryRatio; + pOutR[i] += rightPlugOutput[i] * wetRatio + plugInputR[i] * dryRatio; + } + break; + + // Wet subtract + case 1: + for(uint32 i = 0; i < numFrames; i++) + { + pOutL[i] += plugInputL[i] - leftPlugOutput[i] * wetRatio; + pOutR[i] += plugInputR[i] - rightPlugOutput[i] * wetRatio; + } + break; + + // Dry subtract + case 2: + for(uint32 i = 0; i < numFrames; i++) + { + pOutL[i] += leftPlugOutput[i] - plugInputL[i] * dryRatio; + pOutR[i] += rightPlugOutput[i] - plugInputR[i] * dryRatio; + } + break; + + // Mix subtract + case 3: + for(uint32 i = 0; i < numFrames; i++) + { + pOutL[i] -= leftPlugOutput[i] - plugInputL[i] * wetRatio; + pOutR[i] -= rightPlugOutput[i] - plugInputR[i] * wetRatio; + } + break; + + // Middle subtract + case 4: + for(uint32 i = 0; i < numFrames; i++) + { + float middle = (pOutL[i] + plugInputL[i] + pOutR[i] + plugInputR[i]) / 2.0f; + pOutL[i] -= middle - leftPlugOutput[i] * wetRatio + middle - plugInputL[i]; + pOutR[i] -= middle - rightPlugOutput[i] * wetRatio + middle - plugInputR[i]; + } + break; + + // Left / Right balance + case 5: + if(m_pMixStruct->IsExpandedMix()) + { + wetRatio /= 2.0f; + dryRatio /= 2.0f; + } + + for(uint32 i = 0; i < numFrames; i++) + { + pOutL[i] += wetRatio * (leftPlugOutput[i] - plugInputL[i]) + dryRatio * (plugInputR[i] - rightPlugOutput[i]); + pOutR[i] += dryRatio * (leftPlugOutput[i] - plugInputL[i]) + wetRatio * (plugInputR[i] - rightPlugOutput[i]); + } + break; + } + + // If dry mix is ticked, we add the unprocessed buffer, + // except if this is an instrument since then it has already been done: + if(m_pMixStruct->IsWetMix() && !IsInstrument()) + { + for(uint32 i = 0; i < numFrames; i++) + { + pOutL[i] += plugInputL[i]; + pOutR[i] += plugInputR[i]; + } + } +} + + +// Render some silence and return maximum level returned by the plugin. +float IMixPlugin::RenderSilence(uint32 numFrames) +{ + // The JUCE framework doesn't like processing while being suspended. + const bool wasSuspended = !IsResumed(); + if(wasSuspended) + { + Resume(); + } + + float out[2][MIXBUFFERSIZE]; // scratch buffers + float maxVal = 0.0f; + m_mixBuffer.ClearInputBuffers(MIXBUFFERSIZE); + + while(numFrames > 0) + { + uint32 renderSamples = numFrames; + LimitMax(renderSamples, mpt::saturate_cast<uint32>(std::size(out[0]))); + MemsetZero(out); + + Process(out[0], out[1], renderSamples); + for(size_t i = 0; i < renderSamples; i++) + { + maxVal = std::max(maxVal, std::fabs(out[0][i])); + maxVal = std::max(maxVal, std::fabs(out[1][i])); + } + + numFrames -= renderSamples; + } + + if(wasSuspended) + { + Suspend(); + } + + return maxVal; +} + + +// Get list of plugins to which output is sent. A nullptr indicates master output. +size_t IMixPlugin::GetOutputPlugList(std::vector<IMixPlugin *> &list) +{ + // At the moment we know there will only be 1 output. + // Returning nullptr means plugin outputs directly to master. + list.clear(); + + IMixPlugin *outputPlug = nullptr; + if(!m_pMixStruct->IsOutputToMaster()) + { + PLUGINDEX nOutput = m_pMixStruct->GetOutputPlugin(); + if(nOutput > m_nSlot && nOutput != PLUGINDEX_INVALID) + { + outputPlug = m_SndFile.m_MixPlugins[nOutput].pMixPlugin; + } + } + list.push_back(outputPlug); + + return 1; +} + + +// Get a list of plugins that send data to this plugin. +size_t IMixPlugin::GetInputPlugList(std::vector<IMixPlugin *> &list) +{ + std::vector<IMixPlugin *> candidatePlugOutputs; + list.clear(); + + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + IMixPlugin *candidatePlug = m_SndFile.m_MixPlugins[plug].pMixPlugin; + if(candidatePlug) + { + candidatePlug->GetOutputPlugList(candidatePlugOutputs); + + for(auto &outPlug : candidatePlugOutputs) + { + if(outPlug == this) + { + list.push_back(candidatePlug); + break; + } + } + } + } + + return list.size(); +} + + +// Get a list of instruments that send data to this plugin. +size_t IMixPlugin::GetInputInstrumentList(std::vector<INSTRUMENTINDEX> &list) +{ + list.clear(); + const PLUGINDEX nThisMixPlug = m_nSlot + 1; //m_nSlot is position in mixplug array. + + for(INSTRUMENTINDEX ins = 0; ins <= m_SndFile.GetNumInstruments(); ins++) + { + if(m_SndFile.Instruments[ins] != nullptr && m_SndFile.Instruments[ins]->nMixPlug == nThisMixPlug) + { + list.push_back(ins); + } + } + + return list.size(); +} + + +size_t IMixPlugin::GetInputChannelList(std::vector<CHANNELINDEX> &list) +{ + list.clear(); + + PLUGINDEX nThisMixPlug = m_nSlot + 1; //m_nSlot is position in mixplug array. + const CHANNELINDEX chnCount = m_SndFile.GetNumChannels(); + for(CHANNELINDEX nChn=0; nChn<chnCount; nChn++) + { + if(m_SndFile.ChnSettings[nChn].nMixPlugin == nThisMixPlug) + { + list.push_back(nChn); + } + } + + return list.size(); + +} + + +void IMixPlugin::SaveAllParameters() +{ + if (m_pMixStruct == nullptr) + { + return; + } + m_pMixStruct->defaultProgram = -1; + + // Default implementation: Save all parameter values + PlugParamIndex numParams = std::min(GetNumParameters(), static_cast<PlugParamIndex>((std::numeric_limits<uint32>::max() - sizeof(uint32)) / sizeof(IEEE754binary32LE))); + uint32 nLen = numParams * sizeof(IEEE754binary32LE); + if (!nLen) return; + nLen += sizeof(uint32); + + try + { + m_pMixStruct->pluginData.resize(nLen); + auto memFile = std::make_pair(mpt::as_span(m_pMixStruct->pluginData), mpt::IO::Offset(0)); + mpt::IO::WriteIntLE<uint32>(memFile, 0); // Plugin data type + BeginGetProgram(); + for(PlugParamIndex i = 0; i < numParams; i++) + { + mpt::IO::Write(memFile, IEEE754binary32LE(GetParameter(i))); + } + EndGetProgram(); + } catch(mpt::out_of_memory e) + { + m_pMixStruct->pluginData.clear(); + mpt::delete_out_of_memory(e); + } +} + + +void IMixPlugin::RestoreAllParameters(int32 /*program*/) +{ + if(m_pMixStruct != nullptr && m_pMixStruct->pluginData.size() >= sizeof(uint32)) + { + FileReader memFile(mpt::as_span(m_pMixStruct->pluginData)); + uint32 type = memFile.ReadUint32LE(); + if(type == 0) + { + const uint32 numParams = GetNumParameters(); + if((m_pMixStruct->pluginData.size() - sizeof(uint32)) >= (numParams * sizeof(IEEE754binary32LE))) + { + BeginSetProgram(); + for(uint32 i = 0; i < numParams; i++) + { + const auto value = memFile.ReadFloatLE(); + SetParameter(i, std::isfinite(value) ? value : 0.0f); + } + EndSetProgram(); + } + } + } +} + + +#ifdef MODPLUG_TRACKER +void IMixPlugin::ToggleEditor() +{ + // We only really need this mutex for bridged plugins, as we may be processing window messages (in the same thread) while the editor opens. + // The user could press the toggle button while the editor is loading and thus close the editor while still being initialized. + // Note that this does not protect against closing the module while the editor is still loading. + static bool initializing = false; + if(initializing) + return; + initializing = true; + + if (m_pEditor) + { + CloseEditor(); + } else + { + m_pEditor = OpenEditor(); + + if (m_pEditor) + m_pEditor->OpenEditor(CMainFrame::GetMainFrame()); + } + initializing = false; +} + + +// Provide default plugin editor +CAbstractVstEditor *IMixPlugin::OpenEditor() +{ + try + { + return new CDefaultVstEditor(*this); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return nullptr; + } +} + + +void IMixPlugin::CloseEditor() +{ + if(m_pEditor) + { + if (m_pEditor->m_hWnd) m_pEditor->DoClose(); + delete m_pEditor; + m_pEditor = nullptr; + } +} + + +// Automate a parameter from the plugin GUI (both custom and default plugin GUI) +void IMixPlugin::AutomateParameter(PlugParamIndex param) +{ + CModDoc *modDoc = GetModDoc(); + if(modDoc == nullptr) + { + return; + } + + // TODO: Check if any params are actually automatable, and if there are but this one isn't, chicken out + + if(m_recordAutomation) + { + // Record parameter change + modDoc->RecordParamChange(GetSlot(), param); + } + + modDoc->SendNotifyMessageToAllViews(WM_MOD_PLUGPARAMAUTOMATE, m_nSlot, param); + + if(auto *vstEditor = GetEditor(); vstEditor && vstEditor->m_hWnd) + { + // Mark track modified if GUI is open and format supports plugins + SetModified(); + + // Do not use InputHandler in case we are coming from a bridged plugin editor + if((GetAsyncKeyState(VK_SHIFT) & 0x8000) && TrackerSettings::Instance().midiMappingInPluginEditor) + { + // Shift pressed -> Open MIDI mapping dialog + CMainFrame::GetMainFrame()->PostMessage(WM_MOD_MIDIMAPPING, m_nSlot, param); + } + + // Learn macro + int macroToLearn = vstEditor->GetLearnMacro(); + if (macroToLearn > -1) + { + modDoc->LearnMacro(macroToLearn, param); + vstEditor->SetLearnMacro(-1); + } + } +} + + +void IMixPlugin::SetModified() +{ + CModDoc *modDoc = GetModDoc(); + if(modDoc != nullptr && m_SndFile.GetModSpecifications().supportsPlugins) + { + modDoc->SetModified(); + } +} + + +bool IMixPlugin::SaveProgram() +{ + mpt::PathString defaultDir = TrackerSettings::Instance().PathPluginPresets.GetWorkingDir(); + const bool useDefaultDir = !defaultDir.empty(); + if(!useDefaultDir && m_Factory.dllPath.IsFile()) + { + defaultDir = m_Factory.dllPath.GetPath(); + } + + CString progName = m_Factory.libraryName.ToCString() + _T(" - ") + GetCurrentProgramName(); + SanitizeFilename(progName); + + FileDialog dlg = SaveFileDialog() + .DefaultExtension("fxb") + .DefaultFilename(progName) + .ExtensionFilter("VST Plugin Programs (*.fxp)|*.fxp|" + "VST Plugin Banks (*.fxb)|*.fxb||") + .WorkingDirectory(defaultDir); + if(!dlg.Show(m_pEditor)) return false; + + if(useDefaultDir) + { + TrackerSettings::Instance().PathPluginPresets.SetWorkingDir(dlg.GetWorkingDirectory()); + } + + const bool isBank = (dlg.GetExtension() == P_("fxb")); + + try + { + mpt::SafeOutputFile sf(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + if(f.good() && VSTPresets::SaveFile(f, *this, isBank)) + return true; + } catch(const std::exception &) + { + + } + Reporting::Error("Error saving preset.", m_pEditor); + return false; +} + + +bool IMixPlugin::LoadProgram(mpt::PathString fileName) +{ + mpt::PathString defaultDir = TrackerSettings::Instance().PathPluginPresets.GetWorkingDir(); + bool useDefaultDir = !defaultDir.empty(); + if(!useDefaultDir && m_Factory.dllPath.IsFile()) + { + defaultDir = m_Factory.dllPath.GetPath(); + } + + if(fileName.empty()) + { + FileDialog dlg = OpenFileDialog() + .DefaultExtension("fxp") + .ExtensionFilter("VST Plugin Programs and Banks (*.fxp,*.fxb)|*.fxp;*.fxb|" + "VST Plugin Programs (*.fxp)|*.fxp|" + "VST Plugin Banks (*.fxb)|*.fxb|" + "All Files|*.*||") + .WorkingDirectory(defaultDir); + if(!dlg.Show(m_pEditor)) return false; + + if(useDefaultDir) + { + TrackerSettings::Instance().PathPluginPresets.SetWorkingDir(dlg.GetWorkingDirectory()); + } + fileName = dlg.GetFirstFile(); + } + + const char *errorStr = nullptr; + InputFile f(fileName, SettingCacheCompleteFileBeforeLoading()); + if(f.IsValid()) + { + FileReader file = GetFileReader(f); + errorStr = VSTPresets::GetErrorMessage(VSTPresets::LoadFile(file, *this)); + } else + { + errorStr = "Can't open file."; + } + + if(errorStr == nullptr) + { + if(GetModDoc() != nullptr && GetSoundFile().GetModSpecifications().supportsPlugins) + { + GetModDoc()->SetModified(); + } + return true; + } else + { + Reporting::Error(errorStr, m_pEditor); + return false; + } +} + + +#endif // MODPLUG_TRACKER + + +//////////////////////////////////////////////////////////////////// +// IMidiPlugin: Default implementation of plugins with MIDI input // +//////////////////////////////////////////////////////////////////// + +IMidiPlugin::IMidiPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_MidiCh{{}} +{ + for(auto &chn : m_MidiCh) + { + chn.midiPitchBendPos = EncodePitchBendParam(MIDIEvents::pitchBendCentre); // centre pitch bend on all channels + chn.ResetProgram(); + } +} + + +void IMidiPlugin::ApplyPitchWheelDepth(int32 &value, int8 pwd) +{ + if(pwd != 0) + { + value = (value * ((MIDIEvents::pitchBendMax - MIDIEvents::pitchBendCentre + 1) / 64)) / pwd; + } else + { + value = 0; + } +} + + +// Get the MIDI channel currently associated with a given tracker channel +uint8 IMidiPlugin::GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const +{ + if(auto ins = chn.pModInstrument; ins != nullptr) + return ins->GetMIDIChannel(chn, trackChannel); + else + return 0; +} + + +uint8 IMidiPlugin::GetMidiChannel(CHANNELINDEX trackChannel) const +{ + if(trackChannel < std::size(m_SndFile.m_PlayState.Chn)) + return GetMidiChannel(m_SndFile.m_PlayState.Chn[trackChannel], trackChannel); + else + return 0; +} + + +void IMidiPlugin::MidiCC(MIDIEvents::MidiCC nController, uint8 nParam, CHANNELINDEX trackChannel) +{ + //Error checking + LimitMax(nController, MIDIEvents::MIDICC_end); + LimitMax(nParam, uint8(127)); + auto midiCh = GetMidiChannel(trackChannel); + + if(m_SndFile.m_playBehaviour[kMIDICCBugEmulation]) + MidiSend(MIDIEvents::Event(MIDIEvents::evControllerChange, midiCh, nParam, static_cast<uint8>(nController))); // param and controller are swapped (old broken implementation) + else + MidiSend(MIDIEvents::CC(nController, midiCh, nParam)); +} + + +// Set MIDI pitch for given MIDI channel to the specified raw 14-bit position +void IMidiPlugin::MidiPitchBendRaw(int32 pitchbend, CHANNELINDEX trackerChn) +{ + SendMidiPitchBend(GetMidiChannel(trackerChn), EncodePitchBendParam(Clamp(pitchbend, MIDIEvents::pitchBendMin, MIDIEvents::pitchBendMax))); +} + + +// Bend MIDI pitch for given MIDI channel using fine tracker param (one unit = 1/64th of a note step) +void IMidiPlugin::MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackerChn) +{ + auto midiCh = GetMidiChannel(trackerChn); + if(m_SndFile.m_playBehaviour[kOldMIDIPitchBends]) + { + // OpenMPT Legacy: Old pitch slides never were really accurate, but setting the PWD to 13 in plugins would give the closest results. + increment = (increment * 0x800 * 13) / (0xFF * pwd); + increment = EncodePitchBendParam(increment); + } else + { + increment = EncodePitchBendParam(increment); + ApplyPitchWheelDepth(increment, pwd); + } + + int32 newPitchBendPos = (increment + m_MidiCh[midiCh].midiPitchBendPos) & kPitchBendMask; + Limit(newPitchBendPos, EncodePitchBendParam(MIDIEvents::pitchBendMin), EncodePitchBendParam(MIDIEvents::pitchBendMax)); + + SendMidiPitchBend(midiCh, newPitchBendPos); +} + + +// Set MIDI pitch for given MIDI channel using fixed point pitch bend value (converted back to 0-16383 MIDI range) +void IMidiPlugin::SendMidiPitchBend(uint8 midiCh, int32 newPitchBendPos) +{ + MPT_ASSERT(EncodePitchBendParam(MIDIEvents::pitchBendMin) <= newPitchBendPos && newPitchBendPos <= EncodePitchBendParam(MIDIEvents::pitchBendMax)); + m_MidiCh[midiCh].midiPitchBendPos = newPitchBendPos; + MidiSend(MIDIEvents::PitchBend(midiCh, DecodePitchBendParam(newPitchBendPos))); +} + + +// Apply vibrato effect through pitch wheel commands on a given MIDI channel. +void IMidiPlugin::MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackerChn) +{ + auto midiCh = GetMidiChannel(trackerChn); + depth = EncodePitchBendParam(depth); + if(depth != 0 || (m_MidiCh[midiCh].midiPitchBendPos & kVibratoFlag)) + { + ApplyPitchWheelDepth(depth, pwd); + + // Temporarily add vibrato offset to current pitch + int32 newPitchBendPos = (depth + m_MidiCh[midiCh].midiPitchBendPos) & kPitchBendMask; + Limit(newPitchBendPos, EncodePitchBendParam(MIDIEvents::pitchBendMin), EncodePitchBendParam(MIDIEvents::pitchBendMax)); + + MidiSend(MIDIEvents::PitchBend(midiCh, DecodePitchBendParam(newPitchBendPos))); + } + + // Update vibrato status + if(depth != 0) + m_MidiCh[midiCh].midiPitchBendPos |= kVibratoFlag; + else + m_MidiCh[midiCh].midiPitchBendPos &= ~kVibratoFlag; +} + + +void IMidiPlugin::MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) +{ + if(trackChannel >= MAX_CHANNELS) + return; + + auto midiCh = GetMidiChannel(trackChannel); + PlugInstrChannel &channel = m_MidiCh[midiCh]; + + uint16 midiBank = instr.wMidiBank - 1; + uint8 midiProg = instr.nMidiProgram - 1; + bool bankChanged = (channel.currentBank != midiBank) && (midiBank < 0x4000); + bool progChanged = (channel.currentProgram != midiProg) && (midiProg < 0x80); + //get vol in [0,128[ + uint8 volume = static_cast<uint8>(std::min((vol + 1u) / 2u, 127u)); + + // Bank change + if(bankChanged) + { + uint8 high = static_cast<uint8>(midiBank >> 7); + uint8 low = static_cast<uint8>(midiBank & 0x7F); + + //m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.Global[MIDIOUT_BANKSEL], 0, m_nSlot + 1); + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_BankSelect_Coarse, midiCh, high)); + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_BankSelect_Fine, midiCh, low)); + + channel.currentBank = midiBank; + } + + // Program change + // According to the MIDI specs, a bank change alone doesn't have to change the active program - it will only change the bank of subsequent program changes. + // Thus we send program changes also if only the bank has changed. + if(progChanged || (midiProg < 0x80 && bankChanged)) + { + channel.currentProgram = midiProg; + //m_SndFile.ProcessMIDIMacro(trackChannel, false, m_SndFile.m_MidiCfg.Global[MIDIOUT_PROGRAM], 0, m_nSlot + 1); + MidiSend(MIDIEvents::ProgramChange(midiCh, midiProg)); + } + + + // Specific Note Off + if(note > NOTE_MAX_SPECIAL) + { + uint8 i = static_cast<uint8>(note - NOTE_MAX_SPECIAL - NOTE_MIN); + if(channel.noteOnMap[i][trackChannel]) + { + channel.noteOnMap[i][trackChannel]--; + MidiSend(MIDIEvents::NoteOff(midiCh, i, 0)); + } + } + + // "Hard core" All Sounds Off on this midi and tracker channel + // This one doesn't check the note mask - just one note off per note. + // Also less likely to cause a VST event buffer overflow. + else if(note == NOTE_NOTECUT) // ^^ + { + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllNotesOff, midiCh, 0)); + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllSoundOff, midiCh, 0)); + + // Turn off all notes + for(uint8 i = 0; i < std::size(channel.noteOnMap); i++) + { + channel.noteOnMap[i][trackChannel] = 0; + MidiSend(MIDIEvents::NoteOff(midiCh, i, volume)); + } + + } + + // All "active" notes off on this midi and tracker channel + // using note mask. + else if(note == NOTE_KEYOFF || note == NOTE_FADE) // ==, ~~ + { + for(uint8 i = 0; i < std::size(channel.noteOnMap); i++) + { + // Some VSTis need a note off for each instance of a note on, e.g. fabfilter. + while(channel.noteOnMap[i][trackChannel]) + { + MidiSend(MIDIEvents::NoteOff(midiCh, i, volume)); + channel.noteOnMap[i][trackChannel]--; + } + } + } + + // Note On + else if(note >= NOTE_MIN && note < NOTE_MIN + mpt::array_size<decltype(channel.noteOnMap)>::size) + { + note -= NOTE_MIN; + + // Reset pitch bend on each new note, tracker style. + // This is done if the pitch wheel has been moved or there was a vibrato on the previous row (in which case the "vstVibratoFlag" bit of the pitch bend memory is set) + auto newPitchBendPos = EncodePitchBendParam(Clamp(m_SndFile.m_PlayState.Chn[trackChannel].GetMIDIPitchBend(), MIDIEvents::pitchBendMin, MIDIEvents::pitchBendMax)); + if(m_MidiCh[midiCh].midiPitchBendPos != newPitchBendPos) + { + SendMidiPitchBend(midiCh, newPitchBendPos); + } + + // count instances of active notes. + // This is to send a note off for each instance of a note, for plugs like Fabfilter. + // Problem: if a note dies out naturally and we never send a note off, this counter + // will block at max until note off. Is this a problem? + // Safe to assume we won't need more than 255 note offs max on a given note? + if(channel.noteOnMap[note][trackChannel] < uint8_max) + { + channel.noteOnMap[note][trackChannel]++; + } + + MidiSend(MIDIEvents::NoteOn(midiCh, static_cast<uint8>(note), volume)); + } +} + + +bool IMidiPlugin::IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) +{ + if(!ModCommand::IsNote(note) || trackerChn >= std::size(m_MidiCh[GetMidiChannel(trackerChn)].noteOnMap[note])) + return false; + + note -= NOTE_MIN; + return (m_MidiCh[GetMidiChannel(trackerChn)].noteOnMap[note][trackerChn] != 0); +} + + +void IMidiPlugin::ReceiveMidi(uint32 midiCode) +{ + ResetSilence(); + + // I think we should only route events to plugins that are explicitely specified as output plugins of the current plugin. + // This should probably use GetOutputPlugList here if we ever get to support multiple output plugins. + PLUGINDEX receiver; + if(m_pMixStruct != nullptr && (receiver = m_pMixStruct->GetOutputPlugin()) != PLUGINDEX_INVALID) + { + IMixPlugin *plugin = m_SndFile.m_MixPlugins[receiver].pMixPlugin; + // Add all events to the plugin's queue. + plugin->MidiSend(midiCode); + } + +#ifdef MODPLUG_TRACKER + if(m_recordMIDIOut) + { + // Spam MIDI data to all views + ::PostMessage(CMainFrame::GetMainFrame()->GetMidiRecordWnd(), WM_MOD_MIDIMSG, midiCode, reinterpret_cast<LPARAM>(this)); + } +#endif // MODPLUG_TRACKER +} + + +void IMidiPlugin::ReceiveSysex(mpt::const_byte_span sysex) +{ + ResetSilence(); + + // I think we should only route events to plugins that are explicitely specified as output plugins of the current plugin. + // This should probably use GetOutputPlugList here if we ever get to support multiple output plugins. + PLUGINDEX receiver; + if(m_pMixStruct != nullptr && (receiver = m_pMixStruct->GetOutputPlugin()) != PLUGINDEX_INVALID) + { + IMixPlugin *plugin = m_SndFile.m_MixPlugins[receiver].pMixPlugin; + // Add all events to the plugin's queue. + plugin->MidiSysexSend(sysex); + } +} + + +// SNDMIXPLUGIN functions + +void SNDMIXPLUGIN::SetGain(uint8 gain) +{ + Info.gain = gain; + if(pMixPlugin != nullptr) pMixPlugin->RecalculateGain(); +} + + +void SNDMIXPLUGIN::SetBypass(bool bypass) +{ + if(pMixPlugin != nullptr) + pMixPlugin->Bypass(bypass); + else + Info.SetBypass(bypass); +} + + +void SNDMIXPLUGIN::Destroy() +{ + if(pMixPlugin) + { + pMixPlugin->Release(); + pMixPlugin = nullptr; + } + pluginData.clear(); + pluginData.shrink_to_fit(); +} + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.h new file mode 100644 index 00000000..34cf3121 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PlugInterface.h @@ -0,0 +1,301 @@ +/* + * PlugInterface.h + * --------------- + * Purpose: Interface class for plugin handling + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "../../soundlib/Snd_defs.h" +#include "../../soundlib/MIDIEvents.h" +#include "../../soundlib/Mixer.h" +#include "PluginMixBuffer.h" +#include "PluginStructs.h" + +OPENMPT_NAMESPACE_BEGIN + +struct VSTPluginLib; +struct SNDMIXPLUGIN; +struct ModInstrument; +struct ModChannel; +class CSoundFile; +class CModDoc; +class CAbstractVstEditor; + +struct SNDMIXPLUGINSTATE +{ + // dwFlags flags + enum PluginStateFlags + { + psfMixReady = 0x01, // Set when cleared + psfHasInput = 0x02, // Set when plugin has non-silent input + psfSilenceBypass = 0x04, // Bypass because of silence detection + }; + + mixsample_t *pMixBuffer = nullptr; // Stereo effect send buffer + uint32 dwFlags = 0; // PluginStateFlags + uint32 inputSilenceCount = 0; // How much silence has been processed? (for plugin auto-turnoff) + mixsample_t nVolDecayL = 0, nVolDecayR = 0; // End of sample click removal + + void ResetSilence() + { + dwFlags |= psfHasInput; + dwFlags &= ~psfSilenceBypass; + inputSilenceCount = 0; + } +}; + + +class IMixPlugin +{ + friend class CAbstractVstEditor; + +protected: + IMixPlugin *m_pNext = nullptr, *m_pPrev = nullptr; + VSTPluginLib &m_Factory; + CSoundFile &m_SndFile; + SNDMIXPLUGIN *m_pMixStruct; +#ifdef MODPLUG_TRACKER + CAbstractVstEditor *m_pEditor = nullptr; +#endif // MODPLUG_TRACKER + +public: + SNDMIXPLUGINSTATE m_MixState; + PluginMixBuffer<float, MIXBUFFERSIZE> m_mixBuffer; // Float buffers (input and output) for plugins + +protected: + mixsample_t m_MixBuffer[MIXBUFFERSIZE * 2 + 2]; // Stereo interleaved input (sample mixer renders here) + + float m_fGain = 1.0f; + PLUGINDEX m_nSlot = 0; + + bool m_isSongPlaying = false; + bool m_isResumed = false; + +public: + bool m_recordAutomation = false; + bool m_passKeypressesToPlug = false; + bool m_recordMIDIOut = false; + +protected: + virtual ~IMixPlugin(); + + // Insert plugin into list of loaded plugins. + void InsertIntoFactoryList(); + +public: + // Non-virtual part of the interface + IMixPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + inline CSoundFile &GetSoundFile() { return m_SndFile; } + inline const CSoundFile &GetSoundFile() const { return m_SndFile; } + +#ifdef MODPLUG_TRACKER + CModDoc *GetModDoc(); + const CModDoc *GetModDoc() const; + + void SetSlot(PLUGINDEX slot); + inline PLUGINDEX GetSlot() const { return m_nSlot; } +#endif // MODPLUG_TRACKER + + inline VSTPluginLib &GetPluginFactory() const { return m_Factory; } + // Returns the next instance of the same plugin + inline IMixPlugin *GetNextInstance() const { return m_pNext; } + + void SetDryRatio(float dryRatio); + bool IsBypassed() const; + void RecalculateGain(); + // Query output latency from host (in seconds) + double GetOutputLatency() const; + + // Destroy the plugin + virtual void Release() = 0; + virtual int32 GetUID() const = 0; + virtual int32 GetVersion() const = 0; + virtual void Idle() = 0; + // Plugin latency in samples + virtual uint32 GetLatency() const = 0; + + virtual int32 GetNumPrograms() const = 0; + virtual int32 GetCurrentProgram() = 0; + virtual void SetCurrentProgram(int32 nIndex) = 0; + + virtual PlugParamIndex GetNumParameters() const = 0; + virtual void SetParameter(PlugParamIndex paramindex, PlugParamValue paramvalue) = 0; + virtual PlugParamValue GetParameter(PlugParamIndex nIndex) = 0; + + // Save parameters for storing them in a module file + virtual void SaveAllParameters(); + // Restore parameters from module file + virtual void RestoreAllParameters(int32 program); + virtual void Process(float *pOutL, float *pOutR, uint32 numFrames) = 0; + void ProcessMixOps(float *pOutL, float *pOutR, float *leftPlugOutput, float *rightPlugOutput, uint32 numFrames); + // Render silence and return the highest resulting output level + virtual float RenderSilence(uint32 numSamples); + + // MIDI event handling + virtual bool MidiSend(uint32 /*midiCode*/) { return true; } + virtual bool MidiSysexSend(mpt::const_byte_span /*sysex*/) { return true; } + virtual void MidiCC(MIDIEvents::MidiCC /*nController*/, uint8 /*nParam*/, CHANNELINDEX /*trackChannel*/) { } + virtual void MidiPitchBendRaw(int32 /*pitchbend*/, CHANNELINDEX /*trackChannel*/) {} + virtual void MidiPitchBend(int32 /*increment*/, int8 /*pwd*/, CHANNELINDEX /*trackChannel*/) { } + virtual void MidiVibrato(int32 /*depth*/, int8 /*pwd*/, CHANNELINDEX /*trackerChn*/) { } + virtual void MidiCommand(const ModInstrument &/*instr*/, uint16 /*note*/, uint16 /*vol*/, CHANNELINDEX /*trackChannel*/) { } + virtual void HardAllNotesOff() { } + virtual bool IsNotePlaying(uint8 /*note*/, CHANNELINDEX /*trackerChn*/) { return false; } + + // Modify parameter by given amount. Only needs to be re-implemented if plugin architecture allows this to be performed atomically. + virtual void ModifyParameter(PlugParamIndex nIndex, PlugParamValue diff); + virtual void NotifySongPlaying(bool playing) { m_isSongPlaying = playing; } + virtual bool IsSongPlaying() const { return m_isSongPlaying; } + virtual bool IsResumed() const { return m_isResumed; } + virtual void Resume() = 0; + virtual void Suspend() = 0; + // Tell the plugin that there is a discontinuity between the previous and next render call (e.g. aftert jumping around in the module) + virtual void PositionChanged() = 0; + virtual void Bypass(bool = true); + bool ToggleBypass() { Bypass(!IsBypassed()); return IsBypassed(); } + virtual bool IsInstrument() const = 0; + virtual bool CanRecieveMidiEvents() = 0; + // If false is returned, mixing this plugin can be skipped if its input are currently completely silent. + virtual bool ShouldProcessSilence() = 0; + virtual void ResetSilence() { m_MixState.ResetSilence(); } + + size_t GetOutputPlugList(std::vector<IMixPlugin *> &list); + size_t GetInputPlugList(std::vector<IMixPlugin *> &list); + size_t GetInputInstrumentList(std::vector<INSTRUMENTINDEX> &list); + size_t GetInputChannelList(std::vector<CHANNELINDEX> &list); + +#ifdef MODPLUG_TRACKER + bool SaveProgram(); + bool LoadProgram(mpt::PathString fileName = mpt::PathString()); + + virtual CString GetDefaultEffectName() = 0; + + // Cache a range of names, in case one-by-one retrieval would be slow (e.g. when using plugin bridge) + virtual void CacheProgramNames(int32 /*firstProg*/, int32 /*lastProg*/) { } + virtual void CacheParameterNames(int32 /*firstParam*/, int32 /*lastParam*/) { } + + // Allowed value range for a parameter + virtual std::pair<PlugParamValue, PlugParamValue> GetParamUIRange(PlugParamIndex /*param*/) { return {0.0f, 1.0f}; } + // Scale allowed value range of a parameter to/from [0,1] + PlugParamValue GetScaledUIParam(PlugParamIndex param); + void SetScaledUIParam(PlugParamIndex param, PlugParamValue value); + + virtual CString GetParamName(PlugParamIndex param) = 0; + virtual CString GetParamLabel(PlugParamIndex param) = 0; + virtual CString GetParamDisplay(PlugParamIndex param) = 0; + CString GetFormattedParamName(PlugParamIndex param); + CString GetFormattedParamValue(PlugParamIndex param); + virtual CString GetCurrentProgramName() = 0; + virtual void SetCurrentProgramName(const CString &name) = 0; + virtual CString GetProgramName(int32 program) = 0; + CString GetFormattedProgramName(int32 index); + + virtual bool HasEditor() const = 0; +protected: + virtual CAbstractVstEditor *OpenEditor(); +public: + // Get the plugin's editor window + CAbstractVstEditor *GetEditor() { return m_pEditor; } + const CAbstractVstEditor *GetEditor() const { return m_pEditor; } + void ToggleEditor(); + void CloseEditor(); + void SetEditorPos(int32 x, int32 y); + void GetEditorPos(int32 &x, int32 &y) const; + + // Notify OpenMPT that a plugin parameter has changed and set document as modified + void AutomateParameter(PlugParamIndex param); + // Plugin state changed, set document as modified. + void SetModified(); +#endif + + virtual int GetNumInputChannels() const = 0; + virtual int GetNumOutputChannels() const = 0; + + using ChunkData = mpt::const_byte_span; + virtual bool ProgramsAreChunks() const { return false; } + virtual ChunkData GetChunk(bool /*isBank*/) { return ChunkData(); } + virtual void SetChunk(const ChunkData &/*chunk*/, bool /*isBank*/) { } + + virtual void BeginSetProgram(int32 /*program*/ = -1) {} + virtual void EndSetProgram() {} + virtual void BeginGetProgram(int32 /*program*/ = -1) {} + virtual void EndGetProgram() {} +}; + + +inline void IMixPlugin::ModifyParameter(PlugParamIndex nIndex, PlugParamValue diff) +{ + PlugParamValue val = GetParameter(nIndex) + diff; + Limit(val, PlugParamValue(0), PlugParamValue(1)); + SetParameter(nIndex, val); +} + + +// IMidiPlugin: Default implementation of plugins with MIDI input + +class IMidiPlugin : public IMixPlugin +{ +protected: + enum + { + // Pitch wheel constants + kPitchBendShift = 12, // Use lowest 12 bits for fractional part and vibrato flag => 16.11 fixed point precision + kPitchBendMask = (~1), + kVibratoFlag = 1, + }; + + struct PlugInstrChannel + { + int32 midiPitchBendPos = 0; // Current Pitch Wheel position, in 16.11 fixed point format. Lowest bit is used for indicating that vibrato was applied. Vibrato offset itself is not stored in this value. + uint16 currentProgram = uint16_max; + uint16 currentBank = uint16_max; + uint8 noteOnMap[128][MAX_CHANNELS]; + + void ResetProgram() { currentProgram = uint16_max; currentBank = uint16_max; } + }; + + std::array<PlugInstrChannel, 16> m_MidiCh; // MIDI channel state + +public: + IMidiPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void MidiCC(MIDIEvents::MidiCC nController, uint8 nParam, CHANNELINDEX trackChannel) override; + void MidiPitchBendRaw(int32 pitchbend, CHANNELINDEX trackerChn) override; + void MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackerChn) override; + void MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackerChn) override; + void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override; + bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override; + + // Get the MIDI channel currently associated with a given tracker channel + virtual uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const; + +protected: + uint8 GetMidiChannel(CHANNELINDEX trackChannel) const; + + // Plugin wants to send MIDI to OpenMPT + virtual void ReceiveMidi(uint32 midiCode); + virtual void ReceiveSysex(mpt::const_byte_span sysex); + + // Converts a 14-bit MIDI pitch bend position to our internal pitch bend position representation + static constexpr int32 EncodePitchBendParam(int32 position) { return (position << kPitchBendShift); } + // Converts the internal pitch bend position to a 14-bit MIDI pitch bend position + static constexpr int16 DecodePitchBendParam(int32 position) { return static_cast<int16>(position >> kPitchBendShift); } + // Apply Pitch Wheel Depth (PWD) to some MIDI pitch bend value. + static inline void ApplyPitchWheelDepth(int32 &value, int8 pwd); + + void SendMidiPitchBend(uint8 midiCh, int32 newPitchBendPos); +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS + diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.cpp new file mode 100644 index 00000000..ab4e4abe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.cpp @@ -0,0 +1,816 @@ +/* + * PluginManager.cpp + * ----------------- + * Purpose: Implementation of the plugin manager, which keeps a list of known plugins and instantiates them. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS + +#include "../../common/version.h" +#include "PluginManager.h" +#include "PlugInterface.h" + +#include "mpt/uuid/guid.hpp" +#include "mpt/uuid/uuid.hpp" + +// Built-in plugins +#include "DigiBoosterEcho.h" +#include "LFOPlugin.h" +#include "SymMODEcho.h" +#include "dmo/DMOPlugin.h" +#include "dmo/Chorus.h" +#include "dmo/Compressor.h" +#include "dmo/Distortion.h" +#include "dmo/Echo.h" +#include "dmo/Flanger.h" +#include "dmo/Gargle.h" +#include "dmo/I3DL2Reverb.h" +#include "dmo/ParamEq.h" +#include "dmo/WavesReverb.h" +#ifdef MODPLUG_TRACKER +#include "../../mptrack/plugins/MidiInOut.h" +#endif // MODPLUG_TRACKER + +#include "../../common/mptStringBuffer.h" +#include "../Sndfile.h" +#include "../Loaders.h" + +#ifdef MPT_WITH_VST +#include "../../mptrack/Vstplug.h" +#include "../../pluginBridge/BridgeWrapper.h" +#endif // MPT_WITH_VST + +#if defined(MPT_WITH_DMO) +#include <winreg.h> +#include <strmif.h> +#include <tchar.h> +#endif // MPT_WITH_DMO + +#ifdef MODPLUG_TRACKER +#include "../../mptrack/Mptrack.h" +#include "../../mptrack/TrackerSettings.h" +#include "../../mptrack/AbstractVstEditor.h" +#include "../../soundlib/AudioCriticalSection.h" +#include "../mptrack/ExceptionHandler.h" +#include "mpt/crc/crc.hpp" +#endif // MODPLUG_TRACKER + + +OPENMPT_NAMESPACE_BEGIN + + +using namespace mpt::uuid_literals; + + +#ifdef MPT_ALL_LOGGING +#define VST_LOG +#define DMO_LOG +#endif + +#ifdef MODPLUG_TRACKER +static constexpr const mpt::uchar *cacheSection = UL_("PluginCache"); +#endif // MODPLUG_TRACKER + + +#ifdef MPT_WITH_VST + + +uint8 VSTPluginLib::GetNativePluginArch() +{ + uint8 result = 0; + switch(mpt::OS::Windows::GetProcessArchitecture()) + { + case mpt::OS::Windows::Architecture::x86: + result = PluginArch_x86; + break; + case mpt::OS::Windows::Architecture::amd64: + result = PluginArch_amd64; + break; + case mpt::OS::Windows::Architecture::arm: + result = PluginArch_arm; + break; + case mpt::OS::Windows::Architecture::arm64: + result = PluginArch_arm64; + break; + default: + result = 0; + break; + } + return result; +} + + +mpt::ustring VSTPluginLib::GetPluginArchName(uint8 arch) +{ + mpt::ustring result; + switch(arch) + { + case PluginArch_x86: + result = U_("x86"); + break; + case PluginArch_amd64: + result = U_("amd64"); + break; + case PluginArch_arm: + result = U_("arm"); + break; + case PluginArch_arm64: + result = U_("arm64"); + break; + default: + result = U_(""); + break; + } + return result; +} + + +mpt::ustring VSTPluginLib::GetPluginArchNameUser(uint8 arch) +{ + mpt::ustring result; + #if defined(MPT_WITH_WINDOWS10) + switch(arch) + { + case PluginArch_x86: + result = U_("x86 (32bit)"); + break; + case PluginArch_amd64: + result = U_("amd64 (64bit)"); + break; + case PluginArch_arm: + result = U_("arm (32bit)"); + break; + case PluginArch_arm64: + result = U_("arm64 (64bit)"); + break; + default: + result = U_(""); + break; + } + #else // !MPT_WITH_WINDOWS10 + switch(arch) + { + case PluginArch_x86: + result = U_("32-Bit"); + break; + case PluginArch_amd64: + result = U_("64-Bit"); + break; + case PluginArch_arm: + result = U_("32-Bit"); + break; + case PluginArch_arm64: + result = U_("64-Bit"); + break; + default: + result = U_(""); + break; + } + #endif // MPT_WITH_WINDOWS10 + return result; +} + + +uint8 VSTPluginLib::GetDllArch(bool fromCache) const +{ + // Built-in plugins are always native. + if(dllPath.empty()) + return GetNativePluginArch(); +#ifdef MPT_WITH_VST + if(!dllArch || !fromCache) + { + dllArch = static_cast<uint8>(BridgeWrapper::GetPluginBinaryType(dllPath)); + } +#else // !MPT_WITH_VST + MPT_UNREFERENCED_PARAMETER(fromCache); +#endif // MPT_WITH_VST + return dllArch; +} + + +mpt::ustring VSTPluginLib::GetDllArchName(bool fromCache) const +{ + return GetPluginArchName(GetDllArch(fromCache)); +} + + +mpt::ustring VSTPluginLib::GetDllArchNameUser(bool fromCache) const +{ + return GetPluginArchNameUser(GetDllArch(fromCache)); +} + + +bool VSTPluginLib::IsNative(bool fromCache) const +{ + return GetDllArch(fromCache) == GetNativePluginArch(); +} + + +bool VSTPluginLib::IsNativeFromCache() const +{ + return dllArch == GetNativePluginArch() || dllArch == 0; +} + + +#endif // MPT_WITH_VST + + +// PluginCache format: +// FullDllPath = <ID1><ID2><CRC32> (hex-encoded) +// <ID1><ID2><CRC32>.Flags = Plugin Flags (see VSTPluginLib::DecodeCacheFlags). +// <ID1><ID2><CRC32>.Vendor = Plugin Vendor String. + +#ifdef MODPLUG_TRACKER +void VSTPluginLib::WriteToCache() const +{ + SettingsContainer &cacheFile = theApp.GetPluginCache(); + + const std::string crcName = dllPath.ToUTF8(); + const mpt::crc32 crc(crcName); + const mpt::ustring IDs = mpt::ufmt::HEX0<8>(pluginId1) + mpt::ufmt::HEX0<8>(pluginId2) + mpt::ufmt::HEX0<8>(crc.result()); + + mpt::PathString writePath = dllPath; + if(theApp.IsPortableMode()) + { + writePath = theApp.PathAbsoluteToInstallRelative(writePath); + } + + cacheFile.Write<mpt::ustring>(cacheSection, writePath.ToUnicode(), IDs); + cacheFile.Write<CString>(cacheSection, IDs + U_(".Vendor"), vendor); + cacheFile.Write<int32>(cacheSection, IDs + U_(".Flags"), EncodeCacheFlags()); +} +#endif // MODPLUG_TRACKER + + +bool CreateMixPluginProc(SNDMIXPLUGIN &mixPlugin, CSoundFile &sndFile) +{ +#ifdef MODPLUG_TRACKER + CVstPluginManager *that = theApp.GetPluginManager(); + if(that) + { + return that->CreateMixPlugin(mixPlugin, sndFile); + } + return false; +#else + if(!sndFile.m_PluginManager) + { + sndFile.m_PluginManager = std::make_unique<CVstPluginManager>(); + } + return sndFile.m_PluginManager->CreateMixPlugin(mixPlugin, sndFile); +#endif // MODPLUG_TRACKER +} + + +CVstPluginManager::CVstPluginManager() +{ +#if defined(MPT_WITH_DMO) + HRESULT COMinit = CoInitializeEx(NULL, COINIT_MULTITHREADED); + if(COMinit == S_OK || COMinit == S_FALSE) + { + MustUnInitilizeCOM = true; + } +#endif + + // Hard-coded "plugins" + static constexpr struct + { + VSTPluginLib::CreateProc createProc; + const char *filename, *name; + uint32 pluginId1, pluginId2; + VSTPluginLib::PluginCategory category; + bool isInstrument, isOurs; + } BuiltInPlugins[] = + { + // DirectX Media Objects Emulation + { DMO::Chorus::Create, "{EFE6629C-81F7-4281-BD91-C9D604A95AF6}", "Chorus", kDmoMagic, 0xEFE6629C, VSTPluginLib::catDMO, false, false }, + { DMO::Compressor::Create, "{EF011F79-4000-406D-87AF-BFFB3FC39D57}", "Compressor", kDmoMagic, 0xEF011F79, VSTPluginLib::catDMO, false, false }, + { DMO::Distortion::Create, "{EF114C90-CD1D-484E-96E5-09CFAF912A21}", "Distortion", kDmoMagic, 0xEF114C90, VSTPluginLib::catDMO, false, false }, + { DMO::Echo::Create, "{EF3E932C-D40B-4F51-8CCF-3F98F1B29D5D}", "Echo", kDmoMagic, 0xEF3E932C, VSTPluginLib::catDMO, false, false }, + { DMO::Flanger::Create, "{EFCA3D92-DFD8-4672-A603-7420894BAD98}", "Flanger", kDmoMagic, 0xEFCA3D92, VSTPluginLib::catDMO, false, false }, + { DMO::Gargle::Create, "{DAFD8210-5711-4B91-9FE3-F75B7AE279BF}", "Gargle", kDmoMagic, 0xDAFD8210, VSTPluginLib::catDMO, false, false }, + { DMO::I3DL2Reverb::Create, "{EF985E71-D5C7-42D4-BA4D-2D073E2E96F4}", "I3DL2Reverb", kDmoMagic, 0xEF985E71, VSTPluginLib::catDMO, false, false }, + { DMO::ParamEq::Create, "{120CED89-3BF4-4173-A132-3CB406CF3231}", "ParamEq", kDmoMagic, 0x120CED89, VSTPluginLib::catDMO, false, false }, + { DMO::WavesReverb::Create, "{87FC0268-9A55-4360-95AA-004A1D9DE26C}", "WavesReverb", kDmoMagic, 0x87FC0268, VSTPluginLib::catDMO, false, false }, + // First (inaccurate) Flanger implementation (will be chosen based on library name, shares ID1 and ID2 with regular Flanger) + { DMO::Flanger::CreateLegacy, "{EFCA3D92-DFD8-4672-A603-7420894BAD98}", "Flanger (Legacy)", kDmoMagic, 0xEFCA3D92, VSTPluginLib::catHidden, false, false }, + // DigiBooster Pro Echo DSP + { DigiBoosterEcho::Create, "", "DigiBooster Pro Echo", MagicLE("DBM0"), MagicLE("Echo"), VSTPluginLib::catRoomFx, false, true }, + // LFO + { LFOPlugin::Create, "", "LFO", MagicLE("OMPT"), MagicLE("LFO "), VSTPluginLib::catGenerator, false, true }, + // SymMOD Echo + { SymMODEcho::Create, "", "SymMOD Echo", MagicLE("SymM"), MagicLE("Echo"), VSTPluginLib::catRoomFx, false, true }, +#ifdef MODPLUG_TRACKER + { MidiInOut::Create, "", "MIDI Input Output", PLUGMAGIC('V','s','t','P'), PLUGMAGIC('M','M','I','D'), VSTPluginLib::catSynth, true, true }, +#endif // MODPLUG_TRACKER + }; + + pluginList.reserve(std::size(BuiltInPlugins)); + for(const auto &plugin : BuiltInPlugins) + { + VSTPluginLib *plug = new (std::nothrow) VSTPluginLib(plugin.createProc, true, mpt::PathString::FromUTF8(plugin.filename), mpt::PathString::FromUTF8(plugin.name)); + if(plug != nullptr) + { + pluginList.push_back(plug); + plug->pluginId1 = plugin.pluginId1; + plug->pluginId2 = plugin.pluginId2; + plug->category = plugin.category; + plug->isInstrument = plugin.isInstrument; +#ifdef MODPLUG_TRACKER + if(plugin.isOurs) + plug->vendor = _T("OpenMPT Project"); +#endif // MODPLUG_TRACKER + } + } + +#ifdef MODPLUG_TRACKER + // For security reasons, we do not load untrusted DMO plugins in libopenmpt. + EnumerateDirectXDMOs(); +#endif +} + + +CVstPluginManager::~CVstPluginManager() +{ + for(auto &plug : pluginList) + { + while(plug->pPluginsList != nullptr) + { + plug->pPluginsList->Release(); + } + delete plug; + } +#if defined(MPT_WITH_DMO) + if(MustUnInitilizeCOM) + { + CoUninitialize(); + MustUnInitilizeCOM = false; + } +#endif +} + + +bool CVstPluginManager::IsValidPlugin(const VSTPluginLib *pLib) const +{ + return mpt::contains(pluginList, pLib); +} + + +void CVstPluginManager::EnumerateDirectXDMOs() +{ +#if defined(MPT_WITH_DMO) + static constexpr mpt::UUID knownDMOs[] = + { + "745057C7-F353-4F2D-A7EE-58434477730E"_uuid, // AEC (Acoustic echo cancellation, not usable) + "EFE6629C-81F7-4281-BD91-C9D604A95AF6"_uuid, // Chorus + "EF011F79-4000-406D-87AF-BFFB3FC39D57"_uuid, // Compressor + "EF114C90-CD1D-484E-96E5-09CFAF912A21"_uuid, // Distortion + "EF3E932C-D40B-4F51-8CCF-3F98F1B29D5D"_uuid, // Echo + "EFCA3D92-DFD8-4672-A603-7420894BAD98"_uuid, // Flanger + "DAFD8210-5711-4B91-9FE3-F75B7AE279BF"_uuid, // Gargle + "EF985E71-D5C7-42D4-BA4D-2D073E2E96F4"_uuid, // I3DL2Reverb + "120CED89-3BF4-4173-A132-3CB406CF3231"_uuid, // ParamEq + "87FC0268-9A55-4360-95AA-004A1D9DE26C"_uuid, // WavesReverb + "F447B69E-1884-4A7E-8055-346F74D6EDB3"_uuid, // Resampler DMO (not usable) + }; + + HKEY hkEnum; + TCHAR keyname[128]; + + LONG cr = RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("software\\classes\\DirectShow\\MediaObjects\\Categories\\f3602b3f-0592-48df-a4cd-674721e7ebeb"), 0, KEY_READ, &hkEnum); + DWORD index = 0; + while (cr == ERROR_SUCCESS) + { + if ((cr = RegEnumKey(hkEnum, index, keyname, mpt::saturate_cast<DWORD>(std::size(keyname)))) == ERROR_SUCCESS) + { + CLSID clsid; + mpt::winstring formattedKey = mpt::winstring(_T("{")) + mpt::winstring(keyname) + mpt::winstring(_T("}")); + if(mpt::VerifyStringToCLSID(formattedKey, clsid)) + { + if(!mpt::contains(knownDMOs, clsid)) + { + HKEY hksub; + formattedKey = mpt::winstring(_T("software\\classes\\DirectShow\\MediaObjects\\")) + mpt::winstring(keyname); + if (RegOpenKey(HKEY_LOCAL_MACHINE, formattedKey.c_str(), &hksub) == ERROR_SUCCESS) + { + TCHAR name[64]; + DWORD datatype = REG_SZ; + DWORD datasize = sizeof(name); + + if(ERROR_SUCCESS == RegQueryValueEx(hksub, nullptr, 0, &datatype, (LPBYTE)name, &datasize)) + { + VSTPluginLib *plug = new (std::nothrow) VSTPluginLib(DMOPlugin::Create, true, mpt::PathString::FromNative(mpt::GUIDToString(clsid)), mpt::PathString::FromNative(ParseMaybeNullTerminatedStringFromBufferWithSizeInBytes<mpt::winstring>(name, datasize))); + if(plug != nullptr) + { + try + { + pluginList.push_back(plug); + plug->pluginId1 = kDmoMagic; + plug->pluginId2 = clsid.Data1; + plug->category = VSTPluginLib::catDMO; + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + delete plug; + } +#ifdef DMO_LOG + MPT_LOG_GLOBAL(LogDebug, "DMO", MPT_UFORMAT("Found \"{}\" clsid={}\n")(plug->libraryName, plug->dllPath)); +#endif + } + } + RegCloseKey(hksub); + } + } + } + } + index++; + } + if (hkEnum) RegCloseKey(hkEnum); +#endif // MPT_WITH_DMO +} + + +// Extract instrument and category information from plugin. +#ifdef MPT_WITH_VST +static void GetPluginInformation(bool maskCrashes, Vst::AEffect *effect, VSTPluginLib &library) +{ + unsigned long exception = 0; + library.category = static_cast<VSTPluginLib::PluginCategory>(CVstPlugin::DispatchSEH(maskCrashes, effect, Vst::effGetPlugCategory, 0, 0, nullptr, 0, exception)); + library.isInstrument = ((effect->flags & Vst::effFlagsIsSynth) || !effect->numInputs); + + if(library.isInstrument) + { + library.category = VSTPluginLib::catSynth; + } else if(library.category >= VSTPluginLib::numCategories) + { + library.category = VSTPluginLib::catUnknown; + } + +#ifdef MODPLUG_TRACKER + std::vector<char> s(256, 0); + CVstPlugin::DispatchSEH(maskCrashes, effect, Vst::effGetVendorString, 0, 0, s.data(), 0, exception); + library.vendor = mpt::ToCString(mpt::Charset::Locale, s.data()); +#endif // MODPLUG_TRACKER +} +#endif // MPT_WITH_VST + + +#ifdef MPT_WITH_VST +static bool TryLoadPlugin(bool maskCrashes, VSTPluginLib *plug, HINSTANCE hLib, unsigned long &exception) +{ + Vst::AEffect *pEffect = CVstPlugin::LoadPlugin(maskCrashes, *plug, hLib, CVstPlugin::BridgeMode::DetectRequiredBridgeMode); + if(!pEffect || pEffect->magic != Vst::kEffectMagic || !pEffect->dispatcher) + { + return false; + } + + CVstPlugin::DispatchSEH(maskCrashes, pEffect, Vst::effOpen, 0, 0, 0, 0, exception); + + plug->pluginId1 = pEffect->magic; + plug->pluginId2 = pEffect->uniqueID; + + GetPluginInformation(maskCrashes, pEffect, *plug); + +#ifdef VST_LOG + intptr_t nver = CVstPlugin::DispatchSEH(maskCrashes, pEffect, Vst::effGetVstVersion, 0,0, nullptr, 0, exception); + if (!nver) nver = pEffect->version; + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("{}: v{}.0, {} in, {} out, {} programs, {} params, flags=0x{} realQ={} offQ={}")( + plug->libraryName, nver, + pEffect->numInputs, pEffect->numOutputs, + mpt::ufmt::dec0<2>(pEffect->numPrograms), mpt::ufmt::dec0<2>(pEffect->numParams), + mpt::ufmt::HEX0<4>(static_cast<int32>(pEffect->flags)), pEffect->realQualities, pEffect->offQualities)); +#endif // VST_LOG + + CVstPlugin::DispatchSEH(maskCrashes, pEffect, Vst::effClose, 0, 0, 0, 0, exception); + + return true; +} +#endif // !NO_NVST + + +#ifdef MODPLUG_TRACKER +// Add a plugin to the list of known plugins. +VSTPluginLib *CVstPluginManager::AddPlugin(const mpt::PathString &dllPath, bool maskCrashes, const mpt::ustring &tags, bool fromCache, bool *fileFound) +{ + const mpt::PathString fileName = dllPath.GetFileName(); + + // Check if this is already a known plugin. + for(const auto &dupePlug : pluginList) + { + if(!dllPath.CompareNoCase(dllPath, dupePlug->dllPath)) return dupePlug; + } + + if(fileFound != nullptr) + { + *fileFound = dllPath.IsFile(); + } + + // Look if the plugin info is stored in the PluginCache + if(fromCache) + { + SettingsContainer & cacheFile = theApp.GetPluginCache(); + // First try finding the full path + mpt::ustring IDs = cacheFile.Read<mpt::ustring>(cacheSection, dllPath.ToUnicode(), U_("")); + if(IDs.length() < 16) + { + // If that didn't work out, find relative path + mpt::PathString relPath = theApp.PathAbsoluteToInstallRelative(dllPath); + IDs = cacheFile.Read<mpt::ustring>(cacheSection, relPath.ToUnicode(), U_("")); + } + + if(IDs.length() >= 16) + { + VSTPluginLib *plug = new (std::nothrow) VSTPluginLib(nullptr, false, dllPath, fileName, tags); + if(plug == nullptr) + { + return nullptr; + } + pluginList.push_back(plug); + + // Extract plugin IDs + for (int i = 0; i < 16; i++) + { + int32 n = IDs[i] - '0'; + if (n > 9) n = IDs[i] + 10 - 'A'; + n &= 0x0f; + if (i < 8) + { + plug->pluginId1 = (plug->pluginId1 << 4) | n; + } else + { + plug->pluginId2 = (plug->pluginId2 << 4) | n; + } + } + + const mpt::ustring flagKey = IDs + U_(".Flags"); + plug->DecodeCacheFlags(cacheFile.Read<int32>(cacheSection, flagKey, 0)); + plug->vendor = cacheFile.Read<CString>(cacheSection, IDs + U_(".Vendor"), CString()); + +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("Plugin \"{}\" found in PluginCache")(plug->libraryName)); +#endif // VST_LOG + return plug; + } else + { +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("Plugin mismatch in PluginCache: \"{}\" [{}]")(dllPath, IDs)); +#endif // VST_LOG + } + } + + // If this key contains a file name on program launch, a plugin previously crashed OpenMPT. + theApp.GetSettings().Write<mpt::PathString>(U_("VST Plugins"), U_("FailedPlugin"), dllPath, SettingWriteThrough); + + bool validPlug = false; + + VSTPluginLib *plug = new (std::nothrow) VSTPluginLib(nullptr, false, dllPath, fileName, tags); + if(plug == nullptr) + { + return nullptr; + } + +#ifdef MPT_WITH_VST + unsigned long exception = 0; + // Always scan plugins in a separate process + HINSTANCE hLib = NULL; + { +#ifdef MODPLUG_TRACKER + ExceptionHandler::Context ectx{ MPT_UFORMAT("VST Plugin: {}")(plug->dllPath.ToUnicode()) }; + ExceptionHandler::ContextSetter ectxguard{&ectx}; +#endif // MODPLUG_TRACKER + + validPlug = TryLoadPlugin(maskCrashes, plug, hLib, exception); + } + if(hLib) + { + FreeLibrary(hLib); + } + if(exception != 0) + { + CVstPluginManager::ReportPlugException(MPT_UFORMAT("Exception {} while trying to load plugin \"{}\"!\n")(mpt::ufmt::HEX0<8>(exception), plug->libraryName)); + } + +#endif // MPT_WITH_VST + + // Now it should be safe to assume that this plugin loaded properly. :) + theApp.GetSettings().Remove(U_("VST Plugins"), U_("FailedPlugin")); + + // If OK, write the information in PluginCache + if(validPlug) + { + pluginList.push_back(plug); + plug->WriteToCache(); + } else + { + delete plug; + } + + return (validPlug ? plug : nullptr); +} + + +// Remove a plugin from the list of known plugins and release any remaining instances of it. +bool CVstPluginManager::RemovePlugin(VSTPluginLib *pFactory) +{ + for(const_iterator p = begin(); p != end(); p++) + { + VSTPluginLib *plug = *p; + if(plug == pFactory) + { + // Kill all instances of this plugin + CriticalSection cs; + + while(plug->pPluginsList != nullptr) + { + plug->pPluginsList->Release(); + } + pluginList.erase(p); + delete plug; + return true; + } + } + return false; +} +#endif // MODPLUG_TRACKER + + +// Create an instance of a plugin. +bool CVstPluginManager::CreateMixPlugin(SNDMIXPLUGIN &mixPlugin, CSoundFile &sndFile) +{ + VSTPluginLib *pFound = nullptr; + + // Find plugin in library + enum PlugMatchQuality + { + kNoMatch, + kMatchName, + kMatchId, + kMatchNameAndId, + }; + + PlugMatchQuality match = kNoMatch; // "Match quality" of found plugin. Higher value = better match. +#if MPT_OS_WINDOWS && !MPT_OS_WINDOWS_WINRT + const mpt::PathString libraryName = mpt::PathString::FromUnicode(mixPlugin.GetLibraryName()); +#else + const std::string libraryName = mpt::ToCharset(mpt::Charset::UTF8, mixPlugin.GetLibraryName()); +#endif + for(const auto &plug : pluginList) + { + const bool matchID = (plug->pluginId1 == mixPlugin.Info.dwPluginId1) + && (plug->pluginId2 == mixPlugin.Info.dwPluginId2); +#if MPT_OS_WINDOWS && !MPT_OS_WINDOWS_WINRT + const bool matchName = !mpt::PathString::CompareNoCase(plug->libraryName, libraryName); +#else + const bool matchName = !mpt::CompareNoCaseAscii(plug->libraryName.ToUTF8(), libraryName); +#endif + + if(matchID && matchName) + { + pFound = plug; +#ifdef MPT_WITH_VST + if(plug->IsNative(false)) + { + break; + } +#endif // MPT_WITH_VST + // If the plugin isn't native, first check if a native version can be found. + match = kMatchNameAndId; + } else if(matchID && match < kMatchId) + { + pFound = plug; + match = kMatchId; + } else if(matchName && match < kMatchName) + { + pFound = plug; + match = kMatchName; + } + } + + if(pFound != nullptr && pFound->Create != nullptr) + { + IMixPlugin *plugin = pFound->Create(*pFound, sndFile, &mixPlugin); + return plugin != nullptr; + } + +#ifdef MODPLUG_TRACKER + bool maskCrashes = TrackerSettings::Instance().BrokenPluginsWorkaroundVSTMaskAllCrashes; + + if(!pFound && (mixPlugin.GetLibraryName() != U_(""))) + { + // Try finding the plugin DLL in the plugin directory or plugin cache instead. + mpt::PathString fullPath = TrackerSettings::Instance().PathPlugins.GetDefaultDir(); + if(fullPath.empty()) + { + fullPath = theApp.GetInstallPath() + P_("Plugins\\"); + } + fullPath += mpt::PathString::FromUnicode(mixPlugin.GetLibraryName()) + P_(".dll"); + + pFound = AddPlugin(fullPath, maskCrashes); + if(!pFound) + { + // Try plugin cache (search for library name) + SettingsContainer &cacheFile = theApp.GetPluginCache(); + mpt::ustring IDs = cacheFile.Read<mpt::ustring>(cacheSection, mixPlugin.GetLibraryName(), U_("")); + if(IDs.length() >= 16) + { + fullPath = cacheFile.Read<mpt::PathString>(cacheSection, IDs, P_("")); + if(!fullPath.empty()) + { + fullPath = theApp.PathInstallRelativeToAbsolute(fullPath); + if(fullPath.IsFile()) + { + pFound = AddPlugin(fullPath, maskCrashes); + } + } + } + } + } + +#ifdef MPT_WITH_VST + if(pFound && mixPlugin.Info.dwPluginId1 == Vst::kEffectMagic) + { + Vst::AEffect *pEffect = nullptr; + HINSTANCE hLibrary = nullptr; + bool validPlugin = false; + + pEffect = CVstPlugin::LoadPlugin(maskCrashes, *pFound, hLibrary, TrackerSettings::Instance().bridgeAllPlugins ? CVstPlugin::BridgeMode::ForceBridgeWithFallback : CVstPlugin::BridgeMode::Automatic); + + if(pEffect != nullptr && pEffect->dispatcher != nullptr && pEffect->magic == Vst::kEffectMagic) + { + validPlugin = true; + + GetPluginInformation(maskCrashes, pEffect, *pFound); + + // Update cached information + pFound->WriteToCache(); + + CVstPlugin *pVstPlug = new (std::nothrow) CVstPlugin(maskCrashes, hLibrary, *pFound, mixPlugin, *pEffect, sndFile); + if(pVstPlug == nullptr) + { + validPlugin = false; + } + } + + if(!validPlugin) + { + FreeLibrary(hLibrary); + CVstPluginManager::ReportPlugException(MPT_UFORMAT("Unable to create plugin \"{}\"!\n")(pFound->libraryName)); + } + return validPlugin; + } else + { + // "plug not found" notification code MOVED to CSoundFile::Create +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", U_("Unknown plugin")); +#endif + } +#endif // MPT_WITH_VST + +#endif // MODPLUG_TRACKER + return false; +} + + +#ifdef MODPLUG_TRACKER +void CVstPluginManager::OnIdle() +{ + for(auto &factory : pluginList) + { + // Note: bridged plugins won't receive these messages and generate their own idle messages. + IMixPlugin *p = factory->pPluginsList; + while (p) + { + //rewbs. VSTCompliance: A specific plug has requested indefinite periodic processing time. + p->Idle(); + //We need to update all open editors + CAbstractVstEditor *editor = p->GetEditor(); + if (editor && editor->m_hWnd) + { + editor->UpdateParamDisplays(); + } + //end rewbs. VSTCompliance: + + p = p->GetNextInstance(); + } + } +} + + +void CVstPluginManager::ReportPlugException(const mpt::ustring &msg) +{ + Reporting::Notification(msg); +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", mpt::ToUnicode(msg)); +#endif +} + +#endif // MODPLUG_TRACKER + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.h new file mode 100644 index 00000000..84575ad6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginManager.h @@ -0,0 +1,190 @@ +/* + * PluginManager.h + * --------------- + * Purpose: Plugin management + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +constexpr int32 PLUGMAGIC(char a, char b, char c, char d) noexcept +{ + return static_cast<int32>((static_cast<uint32>(a) << 24) | (static_cast<uint32>(b) << 16) | (static_cast<uint32>(c) << 8) | (static_cast<uint32>(d) << 0)); +} + +//#define kBuzzMagic PLUGMAGIC('B', 'u', 'z', 'z') +inline constexpr int32 kDmoMagic = PLUGMAGIC('D', 'X', 'M', 'O'); + +class CSoundFile; +class IMixPlugin; +struct SNDMIXPLUGIN; +enum PluginArch : int; + +struct VSTPluginLib +{ +public: + enum PluginCategory : uint8 + { + // Same plugin categories as defined in VST SDK + catUnknown = 0, + catEffect, // Simple Effect + catSynth, // VST Instrument (Synths, samplers,...) + catAnalysis, // Scope, Tuner, ... + catMastering, // Dynamics, ... + catSpacializer, // Panners, ... + catRoomFx, // Delays and Reverbs + catSurroundFx, // Dedicated surround processor + catRestoration, // Denoiser, ... + catOfflineProcess, // Offline Process + catShell, // Plug-in is container of other plug-ins + catGenerator, // Tone Generator, ... + // Custom categories + catDMO, // DirectX media object plugin + catHidden, // For internal plugins that should not be visible to the user (e.g. because they only exist for legacy reasons) + + numCategories + }; + +public: + using CreateProc = IMixPlugin *(*)(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + IMixPlugin *pPluginsList = nullptr; // Pointer to first plugin instance (this instance carries pointers to other instances) + CreateProc Create; // Factory to call for this plugin + mpt::PathString libraryName; // Display name + mpt::PathString dllPath; // Full path name +#ifdef MODPLUG_TRACKER + mpt::ustring tags; // User tags + CString vendor; +#endif // MODPLUG_TRACKER + int32 pluginId1 = 0; // Plugin type (kEffectMagic, kDmoMagic, ...) + int32 pluginId2 = 0; // Plugin unique ID + PluginCategory category = catUnknown; + const bool isBuiltIn : 1; + bool isInstrument : 1; + bool useBridge : 1, shareBridgeInstance : 1, modernBridge : 1; +protected: + mutable uint8 dllArch = 0; + +public: + VSTPluginLib(CreateProc factoryProc, bool isBuiltIn, const mpt::PathString &dllPath, const mpt::PathString &libraryName +#ifdef MODPLUG_TRACKER + , const mpt::ustring &tags = mpt::ustring(), const CString &vendor = CString() +#endif // MODPLUG_TRACKER + ) + : Create(factoryProc) + , libraryName(libraryName), dllPath(dllPath) +#ifdef MODPLUG_TRACKER + , tags(tags) + , vendor(vendor) +#endif // MODPLUG_TRACKER + , category(catUnknown) + , isBuiltIn(isBuiltIn), isInstrument(false) + , useBridge(false), shareBridgeInstance(true), modernBridge(true) + { + } + +#ifdef MPT_WITH_VST + + // Get native phost process arch encoded as plugin arch + static uint8 GetNativePluginArch(); + static mpt::ustring GetPluginArchName(uint8 arch); + static mpt::ustring GetPluginArchNameUser(uint8 arch); + + // Check whether a plugin can be hosted inside OpenMPT or requires bridging + uint8 GetDllArch(bool fromCache = true) const; + mpt::ustring GetDllArchName(bool fromCache = true) const; + mpt::ustring GetDllArchNameUser(bool fromCache = true) const; + bool IsNative(bool fromCache = true) const; + // Check if a plugin is native, and if it is currently unknown, assume that it is native. Use this function only for performance reasons + // (e.g. if tons of unscanned plugins would slow down generation of the plugin selection dialog) + bool IsNativeFromCache() const; + +#endif // MPT_WITH_VST + + void WriteToCache() const; + + uint32 EncodeCacheFlags() const + { + // Format: 00000000.0000000M.AAAAAASB.CCCCCCCI + return (isInstrument ? 1 : 0) + | (category << 1) + | (useBridge ? 0x100 : 0) + | (shareBridgeInstance ? 0x200 : 0) + | ((dllArch / 8) << 10) + | (modernBridge ? 0x10000 : 0) + ; + } + + void DecodeCacheFlags(uint32 flags) + { + category = static_cast<PluginCategory>((flags & 0xFF) >> 1); + if(category >= numCategories) + { + category = catUnknown; + } + if(flags & 1) + { + isInstrument = true; + category = catSynth; + } + useBridge = (flags & 0x100) != 0; + shareBridgeInstance = (flags & 0x200) != 0; + dllArch = ((flags >> 10) & 0x3F) * 8; + modernBridge = (flags & 0x10000) != 0; + } +}; + + +class CVstPluginManager +{ +#ifndef NO_PLUGINS +protected: +#if defined(MPT_WITH_DMO) + bool MustUnInitilizeCOM = false; +#endif + std::vector<VSTPluginLib *> pluginList; + +public: + CVstPluginManager(); + ~CVstPluginManager(); + + using iterator = std::vector<VSTPluginLib *>::iterator; + using const_iterator = std::vector<VSTPluginLib *>::const_iterator; + + iterator begin() { return pluginList.begin(); } + const_iterator begin() const { return pluginList.begin(); } + iterator end() { return pluginList.end(); } + const_iterator end() const { return pluginList.end(); } + void reserve(size_t num) { pluginList.reserve(num); } + size_t size() const { return pluginList.size(); } + + bool IsValidPlugin(const VSTPluginLib *pLib) const; + VSTPluginLib *AddPlugin(const mpt::PathString &dllPath, bool maskCrashes, const mpt::ustring &tags = mpt::ustring(), bool fromCache = true, bool *fileFound = nullptr); + bool RemovePlugin(VSTPluginLib *); + bool CreateMixPlugin(SNDMIXPLUGIN &, CSoundFile &); + void OnIdle(); + static void ReportPlugException(const mpt::ustring &msg); + +protected: + void EnumerateDirectXDMOs(); + +#else // NO_PLUGINS +public: + const VSTPluginLib **begin() const { return nullptr; } + const VSTPluginLib **end() const { return nullptr; } + void reserve(size_t) { } + size_t size() const { return 0; } + + void OnIdle() {} +#endif // NO_PLUGINS +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginMixBuffer.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginMixBuffer.h new file mode 100644 index 00000000..55178778 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginMixBuffer.h @@ -0,0 +1,137 @@ +/* + * PluginMixBuffer.h + * ----------------- + * Purpose: Helper class for managing plugin audio input and output buffers. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <algorithm> +#include <array> + +#if defined(MPT_ENABLE_ARCH_INTRINSICS) || defined(MPT_WITH_VST) +#include "mpt/base/aligned_array.hpp" +#endif // MPT_ENABLE_ARCH_INTRINSICS || MPT_WITH_VST + + +OPENMPT_NAMESPACE_BEGIN + + +// At least this part of the code is ready for double-precision rendering... :> +// buffer_t: Sample buffer type (float, double, ...) +// bufferSize: Buffer size in samples +template<typename buffer_t, uint32 bufferSize> +class PluginMixBuffer +{ + +private: + +#if defined(MPT_ENABLE_ARCH_INTRINSICS) || defined(MPT_WITH_VST) + static constexpr std::align_val_t alignment = std::align_val_t{16}; + static_assert(sizeof(mpt::aligned_array<buffer_t, bufferSize, alignment>) == sizeof(std::array<buffer_t, bufferSize>)); + static_assert(alignof(mpt::aligned_array<buffer_t, bufferSize, alignment>) == static_cast<std::size_t>(alignment)); +#endif // MPT_ENABLE_ARCH_INTRINSICS || MPT_WITH_VST + +protected: + +#if defined(MPT_ENABLE_ARCH_INTRINSICS) || defined(MPT_WITH_VST) + std::vector<mpt::aligned_array<buffer_t, bufferSize, alignment>> inputs; + std::vector<mpt::aligned_array<buffer_t, bufferSize, alignment>> outputs; +#else // !(MPT_ENABLE_ARCH_INTRINSICS || MPT_WITH_VST) + std::vector<std::array<buffer_t, bufferSize>> inputs; + std::vector<std::array<buffer_t, bufferSize>> outputs; +#endif // MPT_ENABLE_ARCH_INTRINSICS || MPT_WITH_VST + std::vector<buffer_t*> inputsarray; + std::vector<buffer_t*> outputsarray; + +public: + + // Allocate input and output buffers + bool Initialize(uint32 numInputs, uint32 numOutputs) + { + // Short cut - we do not need to recreate the buffers. + if(inputs.size() == numInputs && outputs.size() == numOutputs) + { + return true; + } + + try + { + inputs.resize(numInputs); + outputs.resize(numOutputs); + inputsarray.resize(numInputs); + outputsarray.resize(numOutputs); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + inputs.clear(); + inputs.shrink_to_fit(); + outputs.clear(); + outputs.shrink_to_fit(); + inputsarray.clear(); + inputsarray.shrink_to_fit(); + outputsarray.clear(); + outputsarray.shrink_to_fit(); + return false; + } + + for(uint32 i = 0; i < numInputs; i++) + { + inputsarray[i] = inputs[i].data(); + } + + for(uint32 i = 0; i < numOutputs; i++) + { + outputsarray[i] = outputs[i].data(); + } + + return true; + } + + // Silence all input buffers. + void ClearInputBuffers(uint32 numSamples) + { + MPT_ASSERT(numSamples <= bufferSize); + for(size_t i = 0; i < inputs.size(); i++) + { + std::fill(inputs[i].data(), inputs[i].data() + numSamples, buffer_t{0}); + } + } + + // Silence all output buffers. + void ClearOutputBuffers(uint32 numSamples) + { + MPT_ASSERT(numSamples <= bufferSize); + for(size_t i = 0; i < outputs.size(); i++) + { + std::fill(outputs[i].data(), outputs[i].data() + numSamples, buffer_t{0}); + } + } + + PluginMixBuffer() + { + Initialize(2, 0); + } + + // Return pointer to a given input or output buffer + const buffer_t *GetInputBuffer(uint32 index) const { return inputs[index].data(); } + const buffer_t *GetOutputBuffer(uint32 index) const { return outputs[index].data(); } + buffer_t *GetInputBuffer(uint32 index) { return inputs[index].data(); } + buffer_t *GetOutputBuffer(uint32 index) { return outputs[index].data(); } + + // Return pointer array to all input or output buffers + buffer_t **GetInputBufferArray() { return inputs.empty() ? nullptr : inputsarray.data(); } + buffer_t **GetOutputBufferArray() { return outputs.empty() ? nullptr : outputsarray.data(); } + + bool Ok() const { return (inputs.size() + outputs.size()) > 0; } + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginStructs.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginStructs.h new file mode 100644 index 00000000..4dc58d30 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/PluginStructs.h @@ -0,0 +1,141 @@ +/* + * PluginStructs.h + * --------------- + * Purpose: Basic plugin structs for CSoundFile. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../Snd_defs.h" +#ifndef NO_PLUGINS +#include "openmpt/base/Endian.hpp" +#endif // NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +//////////////////////////////////////////////////////////////////// +// Mix Plugins + +using PlugParamIndex = uint32; +using PlugParamValue = float; + +struct SNDMIXPLUGINSTATE; +struct SNDMIXPLUGIN; +class IMixPlugin; +class CSoundFile; + +#ifndef NO_PLUGINS + +struct SNDMIXPLUGININFO +{ + // dwInputRouting flags + enum RoutingFlags + { + irApplyToMaster = 0x01, // Apply to master mix + irBypass = 0x02, // Bypass effect + irWetMix = 0x04, // Wet Mix (dry added) + irExpandMix = 0x08, // [0%,100%] -> [-200%,200%] + irAutoSuspend = 0x10, // Plugin will automatically suspend on silence + }; + + int32le dwPluginId1; // Plugin type (kEffectMagic, kDmoMagic, kBuzzMagic) + int32le dwPluginId2; // Plugin unique ID + uint8le routingFlags; // See RoutingFlags + uint8le mixMode; + uint8le gain; // Divide by 10 to get real gain + uint8le reserved; + uint32le dwOutputRouting; // 0 = send to master 0x80 + x = send to plugin x + uint32le dwReserved[4]; // Reserved for routing info + mpt::modecharbuf<32, mpt::String::nullTerminated> szName; // User-chosen plugin display name - this is locale ANSI! + mpt::modecharbuf<64, mpt::String::nullTerminated> szLibraryName; // original DLL name - this is UTF-8! + + // Should only be called from SNDMIXPLUGIN::SetBypass() and IMixPlugin::Bypass() + void SetBypass(bool bypass = true) { if(bypass) routingFlags |= irBypass; else routingFlags &= uint8(~irBypass); } +}; + +MPT_BINARY_STRUCT(SNDMIXPLUGININFO, 128) // this is directly written to files, so the size must be correct! + + +struct SNDMIXPLUGIN +{ + IMixPlugin *pMixPlugin = nullptr; + std::vector<std::byte> pluginData; + SNDMIXPLUGININFO Info = {}; + float fDryRatio = 0; + int32 defaultProgram = 0; + int32 editorX = 0, editorY = 0; + +#if defined(MPT_ENABLE_CHARSET_LOCALE) + const char * GetNameLocale() const + { + return Info.szName.buf; + } + mpt::ustring GetName() const + { + return mpt::ToUnicode(mpt::Charset::Locale, Info.szName); + } +#endif // MPT_ENABLE_CHARSET_LOCALE + mpt::ustring GetLibraryName() const + { + return mpt::ToUnicode(mpt::Charset::UTF8, Info.szLibraryName); + } + + // Check if a plugin is loaded into this slot (also returns true if the plugin in this slot has not been found) + bool IsValidPlugin() const { return (Info.dwPluginId1 | Info.dwPluginId2) != 0; } + + // Input routing getters + uint8 GetGain() const + { return Info.gain; } + uint8 GetMixMode() const + { return Info.mixMode; } + bool IsMasterEffect() const + { return (Info.routingFlags & SNDMIXPLUGININFO::irApplyToMaster) != 0; } + bool IsWetMix() const + { return (Info.routingFlags & SNDMIXPLUGININFO::irWetMix) != 0; } + bool IsExpandedMix() const + { return (Info.routingFlags & SNDMIXPLUGININFO::irExpandMix) != 0; } + bool IsBypassed() const + { return (Info.routingFlags & SNDMIXPLUGININFO::irBypass) != 0; } + bool IsAutoSuspendable() const + { return (Info.routingFlags & SNDMIXPLUGININFO::irAutoSuspend) != 0; } + + // Input routing setters + void SetGain(uint8 gain); + void SetMixMode(uint8 mixMode) + { Info.mixMode = mixMode; } + void SetMasterEffect(bool master = true) + { if(master) Info.routingFlags |= SNDMIXPLUGININFO::irApplyToMaster; else Info.routingFlags &= uint8(~SNDMIXPLUGININFO::irApplyToMaster); } + void SetWetMix(bool wetMix = true) + { if(wetMix) Info.routingFlags |= SNDMIXPLUGININFO::irWetMix; else Info.routingFlags &= uint8(~SNDMIXPLUGININFO::irWetMix); } + void SetExpandedMix(bool expanded = true) + { if(expanded) Info.routingFlags |= SNDMIXPLUGININFO::irExpandMix; else Info.routingFlags &= uint8(~SNDMIXPLUGININFO::irExpandMix); } + void SetBypass(bool bypass = true); + void SetAutoSuspend(bool suspend = true) + { if(suspend) Info.routingFlags |= SNDMIXPLUGININFO::irAutoSuspend; else Info.routingFlags &= uint8(~SNDMIXPLUGININFO::irAutoSuspend); } + + // Output routing getters + bool IsOutputToMaster() const + { return Info.dwOutputRouting == 0; } + PLUGINDEX GetOutputPlugin() const + { return Info.dwOutputRouting >= 0x80 ? static_cast<PLUGINDEX>(Info.dwOutputRouting - 0x80) : PLUGINDEX_INVALID; } + + // Output routing setters + void SetOutputToMaster() + { Info.dwOutputRouting = 0; } + void SetOutputPlugin(PLUGINDEX plugin) + { if(plugin < MAX_MIXPLUGINS) Info.dwOutputRouting = plugin + 0x80; else Info.dwOutputRouting = 0; } + + void Destroy(); +}; + +bool CreateMixPluginProc(SNDMIXPLUGIN &mixPlugin, CSoundFile &sndFile); + +#endif // NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.cpp new file mode 100644 index 00000000..8c2c9280 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.cpp @@ -0,0 +1,271 @@ +/* + * SymMODEcho.cpp + * -------------- + * Purpose: Implementation of the SymMOD Echo DSP + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../Sndfile.h" +#include "SymMODEcho.h" + +OPENMPT_NAMESPACE_BEGIN + +IMixPlugin *SymMODEcho::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) SymMODEcho(factory, sndFile, mixStruct); +} + + +SymMODEcho::SymMODEcho(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_chunk(PluginChunk::Default()) +{ + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); + RecalculateEchoParams(); +} + + +void SymMODEcho::Process(float* pOutL, float* pOutR, uint32 numFrames) +{ + const float *srcL = m_mixBuffer.GetInputBuffer(0), *srcR = m_mixBuffer.GetInputBuffer(1); + float *outL = m_mixBuffer.GetOutputBuffer(0), *outR = m_mixBuffer.GetOutputBuffer(1); + + const uint32 delayTime = m_SndFile.m_PlayState.m_nSamplesPerTick * m_chunk.param[kEchoDelay]; + // SymMODs don't have a variable tempo so the tick duration should never change... but if someone loads a module into an MPTM file we have to account for this. + if(m_delayLine.size() < delayTime * 2) + m_delayLine.resize(delayTime * 2); + + const auto dspType = GetDSPType(); + if(dspType == DSPType::Off) + { + // Toggling the echo while it's running keeps its delay line untouched + std::copy(srcL, srcL + numFrames, outL); + std::copy(srcR, srcR + numFrames, outR); + } else + { + for(uint32 i = 0; i < numFrames; i++) + { + if(m_writePos >= delayTime) + m_writePos = 0; + int readPos = m_writePos - delayTime; + if(readPos < 0) + readPos += delayTime; + + const float lDry = *srcL++, rDry = *srcR++; + const float lDelay = m_delayLine[readPos * 2], rDelay = m_delayLine[readPos * 2 + 1]; + + // Output samples + *outL++ = (lDry + lDelay); + *outR++ = (rDry + rDelay); + + // Compute new delay line values + float lOut = 0.0f, rOut = 0.0f; + switch(dspType) + { + case DSPType::Off: + break; + case DSPType::Normal: // Normal + lOut = (lDelay + lDry) * m_feedback; + rOut = (rDelay + rDry) * m_feedback; + break; + case DSPType::Cross: + case DSPType::Cross2: + lOut = (rDelay + rDry) * m_feedback; + rOut = (lDelay + lDry) * m_feedback; + break; + case DSPType::Center: + lOut = (lDelay + (lDry + rDry) * 0.5f) * m_feedback; + rOut = lOut; + break; + case DSPType::NumTypes: + break; + } + + // Prevent denormals + if(std::abs(lOut) < 1e-24f) + lOut = 0.0f; + if(std::abs(rOut) < 1e-24f) + rOut = 0.0f; + + m_delayLine[m_writePos * 2 + 0] = lOut; + m_delayLine[m_writePos * 2 + 1] = rOut; + m_writePos++; + } + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +void SymMODEcho::SaveAllParameters() +{ + m_pMixStruct->defaultProgram = -1; + try + { + const auto pluginData = mpt::as_raw_memory(m_chunk); + m_pMixStruct->pluginData.assign(pluginData.begin(), pluginData.end()); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_pMixStruct->pluginData.clear(); + } +} + + +void SymMODEcho::RestoreAllParameters(int32 program) +{ + if(m_pMixStruct->pluginData.size() == sizeof(m_chunk) && !memcmp(m_pMixStruct->pluginData.data(), "Echo", 4)) + { + std::copy(m_pMixStruct->pluginData.begin(), m_pMixStruct->pluginData.end(), mpt::as_raw_memory(m_chunk).begin()); + } else + { + IMixPlugin::RestoreAllParameters(program); + } + RecalculateEchoParams(); +} + + +PlugParamValue SymMODEcho::GetParameter(PlugParamIndex index) +{ + if(index < kEchoNumParameters) + { + return m_chunk.param[index] / 127.0f; + } + return 0; +} + + +void SymMODEcho::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kEchoNumParameters) + { + m_chunk.param[index] = mpt::saturate_round<uint8>(mpt::safe_clamp(value, 0.0f, 1.0f) * 127.0f); + RecalculateEchoParams(); + } +} + + +void SymMODEcho::Resume() +{ + m_isResumed = true; + PositionChanged(); +} + + +void SymMODEcho::PositionChanged() +{ + try + { + m_delayLine.assign(127 * 2 * m_SndFile.m_PlayState.m_nSamplesPerTick, 0.0f); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + m_writePos = 0; +} + + +#ifdef MODPLUG_TRACKER + +std::pair<PlugParamValue, PlugParamValue> SymMODEcho::GetParamUIRange(PlugParamIndex param) +{ + if(param == kEchoType) + return {0.0f, (static_cast<uint8>(DSPType::NumTypes) - 1) / 127.0f}; + else + return {0.0f, 1.0f}; +} + +CString SymMODEcho::GetParamName(PlugParamIndex param) +{ + switch (param) + { + case kEchoType: return _T("Type"); + case kEchoDelay: return _T("Delay"); + case kEchoFeedback: return _T("Feedback"); + case kEchoNumParameters: break; + } + return {}; +} + + +CString SymMODEcho::GetParamLabel(PlugParamIndex param) +{ + if(param == kEchoDelay) + return _T("Ticks"); + if(param == kEchoFeedback) + return _T("%"); + return {}; +} + + +CString SymMODEcho::GetParamDisplay(PlugParamIndex param) +{ + switch(static_cast<Parameters>(param)) + { + case kEchoType: + switch(GetDSPType()) + { + case DSPType::Off: return _T("Off"); + case DSPType::Normal: return _T("Normal"); + case DSPType::Cross: return _T("Cross"); + case DSPType::Cross2: return _T("Cross 2"); + case DSPType::Center: return _T("Center"); + case DSPType::NumTypes: break; + } + break; + case kEchoDelay: + return mpt::cfmt::val(m_chunk.param[kEchoDelay]); + case kEchoFeedback: + return mpt::cfmt::flt(m_feedback * 100.0f, 4); + case kEchoNumParameters: + break; + } + return {}; +} + +#endif // MODPLUG_TRACKER + + +IMixPlugin::ChunkData SymMODEcho::GetChunk(bool) +{ + auto data = reinterpret_cast<const std::byte *>(&m_chunk); + return ChunkData(data, sizeof(m_chunk)); +} + + +void SymMODEcho::SetChunk(const ChunkData& chunk, bool) +{ + auto data = chunk.data(); + if(chunk.size() == sizeof(chunk) && !memcmp(data, "Echo", 4)) + { + memcpy(&m_chunk, data, chunk.size()); + RecalculateEchoParams(); + } +} + + +void SymMODEcho::RecalculateEchoParams() +{ + if(m_chunk.param[kEchoType] >= static_cast<uint8>(DSPType::NumTypes)) + m_chunk.param[kEchoType] = 0; + if(m_chunk.param[kEchoDelay] > 127) + m_chunk.param[kEchoDelay] = 127; + if(m_chunk.param[kEchoFeedback] > 127) + m_chunk.param[kEchoFeedback] = 127; + + if(GetDSPType() == DSPType::Cross2) + m_feedback = 1.0f - std::pow(2.0f, -static_cast<float>(m_chunk.param[kEchoFeedback] + 1)); + else + m_feedback = std::pow(2.0f, -static_cast<float>(m_chunk.param[kEchoFeedback])); +} + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.h new file mode 100644 index 00000000..4b54e0c8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/SymMODEcho.h @@ -0,0 +1,131 @@ +/* + * SymMODEcho.h + * ------------ + * Purpose: Implementation of the SymMOD Echo DSP + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +class SymMODEcho final : public IMixPlugin +{ +public: + enum class DSPType : uint8 + { + Off = 0, + Normal, + Cross, + Cross2, + Center, + NumTypes + }; + + enum Parameters + { + kEchoType = 0, + kEchoDelay, + kEchoFeedback, + kEchoNumParameters + }; + + // Our settings chunk for file I/O, as it will be written to files + struct PluginChunk + { + char id[4]; + uint8 param[kEchoNumParameters]; + + static PluginChunk Create(uint8 type, uint8 delay, uint8 feedback) + { + static_assert(sizeof(PluginChunk) == 7); + PluginChunk result; + memcpy(result.id, "Echo", 4); + result.param[kEchoType] = type; + result.param[kEchoDelay] = delay; + result.param[kEchoFeedback] = feedback; + return result; + } + static PluginChunk Default() + { + return Create(0, 4, 1); + } + }; + + std::vector<float> m_delayLine; + uint32 m_writePos = 0; // Current write position in the delay line + float m_feedback = 0.5f; + + // Settings chunk for file I/O + PluginChunk m_chunk; + +public: + static IMixPlugin* Create(VSTPluginLib& factory, CSoundFile& sndFile, SNDMIXPLUGIN* mixStruct); + SymMODEcho(VSTPluginLib& factory, CSoundFile& sndFile, SNDMIXPLUGIN* mixStruct); + + void Release() override { delete this; } + void SaveAllParameters() override; + void RestoreAllParameters(int32 program) override; + int32 GetUID() const override { int32le id; memcpy(&id, "Echo", 4); return id; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float* pOutL, float* pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kEchoNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Echo"); } + + std::pair<PlugParamValue, PlugParamValue> GetParamUIRange(PlugParamIndex param) override; + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString&) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + + bool ProgramsAreChunks() const override { return true; } + ChunkData GetChunk(bool) override; + void SetChunk(const ChunkData& chunk, bool) override; + +protected: + DSPType GetDSPType() const { return static_cast<DSPType>(m_chunk.param[kEchoType]); } + void RecalculateEchoParams(); +}; + +MPT_BINARY_STRUCT(SymMODEcho::PluginChunk, 7) + + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.cpp new file mode 100644 index 00000000..ba43b1fb --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.cpp @@ -0,0 +1,306 @@ +/* + * Chorus.cpp + * ---------- + * Purpose: Implementation of the DMO Chorus DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Chorus.h" +#include "mpt/base/numbers.hpp" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* Chorus::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Chorus(factory, sndFile, mixStruct); +} + + +Chorus::Chorus(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, bool isFlanger) + : IMixPlugin(factory, sndFile, mixStruct) + , m_isFlanger(isFlanger) +{ + m_param[kChorusWetDryMix] = 0.5f; + m_param[kChorusDepth] = 0.1f; + m_param[kChorusFrequency] = 0.11f; + m_param[kChorusWaveShape] = 1.0f; + m_param[kChorusPhase] = 0.75f; + m_param[kChorusFeedback] = (25.0f + 99.0f) / 198.0f; + m_param[kChorusDelay] = 0.8f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +// Integer part of buffer position +int32 Chorus::GetBufferIntOffset(int32 fpOffset) const +{ + if(fpOffset < 0) + fpOffset += m_bufSize * 4096; + MPT_ASSERT(fpOffset >= 0); + return (fpOffset / 4096) % m_bufSize; +} + + +void Chorus::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_bufSize || !m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + const bool isTriangle = IsTriangle(); + const float feedback = Feedback() / 100.0f; + const float wetDryMix = WetDryMix(); + const uint32 phase = Phase(); + const auto &bufferR = m_isFlanger ? m_bufferR : m_bufferL; + + for(uint32 i = numFrames; i != 0; i--) + { + const float leftIn = *(in[0])++; + const float rightIn = *(in[1])++; + + const int32 readOffset = GetBufferIntOffset(m_bufPos + m_delayOffset); + const int32 writeOffset = GetBufferIntOffset(m_bufPos); + if(m_isFlanger) + { + m_DryBufferL[m_dryWritePos] = leftIn; + m_DryBufferR[m_dryWritePos] = rightIn; + m_bufferL[writeOffset] = (m_bufferL[readOffset] * feedback) + leftIn; + m_bufferR[writeOffset] = (m_bufferR[readOffset] * feedback) + rightIn; + } else + { + m_bufferL[writeOffset] = (m_bufferL[readOffset] * feedback) + (leftIn + rightIn) * 0.5f; + } + + float waveMin; + float waveMax; + if(isTriangle) + { + m_waveShapeMin += m_waveShapeVal; + m_waveShapeMax += m_waveShapeVal; + if(m_waveShapeMin > 1) + m_waveShapeMin -= 2; + if(m_waveShapeMax > 1) + m_waveShapeMax -= 2; + waveMin = std::abs(m_waveShapeMin) * 2 - 1; + waveMax = std::abs(m_waveShapeMax) * 2 - 1; + } else + { + m_waveShapeMin = m_waveShapeMax * m_waveShapeVal + m_waveShapeMin; + m_waveShapeMax = m_waveShapeMax - m_waveShapeMin * m_waveShapeVal; + waveMin = m_waveShapeMin; + waveMax = m_waveShapeMax; + } + + const float leftDelayIn = m_isFlanger ? m_DryBufferL[(m_dryWritePos + 2) % 3] : leftIn; + const float rightDelayIn = m_isFlanger ? m_DryBufferR[(m_dryWritePos + 2) % 3] : rightIn; + + float left1 = m_bufferL[GetBufferIntOffset(m_bufPos + m_delayL)]; + float left2 = m_bufferL[GetBufferIntOffset(m_bufPos + m_delayL + 4096)]; + float fracPos = (m_delayL & 0xFFF) * (1.0f / 4096.0f); + float leftOut = (left2 - left1) * fracPos + left1; + *(out[0])++ = leftDelayIn + (leftOut - leftDelayIn) * wetDryMix; + + float right1 = bufferR[GetBufferIntOffset(m_bufPos + m_delayR)]; + float right2 = bufferR[GetBufferIntOffset(m_bufPos + m_delayR + 4096)]; + fracPos = (m_delayR & 0xFFF) * (1.0f / 4096.0f); + float rightOut = (right2 - right1) * fracPos + right1; + *(out[1])++ = rightDelayIn + (rightOut - rightDelayIn) * wetDryMix; + + // Increment delay positions + if(m_dryWritePos <= 0) + m_dryWritePos += 3; + m_dryWritePos--; + + m_delayL = m_delayOffset + (phase < 4 ? 1 : -1) * static_cast<int32>(waveMin * m_depthDelay); + m_delayR = m_delayOffset + (phase < 2 ? -1 : 1) * static_cast<int32>(((phase % 2u) ? waveMax : waveMin) * m_depthDelay); + + if(m_bufPos <= 0) + m_bufPos += m_bufSize * 4096; + m_bufPos -= 4096; + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue Chorus::GetParameter(PlugParamIndex index) +{ + if(index < kChorusNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void Chorus::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kChorusNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + if(index == kChorusWaveShape) + { + value = mpt::round(value); + if(m_param[index] != value) + { + m_waveShapeMin = 0.0f; + m_waveShapeMax = 0.5f + value * 0.5f; + } + } else if(index == kChorusPhase) + { + value = mpt::round(value * 4.0f) / 4.0f; + } + m_param[index] = value; + RecalculateChorusParams(); + } +} + + +void Chorus::Resume() +{ + PositionChanged(); + RecalculateChorusParams(); + + m_isResumed = true; + m_waveShapeMin = 0.0f; + m_waveShapeMax = IsTriangle() ? 0.5f : 1.0f; + m_delayL = m_delayR = m_delayOffset; + m_bufPos = 0; + m_dryWritePos = 0; +} + + +void Chorus::PositionChanged() +{ + m_bufSize = Util::muldiv(m_SndFile.GetSampleRate(), 3840, 1000); + try + { + m_bufferL.assign(m_bufSize, 0.0f); + if(m_isFlanger) + m_bufferR.assign(m_bufSize, 0.0f); + m_DryBufferL.fill(0.0f); + m_DryBufferR.fill(0.0f); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_bufSize = 0; + } +} + + +#ifdef MODPLUG_TRACKER + +CString Chorus::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kChorusWetDryMix: return _T("WetDryMix"); + case kChorusDepth: return _T("Depth"); + case kChorusFrequency: return _T("Frequency"); + case kChorusWaveShape: return _T("WaveShape"); + case kChorusPhase: return _T("Phase"); + case kChorusFeedback: return _T("Feedback"); + case kChorusDelay: return _T("Delay"); + } + return CString(); +} + + +CString Chorus::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kChorusWetDryMix: + case kChorusDepth: + case kChorusFeedback: + return _T("%"); + case kChorusFrequency: + return _T("Hz"); + case kChorusPhase: + return mpt::ToCString(MPT_UTF8("\xC2\xB0")); // U+00B0 DEGREE SIGN + case kChorusDelay: + return _T("ms"); + } + return CString(); +} + + +CString Chorus::GetParamDisplay(PlugParamIndex param) +{ + CString s; + float value = m_param[param]; + switch(param) + { + case kChorusWetDryMix: + case kChorusDepth: + value *= 100.0f; + break; + case kChorusFrequency: + value = FrequencyInHertz(); + break; + case kChorusWaveShape: + return (value < 1) ? _T("Triangle") : _T("Sine"); + break; + case kChorusPhase: + switch(Phase()) + { + case 0: return _T("-180"); + case 1: return _T("-90"); + case 2: return _T("0"); + case 3: return _T("90"); + case 4: return _T("180"); + } + break; + case kChorusFeedback: + value = Feedback(); + break; + case kChorusDelay: + value = Delay(); + } + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + + +void Chorus::RecalculateChorusParams() +{ + const float sampleRate = static_cast<float>(m_SndFile.GetSampleRate()); + + float delaySamples = Delay() * sampleRate / 1000.0f; + m_depthDelay = Depth() * delaySamples * 2048.0f; + m_delayOffset = mpt::saturate_round<int32>(4096.0f * (delaySamples + 2.0f)); + m_frequency = FrequencyInHertz(); + const float frequencySamples = m_frequency / sampleRate; + if(IsTriangle()) + m_waveShapeVal = frequencySamples * 2.0f; + else + m_waveShapeVal = std::sin(frequencySamples * mpt::numbers::pi_v<float>) * 2.0f; +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Chorus) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.h new file mode 100644 index 00000000..62c1db6d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Chorus.h @@ -0,0 +1,122 @@ +/* + * Chorus.h + * -------- + * Purpose: Implementation of the DMO Chorus DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Chorus : public IMixPlugin +{ +protected: + enum Parameters + { + kChorusWetDryMix = 0, + kChorusDepth, + kChorusFrequency, + kChorusWaveShape, + kChorusPhase, + kChorusFeedback, + kChorusDelay, + kChorusNumParameters + }; + + std::array<float, kChorusNumParameters> m_param; + + // Calculated parameters + float m_waveShapeMin, m_waveShapeMax, m_waveShapeVal; + float m_depthDelay; + float m_frequency; + int32 m_delayOffset; + const bool m_isFlanger = false; + + // State + std::vector<float> m_bufferL, m_bufferR; // Only m_bufferL is used in case of !m_isFlanger + std::array<float, 3> m_DryBufferL, m_DryBufferR; + int32 m_bufPos = 0, m_bufSize = 0; + + int32 m_delayL = 0, m_delayR = 0; + int32 m_dryWritePos = 0; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + Chorus(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, bool stereoBuffers = false); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEFE6629C; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kChorusNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Chorus"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + void BeginSetProgram(int32) override { } + void EndSetProgram() override { } + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + int32 GetBufferIntOffset(int32 fpOffset) const; + + virtual float WetDryMix() const { return m_param[kChorusWetDryMix]; } + virtual bool IsTriangle() const { return m_param[kChorusWaveShape] < 1; } + virtual float Depth() const { return m_param[kChorusDepth]; } + virtual float Feedback() const { return -99.0f + m_param[kChorusFeedback] * 198.0f; } + virtual float Delay() const { return m_param[kChorusDelay] * 20.0f; } + virtual float FrequencyInHertz() const { return m_param[kChorusFrequency] * 10.0f; } + virtual int Phase() const { return mpt::saturate_round<uint32>(m_param[kChorusPhase] * 4.0f); } + void RecalculateChorusParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.cpp new file mode 100644 index 00000000..97d5cbcf --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.cpp @@ -0,0 +1,238 @@ +/* + * Compressor.cpp + * --------------- + * Purpose: Implementation of the DMO Compressor DSP (for non-Windows platforms) + * Notes : The original plugin's integer and floating point code paths only + * behave identically when feeding floating point numbers in range + * [-32768, +32768] rather than the usual [-1, +1] into the plugin. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Compressor.h" +#include "DMOUtils.h" +#include "mpt/base/numbers.hpp" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + + +IMixPlugin* Compressor::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Compressor(factory, sndFile, mixStruct); +} + + +Compressor::Compressor(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) +{ + m_param[kCompGain] = 0.5f; + m_param[kCompAttack] = 0.02f; + m_param[kCompRelease] = 150.0f / 2950.0f; + m_param[kCompThreshold] = 2.0f / 3.0f; + m_param[kCompRatio] = 0.02f; + m_param[kCompPredelay] = 1.0f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void Compressor::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_bufSize || !m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + for(uint32 i = numFrames; i != 0; i--) + { + float leftIn = *(in[0])++; + float rightIn = *(in[1])++; + + m_buffer[m_bufPos * 2] = leftIn; + m_buffer[m_bufPos * 2 + 1] = rightIn; + + leftIn = std::abs(leftIn); + rightIn = std::abs(rightIn); + + float mono = (leftIn + rightIn) * (0.5f * 32768.0f * 32768.0f); + float monoLog = std::abs(logGain(mono, 31, 5)) * (1.0f / float(1u << 31)); + + float newPeak = monoLog + (m_peak - monoLog) * ((m_peak <= monoLog) ? m_attack : m_release); + m_peak = newPeak; + + if(newPeak < m_threshold) + newPeak = m_threshold; + + float compGain = (m_threshold - newPeak) * m_ratio + 0.9999999f; + + // Computes 2 ^ (2 ^ (log2(x) - 26) - 1) (x = 0...2^31) + uint32 compGainInt = static_cast<uint32>(compGain * 2147483648.0f); + uint32 compGainPow = compGainInt << 5; + compGainInt >>= 26; + if(compGainInt) // compGainInt >= 2^26 + { + compGainPow |= 0x80000000u; + compGainInt--; + } + compGainPow >>= (31 - compGainInt); + + int32 readOffset = m_predelay + m_bufPos * 4096 + m_bufSize - 1; + readOffset /= 4096; + readOffset %= m_bufSize; + + float outGain = (compGainPow * (1.0f / 2147483648.0f)) * m_gain; + *(out[0])++ = m_buffer[readOffset * 2] * outGain; + *(out[1])++ = m_buffer[readOffset * 2 + 1] * outGain; + + if(m_bufPos-- == 0) + m_bufPos += m_bufSize; + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue Compressor::GetParameter(PlugParamIndex index) +{ + if(index < kCompNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void Compressor::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kCompNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + m_param[index] = value; + RecalculateCompressorParams(); + } +} + + +void Compressor::Resume() +{ + m_isResumed = true; + PositionChanged(); + RecalculateCompressorParams(); +} + + +void Compressor::PositionChanged() +{ + m_bufSize = Util::muldiv(m_SndFile.GetSampleRate(), 200, 1000); + try + { + m_buffer.assign(m_bufSize * 2, 0.0f); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_bufSize = 0; + } + m_bufPos = 0; + m_peak = 0.0f; +} + + +#ifdef MODPLUG_TRACKER + +CString Compressor::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kCompGain: return _T("Gain"); + case kCompAttack: return _T("Attack"); + case kCompRelease: return _T("Release"); + case kCompThreshold: return _T("Threshold"); + case kCompRatio: return _T("Ratio"); + case kCompPredelay: return _T("Predelay"); + } + return CString(); +} + + +CString Compressor::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kCompGain: + case kCompThreshold: + return _T("dB"); + case kCompAttack: + case kCompRelease: + case kCompPredelay: + return _T("ms"); + } + return CString(); +} + + +CString Compressor::GetParamDisplay(PlugParamIndex param) +{ + float value = m_param[param]; + switch(param) + { + case kCompGain: + value = GainInDecibel(); + break; + case kCompAttack: + value = AttackTime(); + break; + case kCompRelease: + value = ReleaseTime(); + break; + case kCompThreshold: + value = ThresholdInDecibel(); + break; + case kCompRatio: + value = CompressorRatio(); + break; + case kCompPredelay: + value = PreDelay(); + break; + } + CString s; + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + + +void Compressor::RecalculateCompressorParams() +{ + const float sampleRate = m_SndFile.GetSampleRate() / 1000.0f; + m_gain = std::pow(10.0f, GainInDecibel() / 20.0f); + m_attack = std::pow(10.0f, -1.0f / (AttackTime() * sampleRate)); + m_release = std::pow(10.0f, -1.0f / (ReleaseTime() * sampleRate)); + const float _2e31 = float(1u << 31); + const float _2e26 = float(1u << 26); + m_threshold = std::min((_2e31 - 1.0f), (std::log(std::pow(10.0f, ThresholdInDecibel() / 20.0f) * _2e31) * _2e26) / mpt::numbers::ln2_v<float> + _2e26) * (1.0f / _2e31); + m_ratio = 1.0f - (1.0f / CompressorRatio()); + m_predelay = static_cast<int32>((PreDelay() * sampleRate) + 2.0f); +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Compressor) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.h new file mode 100644 index 00000000..7fc60f5a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Compressor.h @@ -0,0 +1,109 @@ +/* + * Compressor.h + * ------------- + * Purpose: Implementation of the DMO Compressor DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Compressor final : public IMixPlugin +{ +protected: + enum Parameters + { + kCompGain = 0, + kCompAttack, + kCompRelease, + kCompThreshold, + kCompRatio, + kCompPredelay, + kCompNumParameters + }; + + std::array<float, kCompNumParameters> m_param; + + // Calculated parameters and coefficients + float m_gain; + float m_attack; + float m_release; + float m_threshold; + float m_ratio; + int32 m_predelay; + + // State + std::vector<float> m_buffer; + int32 m_bufPos, m_bufSize; + float m_peak; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + Compressor(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEF011F79; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kCompNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Compressor"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + float GainInDecibel() const { return -60.0f + m_param[kCompGain] * 120.0f; } + float AttackTime() const { return 0.01f + m_param[kCompAttack] * 499.99f; } + float ReleaseTime() const { return 50.0f + m_param[kCompRelease] * 2950.0f; } + float ThresholdInDecibel() const { return -60.0f + m_param[kCompThreshold] * 60.0f; } + float CompressorRatio() const { return 1.0f + m_param[kCompRatio] * 99.0f; } + float PreDelay() const { return m_param[kCompPredelay] * 4.0f; } + void RecalculateCompressorParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.cpp new file mode 100644 index 00000000..bb7df181 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.cpp @@ -0,0 +1,431 @@ +/* + * DMOPlugin.h + * ----------- + * Purpose: DirectX Media Object plugin handling / processing. + * Notes : Some default plugins only have the same output characteristics in the floating point code path (compared to integer PCM) + * if we feed them input in the range [-32768, +32768] rather than the more usual [-1, +1]. + * Hence, OpenMPT uses this range for both the floating-point and integer path. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "mpt/base/aligned_array.hpp" +#if defined(MPT_WITH_DMO) +#include "mpt/uuid/guid.hpp" +#include "../../Sndfile.h" +#include "DMOPlugin.h" +#include "../PluginManager.h" +#include <uuids.h> +#include <medparam.h> +#include <mmsystem.h> +#endif // MPT_WITH_DMO + +OPENMPT_NAMESPACE_BEGIN + + +#if defined(MPT_WITH_DMO) + + +#ifdef MPT_ALL_LOGGING +#define DMO_LOG +#else +#define DMO_LOG +#endif + + +IMixPlugin* DMOPlugin::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + CLSID clsid; + if(mpt::VerifyStringToCLSID(factory.dllPath.AsNative(), clsid)) + { + IMediaObject *pMO = nullptr; + IMediaObjectInPlace *pMOIP = nullptr; + if ((CoCreateInstance(clsid, nullptr, CLSCTX_INPROC_SERVER, IID_IMediaObject, (VOID **)&pMO) == S_OK) && (pMO)) + { + if (pMO->QueryInterface(IID_IMediaObjectInPlace, (void **)&pMOIP) != S_OK) pMOIP = nullptr; + } else pMO = nullptr; + if ((pMO) && (pMOIP)) + { + DWORD dwInputs = 0, dwOutputs = 0; + pMO->GetStreamCount(&dwInputs, &dwOutputs); + if (dwInputs == 1 && dwOutputs == 1) + { + DMOPlugin *p = new (std::nothrow) DMOPlugin(factory, sndFile, mixStruct, pMO, pMOIP, clsid.Data1); + return p; + } +#ifdef DMO_LOG + MPT_LOG_GLOBAL(LogDebug, "DMO", factory.libraryName.ToUnicode() + U_(": Unable to use this DMO")); +#endif + } +#ifdef DMO_LOG + else MPT_LOG_GLOBAL(LogDebug, "DMO", factory.libraryName.ToUnicode() + U_(": Failed to get IMediaObject & IMediaObjectInPlace interfaces")); +#endif + if (pMO) pMO->Release(); + if (pMOIP) pMOIP->Release(); + } + return nullptr; +} + + +DMOPlugin::DMOPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, IMediaObject *pMO, IMediaObjectInPlace *pMOIP, uint32 uid) + : IMixPlugin(factory, sndFile, mixStruct) + , m_pMediaObject(pMO) + , m_pMediaProcess(pMOIP) + , m_pParamInfo(nullptr) + , m_pMediaParams(nullptr) + , m_nSamplesPerSec(sndFile.GetSampleRate()) + , m_uid(uid) +{ + if(FAILED(m_pMediaObject->QueryInterface(IID_IMediaParamInfo, (void **)&m_pParamInfo))) + m_pParamInfo = nullptr; + if (FAILED(m_pMediaObject->QueryInterface(IID_IMediaParams, (void **)&m_pMediaParams))) + m_pMediaParams = nullptr; + m_alignedBuffer.f32 = mpt::align_bytes<16, MIXBUFFERSIZE * 2>(m_interleavedBuffer.f32); + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +DMOPlugin::~DMOPlugin() +{ + if(m_pMediaParams) + { + m_pMediaParams->Release(); + m_pMediaParams = nullptr; + } + if(m_pParamInfo) + { + m_pParamInfo->Release(); + m_pParamInfo = nullptr; + } + if(m_pMediaProcess) + { + m_pMediaProcess->Release(); + m_pMediaProcess = nullptr; + } + if(m_pMediaObject) + { + m_pMediaObject->Release(); + m_pMediaObject = nullptr; + } +} + + +uint32 DMOPlugin::GetLatency() const +{ + REFERENCE_TIME time; // Unit 100-nanoseconds + if(m_pMediaProcess->GetLatency(&time) == S_OK) + { + return static_cast<uint32>(time * m_nSamplesPerSec / (10 * 1000 * 1000)); + } + return 0; +} + + +static constexpr float _f2si = 32768.0f; +static constexpr float _si2f = 1.0f / 32768.0f; + + +static void InterleaveStereo(const float * MPT_RESTRICT inputL, const float * MPT_RESTRICT inputR, float * MPT_RESTRICT output, uint32 numFrames) +{ + while(numFrames--) + { + *(output++) = *(inputL++) * _f2si; + *(output++) = *(inputR++) * _f2si; + } +} + + +static void DeinterleaveStereo(const float * MPT_RESTRICT input, float * MPT_RESTRICT outputL, float * MPT_RESTRICT outputR, uint32 numFrames) +{ + while(numFrames--) + { + *(outputL++) = *(input++) * _si2f; + *(outputR++) = *(input++) * _si2f; + } +} + + +// Interleave two float streams into one int16 stereo stream. +static void InterleaveFloatToInt16(const float * MPT_RESTRICT inputL, const float * MPT_RESTRICT inputR, int16 * MPT_RESTRICT output, uint32 numFrames) +{ + while(numFrames--) + { + *(output++) = static_cast<int16>(Clamp(*(inputL++) * _f2si, static_cast<float>(int16_min), static_cast<float>(int16_max))); + *(output++) = static_cast<int16>(Clamp(*(inputR++) * _f2si, static_cast<float>(int16_min), static_cast<float>(int16_max))); + } +} + + +// Deinterleave an int16 stereo stream into two float streams. +static void DeinterleaveInt16ToFloat(const int16 * MPT_RESTRICT input, float * MPT_RESTRICT outputL, float * MPT_RESTRICT outputR, uint32 numFrames) +{ + while(numFrames--) + { + *outputL++ += _si2f * static_cast<float>(*input++); + *outputR++ += _si2f * static_cast<float>(*input++); + } +} + + +void DMOPlugin::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!numFrames || !m_mixBuffer.Ok()) + return; + m_mixBuffer.ClearOutputBuffers(numFrames); + REFERENCE_TIME startTime = Util::muldiv(m_SndFile.GetTotalSampleCount(), 10000000, m_nSamplesPerSec); + + if(m_useFloat) + { + InterleaveStereo(m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1), m_alignedBuffer.f32, numFrames); + m_pMediaProcess->Process(numFrames * 2 * sizeof(float), reinterpret_cast<BYTE *>(m_alignedBuffer.f32), startTime, DMO_INPLACE_NORMAL); + DeinterleaveStereo(m_alignedBuffer.f32, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); + } else + { + InterleaveFloatToInt16(m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1), m_alignedBuffer.i16, numFrames); + m_pMediaProcess->Process(numFrames * 2 * sizeof(int16), reinterpret_cast<BYTE *>(m_alignedBuffer.i16), startTime, DMO_INPLACE_NORMAL); + DeinterleaveInt16ToFloat(m_alignedBuffer.i16, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamIndex DMOPlugin::GetNumParameters() const +{ + DWORD dwParamCount = 0; + m_pParamInfo->GetParamCount(&dwParamCount); + return dwParamCount; +} + + +PlugParamValue DMOPlugin::GetParameter(PlugParamIndex index) +{ + if(index < GetNumParameters() && m_pParamInfo != nullptr && m_pMediaParams != nullptr) + { + MP_PARAMINFO mpi; + MP_DATA md; + + MemsetZero(mpi); + md = 0; + if (m_pParamInfo->GetParamInfo(index, &mpi) == S_OK + && m_pMediaParams->GetParam(index, &md) == S_OK) + { + float fValue, fMin, fMax, fDefault; + + fValue = md; + fMin = mpi.mpdMinValue; + fMax = mpi.mpdMaxValue; + fDefault = mpi.mpdNeutralValue; + if (mpi.mpType == MPT_BOOL) + { + fMin = 0; + fMax = 1; + } + fValue -= fMin; + if (fMax > fMin) fValue /= (fMax - fMin); + return fValue; + } + } + return 0; +} + + +void DMOPlugin::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < GetNumParameters() && m_pParamInfo != nullptr && m_pMediaParams != nullptr) + { + MP_PARAMINFO mpi; + MemsetZero(mpi); + if (m_pParamInfo->GetParamInfo(index, &mpi) == S_OK) + { + float fMin = mpi.mpdMinValue; + float fMax = mpi.mpdMaxValue; + + if (mpi.mpType == MPT_BOOL) + { + fMin = 0; + fMax = 1; + value = (value > 0.5f) ? 1.0f : 0.0f; + } + if (fMax > fMin) value *= (fMax - fMin); + value += fMin; + value = mpt::safe_clamp(value, fMin, fMax); + if (mpi.mpType != MPT_FLOAT) value = mpt::round(value); + m_pMediaParams->SetParam(index, value); + } + } +} + + +void DMOPlugin::Resume() +{ + m_nSamplesPerSec = m_SndFile.GetSampleRate(); + m_isResumed = true; + + DMO_MEDIA_TYPE mt; + WAVEFORMATEX wfx; + + mt.majortype = MEDIATYPE_Audio; + mt.subtype = MEDIASUBTYPE_PCM; + mt.bFixedSizeSamples = TRUE; + mt.bTemporalCompression = FALSE; + mt.formattype = FORMAT_WaveFormatEx; + mt.pUnk = nullptr; + mt.pbFormat = (LPBYTE)&wfx; + mt.cbFormat = sizeof(WAVEFORMATEX); + mt.lSampleSize = 2 * sizeof(float); + wfx.wFormatTag = 3; // WAVE_FORMAT_IEEE_FLOAT; + wfx.nChannels = 2; + wfx.nSamplesPerSec = m_nSamplesPerSec; + wfx.wBitsPerSample = sizeof(float) * 8; + wfx.nBlockAlign = wfx.nChannels * (wfx.wBitsPerSample / 8); + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + wfx.cbSize = 0; + + // First try 32-bit float (DirectX 9+) + m_useFloat = true; + if(FAILED(m_pMediaObject->SetInputType(0, &mt, 0)) + || FAILED(m_pMediaObject->SetOutputType(0, &mt, 0))) + { + m_useFloat = false; + // Try again with 16-bit PCM + mt.lSampleSize = 2 * sizeof(int16); + wfx.wFormatTag = WAVE_FORMAT_PCM; + wfx.wBitsPerSample = sizeof(int16) * 8; + wfx.nBlockAlign = wfx.nChannels * (wfx.wBitsPerSample / 8); + wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign; + if(FAILED(m_pMediaObject->SetInputType(0, &mt, 0)) + || FAILED(m_pMediaObject->SetOutputType(0, &mt, 0))) + { +#ifdef DMO_LOG + MPT_LOG_GLOBAL(LogDebug, "DMO", U_("DMO: Failed to set I/O media type")); +#endif + } + } +} + + +void DMOPlugin::PositionChanged() +{ + m_pMediaObject->Discontinuity(0); + m_pMediaObject->Flush(); +} + + +void DMOPlugin::Suspend() +{ + m_isResumed = false; + m_pMediaObject->Flush(); + m_pMediaObject->SetInputType(0, nullptr, DMO_SET_TYPEF_CLEAR); + m_pMediaObject->SetOutputType(0, nullptr, DMO_SET_TYPEF_CLEAR); +} + + +#ifdef MODPLUG_TRACKER + +CString DMOPlugin::GetParamName(PlugParamIndex param) +{ + if(param < GetNumParameters() && m_pParamInfo != nullptr) + { + MP_PARAMINFO mpi; + mpi.mpType = MPT_INT; + mpi.szUnitText[0] = 0; + mpi.szLabel[0] = 0; + if(m_pParamInfo->GetParamInfo(param, &mpi) == S_OK) + { + return mpt::ToCString(mpi.szLabel); + } + } + return CString(); + +} + + +CString DMOPlugin::GetParamLabel(PlugParamIndex param) +{ + if(param < GetNumParameters() && m_pParamInfo != nullptr) + { + MP_PARAMINFO mpi; + mpi.mpType = MPT_INT; + mpi.szUnitText[0] = 0; + mpi.szLabel[0] = 0; + if(m_pParamInfo->GetParamInfo(param, &mpi) == S_OK) + { + return mpt::ToCString(mpi.szUnitText); + } + } + return CString(); +} + + +CString DMOPlugin::GetParamDisplay(PlugParamIndex param) +{ + if(param < GetNumParameters() && m_pParamInfo != nullptr && m_pMediaParams != nullptr) + { + MP_PARAMINFO mpi; + mpi.mpType = MPT_INT; + mpi.szUnitText[0] = 0; + mpi.szLabel[0] = 0; + if (m_pParamInfo->GetParamInfo(param, &mpi) == S_OK) + { + MP_DATA md; + if(m_pMediaParams->GetParam(param, &md) == S_OK) + { + switch(mpi.mpType) + { + case MPT_FLOAT: + { + CString s; + s.Format(_T("%.2f"), md); + return s; + } + break; + + case MPT_BOOL: + return ((int)md) ? _T("Yes") : _T("No"); + break; + + case MPT_ENUM: + { + WCHAR *text = nullptr; + m_pParamInfo->GetParamText(param, &text); + + const int nValue = mpt::saturate_round<int>(md * (mpi.mpdMaxValue - mpi.mpdMinValue)); + // Always skip first two strings (param name, unit name) + for(int i = 0; i < nValue + 2; i++) + { + text += wcslen(text) + 1; + } + return mpt::ToCString(text); + } + break; + + case MPT_INT: + default: + { + CString s; + s.Format(_T("%d"), mpt::saturate_round<int>(md)); + return s; + } + break; + } + } + } + } + return CString(); +} + +#endif // MODPLUG_TRACKER + +#else // !MPT_WITH_DMO + +MPT_MSVC_WORKAROUND_LNK4221(DMOPlugin) + +#endif // MPT_WITH_DMO + +OPENMPT_NAMESPACE_END + diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.h new file mode 100644 index 00000000..9dabc877 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOPlugin.h @@ -0,0 +1,100 @@ +/* + * DMOPlugin.h + * ----------- + * Purpose: DirectX Media Object plugin handling / processing. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#if defined(MPT_WITH_DMO) + +#include "../PlugInterface.h" +#include <dmoreg.h> +#include <strmif.h> + +typedef interface IMediaObject IMediaObject; +typedef interface IMediaObjectInPlace IMediaObjectInPlace; +typedef interface IMediaParamInfo IMediaParamInfo; +typedef interface IMediaParams IMediaParams; + +OPENMPT_NAMESPACE_BEGIN + +class DMOPlugin final : public IMixPlugin +{ +protected: + IMediaObject *m_pMediaObject; + IMediaObjectInPlace *m_pMediaProcess; + IMediaParamInfo *m_pParamInfo; + IMediaParams *m_pMediaParams; + + uint32 m_nSamplesPerSec; + const uint32 m_uid; + union + { + int16 *i16; + float *f32; + } m_alignedBuffer; + union + { + int16 i16[MIXBUFFERSIZE * 2 + 16]; // 16-bit PCM Stereo interleaved + float f32[MIXBUFFERSIZE * 2 + 16]; // 32-bit Float Stereo interleaved + } m_interleavedBuffer; + bool m_useFloat; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + +protected: + DMOPlugin(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, IMediaObject *pMO, IMediaObjectInPlace *pMOIP, uint32 uid); + ~DMOPlugin(); + +public: + void Release() override { delete this; } + int32 GetUID() const override { return m_uid; } + int32 GetVersion() const override { return 2; } + void Idle() override { } + uint32 GetLatency() const override; + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32 /*nIndex*/) override { } + + PlugParamIndex GetNumParameters() const override; + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override; + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return CString(); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex param) override; + CString GetParamDisplay(PlugParamIndex param) override; + + // TODO we could simply add our own preset mechanism. But is it really useful for these plugins? + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } +}; + +OPENMPT_NAMESPACE_END + +#endif // MPT_WITH_DMO + diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.cpp new file mode 100644 index 00000000..2ad32f6b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.cpp @@ -0,0 +1,59 @@ +/* + * DMOUtils.cpp + * ------------ + * Purpose: Utility functions shared by DMO plugins + * Notes : none + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "DMOUtils.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +// Computes (log2(x) + 1) * 2 ^ (shiftL - shiftR) (x = -2^31...2^31) +float logGain(float x, int32 shiftL, int32 shiftR) +{ + uint32 intSample = static_cast<uint32>(static_cast<int64>(x)); + const uint32 sign = intSample & 0x80000000; + if(sign) + intSample = (~intSample) + 1; + + // Multiply until overflow (or edge shift factor is reached) + while(shiftL > 0 && intSample < 0x80000000) + { + intSample += intSample; + shiftL--; + } + // Unsign clipped sample + if(intSample >= 0x80000000) + { + intSample &= 0x7FFFFFFF; + shiftL++; + } + intSample = (shiftL << (31 - shiftR)) | (intSample >> shiftR); + if(sign) + intSample = ~intSample | sign; + return static_cast<float>(static_cast<int32>(intSample)); +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Distortion) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.h new file mode 100644 index 00000000..2e17c007 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/DMOUtils.h @@ -0,0 +1,26 @@ +/* + * DMOUtils.h + * ---------- + * Purpose: Utility functions shared by DMO plugins + * Notes : none + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +// Computes (log2(x) + 1) * 2 ^ (shiftL - shiftR) (x = -2^31...2^31) +float logGain(float x, int32 shiftL, int32 shiftR); + +} + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END + diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.cpp new file mode 100644 index 00000000..cce38508 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.cpp @@ -0,0 +1,216 @@ +/* + * Distortion.cpp + * -------------- + * Purpose: Implementation of the DMO Distortion DSP (for non-Windows platforms) + * Notes : The original plugin's integer and floating point code paths only + * behave identically when feeding floating point numbers in range + * [-32768, +32768] rather than the usual [-1, +1] into the plugin. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Distortion.h" +#include "DMOUtils.h" +#include "mpt/base/numbers.hpp" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* Distortion::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Distortion(factory, sndFile, mixStruct); +} + + +Distortion::Distortion(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) +{ + m_param[kDistGain] = 0.7f; + m_param[kDistEdge] = 0.15f; + m_param[kDistPreLowpassCutoff] = 1.0f; + m_param[kDistPostEQCenterFrequency] = 0.291f; + m_param[kDistPostEQBandwidth] = 0.291f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void Distortion::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + for(uint32 i = numFrames; i != 0; i--) + { + for(uint8 channel = 0; channel < 2; channel++) + { + float x = *(in[channel])++; + + // Pre EQ + float z = x * m_preEQa0 + m_preEQz1[channel] * m_preEQb1; + m_preEQz1[channel] = z; + + z *= 1073741824.0f; // 32768^2 + + // The actual distortion + z = logGain(z, m_edge, m_shift); + + // Post EQ / Gain + z = (z * m_postEQa0) - m_postEQz1[channel] * m_postEQb1 - m_postEQz2[channel] * m_postEQb0; + m_postEQz1[channel] = z * m_postEQb0 + m_postEQz2[channel]; + m_postEQz2[channel] = z; + + z *= (1.0f / 1073741824.0f); // 32768^2 + *(out[channel])++ = z; + } + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue Distortion::GetParameter(PlugParamIndex index) +{ + if(index < kDistNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void Distortion::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kDistNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + m_param[index] = value; + RecalculateDistortionParams(); + } +} + + +void Distortion::Resume() +{ + m_isResumed = true; + RecalculateDistortionParams(); + PositionChanged(); +} + + +void Distortion::PositionChanged() +{ + // Reset filter state + m_preEQz1[0] = m_preEQz1[1] = 0; + m_postEQz1[0] = m_postEQz2[0] = 0; + m_postEQz1[1] = m_postEQz2[1] = 0; +} + + +#ifdef MODPLUG_TRACKER + +CString Distortion::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kDistGain: return _T("Gain"); + case kDistEdge: return _T("Edge"); + case kDistPreLowpassCutoff: return _T("PreLowpassCutoff"); + case kDistPostEQCenterFrequency: return _T("PostEQCenterFrequency"); + case kDistPostEQBandwidth: return _T("PostEQBandwidth"); + } + return CString(); +} + + +CString Distortion::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kDistGain: + return _T("dB"); + case kDistPreLowpassCutoff: + case kDistPostEQCenterFrequency: + case kDistPostEQBandwidth: + return _T("Hz"); + } + return CString(); +} + + +CString Distortion::GetParamDisplay(PlugParamIndex param) +{ + float value = m_param[param]; + switch(param) + { + case kDistGain: + value = GainInDecibel(); + break; + case kDistEdge: + value *= 100.0f; + break; + case kDistPreLowpassCutoff: + case kDistPostEQCenterFrequency: + case kDistPostEQBandwidth: + value = FreqInHertz(value); + break; + } + CString s; + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + + +void Distortion::RecalculateDistortionParams() +{ + // Pre-EQ + m_preEQb1 = std::sqrt((2.0f * std::cos(2.0f * mpt::numbers::pi_v<float> * std::min(FreqInHertz(m_param[kDistPreLowpassCutoff]) / m_SndFile.GetSampleRate(), 0.5f)) + 3.0f) / 5.0f); + m_preEQa0 = std::sqrt(1.0f - m_preEQb1 * m_preEQb1); + + // Distortion + float edge = 2.0f + m_param[kDistEdge] * 29.0f; + m_edge = static_cast<uint8>(edge); // 2...31 shifted bits + m_shift = mpt::bit_width(m_edge); + + static constexpr float LogNorm[32] = + { + 1.00f, 1.00f, 1.50f, 1.00f, 1.75f, 1.40f, 1.17f, 1.00f, + 1.88f, 1.76f, 1.50f, 1.36f, 1.25f, 1.15f, 1.07f, 1.00f, + 1.94f, 1.82f, 1.72f, 1.63f, 1.55f, 1.48f, 1.41f, 1.35f, + 1.29f, 1.24f, 1.19f, 1.15f, 1.11f, 1.07f, 1.03f, 1.00f, + }; + + // Post-EQ + const float gain = std::pow(10.0f, GainInDecibel() / 20.0f); + const float postFreq = 2.0f * mpt::numbers::pi_v<float> * std::min(FreqInHertz(m_param[kDistPostEQCenterFrequency]) / m_SndFile.GetSampleRate(), 0.5f); + const float postBw = 2.0f * mpt::numbers::pi_v<float> * std::min(FreqInHertz(m_param[kDistPostEQBandwidth]) / m_SndFile.GetSampleRate(), 0.5f); + const float t = std::tan(5.0e-1f * postBw); + m_postEQb1 = ((1.0f - t) / (1.0f + t)); + m_postEQb0 = -std::cos(postFreq); + m_postEQa0 = gain * std::sqrt(1.0f - m_postEQb0 * m_postEQb0) * std::sqrt(1.0f - m_postEQb1 * m_postEQb1) * LogNorm[m_edge]; +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Distortion) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.h new file mode 100644 index 00000000..3c88e8e7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Distortion.h @@ -0,0 +1,97 @@ +/* + * Distortion.h + * ------------ + * Purpose: Implementation of the DMO Distortion DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Distortion final : public IMixPlugin +{ +protected: + enum Parameters + { + kDistGain = 0, + kDistEdge, + kDistPreLowpassCutoff, + kDistPostEQCenterFrequency, + kDistPostEQBandwidth, + kDistNumParameters + }; + + std::array<float, kDistNumParameters> m_param; + + // Pre-EQ coefficients + float m_preEQz1[2], m_preEQb1, m_preEQa0; + // Post-EQ coefficients + float m_postEQz1[2], m_postEQz2[2], m_postEQa0, m_postEQb0, m_postEQb1; + uint8 m_edge, m_shift; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + Distortion(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEF114C90; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kDistNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Distortion"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + static float FreqInHertz(float param) { return 100.0f + param * 7900.0f; } + float GainInDecibel() const { return -60.0f + m_param[kDistGain] * 60.0f; } + void RecalculateDistortionParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.cpp new file mode 100644 index 00000000..98251cca --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.cpp @@ -0,0 +1,207 @@ +/* + * Echo.cpp + * -------- + * Purpose: Implementation of the DMO Echo DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Echo.h" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* Echo::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Echo(factory, sndFile, mixStruct); +} + + +Echo::Echo(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_bufferSize(0) + , m_writePos(0) + , m_sampleRate(sndFile.GetSampleRate()) + , m_initialFeedback(0.0f) +{ + m_param[kEchoWetDry] = 0.5f; + m_param[kEchoFeedback] = 0.5f; + m_param[kEchoLeftDelay] = (500.0f - 1.0f) / 1999.0f; + m_param[kEchoRightDelay] = (500.0f - 1.0f) / 1999.0f; + m_param[kEchoPanDelay] = 0.0f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void Echo::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_bufferSize || !m_mixBuffer.Ok()) + return; + const float wetMix = m_param[kEchoWetDry], dryMix = 1 - wetMix; + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + for(uint32 i = numFrames; i != 0; i--) + { + for(uint8 channel = 0; channel < 2; channel++) + { + const uint8 readChannel = (m_crossEcho ? (1 - channel) : channel); + int readPos = m_writePos - m_delayTime[readChannel]; + if(readPos < 0) + readPos += m_bufferSize; + + float chnInput = *(in[channel])++; + float chnDelay = m_delayLine[readPos * 2 + readChannel]; + + // Calculate the delay + float chnOutput = chnInput * m_initialFeedback; + chnOutput += chnDelay * m_param[kEchoFeedback]; + + // Prevent denormals + if(std::abs(chnOutput) < 1e-24f) + chnOutput = 0.0f; + + m_delayLine[m_writePos * 2 + channel] = chnOutput; + // Output samples now + *(out[channel])++ = (chnInput * dryMix + chnDelay * wetMix); + } + m_writePos++; + if(m_writePos == m_bufferSize) + m_writePos = 0; + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue Echo::GetParameter(PlugParamIndex index) +{ + if(index < kEchoNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void Echo::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kEchoNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + if(index == kEchoPanDelay) + value = mpt::round(value); + m_param[index] = value; + RecalculateEchoParams(); + } +} + + +void Echo::Resume() +{ + m_isResumed = true; + m_sampleRate = m_SndFile.GetSampleRate(); + RecalculateEchoParams(); + PositionChanged(); +} + + +void Echo::PositionChanged() +{ + m_bufferSize = m_sampleRate * 2u; + try + { + m_delayLine.assign(m_bufferSize * 2, 0); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + m_bufferSize = 0; + } + m_writePos = 0; +} + + +#ifdef MODPLUG_TRACKER + +CString Echo::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kEchoWetDry: return _T("WetDryMix"); + case kEchoFeedback: return _T("Feedback"); + case kEchoLeftDelay: return _T("LeftDelay"); + case kEchoRightDelay: return _T("RightDelay"); + case kEchoPanDelay: return _T("PanDelay"); + } + return CString(); +} + + +CString Echo::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kEchoFeedback: + return _T("%"); + case kEchoLeftDelay: + case kEchoRightDelay: + return _T("ms"); + default: + return CString{}; + } +} + + +CString Echo::GetParamDisplay(PlugParamIndex param) +{ + CString s; + switch(param) + { + case kEchoWetDry: + s.Format(_T("%.1f : %.1f"), m_param[param] * 100.0f, 100.0f - m_param[param] * 100.0f); + break; + case kEchoFeedback: + s.Format(_T("%.2f"), m_param[param] * 100.0f); + break; + case kEchoLeftDelay: + case kEchoRightDelay: + s.Format(_T("%.2f"), 1.0f + m_param[param] * 1999.0f); + break; + case kEchoPanDelay: + s = (m_param[param] <= 0.5) ? _T("No") : _T("Yes"); + } + return s; +} + +#endif // MODPLUG_TRACKER + + +void Echo::RecalculateEchoParams() +{ + m_initialFeedback = std::sqrt(1.0f - (m_param[kEchoFeedback] * m_param[kEchoFeedback])); + m_delayTime[0] = static_cast<uint32>((1.0f + m_param[kEchoLeftDelay] * 1999.0f) / 1000.0f * m_sampleRate); + m_delayTime[1] = static_cast<uint32>((1.0f + m_param[kEchoRightDelay] * 1999.0f) / 1000.0f * m_sampleRate); + m_crossEcho = (m_param[kEchoPanDelay]) > 0.5f; +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Echo) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.h new file mode 100644 index 00000000..b56d6c95 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Echo.h @@ -0,0 +1,99 @@ +/* + * Echo.h + * ------ + * Purpose: Implementation of the DMO Echo DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Echo final : public IMixPlugin +{ +protected: + enum Parameters + { + kEchoWetDry = 0, + kEchoFeedback, + kEchoLeftDelay, + kEchoRightDelay, + kEchoPanDelay, + kEchoNumParameters + }; + + std::vector<float> m_delayLine; // Echo delay line + float m_param[kEchoNumParameters]; + uint32 m_bufferSize; // Delay line length in frames + uint32 m_writePos; // Current write position in the delay line + uint32 m_delayTime[2]; // In frames + uint32 m_sampleRate; + + // Echo calculation coefficients + float m_initialFeedback; + bool m_crossEcho; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + Echo(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEF3E932C; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames)override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kEchoNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Echo"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + void RecalculateEchoParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.cpp new file mode 100644 index 00000000..2da25110 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.cpp @@ -0,0 +1,158 @@ +/* + * Flanger.cpp + * ----------- + * Purpose: Implementation of the DMO Flanger DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Flanger.h" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* Flanger::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Flanger(factory, sndFile, mixStruct, false); +} + +IMixPlugin* Flanger::CreateLegacy(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new(std::nothrow) Flanger(factory, sndFile, mixStruct, true); +} + + +Flanger::Flanger(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, const bool legacy) + : Chorus(factory, sndFile, mixStruct, !legacy) +{ + m_param[kFlangerWetDryMix] = 0.5f; + m_param[kFlangerWaveShape] = 1.0f; + m_param[kFlangerFrequency] = 0.025f; + m_param[kFlangerDepth] = 1.0f; + m_param[kFlangerPhase] = 0.5f; + m_param[kFlangerFeedback] = (-50.0f + 99.0f) / 198.0f; + m_param[kFlangerDelay] = 0.5f; + + // Already done in Chorus constructor + //m_mixBuffer.Initialize(2, 2); + //InsertIntoFactoryList(); +} + + +void Flanger::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kFlangerNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + if(index == kFlangerWaveShape) + { + value = mpt::round(value); + if(m_param[index] != value) + { + m_waveShapeMin = 0.0f; + m_waveShapeMax = 0.5f + value * 0.5f; + } + } else if(index == kFlangerPhase) + { + value = mpt::round(value * 4.0f) / 4.0f; + } + m_param[index] = value; + RecalculateChorusParams(); + } +} + + +#ifdef MODPLUG_TRACKER + +CString Flanger::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kFlangerWetDryMix: return _T("WetDryMix"); + case kFlangerWaveShape: return _T("WaveShape"); + case kFlangerFrequency: return _T("Frequency"); + case kFlangerDepth: return _T("Depth"); + case kFlangerPhase: return _T("Phase"); + case kFlangerFeedback: return _T("Feedback"); + case kFlangerDelay: return _T("Delay"); + } + return CString(); +} + + +CString Flanger::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kFlangerWetDryMix: + case kFlangerDepth: + case kFlangerFeedback: + return _T("%"); + case kFlangerFrequency: + return _T("Hz"); + case kFlangerPhase: + return mpt::ToCString(MPT_UTF8("\xC2\xB0")); // U+00B0 DEGREE SIGN + case kFlangerDelay: + return _T("ms"); + } + return CString(); +} + + +CString Flanger::GetParamDisplay(PlugParamIndex param) +{ + CString s; + float value = m_param[param]; + switch(param) + { + case kFlangerWetDryMix: + case kFlangerDepth: + value *= 100.0f; + break; + case kFlangerFrequency: + value = FrequencyInHertz(); + break; + case kFlangerWaveShape: + return (value < 1) ? _T("Triangle") : _T("Sine"); + break; + case kFlangerPhase: + switch(Phase()) + { + case 0: return _T("-180"); + case 1: return _T("-90"); + case 2: return _T("0"); + case 3: return _T("90"); + case 4: return _T("180"); + } + break; + case kFlangerFeedback: + value = Feedback(); + break; + case kFlangerDelay: + value = Delay(); + } + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Flanger) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.h new file mode 100644 index 00000000..1383028a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Flanger.h @@ -0,0 +1,72 @@ +/* + * Flanger.h + * --------- + * Purpose: Implementation of the DMO Flanger DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "Chorus.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Flanger final : public Chorus +{ +protected: + enum Parameters + { + kFlangerWetDryMix = 0, + kFlangerWaveShape, + kFlangerFrequency, + kFlangerDepth, + kFlangerPhase, + kFlangerFeedback, + kFlangerDelay, + kFlangerNumParameters + }; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + static IMixPlugin* CreateLegacy(VSTPluginLib& factory, CSoundFile& sndFile, SNDMIXPLUGIN* mixStruct); + Flanger(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct, const bool legacy); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEFCA3D92; } + + PlugParamIndex GetNumParameters() const override { return kFlangerNumParameters; } + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Flanger"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; +#endif + +protected: + float WetDryMix() const override { return m_param[kFlangerWetDryMix]; } + bool IsTriangle() const override { return m_param[kFlangerWaveShape] < 1; } + float Depth() const override { return m_param[kFlangerDepth]; } + float Feedback() const override { return -99.0f + m_param[kFlangerFeedback] * 198.0f; } + float Delay() const override { return m_param[kFlangerDelay] * 4.0f; } + float FrequencyInHertz() const override { return m_param[kFlangerFrequency] * 10.0f; } + int Phase() const override { return mpt::saturate_round<uint32>(m_param[kFlangerPhase] * 4.0f); } +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.cpp new file mode 100644 index 00000000..91f5e145 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.cpp @@ -0,0 +1,202 @@ +/* + * Gargle.cpp + * ---------- + * Purpose: Implementation of the DMO Gargle DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "Gargle.h" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* Gargle::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) Gargle(factory, sndFile, mixStruct); +} + + +Gargle::Gargle(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) +{ + m_param[kGargleRate] = 0.02f; + m_param[kGargleWaveShape] = 0.0f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void Gargle::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_mixBuffer.Ok()) + return; + + const float *inL = m_mixBuffer.GetInputBuffer(0), *inR = m_mixBuffer.GetInputBuffer(1); + float *outL = m_mixBuffer.GetOutputBuffer(0), *outR = m_mixBuffer.GetOutputBuffer(1); + const bool triangle = m_param[kGargleWaveShape] < 1.0f; + + for(uint32 frame = numFrames; frame != 0;) + { + if(m_counter < m_periodHalf) + { + // First half of gargle period + const uint32 remain = std::min(frame, m_periodHalf - m_counter); + if(triangle) + { + const uint32 stop = m_counter + remain; + const float factor = 1.0f / m_periodHalf; + for(uint32 i = m_counter; i < stop; i++) + { + *outL++ = *inL++ * i * factor; + *outR++ = *inR++ * i * factor; + } + } else + { + for(uint32 i = 0; i < remain; i++) + { + *outL++ = *inL++; + *outR++ = *inR++; + } + } + frame -= remain; + m_counter += remain; + } else + { + // Second half of gargle period + const uint32 remain = std::min(frame, m_period - m_counter); + if(triangle) + { + const uint32 stop = m_period - m_counter - remain; + const float factor = 1.0f / m_periodHalf; + for(uint32 i = m_period - m_counter; i > stop; i--) + { + *outL++ = *inL++ * i * factor; + *outR++ = *inR++ * i * factor; + } + } else + { + for(uint32 i = 0; i < remain; i++) + { + *outL++ = 0; + *outR++ = 0; + } + inL += remain; + inR += remain; + + } + frame -= remain; + m_counter += remain; + if(m_counter >= m_period) m_counter = 0; + } + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue Gargle::GetParameter(PlugParamIndex index) +{ + if(index < kGargleNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void Gargle::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kGargleNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + if(index == kGargleWaveShape) + value = mpt::round(value); + m_param[index] = value; + RecalculateGargleParams(); + } +} + + +void Gargle::Resume() +{ + RecalculateGargleParams(); + m_counter = 0; + m_isResumed = true; +} + + +#ifdef MODPLUG_TRACKER + +CString Gargle::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kGargleRate: return _T("Rate"); + case kGargleWaveShape: return _T("WaveShape"); + } + return CString(); +} + + +CString Gargle::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kGargleRate: return _T("Hz"); + } + return CString(); +} + + +CString Gargle::GetParamDisplay(PlugParamIndex param) +{ + CString s; + switch(param) + { + case kGargleRate: + s.Format(_T("%u"), RateInHertz()); + break; + case kGargleWaveShape: + return (m_param[param] < 0.5) ? _T("Triangle") : _T("Square"); + } + return s; +} + +#endif // MODPLUG_TRACKER + + +uint32 Gargle::RateInHertz() const +{ + return static_cast<uint32>(mpt::round(std::clamp(m_param[kGargleRate], 0.0f, 1.0f) * 999.0f)) + 1; +} + + +void Gargle::RecalculateGargleParams() +{ + m_period = m_SndFile.GetSampleRate() / RateInHertz(); + if(m_period < 2) m_period = 2; + m_periodHalf = m_period / 2; + LimitMax(m_counter, m_period); +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(Gargle) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.h new file mode 100644 index 00000000..c4a721c5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/Gargle.h @@ -0,0 +1,90 @@ +/* + * Gargle.h + * -------- + * Purpose: Implementation of the DMO Gargle DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class Gargle final : public IMixPlugin +{ +protected: + enum Parameters + { + kGargleRate = 0, + kGargleWaveShape, + kGargleNumParameters + }; + + std::array<float, kGargleNumParameters> m_param; + + uint32 m_period, m_periodHalf, m_counter; // In frames + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + Gargle(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xDAFD8210; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kGargleNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override { m_counter = 0; } + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("Gargle"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + uint32 RateInHertz() const; + void RecalculateGargleParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.cpp new file mode 100644 index 00000000..63f5b9c1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.cpp @@ -0,0 +1,645 @@ +/* + * I3DL2Reverb.cpp + * --------------- + * Purpose: Implementation of the DMO I3DL2Reverb DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "I3DL2Reverb.h" +#ifdef MODPLUG_TRACKER +#include "../../../sounddsp/Reverb.h" +#endif // MODPLUG_TRACKER +#include "mpt/base/numbers.hpp" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +void I3DL2Reverb::DelayLine::Init(int32 ms, int32 padding, uint32 sampleRate, int32 delayTap) +{ + m_length = Util::muldiv(sampleRate, ms, 1000) + padding; + m_position = 0; + SetDelayTap(delayTap); + assign(m_length, 0.0f); +} + + +void I3DL2Reverb::DelayLine::SetDelayTap(int32 delayTap) +{ + if(m_length > 0) + m_delayPosition = (delayTap + m_position + m_length) % m_length; +} + + +void I3DL2Reverb::DelayLine::Advance() +{ + if(--m_position < 0) + m_position += m_length; + if(--m_delayPosition < 0) + m_delayPosition += m_length; +} + + +MPT_FORCEINLINE void I3DL2Reverb::DelayLine::Set(float value) +{ + at(m_position) = value; +} + + +float I3DL2Reverb::DelayLine::Get(int32 offset) const +{ + offset = (offset + m_position) % m_length; + if(offset < 0) + offset += m_length; + return at(offset); +} + + +MPT_FORCEINLINE float I3DL2Reverb::DelayLine::Get() const +{ + return at(m_delayPosition); +} + + +IMixPlugin* I3DL2Reverb::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) I3DL2Reverb(factory, sndFile, mixStruct); +} + + +I3DL2Reverb::I3DL2Reverb(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) +{ + m_param[kI3DL2ReverbRoom] = 0.9f; + m_param[kI3DL2ReverbRoomHF] = 0.99f; + m_param[kI3DL2ReverbRoomRolloffFactor] = 0.0f; + m_param[kI3DL2ReverbDecayTime] = 0.07f; + m_param[kI3DL2ReverbDecayHFRatio] = 0.3842105f; + m_param[kI3DL2ReverbReflections] = 0.672545433f; + m_param[kI3DL2ReverbReflectionsDelay] = 0.233333333f; + m_param[kI3DL2ReverbReverb] = 0.85f; + m_param[kI3DL2ReverbReverbDelay] = 0.11f; + m_param[kI3DL2ReverbDiffusion] = 1.0f; + m_param[kI3DL2ReverbDensity] = 1.0f; + m_param[kI3DL2ReverbHFReference] = (5000.0f - 20.0f) / 19980.0f; + m_param[kI3DL2ReverbQuality] = 2.0f / 3.0f; + + SetCurrentProgram(m_program); + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void I3DL2Reverb::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(m_recalcParams) + { + auto sampleRate = m_effectiveSampleRate; + RecalculateI3DL2ReverbParams(); + // Resize and clear delay lines if quality has changed + if(sampleRate != m_effectiveSampleRate) + PositionChanged(); + } + + if(!m_ok || !m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + uint32 frames = numFrames; + if(!(m_quality & kFullSampleRate) && m_remain && frames > 0) + { + // Remaining frame from previous render call + frames--; + *(out[0]++) = m_prevL; + *(out[1]++) = m_prevR; + in[0]++; + in[1]++; + m_remain = false; + } + + while(frames > 0) + { + // Apply room filter and insert into early reflection delay lines + const float inL = *(in[0]++); + const float inRoomL = (m_filterHist[12] - inL) * m_roomFilter + inL; + m_filterHist[12] = inRoomL; + m_delayLines[15].Set(inRoomL); + + const float inR = *(in[1]++); + const float inRoomR = (m_filterHist[13] - inR) * m_roomFilter + inR; + m_filterHist[13] = inRoomR; + m_delayLines[16].Set(inRoomR); + + // Early reflections (left) + float earlyL = m_delayLines[15].Get(m_earlyTaps[0][1]) * 0.68f + - m_delayLines[15].Get(m_earlyTaps[0][2]) * 0.5f + - m_delayLines[15].Get(m_earlyTaps[0][3]) * 0.62f + - m_delayLines[15].Get(m_earlyTaps[0][4]) * 0.5f + - m_delayLines[15].Get(m_earlyTaps[0][5]) * 0.62f; + if(m_quality & kMoreDelayLines) + { + float earlyL2 = earlyL; + earlyL = m_delayLines[13].Get() + earlyL * 0.618034f; + m_delayLines[13].Set(earlyL2 - earlyL * 0.618034f); + } + const float earlyRefOutL = earlyL * m_ERLevel; + m_filterHist[15] = m_delayLines[15].Get(m_earlyTaps[0][0]) + m_filterHist[15]; + m_filterHist[16] = m_delayLines[16].Get(m_earlyTaps[1][0]) + m_filterHist[16]; + + // Lots of slightly different copy-pasta ahead + float reverbL1, reverbL2, reverbL3, reverbR1, reverbR2, reverbR3; + + reverbL1 = -m_filterHist[15] * 0.707f; + reverbL2 = m_filterHist[16] * 0.707f + reverbL1; + reverbR2 = reverbL1 - m_filterHist[16] * 0.707f; + + m_filterHist[5] = (m_filterHist[5] - m_delayLines[5].Get()) * m_delayCoeffs[5][1] + m_delayLines[5].Get(); + reverbL1 = m_filterHist[5] * m_delayCoeffs[5][0] + reverbL2 * m_diffusion; + m_delayLines[5].Set(reverbL2 - reverbL1 * m_diffusion); + reverbL2 = reverbL1; + reverbL3 = -0.15f * reverbL1; + + m_filterHist[4] = (m_filterHist[4] - m_delayLines[4].Get()) * m_delayCoeffs[4][1] + m_delayLines[4].Get(); + reverbL1 = m_filterHist[4] * m_delayCoeffs[4][0] + reverbL2 * m_diffusion; + m_delayLines[4].Set(reverbL2 - reverbL1 * m_diffusion); + reverbL2 = reverbL1; + reverbL3 -= reverbL1 * 0.2f; + + if(m_quality & kMoreDelayLines) + { + m_filterHist[3] = (m_filterHist[3] - m_delayLines[3].Get()) * m_delayCoeffs[3][1] + m_delayLines[3].Get(); + reverbL1 = m_filterHist[3] * m_delayCoeffs[3][0] + reverbL2 * m_diffusion; + m_delayLines[3].Set(reverbL2 - reverbL1 * m_diffusion); + reverbL2 = reverbL1; + reverbL3 += 0.35f * reverbL1; + + m_filterHist[2] = (m_filterHist[2] - m_delayLines[2].Get()) * m_delayCoeffs[2][1] + m_delayLines[2].Get(); + reverbL1 = m_filterHist[2] * m_delayCoeffs[2][0] + reverbL2 * m_diffusion; + m_delayLines[2].Set(reverbL2 - reverbL1 * m_diffusion); + reverbL2 = reverbL1; + reverbL3 -= reverbL1 * 0.38f; + } + m_delayLines[17].Set(reverbL2); + + reverbL1 = m_delayLines[17].Get() * m_delayCoeffs[12][0]; + m_filterHist[17] = (m_filterHist[17] - reverbL1) * m_delayCoeffs[12][1] + reverbL1; + + m_filterHist[1] = (m_filterHist[1] - m_delayLines[1].Get()) * m_delayCoeffs[1][1] + m_delayLines[1].Get(); + reverbL1 = m_filterHist[17] * m_diffusion + m_filterHist[1] * m_delayCoeffs[1][0]; + m_delayLines[1].Set(m_filterHist[17] - reverbL1 * m_diffusion); + reverbL2 = reverbL1; + float reverbL4 = reverbL1 * 0.38f; + + m_filterHist[0] = (m_filterHist[0] - m_delayLines[0].Get()) * m_delayCoeffs[0][1] + m_delayLines[0].Get(); + reverbL1 = m_filterHist[0] * m_delayCoeffs[0][0] + reverbL2 * m_diffusion; + m_delayLines[0].Set(reverbL2 - reverbL1 * m_diffusion); + reverbL3 -= reverbL1 * 0.38f; + m_filterHist[15] = reverbL1; + + // Early reflections (right) + float earlyR = m_delayLines[16].Get(m_earlyTaps[1][1]) * 0.707f + - m_delayLines[16].Get(m_earlyTaps[1][2]) * 0.6f + - m_delayLines[16].Get(m_earlyTaps[1][3]) * 0.5f + - m_delayLines[16].Get(m_earlyTaps[1][4]) * 0.6f + - m_delayLines[16].Get(m_earlyTaps[1][5]) * 0.5f; + if(m_quality & kMoreDelayLines) + { + float earlyR2 = earlyR; + earlyR = m_delayLines[14].Get() + earlyR * 0.618034f; + m_delayLines[14].Set(earlyR2 - earlyR * 0.618034f); + } + const float earlyRefOutR = earlyR * m_ERLevel; + + m_filterHist[11] = (m_filterHist[11] - m_delayLines[11].Get()) * m_delayCoeffs[11][1] + m_delayLines[11].Get(); + reverbR1 = m_filterHist[11] * m_delayCoeffs[11][0] + reverbR2 * m_diffusion; + m_delayLines[11].Set(reverbR2 - reverbR1 * m_diffusion); + reverbR2 = reverbR1; + + m_filterHist[10] = (m_filterHist[10] - m_delayLines[10].Get()) * m_delayCoeffs[10][1] + m_delayLines[10].Get(); + reverbR1 = m_filterHist[10] * m_delayCoeffs[10][0] + reverbR2 * m_diffusion; + m_delayLines[10].Set(reverbR2 - reverbR1 * m_diffusion); + reverbR3 = reverbL4 - reverbR2 * 0.15f - reverbR1 * 0.2f; + reverbR2 = reverbR1; + + if(m_quality & kMoreDelayLines) + { + m_filterHist[9] = (m_filterHist[9] - m_delayLines[9].Get()) * m_delayCoeffs[9][1] + m_delayLines[9].Get(); + reverbR1 = m_filterHist[9] * m_delayCoeffs[9][0] + reverbR2 * m_diffusion; + m_delayLines[9].Set(reverbR2 - reverbR1 * m_diffusion); + reverbR2 = reverbR1; + reverbR3 += reverbR1 * 0.35f; + + m_filterHist[8] = (m_filterHist[8] - m_delayLines[8].Get()) * m_delayCoeffs[8][1] + m_delayLines[8].Get(); + reverbR1 = m_filterHist[8] * m_delayCoeffs[8][0] + reverbR2 * m_diffusion; + m_delayLines[8].Set(reverbR2 - reverbR1 * m_diffusion); + reverbR2 = reverbR1; + reverbR3 -= reverbR1 * 0.38f; + } + m_delayLines[18].Set(reverbR2); + + reverbR1 = m_delayLines[18].Get() * m_delayCoeffs[12][0]; + m_filterHist[18] = (m_filterHist[18] - reverbR1) * m_delayCoeffs[12][1] + reverbR1; + + m_filterHist[7] = (m_filterHist[7] - m_delayLines[7].Get()) * m_delayCoeffs[7][1] + m_delayLines[7].Get(); + reverbR1 = m_filterHist[18] * m_diffusion + m_filterHist[7] * m_delayCoeffs[7][0]; + m_delayLines[7].Set(m_filterHist[18] - reverbR1 * m_diffusion); + reverbR2 = reverbR1; + + float lateRevOutL = (reverbL3 + reverbR1 * 0.38f) * m_ReverbLevelL; + + m_filterHist[6] = (m_filterHist[6] - m_delayLines[6].Get()) * m_delayCoeffs[6][1] + m_delayLines[6].Get(); + reverbR1 = m_filterHist[6] * m_delayCoeffs[6][0] + reverbR2 * m_diffusion; + m_delayLines[6].Set(reverbR2 - reverbR1 * m_diffusion); + m_filterHist[16] = reverbR1; + + float lateRevOutR = (reverbR3 - reverbR1 * 0.38f) * m_ReverbLevelR; + + float outL = earlyRefOutL + lateRevOutL; + float outR = earlyRefOutR + lateRevOutR; + + for(auto &line : m_delayLines) + line.Advance(); + + if(!(m_quality & kFullSampleRate)) + { + *(out[0]++) = (outL + m_prevL) * 0.5f; + *(out[1]++) = (outR + m_prevR) * 0.5f; + m_prevL = outL; + m_prevR = outR; + in[0]++; + in[1]++; + if(frames-- == 1) + { + m_remain = true; + break; + } + } + *(out[0]++) = outL; + *(out[1]++) = outR; + frames--; + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +int32 I3DL2Reverb::GetNumPrograms() const +{ +#ifdef MODPLUG_TRACKER + return NUM_REVERBTYPES; +#else + return 0; +#endif +} + +void I3DL2Reverb::SetCurrentProgram(int32 program) +{ +#ifdef MODPLUG_TRACKER + if(program < static_cast<int32>(NUM_REVERBTYPES)) + { + m_program = program; + const auto &preset = *GetReverbPreset(m_program); + m_param[kI3DL2ReverbRoom] = (preset.lRoom + 10000) / 10000.0f; + m_param[kI3DL2ReverbRoomHF] = (preset.lRoomHF + 10000) / 10000.0f; + m_param[kI3DL2ReverbRoomRolloffFactor] = 0.0f; + m_param[kI3DL2ReverbDecayTime] = (preset.flDecayTime - 0.1f) / 19.9f; + m_param[kI3DL2ReverbDecayHFRatio] = (preset.flDecayHFRatio - 0.1f) / 1.9f; + m_param[kI3DL2ReverbReflections] = (preset.lReflections + 10000) / 11000.0f; + m_param[kI3DL2ReverbReflectionsDelay] = preset.flReflectionsDelay / 0.3f; + m_param[kI3DL2ReverbReverb] = (preset.lReverb + 10000) / 12000.0f; + m_param[kI3DL2ReverbReverbDelay] = preset.flReverbDelay / 0.1f; + m_param[kI3DL2ReverbDiffusion] = preset.flDiffusion / 100.0f; + m_param[kI3DL2ReverbDensity] = preset.flDensity / 100.0f; + m_param[kI3DL2ReverbHFReference] = (5000.0f - 20.0f) / 19980.0f; + RecalculateI3DL2ReverbParams(); + } +#else + MPT_UNUSED_VARIABLE(program); +#endif +} + + +PlugParamValue I3DL2Reverb::GetParameter(PlugParamIndex index) +{ + if(index < kI3DL2ReverbNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void I3DL2Reverb::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kI3DL2ReverbNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + if(index == kI3DL2ReverbQuality) + value = mpt::round(value * 3.0f) / 3.0f; + m_param[index] = value; + m_recalcParams = true; + } +} + + +void I3DL2Reverb::Resume() +{ + RecalculateI3DL2ReverbParams(); + PositionChanged(); + m_isResumed = true; +} + + +void I3DL2Reverb::PositionChanged() +{ + MemsetZero(m_filterHist); + m_prevL = 0; + m_prevR = 0; + m_remain = false; + + try + { + uint32 sampleRate = static_cast<uint32>(m_effectiveSampleRate); + m_delayLines[0].Init(67, 5, sampleRate, m_delayTaps[0]); + m_delayLines[1].Init(62, 5, sampleRate, m_delayTaps[1]); + m_delayLines[2].Init(53, 5, sampleRate, m_delayTaps[2]); + m_delayLines[3].Init(43, 5, sampleRate, m_delayTaps[3]); + m_delayLines[4].Init(32, 5, sampleRate, m_delayTaps[4]); + m_delayLines[5].Init(22, 5, sampleRate, m_delayTaps[5]); + m_delayLines[6].Init(75, 5, sampleRate, m_delayTaps[6]); + m_delayLines[7].Init(69, 5, sampleRate, m_delayTaps[7]); + m_delayLines[8].Init(60, 5, sampleRate, m_delayTaps[8]); + m_delayLines[9].Init(48, 5, sampleRate, m_delayTaps[9]); + m_delayLines[10].Init(36, 5, sampleRate, m_delayTaps[10]); + m_delayLines[11].Init(25, 5, sampleRate, m_delayTaps[11]); + m_delayLines[12].Init(0, 0, 0); // Dummy for array index consistency with both tap and coefficient arrays + m_delayLines[13].Init(3, 0, sampleRate, m_delayTaps[13]); + m_delayLines[14].Init(3, 0, sampleRate, m_delayTaps[14]); + m_delayLines[15].Init(407, 1, sampleRate); + m_delayLines[16].Init(400, 1, sampleRate); + m_delayLines[17].Init(10, 0, sampleRate, -1); + m_delayLines[18].Init(10, 0, sampleRate, -1); + m_ok = true; + } catch(mpt::out_of_memory e) + { + m_ok = false; + mpt::delete_out_of_memory(e); + } +} + + +#ifdef MODPLUG_TRACKER + +CString I3DL2Reverb::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kI3DL2ReverbRoom: return _T("Room"); + case kI3DL2ReverbRoomHF: return _T("RoomHF"); + case kI3DL2ReverbRoomRolloffFactor: return _T("RoomRolloffFactor"); + case kI3DL2ReverbDecayTime: return _T("DecayTime"); + case kI3DL2ReverbDecayHFRatio: return _T("DecayHFRatio"); + case kI3DL2ReverbReflections: return _T("Reflections"); + case kI3DL2ReverbReflectionsDelay: return _T("ReflectionsDelay"); + case kI3DL2ReverbReverb: return _T("Reverb"); + case kI3DL2ReverbReverbDelay: return _T("ReverbDelay"); + case kI3DL2ReverbDiffusion: return _T("Diffusion"); + case kI3DL2ReverbDensity: return _T("Density"); + case kI3DL2ReverbHFReference: return _T("HFRefrence"); + case kI3DL2ReverbQuality: return _T("Quality"); + } + return CString(); +} + + +CString I3DL2Reverb::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kI3DL2ReverbRoom: + case kI3DL2ReverbRoomHF: + case kI3DL2ReverbReflections: + case kI3DL2ReverbReverb: + return _T("dB"); + case kI3DL2ReverbDecayTime: + case kI3DL2ReverbReflectionsDelay: + case kI3DL2ReverbReverbDelay: + return _T("s"); + case kI3DL2ReverbDiffusion: + case kI3DL2ReverbDensity: + return _T("%"); + case kI3DL2ReverbHFReference: + return _T("Hz"); + } + return CString(); +} + + +CString I3DL2Reverb::GetParamDisplay(PlugParamIndex param) +{ + static constexpr const TCHAR * const modes[] = { _T("LQ"), _T("LQ+"), _T("HQ"), _T("HQ+") }; + float value = m_param[param]; + switch(param) + { + case kI3DL2ReverbRoom: value = Room() * 0.01f; break; + case kI3DL2ReverbRoomHF: value = RoomHF() * 0.01f; break; + case kI3DL2ReverbRoomRolloffFactor: value = RoomRolloffFactor(); break; + case kI3DL2ReverbDecayTime: value = DecayTime(); break; + case kI3DL2ReverbDecayHFRatio: value = DecayHFRatio(); break; + case kI3DL2ReverbReflections: value = Reflections() * 0.01f; break; + case kI3DL2ReverbReflectionsDelay: value = ReflectionsDelay(); break; + case kI3DL2ReverbReverb: value = Reverb() * 0.01f; break; + case kI3DL2ReverbReverbDelay: value = ReverbDelay(); break; + case kI3DL2ReverbDiffusion: value = Diffusion(); break; + case kI3DL2ReverbDensity: value = Density(); break; + case kI3DL2ReverbHFReference: value = HFReference(); break; + case kI3DL2ReverbQuality: return modes[Quality() % 4u]; + } + CString s; + s.Format(_T("%.2f"), value); + return s; +} + + +CString I3DL2Reverb::GetCurrentProgramName() +{ + return GetProgramName(m_program); +} + + +CString I3DL2Reverb::GetProgramName(int32 program) +{ + return mpt::ToCString(GetReverbPresetName(program)); +} + +#endif // MODPLUG_TRACKER + + +void I3DL2Reverb::RecalculateI3DL2ReverbParams() +{ + m_quality = Quality(); + m_effectiveSampleRate = static_cast<float>(m_SndFile.GetSampleRate() / ((m_quality & kFullSampleRate) ? 1u : 2u)); + + // Diffusion + m_diffusion = Diffusion() * (0.618034f / 100.0f); + // Early Reflection Level + m_ERLevel = std::min(std::pow(10.0f, (Room() + Reflections()) / (100.0f * 20.0f)), 1.0f) * 0.761f; + + // Room Filter + float roomHF = std::pow(10.0f, RoomHF() / 100.0f / 10.0f); + if(roomHF == 1.0f) + { + m_roomFilter = 0.0f; + } else + { + float freq = std::cos(HFReference() * (2.0f * mpt::numbers::pi_v<float>) / m_effectiveSampleRate); + 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); + } + + SetDelayTaps(); + SetDecayCoeffs(); + + m_recalcParams = false; +} + + +void I3DL2Reverb::SetDelayTaps() +{ + // Early reflections + static constexpr float delays[] = + { + 1.0000f, 1.0000f, 0.0000f, 0.1078f, 0.1768f, 0.2727f, + 0.3953f, 0.5386f, 0.6899f, 0.8306f, 0.9400f, 0.9800f, + }; + + const float sampleRate = m_effectiveSampleRate; + const float reflectionsDelay = ReflectionsDelay(); + const float reverbDelay = std::max(ReverbDelay(), 5.0f / 1000.0f); + m_earlyTaps[0][0] = static_cast<int32>((reverbDelay + reflectionsDelay + 7.0f / 1000.0f) * sampleRate); + for(uint32 i = 1; i < 12; i++) + { + m_earlyTaps[i % 2u][i / 2u] = static_cast<int32>((reverbDelay * delays[i] + reflectionsDelay) * sampleRate); + } + + // Late reflections + float density = std::min((Density() / 100.0f + 0.1f) * 0.9091f, 1.0f); + float delayL = density * 67.0f / 1000.0f * sampleRate; + float delayR = density * 75.0f / 1000.0f * sampleRate; + for(int i = 0, power = 0; i < 6; i++) + { + power += i; + float factor = std::pow(0.93f, static_cast<float>(power)); + m_delayTaps[i + 0] = static_cast<int32>(delayL * factor); + m_delayTaps[i + 6] = static_cast<int32>(delayR * factor); + } + m_delayTaps[12] = static_cast<int32>(10.0f / 1000.0f * sampleRate); + // Early reflections (extra delay lines) + m_delayTaps[13] = static_cast<int32>(3.25f / 1000.0f * sampleRate); + m_delayTaps[14] = static_cast<int32>(3.53f / 1000.0f * sampleRate); + + for(std::size_t d = 0; d < std::size(m_delayTaps); d++) + m_delayLines[d].SetDelayTap(m_delayTaps[d]); +} + + +void I3DL2Reverb::SetDecayCoeffs() +{ + float levelLtmp = 1.0f, levelRtmp = 1.0f; + float levelL = 0.0f, levelR = 0.0f; + + levelLtmp *= CalcDecayCoeffs(5); + levelRtmp *= CalcDecayCoeffs(11); + levelL += levelLtmp * 0.0225f; + levelR += levelRtmp * 0.0225f; + + levelLtmp *= CalcDecayCoeffs(4); + levelRtmp *= CalcDecayCoeffs(10); + levelL += levelLtmp * 0.04f; + levelR += levelRtmp * 0.04f; + + if(m_quality & kMoreDelayLines) + { + levelLtmp *= CalcDecayCoeffs(3); + levelRtmp *= CalcDecayCoeffs(9); + levelL += levelLtmp * 0.1225f; + levelR += levelRtmp * 0.1225f; + + levelLtmp *= CalcDecayCoeffs(2); + levelRtmp *= CalcDecayCoeffs(8); + levelL += levelLtmp * 0.1444f; + levelR += levelRtmp * 0.1444f; + } + CalcDecayCoeffs(12); + levelLtmp *= m_delayCoeffs[12][0] * m_delayCoeffs[12][0]; + levelRtmp *= m_delayCoeffs[12][0] * m_delayCoeffs[12][0]; + + levelLtmp *= CalcDecayCoeffs(1); + levelRtmp *= CalcDecayCoeffs(7); + levelL += levelRtmp * 0.1444f; + levelR += levelLtmp * 0.1444f; + + levelLtmp *= CalcDecayCoeffs(0); + levelRtmp *= CalcDecayCoeffs(6); + levelL += levelLtmp * 0.1444f; + levelR += levelRtmp * 0.1444f; + + // Final Reverb Level + float level = std::min(std::pow(10.0f, (Room() + Reverb()) / (100.0f * 20.0f)), 1.0f); + float monoInv = 1.0f - ((levelLtmp + levelRtmp) * 0.5f); + m_ReverbLevelL = level * std::sqrt(monoInv / levelL); + m_ReverbLevelR = level * std::sqrt(monoInv / levelR); +} + + +float I3DL2Reverb::CalcDecayCoeffs(int32 index) +{ + float hfRef = (2.0f * mpt::numbers::pi_v<float>) / m_effectiveSampleRate * HFReference(); + float decayHFRatio = DecayHFRatio(); + if(decayHFRatio > 1.0f) + hfRef = mpt::numbers::pi_v<float>; + + float c1 = std::pow(10.0f, ((m_delayTaps[index] / m_effectiveSampleRate) * -60.0f / DecayTime()) / 20.0f); + float c2 = 0.0f; + + float c21 = (std::pow(c1, 2.0f - 2.0f / decayHFRatio) - 1.0f) / (1.0f - std::cos(hfRef)); + if(c21 != 0 && std::isfinite(c21)) + { + float c22 = -2.0f * c21 - 2.0f; + float c23sq = c22 * c22 - c21 * c21 * 4.0f; + float c23 = c23sq > 0.0f ? std::sqrt(c23sq) : 0.0f; + c2 = (c23 - c22) / (c21 + c21); + if(std::abs(c2) > 1.0f) + c2 = (-c22 - c23) / (c21 + c21); + c2 = mpt::sanitize_nan(c2); + } + m_delayCoeffs[index][0] = c1; + m_delayCoeffs[index][1] = c2; + + c1 *= c1; + float diff2 = m_diffusion * m_diffusion; + return diff2 + c1 / (1.0f - diff2 * c1) * (1.0f - diff2) * (1.0f - diff2); +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(I3DL2Reverb) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.h new file mode 100644 index 00000000..ee3ea690 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/I3DL2Reverb.h @@ -0,0 +1,169 @@ +/* + * I3DL2Reverb.h + * ------------- + * Purpose: Implementation of the DMO I3DL2Reverb DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class I3DL2Reverb final : public IMixPlugin +{ +protected: + enum Parameters + { + kI3DL2ReverbRoom = 0, + kI3DL2ReverbRoomHF, + kI3DL2ReverbRoomRolloffFactor, // Doesn't actually do anything :) + kI3DL2ReverbDecayTime, + kI3DL2ReverbDecayHFRatio, + kI3DL2ReverbReflections, + kI3DL2ReverbReflectionsDelay, + kI3DL2ReverbReverb, + kI3DL2ReverbReverbDelay, + kI3DL2ReverbDiffusion, + kI3DL2ReverbDensity, + kI3DL2ReverbHFReference, + kI3DL2ReverbQuality, + kI3DL2ReverbNumParameters + }; + + enum QualityFlags + { + kMoreDelayLines = 0x01, + kFullSampleRate = 0x02, + }; + + class DelayLine : private std::vector<float> + { + int32 m_length; + int32 m_position; + int32 m_delayPosition; + + public: + void Init(int32 ms, int32 padding, uint32 sampleRate, int32 delayTap = 0); + void SetDelayTap(int32 delayTap); + void Advance(); + void Set(float value); + float Get(int32 offset) const; + float Get() const; + }; + + std::array<float, kI3DL2ReverbNumParameters> m_param; + int32 m_program = 0; + + // Calculated parameters + uint32 m_quality; + float m_effectiveSampleRate; + float m_diffusion; + float m_roomFilter; + float m_ERLevel; + float m_ReverbLevelL; + float m_ReverbLevelR; + + int32 m_delayTaps[15]; // 6*L + 6*R + LR + Early L + Early R + int32 m_earlyTaps[2][6]; + float m_delayCoeffs[13][2]; + + // State + DelayLine m_delayLines[19]; + float m_filterHist[19]; + + // Remaining frame for downsampled reverb + float m_prevL; + float m_prevR; + bool m_remain = false; + + bool m_ok = false, m_recalcParams = true; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + I3DL2Reverb(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0xEF985E71; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override; + int32 GetCurrentProgram() override { return m_program; } + // cppcheck-suppress virtualCallInConstructor + void SetCurrentProgram(int32) override; + + PlugParamIndex GetNumParameters() const override { return kI3DL2ReverbNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("I3DL2Reverb"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override; + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32 program) override; + + bool HasEditor() const override { return false; } +#endif + + void BeginSetProgram(int32) override { } + void EndSetProgram() override { } + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + float Room() const { return -10000.0f + m_param[kI3DL2ReverbRoom] * 10000.0f; } + float RoomHF() const { return -10000.0f + m_param[kI3DL2ReverbRoomHF] * 10000.0f; } + float RoomRolloffFactor() const { return m_param[kI3DL2ReverbRoomRolloffFactor] * 10.0f; } + float DecayTime() const { return 0.1f + m_param[kI3DL2ReverbDecayTime] * 19.9f; } + float DecayHFRatio() const { return 0.1f + m_param[kI3DL2ReverbDecayHFRatio] * 1.9f; } + float Reflections() const { return -10000.0f + m_param[kI3DL2ReverbReflections] * 11000.0f; } + float ReflectionsDelay() const { return m_param[kI3DL2ReverbReflectionsDelay] * 0.3f; } + float Reverb() const { return -10000.0f + m_param[kI3DL2ReverbReverb] * 12000.0f; } + float ReverbDelay() const { return m_param[kI3DL2ReverbReverbDelay] * 0.1f; } + float Diffusion() const { return m_param[kI3DL2ReverbDiffusion] * 100.0f; } + float Density() const { return m_param[kI3DL2ReverbDensity] * 100.0f; } + float HFReference() const { return 20.0f + m_param[kI3DL2ReverbHFReference] * 19980.0f; } + uint32 Quality() const { return mpt::saturate_round<uint32>(m_param[kI3DL2ReverbQuality] * 3.0f); } + + void RecalculateI3DL2ReverbParams(); + + void SetDelayTaps(); + void SetDecayCoeffs(); + float CalcDecayCoeffs(int32 index); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.cpp new file mode 100644 index 00000000..eb237d6f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.cpp @@ -0,0 +1,201 @@ +/* + * ParamEq.cpp + * ----------- + * Purpose: Implementation of the DMO Parametric Equalizer DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "ParamEq.h" +#include "mpt/base/numbers.hpp" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* ParamEq::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) ParamEq(factory, sndFile, mixStruct); +} + + +ParamEq::ParamEq(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) + , m_maxFreqParam(1.0f) +{ + m_param[kEqCenter] = (8000.0f - 80.0f) / 15920.0f; + m_param[kEqBandwidth] = 0.314286f; + m_param[kEqGain] = 0.5f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void ParamEq::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + if(m_param[kEqGain] == 0.5f) + { + memcpy(out[0], in[0], numFrames * sizeof(float)); + memcpy(out[1], in[1], numFrames * sizeof(float)); + } else + { + for(uint32 i = numFrames; i != 0; i--) + { + for(uint8 channel = 0; channel < 2; channel++) + { + float x = *(in[channel])++; + float y = b0DIVa0 * x + b1DIVa0 * x1[channel] + b2DIVa0 * x2[channel] - a1DIVa0 * y1[channel] - a2DIVa0 * y2[channel]; + + x2[channel] = x1[channel]; + x1[channel] = x; + y2[channel] = y1[channel]; + y1[channel] = y; + + *(out[channel])++ = y; + } + } + } + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue ParamEq::GetParameter(PlugParamIndex index) +{ + if(index < kEqNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void ParamEq::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kEqNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + m_param[index] = value; + RecalculateEqParams(); + } +} + + +void ParamEq::Resume() +{ + m_isResumed = true; + // Limit center frequency to a third of the sampling rate. + m_maxFreqParam = Clamp((m_SndFile.GetSampleRate() / 3.0f - 80.0f) / 15920.0f, 0.0f, 1.0f); + RecalculateEqParams(); + PositionChanged(); +} + + +void ParamEq::PositionChanged() +{ + // Reset filter state + x1[0] = x2[0] = 0; + x1[1] = x2[1] = 0; + y1[0] = y2[0] = 0; + y1[1] = y2[1] = 0; +} + + +#ifdef MODPLUG_TRACKER + +CString ParamEq::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kEqCenter: return _T("Center"); + case kEqBandwidth: return _T("Bandwidth"); + case kEqGain: return _T("Gain"); + } + return CString(); +} + + +CString ParamEq::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kEqCenter: return _T("Hz"); + case kEqBandwidth: return _T("Semitones"); + case kEqGain: return _T("dB"); + } + return CString(); +} + + +CString ParamEq::GetParamDisplay(PlugParamIndex param) +{ + float value = 0.0f; + switch(param) + { + case kEqCenter: + value = FreqInHertz(); + break; + case kEqBandwidth: + value = BandwidthInSemitones(); + break; + case kEqGain: + value = GainInDecibel(); + break; + } + CString s; + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + + +void ParamEq::RecalculateEqParams() +{ + LimitMax(m_param[kEqCenter], m_maxFreqParam); + const float freq = FreqInHertz() / m_SndFile.GetSampleRate(); + const float a = std::pow(10.0f, GainInDecibel() / 40.0f); + const float w0 = 2.0f * mpt::numbers::pi_v<float> * freq; + const float sinW0 = std::sin(w0); + const float cosW0 = std::cos(w0); + const float alpha = sinW0 * std::sinh((BandwidthInSemitones() * (mpt::numbers::ln2_v<float> / 24.0f)) * w0 / sinW0); + + const float b0 = 1.0f + alpha * a; + const float b1 = -2.0f * cosW0; + const float b2 = 1.0f - alpha * a; + const float a0 = 1.0f + alpha / a; + const float a1 = -2.0f * cosW0; + const float a2 = 1.0f - alpha / a; + + b0DIVa0 = b0 / a0; + b1DIVa0 = b1 / a0; + b2DIVa0 = b2 / a0; + a1DIVa0 = a1 / a0; + a2DIVa0 = a2 / a0; +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(ParamEq) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.h new file mode 100644 index 00000000..6ad3e4f3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/ParamEq.h @@ -0,0 +1,98 @@ +/* + * ParamEq.h + * --------- + * Purpose: Implementation of the DMO Parametric Equalizer DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class ParamEq final : public IMixPlugin +{ +protected: + enum Parameters + { + kEqCenter = 0, + kEqBandwidth, + kEqGain, + kEqNumParameters + }; + + std::array<float, kEqNumParameters> m_param; + + // Equalizer coefficients + float b0DIVa0, b1DIVa0, b2DIVa0, a1DIVa0, a2DIVa0; + // Equalizer memory + float x1[2], x2[2]; + float y1[2], y2[2]; + float m_maxFreqParam; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + ParamEq(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0x120CED89; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kEqNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("ParamEq"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + float BandwidthInSemitones() const { return 1.0f + m_param[kEqBandwidth] * 35.0f; } + float FreqInHertz() const { return 80.0f + m_param[kEqCenter] * 15920.0f; } + float GainInDecibel() const { return (m_param[kEqGain] - 0.5f) * 30.0f; } + void RecalculateEqParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.cpp new file mode 100644 index 00000000..3f8757a2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.cpp @@ -0,0 +1,261 @@ +/* + * WavesReverb.cpp + * --------------- + * Purpose: Implementation of the DMO WavesReverb DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../../Sndfile.h" +#include "WavesReverb.h" +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +#ifndef NO_PLUGINS + +namespace DMO +{ + +IMixPlugin* WavesReverb::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + return new (std::nothrow) WavesReverb(factory, sndFile, mixStruct); +} + + +WavesReverb::WavesReverb(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMixPlugin(factory, sndFile, mixStruct) +{ + m_param[kRvbInGain] = 1.0f; + m_param[kRvbReverbMix] = 1.0f; + m_param[kRvbReverbTime] = 1.0f / 3.0f; + m_param[kRvbHighFreqRTRatio] = 0.0f; + + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +void WavesReverb::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + if(!m_mixBuffer.Ok()) + return; + + const float *in[2] = { m_mixBuffer.GetInputBuffer(0), m_mixBuffer.GetInputBuffer(1) }; + float *out[2] = { m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1) }; + + uint32 combPos = m_state.combPos, allpassPos = m_state.allpassPos; + uint32 delay0 = (m_delay[0] + combPos + 1) & 0xFFF; + uint32 delay1 = (m_delay[1] + combPos + 1) & 0xFFF; + uint32 delay2 = (m_delay[2] + combPos + 1) & 0xFFF; + uint32 delay3 = (m_delay[3] + combPos + 1) & 0xFFF; + uint32 delay4 = (m_delay[4] + allpassPos) & 0x3FF; + uint32 delay5 = (m_delay[5] + allpassPos) & 0x3FF; + float delay0old = m_state.comb[delay0][0]; + float delay1old = m_state.comb[delay1][1]; + float delay2old = m_state.comb[delay2][2]; + float delay3old = m_state.comb[delay3][3]; + + for(uint32 i = numFrames; i != 0; i--) + { + const float leftIn = *(in[0])++ + 1e-30f; // Prevent denormals + const float rightIn = *(in[1])++ + 1e-30f; // Prevent denormals + + // Advance buffer index for the four comb filters + delay0 = (delay0 - 1) & 0xFFF; + delay1 = (delay1 - 1) & 0xFFF; + delay2 = (delay2 - 1) & 0xFFF; + delay3 = (delay3 - 1) & 0xFFF; + float &delay0new = m_state.comb[delay0][0]; + float &delay1new = m_state.comb[delay1][1]; + float &delay2new = m_state.comb[delay2][2]; + float &delay3new = m_state.comb[delay3][3]; + + float r1, r2; + + r1 = delay1new * 0.61803401f + m_state.allpass1[delay4][0] * m_coeffs[0]; + r2 = m_state.allpass1[delay4][1] * m_coeffs[0] - delay0new * 0.61803401f; + m_state.allpass1[allpassPos][0] = r2 * 0.61803401f + delay0new; + m_state.allpass1[allpassPos][1] = delay1new - r1 * 0.61803401f; + delay0new = r1; + delay1new = r2; + + r1 = delay3new * 0.61803401f + m_state.allpass2[delay5][0] * m_coeffs[1]; + r2 = m_state.allpass2[delay5][1] * m_coeffs[1] - delay2new * 0.61803401f; + m_state.allpass2[allpassPos][0] = r2 * 0.61803401f + delay2new; + m_state.allpass2[allpassPos][1] = delay3new - r1 * 0.61803401f; + delay2new = r1; + delay3new = r2; + + *(out[0])++ = (leftIn * m_dryFactor) + delay0new + delay2new; + *(out[1])++ = (rightIn * m_dryFactor) + delay1new + delay3new; + + const float leftWet = leftIn * m_wetFactor; + const float rightWet = rightIn * m_wetFactor; + m_state.comb[combPos][0] = (delay0new * m_coeffs[2]) + (delay0old * m_coeffs[3]) + leftWet; + m_state.comb[combPos][1] = (delay1new * m_coeffs[4]) + (delay1old * m_coeffs[5]) + rightWet; + m_state.comb[combPos][2] = (delay2new * m_coeffs[6]) + (delay2old * m_coeffs[7]) - rightWet; + m_state.comb[combPos][3] = (delay3new * m_coeffs[8]) + (delay3old * m_coeffs[9]) + leftWet; + + delay0old = delay0new; + delay1old = delay1new; + delay2old = delay2new; + delay3old = delay3new; + + // Advance buffer index + combPos = (combPos - 1) & 0xFFF; + allpassPos = (allpassPos - 1) & 0x3FF; + delay4 = (delay4 - 1) & 0x3FF; + delay5 = (delay5 - 1) & 0x3FF; + } + m_state.combPos = combPos; + m_state.allpassPos = allpassPos; + + ProcessMixOps(pOutL, pOutR, m_mixBuffer.GetOutputBuffer(0), m_mixBuffer.GetOutputBuffer(1), numFrames); +} + + +PlugParamValue WavesReverb::GetParameter(PlugParamIndex index) +{ + if(index < kRvbNumParameters) + { + return m_param[index]; + } + return 0; +} + + +void WavesReverb::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + if(index < kRvbNumParameters) + { + value = mpt::safe_clamp(value, 0.0f, 1.0f); + m_param[index] = value; + RecalculateWavesReverbParams(); + } +} + + +void WavesReverb::Resume() +{ + m_isResumed = true; + // Recalculate delays + uint32 delay0 = mpt::saturate_round<uint32>(m_SndFile.GetSampleRate() * 0.045f); + uint32 delay1 = mpt::saturate_round<uint32>(delay0 * 1.18920707f); // 2^0.25 + uint32 delay2 = mpt::saturate_round<uint32>(delay1 * 1.18920707f); + uint32 delay3 = mpt::saturate_round<uint32>(delay2 * 1.18920707f); + uint32 delay4 = mpt::saturate_round<uint32>((delay0 + delay2) * 0.11546667f); + uint32 delay5 = mpt::saturate_round<uint32>((delay1 + delay3) * 0.11546667f); + // Comb delays + m_delay[0] = delay0 - delay4; + m_delay[1] = delay2 - delay4; + m_delay[2] = delay1 - delay5; + m_delay[3] = delay3 - delay5; + // Allpass delays + m_delay[4] = delay4; + m_delay[5] = delay5; + + RecalculateWavesReverbParams(); + PositionChanged(); +} + + +void WavesReverb::PositionChanged() +{ + MemsetZero(m_state); +} + + +#ifdef MODPLUG_TRACKER + +CString WavesReverb::GetParamName(PlugParamIndex param) +{ + switch(param) + { + case kRvbInGain: return _T("InGain"); + case kRvbReverbMix: return _T("ReverbMix"); + case kRvbReverbTime: return _T("ReverbTime"); + case kRvbHighFreqRTRatio: return _T("HighFreqRTRatio"); + } + return CString(); +} + + +CString WavesReverb::GetParamLabel(PlugParamIndex param) +{ + switch(param) + { + case kRvbInGain: + case kRvbReverbMix: + return _T("dB"); + case kRvbReverbTime: + return _T("ms"); + } + return CString(); +} + + +CString WavesReverb::GetParamDisplay(PlugParamIndex param) +{ + float value = m_param[param]; + switch(param) + { + case kRvbInGain: + case kRvbReverbMix: + value = GainInDecibel(value); + break; + case kRvbReverbTime: + value = ReverbTime(); + break; + case kRvbHighFreqRTRatio: + value = HighFreqRTRatio(); + break; + } + CString s; + s.Format(_T("%.2f"), value); + return s; +} + +#endif // MODPLUG_TRACKER + + +void WavesReverb::RecalculateWavesReverbParams() +{ + // Recalculate filters + const double ReverbTimeSmp = -3000.0 / (m_SndFile.GetSampleRate() * ReverbTime()); + const double ReverbTimeSmpHF = ReverbTimeSmp * (1.0 / HighFreqRTRatio() - 1.0); + + m_coeffs[0] = static_cast<float>(std::pow(10.0, m_delay[4] * ReverbTimeSmp)); + m_coeffs[1] = static_cast<float>(std::pow(10.0, m_delay[5] * ReverbTimeSmp)); + + double sum = 0.0; + for(uint32 pair = 0; pair < 4; pair++) + { + double gain1 = std::pow(10.0, m_delay[pair] * ReverbTimeSmp); + double gain2 = (1.0 - std::pow(10.0, (m_delay[pair] + m_delay[4 + pair / 2]) * ReverbTimeSmpHF)) * 0.5; + double gain3 = gain1 * m_coeffs[pair / 2]; + double gain4 = gain3 * (((gain3 + 1.0) * gain3 + 1.0) * gain3 + 1.0) + 1.0; + m_coeffs[2 + pair * 2] = static_cast<float>(gain1 * (1.0 - gain2)); + m_coeffs[3 + pair * 2] = static_cast<float>(gain1 * gain2); + sum += gain4 * gain4; + } + + double inGain = std::pow(10.0, GainInDecibel(m_param[kRvbInGain]) * 0.05); + double reverbMix = std::pow(10.0, GainInDecibel(m_param[kRvbReverbMix]) * 0.1); + m_dryFactor = static_cast<float>(std::sqrt(1.0 - reverbMix) * inGain); + m_wetFactor = static_cast<float>(std::sqrt(reverbMix) * (4.0 / std::sqrt(sum) * inGain)); +} + +} // namespace DMO + +#else +MPT_MSVC_WORKAROUND_LNK4221(WavesReverb) + +#endif // !NO_PLUGINS + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.h b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.h new file mode 100644 index 00000000..13e9af1e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/plugins/dmo/WavesReverb.h @@ -0,0 +1,107 @@ +/* + * WavesReverb.h + * ------------- + * Purpose: Implementation of the DMO WavesReverb DSP (for non-Windows platforms) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#ifndef NO_PLUGINS + +#include "../PlugInterface.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace DMO +{ + +class WavesReverb final : public IMixPlugin +{ +protected: + enum Parameters + { + kRvbInGain = 0, + kRvbReverbMix, + kRvbReverbTime, + kRvbHighFreqRTRatio, + kRvbNumParameters + }; + + std::array<float, kRvbNumParameters> m_param; + + // Parameters and coefficients + float m_dryFactor; + float m_wetFactor; + std::array<float, 10> m_coeffs; + std::array<uint32, 6> m_delay; + + // State + struct ReverbState + { + uint32 combPos, allpassPos; + float comb[4096][4]; + float allpass1[1024][2]; + float allpass2[1024][2]; + } m_state; + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + WavesReverb(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + + void Release() override { delete this; } + int32 GetUID() const override { return 0x87FC0268; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + + float RenderSilence(uint32) override { return 0.0f; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return kRvbNumParameters; } + PlugParamValue GetParameter(PlugParamIndex index) override; + void SetParameter(PlugParamIndex index, PlugParamValue value) override; + + void Resume() override; + void Suspend() override { m_isResumed = false; } + void PositionChanged() override; + + bool IsInstrument() const override { return false; } + bool CanRecieveMidiEvents() override { return false; } + bool ShouldProcessSilence() override { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return _T("WavesReverb"); } + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex) override; + CString GetParamDisplay(PlugParamIndex param) override; + + CString GetCurrentProgramName() override { return CString(); } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return CString(); } + + bool HasEditor() const override { return false; } +#endif + + int GetNumInputChannels() const override { return 2; } + int GetNumOutputChannels() const override { return 2; } + +protected: + static float GainInDecibel(float param) { return -96.0f + param * 96.0f; } + float ReverbTime() const { return 0.001f + m_param[kRvbReverbTime] * 2999.999f; } + float HighFreqRTRatio() const { return 0.001f + m_param[kRvbHighFreqRTRatio] * 0.998f; } + void RecalculateWavesReverbParams(); +}; + +} // namespace DMO + +OPENMPT_NAMESPACE_END + +#endif // !NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/tuning.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/tuning.cpp new file mode 100644 index 00000000..15562083 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/tuning.cpp @@ -0,0 +1,1011 @@ +/* + * tuning.cpp + * ---------- + * Purpose: Alternative sample tuning. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "tuning.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/serialization_utils.h" +#include "../common/misc_util.h" +#include <string> +#include <cmath> + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Tuning { + +static RATIOTYPE SanitizeGroupRatio(RATIOTYPE ratio) +{ + return std::clamp(std::abs(ratio), 1e-15f, 1e+07f); +} + +namespace CTuningS11n +{ + void ReadStr(std::istream &iStrm, mpt::ustring &ustr, const std::size_t dummy, mpt::Charset charset); + void ReadNoteMap(std::istream &iStrm, std::map<NOTEINDEXTYPE, mpt::ustring> &m, const std::size_t dummy, mpt::Charset charset); + void ReadRatioTable(std::istream& iStrm, std::vector<RATIOTYPE>& v, const size_t); + + void WriteNoteMap(std::ostream &oStrm, const std::map<NOTEINDEXTYPE, mpt::ustring> &m); + void WriteStr(std::ostream &oStrm, const mpt::ustring &ustr); + + struct RatioWriter + { + RatioWriter(uint16 nWriteCount = s_nDefaultWriteCount) : m_nWriteCount(nWriteCount) {} + + void operator()(std::ostream& oStrm, const std::vector<float>& v); + uint16 m_nWriteCount; + enum : uint16 { s_nDefaultWriteCount = (uint16_max >> 2) }; + }; +} + +using namespace CTuningS11n; + + +/* +Version history: + 4->5: Lots of changes, finestep interpretation revamp, fileformat revamp. + 3->4: Changed sizetypes in serialisation from size_t(uint32) to + smaller types (uint8, USTEPTYPE) (March 2007) +*/ +/* +Version changes: + 3->4: Finetune related internal structure and serialization revamp. + 2->3: The type for the size_type in the serialisation changed + from default(size_t, uint32) to unsigned STEPTYPE. (March 2007) +*/ + + +static_assert(CTuning::s_RatioTableFineSizeMaxDefault < static_cast<USTEPINDEXTYPE>(FINESTEPCOUNT_MAX)); + + +CTuning::CTuning() + : m_TuningType(Type::GENERAL) + , m_FineStepCount(0) +{ + m_RatioTable.clear(); + m_NoteMin = s_NoteMinDefault; + m_RatioTable.resize(s_RatioTableSizeDefault, 1); + m_GroupSize = 0; + m_GroupRatio = 0; + m_RatioTableFine.clear(); +} + + +bool CTuning::CreateGroupGeometric(const NOTEINDEXTYPE &s, const RATIOTYPE &r, const NOTEINDEXTYPE &startindex) +{ + if(s < 1 || !IsValidRatio(r) || startindex < GetNoteRange().first) + { + return false; + } + std::vector<RATIOTYPE> v; + v.reserve(s); + for(NOTEINDEXTYPE i = startindex; i < startindex + s; i++) + { + v.push_back(GetRatio(i)); + } + return CreateGroupGeometric(v, r, GetNoteRange(), startindex); +} + + +bool CTuning::CreateGroupGeometric(const std::vector<RATIOTYPE> &v, const RATIOTYPE &r, const NoteRange &range, const NOTEINDEXTYPE &ratiostartpos) +{ + if(range.first > range.last || v.size() == 0) + { + return false; + } + if(ratiostartpos < range.first || range.last < ratiostartpos || static_cast<UNOTEINDEXTYPE>(range.last - ratiostartpos) < static_cast<UNOTEINDEXTYPE>(v.size() - 1)) + { + return false; + } + if(GetFineStepCount() > FINESTEPCOUNT_MAX) + { + return false; + } + for(size_t i = 0; i < v.size(); i++) + { + if(v[i] < 0) + { + return false; + } + } + if(r <= 0) + { + return false; + } + m_TuningType = Type::GROUPGEOMETRIC; + m_NoteMin = range.first; + m_GroupSize = mpt::saturate_cast<NOTEINDEXTYPE>(v.size()); + m_GroupRatio = std::fabs(r); + m_RatioTable.resize(range.last - range.first + 1); + std::copy(v.begin(), v.end(), m_RatioTable.begin() + (ratiostartpos - range.first)); + for(int32 i = ratiostartpos - 1; i >= m_NoteMin && ratiostartpos > NOTEINDEXTYPE_MIN; i--) + { + m_RatioTable[i - m_NoteMin] = m_RatioTable[i - m_NoteMin + m_GroupSize] / m_GroupRatio; + } + for(int32 i = ratiostartpos + m_GroupSize; i <= range.last && ratiostartpos <= (NOTEINDEXTYPE_MAX - m_GroupSize); i++) + { + m_RatioTable[i - m_NoteMin] = m_GroupRatio * m_RatioTable[i - m_NoteMin - m_GroupSize]; + } + UpdateFineStepTable(); + return true; +} + + +bool CTuning::CreateGeometric(const UNOTEINDEXTYPE &p, const RATIOTYPE &r) +{ + return CreateGeometric(p, r, GetNoteRange()); +} + + +bool CTuning::CreateGeometric(const UNOTEINDEXTYPE &s, const RATIOTYPE &r, const NoteRange &range) +{ + if(range.first > range.last) + { + return false; + } + if(s < 1 || !IsValidRatio(r)) + { + return false; + } + if(range.last - range.first + 1 > NOTEINDEXTYPE_MAX) + { + return false; + } + m_TuningType = Type::GEOMETRIC; + m_RatioTable.clear(); + m_NoteMin = s_NoteMinDefault; + m_RatioTable.resize(s_RatioTableSizeDefault, static_cast<RATIOTYPE>(1.0)); + m_GroupSize = 0; + m_GroupRatio = 0; + m_RatioTableFine.clear(); + m_NoteMin = range.first; + m_GroupSize = mpt::saturate_cast<NOTEINDEXTYPE>(s); + m_GroupRatio = std::fabs(r); + const RATIOTYPE stepRatio = std::pow(m_GroupRatio, static_cast<RATIOTYPE>(1.0) / static_cast<RATIOTYPE>(m_GroupSize)); + m_RatioTable.resize(range.last - range.first + 1); + for(int32 i = range.first; i <= range.last; i++) + { + m_RatioTable[i - m_NoteMin] = std::pow(stepRatio, static_cast<RATIOTYPE>(i)); + } + UpdateFineStepTable(); + return true; +} + + +mpt::ustring CTuning::GetNoteName(const NOTEINDEXTYPE &x, bool addOctave) const +{ + if(!IsValidNote(x)) + { + return mpt::ustring(); + } + if(GetGroupSize() < 1) + { + const auto i = m_NoteNameMap.find(x); + if(i != m_NoteNameMap.end()) + return i->second; + else + return mpt::ufmt::val(x); + } + else + { + const NOTEINDEXTYPE pos = static_cast<NOTEINDEXTYPE>(mpt::wrapping_modulo(x, m_GroupSize)); + const NOTEINDEXTYPE middlePeriodNumber = 5; + mpt::ustring rValue; + const auto nmi = m_NoteNameMap.find(pos); + if(nmi != m_NoteNameMap.end()) + { + rValue = nmi->second; + if(addOctave) + { + rValue += mpt::ufmt::val(middlePeriodNumber + mpt::wrapping_divide(x, m_GroupSize)); + } + } + else + { + //By default, using notation nnP for notes; nn <-> note character starting + //from 'A' with char ':' as fill char, and P is period integer. For example: + //C:5, D:3, R:7 + if(m_GroupSize <= 26) + { + rValue = mpt::ToUnicode(mpt::Charset::UTF8, std::string(1, static_cast<char>(pos + 'A'))); + rValue += UL_(":"); + } else + { + rValue = mpt::ufmt::HEX0<1>(pos % 16) + mpt::ufmt::HEX0<1>((pos / 16) % 16); + if(pos > 0xff) + { + rValue = mpt::ToUnicode(mpt::Charset::UTF8, mpt::ToLowerCaseAscii(mpt::ToCharset(mpt::Charset::UTF8, rValue))); + } + } + if(addOctave) + { + rValue += mpt::ufmt::val(middlePeriodNumber + mpt::wrapping_divide(x, m_GroupSize)); + } + } + return rValue; + } +} + + +void CTuning::SetNoteName(const NOTEINDEXTYPE &n, const mpt::ustring &str) +{ + const NOTEINDEXTYPE pos = (GetGroupSize() < 1) ? n : static_cast<NOTEINDEXTYPE>(mpt::wrapping_modulo(n, m_GroupSize)); + if(!str.empty()) + { + m_NoteNameMap[pos] = str; + } else + { + const auto iter = m_NoteNameMap.find(pos); + if(iter != m_NoteNameMap.end()) + { + m_NoteNameMap.erase(iter); + } + } +} + + +// Without finetune +RATIOTYPE CTuning::GetRatio(const NOTEINDEXTYPE note) const +{ + if(!IsValidNote(note)) + { + return s_DefaultFallbackRatio; + } + const auto ratio = m_RatioTable[note - m_NoteMin]; + if(ratio <= 1e-15f) + { + return s_DefaultFallbackRatio; + } + return ratio; +} + + +// With finetune +RATIOTYPE CTuning::GetRatio(const NOTEINDEXTYPE baseNote, const STEPINDEXTYPE baseFineSteps) const +{ + const STEPINDEXTYPE fineStepCount = static_cast<STEPINDEXTYPE>(GetFineStepCount()); + if(fineStepCount == 0 || baseFineSteps == 0) + { + return GetRatio(static_cast<NOTEINDEXTYPE>(baseNote + baseFineSteps)); + } + + // If baseFineSteps is more than the number of finesteps between notes, note is increased. + // So first figuring out what note and fineStep values to actually use. + // Interpreting finestep==-1 on note x so that it is the same as finestep==fineStepCount on note x-1. + // Note: If fineStepCount is n, n+1 steps are needed to get to next note. + const NOTEINDEXTYPE note = static_cast<NOTEINDEXTYPE>(baseNote + mpt::wrapping_divide(baseFineSteps, (fineStepCount + 1))); + const STEPINDEXTYPE fineStep = mpt::wrapping_modulo(baseFineSteps, (fineStepCount + 1)); + if(!IsValidNote(note)) + { + return s_DefaultFallbackRatio; + } + if(fineStep == 0) + { + return m_RatioTable[note - m_NoteMin]; + } + + RATIOTYPE fineRatio = static_cast<RATIOTYPE>(1.0); + if(GetType() == Type::GEOMETRIC && m_RatioTableFine.size() > 0) + { + fineRatio = m_RatioTableFine[fineStep - 1]; + } else if(GetType() == Type::GROUPGEOMETRIC && m_RatioTableFine.size() > 0) + { + fineRatio = m_RatioTableFine[GetRefNote(note) * fineStepCount + fineStep - 1]; + } else + { + // Geometric finestepping + fineRatio = std::pow(GetRatio(note + 1) / GetRatio(note), static_cast<RATIOTYPE>(fineStep) / (fineStepCount + 1)); + } + return m_RatioTable[note - m_NoteMin] * fineRatio; +} + + +bool CTuning::SetRatio(const NOTEINDEXTYPE& s, const RATIOTYPE& r) +{ + if(GetType() != Type::GROUPGEOMETRIC && GetType() != Type::GENERAL) + { + return false; + } + //Creating ratio table if doesn't exist. + if(m_RatioTable.empty()) + { + m_RatioTable.assign(s_RatioTableSizeDefault, 1); + m_NoteMin = s_NoteMinDefault; + } + if(!IsValidNote(s)) + { + return false; + } + m_RatioTable[s - m_NoteMin] = std::fabs(r); + if(GetType() == Type::GROUPGEOMETRIC) + { // update other groups + for(NOTEINDEXTYPE n = m_NoteMin; n < m_NoteMin + static_cast<NOTEINDEXTYPE>(m_RatioTable.size()); ++n) + { + if(n == s) + { + // nothing + } else if(std::abs(n - s) % m_GroupSize == 0) + { + m_RatioTable[n - m_NoteMin] = std::pow(m_GroupRatio, static_cast<RATIOTYPE>(n - s) / static_cast<RATIOTYPE>(m_GroupSize)) * m_RatioTable[s - m_NoteMin]; + } + } + UpdateFineStepTable(); + } + return true; +} + + +void CTuning::SetFineStepCount(const USTEPINDEXTYPE& fs) +{ + m_FineStepCount = std::clamp(mpt::saturate_cast<STEPINDEXTYPE>(fs), STEPINDEXTYPE(0), FINESTEPCOUNT_MAX); + UpdateFineStepTable(); +} + + +void CTuning::UpdateFineStepTable() +{ + if(m_FineStepCount <= 0) + { + m_RatioTableFine.clear(); + return; + } + if(GetType() == Type::GEOMETRIC) + { + if(m_FineStepCount > s_RatioTableFineSizeMaxDefault) + { + m_RatioTableFine.clear(); + return; + } + m_RatioTableFine.resize(m_FineStepCount); + const RATIOTYPE q = GetRatio(GetNoteRange().first + 1) / GetRatio(GetNoteRange().first); + const RATIOTYPE rFineStep = std::pow(q, static_cast<RATIOTYPE>(1)/(m_FineStepCount+1)); + for(USTEPINDEXTYPE i = 1; i<=m_FineStepCount; i++) + m_RatioTableFine[i-1] = std::pow(rFineStep, static_cast<RATIOTYPE>(i)); + return; + } + if(GetType() == Type::GROUPGEOMETRIC) + { + const UNOTEINDEXTYPE p = GetGroupSize(); + if(p > s_RatioTableFineSizeMaxDefault / m_FineStepCount) + { + //In case fineratiotable would become too large, not using + //table for it. + m_RatioTableFine.clear(); + return; + } + else + { + //Creating 'geometric' finestepping between notes. + m_RatioTableFine.resize(p * m_FineStepCount); + const NOTEINDEXTYPE startnote = GetRefNote(GetNoteRange().first); + for(UNOTEINDEXTYPE i = 0; i<p; i++) + { + const NOTEINDEXTYPE refnote = GetRefNote(startnote+i); + const RATIOTYPE rFineStep = std::pow(GetRatio(refnote+1) / GetRatio(refnote), static_cast<RATIOTYPE>(1)/(m_FineStepCount+1)); + for(UNOTEINDEXTYPE j = 1; j<=m_FineStepCount; j++) + { + m_RatioTableFine[m_FineStepCount * refnote + (j-1)] = std::pow(rFineStep, static_cast<RATIOTYPE>(j)); + } + } + return; + } + + } + if(GetType() == Type::GENERAL) + { + //Not using table with tuning of type general. + m_RatioTableFine.clear(); + return; + } + + //Should not reach here. + m_RatioTableFine.clear(); + m_FineStepCount = 0; +} + + +bool CTuning::Multiply(const RATIOTYPE r) +{ + if(!IsValidRatio(r)) + { + return false; + } + for(auto & ratio : m_RatioTable) + { + ratio *= r; + } + return true; +} + + +bool CTuning::ChangeGroupsize(const NOTEINDEXTYPE& s) +{ + if(s < 1) + return false; + + if(m_TuningType == Type::GROUPGEOMETRIC) + return CreateGroupGeometric(s, GetGroupRatio(), 0); + + if(m_TuningType == Type::GEOMETRIC) + return CreateGeometric(s, GetGroupRatio()); + + return false; +} + + +bool CTuning::ChangeGroupRatio(const RATIOTYPE& r) +{ + if(!IsValidRatio(r)) + return false; + + if(m_TuningType == Type::GROUPGEOMETRIC) + return CreateGroupGeometric(GetGroupSize(), r, 0); + + if(m_TuningType == Type::GEOMETRIC) + return CreateGeometric(GetGroupSize(), r); + + return false; +} + + +SerializationResult CTuning::InitDeserialize(std::istream &iStrm, mpt::Charset defaultCharset) +{ + // Note: OpenMPT since at least r323 writes version number (4<<24)+4 while it + // reads version number (5<<24)+4 or earlier. + // We keep this behaviour. + + if(iStrm.fail()) + return SerializationResult::Failure; + + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead("CTB244RTI", (5 << 24) + 4); // version + int8 use_utf8 = 0; + ssb.ReadItem(use_utf8, "UTF8"); + const mpt::Charset charset = use_utf8 ? mpt::Charset::UTF8 : defaultCharset; + ssb.ReadItem(m_TuningName, "0", [charset](std::istream &iStrm, mpt::ustring &ustr, const std::size_t dummy){ return ReadStr(iStrm, ustr, dummy, charset); }); + uint16 dummyEditMask = 0xffff; + ssb.ReadItem(dummyEditMask, "1"); + std::underlying_type<Type>::type type = 0; + ssb.ReadItem(type, "2"); + m_TuningType = static_cast<Type>(type); + ssb.ReadItem(m_NoteNameMap, "3", [charset](std::istream &iStrm, std::map<NOTEINDEXTYPE, mpt::ustring> &m, const std::size_t dummy){ return ReadNoteMap(iStrm, m, dummy, charset); }); + ssb.ReadItem(m_FineStepCount, "4"); + + // RTI entries. + ssb.ReadItem(m_RatioTable, "RTI0", ReadRatioTable); + ssb.ReadItem(m_NoteMin, "RTI1"); + ssb.ReadItem(m_GroupSize, "RTI2"); + ssb.ReadItem(m_GroupRatio, "RTI3"); + UNOTEINDEXTYPE ratiotableSize = 0; + ssb.ReadItem(ratiotableSize, "RTI4"); + + m_GroupRatio = SanitizeGroupRatio(m_GroupRatio); + if(!std::isfinite(m_GroupRatio)) + { + return SerializationResult::Failure; + } + for(auto ratio : m_RatioTable) + { + if(!std::isfinite(ratio)) + return SerializationResult::Failure; + } + + // If reader status is ok and m_NoteMin is somewhat reasonable, process data. + if(!((ssb.GetStatus() & srlztn::SNT_FAILURE) == 0 && m_NoteMin >= -300 && m_NoteMin <= 300)) + { + return SerializationResult::Failure; + } + + // reject unknown types + if(m_TuningType != Type::GENERAL && m_TuningType != Type::GROUPGEOMETRIC && m_TuningType != Type::GEOMETRIC) + { + return SerializationResult::Failure; + } + if(m_GroupSize < 0) + { + return SerializationResult::Failure; + } + m_FineStepCount = std::clamp(mpt::saturate_cast<STEPINDEXTYPE>(m_FineStepCount), STEPINDEXTYPE(0), FINESTEPCOUNT_MAX); + if(m_RatioTable.size() > static_cast<size_t>(NOTEINDEXTYPE_MAX)) + { + return SerializationResult::Failure; + } + if((GetType() == Type::GROUPGEOMETRIC) || (GetType() == Type::GEOMETRIC)) + { + if(ratiotableSize < 1 || ratiotableSize > NOTEINDEXTYPE_MAX) + { + return SerializationResult::Failure; + } + if(GetType() == Type::GEOMETRIC) + { + if(!CreateGeometric(GetGroupSize(), GetGroupRatio(), NoteRange{m_NoteMin, static_cast<NOTEINDEXTYPE>(m_NoteMin + ratiotableSize - 1)})) + { + return SerializationResult::Failure; + } + } else + { + if(!CreateGroupGeometric(m_RatioTable, GetGroupRatio(), NoteRange{m_NoteMin, static_cast<NOTEINDEXTYPE>(m_NoteMin + ratiotableSize - 1)}, m_NoteMin)) + { + return SerializationResult::Failure; + } + } + } else + { + UpdateFineStepTable(); + } + return SerializationResult::Success; +} + + +template<class T, class SIZETYPE, class Tdst> +static bool VectorFromBinaryStream(std::istream& inStrm, std::vector<Tdst>& v, const SIZETYPE maxSize = (std::numeric_limits<SIZETYPE>::max)()) +{ + if(!inStrm.good()) + return false; + + SIZETYPE size = 0; + mpt::IO::ReadIntLE<SIZETYPE>(inStrm, size); + + if(size > maxSize) + return false; + + v.resize(size); + for(std::size_t i = 0; i<size; i++) + { + T tmp = T(); + mpt::IO::Read(inStrm, tmp); + v[i] = tmp; + } + + return inStrm.good(); +} + + +SerializationResult CTuning::InitDeserializeOLD(std::istream &inStrm, mpt::Charset defaultCharset) +{ + if(!inStrm.good()) + return SerializationResult::Failure; + + const std::streamoff startPos = inStrm.tellg(); + + //First checking is there expected begin sequence. + char begin[8]; + MemsetZero(begin); + inStrm.read(begin, sizeof(begin)); + if(std::memcmp(begin, "CTRTI_B.", 8)) + { + //Returning stream position if beginmarker was not found. + inStrm.seekg(startPos); + return SerializationResult::Failure; + } + + //Version + int16 version = 0; + mpt::IO::ReadIntLE<int16>(inStrm, version); + if(version != 2 && version != 3) + return SerializationResult::Failure; + + char begin2[8]; + MemsetZero(begin2); + inStrm.read(begin2, sizeof(begin2)); + if(std::memcmp(begin2, "CT<sfs>B", 8)) + { + return SerializationResult::Failure; + } + + int16 version2 = 0; + mpt::IO::ReadIntLE<int16>(inStrm, version2); + if(version2 != 3 && version2 != 4) + { + return SerializationResult::Failure; + } + + //Tuning name + if(version2 <= 3) + { + std::string tmpName; + if(!mpt::IO::ReadSizedStringLE<uint32>(inStrm, tmpName, 0xffff)) + { + return SerializationResult::Failure; + } + m_TuningName = mpt::ToUnicode(defaultCharset, tmpName); + } else + { + std::string tmpName; + if(!mpt::IO::ReadSizedStringLE<uint8>(inStrm, tmpName)) + { + return SerializationResult::Failure; + } + m_TuningName = mpt::ToUnicode(defaultCharset, tmpName); + } + + //Const mask + int16 em = 0; + mpt::IO::ReadIntLE<int16>(inStrm, em); + + //Tuning type + int16 tt = 0; + mpt::IO::ReadIntLE<int16>(inStrm, tt); + m_TuningType = static_cast<Type>(tt); + + //Notemap + uint16 size = 0; + if(version2 <= 3) + { + uint32 tempsize = 0; + mpt::IO::ReadIntLE<uint32>(inStrm, tempsize); + if(tempsize > 0xffff) + { + return SerializationResult::Failure; + } + size = mpt::saturate_cast<uint16>(tempsize); + } else + { + mpt::IO::ReadIntLE<uint16>(inStrm, size); + } + for(UNOTEINDEXTYPE i = 0; i<size; i++) + { + std::string str; + int16 n = 0; + mpt::IO::ReadIntLE<int16>(inStrm, n); + if(version2 <= 3) + { + if(!mpt::IO::ReadSizedStringLE<uint32>(inStrm, str, 0xffff)) + { + return SerializationResult::Failure; + } + } else + { + if(!mpt::IO::ReadSizedStringLE<uint8>(inStrm, str)) + { + return SerializationResult::Failure; + } + } + m_NoteNameMap[n] = mpt::ToUnicode(defaultCharset, str); + } + + //End marker + char end2[8]; + MemsetZero(end2); + inStrm.read(end2, sizeof(end2)); + if(std::memcmp(end2, "CT<sfs>E", 8)) + { + return SerializationResult::Failure; + } + + // reject unknown types + if(m_TuningType != Type::GENERAL && m_TuningType != Type::GROUPGEOMETRIC && m_TuningType != Type::GEOMETRIC) + { + return SerializationResult::Failure; + } + + //Ratiotable + if(version <= 2) + { + if(!VectorFromBinaryStream<IEEE754binary32LE, uint32>(inStrm, m_RatioTable, 0xffff)) + { + return SerializationResult::Failure; + } + } else + { + if(!VectorFromBinaryStream<IEEE754binary32LE, uint16>(inStrm, m_RatioTable)) + { + return SerializationResult::Failure; + } + } + for(auto ratio : m_RatioTable) + { + if(!std::isfinite(ratio)) + return SerializationResult::Failure; + } + + //Fineratios + if(version <= 2) + { + if(!VectorFromBinaryStream<IEEE754binary32LE, uint32>(inStrm, m_RatioTableFine, 0xffff)) + { + return SerializationResult::Failure; + } + } else + { + if(!VectorFromBinaryStream<IEEE754binary32LE, uint16>(inStrm, m_RatioTableFine)) + { + return SerializationResult::Failure; + } + } + for(auto ratio : m_RatioTableFine) + { + if(!std::isfinite(ratio)) + return SerializationResult::Failure; + } + m_FineStepCount = mpt::saturate_cast<USTEPINDEXTYPE>(m_RatioTableFine.size()); + + // m_NoteMin + int16 notemin = 0; + mpt::IO::ReadIntLE<int16>(inStrm, notemin); + m_NoteMin = notemin; + if(m_NoteMin < -200 || m_NoteMin > 200) + { + return SerializationResult::Failure; + } + + //m_GroupSize + int16 groupsize = 0; + mpt::IO::ReadIntLE<int16>(inStrm, groupsize); + m_GroupSize = groupsize; + if(m_GroupSize < 0) + { + return SerializationResult::Failure; + } + + //m_GroupRatio + IEEE754binary32LE groupratio = IEEE754binary32LE(0.0f); + mpt::IO::Read(inStrm, groupratio); + m_GroupRatio = SanitizeGroupRatio(groupratio); + if(!std::isfinite(m_GroupRatio)) + { + return SerializationResult::Failure; + } + + char end[8]; + MemsetZero(end); + inStrm.read(reinterpret_cast<char*>(&end), sizeof(end)); + if(std::memcmp(end, "CTRTI_E.", 8)) + { + return SerializationResult::Failure; + } + + // reject corrupt tunings + if(m_RatioTable.size() > static_cast<std::size_t>(NOTEINDEXTYPE_MAX)) + { + return SerializationResult::Failure; + } + if((m_GroupSize <= 0 || m_GroupRatio <= 0) && m_TuningType != Type::GENERAL) + { + return SerializationResult::Failure; + } + if(m_TuningType == Type::GROUPGEOMETRIC || m_TuningType == Type::GEOMETRIC) + { + if(m_RatioTable.size() < static_cast<std::size_t>(m_GroupSize)) + { + return SerializationResult::Failure; + } + } + + // convert old finestepcount + if(m_FineStepCount > 0) + { + m_FineStepCount -= 1; + } + m_FineStepCount = std::clamp(mpt::saturate_cast<STEPINDEXTYPE>(m_FineStepCount), STEPINDEXTYPE(0), FINESTEPCOUNT_MAX); + UpdateFineStepTable(); + + if(m_TuningType == Type::GEOMETRIC) + { + // Convert old geometric to new groupgeometric because old geometric tunings + // can have ratio(0) != 1.0, which would get lost when saving nowadays. + if(mpt::saturate_cast<NOTEINDEXTYPE>(m_RatioTable.size()) >= m_GroupSize - m_NoteMin) + { + std::vector<RATIOTYPE> ratios; + for(NOTEINDEXTYPE n = 0; n < m_GroupSize; ++n) + { + ratios.push_back(m_RatioTable[n - m_NoteMin]); + } + CreateGroupGeometric(ratios, m_GroupRatio, GetNoteRange(), 0); + } + } + + return SerializationResult::Success; +} + + +Tuning::SerializationResult CTuning::Serialize(std::ostream& outStrm) const +{ + // Note: OpenMPT since at least r323 writes version number (4<<24)+4 while it + // reads version number (5<<24)+4. + // We keep this behaviour. + srlztn::SsbWrite ssb(outStrm); + ssb.BeginWrite("CTB244RTI", (4 << 24) + 4); // version + ssb.WriteItem(int8(1), "UTF8"); + if (m_TuningName.length() > 0) + ssb.WriteItem(m_TuningName, "0", WriteStr); + uint16 dummyEditMask = 0xffff; + ssb.WriteItem(dummyEditMask, "1"); + ssb.WriteItem(mpt::to_underlying(m_TuningType), "2"); + if (m_NoteNameMap.size() > 0) + ssb.WriteItem(m_NoteNameMap, "3", WriteNoteMap); + if (GetFineStepCount() > 0) + ssb.WriteItem(m_FineStepCount, "4"); + + const Tuning::Type tt = GetType(); + if (GetGroupRatio() > 0) + ssb.WriteItem(m_GroupRatio, "RTI3"); + if (tt == Type::GROUPGEOMETRIC) + ssb.WriteItem(m_RatioTable, "RTI0", RatioWriter(GetGroupSize())); + if (tt == Type::GENERAL) + ssb.WriteItem(m_RatioTable, "RTI0", RatioWriter()); + if (tt == Type::GEOMETRIC) + ssb.WriteItem(m_GroupSize, "RTI2"); + + if(tt == Type::GEOMETRIC || tt == Type::GROUPGEOMETRIC) + { //For Groupgeometric this data is the number of ratios in ratiotable. + UNOTEINDEXTYPE ratiotableSize = static_cast<UNOTEINDEXTYPE>(m_RatioTable.size()); + ssb.WriteItem(ratiotableSize, "RTI4"); + } + + // m_NoteMin + ssb.WriteItem(m_NoteMin, "RTI1"); + + ssb.FinishWrite(); + + return ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) ? Tuning::SerializationResult::Failure : Tuning::SerializationResult::Success; +} + + +#ifdef MODPLUG_TRACKER + +bool CTuning::WriteSCL(std::ostream &f, const mpt::PathString &filename) const +{ + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT("! {}")(mpt::ToCharset(mpt::Charset::ISO8859_1, (filename.GetFileName() + filename.GetFileExt()).ToUnicode()))); + mpt::IO::WriteTextCRLF(f, "!"); + std::string name = mpt::ToCharset(mpt::Charset::ISO8859_1, GetName()); + for(auto & c : name) { if(static_cast<uint8>(c) < 32) c = ' '; } // remove control characters + if(name.length() >= 1 && name[0] == '!') name[0] = '?'; // do not confuse description with comment + mpt::IO::WriteTextCRLF(f, name); + if(GetType() == Type::GEOMETRIC) + { + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {}")(m_GroupSize)); + mpt::IO::WriteTextCRLF(f, "!"); + for(NOTEINDEXTYPE n = 0; n < m_GroupSize; ++n) + { + double ratio = std::pow(static_cast<double>(m_GroupRatio), static_cast<double>(n + 1) / static_cast<double>(m_GroupSize)); + double cents = std::log2(ratio) * 1200.0; + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {} ! {}")( + mpt::afmt::fix(cents), + mpt::ToCharset(mpt::Charset::ISO8859_1, GetNoteName((n + 1) % m_GroupSize, false)) + )); + } + } else if(GetType() == Type::GROUPGEOMETRIC) + { + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {}")(m_GroupSize)); + mpt::IO::WriteTextCRLF(f, "!"); + for(NOTEINDEXTYPE n = 0; n < m_GroupSize; ++n) + { + bool last = (n == (m_GroupSize - 1)); + double baseratio = static_cast<double>(GetRatio(0)); + double ratio = static_cast<double>(last ? m_GroupRatio : GetRatio(n + 1)) / baseratio; + double cents = std::log2(ratio) * 1200.0; + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {} ! {}")( + mpt::afmt::fix(cents), + mpt::ToCharset(mpt::Charset::ISO8859_1, GetNoteName((n + 1) % m_GroupSize, false)) + )); + } + } else if(GetType() == Type::GENERAL) + { + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {}")(m_RatioTable.size() + 1)); + mpt::IO::WriteTextCRLF(f, "!"); + double baseratio = 1.0; + for(NOTEINDEXTYPE n = 0; n < mpt::saturate_cast<NOTEINDEXTYPE>(m_RatioTable.size()); ++n) + { + baseratio = std::min(baseratio, static_cast<double>(m_RatioTable[n])); + } + for(NOTEINDEXTYPE n = 0; n < mpt::saturate_cast<NOTEINDEXTYPE>(m_RatioTable.size()); ++n) + { + double ratio = static_cast<double>(m_RatioTable[n]) / baseratio; + double cents = std::log2(ratio) * 1200.0; + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {} ! {}")( + mpt::afmt::fix(cents), + mpt::ToCharset(mpt::Charset::ISO8859_1, GetNoteName(n + m_NoteMin, false)) + )); + } + mpt::IO::WriteTextCRLF(f, MPT_AFORMAT(" {} ! {}")( + mpt::afmt::val(1), + std::string() + )); + } else + { + return false; + } + return true; +} + +#endif + + +namespace CTuningS11n +{ + +void RatioWriter::operator()(std::ostream& oStrm, const std::vector<float>& v) +{ + const std::size_t nWriteCount = std::min(v.size(), static_cast<std::size_t>(m_nWriteCount)); + mpt::IO::WriteAdaptiveInt64LE(oStrm, nWriteCount); + for(size_t i = 0; i < nWriteCount; i++) + mpt::IO::Write(oStrm, IEEE754binary32LE(v[i])); +} + + +void ReadNoteMap(std::istream &iStrm, std::map<NOTEINDEXTYPE, mpt::ustring> &m, const std::size_t dummy, mpt::Charset charset) +{ + MPT_UNREFERENCED_PARAMETER(dummy); + uint64 val; + mpt::IO::ReadAdaptiveInt64LE(iStrm, val); + LimitMax(val, 256u); // Read 256 at max. + for(size_t i = 0; i < val; i++) + { + int16 key; + mpt::IO::ReadIntLE<int16>(iStrm, key); + std::string str; + mpt::IO::ReadSizedStringLE<uint8>(iStrm, str); + m[key] = mpt::ToUnicode(charset, str); + } +} + + +void ReadRatioTable(std::istream& iStrm, std::vector<RATIOTYPE>& v, const size_t) +{ + uint64 val; + mpt::IO::ReadAdaptiveInt64LE(iStrm, val); + v.resize(std::min(mpt::saturate_cast<std::size_t>(val), std::size_t(256))); // Read 256 vals at max. + for(size_t i = 0; i < v.size(); i++) + { + IEEE754binary32LE tmp(0.0f); + mpt::IO::Read(iStrm, tmp); + v[i] = tmp; + } +} + + +void ReadStr(std::istream &iStrm, mpt::ustring &ustr, const std::size_t dummy, mpt::Charset charset) +{ + MPT_UNREFERENCED_PARAMETER(dummy); + std::string str; + uint64 val; + mpt::IO::ReadAdaptiveInt64LE(iStrm, val); + size_t nSize = (val > 255) ? 255 : static_cast<size_t>(val); // Read 255 characters at max. + str.clear(); + str.resize(nSize); + for(size_t i = 0; i < nSize; i++) + mpt::IO::ReadIntLE(iStrm, str[i]); + if(str.find_first_of('\0') != std::string::npos) + { // trim \0 at the end + str.resize(str.find_first_of('\0')); + } + ustr = mpt::ToUnicode(charset, str); +} + + +void WriteNoteMap(std::ostream &oStrm, const std::map<NOTEINDEXTYPE, mpt::ustring> &m) +{ + mpt::IO::WriteAdaptiveInt64LE(oStrm, m.size()); + for(auto &mi : m) + { + mpt::IO::WriteIntLE<int16>(oStrm, mi.first); + mpt::IO::WriteSizedStringLE<uint8>(oStrm, mpt::ToCharset(mpt::Charset::UTF8, mi.second)); + } +} + + +void WriteStr(std::ostream &oStrm, const mpt::ustring &ustr) +{ + std::string str = mpt::ToCharset(mpt::Charset::UTF8, ustr); + mpt::IO::WriteAdaptiveInt64LE(oStrm, str.size()); + oStrm.write(str.c_str(), str.size()); +} + +} // namespace CTuningS11n. + + +} // namespace Tuning + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/tuning.h b/Src/external_dependencies/openmpt-trunk/soundlib/tuning.h new file mode 100644 index 00000000..c6dc90a6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/tuning.h @@ -0,0 +1,252 @@ +/* + * tuning.h + * -------- + * Purpose: Alternative sample tuning. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <map> + +#include "tuningbase.h" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Tuning { + + +class CTuning +{ + +public: + + static constexpr char s_FileExtension[5] = ".tun"; + + static constexpr RATIOTYPE s_DefaultFallbackRatio = 1.0f; + static constexpr NOTEINDEXTYPE s_NoteMinDefault = -64; + static constexpr UNOTEINDEXTYPE s_RatioTableSizeDefault = 128; + static constexpr USTEPINDEXTYPE s_RatioTableFineSizeMaxDefault = 1000; + +public: + + // To return ratio of certain note. + RATIOTYPE GetRatio(const NOTEINDEXTYPE note) const; + + // To return ratio from a 'step'(noteindex + stepindex) + RATIOTYPE GetRatio(const NOTEINDEXTYPE baseNote, const STEPINDEXTYPE baseFineSteps) const; + + //Tuning might not be valid for arbitrarily large range, + //so this can be used to ask where it is valid. Tells the lowest and highest + //note that are valid. + MPT_FORCEINLINE NoteRange GetNoteRange() const + { + return NoteRange{m_NoteMin, static_cast<NOTEINDEXTYPE>(m_NoteMin + static_cast<NOTEINDEXTYPE>(m_RatioTable.size()) - 1)}; + } + + // Return true if note is within note range + MPT_FORCEINLINE bool IsValidNote(const NOTEINDEXTYPE n) const + { + return (GetNoteRange().first <= n && n <= GetNoteRange().last); + } + + MPT_FORCEINLINE UNOTEINDEXTYPE GetGroupSize() const + { + return m_GroupSize; + } + + RATIOTYPE GetGroupRatio() const {return m_GroupRatio;} + + // To return (fine)stepcount between two consecutive mainsteps. + MPT_FORCEINLINE USTEPINDEXTYPE GetFineStepCount() const + { + return m_FineStepCount; + } + + //To return 'directed distance' between given notes. + STEPINDEXTYPE GetStepDistance(const NOTEINDEXTYPE& from, const NOTEINDEXTYPE& to) const + {return (to - from)*(static_cast<NOTEINDEXTYPE>(GetFineStepCount())+1);} + + //To return 'directed distance' between given steps. + STEPINDEXTYPE GetStepDistance(const NOTEINDEXTYPE& noteFrom, const STEPINDEXTYPE& stepDistFrom, const NOTEINDEXTYPE& noteTo, const STEPINDEXTYPE& stepDistTo) const + {return GetStepDistance(noteFrom, noteTo) + stepDistTo - stepDistFrom;} + + //To set finestepcount between two consecutive mainsteps. + //Finestep count == 0 means that + //stepdistances become the same as note distances. + void SetFineStepCount(const USTEPINDEXTYPE& fs); + + // Multiply all ratios by given number. + bool Multiply(const RATIOTYPE r); + + bool SetRatio(const NOTEINDEXTYPE& s, const RATIOTYPE& r); + + MPT_FORCEINLINE Tuning::Type GetType() const + { + return m_TuningType; + } + + mpt::ustring GetNoteName(const NOTEINDEXTYPE &x, bool addOctave = true) const; + + void SetNoteName(const NOTEINDEXTYPE &, const mpt::ustring &); + + static std::unique_ptr<CTuning> CreateDeserialize(std::istream &f, mpt::Charset defaultCharset) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + if(pT->InitDeserialize(f, defaultCharset) != SerializationResult::Success) + { + return nullptr; + } + return pT; + } + + //Try to read old version (v.3) and return pointer to new instance if succesfull, else nullptr. + static std::unique_ptr<CTuning> CreateDeserializeOLD(std::istream &f, mpt::Charset defaultCharset) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + if(pT->InitDeserializeOLD(f, defaultCharset) != SerializationResult::Success) + { + return nullptr; + } + return pT; + } + + static std::unique_ptr<CTuning> CreateGeneral(const mpt::ustring &name) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + pT->SetName(name); + return pT; + } + + static std::unique_ptr<CTuning> CreateGroupGeometric(const mpt::ustring &name, UNOTEINDEXTYPE groupsize, RATIOTYPE groupratio, USTEPINDEXTYPE finestepcount) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + pT->SetName(name); + if(!pT->CreateGroupGeometric(groupsize, groupratio, 0)) + { + return nullptr; + } + pT->SetFineStepCount(finestepcount); + return pT; + } + + static std::unique_ptr<CTuning> CreateGroupGeometric(const mpt::ustring &name, const std::vector<RATIOTYPE> &ratios, RATIOTYPE groupratio, USTEPINDEXTYPE finestepcount) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + pT->SetName(name); + NoteRange range = NoteRange{s_NoteMinDefault, static_cast<NOTEINDEXTYPE>(s_NoteMinDefault + s_RatioTableSizeDefault - 1)}; + range.last = std::max(range.last, mpt::saturate_cast<NOTEINDEXTYPE>(ratios.size() - 1)); + range.first = 0 - range.last - 1; + if(!pT->CreateGroupGeometric(ratios, groupratio, range, 0)) + { + return nullptr; + } + pT->SetFineStepCount(finestepcount); + return pT; + } + + static std::unique_ptr<CTuning> CreateGeometric(const mpt::ustring &name, UNOTEINDEXTYPE groupsize, RATIOTYPE groupratio, USTEPINDEXTYPE finestepcount) + { + std::unique_ptr<CTuning> pT = std::unique_ptr<CTuning>(new CTuning()); + pT->SetName(name); + if(!pT->CreateGeometric(groupsize, groupratio)) + { + return nullptr; + } + pT->SetFineStepCount(finestepcount); + return pT; + } + + Tuning::SerializationResult Serialize(std::ostream& out) const; + +#ifdef MODPLUG_TRACKER + bool WriteSCL(std::ostream &f, const mpt::PathString &filename) const; +#endif + + bool ChangeGroupsize(const NOTEINDEXTYPE&); + bool ChangeGroupRatio(const RATIOTYPE&); + + void SetName(const mpt::ustring &s) + { + m_TuningName = s; + } + + mpt::ustring GetName() const + { + return m_TuningName; + } + +private: + + CTuning(); + + SerializationResult InitDeserialize(std::istream &inStrm, mpt::Charset defaultCharset); + + //Try to read old version (v.3) and return pointer to new instance if succesfull, else nullptr. + SerializationResult InitDeserializeOLD(std::istream &inStrm, mpt::Charset defaultCharset); + + //Create GroupGeometric tuning of *this using virtual ProCreateGroupGeometric. + bool CreateGroupGeometric(const std::vector<RATIOTYPE> &v, const RATIOTYPE &r, const NoteRange &range, const NOTEINDEXTYPE &ratiostartpos); + + //Create GroupGeometric of *this using ratios from 'itself' and ratios starting from + //position given as third argument. + bool CreateGroupGeometric(const NOTEINDEXTYPE &s, const RATIOTYPE &r, const NOTEINDEXTYPE &startindex); + + //Create geometric tuning of *this using ratio(0) = 1. + bool CreateGeometric(const UNOTEINDEXTYPE &p, const RATIOTYPE &r); + bool CreateGeometric(const UNOTEINDEXTYPE &s, const RATIOTYPE &r, const NoteRange &range); + + void UpdateFineStepTable(); + + // GroupPeriodic-specific. + // Get the corresponding note in [0, period-1]. + // For example GetRefNote(-1) is to return note :'groupsize-1'. + MPT_FORCEINLINE NOTEINDEXTYPE GetRefNote(NOTEINDEXTYPE note) const + { + MPT_ASSERT(GetType() == Type::GROUPGEOMETRIC || GetType() == Type::GEOMETRIC); + return static_cast<NOTEINDEXTYPE>(mpt::wrapping_modulo(note, GetGroupSize())); + } + + static bool IsValidRatio(RATIOTYPE ratio) + { + return (ratio > static_cast<RATIOTYPE>(0.0)); + } + +private: + + Tuning::Type m_TuningType; + + //Noteratios + std::vector<RATIOTYPE> m_RatioTable; + + //'Fineratios' + std::vector<RATIOTYPE> m_RatioTableFine; + + // The lowest index of note in the table + NOTEINDEXTYPE m_NoteMin; + + //For groupgeometric tunings, tells the 'group size' and 'group ratio' + //m_GroupSize should always be >= 0. + NOTEINDEXTYPE m_GroupSize; + RATIOTYPE m_GroupRatio; + + USTEPINDEXTYPE m_FineStepCount; // invariant: 0 <= m_FineStepCount <= FINESTEPCOUNT_MAX + + mpt::ustring m_TuningName; + + std::map<NOTEINDEXTYPE, mpt::ustring> m_NoteNameMap; + +}; // class CTuning + + +} // namespace Tuning + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/tuningCollection.cpp b/Src/external_dependencies/openmpt-trunk/soundlib/tuningCollection.cpp new file mode 100644 index 00000000..f11260b2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/tuningCollection.cpp @@ -0,0 +1,320 @@ +/* + * tuningCollection.cpp + * -------------------- + * Purpose: Alternative sample tuning collection class. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "tuningcollection.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "../common/serialization_utils.h" +#include <algorithm> +#include "../common/mptFileIO.h" +#include "Loaders.h" +#ifdef MODPLUG_TRACKER +#include "../mptrack/TrackerSettings.h" +#endif //MODPLUG_TRACKER + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Tuning { + + +/* +Version history: + 2->3: Serialization revamp(August 2007) + 1->2: Sizetypes of string serialisation from size_t(uint32) + to uint8. (March 2007) +*/ + + +namespace CTuningS11n +{ + void ReadStr(std::istream &iStrm, mpt::ustring &ustr, const std::size_t dummy, mpt::Charset charset); + void WriteStr(std::ostream &oStrm, const mpt::ustring &ustr); +} // namespace CTuningS11n + +using namespace CTuningS11n; + + +static void ReadTuning(std::istream &iStrm, CTuningCollection &Tc, const std::size_t dummy, mpt::Charset defaultCharset) +{ + MPT_UNREFERENCED_PARAMETER(dummy); + Tc.AddTuning(iStrm, defaultCharset); +} + +static void WriteTuning(std::ostream& oStrm, const CTuning& t) +{ + t.Serialize(oStrm); +} + + +CTuning* CTuningCollection::GetTuning(const mpt::ustring &name) +{ + for(std::size_t i = 0; i<m_Tunings.size(); i++) + { + if(m_Tunings[i]->GetName() == name) + { + return m_Tunings[i].get(); + } + } + return nullptr; +} + +const CTuning* CTuningCollection::GetTuning(const mpt::ustring &name) const +{ + for(std::size_t i = 0; i<m_Tunings.size(); i++) + { + if(m_Tunings[i]->GetName() == name) + { + return m_Tunings[i].get(); + } + } + return nullptr; +} + + +Tuning::SerializationResult CTuningCollection::Serialize(std::ostream& oStrm, const mpt::ustring &name) const +{ + srlztn::SsbWrite ssb(oStrm); + ssb.BeginWrite("TC", 3); // version + ssb.WriteItem(int8(1), "UTF8"); + ssb.WriteItem(name, "0", &WriteStr); + uint16 dummyEditMask = 0xffff; + ssb.WriteItem(dummyEditMask, "1"); + + const size_t tcount = m_Tunings.size(); + for(size_t i = 0; i<tcount; i++) + ssb.WriteItem(*m_Tunings[i], "2", &WriteTuning); + ssb.FinishWrite(); + + if(ssb.GetStatus() & srlztn::SNT_FAILURE) + return Tuning::SerializationResult::Failure; + else + return Tuning::SerializationResult::Success; +} + + +Tuning::SerializationResult CTuningCollection::Deserialize(std::istream &iStrm, mpt::ustring &name, mpt::Charset defaultCharset) +{ + std::istream::pos_type startpos = iStrm.tellg(); + + const Tuning::SerializationResult oldLoadingResult = DeserializeOLD(iStrm, name, defaultCharset); + + if(oldLoadingResult == Tuning::SerializationResult::NoMagic) + { // An old version was not recognised - trying new version. + iStrm.clear(); + iStrm.seekg(startpos); + srlztn::SsbRead ssb(iStrm); + ssb.BeginRead("TC", 3); // version + int8 use_utf8 = 0; + ssb.ReadItem(use_utf8, "UTF8"); + const mpt::Charset charset = use_utf8 ? mpt::Charset::UTF8 : defaultCharset; + + const srlztn::SsbRead::ReadIterator iterBeg = ssb.GetReadBegin(); + const srlztn::SsbRead::ReadIterator iterEnd = ssb.GetReadEnd(); + for(srlztn::SsbRead::ReadIterator iter = iterBeg; iter != iterEnd; iter++) + { + uint16 dummyEditMask = 0xffff; + if (ssb.CompareId(iter, "0") == srlztn::SsbRead::IdMatch) + ssb.ReadIterItem(iter, name, [charset](std::istream &iStrm, mpt::ustring &ustr, const std::size_t dummy){ return ReadStr(iStrm, ustr, dummy, charset); }); + else if (ssb.CompareId(iter, "1") == srlztn::SsbRead::IdMatch) + ssb.ReadIterItem(iter, dummyEditMask); + else if (ssb.CompareId(iter, "2") == srlztn::SsbRead::IdMatch) + ssb.ReadIterItem(iter, *this, [charset](std::istream &iStrm, CTuningCollection &Tc, const std::size_t dummy){ return ReadTuning(iStrm, Tc, dummy, charset); }); + } + + if(ssb.GetStatus() & srlztn::SNT_FAILURE) + return Tuning::SerializationResult::Failure; + else + return Tuning::SerializationResult::Success; + } + else + { + return oldLoadingResult; + } +} + + +Tuning::SerializationResult CTuningCollection::DeserializeOLD(std::istream &inStrm, mpt::ustring &uname, mpt::Charset defaultCharset) +{ + + //1. begin marker: + uint32 beginMarker = 0; + mpt::IO::ReadIntLE<uint32>(inStrm, beginMarker); + if(beginMarker != MagicBE("TCSH")) // Magic is reversed in file, hence BE + return Tuning::SerializationResult::NoMagic; + + //2. version + int32 version = 0; + mpt::IO::ReadIntLE<int32>(inStrm, version); + if(version > 2 || version < 1) + return Tuning::SerializationResult::Failure; + + //3. Name + if(version < 2) + { + std::string name; + if(!mpt::IO::ReadSizedStringLE<uint32>(inStrm, name, 256)) + return Tuning::SerializationResult::Failure; + uname = mpt::ToUnicode(defaultCharset, name); + } + else + { + std::string name; + if(!mpt::IO::ReadSizedStringLE<uint8>(inStrm, name)) + return Tuning::SerializationResult::Failure; + uname = mpt::ToUnicode(defaultCharset, name); + } + + //4. Editmask + int16 em = 0; + mpt::IO::ReadIntLE<int16>(inStrm, em); + //Not assigning the value yet, for if it sets some property const, + //further loading might fail. + + //5. Tunings + { + uint32 s = 0; + mpt::IO::ReadIntLE<uint32>(inStrm, s); + if(s > 50) + return Tuning::SerializationResult::Failure; + for(size_t i = 0; i<s; i++) + { + if(!AddTuning(inStrm, defaultCharset)) + { + return Tuning::SerializationResult::Failure; + } + } + } + + //6. End marker + uint32 endMarker = 0; + mpt::IO::ReadIntLE<uint32>(inStrm, endMarker); + if(endMarker != MagicBE("TCSF")) // Magic is reversed in file, hence BE + return Tuning::SerializationResult::Failure; + + return Tuning::SerializationResult::Success; +} + + + +bool CTuningCollection::Remove(const CTuning *pT) +{ + const auto it = std::find_if(m_Tunings.begin(), m_Tunings.end(), + [&] (const std::unique_ptr<CTuning> & upT) -> bool + { + return upT.get() == pT; + } + ); + if(it == m_Tunings.end()) + { + return false; + } + m_Tunings.erase(it); + return true; +} + + +bool CTuningCollection::Remove(const std::size_t i) +{ + if(i >= m_Tunings.size()) + { + return false; + } + m_Tunings.erase(m_Tunings.begin() + i); + return true; +} + + +CTuning* CTuningCollection::AddTuning(std::unique_ptr<CTuning> pT) +{ + if(m_Tunings.size() >= s_nMaxTuningCount) + { + return nullptr; + } + if(!pT) + { + return nullptr; + } + CTuning *result = pT.get(); + m_Tunings.push_back(std::move(pT)); + return result; +} + + +CTuning* CTuningCollection::AddTuning(std::istream &inStrm, mpt::Charset defaultCharset) +{ + if(m_Tunings.size() >= s_nMaxTuningCount) + { + return nullptr; + } + if(!inStrm.good()) + { + return nullptr; + } + std::unique_ptr<CTuning> pT = CTuning::CreateDeserializeOLD(inStrm, defaultCharset); + if(!pT) + { + pT = CTuning::CreateDeserialize(inStrm, defaultCharset); + } + if(!pT) + { + return nullptr; + } + CTuning *result = pT.get(); + m_Tunings.push_back(std::move(pT)); + return result; +} + + +#ifdef MODPLUG_TRACKER + + +bool UnpackTuningCollection(const CTuningCollection &tc, const mpt::PathString &prefix) +{ + bool error = false; + auto numberFmt = mpt::FormatSpec().Dec().FillNul().Width(1 + static_cast<int>(std::log10(tc.GetNumTunings()))); + for(std::size_t i = 0; i < tc.GetNumTunings(); ++i) + { + const CTuning & tuning = *(tc.GetTuning(i)); + mpt::PathString fn; + fn += prefix; + mpt::ustring tuningName = tuning.GetName(); + if(tuningName.empty()) + { + tuningName = U_("untitled"); + } + SanitizeFilename(tuningName); + fn += mpt::PathString::FromUnicode(MPT_UFORMAT("{} - {}")(mpt::ufmt::fmt(i + 1, numberFmt), tuningName)); + fn += mpt::PathString::FromUTF8(CTuning::s_FileExtension); + if(fn.FileOrDirectoryExists()) + { + error = true; + } else + { + mpt::SafeOutputFile sfout(fn, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + if(tuning.Serialize(sfout) != Tuning::SerializationResult::Success) + { + error = true; + } + } + } + return !error; +} + + +#endif + + +} // namespace Tuning + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/tuningbase.h b/Src/external_dependencies/openmpt-trunk/soundlib/tuningbase.h new file mode 100644 index 00000000..49988136 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/tuningbase.h @@ -0,0 +1,80 @@ +/* + * tuningbase.h + * ------------ + * Purpose: Alternative sample tuning. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include <limits> + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Tuning { + + +enum class SerializationResult : int { + Success = 1, + NoMagic = 0, + Failure = -1 +}; + + +using NOTEINDEXTYPE = int16; // Some signed integer-type. +using UNOTEINDEXTYPE = uint16; // Unsigned NOTEINDEXTYPE. + +using RATIOTYPE = float32; // Some 'real figure' type able to present ratios. If changing RATIOTYPE, serialization methods may need modifications. + +// Counter of steps between notes. If there is no 'finetune'(finestepcount == 0), +// then 'step difference' between notes is the +// same as differences in NOTEINDEXTYPE. In a way similar to ticks and rows in pattern - +// ticks <-> STEPINDEX, rows <-> NOTEINDEX +using STEPINDEXTYPE = int32; +using USTEPINDEXTYPE = uint32; + +struct NoteRange +{ + NOTEINDEXTYPE first; + NOTEINDEXTYPE last; +}; + + +// Derived from old IsStepCountRangeSufficient(), this is actually a more +// sensible value than what was calculated in earlier versions. +inline constexpr STEPINDEXTYPE FINESTEPCOUNT_MAX = 0xffff; + +inline constexpr auto NOTEINDEXTYPE_MIN = std::numeric_limits<NOTEINDEXTYPE>::min(); +inline constexpr auto NOTEINDEXTYPE_MAX = std::numeric_limits<NOTEINDEXTYPE>::max(); +inline constexpr auto UNOTEINDEXTYPE_MAX = std::numeric_limits<UNOTEINDEXTYPE>::max(); +inline constexpr auto STEPINDEXTYPE_MIN = std::numeric_limits<STEPINDEXTYPE>::min(); +inline constexpr auto STEPINDEXTYPE_MAX = std::numeric_limits<STEPINDEXTYPE>::max(); +inline constexpr auto USTEPINDEXTYPE_MAX = std::numeric_limits<USTEPINDEXTYPE>::max(); + + +enum class Type : uint16 +{ + GENERAL = 0, + GROUPGEOMETRIC = 1, + GEOMETRIC = 3, +}; + + +class CTuning; + + +} // namespace Tuning + + +typedef Tuning::CTuning CTuning; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/soundlib/tuningcollection.h b/Src/external_dependencies/openmpt-trunk/soundlib/tuningcollection.h new file mode 100644 index 00000000..bf0b3327 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/soundlib/tuningcollection.h @@ -0,0 +1,112 @@ +/* + * tuningCollection.h + * ------------------ + * Purpose: Alternative sample tuning collection class. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "tuning.h" +#include <vector> +#include <string> + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Tuning { + + +class CTuningCollection +{ + +public: + + static constexpr char s_FileExtension[4] = ".tc"; + + // OpenMPT <= 1.26 had to following limits: + // * 255 built-in tunings (only 2 were ever actually provided) + // * 255 local tunings + // * 255 tune-specific tunings + // As 1.27 copies all used tunings into the module, the limit of 255 is no + // longer sufficient. In the worst case scenario, the module contains 255 + // unused tunings and uses 255 local ones. In addition to that, allow the + // user to additionally import both built-in tunings. + // Older OpenMPT versions will silently skip loading tunings beyond index + // 255. + static constexpr size_t s_nMaxTuningCount = 255 + 255 + 2 ; + +public: + + // returns observer ptr if successful + CTuning* AddTuning(std::unique_ptr<CTuning> pT); + CTuning* AddTuning(std::istream &inStrm, mpt::Charset defaultCharset); + + bool Remove(const std::size_t i); + bool Remove(const CTuning *pT); + + std::size_t GetNumTunings() const + { + return m_Tunings.size(); + } + + CTuning* GetTuning(std::size_t i) + { + if(i >= m_Tunings.size()) + { + return nullptr; + } + return m_Tunings[i].get(); + } + const CTuning* GetTuning(std::size_t i) const + { + if (i >= m_Tunings.size()) + { + return nullptr; + } + return m_Tunings[i].get(); + } + + CTuning* GetTuning(const mpt::ustring &name); + const CTuning* GetTuning(const mpt::ustring &name) const; + + + Tuning::SerializationResult Serialize(std::ostream &oStrm, const mpt::ustring &name) const; + Tuning::SerializationResult Deserialize(std::istream &iStrm, mpt::ustring &name, mpt::Charset defaultCharset); + + auto begin() { return m_Tunings.begin(); } + auto begin() const { return m_Tunings.begin(); } + auto cbegin() { return m_Tunings.cbegin(); } + auto end() { return m_Tunings.end(); } + auto end() const { return m_Tunings.end(); } + auto cend() { return m_Tunings.cend(); } + +private: + + std::vector<std::unique_ptr<CTuning> > m_Tunings; + +private: + + Tuning::SerializationResult DeserializeOLD(std::istream &inStrm, mpt::ustring &uname, mpt::Charset defaultCharset); + +}; + + +#ifdef MODPLUG_TRACKER +bool UnpackTuningCollection(const CTuningCollection &tc, const mpt::PathString &prefix); +#endif + + +} // namespace Tuning + + +typedef Tuning::CTuningCollection CTuningCollection; + + +OPENMPT_NAMESPACE_END |