#include "stdafx.h"
#include "Loaders.h"
#include "S3MTools.h"
#include "ITTools.h"
#ifndef MODPLUG_NO_FILESAVE
#include "../common/mptFileIO.h"
#ifdef MODPLUG_TRACKER
#include "../mptrack/TrackerSettings.h"
#endif #endif #include "../common/version.h"
OPENMPT_NAMESPACE_BEGIN
void CSoundFile::S3MConvert(ModCommand &m, bool fromIT)
{
switch(m.command | 0x40)
{
case 'A': m.command = CMD_SPEED; break;
case 'B': m.command = CMD_POSITIONJUMP; break;
case 'C': m.command = CMD_PATTERNBREAK; if (!fromIT) m.param = (m.param >> 4) * 10 + (m.param & 0x0F); break;
case 'D': m.command = CMD_VOLUMESLIDE; break;
case 'E': m.command = CMD_PORTAMENTODOWN; break;
case 'F': m.command = CMD_PORTAMENTOUP; break;
case 'G': m.command = CMD_TONEPORTAMENTO; break;
case 'H': m.command = CMD_VIBRATO; break;
case 'I': m.command = CMD_TREMOR; break;
case 'J': m.command = CMD_ARPEGGIO; break;
case 'K': m.command = CMD_VIBRATOVOL; break;
case 'L': m.command = CMD_TONEPORTAVOL; break;
case 'M': m.command = CMD_CHANNELVOLUME; break;
case 'N': m.command = CMD_CHANNELVOLSLIDE; break;
case 'O': m.command = CMD_OFFSET; break;
case 'P': m.command = CMD_PANNINGSLIDE; break;
case 'Q': m.command = CMD_RETRIG; break;
case 'R': m.command = CMD_TREMOLO; break;
case 'S': m.command = CMD_S3MCMDEX; break;
case 'T': m.command = CMD_TEMPO; break;
case 'U': m.command = CMD_FINEVIBRATO; break;
case 'V': m.command = CMD_GLOBALVOLUME; break;
case 'W': m.command = CMD_GLOBALVOLSLIDE; break;
case 'X': m.command = CMD_PANNING8; break;
case 'Y': m.command = CMD_PANBRELLO; break;
case 'Z': m.command = CMD_MIDI; break;
case '\\': m.command = static_cast<ModCommand::COMMAND>(fromIT ? CMD_SMOOTHMIDI : CMD_MIDI); break;
case ']': m.command = CMD_DELAYCUT; break;
case '[': m.command = CMD_XPARAM; break;
default: m.command = CMD_NONE;
}
}
#ifndef MODPLUG_NO_FILESAVE
void CSoundFile::S3MSaveConvert(uint8 &command, uint8 ¶m, bool toIT, bool compatibilityExport) const
{
switch(command)
{
case CMD_SPEED: command = 'A'; break;
case CMD_POSITIONJUMP: command = 'B'; break;
case CMD_PATTERNBREAK: command = 'C'; if(!toIT) param = ((param / 10) << 4) + (param % 10); break;
case CMD_VOLUMESLIDE: command = 'D'; break;
case CMD_PORTAMENTODOWN: command = 'E'; if (param >= 0xE0 && (GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) param = 0xDF; break;
case CMD_PORTAMENTOUP: command = 'F'; if (param >= 0xE0 && (GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM))) param = 0xDF; break;
case CMD_TONEPORTAMENTO: command = 'G'; break;
case CMD_VIBRATO: command = 'H'; break;
case CMD_TREMOR: command = 'I'; break;
case CMD_ARPEGGIO: command = 'J'; break;
case CMD_VIBRATOVOL: command = 'K'; break;
case CMD_TONEPORTAVOL: command = 'L'; break;
case CMD_CHANNELVOLUME: command = 'M'; break;
case CMD_CHANNELVOLSLIDE: command = 'N'; break;
case CMD_OFFSET: command = 'O'; break;
case CMD_PANNINGSLIDE: command = 'P'; break;
case CMD_RETRIG: command = 'Q'; break;
case CMD_TREMOLO: command = 'R'; break;
case CMD_S3MCMDEX: command = 'S'; break;
case CMD_TEMPO: command = 'T'; break;
case CMD_FINEVIBRATO: command = 'U'; break;
case CMD_GLOBALVOLUME: command = 'V'; break;
case CMD_GLOBALVOLSLIDE: command = 'W'; break;
case CMD_PANNING8:
command = 'X';
if(toIT && !(GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM | MOD_TYPE_MOD)))
{
if (param == 0xA4) { command = 'S'; param = 0x91; }
else if (param == 0x80) { param = 0xFF; }
else if (param < 0x80) { param <<= 1; }
else command = 0;
} else if (!toIT && (GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM | MOD_TYPE_MOD)))
{
param >>= 1;
}
break;
case CMD_PANBRELLO: command = 'Y'; break;
case CMD_MIDI: command = 'Z'; break;
case CMD_SMOOTHMIDI:
if(compatibilityExport || !toIT)
command = 'Z';
else
command = '\\';
break;
case CMD_XFINEPORTAUPDOWN:
switch(param & 0xF0)
{
case 0x10: command = 'F'; param = (param & 0x0F) | 0xE0; break;
case 0x20: command = 'E'; param = (param & 0x0F) | 0xE0; break;
case 0x90: command = 'S'; break;
default: command = 0;
}
break;
case CMD_MODCMDEX:
{
ModCommand m;
m.command = CMD_MODCMDEX;
m.param = param;
m.ExtendedMODtoS3MEffect();
command = m.command;
param = m.param;
S3MSaveConvert(command, param, toIT, compatibilityExport);
}
return;
case CMD_DELAYCUT:
if(compatibilityExport || !toIT)
command = 0;
else
command = ']';
break;
case CMD_XPARAM:
if(compatibilityExport || !toIT)
command = 0;
else
command = '[';
break;
default:
command = 0;
}
if(command == 0)
{
param = 0;
}
command &= ~0x40;
}
#endif
enum S3MPattern
{
s3mEndOfRow = 0x00,
s3mChannelMask = 0x1F,
s3mNotePresent = 0x20,
s3mVolumePresent = 0x40,
s3mEffectPresent = 0x80,
s3mAnyPresent = 0xE0,
s3mNoteOff = 0xFE,
s3mNoteNone = 0xFF,
};
static bool ValidateHeader(const S3MFileHeader &fileHeader)
{
if(std::memcmp(fileHeader.magic, "SCRM", 4)
|| fileHeader.fileType != S3MFileHeader::idS3MType
|| (fileHeader.formatVersion != S3MFileHeader::oldVersion && fileHeader.formatVersion != S3MFileHeader::newVersion)
)
{
return false;
}
return true;
}
static uint64 GetHeaderMinimumAdditionalSize(const S3MFileHeader &fileHeader)
{
return fileHeader.ordNum + (fileHeader.smpNum + fileHeader.patNum) * 2;
}
CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderS3M(MemoryFileReader file, const uint64 *pfilesize)
{
S3MFileHeader fileHeader;
if(!file.ReadStruct(fileHeader))
{
return ProbeWantMoreData;
}
if(!ValidateHeader(fileHeader))
{
return ProbeFailure;
}
return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader));
}
bool CSoundFile::ReadS3M(FileReader &file, ModLoadingFlags loadFlags)
{
file.Rewind();
S3MFileHeader 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_S3M);
m_nMinPeriod = 64;
m_nMaxPeriod = 32767;
bool keepMidiMacros = false;
mpt::ustring madeWithTracker;
bool formatTrackerStr = false;
bool nonCompatTracker = false;
bool isST3 = false;
bool isSchism = false;
switch(fileHeader.cwtv & S3MFileHeader::trackerMask)
{
case S3MFileHeader::trkScreamTracker:
if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && (fileHeader.ordNum & 0x0F) == 0 && fileHeader.ultraClicks == 0 && (fileHeader.flags & ~0x50) == 0)
{
m_dwLastSavedWithVersion = MAKE_VERSION_NUMERIC(1, 16, 00, 00);
madeWithTracker = U_("ModPlug Tracker / OpenMPT");
keepMidiMacros = true;
nonCompatTracker = true;
m_playBehaviour.set(kST3LimitPeriod);
} else if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && fileHeader.ultraClicks == 0 && fileHeader.flags == 0 && fileHeader.usePanningTable == 0)
{
madeWithTracker = U_("Velvet Studio");
} else
{
madeWithTracker = U_("Scream Tracker");
formatTrackerStr = true;
isST3 = true;
}
break;
case S3MFileHeader::trkImagoOrpheus:
madeWithTracker = U_("Imago Orpheus");
formatTrackerStr = true;
nonCompatTracker = true;
break;
case S3MFileHeader::trkImpulseTracker:
if(fileHeader.cwtv <= S3MFileHeader::trkIT2_14)
{
madeWithTracker = U_("Impulse Tracker");
formatTrackerStr = true;
} else
{
madeWithTracker = mpt::format(U_("Impulse Tracker 2.14p%1"))(fileHeader.cwtv - S3MFileHeader::trkIT2_14);
}
nonCompatTracker = true;
m_nMinPeriod = 1;
break;
case S3MFileHeader::trkSchismTracker:
if(fileHeader.cwtv == S3MFileHeader::trkBeRoTrackerOld)
{
madeWithTracker = U_("BeRoTracker");
m_playBehaviour.set(kST3LimitPeriod);
} else
{
madeWithTracker = GetSchismTrackerVersion(fileHeader.cwtv);
m_nMinPeriod = 1;
isSchism = true;
}
nonCompatTracker = true;
break;
case S3MFileHeader::trkOpenMPT:
madeWithTracker = U_("OpenMPT");
formatTrackerStr = true;
m_dwLastSavedWithVersion = Version((fileHeader.cwtv & S3MFileHeader::versionMask) << 16);
break;
case S3MFileHeader::trkBeRoTracker:
madeWithTracker = U_("BeRoTracker");
m_playBehaviour.set(kST3LimitPeriod);
break;
case S3MFileHeader::trkCreamTracker:
madeWithTracker = U_("CreamTracker");
break;
default:
if(fileHeader.cwtv == S3MFileHeader::trkCamoto)
madeWithTracker = U_("Camoto");
break;
}
if(formatTrackerStr)
{
madeWithTracker = mpt::format(U_("%1 %2.%3"))(madeWithTracker, (fileHeader.cwtv & 0xF00) >> 8, mpt::ufmt::hex0<2>(fileHeader.cwtv & 0xFF));
}
m_modFormat.formatName = U_("ScreamTracker 3");
m_modFormat.type = U_("s3m");
m_modFormat.madeWithTracker = std::move(madeWithTracker);
m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::CharsetWindows1252 : mpt::CharsetCP437;
if(nonCompatTracker)
{
m_playBehaviour.reset(kST3NoMutedChannels);
m_playBehaviour.reset(kST3EffectMemory);
m_playBehaviour.reset(kST3PortaSampleChange);
m_playBehaviour.reset(kST3VibratoMemory);
m_playBehaviour.reset(KST3PortaAfterArpeggio);
m_playBehaviour.reset(kST3OffsetWithoutInstrument);
}
if((fileHeader.cwtv & S3MFileHeader::trackerMask) > S3MFileHeader::trkScreamTracker)
{
if((fileHeader.cwtv & S3MFileHeader::trackerMask) != S3MFileHeader::trkImpulseTracker || fileHeader.cwtv >= S3MFileHeader::trkIT2_14)
{
keepMidiMacros = true;
}
}
m_MidiCfg.Reset();
if(!keepMidiMacros)
{
m_MidiCfg.ClearZxxMacros();
}
mpt::String::Read<mpt::String::nullTerminated>(m_songName, fileHeader.name);
if(fileHeader.flags & S3MFileHeader::amigaLimits) m_SongFlags.set(SONG_AMIGALIMITS);
if(fileHeader.flags & S3MFileHeader::st2Vibrato) m_SongFlags.set(SONG_S3MOLDVIBRATO);
if(fileHeader.cwtv < S3MFileHeader::trkST3_20 || (fileHeader.flags & S3MFileHeader::fastVolumeSlides) != 0)
{
m_SongFlags.set(SONG_FASTVOLSLIDES);
}
m_nDefaultSpeed = fileHeader.speed;
if(m_nDefaultSpeed == 0 || (m_nDefaultSpeed == 255 && isST3))
{
m_nDefaultSpeed = 6;
}
m_nDefaultTempo.Set(fileHeader.tempo);
if(fileHeader.tempo < 33)
{
m_nDefaultTempo.Set(isST3 ? 125 : 32);
}
m_nDefaultGlobalVolume = std::min<uint32>(fileHeader.globalVol, 64) * 4u;
if(m_nDefaultGlobalVolume == 0 && fileHeader.cwtv < S3MFileHeader::trkST3_20)
{
m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
}
m_nSamplePreAmp = std::max(fileHeader.masterVolume & 0x7F, 0x10);
m_nVSTiVolume = 36;
if(isSchism && fileHeader.cwtv < SchismVersionFromDate<2018, 11, 12>::Version(S3MFileHeader::trkSchismTracker))
m_nVSTiVolume = 64;
m_nChannels = 4;
std::bitset<32> isAdlibChannel;
for(CHANNELINDEX i = 0; i < 32; i++)
{
ChnSettings[i].Reset();
uint8 ctype = fileHeader.channels[i] & ~0x80;
if(fileHeader.channels[i] != 0xFF)
{
m_nChannels = i + 1;
ChnSettings[i].nPan = (ctype & 8) ? 0xCC : 0x33; }
if(fileHeader.channels[i] & 0x80)
{
ChnSettings[i].dwFlags = CHN_MUTE;
}
if(ctype >= 16 && ctype <= 29)
{
ChnSettings[i].nPan = 128;
isAdlibChannel[i] = true;
}
}
if(m_nChannels < 1)
{
m_nChannels = 1;
}
ReadOrderFromFile<uint8>(Order(), file, fileHeader.ordNum, 0xFF, 0xFE);
std::vector<uint16le> sampleOffsets;
file.ReadVector(sampleOffsets, fileHeader.smpNum);
std::vector<uint16le> patternOffsets;
file.ReadVector(patternOffsets, fileHeader.patNum);
if(fileHeader.usePanningTable == S3MFileHeader::idPanning)
{
uint8 pan[32];
file.ReadArray(pan);
for(CHANNELINDEX i = 0; i < 32; i++)
{
if((pan[i] & 0x20) != 0 && (!isST3 || !isAdlibChannel[i]))
{
ChnSettings[i].nPan = (static_cast<uint16>(pan[i] & 0x0F) * 256 + 8) / 15;
}
}
}
m_nSamples = std::min<SAMPLEINDEX>(fileHeader.smpNum, MAX_SAMPLES - 1);
for(SAMPLEINDEX smp = 0; smp < m_nSamples; smp++)
{
S3MSampleHeader sampleHeader;
if(!file.Seek(sampleOffsets[smp] * 16) || !file.ReadStruct(sampleHeader))
{
continue;
}
sampleHeader.ConvertToMPT(Samples[smp + 1]);
mpt::String::Read<mpt::String::nullTerminated>(m_szNames[smp + 1], sampleHeader.name);
if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel)
{
const uint32 sampleOffset = (sampleHeader.dataPointer[1] << 4) | (sampleHeader.dataPointer[2] << 12) | (sampleHeader.dataPointer[0] << 20);
if((loadFlags & loadSampleData) && sampleHeader.length != 0 && file.Seek(sampleOffset))
{
sampleHeader.GetSampleFormat((fileHeader.formatVersion == S3MFileHeader::oldVersion)).ReadSample(Samples[smp + 1], file);
}
}
}
bool pixPlayPanning = (fileHeader.cwtv < S3MFileHeader::trkST3_20);
int zxxCountRight = 0, zxxCountLeft = 0;
if(!(loadFlags & loadPatternData))
{
return true;
}
const PATTERNINDEX readPatterns = std::min<PATTERNINDEX>(fileHeader.patNum, uint8_max);
Patterns.ResizeArray(readPatterns);
for(PATTERNINDEX pat = 0; pat < readPatterns; pat++)
{
if(!Patterns.Insert(pat, 64) || patternOffsets[pat] == 0 || !file.Seek(patternOffsets[pat] * 16))
{
continue;
}
file.Skip(2);
ROWINDEX row = 0;
PatternRow rowBase = Patterns[pat].GetRow(0);
while(row < 64)
{
uint8 info = file.ReadUint8();
if(info == s3mEndOfRow)
{
if(++row < 64)
{
rowBase = Patterns[pat].GetRow(row);
}
continue;
}
CHANNELINDEX channel = (info & s3mChannelMask);
ModCommand dummy = ModCommand::Empty();
ModCommand &m = (channel < GetNumChannels()) ? rowBase[channel] : dummy;
if(info & s3mNotePresent)
{
uint8 note = file.ReadUint8(), instr = file.ReadUint8();
if(note < 0xF0)
{
note = (note & 0x0F) + 12 * (note >> 4) + 12 + NOTE_MIN;
} else if(note == s3mNoteOff)
{
note = NOTE_NOTECUT;
} else if(note == s3mNoteNone)
{
note = NOTE_NONE;
}
m.note = note;
m.instr = instr;
}
if(info & s3mVolumePresent)
{
uint8 volume = file.ReadUint8();
if(volume >= 128 && volume <= 192)
{
m.volcmd = VOLCMD_PANNING;
m.vol = volume - 128;
} else
{
m.volcmd = VOLCMD_VOLUME;
m.vol = MIN(volume, 64);
}
}
if(info & s3mEffectPresent)
{
uint8 command = file.ReadUint8(), param = file.ReadUint8();
if(command != 0)
{
m.command = command;
m.param = param;
S3MConvert(m, false);
}
if(m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0xA0 && fileHeader.cwtv < S3MFileHeader::trkST3_20)
{
m.param = 0x80 | ((m.param & 0x0F) ^ 8);
} else if(m.command == CMD_MIDI)
{
if(m.param > 0x0F)
{
pixPlayPanning = false;
} else
{
if(m.param < 0x08)
{
zxxCountLeft++;
} else if(m.param > 0x08)
{
zxxCountRight++;
}
}
}
}
}
}
if(pixPlayPanning && zxxCountLeft + zxxCountRight >= m_nChannels && (-zxxCountLeft + zxxCountRight) < static_cast<int>(m_nChannels))
{
Patterns.ForEachModCommand([](ModCommand &m)
{
if(m.command == CMD_MIDI)
{
m.command = CMD_S3MCMDEX;
m.param |= 0x80;
}
});
}
return true;
}
#ifndef MODPLUG_NO_FILESAVE
bool CSoundFile::SaveS3M(std::ostream &f) const
{
static const uint8 filler[16] =
{
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80,
};
if(m_nChannels == 0)
{
return false;
}
const bool saveMuteStatus =
#ifdef MODPLUG_TRACKER
TrackerSettings::Instance().MiscSaveChannelMuteStatus;
#else
true;
#endif
S3MFileHeader fileHeader;
MemsetZero(fileHeader);
mpt::String::Write<mpt::String::nullTerminated>(fileHeader.name, m_songName);
fileHeader.dosEof = S3MFileHeader::idEOF;
fileHeader.fileType = S3MFileHeader::idS3MType;
ORDERINDEX writeOrders = Order().GetLengthTailTrimmed();
if(writeOrders < 2)
{
writeOrders = 2;
} else if((writeOrders % 2u) != 0)
{
writeOrders++;
}
LimitMax(writeOrders, static_cast<ORDERINDEX>(256));
fileHeader.ordNum = static_cast<uint16>(writeOrders);
SAMPLEINDEX writeSamples = static_cast<SAMPLEINDEX>(GetNumInstruments());
if(writeSamples == 0)
{
writeSamples = GetNumSamples();
}
writeSamples = Clamp(writeSamples, static_cast<SAMPLEINDEX>(1), static_cast<SAMPLEINDEX>(99));
fileHeader.smpNum = static_cast<uint16>(writeSamples);
PATTERNINDEX writePatterns = MIN(Patterns.GetNumPatterns(), 100u);
fileHeader.patNum = static_cast<uint16>(writePatterns);
if(m_SongFlags[SONG_FASTVOLSLIDES])
{
fileHeader.flags |= S3MFileHeader::fastVolumeSlides;
}
if(m_nMaxPeriod < 20000 || m_SongFlags[SONG_AMIGALIMITS])
{
fileHeader.flags |= S3MFileHeader::amigaLimits;
}
if(m_SongFlags[SONG_S3MOLDVIBRATO])
{
fileHeader.flags |= S3MFileHeader::st2Vibrato;
}
fileHeader.cwtv = S3MFileHeader::trkOpenMPT | static_cast<uint16>((Version::Current().GetRawVersion() >> 16) & S3MFileHeader::versionMask);
fileHeader.formatVersion = S3MFileHeader::newVersion;
memcpy(fileHeader.magic, "SCRM", 4);
fileHeader.globalVol = static_cast<uint8>(std::min<unsigned int>(m_nDefaultGlobalVolume / 4u, 64u));
fileHeader.speed = static_cast<uint8>(Clamp(m_nDefaultSpeed, 1u, 254u));
fileHeader.tempo = static_cast<uint8>(Clamp(m_nDefaultTempo.GetInt(), 33u, 255u));
fileHeader.masterVolume = static_cast<uint8>(Clamp(m_nSamplePreAmp, 16u, 127u) | 0x80);
fileHeader.ultraClicks = 8;
fileHeader.usePanningTable = S3MFileHeader::idPanning;
mpt::IO::Write(f, fileHeader);
Order().WriteAsByte(f, writeOrders);
mpt::IO::Offset sampleHeaderOffset = mpt::IO::TellWrite(f) + (writeSamples + writePatterns) * 2 + 32;
sampleHeaderOffset = (sampleHeaderOffset + 15) & ~15;
std::vector<uint16le> sampleOffsets(writeSamples);
for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++)
{
STATIC_ASSERT((sizeof(S3MSampleHeader) % 16) == 0);
sampleOffsets[smp] = static_cast<uint16>((sampleHeaderOffset + smp * sizeof(S3MSampleHeader)) / 16);
}
mpt::IO::Write(f, sampleOffsets);
mpt::IO::Offset patternPointerOffset = mpt::IO::TellWrite(f);
mpt::IO::Offset firstPatternOffset = sampleHeaderOffset + writeSamples * sizeof(S3MSampleHeader);
std::vector<uint16le> patternOffsets(writePatterns);
mpt::IO::Write(f, patternOffsets);
uint8 chnPan[32];
for(CHANNELINDEX chn = 0; chn < 32; chn++)
{
if(chn < GetNumChannels())
chnPan[chn] = static_cast<uint8>(((ChnSettings[chn].nPan * 15 + 128) / 256) | 0x20);
else
chnPan[chn] = 0x08;
}
mpt::IO::Write(f, chnPan);
mpt::IO::Offset curPos = mpt::IO::TellWrite(f);
if(curPos < sampleHeaderOffset)
{
MPT_ASSERT(sampleHeaderOffset - curPos < 16);
mpt::IO::WriteRaw(f, filler, static_cast<std::size_t>(sampleHeaderOffset - curPos));
}
mpt::IO::SeekAbsolute(f, firstPatternOffset);
enum class S3MChannelType : uint8 { kUnused = 0, kPCM = 1, kAdlib = 2 };
FlagSet<S3MChannelType> channelType[32] = { S3MChannelType::kUnused };
bool globalCmdOnMutedChn = false;
for(PATTERNINDEX pat = 0; pat < writePatterns; pat++)
{
if(Patterns.IsPatternEmpty(pat))
{
patternOffsets[pat] = 0;
continue;
}
mpt::IO::Offset patOffset = mpt::IO::TellWrite(f);
if(patOffset > 0xFFFF0)
{
AddToLog(LogError, mpt::format(U_("Too much pattern data! Writing patterns failed starting from pattern %1."))(pat));
break;
}
MPT_ASSERT((patOffset % 16) == 0);
patternOffsets[pat] = static_cast<uint16>(patOffset / 16);
std::vector<uint8> buffer;
buffer.reserve(5 * 1024);
buffer.resize(2, 0);
if(Patterns.IsValidPat(pat))
{
for(ROWINDEX row = 0; row < 64; row++)
{
if(row >= Patterns[pat].GetNumRows())
{
buffer.push_back(s3mEndOfRow);
continue;
}
const PatternRow rowBase = Patterns[pat].GetRow(row);
CHANNELINDEX writeChannels = std::min(CHANNELINDEX(32), GetNumChannels());
for(CHANNELINDEX chn = 0; chn < writeChannels; chn++)
{
const ModCommand &m = rowBase[chn];
uint8 info = static_cast<uint8>(chn);
uint8 note = m.note;
ModCommand::VOLCMD volcmd = m.volcmd;
uint8 vol = m.vol;
uint8 command = m.command;
uint8 param = m.param;
if(note != NOTE_NONE || m.instr != 0)
{
info |= s3mNotePresent;
if(note == NOTE_NONE)
{
note = s3mNoteNone;
} else if(ModCommand::IsSpecialNote(note))
{
note = s3mNoteOff;
} else if(note < 12 + NOTE_MIN)
{
note = 0;
} else if(note <= NOTE_MAX)
{
note -= (12 + NOTE_MIN);
note = (note % 12) + ((note / 12) << 4);
}
if(m.instr > 0 && m.instr <= GetNumSamples())
{
const ModSample &smp = Samples[m.instr];
if(smp.uFlags[CHN_ADLIB])
channelType[chn].set(S3MChannelType::kAdlib);
else if(smp.HasSampleData())
channelType[chn].set(S3MChannelType::kPCM);
}
}
if(command == CMD_VOLUME)
{
command = CMD_NONE;
volcmd = VOLCMD_VOLUME;
vol = MIN(param, 64);
}
if(volcmd == VOLCMD_VOLUME)
{
info |= s3mVolumePresent;
} else if(volcmd == VOLCMD_PANNING)
{
info |= s3mVolumePresent;
vol |= 0x80;
}
if(command != CMD_NONE)
{
S3MSaveConvert(command, param, false, true);
if(command)
{
info |= s3mEffectPresent;
if(saveMuteStatus && ChnSettings[chn].dwFlags[CHN_MUTE] && m.IsGlobalCommand())
{
globalCmdOnMutedChn = true;
}
}
}
if(info & s3mAnyPresent)
{
buffer.push_back(info);
if(info & s3mNotePresent)
{
buffer.push_back(note);
buffer.push_back(m.instr);
}
if(info & s3mVolumePresent)
{
buffer.push_back(vol);
}
if(info & s3mEffectPresent)
{
buffer.push_back(command);
buffer.push_back(param);
}
}
}
buffer.push_back(s3mEndOfRow);
}
} else
{
buffer.insert(buffer.end(), 64, s3mEndOfRow);
}
uint16 length = mpt::saturate_cast<uint16>(buffer.size());
buffer[0] = static_cast<uint8>(length & 0xFF);
buffer[1] = static_cast<uint8>((length >> 8) & 0xFF);
if((buffer.size() % 16u) != 0)
{
buffer.insert(buffer.end(), 16 - (buffer.size() % 16u), 0);
}
mpt::IO::Write(f, buffer);
}
if(globalCmdOnMutedChn)
{
}
mpt::IO::Offset sampleDataOffset = mpt::IO::TellWrite(f);
std::vector<S3MSampleHeader> sampleHeader(writeSamples);
for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++)
{
SAMPLEINDEX realSmp = smp + 1;
if(GetNumInstruments() != 0 && Instruments[smp] != nullptr)
{
for(SAMPLEINDEX keySmp : Instruments[smp]->Keyboard)
{
if(keySmp > 0 && keySmp <= GetNumSamples())
{
realSmp = keySmp;
break;
}
}
}
if(realSmp > GetNumSamples())
{
continue;
}
const SmpLength smpLength = sampleHeader[smp].ConvertToS3M(Samples[realSmp]);
mpt::String::Write<mpt::String::nullTerminated>(sampleHeader[smp].name, m_szNames[realSmp]);
if(smpLength != 0)
{
if(sampleDataOffset > 0xFFFFFF0)
{
AddToLog(LogError, mpt::format(U_("Too much sample data! Writing samples failed starting from sample %1."))(realSmp));
break;
}
sampleHeader[smp].dataPointer[1] = static_cast<uint8>((sampleDataOffset >> 4) & 0xFF);
sampleHeader[smp].dataPointer[2] = static_cast<uint8>((sampleDataOffset >> 12) & 0xFF);
sampleHeader[smp].dataPointer[0] = static_cast<uint8>((sampleDataOffset >> 20) & 0xFF);
size_t writtenLength = sampleHeader[smp].GetSampleFormat(false).WriteSample(f, Samples[realSmp], smpLength);
sampleDataOffset += writtenLength;
if((writtenLength % 16u) != 0)
{
size_t fillSize = 16 - (writtenLength % 16u);
mpt::IO::WriteRaw(f, filler, fillSize);
sampleDataOffset += fillSize;
}
}
}
uint8 sampleCh = 0, adlibCh = 0;
for(CHANNELINDEX chn = 0; chn < 32; chn++)
{
if(chn < GetNumChannels())
{
if(channelType[chn][S3MChannelType::kPCM] && channelType[chn][S3MChannelType::kAdlib])
{
AddToLog(LogWarning, mpt::format(U_("Pattern channel %1 constains both samples and OPL instruments, which is not supported by Scream Tracker 3."))(chn + 1));
}
uint8 ch = sampleCh % 16u; if(channelType[chn][S3MChannelType::kPCM])
ch = (sampleCh++) % 16u;
else if(channelType[chn][S3MChannelType::kAdlib])
ch = 16 + ((adlibCh++) % 9u);
if(saveMuteStatus && ChnSettings[chn].dwFlags[CHN_MUTE])
{
ch |= 0x80;
}
fileHeader.channels[chn] = ch;
} else
{
fileHeader.channels[chn] = 0xFF;
}
}
if(sampleCh > 16)
{
AddToLog(LogWarning, mpt::format(U_("This module has more than 16 (%1) sample channels, which is not supported by Scream Tracker 3."))(sampleCh));
}
if(adlibCh > 9)
{
AddToLog(LogWarning, mpt::format(U_("This module has more than 9 (%1) OPL channels, which is not supported by Scream Tracker 3."))(adlibCh));
}
mpt::IO::SeekAbsolute(f, 0);
mpt::IO::Write(f, fileHeader);
if(writePatterns != 0)
{
mpt::IO::SeekAbsolute(f, patternPointerOffset);
mpt::IO::Write(f, patternOffsets);
}
if(writeSamples != 0)
{
mpt::IO::SeekAbsolute(f, sampleHeaderOffset);
mpt::IO::Write(f, sampleHeader);
}
return true;
}
#endif
OPENMPT_NAMESPACE_END