516 lines
12 KiB
C++
516 lines
12 KiB
C++
/*
|
|
* 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
|