winamp/Src/external_dependencies/openmpt-trunk/soundlib/pattern.cpp

644 lines
15 KiB
C++
Raw Normal View History

2024-09-24 13:54:57 +01:00
/*
* 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<ModCommand::PARAM>(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<EffectCommand>(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<VolumeCommand>(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<uint32>(pat.GetRowsPerBeat(), "RPB.");
ssb.WriteItem<uint32>(pat.GetRowsPerMeasure(), "RPM.");
}
if(pat.HasTempoSwing())
{
ssb.WriteItem<TempoSwing>(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<uint32>(rpb, "RPB.");
ssb.ReadItem<uint32>(rpm, "RPM.");
pat.SetSignature(rpb, rpm);
TempoSwing swing;
ssb.ReadItem<TempoSwing>(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<ModCommand> lastChnMC(chns);
for(ROWINDEX r = 0; r<rows; r++)
{
for(CHANNELINDEX c = 0; c<chns; c++)
{
const ModCommand m = *pat.GetpModCommand(r, c);
// Writing only commands not written in IT-pattern writing:
// For now this means only NOTE_PC and NOTE_PCS.
if(!m.IsPcNote())
continue;
uint8 diffmask = CreateDiffMask(lastChnMC[c], m);
uint8 chval = static_cast<uint8>(c+1);
if(diffmask != 0)
chval |= IT_bitmask_patternChanEnabled_c;
mpt::IO::WriteIntLE<uint8>(oStrm, chval);
if(diffmask)
{
lastChnMC[c] = m;
mpt::IO::WriteIntLE<uint8>(oStrm, diffmask);
if(diffmask & noteBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.note);
if(diffmask & instrBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.instr);
if(diffmask & volcmdBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.volcmd);
if(diffmask & volBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.vol);
if(diffmask & commandBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.command);
if(diffmask & effectParamBit) mpt::IO::WriteIntLE<uint8>(oStrm, m.param);
}
}
mpt::IO::WriteIntLE<uint8>(oStrm, 0); // Write end of row marker.
}
}
#define READITEM(itembit,id) \
if(diffmask & itembit) \
{ \
mpt::IO::ReadIntLE<uint8>(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<ModCommand> lastChnMC(chns);
ROWINDEX row = 0;
while(row < rows && iStrm.good())
{
uint8 t = 0;
mpt::IO::ReadIntLE<uint8>(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<uint8>(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<uint8>(iStrm, size);
iStrm.ignore(size);
}
}
}
#undef READITEM
OPENMPT_NAMESPACE_END