/* * 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 #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(&m_bModified), modified ? TRUE : FALSE) != modified) { // Update window titles in GUI thread CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_SETMODIFIED, reinterpret_cast(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(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(); 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(r * 255), mpt::saturate_round(g * 255), mpt::saturate_round(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(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(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(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 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(this), hint.AsLPARAM()); } ///////////////////////////////////////////////////////////////////////////// // CModDoc commands void CModDoc::OnFileWaveConvert() { OnFileWaveConvert(ORDERINDEX_INVALID, ORDERINDEX_INVALID); } void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector &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 usedChannels; std::vector> channelFlags; // Instrument mode std::vector 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(i), true); if(!m_SndFile.GetSample(static_cast(i + 1)).HasSampleData() || !IsSampleUsed(static_cast(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(i + 1), false); } else { // Re-mute previously processed instrument if(i > 0) MuteInstrument(static_cast(i), true); if(m_SndFile.Instruments[i + 1] == nullptr || !IsInstrumentUsed(static_cast(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(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(i + 1), instrMuteState[i]); else MuteInstrument(static_cast(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 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 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::infinity()) { songLength = mpt::round(songLength); s += FormatSongLength(songLength); } else { s += _T("Song too long!"); } } if(subSongs.size() > 1 && totalLength != std::numeric_limits::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(pMDIActive); } POSITION pos = GetFirstViewPosition(); while (pos != NULL) { CView *pView = GetNextView(pos); if ((pView) && (pView->GetDocument() == this)) return static_cast(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(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(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(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(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(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(f, 0); // Window type mpt::IO::WriteIntLE(f, windowState); mpt::IO::WriteIntLE(f, Util::muldivr(rect.left, 1 << 30, width)); mpt::IO::WriteIntLE(f, Util::muldivr(rect.top, 1 << 30, height)); mpt::IO::WriteIntLE(f, Util::muldivr(rect.Width(), 1 << 30, width)); mpt::IO::WriteIntLE(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(f, 1); // Window type mpt::IO::WriteIntLE(f, 0); // Version mpt::IO::WriteVarInt(f, i); mpt::IO::WriteIntLE(f, editorX); mpt::IO::WriteIntLE(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(U_("WindowSettings"), pathName.ToUnicode()); if(s.size() < 2) { // Try relative path pathName = pathName.RelativePathToAbsolute(theApp.GetInstallPath()); s = settings.Read(U_("WindowSettings"), pathName.ToUnicode()); if(s.size() < 2) { // Try searching for filename instead of full path name const mpt::ustring altName = settings.Read(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode()); s = settings.Read(U_("WindowSettings"), altName); if(s.size() < 2) return; } } std::vector 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(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(GetDocTemplate()); ASSERT_VALID(pTemplate); pChildFrm = static_cast(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