#include "stdafx.h"
#include "Loaders.h"
OPENMPT_NAMESPACE_BEGIN
struct STMSampleHeader
{
char filename[12]; uint8le zero;
uint8le disk; uint16le offset; uint16le length; uint16le loopStart; uint16le loopEnd; uint8le volume; uint8le reserved2;
uint16le sampleRate;
uint8le reserved3[6];
void ConvertToMPT(ModSample &mptSmp) const
{
mptSmp.Initialize();
mpt::String::Read<mpt::String::maybeNullTerminated>(mptSmp.filename, filename);
mptSmp.nC5Speed = sampleRate;
mptSmp.nVolume = std::min<uint8>(volume, 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)
struct STMFileHeader
{
char songname[20];
char trackername[8]; uint8 dosEof; uint8 filetype; uint8 verMajor;
uint8 verMinor;
uint8 initTempo; uint8 numPatterns; uint8 globalVolume;
uint8 reserved[13];
};
MPT_BINARY_STRUCT(STMFileHeader, 48)
static bool ValidateHeader(const STMFileHeader &fileHeader)
{
if(fileHeader.filetype != 2
|| (fileHeader.dosEof != 0x1A && fileHeader.dosEof != 2) || fileHeader.verMajor != 2
|| (fileHeader.verMinor != 0 && fileHeader.verMinor != 10 && fileHeader.verMinor != 20 && fileHeader.verMinor != 21)
|| fileHeader.numPatterns > 64
|| (fileHeader.globalVolume > 64 && fileHeader.globalVolume != 0x58)) {
return false;
}
for(uint8 c : fileHeader.trackername)
{
if(c < 0x20 || c >= 0x7F)
return false;
}
return true;
}
static uint64 GetHeaderMinimumAdditionalSize(const STMFileHeader &fileHeader)
{
return 31 * sizeof(STMSampleHeader) + (fileHeader.verMinor == 0 ? 64 : 128) + fileHeader.numPatterns * 64 * 4;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSTM(MemoryFileReader file, const uint64 *pfilesize)
{
STMFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
}
bool CSoundFile::ReadSTM(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
STMFileHeader 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_STM);
mpt::String::Read<mpt::String::maybeNullTerminated>(m_songName, fileHeader.songname);
m_modFormat.formatName = U_("Scream Tracker 2");
m_modFormat.type = U_("stm");
m_modFormat.madeWithTracker = mpt::format(U_("Scream Tracker %1.%2"))(fileHeader.verMajor, mpt::ufmt::dec0<2>(fileHeader.verMinor));
m_modFormat.charset = mpt::CharsetCP437;
m_nSamples = 31;
m_nChannels = 4;
m_nMinPeriod = 64;
m_nMaxPeriod = 0x7FFF;
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<uint8>(fileHeader.globalVolume, 64) * 4u;
for(CHANNELINDEX chn = 0; chn < 4; chn++)
{
ChnSettings[chn].Reset();
ChnSettings[chn].nPan = (chn & 1) ? 0x40 : 0xC0;
}
uint16 sampleOffsets[31];
for(SAMPLEINDEX smp = 1; smp <= 31; smp++)
{
STMSampleHeader sampleHeader;
file.ReadStruct(sampleHeader);
if(sampleHeader.zero != 0 && sampleHeader.zero != 46) return false;
sampleHeader.ConvertToMPT(Samples[smp]);
mpt::String::Read<mpt::String::maybeNullTerminated>(m_szNames[smp], sampleHeader.filename);
sampleOffsets[smp - 1] = sampleHeader.offset;
}
ReadOrderFromFile<uint8>(Order(), file, fileHeader.verMinor == 0 ? 64 : 128);
for(auto &pat : Order())
{
if(pat == 99 || pat == 255) pat = Order.GetInvalidPatIndex();
else if(pat > 63)
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;
for(unsigned int i = 0; i < 64 * 4; i++, 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:
{
uint8 patData[3];
file.ReadArray(patData);
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;
}
static const EffectCommand stmEffects[] =
{
CMD_NONE, CMD_SPEED, CMD_POSITIONJUMP, CMD_PATTERNBREAK, CMD_VOLUMESLIDE, CMD_PORTAMENTODOWN, CMD_PORTAMENTOUP, CMD_TONEPORTAMENTO, CMD_VIBRATO, CMD_TREMOR, CMD_ARPEGGIO, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, };
m->command = stmEffects[volcmd & 0x0F];
m->param = cmdinf;
switch(m->command)
{
case CMD_VOLUMESLIDE:
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)
{
m->command = CMD_POSITIONJUMP;
m->param = static_cast<ModCommand::PARAM>(breakPos);
breakPos = ORDERINDEX_INVALID;
}
LimitMax(breakRow, i / 4u);
break;
case CMD_POSITIONJUMP:
breakPos = m->param;
breakRow = 63;
m->command = CMD_NONE;
break;
case CMD_TREMOR:
break;
case CMD_SPEED:
if(fileHeader.verMinor < 21)
{
m->param = ((m->param / 10u) << 4u) + m->param % 10u;
}
#ifdef MODPLUG_TRACKER
m->param >>= 4;
#endif
MPT_FALLTHROUGH;
default:
if(!m->param)
{
m->command = CMD_NONE;
}
break;
}
}
if(breakPos != ORDERINDEX_INVALID)
{
Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(breakPos)).Row(breakRow).RetryPreviousRow());
}
}
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];
if(sample.nLength && sample.nVolume > 0)
{
FileReader::off_t sampleOffset = sampleOffsets[smp - 1] << 4;
if(sampleOffset > sizeof(STMFileHeader) && file.Seek(sampleOffset))
{
sampleIO.ReadSample(sample, file);
}
}
}
}
return true;
}
OPENMPT_NAMESPACE_END