diff --git a/OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk b/OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk index 7ec6552..05ad6fb 100644 --- a/OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk +++ b/OpenMPT/build/android_ndk/Android-minimp3-stbvorbis.mk @@ -74,6 +74,7 @@ LOCAL_SRC_FILES := \ soundlib/Load_ptm.cpp \ soundlib/Load_s3m.cpp \ soundlib/Load_stm.cpp \ + soundlib/Load_stp.cpp \ soundlib/Load_ult.cpp \ soundlib/Load_umx.cpp \ soundlib/Load_wav.cpp \ diff --git a/OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk b/OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk index 0c09b5e..456aa21 100644 --- a/OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk +++ b/OpenMPT/build/android_ndk/Android-mpg123-vorbis.mk @@ -72,6 +72,7 @@ LOCAL_SRC_FILES := \ soundlib/Load_ptm.cpp \ soundlib/Load_s3m.cpp \ soundlib/Load_stm.cpp \ + soundlib/Load_stp.cpp \ soundlib/Load_ult.cpp \ soundlib/Load_umx.cpp \ soundlib/Load_wav.cpp \ diff --git a/OpenMPT/build/android_ndk/Android-unmo3.mk b/OpenMPT/build/android_ndk/Android-unmo3.mk index f28d829..aa56d61 100644 --- a/OpenMPT/build/android_ndk/Android-unmo3.mk +++ b/OpenMPT/build/android_ndk/Android-unmo3.mk @@ -105,6 +105,7 @@ LOCAL_SRC_FILES := \ soundlib/Load_ptm.cpp \ soundlib/Load_s3m.cpp \ soundlib/Load_stm.cpp \ + soundlib/Load_stp.cpp \ soundlib/Load_ult.cpp \ soundlib/Load_umx.cpp \ soundlib/Load_wav.cpp \ diff --git a/OpenMPT/build/android_ndk/Android.mk b/OpenMPT/build/android_ndk/Android.mk index 6ef628b..6643057 100644 --- a/OpenMPT/build/android_ndk/Android.mk +++ b/OpenMPT/build/android_ndk/Android.mk @@ -73,6 +73,7 @@ LOCAL_SRC_FILES := \ soundlib/Load_ptm.cpp \ soundlib/Load_s3m.cpp \ soundlib/Load_stm.cpp \ + soundlib/Load_stp.cpp \ soundlib/Load_ult.cpp \ soundlib/Load_umx.cpp \ soundlib/Load_wav.cpp \ diff --git a/OpenMPT/build/autotools/Makefile.am b/OpenMPT/build/autotools/Makefile.am index 86dbdd2..ea60f94 100644 --- a/OpenMPT/build/autotools/Makefile.am +++ b/OpenMPT/build/autotools/Makefile.am @@ -185,6 +185,7 @@ libopenmpt_la_SOURCES += soundlib/Load_psm.cpp libopenmpt_la_SOURCES += soundlib/Load_ptm.cpp libopenmpt_la_SOURCES += soundlib/Load_s3m.cpp libopenmpt_la_SOURCES += soundlib/Load_stm.cpp +libopenmpt_la_SOURCES += soundlib/Load_stp.cpp libopenmpt_la_SOURCES += soundlib/Load_ult.cpp libopenmpt_la_SOURCES += soundlib/Load_umx.cpp libopenmpt_la_SOURCES += soundlib/Load_wav.cpp @@ -418,6 +419,7 @@ libopenmpttest_SOURCES += soundlib/Load_psm.cpp libopenmpttest_SOURCES += soundlib/Load_ptm.cpp libopenmpttest_SOURCES += soundlib/Load_s3m.cpp libopenmpttest_SOURCES += soundlib/Load_stm.cpp +libopenmpttest_SOURCES += soundlib/Load_stp.cpp libopenmpttest_SOURCES += soundlib/Load_ult.cpp libopenmpttest_SOURCES += soundlib/Load_umx.cpp libopenmpttest_SOURCES += soundlib/Load_wav.cpp diff --git a/OpenMPT/libopenmpt/foo_openmpt.cpp b/OpenMPT/libopenmpt/foo_openmpt.cpp index ac7ce34..8947cdf 100644 --- a/OpenMPT/libopenmpt/foo_openmpt.cpp +++ b/OpenMPT/libopenmpt/foo_openmpt.cpp @@ -319,6 +319,7 @@ DECLARE_FILE_TYPE("OpenMPT compatible module files", "*.imf" ";" "*.j2b" ";" "*.plm" ";" + "*.stp" ";" "*.gdm" ";" "*.umx" ";" "*.mo3" ";" diff --git a/OpenMPT/soundlib/Load_stp.cpp b/OpenMPT/soundlib/Load_stp.cpp new file mode 100644 index 0000000..2a054ee --- /dev/null +++ b/OpenMPT/soundlib/Load_stp.cpp @@ -0,0 +1,945 @@ +/* + * 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" +#include "Tables.h" + +OPENMPT_NAMESPACE_BEGIN + +#ifdef NEEDS_PRAGMA_PACK +#pragma pack(push, 1) +#endif + +// File header (except for "STP3" magic) +struct PACKED STPFileHeader { + uint16 version; + uint8 numOrders; + uint8 patternLength; + uint8 orderList[128]; + uint16 speed; + uint16 speedFrac; + uint16 timerCount; + uint16 flags; + uint32 reserved; + uint16 midiCount; // always 50 + uint8 midi[50]; + uint16 numSamples; + uint16 sampleStructSize; + + void ConvertEndianness() + { + SwapBytesBE(version); + SwapBytesBE(speed); + SwapBytesBE(speedFrac); + SwapBytesBE(timerCount); + SwapBytesBE(flags); + SwapBytesBE(midiCount); + SwapBytesBE(numSamples); + SwapBytesBE(sampleStructSize); + } +}; + +STATIC_ASSERT(sizeof(STPFileHeader) == 200); + +// Sample header (versions 0 and 1) +struct PACKED STPSampleHeaderOld { + char pathName[31]; + uint8 flags; + char fileName[30]; + uint32 length; + uint8 volume; + uint8 reserved1; + uint32 loopStart; + uint32 loopLength; + uint16 defaultCmd; + uint32 reserved2; + + void ConvertEndianness() + { + SwapBytesBE(length); + SwapBytesBE(loopStart); + SwapBytesBE(loopLength); + SwapBytesBE(defaultCmd); + } + + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(MOD_TYPE_MOD); + mptSmp.nLength = length; + mptSmp.nVolume = 4 * MIN(volume, 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); + } + } + + void Read(FileReader &file) { + file.ReadConvertEndianness(*this); + pathName[30] = '\0'; + } +}; + +STATIC_ASSERT(sizeof(STPSampleHeaderOld) == 82); + +#ifdef NEEDS_PRAGMA_PACK +#pragma pack(pop) +#endif + +// Sample header (version 2), not packed due to variable string length and alignment in file +struct STPSampleHeader { + char pathName[256]; + uint8 flags; + char fileName[30]; + uint32 length; + uint8 volume; + uint8 reserved1; + uint32 loopStart; + uint32 loopLength; + uint16 defaultCmd; + uint16 defaultPeriod; + uint8 finetune; + uint8 reserved2; + + void ConvertToMPT(ModSample &mptSmp) const + { + mptSmp.Initialize(MOD_TYPE_MOD); + mptSmp.nLength = length; + mptSmp.nFineTune = static_cast(finetune << 3); + mptSmp.nVolume = 4 * MIN(volume, 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.cues[0] = mptSmp.nLoopStart; + mptSmp.uFlags.set(CHN_LOOP); + } + } + + void Read(FileReader &file) { + std::string str; + + file.ReadNullString(str, 255); + std::strncpy(pathName, str.c_str(), 255); + pathName[255] = '\0'; + + flags = file.ReadUint8(); + + file.ReadNullString(str, 29); + std::strncpy(fileName, str.c_str(), 29); + pathName[29] = '\0'; + + // seek to even boundary + if(file.GetPosition() & 1) + file.Skip(1); + + length = file.ReadUint32BE(); + volume = file.ReadUint8(); + reserved1 = file.ReadUint8(); + + loopStart = file.ReadUint32BE(); + loopLength = file.ReadUint32BE(); + + defaultCmd = file.ReadUint16BE(); + defaultPeriod = file.ReadUint16BE(); + + finetune = file.ReadUint8(); + reserved2 = file.ReadUint8(); + } +}; + +struct STPLoopInfo { + SmpLength loopStart; + SmpLength loopLength; + SAMPLEINDEX looped; + SAMPLEINDEX nonLooped; +}; + +typedef std::vector STPLoopList; + +template +static void ReadSample(FileReader &file, T &sampleHeader, ModSample &sample, char (&sampleName)[MAX_SAMPLENAME]) +//-------------------------------------------------------------------------------------------------------------- +{ + sampleHeader.Read(file); + sampleHeader.ConvertToMPT(sample); + + mpt::String::Read(sampleName, sampleHeader.fileName); + // Get rid of weird characters in sample names. + for(size_t i = 0; i < CountOf(sampleName); i++) + { + if(sampleName[i] > 0 && sampleName[i] < ' ') + { + sampleName[i] = ' '; + } + } +} + +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()) return; + + dest.FreeSample(); + + dest = src; + dest.nLength = len; + dest.pSample = nullptr; + + if(dest.AllocateSample() != len) + { + dest.FreeSample(); + return; + } + + // only preserve cue points if the target sample length is the same + if(len != src.nLength) + memset(dest.cues, 0, sizeof(dest.cues)); + + memcpy(dest.pSample8, src.pSample8 + start, len); + if(loop) + { + dest.nLoopStart = 0; + dest.nLoopEnd = len; + dest.uFlags.set(CHN_LOOP); + } else + { + dest.nLoopStart = 0; + dest.nLoopEnd = 0; + dest.uFlags.reset(CHN_LOOP); + } +} + +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.pSample = nullptr; + + SAMPLEINDEX numLoops = loopList.size(); + + // get the total length of the sample after combining all looped sections + for(int 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((newSmp.nLength + info.loopLength > MAX_SAMPLE_LENGTH) || + (info.loopLength > MAX_SAMPLE_LENGTH) || + (info.loopStart + info.loopLength > smp.nLength)) + { + numLoops = i; + break; + } + + newSmp.nLength += info.loopLength; + } + + if(newSmp.AllocateSample() != newSmp.nLength) + { + newSmp.FreeSample(); + return; + } + + // start copying the looped sample data parts + SmpLength start = 0; + + for(int i = 0; i < numLoops; i++) + { + STPLoopInfo &info = loopList[i]; + + memcpy(newSmp.pSample8 + start, smp.pSample8 + info.loopStart, info.loopLength); + + // update loop info based on position in edited sample + info.loopStart = start; + if(i > 0 && i < 10) + { + 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); +} + +bool CSoundFile::ReadSTP(FileReader &file, ModLoadingFlags loadFlags) +//------------------------------------------------------------------- +{ + file.Rewind(); + if(!file.ReadMagic("STP3")) + return false; + + STPFileHeader fileHeader; + file.ReadConvertEndianness(fileHeader); + if(fileHeader.version > 2 || + fileHeader.numOrders > 128 || fileHeader.numSamples > MAX_SAMPLES || + fileHeader.midiCount != 50) + return false; + + if(loadFlags == onlyVerifyHeader) + return true; + + InitializeGlobals(MOD_TYPE_STP); + + m_nChannels = 4; + m_nSamples = 0; + m_nInstruments = 0; + + m_nDefaultSpeed = fileHeader.speed; + m_nDefaultTempo = ConvertTempo(fileHeader.timerCount); + + m_nMinPeriod = 14 * 4; + m_nMaxPeriod = 3424 * 4; + + Order.ReadFromArray(fileHeader.orderList, fileHeader.numOrders); + + std::vector loopInfo; + // non-looped versions of samples with loopes (when needed) + std::vector nonLooped; + + // Load sample headers + SAMPLEINDEX samplesInFile = 0; + + for(SAMPLEINDEX smp = 1; smp <= fileHeader.numSamples; smp++) + { + // this is 1-based the same as smp + SAMPLEINDEX actualSmp = file.ReadUint16BE(); + if(actualSmp > MAX_SAMPLES) + return false; + + samplesInFile = m_nSamples = MAX(m_nSamples, actualSmp); + + ModSample &thisSmp = Samples[actualSmp]; + + if(fileHeader.version == 2) + { + STPSampleHeader sampleHeader; + uint32 headerSize = file.ReadUint32BE(); + + ReadSample(file, sampleHeader, thisSmp, m_szNames[actualSmp]); + // TODO: verify string lengths against headerSize? + (void)headerSize; + } else + { + STPSampleHeaderOld sampleHeaderOld; + ReadSample(file, sampleHeaderOld, thisSmp, m_szNames[actualSmp]); + } + + STPLoopList loopList; + + if(fileHeader.version >= 1) + { + uint16 numLoops = file.ReadUint16BE(); + + STPLoopInfo loop; + loop.looped = loop.nonLooped = 0; + + if(numLoops == 0 && thisSmp.uFlags[CHN_LOOP]) + { + loop.loopStart = thisSmp.nLoopStart; + loop.loopLength = thisSmp.nLoopEnd - thisSmp.nLoopStart; + loopList.push_back(loop); + + } else for(SAMPLEINDEX i = 0; i < numLoops; i++) + { + loop.loopStart = file.ReadUint32BE(); + loop.loopLength = file.ReadUint32BE(); + loopList.push_back(loop); + } + } + + nonLooped.resize(actualSmp); + loopInfo.resize(actualSmp); + loopInfo[actualSmp - 1] = loopList; + } + + // Load patterns + uint16 numPatterns = 128; + if (fileHeader.version == 0) + numPatterns = file.ReadUint16BE(); + + uint16 patternLength = fileHeader.patternLength; + uint16 channels = 4; + + uint8 globalVolSlide = 0; + std::vector autoFinePorta; + std::vector autoPortaUp; + std::vector autoPortaDown; + std::vector autoVolSlide; + std::vector autoVibrato; + std::vector vibratoMem; + std::vector autoTremolo; + std::vector autoTonePorta; + std::vector tonePortaMem; + + for(PATTERNINDEX 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(); + + m_nChannels = MIN(m_nChannels, channels); + } + + if(!(loadFlags & loadPatternData) || !Patterns.Insert(actualPat, patternLength)) + { + file.Skip(channels * patternLength * 4); + continue; + } + + autoFinePorta.resize(channels, 0); + autoPortaUp.resize(channels, 0); + autoPortaDown.resize(channels, 0); + autoVolSlide.resize(channels, 0); + autoVibrato.resize(channels, 0); + vibratoMem.resize(channels, 0); + autoTremolo.resize(channels, 0); + autoTonePorta.resize(channels, 0); + tonePortaMem.resize(channels, 0); + + for(ROWINDEX row = 0; row < patternLength; row++) + { + PatternRow rowBase = Patterns[actualPat].GetpModCommand(row, 0); + + bool didGlobalVolSlide = false; + bool shouldDelay = false; + + // if a fractional speed value is in use then determine if we should stick a fine pattern delay somewhere + switch(fileHeader.speedFrac & 3) + { + // 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++) + { + ModCommand &m = rowBase[chn]; + + m.instr = file.ReadUint8(); + m.note = file.ReadUint8(); + m.command = file.ReadUint8(); + m.param = file.ReadUint8(); + + if(m.note) + { + m.note += 25; + + autoFinePorta[chn] = 0; + autoPortaUp[chn] = 0; + autoPortaDown[chn] = 0; + autoVolSlide[chn] = 0; + autoVibrato[chn] = vibratoMem[chn] = 0; + autoTremolo[chn] = 0; + autoTonePorta[chn] = tonePortaMem[chn] = 0; + } + + // this is a nibble-swapped param value used for auto fine volside + // and auto global fine volside + uint8 swap = (m.param >> 4) | (m.param << 4); + + if((m.command & 0xF0) == 0xF0) + { + m.param = Util::Round(ConvertTempo(m.param | (((uint16)m.command & 0xF) << 8)).ToDouble()); + m.command = CMD_TEMPO; + } else switch(m.command) + { + case 0x00: // arpeggio + if(m.param) + m.command = CMD_ARPEGGIO; + 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 + autoFinePorta[chn] = 0x10 | MIN(m.param, 15); + autoPortaUp[chn] = 0; + autoPortaDown[chn] = 0; + autoTonePorta[chn] = 0; + + m.command = m.param = 0; + break; + + case 0x04: // auto fine portamento down + autoFinePorta[chn] = 0x20 | MIN(m.param, 15); + autoPortaUp[chn] = 0; + autoPortaDown[chn] = 0; + autoTonePorta[chn] = 0; + + m.command = m.param = 0; + break; + + case 0x05: // auto portamento up + autoFinePorta[chn] = 0; + autoPortaUp[chn] = m.param; + autoPortaDown[chn] = 0; + autoTonePorta[chn] = 0; + + m.command = m.param = 0; + break; + + case 0x06: // auto portamento down + autoFinePorta[chn] = 0; + autoPortaUp[chn] = 0; + autoPortaDown[chn] = m.param; + autoTonePorta[chn] = 0; + + m.command = m.param = 0; + break; + + case 0x07: // set global volume + m.command = CMD_GLOBALVOLUME; + globalVolSlide = 0; + break; + + case 0x08: // auto global fine volume slide + globalVolSlide = swap; + m.command = m.param = 0; + break; + + case 0x09: // fine portamento up + m.command = CMD_MODCMDEX; + m.param = 0x10 | MIN(m.param, 15); + break; + + case 0x0A: // fine portamento down + m.command = CMD_MODCMDEX; + m.param = 0x20 | MIN(m.param, 15); + break; + + case 0x0B: // auto fine volume slide + autoVolSlide[chn] = swap; + m.command = m.param = 0; + break; + + case 0x0C: // set volume + m.volcmd = VOLCMD_VOLUME; + m.vol = m.param; + autoVolSlide[chn] = 0; + m.command = m.param = 0; + 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; + } + autoVolSlide[chn] = 0; + m.command = m.param = 0; + break; + + case 0x0E: // set filter (also uses opposite value compared to .mod) + m.command = CMD_MODCMDEX; + m.param = 1 ^ MIN(m.param, 1); + break; + + case 0x0F: // set speed + m.command = CMD_SPEED; + fileHeader.speedFrac = m.param & 0xF; + m.param >>= 4; + break; + + case 0x10: // auto vibrato + autoVibrato[chn] = m.param; + vibratoMem[chn] = 0; + m.command = m.param = 0; + break; + + case 0x11: // auto tremolo + if(m.param & 0xF) + autoTremolo[chn] = m.param; + else + autoTremolo[chn] = 0; + m.command = m.param = 0; + break; + + case 0x12: // pattern break + m.command = CMD_PATTERNBREAK; + break; + + case 0x13: // auto tone portamento + autoFinePorta[chn] = 0; + autoPortaUp[chn] = 0; + autoPortaDown[chn] = 0; + autoTonePorta[chn] = m.param; + + tonePortaMem[chn] = 0; + m.command = m.param = 0; + 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 < MIN(9u, loopList.size())) + { + m.volcmd = VOLCMD_OFFSET; + m.vol = m.param; + } + } + + m.command = m.param = 0; + 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 && m_nSamples < MAX_SAMPLES-1) + loopList[m.param].looped = ++m_nSamples; + m.instr = loopList[m.param].looped; + } + } + + m.command = m.param = 0; + 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 < MIN(9u, 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] && m_nSamples < MAX_SAMPLES-1) + nonLooped[m.instr-1] = ++m_nSamples; + m.instr = nonLooped[m.instr-1]; + } + + m.command = m.param = 0; + 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 && m_nSamples < MAX_SAMPLES-1) + loopList[m.param].nonLooped = ++m_nSamples; + m.instr = loopList[m.param].nonLooped; + } + } + + m.command = m.param = 0; + break; + + case 0x1D: // fine volume slide (nibble order also swapped) + m.command = CMD_VOLUMESLIDE; + m.param = swap; + 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) + { + autoVolSlide[chn] = m.param >> 4; + m.command = m.param = 0; + } else + { + m.command = CMD_MODCMDEX; + m.param = 0xC0 | (m.param & 0xF); + } + break; + + case 0x21: // note delay + m.command = CMD_MODCMDEX; + m.param = 0xD0 | MIN(m.param, 15); + break; + + case 0x22: // retrigger note + m.command = CMD_RETRIG; + m.param = MIN(m.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 = m.param = 0; + break; + + case 0x4F: // set speed/tempo + if(m.param < 0x20) + { + m.command = CMD_SPEED; + fileHeader.speedFrac = 0; + } else + { + m.command = CMD_TEMPO; + } + break; + + default: + m.command = m.param = 0; + break; + } + + bool didVolSlide = false; + + // try to put volume slide in volume command + if(autoVolSlide[chn] && !m.volcmd) + { + if(autoVolSlide[chn] & 0xF0) + { + m.volcmd = VOLCMD_FINEVOLUP; + m.vol = autoVolSlide[chn] >> 4; + } else + { + m.volcmd = VOLCMD_FINEVOLDOWN; + m.vol = autoVolSlide[chn] & 0xF; + } + didVolSlide = true; + } + + // try to place/combine all remaining running effects. + if(!m.command) + { + if(autoPortaUp[chn]) + { + m.command = CMD_PORTAMENTOUP; + m.param = autoPortaUp[chn]; + + } else if(autoPortaDown[chn]) + { + m.command = CMD_PORTAMENTODOWN; + m.param = autoPortaDown[chn]; + + } else if(autoFinePorta[chn]) + { + m.command = CMD_MODCMDEX; + m.param = autoFinePorta[chn]; + + } else if(autoTonePorta[chn]) + { + m.command = CMD_TONEPORTAMENTO; + m.param = tonePortaMem[chn] = autoTonePorta[chn]; + + } else if(autoVibrato[chn]) + { + m.command = CMD_VIBRATO; + m.param = vibratoMem[chn] = autoVibrato[chn]; + + } else if(!didVolSlide && autoVolSlide[chn]) + { + m.command = CMD_VOLUMESLIDE; + m.param = autoVolSlide[chn]; + // 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; + + } else if(autoTremolo[chn]) + { + m.command = CMD_TREMOLO; + m.param = autoTremolo[chn]; + + } 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) + { + uint16 scriptNum; + uint32 length; + + while(file.CanRead(2)) + { + scriptNum = file.ReadUint16BE(); + if(scriptNum == 0xFFFF) + break; + + file.Skip(2); + 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); + + 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(SAMPLEINDEX loop = 0; loop < loopInfo[smp-1].size(); loop++) + { + STPLoopInfo &info = loopInfo[smp-1][loop]; + + // 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/OpenMPT/soundlib/Snd_defs.h b/OpenMPT/soundlib/Snd_defs.h index 687b0e8..ce5fa00 100644 --- a/OpenMPT/soundlib/Snd_defs.h +++ b/OpenMPT/soundlib/Snd_defs.h @@ -97,6 +97,7 @@ enum MODTYPE MOD_TYPE_DIGI = 0x8000000, MOD_TYPE_UAX = 0x10000000, // sampleset as module MOD_TYPE_PLM = 0x20000000, + MOD_TYPE_STP = 0x40000000, }; DECLARE_FLAGSET(MODTYPE) diff --git a/OpenMPT/soundlib/Snd_fx.cpp b/OpenMPT/soundlib/Snd_fx.cpp index 3542afb..7a482bc 100644 --- a/OpenMPT/soundlib/Snd_fx.cpp +++ b/OpenMPT/soundlib/Snd_fx.cpp @@ -3363,7 +3363,7 @@ void CSoundFile::PortamentoUp(CHANNELINDEX nChn, ModCommand::PARAM param, const else param = pChn->nOldPortaUpDown; - const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI)); + 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)); // Process MIDI pitch bend for instrument plugins MidiPortamento(nChn, param, doFineSlides); @@ -3422,7 +3422,7 @@ void CSoundFile::PortamentoDown(CHANNELINDEX nChn, ModCommand::PARAM param, cons else param = pChn->nOldPortaUpDown; - const bool doFineSlides = !doFinePortamentoAsRegular && !(GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_AMF0 | MOD_TYPE_DIGI)); + 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)); // Process MIDI pitch bend for instrument plugins MidiPortamento(nChn, -static_cast(param), doFineSlides); @@ -3894,7 +3894,7 @@ void CSoundFile::VolumeSlide(ModChannel *pChn, ModCommand::PARAM param) else param = pChn->nOldVolumeSlide; - if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_DIGI))) + if((GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM | MOD_TYPE_MT2 | MOD_TYPE_MED | MOD_TYPE_DIGI | MOD_TYPE_STP))) { // MOD / XM nibble priority if((param & 0xF0) != 0) @@ -5458,7 +5458,7 @@ uint32 CSoundFile::GetFreqFromPeriod(uint32 period, uint32 c5speed, int32 nPerio //------------------------------------------------------------------------------------------ { if (!period) return 0; - if (GetType() & (MOD_TYPE_MED|MOD_TYPE_MOD|MOD_TYPE_DIGI|MOD_TYPE_MTM|MOD_TYPE_AMF0)) + if (GetType() & (MOD_TYPE_MED|MOD_TYPE_MOD|MOD_TYPE_DIGI|MOD_TYPE_MTM|MOD_TYPE_AMF0|MOD_TYPE_STP)) { return ((3546895L * 4) << FREQ_FRACBITS) / period; } else if (GetType() == MOD_TYPE_XM) diff --git a/OpenMPT/soundlib/Sndfile.cpp b/OpenMPT/soundlib/Sndfile.cpp index c463551..30df0c8 100644 --- a/OpenMPT/soundlib/Sndfile.cpp +++ b/OpenMPT/soundlib/Sndfile.cpp @@ -310,6 +310,7 @@ bool CSoundFile::Create(FileReader file, ModLoadingFlags loadFlags) && !ReadJ2B(file, loadFlags) && !ReadMO3(file, loadFlags) && !ReadPT36(file, loadFlags) + && !ReadSTP(file, loadFlags) && !ReadMod(file, loadFlags) && !Read669(file, loadFlags) && !ReadICE(file, loadFlags) @@ -1043,6 +1044,8 @@ MODTYPE CSoundFile::GetBestSaveFormat() const case MOD_TYPE_AMF0: case MOD_TYPE_DIGI: return MOD_TYPE_MOD; + case MOD_TYPE_STP: + return MOD_TYPE_XM; case MOD_TYPE_MED: if(m_nDefaultTempo == TEMPO(125, 0) && m_nDefaultSpeed == 6 && !m_nInstruments) { diff --git a/OpenMPT/soundlib/Sndfile.h b/OpenMPT/soundlib/Sndfile.h index 5b1f4d1..c3f8dee 100644 --- a/OpenMPT/soundlib/Sndfile.h +++ b/OpenMPT/soundlib/Sndfile.h @@ -670,6 +670,7 @@ public: bool ReadDIGI(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); bool ReadPLM(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); bool ReadMID(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); + bool ReadSTP(FileReader &file, ModLoadingFlags loadFlags = loadCompleteModule); static std::vector GetSupportedExtensions(bool otherFormats); static mpt::Charset GetCharsetFromModType(MODTYPE modtype); diff --git a/OpenMPT/soundlib/Tables.cpp b/OpenMPT/soundlib/Tables.cpp index abf91a9..06250bd 100644 --- a/OpenMPT/soundlib/Tables.cpp +++ b/OpenMPT/soundlib/Tables.cpp @@ -87,6 +87,7 @@ static const ModFormatInfo modFormatInfo[] = { MOD_TYPE_IMF, "Imago Orpheus", "imf" }, { MOD_TYPE_J2B, "Galaxy Sound System", "j2b" }, { MOD_TYPE_PLM, "Disorder Tracker 2", "plm" }, + { MOD_TYPE_STP, "Soundtracker Pro II", "stp" }, #ifndef NO_ARCHIVE_SUPPORT // Compressed modules @@ -147,6 +148,7 @@ static const ModCharsetInfo ModCharsetInfos[] = { MOD_TYPE_OKT , mpt::CharsetISO8859_1 }, { MOD_TYPE_DBM , mpt::CharsetISO8859_1 }, { MOD_TYPE_DIGI, mpt::CharsetISO8859_1 }, + { MOD_TYPE_STP, mpt::CharsetISO8859_1 }, // Amiga // DOS { MOD_TYPE_MOD , mpt::CharsetISO8859_1 }, { MOD_TYPE_MED , mpt::CharsetISO8859_1 },