269 lines
7.0 KiB
C++
269 lines
7.0 KiB
C++
|
/*
|
||
|
* Load_c67.cpp
|
||
|
* ------------
|
||
|
* Purpose: C67 (CDFM Composer) module loader
|
||
|
* Notes : C67 is the composer format; 670 files can be converted back to C67 using the converter that comes with CDFM.
|
||
|
* 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 C67SampleHeader
|
||
|
{
|
||
|
uint32le unknown; // Probably placeholder for in-memory address, 0 on disk
|
||
|
uint32le length;
|
||
|
uint32le loopStart;
|
||
|
uint32le loopEnd;
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(C67SampleHeader, 16)
|
||
|
|
||
|
|
||
|
struct C67FileHeader
|
||
|
{
|
||
|
uint8 speed;
|
||
|
uint8 restartPos;
|
||
|
char sampleNames[32][13];
|
||
|
C67SampleHeader samples[32];
|
||
|
char fmInstrNames[32][13];
|
||
|
uint8 fmInstr[32][11];
|
||
|
uint8 orders[256];
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(C67FileHeader, 1954)
|
||
|
|
||
|
|
||
|
static bool ValidateHeader(const C67FileHeader &fileHeader)
|
||
|
{
|
||
|
if(fileHeader.speed < 1 || fileHeader.speed > 15)
|
||
|
return false;
|
||
|
for(auto ord : fileHeader.orders)
|
||
|
{
|
||
|
if(ord >= 128 && ord != 0xFF)
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
bool anyNonSilent = false;
|
||
|
for(SAMPLEINDEX smp = 0; smp < 32; smp++)
|
||
|
{
|
||
|
if(fileHeader.sampleNames[smp][12] != 0
|
||
|
|| fileHeader.samples[smp].unknown != 0
|
||
|
|| fileHeader.samples[smp].length > 0xFFFFF
|
||
|
|| fileHeader.fmInstrNames[smp][12] != 0
|
||
|
|| (fileHeader.fmInstr[smp][0] & 0xF0) // No OPL3
|
||
|
|| (fileHeader.fmInstr[smp][5] & 0xFC) // No OPL3
|
||
|
|| (fileHeader.fmInstr[smp][10] & 0xFC)) // No OPL3
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
if(fileHeader.samples[smp].length != 0 && fileHeader.samples[smp].loopEnd < 0xFFFFF)
|
||
|
{
|
||
|
if(fileHeader.samples[smp].loopEnd > fileHeader.samples[smp].length
|
||
|
|| fileHeader.samples[smp].loopStart > fileHeader.samples[smp].loopEnd)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
if(!anyNonSilent && (fileHeader.samples[smp].length != 0 || memcmp(fileHeader.fmInstr[smp], "\0\0\0\0\0\0\0\0\0\0\0", 11)))
|
||
|
{
|
||
|
anyNonSilent = true;
|
||
|
}
|
||
|
}
|
||
|
return anyNonSilent;
|
||
|
}
|
||
|
|
||
|
|
||
|
static uint64 GetHeaderMinimumAdditionalSize(const C67FileHeader &)
|
||
|
{
|
||
|
return 1024; // Pattern offsets and lengths
|
||
|
}
|
||
|
|
||
|
|
||
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderC67(MemoryFileReader file, const uint64 *pfilesize)
|
||
|
{
|
||
|
C67FileHeader fileHeader;
|
||
|
if(!file.ReadStruct(fileHeader))
|
||
|
{
|
||
|
return ProbeWantMoreData;
|
||
|
}
|
||
|
if(!ValidateHeader(fileHeader))
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
|
||
|
}
|
||
|
|
||
|
|
||
|
static void TranslateVolume(ModCommand &m, uint8 volume, bool isFM)
|
||
|
{
|
||
|
// CDFM uses a linear volume scale for FM instruments.
|
||
|
// ScreamTracker, on the other hand, directly uses the OPL chip's logarithmic volume scale.
|
||
|
// Neither FM nor PCM instruments can be fully muted in CDFM.
|
||
|
static constexpr uint8 fmVolume[16] =
|
||
|
{
|
||
|
0x08, 0x10, 0x18, 0x20, 0x28, 0x2C, 0x30, 0x34,
|
||
|
0x36, 0x38, 0x3A, 0x3C, 0x3D, 0x3E, 0x3F, 0x40,
|
||
|
};
|
||
|
|
||
|
volume &= 0x0F;
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.vol = isFM ? fmVolume[volume] : (4u + volume * 4u);
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CSoundFile::ReadC67(FileReader &file, ModLoadingFlags loadFlags)
|
||
|
{
|
||
|
C67FileHeader fileHeader;
|
||
|
|
||
|
file.Rewind();
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// Validate pattern offsets and lengths
|
||
|
uint32le patOffsets[128], patLengths[128];
|
||
|
file.ReadArray(patOffsets);
|
||
|
file.ReadArray(patLengths);
|
||
|
for(PATTERNINDEX pat = 0; pat < 128; pat++)
|
||
|
{
|
||
|
if(patOffsets[pat] > 0xFFFFFF
|
||
|
|| patLengths[pat] < 3 // Smallest well-formed pattern consists of command 0x40 followed by command 0x60
|
||
|
|| patLengths[pat] > 0x1000 // Any well-formed pattern is smaller than this
|
||
|
|| !file.LengthIsAtLeast(2978 + patOffsets[pat] + patLengths[pat]))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
InitializeGlobals(MOD_TYPE_S3M);
|
||
|
InitializeChannels();
|
||
|
|
||
|
m_modFormat.formatName = U_("CDFM");
|
||
|
m_modFormat.type = U_("c67");
|
||
|
m_modFormat.madeWithTracker = U_("Composer 670");
|
||
|
m_modFormat.charset = mpt::Charset::CP437;
|
||
|
|
||
|
m_nDefaultSpeed = fileHeader.speed;
|
||
|
m_nDefaultTempo.Set(143);
|
||
|
Order().SetRestartPos(fileHeader.restartPos);
|
||
|
m_nSamples = 64;
|
||
|
m_nChannels = 4 + 9;
|
||
|
m_playBehaviour.set(kOPLBeatingOscillators);
|
||
|
m_SongFlags.set(SONG_IMPORTED);
|
||
|
|
||
|
// Pan PCM channels only
|
||
|
for(CHANNELINDEX chn = 0; chn < 4; chn++)
|
||
|
{
|
||
|
ChnSettings[chn].nPan = (chn & 1) ? 192 : 64;
|
||
|
}
|
||
|
|
||
|
// PCM instruments
|
||
|
for(SAMPLEINDEX smp = 0; smp < 32; smp++)
|
||
|
{
|
||
|
ModSample &mptSmp = Samples[smp + 1];
|
||
|
mptSmp.Initialize(MOD_TYPE_S3M);
|
||
|
m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.sampleNames[smp]);
|
||
|
mptSmp.nLength = fileHeader.samples[smp].length;
|
||
|
if(fileHeader.samples[smp].loopEnd <= fileHeader.samples[smp].length)
|
||
|
{
|
||
|
mptSmp.nLoopStart = fileHeader.samples[smp].loopStart;
|
||
|
mptSmp.nLoopEnd = fileHeader.samples[smp].loopEnd;
|
||
|
mptSmp.uFlags = CHN_LOOP;
|
||
|
}
|
||
|
mptSmp.nC5Speed = 8287;
|
||
|
}
|
||
|
// OPL instruments
|
||
|
for(SAMPLEINDEX smp = 0; smp < 32; smp++)
|
||
|
{
|
||
|
ModSample &mptSmp = Samples[smp + 33];
|
||
|
mptSmp.Initialize(MOD_TYPE_S3M);
|
||
|
m_szNames[smp + 33] = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.fmInstrNames[smp]);
|
||
|
// Reorder OPL patch bytes (interleave modulator and carrier)
|
||
|
const auto &fm = fileHeader.fmInstr[smp];
|
||
|
OPLPatch patch{{}};
|
||
|
patch[0] = fm[1]; patch[1] = fm[6];
|
||
|
patch[2] = fm[2]; patch[3] = fm[7];
|
||
|
patch[4] = fm[3]; patch[5] = fm[8];
|
||
|
patch[6] = fm[4]; patch[7] = fm[9];
|
||
|
patch[8] = fm[5]; patch[9] = fm[10];
|
||
|
patch[10] = fm[0];
|
||
|
mptSmp.SetAdlib(true, patch);
|
||
|
}
|
||
|
|
||
|
ReadOrderFromArray<uint8>(Order(), fileHeader.orders, 256, 0xFF);
|
||
|
Patterns.ResizeArray(128);
|
||
|
for(PATTERNINDEX pat = 0; pat < 128; pat++)
|
||
|
{
|
||
|
file.Seek(2978 + patOffsets[pat]);
|
||
|
FileReader patChunk = file.ReadChunk(patLengths[pat]);
|
||
|
if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
CPattern &pattern = Patterns[pat];
|
||
|
ROWINDEX row = 0;
|
||
|
while(row < 64 && patChunk.CanRead(1))
|
||
|
{
|
||
|
uint8 cmd = patChunk.ReadUint8();
|
||
|
if(cmd <= 0x0C)
|
||
|
{
|
||
|
// Note, instrument, volume
|
||
|
ModCommand &m = *pattern.GetpModCommand(row, cmd);
|
||
|
const auto [note, instrVol] = patChunk.ReadArray<uint8, 2>();
|
||
|
bool fmChn = (cmd >= 4);
|
||
|
m.note = NOTE_MIN + (fmChn ? 12 : 36) + (note & 0x0F) + ((note >> 4) & 0x07) * 12;
|
||
|
m.instr = (fmChn ? 33 : 1) + (instrVol >> 4) + ((note & 0x80) >> 3);
|
||
|
TranslateVolume(m, instrVol, fmChn);
|
||
|
} else if(cmd >= 0x20 && cmd <= 0x2C)
|
||
|
{
|
||
|
// Volume
|
||
|
TranslateVolume(*pattern.GetpModCommand(row, cmd - 0x20), patChunk.ReadUint8(), cmd >= 0x24);
|
||
|
} else if(cmd == 0x40)
|
||
|
{
|
||
|
// Delay (row done)
|
||
|
row += patChunk.ReadUint8();
|
||
|
} else if(cmd == 0x60)
|
||
|
{
|
||
|
// End of pattern
|
||
|
if(row > 0)
|
||
|
{
|
||
|
pattern.GetpModCommand(row - 1, 0)->command = CMD_PATTERNBREAK;
|
||
|
}
|
||
|
break;
|
||
|
} else
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if(loadFlags & loadSampleData)
|
||
|
{
|
||
|
for(SAMPLEINDEX smp = 1; smp <= 32; smp++)
|
||
|
{
|
||
|
SampleIO(SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::unsignedPCM).ReadSample(Samples[smp], file);
|
||
|
}
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|