Bundle libOpenMPT as a dynamic framework, which should be safe once again, now that there is only one version to bundle. Also, now it is using the versions of libvorbisfile and libmpg123 that are bundled with the player, instead of compiling minimp3 and stbvorbis. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
515 lines
12 KiB
C++
515 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
|