123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385 |
- /*
- * Load_mus_km.cpp
- * ---------------
- * Purpose: Karl Morton Music Format module loader
- * Notes : This is probably not the official name of this format.
- * Karl Morton's engine has been used in Psycho Pinball and Micro Machines 2 and also Back To Baghdad
- * but the latter game only uses its sound effect format, not the music format.
- * So there are only two known games using this music format, and no official tools or documentation are available.
- * Authors: OpenMPT Devs
- * 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 KMChunkHeader
- {
- // 32-Bit chunk identifiers
- enum ChunkIdentifiers
- {
- idSONG = MagicLE("SONG"),
- idSMPL = MagicLE("SMPL"),
- };
- uint32le id; // See ChunkIdentifiers
- uint32le length; // Chunk size including header
- size_t GetLength() const
- {
- return length <= 8 ? 0 : (length - 8);
- }
- ChunkIdentifiers GetID() const
- {
- return static_cast<ChunkIdentifiers>(id.get());
- }
- };
- MPT_BINARY_STRUCT(KMChunkHeader, 8)
- struct KMSampleHeader
- {
- char name[32];
- uint32le loopStart;
- uint32le size;
- };
- MPT_BINARY_STRUCT(KMSampleHeader, 40)
- struct KMSampleReference
- {
- char name[32];
- uint8 finetune;
- uint8 volume;
- };
- MPT_BINARY_STRUCT(KMSampleReference, 34)
- struct KMSongHeader
- {
- char name[32];
- KMSampleReference samples[31];
- uint16le unknown; // always 0
- uint32le numChannels;
- uint32le restartPos;
- uint32le musicSize;
- };
- MPT_BINARY_STRUCT(KMSongHeader, 32 + 31 * 34 + 14)
- struct KMFileHeader
- {
- KMChunkHeader chunkHeader;
- KMSongHeader songHeader;
- };
- MPT_BINARY_STRUCT(KMFileHeader, sizeof(KMChunkHeader) + sizeof(KMSongHeader))
- static uint64 GetHeaderMinimumAdditionalSize(const KMFileHeader &fileHeader)
- {
- // Require room for at least one more sample chunk header
- return static_cast<uint64>(fileHeader.songHeader.musicSize) + sizeof(KMChunkHeader);
- }
- // Check if string only contains printable characters and doesn't contain any garbage after the required terminating null
- static bool IsValidKMString(const char (&str)[32])
- {
- bool nullFound = false;
- for(char c : str)
- {
- if(c > 0x00 && c < 0x20)
- return false;
- else if(c == 0x00)
- nullFound = true;
- else if(nullFound)
- return false;
- }
- return nullFound;
- }
- static bool ValidateHeader(const KMFileHeader &fileHeader)
- {
- if(fileHeader.chunkHeader.id != KMChunkHeader::idSONG
- || fileHeader.chunkHeader.length < sizeof(fileHeader)
- || fileHeader.chunkHeader.length - sizeof(fileHeader) != fileHeader.songHeader.musicSize
- || fileHeader.chunkHeader.length > 0x40000 // That's enough space for 256 crammed 64-row patterns ;)
- || fileHeader.songHeader.unknown != 0
- || fileHeader.songHeader.numChannels < 1
- || fileHeader.songHeader.numChannels > 4 // Engine rejects anything above 32, channels 5 to 32 are simply ignored
- || !IsValidKMString(fileHeader.songHeader.name))
- {
- return false;
- }
- for(const auto &sample : fileHeader.songHeader.samples)
- {
- if(sample.finetune > 15 || sample.volume > 64 || !IsValidKMString(sample.name))
- return false;
- }
- return true;
- }
- CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMUS_KM(MemoryFileReader file, const uint64 *pfilesize)
- {
- KMFileHeader fileHeader;
- if(!file.Read(fileHeader))
- return ProbeWantMoreData;
- if(!ValidateHeader(fileHeader))
- return ProbeFailure;
- return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
- }
- bool CSoundFile::ReadMUS_KM(FileReader &file, ModLoadingFlags loadFlags)
- {
- {
- file.Rewind();
- KMFileHeader fileHeader;
- if(!file.Read(fileHeader))
- return false;
- if(!ValidateHeader(fileHeader))
- return false;
- if(!file.CanRead(mpt::saturate_cast<FileReader::off_t>(GetHeaderMinimumAdditionalSize(fileHeader))))
- return false;
- if(loadFlags == onlyVerifyHeader)
- return true;
- }
- file.Rewind();
- const auto chunks = ChunkReader(file).ReadChunks<KMChunkHeader>(1);
- auto songChunks = chunks.GetAllChunks(KMChunkHeader::idSONG);
- auto sampleChunks = chunks.GetAllChunks(KMChunkHeader::idSMPL);
- if(songChunks.empty() || sampleChunks.empty())
- return false;
- InitializeGlobals(MOD_TYPE_MOD);
- InitializeChannels();
- m_SongFlags = SONG_AMIGALIMITS | SONG_IMPORTED | SONG_ISAMIGA; // Yes, those were not Amiga games but the format fully conforms to Amiga limits, so allow the Amiga Resampler to be used.
- m_nChannels = 4;
- m_nSamples = 0;
- static constexpr uint16 MUS_SAMPLE_UNUSED = 255; // Sentinel value to check if a sample needs to be duplicated
- for(auto &chunk : sampleChunks)
- {
- if(!CanAddMoreSamples())
- break;
- m_nSamples++;
- ModSample &mptSample = Samples[m_nSamples];
- mptSample.Initialize(MOD_TYPE_MOD);
- KMSampleHeader sampleHeader;
- if(!chunk.Read(sampleHeader)
- || !IsValidKMString(sampleHeader.name))
- return false;
- m_szNames[m_nSamples] = sampleHeader.name;
- mptSample.nLoopEnd = mptSample.nLength = sampleHeader.size;
- mptSample.nLoopStart = sampleHeader.loopStart;
- mptSample.uFlags.set(CHN_LOOP);
- mptSample.nVolume = MUS_SAMPLE_UNUSED;
- if(!(loadFlags & loadSampleData))
- continue;
- SampleIO(SampleIO::_8bit,
- SampleIO::mono,
- SampleIO::littleEndian,
- SampleIO::signedPCM)
- .ReadSample(mptSample, chunk);
- }
-
- bool firstSong = true;
- for(auto &chunk : songChunks)
- {
- if(!firstSong && !Order.AddSequence())
- break;
- firstSong = false;
- Order().clear();
- KMSongHeader songHeader;
- if(!chunk.Read(songHeader)
- || songHeader.unknown != 0
- || songHeader.numChannels < 1
- || songHeader.numChannels > 4)
- return false;
-
- Order().SetName(mpt::ToUnicode(mpt::Charset::CP437, songHeader.name));
- FileReader musicData = (loadFlags & loadPatternData) ? chunk.ReadChunk(songHeader.musicSize) : FileReader{};
- // Map the samples for this subsong
- std::array<SAMPLEINDEX, 32> sampleMap{};
- for(uint8 smp = 1; smp <= 31; smp++)
- {
- const auto &srcSample = songHeader.samples[smp - 1];
- const auto srcName = mpt::String::ReadAutoBuf(srcSample.name);
- if(srcName.empty())
- continue;
- if(srcSample.finetune > 15 || srcSample.volume > 64 || !IsValidKMString(srcSample.name))
- return false;
- const auto finetune = MOD2XMFineTune(srcSample.finetune);
- const uint16 volume = srcSample.volume * 4u;
- SAMPLEINDEX copyFrom = 0;
- for(SAMPLEINDEX srcSmp = 1; srcSmp <= m_nSamples; srcSmp++)
- {
- if(srcName != m_szNames[srcSmp])
- continue;
- auto &mptSample = Samples[srcSmp];
- sampleMap[smp] = srcSmp;
- if(mptSample.nVolume == MUS_SAMPLE_UNUSED
- || (mptSample.nFineTune == finetune && mptSample.nVolume == volume))
- {
- // Sample was not used yet, or it uses the same finetune and volume
- mptSample.nFineTune = finetune;
- mptSample.nVolume = volume;
- copyFrom = 0;
- break;
- } else
- {
- copyFrom = srcSmp;
- }
- }
- if(copyFrom && CanAddMoreSamples())
- {
- m_nSamples++;
- sampleMap[smp] = m_nSamples;
- const auto &smpFrom = Samples[copyFrom];
- auto &newSample = Samples[m_nSamples];
- newSample.FreeSample();
- newSample = smpFrom;
- newSample.nFineTune = finetune;
- newSample.nVolume = volume;
- newSample.CopyWaveform(smpFrom);
- m_szNames[m_nSamples] = m_szNames[copyFrom];
- }
- }
- struct ChannelState
- {
- ModCommand prevCommand;
- uint8 repeat = 0;
- };
- std::array<ChannelState, 4> chnStates{};
- static constexpr ROWINDEX MUS_PATTERN_LENGTH = 64;
- const CHANNELINDEX numChannels = static_cast<CHANNELINDEX>(songHeader.numChannels);
- PATTERNINDEX pat = PATTERNINDEX_INVALID;
- ROWINDEX row = MUS_PATTERN_LENGTH;
- ROWINDEX restartRow = 0;
- uint32 repeatsLeft = 0;
- while(repeatsLeft || musicData.CanRead(1))
- {
- row++;
- if(row >= MUS_PATTERN_LENGTH)
- {
- pat = Patterns.InsertAny(MUS_PATTERN_LENGTH);
- if(pat == PATTERNINDEX_INVALID)
- break;
- Order().push_back(pat);
- row = 0;
- }
- ModCommand *m = Patterns[pat].GetpModCommand(row, 0);
- for(CHANNELINDEX chn = 0; chn < numChannels; chn++, m++)
- {
- auto &chnState = chnStates[chn];
- if(chnState.repeat)
- {
- chnState.repeat--;
- repeatsLeft--;
- *m = chnState.prevCommand;
- continue;
- }
- if(!musicData.CanRead(1))
- continue;
- if(musicData.GetPosition() == songHeader.restartPos)
- {
- Order().SetRestartPos(Order().GetLastIndex());
- restartRow = row;
- }
- const uint8 note = musicData.ReadUint8();
- if(note & 0x80)
- {
- chnState.repeat = note & 0x7F;
- repeatsLeft += chnState.repeat;
- *m = chnState.prevCommand;
- continue;
- }
- if(note > 0 && note <= 3 * 12)
- m->note = note + NOTE_MIDDLEC - 13;
- const auto instr = musicData.ReadUint8();
- m->instr = static_cast<ModCommand::INSTR>(sampleMap[instr & 0x1F]);
- if(instr & 0x80)
- {
- m->command = chnState.prevCommand.command;
- m->param = chnState.prevCommand.param;
- } else
- {
- static constexpr struct { ModCommand::COMMAND command; uint8 mask; } effTrans[] =
- {
- {CMD_VOLUME, 0x00}, {CMD_MODCMDEX, 0xA0}, {CMD_MODCMDEX, 0xB0}, {CMD_MODCMDEX, 0x10},
- {CMD_MODCMDEX, 0x20}, {CMD_MODCMDEX, 0x50}, {CMD_OFFSET, 0x00}, {CMD_TONEPORTAMENTO, 0x00},
- {CMD_TONEPORTAVOL, 0x00}, {CMD_VIBRATO, 0x00}, {CMD_VIBRATOVOL, 0x00}, {CMD_ARPEGGIO, 0x00},
- {CMD_PORTAMENTOUP, 0x00}, {CMD_PORTAMENTODOWN, 0x00}, {CMD_VOLUMESLIDE, 0x00}, {CMD_MODCMDEX, 0x90},
- {CMD_TONEPORTAMENTO, 0xFF}, {CMD_MODCMDEX, 0xC0}, {CMD_SPEED, 0x00}, {CMD_TREMOLO, 0x00},
- };
-
- const auto [command, param] = musicData.ReadArray<uint8, 2>();
- if(command < std::size(effTrans))
- {
- m->command = effTrans[command].command;
- m->param = param;
- if(m->command == CMD_SPEED && m->param >= 0x20)
- m->command = CMD_TEMPO;
- else if(effTrans[command].mask)
- m->param = effTrans[command].mask | (m->param & 0x0F);
- }
- }
- chnState.prevCommand = *m;
- }
- }
- if((restartRow != 0 || row < (MUS_PATTERN_LENGTH - 1u)) && pat != PATTERNINDEX_INVALID)
- {
- Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, static_cast<ModCommand::PARAM>(restartRow)).Row(row).RetryNextRow());
- }
- }
- Order.SetSequence(0);
- m_modFormat.formatName = U_("Karl Morton Music Format");
- m_modFormat.type = U_("mus");
- m_modFormat.charset = mpt::Charset::CP437;
- return true;
- }
- OPENMPT_NAMESPACE_END
|