/* * Load_s3m.cpp * ------------ * Purpose: S3M (ScreamTracker 3) module loader / saver * 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" #include "S3MTools.h" #include "ITTools.h" #ifndef MODPLUG_NO_FILESAVE #include "mpt/io/base.hpp" #include "mpt/io/io.hpp" #include "mpt/io/io_stdstream.hpp" #include "../common/mptFileIO.h" #ifdef MODPLUG_TRACKER #include "../mptrack/TrackerSettings.h" #endif // MODPLUG_TRACKER #endif // MODPLUG_NO_FILESAVE #include "../common/version.h" OPENMPT_NAMESPACE_BEGIN void CSoundFile::S3MConvert(ModCommand &m, bool fromIT) { switch(m.command | 0x40) { case '@': m.command = (m.param ? CMD_DUMMY : CMD_NONE); break; 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 = fromIT ? CMD_SMOOTHMIDI : CMD_MIDI; break; // Chars under 0x40 don't save properly, so the following commands don't map to their pattern editor representations case ']': m.command = fromIT ? CMD_DELAYCUT : CMD_NONE; break; case '[': m.command = fromIT ? CMD_XPARAM : CMD_NONE; break; case '^': m.command = fromIT ? CMD_FINETUNE : CMD_NONE; break; case '_': m.command = fromIT ? CMD_FINETUNE_SMOOTH : CMD_NONE; break; // BeRoTracker extensions case '1' + 0x41: m.command = fromIT ? CMD_KEYOFF : CMD_NONE; break; case '2' + 0x41: m.command = fromIT ? CMD_SETENVPOSITION : CMD_NONE; break; default: m.command = CMD_NONE; } } #ifndef MODPLUG_NO_FILESAVE void CSoundFile::S3MSaveConvert(uint8 &command, uint8 ¶m, bool toIT, bool compatibilityExport) const { const bool extendedIT = !compatibilityExport && toIT; switch(command) { case CMD_DUMMY: command = (param ? '@' : 0); break; 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_OFFSETPERCENTAGE: 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(extendedIT) command = '\\'; else command = 'Z'; 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; // Chars under 0x40 don't save properly, so map : to ] and # to [. case CMD_DELAYCUT: command = extendedIT ? ']' : 0; break; case CMD_XPARAM: command = extendedIT ? '[' : 0; break; case CMD_FINETUNE: command = extendedIT ? '^' : 0; break; case CMD_FINETUNE_SMOOTH: command = extendedIT ? '_' : 0; break; default: command = 0; } if(command == 0) { param = 0; } command &= ~0x40; } #endif // MODPLUG_NO_FILESAVE 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(); // Is it a valid S3M file? S3MFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(!ValidateHeader(fileHeader)) { return false; } if(!file.CanRead(mpt::saturate_cast(GetHeaderMinimumAdditionalSize(fileHeader)))) { return false; } if(loadFlags == onlyVerifyHeader) { return true; } InitializeGlobals(MOD_TYPE_S3M); m_nMinPeriod = 64; m_nMaxPeriod = 32767; // ST3 ignored Zxx commands, so if we find that a file was made with ST3, we should erase all MIDI macros. bool keepMidiMacros = false; mpt::ustring madeWithTracker; bool formatTrackerStr = false; bool nonCompatTracker = false; bool isST3 = false; bool isSchism = false; const int32 schismDateVersion = SchismTrackerEpoch + ((fileHeader.cwtv == 0x4FFF) ? fileHeader.reserved2 : (fileHeader.cwtv - 0x4050)); switch(fileHeader.cwtv & S3MFileHeader::trackerMask) { case S3MFileHeader::trkAkord & S3MFileHeader::trackerMask: if(fileHeader.cwtv == S3MFileHeader::trkAkord) madeWithTracker = U_("Akord"); break; case S3MFileHeader::trkScreamTracker: if(fileHeader.cwtv == S3MFileHeader::trkST3_20 && fileHeader.special == 0 && (fileHeader.ordNum & 0x0F) == 0 && fileHeader.ultraClicks == 0 && (fileHeader.flags & ~0x50) == 0) { // MPT and OpenMPT before 1.17.03.02 - Simply keep default (filter) MIDI macros if((fileHeader.masterVolume & 0x80) != 0) { m_dwLastSavedWithVersion = MPT_V("1.16.00.00"); madeWithTracker = U_("ModPlug Tracker / OpenMPT 1.17"); } else { // MPT 1.0 alpha5 doesn't set the stereo flag, but MPT 1.0 beta1 does. m_dwLastSavedWithVersion = MPT_V("1.00.00.00"); madeWithTracker = U_("ModPlug Tracker 1.0 alpha"); } 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 { // ST3.20 should only ever write ultra-click values 16, 24 and 32 (corresponding to 8, 12 and 16 in the GUI), ST3.01/3.03 should only write 0, // though several ST3.01/3.03 files with ultra-click values of 16 have been found as well. // However, we won't fingerprint these values here as it's unlikely that there is any other tracker out there disguising as ST3 and using a strange ultra-click value. // Also, re-saving a file with a strange ultra-click value in ST3 doesn't fix this value unless the user manually changes it, or if it's below 16. 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_UFORMAT("Impulse Tracker 2.14p{}")(fileHeader.cwtv - S3MFileHeader::trkIT2_14); } if(fileHeader.cwtv >= S3MFileHeader::trkIT2_07 && fileHeader.reserved3 != 0) { // Starting from version 2.07, IT stores the total edit time of a module in the "reserved" field uint32 editTime = DecodeITEditTimer(fileHeader.cwtv, fileHeader.reserved3); FileHistory hist; hist.openTime = static_cast(editTime * (HISTORY_TIMER_PRECISION / 18.2)); m_FileHistory.push_back(hist); } nonCompatTracker = true; m_playBehaviour.set(kPeriodsAreHertz); m_playBehaviour.set(kITRetrigger); m_playBehaviour.set(kITShortSampleRetrig); m_playBehaviour.set(kST3SampleSwap); // Not exactly like ST3, but close enough m_nMinPeriod = 1; break; case S3MFileHeader::trkSchismTracker: if(fileHeader.cwtv == S3MFileHeader::trkBeRoTrackerOld) { madeWithTracker = U_("BeRoTracker"); m_playBehaviour.set(kST3LimitPeriod); } else { madeWithTracker = GetSchismTrackerVersion(fileHeader.cwtv, fileHeader.reserved2); m_nMinPeriod = 1; isSchism = true; if(schismDateVersion >= SchismVersionFromDate<2021, 05, 02>::date) m_playBehaviour.set(kPeriodsAreHertz); if(schismDateVersion >= SchismVersionFromDate<2016, 05, 13>::date) m_playBehaviour.set(kITShortSampleRetrig); } nonCompatTracker = true; break; case S3MFileHeader::trkOpenMPT: { uint32 mptVersion = (fileHeader.cwtv & S3MFileHeader::versionMask) << 16; if(mptVersion >= 0x01'29'00'00) mptVersion |= fileHeader.reserved2; m_dwLastSavedWithVersion = Version(mptVersion); madeWithTracker = U_("OpenMPT ") + mpt::ufmt::val(m_dwLastSavedWithVersion); } 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_UFORMAT("{} {}.{}")(madeWithTracker, (fileHeader.cwtv & 0xF00) >> 8, mpt::ufmt::hex0<2>(fileHeader.cwtv & 0xFF)); } m_modFormat.formatName = U_("Scream Tracker 3"); m_modFormat.type = U_("s3m"); m_modFormat.madeWithTracker = std::move(madeWithTracker); m_modFormat.charset = m_dwLastSavedWithVersion ? mpt::Charset::Windows1252 : mpt::Charset::CP437; 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); m_playBehaviour.reset(kApplyUpperPeriodLimit); } if((fileHeader.cwtv & S3MFileHeader::trackerMask) > S3MFileHeader::trkScreamTracker) { if((fileHeader.cwtv & S3MFileHeader::trackerMask) != S3MFileHeader::trkImpulseTracker || fileHeader.cwtv >= S3MFileHeader::trkIT2_14) { // Keep MIDI macros if this is not an old IT version (BABYLON.S3M by Necros has Zxx commands and was saved with IT 2.05) keepMidiMacros = true; } } m_MidiCfg.Reset(); if(!keepMidiMacros) { // Remove macros so they don't interfere with tunes made in trackers that don't support Zxx m_MidiCfg.ClearZxxMacros(); } m_songName = mpt::String::ReadBuf(mpt::String::nullTerminated, 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_00 || (fileHeader.flags & S3MFileHeader::fastVolumeSlides) != 0) { m_SongFlags.set(SONG_FASTVOLSLIDES); } // Speed m_nDefaultSpeed = fileHeader.speed; if(m_nDefaultSpeed == 0 || (m_nDefaultSpeed == 255 && isST3)) { // Even though ST3 accepts the command AFF as expected, it mysteriously fails to load a default speed of 255... m_nDefaultSpeed = 6; } // Tempo m_nDefaultTempo.Set(fileHeader.tempo); if(fileHeader.tempo < 33) { // ST3 also fails to load an otherwise valid default tempo of 32... m_nDefaultTempo.Set(isST3 ? 125 : 32); } // Global Volume m_nDefaultGlobalVolume = std::min(fileHeader.globalVol.get(), uint8(64)) * 4u; // The following check is probably not very reliable, but it fixes a few tunes, e.g. // DARKNESS.S3M by Purple Motion (ST 3.00) and "Image of Variance" by C.C.Catch (ST 3.01): if(m_nDefaultGlobalVolume == 0 && fileHeader.cwtv < S3MFileHeader::trkST3_20) { m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; } if(fileHeader.formatVersion == S3MFileHeader::oldVersion && fileHeader.masterVolume < 8) m_nSamplePreAmp = std::min((fileHeader.masterVolume + 1) * 0x10, 0x7F); // These changes were probably only supposed to be done for older format revisions, where supposedly 0x10 was the stereo flag. // However, this version check is missing in ST3, so any mono file with a master volume of 18 will be converted to a stereo file with master volume 32. else if(fileHeader.masterVolume == 2 || fileHeader.masterVolume == (2 | 0x10)) m_nSamplePreAmp = 0x20; else if(!(fileHeader.masterVolume & 0x7F)) m_nSamplePreAmp = 48; else m_nSamplePreAmp = std::max(fileHeader.masterVolume & 0x7F, 0x10); // Bit 7 = Stereo (we always use stereo) const bool isStereo = (fileHeader.masterVolume & 0x80) != 0 || m_dwLastSavedWithVersion; if(!isStereo) m_nSamplePreAmp = Util::muldivr_unsigned(m_nSamplePreAmp, 8, 11); // Approximately as loud as in DOSBox and a real SoundBlaster 16 m_nVSTiVolume = 36; if(isSchism && schismDateVersion < SchismVersionFromDate<2018, 11, 12>::date) m_nVSTiVolume = 64; // Channel setup 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; if(isStereo) ChnSettings[i].nPan = (ctype & 8) ? 0xCC : 0x33; // 200 : 56 } if(fileHeader.channels[i] & 0x80) { ChnSettings[i].dwFlags = CHN_MUTE; } if(ctype >= 16 && ctype <= 29) { // Adlib channel - except for OpenMPT 1.19 and older, which would write wrong channel types for PCM channels 16-32. // However, MPT/OpenMPT always wrote the extra panning table, so there is no need to consider this here. ChnSettings[i].nPan = 128; isAdlibChannel[i] = true; } } if(m_nChannels < 1) { m_nChannels = 1; } ReadOrderFromFile(Order(), file, fileHeader.ordNum, 0xFF, 0xFE); // Read sample header offsets std::vector sampleOffsets; file.ReadVector(sampleOffsets, fileHeader.smpNum); // Read pattern offsets std::vector patternOffsets; file.ReadVector(patternOffsets, fileHeader.patNum); // Read extended channel panning 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(pan[i] & 0x0F) * 256 + 8) / 15; } } } // Reading sample headers m_nSamples = std::min(static_cast(fileHeader.smpNum), static_cast(MAX_SAMPLES - 1)); bool anySamples = false; uint16 gusAddresses = 0; 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], isST3); m_szNames[smp + 1] = mpt::String::ReadBuf(mpt::String::nullTerminated, sampleHeader.name); if(sampleHeader.sampleType < S3MSampleHeader::typeAdMel) { const uint32 sampleOffset = sampleHeader.GetSampleOffset(); if((loadFlags & loadSampleData) && sampleHeader.length != 0 && file.Seek(sampleOffset)) { sampleHeader.GetSampleFormat((fileHeader.formatVersion == S3MFileHeader::oldVersion)).ReadSample(Samples[smp + 1], file); anySamples = true; } gusAddresses |= sampleHeader.gusAddress; } } if(isST3 && anySamples && !gusAddresses && fileHeader.cwtv != S3MFileHeader::trkST3_00) { // All Scream Tracker versions except for some probably early revisions of Scream Tracker 3.00 write GUS addresses. GUS support might not have existed at that point (1992). // Hence if a file claims to be written with ST3 (but not ST3.00), but has no GUS addresses, we deduce that it must be written by some other software (e.g. some PSM -> S3M conversions) isST3 = false; MPT_UNUSED(isST3); m_modFormat.madeWithTracker = U_("Unknown"); } else if(isST3) { // Saving an S3M file in ST3 with the Gravis Ultrasound driver loaded will write a unique GUS memory address for each non-empty sample slot (and 0 for unused slots). // Re-saving that file in ST3 with the SoundBlaster driver loaded will reset the GUS address for all samples to 0 (unused) or 1 (used). // The first used sample will also have an address of 1 with the GUS driver. // So this is a safe way of telling if the file was last saved with the GUS driver loaded or not if there's more than one sample. const bool useGUS = gusAddresses > 1; m_playBehaviour.set(kST3PortaSampleChange, useGUS); m_playBehaviour.set(kST3SampleSwap, !useGUS); m_playBehaviour.set(kITShortSampleRetrig, !useGUS); // Only half the truth but close enough for now m_modFormat.madeWithTracker += useGUS ? UL_(" (GUS)") : UL_(" (SB)"); // ST3's GUS driver doesn't use this value. Ignoring it fixes the balance between FM and PCM samples (e.g. in Rotagilla by Manwe) if(useGUS) m_nSamplePreAmp = 48; } // Try to find out if Zxx commands are supposed to be panning commands (PixPlay). // Actually I am only aware of one module that uses this panning style, namely "Crawling Despair" by $volkraq // and I have no idea what PixPlay is, so this code is solely based on the sample text of that module. // We won't convert if there are not enough Zxx commands, too "high" Zxx commands // or there are only "left" or "right" pannings (we assume that stereo should be somewhat balanced), // and modules not made with an old version of ST3 were probably made in a tracker that supports panning anyway. bool pixPlayPanning = (fileHeader.cwtv < S3MFileHeader::trkST3_20); int zxxCountRight = 0, zxxCountLeft = 0; // Reading patterns if(!(loadFlags & loadPatternData)) { return true; } // Order list cannot contain pattern indices > 255, so do not even try to load higher patterns const PATTERNINDEX readPatterns = std::min(static_cast(fileHeader.patNum), static_cast(uint8_max)); Patterns.ResizeArray(readPatterns); for(PATTERNINDEX pat = 0; pat < readPatterns; pat++) { // A zero parapointer indicates an empty pattern. if(!Patterns.Insert(pat, 64) || patternOffsets[pat] == 0 || !file.Seek(patternOffsets[pat] * 16)) { continue; } // Skip pattern length indication. // Some modules, for example http://aminet.net/mods/8voic/s3m_hunt.lha seem to have a wrong pattern length - // If you strictly adhere the pattern length, you won't read some patterns (e.g. 17) correctly in that module. // It's most likely a broken copy because there are other versions of the track which don't have this issue. // Still, we don't really need this information, so we just ignore it. file.Skip(2); // Read pattern data ROWINDEX row = 0; PatternRow rowBase = Patterns[pat].GetRow(0); while(row < 64) { uint8 info = file.ReadUint8(); if(info == s3mEndOfRow) { // End of row if(++row < 64) { rowBase = Patterns[pat].GetRow(row); } continue; } CHANNELINDEX channel = (info & s3mChannelMask); ModCommand dummy; ModCommand &m = (channel < GetNumChannels()) ? rowBase[channel] : dummy; if(info & s3mNotePresent) { const auto [note, instr] = file.ReadArray(); if(note < 0xF0) m.note = static_cast(Clamp((note & 0x0F) + 12 * (note >> 4) + 12 + NOTE_MIN, NOTE_MIN, NOTE_MAX)); else if(note == s3mNoteOff) m.note = NOTE_NOTECUT; else if(note == s3mNoteNone) m.note = NOTE_NONE; 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 = std::min(volume, uint8(64)); } } if(info & s3mEffectPresent) { const auto [command, param] = file.ReadArray(); m.command = command; m.param = param; S3MConvert(m, false); if(m.command == CMD_S3MCMDEX && (m.param & 0xF0) == 0xA0 && fileHeader.cwtv < S3MFileHeader::trkST3_20) { // Convert old SAx panning to S8x (should only be found in PANIC.S3M by Purple Motion) m.param = 0x80 | ((m.param & 0x0F) ^ 8); } else if(m.command == CMD_MIDI) { // PixPlay panning test if(m.param > 0x0F) { // PixPlay has Z00 to Z0F panning, so we ignore this. pixPlayPanning = false; } else { if(m.param < 0x08) zxxCountLeft++; else if(m.param > 0x08) zxxCountRight++; } } else if(m.command == CMD_OFFSET && m.param == 0 && fileHeader.cwtv <= S3MFileHeader::trkST3_01) { // Offset command didn't have effect memory in ST3.01; fixed in ST3.03 m.command = CMD_DUMMY; } } } } if(pixPlayPanning && zxxCountLeft + zxxCountRight >= m_nChannels && (-zxxCountLeft + zxxCountRight) < static_cast(m_nChannels)) { // There are enough Zxx commands, so let's assume this was made to be played with PixPlay 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 constexpr 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::WriteBuf(mpt::String::nullTerminated, fileHeader.name) = m_songName; fileHeader.dosEof = S3MFileHeader::idEOF; fileHeader.fileType = S3MFileHeader::idS3MType; // Orders ORDERINDEX writeOrders = Order().GetLengthTailTrimmed(); if(writeOrders < 2) { writeOrders = 2; } else if((writeOrders % 2u) != 0) { // Number of orders should be even writeOrders++; } LimitMax(writeOrders, static_cast(256)); fileHeader.ordNum = static_cast(writeOrders); // Samples SAMPLEINDEX writeSamples = static_cast(GetNumInstruments()); if(writeSamples == 0) { writeSamples = GetNumSamples(); } writeSamples = Clamp(writeSamples, static_cast(1), static_cast(99)); fileHeader.smpNum = static_cast(writeSamples); // Patterns PATTERNINDEX writePatterns = std::min(Patterns.GetNumPatterns(), PATTERNINDEX(100)); fileHeader.patNum = static_cast(writePatterns); // Flags 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; } // Version info following: ST3.20 = 0x1320 // Most significant nibble = Tracker ID, see S3MFileHeader::S3MTrackerVersions // Following: One nibble = Major version, one byte = Minor version (hex) const uint32 mptVersion = Version::Current().GetRawVersion(); fileHeader.cwtv = S3MFileHeader::trkOpenMPT | static_cast((mptVersion >> 16) & S3MFileHeader::versionMask); fileHeader.reserved2 = static_cast(mptVersion); fileHeader.formatVersion = S3MFileHeader::newVersion; memcpy(fileHeader.magic, "SCRM", 4); // Song Variables fileHeader.globalVol = static_cast(std::min(m_nDefaultGlobalVolume / 4u, uint32(64))); fileHeader.speed = static_cast(Clamp(m_nDefaultSpeed, 1u, 254u)); fileHeader.tempo = static_cast(Clamp(m_nDefaultTempo.GetInt(), 33u, 255u)); fileHeader.masterVolume = static_cast(Clamp(m_nSamplePreAmp, 16u, 127u) | 0x80); fileHeader.ultraClicks = 16; fileHeader.usePanningTable = S3MFileHeader::idPanning; mpt::IO::Write(f, fileHeader); Order().WriteAsByte(f, writeOrders); // Comment about parapointers stolen from Schism Tracker: // The sample data parapointers are 24+4 bits, whereas pattern data and sample headers are only 16+4 // bits -- so while the sample data can be written up to 268 MB within the file (starting at 0xffffff0), // the pattern data and sample headers are restricted to the first 1 MB (starting at 0xffff0). In effect, // this practically requires the sample data to be written last in the file, as it is entirely possible // (and quite easy, even) to write more than 1 MB of sample data in a file. // The "practical standard order" listed in TECH.DOC is sample headers, patterns, then sample data. // Calculate offset of first sample header... mpt::IO::Offset sampleHeaderOffset = mpt::IO::TellWrite(f) + (writeSamples + writePatterns) * 2 + 32; // ...which must be a multiple of 16, because parapointers omit the lowest 4 bits. sampleHeaderOffset = (sampleHeaderOffset + 15) & ~15; std::vector sampleOffsets(writeSamples); for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++) { static_assert((sizeof(S3MSampleHeader) % 16) == 0); sampleOffsets[smp] = static_cast((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 patternOffsets(writePatterns); // Need to calculate the real offsets later. mpt::IO::Write(f, patternOffsets); // Write channel panning uint8 chnPan[32]; for(CHANNELINDEX chn = 0; chn < 32; chn++) { if(chn < GetNumChannels()) chnPan[chn] = static_cast(((ChnSettings[chn].nPan * 15 + 128) / 256) | 0x20); else chnPan[chn] = 0x08; } mpt::IO::Write(f, chnPan); // Do we need to fill up the file with some padding bytes for 16-Byte alignment? mpt::IO::Offset curPos = mpt::IO::TellWrite(f); if(curPos < sampleHeaderOffset) { MPT_ASSERT(sampleHeaderOffset - curPos < 16); mpt::IO::WriteRaw(f, filler, static_cast(sampleHeaderOffset - curPos)); } // Don't write sample headers for now, we are lacking the sample offset data. mpt::IO::SeekAbsolute(f, firstPatternOffset); // Write patterns enum class S3MChannelType : uint8 { kUnused = 0, kPCM = 1, kAdlib = 2 }; FlagSet 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_UFORMAT("Too much pattern data! Writing patterns failed starting from pattern {}.")(pat)); break; } MPT_ASSERT((patOffset % 16) == 0); patternOffsets[pat] = static_cast(patOffset / 16); std::vector buffer; buffer.reserve(5 * 1024); // Reserve space for length bytes buffer.resize(2, 0); if(Patterns.IsValidPat(pat)) { for(ROWINDEX row = 0; row < 64; row++) { if(row >= Patterns[pat].GetNumRows()) { // Invent empty row 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(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 Cut note = s3mNoteOff; } else if(note < 12 + NOTE_MIN) { // Too low 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 = std::min(param, uint8(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 || param) { 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 { // Invent empty pattern buffer.insert(buffer.end(), 64, s3mEndOfRow); } uint16 length = mpt::saturate_cast(buffer.size()); buffer[0] = static_cast(length & 0xFF); buffer[1] = static_cast((length >> 8) & 0xFF); if((buffer.size() % 16u) != 0) { // Add padding bytes buffer.insert(buffer.end(), 16 - (buffer.size() % 16u), 0); } mpt::IO::Write(f, buffer); } if(globalCmdOnMutedChn) { //AddToLog(LogWarning, U_("Global commands on muted channels are interpreted only by some S3M players.")); } mpt::IO::Offset sampleDataOffset = mpt::IO::TellWrite(f); // Write samples std::vector sampleHeader(writeSamples); for(SAMPLEINDEX smp = 0; smp < writeSamples; smp++) { SAMPLEINDEX realSmp = smp + 1; if(GetNumInstruments() != 0 && Instruments[smp] != nullptr) { // Find some valid sample associated with this instrument. 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::WriteBuf(mpt::String::nullTerminated, sampleHeader[smp].name) = m_szNames[realSmp]; if(smpLength != 0) { // Write sample data if(sampleDataOffset > 0xFFFFFF0) { AddToLog(LogError, MPT_UFORMAT("Too much sample data! Writing samples failed starting from sample {}.")(realSmp)); break; } sampleHeader[smp].dataPointer[1] = static_cast((sampleDataOffset >> 4) & 0xFF); sampleHeader[smp].dataPointer[2] = static_cast((sampleDataOffset >> 12) & 0xFF); sampleHeader[smp].dataPointer[0] = static_cast((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; } } } // Channel Table 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_UFORMAT("Pattern channel {} constains both samples and OPL instruments, which is not supported by Scream Tracker 3.")(chn + 1)); } // ST3 only supports 16 PCM channels, so if channels 17-32 are used, // they must be mapped to the same "internal channels" as channels 1-16. // The channel indices determine in which order channels are evaluated in ST3. // First, the "left" channels (0...7) are evaluated, then the "right" channels (8...15). // Previously, an alternating LRLR scheme was written, which would lead to a different // effect processing in ST3 than LLL...RRR, but since OpenMPT doesn't care about the // channel order and always parses them left to right as they appear in the pattern, // we should just write in the LLL...RRR manner. uint8 ch = sampleCh % 16u; // If there are neither PCM nor AdLib instruments on this channel, just fall back a regular sample-based channel for maximum compatibility. 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_UFORMAT("This module has more than 16 ({}) sample channels, which is not supported by Scream Tracker 3.")(sampleCh)); } if(adlibCh > 9) { AddToLog(LogWarning, MPT_UFORMAT("This module has more than 9 ({}) OPL channels, which is not supported by Scream Tracker 3.")(adlibCh)); } mpt::IO::SeekAbsolute(f, 0); mpt::IO::Write(f, fileHeader); // Now we know where the patterns are. if(writePatterns != 0) { mpt::IO::SeekAbsolute(f, patternPointerOffset); mpt::IO::Write(f, patternOffsets); } // And we can finally write the sample headers. if(writeSamples != 0) { mpt::IO::SeekAbsolute(f, sampleHeaderOffset); mpt::IO::Write(f, sampleHeader); } return true; } #endif // MODPLUG_NO_FILESAVE OPENMPT_NAMESPACE_END