668 lines
18 KiB
C++
668 lines
18 KiB
C++
|
/*
|
||
|
* Load_imf.cpp
|
||
|
* ------------
|
||
|
* Purpose: IMF (Imago Orpheus) module loader
|
||
|
* Notes : Reverb and Chorus are not supported.
|
||
|
* 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 IMFChannel
|
||
|
{
|
||
|
char name[12]; // Channel name (ASCIIZ-String, max 11 chars)
|
||
|
uint8 chorus; // Default chorus
|
||
|
uint8 reverb; // Default reverb
|
||
|
uint8 panning; // Pan positions 00-FF
|
||
|
uint8 status; // Channel status: 0 = enabled, 1 = mute, 2 = disabled (ignore effects!)
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFChannel, 16)
|
||
|
|
||
|
struct IMFFileHeader
|
||
|
{
|
||
|
enum SongFlags
|
||
|
{
|
||
|
linearSlides = 0x01,
|
||
|
};
|
||
|
|
||
|
char title[32]; // Songname (ASCIIZ-String, max. 31 chars)
|
||
|
uint16le ordNum; // Number of orders saved
|
||
|
uint16le patNum; // Number of patterns saved
|
||
|
uint16le insNum; // Number of instruments saved
|
||
|
uint16le flags; // See SongFlags
|
||
|
uint8le unused1[8];
|
||
|
uint8le tempo; // Default tempo (Axx, 1...255)
|
||
|
uint8le bpm; // Default beats per minute (BPM) (Txx, 32...255)
|
||
|
uint8le master; // Default master volume (Vxx, 0...64)
|
||
|
uint8le amp; // Amplification factor (mixing volume, 4...127)
|
||
|
uint8le unused2[8];
|
||
|
char im10[4]; // 'IM10'
|
||
|
IMFChannel channels[32]; // Channel settings
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFFileHeader, 576)
|
||
|
|
||
|
struct IMFEnvelope
|
||
|
{
|
||
|
enum EnvFlags
|
||
|
{
|
||
|
envEnabled = 0x01,
|
||
|
envSustain = 0x02,
|
||
|
envLoop = 0x04,
|
||
|
};
|
||
|
|
||
|
uint8 points; // Number of envelope points
|
||
|
uint8 sustain; // Envelope sustain point
|
||
|
uint8 loopStart; // Envelope loop start point
|
||
|
uint8 loopEnd; // Envelope loop end point
|
||
|
uint8 flags; // See EnvFlags
|
||
|
uint8 unused[3];
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFEnvelope, 8)
|
||
|
|
||
|
struct IMFEnvNode
|
||
|
{
|
||
|
uint16le tick;
|
||
|
uint16le value;
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFEnvNode, 4)
|
||
|
|
||
|
struct IMFInstrument
|
||
|
{
|
||
|
enum EnvTypes
|
||
|
{
|
||
|
volEnv = 0,
|
||
|
panEnv = 1,
|
||
|
filterEnv = 2,
|
||
|
};
|
||
|
|
||
|
char name[32]; // Inst. name (ASCIIZ-String, max. 31 chars)
|
||
|
uint8le map[120]; // Multisample settings
|
||
|
uint8le unused[8];
|
||
|
IMFEnvNode nodes[3][16];
|
||
|
IMFEnvelope env[3];
|
||
|
uint16le fadeout; // Fadeout rate (0...0FFFH)
|
||
|
uint16le smpNum; // Number of samples in instrument
|
||
|
char ii10[4]; // 'II10' (not verified by Orpheus)
|
||
|
|
||
|
void ConvertEnvelope(InstrumentEnvelope &mptEnv, EnvTypes e) const
|
||
|
{
|
||
|
const uint8 shift = (e == volEnv) ? 0 : 2;
|
||
|
const uint8 mirror = (e == filterEnv) ? 0xFF : 0x00;
|
||
|
|
||
|
mptEnv.dwFlags.set(ENV_ENABLED, (env[e].flags & 1) != 0);
|
||
|
mptEnv.dwFlags.set(ENV_SUSTAIN, (env[e].flags & 2) != 0);
|
||
|
mptEnv.dwFlags.set(ENV_LOOP, (env[e].flags & 4) != 0);
|
||
|
|
||
|
mptEnv.resize(Clamp(env[e].points, uint8(2), uint8(16)));
|
||
|
mptEnv.nLoopStart = env[e].loopStart;
|
||
|
mptEnv.nLoopEnd = env[e].loopEnd;
|
||
|
mptEnv.nSustainStart = mptEnv.nSustainEnd = env[e].sustain;
|
||
|
|
||
|
uint16 minTick = 0; // minimum tick value for next node
|
||
|
for(uint32 n = 0; n < mptEnv.size(); n++)
|
||
|
{
|
||
|
mptEnv[n].tick = minTick = std::max(minTick, nodes[e][n].tick.get());
|
||
|
minTick++;
|
||
|
uint8 value = static_cast<uint8>(nodes[e][n].value ^ mirror) >> shift;
|
||
|
mptEnv[n].value = std::min(value, uint8(ENVELOPE_MAX));
|
||
|
}
|
||
|
mptEnv.Convert(MOD_TYPE_XM, MOD_TYPE_IT);
|
||
|
}
|
||
|
|
||
|
// Convert an IMFInstrument to OpenMPT's internal instrument representation.
|
||
|
void ConvertToMPT(ModInstrument &mptIns, SAMPLEINDEX firstSample) const
|
||
|
{
|
||
|
mptIns.name = mpt::String::ReadBuf(mpt::String::nullTerminated, name);
|
||
|
|
||
|
if(smpNum)
|
||
|
{
|
||
|
for(size_t note = 0; note < std::min(std::size(map), std::size(mptIns.Keyboard) - 12u); note++)
|
||
|
{
|
||
|
mptIns.Keyboard[note + 12] = firstSample + map[note];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
mptIns.nFadeOut = fadeout;
|
||
|
|
||
|
ConvertEnvelope(mptIns.VolEnv, volEnv);
|
||
|
ConvertEnvelope(mptIns.PanEnv, panEnv);
|
||
|
ConvertEnvelope(mptIns.PitchEnv, filterEnv);
|
||
|
if(mptIns.PitchEnv.dwFlags[ENV_ENABLED])
|
||
|
mptIns.PitchEnv.dwFlags.set(ENV_FILTER);
|
||
|
|
||
|
// hack to get === to stop notes
|
||
|
if(!mptIns.VolEnv.dwFlags[ENV_ENABLED] && !mptIns.nFadeOut)
|
||
|
mptIns.nFadeOut = 32767;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFInstrument, 384)
|
||
|
|
||
|
struct IMFSample
|
||
|
{
|
||
|
enum SampleFlags
|
||
|
{
|
||
|
smpLoop = 0x01,
|
||
|
smpPingPongLoop = 0x02,
|
||
|
smp16Bit = 0x04,
|
||
|
smpPanning = 0x08,
|
||
|
};
|
||
|
|
||
|
char filename[13]; // Sample filename (12345678.ABC) */
|
||
|
uint8le unused1[3];
|
||
|
uint32le length; // Length (in bytes)
|
||
|
uint32le loopStart; // Loop start (in bytes)
|
||
|
uint32le loopEnd; // Loop end (in bytes)
|
||
|
uint32le c5Speed; // Samplerate
|
||
|
uint8le volume; // Default volume (0...64)
|
||
|
uint8le panning; // Default pan (0...255)
|
||
|
uint8le unused2[14];
|
||
|
uint8le flags; // Sample flags
|
||
|
uint8le unused3[5];
|
||
|
uint16le ems; // Reserved for internal usage
|
||
|
uint32le dram; // Reserved for internal usage
|
||
|
char is10[4]; // 'IS10'
|
||
|
|
||
|
// Convert an IMFSample to OpenMPT's internal sample representation.
|
||
|
void ConvertToMPT(ModSample &mptSmp) const
|
||
|
{
|
||
|
mptSmp.Initialize();
|
||
|
mptSmp.filename = mpt::String::ReadBuf(mpt::String::nullTerminated, filename);
|
||
|
|
||
|
mptSmp.nLength = length;
|
||
|
mptSmp.nLoopStart = loopStart;
|
||
|
mptSmp.nLoopEnd = loopEnd;
|
||
|
mptSmp.nC5Speed = c5Speed;
|
||
|
mptSmp.nVolume = volume * 4;
|
||
|
mptSmp.nPan = panning;
|
||
|
if(flags & smpLoop)
|
||
|
mptSmp.uFlags.set(CHN_LOOP);
|
||
|
if(flags & smpPingPongLoop)
|
||
|
mptSmp.uFlags.set(CHN_PINGPONGLOOP);
|
||
|
if(flags & smp16Bit)
|
||
|
{
|
||
|
mptSmp.uFlags.set(CHN_16BIT);
|
||
|
mptSmp.nLength /= 2;
|
||
|
mptSmp.nLoopStart /= 2;
|
||
|
mptSmp.nLoopEnd /= 2;
|
||
|
}
|
||
|
if(flags & smpPanning)
|
||
|
mptSmp.uFlags.set(CHN_PANNING);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(IMFSample, 64)
|
||
|
|
||
|
|
||
|
static constexpr EffectCommand imfEffects[] =
|
||
|
{
|
||
|
CMD_NONE,
|
||
|
CMD_SPEED, // 0x01 1xx Set Tempo
|
||
|
CMD_TEMPO, // 0x02 2xx Set BPM
|
||
|
CMD_TONEPORTAMENTO, // 0x03 3xx Tone Portamento
|
||
|
CMD_TONEPORTAVOL, // 0x04 4xy Tone Portamento + Volume Slide
|
||
|
CMD_VIBRATO, // 0x05 5xy Vibrato
|
||
|
CMD_VIBRATOVOL, // 0x06 6xy Vibrato + Volume Slide
|
||
|
CMD_FINEVIBRATO, // 0x07 7xy Fine Vibrato
|
||
|
CMD_TREMOLO, // 0x08 8xy Tremolo
|
||
|
CMD_ARPEGGIO, // 0x09 9xy Arpeggio
|
||
|
CMD_PANNING8, // 0x0A Axx Set Pan Position
|
||
|
CMD_PANNINGSLIDE, // 0x0B Bxy Pan Slide
|
||
|
CMD_VOLUME, // 0x0C Cxx Set Volume
|
||
|
CMD_VOLUMESLIDE, // 0x0D Dxy Volume Slide
|
||
|
CMD_VOLUMESLIDE, // 0x0E Exy Fine Volume Slide
|
||
|
CMD_S3MCMDEX, // 0x0F Fxx Set Finetune
|
||
|
CMD_NOTESLIDEUP, // 0x10 Gxy Note Slide Up
|
||
|
CMD_NOTESLIDEDOWN, // 0x11 Hxy Note Slide Down
|
||
|
CMD_PORTAMENTOUP, // 0x12 Ixx Slide Up
|
||
|
CMD_PORTAMENTODOWN, // 0x13 Jxx Slide Down
|
||
|
CMD_PORTAMENTOUP, // 0x14 Kxx Fine Slide Up
|
||
|
CMD_PORTAMENTODOWN, // 0x15 Lxx Fine Slide Down
|
||
|
CMD_MIDI, // 0x16 Mxx Set Filter Cutoff
|
||
|
CMD_MIDI, // 0x17 Nxy Filter Slide + Resonance
|
||
|
CMD_OFFSET, // 0x18 Oxx Set Sample Offset
|
||
|
CMD_NONE, // 0x19 Pxx Set Fine Sample Offset - XXX
|
||
|
CMD_KEYOFF, // 0x1A Qxx Key Off
|
||
|
CMD_RETRIG, // 0x1B Rxy Retrig
|
||
|
CMD_TREMOR, // 0x1C Sxy Tremor
|
||
|
CMD_POSITIONJUMP, // 0x1D Txx Position Jump
|
||
|
CMD_PATTERNBREAK, // 0x1E Uxx Pattern Break
|
||
|
CMD_GLOBALVOLUME, // 0x1F Vxx Set Mastervolume
|
||
|
CMD_GLOBALVOLSLIDE, // 0x20 Wxy Mastervolume Slide
|
||
|
CMD_S3MCMDEX, // 0x21 Xxx Extended Effect
|
||
|
// X1x Set Filter
|
||
|
// X3x Glissando
|
||
|
// X5x Vibrato Waveform
|
||
|
// X8x Tremolo Waveform
|
||
|
// XAx Pattern Loop
|
||
|
// XBx Pattern Delay
|
||
|
// XCx Note Cut
|
||
|
// XDx Note Delay
|
||
|
// XEx Ignore Envelope
|
||
|
// XFx Invert Loop
|
||
|
CMD_NONE, // 0x22 Yxx Chorus - XXX
|
||
|
CMD_NONE, // 0x23 Zxx Reverb - XXX
|
||
|
};
|
||
|
|
||
|
static void ImportIMFEffect(ModCommand &m)
|
||
|
{
|
||
|
uint8 n;
|
||
|
// fix some of them
|
||
|
switch(m.command)
|
||
|
{
|
||
|
case 0xE: // fine volslide
|
||
|
// hackaround to get almost-right behavior for fine slides (i think!)
|
||
|
if(m.param == 0)
|
||
|
/* nothing */;
|
||
|
else if(m.param == 0xF0)
|
||
|
m.param = 0xEF;
|
||
|
else if(m.param == 0x0F)
|
||
|
m.param = 0xFE;
|
||
|
else if(m.param & 0xF0)
|
||
|
m.param |= 0x0F;
|
||
|
else
|
||
|
m.param |= 0xF0;
|
||
|
break;
|
||
|
case 0xF: // set finetune
|
||
|
// we don't implement this, but let's at least import the value
|
||
|
m.param = 0x20 | std::min(static_cast<uint8>(m.param >> 4), uint8(0x0F));
|
||
|
break;
|
||
|
case 0x14: // fine slide up
|
||
|
case 0x15: // fine slide down
|
||
|
// this is about as close as we can do...
|
||
|
if(m.param >> 4)
|
||
|
m.param = 0xF0 | (m.param >> 4);
|
||
|
else
|
||
|
m.param |= 0xE0;
|
||
|
break;
|
||
|
case 0x16: // cutoff
|
||
|
m.param = (0xFF - m.param) / 2u;
|
||
|
break;
|
||
|
case 0x17: // cutoff slide + resonance (TODO: cutoff slide is currently not handled)
|
||
|
m.param = 0x80 | (m.param & 0x0F);
|
||
|
break;
|
||
|
case 0x1F: // set global volume
|
||
|
m.param = mpt::saturate_cast<uint8>(m.param * 2);
|
||
|
break;
|
||
|
case 0x21:
|
||
|
n = 0;
|
||
|
switch (m.param >> 4)
|
||
|
{
|
||
|
case 0:
|
||
|
/* undefined, but since S0x does nothing in IT anyway, we won't care.
|
||
|
this is here to allow S00 to pick up the previous value (assuming IMF
|
||
|
even does that -- I haven't actually tried it) */
|
||
|
break;
|
||
|
default: // undefined
|
||
|
case 0x1: // set filter
|
||
|
case 0xF: // invert loop
|
||
|
m.command = CMD_NONE;
|
||
|
break;
|
||
|
case 0x3: // glissando
|
||
|
n = 0x20;
|
||
|
break;
|
||
|
case 0x5: // vibrato waveform
|
||
|
n = 0x30;
|
||
|
break;
|
||
|
case 0x8: // tremolo waveform
|
||
|
n = 0x40;
|
||
|
break;
|
||
|
case 0xA: // pattern loop
|
||
|
n = 0xB0;
|
||
|
break;
|
||
|
case 0xB: // pattern delay
|
||
|
n = 0xE0;
|
||
|
break;
|
||
|
case 0xC: // note cut
|
||
|
case 0xD: // note delay
|
||
|
// Apparently, Imago Orpheus doesn't cut samples on tick 0.
|
||
|
if(!m.param)
|
||
|
m.command = CMD_NONE;
|
||
|
break;
|
||
|
case 0xE: // ignore envelope
|
||
|
switch(m.param & 0x0F)
|
||
|
{
|
||
|
// All envelopes
|
||
|
// Predicament: we can only disable one envelope at a time. Volume is probably most noticeable, so let's go with that.
|
||
|
case 0: m.param = 0x77; break;
|
||
|
// Volume
|
||
|
case 1: m.param = 0x77; break;
|
||
|
// Panning
|
||
|
case 2: m.param = 0x79; break;
|
||
|
// Filter
|
||
|
case 3: m.param = 0x7B; break;
|
||
|
}
|
||
|
break;
|
||
|
case 0x18: // sample offset
|
||
|
// O00 doesn't pick up the previous value
|
||
|
if(!m.param)
|
||
|
m.command = CMD_NONE;
|
||
|
break;
|
||
|
}
|
||
|
if(n)
|
||
|
m.param = n | (m.param & 0x0F);
|
||
|
break;
|
||
|
}
|
||
|
m.command = (m.command < CountOf(imfEffects)) ? imfEffects[m.command] : CMD_NONE;
|
||
|
if(m.command == CMD_VOLUME && m.volcmd == VOLCMD_NONE)
|
||
|
{
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.vol = m.param;
|
||
|
m.command = CMD_NONE;
|
||
|
m.param = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
static bool ValidateHeader(const IMFFileHeader &fileHeader)
|
||
|
{
|
||
|
if(std::memcmp(fileHeader.im10, "IM10", 4)
|
||
|
|| fileHeader.ordNum > 256
|
||
|
|| fileHeader.insNum >= MAX_INSTRUMENTS
|
||
|
|| fileHeader.bpm < 32
|
||
|
|| fileHeader.master > 64
|
||
|
|| fileHeader.amp < 4
|
||
|
|| fileHeader.amp > 127)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
bool channelFound = false;
|
||
|
for(const auto &chn : fileHeader.channels)
|
||
|
{
|
||
|
switch(chn.status)
|
||
|
{
|
||
|
case 0: // enabled; don't worry about it
|
||
|
channelFound = true;
|
||
|
break;
|
||
|
case 1: // mute
|
||
|
channelFound = true;
|
||
|
break;
|
||
|
case 2: // disabled
|
||
|
// nothing
|
||
|
break;
|
||
|
default: // uhhhh.... freak out
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
if(!channelFound)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
static uint64 GetHeaderMinimumAdditionalSize(const IMFFileHeader &fileHeader)
|
||
|
{
|
||
|
return 256 + fileHeader.patNum * 4 + fileHeader.insNum * sizeof(IMFInstrument);
|
||
|
}
|
||
|
|
||
|
|
||
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderIMF(MemoryFileReader file, const uint64 *pfilesize)
|
||
|
{
|
||
|
IMFFileHeader fileHeader;
|
||
|
if(!file.ReadStruct(fileHeader))
|
||
|
{
|
||
|
return ProbeWantMoreData;
|
||
|
}
|
||
|
if(!ValidateHeader(fileHeader))
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CSoundFile::ReadIMF(FileReader &file, ModLoadingFlags loadFlags)
|
||
|
{
|
||
|
IMFFileHeader fileHeader;
|
||
|
file.Rewind();
|
||
|
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;
|
||
|
}
|
||
|
|
||
|
// Read channel configuration
|
||
|
std::bitset<32> ignoreChannels; // bit set for each channel that's completely disabled
|
||
|
uint8 detectedChannels = 0;
|
||
|
for(uint8 chn = 0; chn < 32; chn++)
|
||
|
{
|
||
|
ChnSettings[chn].Reset();
|
||
|
ChnSettings[chn].nPan = fileHeader.channels[chn].panning * 256 / 255;
|
||
|
|
||
|
ChnSettings[chn].szName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.channels[chn].name);
|
||
|
|
||
|
// TODO: reverb/chorus?
|
||
|
switch(fileHeader.channels[chn].status)
|
||
|
{
|
||
|
case 0: // enabled; don't worry about it
|
||
|
detectedChannels = chn + 1;
|
||
|
break;
|
||
|
case 1: // mute
|
||
|
ChnSettings[chn].dwFlags = CHN_MUTE;
|
||
|
detectedChannels = chn + 1;
|
||
|
break;
|
||
|
case 2: // disabled
|
||
|
ChnSettings[chn].dwFlags = CHN_MUTE;
|
||
|
ignoreChannels[chn] = true;
|
||
|
break;
|
||
|
default: // uhhhh.... freak out
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
InitializeGlobals(MOD_TYPE_IMF);
|
||
|
m_nChannels = detectedChannels;
|
||
|
|
||
|
m_modFormat.formatName = U_("Imago Orpheus");
|
||
|
m_modFormat.type = U_("imf");
|
||
|
m_modFormat.charset = mpt::Charset::CP437;
|
||
|
|
||
|
//From mikmod: work around an Orpheus bug
|
||
|
if(fileHeader.channels[0].status == 0)
|
||
|
{
|
||
|
CHANNELINDEX chn;
|
||
|
for(chn = 1; chn < 16; chn++)
|
||
|
if(fileHeader.channels[chn].status != 1)
|
||
|
break;
|
||
|
if(chn == 16)
|
||
|
for(chn = 1; chn < 16; chn++)
|
||
|
ChnSettings[chn].dwFlags.reset(CHN_MUTE);
|
||
|
}
|
||
|
|
||
|
// Song Name
|
||
|
m_songName = mpt::String::ReadBuf(mpt::String::nullTerminated, fileHeader.title);
|
||
|
|
||
|
m_SongFlags.set(SONG_LINEARSLIDES, fileHeader.flags & IMFFileHeader::linearSlides);
|
||
|
m_nDefaultSpeed = fileHeader.tempo;
|
||
|
m_nDefaultTempo.Set(fileHeader.bpm);
|
||
|
m_nDefaultGlobalVolume = fileHeader.master * 4u;
|
||
|
m_nSamplePreAmp = fileHeader.amp;
|
||
|
|
||
|
m_nInstruments = fileHeader.insNum;
|
||
|
m_nSamples = 0; // Will be incremented later
|
||
|
|
||
|
uint8 orders[256];
|
||
|
file.ReadArray(orders);
|
||
|
ReadOrderFromArray(Order(), orders, fileHeader.ordNum, uint16_max, 0xFF);
|
||
|
|
||
|
// Read patterns
|
||
|
if(loadFlags & loadPatternData)
|
||
|
Patterns.ResizeArray(fileHeader.patNum);
|
||
|
for(PATTERNINDEX pat = 0; pat < fileHeader.patNum; pat++)
|
||
|
{
|
||
|
const uint16 length = file.ReadUint16LE(), numRows = file.ReadUint16LE();
|
||
|
FileReader patternChunk = file.ReadChunk(length - 4);
|
||
|
|
||
|
if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, numRows))
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
ModCommand dummy;
|
||
|
ROWINDEX row = 0;
|
||
|
while(row < numRows)
|
||
|
{
|
||
|
uint8 mask = patternChunk.ReadUint8();
|
||
|
if(mask == 0)
|
||
|
{
|
||
|
row++;
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
uint8 channel = mask & 0x1F;
|
||
|
ModCommand &m = (channel < GetNumChannels()) ? *Patterns[pat].GetpModCommand(row, channel) : dummy;
|
||
|
|
||
|
if(mask & 0x20)
|
||
|
{
|
||
|
// Read note/instrument
|
||
|
const auto [note, instr] = patternChunk.ReadArray<uint8, 2>();
|
||
|
m.note = note;
|
||
|
m.instr = instr;
|
||
|
|
||
|
if(m.note == 160)
|
||
|
{
|
||
|
m.note = NOTE_KEYOFF;
|
||
|
} else if(m.note == 255)
|
||
|
{
|
||
|
m.note = NOTE_NONE;
|
||
|
} else
|
||
|
{
|
||
|
m.note = (m.note >> 4) * 12 + (m.note & 0x0F) + 12 + 1;
|
||
|
if(!m.IsNoteOrEmpty())
|
||
|
{
|
||
|
m.note = NOTE_NONE;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if((mask & 0xC0) == 0xC0)
|
||
|
{
|
||
|
// Read both effects and figure out what to do with them
|
||
|
const auto [e1c, e1d, e2c, e2d] = patternChunk.ReadArray<uint8, 4>(); // Command 1, Data 1, Command 2, Data 2
|
||
|
|
||
|
if(e1c == 0x0C)
|
||
|
{
|
||
|
m.vol = std::min(e1d, uint8(0x40));
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.command = e2c;
|
||
|
m.param = e2d;
|
||
|
} else if(e2c == 0x0C)
|
||
|
{
|
||
|
m.vol = std::min(e2d, uint8(0x40));
|
||
|
m.volcmd = VOLCMD_VOLUME;
|
||
|
m.command = e1c;
|
||
|
m.param = e1d;
|
||
|
} else if(e1c == 0x0A)
|
||
|
{
|
||
|
m.vol = e1d * 64 / 255;
|
||
|
m.volcmd = VOLCMD_PANNING;
|
||
|
m.command = e2c;
|
||
|
m.param = e2d;
|
||
|
} else if(e2c == 0x0A)
|
||
|
{
|
||
|
m.vol = e2d * 64 / 255;
|
||
|
m.volcmd = VOLCMD_PANNING;
|
||
|
m.command = e1c;
|
||
|
m.param = e1d;
|
||
|
} else
|
||
|
{
|
||
|
/* check if one of the effects is a 'global' effect
|
||
|
-- if so, put it in some unused channel instead.
|
||
|
otherwise pick the most important effect. */
|
||
|
m.command = e2c;
|
||
|
m.param = e2d;
|
||
|
}
|
||
|
} else if(mask & 0xC0)
|
||
|
{
|
||
|
// There's one effect, just stick it in the effect column
|
||
|
const auto [command, param] = patternChunk.ReadArray<uint8, 2>();
|
||
|
m.command = command;
|
||
|
m.param = param;
|
||
|
}
|
||
|
if(m.command)
|
||
|
ImportIMFEffect(m);
|
||
|
if(ignoreChannels[channel] && m.IsGlobalCommand())
|
||
|
m.command = CMD_NONE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SAMPLEINDEX firstSample = 1; // first sample index of the current instrument
|
||
|
|
||
|
// read instruments
|
||
|
for(INSTRUMENTINDEX ins = 0; ins < GetNumInstruments(); ins++)
|
||
|
{
|
||
|
ModInstrument *instr = AllocateInstrument(ins + 1);
|
||
|
IMFInstrument instrumentHeader;
|
||
|
if(!file.ReadStruct(instrumentHeader) || instr == nullptr)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Orpheus does not check this!
|
||
|
//if(memcmp(instrumentHeader.ii10, "II10", 4) != 0)
|
||
|
// return false;
|
||
|
instrumentHeader.ConvertToMPT(*instr, firstSample);
|
||
|
|
||
|
// Read this instrument's samples
|
||
|
for(SAMPLEINDEX smp = 0; smp < instrumentHeader.smpNum; smp++)
|
||
|
{
|
||
|
IMFSample sampleHeader;
|
||
|
file.ReadStruct(sampleHeader);
|
||
|
|
||
|
const SAMPLEINDEX smpID = firstSample + smp;
|
||
|
if(memcmp(sampleHeader.is10, "IS10", 4) || smpID >= MAX_SAMPLES)
|
||
|
{
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
m_nSamples = smpID;
|
||
|
ModSample &sample = Samples[smpID];
|
||
|
|
||
|
sampleHeader.ConvertToMPT(sample);
|
||
|
m_szNames[smpID] = sample.filename;
|
||
|
|
||
|
if(sampleHeader.length)
|
||
|
{
|
||
|
FileReader sampleChunk = file.ReadChunk(sampleHeader.length);
|
||
|
if(loadFlags & loadSampleData)
|
||
|
{
|
||
|
SampleIO(
|
||
|
sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
|
||
|
SampleIO::mono,
|
||
|
SampleIO::littleEndian,
|
||
|
SampleIO::signedPCM)
|
||
|
.ReadSample(sample, sampleChunk);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
firstSample += instrumentHeader.smpNum;
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|