500 lines
12 KiB
C++
500 lines
12 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, CSoundFile &sndFile)
|
||
|
{
|
||
|
sndFile.m_nSamples = std::min(static_cast<SAMPLEINDEX>(chunk.BytesLeft() / sizeof(OktSample)), static_cast<SAMPLEINDEX>(MAX_SAMPLES - 1));
|
||
|
|
||
|
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.nVolume = std::min(oktSmp.volume.get(), uint16(64)) * 4u;
|
||
|
mptSmp.nLength = oktSmp.length & ~1;
|
||
|
mptSmp.cues[0] = oktSmp.type; // Temporary storage for pattern reader, will be reset later
|
||
|
// 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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Parse a pattern block
|
||
|
static void ReadOKTPattern(FileReader &chunk, PATTERNINDEX pat, CSoundFile &sndFile, const std::array<int8, 8> pairedChn)
|
||
|
{
|
||
|
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++)
|
||
|
{
|
||
|
auto 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>();
|
||
|
|
||
|
if(note > 0 && note <= 36)
|
||
|
{
|
||
|
m.note = note + (NOTE_MIDDLEC - 13);
|
||
|
m.instr = instr + 1;
|
||
|
if(m.instr > 0 && m.instr <= sndFile.GetNumSamples())
|
||
|
{
|
||
|
const auto &sample = sndFile.GetSample(m.instr);
|
||
|
// Default volume only works on raw Paula channels
|
||
|
if(pairedChn[chn] && sample.nVolume < 256)
|
||
|
{
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.vol = 64;
|
||
|
}
|
||
|
// If channel and sample type don't match, stop this channel (add 100 to the instrument number to make it understandable what happened during import)
|
||
|
if((sample.cues[0] == 1 && pairedChn[chn] != 0) || (sample.cues[0] == 0 && pairedChn[chn] == 0))
|
||
|
{
|
||
|
m.instr += 100;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch(effect)
|
||
|
{
|
||
|
case 0: // Nothing
|
||
|
break;
|
||
|
|
||
|
case 1: // 1 Portamento Down (Period)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_PORTAMENTOUP;
|
||
|
m.param = param;
|
||
|
}
|
||
|
break;
|
||
|
case 2: // 2 Portamento Up (Period)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_PORTAMENTODOWN;
|
||
|
m.param = param;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
#if 0
|
||
|
/* these aren't like regular arpeggio: "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(param)
|
||
|
{
|
||
|
m.command = CMD_ARPEGGIO;
|
||
|
m.param = param;
|
||
|
}
|
||
|
break;
|
||
|
#endif
|
||
|
// This one is close enough to "standard" arpeggio -- I think!
|
||
|
case 12: // C Arpeggio 3 (up, up, orig)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_ARPEGGIO;
|
||
|
m.param = param;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 13: // D Slide Down (Notes)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEDOWN;
|
||
|
m.param = 0x10 | std::min(uint8(0x0F), param);
|
||
|
}
|
||
|
break;
|
||
|
case 30: // U Slide Up (Notes)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEUP;
|
||
|
m.param = 0x10 | std::min(uint8(0x0F), param);
|
||
|
}
|
||
|
break;
|
||
|
// Fine Slides are only implemented for libopenmpt. For OpenMPT,
|
||
|
// sliding every 5 (non-note) ticks kind of works (at least at
|
||
|
// speed 6), but implementing separate (format-agnostic) fine slide commands would of course be better.
|
||
|
case 21: // L Slide Down Once (Notes)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEDOWN;
|
||
|
m.param = 0x50 | std::min(uint8(0x0F), param);
|
||
|
}
|
||
|
break;
|
||
|
case 17: // H Slide Up Once (Notes)
|
||
|
if(param)
|
||
|
{
|
||
|
m.command = CMD_NOTESLIDEUP;
|
||
|
m.param = 0x50 | std::min(uint8(0x0F), param);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 15: // F Set Filter <>00:ON
|
||
|
m.command = CMD_MODCMDEX;
|
||
|
m.param = !!param;
|
||
|
break;
|
||
|
|
||
|
case 25: // P Pos Jump
|
||
|
m.command = CMD_POSITIONJUMP;
|
||
|
m.param = param;
|
||
|
break;
|
||
|
|
||
|
case 27: // R Release sample (apparently not listed in the help!)
|
||
|
m.Clear();
|
||
|
m.note = NOTE_KEYOFF;
|
||
|
break;
|
||
|
|
||
|
case 28: // S Speed
|
||
|
if(param < 0x20)
|
||
|
{
|
||
|
m.command = CMD_SPEED;
|
||
|
m.param = param;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case 31: // V Volume
|
||
|
// Volume on mixed channels is permanent, on hardware channels it behaves like in regular MODs
|
||
|
if(param & 0x0F)
|
||
|
{
|
||
|
m.command = pairedChn[chn] ? CMD_CHANNELVOLSLIDE : CMD_VOLUMESLIDE;
|
||
|
m.param = param & 0x0F;
|
||
|
}
|
||
|
|
||
|
switch(param >> 4)
|
||
|
{
|
||
|
case 4: // Normal slide down
|
||
|
if(param != 0x40)
|
||
|
break;
|
||
|
// 0x40 is set volume -- fall through
|
||
|
[[fallthrough]];
|
||
|
case 0: case 1: case 2: case 3:
|
||
|
if(pairedChn[chn])
|
||
|
{
|
||
|
m.command = CMD_CHANNELVOLUME;
|
||
|
m.param = param;
|
||
|
} else
|
||
|
{
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.vol = param;
|
||
|
m.command = CMD_NONE;
|
||
|
}
|
||
|
break;
|
||
|
case 5: // Normal slide up
|
||
|
m.param <<= 4;
|
||
|
break;
|
||
|
case 6: // Fine slide down
|
||
|
m.param = 0xF0 | std::min(static_cast<uint8>(m.param), uint8(0x0E));
|
||
|
break;
|
||
|
case 7: // Fine slide up
|
||
|
m.param = (std::min(static_cast<uint8>(m.param), uint8(0x0E)) << 4) | 0x0F;
|
||
|
break;
|
||
|
default:
|
||
|
// Junk.
|
||
|
m.command = CMD_NONE;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Volume is shared between two mixed channels, second channel has priority
|
||
|
if(m.command == CMD_CHANNELVOLUME || m.command == CMD_CHANNELVOLSLIDE)
|
||
|
{
|
||
|
ModCommand &other = rowCmd[chn + pairedChn[chn]];
|
||
|
// Try to preserve effect if there already was one
|
||
|
if(other.ConvertVolEffect(other.command, other.param, true))
|
||
|
{
|
||
|
other.volcmd = other.command;
|
||
|
other.vol = other.param;
|
||
|
}
|
||
|
other.command = m.command;
|
||
|
other.param = m.param;
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
#if 0
|
||
|
case 24: // O Old Volume (???)
|
||
|
m.command = CMD_VOLUMESLIDE;
|
||
|
m.param = 0;
|
||
|
break;
|
||
|
#endif
|
||
|
|
||
|
default:
|
||
|
m.command = CMD_NONE;
|
||
|
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::array<int8, 8> pairedChn{{}};
|
||
|
ORDERINDEX numOrders = 0;
|
||
|
|
||
|
InitializeGlobals(MOD_TYPE_OKT);
|
||
|
|
||
|
m_modFormat.formatName = U_("Oktalyzer");
|
||
|
m_modFormat.type = U_("okt");
|
||
|
m_modFormat.charset = mpt::Charset::Amiga_no_C1;
|
||
|
|
||
|
// 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:
|
||
|
// Channel setup table
|
||
|
if(m_nChannels == 0 && chunk.GetLength() >= 8)
|
||
|
{
|
||
|
const auto chnTable = chunk.ReadArray<uint16be, 4>();
|
||
|
for(CHANNELINDEX chn = 0; chn < 4; chn++)
|
||
|
{
|
||
|
if(chnTable[chn])
|
||
|
{
|
||
|
pairedChn[m_nChannels] = 1;
|
||
|
pairedChn[m_nChannels + 1] = -1;
|
||
|
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, *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 = 113 * 4;
|
||
|
m_nMaxPeriod = 856 * 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, pairedChn);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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];
|
||
|
mptSample.SetDefaultCuePoints();
|
||
|
if(mptSample.nLength == 0)
|
||
|
continue;
|
||
|
|
||
|
// Weird stuff?
|
||
|
LimitMax(mptSample.nLength, mpt::saturate_cast<SmpLength>(sampleChunks[fileSmp].GetLength()));
|
||
|
|
||
|
SampleIO(
|
||
|
SampleIO::_8bit,
|
||
|
SampleIO::mono,
|
||
|
SampleIO::bigEndian,
|
||
|
SampleIO::signedPCM)
|
||
|
.ReadSample(mptSample, sampleChunks[fileSmp]);
|
||
|
|
||
|
fileSmp++;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|