Bundle libOpenMPT as a dynamic framework, which should be safe once again, now that there is only one version to bundle. Also, now it is using the versions of libvorbisfile and libmpg123 that are bundled with the player, instead of compiling minimp3 and stbvorbis. Signed-off-by: Christopher Snowhill <kode54@gmail.com>
344 lines
7.6 KiB
C++
344 lines
7.6 KiB
C++
/*
|
|
* Load_far.cpp
|
|
* ------------
|
|
* Purpose: Farandole (FAR) module loader
|
|
* Notes : (currently none)
|
|
* Authors: OpenMPT Devs (partly inspired by Storlek's FAR loader from Schism Tracker)
|
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
|
*/
|
|
|
|
|
|
#include "stdafx.h"
|
|
#include "Loaders.h"
|
|
|
|
|
|
OPENMPT_NAMESPACE_BEGIN
|
|
|
|
// FAR File Header
|
|
struct FARFileHeader
|
|
{
|
|
uint8le magic[4];
|
|
char songName[40];
|
|
uint8le eof[3];
|
|
uint16le headerLength;
|
|
uint8le version;
|
|
uint8le onOff[16];
|
|
uint8le editingState[9]; // Stuff we don't care about
|
|
uint8le defaultSpeed;
|
|
uint8le chnPanning[16];
|
|
uint8le patternState[4]; // More stuff we don't care about
|
|
uint16le messageLength;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(FARFileHeader, 98)
|
|
|
|
|
|
struct FAROrderHeader
|
|
{
|
|
uint8le orders[256];
|
|
uint8le numPatterns; // supposed to be "number of patterns stored in the file"; apparently that's wrong
|
|
uint8le numOrders;
|
|
uint8le restartPos;
|
|
uint16le patternSize[256];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(FAROrderHeader, 771)
|
|
|
|
|
|
// FAR Sample header
|
|
struct FARSampleHeader
|
|
{
|
|
// Sample flags
|
|
enum SampleFlags
|
|
{
|
|
smp16Bit = 0x01,
|
|
smpLoop = 0x08,
|
|
};
|
|
|
|
char name[32];
|
|
uint32le length;
|
|
uint8le finetune;
|
|
uint8le volume;
|
|
uint32le loopStart;
|
|
uint32le loopEnd;
|
|
uint8le type;
|
|
uint8le loop;
|
|
|
|
// Convert sample header to OpenMPT's internal format.
|
|
void ConvertToMPT(ModSample &mptSmp) const
|
|
{
|
|
mptSmp.Initialize();
|
|
|
|
mptSmp.nLength = length;
|
|
mptSmp.nLoopStart = loopStart;
|
|
mptSmp.nLoopEnd = loopEnd;
|
|
mptSmp.nC5Speed = 8363 * 2;
|
|
mptSmp.nVolume = volume * 16;
|
|
|
|
if(type & smp16Bit)
|
|
{
|
|
mptSmp.nLength /= 2;
|
|
mptSmp.nLoopStart /= 2;
|
|
mptSmp.nLoopEnd /= 2;
|
|
}
|
|
|
|
if((loop & 8) && mptSmp.nLoopEnd > mptSmp.nLoopStart)
|
|
{
|
|
mptSmp.uFlags.set(CHN_LOOP);
|
|
}
|
|
}
|
|
|
|
// Retrieve the internal sample format flags for this sample.
|
|
SampleIO GetSampleFormat() const
|
|
{
|
|
return SampleIO(
|
|
(type & smp16Bit) ? SampleIO::_16bit : SampleIO::_8bit,
|
|
SampleIO::mono,
|
|
SampleIO::littleEndian,
|
|
SampleIO::signedPCM);
|
|
}
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(FARSampleHeader, 48)
|
|
|
|
|
|
static bool ValidateHeader(const FARFileHeader &fileHeader)
|
|
{
|
|
if(std::memcmp(fileHeader.magic, "FAR\xFE", 4) != 0
|
|
|| std::memcmp(fileHeader.eof, "\x0D\x0A\x1A", 3)
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
if(fileHeader.headerLength < sizeof(FARFileHeader))
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
static uint64 GetHeaderMinimumAdditionalSize(const FARFileHeader &fileHeader)
|
|
{
|
|
return fileHeader.headerLength - sizeof(FARFileHeader);
|
|
}
|
|
|
|
|
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderFAR(MemoryFileReader file, const uint64 *pfilesize)
|
|
{
|
|
FARFileHeader fileHeader;
|
|
if(!file.ReadStruct(fileHeader))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
if(!ValidateHeader(fileHeader))
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
|
|
}
|
|
|
|
|
|
bool CSoundFile::ReadFAR(FileReader &file, ModLoadingFlags loadFlags)
|
|
{
|
|
file.Rewind();
|
|
|
|
FARFileHeader fileHeader;
|
|
if(!file.ReadStruct(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;
|
|
}
|
|
|
|
// Globals
|
|
InitializeGlobals(MOD_TYPE_FAR);
|
|
m_nChannels = 16;
|
|
m_nSamplePreAmp = 32;
|
|
m_nDefaultSpeed = fileHeader.defaultSpeed;
|
|
m_nDefaultTempo.Set(80);
|
|
m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
|
|
m_SongFlags = SONG_LINEARSLIDES;
|
|
m_playBehaviour.set(kPeriodsAreHertz);
|
|
|
|
m_modFormat.formatName = U_("Farandole Composer");
|
|
m_modFormat.type = U_("far");
|
|
m_modFormat.charset = mpt::Charset::CP437;
|
|
|
|
m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName);
|
|
|
|
// Read channel settings
|
|
for(CHANNELINDEX chn = 0; chn < 16; chn++)
|
|
{
|
|
ChnSettings[chn].Reset();
|
|
ChnSettings[chn].dwFlags = fileHeader.onOff[chn] ? ChannelFlags(0) : CHN_MUTE;
|
|
ChnSettings[chn].nPan = ((fileHeader.chnPanning[chn] & 0x0F) << 4) + 8;
|
|
}
|
|
|
|
// Read song message
|
|
if(fileHeader.messageLength != 0)
|
|
{
|
|
m_songMessage.ReadFixedLineLength(file, fileHeader.messageLength, 132, 0); // 132 characters per line... wow. :)
|
|
}
|
|
|
|
// Read orders
|
|
FAROrderHeader orderHeader;
|
|
if(!file.ReadStruct(orderHeader))
|
|
{
|
|
return false;
|
|
}
|
|
ReadOrderFromArray(Order(), orderHeader.orders, orderHeader.numOrders, 0xFF, 0xFE);
|
|
Order().SetRestartPos(orderHeader.restartPos);
|
|
|
|
file.Seek(fileHeader.headerLength);
|
|
|
|
// Pattern effect LUT
|
|
static constexpr EffectCommand farEffects[] =
|
|
{
|
|
CMD_NONE,
|
|
CMD_PORTAMENTOUP,
|
|
CMD_PORTAMENTODOWN,
|
|
CMD_TONEPORTAMENTO,
|
|
CMD_RETRIG,
|
|
CMD_VIBRATO, // depth
|
|
CMD_VIBRATO, // speed
|
|
CMD_VOLUMESLIDE, // up
|
|
CMD_VOLUMESLIDE, // down
|
|
CMD_VIBRATO, // sustained (?)
|
|
CMD_NONE, // actually slide-to-volume
|
|
CMD_S3MCMDEX, // panning
|
|
CMD_S3MCMDEX, // note offset => note delay?
|
|
CMD_NONE, // fine tempo down
|
|
CMD_NONE, // fine tempo up
|
|
CMD_SPEED,
|
|
};
|
|
|
|
// Read patterns
|
|
for(PATTERNINDEX pat = 0; pat < 256; pat++)
|
|
{
|
|
if(!orderHeader.patternSize[pat])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FileReader patternChunk = file.ReadChunk(orderHeader.patternSize[pat]);
|
|
|
|
// Calculate pattern length in rows (every event is 4 bytes, and we have 16 channels)
|
|
ROWINDEX numRows = (orderHeader.patternSize[pat] - 2) / (16 * 4);
|
|
if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Read break row and unused value (used to be pattern tempo)
|
|
ROWINDEX breakRow = patternChunk.ReadUint8();
|
|
patternChunk.Skip(1);
|
|
if(breakRow > 0 && breakRow < numRows - 2)
|
|
{
|
|
breakRow++;
|
|
} else
|
|
{
|
|
breakRow = ROWINDEX_INVALID;
|
|
}
|
|
|
|
// Read pattern data
|
|
for(ROWINDEX row = 0; row < numRows; row++)
|
|
{
|
|
PatternRow rowBase = Patterns[pat].GetRow(row);
|
|
for(CHANNELINDEX chn = 0; chn < 16; chn++)
|
|
{
|
|
ModCommand &m = rowBase[chn];
|
|
|
|
const auto [note, instr, volume, effect] = patternChunk.ReadArray<uint8, 4>();
|
|
|
|
if(note > 0 && note <= 72)
|
|
{
|
|
m.note = note + 35 + NOTE_MIN;
|
|
m.instr = instr + 1;
|
|
}
|
|
|
|
if(volume > 0 && volume <= 16)
|
|
{
|
|
m.volcmd = VOLCMD_VOLUME;
|
|
m.vol = (volume - 1u) * 64u / 15u;
|
|
}
|
|
|
|
m.param = effect & 0x0F;
|
|
|
|
switch(effect >> 4)
|
|
{
|
|
case 0x01:
|
|
case 0x02:
|
|
m.param |= 0xF0;
|
|
break;
|
|
case 0x03: // Porta to note (TODO: Parameter is number of rows the portamento should take)
|
|
m.param <<= 2;
|
|
break;
|
|
case 0x04: // Retrig
|
|
m.param = 6 / (1 + (m.param & 0xf)) + 1; // ugh?
|
|
break;
|
|
case 0x06: // Vibrato speed
|
|
case 0x07: // Volume slide up
|
|
m.param *= 8;
|
|
break;
|
|
case 0x0A: // Volume-portamento (what!)
|
|
m.volcmd = VOLCMD_VOLUME;
|
|
m.vol = (m.param << 2) + 4;
|
|
break;
|
|
case 0x0B: // Panning
|
|
m.param |= 0x80;
|
|
break;
|
|
case 0x0C: // Note offset
|
|
m.param = 6 / (1 + m.param) + 1;
|
|
m.param |= 0x0D;
|
|
}
|
|
m.command = farEffects[effect >> 4];
|
|
}
|
|
}
|
|
|
|
Patterns[pat].WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(breakRow).RetryNextRow());
|
|
}
|
|
|
|
if(!(loadFlags & loadSampleData))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Read samples
|
|
uint8 sampleMap[8]; // Sample usage bitset
|
|
file.ReadArray(sampleMap);
|
|
|
|
for(SAMPLEINDEX smp = 0; smp < 64; smp++)
|
|
{
|
|
if(!(sampleMap[smp >> 3] & (1 << (smp & 7))))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FARSampleHeader sampleHeader;
|
|
if(!file.ReadStruct(sampleHeader))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
m_nSamples = smp + 1;
|
|
ModSample &sample = Samples[m_nSamples];
|
|
m_szNames[m_nSamples] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name);
|
|
sampleHeader.ConvertToMPT(sample);
|
|
sampleHeader.GetSampleFormat().ReadSample(sample, file);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
OPENMPT_NAMESPACE_END
|