3362 lines
97 KiB
C++
3362 lines
97 KiB
C++
|
/*
|
||
|
* Moddoc.cpp
|
||
|
* ----------
|
||
|
* Purpose: Module document handling in OpenMPT.
|
||
|
* Notes : (currently none)
|
||
|
* Authors: OpenMPT Devs
|
||
|
* The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
|
||
|
*/
|
||
|
|
||
|
|
||
|
#include "stdafx.h"
|
||
|
#include "Mptrack.h"
|
||
|
#include "Mainfrm.h"
|
||
|
#include "InputHandler.h"
|
||
|
#include "Moddoc.h"
|
||
|
#include "ModDocTemplate.h"
|
||
|
#include "../soundlib/mod_specifications.h"
|
||
|
#include "../soundlib/plugins/PlugInterface.h"
|
||
|
#include "Childfrm.h"
|
||
|
#include "Mpdlgs.h"
|
||
|
#include "dlg_misc.h"
|
||
|
#include "TempoSwingDialog.h"
|
||
|
#include "mod2wave.h"
|
||
|
#include "ChannelManagerDlg.h"
|
||
|
#include "MIDIMacroDialog.h"
|
||
|
#include "MIDIMappingDialog.h"
|
||
|
#include "StreamEncoderAU.h"
|
||
|
#include "StreamEncoderFLAC.h"
|
||
|
#include "StreamEncoderMP3.h"
|
||
|
#include "StreamEncoderOpus.h"
|
||
|
#include "StreamEncoderRAW.h"
|
||
|
#include "StreamEncoderVorbis.h"
|
||
|
#include "StreamEncoderWAV.h"
|
||
|
#include "mod2midi.h"
|
||
|
#include "../common/version.h"
|
||
|
#include "../tracklib/SampleEdit.h"
|
||
|
#include "../soundlib/modsmp_ctrl.h"
|
||
|
#include "CleanupSong.h"
|
||
|
#include "../common/mptStringBuffer.h"
|
||
|
#include "../common/mptFileIO.h"
|
||
|
#include <sstream>
|
||
|
#include "../common/FileReader.h"
|
||
|
#include "FileDialog.h"
|
||
|
#include "ExternalSamples.h"
|
||
|
#include "Globals.h"
|
||
|
#include "../soundlib/OPL.h"
|
||
|
#ifndef NO_PLUGINS
|
||
|
#include "AbstractVstEditor.h"
|
||
|
#endif
|
||
|
#include "mpt/binary/hex.hpp"
|
||
|
#include "mpt/base/numbers.hpp"
|
||
|
#include "mpt/io/io.hpp"
|
||
|
#include "mpt/io/io_stdstream.hpp"
|
||
|
|
||
|
|
||
|
OPENMPT_NAMESPACE_BEGIN
|
||
|
|
||
|
|
||
|
const TCHAR FileFilterMOD[] = _T("ProTracker Modules (*.mod)|*.mod||");
|
||
|
const TCHAR FileFilterXM[] = _T("FastTracker Modules (*.xm)|*.xm||");
|
||
|
const TCHAR FileFilterS3M[] = _T("Scream Tracker Modules (*.s3m)|*.s3m||");
|
||
|
const TCHAR FileFilterIT[] = _T("Impulse Tracker Modules (*.it)|*.it||");
|
||
|
const TCHAR FileFilterMPT[] = _T("OpenMPT Modules (*.mptm)|*.mptm||");
|
||
|
const TCHAR FileFilterNone[] = _T("");
|
||
|
|
||
|
const CString ModTypeToFilter(const CSoundFile& sndFile)
|
||
|
{
|
||
|
const MODTYPE modtype = sndFile.GetType();
|
||
|
switch(modtype)
|
||
|
{
|
||
|
case MOD_TYPE_MOD: return FileFilterMOD;
|
||
|
case MOD_TYPE_XM: return FileFilterXM;
|
||
|
case MOD_TYPE_S3M: return FileFilterS3M;
|
||
|
case MOD_TYPE_IT: return FileFilterIT;
|
||
|
case MOD_TYPE_MPT: return FileFilterMPT;
|
||
|
default: return FileFilterNone;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////
|
||
|
// CModDoc
|
||
|
|
||
|
IMPLEMENT_DYNCREATE(CModDoc, CDocument)
|
||
|
|
||
|
BEGIN_MESSAGE_MAP(CModDoc, CDocument)
|
||
|
//{{AFX_MSG_MAP(CModDoc)
|
||
|
ON_COMMAND(ID_FILE_SAVE_COPY, &CModDoc::OnSaveCopy)
|
||
|
ON_COMMAND(ID_FILE_SAVEASTEMPLATE, &CModDoc::OnSaveTemplateModule)
|
||
|
ON_COMMAND(ID_FILE_SAVEASWAVE, &CModDoc::OnFileWaveConvert)
|
||
|
ON_COMMAND(ID_FILE_SAVEMIDI, &CModDoc::OnFileMidiConvert)
|
||
|
ON_COMMAND(ID_FILE_SAVEOPL, &CModDoc::OnFileOPLExport)
|
||
|
ON_COMMAND(ID_FILE_SAVECOMPAT, &CModDoc::OnFileCompatibilitySave)
|
||
|
ON_COMMAND(ID_FILE_APPENDMODULE, &CModDoc::OnAppendModule)
|
||
|
ON_COMMAND(ID_PLAYER_PLAY, &CModDoc::OnPlayerPlay)
|
||
|
ON_COMMAND(ID_PLAYER_PAUSE, &CModDoc::OnPlayerPause)
|
||
|
ON_COMMAND(ID_PLAYER_STOP, &CModDoc::OnPlayerStop)
|
||
|
ON_COMMAND(ID_PLAYER_PLAYFROMSTART, &CModDoc::OnPlayerPlayFromStart)
|
||
|
ON_COMMAND(ID_VIEW_SONGPROPERTIES, &CModDoc::OnSongProperties)
|
||
|
ON_COMMAND(ID_VIEW_GLOBALS, &CModDoc::OnEditGlobals)
|
||
|
ON_COMMAND(ID_VIEW_PATTERNS, &CModDoc::OnEditPatterns)
|
||
|
ON_COMMAND(ID_VIEW_SAMPLES, &CModDoc::OnEditSamples)
|
||
|
ON_COMMAND(ID_VIEW_INSTRUMENTS, &CModDoc::OnEditInstruments)
|
||
|
ON_COMMAND(ID_VIEW_COMMENTS, &CModDoc::OnEditComments)
|
||
|
ON_COMMAND(ID_VIEW_EDITHISTORY, &CModDoc::OnViewEditHistory)
|
||
|
ON_COMMAND(ID_VIEW_MIDIMAPPING, &CModDoc::OnViewMIDIMapping)
|
||
|
ON_COMMAND(ID_VIEW_MPTHACKS, &CModDoc::OnViewMPTHacks)
|
||
|
ON_COMMAND(ID_EDIT_CLEANUP, &CModDoc::OnShowCleanup)
|
||
|
ON_COMMAND(ID_EDIT_SAMPLETRIMMER, &CModDoc::OnShowSampleTrimmer)
|
||
|
ON_COMMAND(ID_PATTERN_MIDIMACRO, &CModDoc::OnSetupZxxMacros)
|
||
|
ON_COMMAND(ID_CHANNEL_MANAGER, &CModDoc::OnChannelManager)
|
||
|
|
||
|
ON_COMMAND(ID_ESTIMATESONGLENGTH, &CModDoc::OnEstimateSongLength)
|
||
|
ON_COMMAND(ID_APPROX_BPM, &CModDoc::OnApproximateBPM)
|
||
|
ON_COMMAND(ID_PATTERN_PLAY, &CModDoc::OnPatternPlay)
|
||
|
ON_COMMAND(ID_PATTERN_PLAYNOLOOP, &CModDoc::OnPatternPlayNoLoop)
|
||
|
ON_COMMAND(ID_PATTERN_RESTART, &CModDoc::OnPatternRestart)
|
||
|
ON_UPDATE_COMMAND_UI(ID_VIEW_INSTRUMENTS, &CModDoc::OnUpdateXMITMPTOnly)
|
||
|
ON_UPDATE_COMMAND_UI(ID_PATTERN_MIDIMACRO, &CModDoc::OnUpdateXMITMPTOnly)
|
||
|
ON_UPDATE_COMMAND_UI(ID_VIEW_MIDIMAPPING, &CModDoc::OnUpdateHasMIDIMappings)
|
||
|
ON_UPDATE_COMMAND_UI(ID_VIEW_EDITHISTORY, &CModDoc::OnUpdateHasEditHistory)
|
||
|
ON_UPDATE_COMMAND_UI(ID_FILE_SAVECOMPAT, &CModDoc::OnUpdateCompatExportableOnly)
|
||
|
//}}AFX_MSG_MAP
|
||
|
END_MESSAGE_MAP()
|
||
|
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////
|
||
|
// CModDoc construction/destruction
|
||
|
|
||
|
CModDoc::CModDoc()
|
||
|
: m_notifyType(Notification::Default)
|
||
|
, m_PatternUndo(*this)
|
||
|
, m_SampleUndo(*this)
|
||
|
, m_InstrumentUndo(*this)
|
||
|
{
|
||
|
// Set the creation date of this file (or the load time if we're loading an existing file)
|
||
|
time(&m_creationTime);
|
||
|
|
||
|
ReinitRecordState();
|
||
|
|
||
|
CMainFrame::UpdateAudioParameters(m_SndFile, true);
|
||
|
}
|
||
|
|
||
|
|
||
|
CModDoc::~CModDoc()
|
||
|
{
|
||
|
ClearLog();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SetModified(bool modified)
|
||
|
{
|
||
|
static_assert(sizeof(long) == sizeof(m_bModified));
|
||
|
m_modifiedAutosave = modified;
|
||
|
if(!!InterlockedExchange(reinterpret_cast<long *>(&m_bModified), modified ? TRUE : FALSE) != modified)
|
||
|
{
|
||
|
// Update window titles in GUI thread
|
||
|
CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_SETMODIFIED, reinterpret_cast<WPARAM>(this), 0);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Return "modified since last autosave" status and reset it until the next SetModified() (as this is only used for polling during autosave)
|
||
|
bool CModDoc::ModifiedSinceLastAutosave()
|
||
|
{
|
||
|
return m_modifiedAutosave.exchange(false);
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL CModDoc::OnNewDocument()
|
||
|
{
|
||
|
if (!CDocument::OnNewDocument()) return FALSE;
|
||
|
|
||
|
m_SndFile.Create(FileReader(), CSoundFile::loadCompleteModule, this);
|
||
|
m_SndFile.ChangeModTypeTo(CTrackApp::GetDefaultDocType());
|
||
|
|
||
|
theApp.GetDefaultMidiMacro(m_SndFile.m_MidiCfg);
|
||
|
m_SndFile.m_SongFlags.set((SONG_LINEARSLIDES | SONG_ISAMIGA) & m_SndFile.GetModSpecifications().songFlags);
|
||
|
|
||
|
ReinitRecordState();
|
||
|
InitializeMod();
|
||
|
SetModified(false);
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL CModDoc::OnOpenDocument(LPCTSTR lpszPathName)
|
||
|
{
|
||
|
const mpt::PathString filename = lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString();
|
||
|
|
||
|
ScopedLogCapturer logcapturer(*this);
|
||
|
|
||
|
if(filename.empty()) return OnNewDocument();
|
||
|
|
||
|
BeginWaitCursor();
|
||
|
|
||
|
{
|
||
|
|
||
|
MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open..."));
|
||
|
|
||
|
InputFile f(filename, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
|
||
|
if (f.IsValid())
|
||
|
{
|
||
|
FileReader file = GetFileReader(f);
|
||
|
MPT_ASSERT(GetPathNameMpt().empty());
|
||
|
SetPathName(filename, FALSE); // Path is not set yet, but loaders processing external samples/instruments (ITP/MPTM) need this for relative paths.
|
||
|
try
|
||
|
{
|
||
|
if(!m_SndFile.Create(file, CSoundFile::loadCompleteModule, this))
|
||
|
{
|
||
|
EndWaitCursor();
|
||
|
return FALSE;
|
||
|
}
|
||
|
} catch(mpt::out_of_memory e)
|
||
|
{
|
||
|
mpt::delete_out_of_memory(e);
|
||
|
EndWaitCursor();
|
||
|
AddToLog(LogError, U_("Out of Memory"));
|
||
|
return FALSE;
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
EndWaitCursor();
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open."));
|
||
|
|
||
|
}
|
||
|
|
||
|
EndWaitCursor();
|
||
|
|
||
|
logcapturer.ShowLog(
|
||
|
MPT_CFORMAT("File: {}\nLast saved with: {}, you are using OpenMPT {}\n\n")
|
||
|
(filename, m_SndFile.m_modFormat.madeWithTracker, Version::Current()));
|
||
|
|
||
|
if((m_SndFile.m_nType == MOD_TYPE_NONE) || (!m_SndFile.GetNumChannels()))
|
||
|
return FALSE;
|
||
|
|
||
|
const bool noColors = std::find_if(std::begin(m_SndFile.ChnSettings), std::begin(m_SndFile.ChnSettings) + GetNumChannels(), [](const auto &settings) {
|
||
|
return settings.color != ModChannelSettings::INVALID_COLOR;
|
||
|
}) == std::begin(m_SndFile.ChnSettings) + GetNumChannels();
|
||
|
if(noColors)
|
||
|
{
|
||
|
SetDefaultChannelColors();
|
||
|
}
|
||
|
|
||
|
// Convert to MOD/S3M/XM/IT
|
||
|
switch(m_SndFile.GetType())
|
||
|
{
|
||
|
case MOD_TYPE_MOD:
|
||
|
case MOD_TYPE_S3M:
|
||
|
case MOD_TYPE_XM:
|
||
|
case MOD_TYPE_IT:
|
||
|
case MOD_TYPE_MPT:
|
||
|
break;
|
||
|
default:
|
||
|
m_SndFile.ChangeModTypeTo(m_SndFile.GetBestSaveFormat(), false);
|
||
|
m_SndFile.m_SongFlags.set(SONG_IMPORTED);
|
||
|
break;
|
||
|
}
|
||
|
// If the file was packed in some kind of container (e.g. ZIP, or simply a format like MO3), prompt for new file extension as well
|
||
|
// Same if MOD_TYPE_XXX does not indicate actual song format
|
||
|
if(m_SndFile.GetContainerType() != MOD_CONTAINERTYPE_NONE || m_SndFile.m_SongFlags[SONG_IMPORTED])
|
||
|
{
|
||
|
m_ShowSavedialog = true;
|
||
|
}
|
||
|
|
||
|
ReinitRecordState();
|
||
|
|
||
|
if(TrackerSettings::Instance().rememberSongWindows)
|
||
|
DeserializeViews();
|
||
|
|
||
|
// This is only needed when opening a module with stored window positions.
|
||
|
// The MDI child is activated before it has an active view and thus there is no CModDoc associated with it.
|
||
|
CMainFrame::GetMainFrame()->UpdateEffectKeys(this);
|
||
|
auto instance = CChannelManagerDlg::sharedInstance();
|
||
|
if(instance != nullptr)
|
||
|
{
|
||
|
instance->SetDocument(this);
|
||
|
}
|
||
|
|
||
|
// Show warning if file was made with more recent version of OpenMPT except
|
||
|
if(m_SndFile.m_dwLastSavedWithVersion.WithoutTestNumber() > Version::Current())
|
||
|
{
|
||
|
Reporting::Notification(MPT_UFORMAT("Warning: this song was last saved with a more recent version of OpenMPT.\r\nSong saved with: v{}. Current version: v{}.\r\n")(
|
||
|
m_SndFile.m_dwLastSavedWithVersion,
|
||
|
Version::Current()));
|
||
|
}
|
||
|
|
||
|
SetModified(false);
|
||
|
m_bHasValidPath = true;
|
||
|
|
||
|
// Check if there are any missing samples, and if there are, show a dialog to relocate them.
|
||
|
for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
|
||
|
{
|
||
|
if(m_SndFile.IsExternalSampleMissing(smp))
|
||
|
{
|
||
|
MissingExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame());
|
||
|
dlg.DoModal();
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return TRUE;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::OnSaveDocument(const mpt::PathString &filename, const bool setPath)
|
||
|
{
|
||
|
ScopedLogCapturer logcapturer(*this);
|
||
|
if(filename.empty())
|
||
|
return false;
|
||
|
|
||
|
bool ok = false;
|
||
|
BeginWaitCursor();
|
||
|
m_SndFile.m_dwLastSavedWithVersion = Version::Current();
|
||
|
try
|
||
|
{
|
||
|
mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
mpt::ofstream &f = sf;
|
||
|
if(f)
|
||
|
{
|
||
|
if(m_SndFile.m_SongFlags[SONG_IMPORTED] && !(GetModType() & (MOD_TYPE_MOD | MOD_TYPE_S3M)))
|
||
|
{
|
||
|
// Check if any non-supported playback behaviours are enabled due to being imported from a different format
|
||
|
const auto supportedBehaviours = m_SndFile.GetSupportedPlaybackBehaviour(GetModType());
|
||
|
bool showWarning = true;
|
||
|
for(size_t i = 0; i < kMaxPlayBehaviours; i++)
|
||
|
{
|
||
|
if(m_SndFile.m_playBehaviour[i] && !supportedBehaviours[i])
|
||
|
{
|
||
|
if(showWarning)
|
||
|
{
|
||
|
AddToLog(LogWarning, mpt::ToUnicode(mpt::Charset::ASCII, MPT_AFORMAT("Some imported Compatibility Settings that are not supported by the {} format have been disabled. Verify that the module still sounds as intended.")
|
||
|
(mpt::ToUpperCaseAscii(m_SndFile.GetModSpecifications().fileExtension))));
|
||
|
showWarning = false;
|
||
|
}
|
||
|
m_SndFile.m_playBehaviour.reset(i);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
FixNullStrings();
|
||
|
switch(m_SndFile.GetType())
|
||
|
{
|
||
|
case MOD_TYPE_MOD: ok = m_SndFile.SaveMod(f); break;
|
||
|
case MOD_TYPE_S3M: ok = m_SndFile.SaveS3M(f); break;
|
||
|
case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f); break;
|
||
|
case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename); break;
|
||
|
case MOD_TYPE_MPT: ok = m_SndFile.SaveIT(f, filename); break;
|
||
|
default: MPT_ASSERT_NOTREACHED();
|
||
|
}
|
||
|
}
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
ok = false;
|
||
|
}
|
||
|
EndWaitCursor();
|
||
|
|
||
|
if(ok)
|
||
|
{
|
||
|
if(setPath)
|
||
|
{
|
||
|
// Set new path for this file, unless we are saving a template or a copy, in which case we want to keep the old file path.
|
||
|
SetPathName(filename);
|
||
|
}
|
||
|
logcapturer.ShowLog(true);
|
||
|
if(TrackerSettings::Instance().rememberSongWindows)
|
||
|
SerializeViews();
|
||
|
} else
|
||
|
{
|
||
|
ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
|
||
|
}
|
||
|
return ok;
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL CModDoc::SaveModified()
|
||
|
{
|
||
|
if(m_SndFile.GetType() == MOD_TYPE_MPT && !SaveAllSamples())
|
||
|
return FALSE;
|
||
|
return CDocument::SaveModified();
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SaveAllSamples(bool showPrompt)
|
||
|
{
|
||
|
if(showPrompt)
|
||
|
{
|
||
|
ModifiedExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame());
|
||
|
return dlg.DoModal() == IDOK;
|
||
|
} else
|
||
|
{
|
||
|
bool ok = true;
|
||
|
for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++)
|
||
|
{
|
||
|
ok &= SaveSample(smp);
|
||
|
}
|
||
|
return ok;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SaveSample(SAMPLEINDEX smp)
|
||
|
{
|
||
|
bool success = false;
|
||
|
if(smp > 0 && smp <= GetNumSamples())
|
||
|
{
|
||
|
const mpt::PathString filename = m_SndFile.GetSamplePath(smp);
|
||
|
if(!filename.empty())
|
||
|
{
|
||
|
auto &sample = m_SndFile.GetSample(smp);
|
||
|
const auto ext = filename.GetFileExt().ToUnicode().substr(1);
|
||
|
const auto format = FromSettingValue<SampleEditorDefaultFormat>(ext);
|
||
|
|
||
|
try
|
||
|
{
|
||
|
mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
if(sf)
|
||
|
{
|
||
|
mpt::ofstream &f = sf;
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
|
||
|
if(sample.uFlags[CHN_ADLIB] || format == dfS3I)
|
||
|
success = m_SndFile.SaveS3ISample(smp, f);
|
||
|
else if(format != dfWAV)
|
||
|
success = m_SndFile.SaveFLACSample(smp, f);
|
||
|
else
|
||
|
success = m_SndFile.SaveWAVSample(smp, f);
|
||
|
}
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
success = false;
|
||
|
}
|
||
|
|
||
|
if(success)
|
||
|
sample.uFlags.reset(SMP_MODIFIED);
|
||
|
else
|
||
|
AddToLog(LogError, MPT_UFORMAT("Unable to save sample {}: {}")(smp, filename));
|
||
|
}
|
||
|
}
|
||
|
return success;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnCloseDocument()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if(pMainFrm) pMainFrm->OnDocumentClosed(this);
|
||
|
CDocument::OnCloseDocument();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::DeleteContents()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm) pMainFrm->StopMod(this);
|
||
|
m_SndFile.Destroy();
|
||
|
ReinitRecordState();
|
||
|
}
|
||
|
|
||
|
|
||
|
BOOL CModDoc::DoSave(const mpt::PathString &filename, bool setPath)
|
||
|
{
|
||
|
const mpt::PathString docFileName = GetPathNameMpt();
|
||
|
const std::string defaultExtension = m_SndFile.GetModSpecifications().fileExtension;
|
||
|
|
||
|
switch(m_SndFile.GetBestSaveFormat())
|
||
|
{
|
||
|
case MOD_TYPE_MOD:
|
||
|
MsgBoxHidable(ModSaveHint);
|
||
|
break;
|
||
|
case MOD_TYPE_S3M:
|
||
|
break;
|
||
|
case MOD_TYPE_XM:
|
||
|
MsgBoxHidable(XMCompatibilityExportTip);
|
||
|
break;
|
||
|
case MOD_TYPE_IT:
|
||
|
MsgBoxHidable(ItCompatibilityExportTip);
|
||
|
break;
|
||
|
case MOD_TYPE_MPT:
|
||
|
break;
|
||
|
default:
|
||
|
ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
|
||
|
return FALSE;
|
||
|
}
|
||
|
|
||
|
mpt::PathString ext = P_(".") + mpt::PathString::FromUTF8(defaultExtension);
|
||
|
|
||
|
mpt::PathString saveFileName;
|
||
|
|
||
|
if(filename.empty() || m_ShowSavedialog)
|
||
|
{
|
||
|
mpt::PathString drive = docFileName.GetDrive();
|
||
|
mpt::PathString dir = docFileName.GetDir();
|
||
|
mpt::PathString fileName = docFileName.GetFileName();
|
||
|
if(fileName.empty())
|
||
|
{
|
||
|
fileName = mpt::PathString::FromCString(GetTitle()).SanitizeComponent();
|
||
|
}
|
||
|
mpt::PathString defaultSaveName = drive + dir + fileName + ext;
|
||
|
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension(defaultExtension)
|
||
|
.DefaultFilename(defaultSaveName)
|
||
|
.ExtensionFilter(ModTypeToFilter(m_SndFile))
|
||
|
.WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir());
|
||
|
if(!dlg.Show()) return FALSE;
|
||
|
|
||
|
TrackerSettings::Instance().PathSongs.SetWorkingDir(dlg.GetWorkingDirectory());
|
||
|
|
||
|
saveFileName = dlg.GetFirstFile();
|
||
|
} else
|
||
|
{
|
||
|
saveFileName = filename;
|
||
|
}
|
||
|
|
||
|
// Do we need to create a backup file ?
|
||
|
if((TrackerSettings::Instance().CreateBackupFiles)
|
||
|
&& (IsModified()) && (!mpt::PathString::CompareNoCase(saveFileName, docFileName)))
|
||
|
{
|
||
|
if(saveFileName.IsFile())
|
||
|
{
|
||
|
mpt::PathString backupFileName = saveFileName.ReplaceExt(P_(".bak"));
|
||
|
if(backupFileName.IsFile())
|
||
|
{
|
||
|
DeleteFile(backupFileName.AsNative().c_str());
|
||
|
}
|
||
|
MoveFile(saveFileName.AsNative().c_str(), backupFileName.AsNative().c_str());
|
||
|
}
|
||
|
}
|
||
|
if(OnSaveDocument(saveFileName, setPath))
|
||
|
{
|
||
|
SetModified(false);
|
||
|
m_SndFile.m_SongFlags.reset(SONG_IMPORTED);
|
||
|
m_bHasValidPath = true;
|
||
|
m_ShowSavedialog = false;
|
||
|
CMainFrame::GetMainFrame()->UpdateTree(this, GeneralHint().General()); // Update treeview (e.g. filename might have changed)
|
||
|
return TRUE;
|
||
|
} else
|
||
|
{
|
||
|
return FALSE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnAppendModule()
|
||
|
{
|
||
|
FileDialog::PathList files;
|
||
|
CTrackApp::OpenModulesDialog(files);
|
||
|
|
||
|
ScopedLogCapturer logcapture(*this, _T("Append Failures"));
|
||
|
try
|
||
|
{
|
||
|
auto source = std::make_unique<CSoundFile>();
|
||
|
for(const auto &file : files)
|
||
|
{
|
||
|
InputFile f(file, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
|
||
|
if(!f.IsValid())
|
||
|
{
|
||
|
AddToLog("Unable to open source file!");
|
||
|
continue;
|
||
|
}
|
||
|
try
|
||
|
{
|
||
|
if(!source->Create(GetFileReader(f), CSoundFile::loadCompleteModule))
|
||
|
{
|
||
|
AddToLog("Unable to open source file!");
|
||
|
continue;
|
||
|
}
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
AddToLog("Unable to open source file!");
|
||
|
continue;
|
||
|
}
|
||
|
AppendModule(*source);
|
||
|
source->Destroy();
|
||
|
SetModified();
|
||
|
}
|
||
|
} catch(mpt::out_of_memory e)
|
||
|
{
|
||
|
mpt::delete_out_of_memory(e);
|
||
|
AddToLog("Out of memory.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
UpdateAllViews(nullptr, SequenceHint().Data().ModType());
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::InitializeMod()
|
||
|
{
|
||
|
// New module ?
|
||
|
if (!m_SndFile.m_nChannels)
|
||
|
{
|
||
|
switch(GetModType())
|
||
|
{
|
||
|
case MOD_TYPE_MOD:
|
||
|
m_SndFile.m_nChannels = 4;
|
||
|
break;
|
||
|
case MOD_TYPE_S3M:
|
||
|
m_SndFile.m_nChannels = 16;
|
||
|
break;
|
||
|
default:
|
||
|
m_SndFile.m_nChannels = 32;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
SetDefaultChannelColors();
|
||
|
|
||
|
if(GetModType() == MOD_TYPE_MPT)
|
||
|
{
|
||
|
m_SndFile.m_nTempoMode = TempoMode::Modern;
|
||
|
m_SndFile.m_SongFlags.set(SONG_EXFILTERRANGE);
|
||
|
}
|
||
|
m_SndFile.SetDefaultPlaybackBehaviour(GetModType());
|
||
|
|
||
|
// Refresh mix levels now that the correct mod type has been set
|
||
|
m_SndFile.SetMixLevels(m_SndFile.GetModSpecifications().defaultMixLevels);
|
||
|
|
||
|
m_SndFile.Order().assign(1, 0);
|
||
|
if (!m_SndFile.Patterns.IsValidPat(0))
|
||
|
{
|
||
|
m_SndFile.Patterns.Insert(0, 64);
|
||
|
}
|
||
|
|
||
|
Clear(m_SndFile.m_szNames);
|
||
|
|
||
|
m_SndFile.m_PlayState.m_nMusicTempo.Set(125);
|
||
|
m_SndFile.m_nDefaultTempo.Set(125);
|
||
|
m_SndFile.m_PlayState.m_nMusicSpeed = m_SndFile.m_nDefaultSpeed = 6;
|
||
|
|
||
|
// Set up mix levels
|
||
|
m_SndFile.m_PlayState.m_nGlobalVolume = m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME;
|
||
|
m_SndFile.m_nSamplePreAmp = m_SndFile.m_nVSTiVolume = 48;
|
||
|
|
||
|
for (CHANNELINDEX nChn = 0; nChn < MAX_BASECHANNELS; nChn++)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.reset();
|
||
|
m_SndFile.ChnSettings[nChn].nVolume = 64;
|
||
|
m_SndFile.ChnSettings[nChn].nPan = 128;
|
||
|
m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = 64;
|
||
|
}
|
||
|
// Setup LRRL panning scheme for MODs
|
||
|
m_SndFile.SetupMODPanning();
|
||
|
}
|
||
|
if (!m_SndFile.m_nSamples)
|
||
|
{
|
||
|
m_SndFile.m_szNames[1] = "untitled";
|
||
|
m_SndFile.m_nSamples = (GetModType() == MOD_TYPE_MOD) ? 31 : 1;
|
||
|
|
||
|
SampleEdit::ResetSamples(m_SndFile, SampleEdit::SmpResetInit);
|
||
|
|
||
|
m_SndFile.GetSample(1).Initialize(m_SndFile.GetType());
|
||
|
|
||
|
if ((!m_SndFile.m_nInstruments) && (m_SndFile.GetType() & MOD_TYPE_XM))
|
||
|
{
|
||
|
if(m_SndFile.AllocateInstrument(1, 1))
|
||
|
{
|
||
|
m_SndFile.m_nInstruments = 1;
|
||
|
InitializeInstrument(m_SndFile.Instruments[1]);
|
||
|
}
|
||
|
}
|
||
|
if (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))
|
||
|
{
|
||
|
m_SndFile.m_SongFlags.set(SONG_LINEARSLIDES);
|
||
|
}
|
||
|
}
|
||
|
m_SndFile.ResetPlayPos();
|
||
|
m_SndFile.m_songArtist = TrackerSettings::Instance().defaultArtist;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SetDefaultChannelColors(CHANNELINDEX minChannel, CHANNELINDEX maxChannel)
|
||
|
{
|
||
|
LimitMax(minChannel, GetNumChannels());
|
||
|
LimitMax(maxChannel, GetNumChannels());
|
||
|
if(maxChannel < minChannel)
|
||
|
std::swap(minChannel, maxChannel);
|
||
|
bool modified = false;
|
||
|
if(TrackerSettings::Instance().defaultRainbowChannelColors != DefaultChannelColors::NoColors)
|
||
|
{
|
||
|
const bool rainbow = TrackerSettings::Instance().defaultRainbowChannelColors == DefaultChannelColors::Rainbow;
|
||
|
CHANNELINDEX numGroups = 0;
|
||
|
if(rainbow)
|
||
|
{
|
||
|
for(CHANNELINDEX i = minChannel + 1u; i < maxChannel; i++)
|
||
|
{
|
||
|
if(m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName)
|
||
|
numGroups++;
|
||
|
}
|
||
|
}
|
||
|
const double hueFactor = rainbow ? (1.5 * mpt::numbers::pi) / std::max(1, numGroups - 1) : 1000.0; // Three quarters of the color wheel, red to purple
|
||
|
for(CHANNELINDEX i = minChannel, group = minChannel; i < maxChannel; i++)
|
||
|
{
|
||
|
if(i > minChannel && (m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName))
|
||
|
group++;
|
||
|
const double hue = group * hueFactor; // 0...2pi
|
||
|
const double saturation = 0.3; // 0...2/3
|
||
|
const double brightness = 1.2; // 0...4/3
|
||
|
const double r = brightness * (1 + saturation * (std::cos(hue) - 1.0));
|
||
|
const double g = brightness * (1 + saturation * (std::cos(hue - 2.09439) - 1.0));
|
||
|
const double b = brightness * (1 + saturation * (std::cos(hue + 2.09439) - 1.0));
|
||
|
const auto color = RGB(mpt::saturate_round<uint8>(r * 255), mpt::saturate_round<uint8>(g * 255), mpt::saturate_round<uint8>(b * 255));
|
||
|
if(m_SndFile.ChnSettings[i].color != color)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[i].color = color;
|
||
|
modified = true;
|
||
|
}
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
for(CHANNELINDEX i = minChannel; i < maxChannel; i++)
|
||
|
{
|
||
|
if(m_SndFile.ChnSettings[i].color != ModChannelSettings::INVALID_COLOR)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[i].color = ModChannelSettings::INVALID_COLOR;
|
||
|
modified = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return modified;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::PostMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||
|
{
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
while(pos != nullptr)
|
||
|
{
|
||
|
if(CView *pView = GetNextView(pos); pView != nullptr)
|
||
|
pView->PostMessage(uMsg, wParam, lParam);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SendNotifyMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||
|
{
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
while(pos != nullptr)
|
||
|
{
|
||
|
if(CView *pView = GetNextView(pos); pView != nullptr)
|
||
|
pView->SendNotifyMessage(uMsg, wParam, lParam);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SendMessageToActiveView(UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||
|
{
|
||
|
if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr)
|
||
|
{
|
||
|
lastActiveFrame->SendMessageToDescendants(uMsg, wParam, lParam);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ViewPattern(UINT nPat, UINT nOrd)
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, ((nPat+1) << 16) | nOrd);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ViewSample(UINT nSmp)
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, nSmp);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ViewInstrument(UINT nIns)
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, nIns);
|
||
|
}
|
||
|
|
||
|
|
||
|
ScopedLogCapturer::ScopedLogCapturer(CModDoc &modDoc, const CString &title, CWnd *parent, bool showLog) :
|
||
|
m_modDoc(modDoc), m_oldLogMode(m_modDoc.GetLogMode()), m_title(title), m_pParent(parent), m_showLog(showLog)
|
||
|
{
|
||
|
m_modDoc.SetLogMode(LogModeGather);
|
||
|
}
|
||
|
|
||
|
|
||
|
void ScopedLogCapturer::ShowLog(bool force)
|
||
|
{
|
||
|
if(force || m_oldLogMode == LogModeInstantReporting)
|
||
|
{
|
||
|
m_modDoc.ShowLog(m_title, m_pParent);
|
||
|
m_modDoc.ClearLog();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void ScopedLogCapturer::ShowLog(const std::string &preamble, bool force)
|
||
|
{
|
||
|
if(force || m_oldLogMode == LogModeInstantReporting)
|
||
|
{
|
||
|
m_modDoc.ShowLog(mpt::ToCString(mpt::Charset::Locale, preamble), m_title, m_pParent);
|
||
|
m_modDoc.ClearLog();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void ScopedLogCapturer::ShowLog(const CString &preamble, bool force)
|
||
|
{
|
||
|
if(force || m_oldLogMode == LogModeInstantReporting)
|
||
|
{
|
||
|
m_modDoc.ShowLog(preamble, m_title, m_pParent);
|
||
|
m_modDoc.ClearLog();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void ScopedLogCapturer::ShowLog(const mpt::ustring &preamble, bool force)
|
||
|
{
|
||
|
if(force || m_oldLogMode == LogModeInstantReporting)
|
||
|
{
|
||
|
m_modDoc.ShowLog(mpt::ToCString(preamble), m_title, m_pParent);
|
||
|
m_modDoc.ClearLog();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
ScopedLogCapturer::~ScopedLogCapturer()
|
||
|
{
|
||
|
if(m_showLog)
|
||
|
ShowLog();
|
||
|
else
|
||
|
m_modDoc.ClearLog();
|
||
|
m_modDoc.SetLogMode(m_oldLogMode);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::AddToLog(LogLevel level, const mpt::ustring &text) const
|
||
|
{
|
||
|
if(m_LogMode == LogModeGather)
|
||
|
{
|
||
|
m_Log.push_back(LogEntry(level, text));
|
||
|
} else
|
||
|
{
|
||
|
if(level < LogDebug)
|
||
|
{
|
||
|
Reporting::Message(level, text);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
mpt::ustring CModDoc::GetLogString() const
|
||
|
{
|
||
|
mpt::ustring ret;
|
||
|
for(const auto &i : m_Log)
|
||
|
{
|
||
|
ret += i.message;
|
||
|
ret += U_("\r\n");
|
||
|
}
|
||
|
return ret;
|
||
|
}
|
||
|
|
||
|
|
||
|
LogLevel CModDoc::GetMaxLogLevel() const
|
||
|
{
|
||
|
LogLevel retval = LogInformation;
|
||
|
// find the most severe loglevel
|
||
|
for(const auto &i : m_Log)
|
||
|
{
|
||
|
retval = std::min(retval, i.level);
|
||
|
}
|
||
|
return retval;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ClearLog()
|
||
|
{
|
||
|
m_Log.clear();
|
||
|
}
|
||
|
|
||
|
|
||
|
UINT CModDoc::ShowLog(const CString &preamble, const CString &title, CWnd *parent)
|
||
|
{
|
||
|
if(!parent) parent = CMainFrame::GetMainFrame();
|
||
|
if(GetLog().size() > 0)
|
||
|
{
|
||
|
LogLevel level = GetMaxLogLevel();
|
||
|
if(level < LogDebug)
|
||
|
{
|
||
|
CString text = preamble + mpt::ToCString(GetLogString());
|
||
|
CString actualTitle = (title.GetLength() == 0) ? CString(MAINFRAME_TITLE) : title;
|
||
|
Reporting::Message(level, text, actualTitle, parent);
|
||
|
return IDOK;
|
||
|
}
|
||
|
}
|
||
|
return IDCANCEL;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ProcessMIDI(uint32 midiData, INSTRUMENTINDEX ins, IMixPlugin *plugin, InputTargetContext ctx)
|
||
|
{
|
||
|
static uint8 midiVolume = 127;
|
||
|
|
||
|
MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData);
|
||
|
const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData);
|
||
|
const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData);
|
||
|
const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData);
|
||
|
uint8 note = midiByte1 + NOTE_MIN;
|
||
|
int vol = midiByte2;
|
||
|
|
||
|
if((event == MIDIEvents::evNoteOn) && !vol)
|
||
|
event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd
|
||
|
|
||
|
PLUGINDEX mappedIndex = 0;
|
||
|
PlugParamIndex paramIndex = 0;
|
||
|
uint16 paramValue = 0;
|
||
|
bool captured = m_SndFile.GetMIDIMapper().OnMIDImsg(midiData, mappedIndex, paramIndex, paramValue);
|
||
|
|
||
|
// Handle MIDI messages assigned to shortcuts
|
||
|
CInputHandler *ih = CMainFrame::GetInputHandler();
|
||
|
if(ih->HandleMIDIMessage(ctx, midiData) != kcNull
|
||
|
|| ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull)
|
||
|
{
|
||
|
// Mapped to a command, no need to pass message on.
|
||
|
captured = true;
|
||
|
}
|
||
|
|
||
|
if(captured)
|
||
|
{
|
||
|
// Event captured by MIDI mapping or shortcut, no need to pass message on.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
switch(event)
|
||
|
{
|
||
|
case MIDIEvents::evNoteOff:
|
||
|
if(m_midiSustainActive[channel])
|
||
|
{
|
||
|
m_midiSustainBuffer[channel].push_back(midiData);
|
||
|
return;
|
||
|
}
|
||
|
if(ins > 0 && ins <= GetNumInstruments())
|
||
|
{
|
||
|
LimitMax(note, NOTE_MAX);
|
||
|
if(m_midiPlayingNotes[channel][note])
|
||
|
m_midiPlayingNotes[channel][note] = false;
|
||
|
NoteOff(note, false, ins, m_noteChannel[note - NOTE_MIN]);
|
||
|
return;
|
||
|
} else if(plugin != nullptr)
|
||
|
{
|
||
|
plugin->MidiSend(midiData);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case MIDIEvents::evNoteOn:
|
||
|
if(ins > 0 && ins <= GetNumInstruments())
|
||
|
{
|
||
|
LimitMax(note, NOTE_MAX);
|
||
|
vol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume);
|
||
|
PlayNote(PlayNoteParam(note).Instrument(ins).Volume(vol).CheckNNA(m_midiPlayingNotes[channel]), &m_noteChannel);
|
||
|
return;
|
||
|
} else if(plugin != nullptr)
|
||
|
{
|
||
|
plugin->MidiSend(midiData);
|
||
|
}
|
||
|
break;
|
||
|
|
||
|
case MIDIEvents::evControllerChange:
|
||
|
switch(midiByte1)
|
||
|
{
|
||
|
case MIDIEvents::MIDICC_Volume_Coarse:
|
||
|
midiVolume = midiByte2;
|
||
|
break;
|
||
|
case MIDIEvents::MIDICC_HoldPedal_OnOff:
|
||
|
m_midiSustainActive[channel] = (midiByte2 >= 0x40);
|
||
|
if(!m_midiSustainActive[channel])
|
||
|
{
|
||
|
// Release all notes
|
||
|
for(const auto offEvent : m_midiSustainBuffer[channel])
|
||
|
{
|
||
|
ProcessMIDI(offEvent, ins, plugin, ctx);
|
||
|
}
|
||
|
m_midiSustainBuffer[channel].clear();
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if((TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDITOPLUG) && CMainFrame::GetMainFrame()->GetModPlaying() == this && plugin != nullptr)
|
||
|
{
|
||
|
plugin->MidiSend(midiData);
|
||
|
// Sending midi may modify the plug. For now, if MIDI data is not active sensing or aftertouch messages, set modified.
|
||
|
if(midiData != MIDIEvents::System(MIDIEvents::sysActiveSense)
|
||
|
&& event != MIDIEvents::evPolyAftertouch && event != MIDIEvents::evChannelAftertouch
|
||
|
&& event != MIDIEvents::evPitchBend
|
||
|
&& m_SndFile.GetModSpecifications().supportsPlugins)
|
||
|
{
|
||
|
SetModified();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
CHANNELINDEX CModDoc::PlayNote(PlayNoteParam ¶ms, NoteToChannelMap *noteChannel)
|
||
|
{
|
||
|
CHANNELINDEX channel = GetNumChannels();
|
||
|
|
||
|
ModCommand::NOTE note = params.m_note;
|
||
|
if(ModCommand::IsNote(ModCommand::NOTE(note)))
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if(pMainFrm == nullptr || note == NOTE_NONE) return CHANNELINDEX_INVALID;
|
||
|
if (pMainFrm->GetModPlaying() != this)
|
||
|
{
|
||
|
// All notes off when resuming paused playback
|
||
|
m_SndFile.ResetChannels();
|
||
|
|
||
|
m_SndFile.m_SongFlags.set(SONG_PAUSED);
|
||
|
pMainFrm->PlayMod(this);
|
||
|
}
|
||
|
|
||
|
CriticalSection cs;
|
||
|
|
||
|
if(params.m_notesPlaying)
|
||
|
CheckNNA(note, params.m_instr, *params.m_notesPlaying);
|
||
|
|
||
|
// Find a channel to play on
|
||
|
channel = FindAvailableChannel();
|
||
|
ModChannel &chn = m_SndFile.m_PlayState.Chn[channel];
|
||
|
|
||
|
// reset channel properties; in theory the chan is completely unused anyway.
|
||
|
chn.Reset(ModChannel::resetTotal, m_SndFile, CHANNELINDEX_INVALID, CHN_MUTE);
|
||
|
chn.nNewNote = chn.nLastNote = static_cast<uint8>(note);
|
||
|
chn.nVolume = 256;
|
||
|
|
||
|
if(params.m_instr)
|
||
|
{
|
||
|
// Set instrument (or sample if there are no instruments)
|
||
|
chn.ResetEnvelopes();
|
||
|
m_SndFile.InstrumentChange(chn, params.m_instr);
|
||
|
} else if(params.m_sample > 0 && params.m_sample <= GetNumSamples()) // Or set sample explicitely
|
||
|
{
|
||
|
ModSample &sample = m_SndFile.GetSample(params.m_sample);
|
||
|
chn.pCurrentSample = sample.samplev();
|
||
|
chn.pModInstrument = nullptr;
|
||
|
chn.pModSample = &sample;
|
||
|
chn.nFineTune = sample.nFineTune;
|
||
|
chn.nC5Speed = sample.nC5Speed;
|
||
|
chn.nLoopStart = sample.nLoopStart;
|
||
|
chn.nLoopEnd = sample.nLoopEnd;
|
||
|
chn.dwFlags = (sample.uFlags & (CHN_SAMPLEFLAGS & ~CHN_MUTE));
|
||
|
chn.nPan = 128;
|
||
|
if(sample.uFlags[CHN_PANNING]) chn.nPan = sample.nPan;
|
||
|
chn.UpdateInstrumentVolume(&sample, nullptr);
|
||
|
}
|
||
|
chn.nFadeOutVol = 0x10000;
|
||
|
chn.isPreviewNote = true;
|
||
|
if(params.m_currentChannel != CHANNELINDEX_INVALID)
|
||
|
chn.nMasterChn = params.m_currentChannel + 1;
|
||
|
else
|
||
|
chn.nMasterChn = 0;
|
||
|
|
||
|
if(chn.dwFlags[CHN_ADLIB] && chn.pModSample && m_SndFile.m_opl)
|
||
|
{
|
||
|
m_SndFile.m_opl->Patch(channel, chn.pModSample->adlib);
|
||
|
}
|
||
|
|
||
|
m_SndFile.NoteChange(chn, note, false, true, true, channel);
|
||
|
if(params.m_volume >= 0) chn.nVolume = std::min(params.m_volume, 256);
|
||
|
|
||
|
// Handle sample looping.
|
||
|
// Changed line to fix http://forum.openmpt.org/index.php?topic=1700.0
|
||
|
//if ((loopstart + 16 < loopend) && (loopstart >= 0) && (loopend <= (LONG)pchn.nLength))
|
||
|
if ((params.m_loopStart + 16 < params.m_loopEnd) && (params.m_loopStart >= 0) && (chn.pModSample != nullptr))
|
||
|
{
|
||
|
chn.position.Set(params.m_loopStart);
|
||
|
chn.nLoopStart = params.m_loopStart;
|
||
|
chn.nLoopEnd = params.m_loopEnd;
|
||
|
chn.nLength = std::min(params.m_loopEnd, chn.pModSample->nLength);
|
||
|
}
|
||
|
|
||
|
// Handle extra-loud flag
|
||
|
chn.dwFlags.set(CHN_EXTRALOUD, !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOEXTRALOUD) && params.m_sample);
|
||
|
|
||
|
// Handle custom start position
|
||
|
if(params.m_sampleOffset > 0 && chn.pModSample)
|
||
|
{
|
||
|
chn.position.Set(params.m_sampleOffset);
|
||
|
// If start position is after loop end, set loop end to sample end so that the sample starts
|
||
|
// playing.
|
||
|
if(chn.nLoopEnd < params.m_sampleOffset)
|
||
|
chn.nLength = chn.nLoopEnd = chn.pModSample->nLength;
|
||
|
}
|
||
|
|
||
|
// VSTi preview
|
||
|
if(params.m_instr > 0 && params.m_instr <= m_SndFile.GetNumInstruments())
|
||
|
{
|
||
|
const ModInstrument *pIns = m_SndFile.Instruments[params.m_instr];
|
||
|
if (pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan
|
||
|
{
|
||
|
PLUGINDEX nPlugin = 0;
|
||
|
if (chn.pModInstrument)
|
||
|
nPlugin = chn.pModInstrument->nMixPlug; // First try instrument plugin
|
||
|
if ((!nPlugin || nPlugin > MAX_MIXPLUGINS) && params.m_currentChannel != CHANNELINDEX_INVALID)
|
||
|
nPlugin = m_SndFile.ChnSettings[params.m_currentChannel].nMixPlugin; // Then try channel plugin
|
||
|
|
||
|
if ((nPlugin) && (nPlugin <= MAX_MIXPLUGINS))
|
||
|
{
|
||
|
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[nPlugin - 1].pMixPlugin;
|
||
|
if(pPlugin != nullptr)
|
||
|
{
|
||
|
pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN], static_cast<uint16>(chn.nVolume), channel);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Remove channel from list of mixed channels to fix https://bugs.openmpt.org/view.php?id=209
|
||
|
// This is required because a previous note on the same channel might have just stopped playing,
|
||
|
// but the channel is still in the mix list.
|
||
|
// Since the channel volume / etc is only updated every tick in CSoundFile::ReadNote, and we
|
||
|
// do not want to duplicate mixmode-dependant logic here, CSoundFile::CreateStereoMix may already
|
||
|
// try to mix our newly set up channel at volume 0 if we don't remove it from the list.
|
||
|
auto mixBegin = std::begin(m_SndFile.m_PlayState.ChnMix);
|
||
|
auto mixEnd = std::remove(mixBegin, mixBegin + m_SndFile.m_nMixChannels, channel);
|
||
|
m_SndFile.m_nMixChannels = static_cast<CHANNELINDEX>(std::distance(mixBegin, mixEnd));
|
||
|
|
||
|
if(noteChannel)
|
||
|
{
|
||
|
noteChannel->at(note - NOTE_MIN) = channel;
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
CriticalSection cs;
|
||
|
// Apply note cut / off / fade (also on preview channels)
|
||
|
m_SndFile.NoteChange(m_SndFile.m_PlayState.Chn[channel], note);
|
||
|
for(CHANNELINDEX c = m_SndFile.GetNumChannels(); c < MAX_CHANNELS; c++)
|
||
|
{
|
||
|
ModChannel &chn = m_SndFile.m_PlayState.Chn[c];
|
||
|
if(chn.isPreviewNote && (chn.pModSample || chn.pModInstrument))
|
||
|
{
|
||
|
m_SndFile.NoteChange(chn, note);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return channel;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::NoteOff(UINT note, bool fade, INSTRUMENTINDEX ins, CHANNELINDEX currentChn)
|
||
|
{
|
||
|
CriticalSection cs;
|
||
|
|
||
|
if(ins != INSTRUMENTINDEX_INVALID && ins <= m_SndFile.GetNumInstruments() && ModCommand::IsNote(ModCommand::NOTE(note)))
|
||
|
{
|
||
|
const ModInstrument *pIns = m_SndFile.Instruments[ins];
|
||
|
if(pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan
|
||
|
{
|
||
|
PLUGINDEX plug = pIns->nMixPlug; // First try intrument VST
|
||
|
if((!plug || plug > MAX_MIXPLUGINS) // No good plug yet
|
||
|
&& currentChn < MAX_BASECHANNELS) // Chan OK
|
||
|
{
|
||
|
plug = m_SndFile.ChnSettings[currentChn].nMixPlugin;// Then try Channel VST
|
||
|
}
|
||
|
|
||
|
if(plug && plug <= MAX_MIXPLUGINS)
|
||
|
{
|
||
|
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plug - 1].pMixPlugin;
|
||
|
if(pPlugin)
|
||
|
{
|
||
|
pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN] + NOTE_KEYOFF, 0, currentChn);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const FlagSet<ChannelFlags> mask = (fade ? CHN_NOTEFADE : (CHN_NOTEFADE | CHN_KEYOFF));
|
||
|
const CHANNELINDEX startChn = currentChn != CHANNELINDEX_INVALID ? currentChn : m_SndFile.m_nChannels;
|
||
|
const CHANNELINDEX endChn = currentChn != CHANNELINDEX_INVALID ? currentChn + 1 : MAX_CHANNELS;
|
||
|
ModChannel *pChn = &m_SndFile.m_PlayState.Chn[startChn];
|
||
|
for(CHANNELINDEX i = startChn; i < endChn; i++, pChn++)
|
||
|
{
|
||
|
// Fade all channels > m_nChannels which are playing this note and aren't NNA channels.
|
||
|
if((pChn->isPreviewNote || i < m_SndFile.GetNumChannels())
|
||
|
&& !pChn->dwFlags[mask]
|
||
|
&& (pChn->nLength || pChn->dwFlags[CHN_ADLIB])
|
||
|
&& (note == pChn->nNewNote || note == NOTE_NONE))
|
||
|
{
|
||
|
m_SndFile.KeyOff(*pChn);
|
||
|
if (!m_SndFile.m_nInstruments) pChn->dwFlags.reset(CHN_LOOP | CHN_PINGPONGFLAG);
|
||
|
if (fade) pChn->dwFlags.set(CHN_NOTEFADE);
|
||
|
// Instantly stop samples that would otherwise play forever
|
||
|
if (pChn->pModInstrument && !pChn->pModInstrument->nFadeOut)
|
||
|
pChn->nFadeOutVol = 0;
|
||
|
if(pChn->dwFlags[CHN_ADLIB] && m_SndFile.m_opl)
|
||
|
{
|
||
|
m_SndFile.m_opl->NoteOff(i);
|
||
|
}
|
||
|
if (note) break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Apply DNA/NNA settings for note preview. It will also set the specified note to be playing in the playingNotes set.
|
||
|
void CModDoc::CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes)
|
||
|
{
|
||
|
if(ins > GetNumInstruments() || m_SndFile.Instruments[ins] == nullptr || note >= playingNotes.size())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
const ModInstrument *pIns = m_SndFile.Instruments[ins];
|
||
|
for(CHANNELINDEX chn = GetNumChannels(); chn < MAX_CHANNELS; chn++)
|
||
|
{
|
||
|
const ModChannel &channel = m_SndFile.m_PlayState.Chn[chn];
|
||
|
if(channel.pModInstrument == pIns && channel.isPreviewNote && ModCommand::IsNote(channel.nLastNote)
|
||
|
&& (channel.nLength || pIns->HasValidMIDIChannel()) && !playingNotes[channel.nLastNote])
|
||
|
{
|
||
|
CHANNELINDEX nnaChn = m_SndFile.CheckNNA(chn, ins, note, false);
|
||
|
if(nnaChn != CHANNELINDEX_INVALID)
|
||
|
{
|
||
|
// Keep the new NNA channel playing in the same channel slot.
|
||
|
// That way, we do not need to touch the ChnMix array, and we avoid the same channel being checked twice.
|
||
|
if(nnaChn != chn)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[chn] = std::move(m_SndFile.m_PlayState.Chn[nnaChn]);
|
||
|
m_SndFile.m_PlayState.Chn[nnaChn] = {};
|
||
|
}
|
||
|
// Avoid clicks if the channel wasn't ramping before.
|
||
|
m_SndFile.m_PlayState.Chn[chn].dwFlags.set(CHN_FASTVOLRAMP);
|
||
|
m_SndFile.ProcessRamping(m_SndFile.m_PlayState.Chn[chn]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
playingNotes.set(note);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Check if a given note of an instrument or sample is playing from the editor.
|
||
|
// If note == 0, just check if an instrument or sample is playing.
|
||
|
bool CModDoc::IsNotePlaying(UINT note, SAMPLEINDEX nsmp, INSTRUMENTINDEX nins)
|
||
|
{
|
||
|
ModChannel *pChn = &m_SndFile.m_PlayState.Chn[m_SndFile.GetNumChannels()];
|
||
|
for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++, pChn++) if (pChn->isPreviewNote)
|
||
|
{
|
||
|
if(pChn->nLength != 0 && !pChn->dwFlags[CHN_NOTEFADE | CHN_KEYOFF| CHN_MUTE]
|
||
|
&& (note == pChn->nNewNote || note == NOTE_NONE)
|
||
|
&& (pChn->pModSample == &m_SndFile.GetSample(nsmp) || !nsmp)
|
||
|
&& (pChn->pModInstrument == m_SndFile.Instruments[nins] || !nins)) return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::MuteToggleModifiesDocument() const
|
||
|
{
|
||
|
return (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)) && TrackerSettings::Instance().MiscSaveChannelMuteStatus;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::MuteChannel(CHANNELINDEX nChn, bool doMute)
|
||
|
{
|
||
|
if (nChn >= m_SndFile.GetNumChannels())
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
// Mark channel as muted in channel settings
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_MUTE, doMute);
|
||
|
|
||
|
const bool success = UpdateChannelMuteStatus(nChn);
|
||
|
if(success && MuteToggleModifiesDocument())
|
||
|
{
|
||
|
SetModified();
|
||
|
}
|
||
|
|
||
|
return success;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::UpdateChannelMuteStatus(CHANNELINDEX nChn)
|
||
|
{
|
||
|
const ChannelFlags muteType = CSoundFile::GetChannelMuteFlag();
|
||
|
|
||
|
if (nChn >= m_SndFile.GetNumChannels())
|
||
|
{
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
const bool doMute = m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];
|
||
|
|
||
|
// Mute pattern channel
|
||
|
if (doMute)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(muteType);
|
||
|
if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(nChn);
|
||
|
// Kill VSTi notes on muted channel.
|
||
|
PLUGINDEX nPlug = m_SndFile.GetBestPlugin(m_SndFile.m_PlayState, nChn, PrioritiseInstrument, EvenIfMuted);
|
||
|
if ((nPlug) && (nPlug<=MAX_MIXPLUGINS))
|
||
|
{
|
||
|
IMixPlugin *pPlug = m_SndFile.m_MixPlugins[nPlug - 1].pMixPlugin;
|
||
|
const ModInstrument* pIns = m_SndFile.m_PlayState.Chn[nChn].pModInstrument;
|
||
|
if (pPlug && pIns)
|
||
|
{
|
||
|
pPlug->MidiCommand(*pIns, NOTE_KEYOFF, 0, nChn);
|
||
|
}
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
|
||
|
m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
|
||
|
}
|
||
|
|
||
|
// Mute any NNA'd channels
|
||
|
for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
|
||
|
{
|
||
|
if (m_SndFile.m_PlayState.Chn[i].nMasterChn == nChn + 1u)
|
||
|
{
|
||
|
if (doMute)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[i].dwFlags.set(muteType);
|
||
|
if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(i);
|
||
|
} else
|
||
|
{
|
||
|
// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
|
||
|
m_SndFile.m_PlayState.Chn[i].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsChannelSolo(CHANNELINDEX nChn) const
|
||
|
{
|
||
|
if (nChn >= m_SndFile.m_nChannels) return true;
|
||
|
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_SOLO];
|
||
|
}
|
||
|
|
||
|
bool CModDoc::SoloChannel(CHANNELINDEX nChn, bool bSolo)
|
||
|
{
|
||
|
if (nChn >= m_SndFile.m_nChannels) return false;
|
||
|
if (MuteToggleModifiesDocument()) SetModified();
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SOLO, bSolo);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsChannelNoFx(CHANNELINDEX nChn) const
|
||
|
{
|
||
|
if (nChn >= m_SndFile.m_nChannels) return true;
|
||
|
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_NOFX];
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix)
|
||
|
{
|
||
|
if (nChn >= m_SndFile.m_nChannels) return false;
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_NOFX, bNoFx);
|
||
|
if(updateMix) m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_NOFX, bNoFx);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
RecordGroup CModDoc::GetChannelRecordGroup(CHANNELINDEX channel) const
|
||
|
{
|
||
|
if(channel >= GetNumChannels())
|
||
|
return RecordGroup::NoGroup;
|
||
|
if(m_bsMultiRecordMask[channel])
|
||
|
return RecordGroup::Group1;
|
||
|
if(m_bsMultiSplitRecordMask[channel])
|
||
|
return RecordGroup::Group2;
|
||
|
return RecordGroup::NoGroup;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
|
||
|
{
|
||
|
if(channel >= GetNumChannels())
|
||
|
return;
|
||
|
m_bsMultiRecordMask.set(channel, recordGroup == RecordGroup::Group1);
|
||
|
m_bsMultiSplitRecordMask.set(channel, recordGroup == RecordGroup::Group2);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
|
||
|
{
|
||
|
if(channel >= GetNumChannels())
|
||
|
return;
|
||
|
if(recordGroup == RecordGroup::Group1)
|
||
|
{
|
||
|
m_bsMultiRecordMask.flip(channel);
|
||
|
m_bsMultiSplitRecordMask.reset(channel);
|
||
|
} else if(recordGroup == RecordGroup::Group2)
|
||
|
{
|
||
|
m_bsMultiRecordMask.reset(channel);
|
||
|
m_bsMultiSplitRecordMask.flip(channel);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ReinitRecordState(bool unselect)
|
||
|
{
|
||
|
if(unselect)
|
||
|
{
|
||
|
m_bsMultiRecordMask.reset();
|
||
|
m_bsMultiSplitRecordMask.reset();
|
||
|
} else
|
||
|
{
|
||
|
m_bsMultiRecordMask.set();
|
||
|
m_bsMultiSplitRecordMask.set();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::MuteSample(SAMPLEINDEX nSample, bool bMute)
|
||
|
{
|
||
|
if ((nSample < 1) || (nSample > m_SndFile.GetNumSamples())) return false;
|
||
|
m_SndFile.GetSample(nSample).uFlags.set(CHN_MUTE, bMute);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute)
|
||
|
{
|
||
|
if ((nInstr < 1) || (nInstr > m_SndFile.GetNumInstruments()) || (!m_SndFile.Instruments[nInstr])) return false;
|
||
|
m_SndFile.Instruments[nInstr]->dwFlags.set(INS_MUTE, bMute);
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SurroundChannel(CHANNELINDEX nChn, bool surround)
|
||
|
{
|
||
|
if(nChn >= m_SndFile.GetNumChannels()) return false;
|
||
|
|
||
|
if(!(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) surround = false;
|
||
|
|
||
|
if(surround != m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
|
||
|
{
|
||
|
// Update channel configuration
|
||
|
if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
|
||
|
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SURROUND, surround);
|
||
|
if(surround)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[nChn].nPan = 128;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Update playing channel
|
||
|
m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_SURROUND, surround);
|
||
|
if(surround)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[nChn].nPan = 128;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume)
|
||
|
{
|
||
|
bool ok = false;
|
||
|
if(nChn >= m_SndFile.GetNumChannels() || nVolume > 64) return false;
|
||
|
if(m_SndFile.ChnSettings[nChn].nVolume != nVolume)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[nChn].nVolume = nVolume;
|
||
|
if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
|
||
|
ok = true;
|
||
|
}
|
||
|
m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = nVolume;
|
||
|
return ok;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan)
|
||
|
{
|
||
|
bool ok = false;
|
||
|
if(nChn >= m_SndFile.GetNumChannels() || nPan > 256) return false;
|
||
|
if(m_SndFile.ChnSettings[nChn].nPan != nPan || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
|
||
|
{
|
||
|
m_SndFile.ChnSettings[nChn].nPan = nPan;
|
||
|
m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND);
|
||
|
if(m_SndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
|
||
|
ok = true;
|
||
|
}
|
||
|
m_SndFile.m_PlayState.Chn[nChn].nPan = nPan;
|
||
|
m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SURROUND);
|
||
|
return ok;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsChannelMuted(CHANNELINDEX nChn) const
|
||
|
{
|
||
|
if(nChn >= m_SndFile.GetNumChannels()) return true;
|
||
|
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsSampleMuted(SAMPLEINDEX nSample) const
|
||
|
{
|
||
|
if(!nSample || nSample > m_SndFile.GetNumSamples()) return false;
|
||
|
return m_SndFile.GetSample(nSample).uFlags[CHN_MUTE];
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsInstrumentMuted(INSTRUMENTINDEX nInstr) const
|
||
|
{
|
||
|
if(!nInstr || nInstr > m_SndFile.GetNumInstruments() || !m_SndFile.Instruments[nInstr]) return false;
|
||
|
return m_SndFile.Instruments[nInstr]->dwFlags[INS_MUTE];
|
||
|
}
|
||
|
|
||
|
|
||
|
UINT CModDoc::GetPatternSize(PATTERNINDEX nPat) const
|
||
|
{
|
||
|
if(m_SndFile.Patterns.IsValidIndex(nPat)) return m_SndFile.Patterns[nPat].GetNumRows();
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SetFollowWnd(HWND hwnd)
|
||
|
{
|
||
|
m_hWndFollow = hwnd;
|
||
|
}
|
||
|
|
||
|
|
||
|
bool CModDoc::IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const
|
||
|
{
|
||
|
return m_SndFile.IsSampleReferencedByInstrument(nSmp, nIns);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Find an instrument that references the given sample.
|
||
|
// If no such instrument is found, INSTRUMENTINDEX_INVALID is returned.
|
||
|
INSTRUMENTINDEX CModDoc::FindSampleParent(SAMPLEINDEX sample) const
|
||
|
{
|
||
|
if(sample == 0)
|
||
|
{
|
||
|
return INSTRUMENTINDEX_INVALID;
|
||
|
}
|
||
|
for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++)
|
||
|
{
|
||
|
const ModInstrument *pIns = m_SndFile.Instruments[i];
|
||
|
if(pIns != nullptr)
|
||
|
{
|
||
|
for(size_t j = 0; j < NOTE_MAX; j++)
|
||
|
{
|
||
|
if(pIns->Keyboard[j] == sample)
|
||
|
{
|
||
|
return i;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return INSTRUMENTINDEX_INVALID;
|
||
|
}
|
||
|
|
||
|
|
||
|
SAMPLEINDEX CModDoc::FindInstrumentChild(INSTRUMENTINDEX nIns) const
|
||
|
{
|
||
|
if ((!nIns) || (nIns > m_SndFile.GetNumInstruments())) return 0;
|
||
|
const ModInstrument *pIns = m_SndFile.Instruments[nIns];
|
||
|
if (pIns)
|
||
|
{
|
||
|
for (auto n : pIns->Keyboard)
|
||
|
{
|
||
|
if ((n) && (n <= m_SndFile.GetNumSamples())) return n;
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
LRESULT CModDoc::ActivateView(UINT nIdView, DWORD dwParam)
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (!pMainFrm) return 0;
|
||
|
CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
|
||
|
if (pMDIActive)
|
||
|
{
|
||
|
CView *pView = pMDIActive->GetActiveView();
|
||
|
if ((pView) && (pView->GetDocument() == this))
|
||
|
{
|
||
|
return ((CChildFrame *)pMDIActive)->ActivateView(nIdView, dwParam);
|
||
|
}
|
||
|
}
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
while (pos != NULL)
|
||
|
{
|
||
|
CView *pView = GetNextView(pos);
|
||
|
if ((pView) && (pView->GetDocument() == this))
|
||
|
{
|
||
|
CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
|
||
|
pChildFrm->MDIActivate();
|
||
|
return pChildFrm->ActivateView(nIdView, dwParam);
|
||
|
}
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Activate document's window.
|
||
|
void CModDoc::ActivateWindow()
|
||
|
{
|
||
|
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
if(pChildFrm) pChildFrm->MDIActivate();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint)
|
||
|
{
|
||
|
// Tunnel our UpdateHint into an LPARAM
|
||
|
CDocument::UpdateAllViews(pSender, hint.AsLPARAM(), pHint);
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm) pMainFrm->UpdateTree(this, hint, pHint);
|
||
|
|
||
|
if(hint.GetType()[HINT_MODCHANNELS | HINT_MODTYPE])
|
||
|
{
|
||
|
auto instance = CChannelManagerDlg::sharedInstance();
|
||
|
if(instance != nullptr && pHint != instance && instance->GetDocument() == this)
|
||
|
instance->Update(hint, pHint);
|
||
|
}
|
||
|
#ifndef NO_PLUGINS
|
||
|
if(hint.GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES])
|
||
|
{
|
||
|
for(auto &plug : m_SndFile.m_MixPlugins)
|
||
|
{
|
||
|
auto mixPlug = plug.pMixPlugin;
|
||
|
if(mixPlug != nullptr && mixPlug->GetEditor())
|
||
|
{
|
||
|
mixPlug->GetEditor()->UpdateView(hint);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
#endif
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::UpdateAllViews(UpdateHint hint)
|
||
|
{
|
||
|
CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_UPDATEVIEWS, reinterpret_cast<WPARAM>(this), hint.AsLPARAM());
|
||
|
}
|
||
|
|
||
|
|
||
|
/////////////////////////////////////////////////////////////////////////////
|
||
|
// CModDoc commands
|
||
|
|
||
|
void CModDoc::OnFileWaveConvert()
|
||
|
{
|
||
|
OnFileWaveConvert(ORDERINDEX_INVALID, ORDERINDEX_INVALID);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories)
|
||
|
{
|
||
|
ASSERT(!encFactories.empty());
|
||
|
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
|
||
|
if ((!pMainFrm) || (!m_SndFile.GetType()) || encFactories.empty()) return;
|
||
|
|
||
|
CWaveConvert wsdlg(pMainFrm, nMinOrder, nMaxOrder, m_SndFile.Order().GetLengthTailTrimmed() - 1, m_SndFile, encFactories);
|
||
|
{
|
||
|
BypassInputHandler bih;
|
||
|
if (wsdlg.DoModal() != IDOK) return;
|
||
|
}
|
||
|
|
||
|
EncoderFactoryBase *encFactory = wsdlg.m_Settings.GetEncoderFactory();
|
||
|
|
||
|
const mpt::PathString extension = encFactory->GetTraits().fileExtension;
|
||
|
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension(extension)
|
||
|
.DefaultFilename(GetPathNameMpt().GetFileName() + P_(".") + extension)
|
||
|
.ExtensionFilter(encFactory->GetTraits().fileDescription + U_(" (*.") + extension.ToUnicode() + U_(")|*.") + extension.ToUnicode() + U_("||"))
|
||
|
.WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir());
|
||
|
if(!wsdlg.m_Settings.outputToSample && !dlg.Show()) return;
|
||
|
|
||
|
// will set default dir here because there's no setup option for export dir yet (feel free to add one...)
|
||
|
TrackerSettings::Instance().PathExport.SetDefaultDir(dlg.GetWorkingDirectory(), true);
|
||
|
|
||
|
mpt::PathString drive, dir, name, ext;
|
||
|
dlg.GetFirstFile().SplitPath(&drive, &dir, &name, &ext);
|
||
|
const mpt::PathString fileName = drive + dir + name;
|
||
|
const mpt::PathString fileExt = ext;
|
||
|
|
||
|
const ORDERINDEX currentOrd = m_SndFile.m_PlayState.m_nCurrentOrder;
|
||
|
const ROWINDEX currentRow = m_SndFile.m_PlayState.m_nRow;
|
||
|
|
||
|
int nRenderPasses = 1;
|
||
|
// Channel mode
|
||
|
std::vector<bool> usedChannels;
|
||
|
std::vector<FlagSet<ChannelFlags>> channelFlags;
|
||
|
// Instrument mode
|
||
|
std::vector<bool> instrMuteState;
|
||
|
|
||
|
// CHN_SYNCMUTE is used with formats where CHN_MUTE would stop processing global effects and could thus mess synchronization between exported channels
|
||
|
const ChannelFlags muteFlag = m_SndFile.m_playBehaviour[kST3NoMutedChannels] ? CHN_SYNCMUTE : CHN_MUTE;
|
||
|
|
||
|
// Channel mode: save song in multiple wav files (one for each enabled channels)
|
||
|
if(wsdlg.m_bChannelMode)
|
||
|
{
|
||
|
// Don't save empty channels
|
||
|
CheckUsedChannels(usedChannels);
|
||
|
|
||
|
nRenderPasses = m_SndFile.GetNumChannels();
|
||
|
channelFlags.resize(nRenderPasses, ChannelFlags(0));
|
||
|
for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
|
||
|
{
|
||
|
// Save channels' flags
|
||
|
channelFlags[i] = m_SndFile.ChnSettings[i].dwFlags;
|
||
|
// Ignore muted channels
|
||
|
if(channelFlags[i][CHN_MUTE]) usedChannels[i] = false;
|
||
|
// Mute each channel
|
||
|
m_SndFile.ChnSettings[i].dwFlags.set(muteFlag);
|
||
|
}
|
||
|
}
|
||
|
// Instrument mode: Same as channel mode, but renders per instrument (or sample)
|
||
|
if(wsdlg.m_bInstrumentMode)
|
||
|
{
|
||
|
if(m_SndFile.GetNumInstruments() == 0)
|
||
|
{
|
||
|
nRenderPasses = m_SndFile.GetNumSamples();
|
||
|
instrMuteState.resize(nRenderPasses, false);
|
||
|
for(SAMPLEINDEX i = 0; i < m_SndFile.GetNumSamples(); i++)
|
||
|
{
|
||
|
instrMuteState[i] = IsSampleMuted(i + 1);
|
||
|
MuteSample(i + 1, true);
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
nRenderPasses = m_SndFile.GetNumInstruments();
|
||
|
instrMuteState.resize(nRenderPasses, false);
|
||
|
for(INSTRUMENTINDEX i = 0; i < m_SndFile.GetNumInstruments(); i++)
|
||
|
{
|
||
|
instrMuteState[i] = IsInstrumentMuted(i + 1);
|
||
|
MuteInstrument(i + 1, true);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pMainFrm->PauseMod(this);
|
||
|
int oldRepeat = m_SndFile.GetRepeatCount();
|
||
|
|
||
|
const SEQUENCEINDEX currentSeq = m_SndFile.Order.GetCurrentSequenceIndex();
|
||
|
for(SEQUENCEINDEX seq = wsdlg.m_Settings.minSequence; seq <= wsdlg.m_Settings.maxSequence; seq++)
|
||
|
{
|
||
|
m_SndFile.Order.SetSequence(seq);
|
||
|
mpt::ustring fileNameAdd;
|
||
|
for(int i = 0; i < nRenderPasses; i++)
|
||
|
{
|
||
|
mpt::PathString thisName = fileName;
|
||
|
CString caption = _T("file");
|
||
|
fileNameAdd.clear();
|
||
|
if(wsdlg.m_Settings.minSequence != wsdlg.m_Settings.maxSequence)
|
||
|
{
|
||
|
fileNameAdd = MPT_UFORMAT("-{}")(mpt::ufmt::dec0<2>(seq + 1));
|
||
|
mpt::ustring seqName = m_SndFile.Order(seq).GetName();
|
||
|
if(!seqName.empty())
|
||
|
{
|
||
|
fileNameAdd += UL_("-") + seqName;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Channel mode
|
||
|
if(wsdlg.m_bChannelMode)
|
||
|
{
|
||
|
// Re-mute previously processed channel
|
||
|
if(i > 0)
|
||
|
m_SndFile.ChnSettings[i - 1].dwFlags.set(muteFlag);
|
||
|
|
||
|
// Was this channel actually muted? Don't process it then.
|
||
|
if(!usedChannels[i])
|
||
|
continue;
|
||
|
|
||
|
// Add channel number & name (if available) to path string
|
||
|
if(!m_SndFile.ChnSettings[i].szName.empty())
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
|
||
|
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
|
||
|
} else
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
|
||
|
caption = MPT_CFORMAT("channel {}")(i + 1);
|
||
|
}
|
||
|
// Unmute channel to process
|
||
|
m_SndFile.ChnSettings[i].dwFlags.reset(muteFlag);
|
||
|
}
|
||
|
// Instrument mode
|
||
|
if(wsdlg.m_bInstrumentMode)
|
||
|
{
|
||
|
if(m_SndFile.GetNumInstruments() == 0)
|
||
|
{
|
||
|
// Re-mute previously processed sample
|
||
|
if(i > 0) MuteSample(static_cast<SAMPLEINDEX>(i), true);
|
||
|
|
||
|
if(!m_SndFile.GetSample(static_cast<SAMPLEINDEX>(i + 1)).HasSampleData() || !IsSampleUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
|
||
|
continue;
|
||
|
|
||
|
// Add sample number & name (if available) to path string
|
||
|
if(!m_SndFile.m_szNames[i + 1].empty())
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
|
||
|
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
|
||
|
} else
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
|
||
|
caption = MPT_CFORMAT("sample {}")(i + 1);
|
||
|
}
|
||
|
// Unmute sample to process
|
||
|
MuteSample(static_cast<SAMPLEINDEX>(i + 1), false);
|
||
|
} else
|
||
|
{
|
||
|
// Re-mute previously processed instrument
|
||
|
if(i > 0) MuteInstrument(static_cast<INSTRUMENTINDEX>(i), true);
|
||
|
|
||
|
if(m_SndFile.Instruments[i + 1] == nullptr || !IsInstrumentUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
|
||
|
continue;
|
||
|
|
||
|
if(!m_SndFile.Instruments[i + 1]->name.empty())
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
|
||
|
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
|
||
|
} else
|
||
|
{
|
||
|
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
|
||
|
caption = MPT_CFORMAT("instrument {}")(i + 1);
|
||
|
}
|
||
|
// Unmute instrument to process
|
||
|
MuteInstrument(static_cast<SAMPLEINDEX>(i + 1), false);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(!fileNameAdd.empty())
|
||
|
{
|
||
|
SanitizeFilename(fileNameAdd);
|
||
|
thisName += mpt::PathString::FromUnicode(fileNameAdd);
|
||
|
}
|
||
|
thisName += fileExt;
|
||
|
if(wsdlg.m_Settings.outputToSample)
|
||
|
{
|
||
|
thisName = mpt::CreateTempFileName(P_("OpenMPT"));
|
||
|
// Ensure this temporary file is marked as temporary in the file system, to increase the chance it will never be written to disk
|
||
|
HANDLE hFile = ::CreateFile(thisName.AsNative().c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
|
||
|
if(hFile != INVALID_HANDLE_VALUE)
|
||
|
{
|
||
|
::CloseHandle(hFile);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Render song (or current channel, or current sample/instrument)
|
||
|
bool cancel = true;
|
||
|
try
|
||
|
{
|
||
|
mpt::SafeOutputFile safeFileStream(thisName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
mpt::ofstream &f = safeFileStream;
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
|
||
|
if(!f)
|
||
|
{
|
||
|
Reporting::Error("Could not open file for writing. Is it open in another application?");
|
||
|
} else
|
||
|
{
|
||
|
BypassInputHandler bih;
|
||
|
CDoWaveConvert dwcdlg(m_SndFile, f, caption, wsdlg.m_Settings, pMainFrm);
|
||
|
dwcdlg.m_bGivePlugsIdleTime = wsdlg.m_bGivePlugsIdleTime;
|
||
|
dwcdlg.m_dwSongLimit = wsdlg.m_dwSongLimit;
|
||
|
cancel = dwcdlg.DoModal() != IDOK;
|
||
|
}
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
Reporting::Error(_T("Error while writing file!"));
|
||
|
}
|
||
|
|
||
|
if(wsdlg.m_Settings.outputToSample)
|
||
|
{
|
||
|
if(!cancel)
|
||
|
{
|
||
|
InputFile f(thisName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
|
||
|
if(f.IsValid())
|
||
|
{
|
||
|
FileReader file = GetFileReader(f);
|
||
|
SAMPLEINDEX smp = wsdlg.m_Settings.sampleSlot;
|
||
|
if(smp == 0 || smp > GetNumSamples()) smp = m_SndFile.GetNextFreeSample();
|
||
|
if(smp == SAMPLEINDEX_INVALID)
|
||
|
{
|
||
|
Reporting::Error(_T("Too many samples!"));
|
||
|
cancel = true;
|
||
|
}
|
||
|
if(!cancel)
|
||
|
{
|
||
|
if(GetNumSamples() < smp) m_SndFile.m_nSamples = smp;
|
||
|
GetSampleUndo().PrepareUndo(smp, sundo_replace, "Render To Sample");
|
||
|
if(m_SndFile.ReadSampleFromFile(smp, file, false))
|
||
|
{
|
||
|
m_SndFile.m_szNames[smp] = "Render To Sample" + mpt::ToCharset(m_SndFile.GetCharsetInternal(), fileNameAdd);
|
||
|
UpdateAllViews(nullptr, SampleHint().Info().Data().Names());
|
||
|
if(m_SndFile.GetNumInstruments() && !IsSampleUsed(smp))
|
||
|
{
|
||
|
// Insert new instrument for the generated sample in case it is not referenced by any instruments yet.
|
||
|
// It should only be already referenced if the user chose to export to an existing sample slot.
|
||
|
InsertInstrument(smp);
|
||
|
UpdateAllViews(nullptr, InstrumentHint().Info().Names());
|
||
|
}
|
||
|
SetModified();
|
||
|
} else
|
||
|
{
|
||
|
GetSampleUndo().RemoveLastUndoStep(smp);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Always clean up after ourselves
|
||
|
for(int retry = 0; retry < 10; retry++)
|
||
|
{
|
||
|
// stupid virus scanners
|
||
|
if(DeleteFile(thisName.AsNative().c_str()) != EACCES)
|
||
|
{
|
||
|
break;
|
||
|
}
|
||
|
Sleep(10);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if(cancel) break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Restore channels' flags
|
||
|
if(wsdlg.m_bChannelMode)
|
||
|
{
|
||
|
for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
|
||
|
{
|
||
|
m_SndFile.ChnSettings[i].dwFlags = channelFlags[i];
|
||
|
}
|
||
|
}
|
||
|
// Restore instruments' / samples' flags
|
||
|
if(wsdlg.m_bInstrumentMode)
|
||
|
{
|
||
|
for(size_t i = 0; i < instrMuteState.size(); i++)
|
||
|
{
|
||
|
if(m_SndFile.GetNumInstruments() == 0)
|
||
|
MuteSample(static_cast<SAMPLEINDEX>(i + 1), instrMuteState[i]);
|
||
|
else
|
||
|
MuteInstrument(static_cast<INSTRUMENTINDEX>(i + 1), instrMuteState[i]);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
m_SndFile.Order.SetSequence(currentSeq);
|
||
|
m_SndFile.SetRepeatCount(oldRepeat);
|
||
|
m_SndFile.GetLength(eAdjust, GetLengthTarget(currentOrd, currentRow));
|
||
|
m_SndFile.m_PlayState.m_nNextOrder = currentOrd;
|
||
|
m_SndFile.m_PlayState.m_nNextRow = currentRow;
|
||
|
CMainFrame::UpdateAudioParameters(m_SndFile, true);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder)
|
||
|
{
|
||
|
WAVEncoder wavencoder;
|
||
|
FLACEncoder flacencoder;
|
||
|
AUEncoder auencoder;
|
||
|
OggOpusEncoder opusencoder;
|
||
|
VorbisEncoder vorbisencoder;
|
||
|
MP3Encoder mp3lame(MP3EncoderLame);
|
||
|
MP3Encoder mp3lamecompatible(MP3EncoderLameCompatible);
|
||
|
RAWEncoder rawencoder;
|
||
|
std::vector<EncoderFactoryBase*> encoders;
|
||
|
if(wavencoder.IsAvailable()) encoders.push_back(&wavencoder);
|
||
|
if(flacencoder.IsAvailable()) encoders.push_back(&flacencoder);
|
||
|
if(auencoder.IsAvailable()) encoders.push_back(&auencoder);
|
||
|
if(rawencoder.IsAvailable()) encoders.push_back(&rawencoder);
|
||
|
if(opusencoder.IsAvailable()) encoders.push_back(&opusencoder);
|
||
|
if(vorbisencoder.IsAvailable()) encoders.push_back(&vorbisencoder);
|
||
|
if(mp3lame.IsAvailable())
|
||
|
{
|
||
|
encoders.push_back(&mp3lame);
|
||
|
}
|
||
|
if(mp3lamecompatible.IsAvailable()) encoders.push_back(&mp3lamecompatible);
|
||
|
OnFileWaveConvert(nMinOrder, nMaxOrder, encoders);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnFileMidiConvert()
|
||
|
{
|
||
|
#ifndef NO_PLUGINS
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
|
||
|
if ((!pMainFrm) || (!m_SndFile.GetType())) return;
|
||
|
|
||
|
mpt::PathString filename = GetPathNameMpt().ReplaceExt(P_(".mid"));
|
||
|
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension("mid")
|
||
|
.DefaultFilename(filename)
|
||
|
.ExtensionFilter("MIDI Files (*.mid)|*.mid||");
|
||
|
if(!dlg.Show()) return;
|
||
|
|
||
|
CModToMidi mididlg(m_SndFile, pMainFrm);
|
||
|
BypassInputHandler bih;
|
||
|
if(mididlg.DoModal() == IDOK)
|
||
|
{
|
||
|
try
|
||
|
{
|
||
|
mpt::SafeOutputFile sf(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
mpt::ofstream &f = sf;
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
|
||
|
if(!f.good())
|
||
|
{
|
||
|
Reporting::Error("Could not open file for writing. Is it open in another application?");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CDoMidiConvert doconv(m_SndFile, f, mididlg.m_instrMap);
|
||
|
doconv.DoModal();
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
Reporting::Error(_T("Error while writing file!"));
|
||
|
}
|
||
|
}
|
||
|
#else
|
||
|
Reporting::Error("In order to use MIDI export, OpenMPT must be built with plugin support.");
|
||
|
#endif // NO_PLUGINS
|
||
|
}
|
||
|
|
||
|
//HACK: This is a quick fix. Needs to be better integrated into player and GUI.
|
||
|
void CModDoc::OnFileCompatibilitySave()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (!pMainFrm) return;
|
||
|
|
||
|
CString pattern;
|
||
|
|
||
|
const MODTYPE type = m_SndFile.GetType();
|
||
|
switch(type)
|
||
|
{
|
||
|
case MOD_TYPE_IT:
|
||
|
pattern = FileFilterIT;
|
||
|
MsgBoxHidable(CompatExportDefaultWarning);
|
||
|
break;
|
||
|
case MOD_TYPE_XM:
|
||
|
pattern = FileFilterXM;
|
||
|
MsgBoxHidable(CompatExportDefaultWarning);
|
||
|
break;
|
||
|
default:
|
||
|
// Not available for this format.
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
const std::string ext = m_SndFile.GetModSpecifications().fileExtension;
|
||
|
|
||
|
mpt::PathString filename;
|
||
|
|
||
|
{
|
||
|
mpt::PathString drive;
|
||
|
mpt::PathString dir;
|
||
|
mpt::PathString fileName;
|
||
|
GetPathNameMpt().SplitPath(&drive, &dir, &fileName, nullptr);
|
||
|
|
||
|
filename = drive;
|
||
|
filename += dir;
|
||
|
filename += fileName;
|
||
|
if(!strstr(fileName.ToUTF8().c_str(), "compat"))
|
||
|
filename += P_(".compat.");
|
||
|
else
|
||
|
filename += P_(".");
|
||
|
filename += mpt::PathString::FromUTF8(ext);
|
||
|
}
|
||
|
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension(ext)
|
||
|
.DefaultFilename(filename)
|
||
|
.ExtensionFilter(pattern)
|
||
|
.WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir());
|
||
|
if(!dlg.Show()) return;
|
||
|
|
||
|
filename = dlg.GetFirstFile();
|
||
|
|
||
|
bool ok = false;
|
||
|
BeginWaitCursor();
|
||
|
try
|
||
|
{
|
||
|
mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
|
||
|
mpt::ofstream &f = sf;
|
||
|
if(f)
|
||
|
{
|
||
|
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
|
||
|
ScopedLogCapturer logcapturer(*this);
|
||
|
FixNullStrings();
|
||
|
switch(type)
|
||
|
{
|
||
|
case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f, true); break;
|
||
|
case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename, true); break;
|
||
|
default: MPT_ASSERT_NOTREACHED();
|
||
|
}
|
||
|
}
|
||
|
} catch(const std::exception &)
|
||
|
{
|
||
|
ok = false;
|
||
|
}
|
||
|
EndWaitCursor();
|
||
|
|
||
|
if(!ok)
|
||
|
{
|
||
|
ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnPlayerPlay()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm)
|
||
|
{
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
|
||
|
{
|
||
|
//User has sent play song command: set loop pattern checkbox to false.
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
|
||
|
}
|
||
|
|
||
|
bool isPlaying = (pMainFrm->GetModPlaying() == this);
|
||
|
if(isPlaying && !m_SndFile.m_SongFlags[SONG_PAUSED | SONG_STEP/*|SONG_PATTERNLOOP*/])
|
||
|
{
|
||
|
OnPlayerPause();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CriticalSection cs;
|
||
|
|
||
|
// Kill editor voices
|
||
|
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) if (m_SndFile.m_PlayState.Chn[i].isPreviewNote)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
|
||
|
if (!isPlaying) m_SndFile.m_PlayState.Chn[i].nLength = 0;
|
||
|
}
|
||
|
|
||
|
m_SndFile.m_PlayState.m_bPositionChanged = true;
|
||
|
|
||
|
if(isPlaying)
|
||
|
{
|
||
|
m_SndFile.StopAllVsti();
|
||
|
}
|
||
|
|
||
|
cs.Leave();
|
||
|
|
||
|
m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PAUSED | SONG_PATTERNLOOP);
|
||
|
pMainFrm->PlayMod(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnPlayerPause()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm)
|
||
|
{
|
||
|
if (pMainFrm->GetModPlaying() == this)
|
||
|
{
|
||
|
bool isLooping = m_SndFile.m_SongFlags[SONG_PATTERNLOOP];
|
||
|
PATTERNINDEX nPat = m_SndFile.m_PlayState.m_nPattern;
|
||
|
ROWINDEX nRow = m_SndFile.m_PlayState.m_nRow;
|
||
|
ROWINDEX nNextRow = m_SndFile.m_PlayState.m_nNextRow;
|
||
|
pMainFrm->PauseMod();
|
||
|
|
||
|
if ((isLooping) && (nPat < m_SndFile.Patterns.Size()))
|
||
|
{
|
||
|
CriticalSection cs;
|
||
|
|
||
|
if ((m_SndFile.m_PlayState.m_nCurrentOrder < m_SndFile.Order().GetLength()) && (m_SndFile.Order()[m_SndFile.m_PlayState.m_nCurrentOrder] == nPat))
|
||
|
{
|
||
|
m_SndFile.m_PlayState.m_nNextOrder = m_SndFile.m_PlayState.m_nCurrentOrder;
|
||
|
m_SndFile.m_PlayState.m_nNextRow = nNextRow;
|
||
|
m_SndFile.m_PlayState.m_nRow = nRow;
|
||
|
} else
|
||
|
{
|
||
|
for (ORDERINDEX nOrd = 0; nOrd < m_SndFile.Order().GetLength(); nOrd++)
|
||
|
{
|
||
|
if (m_SndFile.Order()[nOrd] == m_SndFile.Order.GetInvalidPatIndex()) break;
|
||
|
if (m_SndFile.Order()[nOrd] == nPat)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.m_nCurrentOrder = nOrd;
|
||
|
m_SndFile.m_PlayState.m_nNextOrder = nOrd;
|
||
|
m_SndFile.m_PlayState.m_nNextRow = nNextRow;
|
||
|
m_SndFile.m_PlayState.m_nRow = nRow;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
pMainFrm->PauseMod();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnPlayerStop()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm) pMainFrm->StopMod();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnPlayerPlayFromStart()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (pMainFrm)
|
||
|
{
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
|
||
|
{
|
||
|
//User has sent play song command: set loop pattern checkbox to false.
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
|
||
|
}
|
||
|
|
||
|
pMainFrm->PauseMod();
|
||
|
CriticalSection cs;
|
||
|
m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PATTERNLOOP);
|
||
|
m_SndFile.ResetPlayPos();
|
||
|
//m_SndFile.visitedSongRows.Initialize(true);
|
||
|
|
||
|
m_SndFile.m_PlayState.m_bPositionChanged = true;
|
||
|
|
||
|
cs.Leave();
|
||
|
|
||
|
pMainFrm->PlayMod(this);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEditGlobals()
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_GLOBALS);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEditPatterns()
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, -1);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEditSamples()
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, -1);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEditInstruments()
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, -1);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEditComments()
|
||
|
{
|
||
|
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_COMMENTS);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnShowCleanup()
|
||
|
{
|
||
|
CModCleanupDlg dlg(*this, CMainFrame::GetMainFrame());
|
||
|
dlg.DoModal();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnSetupZxxMacros()
|
||
|
{
|
||
|
CMidiMacroSetup dlg(m_SndFile);
|
||
|
if(dlg.DoModal() == IDOK)
|
||
|
{
|
||
|
if(m_SndFile.m_MidiCfg != dlg.m_MidiCfg)
|
||
|
{
|
||
|
m_SndFile.m_MidiCfg = dlg.m_MidiCfg;
|
||
|
SetModified();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Enable menu item only module types that support MIDI Mappings
|
||
|
void CModDoc::OnUpdateHasMIDIMappings(CCmdUI *p)
|
||
|
{
|
||
|
if(p)
|
||
|
p->Enable((m_SndFile.GetModSpecifications().MIDIMappingDirectivesMax > 0) ? TRUE : FALSE);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Enable menu item only for IT / MPTM / XM files
|
||
|
void CModDoc::OnUpdateXMITMPTOnly(CCmdUI *p)
|
||
|
{
|
||
|
if (p)
|
||
|
p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Enable menu item only for IT / MPTM files
|
||
|
void CModDoc::OnUpdateHasEditHistory(CCmdUI *p)
|
||
|
{
|
||
|
if (p)
|
||
|
p->Enable(((m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_SndFile.GetFileHistory().empty()) ? TRUE : FALSE);
|
||
|
}
|
||
|
|
||
|
|
||
|
// Enable menu item if current module type supports compatibility export
|
||
|
void CModDoc::OnUpdateCompatExportableOnly(CCmdUI *p)
|
||
|
{
|
||
|
if(p)
|
||
|
p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT)) ? TRUE : FALSE);
|
||
|
}
|
||
|
|
||
|
|
||
|
static CString FormatSongLength(double length)
|
||
|
{
|
||
|
length = mpt::round(length);
|
||
|
double minutes = std::floor(length / 60.0), seconds = std::fmod(length, 60.0);
|
||
|
CString s;
|
||
|
s.Format(_T("%.0fmn%02.0fs"), minutes, seconds);
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnEstimateSongLength()
|
||
|
{
|
||
|
CString s = _T("Approximate song length: ");
|
||
|
const auto subSongs = m_SndFile.GetAllSubSongs();
|
||
|
if (subSongs.empty())
|
||
|
{
|
||
|
Reporting::Information(_T("No patterns found!"));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
std::vector<uint32> songsPerSequence(m_SndFile.Order.GetNumSequences(), 0);
|
||
|
SEQUENCEINDEX prevSeq = subSongs[0].sequence;
|
||
|
for(const auto &song : subSongs)
|
||
|
{
|
||
|
songsPerSequence[song.sequence]++;
|
||
|
if(prevSeq != song.sequence)
|
||
|
prevSeq = SEQUENCEINDEX_INVALID;
|
||
|
}
|
||
|
|
||
|
double totalLength = 0.0;
|
||
|
uint32 songCount = 0;
|
||
|
// If there are multiple sequences, indent their subsongs
|
||
|
const TCHAR *indent = (prevSeq == SEQUENCEINDEX_INVALID) ? _T("\t") : _T("");
|
||
|
for(const auto &song : subSongs)
|
||
|
{
|
||
|
double songLength = song.duration;
|
||
|
if(subSongs.size() > 1)
|
||
|
{
|
||
|
totalLength += songLength;
|
||
|
if(prevSeq != song.sequence)
|
||
|
{
|
||
|
songCount = 0;
|
||
|
prevSeq = song.sequence;
|
||
|
if(m_SndFile.Order(prevSeq).GetName().empty())
|
||
|
s.AppendFormat(_T("\nSequence %u:"), prevSeq + 1u);
|
||
|
else
|
||
|
s.AppendFormat(_T("\nSequence %u (%s):"), prevSeq + 1u, mpt::ToWin(m_SndFile.Order(prevSeq).GetName()).c_str());
|
||
|
}
|
||
|
songCount++;
|
||
|
if(songsPerSequence[song.sequence] > 1)
|
||
|
s.AppendFormat(_T("\n%sSong %u, starting at order %u:\t"), indent, songCount, song.startOrder);
|
||
|
else
|
||
|
s.AppendChar(_T('\t'));
|
||
|
}
|
||
|
if(songLength != std::numeric_limits<double>::infinity())
|
||
|
{
|
||
|
songLength = mpt::round(songLength);
|
||
|
s += FormatSongLength(songLength);
|
||
|
} else
|
||
|
{
|
||
|
s += _T("Song too long!");
|
||
|
}
|
||
|
}
|
||
|
if(subSongs.size() > 1 && totalLength != std::numeric_limits<double>::infinity())
|
||
|
{
|
||
|
s += _T("\n\nTotal length:\t") + FormatSongLength(totalLength);
|
||
|
}
|
||
|
|
||
|
Reporting::Information(s);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnApproximateBPM()
|
||
|
{
|
||
|
if(CMainFrame::GetMainFrame()->GetModPlaying() != this)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.m_nCurrentRowsPerBeat = m_SndFile.m_nDefaultRowsPerBeat;
|
||
|
m_SndFile.m_PlayState.m_nCurrentRowsPerMeasure = m_SndFile.m_nDefaultRowsPerMeasure;
|
||
|
}
|
||
|
m_SndFile.RecalculateSamplesPerTick();
|
||
|
const double bpm = m_SndFile.GetCurrentBPM();
|
||
|
|
||
|
CString s;
|
||
|
switch(m_SndFile.m_nTempoMode)
|
||
|
{
|
||
|
case TempoMode::Alternative:
|
||
|
s.Format(_T("Using alternative tempo interpretation.\n\nAssuming:\n. %.8g ticks per second\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
|
||
|
m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
|
||
|
break;
|
||
|
|
||
|
case TempoMode::Modern:
|
||
|
s.Format(_T("Using modern tempo interpretation.\n\nThe tempo is: %.8g BPM"), bpm);
|
||
|
break;
|
||
|
|
||
|
case TempoMode::Classic:
|
||
|
default:
|
||
|
s.Format(_T("Using standard tempo interpretation.\n\nAssuming:\n. A mod tempo (tick duration factor) of %.8g\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
|
||
|
m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
Reporting::Information(s);
|
||
|
}
|
||
|
|
||
|
|
||
|
CChildFrame *CModDoc::GetChildFrame()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if (!pMainFrm) return nullptr;
|
||
|
CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
|
||
|
if (pMDIActive)
|
||
|
{
|
||
|
CView *pView = pMDIActive->GetActiveView();
|
||
|
if ((pView) && (pView->GetDocument() == this))
|
||
|
return static_cast<CChildFrame *>(pMDIActive);
|
||
|
}
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
while (pos != NULL)
|
||
|
{
|
||
|
CView *pView = GetNextView(pos);
|
||
|
if ((pView) && (pView->GetDocument() == this))
|
||
|
return static_cast<CChildFrame *>(pView->GetParentFrame());
|
||
|
}
|
||
|
|
||
|
return nullptr;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Get the currently edited pattern position. Note that ord might be ORDERINDEX_INVALID when editing a pattern that is not present in the order list.
|
||
|
void CModDoc::GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord)
|
||
|
{
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
|
||
|
if(strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) // dirty HACK
|
||
|
{
|
||
|
PATTERNVIEWSTATE patternViewState;
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)(&patternViewState));
|
||
|
|
||
|
pat = patternViewState.nPattern;
|
||
|
row = patternViewState.cursor.GetRow();
|
||
|
ord = patternViewState.nOrder;
|
||
|
} else
|
||
|
{
|
||
|
//patern editor object does not exist (i.e. is not active) - use saved state.
|
||
|
PATTERNVIEWSTATE &patternViewState = pChildFrm->GetPatternViewState();
|
||
|
|
||
|
pat = patternViewState.nPattern;
|
||
|
row = patternViewState.cursor.GetRow();
|
||
|
ord = patternViewState.nOrder;
|
||
|
}
|
||
|
|
||
|
const auto &order = m_SndFile.Order();
|
||
|
if(order.empty())
|
||
|
{
|
||
|
ord = ORDERINDEX_INVALID;
|
||
|
pat = 0;
|
||
|
row = 0;
|
||
|
} else if(ord >= order.size())
|
||
|
{
|
||
|
ord = 0;
|
||
|
pat = m_SndFile.Order()[ord];
|
||
|
}
|
||
|
if(!m_SndFile.Patterns.IsValidPat(pat))
|
||
|
{
|
||
|
pat = 0;
|
||
|
row = 0;
|
||
|
} else if(row >= m_SndFile.Patterns[pat].GetNumRows())
|
||
|
{
|
||
|
row = 0;
|
||
|
}
|
||
|
|
||
|
//ensure order correlates with pattern.
|
||
|
if(ord >= order.size() || order[ord] != pat)
|
||
|
{
|
||
|
ord = order.FindOrder(pat);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
////////////////////////////////////////////////////////////////////////////////////////
|
||
|
// Playback
|
||
|
|
||
|
|
||
|
void CModDoc::OnPatternRestart(bool loop)
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
|
||
|
if ((pMainFrm) && (pChildFrm))
|
||
|
{
|
||
|
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
|
||
|
{
|
||
|
//User has sent play pattern command: set loop pattern checkbox to true.
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, loop ? 1 : 0);
|
||
|
}
|
||
|
|
||
|
ROWINDEX nRow;
|
||
|
PATTERNINDEX nPat;
|
||
|
ORDERINDEX nOrd;
|
||
|
GetEditPosition(nRow, nPat, nOrd);
|
||
|
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
|
||
|
|
||
|
CriticalSection cs;
|
||
|
|
||
|
// Cut instruments/samples
|
||
|
for(auto &chn : m_SndFile.m_PlayState.Chn)
|
||
|
{
|
||
|
chn.nPatternLoopCount = 0;
|
||
|
chn.nPatternLoop = 0;
|
||
|
chn.nFadeOutVol = 0;
|
||
|
chn.dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
|
||
|
}
|
||
|
if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
|
||
|
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
|
||
|
if(loop)
|
||
|
m_SndFile.LoopPattern(nPat);
|
||
|
else
|
||
|
m_SndFile.LoopPattern(PATTERNINDEX_INVALID);
|
||
|
|
||
|
// set playback timer in the status bar (and update channel status)
|
||
|
SetElapsedTime(nOrd, 0, true);
|
||
|
|
||
|
if(pModPlaying == this)
|
||
|
{
|
||
|
m_SndFile.StopAllVsti();
|
||
|
}
|
||
|
|
||
|
cs.Leave();
|
||
|
|
||
|
if(pModPlaying != this)
|
||
|
{
|
||
|
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
|
||
|
SetFollowWnd(pChildFrm->GetHwndView());
|
||
|
pMainFrm->PlayMod(this); //rewbs.fix2977
|
||
|
}
|
||
|
}
|
||
|
//SwitchToView();
|
||
|
}
|
||
|
|
||
|
void CModDoc::OnPatternPlay()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
|
||
|
if ((pMainFrm) && (pChildFrm))
|
||
|
{
|
||
|
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
|
||
|
{
|
||
|
//User has sent play pattern command: set loop pattern checkbox to true.
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 1);
|
||
|
}
|
||
|
|
||
|
ROWINDEX nRow;
|
||
|
PATTERNINDEX nPat;
|
||
|
ORDERINDEX nOrd;
|
||
|
GetEditPosition(nRow, nPat, nOrd);
|
||
|
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
|
||
|
|
||
|
CriticalSection cs;
|
||
|
|
||
|
// Cut instruments/samples
|
||
|
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
|
||
|
}
|
||
|
if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
|
||
|
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
|
||
|
m_SndFile.LoopPattern(nPat);
|
||
|
|
||
|
// set playback timer in the status bar (and update channel status)
|
||
|
SetElapsedTime(nOrd, nRow, true);
|
||
|
|
||
|
if(pModPlaying == this)
|
||
|
{
|
||
|
m_SndFile.StopAllVsti();
|
||
|
}
|
||
|
|
||
|
cs.Leave();
|
||
|
|
||
|
if(pModPlaying != this)
|
||
|
{
|
||
|
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
|
||
|
SetFollowWnd(pChildFrm->GetHwndView());
|
||
|
pMainFrm->PlayMod(this); //rewbs.fix2977
|
||
|
}
|
||
|
}
|
||
|
//SwitchToView();
|
||
|
|
||
|
}
|
||
|
|
||
|
void CModDoc::OnPatternPlayNoLoop()
|
||
|
{
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
CChildFrame *pChildFrm = GetChildFrame();
|
||
|
|
||
|
if ((pMainFrm) && (pChildFrm))
|
||
|
{
|
||
|
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
|
||
|
{
|
||
|
//User has sent play song command: set loop pattern checkbox to false.
|
||
|
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
|
||
|
}
|
||
|
|
||
|
ROWINDEX nRow;
|
||
|
PATTERNINDEX nPat;
|
||
|
ORDERINDEX nOrd;
|
||
|
GetEditPosition(nRow, nPat, nOrd);
|
||
|
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
|
||
|
|
||
|
CriticalSection cs;
|
||
|
// Cut instruments/samples
|
||
|
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
|
||
|
{
|
||
|
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
|
||
|
}
|
||
|
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
|
||
|
m_SndFile.SetCurrentOrder(nOrd);
|
||
|
if(nOrd < m_SndFile.Order().size() && m_SndFile.Order()[nOrd] == nPat)
|
||
|
m_SndFile.DontLoopPattern(nPat, nRow);
|
||
|
else
|
||
|
m_SndFile.LoopPattern(nPat);
|
||
|
|
||
|
// set playback timer in the status bar (and update channel status)
|
||
|
SetElapsedTime(nOrd, nRow, true);
|
||
|
|
||
|
if(pModPlaying == this)
|
||
|
{
|
||
|
m_SndFile.StopAllVsti();
|
||
|
}
|
||
|
|
||
|
cs.Leave();
|
||
|
|
||
|
if(pModPlaying != this)
|
||
|
{
|
||
|
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
|
||
|
SetFollowWnd(pChildFrm->GetHwndView());
|
||
|
pMainFrm->PlayMod(this); //rewbs.fix2977
|
||
|
}
|
||
|
}
|
||
|
//SwitchToView();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnViewEditHistory()
|
||
|
{
|
||
|
CEditHistoryDlg dlg(CMainFrame::GetMainFrame(), *this);
|
||
|
dlg.DoModal();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnViewMPTHacks()
|
||
|
{
|
||
|
ScopedLogCapturer logcapturer(*this);
|
||
|
if(!HasMPTHacks())
|
||
|
{
|
||
|
AddToLog("No hacks found.");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnViewTempoSwingSettings()
|
||
|
{
|
||
|
if(m_SndFile.m_nDefaultRowsPerBeat > 0 && m_SndFile.m_nTempoMode == TempoMode::Modern)
|
||
|
{
|
||
|
TempoSwing tempoSwing = m_SndFile.m_tempoSwing;
|
||
|
tempoSwing.resize(m_SndFile.m_nDefaultRowsPerBeat, TempoSwing::Unity);
|
||
|
CTempoSwingDlg dlg(CMainFrame::GetMainFrame(), tempoSwing, m_SndFile);
|
||
|
if(dlg.DoModal() == IDOK)
|
||
|
{
|
||
|
SetModified();
|
||
|
m_SndFile.m_tempoSwing = dlg.m_tempoSwing;
|
||
|
}
|
||
|
} else if(GetModType() == MOD_TYPE_MPT)
|
||
|
{
|
||
|
Reporting::Error(_T("Modern tempo mode needs to be enabled in order to edit tempo swing settings."));
|
||
|
OnSongProperties();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
LRESULT CModDoc::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/)
|
||
|
{
|
||
|
const auto &modSpecs = m_SndFile.GetModSpecifications();
|
||
|
switch(wParam)
|
||
|
{
|
||
|
case kcViewGeneral: OnEditGlobals(); break;
|
||
|
case kcViewPattern: OnEditPatterns(); break;
|
||
|
case kcViewSamples: OnEditSamples(); break;
|
||
|
case kcViewInstruments: OnEditInstruments(); break;
|
||
|
case kcViewComments: OnEditComments(); break;
|
||
|
case kcViewSongProperties: OnSongProperties(); break;
|
||
|
case kcViewTempoSwing: OnViewTempoSwingSettings(); break;
|
||
|
case kcShowMacroConfig: OnSetupZxxMacros(); break;
|
||
|
case kcViewMIDImapping: OnViewMIDIMapping(); break;
|
||
|
case kcViewEditHistory: OnViewEditHistory(); break;
|
||
|
case kcViewChannelManager: OnChannelManager(); break;
|
||
|
|
||
|
case kcFileSaveAsWave: OnFileWaveConvert(); break;
|
||
|
case kcFileSaveMidi: OnFileMidiConvert(); break;
|
||
|
case kcFileSaveOPL: OnFileOPLExport(); break;
|
||
|
case kcFileExportCompat: OnFileCompatibilitySave(); break;
|
||
|
case kcEstimateSongLength: OnEstimateSongLength(); break;
|
||
|
case kcApproxRealBPM: OnApproximateBPM(); break;
|
||
|
case kcFileSave: DoSave(GetPathNameMpt()); break;
|
||
|
case kcFileSaveAs: DoSave(mpt::PathString()); break;
|
||
|
case kcFileSaveCopy: OnSaveCopy(); break;
|
||
|
case kcFileSaveTemplate: OnSaveTemplateModule(); break;
|
||
|
case kcFileClose: SafeFileClose(); break;
|
||
|
case kcFileAppend: OnAppendModule(); break;
|
||
|
|
||
|
case kcPlayPatternFromCursor: OnPatternPlay(); break;
|
||
|
case kcPlayPatternFromStart: OnPatternRestart(); break;
|
||
|
case kcPlaySongFromCursor: OnPatternPlayNoLoop(); break;
|
||
|
case kcPlaySongFromStart: OnPlayerPlayFromStart(); break;
|
||
|
case kcPlayPauseSong: OnPlayerPlay(); break;
|
||
|
case kcPlaySongFromPattern: OnPatternRestart(false); break;
|
||
|
case kcStopSong: OnPlayerStop(); break;
|
||
|
case kcPanic: OnPanic(); break;
|
||
|
case kcToggleLoopSong: SetLoopSong(!TrackerSettings::Instance().gbLoopSong); break;
|
||
|
|
||
|
case kcTempoIncreaseFine:
|
||
|
if(!modSpecs.hasFractionalTempo)
|
||
|
break;
|
||
|
[[fallthrough]];
|
||
|
case kcTempoIncrease:
|
||
|
if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo < modSpecs.GetTempoMax())
|
||
|
m_SndFile.m_PlayState.m_nMusicTempo = std::min(modSpecs.GetTempoMax(), tempo + TEMPO(wParam == kcTempoIncrease ? 1.0 : 0.1));
|
||
|
break;
|
||
|
case kcTempoDecreaseFine:
|
||
|
if(!modSpecs.hasFractionalTempo)
|
||
|
break;
|
||
|
[[fallthrough]];
|
||
|
case kcTempoDecrease:
|
||
|
if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo > modSpecs.GetTempoMin())
|
||
|
m_SndFile.m_PlayState.m_nMusicTempo = std::max(modSpecs.GetTempoMin(), tempo - TEMPO(wParam == kcTempoDecrease ? 1.0 : 0.1));
|
||
|
break;
|
||
|
case kcSpeedIncrease:
|
||
|
if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed < modSpecs.speedMax)
|
||
|
m_SndFile.m_PlayState.m_nMusicSpeed = speed + 1;
|
||
|
break;
|
||
|
case kcSpeedDecrease:
|
||
|
if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed > modSpecs.speedMin)
|
||
|
m_SndFile.m_PlayState.m_nMusicSpeed = speed - 1;
|
||
|
break;
|
||
|
|
||
|
case kcViewToggle:
|
||
|
if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr)
|
||
|
lastActiveFrame->ToggleViews();
|
||
|
break;
|
||
|
|
||
|
default: return kcNull;
|
||
|
}
|
||
|
|
||
|
return wParam;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::TogglePluginEditor(UINT plugin, bool onlyThisEditor)
|
||
|
{
|
||
|
if(plugin < MAX_MIXPLUGINS)
|
||
|
{
|
||
|
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plugin].pMixPlugin;
|
||
|
if(pPlugin != nullptr)
|
||
|
{
|
||
|
if(onlyThisEditor)
|
||
|
{
|
||
|
int32 posX = int32_min, posY = int32_min;
|
||
|
for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
|
||
|
{
|
||
|
SNDMIXPLUGIN &otherPlug = m_SndFile.m_MixPlugins[i];
|
||
|
if(i != plugin && otherPlug.pMixPlugin != nullptr && otherPlug.pMixPlugin->GetEditor() != nullptr)
|
||
|
{
|
||
|
otherPlug.pMixPlugin->CloseEditor();
|
||
|
if(otherPlug.editorX != int32_min)
|
||
|
{
|
||
|
posX = otherPlug.editorX;
|
||
|
posY = otherPlug.editorY;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
if(posX != int32_min)
|
||
|
{
|
||
|
m_SndFile.m_MixPlugins[plugin].editorX = posX;
|
||
|
m_SndFile.m_MixPlugins[plugin].editorY = posY;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
pPlugin->ToggleEditor();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SetLoopSong(bool loop)
|
||
|
{
|
||
|
TrackerSettings::Instance().gbLoopSong = loop;
|
||
|
m_SndFile.SetRepeatCount(loop ? -1 : 0);
|
||
|
CMainFrame::GetMainFrame()->UpdateAllViews(UpdateHint().MPTOptions());
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ChangeFileExtension(MODTYPE nNewType)
|
||
|
{
|
||
|
//Not making path if path is empty(case only(?) for new file)
|
||
|
if(!GetPathNameMpt().empty())
|
||
|
{
|
||
|
mpt::PathString drive;
|
||
|
mpt::PathString dir;
|
||
|
mpt::PathString fname;
|
||
|
mpt::PathString fext;
|
||
|
GetPathNameMpt().SplitPath(&drive, &dir, &fname, &fext);
|
||
|
|
||
|
mpt::PathString newPath = drive + dir;
|
||
|
|
||
|
// Catch case where we don't have a filename yet.
|
||
|
if(fname.empty())
|
||
|
{
|
||
|
newPath += mpt::PathString::FromCString(GetTitle()).SanitizeComponent();
|
||
|
} else
|
||
|
{
|
||
|
newPath += fname;
|
||
|
}
|
||
|
|
||
|
newPath += P_(".") + mpt::PathString::FromUTF8(CSoundFile::GetModSpecifications(nNewType).fileExtension);
|
||
|
|
||
|
// Forcing save dialog to appear after extension change - otherwise unnotified file overwriting may occur.
|
||
|
m_ShowSavedialog = true;
|
||
|
|
||
|
SetPathName(newPath, FALSE);
|
||
|
}
|
||
|
|
||
|
UpdateAllViews(NULL, UpdateHint().ModType());
|
||
|
}
|
||
|
|
||
|
|
||
|
CHANNELINDEX CModDoc::FindAvailableChannel() const
|
||
|
{
|
||
|
CHANNELINDEX chn = m_SndFile.GetNNAChannel(CHANNELINDEX_INVALID);
|
||
|
if(chn != CHANNELINDEX_INVALID)
|
||
|
return chn;
|
||
|
else
|
||
|
return GetNumChannels();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::RecordParamChange(PLUGINDEX plugSlot, PlugParamIndex paramIndex)
|
||
|
{
|
||
|
::SendNotifyMessage(m_hWndFollow, WM_MOD_RECORDPARAM, plugSlot, paramIndex);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::LearnMacro(int macroToSet, PlugParamIndex paramToUse)
|
||
|
{
|
||
|
if(macroToSet < 0 || macroToSet > kSFxMacros)
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If macro already exists for this param, inform user and return
|
||
|
if(auto macro = m_SndFile.m_MidiCfg.FindMacroForParam(paramToUse); macro >= 0)
|
||
|
{
|
||
|
CString message;
|
||
|
message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), macro);
|
||
|
Reporting::Information(message, _T("Macro exists for this parameter"));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Set new macro
|
||
|
if(paramToUse < 384)
|
||
|
{
|
||
|
m_SndFile.m_MidiCfg.CreateParameteredMacro(macroToSet, kSFxPlugParam, paramToUse);
|
||
|
} else
|
||
|
{
|
||
|
CString message;
|
||
|
message.Format(_T("Parameter %i beyond controllable range. Use Parameter Control Events to automate this parameter."), static_cast<int>(paramToUse));
|
||
|
Reporting::Information(message, _T("Macro not assigned for this parameter"));
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
CString message;
|
||
|
message.Format(_T("Parameter %i can now be controlled with macro %X."), static_cast<int>(paramToUse), macroToSet);
|
||
|
Reporting::Information(message, _T("Macro assigned for this parameter"));
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnSongProperties()
|
||
|
{
|
||
|
const bool wasUsingFrequencies = m_SndFile.PeriodsAreFrequencies();
|
||
|
CModTypeDlg dlg(m_SndFile, CMainFrame::GetMainFrame());
|
||
|
if(dlg.DoModal() == IDOK)
|
||
|
{
|
||
|
UpdateAllViews(nullptr, GeneralHint().General());
|
||
|
ScopedLogCapturer logcapturer(*this, _T("Conversion Status"));
|
||
|
bool showLog = false;
|
||
|
if(dlg.m_nType != GetModType())
|
||
|
{
|
||
|
if(!ChangeModType(dlg.m_nType))
|
||
|
return;
|
||
|
showLog = true;
|
||
|
}
|
||
|
|
||
|
CHANNELINDEX newChannels = Clamp(dlg.m_nChannels, m_SndFile.GetModSpecifications().channelsMin, m_SndFile.GetModSpecifications().channelsMax);
|
||
|
if(newChannels != GetNumChannels())
|
||
|
{
|
||
|
const bool showCancelInRemoveDlg = m_SndFile.GetModSpecifications().channelsMax >= m_SndFile.GetNumChannels();
|
||
|
if(ChangeNumChannels(newChannels, showCancelInRemoveDlg))
|
||
|
showLog = true;
|
||
|
|
||
|
// Force update of pattern highlights / num channels
|
||
|
UpdateAllViews(nullptr, PatternHint().Data());
|
||
|
UpdateAllViews(nullptr, GeneralHint().Channels());
|
||
|
}
|
||
|
|
||
|
if(wasUsingFrequencies != m_SndFile.PeriodsAreFrequencies())
|
||
|
{
|
||
|
for(auto &chn : m_SndFile.m_PlayState.Chn)
|
||
|
{
|
||
|
chn.nPeriod = 0;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SetModified();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::ViewMIDIMapping(PLUGINDEX plugin, PlugParamIndex param)
|
||
|
{
|
||
|
CMIDIMappingDialog dlg(CMainFrame::GetMainFrame(), m_SndFile);
|
||
|
if(plugin != PLUGINDEX_INVALID)
|
||
|
{
|
||
|
dlg.m_Setting.SetPlugIndex(plugin + 1);
|
||
|
dlg.m_Setting.SetParamIndex(param);
|
||
|
}
|
||
|
dlg.DoModal();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnChannelManager()
|
||
|
{
|
||
|
CChannelManagerDlg *instance = CChannelManagerDlg::sharedInstanceCreate();
|
||
|
if(instance != nullptr)
|
||
|
{
|
||
|
if(instance->IsDisplayed())
|
||
|
instance->Hide();
|
||
|
else
|
||
|
{
|
||
|
instance->SetDocument(this);
|
||
|
instance->Show();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Sets playback timer to playback time at given position.
|
||
|
// At the same time, the playback parameters (global volume, channel volume and stuff like that) are calculated for this position.
|
||
|
// Sample channels positions are only updated if setSamplePos is true *and* the user has chosen to update sample play positions on seek.
|
||
|
void CModDoc::SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos)
|
||
|
{
|
||
|
if(nOrd == ORDERINDEX_INVALID) return;
|
||
|
|
||
|
double t = m_SndFile.GetPlaybackTimeAt(nOrd, nRow, true, setSamplePos && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SYNCSAMPLEPOS) != 0);
|
||
|
if(t < 0)
|
||
|
{
|
||
|
// Position is never played regularly, but we may want to continue playing from here nevertheless.
|
||
|
m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
|
||
|
m_SndFile.m_PlayState.m_nRow = m_SndFile.m_PlayState.m_nNextRow = nRow;
|
||
|
}
|
||
|
|
||
|
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
|
||
|
if(pMainFrm != nullptr) pMainFrm->SetElapsedTime(std::max(0.0, t));
|
||
|
}
|
||
|
|
||
|
|
||
|
CString CModDoc::GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr,
|
||
|
bool bEmptyInsteadOfNoName /* = false*/,
|
||
|
bool bIncludeIndex /* = true*/) const
|
||
|
{
|
||
|
if(nInstr >= MAX_INSTRUMENTS || m_SndFile.GetNumInstruments() == 0 || m_SndFile.Instruments[nInstr] == nullptr)
|
||
|
return CString();
|
||
|
|
||
|
CString displayName, instrumentName, pluginName;
|
||
|
|
||
|
// Get instrument name.
|
||
|
instrumentName = mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetInstrumentName(nInstr));
|
||
|
|
||
|
// If instrument name is empty, use name of the sample mapped to C-5.
|
||
|
if (instrumentName.IsEmpty())
|
||
|
{
|
||
|
const SAMPLEINDEX nSmp = m_SndFile.Instruments[nInstr]->Keyboard[NOTE_MIDDLEC - 1];
|
||
|
if (nSmp <= m_SndFile.GetNumSamples() && m_SndFile.GetSample(nSmp).HasSampleData())
|
||
|
instrumentName = _T("s: ") + mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetSampleName(nSmp));
|
||
|
}
|
||
|
|
||
|
// Get plugin name.
|
||
|
const PLUGINDEX nPlug = m_SndFile.Instruments[nInstr]->nMixPlug;
|
||
|
if (nPlug > 0 && nPlug < MAX_MIXPLUGINS)
|
||
|
pluginName = mpt::ToCString(m_SndFile.m_MixPlugins[nPlug-1].GetName());
|
||
|
|
||
|
if (pluginName.IsEmpty())
|
||
|
{
|
||
|
if(bEmptyInsteadOfNoName && instrumentName.IsEmpty())
|
||
|
return TEXT("");
|
||
|
if(instrumentName.IsEmpty())
|
||
|
instrumentName = _T("(no name)");
|
||
|
if (bIncludeIndex)
|
||
|
displayName.Format(_T("%02d: %s"), nInstr, instrumentName.GetString());
|
||
|
else
|
||
|
displayName = instrumentName;
|
||
|
} else
|
||
|
{
|
||
|
if (bIncludeIndex)
|
||
|
displayName.Format(TEXT("%02d: %s (%s)"), nInstr, instrumentName.GetString(), pluginName.GetString());
|
||
|
else
|
||
|
displayName.Format(TEXT("%s (%s)"), instrumentName.GetString(), pluginName.GetString());
|
||
|
}
|
||
|
return displayName;
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::SafeFileClose()
|
||
|
{
|
||
|
// Verify that the main window has the focus. This saves us a lot of trouble because active modal dialogs cannot know if their pSndFile pointers are still valid.
|
||
|
if(GetActiveWindow() == CMainFrame::GetMainFrame()->m_hWnd)
|
||
|
OnFileClose();
|
||
|
}
|
||
|
|
||
|
|
||
|
// "Panic button". This resets all VSTi, OPL and sample notes.
|
||
|
void CModDoc::OnPanic()
|
||
|
{
|
||
|
CriticalSection cs;
|
||
|
m_SndFile.ResetChannels();
|
||
|
m_SndFile.StopAllVsti();
|
||
|
}
|
||
|
|
||
|
|
||
|
// Before saving, make sure that every char after the terminating null char is also null.
|
||
|
// Else, garbage might end up in various text strings that wasn't supposed to be there.
|
||
|
void CModDoc::FixNullStrings()
|
||
|
{
|
||
|
// Macros
|
||
|
m_SndFile.m_MidiCfg.Sanitize();
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnSaveCopy()
|
||
|
{
|
||
|
DoSave(mpt::PathString(), false);
|
||
|
}
|
||
|
|
||
|
|
||
|
void CModDoc::OnSaveTemplateModule()
|
||
|
{
|
||
|
// Create template folder if doesn't exist already.
|
||
|
const mpt::PathString templateFolder = TrackerSettings::Instance().PathUserTemplates.GetDefaultDir();
|
||
|
if (!templateFolder.IsDirectory())
|
||
|
{
|
||
|
if (!CreateDirectory(templateFolder.AsNative().c_str(), nullptr))
|
||
|
{
|
||
|
Reporting::Notification(MPT_CFORMAT("Error: Unable to create template folder '{}'")( templateFolder));
|
||
|
return;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Generate file name candidate.
|
||
|
mpt::PathString sName;
|
||
|
for(size_t i = 0; i < 1000; ++i)
|
||
|
{
|
||
|
sName += P_("newTemplate") + mpt::PathString::FromUnicode(mpt::ufmt::val(i));
|
||
|
sName += P_(".") + mpt::PathString::FromUTF8(m_SndFile.GetModSpecifications().fileExtension);
|
||
|
if (!(templateFolder + sName).FileOrDirectoryExists())
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// Ask file name from user.
|
||
|
FileDialog dlg = SaveFileDialog()
|
||
|
.DefaultExtension(m_SndFile.GetModSpecifications().fileExtension)
|
||
|
.DefaultFilename(sName)
|
||
|
.ExtensionFilter(ModTypeToFilter(m_SndFile))
|
||
|
.WorkingDirectory(templateFolder);
|
||
|
if(!dlg.Show())
|
||
|
return;
|
||
|
|
||
|
if (OnSaveDocument(dlg.GetFirstFile(), false))
|
||
|
{
|
||
|
// Update template menu.
|
||
|
CMainFrame::GetMainFrame()->CreateTemplateModulesMenu();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Create an undo point that stores undo data for all existing patterns
|
||
|
void CModDoc::PrepareUndoForAllPatterns(bool storeChannelInfo, const char *description)
|
||
|
{
|
||
|
bool linkUndo = false;
|
||
|
|
||
|
PATTERNINDEX lastPat = 0;
|
||
|
for(PATTERNINDEX pat = 0; pat < m_SndFile.Patterns.Size(); pat++)
|
||
|
{
|
||
|
if(m_SndFile.Patterns.IsValidPat(pat)) lastPat = pat;
|
||
|
}
|
||
|
|
||
|
for(PATTERNINDEX pat = 0; pat <= lastPat; pat++)
|
||
|
{
|
||
|
if(m_SndFile.Patterns.IsValidPat(pat))
|
||
|
{
|
||
|
GetPatternUndo().PrepareUndo(pat, 0, 0, GetNumChannels(), m_SndFile.Patterns[pat].GetNumRows(), description, linkUndo, storeChannelInfo && pat == lastPat);
|
||
|
linkUndo = true;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
CString CModDoc::LinearToDecibels(double value, double valueAtZeroDB)
|
||
|
{
|
||
|
if (value == 0) return _T("-inf");
|
||
|
|
||
|
double changeFactor = value / valueAtZeroDB;
|
||
|
double dB = 20.0 * std::log10(changeFactor);
|
||
|
|
||
|
CString s = (dB >= 0) ? _T("+") : _T("");
|
||
|
s.AppendFormat(_T("%.2f dB"), dB);
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
|
||
|
CString CModDoc::PanningToString(int32 value, int32 valueAtCenter)
|
||
|
{
|
||
|
if(value == valueAtCenter)
|
||
|
return _T("Center");
|
||
|
|
||
|
CString s;
|
||
|
s.Format(_T("%i%% %s"), (std::abs(static_cast<int>(value) - valueAtCenter) * 100) / valueAtCenter, value < valueAtCenter ? _T("Left") : _T("Right"));
|
||
|
return s;
|
||
|
}
|
||
|
|
||
|
|
||
|
// Apply OPL patch changes to live playback
|
||
|
void CModDoc::UpdateOPLInstrument(SAMPLEINDEX smp)
|
||
|
{
|
||
|
const ModSample &sample = m_SndFile.GetSample(smp);
|
||
|
if(!sample.uFlags[CHN_ADLIB] || !m_SndFile.m_opl || CMainFrame::GetMainFrame()->GetModPlaying() != this)
|
||
|
return;
|
||
|
|
||
|
CriticalSection cs;
|
||
|
const auto &patch = sample.adlib;
|
||
|
for(CHANNELINDEX chn = 0; chn < MAX_CHANNELS; chn++)
|
||
|
{
|
||
|
const auto &c = m_SndFile.m_PlayState.Chn[chn];
|
||
|
if(c.pModSample == &sample && c.IsSamplePlaying())
|
||
|
{
|
||
|
m_SndFile.m_opl->Patch(chn, patch);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
// Store all view positions t settings file
|
||
|
void CModDoc::SerializeViews() const
|
||
|
{
|
||
|
const mpt::PathString pathName = theApp.IsPortableMode() ? GetPathNameMpt().AbsolutePathToRelative(theApp.GetInstallPath()) : GetPathNameMpt();
|
||
|
if(pathName.empty())
|
||
|
{
|
||
|
return;
|
||
|
}
|
||
|
std::ostringstream f(std::ios::out | std::ios::binary);
|
||
|
|
||
|
CRect mdiRect;
|
||
|
::GetClientRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
|
||
|
const int width = mdiRect.Width();
|
||
|
const int height = mdiRect.Height();
|
||
|
|
||
|
const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||
|
|
||
|
// Document view positions and sizes
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
while(pos != nullptr && !mdiRect.IsRectEmpty())
|
||
|
{
|
||
|
CModControlView *pView = dynamic_cast<CModControlView *>(GetNextView(pos));
|
||
|
if(pView)
|
||
|
{
|
||
|
CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
|
||
|
WINDOWPLACEMENT wnd;
|
||
|
wnd.length = sizeof(WINDOWPLACEMENT);
|
||
|
pChildFrm->GetWindowPlacement(&wnd);
|
||
|
const CRect rect = wnd.rcNormalPosition;
|
||
|
|
||
|
// Write size information
|
||
|
uint8 windowState = 0;
|
||
|
if(wnd.showCmd == SW_SHOWMAXIMIZED) windowState = 1;
|
||
|
else if(wnd.showCmd == SW_SHOWMINIMIZED) windowState = 2;
|
||
|
mpt::IO::WriteIntLE<uint8>(f, 0); // Window type
|
||
|
mpt::IO::WriteIntLE<uint8>(f, windowState);
|
||
|
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.left, 1 << 30, width));
|
||
|
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.top, 1 << 30, height));
|
||
|
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Width(), 1 << 30, width));
|
||
|
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Height(), 1 << 30, height));
|
||
|
|
||
|
std::string s = pChildFrm->SerializeView();
|
||
|
mpt::IO::WriteVarInt(f, s.size());
|
||
|
f << s;
|
||
|
}
|
||
|
}
|
||
|
// Plugin window positions
|
||
|
for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
|
||
|
{
|
||
|
if(m_SndFile.m_MixPlugins[i].IsValidPlugin() && m_SndFile.m_MixPlugins[i].editorX != int32_min && cxScreen && cyScreen)
|
||
|
{
|
||
|
// Translate screen position into percentage (to make it independent of the actual screen resolution)
|
||
|
int32 editorX = Util::muldivr(m_SndFile.m_MixPlugins[i].editorX, 1 << 30, cxScreen);
|
||
|
int32 editorY = Util::muldivr(m_SndFile.m_MixPlugins[i].editorY, 1 << 30, cyScreen);
|
||
|
|
||
|
mpt::IO::WriteIntLE<uint8>(f, 1); // Window type
|
||
|
mpt::IO::WriteIntLE<uint8>(f, 0); // Version
|
||
|
mpt::IO::WriteVarInt(f, i);
|
||
|
mpt::IO::WriteIntLE<int32>(f, editorX);
|
||
|
mpt::IO::WriteIntLE<int32>(f, editorY);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
SettingsContainer &settings = theApp.GetSongSettings();
|
||
|
const std::string s = f.str();
|
||
|
settings.Write(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode(), pathName);
|
||
|
settings.Write(U_("WindowSettings"), pathName.ToUnicode(), mpt::encode_hex(mpt::as_span(s)));
|
||
|
}
|
||
|
|
||
|
|
||
|
// Restore all view positions from settings file
|
||
|
void CModDoc::DeserializeViews()
|
||
|
{
|
||
|
mpt::PathString pathName = GetPathNameMpt();
|
||
|
if(pathName.empty()) return;
|
||
|
|
||
|
SettingsContainer &settings = theApp.GetSongSettings();
|
||
|
mpt::ustring s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
|
||
|
if(s.size() < 2)
|
||
|
{
|
||
|
// Try relative path
|
||
|
pathName = pathName.RelativePathToAbsolute(theApp.GetInstallPath());
|
||
|
s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
|
||
|
if(s.size() < 2)
|
||
|
{
|
||
|
// Try searching for filename instead of full path name
|
||
|
const mpt::ustring altName = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode());
|
||
|
s = settings.Read<mpt::ustring>(U_("WindowSettings"), altName);
|
||
|
if(s.size() < 2) return;
|
||
|
}
|
||
|
}
|
||
|
std::vector<std::byte> bytes = mpt::decode_hex(s);
|
||
|
|
||
|
FileReader file(mpt::as_span(bytes));
|
||
|
|
||
|
CRect mdiRect;
|
||
|
::GetWindowRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
|
||
|
const int width = mdiRect.Width();
|
||
|
const int height = mdiRect.Height();
|
||
|
|
||
|
const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||
|
|
||
|
POSITION pos = GetFirstViewPosition();
|
||
|
CChildFrame *pChildFrm = nullptr;
|
||
|
if(pos != nullptr) pChildFrm = dynamic_cast<CChildFrame *>(GetNextView(pos)->GetParentFrame());
|
||
|
|
||
|
bool anyMaximized = false;
|
||
|
while(file.CanRead(1))
|
||
|
{
|
||
|
const uint8 windowType = file.ReadUint8();
|
||
|
if(windowType == 0)
|
||
|
{
|
||
|
// Document view positions and sizes
|
||
|
const uint8 windowState = file.ReadUint8();
|
||
|
CRect rect;
|
||
|
rect.left = Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
|
||
|
rect.top = Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
|
||
|
rect.right = rect.left + Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
|
||
|
rect.bottom = rect.top + Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
|
||
|
size_t dataSize;
|
||
|
file.ReadVarInt(dataSize);
|
||
|
FileReader data = file.ReadChunk(dataSize);
|
||
|
|
||
|
if(pChildFrm == nullptr)
|
||
|
{
|
||
|
CModDocTemplate *pTemplate = static_cast<CModDocTemplate *>(GetDocTemplate());
|
||
|
ASSERT_VALID(pTemplate);
|
||
|
pChildFrm = static_cast<CChildFrame *>(pTemplate->CreateNewFrame(this, nullptr));
|
||
|
if(pChildFrm != nullptr)
|
||
|
{
|
||
|
pTemplate->InitialUpdateFrame(pChildFrm, this);
|
||
|
}
|
||
|
}
|
||
|
if(pChildFrm != nullptr)
|
||
|
{
|
||
|
if(!mdiRect.IsRectEmpty())
|
||
|
{
|
||
|
WINDOWPLACEMENT wnd;
|
||
|
wnd.length = sizeof(wnd);
|
||
|
pChildFrm->GetWindowPlacement(&wnd);
|
||
|
wnd.showCmd = SW_SHOWNOACTIVATE;
|
||
|
if(windowState == 1 || anyMaximized)
|
||
|
{
|
||
|
// Once a window has been maximized, all following windows have to be marked as maximized as well.
|
||
|
wnd.showCmd = SW_MAXIMIZE;
|
||
|
anyMaximized = true;
|
||
|
} else if(windowState == 2)
|
||
|
{
|
||
|
wnd.showCmd = SW_MINIMIZE;
|
||
|
}
|
||
|
if(rect.left < width && rect.right > 0 && rect.top < height && rect.bottom > 0)
|
||
|
{
|
||
|
wnd.rcNormalPosition = CRect(rect.left, rect.top, rect.right, rect.bottom);
|
||
|
}
|
||
|
pChildFrm->SetWindowPlacement(&wnd);
|
||
|
}
|
||
|
pChildFrm->DeserializeView(data);
|
||
|
pChildFrm = nullptr;
|
||
|
}
|
||
|
} else if(windowType == 1)
|
||
|
{
|
||
|
if(file.ReadUint8() != 0)
|
||
|
break;
|
||
|
// Plugin window positions
|
||
|
PLUGINDEX plug = 0;
|
||
|
if(file.ReadVarInt(plug) && plug < MAX_MIXPLUGINS)
|
||
|
{
|
||
|
int32 editorX = file.ReadInt32LE();
|
||
|
int32 editorY = file.ReadInt32LE();
|
||
|
if(editorX != int32_min && editorY != int32_min)
|
||
|
{
|
||
|
m_SndFile.m_MixPlugins[plug].editorX = Util::muldivr(editorX, cxScreen, 1 << 30);
|
||
|
m_SndFile.m_MixPlugins[plug].editorY = Util::muldivr(editorY, cyScreen, 1 << 30);
|
||
|
}
|
||
|
}
|
||
|
} else
|
||
|
{
|
||
|
// Unknown type
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
OPENMPT_NAMESPACE_END
|