winamp/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp
2024-09-24 14:54:57 +02:00

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 &params, NoteToChannelMap *noteChannel)
{
CHANNELINDEX channel = GetNumChannels();
ModCommand::NOTE note = params.m_note;
if(ModCommand::IsNote(ModCommand::NOTE(note)))
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if(pMainFrm == nullptr || note == NOTE_NONE) return CHANNELINDEX_INVALID;
if (pMainFrm->GetModPlaying() != this)
{
// All notes off when resuming paused playback
m_SndFile.ResetChannels();
m_SndFile.m_SongFlags.set(SONG_PAUSED);
pMainFrm->PlayMod(this);
}
CriticalSection cs;
if(params.m_notesPlaying)
CheckNNA(note, params.m_instr, *params.m_notesPlaying);
// Find a channel to play on
channel = FindAvailableChannel();
ModChannel &chn = m_SndFile.m_PlayState.Chn[channel];
// reset channel properties; in theory the chan is completely unused anyway.
chn.Reset(ModChannel::resetTotal, m_SndFile, CHANNELINDEX_INVALID, CHN_MUTE);
chn.nNewNote = chn.nLastNote = static_cast<uint8>(note);
chn.nVolume = 256;
if(params.m_instr)
{
// Set instrument (or sample if there are no instruments)
chn.ResetEnvelopes();
m_SndFile.InstrumentChange(chn, params.m_instr);
} else if(params.m_sample > 0 && params.m_sample <= GetNumSamples()) // Or set sample explicitely
{
ModSample &sample = m_SndFile.GetSample(params.m_sample);
chn.pCurrentSample = sample.samplev();
chn.pModInstrument = nullptr;
chn.pModSample = &sample;
chn.nFineTune = sample.nFineTune;
chn.nC5Speed = sample.nC5Speed;
chn.nLoopStart = sample.nLoopStart;
chn.nLoopEnd = sample.nLoopEnd;
chn.dwFlags = (sample.uFlags & (CHN_SAMPLEFLAGS & ~CHN_MUTE));
chn.nPan = 128;
if(sample.uFlags[CHN_PANNING]) chn.nPan = sample.nPan;
chn.UpdateInstrumentVolume(&sample, nullptr);
}
chn.nFadeOutVol = 0x10000;
chn.isPreviewNote = true;
if(params.m_currentChannel != CHANNELINDEX_INVALID)
chn.nMasterChn = params.m_currentChannel + 1;
else
chn.nMasterChn = 0;
if(chn.dwFlags[CHN_ADLIB] && chn.pModSample && m_SndFile.m_opl)
{
m_SndFile.m_opl->Patch(channel, chn.pModSample->adlib);
}
m_SndFile.NoteChange(chn, note, false, true, true, channel);
if(params.m_volume >= 0) chn.nVolume = std::min(params.m_volume, 256);
// Handle sample looping.
// Changed line to fix http://forum.openmpt.org/index.php?topic=1700.0
//if ((loopstart + 16 < loopend) && (loopstart >= 0) && (loopend <= (LONG)pchn.nLength))
if ((params.m_loopStart + 16 < params.m_loopEnd) && (params.m_loopStart >= 0) && (chn.pModSample != nullptr))
{
chn.position.Set(params.m_loopStart);
chn.nLoopStart = params.m_loopStart;
chn.nLoopEnd = params.m_loopEnd;
chn.nLength = std::min(params.m_loopEnd, chn.pModSample->nLength);
}
// Handle extra-loud flag
chn.dwFlags.set(CHN_EXTRALOUD, !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOEXTRALOUD) && params.m_sample);
// Handle custom start position
if(params.m_sampleOffset > 0 && chn.pModSample)
{
chn.position.Set(params.m_sampleOffset);
// If start position is after loop end, set loop end to sample end so that the sample starts
// playing.
if(chn.nLoopEnd < params.m_sampleOffset)
chn.nLength = chn.nLoopEnd = chn.pModSample->nLength;
}
// VSTi preview
if(params.m_instr > 0 && params.m_instr <= m_SndFile.GetNumInstruments())
{
const ModInstrument *pIns = m_SndFile.Instruments[params.m_instr];
if (pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan
{
PLUGINDEX nPlugin = 0;
if (chn.pModInstrument)
nPlugin = chn.pModInstrument->nMixPlug; // First try instrument plugin
if ((!nPlugin || nPlugin > MAX_MIXPLUGINS) && params.m_currentChannel != CHANNELINDEX_INVALID)
nPlugin = m_SndFile.ChnSettings[params.m_currentChannel].nMixPlugin; // Then try channel plugin
if ((nPlugin) && (nPlugin <= MAX_MIXPLUGINS))
{
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[nPlugin - 1].pMixPlugin;
if(pPlugin != nullptr)
{
pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN], static_cast<uint16>(chn.nVolume), channel);
}
}
}
}
// Remove channel from list of mixed channels to fix https://bugs.openmpt.org/view.php?id=209
// This is required because a previous note on the same channel might have just stopped playing,
// but the channel is still in the mix list.
// Since the channel volume / etc is only updated every tick in CSoundFile::ReadNote, and we
// do not want to duplicate mixmode-dependant logic here, CSoundFile::CreateStereoMix may already
// try to mix our newly set up channel at volume 0 if we don't remove it from the list.
auto mixBegin = std::begin(m_SndFile.m_PlayState.ChnMix);
auto mixEnd = std::remove(mixBegin, mixBegin + m_SndFile.m_nMixChannels, channel);
m_SndFile.m_nMixChannels = static_cast<CHANNELINDEX>(std::distance(mixBegin, mixEnd));
if(noteChannel)
{
noteChannel->at(note - NOTE_MIN) = channel;
}
} else
{
CriticalSection cs;
// Apply note cut / off / fade (also on preview channels)
m_SndFile.NoteChange(m_SndFile.m_PlayState.Chn[channel], note);
for(CHANNELINDEX c = m_SndFile.GetNumChannels(); c < MAX_CHANNELS; c++)
{
ModChannel &chn = m_SndFile.m_PlayState.Chn[c];
if(chn.isPreviewNote && (chn.pModSample || chn.pModInstrument))
{
m_SndFile.NoteChange(chn, note);
}
}
}
return channel;
}
bool CModDoc::NoteOff(UINT note, bool fade, INSTRUMENTINDEX ins, CHANNELINDEX currentChn)
{
CriticalSection cs;
if(ins != INSTRUMENTINDEX_INVALID && ins <= m_SndFile.GetNumInstruments() && ModCommand::IsNote(ModCommand::NOTE(note)))
{
const ModInstrument *pIns = m_SndFile.Instruments[ins];
if(pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan
{
PLUGINDEX plug = pIns->nMixPlug; // First try intrument VST
if((!plug || plug > MAX_MIXPLUGINS) // No good plug yet
&& currentChn < MAX_BASECHANNELS) // Chan OK
{
plug = m_SndFile.ChnSettings[currentChn].nMixPlugin;// Then try Channel VST
}
if(plug && plug <= MAX_MIXPLUGINS)
{
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plug - 1].pMixPlugin;
if(pPlugin)
{
pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN] + NOTE_KEYOFF, 0, currentChn);
}
}
}
}
const FlagSet<ChannelFlags> mask = (fade ? CHN_NOTEFADE : (CHN_NOTEFADE | CHN_KEYOFF));
const CHANNELINDEX startChn = currentChn != CHANNELINDEX_INVALID ? currentChn : m_SndFile.m_nChannels;
const CHANNELINDEX endChn = currentChn != CHANNELINDEX_INVALID ? currentChn + 1 : MAX_CHANNELS;
ModChannel *pChn = &m_SndFile.m_PlayState.Chn[startChn];
for(CHANNELINDEX i = startChn; i < endChn; i++, pChn++)
{
// Fade all channels > m_nChannels which are playing this note and aren't NNA channels.
if((pChn->isPreviewNote || i < m_SndFile.GetNumChannels())
&& !pChn->dwFlags[mask]
&& (pChn->nLength || pChn->dwFlags[CHN_ADLIB])
&& (note == pChn->nNewNote || note == NOTE_NONE))
{
m_SndFile.KeyOff(*pChn);
if (!m_SndFile.m_nInstruments) pChn->dwFlags.reset(CHN_LOOP | CHN_PINGPONGFLAG);
if (fade) pChn->dwFlags.set(CHN_NOTEFADE);
// Instantly stop samples that would otherwise play forever
if (pChn->pModInstrument && !pChn->pModInstrument->nFadeOut)
pChn->nFadeOutVol = 0;
if(pChn->dwFlags[CHN_ADLIB] && m_SndFile.m_opl)
{
m_SndFile.m_opl->NoteOff(i);
}
if (note) break;
}
}
return true;
}
// Apply DNA/NNA settings for note preview. It will also set the specified note to be playing in the playingNotes set.
void CModDoc::CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes)
{
if(ins > GetNumInstruments() || m_SndFile.Instruments[ins] == nullptr || note >= playingNotes.size())
{
return;
}
const ModInstrument *pIns = m_SndFile.Instruments[ins];
for(CHANNELINDEX chn = GetNumChannels(); chn < MAX_CHANNELS; chn++)
{
const ModChannel &channel = m_SndFile.m_PlayState.Chn[chn];
if(channel.pModInstrument == pIns && channel.isPreviewNote && ModCommand::IsNote(channel.nLastNote)
&& (channel.nLength || pIns->HasValidMIDIChannel()) && !playingNotes[channel.nLastNote])
{
CHANNELINDEX nnaChn = m_SndFile.CheckNNA(chn, ins, note, false);
if(nnaChn != CHANNELINDEX_INVALID)
{
// Keep the new NNA channel playing in the same channel slot.
// That way, we do not need to touch the ChnMix array, and we avoid the same channel being checked twice.
if(nnaChn != chn)
{
m_SndFile.m_PlayState.Chn[chn] = std::move(m_SndFile.m_PlayState.Chn[nnaChn]);
m_SndFile.m_PlayState.Chn[nnaChn] = {};
}
// Avoid clicks if the channel wasn't ramping before.
m_SndFile.m_PlayState.Chn[chn].dwFlags.set(CHN_FASTVOLRAMP);
m_SndFile.ProcessRamping(m_SndFile.m_PlayState.Chn[chn]);
}
}
}
playingNotes.set(note);
}
// Check if a given note of an instrument or sample is playing from the editor.
// If note == 0, just check if an instrument or sample is playing.
bool CModDoc::IsNotePlaying(UINT note, SAMPLEINDEX nsmp, INSTRUMENTINDEX nins)
{
ModChannel *pChn = &m_SndFile.m_PlayState.Chn[m_SndFile.GetNumChannels()];
for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++, pChn++) if (pChn->isPreviewNote)
{
if(pChn->nLength != 0 && !pChn->dwFlags[CHN_NOTEFADE | CHN_KEYOFF| CHN_MUTE]
&& (note == pChn->nNewNote || note == NOTE_NONE)
&& (pChn->pModSample == &m_SndFile.GetSample(nsmp) || !nsmp)
&& (pChn->pModInstrument == m_SndFile.Instruments[nins] || !nins)) return true;
}
return false;
}
bool CModDoc::MuteToggleModifiesDocument() const
{
return (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)) && TrackerSettings::Instance().MiscSaveChannelMuteStatus;
}
bool CModDoc::MuteChannel(CHANNELINDEX nChn, bool doMute)
{
if (nChn >= m_SndFile.GetNumChannels())
{
return false;
}
// Mark channel as muted in channel settings
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_MUTE, doMute);
const bool success = UpdateChannelMuteStatus(nChn);
if(success && MuteToggleModifiesDocument())
{
SetModified();
}
return success;
}
bool CModDoc::UpdateChannelMuteStatus(CHANNELINDEX nChn)
{
const ChannelFlags muteType = CSoundFile::GetChannelMuteFlag();
if (nChn >= m_SndFile.GetNumChannels())
{
return false;
}
const bool doMute = m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];
// Mute pattern channel
if (doMute)
{
m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(muteType);
if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(nChn);
// Kill VSTi notes on muted channel.
PLUGINDEX nPlug = m_SndFile.GetBestPlugin(m_SndFile.m_PlayState, nChn, PrioritiseInstrument, EvenIfMuted);
if ((nPlug) && (nPlug<=MAX_MIXPLUGINS))
{
IMixPlugin *pPlug = m_SndFile.m_MixPlugins[nPlug - 1].pMixPlugin;
const ModInstrument* pIns = m_SndFile.m_PlayState.Chn[nChn].pModInstrument;
if (pPlug && pIns)
{
pPlug->MidiCommand(*pIns, NOTE_KEYOFF, 0, nChn);
}
}
} else
{
// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
}
// Mute any NNA'd channels
for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
{
if (m_SndFile.m_PlayState.Chn[i].nMasterChn == nChn + 1u)
{
if (doMute)
{
m_SndFile.m_PlayState.Chn[i].dwFlags.set(muteType);
if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(i);
} else
{
// On unmute alway cater for both mute types - this way there's no probs if user changes mute mode.
m_SndFile.m_PlayState.Chn[i].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE);
}
}
}
return true;
}
bool CModDoc::IsChannelSolo(CHANNELINDEX nChn) const
{
if (nChn >= m_SndFile.m_nChannels) return true;
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_SOLO];
}
bool CModDoc::SoloChannel(CHANNELINDEX nChn, bool bSolo)
{
if (nChn >= m_SndFile.m_nChannels) return false;
if (MuteToggleModifiesDocument()) SetModified();
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SOLO, bSolo);
return true;
}
bool CModDoc::IsChannelNoFx(CHANNELINDEX nChn) const
{
if (nChn >= m_SndFile.m_nChannels) return true;
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_NOFX];
}
bool CModDoc::NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix)
{
if (nChn >= m_SndFile.m_nChannels) return false;
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_NOFX, bNoFx);
if(updateMix) m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_NOFX, bNoFx);
return true;
}
RecordGroup CModDoc::GetChannelRecordGroup(CHANNELINDEX channel) const
{
if(channel >= GetNumChannels())
return RecordGroup::NoGroup;
if(m_bsMultiRecordMask[channel])
return RecordGroup::Group1;
if(m_bsMultiSplitRecordMask[channel])
return RecordGroup::Group2;
return RecordGroup::NoGroup;
}
void CModDoc::SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
{
if(channel >= GetNumChannels())
return;
m_bsMultiRecordMask.set(channel, recordGroup == RecordGroup::Group1);
m_bsMultiSplitRecordMask.set(channel, recordGroup == RecordGroup::Group2);
}
void CModDoc::ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup)
{
if(channel >= GetNumChannels())
return;
if(recordGroup == RecordGroup::Group1)
{
m_bsMultiRecordMask.flip(channel);
m_bsMultiSplitRecordMask.reset(channel);
} else if(recordGroup == RecordGroup::Group2)
{
m_bsMultiRecordMask.reset(channel);
m_bsMultiSplitRecordMask.flip(channel);
}
}
void CModDoc::ReinitRecordState(bool unselect)
{
if(unselect)
{
m_bsMultiRecordMask.reset();
m_bsMultiSplitRecordMask.reset();
} else
{
m_bsMultiRecordMask.set();
m_bsMultiSplitRecordMask.set();
}
}
bool CModDoc::MuteSample(SAMPLEINDEX nSample, bool bMute)
{
if ((nSample < 1) || (nSample > m_SndFile.GetNumSamples())) return false;
m_SndFile.GetSample(nSample).uFlags.set(CHN_MUTE, bMute);
return true;
}
bool CModDoc::MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute)
{
if ((nInstr < 1) || (nInstr > m_SndFile.GetNumInstruments()) || (!m_SndFile.Instruments[nInstr])) return false;
m_SndFile.Instruments[nInstr]->dwFlags.set(INS_MUTE, bMute);
return true;
}
bool CModDoc::SurroundChannel(CHANNELINDEX nChn, bool surround)
{
if(nChn >= m_SndFile.GetNumChannels()) return false;
if(!(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) surround = false;
if(surround != m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
{
// Update channel configuration
if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SURROUND, surround);
if(surround)
{
m_SndFile.ChnSettings[nChn].nPan = 128;
}
}
// Update playing channel
m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_SURROUND, surround);
if(surround)
{
m_SndFile.m_PlayState.Chn[nChn].nPan = 128;
}
return true;
}
bool CModDoc::SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume)
{
bool ok = false;
if(nChn >= m_SndFile.GetNumChannels() || nVolume > 64) return false;
if(m_SndFile.ChnSettings[nChn].nVolume != nVolume)
{
m_SndFile.ChnSettings[nChn].nVolume = nVolume;
if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
ok = true;
}
m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = nVolume;
return ok;
}
bool CModDoc::SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan)
{
bool ok = false;
if(nChn >= m_SndFile.GetNumChannels() || nPan > 256) return false;
if(m_SndFile.ChnSettings[nChn].nPan != nPan || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND])
{
m_SndFile.ChnSettings[nChn].nPan = nPan;
m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND);
if(m_SndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified();
ok = true;
}
m_SndFile.m_PlayState.Chn[nChn].nPan = nPan;
m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SURROUND);
return ok;
}
bool CModDoc::IsChannelMuted(CHANNELINDEX nChn) const
{
if(nChn >= m_SndFile.GetNumChannels()) return true;
return m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE];
}
bool CModDoc::IsSampleMuted(SAMPLEINDEX nSample) const
{
if(!nSample || nSample > m_SndFile.GetNumSamples()) return false;
return m_SndFile.GetSample(nSample).uFlags[CHN_MUTE];
}
bool CModDoc::IsInstrumentMuted(INSTRUMENTINDEX nInstr) const
{
if(!nInstr || nInstr > m_SndFile.GetNumInstruments() || !m_SndFile.Instruments[nInstr]) return false;
return m_SndFile.Instruments[nInstr]->dwFlags[INS_MUTE];
}
UINT CModDoc::GetPatternSize(PATTERNINDEX nPat) const
{
if(m_SndFile.Patterns.IsValidIndex(nPat)) return m_SndFile.Patterns[nPat].GetNumRows();
return 0;
}
void CModDoc::SetFollowWnd(HWND hwnd)
{
m_hWndFollow = hwnd;
}
bool CModDoc::IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const
{
return m_SndFile.IsSampleReferencedByInstrument(nSmp, nIns);
}
// Find an instrument that references the given sample.
// If no such instrument is found, INSTRUMENTINDEX_INVALID is returned.
INSTRUMENTINDEX CModDoc::FindSampleParent(SAMPLEINDEX sample) const
{
if(sample == 0)
{
return INSTRUMENTINDEX_INVALID;
}
for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++)
{
const ModInstrument *pIns = m_SndFile.Instruments[i];
if(pIns != nullptr)
{
for(size_t j = 0; j < NOTE_MAX; j++)
{
if(pIns->Keyboard[j] == sample)
{
return i;
}
}
}
}
return INSTRUMENTINDEX_INVALID;
}
SAMPLEINDEX CModDoc::FindInstrumentChild(INSTRUMENTINDEX nIns) const
{
if ((!nIns) || (nIns > m_SndFile.GetNumInstruments())) return 0;
const ModInstrument *pIns = m_SndFile.Instruments[nIns];
if (pIns)
{
for (auto n : pIns->Keyboard)
{
if ((n) && (n <= m_SndFile.GetNumSamples())) return n;
}
}
return 0;
}
LRESULT CModDoc::ActivateView(UINT nIdView, DWORD dwParam)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (!pMainFrm) return 0;
CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
if (pMDIActive)
{
CView *pView = pMDIActive->GetActiveView();
if ((pView) && (pView->GetDocument() == this))
{
return ((CChildFrame *)pMDIActive)->ActivateView(nIdView, dwParam);
}
}
POSITION pos = GetFirstViewPosition();
while (pos != NULL)
{
CView *pView = GetNextView(pos);
if ((pView) && (pView->GetDocument() == this))
{
CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
pChildFrm->MDIActivate();
return pChildFrm->ActivateView(nIdView, dwParam);
}
}
return 0;
}
// Activate document's window.
void CModDoc::ActivateWindow()
{
CChildFrame *pChildFrm = GetChildFrame();
if(pChildFrm) pChildFrm->MDIActivate();
}
void CModDoc::UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint)
{
// Tunnel our UpdateHint into an LPARAM
CDocument::UpdateAllViews(pSender, hint.AsLPARAM(), pHint);
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (pMainFrm) pMainFrm->UpdateTree(this, hint, pHint);
if(hint.GetType()[HINT_MODCHANNELS | HINT_MODTYPE])
{
auto instance = CChannelManagerDlg::sharedInstance();
if(instance != nullptr && pHint != instance && instance->GetDocument() == this)
instance->Update(hint, pHint);
}
#ifndef NO_PLUGINS
if(hint.GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES])
{
for(auto &plug : m_SndFile.m_MixPlugins)
{
auto mixPlug = plug.pMixPlugin;
if(mixPlug != nullptr && mixPlug->GetEditor())
{
mixPlug->GetEditor()->UpdateView(hint);
}
}
}
#endif
}
void CModDoc::UpdateAllViews(UpdateHint hint)
{
CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_UPDATEVIEWS, reinterpret_cast<WPARAM>(this), hint.AsLPARAM());
}
/////////////////////////////////////////////////////////////////////////////
// CModDoc commands
void CModDoc::OnFileWaveConvert()
{
OnFileWaveConvert(ORDERINDEX_INVALID, ORDERINDEX_INVALID);
}
void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories)
{
ASSERT(!encFactories.empty());
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if ((!pMainFrm) || (!m_SndFile.GetType()) || encFactories.empty()) return;
CWaveConvert wsdlg(pMainFrm, nMinOrder, nMaxOrder, m_SndFile.Order().GetLengthTailTrimmed() - 1, m_SndFile, encFactories);
{
BypassInputHandler bih;
if (wsdlg.DoModal() != IDOK) return;
}
EncoderFactoryBase *encFactory = wsdlg.m_Settings.GetEncoderFactory();
const mpt::PathString extension = encFactory->GetTraits().fileExtension;
FileDialog dlg = SaveFileDialog()
.DefaultExtension(extension)
.DefaultFilename(GetPathNameMpt().GetFileName() + P_(".") + extension)
.ExtensionFilter(encFactory->GetTraits().fileDescription + U_(" (*.") + extension.ToUnicode() + U_(")|*.") + extension.ToUnicode() + U_("||"))
.WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir());
if(!wsdlg.m_Settings.outputToSample && !dlg.Show()) return;
// will set default dir here because there's no setup option for export dir yet (feel free to add one...)
TrackerSettings::Instance().PathExport.SetDefaultDir(dlg.GetWorkingDirectory(), true);
mpt::PathString drive, dir, name, ext;
dlg.GetFirstFile().SplitPath(&drive, &dir, &name, &ext);
const mpt::PathString fileName = drive + dir + name;
const mpt::PathString fileExt = ext;
const ORDERINDEX currentOrd = m_SndFile.m_PlayState.m_nCurrentOrder;
const ROWINDEX currentRow = m_SndFile.m_PlayState.m_nRow;
int nRenderPasses = 1;
// Channel mode
std::vector<bool> usedChannels;
std::vector<FlagSet<ChannelFlags>> channelFlags;
// Instrument mode
std::vector<bool> instrMuteState;
// CHN_SYNCMUTE is used with formats where CHN_MUTE would stop processing global effects and could thus mess synchronization between exported channels
const ChannelFlags muteFlag = m_SndFile.m_playBehaviour[kST3NoMutedChannels] ? CHN_SYNCMUTE : CHN_MUTE;
// Channel mode: save song in multiple wav files (one for each enabled channels)
if(wsdlg.m_bChannelMode)
{
// Don't save empty channels
CheckUsedChannels(usedChannels);
nRenderPasses = m_SndFile.GetNumChannels();
channelFlags.resize(nRenderPasses, ChannelFlags(0));
for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
{
// Save channels' flags
channelFlags[i] = m_SndFile.ChnSettings[i].dwFlags;
// Ignore muted channels
if(channelFlags[i][CHN_MUTE]) usedChannels[i] = false;
// Mute each channel
m_SndFile.ChnSettings[i].dwFlags.set(muteFlag);
}
}
// Instrument mode: Same as channel mode, but renders per instrument (or sample)
if(wsdlg.m_bInstrumentMode)
{
if(m_SndFile.GetNumInstruments() == 0)
{
nRenderPasses = m_SndFile.GetNumSamples();
instrMuteState.resize(nRenderPasses, false);
for(SAMPLEINDEX i = 0; i < m_SndFile.GetNumSamples(); i++)
{
instrMuteState[i] = IsSampleMuted(i + 1);
MuteSample(i + 1, true);
}
} else
{
nRenderPasses = m_SndFile.GetNumInstruments();
instrMuteState.resize(nRenderPasses, false);
for(INSTRUMENTINDEX i = 0; i < m_SndFile.GetNumInstruments(); i++)
{
instrMuteState[i] = IsInstrumentMuted(i + 1);
MuteInstrument(i + 1, true);
}
}
}
pMainFrm->PauseMod(this);
int oldRepeat = m_SndFile.GetRepeatCount();
const SEQUENCEINDEX currentSeq = m_SndFile.Order.GetCurrentSequenceIndex();
for(SEQUENCEINDEX seq = wsdlg.m_Settings.minSequence; seq <= wsdlg.m_Settings.maxSequence; seq++)
{
m_SndFile.Order.SetSequence(seq);
mpt::ustring fileNameAdd;
for(int i = 0; i < nRenderPasses; i++)
{
mpt::PathString thisName = fileName;
CString caption = _T("file");
fileNameAdd.clear();
if(wsdlg.m_Settings.minSequence != wsdlg.m_Settings.maxSequence)
{
fileNameAdd = MPT_UFORMAT("-{}")(mpt::ufmt::dec0<2>(seq + 1));
mpt::ustring seqName = m_SndFile.Order(seq).GetName();
if(!seqName.empty())
{
fileNameAdd += UL_("-") + seqName;
}
}
// Channel mode
if(wsdlg.m_bChannelMode)
{
// Re-mute previously processed channel
if(i > 0)
m_SndFile.ChnSettings[i - 1].dwFlags.set(muteFlag);
// Was this channel actually muted? Don't process it then.
if(!usedChannels[i])
continue;
// Add channel number & name (if available) to path string
if(!m_SndFile.ChnSettings[i].szName.empty())
{
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName));
} else
{
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
caption = MPT_CFORMAT("channel {}")(i + 1);
}
// Unmute channel to process
m_SndFile.ChnSettings[i].dwFlags.reset(muteFlag);
}
// Instrument mode
if(wsdlg.m_bInstrumentMode)
{
if(m_SndFile.GetNumInstruments() == 0)
{
// Re-mute previously processed sample
if(i > 0) MuteSample(static_cast<SAMPLEINDEX>(i), true);
if(!m_SndFile.GetSample(static_cast<SAMPLEINDEX>(i + 1)).HasSampleData() || !IsSampleUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
continue;
// Add sample number & name (if available) to path string
if(!m_SndFile.m_szNames[i + 1].empty())
{
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1]));
} else
{
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
caption = MPT_CFORMAT("sample {}")(i + 1);
}
// Unmute sample to process
MuteSample(static_cast<SAMPLEINDEX>(i + 1), false);
} else
{
// Re-mute previously processed instrument
if(i > 0) MuteInstrument(static_cast<INSTRUMENTINDEX>(i), true);
if(m_SndFile.Instruments[i + 1] == nullptr || !IsInstrumentUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i])
continue;
if(!m_SndFile.Instruments[i + 1]->name.empty())
{
fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name));
} else
{
fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1));
caption = MPT_CFORMAT("instrument {}")(i + 1);
}
// Unmute instrument to process
MuteInstrument(static_cast<SAMPLEINDEX>(i + 1), false);
}
}
if(!fileNameAdd.empty())
{
SanitizeFilename(fileNameAdd);
thisName += mpt::PathString::FromUnicode(fileNameAdd);
}
thisName += fileExt;
if(wsdlg.m_Settings.outputToSample)
{
thisName = mpt::CreateTempFileName(P_("OpenMPT"));
// Ensure this temporary file is marked as temporary in the file system, to increase the chance it will never be written to disk
HANDLE hFile = ::CreateFile(thisName.AsNative().c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL);
if(hFile != INVALID_HANDLE_VALUE)
{
::CloseHandle(hFile);
}
}
// Render song (or current channel, or current sample/instrument)
bool cancel = true;
try
{
mpt::SafeOutputFile safeFileStream(thisName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
mpt::ofstream &f = safeFileStream;
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
if(!f)
{
Reporting::Error("Could not open file for writing. Is it open in another application?");
} else
{
BypassInputHandler bih;
CDoWaveConvert dwcdlg(m_SndFile, f, caption, wsdlg.m_Settings, pMainFrm);
dwcdlg.m_bGivePlugsIdleTime = wsdlg.m_bGivePlugsIdleTime;
dwcdlg.m_dwSongLimit = wsdlg.m_dwSongLimit;
cancel = dwcdlg.DoModal() != IDOK;
}
} catch(const std::exception &)
{
Reporting::Error(_T("Error while writing file!"));
}
if(wsdlg.m_Settings.outputToSample)
{
if(!cancel)
{
InputFile f(thisName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading);
if(f.IsValid())
{
FileReader file = GetFileReader(f);
SAMPLEINDEX smp = wsdlg.m_Settings.sampleSlot;
if(smp == 0 || smp > GetNumSamples()) smp = m_SndFile.GetNextFreeSample();
if(smp == SAMPLEINDEX_INVALID)
{
Reporting::Error(_T("Too many samples!"));
cancel = true;
}
if(!cancel)
{
if(GetNumSamples() < smp) m_SndFile.m_nSamples = smp;
GetSampleUndo().PrepareUndo(smp, sundo_replace, "Render To Sample");
if(m_SndFile.ReadSampleFromFile(smp, file, false))
{
m_SndFile.m_szNames[smp] = "Render To Sample" + mpt::ToCharset(m_SndFile.GetCharsetInternal(), fileNameAdd);
UpdateAllViews(nullptr, SampleHint().Info().Data().Names());
if(m_SndFile.GetNumInstruments() && !IsSampleUsed(smp))
{
// Insert new instrument for the generated sample in case it is not referenced by any instruments yet.
// It should only be already referenced if the user chose to export to an existing sample slot.
InsertInstrument(smp);
UpdateAllViews(nullptr, InstrumentHint().Info().Names());
}
SetModified();
} else
{
GetSampleUndo().RemoveLastUndoStep(smp);
}
}
}
}
// Always clean up after ourselves
for(int retry = 0; retry < 10; retry++)
{
// stupid virus scanners
if(DeleteFile(thisName.AsNative().c_str()) != EACCES)
{
break;
}
Sleep(10);
}
}
if(cancel) break;
}
}
// Restore channels' flags
if(wsdlg.m_bChannelMode)
{
for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++)
{
m_SndFile.ChnSettings[i].dwFlags = channelFlags[i];
}
}
// Restore instruments' / samples' flags
if(wsdlg.m_bInstrumentMode)
{
for(size_t i = 0; i < instrMuteState.size(); i++)
{
if(m_SndFile.GetNumInstruments() == 0)
MuteSample(static_cast<SAMPLEINDEX>(i + 1), instrMuteState[i]);
else
MuteInstrument(static_cast<INSTRUMENTINDEX>(i + 1), instrMuteState[i]);
}
}
m_SndFile.Order.SetSequence(currentSeq);
m_SndFile.SetRepeatCount(oldRepeat);
m_SndFile.GetLength(eAdjust, GetLengthTarget(currentOrd, currentRow));
m_SndFile.m_PlayState.m_nNextOrder = currentOrd;
m_SndFile.m_PlayState.m_nNextRow = currentRow;
CMainFrame::UpdateAudioParameters(m_SndFile, true);
}
void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder)
{
WAVEncoder wavencoder;
FLACEncoder flacencoder;
AUEncoder auencoder;
OggOpusEncoder opusencoder;
VorbisEncoder vorbisencoder;
MP3Encoder mp3lame(MP3EncoderLame);
MP3Encoder mp3lamecompatible(MP3EncoderLameCompatible);
RAWEncoder rawencoder;
std::vector<EncoderFactoryBase*> encoders;
if(wavencoder.IsAvailable()) encoders.push_back(&wavencoder);
if(flacencoder.IsAvailable()) encoders.push_back(&flacencoder);
if(auencoder.IsAvailable()) encoders.push_back(&auencoder);
if(rawencoder.IsAvailable()) encoders.push_back(&rawencoder);
if(opusencoder.IsAvailable()) encoders.push_back(&opusencoder);
if(vorbisencoder.IsAvailable()) encoders.push_back(&vorbisencoder);
if(mp3lame.IsAvailable())
{
encoders.push_back(&mp3lame);
}
if(mp3lamecompatible.IsAvailable()) encoders.push_back(&mp3lamecompatible);
OnFileWaveConvert(nMinOrder, nMaxOrder, encoders);
}
void CModDoc::OnFileMidiConvert()
{
#ifndef NO_PLUGINS
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if ((!pMainFrm) || (!m_SndFile.GetType())) return;
mpt::PathString filename = GetPathNameMpt().ReplaceExt(P_(".mid"));
FileDialog dlg = SaveFileDialog()
.DefaultExtension("mid")
.DefaultFilename(filename)
.ExtensionFilter("MIDI Files (*.mid)|*.mid||");
if(!dlg.Show()) return;
CModToMidi mididlg(m_SndFile, pMainFrm);
BypassInputHandler bih;
if(mididlg.DoModal() == IDOK)
{
try
{
mpt::SafeOutputFile sf(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
mpt::ofstream &f = sf;
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
if(!f.good())
{
Reporting::Error("Could not open file for writing. Is it open in another application?");
return;
}
CDoMidiConvert doconv(m_SndFile, f, mididlg.m_instrMap);
doconv.DoModal();
} catch(const std::exception &)
{
Reporting::Error(_T("Error while writing file!"));
}
}
#else
Reporting::Error("In order to use MIDI export, OpenMPT must be built with plugin support.");
#endif // NO_PLUGINS
}
//HACK: This is a quick fix. Needs to be better integrated into player and GUI.
void CModDoc::OnFileCompatibilitySave()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (!pMainFrm) return;
CString pattern;
const MODTYPE type = m_SndFile.GetType();
switch(type)
{
case MOD_TYPE_IT:
pattern = FileFilterIT;
MsgBoxHidable(CompatExportDefaultWarning);
break;
case MOD_TYPE_XM:
pattern = FileFilterXM;
MsgBoxHidable(CompatExportDefaultWarning);
break;
default:
// Not available for this format.
return;
}
const std::string ext = m_SndFile.GetModSpecifications().fileExtension;
mpt::PathString filename;
{
mpt::PathString drive;
mpt::PathString dir;
mpt::PathString fileName;
GetPathNameMpt().SplitPath(&drive, &dir, &fileName, nullptr);
filename = drive;
filename += dir;
filename += fileName;
if(!strstr(fileName.ToUTF8().c_str(), "compat"))
filename += P_(".compat.");
else
filename += P_(".");
filename += mpt::PathString::FromUTF8(ext);
}
FileDialog dlg = SaveFileDialog()
.DefaultExtension(ext)
.DefaultFilename(filename)
.ExtensionFilter(pattern)
.WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir());
if(!dlg.Show()) return;
filename = dlg.GetFirstFile();
bool ok = false;
BeginWaitCursor();
try
{
mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave));
mpt::ofstream &f = sf;
if(f)
{
f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit);
ScopedLogCapturer logcapturer(*this);
FixNullStrings();
switch(type)
{
case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f, true); break;
case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename, true); break;
default: MPT_ASSERT_NOTREACHED();
}
}
} catch(const std::exception &)
{
ok = false;
}
EndWaitCursor();
if(!ok)
{
ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame());
}
}
void CModDoc::OnPlayerPlay()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (pMainFrm)
{
CChildFrame *pChildFrm = GetChildFrame();
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
{
//User has sent play song command: set loop pattern checkbox to false.
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
}
bool isPlaying = (pMainFrm->GetModPlaying() == this);
if(isPlaying && !m_SndFile.m_SongFlags[SONG_PAUSED | SONG_STEP/*|SONG_PATTERNLOOP*/])
{
OnPlayerPause();
return;
}
CriticalSection cs;
// Kill editor voices
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) if (m_SndFile.m_PlayState.Chn[i].isPreviewNote)
{
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
if (!isPlaying) m_SndFile.m_PlayState.Chn[i].nLength = 0;
}
m_SndFile.m_PlayState.m_bPositionChanged = true;
if(isPlaying)
{
m_SndFile.StopAllVsti();
}
cs.Leave();
m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PAUSED | SONG_PATTERNLOOP);
pMainFrm->PlayMod(this);
}
}
void CModDoc::OnPlayerPause()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (pMainFrm)
{
if (pMainFrm->GetModPlaying() == this)
{
bool isLooping = m_SndFile.m_SongFlags[SONG_PATTERNLOOP];
PATTERNINDEX nPat = m_SndFile.m_PlayState.m_nPattern;
ROWINDEX nRow = m_SndFile.m_PlayState.m_nRow;
ROWINDEX nNextRow = m_SndFile.m_PlayState.m_nNextRow;
pMainFrm->PauseMod();
if ((isLooping) && (nPat < m_SndFile.Patterns.Size()))
{
CriticalSection cs;
if ((m_SndFile.m_PlayState.m_nCurrentOrder < m_SndFile.Order().GetLength()) && (m_SndFile.Order()[m_SndFile.m_PlayState.m_nCurrentOrder] == nPat))
{
m_SndFile.m_PlayState.m_nNextOrder = m_SndFile.m_PlayState.m_nCurrentOrder;
m_SndFile.m_PlayState.m_nNextRow = nNextRow;
m_SndFile.m_PlayState.m_nRow = nRow;
} else
{
for (ORDERINDEX nOrd = 0; nOrd < m_SndFile.Order().GetLength(); nOrd++)
{
if (m_SndFile.Order()[nOrd] == m_SndFile.Order.GetInvalidPatIndex()) break;
if (m_SndFile.Order()[nOrd] == nPat)
{
m_SndFile.m_PlayState.m_nCurrentOrder = nOrd;
m_SndFile.m_PlayState.m_nNextOrder = nOrd;
m_SndFile.m_PlayState.m_nNextRow = nNextRow;
m_SndFile.m_PlayState.m_nRow = nRow;
break;
}
}
}
}
} else
{
pMainFrm->PauseMod();
}
}
}
void CModDoc::OnPlayerStop()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (pMainFrm) pMainFrm->StopMod();
}
void CModDoc::OnPlayerPlayFromStart()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (pMainFrm)
{
CChildFrame *pChildFrm = GetChildFrame();
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
{
//User has sent play song command: set loop pattern checkbox to false.
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
}
pMainFrm->PauseMod();
CriticalSection cs;
m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PATTERNLOOP);
m_SndFile.ResetPlayPos();
//m_SndFile.visitedSongRows.Initialize(true);
m_SndFile.m_PlayState.m_bPositionChanged = true;
cs.Leave();
pMainFrm->PlayMod(this);
}
}
void CModDoc::OnEditGlobals()
{
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_GLOBALS);
}
void CModDoc::OnEditPatterns()
{
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, -1);
}
void CModDoc::OnEditSamples()
{
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, -1);
}
void CModDoc::OnEditInstruments()
{
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, -1);
}
void CModDoc::OnEditComments()
{
SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_COMMENTS);
}
void CModDoc::OnShowCleanup()
{
CModCleanupDlg dlg(*this, CMainFrame::GetMainFrame());
dlg.DoModal();
}
void CModDoc::OnSetupZxxMacros()
{
CMidiMacroSetup dlg(m_SndFile);
if(dlg.DoModal() == IDOK)
{
if(m_SndFile.m_MidiCfg != dlg.m_MidiCfg)
{
m_SndFile.m_MidiCfg = dlg.m_MidiCfg;
SetModified();
}
}
}
// Enable menu item only module types that support MIDI Mappings
void CModDoc::OnUpdateHasMIDIMappings(CCmdUI *p)
{
if(p)
p->Enable((m_SndFile.GetModSpecifications().MIDIMappingDirectivesMax > 0) ? TRUE : FALSE);
}
// Enable menu item only for IT / MPTM / XM files
void CModDoc::OnUpdateXMITMPTOnly(CCmdUI *p)
{
if (p)
p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE);
}
// Enable menu item only for IT / MPTM files
void CModDoc::OnUpdateHasEditHistory(CCmdUI *p)
{
if (p)
p->Enable(((m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_SndFile.GetFileHistory().empty()) ? TRUE : FALSE);
}
// Enable menu item if current module type supports compatibility export
void CModDoc::OnUpdateCompatExportableOnly(CCmdUI *p)
{
if(p)
p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT)) ? TRUE : FALSE);
}
static CString FormatSongLength(double length)
{
length = mpt::round(length);
double minutes = std::floor(length / 60.0), seconds = std::fmod(length, 60.0);
CString s;
s.Format(_T("%.0fmn%02.0fs"), minutes, seconds);
return s;
}
void CModDoc::OnEstimateSongLength()
{
CString s = _T("Approximate song length: ");
const auto subSongs = m_SndFile.GetAllSubSongs();
if (subSongs.empty())
{
Reporting::Information(_T("No patterns found!"));
return;
}
std::vector<uint32> songsPerSequence(m_SndFile.Order.GetNumSequences(), 0);
SEQUENCEINDEX prevSeq = subSongs[0].sequence;
for(const auto &song : subSongs)
{
songsPerSequence[song.sequence]++;
if(prevSeq != song.sequence)
prevSeq = SEQUENCEINDEX_INVALID;
}
double totalLength = 0.0;
uint32 songCount = 0;
// If there are multiple sequences, indent their subsongs
const TCHAR *indent = (prevSeq == SEQUENCEINDEX_INVALID) ? _T("\t") : _T("");
for(const auto &song : subSongs)
{
double songLength = song.duration;
if(subSongs.size() > 1)
{
totalLength += songLength;
if(prevSeq != song.sequence)
{
songCount = 0;
prevSeq = song.sequence;
if(m_SndFile.Order(prevSeq).GetName().empty())
s.AppendFormat(_T("\nSequence %u:"), prevSeq + 1u);
else
s.AppendFormat(_T("\nSequence %u (%s):"), prevSeq + 1u, mpt::ToWin(m_SndFile.Order(prevSeq).GetName()).c_str());
}
songCount++;
if(songsPerSequence[song.sequence] > 1)
s.AppendFormat(_T("\n%sSong %u, starting at order %u:\t"), indent, songCount, song.startOrder);
else
s.AppendChar(_T('\t'));
}
if(songLength != std::numeric_limits<double>::infinity())
{
songLength = mpt::round(songLength);
s += FormatSongLength(songLength);
} else
{
s += _T("Song too long!");
}
}
if(subSongs.size() > 1 && totalLength != std::numeric_limits<double>::infinity())
{
s += _T("\n\nTotal length:\t") + FormatSongLength(totalLength);
}
Reporting::Information(s);
}
void CModDoc::OnApproximateBPM()
{
if(CMainFrame::GetMainFrame()->GetModPlaying() != this)
{
m_SndFile.m_PlayState.m_nCurrentRowsPerBeat = m_SndFile.m_nDefaultRowsPerBeat;
m_SndFile.m_PlayState.m_nCurrentRowsPerMeasure = m_SndFile.m_nDefaultRowsPerMeasure;
}
m_SndFile.RecalculateSamplesPerTick();
const double bpm = m_SndFile.GetCurrentBPM();
CString s;
switch(m_SndFile.m_nTempoMode)
{
case TempoMode::Alternative:
s.Format(_T("Using alternative tempo interpretation.\n\nAssuming:\n. %.8g ticks per second\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
break;
case TempoMode::Modern:
s.Format(_T("Using modern tempo interpretation.\n\nThe tempo is: %.8g BPM"), bpm);
break;
case TempoMode::Classic:
default:
s.Format(_T("Using standard tempo interpretation.\n\nAssuming:\n. A mod tempo (tick duration factor) of %.8g\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"),
m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm);
break;
}
Reporting::Information(s);
}
CChildFrame *CModDoc::GetChildFrame()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if (!pMainFrm) return nullptr;
CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive();
if (pMDIActive)
{
CView *pView = pMDIActive->GetActiveView();
if ((pView) && (pView->GetDocument() == this))
return static_cast<CChildFrame *>(pMDIActive);
}
POSITION pos = GetFirstViewPosition();
while (pos != NULL)
{
CView *pView = GetNextView(pos);
if ((pView) && (pView->GetDocument() == this))
return static_cast<CChildFrame *>(pView->GetParentFrame());
}
return nullptr;
}
// Get the currently edited pattern position. Note that ord might be ORDERINDEX_INVALID when editing a pattern that is not present in the order list.
void CModDoc::GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord)
{
CChildFrame *pChildFrm = GetChildFrame();
if(strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) // dirty HACK
{
PATTERNVIEWSTATE patternViewState;
pChildFrm->SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)(&patternViewState));
pat = patternViewState.nPattern;
row = patternViewState.cursor.GetRow();
ord = patternViewState.nOrder;
} else
{
//patern editor object does not exist (i.e. is not active) - use saved state.
PATTERNVIEWSTATE &patternViewState = pChildFrm->GetPatternViewState();
pat = patternViewState.nPattern;
row = patternViewState.cursor.GetRow();
ord = patternViewState.nOrder;
}
const auto &order = m_SndFile.Order();
if(order.empty())
{
ord = ORDERINDEX_INVALID;
pat = 0;
row = 0;
} else if(ord >= order.size())
{
ord = 0;
pat = m_SndFile.Order()[ord];
}
if(!m_SndFile.Patterns.IsValidPat(pat))
{
pat = 0;
row = 0;
} else if(row >= m_SndFile.Patterns[pat].GetNumRows())
{
row = 0;
}
//ensure order correlates with pattern.
if(ord >= order.size() || order[ord] != pat)
{
ord = order.FindOrder(pat);
}
}
////////////////////////////////////////////////////////////////////////////////////////
// Playback
void CModDoc::OnPatternRestart(bool loop)
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CChildFrame *pChildFrm = GetChildFrame();
if ((pMainFrm) && (pChildFrm))
{
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
{
//User has sent play pattern command: set loop pattern checkbox to true.
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, loop ? 1 : 0);
}
ROWINDEX nRow;
PATTERNINDEX nPat;
ORDERINDEX nOrd;
GetEditPosition(nRow, nPat, nOrd);
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
CriticalSection cs;
// Cut instruments/samples
for(auto &chn : m_SndFile.m_PlayState.Chn)
{
chn.nPatternLoopCount = 0;
chn.nPatternLoop = 0;
chn.nFadeOutVol = 0;
chn.dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
}
if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
if(loop)
m_SndFile.LoopPattern(nPat);
else
m_SndFile.LoopPattern(PATTERNINDEX_INVALID);
// set playback timer in the status bar (and update channel status)
SetElapsedTime(nOrd, 0, true);
if(pModPlaying == this)
{
m_SndFile.StopAllVsti();
}
cs.Leave();
if(pModPlaying != this)
{
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
SetFollowWnd(pChildFrm->GetHwndView());
pMainFrm->PlayMod(this); //rewbs.fix2977
}
}
//SwitchToView();
}
void CModDoc::OnPatternPlay()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CChildFrame *pChildFrm = GetChildFrame();
if ((pMainFrm) && (pChildFrm))
{
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
{
//User has sent play pattern command: set loop pattern checkbox to true.
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 1);
}
ROWINDEX nRow;
PATTERNINDEX nPat;
ORDERINDEX nOrd;
GetEditPosition(nRow, nPat, nOrd);
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
CriticalSection cs;
// Cut instruments/samples
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
{
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
}
if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
m_SndFile.LoopPattern(nPat);
// set playback timer in the status bar (and update channel status)
SetElapsedTime(nOrd, nRow, true);
if(pModPlaying == this)
{
m_SndFile.StopAllVsti();
}
cs.Leave();
if(pModPlaying != this)
{
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
SetFollowWnd(pChildFrm->GetHwndView());
pMainFrm->PlayMod(this); //rewbs.fix2977
}
}
//SwitchToView();
}
void CModDoc::OnPatternPlayNoLoop()
{
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
CChildFrame *pChildFrm = GetChildFrame();
if ((pMainFrm) && (pChildFrm))
{
if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0)
{
//User has sent play song command: set loop pattern checkbox to false.
pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0);
}
ROWINDEX nRow;
PATTERNINDEX nPat;
ORDERINDEX nOrd;
GetEditPosition(nRow, nPat, nOrd);
CModDoc *pModPlaying = pMainFrm->GetModPlaying();
CriticalSection cs;
// Cut instruments/samples
for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++)
{
m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF);
}
m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP);
m_SndFile.SetCurrentOrder(nOrd);
if(nOrd < m_SndFile.Order().size() && m_SndFile.Order()[nOrd] == nPat)
m_SndFile.DontLoopPattern(nPat, nRow);
else
m_SndFile.LoopPattern(nPat);
// set playback timer in the status bar (and update channel status)
SetElapsedTime(nOrd, nRow, true);
if(pModPlaying == this)
{
m_SndFile.StopAllVsti();
}
cs.Leave();
if(pModPlaying != this)
{
SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem);
SetFollowWnd(pChildFrm->GetHwndView());
pMainFrm->PlayMod(this); //rewbs.fix2977
}
}
//SwitchToView();
}
void CModDoc::OnViewEditHistory()
{
CEditHistoryDlg dlg(CMainFrame::GetMainFrame(), *this);
dlg.DoModal();
}
void CModDoc::OnViewMPTHacks()
{
ScopedLogCapturer logcapturer(*this);
if(!HasMPTHacks())
{
AddToLog("No hacks found.");
}
}
void CModDoc::OnViewTempoSwingSettings()
{
if(m_SndFile.m_nDefaultRowsPerBeat > 0 && m_SndFile.m_nTempoMode == TempoMode::Modern)
{
TempoSwing tempoSwing = m_SndFile.m_tempoSwing;
tempoSwing.resize(m_SndFile.m_nDefaultRowsPerBeat, TempoSwing::Unity);
CTempoSwingDlg dlg(CMainFrame::GetMainFrame(), tempoSwing, m_SndFile);
if(dlg.DoModal() == IDOK)
{
SetModified();
m_SndFile.m_tempoSwing = dlg.m_tempoSwing;
}
} else if(GetModType() == MOD_TYPE_MPT)
{
Reporting::Error(_T("Modern tempo mode needs to be enabled in order to edit tempo swing settings."));
OnSongProperties();
}
}
LRESULT CModDoc::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/)
{
const auto &modSpecs = m_SndFile.GetModSpecifications();
switch(wParam)
{
case kcViewGeneral: OnEditGlobals(); break;
case kcViewPattern: OnEditPatterns(); break;
case kcViewSamples: OnEditSamples(); break;
case kcViewInstruments: OnEditInstruments(); break;
case kcViewComments: OnEditComments(); break;
case kcViewSongProperties: OnSongProperties(); break;
case kcViewTempoSwing: OnViewTempoSwingSettings(); break;
case kcShowMacroConfig: OnSetupZxxMacros(); break;
case kcViewMIDImapping: OnViewMIDIMapping(); break;
case kcViewEditHistory: OnViewEditHistory(); break;
case kcViewChannelManager: OnChannelManager(); break;
case kcFileSaveAsWave: OnFileWaveConvert(); break;
case kcFileSaveMidi: OnFileMidiConvert(); break;
case kcFileSaveOPL: OnFileOPLExport(); break;
case kcFileExportCompat: OnFileCompatibilitySave(); break;
case kcEstimateSongLength: OnEstimateSongLength(); break;
case kcApproxRealBPM: OnApproximateBPM(); break;
case kcFileSave: DoSave(GetPathNameMpt()); break;
case kcFileSaveAs: DoSave(mpt::PathString()); break;
case kcFileSaveCopy: OnSaveCopy(); break;
case kcFileSaveTemplate: OnSaveTemplateModule(); break;
case kcFileClose: SafeFileClose(); break;
case kcFileAppend: OnAppendModule(); break;
case kcPlayPatternFromCursor: OnPatternPlay(); break;
case kcPlayPatternFromStart: OnPatternRestart(); break;
case kcPlaySongFromCursor: OnPatternPlayNoLoop(); break;
case kcPlaySongFromStart: OnPlayerPlayFromStart(); break;
case kcPlayPauseSong: OnPlayerPlay(); break;
case kcPlaySongFromPattern: OnPatternRestart(false); break;
case kcStopSong: OnPlayerStop(); break;
case kcPanic: OnPanic(); break;
case kcToggleLoopSong: SetLoopSong(!TrackerSettings::Instance().gbLoopSong); break;
case kcTempoIncreaseFine:
if(!modSpecs.hasFractionalTempo)
break;
[[fallthrough]];
case kcTempoIncrease:
if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo < modSpecs.GetTempoMax())
m_SndFile.m_PlayState.m_nMusicTempo = std::min(modSpecs.GetTempoMax(), tempo + TEMPO(wParam == kcTempoIncrease ? 1.0 : 0.1));
break;
case kcTempoDecreaseFine:
if(!modSpecs.hasFractionalTempo)
break;
[[fallthrough]];
case kcTempoDecrease:
if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo > modSpecs.GetTempoMin())
m_SndFile.m_PlayState.m_nMusicTempo = std::max(modSpecs.GetTempoMin(), tempo - TEMPO(wParam == kcTempoDecrease ? 1.0 : 0.1));
break;
case kcSpeedIncrease:
if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed < modSpecs.speedMax)
m_SndFile.m_PlayState.m_nMusicSpeed = speed + 1;
break;
case kcSpeedDecrease:
if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed > modSpecs.speedMin)
m_SndFile.m_PlayState.m_nMusicSpeed = speed - 1;
break;
case kcViewToggle:
if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr)
lastActiveFrame->ToggleViews();
break;
default: return kcNull;
}
return wParam;
}
void CModDoc::TogglePluginEditor(UINT plugin, bool onlyThisEditor)
{
if(plugin < MAX_MIXPLUGINS)
{
IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plugin].pMixPlugin;
if(pPlugin != nullptr)
{
if(onlyThisEditor)
{
int32 posX = int32_min, posY = int32_min;
for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
{
SNDMIXPLUGIN &otherPlug = m_SndFile.m_MixPlugins[i];
if(i != plugin && otherPlug.pMixPlugin != nullptr && otherPlug.pMixPlugin->GetEditor() != nullptr)
{
otherPlug.pMixPlugin->CloseEditor();
if(otherPlug.editorX != int32_min)
{
posX = otherPlug.editorX;
posY = otherPlug.editorY;
}
}
}
if(posX != int32_min)
{
m_SndFile.m_MixPlugins[plugin].editorX = posX;
m_SndFile.m_MixPlugins[plugin].editorY = posY;
}
}
pPlugin->ToggleEditor();
}
}
}
void CModDoc::SetLoopSong(bool loop)
{
TrackerSettings::Instance().gbLoopSong = loop;
m_SndFile.SetRepeatCount(loop ? -1 : 0);
CMainFrame::GetMainFrame()->UpdateAllViews(UpdateHint().MPTOptions());
}
void CModDoc::ChangeFileExtension(MODTYPE nNewType)
{
//Not making path if path is empty(case only(?) for new file)
if(!GetPathNameMpt().empty())
{
mpt::PathString drive;
mpt::PathString dir;
mpt::PathString fname;
mpt::PathString fext;
GetPathNameMpt().SplitPath(&drive, &dir, &fname, &fext);
mpt::PathString newPath = drive + dir;
// Catch case where we don't have a filename yet.
if(fname.empty())
{
newPath += mpt::PathString::FromCString(GetTitle()).SanitizeComponent();
} else
{
newPath += fname;
}
newPath += P_(".") + mpt::PathString::FromUTF8(CSoundFile::GetModSpecifications(nNewType).fileExtension);
// Forcing save dialog to appear after extension change - otherwise unnotified file overwriting may occur.
m_ShowSavedialog = true;
SetPathName(newPath, FALSE);
}
UpdateAllViews(NULL, UpdateHint().ModType());
}
CHANNELINDEX CModDoc::FindAvailableChannel() const
{
CHANNELINDEX chn = m_SndFile.GetNNAChannel(CHANNELINDEX_INVALID);
if(chn != CHANNELINDEX_INVALID)
return chn;
else
return GetNumChannels();
}
void CModDoc::RecordParamChange(PLUGINDEX plugSlot, PlugParamIndex paramIndex)
{
::SendNotifyMessage(m_hWndFollow, WM_MOD_RECORDPARAM, plugSlot, paramIndex);
}
void CModDoc::LearnMacro(int macroToSet, PlugParamIndex paramToUse)
{
if(macroToSet < 0 || macroToSet > kSFxMacros)
{
return;
}
// If macro already exists for this param, inform user and return
if(auto macro = m_SndFile.m_MidiCfg.FindMacroForParam(paramToUse); macro >= 0)
{
CString message;
message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), macro);
Reporting::Information(message, _T("Macro exists for this parameter"));
return;
}
// Set new macro
if(paramToUse < 384)
{
m_SndFile.m_MidiCfg.CreateParameteredMacro(macroToSet, kSFxPlugParam, paramToUse);
} else
{
CString message;
message.Format(_T("Parameter %i beyond controllable range. Use Parameter Control Events to automate this parameter."), static_cast<int>(paramToUse));
Reporting::Information(message, _T("Macro not assigned for this parameter"));
return;
}
CString message;
message.Format(_T("Parameter %i can now be controlled with macro %X."), static_cast<int>(paramToUse), macroToSet);
Reporting::Information(message, _T("Macro assigned for this parameter"));
return;
}
void CModDoc::OnSongProperties()
{
const bool wasUsingFrequencies = m_SndFile.PeriodsAreFrequencies();
CModTypeDlg dlg(m_SndFile, CMainFrame::GetMainFrame());
if(dlg.DoModal() == IDOK)
{
UpdateAllViews(nullptr, GeneralHint().General());
ScopedLogCapturer logcapturer(*this, _T("Conversion Status"));
bool showLog = false;
if(dlg.m_nType != GetModType())
{
if(!ChangeModType(dlg.m_nType))
return;
showLog = true;
}
CHANNELINDEX newChannels = Clamp(dlg.m_nChannels, m_SndFile.GetModSpecifications().channelsMin, m_SndFile.GetModSpecifications().channelsMax);
if(newChannels != GetNumChannels())
{
const bool showCancelInRemoveDlg = m_SndFile.GetModSpecifications().channelsMax >= m_SndFile.GetNumChannels();
if(ChangeNumChannels(newChannels, showCancelInRemoveDlg))
showLog = true;
// Force update of pattern highlights / num channels
UpdateAllViews(nullptr, PatternHint().Data());
UpdateAllViews(nullptr, GeneralHint().Channels());
}
if(wasUsingFrequencies != m_SndFile.PeriodsAreFrequencies())
{
for(auto &chn : m_SndFile.m_PlayState.Chn)
{
chn.nPeriod = 0;
}
}
SetModified();
}
}
void CModDoc::ViewMIDIMapping(PLUGINDEX plugin, PlugParamIndex param)
{
CMIDIMappingDialog dlg(CMainFrame::GetMainFrame(), m_SndFile);
if(plugin != PLUGINDEX_INVALID)
{
dlg.m_Setting.SetPlugIndex(plugin + 1);
dlg.m_Setting.SetParamIndex(param);
}
dlg.DoModal();
}
void CModDoc::OnChannelManager()
{
CChannelManagerDlg *instance = CChannelManagerDlg::sharedInstanceCreate();
if(instance != nullptr)
{
if(instance->IsDisplayed())
instance->Hide();
else
{
instance->SetDocument(this);
instance->Show();
}
}
}
// Sets playback timer to playback time at given position.
// At the same time, the playback parameters (global volume, channel volume and stuff like that) are calculated for this position.
// Sample channels positions are only updated if setSamplePos is true *and* the user has chosen to update sample play positions on seek.
void CModDoc::SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos)
{
if(nOrd == ORDERINDEX_INVALID) return;
double t = m_SndFile.GetPlaybackTimeAt(nOrd, nRow, true, setSamplePos && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SYNCSAMPLEPOS) != 0);
if(t < 0)
{
// Position is never played regularly, but we may want to continue playing from here nevertheless.
m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd;
m_SndFile.m_PlayState.m_nRow = m_SndFile.m_PlayState.m_nNextRow = nRow;
}
CMainFrame *pMainFrm = CMainFrame::GetMainFrame();
if(pMainFrm != nullptr) pMainFrm->SetElapsedTime(std::max(0.0, t));
}
CString CModDoc::GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr,
bool bEmptyInsteadOfNoName /* = false*/,
bool bIncludeIndex /* = true*/) const
{
if(nInstr >= MAX_INSTRUMENTS || m_SndFile.GetNumInstruments() == 0 || m_SndFile.Instruments[nInstr] == nullptr)
return CString();
CString displayName, instrumentName, pluginName;
// Get instrument name.
instrumentName = mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetInstrumentName(nInstr));
// If instrument name is empty, use name of the sample mapped to C-5.
if (instrumentName.IsEmpty())
{
const SAMPLEINDEX nSmp = m_SndFile.Instruments[nInstr]->Keyboard[NOTE_MIDDLEC - 1];
if (nSmp <= m_SndFile.GetNumSamples() && m_SndFile.GetSample(nSmp).HasSampleData())
instrumentName = _T("s: ") + mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetSampleName(nSmp));
}
// Get plugin name.
const PLUGINDEX nPlug = m_SndFile.Instruments[nInstr]->nMixPlug;
if (nPlug > 0 && nPlug < MAX_MIXPLUGINS)
pluginName = mpt::ToCString(m_SndFile.m_MixPlugins[nPlug-1].GetName());
if (pluginName.IsEmpty())
{
if(bEmptyInsteadOfNoName && instrumentName.IsEmpty())
return TEXT("");
if(instrumentName.IsEmpty())
instrumentName = _T("(no name)");
if (bIncludeIndex)
displayName.Format(_T("%02d: %s"), nInstr, instrumentName.GetString());
else
displayName = instrumentName;
} else
{
if (bIncludeIndex)
displayName.Format(TEXT("%02d: %s (%s)"), nInstr, instrumentName.GetString(), pluginName.GetString());
else
displayName.Format(TEXT("%s (%s)"), instrumentName.GetString(), pluginName.GetString());
}
return displayName;
}
void CModDoc::SafeFileClose()
{
// Verify that the main window has the focus. This saves us a lot of trouble because active modal dialogs cannot know if their pSndFile pointers are still valid.
if(GetActiveWindow() == CMainFrame::GetMainFrame()->m_hWnd)
OnFileClose();
}
// "Panic button". This resets all VSTi, OPL and sample notes.
void CModDoc::OnPanic()
{
CriticalSection cs;
m_SndFile.ResetChannels();
m_SndFile.StopAllVsti();
}
// Before saving, make sure that every char after the terminating null char is also null.
// Else, garbage might end up in various text strings that wasn't supposed to be there.
void CModDoc::FixNullStrings()
{
// Macros
m_SndFile.m_MidiCfg.Sanitize();
}
void CModDoc::OnSaveCopy()
{
DoSave(mpt::PathString(), false);
}
void CModDoc::OnSaveTemplateModule()
{
// Create template folder if doesn't exist already.
const mpt::PathString templateFolder = TrackerSettings::Instance().PathUserTemplates.GetDefaultDir();
if (!templateFolder.IsDirectory())
{
if (!CreateDirectory(templateFolder.AsNative().c_str(), nullptr))
{
Reporting::Notification(MPT_CFORMAT("Error: Unable to create template folder '{}'")( templateFolder));
return;
}
}
// Generate file name candidate.
mpt::PathString sName;
for(size_t i = 0; i < 1000; ++i)
{
sName += P_("newTemplate") + mpt::PathString::FromUnicode(mpt::ufmt::val(i));
sName += P_(".") + mpt::PathString::FromUTF8(m_SndFile.GetModSpecifications().fileExtension);
if (!(templateFolder + sName).FileOrDirectoryExists())
break;
}
// Ask file name from user.
FileDialog dlg = SaveFileDialog()
.DefaultExtension(m_SndFile.GetModSpecifications().fileExtension)
.DefaultFilename(sName)
.ExtensionFilter(ModTypeToFilter(m_SndFile))
.WorkingDirectory(templateFolder);
if(!dlg.Show())
return;
if (OnSaveDocument(dlg.GetFirstFile(), false))
{
// Update template menu.
CMainFrame::GetMainFrame()->CreateTemplateModulesMenu();
}
}
// Create an undo point that stores undo data for all existing patterns
void CModDoc::PrepareUndoForAllPatterns(bool storeChannelInfo, const char *description)
{
bool linkUndo = false;
PATTERNINDEX lastPat = 0;
for(PATTERNINDEX pat = 0; pat < m_SndFile.Patterns.Size(); pat++)
{
if(m_SndFile.Patterns.IsValidPat(pat)) lastPat = pat;
}
for(PATTERNINDEX pat = 0; pat <= lastPat; pat++)
{
if(m_SndFile.Patterns.IsValidPat(pat))
{
GetPatternUndo().PrepareUndo(pat, 0, 0, GetNumChannels(), m_SndFile.Patterns[pat].GetNumRows(), description, linkUndo, storeChannelInfo && pat == lastPat);
linkUndo = true;
}
}
}
CString CModDoc::LinearToDecibels(double value, double valueAtZeroDB)
{
if (value == 0) return _T("-inf");
double changeFactor = value / valueAtZeroDB;
double dB = 20.0 * std::log10(changeFactor);
CString s = (dB >= 0) ? _T("+") : _T("");
s.AppendFormat(_T("%.2f dB"), dB);
return s;
}
CString CModDoc::PanningToString(int32 value, int32 valueAtCenter)
{
if(value == valueAtCenter)
return _T("Center");
CString s;
s.Format(_T("%i%% %s"), (std::abs(static_cast<int>(value) - valueAtCenter) * 100) / valueAtCenter, value < valueAtCenter ? _T("Left") : _T("Right"));
return s;
}
// Apply OPL patch changes to live playback
void CModDoc::UpdateOPLInstrument(SAMPLEINDEX smp)
{
const ModSample &sample = m_SndFile.GetSample(smp);
if(!sample.uFlags[CHN_ADLIB] || !m_SndFile.m_opl || CMainFrame::GetMainFrame()->GetModPlaying() != this)
return;
CriticalSection cs;
const auto &patch = sample.adlib;
for(CHANNELINDEX chn = 0; chn < MAX_CHANNELS; chn++)
{
const auto &c = m_SndFile.m_PlayState.Chn[chn];
if(c.pModSample == &sample && c.IsSamplePlaying())
{
m_SndFile.m_opl->Patch(chn, patch);
}
}
}
// Store all view positions t settings file
void CModDoc::SerializeViews() const
{
const mpt::PathString pathName = theApp.IsPortableMode() ? GetPathNameMpt().AbsolutePathToRelative(theApp.GetInstallPath()) : GetPathNameMpt();
if(pathName.empty())
{
return;
}
std::ostringstream f(std::ios::out | std::ios::binary);
CRect mdiRect;
::GetClientRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
const int width = mdiRect.Width();
const int height = mdiRect.Height();
const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);
// Document view positions and sizes
POSITION pos = GetFirstViewPosition();
while(pos != nullptr && !mdiRect.IsRectEmpty())
{
CModControlView *pView = dynamic_cast<CModControlView *>(GetNextView(pos));
if(pView)
{
CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame();
WINDOWPLACEMENT wnd;
wnd.length = sizeof(WINDOWPLACEMENT);
pChildFrm->GetWindowPlacement(&wnd);
const CRect rect = wnd.rcNormalPosition;
// Write size information
uint8 windowState = 0;
if(wnd.showCmd == SW_SHOWMAXIMIZED) windowState = 1;
else if(wnd.showCmd == SW_SHOWMINIMIZED) windowState = 2;
mpt::IO::WriteIntLE<uint8>(f, 0); // Window type
mpt::IO::WriteIntLE<uint8>(f, windowState);
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.left, 1 << 30, width));
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.top, 1 << 30, height));
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Width(), 1 << 30, width));
mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Height(), 1 << 30, height));
std::string s = pChildFrm->SerializeView();
mpt::IO::WriteVarInt(f, s.size());
f << s;
}
}
// Plugin window positions
for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++)
{
if(m_SndFile.m_MixPlugins[i].IsValidPlugin() && m_SndFile.m_MixPlugins[i].editorX != int32_min && cxScreen && cyScreen)
{
// Translate screen position into percentage (to make it independent of the actual screen resolution)
int32 editorX = Util::muldivr(m_SndFile.m_MixPlugins[i].editorX, 1 << 30, cxScreen);
int32 editorY = Util::muldivr(m_SndFile.m_MixPlugins[i].editorY, 1 << 30, cyScreen);
mpt::IO::WriteIntLE<uint8>(f, 1); // Window type
mpt::IO::WriteIntLE<uint8>(f, 0); // Version
mpt::IO::WriteVarInt(f, i);
mpt::IO::WriteIntLE<int32>(f, editorX);
mpt::IO::WriteIntLE<int32>(f, editorY);
}
}
SettingsContainer &settings = theApp.GetSongSettings();
const std::string s = f.str();
settings.Write(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode(), pathName);
settings.Write(U_("WindowSettings"), pathName.ToUnicode(), mpt::encode_hex(mpt::as_span(s)));
}
// Restore all view positions from settings file
void CModDoc::DeserializeViews()
{
mpt::PathString pathName = GetPathNameMpt();
if(pathName.empty()) return;
SettingsContainer &settings = theApp.GetSongSettings();
mpt::ustring s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
if(s.size() < 2)
{
// Try relative path
pathName = pathName.RelativePathToAbsolute(theApp.GetInstallPath());
s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode());
if(s.size() < 2)
{
// Try searching for filename instead of full path name
const mpt::ustring altName = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode());
s = settings.Read<mpt::ustring>(U_("WindowSettings"), altName);
if(s.size() < 2) return;
}
}
std::vector<std::byte> bytes = mpt::decode_hex(s);
FileReader file(mpt::as_span(bytes));
CRect mdiRect;
::GetWindowRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect);
const int width = mdiRect.Width();
const int height = mdiRect.Height();
const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN);
POSITION pos = GetFirstViewPosition();
CChildFrame *pChildFrm = nullptr;
if(pos != nullptr) pChildFrm = dynamic_cast<CChildFrame *>(GetNextView(pos)->GetParentFrame());
bool anyMaximized = false;
while(file.CanRead(1))
{
const uint8 windowType = file.ReadUint8();
if(windowType == 0)
{
// Document view positions and sizes
const uint8 windowState = file.ReadUint8();
CRect rect;
rect.left = Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
rect.top = Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
rect.right = rect.left + Util::muldivr(file.ReadInt32LE(), width, 1 << 30);
rect.bottom = rect.top + Util::muldivr(file.ReadInt32LE(), height, 1 << 30);
size_t dataSize;
file.ReadVarInt(dataSize);
FileReader data = file.ReadChunk(dataSize);
if(pChildFrm == nullptr)
{
CModDocTemplate *pTemplate = static_cast<CModDocTemplate *>(GetDocTemplate());
ASSERT_VALID(pTemplate);
pChildFrm = static_cast<CChildFrame *>(pTemplate->CreateNewFrame(this, nullptr));
if(pChildFrm != nullptr)
{
pTemplate->InitialUpdateFrame(pChildFrm, this);
}
}
if(pChildFrm != nullptr)
{
if(!mdiRect.IsRectEmpty())
{
WINDOWPLACEMENT wnd;
wnd.length = sizeof(wnd);
pChildFrm->GetWindowPlacement(&wnd);
wnd.showCmd = SW_SHOWNOACTIVATE;
if(windowState == 1 || anyMaximized)
{
// Once a window has been maximized, all following windows have to be marked as maximized as well.
wnd.showCmd = SW_MAXIMIZE;
anyMaximized = true;
} else if(windowState == 2)
{
wnd.showCmd = SW_MINIMIZE;
}
if(rect.left < width && rect.right > 0 && rect.top < height && rect.bottom > 0)
{
wnd.rcNormalPosition = CRect(rect.left, rect.top, rect.right, rect.bottom);
}
pChildFrm->SetWindowPlacement(&wnd);
}
pChildFrm->DeserializeView(data);
pChildFrm = nullptr;
}
} else if(windowType == 1)
{
if(file.ReadUint8() != 0)
break;
// Plugin window positions
PLUGINDEX plug = 0;
if(file.ReadVarInt(plug) && plug < MAX_MIXPLUGINS)
{
int32 editorX = file.ReadInt32LE();
int32 editorY = file.ReadInt32LE();
if(editorX != int32_min && editorY != int32_min)
{
m_SndFile.m_MixPlugins[plug].editorX = Util::muldivr(editorX, cxScreen, 1 << 30);
m_SndFile.m_MixPlugins[plug].editorY = Util::muldivr(editorY, cyScreen, 1 << 30);
}
}
} else
{
// Unknown type
break;
}
}
}
OPENMPT_NAMESPACE_END