| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383 | /* * ModEdit.cpp * ----------- * Purpose: Song (pattern, samples, instruments) editing functions * 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 "Mptrack.h"#include "Mainfrm.h"#include "Moddoc.h"#include "Clipboard.h"#include "dlg_misc.h"#include "Dlsbank.h"#include "../soundlib/modsmp_ctrl.h"#include "../soundlib/mod_specifications.h"#include "../soundlib/tuning.h"#include "../soundlib/OPL.h"#include "../common/misc_util.h"#include "../common/mptStringBuffer.h"#include "../common/mptFileIO.h"#include <sstream>// Plugin cloning#include "../soundlib/plugins/PluginManager.h"#include "../soundlib/plugins/PlugInterface.h"#include "VstPresets.h"#include "../common/FileReader.h"#include "mpt/io/io.hpp"#include "mpt/io/io_stdstream.hpp"OPENMPT_NAMESPACE_BEGIN// Change the number of channels.// Return true on success.bool CModDoc::ChangeNumChannels(CHANNELINDEX nNewChannels, const bool showCancelInRemoveDlg){	const CHANNELINDEX maxChans = m_SndFile.GetModSpecifications().channelsMax;	if(nNewChannels > maxChans)	{		CString error;		error.Format(_T("Error: Max number of channels for this file type is %u"), maxChans);		Reporting::Warning(error);		return false;	}	if(nNewChannels == GetNumChannels())		return false;	if(nNewChannels < GetNumChannels())	{		// Remove channels		CHANNELINDEX chnsToRemove = 0, maxRemoveCount = 0;		//nNewChannels = 0 means user can choose how many channels to remove		if(nNewChannels > 0)		{			chnsToRemove = GetNumChannels() - nNewChannels;			maxRemoveCount = chnsToRemove;		} else		{			chnsToRemove = 0;			maxRemoveCount = GetNumChannels();		}		CRemoveChannelsDlg rem(m_SndFile, chnsToRemove, showCancelInRemoveDlg);		CheckUsedChannels(rem.m_bKeepMask, maxRemoveCount);		if(rem.DoModal() != IDOK)			return false;		// Removing selected channels		return RemoveChannels(rem.m_bKeepMask, true);	} else	{		// Increasing number of channels		BeginWaitCursor();		std::vector<CHANNELINDEX> channels(nNewChannels, CHANNELINDEX_INVALID);		for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)		{			channels[chn] = chn;		}		bool success = (ReArrangeChannels(channels) == nNewChannels);		if(success)		{			SetModified();			UpdateAllViews(nullptr, UpdateHint().ModType());		}		EndWaitCursor();		return success;	}}// To remove all channels whose index corresponds to false in the keepMask vector.// Return true on success.bool CModDoc::RemoveChannels(const std::vector<bool> &keepMask, bool verbose){	CHANNELINDEX nRemainingChannels = 0;	//First calculating how many channels are to be left	for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)	{		if(keepMask[chn])			nRemainingChannels++;	}	if(nRemainingChannels == GetNumChannels() || nRemainingChannels < m_SndFile.GetModSpecifications().channelsMin)	{		if(verbose)		{			CString str;			if(nRemainingChannels == GetNumChannels())				str = _T("No channels chosen to be removed.");			else				str = _T("No removal done - channel number is already at minimum.");			Reporting::Information(str, _T("Remove Channels"));		}		return false;	}	BeginWaitCursor();	// Create new channel order, with only channels from m_bChnMask left.	std::vector<CHANNELINDEX> channels(nRemainingChannels, 0);	CHANNELINDEX i = 0;	for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++)	{		if(keepMask[chn])		{			channels[i++] = chn;		}	}	const bool success = (ReArrangeChannels(channels) == nRemainingChannels);	if(success)	{		SetModified();		UpdateAllViews(nullptr, UpdateHint().ModType());	}	EndWaitCursor();	return success;}// Base code for adding, removing, moving and duplicating channels. Returns new number of channels on success, CHANNELINDEX_INVALID otherwise.// The new channel vector can contain CHANNELINDEX_INVALID for adding new (empty) channels.CHANNELINDEX CModDoc::ReArrangeChannels(const std::vector<CHANNELINDEX> &newOrder, const bool createUndoPoint){	//newOrder[i] tells which current channel should be placed to i:th position in	//the new order, or if i is not an index of current channels, then new channel is	//added to position i. If index of some current channel is missing from the	//newOrder-vector, then the channel gets removed.	const CHANNELINDEX newNumChannels = static_cast<CHANNELINDEX>(newOrder.size()), oldNumChannels = GetNumChannels();	auto &specs = m_SndFile.GetModSpecifications();	if(newNumChannels > specs.channelsMax || newNumChannels < specs.channelsMin)	{		CString str;		str.Format(_T("Can't apply change: Number of channels should be between %u and %u."), specs.channelsMin, specs.channelsMax);		Reporting::Error(str, _T("Rearrange Channels"));		return CHANNELINDEX_INVALID;	}	if(createUndoPoint)	{		PrepareUndoForAllPatterns(true, "Rearrange Channels");	}	CriticalSection cs;	if(oldNumChannels == newNumChannels)	{		// Optimization with no pattern re-allocation		std::vector<ModCommand> oldRow(oldNumChannels);		for(auto &pat : m_SndFile.Patterns)		{			auto m = pat.begin();			for(ROWINDEX row = 0; row < pat.GetNumRows(); row++)			{				oldRow.assign(m, m + oldNumChannels);				for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++, m++)				{					if(newOrder[chn] < oldNumChannels)  // Case: getting old channel to the new channel order.						*m = oldRow[newOrder[chn]];					else						*m = ModCommand::Empty();				}			}		}	} else	{		// Create all patterns first so that we can exit cleanly in case of OOM		std::vector<std::vector<ModCommand>> newPatterns;		try		{			newPatterns.resize(m_SndFile.Patterns.Size());			for(PATTERNINDEX i = 0; i < m_SndFile.Patterns.Size(); i++)			{				newPatterns[i].resize(m_SndFile.Patterns[i].GetNumRows() * newNumChannels);			}		} catch(mpt::out_of_memory e)		{			mpt::delete_out_of_memory(e);			Reporting::Error("Out of memory!", "Rearrange Channels");			return oldNumChannels;		}		m_SndFile.m_nChannels = newNumChannels;		for(PATTERNINDEX i = 0; i < m_SndFile.Patterns.Size(); i++)		{			CPattern &pat = m_SndFile.Patterns[i];			if(pat.IsValid())			{				auto mNew = newPatterns[i].begin(), mOld = pat.begin();				for(ROWINDEX row = 0; row < pat.GetNumRows(); row++, mOld += oldNumChannels)				{					for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++, mNew++)					{						if(newOrder[chn] < oldNumChannels)  // Case: getting old channel to the new channel order.							*mNew = mOld[newOrder[chn]];					}				}				pat.SetData(std::move(newPatterns[i]));			}		}	}	// Reverse mapping: One of the new indices of an old channel	std::vector<CHANNELINDEX> newIndex(oldNumChannels, CHANNELINDEX_INVALID);	for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++)	{		if(newOrder[chn] < oldNumChannels)		{			newIndex[newOrder[chn]] = chn;		}	}	// Reassign NNA channels (note: if we increase the number of channels, the lowest-indexed NNA channels will still be lost)	const auto muteFlag = CSoundFile::GetChannelMuteFlag();	for(CHANNELINDEX chn = oldNumChannels; chn < MAX_CHANNELS; chn++)	{		auto &channel = m_SndFile.m_PlayState.Chn[chn];		CHANNELINDEX masterChn = channel.nMasterChn, newMasterChn;		if(masterChn > 0 && masterChn <= newIndex.size() && (newMasterChn = newIndex[masterChn - 1]) != CHANNELINDEX_INVALID)			channel.nMasterChn = newMasterChn + 1;		else			channel.Reset(ModChannel::resetTotal, m_SndFile, chn, muteFlag);	}	std::vector<ModChannel> chns(std::begin(m_SndFile.m_PlayState.Chn), std::begin(m_SndFile.m_PlayState.Chn) + oldNumChannels);	std::vector<ModChannelSettings> settings(std::begin(m_SndFile.ChnSettings), std::begin(m_SndFile.ChnSettings) + oldNumChannels);	std::vector<RecordGroup> recordStates(oldNumChannels);	auto chnMutePendings = m_SndFile.m_bChannelMuteTogglePending;	for(CHANNELINDEX chn = 0; chn < oldNumChannels; chn++)	{		recordStates[chn] = GetChannelRecordGroup(chn);	}	ReinitRecordState();	for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++)	{		CHANNELINDEX srcChn = newOrder[chn];		if(srcChn < oldNumChannels)		{			m_SndFile.ChnSettings[chn] = settings[srcChn];			m_SndFile.m_PlayState.Chn[chn] = chns[srcChn];			SetChannelRecordGroup(chn, recordStates[srcChn]);			m_SndFile.m_bChannelMuteTogglePending[chn] = chnMutePendings[srcChn];			if(m_SndFile.m_opl)				m_SndFile.m_opl->MoveChannel(srcChn, chn);		} else		{			m_SndFile.InitChannel(chn);			SetDefaultChannelColors(chn);		}	}	// Reset MOD panning (won't affect other module formats)	m_SndFile.SetupMODPanning();	m_SndFile.InitAmigaResampler();	return newNumChannels;}// Base code for adding, removing, moving and duplicating samples. Returns new number of samples on success, SAMPLEINDEX_INVALID otherwise.// The new sample vector can contain SAMPLEINDEX_INVALID for adding new (empty) samples.// newOrder indices are zero-based, i.e. newOrder[0] will define the contents of the first sample slot.SAMPLEINDEX CModDoc::ReArrangeSamples(const std::vector<SAMPLEINDEX> &newOrder){	if(newOrder.size() > m_SndFile.GetModSpecifications().samplesMax)	{		return SAMPLEINDEX_INVALID;	}	CriticalSection cs;	const SAMPLEINDEX oldNumSamples = m_SndFile.GetNumSamples(), newNumSamples = static_cast<SAMPLEINDEX>(newOrder.size());	std::vector<int> sampleCount(oldNumSamples + 1, 0);	std::vector<ModSample> sampleHeaders(oldNumSamples + 1);	std::vector<SAMPLEINDEX> newIndex(oldNumSamples + 1, 0);  // One of the new indexes for the old sample	std::vector<std::string> sampleNames(oldNumSamples + 1);	std::vector<mpt::PathString> samplePaths(oldNumSamples + 1);	for(SAMPLEINDEX i = 0; i < newNumSamples; i++)	{		const SAMPLEINDEX origSlot = newOrder[i];		if(origSlot > 0 && origSlot <= oldNumSamples)		{			sampleCount[origSlot]++;			sampleHeaders[origSlot] = m_SndFile.GetSample(origSlot);			if(!newIndex[origSlot])				newIndex[origSlot] = i + 1;		}	}	// First, delete all samples that will be removed anyway.	for(SAMPLEINDEX i = 1; i < sampleCount.size(); i++)	{		if(sampleCount[i] == 0)		{			m_SndFile.DestroySample(i);			GetSampleUndo().ClearUndo(i);		}		sampleNames[i] = m_SndFile.m_szNames[i];		samplePaths[i] = m_SndFile.GetSamplePath(i);	}	// Remove sample data references from now unused slots.	for(SAMPLEINDEX i = newNumSamples + 1; i <= oldNumSamples; i++)	{		m_SndFile.GetSample(i).pData.pSample = nullptr;		m_SndFile.GetSample(i).nLength = 0;		m_SndFile.m_szNames[i] = "";	}	// Now, create new sample list.	m_SndFile.m_nSamples = std::max(m_SndFile.m_nSamples, newNumSamples);  // Avoid assertions when using GetSample()...	for(SAMPLEINDEX i = 0; i < newNumSamples; i++)	{		const SAMPLEINDEX origSlot = newOrder[i];		ModSample &target = m_SndFile.GetSample(i + 1);		if(origSlot > 0 && origSlot <= oldNumSamples)		{			// Copy an original sample.			target = sampleHeaders[origSlot];			if(--sampleCount[origSlot] > 0 && sampleHeaders[origSlot].HasSampleData())			{				// This sample slot is referenced multiple times, so we have to copy the actual sample.				if(target.CopyWaveform(sampleHeaders[origSlot]))				{					target.PrecomputeLoops(m_SndFile, false);				} else				{					Reporting::Error("Cannot duplicate sample - out of memory!");				}			}			m_SndFile.m_szNames[i + 1] = sampleNames[origSlot];			m_SndFile.SetSamplePath(i + 1, samplePaths[origSlot]);		} else		{			// Invalid sample reference.			target.pData.pSample = nullptr;			target.Initialize(m_SndFile.GetType());			m_SndFile.m_szNames[i + 1] = "";			m_SndFile.ResetSamplePath(i + 1);		}	}	GetSampleUndo().RearrangeSamples(newIndex);	const auto muteFlag = CSoundFile::GetChannelMuteFlag();	for(CHANNELINDEX c = 0; c < std::size(m_SndFile.m_PlayState.Chn); c++)	{		ModChannel &chn = m_SndFile.m_PlayState.Chn[c];		for(SAMPLEINDEX i = 1; i <= oldNumSamples; i++)		{			if(chn.pModSample == &m_SndFile.GetSample(i))			{				chn.pModSample = &m_SndFile.GetSample(newIndex[i]);				if(i == 0 || i > newNumSamples)				{					chn.Reset(ModChannel::resetTotal, m_SndFile, c, muteFlag);				}				break;			}		}	}	m_SndFile.m_nSamples = newNumSamples;	if(m_SndFile.GetNumInstruments())	{		// Instrument mode: Update sample maps.		for(INSTRUMENTINDEX i = 0; i <= m_SndFile.GetNumInstruments(); i++)		{			ModInstrument *ins = m_SndFile.Instruments[i];			if(ins == nullptr)			{				continue;			}			GetInstrumentUndo().RearrangeSamples(i, newIndex);			for(auto &sample : ins->Keyboard)			{				if(sample < newIndex.size())					sample = newIndex[sample];				else					sample = 0;			}		}	} else	{		PrepareUndoForAllPatterns(false, "Rearrange Samples");		m_SndFile.Patterns.ForEachModCommand([&newIndex] (ModCommand &m)		{			if(!m.IsPcNote() && m.instr < newIndex.size())			{				m.instr = static_cast<ModCommand::INSTR>(newIndex[m.instr]);			}		});	}	return GetNumSamples();}// Base code for adding, removing, moving and duplicating instruments. Returns new number of instruments on success, INSTRUMENTINDEX_INVALID otherwise.// The new instrument vector can contain INSTRUMENTINDEX_INVALID for adding new (empty) instruments.// newOrder indices are zero-based, i.e. newOrder[0] will define the contents of the first instrument slot.INSTRUMENTINDEX CModDoc::ReArrangeInstruments(const std::vector<INSTRUMENTINDEX> &newOrder, deleteInstrumentSamples removeSamples){	if(newOrder.size() > m_SndFile.GetModSpecifications().instrumentsMax || GetNumInstruments() == 0)	{		return INSTRUMENTINDEX_INVALID;	}	CriticalSection cs;	const INSTRUMENTINDEX oldNumInstruments = m_SndFile.GetNumInstruments(), newNumInstruments = static_cast<INSTRUMENTINDEX>(newOrder.size());	std::vector<ModInstrument> instrumentHeaders(oldNumInstruments + 1);	std::vector<INSTRUMENTINDEX> newIndex(oldNumInstruments + 1, 0);  // One of the new indexes for the old instrument	for(INSTRUMENTINDEX i = 0; i < newNumInstruments; i++)	{		const INSTRUMENTINDEX origSlot = newOrder[i];		if(origSlot > 0 && origSlot <= oldNumInstruments)		{			if(m_SndFile.Instruments[origSlot] != nullptr)				instrumentHeaders[origSlot] = *m_SndFile.Instruments[origSlot];			newIndex[origSlot] = i + 1;		}	}	// Delete unused instruments first.	for(INSTRUMENTINDEX i = 1; i <= oldNumInstruments; i++)	{		if(newIndex[i] == 0)		{			m_SndFile.DestroyInstrument(i, removeSamples);		}	}	m_SndFile.m_nInstruments = newNumInstruments;	// Now, create new instrument list.	for(INSTRUMENTINDEX i = 0; i < newNumInstruments; i++)	{		ModInstrument *ins = m_SndFile.AllocateInstrument(i + 1);		if(ins == nullptr)		{			continue;		}		const INSTRUMENTINDEX origSlot = newOrder[i];		if(origSlot > 0 && origSlot <= oldNumInstruments)		{			// Copy an original instrument.			*ins = instrumentHeaders[origSlot];		}	}	// Free unused instruments	for(INSTRUMENTINDEX i = newNumInstruments + 1; i <= oldNumInstruments; i++)	{		m_SndFile.DestroyInstrument(i, doNoDeleteAssociatedSamples);	}	PrepareUndoForAllPatterns(false, "Rearrange Instruments");	GetInstrumentUndo().RearrangeInstruments(newIndex);	m_SndFile.Patterns.ForEachModCommand([&newIndex] (ModCommand &m)	{		if(!m.IsPcNote() && m.instr < newIndex.size())		{			m.instr = static_cast<ModCommand::INSTR>(newIndex[m.instr]);		}	});	return GetNumInstruments();}SEQUENCEINDEX CModDoc::ReArrangeSequences(const std::vector<SEQUENCEINDEX> &newOrder){	CriticalSection cs;	return m_SndFile.Order.Rearrange(newOrder) ? m_SndFile.Order.GetNumSequences() : SEQUENCEINDEX_INVALID;}bool CModDoc::ConvertInstrumentsToSamples(){	if(!m_SndFile.GetNumInstruments())		return false;	GetInstrumentUndo().ClearUndo();	m_SndFile.Patterns.ForEachModCommand([&] (ModCommand &m)	{		if(m.instr && !m.IsPcNote())		{			ModCommand::INSTR instr = m.instr, newinstr = 0;			ModCommand::NOTE note = m.note, newnote = note;			if(ModCommand::IsNote(note))				note = note - NOTE_MIN;			else				note = NOTE_MIDDLEC - NOTE_MIN;			if((instr < MAX_INSTRUMENTS) && (m_SndFile.Instruments[instr]))			{				const ModInstrument *pIns = m_SndFile.Instruments[instr];				newinstr = static_cast<ModCommand::INSTR>(pIns->Keyboard[note]);				newnote = pIns->NoteMap[note];				if(pIns->Keyboard[note] > Util::MaxValueOfType(m.instr))					newinstr = 0;			}			m.instr = newinstr;			if(m.IsNote())			{				m.note = newnote;			}		}	});	return true;}bool CModDoc::ConvertSamplesToInstruments(){	const INSTRUMENTINDEX instrumentMax = m_SndFile.GetModSpecifications().instrumentsMax;	if(GetNumInstruments() > 0 || instrumentMax == 0)		return false;	// If there is no actual sample data, don't bother creating any instruments	bool anySamples = false;	for(SAMPLEINDEX smp = 1; smp <= m_SndFile.m_nSamples; smp++)	{		if(m_SndFile.GetSample(smp).HasSampleData())		{			anySamples = true;			break;		}	}	if(!anySamples)		return true;	m_SndFile.m_nInstruments = std::min(m_SndFile.GetNumSamples(), instrumentMax);	for(SAMPLEINDEX smp = 1; smp <= m_SndFile.m_nInstruments; smp++)	{		const bool muted = IsSampleMuted(smp);		MuteSample(smp, false);		ModInstrument *instrument = m_SndFile.AllocateInstrument(smp, smp);		if(instrument == nullptr)		{			ErrorBox(IDS_ERR_OUTOFMEMORY, CMainFrame::GetMainFrame());			return false;		}		InitializeInstrument(instrument);		instrument->name = m_SndFile.m_szNames[smp];		MuteInstrument(smp, muted);	}	return true;}PLUGINDEX CModDoc::RemovePlugs(const std::vector<bool> &keepMask){	// Remove all plugins whose keepMask[plugindex] is false.	PLUGINDEX nRemoved = 0;	const PLUGINDEX maxPlug = std::min(MAX_MIXPLUGINS, static_cast<PLUGINDEX>(keepMask.size()));	CriticalSection cs;	for(PLUGINDEX nPlug = 0; nPlug < maxPlug; nPlug++)	{		SNDMIXPLUGIN &plug = m_SndFile.m_MixPlugins[nPlug];		if(keepMask[nPlug])		{			continue;		}		if(plug.pMixPlugin || plug.IsValidPlugin())		{			nRemoved++;		}		plug.Destroy();		plug = {};		for(PLUGINDEX srcPlugSlot = 0; srcPlugSlot < nPlug; srcPlugSlot++)		{			SNDMIXPLUGIN &srcPlug = GetSoundFile().m_MixPlugins[srcPlugSlot];			if(srcPlug.GetOutputPlugin() == nPlug)			{				srcPlug.SetOutputToMaster();				UpdateAllViews(nullptr, PluginHint(static_cast<PLUGINDEX>(srcPlugSlot + 1)).Info());			}		}		UpdateAllViews(nullptr, PluginHint(static_cast<PLUGINDEX>(nPlug + 1)).Info().Names());	}	if(nRemoved && m_SndFile.GetModSpecifications().supportsPlugins)		SetModified();	return nRemoved;}bool CModDoc::RemovePlugin(PLUGINDEX plugin){	if(plugin >= MAX_MIXPLUGINS)		return false;	std::vector<bool> keepMask(MAX_MIXPLUGINS, true);	keepMask[plugin] = false;	return RemovePlugs(keepMask) == 1;}// Clone a plugin slot (source does not necessarily have to be from the current module)void CModDoc::ClonePlugin(SNDMIXPLUGIN &target, const SNDMIXPLUGIN &source){	IMixPlugin *srcVstPlug = source.pMixPlugin;	target.Destroy();	target = source;	// Don't want this plugin to be accidentally erased again...	target.pMixPlugin = nullptr;	if(target.editorX != int32_min)	{		// Move target editor a bit to visually distinguish it from the original editor		int addPixels = Util::ScalePixels(16, CMainFrame::GetMainFrame()->m_hWnd);		target.editorX += addPixels;		target.editorY += addPixels;	}#ifndef NO_PLUGINS	if(theApp.GetPluginManager()->CreateMixPlugin(target, GetSoundFile()))	{		IMixPlugin *newVstPlug = target.pMixPlugin;		newVstPlug->SetCurrentProgram(srcVstPlug->GetCurrentProgram());		std::ostringstream f(std::ios::out | std::ios::binary);		if(VSTPresets::SaveFile(f, *srcVstPlug, false))		{			const std::string data = f.str();			FileReader file(mpt::as_span(data));			VSTPresets::LoadFile(file, *newVstPlug);		}	}#endif // !NO_PLUGINS}PATTERNINDEX CModDoc::InsertPattern(ROWINDEX rows, ORDERINDEX ord){	if(ord != ORDERINDEX_INVALID && m_SndFile.Order().GetLengthTailTrimmed() >= m_SndFile.GetModSpecifications().ordersMax)		return PATTERNINDEX_INVALID;	PATTERNINDEX pat = m_SndFile.Patterns.InsertAny(rows, true);	if(pat != PATTERNINDEX_INVALID)	{		if(ord != ORDERINDEX_INVALID)		{			m_SndFile.Order().insert(ord, 1, pat);		}		SetModified();	}	return pat;}SAMPLEINDEX CModDoc::InsertSample(){	SAMPLEINDEX i = m_SndFile.GetNextFreeSample();	if((i > std::numeric_limits<ModCommand::INSTR>::max() && !m_SndFile.GetNumInstruments()) || i == SAMPLEINDEX_INVALID)	{		ErrorBox(IDS_ERR_TOOMANYSMP, CMainFrame::GetMainFrame());		return SAMPLEINDEX_INVALID;	}	const bool newSlot = (i > m_SndFile.GetNumSamples());	if(newSlot || !m_SndFile.m_szNames[i][0])		m_SndFile.m_szNames[i] = "untitled";	if(newSlot)		m_SndFile.m_nSamples = i;	m_SndFile.GetSample(i).Initialize(m_SndFile.GetType());	m_SndFile.ResetSamplePath(i);	SetModified();	return i;}// Insert a new instrument assigned to sample nSample or duplicate instrument "duplicateSource".// If "sample" is invalid, an appropriate sample slot is selected. 0 means "no sample".INSTRUMENTINDEX CModDoc::InsertInstrument(SAMPLEINDEX sample, INSTRUMENTINDEX duplicateSource, bool silent){	if(m_SndFile.GetModSpecifications().instrumentsMax == 0)		return INSTRUMENTINDEX_INVALID;	ModInstrument *pDup = nullptr;	if(duplicateSource > 0 && duplicateSource <= m_SndFile.m_nInstruments)	{		pDup = m_SndFile.Instruments[duplicateSource];	}	if(!m_SndFile.GetNumInstruments() && (m_SndFile.GetNumSamples() > 1 || m_SndFile.GetSample(1).HasSampleData()))	{		bool doConvert = true;		if(!silent)		{			ConfirmAnswer result = Reporting::Confirm("Convert existing samples to instruments first?", true);			if(result == cnfCancel)			{				return INSTRUMENTINDEX_INVALID;			}			doConvert = (result == cnfYes);		}		if(doConvert)		{			if(!ConvertSamplesToInstruments())			{				return INSTRUMENTINDEX_INVALID;			}		}	}	const INSTRUMENTINDEX newins = m_SndFile.GetNextFreeInstrument();	if(newins == INSTRUMENTINDEX_INVALID)	{		if(!silent)		{			ErrorBox(IDS_ERR_TOOMANYINS, CMainFrame::GetMainFrame());		}		return INSTRUMENTINDEX_INVALID;	} else if(newins > m_SndFile.GetNumInstruments())	{		m_SndFile.m_nInstruments = newins;	}	// Determine which sample slot to use	SAMPLEINDEX newsmp = 0;	if(sample < m_SndFile.GetModSpecifications().samplesMax)	{		// Use specified slot		newsmp = sample;	} else if(!pDup)	{		newsmp = m_SndFile.GetNextFreeSample(newins);		if(newsmp > m_SndFile.GetNumSamples())		{			// Add a new sample			const SAMPLEINDEX inssmp = InsertSample();			if(inssmp != SAMPLEINDEX_INVALID)				newsmp = inssmp;		}	}	CriticalSection cs;	ModInstrument *pIns = m_SndFile.AllocateInstrument(newins, newsmp);	if(pIns == nullptr)	{		if(!silent)		{			cs.Leave();			ErrorBox(IDS_ERR_OUTOFMEMORY, CMainFrame::GetMainFrame());		}		return INSTRUMENTINDEX_INVALID;	}	InitializeInstrument(pIns);	if(pDup)	{		*pIns = *pDup;	}	SetModified();	return newins;}// Load default instrument values for inserting new instrument during editingvoid CModDoc::InitializeInstrument(ModInstrument *pIns){	pIns->pluginVolumeHandling = TrackerSettings::Instance().DefaultPlugVolumeHandling;}// Try to set up a new instrument that is linked to a given pluginINSTRUMENTINDEX CModDoc::InsertInstrumentForPlugin(PLUGINDEX plug){#ifndef NO_PLUGINS	const bool first = (GetNumInstruments() == 0);	INSTRUMENTINDEX instr = InsertInstrument(0, INSTRUMENTINDEX_INVALID, true);	if(instr == INSTRUMENTINDEX_INVALID)		return INSTRUMENTINDEX_INVALID;	ModInstrument &ins = *m_SndFile.Instruments[instr];	ins.name = mpt::ToCharset(m_SndFile.GetCharsetInternal(), MPT_UFORMAT("{}: {}")(plug + 1, m_SndFile.m_MixPlugins[plug].GetName()));	ins.filename = mpt::ToCharset(m_SndFile.GetCharsetInternal(), m_SndFile.m_MixPlugins[plug].GetLibraryName());	ins.nMixPlug = plug + 1;	ins.nMidiChannel = 1;	InstrumentHint hint = InstrumentHint(instr).Info().Envelope().Names();	if(first)		hint.ModType();	UpdateAllViews(nullptr, hint);	if(m_SndFile.GetModSpecifications().supportsPlugins)	{		SetModified();	}	return instr;#else	return INSTRUMENTINDEX_INVALID;#endif}INSTRUMENTINDEX CModDoc::HasInstrumentForPlugin(PLUGINDEX plug) const{	for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++)	{		if(m_SndFile.Instruments[i] != nullptr && m_SndFile.Instruments[i]->nMixPlug == plug + 1)		{			return i;		}	}	return INSTRUMENTINDEX_INVALID;}bool CModDoc::RemoveOrder(SEQUENCEINDEX nSeq, ORDERINDEX nOrd){	if(nSeq >= m_SndFile.Order.GetNumSequences() || nOrd >= m_SndFile.Order(nSeq).size())		return false;	CriticalSection cs;	m_SndFile.Order(nSeq).Remove(nOrd, nOrd);	SetModified();	return true;}bool CModDoc::RemovePattern(PATTERNINDEX nPat){	if(m_SndFile.Patterns.IsValidPat(nPat))	{		CriticalSection cs;		GetPatternUndo().PrepareUndo(nPat, 0, 0, GetNumChannels(), m_SndFile.Patterns[nPat].GetNumRows(), "Remove Pattern");		m_SndFile.Patterns.Remove(nPat);		SetModified();		return true;	}	return false;}bool CModDoc::RemoveSample(SAMPLEINDEX nSmp){	if((nSmp) && (nSmp <= m_SndFile.GetNumSamples()))	{		CriticalSection cs;		m_SndFile.DestroySample(nSmp);		m_SndFile.m_szNames[nSmp] = "";		while((m_SndFile.GetNumSamples() > 1)		      && (!m_SndFile.m_szNames[m_SndFile.GetNumSamples()][0])		      && (!m_SndFile.GetSample(m_SndFile.GetNumSamples()).HasSampleData()))		{			m_SndFile.m_nSamples--;		}		SetModified();		return true;	}	return false;}bool CModDoc::RemoveInstrument(INSTRUMENTINDEX nIns){	if((nIns) && (nIns <= m_SndFile.GetNumInstruments()) && (m_SndFile.Instruments[nIns]))	{		ConfirmAnswer result = cnfNo;		if(!m_SndFile.Instruments[nIns]->GetSamples().empty())			result = Reporting::Confirm("Remove samples associated with an instrument if they are unused?", "Removing instrument", true);		if(result == cnfCancel)		{			return false;		}		if(m_SndFile.DestroyInstrument(nIns, (result == cnfYes) ? deleteAssociatedSamples : doNoDeleteAssociatedSamples))		{			CriticalSection cs;			if(nIns == m_SndFile.m_nInstruments)				m_SndFile.m_nInstruments--;			bool instrumentsLeft = std::find_if(std::begin(m_SndFile.Instruments), std::end(m_SndFile.Instruments), [](ModInstrument *ins) { return ins != nullptr; }) != std::end(m_SndFile.Instruments);			if(!instrumentsLeft)				m_SndFile.m_nInstruments = 0;			SetModified();			return true;		}	}	return false;}bool CModDoc::MoveOrder(ORDERINDEX sourceOrd, ORDERINDEX destOrd, bool update, bool copy, SEQUENCEINDEX sourceSeq, SEQUENCEINDEX destSeq){	if(sourceSeq == SEQUENCEINDEX_INVALID)		sourceSeq = m_SndFile.Order.GetCurrentSequenceIndex();	if(destSeq == SEQUENCEINDEX_INVALID)		destSeq = m_SndFile.Order.GetCurrentSequenceIndex();	if(std::max(sourceSeq, destSeq) >= m_SndFile.Order.GetNumSequences())		return false;	const ORDERINDEX maxOrders = m_SndFile.GetModSpecifications().ordersMax;	if(destOrd > maxOrders)		return false;	if(destOrd == maxOrders && (sourceSeq != destSeq || copy))		return false;	auto &sourceSequence = m_SndFile.Order(sourceSeq);	const PATTERNINDEX sourcePat = sourceOrd < sourceSequence.size() ? sourceSequence[sourceOrd] : sourceSequence.GetInvalidPatIndex();	// Delete source	if(!copy)	{		sourceSequence.Remove(sourceOrd, sourceOrd);		if(sourceOrd < destOrd && sourceSeq == destSeq)			destOrd--;	}	// Insert at dest	m_SndFile.Order(destSeq).insert(destOrd, 1, sourcePat);	if(update)	{		UpdateAllViews(nullptr, SequenceHint().Data());	}	return true;}BOOL CModDoc::ExpandPattern(PATTERNINDEX nPattern){	ROWINDEX numRows;	if(!m_SndFile.Patterns.IsValidPat(nPattern)	   || (numRows = m_SndFile.Patterns[nPattern].GetNumRows()) > m_SndFile.GetModSpecifications().patternRowsMax / 2)	{		return false;	}	BeginWaitCursor();	CriticalSection cs;	GetPatternUndo().PrepareUndo(nPattern, 0, 0, GetNumChannels(), numRows, "Expand Pattern");	bool success = m_SndFile.Patterns[nPattern].Expand();	cs.Leave();	EndWaitCursor();	if(success)	{		SetModified();		UpdateAllViews(NULL, PatternHint(nPattern).Data(), NULL);	} else	{		GetPatternUndo().RemoveLastUndoStep();	}	return success;}BOOL CModDoc::ShrinkPattern(PATTERNINDEX nPattern){	ROWINDEX numRows;	if(!m_SndFile.Patterns.IsValidPat(nPattern)	   || (numRows = m_SndFile.Patterns[nPattern].GetNumRows()) < m_SndFile.GetModSpecifications().patternRowsMin * 2)	{		return false;	}	BeginWaitCursor();	CriticalSection cs;	GetPatternUndo().PrepareUndo(nPattern, 0, 0, GetNumChannels(), numRows, "Shrink Pattern");	bool success = m_SndFile.Patterns[nPattern].Shrink();	cs.Leave();	EndWaitCursor();	if(success)	{		SetModified();		UpdateAllViews(NULL, PatternHint(nPattern).Data(), NULL);	} else	{		GetPatternUndo().RemoveLastUndoStep();	}	return success;}/////////////////////////////////////////////////////////////////////////////////////////// Copy/Paste envelopestatic constexpr const char pszEnvHdr[] = "ModPlug Tracker Envelope\r\n";static constexpr const char pszEnvFmt[] = "%d,%d,%d,%d,%d,%d,%d,%d\r\n";static bool EnvelopeToString(CStringA &s, const InstrumentEnvelope &env){	// We don't want to copy empty envelopes	if(env.empty())	{		return false;	}	s.Preallocate(2048);	s = pszEnvHdr;	s.AppendFormat(pszEnvFmt, env.size(), env.nSustainStart, env.nSustainEnd, env.nLoopStart, env.nLoopEnd, env.dwFlags[ENV_SUSTAIN] ? 1 : 0, env.dwFlags[ENV_LOOP] ? 1 : 0, env.dwFlags[ENV_CARRY] ? 1 : 0);	for(auto &p : env)	{		s.AppendFormat("%d,%d\r\n", p.tick, p.value);	}	// Writing release node	s.AppendFormat("%u\r\n", env.nReleaseNode);	return true;}static bool StringToEnvelope(const std::string_view &s, InstrumentEnvelope &env, const CModSpecifications &specs){	uint32 susBegin = 0, susEnd = 0, loopBegin = 0, loopEnd = 0, bSus = 0, bLoop = 0, bCarry = 0, nPoints = 0, releaseNode = ENV_RELEASE_NODE_UNSET;	size_t length = s.size(), pos = std::size(pszEnvHdr) - 1;	if(length <= pos || mpt::CompareNoCaseAscii(s.data(), pszEnvHdr, pos - 2))	{		return false;	}	sscanf(&s[pos], pszEnvFmt, &nPoints, &susBegin, &susEnd, &loopBegin, &loopEnd, &bSus, &bLoop, &bCarry);	while(pos < length && s[pos] != '\r' && s[pos] != '\n')		pos++;	nPoints = std::min(nPoints, static_cast<uint32>(specs.envelopePointsMax));	if(susEnd >= nPoints)		susEnd = 0;	if(susBegin > susEnd)		susBegin = susEnd;	if(loopEnd >= nPoints)		loopEnd = 0;	if(loopBegin > loopEnd)		loopBegin = loopEnd;	try	{		env.resize(nPoints);	} catch(mpt::out_of_memory e)	{		mpt::delete_out_of_memory(e);		return false;	}	env.nSustainStart = static_cast<decltype(env.nSustainStart)>(susBegin);	env.nSustainEnd = static_cast<decltype(env.nSustainEnd)>(susEnd);	env.nLoopStart = static_cast<decltype(env.nLoopStart)>(loopBegin);	env.nLoopEnd = static_cast<decltype(env.nLoopEnd)>(loopEnd);	env.nReleaseNode = static_cast<decltype(env.nReleaseNode)>(releaseNode);	env.dwFlags.set(ENV_LOOP, bLoop != 0);	env.dwFlags.set(ENV_SUSTAIN, bSus != 0);	env.dwFlags.set(ENV_CARRY, bCarry != 0);	env.dwFlags.set(ENV_ENABLED, nPoints > 0);	int oldn = 0;	for(auto &p : env)	{		while(pos < length && (s[pos] < '0' || s[pos] > '9'))			pos++;		if(pos >= length)			break;		int n1 = atoi(&s[pos]);		while(pos < length && s[pos] != ',')			pos++;		while(pos < length && (s[pos] < '0' || s[pos] > '9'))			pos++;		if(pos >= length)			break;		int n2 = atoi(&s[pos]);		if(n1 < oldn)			n1 = oldn + 1;		Limit(n2, ENVELOPE_MIN, ENVELOPE_MAX);		p.tick = (uint16)n1;		p.value = (uint8)n2;		oldn = n1;		while(pos < length && s[pos] != '\r' && s[pos] != '\n')			pos++;		if(pos >= length)			break;	}	env.Sanitize();	// Read release node information.	env.nReleaseNode = ENV_RELEASE_NODE_UNSET;	if(pos < length)	{		auto r = static_cast<decltype(env.nReleaseNode)>(atoi(&s[pos]));		if(r == 0 || r >= nPoints || !specs.hasReleaseNode)			r = ENV_RELEASE_NODE_UNSET;		env.nReleaseNode = r;	}	return true;}bool CModDoc::CopyEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv){	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();	if((nIns < 1) || (nIns > m_SndFile.m_nInstruments) || (!m_SndFile.Instruments[nIns]) || (!pMainFrm))		return false;	BeginWaitCursor();	const ModInstrument *pIns = m_SndFile.Instruments[nIns];	if(pIns == nullptr)		return false;	CStringA s;	EnvelopeToString(s, pIns->GetEnvelope(nEnv));	int memSize = s.GetLength() + 1;	Clipboard clipboard(CF_TEXT, memSize);	if(auto p = clipboard.As<char>())	{		memcpy(p, s.GetString(), memSize);	}	EndWaitCursor();	return true;}bool CModDoc::SaveEnvelope(INSTRUMENTINDEX ins, EnvelopeType env, const mpt::PathString &fileName){	if(ins < 1 || ins > m_SndFile.m_nInstruments || !m_SndFile.Instruments[ins])		return false;	BeginWaitCursor();	const ModInstrument *pIns = m_SndFile.Instruments[ins];	if(pIns == nullptr)		return false;	CStringA s;	EnvelopeToString(s, pIns->GetEnvelope(env));	bool ok = false;	try	{		mpt::SafeOutputFile sf(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));		mpt::ofstream &f = sf;		f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);		if(f)			ok = mpt::IO::WriteRaw(f, s.GetString(), s.GetLength());		else			ok = false;	} catch(const std::exception &)	{		ok = false;	}	EndWaitCursor();	return ok;}bool CModDoc::PasteEnvelope(INSTRUMENTINDEX ins, EnvelopeType env){	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();	if(ins < 1 || ins > m_SndFile.m_nInstruments || !m_SndFile.Instruments[ins] || !pMainFrm)		return false;	BeginWaitCursor();	Clipboard clipboard(CF_TEXT);	auto data = clipboard.GetString();	if(!data.length())	{		EndWaitCursor();		return false;	}	bool result = StringToEnvelope(data, m_SndFile.Instruments[ins]->GetEnvelope(env), m_SndFile.GetModSpecifications());	EndWaitCursor();	return result;}bool CModDoc::LoadEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName){	InputFile f(fileName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);	if(nIns < 1 || nIns > m_SndFile.m_nInstruments || !m_SndFile.Instruments[nIns] || !f.IsValid())		return false;	BeginWaitCursor();	FileReader file = GetFileReader(f);	std::string data;	file.ReadNullString(data, 1 << 16);	bool result = StringToEnvelope(data, m_SndFile.Instruments[nIns]->GetEnvelope(nEnv), m_SndFile.GetModSpecifications());	EndWaitCursor();	return result;}// Check which channels contain note data. maxRemoveCount specified how many empty channels are reported at max.void CModDoc::CheckUsedChannels(std::vector<bool> &usedMask, CHANNELINDEX maxRemoveCount) const{	// Checking for unused channels	CHANNELINDEX chn = GetNumChannels();	usedMask.assign(chn, true);	while(chn-- > 0)	{		if(IsChannelUnused(chn))		{			usedMask[chn] = false;			// Found enough empty channels yet?			if((--maxRemoveCount) == 0)				break;		}	}}// Check if a given channel contains note data or global effects.bool CModDoc::IsChannelUnused(CHANNELINDEX nChn) const{	const CHANNELINDEX nChannels = GetNumChannels();	if(nChn >= nChannels)	{		return true;	}	for(auto &pat : m_SndFile.Patterns)	{		if(pat.IsValid())		{			const ModCommand *p = pat.GetpModCommand(0, nChn);			for(ROWINDEX row = pat.GetNumRows(); row > 0; row--, p += nChannels)			{				if(p->IsNote() || p->IsInstrPlug() || p->IsGlobalCommand())					return false;			}		}	}	return true;}bool CModDoc::IsSampleUsed(SAMPLEINDEX sample, bool searchInMutedChannels) const{	if(!sample || sample > GetNumSamples())		return false;	if(GetNumInstruments())	{		for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++)		{			if(m_SndFile.IsSampleReferencedByInstrument(sample, i))				return true;		}	} else	{		for(const auto &pattern : m_SndFile.Patterns)		{			if(!pattern.IsValid())				continue;			auto m = pattern.cbegin();			for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)			{				for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels(); chn++, m++)				{					if(searchInMutedChannels || !m_SndFile.ChnSettings[chn].dwFlags[CHN_MUTE])					{						if(m->instr == sample && !m->IsPcNote())							return true;					}				}			}		}	}	return false;}bool CModDoc::IsInstrumentUsed(INSTRUMENTINDEX instr, bool searchInMutedChannels) const{	if(instr < 1 || instr > GetNumInstruments())		return false;	for(const auto &pattern : m_SndFile.Patterns)	{		if(!pattern.IsValid())			continue;		auto m = pattern.cbegin();		for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++)		{			for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels(); chn++, m++)			{				if(searchInMutedChannels || !m_SndFile.ChnSettings[chn].dwFlags[CHN_MUTE])				{					if(m->instr == instr && !m->IsPcNote())						return true;				}			}		}	}	return false;}// Convert module's default global volume to a pattern command.bool CModDoc::GlobalVolumeToPattern(){	bool result = false;	if(m_SndFile.GetModSpecifications().HasCommand(CMD_GLOBALVOLUME))	{		for(PATTERNINDEX pat : m_SndFile.Order())		{			if(m_SndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_GLOBALVOLUME, mpt::saturate_cast<ModCommand::PARAM>(m_SndFile.m_nDefaultGlobalVolume * 64 / MAX_GLOBAL_VOLUME)).RetryNextRow()))			{				result = true;				break;			}		}	}	m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;	return result;}SAMPLEINDEX CModDoc::GetSampleIndex(const ModCommand &m, ModCommand::INSTR lastInstr) const{	if(m.IsPcNote())		return 0;	return m_SndFile.GetSampleIndex(m.note, m.instr > 0 ? m.instr : lastInstr);}int CModDoc::GetInstrumentGroupSize(INSTRUMENTINDEX instr) const{	const ModInstrument *ins;	if(instr > 0 && instr <= GetNumInstruments()	   && (ins = m_SndFile.Instruments[instr]) != nullptr && ins->pTuning != nullptr	   && ins->pTuning->GetGroupSize() != 0)	{		return ins->pTuning->GetGroupSize();	}	return 12;}int CModDoc::GetBaseNote(INSTRUMENTINDEX instr) const{	// This may look a bit strange (using -12 and -4 instead of just -5 in the second part) but this is to keep custom tunings centered around middle-C on the keyboard.	return NOTE_MIDDLEC - 12 + (CMainFrame::GetMainFrame()->GetBaseOctave() - 4) * GetInstrumentGroupSize(instr);}ModCommand::NOTE CModDoc::GetNoteWithBaseOctave(int noteOffset, INSTRUMENTINDEX instr) const{	return static_cast<ModCommand::NOTE>(Clamp(GetBaseNote(instr) + noteOffset, NOTE_MIN, NOTE_MAX));}OPENMPT_NAMESPACE_END
 |