/* * RowVisitor.cpp * -------------- * Purpose: Class for managing which rows of a song has already been visited. Useful for detecting backwards jumps, loops, etc. * Notes : The class keeps track of rows that have been visited by the player before. * This way, we can tell when the module starts to loop, i.e. we can determine the song length, * or find out that a given point of the module can never be reached. * * Specific implementations: * * Length detection code: * As the ModPlug engine already deals with pattern loops sufficiently (though not always correctly), * there's no problem with (infinite) pattern loops in this code. * * Normal player code: * Bear in mind that rows inside pattern loops should only be evaluated once, or else the algorithm will cancel too early! * So in that case, the pattern loop rows have to be reset when looping back. * Authors: OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #include "stdafx.h" #include "Sndfile.h" #include "RowVisitor.h" OPENMPT_NAMESPACE_BEGIN RowVisitor::RowVisitor(const CSoundFile &sf, SEQUENCEINDEX sequence) : m_sndFile(sf) , m_currentOrder(0) , m_sequence(sequence) { Initialize(true); } void RowVisitor::MoveVisitedRowsFrom(RowVisitor &other) { m_visitedRows = std::move(other.m_visitedRows); } const ModSequence &RowVisitor::Order() const { if(m_sequence >= m_sndFile.Order.GetNumSequences()) return m_sndFile.Order(); else return m_sndFile.Order(m_sequence); } // Resize / Clear the row vector. // If reset is true, the vector is not only resized to the required dimensions, but also completely cleared (i.e. all visited rows are unset). void RowVisitor::Initialize(bool reset) { auto &order = Order(); const ORDERINDEX endOrder = order.GetLengthTailTrimmed(); m_visitedRows.resize(endOrder); if(reset) { m_visitOrder.clear(); // Pre-allocate maximum amount of memory most likely needed for keeping track of visited rows in a pattern if(m_visitOrder.capacity() < MAX_PATTERN_ROWS) { ROWINDEX maxRows = 0; for(const auto &pat :m_sndFile.Patterns) { maxRows = std::max(maxRows, pat.GetNumRows()); } m_visitOrder.reserve(maxRows); } } for(ORDERINDEX ord = 0; ord < endOrder; ord++) { auto &row = m_visitedRows[ord]; const size_t size = GetVisitedRowsVectorSize(order[ord]); if(reset) { // If we want to reset the vectors completely, we overwrite existing items with false. row.assign(size, false); } else { row.resize(size, false); } } } // (Un)sets a given row as visited. // order, row - which row should be (un)set // If visited is true, the row will be set as visited. void RowVisitor::SetVisited(ORDERINDEX ord, ROWINDEX row, bool visited) { auto &order = Order(); if(ord >= order.size() || row >= GetVisitedRowsVectorSize(order[ord])) { return; } // The module might have been edited in the meantime - so we have to extend this a bit. if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size()) { Initialize(false); // If it's still past the end of the vector, this means that ord >= order.GetLengthTailTrimmed(), i.e. we are trying to play an empty order. if(ord >= m_visitedRows.size()) { return; } } m_visitedRows[ord][row] = visited; if(visited) { AddVisitedRow(ord, row); } } // Returns whether a given row has been visited yet. // If autoSet is true, the queried row will automatically be marked as visited. // Use this parameter instead of consecutive IsRowVisited / SetRowVisited calls. bool RowVisitor::IsVisited(ORDERINDEX ord, ROWINDEX row, bool autoSet) { if(ord >= Order().size()) { return false; } // The row slot for this row has not been assigned yet - Just return false, as this means that the program has not played the row yet. if(ord >= m_visitedRows.size() || row >= m_visitedRows[ord].size()) { if(autoSet) { SetVisited(ord, row, true); } return false; } if(m_visitedRows[ord][row]) { // We visited this row already - this module must be looping. return true; } else if(autoSet) { m_visitedRows[ord][row] = true; AddVisitedRow(ord, row); } return false; } // Get the needed vector size for pattern nPat. size_t RowVisitor::GetVisitedRowsVectorSize(PATTERNINDEX pattern) const { if(m_sndFile.Patterns.IsValidPat(pattern)) { return static_cast(m_sndFile.Patterns[pattern].GetNumRows()); } else { // Invalid patterns consist of a "fake" row. return 1; } } // Find the first row that has not been played yet. // The order and row is stored in the order and row variables on success, on failure they contain invalid values. // If onlyUnplayedPatterns is true (default), only completely unplayed patterns are considered, otherwise a song can start anywhere. // Function returns true on success. bool RowVisitor::GetFirstUnvisitedRow(ORDERINDEX &ord, ROWINDEX &row, bool onlyUnplayedPatterns) const { const auto &order = Order(); const ORDERINDEX endOrder = order.GetLengthTailTrimmed(); for(ord = 0; ord < endOrder; ord++) { const PATTERNINDEX pattern = order[ord]; if(!m_sndFile.Patterns.IsValidPat(pattern)) { continue; } if(ord >= m_visitedRows.size()) { // Not yet initialized => unvisited row = 0; return true; } const auto &visitedRows = m_visitedRows[ord]; auto foundRow = std::find(visitedRows.begin(), visitedRows.end(), onlyUnplayedPatterns); if(onlyUnplayedPatterns && foundRow == visitedRows.end()) { // No row of this pattern has been played yet. row = 0; return true; } else if(!onlyUnplayedPatterns) { // Return the first unplayed row in this pattern if(foundRow != visitedRows.end()) { row = static_cast(std::distance(visitedRows.begin(), foundRow)); return true; } if(visitedRows.size() < m_sndFile.Patterns[pattern].GetNumRows()) { // History is not fully initialized row = static_cast(visitedRows.size()); return true; } } } // Didn't find anything :( ord = ORDERINDEX_INVALID; row = ROWINDEX_INVALID; return false; } // Set all rows of a previous pattern loop as unvisited. void RowVisitor::ResetPatternLoop(ORDERINDEX ord, ROWINDEX startRow) { MPT_ASSERT(ord == m_currentOrder); // Shouldn't trigger, unless we're jumping around in the GUI during a pattern loop. // Unvisit all rows that are in the visited row buffer, until we hit the start row for this pattern loop. ROWINDEX row = ROWINDEX_INVALID; for(auto iter = m_visitOrder.crbegin(); iter != m_visitOrder.crend() && row != startRow; iter++) { row = *iter; Unvisit(ord, row); } } // Add a row to the visited row memory for this pattern. void RowVisitor::AddVisitedRow(ORDERINDEX ord, ROWINDEX row) { if(ord != m_currentOrder) { // We're in a new pattern! Forget about which rows we previously visited... m_visitOrder.clear(); m_currentOrder = ord; } if(m_visitOrder.empty()) { m_visitOrder.reserve(GetVisitedRowsVectorSize(Order()[ord])); } // And now add the played row to our memory. m_visitOrder.push_back(row); } // Returns the last visited row index of the current pattern, or ROWINDEX_INVALID if there is none. ROWINDEX RowVisitor::GetLastVisitedRow() const { if(m_visitOrder.empty()) return ROWINDEX_INVALID; return m_visitOrder.back(); } OPENMPT_NAMESPACE_END