123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- /*
- * 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
|