diff options
author | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
---|---|---|
committer | Jef <jef@targetspot.com> | 2024-09-24 08:54:57 -0400 |
commit | 20d28e80a5c861a9d5f449ea911ab75b4f37ad0d (patch) | |
tree | 12f17f78986871dd2cfb0a56e5e93b545c1ae0d0 /Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp | |
parent | 537bcbc86291b32fc04ae4133ce4d7cac8ebe9a7 (diff) | |
download | winamp-20d28e80a5c861a9d5f449ea911ab75b4f37ad0d.tar.gz |
Initial community commit
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp')
-rw-r--r-- | Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp | 3361 |
1 files changed, 3361 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp new file mode 100644 index 00000000..f2c7ac44 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp @@ -0,0 +1,3361 @@ +/* + * 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 ¶ms, 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 |