123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- /*
- * 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
|