1174 lines
32 KiB
C++
1174 lines
32 KiB
C++
/*
|
|
* Load_mt2.cpp
|
|
* ------------
|
|
* Purpose: MT2 (MadTracker 2) module loader
|
|
* Notes : A couple of things are not handled properly or not at all, such as internal effects and automation envelopes
|
|
* Authors: Olivier Lapicque
|
|
* OpenMPT Devs
|
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
|
*/
|
|
|
|
|
|
#include "stdafx.h"
|
|
#include "Loaders.h"
|
|
#ifdef MPT_EXTERNAL_SAMPLES
|
|
// For loading external samples
|
|
#include "../common/mptPathString.h"
|
|
#endif // MPT_EXTERNAL_SAMPLES
|
|
#ifdef MPT_WITH_VST
|
|
#include "../mptrack/Vstplug.h"
|
|
#endif // MPT_WITH_VST
|
|
|
|
OPENMPT_NAMESPACE_BEGIN
|
|
|
|
struct MT2FileHeader
|
|
{
|
|
enum MT2HeaderFlags
|
|
{
|
|
packedPatterns = 0x01,
|
|
automation = 0x02,
|
|
drumsAutomation = 0x08,
|
|
masterAutomation = 0x10,
|
|
};
|
|
|
|
char signature[4]; // "MT20"
|
|
uint32le userID;
|
|
uint16le version;
|
|
char trackerName[32]; // "MadTracker 2.0"
|
|
char songName[64];
|
|
uint16le numOrders;
|
|
uint16le restartPos;
|
|
uint16le numPatterns;
|
|
uint16le numChannels;
|
|
uint16le samplesPerTick;
|
|
uint8le ticksPerLine;
|
|
uint8le linesPerBeat;
|
|
uint32le flags; // See HeaderFlags
|
|
uint16le numInstruments;
|
|
uint16le numSamples;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2FileHeader, 126)
|
|
|
|
|
|
struct MT2DrumsData
|
|
{
|
|
uint16le numDrumPatterns;
|
|
uint16le DrumSamples[8];
|
|
uint8le DrumPatternOrder[256];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2DrumsData, 274)
|
|
|
|
|
|
struct MT2TrackSettings
|
|
{
|
|
uint16le volume;
|
|
uint8le trackfx; // Built-in effect type is used
|
|
uint8le output;
|
|
uint16le fxID;
|
|
uint16le trackEffectParam[64][8];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2TrackSettings, 1030)
|
|
|
|
|
|
struct MT2Command
|
|
{
|
|
uint8 note; // 0=nothing, 97=note off
|
|
uint8 instr;
|
|
uint8 vol;
|
|
uint8 pan;
|
|
uint8 fxcmd;
|
|
uint8 fxparam1;
|
|
uint8 fxparam2;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2Command, 7)
|
|
|
|
|
|
struct MT2EnvPoint
|
|
{
|
|
uint16le x;
|
|
uint16le y;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2EnvPoint, 4)
|
|
|
|
|
|
struct MT2Instrument
|
|
{
|
|
enum EnvTypes
|
|
{
|
|
VolumeEnv = 1,
|
|
PanningEnv = 2,
|
|
PitchEnv = 4,
|
|
FilterEnv = 8,
|
|
};
|
|
|
|
uint16le numSamples;
|
|
uint8le groupMap[96];
|
|
uint8le vibtype, vibsweep, vibdepth, vibrate;
|
|
uint16le fadeout;
|
|
uint16le nna;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2Instrument, 106)
|
|
|
|
|
|
struct MT2IEnvelope
|
|
{
|
|
uint8le flags;
|
|
uint8le numPoints;
|
|
uint8le sustainPos;
|
|
uint8le loopStart;
|
|
uint8le loopEnd;
|
|
uint8le reserved[3];
|
|
MT2EnvPoint points[16];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2IEnvelope, 72)
|
|
|
|
|
|
// Note: The order of these fields differs a bit in MTIOModule_MT2.cpp - maybe just typos, I'm not sure.
|
|
// This struct follows the save format of MadTracker 2.6.1.
|
|
struct MT2InstrSynth
|
|
{
|
|
uint8le synthID;
|
|
uint8le effectID; // 0 = Lowpass filter, 1 = Highpass filter
|
|
uint16le cutoff; // 100...11000 Hz
|
|
uint8le resonance; // 0...128
|
|
uint8le attack; // 0...128
|
|
uint8le decay; // 0...128
|
|
uint8le midiChannel; // 0...15
|
|
int8le device; // VST slot (positive) or MIDI device (negative)
|
|
int8le unknown1; // Missing in MTIOModule_MT2.cpp
|
|
uint8le volume; // 0...255
|
|
int8le finetune; // -96...96
|
|
int8le transpose; // -48...48
|
|
uint8le unknown2; // Seems to be equal to instrument number.
|
|
uint8le unknown3;
|
|
uint8le midiProgram;
|
|
uint8le reserved[16];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2InstrSynth, 32)
|
|
|
|
|
|
struct MT2Sample
|
|
{
|
|
uint32le length;
|
|
uint32le frequency;
|
|
uint8le depth;
|
|
uint8le channels;
|
|
uint8le flags;
|
|
uint8le loopType;
|
|
uint32le loopStart;
|
|
uint32le loopEnd;
|
|
uint16le volume;
|
|
int8le panning;
|
|
int8le note;
|
|
int16le spb;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2Sample, 26)
|
|
|
|
|
|
struct MT2Group
|
|
{
|
|
uint8le sample;
|
|
uint8le vol; // 0...128
|
|
int8le pitch; // -128...127
|
|
uint8le reserved[5];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2Group, 8)
|
|
|
|
|
|
struct MT2VST
|
|
{
|
|
char dll[64];
|
|
char programName[28];
|
|
uint32le fxID;
|
|
uint32le fxVersion;
|
|
uint32le programNr;
|
|
uint8le useChunks;
|
|
uint8le track;
|
|
int8le pan; // Not imported - could use pan mix mode for D/W ratio, but this is not implemented for instrument plugins!
|
|
char reserved[17];
|
|
uint32le n;
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(MT2VST, 128)
|
|
|
|
|
|
static bool ConvertMT2Command(CSoundFile *that, ModCommand &m, MT2Command &p)
|
|
{
|
|
bool hasLegacyTempo = false;
|
|
|
|
// Note
|
|
m.note = NOTE_NONE;
|
|
if(p.note) m.note = (p.note > 96) ? NOTE_KEYOFF : (p.note + NOTE_MIN + 11);
|
|
// Instrument
|
|
m.instr = p.instr;
|
|
// Volume Column
|
|
if(p.vol >= 0x10 && p.vol <= 0x90)
|
|
{
|
|
m.volcmd = VOLCMD_VOLUME;
|
|
m.vol = (p.vol - 0x10) / 2;
|
|
} else if(p.vol >= 0xA0 && p.vol <= 0xAF)
|
|
{
|
|
m.volcmd = VOLCMD_VOLSLIDEDOWN;
|
|
m.vol = (p.vol & 0x0F);
|
|
} else if(p.vol >= 0xB0 && p.vol <= 0xBF)
|
|
{
|
|
m.volcmd = VOLCMD_VOLSLIDEUP;
|
|
m.vol = (p.vol & 0x0F);
|
|
} else if(p.vol >= 0xC0 && p.vol <= 0xCF)
|
|
{
|
|
m.volcmd = VOLCMD_FINEVOLDOWN;
|
|
m.vol = (p.vol & 0x0F);
|
|
} else if(p.vol >= 0xD0 && p.vol <= 0xDF)
|
|
{
|
|
m.volcmd = VOLCMD_FINEVOLUP;
|
|
m.vol = (p.vol & 0x0F);
|
|
}
|
|
|
|
// Effects
|
|
if(p.fxcmd || p.fxparam1 || p.fxparam2)
|
|
{
|
|
switch(p.fxcmd)
|
|
{
|
|
case 0x00: // FastTracker effect
|
|
m.command = p.fxparam2;
|
|
m.param = p.fxparam1;
|
|
CSoundFile::ConvertModCommand(m);
|
|
#ifdef MODPLUG_TRACKER
|
|
m.Convert(MOD_TYPE_XM, MOD_TYPE_IT, *that);
|
|
#else
|
|
MPT_UNREFERENCED_PARAMETER(that);
|
|
#endif // MODPLUG_TRACKER
|
|
if(p.fxparam2 == 0x0F)
|
|
hasLegacyTempo = true;
|
|
break;
|
|
|
|
case 0x01: // Portamento up (on every tick)
|
|
m.command = CMD_PORTAMENTOUP;
|
|
m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4));
|
|
break;
|
|
|
|
case 0x02: // Portamento down (on every tick)
|
|
m.command = CMD_PORTAMENTODOWN;
|
|
m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4));
|
|
break;
|
|
|
|
case 0x03: // Tone Portamento (on every tick)
|
|
m.command = CMD_TONEPORTAMENTO;
|
|
m.param = mpt::saturate_cast<ModCommand::PARAM>((p.fxparam2 << 4) | (p.fxparam1 >> 4));
|
|
break;
|
|
|
|
case 0x04: // Vibrato
|
|
m.command = CMD_VIBRATO;
|
|
m.param = (p.fxparam2 & 0xF0) | (p.fxparam1 >> 4);
|
|
break;
|
|
|
|
case 0x08: // Panning + Polarity (we can only import panning for now)
|
|
if(p.fxparam1)
|
|
{
|
|
m.command = CMD_PANNING8;
|
|
m.param = p.fxparam1;
|
|
} else if(p.fxparam2 == 1 || p.fxparam2 == 2)
|
|
{
|
|
// Invert left or right channel
|
|
m.command = CMD_S3MCMDEX;
|
|
m.param = 0x91;
|
|
}
|
|
break;
|
|
|
|
case 0x0C: // Set volume (0x80 = 100%)
|
|
m.command = CMD_VOLUME;
|
|
m.param = p.fxparam2 / 2;
|
|
break;
|
|
|
|
case 0x0F: // Set tempo, LPB and ticks (we can only import tempo for now)
|
|
if(p.fxparam2 != 0)
|
|
{
|
|
m.command = CMD_TEMPO;
|
|
m.param = p.fxparam2;
|
|
} else
|
|
{
|
|
m.command = CMD_SPEED;
|
|
m.param = (p.fxparam1 & 0x0F);
|
|
}
|
|
break;
|
|
|
|
case 0x10: // Impulse Tracker effect
|
|
m.command = p.fxparam2;
|
|
m.param = p.fxparam1;
|
|
CSoundFile::S3MConvert(m, true);
|
|
if(m.command == CMD_TEMPO || m.command == CMD_SPEED)
|
|
hasLegacyTempo = true;
|
|
break;
|
|
|
|
case 0x1D: // Gapper (like IT Tremor with old FX, i.e. 1D 00 XY = ontime X + 1 ticks, offtime Y + 1 ticks)
|
|
m.command = CMD_TREMOR;
|
|
m.param = p.fxparam1;
|
|
break;
|
|
|
|
case 0x20: // Cutoff + Resonance (we can only import cutoff for now)
|
|
m.command = CMD_MIDI;
|
|
m.param = p.fxparam2 >> 1;
|
|
break;
|
|
|
|
case 0x22: // Cutoff + Resonance + Attack + Decay (we can only import cutoff for now)
|
|
m.command = CMD_MIDI;
|
|
m.param = (p.fxparam2 & 0xF0) >> 1;
|
|
break;
|
|
|
|
case 0x24: // Reverse
|
|
m.command = CMD_S3MCMDEX;
|
|
m.param = 0x9F;
|
|
break;
|
|
|
|
case 0x80: // Track volume
|
|
m.command = CMD_CHANNELVOLUME;
|
|
m.param = p.fxparam2 / 4u;
|
|
break;
|
|
|
|
case 0x9D: // Offset + delay
|
|
m.volcmd = VOLCMD_OFFSET;
|
|
m.vol = p.fxparam2 >> 3;
|
|
m.command = CMD_S3MCMDEX;
|
|
m.param = 0xD0 | std::min(p.fxparam1, uint8(0x0F));
|
|
break;
|
|
|
|
case 0xCC: // MIDI CC
|
|
//m.command = CMD_MIDI;
|
|
break;
|
|
|
|
// TODO: More MT2 Effects
|
|
}
|
|
}
|
|
|
|
if(p.pan)
|
|
{
|
|
if(m.command == CMD_NONE)
|
|
{
|
|
m.command = CMD_PANNING8;
|
|
m.param = p.pan;
|
|
} else if(m.volcmd == VOLCMD_NONE)
|
|
{
|
|
m.volcmd = VOLCMD_PANNING;
|
|
m.vol = p.pan / 4;
|
|
}
|
|
}
|
|
|
|
return hasLegacyTempo;
|
|
}
|
|
|
|
|
|
// This doesn't really do anything but skipping the envelope chunk at the moment.
|
|
static void ReadMT2Automation(uint16 version, FileReader &file)
|
|
{
|
|
uint32 flags;
|
|
uint32 trkfxid;
|
|
if(version >= 0x203)
|
|
{
|
|
flags = file.ReadUint32LE();
|
|
trkfxid = file.ReadUint32LE();
|
|
} else
|
|
{
|
|
flags = file.ReadUint16LE();
|
|
trkfxid = file.ReadUint16LE();
|
|
}
|
|
MPT_UNREFERENCED_PARAMETER(trkfxid);
|
|
while(flags != 0)
|
|
{
|
|
if(flags & 1)
|
|
{
|
|
file.Skip(4 + sizeof(MT2EnvPoint) * 64);
|
|
}
|
|
flags >>= 1;
|
|
}
|
|
}
|
|
|
|
|
|
static bool ValidateHeader(const MT2FileHeader &fileHeader)
|
|
{
|
|
if(std::memcmp(fileHeader.signature, "MT20", 4)
|
|
|| fileHeader.version < 0x200 || fileHeader.version >= 0x300
|
|
|| fileHeader.numChannels < 1 || fileHeader.numChannels > 64
|
|
|| fileHeader.numOrders > 256
|
|
|| fileHeader.numInstruments >= MAX_INSTRUMENTS
|
|
|| fileHeader.numSamples >= MAX_SAMPLES
|
|
)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
static uint64 GetHeaderMinimumAdditionalSize(const MT2FileHeader &fileHeader)
|
|
{
|
|
MPT_UNREFERENCED_PARAMETER(fileHeader);
|
|
return 256;
|
|
}
|
|
|
|
|
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderMT2(MemoryFileReader file, const uint64 *pfilesize)
|
|
{
|
|
MT2FileHeader fileHeader;
|
|
if(!file.ReadStruct(fileHeader))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
if(!ValidateHeader(fileHeader))
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
|
|
}
|
|
|
|
|
|
bool CSoundFile::ReadMT2(FileReader &file, ModLoadingFlags loadFlags)
|
|
{
|
|
file.Rewind();
|
|
MT2FileHeader 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;
|
|
}
|
|
|
|
InitializeGlobals(MOD_TYPE_MT2);
|
|
InitializeChannels();
|
|
|
|
m_modFormat.formatName = MPT_UFORMAT("MadTracker {}.{}")(fileHeader.version >> 8, mpt::ufmt::hex0<2>(fileHeader.version & 0xFF));
|
|
m_modFormat.type = U_("mt2");
|
|
m_modFormat.madeWithTracker = mpt::ToUnicode(mpt::Charset::Windows1252, mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.trackerName));
|
|
m_modFormat.charset = mpt::Charset::Windows1252;
|
|
|
|
m_songName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, fileHeader.songName);
|
|
m_nChannels = fileHeader.numChannels;
|
|
m_nDefaultSpeed = Clamp<uint8, uint8>(fileHeader.ticksPerLine, 1, 31);
|
|
m_nDefaultTempo.Set(125);
|
|
m_SongFlags = SONG_LINEARSLIDES | SONG_ITCOMPATGXX | SONG_EXFILTERRANGE;
|
|
m_nInstruments = fileHeader.numInstruments;
|
|
m_nSamples = fileHeader.numSamples;
|
|
m_nDefaultRowsPerBeat = Clamp<uint8, uint8>(fileHeader.linesPerBeat, 1, 32);
|
|
m_nDefaultRowsPerMeasure = m_nDefaultRowsPerBeat * 4;
|
|
m_nVSTiVolume = 48;
|
|
m_nSamplePreAmp = 48 * 2; // Double pre-amp because we will halve the volume of all non-drum instruments, because the volume of drum samples can exceed that of normal samples
|
|
|
|
uint8 orders[256];
|
|
file.ReadArray(orders);
|
|
ReadOrderFromArray(Order(), orders, fileHeader.numOrders);
|
|
Order().SetRestartPos(fileHeader.restartPos);
|
|
|
|
// This value is supposed to be the size of the drums data, but in old MT2.0 files it's 8 bytes too small.
|
|
// MadTracker itself unconditionally reads 274 bytes here if the value is != 0, so we do the same.
|
|
const bool hasDrumChannels = file.ReadUint16LE() != 0;
|
|
FileReader drumData = file.ReadChunk(hasDrumChannels ? sizeof(MT2DrumsData) : 0);
|
|
FileReader extraData = file.ReadChunk(file.ReadUint32LE());
|
|
|
|
const CHANNELINDEX channelsWithoutDrums = m_nChannels;
|
|
static_assert(MAX_BASECHANNELS >= 64 + 8);
|
|
if(hasDrumChannels)
|
|
{
|
|
m_nChannels += 8;
|
|
}
|
|
|
|
bool hasLegacyTempo = false;
|
|
|
|
// Read patterns
|
|
if(loadFlags & loadPatternData)
|
|
Patterns.ResizeArray(fileHeader.numPatterns);
|
|
for(PATTERNINDEX pat = 0; pat < fileHeader.numPatterns; pat++)
|
|
{
|
|
ROWINDEX numRows = file.ReadUint16LE();
|
|
FileReader chunk = file.ReadChunk((file.ReadUint32LE() + 1) & ~1);
|
|
|
|
LimitMax(numRows, MAX_PATTERN_ROWS);
|
|
if(!numRows
|
|
|| !(loadFlags & loadPatternData)
|
|
|| !Patterns.Insert(pat, numRows))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if(fileHeader.flags & MT2FileHeader::packedPatterns)
|
|
{
|
|
ROWINDEX row = 0;
|
|
CHANNELINDEX chn = 0;
|
|
while(chunk.CanRead(1))
|
|
{
|
|
MT2Command cmd;
|
|
|
|
uint8 infobyte = chunk.ReadUint8();
|
|
uint8 repeatCount = 0;
|
|
if(infobyte == 0xFF)
|
|
{
|
|
repeatCount = chunk.ReadUint8();
|
|
infobyte = chunk.ReadUint8();
|
|
}
|
|
if(infobyte & 0x7F)
|
|
{
|
|
ModCommand *m = Patterns[pat].GetpModCommand(row, chn);
|
|
MemsetZero(cmd);
|
|
if(infobyte & 0x01) cmd.note = chunk.ReadUint8();
|
|
if(infobyte & 0x02) cmd.instr = chunk.ReadUint8();
|
|
if(infobyte & 0x04) cmd.vol = chunk.ReadUint8();
|
|
if(infobyte & 0x08) cmd.pan = chunk.ReadUint8();
|
|
if(infobyte & 0x10) cmd.fxcmd = chunk.ReadUint8();
|
|
if(infobyte & 0x20) cmd.fxparam1 = chunk.ReadUint8();
|
|
if(infobyte & 0x40) cmd.fxparam2 = chunk.ReadUint8();
|
|
hasLegacyTempo |= ConvertMT2Command(this, *m, cmd);
|
|
const ModCommand &orig = *m;
|
|
const ROWINDEX fillRows = std::min((uint32)repeatCount, (uint32)numRows - (row + 1));
|
|
for(ROWINDEX r = 0; r < fillRows; r++)
|
|
{
|
|
m += GetNumChannels();
|
|
// cppcheck false-positive
|
|
// cppcheck-suppress selfAssignment
|
|
*m = orig;
|
|
}
|
|
}
|
|
row += repeatCount + 1;
|
|
while(row >= numRows) { row -= numRows; chn++; }
|
|
if(chn >= channelsWithoutDrums) break;
|
|
}
|
|
} else
|
|
{
|
|
for(ROWINDEX row = 0; row < numRows; row++)
|
|
{
|
|
auto rowData = Patterns[pat].GetRow(row);
|
|
for(CHANNELINDEX chn = 0; chn < channelsWithoutDrums; chn++)
|
|
{
|
|
MT2Command cmd;
|
|
chunk.ReadStruct(cmd);
|
|
hasLegacyTempo |= ConvertMT2Command(this, rowData[chn], cmd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(fileHeader.samplesPerTick > 1 && fileHeader.samplesPerTick < 5000)
|
|
{
|
|
if(hasLegacyTempo)
|
|
{
|
|
m_nDefaultTempo.SetRaw(Util::muldivr(110250, TEMPO::fractFact, fileHeader.samplesPerTick));
|
|
m_nTempoMode = TempoMode::Classic;
|
|
} else
|
|
{
|
|
m_nDefaultTempo = TEMPO(44100.0 * 60.0 / (m_nDefaultSpeed * m_nDefaultRowsPerBeat * fileHeader.samplesPerTick));
|
|
m_nTempoMode = TempoMode::Modern;
|
|
}
|
|
}
|
|
|
|
// Read extra data
|
|
uint32 numVST = 0;
|
|
std::vector<int8> trackRouting(GetNumChannels(), 0);
|
|
while(extraData.CanRead(8))
|
|
{
|
|
uint32 id = extraData.ReadUint32LE();
|
|
FileReader chunk = extraData.ReadChunk(extraData.ReadUint32LE());
|
|
|
|
switch(id)
|
|
{
|
|
case MagicLE("BPM+"):
|
|
if(!hasLegacyTempo)
|
|
{
|
|
m_nTempoMode = TempoMode::Modern;
|
|
double d = chunk.ReadDoubleLE();
|
|
if(d > 0.00000001)
|
|
{
|
|
m_nDefaultTempo = TEMPO(44100.0 * 60.0 / (m_nDefaultSpeed * m_nDefaultRowsPerBeat * d));
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MagicLE("TFXM"):
|
|
break;
|
|
|
|
case MagicLE("TRKO"):
|
|
break;
|
|
|
|
case MagicLE("TRKS"):
|
|
m_nSamplePreAmp = chunk.ReadUint16LE() / 256u; // 131072 is 0dB... I think (that's how MTIOModule_MT2.cpp reads)
|
|
// Dirty workaround for modules that use track automation for a fade-in at the song start (e.g. Rock.mt2)
|
|
if(!m_nSamplePreAmp)
|
|
m_nSamplePreAmp = 48;
|
|
m_nVSTiVolume = m_nSamplePreAmp / 2u;
|
|
for(CHANNELINDEX c = 0; c < GetNumChannels(); c++)
|
|
{
|
|
MT2TrackSettings trackSettings;
|
|
if(chunk.ReadStruct(trackSettings))
|
|
{
|
|
ChnSettings[c].nVolume = static_cast<uint8>(trackSettings.volume >> 10); // 32768 is 0dB
|
|
trackRouting[c] = trackSettings.output;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MagicLE("TRKL"):
|
|
for(CHANNELINDEX i = 0; i < m_nChannels && chunk.CanRead(1); i++)
|
|
{
|
|
std::string name;
|
|
chunk.ReadNullString(name);
|
|
ChnSettings[i].szName = mpt::String::ReadBuf(mpt::String::spacePadded, name.c_str(), name.length());
|
|
}
|
|
break;
|
|
|
|
case MagicLE("PATN"):
|
|
for(PATTERNINDEX i = 0; i < fileHeader.numPatterns && chunk.CanRead(1) && Patterns.IsValidIndex(i); i++)
|
|
{
|
|
std::string name;
|
|
chunk.ReadNullString(name);
|
|
Patterns[i].SetName(name);
|
|
}
|
|
break;
|
|
|
|
case MagicLE("MSG\0"):
|
|
chunk.Skip(1); // Show message on startup
|
|
m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF);
|
|
break;
|
|
|
|
case MagicLE("PICT"):
|
|
break;
|
|
|
|
case MagicLE("SUM\0"):
|
|
{
|
|
uint8 summaryMask[6];
|
|
chunk.ReadArray(summaryMask);
|
|
std::string artist;
|
|
chunk.ReadNullString(artist);
|
|
if(artist != "Unregistered")
|
|
{
|
|
m_songArtist = mpt::ToUnicode(mpt::Charset::Windows1252, artist);
|
|
}
|
|
}
|
|
break;
|
|
|
|
case MagicLE("TMAP"):
|
|
break;
|
|
case MagicLE("MIDI"):
|
|
break;
|
|
case MagicLE("TREQ"):
|
|
break;
|
|
|
|
case MagicLE("VST2"):
|
|
numVST = chunk.ReadUint32LE();
|
|
#ifdef MPT_WITH_VST
|
|
if(!(loadFlags & loadPluginData))
|
|
{
|
|
break;
|
|
}
|
|
for(uint32 i = 0; i < std::min(numVST, uint32(MAX_MIXPLUGINS)); i++)
|
|
{
|
|
MT2VST vstHeader;
|
|
if(chunk.ReadStruct(vstHeader))
|
|
{
|
|
if(fileHeader.version >= 0x0250)
|
|
chunk.Skip(16 * 4); // Parameter automation map for 16 parameters
|
|
|
|
SNDMIXPLUGIN &mixPlug = m_MixPlugins[i];
|
|
mixPlug.Destroy();
|
|
std::string libraryName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, vstHeader.dll);
|
|
mixPlug.Info.szName = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, vstHeader.programName);
|
|
if(libraryName.length() > 4 && libraryName[libraryName.length() - 4] == '.')
|
|
{
|
|
// Remove ".dll" from library name
|
|
libraryName.resize(libraryName.length() - 4 );
|
|
}
|
|
mixPlug.Info.szLibraryName = libraryName;
|
|
mixPlug.Info.dwPluginId1 = Vst::kEffectMagic;
|
|
mixPlug.Info.dwPluginId2 = vstHeader.fxID;
|
|
if(vstHeader.track >= m_nChannels)
|
|
{
|
|
mixPlug.SetMasterEffect(true);
|
|
} else
|
|
{
|
|
if(!ChnSettings[vstHeader.track].nMixPlugin)
|
|
{
|
|
ChnSettings[vstHeader.track].nMixPlugin = static_cast<PLUGINDEX>(i + 1);
|
|
} else
|
|
{
|
|
// Channel already has plugin assignment - chain the plugins
|
|
PLUGINDEX outPlug = ChnSettings[vstHeader.track].nMixPlugin - 1;
|
|
while(true)
|
|
{
|
|
if(m_MixPlugins[outPlug].GetOutputPlugin() == PLUGINDEX_INVALID)
|
|
{
|
|
m_MixPlugins[outPlug].SetOutputPlugin(static_cast<PLUGINDEX>(i));
|
|
break;
|
|
}
|
|
outPlug = m_MixPlugins[outPlug].GetOutputPlugin();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read plugin settings
|
|
uint32 dataSize;
|
|
if(vstHeader.useChunks)
|
|
{
|
|
// MT2 only ever calls effGetChunk for programs, and OpenMPT uses the defaultProgram value to determine
|
|
// whether it should use effSetChunk for programs or banks...
|
|
mixPlug.defaultProgram = -1;
|
|
LimitMax(vstHeader.n, std::numeric_limits<decltype(dataSize)>::max() - 4);
|
|
dataSize = vstHeader.n + 4;
|
|
} else
|
|
{
|
|
mixPlug.defaultProgram = vstHeader.programNr;
|
|
LimitMax(vstHeader.n, (std::numeric_limits<decltype(dataSize)>::max() / 4u) - 1);
|
|
dataSize = vstHeader.n * 4 + 4;
|
|
}
|
|
mixPlug.pluginData.resize(dataSize);
|
|
if(vstHeader.useChunks)
|
|
{
|
|
std::memcpy(mixPlug.pluginData.data(), "fEvN", 4); // 'NvEf' plugin data type
|
|
chunk.ReadRaw(mpt::span(mixPlug.pluginData.data() + 4, vstHeader.n));
|
|
} else
|
|
{
|
|
auto memFile = std::make_pair(mpt::as_span(mixPlug.pluginData), mpt::IO::Offset(0));
|
|
mpt::IO::WriteIntLE<uint32>(memFile, 0); // Plugin data type
|
|
for(uint32 param = 0; param < vstHeader.n; param++)
|
|
{
|
|
mpt::IO::Write(memFile, IEEE754binary32LE{chunk.ReadFloatLE()});
|
|
}
|
|
}
|
|
} else
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
#endif // MPT_WITH_VST
|
|
break;
|
|
}
|
|
}
|
|
|
|
#ifndef NO_PLUGINS
|
|
// Now that we have both the track settings and plugins, establish the track routing by applying the same plugins to the source track as to the target track:
|
|
for(CHANNELINDEX c = 0; c < GetNumChannels(); c++)
|
|
{
|
|
int8 outTrack = trackRouting[c];
|
|
if(outTrack > c && outTrack < GetNumChannels() && ChnSettings[outTrack].nMixPlugin != 0)
|
|
{
|
|
if(ChnSettings[c].nMixPlugin == 0)
|
|
{
|
|
ChnSettings[c].nMixPlugin = ChnSettings[outTrack].nMixPlugin;
|
|
} else
|
|
{
|
|
PLUGINDEX outPlug = ChnSettings[c].nMixPlugin - 1;
|
|
for(;;)
|
|
{
|
|
if(m_MixPlugins[outPlug].GetOutputPlugin() == PLUGINDEX_INVALID)
|
|
{
|
|
m_MixPlugins[outPlug].SetOutputPlugin(ChnSettings[outTrack].nMixPlugin - 1);
|
|
break;
|
|
}
|
|
outPlug = m_MixPlugins[outPlug].GetOutputPlugin();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif // NO_PLUGINS
|
|
|
|
// Read drum channels
|
|
INSTRUMENTINDEX drumMap[8] = { 0 };
|
|
uint16 drumSample[8] = { 0 };
|
|
if(hasDrumChannels)
|
|
{
|
|
MT2DrumsData drumHeader;
|
|
drumData.ReadStruct(drumHeader);
|
|
|
|
// Allocate some instruments to handle the drum samples
|
|
for(INSTRUMENTINDEX i = 0; i < 8; i++)
|
|
{
|
|
drumMap[i] = GetNextFreeInstrument(m_nInstruments + 1);
|
|
drumSample[i] = drumHeader.DrumSamples[i];
|
|
if(drumMap[i] != INSTRUMENTINDEX_INVALID)
|
|
{
|
|
ModInstrument *mptIns = AllocateInstrument(drumMap[i], drumHeader.DrumSamples[i] + 1);
|
|
if(mptIns != nullptr)
|
|
{
|
|
mptIns->name = MPT_AFORMAT("Drum #{}")(i+1);
|
|
}
|
|
} else
|
|
{
|
|
drumMap[i] = 0;
|
|
}
|
|
}
|
|
|
|
// Get all the drum pattern chunks
|
|
std::vector<FileReader> patternChunks(drumHeader.numDrumPatterns);
|
|
for(uint32 pat = 0; pat < drumHeader.numDrumPatterns; pat++)
|
|
{
|
|
uint16 numRows = file.ReadUint16LE();
|
|
patternChunks[pat] = file.ReadChunk(numRows * 32);
|
|
}
|
|
|
|
std::vector<PATTERNINDEX> patMapping(fileHeader.numPatterns, PATTERNINDEX_INVALID);
|
|
for(uint32 ord = 0; ord < fileHeader.numOrders; ord++)
|
|
{
|
|
if(drumHeader.DrumPatternOrder[ord] >= drumHeader.numDrumPatterns || Order()[ord] >= fileHeader.numPatterns)
|
|
continue;
|
|
|
|
// Figure out where to write this drum pattern
|
|
PATTERNINDEX writePat = Order()[ord];
|
|
if(patMapping[writePat] == PATTERNINDEX_INVALID)
|
|
{
|
|
patMapping[writePat] = drumHeader.DrumPatternOrder[ord];
|
|
} else if(patMapping[writePat] != drumHeader.DrumPatternOrder[ord])
|
|
{
|
|
// Damn, this pattern has previously used a different drum pattern. Duplicate it...
|
|
PATTERNINDEX newPat = Patterns.Duplicate(writePat);
|
|
if(newPat != PATTERNINDEX_INVALID)
|
|
{
|
|
writePat = newPat;
|
|
Order()[ord] = writePat;
|
|
}
|
|
}
|
|
if(!Patterns.IsValidPat(writePat))
|
|
continue;
|
|
|
|
FileReader &chunk = patternChunks[drumHeader.DrumPatternOrder[ord]];
|
|
chunk.Rewind();
|
|
const ROWINDEX numRows = static_cast<ROWINDEX>(chunk.GetLength() / 32u);
|
|
for(ROWINDEX row = 0; row < Patterns[writePat].GetNumRows(); row++)
|
|
{
|
|
ModCommand *m = Patterns[writePat].GetpModCommand(row, m_nChannels - 8);
|
|
for(CHANNELINDEX chn = 0; chn < 8; chn++, m++)
|
|
{
|
|
*m = ModCommand::Empty();
|
|
if(row >= numRows)
|
|
continue;
|
|
|
|
uint8 drums[4];
|
|
chunk.ReadArray(drums);
|
|
if(drums[0] & 0x80)
|
|
{
|
|
m->note = NOTE_MIDDLEC;
|
|
m->instr = static_cast<ModCommand::INSTR>(drumMap[chn]);
|
|
uint8 delay = drums[0] & 0x1F;
|
|
if(delay)
|
|
{
|
|
LimitMax(delay, uint8(0x0F));
|
|
m->command = CMD_S3MCMDEX;
|
|
m->param = 0xD0 | delay;
|
|
}
|
|
m->volcmd = VOLCMD_VOLUME;
|
|
// Volume is 0...255, but 128 is equivalent to v64 - we compensate this by halving the global volume of all non-drum instruments
|
|
m->vol = static_cast<ModCommand::VOL>((static_cast<uint16>(drums[1]) + 3) / 4u);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read automation envelopes
|
|
if(fileHeader.flags & MT2FileHeader::automation)
|
|
{
|
|
const uint32 numEnvelopes = ((fileHeader.flags & MT2FileHeader::drumsAutomation) ? m_nChannels : channelsWithoutDrums)
|
|
+ ((fileHeader.version >= 0x0250) ? numVST : 0)
|
|
+ ((fileHeader.flags & MT2FileHeader::masterAutomation) ? 1 : 0);
|
|
|
|
for(uint32 pat = 0; pat < fileHeader.numPatterns; pat++)
|
|
{
|
|
for(uint32 env = 0; env < numEnvelopes && file.CanRead(4); env++)
|
|
{
|
|
// TODO
|
|
ReadMT2Automation(fileHeader.version, file);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read instruments
|
|
std::vector<FileReader> instrChunks(255);
|
|
for(INSTRUMENTINDEX i = 0; i < 255; i++)
|
|
{
|
|
char instrName[32];
|
|
file.ReadArray(instrName);
|
|
uint32 dataLength = file.ReadUint32LE();
|
|
if(dataLength == 32) dataLength += 108 + sizeof(MT2IEnvelope) * 4;
|
|
if(fileHeader.version > 0x0201 && dataLength) dataLength += 4;
|
|
|
|
FileReader instrChunk = instrChunks[i] = file.ReadChunk(dataLength);
|
|
|
|
ModInstrument *mptIns = nullptr;
|
|
if(i < fileHeader.numInstruments)
|
|
{
|
|
// Default sample assignment if there is no data chunk? Fixes e.g. instrument 33 in Destiny - Dream Alone.mt2
|
|
mptIns = AllocateInstrument(i + 1, i + 1);
|
|
}
|
|
if(mptIns == nullptr)
|
|
continue;
|
|
|
|
mptIns->name = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, instrName);
|
|
|
|
if(!dataLength)
|
|
continue;
|
|
|
|
MT2Instrument insHeader;
|
|
instrChunk.ReadStruct(insHeader);
|
|
uint16 flags = 0;
|
|
if(fileHeader.version >= 0x0201) flags = instrChunk.ReadUint16LE();
|
|
uint32 envMask = MT2Instrument::VolumeEnv | MT2Instrument::PanningEnv;
|
|
if(fileHeader.version >= 0x0202) envMask = instrChunk.ReadUint32LE();
|
|
|
|
mptIns->nFadeOut = insHeader.fadeout;
|
|
const NewNoteAction NNA[4] = { NewNoteAction::NoteCut, NewNoteAction::Continue, NewNoteAction::NoteOff, NewNoteAction::NoteFade };
|
|
const DuplicateCheckType DCT[4] = { DuplicateCheckType::None, DuplicateCheckType::Note, DuplicateCheckType::Sample, DuplicateCheckType::Instrument };
|
|
const DuplicateNoteAction DNA[4] = { DuplicateNoteAction::NoteCut, DuplicateNoteAction::NoteFade /* actually continue, but IT doesn't have that */, DuplicateNoteAction::NoteOff, DuplicateNoteAction::NoteFade };
|
|
mptIns->nNNA = NNA[insHeader.nna & 3];
|
|
mptIns->nDCT = DCT[(insHeader.nna >> 8) & 3];
|
|
mptIns->nDNA = DNA[(insHeader.nna >> 12) & 3];
|
|
|
|
// Load envelopes
|
|
for(uint32 env = 0; env < 4; env++)
|
|
{
|
|
if(envMask & 1)
|
|
{
|
|
MT2IEnvelope mt2Env;
|
|
instrChunk.ReadStruct(mt2Env);
|
|
|
|
const EnvelopeType envType[4] = { ENV_VOLUME, ENV_PANNING, ENV_PITCH, ENV_PITCH };
|
|
InstrumentEnvelope &mptEnv = mptIns->GetEnvelope(envType[env]);
|
|
|
|
mptEnv.dwFlags.set(ENV_FILTER, (env == 3) && (mt2Env.flags & 1) != 0);
|
|
mptEnv.dwFlags.set(ENV_ENABLED, (mt2Env.flags & 1) != 0);
|
|
mptEnv.dwFlags.set(ENV_SUSTAIN, (mt2Env.flags & 2) != 0);
|
|
mptEnv.dwFlags.set(ENV_LOOP, (mt2Env.flags & 4) != 0);
|
|
mptEnv.resize(std::min(mt2Env.numPoints.get(), uint8(16)));
|
|
mptEnv.nSustainStart = mptEnv.nSustainEnd = mt2Env.sustainPos;
|
|
mptEnv.nLoopStart = mt2Env.loopStart;
|
|
mptEnv.nLoopEnd = mt2Env.loopEnd;
|
|
|
|
for(uint32 p = 0; p < mptEnv.size(); p++)
|
|
{
|
|
mptEnv[p].tick = mt2Env.points[p].x;
|
|
mptEnv[p].value = static_cast<uint8>(Clamp<uint16, uint16>(mt2Env.points[p].y, 0, 64));
|
|
}
|
|
}
|
|
envMask >>= 1;
|
|
}
|
|
if(!mptIns->VolEnv.dwFlags[ENV_ENABLED] && mptIns->nNNA != NewNoteAction::NoteFade)
|
|
{
|
|
mptIns->nFadeOut = int16_max;
|
|
}
|
|
|
|
mptIns->SetCutoff(0x7F, true);
|
|
mptIns->SetResonance(0, true);
|
|
|
|
if(flags)
|
|
{
|
|
MT2InstrSynth synthData;
|
|
instrChunk.ReadStruct(synthData);
|
|
|
|
if(flags & 2)
|
|
{
|
|
mptIns->SetCutoff(FrequencyToCutOff(synthData.cutoff), true);
|
|
mptIns->SetResonance(synthData.resonance, true);
|
|
}
|
|
mptIns->filterMode = synthData.effectID == 1 ? FilterMode::HighPass : FilterMode::LowPass;
|
|
if(flags & 4)
|
|
{
|
|
// VSTi / MIDI synth enabled
|
|
mptIns->nMidiChannel = synthData.midiChannel + 1;
|
|
mptIns->nMixPlug = static_cast<PLUGINDEX>(synthData.device + 1);
|
|
if(synthData.device < 0)
|
|
{
|
|
// TODO: This is a MIDI device - maybe use MIDI I/O plugin to emulate those?
|
|
mptIns->nMidiProgram = synthData.midiProgram + 1; // MT2 only seems to use this for MIDI devices, not VSTis!
|
|
}
|
|
if(synthData.transpose)
|
|
{
|
|
for(uint32 n = 0; n < std::size(mptIns->NoteMap); n++)
|
|
{
|
|
int note = NOTE_MIN + n + synthData.transpose;
|
|
Limit(note, NOTE_MIN, NOTE_MAX);
|
|
mptIns->NoteMap[n] = static_cast<uint8>(note);
|
|
}
|
|
}
|
|
// Instruments with plugin assignments never play samples at the same time!
|
|
mptIns->AssignSample(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read sample headers
|
|
std::bitset<256> sampleNoInterpolation;
|
|
std::bitset<256> sampleSynchronized;
|
|
for(SAMPLEINDEX i = 0; i < 256; i++)
|
|
{
|
|
char sampleName[32];
|
|
file.ReadArray(sampleName);
|
|
uint32 dataLength = file.ReadUint32LE();
|
|
|
|
FileReader sampleChunk = file.ReadChunk(dataLength);
|
|
|
|
if(i < fileHeader.numSamples)
|
|
{
|
|
m_szNames[i + 1] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, sampleName);
|
|
}
|
|
|
|
if(dataLength && i < fileHeader.numSamples)
|
|
{
|
|
ModSample &mptSmp = Samples[i + 1];
|
|
mptSmp.Initialize(MOD_TYPE_IT);
|
|
mptSmp.SetDefaultCuePoints();
|
|
MT2Sample sampleHeader;
|
|
sampleChunk.ReadStruct(sampleHeader);
|
|
|
|
mptSmp.nLength = sampleHeader.length;
|
|
mptSmp.nC5Speed = sampleHeader.frequency;
|
|
if(sampleHeader.depth > 1) { mptSmp.uFlags.set(CHN_16BIT); mptSmp.nLength /= 2u; }
|
|
if(sampleHeader.channels > 1) { mptSmp.uFlags.set(CHN_STEREO); mptSmp.nLength /= 2u; }
|
|
if(sampleHeader.loopType == 1) mptSmp.uFlags.set(CHN_LOOP);
|
|
else if(sampleHeader.loopType == 2) mptSmp.uFlags.set(CHN_LOOP | CHN_PINGPONGLOOP);
|
|
mptSmp.nLoopStart = sampleHeader.loopStart;
|
|
mptSmp.nLoopEnd = sampleHeader.loopEnd;
|
|
mptSmp.nVolume = sampleHeader.volume >> 7;
|
|
if(sampleHeader.panning == -128)
|
|
mptSmp.uFlags.set(CHN_SURROUND);
|
|
else
|
|
mptSmp.nPan = sampleHeader.panning + 128;
|
|
mptSmp.uFlags.set(CHN_PANNING);
|
|
mptSmp.RelativeTone = sampleHeader.note;
|
|
|
|
if(sampleHeader.flags & 2)
|
|
{
|
|
// Sample is synchronized to beat
|
|
// The synchronization part is not supported in OpenMPT, but synchronized samples also always play at the same pitch as C-5, which we CAN do!
|
|
sampleSynchronized[i] = true;
|
|
//mptSmp.nC5Speed = Util::muldiv(mptSmp.nC5Speed, sampleHeader.spb, 22050);
|
|
}
|
|
if(sampleHeader.flags & 5)
|
|
{
|
|
// External sample
|
|
mptSmp.uFlags.set(SMP_KEEPONDISK);
|
|
}
|
|
if(sampleHeader.flags & 8)
|
|
{
|
|
sampleNoInterpolation[i] = true;
|
|
for(INSTRUMENTINDEX drum = 0; drum < 8; drum++)
|
|
{
|
|
if(drumSample[drum] == i && Instruments[drumMap[drum]] != nullptr)
|
|
{
|
|
Instruments[drumMap[drum]]->resampling = SRCMODE_NEAREST;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Read sample groups
|
|
for(INSTRUMENTINDEX ins = 0; ins < fileHeader.numInstruments; ins++)
|
|
{
|
|
if(instrChunks[ins].GetLength())
|
|
{
|
|
FileReader &chunk = instrChunks[ins];
|
|
MT2Instrument insHeader;
|
|
chunk.Rewind();
|
|
chunk.ReadStruct(insHeader);
|
|
|
|
std::vector<MT2Group> groups;
|
|
file.ReadVector(groups, insHeader.numSamples);
|
|
|
|
ModInstrument *mptIns = Instruments[ins + 1];
|
|
// Instruments with plugin assignments never play samples at the same time!
|
|
if(mptIns == nullptr || mptIns->nMixPlug != 0)
|
|
continue;
|
|
|
|
mptIns->nGlobalVol = 32; // Compensate for extended dynamic range of drum instruments
|
|
mptIns->AssignSample(0);
|
|
for(uint32 note = 0; note < 96; note++)
|
|
{
|
|
if(insHeader.groupMap[note] < insHeader.numSamples)
|
|
{
|
|
const MT2Group &group = groups[insHeader.groupMap[note]];
|
|
SAMPLEINDEX sample = group.sample + 1;
|
|
mptIns->Keyboard[note + 11 + NOTE_MIN] = sample;
|
|
if(sample > 0 && sample <= m_nSamples)
|
|
{
|
|
ModSample &mptSmp = Samples[sample];
|
|
mptSmp.nVibType = static_cast<VibratoType>(insHeader.vibtype & 3); // In fact, MT2 only implements sine vibrato
|
|
mptSmp.nVibSweep = insHeader.vibsweep;
|
|
mptSmp.nVibDepth = insHeader.vibdepth;
|
|
mptSmp.nVibRate = insHeader.vibrate;
|
|
mptSmp.nGlobalVol = uint16(group.vol) * 2;
|
|
mptSmp.nFineTune = group.pitch;
|
|
if(sampleNoInterpolation[sample - 1])
|
|
{
|
|
mptIns->resampling = SRCMODE_NEAREST;
|
|
}
|
|
if(sampleSynchronized[sample - 1])
|
|
{
|
|
mptIns->NoteMap[note + 11 + NOTE_MIN] = NOTE_MIDDLEC;
|
|
}
|
|
}
|
|
// TODO: volume, finetune for duplicated samples
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(!(loadFlags & loadSampleData))
|
|
return true;
|
|
|
|
// Read sample data
|
|
for(SAMPLEINDEX i = 0; i < m_nSamples; i++)
|
|
{
|
|
ModSample &mptSmp = Samples[i + 1];
|
|
mptSmp.Transpose(-(mptSmp.RelativeTone - 49 - (mptSmp.nFineTune / 128.0)) / 12.0);
|
|
mptSmp.nFineTune = 0;
|
|
mptSmp.RelativeTone = 0;
|
|
|
|
if(!mptSmp.uFlags[SMP_KEEPONDISK])
|
|
{
|
|
SampleIO(
|
|
mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
|
|
mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoSplit : SampleIO::mono,
|
|
SampleIO::littleEndian,
|
|
SampleIO::MT2)
|
|
.ReadSample(mptSmp, file);
|
|
} else
|
|
{
|
|
// External sample
|
|
const uint32 filenameSize = file.ReadUint32LE();
|
|
file.Skip(12); // Reserved
|
|
std::string filename;
|
|
file.ReadString<mpt::String::maybeNullTerminated>(filename, filenameSize);
|
|
mptSmp.filename = filename;
|
|
|
|
#if defined(MPT_EXTERNAL_SAMPLES)
|
|
if(filename.length() >= 2
|
|
&& filename[0] != '\\' // Relative path on same drive
|
|
&& filename[1] != ':') // Absolute path
|
|
{
|
|
// Relative path in same folder or sub folder
|
|
filename = ".\\" + filename;
|
|
}
|
|
SetSamplePath(i + 1, mpt::PathString::FromLocaleSilent(filename));
|
|
#elif !defined(LIBOPENMPT_BUILD_TEST)
|
|
AddToLog(LogWarning, MPT_UFORMAT("Loading external sample {} ('{}') failed: External samples are not supported.")(i + 1, mpt::ToUnicode(GetCharsetFile(), filename)));
|
|
#endif // MPT_EXTERNAL_SAMPLES
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
OPENMPT_NAMESPACE_END
|