453 lines
10 KiB
C++
453 lines
10 KiB
C++
|
/*
|
||
|
* Load_okt.cpp
|
||
|
* ------------
|
||
|
* Purpose: OKT (Oktalyzer) 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 OktIffChunk
|
||
|
{
|
||
|
// IFF chunk names
|
||
|
enum ChunkIdentifiers
|
||
|
{
|
||
|
idCMOD = MagicBE("CMOD"),
|
||
|
idSAMP = MagicBE("SAMP"),
|
||
|
idSPEE = MagicBE("SPEE"),
|
||
|
idSLEN = MagicBE("SLEN"),
|
||
|
idPLEN = MagicBE("PLEN"),
|
||
|
idPATT = MagicBE("PATT"),
|
||
|
idPBOD = MagicBE("PBOD"),
|
||
|
idSBOD = MagicBE("SBOD"),
|
||
|
};
|
||
|
|
||
|
uint32be signature; // IFF chunk name
|
||
|
uint32be chunksize; // chunk size without header
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(OktIffChunk, 8)
|
||
|
|
||
|
struct OktSample
|
||
|
{
|
||
|
char name[20];
|
||
|
uint32be length; // length in bytes
|
||
|
uint16be loopStart; // *2 for real value
|
||
|
uint16be loopLength; // ditto
|
||
|
uint16be volume; // default volume
|
||
|
uint16be type; // 7-/8-bit sample
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(OktSample, 32)
|
||
|
|
||
|
|
||
|
// Parse the sample header block
|
||
|
static void ReadOKTSamples(FileReader &chunk, std::vector<bool> &sample7bit, CSoundFile &sndFile)
|
||
|
{
|
||
|
sndFile.m_nSamples = std::min(static_cast<SAMPLEINDEX>(chunk.BytesLeft() / sizeof(OktSample)), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1));
|
||
|
sample7bit.resize(sndFile.GetNumSamples());
|
||
|
|
||
|
for(SAMPLEINDEX smp = 1; smp <= sndFile.GetNumSamples(); smp++)
|
||
|
{
|
||
|
ModSample &mptSmp = sndFile.GetSample(smp);
|
||
|
OktSample oktSmp;
|
||
|
chunk.ReadStruct(oktSmp);
|
||
|
|
||
|
mptSmp.Initialize();
|
||
|
sndFile.m_szNames[smp] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, oktSmp.name);
|
||
|
|
||
|
mptSmp.nC5Speed = 8287;
|
||
|
mptSmp.nGlobalVol = 64;
|
||
|
mptSmp.nVolume = std::min(oktSmp.volume.get(), uint16(64)) * 4u;
|
||
|
mptSmp.nLength = oktSmp.length & ~1; // round down
|
||
|
// Parse loops
|
||
|
const SmpLength loopStart = oktSmp.loopStart * 2;
|
||
|
const SmpLength loopLength = oktSmp.loopLength * 2;
|
||
|
if(loopLength > 2 && loopStart + loopLength <= mptSmp.nLength)
|
||
|
{
|
||
|
mptSmp.uFlags.set(CHN_SUSTAINLOOP);
|
||
|
mptSmp.nSustainStart = loopStart;
|
||
|
mptSmp.nSustainEnd = loopStart + loopLength;
|
||
|
}
|
||
|
sample7bit[smp - 1] = (oktSmp.type == 0 || oktSmp.type == 2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Parse a pattern block
|
||
|
static void ReadOKTPattern(FileReader &chunk, PATTERNINDEX pat, CSoundFile &sndFile)
|
||
|
{
|
||
|
if(!chunk.CanRead(2))
|
||
|
{
|
||
|
// Invent empty pattern
|
||
|
sndFile.Patterns.Insert(pat, 64);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
ROWINDEX rows = Clamp(static_cast<ROWINDEX>(chunk.ReadUint16BE()), ROWINDEX(1), MAX_PATTERN_ROWS);
|
||
|
|
||
|
if(!sndFile.Patterns.Insert(pat, rows))
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const CHANNELINDEX chns = sndFile.GetNumChannels();
|
||
|
|
||
|
for(ROWINDEX row = 0; row < rows; row++)
|
||
|
{
|
||
|
ModCommand *rowCmd = sndFile.Patterns[pat].GetRow(row);
|
||
|
for(CHANNELINDEX chn = 0; chn < chns; chn++)
|
||
|
{
|
||
|
ModCommand &m = rowCmd[chn];
|
||
|
const auto [note, instr, effect, param] = chunk.ReadArray<uint8, 4>();
|
||
|
m.param = param;
|
||
|
|
||
|
if(note > 0 && note <= 36)
|
||
|
{
|
||
|
m.note = note + (NOTE_MIDDLEC - 13);
|
||
|
m.instr = instr + 1;
|
||
|
} else
|
||
|
{
|
||
|
m.instr = 0;
|
||
|
}
|
||
|
|
||
|
switch(effect)
|
||
|
{
|
||
|
case 0: // Nothing
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
|
||
|
case 1: // 1 Portamento Down (Period)
|
||
|
m.command = CMD_PORTAMENTOUP;
|
||
|
m.param &= 0x0F;
|
||
|
break;
|
||
|
case 2: // 2 Portamento Up (Period)
|
||
|
m.command = CMD_PORTAMENTODOWN;
|
||
|
m.param &= 0x0F;
|
||
|
break;
|
||
|
|
||
|
#if 0
|
||
|
/* these aren't like Jxx: "down" means to *subtract* the offset from the note.
|
||
|
For now I'm going to leave these unimplemented. */
|
||
|
case 10: // A Arpeggio 1 (down, orig, up)
|
||
|
case 11: // B Arpeggio 2 (orig, up, orig, down)
|
||
|
if (m.param)
|
||
|
m.command = CMD_ARPEGGIO;
|
||
|
break;
|
||
|
#endif
|
||
|
// This one is close enough to "standard" arpeggio -- I think!
|
||
|
case 12: // C Arpeggio 3 (up, up, orig)
|
||
|
if (m.param)
|
||
|
m.command = CMD_ARPEGGIO;
|
||
|
break;
|
||
|
|
||
|
case 13: // D Slide Down (Notes)
|
||
|
if (m.param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEDOWN;
|
||
|
m.param = 0x10 | std::min(uint8(0x0F), m.param);
|
||
|
}
|
||
|
break;
|
||
|
case 30: // U Slide Up (Notes)
|
||
|
if (m.param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEUP;
|
||
|
m.param = 0x10 | std::min(uint8(0x0F), m.param);
|
||
|
}
|
||
|
break;
|
||
|
// We don't have fine note slide, but this is supposed to happen once
|
||
|
// per row. Sliding every 5 (non-note) ticks kind of works (at least at
|
||
|
// speed 6), but implementing fine slides would of course be better.
|
||
|
case 21: // L Slide Down Once (Notes)
|
||
|
if (m.param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEDOWN;
|
||
|
m.param = 0x50 | std::min(uint8(0x0F), m.param);
|
||
|
}
|
||
|
break;
|
||
|
case 17: // H Slide Up Once (Notes)
|
||
|
if (m.param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEUP;
|
||
|
m.param = 0x50 | std::min(uint8(0x0F), m.param);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 15: // F Set Filter <>00:ON
|
||
|
m.command = CMD_MODCMDEX;
|
||
|
m.param = !!m.param;
|
||
|
break;
|
||
|
|
||
|
case 25: // P Pos Jump
|
||
|
m.command = CMD_POSITIONJUMP;
|
||
|
break;
|
||
|
|
||
|
case 27: // R Release sample (apparently not listed in the help!)
|
||
|
m.Clear();
|
||
|
m.note = NOTE_KEYOFF;
|
||
|
break;
|
||
|
|
||
|
case 28: // S Speed
|
||
|
m.command = CMD_SPEED; // or tempo?
|
||
|
break;
|
||
|
|
||
|
case 31: // V Volume
|
||
|
m.command = CMD_VOLUMESLIDE;
|
||
|
switch (m.param >> 4)
|
||
|
{
|
||
|
case 4:
|
||
|
if (m.param != 0x40)
|
||
|
{
|
||
|
m.param &= 0x0F; // D0x
|
||
|
break;
|
||
|
}
|
||
|
// 0x40 is set volume -- fall through
|
||
|
[[fallthrough]];
|
||
|
case 0: case 1: case 2: case 3:
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.vol = m.param;
|
||
|
m.command = CMD_NONE;
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
case 5:
|
||
|
m.param = (m.param & 0x0F) << 4; // Dx0
|
||
|
break;
|
||
|
case 6:
|
||
|
m.param = 0xF0 | std::min(static_cast<uint8>(m.param & 0x0F), uint8(0x0E)); // DFx
|
||
|
break;
|
||
|
case 7:
|
||
|
m.param = (std::min(static_cast<uint8>(m.param & uint8(0x0F)), uint8(0x0E)) << 4) | 0x0F; // DxF
|
||
|
break;
|
||
|
default:
|
||
|
// Junk.
|
||
|
m.command = CMD_NONE;
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
#if 0
|
||
|
case 24: // O Old Volume (???)
|
||
|
m.command = CMD_VOLUMESLIDE;
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
#endif
|
||
|
|
||
|
default:
|
||
|
m.command = CMD_NONE;
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderOKT(MemoryFileReader file, const uint64 *pfilesize)
|
||
|
{
|
||
|
if(!file.CanRead(8))
|
||
|
{
|
||
|
return ProbeWantMoreData;
|
||
|
}
|
||
|
if(!file.ReadMagic("OKTASONG"))
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
OktIffChunk iffHead;
|
||
|
if(!file.ReadStruct(iffHead))
|
||
|
{
|
||
|
return ProbeWantMoreData;
|
||
|
}
|
||
|
if(iffHead.chunksize == 0)
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
if((iffHead.signature & 0x80808080u) != 0) // ASCII?
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
MPT_UNREFERENCED_PARAMETER(pfilesize);
|
||
|
return ProbeSuccess;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CSoundFile::ReadOKT(FileReader &file, ModLoadingFlags loadFlags)
|
||
|
{
|
||
|
file.Rewind();
|
||
|
if(!file.ReadMagic("OKTASONG"))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// prepare some arrays to store offsets etc.
|
||
|
std::vector<FileReader> patternChunks;
|
||
|
std::vector<FileReader> sampleChunks;
|
||
|
std::vector<bool> sample7bit; // 7-/8-bit sample
|
||
|
ORDERINDEX numOrders = 0;
|
||
|
|
||
|
InitializeGlobals(MOD_TYPE_OKT);
|
||
|
|
||
|
m_modFormat.formatName = U_("Oktalyzer");
|
||
|
m_modFormat.type = U_("okt");
|
||
|
m_modFormat.charset = mpt::Charset::ISO8859_1;
|
||
|
|
||
|
// Go through IFF chunks...
|
||
|
while(file.CanRead(sizeof(OktIffChunk)))
|
||
|
{
|
||
|
OktIffChunk iffHead;
|
||
|
if(!file.ReadStruct(iffHead))
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
FileReader chunk = file.ReadChunk(iffHead.chunksize);
|
||
|
if(!chunk.IsValid())
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
switch(iffHead.signature)
|
||
|
{
|
||
|
case OktIffChunk::idCMOD:
|
||
|
// Read that weird channel setup table
|
||
|
if(m_nChannels > 0 || chunk.GetLength() < 8)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
for(CHANNELINDEX chn = 0; chn < 4; chn++)
|
||
|
{
|
||
|
const uint8 ch1 = chunk.ReadUint8(), ch2 = chunk.ReadUint8();
|
||
|
if(ch1 || ch2)
|
||
|
{
|
||
|
ChnSettings[m_nChannels].Reset();
|
||
|
ChnSettings[m_nChannels++].nPan = (((chn & 3) == 1) || ((chn & 3) == 2)) ? 0xC0 : 0x40;
|
||
|
}
|
||
|
ChnSettings[m_nChannels].Reset();
|
||
|
ChnSettings[m_nChannels++].nPan = (((chn & 3) == 1) || ((chn & 3) == 2)) ? 0xC0 : 0x40;
|
||
|
}
|
||
|
|
||
|
if(loadFlags == onlyVerifyHeader)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idSAMP:
|
||
|
// Convert sample headers
|
||
|
if(m_nSamples > 0)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
ReadOKTSamples(chunk, sample7bit, *this);
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idSPEE:
|
||
|
// Read default speed
|
||
|
if(chunk.GetLength() >= 2)
|
||
|
{
|
||
|
m_nDefaultSpeed = Clamp(chunk.ReadUint16BE(), uint16(1), uint16(255));
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idSLEN:
|
||
|
// Number of patterns, we don't need this.
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idPLEN:
|
||
|
// Read number of valid orders
|
||
|
if(chunk.GetLength() >= 2)
|
||
|
{
|
||
|
numOrders = chunk.ReadUint16BE();
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idPATT:
|
||
|
// Read the orderlist
|
||
|
ReadOrderFromFile<uint8>(Order(), chunk, chunk.GetLength(), 0xFF, 0xFE);
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idPBOD:
|
||
|
// Don't read patterns for now, as the number of channels might be unknown at this point.
|
||
|
if(patternChunks.size() < 256)
|
||
|
{
|
||
|
patternChunks.push_back(chunk);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case OktIffChunk::idSBOD:
|
||
|
// Sample data - same as with patterns, as we need to know the sample format / length
|
||
|
if(sampleChunks.size() < MAX_SAMPLES - 1 && chunk.GetLength() > 0)
|
||
|
{
|
||
|
sampleChunks.push_back(chunk);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
// Non-ASCII chunk ID?
|
||
|
if(iffHead.signature & 0x80808080)
|
||
|
return false;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If there wasn't even a CMOD chunk, we can't really load this.
|
||
|
if(m_nChannels == 0)
|
||
|
return false;
|
||
|
|
||
|
m_nDefaultTempo.Set(125);
|
||
|
m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
|
||
|
m_nSamplePreAmp = m_nVSTiVolume = 48;
|
||
|
m_nMinPeriod = 0x71 * 4;
|
||
|
m_nMaxPeriod = 0x358 * 4;
|
||
|
|
||
|
// Fix orderlist
|
||
|
Order().resize(numOrders);
|
||
|
|
||
|
// Read patterns
|
||
|
if(loadFlags & loadPatternData)
|
||
|
{
|
||
|
Patterns.ResizeArray(static_cast<PATTERNINDEX>(patternChunks.size()));
|
||
|
for(PATTERNINDEX pat = 0; pat < patternChunks.size(); pat++)
|
||
|
{
|
||
|
ReadOKTPattern(patternChunks[pat], pat, *this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read samples
|
||
|
size_t fileSmp = 0;
|
||
|
for(SAMPLEINDEX smp = 1; smp < m_nSamples; smp++)
|
||
|
{
|
||
|
if(fileSmp >= sampleChunks.size() || !(loadFlags & loadSampleData))
|
||
|
break;
|
||
|
|
||
|
ModSample &mptSample = Samples[smp];
|
||
|
if(mptSample.nLength == 0)
|
||
|
continue;
|
||
|
|
||
|
// Weird stuff?
|
||
|
LimitMax(mptSample.nLength, mpt::saturate_cast<SmpLength>(sampleChunks[fileSmp].GetLength()));
|
||
|
|
||
|
SampleIO(
|
||
|
SampleIO::_8bit,
|
||
|
SampleIO::mono,
|
||
|
SampleIO::bigEndian,
|
||
|
sample7bit[smp - 1] ? SampleIO::PCM7to8 : SampleIO::signedPCM)
|
||
|
.ReadSample(mptSample, sampleChunks[fileSmp]);
|
||
|
|
||
|
fileSmp++;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|