484 lines
11 KiB
C++
484 lines
11 KiB
C++
/*
|
|
* Load_sfx.cpp
|
|
* ------------
|
|
* Purpose: SFX / MMS (SoundFX / MultiMedia Sound) module loader
|
|
* Notes : Mostly based on the Soundtracker loader, some effect behavior is based on Flod's implementation.
|
|
* Authors: Devin Acker
|
|
* OpenMPT Devs
|
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
|
*/
|
|
|
|
#include "stdafx.h"
|
|
#include "Loaders.h"
|
|
#include "Tables.h"
|
|
|
|
OPENMPT_NAMESPACE_BEGIN
|
|
|
|
// File Header
|
|
struct SFXFileHeader
|
|
{
|
|
uint8be numOrders;
|
|
uint8be restartPos;
|
|
uint8be orderList[128];
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(SFXFileHeader, 130)
|
|
|
|
// Sample Header
|
|
struct SFXSampleHeader
|
|
{
|
|
char name[22];
|
|
char dummy[2]; // Supposedly sample length, but almost always incorrect
|
|
uint8be finetune;
|
|
uint8be volume;
|
|
uint16be loopStart;
|
|
uint16be loopLength;
|
|
|
|
// Convert an MOD sample header to OpenMPT's internal sample header.
|
|
void ConvertToMPT(ModSample &mptSmp, uint32 length) const
|
|
{
|
|
mptSmp.Initialize(MOD_TYPE_MOD);
|
|
mptSmp.nLength = length;
|
|
mptSmp.nFineTune = MOD2XMFineTune(finetune);
|
|
mptSmp.nVolume = 4u * std::min(volume.get(), uint8(64));
|
|
|
|
SmpLength lStart = loopStart;
|
|
SmpLength lLength = loopLength * 2u;
|
|
|
|
if(mptSmp.nLength)
|
|
{
|
|
mptSmp.nLoopStart = lStart;
|
|
mptSmp.nLoopEnd = lStart + lLength;
|
|
|
|
if(mptSmp.nLoopStart >= mptSmp.nLength)
|
|
{
|
|
mptSmp.nLoopStart = mptSmp.nLength - 1;
|
|
}
|
|
if(mptSmp.nLoopEnd > mptSmp.nLength)
|
|
{
|
|
mptSmp.nLoopEnd = mptSmp.nLength;
|
|
}
|
|
if(mptSmp.nLoopStart > mptSmp.nLoopEnd || mptSmp.nLoopEnd < 4 || mptSmp.nLoopEnd - mptSmp.nLoopStart < 4)
|
|
{
|
|
mptSmp.nLoopStart = 0;
|
|
mptSmp.nLoopEnd = 0;
|
|
}
|
|
|
|
if(mptSmp.nLoopEnd > mptSmp.nLoopStart)
|
|
{
|
|
mptSmp.uFlags.set(CHN_LOOP);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
MPT_BINARY_STRUCT(SFXSampleHeader, 30)
|
|
|
|
static uint8 ClampSlideParam(uint8 value, uint8 lowNote, uint8 highNote)
|
|
{
|
|
uint16 lowPeriod, highPeriod;
|
|
|
|
if(lowNote < highNote &&
|
|
lowNote >= 24 + NOTE_MIN &&
|
|
highNote >= 24 + NOTE_MIN &&
|
|
lowNote < std::size(ProTrackerPeriodTable) + 24 + NOTE_MIN &&
|
|
highNote < std::size(ProTrackerPeriodTable) + 24 + NOTE_MIN)
|
|
{
|
|
lowPeriod = ProTrackerPeriodTable[lowNote - 24 - NOTE_MIN];
|
|
highPeriod = ProTrackerPeriodTable[highNote - 24 - NOTE_MIN];
|
|
|
|
// with a fixed speed of 6 ticks/row, and excluding the first row,
|
|
// 1xx/2xx param has a max value of (low-high)/5 to avoid sliding too far
|
|
return std::min(value, static_cast<uint8>((lowPeriod - highPeriod) / 5));
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
|
|
static bool ValidateHeader(const SFXFileHeader &fileHeader)
|
|
{
|
|
if(fileHeader.numOrders > 128)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderSFX(MemoryFileReader file, const uint64 *pfilesize)
|
|
{
|
|
SAMPLEINDEX numSamples = 0;
|
|
if(numSamples == 0)
|
|
{
|
|
file.Rewind();
|
|
if(!file.CanRead(0x40))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
if(file.Seek(0x3c) && file.ReadMagic("SONG"))
|
|
{
|
|
numSamples = 15;
|
|
}
|
|
}
|
|
if(numSamples == 0)
|
|
{
|
|
file.Rewind();
|
|
if(!file.CanRead(0x80))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
if(file.Seek(0x7C) && file.ReadMagic("SO31"))
|
|
{
|
|
numSamples = 31;
|
|
}
|
|
}
|
|
if(numSamples == 0)
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
file.Rewind();
|
|
for(SAMPLEINDEX smp = 0; smp < numSamples; smp++)
|
|
{
|
|
if(file.ReadUint32BE() > 131072)
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
}
|
|
file.Skip(4);
|
|
if(!file.CanRead(2))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
uint16 speed = file.ReadUint16BE();
|
|
if(speed < 178)
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
if(!file.CanRead(sizeof(SFXSampleHeader) * numSamples))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
file.Skip(sizeof(SFXSampleHeader) * numSamples);
|
|
SFXFileHeader fileHeader;
|
|
if(!file.ReadStruct(fileHeader))
|
|
{
|
|
return ProbeWantMoreData;
|
|
}
|
|
if(!ValidateHeader(fileHeader))
|
|
{
|
|
return ProbeFailure;
|
|
}
|
|
MPT_UNREFERENCED_PARAMETER(pfilesize);
|
|
return ProbeSuccess;
|
|
}
|
|
|
|
|
|
bool CSoundFile::ReadSFX(FileReader &file, ModLoadingFlags loadFlags)
|
|
{
|
|
if(file.Seek(0x3C), file.ReadMagic("SONG"))
|
|
{
|
|
InitializeGlobals(MOD_TYPE_SFX);
|
|
m_nSamples = 15;
|
|
} else if(file.Seek(0x7C), file.ReadMagic("SO31"))
|
|
{
|
|
InitializeGlobals(MOD_TYPE_SFX);
|
|
m_nSamples = 31;
|
|
} else
|
|
{
|
|
return false;
|
|
}
|
|
|
|
uint32 sampleLen[31];
|
|
|
|
file.Rewind();
|
|
for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++)
|
|
{
|
|
sampleLen[smp] = file.ReadUint32BE();
|
|
if(sampleLen[smp] > 131072)
|
|
return false;
|
|
}
|
|
|
|
m_nChannels = 4;
|
|
m_nInstruments = 0;
|
|
m_nDefaultSpeed = 6;
|
|
m_nMinPeriod = 14 * 4;
|
|
m_nMaxPeriod = 3424 * 4;
|
|
m_nSamplePreAmp = 64;
|
|
|
|
// Setup channel pan positions and volume
|
|
SetupMODPanning(true);
|
|
|
|
file.Skip(4);
|
|
uint16 speed = file.ReadUint16BE();
|
|
if(speed < 178)
|
|
return false;
|
|
m_nDefaultTempo = TEMPO((14565.0 * 122.0) / speed);
|
|
|
|
file.Skip(14);
|
|
|
|
uint32 invalidChars = 0;
|
|
for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++)
|
|
{
|
|
SFXSampleHeader sampleHeader;
|
|
|
|
file.ReadStruct(sampleHeader);
|
|
sampleHeader.ConvertToMPT(Samples[smp], sampleLen[smp - 1]);
|
|
|
|
// Get rid of weird characters in sample names.
|
|
for(char &c : sampleHeader.name)
|
|
{
|
|
if(c > 0 && c < ' ')
|
|
{
|
|
c = ' ';
|
|
invalidChars++;
|
|
}
|
|
}
|
|
if(invalidChars >= 128)
|
|
return false;
|
|
m_szNames[smp] = mpt::String::ReadBuf(mpt::String::spacePadded, sampleHeader.name);
|
|
}
|
|
|
|
// Broken conversions of the "Operation Stealth" soundtrack (BOND23 / BOND32)
|
|
// There is a converter that shifts all note values except FFFD (empty note) to the left by 1 bit,
|
|
// but it should not do that for FFFE (STP) notes - as a consequence, they turn into pattern breaks (FFFC).
|
|
const bool fixPatternBreaks = (m_szNames[1] == "BASSE2.AMI") || (m_szNames[1] == "PRA1.AMI");
|
|
|
|
SFXFileHeader fileHeader;
|
|
if(!file.ReadStruct(fileHeader))
|
|
{
|
|
return false;
|
|
}
|
|
if(!ValidateHeader(fileHeader))
|
|
{
|
|
return false;
|
|
}
|
|
if(loadFlags == onlyVerifyHeader)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
PATTERNINDEX numPatterns = 0;
|
|
for(ORDERINDEX ord = 0; ord < fileHeader.numOrders; ord++)
|
|
{
|
|
numPatterns = std::max(numPatterns, static_cast<PATTERNINDEX>(fileHeader.orderList[ord] + 1));
|
|
}
|
|
|
|
if(fileHeader.restartPos < fileHeader.numOrders)
|
|
Order().SetRestartPos(fileHeader.restartPos);
|
|
else
|
|
Order().SetRestartPos(0);
|
|
|
|
ReadOrderFromArray(Order(), fileHeader.orderList, fileHeader.numOrders);
|
|
|
|
// SFX v2 / MMS modules have 4 extra bytes here for some reason
|
|
if(m_nSamples == 31)
|
|
file.Skip(4);
|
|
|
|
uint8 lastNote[4] = {0};
|
|
uint8 slideTo[4] = {0};
|
|
uint8 slideRate[4] = {0};
|
|
uint8 version = 0;
|
|
|
|
// Reading patterns
|
|
if(loadFlags & loadPatternData)
|
|
Patterns.ResizeArray(numPatterns);
|
|
for(PATTERNINDEX pat = 0; pat < numPatterns; pat++)
|
|
{
|
|
if(!(loadFlags & loadPatternData) || !Patterns.Insert(pat, 64))
|
|
{
|
|
file.Skip(64 * 4 * 4);
|
|
continue;
|
|
}
|
|
|
|
for(ROWINDEX row = 0; row < 64; row++)
|
|
{
|
|
PatternRow rowBase = Patterns[pat].GetpModCommand(row, 0);
|
|
for(CHANNELINDEX chn = 0; chn < 4; chn++)
|
|
{
|
|
ModCommand &m = rowBase[chn];
|
|
auto data = file.ReadArray<uint8, 4>();
|
|
|
|
if(data[0] == 0xFF)
|
|
{
|
|
lastNote[chn] = slideRate[chn] = 0;
|
|
|
|
if(fixPatternBreaks && data[1] == 0xFC)
|
|
data[1] = 0xFE;
|
|
|
|
switch(data[1])
|
|
{
|
|
case 0xFE: // STP (note cut)
|
|
m.command = CMD_VOLUME;
|
|
continue;
|
|
case 0xFD: // PIC (null)
|
|
continue;
|
|
case 0xFC: // BRK (pattern break)
|
|
m.command = CMD_PATTERNBREAK;
|
|
version = 9;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
ReadMODPatternEntry(data, m);
|
|
if(m.note != NOTE_NONE)
|
|
{
|
|
lastNote[chn] = m.note;
|
|
slideRate[chn] = 0;
|
|
if(m.note < NOTE_MIDDLEC - 12)
|
|
{
|
|
version = std::max(version, uint8(8));
|
|
}
|
|
}
|
|
|
|
if(m.command || m.param)
|
|
{
|
|
switch(m.command)
|
|
{
|
|
case 0x1: // Arpeggio
|
|
m.command = CMD_ARPEGGIO;
|
|
break;
|
|
|
|
case 0x2: // Portamento (like Ultimate Soundtracker)
|
|
if(m.param & 0xF0)
|
|
{
|
|
m.command = CMD_PORTAMENTODOWN;
|
|
m.param >>= 4;
|
|
} else if(m.param & 0xF)
|
|
{
|
|
m.command = CMD_PORTAMENTOUP;
|
|
m.param &= 0x0F;
|
|
} else
|
|
{
|
|
m.command = m.param = 0;
|
|
}
|
|
break;
|
|
|
|
case 0x3: // Enable LED filter
|
|
// Give precedence to 7xy/8xy slides
|
|
if(slideRate[chn])
|
|
{
|
|
m.command = m.param = 0;
|
|
break;
|
|
}
|
|
m.command = CMD_MODCMDEX;
|
|
m.param = 0;
|
|
break;
|
|
|
|
case 0x4: // Disable LED filter
|
|
// Give precedence to 7xy/8xy slides
|
|
if(slideRate[chn])
|
|
{
|
|
m.command = m.param = 0;
|
|
break;
|
|
}
|
|
m.command = CMD_MODCMDEX;
|
|
m.param = 1;
|
|
break;
|
|
|
|
case 0x5: // Increase volume
|
|
if(m.instr)
|
|
{
|
|
m.command = CMD_VOLUME;
|
|
m.param = std::min(ModCommand::PARAM(0x3F), static_cast<ModCommand::PARAM>((Samples[m.instr].nVolume / 4u) + m.param));
|
|
|
|
// Give precedence to 7xy/8xy slides (and move this to the volume column)
|
|
if(slideRate[chn])
|
|
{
|
|
m.volcmd = VOLCMD_VOLUME;
|
|
m.vol = m.param;
|
|
m.command = m.param = 0;
|
|
break;
|
|
}
|
|
} else
|
|
{
|
|
m.command = m.param = 0;
|
|
}
|
|
break;
|
|
|
|
case 0x6: // Decrease volume
|
|
if(m.instr)
|
|
{
|
|
m.command = CMD_VOLUME;
|
|
if((Samples[m.instr].nVolume / 4u) >= m.param)
|
|
m.param = static_cast<ModCommand::PARAM>(Samples[m.instr].nVolume / 4u) - m.param;
|
|
else
|
|
m.param = 0;
|
|
|
|
// Give precedence to 7xy/8xy slides (and move this to the volume column)
|
|
if(slideRate[chn])
|
|
{
|
|
m.volcmd = VOLCMD_VOLUME;
|
|
m.vol = m.param;
|
|
m.command = m.param = 0;
|
|
break;
|
|
}
|
|
} else
|
|
{
|
|
m.command = m.param = 0;
|
|
}
|
|
break;
|
|
|
|
case 0x7: // 7xy: Slide down x semitones at speed y
|
|
slideTo[chn] = lastNote[chn] - (m.param >> 4);
|
|
|
|
m.command = CMD_PORTAMENTODOWN;
|
|
slideRate[chn] = m.param & 0xF;
|
|
m.param = ClampSlideParam(slideRate[chn], slideTo[chn], lastNote[chn]);
|
|
break;
|
|
|
|
case 0x8: // 8xy: Slide up x semitones at speed y
|
|
slideTo[chn] = lastNote[chn] + (m.param >> 4);
|
|
|
|
m.command = CMD_PORTAMENTOUP;
|
|
slideRate[chn] = m.param & 0xF;
|
|
m.param = ClampSlideParam(slideRate[chn], lastNote[chn], slideTo[chn]);
|
|
break;
|
|
|
|
case 0x9: // 9xy: Auto slide
|
|
version = std::max(version, uint8(8));
|
|
[[fallthrough]];
|
|
default:
|
|
m.command = CMD_NONE;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Continue 7xy/8xy slides if needed
|
|
if(m.command == CMD_NONE && slideRate[chn])
|
|
{
|
|
if(slideTo[chn])
|
|
{
|
|
m.note = lastNote[chn] = slideTo[chn];
|
|
m.param = slideRate[chn];
|
|
slideTo[chn] = 0;
|
|
}
|
|
m.command = CMD_TONEPORTAMENTO;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reading samples
|
|
if(loadFlags & loadSampleData)
|
|
{
|
|
for(SAMPLEINDEX smp = 1; smp <= m_nSamples; smp++) if(Samples[smp].nLength)
|
|
{
|
|
SampleIO(
|
|
SampleIO::_8bit,
|
|
SampleIO::mono,
|
|
SampleIO::littleEndian,
|
|
SampleIO::signedPCM)
|
|
.ReadSample(Samples[smp], file);
|
|
}
|
|
}
|
|
|
|
m_modFormat.formatName = m_nSamples == 15 ? MPT_UFORMAT("SoundFX 1.{}")(version) : U_("SoundFX 2.0 / MultiMedia Sound");
|
|
m_modFormat.type = m_nSamples == 15 ? UL_("sfx") : UL_("sfx2");
|
|
m_modFormat.charset = mpt::Charset::Amiga_no_C1;
|
|
|
|
return true;
|
|
}
|
|
|
|
OPENMPT_NAMESPACE_END
|