386 lines
10 KiB
C++
386 lines
10 KiB
C++
/*
|
|
* 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
|