/* * Pattern.cpp * ----------- * Purpose: Module Pattern header class * 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 "pattern.h" #include "patternContainer.h" #include "../common/serialization_utils.h" #include "../common/version.h" #include "ITTools.h" #include "Sndfile.h" #include "mod_specifications.h" #include "mpt/io/io.hpp" #include "mpt/io/io_stdstream.hpp" OPENMPT_NAMESPACE_BEGIN CSoundFile& CPattern::GetSoundFile() { return m_rPatternContainer.GetSoundFile(); } const CSoundFile& CPattern::GetSoundFile() const { return m_rPatternContainer.GetSoundFile(); } CHANNELINDEX CPattern::GetNumChannels() const { return GetSoundFile().GetNumChannels(); } // Check if there is any note data on a given row. bool CPattern::IsEmptyRow(ROWINDEX row) const { if(m_ModCommands.empty() || !IsValidRow(row)) { return true; } PatternRow data = GetRow(row); for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++, data++) { if(!data->IsEmpty()) { return false; } } return true; } bool CPattern::SetSignature(const ROWINDEX rowsPerBeat, const ROWINDEX rowsPerMeasure) { if(rowsPerBeat < 1 || rowsPerBeat > GetSoundFile().GetModSpecifications().patternRowsMax || rowsPerMeasure < rowsPerBeat || rowsPerMeasure > GetSoundFile().GetModSpecifications().patternRowsMax) { return false; } m_RowsPerBeat = rowsPerBeat; m_RowsPerMeasure = rowsPerMeasure; return true; } // Add or remove rows from the pattern. bool CPattern::Resize(const ROWINDEX newRowCount, bool enforceFormatLimits, bool resizeAtEnd) { CSoundFile &sndFile = GetSoundFile(); if(newRowCount == m_Rows || newRowCount < 1 || newRowCount > MAX_PATTERN_ROWS) { return false; } if(enforceFormatLimits) { auto &specs = sndFile.GetModSpecifications(); if(newRowCount > specs.patternRowsMax || newRowCount < specs.patternRowsMin) return false; } try { size_t count = ((newRowCount > m_Rows) ? (newRowCount - m_Rows) : (m_Rows - newRowCount)) * GetNumChannels(); if(newRowCount > m_Rows) m_ModCommands.insert(resizeAtEnd ? m_ModCommands.end() : m_ModCommands.begin(), count, ModCommand::Empty()); else if(resizeAtEnd) m_ModCommands.erase(m_ModCommands.end() - count, m_ModCommands.end()); else m_ModCommands.erase(m_ModCommands.begin(), m_ModCommands.begin() + count); } catch(mpt::out_of_memory e) { mpt::delete_out_of_memory(e); return false; } m_Rows = newRowCount; return true; } void CPattern::ClearCommands() { std::fill(m_ModCommands.begin(), m_ModCommands.end(), ModCommand::Empty()); } bool CPattern::AllocatePattern(ROWINDEX rows) { size_t newSize = GetNumChannels() * rows; if(rows == 0) { return false; } else if(rows == GetNumRows() && m_ModCommands.size() == newSize) { // Re-use allocated memory ClearCommands(); return true; } else { // Do this in two steps in order to keep the old pattern data in case of OOM decltype(m_ModCommands) newPattern(newSize, ModCommand::Empty()); m_ModCommands = std::move(newPattern); } m_Rows = rows; return true; } void CPattern::Deallocate() { m_Rows = m_RowsPerBeat = m_RowsPerMeasure = 0; m_ModCommands.clear(); m_PatternName.clear(); } CPattern& CPattern::operator= (const CPattern &pat) { m_ModCommands = pat.m_ModCommands; m_Rows = pat.m_Rows; m_RowsPerBeat = pat.m_RowsPerBeat; m_RowsPerMeasure = pat.m_RowsPerMeasure; m_tempoSwing = pat.m_tempoSwing; m_PatternName = pat.m_PatternName; return *this; } bool CPattern::operator== (const CPattern &other) const { return GetNumRows() == other.GetNumRows() && GetNumChannels() == other.GetNumChannels() && GetOverrideSignature() == other.GetOverrideSignature() && GetRowsPerBeat() == other.GetRowsPerBeat() && GetRowsPerMeasure() == other.GetRowsPerMeasure() && GetTempoSwing() == other.GetTempoSwing() && m_ModCommands == other.m_ModCommands; } #ifdef MODPLUG_TRACKER bool CPattern::Expand() { const ROWINDEX newRows = m_Rows * 2; const CHANNELINDEX nChns = GetNumChannels(); if(m_ModCommands.empty() || newRows > GetSoundFile().GetModSpecifications().patternRowsMax) { return false; } decltype(m_ModCommands) newPattern; try { newPattern.assign(m_ModCommands.size() * 2, ModCommand::Empty()); } catch(mpt::out_of_memory e) { mpt::delete_out_of_memory(e); return false; } for(auto mSrc = m_ModCommands.begin(), mDst = newPattern.begin(); mSrc != m_ModCommands.end(); mSrc += nChns, mDst += 2 * nChns) { std::copy(mSrc, mSrc + nChns, mDst); } m_ModCommands = std::move(newPattern); m_Rows = newRows; return true; } bool CPattern::Shrink() { if (m_ModCommands.empty() || m_Rows < GetSoundFile().GetModSpecifications().patternRowsMin * 2) { return false; } m_Rows /= 2; const CHANNELINDEX nChns = GetNumChannels(); for(ROWINDEX y = 0; y < m_Rows; y++) { const PatternRow srcRow = GetRow(y * 2); const PatternRow nextSrcRow = GetRow(y * 2 + 1); PatternRow destRow = GetRow(y); for(CHANNELINDEX x = 0; x < nChns; x++) { const ModCommand &src = srcRow[x]; const ModCommand &srcNext = nextSrcRow[x]; ModCommand &dest = destRow[x]; dest = src; if(dest.note == NOTE_NONE && !dest.instr) { // Fill in data from next row if field is empty dest.note = srcNext.note; dest.instr = srcNext.instr; if(srcNext.volcmd != VOLCMD_NONE) { dest.volcmd = srcNext.volcmd; dest.vol = srcNext.vol; } if(dest.command == CMD_NONE) { dest.command = srcNext.command; dest.param = srcNext.param; } } } } m_ModCommands.resize(m_Rows * nChns); return true; } #endif // MODPLUG_TRACKER bool CPattern::SetName(const std::string &newName) { m_PatternName = newName; return true; } bool CPattern::SetName(const char *newName, size_t maxChars) { if(newName == nullptr || maxChars == 0) { return false; } const auto nameEnd = std::find(newName, newName + maxChars, '\0'); m_PatternName.assign(newName, nameEnd); return true; } // Write some kind of effect data to the pattern. Exact data to be written and write behaviour can be found in the EffectWriter object. bool CPattern::WriteEffect(EffectWriter &settings) { // First, reject invalid parameters. if(m_ModCommands.empty() || settings.m_row >= GetNumRows() || (settings.m_channel >= GetNumChannels() && settings.m_channel != CHANNELINDEX_INVALID)) { return false; } CHANNELINDEX scanChnMin = settings.m_channel, scanChnMax = settings.m_channel; // Scan all channels if(settings.m_channel == CHANNELINDEX_INVALID) { scanChnMin = 0; scanChnMax = GetNumChannels() - 1; } ModCommand * const baseCommand = GetpModCommand(settings.m_row, scanChnMin); ModCommand *m; // Scan channel(s) for same effect type - if an effect of the same type is already present, exit. if(!settings.m_allowMultiple) { m = baseCommand; for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) { if(!settings.m_isVolEffect && m->command == settings.m_command) return true; if(settings.m_isVolEffect && m->volcmd == settings.m_volcmd) return true; } } // Easy case: check if there's some space left to put the effect somewhere m = baseCommand; for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) { if(!settings.m_isVolEffect && m->command == CMD_NONE) { m->command = settings.m_command; m->param = settings.m_param; return true; } if(settings.m_isVolEffect && m->volcmd == VOLCMD_NONE) { m->volcmd = settings.m_volcmd; m->vol = settings.m_vol; return true; } } // Ok, apparently there's no space. If we haven't tried already, try to map it to the volume column or effect column instead. if(settings.m_retry) { const bool isS3M = (GetSoundFile().GetType() & MOD_TYPE_S3M); // Move some effects that also work in the volume column, so there's place for our new effect. if(!settings.m_isVolEffect) { m = baseCommand; for(CHANNELINDEX i = scanChnMin; i <= scanChnMax; i++, m++) { switch(m->command) { case CMD_VOLUME: if(!GetSoundFile().GetModSpecifications().HasVolCommand(VOLCMD_VOLUME)) { break; } m->volcmd = VOLCMD_VOLUME; m->vol = m->param; m->command = settings.m_command; m->param = settings.m_param; return true; case CMD_PANNING8: if(isS3M && m->param > 0x80) { break; } m->volcmd = VOLCMD_PANNING; m->command = settings.m_command; if(isS3M) m->vol = (m->param + 1u) / 2u; else m->vol = (m->param + 2u) / 4u; m->param = settings.m_param; return true; default: break; } } } // Let's try it again by writing into the "other" effect column. if(settings.m_isVolEffect) { // Convert volume effect to normal effect ModCommand::COMMAND newCommand = CMD_NONE; ModCommand::PARAM newParam = settings.m_vol; switch(settings.m_volcmd) { case VOLCMD_PANNING: newCommand = CMD_PANNING8; newParam = mpt::saturate_cast(settings.m_vol * (isS3M ? 2u : 4u)); break; case VOLCMD_VOLUME: newCommand = CMD_VOLUME; break; default: break; } if(newCommand != CMD_NONE) { settings.m_command = static_cast(newCommand); settings.m_param = newParam; settings.m_retry = false; } } else { // Convert normal effect to volume effect ModCommand::VOLCMD newVolCmd = VOLCMD_NONE; ModCommand::VOL newVol = settings.m_param; if(settings.m_command == CMD_PANNING8 && isS3M) { // This needs some manual fixing. if(settings.m_param <= 0x80) { // Can't have surround in volume column, only normal panning newVolCmd = VOLCMD_PANNING; newVol /= 2u; } } else { newVolCmd = settings.m_command; if(!ModCommand::ConvertVolEffect(newVolCmd, newVol, true)) { // No Success :( newVolCmd = VOLCMD_NONE; } } if(newVolCmd != CMD_NONE) { settings.m_volcmd = static_cast(newVolCmd); settings.m_vol = newVol; settings.m_retry = false; } } if(!settings.m_retry) { settings.m_isVolEffect = !settings.m_isVolEffect; if(WriteEffect(settings)) { return true; } } } // Try in the next row if possible (this may also happen if we already retried) if(settings.m_retryMode == EffectWriter::rmTryNextRow && settings.m_row + 1 < GetNumRows()) { settings.m_row++; settings.m_retry = true; return WriteEffect(settings); } else if(settings.m_retryMode == EffectWriter::rmTryPreviousRow && settings.m_row > 0) { settings.m_row--; settings.m_retry = true; return WriteEffect(settings); } return false; } //////////////////////////////////////////////////////////////////////// // // Pattern serialization functions // //////////////////////////////////////////////////////////////////////// enum maskbits { noteBit = (1 << 0), instrBit = (1 << 1), volcmdBit = (1 << 2), volBit = (1 << 3), commandBit = (1 << 4), effectParamBit = (1 << 5), extraData = (1 << 6) }; void WriteData(std::ostream& oStrm, const CPattern& pat); void ReadData(std::istream& iStrm, CPattern& pat, const size_t nSize = 0); void WriteModPattern(std::ostream& oStrm, const CPattern& pat) { srlztn::SsbWrite ssb(oStrm); ssb.BeginWrite(FileIdPattern, Version::Current().GetRawVersion()); ssb.WriteItem(pat, "data", &WriteData); // pattern time signature if(pat.GetOverrideSignature()) { ssb.WriteItem(pat.GetRowsPerBeat(), "RPB."); ssb.WriteItem(pat.GetRowsPerMeasure(), "RPM."); } if(pat.HasTempoSwing()) { ssb.WriteItem(pat.GetTempoSwing(), "SWNG", TempoSwing::Serialize); } ssb.FinishWrite(); } void ReadModPattern(std::istream& iStrm, CPattern& pat, const size_t) { srlztn::SsbRead ssb(iStrm); ssb.BeginRead(FileIdPattern, Version::Current().GetRawVersion()); if ((ssb.GetStatus() & srlztn::SNT_FAILURE) != 0) return; ssb.ReadItem(pat, "data", &ReadData); // pattern time signature uint32 rpb = 0, rpm = 0; ssb.ReadItem(rpb, "RPB."); ssb.ReadItem(rpm, "RPM."); pat.SetSignature(rpb, rpm); TempoSwing swing; ssb.ReadItem(swing, "SWNG", TempoSwing::Deserialize); if(!swing.empty()) swing.resize(pat.GetRowsPerBeat()); pat.SetTempoSwing(swing); } static uint8 CreateDiffMask(const ModCommand &chnMC, const ModCommand &newMC) { uint8 mask = 0; if(chnMC.note != newMC.note) mask |= noteBit; if(chnMC.instr != newMC.instr) mask |= instrBit; if(chnMC.volcmd != newMC.volcmd) mask |= volcmdBit; if(chnMC.vol != newMC.vol) mask |= volBit; if(chnMC.command != newMC.command) mask |= commandBit; if(chnMC.param != newMC.param) mask |= effectParamBit; return mask; } // Writes pattern data. Adapted from SaveIT. void WriteData(std::ostream& oStrm, const CPattern& pat) { if(!pat.IsValid()) return; const ROWINDEX rows = pat.GetNumRows(); const CHANNELINDEX chns = pat.GetNumChannels(); std::vector lastChnMC(chns); for(ROWINDEX r = 0; r(c+1); if(diffmask != 0) chval |= IT_bitmask_patternChanEnabled_c; mpt::IO::WriteIntLE(oStrm, chval); if(diffmask) { lastChnMC[c] = m; mpt::IO::WriteIntLE(oStrm, diffmask); if(diffmask & noteBit) mpt::IO::WriteIntLE(oStrm, m.note); if(diffmask & instrBit) mpt::IO::WriteIntLE(oStrm, m.instr); if(diffmask & volcmdBit) mpt::IO::WriteIntLE(oStrm, m.volcmd); if(diffmask & volBit) mpt::IO::WriteIntLE(oStrm, m.vol); if(diffmask & commandBit) mpt::IO::WriteIntLE(oStrm, m.command); if(diffmask & effectParamBit) mpt::IO::WriteIntLE(oStrm, m.param); } } mpt::IO::WriteIntLE(oStrm, 0); // Write end of row marker. } } #define READITEM(itembit,id) \ if(diffmask & itembit) \ { \ mpt::IO::ReadIntLE(iStrm, temp); \ if(ch < chns) \ lastChnMC[ch].id = temp; \ } \ if(ch < chns) \ m.id = lastChnMC[ch].id; void ReadData(std::istream& iStrm, CPattern& pat, const size_t) { if (!pat.IsValid()) // Expecting patterns to be allocated and resized properly. return; const CHANNELINDEX chns = pat.GetNumChannels(); const ROWINDEX rows = pat.GetNumRows(); std::vector lastChnMC(chns); ROWINDEX row = 0; while(row < rows && iStrm.good()) { uint8 t = 0; mpt::IO::ReadIntLE(iStrm, t); if(t == 0) { row++; continue; } CHANNELINDEX ch = (t & IT_bitmask_patternChanField_c); if(ch > 0) ch--; uint8 diffmask = 0; if((t & IT_bitmask_patternChanEnabled_c) != 0) mpt::IO::ReadIntLE(iStrm, diffmask); uint8 temp = 0; ModCommand dummy = ModCommand::Empty(); ModCommand& m = (ch < chns) ? *pat.GetpModCommand(row, ch) : dummy; READITEM(noteBit, note); READITEM(instrBit, instr); READITEM(volcmdBit, volcmd); READITEM(volBit, vol); READITEM(commandBit, command); READITEM(effectParamBit, param); if(diffmask & extraData) { //Ignore additional data. uint8 size; mpt::IO::ReadIntLE(iStrm, size); iStrm.ignore(size); } } } #undef READITEM OPENMPT_NAMESPACE_END