cog/Frameworks/OpenMPT/OpenMPT/soundlib/Load_ult.cpp

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 &param, 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