Cog/Frameworks/OpenMPT/OpenMPT/soundlib/Load_dtm.cpp
Christopher Snowhill da1973bcd9 Build libOpenMPT from source once again
Bundle libOpenMPT as a dynamic framework, which should be safe once
again, now that there is only one version to bundle. Also, now it is
using the versions of libvorbisfile and libmpg123 that are bundled with
the player, instead of compiling minimp3 and stbvorbis.

Signed-off-by: Christopher Snowhill <kode54@gmail.com>
2022-06-30 22:56:52 -07:00

607 lines
16 KiB
C++

/*
* Load_dtm.cpp
* ------------
* Purpose: Digital Tracker / Digital Home Studio module Loader (DTM)
* Notes : (currently none)
* Authors: OpenMPT Devs
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
*/
#include "stdafx.h"
#include "Loaders.h"
OPENMPT_NAMESPACE_BEGIN
enum PatternFormats : uint32
{
DTM_PT_PATTERN_FORMAT = 0,
DTM_204_PATTERN_FORMAT = MagicBE("2.04"),
DTM_206_PATTERN_FORMAT = MagicBE("2.06"),
};
struct DTMFileHeader
{
char magic[4];
uint32be headerSize;
uint16be type; // 0 = module
uint8be stereoMode; // FF = panoramic stereo, 00 = old stereo
uint8be bitDepth; // Typically 8, sometimes 16, but is not actually used anywhere?
uint16be reserved; // Usually 0, but not in unknown title 1.dtm and unknown title 2.dtm
uint16be speed;
uint16be tempo;
uint32be forcedSampleRate; // Seems to be ignored in newer files
};
MPT_BINARY_STRUCT(DTMFileHeader, 22)
// IFF-style Chunk
struct DTMChunk
{
// 32-Bit chunk identifiers
enum ChunkIdentifiers
{
idS_Q_ = MagicBE("S.Q."),
idPATT = MagicBE("PATT"),
idINST = MagicBE("INST"),
idIENV = MagicBE("IENV"),
idDAPT = MagicBE("DAPT"),
idDAIT = MagicBE("DAIT"),
idTEXT = MagicBE("TEXT"),
idPATN = MagicBE("PATN"),
idTRKN = MagicBE("TRKN"),
idVERS = MagicBE("VERS"),
idSV19 = MagicBE("SV19"),
};
uint32be id;
uint32be length;
size_t GetLength() const
{
return length;
}
ChunkIdentifiers GetID() const
{
return static_cast<ChunkIdentifiers>(id.get());
}
};
MPT_BINARY_STRUCT(DTMChunk, 8)
struct DTMSample
{
uint32be reserved; // 0x204 for first sample, 0x208 for second, etc...
uint32be length; // in bytes
uint8be finetune; // -8....7
uint8be volume; // 0...64
uint32be loopStart; // in bytes
uint32be loopLength; // ditto
char name[22];
uint8be stereo;
uint8be bitDepth;
uint16be transpose;
uint16be unknown;
uint32be sampleRate;
void ConvertToMPT(ModSample &mptSmp, uint32 forcedSampleRate, uint32 formatVersion) const
{
mptSmp.Initialize(MOD_TYPE_IT);
mptSmp.nLength = length;
mptSmp.nLoopStart = loopStart;
mptSmp.nLoopEnd = mptSmp.nLoopStart + loopLength;
// In revolution to come.dtm, the file header says samples rate is 24512 Hz, but samples say it's 50000 Hz
// Digital Home Studio ignores the header setting in 2.04-/2.06-style modules
mptSmp.nC5Speed = (formatVersion == DTM_PT_PATTERN_FORMAT && forcedSampleRate > 0) ? forcedSampleRate : sampleRate;
int32 transposeAmount = MOD2XMFineTune(finetune);
if(formatVersion == DTM_206_PATTERN_FORMAT && transpose > 0 && transpose != 48)
{
// Digital Home Studio applies this unconditionally, but some old songs sound wrong then (delirium.dtm).
// Digital Tracker 2.03 ignores the setting.
// Maybe this should not be applied for "real" Digital Tracker modules?
transposeAmount += (48 - transpose) * 128;
}
mptSmp.Transpose(transposeAmount * (1.0 / (12.0 * 128.0)));
mptSmp.nVolume = std::min(volume.get(), uint8(64)) * 4u;
if(stereo & 1)
{
mptSmp.uFlags.set(CHN_STEREO);
mptSmp.nLength /= 2u;
mptSmp.nLoopStart /= 2u;
mptSmp.nLoopEnd /= 2u;
}
if(bitDepth > 8)
{
mptSmp.uFlags.set(CHN_16BIT);
mptSmp.nLength /= 2u;
mptSmp.nLoopStart /= 2u;
mptSmp.nLoopEnd /= 2u;
}
if(mptSmp.nLoopEnd > mptSmp.nLoopStart + 1)
{
mptSmp.uFlags.set(CHN_LOOP);
} else
{
mptSmp.nLoopStart = mptSmp.nLoopEnd = 0;
}
}
};
MPT_BINARY_STRUCT(DTMSample, 50)
struct DTMInstrument
{
uint16be insNum;
uint8be unknown1;
uint8be envelope; // 0xFF = none
uint8be sustain; // 0xFF = no sustain point
uint16be fadeout;
uint8be vibRate;
uint8be vibDepth;
uint8be modulationRate;
uint8be modulationDepth;
uint8be breathRate;
uint8be breathDepth;
uint8be volumeRate;
uint8be volumeDepth;
};
MPT_BINARY_STRUCT(DTMInstrument, 15)
struct DTMEnvelope
{
struct DTMEnvPoint
{
uint8be value;
uint8be tick;
};
uint16be numPoints;
DTMEnvPoint points[16];
};
MPT_BINARY_STRUCT(DTMEnvelope::DTMEnvPoint, 2)
MPT_BINARY_STRUCT(DTMEnvelope, 34)
struct DTMText
{
uint16be textType; // 0 = pattern, 1 = free, 2 = song
uint32be textLength;
uint16be tabWidth;
uint16be reserved;
uint16be oddLength;
};
MPT_BINARY_STRUCT(DTMText, 12)
static bool ValidateHeader(const DTMFileHeader &fileHeader)
{
if(std::memcmp(fileHeader.magic, "D.T.", 4)
|| fileHeader.headerSize < sizeof(fileHeader) - 8u
|| fileHeader.headerSize > 256 // Excessively long song title?
|| fileHeader.type != 0)
{
return false;
}
return true;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderDTM(MemoryFileReader file, const uint64 *pfilesize)
{
DTMFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
MPT_UNREFERENCED_PARAMETER(pfilesize);
return ProbeSuccess;
}
bool CSoundFile::ReadDTM(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
DTMFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return false;
}
if(!ValidateHeader(fileHeader))
{
return false;
}
if(loadFlags == onlyVerifyHeader)
{
return true;
}
InitializeGlobals(MOD_TYPE_DTM);
InitializeChannels();
m_SongFlags.set(SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS);
m_playBehaviour.reset(kITVibratoTremoloPanbrello);
// Various files have a default speed or tempo of 0
if(fileHeader.tempo)
m_nDefaultTempo.Set(fileHeader.tempo);
if(fileHeader.speed)
m_nDefaultSpeed = fileHeader.speed;
if(fileHeader.stereoMode == 0)
SetupMODPanning(true);
file.ReadString<mpt::String::maybeNullTerminated>(m_songName, fileHeader.headerSize - (sizeof(fileHeader) - 8u));
auto chunks = ChunkReader(file).ReadChunks<DTMChunk>(1);
// Read order list
if(FileReader chunk = chunks.GetChunk(DTMChunk::idS_Q_))
{
uint16 ordLen = chunk.ReadUint16BE();
uint16 restartPos = chunk.ReadUint16BE();
chunk.Skip(4); // Reserved
ReadOrderFromFile<uint8>(Order(), chunk, ordLen);
Order().SetRestartPos(restartPos);
} else
{
return false;
}
// Read pattern properties
uint32 patternFormat;
if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATT))
{
m_nChannels = chunk.ReadUint16BE();
if(m_nChannels < 1 || m_nChannels > 32)
{
return false;
}
Patterns.ResizeArray(chunk.ReadUint16BE()); // Number of stored patterns, may be lower than highest pattern number
patternFormat = chunk.ReadUint32BE();
if(patternFormat != DTM_PT_PATTERN_FORMAT && patternFormat != DTM_204_PATTERN_FORMAT && patternFormat != DTM_206_PATTERN_FORMAT)
{
return false;
}
} else
{
return false;
}
// Read global info
if(FileReader chunk = chunks.GetChunk(DTMChunk::idSV19))
{
chunk.Skip(2); // Ticks per quarter note, typically 24
uint32 fractionalTempo = chunk.ReadUint32BE();
m_nDefaultTempo = TEMPO(m_nDefaultTempo.GetInt() + fractionalTempo / 4294967296.0);
uint16be panning[32];
chunk.ReadArray(panning);
for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++)
{
// Panning is in range 0...180, 90 = center
ChnSettings[chn].nPan = static_cast<uint16>(128 + Util::muldivr(std::min(static_cast<int>(panning[chn]), int(180)) - 90, 128, 90));
}
chunk.Skip(16);
// Chunk ends here for old DTM modules
if(chunk.CanRead(2))
{
m_nDefaultGlobalVolume = std::min(chunk.ReadUint16BE(), static_cast<uint16>(MAX_GLOBAL_VOLUME));
}
chunk.Skip(128);
uint16be volume[32];
if(chunk.ReadArray(volume))
{
for(CHANNELINDEX chn = 0; chn < 32 && chn < GetNumChannels(); chn++)
{
// Volume is in range 0...128, 64 = normal
ChnSettings[chn].nVolume = static_cast<uint8>(std::min(static_cast<int>(volume[chn]), int(128)) / 2);
}
m_nSamplePreAmp *= 2; // Compensate for channel volume range
}
}
// Read song message
if(FileReader chunk = chunks.GetChunk(DTMChunk::idTEXT))
{
DTMText text;
chunk.ReadStruct(text);
if(text.oddLength == 0xFFFF)
{
chunk.Skip(1);
}
m_songMessage.Read(chunk, chunk.BytesLeft(), SongMessage::leCRLF);
}
// Read sample headers
if(FileReader chunk = chunks.GetChunk(DTMChunk::idINST))
{
uint16 numSamples = chunk.ReadUint16BE();
bool newSamples = (numSamples >= 0x8000);
numSamples &= 0x7FFF;
if(numSamples >= MAX_SAMPLES || !chunk.CanRead(numSamples * (sizeof(DTMSample) + (newSamples ? 2u : 0u))))
{
return false;
}
m_nSamples = numSamples;
for(SAMPLEINDEX smp = 1; smp <= numSamples; smp++)
{
SAMPLEINDEX realSample = newSamples ? (chunk.ReadUint16BE() + 1u) : smp;
DTMSample dtmSample;
chunk.ReadStruct(dtmSample);
if(realSample < 1 || realSample >= MAX_SAMPLES)
{
continue;
}
m_nSamples = std::max(m_nSamples, realSample);
ModSample &mptSmp = Samples[realSample];
dtmSample.ConvertToMPT(mptSmp, fileHeader.forcedSampleRate, patternFormat);
m_szNames[realSample] = mpt::String::ReadBuf(mpt::String::maybeNullTerminated, dtmSample.name);
}
if(chunk.ReadUint16BE() == 0x0004)
{
// Digital Home Studio instruments
m_nInstruments = std::min(static_cast<INSTRUMENTINDEX>(m_nSamples), static_cast<INSTRUMENTINDEX>(MAX_INSTRUMENTS - 1));
FileReader envChunk = chunks.GetChunk(DTMChunk::idIENV);
while(chunk.CanRead(sizeof(DTMInstrument)))
{
DTMInstrument instr;
chunk.ReadStruct(instr);
if(instr.insNum < GetNumInstruments())
{
ModSample &sample = Samples[instr.insNum + 1];
sample.nVibDepth = instr.vibDepth;
sample.nVibRate = instr.vibRate;
sample.nVibSweep = 255;
ModInstrument *mptIns = AllocateInstrument(instr.insNum + 1, instr.insNum + 1);
if(mptIns != nullptr)
{
InstrumentEnvelope &mptEnv = mptIns->VolEnv;
mptIns->nFadeOut = std::min(static_cast<uint16>(instr.fadeout), uint16(0xFFF));
if(instr.envelope != 0xFF && envChunk.Seek(2 + sizeof(DTMEnvelope) * instr.envelope))
{
DTMEnvelope env;
envChunk.ReadStruct(env);
mptEnv.dwFlags.set(ENV_ENABLED);
mptEnv.resize(std::min({ static_cast<std::size_t>(env.numPoints), std::size(env.points), static_cast<std::size_t>(MAX_ENVPOINTS) }));
for(size_t i = 0; i < mptEnv.size(); i++)
{
mptEnv[i].value = std::min(uint8(64), static_cast<uint8>(env.points[i].value));
mptEnv[i].tick = env.points[i].tick;
}
if(instr.sustain != 0xFF)
{
mptEnv.dwFlags.set(ENV_SUSTAIN);
mptEnv.nSustainStart = mptEnv.nSustainEnd = instr.sustain;
}
if(!mptEnv.empty())
{
mptEnv.dwFlags.set(ENV_LOOP);
mptEnv.nLoopStart = mptEnv.nLoopEnd = static_cast<uint8>(mptEnv.size() - 1);
}
}
}
}
}
}
}
// Read pattern data
for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAPT))
{
chunk.Skip(4); // FF FF FF FF
PATTERNINDEX patNum = chunk.ReadUint16BE();
ROWINDEX numRows = chunk.ReadUint16BE();
if(patternFormat == DTM_206_PATTERN_FORMAT)
{
// The stored data is actually not row-based, but tick-based.
numRows /= m_nDefaultSpeed;
}
if(!(loadFlags & loadPatternData) || patNum > 255 || !Patterns.Insert(patNum, numRows))
{
continue;
}
if(patternFormat == DTM_206_PATTERN_FORMAT)
{
chunk.Skip(4);
for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)
{
uint16 length = chunk.ReadUint16BE();
if(length % 2u) length++;
FileReader rowChunk = chunk.ReadChunk(length);
int tick = 0;
std::div_t position = { 0, 0 };
while(rowChunk.CanRead(6) && static_cast<ROWINDEX>(position.quot) < numRows)
{
ModCommand *m = Patterns[patNum].GetpModCommand(position.quot, chn);
const auto [note, volume, instr, command, param, delay] = rowChunk.ReadArray<uint8, 6>();
if(note > 0 && note <= 96)
{
m->note = note + NOTE_MIN + 12;
if(position.rem)
{
m->command = CMD_MODCMDEX;
m->param = 0xD0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15));
}
} else if(note & 0x80)
{
// Lower 7 bits contain note, probably intended for MIDI-like note-on/note-off events
if(position.rem)
{
m->command = CMD_MODCMDEX;
m->param = 0xC0 | static_cast<ModCommand::PARAM>(std::min(position.rem, 15));
} else
{
m->note = NOTE_NOTECUT;
}
}
if(volume)
{
m->volcmd = VOLCMD_VOLUME;
m->vol = std::min(volume, uint8(64)); // Volume can go up to 255, but we do not support over-amplification at the moment.
}
if(instr)
{
m->instr = instr;
}
if(command || param)
{
m->command = command;
m->param = param;
ConvertModCommand(*m);
#ifdef MODPLUG_TRACKER
m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this);
#endif
// G is 8-bit volume
// P is tremor (need to disable oldfx)
}
if(delay & 0x80)
tick += (delay & 0x7F) * 0x100 + rowChunk.ReadUint8();
else
tick += delay;
position = std::div(tick, m_nDefaultSpeed);
}
}
} else
{
ModCommand *m = Patterns[patNum].GetpModCommand(0, 0);
for(ROWINDEX row = 0; row < numRows; row++)
{
for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, m++)
{
const auto data = chunk.ReadArray<uint8, 4>();
if(patternFormat == DTM_204_PATTERN_FORMAT)
{
const auto [note, instrVol, instrCmd, param] = data;
if(note > 0 && note < 0x80)
{
m->note = (note >> 4) * 12 + (note & 0x0F) + NOTE_MIN + 11;
}
uint8 vol = instrVol >> 2;
if(vol)
{
m->volcmd = VOLCMD_VOLUME;
m->vol = vol - 1u;
}
m->instr = ((instrVol & 0x03) << 4) | (instrCmd >> 4);
m->command = instrCmd & 0x0F;
m->param = param;
} else
{
ReadMODPatternEntry(data, *m);
m->instr |= data[0] & 0x30; // Allow more than 31 instruments
}
ConvertModCommand(*m);
// Fix commands without memory and slide nibble precedence
switch(m->command)
{
case CMD_PORTAMENTOUP:
case CMD_PORTAMENTODOWN:
if(!m->param)
{
m->command = CMD_NONE;
}
break;
case CMD_VOLUMESLIDE:
case CMD_TONEPORTAVOL:
case CMD_VIBRATOVOL:
if(m->param & 0xF0)
{
m->param &= 0xF0;
} else if(!m->param)
{
m->command = CMD_NONE;
}
break;
default:
break;
}
#ifdef MODPLUG_TRACKER
m->Convert(MOD_TYPE_MOD, MOD_TYPE_IT, *this);
#endif
}
}
}
}
// Read pattern names
if(FileReader chunk = chunks.GetChunk(DTMChunk::idPATN))
{
PATTERNINDEX pat = 0;
std::string name;
while(chunk.CanRead(1) && pat < Patterns.Size())
{
chunk.ReadNullString(name, 32);
Patterns[pat].SetName(name);
pat++;
}
}
// Read channel names
if(FileReader chunk = chunks.GetChunk(DTMChunk::idTRKN))
{
CHANNELINDEX chn = 0;
std::string name;
while(chunk.CanRead(1) && chn < GetNumChannels())
{
chunk.ReadNullString(name, 32);
ChnSettings[chn].szName = name;
chn++;
}
}
// Read sample data
for(auto &chunk : chunks.GetAllChunks(DTMChunk::idDAIT))
{
SAMPLEINDEX smp = chunk.ReadUint16BE();
if(smp >= GetNumSamples() || !(loadFlags & loadSampleData))
{
continue;
}
ModSample &mptSmp = Samples[smp + 1];
SampleIO(
mptSmp.uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit,
mptSmp.uFlags[CHN_STEREO] ? SampleIO::stereoInterleaved: SampleIO::mono,
SampleIO::bigEndian,
SampleIO::signedPCM).ReadSample(mptSmp, chunk);
}
// Is this accurate?
mpt::ustring tracker;
if(patternFormat == DTM_206_PATTERN_FORMAT)
{
tracker = U_("Digital Home Studio");
} else if(FileReader chunk = chunks.GetChunk(DTMChunk::idVERS))
{
uint32 version = chunk.ReadUint32BE();
tracker = MPT_UFORMAT("Digital Tracker {}.{}")(version >> 4, version & 0x0F);
} else
{
tracker = U_("Digital Tracker");
}
m_modFormat.formatName = U_("Digital Tracker");
m_modFormat.type = U_("dtm");
m_modFormat.madeWithTracker = std::move(tracker);
m_modFormat.charset = mpt::Charset::Amiga_no_C1;
return true;
}
OPENMPT_NAMESPACE_END