/* * ModSequence.cpp * --------------- * Purpose: Order and sequence handling. * 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 "ModSequence.h" #include "Sndfile.h" #include "mod_specifications.h" #include "../common/version.h" #include "../common/serialization_utils.h" OPENMPT_NAMESPACE_BEGIN #define str_SequenceTruncationNote ("Module has sequence of length %1; it will be truncated to maximum supported length, %2.") ModSequence::ModSequence(CSoundFile &sndFile) : m_sndFile(sndFile) { } ModSequence& ModSequence::operator=(const ModSequence &other) { MPT_ASSERT(&other.m_sndFile == &m_sndFile); if(&other == this) return *this; std::vector::assign(other.begin(), other.end()); m_name = other.m_name; m_restartPos = other.m_restartPos; return *this; } bool ModSequence::operator== (const ModSequence &other) const { return static_cast &>(*this) == other && m_name == other.m_name && m_restartPos == other.m_restartPos; } bool ModSequence::NeedsExtraDatafield() const { return (m_sndFile.GetType() == MOD_TYPE_MPT && m_sndFile.Patterns.GetNumPatterns() > 0xFD); } void ModSequence::AdjustToNewModType(const MODTYPE oldtype) { auto &specs = m_sndFile.GetModSpecifications(); if(oldtype != MOD_TYPE_NONE) { // If not supported, remove "+++" separator order items. if(!specs.hasIgnoreIndex) { RemovePattern(GetIgnoreIndex()); } // If not supported, remove "---" items between patterns. if(!specs.hasStopIndex) { RemovePattern(GetInvalidPatIndex()); } } //Resize orderlist if needed. if(specs.ordersMax < size()) { // Order list too long? Remove "unnecessary" order items first. if(oldtype != MOD_TYPE_NONE && specs.ordersMax < GetLengthTailTrimmed()) { erase(std::remove_if(begin(), end(), [&] (PATTERNINDEX pat) { return !m_sndFile.Patterns.IsValidPat(pat); }), end()); if(GetLengthTailTrimmed() > specs.ordersMax) { m_sndFile.AddToLog("WARNING: Order list has been trimmed!"); } } resize(specs.ordersMax); } } ORDERINDEX ModSequence::GetLengthTailTrimmed() const { if(empty()) return 0; auto last = std::find_if(rbegin(), rend(), [] (PATTERNINDEX pat) { return pat != GetInvalidPatIndex(); }); return static_cast(std::distance(begin(), last.base())); } ORDERINDEX ModSequence::GetLengthFirstEmpty() const { return static_cast(std::distance(begin(), std::find(begin(), end(), GetInvalidPatIndex()))); } ORDERINDEX ModSequence::GetNextOrderIgnoringSkips(const ORDERINDEX start) const { if(empty()) return 0; auto length = GetLength(); ORDERINDEX next = std::min(ORDERINDEX(length - 1), ORDERINDEX(start + 1)); while(next + 1 < length && at(next) == GetIgnoreIndex()) next++; return next; } ORDERINDEX ModSequence::GetPreviousOrderIgnoringSkips(const ORDERINDEX start) const { const ORDERINDEX last = GetLastIndex(); if(start == 0 || last == 0) return 0; ORDERINDEX prev = std::min(ORDERINDEX(start - 1), last); while(prev > 0 && at(prev) == GetIgnoreIndex()) prev--; return prev; } void ModSequence::Remove(ORDERINDEX posBegin, ORDERINDEX posEnd) { if(posEnd < posBegin || posEnd >= size()) return; erase(begin() + posBegin, begin() + posEnd + 1); } // Remove all references to a given pattern index from the order list. Jump commands are updated accordingly. void ModSequence::RemovePattern(PATTERNINDEX pat) { // First, calculate the offset that needs to be applied to jump commands const ORDERINDEX orderLength = GetLengthTailTrimmed(); std::vector newPosition(orderLength); ORDERINDEX maxJump = 0; for(ORDERINDEX i = 0; i < orderLength; i++) { newPosition[i] = i - maxJump; if(at(i) == pat) { maxJump++; } } if(!maxJump) { return; } erase(std::remove(begin(), end(), pat), end()); // Only apply to patterns actually found in this sequence for(auto p : *this) if(m_sndFile.Patterns.IsValidPat(p)) { for(auto &m : m_sndFile.Patterns[p]) { if(m.command == CMD_POSITIONJUMP && m.param < newPosition.size()) { m.param = static_cast(newPosition[m.param]); } } } if(m_restartPos < newPosition.size()) { m_restartPos = newPosition[m_restartPos]; } } void ModSequence::assign(ORDERINDEX newSize, PATTERNINDEX pat) { LimitMax(newSize, m_sndFile.GetModSpecifications().ordersMax); std::vector::assign(newSize, pat); } ORDERINDEX ModSequence::insert(ORDERINDEX pos, ORDERINDEX count, PATTERNINDEX fill) { const auto ordersMax = m_sndFile.GetModSpecifications().ordersMax; if(pos >= ordersMax || GetLengthTailTrimmed() >= ordersMax || count == 0) return 0; // Limit number of orders to be inserted so that we don't exceed the format limit. LimitMax(count, static_cast(ordersMax - pos)); reserve(std::max(pos, GetLength()) + count); // Inserting past the end of the container? if(pos > size()) resize(pos); std::vector::insert(begin() + pos, count, fill); // Did we overgrow? Remove patterns at end. if(size() > ordersMax) resize(ordersMax); return count; } bool ModSequence::IsValidPat(ORDERINDEX ord) const { if(ord < size()) return m_sndFile.Patterns.IsValidPat(at(ord)); return false; } ORDERINDEX ModSequence::FindOrder(PATTERNINDEX pat, ORDERINDEX startSearchAt, bool searchForward) const { const ORDERINDEX length = GetLength(); if(startSearchAt >= length) return ORDERINDEX_INVALID; ORDERINDEX ord = startSearchAt; for(ORDERINDEX p = 0; p < length; p++) { if(at(ord) == pat) { return ord; } if(searchForward) { if(++ord >= length) ord = 0; } else { if(ord-- == 0) ord = length - 1; } } return ORDERINDEX_INVALID; } PATTERNINDEX ModSequence::EnsureUnique(ORDERINDEX ord) { PATTERNINDEX pat = at(ord); if(!IsValidPat(ord)) return pat; for(const auto &sequence : m_sndFile.Order) { ORDERINDEX ords = sequence.GetLength(); for(ORDERINDEX o = 0; o < ords; o++) { if(sequence[o] == pat && (o != ord || &sequence != this)) { // Found duplicate usage. PATTERNINDEX newPat = m_sndFile.Patterns.Duplicate(pat); if(newPat != PATTERNINDEX_INVALID) { at(ord) = newPat; return newPat; } } } } return pat; } ///////////////////////////////////// // ModSequenceSet ///////////////////////////////////// ModSequenceSet::ModSequenceSet(CSoundFile &sndFile) : m_sndFile(sndFile) { Initialize(); } void ModSequenceSet::Initialize() { m_currentSeq = 0; m_Sequences.assign(1, ModSequence(m_sndFile)); } void ModSequenceSet::SetSequence(SEQUENCEINDEX n) { if(n < m_Sequences.size()) m_currentSeq = n; } SEQUENCEINDEX ModSequenceSet::AddSequence() { if(GetNumSequences() >= MAX_SEQUENCES) return SEQUENCEINDEX_INVALID; m_Sequences.push_back(ModSequence{m_sndFile}); SetSequence(GetNumSequences() - 1); return GetNumSequences() - 1; } void ModSequenceSet::RemoveSequence(SEQUENCEINDEX i) { // Do nothing if index is invalid or if there's only one sequence left. if(i >= m_Sequences.size() || m_Sequences.size() <= 1) return; m_Sequences.erase(m_Sequences.begin() + i); if(i < m_currentSeq || m_currentSeq >= GetNumSequences()) m_currentSeq--; } #ifdef MODPLUG_TRACKER bool ModSequenceSet::Rearrange(const std::vector &newOrder) { if(newOrder.empty() || newOrder.size() > MAX_SEQUENCES) return false; const auto oldSequences = std::move(m_Sequences); m_Sequences.assign(newOrder.size(), ModSequence{m_sndFile}); for(size_t i = 0; i < newOrder.size(); i++) { if(newOrder[i] < oldSequences.size()) m_Sequences[i] = oldSequences[newOrder[i]]; } if(m_currentSeq > m_Sequences.size()) m_currentSeq = GetNumSequences() - 1u; return true; } void ModSequenceSet::OnModTypeChanged(MODTYPE oldType) { for(auto &seq : m_Sequences) { seq.AdjustToNewModType(oldType); } if(m_sndFile.GetModSpecifications(oldType).sequencesMax > 1 && m_sndFile.GetModSpecifications().sequencesMax <= 1) MergeSequences(); } bool ModSequenceSet::CanSplitSubsongs() const { return GetNumSequences() == 1 && m_sndFile.GetModSpecifications().sequencesMax > 1 && m_Sequences[0].HasSubsongs(); } bool ModSequenceSet::SplitSubsongsToMultipleSequences() { if(!CanSplitSubsongs()) return false; bool modified = false; const ORDERINDEX length = m_Sequences[0].GetLengthTailTrimmed(); for(ORDERINDEX ord = 0; ord < length; ord++) { // End of subsong? if(!m_Sequences[0].IsValidPat(ord) && m_Sequences[0][ord] != GetIgnoreIndex()) { // Remove all separator patterns between current and next subsong first while(ord < length && !m_sndFile.Patterns.IsValidPat(m_Sequences[0][ord])) { m_Sequences[0][ord] = GetInvalidPatIndex(); ord++; modified = true; } if(ord >= length) break; const SEQUENCEINDEX newSeq = AddSequence(); if(newSeq == SEQUENCEINDEX_INVALID) break; const ORDERINDEX startOrd = ord; m_Sequences[newSeq].reserve(length - startOrd); modified = true; // Now, move all following orders to the new sequence while(ord < length && m_Sequences[0][ord] != GetInvalidPatIndex()) { PATTERNINDEX copyPat = m_Sequences[0][ord]; m_Sequences[newSeq].push_back(copyPat); m_Sequences[0][ord] = GetInvalidPatIndex(); ord++; // Is this a valid pattern? adjust pattern jump commands, if necessary. if(m_sndFile.Patterns.IsValidPat(copyPat)) { for(auto &m : m_sndFile.Patterns[copyPat]) { if(m.command == CMD_POSITIONJUMP && m.param >= startOrd) { m.param = static_cast(m.param - startOrd); } } } } ord--; } } SetSequence(0); return modified; } // Convert the sequence's restart position information to a pattern command. bool ModSequenceSet::RestartPosToPattern(SEQUENCEINDEX seq) { bool result = false; auto length = m_sndFile.GetLength(eNoAdjust, GetLengthTarget(true).StartPos(seq, 0, 0)); ModSequence &order = m_Sequences[seq]; for(const auto &subSong : length) { if(subSong.endOrder != ORDERINDEX_INVALID && subSong.endRow != ROWINDEX_INVALID) { if(Util::TypeCanHoldValue(order.GetRestartPos())) { PATTERNINDEX writePat = order.EnsureUnique(subSong.endOrder); result = m_sndFile.Patterns[writePat].WriteEffect( EffectWriter(CMD_POSITIONJUMP, static_cast(order.GetRestartPos())).Row(subSong.endRow).RetryNextRow()); } else { result = false; } } } order.SetRestartPos(0); return result; } bool ModSequenceSet::MergeSequences() { if(GetNumSequences() <= 1) return false; ModSequence &firstSeq = m_Sequences[0]; firstSeq.resize(firstSeq.GetLengthTailTrimmed()); std::vector patternsFixed(m_sndFile.Patterns.Size(), SEQUENCEINDEX_INVALID); // pattern fixed by other sequence already? // Mark patterns handled in first sequence for(auto pat : firstSeq) { if(m_sndFile.Patterns.IsValidPat(pat)) patternsFixed[pat] = 0; } for(SEQUENCEINDEX seqNum = 1; seqNum < GetNumSequences(); seqNum++) { ModSequence &seq = m_Sequences[seqNum]; const ORDERINDEX firstOrder = firstSeq.GetLength() + 1; // +1 for separator item const ORDERINDEX lengthTrimmed = seq.GetLengthTailTrimmed(); if(firstOrder + lengthTrimmed > m_sndFile.GetModSpecifications().ordersMax) { m_sndFile.AddToLog(mpt::format("WARNING: Cannot merge Sequence %1 (too long!)")(seqNum + 1)); continue; } firstSeq.reserve(firstOrder + lengthTrimmed); firstSeq.push_back(); // Separator item RestartPosToPattern(seqNum); for(ORDERINDEX ord = 0; ord < lengthTrimmed; ord++) { PATTERNINDEX pat = seq[ord]; firstSeq.push_back(pat); // Try to fix pattern jump commands if(!m_sndFile.Patterns.IsValidPat(pat)) continue; auto m = m_sndFile.Patterns[pat].begin(); for(size_t len = 0; len < m_sndFile.Patterns[pat].GetNumRows() * m_sndFile.m_nChannels; m++, len++) { if(m->command == CMD_POSITIONJUMP) { if(patternsFixed[pat] != SEQUENCEINDEX_INVALID && patternsFixed[pat] != seqNum) { // Oops, some other sequence uses this pattern already. const PATTERNINDEX newPat = m_sndFile.Patterns.Duplicate(pat, true); if(newPat != PATTERNINDEX_INVALID) { // Could create new pattern - copy data over and continue from here. firstSeq[firstOrder + ord] = newPat; m = m_sndFile.Patterns[newPat].begin() + len; if(newPat >= patternsFixed.size()) patternsFixed.resize(newPat + 1, SEQUENCEINDEX_INVALID); pat = newPat; } else { // Cannot create new pattern: notify the user m_sndFile.AddToLog(mpt::format("CONFLICT: Pattern break commands in Pattern %1 might be broken since it has been used in several sequences!")(pat)); } } m->param = static_cast(m->param + firstOrder); patternsFixed[pat] = seqNum; } } } } m_Sequences.erase(m_Sequences.begin() + 1, m_Sequences.end()); return true; } // Check if a playback position is currently locked (inaccessible) bool ModSequence::IsPositionLocked(ORDERINDEX position) const { return(m_sndFile.m_lockOrderStart != ORDERINDEX_INVALID && (position < m_sndFile.m_lockOrderStart || position > m_sndFile.m_lockOrderEnd)); } bool ModSequence::HasSubsongs() const { const auto endPat = begin() + GetLengthTailTrimmed(); return std::find_if(begin(), endPat, [&](PATTERNINDEX pat) { return pat != GetIgnoreIndex() && !m_sndFile.Patterns.IsValidPat(pat); }) != endPat; } #endif // MODPLUG_TRACKER ///////////////////////////////////// // Read/Write ///////////////////////////////////// #ifndef MODPLUG_NO_FILESAVE size_t ModSequence::WriteAsByte(std::ostream &f, const ORDERINDEX count, uint8 stopIndex, uint8 ignoreIndex) const { const size_t limit = std::min(count, GetLength()); for(size_t i = 0; i < limit; i++) { const PATTERNINDEX pat = at(i); uint8 temp = static_cast(pat); if(pat == GetInvalidPatIndex()) temp = stopIndex; else if(pat == GetIgnoreIndex() || pat > 0xFF) temp = ignoreIndex; mpt::IO::WriteIntLE(f, temp); } // Fill non-existing order items with stop indices for(size_t i = limit; i < count; i++) { mpt::IO::WriteIntLE(f, stopIndex); } return count; //Returns the number of bytes written. } #endif // MODPLUG_NO_FILESAVE void ReadModSequenceOld(std::istream& iStrm, ModSequenceSet& seq, const size_t) { uint16 size; mpt::IO::ReadIntLE(iStrm, size); if(size > ModSpecs::mptm.ordersMax) { seq.m_sndFile.AddToLog(mpt::format(str_SequenceTruncationNote)(size, ModSpecs::mptm.ordersMax)); size = ModSpecs::mptm.ordersMax; } seq(0).resize(size); for(auto &pat : seq(0)) { uint16 temp; mpt::IO::ReadIntLE(iStrm, temp); pat = temp; } } #ifndef MODPLUG_NO_FILESAVE void WriteModSequenceOld(std::ostream& oStrm, const ModSequenceSet& seq) { const uint16 size = seq().GetLength(); mpt::IO::WriteIntLE(oStrm, size); for(auto pat : seq()) { mpt::IO::WriteIntLE(oStrm, static_cast(pat)); } } #endif // MODPLUG_NO_FILESAVE #ifndef MODPLUG_NO_FILESAVE void WriteModSequence(std::ostream& oStrm, const ModSequence& seq) { srlztn::SsbWrite ssb(oStrm); ssb.BeginWrite(FileIdSequence, Version::Current().GetRawVersion()); int8 useUTF8 = 1; ssb.WriteItem(useUTF8, "u"); ssb.WriteItem(mpt::ToCharset(mpt::Charset::UTF8, seq.GetName()), "n"); const uint16 length = seq.GetLengthTailTrimmed(); ssb.WriteItem(length, "l"); ssb.WriteItem(seq, "a", srlztn::VectorWriter(length)); if(seq.GetRestartPos() > 0) ssb.WriteItem(seq.GetRestartPos(), "r"); ssb.FinishWrite(); } #endif // MODPLUG_NO_FILESAVE void ReadModSequence(std::istream& iStrm, ModSequence& seq, const size_t, mpt::Charset defaultCharset) { srlztn::SsbRead ssb(iStrm); ssb.BeginRead(FileIdSequence, Version::Current().GetRawVersion()); if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) return; int8 useUTF8 = 0; ssb.ReadItem(useUTF8, "u"); std::string str; ssb.ReadItem(str, "n"); seq.SetName(mpt::ToUnicode(useUTF8 ? mpt::Charset::UTF8 : defaultCharset, str)); ORDERINDEX nSize = 0; ssb.ReadItem(nSize, "l"); LimitMax(nSize, ModSpecs::mptm.ordersMax); ssb.ReadItem(seq, "a", srlztn::VectorReader(nSize)); ORDERINDEX restartPos = ORDERINDEX_INVALID; if(ssb.ReadItem(restartPos, "r") != srlztn::SsbRead::EntryNotFound && restartPos < nSize) seq.SetRestartPos(restartPos); } #ifndef MODPLUG_NO_FILESAVE void WriteModSequences(std::ostream& oStrm, const ModSequenceSet& seq) { srlztn::SsbWrite ssb(oStrm); ssb.BeginWrite(FileIdSequences, Version::Current().GetRawVersion()); const uint8 nSeqs = seq.GetNumSequences(); const uint8 nCurrent = seq.GetCurrentSequenceIndex(); ssb.WriteItem(nSeqs, "n"); ssb.WriteItem(nCurrent, "c"); for(uint8 i = 0; i < nSeqs; i++) { ssb.WriteItem(seq(i), srlztn::ID::FromInt(i), &WriteModSequence); } ssb.FinishWrite(); } #endif // MODPLUG_NO_FILESAVE void ReadModSequences(std::istream& iStrm, ModSequenceSet& seq, const size_t, mpt::Charset defaultCharset) { srlztn::SsbRead ssb(iStrm); ssb.BeginRead(FileIdSequences, Version::Current().GetRawVersion()); if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) return; SEQUENCEINDEX seqs = 0; uint8 currentSeq = 0; ssb.ReadItem(seqs, "n"); if (seqs == 0) return; LimitMax(seqs, MAX_SEQUENCES); ssb.ReadItem(currentSeq, "c"); if (seq.GetNumSequences() < seqs) seq.m_Sequences.resize(seqs, ModSequence(seq.m_sndFile)); // There used to be only one restart position for all sequences ORDERINDEX legacyRestartPos = seq(0).GetRestartPos(); for(SEQUENCEINDEX i = 0; i < seqs; i++) { seq(i).SetRestartPos(legacyRestartPos); ssb.ReadItem(seq(i), srlztn::ID::FromInt(i), [defaultCharset](std::istream &iStrm, ModSequence &seq, std::size_t dummy) { return ReadModSequence(iStrm, seq, dummy, defaultCharset); }); } seq.m_currentSeq = (currentSeq < seq.GetNumSequences()) ? currentSeq : 0; } OPENMPT_NAMESPACE_END