1149 lines
35 KiB
C++
1149 lines
35 KiB
C++
|
/*
|
||
|
* load_dmf.cpp
|
||
|
* ------------
|
||
|
* Purpose: DMF module loader (X-Tracker by D-LUSiON).
|
||
|
* Notes : If it wasn't already outdated when the tracker left beta state, this would be a rather interesting
|
||
|
* and in some parts even sophisticated format - effect columns are separated by effect type, an easy to
|
||
|
* understand BPM tempo mode, effect durations are always divided into a 256th row, vibrato effects are
|
||
|
* specified by period length and the same 8-Bit granularity is used for both volume and panning.
|
||
|
* Unluckily, this format does not offer any envelopes or multi-sample instruments, and bidi sample loops
|
||
|
* are missing as well, so it was already well behind FT2 back then.
|
||
|
* Authors: Johannes Schultz (mostly based on DMF.TXT, DMF_EFFC.TXT, trial and error and some invaluable hints by Zatzen)
|
||
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
||
|
*/
|
||
|
|
||
|
|
||
|
#include "stdafx.h"
|
||
|
#include "Loaders.h"
|
||
|
#include "BitReader.h"
|
||
|
|
||
|
OPENMPT_NAMESPACE_BEGIN
|
||
|
|
||
|
// DMF header
|
||
|
struct DMFFileHeader
|
||
|
{
|
||
|
char signature[4]; // "DDMF"
|
||
|
uint8 version; // 1 - 7 are beta versions, 8 is the official thing, 10 is xtracker32
|
||
|
char tracker[8]; // "XTRACKER"
|
||
|
char songname[30];
|
||
|
char composer[20];
|
||
|
uint8 creationDay;
|
||
|
uint8 creationMonth;
|
||
|
uint8 creationYear;
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DMFFileHeader, 66)
|
||
|
|
||
|
struct DMFChunk
|
||
|
{
|
||
|
// 32-Bit chunk identifiers
|
||
|
enum ChunkIdentifiers
|
||
|
{
|
||
|
idCMSG = MagicLE("CMSG"), // Song message
|
||
|
idSEQU = MagicLE("SEQU"), // Order list
|
||
|
idPATT = MagicLE("PATT"), // Patterns
|
||
|
idSMPI = MagicLE("SMPI"), // Sample headers
|
||
|
idSMPD = MagicLE("SMPD"), // Sample data
|
||
|
idSMPJ = MagicLE("SMPJ"), // Sample jump table (XTracker 32 only)
|
||
|
idENDE = MagicLE("ENDE"), // Last four bytes of DMF file
|
||
|
idSETT = MagicLE("SETT"), // Probably contains GUI settings
|
||
|
};
|
||
|
|
||
|
uint32le id;
|
||
|
uint32le length;
|
||
|
|
||
|
size_t GetLength() const
|
||
|
{
|
||
|
return length;
|
||
|
}
|
||
|
|
||
|
ChunkIdentifiers GetID() const
|
||
|
{
|
||
|
return static_cast<ChunkIdentifiers>(id.get());
|
||
|
}
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DMFChunk, 8)
|
||
|
|
||
|
// Pattern header (global)
|
||
|
struct DMFPatterns
|
||
|
{
|
||
|
uint16le numPatterns; // 1..1024 patterns
|
||
|
uint8le numTracks; // 1..32 channels
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DMFPatterns, 3)
|
||
|
|
||
|
// Pattern header (for each pattern)
|
||
|
struct DMFPatternHeader
|
||
|
{
|
||
|
uint8le numTracks; // 1..32 channels
|
||
|
uint8le beat; // [hi|lo] -> hi = rows per beat, lo = reserved
|
||
|
uint16le numRows;
|
||
|
uint32le patternLength;
|
||
|
// patttern data follows here ...
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DMFPatternHeader, 8)
|
||
|
|
||
|
// Sample header
|
||
|
struct DMFSampleHeader
|
||
|
{
|
||
|
enum SampleFlags
|
||
|
{
|
||
|
// Sample flags
|
||
|
smpLoop = 0x01,
|
||
|
smp16Bit = 0x02,
|
||
|
smpCompMask = 0x0C,
|
||
|
smpComp1 = 0x04, // Compression type 1
|
||
|
smpComp2 = 0x08, // Compression type 2 (unused)
|
||
|
smpComp3 = 0x0C, // Compression type 3 (ditto)
|
||
|
smpLibrary = 0x80, // Sample is stored in a library
|
||
|
};
|
||
|
|
||
|
uint32le length;
|
||
|
uint32le loopStart;
|
||
|
uint32le loopEnd;
|
||
|
uint16le c3freq; // 1000..45000hz
|
||
|
uint8le volume; // 0 = ignore
|
||
|
uint8le flags;
|
||
|
|
||
|
// Convert an DMFSampleHeader to OpenMPT's internal sample representation.
|
||
|
void ConvertToMPT(ModSample &mptSmp) const
|
||
|
{
|
||
|
mptSmp.Initialize();
|
||
|
mptSmp.nLength = length;
|
||
|
mptSmp.nSustainStart = loopStart;
|
||
|
mptSmp.nSustainEnd = loopEnd;
|
||
|
|
||
|
mptSmp.nC5Speed = c3freq;
|
||
|
mptSmp.nGlobalVol = 64;
|
||
|
if(volume)
|
||
|
mptSmp.nVolume = volume + 1;
|
||
|
else
|
||
|
mptSmp.nVolume = 256;
|
||
|
mptSmp.uFlags.set(SMP_NODEFAULTVOLUME, volume == 0);
|
||
|
|
||
|
if((flags & smpLoop) != 0 && mptSmp.nSustainEnd > mptSmp.nSustainStart)
|
||
|
{
|
||
|
mptSmp.uFlags.set(CHN_SUSTAINLOOP);
|
||
|
}
|
||
|
if((flags & smp16Bit) != 0)
|
||
|
{
|
||
|
mptSmp.uFlags.set(CHN_16BIT);
|
||
|
mptSmp.nLength /= 2;
|
||
|
mptSmp.nSustainStart /= 2;
|
||
|
mptSmp.nSustainEnd /= 2;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
MPT_BINARY_STRUCT(DMFSampleHeader, 16)
|
||
|
|
||
|
|
||
|
// Pattern translation memory
|
||
|
struct DMFPatternSettings
|
||
|
{
|
||
|
struct ChannelState
|
||
|
{
|
||
|
ModCommand::NOTE noteBuffer = NOTE_NONE; // Note buffer
|
||
|
ModCommand::NOTE lastNote = NOTE_NONE; // Last played note on channel
|
||
|
uint8 vibratoType = 8; // Last used vibrato type on channel
|
||
|
uint8 tremoloType = 4; // Last used tremolo type on channel
|
||
|
uint8 highOffset = 6; // Last used high offset on channel
|
||
|
bool playDir = false; // Sample play direction... false = forward (default)
|
||
|
};
|
||
|
|
||
|
std::vector<ChannelState> channels; // Memory for each channel's state
|
||
|
bool realBPMmode = false; // true = BPM mode
|
||
|
uint8 beat = 0; // Rows per beat
|
||
|
uint8 tempoTicks = 32; // Tick mode param
|
||
|
uint8 tempoBPM = 120; // BPM mode param
|
||
|
uint8 internalTicks = 6; // Ticks per row in final pattern
|
||
|
|
||
|
DMFPatternSettings(CHANNELINDEX numChannels)
|
||
|
: channels(numChannels)
|
||
|
{ }
|
||
|
};
|
||
|
|
||
|
|
||
|
// Convert portamento value (not very accurate due to X-Tracker's higher granularity, to say the least)
|
||
|
static uint8 DMFporta2MPT(uint8 val, const uint8 internalTicks, const bool hasFine)
|
||
|
{
|
||
|
if(val == 0)
|
||
|
return 0;
|
||
|
else if((val <= 0x0F && hasFine) || internalTicks < 2)
|
||
|
return (val | 0xF0);
|
||
|
else
|
||
|
return std::max(uint8(1), static_cast<uint8>((val / (internalTicks - 1)))); // no porta on first tick!
|
||
|
}
|
||
|
|
||
|
|
||
|
// Convert portamento / volume slide value (not very accurate due to X-Tracker's higher granularity, to say the least)
|
||
|
static uint8 DMFslide2MPT(uint8 val, const uint8 internalTicks, const bool up)
|
||
|
{
|
||
|
val = std::max(uint8(1), static_cast<uint8>(val / 4));
|
||
|
const bool isFine = (val < 0x0F) || (internalTicks < 2);
|
||
|
if(!isFine)
|
||
|
val = std::max(uint8(1), static_cast<uint8>((val + internalTicks - 2) / (internalTicks - 1))); // no slides on first tick! "+ internalTicks - 2" for rounding precision
|
||
|
|
||
|
if(up)
|
||
|
return (isFine ? 0x0F : 0x00) | (val << 4);
|
||
|
else
|
||
|
return (isFine ? 0xF0 : 0x00) | (val & 0x0F);
|
||
|
|
||
|
}
|
||
|
|
||
|
|
||
|
// Calculate tremor on/off param
|
||
|
static uint8 DMFtremor2MPT(uint8 val, const uint8 internalTicks)
|
||
|
{
|
||
|
uint8 ontime = (val >> 4);
|
||
|
uint8 offtime = (val & 0x0F);
|
||
|
ontime = static_cast<uint8>(Clamp(ontime * internalTicks / 15, 1, 15));
|
||
|
offtime = static_cast<uint8>(Clamp(offtime * internalTicks / 15, 1, 15));
|
||
|
return (ontime << 4) | offtime;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Calculate delay parameter for note cuts / delays
|
||
|
static uint8 DMFdelay2MPT(uint8 val, const uint8 internalTicks)
|
||
|
{
|
||
|
int newval = (int)val * (int)internalTicks / 255;
|
||
|
Limit(newval, 0, 15);
|
||
|
return (uint8)newval;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Convert vibrato-style command parameters
|
||
|
static uint8 DMFvibrato2MPT(uint8 val, const uint8 internalTicks)
|
||
|
{
|
||
|
// MPT: 1 vibrato period == 64 ticks... we have internalTicks ticks per row.
|
||
|
// X-Tracker: Period length specified in rows!
|
||
|
const int periodInTicks = std::max(1, (val >> 4)) * internalTicks;
|
||
|
const uint8 matchingPeriod = static_cast<uint8>(Clamp((128 / periodInTicks), 1, 15));
|
||
|
return (matchingPeriod << 4) | std::max(uint8(1), static_cast<uint8>(val & 0x0F));
|
||
|
}
|
||
|
|
||
|
|
||
|
// Try using effect memory (zero paramer) to give the effect swapper some optimization hints.
|
||
|
static void ApplyEffectMemory(const ModCommand *m, ROWINDEX row, CHANNELINDEX numChannels, uint8 effect, uint8 ¶m)
|
||
|
{
|
||
|
if(effect == CMD_NONE || param == 0)
|
||
|
return;
|
||
|
|
||
|
const bool isTonePortaEffect = (effect == CMD_PORTAMENTOUP || effect == CMD_PORTAMENTODOWN || effect == CMD_TONEPORTAMENTO);
|
||
|
const bool isVolSlideEffect = (effect == CMD_VOLUMESLIDE || effect == CMD_TONEPORTAVOL || effect == CMD_VIBRATOVOL);
|
||
|
|
||
|
while(row > 0)
|
||
|
{
|
||
|
m -= numChannels;
|
||
|
row--;
|
||
|
|
||
|
// First, keep some extra rules in mind for portamento, where effect memory is shared between various commands.
|
||
|
bool isSame = (effect == m->command);
|
||
|
if(isTonePortaEffect && (m->command == CMD_PORTAMENTOUP || m->command == CMD_PORTAMENTODOWN || m->command == CMD_TONEPORTAMENTO))
|
||
|
{
|
||
|
if(m->param < 0xE0)
|
||
|
{
|
||
|
// Avoid effect param for fine slides, or else we could accidentally put this command in the volume column, where fine slides won't work!
|
||
|
isSame = true;
|
||
|
} else
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
} else if(isVolSlideEffect && (m->command == CMD_VOLUMESLIDE || m->command == CMD_TONEPORTAVOL || m->command == CMD_VIBRATOVOL))
|
||
|
{
|
||
|
isSame = true;
|
||
|
}
|
||
|
if(isTonePortaEffect
|
||
|
&& (m->volcmd == VOLCMD_PORTAUP || m->volcmd == VOLCMD_PORTADOWN || m->volcmd == VOLCMD_TONEPORTAMENTO)
|
||
|
&& m->vol != 0)
|
||
|
{
|
||
|
// Uuh... Don't even try
|
||
|
return;
|
||
|
} else if(isVolSlideEffect
|
||
|
&& (m->volcmd == VOLCMD_FINEVOLUP || m->volcmd == VOLCMD_FINEVOLDOWN || m->volcmd == VOLCMD_VOLSLIDEUP || m->volcmd == VOLCMD_VOLSLIDEDOWN)
|
||
|
&& m->vol != 0)
|
||
|
{
|
||
|
// Same!
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if(isSame)
|
||
|
{
|
||
|
if(param != m->param && m->param != 0)
|
||
|
{
|
||
|
// No way to optimize this
|
||
|
return;
|
||
|
} else if(param == m->param)
|
||
|
{
|
||
|
// Yay!
|
||
|
param = 0;
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
static PATTERNINDEX ConvertDMFPattern(FileReader &file, const uint8 fileVersion, DMFPatternSettings &settings, CSoundFile &sndFile)
|
||
|
{
|
||
|
// Pattern flags
|
||
|
enum PatternFlags
|
||
|
{
|
||
|
// Global Track
|
||
|
patGlobPack = 0x80, // Pack information for global track follows
|
||
|
patGlobMask = 0x3F, // Mask for global effects
|
||
|
// Note tracks
|
||
|
patCounter = 0x80, // Pack information for current channel follows
|
||
|
patInstr = 0x40, // Instrument number present
|
||
|
patNote = 0x20, // Note present
|
||
|
patVolume = 0x10, // Volume present
|
||
|
patInsEff = 0x08, // Instrument effect present
|
||
|
patNoteEff = 0x04, // Note effect present
|
||
|
patVolEff = 0x02, // Volume effect stored
|
||
|
};
|
||
|
|
||
|
file.Rewind();
|
||
|
|
||
|
DMFPatternHeader patHead;
|
||
|
if(fileVersion < 3)
|
||
|
{
|
||
|
patHead.numTracks = file.ReadUint8();
|
||
|
file.Skip(2); // not sure what this is, later X-Tracker versions just skip over it
|
||
|
patHead.numRows = file.ReadUint16LE();
|
||
|
patHead.patternLength = file.ReadUint32LE();
|
||
|
} else
|
||
|
{
|
||
|
file.ReadStruct(patHead);
|
||
|
}
|
||
|
if(fileVersion < 6)
|
||
|
patHead.beat = 0;
|
||
|
|
||
|
const ROWINDEX numRows = Clamp(ROWINDEX(patHead.numRows), ROWINDEX(1), MAX_PATTERN_ROWS);
|
||
|
const PATTERNINDEX pat = sndFile.Patterns.InsertAny(numRows);
|
||
|
if(pat == PATTERNINDEX_INVALID)
|
||
|
{
|
||
|
return pat;
|
||
|
}
|
||
|
|
||
|
PatternRow m = sndFile.Patterns[pat].GetRow(0);
|
||
|
const CHANNELINDEX numChannels = std::min(static_cast<CHANNELINDEX>(sndFile.GetNumChannels() - 1), static_cast<CHANNELINDEX>(patHead.numTracks));
|
||
|
|
||
|
// When breaking to a pattern with less channels that the previous pattern,
|
||
|
// all voices in the now unused channels are killed:
|
||
|
for(CHANNELINDEX chn = numChannels + 1; chn < sndFile.GetNumChannels(); chn++)
|
||
|
{
|
||
|
m[chn].note = NOTE_NOTECUT;
|
||
|
}
|
||
|
|
||
|
// Initialize tempo stuff
|
||
|
settings.beat = (patHead.beat >> 4);
|
||
|
bool tempoChange = settings.realBPMmode;
|
||
|
uint8 writeDelay = 0;
|
||
|
|
||
|
// Counters for channel packing (including global track)
|
||
|
std::vector<uint8> channelCounter(numChannels + 1, 0);
|
||
|
|
||
|
for(ROWINDEX row = 0; row < numRows; row++)
|
||
|
{
|
||
|
// Global track info counter reached 0 => read global track data
|
||
|
if(channelCounter[0] == 0)
|
||
|
{
|
||
|
uint8 globalInfo = file.ReadUint8();
|
||
|
// 0x80: Packing counter (if not present, counter stays at 0)
|
||
|
if((globalInfo & patGlobPack) != 0)
|
||
|
{
|
||
|
channelCounter[0] = file.ReadUint8();
|
||
|
}
|
||
|
|
||
|
globalInfo &= patGlobMask;
|
||
|
|
||
|
uint8 globalData = 0;
|
||
|
if(globalInfo != 0)
|
||
|
{
|
||
|
globalData = file.ReadUint8();
|
||
|
}
|
||
|
|
||
|
switch(globalInfo)
|
||
|
{
|
||
|
case 1: // Set Tick Frame Speed
|
||
|
settings.realBPMmode = false;
|
||
|
settings.tempoTicks = std::max(uint8(1), globalData); // Tempo in 1/4 rows per second
|
||
|
settings.tempoBPM = 0; // Automatically updated by X-Tracker
|
||
|
tempoChange = true;
|
||
|
break;
|
||
|
case 2: // Set BPM Speed (real BPM mode)
|
||
|
if(globalData) // DATA = 0 doesn't do anything
|
||
|
{
|
||
|
settings.realBPMmode = true;
|
||
|
settings.tempoBPM = globalData; // Tempo in real BPM (depends on rows per beat)
|
||
|
if(settings.beat != 0)
|
||
|
{
|
||
|
settings.tempoTicks = (globalData * settings.beat * 15); // Automatically updated by X-Tracker
|
||
|
}
|
||
|
tempoChange = true;
|
||
|
}
|
||
|
break;
|
||
|
case 3: // Set Beat
|
||
|
settings.beat = (globalData >> 4);
|
||
|
if(settings.beat != 0)
|
||
|
{
|
||
|
// Tempo changes only if we're in real BPM mode
|
||
|
tempoChange = settings.realBPMmode;
|
||
|
} else
|
||
|
{
|
||
|
// If beat is 0, change to tick speed mode, but keep current tempo
|
||
|
settings.realBPMmode = false;
|
||
|
}
|
||
|
break;
|
||
|
case 4: // Tick Delay
|
||
|
writeDelay = globalData;
|
||
|
break;
|
||
|
case 5: // Set External Flag
|
||
|
break;
|
||
|
case 6: // Slide Speed Up
|
||
|
if(globalData > 0)
|
||
|
{
|
||
|
uint8 &tempoData = (settings.realBPMmode) ? settings.tempoBPM : settings.tempoTicks;
|
||
|
if(tempoData < 256 - globalData)
|
||
|
{
|
||
|
tempoData += globalData;
|
||
|
} else
|
||
|
{
|
||
|
tempoData = 255;
|
||
|
}
|
||
|
tempoChange = true;
|
||
|
}
|
||
|
break;
|
||
|
case 7: // Slide Speed Down
|
||
|
if(globalData > 0)
|
||
|
{
|
||
|
uint8 &tempoData = (settings.realBPMmode) ? settings.tempoBPM : settings.tempoTicks;
|
||
|
if(tempoData > 1 + globalData)
|
||
|
{
|
||
|
tempoData -= globalData;
|
||
|
} else
|
||
|
{
|
||
|
tempoData = 1;
|
||
|
}
|
||
|
tempoChange = true;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
channelCounter[0]--;
|
||
|
}
|
||
|
|
||
|
// These will eventually be written to the pattern
|
||
|
int speed = 0, tempo = 0;
|
||
|
|
||
|
if(tempoChange)
|
||
|
{
|
||
|
// Can't do anything if we're in BPM mode and there's no rows per beat set...
|
||
|
if(!settings.realBPMmode || settings.beat)
|
||
|
{
|
||
|
// My approach to convert X-Tracker's "tick speed" (1/4 rows per second):
|
||
|
// Tempo * 6 / Speed = Beats per Minute
|
||
|
// => Tempo * 6 / (Speed * 60) = Beats per Second
|
||
|
// => Tempo * 24 / (Speed * 60) = Rows per Second (4 rows per beat at tempo 6)
|
||
|
// => Tempo = 60 * Rows per Second * Speed / 24
|
||
|
// For some reason, using settings.tempoTicks + 1 gives more accurate results than just settings.tempoTicks... (same problem in the old libmodplug DMF loader)
|
||
|
// Original unoptimized formula:
|
||
|
//const int tickspeed = (tempoRealBPMmode) ? std::max(1, (tempoData * beat * 4) / 60) : tempoData;
|
||
|
const int tickspeed = (settings.realBPMmode) ? std::max(1, settings.tempoBPM * settings.beat * 2) : ((settings.tempoTicks + 1) * 30);
|
||
|
// Try to find matching speed - try higher speeds first, so that effects like arpeggio and tremor work better.
|
||
|
for(speed = 255; speed >= 1; speed--)
|
||
|
{
|
||
|
// Original unoptimized formula:
|
||
|
// tempo = 30 * tickspeed * speed / 48;
|
||
|
tempo = tickspeed * speed / 48;
|
||
|
if(tempo >= 32 && tempo <= 255)
|
||
|
break;
|
||
|
}
|
||
|
Limit(tempo, 32, 255);
|
||
|
settings.internalTicks = static_cast<uint8>(std::max(1, speed));
|
||
|
} else
|
||
|
{
|
||
|
tempoChange = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m = sndFile.Patterns[pat].GetpModCommand(row, 1); // Reserve first channel for global effects
|
||
|
|
||
|
for(CHANNELINDEX chn = 1; chn <= numChannels; chn++, m++)
|
||
|
{
|
||
|
// Track info counter reached 0 => read track data
|
||
|
if(channelCounter[chn] == 0)
|
||
|
{
|
||
|
const uint8 channelInfo = file.ReadUint8();
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x80: Packing counter (if not present, counter stays at 0)
|
||
|
if((channelInfo & patCounter) != 0)
|
||
|
{
|
||
|
channelCounter[chn] = file.ReadUint8();
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x40: Instrument
|
||
|
bool slideNote = true; // If there is no instrument number next to a note, the note is not retriggered!
|
||
|
if((channelInfo & patInstr) != 0)
|
||
|
{
|
||
|
m->instr = file.ReadUint8();
|
||
|
if(m->instr != 0)
|
||
|
{
|
||
|
slideNote = false;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x20: Note
|
||
|
if((channelInfo & patNote) != 0)
|
||
|
{
|
||
|
m->note = file.ReadUint8();
|
||
|
if(m->note >= 1 && m->note <= 108)
|
||
|
{
|
||
|
m->note = static_cast<uint8>(Clamp(m->note + 24, NOTE_MIN, NOTE_MAX));
|
||
|
settings.channels[chn].lastNote = m->note;
|
||
|
} else if(m->note >= 129 && m->note <= 236)
|
||
|
{
|
||
|
// "Buffer notes" for portamento (and other effects?) that are actually not played, but just "queued"...
|
||
|
m->note = static_cast<uint8>(Clamp((m->note & 0x7F) + 24, NOTE_MIN, NOTE_MAX));
|
||
|
settings.channels[chn].noteBuffer = m->note;
|
||
|
m->note = NOTE_NONE;
|
||
|
} else if(m->note == 255)
|
||
|
{
|
||
|
m->note = NOTE_NOTECUT;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If there's just an instrument number, but no note, retrigger sample.
|
||
|
if(m->note == NOTE_NONE && m->instr > 0)
|
||
|
{
|
||
|
m->note = settings.channels[chn].lastNote;
|
||
|
m->instr = 0;
|
||
|
}
|
||
|
|
||
|
if(m->IsNote())
|
||
|
{
|
||
|
settings.channels[chn].playDir = false;
|
||
|
}
|
||
|
|
||
|
uint8 effect1 = CMD_NONE, effect2 = CMD_NONE, effect3 = CMD_NONE;
|
||
|
uint8 effectParam1 = 0, effectParam2 = 0, effectParam3 = 0;
|
||
|
bool useMem2 = false, useMem3 = false; // Effect can use memory if necessary
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x10: Volume
|
||
|
if((channelInfo & patVolume) != 0)
|
||
|
{
|
||
|
m->volcmd = VOLCMD_VOLUME;
|
||
|
m->vol = (file.ReadUint8() + 2) / 4; // Should be + 3 instead of + 2, but volume 1 is silent in X-Tracker.
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x08: Instrument effect
|
||
|
if((channelInfo & patInsEff) != 0)
|
||
|
{
|
||
|
effect1 = file.ReadUint8();
|
||
|
effectParam1 = file.ReadUint8();
|
||
|
|
||
|
switch(effect1)
|
||
|
{
|
||
|
case 1: // Stop Sample
|
||
|
m->note = NOTE_NOTECUT;
|
||
|
effect1 = CMD_NONE;
|
||
|
break;
|
||
|
case 2: // Stop Sample Loop
|
||
|
m->note = NOTE_KEYOFF;
|
||
|
effect1 = CMD_NONE;
|
||
|
break;
|
||
|
case 3: // Instrument Volume Override (aka "Restart")
|
||
|
m->note = settings.channels[chn].lastNote;
|
||
|
settings.channels[chn].playDir = false;
|
||
|
effect1 = CMD_NONE;
|
||
|
break;
|
||
|
case 4: // Sample Delay
|
||
|
effectParam1 = DMFdelay2MPT(effectParam1, settings.internalTicks);
|
||
|
if(effectParam1)
|
||
|
{
|
||
|
effect1 = CMD_S3MCMDEX;
|
||
|
effectParam1 = 0xD0 | (effectParam1);
|
||
|
} else
|
||
|
{
|
||
|
effect1 = CMD_NONE;
|
||
|
}
|
||
|
if(m->note == NOTE_NONE)
|
||
|
{
|
||
|
m->note = settings.channels[chn].lastNote;
|
||
|
settings.channels[chn].playDir = false;
|
||
|
}
|
||
|
break;
|
||
|
case 5: // Tremolo Retrig Sample (who invented those stupid effect names?)
|
||
|
effectParam1 = std::max(uint8(1), DMFdelay2MPT(effectParam1, settings.internalTicks));
|
||
|
effect1 = CMD_RETRIG;
|
||
|
settings.channels[chn].playDir = false;
|
||
|
break;
|
||
|
case 6: // Offset
|
||
|
case 7: // Offset + 64k
|
||
|
case 8: // Offset + 128k
|
||
|
case 9: // Offset + 192k
|
||
|
// Put high offset on previous row
|
||
|
if(row > 0 && effect1 != settings.channels[chn].highOffset)
|
||
|
{
|
||
|
if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0xA0 | (effect1 - 6))).Row(row - 1).Channel(chn).RetryPreviousRow()))
|
||
|
{
|
||
|
settings.channels[chn].highOffset = effect1;
|
||
|
}
|
||
|
}
|
||
|
effect1 = CMD_OFFSET;
|
||
|
if(m->note == NOTE_NONE)
|
||
|
{
|
||
|
// Offset without note does also work in DMF.
|
||
|
m->note = settings.channels[chn].lastNote;
|
||
|
}
|
||
|
settings.channels[chn].playDir = false;
|
||
|
break;
|
||
|
case 10: // Invert Sample play direction ("Tekkno Invert")
|
||
|
effect1 = CMD_S3MCMDEX;
|
||
|
if(settings.channels[chn].playDir == false)
|
||
|
effectParam1 = 0x9F;
|
||
|
else
|
||
|
effectParam1 = 0x9E;
|
||
|
settings.channels[chn].playDir = !settings.channels[chn].playDir;
|
||
|
break;
|
||
|
default:
|
||
|
effect1 = CMD_NONE;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x04: Note effect
|
||
|
if((channelInfo & patNoteEff) != 0)
|
||
|
{
|
||
|
effect2 = file.ReadUint8();
|
||
|
effectParam2 = file.ReadUint8();
|
||
|
|
||
|
switch(effect2)
|
||
|
{
|
||
|
case 1: // Note Finetune (1/16th of a semitone signed 8-bit value, not 1/128th as the interface claims)
|
||
|
{
|
||
|
const auto fine = std::div(static_cast<int8>(effectParam2) * 8, 128);
|
||
|
if(m->IsNote())
|
||
|
m->note = static_cast<ModCommand::NOTE>(Clamp(m->note + fine.quot, NOTE_MIN, NOTE_MAX));
|
||
|
effect2 = CMD_FINETUNE;
|
||
|
effectParam2 = static_cast<uint8>(fine.rem) ^ 0x80;
|
||
|
}
|
||
|
break;
|
||
|
case 2: // Note Delay (wtf is the difference to Sample Delay?)
|
||
|
effectParam2 = DMFdelay2MPT(effectParam2, settings.internalTicks);
|
||
|
if(effectParam2)
|
||
|
{
|
||
|
effect2 = CMD_S3MCMDEX;
|
||
|
effectParam2 = 0xD0 | (effectParam2);
|
||
|
} else
|
||
|
{
|
||
|
effect2 = CMD_NONE;
|
||
|
}
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 3: // Arpeggio
|
||
|
effect2 = CMD_ARPEGGIO;
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 4: // Portamento Up
|
||
|
case 5: // Portamento Down
|
||
|
effectParam2 = DMFporta2MPT(effectParam2, settings.internalTicks, true);
|
||
|
effect2 = (effect2 == 4) ? CMD_PORTAMENTOUP : CMD_PORTAMENTODOWN;
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 6: // Portamento to Note
|
||
|
if(m->note == NOTE_NONE)
|
||
|
{
|
||
|
m->note = settings.channels[chn].noteBuffer;
|
||
|
}
|
||
|
effectParam2 = DMFporta2MPT(effectParam2, settings.internalTicks, false);
|
||
|
effect2 = CMD_TONEPORTAMENTO;
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 7: // Scratch to Note (neat! but we don't have such an effect...)
|
||
|
m->note = static_cast<ModCommand::NOTE>(Clamp(effectParam2 + 25, NOTE_MIN, NOTE_MAX));
|
||
|
effect2 = CMD_TONEPORTAMENTO;
|
||
|
effectParam2 = 0xFF;
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 8: // Vibrato Sine
|
||
|
case 9: // Vibrato Triangle (ramp down should be close enough)
|
||
|
case 10: // Vibrato Square
|
||
|
// Put vibrato type on previous row
|
||
|
if(row > 0 && effect2 != settings.channels[chn].vibratoType)
|
||
|
{
|
||
|
if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0x30 | (effect2 - 8))).Row(row - 1).Channel(chn).RetryPreviousRow()))
|
||
|
{
|
||
|
settings.channels[chn].vibratoType = effect2;
|
||
|
}
|
||
|
}
|
||
|
effect2 = CMD_VIBRATO;
|
||
|
effectParam2 = DMFvibrato2MPT(effectParam2, settings.internalTicks);
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 11: // Note Tremolo
|
||
|
effectParam2 = DMFtremor2MPT(effectParam2, settings.internalTicks);
|
||
|
effect2 = CMD_TREMOR;
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
case 12: // Note Cut
|
||
|
effectParam2 = DMFdelay2MPT(effectParam2, settings.internalTicks);
|
||
|
if(effectParam2)
|
||
|
{
|
||
|
effect2 = CMD_S3MCMDEX;
|
||
|
effectParam2 = 0xC0 | (effectParam2);
|
||
|
} else
|
||
|
{
|
||
|
effect2 = CMD_NONE;
|
||
|
m->note = NOTE_NOTECUT;
|
||
|
}
|
||
|
useMem2 = true;
|
||
|
break;
|
||
|
default:
|
||
|
effect2 = CMD_NONE;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
////////////////////////////////////////////////////////////////
|
||
|
// 0x02: Volume effect
|
||
|
if((channelInfo & patVolEff) != 0)
|
||
|
{
|
||
|
effect3 = file.ReadUint8();
|
||
|
effectParam3 = file.ReadUint8();
|
||
|
|
||
|
switch(effect3)
|
||
|
{
|
||
|
case 1: // Volume Slide Up
|
||
|
case 2: // Volume Slide Down
|
||
|
effectParam3 = DMFslide2MPT(effectParam3, settings.internalTicks, (effect3 == 1));
|
||
|
effect3 = CMD_VOLUMESLIDE;
|
||
|
useMem3 = true;
|
||
|
break;
|
||
|
case 3: // Volume Tremolo (actually this is Tremor)
|
||
|
effectParam3 = DMFtremor2MPT(effectParam3, settings.internalTicks);
|
||
|
effect3 = CMD_TREMOR;
|
||
|
useMem3 = true;
|
||
|
break;
|
||
|
case 4: // Tremolo Sine
|
||
|
case 5: // Tremolo Triangle (ramp down should be close enough)
|
||
|
case 6: // Tremolo Square
|
||
|
// Put tremolo type on previous row
|
||
|
if(row > 0 && effect3 != settings.channels[chn].tremoloType)
|
||
|
{
|
||
|
if(sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, (0x40 | (effect3 - 4))).Row(row - 1).Channel(chn).RetryPreviousRow()))
|
||
|
{
|
||
|
settings.channels[chn].tremoloType = effect3;
|
||
|
}
|
||
|
}
|
||
|
effect3 = CMD_TREMOLO;
|
||
|
effectParam3 = DMFvibrato2MPT(effectParam3, settings.internalTicks);
|
||
|
useMem3 = true;
|
||
|
break;
|
||
|
case 7: // Set Balance
|
||
|
effect3 = CMD_PANNING8;
|
||
|
break;
|
||
|
case 8: // Slide Balance Left
|
||
|
case 9: // Slide Balance Right
|
||
|
effectParam3 = DMFslide2MPT(effectParam3, settings.internalTicks, (effect3 == 8));
|
||
|
effect3 = CMD_PANNINGSLIDE;
|
||
|
useMem3 = true;
|
||
|
break;
|
||
|
case 10: // Balance Vibrato Left/Right (always sine modulated)
|
||
|
effect3 = CMD_PANBRELLO;
|
||
|
effectParam3 = DMFvibrato2MPT(effectParam3, settings.internalTicks);
|
||
|
useMem3 = true;
|
||
|
break;
|
||
|
default:
|
||
|
effect3 = CMD_NONE;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Let's see if we can help the effect swapper by reducing some effect parameters to "continue" parameters.
|
||
|
if(useMem2)
|
||
|
ApplyEffectMemory(m, row, sndFile.GetNumChannels(), effect2, effectParam2);
|
||
|
if(useMem3)
|
||
|
ApplyEffectMemory(m, row, sndFile.GetNumChannels(), effect3, effectParam3);
|
||
|
|
||
|
// I guess this is close enough to "not retriggering the note"
|
||
|
if(slideNote && m->IsNote())
|
||
|
{
|
||
|
if(effect2 == CMD_NONE)
|
||
|
{
|
||
|
effect2 = CMD_TONEPORTAMENTO;
|
||
|
effectParam2 = 0xFF;
|
||
|
} else if(effect3 == CMD_NONE && effect2 != CMD_TONEPORTAMENTO) // Tone portamentos normally go in effect #2
|
||
|
{
|
||
|
effect3 = CMD_TONEPORTAMENTO;
|
||
|
effectParam3 = 0xFF;
|
||
|
}
|
||
|
}
|
||
|
// If one of the effects is unused, temporarily put volume commands in there
|
||
|
if(m->volcmd == VOLCMD_VOLUME)
|
||
|
{
|
||
|
if(effect2 == CMD_NONE)
|
||
|
{
|
||
|
effect2 = CMD_VOLUME;
|
||
|
effectParam2 = m->vol;
|
||
|
m->volcmd = VOLCMD_NONE;
|
||
|
} else if(effect3 == CMD_NONE)
|
||
|
{
|
||
|
effect3 = CMD_VOLUME;
|
||
|
effectParam3 = m->vol;
|
||
|
m->volcmd = VOLCMD_NONE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ModCommand::TwoRegularCommandsToMPT(effect2, effectParam2, effect3, effectParam3);
|
||
|
|
||
|
if(m->volcmd == VOLCMD_NONE && effect2 != VOLCMD_NONE)
|
||
|
{
|
||
|
m->volcmd = effect2;
|
||
|
m->vol = effectParam2;
|
||
|
}
|
||
|
// Prefer instrument effects over any other effects
|
||
|
if(effect1 != CMD_NONE)
|
||
|
{
|
||
|
ModCommand::TwoRegularCommandsToMPT(effect3, effectParam3, effect1, effectParam1);
|
||
|
if(m->volcmd == VOLCMD_NONE && effect3 != VOLCMD_NONE)
|
||
|
{
|
||
|
m->volcmd = effect3;
|
||
|
m->vol = effectParam3;
|
||
|
}
|
||
|
m->command = effect1;
|
||
|
m->param = effectParam1;
|
||
|
} else if(effect3 != CMD_NONE)
|
||
|
{
|
||
|
m->command = effect3;
|
||
|
m->param = effectParam3;
|
||
|
}
|
||
|
|
||
|
} else
|
||
|
{
|
||
|
channelCounter[chn]--;
|
||
|
}
|
||
|
} // End for all channels
|
||
|
|
||
|
// Now we can try to write tempo information.
|
||
|
if(tempoChange)
|
||
|
{
|
||
|
tempoChange = false;
|
||
|
|
||
|
sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_TEMPO, static_cast<ModCommand::PARAM>(tempo)).Row(row).Channel(0).RetryNextRow());
|
||
|
sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_SPEED, static_cast<ModCommand::PARAM>(speed)).Row(row).RetryNextRow());
|
||
|
}
|
||
|
// Try to put delay effects somewhere as well
|
||
|
if(writeDelay & 0xF0)
|
||
|
{
|
||
|
sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, 0xE0 | (writeDelay >> 4)).Row(row).AllowMultiple());
|
||
|
}
|
||
|
if(writeDelay & 0x0F)
|
||
|
{
|
||
|
const uint8 param = (writeDelay & 0x0F) * settings.internalTicks / 15;
|
||
|
sndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_S3MCMDEX, 0x60u | Clamp(param, uint8(1), uint8(15))).Row(row).AllowMultiple());
|
||
|
}
|
||
|
writeDelay = 0;
|
||
|
} // End for all rows
|
||
|
|
||
|
return pat;
|
||
|
}
|
||
|
|
||
|
|
||
|
static bool ValidateHeader(const DMFFileHeader &fileHeader)
|
||
|
{
|
||
|
if(std::memcmp(fileHeader.signature, "DDMF", 4)
|
||
|
|| !fileHeader.version || fileHeader.version > 10)
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDMF(MemoryFileReader file, const uint64 *pfilesize)
|
||
|
{
|
||
|
DMFFileHeader fileHeader;
|
||
|
if(!file.ReadStruct(fileHeader))
|
||
|
{
|
||
|
return ProbeWantMoreData;
|
||
|
}
|
||
|
if(!ValidateHeader(fileHeader))
|
||
|
{
|
||
|
return ProbeFailure;
|
||
|
}
|
||
|
MPT_UNREFERENCED_PARAMETER(pfilesize);
|
||
|
return ProbeSuccess;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CSoundFile::ReadDMF(FileReader &file, ModLoadingFlags loadFlags)
|
||
|
{
|
||
|
file.Rewind();
|
||
|
|
||
|
DMFFileHeader fileHeader;
|
||
|
if(!file.ReadStruct(fileHeader))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
if(!ValidateHeader(fileHeader))
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
if(loadFlags == onlyVerifyHeader)
|
||
|
{
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
InitializeGlobals(MOD_TYPE_DMF);
|
||
|
|
||
|
m_modFormat.formatName = MPT_UFORMAT("X-Tracker v{}")(fileHeader.version);
|
||
|
m_modFormat.type = U_("dmf");
|
||
|
m_modFormat.charset = mpt::Charset::CP437;
|
||
|
|
||
|
m_songName = mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.songname);
|
||
|
m_songArtist = mpt::ToUnicode(mpt::Charset::CP437, mpt::String::ReadBuf(mpt::String::spacePadded, fileHeader.composer));
|
||
|
|
||
|
FileHistory mptHistory;
|
||
|
mptHistory.loadDate.tm_mday = Clamp(fileHeader.creationDay, uint8(1), uint8(31));
|
||
|
mptHistory.loadDate.tm_mon = Clamp(fileHeader.creationMonth, uint8(1), uint8(12)) - 1;
|
||
|
mptHistory.loadDate.tm_year = fileHeader.creationYear;
|
||
|
m_FileHistory.clear();
|
||
|
m_FileHistory.push_back(mptHistory);
|
||
|
|
||
|
// Go through all chunks now... cannot use our standard IFF chunk reader here because early X-Tracker versions write some malformed chunk headers... fun code ahead!
|
||
|
ChunkReader::ChunkList<DMFChunk> chunks;
|
||
|
while(file.CanRead(sizeof(DMFChunk)))
|
||
|
{
|
||
|
DMFChunk chunkHeader;
|
||
|
file.Read(chunkHeader);
|
||
|
uint32 chunkLength = chunkHeader.length, chunkSkip = 0;
|
||
|
// When loop start was added to version 3, the chunk size was not updated...
|
||
|
if(fileHeader.version == 3 && chunkHeader.GetID() == DMFChunk::idSEQU)
|
||
|
chunkSkip = 2;
|
||
|
// ...and when the loop end was added to version 4, it was also note updated! Luckily they fixed it in version 5.
|
||
|
else if(fileHeader.version == 4 && chunkHeader.GetID() == DMFChunk::idSEQU)
|
||
|
chunkSkip = 4;
|
||
|
// Earlier X-Tracker versions also write a garbage length for the SMPD chunk if samples are compressed.
|
||
|
// I don't know when exactly this stopped, but I have no version 5-7 files to check (and no X-Tracker version that writes those versions).
|
||
|
// Since this is practically always the last chunk in the file, the following code is safe for those versions, though.
|
||
|
else if(fileHeader.version < 8 && chunkHeader.GetID() == DMFChunk::idSMPD)
|
||
|
chunkLength = uint32_max;
|
||
|
chunks.chunks.push_back(ChunkReader::Item<DMFChunk>{chunkHeader, file.ReadChunk(chunkLength)});
|
||
|
file.Skip(chunkSkip);
|
||
|
}
|
||
|
FileReader chunk;
|
||
|
|
||
|
// Read order list
|
||
|
chunk = chunks.GetChunk(DMFChunk::idSEQU);
|
||
|
ORDERINDEX seqLoopStart = 0, seqLoopEnd = ORDERINDEX_MAX;
|
||
|
if(fileHeader.version >= 3)
|
||
|
seqLoopStart = chunk.ReadUint16LE();
|
||
|
if(fileHeader.version >= 4)
|
||
|
seqLoopEnd = chunk.ReadUint16LE();
|
||
|
// HIPOMATK.DMF has a loop end of 0, other v4 files have proper loop ends. Later X-Tracker versions import it as-is but it cannot be intentional.
|
||
|
// We just assume that this feature might have been buggy in early v4 versions and ignore the loop end in that case.
|
||
|
if(fileHeader.version == 4 && seqLoopEnd == 0)
|
||
|
seqLoopEnd = ORDERINDEX_MAX;
|
||
|
ReadOrderFromFile<uint16le>(Order(), chunk, chunk.BytesLeft() / 2);
|
||
|
LimitMax(seqLoopStart, Order().GetLastIndex());
|
||
|
LimitMax(seqLoopEnd, Order().GetLastIndex());
|
||
|
|
||
|
// Read patterns
|
||
|
chunk = chunks.GetChunk(DMFChunk::idPATT);
|
||
|
if(chunk.IsValid() && (loadFlags & loadPatternData))
|
||
|
{
|
||
|
DMFPatterns patHeader;
|
||
|
chunk.ReadStruct(patHeader);
|
||
|
m_nChannels = Clamp<uint8, uint8>(patHeader.numTracks, 1, 32) + 1; // + 1 for global track (used for tempo stuff)
|
||
|
|
||
|
// First, find out where all of our patterns are...
|
||
|
std::vector<FileReader> patternChunks(patHeader.numPatterns);
|
||
|
for(auto &patternChunk : patternChunks)
|
||
|
{
|
||
|
const uint8 headerSize = fileHeader.version < 3 ? 9 : 8;
|
||
|
chunk.Skip(headerSize - sizeof(uint32le));
|
||
|
const uint32 patLength = chunk.ReadUint32LE();
|
||
|
chunk.SkipBack(headerSize);
|
||
|
patternChunk = chunk.ReadChunk(headerSize + patLength);
|
||
|
}
|
||
|
|
||
|
// Now go through the order list and load them.
|
||
|
DMFPatternSettings settings(GetNumChannels());
|
||
|
|
||
|
Patterns.ResizeArray(Order().GetLength());
|
||
|
for(PATTERNINDEX &pat : Order())
|
||
|
{
|
||
|
// Create one pattern for each order item, as the same pattern can be played with different settings
|
||
|
if(pat < patternChunks.size())
|
||
|
{
|
||
|
pat = ConvertDMFPattern(patternChunks[pat], fileHeader.version, settings, *this);
|
||
|
}
|
||
|
}
|
||
|
// Write loop end if necessary
|
||
|
if(Order().IsValidPat(seqLoopEnd) && (seqLoopStart > 0 || seqLoopEnd < Order().GetLastIndex()))
|
||
|
{
|
||
|
PATTERNINDEX pat = Order()[seqLoopEnd];
|
||
|
Patterns[pat].WriteEffect(EffectWriter(CMD_POSITIONJUMP, static_cast<ModCommand::PARAM>(seqLoopStart)).Row(Patterns[pat].GetNumRows() - 1).RetryPreviousRow());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Read song message
|
||
|
chunk = chunks.GetChunk(DMFChunk::idCMSG);
|
||
|
if(chunk.IsValid())
|
||
|
{
|
||
|
// The song message seems to start at a 1 byte offset.
|
||
|
// The skipped byte seems to always be 0.
|
||
|
// This also matches how XT 1.03 itself displays the song message.
|
||
|
chunk.Skip(1);
|
||
|
m_songMessage.ReadFixedLineLength(chunk, chunk.BytesLeft(), 40, 0);
|
||
|
}
|
||
|
|
||
|
// Read sample headers + data
|
||
|
FileReader sampleDataChunk = chunks.GetChunk(DMFChunk::idSMPD);
|
||
|
chunk = chunks.GetChunk(DMFChunk::idSMPI);
|
||
|
m_nSamples = chunk.ReadUint8();
|
||
|
|
||
|
for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
|
||
|
{
|
||
|
const uint8 nameLength = (fileHeader.version < 2) ? 30 : chunk.ReadUint8();
|
||
|
chunk.ReadString<mpt::String::spacePadded>(m_szNames[smp], nameLength);
|
||
|
DMFSampleHeader sampleHeader;
|
||
|
ModSample &sample = Samples[smp];
|
||
|
chunk.ReadStruct(sampleHeader);
|
||
|
sampleHeader.ConvertToMPT(sample);
|
||
|
|
||
|
// Read library name in version 8 files
|
||
|
if(fileHeader.version >= 8)
|
||
|
chunk.ReadString<mpt::String::spacePadded>(sample.filename, 8);
|
||
|
|
||
|
// Filler + CRC
|
||
|
chunk.Skip(fileHeader.version > 1 ? 6 : 2);
|
||
|
|
||
|
// Now read the sample data from the data chunk
|
||
|
FileReader sampleData = sampleDataChunk.ReadChunk(sampleDataChunk.ReadUint32LE());
|
||
|
if(sampleData.IsValid() && (loadFlags & loadSampleData))
|
||
|
{
|
||
|
SampleIO(
|
||
|
sample.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
|
||
|
SampleIO::mono,
|
||
|
SampleIO::littleEndian,
|
||
|
(sampleHeader.flags & DMFSampleHeader::smpCompMask) == DMFSampleHeader::smpComp1 ? SampleIO::DMF : SampleIO::signedPCM)
|
||
|
.ReadSample(sample, sampleData);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
InitializeChannels();
|
||
|
m_SongFlags = SONG_LINEARSLIDES | SONG_ITCOMPATGXX; // this will be converted to IT format by MPT. SONG_ITOLDEFFECTS is not set because of tremor and vibrato.
|
||
|
m_nDefaultSpeed = 6;
|
||
|
m_nDefaultTempo.Set(120);
|
||
|
m_nDefaultGlobalVolume = 256;
|
||
|
m_nSamplePreAmp = m_nVSTiVolume = 48;
|
||
|
m_playBehaviour.set(kApplyOffsetWithoutNote);
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
///////////////////////////////////////////////////////////////////////
|
||
|
// DMF Compression
|
||
|
|
||
|
struct DMFHNode
|
||
|
{
|
||
|
int16 left, right;
|
||
|
uint8 value;
|
||
|
};
|
||
|
|
||
|
struct DMFHTree
|
||
|
{
|
||
|
BitReader file;
|
||
|
int lastnode = 0, nodecount = 0;
|
||
|
DMFHNode nodes[256]{};
|
||
|
|
||
|
DMFHTree(FileReader &file)
|
||
|
: file(file)
|
||
|
{
|
||
|
}
|
||
|
|
||
|
//
|
||
|
// tree: [8-bit value][12-bit index][12-bit index] = 32-bit
|
||
|
//
|
||
|
|
||
|
void DMFNewNode()
|
||
|
{
|
||
|
int actnode = nodecount;
|
||
|
if(actnode > 255) return;
|
||
|
nodes[actnode].value = static_cast<uint8>(file.ReadBits(7));
|
||
|
bool isLeft = file.ReadBits(1) != 0;
|
||
|
bool isRight = file.ReadBits(1) != 0;
|
||
|
actnode = lastnode;
|
||
|
if(actnode > 255) return;
|
||
|
nodecount++;
|
||
|
lastnode = nodecount;
|
||
|
if(isLeft)
|
||
|
{
|
||
|
nodes[actnode].left = (int16)lastnode;
|
||
|
DMFNewNode();
|
||
|
} else
|
||
|
{
|
||
|
nodes[actnode].left = -1;
|
||
|
}
|
||
|
lastnode = nodecount;
|
||
|
if(isRight)
|
||
|
{
|
||
|
nodes[actnode].right = (int16)lastnode;
|
||
|
DMFNewNode();
|
||
|
} else
|
||
|
{
|
||
|
nodes[actnode].right = -1;
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
|
||
|
uintptr_t DMFUnpack(FileReader &file, uint8 *psample, uint32 maxlen)
|
||
|
{
|
||
|
DMFHTree tree(file);
|
||
|
uint8 value = 0, delta = 0;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
tree.DMFNewNode();
|
||
|
if(tree.nodes[0].left < 0 || tree.nodes[0].right < 0)
|
||
|
return tree.file.GetPosition();
|
||
|
for(uint32 i = 0; i < maxlen; i++)
|
||
|
{
|
||
|
int actnode = 0;
|
||
|
bool sign = tree.file.ReadBits(1) != 0;
|
||
|
do
|
||
|
{
|
||
|
if(tree.file.ReadBits(1))
|
||
|
actnode = tree.nodes[actnode].right;
|
||
|
else
|
||
|
actnode = tree.nodes[actnode].left;
|
||
|
if(actnode > 255) break;
|
||
|
delta = tree.nodes[actnode].value;
|
||
|
} while((tree.nodes[actnode].left >= 0) && (tree.nodes[actnode].right >= 0));
|
||
|
if(sign) delta ^= 0xFF;
|
||
|
value += delta;
|
||
|
psample[i] = value;
|
||
|
}
|
||
|
} catch(const BitReader::eof &)
|
||
|
{
|
||
|
//AddToLog(LogWarning, "Truncated DMF sample block");
|
||
|
}
|
||
|
return tree.file.GetPosition();
|
||
|
}
|
||
|
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|