/* * Load_ams.cpp * ------------ * Purpose: AMS (Extreme's Tracker / Velvet Studio) module loader * Notes : Extreme was renamed to Velvet Development at some point, * and thus they also renamed their tracker from * "Extreme's Tracker" to "Velvet Studio". * While the two programs look rather similiar, the structure of both * programs' "AMS" format is significantly different in some places - * Velvet Studio is a rather advanced tracker in comparison to Extreme's Tracker. * The source code of Velvet Studio has been released into the * public domain in 2013: https://github.com/Patosc/VelvetStudio/commits/master * 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 // Read AMS or AMS2 (newVersion = true) pattern. At least this part of the format is more or less identical between the two trackers... static void ReadAMSPattern(CPattern &pattern, bool newVersion, FileReader &patternChunk) { enum { emptyRow = 0xFF, // No commands on row endOfRowMask = 0x80, // If set, no more commands on this row noteMask = 0x40, // If set, no note+instr in this command channelMask = 0x1F, // Mask for extracting channel // Note flags readNextCmd = 0x80, // One more command follows noteDataMask = 0x7F, // Extract note // Command flags volCommand = 0x40, // Effect is compressed volume command commandMask = 0x3F, // Command or volume mask }; // Effect translation table for extended (non-Protracker) effects static constexpr ModCommand::COMMAND effTrans[] = { CMD_S3MCMDEX, // Forward / Backward CMD_PORTAMENTOUP, // Extra fine slide up CMD_PORTAMENTODOWN, // Extra fine slide up CMD_RETRIG, // Retrigger CMD_NONE, CMD_TONEPORTAVOL, // Toneporta with fine volume slide CMD_VIBRATOVOL, // Vibrato with fine volume slide CMD_NONE, CMD_PANNINGSLIDE, CMD_NONE, CMD_VOLUMESLIDE, // Two times finder volume slide than Axx CMD_NONE, CMD_CHANNELVOLUME, // Channel volume (0...127) CMD_PATTERNBREAK, // Long pattern break (in hex) CMD_S3MCMDEX, // Fine slide commands CMD_NONE, // Fractional BPM CMD_KEYOFF, // Key off at tick xx CMD_PORTAMENTOUP, // Porta up, but uses all octaves (?) CMD_PORTAMENTODOWN, // Porta down, but uses all octaves (?) CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_NONE, CMD_GLOBALVOLSLIDE, // Global volume slide CMD_NONE, CMD_GLOBALVOLUME, // Global volume (0... 127) }; ModCommand dummy; for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) { PatternRow baseRow = pattern.GetRow(row); while(patternChunk.CanRead(1)) { const uint8 flags = patternChunk.ReadUint8(); if(flags == emptyRow) { break; } const CHANNELINDEX chn = (flags & channelMask); ModCommand &m = chn < pattern.GetNumChannels() ? baseRow[chn] : dummy; bool moreCommands = true; if(!(flags & noteMask)) { // Read note + instr uint8 note = patternChunk.ReadUint8(); moreCommands = (note & readNextCmd) != 0; note &= noteDataMask; if(note == 1) { m.note = NOTE_KEYOFF; } else if(note >= 2 && note <= 121 && newVersion) { m.note = note - 2 + NOTE_MIN; } else if(note >= 12 && note <= 108 && !newVersion) { m.note = note + 12 + NOTE_MIN; } m.instr = patternChunk.ReadUint8(); } while(moreCommands) { // Read one more effect command ModCommand origCmd = m; const uint8 command = patternChunk.ReadUint8(), effect = (command & commandMask); moreCommands = (command & readNextCmd) != 0; if(command & volCommand) { m.volcmd = VOLCMD_VOLUME; m.vol = effect; } else { m.param = patternChunk.ReadUint8(); if(effect < 0x10) { // PT commands m.command = effect; CSoundFile::ConvertModCommand(m); // Post-fix some commands switch(m.command) { case CMD_PANNING8: // 4-Bit panning m.command = CMD_PANNING8; m.param = (m.param & 0x0F) * 0x11; break; case CMD_VOLUME: m.command = CMD_NONE; m.volcmd = VOLCMD_VOLUME; m.vol = static_cast(std::min((m.param + 1) / 2, 64)); break; case CMD_MODCMDEX: if(m.param == 0x80) { // Break sample loop (cut after loop) m.command = CMD_NONE; } else { m.ExtendedMODtoS3MEffect(); } break; } } else if(effect < 0x10 + CountOf(effTrans)) { // Extended commands m.command = effTrans[effect - 0x10]; // Post-fix some commands switch(effect) { case 0x10: // Play sample forwards / backwards if(m.param <= 0x01) { m.param |= 0x9E; } else { m.command = CMD_NONE; } break; case 0x11: case 0x12: // Extra fine slides m.param = static_cast(std::min(uint8(0x0F), m.param) | 0xE0); break; case 0x15: case 0x16: // Fine slides m.param = static_cast((std::min(0x10, m.param + 1) / 2) | 0xF0); break; case 0x1E: // More fine slides switch(m.param >> 4) { case 0x1: // Fine porta up m.command = CMD_PORTAMENTOUP; m.param |= 0xF0; break; case 0x2: // Fine porta down m.command = CMD_PORTAMENTODOWN; m.param |= 0xF0; break; case 0xA: // Extra fine volume slide up m.command = CMD_VOLUMESLIDE; m.param = ((((m.param & 0x0F) + 1) / 2) << 4) | 0x0F; break; case 0xB: // Extra fine volume slide down m.command = CMD_VOLUMESLIDE; m.param = (((m.param & 0x0F) + 1) / 2) | 0xF0; break; default: m.command = CMD_NONE; break; } break; case 0x1C: // Adjust channel volume range m.param = static_cast(std::min((m.param + 1) / 2, 64)); break; } } // Try merging commands first ModCommand::CombineEffects(m.command, m.param, origCmd.command, origCmd.param); if(ModCommand::GetEffectWeight(origCmd.command) > ModCommand::GetEffectWeight(m.command)) { if(m.volcmd == VOLCMD_NONE && ModCommand::ConvertVolEffect(m.command, m.param, true)) { // Volume column to the rescue! m.volcmd = m.command; m.vol = m.param; } m.command = origCmd.command; m.param = origCmd.param; } } } if(flags & endOfRowMask) { // End of row break; } } } } ///////////////////////////////////////////////////////////////////// // AMS (Extreme's Tracker) 1.x loader // AMS File Header struct AMSFileHeader { uint8le versionLow; uint8le versionHigh; uint8le channelConfig; uint8le numSamps; uint16le numPats; uint16le numOrds; uint8le midiChannels; uint16le extraSize; }; MPT_BINARY_STRUCT(AMSFileHeader, 11) // AMS Sample Header struct AMSSampleHeader { enum SampleFlags { smp16BitOld = 0x04, // AMS 1.0 (at least according to docs, I yet have to find such a file) smp16Bit = 0x80, // AMS 1.1+ smpPacked = 0x03, }; uint32le length; uint32le loopStart; uint32le loopEnd; uint8le panFinetune; // High nibble = pan position, low nibble = finetune value uint16le sampleRate; uint8le volume; // 0...127 uint8le flags; // See SampleFlags // Convert sample header to OpenMPT's internal format. void ConvertToMPT(ModSample &mptSmp) const { mptSmp.Initialize(); mptSmp.nLength = length; mptSmp.nLoopStart = std::min(loopStart, length); mptSmp.nLoopEnd = std::min(loopEnd, length); mptSmp.nVolume = (std::min(uint8(127), volume.get()) * 256 + 64) / 127; if(panFinetune & 0xF0) { mptSmp.nPan = (panFinetune & 0xF0); mptSmp.uFlags = CHN_PANNING; } mptSmp.nC5Speed = 2 * sampleRate; if(sampleRate == 0) { mptSmp.nC5Speed = 2 * 8363; } uint32 newC4speed = ModSample::TransposeToFrequency(0, MOD2XMFineTune(panFinetune & 0x0F)); mptSmp.nC5Speed = (mptSmp.nC5Speed * newC4speed) / 8363; if(mptSmp.nLoopStart < mptSmp.nLoopEnd) { mptSmp.uFlags.set(CHN_LOOP); } if(flags & (smp16Bit | smp16BitOld)) { mptSmp.uFlags.set(CHN_16BIT); } } }; MPT_BINARY_STRUCT(AMSSampleHeader, 17) static bool ValidateHeader(const AMSFileHeader &fileHeader) { if(fileHeader.versionHigh != 0x01) { return false; } return true; } static uint64 GetHeaderMinimumAdditionalSize(const AMSFileHeader &fileHeader) { return fileHeader.extraSize + 3u + fileHeader.numSamps * (1u + sizeof(AMSSampleHeader)) + fileHeader.numOrds * 2u + fileHeader.numPats * 4u; } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMS(MemoryFileReader file, const uint64 *pfilesize) { if(!file.CanRead(7)) { return ProbeWantMoreData; } if(!file.ReadMagic("Extreme")) { return ProbeFailure; } AMSFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); } bool CSoundFile::ReadAMS(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); if(!file.ReadMagic("Extreme")) { return false; } AMSFileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return false; } if(!ValidateHeader(fileHeader)) { return false; } if(!file.CanRead(mpt::saturate_cast(GetHeaderMinimumAdditionalSize(fileHeader)))) { return false; } if(!file.Skip(fileHeader.extraSize)) { return false; } if(loadFlags == onlyVerifyHeader) { return true; } InitializeGlobals(MOD_TYPE_AMS); m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS; m_nChannels = (fileHeader.channelConfig & 0x1F) + 1; m_nSamples = fileHeader.numSamps; SetupMODPanning(true); m_modFormat.formatName = U_("Extreme's Tracker"); m_modFormat.type = U_("ams"); m_modFormat.madeWithTracker = mpt::format(U_("Extreme's Tracker %1.%2"))(fileHeader.versionHigh, fileHeader.versionLow); m_modFormat.charset = mpt::Charset::CP437; std::vector packSample(fileHeader.numSamps); static_assert(MAX_SAMPLES > 255); for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { AMSSampleHeader sampleHeader; file.ReadStruct(sampleHeader); sampleHeader.ConvertToMPT(Samples[smp]); packSample[smp - 1] = (sampleHeader.flags & AMSSampleHeader::smpPacked) != 0; } // Texts file.ReadSizedString(m_songName); // Read sample names for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { file.ReadSizedString(m_szNames[smp]); } // Read channel names for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) { ChnSettings[chn].Reset(); file.ReadSizedString(ChnSettings[chn].szName); } // Read pattern names and create patterns Patterns.ResizeArray(fileHeader.numPats); for(PATTERNINDEX pat = 0; pat < fileHeader.numPats; pat++) { char name[11]; const bool ok = file.ReadSizedString(name); // Create pattern now, so name won't be reset later. if(Patterns.Insert(pat, 64) && ok) { Patterns[pat].SetName(name); } } // Read packed song message const uint16 packedLength = file.ReadUint16LE(); if(packedLength && file.CanRead(packedLength)) { std::vector textIn; file.ReadVector(textIn, packedLength); std::string textOut; textOut.reserve(packedLength); for(auto c : textIn) { if(c & 0x80) { textOut.insert(textOut.end(), (c & 0x7F), ' '); } else { textOut.push_back(c); } } textOut = mpt::ToCharset(mpt::Charset::CP437, mpt::Charset::CP437AMS, textOut); // Packed text doesn't include any line breaks! m_songMessage.ReadFixedLineLength(mpt::byte_cast(textOut.c_str()), textOut.length(), 76, 0); } // Read Order List ReadOrderFromFile(Order(), file, fileHeader.numOrds); // Read patterns for(PATTERNINDEX pat = 0; pat < fileHeader.numPats && file.CanRead(4); pat++) { uint32 patLength = file.ReadUint32LE(); FileReader patternChunk = file.ReadChunk(patLength); if((loadFlags & loadPatternData) && Patterns.IsValidPat(pat)) { ReadAMSPattern(Patterns[pat], false, patternChunk); } } if(loadFlags & loadSampleData) { // Read Samples for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) { SampleIO( Samples[smp].uFlags[CHN_16BIT] ? SampleIO::_16bit : SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, packSample[smp - 1] ? SampleIO::AMS : SampleIO::signedPCM) .ReadSample(Samples[smp], file); } } return true; } ///////////////////////////////////////////////////////////////////// // AMS (Velvet Studio) 2.0 - 2.02 loader // AMS2 File Header struct AMS2FileHeader { enum FileFlags { linearSlides = 0x40, }; uint8le versionLow; // Version of format (Hi = MainVer, Low = SubVer e.g. 0202 = 2.02) uint8le versionHigh; // ditto uint8le numIns; // Nr of Instruments (0-255) uint16le numPats; // Nr of Patterns (1-1024) uint16le numOrds; // Nr of Positions (1-65535) // Rest of header differs between format revision 2.01 and 2.02 }; MPT_BINARY_STRUCT(AMS2FileHeader, 7) // AMS2 Instument Envelope struct AMS2Envelope { uint8 speed; // Envelope speed (currently not supported, always the same as current BPM) uint8 sustainPoint; // Envelope sustain point uint8 loopStart; // Envelope loop Start uint8 loopEnd; // Envelope loop End uint8 numPoints; // Envelope length // Read envelope and do partial conversion. void ConvertToMPT(InstrumentEnvelope &mptEnv, FileReader &file) { file.ReadStruct(*this); // Read envelope points uint8 data[64][3]; file.ReadStructPartial(data, numPoints * 3); if(numPoints <= 1) { // This is not an envelope. return; } static_assert(MAX_ENVPOINTS >= CountOf(data)); mptEnv.resize(std::min(numPoints, uint8(CountOf(data)))); mptEnv.nLoopStart = loopStart; mptEnv.nLoopEnd = loopEnd; mptEnv.nSustainStart = mptEnv.nSustainEnd = sustainPoint; for(uint32 i = 0; i < mptEnv.size(); i++) { if(i != 0) { mptEnv[i].tick = mptEnv[i - 1].tick + static_cast(std::max(1, data[i][0] | ((data[i][1] & 0x01) << 8))); } mptEnv[i].value = data[i][2]; } } }; MPT_BINARY_STRUCT(AMS2Envelope, 5) // AMS2 Instrument Data struct AMS2Instrument { enum EnvelopeFlags { envLoop = 0x01, envSustain = 0x02, envEnabled = 0x04, // Flag shift amounts volEnvShift = 0, panEnvShift = 1, vibEnvShift = 2, vibAmpMask = 0x3000, vibAmpShift = 12, fadeOutMask = 0xFFF, }; uint8le shadowInstr; // Shadow Instrument. If non-zero, the value=the shadowed inst. uint16le vibampFadeout; // Vib.Amplify + Volume fadeout in one variable! uint16le envFlags; // See EnvelopeFlags void ApplyFlags(InstrumentEnvelope &mptEnv, EnvelopeFlags shift) const { const int flags = envFlags >> (shift * 3); mptEnv.dwFlags.set(ENV_ENABLED, (flags & envEnabled) != 0); mptEnv.dwFlags.set(ENV_LOOP, (flags & envLoop) != 0); mptEnv.dwFlags.set(ENV_SUSTAIN, (flags & envSustain) != 0); // "Break envelope" should stop the envelope loop when encountering a note-off... We can only use the sustain loop to emulate this behaviour. if(!(flags & envSustain) && (flags & envLoop) != 0 && (flags & (1 << (9 - shift * 2))) != 0) { mptEnv.nSustainStart = mptEnv.nLoopStart; mptEnv.nSustainEnd = mptEnv.nLoopEnd; mptEnv.dwFlags.set(ENV_SUSTAIN); mptEnv.dwFlags.reset(ENV_LOOP); } } }; MPT_BINARY_STRUCT(AMS2Instrument, 5) // AMS2 Sample Header struct AMS2SampleHeader { enum SampleFlags { smpPacked = 0x03, smp16Bit = 0x04, smpLoop = 0x08, smpBidiLoop = 0x10, smpReverse = 0x40, }; uint32le length; uint32le loopStart; uint32le loopEnd; uint16le sampledRate; // Whyyyy? uint8le panFinetune; // High nibble = pan position, low nibble = finetune value uint16le c4speed; // Why is all of this so redundant? int8le relativeTone; // q.e.d. uint8le volume; // 0...127 uint8le flags; // See SampleFlags // Convert sample header to OpenMPT's internal format. void ConvertToMPT(ModSample &mptSmp) const { mptSmp.Initialize(); mptSmp.nLength = length; mptSmp.nLoopStart = std::min(loopStart, length); mptSmp.nLoopEnd = std::min(loopEnd, length); mptSmp.nC5Speed = c4speed * 2; if(c4speed == 0) { mptSmp.nC5Speed = 8363 * 2; } // Why, oh why, does this format need a c5speed and transpose/finetune at the same time... uint32 newC4speed = ModSample::TransposeToFrequency(relativeTone, MOD2XMFineTune(panFinetune & 0x0F)); mptSmp.nC5Speed = (mptSmp.nC5Speed * newC4speed) / 8363; mptSmp.nVolume = (std::min(volume.get(), uint8(127)) * 256 + 64) / 127; if(panFinetune & 0xF0) { mptSmp.nPan = (panFinetune & 0xF0); mptSmp.uFlags = CHN_PANNING; } if(flags & smp16Bit) mptSmp.uFlags.set(CHN_16BIT); if((flags & smpLoop) && mptSmp.nLoopStart < mptSmp.nLoopEnd) { mptSmp.uFlags.set(CHN_LOOP); if(flags & smpBidiLoop) mptSmp.uFlags.set(CHN_PINGPONGLOOP); if(flags & smpReverse) mptSmp.uFlags.set(CHN_REVERSE); } } }; MPT_BINARY_STRUCT(AMS2SampleHeader, 20) // AMS2 Song Description Header struct AMS2Description { uint32le packedLen; // Including header uint32le unpackedLen; uint8le packRoutine; // 01 uint8le preProcessing; // None! uint8le packingMethod; // RLE }; MPT_BINARY_STRUCT(AMS2Description, 11) static bool ValidateHeader(const AMS2FileHeader &fileHeader) { if(fileHeader.versionHigh != 2 || fileHeader.versionLow > 2) { return false; } return true; } static uint64 GetHeaderMinimumAdditionalSize(const AMS2FileHeader &fileHeader) { return 36u + sizeof(AMS2Description) + fileHeader.numIns * 2u + fileHeader.numOrds * 2u + fileHeader.numPats * 4u; } CSoundFile::ProbeResult CSoundFile::ProbeFileHeaderAMS2(MemoryFileReader file, const uint64 *pfilesize) { if(!file.CanRead(7)) { return ProbeWantMoreData; } if(!file.ReadMagic("AMShdr\x1A")) { return ProbeFailure; } if(!file.CanRead(1)) { return ProbeWantMoreData; } const uint8 songNameLength = file.ReadUint8(); if(!file.Skip(songNameLength)) { return ProbeWantMoreData; } AMS2FileHeader fileHeader; if(!file.ReadStruct(fileHeader)) { return ProbeWantMoreData; } if(!ValidateHeader(fileHeader)) { return ProbeFailure; } return ProbeAdditionalSize(file, pfilesize, GetHeaderMinimumAdditionalSize(fileHeader)); } bool CSoundFile::ReadAMS2(FileReader &file, ModLoadingFlags loadFlags) { file.Rewind(); if(!file.ReadMagic("AMShdr\x1A")) { return false; } std::string songName; if(!file.ReadSizedString(songName)) { return false; } AMS2FileHeader 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_AMS); m_songName = songName; m_nInstruments = fileHeader.numIns; m_nChannels = 32; SetupMODPanning(true); m_modFormat.formatName = U_("Velvet Studio"); m_modFormat.type = U_("ams"); m_modFormat.madeWithTracker = mpt::format(U_("Velvet Studio %1.%2"))(fileHeader.versionHigh.get(), mpt::ufmt::dec0<2>(fileHeader.versionLow.get())); m_modFormat.charset = mpt::Charset::CP437; uint16 headerFlags; if(fileHeader.versionLow >= 2) { uint16 tempo = std::max(uint16(32 << 8), file.ReadUint16LE()); // 8.8 tempo m_nDefaultTempo.SetRaw((tempo * TEMPO::fractFact) >> 8); m_nDefaultSpeed = std::max(uint8(1), file.ReadUint8()); file.Skip(3); // Default values for pattern editor headerFlags = file.ReadUint16LE(); } else { m_nDefaultTempo.Set(std::max(uint8(32), file.ReadUint8())); m_nDefaultSpeed = std::max(uint8(1), file.ReadUint8()); headerFlags = file.ReadUint8(); } m_SongFlags = SONG_ITCOMPATGXX | SONG_ITOLDEFFECTS | ((headerFlags & AMS2FileHeader::linearSlides) ? SONG_LINEARSLIDES : SongFlags(0)); // Instruments std::vector firstSample; // First sample of instrument std::vector sampleSettings; // Shadow sample map... Lo byte = Instrument, Hi byte, lo nibble = Sample index in instrument, Hi byte, hi nibble = Sample pack status enum { instrIndexMask = 0xFF, // Shadow instrument sampleIndexMask = 0x7F00, // Sample index in instrument sampleIndexShift = 8, packStatusMask = 0x8000, // If bit is set, sample is packed }; static_assert(MAX_INSTRUMENTS > 255); for(INSTRUMENTINDEX ins = 1; ins <= m_nInstruments; ins++) { ModInstrument *instrument = AllocateInstrument(ins); if(instrument == nullptr || !file.ReadSizedString(instrument->name)) { break; } uint8 numSamples = file.ReadUint8(); uint8 sampleAssignment[120]; MemsetZero(sampleAssignment); // Only really needed for v2.0, where the lowest and highest octave aren't cleared. if(numSamples == 0 || (fileHeader.versionLow > 0 && !file.ReadArray(sampleAssignment)) // v2.01+: 120 Notes || (fileHeader.versionLow == 0 && !file.ReadRaw(sampleAssignment + 12, 96))) // v2.0: 96 Notes { continue; } static_assert(mpt::array_sizeKeyboard)>::size >= std::size(sampleAssignment)); for(size_t i = 0; i < 120; i++) { instrument->Keyboard[i] = sampleAssignment[i] + GetNumSamples() + 1; } AMS2Envelope volEnv, panEnv, vibratoEnv; volEnv.ConvertToMPT(instrument->VolEnv, file); panEnv.ConvertToMPT(instrument->PanEnv, file); vibratoEnv.ConvertToMPT(instrument->PitchEnv, file); AMS2Instrument instrHeader; file.ReadStruct(instrHeader); instrument->nFadeOut = (instrHeader.vibampFadeout & AMS2Instrument::fadeOutMask); const int16 vibAmp = 1 << ((instrHeader.vibampFadeout & AMS2Instrument::vibAmpMask) >> AMS2Instrument::vibAmpShift); instrHeader.ApplyFlags(instrument->VolEnv, AMS2Instrument::volEnvShift); instrHeader.ApplyFlags(instrument->PanEnv, AMS2Instrument::panEnvShift); instrHeader.ApplyFlags(instrument->PitchEnv, AMS2Instrument::vibEnvShift); // Scale envelopes to correct range for(auto &p : instrument->VolEnv) { p.value = std::min(uint8(ENVELOPE_MAX), static_cast((p.value * ENVELOPE_MAX + 64u) / 127u)); } for(auto &p : instrument->PanEnv) { p.value = std::min(uint8(ENVELOPE_MAX), static_cast((p.value * ENVELOPE_MAX + 128u) / 255u)); } for(auto &p : instrument->PitchEnv) { #ifdef MODPLUG_TRACKER p.value = std::min(uint8(ENVELOPE_MAX), static_cast(32 + Util::muldivrfloor(static_cast(p.value - 128), vibAmp, 255))); #else // Try to keep as much precision as possible... divide by 8 since that's the highest possible vibAmp factor. p.value = static_cast(128 + Util::muldivrfloor(static_cast(p.value - 128), vibAmp, 8)); #endif } // Sample headers - we will have to read them even for shadow samples, and we will have to load them several times, // as it is possible that shadow samples use different sample settings like base frequency or panning. const SAMPLEINDEX firstSmp = GetNumSamples() + 1; for(SAMPLEINDEX smp = 0; smp < numSamples; smp++) { if(firstSmp + smp >= MAX_SAMPLES) { file.Skip(sizeof(AMS2SampleHeader)); break; } file.ReadSizedString(m_szNames[firstSmp + smp]); AMS2SampleHeader sampleHeader; file.ReadStruct(sampleHeader); sampleHeader.ConvertToMPT(Samples[firstSmp + smp]); uint16 settings = (instrHeader.shadowInstr & instrIndexMask) | ((smp << sampleIndexShift) & sampleIndexMask) | ((sampleHeader.flags & AMS2SampleHeader::smpPacked) ? packStatusMask : 0); sampleSettings.push_back(settings); } firstSample.push_back(firstSmp); m_nSamples = static_cast(std::min(MAX_SAMPLES - 1, GetNumSamples() + numSamples)); } // Text // Read composer name if(std::string composer; file.ReadSizedString(composer)) { m_songArtist = mpt::ToUnicode(mpt::Charset::CP437AMS2, composer); } // Channel names for(CHANNELINDEX chn = 0; chn < 32; chn++) { ChnSettings[chn].Reset(); file.ReadSizedString(ChnSettings[chn].szName); } // RLE-Packed description text AMS2Description descriptionHeader; if(!file.ReadStruct(descriptionHeader)) { return true; } if(descriptionHeader.packedLen > sizeof(descriptionHeader) && file.CanRead(descriptionHeader.packedLen - sizeof(descriptionHeader))) { const uint32 textLength = descriptionHeader.packedLen - static_cast(sizeof(descriptionHeader)); std::vector textIn; file.ReadVector(textIn, textLength); // In the best case, every byte triplet can decode to 255 bytes, which is a ratio of exactly 1:85 const uint32 maxLength = std::min(textLength, Util::MaxValueOfType(textLength) / 85u) * 85u; std::string textOut; textOut.reserve(std::min(maxLength, descriptionHeader.unpackedLen.get())); size_t readLen = 0; while(readLen < textLength) { uint8 c = textIn[readLen++]; if(c == 0xFF && textLength - readLen >= 2) { c = textIn[readLen++]; uint32 count = textIn[readLen++]; textOut.insert(textOut.end(), count, c); } else { textOut.push_back(c); } } textOut = mpt::ToCharset(mpt::Charset::CP437, mpt::Charset::CP437AMS2, textOut); // Packed text doesn't include any line breaks! m_songMessage.ReadFixedLineLength(mpt::byte_cast(textOut.c_str()), textOut.length(), 74, 0); } // Read Order List ReadOrderFromFile(Order(), file, fileHeader.numOrds); // Read Patterns if(loadFlags & loadPatternData) Patterns.ResizeArray(fileHeader.numPats); for(PATTERNINDEX pat = 0; pat < fileHeader.numPats && file.CanRead(4); pat++) { uint32 patLength = file.ReadUint32LE(); FileReader patternChunk = file.ReadChunk(patLength); if(loadFlags & loadPatternData) { const ROWINDEX numRows = patternChunk.ReadUint8() + 1; // We don't need to know the number of channels or commands. patternChunk.Skip(1); if(!Patterns.Insert(pat, numRows)) { continue; } char patternName[11]; if(patternChunk.ReadSizedString(patternName)) Patterns[pat].SetName(patternName); ReadAMSPattern(Patterns[pat], true, patternChunk); } } if(!(loadFlags & loadSampleData)) { return true; } // Read Samples for(SAMPLEINDEX smp = 0; smp < GetNumSamples(); smp++) { if((sampleSettings[smp] & instrIndexMask) == 0) { // Only load samples that aren't part of a shadow instrument SampleIO( (Samples[smp + 1].uFlags & CHN_16BIT) ? SampleIO::_16bit : SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, (sampleSettings[smp] & packStatusMask) ? SampleIO::AMS : SampleIO::signedPCM) .ReadSample(Samples[smp + 1], file); } } // Copy shadow samples for(SAMPLEINDEX smp = 0; smp < GetNumSamples(); smp++) { INSTRUMENTINDEX sourceInstr = (sampleSettings[smp] & instrIndexMask); if(sourceInstr == 0 || --sourceInstr >= firstSample.size()) { continue; } SAMPLEINDEX sourceSample = ((sampleSettings[smp] & sampleIndexMask) >> sampleIndexShift) + firstSample[sourceInstr]; if(sourceSample > GetNumSamples() || !Samples[sourceSample].HasSampleData()) { continue; } // Copy over original sample ModSample &sample = Samples[smp + 1]; ModSample &source = Samples[sourceSample]; sample.uFlags.set(CHN_16BIT, source.uFlags[CHN_16BIT]); sample.nLength = source.nLength; if(sample.AllocateSample()) { memcpy(sample.sampleb(), source.sampleb(), source.GetSampleSizeInBytes()); } } return true; } ///////////////////////////////////////////////////////////////////// // AMS Sample unpacking void AMSUnpack(const int8 * const source, size_t sourceSize, void * const dest, const size_t destSize, char packCharacter) { std::vector tempBuf(destSize, 0); size_t depackSize = destSize; // Unpack Loop { const int8 *in = source; int8 *out = tempBuf.data(); size_t i = sourceSize, j = destSize; while(i != 0 && j != 0) { int8 ch = *(in++); if(--i != 0 && ch == packCharacter) { uint8 repCount = *(in++); repCount = static_cast(std::min(static_cast(repCount), j)); if(--i != 0 && repCount) { ch = *(in++); i--; while(repCount-- != 0) { *(out++) = ch; j--; } } else { *(out++) = packCharacter; j--; } } else { *(out++) = ch; j--; } } // j should only be non-zero for truncated samples depackSize -= j; } // Bit Unpack Loop { int8 *out = tempBuf.data(); uint16 bitcount = 0x80; size_t k = 0; uint8 *dst = static_cast(dest); for(size_t i = 0; i < depackSize; i++) { uint8 al = *out++; uint16 dh = 0; for(uint16 count = 0; count < 8; count++) { uint16 bl = al & bitcount; bl = ((bl | (bl << 8)) >> ((dh + 8 - count) & 7)) & 0xFF; bitcount = ((bitcount | (bitcount << 8)) >> 1) & 0xFF; dst[k++] |= bl; if(k >= destSize) { k = 0; dh++; } } bitcount = ((bitcount | (bitcount << 8)) >> dh) & 0xFF; } } // Delta Unpack { int8 old = 0; int8 *out = static_cast(dest); for(size_t i = depackSize; i != 0; i--) { int pos = *reinterpret_cast(out); if(pos != 128 && (pos & 0x80) != 0) { pos = -(pos & 0x7F); } old -= static_cast(pos); *(out++) = old; } } } OPENMPT_NAMESPACE_END