/*
 * Moddoc.cpp
 * ----------
 * Purpose: Module document handling in OpenMPT.
 * 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 "InputHandler.h"
#include "Moddoc.h"
#include "ModDocTemplate.h"
#include "../soundlib/mod_specifications.h"
#include "../soundlib/plugins/PlugInterface.h"
#include "Childfrm.h"
#include "Mpdlgs.h"
#include "dlg_misc.h"
#include "TempoSwingDialog.h"
#include "mod2wave.h"
#include "ChannelManagerDlg.h"
#include "MIDIMacroDialog.h"
#include "MIDIMappingDialog.h"
#include "StreamEncoderAU.h"
#include "StreamEncoderFLAC.h"
#include "StreamEncoderMP3.h"
#include "StreamEncoderOpus.h"
#include "StreamEncoderRAW.h"
#include "StreamEncoderVorbis.h"
#include "StreamEncoderWAV.h"
#include "mod2midi.h"
#include "../common/version.h"
#include "../tracklib/SampleEdit.h"
#include "../soundlib/modsmp_ctrl.h"
#include "CleanupSong.h"
#include "../common/mptStringBuffer.h"
#include "../common/mptFileIO.h"
#include <sstream>
#include "../common/FileReader.h"
#include "FileDialog.h"
#include "ExternalSamples.h"
#include "Globals.h"
#include "../soundlib/OPL.h"
#ifndef NO_PLUGINS
#include "AbstractVstEditor.h"
#endif
#include "mpt/binary/hex.hpp"
#include "mpt/base/numbers.hpp"
#include "mpt/io/io.hpp"
#include "mpt/io/io_stdstream.hpp"


OPENMPT_NAMESPACE_BEGIN


const TCHAR FileFilterMOD[]	= _T("ProTracker Modules (*.mod)|*.mod||");
const TCHAR FileFilterXM[]	= _T("FastTracker Modules (*.xm)|*.xm||");
const TCHAR FileFilterS3M[] = _T("Scream Tracker Modules (*.s3m)|*.s3m||");
const TCHAR FileFilterIT[]	= _T("Impulse Tracker Modules (*.it)|*.it||");
const TCHAR FileFilterMPT[] = _T("OpenMPT Modules (*.mptm)|*.mptm||");
const TCHAR FileFilterNone[] = _T("");

const CString ModTypeToFilter(const CSoundFile& sndFile)
{
	const MODTYPE modtype = sndFile.GetType();
	switch(modtype)
	{
		case MOD_TYPE_MOD: return FileFilterMOD;
		case MOD_TYPE_XM: return FileFilterXM;
		case MOD_TYPE_S3M: return FileFilterS3M;
		case MOD_TYPE_IT: return FileFilterIT;
		case MOD_TYPE_MPT: return FileFilterMPT;
		default: return FileFilterNone;
	}
}

/////////////////////////////////////////////////////////////////////////////
// CModDoc

IMPLEMENT_DYNCREATE(CModDoc, CDocument)

BEGIN_MESSAGE_MAP(CModDoc, CDocument)
	//{{AFX_MSG_MAP(CModDoc)
	ON_COMMAND(ID_FILE_SAVE_COPY,		&CModDoc::OnSaveCopy)
	ON_COMMAND(ID_FILE_SAVEASTEMPLATE,	&CModDoc::OnSaveTemplateModule)
	ON_COMMAND(ID_FILE_SAVEASWAVE,		&CModDoc::OnFileWaveConvert)
	ON_COMMAND(ID_FILE_SAVEMIDI,		&CModDoc::OnFileMidiConvert)
	ON_COMMAND(ID_FILE_SAVEOPL,			&CModDoc::OnFileOPLExport)
	ON_COMMAND(ID_FILE_SAVECOMPAT,		&CModDoc::OnFileCompatibilitySave)
	ON_COMMAND(ID_FILE_APPENDMODULE,	&CModDoc::OnAppendModule)
	ON_COMMAND(ID_PLAYER_PLAY,			&CModDoc::OnPlayerPlay)
	ON_COMMAND(ID_PLAYER_PAUSE,			&CModDoc::OnPlayerPause)
	ON_COMMAND(ID_PLAYER_STOP,			&CModDoc::OnPlayerStop)
	ON_COMMAND(ID_PLAYER_PLAYFROMSTART,	&CModDoc::OnPlayerPlayFromStart)
	ON_COMMAND(ID_VIEW_SONGPROPERTIES,	&CModDoc::OnSongProperties)
	ON_COMMAND(ID_VIEW_GLOBALS,			&CModDoc::OnEditGlobals)
	ON_COMMAND(ID_VIEW_PATTERNS,		&CModDoc::OnEditPatterns)
	ON_COMMAND(ID_VIEW_SAMPLES,			&CModDoc::OnEditSamples)
	ON_COMMAND(ID_VIEW_INSTRUMENTS,		&CModDoc::OnEditInstruments)
	ON_COMMAND(ID_VIEW_COMMENTS,		&CModDoc::OnEditComments)
	ON_COMMAND(ID_VIEW_EDITHISTORY,		&CModDoc::OnViewEditHistory)
	ON_COMMAND(ID_VIEW_MIDIMAPPING,		&CModDoc::OnViewMIDIMapping)
	ON_COMMAND(ID_VIEW_MPTHACKS,		&CModDoc::OnViewMPTHacks)
	ON_COMMAND(ID_EDIT_CLEANUP,			&CModDoc::OnShowCleanup)
	ON_COMMAND(ID_EDIT_SAMPLETRIMMER,	&CModDoc::OnShowSampleTrimmer)
	ON_COMMAND(ID_PATTERN_MIDIMACRO,	&CModDoc::OnSetupZxxMacros)
	ON_COMMAND(ID_CHANNEL_MANAGER,		&CModDoc::OnChannelManager)

	ON_COMMAND(ID_ESTIMATESONGLENGTH,	&CModDoc::OnEstimateSongLength)
	ON_COMMAND(ID_APPROX_BPM,			&CModDoc::OnApproximateBPM)
	ON_COMMAND(ID_PATTERN_PLAY,			&CModDoc::OnPatternPlay)
	ON_COMMAND(ID_PATTERN_PLAYNOLOOP,	&CModDoc::OnPatternPlayNoLoop)
	ON_COMMAND(ID_PATTERN_RESTART,		&CModDoc::OnPatternRestart)
	ON_UPDATE_COMMAND_UI(ID_VIEW_INSTRUMENTS,		&CModDoc::OnUpdateXMITMPTOnly)
	ON_UPDATE_COMMAND_UI(ID_PATTERN_MIDIMACRO,		&CModDoc::OnUpdateXMITMPTOnly)
	ON_UPDATE_COMMAND_UI(ID_VIEW_MIDIMAPPING,		&CModDoc::OnUpdateHasMIDIMappings)
	ON_UPDATE_COMMAND_UI(ID_VIEW_EDITHISTORY,		&CModDoc::OnUpdateHasEditHistory)
	ON_UPDATE_COMMAND_UI(ID_FILE_SAVECOMPAT,		&CModDoc::OnUpdateCompatExportableOnly)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()


/////////////////////////////////////////////////////////////////////////////
// CModDoc construction/destruction

CModDoc::CModDoc()
	: m_notifyType(Notification::Default)
	, m_PatternUndo(*this)
	, m_SampleUndo(*this)
	, m_InstrumentUndo(*this)
{
	// Set the creation date of this file (or the load time if we're loading an existing file)
	time(&m_creationTime);

	ReinitRecordState();

	CMainFrame::UpdateAudioParameters(m_SndFile, true);
}


CModDoc::~CModDoc()
{
	ClearLog();
}


void CModDoc::SetModified(bool modified)
{
	static_assert(sizeof(long) == sizeof(m_bModified));
	m_modifiedAutosave = modified;
	if(!!InterlockedExchange(reinterpret_cast<long *>(&m_bModified), modified ? TRUE : FALSE) != modified)
	{
		// Update window titles in GUI thread
		CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_SETMODIFIED, reinterpret_cast<WPARAM>(this), 0);
	}
}


// Return "modified since last autosave" status and reset it until the next SetModified() (as this is only used for polling during autosave)
bool CModDoc::ModifiedSinceLastAutosave()
{
	return m_modifiedAutosave.exchange(false);
}


BOOL CModDoc::OnNewDocument()
{
	if (!CDocument::OnNewDocument()) return FALSE;

	m_SndFile.Create(FileReader(), CSoundFile::loadCompleteModule, this);
	m_SndFile.ChangeModTypeTo(CTrackApp::GetDefaultDocType());

	theApp.GetDefaultMidiMacro(m_SndFile.m_MidiCfg);
	m_SndFile.m_SongFlags.set((SONG_LINEARSLIDES | SONG_ISAMIGA) & m_SndFile.GetModSpecifications().songFlags);

	ReinitRecordState();
	InitializeMod();
	SetModified(false);
	return TRUE;
}


BOOL CModDoc::OnOpenDocument(LPCTSTR lpszPathName)
{
	const mpt::PathString filename = lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString();

	ScopedLogCapturer logcapturer(*this);

	if(filename.empty()) return OnNewDocument();

	BeginWaitCursor();

	{

		MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open..."));

		InputFile f(filename, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
		if (f.IsValid())
		{
			FileReader file = GetFileReader(f);
			MPT_ASSERT(GetPathNameMpt().empty());
			SetPathName(filename, FALSE);	// Path is not set yet, but loaders processing external samples/instruments (ITP/MPTM) need this for relative paths.
			try
			{
				if(!m_SndFile.Create(file, CSoundFile::loadCompleteModule, this))
				{
					EndWaitCursor();
					return FALSE;
				}
			} catch(mpt::out_of_memory e)
			{
				mpt::delete_out_of_memory(e);
				EndWaitCursor();
				AddToLog(LogError, U_("Out of Memory"));
				return FALSE;
			} catch(const std::exception &)
			{
				EndWaitCursor();
				return FALSE;
			}
		}

		MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open."));

	}

	EndWaitCursor();

	logcapturer.ShowLog(
		MPT_CFORMAT("File: {}\nLast saved with: {}, you are using OpenMPT {}\n\n")
		(filename, m_SndFile.m_modFormat.madeWithTracker, Version::Current()));

	if((m_SndFile.m_nType == MOD_TYPE_NONE) || (!m_SndFile.GetNumChannels()))
		return FALSE;

	const bool noColors = std::find_if(std::begin(m_SndFile.ChnSettings), std::begin(m_SndFile.ChnSettings) + GetNumChannels(), [](const auto &settings) {
		return settings.color != ModChannelSettings::INVALID_COLOR;
	}) == std::begin(m_SndFile.ChnSettings) + GetNumChannels();
	if(noColors)
	{
		SetDefaultChannelColors();
	}

	// Convert to MOD/S3M/XM/IT
	switch(m_SndFile.GetType())
	{
	case MOD_TYPE_MOD:
	case MOD_TYPE_S3M:
	case MOD_TYPE_XM:
	case MOD_TYPE_IT:
	case MOD_TYPE_MPT:
		break;
	default:
		m_SndFile.ChangeModTypeTo(m_SndFile.GetBestSaveFormat(), false);
		m_SndFile.m_SongFlags.set(SONG_IMPORTED);
		break;
	}
	// If the file was packed in some kind of container (e.g. ZIP, or simply a format like MO3), prompt for new file extension as well
	// Same if MOD_TYPE_XXX does not indicate actual song format
	if(m_SndFile.GetContainerType() != MOD_CONTAINERTYPE_NONE || m_SndFile.m_SongFlags[SONG_IMPORTED])
	{
		m_ShowSavedialog = true;
	}

	ReinitRecordState();

	if(TrackerSettings::Instance().rememberSongWindows)
		DeserializeViews();

	// This is only needed when opening a module with stored window positions.
	// The MDI child is activated before it has an active view and thus there is no CModDoc associated with it.
	CMainFrame::GetMainFrame()->UpdateEffectKeys(this);
	auto instance = CChannelManagerDlg::sharedInstance();
	if(instance != nullptr)
	{
		instance->SetDocument(this);
	}

	// Show warning if file was made with more recent version of OpenMPT except
	if(m_SndFile.m_dwLastSavedWithVersion.WithoutTestNumber() > Version::Current())
	{
		Reporting::Notification(MPT_UFORMAT("Warning: this song was last saved with a more recent version of OpenMPT.\r\nSong saved with: v{}. Current version: v{}.\r\n")(
			m_SndFile.m_dwLastSavedWithVersion,
			Version::Current()));
	}

	SetModified(false);
	m_bHasValidPath = true;

	// Check if there are any missing samples, and if there are, show a dialog to relocate them.
	for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
	{
		if(m_SndFile.IsExternalSampleMissing(smp))
		{
			MissingExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame());
			dlg.DoModal();
			break;
		}
	}

	return TRUE;
}


bool CModDoc::OnSaveDocument(const mpt::PathString &filename, const bool setPath)
{
	ScopedLogCapturer logcapturer(*this);
	if(filename.empty())
		return false;

	bool ok = false;
	BeginWaitCursor();
	m_SndFile.m_dwLastSavedWithVersion = Version::Current();
	try
	{
		mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
		mpt::ofstream &f = sf;
		if(f)
		{
			if(m_SndFile.m_SongFlags[SONG_IMPORTED] && !(GetModType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)))
			{
				// Check if any non-supported playback behaviours are enabled due to being imported from a different format
				const auto supportedBehaviours = m_SndFile.GetSupportedPlaybackBehaviour(GetModType());
				bool showWarning = true;
				for(size_t i = 0; i < kMaxPlayBehaviours; i++)
				{
					if(m_SndFile.m_playBehaviour[i] && !supportedBehaviours[i])
					{
						if(showWarning)
						{
							AddToLog(LogWarning, mpt::ToUnicode(mpt::Charset::ASCII, MPT_AFORMAT("Some imported Compatibility Settings that are not supported by the {} format have been disabled. Verify that the module still sounds as intended.")
								(mpt::ToUpperCaseAscii(m_SndFile.GetModSpecifications().fileExtension))));
							showWarning = false;
						}
						m_SndFile.m_playBehaviour.reset(i);
					}
				}
			}

			f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
			FixNullStrings();
			switch(m_SndFile.GetType())
			{
			case MOD_TYPE_MOD: ok = m_SndFile.SaveMod(f); break;
			case MOD_TYPE_S3M: ok = m_SndFile.SaveS3M(f); break;
			case MOD_TYPE_XM:  ok = m_SndFile.SaveXM(f); break;
			case MOD_TYPE_IT:  ok = m_SndFile.SaveIT(f, filename); break;
			case MOD_TYPE_MPT: ok = m_SndFile.SaveIT(f, filename); break;
			default:           MPT_ASSERT_NOTREACHED();
			}
		}
	} catch(const std::exception &)
	{
		ok = false;
	}
	EndWaitCursor();

	if(ok)
	{
		if(setPath)
		{
			// Set new path for this file, unless we are saving a template or a copy, in which case we want to keep the old file path.
			SetPathName(filename);
		}
		logcapturer.ShowLog(true);
		if(TrackerSettings::Instance().rememberSongWindows)
			SerializeViews();
	} else
	{
		ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
	}
	return ok;
}


BOOL CModDoc::SaveModified()
{
	if(m_SndFile.GetType() == MOD_TYPE_MPT && !SaveAllSamples())
		return FALSE;
	return CDocument::SaveModified();
}


bool CModDoc::SaveAllSamples(bool showPrompt)
{
	if(showPrompt)
	{
		ModifiedExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame());
		return dlg.DoModal() == IDOK;
	} else
	{
		bool ok = true;
		for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
		{
			ok &= SaveSample(smp);
		}
		return ok;
	}
}


bool CModDoc::SaveSample(SAMPLEINDEX smp)
{
	bool success = false;
	if(smp > 0 && smp <= GetNumSamples())
	{
		const mpt::PathString filename = m_SndFile.GetSamplePath(smp);
		if(!filename.empty())
		{
			auto &sample = m_SndFile.GetSample(smp);
			const auto ext = filename.GetFileExt().ToUnicode().substr(1);
			const auto format = FromSettingValue<SampleEditorDefaultFormat>(ext);

			try
			{
				mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
				if(sf)
				{
					mpt::ofstream &f = sf;
					f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);

					if(sample.uFlags[CHN_ADLIB] || format == dfS3I)
						success = m_SndFile.SaveS3ISample(smp, f);
					else if(format != dfWAV)
						success = m_SndFile.SaveFLACSample(smp, f);
					else
						success = m_SndFile.SaveWAVSample(smp, f);
				}
			} catch(const std::exception &)
			{
				success = false;
			}

			if(success)
				sample.uFlags.reset(SMP_MODIFIED);
			else
				AddToLog(LogError, MPT_UFORMAT("Unable to save sample {}: {}")(smp, filename));
		}
	}
	return success;
}


void CModDoc::OnCloseDocument()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if(pMainFrm) pMainFrm->OnDocumentClosed(this);
	CDocument::OnCloseDocument();
}


void CModDoc::DeleteContents()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm) pMainFrm->StopMod(this);
	m_SndFile.Destroy();
	ReinitRecordState();
}


BOOL CModDoc::DoSave(const mpt::PathString &filename, bool setPath)
{
	const mpt::PathString docFileName = GetPathNameMpt();
	const std::string defaultExtension = m_SndFile.GetModSpecifications().fileExtension;

	switch(m_SndFile.GetBestSaveFormat())
	{
	case MOD_TYPE_MOD:
		MsgBoxHidable(ModSaveHint);
		break;
	case MOD_TYPE_S3M:
		break;
	case MOD_TYPE_XM:
		MsgBoxHidable(XMCompatibilityExportTip);
		break;
	case MOD_TYPE_IT:
		MsgBoxHidable(ItCompatibilityExportTip);
		break;
	case MOD_TYPE_MPT:
		break;
	default:
		ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
		return FALSE;
	}

	mpt::PathString ext = P_(".") + mpt::PathString::FromUTF8(defaultExtension);

	mpt::PathString saveFileName;

	if(filename.empty() || m_ShowSavedialog)
	{
		mpt::PathString drive = docFileName.GetDrive();
		mpt::PathString dir = docFileName.GetDir();
		mpt::PathString fileName = docFileName.GetFileName();
		if(fileName.empty())
		{
			fileName = mpt::PathString::FromCString(GetTitle()).SanitizeComponent();
		}
		mpt::PathString defaultSaveName = drive + dir + fileName + ext;

		FileDialog dlg = SaveFileDialog()
			.DefaultExtension(defaultExtension)
			.DefaultFilename(defaultSaveName)
			.ExtensionFilter(ModTypeToFilter(m_SndFile))
			.WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir());
		if(!dlg.Show()) return FALSE;

		TrackerSettings::Instance().PathSongs.SetWorkingDir(dlg.GetWorkingDirectory());

		saveFileName = dlg.GetFirstFile();
	} else
	{
		saveFileName = filename;
	}

	// Do we need to create a backup file ?
	if((TrackerSettings::Instance().CreateBackupFiles)
		&& (IsModified()) && (!mpt::PathString::CompareNoCase(saveFileName, docFileName)))
	{
		if(saveFileName.IsFile())
		{
			mpt::PathString backupFileName = saveFileName.ReplaceExt(P_(".bak"));
			if(backupFileName.IsFile())
			{
				DeleteFile(backupFileName.AsNative().c_str());
			}
			MoveFile(saveFileName.AsNative().c_str(), backupFileName.AsNative().c_str());
		}
	}
	if(OnSaveDocument(saveFileName, setPath))
	{
		SetModified(false);
		m_SndFile.m_SongFlags.reset(SONG_IMPORTED);
		m_bHasValidPath = true;
		m_ShowSavedialog = false;
		CMainFrame::GetMainFrame()->UpdateTree(this, GeneralHint().General()); // Update treeview (e.g. filename might have changed)
		return TRUE;
	} else
	{
		return FALSE;
	}
}


void CModDoc::OnAppendModule()
{
	FileDialog::PathList files;
	CTrackApp::OpenModulesDialog(files);

	ScopedLogCapturer logcapture(*this, _T("Append Failures"));
	try
	{
		auto source = std::make_unique<CSoundFile>();
		for(const auto &file : files)
		{
			InputFile f(file, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
			if(!f.IsValid())
			{
				AddToLog("Unable to open source file!");
				continue;
			}
			try
			{
				if(!source->Create(GetFileReader(f), CSoundFile::loadCompleteModule))
				{
					AddToLog("Unable to open source file!");
					continue;
				}
			} catch(const std::exception &)
			{
				AddToLog("Unable to open source file!");
				continue;
			}
			AppendModule(*source);
			source->Destroy();
			SetModified();
		}
	} catch(mpt::out_of_memory e)
	{
		mpt::delete_out_of_memory(e);
		AddToLog("Out of memory.");
		return;
	}
	
	UpdateAllViews(nullptr, SequenceHint().Data().ModType());
}


void CModDoc::InitializeMod()
{
	// New module ?
	if (!m_SndFile.m_nChannels)
	{
		switch(GetModType())
		{
		case MOD_TYPE_MOD:
			m_SndFile.m_nChannels = 4;
			break;
		case MOD_TYPE_S3M:
			m_SndFile.m_nChannels = 16;
			break;
		default:
			m_SndFile.m_nChannels = 32;
			break;
		}

		SetDefaultChannelColors();

		if(GetModType() == MOD_TYPE_MPT)
		{
			m_SndFile.m_nTempoMode = TempoMode::Modern;
			m_SndFile.m_SongFlags.set(SONG_EXFILTERRANGE);
		}
		m_SndFile.SetDefaultPlaybackBehaviour(GetModType());

		// Refresh mix levels now that the correct mod type has been set
		m_SndFile.SetMixLevels(m_SndFile.GetModSpecifications().defaultMixLevels);

		m_SndFile.Order().assign(1, 0);
		if (!m_SndFile.Patterns.IsValidPat(0))
		{
			m_SndFile.Patterns.Insert(0, 64);
		}

		Clear(m_SndFile.m_szNames);

		m_SndFile.m_PlayState.m_nMusicTempo.Set(125);
		m_SndFile.m_nDefaultTempo.Set(125);
		m_SndFile.m_PlayState.m_nMusicSpeed = m_SndFile.m_nDefaultSpeed = 6;

		// Set up mix levels
		m_SndFile.m_PlayState.m_nGlobalVolume = m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
		m_SndFile.m_nSamplePreAmp = m_SndFile.m_nVSTiVolume = 48;

		for (CHANNELINDEX nChn = 0; nChn < MAX_BASECHANNELS; nChn++)
		{
			m_SndFile.ChnSettings[nChn].dwFlags.reset();
			m_SndFile.ChnSettings[nChn].nVolume = 64;
			m_SndFile.ChnSettings[nChn].nPan = 128;
			m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = 64;
		}
		// Setup LRRL panning scheme for MODs
		m_SndFile.SetupMODPanning();
	}
	if (!m_SndFile.m_nSamples)
	{
		m_SndFile.m_szNames[1] = "untitled";
		m_SndFile.m_nSamples = (GetModType() == MOD_TYPE_MOD) ? 31 : 1;

		SampleEdit::ResetSamples(m_SndFile, SampleEdit::SmpResetInit);

		m_SndFile.GetSample(1).Initialize(m_SndFile.GetType());

		if ((!m_SndFile.m_nInstruments) && (m_SndFile.GetType() & MOD_TYPE_XM))
		{
			if(m_SndFile.AllocateInstrument(1, 1))
			{
				m_SndFile.m_nInstruments = 1;
				InitializeInstrument(m_SndFile.Instruments[1]);
			}
		}
		if (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))
		{
			m_SndFile.m_SongFlags.set(SONG_LINEARSLIDES);
		}
	}
	m_SndFile.ResetPlayPos();
	m_SndFile.m_songArtist = TrackerSettings::Instance().defaultArtist;
}


bool CModDoc::SetDefaultChannelColors(CHANNELINDEX minChannel, CHANNELINDEX maxChannel)
{
	LimitMax(minChannel, GetNumChannels());
	LimitMax(maxChannel, GetNumChannels());
	if(maxChannel < minChannel)
		std::swap(minChannel, maxChannel);
	bool modified = false;
	if(TrackerSettings::Instance().defaultRainbowChannelColors != DefaultChannelColors::NoColors)
	{
		const bool rainbow = TrackerSettings::Instance().defaultRainbowChannelColors == DefaultChannelColors::Rainbow;
		CHANNELINDEX numGroups = 0;
		if(rainbow)
		{
			for(CHANNELINDEX i = minChannel + 1u; i < maxChannel; i++)
			{
				if(m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName)
					numGroups++;
			}
		}
		const double hueFactor = rainbow ? (1.5 * mpt::numbers::pi) / std::max(1, numGroups - 1) : 1000.0;  // Three quarters of the color wheel, red to purple
		for(CHANNELINDEX i = minChannel, group = minChannel; i < maxChannel; i++)
		{
			if(i > minChannel && (m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName))
				group++;
			const double hue = group * hueFactor;  // 0...2pi
			const double saturation = 0.3;         // 0...2/3
			const double brightness = 1.2;         // 0...4/3
			const double r = brightness * (1 + saturation * (std::cos(hue) - 1.0));
			const double g = brightness * (1 + saturation * (std::cos(hue - 2.09439) - 1.0));
			const double b = brightness * (1 + saturation * (std::cos(hue + 2.09439) - 1.0));
			const auto color = RGB(mpt::saturate_round<uint8>(r * 255), mpt::saturate_round<uint8>(g * 255), mpt::saturate_round<uint8>(b * 255));
			if(m_SndFile.ChnSettings[i].color != color)
			{
				m_SndFile.ChnSettings[i].color = color;
				modified = true;
			}
		}
	} else
	{
		for(CHANNELINDEX i = minChannel; i < maxChannel; i++)
		{
			if(m_SndFile.ChnSettings[i].color != ModChannelSettings::INVALID_COLOR)
			{
				m_SndFile.ChnSettings[i].color = ModChannelSettings::INVALID_COLOR;
				modified = true;
			}
		}
	}
	return modified;
}


void CModDoc::PostMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	POSITION pos = GetFirstViewPosition();
	while(pos != nullptr)
	{
		if(CView *pView = GetNextView(pos); pView != nullptr)
			pView->PostMessage(uMsg, wParam, lParam);
	}
}


void CModDoc::SendNotifyMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	POSITION pos = GetFirstViewPosition();
	while(pos != nullptr)
	{
		if(CView *pView = GetNextView(pos); pView != nullptr)
			pView->SendNotifyMessage(uMsg, wParam, lParam);
	}
}


void CModDoc::SendMessageToActiveView(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr)
	{
		lastActiveFrame->SendMessageToDescendants(uMsg, wParam, lParam);
	}
}


void CModDoc::ViewPattern(UINT nPat, UINT nOrd)
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, ((nPat+1) << 16) | nOrd);
}


void CModDoc::ViewSample(UINT nSmp)
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, nSmp);
}


void CModDoc::ViewInstrument(UINT nIns)
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, nIns);
}


ScopedLogCapturer::ScopedLogCapturer(CModDoc &modDoc, const CString &title, CWnd *parent, bool showLog) :
m_modDoc(modDoc), m_oldLogMode(m_modDoc.GetLogMode()), m_title(title), m_pParent(parent), m_showLog(showLog)
{
	m_modDoc.SetLogMode(LogModeGather);
}


void ScopedLogCapturer::ShowLog(bool force)
{
	if(force || m_oldLogMode == LogModeInstantReporting)
	{
		m_modDoc.ShowLog(m_title, m_pParent);
		m_modDoc.ClearLog();
	}
}


void ScopedLogCapturer::ShowLog(const std::string &preamble, bool force)
{
	if(force || m_oldLogMode == LogModeInstantReporting)
	{
		m_modDoc.ShowLog(mpt::ToCString(mpt::Charset::Locale, preamble), m_title, m_pParent);
		m_modDoc.ClearLog();
	}
}


void ScopedLogCapturer::ShowLog(const CString &preamble, bool force)
{
	if(force || m_oldLogMode == LogModeInstantReporting)
	{
		m_modDoc.ShowLog(preamble, m_title, m_pParent);
		m_modDoc.ClearLog();
	}
}


void ScopedLogCapturer::ShowLog(const mpt::ustring &preamble, bool force)
{
	if(force || m_oldLogMode == LogModeInstantReporting)
	{
		m_modDoc.ShowLog(mpt::ToCString(preamble), m_title, m_pParent);
		m_modDoc.ClearLog();
	}
}


ScopedLogCapturer::~ScopedLogCapturer()
{
	if(m_showLog)
		ShowLog();
	else
		m_modDoc.ClearLog();
	m_modDoc.SetLogMode(m_oldLogMode);
}


void CModDoc::AddToLog(LogLevel level, const mpt::ustring &text) const
{
	if(m_LogMode == LogModeGather)
	{
		m_Log.push_back(LogEntry(level, text));
	} else
	{
		if(level < LogDebug)
		{
			Reporting::Message(level, text);
		}
	}
}


mpt::ustring CModDoc::GetLogString() const
{
	mpt::ustring ret;
	for(const auto &i : m_Log)
	{
		ret += i.message;
		ret += U_("\r\n");
	}
	return ret;
}


LogLevel CModDoc::GetMaxLogLevel() const
{
	LogLevel retval = LogInformation;
	// find the most severe loglevel
	for(const auto &i : m_Log)
	{
		retval = std::min(retval, i.level);
	}
	return retval;
}


void CModDoc::ClearLog()
{
	m_Log.clear();
}


UINT CModDoc::ShowLog(const CString &preamble, const CString &title, CWnd *parent)
{
	if(!parent) parent = CMainFrame::GetMainFrame();
	if(GetLog().size() > 0)
	{
		LogLevel level = GetMaxLogLevel();
		if(level < LogDebug)
		{
			CString text = preamble + mpt::ToCString(GetLogString());
			CString actualTitle = (title.GetLength() == 0) ? CString(MAINFRAME_TITLE) : title;
			Reporting::Message(level, text, actualTitle, parent);
			return IDOK;
		}
	}
	return IDCANCEL;
}


void CModDoc::ProcessMIDI(uint32 midiData, INSTRUMENTINDEX ins, IMixPlugin *plugin, InputTargetContext ctx)
{
	static uint8 midiVolume = 127;

	MIDIEvents::EventType event  = MIDIEvents::GetTypeFromEvent(midiData);
	const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData);
	const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData);
	const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData);
	uint8 note  = midiByte1 + NOTE_MIN;
	int vol = midiByte2;

	if((event == MIDIEvents::evNoteOn) && !vol)
		event = MIDIEvents::evNoteOff;  //Convert event to note-off if req'd

	PLUGINDEX mappedIndex = 0;
	PlugParamIndex paramIndex = 0;
	uint16 paramValue = 0;
	bool captured = m_SndFile.GetMIDIMapper().OnMIDImsg(midiData, mappedIndex, paramIndex, paramValue);

	// Handle MIDI messages assigned to shortcuts
	CInputHandler *ih = CMainFrame::GetInputHandler();
	if(ih->HandleMIDIMessage(ctx, midiData) != kcNull
		|| ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull)
	{
		// Mapped to a command, no need to pass message on.
		captured = true;
	}

	if(captured)
	{
		// Event captured by MIDI mapping or shortcut, no need to pass message on.
		return;
	}

	switch(event)
	{
	case MIDIEvents::evNoteOff:
		if(m_midiSustainActive[channel])
		{
			m_midiSustainBuffer[channel].push_back(midiData);
			return;
		}
		if(ins > 0 && ins <= GetNumInstruments())
		{
			LimitMax(note, NOTE_MAX);
			if(m_midiPlayingNotes[channel][note])
				m_midiPlayingNotes[channel][note] = false;
			NoteOff(note, false, ins, m_noteChannel[note - NOTE_MIN]);
			return;
		} else if(plugin != nullptr)
		{
			plugin->MidiSend(midiData);
		}
		break;

	case MIDIEvents::evNoteOn:
		if(ins > 0 && ins <= GetNumInstruments())
		{
			LimitMax(note, NOTE_MAX);
			vol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume);
			PlayNote(PlayNoteParam(note).Instrument(ins).Volume(vol).CheckNNA(m_midiPlayingNotes[channel]), &m_noteChannel);
			return;
		} else if(plugin != nullptr)
		{
			plugin->MidiSend(midiData);
		}
		break;

	case MIDIEvents::evControllerChange:
		switch(midiByte1)
		{
		case MIDIEvents::MIDICC_Volume_Coarse:
			midiVolume = midiByte2;
			break;
		case MIDIEvents::MIDICC_HoldPedal_OnOff:
			m_midiSustainActive[channel] = (midiByte2 >= 0x40);
			if(!m_midiSustainActive[channel])
			{
				// Release all notes
				for(const auto offEvent : m_midiSustainBuffer[channel])
				{
					ProcessMIDI(offEvent, ins, plugin, ctx);
				}
				m_midiSustainBuffer[channel].clear();
			}
			break;
		}
		break;
	}

	if((TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDITOPLUG) && CMainFrame::GetMainFrame()->GetModPlaying() == this && plugin != nullptr)
	{
		plugin->MidiSend(midiData);
		// Sending midi may modify the plug. For now, if MIDI data is not active sensing or aftertouch messages, set modified.
		if(midiData != MIDIEvents::System(MIDIEvents::sysActiveSense)
			&& event != MIDIEvents::evPolyAftertouch && event != MIDIEvents::evChannelAftertouch
			&& event != MIDIEvents::evPitchBend
			&& m_SndFile.GetModSpecifications().supportsPlugins)
		{
			SetModified();
		}
	}
}


CHANNELINDEX CModDoc::PlayNote(PlayNoteParam &params, NoteToChannelMap *noteChannel)
{
	CHANNELINDEX channel = GetNumChannels();

	ModCommand::NOTE note = params.m_note;
	if(ModCommand::IsNote(ModCommand::NOTE(note)))
	{
		CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
		if(pMainFrm == nullptr || note == NOTE_NONE) return CHANNELINDEX_INVALID;
		if (pMainFrm->GetModPlaying() != this)
		{
			// All notes off when resuming paused playback
			m_SndFile.ResetChannels();

			m_SndFile.m_SongFlags.set(SONG_PAUSED);
			pMainFrm->PlayMod(this);
		}

		CriticalSection cs;

		if(params.m_notesPlaying)
			CheckNNA(note, params.m_instr, *params.m_notesPlaying);

		// Find a channel to play on
		channel = FindAvailableChannel();
		ModChannel &chn = m_SndFile.m_PlayState.Chn[channel];

		// reset channel properties; in theory the chan is completely unused anyway.
		chn.Reset(ModChannel::resetTotal, m_SndFile, CHANNELINDEX_INVALID, CHN_MUTE);
		chn.nNewNote = chn.nLastNote = static_cast<uint8>(note);
		chn.nVolume = 256;

		if(params.m_instr)
		{
			// Set instrument (or sample if there are no instruments)
			chn.ResetEnvelopes();
			m_SndFile.InstrumentChange(chn, params.m_instr);
		} else if(params.m_sample > 0 && params.m_sample <= GetNumSamples())	// Or set sample explicitely
		{
			ModSample &sample = m_SndFile.GetSample(params.m_sample);
			chn.pCurrentSample = sample.samplev();
			chn.pModInstrument = nullptr;
			chn.pModSample = &sample;
			chn.nFineTune = sample.nFineTune;
			chn.nC5Speed = sample.nC5Speed;
			chn.nLoopStart = sample.nLoopStart;
			chn.nLoopEnd = sample.nLoopEnd;
			chn.dwFlags = (sample.uFlags & (CHN_SAMPLEFLAGS & ~CHN_MUTE));
			chn.nPan = 128;
			if(sample.uFlags[CHN_PANNING]) chn.nPan = sample.nPan;
			chn.UpdateInstrumentVolume(&sample, nullptr);
		}
		chn.nFadeOutVol = 0x10000;
		chn.isPreviewNote = true;
		if(params.m_currentChannel != CHANNELINDEX_INVALID)
			chn.nMasterChn = params.m_currentChannel + 1;
		else
			chn.nMasterChn = 0;

		if(chn.dwFlags[CHN_ADLIB] && chn.pModSample && m_SndFile.m_opl)
		{
			m_SndFile.m_opl->Patch(channel, chn.pModSample->adlib);
		}

		m_SndFile.NoteChange(chn, note, false, true, true, channel);
		if(params.m_volume >= 0) chn.nVolume = std::min(params.m_volume, 256);

		// Handle sample looping.
		// Changed line to fix http://forum.openmpt.org/index.php?topic=1700.0
		//if ((loopstart + 16 < loopend) && (loopstart >= 0) && (loopend <= (LONG)pchn.nLength))
		if ((params.m_loopStart + 16 < params.m_loopEnd) && (params.m_loopStart >= 0) && (chn.pModSample != nullptr))
		{
			chn.position.Set(params.m_loopStart);
			chn.nLoopStart = params.m_loopStart;
			chn.nLoopEnd = params.m_loopEnd;
			chn.nLength = std::min(params.m_loopEnd, chn.pModSample->nLength);
		}

		// Handle extra-loud flag
		chn.dwFlags.set(CHN_EXTRALOUD, !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOEXTRALOUD) && params.m_sample);

		// Handle custom start position
		if(params.m_sampleOffset > 0 && chn.pModSample)
		{
			chn.position.Set(params.m_sampleOffset);
			// If start position is after loop end, set loop end to sample end so that the sample starts
			// playing.
			if(chn.nLoopEnd < params.m_sampleOffset)
				chn.nLength = chn.nLoopEnd = chn.pModSample->nLength;
		}

		// VSTi preview
		if(params.m_instr > 0 && params.m_instr <= m_SndFile.GetNumInstruments())
		{
			const ModInstrument *pIns = m_SndFile.Instruments[params.m_instr];
			if (pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan
			{
				PLUGINDEX nPlugin = 0;
				if (chn.pModInstrument)
					nPlugin = chn.pModInstrument->nMixPlug;					// First try instrument plugin
				if ((!nPlugin || nPlugin > MAX_MIXPLUGINS) && params.m_currentChannel != CHANNELINDEX_INVALID)
					nPlugin = m_SndFile.ChnSettings[params.m_currentChannel].nMixPlugin;	// Then try channel plugin

				if ((nPlugin) && (nPlugin <= MAX_MIXPLUGINS))
				{
					IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[nPlugin - 1].pMixPlugin;
					if(pPlugin != nullptr)
					{
						pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN], static_cast<uint16>(chn.nVolume), channel);
					}
				}
			}
		}

		// Remove channel from list of mixed channels to fix https://bugs.openmpt.org/view.php?id=209
		// This is required because a previous note on the same channel might have just stopped playing,
		// but the channel is still in the mix list.
		// Since the channel volume / etc is only updated every tick in CSoundFile::ReadNote, and we
		// do not want to duplicate mixmode-dependant logic here, CSoundFile::CreateStereoMix may already
		// try to mix our newly set up channel at volume 0 if we don't remove it from the list.
		auto mixBegin = std::begin(m_SndFile.m_PlayState.ChnMix);
		auto mixEnd = std::remove(mixBegin, mixBegin + m_SndFile.m_nMixChannels, channel);
		m_SndFile.m_nMixChannels = static_cast<CHANNELINDEX>(std::distance(mixBegin, mixEnd));

		if(noteChannel)
		{
			noteChannel->at(note - NOTE_MIN) = channel;
		}
	} else
	{
		CriticalSection cs;
		// Apply note cut / off / fade (also on preview channels)
		m_SndFile.NoteChange(m_SndFile.m_PlayState.Chn[channel], note);
		for(CHANNELINDEX c = m_SndFile.GetNumChannels(); c < MAX_CHANNELS; c++)
		{
			ModChannel &chn = m_SndFile.m_PlayState.Chn[c];
			if(chn.isPreviewNote && (chn.pModSample || chn.pModInstrument))
			{
				m_SndFile.NoteChange(chn, note);
			}
		}
	}
	return channel;
}


bool CModDoc::NoteOff(UINT note, bool fade, INSTRUMENTINDEX ins, CHANNELINDEX currentChn)
{
	CriticalSection cs;

	if(ins != INSTRUMENTINDEX_INVALID && ins <= m_SndFile.GetNumInstruments() && ModCommand::IsNote(ModCommand::NOTE(note)))
	{
		const ModInstrument *pIns = m_SndFile.Instruments[ins];
		if(pIns && pIns->HasValidMIDIChannel())	// instro sends to a midi chan
		{
			PLUGINDEX plug = pIns->nMixPlug;      // First try intrument VST
			if((!plug || plug > MAX_MIXPLUGINS)   // No good plug yet
				&& currentChn < MAX_BASECHANNELS) // Chan OK
			{
				plug = m_SndFile.ChnSettings[currentChn].nMixPlugin;// Then try Channel VST
			}

			if(plug && plug <= MAX_MIXPLUGINS)
			{
				IMixPlugin *pPlugin =  m_SndFile.m_MixPlugins[plug - 1].pMixPlugin;
				if(pPlugin)
				{
					pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN] + NOTE_KEYOFF, 0, currentChn);
				}
			}
		}
	}

	const FlagSet<ChannelFlags> mask = (fade ? CHN_NOTEFADE : (CHN_NOTEFADE | CHN_KEYOFF));
	const CHANNELINDEX startChn = currentChn != CHANNELINDEX_INVALID ? currentChn : m_SndFile.m_nChannels;
	const CHANNELINDEX endChn = currentChn != CHANNELINDEX_INVALID ? currentChn + 1 : MAX_CHANNELS;
	ModChannel *pChn = &m_SndFile.m_PlayState.Chn[startChn];
	for(CHANNELINDEX i = startChn; i < endChn; i++, pChn++)
	{
		// Fade all channels > m_nChannels which are playing this note and aren't NNA channels.
		if((pChn->isPreviewNote || i < m_SndFile.GetNumChannels())
			&& !pChn->dwFlags[mask]
			&& (pChn->nLength || pChn->dwFlags[CHN_ADLIB])
			&& (note == pChn->nNewNote || note == NOTE_NONE))
		{
			m_SndFile.KeyOff(*pChn);
			if (!m_SndFile.m_nInstruments) pChn->dwFlags.reset(CHN_LOOP | CHN_PINGPONGFLAG);
			if (fade) pChn->dwFlags.set(CHN_NOTEFADE);
			// Instantly stop samples that would otherwise play forever
			if (pChn->pModInstrument && !pChn->pModInstrument->nFadeOut)
				pChn->nFadeOutVol = 0;
			if(pChn->dwFlags[CHN_ADLIB] && m_SndFile.m_opl)
			{
				m_SndFile.m_opl->NoteOff(i);
			}
			if (note) break;
		}
	}

	return true;
}


// Apply DNA/NNA settings for note preview. It will also set the specified note to be playing in the playingNotes set.
void CModDoc::CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes)
{
	if(ins > GetNumInstruments() || m_SndFile.Instruments[ins] == nullptr || note >= playingNotes.size())
	{
		return;
	}
	const ModInstrument *pIns = m_SndFile.Instruments[ins];
	for(CHANNELINDEX chn = GetNumChannels(); chn < MAX_CHANNELS; chn++)
	{
		const ModChannel &channel = m_SndFile.m_PlayState.Chn[chn];
		if(channel.pModInstrument == pIns && channel.isPreviewNote && ModCommand::IsNote(channel.nLastNote)
			&& (channel.nLength || pIns->HasValidMIDIChannel()) && !playingNotes[channel.nLastNote])
		{
			CHANNELINDEX nnaChn = m_SndFile.CheckNNA(chn, ins, note, false);
			if(nnaChn != CHANNELINDEX_INVALID)
			{
				// Keep the new NNA channel playing in the same channel slot.
				// That way, we do not need to touch the ChnMix array, and we avoid the same channel being checked twice.
				if(nnaChn != chn)
				{
					m_SndFile.m_PlayState.Chn[chn] = std::move(m_SndFile.m_PlayState.Chn[nnaChn]);
					m_SndFile.m_PlayState.Chn[nnaChn] = {};
				}
				// Avoid clicks if the channel wasn't ramping before.
				m_SndFile.m_PlayState.Chn[chn].dwFlags.set(CHN_FASTVOLRAMP);
				m_SndFile.ProcessRamping(m_SndFile.m_PlayState.Chn[chn]);
			}
		}
	}
	playingNotes.set(note);
}


// Check if a given note of an instrument or sample is playing from the editor.
// If note == 0, just check if an instrument or sample is playing.
bool CModDoc::IsNotePlaying(UINT note, SAMPLEINDEX nsmp, INSTRUMENTINDEX nins)
{
	ModChannel *pChn = &m_SndFile.m_PlayState.Chn[m_SndFile.GetNumChannels()];
	for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++, pChn++) if (pChn->isPreviewNote)
	{
		if(pChn->nLength != 0 && !pChn->dwFlags[CHN_NOTEFADE | CHN_KEYOFF| CHN_MUTE]
			&& (note == pChn->nNewNote || note == NOTE_NONE)
			&& (pChn->pModSample == &m_SndFile.GetSample(nsmp) || !nsmp)
			&& (pChn->pModInstrument == m_SndFile.Instruments[nins] || !nins)) return true;
	}
	return false;
}


bool CModDoc::MuteToggleModifiesDocument() const
{
	return (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)) && TrackerSettings::Instance().MiscSaveChannelMuteStatus;
}


bool CModDoc::MuteChannel(CHANNELINDEX nChn, bool doMute)
{
	if (nChn >= m_SndFile.GetNumChannels())
	{
		return false;
	}

	// Mark channel as muted in channel settings
	m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_MUTE, doMute);

	const bool success = UpdateChannelMuteStatus(nChn);
	if(success && MuteToggleModifiesDocument())
	{
		SetModified();
	}

	return success;
}


bool CModDoc::UpdateChannelMuteStatus(CHANNELINDEX nChn)
{
	const ChannelFlags muteType = CSoundFile::GetChannelMuteFlag();

	if (nChn >= m_SndFile.GetNumChannels())
	{
		return false;
	}

	const bool doMute = m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];

	// Mute pattern channel
	if (doMute)
	{
		m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(muteType);
		if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(nChn);
		// Kill VSTi notes on muted channel.
		PLUGINDEX nPlug = m_SndFile.GetBestPlugin(m_SndFile.m_PlayState, nChn, PrioritiseInstrument, EvenIfMuted);
		if ((nPlug) && (nPlug<=MAX_MIXPLUGINS))
		{
			IMixPlugin *pPlug = m_SndFile.m_MixPlugins[nPlug - 1].pMixPlugin;
			const ModInstrument* pIns = m_SndFile.m_PlayState.Chn[nChn].pModInstrument;
			if (pPlug && pIns)
			{
				pPlug->MidiCommand(*pIns, NOTE_KEYOFF, 0, nChn);
			}
		}
	} else
	{
		// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
		m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
	}

	// Mute any NNA'd channels
	for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
	{
		if (m_SndFile.m_PlayState.Chn[i].nMasterChn == nChn + 1u)
		{
			if (doMute)
			{
				m_SndFile.m_PlayState.Chn[i].dwFlags.set(muteType);
				if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(i);
			} else
			{
				// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
				m_SndFile.m_PlayState.Chn[i].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
			}
		}
	}

	return true;
}


bool CModDoc::IsChannelSolo(CHANNELINDEX nChn) const
{
	if (nChn >= m_SndFile.m_nChannels) return true;
	return m_SndFile.ChnSettings[nChn].dwFlags[CHN_SOLO];
}

bool CModDoc::SoloChannel(CHANNELINDEX nChn, bool bSolo)
{
	if (nChn >= m_SndFile.m_nChannels) return false;
	if (MuteToggleModifiesDocument()) SetModified();
	m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SOLO, bSolo);
	return true;
}


bool CModDoc::IsChannelNoFx(CHANNELINDEX nChn) const
{
	if (nChn >= m_SndFile.m_nChannels) return true;
	return m_SndFile.ChnSettings[nChn].dwFlags[CHN_NOFX];
}


bool CModDoc::NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix)
{
	if (nChn >= m_SndFile.m_nChannels) return false;
	m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_NOFX, bNoFx);
	if(updateMix) m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_NOFX, bNoFx);
	return true;
}


RecordGroup CModDoc::GetChannelRecordGroup(CHANNELINDEX channel) const
{
	if(channel >= GetNumChannels())
		return RecordGroup::NoGroup;
	if(m_bsMultiRecordMask[channel])
		return RecordGroup::Group1;
	if(m_bsMultiSplitRecordMask[channel])
		return RecordGroup::Group2;
	return RecordGroup::NoGroup;
}


void CModDoc::SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
{
	if(channel >= GetNumChannels())
		return;
	m_bsMultiRecordMask.set(channel, recordGroup == RecordGroup::Group1);
	m_bsMultiSplitRecordMask.set(channel, recordGroup == RecordGroup::Group2);
}


void CModDoc::ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
{
	if(channel >= GetNumChannels())
		return;
	if(recordGroup == RecordGroup::Group1)
	{
		m_bsMultiRecordMask.flip(channel);
		m_bsMultiSplitRecordMask.reset(channel);
	} else if(recordGroup == RecordGroup::Group2)
	{
		m_bsMultiRecordMask.reset(channel);
		m_bsMultiSplitRecordMask.flip(channel);
	}
}


void CModDoc::ReinitRecordState(bool unselect)
{
	if(unselect)
	{
		m_bsMultiRecordMask.reset();
		m_bsMultiSplitRecordMask.reset();
	} else
	{
		m_bsMultiRecordMask.set();
		m_bsMultiSplitRecordMask.set();
	}
}


bool CModDoc::MuteSample(SAMPLEINDEX nSample, bool bMute)
{
	if ((nSample < 1) || (nSample > m_SndFile.GetNumSamples())) return false;
	m_SndFile.GetSample(nSample).uFlags.set(CHN_MUTE, bMute);
	return true;
}


bool CModDoc::MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute)
{
	if ((nInstr < 1) || (nInstr > m_SndFile.GetNumInstruments()) || (!m_SndFile.Instruments[nInstr])) return false;
	m_SndFile.Instruments[nInstr]->dwFlags.set(INS_MUTE, bMute);
	return true;
}


bool CModDoc::SurroundChannel(CHANNELINDEX nChn, bool surround)
{
	if(nChn >= m_SndFile.GetNumChannels()) return false;

	if(!(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) surround = false;

	if(surround != m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
	{
		// Update channel configuration
		if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();

		m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SURROUND, surround);
		if(surround)
		{
			m_SndFile.ChnSettings[nChn].nPan = 128;
		}
	}

	// Update playing channel
	m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_SURROUND, surround);
	if(surround)
	{
		m_SndFile.m_PlayState.Chn[nChn].nPan = 128;
	}
	return true;
}


bool CModDoc::SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume)
{
	bool ok = false;
	if(nChn >= m_SndFile.GetNumChannels() || nVolume > 64) return false;
	if(m_SndFile.ChnSettings[nChn].nVolume != nVolume)
	{
		m_SndFile.ChnSettings[nChn].nVolume = nVolume;
		if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
		ok = true;
	}
	m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = nVolume;
	return ok;
}


bool CModDoc::SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan)
{
	bool ok = false;
	if(nChn >= m_SndFile.GetNumChannels() || nPan > 256) return false;
	if(m_SndFile.ChnSettings[nChn].nPan != nPan || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
	{
		m_SndFile.ChnSettings[nChn].nPan = nPan;
		m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND);
		if(m_SndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
		ok = true;
	}
	m_SndFile.m_PlayState.Chn[nChn].nPan = nPan;
	m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SURROUND);
	return ok;
}


bool CModDoc::IsChannelMuted(CHANNELINDEX nChn) const
{
	if(nChn >= m_SndFile.GetNumChannels()) return true;
	return m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];
}


bool CModDoc::IsSampleMuted(SAMPLEINDEX nSample) const
{
	if(!nSample || nSample > m_SndFile.GetNumSamples()) return false;
	return m_SndFile.GetSample(nSample).uFlags[CHN_MUTE];
}


bool CModDoc::IsInstrumentMuted(INSTRUMENTINDEX nInstr) const
{
	if(!nInstr || nInstr > m_SndFile.GetNumInstruments() || !m_SndFile.Instruments[nInstr]) return false;
	return m_SndFile.Instruments[nInstr]->dwFlags[INS_MUTE];
}


UINT CModDoc::GetPatternSize(PATTERNINDEX nPat) const
{
	if(m_SndFile.Patterns.IsValidIndex(nPat)) return m_SndFile.Patterns[nPat].GetNumRows();
	return 0;
}


void CModDoc::SetFollowWnd(HWND hwnd)
{
	m_hWndFollow = hwnd;
}


bool CModDoc::IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const
{
	return m_SndFile.IsSampleReferencedByInstrument(nSmp, nIns);
}


// Find an instrument that references the given sample.
// If no such instrument is found, INSTRUMENTINDEX_INVALID is returned.
INSTRUMENTINDEX CModDoc::FindSampleParent(SAMPLEINDEX sample) const
{
	if(sample == 0)
	{
		return INSTRUMENTINDEX_INVALID;
	}
	for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++)
	{
		const ModInstrument *pIns = m_SndFile.Instruments[i];
		if(pIns != nullptr)
		{
			for(size_t j = 0; j < NOTE_MAX; j++)
			{
				if(pIns->Keyboard[j] == sample)
				{
					return i;
				}
			}
		}
	}
	return INSTRUMENTINDEX_INVALID;
}


SAMPLEINDEX CModDoc::FindInstrumentChild(INSTRUMENTINDEX nIns) const
{
	if ((!nIns) || (nIns > m_SndFile.GetNumInstruments())) return 0;
	const ModInstrument *pIns = m_SndFile.Instruments[nIns];
	if (pIns)
	{
		for (auto n : pIns->Keyboard)
		{
			if ((n) && (n <= m_SndFile.GetNumSamples())) return n;
		}
	}
	return 0;
}


LRESULT CModDoc::ActivateView(UINT nIdView, DWORD dwParam)
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (!pMainFrm) return 0;
	CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
	if (pMDIActive)
	{
		CView *pView = pMDIActive->GetActiveView();
		if ((pView) && (pView->GetDocument() == this))
		{
			return ((CChildFrame *)pMDIActive)->ActivateView(nIdView, dwParam);
		}
	}
	POSITION pos = GetFirstViewPosition();
	while (pos != NULL)
	{
		CView *pView = GetNextView(pos);
		if ((pView) && (pView->GetDocument() == this))
		{
			CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
			pChildFrm->MDIActivate();
			return pChildFrm->ActivateView(nIdView, dwParam);
		}
	}
	return 0;
}


// Activate document's window.
void CModDoc::ActivateWindow()
{

	CChildFrame *pChildFrm = GetChildFrame();
	if(pChildFrm) pChildFrm->MDIActivate();
}


void CModDoc::UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint)
{
	// Tunnel our UpdateHint into an LPARAM
	CDocument::UpdateAllViews(pSender, hint.AsLPARAM(), pHint);
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm) pMainFrm->UpdateTree(this, hint, pHint);
	
	if(hint.GetType()[HINT_MODCHANNELS | HINT_MODTYPE])
	{
		auto instance = CChannelManagerDlg::sharedInstance();
		if(instance != nullptr && pHint != instance && instance->GetDocument() == this)
			instance->Update(hint, pHint);
	}
#ifndef NO_PLUGINS
	if(hint.GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES])
	{
		for(auto &plug : m_SndFile.m_MixPlugins)
		{
			auto mixPlug = plug.pMixPlugin;
			if(mixPlug != nullptr && mixPlug->GetEditor())
			{
				mixPlug->GetEditor()->UpdateView(hint);
			}
		}
	}
#endif
}


void CModDoc::UpdateAllViews(UpdateHint hint)
{
	CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_UPDATEVIEWS, reinterpret_cast<WPARAM>(this), hint.AsLPARAM());
}


/////////////////////////////////////////////////////////////////////////////
// CModDoc commands

void CModDoc::OnFileWaveConvert()
{
	OnFileWaveConvert(ORDERINDEX_INVALID, ORDERINDEX_INVALID);
}


void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories)
{
	ASSERT(!encFactories.empty());

	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();

	if ((!pMainFrm) || (!m_SndFile.GetType()) || encFactories.empty()) return;

	CWaveConvert wsdlg(pMainFrm, nMinOrder, nMaxOrder, m_SndFile.Order().GetLengthTailTrimmed() - 1, m_SndFile, encFactories);
	{
		BypassInputHandler bih;
		if (wsdlg.DoModal() != IDOK) return;
	}

	EncoderFactoryBase *encFactory = wsdlg.m_Settings.GetEncoderFactory();

	const mpt::PathString extension = encFactory->GetTraits().fileExtension;

	FileDialog dlg = SaveFileDialog()
		.DefaultExtension(extension)
		.DefaultFilename(GetPathNameMpt().GetFileName() + P_(".") + extension)
		.ExtensionFilter(encFactory->GetTraits().fileDescription + U_(" (*.") + extension.ToUnicode() + U_(")|*.") + extension.ToUnicode() + U_("||"))
		.WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir());
	if(!wsdlg.m_Settings.outputToSample && !dlg.Show()) return;

	// will set default dir here because there's no setup option for export dir yet (feel free to add one...)
	TrackerSettings::Instance().PathExport.SetDefaultDir(dlg.GetWorkingDirectory(), true);

	mpt::PathString drive, dir, name, ext;
	dlg.GetFirstFile().SplitPath(&drive, &dir, &name, &ext);
	const mpt::PathString fileName = drive + dir + name;
	const mpt::PathString fileExt = ext;

	const ORDERINDEX currentOrd = m_SndFile.m_PlayState.m_nCurrentOrder;
	const ROWINDEX  currentRow = m_SndFile.m_PlayState.m_nRow;

	int nRenderPasses = 1;
	// Channel mode
	std::vector<bool> usedChannels;
	std::vector<FlagSet<ChannelFlags>> channelFlags;
	// Instrument mode
	std::vector<bool> instrMuteState;

	// CHN_SYNCMUTE is used with formats where CHN_MUTE would stop processing global effects and could thus mess synchronization between exported channels
	const ChannelFlags muteFlag = m_SndFile.m_playBehaviour[kST3NoMutedChannels] ? CHN_SYNCMUTE : CHN_MUTE;

	// Channel mode: save song in multiple wav files (one for each enabled channels)
	if(wsdlg.m_bChannelMode)
	{
		// Don't save empty channels
		CheckUsedChannels(usedChannels);

		nRenderPasses = m_SndFile.GetNumChannels();
		channelFlags.resize(nRenderPasses, ChannelFlags(0));
		for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
		{
			// Save channels' flags
			channelFlags[i] = m_SndFile.ChnSettings[i].dwFlags;
			// Ignore muted channels
			if(channelFlags[i][CHN_MUTE]) usedChannels[i] = false;
			// Mute each channel
			m_SndFile.ChnSettings[i].dwFlags.set(muteFlag);
		}
	}
	// Instrument mode: Same as channel mode, but renders per instrument (or sample)
	if(wsdlg.m_bInstrumentMode)
	{
		if(m_SndFile.GetNumInstruments() == 0)
		{
			nRenderPasses = m_SndFile.GetNumSamples();
			instrMuteState.resize(nRenderPasses, false);
			for(SAMPLEINDEX i = 0; i < m_SndFile.GetNumSamples(); i++)
			{
				instrMuteState[i] = IsSampleMuted(i + 1);
				MuteSample(i + 1, true);
			}
		} else
		{
			nRenderPasses = m_SndFile.GetNumInstruments();
			instrMuteState.resize(nRenderPasses, false);
			for(INSTRUMENTINDEX i = 0; i < m_SndFile.GetNumInstruments(); i++)
			{
				instrMuteState[i] = IsInstrumentMuted(i + 1);
				MuteInstrument(i + 1, true);
			}
		}
	}

	pMainFrm->PauseMod(this);
	int oldRepeat = m_SndFile.GetRepeatCount();

	const SEQUENCEINDEX currentSeq = m_SndFile.Order.GetCurrentSequenceIndex();
	for(SEQUENCEINDEX seq = wsdlg.m_Settings.minSequence; seq <= wsdlg.m_Settings.maxSequence; seq++)
	{
		m_SndFile.Order.SetSequence(seq);
		mpt::ustring fileNameAdd;
		for(int i = 0; i < nRenderPasses; i++)
		{
			mpt::PathString thisName = fileName;
			CString caption = _T("file");
			fileNameAdd.clear();
			if(wsdlg.m_Settings.minSequence != wsdlg.m_Settings.maxSequence)
			{
				fileNameAdd = MPT_UFORMAT("-{}")(mpt::ufmt::dec0<2>(seq + 1));
				mpt::ustring seqName = m_SndFile.Order(seq).GetName();
				if(!seqName.empty())
				{
					fileNameAdd += UL_("-") + seqName;
				}
			}

			// Channel mode
			if(wsdlg.m_bChannelMode)
			{
				// Re-mute previously processed channel
				if(i > 0)
					m_SndFile.ChnSettings[i - 1].dwFlags.set(muteFlag);

				// Was this channel actually muted? Don't process it then.
				if(!usedChannels[i])
					continue;

				// Add channel number & name (if available) to path string
				if(!m_SndFile.ChnSettings[i].szName.empty())
				{
					fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
					caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
				} else
				{
					fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
					caption = MPT_CFORMAT("channel {}")(i + 1);
				}
				// Unmute channel to process
				m_SndFile.ChnSettings[i].dwFlags.reset(muteFlag);
			}
			// Instrument mode
			if(wsdlg.m_bInstrumentMode)
			{
				if(m_SndFile.GetNumInstruments() == 0)
				{
					// Re-mute previously processed sample
					if(i > 0) MuteSample(static_cast<SAMPLEINDEX>(i), true);

					if(!m_SndFile.GetSample(static_cast<SAMPLEINDEX>(i + 1)).HasSampleData() || !IsSampleUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
						continue;

					// Add sample number & name (if available) to path string
					if(!m_SndFile.m_szNames[i + 1].empty())
					{
						fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
						caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
					} else
					{
						fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
						caption = MPT_CFORMAT("sample {}")(i + 1);
					}
					// Unmute sample to process
					MuteSample(static_cast<SAMPLEINDEX>(i + 1), false);
				} else
				{
					// Re-mute previously processed instrument
					if(i > 0) MuteInstrument(static_cast<INSTRUMENTINDEX>(i), true);

					if(m_SndFile.Instruments[i + 1] == nullptr || !IsInstrumentUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
						continue;

					if(!m_SndFile.Instruments[i + 1]->name.empty())
					{
						fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
						caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
					} else
					{
						fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
						caption = MPT_CFORMAT("instrument {}")(i + 1);
					}
					// Unmute instrument to process
					MuteInstrument(static_cast<SAMPLEINDEX>(i + 1), false);
				}
			}

			if(!fileNameAdd.empty())
			{
				SanitizeFilename(fileNameAdd);
				thisName += mpt::PathString::FromUnicode(fileNameAdd);
			}
			thisName += fileExt;
			if(wsdlg.m_Settings.outputToSample)
			{
				thisName = mpt::CreateTempFileName(P_("OpenMPT"));
				// Ensure this temporary file is marked as temporary in the file system, to increase the chance it will never be written to disk
				HANDLE hFile = ::CreateFile(thisName.AsNative().c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
				if(hFile != INVALID_HANDLE_VALUE)
				{
					::CloseHandle(hFile);
				}
			}

			// Render song (or current channel, or current sample/instrument)
			bool cancel = true;
			try
			{
				mpt::SafeOutputFile safeFileStream(thisName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
				mpt::ofstream &f = safeFileStream;
				f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);

				if(!f)
				{
					Reporting::Error("Could not open file for writing. Is it open in another application?");
				} else
				{
					BypassInputHandler bih;
					CDoWaveConvert dwcdlg(m_SndFile, f, caption, wsdlg.m_Settings, pMainFrm);
					dwcdlg.m_bGivePlugsIdleTime = wsdlg.m_bGivePlugsIdleTime;
					dwcdlg.m_dwSongLimit = wsdlg.m_dwSongLimit;
					cancel = dwcdlg.DoModal() != IDOK;
				}
			} catch(const std::exception &)
			{
				Reporting::Error(_T("Error while writing file!"));
			}

			if(wsdlg.m_Settings.outputToSample)
			{
				if(!cancel)
				{
					InputFile f(thisName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
					if(f.IsValid())
					{
						FileReader file = GetFileReader(f);
						SAMPLEINDEX smp = wsdlg.m_Settings.sampleSlot;
						if(smp == 0 || smp > GetNumSamples()) smp = m_SndFile.GetNextFreeSample();
						if(smp == SAMPLEINDEX_INVALID)
						{
							Reporting::Error(_T("Too many samples!"));
							cancel = true;
						}
						if(!cancel)
						{
							if(GetNumSamples() < smp) m_SndFile.m_nSamples = smp;
							GetSampleUndo().PrepareUndo(smp, sundo_replace, "Render To Sample");
							if(m_SndFile.ReadSampleFromFile(smp, file, false))
							{
								m_SndFile.m_szNames[smp] = "Render To Sample" + mpt::ToCharset(m_SndFile.GetCharsetInternal(), fileNameAdd);
								UpdateAllViews(nullptr, SampleHint().Info().Data().Names());
								if(m_SndFile.GetNumInstruments() && !IsSampleUsed(smp))
								{
									// Insert new instrument for the generated sample in case it is not referenced by any instruments yet.
									// It should only be already referenced if the user chose to export to an existing sample slot.
									InsertInstrument(smp);
									UpdateAllViews(nullptr, InstrumentHint().Info().Names());
								}
								SetModified();
							} else
							{
								GetSampleUndo().RemoveLastUndoStep(smp);
							}
						}
					}
				}

				// Always clean up after ourselves
				for(int retry = 0; retry < 10; retry++)
				{
					// stupid virus scanners
					if(DeleteFile(thisName.AsNative().c_str()) != EACCES)
					{
						break;
					}
					Sleep(10);
				}
			}

			if(cancel) break;
		}
	}

	// Restore channels' flags
	if(wsdlg.m_bChannelMode)
	{
		for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
		{
			m_SndFile.ChnSettings[i].dwFlags = channelFlags[i];
		}
	}
	// Restore instruments' / samples' flags
	if(wsdlg.m_bInstrumentMode)
	{
		for(size_t i = 0; i < instrMuteState.size(); i++)
		{
			if(m_SndFile.GetNumInstruments() == 0)
				MuteSample(static_cast<SAMPLEINDEX>(i + 1), instrMuteState[i]);
			else
				MuteInstrument(static_cast<INSTRUMENTINDEX>(i + 1), instrMuteState[i]);
		}
	}

	m_SndFile.Order.SetSequence(currentSeq);
	m_SndFile.SetRepeatCount(oldRepeat);
	m_SndFile.GetLength(eAdjust, GetLengthTarget(currentOrd, currentRow));
	m_SndFile.m_PlayState.m_nNextOrder = currentOrd;
	m_SndFile.m_PlayState.m_nNextRow = currentRow;
	CMainFrame::UpdateAudioParameters(m_SndFile, true);
}


void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder)
{
	WAVEncoder wavencoder;
	FLACEncoder flacencoder;
	AUEncoder auencoder;
	OggOpusEncoder opusencoder;
	VorbisEncoder vorbisencoder;
	MP3Encoder mp3lame(MP3EncoderLame);
	MP3Encoder mp3lamecompatible(MP3EncoderLameCompatible);
	RAWEncoder rawencoder;
	std::vector<EncoderFactoryBase*> encoders;
	if(wavencoder.IsAvailable()) encoders.push_back(&wavencoder);
	if(flacencoder.IsAvailable()) encoders.push_back(&flacencoder);
	if(auencoder.IsAvailable()) encoders.push_back(&auencoder);
	if(rawencoder.IsAvailable()) encoders.push_back(&rawencoder);
	if(opusencoder.IsAvailable()) encoders.push_back(&opusencoder);
	if(vorbisencoder.IsAvailable()) encoders.push_back(&vorbisencoder);
	if(mp3lame.IsAvailable())
	{
		encoders.push_back(&mp3lame);
	}
	if(mp3lamecompatible.IsAvailable()) encoders.push_back(&mp3lamecompatible);
	OnFileWaveConvert(nMinOrder, nMaxOrder, encoders);
}


void CModDoc::OnFileMidiConvert()
{
#ifndef NO_PLUGINS
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();

	if ((!pMainFrm) || (!m_SndFile.GetType())) return;

	mpt::PathString filename = GetPathNameMpt().ReplaceExt(P_(".mid"));

	FileDialog dlg = SaveFileDialog()
		.DefaultExtension("mid")
		.DefaultFilename(filename)
		.ExtensionFilter("MIDI Files (*.mid)|*.mid||");
	if(!dlg.Show()) return;

	CModToMidi mididlg(m_SndFile, pMainFrm);
	BypassInputHandler bih;
	if(mididlg.DoModal() == IDOK)
	{
		try
		{
			mpt::SafeOutputFile sf(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
			mpt::ofstream &f = sf;
			f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);

			if(!f.good())
			{
				Reporting::Error("Could not open file for writing. Is it open in another application?");
				return;
			}
			
			CDoMidiConvert doconv(m_SndFile, f, mididlg.m_instrMap);
			doconv.DoModal();
		} catch(const std::exception &)
		{
			Reporting::Error(_T("Error while writing file!"));
		}
	}
#else
	Reporting::Error("In order to use MIDI export, OpenMPT must be built with plugin support.");
#endif // NO_PLUGINS
}

//HACK: This is a quick fix. Needs to be better integrated into player and GUI.
void CModDoc::OnFileCompatibilitySave()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (!pMainFrm) return;

	CString pattern;

	const MODTYPE type = m_SndFile.GetType();
	switch(type)
	{
		case MOD_TYPE_IT:
			pattern = FileFilterIT;
			MsgBoxHidable(CompatExportDefaultWarning);
			break;
		case MOD_TYPE_XM:
			pattern = FileFilterXM;
			MsgBoxHidable(CompatExportDefaultWarning);
			break;
		default:
			// Not available for this format.
			return;
	}

	const std::string ext = m_SndFile.GetModSpecifications().fileExtension;

	mpt::PathString filename;

	{
		mpt::PathString drive;
		mpt::PathString dir;
		mpt::PathString fileName;
		GetPathNameMpt().SplitPath(&drive, &dir, &fileName, nullptr);

		filename = drive;
		filename += dir;
		filename += fileName;
		if(!strstr(fileName.ToUTF8().c_str(), "compat"))
			filename += P_(".compat.");
		else
			filename += P_(".");
		filename += mpt::PathString::FromUTF8(ext);
	}

	FileDialog dlg = SaveFileDialog()
		.DefaultExtension(ext)
		.DefaultFilename(filename)
		.ExtensionFilter(pattern)
		.WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir());
	if(!dlg.Show()) return;

	filename = dlg.GetFirstFile();
	
	bool ok = false;
	BeginWaitCursor();
	try
	{
		mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
		mpt::ofstream &f = sf;
		if(f)
		{
			f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
			ScopedLogCapturer logcapturer(*this);
			FixNullStrings();
			switch(type)
			{
			case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f, true); break;
			case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename, true); break;
			default: MPT_ASSERT_NOTREACHED();
			}
		}
	} catch(const std::exception &)
	{
		ok = false;
	}
	EndWaitCursor();

	if(!ok)
	{
		ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
	}
}


void CModDoc::OnPlayerPlay()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm)
	{
		CChildFrame *pChildFrm = GetChildFrame();
 		if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
		{
			//User has sent play song command: set loop pattern checkbox to false.
			pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
		}

		bool isPlaying = (pMainFrm->GetModPlaying() == this);
		if(isPlaying && !m_SndFile.m_SongFlags[SONG_PAUSED | SONG_STEP/*|SONG_PATTERNLOOP*/])
		{
			OnPlayerPause();
			return;
		}

		CriticalSection cs;

		// Kill editor voices
		for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) if (m_SndFile.m_PlayState.Chn[i].isPreviewNote)
		{
			m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
			if (!isPlaying) m_SndFile.m_PlayState.Chn[i].nLength = 0;
		}

		m_SndFile.m_PlayState.m_bPositionChanged = true;

		if(isPlaying)
		{
			m_SndFile.StopAllVsti();
		}

		cs.Leave();

		m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PAUSED | SONG_PATTERNLOOP);
		pMainFrm->PlayMod(this);
	}
}


void CModDoc::OnPlayerPause()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm)
	{
		if (pMainFrm->GetModPlaying() == this)
		{
			bool isLooping = m_SndFile.m_SongFlags[SONG_PATTERNLOOP];
			PATTERNINDEX nPat = m_SndFile.m_PlayState.m_nPattern;
			ROWINDEX nRow = m_SndFile.m_PlayState.m_nRow;
			ROWINDEX nNextRow = m_SndFile.m_PlayState.m_nNextRow;
			pMainFrm->PauseMod();

			if ((isLooping) && (nPat < m_SndFile.Patterns.Size()))
			{
				CriticalSection cs;

				if ((m_SndFile.m_PlayState.m_nCurrentOrder < m_SndFile.Order().GetLength()) && (m_SndFile.Order()[m_SndFile.m_PlayState.m_nCurrentOrder] == nPat))
				{
					m_SndFile.m_PlayState.m_nNextOrder = m_SndFile.m_PlayState.m_nCurrentOrder;
					m_SndFile.m_PlayState.m_nNextRow = nNextRow;
					m_SndFile.m_PlayState.m_nRow = nRow;
				} else
				{
					for (ORDERINDEX nOrd = 0; nOrd < m_SndFile.Order().GetLength(); nOrd++)
					{
						if (m_SndFile.Order()[nOrd] == m_SndFile.Order.GetInvalidPatIndex()) break;
						if (m_SndFile.Order()[nOrd] == nPat)
						{
							m_SndFile.m_PlayState.m_nCurrentOrder = nOrd;
							m_SndFile.m_PlayState.m_nNextOrder = nOrd;
							m_SndFile.m_PlayState.m_nNextRow = nNextRow;
							m_SndFile.m_PlayState.m_nRow = nRow;
							break;
						}
					}
				}
			}
		} else
		{
			pMainFrm->PauseMod();
		}
	}
}


void CModDoc::OnPlayerStop()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm) pMainFrm->StopMod();
}


void CModDoc::OnPlayerPlayFromStart()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (pMainFrm)
	{
		CChildFrame *pChildFrm = GetChildFrame();
		if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
		{
			//User has sent play song command: set loop pattern checkbox to false.
			pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
		}

		pMainFrm->PauseMod();
		CriticalSection cs;
		m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PATTERNLOOP);
		m_SndFile.ResetPlayPos();
		//m_SndFile.visitedSongRows.Initialize(true);

		m_SndFile.m_PlayState.m_bPositionChanged = true;

		cs.Leave();

		pMainFrm->PlayMod(this);
	}
}


void CModDoc::OnEditGlobals()
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW,	IDD_CONTROL_GLOBALS);
}


void CModDoc::OnEditPatterns()
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, -1);
}


void CModDoc::OnEditSamples()
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, -1);
}


void CModDoc::OnEditInstruments()
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, -1);
}


void CModDoc::OnEditComments()
{
	SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_COMMENTS);
}


void CModDoc::OnShowCleanup()
{
	CModCleanupDlg dlg(*this, CMainFrame::GetMainFrame());
	dlg.DoModal();
}


void CModDoc::OnSetupZxxMacros()
{
	CMidiMacroSetup dlg(m_SndFile);
	if(dlg.DoModal() == IDOK)
	{
		if(m_SndFile.m_MidiCfg != dlg.m_MidiCfg)
		{
			m_SndFile.m_MidiCfg = dlg.m_MidiCfg;
			SetModified();
		}
	}
}


// Enable menu item only module types that support MIDI Mappings
void CModDoc::OnUpdateHasMIDIMappings(CCmdUI *p)
{
	if(p)
		p->Enable((m_SndFile.GetModSpecifications().MIDIMappingDirectivesMax > 0) ? TRUE : FALSE);
}


// Enable menu item only for IT / MPTM / XM files
void CModDoc::OnUpdateXMITMPTOnly(CCmdUI *p)
{
	if (p)
		p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE);
}


// Enable menu item only for IT / MPTM files
void CModDoc::OnUpdateHasEditHistory(CCmdUI *p)
{
	if (p)
		p->Enable(((m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_SndFile.GetFileHistory().empty()) ? TRUE : FALSE);
}


// Enable menu item if current module type supports compatibility export
void CModDoc::OnUpdateCompatExportableOnly(CCmdUI *p)
{
	if(p)
		p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT)) ? TRUE : FALSE);
}


static CString FormatSongLength(double length)
{
	length = mpt::round(length);
	double minutes = std::floor(length / 60.0), seconds = std::fmod(length, 60.0);
	CString s;
	s.Format(_T("%.0fmn%02.0fs"), minutes, seconds);
	return s;
}


void CModDoc::OnEstimateSongLength()
{
	CString s = _T("Approximate song length: ");
	const auto subSongs = m_SndFile.GetAllSubSongs();
	if (subSongs.empty())
	{
		Reporting::Information(_T("No patterns found!"));
		return;
	}

	std::vector<uint32> songsPerSequence(m_SndFile.Order.GetNumSequences(), 0);
	SEQUENCEINDEX prevSeq = subSongs[0].sequence;
	for(const auto &song : subSongs)
	{
		songsPerSequence[song.sequence]++;
		if(prevSeq != song.sequence)
			prevSeq = SEQUENCEINDEX_INVALID;
	}

	double totalLength = 0.0;
	uint32 songCount = 0;
	// If there are multiple sequences, indent their subsongs
	const TCHAR *indent = (prevSeq == SEQUENCEINDEX_INVALID) ? _T("\t") : _T("");
	for(const auto &song : subSongs)
	{
		double songLength = song.duration;
		if(subSongs.size() > 1)
		{
			totalLength += songLength;
			if(prevSeq != song.sequence)
			{
				songCount = 0;
				prevSeq = song.sequence;
				if(m_SndFile.Order(prevSeq).GetName().empty())
					s.AppendFormat(_T("\nSequence %u:"), prevSeq + 1u);
				else
					s.AppendFormat(_T("\nSequence %u (%s):"), prevSeq + 1u, mpt::ToWin(m_SndFile.Order(prevSeq).GetName()).c_str());
			}
			songCount++;
			if(songsPerSequence[song.sequence] > 1)
				s.AppendFormat(_T("\n%sSong %u, starting at order %u:\t"), indent, songCount, song.startOrder);
			else
				s.AppendChar(_T('\t'));
		}
		if(songLength != std::numeric_limits<double>::infinity())
		{
			songLength = mpt::round(songLength);
			s += FormatSongLength(songLength);
		} else
		{
			s += _T("Song too long!");
		}
	}
	if(subSongs.size() > 1 && totalLength != std::numeric_limits<double>::infinity())
	{
		s += _T("\n\nTotal length:\t") + FormatSongLength(totalLength);
	}

	Reporting::Information(s);
}


void CModDoc::OnApproximateBPM()
{
	if(CMainFrame::GetMainFrame()->GetModPlaying() != this)
	{
		m_SndFile.m_PlayState.m_nCurrentRowsPerBeat = m_SndFile.m_nDefaultRowsPerBeat;
		m_SndFile.m_PlayState.m_nCurrentRowsPerMeasure = m_SndFile.m_nDefaultRowsPerMeasure;
	}
	m_SndFile.RecalculateSamplesPerTick();
	const double bpm = m_SndFile.GetCurrentBPM();

	CString s;
	switch(m_SndFile.m_nTempoMode)
	{
		case TempoMode::Alternative:
			s.Format(_T("Using alternative tempo interpretation.\n\nAssuming:\n. %.8g ticks per second\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
			m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
			break;

		case TempoMode::Modern:
			s.Format(_T("Using modern tempo interpretation.\n\nThe tempo is: %.8g BPM"), bpm);
			break;

		case TempoMode::Classic:
		default:
			s.Format(_T("Using standard tempo interpretation.\n\nAssuming:\n. A mod tempo (tick duration factor) of %.8g\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
			m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
			break;
	}

	Reporting::Information(s);
}


CChildFrame *CModDoc::GetChildFrame()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if (!pMainFrm) return nullptr;
	CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
	if (pMDIActive)
	{
		CView *pView = pMDIActive->GetActiveView();
		if ((pView) && (pView->GetDocument() == this))
			return static_cast<CChildFrame *>(pMDIActive);
	}
	POSITION pos = GetFirstViewPosition();
	while (pos != NULL)
	{
		CView *pView = GetNextView(pos);
		if ((pView) && (pView->GetDocument() == this))
			return static_cast<CChildFrame *>(pView->GetParentFrame());
	}

	return nullptr;
}


// Get the currently edited pattern position. Note that ord might be ORDERINDEX_INVALID when editing a pattern that is not present in the order list.
void CModDoc::GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord)
{
	CChildFrame *pChildFrm = GetChildFrame();

	if(strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) // dirty HACK
	{
		PATTERNVIEWSTATE patternViewState;
		pChildFrm->SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)(&patternViewState));

		pat = patternViewState.nPattern;
		row = patternViewState.cursor.GetRow();
		ord = patternViewState.nOrder;
	} else
	{
		//patern editor object does not exist (i.e. is not active)  - use saved state.
		PATTERNVIEWSTATE &patternViewState = pChildFrm->GetPatternViewState();

		pat = patternViewState.nPattern;
		row = patternViewState.cursor.GetRow();
		ord = patternViewState.nOrder;
	}

	const auto &order = m_SndFile.Order();
	if(order.empty())
	{
		ord = ORDERINDEX_INVALID;
		pat = 0;
		row = 0;
	} else if(ord >= order.size())
	{
		ord = 0;
		pat = m_SndFile.Order()[ord];
	}
	if(!m_SndFile.Patterns.IsValidPat(pat))
	{
		pat = 0;
		row = 0;
	} else if(row >= m_SndFile.Patterns[pat].GetNumRows())
	{
		row = 0;
	}

	//ensure order correlates with pattern.
	if(ord >= order.size() || order[ord] != pat)
	{
		ord = order.FindOrder(pat);
	}
}


////////////////////////////////////////////////////////////////////////////////////////
// Playback


void CModDoc::OnPatternRestart(bool loop)
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	CChildFrame *pChildFrm = GetChildFrame();

	if ((pMainFrm) && (pChildFrm))
	{
		if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
		{
			//User has sent play pattern command: set loop pattern checkbox to true.
			pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, loop ? 1 : 0);
		}

		ROWINDEX nRow;
		PATTERNINDEX nPat;
		ORDERINDEX nOrd;
		GetEditPosition(nRow, nPat, nOrd);
		CModDoc *pModPlaying = pMainFrm->GetModPlaying();

		CriticalSection cs;

		// Cut instruments/samples
		for(auto &chn : m_SndFile.m_PlayState.Chn)
		{
			chn.nPatternLoopCount = 0;
			chn.nPatternLoop = 0;
			chn.nFadeOutVol = 0;
			chn.dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
		}
		if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
		m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
		if(loop)
			m_SndFile.LoopPattern(nPat);
		else
			m_SndFile.LoopPattern(PATTERNINDEX_INVALID);

		// set playback timer in the status bar (and update channel status)
		SetElapsedTime(nOrd, 0, true);

		if(pModPlaying == this)
		{
			m_SndFile.StopAllVsti();
		}

		cs.Leave();

		if(pModPlaying != this)
		{
			SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
			SetFollowWnd(pChildFrm->GetHwndView());
			pMainFrm->PlayMod(this); //rewbs.fix2977
		}
	}
	//SwitchToView();
}

void CModDoc::OnPatternPlay()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	CChildFrame *pChildFrm = GetChildFrame();

	if ((pMainFrm) && (pChildFrm))
	{
		if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
		{
			//User has sent play pattern command: set loop pattern checkbox to true.
			pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 1);
		}

		ROWINDEX nRow;
		PATTERNINDEX nPat;
		ORDERINDEX nOrd;
		GetEditPosition(nRow, nPat, nOrd);
		CModDoc *pModPlaying = pMainFrm->GetModPlaying();

		CriticalSection cs;

		// Cut instruments/samples
		for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
		{
			m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
		}
		if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
		m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
		m_SndFile.LoopPattern(nPat);

		// set playback timer in the status bar (and update channel status)
		SetElapsedTime(nOrd, nRow, true);

		if(pModPlaying == this)
		{
			m_SndFile.StopAllVsti();
		}

		cs.Leave();

		if(pModPlaying != this)
		{
			SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
			SetFollowWnd(pChildFrm->GetHwndView());
			pMainFrm->PlayMod(this);  //rewbs.fix2977
		}
	}
	//SwitchToView();

}

void CModDoc::OnPatternPlayNoLoop()
{
	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	CChildFrame *pChildFrm = GetChildFrame();

	if ((pMainFrm) && (pChildFrm))
	{
		if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
		{
			//User has sent play song command: set loop pattern checkbox to false.
			pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
		}

		ROWINDEX nRow;
		PATTERNINDEX nPat;
		ORDERINDEX nOrd;
		GetEditPosition(nRow, nPat, nOrd);
		CModDoc *pModPlaying = pMainFrm->GetModPlaying();

		CriticalSection cs;
		// Cut instruments/samples
		for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
		{
			m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
		}
		m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
		m_SndFile.SetCurrentOrder(nOrd);
		if(nOrd < m_SndFile.Order().size() && m_SndFile.Order()[nOrd] == nPat)
			m_SndFile.DontLoopPattern(nPat, nRow);
		else
			m_SndFile.LoopPattern(nPat);

		// set playback timer in the status bar (and update channel status)
		SetElapsedTime(nOrd, nRow, true);

		if(pModPlaying == this)
		{
			m_SndFile.StopAllVsti();
		}

		cs.Leave();

		if(pModPlaying != this)
		{
			SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
			SetFollowWnd(pChildFrm->GetHwndView());
			pMainFrm->PlayMod(this);  //rewbs.fix2977
		}
	}
	//SwitchToView();
}


void CModDoc::OnViewEditHistory()
{
	CEditHistoryDlg dlg(CMainFrame::GetMainFrame(), *this);
	dlg.DoModal();
}


void CModDoc::OnViewMPTHacks()
{
	ScopedLogCapturer logcapturer(*this);
	if(!HasMPTHacks())
	{
		AddToLog("No hacks found.");
	}
}


void CModDoc::OnViewTempoSwingSettings()
{
	if(m_SndFile.m_nDefaultRowsPerBeat > 0 && m_SndFile.m_nTempoMode == TempoMode::Modern)
	{
		TempoSwing tempoSwing = m_SndFile.m_tempoSwing;
		tempoSwing.resize(m_SndFile.m_nDefaultRowsPerBeat, TempoSwing::Unity);
		CTempoSwingDlg dlg(CMainFrame::GetMainFrame(), tempoSwing, m_SndFile);
		if(dlg.DoModal() == IDOK)
		{
			SetModified();
			m_SndFile.m_tempoSwing = dlg.m_tempoSwing;
		}
	} else if(GetModType() == MOD_TYPE_MPT)
	{
		Reporting::Error(_T("Modern tempo mode needs to be enabled in order to edit tempo swing settings."));
		OnSongProperties();
	}
}


LRESULT CModDoc::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/)
{
	const auto &modSpecs = m_SndFile.GetModSpecifications();
	switch(wParam)
	{
		case kcViewGeneral: OnEditGlobals(); break;
		case kcViewPattern: OnEditPatterns(); break;
		case kcViewSamples: OnEditSamples(); break;
		case kcViewInstruments: OnEditInstruments(); break;
		case kcViewComments: OnEditComments(); break;
		case kcViewSongProperties: OnSongProperties(); break;
		case kcViewTempoSwing: OnViewTempoSwingSettings(); break;
		case kcShowMacroConfig:	OnSetupZxxMacros(); break;
		case kcViewMIDImapping: OnViewMIDIMapping(); break;
		case kcViewEditHistory:	OnViewEditHistory(); break;
		case kcViewChannelManager: OnChannelManager(); break;

		case kcFileSaveAsWave:	OnFileWaveConvert(); break;
		case kcFileSaveMidi:	OnFileMidiConvert(); break;
		case kcFileSaveOPL:		OnFileOPLExport(); break;
		case kcFileExportCompat:  OnFileCompatibilitySave(); break;
		case kcEstimateSongLength: OnEstimateSongLength(); break;
		case kcApproxRealBPM:	OnApproximateBPM(); break;
		case kcFileSave:		DoSave(GetPathNameMpt()); break;
		case kcFileSaveAs:		DoSave(mpt::PathString()); break;
		case kcFileSaveCopy:	OnSaveCopy(); break;
		case kcFileSaveTemplate: OnSaveTemplateModule(); break;
		case kcFileClose:		SafeFileClose(); break;
		case kcFileAppend:		OnAppendModule(); break;

		case kcPlayPatternFromCursor: OnPatternPlay(); break;
		case kcPlayPatternFromStart: OnPatternRestart(); break;
		case kcPlaySongFromCursor: OnPatternPlayNoLoop(); break;
		case kcPlaySongFromStart: OnPlayerPlayFromStart(); break;
		case kcPlayPauseSong: OnPlayerPlay(); break;
		case kcPlaySongFromPattern: OnPatternRestart(false); break;
		case kcStopSong: OnPlayerStop(); break;
		case kcPanic: OnPanic(); break;
		case kcToggleLoopSong: SetLoopSong(!TrackerSettings::Instance().gbLoopSong); break;

		case kcTempoIncreaseFine:
			if(!modSpecs.hasFractionalTempo)
				break;
			[[fallthrough]];
		case kcTempoIncrease:
			if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo < modSpecs.GetTempoMax())
				m_SndFile.m_PlayState.m_nMusicTempo = std::min(modSpecs.GetTempoMax(), tempo + TEMPO(wParam == kcTempoIncrease ? 1.0 : 0.1));
			break;
		case kcTempoDecreaseFine:
			if(!modSpecs.hasFractionalTempo)
				break;
			[[fallthrough]];
		case kcTempoDecrease:
			if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo > modSpecs.GetTempoMin())
				m_SndFile.m_PlayState.m_nMusicTempo = std::max(modSpecs.GetTempoMin(), tempo - TEMPO(wParam == kcTempoDecrease ? 1.0 : 0.1));
			break;
		case kcSpeedIncrease:
			if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed < modSpecs.speedMax)
				m_SndFile.m_PlayState.m_nMusicSpeed = speed + 1;
			break;
		case kcSpeedDecrease:
			if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed > modSpecs.speedMin)
				m_SndFile.m_PlayState.m_nMusicSpeed = speed -  1;
			break;

		case kcViewToggle:
			if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr)
				lastActiveFrame->ToggleViews();
			break;

		default: return kcNull;
	}

	return wParam;
}


void CModDoc::TogglePluginEditor(UINT plugin, bool onlyThisEditor)
{
	if(plugin < MAX_MIXPLUGINS)
	{
		IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plugin].pMixPlugin;
		if(pPlugin != nullptr)
		{
			if(onlyThisEditor)
			{
				int32 posX = int32_min, posY = int32_min;
				for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
				{
					SNDMIXPLUGIN &otherPlug = m_SndFile.m_MixPlugins[i];
					if(i != plugin && otherPlug.pMixPlugin != nullptr && otherPlug.pMixPlugin->GetEditor() != nullptr)
					{
						otherPlug.pMixPlugin->CloseEditor();
						if(otherPlug.editorX != int32_min)
						{
							posX = otherPlug.editorX;
							posY = otherPlug.editorY;
						}
					}
				}
				if(posX != int32_min)
				{
					m_SndFile.m_MixPlugins[plugin].editorX = posX;
					m_SndFile.m_MixPlugins[plugin].editorY = posY;
				}
			}

			pPlugin->ToggleEditor();
		}
	}
}


void CModDoc::SetLoopSong(bool loop)
{
	TrackerSettings::Instance().gbLoopSong = loop;
	m_SndFile.SetRepeatCount(loop ? -1 : 0);
	CMainFrame::GetMainFrame()->UpdateAllViews(UpdateHint().MPTOptions());
}


void CModDoc::ChangeFileExtension(MODTYPE nNewType)
{
	//Not making path if path is empty(case only(?) for new file)
	if(!GetPathNameMpt().empty())
	{
		mpt::PathString drive;
		mpt::PathString dir;
		mpt::PathString fname;
		mpt::PathString fext;
		GetPathNameMpt().SplitPath(&drive, &dir, &fname, &fext);

		mpt::PathString newPath = drive + dir;

		// Catch case where we don't have a filename yet.
		if(fname.empty())
		{
			newPath += mpt::PathString::FromCString(GetTitle()).SanitizeComponent();
		} else
		{
			newPath += fname;
		}

		newPath += P_(".") + mpt::PathString::FromUTF8(CSoundFile::GetModSpecifications(nNewType).fileExtension);

		// Forcing save dialog to appear after extension change - otherwise unnotified file overwriting may occur.
		m_ShowSavedialog = true;

		SetPathName(newPath, FALSE);
	}

	UpdateAllViews(NULL, UpdateHint().ModType());
}


CHANNELINDEX CModDoc::FindAvailableChannel() const
{
	CHANNELINDEX chn = m_SndFile.GetNNAChannel(CHANNELINDEX_INVALID);
	if(chn != CHANNELINDEX_INVALID)
		return chn;
	else
		return GetNumChannels();
}


void CModDoc::RecordParamChange(PLUGINDEX plugSlot, PlugParamIndex paramIndex)
{
	::SendNotifyMessage(m_hWndFollow, WM_MOD_RECORDPARAM, plugSlot, paramIndex);
}


void CModDoc::LearnMacro(int macroToSet, PlugParamIndex paramToUse)
{
	if(macroToSet < 0 || macroToSet > kSFxMacros)
	{
		return;
	}

	// If macro already exists for this param, inform user and return
	if(auto macro = m_SndFile.m_MidiCfg.FindMacroForParam(paramToUse); macro >= 0)
	{
		CString message;
		message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), macro);
		Reporting::Information(message, _T("Macro exists for this parameter"));
		return;
	}

	// Set new macro
	if(paramToUse < 384)
	{
		m_SndFile.m_MidiCfg.CreateParameteredMacro(macroToSet, kSFxPlugParam, paramToUse);
	} else
	{
		CString message;
		message.Format(_T("Parameter %i beyond controllable range. Use Parameter Control Events to automate this parameter."), static_cast<int>(paramToUse));
		Reporting::Information(message, _T("Macro not assigned for this parameter"));
		return;
	}

	CString message;
	message.Format(_T("Parameter %i can now be controlled with macro %X."), static_cast<int>(paramToUse), macroToSet);
	Reporting::Information(message, _T("Macro assigned for this parameter"));

	return;
}


void CModDoc::OnSongProperties()
{
	const bool wasUsingFrequencies = m_SndFile.PeriodsAreFrequencies();
	CModTypeDlg dlg(m_SndFile, CMainFrame::GetMainFrame());
	if(dlg.DoModal() == IDOK)
	{
		UpdateAllViews(nullptr, GeneralHint().General());
		ScopedLogCapturer logcapturer(*this, _T("Conversion Status"));
		bool showLog = false;
		if(dlg.m_nType != GetModType())
		{
			if(!ChangeModType(dlg.m_nType))
				return;
			showLog = true;
		}

		CHANNELINDEX newChannels = Clamp(dlg.m_nChannels, m_SndFile.GetModSpecifications().channelsMin, m_SndFile.GetModSpecifications().channelsMax);
		if(newChannels != GetNumChannels())
		{
			const bool showCancelInRemoveDlg = m_SndFile.GetModSpecifications().channelsMax >= m_SndFile.GetNumChannels();
			if(ChangeNumChannels(newChannels, showCancelInRemoveDlg))
				showLog = true;

			// Force update of pattern highlights / num channels
			UpdateAllViews(nullptr, PatternHint().Data());
			UpdateAllViews(nullptr, GeneralHint().Channels());
		}

		if(wasUsingFrequencies != m_SndFile.PeriodsAreFrequencies())
		{
			for(auto &chn : m_SndFile.m_PlayState.Chn)
			{
				chn.nPeriod = 0;
			}
		}

		SetModified();
	}
}


void CModDoc::ViewMIDIMapping(PLUGINDEX plugin, PlugParamIndex param)
{
	CMIDIMappingDialog dlg(CMainFrame::GetMainFrame(), m_SndFile);
	if(plugin != PLUGINDEX_INVALID)
	{
		dlg.m_Setting.SetPlugIndex(plugin + 1);
		dlg.m_Setting.SetParamIndex(param);
	}
	dlg.DoModal();
}


void CModDoc::OnChannelManager()
{
	CChannelManagerDlg *instance = CChannelManagerDlg::sharedInstanceCreate();
	if(instance != nullptr)
	{
		if(instance->IsDisplayed())
			instance->Hide();
		else
		{
			instance->SetDocument(this);
			instance->Show();
		}
	}
}


// Sets playback timer to playback time at given position.
// At the same time, the playback parameters (global volume, channel volume and stuff like that) are calculated for this position.
// Sample channels positions are only updated if setSamplePos is true *and* the user has chosen to update sample play positions on seek.
void CModDoc::SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos)
{
	if(nOrd == ORDERINDEX_INVALID) return;

	double t = m_SndFile.GetPlaybackTimeAt(nOrd, nRow, true, setSamplePos && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SYNCSAMPLEPOS) != 0);
	if(t < 0)
	{
		// Position is never played regularly, but we may want to continue playing from here nevertheless.
		m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
		m_SndFile.m_PlayState.m_nRow = m_SndFile.m_PlayState.m_nNextRow = nRow;
	}

	CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
	if(pMainFrm != nullptr) pMainFrm->SetElapsedTime(std::max(0.0, t));
}


CString CModDoc::GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr,
											  bool bEmptyInsteadOfNoName /* = false*/,
											  bool bIncludeIndex /* = true*/) const
{
	if(nInstr >= MAX_INSTRUMENTS || m_SndFile.GetNumInstruments() == 0 || m_SndFile.Instruments[nInstr] == nullptr)
		return CString();

	CString displayName, instrumentName, pluginName;

	// Get instrument name.
	instrumentName = mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetInstrumentName(nInstr));

	// If instrument name is empty, use name of the sample mapped to C-5.
	if (instrumentName.IsEmpty())
	{
		const SAMPLEINDEX nSmp = m_SndFile.Instruments[nInstr]->Keyboard[NOTE_MIDDLEC - 1];
		if (nSmp <= m_SndFile.GetNumSamples() && m_SndFile.GetSample(nSmp).HasSampleData())
			instrumentName = _T("s: ") + mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetSampleName(nSmp));
	}

	// Get plugin name.
	const PLUGINDEX nPlug = m_SndFile.Instruments[nInstr]->nMixPlug;
	if (nPlug > 0 && nPlug < MAX_MIXPLUGINS)
		pluginName = mpt::ToCString(m_SndFile.m_MixPlugins[nPlug-1].GetName());

	if (pluginName.IsEmpty())
	{
		if(bEmptyInsteadOfNoName && instrumentName.IsEmpty())
			return TEXT("");
		if(instrumentName.IsEmpty())
			instrumentName = _T("(no name)");
		if (bIncludeIndex)
			displayName.Format(_T("%02d: %s"), nInstr, instrumentName.GetString());
		else
			displayName = instrumentName;
	} else
	{
		if (bIncludeIndex)
			displayName.Format(TEXT("%02d: %s (%s)"), nInstr, instrumentName.GetString(), pluginName.GetString());
		else
			displayName.Format(TEXT("%s (%s)"), instrumentName.GetString(), pluginName.GetString());
	}
	return displayName;
}


void CModDoc::SafeFileClose()
{
	// Verify that the main window has the focus. This saves us a lot of trouble because active modal dialogs cannot know if their pSndFile pointers are still valid.
	if(GetActiveWindow() == CMainFrame::GetMainFrame()->m_hWnd)
		OnFileClose();
}


// "Panic button". This resets all VSTi, OPL and sample notes.
void CModDoc::OnPanic()
{
	CriticalSection cs;
	m_SndFile.ResetChannels();
	m_SndFile.StopAllVsti();
}


// Before saving, make sure that every char after the terminating null char is also null.
// Else, garbage might end up in various text strings that wasn't supposed to be there.
void CModDoc::FixNullStrings()
{
	// Macros
	m_SndFile.m_MidiCfg.Sanitize();
}


void CModDoc::OnSaveCopy()
{
	DoSave(mpt::PathString(), false);
}


void CModDoc::OnSaveTemplateModule()
{
	// Create template folder if doesn't exist already.
	const mpt::PathString templateFolder = TrackerSettings::Instance().PathUserTemplates.GetDefaultDir();
	if (!templateFolder.IsDirectory())
	{
		if (!CreateDirectory(templateFolder.AsNative().c_str(), nullptr))
		{
			Reporting::Notification(MPT_CFORMAT("Error: Unable to create template folder '{}'")( templateFolder));
			return;
		}
	}

	// Generate file name candidate.
	mpt::PathString sName;
	for(size_t i = 0; i < 1000; ++i)
	{
		sName += P_("newTemplate") + mpt::PathString::FromUnicode(mpt::ufmt::val(i));
		sName += P_(".") + mpt::PathString::FromUTF8(m_SndFile.GetModSpecifications().fileExtension);
		if (!(templateFolder + sName).FileOrDirectoryExists())
			break;
	}

	// Ask file name from user.
	FileDialog dlg = SaveFileDialog()
		.DefaultExtension(m_SndFile.GetModSpecifications().fileExtension)
		.DefaultFilename(sName)
		.ExtensionFilter(ModTypeToFilter(m_SndFile))
		.WorkingDirectory(templateFolder);
	if(!dlg.Show())
		return;

	if (OnSaveDocument(dlg.GetFirstFile(), false))
	{
		// Update template menu.
		CMainFrame::GetMainFrame()->CreateTemplateModulesMenu();
	}
}


// Create an undo point that stores undo data for all existing patterns
void CModDoc::PrepareUndoForAllPatterns(bool storeChannelInfo, const char *description)
{
	bool linkUndo = false;

	PATTERNINDEX lastPat = 0;
	for(PATTERNINDEX pat = 0; pat < m_SndFile.Patterns.Size(); pat++)
	{
		if(m_SndFile.Patterns.IsValidPat(pat)) lastPat = pat;
	}

	for(PATTERNINDEX pat = 0; pat <= lastPat; pat++)
	{
		if(m_SndFile.Patterns.IsValidPat(pat))
		{
			GetPatternUndo().PrepareUndo(pat, 0, 0, GetNumChannels(), m_SndFile.Patterns[pat].GetNumRows(), description, linkUndo, storeChannelInfo && pat == lastPat);
			linkUndo = true;
		}
	}
}


CString CModDoc::LinearToDecibels(double value, double valueAtZeroDB)
{
	if (value == 0) return _T("-inf");

	double changeFactor = value / valueAtZeroDB;
	double dB = 20.0 * std::log10(changeFactor);

	CString s = (dB >= 0) ? _T("+") : _T("");
	s.AppendFormat(_T("%.2f dB"), dB);
	return s;
}


CString CModDoc::PanningToString(int32 value, int32 valueAtCenter)
{
	if(value == valueAtCenter)
		return _T("Center");

	CString s;
	s.Format(_T("%i%% %s"), (std::abs(static_cast<int>(value) - valueAtCenter) * 100) / valueAtCenter, value < valueAtCenter ? _T("Left") : _T("Right"));
	return s;
}


// Apply OPL patch changes to live playback
void CModDoc::UpdateOPLInstrument(SAMPLEINDEX smp)
{
	const ModSample &sample = m_SndFile.GetSample(smp);
	if(!sample.uFlags[CHN_ADLIB] || !m_SndFile.m_opl || CMainFrame::GetMainFrame()->GetModPlaying() != this)
		return;

	CriticalSection cs;
	const auto &patch = sample.adlib;
	for(CHANNELINDEX chn = 0; chn < MAX_CHANNELS; chn++)
	{
		const auto &c = m_SndFile.m_PlayState.Chn[chn];
		if(c.pModSample == &sample && c.IsSamplePlaying())
		{
			m_SndFile.m_opl->Patch(chn, patch);
		}
	}
}


// Store all view positions t settings file
void CModDoc::SerializeViews() const
{
	const mpt::PathString pathName = theApp.IsPortableMode() ? GetPathNameMpt().AbsolutePathToRelative(theApp.GetInstallPath()) : GetPathNameMpt();
	if(pathName.empty())
	{
		return;
	}
	std::ostringstream f(std::ios::out | std::ios::binary);

	CRect mdiRect;
	::GetClientRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
	const int width = mdiRect.Width();
	const int height = mdiRect.Height();

	const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);

	// Document view positions and sizes
	POSITION pos = GetFirstViewPosition();
	while(pos != nullptr && !mdiRect.IsRectEmpty())
	{
		CModControlView *pView = dynamic_cast<CModControlView *>(GetNextView(pos));
		if(pView)
		{
			CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
			WINDOWPLACEMENT wnd;
			wnd.length = sizeof(WINDOWPLACEMENT);
			pChildFrm->GetWindowPlacement(&wnd);
			const CRect rect = wnd.rcNormalPosition;

			// Write size information
			uint8 windowState = 0;
			if(wnd.showCmd == SW_SHOWMAXIMIZED) windowState = 1;
			else if(wnd.showCmd == SW_SHOWMINIMIZED) windowState = 2;
			mpt::IO::WriteIntLE<uint8>(f, 0);	// Window type
			mpt::IO::WriteIntLE<uint8>(f, windowState);
			mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.left, 1 << 30, width));
			mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.top, 1 << 30, height));
			mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Width(), 1 << 30, width));
			mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Height(), 1 << 30, height));

			std::string s = pChildFrm->SerializeView();
			mpt::IO::WriteVarInt(f, s.size());
			f << s;
		}
	}
	// Plugin window positions
	for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
	{
		if(m_SndFile.m_MixPlugins[i].IsValidPlugin() && m_SndFile.m_MixPlugins[i].editorX != int32_min && cxScreen && cyScreen)
		{
			// Translate screen position into percentage (to make it independent of the actual screen resolution)
			int32 editorX = Util::muldivr(m_SndFile.m_MixPlugins[i].editorX, 1 << 30, cxScreen);
			int32 editorY = Util::muldivr(m_SndFile.m_MixPlugins[i].editorY, 1 << 30, cyScreen);

			mpt::IO::WriteIntLE<uint8>(f, 1);	// Window type
			mpt::IO::WriteIntLE<uint8>(f, 0);	// Version
			mpt::IO::WriteVarInt(f, i);
			mpt::IO::WriteIntLE<int32>(f, editorX);
			mpt::IO::WriteIntLE<int32>(f, editorY);
		}
	}

	SettingsContainer &settings = theApp.GetSongSettings();
	const std::string s = f.str();
	settings.Write(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode(), pathName);
	settings.Write(U_("WindowSettings"), pathName.ToUnicode(), mpt::encode_hex(mpt::as_span(s)));
}


// Restore all view positions from settings file
void CModDoc::DeserializeViews()
{
	mpt::PathString pathName = GetPathNameMpt();
	if(pathName.empty()) return;

	SettingsContainer &settings = theApp.GetSongSettings();
	mpt::ustring s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
	if(s.size() < 2)
	{
		// Try relative path
		pathName = pathName.RelativePathToAbsolute(theApp.GetInstallPath());
		s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
		if(s.size() < 2)
		{
			// Try searching for filename instead of full path name
			const mpt::ustring altName = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode());
			s = settings.Read<mpt::ustring>(U_("WindowSettings"), altName);
			if(s.size() < 2) return;
		}
	}
	std::vector<std::byte> bytes = mpt::decode_hex(s);

	FileReader file(mpt::as_span(bytes));

	CRect mdiRect;
	::GetWindowRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
	const int width = mdiRect.Width();
	const int height = mdiRect.Height();

	const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);

	POSITION pos = GetFirstViewPosition();
	CChildFrame *pChildFrm = nullptr;
	if(pos != nullptr) pChildFrm = dynamic_cast<CChildFrame *>(GetNextView(pos)->GetParentFrame());

	bool anyMaximized = false;
	while(file.CanRead(1))
	{
		const uint8 windowType = file.ReadUint8();
		if(windowType == 0)
		{
			// Document view positions and sizes
			const uint8 windowState = file.ReadUint8();
			CRect rect;
			rect.left = Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
			rect.top = Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
			rect.right = rect.left + Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
			rect.bottom = rect.top + Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
			size_t dataSize;
			file.ReadVarInt(dataSize);
			FileReader data = file.ReadChunk(dataSize);

			if(pChildFrm == nullptr)
			{
				CModDocTemplate *pTemplate = static_cast<CModDocTemplate *>(GetDocTemplate());
				ASSERT_VALID(pTemplate);
				pChildFrm = static_cast<CChildFrame *>(pTemplate->CreateNewFrame(this, nullptr));
				if(pChildFrm != nullptr)
				{
					pTemplate->InitialUpdateFrame(pChildFrm, this);
				}
			}
			if(pChildFrm != nullptr)
			{
				if(!mdiRect.IsRectEmpty())
				{
					WINDOWPLACEMENT wnd;
					wnd.length = sizeof(wnd);
					pChildFrm->GetWindowPlacement(&wnd);
					wnd.showCmd = SW_SHOWNOACTIVATE;
					if(windowState == 1 || anyMaximized)
					{
						// Once a window has been maximized, all following windows have to be marked as maximized as well.
						wnd.showCmd = SW_MAXIMIZE;
						anyMaximized = true;
					} else if(windowState == 2)
					{
						wnd.showCmd = SW_MINIMIZE;
					}
					if(rect.left < width && rect.right > 0 && rect.top < height && rect.bottom > 0)
					{
						wnd.rcNormalPosition = CRect(rect.left, rect.top, rect.right, rect.bottom);
					}
					pChildFrm->SetWindowPlacement(&wnd);
				}
				pChildFrm->DeserializeView(data);
				pChildFrm = nullptr;
			}
		} else if(windowType == 1)
		{
			if(file.ReadUint8() != 0)
				break;
			// Plugin window positions
			PLUGINDEX plug = 0;
			if(file.ReadVarInt(plug) && plug < MAX_MIXPLUGINS)
			{
				int32 editorX = file.ReadInt32LE();
				int32 editorY = file.ReadInt32LE();
				if(editorX != int32_min && editorY != int32_min)
				{
					m_SndFile.m_MixPlugins[plug].editorX = Util::muldivr(editorX, cxScreen, 1 << 30);
					m_SndFile.m_MixPlugins[plug].editorY = Util::muldivr(editorY, cyScreen, 1 << 30);
				}
			}
		} else
		{
			// Unknown type
			break;
		}
	}
}

OPENMPT_NAMESPACE_END