Initial community commit
This commit is contained in:
parent
537bcbc862
commit
fc06254474
16440 changed files with 4239995 additions and 2 deletions
515
Src/external_dependencies/openmpt-trunk/soundlib/Load_ult.cpp
Normal file
515
Src/external_dependencies/openmpt-trunk/soundlib/Load_ult.cpp
Normal file
|
@ -0,0 +1,515 @@
|
|||
/*
|
||||
* 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
|
Loading…
Add table
Add a link
Reference in a new issue