Archived
1
0
This repository has been archived on 2024-10-17. You can view files and clone it, but cannot push or open issues or pull requests.
winamp/Src/external_dependencies/openmpt-trunk/soundlib/Load_dtm.cpp

608 lines
16 KiB
C++
Raw Normal View History

2024-09-24 12:54:57 +00:00
/*
* 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