diff options
Diffstat (limited to 'Src/external_dependencies/openmpt-trunk/mptrack')
284 files changed, 125323 insertions, 0 deletions
diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.cpp new file mode 100644 index 00000000..903729fe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.cpp @@ -0,0 +1,589 @@ +/* + * AboutDialog.cpp + * --------------- + * Purpose: About dialog with credits, system information and a fancy demo effect. + * 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 "resource.h" +#include "AboutDialog.h" +#include "Image.h" +#include "Mptrack.h" +#include "TrackerSettings.h" +#include "BuildVariants.h" +#include "../common/version.h" +#include "../misc/mptWine.h" + + +OPENMPT_NAMESPACE_BEGIN + + +CAboutDlg *CAboutDlg::instance = nullptr; + +BEGIN_MESSAGE_MAP(CRippleBitmap, CWnd) + ON_WM_PAINT() + ON_WM_ERASEBKGND() + ON_WM_MOUSEMOVE() + ON_WM_MOUSEHOVER() + ON_WM_MOUSELEAVE() +END_MESSAGE_MAP() + + +CRippleBitmap::CRippleBitmap() +{ + m_bitmapSrc = LoadPixelImage(GetResource(MAKEINTRESOURCE(IDB_MPTRACK), _T("PNG"))); + m_bitmapTarget = std::make_unique<RawGDIDIB>(m_bitmapSrc->Width(), m_bitmapSrc->Height()); + m_offset1.assign(m_bitmapSrc->Pixels().size(), 0); + m_offset2.assign(m_bitmapSrc->Pixels().size(), 0); + m_frontBuf = m_offset2.data(); + m_backBuf = m_offset1.data(); + + // Pre-fill first and last row of output bitmap, since those won't be touched. + const RawGDIDIB::Pixel *in1 = m_bitmapSrc->Pixels().data(), *in2 = m_bitmapSrc->Pixels().data() + (m_bitmapSrc->Height() - 1) * m_bitmapSrc->Width(); + RawGDIDIB::Pixel *out1 = m_bitmapTarget->Pixels().data(), *out2 = m_bitmapTarget->Pixels().data() + (m_bitmapSrc->Height() - 1) * m_bitmapSrc->Width(); + for(uint32 i = 0; i < m_bitmapSrc->Width(); i++) + { + *(out1++) = *(in1++); + *(out2++) = *(in2++); + } + + MemsetZero(m_bi); + m_bi.biSize = sizeof(BITMAPINFOHEADER); + m_bi.biWidth = m_bitmapSrc->Width(); + m_bi.biHeight = -(int32)m_bitmapSrc->Height(); + m_bi.biPlanes = 1; + m_bi.biBitCount = 32; + m_bi.biCompression = BI_RGB; + m_bi.biSizeImage = m_bitmapSrc->Width() * m_bitmapSrc->Height() * 4; +} + + +CRippleBitmap::~CRippleBitmap() +{ + if(!m_showMouse) + { + ShowCursor(TRUE); + } +} + + +void CRippleBitmap::OnMouseMove(UINT nFlags, CPoint point) +{ + + // Rate limit in order to avoid too may ripples. + DWORD now = timeGetTime(); + if(now - m_lastRipple < UPDATE_INTERVAL) + return; + m_lastRipple = now; + + // Initiate ripples at cursor location + point.x = Util::ScalePixelsInv(point.x, m_hWnd); + point.y = Util::ScalePixelsInv(point.y, m_hWnd); + Limit(point.x, 1, int(m_bitmapSrc->Width()) - 2); + Limit(point.y, 2, int(m_bitmapSrc->Height()) - 3); + int32 *p = m_backBuf + point.x + point.y * m_bitmapSrc->Width(); + p[0] += (nFlags & MK_LBUTTON) ? 50 : 150; + p[0] += (nFlags & MK_MBUTTON) ? 150 : 0; + + int32 w = m_bitmapSrc->Width(); + // Make the initial point of this ripple a bit "fatter". + p[-1] += p[0] / 2; p[1] += p[0] / 2; + p[-w] += p[0] / 2; p[w] += p[0] / 2; + p[-w - 1] += p[0] / 4; p[-w + 1] += p[0] / 4; + p[w - 1] += p[0] / 4; p[w + 1] += p[0] / 4; + + m_damp = !(nFlags & MK_RBUTTON); + m_activity = true; + + // Wine will only ever generate MouseLeave message when the message + // queue is completely empty and the hover timeout has expired. + // This results in a hidden mouse cursor long after it had already left the + // control. + // Avoid hiding the mouse cursor on Wine. Interferring with the users input + // methods is an absolute no-go. + if(mpt::OS::Windows::IsWine()) + { + return; + } + + TRACKMOUSEEVENT me; + me.cbSize = sizeof(TRACKMOUSEEVENT); + me.hwndTrack = m_hWnd; + me.dwFlags = TME_LEAVE | TME_HOVER; + me.dwHoverTime = 1500; + + if(TrackMouseEvent(&me) && m_showMouse) + { + ShowCursor(FALSE); + m_showMouse = false; + } +} + + +void CRippleBitmap::OnMouseLeave() +{ + if(!m_showMouse) + { + ShowCursor(TRUE); + m_showMouse = true; + } +} + + +void CRippleBitmap::OnPaint() +{ + CPaintDC dc(this); + + CRect rect; + GetClientRect(rect); + StretchDIBits(dc.m_hDC, + 0, 0, rect.Width(), rect.Height(), + 0, 0, m_bitmapTarget->Width(), m_bitmapTarget->Height(), + m_bitmapTarget->Pixels().data(), + reinterpret_cast<BITMAPINFO *>(&m_bi), DIB_RGB_COLORS, SRCCOPY); +} + + +bool CRippleBitmap::Animate() +{ + // Were there any pixels being moved in the last frame? + if(!m_activity) + return false; + + DWORD now = timeGetTime(); + if(now - m_lastFrame < UPDATE_INTERVAL) + return true; + m_lastFrame = now; + m_activity = false; + + m_frontBuf = (m_frame ? m_offset2 : m_offset1).data(); + m_backBuf = (m_frame ? m_offset1 : m_offset2).data(); + + // Spread the ripples... + const int32 w = m_bitmapSrc->Width(), h = m_bitmapSrc->Height(); + const int32 numPixels = w * (h - 2); + const int32 *back = m_backBuf + w; + int32 *front = m_frontBuf + w; + for(int32 i = numPixels; i != 0; i--, back++, front++) + { + (*front) = (back[-1] + back[1] + back[w] + back[-w]) / 2 - (*front); + if(m_damp) (*front) -= (*front) >> 5; + } + + // ...and compute the final picture. + const int32 *offset = m_frontBuf + w; + const RawGDIDIB::Pixel *pixelIn = m_bitmapSrc->Pixels().data() + w; + RawGDIDIB::Pixel *pixelOut = m_bitmapTarget->Pixels().data() + w; + RawGDIDIB::Pixel *limitMin = m_bitmapSrc->Pixels().data(), *limitMax = m_bitmapSrc->Pixels().data() + m_bitmapSrc->Pixels().size() - 1; + for(int32 i = numPixels; i != 0; i--, pixelIn++, pixelOut++, offset++) + { + // Compute pixel displacement + const int32 xOff = offset[-1] - offset[1]; + const int32 yOff = offset[-w] - offset[w]; + + if(xOff | yOff) + { + const RawGDIDIB::Pixel *p = pixelIn + xOff + yOff * w; + Limit(p, limitMin, limitMax); + // Add a bit of shading depending on how far we're displacing the pixel... + pixelOut->r = mpt::saturate_cast<uint8>(p->r + (p->r * xOff) / 32); + pixelOut->g = mpt::saturate_cast<uint8>(p->g + (p->g * xOff) / 32); + pixelOut->b = mpt::saturate_cast<uint8>(p->b + (p->b * xOff) / 32); + // ...and mix it with original picture + pixelOut->r = (pixelOut->r + pixelIn->r) / 2u; + pixelOut->g = (pixelOut->g + pixelIn->g) / 2u; + pixelOut->b = (pixelOut->b + pixelIn->b) / 2u; + // And now some cheap image smoothing... + pixelOut[-1].r = (pixelOut->r + pixelOut[-1].r) / 2u; + pixelOut[-1].g = (pixelOut->g + pixelOut[-1].g) / 2u; + pixelOut[-1].b = (pixelOut->b + pixelOut[-1].b) / 2u; + pixelOut[-w].r = (pixelOut->r + pixelOut[-w].r) / 2u; + pixelOut[-w].g = (pixelOut->g + pixelOut[-w].g) / 2u; + pixelOut[-w].b = (pixelOut->b + pixelOut[-w].b) / 2u; + m_activity = true; // Also use this to update activity status... + } else + { + *pixelOut = *pixelIn; + } + } + + m_frame = !m_frame; + + InvalidateRect(NULL, FALSE); + + return true; +} + + +CAboutDlg::~CAboutDlg() +{ + instance = nullptr; +} + + +void CAboutDlg::OnOK() +{ + instance = nullptr; + if(m_TimerID != 0) + { + KillTimer(m_TimerID); + m_TimerID = 0; + } + DestroyWindow(); + delete this; +} + + +void CAboutDlg::OnCancel() +{ + OnOK(); +} + + +BOOL CAboutDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + mpt::ustring app; + app += MPT_UFORMAT("OpenMPT{} ({} ({} bit))")( + BuildVariants().GetBuildVariantDescription(BuildVariants().GetBuildVariant()), + mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()), + mpt::arch_bits) + + U_("\n"); + app += U_("Version ") + Build::GetVersionStringSimple() + U_("\n\n"); + app += Build::GetURL(Build::Url::Website) + U_("\n"); + SetDlgItemText(IDC_EDIT3, mpt::ToCString(mpt::String::Replace(app, U_("\n"), U_("\r\n")))); + + m_bmp.SubclassDlgItem(IDC_BITMAP1, this); + + m_Tab.InsertItem(TCIF_TEXT, 0, _T("OpenMPT"), 0, 0, 0, 0); + m_Tab.InsertItem(TCIF_TEXT, 1, _T("Components"), 0, 0, 0, 0); + m_Tab.InsertItem(TCIF_TEXT, 2, _T("Credits"), 0, 0, 0, 0); + m_Tab.InsertItem(TCIF_TEXT, 3, _T("License"), 0, 0, 0, 0); + m_Tab.InsertItem(TCIF_TEXT, 4, _T("Resources"), 0, 0, 0, 0); + if(mpt::OS::Windows::IsWine()) m_Tab.InsertItem(TCIF_TEXT, 5, _T("Wine"), 0, 0, 0, 0); + m_Tab.SetCurSel(0); + + OnTabChange(nullptr, nullptr); + + if(m_TimerID != 0) + { + KillTimer(m_TimerID); + m_TimerID = 0; + } + m_TimerID = SetTimer(TIMERID_ABOUT_DEFAULT, CRippleBitmap::UPDATE_INTERVAL, nullptr); + + return TRUE; +} + + +void CAboutDlg::OnTimer(UINT_PTR nIDEvent) +{ + if(nIDEvent == m_TimerID) + { + m_bmp.Animate(); + } +} + + +void CAboutDlg::OnTabChange(NMHDR * /*pNMHDR*/ , LRESULT * /*pResult*/ ) +{ + m_TabEdit.SetWindowText(mpt::ToCString(mpt::String::Replace(GetTabText(m_Tab.GetCurSel()), U_("\n"), U_("\r\n")))); +} + + +#ifdef MPT_ENABLE_ARCH_INTRINSICS +static mpt::ustring ProcSupportToString(uint32 procSupport) +{ + std::vector<mpt::ustring> features; +#if MPT_COMPILER_MSVC +#if defined(MPT_ENABLE_ARCH_X86) + features.push_back(U_("x86")); +#endif +#if defined(MPT_ENABLE_ARCH_AMD64) + features.push_back(U_("amd64")); +#endif + struct ProcFlag + { + decltype(procSupport) flag; + const char *name; + }; + static constexpr ProcFlag flags[] = + { + { 0, "" }, +#if defined(MPT_ENABLE_ARCH_X86) || defined(MPT_ENABLE_ARCH_AMD64) + { CPU::feature::mmx, "mmx" }, + { CPU::feature::sse, "sse" }, + { CPU::feature::sse2, "sse2" }, + { CPU::feature::sse3, "sse3" }, + { CPU::feature::ssse3, "ssse3" }, + { CPU::feature::sse4_1, "sse4.1" }, + { CPU::feature::sse4_2, "sse4.2" }, + { CPU::feature::avx, "avx" }, + { CPU::feature::avx2, "avx2" }, +#endif + }; + for(const auto &f : flags) + { + if(procSupport & f.flag) features.push_back(mpt::ToUnicode(mpt::Charset::ASCII, f.name)); + } +#else + MPT_UNUSED_VARIABLE(procSupport); +#endif + return mpt::String::Combine(features, U_(" ")); +} +#endif // MPT_ENABLE_ARCH_INTRINSICS + + +mpt::ustring CAboutDlg::GetTabText(int tab) +{ + const mpt::ustring lf = U_("\n"); + const mpt::ustring yes = U_("yes"); + const mpt::ustring no = U_("no"); +#ifdef MPT_ENABLE_ARCH_INTRINSICS + const CPU::Info CPUInfo = CPU::Info::Get(); +#endif // MPT_ENABLE_ARCH_INTRINSICS + mpt::ustring text; + switch(tab) + { + case 0: + text = U_("OpenMPT - Open ModPlug Tracker\n\n") + + MPT_UFORMAT("Version: {}\n")(Build::GetVersionStringExtended()) + + MPT_UFORMAT("Source Code: {}\n")(SourceInfo::Current().GetUrlWithRevision() + UL_(" ") + SourceInfo::Current().GetStateString()) + + MPT_UFORMAT("Build Date: {}\n")(Build::GetBuildDateString()) + + MPT_UFORMAT("Compiler: {}\n")(Build::GetBuildCompilerString()) + + MPT_UFORMAT("Architecture: {}\n")(mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())) + + MPT_UFORMAT("Required Windows Kernel Level: {}\n")(mpt::OS::Windows::Version::GetName(mpt::OS::Windows::Version::GetMinimumKernelLevel())) + + MPT_UFORMAT("Required Windows API Level: {}\n")(mpt::OS::Windows::Version::GetName(mpt::OS::Windows::Version::GetMinimumAPILevel())); + { + text += U_("Required CPU features: "); + std::vector<mpt::ustring> features; + #if MPT_COMPILER_MSVC + #if defined(_M_X64) + features.push_back(U_("x86-64")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx) features.push_back(U_("avx")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx2) features.push_back(U_("avx2")); + #elif defined(_M_IX86) + if(CPU::GetMinimumFeatures() & CPU::feature::sse) features.push_back(U_("sse")); + if(CPU::GetMinimumFeatures() & CPU::feature::sse2) features.push_back(U_("sse2")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx) features.push_back(U_("avx")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx2) features.push_back(U_("avx2")); + #else + if(CPU::GetMinimumFeatures() & CPU::feature::sse) features.push_back(U_("sse")); + if(CPU::GetMinimumFeatures() & CPU::feature::sse2) features.push_back(U_("sse2")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx) features.push_back(U_("avx")); + if(CPU::GetMinimumFeatures() & CPU::feature::avx2) features.push_back(U_("avx2")); + #endif + #endif + text += mpt::String::Combine(features, U_(" ")); + text += lf; + } +#ifdef MPT_ENABLE_ARCH_INTRINSICS + text += MPT_UFORMAT("Optional CPU features used: {}\n")(ProcSupportToString(CPU::GetEnabledFeatures())); +#endif // MPT_ENABLE_ARCH_INTRINSICS + text += lf; + text += MPT_UFORMAT("System Architecture: {}\n")(mpt::OS::Windows::Name(mpt::OS::Windows::GetHostArchitecture())); +#ifdef MPT_ENABLE_ARCH_INTRINSICS + text += MPT_UFORMAT("CPU: {}, Family {}, Model {}, Stepping {}\n") + ( mpt::ToUnicode(mpt::Charset::ASCII, (std::strlen(CPUInfo.VendorID) > 0) ? std::string(CPUInfo.VendorID) : std::string("Generic")) + , CPUInfo.Family + , CPUInfo.Model + , CPUInfo.Stepping + ); + text += MPT_UFORMAT("CPU Name: {}\n")(mpt::ToUnicode(mpt::Charset::ASCII, (std::strlen(CPUInfo.BrandID) > 0) ? std::string(CPUInfo.BrandID) : std::string(""))); + text += MPT_UFORMAT("Available CPU features: {}\n")(ProcSupportToString(CPUInfo.AvailableFeatures)); +#endif // MPT_ENABLE_ARCH_INTRINSICS + text += MPT_UFORMAT("Operating System: {}\n\n")(mpt::OS::Windows::Version::GetName(mpt::OS::Windows::Version::Current())); + text += MPT_UFORMAT("OpenMPT Install Path{1}: {0}\n")(theApp.GetInstallPath(), theApp.IsPortableMode() ? U_(" (portable)") : U_("")); + text += MPT_UFORMAT("OpenMPT Executable Path{1}: {0}\n")(theApp.GetInstallBinArchPath(), theApp.IsPortableMode() ? U_(" (portable)") : U_("")); + text += MPT_UFORMAT("Settings{1}: {0}\n")(theApp.GetConfigFileName(), theApp.IsPortableMode() ? U_(" (portable)") : U_("")); + break; + case 1: + { + std::vector<std::string> components = ComponentManager::Instance()->GetRegisteredComponents(); + if(!TrackerSettings::Instance().ComponentsKeepLoaded) + { + text += U_("Components are loaded and unloaded as needed.\n\n"); + for(const auto &component : components) + { + ComponentInfo info = ComponentManager::Instance()->GetComponentInfo(component); + mpt::ustring name = mpt::ToUnicode(mpt::Charset::ASCII, (info.name.substr(0, 9) == "Component") ? info.name.substr(9) : info.name); + if(!info.settingsKey.empty()) + { + name = mpt::ToUnicode(mpt::Charset::ASCII, info.settingsKey); + } + text += name + lf; + } + } else + { + for(int available = 1; available >= 0; --available) + { + if(available) + { + text += U_("Loaded Components:\n"); + } else + { + text += U_("\nUnloaded Components:\n"); + } + for(const auto &component : components) + { + ComponentInfo info = ComponentManager::Instance()->GetComponentInfo(component); + if(available && info.state != ComponentStateAvailable) continue; + if(!available && info.state == ComponentStateAvailable) continue; + mpt::ustring name = mpt::ToUnicode(mpt::Charset::ASCII, (info.name.substr(0, 9) == "Component") ? info.name.substr(9) : info.name); + if(!info.settingsKey.empty()) + { + name = mpt::ToUnicode(mpt::Charset::ASCII, info.settingsKey); + } + text += MPT_UFORMAT("{}: {}") + ( name + , info.state == ComponentStateAvailable ? U_("ok") : + info.state == ComponentStateUnavailable? U_("missing") : + info.state == ComponentStateUnintialized ? U_("not loaded") : + info.state == ComponentStateBlocked ? U_("blocked") : + info.state == ComponentStateUnregistered ? U_("unregistered") : + U_("unknown") + ); + if(info.type != ComponentTypeUnknown) + { + text += MPT_UFORMAT(" ({})") + ( info.type == ComponentTypeBuiltin ? U_("builtin") : + info.type == ComponentTypeSystem ? U_("system") : + info.type == ComponentTypeSystemInstallable ? U_("system, optional") : + info.type == ComponentTypeBundled ? U_("bundled") : + info.type == ComponentTypeForeign ? U_("foreign") : + U_("unknown") + ); + } + text += lf; + } + } + } + } + break; + case 2: + text += Build::GetFullCreditsString(); + break; + case 3: + text += Build::GetLicenseString(); + break; + case 4: + text += U_("Website:\n") + Build::GetURL(Build::Url::Website); + text += U_("\n\nForum:\n") + Build::GetURL(Build::Url::Forum); + text += U_("\n\nBug Tracker:\n") + Build::GetURL(Build::Url::Bugtracker); + text += U_("\n\nUpdates:\n") + Build::GetURL(Build::Url::Updates); + break; + case 5: + try + { + if(!theApp.GetWine()) + { + text += U_("Wine integration not available.\n"); + } else + { + + mpt::Wine::Context & wine = *theApp.GetWine(); + + text += MPT_UFORMAT("Windows: {}\n") + ( mpt::OS::Windows::Version::Current().IsWindows() ? yes : no + ); + text += MPT_UFORMAT("Windows version: {}\n") + ( + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::Win81) ? U_("Windows 8.1") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::Win8) ? U_("Windows 8") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::Win7) ? U_("Windows 7") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::WinVista) ? U_("Windows Vista") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::WinXP) ? U_("Windows XP") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::Win2000) ? U_("Windows 2000") : + mpt::OS::Windows::Version::Current().IsAtLeast(mpt::OS::Windows::Version::WinNT4) ? U_("Windows NT4") : + U_("unknown") + ); + text += MPT_UFORMAT("Windows original: {}\n") + ( mpt::OS::Windows::IsOriginal() ? yes : no + ); + + text += U_("\n"); + + text += MPT_UFORMAT("Wine: {}\n") + ( mpt::OS::Windows::IsWine() ? yes : no + ); + text += MPT_UFORMAT("Wine Version: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.VersionContext().RawVersion()) + ); + text += MPT_UFORMAT("Wine Build ID: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.VersionContext().RawBuildID()) + ); + text += MPT_UFORMAT("Wine Host Sys Name: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.VersionContext().RawHostSysName()) + ); + text += MPT_UFORMAT("Wine Host Release: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.VersionContext().RawHostRelease()) + ); + + text += U_("\n"); + + text += MPT_UFORMAT("uname -m: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.Uname_m()) + ); + text += MPT_UFORMAT("HOME: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.HOME()) + ); + text += MPT_UFORMAT("XDG_DATA_HOME: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.XDG_DATA_HOME()) + ); + text += MPT_UFORMAT("XDG_CACHE_HOME: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.XDG_CACHE_HOME()) + ); + text += MPT_UFORMAT("XDG_CONFIG_HOME: {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.XDG_CONFIG_HOME()) + ); + + text += U_("\n"); + + text += MPT_UFORMAT("OpenMPT folder: {}\n") + ( theApp.GetInstallPath().ToUnicode() + ); + text += MPT_UFORMAT("OpenMPT folder (host): {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.PathToPosix(theApp.GetInstallPath())) + ); + text += MPT_UFORMAT("OpenMPT config folder: {}\n") + ( theApp.GetConfigPath().ToUnicode() + ); + text += MPT_UFORMAT("OpenMPT config folder (host): {}\n") + ( mpt::ToUnicode(mpt::Charset::UTF8, wine.PathToPosix(theApp.GetConfigPath())) + ); + text += MPT_UFORMAT("Host root: {}\n") + ( wine.PathToWindows("/").ToUnicode() + ); + + } + } catch(const mpt::Wine::Exception & e) + { + text += U_("Exception: ") + mpt::get_exception_text<mpt::ustring>(e) + U_("\n"); + } + break; + } + return text; +} + + +void CAboutDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_TABABOUT, m_Tab); + DDX_Control(pDX, IDC_EDITABOUT, m_TabEdit); +} + + +BEGIN_MESSAGE_MAP(CAboutDlg, CDialog) + ON_WM_TIMER() + ON_NOTIFY(TCN_SELCHANGE, IDC_TABABOUT, &CAboutDlg::OnTabChange) +END_MESSAGE_MAP() + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.h new file mode 100644 index 00000000..28b7b2f3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AboutDialog.h @@ -0,0 +1,83 @@ +/* + * AboutDialog.h + * ------------- + * Purpose: About dialog with credits, system information and a fancy demo effect. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class RawGDIDIB; + +class CRippleBitmap: public CWnd +{ + +public: + + static constexpr DWORD UPDATE_INTERVAL = 15; // milliseconds + +protected: + + BITMAPINFOHEADER m_bi; + std::unique_ptr<RawGDIDIB> m_bitmapSrc, m_bitmapTarget; + std::vector<int32> m_offset1, m_offset2; + int32 *m_frontBuf, *m_backBuf; + DWORD m_lastFrame = 0; // Time of last frame + DWORD m_lastRipple = 0; // Time of last added ripple + bool m_frame = false; // Backbuffer toggle + bool m_damp = true; // Ripple damping status + bool m_activity = true; // There are actually some ripples + bool m_showMouse = true; + +public: + + CRippleBitmap(); + ~CRippleBitmap(); + bool Animate(); + +protected: + void OnPaint(); + BOOL OnEraseBkgnd(CDC *) { return TRUE; } + + void OnMouseMove(UINT nFlags, CPoint point); + void OnMouseHover(UINT nFlags, CPoint point) { OnMouseMove(nFlags, point); } + void OnMouseLeave(); + + DECLARE_MESSAGE_MAP() +}; + + +class CAboutDlg: public CDialog +{ +protected: + CRippleBitmap m_bmp; + CTabCtrl m_Tab; + CEdit m_TabEdit; + UINT_PTR m_TimerID = 0; + static constexpr UINT_PTR TIMERID_ABOUT_DEFAULT = 3; + +public: + static CAboutDlg *instance; + + ~CAboutDlg(); + + // Implementation +protected: + BOOL OnInitDialog() override; + void OnOK() override; + void OnCancel() override; + DECLARE_MESSAGE_MAP(); + void DoDataExchange(CDataExchange* pDX) override; + afx_msg void OnTabChange(NMHDR *pNMHDR, LRESULT *pResult); + void OnTimer(UINT_PTR nIDEvent); +public: + static mpt::ustring GetTabText(int tab); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.cpp new file mode 100644 index 00000000..4c11e597 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.cpp @@ -0,0 +1,1022 @@ +/* + * AbstractVstEditor.cpp + * --------------------- + * Purpose: Common plugin editor interface class. This code is shared between custom and default plugin user interfaces. + * 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 "Clipboard.h" +#include "../soundlib/Sndfile.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../soundlib/plugins/PluginManager.h" +#include "Vstplug.h" +#include "dlg_misc.h" +#include "AbstractVstEditor.h" +#include "../common/mptStringBuffer.h" +#include "MIDIMacros.h" +#include "VstPresets.h" +#include "../common/FileReader.h" +#include "InputHandler.h" +#include "dlg_misc.h" +#include <sstream> +#include "Globals.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#ifndef NO_PLUGINS + +CAbstractVstEditor::WindowSizeAdjuster::WindowSizeAdjuster(CWnd &wnd) + : m_wnd(wnd) +{ + MENUBARINFO mbi = { sizeof(mbi) }; + if(GetMenuBarInfo(m_wnd, OBJID_MENU, 0, &mbi)) + m_menuHeight = (mbi.rcBar.bottom - mbi.rcBar.top); +} + +CAbstractVstEditor::WindowSizeAdjuster::~WindowSizeAdjuster() +{ + // Extend window height by the menu size if it changed + MENUBARINFO mbi = { sizeof(mbi) }; + if(GetMenuBarInfo(m_wnd, OBJID_MENU, 0, &mbi)) + { + CRect windowRect; + m_wnd.GetWindowRect(&windowRect); + windowRect.bottom += (mbi.rcBar.bottom - mbi.rcBar.top) - m_menuHeight; + m_wnd.SetWindowPos(nullptr, 0, 0, + windowRect.Width(), windowRect.Height(), + SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); + } +} + +#define PRESETS_PER_COLUMN 32 +#define PRESETS_PER_GROUP 128 + +UINT CAbstractVstEditor::m_clipboardFormat = RegisterClipboardFormat(_T("VST Preset Data")); + +BEGIN_MESSAGE_MAP(CAbstractVstEditor, CDialog) + ON_WM_CLOSE() + ON_WM_INITMENU() + ON_WM_MENUSELECT() + ON_WM_ACTIVATE() + ON_WM_DROPFILES() + ON_WM_MOVE() + ON_WM_NCLBUTTONDBLCLK() + ON_COMMAND(ID_EDIT_COPY, &CAbstractVstEditor::OnCopyParameters) + ON_COMMAND(ID_EDIT_PASTE, &CAbstractVstEditor::OnPasteParameters) + ON_COMMAND(ID_PRESET_LOAD, &CAbstractVstEditor::OnLoadPreset) + ON_COMMAND(ID_PLUG_BYPASS, &CAbstractVstEditor::OnBypassPlug) + ON_COMMAND(ID_PLUG_RECORDAUTOMATION,&CAbstractVstEditor::OnRecordAutomation) + ON_COMMAND(ID_PLUG_RECORD_MIDIOUT, &CAbstractVstEditor::OnRecordMIDIOut) + ON_COMMAND(ID_PLUG_PASSKEYS, &CAbstractVstEditor::OnPassKeypressesToPlug) + ON_COMMAND(ID_PRESET_SAVE, &CAbstractVstEditor::OnSavePreset) + ON_COMMAND(ID_PRESET_RANDOM, &CAbstractVstEditor::OnRandomizePreset) + ON_COMMAND(ID_RENAME_PLUGIN, &CAbstractVstEditor::OnRenamePlugin) + ON_COMMAND(ID_PREVIOUSVSTPRESET, &CAbstractVstEditor::OnSetPreviousVSTPreset) + ON_COMMAND(ID_NEXTVSTPRESET, &CAbstractVstEditor::OnSetNextVSTPreset) + ON_COMMAND(ID_VSTPRESETBACKWARDJUMP,&CAbstractVstEditor::OnVSTPresetBackwardJump) + ON_COMMAND(ID_VSTPRESETFORWARDJUMP, &CAbstractVstEditor::OnVSTPresetForwardJump) + ON_COMMAND(ID_VSTPRESETNAME, &CAbstractVstEditor::OnVSTPresetRename) + ON_COMMAND(ID_PLUGINTOINSTRUMENT, &CAbstractVstEditor::OnCreateInstrument) + ON_COMMAND_RANGE(ID_PRESET_SET, ID_PRESET_SET + PRESETS_PER_GROUP, &CAbstractVstEditor::OnSetPreset) + ON_MESSAGE(WM_MOD_MIDIMSG, &CAbstractVstEditor::OnMidiMsg) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CAbstractVstEditor::OnCustomKeyMsg) //rewbs.customKeys + ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + MAX_MIXPLUGINS, &CAbstractVstEditor::OnToggleEditor) //rewbs.patPlugName + ON_COMMAND_RANGE(ID_SELECTINST, ID_SELECTINST + MAX_INSTRUMENTS, &CAbstractVstEditor::OnSetInputInstrument) //rewbs.patPlugName + ON_COMMAND_RANGE(ID_LEARN_MACRO_FROM_PLUGGUI, ID_LEARN_MACRO_FROM_PLUGGUI + kSFxMacros, &CAbstractVstEditor::PrepareToLearnMacro) +END_MESSAGE_MAP() + + +CAbstractVstEditor::CAbstractVstEditor(IMixPlugin &plugin) + : m_VstPlugin(plugin) +{ + m_Menu.LoadMenu(IDR_VSTMENU); + m_nInstrument = GetBestInstrumentCandidate(); +} + + +CAbstractVstEditor::~CAbstractVstEditor() +{ + m_VstPlugin.m_pEditor = nullptr; +} + + +void CAbstractVstEditor::PostNcDestroy() +{ + CDialog::PostNcDestroy(); + delete this; +} + + +void CAbstractVstEditor::OnNcLButtonDblClk(UINT nHitTest, CPoint point) +{ + CDialog::OnNcLButtonDblClk(nHitTest, point); + // Double click on title bar = reduce plugin window to non-client area + if(nHitTest == HTCAPTION) + { + CRect rcWnd, rcClient; + GetWindowRect(&rcWnd); + if(!m_isMinimized) + { + // When minimizing, remove the client area + GetClientRect(&rcClient); + m_clientHeight = rcClient.Height(); + } + m_isMinimized = !m_isMinimized; + m_clientHeight = -m_clientHeight; + int rcHeight = rcWnd.Height() + m_clientHeight; + + SetWindowPos(NULL, 0, 0, + rcWnd.Width(), rcHeight, + SWP_NOZORDER | SWP_NOMOVE); + } +} + + +void CAbstractVstEditor::OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized) +{ + CDialog::OnActivate(nState, pWndOther, bMinimized); + if(nState != WA_INACTIVE) CMainFrame::GetMainFrame()->SetMidiRecordWnd(GetSafeHwnd()); +} + + +LRESULT CAbstractVstEditor::OnMidiMsg(WPARAM midiData, LPARAM sender) +{ + CModDoc *modDoc = m_VstPlugin.GetModDoc(); + if(modDoc != nullptr && sender != reinterpret_cast<LPARAM>(&m_VstPlugin)) + { + if(!CheckInstrument(m_nInstrument)) + m_nInstrument = GetBestInstrumentCandidate(); + modDoc->ProcessMIDI((uint32)midiData, m_nInstrument, &m_VstPlugin, kCtxVSTGUI); + return 1; + } + return 0; +} + + +// Drop files from Windows +void CAbstractVstEditor::OnDropFiles(HDROP hDropInfo) +{ + const UINT nFiles = ::DragQueryFileW(hDropInfo, (UINT)-1, NULL, 0); + CMainFrame::GetMainFrame()->SetForegroundWindow(); + for(UINT f = 0; f < nFiles; f++) + { + UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, _T('\0')); + if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) + { + m_VstPlugin.LoadProgram(mpt::PathString::FromNative(fileName.data())); + } + } + ::DragFinish(hDropInfo); +} + + +void CAbstractVstEditor::OnLoadPreset() +{ + if(m_VstPlugin.LoadProgram()) + { + UpdatePresetMenu(true); + UpdatePresetField(); + } +} + + +void CAbstractVstEditor::OnSavePreset() +{ + m_VstPlugin.SaveProgram(); +} + + +void CAbstractVstEditor::OnCopyParameters() +{ + if(CMainFrame::GetMainFrame() == nullptr) return; + + BeginWaitCursor(); + std::ostringstream f(std::ios::out | std::ios::binary); + if(VSTPresets::SaveFile(f, m_VstPlugin, false)) + { + const std::string data = f.str(); + Clipboard clipboard(m_clipboardFormat, data.length()); + if(auto dst = clipboard.As<char>()) + { + memcpy(dst, data.data(), data.length()); + } + } + EndWaitCursor(); +} + + +void CAbstractVstEditor::OnPasteParameters() +{ + if(CMainFrame::GetMainFrame() == nullptr) return; + + BeginWaitCursor(); + Clipboard clipboard(m_clipboardFormat); + if(auto data = clipboard.Get(); data.data()) + { + FileReader file(data); + VSTPresets::ErrorCode error = VSTPresets::LoadFile(file, m_VstPlugin); + clipboard.Close(); + + if(error == VSTPresets::noError) + { + const CSoundFile &sndFile = m_VstPlugin.GetSoundFile(); + CModDoc *pModDoc; + if(sndFile.GetModSpecifications().supportsPlugins && (pModDoc = sndFile.GetpModDoc()) != nullptr) + { + pModDoc->SetModified(); + } + UpdatePresetField(); + } else + { + Reporting::Error(VSTPresets::GetErrorMessage(error)); + } + } + EndWaitCursor(); +} + + +void CAbstractVstEditor::OnRandomizePreset() +{ + static double randomFactor = 10.0; + CInputDlg dlg(this, _T("Input parameter randomization amount (0 = no change, 100 = completely random)"), 0.0, 100.0, randomFactor); + if(dlg.DoModal() == IDOK) + { + randomFactor = dlg.resultAsDouble; + PlugParamValue factor = PlugParamValue(randomFactor / 100.0); + PlugParamIndex numParams = m_VstPlugin.GetNumParameters(); + for(PlugParamIndex p = 0; p < numParams; p++) + { + PlugParamValue val = m_VstPlugin.GetParameter(p); + val += mpt::random(theApp.PRNG(), PlugParamValue(-1.0), PlugParamValue(1.0)) * factor; + Limit(val, 0.0f, 1.0f); + m_VstPlugin.SetParameter(p, val); + } + UpdateParamDisplays(); + } +} + + +void CAbstractVstEditor::OnRenamePlugin() +{ + auto &sndFile = m_VstPlugin.GetSoundFile(); + auto &plugin = sndFile.m_MixPlugins[m_VstPlugin.m_nSlot]; + + CInputDlg dlg(this, _T("New name for this plugin instance:"), mpt::ToCString(plugin.GetName()), static_cast<int32>(std::size(plugin.Info.szName.buf))); + if(dlg.DoModal() == IDOK) + { + if(dlg.resultAsString != mpt::ToCString(plugin.GetName())) + { + plugin.Info.szName = mpt::ToCharset(mpt::Charset::Locale, dlg.resultAsString); + if(auto *modDoc = sndFile.GetpModDoc(); modDoc != nullptr) + { + if(sndFile.GetModSpecifications().supportsPlugins) + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, PluginHint(m_VstPlugin.m_nSlot + 1).Info().Names(), this); + } + SetTitle(); + } + } +} + + +bool CAbstractVstEditor::OpenEditor(CWnd *) +{ + ModifyStyleEx(0, WS_EX_ACCEPTFILES); + RestoreWindowPos(); + SetTitle(); + SetupMenu(); + ShowWindow(SW_SHOW); + return true; +} + + +void CAbstractVstEditor::DoClose() +{ + StoreWindowPos(); + m_presetMenuGroup.clear(); + DestroyWindow(); +} + + +void CAbstractVstEditor::SetupMenu(bool force) +{ + WindowSizeAdjuster adjuster(*this); + SetMenu(&m_Menu); + UpdatePresetMenu(force); + UpdateInputMenu(); + UpdateOutputMenu(); + UpdateMacroMenu(); + UpdateOptionsMenu(); + UpdatePresetField(); +} + + +void CAbstractVstEditor::UpdatePresetField() +{ + if(m_VstPlugin.GetNumPrograms() > 0) + { + if(m_Menu.GetMenuItemCount() < 5) + { + m_Menu.AppendMenu(MF_BYPOSITION, ID_VSTPRESETBACKWARDJUMP, _T("<<")); + m_Menu.AppendMenu(MF_BYPOSITION, ID_PREVIOUSVSTPRESET, _T("<")); + m_Menu.AppendMenu(MF_BYPOSITION, ID_NEXTVSTPRESET, _T(">")); + m_Menu.AppendMenu(MF_BYPOSITION, ID_VSTPRESETFORWARDJUMP, _T(">>")); + m_Menu.AppendMenu(MF_BYPOSITION|MF_DISABLED, ID_VSTPRESETNAME, _T("")); + } + + CString programName = m_VstPlugin.GetFormattedProgramName(m_VstPlugin.GetCurrentProgram()); + programName.Replace(_T("&"), _T("&&")); + m_Menu.ModifyMenu(8, MF_BYPOSITION, ID_VSTPRESETNAME, programName); + } + + DrawMenuBar(); + +} + + +void CAbstractVstEditor::OnSetPreset(UINT nID) +{ + SetPreset(nID - ID_PRESET_SET + m_currentPresetMenu * PRESETS_PER_GROUP); +} + + +void CAbstractVstEditor::OnSetPreviousVSTPreset() +{ + SetPreset(m_VstPlugin.GetCurrentProgram() - 1); +} + + +void CAbstractVstEditor::OnSetNextVSTPreset() +{ + SetPreset(m_VstPlugin.GetCurrentProgram() + 1); +} + + +void CAbstractVstEditor::OnVSTPresetBackwardJump() +{ + SetPreset(std::max(0, m_VstPlugin.GetCurrentProgram() - 10)); +} + + +void CAbstractVstEditor::OnVSTPresetForwardJump() +{ + SetPreset(std::min(m_VstPlugin.GetCurrentProgram() + 10, m_VstPlugin.GetNumPrograms() - 1)); +} + + +void CAbstractVstEditor::SetPreset(int32 preset) +{ + if(preset >= 0 && preset < m_VstPlugin.GetNumPrograms()) + { + m_VstPlugin.SetCurrentProgram(preset); + WindowSizeAdjuster adjuster(*this); + UpdatePresetField(); + + if(m_VstPlugin.GetSoundFile().GetModSpecifications().supportsPlugins) + { + m_VstPlugin.GetModDoc()->SetModified(); + } + } +} + + +void CAbstractVstEditor::OnVSTPresetRename() +{ + auto currentName = m_VstPlugin.GetCurrentProgramName(); + CInputDlg dlg(this, _T("New program name:"), currentName); + if(dlg.DoModal() == IDOK) + { + m_VstPlugin.SetCurrentProgramName(dlg.resultAsString); + if(m_VstPlugin.GetCurrentProgramName() != currentName) + { + m_VstPlugin.SetModified(); + WindowSizeAdjuster adjuster(*this); + UpdatePresetField(); + UpdatePresetMenu(true); + } + } +} + + +void CAbstractVstEditor::OnBypassPlug() +{ + m_VstPlugin.ToggleBypass(); + if(m_VstPlugin.GetSoundFile().GetModSpecifications().supportsPlugins) + { + m_VstPlugin.GetModDoc()->SetModified(); + } + SetTitle(); +} + + +void CAbstractVstEditor::OnRecordAutomation() +{ + m_VstPlugin.m_recordAutomation = !m_VstPlugin.m_recordAutomation; +} + + +void CAbstractVstEditor::OnRecordMIDIOut() +{ + m_VstPlugin.m_recordMIDIOut = !m_VstPlugin.m_recordMIDIOut; +} + + +void CAbstractVstEditor::OnPassKeypressesToPlug() +{ + m_VstPlugin.m_passKeypressesToPlug = !m_VstPlugin.m_passKeypressesToPlug; +} + + +BOOL CAbstractVstEditor::PreTranslateMessage(MSG *msg) +{ + if(msg && HandleKeyMessage(*msg)) + return TRUE; + + return CDialog::PreTranslateMessage(msg); +} + + +bool CAbstractVstEditor::HandleKeyMessage(MSG &msg) +{ + if(m_VstPlugin.m_passKeypressesToPlug) + return false; + if(msg.message != WM_SYSKEYUP && msg.message != WM_KEYUP && msg.message != WM_SYSKEYDOWN && msg.message != WM_KEYDOWN) + return false; + + CInputHandler *ih = CMainFrame::GetInputHandler(); + if(ih->IsKeyPressHandledByTextBox(static_cast<DWORD>(msg.wParam), ::GetFocus())) + return false; + + // Translate message manually + UINT nChar = (UINT)msg.wParam; + UINT nRepCnt = LOWORD(msg.lParam); + UINT nFlags = HIWORD(msg.lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + + // If we successfully mapped to a command and plug does not listen for keypresses, no need to pass message on. + if(ih->KeyEvent(kCtxVSTGUI, nChar, nRepCnt, nFlags, kT, this) != kcNull) + return true; + + // Don't forward key repeats if plug does not listen for keypresses + // (avoids system beeps on note hold) + if(kT == kKeyEventRepeat) + return true; + + return false; +} + + +void CAbstractVstEditor::UpdateView(UpdateHint hint) +{ + if(!hint.GetType()[HINT_PLUGINNAMES | HINT_MIXPLUGINS]) + return; + + PLUGINDEX hintPlug = hint.ToType<PluginHint>().GetPlugin(); + if(hintPlug > 0 && (hintPlug - 1) != m_VstPlugin.GetSlot()) + return; + + SetTitle(); +} + + +void CAbstractVstEditor::SetTitle() +{ + if(m_VstPlugin.m_pMixStruct) + { + CString title = MPT_CFORMAT("FX {}: ")(mpt::cfmt::dec0<2>(m_VstPlugin.m_nSlot + 1)); + + bool hasCustomName = (m_VstPlugin.m_pMixStruct->GetName() != U_("")) && (m_VstPlugin.m_pMixStruct->GetName() != m_VstPlugin.m_pMixStruct->GetLibraryName()); + if(hasCustomName) + title += mpt::ToCString(m_VstPlugin.m_pMixStruct->GetName()) + _T(" ("); + title += mpt::ToCString(m_VstPlugin.m_pMixStruct->GetLibraryName()); + if(hasCustomName) + title += _T(")"); + +#ifdef MPT_WITH_VST + const CVstPlugin *vstPlugin = dynamic_cast<CVstPlugin *>(&m_VstPlugin); + if(vstPlugin != nullptr && vstPlugin->isBridged) + title += MPT_CFORMAT(" ({} Bridged)")(m_VstPlugin.GetPluginFactory().GetDllArchNameUser()); +#endif // MPT_WITH_VST + + if(m_VstPlugin.IsBypassed()) + title += _T(" - Bypass"); + + SetWindowText(title); + } +} + + +LRESULT CAbstractVstEditor::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + switch(wParam) + { + case kcVSTGUIPrevPreset: OnSetPreviousVSTPreset(); return wParam; + case kcVSTGUIPrevPresetJump: OnVSTPresetBackwardJump(); return wParam; + case kcVSTGUINextPreset: OnSetNextVSTPreset(); return wParam; + case kcVSTGUINextPresetJump: OnVSTPresetForwardJump(); return wParam; + case kcVSTGUIRandParams: OnRandomizePreset() ; return wParam; + case kcVSTGUIToggleRecordParams: OnRecordAutomation(); return wParam; + case kcVSTGUIToggleSendKeysToPlug: OnPassKeypressesToPlug(); return wParam; + case kcVSTGUIBypassPlug: OnBypassPlug(); return wParam; + } + if (wParam >= kcVSTGUIStartNotes && wParam <= kcVSTGUIEndNotes) + { + if(ValidateCurrentInstrument()) + { + CModDoc *pModDoc = m_VstPlugin.GetModDoc(); + const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcVSTGUIStartNotes), m_nInstrument); + if(ModCommand::IsNote(note)) + { + pModDoc->PlayNote(PlayNoteParam(note).Instrument(m_nInstrument), &m_noteChannel); + } + } + return wParam; + } + if (wParam >= kcVSTGUIStartNoteStops && wParam <= kcVSTGUIEndNoteStops) + { + if(ValidateCurrentInstrument()) + { + CModDoc *pModDoc = m_VstPlugin.GetModDoc(); + const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcVSTGUIStartNoteStops), m_nInstrument); + if(ModCommand::IsNote(note)) + { + pModDoc->NoteOff(note, false, m_nInstrument, m_noteChannel[note - NOTE_MIN]); + } + } + return wParam; + } + + return kcNull; +} + + +// When trying to play a note using this plugin, but no instrument is assigned to it, +// the user is asked whether a new instrument should be added. +bool CAbstractVstEditor::ValidateCurrentInstrument() +{ + if(!CheckInstrument(m_nInstrument)) + m_nInstrument = GetBestInstrumentCandidate(); + + //only show messagebox if plug is able to process notes. + if(m_nInstrument == INSTRUMENTINDEX_INVALID) + { + if(m_VstPlugin.CanRecieveMidiEvents()) + { + // We might need to steal the focus from the plugin bridge. This is going to work + // as the plugin bridge will call AllowSetForegroundWindow on key messages. + SetForegroundWindow(); + if(!m_VstPlugin.IsInstrument() || m_VstPlugin.GetSoundFile().GetModSpecifications().instrumentsMax == 0 || + Reporting::Confirm(_T("You need to assign an instrument to this plugin before you can play notes from here.\nCreate a new instrument and assign this plugin to the instrument?"), false, false, this) == cnfNo) + { + return false; + } else + { + OnCreateInstrument(); + // Return true since we don't want to trigger the note for which the instrument has been validated yet. + // Otherwise, the note might hang forever because the key-up event will go missing. + return false; + } + } else + { + // Can't process notes + return false; + } + } + return true; + +} + + +static int GetNumSubMenus(int32 numProgs) { return (numProgs + (PRESETS_PER_GROUP - 1)) / PRESETS_PER_GROUP; } + + +void CAbstractVstEditor::OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hSysMenu) +{ + if(!(nFlags & MF_POPUP)) + { + return; + } + if(hSysMenu == m_Menu.m_hMenu) + { + // Main menu + switch(nItemID) + { + case 0: + // Grey out paste menu item. + m_Menu.EnableMenuItem(ID_EDIT_PASTE, MF_BYCOMMAND | (IsClipboardFormatAvailable(m_clipboardFormat) ? 0 : MF_GRAYED)); + break; + case 1: + // If there would be only one sub menu, we add presets directly to the factory menu + { + const int32 numProgs = m_VstPlugin.GetNumPrograms(); + if(GetNumSubMenus(numProgs) <= 1) + { + GeneratePresetMenu(0, m_PresetMenu); + } + } + break; + } + } else if(hSysMenu == m_Menu.GetSubMenu(1)->m_hMenu) + { + // Preset menu + m_currentPresetMenu = nItemID; + GeneratePresetMenu(nItemID * PRESETS_PER_GROUP, *m_presetMenuGroup[nItemID]); + } +} + + +void CAbstractVstEditor::UpdatePresetMenu(bool force) +{ + const int32 numProgs = m_VstPlugin.GetNumPrograms(); + const int32 curProg = m_VstPlugin.GetCurrentProgram(); + + if(m_PresetMenu.m_hMenu) // We rebuild the menu from scratch, so remove any exiting menus... + { + if(curProg == m_nCurProg && !force) // ... unless menu exists and is accurate, in which case we are done. + return; + + m_presetMenuGroup.clear(); + + // If there were no preset groups, delete the remaining content so that it can be refilled dynamically + while(m_PresetMenu.GetMenuItemCount() > 0) + m_PresetMenu.RemoveMenu(0, MF_BYPOSITION); + } else + { + // Create Factory preset menu + m_PresetMenu.CreatePopupMenu(); + m_Menu.InsertMenu(1, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(m_PresetMenu.m_hMenu), _T("&Presets")); + } + + m_Menu.EnableMenuItem(1, MF_BYPOSITION | (numProgs ? 0 : MF_GRAYED)); + + const int numSubMenus = GetNumSubMenus(numProgs); + + if(numSubMenus > 1) + { + // Depending on the plugin and its number of presets, filling the sub menus can take quite a while (e.g. Synth1), + // so we fill the menus only on demand (when they are opened), so that the editor GUI creation doesn't take forever. + m_presetMenuGroup.resize(numSubMenus); + for(int bank = 0, prog = 0; bank < numSubMenus; bank++, prog += PRESETS_PER_GROUP) + { + m_presetMenuGroup[bank] = std::make_unique<CMenu>(); + m_presetMenuGroup[bank]->CreatePopupMenu(); + + CString label; + label.Format(_T("Bank %d (%d-%d)"), bank + 1, prog + 1, std::min(prog + PRESETS_PER_GROUP, numProgs)); + m_PresetMenu.AppendMenu(MF_POPUP + | (bank % 32 == 0 ? MF_MENUBREAK : 0) + | (curProg >= prog && curProg < prog + PRESETS_PER_GROUP ? MF_CHECKED : MF_UNCHECKED), + reinterpret_cast<UINT_PTR>(m_presetMenuGroup[bank]->m_hMenu), label); + } + } + + m_currentPresetMenu = 0; + m_nCurProg = curProg; +} + + +void CAbstractVstEditor::GeneratePresetMenu(int32 offset, CMenu &parent) +{ + const int32 numProgs = m_VstPlugin.GetNumPrograms(); + const int32 curProg = m_VstPlugin.GetCurrentProgram(); + const int32 endProg = std::min(offset + PRESETS_PER_GROUP, numProgs); + + if(parent.GetMenuItemCount() != 0) + { + // Already generated. + return; + } + + m_VstPlugin.CacheProgramNames(offset, endProg); + for(int32 p = offset, row = 0, id = 0; p < endProg; p++, row++, id++) + { + CString programName = m_VstPlugin.GetFormattedProgramName(p); + programName.Replace(_T("&"), _T("&&")); + UINT splitMenuFlag = 0; + + if(row == PRESETS_PER_COLUMN) + { + // Advance to next menu column + row = 0; + splitMenuFlag = MF_MENUBARBREAK; + } + + parent.AppendMenu(MF_STRING | (p == curProg ? MF_CHECKED : MF_UNCHECKED) | splitMenuFlag, ID_PRESET_SET + id, programName); + } +} + + +void CAbstractVstEditor::UpdateInputMenu() +{ + CMenu *pInfoMenu = m_Menu.GetSubMenu(2); + pInfoMenu->DeleteMenu(0, MF_BYPOSITION); + + const CSoundFile &sndFile = m_VstPlugin.GetSoundFile(); + + if(m_InputMenu.m_hMenu) + { + m_InputMenu.DestroyMenu(); + } + if(!m_InputMenu.m_hMenu) + { + m_InputMenu.CreatePopupMenu(); + } + + std::vector<IMixPlugin *> inputPlugs; + m_VstPlugin.GetInputPlugList(inputPlugs); + for(auto plug : inputPlugs) + { + CString name = MPT_CFORMAT("FX{}: {}")(mpt::cfmt::dec0<2>(plug->m_nSlot + 1), mpt::ToCString(plug->m_pMixStruct->GetName())); + m_InputMenu.AppendMenu(MF_STRING, ID_PLUGSELECT + plug->m_nSlot, name); + } + + std::vector<CHANNELINDEX> inputChannels; + m_VstPlugin.GetInputChannelList(inputChannels); + bool addSeparator = !inputPlugs.empty(); + for(auto chn : inputChannels) + { + if(addSeparator) + { + m_InputMenu.AppendMenu(MF_SEPARATOR); + addSeparator = false; + } + CString name = MPT_CFORMAT("Chn{}: {}")(mpt::cfmt::dec0<2>(chn + 1), mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.ChnSettings[chn].szName)); + m_InputMenu.AppendMenu(MF_STRING, NULL, name); + } + + std::vector<INSTRUMENTINDEX> inputInstruments; + m_VstPlugin.GetInputInstrumentList(inputInstruments); + addSeparator = !inputPlugs.empty() || !inputChannels.empty(); + for(auto ins : inputInstruments) + { + if(addSeparator) + { + m_InputMenu.AppendMenu(MF_SEPARATOR); + addSeparator = false; + } + CString name = MPT_CFORMAT("Ins{}: {}")(mpt::cfmt::dec0<2>(ins), mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.GetInstrumentName(ins))); + m_InputMenu.AppendMenu(MF_STRING | ((ins == m_nInstrument) ? MF_CHECKED : 0), ID_SELECTINST + ins, name); + } + + if(inputPlugs.empty() && inputChannels.empty() && inputInstruments.empty()) + { + m_InputMenu.AppendMenu(MF_STRING | MF_GRAYED, NULL, _T("None")); + } + + pInfoMenu->InsertMenu(0, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(m_InputMenu.m_hMenu), _T("I&nputs")); +} + + +void CAbstractVstEditor::UpdateOutputMenu() +{ + CMenu *pInfoMenu = m_Menu.GetSubMenu(2); + pInfoMenu->DeleteMenu(1, MF_BYPOSITION); + + if(m_OutputMenu.m_hMenu) + { + m_OutputMenu.DestroyMenu(); + } + if(!m_OutputMenu.m_hMenu) + { + m_OutputMenu.CreatePopupMenu(); + } + + std::vector<IMixPlugin *> outputPlugs; + m_VstPlugin.GetOutputPlugList(outputPlugs); + CString name; + + for(auto plug : outputPlugs) + { + if(plug != nullptr) + { + name.Format(_T("FX%02d: "), plug->m_nSlot + 1); + name += mpt::ToCString(plug->m_pMixStruct->GetName()); + m_OutputMenu.AppendMenu(MF_STRING, ID_PLUGSELECT + plug->m_nSlot, name); + } else + { + name = _T("Master Output"); + m_OutputMenu.AppendMenu(MF_STRING | MF_GRAYED, NULL, name); + } + } + pInfoMenu->InsertMenu(1, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(m_OutputMenu.m_hMenu), _T("Ou&tputs")); +} + + +void CAbstractVstEditor::UpdateMacroMenu() +{ + CMenu *pInfoMenu = m_Menu.GetSubMenu(2); + pInfoMenu->DeleteMenu(2, MF_BYPOSITION); + + if(m_MacroMenu.m_hMenu) + { + m_MacroMenu.DestroyMenu(); + } + if(!m_MacroMenu.m_hMenu) + { + m_MacroMenu.CreatePopupMenu(); + } + + CString label, macroName; + for(int nMacro = 0; nMacro < kSFxMacros; nMacro++) + { + int action = 0; + UINT greyed = MF_GRAYED; + + const MIDIMacroConfig &midiCfg = m_VstPlugin.GetSoundFile().m_MidiCfg; + + const ParameteredMacro macroType = midiCfg.GetParameteredMacroType(nMacro); + + if(macroType == kSFxUnused) + { + macroName = _T("Unused. Learn Param..."); + action= ID_LEARN_MACRO_FROM_PLUGGUI + nMacro; + greyed = 0; + } else + { + macroName = midiCfg.GetParameteredMacroName(nMacro, &m_VstPlugin); + if(macroType != kSFxPlugParam || macroName.Left(3) != _T("N/A")) + { + greyed = 0; + } + } + + label.Format(_T("SF%X: "), nMacro); + label += macroName; + m_MacroMenu.AppendMenu(MF_STRING | greyed, action, label); + } + + pInfoMenu->InsertMenu(2, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(m_MacroMenu.m_hMenu), _T("&Macros")); +} + +void CAbstractVstEditor::UpdateOptionsMenu() +{ + + if(m_OptionsMenu.m_hMenu) + m_OptionsMenu.DestroyMenu(); + + CInputHandler *ih = CMainFrame::GetInputHandler(); + + m_OptionsMenu.CreatePopupMenu(); + + //Bypass + m_OptionsMenu.AppendMenu(MF_STRING | (m_VstPlugin.IsBypassed() ? MF_CHECKED : 0), + ID_PLUG_BYPASS, ih->GetKeyTextFromCommand(kcVSTGUIBypassPlug, _T("&Bypass Plugin"))); + //Record Params + m_OptionsMenu.AppendMenu(MF_STRING | (m_VstPlugin.m_recordAutomation ? MF_CHECKED : 0), + ID_PLUG_RECORDAUTOMATION, ih->GetKeyTextFromCommand(kcVSTGUIToggleRecordParams, _T("Record &Parameter Changes"))); + //Record MIDI Out + m_OptionsMenu.AppendMenu(MF_STRING | (m_VstPlugin.m_recordMIDIOut ? MF_CHECKED : 0), + ID_PLUG_RECORD_MIDIOUT, ih->GetKeyTextFromCommand(kcVSTGUIToggleRecordMIDIOut, _T("Record &MIDI Out to Pattern Editor"))); + //Pass on keypresses + m_OptionsMenu.AppendMenu(MF_STRING | (m_VstPlugin.m_passKeypressesToPlug ? MF_CHECKED : 0), + ID_PLUG_PASSKEYS, ih->GetKeyTextFromCommand(kcVSTGUIToggleSendKeysToPlug, _T("Pass &Keys to Plugin"))); + + + m_Menu.DeleteMenu(3, MF_BYPOSITION); + m_Menu.InsertMenu(3, MF_BYPOSITION | MF_POPUP, reinterpret_cast<UINT_PTR>(m_OptionsMenu.m_hMenu), _T("&Options")); + +} + + +void CAbstractVstEditor::OnToggleEditor(UINT nID) +{ + CModDoc *pModDoc = m_VstPlugin.GetModDoc(); + + if(pModDoc) + { + pModDoc->TogglePluginEditor(nID - ID_PLUGSELECT); + } +} + + +void CAbstractVstEditor::OnInitMenu(CMenu* /*pMenu*/) +{ + SetupMenu(); +} + + +bool CAbstractVstEditor::CheckInstrument(INSTRUMENTINDEX ins) const +{ + const CSoundFile &sndFile = m_VstPlugin.GetSoundFile(); + + if(ins != INSTRUMENTINDEX_INVALID && ins < MAX_INSTRUMENTS && sndFile.Instruments[ins] != nullptr) + { + return (sndFile.Instruments[ins]->nMixPlug) == (m_VstPlugin.m_nSlot + 1); + } + return false; +} + + +INSTRUMENTINDEX CAbstractVstEditor::GetBestInstrumentCandidate() const +{ + // First try current instrument: + const CModDoc *modDoc = m_VstPlugin.GetModDoc(); + POSITION pos = modDoc->GetFirstViewPosition(); + while(pos != NULL) + { + CModControlView *pView = dynamic_cast<CModControlView *>(modDoc->GetNextView(pos)); + if(pView != nullptr && pView->GetDocument() == modDoc) + { + INSTRUMENTINDEX ins = static_cast<INSTRUMENTINDEX>(pView->GetInstrumentChange()); + if(CheckInstrument(ins)) + return ins; + } + } + + // Just take the first instrument that points to this plug.. + return modDoc->HasInstrumentForPlugin(m_VstPlugin.m_nSlot); +} + + +void CAbstractVstEditor::OnSetInputInstrument(UINT nID) +{ + m_nInstrument = static_cast<INSTRUMENTINDEX>(nID - ID_SELECTINST); +} + + +void CAbstractVstEditor::OnCreateInstrument() +{ + if(m_VstPlugin.GetModDoc() != nullptr) + { + INSTRUMENTINDEX instr = m_VstPlugin.GetModDoc()->InsertInstrumentForPlugin(m_VstPlugin.GetSlot()); + if(instr != INSTRUMENTINDEX_INVALID) m_nInstrument = instr; + } +} + + +void CAbstractVstEditor::PrepareToLearnMacro(UINT nID) +{ + m_nLearnMacro = (nID-ID_LEARN_MACRO_FROM_PLUGGUI); + //Now we wait for a param to be touched. We'll get the message from the VST Plug Manager. + //Then pModDoc->LearnMacro(macro, param) is called +} + +void CAbstractVstEditor::SetLearnMacro(int inMacro) +{ + if (inMacro < kSFxMacros) + { + m_nLearnMacro=inMacro; + } +} + +int CAbstractVstEditor::GetLearnMacro() +{ + return m_nLearnMacro; +} + + +void CAbstractVstEditor::OnMove(int, int) +{ + if(IsWindowVisible()) + { + StoreWindowPos(); + } +} + + +void CAbstractVstEditor::StoreWindowPos() +{ + if(m_hWnd) + { + WINDOWPLACEMENT wnd; + wnd.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(&wnd); + m_VstPlugin.SetEditorPos(wnd.rcNormalPosition.left, wnd.rcNormalPosition.top); + } +} + + +void CAbstractVstEditor::RestoreWindowPos() +{ + // Restore previous editor position + int32 editorX, editorY; + m_VstPlugin.GetEditorPos(editorX, editorY); + + if(editorX != int32_min && editorY != int32_min) + { + WINDOWPLACEMENT wnd; + wnd.length = sizeof(wnd); + GetWindowPlacement(&wnd); + wnd.showCmd = SW_SHOWNOACTIVATE; + CRect rect = wnd.rcNormalPosition; + rect.MoveToXY(editorX, editorY); + wnd.rcNormalPosition = rect; + SetWindowPlacement(&wnd); + } +} + + +#endif // NO_PLUGINS + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.h b/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.h new file mode 100644 index 00000000..62f01a0f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AbstractVstEditor.h @@ -0,0 +1,140 @@ +/* + * AbstractVstEditor.h + * ------------------- + * Purpose: Common plugin editor interface class. This code is shared between custom and default plugin user interfaces. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include <vector> +#include "../soundlib/Snd_defs.h" +#include "Moddoc.h" + +OPENMPT_NAMESPACE_BEGIN + +class IMixPlugin; +struct UpdateHint; + +class CAbstractVstEditor: public CDialog +{ +protected: + CMenu m_Menu; + CMenu m_PresetMenu; + std::vector<std::unique_ptr<CMenu>> m_presetMenuGroup; + CMenu m_InputMenu; + CMenu m_OutputMenu; + CMenu m_MacroMenu; + CMenu m_OptionsMenu; + static UINT m_clipboardFormat; + int32 m_currentPresetMenu = 0; + int32 m_clientHeight; + int m_nLearnMacro = -1; + int m_nCurProg = -1; + INSTRUMENTINDEX m_nInstrument; + bool m_isMinimized = false; + bool m_updateDisplay = false; + CModDoc::NoteToChannelMap m_noteChannel; // Note -> Preview channel assignment + + // Adjust window size if menu bar height changes + class WindowSizeAdjuster + { + CWnd &m_wnd; + int m_menuHeight = 0; + public: + WindowSizeAdjuster(CWnd &wnd); + ~WindowSizeAdjuster(); + }; + +public: + IMixPlugin &m_VstPlugin; + + CAbstractVstEditor(IMixPlugin &plugin); + virtual ~CAbstractVstEditor(); + + void SetupMenu(bool force = false); + void SetTitle(); + void SetLearnMacro(int inMacro); + int GetLearnMacro(); + + void SetPreset(int32 preset); + void UpdatePresetField(); + + afx_msg void OnNcLButtonDblClk(UINT nHitTest, CPoint point); + afx_msg void OnLoadPreset(); + afx_msg void OnSavePreset(); + afx_msg void OnCopyParameters(); + afx_msg void OnPasteParameters(); + afx_msg void OnRandomizePreset(); + afx_msg void OnRenamePlugin(); + afx_msg void OnSetPreset(UINT nID); + afx_msg void OnBypassPlug(); + afx_msg void OnRecordAutomation(); + afx_msg void OnRecordMIDIOut(); + afx_msg void OnPassKeypressesToPlug(); + afx_msg void OnSetPreviousVSTPreset(); + afx_msg void OnSetNextVSTPreset(); + afx_msg void OnVSTPresetBackwardJump(); + afx_msg void OnVSTPresetForwardJump(); + afx_msg void OnVSTPresetRename(); + afx_msg void OnCreateInstrument(); + afx_msg void OnMenuSelect(UINT nItemID, UINT nFlags, HMENU hMenu); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + afx_msg void OnDropFiles(HDROP hDropInfo); + afx_msg void OnMove(int x, int y); + afx_msg void OnClose() { DoClose(); } + + // Overridden methods: + void PostNcDestroy() override; + void OnOK() override { DoClose(); } + void OnCancel() override { DoClose(); } + + virtual bool OpenEditor(CWnd *parent); + virtual void DoClose(); + virtual void UpdateParamDisplays() { if(m_updateDisplay) { SetupMenu(true); m_updateDisplay = false; } } + virtual void UpdateParam(int32 /*param*/) { } + virtual void UpdateView(UpdateHint hint); + + virtual bool IsResizable() const = 0; + virtual bool SetSize(int contentWidth, int contentHeight) = 0; + + void UpdateDisplay() { m_updateDisplay = true; } + + DECLARE_MESSAGE_MAP() + +protected: + BOOL PreTranslateMessage(MSG *msg) override; + bool HandleKeyMessage(MSG &msg); + void UpdatePresetMenu(bool force = false); + void GeneratePresetMenu(int32 offset, CMenu &parent); + void UpdateInputMenu(); + void UpdateOutputMenu(); + void UpdateMacroMenu(); + void UpdateOptionsMenu(); + INSTRUMENTINDEX GetBestInstrumentCandidate() const; + bool CheckInstrument(INSTRUMENTINDEX ins) const; + bool ValidateCurrentInstrument(); + + void OnToggleEditor(UINT nID); + void OnSetInputInstrument(UINT nID); + afx_msg void OnInitMenu(CMenu* pMenu); + void PrepareToLearnMacro(UINT nID); + + void OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized); + + void StoreWindowPos(); + void RestoreWindowPos(); + +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.cpp new file mode 100644 index 00000000..101a148d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.cpp @@ -0,0 +1,330 @@ +/* + * AdvancedConfigDlg.cpp + * --------------------- + * Purpose: Implementation of the advanced settings dialog. + * 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 "Mainfrm.h" +#include "AdvancedConfigDlg.h" +#include "Settings.h" +#include "dlg_misc.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(COptionsAdvanced, CPropertyPage) + ON_NOTIFY(NM_DBLCLK, IDC_LIST1, &COptionsAdvanced::OnOptionDblClick) +#ifndef MPT_MFC_FULL + ON_NOTIFY(NM_CUSTOMDRAW, IDC_LIST1, &COptionsAdvanced::OnCustomDrawList) +#endif + ON_EN_CHANGE(IDC_EDIT1, &COptionsAdvanced::OnFindStringChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsAdvanced::OnSaveNow) +END_MESSAGE_MAP() + +void COptionsAdvanced::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CModTypeDlg) + DDX_Control(pDX, IDC_LIST1, m_List); + //}}AFX_DATA_MAP +} + + +BOOL COptionsAdvanced::PreTranslateMessage(MSG *msg) +{ + if(msg->message == WM_KEYDOWN && msg->wParam == VK_RETURN) + { + OnOptionDblClick(nullptr, nullptr); + return TRUE; + } + return FALSE; +} + + +BOOL COptionsAdvanced::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + m_List.SetExtendedStyle(m_List.GetExtendedStyle() | LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT); + + ListView_EnableGroupView(m_List.m_hWnd, FALSE); // try to set known state + int enableGroupsResult1 = static_cast<int>(ListView_EnableGroupView(m_List.m_hWnd, TRUE)); + int enableGroupsResult2 = static_cast<int>(ListView_EnableGroupView(m_List.m_hWnd, TRUE)); + // Looks like we have to check enabling and check that a second enabling does + // not change anything. + // Just checking if enabling fails with -1 does not work for older control + // versions because they just do not know the window message at all and return + // 0, always. At least Wine does behave this way. + if(enableGroupsResult1 == 1 && enableGroupsResult2 == 0) + { + m_listGrouped = true; + } else + { + // Did not behave as documented or expected, the actual state of the + // control is unknown by now. + // Play safe and set and assume the traditional ungrouped mode again. + ListView_EnableGroupView(m_List.m_hWnd, FALSE); + m_listGrouped = false; + } + + if(m_listGrouped) + { + static constexpr ListCtrl::Header headers[] = + { + { _T("Setting"), 150, LVCFMT_LEFT }, + { _T("Type"), 40, LVCFMT_LEFT }, + { _T("Value"), 140, LVCFMT_LEFT }, + { _T("Default"), 62, LVCFMT_LEFT }, + }; + m_List.SetHeaders(headers); + } else + { + static constexpr ListCtrl::Header headers[] = + { + { _T("Setting"), 200, LVCFMT_LEFT }, + { _T("Type"), 40, LVCFMT_LEFT }, + { _T("Value"), 100, LVCFMT_LEFT }, + { _T("Default"), 52, LVCFMT_LEFT }, + }; + m_List.SetHeaders(headers); + } + + ReInit(); + return TRUE; +} + + +void COptionsAdvanced::ReInit() +{ + m_List.SetRedraw(FALSE); + m_List.DeleteAllItems(); + if(m_listGrouped) + { + ListView_RemoveAllGroups(m_List.m_hWnd); + } + m_List.SetItemCount(static_cast<int>(theApp.GetSettings().size())); + + m_indexToPath.clear(); + m_indexToPath.reserve(theApp.GetSettings().size()); + m_groups.clear(); + int numGroups = 0; + + mpt::ustring findStr = mpt::ToLowerCase(GetWindowTextUnicode(*GetDlgItem(IDC_EDIT1))); + + LVITEMW lvi; + lvi.mask = LVIF_TEXT | LVIF_PARAM; + lvi.mask |= (m_listGrouped ? LVIF_GROUPID : 0); + lvi.iSubItem = 0; + lvi.state = 0; + lvi.stateMask = 0; + lvi.cchTextMax = 0; + lvi.iImage = 0; + lvi.iIndent = 0; + lvi.iGroupId = 0; + + int i = 0; + for(const auto &[path, state] : theApp.GetSettings()) + { + // In MPT_USTRING_MODE_WIDE mode, + // this loop is heavily optimized to avoid as much string copies as possible + // in order to perform ok-ish in debug builds. + // MPT_USTRING_MODE_UTF8 is not optimized as we (currently) do not build in + // this mode by default. + const mpt::ustring §ion = path.GetRefSection(); + const mpt::ustring &key = path.GetRefKey(); + const SettingValue &value = state.GetRefValue(); + const SettingValue &defaultValue = state.GetRefDefault(); + + if(!findStr.empty()) + { + mpt::ustring str = path.FormatAsString() + U_("=") + value.FormatValueAsString(); + str = mpt::ToLowerCase(str); + if(str.find(findStr) == mpt::ustring::npos) + { + continue; + } + } + + int index; + lvi.iItem = i++; + lvi.lParam = m_indexToPath.size(); + + if(m_listGrouped) + { + + auto gi = m_groups.find(section); + if(gi == m_groups.end()) + { + LVGROUP group; + #if _WIN32_WINNT >= 0x0600 + group.cbSize = LVGROUP_V5_SIZE; + #else + group.cbSize = sizeof(group); + #endif + group.mask = LVGF_HEADER | LVGF_GROUPID; +#if MPT_USTRING_MODE_WIDE + group.pszHeader = const_cast<wchar_t *>(section.c_str()); +#else + const std::wstring wsection = mpt::ToWide(section); + group.pszHeader = const_cast<wchar_t *>(wsection.c_str()); +#endif + group.cchHeader = 0; + group.pszFooter = nullptr; + group.cchFooter = 0; + group.iGroupId = lvi.iGroupId = numGroups++; + group.stateMask = LVGS_COLLAPSIBLE; + group.state = LVGS_COLLAPSIBLE; + group.uAlign = LVGA_HEADER_LEFT; + ListView_InsertGroup(m_List.m_hWnd, -1, &group); + m_groups.insert(std::make_pair(section, lvi.iGroupId)); + } else + { + lvi.iGroupId = gi->second; + } + +#if MPT_USTRING_MODE_WIDE + lvi.pszText = const_cast<wchar_t *>(key.c_str()); +#else + const std::wstring wkey = mpt::ToWide(key); + lvi.pszText = const_cast<wchar_t *>(wkey.c_str()); +#endif + index = static_cast<int>(m_List.SendMessage(LVM_INSERTITEMW, 0, (LPARAM)(&lvi))); + + } else + { + + const mpt::ustring sectionAndKey = path.FormatAsString(); +#if MPT_USTRING_MODE_WIDE + lvi.pszText = const_cast<wchar_t *>(sectionAndKey.c_str()); +#else + const std::wstring wsectionAndKey = mpt::ToWide(sectionAndKey); + lvi.pszText = const_cast<wchar_t *>(wsectionAndKey.c_str()); +#endif + index = static_cast<int>(m_List.SendMessage(LVM_INSERTITEMW, 0, (LPARAM)(&lvi))); + + } + +#if MPT_USTRING_MODE_WIDE + m_List.SetItemText(index, 1, value.FormatTypeAsString().c_str()); + m_List.SetItemText(index, 2, value.FormatValueAsString().c_str()); + m_List.SetItemText(index, 3, defaultValue.FormatValueAsString().c_str()); +#else + m_List.SetItemText(index, 1, mpt::ToCString(value.FormatTypeAsString())); + m_List.SetItemText(index, 2, mpt::ToCString(value.FormatValueAsString())); + m_List.SetItemText(index, 3, mpt::ToCString(defaultValue.FormatValueAsString())); +#endif + m_indexToPath.push_back(path); + } + + m_List.SetItemCount(i); + m_List.SetRedraw(TRUE); + m_List.Invalidate(FALSE); +} + + +void COptionsAdvanced::OnOK() +{ + CSoundFile::SetDefaultNoteNames(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->PostMessage(WM_MOD_INVALIDATEPATTERNS, HINT_MPTOPTIONS); + CPropertyPage::OnOK(); +} + + +BOOL COptionsAdvanced::OnSetActive() +{ + ReInit(); + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_ADVANCED; + return CPropertyPage::OnSetActive(); +} + + +#ifdef MPT_MFC_FULL + + +COLORREF CAdvancedSettingsList::OnGetCellBkColor(int nRow, int /* nColumn */ ) +{ + const bool isDefault = theApp.GetSettings().GetMap().find(m_indexToPath[GetItemData(nRow)])->second.IsDefault(); + COLORREF defColor = GetBkColor(); + COLORREF txtColor = GetTextColor(); + COLORREF modColor = RGB(GetRValue(defColor) * 0.9 + GetRValue(txtColor) * 0.1, GetGValue(defColor) * 0.9 + GetGValue(txtColor) * 0.1, GetBValue(defColor) * 0.9 + GetBValue(txtColor) * 0.1); + return isDefault ? defColor : modColor; +} + + +COLORREF CAdvancedSettingsList::OnGetCellTextColor(int nRow, int nColumn) +{ + return CMFCListCtrlEx::OnGetCellTextColor(nRow, nColumn); +} + + +#else // !MPT_MFC_FULL + + +void COptionsAdvanced::OnCustomDrawList(NMHDR* pNMHDR, LRESULT* pResult) +{ + NMLVCUSTOMDRAW *pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR); + *pResult = CDRF_DODEFAULT; + switch(pLVCD->nmcd.dwDrawStage) + { + case CDDS_PREPAINT: + *pResult = CDRF_NOTIFYITEMDRAW; + break; + case CDDS_ITEMPREPAINT: + { + const bool isDefault = theApp.GetSettings().GetMap().find(m_indexToPath[pLVCD->nmcd.lItemlParam])->second.IsDefault(); + COLORREF defColor = m_List.GetBkColor(); + COLORREF txtColor = m_List.GetTextColor(); + COLORREF modColor = RGB(GetRValue(defColor) * 0.9 + GetRValue(txtColor) * 0.1, GetGValue(defColor) * 0.9 + GetGValue(txtColor) * 0.1, GetBValue(defColor) * 0.9 + GetBValue(txtColor) * 0.1); + pLVCD->clrTextBk = isDefault ? defColor : modColor; + } + break; + } +} + + +#endif // MPT_MFC_FULL + + +void COptionsAdvanced::OnOptionDblClick(NMHDR *, LRESULT *) +{ + const int index = m_List.GetSelectionMark(); + if(index < 0) + return; + const SettingPath path = m_indexToPath[m_List.GetItemData(index)]; + SettingValue val = theApp.GetSettings().GetMap().find(path)->second; + if(val.GetType() == SettingTypeBool) + { + val = !val.as<bool>(); + } else + { + CInputDlg inputDlg(this, _T("Enter new value for ") + mpt::ToCString(path.FormatAsString()), mpt::ToCString(val.FormatValueAsString())); + if(inputDlg.DoModal() != IDOK) + { + return; + } + val.SetFromString(inputDlg.resultAsString); + } + theApp.GetSettings().Write(path, val); +#if MPT_USTRING_MODE_WIDE + m_List.SetItemText(index, 2, val.FormatValueAsString().c_str()); +#else + m_List.SetItemText(index, 2, mpt::ToCString(val.FormatValueAsString())); +#endif + m_List.SetSelectionMark(index); + OnSettingsChanged(); +} + + +void COptionsAdvanced::OnSaveNow() +{ + TrackerSettings::Instance().SaveSettings(); + theApp.GetSettings().WriteSettings(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.h new file mode 100644 index 00000000..cd30f085 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AdvancedConfigDlg.h @@ -0,0 +1,86 @@ +/* + * AdvancedConfigDlg.h + * ------------------- + * Purpose: Implementation of the advanced settings dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CListCtrl.h" +#if MPT_USTRING_MODE_WIDE +#include <unordered_map> +#else +#include <map> +#endif + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MPT_MFC_FULL + +class CAdvancedSettingsList : public CMFCListCtrlEx +{ +private: + std::vector<SettingPath> & m_indexToPath; +public: + CAdvancedSettingsList(std::vector<SettingPath> & indexToPath) : m_indexToPath(indexToPath) {} + COLORREF OnGetCellBkColor(int nRow, int nColumn) override; + COLORREF OnGetCellTextColor(int nRow, int nColumn) override; +}; + +#endif // MPT_MFC_FULL + + +class COptionsAdvanced: public CPropertyPage +{ +#ifdef MPT_MFC_FULL + using ListCtrl = CAdvancedSettingsList; +#else // MPT_MFC_FULL + using ListCtrl = CListCtrlEx; +#endif // !MPT_MFC_FULL + +protected: + ListCtrl m_List; +#if MPT_USTRING_MODE_WIDE + using GroupMap = std::unordered_map<mpt::ustring, int>; +#else + using GroupMap = std::map<mpt::ustring, int>; +#endif + std::vector<SettingPath> m_indexToPath; + GroupMap m_groups; + bool m_listGrouped = false; + +public: +#ifdef MPT_MFC_FULL + COptionsAdvanced():CPropertyPage(IDD_OPTIONS_ADVANCED), m_List(m_indexToPath) {} +#else // !MPT_MFC_FULL + COptionsAdvanced():CPropertyPage(IDD_OPTIONS_ADVANCED) {} +#endif // MPT_MFC_FULL + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + BOOL PreTranslateMessage(MSG *msg) override; + + afx_msg void OnOptionDblClick(NMHDR *, LRESULT *); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnFindStringChanged() { ReInit(); } + afx_msg void OnSaveNow(); +#ifndef MPT_MFC_FULL + afx_msg void OnCustomDrawList(NMHDR* pNMHDR, LRESULT* pResult); +#endif // !MPT_MFC_FULL + + void ReInit(); + + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AppendModule.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/AppendModule.cpp new file mode 100644 index 00000000..a7dcf221 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AppendModule.cpp @@ -0,0 +1,316 @@ +/* + * AppendModule.cpp + * ---------------- + * Purpose: Appending one module to an existing module + * 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 "Moddoc.h" +#include "../soundlib/mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + +// Add samples, instruments, plugins and patterns from another module to the current module +void CModDoc::AppendModule(const CSoundFile &source) +{ + const CModSpecifications &specs = m_SndFile.GetModSpecifications(); + // Mappings between old and new indices + std::vector<PLUGINDEX> pluginMapping(MAX_MIXPLUGINS + 1, 0); + std::vector<INSTRUMENTINDEX> instrMapping((source.GetNumInstruments() ? source.GetNumInstruments() : source.GetNumSamples()) + 1, INSTRUMENTINDEX_INVALID); + std::vector<ORDERINDEX> orderMapping; + std::vector<PATTERNINDEX> patternMapping(source.Patterns.GetNumPatterns(), PATTERNINDEX_INVALID); + + /////////////////////////////////////////////////////////////////////////// + // Copy plugins +#ifndef NO_PLUGINS + if(specs.supportsPlugins) + { + PLUGINDEX plug = 0; + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + if(!source.m_MixPlugins[i].IsValidPlugin()) + { + continue; + } + while(plug < MAX_MIXPLUGINS && m_SndFile.m_MixPlugins[plug].IsValidPlugin()) + { + plug++; + } + if(plug < MAX_MIXPLUGINS) + { + ClonePlugin(m_SndFile.m_MixPlugins[plug], source.m_MixPlugins[i]); + pluginMapping[i + 1] = plug + 1; + } else + { + AddToLog("Too many plugins!"); + break; + } + } + // Fix up references between plugins + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + if(pluginMapping[i + 1] != 0 && source.m_MixPlugins[i].GetOutputPlugin() < MAX_MIXPLUGINS) + { + m_SndFile.m_MixPlugins[pluginMapping[i + 1] - 1].SetOutputPlugin(pluginMapping[source.m_MixPlugins[i].GetOutputPlugin() + 1] - 1); + } + } + } +#endif // NO_PLUGINS + + /////////////////////////////////////////////////////////////////////////// + // Copy samples / instruments + if(source.GetNumInstruments() != 0 && m_SndFile.GetNumInstruments() == 0 && specs.instrumentsMax) + { + // Convert to instruments first + ConvertSamplesToInstruments(); + } + + // Check which samples / instruments are actually referenced. + for(const auto &pat : source.Patterns) if(pat.IsValid()) + { + for(const auto &m : pat) + { + if(!m.IsPcNote() && m.instr < instrMapping.size()) instrMapping[m.instr] = 0; + } + } + + if(m_SndFile.GetNumInstruments()) + { + INSTRUMENTINDEX targetIns = 0; + if(source.GetNumInstruments()) + { + for(INSTRUMENTINDEX i = 1; i <= source.GetNumInstruments(); i++) if(source.Instruments[i] != nullptr && !instrMapping[i]) + { + targetIns = m_SndFile.GetNextFreeInstrument(targetIns + 1); + if(targetIns == INSTRUMENTINDEX_INVALID) + { + AddToLog("Too many instruments!"); + break; + } + if(m_SndFile.ReadInstrumentFromSong(targetIns, source, i)) + { + ModInstrument *ins = m_SndFile.Instruments[targetIns]; + if(ins->nMixPlug <= MAX_MIXPLUGINS) + { + ins->nMixPlug = pluginMapping[ins->nMixPlug]; + } + instrMapping[i] = targetIns; + } + } + } else + { + SAMPLEINDEX targetSmp = 0; + for(SAMPLEINDEX i = 1; i <= source.GetNumSamples(); i++) if(!instrMapping[i]) + { + targetIns = m_SndFile.GetNextFreeInstrument(targetIns + 1); + targetSmp = m_SndFile.GetNextFreeSample(targetIns, targetSmp + 1); + if(targetIns == INSTRUMENTINDEX_INVALID) + { + AddToLog("Too many instruments!"); + break; + } else if(targetSmp == SAMPLEINDEX_INVALID) + { + AddToLog("Too many samples!"); + break; + } + if(m_SndFile.AllocateInstrument(targetIns, targetSmp) != nullptr) + { + m_SndFile.ReadSampleFromSong(targetSmp, source, i); + } + instrMapping[i] = targetIns; + } + } + } else + { + SAMPLEINDEX targetSmp = 0; + if(source.GetNumInstruments()) + { + for(INSTRUMENTINDEX i = 1; i <= source.GetNumInstruments(); i++) if(source.Instruments[i] != nullptr && !instrMapping[i]) + { + targetSmp = m_SndFile.GetNextFreeSample(INSTRUMENTINDEX_INVALID, targetSmp + 1); + if(targetSmp == SAMPLEINDEX_INVALID) + { + AddToLog("Too many samples!"); + break; + } + m_SndFile.ReadSampleFromSong(targetSmp, source, source.Instruments[i]->Keyboard[NOTE_MIDDLEC - NOTE_MIN]); + instrMapping[i] = targetSmp; + } + } else + { + for(SAMPLEINDEX i = 1; i <= source.GetNumSamples(); i++) if(!instrMapping[i]) + { + targetSmp = m_SndFile.GetNextFreeSample(INSTRUMENTINDEX_INVALID, targetSmp + 1); + if(targetSmp == SAMPLEINDEX_INVALID) + { + AddToLog("Too many samples!"); + break; + } + m_SndFile.ReadSampleFromSong(targetSmp, source, i); + instrMapping[i] = targetSmp; + } + } + } + + /////////////////////////////////////////////////////////////////////////// + // Copy order lists + const bool useOrderMapping = source.Order.GetNumSequences() == 1; + for(auto &srcOrder : source.Order) + { + ORDERINDEX insertPos = 0; + if(m_SndFile.Order.GetNumSequences() < specs.sequencesMax) + { + m_SndFile.Order.AddSequence(); + m_SndFile.Order().SetName(srcOrder.GetName()); + } else + { + insertPos = m_SndFile.Order().GetLengthTailTrimmed(); + if(specs.hasStopIndex) + insertPos++; + } + const ORDERINDEX ordLen = srcOrder.GetLengthTailTrimmed(); + if(useOrderMapping) orderMapping.resize(ordLen, ORDERINDEX_INVALID); + for(ORDERINDEX ord = 0; ord < ordLen; ord++) + { + if(insertPos >= specs.ordersMax) + { + AddToLog("Too many order items!"); + break; + } + + PATTERNINDEX insertPat = PATTERNINDEX_INVALID; + PATTERNINDEX srcPat = srcOrder[ord]; + if(source.Patterns.IsValidPat(srcPat) && srcPat < patternMapping.size()) + { + if(patternMapping[srcPat] == PATTERNINDEX_INVALID && source.Patterns.IsValidPat(srcPat)) + { + patternMapping[srcPat] = InsertPattern(Clamp(source.Patterns[srcPat].GetNumRows(), specs.patternRowsMin, specs.patternRowsMax)); + if(patternMapping[srcPat] == PATTERNINDEX_INVALID) + { + AddToLog("Too many patterns!"); + break; + } + } + if(patternMapping[srcPat] == PATTERNINDEX_INVALID) + { + continue; + } + insertPat = patternMapping[srcPat]; + } else if(srcPat == srcOrder.GetIgnoreIndex() && specs.hasIgnoreIndex) + { + insertPat = m_SndFile.Order.GetIgnoreIndex(); + } else if(srcPat == srcOrder.GetInvalidPatIndex() && specs.hasStopIndex) + { + insertPat = m_SndFile.Order.GetInvalidPatIndex(); + } else + { + continue; + } + m_SndFile.Order().insert(insertPos, 1, insertPat); + if(useOrderMapping) orderMapping[ord] = insertPos++; + } + } + + /////////////////////////////////////////////////////////////////////////// + // Adjust number of channels + if(source.GetNumChannels() > m_SndFile.GetNumChannels()) + { + CHANNELINDEX newChn = source.GetNumChannels(); + if(newChn > specs.channelsMax) + { + AddToLog("Too many channels!"); + newChn = specs.channelsMax; + } + if(newChn > m_SndFile.GetNumChannels()) + { + ChangeNumChannels(newChn, false); + } + } + + /////////////////////////////////////////////////////////////////////////// + // Copy patterns + const bool tempoSwingDiffers = source.m_tempoSwing != m_SndFile.m_tempoSwing; + const bool timeSigDiffers = source.m_nDefaultRowsPerBeat != m_SndFile.m_nDefaultRowsPerBeat || source.m_nDefaultRowsPerMeasure != m_SndFile.m_nDefaultRowsPerMeasure; + const CHANNELINDEX copyChannels = std::min(m_SndFile.GetNumChannels(), source.GetNumChannels()); + for(PATTERNINDEX pat = 0; pat < patternMapping.size(); pat++) + { + if(patternMapping[pat] == PATTERNINDEX_INVALID) + { + continue; + } + + const CPattern &sourcePat = source.Patterns[pat]; + CPattern &targetPat = m_SndFile.Patterns[patternMapping[pat]]; + + if(specs.hasPatternNames) + { + targetPat.SetName(sourcePat.GetName()); + } + if(specs.hasPatternSignatures) + { + if(sourcePat.GetOverrideSignature()) + { + targetPat.SetSignature(sourcePat.GetRowsPerBeat(), sourcePat.GetRowsPerMeasure()); + } else if(timeSigDiffers) + { + // Try fixing differing signature settings by copying them to the newly created patterns + targetPat.SetSignature(source.m_nDefaultRowsPerBeat, source.m_nDefaultRowsPerMeasure); + } + } + if(m_SndFile.m_nTempoMode == TempoMode::Modern) + { + // Swing only works in modern tempo mode + if(sourcePat.HasTempoSwing()) + { + targetPat.SetTempoSwing(sourcePat.GetTempoSwing()); + } else if(tempoSwingDiffers) + { + // Try fixing differing swing settings by copying them to the newly created patterns + targetPat.SetSignature(source.m_nDefaultRowsPerBeat, source.m_nDefaultRowsPerMeasure); + targetPat.SetTempoSwing(source.m_tempoSwing); + } + } + + const ROWINDEX copyRows = std::min(sourcePat.GetNumRows(), targetPat.GetNumRows()); + for(ROWINDEX row = 0; row < copyRows; row++) + { + const ModCommand *src = sourcePat.GetRow(row); + ModCommand *m = targetPat.GetRow(row); + for(CHANNELINDEX chn = 0; chn < copyChannels; chn++, src++, m++) + { + *m = *src; + m->Convert(source.GetType(), m_SndFile.GetType(), source); + if(m->IsPcNote()) + { + if(m->instr && m->instr < pluginMapping.size()) m->instr = static_cast<ModCommand::INSTR>(pluginMapping[m->instr]); + } else + { + if(m->instr && m->instr < instrMapping.size()) m->instr = static_cast<ModCommand::INSTR>(instrMapping[m->instr]); + + if(m->command == CMD_POSITIONJUMP && m->param < orderMapping.size()) + { + if(orderMapping[m->param] == ORDERINDEX_INVALID) + { + m->command = CMD_NONE; + } else + { + m->param = static_cast<ModCommand::PARAM>(orderMapping[m->param]); + } + } + } + } + } + if(copyRows < targetPat.GetNumRows()) + { + // If source pattern was smaller, write pattern break effect. + targetPat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(copyRows - 1).RetryNextRow()); + } + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.cpp new file mode 100644 index 00000000..c6eeac6d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.cpp @@ -0,0 +1,190 @@ +/* + * AutoSaver.cpp + * ------------- + * Purpose: Class for automatically saving open modules at a specified interval. + * 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 "Moddoc.h" +#include "AutoSaver.h" +#include "FileDialog.h" +#include "FolderScanner.h" +#include "resource.h" +#include "../soundlib/mod_specifications.h" +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +CAutoSaver::CAutoSaver() + : m_lastSave(timeGetTime()) +{ +} + + +bool CAutoSaver::IsEnabled() const +{ + return TrackerSettings::Instance().AutosaveEnabled; +} + +bool CAutoSaver::GetUseOriginalPath() const +{ + return TrackerSettings::Instance().AutosaveUseOriginalPath; +} + +mpt::PathString CAutoSaver::GetPath() const +{ + return TrackerSettings::Instance().AutosavePath.GetDefaultDir(); +} + +uint32 CAutoSaver::GetHistoryDepth() const +{ + return TrackerSettings::Instance().AutosaveHistoryDepth; +} + +uint32 CAutoSaver::GetSaveInterval() const +{ + return TrackerSettings::Instance().AutosaveIntervalMinutes; +} + + +bool CAutoSaver::DoSave(DWORD curTime) +{ + bool success = true; + + //If time to save and not already having save in progress. + if (CheckTimer(curTime) && !m_saveInProgress) + { + m_saveInProgress = true; + + theApp.BeginWaitCursor(); //display hour glass + + for(auto &modDoc : theApp.GetOpenDocuments()) + { + if(modDoc->ModifiedSinceLastAutosave()) + { + if(SaveSingleFile(*modDoc)) + { + CleanUpBackups(*modDoc); + } else + { + TrackerSettings::Instance().AutosaveEnabled = false; + Reporting::Warning("Warning: Auto Save failed and has been disabled. Please:\n- Review your Auto Save paths\n- Check available disk space and filesystem access rights"); + success = false; + } + } + } + + m_lastSave = timeGetTime(); + theApp.EndWaitCursor(); // End display hour glass + m_saveInProgress = false; + } + + return success; +} + + +bool CAutoSaver::CheckTimer(DWORD curTime) const +{ + return (curTime - m_lastSave) >= GetSaveIntervalMilliseconds(); +} + + +mpt::PathString CAutoSaver::GetBasePath(const CModDoc &modDoc, bool createPath) const +{ + mpt::PathString path; + if(GetUseOriginalPath()) + { + if(modDoc.m_bHasValidPath && !(path = modDoc.GetPathNameMpt()).empty()) + { + // File has a user-chosen path - remove filename + path = path.GetPath(); + } else + { + // if it doesn't, put it in settings dir + path = theApp.GetConfigPath() + P_("Autosave\\"); + if(createPath && !CreateDirectory(path.AsNative().c_str(), nullptr) && GetLastError() == ERROR_PATH_NOT_FOUND) + path = theApp.GetConfigPath(); + else if(!createPath && !path.IsDirectory()) + path = theApp.GetConfigPath(); + } + } else + { + path = GetPath(); + } + return path.EnsureTrailingSlash(); +} + + +mpt::PathString CAutoSaver::GetBaseName(const CModDoc &modDoc) const +{ + return mpt::PathString::FromCString(modDoc.GetTitle()).SanitizeComponent(); +} + + +mpt::PathString CAutoSaver::BuildFileName(const CModDoc &modDoc) const +{ + mpt::PathString name = GetBasePath(modDoc, true) + GetBaseName(modDoc); + const CString timeStamp = CTime::GetCurrentTime().Format(_T(".AutoSave.%Y%m%d.%H%M%S.")); + name += mpt::PathString::FromCString(timeStamp); //append backtup tag + timestamp + name += mpt::PathString::FromUTF8(modDoc.GetSoundFile().GetModSpecifications().fileExtension); + return name; +} + + +bool CAutoSaver::SaveSingleFile(CModDoc &modDoc) +{ + // We do not call CModDoc::DoSave as this populates the Recent Files + // list with backups... hence we have duplicated code.. :( + CSoundFile &sndFile = modDoc.GetSoundFile(); + + mpt::PathString fileName = BuildFileName(modDoc); + + // We are actually not going to show the log for autosaved files. + ScopedLogCapturer logcapturer(modDoc, _T(""), nullptr, false); + + bool success = false; + mpt::ofstream f(fileName, std::ios::binary); + if(f) + { + switch(modDoc.GetSoundFile().GetBestSaveFormat()) + { + case MOD_TYPE_MOD: success = sndFile.SaveMod(f); break; + case MOD_TYPE_S3M: success = sndFile.SaveS3M(f); break; + case MOD_TYPE_XM: success = sndFile.SaveXM(f); break; + case MOD_TYPE_IT: success = sndFile.SaveIT(f, fileName); break; + case MOD_TYPE_MPT: success = sndFile.SaveIT(f, fileName); break; + } + } + + return success; +} + + +void CAutoSaver::CleanUpBackups(const CModDoc &modDoc) const +{ + // Find all autosave files for this document, and delete the oldest ones if there are more than the user wants. + std::vector<mpt::PathString> foundfiles; + FolderScanner scanner(GetBasePath(modDoc, false), FolderScanner::kOnlyFiles, GetBaseName(modDoc) + P_(".AutoSave.*")); + mpt::PathString fileName; + while(scanner.Next(fileName)) + { + foundfiles.push_back(std::move(fileName)); + } + std::sort(foundfiles.begin(), foundfiles.end()); + size_t filesToDelete = std::max(static_cast<size_t>(GetHistoryDepth()), foundfiles.size()) - GetHistoryDepth(); + for(size_t i = 0; i < filesToDelete; i++) + { + DeleteFile(foundfiles[i].AsNative().c_str()); + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.h b/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.h new file mode 100644 index 00000000..42f7b74d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/AutoSaver.h @@ -0,0 +1,51 @@ +/* + * AutoSaver.h + * ----------- + * Purpose: Class for automatically saving open modules at a specified interval. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; + +class CAutoSaver +{ +public: + CAutoSaver(); + + bool DoSave(DWORD curTime); + + bool IsEnabled() const; + bool GetUseOriginalPath() const; + mpt::PathString GetPath() const; + uint32 GetHistoryDepth() const; + uint32 GetSaveInterval() const; + uint32 GetSaveIntervalMilliseconds() const + { + return Clamp(GetSaveInterval(), 0u, (1u << 30) / 60u / 1000u) * 60 * 1000; + } + +private: + bool SaveSingleFile(CModDoc &modDoc); + mpt::PathString GetBasePath(const CModDoc &modDoc, bool createPath) const; + mpt::PathString GetBaseName(const CModDoc &modDoc) const; + mpt::PathString BuildFileName(const CModDoc &modDoc) const; + void CleanUpBackups(const CModDoc &modDoc) const; + bool CheckTimer(DWORD curTime) const; + + DWORD m_lastSave = 0; + //Flag to prevent autosave from starting new saving if previous is still in progress. + bool m_saveInProgress = false; + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.cpp new file mode 100644 index 00000000..41e49be7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.cpp @@ -0,0 +1,414 @@ +/* + * Autotune.cpp + * ------------ + * Purpose: Class for tuning a sample to a given base note automatically. + * 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 "Autotune.h" +#include <math.h> +#include "../common/misc_util.h" +#include "../soundlib/Sndfile.h" +#include <algorithm> +#include <execution> +#include <numeric> +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) +#include <emmintrin.h> +#endif + + +OPENMPT_NAMESPACE_BEGIN + + +// The more bins, the more autocorrelations are done and the more precise the result is. +#define BINS_PER_NOTE 32 +#define MIN_SAMPLE_LENGTH 2 + +#define START_NOTE (24 * BINS_PER_NOTE) // C-2 +#define END_NOTE (96 * BINS_PER_NOTE) // C-8 +#define HISTORY_BINS (12 * BINS_PER_NOTE) // One octave + + +static double FrequencyToNote(double freq, double pitchReference) +{ + return ((12.0 * (log(freq / (pitchReference / 2.0)) / log(2.0))) + 57.0); +} + + +static double NoteToFrequency(double note, double pitchReference) +{ + return pitchReference * pow(2.0, (note - 69.0) / 12.0); +} + + +// Calculate the amount of samples for autocorrelation shifting for a given note +static SmpLength NoteToShift(uint32 sampleFreq, int note, double pitchReference) +{ + const double fundamentalFrequency = NoteToFrequency((double)note / BINS_PER_NOTE, pitchReference); + return std::max(mpt::saturate_round<SmpLength>((double)sampleFreq / fundamentalFrequency), SmpLength(1)); +} + + +// Create an 8-Bit sample buffer with loop unrolling and mono conversion for autocorrelation. +template <class T> +void Autotune::CopySamples(const T* origSample, SmpLength sampleLoopStart, SmpLength sampleLoopEnd) +{ + const uint8 channels = m_sample.GetNumChannels(); + sampleLoopStart *= channels; + sampleLoopEnd *= channels; + + for(SmpLength i = 0, pos = 0; i < m_sampleLength; i++, pos += channels) + { + if(pos >= sampleLoopEnd) + { + pos = sampleLoopStart; + } + + const T* smp = origSample + pos; + + int32 data = 0; // More than enough for 256 channels... :) + for(uint8 chn = 0; chn < channels; chn++) + { + // We only want the MSB. + data += static_cast<int32>(smp[chn] >> ((sizeof(T) - 1) * 8)); + } + + data /= channels; + + m_sampleData[i] = static_cast<int16>(data); + } +} + + +// Prepare a sample buffer for autocorrelation +bool Autotune::PrepareSample(SmpLength maxShift) +{ + + // Determine which parts of the sample should be examined. + SmpLength sampleOffset = 0, sampleLoopStart = 0, sampleLoopEnd = m_sample.nLength; + if(m_selectionEnd >= sampleLoopStart + MIN_SAMPLE_LENGTH) + { + // A selection has been specified: Examine selection + sampleOffset = m_selectionStart; + sampleLoopStart = 0; + sampleLoopEnd = m_selectionEnd - m_selectionStart; + } else if(m_sample.uFlags[CHN_SUSTAINLOOP] && m_sample.nSustainEnd >= m_sample.nSustainStart + MIN_SAMPLE_LENGTH) + { + // A sustain loop is set: Examine sample up to sustain loop and, if necessary, execute the loop several times + sampleOffset = 0; + sampleLoopStart = m_sample.nSustainStart; + sampleLoopEnd = m_sample.nSustainEnd; + } else if(m_sample.uFlags[CHN_LOOP] && m_sample.nLoopEnd >= m_sample.nLoopStart + MIN_SAMPLE_LENGTH) + { + // A normal loop is set: Examine sample up to loop and, if necessary, execute the loop several times + sampleOffset = 0; + sampleLoopStart = m_sample.nLoopStart; + sampleLoopEnd = m_sample.nLoopEnd; + } + + // We should analyse at least a one second (= GetSampleRate() samples) long sample. + m_sampleLength = std::max(sampleLoopEnd, static_cast<SmpLength>(m_sample.GetSampleRate(m_modType))) + maxShift; + m_sampleLength = (m_sampleLength + 7) & ~7; + + if(m_sampleData != nullptr) + { + delete[] m_sampleData; + } + m_sampleData = new int16[m_sampleLength]; + if(m_sampleData == nullptr) + { + return false; + } + + // Copy sample over. + switch(m_sample.GetElementarySampleSize()) + { + case 1: + CopySamples(m_sample.sample8() + sampleOffset * m_sample.GetNumChannels(), sampleLoopStart, sampleLoopEnd); + return true; + + case 2: + CopySamples(m_sample.sample16() + sampleOffset * m_sample.GetNumChannels(), sampleLoopStart, sampleLoopEnd); + return true; + } + + return false; + +} + + +bool Autotune::CanApply() const +{ + return (m_sample.HasSampleData() && m_sample.nLength >= MIN_SAMPLE_LENGTH) || m_sample.uFlags[CHN_ADLIB]; +} + + +namespace +{ + + +struct AutotuneHistogramEntry +{ + int index; + uint64 sum; +}; + +struct AutotuneHistogram +{ + std::array<uint64, HISTORY_BINS> histogram{}; +}; + +struct AutotuneContext +{ + const int16 *m_sampleData; + double pitchReference; + SmpLength processLength; + uint32 sampleFreq; +}; + +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) + +static inline AutotuneHistogramEntry CalculateNoteHistogramSSE2(int note, AutotuneContext ctx) +{ + const SmpLength autocorrShift = NoteToShift(ctx.sampleFreq, note, ctx.pitchReference); + uint64 autocorrSum = 0; + { + const __m128i *normalData = reinterpret_cast<const __m128i *>(ctx.m_sampleData); + const __m128i *shiftedData = reinterpret_cast<const __m128i *>(ctx.m_sampleData + autocorrShift); + for(SmpLength i = ctx.processLength / 8; i != 0; i--) + { + __m128i normal = _mm_loadu_si128(normalData++); + __m128i shifted = _mm_loadu_si128(shiftedData++); + __m128i diff = _mm_sub_epi16(normal, shifted); // 8 16-bit differences + __m128i squares = _mm_madd_epi16(diff, diff); // Multiply and add: 4 32-bit squares + + __m128i sum1 = _mm_shuffle_epi32(squares, _MM_SHUFFLE(0, 1, 2, 3)); // Move upper two integers to lower + __m128i sum2 = _mm_add_epi32(squares, sum1); // Now we can add the (originally) upper two and lower two integers + __m128i sum3 = _mm_shuffle_epi32(sum2, _MM_SHUFFLE(1, 1, 1, 1)); // Move the second-lowest integer to lowest position + __m128i sum4 = _mm_add_epi32(sum2, sum3); // Add the two lowest positions + autocorrSum += _mm_cvtsi128_si32(sum4); + } + } + return {note % HISTORY_BINS, autocorrSum}; +} + +#endif + +static inline AutotuneHistogramEntry CalculateNoteHistogram(int note, AutotuneContext ctx) +{ + const SmpLength autocorrShift = NoteToShift(ctx.sampleFreq, note, ctx.pitchReference); + uint64 autocorrSum = 0; + { + const int16 *normalData = ctx.m_sampleData; + const int16 *shiftedData = ctx.m_sampleData + autocorrShift; + // Add up squared differences of all values + for(SmpLength i = ctx.processLength; i != 0; i--, normalData++, shiftedData++) + { + autocorrSum += (*normalData - *shiftedData) * (*normalData - *shiftedData); + } + } + return {note % HISTORY_BINS, autocorrSum}; +} + + +static inline AutotuneHistogram operator+(AutotuneHistogram a, AutotuneHistogram b) noexcept +{ + AutotuneHistogram result; + for(std::size_t i = 0; i < HISTORY_BINS; ++i) + { + result.histogram[i] = a.histogram[i] + b.histogram[i]; + } + return result; +} + + +static inline AutotuneHistogram & operator+=(AutotuneHistogram &a, AutotuneHistogram b) noexcept +{ + for(std::size_t i = 0; i < HISTORY_BINS; ++i) + { + a.histogram[i] += b.histogram[i]; + } + return a; +} + + +static inline AutotuneHistogram &operator+=(AutotuneHistogram &a, AutotuneHistogramEntry b) noexcept +{ + a.histogram[b.index] += b.sum; + return a; +} + + +struct AutotuneHistogramReduce +{ + inline AutotuneHistogram operator()(AutotuneHistogram a, AutotuneHistogram b) noexcept + { + return a + b; + } + inline AutotuneHistogram operator()(AutotuneHistogramEntry a, AutotuneHistogramEntry b) noexcept + { + AutotuneHistogram result; + result += a; + result += b; + return result; + } + inline AutotuneHistogram operator()(AutotuneHistogramEntry a, AutotuneHistogram b) noexcept + { + b += a; + return b; + } + inline AutotuneHistogram operator()(AutotuneHistogram a, AutotuneHistogramEntry b) noexcept + { + a += b; + return a; + } +}; + + +} // local + + + +bool Autotune::Apply(double pitchReference, int targetNote) +{ + if(!CanApply()) + { + return false; + } + + const uint32 sampleFreq = m_sample.GetSampleRate(m_modType); + // At the lowest frequency, we get the highest autocorrelation shift amount. + const SmpLength maxShift = NoteToShift(sampleFreq, START_NOTE, pitchReference); + if(!PrepareSample(maxShift)) + { + return false; + } + // We don't process the autocorrelation overhead. + const SmpLength processLength = m_sampleLength - maxShift; + + AutotuneContext ctx; + ctx.m_sampleData = m_sampleData; + ctx.pitchReference = pitchReference; + ctx.processLength = processLength; + ctx.sampleFreq = sampleFreq; + + // Note that we cannot use a fake integer iterator here because of the requirement on ForwardIterator to return a reference to the elements. + std::array<int, END_NOTE - START_NOTE> notes; + std::iota(notes.begin(), notes.end(), START_NOTE); + + AutotuneHistogram autocorr = +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) + (CPU::HasFeatureSet(CPU::feature::sse2)) ? std::transform_reduce(std::execution::par_unseq, std::begin(notes), std::end(notes), AutotuneHistogram{}, AutotuneHistogramReduce{}, [ctx](int note) { return CalculateNoteHistogramSSE2(note, ctx); } ) : +#endif + std::transform_reduce(std::execution::par_unseq, std::begin(notes), std::end(notes), AutotuneHistogram{}, AutotuneHistogramReduce{}, [ctx](int note) { return CalculateNoteHistogram(note, ctx); } ); + + // Interpolate the histogram... + AutotuneHistogram interpolated; + for(int i = 0; i < HISTORY_BINS; i++) + { + interpolated.histogram[i] = autocorr.histogram[i]; + const int kernelWidth = 4; + for(int ki = kernelWidth; ki >= 0; ki--) + { + // Choose bins to interpolate with + int left = i - ki; + if(left < 0) left += HISTORY_BINS; + int right = i + ki; + if(right >= HISTORY_BINS) right -= HISTORY_BINS; + + interpolated.histogram[i] = interpolated.histogram[i] / 2 + (autocorr.histogram[left] + autocorr.histogram[right]) / 2; + } + } + + // ...and find global minimum + int minimumBin = static_cast<int>(std::min_element(std::begin(interpolated.histogram), std::end(interpolated.histogram)) - std::begin(interpolated.histogram)); + + // Center target notes around C + if(targetNote >= 6) + { + targetNote -= 12; + } + + // Center bins around target note + minimumBin -= targetNote * BINS_PER_NOTE; + if(minimumBin >= 6 * BINS_PER_NOTE) + { + minimumBin -= 12 * BINS_PER_NOTE; + } + minimumBin += targetNote * BINS_PER_NOTE; + + const double newFundamentalFreq = NoteToFrequency(static_cast<double>(69 - targetNote) + static_cast<double>(minimumBin) / BINS_PER_NOTE, pitchReference); + + if(const auto newFreq = mpt::saturate_round<uint32>(sampleFreq * pitchReference / newFundamentalFreq); newFreq != sampleFreq) + m_sample.nC5Speed = newFreq; + else + return false; + + if((m_modType & (MOD_TYPE_XM | MOD_TYPE_MOD))) + { + m_sample.FrequencyToTranspose(); + if((m_modType & MOD_TYPE_MOD)) + { + m_sample.RelativeTone = 0; + } + } + + return true; +} + + +///////////////////////////////////////////////////////////// +// CAutotuneDlg + +int CAutotuneDlg::m_pitchReference = 440; // Pitch reference in Hz +int CAutotuneDlg::m_targetNote = 0; // Target note (C- = 0, C# = 1, etc...) + +void CAutotuneDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CAutotuneDlg) + DDX_Control(pDX, IDC_COMBO1, m_CbnNoteBox); + //}}AFX_DATA_MAP +} + + +BOOL CAutotuneDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + m_CbnNoteBox.ResetContent(); + for(int note = 0; note < 12; note++) + { + const int item = m_CbnNoteBox.AddString(mpt::ToCString(CSoundFile::GetDefaultNoteName(note))); + m_CbnNoteBox.SetItemData(item, note); + if(note == m_targetNote) + { + m_CbnNoteBox.SetCurSel(item); + } + } + + SetDlgItemInt(IDC_EDIT1, m_pitchReference, FALSE); + + return TRUE; +} + + +void CAutotuneDlg::OnOK() +{ + int pitch = GetDlgItemInt(IDC_EDIT1); + if(pitch <= 0) + { + MessageBeep(MB_ICONWARNING); + return; + } + + CDialog::OnOK(); + m_targetNote = (int)m_CbnNoteBox.GetItemData(m_CbnNoteBox.GetCurSel()); + m_pitchReference = pitch; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.h b/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.h new file mode 100644 index 00000000..80c4de5c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Autotune.h @@ -0,0 +1,81 @@ +/* + * Autotune.h + * ---------- + * Purpose: Class for tuning a sample to a given base note automatically. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/Snd_defs.h" +#include "resource.h" + + +OPENMPT_NAMESPACE_BEGIN + + +struct ModSample; + + +class Autotune +{ +protected: + ModSample &m_sample; + MODTYPE m_modType; + + SmpLength m_selectionStart, m_selectionEnd; + + int16 *m_sampleData = nullptr; + SmpLength m_sampleLength = 0; + +public: + Autotune(ModSample &smp, MODTYPE type, SmpLength selStart, SmpLength selEnd) : m_sample(smp), m_modType(type), m_selectionStart(selStart), m_selectionEnd(selEnd) + { }; + + ~Autotune() + { + delete[] m_sampleData; + } + + bool CanApply() const; + bool Apply(double pitchReference, int targetNote); + +protected: + + template <class T> + void CopySamples(const T* origSample, SmpLength sampleLoopStart, SmpLength sampleLoopEnd); + + bool PrepareSample(SmpLength maxShift); + +}; + + +class CAutotuneDlg : public CDialog +{ +protected: + static int m_pitchReference; // Pitch reference (440Hz by default) + static int m_targetNote; // Note which the sample should be tuned to (C by default) + + CComboBox m_CbnNoteBox; + +public: + CAutotuneDlg(CWnd *parent) : CDialog(IDD_AUTOTUNE, parent) + { }; + + int GetPitchReference() const { return m_pitchReference; } + int GetTargetNote() const { return m_targetNote; } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + void DoDataExchange(CDataExchange* pDX) override; + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.cpp new file mode 100644 index 00000000..74274482 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.cpp @@ -0,0 +1,171 @@ +/* + * BuildVariants.cpp + * ----------------- + * Purpose: Handling of various OpenMPT build variants. + * 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 "BuildVariants.h" +#include "../common/version.h" +#include "../misc/mptOS.h" +#include "../misc/mptCPU.h" +#include "Mptrack.h" + + +OPENMPT_NAMESPACE_BEGIN + + +bool BuildVariants::IsKnownSystem() +{ + return false + || mpt::OS::Windows::IsOriginal() + || (mpt::OS::Windows::IsWine() && theApp.GetWineVersion()->Version().IsValid()) + ; +} + + +BuildVariants::Variants BuildVariants::GetBuildVariant() +{ +#if defined(MPT_BUILD_RETRO) + return Retro; +#else +#if defined(_WIN32_WINNT) +#if (_WIN32_WINNT >= 0x0A00) // Windows 10 + return Standard; +#else + return Legacy; +#endif +#else + return Unknown; +#endif +#endif +} + + +mpt::ustring BuildVariants::GetBuildVariantName(BuildVariants::Variants variant) +{ + mpt::ustring result; + switch(variant) + { + case Standard: + result = U_("Standard"); + break; + case Legacy: + result = U_("Legacy"); + break; + case Retro: + result = U_("Retro"); + break; + case Unknown: + result = U_("Unknown"); + break; + } + return result; +} + + +mpt::ustring BuildVariants::GetBuildVariantDescription(BuildVariants::Variants variant) +{ + mpt::ustring result; + switch(variant) + { + case Standard: + result = U_(""); + break; + case Legacy: + result = U_(""); + break; + case Retro: + result = U_(" RETRO"); + break; + case Unknown: + result = U_(""); + break; + } + return result; +} + + +mpt::ustring BuildVariants::GuessCurrentBuildName() +{ + if(GetBuildVariant() == Unknown) + { + return mpt::ustring(); + } + if(mpt::arch_bits == 64) + { + if(GetBuildVariant() == Standard) + { + return U_("win64"); + } else + { + return U_("win64old"); + } + } else if(mpt::arch_bits == 32) + { + if(GetBuildVariant() == Standard) + { + return U_("win32"); + } else + { + return U_("win32old"); + } + } else + { + return mpt::ustring(); + } +} + + +bool BuildVariants::ProcessorCanRunCurrentBuild() +{ +#ifdef MPT_ENABLE_ARCH_INTRINSICS + if((CPU::Info::Get().AvailableFeatures & CPU::GetMinimumFeatures()) != CPU::GetMinimumFeatures()) + { + return false; + } +#endif // MPT_ENABLE_ARCH_INTRINSICS + return true; +} + + +bool BuildVariants::SystemCanRunCurrentBuild() +{ + if(mpt::OS::Windows::HostCanRun(mpt::OS::Windows::GetHostArchitecture(), mpt::OS::Windows::GetProcessArchitecture()) == mpt::OS::Windows::EmulationLevel::NA) + { + return false; + } +#ifdef MPT_ENABLE_ARCH_INTRINSICS + if((CPU::Info::Get().AvailableFeatures & CPU::GetMinimumFeatures()) != CPU::GetMinimumFeatures()) + { + return false; + } +#endif // MPT_ENABLE_ARCH_INTRINSICS + if(IsKnownSystem()) + { + if(mpt::OS::Windows::IsOriginal()) + { + if(mpt::OS::Windows::Version::Current().IsBefore(mpt::OS::Windows::Version::GetMinimumKernelLevel())) + { + return false; + } + if(mpt::OS::Windows::Version::Current().IsBefore(mpt::OS::Windows::Version::GetMinimumAPILevel())) + { + return false; + } + } else if(mpt::OS::Windows::IsWine()) + { + if(theApp.GetWineVersion()->Version().IsBefore(mpt::OS::Wine::GetMinimumWineVersion())) + { + return false; + } + } + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.h b/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.h new file mode 100644 index 00000000..65ebfcff --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/BuildVariants.h @@ -0,0 +1,45 @@ +/* + * BuildVariants.h + * --------------- + * Purpose: Handling of various OpenMPT build variants. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +class BuildVariants +{ + +public: + + enum Variants { + Standard, + Legacy, + Retro, + Unknown, + }; + + static bool IsKnownSystem(); + + static BuildVariants::Variants GetBuildVariant(); + static mpt::ustring GetBuildVariantName(BuildVariants::Variants variant); + static mpt::ustring GetBuildVariantDescription(BuildVariants::Variants variant); + + static mpt::ustring GuessCurrentBuildName(); + + static bool ProcessorCanRunCurrentBuild(); + static bool SystemCanRunCurrentBuild(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.cpp new file mode 100644 index 00000000..520a5b92 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.cpp @@ -0,0 +1,55 @@ +/* + * CDecimalSupport.cpp + * ------------------- + * Purpose: Various extensions of the CDecimalSupport implementation. + * 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 "Snd_defs.h" +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(CNumberEdit, CEdit) + ON_WM_CHAR() + ON_MESSAGE(WM_PASTE, &CNumberEdit::OnPaste) +END_MESSAGE_MAP() + + +void CNumberEdit::SetTempoValue(const TEMPO &t) +{ + SetFixedValue(t.ToDouble(), 4); +} + + +TEMPO CNumberEdit::GetTempoValue() +{ + double d; + GetDecimalValue(d); + return TEMPO(d); +} + + +void CNumberEdit::OnChar(UINT nChar, UINT nRepCnt, UINT nFlags) +{ + BOOL bHandled = false; + CDecimalSupport<CNumberEdit>::OnChar(0, nChar, 0, bHandled); + if(!bHandled) CEdit::OnChar(nChar , nRepCnt, nFlags); +} + + +LPARAM CNumberEdit::OnPaste(WPARAM wParam, LPARAM lParam) +{ + bool bHandled = false; + CDecimalSupport<CNumberEdit>::OnPaste(0, wParam, lParam, bHandled); + if(!bHandled) + return CEdit::DefWindowProc(WM_PASTE, wParam, lParam); + else + return 0; +} + +OPENMPT_NAMESPACE_END
\ No newline at end of file diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.h b/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.h new file mode 100644 index 00000000..2dc6017b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CDecimalSupport.h @@ -0,0 +1,389 @@ +/* + * CDecimalSupport.h + * ----------------- + * Purpose: Edit field which allows negative and fractional values to be entered + * Notes : Alexander Uckun's original code has been modified a bit to suit our purposes. + * Authors: OpenMPT Devs + * Alexander Uckun + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +/////////////////////////////////////////////////////////////////////////////// +/// \class CDecimalSupport +/// \brief decimal number support for your control +/// \author Alexander Uckun +/// \version 1.0 + +// Copyright (c) 2007 - Alexander Uckun +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright notice, +// this list of conditions and the following disclaimer. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +template <class T, int limit = _CVTBUFSIZE> +class CDecimalSupport +{ +protected: + /// the locale dependant decimal separator + TCHAR m_DecimalSeparator[5]; + /// the locale dependant negative sign + TCHAR m_NegativeSign[6]; + + bool m_allowNegative = true, m_allowFractions = true; + +public: + +#ifdef BEGIN_MSG_MAP + BEGIN_MSG_MAP(CDecimalSupport) + + ALT_MSG_MAP(8) + MESSAGE_HANDLER(WM_CHAR, OnChar) + MESSAGE_HANDLER(WM_PASTE, OnPaste) + END_MSG_MAP() +#endif + + /// \brief Initialize m_DecimalSeparator and m_NegativeSign + /// \remarks calls InitDecimalSeparator and InitNegativeSign + CDecimalSupport() + { + InitDecimalSeparator(); + InitNegativeSign(); + } + + /// \brief sets m_DecimalSeparator + /// \remarks calls GetLocaleInfo with LOCALE_SDECIMAL to set m_DecimalSeparator + /// \param[in] Locale the locale parameter (see GetLocaleInfo) + /// \return the number of TCHARs written to the destination buffer + int InitDecimalSeparator(LCID Locale = LOCALE_USER_DEFAULT) + { + return ::GetLocaleInfo(Locale, LOCALE_SDECIMAL, m_DecimalSeparator, sizeof(m_DecimalSeparator) / sizeof(TCHAR)); + } + + /// \brief sets m_NegativeSign + /// \remarks calls GetLocaleInfo with LOCALE_SNEGATIVESIGN to set m_NegativeSign + /// \param[in] Locale the locale parameter (see GetLocaleInfo) + /// \return the number of TCHARs written to the destination buffer + int InitNegativeSign(LCID Locale = LOCALE_USER_DEFAULT) + { + return ::GetLocaleInfo(Locale, LOCALE_SNEGATIVESIGN, m_NegativeSign, sizeof(m_NegativeSign) / sizeof(TCHAR)); + } + + /// callback for the WM_PASTE message + /// validates the input + /// \param uMsg + /// \param wParam + /// \param lParam + /// \param[out] bHandled true, if the text is a valid number + /// \return 0 + LRESULT OnPaste(UINT /*uMsg*/, WPARAM /*wParam*/, LPARAM /*lParam*/, bool &bHandled) + { + bHandled = false; + int neg_sign = 0; + int dec_point = 0; + + T* pT = static_cast<T*>(this); + int nStartChar; + int nEndChar; + pT->GetSel(nStartChar, nEndChar); + TCHAR buffer[limit]; + pT->GetWindowText(buffer, limit); + + // Check if the text already contains a decimal point + for (TCHAR* x = buffer; *x; ++x) + { + if (x - buffer == nStartChar) x += nEndChar - nStartChar; + if (*x == m_DecimalSeparator[0] || *x == _T('.')) ++dec_point; + if (*x == m_NegativeSign[0] || *x == _T('-')) ++neg_sign; + } + +#ifdef _UNICODE + if (!IsClipboardFormatAvailable(CF_UNICODETEXT)) return 0; +#else + if (!IsClipboardFormatAvailable(CF_TEXT)) return 0; +#endif + + if (!OpenClipboard((HWND) *pT)) return 0; + +#ifdef _UNICODE + HGLOBAL hglb = GetClipboardData(CF_UNICODETEXT); +#else + HGLOBAL hglb = ::GetClipboardData(CF_TEXT); +#endif + if (hglb != NULL) + { + TCHAR *lptstr = static_cast<TCHAR *>(GlobalLock(hglb)); + if (lptstr != nullptr) + { + bHandled = true; + for (TCHAR* s = lptstr; *s; ++s) + { + if ((*s == m_NegativeSign[0] ||*s == _T('-')) && m_allowNegative) + { + + for (TCHAR* t = m_NegativeSign + 1; *t; ++t, ++s) + { + if (*t != *(s+1)) ++neg_sign; + } + + if (neg_sign || nStartChar > 0) + { + bHandled = false; + break; + } + + ++neg_sign; + continue; + } + if ((*s == m_DecimalSeparator[0] || *s == _T('.')) && m_allowFractions) + { + for (TCHAR* t = m_DecimalSeparator + 1; *t ; ++t, ++s) + { + if (*t != *(s+1)) ++dec_point; + } + + if (dec_point) + { + bHandled = false; + break; + } + ++dec_point; + continue; + } + + if (*s == _T('\r')) + { + // Stop at new line + *s = 0; + break; + } + if (*s < _T('0') || *s > _T('9')) + { + bHandled = false; + break; + } + + } + if(bHandled) pT->ReplaceSel(lptstr, true); + + GlobalUnlock(hglb); + + } + } + CloseClipboard(); + return 0; + } + + + /// callback for the WM_CHAR message + /// handles the decimal point and the negative sign keys + /// \param uMsg + /// \param[in] wParam contains the pressed key + /// \param lParam + /// \param[out] bHandled true, if the key press was handled in this function + /// \return 0 + LRESULT OnChar(UINT /*uMsg*/, WPARAM wParam, LPARAM /*lParam*/, BOOL& bHandled) + { + bHandled = false; + if ((static_cast<TCHAR>(wParam) == m_DecimalSeparator[0] || wParam == _T('.')) && m_allowFractions) + { + T* pT = static_cast<T*>(this); + int nStartChar; + int nEndChar; + pT->GetSel(nStartChar, nEndChar); + TCHAR buffer[limit]; + pT->GetWindowText(buffer, limit); + //Verify that the control doesn't already contain a decimal point + for (TCHAR* x = buffer; *x; ++x) + { + if (x - buffer == nStartChar) x += nEndChar - nStartChar; + if (*x == m_DecimalSeparator[0]) return 0; + } + + pT->ReplaceSel(m_DecimalSeparator, true); + bHandled = true; + } + + if ((static_cast<TCHAR>(wParam) == m_NegativeSign[0] || wParam == _T('-')) && m_allowNegative) + { + T* pT = static_cast<T*>(this); + int nStartChar; + int nEndChar; + pT->GetSel(nStartChar, nEndChar); + if (nStartChar) return 0; + + TCHAR buffer[limit]; + pT->GetWindowText(buffer, limit); + //Verify that the control doesn't already contain a negative sign + if (nEndChar == 0 && buffer[0] == m_NegativeSign[0]) return 0; + pT->ReplaceSel(m_NegativeSign, true); + bHandled = true; + } + return 0; + } + + /// converts the controls text to double + /// \param[out] d the converted value + /// \return true on success + bool GetDecimalValue(double& d) const + { + TCHAR szBuff[limit]; + static_cast<const T*>(this)->GetWindowText(szBuff, limit); + return TextToDouble(szBuff, d); + } + + /// converts a string to double + /// \remarks the decimal separator and the negative sign may change in the string + /// \param[in, out] szBuff the string to convert + /// \param[out] d the converted value + /// \return true on success + bool TextToDouble(TCHAR* szBuff, double& d) const + { + //replace the locale dependant separator with . + if (m_DecimalSeparator[0] != _T('.')) + { + for (TCHAR* x = szBuff; *x; ++x) + { + if (*x == m_DecimalSeparator[0]) + { + *x = _T('.'); + break; + } + } + + } + + TCHAR* endPtr; + //replace the negative sign with - + if (szBuff[0] == m_NegativeSign[0]) szBuff[0] = _T('-'); + d = _tcstod(szBuff, &endPtr); + return *endPtr == _T('\0'); + } + + /// sets a number as the controls text + /// \param[in] d the value + /// \param[in] count digits after the decimal point + void SetFixedValue(double d, int count) + { + int decimal_pos; + int sign; + char digits[limit]; + _fcvt_s(digits, d, count, &decimal_pos, &sign); + return DisplayDecimalValue(digits, decimal_pos, sign); + } + + /// sets a number as the controls text + /// \param[in] d the value + /// \param[in] count total number of digits + + void SetDecimalValue(double d, int count) + { + int decimal_pos; + int sign; + char digits[limit]; + _ecvt_s(digits, d, count, &decimal_pos, &sign); + DisplayDecimalValue(digits, decimal_pos, sign); + } + /// sets a number as the controls text + /// \param[in] d the value + /// \remarks the total number of digits is calculated using the GetLimitText function + + void SetDecimalValue(double d) + { + SetDecimalValue(d, std::min(limit, static_cast<int>(static_cast<const T*>(this)->GetLimitText())) - 2); + } + + /// sets the controls text + /// \param[in] digits array containing the digits + /// \param[in] decimal_pos the position of the decimal point + /// \param[in] sign 1 if negative + + void DisplayDecimalValue(const char* digits, int decimal_pos, int sign) + { + TCHAR szBuff[limit]; + DecimalToText(szBuff, limit, digits, decimal_pos, sign); + static_cast<T*>(this)->SetWindowText(szBuff); + } + + /// convert a digit array to string + /// \param[out] szBuff target buffer for output + /// \param[in] buflen maximum characters in output buffer + /// \param[in] digits array containing the digits + /// \param[in] decimal_pos the position of the decimal point + /// \param[in] sign 1 if negative + + void DecimalToText(TCHAR* szBuff, size_t buflen, const char* digits, int decimal_pos, int sign) const + { + int i = 0; + size_t pos = 0; + if (sign) + { + for (const TCHAR *x = m_NegativeSign; *x ; ++x, ++pos) szBuff[pos] = *x; + } + + for (; pos < buflen && digits[i] && i < decimal_pos ; ++i, ++pos) szBuff[pos] = digits[i]; + + if (decimal_pos < 1) szBuff[pos++] = _T('0'); + size_t last_nonzero = pos; + + for (const TCHAR *x = m_DecimalSeparator; *x ; ++x, ++pos) szBuff[pos] = *x; + for (; pos < buflen && decimal_pos < 0; ++decimal_pos, ++pos) szBuff[pos] = _T('0'); + + for (; pos < buflen && digits[i]; ++i, ++pos) + { + szBuff[pos] = digits[i]; + if (digits[i] != '0') last_nonzero = pos+1; + } + szBuff[std::min(buflen - 1, last_nonzero)] = _T('\0'); + } + + void AllowNegative(bool allow) + { + m_allowNegative = allow; + } + + void AllowFractions(bool allow) + { + m_allowFractions = allow; + } + +}; + + +class CNumberEdit : public CEdit, public CDecimalSupport<CNumberEdit> +{ +public: + void SetTempoValue(const TEMPO &t); + TEMPO GetTempoValue(); + +protected: + afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); + afx_msg LPARAM OnPaste(WPARAM wParam, LPARAM lParam); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.cpp new file mode 100644 index 00000000..e8fdd666 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.cpp @@ -0,0 +1,154 @@ +/* + * CImageListEx.cpp + * ---------------- + * Purpose: A class that extends MFC's CImageList to handle alpha-blended images properly. + * 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 "CImageListEx.h" +#include "Image.h" +#include "../misc/mptColor.h" + +#include "Mptrack.h" + + +OPENMPT_NAMESPACE_BEGIN + +bool CImageListEx::Create(UINT resourceID, int cx, int cy, int nInitial, int nGrow, CDC *dc, double scaling, bool disabled, const mpt::span<const int> invertImages) +{ + std::unique_ptr<RawGDIDIB> bitmap; + try + { + bitmap = LoadPixelImage(GetResource(MAKEINTRESOURCE(resourceID), _T("PNG")), scaling, cx, cy); + cx = mpt::saturate_round<int>(cx * scaling); + cy = mpt::saturate_round<int>(cy * scaling); + } catch(...) + { + return false; + } + + const RawGDIDIB::Pixel buttonColor = GetSysColor(COLOR_BTNFACE); + const bool isDark = mpt::Color::GetLuma(buttonColor.r, buttonColor.g, buttonColor.b) < 128; + if(isDark) + { + // Invert brightness of icons on dark themes + for(const int img : invertImages) + { + for(int y = 0; y < cy; y++) + { + RawGDIDIB::Pixel *pixel = &(*bitmap)(img * cx, y); + for(int x = 0; x < cx; x++, pixel++) + { + auto hsv = mpt::Color::RGB{pixel->r / 255.0f, pixel->g / 255.0f, pixel->b / 255.0f}.ToHSV(); + hsv.v = (1.0f - hsv.v) * (1.0f - hsv.s) + (hsv.v) * hsv.s; + const auto rgb = hsv.ToRGB(); + pixel->r = mpt::saturate_cast<uint8>(rgb.r * 255.0f); + pixel->g = mpt::saturate_cast<uint8>(rgb.g * 255.0f); + pixel->b = mpt::saturate_cast<uint8>(rgb.b * 255.0f); + } + } + } + } + + if(disabled) + { + // Grayed out icons + for(auto &pixel : bitmap->Pixels()) + { + if(pixel.a != 0) + { + uint8 y = mpt::Color::GetLuma(pixel.r, pixel.g, pixel.b); + pixel.r = pixel.g = pixel.b = y; + if(isDark) + pixel.a -= pixel.a / 3; + else + pixel.a /= 2; + } + } + } + + bool result; + + if(dc == nullptr) dc = CDC::FromHandle(GetDC(NULL)); + + // Use 1-bit transperency when there is no alpha channel. + if(GetDeviceCaps(dc->GetSafeHdc(), BITSPIXEL) * GetDeviceCaps(dc->GetSafeHdc(), PLANES) < 32) + { + const uint32 rowSize = (bitmap->Width() + 31u) / 32u * 4u; + std::vector<uint8> bitmapMask(rowSize * bitmap->Height()); + + RawGDIDIB::Pixel *pixel = bitmap->Pixels().data(); + for(uint32 y = 0; y < bitmap->Height(); y++) + { + uint8 *mask = bitmapMask.data() + rowSize * y; + for(uint32 x = 0; x < bitmap->Width(); x++, pixel++) + { + if(pixel->a != 0) + { + // Pixel not fully transparent - multiply with default background colour +#define MIXCOLOR(c) (((pixel-> c ) * pixel->a + (buttonColor. c) * (255 - pixel->a)) >> 8); + pixel->r = MIXCOLOR(r); + pixel->g = MIXCOLOR(g); + pixel->b = MIXCOLOR(b); +#undef MIXCOLOR + } else + { + // Transparent pixel + mask[x / 8u] |= 1 << (7 - (x & 7)); + } + } + } + + CBitmap ddb, dibMask; + CopyToCompatibleBitmap(ddb, *dc, *bitmap); + + struct + { + BITMAPINFOHEADER bi; + RGBQUAD col[2]; + } bi; + MemsetZero(bi); + bi.bi.biSize = sizeof(BITMAPINFOHEADER); + bi.bi.biWidth = bitmap->Width(); + bi.bi.biHeight = -(int32)bitmap->Height(); + bi.bi.biPlanes = 1; + bi.bi.biBitCount = 1; + bi.bi.biCompression = BI_RGB; + bi.bi.biSizeImage = static_cast<DWORD>(bitmapMask.size() * sizeof(decltype(bitmapMask[0]))); + bi.col[1].rgbBlue = bi.col[1].rgbGreen = bi.col[1].rgbRed = 255; + + dibMask.CreateCompatibleBitmap(dc, bitmap->Width(), bitmap->Height()); + SetDIBits(dc->GetSafeHdc(), dibMask, 0, bitmap->Height(), bitmapMask.data(), reinterpret_cast<BITMAPINFO *>(&bi), DIB_RGB_COLORS); + + result = CImageList::Create(cx, cy, ILC_COLOR24 | ILC_MASK, nInitial, nGrow) + && CImageList::Add(&ddb, &dibMask); + } else + { + // 32-bit image on modern system + + // Make fully transparent pixels use the mask color. This should hopefully make the icons look "somewhat" okay + // on system where the alpha channel is magically missing in 32-bit mode (https://bugs.openmpt.org/view.php?id=520) + for(auto &pixel : bitmap->Pixels()) + { + if(pixel.a == 0) + { + pixel.r = 255; + pixel.g = 0; + pixel.b = 255; + } + } + + CBitmap ddb; + CopyToCompatibleBitmap(ddb, *dc, *bitmap); + result = CImageList::Create(cx, cy, ILC_COLOR32 | ILC_MASK, nInitial, nGrow) + && CImageList::Add(&ddb, RGB(255, 0, 255)); + } + + return result; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.h b/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.h new file mode 100644 index 00000000..36b6df88 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CImageListEx.h @@ -0,0 +1,24 @@ +/* + * CImageListEx.h + * -------------- + * Purpose: A class that extends MFC's CImageList to handle alpha-blended images properly. Also provided 1-bit transparency fallback when needed. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "mpt/base/span.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CImageListEx : public CImageList +{ +public: + bool Create(UINT resourceID, int cx, int cy, int nInitial, int nGrow, CDC *dc, double scaling, bool disabled, const mpt::span<const int> invertImages = {}); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CListCtrl.h b/Src/external_dependencies/openmpt-trunk/mptrack/CListCtrl.h new file mode 100644 index 00000000..e14f055a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CListCtrl.h @@ -0,0 +1,104 @@ +/* + * CListCtrl.h + * ----------- + * Purpose: A class that extends MFC's CListCtrl with some more functionality and to handle unicode strings in ANSI builds. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "MPTrackUtil.h" + +OPENMPT_NAMESPACE_BEGIN + +class CListCtrlEx : public CListCtrl +{ +public: + struct Header + { + const TCHAR *text = nullptr; + int width = 0; + UINT mask = 0; + }; + void SetHeaders(const mpt::span<const Header> &header) + { + for(int i = 0; i < static_cast<int>(header.size()); i++) + { + int width = header[i].width; + InsertColumn(i, header[i].text, header[i].mask, width >= 0 ? Util::ScalePixels(width, m_hWnd) : 16); + if(width < 0) + SetColumnWidth(i, width); + } + } + + void SetItemDataPtr(int item, void *value) + { + SetItemData(item, reinterpret_cast<DWORD_PTR>(value)); + } + + void *GetItemDataPtr(int item) + { + return reinterpret_cast<void *>(GetItemData(item)); + } + + // Unicode strings in ANSI builds +#ifndef UNICODE + BOOL SetItemText(int nItem, int nSubItem, const WCHAR *lpszText) + { + ASSERT(::IsWindow(m_hWnd)); + ASSERT((GetStyle() & LVS_OWNERDATA)==0); + LVITEMW lvi; + lvi.iSubItem = nSubItem; + lvi.pszText = (LPWSTR) lpszText; + return (BOOL) ::SendMessage(m_hWnd, LVM_SETITEMTEXTW, nItem, (LPARAM)&lvi); + } + + using CListCtrl::SetItemText; +#endif +}; + + +#ifdef MPT_MFC_FULL + +class CMFCListCtrlEx : public CMFCListCtrl +{ +public: + struct Header + { + const TCHAR *text = nullptr; + int width = 0; + UINT mask = 0; + }; + void SetHeaders(const mpt::span<const Header> &header) + { + for(int i = 0; i < static_cast<int>(header.size()); i++) + { + InsertColumn(i, header[i].text, header[i].mask, Util::ScalePixels(header[i].width, m_hWnd)); + } + } + + // Unicode strings in ANSI builds +#ifndef UNICODE + BOOL SetItemText(int nItem, int nSubItem, const WCHAR *lpszText) + { + ASSERT(::IsWindow(m_hWnd)); + ASSERT((GetStyle() & LVS_OWNERDATA)==0); + LVITEMW lvi; + lvi.iSubItem = nSubItem; + lvi.pszText = (LPWSTR) lpszText; + return (BOOL) ::SendMessage(m_hWnd, LVM_SETITEMTEXTW, nItem, (LPARAM)&lvi); + } + + using CListCtrl::SetItemText; +#endif +}; + +#endif // MPT_MFC_FULL + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.cpp new file mode 100644 index 00000000..a96265f0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.cpp @@ -0,0 +1,1087 @@ +/* + * ChannelManagerDlg.cpp + * --------------------- + * Purpose: Dialog class for moving, removing, managing channels + * 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 "Moddoc.h" +#include "Mainfrm.h" +#include "ChannelManagerDlg.h" +#include "../common/mptStringBuffer.h" + +#include <functional> + +OPENMPT_NAMESPACE_BEGIN + +#define CM_NB_COLS 8 +#define CM_BT_HEIGHT 22 + +/////////////////////////////////////////////////////////// +// CChannelManagerDlg + +BEGIN_MESSAGE_MAP(CChannelManagerDlg, CDialog) + ON_WM_PAINT() + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONUP() + ON_WM_LBUTTONDOWN() + ON_WM_RBUTTONUP() + ON_WM_RBUTTONDOWN() + ON_WM_MBUTTONDOWN() + ON_WM_CLOSE() + + ON_COMMAND(IDC_BUTTON1, &CChannelManagerDlg::OnApply) + ON_COMMAND(IDC_BUTTON2, &CChannelManagerDlg::OnClose) + ON_COMMAND(IDC_BUTTON3, &CChannelManagerDlg::OnSelectAll) + ON_COMMAND(IDC_BUTTON4, &CChannelManagerDlg::OnInvert) + ON_COMMAND(IDC_BUTTON5, &CChannelManagerDlg::OnAction1) + ON_COMMAND(IDC_BUTTON6, &CChannelManagerDlg::OnAction2) + ON_COMMAND(IDC_BUTTON7, &CChannelManagerDlg::OnStore) + ON_COMMAND(IDC_BUTTON8, &CChannelManagerDlg::OnRestore) + ON_NOTIFY(TCN_SELCHANGE, IDC_TAB1, &CChannelManagerDlg::OnTabSelchange) + + ON_WM_LBUTTONDBLCLK() + ON_WM_RBUTTONDBLCLK() +END_MESSAGE_MAP() + +CChannelManagerDlg * CChannelManagerDlg::sharedInstance_ = nullptr; + +CChannelManagerDlg * CChannelManagerDlg::sharedInstanceCreate() +{ + try + { + if(sharedInstance_ == nullptr) + sharedInstance_ = new CChannelManagerDlg(); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + return sharedInstance_; +} + +void CChannelManagerDlg::SetDocument(CModDoc *modDoc) +{ + if(modDoc != m_ModDoc) + { + m_ModDoc = modDoc; + ResetState(true, true, true, true, false); + if(m_show) + { + if(m_ModDoc) + { + ResizeWindow(); + ShowWindow(SW_SHOWNOACTIVATE); // In case the window was hidden because no module was loaded + InvalidateRect(m_drawableArea, FALSE); + } else + { + ShowWindow(SW_HIDE); + } + } + } +} + +bool CChannelManagerDlg::IsDisplayed() const +{ + return m_show; +} + +void CChannelManagerDlg::Update(UpdateHint hint, CObject* pHint) +{ + if(!m_hWnd || !m_show) + return; + if(!hint.ToType<GeneralHint>().GetType()[HINT_MODCHANNELS | HINT_MODGENERAL | HINT_MODTYPE | HINT_MPTOPTIONS]) + return; + ResizeWindow(); + InvalidateRect(nullptr, FALSE); + if(hint.ToType<GeneralHint>().GetType()[HINT_MODCHANNELS] && m_quickChannelProperties.m_hWnd && pHint != &m_quickChannelProperties) + m_quickChannelProperties.UpdateDisplay(); +} + +void CChannelManagerDlg::Show() +{ + if(!m_hWnd) + { + Create(IDD_CHANNELMANAGER, nullptr); + } + ResizeWindow(); + ShowWindow(SW_SHOW); + m_show = true; +} + +void CChannelManagerDlg::Hide() +{ + if(m_hWnd != nullptr && m_show) + { + ResetState(true, true, true, true, true); + ShowWindow(SW_HIDE); + m_show = false; + } +} + + +CChannelManagerDlg::CChannelManagerDlg() + : m_buttonHeight(CM_BT_HEIGHT) +{ + for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++) + { + pattern[chn] = chn; + memory[0][chn] = 0; + memory[1][chn] = 0; + memory[2][chn] = 0; + memory[3][chn] = chn; + } +} + +CChannelManagerDlg::~CChannelManagerDlg() +{ + if(this == sharedInstance_) + sharedInstance_ = nullptr; + if(m_bkgnd) + DeleteBitmap(m_bkgnd); + DestroyWindow(); +} + +BOOL CChannelManagerDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + HWND menu = ::GetDlgItem(m_hWnd, IDC_TAB1); + + TCITEM tie; + tie.mask = TCIF_TEXT | TCIF_IMAGE; + tie.iImage = -1; + tie.pszText = const_cast<LPTSTR>(_T("Solo/Mute")); + TabCtrl_InsertItem(menu, kSoloMute, &tie); + tie.pszText = const_cast<LPTSTR>(_T("Record select")); + TabCtrl_InsertItem(menu, kRecordSelect, &tie); + tie.pszText = const_cast<LPTSTR>(_T("Plugins")); + TabCtrl_InsertItem(menu, kPluginState, &tie); + tie.pszText = const_cast<LPTSTR>(_T("Reorder/Remove")); + TabCtrl_InsertItem(menu, kReorderRemove, &tie); + m_currentTab = kSoloMute; + + m_buttonHeight = MulDiv(CM_BT_HEIGHT, Util::GetDPIy(m_hWnd), 96); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1), SW_HIDE); + + return TRUE; +} + +void CChannelManagerDlg::OnApply() +{ + if(!m_ModDoc) return; + + CHANNELINDEX numChannels, newMemory[4][MAX_BASECHANNELS]; + std::vector<CHANNELINDEX> newChnOrder; + newChnOrder.reserve(m_ModDoc->GetNumChannels()); + + // Count new number of channels, copy pattern pointers & manager internal store memory + numChannels = 0; + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + if(!removed[pattern[chn]]) + { + newMemory[0][numChannels] = memory[0][numChannels]; + newMemory[1][numChannels] = memory[1][numChannels]; + newMemory[2][numChannels] = memory[2][numChannels]; + newChnOrder.push_back(pattern[chn]); + numChannels++; + } + } + + BeginWaitCursor(); + + //Creating new order-vector for ReArrangeChannels. + CriticalSection cs; + if(m_ModDoc->ReArrangeChannels(newChnOrder) != numChannels) + { + cs.Leave(); + EndWaitCursor(); + return; + } + + // Update manager internal store memory + for(CHANNELINDEX chn = 0; chn < numChannels; chn++) + { + CHANNELINDEX newChn = newChnOrder[chn]; + if(chn != newChn) + { + memory[0][chn] = newMemory[0][newChn]; + memory[1][chn] = newMemory[1][newChn]; + memory[2][chn] = newMemory[2][newChn]; + } + memory[3][chn] = chn; + } + + cs.Leave(); + EndWaitCursor(); + + ResetState(true, true, true, true, true); + + // Update document & windows + m_ModDoc->SetModified(); + m_ModDoc->UpdateAllViews(nullptr, GeneralHint().Channels().ModType(), this); //refresh channel headers + + // Redraw channel manager window + ResizeWindow(); + InvalidateRect(nullptr, FALSE); +} + +void CChannelManagerDlg::OnClose() +{ + if(m_bkgnd) DeleteBitmap(m_bkgnd); + ResetState(true, true, true, true, true); + m_bkgnd = nullptr; + m_show = false; + + CDialog::OnCancel(); +} + +void CChannelManagerDlg::OnSelectAll() +{ + select.set(); + InvalidateRect(m_drawableArea, FALSE); +} + +void CChannelManagerDlg::OnInvert() +{ + select.flip(); + InvalidateRect(m_drawableArea, FALSE); +} + +void CChannelManagerDlg::OnAction1() +{ + if(m_ModDoc) + { + int nbOk = 0, nbSelect = 0; + + switch(m_currentTab) + { + case kSoloMute: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn]) + { + if(select[sourceChn]) + nbSelect++; + if(select[sourceChn] && m_ModDoc->IsChannelSolo(sourceChn)) + nbOk++; + } + } + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn] && !removed[sourceChn]) + { + if(m_ModDoc->IsChannelMuted(sourceChn)) + m_ModDoc->MuteChannel(sourceChn, false); + if(nbSelect == nbOk) + m_ModDoc->SoloChannel(sourceChn, !m_ModDoc->IsChannelSolo(sourceChn)); + else + m_ModDoc->SoloChannel(sourceChn, true); + } + else if(!m_ModDoc->IsChannelSolo(sourceChn)) + m_ModDoc->MuteChannel(sourceChn, true); + } + break; + case kRecordSelect: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn]) + { + if(select[sourceChn]) + nbSelect++; + if(select[sourceChn] && m_ModDoc->GetChannelRecordGroup(sourceChn) == RecordGroup::Group1) + nbOk++; + } + } + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn] && select[sourceChn]) + { + if(select[sourceChn] && nbSelect != nbOk && m_ModDoc->GetChannelRecordGroup(sourceChn) != RecordGroup::Group1) + m_ModDoc->SetChannelRecordGroup(sourceChn, RecordGroup::Group1); + else if(nbSelect == nbOk) + m_ModDoc->SetChannelRecordGroup(sourceChn, RecordGroup::NoGroup); + } + } + break; + case kPluginState: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn] && !removed[sourceChn]) + m_ModDoc->NoFxChannel(sourceChn, false); + } + break; + case kReorderRemove: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn]) + removed[sourceChn] = !removed[sourceChn]; + } + break; + default: + break; + } + + + ResetState(); + + m_ModDoc->UpdateAllViews(nullptr, GeneralHint().Channels(), this); + InvalidateRect(m_drawableArea, FALSE); + } +} + +void CChannelManagerDlg::OnAction2() +{ + if(m_ModDoc) + { + + int nbOk = 0, nbSelect = 0; + + switch(m_currentTab) + { + case kSoloMute: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn]) + { + if(select[sourceChn]) + nbSelect++; + if(select[sourceChn] && m_ModDoc->IsChannelMuted(sourceChn)) + nbOk++; + } + } + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn] && !removed[sourceChn]) + { + if(m_ModDoc->IsChannelSolo(sourceChn)) + m_ModDoc->SoloChannel(sourceChn, false); + if(nbSelect == nbOk) + m_ModDoc->MuteChannel(sourceChn, !m_ModDoc->IsChannelMuted(sourceChn)); + else + m_ModDoc->MuteChannel(sourceChn, true); + } + } + break; + case kRecordSelect: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn]) + { + if(select[sourceChn]) + nbSelect++; + if(select[sourceChn] && m_ModDoc->GetChannelRecordGroup(sourceChn) == RecordGroup::Group2) + nbOk++; + } + } + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(!removed[sourceChn] && select[sourceChn]) + { + if(select[sourceChn] && nbSelect != nbOk && m_ModDoc->GetChannelRecordGroup(sourceChn) != RecordGroup::Group2) + m_ModDoc->SetChannelRecordGroup(sourceChn, RecordGroup::Group2); + else if(nbSelect == nbOk) + m_ModDoc->SetChannelRecordGroup(sourceChn, RecordGroup::NoGroup); + } + } + break; + case kPluginState: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn] && !removed[sourceChn]) + m_ModDoc->NoFxChannel(sourceChn, true); + } + break; + case kReorderRemove: + ResetState(false, false, false, false, true); + break; + default: + break; + } + + if(m_currentTab != 3) ResetState(); + + m_ModDoc->UpdateAllViews(nullptr, GeneralHint().Channels(), this); + InvalidateRect(m_drawableArea, FALSE); + } +} + +void CChannelManagerDlg::OnStore(void) +{ + if(!m_show || m_ModDoc == nullptr) + { + return; + } + + switch(m_currentTab) + { + case kSoloMute: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + memory[0][sourceChn] = 0; + if(m_ModDoc->IsChannelMuted(sourceChn)) memory[0][chn] |= 1; + if(m_ModDoc->IsChannelSolo(sourceChn)) memory[0][chn] |= 2; + } + break; + case kRecordSelect: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + memory[1][chn] = static_cast<uint8>(m_ModDoc->GetChannelRecordGroup(pattern[chn])); + break; + case kPluginState: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + memory[2][chn] = m_ModDoc->IsChannelNoFx(pattern[chn]); + break; + case kReorderRemove: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + memory[3][chn] = pattern[chn]; + break; + default: + break; + } +} + +void CChannelManagerDlg::OnRestore(void) +{ + if(!m_show || m_ModDoc == nullptr) + { + return; + } + + switch(m_currentTab) + { + case kSoloMute: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + m_ModDoc->MuteChannel(sourceChn, (memory[0][chn] & 1) != 0); + m_ModDoc->SoloChannel(sourceChn, (memory[0][chn] & 2) != 0); + } + break; + case kRecordSelect: + m_ModDoc->ReinitRecordState(true); + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + m_ModDoc->SetChannelRecordGroup(chn, static_cast<RecordGroup>(memory[1][chn])); + } + break; + case kPluginState: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + m_ModDoc->NoFxChannel(pattern[chn], memory[2][chn] != 0); + break; + case kReorderRemove: + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + pattern[chn] = memory[3][chn]; + ResetState(false, false, false, false, true); + break; + default: + break; + } + + if(m_currentTab != 3) ResetState(); + + m_ModDoc->UpdateAllViews(nullptr, GeneralHint().Channels(), this); + InvalidateRect(m_drawableArea, FALSE); +} + +void CChannelManagerDlg::OnTabSelchange(NMHDR* /*header*/, LRESULT* /*pResult*/) +{ + if(!m_show) return; + + m_currentTab = static_cast<Tab>(TabCtrl_GetCurFocus(::GetDlgItem(m_hWnd, IDC_TAB1))); + + switch(m_currentTab) + { + case kSoloMute: + SetDlgItemText(IDC_BUTTON5, _T("Solo")); + SetDlgItemText(IDC_BUTTON6, _T("Mute")); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON5),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON6),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1),SW_HIDE); + break; + case kRecordSelect: + SetDlgItemText(IDC_BUTTON5, _T("Instrument 1")); + SetDlgItemText(IDC_BUTTON6, _T("Instrument 2")); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON5),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON6),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1),SW_HIDE); + break; + case kPluginState: + SetDlgItemText(IDC_BUTTON5, _T("Enable FX")); + SetDlgItemText(IDC_BUTTON6, _T("Disable FX")); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON5),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON6),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1),SW_HIDE); + break; + case kReorderRemove: + SetDlgItemText(IDC_BUTTON5, _T("Remove")); + SetDlgItemText(IDC_BUTTON6, _T("Cancel All")); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON5),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON6),SW_SHOW); + ::ShowWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1),SW_SHOW); + break; + default: + break; + } + + InvalidateRect(m_drawableArea, FALSE); +} + + +void CChannelManagerDlg::ResizeWindow() +{ + if(!m_hWnd || !m_ModDoc) return; + + const int dpiX = Util::GetDPIx(m_hWnd); + const int dpiY = Util::GetDPIy(m_hWnd); + + m_buttonHeight = MulDiv(CM_BT_HEIGHT, dpiY, 96); + + CHANNELINDEX channels = m_ModDoc->GetNumChannels(); + int lines = channels / CM_NB_COLS + (channels % CM_NB_COLS ? 1 : 0); + + CRect window; + GetWindowRect(window); + + CRect client; + GetClientRect(client); + m_drawableArea = client; + m_drawableArea.DeflateRect(MulDiv(10, dpiX, 96), MulDiv(38, dpiY, 96), MulDiv(8, dpiX, 96), MulDiv(30, dpiY, 96)); + + int chnSizeY = m_drawableArea.Height() / lines; + + if(chnSizeY != m_buttonHeight) + { + SetWindowPos(nullptr, 0, 0, window.Width(), window.Height() + (m_buttonHeight - chnSizeY) * lines, SWP_NOMOVE | SWP_NOZORDER | SWP_NOREDRAW); + + GetClientRect(client); + + // Move butttons to bottom of the window + for(auto id : { IDC_BUTTON1, IDC_BUTTON2, IDC_BUTTON3, IDC_BUTTON4, IDC_BUTTON5, IDC_BUTTON6 }) + { + CWnd *button = GetDlgItem(id); + if(button != nullptr) + { + CRect btn; + button->GetClientRect(btn); + button->MapWindowPoints(this, btn); + button->SetWindowPos(nullptr, btn.left, client.Height() - btn.Height() - MulDiv(3, dpiY, 96), 0, 0, SWP_NOSIZE | SWP_NOZORDER); + } + } + + if(m_bkgnd) + { + DeleteObject(m_bkgnd); + m_bkgnd = nullptr; + } + + m_drawableArea = client; + m_drawableArea.DeflateRect(MulDiv(10, dpiX, 96), MulDiv(38, dpiY, 96), MulDiv(8, dpiX, 96), MulDiv(30, dpiY, 96)); + InvalidateRect(nullptr, FALSE); + } +} + + +void CChannelManagerDlg::OnPaint() +{ + if(!m_hWnd || !m_show || m_ModDoc == nullptr) + { + CDialog::OnPaint(); + ShowWindow(SW_HIDE); + return; + } + if(IsIconic()) + { + CDialog::OnPaint(); + return; + } + + const int dpiX = Util::GetDPIx(m_hWnd); + const int dpiY = Util::GetDPIy(m_hWnd); + const CHANNELINDEX channels = m_ModDoc->GetNumChannels(); + + PAINTSTRUCT pDC; + ::BeginPaint(m_hWnd, &pDC); + const CRect &rcPaint = pDC.rcPaint; + + const int chnSizeX = m_drawableArea.Width() / CM_NB_COLS; + const int chnSizeY = m_buttonHeight; + + if(m_currentTab == 3 && m_moveRect && m_bkgnd) + { + // Only draw channels to be moved around + HDC bdc = ::CreateCompatibleDC(pDC.hdc); + ::SelectObject(bdc, m_bkgnd); + ::BitBlt(pDC.hdc, rcPaint.left, rcPaint.top, rcPaint.Width(), rcPaint.Height(), bdc, rcPaint.left, rcPaint.top, SRCCOPY); + + BLENDFUNCTION ftn; + ftn.BlendOp = AC_SRC_OVER; + ftn.BlendFlags = 0; + ftn.SourceConstantAlpha = 192; + ftn.AlphaFormat = 0; + + for(CHANNELINDEX chn = 0; chn < channels; chn++) + { + CHANNELINDEX sourceChn = pattern[chn]; + if(select[sourceChn]) + { + CRect btn = move[sourceChn]; + btn.DeflateRect(3, 3, 0, 0); + + AlphaBlend(pDC.hdc, btn.left + m_moveX - m_downX, btn.top + m_moveY - m_downY, btn.Width(), btn.Height(), bdc, + btn.left, btn.top, btn.Width(), btn.Height(), ftn); + } + } + ::SelectObject(bdc, (HBITMAP)NULL); + ::DeleteDC(bdc); + + ::EndPaint(m_hWnd, &pDC); + return; + } + + CRect client; + GetClientRect(&client); + + HDC dc = ::CreateCompatibleDC(pDC.hdc); + if(!m_bkgnd) + m_bkgnd = ::CreateCompatibleBitmap(pDC.hdc, client.Width(), client.Height()); + HGDIOBJ oldBmp = ::SelectObject(dc, m_bkgnd); + HGDIOBJ oldFont = ::SelectObject(dc, CMainFrame::GetGUIFont()); + + const auto dcBrush = GetStockBrush(DC_BRUSH); + + client.SetRect(client.left + MulDiv(2, dpiX, 96), client.top + MulDiv(32, dpiY, 96), client.right - MulDiv(2, dpiX, 96), client.bottom - MulDiv(24, dpiY, 96)); + // Draw background + { + const auto bgIntersected = client & pDC.rcPaint; // In case of partial redraws, FillRect may still draw into areas that are not part of the redraw area and thus make some buttons disappear + ::FillRect(dc, &pDC.rcPaint, GetSysColorBrush(COLOR_BTNFACE)); + ::FillRect(dc, &bgIntersected, GetSysColorBrush(COLOR_HIGHLIGHT)); + ::SetDCBrushColor(dc, RGB(20, 20, 20)); + ::FrameRect(dc, &client, dcBrush); + } + + client.SetRect(client.left + 8,client.top + 6,client.right - 6,client.bottom - 6); + + const COLORREF highlight = GetSysColor(COLOR_HIGHLIGHT), red = RGB(192, 96, 96), green = RGB(96, 192, 96), redBright = RGB(218, 163, 163), greenBright = RGB(163, 218, 163); + const COLORREF brushColors[] = { highlight, green, red }; + const COLORREF brushColorsBright[] = { highlight, greenBright, redBright }; + const auto buttonFaceColor = GetSysColor(COLOR_BTNFACE), windowColor = GetSysColor(COLOR_WINDOW); + + uint32 col = 0, row = 0; + const CSoundFile &sndFile = m_ModDoc->GetSoundFile(); + CString s; + for(CHANNELINDEX chn = 0; chn < channels; chn++, col++) + { + if(col >= CM_NB_COLS) + { + col = 0; + row++; + } + + const CHANNELINDEX sourceChn = pattern[chn]; + const auto &chnSettings = sndFile.ChnSettings[sourceChn]; + + if(!chnSettings.szName.empty()) + s = MPT_CFORMAT("{}: {}")(sourceChn + 1, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.ChnSettings[sourceChn].szName)); + else + s = MPT_CFORMAT("Channel {}")(sourceChn + 1); + + const int borderX = MulDiv(3, dpiX, 96), borderY = MulDiv(3, dpiY, 96); + CRect btn; + btn.left = client.left + col * chnSizeX + borderX; + btn.right = btn.left + chnSizeX - borderX; + btn.top = client.top + row * chnSizeY + borderY; + btn.bottom = btn.top + chnSizeY - borderY; + + if(!CRect{}.IntersectRect(&pDC.rcPaint, &btn)) + continue; + + // Button + const bool activate = select[sourceChn]; + const bool enable = !removed[sourceChn]; + auto btnAdjusted = btn; // Without border + ::DrawEdge(dc, btnAdjusted, enable ? EDGE_RAISED : EDGE_SUNKEN, BF_RECT | BF_MIDDLE | BF_ADJUST); + if(activate) + ::FillRect(dc, btnAdjusted, GetSysColorBrush(COLOR_WINDOW)); + + if(chnSettings.color != ModChannelSettings::INVALID_COLOR) + { + // Channel color + const auto startColor = chnSettings.color; + const auto endColor = activate ? windowColor : buttonFaceColor; + const auto width = btnAdjusted.Width() / 2; + auto rect = btnAdjusted; + rect.right = rect.left + 1; + for(int i = 0; i < width; i++) + { + auto blend = static_cast<double>(i) / width, blendInv = 1.0 - blend; + auto blendColor = RGB(mpt::saturate_round<uint8>(GetRValue(startColor) * blendInv + GetRValue(endColor) * blend), + mpt::saturate_round<uint8>(GetGValue(startColor) * blendInv + GetGValue(endColor) * blend), + mpt::saturate_round<uint8>(GetBValue(startColor) * blendInv + GetBValue(endColor) * blend)); + ::SetDCBrushColor(dc, blendColor); + ::FillRect(dc, &rect, dcBrush); + rect.left++; + rect.right++; + } + } + + // Text + { + auto rect = btnAdjusted; + rect.left += Util::ScalePixels(9, m_hWnd); + rect.right -= Util::ScalePixels(3, m_hWnd); + + ::SetBkMode(dc, TRANSPARENT); + ::SetTextColor(dc, GetSysColor(enable || activate ? COLOR_BTNTEXT : COLOR_GRAYTEXT)); + ::DrawText(dc, s, -1, &rect, DT_RIGHT | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); + } + + // Draw red/green markers + { + const int margin = Util::ScalePixels(1, m_hWnd); + auto rect = btnAdjusted; + rect.DeflateRect(margin, margin); + rect.right = rect.left + Util::ScalePixels(7, m_hWnd); + const auto &brushes = activate ? brushColorsBright : brushColors; + const auto redBrush = brushes[2], greenBrush = brushes[1]; + COLORREF color = 0; + switch(m_currentTab) + { + case kSoloMute: + color = chnSettings.dwFlags[CHN_MUTE] ? redBrush : greenBrush; + break; + case kRecordSelect: + color = brushColors[static_cast<size_t>(m_ModDoc->GetChannelRecordGroup(sourceChn)) % std::size(brushColors)]; + break; + case kPluginState: + color = chnSettings.dwFlags[CHN_NOFX] ? redBrush : greenBrush; + break; + case kReorderRemove: + color = removed[sourceChn] ? redBrush : greenBrush; + break; + } + ::SetDCBrushColor(dc, color); + ::FillRect(dc, rect, dcBrush); + // Draw border around marker + ::SetDCBrushColor(dc, RGB(20, 20, 20)); + ::FrameRect(dc, rect, dcBrush); + } + } + + ::BitBlt(pDC.hdc, rcPaint.left, rcPaint.top, rcPaint.Width(), rcPaint.Height(), dc, rcPaint.left, rcPaint.top, SRCCOPY); + ::SelectObject(dc, oldFont); + ::SelectObject(dc, oldBmp); + ::DeleteDC(dc); + + ::EndPaint(m_hWnd, &pDC); +} + + +bool CChannelManagerDlg::ButtonHit(CPoint point, CHANNELINDEX *id, CRect *invalidate) const +{ + const CRect &client = m_drawableArea; + + if(PtInRect(client, point) && m_ModDoc != nullptr) + { + UINT nColns = CM_NB_COLS; + + int x = point.x - client.left; + int y = point.y - client.top; + + int dx = client.Width() / (int)nColns; + int dy = m_buttonHeight; + + x = x / dx; + y = y / dy; + CHANNELINDEX n = static_cast<CHANNELINDEX>(y * nColns + x); + if(n < m_ModDoc->GetNumChannels()) + { + if(id) *id = n; + if(invalidate) + { + invalidate->left = client.left + x * dx; + invalidate->right = invalidate->left + dx; + invalidate->top = client.top + y * dy; + invalidate->bottom = invalidate->top + dy; + } + return true; + } + } + return false; +} + + +void CChannelManagerDlg::ResetState(bool bSelection, bool bMove, bool bButton, bool bInternal, bool bOrder) +{ + for(CHANNELINDEX chn = 0; chn < MAX_BASECHANNELS; chn++) + { + if(bSelection) + select[pattern[chn]] = false; + if(bButton) + state[pattern[chn]] = false; + if(bOrder) + { + pattern[chn] = chn; + removed[chn] = false; + } + } + if(bMove || bInternal) + { + m_leftButton = false; + m_rightButton = false; + } + if(bMove) m_moveRect = false; +} + + +void CChannelManagerDlg::OnMouseMove(UINT nFlags,CPoint point) +{ + if(!m_hWnd || m_show == false) return; + + if(!m_leftButton && !m_rightButton) + { + m_moveX = point.x; + m_moveY = point.y; + return; + } + MouseEvent(nFlags, point, m_moveRect ? CM_BT_NONE : (m_leftButton ? CM_BT_LEFT : CM_BT_RIGHT)); +} + +void CChannelManagerDlg::OnLButtonUp(UINT /*nFlags*/,CPoint point) +{ + ReleaseCapture(); + if(!m_hWnd || m_show == false) return; + + if(m_moveRect && m_ModDoc) + { + CHANNELINDEX dropChn = 0; + CRect dropRect; + if(ButtonHit(point, &dropChn, &dropRect)) + { + // Rearrange channels + const auto IsSelected = std::bind(&decltype(select)::test, &select, std::placeholders::_1); + + const auto numChannels = m_ModDoc->GetNumChannels(); + if(point.x > dropRect.left + dropRect.Width() / 2 && dropChn < numChannels) + dropChn++; + + std::vector<CHANNELINDEX> newOrder{ pattern.begin(), pattern.begin() + numChannels }; + // How many selected channels are there before the drop target? + // cppcheck false-positive + // cppcheck-suppress danglingTemporaryLifetime + const CHANNELINDEX selectedBeforeDropChn = static_cast<CHANNELINDEX>(std::count_if(pattern.begin(), pattern.begin() + dropChn, IsSelected)); + dropChn -= selectedBeforeDropChn; + // Remove all selected channels from the order + newOrder.erase(std::remove_if(newOrder.begin(), newOrder.end(), IsSelected), newOrder.end()); + const CHANNELINDEX numSelected = static_cast<CHANNELINDEX>(numChannels - newOrder.size()); + // Then insert them at the drop position + newOrder.insert(newOrder.begin() + dropChn, numSelected, PATTERNINDEX_INVALID); + std::copy_if(pattern.begin(), pattern.begin() + numChannels, newOrder.begin() + dropChn, IsSelected); + + std::copy(newOrder.begin(), newOrder.begin() + numChannels, pattern.begin()); + select.reset(); + } else + { + ResetState(true, false, false, false, false); + } + + m_moveRect = false; + InvalidateRect(m_drawableArea, FALSE); + if(m_ModDoc) m_ModDoc->UpdateAllViews(nullptr, GeneralHint().Channels(), this); + } + + m_leftButton = false; + + for(CHANNELINDEX chn : pattern) + state[chn] = false; +} + +void CChannelManagerDlg::OnLButtonDown(UINT nFlags,CPoint point) +{ + if(!m_hWnd || m_show == false) return; + SetCapture(); + + if(!ButtonHit(point, nullptr, nullptr)) ResetState(true, false, false, false); + + m_leftButton = true; + m_buttonAction = kUndetermined; + MouseEvent(nFlags,point,CM_BT_LEFT); + m_downX = point.x; + m_downY = point.y; +} + +void CChannelManagerDlg::OnRButtonUp(UINT /*nFlags*/,CPoint /*point*/) +{ + ReleaseCapture(); + if(!m_hWnd || m_show == false) return; + + ResetState(false, false, true, false); + + m_rightButton = false; +} + +void CChannelManagerDlg::OnRButtonDown(UINT nFlags,CPoint point) +{ + if(!m_hWnd || m_show == false) return; + SetCapture(); + + m_rightButton = true; + m_buttonAction = kUndetermined; + if(m_moveRect) + { + ResetState(true, true, false, false, false); + InvalidateRect(m_drawableArea, FALSE); + } else + { + MouseEvent(nFlags, point, CM_BT_RIGHT); + m_downX = point.x; + m_downY = point.y; + } +} + +void CChannelManagerDlg::OnMButtonDown(UINT /*nFlags*/, CPoint point) +{ + CHANNELINDEX chn; + CRect rect; + if(m_ModDoc != nullptr && (m_ModDoc->GetModType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) && ButtonHit(point, &chn, &rect)) + { + ClientToScreen(&point); + m_quickChannelProperties.Show(m_ModDoc, pattern[chn], point); + } +} + +void CChannelManagerDlg::MouseEvent(UINT nFlags,CPoint point, MouseButton button) +{ + CHANNELINDEX n; + CRect client, invalidate; + bool hit = ButtonHit(point, &n, &invalidate); + if(hit) n = pattern[n]; + + m_moveX = point.x; + m_moveY = point.y; + + if(!m_ModDoc) return; + + if(hit && !state[n] && button != CM_BT_NONE) + { + if(nFlags & MK_CONTROL) + { + if(button == CM_BT_LEFT) + { + if(!select[n] && !removed[n]) move[n] = invalidate; + select[n] = true; + } + else if(button == CM_BT_RIGHT) select[n] = false; + } + else if(!removed[n] || m_currentTab == 3) + { + switch(m_currentTab) + { + case kSoloMute: + if(button == CM_BT_LEFT) + { + if(m_buttonAction == kUndetermined) + m_buttonAction = (!m_ModDoc->IsChannelSolo(n) || m_ModDoc->IsChannelMuted(n)) ? kAction1 : kAction2; + if(m_buttonAction == kAction1) + { + m_ModDoc->MuteChannel(n, false); + m_ModDoc->SoloChannel(n, true); + for(CHANNELINDEX chn = 0; chn < m_ModDoc->GetNumChannels(); chn++) + { + if(chn != n) + m_ModDoc->MuteChannel(chn, true); + } + invalidate = client = m_drawableArea; + } + else m_ModDoc->SoloChannel(n, false); + } else + { + if(m_ModDoc->IsChannelSolo(n)) m_ModDoc->SoloChannel(n, false); + if(m_buttonAction == kUndetermined) + m_buttonAction = m_ModDoc->IsChannelMuted(n) ? kAction1 : kAction2; + m_ModDoc->MuteChannel(n, m_buttonAction == kAction2); + } + m_ModDoc->SetModified(); + m_ModDoc->UpdateAllViews(nullptr, GeneralHint(n).Channels(), this); + break; + case kRecordSelect: + { + auto rec = m_ModDoc->GetChannelRecordGroup(n); + if(m_buttonAction == kUndetermined) + m_buttonAction = (rec == RecordGroup::NoGroup || rec != (button == CM_BT_LEFT ? RecordGroup::Group1 : RecordGroup::Group2)) ? kAction1 : kAction2; + + if(m_buttonAction == kAction1 && button == CM_BT_LEFT) + m_ModDoc->SetChannelRecordGroup(n, RecordGroup::Group1); + else if(m_buttonAction == kAction1 && button == CM_BT_RIGHT) + m_ModDoc->SetChannelRecordGroup(n, RecordGroup::Group2); + else + m_ModDoc->SetChannelRecordGroup(n, RecordGroup::NoGroup); + m_ModDoc->UpdateAllViews(nullptr, GeneralHint(n).Channels(), this); + break; + } + case kPluginState: + if(button == CM_BT_LEFT) m_ModDoc->NoFxChannel(n, false); + else m_ModDoc->NoFxChannel(n, true); + m_ModDoc->SetModified(); + m_ModDoc->UpdateAllViews(nullptr, GeneralHint(n).Channels(), this); + break; + case kReorderRemove: + if(button == CM_BT_LEFT) + { + move[n] = invalidate; + select[n] = true; + } + if(button == CM_BT_RIGHT) + { + if(m_buttonAction == kUndetermined) + m_buttonAction = removed[n] ? kAction1 : kAction2; + select[n] = false; + removed[n] = (m_buttonAction == kAction2); + } + + if(select[n] || button == 0) + { + m_moveRect = true; + } + break; + } + } + + state[n] = false; + InvalidateRect(invalidate, FALSE); + } else + { + InvalidateRect(m_drawableArea, FALSE); + } +} + + +void CChannelManagerDlg::OnLButtonDblClk(UINT nFlags, CPoint point) +{ + OnLButtonDown(nFlags, point); + CDialog::OnLButtonDblClk(nFlags, point); +} + +void CChannelManagerDlg::OnRButtonDblClk(UINT nFlags, CPoint point) +{ + OnRButtonDown(nFlags, point); + CDialog::OnRButtonDblClk(nFlags, point); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.h new file mode 100644 index 00000000..8cedd8f6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ChannelManagerDlg.h @@ -0,0 +1,115 @@ +/* + * ChannelManagerDlg.h + * ------------------- + * Purpose: Dialog class for moving, removing, managing channels + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "PatternEditorDialogs.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; + +class CChannelManagerDlg: public CDialog +{ + enum Tab + { + kSoloMute = 0, + kRecordSelect = 1, + kPluginState = 2, + kReorderRemove = 3, + }; + +public: + + static CChannelManagerDlg * sharedInstance() { return sharedInstance_; } + static CChannelManagerDlg * sharedInstanceCreate(); + static void DestroySharedInstance() { delete sharedInstance_; sharedInstance_ = nullptr; } + void SetDocument(CModDoc *modDoc); + CModDoc *GetDocument() const { return m_ModDoc; } + bool IsDisplayed() const; + void Update(UpdateHint hint, CObject* pHint); + void Show(); + void Hide(); + +private: + static CChannelManagerDlg *sharedInstance_; + QuickChannelProperties m_quickChannelProperties; + +protected: + + enum ButtonAction : uint8 + { + kUndetermined, + kAction1, + kAction2, + }; + + enum MouseButton : uint8 + { + CM_BT_NONE, + CM_BT_LEFT, + CM_BT_RIGHT, + }; + + CChannelManagerDlg(); + ~CChannelManagerDlg(); + + CHANNELINDEX memory[4][MAX_BASECHANNELS]; + std::array<CHANNELINDEX, MAX_BASECHANNELS> pattern; + std::bitset<MAX_BASECHANNELS> removed; + std::bitset<MAX_BASECHANNELS> select; + std::bitset<MAX_BASECHANNELS> state; + CRect move[MAX_BASECHANNELS]; + CRect m_drawableArea; + CModDoc *m_ModDoc = nullptr; + HBITMAP m_bkgnd = nullptr; + Tab m_currentTab = kSoloMute; + int m_downX = 0, m_downY = 0; + int m_moveX = 0, m_moveY = 0; + int m_buttonHeight = 0; + ButtonAction m_buttonAction; + bool m_leftButton = false; + bool m_rightButton = false; + bool m_moveRect = false; + bool m_show = false; + + bool ButtonHit(CPoint point, CHANNELINDEX *id, CRect *invalidate) const; + void MouseEvent(UINT nFlags, CPoint point, MouseButton button); + void ResetState(bool bSelection = true, bool bMove = true, bool bButton = true, bool bInternal = true, bool bOrder = false); + void ResizeWindow(); + + //{{AFX_VIRTUAL(CChannelManagerDlg) + BOOL OnInitDialog() override; + //}}AFX_VIRTUAL + //{{AFX_MSG(CChannelManagerDlg) + afx_msg void OnApply(); + afx_msg void OnClose(); + afx_msg void OnSelectAll(); + afx_msg void OnInvert(); + afx_msg void OnAction1(); + afx_msg void OnAction2(); + afx_msg void OnStore(); + afx_msg void OnRestore(); + afx_msg void OnTabSelchange(NMHDR*, LRESULT* pResult); + afx_msg void OnPaint(); + afx_msg void OnMouseMove(UINT nFlags,CPoint point); + afx_msg void OnLButtonUp(UINT nFlags,CPoint point); + afx_msg void OnLButtonDown(UINT nFlags,CPoint point); + afx_msg void OnRButtonUp(UINT nFlags,CPoint point); + afx_msg void OnRButtonDown(UINT nFlags,CPoint point); + afx_msg void OnMButtonDown(UINT nFlags,CPoint point); + afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point); + afx_msg void OnRButtonDblClk(UINT nFlags, CPoint point); + //}}AFX_MSG + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.cpp new file mode 100644 index 00000000..5fc920b6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.cpp @@ -0,0 +1,483 @@ +/* + * ChildFrm.cpp + * ------------ + * Purpose: Implementation of the MDI document child windows. + * 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 <afxpriv.h> +#include "Mptrack.h" +#include "Mainfrm.h" +#include "Childfrm.h" +#include "Moddoc.h" +#include "Globals.h" +#include "View_gen.h" +#include "Ctrl_pat.h" +#include "View_pat.h" +#include "Ctrl_smp.h" +#include "View_smp.h" +#include "Ctrl_ins.h" +#include "View_ins.h" +#include "view_com.h" +#include "Childfrm.h" +#include "ChannelManagerDlg.h" + +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#include "../common/FileReader.h" +#include <sstream> + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////////// +// CChildFrame + +IMPLEMENT_DYNCREATE(CChildFrame, CMDIChildWnd) + +BEGIN_MESSAGE_MAP(CChildFrame, CMDIChildWnd) + //{{AFX_MSG_MAP(CChildFrame) + ON_WM_DESTROY() + ON_WM_NCACTIVATE() + ON_WM_MDIACTIVATE() + ON_MESSAGE(WM_MOD_CHANGEVIEWCLASS, &CChildFrame::OnChangeViewClass) + ON_MESSAGE(WM_MOD_INSTRSELECTED, &CChildFrame::OnInstrumentSelected) + // toolbar "tooltip" notification + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &CChildFrame::OnToolTipText) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +CChildFrame *CChildFrame::m_lastActiveFrame = nullptr; +int CChildFrame::glMdiOpenCount = 0; + +///////////////////////////////////////////////////////////////////////////// +// CChildFrame construction/destruction + +CChildFrame::CChildFrame() +{ + m_bInitialActivation=true; //rewbs.fix3185 + m_szCurrentViewClassName[0] = 0; + m_hWndCtrl = m_hWndView = NULL; + m_bMaxWhenClosed = false; + glMdiOpenCount++; +} + + +CChildFrame::~CChildFrame() +{ + if ((--glMdiOpenCount) == 0) + { + TrackerSettings::Instance().gbMdiMaximize = m_bMaxWhenClosed; + } +} + + +BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) +{ + // create a splitter with 2 rows, 1 column + if (!m_wndSplitter.CreateStatic(this, 2, 1)) return FALSE; + + // add the first splitter pane - the default view in row 0 + int cy = Util::ScalePixels(TrackerSettings::Instance().glGeneralWindowHeight, m_hWnd); //rewbs.varWindowSize - default to general tab. + if (cy <= 1) cy = (lpcs->cy*2) / 3; + if (!m_wndSplitter.CreateView(0, 0, pContext->m_pNewViewClass, CSize(0, cy), pContext)) return FALSE; + + // Get 2nd window handle + CModControlView *pModView; + if ((pModView = GetModControlView()) != nullptr) + { + m_hWndCtrl = pModView->m_hWnd; + pModView->SetMDIParentFrame(m_hWnd); + } + + const BOOL bStatus = ChangeViewClass(RUNTIME_CLASS(CViewGlobals), pContext); + + // If it all worked, we now have a splitter window which contain two different views + return bStatus; +} + + +void CChildFrame::SetSplitterHeight(int cy) +{ + if (cy <= 1) cy = 188; //default to 188? why not.. + m_wndSplitter.SetRowInfo(0, Util::ScalePixels(cy, m_hWnd), 15); +} + + +BOOL CChildFrame::PreCreateWindow(CREATESTRUCT& cs) +{ + return CMDIChildWnd::PreCreateWindow(cs); +} + + +BOOL CChildFrame::OnNcActivate(BOOL bActivate) +{ + if(bActivate && m_hWndView) + { + // Need this in addition to OnMDIActivate when switching from a non-MDI window such as a plugin editor + CMainFrame::GetMainFrame()->SetMidiRecordWnd(m_hWndView); + } + if(m_hWndCtrl) + ::SendMessage(m_hWndCtrl, bActivate ? WM_MOD_MDIACTIVATE : WM_MOD_MDIDEACTIVATE, 0, 0); + if(m_hWndView) + ::SendMessage(m_hWndView, bActivate ? WM_MOD_MDIACTIVATE : WM_MOD_MDIDEACTIVATE, 0, 0); + + return CMDIChildWnd::OnNcActivate(bActivate); +} + + +void CChildFrame::OnMDIActivate(BOOL bActivate, CWnd *pActivateWnd, CWnd *pDeactivateWnd) +{ + CMDIChildWnd::OnMDIActivate(bActivate, pActivateWnd, pDeactivateWnd); + + if(bActivate) + { + MPT_ASSERT(pActivateWnd == this); + CMainFrame::GetMainFrame()->UpdateEffectKeys(static_cast<CModDoc *>(GetActiveDocument())); + CMainFrame::GetMainFrame()->SetMidiRecordWnd(m_hWndView); + m_lastActiveFrame = this; + } + if(m_hWndCtrl) + ::SendMessage(m_hWndCtrl, bActivate ? WM_MOD_MDIACTIVATE : WM_MOD_MDIDEACTIVATE, 0, 0); + if(m_hWndView) + ::SendMessage(m_hWndView, bActivate ? WM_MOD_MDIACTIVATE : WM_MOD_MDIDEACTIVATE, 0, 0); + + // Update channel manager according to active document + auto instance = CChannelManagerDlg::sharedInstance(); + if(instance != nullptr) + { + if(!bActivate && pActivateWnd == nullptr) + instance->SetDocument(nullptr); + else if(bActivate) + instance->SetDocument(static_cast<CModDoc *>(GetActiveDocument())); + } +} + + +void CChildFrame::ActivateFrame(int nCmdShow) +{ + if ((glMdiOpenCount == 1) && (TrackerSettings::Instance().gbMdiMaximize) && (nCmdShow == -1)) + { + nCmdShow = SW_SHOWMAXIMIZED; + } + CMDIChildWnd::ActivateFrame(nCmdShow); + + // When song first loads, initialise patternViewState to point to start of song. + CView *pView = GetActiveView(); + CModDoc *pModDoc = nullptr; + if (pView) pModDoc = (CModDoc *)pView->GetDocument(); + if ((m_hWndCtrl) && (pModDoc)) + { + if (m_bInitialActivation && m_ViewPatterns.nPattern == 0) + { + if(!pModDoc->GetSoundFile().Order().empty()) + m_ViewPatterns.nPattern = pModDoc->GetSoundFile().Order()[0]; + m_bInitialActivation = false; + } + } +} + + +void CChildFrame::OnUpdateFrameTitle(BOOL bAddToTitle) +{ + // update our parent window first + GetMDIFrame()->OnUpdateFrameTitle(bAddToTitle); + + if ((GetStyle() & FWS_ADDTOTITLE) == 0) return; // leave child window alone! + + CDocument* pDocument = GetActiveDocument(); + if (bAddToTitle) + { + CString szText; + if (pDocument == nullptr) + { + szText.Preallocate(m_strTitle.GetLength() + 10); + szText = m_strTitle; + } else + { + szText.Preallocate(pDocument->GetTitle().GetLength() + 10); + szText = pDocument->GetTitle(); + if (pDocument->IsModified()) szText += _T("*"); + } + if (m_nWindow > 0) + szText.AppendFormat(_T(":%d"), m_nWindow); + + // set title if changed, but don't remove completely + AfxSetWindowText(m_hWnd, szText); + } +} + + +BOOL CChildFrame::ChangeViewClass(CRuntimeClass* pViewClass, CCreateContext* pContext) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CWnd *pWnd; + if (!strcmp(pViewClass->m_lpszClassName, m_szCurrentViewClassName)) return TRUE; + if (m_szCurrentViewClassName[0]) + { + m_szCurrentViewClassName[0] = 0; + m_wndSplitter.DeleteView(1, 0); + } + if ((m_hWndView) && (pMainFrm)) + { + if (pMainFrm->GetMidiRecordWnd() == m_hWndView) + { + pMainFrm->SetMidiRecordWnd(NULL); + } + } + m_hWndView = NULL; + if (!m_wndSplitter.CreateView(1, 0, pViewClass, CSize(0, 0), pContext)) return FALSE; + // Get 2nd window handle + if ((pWnd = m_wndSplitter.GetPane(1, 0)) != NULL) m_hWndView = pWnd->m_hWnd; + strcpy(m_szCurrentViewClassName, pViewClass->m_lpszClassName); + m_wndSplitter.RecalcLayout(); + if ((m_hWndView) && (m_hWndCtrl)) + { + ::PostMessage(m_hWndView, WM_MOD_VIEWMSG, VIEWMSG_SETCTRLWND, (LPARAM)m_hWndCtrl); + ::PostMessage(m_hWndCtrl, WM_MOD_CTRLMSG, CTRLMSG_SETVIEWWND, (LPARAM)m_hWndView); + pMainFrm->SetMidiRecordWnd(m_hWndView); + } + return TRUE; +} + +void CChildFrame::ForceRefresh() +{ + CModControlView *pModView; + if ((pModView = GetModControlView()) != nullptr) + { + pModView->ForceRefresh(); + } + + return; +} + +void CChildFrame::SavePosition(BOOL bForce) +{ + if (m_hWnd) + { + m_bMaxWhenClosed = IsZoomed() != FALSE; + if (bForce) TrackerSettings::Instance().gbMdiMaximize = m_bMaxWhenClosed; + if (!IsIconic()) + { + CWnd *pWnd = m_wndSplitter.GetPane(0, 0); + if (pWnd) + { + CRect rect(0, 0, 0, 0); + pWnd->GetWindowRect(&rect); + if(rect.Width() == 0) + return; + int l = Util::ScalePixelsInv(rect.Height(), m_hWnd); + //rewbs.varWindowSize - not the nicest piece of code, but we need to distinguish between the views: + if (strcmp(CViewGlobals::classCViewGlobals.m_lpszClassName, m_szCurrentViewClassName) == 0) + TrackerSettings::Instance().glGeneralWindowHeight = l; + else if (strcmp(CViewPattern::classCViewPattern.m_lpszClassName, m_szCurrentViewClassName) == 0) + TrackerSettings::Instance().glPatternWindowHeight = l; + else if (strcmp(CViewSample::classCViewSample.m_lpszClassName, m_szCurrentViewClassName) == 0) + TrackerSettings::Instance().glSampleWindowHeight = l; + else if (strcmp(CViewInstrument::classCViewInstrument.m_lpszClassName, m_szCurrentViewClassName) == 0) + TrackerSettings::Instance().glInstrumentWindowHeight = l; + else if (strcmp(CViewComments::classCViewComments.m_lpszClassName, m_szCurrentViewClassName) == 0) + TrackerSettings::Instance().glCommentsWindowHeight = l; + } + } + } +} + + +int CChildFrame::GetSplitterHeight() +{ + if (m_hWnd) + { + CRect rect; + + CWnd *pWnd = m_wndSplitter.GetPane(0, 0); + if (pWnd) + { + pWnd->GetWindowRect(&rect); + return Util::ScalePixelsInv(rect.Height(), m_hWnd); + } + } + return 15; // tidy default +}; + + +LRESULT CChildFrame::SendCtrlMessage(UINT uMsg, LPARAM lParam) const +{ + if(m_hWndCtrl) + return ::SendMessage(m_hWndCtrl, WM_MOD_CTRLMSG, uMsg, lParam); + return 0; +} + + +LRESULT CChildFrame::SendViewMessage(UINT uMsg, LPARAM lParam) const +{ + if(m_hWndView) + return ::SendMessage(m_hWndView, WM_MOD_VIEWMSG, uMsg, lParam); + return 0; +} + + +LRESULT CChildFrame::OnInstrumentSelected(WPARAM wParam, LPARAM lParam) +{ + CView *pView = GetActiveView(); + CModDoc *pModDoc = NULL; + if (pView) pModDoc = (CModDoc *)pView->GetDocument(); + if ((m_hWndCtrl) && (pModDoc)) + { + auto nIns = lParam; + + if ((!wParam) && (pModDoc->GetNumInstruments() > 0)) + { + nIns = pModDoc->FindSampleParent(static_cast<SAMPLEINDEX>(nIns)); + if(nIns == INSTRUMENTINDEX_INVALID) + { + nIns = 0; + } + } + ::SendMessage(m_hWndCtrl, WM_MOD_CTRLMSG, CTRLMSG_PAT_SETINSTRUMENT, nIns); + } + return 0; +} + + +///////////////////////////////////////////////////////////////////////////// +// CChildFrame message handlers + +void CChildFrame::OnDestroy() +{ + SavePosition(); + if(m_lastActiveFrame == this) + m_lastActiveFrame = nullptr; + CMDIChildWnd::OnDestroy(); +} + + +BOOL CChildFrame::OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult) +{ + auto pTTT = reinterpret_cast<TOOLTIPTEXT *>(pNMHDR); + TCHAR szFullText[256] = _T(""); + CString strTipText; + + UINT_PTR nID = pNMHDR->idFrom; + if (pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = static_cast<UINT_PTR>(::GetDlgCtrlID(reinterpret_cast<HWND>(nID))); + } + + if ((nID >= 1000) && (nID < 65536) && (m_hWndCtrl) && (::SendMessage(m_hWndCtrl, WM_MOD_GETTOOLTIPTEXT, nID, (LPARAM)szFullText))) + { + strTipText = szFullText; + } else + { + // allow top level routing frame to handle the message + if (GetRoutingFrame() != NULL) return FALSE; + if (nID != 0) // will be zero on a separator + { + AfxLoadString((UINT)nID, szFullText); + // this is the command id, not the button index + AfxExtractSubString(strTipText, szFullText, 1, _T('\n')); + } + } + mpt::String::WriteCStringBuf(pTTT->szText) = strTipText; + *pResult = 0; + + // bring the tooltip window above other popup windows + ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, + SWP_NOACTIVATE|SWP_NOSIZE|SWP_NOMOVE|SWP_NOOWNERZORDER); + + return TRUE; // message was handled +} + + +LRESULT CChildFrame::OnChangeViewClass(WPARAM wParam, LPARAM lParam) +{ + CModControlDlg *pDlg = (CModControlDlg *)lParam; + if (pDlg) + { + CRuntimeClass *pNewViewClass = pDlg->GetAssociatedViewClass(); + if (pNewViewClass) ChangeViewClass(pNewViewClass); + ::PostMessage(m_hWndCtrl, WM_MOD_CTRLMSG, CTRLMSG_ACTIVATEPAGE, (LPARAM)wParam); + } + return 0; +} + + +const char *CChildFrame::GetCurrentViewClassName() const +{ + return m_szCurrentViewClassName; +} + + +std::string CChildFrame::SerializeView() const +{ + std::ostringstream f(std::ios::out | std::ios::binary); + // Version + mpt::IO::WriteVarInt(f, 0u); + // Current page + mpt::IO::WriteVarInt(f, static_cast<uint8>(GetModControlView()->GetActivePage())); + + CModControlView *view = GetModControlView(); + if (strcmp(CViewPattern::classCViewPattern.m_lpszClassName, m_szCurrentViewClassName) == 0) + { + mpt::IO::WriteVarInt(f, (uint32)view->SendMessage(WM_MOD_CTRLMSG, CTRLMSG_GETCURRENTORDER)); // Order number + } else if (strcmp(CViewSample::classCViewSample.m_lpszClassName, m_szCurrentViewClassName) == 0) + { + mpt::IO::WriteVarInt(f, (uint32)view->SendMessage(WM_MOD_CTRLMSG, CTRLMSG_GETCURRENTINSTRUMENT)); // Sample number + } else if (strcmp(CViewInstrument::classCViewInstrument.m_lpszClassName, m_szCurrentViewClassName) == 0) + { + mpt::IO::WriteVarInt(f, (uint32)view->SendMessage(WM_MOD_CTRLMSG, CTRLMSG_GETCURRENTINSTRUMENT)); // Instrument number + } + return f.str(); +} + + +void CChildFrame::DeserializeView(FileReader &file) +{ + uint32 version, page; + if(file.ReadVarInt(version) && version == 0 && + file.ReadVarInt(page) && page >= 0 && page < CModControlView::MAX_PAGES) + { + UINT pageDlg = 0; + switch(page) + { + case CModControlView::VIEW_GLOBALS: + pageDlg = IDD_CONTROL_GLOBALS; + break; + case CModControlView::VIEW_PATTERNS: + pageDlg = IDD_CONTROL_PATTERNS; + file.ReadVarInt(m_ViewPatterns.initialOrder); + break; + case CModControlView::VIEW_SAMPLES: + pageDlg = IDD_CONTROL_SAMPLES; + file.ReadVarInt(m_ViewSamples.initialSample); + break; + case CModControlView::VIEW_INSTRUMENTS: + pageDlg = IDD_CONTROL_INSTRUMENTS; + file.ReadVarInt(m_ViewInstruments.initialInstrument); + break; + case CModControlView::VIEW_COMMENTS: + pageDlg = IDD_CONTROL_COMMENTS; + break; + } + GetModControlView()->PostMessage(WM_MOD_ACTIVATEVIEW, pageDlg, (LPARAM)-1); + } +} + + +void CChildFrame::ToggleViews() +{ + auto focus = ::GetFocus(); + if(focus == GetHwndView() || ::IsChild(GetHwndView(), focus)) + SendCtrlMessage(CTRLMSG_SETFOCUS); + else if(focus == GetHwndCtrl() || ::IsChild(GetHwndCtrl(), focus)) + SendViewMessage(VIEWMSG_SETFOCUS); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.h b/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.h new file mode 100644 index 00000000..a9ff360a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Childfrm.h @@ -0,0 +1,156 @@ +/* + * Childfrm.h + * ---------- + * Purpose: Implementation of tab interface class. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "PatternCursor.h" + +#include "../common/FileReaderFwd.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModControlView; +class CModControlDlg; + +struct GENERALVIEWSTATE +{ + PlugParamIndex nParam = 0; + CHANNELINDEX nTab = 0; + PLUGINDEX nPlugin = 0; + bool initialized = false; +}; + + +struct PATTERNVIEWSTATE +{ + PATTERNINDEX nPattern = 0; + PatternCursor cursor = 0; + PatternRect selection; + PatternCursor::Columns nDetailLevel = PatternCursor::firstColumn; + ORDERINDEX nOrder = 0; + ORDERINDEX initialOrder = ORDERINDEX_INVALID; + bool initialized = false; +}; + +struct SAMPLEVIEWSTATE +{ + SmpLength dwScrollPos = 0; + SmpLength dwBeginSel = 0; + SmpLength dwEndSel = 0; + SAMPLEINDEX nSample = 0; + SAMPLEINDEX initialSample = 0; +}; + + +struct INSTRUMENTVIEWSTATE +{ + float zoom = 4; + EnvelopeType nEnv = ENV_VOLUME; + INSTRUMENTINDEX initialInstrument = 0; + bool bGrid = false; + bool initialized = false; +}; + +struct COMMENTVIEWSTATE +{ + UINT nId = 0; + bool initialized = false; +}; + + + +class CChildFrame: public CMDIChildWnd +{ + friend class CModControlDlg; + DECLARE_DYNCREATE(CChildFrame) +public: + CChildFrame(); + +protected: + static CChildFrame *m_lastActiveFrame; + static int glMdiOpenCount; + +// Attributes +protected: + CSplitterWnd m_wndSplitter; + HWND m_hWndCtrl, m_hWndView; + GENERALVIEWSTATE m_ViewGeneral; + PATTERNVIEWSTATE m_ViewPatterns; + SAMPLEVIEWSTATE m_ViewSamples; + INSTRUMENTVIEWSTATE m_ViewInstruments; + COMMENTVIEWSTATE m_ViewComments; + CHAR m_szCurrentViewClassName[256]; + bool m_bMaxWhenClosed; + bool m_bInitialActivation; + +// Operations +public: + CModControlView *GetModControlView() const { return (CModControlView *)m_wndSplitter.GetPane(0, 0); } + BOOL ChangeViewClass(CRuntimeClass* pNewViewClass, CCreateContext* pContext=NULL); + void ForceRefresh(); + void SavePosition(BOOL bExit=FALSE); + const char *GetCurrentViewClassName() const; + LRESULT SendCtrlMessage(UINT uMsg, LPARAM lParam = 0) const; + LRESULT SendViewMessage(UINT uMsg, LPARAM lParam = 0) const; + LRESULT ActivateView(UINT nId, LPARAM lParam) { return ::SendMessage(m_hWndCtrl, WM_MOD_ACTIVATEVIEW, nId, lParam); } + HWND GetHwndCtrl() const { return m_hWndCtrl; } + HWND GetHwndView() const { return m_hWndView; } + GENERALVIEWSTATE &GetGeneralViewState() { return m_ViewGeneral; } + PATTERNVIEWSTATE &GetPatternViewState() { return m_ViewPatterns; } + SAMPLEVIEWSTATE &GetSampleViewState() { return m_ViewSamples; } + INSTRUMENTVIEWSTATE &GetInstrumentViewState() { return m_ViewInstruments; } + COMMENTVIEWSTATE &GetCommentViewState() { return m_ViewComments; } + + void SetSplitterHeight(int x); + int GetSplitterHeight(); + + std::string SerializeView() const; + void DeserializeView(FileReader &file); + + void ToggleViews(); + + static CChildFrame *LastActiveFrame() { return m_lastActiveFrame; } + +// Overrides + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(CChildFrame) + public: + BOOL OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) override; + BOOL PreCreateWindow(CREATESTRUCT& cs) override; + void ActivateFrame(int nCmdShow) override; + void OnUpdateFrameTitle(BOOL bAddToTitle) override; + //}}AFX_VIRTUAL + +// Implementation +public: + virtual ~CChildFrame(); + +// Generated message map functions +protected: + //{{AFX_MSG(CChildFrame) + afx_msg void OnDestroy(); + afx_msg BOOL OnNcActivate(BOOL bActivate); + afx_msg void OnMDIActivate(BOOL bActivate, CWnd *pActivateWnd, CWnd *pDeactivateWnd); + afx_msg LRESULT OnChangeViewClass(WPARAM, LPARAM lParam); + afx_msg LRESULT OnInstrumentSelected(WPARAM, LPARAM lParam); + afx_msg BOOL OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +///////////////////////////////////////////////////////////////////////////// + +//{{AFX_INSERT_LOCATION}} +// Microsoft Developer Studio will insert additional declarations immediately before the previous line. + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.cpp new file mode 100644 index 00000000..6120516f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.cpp @@ -0,0 +1,977 @@ +/* + * CleanupSong.cpp + * --------------- + * Purpose: Dialog for cleaning up modules (rearranging, removing unused items). + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Moddoc.h" +#include "Mainfrm.h" +#include "CleanupSong.h" +#include "../common/mptStringBuffer.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/modsmp_ctrl.h" +#include "../tracklib/SampleEdit.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Default checkbox state +bool CModCleanupDlg::m_CheckBoxes[kMaxCleanupOptions] = +{ + true, false, true, true, // patterns + false, false, // orders + true, false, false, true, // samples + true, false, // instruments + true, false, // plugins + false, true, // misc +}; + +// Checkbox -> Control ID LUT +WORD const CModCleanupDlg::m_CleanupIDtoDlgID[kMaxCleanupOptions] = +{ + // patterns + IDC_CHK_CLEANUP_PATTERNS, IDC_CHK_REMOVE_PATTERNS, + IDC_CHK_REARRANGE_PATTERNS, IDC_CHK_REMOVE_DUPLICATES, + // orders + IDC_CHK_MERGE_SEQUENCES, IDC_CHK_REMOVE_ORDERS, + // samples + IDC_CHK_CLEANUP_SAMPLES, IDC_CHK_REMOVE_SAMPLES, + IDC_CHK_REARRANGE_SAMPLES, IDC_CHK_OPTIMIZE_SAMPLES, + // instruments + IDC_CHK_CLEANUP_INSTRUMENTS, IDC_CHK_REMOVE_INSTRUMENTS, + // plugins + IDC_CHK_CLEANUP_PLUGINS, IDC_CHK_REMOVE_PLUGINS, + // misc + IDC_CHK_RESET_VARIABLES, IDC_CHK_UNUSED_CHANNELS, +}; + +// Options that are mutually exclusive to each other +CModCleanupDlg::CleanupOptions const CModCleanupDlg::m_MutuallyExclusive[CModCleanupDlg::kMaxCleanupOptions] = +{ + // patterns + kRemovePatterns, kCleanupPatterns, + kRemovePatterns, kRemovePatterns, + // orders + kRemoveOrders, kMergeSequences, + // samples + kRemoveSamples, kCleanupSamples, + kRemoveSamples, kRemoveSamples, + // instruments + kRemoveAllInstruments, kCleanupInstruments, + // plugins + kRemoveAllPlugins, kCleanupPlugins, + // misc + kNone, kNone, + +}; + +/////////////////////////////////////////////////////////////////////// +// CModCleanupDlg + +BEGIN_MESSAGE_MAP(CModCleanupDlg, CDialog) + //{{AFX_MSG_MAP(CModTypeDlg) + ON_COMMAND(IDC_BTN_CLEANUP_SONG, &CModCleanupDlg::OnPresetCleanupSong) + ON_COMMAND(IDC_BTN_COMPO_CLEANUP, &CModCleanupDlg::OnPresetCompoCleanup) + + ON_COMMAND(IDC_CHK_CLEANUP_PATTERNS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_PATTERNS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REARRANGE_PATTERNS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_DUPLICATES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_MERGE_SEQUENCES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_ORDERS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_CLEANUP_SAMPLES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_SAMPLES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REARRANGE_SAMPLES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_OPTIMIZE_SAMPLES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_CLEANUP_INSTRUMENTS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_INSTRUMENTS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_CLEANUP_PLUGINS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_REMOVE_PLUGINS, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_RESET_VARIABLES, &CModCleanupDlg::OnVerifyMutualExclusive) + ON_COMMAND(IDC_CHK_UNUSED_CHANNELS, &CModCleanupDlg::OnVerifyMutualExclusive) + + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CModCleanupDlg::OnToolTipNotify) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +BOOL CModCleanupDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + for(int i = 0; i < kMaxCleanupOptions; i++) + { + CheckDlgButton(m_CleanupIDtoDlgID[i], (m_CheckBoxes[i]) ? BST_CHECKED : BST_UNCHECKED); + } + + CSoundFile &sndFile = modDoc.GetSoundFile(); + + GetDlgItem(m_CleanupIDtoDlgID[kMergeSequences])->EnableWindow((sndFile.Order.GetNumSequences() > 1) ? TRUE : FALSE); + + GetDlgItem(m_CleanupIDtoDlgID[kRemoveSamples])->EnableWindow((sndFile.GetNumSamples() > 0) ? TRUE : FALSE); + GetDlgItem(m_CleanupIDtoDlgID[kRearrangeSamples])->EnableWindow((sndFile.GetNumSamples() > 1) ? TRUE : FALSE); + + GetDlgItem(m_CleanupIDtoDlgID[kCleanupInstruments])->EnableWindow((sndFile.GetNumInstruments() > 0) ? TRUE : FALSE); + GetDlgItem(m_CleanupIDtoDlgID[kRemoveAllInstruments])->EnableWindow((sndFile.GetNumInstruments() > 0) ? TRUE : FALSE); + + EnableToolTips(TRUE); + return TRUE; +} + + +void CModCleanupDlg::OnOK() +{ + ScopedLogCapturer logcapturer(modDoc, _T("cleanup"), this); + for(int i = 0; i < kMaxCleanupOptions; i++) + { + m_CheckBoxes[i] = IsDlgButtonChecked(m_CleanupIDtoDlgID[i]) != BST_UNCHECKED; + } + + bool modified = false; + + // Orders + if(m_CheckBoxes[kMergeSequences]) modified |= MergeSequences(); + if(m_CheckBoxes[kRemoveOrders]) modified |= RemoveAllOrders(); + + // Patterns + if(m_CheckBoxes[kRemovePatterns]) modified |= RemoveAllPatterns(); + if(m_CheckBoxes[kCleanupPatterns]) modified |= RemoveUnusedPatterns(); + if(m_CheckBoxes[kRemoveDuplicatePatterns]) modified |= RemoveDuplicatePatterns(); + if(m_CheckBoxes[kRearrangePatterns]) modified |= RearrangePatterns(); + + // Instruments + if(modDoc.GetNumInstruments() > 0) + { + if(m_CheckBoxes[kRemoveAllInstruments]) modified |= RemoveAllInstruments(); + if(m_CheckBoxes[kCleanupInstruments]) modified |= RemoveUnusedInstruments(); + } + + // Samples + if(m_CheckBoxes[kRemoveSamples]) modified |= RemoveAllSamples(); + if(m_CheckBoxes[kCleanupSamples]) modified |= RemoveUnusedSamples(); + if(m_CheckBoxes[kOptimizeSamples]) modified |= OptimizeSamples(); + if(modDoc.GetNumSamples() > 1) + { + if(m_CheckBoxes[kRearrangeSamples]) modified |= RearrangeSamples(); + } + + // Plugins + if(m_CheckBoxes[kRemoveAllPlugins]) modified |= RemoveAllPlugins(); + if(m_CheckBoxes[kCleanupPlugins]) modified |= RemoveUnusedPlugins(); + + // Create samplepack + if(m_CheckBoxes[kResetVariables]) modified |= ResetVariables(); + + // Remove unused channels + if(m_CheckBoxes[kCleanupChannels]) modified |= RemoveUnusedChannels(); + + if(modified) modDoc.SetModified(); + modDoc.UpdateAllViews(nullptr, UpdateHint().ModType()); + logcapturer.ShowLog(true); + CDialog::OnOK(); +} + + +void CModCleanupDlg::OnVerifyMutualExclusive() +{ + HWND hFocus = GetFocus()->m_hWnd; + for(int i = 0; i < kMaxCleanupOptions; i++) + { + // if this item is focussed, we have just (un)checked it. + if(hFocus == GetDlgItem(m_CleanupIDtoDlgID[i])->m_hWnd) + { + // if we just unchecked it, there's nothing to verify. + if(IsDlgButtonChecked(m_CleanupIDtoDlgID[i]) == BST_UNCHECKED) + return; + + // now we can disable all elements that are mutually exclusive. + if(m_MutuallyExclusive[i] != kNone) + CheckDlgButton(m_CleanupIDtoDlgID[m_MutuallyExclusive[i]], BST_UNCHECKED); + // find other elements which are mutually exclusive with the selected element. + for(int j = 0; j < kMaxCleanupOptions; j++) + { + if(m_MutuallyExclusive[j] == i) + CheckDlgButton(m_CleanupIDtoDlgID[j], BST_UNCHECKED); + } + return; + } + } +} + + +void CModCleanupDlg::OnPresetCleanupSong() +{ + // patterns + CheckDlgButton(IDC_CHK_CLEANUP_PATTERNS, BST_CHECKED); + CheckDlgButton(IDC_CHK_REMOVE_PATTERNS, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REARRANGE_PATTERNS, BST_CHECKED); + CheckDlgButton(IDC_CHK_REMOVE_DUPLICATES, BST_CHECKED); + // orders + CheckDlgButton(IDC_CHK_MERGE_SEQUENCES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_ORDERS, BST_UNCHECKED); + // samples + CheckDlgButton(IDC_CHK_CLEANUP_SAMPLES, BST_CHECKED); + CheckDlgButton(IDC_CHK_REMOVE_SAMPLES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REARRANGE_SAMPLES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_OPTIMIZE_SAMPLES, BST_CHECKED); + // instruments + CheckDlgButton(IDC_CHK_CLEANUP_INSTRUMENTS, BST_CHECKED); + CheckDlgButton(IDC_CHK_REMOVE_INSTRUMENTS, BST_UNCHECKED); + // plugins + CheckDlgButton(IDC_CHK_CLEANUP_PLUGINS, BST_CHECKED); + CheckDlgButton(IDC_CHK_REMOVE_PLUGINS, BST_UNCHECKED); + // misc + CheckDlgButton(IDC_CHK_SAMPLEPACK, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_UNUSED_CHANNELS, BST_CHECKED); +} + + +void CModCleanupDlg::OnPresetCompoCleanup() +{ + // patterns + CheckDlgButton(IDC_CHK_CLEANUP_PATTERNS, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_PATTERNS, BST_CHECKED); + CheckDlgButton(IDC_CHK_REARRANGE_PATTERNS, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_DUPLICATES, BST_UNCHECKED); + // orders + CheckDlgButton(IDC_CHK_MERGE_SEQUENCES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_ORDERS, BST_CHECKED); + // samples + CheckDlgButton(IDC_CHK_CLEANUP_SAMPLES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_SAMPLES, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REARRANGE_SAMPLES, BST_CHECKED); + CheckDlgButton(IDC_CHK_OPTIMIZE_SAMPLES, BST_UNCHECKED); + // instruments + CheckDlgButton(IDC_CHK_CLEANUP_INSTRUMENTS, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_INSTRUMENTS, BST_CHECKED); + // plugins + CheckDlgButton(IDC_CHK_CLEANUP_PLUGINS, BST_UNCHECKED); + CheckDlgButton(IDC_CHK_REMOVE_PLUGINS, BST_CHECKED); + // misc + CheckDlgButton(IDC_CHK_SAMPLEPACK, BST_CHECKED); + CheckDlgButton(IDC_CHK_UNUSED_CHANNELS, BST_CHECKED); +} + + +BOOL CModCleanupDlg::OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *) +{ + TOOLTIPTEXT* pTTT = (TOOLTIPTEXT*)pNMHDR; + UINT_PTR nID = pNMHDR->idFrom; + if (pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = ::GetDlgCtrlID((HWND)nID); + } + + LPCTSTR lpszText = nullptr; + switch(nID) + { + // patterns + case IDC_CHK_CLEANUP_PATTERNS: + lpszText = _T("Remove all unused patterns and rearrange them."); + break; + case IDC_CHK_REMOVE_PATTERNS: + lpszText = _T("Remove all patterns."); + break; + case IDC_CHK_REARRANGE_PATTERNS: + lpszText = _T("Number the patterns given by their order in the sequence."); + break; + case IDC_CHK_REMOVE_DUPLICATES: + lpszText = _T("Merge patterns with identical content."); + break; + // orders + case IDC_CHK_REMOVE_ORDERS: + lpszText = _T("Reset the order list."); + break; + case IDC_CHK_MERGE_SEQUENCES: + lpszText = _T("Merge multiple sequences into one."); + break; + // samples + case IDC_CHK_CLEANUP_SAMPLES: + lpszText = _T("Remove all unused samples."); + break; + case IDC_CHK_REMOVE_SAMPLES: + lpszText = _T("Remove all samples."); + break; + case IDC_CHK_REARRANGE_SAMPLES: + lpszText = _T("Reorder sample list by removing empty samples."); + break; + case IDC_CHK_OPTIMIZE_SAMPLES: + lpszText = _T("Remove unused data after the sample loop end."); + break; + // instruments + case IDC_CHK_CLEANUP_INSTRUMENTS: + lpszText = _T("Remove all unused instruments."); + break; + case IDC_CHK_REMOVE_INSTRUMENTS: + lpszText = _T("Remove all instruments and convert them to samples."); + break; + // plugins + case IDC_CHK_CLEANUP_PLUGINS: + lpszText = _T("Remove all unused plugins."); + break; + case IDC_CHK_REMOVE_PLUGINS: + lpszText = _T("Remove all plugins."); + break; + // misc + case IDC_CHK_SAMPLEPACK: + lpszText = _T("Convert the module to .IT and reset song / sample / instrument variables"); + break; + case IDC_CHK_UNUSED_CHANNELS: + lpszText = _T("Removes all empty pattern channels."); + break; + default: + lpszText = _T(""); + break; + } + pTTT->lpszText = const_cast<LPTSTR>(lpszText); + return TRUE; +} + + +/////////////////////////////////////////////////////////////////////// +// Actual cleanup implementations + +bool CModCleanupDlg::RemoveDuplicatePatterns() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + const PATTERNINDEX numPatterns = sndFile.Patterns.Size(); + std::vector<PATTERNINDEX> patternMapping(numPatterns, PATTERNINDEX_INVALID); + + BeginWaitCursor(); + CriticalSection cs; + + PATTERNINDEX foundDupes = 0; + for(PATTERNINDEX pat1 = 0; pat1 < numPatterns; pat1++) + { + if(!sndFile.Patterns.IsValidPat(pat1)) + continue; + const CPattern &pattern1 = sndFile.Patterns[pat1]; + for(PATTERNINDEX pat2 = pat1 + 1; pat2 < numPatterns; pat2++) + { + if(!sndFile.Patterns.IsValidPat(pat2) || patternMapping[pat2] != PATTERNINDEX_INVALID) + continue; + const CPattern &pattern2 = sndFile.Patterns[pat2]; + if(pattern1 == pattern2) + { + modDoc.GetPatternUndo().PrepareUndo(pat2, 0, 0, pattern2.GetNumChannels(), pattern2.GetNumRows(), "Remove Duplicate Patterns", foundDupes != 0, false); + sndFile.Patterns.Remove(pat2); + + patternMapping[pat2] = pat1; + foundDupes++; + } + } + } + + if(foundDupes != 0) + { + modDoc.AddToLog(MPT_AFORMAT("{} duplicate pattern{} merged.")(foundDupes, foundDupes == 1 ? "" : "s")); + + // Fix order list + for(auto &order : sndFile.Order) + { + for(auto &pat : order) + { + if(pat < numPatterns && patternMapping[pat] != PATTERNINDEX_INVALID) + { + pat = patternMapping[pat]; + } + } + } + } + + EndWaitCursor(); + + return foundDupes != 0; +} + + +// Remove unused patterns +bool CModCleanupDlg::RemoveUnusedPatterns() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + const PATTERNINDEX numPatterns = sndFile.Patterns.Size(); + std::vector<bool> patternUsed(numPatterns, false); + + BeginWaitCursor(); + // First, find all used patterns in all sequences. + for(auto &order : sndFile.Order) + { + for(auto pat : order) + { + if(pat < numPatterns) + { + patternUsed[pat] = true; + } + } + } + + // Remove all other patterns. + CriticalSection cs; + PATTERNINDEX numRemovedPatterns = 0; + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + if(!patternUsed[pat] && sndFile.Patterns.IsValidPat(pat)) + { + numRemovedPatterns++; + modDoc.GetPatternUndo().PrepareUndo(pat, 0, 0, sndFile.GetNumChannels(), sndFile.Patterns[pat].GetNumRows(), "Remove Unused Patterns", numRemovedPatterns != 0, false); + sndFile.Patterns.Remove(pat); + } + } + EndWaitCursor(); + + if(numRemovedPatterns) + { + modDoc.AddToLog(MPT_AFORMAT("{} pattern{} removed.")(numRemovedPatterns, numRemovedPatterns == 1 ? "" : "s")); + return true; + } + return false; +} + + +// Rearrange patterns (first pattern in order list = 0, etc...) +bool CModCleanupDlg::RearrangePatterns() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + const PATTERNINDEX numPatterns = sndFile.Patterns.Size(); + std::vector<PATTERNINDEX> newIndex(numPatterns, PATTERNINDEX_INVALID); + + bool modified = false; + + BeginWaitCursor(); + CriticalSection cs; + + // First, find all used patterns in all sequences. + PATTERNINDEX patOrder = 0; + for(auto &order : sndFile.Order) + { + for(auto &pat : order) + { + if(pat < numPatterns) + { + if(newIndex[pat] == PATTERNINDEX_INVALID) + { + newIndex[pat] = patOrder++; + } + pat = newIndex[pat]; + } + } + } + // All unused patterns are moved to the end of the pattern list. + for(PATTERNINDEX pat = 0; pat < numPatterns; pat++) + { + PATTERNINDEX &index = newIndex[pat]; + if(index == PATTERNINDEX_INVALID && sndFile.Patterns.IsValidPat(pat)) + { + index = patOrder++; + } + } + // Also need new indices for any non-existent patterns + for(auto &index : newIndex) + { + if(index == PATTERNINDEX_INVALID) + { + index = patOrder++; + } + } + + modDoc.GetPatternUndo().RearrangePatterns(newIndex); + + // Now rearrange the actual patterns + for(PATTERNINDEX i = 0; i < static_cast<PATTERNINDEX>(newIndex.size()); i++) + { + PATTERNINDEX j = newIndex[i]; + if(i == j) + continue; + while(i < j) + j = newIndex[j]; + std::swap(sndFile.Patterns[i], sndFile.Patterns[j]); + modified = true; + } + + EndWaitCursor(); + + return modified; +} + + +// Remove unused samples +bool CModCleanupDlg::RemoveUnusedSamples() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + std::vector<bool> samplesUsed(sndFile.GetNumSamples() + 1, true); + + BeginWaitCursor(); + + // Check if any samples are not referenced in the patterns (sample mode) or by an instrument (instrument mode). + // This doesn't check yet if a sample is referenced by an instrument, but actually unused in the patterns. + for(SAMPLEINDEX smp = 1; smp <= sndFile.GetNumSamples(); smp++) if (sndFile.GetSample(smp).HasSampleData()) + { + if(!modDoc.IsSampleUsed(smp)) + { + samplesUsed[smp] = false; + } + } + + SAMPLEINDEX nRemoved = sndFile.RemoveSelectedSamples(samplesUsed); + + const SAMPLEINDEX unusedInsSamples = sndFile.DetectUnusedSamples(samplesUsed); + + EndWaitCursor(); + + if(unusedInsSamples) + { + mpt::ustring s = MPT_UFORMAT("OpenMPT detected {} sample{} referenced by an instrument,\nbut not used in the song. Do you want to remove them?") + ( unusedInsSamples + , (unusedInsSamples == 1) ? U_("") : U_("s") + ); + if(Reporting::Confirm(s, "Sample Cleanup", false, false, this) == cnfYes) + { + nRemoved += sndFile.RemoveSelectedSamples(samplesUsed); + } + } + + if(nRemoved > 0) + { + modDoc.AddToLog(LogNotification, MPT_UFORMAT("{} unused sample{} removed")(nRemoved, (nRemoved == 1) ? U_("") : U_("s"))); + } + + return (nRemoved > 0); +} + + +// Check if the stereo channels of a sample contain identical data +template<typename T> +static bool ComapreStereoChannels(SmpLength length, const T *sampleData) +{ + for(SmpLength i = 0; i < length; i++, sampleData += 2) + { + if(sampleData[0] != sampleData[1]) + { + return false; + } + } + return true; +} + +// Remove unused sample data +bool CModCleanupDlg::OptimizeSamples() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + SAMPLEINDEX numLoopOpt = 0, numStereoOpt = 0; + std::vector<bool> stereoOptSamples(sndFile.GetNumSamples(), false); + + for(SAMPLEINDEX smp = 1; smp <= sndFile.GetNumSamples(); smp++) + { + const ModSample &sample = sndFile.GetSample(smp); + + // Determine how much of the sample will be played + SmpLength loopLength = sample.nLength; + if(sample.uFlags[CHN_LOOP]) + { + loopLength = sample.nLoopEnd; + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + loopLength = std::max(sample.nLoopEnd, sample.nSustainEnd); + } + } + + // Check if the sample contains identical stereo channels + if(sample.GetNumChannels() == 2) + { + bool identicalChannels = false; + if(sample.GetElementarySampleSize() == 1) + { + identicalChannels = ComapreStereoChannels(loopLength, sample.sample8()); + } else if(sample.GetElementarySampleSize() == 2) + { + identicalChannels = ComapreStereoChannels(loopLength, sample.sample16()); + } + if(identicalChannels) + { + numStereoOpt++; + stereoOptSamples[smp - 1] = true; + } + } + + if(sample.HasSampleData() && sample.nLength > loopLength + 2) numLoopOpt++; + } + if(!numLoopOpt && !numStereoOpt) return false; + + std::string s; + if(numLoopOpt) + s = MPT_AFORMAT("{} sample{} unused data after the loop end point.\n")(numLoopOpt, (numLoopOpt == 1) ? " has" : "s have"); + if(numStereoOpt) + s += MPT_AFORMAT("{} stereo sample{} actually mono.\n")(numStereoOpt, (numStereoOpt == 1) ? " is" : "s are"); + if(numLoopOpt + numStereoOpt == 1) + s += "Do you want to optimize it and remove this unused data?"; + else + s += "Do you want to optimize them and remove this unused data?"; + + if(Reporting::Confirm(s.c_str(), "Sample Optimization", false, false, this) != cnfYes) + { + return false; + } + + for(SAMPLEINDEX smp = 1; smp <= sndFile.m_nSamples; smp++) + { + ModSample &sample = sndFile.GetSample(smp); + + // Determine how much of the sample will be played + SmpLength loopLength = sample.nLength; + if(sample.uFlags[CHN_LOOP]) + { + loopLength = sample.nLoopEnd; + + // Sustain loop is played before normal loop, and it can actually be located after the normal loop. + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + loopLength = std::max(sample.nLoopEnd, sample.nSustainEnd); + } + } + + if(sample.nLength > loopLength && loopLength >= 2) + { + modDoc.GetSampleUndo().PrepareUndo(smp, sundo_delete, "Trim Unused Data", loopLength, sample.nLength); + SampleEdit::ResizeSample(sample, loopLength, sndFile); + } + + // Convert stereo samples with identical channels to mono + if(stereoOptSamples[smp - 1]) + { + modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, "Mono Conversion"); + ctrlSmp::ConvertToMono(sample, sndFile, ctrlSmp::onlyLeft); + } + } + if(numLoopOpt) + { + s = MPT_AFORMAT("{} sample loop{} optimized")(numLoopOpt, (numLoopOpt == 1) ? "" : "s"); + modDoc.AddToLog(s); + } + if(numStereoOpt) + { + s = MPT_AFORMAT("{} sample{} converted to mono")(numStereoOpt, (numStereoOpt == 1) ? "" : "s"); + modDoc.AddToLog(s); + } + return true; +} + +// Rearrange sample list +bool CModCleanupDlg::RearrangeSamples() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + if(sndFile.GetNumSamples() < 2) + return false; + + std::vector<SAMPLEINDEX> sampleMap; + sampleMap.reserve(sndFile.GetNumSamples()); + + // First, find out which sample slots are unused and create the new sample map only with used samples + for(SAMPLEINDEX i = 1; i <= sndFile.GetNumSamples(); i++) + { + if(sndFile.GetSample(i).HasSampleData()) + { + sampleMap.push_back(i); + } + } + + // Nothing found to remove... + if(sndFile.GetNumSamples() == sampleMap.size()) + { + return false; + } + + return (modDoc.ReArrangeSamples(sampleMap) != SAMPLEINDEX_INVALID); +} + + +// Remove unused instruments +bool CModCleanupDlg::RemoveUnusedInstruments() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + if(!sndFile.GetNumInstruments()) + return false; + + deleteInstrumentSamples removeSamples = doNoDeleteAssociatedSamples; + if(Reporting::Confirm("Remove samples associated with unused instruments?", "Removing unused instruments", false, false, this) == cnfYes) + { + removeSamples = deleteAssociatedSamples; + } + + BeginWaitCursor(); + + std::vector<bool> instrUsed(sndFile.GetNumInstruments()); + bool prevUsed = true, reorder = false; + INSTRUMENTINDEX numUsed = 0, lastUsed = 1; + for(INSTRUMENTINDEX i = 0; i < sndFile.GetNumInstruments(); i++) + { + instrUsed[i] = (modDoc.IsInstrumentUsed(i + 1)); + if(instrUsed[i]) + { + numUsed++; + lastUsed = i; + if(!prevUsed) + { + reorder = true; + } + } + prevUsed = instrUsed[i]; + } + + EndWaitCursor(); + + if(reorder && numUsed >= 1) + { + reorder = (Reporting::Confirm("Do you want to reorganize the remaining instruments?", "Removing unused instruments", false, false, this) == cnfYes); + } else + { + reorder = false; + } + + const INSTRUMENTINDEX numRemoved = sndFile.GetNumInstruments() - numUsed; + + if(numRemoved != 0) + { + BeginWaitCursor(); + + std::vector<INSTRUMENTINDEX> instrMap; + instrMap.reserve(sndFile.GetNumInstruments()); + for(INSTRUMENTINDEX i = 0; i < sndFile.GetNumInstruments(); i++) + { + if(instrUsed[i]) + { + instrMap.push_back(i + 1); + } else if(!reorder && i < lastUsed) + { + instrMap.push_back(INSTRUMENTINDEX_INVALID); + } + } + + modDoc.ReArrangeInstruments(instrMap, removeSamples); + + EndWaitCursor(); + + modDoc.AddToLog(LogNotification, MPT_UFORMAT("{} unused instrument{} removed")(numRemoved, (numRemoved == 1) ? U_("") : U_("s"))); + return true; + } + return false; +} + + +// Remove ununsed plugins +bool CModCleanupDlg::RemoveUnusedPlugins() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + std::vector<bool> usedmap(MAX_MIXPLUGINS, false); + + for(PLUGINDEX nPlug = 0; nPlug < MAX_MIXPLUGINS; nPlug++) + { + + // Is the plugin assigned to a channel? + for(CHANNELINDEX nChn = 0; nChn < sndFile.GetNumChannels(); nChn++) + { + if (sndFile.ChnSettings[nChn].nMixPlugin == nPlug + 1) + { + usedmap[nPlug] = true; + break; + } + } + + // Is the plugin used by an instrument? + for(INSTRUMENTINDEX nIns = 1; nIns <= sndFile.GetNumInstruments(); nIns++) + { + if (sndFile.Instruments[nIns] && (sndFile.Instruments[nIns]->nMixPlug == nPlug + 1)) + { + usedmap[nPlug] = true; + break; + } + } + + // Is the plugin assigned to master? + if(sndFile.m_MixPlugins[nPlug].IsMasterEffect()) + usedmap[nPlug] = true; + + // All outputs of used plugins count as used + if(usedmap[nPlug]) + { + if(!sndFile.m_MixPlugins[nPlug].IsOutputToMaster()) + { + PLUGINDEX output = sndFile.m_MixPlugins[nPlug].GetOutputPlugin(); + if(output != PLUGINDEX_INVALID) + { + usedmap[output] = true; + } + } + } + + } + + PLUGINDEX numRemoved = modDoc.RemovePlugs(usedmap); + if(numRemoved != 0) + { + modDoc.AddToLog(LogInformation, MPT_UFORMAT("{} unused plugin{} removed")(numRemoved, (numRemoved == 1) ? U_("") : U_("s"))); + return true; + } + return false; +} + + +// Reset variables (convert to IT, reset global/smp/ins vars, etc.) +bool CModCleanupDlg::ResetVariables() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + if(Reporting::Confirm(_T("OpenMPT will convert the module to IT format and reset all song, sample and instrument attributes to default values. Continue?"), _T("Resetting variables"), false, false, this) == cnfNo) + return false; + + // Stop play. + CMainFrame::GetMainFrame()->StopMod(&modDoc); + + BeginWaitCursor(); + CriticalSection cs; + + // Convert to IT... + modDoc.ChangeModType(MOD_TYPE_IT); + sndFile.SetDefaultPlaybackBehaviour(sndFile.GetType()); + sndFile.SetMixLevels(MixLevels::Compatible); + sndFile.m_songArtist.clear(); + sndFile.m_nTempoMode = TempoMode::Classic; + sndFile.m_SongFlags = SONG_LINEARSLIDES; + sndFile.m_MidiCfg.Reset(); + + // Global vars + sndFile.m_nDefaultTempo.Set(125); + sndFile.m_nDefaultSpeed = 6; + sndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + sndFile.m_nSamplePreAmp = 48; + sndFile.m_nVSTiVolume = 48; + sndFile.Order().SetRestartPos(0); + + if(sndFile.Order().empty()) + { + modDoc.InsertPattern(64, 0); + } + + // Reset instruments (if there are any) + for(INSTRUMENTINDEX i = 1; i <= sndFile.GetNumInstruments(); i++) if(sndFile.Instruments[i]) + { + sndFile.Instruments[i]->nFadeOut = 256; + sndFile.Instruments[i]->nGlobalVol = 64; + sndFile.Instruments[i]->nPan = 128; + sndFile.Instruments[i]->dwFlags.reset(INS_SETPANNING); + sndFile.Instruments[i]->nMixPlug = 0; + + sndFile.Instruments[i]->nVolSwing = 0; + sndFile.Instruments[i]->nPanSwing = 0; + sndFile.Instruments[i]->nCutSwing = 0; + sndFile.Instruments[i]->nResSwing = 0; + } + + for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++) + { + sndFile.InitChannel(chn); + } + + // reset samples + SampleEdit::ResetSamples(sndFile, SampleEdit::SmpResetCompo); + + cs.Leave(); + EndWaitCursor(); + + return true; +} + + +bool CModCleanupDlg::RemoveUnusedChannels() +{ + // Avoid M.K. modules to become xCHN modules if some channels are unused. + if(modDoc.GetModType() == MOD_TYPE_MOD && modDoc.GetNumChannels() == 4) + return false; + + std::vector<bool> usedChannels; + modDoc.CheckUsedChannels(usedChannels, modDoc.GetNumChannels() - modDoc.GetSoundFile().GetModSpecifications().channelsMin); + return modDoc.RemoveChannels(usedChannels); +} + + +// Remove all patterns +bool CModCleanupDlg::RemoveAllPatterns() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + if(sndFile.Patterns.Size() == 0) return false; + modDoc.GetPatternUndo().ClearUndo(); + sndFile.Patterns.ResizeArray(0); + sndFile.SetCurrentOrder(0); + return true; +} + +// Remove all orders +bool CModCleanupDlg::RemoveAllOrders() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + sndFile.Order.Initialize(); + sndFile.SetCurrentOrder(0); + return true; +} + +// Remove all samples +bool CModCleanupDlg::RemoveAllSamples() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + if (sndFile.GetNumSamples() == 0) return false; + + std::vector<bool> keepSamples(sndFile.GetNumSamples() + 1, false); + sndFile.RemoveSelectedSamples(keepSamples); + + SampleEdit::ResetSamples(sndFile, SampleEdit::SmpResetInit, 1, MAX_SAMPLES - 1); + + return true; +} + +// Remove all instruments +bool CModCleanupDlg::RemoveAllInstruments() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + if(sndFile.GetNumInstruments() == 0) return false; + + modDoc.ConvertInstrumentsToSamples(); + + for(INSTRUMENTINDEX i = 1; i <= sndFile.GetNumInstruments(); i++) + { + sndFile.DestroyInstrument(i, doNoDeleteAssociatedSamples); + } + + sndFile.m_nInstruments = 0; + return true; +} + +// Remove all plugins +bool CModCleanupDlg::RemoveAllPlugins() +{ + std::vector<bool> keepMask(MAX_MIXPLUGINS, false); + modDoc.RemovePlugs(keepMask); + return true; +} + + +bool CModCleanupDlg::MergeSequences() +{ + return modDoc.GetSoundFile().Order.MergeSequences(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.h b/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.h new file mode 100644 index 00000000..249b86a1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CleanupSong.h @@ -0,0 +1,97 @@ +/* + * CleanupSong.h + * --------------- + * Purpose: Dialog for cleaning up modules (rearranging, removing unused items). + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CModCleanupDlg: public CDialog +{ +private: + enum CleanupOptions + { + // patterns + kCleanupPatterns = 0, + kRemovePatterns, + kRearrangePatterns, + kRemoveDuplicatePatterns, + // orders + kMergeSequences, + kRemoveOrders, + // samples + kCleanupSamples, + kRemoveSamples, + kRearrangeSamples, + kOptimizeSamples, + // instruments + kCleanupInstruments, + kRemoveAllInstruments, + // plugins + kCleanupPlugins, + kRemoveAllPlugins, + // misc + kResetVariables, + kCleanupChannels, + + kNone, + kMaxCleanupOptions = kNone + }; + + CModDoc &modDoc; + static bool m_CheckBoxes[kMaxCleanupOptions]; // Checkbox state + static const WORD m_CleanupIDtoDlgID[kMaxCleanupOptions]; // Checkbox -> Control ID LUT + static const CleanupOptions m_MutuallyExclusive[kMaxCleanupOptions]; // Options that are mutually exclusive to each other. + + // Actual cleanup implementations: + // Patterns + bool RemoveDuplicatePatterns(); + bool RemoveUnusedPatterns(); // Remove unused patterns + bool RearrangePatterns(); // Rearrange patterns + bool RemoveAllPatterns(); + // Orders + bool MergeSequences(); + bool RemoveAllOrders(); + // Samples + bool RemoveUnusedSamples(); // Remove unused samples + bool RemoveAllSamples(); + bool RearrangeSamples(); // Rearrange sample list + bool OptimizeSamples(); // Remove unused sample data + // Instruments + bool RemoveUnusedInstruments(); // Remove unused instruments + bool RemoveAllInstruments(); + // Plugins + bool RemoveUnusedPlugins(); // Remove ununsed plugins + bool RemoveAllPlugins(); + // Misc + bool ResetVariables(); // Turn module into samplepack (convert to IT, remove patterns, etc.) + bool RemoveUnusedChannels(); + +public: + CModCleanupDlg(CModDoc &modParent, CWnd *parent) : CDialog(IDD_CLEANUP_SONG, parent), modDoc(modParent) { } + +protected: + //{{AFX_VIRTUAL(CModCleanupDlg) + virtual BOOL OnInitDialog(); + virtual void OnOK(); + //}}AFX_VIRTUAL + + BOOL OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult); + + //{{AFX_MSG(CModCleanupDlg) + afx_msg void OnPresetCleanupSong(); + afx_msg void OnPresetCompoCleanup(); + afx_msg void OnVerifyMutualExclusive(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Clipboard.h b/Src/external_dependencies/openmpt-trunk/mptrack/Clipboard.h new file mode 100644 index 00000000..03c8cb4a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Clipboard.h @@ -0,0 +1,105 @@ +/* +* Clipboard.h +* ----------- +* Purpose: RAII wrapper around operating system clipboard +* Notes : (currently none) +* Authors: OpenMPT Devs +* The OpenMPT source code is released under the BSD license. Read LICENSE for more details. +*/ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +class Clipboard +{ +public: + // Open clipboard for writing (size > 0) or reading (size == 0). + Clipboard(UINT clipFormat, size_t size = 0) + : m_clipFormat(clipFormat) + { + m_opened = theApp.GetMainWnd()->OpenClipboard() != FALSE; + if(size > 0) + { + if(m_opened && (m_hCpy = ::GlobalAlloc(GMEM_MOVEABLE | GMEM_DDESHARE, size)) != nullptr) + { + ::EmptyClipboard(); + m_data = mpt::as_span(static_cast<std::byte *>(::GlobalLock(m_hCpy)), size); + } + } else + { + HGLOBAL hCpy = ::GetClipboardData(m_clipFormat); + void *p = nullptr; + if(hCpy != nullptr && (p = ::GlobalLock(hCpy)) != nullptr) + { + m_data = mpt::as_span(mpt::void_cast<std::byte *>(p), ::GlobalSize(hCpy)); + } + } + } + + bool IsValid() const + { + return m_opened && m_hCpy && m_data.data(); + } + + template<typename T> + T *As() + { + return mpt::byte_cast<T*>(m_data.data()); + } + + mpt::byte_span Get() + { + return m_data; + } + + std::string_view GetString() const + { + if(m_data.data()) + return { mpt::byte_cast<const char *>(m_data.data()), m_data.size() }; + else + return {}; + } + + Clipboard operator =(mpt::const_byte_span data) + { + MPT_ASSERT(m_data.size() >= data.size()); + std::copy(data.begin(), data.end(), m_data.begin()); + return *this; + } + + template <typename T> + Clipboard operator =(const T &v) + { + mpt::const_byte_span data = mpt::as_raw_memory(v); + MPT_ASSERT(m_data.size() >= data.size()); + std::copy(data.begin(), data.end(), m_data.begin()); + return *this; + } + + void Close() + { + if(m_hCpy) + { + ::GlobalUnlock(m_hCpy); + ::SetClipboardData(m_clipFormat, static_cast<HANDLE>(m_hCpy)); + m_hCpy = nullptr; + } + if(m_opened) + { + ::CloseClipboard(); + m_opened = false; + } + } + + ~Clipboard() + { + Close(); + } + +protected: + HGLOBAL m_hCpy = nullptr; + mpt::byte_span m_data; + UINT m_clipFormat; + bool m_opened = false; +}; diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.cpp new file mode 100644 index 00000000..4fadaddb --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.cpp @@ -0,0 +1,144 @@ +/* + * CloseMainDialog.cpp + * ------------------- + * Purpose: Dialog showing a list of unsaved documents, with the ability to choose which documents should be saved or not. + * 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 "Moddoc.h" +#include "CloseMainDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(CloseMainDialog, ResizableDialog) + ON_COMMAND(IDC_BUTTON1, &CloseMainDialog::OnSaveAll) + ON_COMMAND(IDC_BUTTON2, &CloseMainDialog::OnSaveNone) + ON_COMMAND(IDC_CHECK1, &CloseMainDialog::OnSwitchFullPaths) +END_MESSAGE_MAP() + + +void CloseMainDialog::DoDataExchange(CDataExchange* pDX) +{ + ResizableDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(DoDataExchange) + DDX_Control(pDX, IDC_LIST1, m_List); + //}}AFX_DATA_MAP +} + + +CloseMainDialog::CloseMainDialog() : ResizableDialog(IDD_CLOSEDOCUMENTS) +{ +}; + + +CString CloseMainDialog::FormatTitle(const CModDoc *modDoc, bool fullPath) +{ + return MPT_CFORMAT("{} ({})") + (mpt::ToCString(modDoc->GetSoundFile().GetCharsetInternal(), modDoc->GetSoundFile().GetTitle()), + (!fullPath || modDoc->GetPathNameMpt().empty()) ? modDoc->GetTitle() : modDoc->GetPathNameMpt().ToCString()); +} + + +BOOL CloseMainDialog::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + + // Create list of unsaved documents + m_List.ResetContent(); + + CheckDlgButton(IDC_CHECK1, BST_CHECKED); + + m_List.SetRedraw(FALSE); + for(const auto &modDoc : theApp.GetOpenDocuments()) + { + if(modDoc->IsModified()) + { + int item = m_List.AddString(FormatTitle(modDoc, true)); + m_List.SetItemDataPtr(item, modDoc); + m_List.SetSel(item, TRUE); + } + } + m_List.SetRedraw(TRUE); + + if(m_List.GetCount() == 0) + { + // No modified documents... + OnOK(); + } + + return TRUE; +} + + +void CloseMainDialog::OnOK() +{ + const int count = m_List.GetCount(); + for(int i = 0; i < count; i++) + { + CModDoc *modDoc = static_cast<CModDoc *>(m_List.GetItemDataPtr(i)); + MPT_ASSERT(modDoc != nullptr); + if(m_List.GetSel(i)) + { + modDoc->ActivateWindow(); + if(modDoc->DoFileSave() == FALSE) + { + // If something went wrong, or if the user decided to cancel saving (when using "Save As"), we'll better not proceed... + OnCancel(); + return; + } + } else + { + modDoc->SetModified(FALSE); + } + } + + ResizableDialog::OnOK(); +} + + +void CloseMainDialog::OnSaveAll() +{ + if(m_List.GetCount() == 1) + m_List.SetSel(0, TRUE); // SelItemRange can't select one item: https://jeffpar.github.io/kbarchive/kb/129/Q129428/ + else + m_List.SelItemRange(TRUE, 0, m_List.GetCount() - 1); + OnOK(); +} + + +void CloseMainDialog::OnSaveNone() +{ + if(m_List.GetCount() == 1) + m_List.SetSel(0, FALSE); // SelItemRange can't select one item: https://jeffpar.github.io/kbarchive/kb/129/Q129428/ + else + m_List.SelItemRange(FALSE, 0, m_List.GetCount() - 1); + OnOK(); +} + + +// Switch between full path / filename only display +void CloseMainDialog::OnSwitchFullPaths() +{ + const int count = m_List.GetCount(); + const bool fullPath = (IsDlgButtonChecked(IDC_CHECK1) == BST_CHECKED); + m_List.SetRedraw(FALSE); + for(int i = 0; i < count; i++) + { + CModDoc *modDoc = static_cast<CModDoc *>(m_List.GetItemDataPtr(i)); + int item = m_List.InsertString(i + 1, FormatTitle(modDoc, fullPath)); + m_List.SetItemDataPtr(item, modDoc); + m_List.SetSel(item, m_List.GetSel(i)); + m_List.DeleteString(i); + } + m_List.SetRedraw(TRUE); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.h new file mode 100644 index 00000000..31f6d39a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CloseMainDialog.h @@ -0,0 +1,43 @@ +/* + * CloseMainDialog.h + * ----------------- + * Purpose: Dialog showing a list of unsaved documents, with the ability to choose which documents should be saved or not. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "InputHandler.h" +#include "ResizableDialog.h" + +OPENMPT_NAMESPACE_BEGIN + +class CloseMainDialog: public ResizableDialog +{ +protected: + CListBox m_List; + CPoint m_minSize; + BypassInputHandler m_bih; + + static CString FormatTitle(const CModDoc *modDoc, bool fullPath); + +public: + CloseMainDialog(); + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + + afx_msg void OnSaveAll(); + afx_msg void OnSaveNone(); + afx_msg void OnSwitchFullPaths(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.cpp new file mode 100644 index 00000000..1ad4bb5f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.cpp @@ -0,0 +1,560 @@ +/* + * ColorConfigDlg.cpp + * ------------------ + * Purpose: Implementation of the display setup dialog. + * 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 "Mainfrm.h" +#include "ColorConfigDlg.h" +#include "Settings.h" +#include "FileDialog.h" +#include "ColorSchemes.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + +static constexpr struct ColorDescriptions +{ + const TCHAR *name; + int previewImage; + ModColor colorIndex[3]; + const TCHAR *descText[3]; +} colorDefs[] = +{ + { _T("Pattern Editor"), 0, { MODCOLOR_BACKNORMAL, MODCOLOR_TEXTNORMAL, MODCOLOR_BACKHILIGHT }, { _T("Background:"), _T("Foreground:"), _T("Highlighted:") } }, + { _T("Active Row"), 0, { MODCOLOR_BACKCURROW, MODCOLOR_TEXTCURROW, {} }, { _T("Background:"), _T("Foreground:"), nullptr } }, + { _T("Pattern Selection"), 0, { MODCOLOR_BACKSELECTED, MODCOLOR_TEXTSELECTED, {} }, { _T("Background:"), _T("Foreground:"), nullptr } }, + { _T("Play Cursor"), 0, { MODCOLOR_BACKPLAYCURSOR, MODCOLOR_TEXTPLAYCURSOR, {} }, { _T("Background:"), _T("Foreground:"), nullptr } }, + { _T("Note Highlight"), 0, { MODCOLOR_NOTE, MODCOLOR_INSTRUMENT, MODCOLOR_VOLUME }, { _T("Note:"), _T("Instrument:"), _T("Volume:") } }, + { _T("Effect Highlight"), 0, { MODCOLOR_PANNING, MODCOLOR_PITCH, MODCOLOR_GLOBALS }, { _T("Panning Effects:"), _T("Pitch Effects:"), _T("Global Effects:") } }, + { _T("Invalid Commands"), 0, { MODCOLOR_DODGY_COMMANDS, {}, {} }, { _T("Invalid Note:"), nullptr, nullptr } }, + { _T("Channel Separator"), 0, { MODCOLOR_SEPHILITE, MODCOLOR_SEPFACE, MODCOLOR_SEPSHADOW }, { _T("Highlight:"), _T("Face:"), _T("Shadow:") } }, + { _T("Next/Prev Pattern"), 0, { MODCOLOR_BLENDCOLOR, {}, {} }, { _T("Blend Colour:"), nullptr, nullptr } }, + { _T("Sample Waveform"), 1, { MODCOLOR_SAMPLE, MODCOLOR_BACKSAMPLE, MODCOLOR_SAMPLESELECTED }, { _T("Sample Data:"), _T("Background:"), _T("Selection:") } }, + { _T("Sample Markers"), 1, { MODCOLOR_SAMPLE_LOOPMARKER, MODCOLOR_SAMPLE_SUSTAINMARKER, MODCOLOR_SAMPLE_CUEPOINT}, { _T("Loop Marker:"), _T("Sustain Marker:"), _T("Cue Point:") } }, + { _T("Instrument Editor"), 2, { MODCOLOR_ENVELOPES, MODCOLOR_ENVELOPE_RELEASE, MODCOLOR_BACKENV }, { _T("Envelopes:"), _T("Release Envelope:"), _T("Background:") } }, + { _T("VU-Meters"), 0, { MODCOLOR_VUMETER_HI, MODCOLOR_VUMETER_MED, MODCOLOR_VUMETER_LO }, { _T("Hi:"), _T("Med:"), _T("Lo:") } }, + { _T("VU-Meters (Plugins)"), 0, { MODCOLOR_VUMETER_HI_VST, MODCOLOR_VUMETER_MED_VST, MODCOLOR_VUMETER_LO_VST }, { _T("Hi:"), _T("Med:"), _T("Lo:") } } +}; + +#define PREVIEWBMP_WIDTH 88 +#define PREVIEWBMP_HEIGHT 39 + + +BEGIN_MESSAGE_MAP(COptionsColors, CPropertyPage) + ON_WM_DRAWITEM() + ON_WM_VSCROLL() + ON_CBN_SELCHANGE(IDC_COMBO1, &COptionsColors::OnColorSelChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &COptionsColors::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &COptionsColors::OnPresetChange) + ON_EN_CHANGE(IDC_PRIMARYHILITE, &COptionsColors::OnSettingsChanged) + ON_EN_CHANGE(IDC_SECONDARYHILITE, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsColors::OnSelectColor1) + ON_COMMAND(IDC_BUTTON2, &COptionsColors::OnSelectColor2) + ON_COMMAND(IDC_BUTTON3, &COptionsColors::OnSelectColor3) + ON_COMMAND(IDC_BUTTON9, &COptionsColors::OnChoosePatternFont) + ON_COMMAND(IDC_BUTTON10, &COptionsColors::OnChooseCommentFont) + ON_COMMAND(IDC_BUTTON11, &COptionsColors::OnClearWindowCache) + ON_COMMAND(IDC_LOAD_COLORSCHEME, &COptionsColors::OnLoadColorScheme) + ON_COMMAND(IDC_SAVE_COLORSCHEME, &COptionsColors::OnSaveColorScheme) + ON_COMMAND(IDC_CHECK1, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_CHECK2, &COptionsColors::OnPreviewChanged) + ON_COMMAND(IDC_CHECK3, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_CHECK4, &COptionsColors::OnPreviewChanged) + ON_COMMAND(IDC_CHECK5, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_RADIO1, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_RADIO2, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_RADIO3, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_RADIO4, &COptionsColors::OnSettingsChanged) + ON_COMMAND(IDC_RADIO5, &COptionsColors::OnSettingsChanged) +END_MESSAGE_MAP() + + +void COptionsColors::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsColors) + DDX_Control(pDX, IDC_COMBO1, m_ComboItem); + DDX_Control(pDX, IDC_COMBO2, m_ComboFont); + DDX_Control(pDX, IDC_COMBO3, m_ComboPreset); + DDX_Control(pDX, IDC_BUTTON4, m_BtnPreview); + DDX_Control(pDX, IDC_TEXT1, m_TxtColor[0]); + DDX_Control(pDX, IDC_TEXT2, m_TxtColor[1]); + DDX_Control(pDX, IDC_TEXT3, m_TxtColor[2]); + DDX_Control(pDX, IDC_SPIN1, m_ColorSpin); + //}}AFX_DATA_MAP +} + + +COptionsColors::COptionsColors() + : CPropertyPage(IDD_OPTIONS_COLORS) + , CustomColors(TrackerSettings::Instance().rgbCustomColors) +{ +} + +static CString FormatFontName(const FontSetting &font) +{ + return mpt::ToCString(font.name + U_(", ") + mpt::ufmt::val(font.size / 10)); +} + + +BOOL COptionsColors::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + m_pPreviewDib = LoadDib(MAKEINTRESOURCE(IDB_COLORSETUP)); + for (size_t i = 0; i < std::size(colorDefs); i++) + { + m_ComboItem.SetItemData(m_ComboItem.AddString(colorDefs[i].name), i); + } + m_ComboItem.SetCurSel(0); + + m_BtnColor[0].SubclassDlgItem(IDC_BUTTON1, this); + m_BtnColor[1].SubclassDlgItem(IDC_BUTTON2, this); + m_BtnColor[2].SubclassDlgItem(IDC_BUTTON3, this); + + m_BtnPreview.SetWindowPos(nullptr, + 0, 0, + Util::ScalePixels(PREVIEWBMP_WIDTH * 2, m_hWnd) + 2, Util::ScalePixels(PREVIEWBMP_HEIGHT * 2, m_hWnd) + 2, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE); + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_STDHIGHLIGHT) CheckDlgButton(IDC_CHECK1, BST_CHECKED); + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_EFFECTHILIGHT) CheckDlgButton(IDC_CHECK2, BST_CHECKED); + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_2NDHIGHLIGHT) CheckDlgButton(IDC_CHECK4, BST_CHECKED); + CheckDlgButton(IDC_CHECK5, TrackerSettings::Instance().rememberSongWindows ? BST_CHECKED : BST_UNCHECKED); + SetDlgItemInt(IDC_PRIMARYHILITE, TrackerSettings::Instance().m_nRowHighlightMeasures); + SetDlgItemInt(IDC_SECONDARYHILITE, TrackerSettings::Instance().m_nRowHighlightBeats); + CheckRadioButton(IDC_RADIO1, IDC_RADIO2, TrackerSettings::Instance().accidentalFlats ? IDC_RADIO2 : IDC_RADIO1); + CheckRadioButton(IDC_RADIO3, IDC_RADIO5, IDC_RADIO3 + static_cast<int>(TrackerSettings::Instance().defaultRainbowChannelColors.Get())); + + patternFont = TrackerSettings::Instance().patternFont; + m_ComboFont.AddString(_T("Built-in (small)")); + m_ComboFont.AddString(_T("Built-in (large)")); + m_ComboFont.AddString(_T("Built-in (small, x2)")); + m_ComboFont.AddString(_T("Built-in (large, x2)")); + m_ComboFont.AddString(_T("Built-in (small, x3)")); + m_ComboFont.AddString(_T("Built-in (large, x3)")); + int sel = 0; + if(patternFont.name == PATTERNFONT_SMALL) + { + sel = patternFont.size * 2; + } else if(patternFont.name == PATTERNFONT_LARGE) + { + sel = patternFont.size * 2 + 1; + } else + { + m_ComboFont.AddString(FormatFontName(patternFont)); + sel = 6; + } + m_ComboFont.SetCurSel(sel); + + commentFont = TrackerSettings::Instance().commentsFont; + SetDlgItemText(IDC_BUTTON10, FormatFontName(commentFont)); + + m_ComboPreset.SetRedraw(FALSE); + m_ComboPreset.InitStorage(static_cast<int>(2 + std::size(ColorSchemes)), 20 * sizeof(TCHAR)); + m_ComboPreset.AddString(_T("Choose a Colour Scheme...")); + m_ComboPreset.AddString(_T("OpenMPT (Default)")); + for(const auto &preset : ColorSchemes) + { + m_ComboPreset.SetItemDataPtr(m_ComboPreset.AddString(preset.name), const_cast<ColorScheme *>(&preset)); + } + m_ComboPreset.SetCurSel(0); + m_ComboPreset.SetRedraw(TRUE); + + m_ColorSpin.SetRange32(-1, 1); + + OnColorSelChanged(); + return TRUE; +} + + +BOOL COptionsColors::OnKillActive() +{ + int highlightMeasures = GetDlgItemInt(IDC_PRIMARYHILITE); + int highlightBeats = GetDlgItemInt(IDC_SECONDARYHILITE); + + if(highlightBeats > highlightMeasures) + { + Reporting::Warning("Error: Primary highlight must be greater than or equal secondary highlight."); + ::SetFocus(::GetDlgItem(m_hWnd, IDC_PRIMARYHILITE)); + return 0; + } + + return CPropertyPage::OnKillActive(); +} + + +void COptionsColors::OnOK() +{ + TrackerSettings::Instance().m_dwPatternSetup &= ~(PATTERN_STDHIGHLIGHT|PATTERN_2NDHIGHLIGHT|PATTERN_EFFECTHILIGHT); + if(IsDlgButtonChecked(IDC_CHECK1)) TrackerSettings::Instance().m_dwPatternSetup |= PATTERN_STDHIGHLIGHT; + if(IsDlgButtonChecked(IDC_CHECK2)) TrackerSettings::Instance().m_dwPatternSetup |= PATTERN_EFFECTHILIGHT; + if(IsDlgButtonChecked(IDC_CHECK4)) TrackerSettings::Instance().m_dwPatternSetup |= PATTERN_2NDHIGHLIGHT; + TrackerSettings::Instance().rememberSongWindows = IsDlgButtonChecked(IDC_CHECK5) != BST_UNCHECKED; + TrackerSettings::Instance().accidentalFlats = IsDlgButtonChecked(IDC_RADIO2) != BST_UNCHECKED; + + int channelColors = GetCheckedRadioButton(IDC_RADIO3, IDC_RADIO5); + if(channelColors == IDC_RADIO3) + TrackerSettings::Instance().defaultRainbowChannelColors = DefaultChannelColors::NoColors; + else if(channelColors == IDC_RADIO4) + TrackerSettings::Instance().defaultRainbowChannelColors = DefaultChannelColors::Rainbow; + else + TrackerSettings::Instance().defaultRainbowChannelColors = DefaultChannelColors::Random; + + FontSetting newPatternFont = patternFont; + const int fontSel = m_ComboFont.GetCurSel(); + switch(fontSel) + { + case 0: + case 2: + case 4: + default: + newPatternFont.name = PATTERNFONT_SMALL; + newPatternFont.size = fontSel / 2; + break; + case 1: + case 3: + case 5: + newPatternFont.name = PATTERNFONT_LARGE; + newPatternFont.size = fontSel / 2; + break; + case 6: + break; + } + TrackerSettings::Instance().patternFont = newPatternFont; + TrackerSettings::Instance().commentsFont = commentFont; + + TrackerSettings::Instance().m_nRowHighlightMeasures = GetDlgItemInt(IDC_PRIMARYHILITE); + TrackerSettings::Instance().m_nRowHighlightBeats = GetDlgItemInt(IDC_SECONDARYHILITE); + + TrackerSettings::Instance().rgbCustomColors = CustomColors; + CMainFrame::UpdateColors(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->PostMessage(WM_MOD_INVALIDATEPATTERNS, HINT_MPTOPTIONS); + CSoundFile::SetDefaultNoteNames(); + CPropertyPage::OnOK(); +} + + +BOOL COptionsColors::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_COLORS; + return CPropertyPage::OnSetActive(); +} + + +void COptionsColors::OnChoosePatternFont() +{ + LOGFONT lf; + MemsetZero(lf); + const int32 size = patternFont.size < 10 ? 120 : patternFont.size; + // Point size to pixels + lf.lfHeight = -MulDiv(size, Util::GetDPIy(m_hWnd), 720); + lf.lfWeight = patternFont.flags[FontSetting::Bold] ? FW_BOLD : FW_NORMAL; + lf.lfItalic = patternFont.flags[FontSetting::Italic] ? TRUE : FALSE; + mpt::String::WriteWinBuf(lf.lfFaceName) = mpt::ToWin(patternFont.name); + CFontDialog dlg(&lf); + dlg.m_cf.hwndOwner = m_hWnd; + if(patternFont.name != PATTERNFONT_SMALL && patternFont.name != PATTERNFONT_LARGE) + { + dlg.m_cf.lpLogFont = &lf; + } + dlg.m_cf.Flags &= ~CF_EFFECTS; + dlg.m_cf.Flags |= /*CF_FIXEDPITCHONLY | */CF_FORCEFONTEXIST | CF_NOSCRIPTSEL; + if(dlg.DoModal() == IDOK) + { + while(m_ComboFont.GetCount() > 6) + { + m_ComboFont.DeleteString(6); + } + patternFont.name = mpt::ToUnicode(dlg.GetFaceName()); + patternFont.size = dlg.GetSize(); + patternFont.flags = FontSetting::None; + if(dlg.IsBold()) patternFont.flags |= FontSetting::Bold; + if(dlg.IsItalic()) patternFont.flags |= FontSetting::Italic; + m_ComboFont.AddString(FormatFontName(patternFont)); + m_ComboFont.SetCurSel(6); + OnSettingsChanged(); + } +} + + +void COptionsColors::OnChooseCommentFont() +{ + LOGFONT lf; + MemsetZero(lf); + // Point size to pixels + lf.lfHeight = -MulDiv(commentFont.size, Util::GetDPIy(m_hWnd), 720); + lf.lfWeight = commentFont.flags[FontSetting::Bold] ? FW_BOLD : FW_NORMAL; + lf.lfItalic = commentFont.flags[FontSetting::Italic] ? TRUE : FALSE; + mpt::String::WriteWinBuf(lf.lfFaceName) = mpt::ToWin(commentFont.name); + CFontDialog dlg(&lf); + dlg.m_cf.hwndOwner = m_hWnd; + dlg.m_cf.lpLogFont = &lf; + dlg.m_cf.Flags &= ~CF_EFFECTS; + dlg.m_cf.Flags |= CF_FORCEFONTEXIST | CF_NOSCRIPTSEL; + if(dlg.DoModal() == IDOK) + { + commentFont.name = mpt::ToUnicode(dlg.GetFaceName()); + commentFont.size = dlg.GetSize(); + commentFont.flags = FontSetting::None; + if(dlg.IsBold()) commentFont.flags |= FontSetting::Bold; + if(dlg.IsItalic()) commentFont.flags |= FontSetting::Italic; + SetDlgItemText(IDC_BUTTON10, FormatFontName(commentFont)); + OnSettingsChanged(); + } +} + + +void COptionsColors::OnDrawItem(int nIdCtl, LPDRAWITEMSTRUCT lpdis) +{ + if(!lpdis || nIdCtl != IDC_BUTTON4 || !m_pPreviewDib) + { + CDialog::OnDrawItem(nIdCtl, lpdis); + return; + } + + const int img = colorDefs[m_nColorItem].previewImage; + auto &p = m_pPreviewDib->bmiColors; + if (IsDlgButtonChecked(IDC_CHECK2)) + { + p[1] = rgb2quad(CustomColors[MODCOLOR_GLOBALS]); + p[3] = rgb2quad(CustomColors[MODCOLOR_PITCH]); + p[5] = rgb2quad(CustomColors[MODCOLOR_INSTRUMENT]); + p[6] = rgb2quad(CustomColors[MODCOLOR_VOLUME]); + p[12] = rgb2quad(CustomColors[MODCOLOR_NOTE]); + p[14] = rgb2quad(CustomColors[MODCOLOR_PANNING]); + } else + { + p[1] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[3] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[5] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[6] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[12] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[14] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + } + p[4] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[8] = rgb2quad(CustomColors[MODCOLOR_TEXTNORMAL]); + p[9] = rgb2quad(CustomColors[MODCOLOR_SAMPLE]); + p[10] = rgb2quad(CustomColors[MODCOLOR_BACKNORMAL]); + p[11] = rgb2quad(CustomColors[MODCOLOR_BACKHILIGHT]); + p[13] = rgb2quad(CustomColors[MODCOLOR_ENVELOPES]); + p[15] = rgb2quad(img ? RGB(255, 255, 255) : CustomColors[MODCOLOR_BACKNORMAL]); + // Special cases: same bitmap, different palette + switch(m_nColorItem) + { + // Current Row + case 1: + p[8] = rgb2quad(CustomColors[MODCOLOR_TEXTCURROW]); + p[11] = rgb2quad(CustomColors[MODCOLOR_BACKCURROW]); + break; + // Selection + case 2: + p[5] = rgb2quad(CustomColors[MODCOLOR_TEXTSELECTED]); + p[6] = rgb2quad(CustomColors[MODCOLOR_TEXTSELECTED]); + p[8] = rgb2quad(CustomColors[MODCOLOR_TEXTSELECTED]); + p[11] = rgb2quad(CustomColors[MODCOLOR_BACKSELECTED]); + p[12] = rgb2quad(CustomColors[MODCOLOR_TEXTSELECTED]); + break; + // Play Cursor + case 3: + p[8] = rgb2quad(CustomColors[MODCOLOR_TEXTPLAYCURSOR]); + p[11] = rgb2quad(CustomColors[MODCOLOR_BACKPLAYCURSOR]); + break; + // Sample Editor + case 9: + case 10: + p[0] = rgb2quad(CustomColors[MODCOLOR_BACKSAMPLE]); + p[7] = rgb2quad(GetSysColor(COLOR_BTNFACE)); + p[8] = rgb2quad(GetSysColor(COLOR_BTNSHADOW)); + p[10] = rgb2quad(CustomColors[MODCOLOR_SAMPLE_LOOPMARKER]); + p[11] = rgb2quad(CustomColors[MODCOLOR_SAMPLE_CUEPOINT]); + p[14] = rgb2quad(CustomColors[MODCOLOR_SAMPLE_SUSTAINMARKER]); + p[15] = rgb2quad(CustomColors[MODCOLOR_SAMPLESELECTED]); + break; + // Envelope Editor + case 11: + p[0] = rgb2quad(CustomColors[MODCOLOR_BACKENV]); + p[2] = rgb2quad(CustomColors[MODCOLOR_ENVELOPE_RELEASE]); + break; + } + + CRect rect = lpdis->rcItem; + HDC hdc = lpdis->hDC; + ::DrawEdge(hdc, rect, BDR_SUNKENINNER, BF_RECT | BF_ADJUST); + StretchDIBits( hdc, + rect.left, + rect.top, + rect.Width(), + rect.Height(), + 0, + m_pPreviewDib->bmiHeader.biHeight - ((img+1) * PREVIEWBMP_HEIGHT), + m_pPreviewDib->bmiHeader.biWidth, + PREVIEWBMP_HEIGHT, + m_pPreviewDib->lpDibBits, + (LPBITMAPINFO)m_pPreviewDib, + DIB_RGB_COLORS, + SRCCOPY); +} + + +static COLORREF rgbCustomColors[16] = +{ + 0x808080, 0x0000FF, 0x00FF00, 0x00FFFF, + 0xFF0000, 0xFF00FF, 0xFFFF00, 0xFFFFFF, + 0xC0C0C0, 0x80FFFF, 0xE0E8E0, 0x606060, + 0x505050, 0x404040, 0x004000, 0x000000, +}; + + +void COptionsColors::SelectColor(int colorIndex) +{ + auto &color = CustomColors[colorDefs[m_nColorItem].colorIndex[colorIndex]]; + CHOOSECOLOR cc; + cc.lStructSize = sizeof(CHOOSECOLOR); + cc.hwndOwner = m_hWnd; + cc.hInstance = NULL; + cc.rgbResult = color; + cc.lpCustColors = rgbCustomColors; + cc.Flags = CC_RGBINIT | CC_FULLOPEN; + cc.lCustData = 0; + cc.lpfnHook = nullptr; + cc.lpTemplateName = nullptr; + if(::ChooseColor(&cc)) + { + color = cc.rgbResult; + m_BtnColor[colorIndex].SetColor(color); + OnSettingsChanged(); + } +} + + +void COptionsColors::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) +{ + CPropertyPage::OnVScroll(nSBCode, nPos, pScrollBar); + int newSel = m_ComboItem.GetCurSel() - m_ColorSpin.GetPos32(); + if(newSel >= 0) + { + m_ComboItem.SetCurSel(newSel); + OnColorSelChanged(); + } + m_ColorSpin.SetPos(0); +} + + +void COptionsColors::OnColorSelChanged() +{ + int sel = m_ComboItem.GetCurSel(); + if (sel >= 0) + { + m_nColorItem = static_cast<uint32>(m_ComboItem.GetItemData(sel)); + OnUpdateDialog(); + } +} + +void COptionsColors::OnSettingsChanged() +{ + SetModified(TRUE); +} + +void COptionsColors::OnUpdateDialog() +{ + const ColorDescriptions &cd = colorDefs[m_nColorItem]; + for(int i = 0; i < 3; i++) + { + if(cd.descText[i]) + { + m_TxtColor[i].SetWindowText(cd.descText[i]); + m_BtnColor[i].SetColor(CustomColors[cd.colorIndex[i]]); + } + m_TxtColor[i].ShowWindow(cd.descText[i] ? SW_SHOW : SW_HIDE); + m_BtnColor[i].ShowWindow(cd.descText[i] ? SW_SHOW : SW_HIDE); + } + + m_BtnPreview.Invalidate(FALSE); +} + + +void COptionsColors::OnPreviewChanged() +{ + OnSettingsChanged(); + m_BtnPreview.Invalidate(FALSE); + for(int i = 0; i < 3; i++) + { + m_BtnColor[i].SetColor(CustomColors[colorDefs[m_nColorItem].colorIndex[i]]); + } +} + + +void COptionsColors::OnPresetChange() +{ + auto curSel = m_ComboPreset.GetCurSel(); + if(curSel == 0) + return; + TrackerSettings::GetDefaultColourScheme(CustomColors); + auto scheme = static_cast<const ColorScheme *>(m_ComboPreset.GetItemDataPtr(curSel)); + if(scheme != nullptr) + { + for(const auto &c : scheme->colors) + { + CustomColors[c.id] = c.color; + } + } + OnPreviewChanged(); +} + + +void COptionsColors::OnLoadColorScheme() +{ + FileDialog dlg = OpenFileDialog() + .DefaultExtension("mptcolor") + .ExtensionFilter("OpenMPT Color Schemes|*.mptcolor||") + .WorkingDirectory(theApp.GetConfigPath()); + if(!dlg.Show(this)) return; + + // Ensure that all colours are reset (for outdated colour schemes) + TrackerSettings::GetDefaultColourScheme(CustomColors); + { + IniFileSettingsContainer file(dlg.GetFirstFile()); + for(uint32 i = 0; i < MAX_MODCOLORS; i++) + { + CustomColors[i] = file.Read<int32>(U_("Colors"), MPT_UFORMAT("Color{}")(mpt::ufmt::dec0<2>(i)), CustomColors[i]); + } + } + OnPreviewChanged(); +} + +void COptionsColors::OnSaveColorScheme() +{ + FileDialog dlg = SaveFileDialog() + .DefaultExtension("mptcolor") + .ExtensionFilter("OpenMPT Color Schemes|*.mptcolor||") + .WorkingDirectory(theApp.GetConfigPath()); + if(!dlg.Show(this)) return; + + { + IniFileSettingsContainer file(dlg.GetFirstFile()); + for(uint32 i = 0; i < MAX_MODCOLORS; i++) + { + file.Write<int32>(U_("Colors"), MPT_UFORMAT("Color{}")(mpt::ufmt::dec0<2>(i)), CustomColors[i]); + } + } +} + + +void COptionsColors::OnClearWindowCache() +{ + SettingsContainer &settings = theApp.GetSongSettings(); + // First, forget all settings... + settings.ForgetAll(); + // Then make sure they are gone for good. + ::DeleteFile(theApp.GetSongSettingsFilename().AsNative().c_str()); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.h new file mode 100644 index 00000000..1833115a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColorConfigDlg.h @@ -0,0 +1,62 @@ +/* + * ColorConfigDlg.cpp + * ------------------ + * Purpose: Implementation of the color setup dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "ColorPickerButton.h" + +OPENMPT_NAMESPACE_BEGIN + +class COptionsColors : public CPropertyPage +{ +protected: + std::array<COLORREF, MAX_MODCOLORS> CustomColors; + CComboBox m_ComboItem, m_ComboFont, m_ComboPreset; + ColorPickerButton m_BtnColor[3]; + CButton m_BtnPreview; + CSpinButtonCtrl m_ColorSpin; + CStatic m_TxtColor[3]; + MODPLUGDIB *m_pPreviewDib = nullptr; + FontSetting patternFont, commentFont; + uint32 m_nColorItem = 0; + +public: + COptionsColors(); + ~COptionsColors() { delete m_pPreviewDib; } + +protected: + BOOL OnInitDialog() override; + BOOL OnKillActive() override; + void OnOK() override; + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnSetActive() override; + void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + + void SelectColor(int colorIndex); + + afx_msg void OnChoosePatternFont(); + afx_msg void OnChooseCommentFont(); + afx_msg void OnUpdateDialog(); + afx_msg void OnDrawItem(int nIdCtl, LPDRAWITEMSTRUCT lpdis); + afx_msg void OnColorSelChanged(); + afx_msg void OnSettingsChanged(); + afx_msg void OnSelectColor1() { SelectColor(0); } + afx_msg void OnSelectColor2() { SelectColor(1); } + afx_msg void OnSelectColor3() { SelectColor(2); } + afx_msg void OnPresetChange(); + afx_msg void OnLoadColorScheme(); + afx_msg void OnSaveColorScheme(); + afx_msg void OnClearWindowCache(); + afx_msg void OnPreviewChanged(); + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.cpp new file mode 100644 index 00000000..090ab571 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.cpp @@ -0,0 +1,99 @@ +/* + * ColorPickerButton.cpp + * --------------------- + * Purpose: A button for picking UI colors + * 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 "ColorPickerButton.h" +#include "MPTrackUtil.h" +#include "Sndfile.h" + +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +void ColorPickerButton::SetColor(COLORREF color) +{ + m_color = color; + SetWindowText(MPT_TFORMAT("Colour: {}% red, {}% green, {}% blue") + (Util::muldivr(GetRValue(color), 100, 255), Util::muldivr(GetGValue(color), 100, 255), Util::muldivr(GetBValue(color), 100, 255)).c_str()); + Invalidate(FALSE); +} + + +std::optional<COLORREF> ColorPickerButton::PickColor(const CSoundFile &sndFile, CHANNELINDEX chn) +{ + static std::array<COLORREF, 16> colors = {0}; + // Build a set of currently used channel colors to be displayed in the color picker. + // Channels that are close to the currently edited channel are preferred. + std::map<COLORREF, int> usedColors; + for(CHANNELINDEX i = 0; i < sndFile.GetNumChannels(); i++) + { + auto color = sndFile.ChnSettings[i].color; + if(color == ModChannelSettings::INVALID_COLOR) + continue; + const int distance = std::abs(static_cast<int>(i) - chn); + usedColors[color] = usedColors.count(color) ? std::min(distance, usedColors[color]) : distance; + } + std::vector<std::pair<COLORREF, int>> sortedColors(usedColors.begin(), usedColors.end()); + std::sort(sortedColors.begin(), sortedColors.end(), [](const auto &l, const auto &r) { return l.second < r.second; }); + + size_t numColors = std::min(colors.size(), sortedColors.size()); + if(numColors < colors.size()) + { + // Try to keep as many currently unused colors as possible by shifting them to the end + std::stable_sort(colors.begin(), colors.end(), [&usedColors](COLORREF l, COLORREF r) { return usedColors.count(l) > usedColors.count(r); }); + } + auto col = sortedColors.begin(); + for(size_t i = 0; i < numColors; i++) + { + colors[i] = (col++)->first; + } + + CColorDialog dlg; + dlg.m_cc.hwndOwner = m_hWnd; + dlg.m_cc.rgbResult = m_color; + dlg.m_cc.Flags |= CC_RGBINIT | CC_FULLOPEN; + dlg.m_cc.lpCustColors = colors.data(); + if(dlg.DoModal() == IDOK) + { + SetColor(dlg.GetColor()); + return dlg.GetColor(); + } + + return {}; +} + + +void ColorPickerButton::DrawItem(DRAWITEMSTRUCT *dis) +{ + if(dis == nullptr) + return; + HDC hdc = dis->hDC; + CRect rect = dis->rcItem; + ::DrawEdge(hdc, rect, (dis->itemState & ODS_SELECTED) ? BDR_SUNKENINNER : BDR_RAISEDINNER, BF_RECT | BF_ADJUST); + if(m_color == ModChannelSettings::INVALID_COLOR || (dis->itemState & ODS_DISABLED)) + { + ::FillRect(hdc, rect, GetSysColorBrush(COLOR_BTNFACE)); + } else + { + ::SetDCBrushColor(hdc, m_color); + ::FillRect(hdc, rect, GetStockBrush(DC_BRUSH)); + } + if((dis->itemState & ODS_FOCUS)) + { + int offset = Util::ScalePixels(1, dis->hwndItem); + rect.DeflateRect(offset, offset); + ::DrawFocusRect(hdc, rect); + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.h b/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.h new file mode 100644 index 00000000..6a8fa548 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColorPickerButton.h @@ -0,0 +1,32 @@ +/* + * ColorPickerButton.h + * ------------------- + * Purpose: A button for picking UI colors + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +class ColorPickerButton : public CButton +{ +public: + void SetColor(COLORREF color); + std::optional<COLORREF> PickColor(const CSoundFile &sndFile, CHANNELINDEX chn); + +protected: + COLORREF m_color = 0; + + void DrawItem(DRAWITEMSTRUCT *dis) override; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColorSchemes.h b/Src/external_dependencies/openmpt-trunk/mptrack/ColorSchemes.h new file mode 100644 index 00000000..a4a834c0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColorSchemes.h @@ -0,0 +1,2564 @@ +/* + * ColorSchemes.h + * -------------- + * Purpose: Various user-contributed color schemes + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + +struct ColorScheme +{ + const TCHAR *name; + struct { ModColor id : 8; COLORREF color : 24; } colors[MAX_MODCOLORS]; +}; + +inline constexpr ColorScheme ColorSchemes[] = +{ +{ _T("Blue (FT2)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xE0, 0xE0, 0x40) }, + { MODCOLOR_BACKCURROW, RGB(0x70, 0x70, 0x70) }, + { MODCOLOR_TEXTCURROW, RGB(0xF0, 0xF0, 0x50) }, + { MODCOLOR_BACKSELECTED, RGB(0x40, 0x40, 0xA0) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x50, 0x50, 0x70) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xE0, 0xE0, 0x40) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x40, 0x80) }, + { MODCOLOR_NOTE, RGB(0xE0, 0xE0, 0x40) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x2E, 0x2E, 0x5C) }, + { MODCOLOR_SEPFACE, RGB(0x40, 0x40, 0x80) }, + { MODCOLOR_SEPHILITE, RGB(0x99, 0x99, 0xCC) }, + { MODCOLOR_BLENDCOLOR, RGB(0x2E, 0x2E, 0x5A) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x40, 0x40) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x40, 0x40, 0xA0) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xF0, 0xF0, 0x50) }, +} +}, +{ _T("Green (IT2)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0x70, 0x70, 0x70) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x68, 0x40) }, + { MODCOLOR_NOTE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x23, 0x38, 0x23) }, + { MODCOLOR_SEPFACE, RGB(0x40, 0x68, 0x40) }, + { MODCOLOR_SEPHILITE, RGB(0x94, 0xBC, 0x94) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("Buzz"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xE1, 0xDB, 0xD0) }, + { MODCOLOR_TEXTNORMAL, RGB(0x3A, 0x34, 0x27) }, + { MODCOLOR_BACKCURROW, RGB(0xC0, 0xB4, 0x9E) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xDD, 0xD7, 0xCC) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xA9, 0x99, 0x7A) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xCE, 0xC5, 0xB5) }, + { MODCOLOR_NOTE, RGB(0x00, 0x00, 0x5B) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x55, 0x55) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x5E, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x68, 0x68) }, + { MODCOLOR_PITCH, RGB(0x62, 0x62, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x66, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xAC, 0xA8, 0xA1) }, + { MODCOLOR_SEPFACE, RGB(0xD6, 0xD0, 0xC6) }, + { MODCOLOR_SEPHILITE, RGB(0xEC, 0xE8, 0xE1) }, + { MODCOLOR_BLENDCOLOR, RGB(0xE1, 0xDB, 0xD0) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("arseniiv - my color scheme 2"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xFE, 0xF9, 0xE7) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0xC6, 0xD0, 0xD7) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFB, 0xF9, 0xAA) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x71, 0x22) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xDD, 0xFF, 0x84) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD1, 0xE4, 0xD6) }, + { MODCOLOR_NOTE, RGB(0x00, 0x00, 0x80) }, + { MODCOLOR_INSTRUMENT, RGB(0x6B, 0x6B, 0xB6) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x66, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x80, 0x80) }, + { MODCOLOR_PITCH, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x9B, 0x88) }, + { MODCOLOR_VUMETER_MED, RGB(0x20, 0x74, 0xFF) }, + { MODCOLOR_VUMETER_HI, RGB(0x8B, 0x46, 0xAC) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xA0, 0xA0, 0xA0) }, + { MODCOLOR_SEPFACE, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xED, 0xDF, 0xCD) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x48, 0x48) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xEF, 0xE9, 0xDE) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x24, 0x4A, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0x88, 0x28) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFB, 0xF9, 0xAA) }, +} +}, +{ _T("barryvan - Tango"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_TEXTNORMAL, RGB(0x2E, 0x34, 0x36) }, + { MODCOLOR_BACKCURROW, RGB(0xBA, 0xBD, 0xB6) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x55, 0x57, 0x53) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x8A, 0xE2, 0x34) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xBA, 0xBD, 0xB6) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD3, 0xD7, 0xCF) }, + { MODCOLOR_NOTE, RGB(0x20, 0x4A, 0x87) }, + { MODCOLOR_INSTRUMENT, RGB(0x5C, 0x35, 0x66) }, + { MODCOLOR_VOLUME, RGB(0x4E, 0x9A, 0x06) }, + { MODCOLOR_PANNING, RGB(0xCE, 0x5C, 0x00) }, + { MODCOLOR_PITCH, RGB(0xC4, 0xA0, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xA4, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0xFC, 0xE9, 0x4F) }, + { MODCOLOR_VUMETER_MED, RGB(0xFC, 0xAF, 0x3E) }, + { MODCOLOR_VUMETER_HI, RGB(0xEF, 0x29, 0x29) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFC, 0xAF, 0x4B) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xEF, 0x29, 0x29) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPFACE, RGB(0xD4, 0xD7, 0xCF) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xD4, 0xD0, 0xC8) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x72, 0x9F, 0xCF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xCF, 0xCF, 0x72) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x8A, 0xE2, 0x34) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x72, 0x9F, 0xCF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xCF, 0xCF, 0x72) }, +} +}, +{ _T("barryvan - TangoDark"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x2E, 0x34, 0x36) }, + { MODCOLOR_TEXTNORMAL, RGB(0x88, 0x8A, 0x85) }, + { MODCOLOR_BACKCURROW, RGB(0x3A, 0x43, 0x45) }, + { MODCOLOR_TEXTCURROW, RGB(0xBA, 0xBD, 0xB6) }, + { MODCOLOR_BACKSELECTED, RGB(0x50, 0x5C, 0x5F) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x8A, 0xE2, 0x34) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x3A, 0x43, 0x45) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x3A, 0x43, 0x45) }, + { MODCOLOR_NOTE, RGB(0x72, 0x9F, 0xCF) }, + { MODCOLOR_INSTRUMENT, RGB(0xAD, 0x7F, 0xA8) }, + { MODCOLOR_VOLUME, RGB(0x8A, 0xE2, 0x34) }, + { MODCOLOR_PANNING, RGB(0xFC, 0xAF, 0x3E) }, + { MODCOLOR_PITCH, RGB(0xFC, 0xE9, 0x4F) }, + { MODCOLOR_GLOBALS, RGB(0xE9, 0xB9, 0x6E) }, + { MODCOLOR_VUMETER_LO, RGB(0xFC, 0xE9, 0x4F) }, + { MODCOLOR_VUMETER_MED, RGB(0xFC, 0xAF, 0x3E) }, + { MODCOLOR_VUMETER_HI, RGB(0xEF, 0x29, 0x29) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFC, 0xAF, 0x3E) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xEF, 0x29, 0x29) }, + { MODCOLOR_SEPSHADOW, RGB(0x62, 0x66, 0x67) }, + { MODCOLOR_SEPFACE, RGB(0x91, 0x93, 0x8F) }, + { MODCOLOR_SEPHILITE, RGB(0xA9, 0xAD, 0xAE) }, + { MODCOLOR_BLENDCOLOR, RGB(0x22, 0x27, 0x28) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x72, 0x9F, 0xCF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xCF, 0xCF, 0x72) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x8A, 0xE2, 0x34) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x72, 0x9F, 0xCF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xCF, 0xCF, 0x72) }, +} +}, +{ _T("Fasttracker 2 (Why Colors?)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_BACKCURROW, RGB(0x49, 0x75, 0x82) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0x5F) }, + { MODCOLOR_BACKSELECTED, RGB(0x28, 0x28, 0x28) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x49, 0x75, 0x82) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x27, 0x41, 0x47) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_VOLUME, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_PANNING, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_VUMETER_LO, RGB(0x4B, 0x7C, 0x89) }, + { MODCOLOR_VUMETER_MED, RGB(0x5F, 0x98, 0xA7) }, + { MODCOLOR_VUMETER_HI, RGB(0xEA, 0x1C, 0x78) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xEA, 0x1C, 0x78) }, + { MODCOLOR_SEPSHADOW, RGB(0x18, 0x28, 0x2C) }, + { MODCOLOR_SEPFACE, RGB(0x49, 0x75, 0x82) }, + { MODCOLOR_SEPHILITE, RGB(0x8A, 0xDB, 0xF3) }, + { MODCOLOR_BLENDCOLOR, RGB(0x27, 0x41, 0x47) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x27, 0x41, 0x47) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xFF, 0x82) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x49, 0x75, 0x82) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x8A, 0xDB, 0xF3) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x5F) }, +} +}, +{ _T("herodotas - darkwater"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x49, 0x93) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x55, 0x55) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_BACKSELECTED, RGB(0x48, 0xC4, 0xFF) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x00, 0xBF, 0xBF) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x00, 0x88) }, + { MODCOLOR_NOTE, RGB(0x00, 0xA5, 0xF4) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0xA5, 0xF4) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0x00, 0x93, 0x93) }, + { MODCOLOR_GLOBALS, RGB(0x00, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x40, 0x80, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_SEPSHADOW, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_SEPFACE, RGB(0x40, 0x80, 0x80) }, + { MODCOLOR_SEPHILITE, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x16, 0x2D) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x00, 0x55, 0x7D) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x16, 0x2D) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x00, 0x80, 0xC0) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("herodotas - grey&yellow"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x23, 0x23, 0x23) }, + { MODCOLOR_TEXTNORMAL, RGB(0xC7, 0xC7, 0xC7) }, + { MODCOLOR_BACKCURROW, RGB(0x88, 0x7D, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0xAA, 0xAA, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0x9F) }, + { MODCOLOR_SAMPLE, RGB(0xE4, 0xD5, 0x0C) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x86, 0x9D, 0x02) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x48, 0x48, 0x48) }, + { MODCOLOR_NOTE, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0xF1, 0xCD, 0x01) }, + { MODCOLOR_PANNING, RGB(0x4A, 0x8E, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xD9, 0xD9, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x64, 0xFF, 0x37) }, + { MODCOLOR_VUMETER_LO, RGB(0x64, 0xFF, 0x37) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xD6, 0x82, 0x03) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x20, 0xFF, 0x8F) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xE0, 0x75) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_SEPFACE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPHILITE, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_BLENDCOLOR, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_BACKSAMPLE, RGB(0x2C, 0x2C, 0x2C) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_BACKENV, RGB(0x28, 0x28, 0x28) }, + { MODCOLOR_ENVELOPES, RGB(0xF2, 0xBA, 0x11) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xE4, 0xD5, 0x0C) }, +} +}, +{ _T("herodotas - junglist"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xFF, 0xFF, 0x9F) }, + { MODCOLOR_BACKCURROW, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xDF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x9D, 0x00, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_VOLUME, RGB(0xFE, 0x12, 0x18) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x80) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x4A, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x35, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("herodotas - mortician"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKCURROW, RGB(0x70, 0x70, 0x70) }, + { MODCOLOR_TEXTCURROW, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_BACKSELECTED, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x50, 0x50, 0x50) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x37, 0x37, 0x37) }, + { MODCOLOR_NOTE, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_INSTRUMENT, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_VOLUME, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_PANNING, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_PITCH, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_SEPHILITE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BLENDCOLOR, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC8, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xCA, 0xCA, 0xCA) }, +} +}, +{ _T("Hot Dog Stand"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0xFF, 0xC0, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_BACKSELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0xFF, 0xE8, 0x00) }, + { MODCOLOR_NOTE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_PANNING, RGB(0xC0, 0x80, 0x00) }, + { MODCOLOR_PITCH, RGB(0xC0, 0x40, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_SEPFACE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xFF, 0xC0, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xE8, 0x00) }, +} +}, +{ _T("Impulse Tracker (Camouflage)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_BACKCURROW, RGB(0x10, 0x35, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xEB, 0xEB, 0xCB) }, + { MODCOLOR_BACKSELECTED, RGB(0x20, 0x55, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xEB, 0xEB, 0xCB) }, + { MODCOLOR_SAMPLE, RGB(0x8E, 0x14, 0x55) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x59, 0x41, 0x3C) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_BACKHILIGHT, RGB(0x59, 0x41, 0x3C) }, + { MODCOLOR_NOTE, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_INSTRUMENT, RGB(0x38, 0x9E, 0x75) }, + { MODCOLOR_VOLUME, RGB(0xA2, 0xA2, 0xA2) }, + { MODCOLOR_PANNING, RGB(0x18, 0x75, 0x2C) }, + { MODCOLOR_PITCH, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_GLOBALS, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_VUMETER_LO, RGB(0x25, 0x7D, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xEC, 0x99, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x71, 0xB5) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xEC, 0x99, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xB6, 0x96, 0x79) }, + { MODCOLOR_SEPFACE, RGB(0xB6, 0x96, 0x79) }, + { MODCOLOR_SEPHILITE, RGB(0xB6, 0x96, 0x79) }, + { MODCOLOR_BLENDCOLOR, RGB(0x35, 0x27, 0x24) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xEB, 0xEB, 0xCB) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xA2, 0xA2, 0xA2) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x38, 0x9E, 0x75) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xEB, 0xEB, 0xCB) }, +} +}, +{ _T("jmkz - Aqua LCD"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x0F, 0x13) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x66, 0x80) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x66, 0x80) }, + { MODCOLOR_TEXTCURROW, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x66, 0x80) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x99, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x4A, 0x4A, 0x4A) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x33, 0x40) }, + { MODCOLOR_NOTE, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x99, 0xBF) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x99, 0xBF) }, + { MODCOLOR_PANNING, RGB(0x00, 0xB3, 0xDF) }, + { MODCOLOR_PITCH, RGB(0x00, 0xB3, 0xDF) }, + { MODCOLOR_GLOBALS, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x66, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_VUMETER_HI, RGB(0x60, 0xDF, 0xFF) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x01, 0xA6, 0xFE) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x60, 0xDF, 0xFF) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0x80, 0x9F) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x4D, 0x60) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x33, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xCC, 0xFF) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0x99, 0xBF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0x00, 0xB3, 0xDF) }, +} +}, +{ _T("jmkz - BeRoTracker"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x1E, 0x2C, 0x44) }, + { MODCOLOR_TEXTNORMAL, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_BACKCURROW, RGB(0xDB, 0x80, 0x24) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0xE0, 0xB3, 0x85) }, + { MODCOLOR_TEXTSELECTED, RGB(0x2C, 0x39, 0x43) }, + { MODCOLOR_SAMPLE, RGB(0x48, 0xF3, 0xA7) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xB9, 0xCC, 0xD7) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x1E, 0x2C, 0x44) }, + { MODCOLOR_BACKHILIGHT, RGB(0x64, 0x7A, 0x91) }, + { MODCOLOR_NOTE, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_INSTRUMENT, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_VOLUME, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_PANNING, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_PITCH, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_GLOBALS, RGB(0xD2, 0xF3, 0xF7) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x9B, 0xF9) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x38, 0x47, 0x58) }, + { MODCOLOR_SEPFACE, RGB(0x38, 0x47, 0x58) }, + { MODCOLOR_SEPHILITE, RGB(0x38, 0x47, 0x58) }, + { MODCOLOR_BLENDCOLOR, RGB(0x4D, 0x61, 0x85) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x48, 0xF3, 0xA7) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x48, 0xF3, 0xA7) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x64, 0x7A, 0x91) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xD2, 0xF3, 0xF7) }, +} +}, +{ _T("jmkz - Classic"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xF4, 0xF1, 0xEC) }, + { MODCOLOR_TEXTNORMAL, RGB(0x54, 0x47, 0x2C) }, + { MODCOLOR_BACKCURROW, RGB(0xFF, 0xE2, 0x9F) }, + { MODCOLOR_TEXTCURROW, RGB(0x51, 0x46, 0x2F) }, + { MODCOLOR_BACKSELECTED, RGB(0x8F, 0x85, 0x70) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xEF, 0xCE) }, + { MODCOLOR_SAMPLE, RGB(0x8F, 0xEF, 0x8F) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xFF, 0xEE, 0xCE) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x54, 0x47, 0x2C) }, + { MODCOLOR_BACKHILIGHT, RGB(0xE8, 0xE2, 0xD9) }, + { MODCOLOR_NOTE, RGB(0x28, 0x23, 0x17) }, + { MODCOLOR_INSTRUMENT, RGB(0x48, 0x43, 0x37) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x80, 0x80) }, + { MODCOLOR_PITCH, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x40, 0xFF, 0x40) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x40) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPFACE, RGB(0xD5, 0xD0, 0xC8) }, + { MODCOLOR_SEPHILITE, RGB(0xEC, 0xEC, 0xEC) }, + { MODCOLOR_BLENDCOLOR, RGB(0xBA, 0xAF, 0x9A) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xEF, 0xD0, 0x8F) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x8F, 0xEF, 0x8F) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xEF, 0xD0, 0x8F) }, +} +}, +{ _T("jmkz - Custom Colors"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x20, 0x21, 0x26) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKCURROW, RGB(0x52, 0x4C, 0x34) }, + { MODCOLOR_TEXTCURROW, RGB(0xAE, 0xA3, 0x84) }, + { MODCOLOR_BACKSELECTED, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_TEXTSELECTED, RGB(0xD8, 0xD8, 0xD8) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xB0, 0xB0) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x51, 0x5C, 0x7B) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xAD, 0xB3, 0xCB) }, + { MODCOLOR_BACKHILIGHT, RGB(0x31, 0x33, 0x3E) }, + { MODCOLOR_NOTE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_INSTRUMENT, RGB(0x4B, 0x8E, 0xD8) }, + { MODCOLOR_VOLUME, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xB0, 0xB0) }, + { MODCOLOR_PITCH, RGB(0xC4, 0xC4, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_VUMETER_LO, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xCE, 0x0B, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x0C, 0x0C, 0xDC) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x0C, 0x0C, 0xDC) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xCE, 0x0B, 0x0B) }, + { MODCOLOR_SEPSHADOW, RGB(0x18, 0x1A, 0x1D) }, + { MODCOLOR_SEPFACE, RGB(0x40, 0x41, 0x4A) }, + { MODCOLOR_SEPHILITE, RGB(0x74, 0x77, 0x89) }, + { MODCOLOR_BLENDCOLOR, RGB(0x40, 0x41, 0x53) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xC4, 0xC4, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xB0, 0xB0) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xEA, 0xEA, 0x00) }, +} +}, +{ _T("jmkz - Custom Colors (inverted)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xDF, 0xDE, 0xE3) }, + { MODCOLOR_TEXTNORMAL, RGB(0x7F, 0x7F, 0x7F) }, + { MODCOLOR_BACKCURROW, RGB(0xAD, 0xB3, 0xCB) }, + { MODCOLOR_TEXTCURROW, RGB(0x51, 0x5C, 0x7B) }, + { MODCOLOR_BACKSELECTED, RGB(0x7F, 0x7F, 0x7F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x27, 0x27, 0x27) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x4F, 0x4F) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xAE, 0xA3, 0x84) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xCE, 0xCC, 0xC1) }, + { MODCOLOR_NOTE, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_INSTRUMENT, RGB(0xB4, 0x71, 0x27) }, + { MODCOLOR_VOLUME, RGB(0xF6, 0x4C, 0xFF) }, + { MODCOLOR_PANNING, RGB(0xFF, 0x4F, 0x4F) }, + { MODCOLOR_PITCH, RGB(0x3B, 0x3B, 0xFF) }, + { MODCOLOR_GLOBALS, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_VUMETER_LO, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x09, 0xB3, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xCE, 0x0B, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x0C, 0x0C, 0xDC) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x0C, 0x0C, 0xDC) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xCE, 0x0B, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x8B, 0x88, 0x76) }, + { MODCOLOR_SEPFACE, RGB(0xBF, 0xBE, 0xB5) }, + { MODCOLOR_SEPHILITE, RGB(0xE7, 0xE5, 0xE2) }, + { MODCOLOR_BLENDCOLOR, RGB(0xBF, 0xBE, 0xAC) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x3B, 0x3B, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("jmkz - Dark Pastels"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_TEXTNORMAL, RGB(0x5F, 0x5F, 0x5F) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x31, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x9F, 0xFF, 0x9F) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x31) }, + { MODCOLOR_TEXTSELECTED, RGB(0x9F, 0x9F, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x40, 0x80, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x31, 0x00, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0x9F, 0x9F) }, + { MODCOLOR_BACKHILIGHT, RGB(0x17, 0x17, 0x17) }, + { MODCOLOR_NOTE, RGB(0xDF, 0xDF, 0xDF) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0xBF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0xBF, 0xFF, 0x80) }, + { MODCOLOR_PANNING, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xDF, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x00, 0x80) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_SEPFACE, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_SEPHILITE, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_BLENDCOLOR, RGB(0x4F, 0x4F, 0x4F) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x80, 0x60, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x9F, 0xFF, 0x9F) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x9F, 0x9F, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xDF, 0x80) }, +} +}, +{ _T("jmkz - Deep Sea"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x20, 0x40) }, + { MODCOLOR_TEXTNORMAL, RGB(0xAF, 0xAF, 0xAF) }, + { MODCOLOR_BACKCURROW, RGB(0x60, 0x90, 0x30) }, + { MODCOLOR_TEXTCURROW, RGB(0xDF, 0xEF, 0xCF) }, + { MODCOLOR_BACKSELECTED, RGB(0x90, 0x30, 0x30) }, + { MODCOLOR_TEXTSELECTED, RGB(0xDF, 0xDF, 0xDF) }, + { MODCOLOR_SAMPLE, RGB(0x9F, 0xFF, 0x40) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x30, 0x60, 0x90) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xDF, 0xDF, 0xDF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x33, 0x3F, 0x4A) }, + { MODCOLOR_NOTE, RGB(0xED, 0xEF, 0xF1) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0xBF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0xC0, 0xFF, 0x80) }, + { MODCOLOR_PANNING, RGB(0x80, 0xC0, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xE0, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x00, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x20, 0x40) }, + { MODCOLOR_SEPFACE, RGB(0x9F, 0x9F, 0x9F) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x20, 0x40) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x60, 0xBF) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xCF, 0x40) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x80, 0xC0, 0xFF) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xC0, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x80, 0xBF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xE0, 0x80) }, +} +}, +{ _T("jmkz - Desert Mint"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x2C, 0x32, 0x2C) }, + { MODCOLOR_TEXTNORMAL, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_BACKCURROW, RGB(0x70, 0x70, 0x70) }, + { MODCOLOR_TEXTCURROW, RGB(0xF0, 0xF0, 0x50) }, + { MODCOLOR_BACKSELECTED, RGB(0x70, 0x96, 0x70) }, + { MODCOLOR_TEXTSELECTED, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_SAMPLE, RGB(0x53, 0x9A, 0x4B) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x4D, 0x4D, 0x4D) }, + { MODCOLOR_NOTE, RGB(0x75, 0xCE, 0x40) }, + { MODCOLOR_INSTRUMENT, RGB(0xE0, 0xDE, 0x83) }, + { MODCOLOR_VOLUME, RGB(0x9A, 0xE0, 0x8F) }, + { MODCOLOR_PANNING, RGB(0x00, 0xB0, 0xB0) }, + { MODCOLOR_PITCH, RGB(0xD2, 0xD2, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x55, 0x55) }, + { MODCOLOR_VUMETER_LO, RGB(0xB4, 0xCC, 0xB0) }, + { MODCOLOR_VUMETER_MED, RGB(0x65, 0xB4, 0x52) }, + { MODCOLOR_VUMETER_HI, RGB(0x10, 0x80, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xB0, 0xB4, 0xCC) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x52, 0x65, 0xB4) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x00, 0x10, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_SEPFACE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPHILITE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BLENDCOLOR, RGB(0x66, 0x8A, 0x66) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x9B, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xF0, 0xF0, 0x50) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x75, 0xCE, 0x40) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xB0, 0xB0) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xF0, 0xF0, 0x50) }, +} +}, +{ _T("jmkz - EL Glow"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x0C, 0x0C, 0x0C) }, + { MODCOLOR_TEXTNORMAL, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKCURROW, RGB(0x5F, 0x5F, 0x5F) }, + { MODCOLOR_TEXTCURROW, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_BACKSELECTED, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x9F, 0x9F, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xBF, 0xBF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x1F, 0x1F, 0x1F) }, + { MODCOLOR_NOTE, RGB(0xEE, 0xEE, 0xEE) }, + { MODCOLOR_INSTRUMENT, RGB(0xFE, 0xDF, 0x81) }, + { MODCOLOR_VOLUME, RGB(0xBF, 0x81, 0xFE) }, + { MODCOLOR_PANNING, RGB(0xFF, 0x80, 0x86) }, + { MODCOLOR_PITCH, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x60, 0x80, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0x0F, 0x0F, 0x0F) }, + { MODCOLOR_SEPFACE, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_SEPHILITE, RGB(0x0F, 0x0F, 0x0F) }, + { MODCOLOR_BLENDCOLOR, RGB(0x5F, 0x5F, 0x5F) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFE, 0xDF, 0x81) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x9F, 0x9F, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFE, 0xDF, 0x81) }, +} +}, +{ _T("jmkz - Engrayed"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x2C, 0x2C, 0x2C) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKCURROW, RGB(0x50, 0x50, 0x50) }, + { MODCOLOR_TEXTCURROW, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_BACKSELECTED, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xC7, 0xC7, 0xC7) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x27, 0x27, 0x27) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKHILIGHT, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_NOTE, RGB(0xD3, 0xD3, 0xD3) }, + { MODCOLOR_INSTRUMENT, RGB(0x9E, 0x9E, 0x9E) }, + { MODCOLOR_VOLUME, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_PANNING, RGB(0x9B, 0x9B, 0x9B) }, + { MODCOLOR_PITCH, RGB(0x67, 0x67, 0x67) }, + { MODCOLOR_GLOBALS, RGB(0xA3, 0xA3, 0xA3) }, + { MODCOLOR_VUMETER_LO, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_VUMETER_MED, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_VUMETER_HI, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xA0, 0xA0, 0xA0) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_SEPFACE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_SEPHILITE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BLENDCOLOR, RGB(0x67, 0x67, 0x67) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xAC, 0xAC, 0xAC) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xDC, 0xDC, 0xDC) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC8, 0xC8, 0xC8) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xF0, 0xF0, 0xF0) }, +} +}, +{ _T("jmkz - GeneGreen"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x37, 0x09) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xE0, 0x40, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("jmkz - Harmony"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xF0, 0xF8, 0xFF) }, + { MODCOLOR_TEXTNORMAL, RGB(0x13, 0x4D, 0x86) }, + { MODCOLOR_BACKCURROW, RGB(0xCC, 0xE6, 0xFF) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x4D, 0x99) }, + { MODCOLOR_BACKSELECTED, RGB(0xAA, 0xD5, 0xFF) }, + { MODCOLOR_TEXTSELECTED, RGB(0x13, 0x4D, 0x86) }, + { MODCOLOR_SAMPLE, RGB(0x99, 0xFF, 0x33) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x80, 0xBF, 0xFF) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xF4, 0xF4, 0xF4) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD9, 0xEB, 0xFF) }, + { MODCOLOR_NOTE, RGB(0x00, 0x30, 0x60) }, + { MODCOLOR_INSTRUMENT, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_VOLUME, RGB(0x33, 0x99, 0xFF) }, + { MODCOLOR_PANNING, RGB(0x99, 0x33, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0x99, 0x33) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x33, 0x99) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xA0, 0xA0, 0xA0) }, + { MODCOLOR_SEPFACE, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x33, 0xFF, 0x99) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x99, 0xFF, 0x33) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xAA, 0xD5, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xAA, 0x55) }, +} +}, +{ _T("jmkz - High Contrast"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x80, 0x00, 0x80) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x30, 0x30, 0x30) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("jmkz - Matrix"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x60, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x4B, 0x4B, 0x4B) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_NOTE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xDF, 0x00) }, + { MODCOLOR_PITCH, RGB(0x00, 0xDF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0xC0, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0x9F, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x60, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0x00, 0xE6, 0x00) }, +} +}, +{ _T("jmkz - Midnite Blue"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x27, 0x27, 0x78) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x00, 0x9F) }, + { MODCOLOR_TEXTCURROW, RGB(0x7F, 0x7F, 0x7F) }, + { MODCOLOR_BACKSELECTED, RGB(0x1F, 0x1F, 0x1F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x00, 0x9F) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x23, 0x23, 0x23) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x00, 0x40) }, + { MODCOLOR_NOTE, RGB(0xD3, 0xD3, 0xD3) }, + { MODCOLOR_INSTRUMENT, RGB(0x9E, 0x9E, 0x9E) }, + { MODCOLOR_VOLUME, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_PANNING, RGB(0x9B, 0x9B, 0x9B) }, + { MODCOLOR_PITCH, RGB(0x67, 0x67, 0x67) }, + { MODCOLOR_GLOBALS, RGB(0xA3, 0xA3, 0xA3) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x00, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0x00, 0x00, 0xBF) }, + { MODCOLOR_VUMETER_HI, RGB(0x40, 0x40, 0xFF) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x15, 0x4E, 0xE3) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x44, 0x73, 0xEE) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x69, 0x8E, 0xF1) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x00, 0x40) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xBF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0x00, 0xC8) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0x00, 0xA0) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0x62, 0x62, 0xFF) }, +} +}, +{ _T("jmkz - Midnite Green"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x27, 0x78, 0x27) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x9F, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_BACKSELECTED, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x9F, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x5D, 0x5D, 0x5D) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_NOTE, RGB(0xD3, 0xD3, 0xD3) }, + { MODCOLOR_INSTRUMENT, RGB(0x9E, 0x9E, 0x9E) }, + { MODCOLOR_VOLUME, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_PANNING, RGB(0x9B, 0x9B, 0x9B) }, + { MODCOLOR_PITCH, RGB(0x67, 0x67, 0x67) }, + { MODCOLOR_GLOBALS, RGB(0xA3, 0xA3, 0xA3) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0xC0, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x00, 0xE0, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x40, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0xBF, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xA0, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0x00, 0xDC, 0x00) }, +} +}, +{ _T("jmkz - Midnite Red"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x78, 0x27, 0x27) }, + { MODCOLOR_BACKCURROW, RGB(0x9F, 0x00, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x7F, 0x7F, 0x7F) }, + { MODCOLOR_BACKSELECTED, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x9F, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x00, 0x00) }, + { MODCOLOR_NOTE, RGB(0xD3, 0xD3, 0xD3) }, + { MODCOLOR_INSTRUMENT, RGB(0x9E, 0x9E, 0x9E) }, + { MODCOLOR_VOLUME, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_PANNING, RGB(0x9B, 0x9B, 0x9B) }, + { MODCOLOR_PITCH, RGB(0x67, 0x67, 0x67) }, + { MODCOLOR_GLOBALS, RGB(0xA3, 0xA3, 0xA3) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xE0, 0x20, 0x20) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x40, 0x00, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xC8, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xB4, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0x00, 0x00) }, +} +}, +{ _T("jmkz - Mint"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x1F, 0x1F, 0x1F) }, + { MODCOLOR_TEXTNORMAL, RGB(0x9F, 0x9F, 0x9F) }, + { MODCOLOR_BACKCURROW, RGB(0x3C, 0x3C, 0x84) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_BACKSELECTED, RGB(0x7F, 0x7F, 0x7F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xFF, 0x80) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x36, 0x36, 0x50) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_BACKHILIGHT, RGB(0x2E, 0x2E, 0x2E) }, + { MODCOLOR_NOTE, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_INSTRUMENT, RGB(0xBF, 0xFF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_PANNING, RGB(0x80, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0x60, 0xFF, 0x60) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x60) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x60, 0x60) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x60, 0x60, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x60) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x60, 0x60) }, + { MODCOLOR_SEPSHADOW, RGB(0x2E, 0x2E, 0x2E) }, + { MODCOLOR_SEPFACE, RGB(0x9F, 0x9F, 0x9F) }, + { MODCOLOR_SEPHILITE, RGB(0x2E, 0x2E, 0x2E) }, + { MODCOLOR_BLENDCOLOR, RGB(0x9F, 0x9F, 0x9F) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xBF, 0xBF, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xBF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x80) }, +} +}, +{ _T("jmkz - MT2 XP Classic"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x36, 0x36, 0x2F) }, + { MODCOLOR_TEXTNORMAL, RGB(0xEC, 0xE9, 0xD8) }, + { MODCOLOR_BACKCURROW, RGB(0xE0, 0xDF, 0xE3) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x4D, 0x5D, 0x85) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xAC, 0xA8, 0x99) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x4D, 0x61, 0x85) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x55, 0x54, 0x4A) }, + { MODCOLOR_NOTE, RGB(0xEC, 0xE9, 0xD2) }, + { MODCOLOR_INSTRUMENT, RGB(0xCF, 0xC8, 0x8F) }, + { MODCOLOR_VOLUME, RGB(0xE2, 0xDE, 0xBC) }, + { MODCOLOR_PANNING, RGB(0xE2, 0xDE, 0xBC) }, + { MODCOLOR_PITCH, RGB(0xE2, 0xDE, 0xBC) }, + { MODCOLOR_GLOBALS, RGB(0xEC, 0xE9, 0xD2) }, + { MODCOLOR_VUMETER_LO, RGB(0x4D, 0x61, 0x85) }, + { MODCOLOR_VUMETER_MED, RGB(0x85, 0x97, 0xBE) }, + { MODCOLOR_VUMETER_HI, RGB(0xC0, 0xD2, 0xFB) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x80, 0x93, 0xB5) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x85, 0x97, 0xBE) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xC0, 0xD2, 0xFB) }, + { MODCOLOR_SEPSHADOW, RGB(0x71, 0x6F, 0x64) }, + { MODCOLOR_SEPFACE, RGB(0x9D, 0x9D, 0xA1) }, + { MODCOLOR_SEPHILITE, RGB(0xF1, 0xEF, 0xE2) }, + { MODCOLOR_BLENDCOLOR, RGB(0x4D, 0x61, 0x85) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xAC, 0xA8, 0x99) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x46, 0xA6, 0x4E) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x4D, 0x61, 0x85) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xEC, 0xE9, 0xD2) }, +} +}, +{ _T("jmkz - Night Vision Red"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKSELECTED, RGB(0x60, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x26, 0x26, 0x26) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x00, 0x00) }, + { MODCOLOR_NOTE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_VOLUME, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_PANNING, RGB(0xDF, 0x00, 0x00) }, + { MODCOLOR_PITCH, RGB(0xDF, 0x00, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xE0, 0x00, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x9F, 0x00, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x60, 0x00, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xDC, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC8, 0x00, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0x42, 0x42) }, +} +}, +{ _T("jmkz - Oil Pastels"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_TEXTNORMAL, RGB(0x78, 0x78, 0x78) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x31, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x40) }, + { MODCOLOR_TEXTSELECTED, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x70, 0xDF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x40, 0x00, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_BACKHILIGHT, RGB(0x17, 0x17, 0x17) }, + { MODCOLOR_NOTE, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x80, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xBF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0x9F, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0x9F, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_SEPFACE, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_SEPHILITE, RGB(0x08, 0x08, 0x08) }, + { MODCOLOR_BLENDCOLOR, RGB(0x4F, 0x4F, 0x4F) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x40, 0x40) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xDF, 0xA8, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xEA, 0xEA, 0x15) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xBF, 0x00) }, +} +}, +{ _T("jmkz - Old Sunset"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x93, 0x93, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xDF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xA6, 0x00, 0x00) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKHILIGHT, RGB(0x58, 0x2C, 0x2C) }, + { MODCOLOR_NOTE, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x80, 0x80) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x97, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x9B, 0x9B) }, + { MODCOLOR_PITCH, RGB(0x9D, 0x96, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xBB, 0x5E, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x20, 0xC0, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x5B, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x97, 0x00, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x71, 0x00, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0x97, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0x9B, 0x9B) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("jmkz - Oxygen Glow"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x0C, 0x13, 0x14) }, + { MODCOLOR_TEXTNORMAL, RGB(0x40, 0xAF, 0xBF) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x94, 0x9F) }, + { MODCOLOR_TEXTCURROW, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_BACKSELECTED, RGB(0x1F, 0x5E, 0x61) }, + { MODCOLOR_TEXTSELECTED, RGB(0x58, 0xDC, 0xE7) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xB1, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x68, 0x68, 0x68) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x40, 0xB6, 0xBF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x17, 0x27, 0x28) }, + { MODCOLOR_NOTE, RGB(0xDF, 0xFD, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0xD2, 0x80) }, + { MODCOLOR_VOLUME, RGB(0xCC, 0x80, 0xFF) }, + { MODCOLOR_PANNING, RGB(0xFF, 0x80, 0x86) }, + { MODCOLOR_PITCH, RGB(0x80, 0xFF, 0x93) }, + { MODCOLOR_GLOBALS, RGB(0x88, 0xEC, 0xF7) }, + { MODCOLOR_VUMETER_LO, RGB(0x20, 0x5B, 0x60) }, + { MODCOLOR_VUMETER_MED, RGB(0x40, 0xB6, 0xBF) }, + { MODCOLOR_VUMETER_HI, RGB(0x80, 0xF5, 0xFF) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x7A, 0x00, 0xCC) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x99, 0x00, 0xFF) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xCC, 0x80, 0xFF) }, + { MODCOLOR_SEPSHADOW, RGB(0x0C, 0x13, 0x14) }, + { MODCOLOR_SEPFACE, RGB(0x1F, 0x5E, 0x61) }, + { MODCOLOR_SEPHILITE, RGB(0x0C, 0x13, 0x14) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x38, 0x40) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xD9, 0x80) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xA8, 0x51) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0x93) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x88, 0xEC, 0xF7) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xD2, 0x80) }, +} +}, +{ _T("jmkz - Oxygen"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xEB, 0xF3, 0xF4) }, + { MODCOLOR_TEXTNORMAL, RGB(0x20, 0x59, 0x60) }, + { MODCOLOR_BACKCURROW, RGB(0xB8, 0xE3, 0xE7) }, + { MODCOLOR_TEXTCURROW, RGB(0x20, 0x5B, 0x60) }, + { MODCOLOR_BACKSELECTED, RGB(0x9F, 0xD9, 0xDF) }, + { MODCOLOR_TEXTSELECTED, RGB(0x20, 0x5B, 0x60) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0xCE, 0xD5) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xBC, 0xC3, 0xC4) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x34, 0x49, 0x4B) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD8, 0xE6, 0xE8) }, + { MODCOLOR_NOTE, RGB(0x10, 0x2E, 0x30) }, + { MODCOLOR_INSTRUMENT, RGB(0x48, 0x36, 0x17) }, + { MODCOLOR_VOLUME, RGB(0x65, 0x30, 0x8F) }, + { MODCOLOR_PANNING, RGB(0x8F, 0x30, 0x32) }, + { MODCOLOR_PITCH, RGB(0x30, 0x8F, 0x3C) }, + { MODCOLOR_GLOBALS, RGB(0x49, 0x73, 0x76) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0x9B, 0xF9) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPFACE, RGB(0xC8, 0xD3, 0xD5) }, + { MODCOLOR_SEPHILITE, RGB(0xF4, 0xF4, 0xF4) }, + { MODCOLOR_BLENDCOLOR, RGB(0x9A, 0xB8, 0xBA) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x40, 0xB9, 0xBF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0x8F, 0x3C) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x49, 0x73, 0x76) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("jmkz - Pastels"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xF6, 0xF6, 0xF6) }, + { MODCOLOR_TEXTNORMAL, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_BACKCURROW, RGB(0xCE, 0xFF, 0xCE) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x60, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0xCE, 0xCE, 0xFF) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x60) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xFF, 0x80) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xFF, 0xCE, 0xCE) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x60, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xE8, 0xE8, 0xE8) }, + { MODCOLOR_NOTE, RGB(0x1F, 0x1F, 0x1F) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x40, 0x80) }, + { MODCOLOR_VOLUME, RGB(0x40, 0x80, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x80, 0x80) }, + { MODCOLOR_PITCH, RGB(0x80, 0x60, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0xBF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x80, 0xBF, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0xF6, 0xF6, 0xF6) }, + { MODCOLOR_SEPFACE, RGB(0xBF, 0xBF, 0xBF) }, + { MODCOLOR_SEPHILITE, RGB(0xF6, 0xF6, 0xF6) }, + { MODCOLOR_BLENDCOLOR, RGB(0xAF, 0xAF, 0xAF) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xDF, 0x80) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x1A, 0x80, 0xE6) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xCE, 0xFF, 0xCE) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xCE, 0xCE, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xDF, 0x80) }, +} +}, +{ _T("jmkz - Plumbum"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x0C, 0x0D, 0x14) }, + { MODCOLOR_TEXTNORMAL, RGB(0x61, 0x65, 0x76) }, + { MODCOLOR_BACKCURROW, RGB(0x70, 0x78, 0x8F) }, + { MODCOLOR_TEXTCURROW, RGB(0xD8, 0xDA, 0xE7) }, + { MODCOLOR_BACKSELECTED, RGB(0x46, 0x47, 0x59) }, + { MODCOLOR_TEXTSELECTED, RGB(0xD6, 0xD9, 0xE9) }, + { MODCOLOR_SAMPLE, RGB(0x8E, 0x98, 0xB3) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x46, 0x4B, 0x59) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xCE, 0xD0, 0xE1) }, + { MODCOLOR_BACKHILIGHT, RGB(0x1A, 0x1C, 0x26) }, + { MODCOLOR_NOTE, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_INSTRUMENT, RGB(0xB4, 0xBB, 0xCB) }, + { MODCOLOR_VOLUME, RGB(0xBF, 0xD7, 0xA8) }, + { MODCOLOR_PANNING, RGB(0xA8, 0xD7, 0xD7) }, + { MODCOLOR_PITCH, RGB(0xD7, 0xD7, 0xA8) }, + { MODCOLOR_GLOBALS, RGB(0xD7, 0xA8, 0xBF) }, + { MODCOLOR_VUMETER_LO, RGB(0x34, 0x9E, 0x34) }, + { MODCOLOR_VUMETER_MED, RGB(0xDB, 0xDB, 0x64) }, + { MODCOLOR_VUMETER_HI, RGB(0xEF, 0x8F, 0x8F) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x34, 0x80, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xDB, 0xDB, 0x64) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xEF, 0x8F, 0x8F) }, + { MODCOLOR_SEPSHADOW, RGB(0x10, 0x12, 0x1B) }, + { MODCOLOR_SEPFACE, RGB(0x54, 0x55, 0x6B) }, + { MODCOLOR_SEPHILITE, RGB(0x10, 0x12, 0x1B) }, + { MODCOLOR_BLENDCOLOR, RGB(0x30, 0x3C, 0x50) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xC7, 0xCB, 0xD8) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xD7, 0xD7, 0xA8) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xBF, 0xD7, 0xA8) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xA8, 0xD7, 0xD7) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xD7, 0xD7, 0xA8) }, +} +}, +{ _T("jmkz - Preserve (black)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_BACKCURROW, RGB(0x52, 0x52, 0x74) }, + { MODCOLOR_TEXTCURROW, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKSELECTED, RGB(0x3C, 0x3C, 0x77) }, + { MODCOLOR_TEXTSELECTED, RGB(0x80, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0x80, 0xC0) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x3D, 0x3D, 0x7A) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_BACKHILIGHT, RGB(0x32, 0x32, 0x47) }, + { MODCOLOR_NOTE, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0xCE, 0xCE) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xB7, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xC4, 0xC4) }, + { MODCOLOR_PITCH, RGB(0xC4, 0xC4, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xED, 0x0E, 0x62) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x64, 0x64, 0x64) }, + { MODCOLOR_SEPFACE, RGB(0x70, 0x70, 0xB8) }, + { MODCOLOR_SEPHILITE, RGB(0x4D, 0x4D, 0x99) }, + { MODCOLOR_BLENDCOLOR, RGB(0x2B, 0x2B, 0x55) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x53, 0xA6, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xC4, 0xC4, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xCE, 0xCE) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xC4, 0xC4, 0x00) }, +} +}, +{ _T("jmkz - Protrekkr"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x68, 0x8C, 0xAC) }, + { MODCOLOR_BACKCURROW, RGB(0x30, 0x24, 0x34) }, + { MODCOLOR_TEXTCURROW, RGB(0xEA, 0xF0, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0x68, 0x8C, 0xAC) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0x88, 0xD0, 0xFF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x30, 0x24, 0x34) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xDC, 0xF2, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x06, 0x1C, 0x28) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xD4, 0xB6) }, + { MODCOLOR_INSTRUMENT, RGB(0x68, 0x8C, 0xAC) }, + { MODCOLOR_VOLUME, RGB(0xA0, 0xBE, 0xE4) }, + { MODCOLOR_PANNING, RGB(0xA0, 0xBE, 0xE4) }, + { MODCOLOR_PITCH, RGB(0xA0, 0xBE, 0xE4) }, + { MODCOLOR_GLOBALS, RGB(0xA0, 0xBE, 0xE4) }, + { MODCOLOR_VUMETER_LO, RGB(0x10, 0xEA, 0xB0) }, + { MODCOLOR_VUMETER_MED, RGB(0x10, 0xEA, 0xB0) }, + { MODCOLOR_VUMETER_HI, RGB(0x10, 0xEA, 0xB0) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x28, 0x8F, 0xEC) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x28, 0x8F, 0xEC) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x28, 0x8F, 0xEC) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x88, 0xD0, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x88) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xA0, 0xBE, 0xE4) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x88, 0xD0, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x88) }, +} +}, +{ _T("jmkz - Pure Yellow"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKSELECTED, RGB(0x60, 0x60, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xBF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x26, 0x26, 0x26) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x40, 0x40, 0x00) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_INSTRUMENT, RGB(0xBF, 0xBF, 0x00) }, + { MODCOLOR_VOLUME, RGB(0xBF, 0xBF, 0x00) }, + { MODCOLOR_PANNING, RGB(0xDF, 0xDF, 0x00) }, + { MODCOLOR_PITCH, RGB(0xDF, 0xDF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x80, 0x80, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xBF, 0xBF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xC0, 0xC0, 0x00) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xE0, 0xE0, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPFACE, RGB(0x9F, 0x9F, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x60, 0x60, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xCA, 0xCA, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xDF, 0xDF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC8, 0xC8, 0x00) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("jmkz - Renoise Visual"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_TEXTNORMAL, RGB(0xAD, 0xAF, 0xB4) }, + { MODCOLOR_BACKCURROW, RGB(0x38, 0x38, 0x38) }, + { MODCOLOR_TEXTCURROW, RGB(0xC9, 0xC9, 0xC9) }, + { MODCOLOR_BACKSELECTED, RGB(0x18, 0x31, 0x3C) }, + { MODCOLOR_TEXTSELECTED, RGB(0x66, 0x80, 0x8B) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xBF, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xC9, 0xC9, 0xC9) }, + { MODCOLOR_BACKHILIGHT, RGB(0x1D, 0x20, 0x24) }, + { MODCOLOR_NOTE, RGB(0xD1, 0xD2, 0xD5) }, + { MODCOLOR_INSTRUMENT, RGB(0x43, 0xBD, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0xB8, 0xF6, 0xBF) }, + { MODCOLOR_PANNING, RGB(0xE2, 0xE0, 0x93) }, + { MODCOLOR_PITCH, RGB(0xF6, 0xB5, 0x84) }, + { MODCOLOR_GLOBALS, RGB(0xED, 0xA6, 0xDD) }, + { MODCOLOR_VUMETER_LO, RGB(0x07, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_MED, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x52, 0x67, 0xFE) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_SEPSHADOW, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_SEPFACE, RGB(0x70, 0x74, 0x7E) }, + { MODCOLOR_SEPHILITE, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_BLENDCOLOR, RGB(0x40, 0x40, 0x54) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x64, 0xAD, 0xB1) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xC0, 0xC0, 0x54) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xB8, 0xF6, 0xBF) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x43, 0xBD, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xE2, 0xE0, 0x93) }, +} +}, +{ _T("jmkz - Renoise"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_TEXTNORMAL, RGB(0xAD, 0xAF, 0xB4) }, + { MODCOLOR_BACKCURROW, RGB(0x38, 0x38, 0x38) }, + { MODCOLOR_TEXTCURROW, RGB(0xC9, 0xC9, 0xC9) }, + { MODCOLOR_BACKSELECTED, RGB(0x18, 0x31, 0x3C) }, + { MODCOLOR_TEXTSELECTED, RGB(0x66, 0x80, 0x8B) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xBF, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x2F, 0x2F, 0x2F) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xC9, 0xC9, 0xC9) }, + { MODCOLOR_BACKHILIGHT, RGB(0x1D, 0x20, 0x24) }, + { MODCOLOR_NOTE, RGB(0xD1, 0xD2, 0xD5) }, + { MODCOLOR_INSTRUMENT, RGB(0xD1, 0xD2, 0xD5) }, + { MODCOLOR_VOLUME, RGB(0xB8, 0xF6, 0xBF) }, + { MODCOLOR_PANNING, RGB(0xE2, 0xE0, 0x93) }, + { MODCOLOR_PITCH, RGB(0xF6, 0xB5, 0x84) }, + { MODCOLOR_GLOBALS, RGB(0xED, 0xA6, 0xDD) }, + { MODCOLOR_VUMETER_LO, RGB(0x07, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_MED, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x52, 0x67, 0xFE) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_SEPSHADOW, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_SEPFACE, RGB(0x38, 0x3A, 0x40) }, + { MODCOLOR_SEPHILITE, RGB(0x10, 0x10, 0x15) }, + { MODCOLOR_BLENDCOLOR, RGB(0x40, 0x40, 0x54) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x64, 0xAD, 0xB1) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xC0, 0xC0, 0x54) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xB8, 0xF6, 0xBF) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x43, 0xBD, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xE2, 0xE0, 0x93) }, +} +}, +{ _T("jmkz - Silver Luna"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xF1, 0xF0, 0xF2) }, + { MODCOLOR_TEXTNORMAL, RGB(0x58, 0x57, 0x68) }, + { MODCOLOR_BACKCURROW, RGB(0xD8, 0xD8, 0xDC) }, + { MODCOLOR_TEXTCURROW, RGB(0x58, 0x57, 0x68) }, + { MODCOLOR_BACKSELECTED, RGB(0xB2, 0xB4, 0xBF) }, + { MODCOLOR_TEXTSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLE, RGB(0xB5, 0xB4, 0xC0) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xC2, 0xC2, 0xCD) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xEA, 0xEA, 0xEE) }, + { MODCOLOR_BACKHILIGHT, RGB(0xE0, 0xDF, 0xE3) }, + { MODCOLOR_NOTE, RGB(0x2C, 0x2C, 0x34) }, + { MODCOLOR_INSTRUMENT, RGB(0x58, 0x57, 0x68) }, + { MODCOLOR_VOLUME, RGB(0x49, 0x88, 0x40) }, + { MODCOLOR_PANNING, RGB(0x40, 0x88, 0x7D) }, + { MODCOLOR_PITCH, RGB(0x88, 0x7B, 0x40) }, + { MODCOLOR_GLOBALS, RGB(0x88, 0x40, 0x4A) }, + { MODCOLOR_VUMETER_LO, RGB(0x68, 0xE6, 0x15) }, + { MODCOLOR_VUMETER_MED, RGB(0xFB, 0xDC, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFC, 0x1B, 0x42) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x64, 0x64, 0xFD) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFB, 0xDC, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFC, 0x1B, 0x42) }, + { MODCOLOR_SEPSHADOW, RGB(0x9D, 0x9D, 0xA1) }, + { MODCOLOR_SEPFACE, RGB(0xE0, 0xDF, 0xE3) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0x58, 0x57, 0x68) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xD7, 0x28, 0x47) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xDC, 0xDC, 0xE2) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xEA, 0xEA, 0x15) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x49, 0x88, 0x40) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x40, 0x88, 0x7D) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xEA, 0xEA, 0x15) }, +} +}, +{ _T("jmkz - Skale"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x04, 0x0E, 0x12) }, + { MODCOLOR_TEXTNORMAL, RGB(0x9B, 0xA4, 0xAE) }, + { MODCOLOR_BACKCURROW, RGB(0x52, 0x60, 0x6E) }, + { MODCOLOR_TEXTCURROW, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_BACKSELECTED, RGB(0x22, 0x68, 0x8B) }, + { MODCOLOR_TEXTSELECTED, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_SAMPLE, RGB(0xD6, 0xDB, 0xDE) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x36, 0x40, 0x49) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_BACKHILIGHT, RGB(0x2A, 0x35, 0x3F) }, + { MODCOLOR_NOTE, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_INSTRUMENT, RGB(0x9B, 0xA4, 0xAE) }, + { MODCOLOR_VOLUME, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_PANNING, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_PITCH, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_GLOBALS, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_VUMETER_LO, RGB(0x51, 0xCD, 0x32) }, + { MODCOLOR_VUMETER_MED, RGB(0xF4, 0xC0, 0x0B) }, + { MODCOLOR_VUMETER_HI, RGB(0xFD, 0x0F, 0x02) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFD, 0x0F, 0x02) }, + { MODCOLOR_SEPSHADOW, RGB(0x04, 0x0E, 0x12) }, + { MODCOLOR_SEPFACE, RGB(0x59, 0x6B, 0x6B) }, + { MODCOLOR_SEPHILITE, RGB(0x04, 0x0E, 0x12) }, + { MODCOLOR_BLENDCOLOR, RGB(0x14, 0x29, 0x2C) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xD6, 0xDB, 0xDE) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x9B, 0xA4, 0xAE) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x22, 0x68, 0x8B) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0x81, 0xC1, 0xE0) }, +} +}, +{ _T("jmkz - Technic"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xEF, 0xEF, 0xEA) }, + { MODCOLOR_TEXTNORMAL, RGB(0x52, 0x50, 0x4B) }, + { MODCOLOR_BACKCURROW, RGB(0xC7, 0xC7, 0xC7) }, + { MODCOLOR_TEXTCURROW, RGB(0x36, 0x36, 0x2D) }, + { MODCOLOR_BACKSELECTED, RGB(0xE7, 0xCE, 0xC3) }, + { MODCOLOR_TEXTSELECTED, RGB(0x99, 0x7F, 0x74) }, + { MODCOLOR_SAMPLE, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xD0, 0xD0, 0xD0) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x36, 0x36, 0x36) }, + { MODCOLOR_BACKHILIGHT, RGB(0xE2, 0xDF, 0xDB) }, + { MODCOLOR_NOTE, RGB(0x2E, 0x2D, 0x2A) }, + { MODCOLOR_INSTRUMENT, RGB(0x2E, 0x2D, 0x2A) }, + { MODCOLOR_VOLUME, RGB(0x6B, 0x0E, 0x60) }, + { MODCOLOR_PANNING, RGB(0x2C, 0x2F, 0xA0) }, + { MODCOLOR_PITCH, RGB(0x0E, 0x70, 0xB8) }, + { MODCOLOR_GLOBALS, RGB(0x1B, 0x85, 0x33) }, + { MODCOLOR_VUMETER_LO, RGB(0x07, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_MED, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x70, 0x92, 0xFE) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xDA, 0xF6, 0x01) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xF6, 0x01, 0x07) }, + { MODCOLOR_SEPSHADOW, RGB(0xEF, 0xEF, 0xEA) }, + { MODCOLOR_SEPFACE, RGB(0xC7, 0xC5, 0xBF) }, + { MODCOLOR_SEPHILITE, RGB(0xEF, 0xEF, 0xEA) }, + { MODCOLOR_BLENDCOLOR, RGB(0xBF, 0xBF, 0xAB) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x64, 0xAC, 0xB0) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x1B, 0x85, 0x33) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("jmkz - Tekton"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_TEXTNORMAL, RGB(0x3F, 0x3F, 0x3F) }, + { MODCOLOR_BACKCURROW, RGB(0xC1, 0xFF, 0xFF) }, + { MODCOLOR_TEXTCURROW, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_BACKSELECTED, RGB(0xFF, 0xFF, 0x9F) }, + { MODCOLOR_TEXTSELECTED, RGB(0x60, 0x60, 0x2A) }, + { MODCOLOR_SAMPLE, RGB(0xBF, 0xBF, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xFF, 0xFF, 0xC1) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_BACKHILIGHT, RGB(0xE4, 0xE2, 0xDC) }, + { MODCOLOR_NOTE, RGB(0x1F, 0x1F, 0x1F) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x18, 0x60) }, + { MODCOLOR_VOLUME, RGB(0x00, 0x60, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x60, 0x60) }, + { MODCOLOR_PITCH, RGB(0x60, 0x60, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0x60, 0x00, 0x60) }, + { MODCOLOR_VUMETER_LO, RGB(0xAA, 0xFF, 0xAA) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0xAE, 0xAE) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xAA, 0xAA, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0xAE, 0xAE) }, + { MODCOLOR_SEPSHADOW, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_SEPFACE, RGB(0x9F, 0x9F, 0x9F) }, + { MODCOLOR_SEPHILITE, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_BLENDCOLOR, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xBF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xE0, 0xE0, 0xE0) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x2E, 0xBC, 0xBC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x9F) }, +} +}, +{ _T("jmkz - Tundra"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xEC, 0xEE, 0xF4) }, + { MODCOLOR_TEXTNORMAL, RGB(0x2C, 0x36, 0x54) }, + { MODCOLOR_BACKCURROW, RGB(0xCF, 0xD7, 0xF1) }, + { MODCOLOR_TEXTCURROW, RGB(0x88, 0x94, 0xB8) }, + { MODCOLOR_BACKSELECTED, RGB(0x70, 0x70, 0x90) }, + { MODCOLOR_TEXTSELECTED, RGB(0xEC, 0xEE, 0xF4) }, + { MODCOLOR_SAMPLE, RGB(0xCA, 0x8A, 0x8D) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xBC, 0xBE, 0xC4) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xEE, 0xEF, 0xF2) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD8, 0xDC, 0xE8) }, + { MODCOLOR_NOTE, RGB(0x1F, 0x30, 0x61) }, + { MODCOLOR_INSTRUMENT, RGB(0x7D, 0x67, 0x42) }, + { MODCOLOR_VOLUME, RGB(0x65, 0x47, 0x78) }, + { MODCOLOR_PANNING, RGB(0x84, 0x3C, 0x3E) }, + { MODCOLOR_PITCH, RGB(0x3C, 0x84, 0x45) }, + { MODCOLOR_GLOBALS, RGB(0x48, 0x57, 0x77) }, + { MODCOLOR_VUMETER_LO, RGB(0x83, 0xD1, 0x83) }, + { MODCOLOR_VUMETER_MED, RGB(0x83, 0xD1, 0x83) }, + { MODCOLOR_VUMETER_HI, RGB(0xD1, 0x83, 0x83) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x97, 0x9F, 0xD9) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x97, 0x9F, 0xD9) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xD1, 0x83, 0x83) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPFACE, RGB(0xC8, 0xCB, 0xD5) }, + { MODCOLOR_SEPHILITE, RGB(0xEC, 0xEC, 0xEC) }, + { MODCOLOR_BLENDCOLOR, RGB(0x9A, 0xA2, 0xBA) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x96, 0xA0, 0xBF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0xCF, 0xD7, 0xF1) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x88, 0x94, 0xB8) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xE0, 0xBE, 0xC0) }, +} +}, +{ _T("jmkz - Xenon Glow"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x0C, 0x10, 0x14) }, + { MODCOLOR_TEXTNORMAL, RGB(0x41, 0x7C, 0xBF) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x43, 0x9F) }, + { MODCOLOR_TEXTCURROW, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_BACKSELECTED, RGB(0x20, 0x39, 0x61) }, + { MODCOLOR_TEXTSELECTED, RGB(0x59, 0x95, 0xE7) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0x50, 0xBF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x39, 0x39, 0x39) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x41, 0x76, 0xBF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x18, 0x1E, 0x28) }, + { MODCOLOR_NOTE, RGB(0xDF, 0xEC, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0xFF, 0x85, 0x80) }, + { MODCOLOR_VOLUME, RGB(0x80, 0xE1, 0x8B) }, + { MODCOLOR_PANNING, RGB(0xFF, 0xD9, 0x80) }, + { MODCOLOR_PITCH, RGB(0xC4, 0x80, 0xFF) }, + { MODCOLOR_GLOBALS, RGB(0x89, 0xB9, 0xF7) }, + { MODCOLOR_VUMETER_LO, RGB(0x20, 0x3B, 0x60) }, + { MODCOLOR_VUMETER_MED, RGB(0x41, 0x76, 0xBF) }, + { MODCOLOR_VUMETER_HI, RGB(0x80, 0xB5, 0xFF) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x3F, 0x75, 0xBE) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x78, 0x9E, 0xD1) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xB9, 0xD7, 0xFF) }, + { MODCOLOR_SEPSHADOW, RGB(0x0C, 0x10, 0x14) }, + { MODCOLOR_SEPFACE, RGB(0x27, 0x46, 0x78) }, + { MODCOLOR_SEPHILITE, RGB(0x0C, 0x10, 0x14) }, + { MODCOLOR_BLENDCOLOR, RGB(0x20, 0x39, 0x61) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x80, 0x85) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x59, 0x95, 0xE7) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xDF, 0xEC, 0xFF) }, +} +}, +{ _T("LegovitchColors"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xF2, 0xF0, 0xEC) }, + { MODCOLOR_TEXTNORMAL, RGB(0x3A, 0x34, 0x27) }, + { MODCOLOR_BACKCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x91, 0x00, 0x48) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xC6, 0x1A) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xD0, 0xDD, 0xDF) }, + { MODCOLOR_NOTE, RGB(0x00, 0x2E, 0x5B) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0x80, 0x40) }, + { MODCOLOR_VOLUME, RGB(0x1E, 0x30, 0x5B) }, + { MODCOLOR_PANNING, RGB(0x57, 0x85, 0x2E) }, + { MODCOLOR_PITCH, RGB(0xFF, 0x00, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0xFF) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SEPFACE, RGB(0x8F, 0x8F, 0x8F) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0x36, 0x57, 0x72) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x81, 0xA8, 0xD3) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xC6, 0x1A) }, +} +}, +{ _T("melcom - Aquamarine & Blue - Alternative)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x14, 0x2D, 0x32) }, + { MODCOLOR_TEXTNORMAL, RGB(0x74, 0xA4, 0xB4) }, + { MODCOLOR_BACKCURROW, RGB(0x38, 0x58, 0x78) }, + { MODCOLOR_TEXTCURROW, RGB(0xBC, 0xDC, 0xF4) }, + { MODCOLOR_BACKSELECTED, RGB(0xB4, 0xDC, 0xF4) }, + { MODCOLOR_TEXTSELECTED, RGB(0x28, 0x58, 0x64) }, + { MODCOLOR_SAMPLE, RGB(0xA4, 0xCF, 0xF0) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x41, 0x66, 0x8B) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xB4, 0xDC, 0xF4) }, + { MODCOLOR_BACKHILIGHT, RGB(0x38, 0x58, 0x78) }, + { MODCOLOR_NOTE, RGB(0xA4, 0xCF, 0xF0) }, + { MODCOLOR_INSTRUMENT, RGB(0x62, 0xAC, 0xBF) }, + { MODCOLOR_VOLUME, RGB(0xA2, 0xC1, 0xAA) }, + { MODCOLOR_PANNING, RGB(0x8A, 0xFF, 0xA8) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0x8A, 0xFF, 0xA8) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xA4, 0xCF, 0xF0) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x80, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0x37, 0x79, 0x8A) }, + { MODCOLOR_SEPFACE, RGB(0x28, 0x58, 0x64) }, + { MODCOLOR_SEPHILITE, RGB(0x15, 0x2E, 0x35) }, + { MODCOLOR_BLENDCOLOR, RGB(0x28, 0x58, 0x64) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_BACKSAMPLE, RGB(0x38, 0x58, 0x78) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x14, 0x2D, 0x32) }, + { MODCOLOR_BACKENV, RGB(0x50, 0x7D, 0xAA) }, + { MODCOLOR_ENVELOPES, RGB(0xA4, 0xCF, 0xF0) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x14, 0x2D, 0x32) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("melcom - Brown, Olive & Green"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x2D, 0x25, 0x1E) }, + { MODCOLOR_TEXTNORMAL, RGB(0x98, 0xAA, 0x7D) }, + { MODCOLOR_BACKCURROW, RGB(0x10, 0x35, 0x00) }, + { MODCOLOR_TEXTCURROW, RGB(0xEB, 0xEB, 0xCB) }, + { MODCOLOR_BACKSELECTED, RGB(0x20, 0x55, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xEB, 0xEB, 0xCB) }, + { MODCOLOR_SAMPLE, RGB(0x98, 0xAA, 0x7D) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x59, 0x41, 0x3C) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_BACKHILIGHT, RGB(0x43, 0x4A, 0x3E) }, + { MODCOLOR_NOTE, RGB(0x45, 0x9A, 0x49) }, + { MODCOLOR_INSTRUMENT, RGB(0x38, 0x9E, 0x75) }, + { MODCOLOR_VOLUME, RGB(0xA2, 0xA2, 0xA2) }, + { MODCOLOR_PANNING, RGB(0x8A, 0xFF, 0xA8) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO, RGB(0x8A, 0xFF, 0xA8) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x80, 0x80) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xA4, 0xCF, 0xF0) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x64) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x80, 0x80) }, + { MODCOLOR_SEPSHADOW, RGB(0x98, 0xAA, 0x7D) }, + { MODCOLOR_SEPFACE, RGB(0x43, 0x4A, 0x3E) }, + { MODCOLOR_SEPHILITE, RGB(0x2D, 0x25, 0x1E) }, + { MODCOLOR_BLENDCOLOR, RGB(0x35, 0x27, 0x24) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x43, 0x4A, 0x3E) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x2D, 0x25, 0x1E) }, + { MODCOLOR_BACKENV, RGB(0x2D, 0x25, 0x1E) }, + { MODCOLOR_ENVELOPES, RGB(0x98, 0xAA, 0x7D) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x43, 0x4A, 0x3E) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x30, 0xCC, 0x30) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x32, 0xCC, 0xCC) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xCC, 0x30) }, +} +}, +{ _T("MiDoRi - Coral Reef"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x16, 0x1D, 0x25) }, + { MODCOLOR_TEXTNORMAL, RGB(0x33, 0x71, 0xA4) }, + { MODCOLOR_BACKCURROW, RGB(0x1A, 0x5F, 0x97) }, + { MODCOLOR_TEXTCURROW, RGB(0x31, 0x2D, 0x20) }, + { MODCOLOR_BACKSELECTED, RGB(0xF1, 0xC9, 0x8D) }, + { MODCOLOR_TEXTSELECTED, RGB(0x18, 0x2D, 0x43) }, + { MODCOLOR_SAMPLE, RGB(0x1A, 0x6A, 0xA6) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x51, 0x39, 0x59) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xB4, 0x84, 0xBD) }, + { MODCOLOR_BACKHILIGHT, RGB(0x41, 0x52, 0x67) }, + { MODCOLOR_NOTE, RGB(0x26, 0xD9, 0x92) }, + { MODCOLOR_INSTRUMENT, RGB(0x9C, 0xE2, 0x8B) }, + { MODCOLOR_VOLUME, RGB(0xEC, 0x93, 0x93) }, + { MODCOLOR_PANNING, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_PITCH, RGB(0xC0, 0x98, 0xCB) }, + { MODCOLOR_GLOBALS, RGB(0xD8, 0xCD, 0xA0) }, + { MODCOLOR_VUMETER_LO, RGB(0x3A, 0xC0, 0x66) }, + { MODCOLOR_VUMETER_MED, RGB(0xD9, 0xB4, 0x6F) }, + { MODCOLOR_VUMETER_HI, RGB(0xDF, 0x78, 0x75) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x29, 0x7D, 0xAF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x56, 0xC5, 0xDC) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xDF, 0x78, 0x75) }, + { MODCOLOR_SEPSHADOW, RGB(0x38, 0x3D, 0x5A) }, + { MODCOLOR_SEPFACE, RGB(0x18, 0x1E, 0x2E) }, + { MODCOLOR_SEPHILITE, RGB(0x32, 0x36, 0x52) }, + { MODCOLOR_BLENDCOLOR, RGB(0x24, 0x24, 0x2D) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x1A, 0x1C, 0x28) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_BACKENV, RGB(0x1A, 0x1C, 0x28) }, + { MODCOLOR_ENVELOPES, RGB(0x44, 0xB9, 0x8A) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x9C, 0xE2, 0x8B) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC0, 0x98, 0xCB) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xF1, 0xC9, 0x8D) }, +} +}, +{ _T("MiDoRi - DarkPlug"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x0F, 0x0F, 0x0F) }, + { MODCOLOR_TEXTNORMAL, RGB(0x50, 0x50, 0x50) }, + { MODCOLOR_BACKCURROW, RGB(0x33, 0x0D, 0x30) }, + { MODCOLOR_TEXTCURROW, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x80, 0x80) }, + { MODCOLOR_TEXTSELECTED, RGB(0xE6, 0xE6, 0xE6) }, + { MODCOLOR_SAMPLE, RGB(0x9F, 0xC7, 0x29) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x7C, 0x1A, 0x07) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x35, 0x35, 0x35) }, + { MODCOLOR_NOTE, RGB(0xF0, 0x55, 0x44) }, + { MODCOLOR_INSTRUMENT, RGB(0xA6, 0xA6, 0xA6) }, + { MODCOLOR_VOLUME, RGB(0xDE, 0xC4, 0x9E) }, + { MODCOLOR_PANNING, RGB(0x6A, 0xB9, 0xA4) }, + { MODCOLOR_PITCH, RGB(0x4B, 0x79, 0x94) }, + { MODCOLOR_GLOBALS, RGB(0x85, 0x97, 0x5B) }, + { MODCOLOR_VUMETER_LO, RGB(0xA8, 0xDD, 0x6C) }, + { MODCOLOR_VUMETER_MED, RGB(0xF8, 0xEA, 0x8B) }, + { MODCOLOR_VUMETER_HI, RGB(0xF4, 0x8C, 0x6F) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x30, 0xAE, 0xC9) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xF8, 0xEA, 0x8B) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xF4, 0x8C, 0x6F) }, + { MODCOLOR_SEPSHADOW, RGB(0x32, 0x22, 0x21) }, + { MODCOLOR_SEPFACE, RGB(0x18, 0x14, 0x14) }, + { MODCOLOR_SEPHILITE, RGB(0x35, 0x26, 0x24) }, + { MODCOLOR_BLENDCOLOR, RGB(0x20, 0x05, 0x00) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x12, 0x12, 0x12) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x2B, 0x35, 0x0B) }, + { MODCOLOR_BACKENV, RGB(0x12, 0x12, 0x12) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x6A, 0xB9, 0xA4) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x4B, 0x79, 0x94) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xAE, 0xD6, 0x38) }, +} +}, +{ _T("MiDoRi - DX7"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x1F, 0x1B, 0x18) }, + { MODCOLOR_TEXTNORMAL, RGB(0x54, 0x6D, 0x70) }, + { MODCOLOR_BACKCURROW, RGB(0x36, 0x69, 0x62) }, + { MODCOLOR_TEXTCURROW, RGB(0xDB, 0xD7, 0xC8) }, + { MODCOLOR_BACKSELECTED, RGB(0xDC, 0xC5, 0xA3) }, + { MODCOLOR_TEXTSELECTED, RGB(0x33, 0x33, 0x33) }, + { MODCOLOR_SAMPLE, RGB(0x3C, 0xB7, 0xA8) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x60, 0x51, 0x33) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xEC, 0xC8, 0x75) }, + { MODCOLOR_BACKHILIGHT, RGB(0x38, 0x31, 0x2C) }, + { MODCOLOR_NOTE, RGB(0x6C, 0xDB, 0xCA) }, + { MODCOLOR_INSTRUMENT, RGB(0xDF, 0xD6, 0xB7) }, + { MODCOLOR_VOLUME, RGB(0xF7, 0xF5, 0xF0) }, + { MODCOLOR_PANNING, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_PITCH, RGB(0x2D, 0xAC, 0x79) }, + { MODCOLOR_GLOBALS, RGB(0x53, 0xA4, 0xB5) }, + { MODCOLOR_VUMETER_LO, RGB(0x87, 0x5E, 0x9D) }, + { MODCOLOR_VUMETER_MED, RGB(0xEF, 0x56, 0x56) }, + { MODCOLOR_VUMETER_HI, RGB(0xFA, 0xB6, 0xB4) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xBB, 0xA5, 0xC0) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xED, 0xAD, 0xAD) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0xDD, 0xDD) }, + { MODCOLOR_SEPSHADOW, RGB(0x1B, 0x28, 0x2C) }, + { MODCOLOR_SEPFACE, RGB(0x0D, 0x13, 0x13) }, + { MODCOLOR_SEPHILITE, RGB(0x1F, 0x29, 0x26) }, + { MODCOLOR_BLENDCOLOR, RGB(0x24, 0x24, 0x2D) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xE0, 0x1F, 0x1F) }, + { MODCOLOR_BACKSAMPLE, RGB(0x24, 0x1F, 0x1C) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xCC, 0xBE, 0x8C) }, + { MODCOLOR_BACKENV, RGB(0x24, 0x1F, 0x1C) }, + { MODCOLOR_ENVELOPES, RGB(0x3C, 0xB7, 0xA8) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xB0, 0xE6, 0xE3) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x2D, 0xAC, 0x79) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x6C, 0xDB, 0xCA) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xDC, 0xC5, 0xA3) }, +} +}, +{ _T("MiDoRi - Midorian Mode"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x17, 0x17, 0x17) }, + { MODCOLOR_TEXTNORMAL, RGB(0x3E, 0x3E, 0x3E) }, + { MODCOLOR_BACKCURROW, RGB(0x75, 0x75, 0x75) }, + { MODCOLOR_TEXTCURROW, RGB(0x31, 0x2D, 0x20) }, + { MODCOLOR_BACKSELECTED, RGB(0xCF, 0xF4, 0x8A) }, + { MODCOLOR_TEXTSELECTED, RGB(0x32, 0x27, 0x3F) }, + { MODCOLOR_SAMPLE, RGB(0xCC, 0xE3, 0x6C) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x67, 0x60, 0x54) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xEF, 0xEF, 0xEF) }, + { MODCOLOR_BACKHILIGHT, RGB(0x48, 0x48, 0x48) }, + { MODCOLOR_NOTE, RGB(0xD3, 0xF4, 0xAE) }, + { MODCOLOR_INSTRUMENT, RGB(0xA3, 0xF0, 0x7D) }, + { MODCOLOR_VOLUME, RGB(0x28, 0xC8, 0x31) }, + { MODCOLOR_PANNING, RGB(0xD9, 0xC8, 0xA4) }, + { MODCOLOR_PITCH, RGB(0xD5, 0xD7, 0xCC) }, + { MODCOLOR_GLOBALS, RGB(0x96, 0x93, 0x72) }, + { MODCOLOR_VUMETER_LO, RGB(0x2E, 0x47, 0x29) }, + { MODCOLOR_VUMETER_MED, RGB(0x73, 0xB8, 0x52) }, + { MODCOLOR_VUMETER_HI, RGB(0xDA, 0xF9, 0xCC) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x6D, 0x77, 0x4D) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x9A, 0xAE, 0x6C) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE8, 0xF0, 0xDD) }, + { MODCOLOR_SEPSHADOW, RGB(0x29, 0x29, 0x29) }, + { MODCOLOR_SEPFACE, RGB(0x17, 0x17, 0x17) }, + { MODCOLOR_SEPHILITE, RGB(0x2E, 0x2E, 0x2E) }, + { MODCOLOR_BLENDCOLOR, RGB(0x24, 0x24, 0x2D) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xDA, 0x25, 0x25) }, + { MODCOLOR_BACKSAMPLE, RGB(0x2C, 0x2C, 0x2C) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x74, 0x74, 0x74) }, + { MODCOLOR_BACKENV, RGB(0x2C, 0x2C, 0x2C) }, + { MODCOLOR_ENVELOPES, RGB(0xD1, 0xEB, 0x96) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x94, 0xCB, 0x2E) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x28, 0xC8, 0x31) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xA3, 0xF0, 0x7D) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xD9, 0xC8, 0xA4) }, +} +}, +{ _T("MiDoRi - Strawberry"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x17, 0x1A, 0x1C) }, + { MODCOLOR_TEXTNORMAL, RGB(0x55, 0x60, 0x6C) }, + { MODCOLOR_BACKCURROW, RGB(0x43, 0x4D, 0x7C) }, + { MODCOLOR_TEXTCURROW, RGB(0x31, 0x2D, 0x20) }, + { MODCOLOR_BACKSELECTED, RGB(0xF1, 0xC9, 0x8D) }, + { MODCOLOR_TEXTSELECTED, RGB(0x18, 0x2D, 0x43) }, + { MODCOLOR_SAMPLE, RGB(0xF4, 0xAB, 0x93) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x51, 0x39, 0x59) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xB4, 0x84, 0xBD) }, + { MODCOLOR_BACKHILIGHT, RGB(0x36, 0x45, 0x56) }, + { MODCOLOR_NOTE, RGB(0xEC, 0x7E, 0x6F) }, + { MODCOLOR_INSTRUMENT, RGB(0xF1, 0xB8, 0xB8) }, + { MODCOLOR_VOLUME, RGB(0xB1, 0xCD, 0xC7) }, + { MODCOLOR_PANNING, RGB(0xF3, 0xFA, 0xDC) }, + { MODCOLOR_PITCH, RGB(0xC0, 0x98, 0xCB) }, + { MODCOLOR_GLOBALS, RGB(0x88, 0xD2, 0xB7) }, + { MODCOLOR_VUMETER_LO, RGB(0x3A, 0xC0, 0x66) }, + { MODCOLOR_VUMETER_MED, RGB(0xD9, 0xB4, 0x6F) }, + { MODCOLOR_VUMETER_HI, RGB(0xDF, 0x78, 0x75) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x29, 0x7D, 0xAF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x56, 0xC5, 0xDC) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xDF, 0x78, 0x75) }, + { MODCOLOR_SEPSHADOW, RGB(0x3F, 0x40, 0x45) }, + { MODCOLOR_SEPFACE, RGB(0x18, 0x1E, 0x2E) }, + { MODCOLOR_SEPHILITE, RGB(0x3F, 0x40, 0x45) }, + { MODCOLOR_BLENDCOLOR, RGB(0x48, 0x48, 0x5B) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFC, 0x03, 0xD7) }, + { MODCOLOR_BACKSAMPLE, RGB(0x36, 0x3A, 0x3F) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xB6, 0x29, 0x29) }, + { MODCOLOR_BACKENV, RGB(0x1A, 0x1C, 0x28) }, + { MODCOLOR_ENVELOPES, RGB(0x44, 0xB9, 0x8A) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x88, 0xD2, 0xB7) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC0, 0x98, 0xCB) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xF3, 0xFA, 0xDC) }, +} +}, +{ _T("MiDoRi - Wineplume"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x1F, 0x16, 0x21) }, + { MODCOLOR_TEXTNORMAL, RGB(0x59, 0x44, 0x60) }, + { MODCOLOR_BACKCURROW, RGB(0x7A, 0x27, 0x18) }, + { MODCOLOR_TEXTCURROW, RGB(0x31, 0x2D, 0x20) }, + { MODCOLOR_BACKSELECTED, RGB(0xF3, 0x8B, 0x8B) }, + { MODCOLOR_TEXTSELECTED, RGB(0x32, 0x27, 0x3F) }, + { MODCOLOR_SAMPLE, RGB(0xED, 0x81, 0x94) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x8C, 0x38, 0x2F) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0xDC, 0x85, 0x85) }, + { MODCOLOR_BACKHILIGHT, RGB(0x39, 0x24, 0x3E) }, + { MODCOLOR_NOTE, RGB(0xFC, 0xF4, 0xE4) }, + { MODCOLOR_INSTRUMENT, RGB(0xED, 0x81, 0x94) }, + { MODCOLOR_VOLUME, RGB(0xC8, 0xA0, 0xCB) }, + { MODCOLOR_PANNING, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_PITCH, RGB(0xE1, 0x8C, 0xB5) }, + { MODCOLOR_GLOBALS, RGB(0x53, 0xA4, 0xB5) }, + { MODCOLOR_VUMETER_LO, RGB(0x87, 0x5E, 0x9D) }, + { MODCOLOR_VUMETER_MED, RGB(0xEF, 0x56, 0x56) }, + { MODCOLOR_VUMETER_HI, RGB(0xFA, 0xB6, 0xB4) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0xBB, 0xA5, 0xC0) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xED, 0xAD, 0xAD) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0xDD, 0xDD) }, + { MODCOLOR_SEPSHADOW, RGB(0x3D, 0x21, 0x37) }, + { MODCOLOR_SEPFACE, RGB(0x37, 0x1E, 0x37) }, + { MODCOLOR_SEPHILITE, RGB(0x39, 0x22, 0x4A) }, + { MODCOLOR_BLENDCOLOR, RGB(0x24, 0x24, 0x2D) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_BACKSAMPLE, RGB(0x34, 0x20, 0x39) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_BACKENV, RGB(0x34, 0x20, 0x39) }, + { MODCOLOR_ENVELOPES, RGB(0xF9, 0xB7, 0xB7) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x7E, 0xD6, 0xD1) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x53, 0xA4, 0xB5) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0xC8, 0xA0, 0xCB) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFC, 0xF4, 0xE4) }, +} +}, +{ _T("MilkyTracker"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKCURROW, RGB(0x60, 0x20, 0x40) }, + { MODCOLOR_TEXTCURROW, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKSELECTED, RGB(0x10, 0x30, 0x60) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x17, 0x17, 0x17) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x80, 0xE0, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0xFF, 0xE0, 0x80) }, + { MODCOLOR_PANNING, RGB(0x80, 0xFF, 0x80) }, + { MODCOLOR_PITCH, RGB(0xFF, 0x80, 0xE0) }, + { MODCOLOR_GLOBALS, RGB(0x80, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x52, 0x7D, 0xAD) }, + { MODCOLOR_SEPFACE, RGB(0x42, 0x61, 0x84) }, + { MODCOLOR_SEPHILITE, RGB(0x21, 0x30, 0x42) }, + { MODCOLOR_BLENDCOLOR, RGB(0x21, 0x30, 0x42) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0x78, 0x78, 0xF0) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0x80, 0xE0, 0xFF) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x40, 0x60, 0x80) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x55, 0x80, 0xAA) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x80) }, +} +}, +{ _T("mp64 - Chromatic v2"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_TEXTNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKCURROW, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_NOTE, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xA0, 0xA0, 0xA0) }, + { MODCOLOR_SEPFACE, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("mp64 - Chromatic"), +{ + { MODCOLOR_BACKNORMAL, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKCURROW, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_TEXTCURROW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKSELECTED, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0xE0, 0xE8, 0xE0) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_NOTE, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0x80, 0x00) }, + { MODCOLOR_VOLUME, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_PANNING, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_GLOBALS, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_VUMETER_MED, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_VUMETER_HI, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_SEPSHADOW, RGB(0xA0, 0xA0, 0xA0) }, + { MODCOLOR_SEPFACE, RGB(0xF0, 0xF0, 0xF0) }, + { MODCOLOR_SEPHILITE, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BLENDCOLOR, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xFF, 0x80, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xC0, 0xC0, 0xC0) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x00) }, +} +}, +{ _T("Oerg866 - AquaMPT"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x40) }, + { MODCOLOR_TEXTNORMAL, RGB(0x01, 0x54, 0xA7) }, + { MODCOLOR_BACKCURROW, RGB(0x00, 0x40, 0x80) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x2D, 0x45, 0x99) }, + { MODCOLOR_TEXTSELECTED, RGB(0xC6, 0xE1, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x00, 0x4A, 0x95) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x02, 0x21, 0x5B) }, + { MODCOLOR_NOTE, RGB(0x97, 0xBB, 0xFF) }, + { MODCOLOR_INSTRUMENT, RGB(0x6F, 0x88, 0xFF) }, + { MODCOLOR_VOLUME, RGB(0x46, 0x46, 0xFF) }, + { MODCOLOR_PANNING, RGB(0x00, 0x16, 0x2D) }, + { MODCOLOR_PITCH, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_GLOBALS, RGB(0x00, 0x00, 0x66) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0x00, 0x59) }, + { MODCOLOR_VUMETER_MED, RGB(0x00, 0x58, 0xB0) }, + { MODCOLOR_VUMETER_HI, RGB(0x00, 0xA3, 0xF0) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x40, 0x40, 0x80) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0x60, 0x60, 0xC0) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0x80, 0x80, 0xFF) }, + { MODCOLOR_SEPSHADOW, RGB(0x01, 0x38, 0x70) }, + { MODCOLOR_SEPFACE, RGB(0x01, 0x3C, 0x76) }, + { MODCOLOR_SEPHILITE, RGB(0x01, 0x60, 0xBE) }, + { MODCOLOR_BLENDCOLOR, RGB(0x01, 0x54, 0xA7) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x22, 0xA2, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x6F, 0x88, 0xFF) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x97, 0xBB, 0xFF) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xC6, 0xE1, 0xFF) }, +} +}, +{ _T("Scream Tracker 3 (Gold)"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTNORMAL, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_BACKCURROW, RGB(0x1A, 0x2D, 0x71) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x14, 0x24, 0x59) }, + { MODCOLOR_TEXTSELECTED, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_SAMPLE, RGB(0x00, 0xB0, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x0A, 0x11, 0x2C) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_BACKHILIGHT, RGB(0x30, 0x2C, 0x28) }, + { MODCOLOR_NOTE, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_INSTRUMENT, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_VOLUME, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_PANNING, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_PITCH, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_GLOBALS, RGB(0x9A, 0x96, 0x92) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0xA6, 0x92, 0x55) }, + { MODCOLOR_SEPFACE, RGB(0xA6, 0x92, 0x55) }, + { MODCOLOR_SEPHILITE, RGB(0xA6, 0x92, 0x55) }, + { MODCOLOR_BLENDCOLOR, RGB(0x69, 0x5C, 0x36) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0xA2, 0xA2, 0xA2) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x00, 0xB0, 0x00) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x22, 0x46, 0xA4) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xDF, 0x86) }, +} +}, +{ _T("tomaes"), +{ + { MODCOLOR_BACKNORMAL, RGB(0x40, 0x40, 0x40) }, + { MODCOLOR_TEXTNORMAL, RGB(0x80, 0x86, 0x93) }, + { MODCOLOR_BACKCURROW, RGB(0x60, 0x60, 0x60) }, + { MODCOLOR_TEXTCURROW, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKSELECTED, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_TEXTSELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_SAMPLE, RGB(0xFF, 0x00, 0x00) }, + { MODCOLOR_BACKPLAYCURSOR, RGB(0x50, 0x50, 0x50) }, + { MODCOLOR_TEXTPLAYCURSOR, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BACKHILIGHT, RGB(0x6D, 0x6D, 0x6D) }, + { MODCOLOR_NOTE, RGB(0xFF, 0xA2, 0x44) }, + { MODCOLOR_INSTRUMENT, RGB(0x00, 0xDD, 0xDD) }, + { MODCOLOR_VOLUME, RGB(0x98, 0xC8, 0x09) }, + { MODCOLOR_PANNING, RGB(0xFF, 0x80, 0x40) }, + { MODCOLOR_PITCH, RGB(0xFF, 0xFF, 0x80) }, + { MODCOLOR_GLOBALS, RGB(0xEA, 0x00, 0x4D) }, + { MODCOLOR_VUMETER_LO, RGB(0x00, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_MED, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_VUMETER_LO_VST, RGB(0x18, 0x96, 0xE1) }, + { MODCOLOR_VUMETER_MED_VST, RGB(0xFF, 0xC8, 0x00) }, + { MODCOLOR_VUMETER_HI_VST, RGB(0xE1, 0x00, 0x00) }, + { MODCOLOR_SEPSHADOW, RGB(0x80, 0x80, 0x80) }, + { MODCOLOR_SEPFACE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SEPHILITE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_BLENDCOLOR, RGB(0x8B, 0x91, 0x9C) }, + { MODCOLOR_DODGY_COMMANDS, RGB(0xC0, 0x00, 0x00) }, + { MODCOLOR_BACKSAMPLE, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_SAMPLESELECTED, RGB(0xFF, 0xFF, 0xFF) }, + { MODCOLOR_BACKENV, RGB(0x00, 0x00, 0x00) }, + { MODCOLOR_ENVELOPES, RGB(0x00, 0x00, 0xFF) }, + { MODCOLOR_ENVELOPE_RELEASE, RGB(0xFF, 0xFF, 0x00) }, + { MODCOLOR_SAMPLE_LOOPMARKER, RGB(0x98, 0xC8, 0x09) }, + { MODCOLOR_SAMPLE_SUSTAINMARKER, RGB(0x00, 0xDD, 0xDD) }, + { MODCOLOR_SAMPLE_CUEPOINT, RGB(0xFF, 0xFF, 0x80) }, +} +}, +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.cpp new file mode 100644 index 00000000..d0652924 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.cpp @@ -0,0 +1,68 @@ +/* + * ColourEdit.cpp + * -------------- + * Purpose: Implementation of a coloured edit UI item. + * 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 "ColourEdit.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////////// +// CColourEdit + +CColourEdit::CColourEdit() +{ + m_crText = RGB(0, 0, 0); //default text color +} + +CColourEdit::~CColourEdit() +{ + if(m_brBackGnd.GetSafeHandle()) //delete brush + m_brBackGnd.DeleteObject(); +} + + +BEGIN_MESSAGE_MAP(CColourEdit, CEdit) + ON_WM_CTLCOLOR_REFLECT() +END_MESSAGE_MAP() + +///////////////////////////////////////////////////////////////////////////// +// CColourEdit message handlers + +HBRUSH CColourEdit::CtlColor(CDC *pDC, UINT nCtlColor) +{ + MPT_UNREFERENCED_PARAMETER(nCtlColor); + pDC->SetTextColor(m_crText); //set text color + pDC->SetBkColor(m_crBackGnd); //set the text's background color + return m_brBackGnd; //return the brush used for background - this sets control background +} + +///////////////////////////////////////////////////////////////////////////// +// Implementation + +void CColourEdit::SetBackColor(COLORREF rgb) +{ + m_crBackGnd = rgb; //set background color ref (used for text's background) + if(m_brBackGnd.GetSafeHandle()) //free brush + m_brBackGnd.DeleteObject(); + m_brBackGnd.CreateSolidBrush(rgb); //set brush to new color + Invalidate(TRUE); //redraw +} + + +void CColourEdit::SetTextColor(COLORREF rgb) +{ + m_crText = rgb; // set text color ref + Invalidate(TRUE); // redraw +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.h b/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.h new file mode 100644 index 00000000..6ff9daf1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ColourEdit.h @@ -0,0 +1,38 @@ +/* + * ColourEdit.h + * ------------ + * Purpose: Implementation of a coloured edit UI item. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CColourEdit : public CEdit +{ +public: + CColourEdit(); + ~CColourEdit(); + +public: + void SetTextColor(COLORREF rgb); + void SetBackColor(COLORREF rgb); + +private: + COLORREF m_crText; + COLORREF m_crBackGnd; + CBrush m_brBackGnd; + +protected: + afx_msg HBRUSH CtlColor(CDC* pDC, UINT nCtlColor); + DECLARE_MESSAGE_MAP() + +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.cpp new file mode 100644 index 00000000..8fe4f98f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.cpp @@ -0,0 +1,2027 @@ +/* + * CommandSet.cpp + * -------------- + * Purpose: Implementation of custom key handling. + * 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 "CommandSet.h" +#include "resource.h" +#include "Mptrack.h" // For ErrorBox +#include "../soundlib/mod_specifications.h" +#include "../soundlib/Tables.h" +#include "../mptrack/Reporting.h" +#include "../common/mptFileIO.h" +#include <sstream> +#include "TrackerSettings.h" + + +OPENMPT_NAMESPACE_BEGIN + +namespace +{ +// Version of the .mkb format +constexpr int KEYMAP_VERSION = 1; + +constexpr std::tuple<InputTargetContext, CommandID, CommandID> NoteContexts[] = +{ + {kCtxViewPatternsNote, kcVPStartNotes, kcVPStartNoteStops}, + {kCtxViewSamples, kcSampStartNotes, kcSampStartNoteStops}, + {kCtxViewInstruments, kcInstrumentStartNotes, kcInstrumentStartNoteStops}, + {kCtxViewTree, kcTreeViewStartNotes, kcTreeViewStartNoteStops}, + {kCtxInsNoteMap, kcInsNoteMapStartNotes, kcInsNoteMapStartNoteStops}, + {kCtxVSTGUI, kcVSTGUIStartNotes, kcVSTGUIStartNoteStops}, + {kCtxViewComments, kcCommentsStartNotes, kcCommentsStartNoteStops}, +}; + +}; // namespace + +#ifdef MPT_ALL_LOGGING +#define MPT_COMMANDSET_LOGGING +#endif + +#ifdef MPT_COMMANDSET_LOGGING +#define LOG_COMMANDSET(x) MPT_LOG_GLOBAL(LogDebug, "CommandSet", x) +#else +#define LOG_COMMANDSET(x) do { } while(0) +#endif + + +CCommandSet::CCommandSet() +{ + // Which key binding rules to enforce? + m_enforceRule[krPreventDuplicate] = true; + m_enforceRule[krDeleteOldOnConflict] = true; + m_enforceRule[krAllowNavigationWithSelection] = true; + m_enforceRule[krAllowSelectionWithNavigation] = true; + m_enforceRule[krAllowSelectCopySelectCombos] = true; + m_enforceRule[krLockNotesToChords] = true; + m_enforceRule[krNoteOffOnKeyRelease] = true; + m_enforceRule[krPropagateNotes] = true; + m_enforceRule[krReassignDigitsToOctaves] = false; + m_enforceRule[krAutoSelectOff] = true; + m_enforceRule[krAutoSpacing] = true; + m_enforceRule[krCheckModifiers] = true; + m_enforceRule[krPropagateSampleManipulation] = true; +// enforceRule[krCheckContextHierarchy] = true; + + SetupCommands(); + SetupContextHierarchy(); +} + + +// Setup + +KeyCommand::KeyCommand(uint32 uid, const TCHAR *message, std::vector<KeyCombination> keys) + : kcList{std::move(keys)} + , Message{message} + , UID{uid} +{ +} + +static constexpr struct +{ + uint32 uid = 0; // ID | Hidden | Dummy + CommandID cmd = kcNull; + const TCHAR *description = nullptr; +} CommandDefinitions[] = +{ + {1001, kcPatternRecord, _T("Enable Recording")}, + {1002, kcPatternPlayRow, _T("Play Row")}, + {1003, kcCursorCopy, _T("Quick Copy")}, + {1004, kcCursorPaste, _T("Quick Paste")}, + {1005, kcChannelMute, _T("Mute Current Channel")}, + {1006, kcChannelSolo, _T("Solo Current Channel")}, + {1007, kcTransposeUp, _T("Transpose +1")}, + {1008, kcTransposeDown, _T("Transpose -1")}, + {1009, kcTransposeOctUp, _T("Transpose +1 Octave")}, + {1010, kcTransposeOctDown, _T("Transpose -1 Octave")}, + {1011, kcSelectChannel, _T("Select Channel / Select All")}, + {1012, kcPatternAmplify, _T("Amplify selection")}, + {1013, kcPatternSetInstrument, _T("Apply current instrument")}, + {1014, kcPatternInterpolateVol, _T("Interpolate Volume")}, + {1015, kcPatternInterpolateEffect, _T("Interpolate Effect")}, + {1016, kcPatternVisualizeEffect, _T("Open Effect Visualizer")}, + {1017, kcPatternJumpDownh1, _T("Jump down by measure")}, + {1018, kcPatternJumpUph1, _T("Jump up by measure")}, + {1019, kcPatternSnapDownh1, _T("Snap down to measure")}, + {1020, kcPatternSnapUph1, _T("Snap up to measure")}, + {1021, kcViewGeneral, _T("View General")}, + {1022, kcViewPattern, _T("View Pattern")}, + {1023, kcViewSamples, _T("View Samples")}, + {1024, kcViewInstruments, _T("View Instruments")}, + {1025, kcViewComments, _T("View Comments")}, + {1026, kcPlayPatternFromCursor, _T("Play Pattern from Cursor")}, + {1027, kcPlayPatternFromStart, _T("Play Pattern from Start")}, + {1028, kcPlaySongFromCursor, _T("Play Song from Cursor")}, + {1029, kcPlaySongFromStart, _T("Play Song from Start")}, + {1030, kcPlayPauseSong, _T("Play Song / Pause Song")}, + {1031, kcPauseSong, _T("Pause Song")}, + {1032, kcPrevInstrument, _T("Previous Instrument")}, + {1033, kcNextInstrument, _T("Next Instrument")}, + {1034, kcPrevOrder, _T("Previous Order")}, + {1035, kcNextOrder, _T("Next Order")}, + {1036, kcPrevOctave, _T("Previous Octave")}, + {1037, kcNextOctave, _T("Next Octave")}, + {1038, kcNavigateDown, _T("Navigate down by 1 row")}, + {1039, kcNavigateUp, _T("Navigate up by 1 row")}, + {1040, kcNavigateLeft, _T("Navigate left")}, + {1041, kcNavigateRight, _T("Navigate right")}, + {1042, kcNavigateNextChan, _T("Navigate to next channel")}, + {1043, kcNavigatePrevChan, _T("Navigate to previous channel")}, + {1044, kcHomeHorizontal, _T("Go to first channel")}, + {1045, kcHomeVertical, _T("Go to first row")}, + {1046, kcHomeAbsolute, _T("Go to first row of first channel")}, + {1047, kcEndHorizontal, _T("Go to last channel")}, + {1048, kcEndVertical, _T("Go to last row")}, + {1049, kcEndAbsolute, _T("Go to last row of last channel")}, + {1050, kcSelect, _T("Selection key")}, + {1051, kcCopySelect, _T("Copy select key")}, + {KeyCommand::Hidden, kcSelectOff, _T("Deselect")}, + {KeyCommand::Hidden, kcCopySelectOff, _T("Copy deselect key")}, + {1054, kcNextPattern, _T("Next Pattern")}, + {1055, kcPrevPattern, _T("Previous Pattern")}, + //{1056, kcClearSelection, _T("Wipe selection")}, + {1057, kcClearRow, _T("Clear row")}, + {1058, kcClearField, _T("Clear field")}, + {1059, kcClearRowStep, _T("Clear row and step")}, + {1060, kcClearFieldStep, _T("Clear field and step")}, + {1061, kcDeleteRow, _T("Delete Row(s)")}, + {1062, kcShowNoteProperties, _T("Show Note Properties")}, + {1063, kcShowEditMenu, _T("Show Context (Right-Click) Menu")}, + {1064, kcVPNoteC_0, _T("Base octave C")}, + {1065, kcVPNoteCS0, _T("Base octave C#")}, + {1066, kcVPNoteD_0, _T("Base octave D")}, + {1067, kcVPNoteDS0, _T("Base octave D#")}, + {1068, kcVPNoteE_0, _T("Base octave E")}, + {1069, kcVPNoteF_0, _T("Base octave F")}, + {1070, kcVPNoteFS0, _T("Base octave F#")}, + {1071, kcVPNoteG_0, _T("Base octave G")}, + {1072, kcVPNoteGS0, _T("Base octave G#")}, + {1073, kcVPNoteA_1, _T("Base octave A")}, + {1074, kcVPNoteAS1, _T("Base octave A#")}, + {1075, kcVPNoteB_1, _T("Base octave B")}, + {1076, kcVPNoteC_1, _T("Base octave +1 C")}, + {1077, kcVPNoteCS1, _T("Base octave +1 C#")}, + {1078, kcVPNoteD_1, _T("Base octave +1 D")}, + {1079, kcVPNoteDS1, _T("Base octave +1 D#")}, + {1080, kcVPNoteE_1, _T("Base octave +1 E")}, + {1081, kcVPNoteF_1, _T("Base octave +1 F")}, + {1082, kcVPNoteFS1, _T("Base octave +1 F#")}, + {1083, kcVPNoteG_1, _T("Base octave +1 G")}, + {1084, kcVPNoteGS1, _T("Base octave +1 G#")}, + {1085, kcVPNoteA_2, _T("Base octave +1 A")}, + {1086, kcVPNoteAS2, _T("Base octave +1 A#")}, + {1087, kcVPNoteB_2, _T("Base octave +1 B")}, + {1088, kcVPNoteC_2, _T("Base octave +2 C")}, + {1089, kcVPNoteCS2, _T("Base octave +2 C#")}, + {1090, kcVPNoteD_2, _T("Base octave +2 D")}, + {1091, kcVPNoteDS2, _T("Base octave +2 D#")}, + {1092, kcVPNoteE_2, _T("Base octave +2 E")}, + {1093, kcVPNoteF_2, _T("Base octave +2 F")}, + {1094, kcVPNoteFS2, _T("Base octave +2 F#")}, + {1095, kcVPNoteG_2, _T("Base octave +2 G")}, + {1096, kcVPNoteGS2, _T("Base octave +2 G#")}, + {1097, kcVPNoteA_3, _T("Base octave +2 A")}, + {KeyCommand::Hidden, kcVPNoteStopC_0, _T("Stop base octave C")}, + {KeyCommand::Hidden, kcVPNoteStopCS0, _T("Stop base octave C#")}, + {KeyCommand::Hidden, kcVPNoteStopD_0, _T("Stop base octave D")}, + {KeyCommand::Hidden, kcVPNoteStopDS0, _T("Stop base octave D#")}, + {KeyCommand::Hidden, kcVPNoteStopE_0, _T("Stop base octave E")}, + {KeyCommand::Hidden, kcVPNoteStopF_0, _T("Stop base octave F")}, + {KeyCommand::Hidden, kcVPNoteStopFS0, _T("Stop base octave F#")}, + {KeyCommand::Hidden, kcVPNoteStopG_0, _T("Stop base octave G")}, + {KeyCommand::Hidden, kcVPNoteStopGS0, _T("Stop base octave G#")}, + {KeyCommand::Hidden, kcVPNoteStopA_1, _T("Stop base octave +1 A")}, + {KeyCommand::Hidden, kcVPNoteStopAS1, _T("Stop base octave +1 A#")}, + {KeyCommand::Hidden, kcVPNoteStopB_1, _T("Stop base octave +1 B")}, + {KeyCommand::Hidden, kcVPNoteStopC_1, _T("Stop base octave +1 C")}, + {KeyCommand::Hidden, kcVPNoteStopCS1, _T("Stop base octave +1 C#")}, + {KeyCommand::Hidden, kcVPNoteStopD_1, _T("Stop base octave +1 D")}, + {KeyCommand::Hidden, kcVPNoteStopDS1, _T("Stop base octave +1 D#")}, + {KeyCommand::Hidden, kcVPNoteStopE_1, _T("Stop base octave +1 E")}, + {KeyCommand::Hidden, kcVPNoteStopF_1, _T("Stop base octave +1 F")}, + {KeyCommand::Hidden, kcVPNoteStopFS1, _T("Stop base octave +1 F#")}, + {KeyCommand::Hidden, kcVPNoteStopG_1, _T("Stop base octave +1 G")}, + {KeyCommand::Hidden, kcVPNoteStopGS1, _T("Stop base octave +1 G#")}, + {KeyCommand::Hidden, kcVPNoteStopA_2, _T("Stop base octave +2 A")}, + {KeyCommand::Hidden, kcVPNoteStopAS2, _T("Stop base octave +2 A#")}, + {KeyCommand::Hidden, kcVPNoteStopB_2, _T("Stop base octave +2 B")}, + {KeyCommand::Hidden, kcVPNoteStopC_2, _T("Stop base octave +2 C")}, + {KeyCommand::Hidden, kcVPNoteStopCS2, _T("Stop base octave +2 C#")}, + {KeyCommand::Hidden, kcVPNoteStopD_2, _T("Stop base octave +2 D")}, + {KeyCommand::Hidden, kcVPNoteStopDS2, _T("Stop base octave +2 D#")}, + {KeyCommand::Hidden, kcVPNoteStopE_2, _T("Stop base octave +2 E")}, + {KeyCommand::Hidden, kcVPNoteStopF_2, _T("Stop base octave +2 F")}, + {KeyCommand::Hidden, kcVPNoteStopFS2, _T("Stop base octave +2 F#")}, + {KeyCommand::Hidden, kcVPNoteStopG_2, _T("Stop base octave +2 G")}, + {KeyCommand::Hidden, kcVPNoteStopGS2, _T("Stop base octave +2 G#")}, + {KeyCommand::Hidden, kcVPNoteStopA_3, _T("Stop base octave +3 A")}, + {KeyCommand::Hidden, kcVPChordC_0, _T("Base octave chord C")}, + {KeyCommand::Hidden, kcVPChordCS0, _T("Base octave chord C#")}, + {KeyCommand::Hidden, kcVPChordD_0, _T("Base octave chord D")}, + {KeyCommand::Hidden, kcVPChordDS0, _T("Base octave chord D#")}, + {KeyCommand::Hidden, kcVPChordE_0, _T("Base octave chord E")}, + {KeyCommand::Hidden, kcVPChordF_0, _T("Base octave chord F")}, + {KeyCommand::Hidden, kcVPChordFS0, _T("Base octave chord F#")}, + {KeyCommand::Hidden, kcVPChordG_0, _T("Base octave chord G")}, + {KeyCommand::Hidden, kcVPChordGS0, _T("Base octave chord G#")}, + {KeyCommand::Hidden, kcVPChordA_1, _T("Base octave +1 chord A")}, + {KeyCommand::Hidden, kcVPChordAS1, _T("Base octave +1 chord A#")}, + {KeyCommand::Hidden, kcVPChordB_1, _T("Base octave +1 chord B")}, + {KeyCommand::Hidden, kcVPChordC_1, _T("Base octave +1 chord C")}, + {KeyCommand::Hidden, kcVPChordCS1, _T("Base octave +1 chord C#")}, + {KeyCommand::Hidden, kcVPChordD_1, _T("Base octave +1 chord D")}, + {KeyCommand::Hidden, kcVPChordDS1, _T("Base octave +1 chord D#")}, + {KeyCommand::Hidden, kcVPChordE_1, _T("Base octave +1 chord E")}, + {KeyCommand::Hidden, kcVPChordF_1, _T("Base octave +1 chord F")}, + {KeyCommand::Hidden, kcVPChordFS1, _T("Base octave +1 chord F#")}, + {KeyCommand::Hidden, kcVPChordG_1, _T("Base octave +1 chord G")}, + {KeyCommand::Hidden, kcVPChordGS1, _T("Base octave +1 chord G#")}, + {KeyCommand::Hidden, kcVPChordA_2, _T("Base octave +2 chord A")}, + {KeyCommand::Hidden, kcVPChordAS2, _T("Base octave +2 chord A#")}, + {KeyCommand::Hidden, kcVPChordB_2, _T("Base octave +2 chord B")}, + {KeyCommand::Hidden, kcVPChordC_2, _T("Base octave +2 chord C")}, + {KeyCommand::Hidden, kcVPChordCS2, _T("Base octave +2 chord C#")}, + {KeyCommand::Hidden, kcVPChordD_2, _T("Base octave +2 chord D")}, + {KeyCommand::Hidden, kcVPChordDS2, _T("Base octave +2 chord D#")}, + {KeyCommand::Hidden, kcVPChordE_2, _T("Base octave +2 chord E")}, + {KeyCommand::Hidden, kcVPChordF_2, _T("Base octave +2 chord F")}, + {KeyCommand::Hidden, kcVPChordFS2, _T("Base octave +2 chord F#")}, + {KeyCommand::Hidden, kcVPChordG_2, _T("Base octave +2 chord G")}, + {KeyCommand::Hidden, kcVPChordGS2, _T("Base octave +2 chord G#")}, + {KeyCommand::Hidden, kcVPChordA_3, _T("Base octave chord +3 A")}, + {KeyCommand::Hidden, kcVPChordStopC_0, _T("Stop base octave chord C")}, + {KeyCommand::Hidden, kcVPChordStopCS0, _T("Stop base octave chord C#")}, + {KeyCommand::Hidden, kcVPChordStopD_0, _T("Stop base octave chord D")}, + {KeyCommand::Hidden, kcVPChordStopDS0, _T("Stop base octave chord D#")}, + {KeyCommand::Hidden, kcVPChordStopE_0, _T("Stop base octave chord E")}, + {KeyCommand::Hidden, kcVPChordStopF_0, _T("Stop base octave chord F")}, + {KeyCommand::Hidden, kcVPChordStopFS0, _T("Stop base octave chord F#")}, + {KeyCommand::Hidden, kcVPChordStopG_0, _T("Stop base octave chord G")}, + {KeyCommand::Hidden, kcVPChordStopGS0, _T("Stop base octave chord G#")}, + {KeyCommand::Hidden, kcVPChordStopA_1, _T("Stop base octave +1 chord A")}, + {KeyCommand::Hidden, kcVPChordStopAS1, _T("Stop base octave +1 chord A#")}, + {KeyCommand::Hidden, kcVPChordStopB_1, _T("Stop base octave +1 chord B")}, + {KeyCommand::Hidden, kcVPChordStopC_1, _T("Stop base octave +1 chord C")}, + {KeyCommand::Hidden, kcVPChordStopCS1, _T("Stop base octave +1 chord C#")}, + {KeyCommand::Hidden, kcVPChordStopD_1, _T("Stop base octave +1 chord D")}, + {KeyCommand::Hidden, kcVPChordStopDS1, _T("Stop base octave +1 chord D#")}, + {KeyCommand::Hidden, kcVPChordStopE_1, _T("Stop base octave +1 chord E")}, + {KeyCommand::Hidden, kcVPChordStopF_1, _T("Stop base octave +1 chord F")}, + {KeyCommand::Hidden, kcVPChordStopFS1, _T("Stop base octave +1 chord F#")}, + {KeyCommand::Hidden, kcVPChordStopG_1, _T("Stop base octave +1 chord G")}, + {KeyCommand::Hidden, kcVPChordStopGS1, _T("Stop base octave +1 chord G#")}, + {KeyCommand::Hidden, kcVPChordStopA_2, _T("Stop base octave +2 chord A")}, + {KeyCommand::Hidden, kcVPChordStopAS2, _T("Stop base octave +2 chord A#")}, + {KeyCommand::Hidden, kcVPChordStopB_2, _T("Stop base octave +2 chord B")}, + {KeyCommand::Hidden, kcVPChordStopC_2, _T("Stop base octave +2 chord C")}, + {KeyCommand::Hidden, kcVPChordStopCS2, _T("Stop base octave +2 chord C#")}, + {KeyCommand::Hidden, kcVPChordStopD_2, _T("Stop base octave +2 chord D")}, + {KeyCommand::Hidden, kcVPChordStopDS2, _T("Stop base octave +2 chord D#")}, + {KeyCommand::Hidden, kcVPChordStopE_2, _T("Stop base octave +2 chord E")}, + {KeyCommand::Hidden, kcVPChordStopF_2, _T("Stop base octave +2 chord F")}, + {KeyCommand::Hidden, kcVPChordStopFS2, _T("Stop base octave +2 chord F#")}, + {KeyCommand::Hidden, kcVPChordStopG_2, _T("Stop base octave +2 chord G")}, + {KeyCommand::Hidden, kcVPChordStopGS2, _T("Stop base octave +2 chord G#")}, + {KeyCommand::Hidden, kcVPChordStopA_3, _T("Stop base octave +3 chord A")}, + {1200, kcNoteCut, _T("Note Cut")}, + {1201, kcNoteOff, _T("Note Off")}, + {1202, kcSetIns0, _T("Set instrument digit 0")}, + {1203, kcSetIns1, _T("Set instrument digit 1")}, + {1204, kcSetIns2, _T("Set instrument digit 2")}, + {1205, kcSetIns3, _T("Set instrument digit 3")}, + {1206, kcSetIns4, _T("Set instrument digit 4")}, + {1207, kcSetIns5, _T("Set instrument digit 5")}, + {1208, kcSetIns6, _T("Set instrument digit 6")}, + {1209, kcSetIns7, _T("Set instrument digit 7")}, + {1210, kcSetIns8, _T("Set instrument digit 8")}, + {1211, kcSetIns9, _T("Set instrument digit 9")}, + {1212, kcSetOctave0, _T("Set octave 0")}, + {1213, kcSetOctave1, _T("Set octave 1")}, + {1214, kcSetOctave2, _T("Set octave 2")}, + {1215, kcSetOctave3, _T("Set octave 3")}, + {1216, kcSetOctave4, _T("Set octave 4")}, + {1217, kcSetOctave5, _T("Set octave 5")}, + {1218, kcSetOctave6, _T("Set octave 6")}, + {1219, kcSetOctave7, _T("Set octave 7")}, + {1220, kcSetOctave8, _T("Set octave 8")}, + {1221, kcSetOctave9, _T("Set octave 9")}, + {1222, kcSetVolume0, _T("Set volume digit 0")}, + {1223, kcSetVolume1, _T("Set volume digit 1")}, + {1224, kcSetVolume2, _T("Set volume digit 2")}, + {1225, kcSetVolume3, _T("Set volume digit 3")}, + {1226, kcSetVolume4, _T("Set volume digit 4")}, + {1227, kcSetVolume5, _T("Set volume digit 5")}, + {1228, kcSetVolume6, _T("Set volume digit 6")}, + {1229, kcSetVolume7, _T("Set volume digit 7")}, + {1230, kcSetVolume8, _T("Set volume digit 8")}, + {1231, kcSetVolume9, _T("Set volume digit 9")}, + {1232, kcSetVolumeVol, _T("Volume Command - Volume")}, + {1233, kcSetVolumePan, _T("Volume Command - Panning")}, + {1234, kcSetVolumeVolSlideUp, _T("Volume Command - Volume Slide Up")}, + {1235, kcSetVolumeVolSlideDown, _T("Volume Command - Volume Slide Down")}, + {1236, kcSetVolumeFineVolUp, _T("Volume Command - Fine Volume Slide Up")}, + {1237, kcSetVolumeFineVolDown, _T("Volume Command - Fine Volume Slide Down")}, + {1238, kcSetVolumeVibratoSpd, _T("Volume Command - Vibrato Speed")}, + {1239, kcSetVolumeVibrato, _T("Volume Command - Vibrato Depth")}, + {1240, kcSetVolumeXMPanLeft, _T("Volume Command - XM Pan Slide Left")}, + {1241, kcSetVolumeXMPanRight, _T("Volume Command - XM Pan Slide Right")}, + {1242, kcSetVolumePortamento, _T("Volume Command - Tone Portamento")}, + {1243, kcSetVolumeITPortaUp, _T("Volume Command - Portamento Up")}, + {1244, kcSetVolumeITPortaDown, _T("Volume Command - Portamento Down")}, + {KeyCommand::Hidden, kcSetVolumeITUnused, _T("Volume Command - Unused")}, + {1246, kcSetVolumeITOffset, _T("Volume Command - Offset")}, + {1247, kcSetFXParam0, _T("Effect Parameter Digit 0")}, + {1248, kcSetFXParam1, _T("Effect Parameter Digit 1")}, + {1249, kcSetFXParam2, _T("Effect Parameter Digit 2")}, + {1250, kcSetFXParam3, _T("Effect Parameter Digit 3")}, + {1251, kcSetFXParam4, _T("Effect Parameter Digit 4")}, + {1252, kcSetFXParam5, _T("Effect Parameter Digit 5")}, + {1253, kcSetFXParam6, _T("Effect Parameter Digit 6")}, + {1254, kcSetFXParam7, _T("Effect Parameter Digit 7")}, + {1255, kcSetFXParam8, _T("Effect Parameter Digit 8")}, + {1256, kcSetFXParam9, _T("Effect Parameter Digit 9")}, + {1257, kcSetFXParamA, _T("Effect Parameter Digit A")}, + {1258, kcSetFXParamB, _T("Effect Parameter Digit B")}, + {1259, kcSetFXParamC, _T("Effect Parameter Digit C")}, + {1260, kcSetFXParamD, _T("Effect Parameter Digit D")}, + {1261, kcSetFXParamE, _T("Effect Parameter Digit E")}, + {1262, kcSetFXParamF, _T("Effect Parameter Digit F")}, + {KeyCommand::Hidden, kcSetFXarp, _T("FX Arpeggio")}, + {KeyCommand::Hidden, kcSetFXportUp, _T("FX Portamento Up")}, + {KeyCommand::Hidden, kcSetFXportDown, _T("FX Portamento Down")}, + {KeyCommand::Hidden, kcSetFXport, _T("FX Tone Portamento")}, + {KeyCommand::Hidden, kcSetFXvibrato, _T("FX Vibrato")}, + {KeyCommand::Hidden, kcSetFXportSlide, _T("FX Portamento + Volume Slide")}, + {KeyCommand::Hidden, kcSetFXvibSlide, _T("FX Vibrato + Volume Slide")}, + {KeyCommand::Hidden, kcSetFXtremolo, _T("FX Tremolo")}, + {KeyCommand::Hidden, kcSetFXpan, _T("FX Pan")}, + {KeyCommand::Hidden, kcSetFXoffset, _T("FX Offset")}, + {KeyCommand::Hidden, kcSetFXvolSlide, _T("FX Volume Slide")}, + {KeyCommand::Hidden, kcSetFXgotoOrd, _T("FX Pattern Jump")}, + {KeyCommand::Hidden, kcSetFXsetVol, _T("FX Set Volume")}, + {KeyCommand::Hidden, kcSetFXgotoRow, _T("FX Break To Row")}, + {KeyCommand::Hidden, kcSetFXretrig, _T("FX Retrigger")}, + {KeyCommand::Hidden, kcSetFXspeed, _T("FX Set Speed")}, + {KeyCommand::Hidden, kcSetFXtempo, _T("FX Set Tempo")}, + {KeyCommand::Hidden, kcSetFXtremor, _T("FX Tremor")}, + {KeyCommand::Hidden, kcSetFXextendedMOD, _T("FX Extended MOD Commands")}, + {KeyCommand::Hidden, kcSetFXextendedS3M, _T("FX Extended S3M Commands")}, + {KeyCommand::Hidden, kcSetFXchannelVol, _T("FX Set Channel Volume")}, + {KeyCommand::Hidden, kcSetFXchannelVols, _T("FX Channel Volume Slide")}, + {KeyCommand::Hidden, kcSetFXglobalVol, _T("FX Set Global Volume")}, + {KeyCommand::Hidden, kcSetFXglobalVols, _T("FX Global Volume Slide")}, + {KeyCommand::Hidden, kcSetFXkeyoff, _T("FX Key Off (XM)")}, + {KeyCommand::Hidden, kcSetFXfineVib, _T("FX Fine Vibrato")}, + {KeyCommand::Hidden, kcSetFXpanbrello, _T("FX Panbrello")}, + {KeyCommand::Hidden, kcSetFXextendedXM, _T("FX Extended XM Commands")}, + {KeyCommand::Hidden, kcSetFXpanSlide, _T("FX Pan Slide")}, + {KeyCommand::Hidden, kcSetFXsetEnvPos, _T("FX Set Envelope Position (XM)")}, + {KeyCommand::Hidden, kcSetFXmacro, _T("FX MIDI Macro")}, + {1294, kcSetFXmacroSlide, _T("Smooth MIDI Macro Slide")}, + {1295, kcSetFXdelaycut, _T("Combined Note Delay and Note Cut")}, + {KeyCommand::Hidden, kcPatternJumpDownh1Select, _T("kcPatternJumpDownh1Select")}, + {KeyCommand::Hidden, kcPatternJumpUph1Select, _T("kcPatternJumpUph1Select")}, + {KeyCommand::Hidden, kcPatternSnapDownh1Select, _T("kcPatternSnapDownh1Select")}, + {KeyCommand::Hidden, kcPatternSnapUph1Select, _T("kcPatternSnapUph1Select")}, + {KeyCommand::Hidden, kcNavigateDownSelect, _T("kcNavigateDownSelect")}, + {KeyCommand::Hidden, kcNavigateUpSelect, _T("kcNavigateUpSelect")}, + {KeyCommand::Hidden, kcNavigateLeftSelect, _T("kcNavigateLeftSelect")}, + {KeyCommand::Hidden, kcNavigateRightSelect, _T("kcNavigateRightSelect")}, + {KeyCommand::Hidden, kcNavigateNextChanSelect, _T("kcNavigateNextChanSelect")}, + {KeyCommand::Hidden, kcNavigatePrevChanSelect, _T("kcNavigatePrevChanSelect")}, + {KeyCommand::Hidden, kcHomeHorizontalSelect, _T("kcHomeHorizontalSelect")}, + {KeyCommand::Hidden, kcHomeVerticalSelect, _T("kcHomeVerticalSelect")}, + {KeyCommand::Hidden, kcHomeAbsoluteSelect, _T("kcHomeAbsoluteSelect")}, + {KeyCommand::Hidden, kcEndHorizontalSelect, _T("kcEndHorizontalSelect")}, + {KeyCommand::Hidden, kcEndVerticalSelect, _T("kcEndVerticalSelect")}, + {KeyCommand::Hidden, kcEndAbsoluteSelect, _T("kcEndAbsoluteSelect")}, + {KeyCommand::Hidden, kcSelectWithNav, _T("kcSelectWithNav")}, + {KeyCommand::Hidden, kcSelectOffWithNav, _T("kcSelectOffWithNav")}, + {KeyCommand::Hidden, kcCopySelectWithNav, _T("kcCopySelectWithNav")}, + {KeyCommand::Hidden, kcCopySelectOffWithNav, _T("kcCopySelectOffWithNav")}, + {1316 | KeyCommand::Dummy, kcChordModifier, _T("Chord Modifier")}, + {1317 | KeyCommand::Dummy, kcSetSpacing, _T("Set edit step on note entry")}, + {KeyCommand::Hidden, kcSetSpacing0, _T("")}, + {KeyCommand::Hidden, kcSetSpacing1, _T("")}, + {KeyCommand::Hidden, kcSetSpacing2, _T("")}, + {KeyCommand::Hidden, kcSetSpacing3, _T("")}, + {KeyCommand::Hidden, kcSetSpacing4, _T("")}, + {KeyCommand::Hidden, kcSetSpacing5, _T("")}, + {KeyCommand::Hidden, kcSetSpacing6, _T("")}, + {KeyCommand::Hidden, kcSetSpacing7, _T("")}, + {KeyCommand::Hidden, kcSetSpacing8, _T("")}, + {KeyCommand::Hidden, kcSetSpacing9, _T("")}, + {KeyCommand::Hidden, kcCopySelectWithSelect, _T("kcCopySelectWithSelect")}, + {KeyCommand::Hidden, kcCopySelectOffWithSelect, _T("kcCopySelectOffWithSelect")}, + {KeyCommand::Hidden, kcSelectWithCopySelect, _T("kcSelectWithCopySelect")}, + {KeyCommand::Hidden, kcSelectOffWithCopySelect, _T("kcSelectOffWithCopySelect")}, + /* + {1332, kcCopy, _T("Copy pattern data")}, + {1333, kcCut, _T("Cut pattern data")}, + {1334, kcPaste, _T("Paste pattern data")}, + {1335, kcMixPaste, _T("Mix-paste pattern data")}, + {1336, kcSelectAll, _T("Select all pattern data")}, + {CommandStruct::Hidden, kcSelectCol, _T("Select Channel / Select All")}, + */ + {1338, kcPatternJumpDownh2, _T("Jump down by beat")}, + {1339, kcPatternJumpUph2, _T("Jump up by beat")}, + {1340, kcPatternSnapDownh2, _T("Snap down to beat")}, + {1341, kcPatternSnapUph2, _T("Snap up to beat")}, + {KeyCommand::Hidden, kcPatternJumpDownh2Select, _T("kcPatternJumpDownh2Select")}, + {KeyCommand::Hidden, kcPatternJumpUph2Select, _T("kcPatternJumpUph2Select")}, + {KeyCommand::Hidden, kcPatternSnapDownh2Select, _T("kcPatternSnapDownh2Select")}, + {KeyCommand::Hidden, kcPatternSnapUph2Select, _T("kcPatternSnapUph2Select")}, + {1346, kcFileOpen, _T("File/Open")}, + {1347, kcFileNew, _T("File/New")}, + {1348, kcFileClose, _T("File/Close")}, + {1349, kcFileSave, _T("File/Save")}, + {1350, kcFileSaveAs, _T("File/Save As")}, + {1351, kcFileSaveAsWave, _T("File/Stream Export")}, + {1352 | KeyCommand::Hidden, kcFileSaveAsMP3, _T("File/Stream Export")}, // Legacy + {1353, kcFileSaveMidi, _T("File/Export as MIDI")}, + {1354, kcFileImportMidiLib, _T("File/Import MIDI Library")}, + {1355, kcFileAddSoundBank, _T("File/Add Sound Bank")}, + {1359, kcEditUndo, _T("Undo")}, + {1360, kcEditCut, _T("Cut")}, + {1361, kcEditCopy, _T("Copy")}, + {1362, kcEditPaste, _T("Paste")}, + {1363, kcEditMixPaste, _T("Mix Paste")}, + {1364, kcEditSelectAll, _T("Select All")}, + {1365, kcEditFind, _T("Find / Replace")}, + {1366, kcEditFindNext, _T("Find Next")}, + {1367, kcViewMain, _T("Toggle Main Toolbar")}, + {1368, kcViewTree, _T("Toggle Tree View")}, + {1369, kcViewOptions, _T("View Options")}, + {1370, kcHelp, _T("Help")}, + /* + {1370, kcWindowNew, _T("New Window")}, + {1371, kcWindowCascade, _T("Cascade Windows")}, + {1372, kcWindowTileHorz, _T("Tile Windows Horizontally")}, + {1373, kcWindowTileVert, _T("Tile Windows Vertically")}, + */ + {1374, kcEstimateSongLength, _T("Estimate Song Length")}, + {1375, kcStopSong, _T("Stop Song")}, + {1376, kcMidiRecord, _T("Toggle MIDI Record")}, + {1377, kcDeleteWholeRow, _T("Delete Row(s) (All Channels)")}, + {1378, kcInsertRow, _T("Insert Row(s)")}, + {1379, kcInsertWholeRow, _T("Insert Row(s) (All Channels)")}, + {1380, kcSampleTrim, _T("Trim sample around loop points")}, + {1381, kcSampleReverse, _T("Reverse Sample")}, + {1382, kcSampleDelete, _T("Delete Sample Selection")}, + {1383, kcSampleSilence, _T("Silence Sample Selection")}, + {1384, kcSampleNormalize, _T("Normalize Sample")}, + {1385, kcSampleAmplify, _T("Amplify Sample")}, + {1386, kcSampleZoomUp, _T("Zoom In")}, + {1387, kcSampleZoomDown, _T("Zoom Out")}, + {1660, kcPatternGrowSelection, _T("Grow selection")}, + {1661, kcPatternShrinkSelection, _T("Shrink selection")}, + {1662, kcTogglePluginEditor, _T("Toggle channel's plugin editor")}, + {1663, kcToggleFollowSong, _T("Toggle follow song")}, + {1664, kcClearFieldITStyle, _T("Clear field (IT Style)")}, + {1665, kcClearFieldStepITStyle, _T("Clear field and step (IT Style)")}, + {1666, kcSetFXextension, _T("Parameter Extension Command")}, + {1667 | KeyCommand::Hidden, kcNoteCutOld, _T("Note Cut")}, // Legacy + {1668 | KeyCommand::Hidden, kcNoteOffOld, _T("Note Off")}, // Legacy + {1669, kcViewAddPlugin, _T("View Plugin Manager")}, + {1670, kcViewChannelManager, _T("View Channel Manager")}, + {1671, kcCopyAndLoseSelection, _T("Copy and lose selection")}, + {1672, kcNewPattern, _T("Insert new pattern")}, + {1673, kcSampleLoad, _T("Load Sample")}, + {1674, kcSampleSave, _T("Save Sample")}, + {1675, kcSampleNew, _T("New Sample")}, + //{CommandStruct::Hidden, kcSampleCtrlLoad, _T("Load Sample")}, + //{CommandStruct::Hidden, kcSampleCtrlSave, _T("Save Sample")}, + //{CommandStruct::Hidden, kcSampleCtrlNew, _T("New Sample")}, + {KeyCommand::Hidden, kcInstrumentLoad, _T("Load Instrument")}, + {KeyCommand::Hidden, kcInstrumentSave, _T("Save Instrument")}, + {KeyCommand::Hidden, kcInstrumentNew, _T("New Instrument")}, + {KeyCommand::Hidden, kcInstrumentCtrlLoad, _T("Load Instrument")}, + {KeyCommand::Hidden, kcInstrumentCtrlSave, _T("Save Instrument")}, + {KeyCommand::Hidden, kcInstrumentCtrlNew, _T("New Instrument")}, + {1685, kcSwitchToOrderList, _T("Switch to Order List")}, + {1686, kcEditMixPasteITStyle, _T("Mix Paste (IT Style)")}, + {1687, kcApproxRealBPM, _T("Show approx. real BPM")}, + {KeyCommand::Hidden, kcNavigateDownBySpacingSelect, _T("kcNavigateDownBySpacingSelect")}, + {KeyCommand::Hidden, kcNavigateUpBySpacingSelect, _T("kcNavigateUpBySpacingSelect")}, + {1691, kcNavigateDownBySpacing, _T("Navigate down by spacing")}, + {1692, kcNavigateUpBySpacing, _T("Navigate up by spacing")}, + {1693, kcPrevDocument, _T("Previous Document")}, + {1694, kcNextDocument, _T("Next Document")}, + {1763, kcVSTGUIPrevPreset, _T("Previous Plugin Preset")}, + {1764, kcVSTGUINextPreset, _T("Next Plugin Preset")}, + {1765, kcVSTGUIRandParams, _T("Randomize Plugin Parameters")}, + {1766, kcPatternGoto, _T("Go to row/channel/...")}, + {KeyCommand::Hidden, kcPatternOpenRandomizer, _T("Pattern Randomizer")}, // while there's not randomizer yet, let's just disable it for now + {1768, kcPatternInterpolateNote, _T("Interpolate Note")}, + {KeyCommand::Hidden, kcViewGraph, _T("View Graph")}, // while there's no graph yet, let's just disable it for now + {1770, kcToggleChanMuteOnPatTransition, _T("(Un)mute channel on pattern transition")}, + {1771, kcChannelUnmuteAll, _T("Unmute all channels")}, + {1772, kcShowPatternProperties, _T("Show Pattern Properties")}, + {1773, kcShowMacroConfig, _T("View Zxx Macro Configuration")}, + {1775, kcViewSongProperties, _T("View Song Properties")}, + {1776, kcChangeLoopStatus, _T("Toggle Loop Pattern")}, + {1777, kcFileExportCompat, _T("File/Compatibility Export")}, + {1778, kcUnmuteAllChnOnPatTransition, _T("Unmute all channels on pattern transition")}, + {1779, kcSoloChnOnPatTransition, _T("Solo channel on pattern transition")}, + {1780, kcTimeAtRow, _T("Show playback time at current row")}, + {1781, kcViewMIDImapping, _T("View MIDI Mapping")}, + {1782, kcVSTGUIPrevPresetJump, _T("Plugin preset backward jump")}, + {1783, kcVSTGUINextPresetJump, _T("Plugin preset forward jump")}, + {1784, kcSampleInvert, _T("Invert Sample Phase")}, + {1785, kcSampleSignUnsign, _T("Signed / Unsigned Conversion")}, + {1786, kcChannelReset, _T("Reset Channel")}, + {1787, kcToggleOverflowPaste, _T("Toggle overflow paste")}, + {1788, kcNotePC, _T("Parameter Control")}, + {1789, kcNotePCS, _T("Parameter Control (smooth)")}, + {1790, kcSampleRemoveDCOffset, _T("Remove DC Offset")}, + {1791, kcNoteFade, _T("Note Fade")}, + {1792 | KeyCommand::Hidden, kcNoteFadeOld, _T("Note Fade")}, // Legacy + {1793, kcEditPasteFlood, _T("Paste Flood")}, + {1794, kcOrderlistNavigateLeft, _T("Previous Order")}, + {1795, kcOrderlistNavigateRight, _T("Next Order")}, + {1796, kcOrderlistNavigateFirst, _T("First Order")}, + {1797, kcOrderlistNavigateLast, _T("Last Order")}, + {KeyCommand::Hidden, kcOrderlistNavigateLeftSelect, _T("kcOrderlistNavigateLeftSelect")}, + {KeyCommand::Hidden, kcOrderlistNavigateRightSelect, _T("kcOrderlistNavigateRightSelect")}, + {KeyCommand::Hidden, kcOrderlistNavigateFirstSelect, _T("kcOrderlistNavigateFirstSelect")}, + {KeyCommand::Hidden, kcOrderlistNavigateLastSelect, _T("kcOrderlistNavigateLastSelect")}, + {1802, kcOrderlistEditDelete, _T("Delete Order")}, + {1803, kcOrderlistEditInsert, _T("Insert Order")}, + {1804, kcOrderlistEditPattern, _T("Edit Pattern")}, + {1805, kcOrderlistSwitchToPatternView, _T("Switch to pattern editor")}, + {1806, kcDuplicatePattern, _T("Duplicate Pattern")}, + {1807, kcOrderlistPat0, _T("Pattern index digit 0")}, + {1808, kcOrderlistPat1, _T("Pattern index digit 1")}, + {1809, kcOrderlistPat2, _T("Pattern index digit 2")}, + {1810, kcOrderlistPat3, _T("Pattern index digit 3")}, + {1811, kcOrderlistPat4, _T("Pattern index digit 4")}, + {1812, kcOrderlistPat5, _T("Pattern index digit 5")}, + {1813, kcOrderlistPat6, _T("Pattern index digit 6")}, + {1814, kcOrderlistPat7, _T("Pattern index digit 7")}, + {1815, kcOrderlistPat8, _T("Pattern index digit 8")}, + {1816, kcOrderlistPat9, _T("Pattern index digit 9")}, + {1817, kcOrderlistPatPlus, _T("Increase pattern index ")}, + {1818, kcOrderlistPatMinus, _T("Decrease pattern index")}, + {1819, kcShowSplitKeyboardSettings, _T("Split Keyboard Settings dialog")}, + {1820, kcEditPushForwardPaste, _T("Push Forward Paste (Insert)")}, + {1821, kcInstrumentEnvelopePointMoveLeft, _T("Move envelope point left")}, + {1822, kcInstrumentEnvelopePointMoveRight, _T("Move envelope point right")}, + {1823, kcInstrumentEnvelopePointMoveUp, _T("Move envelope point up")}, + {1824, kcInstrumentEnvelopePointMoveDown, _T("Move envelope point down")}, + {1825, kcInstrumentEnvelopePointPrev, _T("Select previous envelope point")}, + {1826, kcInstrumentEnvelopePointNext, _T("Select next envelope point")}, + {1827, kcInstrumentEnvelopePointInsert, _T("Insert Envelope Point")}, + {1828, kcInstrumentEnvelopePointRemove, _T("Remove Envelope Point")}, + {1829, kcInstrumentEnvelopeSetLoopStart, _T("Set Loop Start")}, + {1830, kcInstrumentEnvelopeSetLoopEnd, _T("Set Loop End")}, + {1831, kcInstrumentEnvelopeSetSustainLoopStart, _T("Set sustain loop start")}, + {1832, kcInstrumentEnvelopeSetSustainLoopEnd, _T("Set sustain loop end")}, + {1833, kcInstrumentEnvelopeToggleReleaseNode, _T("Toggle release node")}, + {1834, kcInstrumentEnvelopePointMoveUp8, _T("Move envelope point up (Coarse)")}, + {1835, kcInstrumentEnvelopePointMoveDown8, _T("Move envelope point down (Coarse)")}, + {1836, kcPatternEditPCNotePlugin, _T("Toggle PC Event/instrument plugin editor")}, + {1837, kcInstrumentEnvelopeZoomIn, _T("Zoom In")}, + {1838, kcInstrumentEnvelopeZoomOut, _T("Zoom Out")}, + {1839, kcVSTGUIToggleRecordParams, _T("Toggle Parameter Recording")}, + {1840, kcVSTGUIToggleSendKeysToPlug, _T("Pass Key Presses to Plugin")}, + {1841, kcVSTGUIBypassPlug, _T("Bypass Plugin")}, + {1842, kcInsNoteMapTransposeDown, _T("Transpose -1 (Note Map)")}, + {1843, kcInsNoteMapTransposeUp, _T("Transpose +1 (Note Map)")}, + {1844, kcInsNoteMapTransposeOctDown, _T("Transpose -1 Octave (Note Map)")}, + {1845, kcInsNoteMapTransposeOctUp, _T("Transpose +1 Octave (Note Map)")}, + {1846, kcInsNoteMapCopyCurrentNote, _T("Map all notes to selected note")}, + {1847, kcInsNoteMapCopyCurrentSample, _T("Map all notes to selected sample")}, + {1848, kcInsNoteMapReset, _T("Reset Note Mapping")}, + {1849, kcInsNoteMapEditSample, _T("Edit Current Sample")}, + {1850, kcInsNoteMapEditSampleMap, _T("Edit Sample Map")}, + {1851, kcInstrumentCtrlDuplicate, _T("Duplicate Instrument")}, + {1852, kcPanic, _T("Panic")}, + {1853, kcOrderlistPatIgnore, _T("Separator (+++) Index")}, + {1854, kcOrderlistPatInvalid, _T("Stop (---) Index")}, + {1855, kcViewEditHistory, _T("View Edit History")}, + {1856, kcSampleQuickFade, _T("Quick Fade")}, + {1857, kcSampleXFade, _T("Crossfade Sample Loop")}, + {1858, kcSelectBeat, _T("Select Beat")}, + {1859, kcSelectMeasure, _T("Select Measure")}, + {1860, kcFileSaveTemplate, _T("File/Save As Template")}, + {1861, kcIncreaseSpacing, _T("Increase Edit Step")}, + {1862, kcDecreaseSpacing, _T("Decrease Edit Step")}, + {1863, kcSampleAutotune, _T("Tune Sample to given Note")}, + {1864, kcFileCloseAll, _T("File/Close All")}, + {KeyCommand::Hidden, kcSetOctaveStop0, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop1, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop2, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop3, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop4, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop5, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop6, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop7, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop8, _T("")}, + {KeyCommand::Hidden, kcSetOctaveStop9, _T("")}, + {1875, kcOrderlistLockPlayback, _T("Lock Playback to Selection")}, + {1876, kcOrderlistUnlockPlayback, _T("Unlock Playback")}, + {1877, kcChannelSettings, _T("Quick Channel Settings")}, + {1878, kcChnSettingsPrev, _T("Previous Channel")}, + {1879, kcChnSettingsNext, _T("Next Channel")}, + {1880, kcChnSettingsClose, _T("Switch to Pattern Editor")}, + {1881, kcTransposeCustom, _T("Transpose Custom")}, + {1882, kcSampleZoomSelection, _T("Zoom into Selection")}, + {1883, kcChannelRecordSelect, _T("Channel Record Select")}, + {1884, kcChannelSplitRecordSelect, _T("Channel Split Record Select")}, + {1885, kcDataEntryUp, _T("Data Entry +1")}, + {1886, kcDataEntryDown, _T("Data Entry -1")}, + {1887, kcSample8Bit, _T("Convert to 8-bit / 16-bit")}, + {1888, kcSampleMonoMix, _T("Convert to Mono (Mix)")}, + {1889, kcSampleMonoLeft, _T("Convert to Mono (Left Channel)")}, + {1890, kcSampleMonoRight, _T("Convert to Mono (Right Channel)")}, + {1891, kcSampleMonoSplit, _T("Convert to Mono (Split Sample)")}, + {1892, kcQuantizeSettings, _T("Quantize Settings")}, + {1893, kcDataEntryUpCoarse, _T("Data Entry Up (Coarse)")}, + {1894, kcDataEntryDownCoarse, _T("Data Entry Down (Coarse)")}, + {1895, kcToggleNoteOffRecordPC, _T("Toggle Note Off record (PC keyboard)")}, + {1896, kcToggleNoteOffRecordMIDI, _T("Toggle Note Off record (MIDI)")}, + {1897, kcFindInstrument, _T("Pick up nearest instrument number")}, + {1898, kcPlaySongFromPattern, _T("Play Song from Pattern Start")}, + {1899, kcVSTGUIToggleRecordMIDIOut, _T("Record MIDI Out to Pattern Editor")}, + {1900, kcToggleClipboardManager, _T("Toggle Clipboard Manager")}, + {1901, kcClipboardPrev, _T("Cycle to Previous Clipboard")}, + {1902, kcClipboardNext, _T("Cycle to Next Clipboard")}, + {1903, kcSelectRow, _T("Select Row")}, + {1904, kcSelectEvent, _T("Select Event")}, + {1905, kcEditRedo, _T("Redo")}, + {1906, kcFileAppend, _T("File/Append Module")}, + {1907, kcSampleTransposeUp, _T("Transpose +1")}, + {1908, kcSampleTransposeDown, _T("Transpose -1")}, + {1909, kcSampleTransposeOctUp, _T("Transpose +1 Octave")}, + {1910, kcSampleTransposeOctDown, _T("Transpose -1 Octave")}, + {1911, kcPatternInterpolateInstr, _T("Interpolate Instrument")}, + {1912, kcDummyShortcut, _T("Dummy Shortcut")}, + {1913, kcSampleUpsample, _T("Upsample")}, + {1914, kcSampleDownsample, _T("Downsample")}, + {1915, kcSampleResample, _T("Resample")}, + {1916, kcSampleCenterLoopStart, _T("Center loop start in view")}, + {1917, kcSampleCenterLoopEnd, _T("Center loop end in view")}, + {1918, kcSampleCenterSustainStart, _T("Center sustain loop start in view")}, + {1919, kcSampleCenterSustainEnd, _T("Center sustain loop end in view")}, + {1920, kcInstrumentEnvelopeLoad, _T("Load Envelope")}, + {1921, kcInstrumentEnvelopeSave, _T("Save Envelope")}, + {1922, kcChannelTranspose, _T("Transpose Channel")}, + {1923, kcChannelDuplicate, _T("Duplicate Channel")}, + // Reserved range 1924...1949 for kcStartSampleCues...kcEndSampleCues (generated below) + {1950, kcOrderlistEditCopyOrders, _T("Copy Orders")}, + {KeyCommand::Hidden, kcTreeViewStopPreview, _T("Stop sample preview")}, + {1952, kcSampleDuplicate, _T("Duplicate Sample")}, + {1953, kcSampleSlice, _T("Slice at cue points")}, + {1954, kcInstrumentEnvelopeScale, _T("Scale Envelope Points")}, + {1955, kcInsNoteMapRemove, _T("Remove All Samples")}, + {1956, kcInstrumentEnvelopeSelectLoopStart, _T("Select Envelope Loop Start")}, + {1957, kcInstrumentEnvelopeSelectLoopEnd, _T("Select Envelope Loop End")}, + {1958, kcInstrumentEnvelopeSelectSustainStart, _T("Select Envelope Sustain Start")}, + {1959, kcInstrumentEnvelopeSelectSustainEnd, _T("Select Envelope Sustain End")}, + {1960, kcInstrumentEnvelopePointMoveLeftCoarse, _T("Move envelope point left (Coarse)")}, + {1961, kcInstrumentEnvelopePointMoveRightCoarse, _T("Move envelope point right (Coarse)")}, + {1962, kcSampleCenterSampleStart, _T("Zoom into sample start")}, + {1963, kcSampleCenterSampleEnd, _T("Zoom into sample end")}, + {1964, kcSampleTrimToLoopEnd, _T("Trim to loop end")}, + {1965, kcLockPlaybackToRows, _T("Lock Playback to Rows")}, + {1966, kcSwitchToInstrLibrary, _T("Switch To Instrument Library")}, + {1967, kcPatternSetInstrumentNotEmpty, _T("Apply current instrument to existing only")}, + {1968, kcSelectColumn, _T("Select Column")}, + {1969, kcSampleStereoSep, _T("Change Stereo Separation")}, + {1970, kcTransposeCustomQuick, _T("Transpose Custom (Quick)")}, + {1971, kcPrevEntryInColumn, _T("Jump to previous entry in column")}, + {1972, kcNextEntryInColumn, _T("Jump to next entry in column")}, + {1973, kcViewTempoSwing, _T("View Global Tempo Swing Settings")}, + {1974, kcChordEditor, _T("Show Chord Editor")}, + {1975, kcToggleLoopSong, _T("Toggle Loop Song")}, + {1976, kcInstrumentEnvelopeSwitchToVolume, _T("Switch to Volume Envelope")}, + {1977, kcInstrumentEnvelopeSwitchToPanning, _T("Switch to Panning Envelope")}, + {1978, kcInstrumentEnvelopeSwitchToPitch, _T("Switch to Pitch / Filter Envelope")}, + {1979, kcInstrumentEnvelopeToggleVolume, _T("Toggle Volume Envelope")}, + {1980, kcInstrumentEnvelopeTogglePanning, _T("Toggle Panning Envelope")}, + {1981, kcInstrumentEnvelopeTogglePitch, _T("Toggle Pitch Envelope")}, + {1982, kcInstrumentEnvelopeToggleFilter, _T("Toggle Filter Envelope")}, + {1983, kcInstrumentEnvelopeToggleLoop, _T("Toggle Envelope Loop")}, + {1984, kcInstrumentEnvelopeToggleSustain, _T("Toggle Envelope Sustain Loop")}, + {1985, kcInstrumentEnvelopeToggleCarry, _T("Toggle Envelope Carry")}, + {1986, kcSampleInitializeOPL, _T("Initialize OPL Instrument")}, + {1987, kcFileSaveCopy, _T("File/Save Copy")}, + {1988, kcMergePatterns, _T("Merge Patterns")}, + {1989, kcSplitPattern, _T("Split Pattern")}, + {1990, kcSampleToggleDrawing, _T("Toggle Sample Drawing")}, + {1991, kcSampleResize, _T("Add Silence / Create Sample")}, + {1992, kcSampleGrid, _T("Configure Sample Grid")}, + {1993, kcLoseSelection, _T("Lose Selection")}, + {1994, kcCutPatternChannel, _T("Cut to Pattern Channel Clipboard")}, + {1995, kcCutPattern, _T("Cut to Pattern Clipboard")}, + {1996, kcCopyPatternChannel, _T("Copy to Pattern Channel Clipboard")}, + {1997, kcCopyPattern, _T("Copy to Pattern Clipboard")}, + {1998, kcPastePatternChannel, _T("Paste from Pattern Channel Clipboard")}, + {1999, kcPastePattern, _T("Paste from Pattern Clipboard")}, + {2000, kcToggleSmpInsList, _T("Toggle between lists")}, + {2001, kcExecuteSmpInsListItem, _T("Open item in editor")}, + {2002, kcDeleteRowGlobal, _T("Delete Row(s) (Global)")}, + {2003, kcDeleteWholeRowGlobal, _T("Delete Row(s) (All Channels, Global)")}, + {2004, kcInsertRowGlobal, _T("Insert Row(s) (Global)")}, + {2005, kcInsertWholeRowGlobal, _T("Insert Row(s) (All Channels, Global)")}, + {2006, kcPrevSequence, _T("Previous Sequence")}, + {2007, kcNextSequence, _T("Next Sequence")}, + {2008, kcChnColorFromPrev , _T("Pick Color from Previous Channel")}, + {2009, kcChnColorFromNext , _T("Pick Color from Next Channel") }, + {2010, kcChannelMoveLeft, _T("Move Channels to Left")}, + {2011, kcChannelMoveRight, _T("Move Channels to Right")}, + {2012, kcSampleConvertPingPongLoop, _T("Convert Ping-Pong Loop to Unidirectional") }, + {2013, kcSampleConvertPingPongSustain, _T("Convert Ping-Pong Sustain Loop to Unidirectional") }, + {2014, kcChannelAddBefore, _T("Add Channel Before Current")}, + {2015, kcChannelAddAfter, _T("Add Channel After Current") }, + {2016, kcChannelRemove, _T("Remove Channel") }, + {2017, kcSetFXFinetune, _T("Finetune") }, + {2018, kcSetFXFinetuneSmooth, _T("Finetune (Smooth)")}, + {2019, kcOrderlistEditInsertSeparator, _T("Insert Separator") }, + {2020, kcTempoIncrease, _T("Increase Tempo")}, + {2021, kcTempoDecrease, _T("Decrease Tempo")}, + {2022, kcTempoIncreaseFine, _T("Increase Tempo (Fine)")}, + {2023, kcTempoDecreaseFine, _T("Decrease Tempo (Fine)")}, + {2024, kcSpeedIncrease, _T("Increase Ticks per Row")}, + {2025, kcSpeedDecrease, _T("Decrease Ticks per Row")}, + {2026, kcRenameSmpInsListItem, _T("Rename Item")}, + {2027, kcShowChannelCtxMenu, _T("Show Channel Context (Right-Click) Menu")}, + {2028, kcShowChannelPluginCtxMenu, _T("Show Channel Plugin Context (Right-Click) Menu")}, + {2029, kcViewToggle, _T("Toggle Between Upper / Lower View") }, + {2030, kcFileSaveOPL, _T("File/Export OPL Register Dump") }, + {2031, kcSampleLoadRaw, _T("Load Raw Sample")}, + {2032, kcTogglePatternPlayRow, _T("Toggle row playback when navigating")}, + {2033, kcInsNoteMapTransposeSamples, _T("Transpose Samples / Reset Map") }, + {KeyCommand::Hidden, kcPrevEntryInColumnSelect, _T("kcPrevEntryInColumnSelect")}, + {KeyCommand::Hidden, kcNextEntryInColumnSelect, _T("kcNextEntryInColumnSelect")}, +}; + +// Get command descriptions etc.. loaded up. +void CCommandSet::SetupCommands() +{ + for(const auto &def : CommandDefinitions) + { + m_commands[def.cmd] = {def.uid, def.description}; + } + + for(int j = kcStartSampleCues; j <= kcEndSampleCues; j++) + { + CString s = MPT_CFORMAT("Preview Sample Cue {}")(j - kcStartSampleCues + 1); + m_commands[j] = {static_cast<uint32>(1924 + j - kcStartSampleCues), s}; + } + static_assert(1924 + kcEndSampleCues - kcStartSampleCues < 1950); + + // Automatically generated note entry keys in non-pattern contexts + for(const auto ctx : NoteContexts) + { + const auto contextStartNotes = std::get<1>(ctx); + const auto contextStopNotes = std::get<2>(ctx); + if(contextStartNotes == kcVPStartNotes) + continue; + + for(int i = kcVPStartNotes; i <= kcVPEndNotes; i++) + { + m_commands[i - kcVPStartNotes + contextStartNotes] = {KeyCommand::Hidden, m_commands[i].Message}; + } + for(int i = kcVPStartNoteStops; i <= kcVPEndNoteStops; i++) + { + m_commands[i - kcVPStartNoteStops + contextStopNotes] = {KeyCommand::Hidden, m_commands[i].Message}; + } + } + +#ifdef MPT_BUILD_DEBUG + // Ensure that every visible command has a unique ID + for(size_t i = 0; i < kcNumCommands; i++) + { + if(m_commands[i].ID() != 0 || !m_commands[i].IsHidden()) + { + for(size_t j = i + 1; j < kcNumCommands; j++) + { + if(m_commands[i].ID() == m_commands[j].ID()) + { + LOG_COMMANDSET(MPT_UFORMAT("Duplicate or unset command UID: {}\n")(m_commands[i].ID())); + MPT_ASSERT_NOTREACHED(); + } + } + } + } +#endif // MPT_BUILD_DEBUG +} + + +// Command Manipulation + + +CString CCommandSet::Add(KeyCombination kc, CommandID cmd, bool overwrite, int pos, bool checkEventConflict) +{ + auto &kcList = m_commands[cmd].kcList; + + // Avoid duplicate + if(mpt::contains(kcList, kc)) + { + return CString(); + } + + // Check that this keycombination isn't already assigned (in this context), except for dummy keys + CString report; + if(auto conflictCmd = IsConflicting(kc, cmd, checkEventConflict); conflictCmd.first != kcNull) + { + if (!overwrite) + { + return CString(); + } else + { + if (IsCrossContextConflict(kc, conflictCmd.second)) + { + report += _T("The following commands may conflict:\r\n >") + GetCommandText(conflictCmd.first) + _T(" in ") + conflictCmd.second.GetContextText() + _T("\r\n >") + GetCommandText(cmd) + _T(" in ") + kc.GetContextText() + _T("\r\n\r\n"); + LOG_COMMANDSET(mpt::ToUnicode(report)); + } else + { + //if(!TrackerSettings::Instance().MiscAllowMultipleCommandsPerKey) + // Remove(conflictCmd.second, conflictCmd.first); + report += _T("The following commands in same context share the same key combination:\r\n >") + GetCommandText(conflictCmd.first) + _T(" in ") + conflictCmd.second.GetContextText() + _T("\r\n\r\n"); + LOG_COMMANDSET(mpt::ToUnicode(report)); + } + } + } + + kcList.insert((pos < 0) ? kcList.end() : (kcList.begin() + pos), kc); + + //enfore rules on CommandSet + report += EnforceAll(kc, cmd, true); + return report; +} + + +std::pair<CommandID, KeyCombination> CCommandSet::IsConflicting(KeyCombination kc, CommandID cmd, bool checkEventConflict) const +{ + if(m_commands[cmd].IsDummy()) // no need to search if we are adding a dummy key + return {kcNull, KeyCombination()}; + + for(int pass = 0; pass < 2; pass++) + { + // In the first pass, only look for conflicts in the same context, since + // such conflicts are errors. Cross-context conflicts only emit warnings. + for(int curCmd = 0; curCmd < kcNumCommands; curCmd++) + { + if(m_commands[curCmd].IsDummy()) + continue; + + for(auto &curKc : m_commands[curCmd].kcList) + { + if(pass == 0 && curKc.Context() != kc.Context()) + continue; + + if(KeyCombinationConflict(curKc, kc, checkEventConflict)) + { + return {(CommandID)curCmd, curKc}; + } + } + } + } + + return std::make_pair(kcNull, KeyCombination()); +} + + +CString CCommandSet::Remove(int pos, CommandID cmd) +{ + if (pos>=0 && (size_t)pos<m_commands[cmd].kcList.size()) + { + return Remove(m_commands[cmd].kcList[pos], cmd); + } + + LOG_COMMANDSET(U_("Failed to remove a key: keychoice out of range.")); + return _T(""); +} + + +CString CCommandSet::Remove(KeyCombination kc, CommandID cmd) +{ + auto &kcList = m_commands[cmd].kcList; + auto index = std::find(kcList.begin(), kcList.end(), kc); + if (index != kcList.end()) + { + kcList.erase(index); + LOG_COMMANDSET(U_("Removed a key")); + return EnforceAll(kc, cmd, false); + } else + { + LOG_COMMANDSET(U_("Failed to remove a key as it was not found")); + return CString(); + } + +} + + +CString CCommandSet::EnforceAll(KeyCombination inKc, CommandID inCmd, bool adding) +{ + //World's biggest, most confusing method. :) + //Needs refactoring. Maybe make lots of Rule subclasses, each with their own Enforce() method? + KeyCombination curKc; // for looping through key combinations + KeyCombination newKc; // for adding new key combinations + CString report; + + if(m_enforceRule[krAllowNavigationWithSelection]) + { + // When we get a new navigation command key, we need to + // make sure this navigation will work when any selection key is pressed + if(inCmd >= kcStartPatNavigation && inCmd <= kcEndPatNavigation) + {//Check that it is a nav cmd + CommandID cmdNavSelection = (CommandID)(kcStartPatNavigationSelect + (inCmd-kcStartPatNavigation)); + for(auto &kc :m_commands[kcSelect].kcList) + {//for all selection modifiers + newKc = inKc; + newKc.Modifier(kc); //Add selection modifier's modifiers to this command + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - adding key with modifier:{} to command: {}")(newKc.Modifier().GetRaw(), cmdNavSelection)); + Add(newKc, cmdNavSelection, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - removing key with modifier:{} to command: {}")(newKc.Modifier().GetRaw(), cmdNavSelection)); + Remove(newKc, cmdNavSelection); + } + } + } + // Same applies for orderlist navigation + else if(inCmd >= kcStartOrderlistNavigation && inCmd <= kcEndOrderlistNavigation) + {//Check that it is a nav cmd + CommandID cmdNavSelection = (CommandID)(kcStartOrderlistNavigationSelect+ (inCmd-kcStartOrderlistNavigation)); + for(auto &kc : m_commands[kcSelect].kcList) + {//for all selection modifiers + newKc = inKc; + newKc.AddModifier(kc); //Add selection modifier's modifiers to this command + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - adding key with modifier:{} to command: {}")(newKc.Modifier().GetRaw(), cmdNavSelection)); + Add(newKc, cmdNavSelection, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - removing key with modifier:{} to command: {}")(newKc.Modifier().GetRaw(), cmdNavSelection)); + Remove(newKc, cmdNavSelection); + } + } + } + // When we get a new selection key, we need to make sure that + // all navigation commands will work with this selection key pressed + else if(inCmd == kcSelect) + { + // check that is is a selection + for(int curCmd=kcStartPatNavigation; curCmd<=kcEndPatNavigation; curCmd++) + { + // for all nav commands + for(auto &kc : m_commands[curCmd].kcList) + { + // for all keys for this command + CommandID cmdNavSelection = (CommandID)(kcStartPatNavigationSelect + (curCmd-kcStartPatNavigation)); + newKc = kc; // get all properties from the current nav cmd key + newKc.AddModifier(inKc); // and the new selection modifier + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - adding key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), cmdNavSelection)); + Add(newKc, cmdNavSelection, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - removing key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), cmdNavSelection)); + Remove(newKc, cmdNavSelection); + } + } + } // end all nav commands + for(int curCmd = kcStartOrderlistNavigation; curCmd <= kcEndOrderlistNavigation; curCmd++) + {// for all nav commands + for(auto &kc : m_commands[curCmd].kcList) + {// for all keys for this command + CommandID cmdNavSelection = (CommandID)(kcStartOrderlistNavigationSelect+ (curCmd-kcStartOrderlistNavigation)); + newKc = kc; // get all properties from the current nav cmd key + newKc.AddModifier(inKc); // and the new selection modifier + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - adding key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), cmdNavSelection)); + Add(newKc, cmdNavSelection, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowNavigationWithSelection - removing key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), cmdNavSelection)); + Remove(newKc, cmdNavSelection); + } + } + } // end all nav commands + } + } // end krAllowNavigationWithSelection + + if(m_enforceRule[krAllowSelectionWithNavigation]) + { + KeyCombination newKcSel; + + // When we get a new navigation command key, we need to ensure + // all selection keys will work even when this new selection key is pressed + if(inCmd >= kcStartPatNavigation && inCmd <= kcEndPatNavigation) + {//if this is a navigation command + for(auto &kc : m_commands[kcSelect].kcList) + {//for all deselection modifiers + newKcSel = kc; // get all properties from the selection key + newKcSel.AddModifier(inKc); // add modifiers from the new nav command + if(adding) + { + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectionWithNavigation: adding removing kcSelectWithNav and kcSelectOffWithNav")); + Add(newKcSel, kcSelectWithNav, false); + } else + { + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectionWithNavigation: removing kcSelectWithNav and kcSelectOffWithNav")); + Remove(newKcSel, kcSelectWithNav); + } + } + } + // Same for orderlist navigation + if(inCmd >= kcStartOrderlistNavigation && inCmd <= kcEndOrderlistNavigation) + {//if this is a navigation command + for(auto &kc : m_commands[kcSelect].kcList) + {//for all deselection modifiers + newKcSel = kc; // get all properties from the selection key + newKcSel.AddModifier(inKc); // add modifiers from the new nav command + if(adding) + { + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectionWithNavigation: adding removing kcSelectWithNav and kcSelectOffWithNav")); + Add(newKcSel, kcSelectWithNav, false); + } else + { + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectionWithNavigation: removing kcSelectWithNav and kcSelectOffWithNav")); + Remove(newKcSel, kcSelectWithNav); + } + } + } + // When we get a new selection key, we need to ensure it will work even when + // any navigation key is pressed + else if(inCmd == kcSelect) + { + for(int curCmd = kcStartPatNavigation; curCmd <= kcEndPatNavigation; curCmd++) + {//for all nav commands + for(auto &kc : m_commands[curCmd].kcList) + {// for all keys for this command + newKcSel = inKc; // get all properties from the selection key + newKcSel.AddModifier(kc); //add the nav keys' modifiers + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowSelectionWithNavigation - adding key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), kcSelectWithNav)); + Add(newKcSel, kcSelectWithNav, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowSelectionWithNavigation - removing key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), kcSelectWithNav)); + Remove(newKcSel, kcSelectWithNav); + } + } + } // end all nav commands + + for(int curCmd = kcStartOrderlistNavigation; curCmd <= kcEndOrderlistNavigation; curCmd++) + {//for all nav commands + for(auto &kc : m_commands[curCmd].kcList) + {// for all keys for this command + newKcSel=inKc; // get all properties from the selection key + newKcSel.AddModifier(kc); //add the nav keys' modifiers + if(adding) + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowSelectionWithNavigation - adding key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), kcSelectWithNav)); + Add(newKcSel, kcSelectWithNav, false); + } else + { + LOG_COMMANDSET(MPT_UFORMAT("Enforcing rule krAllowSelectionWithNavigation - removing key:{} with modifier:{} to command: {}")(curCmd, inKc.Modifier().GetRaw(), kcSelectWithNav)); + Remove(newKcSel, kcSelectWithNav); + } + } + } // end all nav commands + } + + } + + // if we add a selector or a copy selector, we need it to switch off when we release the key. + if (m_enforceRule[krAutoSelectOff]) + { + KeyCombination newKcDeSel; + CommandID cmdOff = kcNull; + switch (inCmd) + { + case kcSelect: cmdOff = kcSelectOff; break; + case kcSelectWithNav: cmdOff = kcSelectOffWithNav; break; + case kcCopySelect: cmdOff = kcCopySelectOff; break; + case kcCopySelectWithNav: cmdOff = kcCopySelectOffWithNav; break; + case kcSelectWithCopySelect: cmdOff = kcSelectOffWithCopySelect; break; + case kcCopySelectWithSelect: cmdOff = kcCopySelectOffWithSelect; break; + } + + if(cmdOff != kcNull) + { + newKcDeSel = inKc; + newKcDeSel.EventType(kKeyEventUp); + + // Register key-up when releasing any of the modifiers. + // Otherwise, select key combos might get stuck. Example: + // [Ctrl Down] [Alt Down] [Ctrl Up] [Alt Up] After this action, copy select (Ctrl+Drag) would still be activated without this code. + const UINT maxMod = TrackerSettings::Instance().MiscDistinguishModifiers ? MaxMod : (MaxMod & ~(HOTKEYF_RSHIFT | HOTKEYF_RCONTROL | HOTKEYF_RALT)); + for(UINT i = 0; i <= maxMod; i++) + { + // Avoid Windows key, so that it won't detected as being actively used + if(i & HOTKEYF_EXT) + continue; + newKcDeSel.Modifier(static_cast<Modifiers>(i)); + //newKcDeSel.mod&=~CodeToModifier(inKc.code); //<-- Need to get rid of right modifier!! + + if (adding) + Add(newKcDeSel, cmdOff, false); + else + Remove(newKcDeSel, cmdOff); + } + } + + } + // Allow combinations of copyselect and select + if(m_enforceRule[krAllowSelectCopySelectCombos]) + { + KeyCombination newKcSel, newKcCopySel; + if(inCmd==kcSelect) + { + // On getting a new selection key, make this selection key work with all copy selects' modifiers + // On getting a new selection key, make all copyselects work with this key's modifiers + for(auto &kc : m_commands[kcCopySelect].kcList) + { + newKcSel=inKc; + newKcSel.AddModifier(kc); + newKcCopySel = kc; + newKcCopySel.AddModifier(inKc); + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectCopySelectCombos")); + if(adding) + { + Add(newKcSel, kcSelectWithCopySelect, false); + Add(newKcCopySel, kcCopySelectWithSelect, false); + } else + { + Remove(newKcSel, kcSelectWithCopySelect); + Remove(newKcCopySel, kcCopySelectWithSelect); + } + } + } + if(inCmd == kcCopySelect) + { + // On getting a new copyselection key, make this copyselection key work with all selects' modifiers + // On getting a new copyselection key, make all selects work with this key's modifiers + for(auto &kc : m_commands[kcSelect].kcList) + { + newKcSel = kc; + newKcSel.AddModifier(inKc); + newKcCopySel = inKc; + newKcCopySel.AddModifier(kc); + LOG_COMMANDSET(U_("Enforcing rule krAllowSelectCopySelectCombos")); + if(adding) + { + Add(newKcSel, kcSelectWithCopySelect, false); + Add(newKcCopySel, kcCopySelectWithSelect, false); + } else + { + Remove(newKcSel, kcSelectWithCopySelect); + Remove(newKcCopySel, kcCopySelectWithSelect); + } + } + } + } + + + // Lock Notes to Chords + if (m_enforceRule[krLockNotesToChords]) + { + if (inCmd>=kcVPStartNotes && inCmd<=kcVPEndNotes) + { + int noteOffset = inCmd - kcVPStartNotes; + for(auto &kc : m_commands[kcChordModifier].kcList) + {//for all chord modifier keys + newKc = inKc; + newKc.AddModifier(kc); + if (adding) + { + LOG_COMMANDSET(U_("Enforcing rule krLockNotesToChords: auto adding in a chord command")); + Add(newKc, (CommandID)(kcVPStartChords+noteOffset), false); + } + else + { + LOG_COMMANDSET(U_("Enforcing rule krLockNotesToChords: auto removing a chord command")); + Remove(newKc, (CommandID)(kcVPStartChords+noteOffset)); + } + } + } + if (inCmd==kcChordModifier) + { + int noteOffset; + for (int curCmd=kcVPStartNotes; curCmd<=kcVPEndNotes; curCmd++) + {//for all notes + for(auto kc : m_commands[curCmd].kcList) + {//for all keys for this note + noteOffset = curCmd - kcVPStartNotes; + kc.AddModifier(inKc); + if (adding) + { + LOG_COMMANDSET(U_("Enforcing rule krLockNotesToChords: auto adding in a chord command")); + Add(kc, (CommandID)(kcVPStartChords+noteOffset), false); + } + else + { + LOG_COMMANDSET(U_("Enforcing rule krLockNotesToChords: auto removing a chord command")); + Remove(kc, (CommandID)(kcVPStartChords+noteOffset)); + } + } + } + } + } + + + // Auto set note off on release + if (m_enforceRule[krNoteOffOnKeyRelease]) + { + if (inCmd>=kcVPStartNotes && inCmd<=kcVPEndNotes) + { + int noteOffset = inCmd - kcVPStartNotes; + newKc=inKc; + newKc.EventType(kKeyEventUp); + if (adding) + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: adding note off command")); + Add(newKc, (CommandID)(kcVPStartNoteStops+noteOffset), false); + } + else + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: removing note off command")); + Remove(newKc, (CommandID)(kcVPStartNoteStops+noteOffset)); + } + } + if (inCmd>=kcVPStartChords && inCmd<=kcVPEndChords) + { + int noteOffset = inCmd - kcVPStartChords; + newKc=inKc; + newKc.EventType(kKeyEventUp); + if (adding) + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: adding Chord off command")); + Add(newKc, (CommandID)(kcVPStartChordStops+noteOffset), false); + } + else + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: removing Chord off command")); + Remove(newKc, (CommandID)(kcVPStartChordStops+noteOffset)); + } + } + if(inCmd >= kcSetOctave0 && inCmd <= kcSetOctave9) + { + int noteOffset = inCmd - kcSetOctave0; + newKc=inKc; + newKc.EventType(kKeyEventUp); + if (adding) + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: adding Chord off command")); + Add(newKc, (CommandID)(kcSetOctaveStop0+noteOffset), false); + } + else + { + LOG_COMMANDSET(U_("Enforcing rule krNoteOffOnKeyRelease: removing Chord off command")); + Remove(newKc, (CommandID)(kcSetOctaveStop0+noteOffset)); + } + } + } + + // Reassign freed number keys to octaves + if (m_enforceRule[krReassignDigitsToOctaves] && !adding) + { + if ( (inKc.Modifier() == ModNone) && //no modifier + ( (inKc.Context() == kCtxViewPatternsNote) || (inKc.Context() == kCtxViewPatterns) ) && //note scope or pattern scope + ( ('0'<=inKc.KeyCode() && inKc.KeyCode()<='9') || (VK_NUMPAD0<=inKc.KeyCode() && inKc.KeyCode()<=VK_NUMPAD9) ) ) //is number key + { + newKc = KeyCombination(kCtxViewPatternsNote, ModNone, inKc.KeyCode(), kKeyEventDown); + int offset = ('0'<=inKc.KeyCode() && inKc.KeyCode()<='9') ? newKc.KeyCode()-'0' : newKc.KeyCode()-VK_NUMPAD0; + Add(newKc, (CommandID)(kcSetOctave0 + (newKc.KeyCode()-offset)), false); + } + } + // Add spacing + if (m_enforceRule[krAutoSpacing]) + { + if (inCmd == kcSetSpacing && adding) + { + newKc = KeyCombination(kCtxViewPatterns, inKc.Modifier(), 0, kKeyEventDown); + for (char i = 0; i <= 9; i++) + { + newKc.KeyCode('0' + i); + Add(newKc, (CommandID)(kcSetSpacing0 + i), false); + newKc.KeyCode(VK_NUMPAD0 + i); + Add(newKc, (CommandID)(kcSetSpacing0 + i), false); + } + } + else if (!adding && (inCmd < kcSetSpacing || inCmd > kcSetSpacing9)) + { + // Re-add combinations that might have been overwritten by another command + if(('0' <= inKc.KeyCode() && inKc.KeyCode() <= '9') || (VK_NUMPAD0 <= inKc.KeyCode() && inKc.KeyCode() <= VK_NUMPAD9)) + { + for(const auto &spacing : m_commands[kcSetSpacing].kcList) + { + newKc = KeyCombination(kCtxViewPatterns, spacing.Modifier(), inKc.KeyCode(), spacing.EventType()); + if('0' <= inKc.KeyCode() && inKc.KeyCode() <= '9') + Add(newKc, (CommandID)(kcSetSpacing0 + inKc.KeyCode() - '0'), false); + else if(VK_NUMPAD0 <= inKc.KeyCode() && inKc.KeyCode() <= VK_NUMPAD9) + Add(newKc, (CommandID)(kcSetSpacing0 + inKc.KeyCode() - VK_NUMPAD0), false); + } + } + + } + } + if (m_enforceRule[krPropagateNotes]) + { + if((inCmd >= kcVPStartNotes && inCmd <= kcVPEndNotes) || (inCmd >= kcVPStartNoteStops && inCmd <= kcVPEndNoteStops)) + { + const bool areNoteStarts = (inCmd >= kcVPStartNotes && inCmd <= kcVPEndNotes); + const auto startNote = areNoteStarts ? kcVPStartNotes : kcVPStartNoteStops; + const auto noteOffset = inCmd - startNote; + for(const auto ctx : NoteContexts) + { + const auto context = std::get<0>(ctx); + const auto contextStartNote = areNoteStarts ? std::get<1>(ctx) : std::get<2>(ctx); + + if(contextStartNote == startNote) + continue; + + newKc = inKc; + newKc.Context(context); + + if(adding) + { + LOG_COMMANDSET(U_("Enforcing rule krPropagateNotes: adding Note on/off")); + Add(newKc, static_cast<CommandID>(contextStartNote + noteOffset), false); + } else + { + LOG_COMMANDSET(U_("Enforcing rule krPropagateNotes: removing Note on/off")); + Remove(newKc, static_cast<CommandID>(contextStartNote + noteOffset)); + } + } + } else if(inCmd == kcNoteCut || inCmd == kcNoteOff || inCmd == kcNoteFade) + { + // Stop preview in instrument browser + KeyCombination newKcTree = inKc; + newKcTree.Context(kCtxViewTree); + if(adding) + { + Add(newKcTree, kcTreeViewStopPreview, false); + } else + { + Remove(newKcTree, kcTreeViewStopPreview); + } + } + } + if (m_enforceRule[krCheckModifiers]) + { + // for all commands that must be modifiers + for (auto curCmd : { kcSelect, kcCopySelect, kcChordModifier, kcSetSpacing }) + { + //for all of this command's key combinations + for (auto &kc : m_commands[curCmd].kcList) + { + if ((!kc.Modifier()) || (kc.KeyCode()!=VK_SHIFT && kc.KeyCode()!=VK_CONTROL && kc.KeyCode()!=VK_MENU && kc.KeyCode()!=0 && + kc.KeyCode()!=VK_LWIN && kc.KeyCode()!=VK_RWIN )) // Feature: use Windows keys as modifier keys + { + report += _T("Error! ") + GetCommandText((CommandID)curCmd) + _T(" must be a modifier (shift/ctrl/alt), but is currently ") + inKc.GetKeyText() + _T("\r\n"); + //replace with dummy + kc.Modifier(ModShift); + kc.KeyCode(0); + kc.EventType(kKeyEventNone); + } + } + + } + } + if (m_enforceRule[krPropagateSampleManipulation]) + { + static constexpr CommandID propagateCmds[] = {kcSampleLoad, kcSampleSave, kcSampleNew}; + static constexpr CommandID translatedCmds[] = {kcInstrumentLoad, kcInstrumentSave, kcInstrumentNew}; + if(const auto propCmd = std::find(std::begin(propagateCmds), std::end(propagateCmds), inCmd); propCmd != std::end(propagateCmds)) + { + //propagate to InstrumentView + const auto newCmd = translatedCmds[std::distance(std::begin(propagateCmds), propCmd)]; + m_commands[newCmd].kcList.reserve(m_commands[inCmd].kcList.size()); + for(auto kc : m_commands[inCmd].kcList) + { + kc.Context(kCtxViewInstruments); + m_commands[newCmd].kcList.push_back(kc); + } + } + + } + +/* if (enforceRule[krFoldEffectColumnAnd]) + { + if (inKc.ctx == kCtxViewPatternsFX) { + KeyCombination newKc = inKc; + newKc.ctx = kCtxViewPatternsFXparam; + if (adding) { + Add(newKc, inCmd, false); + } else { + Remove(newKc, inCmd); + } + } + if (inKc.ctx == kCtxViewPatternsFXparam) { + KeyCombination newKc = inKc; + newKc.ctx = kCtxViewPatternsFX; + if (adding) { + Add(newKc, inCmd, false); + } else { + Remove(newKc, inCmd); + } + } + } +*/ + return report; +} + + +//Generate a keymap from a command set +void CCommandSet::GenKeyMap(KeyMap &km) +{ + std::vector<KeyEventType> eventTypes; + std::vector<InputTargetContext> contexts; + + km.clear(); + + const bool allowDupes = TrackerSettings::Instance().MiscAllowMultipleCommandsPerKey; + + // Copy commandlist content into map: + for(UINT cmd = 0; cmd < kcNumCommands; cmd++) + { + if(m_commands[cmd].IsDummy()) + continue; + + for(auto curKc : m_commands[cmd].kcList) + { + eventTypes.clear(); + contexts.clear(); + + // Handle keyEventType mask. + if(curKc.EventType() & kKeyEventDown) + eventTypes.push_back(kKeyEventDown); + if(curKc.EventType() & kKeyEventUp) + eventTypes.push_back(kKeyEventUp); + if(curKc.EventType() & kKeyEventRepeat) + eventTypes.push_back(kKeyEventRepeat); + //ASSERT(eventTypes.GetSize()>0); + + // Handle super-contexts (contexts that represent a set of sub contexts) + if(curKc.Context() == kCtxViewPatterns) + contexts.insert(contexts.end(), {kCtxViewPatternsNote, kCtxViewPatternsIns, kCtxViewPatternsVol, kCtxViewPatternsFX, kCtxViewPatternsFXparam}); + else if(curKc.Context() == kCtxCtrlPatterns) + contexts.push_back(kCtxCtrlOrderlist); + else + contexts.push_back(curKc.Context()); + + for(auto ctx : contexts) + { + for(auto event : eventTypes) + { + KeyCombination kc(ctx, curKc.Modifier(), curKc.KeyCode(), event); + if(!allowDupes) + { + KeyMapRange dupes = km.equal_range(kc); + km.erase(dupes.first, dupes.second); + } + km.insert(std::make_pair(kc, static_cast<CommandID>(cmd))); + } + } + } + } +} + + +void CCommandSet::Copy(const CCommandSet *source) +{ + m_oldSpecs = nullptr; + std::copy(std::begin(source->m_commands), std::end(source->m_commands), std::begin(m_commands)); +} + + +// Export + +bool CCommandSet::SaveFile(const mpt::PathString &filename) +{ + +/* Layout: +//----( Context1 Text (id) )---- +ctx:UID:Description:Modifier:Key:EventMask +ctx:UID:Description:Modifier:Key:EventMask +... +//----( Context2 Text (id) )---- +... +*/ + + mpt::SafeOutputFile sf(filename, std::ios::out, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream& f = sf; + if(!f) + { + ErrorBox(IDS_CANT_OPEN_FILE_FOR_WRITING); + return false; + } + f << "//----------------- OpenMPT key binding definition file ---------------\n" + "//- Format is: -\n" + "//- Context:Command ID:Modifiers:Key:KeypressEventType //Comments -\n" + "//----------------------------------------------------------------------\n" + "version:" << KEYMAP_VERSION << "\n"; + + std::vector<HKL> layouts(GetKeyboardLayoutList(0, nullptr)); + GetKeyboardLayoutList(static_cast<int>(layouts.size()), layouts.data()); + + for(int ctx = 0; ctx < kCtxMaxInputContexts; ctx++) + { + f << "\n//----( " << mpt::ToCharset(mpt::Charset::UTF8, KeyCombination::GetContextText((InputTargetContext)ctx)) << " )------------\n"; + + for(int cmd = kcFirst; cmd < kcNumCommands; cmd++) + { + if(m_commands[cmd].IsHidden()) + continue; + + for(const auto kc : m_commands[cmd].kcList) + { + if(kc.Context() != ctx) + continue; // Sort by context + + f << ctx << ":" + << m_commands[cmd].ID() << ":" + << static_cast<int>(kc.Modifier().GetRaw()) << ":" + << kc.KeyCode(); + if(cmd >= kcVPStartNotes && cmd <= kcVPEndNotes) + { + UINT sc = 0, vk = kc.KeyCode(); + for(auto i = layouts.begin(); i != layouts.end() && sc == 0; i++) + { + sc = MapVirtualKeyEx(vk, MAPVK_VK_TO_VSC, *i); + } + f << "/" << sc; + } + f << ":" + << static_cast<int>(kc.EventType().GetRaw()) << "\t\t//" + << mpt::ToCharset(mpt::Charset::UTF8, GetCommandText((CommandID)cmd)) << ": " + << mpt::ToCharset(mpt::Charset::UTF8, kc.GetKeyText()) << " (" + << mpt::ToCharset(mpt::Charset::UTF8, kc.GetKeyEventText()) << ")\n"; + } + } + } + + return true; +} + + +static std::string GetDefaultKeymap() +{ + return mpt::make_basic_string(mpt::byte_cast<mpt::span<const char>>(GetResource(MAKEINTRESOURCE(IDR_DEFAULT_KEYBINDINGS), TEXT("KEYBINDINGS")))); +} + + +bool CCommandSet::LoadFile(std::istream &iStrm, const mpt::ustring &filenameDescription, const bool fillExistingSet) +{ + KeyCombination kc; + char s[1024]; + std::string curLine; + std::vector<std::string> tokens; + int l = 0; + m_oldSpecs = nullptr; // After clearing the key set, need to fix effect letters + + if(!fillExistingSet) + { + for(auto &cmd : m_commands) + cmd.kcList.clear(); + } + + CString errText; + int errorCount = 0; + + std::vector<HKL> layouts(GetKeyboardLayoutList(0, nullptr)); + GetKeyboardLayoutList(static_cast<int>(layouts.size()), layouts.data()); + + const std::string whitespace(" \n\r\t"); + while(iStrm.getline(s, std::size(s))) + { + curLine = s; + l++; + + // Cut everything after a //, trim whitespace + auto pos = curLine.find("//"); + if(pos != std::string::npos) + curLine.resize(pos); + pos = curLine.find_first_not_of(whitespace); + if(pos == std::string::npos) + continue; + curLine.erase(0, pos); + pos = curLine.find_last_not_of(whitespace); + if(pos != std::string::npos) + curLine.resize(pos + 1); + + if (curLine.empty()) + continue; + + tokens = mpt::String::Split<std::string>(curLine, ":"); + if(tokens.size() == 2 && !mpt::CompareNoCaseAscii(tokens[0], "version")) + { + // This line indicates the version of this keymap file (e.g. "version:1") + int fileVersion = ConvertStrTo<int>(tokens[1]); + if(fileVersion > KEYMAP_VERSION) + { + errText.AppendFormat(_T("File version is %d, but your version of OpenMPT only supports loading files up to version %d.\n"), fileVersion, KEYMAP_VERSION); + } + continue; + } + + // Format: ctx:UID:Description:Modifier:Key:EventMask + CommandID cmd = kcNumCommands; + if(tokens.size() >= 5) + { + kc.Context(static_cast<InputTargetContext>(ConvertStrTo<int>(tokens[0]))); + cmd = FindCmd(ConvertStrTo<uint32>(tokens[1])); + + // Modifier + kc.Modifier(static_cast<Modifiers>(ConvertStrTo<int>(tokens[2]))); + + // Virtual Key code / Scan code + UINT vk = 0; + auto scPos = tokens[3].find('/'); + if(scPos != std::string::npos) + { + // Scan code present + UINT sc = ConvertStrTo<UINT>(tokens[3].substr(scPos + 1)); + for(auto i = layouts.begin(); i != layouts.end() && vk == 0; i++) + { + vk = MapVirtualKeyEx(sc, MAPVK_VSC_TO_VK, *i); + } + } + if(vk == 0) + { + vk = ConvertStrTo<UINT>(tokens[3]); + } + kc.KeyCode(vk); + + // Event + kc.EventType(static_cast<KeyEventType>(ConvertStrTo<int>(tokens[4]))); + + if(fillExistingSet && cmd != kcNull && GetKeyListSize(cmd) != 0) + { + // Do not map shortcuts that already have custom keys assigned. + // In particular with default note keys, this can create awkward keymaps when loading + // e.g. an IT-style keymap and it contains two keys mapped to the same notes. + continue; + } + } + + // Error checking + if(cmd < 0 || cmd >= kcNumCommands || kc.Context() >= kCtxMaxInputContexts || tokens.size() < 4) + { + errorCount++; + if (errorCount < 10) + { + if(tokens.size() < 4) + errText.AppendFormat(_T("Line %d was not understood.\n"), l); + else + errText.AppendFormat(_T("Line %d contained an unknown command.\n"), l); + } else if (errorCount == 10) + { + errText += _T("Too many errors detected, not reporting any more.\n"); + } + } else + { + Add(kc, cmd, !fillExistingSet, -1, !fillExistingSet); + } + } + + if(!fillExistingSet) + { + // Add the default command set to our freshly loaded command set. + std::istringstream ss{ GetDefaultKeymap() }; + LoadFile(ss, mpt::ustring(), true); + } else + { + // We were just adding stuff to an existing command set - don't delete it! + return true; + } + + // Fix up old keymaps containing legacy commands that have been merged into other commands + static constexpr std::pair<CommandID, CommandID> MergeCommands[] = + { + {kcFileSaveAsMP3, kcFileSaveAsWave}, + {kcNoteCutOld, kcNoteCut}, + {kcNoteOffOld, kcNoteOff}, + {kcNoteFadeOld, kcNoteFade}, + }; + for(const auto [from, to] : MergeCommands) + { + m_commands[to].kcList.insert(m_commands[to].kcList.end(), m_commands[from].kcList.begin(), m_commands[from].kcList.end()); + m_commands[from].kcList.clear(); + } + + if(!errText.IsEmpty()) + { + Reporting::Warning(MPT_CFORMAT("The following problems have been encountered while trying to load the key binding file {}:\n{}") + (mpt::ToCString(filenameDescription), errText)); + } + + m_oldSpecs = nullptr; + return true; +} + + +bool CCommandSet::LoadFile(const mpt::PathString &filename) +{ + mpt::ifstream fin(filename); + if(fin.fail()) + { + Reporting::Warning(MPT_TFORMAT("Can't open key bindings file {} for reading. Default key bindings will be used.")(filename)); + return false; + } else + { + return LoadFile(fin, filename.ToUnicode()); + } +} + + +bool CCommandSet::LoadDefaultKeymap() +{ + std::istringstream ss{ GetDefaultKeymap() }; + return LoadFile(ss, U_("\"executable resource\"")); +} + + +CommandID CCommandSet::FindCmd(uint32 uid) const +{ + for(int i = 0; i < kcNumCommands; i++) + { + if(m_commands[i].ID() == uid) + return static_cast<CommandID>(i); + } + return kcNull; +} + + +CString KeyCombination::GetContextText(InputTargetContext ctx) +{ + switch(ctx) + { + case kCtxAllContexts: return _T("Global Context"); + case kCtxViewGeneral: return _T("General Context [bottom]"); + case kCtxViewPatterns: return _T("Pattern Context [bottom]"); + case kCtxViewPatternsNote: return _T("Pattern Context [bottom] - Note Col"); + case kCtxViewPatternsIns: return _T("Pattern Context [bottom] - Ins Col"); + case kCtxViewPatternsVol: return _T("Pattern Context [bottom] - Vol Col"); + case kCtxViewPatternsFX: return _T("Pattern Context [bottom] - FX Col"); + case kCtxViewPatternsFXparam: return _T("Pattern Context [bottom] - Param Col"); + case kCtxViewSamples: return _T("Sample Context [bottom]"); + case kCtxViewInstruments: return _T("Instrument Context [bottom]"); + case kCtxViewComments: return _T("Comments Context [bottom]"); + case kCtxCtrlGeneral: return _T("General Context [top]"); + case kCtxCtrlPatterns: return _T("Pattern Context [top]"); + case kCtxCtrlSamples: return _T("Sample Context [top]"); + case kCtxCtrlInstruments: return _T("Instrument Context [top]"); + case kCtxCtrlComments: return _T("Comments Context [top]"); + case kCtxCtrlOrderlist: return _T("Orderlist"); + case kCtxVSTGUI: return _T("Plugin GUI Context"); + case kCtxChannelSettings: return _T("Quick Channel Settings Context"); + case kCtxUnknownContext: + default: return _T("Unknown Context"); + } +} + + +CString KeyCombination::GetKeyEventText(FlagSet<KeyEventType> event) +{ + CString text; + + bool first = true; + if (event & kKeyEventDown) + { + first=false; + text.Append(_T("KeyDown")); + } + if (event & kKeyEventRepeat) + { + if (!first) text.Append(_T("|")); + text.Append(_T("KeyHold")); + first=false; + } + if (event & kKeyEventUp) + { + if (!first) text.Append(_T("|")); + text.Append(_T("KeyUp")); + } + + return text; +} + + +CString KeyCombination::GetModifierText(FlagSet<Modifiers> mod) +{ + CString text; + if (mod[ModShift]) text.Append(_T("Shift+")); + if (mod[ModCtrl]) text.Append(_T("Ctrl+")); + if (mod[ModAlt]) text.Append(_T("Alt+")); + if (mod[ModRShift]) text.Append(_T("RShift+")); + if (mod[ModRCtrl]) text.Append(_T("RCtrl+")); + if (mod[ModRAlt]) text.Append(_T("RAlt+")); + if (mod[ModWin]) text.Append(_T("Win+")); // Feature: use Windows keys as modifier keys + if (mod[ModMidi]) text.Append(_T("MIDI")); + return text; +} + + +CString KeyCombination::GetKeyText(FlagSet<Modifiers> mod, UINT code) +{ + CString keyText = GetModifierText(mod); + if(mod[ModMidi]) + { + if(code < 0x80) + keyText.AppendFormat(_T(" CC %u"), code); + else + keyText += MPT_CFORMAT(" {}{}")(mpt::ustring(NoteNamesSharp[(code & 0x7F) % 12]), (code & 0x7F) / 12); + } else + { + keyText.Append(CHotKeyCtrl::GetKeyName(code, IsExtended(code))); + } + //HACK: + if (keyText == _T("Ctrl+CTRL")) keyText = _T("Ctrl"); + else if (keyText == _T("Alt+ALT")) keyText = _T("Alt"); + else if (keyText == _T("Shift+SHIFT")) keyText = _T("Shift"); + else if (keyText == _T("RCtrl+CTRL")) keyText = _T("RCtrl"); + else if (keyText == _T("RAlt+ALT")) keyText = _T("RAlt"); + else if (keyText == _T("RShift+SHIFT")) keyText = _T("RShift"); + + return keyText; +} + + +CString CCommandSet::GetKeyTextFromCommand(CommandID c, UINT key) const +{ + if (key < m_commands[c].kcList.size()) + return m_commands[c].kcList[0].GetKeyText(); + else + return CString(); +} + + +// Quick Changes - modify many commands with one call. + +bool CCommandSet::QuickChange_NotesRepeat(bool repeat) +{ + for (CommandID cmd = kcVPStartNotes; cmd <= kcVPEndNotes; cmd=(CommandID)(cmd + 1)) //for all notes + { + for(auto &kc : m_commands[cmd].kcList) + { + if(repeat) + kc.EventType(kc.EventType() | kKeyEventRepeat); + else + kc.EventType(kc.EventType() & ~kKeyEventRepeat); + } + } + return true; +} + + +bool CCommandSet::QuickChange_SetEffects(const CModSpecifications &modSpecs) +{ + // Is this already the active key configuration? + if(&modSpecs == m_oldSpecs) + { + return false; + } + m_oldSpecs = &modSpecs; + + int choices = 0; + KeyCombination kc(kCtxViewPatternsFX, ModNone, 0, kKeyEventDown | kKeyEventRepeat); + + for(CommandID cmd = kcFixedFXStart; cmd <= kcFixedFXend; cmd = static_cast<CommandID>(cmd + 1)) + { + // Remove all old choices + choices = GetKeyListSize(cmd); + for(int p = choices; p >= 0; --p) + { + Remove(p, cmd); + } + + char effect = modSpecs.GetEffectLetter(static_cast<ModCommand::COMMAND>(cmd - kcSetFXStart + 1)); + if(effect >= 'A' && effect <= 'Z') + { + // VkKeyScanEx needs lowercase letters + effect = effect - 'A' + 'a'; + } else if(effect < '0' || effect > '9') + { + // Don't map effects that use "weird" effect letters (such as # or \) + effect = '?'; + } + + if(effect != '?') + { + // Hack for situations where a non-latin keyboard layout without A...Z key code mapping may the current layout (e.g. Russian), + // but a latin layout (e.g. EN-US) is installed as well. + std::vector<HKL> layouts(GetKeyboardLayoutList(0, nullptr)); + GetKeyboardLayoutList(static_cast<int>(layouts.size()), layouts.data()); + SHORT codeNmod = -1; + for(auto i = layouts.begin(); i != layouts.end() && codeNmod == -1; i++) + { + codeNmod = VkKeyScanEx(effect, *i); + } + if(codeNmod != -1) + { + kc.KeyCode(LOBYTE(codeNmod)); + // Don't add modifier keys, since on French keyboards, numbers are input using Shift. + // We don't really want that behaviour here, and I'm sure we don't want that in other cases on other layouts as well. + kc.Modifier(ModNone); + Add(kc, cmd, true); + } + + if (effect >= '0' && effect <= '9') // For numbers, ensure numpad works too + { + kc.KeyCode(VK_NUMPAD0 + (effect - '0')); + Add(kc, cmd, true); + } + } + } + + return true; +} + +// Stupid MFC crap: for some reason VK code isn't enough to get correct string with GetKeyName. +// We also need to figure out the correct "extended" bit. +bool KeyCombination::IsExtended(UINT code) +{ + if (code==VK_SNAPSHOT) //print screen + return true; + if (code>=VK_PRIOR && code<=VK_DOWN) //pgup, pg down, home, end, cursor keys, + return true; + if (code>=VK_INSERT && code<=VK_DELETE) // ins, del + return true; + if (code>=VK_LWIN && code<=VK_APPS) //winkeys & application key + return true; + if (code==VK_DIVIDE) //Numpad '/' + return true; + if (code==VK_NUMLOCK) //print screen + return true; + if (code>=0xA0 && code<=0xA5) //attempt for RL mods + return true; + + return false; +} + + +void CCommandSet::SetupContextHierarchy() +{ + // For now much be fully expanded (i.e. don't rely on grandparent relationships). + m_isParentContext[kCtxAllContexts].set(kCtxViewGeneral); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatterns); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatternsNote); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatternsIns); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatternsVol); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatternsFX); + m_isParentContext[kCtxAllContexts].set(kCtxViewPatternsFXparam); + m_isParentContext[kCtxAllContexts].set(kCtxViewSamples); + m_isParentContext[kCtxAllContexts].set(kCtxViewInstruments); + m_isParentContext[kCtxAllContexts].set(kCtxViewComments); + m_isParentContext[kCtxAllContexts].set(kCtxViewTree); + m_isParentContext[kCtxAllContexts].set(kCtxInsNoteMap); + m_isParentContext[kCtxAllContexts].set(kCtxVSTGUI); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlGeneral); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlPatterns); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlSamples); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlInstruments); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlComments); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlSamples); + m_isParentContext[kCtxAllContexts].set(kCtxCtrlOrderlist); + m_isParentContext[kCtxAllContexts].set(kCtxChannelSettings); + + m_isParentContext[kCtxViewPatterns].set(kCtxViewPatternsNote); + m_isParentContext[kCtxViewPatterns].set(kCtxViewPatternsIns); + m_isParentContext[kCtxViewPatterns].set(kCtxViewPatternsVol); + m_isParentContext[kCtxViewPatterns].set(kCtxViewPatternsFX); + m_isParentContext[kCtxViewPatterns].set(kCtxViewPatternsFXparam); + m_isParentContext[kCtxCtrlPatterns].set(kCtxCtrlOrderlist); + +} + + +bool CCommandSet::KeyCombinationConflict(KeyCombination kc1, KeyCombination kc2, bool checkEventConflict) const +{ + bool modConflict = (kc1.Modifier()==kc2.Modifier()); + bool codeConflict = (kc1.KeyCode()==kc2.KeyCode()); + bool eventConflict = ((kc1.EventType()&kc2.EventType())); + bool ctxConflict = (kc1.Context() == kc2.Context()); + bool crossCxtConflict = IsCrossContextConflict(kc1, kc2); + + bool conflict = modConflict && codeConflict && (eventConflict || !checkEventConflict) && + (ctxConflict || crossCxtConflict); + + return conflict; +} + + +bool CCommandSet::IsCrossContextConflict(KeyCombination kc1, KeyCombination kc2) const +{ + return m_isParentContext[kc1.Context()][kc2.Context()] || m_isParentContext[kc2.Context()][kc1.Context()]; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.h b/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.h new file mode 100644 index 00000000..794f53f3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/CommandSet.h @@ -0,0 +1,1091 @@ +/* + * CommandSet.h + * ------------ + * Purpose: Header file for custom key handling: List of supported keyboard shortcuts and class for them. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include <string> +#include "openmpt/base/FlagSet.hpp" +#include <map> +#include <bitset> + +OPENMPT_NAMESPACE_BEGIN + +#define HOTKEYF_MIDI 0x10 // modifier mask for MIDI CCs +#define HOTKEYF_RSHIFT 0x20 // modifier mask for right Shift key +#define HOTKEYF_RCONTROL 0x40 // modifier mask for right Ctrl key +#define HOTKEYF_RALT 0x80 // modifier mask for right Alt key + +enum InputTargetContext : int8 +{ + kCtxUnknownContext = -1, + kCtxAllContexts = 0, + + kCtxViewGeneral, + kCtxViewPatterns, + kCtxViewPatternsNote, + kCtxViewPatternsIns, + kCtxViewPatternsVol, + kCtxViewPatternsFX, + kCtxViewPatternsFXparam, + kCtxViewSamples, + kCtxViewInstruments, + kCtxViewComments, + kCtxViewTree, + kCtxInsNoteMap, + kCtxVSTGUI, + + kCtxCtrlGeneral, + kCtxCtrlPatterns, + kCtxCtrlSamples, + kCtxCtrlInstruments, + kCtxCtrlComments, + kCtxCtrlOrderlist, + kCtxChannelSettings, + kCtxMaxInputContexts +}; + +enum KeyEventType : int8 +{ + kKeyEventNone = 0, + kKeyEventDown = 1 << 0, + kKeyEventUp = 1 << 1, + kKeyEventRepeat = 1 << 2, + kNumKeyEvents = 1 << 3 +}; +DECLARE_FLAGSET(KeyEventType) + + +enum CommandID +{ + kcCommandSetNumNotes = 33, // kcVPEndNotes - kcVPStartNotes + + kcNull = -1, + kcFirst, + + //Global + kcGlobalStart = kcFirst, + kcStartFile = kcGlobalStart, + kcFileNew = kcGlobalStart, + kcFileOpen, + kcFileAppend, + kcFileClose, + kcFileCloseAll, + kcFileSave, + kcFileSaveAs, + kcFileSaveCopy, + kcFileSaveTemplate, + kcFileSaveAsWave, + kcFileSaveAsMP3, + kcFileSaveMidi, + kcFileSaveOPL, + kcFileExportCompat, + kcPrevDocument, + kcNextDocument, + kcFileImportMidiLib, + kcFileAddSoundBank, + kcEndFile = kcFileAddSoundBank, + + kcStartPlayCommands, + kcPlayPauseSong = kcStartPlayCommands, + kcPauseSong, + kcStopSong, + kcPlaySongFromStart, + kcPlaySongFromCursor, + kcPlaySongFromPattern, + kcPlayPatternFromStart, + kcPlayPatternFromCursor, + kcToggleLoopSong, + kcPanic, + kcEstimateSongLength, + kcApproxRealBPM, + kcMidiRecord, + kcTempoIncrease, + kcTempoDecrease, + kcTempoIncreaseFine, + kcTempoDecreaseFine, + kcSpeedIncrease, + kcSpeedDecrease, + kcEndPlayCommands = kcSpeedDecrease, + + kcStartEditCommands, + kcEditUndo = kcStartEditCommands, + kcEditRedo, + kcEditCut, + kcEditCopy, + kcEditPaste, + kcEditMixPaste, + kcEditMixPasteITStyle, + kcEditPasteFlood, + kcEditPushForwardPaste, + kcEditSelectAll, + kcEditFind, + kcEditFindNext, + kcEndEditCommands = kcEditFindNext, +/* + kcWindowNew, + kcWindowCascade, + kcWindowTileHorz, + kcWindowTileVert, +*/ + kcStartView, + kcViewGeneral = kcStartView, + kcViewPattern, + kcViewSamples, + kcViewInstruments, + kcViewComments, + kcViewGraph, + kcViewMain, + kcViewTree, + kcViewOptions, + kcViewChannelManager, + kcViewAddPlugin, + kcViewSongProperties, + kcViewTempoSwing, + kcShowMacroConfig, + kcViewMIDImapping, + kcViewEditHistory, + kcViewToggle, + kcSwitchToInstrLibrary, + kcHelp, + kcEndView = kcHelp, + + kcStartMisc, + kcPrevInstrument = kcStartMisc, + kcNextInstrument, + kcPrevOctave, + kcNextOctave, + kcPrevOrder, + kcNextOrder, + kcEndMisc = kcNextOrder, + + kcDummyShortcut, + kcGlobalEnd = kcDummyShortcut, + + //Pattern Navigation + kcStartPatNavigation, + kcStartJumpSnap = kcStartPatNavigation, + kcPatternJumpDownh1 = kcStartJumpSnap, + kcPatternJumpUph1, + kcPatternJumpDownh2, + kcPatternJumpUph2, + kcPatternSnapDownh1, + kcPatternSnapUph1, + kcPatternSnapDownh2, + kcPatternSnapUph2, + kcPrevEntryInColumn, + kcNextEntryInColumn, + kcEndJumpSnap = kcNextEntryInColumn, + kcStartPlainNavigate, + kcNavigateDown = kcStartPlainNavigate, + kcNavigateUp, + kcNavigateDownBySpacing, + kcNavigateUpBySpacing, + kcNavigateLeft, + kcNavigateRight, + kcNavigateNextChan, + kcNavigatePrevChan, + kcEndPlainNavigate = kcNavigatePrevChan, + kcStartHomeEnd, + kcHomeHorizontal = kcStartHomeEnd, + kcHomeVertical, + kcHomeAbsolute, + kcEndHorizontal, + kcEndVertical, + kcEndAbsolute, + kcEndHomeEnd = kcEndAbsolute, + kcEndPatNavigation = kcEndHomeEnd, + + // with select. Order must match above. + kcStartPatNavigationSelect, + kcPatternJumpDownh1Select = kcStartPatNavigationSelect, + kcPatternJumpUph1Select, + kcPatternJumpDownh2Select, + kcPatternJumpUph2Select, + kcPatternSnapDownh1Select, + kcPatternSnapUph1Select, + kcPatternSnapDownh2Select, + kcPatternSnapUph2Select, + kcPrevEntryInColumnSelect, + kcNextEntryInColumnSelect, + kcNavigateDownSelect, + kcNavigateUpSelect, + kcNavigateDownBySpacingSelect, + kcNavigateUpBySpacingSelect, + kcNavigateLeftSelect, + kcNavigateRightSelect, + kcNavigateNextChanSelect, + kcNavigatePrevChanSelect, + kcHomeHorizontalSelect, + kcHomeVerticalSelect, + kcHomeAbsoluteSelect, + kcEndHorizontalSelect, + kcEndVerticalSelect, + kcEndAbsoluteSelect, + kcEndPatNavigationSelect = kcEndAbsoluteSelect, + + //Pattern Editing + kcStartPatternEditing, + kcStartSelect = kcStartPatternEditing, + kcSelect = kcStartSelect, + kcSelectWithNav, + kcSelectOff, + kcSelectOffWithNav, + kcSelectWithCopySelect, + kcSelectOffWithCopySelect, + kcCopySelect, + kcCopySelectOff, + kcCopySelectWithNav, + kcCopySelectOffWithNav, + kcCopySelectWithSelect, + kcCopySelectOffWithSelect, + kcSelectChannel, + kcSelectColumn, + kcSelectRow, + kcSelectEvent, + kcSelectBeat, + kcSelectMeasure, + kcLoseSelection, + kcCopyAndLoseSelection, + kcEndSelect = kcCopyAndLoseSelection, + + kcStartPatternClipboard, + kcCutPatternChannel = kcStartPatternClipboard, + kcCutPattern, + kcCopyPatternChannel, + kcCopyPattern, + kcPastePatternChannel, + kcPastePattern, + kcToggleClipboardManager, + kcClipboardPrev, + kcClipboardNext, + kcEndPatternClipboard = kcClipboardNext, + + kcStartPatternEditMisc, + kcToggleFollowSong = kcStartPatternEditMisc, + kcCursorCopy, + kcCursorPaste, + kcFindInstrument, + kcPatternRecord, + kcPatternPlayRow, + kcSetSpacing, + kcSetSpacing0, + kcSetSpacing1, + kcSetSpacing2, + kcSetSpacing3, + kcSetSpacing4, + kcSetSpacing5, + kcSetSpacing6, + kcSetSpacing7, + kcSetSpacing8, + kcSetSpacing9, + kcIncreaseSpacing, + kcDecreaseSpacing, + kcSwitchToOrderList, + kcNewPattern, + kcDuplicatePattern, + kcSplitPattern, + kcPatternEditPCNotePlugin, + kcTogglePluginEditor, + kcShowNoteProperties, + kcShowPatternProperties, + kcShowSplitKeyboardSettings, + kcChordEditor, + kcChangeLoopStatus, + kcShowEditMenu, + kcShowChannelCtxMenu, + kcShowChannelPluginCtxMenu, + kcTimeAtRow, + kcLockPlaybackToRows, + kcQuantizeSettings, + kcTogglePatternPlayRow, + kcToggleOverflowPaste, + kcToggleNoteOffRecordPC, + kcToggleNoteOffRecordMIDI, + kcEndPatternEditMisc = kcToggleNoteOffRecordMIDI, + + kcStartChannelKeys, + kcChannelMute = kcStartChannelKeys, + kcChannelSolo, + kcChannelUnmuteAll, + kcToggleChanMuteOnPatTransition, + kcUnmuteAllChnOnPatTransition, + kcSoloChnOnPatTransition, + kcChannelRecordSelect, + kcChannelSplitRecordSelect, + kcChannelReset, + kcChannelTranspose, + kcChannelDuplicate, + kcChannelAddBefore, + kcChannelAddAfter, + kcChannelRemove, + kcChannelMoveLeft, + kcChannelMoveRight, + kcChannelSettings, + kcEndChannelKeys = kcChannelSettings, + kcBeginTranspose, + kcTransposeUp = kcBeginTranspose, + kcTransposeDown, + kcTransposeOctUp, + kcTransposeOctDown, + kcTransposeCustom, + kcTransposeCustomQuick, + kcDataEntryUp, + kcDataEntryDown, + kcDataEntryUpCoarse, + kcDataEntryDownCoarse, + kcEndTranspose = kcDataEntryDownCoarse, + kcPatternAmplify, + kcPatternInterpolateNote, + kcPatternInterpolateInstr, + kcPatternInterpolateVol, + kcPatternInterpolateEffect, + kcPatternVisualizeEffect, + kcPatternOpenRandomizer, + kcPatternGoto, + kcPatternSetInstrument, + kcPatternSetInstrumentNotEmpty, + kcPatternGrowSelection, + kcPatternShrinkSelection, + // kcClearSelection, + kcClearRow, + kcClearField, + kcClearFieldITStyle, + kcClearRowStep, + kcClearFieldStep, + kcClearFieldStepITStyle, + kcDeleteRow, + kcDeleteWholeRow, + kcDeleteRowGlobal, + kcDeleteWholeRowGlobal, + kcInsertRow, + kcInsertWholeRow, + kcInsertRowGlobal, + kcInsertWholeRowGlobal, + kcPrevPattern, + kcNextPattern, + kcPrevSequence, + kcNextSequence, + kcEndPatternEditing = kcNextSequence, + + //Notes + kcVPStartNotes, + kcVPNoteC_0 = kcVPStartNotes, + kcVPNoteCS0, + kcVPNoteD_0, + kcVPNoteDS0, + kcVPNoteE_0, + kcVPNoteF_0, + kcVPNoteFS0, + kcVPNoteG_0, + kcVPNoteGS0, + kcVPNoteA_1, + kcVPNoteAS1, + kcVPNoteB_1, + kcVPNoteC_1, + kcVPNoteCS1, + kcVPNoteD_1, + kcVPNoteDS1, + kcVPNoteE_1, + kcVPNoteF_1, + kcVPNoteFS1, + kcVPNoteG_1, + kcVPNoteGS1, + kcVPNoteA_2, + kcVPNoteAS2, + kcVPNoteB_2, + kcVPNoteC_2, + kcVPNoteCS2, + kcVPNoteD_2, + kcVPNoteDS2, + kcVPNoteE_2, + kcVPNoteF_2, + kcVPNoteFS2, + kcVPNoteG_2, + kcVPNoteGS2, + kcVPNoteA_3, + kcVPEndNotes = kcVPNoteA_3, + + //Note stops + kcVPStartNoteStops, + kcVPNoteStopC_0 = kcVPStartNoteStops, + kcVPNoteStopCS0, + kcVPNoteStopD_0, + kcVPNoteStopDS0, + kcVPNoteStopE_0, + kcVPNoteStopF_0, + kcVPNoteStopFS0, + kcVPNoteStopG_0, + kcVPNoteStopGS0, + kcVPNoteStopA_1, + kcVPNoteStopAS1, + kcVPNoteStopB_1, + kcVPNoteStopC_1, + kcVPNoteStopCS1, + kcVPNoteStopD_1, + kcVPNoteStopDS1, + kcVPNoteStopE_1, + kcVPNoteStopF_1, + kcVPNoteStopFS1, + kcVPNoteStopG_1, + kcVPNoteStopGS1, + kcVPNoteStopA_2, + kcVPNoteStopAS2, + kcVPNoteStopB_2, + kcVPNoteStopC_2, + kcVPNoteStopCS2, + kcVPNoteStopD_2, + kcVPNoteStopDS2, + kcVPNoteStopE_2, + kcVPNoteStopF_2, + kcVPNoteStopFS2, + kcVPNoteStopG_2, + kcVPNoteStopGS2, + kcVPNoteStopA_3, + kcVPEndNoteStops = kcVPNoteStopA_3, + + //Chords + kcVPStartChords, + kcVPChordC_0 = kcVPStartChords, + kcVPChordCS0, + kcVPChordD_0, + kcVPChordDS0, + kcVPChordE_0, + kcVPChordF_0, + kcVPChordFS0, + kcVPChordG_0, + kcVPChordGS0, + kcVPChordA_1, + kcVPChordAS1, + kcVPChordB_1, + kcVPChordC_1, + kcVPChordCS1, + kcVPChordD_1, + kcVPChordDS1, + kcVPChordE_1, + kcVPChordF_1, + kcVPChordFS1, + kcVPChordG_1, + kcVPChordGS1, + kcVPChordA_2, + kcVPChordAS2, + kcVPChordB_2, + kcVPChordC_2, + kcVPChordCS2, + kcVPChordD_2, + kcVPChordDS2, + kcVPChordE_2, + kcVPChordF_2, + kcVPChordFS2, + kcVPChordG_2, + kcVPChordGS2, + kcVPChordA_3, + kcVPEndChords = kcVPChordA_3, + + //Chord Stops + kcVPStartChordStops, + kcVPChordStopC_0 = kcVPStartChordStops, + kcVPChordStopCS0, + kcVPChordStopD_0, + kcVPChordStopDS0, + kcVPChordStopE_0, + kcVPChordStopF_0, + kcVPChordStopFS0, + kcVPChordStopG_0, + kcVPChordStopGS0, + kcVPChordStopA_1, + kcVPChordStopAS1, + kcVPChordStopB_1, + kcVPChordStopC_1, + kcVPChordStopCS1, + kcVPChordStopD_1, + kcVPChordStopDS1, + kcVPChordStopE_1, + kcVPChordStopF_1, + kcVPChordStopFS1, + kcVPChordStopG_1, + kcVPChordStopGS1, + kcVPChordStopA_2, + kcVPChordStopAS2, + kcVPChordStopB_2, + kcVPChordStopC_2, + kcVPChordStopCS2, + kcVPChordStopD_2, + kcVPChordStopDS2, + kcVPChordStopE_2, + kcVPChordStopF_2, + kcVPChordStopFS2, + kcVPChordStopG_2, + kcVPChordStopGS2, + kcVPChordStopA_3, + kcVPEndChordStops = kcVPChordStopA_3, + + //Set octave from note column + kcSetOctave0, + kcSetOctave1, + kcSetOctave2, + kcSetOctave3, + kcSetOctave4, + kcSetOctave5, + kcSetOctave6, + kcSetOctave7, + kcSetOctave8, + kcSetOctave9, + + // Release set octave key + kcSetOctaveStop0, + kcSetOctaveStop1, + kcSetOctaveStop2, + kcSetOctaveStop3, + kcSetOctaveStop4, + kcSetOctaveStop5, + kcSetOctaveStop6, + kcSetOctaveStop7, + kcSetOctaveStop8, + kcSetOctaveStop9, + + //Note Misc + kcStartNoteMisc, + kcChordModifier = kcStartNoteMisc, + kcNoteCutOld, + kcNoteOffOld, + kcNoteFadeOld, + kcNoteCut, + kcNoteOff, + kcNoteFade, + kcNotePC, + kcNotePCS, + kcEndNoteMisc = kcNotePCS, + + + //Set instruments + kcSetIns0, + kcSetIns1, + kcSetIns2, + kcSetIns3, + kcSetIns4, + kcSetIns5, + kcSetIns6, + kcSetIns7, + kcSetIns8, + kcSetIns9, + + //Volume stuff + kcSetVolumeStart, + kcSetVolume0 = kcSetVolumeStart, + kcSetVolume1, + kcSetVolume2, + kcSetVolume3, + kcSetVolume4, + kcSetVolume5, + kcSetVolume6, + kcSetVolume7, + kcSetVolume8, + kcSetVolume9, + kcSetVolumeVol, //v + kcSetVolumePan, //p + kcSetVolumeVolSlideUp, //c + kcSetVolumeVolSlideDown, //d + kcSetVolumeFineVolUp, //a + kcSetVolumeFineVolDown, //b + kcSetVolumeVibratoSpd, //u + kcSetVolumeVibrato, //h + kcSetVolumeXMPanLeft, //l + kcSetVolumeXMPanRight, //r + kcSetVolumePortamento, //g + kcSetVolumeITPortaUp, //f + kcSetVolumeITPortaDown, //e + kcSetVolumeITUnused, //: + kcSetVolumeITOffset, //o + kcSetVolumeEnd = kcSetVolumeITOffset, + + //Effect params + kcSetFXParam0, + kcSetFXParam1, + kcSetFXParam2, + kcSetFXParam3, + kcSetFXParam4, + kcSetFXParam5, + kcSetFXParam6, + kcSetFXParam7, + kcSetFXParam8, + kcSetFXParam9, + kcSetFXParamA, + kcSetFXParamB, + kcSetFXParamC, + kcSetFXParamD, + kcSetFXParamE, + kcSetFXParamF, + + //Effect commands. ORDER IS CRUCIAL. + kcSetFXStart, + kcFixedFXStart = kcSetFXStart, + kcSetFXarp = kcSetFXStart, //0,j + kcSetFXportUp, //1,f + kcSetFXportDown, //2,e + kcSetFXport, //3,g + kcSetFXvibrato, //4,h + kcSetFXportSlide, //5,l + kcSetFXvibSlide, //6,k + kcSetFXtremolo, //7,r + kcSetFXpan, //8,x + kcSetFXoffset, //9,o + kcSetFXvolSlide, //a,d + kcSetFXgotoOrd, //b,b + kcSetFXsetVol, //c,? + kcSetFXgotoRow, //d,c + kcSetFXretrig, //r,q + kcSetFXspeed, //?,a + kcSetFXtempo, //f,t + kcSetFXtremor, //t,i + kcSetFXextendedMOD, //e,? + kcSetFXextendedS3M, //?,s + kcSetFXchannelVol, //?,m + kcSetFXchannelVols, //?,n + kcSetFXglobalVol, //g,v + kcSetFXglobalVols, //h,w + kcSetFXkeyoff, //k,? + kcSetFXfineVib, //?,u + kcSetFXpanbrello, //y,y + kcSetFXextendedXM, //x,? + kcSetFXpanSlide, //p,p + kcSetFXsetEnvPos, //l,? + kcSetFXmacro, //z,z + kcFixedFXend = kcSetFXmacro, + kcSetFXmacroSlide, //?,\ , + kcSetFXdelaycut, //?,: + kcSetFXextension, //?,# + kcSetFXFinetune, //?,+ + kcSetFXFinetuneSmooth, //?,* + kcSetFXEnd = kcSetFXFinetuneSmooth, + + kcStartInstrumentMisc, + kcInstrumentLoad = kcStartInstrumentMisc, + kcInstrumentSave, + kcInstrumentNew, + kcInstrumentEnvelopeLoad, + kcInstrumentEnvelopeSave, + kcInstrumentEnvelopeZoomIn, + kcInstrumentEnvelopeZoomOut, + kcInstrumentEnvelopeScale, + kcInstrumentEnvelopeSwitchToVolume, + kcInstrumentEnvelopeSwitchToPanning, + kcInstrumentEnvelopeSwitchToPitch, + kcInstrumentEnvelopeToggleVolume, + kcInstrumentEnvelopeTogglePanning, + kcInstrumentEnvelopeTogglePitch, + kcInstrumentEnvelopeToggleFilter, + kcInstrumentEnvelopeToggleLoop, + kcInstrumentEnvelopeSelectLoopStart, + kcInstrumentEnvelopeSelectLoopEnd, + kcInstrumentEnvelopeToggleSustain, + kcInstrumentEnvelopeSelectSustainStart, + kcInstrumentEnvelopeSelectSustainEnd, + kcInstrumentEnvelopeToggleCarry, + kcInstrumentEnvelopePointPrev, + kcInstrumentEnvelopePointNext, + kcInstrumentEnvelopePointMoveLeft, + kcInstrumentEnvelopePointMoveRight, + kcInstrumentEnvelopePointMoveLeftCoarse, + kcInstrumentEnvelopePointMoveRightCoarse, + kcInstrumentEnvelopePointMoveUp, + kcInstrumentEnvelopePointMoveUp8, + kcInstrumentEnvelopePointMoveDown, + kcInstrumentEnvelopePointMoveDown8, + kcInstrumentEnvelopePointInsert, + kcInstrumentEnvelopePointRemove, + kcInstrumentEnvelopeSetLoopStart, + kcInstrumentEnvelopeSetLoopEnd, + kcInstrumentEnvelopeSetSustainLoopStart, + kcInstrumentEnvelopeSetSustainLoopEnd, + kcInstrumentEnvelopeToggleReleaseNode, + kcEndInstrumentMisc = kcInstrumentEnvelopeToggleReleaseNode, + + kcStartInstrumentCtrlMisc, + kcInstrumentCtrlLoad = kcStartInstrumentCtrlMisc, + kcInstrumentCtrlSave, + kcInstrumentCtrlNew, + kcInstrumentCtrlDuplicate, + kcInsNoteMapEditSampleMap, + kcInsNoteMapEditSample, + kcInsNoteMapCopyCurrentNote, + kcInsNoteMapCopyCurrentSample, + kcInsNoteMapReset, + kcInsNoteMapTransposeSamples, + kcInsNoteMapRemove, + kcInsNoteMapTransposeUp, + kcInsNoteMapTransposeDown, + kcInsNoteMapTransposeOctUp, + kcInsNoteMapTransposeOctDown, + kcEndInstrumentCtrlMisc = kcInsNoteMapTransposeOctDown, + + kcStartSampleMisc, + kcSampleLoad = kcStartSampleMisc, + kcSampleLoadRaw, + kcSampleSave, + kcSampleNew, + kcSampleDuplicate, + kcSampleInitializeOPL, + kcSampleTransposeUp, + kcSampleTransposeDown, + kcSampleTransposeOctUp, + kcSampleTransposeOctDown, + kcEndSampleMisc = kcSampleTransposeOctDown, + + kcStartSampleEditing, + kcSampleTrim = kcStartSampleEditing, + kcSampleTrimToLoopEnd, + kcSampleSilence, + kcSampleNormalize, + kcSampleAmplify, + kcSampleReverse, + kcSampleDelete, + kcSampleToggleDrawing, + kcSampleResize, + kcSampleGrid, + kcSampleZoomUp, + kcSampleZoomDown, + kcSampleZoomSelection, + kcSampleCenterSampleStart, + kcSampleCenterSampleEnd, + kcSampleCenterLoopStart, + kcSampleCenterLoopEnd, + kcSampleCenterSustainStart, + kcSampleCenterSustainEnd, + kcSampleConvertPingPongLoop, + kcSampleConvertPingPongSustain, + kcSample8Bit, + kcSampleMonoMix, + kcSampleMonoLeft, + kcSampleMonoRight, + kcSampleMonoSplit, + kcSampleStereoSep, + kcSampleUpsample, + kcSampleDownsample, + kcSampleResample, + kcSampleInvert, + kcSampleSignUnsign, + kcSampleRemoveDCOffset, + kcSampleQuickFade, + kcSampleXFade, + kcSampleAutotune, + kcEndSampleEditing = kcSampleAutotune, + + kcStartSampleCues, + kcEndSampleCues = kcStartSampleCues + 8, + kcSampleSlice, + kcEndSampleCueGroup = kcSampleSlice, + + kcSampStartNotes, + kcSampEndNotes = kcSampStartNotes + kcCommandSetNumNotes, + kcSampStartNoteStops, + kcSampEndNoteStops = kcSampStartNoteStops + kcCommandSetNumNotes, + + kcInstrumentStartNotes, + kcInstrumentEndNotes = kcInstrumentStartNotes + kcCommandSetNumNotes, + kcInstrumentStartNoteStops, + kcInstrumentEndNoteStops = kcInstrumentStartNoteStops + kcCommandSetNumNotes, + + kcTreeViewStartNotes, + kcTreeViewEndNotes = kcTreeViewStartNotes + kcCommandSetNumNotes, + kcTreeViewStartNoteStops, + kcTreeViewEndNoteStops = kcTreeViewStartNoteStops + kcCommandSetNumNotes, + + kcInsNoteMapStartNotes, + kcInsNoteMapEndNotes = kcInsNoteMapStartNotes + kcCommandSetNumNotes, + kcInsNoteMapStartNoteStops, + kcInsNoteMapEndNoteStops = kcInsNoteMapStartNoteStops + kcCommandSetNumNotes, + + kcVSTGUIStartNotes, + kcVSTGUIEndNotes = kcVSTGUIStartNotes + kcCommandSetNumNotes, + kcVSTGUIStartNoteStops, + kcVSTGUIEndNoteStops = kcVSTGUIStartNoteStops + kcCommandSetNumNotes, + + kcCommentsStartNotes, + kcCommentsEndNotes = kcCommentsStartNotes + kcCommandSetNumNotes, + kcCommentsStartNoteStops, + kcCommentsEndNoteStops = kcCommentsStartNoteStops + kcCommandSetNumNotes, + + kcTreeViewStopPreview, + + kcStartVSTGUICommands, + kcVSTGUIPrevPreset = kcStartVSTGUICommands, + kcVSTGUINextPreset, + kcVSTGUIPrevPresetJump, + kcVSTGUINextPresetJump, + kcVSTGUIRandParams, + kcVSTGUIToggleRecordParams, + kcVSTGUIToggleRecordMIDIOut, + kcVSTGUIToggleSendKeysToPlug, + kcVSTGUIBypassPlug, + kcEndVSTGUICommands = kcVSTGUIBypassPlug, + + kcStartOrderlistCommands, + // Orderlist edit + kcStartOrderlistEdit = kcStartOrderlistCommands, + kcOrderlistEditDelete = kcStartOrderlistEdit, + kcOrderlistEditInsert, + kcOrderlistEditInsertSeparator, + kcOrderlistEditCopyOrders, + kcMergePatterns, + kcOrderlistEditPattern, + kcOrderlistSwitchToPatternView, + kcEndOrderlistEdit = kcOrderlistSwitchToPatternView, + // Orderlist navigation + kcStartOrderlistNavigation, + kcOrderlistNavigateLeft = kcStartOrderlistNavigation, + kcOrderlistNavigateRight, + kcOrderlistNavigateFirst, + kcOrderlistNavigateLast, + kcEndOrderlistNavigation = kcOrderlistNavigateLast, + // with selection key(must match order above) + kcStartOrderlistNavigationSelect, + kcOrderlistNavigateLeftSelect = kcStartOrderlistNavigationSelect, + kcOrderlistNavigateRightSelect, + kcOrderlistNavigateFirstSelect, + kcOrderlistNavigateLastSelect, + kcEndOrderlistNavigationSelect = kcOrderlistNavigateLastSelect, + // Orderlist pattern list edit + kcStartOrderlistNum, + kcOrderlistPat0 = kcStartOrderlistNum, + kcOrderlistPat1, + kcOrderlistPat2, + kcOrderlistPat3, + kcOrderlistPat4, + kcOrderlistPat5, + kcOrderlistPat6, + kcOrderlistPat7, + kcOrderlistPat8, + kcOrderlistPat9, + kcOrderlistPatPlus, + kcOrderlistPatMinus, + kcOrderlistPatIgnore, + kcOrderlistPatInvalid, + kcEndOrderlistNum = kcOrderlistPatInvalid, + // Playback lock + kcStartOrderlistLock, + kcOrderlistLockPlayback = kcStartOrderlistLock, + kcOrderlistUnlockPlayback, + kcEndOrderlistLock = kcOrderlistUnlockPlayback, + kcEndOrderlistCommands = kcEndOrderlistLock, + + kcStartChnSettingsCommands, + kcChnSettingsPrev = kcStartChnSettingsCommands, + kcChnSettingsNext, + kcChnColorFromPrev, + kcChnColorFromNext, + kcChnSettingsClose, + kcEndChnSettingsCommands = kcChnSettingsClose, + + kcStartCommentsCommands, + kcToggleSmpInsList = kcStartCommentsCommands, + kcExecuteSmpInsListItem, + kcRenameSmpInsListItem, + kcEndCommentsCommands = kcRenameSmpInsListItem, + + kcNumCommands, +}; + + +enum Modifiers : uint8 +{ + ModNone = 0, + ModShift = HOTKEYF_SHIFT, + ModCtrl = HOTKEYF_CONTROL, + ModAlt = HOTKEYF_ALT, + ModWin = HOTKEYF_EXT, + ModMidi = HOTKEYF_MIDI, + ModRShift = HOTKEYF_RSHIFT, + ModRCtrl = HOTKEYF_RCONTROL, + ModRAlt = HOTKEYF_RALT, + MaxMod = ModShift | ModCtrl | ModAlt | ModWin | ModMidi | ModRShift | ModRCtrl | ModRAlt, +}; +DECLARE_FLAGSET(Modifiers) + + +struct KeyCombination +{ +protected: + InputTargetContext ctx; // TODO: This should probably rather be a member of CommandStruct and not of the individual key combinations for consistency's sake. + FlagSet<Modifiers> mod; + uint8 code; + FlagSet<KeyEventType> event; + + constexpr uint32 AsUint32() const + { + static_assert(sizeof(KeyCombination) == sizeof(uint32)); + return (static_cast<uint32>(ctx) << 0) | + (static_cast<uint32>(mod.GetRaw()) << 8) | + (static_cast<uint32>(code) << 16) | + (static_cast<uint32>(event.GetRaw()) << 24); + } + +public: + KeyCombination(InputTargetContext context = kCtxAllContexts, FlagSet<Modifiers> modifier = ModNone, UINT key = 0, FlagSet<KeyEventType> type = kKeyEventNone) + : ctx(context) + , mod(modifier) + , code(static_cast<uint8>(key)) + , event(type) + { + } + + constexpr bool operator==(const KeyCombination &other) const + { + return ctx == other.ctx && mod == other.mod && code == other.code && event == other.event; + } + + constexpr bool operator<(const KeyCombination &kc) const + { + return AsUint32() < kc.AsUint32(); + } + + // Getters / Setters + void Context(InputTargetContext context) { ctx = context; } + constexpr InputTargetContext Context() const { return ctx; } + + void Modifier(FlagSet<Modifiers> modifier) { mod = modifier; } + constexpr FlagSet<Modifiers> Modifier() const { return mod; } + void Modifier(const KeyCombination &other) { mod = other.mod; } + void AddModifier(FlagSet<Modifiers> modifier) { mod |= modifier; } + void AddModifier(const KeyCombination &other) { mod |= other.mod; } + + void KeyCode(UINT key) { code = static_cast<uint8>(key); } + constexpr UINT KeyCode() const { return code; } + + void EventType(FlagSet<KeyEventType> type) { event = type; } + constexpr FlagSet<KeyEventType> EventType() const { return event; } + + static KeyCombination FromLPARAM(LPARAM lParam) + { + return KeyCombination( + static_cast<InputTargetContext>((lParam >> 0) & 0xFF), + static_cast<Modifiers>((lParam >> 8) & 0xFF), + static_cast<UINT>((lParam >> 16) & 0xFF), + static_cast<KeyEventType>((lParam >> 24) & 0xFF)); + } + LPARAM AsLPARAM() const { return AsUint32(); } + + // Key combination to string + static CString GetContextText(InputTargetContext ctx); + CString GetContextText() const { return GetContextText(Context()); } + + static CString GetModifierText(FlagSet<Modifiers> mod); + CString GetModifierText() const { return GetModifierText(Modifier()); } + + static CString GetKeyText(FlagSet<Modifiers> mod, UINT code); + CString GetKeyText() const { return GetKeyText(Modifier(), KeyCode()); } + + static CString GetKeyEventText(FlagSet<KeyEventType> event); + CString GetKeyEventText() const { return GetKeyEventText(EventType()); } + + static bool IsExtended(UINT code); +}; + +using KeyMap = std::multimap<KeyCombination, CommandID>; +using KeyMapRange = std::pair<KeyMap::const_iterator, KeyMap::const_iterator>; + +//KeyMap + +struct KeyCommand +{ + static constexpr uint32 Dummy = 1u << 31; + static constexpr uint32 Hidden = 1u << 30; + static constexpr uint32 UIDMask = Hidden - 1u; + + std::vector<KeyCombination> kcList; + CString Message; + +protected: + uint32 UID = 0; + +public: + KeyCommand() = default; + KeyCommand(uint32 uid, const TCHAR *message = _T(""), std::vector<KeyCombination> keys = {}); + + // Unique ID for on-disk keymap format. + // Note that hidden commands do not have a unique ID, because they are never written to keymap files. + uint32 ID() const noexcept { return UID & UIDMask; } + // e.g. Chord modifier is a dummy command, which serves only to automatically + // generate a set of key combinations for chords + bool IsDummy() const noexcept { return (UID & Dummy) != 0; } + // Hidden commands are not configurable by the user (e.g. derived from dummy commands or note entry keys duplicated into other contexts) + bool IsHidden() const noexcept { return (UID & Hidden) != 0; } +}; + + +enum RuleID +{ + krPreventDuplicate, + krDeleteOldOnConflict, + + krAllowNavigationWithSelection, + krAllowSelectionWithNavigation, + krAutoSelectOff, + krAllowSelectCopySelectCombos, + krLockNotesToChords, + krNoteOffOnKeyRelease, + krPropagateNotes, + krReassignDigitsToOctaves, + krAutoSpacing, + krCheckModifiers, + krPropagateSampleManipulation, + kNumRules +}; + +struct CModSpecifications; + +class CCommandSet +{ +protected: + //util + void SetupCommands(); + void SetupContextHierarchy(); + CString EnforceAll(KeyCombination kc, CommandID cmd, bool adding); + + CommandID FindCmd(uint32 uid) const; + bool KeyCombinationConflict(KeyCombination kc1, KeyCombination kc2, bool checkEventConflict = true) const; + + const CModSpecifications *m_oldSpecs = nullptr; + KeyCommand m_commands[kcNumCommands]; + std::bitset<kCtxMaxInputContexts> m_isParentContext[kCtxMaxInputContexts]; + std::bitset<kNumRules> m_enforceRule; + +public: + CCommandSet(); + + // Population + CString Add(KeyCombination kc, CommandID cmd, bool overwrite, int pos = -1, bool checkEventConflict = true); + CString Remove(KeyCombination kc, CommandID cmd); + CString Remove(int pos, CommandID cmd); + + std::pair<CommandID, KeyCombination> IsConflicting(KeyCombination kc, CommandID cmd, bool checkEventConflict = true) const; + bool IsCrossContextConflict(KeyCombination kc1, KeyCombination kc2) const; + + // Tranformation + bool QuickChange_SetEffects(const CModSpecifications &modSpecs); + bool QuickChange_NotesRepeat(bool repeat); + + // Communication + KeyCombination GetKey(CommandID cmd, UINT key) const { return m_commands[cmd].kcList[key]; } + bool isHidden(UINT c) const { return m_commands[c].IsHidden(); } + int GetKeyListSize(CommandID cmd) const { return (cmd != kcNull) ? static_cast<int>(m_commands[cmd].kcList.size()) : 0; } + CString GetCommandText(CommandID cmd) const { return m_commands[cmd].Message; } + CString GetKeyTextFromCommand(CommandID c, UINT key) const; + + // Pululation ;) + void Copy(const CCommandSet *source); // Copy the contents of a commandset into this command set + void GenKeyMap(KeyMap &km); // Generate a keymap from this command set + bool SaveFile(const mpt::PathString &filename); + bool LoadFile(const mpt::PathString &filename); + bool LoadFile(std::istream &iStrm, const mpt::ustring &filenameDescription, const bool fillExistingSet = false); + bool LoadDefaultKeymap(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.cpp new file mode 100644 index 00000000..f8b9279f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.cpp @@ -0,0 +1,344 @@ +/* + * Ctrl_com.cpp + * ------------ + * Purpose: Song comments tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Moddoc.h" +#include "Globals.h" +#include "Ctrl_com.h" +#include "view_com.h" +#include "InputHandler.h" +#include "../soundlib/mod_specifications.h" + + +//#define MPT_COMMENTS_LONG_LINES_WRAP +//#define MPT_COMMENTS_LONG_LINES_TRUNCATE + + +#define MPT_COMMENTS_MARGIN 4 + + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(CCtrlComments, CModControlDlg) + //{{AFX_MSG_MAP(CCtrlComments) + ON_EN_UPDATE(IDC_EDIT_COMMENTS, &CCtrlComments::OnCommentsUpdated) + ON_EN_CHANGE(IDC_EDIT_COMMENTS, &CCtrlComments::OnCommentsChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +void CCtrlComments::DoDataExchange(CDataExchange* pDX) +{ + CModControlDlg::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CCtrlComments) + DDX_Control(pDX, IDC_EDIT_COMMENTS, m_EditComments); + //}}AFX_DATA_MAP +} + + +CCtrlComments::CCtrlComments(CModControlView &parent, CModDoc &document) : CModControlDlg(parent, document) +{ +} + + +CRuntimeClass *CCtrlComments::GetAssociatedViewClass() +{ + return RUNTIME_CLASS(CViewComments); +} + + +void CCtrlComments::OnActivatePage(LPARAM) +{ + // Don't stop generating VU meter messages + m_modDoc.SetNotifications(Notification::Default); + m_modDoc.SetFollowWnd(m_hWnd); + m_EditComments.SetFocus(); +} + + +void CCtrlComments::OnDeactivatePage() +{ + CModControlDlg::OnDeactivatePage(); +} + + +BOOL CCtrlComments::OnInitDialog() +{ + CModControlDlg::OnInitDialog(); + // Initialize comments + UINT margin = Util::ScalePixels(MPT_COMMENTS_MARGIN, m_EditComments.m_hWnd); + m_EditComments.SetMargins(margin, margin); + UpdateView(CommentHint().ModType()); + m_EditComments.SetFocus(); + m_EditComments.FmtLines(FALSE); + m_bInitialized = TRUE; + return FALSE; +} + + +void CCtrlComments::RecalcLayout() +{ + CRect rcClient, rect; + int cx0, cy0; + + if ((!m_hWnd) || (!m_EditComments.m_hWnd)) return; + GetClientRect(&rcClient); + m_EditComments.GetWindowRect(&rect); + ScreenToClient(&rect); + cx0 = rect.Width(); + cy0 = rect.Height(); + rect.bottom = rcClient.bottom - 3; + rect.right = rcClient.right - rect.left; + if ((rect.right > rect.left) && (rect.bottom > rect.top)) + { + int cx = rect.Width(), cy = rect.Height(); + if(m_sndFile.GetModSpecifications().commentLineLengthMax != 0) + { + int cxmax = Util::ScalePixels(GetSystemMetrics(SM_CXBORDER) + MPT_COMMENTS_MARGIN + m_sndFile.GetModSpecifications().commentLineLengthMax * charWidth + MPT_COMMENTS_MARGIN + GetSystemMetrics(SM_CXVSCROLL) + GetSystemMetrics(SM_CXBORDER) - 1, m_EditComments.m_hWnd); + if (cx > cxmax && cxmax != 0) cx = cxmax; + //SetWindowLong(m_EditComments.m_hWnd, GWL_STYLE, GetWindowLong(m_EditComments.m_hWnd, GWL_STYLE) & ~WS_HSCROLL); + } else + { + //SetWindowLong(m_EditComments.m_hWnd, GWL_STYLE, GetWindowLong(m_EditComments.m_hWnd, GWL_STYLE) | WS_HSCROLL); + } + if ((cx != cx0) || (cy != cy0)) m_EditComments.SetWindowPos(NULL, 0,0, cx, cy, SWP_NOMOVE|SWP_NOZORDER|SWP_DRAWFRAME); + } +} + + +void CCtrlComments::UpdateView(UpdateHint hint, CObject *pHint) +{ + CommentHint commentHint = hint.ToType<CommentHint>(); + if (pHint == this || !commentHint.GetType()[HINT_MODCOMMENTS | HINT_MPTOPTIONS | HINT_MODTYPE]) return; + if (m_nLockCount) return; + m_nLockCount++; + + static FontSetting previousFont; + FontSetting font = TrackerSettings::Instance().commentsFont; + // Point size to pixels + int32 fontSize = -MulDiv(font.size, m_nDPIy, 720); + if(previousFont != font) + { + previousFont = font; + CMainFrame::GetCommentsFont() = ::CreateFont(fontSize, 0, 0, 0, font.flags[FontSetting::Bold] ? FW_BOLD : FW_NORMAL, + font.flags[FontSetting::Italic] ? TRUE :FALSE, FALSE, FALSE, + DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, + CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, + FIXED_PITCH | FF_MODERN, mpt::ToCString(font.name)); + } + m_EditComments.SendMessage(WM_SETFONT, (WPARAM)CMainFrame::GetCommentsFont()); + CDC * pDC = m_EditComments.GetDC(); + pDC->SelectObject(CMainFrame::GetCommentsFont()); + TEXTMETRIC tm; + pDC->GetTextMetrics(&tm); + charWidth = tm.tmAveCharWidth; + m_EditComments.ReleaseDC(pDC); + + RecalcLayout(); + + m_EditComments.SetRedraw(FALSE); + + std::string text = m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF); + for(std::size_t i = 0; i < text.length(); ++i) + { + // replace control characters + char c = text[i]; + if(c > '\0' && c < ' ' && c != '\r' && c != '\n') + { + c = ' '; + } + text[i] = c; + } + CString new_text = mpt::ToCString(m_sndFile.GetCharsetInternal(), text); + CString old_text; + m_EditComments.GetWindowText(old_text); + if(new_text != old_text) + { + m_EditComments.SetWindowText(new_text); + } + + if(commentHint.GetType() & HINT_MODTYPE) + { + m_EditComments.SetReadOnly(!m_sndFile.GetModSpecifications().hasComments); + } + + m_EditComments.SetRedraw(TRUE); + m_nLockCount--; +} + + +void CCtrlComments::OnCommentsUpdated() +{ + +#if defined(MPT_COMMENTS_LONG_LINES_TRUNCATE) || defined(MPT_COMMENTS_LONG_LINES_WRAP) + + if(m_Reformatting) + { + return; + } + + if(!m_sndFile.GetModSpecifications().hasComments) + { + return; + } + if(m_sndFile.GetModSpecifications().commentLineLengthMax == 0) + { + return; + } + + m_Reformatting = true; + const std::size_t maxline = m_sndFile.GetModSpecifications().commentLineLengthMax; + int beg = 0; + int end = 0; + m_EditComments.GetSel(beg, end); + CString text; + m_EditComments.GetWindowText(text); + std::string lines_new; + lines_new.reserve(text.GetLength()); + bool modified = false; + std::size_t pos = 0; + +#if defined(MPT_COMMENTS_LONG_LINES_WRAP) + + std::string lines = text.GetString(); + std::size_t line_length = 0; + for(std::size_t i = 0; i < lines.length(); ++i) + { + if(lines[i] == '\r') + { + // nothing + } else if (lines[i] == '\n') + { + line_length = 0; + } else + { + line_length += 1; + } + if(line_length > maxline) + { + modified = true; + lines_new.push_back('\r'); + lines_new.push_back('\n'); + if(beg >= 0) + { + if(beg >= pos) + { + beg += 2; + } + } + if(end >= 0) + { + if(end >= pos) + { + end += 2; + } + } + pos += 2; + line_length = 1; + } + lines_new.push_back(lines[i]); + pos++; + } + +#elif defined(MPT_COMMENTS_LONG_LINES_TRUNCATE) + + std::vector<std::string> lines = mpt::String::Split<std::string>(std::string(text.GetString()), std::string("\r\n")); + for(std::size_t i = 0; i < lines.size(); ++i) + { + if(i > 0) + { + pos += 2; + } + if(lines[i].length() > maxline) + { + modified = true; + pos += maxline; + for(std::size_t n = 0; n < lines[i].length() - maxline; ++n) + { + if(beg >= 0) + { + if(beg > pos) + { + beg--; + } + } + if(end >= 0) + { + if(end > pos) + { + end--; + } + } + } + lines[i] = lines[i].substr(0, maxline); + } else + { + pos += lines[i].length(); + } + } + lines_new = mpt::String::Combine(lines, std::string("\r\n")); + +#endif + + if(modified) + { + text = lines_new.c_str(); + m_EditComments.SetWindowText(text); + m_EditComments.SetSel(beg, end); + } + m_Reformatting = false; + +#endif + +} + + +void CCtrlComments::OnCommentsChanged() +{ + if(m_nLockCount) + return; + if ((!m_bInitialized) || (!m_EditComments.m_hWnd) || (!m_EditComments.GetModify())) return; + CString text; + m_EditComments.GetWindowText(text); + m_EditComments.SetModify(FALSE); + if(m_sndFile.m_songMessage.SetFormatted(mpt::ToCharset(m_sndFile.GetCharsetInternal(), text), SongMessage::leCRLF)) + { + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, CommentHint(), this); + } +} + + +BOOL CCtrlComments::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg->message == WM_KEYDOWN && pMsg->wParam == 'A' && GetKeyState(VK_CONTROL) < 0) + { + // Ctrl-A is not handled by multiline edit boxes + if(::GetFocus() == m_EditComments.m_hWnd) + m_EditComments.SetSel(0, -1); + } else if(pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_TAB && CMainFrame::GetMainFrame()->GetInputHandler()->GetModifierMask() == ModNone) + { + int selStart, selEnd; + m_EditComments.GetSel(selStart, selEnd); + int posInLine = (selStart - m_EditComments.LineIndex(m_EditComments.LineFromChar(selStart))); + CString tabs(_T(' '), 4 - (posInLine % 4)); + m_EditComments.ReplaceSel(tabs, TRUE); + m_EditComments.SetSel(-1, -1, TRUE); + return TRUE; + } + return CModControlDlg::PreTranslateMessage(pMsg); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.h b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.h new file mode 100644 index 00000000..ebb2e218 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_com.h @@ -0,0 +1,48 @@ +/* + * Ctrl_com.h + * ---------- + * Purpose: Song comments tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CCtrlComments final : public CModControlDlg +{ +protected: + CEdit m_EditComments; + int charWidth = 0; + bool m_Reformatting = false; + +public: + CCtrlComments(CModControlView &parent, CModDoc &document); + + //{{AFX_VIRTUAL(CCtrlComments) + Setting<LONG> &GetSplitPosRef() override { return TrackerSettings::Instance().glCommentsWindowHeight; } + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange *pDX) override; // DDX/DDV support + void RecalcLayout() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + CRuntimeClass *GetAssociatedViewClass() override; + void OnActivatePage(LPARAM) override; + void OnDeactivatePage() override; + BOOL PreTranslateMessage(MSG *pMsg) override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CCtrlComments) + afx_msg void OnCommentsUpdated(); + afx_msg void OnCommentsChanged(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.cpp new file mode 100644 index 00000000..03374984 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.cpp @@ -0,0 +1,816 @@ +/* + * Ctrl_gen.cpp + * ------------ + * Purpose: General tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Globals.h" +#include "dlg_misc.h" +#include "Ctrl_gen.h" +#include "View_gen.h" +#include "../common/misc_util.h" +#include "../common/mptTime.h" +#include "../soundlib/mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(CCtrlGeneral, CModControlDlg) + //{{AFX_MSG_MAP(CCtrlGeneral) + ON_WM_VSCROLL() + ON_COMMAND(IDC_BUTTON1, &CCtrlGeneral::OnTapTempo) + ON_COMMAND(IDC_BUTTON_MODTYPE, &CCtrlGeneral::OnSongProperties) + ON_COMMAND(IDC_CHECK_LOOPSONG, &CCtrlGeneral::OnLoopSongChanged) + ON_EN_CHANGE(IDC_EDIT_SONGTITLE, &CCtrlGeneral::OnTitleChanged) + ON_EN_CHANGE(IDC_EDIT_ARTIST, &CCtrlGeneral::OnArtistChanged) + ON_EN_CHANGE(IDC_EDIT_TEMPO, &CCtrlGeneral::OnTempoChanged) + ON_EN_CHANGE(IDC_EDIT_SPEED, &CCtrlGeneral::OnSpeedChanged) + ON_EN_CHANGE(IDC_EDIT_GLOBALVOL, &CCtrlGeneral::OnGlobalVolChanged) + ON_EN_CHANGE(IDC_EDIT_RESTARTPOS, &CCtrlGeneral::OnRestartPosChanged) + ON_EN_CHANGE(IDC_EDIT_VSTIVOL, &CCtrlGeneral::OnVSTiVolChanged) + ON_EN_CHANGE(IDC_EDIT_SAMPLEPA, &CCtrlGeneral::OnSamplePAChanged) + ON_MESSAGE(WM_MOD_UPDATEPOSITION, &CCtrlGeneral::OnUpdatePosition) + ON_EN_SETFOCUS(IDC_EDIT_SONGTITLE, &CCtrlGeneral::OnEnSetfocusEditSongtitle) + ON_EN_KILLFOCUS(IDC_EDIT_RESTARTPOS, &CCtrlGeneral::OnRestartPosDone) + ON_CBN_SELCHANGE(IDC_COMBO1, &CCtrlGeneral::OnResamplingChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +void CCtrlGeneral::DoDataExchange(CDataExchange* pDX) +{ + CModControlDlg::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CCtrlGeneral) + DDX_Control(pDX, IDC_EDIT_SONGTITLE, m_EditTitle); + DDX_Control(pDX, IDC_EDIT_ARTIST, m_EditArtist); + //DDX_Control(pDX, IDC_EDIT_TEMPO, m_EditTempo); + DDX_Control(pDX, IDC_SPIN_TEMPO, m_SpinTempo); + DDX_Control(pDX, IDC_EDIT_SPEED, m_EditSpeed); + DDX_Control(pDX, IDC_SPIN_SPEED, m_SpinSpeed); + DDX_Control(pDX, IDC_EDIT_GLOBALVOL, m_EditGlobalVol); + DDX_Control(pDX, IDC_SPIN_GLOBALVOL, m_SpinGlobalVol); + DDX_Control(pDX, IDC_EDIT_VSTIVOL, m_EditVSTiVol); + DDX_Control(pDX, IDC_SPIN_VSTIVOL, m_SpinVSTiVol); + DDX_Control(pDX, IDC_EDIT_SAMPLEPA, m_EditSamplePA); + DDX_Control(pDX, IDC_SPIN_SAMPLEPA, m_SpinSamplePA); + DDX_Control(pDX, IDC_EDIT_RESTARTPOS, m_EditRestartPos); + DDX_Control(pDX, IDC_SPIN_RESTARTPOS, m_SpinRestartPos); + + DDX_Control(pDX, IDC_SLIDER_SONGTEMPO, m_SliderTempo); + DDX_Control(pDX, IDC_SLIDER_VSTIVOL, m_SliderVSTiVol); + DDX_Control(pDX, IDC_SLIDER_GLOBALVOL, m_SliderGlobalVol); + DDX_Control(pDX, IDC_SLIDER_SAMPLEPREAMP, m_SliderSamplePreAmp); + + DDX_Control(pDX, IDC_BUTTON_MODTYPE, m_BtnModType); + DDX_Control(pDX, IDC_VUMETER_LEFT, m_VuMeterLeft); + DDX_Control(pDX, IDC_VUMETER_RIGHT, m_VuMeterRight); + + DDX_Control(pDX, IDC_COMBO1, m_CbnResampling); + //}}AFX_DATA_MAP +} + + +CCtrlGeneral::CCtrlGeneral(CModControlView &parent, CModDoc &document) : CModControlDlg(parent, document) +{ +} + + +BOOL CCtrlGeneral::OnInitDialog() +{ + const auto &specs = m_sndFile.GetModSpecifications(); + CModControlDlg::OnInitDialog(); + // Song Title + m_EditTitle.SetLimitText(specs.modNameLengthMax); + + m_SpinGlobalVol.SetRange(0, (short)(256 / GetGlobalVolumeFactor())); + m_SpinSamplePA.SetRange(0, 2000); + m_SpinVSTiVol.SetRange(0, 2000); + m_SpinRestartPos.SetRange32(0, ORDERINDEX_MAX); + + m_SliderGlobalVol.SetRange(0, MAX_SLIDER_GLOBAL_VOL); + m_SliderVSTiVol.SetRange(0, MAX_SLIDER_VSTI_VOL); + m_SliderSamplePreAmp.SetRange(0, MAX_SLIDER_SAMPLE_VOL); + + m_SpinTempo.SetRange(-10, 10); + m_SliderTempo.SetLineSize(1); + m_SliderTempo.SetPageSize(10); + m_EditTempo.SubclassDlgItem(IDC_EDIT_TEMPO, this); + m_EditTempo.AllowNegative(false); + + m_editsLocked = false; + UpdateView(GeneralHint().ModType()); + OnActivatePage(0); + m_bInitialized = TRUE; + + return FALSE; +} + + +CRuntimeClass *CCtrlGeneral::GetAssociatedViewClass() +{ + return RUNTIME_CLASS(CViewGlobals); +} + + +void CCtrlGeneral::RecalcLayout() +{ +} + + +void CCtrlGeneral::OnActivatePage(LPARAM) +{ + m_modDoc.SetNotifications(Notification::Default); + m_modDoc.SetFollowWnd(m_hWnd); + PostViewMessage(VIEWMSG_SETACTIVE, NULL); + SetFocus(); + + // Combo boxes randomly disappear without this... why? + Invalidate(); +} + + +void CCtrlGeneral::OnDeactivatePage() +{ + m_modDoc.SetFollowWnd(NULL); + m_VuMeterLeft.SetVuMeter(0, true); + m_VuMeterRight.SetVuMeter(0, true); + m_tapTimer = nullptr; // Reset high-precision clock if required +} + + +TEMPO CCtrlGeneral::TempoSliderRange() const +{ + return (TEMPO_SPLIT_THRESHOLD - m_tempoMin) + TEMPO((m_tempoMax - TEMPO_SPLIT_THRESHOLD).GetInt() / TEMPO_SPLIT_PRECISION, 0); +} + + +TEMPO CCtrlGeneral::SliderToTempo(int value) const +{ + if(m_tempoMax < TEMPO_SPLIT_THRESHOLD) + { + return m_tempoMax - TEMPO(value, 0); + } else + { + const auto tempoSliderSplit = TempoToSlider(TEMPO_SPLIT_THRESHOLD); + if(value <= tempoSliderSplit) + return m_tempoMax - TEMPO(value * TEMPO_SPLIT_PRECISION, 0); + else + return m_tempoMin + TempoSliderRange() - TEMPO(value, 0); + } +} + + +int CCtrlGeneral::TempoToSlider(TEMPO tempo) const +{ + if(m_tempoMax < TEMPO_SPLIT_THRESHOLD) + { + return (m_tempoMax - tempo).GetInt(); + } else + { + if(tempo < TEMPO_SPLIT_THRESHOLD) + return (TempoSliderRange() - (std::max(m_tempoMin, tempo) - m_tempoMin)).GetInt(); + else + return (m_tempoMax - std::min(m_tempoMax, tempo)).GetInt() / TEMPO_SPLIT_PRECISION; + } +} + + +void CCtrlGeneral::OnTapTempo() +{ + using TapType = decltype(m_tapTimer->Now()); + static std::array<TapType, 32> tapTime; + static TapType lastTap = 0; + static uint32 numTaps = 0; + + if(m_tapTimer == nullptr) + m_tapTimer = std::make_unique<Util::MultimediaClock>(1); + + const uint32 now = m_tapTimer->Now(); + if(now - lastTap >= 2000) + numTaps = 0; + lastTap = now; + + if(static_cast<size_t>(numTaps) >= tapTime.size()) + { + // Shift back the previously recorded tap history + // cppcheck false-positive + // cppcheck-suppress mismatchingContainers + std::copy(tapTime.begin() + 1, tapTime.end(), tapTime.begin()); + numTaps = static_cast<uint32>(tapTime.size() - 1); + } + + tapTime[numTaps++] = now; + + if(numTaps <= 1) + return; + + // Now apply least squares to tap history + double sum = 0.0, weightedSum = 0.0; + for(uint32 i = 0; i < numTaps; i++) + { + const double tapMs = tapTime[i] / 1000.0; + sum += tapMs; + weightedSum += i * tapMs; + } + + const double lengthSum = numTaps * (numTaps - 1) / 2; + const double lengthSumSum = lengthSum * (2 * numTaps - 1) / 3.0; + const double secondsPerBeat = (numTaps * weightedSum - lengthSum * sum) / (lengthSumSum * numTaps - lengthSum * lengthSum); + + double newTempo = 60.0 / secondsPerBeat; + if(m_sndFile.m_nTempoMode != TempoMode::Modern) + newTempo *= (m_sndFile.m_nDefaultSpeed * m_sndFile.m_nDefaultRowsPerBeat) / 24.0; + if(!m_sndFile.GetModSpecifications().hasFractionalTempo) + newTempo = std::round(newTempo); + TEMPO t(newTempo); + Limit(t, m_tempoMin, m_tempoMax); + m_EditTempo.SetTempoValue(t); +} + + +void CCtrlGeneral::UpdateView(UpdateHint hint, CObject *pHint) +{ + if (pHint == this) return; + FlagSet<HintType> hintType = hint.GetType(); + const bool updateAll = hintType[HINT_MODTYPE]; + + const auto resamplingModes = Resampling::AllModes(); + + if (hintType == HINT_MPTOPTIONS || updateAll) + { + CString defaultResampler; + if(m_sndFile.m_SongFlags[SONG_ISAMIGA] && TrackerSettings::Instance().ResamplerEmulateAmiga != Resampling::AmigaFilter::Off) + defaultResampler = _T("Amiga Resampler"); + else + defaultResampler = CTrackApp::GetResamplingModeName(TrackerSettings::Instance().ResamplerMode, 1, false); + + m_CbnResampling.ResetContent(); + m_CbnResampling.SetItemData(m_CbnResampling.AddString(_T("Default (") + defaultResampler + _T(")")), SRCMODE_DEFAULT); + for(auto mode : resamplingModes) + { + m_CbnResampling.SetItemData(m_CbnResampling.AddString(CTrackApp::GetResamplingModeName(mode, 2, true)), mode); + } + m_CbnResampling.Invalidate(FALSE); + } + + if(updateAll) + { + const auto &specs = m_sndFile.GetModSpecifications(); + + // S3M HACK: ST3 will ignore speed 255, even though it can be used with Axx. + if(m_sndFile.GetType() == MOD_TYPE_S3M) + m_SpinSpeed.SetRange32(1, 254); + else + m_SpinSpeed.SetRange32(specs.speedMin, specs.speedMax); + + m_tempoMin = specs.GetTempoMin(); + m_tempoMax = specs.GetTempoMax(); + // IT Hack: There are legacy OpenMPT-made ITs out there which use a higher default speed than 255. + // Changing the upper tempo limit in the mod specs would break them, so do it here instead. + if(m_sndFile.GetType() == MOD_TYPE_IT && m_sndFile.m_nDefaultTempo <= TEMPO(255, 0)) + m_tempoMax.Set(255); + // Lower resolution for BPM above 256 + if(m_tempoMax >= TEMPO_SPLIT_THRESHOLD) + m_SliderTempo.SetRange(0, TempoSliderRange().GetInt()); + else + m_SliderTempo.SetRange(0, m_tempoMax.GetInt() - m_tempoMin.GetInt()); + m_EditTempo.AllowFractions(specs.hasFractionalTempo); + + const BOOL bIsNotMOD = (m_sndFile.GetType() != MOD_TYPE_MOD); + const BOOL bIsNotMOD_XM = ((bIsNotMOD) && (m_sndFile.GetType() != MOD_TYPE_XM)); + m_EditArtist.EnableWindow(specs.hasArtistName); + m_EditTempo.EnableWindow(bIsNotMOD); + m_SpinTempo.EnableWindow(bIsNotMOD); + GetDlgItem(IDC_BUTTON1)->EnableWindow(bIsNotMOD); + m_SliderTempo.EnableWindow(bIsNotMOD); + m_EditSpeed.EnableWindow(bIsNotMOD); + m_SpinSpeed.EnableWindow(bIsNotMOD); + const BOOL globalVol = bIsNotMOD_XM || m_sndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME; + m_SliderGlobalVol.EnableWindow(globalVol); + m_EditGlobalVol.EnableWindow(globalVol); + m_SpinGlobalVol.EnableWindow(globalVol); + m_EditSamplePA.EnableWindow(bIsNotMOD); + m_SpinSamplePA.EnableWindow(bIsNotMOD); + m_SliderVSTiVol.EnableWindow(bIsNotMOD); + m_EditVSTiVol.EnableWindow(bIsNotMOD); + m_SpinVSTiVol.EnableWindow(bIsNotMOD); + m_EditRestartPos.EnableWindow((specs.hasRestartPos || m_sndFile.Order().GetRestartPos() != 0)); + m_SpinRestartPos.EnableWindow(m_EditRestartPos.IsWindowEnabled()); + + //Note: Sample volume slider is not disabled for MOD + //on purpose (can be used to control play volume) + } + + if(updateAll || (hint.GetCategory() == HINTCAT_GLOBAL && hintType[HINT_MODCHANNELS])) + { + // MOD Type + mpt::ustring modType; + switch(m_sndFile.GetType()) + { + case MOD_TYPE_MOD: modType = U_("MOD (ProTracker)"); break; + case MOD_TYPE_S3M: modType = U_("S3M (Scream Tracker)"); break; + case MOD_TYPE_XM: modType = U_("XM (FastTracker 2)"); break; + case MOD_TYPE_IT: modType = U_("IT (Impulse Tracker)"); break; + case MOD_TYPE_MPT: modType = U_("MPTM (OpenMPT)"); break; + default: modType = MPT_UFORMAT("{} ({})")(mpt::ToUpperCase(m_sndFile.m_modFormat.type), m_sndFile.m_modFormat.formatName); break; + } + CString s; + s.Format(_T("%s, %u channel%s"), mpt::ToCString(modType).GetString(), m_sndFile.GetNumChannels(), (m_sndFile.GetNumChannels() != 1) ? _T("s") : _T("")); + m_BtnModType.SetWindowText(s); + } + + if (updateAll || (hint.GetCategory() == HINTCAT_SEQUENCE && hintType[HINT_MODSEQUENCE | HINT_RESTARTPOS])) + { + // Set max valid restart position + m_SpinRestartPos.SetRange32(0, std::max(m_sndFile.Order().GetRestartPos(), static_cast<ORDERINDEX>(m_sndFile.Order().GetLengthTailTrimmed() - 1))); + SetDlgItemInt(IDC_EDIT_RESTARTPOS, m_sndFile.Order().GetRestartPos(), FALSE); + } + if (updateAll || (hint.GetCategory() == HINTCAT_GENERAL && hintType[HINT_MODGENERAL])) + { + if (!m_editsLocked) + { + m_EditTitle.SetWindowText(mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.GetTitle())); + m_EditArtist.SetWindowText(mpt::ToCString(m_sndFile.m_songArtist)); + m_EditTempo.SetTempoValue(m_sndFile.m_nDefaultTempo); + SetDlgItemInt(IDC_EDIT_SPEED, m_sndFile.m_nDefaultSpeed, FALSE); + SetDlgItemInt(IDC_EDIT_GLOBALVOL, m_sndFile.m_nDefaultGlobalVolume / GetGlobalVolumeFactor(), FALSE); + SetDlgItemInt(IDC_EDIT_VSTIVOL, m_sndFile.m_nVSTiVolume, FALSE); + SetDlgItemInt(IDC_EDIT_SAMPLEPA, m_sndFile.m_nSamplePreAmp, FALSE); + } + + m_SliderGlobalVol.SetPos(MAX_SLIDER_GLOBAL_VOL - m_sndFile.m_nDefaultGlobalVolume); + m_SliderVSTiVol.SetPos(MAX_SLIDER_VSTI_VOL - m_sndFile.m_nVSTiVolume); + m_SliderSamplePreAmp.SetPos(MAX_SLIDER_SAMPLE_VOL - m_sndFile.m_nSamplePreAmp); + m_SliderTempo.SetPos(TempoToSlider(m_sndFile.m_nDefaultTempo)); + } + + if(updateAll || hintType == HINT_MPTOPTIONS || (hint.GetCategory() == HINTCAT_GENERAL && hintType[HINT_MODGENERAL])) + { + for(int i = 0; i < m_CbnResampling.GetCount(); ++i) + { + if(m_sndFile.m_nResampling == static_cast<ResamplingMode>(m_CbnResampling.GetItemData(i))) + { + m_CbnResampling.SetCurSel(i); + break; + } + } + } + + CheckDlgButton(IDC_CHECK_LOOPSONG, (TrackerSettings::Instance().gbLoopSong) ? TRUE : FALSE); + if (hintType[HINT_MPTOPTIONS]) + { + m_VuMeterLeft.InvalidateRect(NULL, FALSE); + m_VuMeterRight.InvalidateRect(NULL, FALSE); + } +} + + +void CCtrlGeneral::OnVScroll(UINT code, UINT pos, CScrollBar *pscroll) +{ + CDialog::OnVScroll(code, pos, pscroll); + + if (m_bInitialized) + { + CSliderCtrl* pSlider = (CSliderCtrl*) pscroll; + + if (pSlider == &m_SliderTempo) + { + const TEMPO tempo = SliderToTempo(m_SliderTempo.GetPos()); + if ((tempo >= m_sndFile.GetModSpecifications().GetTempoMin()) && (tempo <= m_sndFile.GetModSpecifications().GetTempoMax()) && (tempo != m_sndFile.m_nDefaultTempo)) + { + m_sndFile.m_nDefaultTempo = m_sndFile.m_PlayState.m_nMusicTempo = tempo; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + m_EditTempo.SetTempoValue(tempo); + } + } + + else if (pSlider == &m_SliderGlobalVol) + { + const UINT gv = MAX_SLIDER_GLOBAL_VOL - m_SliderGlobalVol.GetPos(); + if ((gv >= 0) && (gv <= MAX_SLIDER_GLOBAL_VOL) && (gv != m_sndFile.m_nDefaultGlobalVolume)) + { + m_sndFile.m_PlayState.m_nGlobalVolume = gv; + m_sndFile.m_nDefaultGlobalVolume = gv; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + SetDlgItemInt(IDC_EDIT_GLOBALVOL, m_sndFile.m_nDefaultGlobalVolume / GetGlobalVolumeFactor(), FALSE); + } + } + + else if (pSlider == &m_SliderSamplePreAmp) + { + const UINT spa = MAX_SLIDER_SAMPLE_VOL - m_SliderSamplePreAmp.GetPos(); + if ((spa >= 0) && (spa <= MAX_SLIDER_SAMPLE_VOL) && (spa != m_sndFile.m_nSamplePreAmp)) + { + m_sndFile.m_nSamplePreAmp = spa; + if(m_sndFile.GetType() != MOD_TYPE_MOD) + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + SetDlgItemInt(IDC_EDIT_SAMPLEPA, m_sndFile.m_nSamplePreAmp, FALSE); + } + } + + else if (pSlider == &m_SliderVSTiVol) + { + const UINT vv = MAX_SLIDER_VSTI_VOL - m_SliderVSTiVol.GetPos(); + if ((vv >= 0) && (vv <= MAX_SLIDER_VSTI_VOL) && (vv != m_sndFile.m_nVSTiVolume)) + { + m_sndFile.m_nVSTiVolume = vv; + m_sndFile.RecalculateGainForAllPlugs(); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + SetDlgItemInt(IDC_EDIT_VSTIVOL, m_sndFile.m_nVSTiVolume, FALSE); + } + } + + else if(pSlider == (CSliderCtrl*)&m_SpinTempo) + { + int pos32 = m_SpinTempo.GetPos32(); + if(pos32 != 0) + { + TEMPO newTempo; + if(m_sndFile.GetModSpecifications().hasFractionalTempo) + { + pos32 *= TEMPO::fractFact; + if(CMainFrame::GetMainFrame()->GetInputHandler()->CtrlPressed()) + pos32 /= 100; + else if(CMainFrame::GetMainFrame()->GetInputHandler()->ShiftPressed()) + pos32 /= 10; + newTempo.SetRaw(pos32); + } else + { + newTempo = TEMPO(pos32, 0); + } + newTempo += m_sndFile.m_nDefaultTempo; + Limit(newTempo, m_tempoMin, m_tempoMax); + m_sndFile.m_nDefaultTempo = m_sndFile.m_PlayState.m_nMusicTempo = newTempo; + m_modDoc.SetModified(); + LockControls(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + UnlockControls(); + m_SliderTempo.SetPos(TempoToSlider(newTempo)); + m_EditTempo.SetTempoValue(newTempo); + } + m_SpinTempo.SetPos(0); + } + } +} + + +void CCtrlGeneral::OnTitleChanged() +{ + if (!m_EditTitle.m_hWnd || !m_EditTitle.GetModify()) return; + + CString title; + m_EditTitle.GetWindowText(title); + if(m_sndFile.SetTitle(mpt::ToCharset(m_sndFile.GetCharsetInternal(), title))) + { + m_EditTitle.SetModify(FALSE); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + } +} + + +void CCtrlGeneral::OnArtistChanged() +{ + if (!m_EditArtist.m_hWnd || !m_EditArtist.GetModify()) return; + + mpt::ustring artist = GetWindowTextUnicode(m_EditArtist); + if(artist != m_sndFile.m_songArtist) + { + m_EditArtist.SetModify(FALSE); + m_sndFile.m_songArtist = artist; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(NULL, GeneralHint().General(), this); + } +} + + +void CCtrlGeneral::OnTempoChanged() +{ + if (m_bInitialized && m_EditTempo.GetWindowTextLength() > 0) + { + TEMPO tempo = m_EditTempo.GetTempoValue(); + Limit(tempo, m_tempoMin, m_tempoMax); + if(!m_sndFile.GetModSpecifications().hasFractionalTempo) tempo.Set(tempo.GetInt()); + if (tempo != m_sndFile.m_nDefaultTempo) + { + m_editsLocked = true; + m_EditTempo.SetModify(FALSE); + m_sndFile.m_nDefaultTempo = tempo; + m_sndFile.m_PlayState.m_nMusicTempo = tempo; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General()); + m_editsLocked = false; + } + } +} + + +void CCtrlGeneral::OnSpeedChanged() +{ + TCHAR s[16]; + if(m_bInitialized) + { + m_EditSpeed.GetWindowText(s, mpt::saturate_cast<int>(std::size(s))); + if (s[0]) + { + UINT n = ConvertStrTo<UINT>(s); + n = Clamp(n, m_sndFile.GetModSpecifications().speedMin, m_sndFile.GetModSpecifications().speedMax); + if (n != m_sndFile.m_nDefaultSpeed) + { + m_editsLocked = true; + m_EditSpeed.SetModify(FALSE); + m_sndFile.m_nDefaultSpeed = n; + m_sndFile.m_PlayState.m_nMusicSpeed = n; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + // Update envelope grid view + m_modDoc.UpdateAllViews(nullptr, InstrumentHint().Envelope(), this); + m_editsLocked = false; + } + } + } +} + + +void CCtrlGeneral::OnVSTiVolChanged() +{ + TCHAR s[16]; + if (m_bInitialized) + { + m_EditVSTiVol.GetWindowText(s, mpt::saturate_cast<int>(std::size(s))); + if (s[0]) + { + UINT n = ConvertStrTo<UINT>(s); + Limit(n, 0u, 2000u); + if (n != m_sndFile.m_nVSTiVolume) + { + m_editsLocked = true; + m_sndFile.m_nVSTiVolume = n; + m_sndFile.RecalculateGainForAllPlugs(); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + UpdateView(GeneralHint().General()); + m_editsLocked = false; + } + } + } +} + +void CCtrlGeneral::OnSamplePAChanged() +{ + TCHAR s[16]; + if(m_bInitialized) + { + m_EditSamplePA.GetWindowText(s, mpt::saturate_cast<int>(std::size(s))); + if (s[0]) + { + UINT n = ConvertStrTo<UINT>(s); + Limit(n, 0u, 2000u); + if (n != m_sndFile.m_nSamplePreAmp) + { + m_editsLocked = true; + m_sndFile.m_nSamplePreAmp = n; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + UpdateView(GeneralHint().General()); + m_editsLocked = false; + } + } + } +} + +void CCtrlGeneral::OnGlobalVolChanged() +{ + TCHAR s[16]; + if(m_bInitialized) + { + m_EditGlobalVol.GetWindowText(s, mpt::saturate_cast<int>(std::size(s))); + if (s[0]) + { + UINT n = ConvertStrTo<ORDERINDEX>(s) * GetGlobalVolumeFactor(); + Limit(n, 0u, 256u); + if (n != m_sndFile.m_nDefaultGlobalVolume) + { + m_editsLocked = true; + m_EditGlobalVol.SetModify(FALSE); + m_sndFile.m_nDefaultGlobalVolume = n; + m_sndFile.m_PlayState.m_nGlobalVolume = n; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + UpdateView(GeneralHint().General()); + m_editsLocked = false; + } + } + } +} + + +void CCtrlGeneral::OnRestartPosChanged() +{ + if(!m_bInitialized) + return; + TCHAR s[32]; + m_EditRestartPos.GetWindowText(s, mpt::saturate_cast<int>(std::size(s))); + if(!s[0]) + return; + + ORDERINDEX n = ConvertStrTo<ORDERINDEX>(s); + LimitMax(n, m_sndFile.Order().GetLastIndex()); + while(n > 0 && n < m_sndFile.Order().GetLastIndex() && !m_sndFile.Order().IsValidPat(n)) + n++; + + if(n == m_sndFile.Order().GetRestartPos()) + return; + + m_EditRestartPos.SetModify(FALSE); + m_sndFile.Order().SetRestartPos(n); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint(m_sndFile.Order.GetCurrentSequenceIndex()).RestartPos(), this); +} + + +void CCtrlGeneral::OnRestartPosDone() +{ + if(m_bInitialized) + SetDlgItemInt(IDC_EDIT_RESTARTPOS, m_sndFile.Order().GetRestartPos()); +} + + +void CCtrlGeneral::OnSongProperties() +{ + m_modDoc.OnSongProperties(); +} + + +void CCtrlGeneral::OnLoopSongChanged() +{ + m_modDoc.SetLoopSong(IsDlgButtonChecked(IDC_CHECK_LOOPSONG) != BST_UNCHECKED); +} + + +LRESULT CCtrlGeneral::OnUpdatePosition(WPARAM, LPARAM lParam) +{ + Notification *pnotify = (Notification *)lParam; + if (pnotify) + { + m_VuMeterLeft.SetVuMeter(pnotify->masterVUout[0] & (~Notification::ClipVU), pnotify->type[Notification::Stop]); + m_VuMeterRight.SetVuMeter(pnotify->masterVUout[1] & (~Notification::ClipVU), pnotify->type[Notification::Stop]); + } + return 0; +} + + +BOOL CCtrlGeneral::GetToolTipText(UINT uId, LPTSTR pszText) +{ + const TCHAR moreRecentMixModeNote[] = _T("Use a more recent mixmode to see dB offsets."); + if ((pszText) && (uId)) + { + const bool displayDBValues = m_sndFile.GetPlayConfig().getDisplayDBValues(); + const CWnd *wnd = GetDlgItem(uId); + const bool isEnabled = wnd ? (wnd->IsWindowEnabled() != FALSE) : true; // nullptr check is for a Wine bug workaround (https://bugs.openmpt.org/view.php?id=1553) + mpt::tstring notAvailable; + if(!isEnabled) + notAvailable = MPT_TFORMAT("Feature is not available in the {} format.")(mpt::ToWin(mpt::Charset::ASCII, mpt::ToUpperCaseAscii(m_sndFile.GetModSpecifications().fileExtension))); + + switch(uId) + { + case IDC_BUTTON_MODTYPE: + _tcscpy(pszText, _T("Song Properties")); + { + const auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(kcViewSongProperties, 0); + if (!keyText.IsEmpty()) + _tcscat(pszText, MPT_TFORMAT(" ({})")(keyText).c_str()); + } + return TRUE; + case IDC_BUTTON1: + if(isEnabled) + _tcscpy(pszText, _T("Click button multiple times to tap in the desired tempo.")); + else + _tcscpy(pszText, notAvailable.c_str()); + return TRUE; + case IDC_SLIDER_SAMPLEPREAMP: + _tcscpy(pszText, displayDBValues ? CModDoc::LinearToDecibels(m_sndFile.m_nSamplePreAmp, m_sndFile.GetPlayConfig().getNormalSamplePreAmp()).GetString() : moreRecentMixModeNote); + return TRUE; + case IDC_SLIDER_VSTIVOL: + if(isEnabled) + _tcscpy(pszText, displayDBValues ? CModDoc::LinearToDecibels(m_sndFile.m_nVSTiVolume, m_sndFile.GetPlayConfig().getNormalVSTiVol()).GetString() : moreRecentMixModeNote); + else + _tcscpy(pszText, notAvailable.c_str()); + return TRUE; + case IDC_SLIDER_GLOBALVOL: + if(isEnabled) + _tcscpy(pszText, displayDBValues ? CModDoc::LinearToDecibels(m_sndFile.m_PlayState.m_nGlobalVolume, m_sndFile.GetPlayConfig().getNormalGlobalVol()).GetString() : moreRecentMixModeNote); + else + _tcscpy(pszText, notAvailable.c_str()); + return TRUE; + case IDC_SLIDER_SONGTEMPO: + case IDC_EDIT_ARTIST: + case IDC_EDIT_TEMPO: + case IDC_EDIT_SPEED: + case IDC_EDIT_RESTARTPOS: + case IDC_EDIT_GLOBALVOL: + case IDC_EDIT_VSTIVOL: + if(isEnabled) + break; + _tcscpy(pszText, notAvailable.c_str()); + return TRUE; + } + } + return FALSE; + +} + + +void CCtrlGeneral::OnEnSetfocusEditSongtitle() +{ + m_EditTitle.SetLimitText(m_sndFile.GetModSpecifications().modNameLengthMax); +} + + +void CCtrlGeneral::OnResamplingChanged() +{ + int sel = m_CbnResampling.GetCurSel(); + if(sel >= 0) + { + m_sndFile.m_nResampling = static_cast<ResamplingMode>(m_CbnResampling.GetItemData(sel)); + if(m_sndFile.GetModSpecifications().hasDefaultResampling) + { + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, GeneralHint().General(), this); + } + } +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// CVuMeter +// + +BEGIN_MESSAGE_MAP(CVuMeter, CWnd) + ON_WM_PAINT() +END_MESSAGE_MAP() + + +void CVuMeter::OnPaint() +{ + CRect rect; + CPaintDC dc(this); + GetClientRect(&rect); + dc.FillSolidRect(rect.left, rect.top, rect.Width(), rect.Height(), RGB(0,0,0)); + m_lastDisplayedLevel = -1; + DrawVuMeter(dc, true); +} + + +void CVuMeter::SetVuMeter(int level, bool force) +{ + level >>= 8; + if (level != m_lastLevel) + { + DWORD curTime = timeGetTime(); + if(curTime - m_lastVuUpdateTime >= TrackerSettings::Instance().VuMeterUpdateInterval || force) + { + m_lastLevel = level; + CClientDC dc(this); + DrawVuMeter(dc); + m_lastVuUpdateTime = curTime; + } + } +} + + +void CVuMeter::DrawVuMeter(CDC &dc, bool /*redraw*/) +{ + CRect rect; + GetClientRect(&rect); + int vu = (m_lastLevel * (rect.bottom-rect.top)) >> 8; + int cy = rect.bottom - rect.top; + if (cy < 1) cy = 1; + for (int ry=rect.bottom-1; ry>rect.top; ry-=2) + { + int y0 = rect.bottom - ry; + int n = Clamp((y0 * NUM_VUMETER_PENS) / cy, 0, NUM_VUMETER_PENS - 1); + if (vu < y0) + n += NUM_VUMETER_PENS; + dc.FillSolidRect(rect.left, ry, rect.Width(), 1, CMainFrame::gcolrefVuMeter[n]); + } + m_lastDisplayedLevel = m_lastLevel; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.h b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.h new file mode 100644 index 00000000..96627076 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_gen.h @@ -0,0 +1,118 @@ +/* + * ctrl_gen.h + * ---------- + * Purpose: General tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +namespace Util { class MultimediaClock; } + +class CVuMeter final : public CWnd +{ +protected: + int m_lastDisplayedLevel = -1, m_lastLevel = 0; + DWORD m_lastVuUpdateTime; + +public: + CVuMeter() { m_lastVuUpdateTime = timeGetTime(); } + void SetVuMeter(int level, bool force = false); + +protected: + void DrawVuMeter(CDC &dc, bool redraw = false); + +protected: + afx_msg void OnPaint(); + DECLARE_MESSAGE_MAP(); +}; + + +class CCtrlGeneral final : public CModControlDlg +{ +public: + CCtrlGeneral(CModControlView &parent, CModDoc &document); + Setting<LONG> &GetSplitPosRef() override { return TrackerSettings::Instance().glGeneralWindowHeight; } + +private: + + // Determine how the global volume slider should be scaled to actual global volume. + // Display range for XM / S3M should be 0...64, for other formats it's 0...256. + uint32 GetGlobalVolumeFactor() const + { + return (m_sndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_S3M)) ? uint32(MAX_SLIDER_GLOBAL_VOL / 64) : uint32(MAX_SLIDER_GLOBAL_VOL / 128); + } + +public: + CEdit m_EditTitle, m_EditArtist; + CEdit m_EditSpeed, m_EditGlobalVol, m_EditRestartPos, + m_EditSamplePA, m_EditVSTiVol; + CNumberEdit m_EditTempo; + CButton m_BtnModType; + CSpinButtonCtrl m_SpinTempo, m_SpinSpeed, m_SpinGlobalVol, m_SpinRestartPos, + m_SpinSamplePA, m_SpinVSTiVol; + CComboBox m_CbnResampling; + + CSliderCtrl m_SliderTempo, m_SliderSamplePreAmp, m_SliderGlobalVol, m_SliderVSTiVol; + CVuMeter m_VuMeterLeft, m_VuMeterRight; + std::unique_ptr<Util::MultimediaClock> m_tapTimer; + bool m_editsLocked = false; + + TEMPO m_tempoMin, m_tempoMax; + + //{{AFX_VIRTUAL(CCtrlGeneral) + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange *pDX) override; // DDX/DDV support + void RecalcLayout() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + CRuntimeClass *GetAssociatedViewClass() override; + void OnActivatePage(LPARAM) override; + void OnDeactivatePage() override; + BOOL GetToolTipText(UINT uId, LPTSTR pszText) override; + //}}AFX_VIRTUAL + +protected: + static constexpr int MAX_SLIDER_GLOBAL_VOL = 256; + static constexpr int MAX_SLIDER_VSTI_VOL = 255; + static constexpr int MAX_SLIDER_SAMPLE_VOL = 255; + + // At this point, the tempo slider moves in more coarse steps to provide detailed values in the regions where it matters + static constexpr auto TEMPO_SPLIT_THRESHOLD = TEMPO(256, 0); + static constexpr int TEMPO_SPLIT_PRECISION = 3; + + TEMPO TempoSliderRange() const; + TEMPO SliderToTempo(int value) const; + int TempoToSlider(TEMPO tempo) const; + + //{{AFX_MSG(CCtrlGeneral) + afx_msg LRESULT OnUpdatePosition(WPARAM, LPARAM); + afx_msg void OnVScroll(UINT, UINT, CScrollBar *); + afx_msg void OnTapTempo(); + afx_msg void OnTitleChanged(); + afx_msg void OnArtistChanged(); + afx_msg void OnTempoChanged(); + afx_msg void OnSpeedChanged(); + afx_msg void OnGlobalVolChanged(); + afx_msg void OnVSTiVolChanged(); + afx_msg void OnSamplePAChanged(); + afx_msg void OnRestartPosChanged(); + afx_msg void OnRestartPosDone(); + afx_msg void OnSongProperties(); + afx_msg void OnLoopSongChanged(); + afx_msg void OnEnSetfocusEditSongtitle(); + afx_msg void OnResamplingChanged(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.cpp new file mode 100644 index 00000000..d2e78547 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.cpp @@ -0,0 +1,3213 @@ +/* + * Ctrl_ins.cpp + * ------------ + * Purpose: Instrument tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Childfrm.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "Globals.h" +#include "Ctrl_ins.h" +#include "View_ins.h" +#include "dlg_misc.h" +#include "TuningDialog.h" +#include "../common/misc_util.h" +#include "../common/mptStringBuffer.h" +#include "SelectPluginDialog.h" +#include "../common/mptFileIO.h" +#include "../common/FileReader.h" +#include "FileDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////// +// CNoteMapWnd + +BEGIN_MESSAGE_MAP(CNoteMapWnd, CStatic) + ON_WM_ERASEBKGND() + ON_WM_PAINT() + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_WM_LBUTTONDOWN() + ON_WM_MBUTTONDOWN() + ON_WM_RBUTTONDOWN() + ON_WM_LBUTTONDBLCLK() + ON_WM_MOUSEWHEEL() + ON_COMMAND(ID_NOTEMAP_TRANS_UP, &CNoteMapWnd::OnMapTransposeUp) + ON_COMMAND(ID_NOTEMAP_TRANS_DOWN, &CNoteMapWnd::OnMapTransposeDown) + ON_COMMAND(ID_NOTEMAP_COPY_NOTE, &CNoteMapWnd::OnMapCopyNote) + ON_COMMAND(ID_NOTEMAP_COPY_SMP, &CNoteMapWnd::OnMapCopySample) + ON_COMMAND(ID_NOTEMAP_RESET, &CNoteMapWnd::OnMapReset) + ON_COMMAND(ID_NOTEMAP_TRANSPOSE_SAMPLES, &CNoteMapWnd::OnTransposeSamples) + ON_COMMAND(ID_NOTEMAP_REMOVE, &CNoteMapWnd::OnMapRemove) + ON_COMMAND(ID_INSTRUMENT_SAMPLEMAP, &CNoteMapWnd::OnEditSampleMap) + ON_COMMAND(ID_INSTRUMENT_DUPLICATE, &CNoteMapWnd::OnInstrumentDuplicate) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CNoteMapWnd::OnCustomKeyMsg) + ON_COMMAND_RANGE(ID_NOTEMAP_EDITSAMPLE, ID_NOTEMAP_EDITSAMPLE + MAX_SAMPLES, &CNoteMapWnd::OnEditSample) +END_MESSAGE_MAP() + + +BOOL CNoteMapWnd::PreTranslateMessage(MSG* pMsg) +{ + if(!pMsg) + return TRUE; + uint32 wParam = static_cast<uint32>(pMsg->wParam); + + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler* ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = wParam; + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxInsNoteMap); + + if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + + // a bit of a hack... + ctx = (InputTargetContext)(kCtxCtrlInstruments); + + if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } + } + + //The key was not handled by a command, but it might still be useful + if (pMsg->message == WM_CHAR) //key is a character + { + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = CMainFrame::GetInputHandler()->GetKeyEventType(nFlags); + + if (kT == kKeyEventDown) + if (HandleChar(wParam)) + return true; + } + else if (pMsg->message == WM_KEYDOWN) //key is not a character + { + if(HandleNav(wParam)) + return true; + + // Handle Application (menu) key + if(wParam == VK_APPS) + { + CRect clientRect; + GetClientRect(clientRect); + clientRect.bottom = clientRect.top + mpt::align_up(clientRect.Height(), m_cyFont); + OnRButtonDown(0, clientRect.CenterPoint()); + } + } + else if (pMsg->message == WM_KEYUP) //stop notes on key release + { + if (((pMsg->wParam >= '0') && (pMsg->wParam <= '9')) || (pMsg->wParam == ' ') || + ((pMsg->wParam >= VK_NUMPAD0) && (pMsg->wParam <= VK_NUMPAD9))) + { + StopNote(); + return true; + } + } + + return CStatic::PreTranslateMessage(pMsg); +} + + +void CNoteMapWnd::PrepareUndo(const char *description) +{ + m_modDoc.GetInstrumentUndo().PrepareUndo(m_nInstrument, description); +} + + +void CNoteMapWnd::SetCurrentInstrument(INSTRUMENTINDEX nIns) +{ + if (nIns != m_nInstrument) + { + if (nIns < MAX_INSTRUMENTS) m_nInstrument = nIns; + + // create missing instrument if needed + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + if(m_nInstrument > 0 && m_nInstrument <= sndFile.GetNumInstruments() && sndFile.Instruments[m_nInstrument] == nullptr) + { + ModInstrument *instrument = sndFile.AllocateInstrument(m_nInstrument); + if(instrument == nullptr) + return; + m_modDoc.InitializeInstrument(instrument); + } + + Invalidate(FALSE); + UpdateAccessibleTitle(); + } +} + + +void CNoteMapWnd::SetCurrentNote(UINT nNote) +{ + if(nNote != m_nNote && ModCommand::IsNote(static_cast<ModCommand::NOTE>(nNote + NOTE_MIN))) + { + m_nNote = nNote; + Invalidate(FALSE); + UpdateAccessibleTitle(); + } +} + + +void CNoteMapWnd::OnPaint() +{ + CPaintDC dc(this); + + CRect rcClient; + GetClientRect(&rcClient); + const auto highlightBrush = GetSysColorBrush(COLOR_HIGHLIGHT), windowBrush = GetSysColorBrush(COLOR_WINDOW); + const auto colorText = GetSysColor(COLOR_WINDOWTEXT); + const auto colorTextSel = GetSysColor(COLOR_HIGHLIGHTTEXT); + auto oldFont = dc.SelectObject(CMainFrame::GetGUIFont()); + dc.SetBkMode(TRANSPARENT); + if ((m_cxFont <= 0) || (m_cyFont <= 0)) + { + CSize sz; + sz = dc.GetTextExtent(_T("C#0."), 4); + m_cyFont = sz.cy + 2; + m_cxFont = rcClient.right / 3; + } + dc.IntersectClipRect(&rcClient); + + const CSoundFile &sndFile = m_modDoc.GetSoundFile(); + if (m_cxFont > 0 && m_cyFont > 0) + { + const bool focus = (::GetFocus() == m_hWnd); + const ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + CRect rect; + + int nNotes = (rcClient.bottom + m_cyFont - 1) / m_cyFont; + int nPos = m_nNote - (nNotes/2); + int ypaint = 0; + mpt::winstring s; + for (int ynote=0; ynote<nNotes; ynote++, ypaint+=m_cyFont, nPos++) + { + // Note + bool isValidPos = (nPos >= 0) && (nPos < NOTE_MAX - NOTE_MIN + 1); + if (isValidPos) + { + s = mpt::ToWin(sndFile.GetNoteName(static_cast<ModCommand::NOTE>(nPos + 1), m_nInstrument)); + s.resize(4); + } else + { + s.clear(); + } + rect.SetRect(0, ypaint, m_cxFont, ypaint+m_cyFont); + DrawButtonRect(dc, &rect, s.c_str(), FALSE, FALSE); + // Mapped Note + bool highlight = ((focus) && (nPos == (int)m_nNote)); + rect.left = rect.right; + rect.right = m_cxFont*2-1; + s = _T("..."); + if(pIns != nullptr && isValidPos && (pIns->NoteMap[nPos] != NOTE_NONE)) + { + ModCommand::NOTE n = pIns->NoteMap[nPos]; + if(ModCommand::IsNote(n)) + { + s = mpt::ToWin(sndFile.GetNoteName(n, m_nInstrument)); + s.resize(4); + } else + { + s = _T("???"); + } + } + FillRect(dc, &rect, highlight ? highlightBrush : windowBrush); + if(nPos == (int)m_nNote && !m_bIns) + { + rect.InflateRect(-1, -1); + dc.DrawFocusRect(&rect); + rect.InflateRect(1, 1); + } + dc.SetTextColor(highlight ? colorTextSel : colorText); + dc.DrawText(s.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_NOPREFIX); + // Sample + highlight = (focus && nPos == (int)m_nNote); + rect.left = rcClient.left + m_cxFont * 2 + 3; + rect.right = rcClient.right; + s = _T(" .."); + if(pIns && nPos >= 0 && nPos < NOTE_MAX && pIns->Keyboard[nPos]) + { + s = mpt::tfmt::right(3, mpt::tfmt::dec(pIns->Keyboard[nPos])); + } + FillRect(dc, &rect, highlight ? highlightBrush : windowBrush); + if((nPos == (int)m_nNote) && (m_bIns)) + { + rect.InflateRect(-1, -1); + dc.DrawFocusRect(&rect); + rect.InflateRect(1, 1); + } + dc.SetTextColor((highlight) ? colorTextSel : colorText); + dc.DrawText(s.c_str(), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_NOPREFIX); + } + rect.SetRect(rcClient.left + m_cxFont * 2 - 1, rcClient.top, rcClient.left + m_cxFont * 2 + 3, ypaint); + DrawButtonRect(dc, &rect, _T(""), FALSE, FALSE); + if (ypaint < rcClient.bottom) + { + rect.SetRect(rcClient.left, ypaint, rcClient.right, rcClient.bottom); + FillRect(dc, &rect, GetSysColorBrush(COLOR_BTNFACE)); + } + } + dc.SelectObject(oldFont); +} + + +void CNoteMapWnd::OnSetFocus(CWnd *pOldWnd) +{ + CStatic::OnSetFocus(pOldWnd); + Invalidate(FALSE); + CMainFrame::GetMainFrame()->m_pNoteMapHasFocus = this; + m_undo = true; +} + + +void CNoteMapWnd::OnKillFocus(CWnd *pNewWnd) +{ + CStatic::OnKillFocus(pNewWnd); + Invalidate(FALSE); + CMainFrame::GetMainFrame()->m_pNoteMapHasFocus = nullptr; +} + + +void CNoteMapWnd::OnLButtonDown(UINT, CPoint pt) +{ + if ((pt.x >= m_cxFont) && (pt.x < m_cxFont*2) && (m_bIns)) + { + m_bIns = false; + Invalidate(FALSE); + } + if ((pt.x > m_cxFont*2) && (pt.x <= m_cxFont*3) && (!m_bIns)) + { + m_bIns = true; + Invalidate(FALSE); + } + if ((pt.x >= 0) && (m_cyFont)) + { + CRect rcClient; + GetClientRect(&rcClient); + int nNotes = (rcClient.bottom + m_cyFont - 1) / m_cyFont; + int n = (pt.y / m_cyFont) + m_nNote - (nNotes/2); + if(n >= 0) + { + SetCurrentNote(n); + } + } + SetFocus(); +} + + +void CNoteMapWnd::OnLButtonDblClk(UINT, CPoint) +{ + // Double-click edits sample map + OnEditSampleMap(); +} + + +void CNoteMapWnd::OnRButtonDown(UINT, CPoint pt) +{ + CInputHandler* ih = CMainFrame::GetInputHandler(); + + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if (pIns) + { + HMENU hMenu = ::CreatePopupMenu(); + HMENU hSubMenu = ::CreatePopupMenu(); + + if (hMenu) + { + AppendMenu(hMenu, MF_STRING, ID_INSTRUMENT_SAMPLEMAP, ih->GetKeyTextFromCommand(kcInsNoteMapEditSampleMap, _T("Edit Sample &Map"))); + if (hSubMenu) + { + // Create sub menu with a list of all samples that are referenced by this instrument. + for(auto sample : pIns->GetSamples()) + { + if(sample <= sndFile.GetNumSamples()) + { + AppendMenu(hSubMenu, MF_STRING, ID_NOTEMAP_EDITSAMPLE + sample, MPT_CFORMAT("{}: {}")(sample, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[sample]))); + } + } + + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(hSubMenu), ih->GetKeyTextFromCommand(kcInsNoteMapEditSample, _T("&Edit Sample"))); + AppendMenu(hMenu, MF_SEPARATOR, 0, NULL); + } + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_COPY_SMP, ih->GetKeyTextFromCommand(kcInsNoteMapCopyCurrentSample, MPT_CFORMAT("Map All Notes to &Sample {}")(pIns->Keyboard[m_nNote]))); + + if(sndFile.GetType() != MOD_TYPE_XM) + { + if(ModCommand::IsNote(pIns->NoteMap[m_nNote])) + { + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_COPY_NOTE, ih->GetKeyTextFromCommand(kcInsNoteMapCopyCurrentNote, MPT_CFORMAT("Map All &Notes to {}")(mpt::ToCString(sndFile.GetNoteName(pIns->NoteMap[m_nNote], m_nInstrument))))); + } + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_TRANS_UP, ih->GetKeyTextFromCommand(kcInsNoteMapTransposeUp, _T("Transpose Map &Up"))); + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_TRANS_DOWN, ih->GetKeyTextFromCommand(kcInsNoteMapTransposeDown, _T("Transpose Map &Down"))); + } + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_RESET, ih->GetKeyTextFromCommand(kcInsNoteMapReset, _T("&Reset Note Mapping"))); + AppendMenu(hMenu, MF_STRING | (pIns->CanConvertToDefaultNoteMap().empty() ? MF_GRAYED : 0), ID_NOTEMAP_TRANSPOSE_SAMPLES, ih->GetKeyTextFromCommand(kcInsNoteMapTransposeSamples, _T("&Transpose Samples / Reset Map"))); + AppendMenu(hMenu, MF_STRING, ID_NOTEMAP_REMOVE, ih->GetKeyTextFromCommand(kcInsNoteMapRemove, _T("Remo&ve All Samples"))); + AppendMenu(hMenu, MF_STRING, ID_INSTRUMENT_DUPLICATE, ih->GetKeyTextFromCommand(kcInstrumentCtrlDuplicate, _T("Duplicate &Instrument"))); + SetMenuDefaultItem(hMenu, ID_INSTRUMENT_SAMPLEMAP, FALSE); + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + if (hSubMenu) ::DestroyMenu(hSubMenu); + } + } +} + + +BOOL CNoteMapWnd::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + SetCurrentNote(m_nNote - mpt::signum(zDelta)); + return CStatic::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CNoteMapWnd::OnMapCopyNote() +{ + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if (pIns) + { + m_undo = true; + bool bModified = false; + auto n = pIns->NoteMap[m_nNote]; + for (auto &key : pIns->NoteMap) if (key != n) + { + if(!bModified) + { + PrepareUndo("Map Notes"); + } + key = n; + bModified = true; + } + if (bModified) + { + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + } + } +} + +void CNoteMapWnd::OnMapCopySample() +{ + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if (pIns) + { + m_undo = true; + bool bModified = false; + auto n = pIns->Keyboard[m_nNote]; + for (auto &sample : pIns->Keyboard) if (sample != n) + { + if(!bModified) + { + PrepareUndo("Map Samples"); + } + sample = n; + bModified = true; + } + if (bModified) + { + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + } +} + + +void CNoteMapWnd::OnMapReset() +{ + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if (pIns) + { + m_undo = true; + bool modified = false; + for (size_t i = 0; i < std::size(pIns->NoteMap); i++) if (pIns->NoteMap[i] != i + 1) + { + if(!modified) + { + PrepareUndo("Reset Note Map"); + } + pIns->NoteMap[i] = static_cast<ModCommand::NOTE>(i + 1); + modified = true; + } + if(modified) + { + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + } +} + + +void CNoteMapWnd::OnTransposeSamples() +{ + auto &sndFile = m_modDoc.GetSoundFile(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if(!pIns) + return; + const auto samples = pIns->CanConvertToDefaultNoteMap(); + if(samples.empty()) + return; + + PrepareUndo("Transpose Samples"); + for(const auto &[smp, transpose] : samples) + { + if(smp > sndFile.GetNumSamples()) + continue; + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_none, "Transpose"); + auto &sample = sndFile.GetSample(smp); + if(sndFile.UseFinetuneAndTranspose()) + sample.RelativeTone += transpose; + else + sample.Transpose(transpose / 12.0); + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info(), &m_pParent); + } + pIns->ResetNoteMap(); + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); +} + + +void CNoteMapWnd::OnMapRemove() +{ + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if (pIns) + { + m_undo = true; + bool modified = false; + for (auto &sample: pIns->Keyboard) if (sample != 0) + { + if(!modified) + { + PrepareUndo("Remove Sample Assocations"); + } + sample = 0; + modified = true; + } + if(modified) + { + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + } +} + + +void CNoteMapWnd::OnMapTransposeUp() +{ + MapTranspose(1); +} + + +void CNoteMapWnd::OnMapTransposeDown() +{ + MapTranspose(-1); +} + + +void CNoteMapWnd::MapTranspose(int nAmount) +{ + if(nAmount == 0 || m_modDoc.GetModType() == MOD_TYPE_XM) return; + + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if((nAmount == 12 || nAmount == -12)) + { + // Special case for instrument-specific tunings + nAmount = m_modDoc.GetInstrumentGroupSize(m_nInstrument) * mpt::signum(nAmount); + } + + m_undo = true; + if (pIns) + { + bool modified = false; + for(NOTEINDEXTYPE i = 0; i < NOTE_MAX; i++) + { + int n = pIns->NoteMap[i]; + if ((n > NOTE_MIN && nAmount < 0) || (n < NOTE_MAX && nAmount > 0)) + { + n = Clamp(n + nAmount, NOTE_MIN, NOTE_MAX); + if(n != pIns->NoteMap[i]) + { + if(!modified) + { + PrepareUndo("Transpose Map"); + } + pIns->NoteMap[i] = static_cast<uint8>(n); + modified = true; + } + } + } + if(modified) + { + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + } +} + + +void CNoteMapWnd::OnEditSample(UINT nID) +{ + UINT nSample = nID - ID_NOTEMAP_EDITSAMPLE; + m_pParent.EditSample(nSample); +} + + +void CNoteMapWnd::OnEditSampleMap() +{ + m_undo = true; + m_pParent.PostMessage(WM_COMMAND, ID_INSTRUMENT_SAMPLEMAP); +} + + +void CNoteMapWnd::OnInstrumentDuplicate() +{ + m_undo = true; + m_pParent.PostMessage(WM_COMMAND, ID_INSTRUMENT_DUPLICATE); +} + + +LRESULT CNoteMapWnd::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) +{ + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + + // Handle notes + + if (wParam >= kcInsNoteMapStartNotes && wParam <= kcInsNoteMapEndNotes) + { + // Special case: number keys override notes if we're in the sample # column. + const auto key = KeyCombination::FromLPARAM(lParam).KeyCode(); + if(m_bIns && ((key >= '0' && key <= '9') || (key == ' '))) + HandleChar(key); + else + EnterNote(m_modDoc.GetNoteWithBaseOctave(static_cast<int>(wParam - kcInsNoteMapStartNotes), m_nInstrument)); + + return wParam; + } + + if (wParam >= kcInsNoteMapStartNoteStops && wParam <= kcInsNoteMapEndNoteStops) + { + StopNote(); + return wParam; + } + + // Other shortcuts + + switch(wParam) + { + case kcInsNoteMapTransposeDown: MapTranspose(-1); return wParam; + case kcInsNoteMapTransposeUp: MapTranspose(1); return wParam; + case kcInsNoteMapTransposeOctDown: MapTranspose(-12); return wParam; + case kcInsNoteMapTransposeOctUp: MapTranspose(12); return wParam; + + case kcInsNoteMapCopyCurrentSample: OnMapCopySample(); return wParam; + case kcInsNoteMapCopyCurrentNote: OnMapCopyNote(); return wParam; + case kcInsNoteMapReset: OnMapReset(); return wParam; + case kcInsNoteMapTransposeSamples: OnTransposeSamples(); return wParam; + case kcInsNoteMapRemove: OnMapRemove(); return wParam; + + case kcInsNoteMapEditSample: if(pIns) OnEditSample(pIns->Keyboard[m_nNote] + ID_NOTEMAP_EDITSAMPLE); return wParam; + case kcInsNoteMapEditSampleMap: OnEditSampleMap(); return wParam; + + // Parent shortcuts (also displayed in context menu of this control) + case kcInstrumentCtrlDuplicate: OnInstrumentDuplicate(); return wParam; + case kcNextInstrument: m_pParent.PostMessage(WM_COMMAND, ID_NEXTINSTRUMENT); return wParam; + case kcPrevInstrument: m_pParent.PostMessage(WM_COMMAND, ID_PREVINSTRUMENT); return wParam; + } + + return kcNull; +} + +void CNoteMapWnd::EnterNote(UINT note) +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if ((pIns) && (m_nNote < NOTE_MAX)) + { + if (!m_bIns && (sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) + { + UINT n = pIns->NoteMap[m_nNote]; + bool ok = false; + if ((note >= sndFile.GetModSpecifications().noteMin) && (note <= sndFile.GetModSpecifications().noteMax)) + { + n = note; + ok = true; + } + if (n != pIns->NoteMap[m_nNote]) + { + StopNote(); // Stop old note according to current instrument settings + pIns->NoteMap[m_nNote] = static_cast<ModCommand::NOTE>(n); + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + if(ok) + { + PlayNote(m_nNote); + } + } + } +} + +bool CNoteMapWnd::HandleChar(WPARAM c) +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if ((pIns) && (m_nNote < NOTE_MAX)) + { + + if ((m_bIns) && (((c >= '0') && (c <= '9')) || (c == ' '))) //in sample # column + { + UINT n = m_nOldIns; + if (c != ' ') + { + n = (10 * pIns->Keyboard[m_nNote] + (c - '0')) % 10000; + if ((n >= MAX_SAMPLES) || ((sndFile.m_nSamples < 1000) && (n >= 1000))) n = (n % 1000); + if ((n >= MAX_SAMPLES) || ((sndFile.m_nSamples < 100) && (n >= 100))) n = (n % 100); else + if ((n > 31) && (sndFile.m_nSamples < 32) && (n % 10)) n = (n % 10); + } + + if (n != pIns->Keyboard[m_nNote]) + { + if(m_undo) + { + PrepareUndo("Enter Instrument"); + m_undo = false; + } + StopNote(); // Stop old note according to current instrument settings + pIns->Keyboard[m_nNote] = static_cast<SAMPLEINDEX>(n); + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + PlayNote(m_nNote); + } + + if (c == ' ') + { + SetCurrentNote(m_nNote + 1); + PlayNote(m_nNote); + } + return true; + } + + else if ((!m_bIns) && (sndFile.m_nType & (MOD_TYPE_IT | MOD_TYPE_MPT))) //in note column + { + uint32 n = pIns->NoteMap[m_nNote]; + + if ((c >= '0') && (c <= '9')) + { + if (n) + n = static_cast<uint32>(((n - 1) % 12) + (c - '0') * 12 + 1); + else + n = static_cast<uint32>((m_nNote % 12) + (c - '0') * 12 + 1); + } else if (c == ' ') + { + n = (m_nOldNote) ? m_nOldNote : m_nNote+1; + } + + if (n != pIns->NoteMap[m_nNote]) + { + if(m_undo) + { + PrepareUndo("Enter Note"); + m_undo = false; + } + + StopNote(); // Stop old note according to current instrument settings + pIns->NoteMap[m_nNote] = static_cast<ModCommand::NOTE>(n); + m_pParent.SetModified(InstrumentHint().Info(), false); + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + + if(c == ' ') + { + SetCurrentNote(m_nNote + 1); + } + + PlayNote(m_nNote); + + return true; + } + } + return false; +} + +bool CNoteMapWnd::HandleNav(WPARAM k) +{ + bool redraw = false; + + //HACK: handle numpad (convert numpad number key to normal number key) + if ((k >= VK_NUMPAD0) && (k <= VK_NUMPAD9)) return HandleChar(k-VK_NUMPAD0+'0'); + + switch(k) + { + case VK_RIGHT: + if (!m_bIns) { m_bIns = true; redraw = true; } else + if (m_nNote < NOTE_MAX - NOTE_MIN) { m_nNote++; m_bIns = false; redraw = true; } + break; + case VK_LEFT: + if (m_bIns) { m_bIns = false; redraw = true; } else + if (m_nNote) { m_nNote--; m_bIns = true; redraw = true; } + break; + case VK_UP: + if (m_nNote > 0) { m_nNote--; redraw = true; } + break; + case VK_DOWN: + if (m_nNote < NOTE_MAX - 1) { m_nNote++; redraw = true; } + break; + case VK_PRIOR: + if (m_nNote > 3) { m_nNote -= 3; redraw = true; } else + if (m_nNote > 0) { m_nNote = 0; redraw = true; } + break; + case VK_NEXT: + if (m_nNote+3 < NOTE_MAX) { m_nNote += 3; redraw = true; } else + if (m_nNote < NOTE_MAX - NOTE_MIN) { m_nNote = NOTE_MAX - NOTE_MIN; redraw = true; } + break; + case VK_HOME: + if(m_nNote > 0) { m_nNote = 0; redraw = true; } + break; + case VK_END: + if(m_nNote < NOTE_MAX - NOTE_MIN) { m_nNote = NOTE_MAX - NOTE_MIN; redraw = true; } + break; +// case VK_TAB: +// return true; + case VK_RETURN: + { + ModInstrument *pIns = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if(pIns) + { + if (m_bIns) + m_nOldIns = pIns->Keyboard[m_nNote]; + else + m_nOldNote = pIns->NoteMap[m_nNote]; + } + } + + return true; + default: + return false; + } + if(redraw) + { + m_undo = true; + Invalidate(FALSE); + UpdateAccessibleTitle(); + } + + return true; +} + + +void CNoteMapWnd::PlayNote(UINT note) +{ + if(m_nPlayingNote != NOTE_NONE) + { + // No polyphony in notemap window + StopNote(); + } + m_nPlayingNote = static_cast<ModCommand::NOTE>(note + NOTE_MIN); + m_noteChannel = m_modDoc.PlayNote(PlayNoteParam(m_nPlayingNote).Instrument(m_nInstrument)); +} + + +void CNoteMapWnd::StopNote() +{ + if(!ModCommand::IsNote(m_nPlayingNote)) return; + + m_modDoc.NoteOff(m_nPlayingNote, true, m_nInstrument, m_noteChannel); + m_nPlayingNote = NOTE_NONE; +} + + +void CNoteMapWnd::UpdateAccessibleTitle() +{ + CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this); +} + + +// Accessible description for screen readers +HRESULT CNoteMapWnd::get_accName(VARIANT varChild, BSTR *pszName) +{ + const auto *ins = m_modDoc.GetSoundFile().Instruments[m_nInstrument]; + if(!ins || m_nNote >= std::size(ins->NoteMap)) + return CStatic::get_accName(varChild, pszName); + + const auto &sndFile = m_modDoc.GetSoundFile(); + CString str = mpt::ToCString(sndFile.GetNoteName(static_cast<ModCommand::NOTE>(m_nNote + NOTE_MIN), m_nInstrument)) + _T(": "); + if(ins->Keyboard[m_nNote] == 0) + { + str += _T("no sample"); + } else + { + auto mappedNote = ins->NoteMap[m_nNote]; + str += MPT_CFORMAT("sample {} at {}")(ins->Keyboard[m_nNote], mpt::ToCString(sndFile.GetNoteName(mappedNote, m_nInstrument))); + } + + *pszName = str.AllocSysString(); + return S_OK; +} + + +///////////////////////////////////////////////////////////////////////// +// CCtrlInstruments + +#define MAX_ATTACK_LENGTH 2001 +#define MAX_ATTACK_VALUE (MAX_ATTACK_LENGTH - 1) // 16 bit unsigned max + +BEGIN_MESSAGE_MAP(CCtrlInstruments, CModControlDlg) + //{{AFX_MSG_MAP(CCtrlInstruments) + ON_WM_VSCROLL() + ON_WM_HSCROLL() + ON_WM_XBUTTONUP() + ON_NOTIFY(TBN_DROPDOWN, IDC_TOOLBAR1, &CCtrlInstruments::OnTbnDropDownToolBar) + ON_COMMAND(IDC_INSTRUMENT_NEW, &CCtrlInstruments::OnInstrumentNew) + ON_COMMAND(IDC_INSTRUMENT_OPEN, &CCtrlInstruments::OnInstrumentOpen) + ON_COMMAND(IDC_INSTRUMENT_SAVEAS, &CCtrlInstruments::OnInstrumentSave) + ON_COMMAND(IDC_SAVE_ONE, &CCtrlInstruments::OnInstrumentSaveOne) + ON_COMMAND(IDC_SAVE_ALL, &CCtrlInstruments::OnInstrumentSaveAll) + ON_COMMAND(IDC_INSTRUMENT_PLAY, &CCtrlInstruments::OnInstrumentPlay) + ON_COMMAND(ID_PREVINSTRUMENT, &CCtrlInstruments::OnPrevInstrument) + ON_COMMAND(ID_NEXTINSTRUMENT, &CCtrlInstruments::OnNextInstrument) + ON_COMMAND(ID_INSTRUMENT_DUPLICATE, &CCtrlInstruments::OnInstrumentDuplicate) + ON_COMMAND(IDC_CHECK1, &CCtrlInstruments::OnSetPanningChanged) + ON_COMMAND(IDC_CHECK2, &CCtrlInstruments::OnEnableCutOff) + ON_COMMAND(IDC_CHECK3, &CCtrlInstruments::OnEnableResonance) + ON_COMMAND(IDC_INSVIEWPLG, &CCtrlInstruments::TogglePluginEditor) + ON_EN_CHANGE(IDC_EDIT_INSTRUMENT, &CCtrlInstruments::OnInstrumentChanged) + ON_EN_CHANGE(IDC_SAMPLE_NAME, &CCtrlInstruments::OnNameChanged) + ON_EN_CHANGE(IDC_SAMPLE_FILENAME, &CCtrlInstruments::OnFileNameChanged) + ON_EN_CHANGE(IDC_EDIT7, &CCtrlInstruments::OnFadeOutVolChanged) + ON_EN_CHANGE(IDC_EDIT8, &CCtrlInstruments::OnGlobalVolChanged) + ON_EN_CHANGE(IDC_EDIT9, &CCtrlInstruments::OnPanningChanged) + ON_EN_CHANGE(IDC_EDIT10, &CCtrlInstruments::OnMPRChanged) + ON_EN_KILLFOCUS(IDC_EDIT10, &CCtrlInstruments::OnMPRKillFocus) + ON_EN_CHANGE(IDC_EDIT11, &CCtrlInstruments::OnMBKChanged) + ON_EN_CHANGE(IDC_EDIT15, &CCtrlInstruments::OnPPSChanged) + ON_EN_CHANGE(IDC_PITCHWHEELDEPTH, &CCtrlInstruments::OnPitchWheelDepthChanged) + ON_EN_CHANGE(IDC_EDIT2, &CCtrlInstruments::OnAttackChanged) + + ON_EN_SETFOCUS(IDC_SAMPLE_NAME, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_SAMPLE_FILENAME, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT7, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT8, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT9, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT10, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT11, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT15, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_PITCHWHEELDEPTH, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT2, &CCtrlInstruments::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT_PITCHTEMPOLOCK, &CCtrlInstruments::OnEditFocus) + + ON_CBN_SELCHANGE(IDC_COMBO1, &CCtrlInstruments::OnNNAChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CCtrlInstruments::OnDCTChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CCtrlInstruments::OnDCAChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &CCtrlInstruments::OnPPCChanged) + ON_CBN_SELCHANGE(IDC_COMBO5, &CCtrlInstruments::OnMCHChanged) + ON_CBN_SELCHANGE(IDC_COMBO6, &CCtrlInstruments::OnMixPlugChanged) + ON_CBN_DROPDOWN(IDC_COMBO6, &CCtrlInstruments::OnOpenPluginList) + ON_CBN_SELCHANGE(IDC_COMBO9, &CCtrlInstruments::OnResamplingChanged) + ON_CBN_SELCHANGE(IDC_FILTERMODE, &CCtrlInstruments::OnFilterModeChanged) + ON_CBN_SELCHANGE(IDC_PLUGIN_VOLUMESTYLE, &CCtrlInstruments::OnPluginVolumeHandlingChanged) + ON_COMMAND(IDC_PLUGIN_VELOCITYSTYLE, &CCtrlInstruments::OnPluginVelocityHandlingChanged) + ON_COMMAND(ID_INSTRUMENT_SAMPLEMAP, &CCtrlInstruments::OnEditSampleMap) + ON_CBN_SELCHANGE(IDC_COMBOTUNING, &CCtrlInstruments::OnCbnSelchangeCombotuning) + ON_EN_CHANGE(IDC_EDIT_PITCHTEMPOLOCK, &CCtrlInstruments::OnEnChangeEditPitchTempoLock) + ON_BN_CLICKED(IDC_CHECK_PITCHTEMPOLOCK, &CCtrlInstruments::OnBnClickedCheckPitchtempolock) + ON_EN_KILLFOCUS(IDC_EDIT_PITCHTEMPOLOCK, &CCtrlInstruments::OnEnKillFocusEditPitchTempoLock) + ON_EN_KILLFOCUS(IDC_EDIT7, &CCtrlInstruments::OnEnKillFocusEditFadeOut) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +void CCtrlInstruments::DoDataExchange(CDataExchange* pDX) +{ + CModControlDlg::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CCtrlInstruments) + DDX_Control(pDX, IDC_TOOLBAR1, m_ToolBar); + DDX_Control(pDX, IDC_NOTEMAP, m_NoteMap); + DDX_Control(pDX, IDC_SAMPLE_NAME, m_EditName); + DDX_Control(pDX, IDC_SAMPLE_FILENAME, m_EditFileName); + DDX_Control(pDX, IDC_SPIN_INSTRUMENT, m_SpinInstrument); + DDX_Control(pDX, IDC_COMBO1, m_ComboNNA); + DDX_Control(pDX, IDC_COMBO2, m_ComboDCT); + DDX_Control(pDX, IDC_COMBO3, m_ComboDCA); + DDX_Control(pDX, IDC_COMBO4, m_ComboPPC); + DDX_Control(pDX, IDC_COMBO5, m_CbnMidiCh); + DDX_Control(pDX, IDC_COMBO6, m_CbnMixPlug); + DDX_Control(pDX, IDC_COMBO9, m_CbnResampling); + DDX_Control(pDX, IDC_FILTERMODE, m_CbnFilterMode); + DDX_Control(pDX, IDC_EDIT7, m_EditFadeOut); + DDX_Control(pDX, IDC_SPIN7, m_SpinFadeOut); + DDX_Control(pDX, IDC_SPIN8, m_SpinGlobalVol); + DDX_Control(pDX, IDC_SPIN9, m_SpinPanning); + DDX_Control(pDX, IDC_SPIN10, m_SpinMidiPR); + DDX_Control(pDX, IDC_SPIN11, m_SpinMidiBK); + DDX_Control(pDX, IDC_SPIN12, m_SpinPPS); + DDX_Control(pDX, IDC_EDIT8, m_EditGlobalVol); + DDX_Control(pDX, IDC_EDIT9, m_EditPanning); + DDX_Control(pDX, IDC_CHECK1, m_CheckPanning); + DDX_Control(pDX, IDC_CHECK2, m_CheckCutOff); + DDX_Control(pDX, IDC_CHECK3, m_CheckResonance); + DDX_Control(pDX, IDC_SLIDER1, m_SliderVolSwing); + DDX_Control(pDX, IDC_SLIDER2, m_SliderPanSwing); + DDX_Control(pDX, IDC_SLIDER3, m_SliderCutOff); + DDX_Control(pDX, IDC_SLIDER4, m_SliderResonance); + DDX_Control(pDX, IDC_SLIDER6, m_SliderCutSwing); + DDX_Control(pDX, IDC_SLIDER7, m_SliderResSwing); + DDX_Control(pDX, IDC_SLIDER5, m_SliderAttack); + DDX_Control(pDX, IDC_SPIN1, m_SpinAttack); + DDX_Control(pDX, IDC_COMBOTUNING, m_ComboTuning); + DDX_Control(pDX, IDC_CHECK_PITCHTEMPOLOCK, m_CheckPitchTempoLock); + DDX_Control(pDX, IDC_PLUGIN_VOLUMESTYLE, m_CbnPluginVolumeHandling); + DDX_Control(pDX, IDC_PLUGIN_VELOCITYSTYLE, velocityStyle); + DDX_Control(pDX, IDC_SPIN2, m_SpinPWD); + //}}AFX_DATA_MAP +} + + +CCtrlInstruments::CCtrlInstruments(CModControlView &parent, CModDoc &document) + : CModControlDlg(parent, document) + , m_NoteMap(*this, document) +{ + m_nLockCount = 1; +} + + +CRuntimeClass *CCtrlInstruments::GetAssociatedViewClass() +{ + return RUNTIME_CLASS(CViewInstrument); +} + + +void CCtrlInstruments::OnEditFocus() +{ + m_startedEdit = false; +} + + +BOOL CCtrlInstruments::OnInitDialog() +{ + CModControlDlg::OnInitDialog(); + m_bInitialized = FALSE; + SetRedraw(FALSE); + + m_ToolBar.SetExtendedStyle(m_ToolBar.GetExtendedStyle() | TBSTYLE_EX_DRAWDDARROWS); + m_ToolBar.Init(CMainFrame::GetMainFrame()->m_PatternIcons,CMainFrame::GetMainFrame()->m_PatternIconsDisabled); + m_ToolBar.AddButton(IDC_INSTRUMENT_NEW, TIMAGE_INSTR_NEW, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN); + m_ToolBar.AddButton(IDC_INSTRUMENT_OPEN, TIMAGE_OPEN); + m_ToolBar.AddButton(IDC_INSTRUMENT_SAVEAS, TIMAGE_SAVE, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN); + m_ToolBar.AddButton(IDC_INSTRUMENT_PLAY, TIMAGE_PREVIEW); + m_SpinInstrument.SetRange(0, 0); + m_SpinInstrument.EnableWindow(FALSE); + // NNA + m_ComboNNA.AddString(_T("Note Cut")); + m_ComboNNA.AddString(_T("Continue")); + m_ComboNNA.AddString(_T("Note Off")); + m_ComboNNA.AddString(_T("Note Fade")); + // DCT + m_ComboDCT.AddString(_T("Disabled")); + m_ComboDCT.AddString(_T("Note")); + m_ComboDCT.AddString(_T("Sample")); + m_ComboDCT.AddString(_T("Instrument")); + m_ComboDCT.AddString(_T("Plugin")); + // DCA + m_ComboDCA.AddString(_T("Note Cut")); + m_ComboDCA.AddString(_T("Note Off")); + m_ComboDCA.AddString(_T("Note Fade")); + // FadeOut Volume + m_SpinFadeOut.SetRange(0, 8192); + // Global Volume + m_SpinGlobalVol.SetRange(0, 64); + // Panning + m_SpinPanning.SetRange(0, (m_modDoc.GetModType() & MOD_TYPE_IT) ? 64 : 256); + // Midi Program + m_SpinMidiPR.SetRange(0, 128); + // Midi Bank + m_SpinMidiBK.SetRange(0, 16384); + // MIDI Pitch Wheel Depth + m_EditPWD.SubclassDlgItem(IDC_PITCHWHEELDEPTH, this); + m_EditPWD.AllowFractions(false); + + const auto resamplingModes = Resampling::AllModes(); + m_CbnResampling.SetItemData(m_CbnResampling.AddString(_T("Default")), SRCMODE_DEFAULT); + for(auto mode : resamplingModes) + { + m_CbnResampling.SetItemData(m_CbnResampling.AddString(CTrackApp::GetResamplingModeName(mode, 1, false)), mode); + } + + m_CbnFilterMode.SetItemData(m_CbnFilterMode.AddString(_T("Channel default")), static_cast<DWORD_PTR>(FilterMode::Unchanged)); + m_CbnFilterMode.SetItemData(m_CbnFilterMode.AddString(_T("Force lowpass")), static_cast<DWORD_PTR>(FilterMode::LowPass)); + m_CbnFilterMode.SetItemData(m_CbnFilterMode.AddString(_T("Force highpass")), static_cast<DWORD_PTR>(FilterMode::HighPass)); + + //VST velocity/volume handling + m_CbnPluginVolumeHandling.AddString(_T("MIDI volume")); + m_CbnPluginVolumeHandling.AddString(_T("Dry/Wet ratio")); + m_CbnPluginVolumeHandling.AddString(_T("None")); + + // Vol/Pan Swing + m_SliderVolSwing.SetRange(0, 100); + m_SliderPanSwing.SetRange(0, 64); + m_SliderCutSwing.SetRange(0, 64); + m_SliderResSwing.SetRange(0, 64); + // Filter + m_SliderCutOff.SetRange(0x00, 0x7F); + m_SliderResonance.SetRange(0x00, 0x7F); + // Pitch/Pan Separation + m_EditPPS.SubclassDlgItem(IDC_EDIT15, this); + m_EditPPS.AllowFractions(false); + m_SpinPPS.SetRange(-32, +32); + // Pitch/Pan Center + SetWindowLongPtr(m_ComboPPC.m_hWnd, GWLP_USERDATA, 0); + + // Volume ramping (attack) + m_SliderAttack.SetRange(0,MAX_ATTACK_VALUE); + m_SpinAttack.SetRange(0,MAX_ATTACK_VALUE); + + m_SpinInstrument.SetFocus(); + + m_EditPWD.EnableWindow(FALSE); + + BuildTuningComboBox(); + + CheckDlgButton(IDC_CHECK_PITCHTEMPOLOCK, BST_UNCHECKED); + m_EditPitchTempoLock.SubclassDlgItem(IDC_EDIT_PITCHTEMPOLOCK, this); + m_EditPitchTempoLock.AllowNegative(false); + m_EditPitchTempoLock.SetLimitText(9); + + SetRedraw(TRUE); + return FALSE; +} + + +void CCtrlInstruments::RecalcLayout() +{ +} + + +void CCtrlInstruments::OnTbnDropDownToolBar(NMHDR *pNMHDR, LRESULT *pResult) +{ + CInputHandler *ih = CMainFrame::GetInputHandler(); + NMTOOLBAR *pToolBar = reinterpret_cast<NMTOOLBAR *>(pNMHDR); + ClientToScreen(&(pToolBar->rcButton)); // TrackPopupMenu uses screen coords + const int offset = Util::ScalePixels(4, m_hWnd); // Compared to the main toolbar, the offset seems to be a bit wrong here...? + int x = pToolBar->rcButton.left + offset, y = pToolBar->rcButton.bottom + offset; + CMenu menu; + switch(pToolBar->iItem) + { + case IDC_INSTRUMENT_NEW: + { + menu.CreatePopupMenu(); + menu.AppendMenu(MF_STRING, ID_INSTRUMENT_DUPLICATE, ih->GetKeyTextFromCommand(kcInstrumentCtrlDuplicate, _T("Duplicate &Instrument"))); + menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this); + menu.DestroyMenu(); + } + break; + case IDC_INSTRUMENT_SAVEAS: + { + menu.CreatePopupMenu(); + menu.AppendMenu(MF_STRING, IDC_SAVE_ALL, _T("Save &All...")); + menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this); + menu.DestroyMenu(); + } + break; + } + *pResult = 0; +} + + +void CCtrlInstruments::PrepareUndo(const char *description) +{ + m_startedEdit = true; + m_modDoc.GetInstrumentUndo().PrepareUndo(m_nInstrument, description); +} + + +// Set document as modified and update other views. +// updateAll: Update all views including this one. Otherwise, only update update other views. +void CCtrlInstruments::SetModified(InstrumentHint hint, bool updateAll) +{ + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, hint.SetData(m_nInstrument), updateAll ? nullptr : this); +} + + +BOOL CCtrlInstruments::SetCurrentInstrument(UINT nIns, BOOL bUpdNum) +{ + if (m_sndFile.m_nInstruments < 1) return FALSE; + if ((nIns < 1) || (nIns > m_sndFile.m_nInstruments)) return FALSE; + LockControls(); + if ((m_nInstrument != nIns) || (!m_bInitialized)) + { + m_nInstrument = static_cast<INSTRUMENTINDEX>(nIns); + m_NoteMap.SetCurrentInstrument(m_nInstrument); + UpdateView(InstrumentHint(m_nInstrument).Info().Envelope(), NULL); + } else + { + // Just in case + m_NoteMap.SetCurrentInstrument(m_nInstrument); + } + if (bUpdNum) + { + SetDlgItemInt(IDC_EDIT_INSTRUMENT, m_nInstrument); + m_SpinInstrument.SetRange(1, m_sndFile.GetNumInstruments()); + m_SpinInstrument.EnableWindow((m_sndFile.GetNumInstruments()) ? TRUE : FALSE); + // Is this a bug ? + m_SliderCutOff.Invalidate(FALSE); + m_SliderResonance.Invalidate(FALSE); + // Volume ramping (attack) + m_SliderAttack.Invalidate(FALSE); + } + SendViewMessage(VIEWMSG_SETCURRENTINSTRUMENT, m_nInstrument); + UnlockControls(); + + return TRUE; +} + + +void CCtrlInstruments::OnActivatePage(LPARAM lParam) +{ + CModControlDlg::OnActivatePage(lParam); + if (lParam < 0) + { + int nIns = m_parent.GetInstrumentChange(); + if (nIns > 0) lParam = nIns; + } else if(lParam > 0) + { + m_parent.InstrumentChanged(static_cast<INSTRUMENTINDEX>(lParam)); + } + + UpdatePluginList(); + + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + INSTRUMENTVIEWSTATE &instrumentState = pFrame->GetInstrumentViewState(); + if(instrumentState.initialInstrument != 0) + { + m_nInstrument = instrumentState.initialInstrument; + instrumentState.initialInstrument = 0; + } + + SetCurrentInstrument(static_cast<INSTRUMENTINDEX>((lParam > 0) ? lParam : m_nInstrument)); + + // Initial Update + if (!m_bInitialized) UpdateView(InstrumentHint(m_nInstrument).Info().Envelope().ModType(), NULL); + + PostViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&instrumentState); + SwitchToView(); + + // Combo boxes randomly disappear without this... why? + Invalidate(); +} + + +void CCtrlInstruments::OnDeactivatePage() +{ + m_modDoc.NoteOff(0, true); + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + if ((pFrame) && (m_hWndView)) SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&pFrame->GetInstrumentViewState()); + CModControlDlg::OnDeactivatePage(); +} + + +LRESULT CCtrlInstruments::OnModCtrlMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case CTRLMSG_GETCURRENTINSTRUMENT: + return m_nInstrument; + break; + + case CTRLMSG_INS_PREVINSTRUMENT: + OnPrevInstrument(); + break; + + case CTRLMSG_INS_NEXTINSTRUMENT: + OnNextInstrument(); + break; + + case CTRLMSG_INS_OPENFILE: + if(lParam) + return OpenInstrument(*reinterpret_cast<const mpt::PathString *>(lParam)); + break; + + case CTRLMSG_INS_SONGDROP: + if(lParam) + { + const auto &dropInfo = *reinterpret_cast<const DRAGONDROP *>(lParam); + if(dropInfo.sndFile) + return OpenInstrument(*dropInfo.sndFile, static_cast<INSTRUMENTINDEX>(dropInfo.dropItem)); + } + break; + + case CTRLMSG_INS_NEWINSTRUMENT: + return InsertInstrument(false) ? 1 : 0; + + case CTRLMSG_SETCURRENTINSTRUMENT: + SetCurrentInstrument(static_cast<INSTRUMENTINDEX>(lParam)); + break; + + case CTRLMSG_INS_SAMPLEMAP: + OnEditSampleMap(); + break; + + case IDC_INSTRUMENT_NEW: + OnInstrumentNew(); + break; + case IDC_INSTRUMENT_OPEN: + OnInstrumentOpen(); + break; + case IDC_INSTRUMENT_SAVEAS: + OnInstrumentSave(); + break; + + default: + return CModControlDlg::OnModCtrlMsg(wParam, lParam); + } + return 0; +} + + +void CCtrlInstruments::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj == this) + return; + if (hint.GetType()[HINT_MPTOPTIONS]) + { + m_ToolBar.UpdateStyle(); + hint.ModType(); // For possibly updating note names in Pitch/Pan Separation dropdown + } + LockControls(); + if(hint.ToType<PluginHint>().GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES]) + { + OnMixPlugChanged(); + } + if(hint.ToType<GeneralHint>().GetType()[HINT_TUNINGS]) + { + BuildTuningComboBox(); + } + UnlockControls(); + + const InstrumentHint instrHint = hint.ToType<InstrumentHint>(); + FlagSet<HintType> hintType = instrHint.GetType(); + if(!m_bInitialized) + hintType.set(HINT_MODTYPE); + if(!hintType[HINT_MODTYPE | HINT_INSTRUMENT | HINT_ENVELOPE | HINT_INSNAMES]) + return; + + const INSTRUMENTINDEX updateIns = instrHint.GetInstrument(); + if(updateIns != m_nInstrument && updateIns != 0 && !hintType[HINT_MODTYPE]) + return; + + LockControls(); + const ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + + if(hintType[HINT_MODTYPE]) + { + auto &specs = m_sndFile.GetModSpecifications(); + + // Limit text fields + m_EditName.SetLimitText(specs.instrNameLengthMax); + m_EditFileName.SetLimitText(specs.instrFilenameLengthMax); + + const BOOL bITandMPT = ((m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) && (m_sndFile.GetNumInstruments())) ? TRUE : FALSE; + const BOOL bITandXM = ((m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) && (m_sndFile.GetNumInstruments())) ? TRUE : FALSE; + const BOOL bMPTOnly = ((m_sndFile.GetType() == MOD_TYPE_MPT) && (m_sndFile.GetNumInstruments())) ? TRUE : FALSE; + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT10), bITandXM); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT11), bITandXM); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT7), bITandXM); + m_EditName.EnableWindow(bITandXM); + m_EditFileName.EnableWindow(bITandMPT); + m_CbnMidiCh.EnableWindow(bITandXM); + m_CbnMixPlug.EnableWindow(bITandXM); + m_SpinMidiPR.EnableWindow(bITandXM); + m_SpinMidiBK.EnableWindow(bITandXM); + + const bool extendedFadeoutRange = !(m_sndFile.GetType() & MOD_TYPE_IT); + m_SpinFadeOut.EnableWindow(bITandXM); + m_SpinFadeOut.SetRange(0, extendedFadeoutRange ? 32767 : 8192); + m_EditFadeOut.SetLimitText(extendedFadeoutRange ? 5 : 4); + // XM-style fade-out is 32 times more precise than IT + UDACCEL accell[2]; + accell[0].nSec = 0; + accell[0].nInc = (m_sndFile.GetType() == MOD_TYPE_IT ? 32 : 1); + accell[1].nSec = 2; + accell[1].nInc = 5 * accell[0].nInc; + m_SpinFadeOut.SetAccel(mpt::saturate_cast<int>(std::size(accell)), accell); + + // Panning ranges (0...64 for IT, 0...256 for MPTM) + m_SpinPanning.SetRange(0, (m_sndFile.GetType() & MOD_TYPE_IT) ? 64 : 256); + + // Pitch Wheel Depth + if(m_sndFile.GetType() == MOD_TYPE_XM) + m_SpinPWD.SetRange(0, 36); + else + m_SpinPWD.SetRange(-128, 127); + m_EditPWD.EnableWindow(bITandXM); + m_SpinPWD.EnableWindow(bITandXM); + + m_NoteMap.EnableWindow(bITandXM); + + m_ComboNNA.EnableWindow(bITandMPT); + m_SliderVolSwing.EnableWindow(bITandMPT); + m_SliderPanSwing.EnableWindow(bITandMPT); + m_ComboDCT.EnableWindow(bITandMPT); + m_ComboDCA.EnableWindow(bITandMPT); + m_ComboPPC.EnableWindow(bITandMPT); + m_SpinPPS.EnableWindow(bITandMPT); + m_EditGlobalVol.EnableWindow(bITandMPT); + m_SpinGlobalVol.EnableWindow(bITandMPT); + m_EditPanning.EnableWindow(bITandMPT); + m_SpinPanning.EnableWindow(bITandMPT); + m_CheckPanning.EnableWindow(bITandMPT); + m_EditPPS.EnableWindow(bITandMPT); + m_CheckCutOff.EnableWindow(bITandMPT); + m_CheckResonance.EnableWindow(bITandMPT); + m_SliderCutOff.EnableWindow(bITandMPT); + m_SliderResonance.EnableWindow(bITandMPT); + m_ComboTuning.EnableWindow(bMPTOnly); + m_EditPitchTempoLock.EnableWindow(bMPTOnly); + m_CheckPitchTempoLock.EnableWindow(bMPTOnly); + + // MIDI Channel + // XM has no "mapped" MIDI channels. + m_CbnMidiCh.ResetContent(); + for(int ich = MidiNoChannel; ich <= (bITandMPT ? MidiMappedChannel : MidiLastChannel); ich++) + { + CString s; + if (ich == MidiNoChannel) + s = _T("None"); + else if (ich == MidiMappedChannel) + s = _T("Mapped"); + else + s.Format(_T("%i"), ich); + m_CbnMidiCh.SetItemData(m_CbnMidiCh.AddString(s), ich); + } + } + if(hintType[HINT_MODTYPE | HINT_INSTRUMENT | HINT_INSNAMES]) + { + if(pIns) + m_EditName.SetWindowText(mpt::ToCString(m_sndFile.GetCharsetInternal(), pIns->name)); + else + m_EditName.SetWindowText(_T("")); + } + if(hintType[HINT_MODTYPE | HINT_INSTRUMENT]) + { + m_SpinInstrument.SetRange(1, m_sndFile.m_nInstruments); + m_SpinInstrument.EnableWindow((m_sndFile.m_nInstruments) ? TRUE : FALSE); + + // Backwards compatibility with legacy IT/XM modules that use now deprecated hack features. + m_SliderCutSwing.EnableWindow(pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->nCutSwing != 0)); + m_SliderResSwing.EnableWindow(pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->nResSwing != 0)); + m_CbnFilterMode.EnableWindow (pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->filterMode != FilterMode::Unchanged)); + m_CbnResampling.EnableWindow (pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->resampling != SRCMODE_DEFAULT)); + m_SliderAttack.EnableWindow (pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->nVolRampUp)); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT2), pIns != nullptr && (m_sndFile.GetType() == MOD_TYPE_MPT || pIns->nVolRampUp)); + + if (pIns) + { + m_EditFileName.SetWindowText(mpt::ToCString(m_sndFile.GetCharsetInternal(), pIns->filename)); + // Fade Out Volume + SetDlgItemInt(IDC_EDIT7, pIns->nFadeOut); + // Global Volume + SetDlgItemInt(IDC_EDIT8, pIns->nGlobalVol); + // Panning + SetDlgItemInt(IDC_EDIT9, (m_modDoc.GetModType() & MOD_TYPE_IT) ? (pIns->nPan / 4) : pIns->nPan); + m_CheckPanning.SetCheck(pIns->dwFlags[INS_SETPANNING] ? TRUE : FALSE); + // Midi + if (pIns->nMidiProgram>0 && pIns->nMidiProgram<=128) + SetDlgItemInt(IDC_EDIT10, pIns->nMidiProgram); + else + SetDlgItemText(IDC_EDIT10, _T("---")); + if (pIns->wMidiBank && pIns->wMidiBank <= 16384) + SetDlgItemInt(IDC_EDIT11, pIns->wMidiBank); + else + SetDlgItemText(IDC_EDIT11, _T("---")); + + if (pIns->nMidiChannel < 18) + { + m_CbnMidiCh.SetCurSel(pIns->nMidiChannel); + } else + { + m_CbnMidiCh.SetCurSel(0); + } + if (pIns->nMixPlug <= MAX_MIXPLUGINS) + { + m_CbnMixPlug.SetCurSel(pIns->nMixPlug); + } else + { + m_CbnMixPlug.SetCurSel(0); + } + OnMixPlugChanged(); + for(int resMode = 0; resMode<m_CbnResampling.GetCount(); resMode++) + { + if(pIns->resampling == m_CbnResampling.GetItemData(resMode)) + { + m_CbnResampling.SetCurSel(resMode); + break; + } + } + for(int fltMode = 0; fltMode<m_CbnFilterMode.GetCount(); fltMode++) + { + if(pIns->filterMode == static_cast<FilterMode>(m_CbnFilterMode.GetItemData(fltMode))) + { + m_CbnFilterMode.SetCurSel(fltMode); + break; + } + } + + // NNA, DCT, DCA + m_ComboNNA.SetCurSel(static_cast<int>(pIns->nNNA)); + m_ComboDCT.SetCurSel(static_cast<int>(pIns->nDCT)); + m_ComboDCA.SetCurSel(static_cast<int>(pIns->nDNA)); + // Pitch/Pan Separation + if(hintType[HINT_MODTYPE] || pIns->pTuning != (CTuning *)GetWindowLongPtr(m_ComboPPC.m_hWnd, GWLP_USERDATA)) + { + // Tuning may have changed, and thus the note names need to be updated + m_ComboPPC.SetRedraw(FALSE); + m_ComboPPC.ResetContent(); + AppendNotesToControlEx(m_ComboPPC, m_sndFile, m_nInstrument, NOTE_MIN, NOTE_MAX); + SetWindowLongPtr(m_ComboPPC.m_hWnd, GWLP_USERDATA, (LONG_PTR)pIns->pTuning); + m_ComboPPC.SetRedraw(TRUE); + } + m_ComboPPC.SetCurSel(pIns->nPPC); + ASSERT((uint8)m_ComboPPC.GetItemData(m_ComboPPC.GetCurSel()) == pIns->nPPC + NOTE_MIN); + SetDlgItemInt(IDC_EDIT15, pIns->nPPS); + // Filter + if (m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + m_CheckCutOff.SetCheck((pIns->IsCutoffEnabled()) ? TRUE : FALSE); + m_CheckResonance.SetCheck((pIns->IsResonanceEnabled()) ? TRUE : FALSE); + m_SliderVolSwing.SetPos(pIns->nVolSwing); + m_SliderPanSwing.SetPos(pIns->nPanSwing); + m_SliderResSwing.SetPos(pIns->nResSwing); + m_SliderCutSwing.SetPos(pIns->nCutSwing); + m_SliderCutOff.SetPos(pIns->GetCutoff()); + m_SliderResonance.SetPos(pIns->GetResonance()); + UpdateFilterText(); + } + // Volume ramping (attack) + int n = pIns->nVolRampUp; //? MAX_ATTACK_LENGTH - pIns->nVolRampUp : 0; + m_SliderAttack.SetPos(n); + if(n == 0) SetDlgItemText(IDC_EDIT2, _T("default")); + else SetDlgItemInt(IDC_EDIT2,n); + + UpdateTuningComboBox(); + + // Only enable Pitch/Tempo Lock for MPTM files or legacy files that have this property enabled. + m_CheckPitchTempoLock.EnableWindow((m_sndFile.GetType() == MOD_TYPE_MPT || pIns->pitchToTempoLock.GetRaw() > 0) ? TRUE : FALSE); + CheckDlgButton(IDC_CHECK_PITCHTEMPOLOCK, pIns->pitchToTempoLock.GetRaw() > 0 ? BST_CHECKED : BST_UNCHECKED); + m_EditPitchTempoLock.EnableWindow(pIns->pitchToTempoLock.GetRaw() > 0 ? TRUE : FALSE); + if(pIns->pitchToTempoLock.GetRaw() > 0) + { + m_EditPitchTempoLock.SetTempoValue(pIns->pitchToTempoLock); + } + + // Pitch Wheel Depth + SetDlgItemInt(IDC_PITCHWHEELDEPTH, pIns->midiPWD, TRUE); + + if(m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) + { + BOOL enableVol = (m_CbnMixPlug.GetCurSel() > 0 && !m_sndFile.m_playBehaviour[kMIDICCBugEmulation]) ? TRUE : FALSE; + velocityStyle.EnableWindow(enableVol); + m_CbnPluginVolumeHandling.EnableWindow(enableVol); + } + } else + { + m_EditFileName.SetWindowText(_T("")); + velocityStyle.EnableWindow(FALSE); + m_CbnPluginVolumeHandling.EnableWindow(FALSE); + if(m_nInstrument > m_sndFile.GetNumInstruments()) + SetCurrentInstrument(m_sndFile.GetNumInstruments()); + + } + m_NoteMap.Invalidate(FALSE); + + m_ComboNNA.Invalidate(FALSE); + m_ComboDCT.Invalidate(FALSE); + m_ComboDCA.Invalidate(FALSE); + m_ComboPPC.Invalidate(FALSE); + m_CbnMidiCh.Invalidate(FALSE); + m_CbnMixPlug.Invalidate(FALSE); + m_CbnResampling.Invalidate(FALSE); + m_CbnFilterMode.Invalidate(FALSE); + m_CbnPluginVolumeHandling.Invalidate(FALSE); + m_ComboTuning.Invalidate(FALSE); + } + if(hint.ToType<PluginHint>().GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES | HINT_MODTYPE]) + { + UpdatePluginList(); + } + + if (!m_bInitialized) + { + // First update + m_bInitialized = TRUE; + UnlockControls(); + } + + UnlockControls(); +} + + +void CCtrlInstruments::UpdateFilterText() +{ + if(m_nInstrument) + { + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(pIns) + { + TCHAR s[32]; + // In IT Compatible mode, it is enough to just have resonance enabled to turn on the filter. + const bool resEnabled = (pIns->IsResonanceEnabled() && pIns->GetResonance() > 0 && m_sndFile.m_playBehaviour[kITFilterBehaviour]); + + if((pIns->IsCutoffEnabled() && pIns->GetCutoff() < 0x7F) || resEnabled) + { + const BYTE cutoff = (resEnabled && !pIns->IsCutoffEnabled()) ? 0x7F : pIns->GetCutoff(); + wsprintf(s, _T("Z%02X (%u Hz)"), cutoff, m_sndFile.CutOffToFrequency(cutoff)); + } else if(pIns->IsCutoffEnabled()) + { + _tcscpy(s, _T("Z7F (Off)")); + } else + { + _tcscpy(s, _T("No Change")); + } + + SetDlgItemText(IDC_FILTERTEXT, s); + } + } +} + + +bool CCtrlInstruments::OpenInstrument(const mpt::PathString &fileName) +{ + BeginWaitCursor(); + InputFile f(fileName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(!f.IsValid()) + { + EndWaitCursor(); + return false; + } + + FileReader file = GetFileReader(f); + + bool first = false, ok = false; + if (file.IsValid()) + { + if (!m_sndFile.GetNumInstruments()) + { + first = true; + m_sndFile.m_nInstruments = 1; + m_modDoc.SetModified(); + } + if (!m_nInstrument) m_nInstrument = 1; + ScopedLogCapturer log(m_modDoc, _T("Instrument Import"), this); + PrepareUndo("Replace Instrument"); + if (m_sndFile.ReadInstrumentFromFile(m_nInstrument, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad)) + { + ok = true; + } else + { + m_modDoc.GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } + } + + if(!ok && first) + { + // Undo adding the instrument + delete m_sndFile.Instruments[1]; + m_sndFile.m_nInstruments = 0; + } else if(ok && first) + { + m_NoteMap.SetCurrentInstrument(1); + } + + EndWaitCursor(); + if(ok) + { + TrackerSettings::Instance().PathInstruments.SetWorkingDir(fileName, true); + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if (pIns) + { + mpt::PathString name, ext; + fileName.SplitPath(nullptr, nullptr, &name, &ext); + + if (!pIns->name[0] && m_sndFile.GetModSpecifications().instrNameLengthMax > 0) + { + pIns->name = mpt::truncate(name.ToLocale(), m_sndFile.GetModSpecifications().instrNameLengthMax); + } + if (!pIns->filename[0] && m_sndFile.GetModSpecifications().instrFilenameLengthMax > 0) + { + name += ext; + pIns->filename = mpt::truncate(name.ToLocale(), m_sndFile.GetModSpecifications().instrFilenameLengthMax); + } + + SetCurrentInstrument(m_nInstrument); + InstrumentHint hint = InstrumentHint().Info().Envelope().Names(); + if(first) hint.ModType(); + SetModified(hint, true); + } else ok = FALSE; + } else + { + // Try loading as module + ok = CMainFrame::GetMainFrame()->SetTreeSoundfile(file); + if(ok) return true; + } + SampleHint hint = SampleHint().Info().Data().Names(); + if (first) hint.ModType(); + m_modDoc.UpdateAllViews(nullptr, hint); + if (!ok) ErrorBox(IDS_ERR_FILETYPE, this); + return ok; +} + + +bool CCtrlInstruments::OpenInstrument(const CSoundFile &sndFile, INSTRUMENTINDEX nInstr) +{ + if((!nInstr) || (nInstr > sndFile.GetNumInstruments())) return false; + BeginWaitCursor(); + + CriticalSection cs; + + bool first = false; + if (!m_sndFile.GetNumInstruments()) + { + first = true; + m_sndFile.m_nInstruments = 1; + SetCurrentInstrument(1); + first = true; + } + PrepareUndo("Replace Instrument"); + m_sndFile.ReadInstrumentFromSong(m_nInstrument, sndFile, nInstr); + + cs.Leave(); + + { + InstrumentHint hint = InstrumentHint().Info().Envelope().Names(); + if (first) hint.ModType(); + SetModified(hint, true); + } + { + SampleHint hint = SampleHint().Info().Data().Names(); + if (first) hint.ModType(); + m_modDoc.UpdateAllViews(nullptr, hint, this); + } + EndWaitCursor(); + return true; +} + + +BOOL CCtrlInstruments::EditSample(UINT nSample) +{ + if ((nSample > 0) && (nSample < MAX_SAMPLES)) + { + m_parent.PostMessage(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, nSample); + return TRUE; + } + return FALSE; +} + + +BOOL CCtrlInstruments::GetToolTipText(UINT uId, LPTSTR pszText) +{ + //Note: pszText points to a TCHAR array of length 256 (see CChildFrame::OnToolTipText). + //Note2: If there's problems in getting tooltips showing for certain tools, + // setting the tab order may have effect. + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + + if(pIns == nullptr) return FALSE; + if ((pszText) && (uId)) + { + CWnd *wnd = GetDlgItem(uId); + bool isEnabled = wnd != nullptr && wnd->IsWindowEnabled() != FALSE; + const auto plusMinus = mpt::ToWin(mpt::Charset::UTF8, "\xC2\xB1"); + const TCHAR *s = nullptr; + CommandID cmd = kcNull; + switch(uId) + { + case IDC_INSTRUMENT_NEW: s = _T("Insert Instrument (Hold Shift to duplicate)"); cmd = kcInstrumentNew; break; + case IDC_INSTRUMENT_OPEN: s = _T("Import Instrument"); cmd = kcInstrumentLoad; break; + case IDC_INSTRUMENT_SAVEAS: s = _T("Save Instrument"); cmd = kcInstrumentSave; break; + case IDC_INSTRUMENT_PLAY: s = _T("Play Instrument"); break; + + case IDC_EDIT_PITCHTEMPOLOCK: + case IDC_CHECK_PITCHTEMPOLOCK: + // Pitch/Tempo lock + if(isEnabled) + { + const CModSpecifications& specs = m_sndFile.GetModSpecifications(); + wsprintf(pszText, _T("Tempo Range: %u - %u"), specs.GetTempoMin().GetInt(), specs.GetTempoMax().GetInt()); + } else + { + _tcscpy(pszText, _T("Only available in MPTM format")); + } + return TRUE; + + case IDC_EDIT7: + // Fade Out + if(!pIns->nFadeOut) + _tcscpy(pszText, _T("Fade disabled")); + else + wsprintf(pszText, _T("%u ticks (Higher value <-> Faster fade out)"), 0x8000 / pIns->nFadeOut); + return TRUE; + + case IDC_EDIT8: + // Global volume + if(isEnabled) + _tcscpy(pszText, CModDoc::LinearToDecibels(GetDlgItemInt(IDC_EDIT8), 64.0)); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + + case IDC_EDIT9: + // Panning + if(isEnabled) + _tcscpy(pszText, CModDoc::PanningToString(pIns->nPan, 128)); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + +#ifndef NO_PLUGINS + case IDC_EDIT10: + case IDC_EDIT11: + // Show plugin program name when hovering program or bank edits + if(pIns->nMixPlug > 0 && pIns->nMidiProgram != 0) + { + const SNDMIXPLUGIN &plugin = m_sndFile.m_MixPlugins[pIns->nMixPlug - 1]; + if(plugin.pMixPlugin != nullptr) + { + int32 prog = pIns->nMidiProgram - 1; + if(pIns->wMidiBank > 1) prog += 128 * (pIns->wMidiBank - 1); + _tcscpy(pszText, plugin.pMixPlugin->GetFormattedProgramName(prog)); + } + } + return TRUE; +#endif // NO_PLUGINS + + case IDC_PLUGIN_VELOCITYSTYLE: + case IDC_PLUGIN_VOLUMESTYLE: + // Plugin volume handling + if(pIns->nMixPlug < 1) return FALSE; + if(m_sndFile.m_playBehaviour[kMIDICCBugEmulation]) + { + velocityStyle.EnableWindow(FALSE); + m_CbnPluginVolumeHandling.EnableWindow(FALSE); + _tcscpy(pszText, _T("To enable, clear Plugin volume command bug emulation flag from Song Properties")); + return TRUE; + } else + { + if(uId == IDC_PLUGIN_VELOCITYSTYLE) + { + _tcscpy(pszText, _T("Volume commands (vxx) next to a note are sent as note velocity instead.")); + return TRUE; + } + return FALSE; + } + + case IDC_COMBO5: + // MIDI Channel + s = _T("Mapped: MIDI channel corresponds to pattern channel modulo 16"); + break; + + case IDC_SLIDER1: + if(isEnabled) + wsprintf(pszText, _T("%s%d%% volume variation"), plusMinus.c_str(), pIns->nVolSwing); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + + case IDC_SLIDER2: + if(isEnabled) + wsprintf(pszText, _T("%s%d panning variation"), plusMinus.c_str(), pIns->nPanSwing); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + + case IDC_SLIDER3: + if(isEnabled) + wsprintf(pszText, _T("%u"), pIns->GetCutoff()); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + + case IDC_SLIDER4: + if(isEnabled) + wsprintf(pszText, _T("%u (%i dB)"), pIns->GetResonance(), Util::muldivr(pIns->GetResonance(), 24, 128)); + else + _tcscpy(pszText, _T("Only available in IT / MPTM format")); + return TRUE; + + case IDC_SLIDER6: + if(isEnabled) + wsprintf(pszText, _T("%s%d cutoff variation"), plusMinus.c_str(), pIns->nCutSwing); + else + _tcscpy(pszText, _T("Only available in MPTM format")); + return TRUE; + + case IDC_SLIDER7: + if(isEnabled) + wsprintf(pszText, _T("%s%d resonance variation"), plusMinus.c_str(), pIns->nResSwing); + else + _tcscpy(pszText, _T("Only available in MPTM format")); + return TRUE; + + case IDC_PITCHWHEELDEPTH: + s = _T("Set this to the actual Pitch Wheel Depth used in your plugin on this channel."); + break; + + case IDC_INSVIEWPLG: // Open Editor + if(!isEnabled) + s = _T("No Plugin Loaded"); + break; + + case IDC_SPIN9: // Pan + case IDC_CHECK1: // Pan + case IDC_COMBO1: // NNA + case IDC_COMBO2: // DCT + case IDC_COMBO3: // DNA + case IDC_COMBO4: // PPC + case IDC_SPIN12: // PPS + case IDC_EDIT15: // PPS + if(!isEnabled) + s = _T("Only available in IT / MPTM format"); + break; + + case IDC_COMBOTUNING: // Tuning + case IDC_COMBO9: // Resampling: + case IDC_SLIDER5: // Ramping + case IDC_SPIN1: // Ramping + case IDC_EDIT2: // Ramping + if(!isEnabled) + s = _T("Only available in MPTM format"); + break; + + } + + if(s != nullptr) + { + _tcscpy(pszText, s); + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if (!keyText.IsEmpty()) + _tcscat(pszText, MPT_TFORMAT(" ({})")(keyText).c_str()); + } + return TRUE; + } + } + return FALSE; +} + + +//////////////////////////////////////////////////////////////////////////// +// CCtrlInstruments Messages + +void CCtrlInstruments::OnInstrumentChanged() +{ + if(!IsLocked()) + { + UINT n = GetDlgItemInt(IDC_EDIT_INSTRUMENT); + if ((n > 0) && (n <= m_sndFile.GetNumInstruments()) && (n != m_nInstrument)) + { + SetCurrentInstrument(n, FALSE); + m_parent.InstrumentChanged(n); + } + } +} + + +void CCtrlInstruments::OnPrevInstrument() +{ + if(m_nInstrument > 1) + SetCurrentInstrument(m_nInstrument - 1); + else + SetCurrentInstrument(m_sndFile.GetNumInstruments()); + m_parent.InstrumentChanged(m_nInstrument); +} + + +void CCtrlInstruments::OnNextInstrument() +{ + if(m_nInstrument < m_sndFile.GetNumInstruments()) + SetCurrentInstrument(m_nInstrument + 1); + else + SetCurrentInstrument(1); + m_parent.InstrumentChanged(m_nInstrument); +} + + +void CCtrlInstruments::OnInstrumentNew() +{ + InsertInstrument(m_sndFile.GetNumInstruments() > 0 && CMainFrame::GetInputHandler()->ShiftPressed()); + SwitchToView(); +} + + +bool CCtrlInstruments::InsertInstrument(bool duplicate) +{ + const bool hasInstruments = m_sndFile.GetNumInstruments() > 0; + + INSTRUMENTINDEX ins = m_modDoc.InsertInstrument(SAMPLEINDEX_INVALID, (duplicate && hasInstruments) ? m_nInstrument : INSTRUMENTINDEX_INVALID); + if (ins == INSTRUMENTINDEX_INVALID) + return false; + + if (!hasInstruments) m_modDoc.UpdateAllViews(nullptr, InstrumentHint().Info().Names().ModType()); + + SetCurrentInstrument(ins); + m_modDoc.UpdateAllViews(nullptr, InstrumentHint(ins).Info().Envelope().Names()); + m_parent.InstrumentChanged(m_nInstrument); + return true; +} + + +void CCtrlInstruments::OnInstrumentOpen() +{ + static int nLastIndex = 0; + + std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes(); + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .EnableAudioPreview() + .ExtensionFilter( + "All Instruments (*.xi,*.pat,*.iti,*.sfz,...)|*.xi;*.pat;*.iti;*.sfz;*.flac;*.wav;*.w64;*.caf;*.aif;*.aiff;*.au;*.snd;*.sbk;*.sf2;*.sf3;*.sf4;*.dls;*.oga;*.ogg;*.opus;*.s3i;*.sb0;*.sb2;*.sbi;*.brr" + ToFilterOnlyString(mediaFoundationTypes, true).ToLocale() + "|" + "FastTracker II Instruments (*.xi)|*.xi|" + "GF1 Patches (*.pat)|*.pat|" + "Impulse Tracker Instruments (*.iti)|*.iti|" + "SFZ Instruments (*.sfz)|*.sfz|" + "SoundFont 2.0 Banks (*.sf2)|*.sbk;*.sf2;*.sf3;*.sf4|" + "DLS Sound Banks (*.dls)|*.dls|" + "All Files (*.*)|*.*||") + .WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir()) + .FilterIndex(&nLastIndex); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory()); + + const FileDialog::PathList &files = dlg.GetFilenames(); + for(size_t counter = 0; counter < files.size(); counter++) + { + //If loading multiple instruments, advancing to next instrument and creating + //new instrument if necessary. + if(counter > 0) + { + if(m_nInstrument >= MAX_INSTRUMENTS - 1) + break; + else + m_nInstrument++; + + if(m_nInstrument > m_sndFile.GetNumInstruments()) + OnInstrumentNew(); + } + + if(!OpenInstrument(files[counter])) + ErrorBox(IDS_ERR_FILEOPEN, this); + } + + m_parent.InstrumentChanged(m_nInstrument); + SwitchToView(); +} + + +void CCtrlInstruments::OnInstrumentSave() +{ + SaveInstrument(CMainFrame::GetInputHandler()->ShiftPressed()); +} + + +void CCtrlInstruments::SaveInstrument(bool doBatchSave) +{ + if(!doBatchSave && m_sndFile.Instruments[m_nInstrument] == nullptr) + { + SwitchToView(); + return; + } + + mpt::PathString fileName; + if(!doBatchSave) + { + const ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(pIns->filename[0]) + fileName = mpt::PathString::FromLocale(pIns->filename); + else + fileName = mpt::PathString::FromLocale(pIns->name); + } else + { + // Save all samples + fileName = m_sndFile.GetpModDoc()->GetPathNameMpt().GetFileName(); + if(fileName.empty()) fileName = P_("untitled"); + + fileName += P_(" - %instrument_number% - "); + if(m_sndFile.GetModSpecifications().sampleFilenameLengthMax == 0) + fileName += P_("%instrument_name%"); + else + fileName += P_("%instrument_filename%"); + + } + SanitizeFilename(fileName); + + int index; + if(TrackerSettings::Instance().compressITI) + index = 2; + else if(m_sndFile.GetType() == MOD_TYPE_XM) + index = 4; + else + index = 1; + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(m_sndFile.GetType() == MOD_TYPE_XM ? "xi" : "iti") + .DefaultFilename(fileName) + .ExtensionFilter( + "Impulse Tracker Instruments (*.iti)|*.iti|" + "Compressed Impulse Tracker Instruments (*.iti)|*.iti|" + "Impulse Tracker Instruments with external Samples (*.iti)|*.iti|" + "FastTracker II Instruments (*.xi)|*.xi|" + "SFZ Instruments with WAV (*.sfz)|*.sfz|" + "SFZ Instruments with FLAC (*.sfz)|*.sfz||") + .WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir()) + .FilterIndex(&index); + if(!dlg.Show(this)) return; + + BeginWaitCursor(); + + INSTRUMENTINDEX minIns = m_nInstrument, maxIns = m_nInstrument; + if(doBatchSave) + { + minIns = 1; + maxIns = m_sndFile.GetNumInstruments(); + } + auto numberFmt = mpt::FormatSpec().Dec().FillNul().Width(1 + static_cast<int>(std::log10(maxIns))); + CString instrName, instrFilename; + + bool ok = true; + const bool saveXI = !mpt::PathString::CompareNoCase(dlg.GetExtension(), P_("xi")); + const bool saveSFZ = !mpt::PathString::CompareNoCase(dlg.GetExtension(), P_("sfz")); + const bool doCompress = index == 2 || index == 6; + const bool allowExternal = index == 3; + + for(INSTRUMENTINDEX ins = minIns; ins <= maxIns; ins++) + { + const ModInstrument *pIns = m_sndFile.Instruments[ins]; + if(pIns != nullptr) + { + fileName = dlg.GetFirstFile(); + if(doBatchSave) + { + instrName = mpt::ToCString(m_sndFile.GetCharsetInternal(), pIns->name[0] ? pIns->GetName() : "untitled"); + instrFilename = mpt::ToCString(m_sndFile.GetCharsetInternal(), pIns->filename[0] ? pIns->GetFilename() : pIns->GetName()); + SanitizeFilename(instrName); + SanitizeFilename(instrFilename); + + mpt::ustring fileNameW = fileName.ToUnicode(); + fileNameW = mpt::String::Replace(fileNameW, U_("%instrument_number%"), mpt::ufmt::fmt(ins, numberFmt)); + fileNameW = mpt::String::Replace(fileNameW, U_("%instrument_filename%"), mpt::ToUnicode(instrFilename)); + fileNameW = mpt::String::Replace(fileNameW, U_("%instrument_name%"), mpt::ToUnicode(instrName)); + fileName = mpt::PathString::FromUnicode(fileNameW); + } + + try + { + ScopedLogCapturer logcapturer(m_modDoc); + mpt::SafeOutputFile sf(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + if(!f) + { + ok = false; + continue; + } + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + + if (saveXI) + ok &= m_sndFile.SaveXIInstrument(ins, f); + else if (saveSFZ) + ok &= m_sndFile.SaveSFZInstrument(ins, f, fileName, doCompress); + else + ok &= m_sndFile.SaveITIInstrument(ins, f, fileName, doCompress, allowExternal); + } catch(const std::exception &) + { + ok = false; + } + } + } + + EndWaitCursor(); + if (!ok) + ErrorBox(IDS_ERR_SAVEINS, this); + else + TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory()); + SwitchToView(); +} + + +void CCtrlInstruments::OnInstrumentPlay() +{ + if (m_modDoc.IsNotePlaying(NOTE_MIDDLEC, 0, m_nInstrument)) + { + m_modDoc.NoteOff(NOTE_MIDDLEC, true, m_nInstrument); + } else + { + m_modDoc.PlayNote(PlayNoteParam(NOTE_MIDDLEC).Instrument(m_nInstrument)); + } + SwitchToView(); +} + + +void CCtrlInstruments::OnNameChanged() +{ + if (!IsLocked()) + { + CString tmp; + m_EditName.GetWindowText(tmp); + const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp); + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((pIns) && (s != pIns->name)) + { + if(!m_startedEdit) PrepareUndo("Set Name"); + pIns->name = s; + SetModified(InstrumentHint().Names(), false); + } + } +} + + +void CCtrlInstruments::OnFileNameChanged() +{ + if (!IsLocked()) + { + CString tmp; + m_EditFileName.GetWindowText(tmp); + const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp); + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((pIns) && (s != pIns->filename)) + { + if(!m_startedEdit) PrepareUndo("Set Filename"); + pIns->filename = s; + SetModified(InstrumentHint().Names(), false); + } + } +} + + +void CCtrlInstruments::OnFadeOutVolChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int minval = 0, maxval = 32767; + m_SpinFadeOut.GetRange(minval, maxval); + int nVol = GetDlgItemInt(IDC_EDIT7); + Limit(nVol, minval, maxval); + + if(nVol != (int)pIns->nFadeOut) + { + if(!m_startedEdit) PrepareUndo("Set Fade Out"); + pIns->nFadeOut = nVol; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnGlobalVolChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int nVol = GetDlgItemInt(IDC_EDIT8); + Limit(nVol, 0, 64); + if (nVol != (int)pIns->nGlobalVol) + { + if(!m_startedEdit) PrepareUndo("Set Global Volume"); + // Live-adjust volume + pIns->nGlobalVol = nVol; + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if(chn.pModInstrument == pIns) + { + chn.UpdateInstrumentVolume(chn.pModSample, pIns); + } + } + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnSetPanningChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + const bool b = m_CheckPanning.GetCheck() != BST_UNCHECKED; + + PrepareUndo("Toggle Panning"); + pIns->dwFlags.set(INS_SETPANNING, b); + + if(b && m_sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) + { + bool smpPanningInUse = false; + + const std::set<SAMPLEINDEX> referencedSamples = pIns->GetSamples(); + + for(auto sample : referencedSamples) + { + if(sample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(sample).uFlags[CHN_PANNING]) + { + smpPanningInUse = true; + break; + } + } + + if(smpPanningInUse) + { + if(Reporting::Confirm(_T("Some of the samples used in the instrument have \"Set Pan\" enabled. " + "Sample panning overrides instrument panning for the notes associated with such samples. " + "Do you wish to disable panning from those samples so that the instrument pan setting is effective " + "for the whole instrument?")) == cnfYes) + { + for (auto sample : referencedSamples) + { + if(sample <= m_sndFile.GetNumSamples()) + m_sndFile.GetSample(sample).uFlags.reset(CHN_PANNING); + } + m_modDoc.UpdateAllViews(nullptr, SampleHint().Info().ModType(), this); + } + } + } + SetModified(InstrumentHint().Info(), false); + } +} + + +void CCtrlInstruments::OnPanningChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int nPan = GetDlgItemInt(IDC_EDIT9); + if(m_modDoc.GetModType() & MOD_TYPE_IT) // IT panning ranges from 0 to 64 + nPan *= 4; + if (nPan < 0) nPan = 0; + if (nPan > 256) nPan = 256; + if (nPan != (int)pIns->nPan) + { + if(!m_startedEdit) PrepareUndo("Set Panning"); + pIns->nPan = nPan; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnNNAChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + const auto nna = static_cast<NewNoteAction>(m_ComboNNA.GetCurSel()); + if(pIns->nNNA != nna) + { + PrepareUndo("Set New Note Action"); + pIns->nNNA = nna; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnDCTChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + const auto dct = static_cast<DuplicateCheckType>(m_ComboDCT.GetCurSel()); + if(pIns->nDCT != dct) + { + PrepareUndo("Set Duplicate Check Type"); + pIns->nDCT = dct; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnDCAChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + const auto dna = static_cast<DuplicateNoteAction>(m_ComboDCA.GetCurSel()); + if (pIns->nDNA != dna) + { + PrepareUndo("Set Duplicate Check Action"); + pIns->nDNA = dna; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnMPRChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int n = GetDlgItemInt(IDC_EDIT10); + if ((n >= 0) && (n <= 128)) + { + if (pIns->nMidiProgram != n) + { + if(!m_startedEdit) PrepareUndo("Set MIDI Program"); + pIns->nMidiProgram = static_cast<uint8>(n); + SetModified(InstrumentHint().Info(), false); + } + } + // we will not set the midi bank/program if it is 0 + if(n == 0) + { + LockControls(); + SetDlgItemText(IDC_EDIT10, _T("---")); + UnlockControls(); + } + } +} + + +void CCtrlInstruments::OnMPRKillFocus() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int n = GetDlgItemInt(IDC_EDIT10); + if (n > 128) + { + n--; + pIns->nMidiProgram = static_cast<uint8>(n % 128 + 1); + pIns->wMidiBank = static_cast<uint16>(n / 128 + 1); + SetModified(InstrumentHint().Info(), false); + + LockControls(); + SetDlgItemInt(IDC_EDIT10, pIns->nMidiProgram); + SetDlgItemInt(IDC_EDIT11, pIns->wMidiBank); + UnlockControls(); + } + } +} + + +void CCtrlInstruments::OnMBKChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + uint16 w = static_cast<uint16>(GetDlgItemInt(IDC_EDIT11)); + if(w >= 0 && w <= 16384 && pIns->wMidiBank != w) + { + if(!m_startedEdit) PrepareUndo("Set MIDI Bank"); + pIns->wMidiBank = w; + SetModified(InstrumentHint().Info(), false); + } + // we will not set the midi bank/program if it is 0 + if(w == 0) + { + LockControls(); + SetDlgItemText(IDC_EDIT11, _T("---")); + UnlockControls(); + } + } +} + + +void CCtrlInstruments::OnMCHChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!IsLocked() && pIns) + { + uint8 ch = static_cast<uint8>(m_CbnMidiCh.GetItemData(m_CbnMidiCh.GetCurSel())); + if(pIns->nMidiChannel != ch) + { + PrepareUndo("Set MIDI Channel"); + pIns->nMidiChannel = ch; + SetModified(InstrumentHint().Info(), false); + } + } +} + +void CCtrlInstruments::OnResamplingChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + ResamplingMode n = static_cast<ResamplingMode>(m_CbnResampling.GetItemData(m_CbnResampling.GetCurSel())); + if (pIns->resampling != n) + { + PrepareUndo("Set Resampling"); + pIns->resampling = n; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnMixPlugChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + PLUGINDEX nPlug = static_cast<PLUGINDEX>(m_CbnMixPlug.GetItemData(m_CbnMixPlug.GetCurSel())); + + bool wasOpenedWithMouse = m_openendPluginListWithMouse; + m_openendPluginListWithMouse = false; + + if (pIns) + { + BOOL enableVol = (nPlug < 1 || m_sndFile.m_playBehaviour[kMIDICCBugEmulation]) ? FALSE : TRUE; + velocityStyle.EnableWindow(enableVol); + m_CbnPluginVolumeHandling.EnableWindow(enableVol); + + if(nPlug >= 0 && nPlug <= MAX_MIXPLUGINS) + { + bool active = !IsLocked(); + if (active && pIns->nMixPlug != nPlug) + { + PrepareUndo("Set Plugin"); + pIns->nMixPlug = nPlug; + SetModified(InstrumentHint().Info(), false); + } + + velocityStyle.SetCheck(pIns->pluginVelocityHandling == PLUGIN_VELOCITYHANDLING_CHANNEL ? BST_CHECKED : BST_UNCHECKED); + m_CbnPluginVolumeHandling.SetCurSel(pIns->pluginVolumeHandling); + +#ifndef NO_PLUGINS + if(pIns->nMixPlug) + { + // we have selected a plugin that's not "no plugin" + const SNDMIXPLUGIN &plugin = m_sndFile.m_MixPlugins[pIns->nMixPlug - 1]; + + if(!plugin.IsValidPlugin() && active && wasOpenedWithMouse) + { + // No plugin in this slot yet: Ask user to add one. + CSelectPluginDlg dlg(&m_modDoc, nPlug - 1, this); + if (dlg.DoModal() == IDOK) + { + if(m_sndFile.GetModSpecifications().supportsPlugins) + { + m_modDoc.SetModified(); + } + UpdatePluginList(); + + m_modDoc.UpdateAllViews(nullptr, PluginHint(nPlug).Info().Names()); + } + } + + if(plugin.pMixPlugin != nullptr) + { + GetDlgItem(IDC_INSVIEWPLG)->EnableWindow(true); + + if(active && plugin.pMixPlugin->IsInstrument()) + { + if(pIns->nMidiChannel == MidiNoChannel) + { + // If this plugin can recieve MIDI events and we have no MIDI channel + // selected for this instrument, automatically select MIDI channel 1. + pIns->nMidiChannel = MidiFirstChannel; + UpdateView(InstrumentHint(m_nInstrument).Info()); + } + if(pIns->midiPWD == 0) + { + pIns->midiPWD = 2; + } + + // If we just dialled up an instrument plugin, zap the sample assignments. + const std::set<SAMPLEINDEX> referencedSamples = pIns->GetSamples(); + bool hasSamples = false; + for(auto sample : referencedSamples) + { + if(sample > 0 && sample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(sample).HasSampleData()) + { + hasSamples = true; + break; + } + } + + if(!hasSamples || Reporting::Confirm("Remove sample associations of this instrument?") == cnfYes) + { + pIns->AssignSample(0); + m_NoteMap.Invalidate(); + } + } + return; + } + } +#endif // NO_PLUGINS + } + + } + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_INSVIEWPLG), false); +} + + +void CCtrlInstruments::OnPPSChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int n = GetDlgItemInt(IDC_EDIT15); + if ((n >= -32) && (n <= 32)) + { + if (pIns->nPPS != (signed char)n) + { + if(!m_startedEdit) PrepareUndo("Set Pitch/Pan Separation"); + pIns->nPPS = (signed char)n; + SetModified(InstrumentHint().Info(), false); + } + } + } +} + + +void CCtrlInstruments::OnPPCChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + int n = m_ComboPPC.GetCurSel(); + if(n >= 0 && n <= NOTE_MAX - NOTE_MIN) + { + if (pIns->nPPC != n) + { + PrepareUndo("Set Pitch/Pan Center"); + pIns->nPPC = static_cast<decltype(pIns->nPPC)>(n); + SetModified(InstrumentHint().Info(), false); + } + } + } +} + + +void CCtrlInstruments::OnAttackChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!IsLocked() && pIns) + { + int n = Clamp(static_cast<int>(GetDlgItemInt(IDC_EDIT2)), 0, MAX_ATTACK_VALUE); + auto newRamp = static_cast<decltype(pIns->nVolRampUp)>(n); + if(pIns->nVolRampUp != newRamp) + { + if(!m_startedEdit) + PrepareUndo("Set Ramping"); + pIns->nVolRampUp = newRamp; + SetModified(InstrumentHint().Info(), false); + } + + m_SliderAttack.SetPos(n); + if(CSpinButtonCtrl *spin = (CSpinButtonCtrl *)GetDlgItem(IDC_SPIN1)) + spin->SetPos(n); + LockControls(); + if (n == 0) SetDlgItemText(IDC_EDIT2, _T("default")); + UnlockControls(); + } +} + + +void CCtrlInstruments::OnEnableCutOff() +{ + const bool enableCutOff = IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED; + + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if (pIns) + { + PrepareUndo("Toggle Cutoff"); + pIns->SetCutoff(pIns->GetCutoff(), enableCutOff); + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if (chn.pModInstrument == pIns) + { + if(enableCutOff) + chn.nCutOff = pIns->GetCutoff(); + else + chn.nCutOff = 0x7F; + } + } + } + UpdateFilterText(); + SetModified(InstrumentHint().Info(), false); + SwitchToView(); +} + + +void CCtrlInstruments::OnEnableResonance() +{ + const bool enableReso = IsDlgButtonChecked(IDC_CHECK3) != BST_UNCHECKED; + + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if (pIns) + { + PrepareUndo("Toggle Resonance"); + pIns->SetResonance(pIns->GetResonance(), enableReso); + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if (chn.pModInstrument == pIns) + { + if (enableReso) + chn.nResonance = pIns->GetResonance(); + else + chn.nResonance = 0; + } + } + } + UpdateFilterText(); + SetModified(InstrumentHint().Info(), false); + SwitchToView(); +} + +void CCtrlInstruments::OnFilterModeChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((!IsLocked()) && (pIns)) + { + FilterMode instFiltermode = static_cast<FilterMode>(m_CbnFilterMode.GetItemData(m_CbnFilterMode.GetCurSel())); + + if(pIns->filterMode != instFiltermode) + { + PrepareUndo("Set Filter Mode"); + pIns->filterMode = instFiltermode; + SetModified(InstrumentHint().Info(), false); + + //Update channel settings where this instrument is active, if required. + if(instFiltermode != FilterMode::Unchanged) + { + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if(chn.pModInstrument == pIns) + chn.nFilterMode = instFiltermode; + } + } + } + + } +} + + +void CCtrlInstruments::OnVScroll(UINT nCode, UINT nPos, CScrollBar *pSB) +{ + // Give focus back to envelope editor when stopping to scroll spin buttons (for instrument preview keyboard focus) + CModControlDlg::OnVScroll(nCode, nPos, pSB); + if (nCode == SB_ENDSCROLL) SwitchToView(); +} + + +void CCtrlInstruments::OnHScroll(UINT nCode, UINT nPos, CScrollBar *pSB) +{ + CModControlDlg::OnHScroll(nCode, nPos, pSB); + if ((m_nInstrument) && (!IsLocked()) && (nCode != SB_ENDSCROLL)) + { + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!pIns) + return; + + auto *pSlider = reinterpret_cast<const CSliderCtrl *>(pSB); + int32 n = pSlider->GetPos(); + bool filterChanged = false; + + if(pSlider == &m_SliderAttack) + { + // Volume ramping (attack) + if(pIns->nVolRampUp != n) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Ramping"); + m_startedHScroll = true; + } + pIns->nVolRampUp = static_cast<decltype(pIns->nVolRampUp)>(n); + SetDlgItemInt(IDC_EDIT2, n); + SetModified(InstrumentHint().Info(), false); + } + } else if(pSlider == &m_SliderVolSwing) + { + // Volume Swing + if((n >= 0) && (n <= 100) && (n != pIns->nVolSwing)) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Volume Random Variation"); + m_startedHScroll = true; + } + pIns->nVolSwing = static_cast<uint8>(n); + SetModified(InstrumentHint().Info(), false); + } + } else if(pSlider == &m_SliderPanSwing) + { + // Pan Swing + if((n >= 0) && (n <= 64) && (n != pIns->nPanSwing)) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Panning Random Variation"); + m_startedHScroll = true; + } + pIns->nPanSwing = static_cast<uint8>(n); + SetModified(InstrumentHint().Info(), false); + } + } else if(pSlider == &m_SliderCutSwing) + { + // Cutoff swing + if((n >= 0) && (n <= 64) && (n != pIns->nCutSwing)) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Cutoff Random Variation"); + m_startedHScroll = true; + } + pIns->nCutSwing = static_cast<uint8>(n); + SetModified(InstrumentHint().Info(), false); + } + } else if(pSlider == &m_SliderResSwing) + { + // Resonance swing + if((n >= 0) && (n <= 64) && (n != pIns->nResSwing)) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Resonance Random Variation"); + m_startedHScroll = true; + } + pIns->nResSwing = static_cast<uint8>(n); + SetModified(InstrumentHint().Info(), false); + } + } else if(pSlider == &m_SliderCutOff) + { + // Filter Cutoff + if((n >= 0) && (n < 0x80) && (n != (int)(pIns->GetCutoff()))) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Cutoff"); + m_startedHScroll = true; + } + pIns->SetCutoff(static_cast<uint8>(n), pIns->IsCutoffEnabled()); + SetModified(InstrumentHint().Info(), false); + UpdateFilterText(); + filterChanged = true; + } + } else if(pSlider == &m_SliderResonance) + { + // Filter Resonance + if((n >= 0) && (n < 0x80) && (n != (int)(pIns->GetResonance()))) + { + if(!m_startedHScroll) + { + PrepareUndo("Set Resonance"); + m_startedHScroll = true; + } + pIns->SetResonance(static_cast<uint8>(n), pIns->IsResonanceEnabled()); + SetModified(InstrumentHint().Info(), false); + UpdateFilterText(); + filterChanged = true; + } + } + + // Update channels + if(filterChanged) + { + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if(chn.pModInstrument == pIns) + { + if(pIns->IsCutoffEnabled()) + chn.nCutOff = pIns->GetCutoff(); + if(pIns->IsResonanceEnabled()) + chn.nResonance = pIns->GetResonance(); + } + } + } + } else if(nCode == SB_ENDSCROLL) + { + m_startedHScroll = false; + } + if ((nCode == SB_ENDSCROLL) || (nCode == SB_THUMBPOSITION)) + { + SwitchToView(); + } + +} + + +void CCtrlInstruments::OnEditSampleMap() +{ + if(m_nInstrument) + { + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if (pIns) + { + PrepareUndo("Edit Sample Map"); + CSampleMapDlg dlg(m_sndFile, m_nInstrument, this); + if (dlg.DoModal() == IDOK) + { + SetModified(InstrumentHint().Info(), true); + m_NoteMap.Invalidate(FALSE); + } else + { + m_modDoc.GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } + } + } +} + + +void CCtrlInstruments::TogglePluginEditor() +{ + if(m_nInstrument) + { + m_modDoc.TogglePluginEditor(static_cast<PLUGINDEX>(m_CbnMixPlug.GetItemData(m_CbnMixPlug.GetCurSel()) - 1), CMainFrame::GetInputHandler()->ShiftPressed()); + } +} + + +BOOL CCtrlInstruments::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler* ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxCtrlInstruments); + + if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } + + } + + return CModControlDlg::PreTranslateMessage(pMsg); +} + +LRESULT CCtrlInstruments::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + switch(wParam) + { + case kcInstrumentCtrlLoad: OnInstrumentOpen(); return wParam; + case kcInstrumentCtrlSave: OnInstrumentSaveOne(); return wParam; + case kcInstrumentCtrlNew: InsertInstrument(false); return wParam; + case kcInstrumentCtrlDuplicate: InsertInstrument(true); return wParam; + } + + return kcNull; +} + + +void CCtrlInstruments::OnCbnSelchangeCombotuning() +{ + if (IsLocked()) return; + + ModInstrument *instr = m_sndFile.Instruments[m_nInstrument]; + if(instr == nullptr) + return; + + size_t sel = m_ComboTuning.GetCurSel(); + if(sel == 0) //Setting IT behavior + { + CriticalSection cs; + PrepareUndo("Reset Tuning"); + instr->SetTuning(nullptr); + cs.Leave(); + + SetModified(InstrumentHint().Info(), true); + return; + } + + sel -= 1; + + if(sel < m_sndFile.GetTuneSpecificTunings().GetNumTunings()) + { + CriticalSection cs; + PrepareUndo("Set Tuning"); + instr->SetTuning(m_sndFile.GetTuneSpecificTunings().GetTuning(sel)); + cs.Leave(); + + SetModified(InstrumentHint().Info(), true); + return; + } + + //Case: Chosen tuning editor to be displayed. + //Creating vector for the CTuningDialog. + CTuningDialog td(this, m_nInstrument, m_sndFile); + td.DoModal(); + if(td.GetModifiedStatus(&m_sndFile.GetTuneSpecificTunings())) + { + m_modDoc.SetModified(); + } + + //Recreating tuning combobox so that possible + //new tuning(s) come visible. + BuildTuningComboBox(); + + m_modDoc.UpdateAllViews(nullptr, GeneralHint().Tunings()); + m_modDoc.UpdateAllViews(nullptr, InstrumentHint().Info()); +} + + +void CCtrlInstruments::UpdateTuningComboBox() +{ + if(m_nInstrument > m_sndFile.GetNumInstruments() + || m_sndFile.Instruments[m_nInstrument] == nullptr) return; + + ModInstrument* const pIns = m_sndFile.Instruments[m_nInstrument]; + if(pIns->pTuning == nullptr) + { + m_ComboTuning.SetCurSel(0); + return; + } + + for(size_t i = 0; i < m_sndFile.GetTuneSpecificTunings().GetNumTunings(); i++) + { + if(pIns->pTuning == m_sndFile.GetTuneSpecificTunings().GetTuning(i)) + { + m_ComboTuning.SetCurSel((int)(i + 1)); + return; + } + } + + Reporting::Notification(MPT_CFORMAT("Tuning {} was not found. Setting to default tuning.")(mpt::ToCString(m_sndFile.Instruments[m_nInstrument]->pTuning->GetName()))); + + CriticalSection cs; + pIns->SetTuning(m_sndFile.GetDefaultTuning()); + + m_modDoc.SetModified(); +} + + +void CCtrlInstruments::OnPluginVelocityHandlingChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!IsLocked() && pIns != nullptr) + { + PlugVelocityHandling n = velocityStyle.GetCheck() != BST_UNCHECKED ? PLUGIN_VELOCITYHANDLING_CHANNEL : PLUGIN_VELOCITYHANDLING_VOLUME; + if(n != pIns->pluginVelocityHandling) + { + PrepareUndo("Set Velocity Handling"); + if(n == PLUGIN_VELOCITYHANDLING_VOLUME && m_CbnPluginVolumeHandling.GetCurSel() == PLUGIN_VOLUMEHANDLING_IGNORE) + { + // This combination doesn't make sense. + m_CbnPluginVolumeHandling.SetCurSel(PLUGIN_VOLUMEHANDLING_MIDI); + } + + pIns->pluginVelocityHandling = n; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnPluginVolumeHandlingChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!IsLocked() && pIns != nullptr) + { + PlugVolumeHandling n = static_cast<PlugVolumeHandling>(m_CbnPluginVolumeHandling.GetCurSel()); + if(n != pIns->pluginVolumeHandling) + { + PrepareUndo("Set Volume Handling"); + + if(velocityStyle.GetCheck() == BST_UNCHECKED && n == PLUGIN_VOLUMEHANDLING_IGNORE) + { + // This combination doesn't make sense. + velocityStyle.SetCheck(BST_CHECKED); + } + + pIns->pluginVolumeHandling = n; + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnPitchWheelDepthChanged() +{ + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if(!IsLocked() && pIns != nullptr) + { + int pwd = GetDlgItemInt(IDC_PITCHWHEELDEPTH, NULL, TRUE); + int lower = -128, upper = 127; + m_SpinPWD.GetRange32(lower, upper); + Limit(pwd, lower, upper); + if(pwd != pIns->midiPWD) + { + if(!m_startedEdit) PrepareUndo("Set Pitch Wheel Depth"); + pIns->midiPWD = static_cast<int8>(pwd); + SetModified(InstrumentHint().Info(), false); + } + } +} + + +void CCtrlInstruments::OnBnClickedCheckPitchtempolock() +{ + if(IsLocked() || !m_nInstrument) return; + + INSTRUMENTINDEX firstIns = m_nInstrument, lastIns = m_nInstrument; + if(CMainFrame::GetInputHandler()->ShiftPressed()) + { + firstIns = 1; + lastIns = m_sndFile.GetNumInstruments(); + } + + m_EditPitchTempoLock.EnableWindow(IsDlgButtonChecked(IDC_CHECK_PITCHTEMPOLOCK)); + TEMPO ptl(0, 0); + bool isZero = false; + if(IsDlgButtonChecked(IDC_CHECK_PITCHTEMPOLOCK)) + { + //Checking what value to put for the wPitchToTempoLock. + if(m_EditPitchTempoLock.GetWindowTextLength() > 0) + { + ptl = m_EditPitchTempoLock.GetTempoValue(); + } + if(!ptl.GetRaw()) + { + ptl = m_sndFile.m_nDefaultTempo; + } + m_EditPitchTempoLock.SetTempoValue(ptl); + isZero = true; + } + + for(INSTRUMENTINDEX i = firstIns; i <= lastIns; i++) + { + if(m_sndFile.Instruments[i] != nullptr && (m_sndFile.Instruments[i]->pitchToTempoLock.GetRaw() == 0) == isZero) + { + m_modDoc.GetInstrumentUndo().PrepareUndo(i, "Set Pitch/Tempo Lock"); + m_sndFile.Instruments[i]->pitchToTempoLock = ptl; + m_modDoc.SetModified(); + } + } + + m_modDoc.UpdateAllViews(nullptr, InstrumentHint().Info(), this); +} + + +void CCtrlInstruments::OnEnChangeEditPitchTempoLock() +{ + if(IsLocked() || !m_nInstrument || !m_sndFile.Instruments[m_nInstrument]) return; + + TEMPO ptlTempo = m_EditPitchTempoLock.GetTempoValue(); + Limit(ptlTempo, m_sndFile.GetModSpecifications().GetTempoMin(), m_sndFile.GetModSpecifications().GetTempoMax()); + + if(m_sndFile.Instruments[m_nInstrument]->pitchToTempoLock != ptlTempo) + { + if(!m_startedEdit) PrepareUndo("Set Pitch/Tempo Lock"); + m_sndFile.Instruments[m_nInstrument]->pitchToTempoLock = ptlTempo; + m_modDoc.SetModified(); // Only update other views after killing focus + } +} + + +void CCtrlInstruments::OnEnKillFocusEditPitchTempoLock() +{ + //Checking that tempo value is in correct range. + if(IsLocked()) return; + + TEMPO ptlTempo = m_EditPitchTempoLock.GetTempoValue(); + bool changed = false; + const CModSpecifications& specs = m_sndFile.GetModSpecifications(); + + if(ptlTempo < specs.GetTempoMin()) + { + ptlTempo = specs.GetTempoMin(); + changed = true; + } else if(ptlTempo > specs.GetTempoMax()) + { + ptlTempo = specs.GetTempoMax(); + changed = true; + } + if(changed) + { + m_EditPitchTempoLock.SetTempoValue(ptlTempo); + m_modDoc.SetModified(); + } + m_modDoc.UpdateAllViews(nullptr, InstrumentHint().Info(), this); +} + + +void CCtrlInstruments::OnEnKillFocusEditFadeOut() +{ + if(IsLocked() || !m_nInstrument || !m_sndFile.Instruments[m_nInstrument]) return; + + if(m_modDoc.GetModType() == MOD_TYPE_IT) + { + // Coarse fade-out in IT files + BOOL success; + uint32 fadeout = (GetDlgItemInt(IDC_EDIT7, &success, FALSE) + 16) & ~31; + if(success && fadeout != m_sndFile.Instruments[m_nInstrument]->nFadeOut) + { + SetDlgItemInt(IDC_EDIT7, fadeout, FALSE); + } + } +} + + +void CCtrlInstruments::BuildTuningComboBox() +{ + m_ComboTuning.SetRedraw(FALSE); + m_ComboTuning.ResetContent(); + + m_ComboTuning.AddString(_T("OpenMPT IT behaviour")); //<-> Instrument pTuning pointer == NULL + for(const auto &tuning : m_sndFile.GetTuneSpecificTunings()) + { + m_ComboTuning.AddString(mpt::ToCString(tuning->GetName())); + } + m_ComboTuning.AddString(_T("Control Tunings...")); + UpdateTuningComboBox(); + m_ComboTuning.SetRedraw(TRUE); +} + + +void CCtrlInstruments::UpdatePluginList() +{ + m_CbnMixPlug.SetRedraw(FALSE); + m_CbnMixPlug.Clear(); + m_CbnMixPlug.ResetContent(); +#ifndef NO_PLUGINS + m_CbnMixPlug.SetItemData(m_CbnMixPlug.AddString(_T("No plugin")), 0); + AddPluginNamesToCombobox(m_CbnMixPlug, m_sndFile.m_MixPlugins, false); +#endif // NO_PLUGINS + m_CbnMixPlug.Invalidate(FALSE); + m_CbnMixPlug.SetRedraw(TRUE); + ModInstrument *pIns = m_sndFile.Instruments[m_nInstrument]; + if ((pIns) && (pIns->nMixPlug <= MAX_MIXPLUGINS)) m_CbnMixPlug.SetCurSel(pIns->nMixPlug); +} + + +void CCtrlInstruments::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) OnPrevInstrument(); + else if(nButton == XBUTTON2) OnNextInstrument(); + CModControlDlg::OnXButtonUp(nFlags, nButton, point); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.h b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.h new file mode 100644 index 00000000..30ab8d0f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_ins.h @@ -0,0 +1,204 @@ +/* + * Ctrl_ins.h + * ---------- + * Purpose: Instrument tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +class CNoteMapWnd; +class CCtrlInstruments; + +class CNoteMapWnd: public CStatic +{ +protected: + CModDoc &m_modDoc; + CCtrlInstruments &m_pParent; + UINT m_nNote = (NOTE_MIDDLEC - NOTE_MIN), m_nOldNote = 0, m_nOldIns = 0; + INSTRUMENTINDEX m_nInstrument = 0; + int m_cxFont = 0, m_cyFont = 0; + CHANNELINDEX m_noteChannel = 0; + ModCommand::NOTE m_nPlayingNote = NOTE_NONE; + + bool m_bIns = false; + bool m_undo = true; + +private: + void MapTranspose(int nAmount); + void PrepareUndo(const char *description); + +public: + CNoteMapWnd(CCtrlInstruments &parent, CModDoc &document) + : m_modDoc(document) + , m_pParent(parent) + { + EnableActiveAccessibility(); + } + void SetCurrentInstrument(INSTRUMENTINDEX nIns); + void SetCurrentNote(UINT nNote); + void EnterNote(UINT note); + bool HandleChar(WPARAM c); + bool HandleNav(WPARAM k); + void PlayNote(UINT note); + void StopNote(); + + void UpdateAccessibleTitle(); + +public: + //{{AFX_VIRTUAL(CNoteMapWnd) + BOOL PreTranslateMessage(MSG* pMsg) override; + HRESULT get_accName(VARIANT varChild, BSTR *pszName) override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CNoteMapWnd) + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnMButtonDown(UINT flags, CPoint pt) { OnLButtonDown(flags, pt); } + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnLButtonDblClk(UINT, CPoint); + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnSetFocus(CWnd *pOldWnd); + afx_msg void OnKillFocus(CWnd *pNewWnd); + afx_msg BOOL OnEraseBkGnd(CDC *) { return TRUE; } + afx_msg void OnPaint(); + afx_msg void OnMapCopySample(); + afx_msg void OnMapCopyNote(); + afx_msg void OnMapTransposeUp(); + afx_msg void OnMapTransposeDown(); + afx_msg void OnMapReset(); + afx_msg void OnTransposeSamples(); + afx_msg void OnMapRemove(); + afx_msg void OnEditSample(UINT nID); + afx_msg void OnEditSampleMap(); + afx_msg void OnInstrumentDuplicate(); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); //rewbs.customKeys + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +class CCtrlInstruments: public CModControlDlg +{ +protected: + CModControlBar m_ToolBar; + CSpinButtonCtrl m_SpinInstrument, m_SpinFadeOut, m_SpinGlobalVol, m_SpinPanning; + CSpinButtonCtrl m_SpinMidiPR, m_SpinPPS, m_SpinMidiBK, m_SpinPWD; + CComboBox m_ComboNNA, m_ComboDCT, m_ComboDCA, m_ComboPPC, m_CbnMidiCh, m_CbnMixPlug, m_CbnResampling, m_CbnFilterMode, m_CbnPluginVolumeHandling; + CEdit m_EditName, m_EditFileName, m_EditGlobalVol, m_EditPanning, m_EditFadeOut; + CNumberEdit m_EditPPS, m_EditPWD; + CButton m_CheckPanning, m_CheckCutOff, m_CheckResonance, velocityStyle; + CSliderCtrl m_SliderVolSwing, m_SliderPanSwing, m_SliderCutSwing, m_SliderResSwing, m_SliderCutOff, m_SliderResonance; + CNoteMapWnd m_NoteMap; + CSliderCtrl m_SliderAttack; + CSpinButtonCtrl m_SpinAttack; + //Tuning + CComboBox m_ComboTuning; + // Pitch/Tempo lock + CNumberEdit m_EditPitchTempoLock; + CButton m_CheckPitchTempoLock; + + INSTRUMENTINDEX m_nInstrument = 1; + bool m_openendPluginListWithMouse = false; + bool m_startedHScroll = false; + bool m_startedEdit = false; + + void UpdateTuningComboBox(); + void BuildTuningComboBox(); + + void UpdatePluginList(); + +public: + CCtrlInstruments(CModControlView &parent, CModDoc &document); + +public: + void SetModified(InstrumentHint hint, bool updateAll); + BOOL SetCurrentInstrument(UINT nIns, BOOL bUpdNum=TRUE); + bool InsertInstrument(bool duplicate); + bool OpenInstrument(const mpt::PathString &fileName); + bool OpenInstrument(const CSoundFile &sndFile, INSTRUMENTINDEX nInstr); + void SaveInstrument(bool doBatchSave); + BOOL EditSample(UINT nSample); + void UpdateFilterText(); + Setting<LONG> &GetSplitPosRef() override {return TrackerSettings::Instance().glInstrumentWindowHeight;} + +public: + //{{AFX_VIRTUAL(CCtrlInstruments) + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + CRuntimeClass *GetAssociatedViewClass() override; + void RecalcLayout() override; + void OnActivatePage(LPARAM) override; + void OnDeactivatePage() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + LRESULT OnModCtrlMsg(WPARAM wParam, LPARAM lParam) override; + BOOL GetToolTipText(UINT uId, LPTSTR pszText) override; + BOOL PreTranslateMessage(MSG* pMsg) override; + //}}AFX_VIRTUAL +protected: + void PrepareUndo(const char *description); + + //{{AFX_MSG(CCtrlInstruments) + afx_msg void OnEditFocus(); + afx_msg void OnVScroll(UINT nCode, UINT nPos, CScrollBar *pSB); + afx_msg void OnHScroll(UINT nCode, UINT nPos, CScrollBar *pSB); + afx_msg void OnTbnDropDownToolBar(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnInstrumentChanged(); + afx_msg void OnPrevInstrument(); + afx_msg void OnNextInstrument(); + afx_msg void OnInstrumentNew(); + afx_msg void OnInstrumentDuplicate() { InsertInstrument(true); } + afx_msg void OnInstrumentOpen(); + afx_msg void OnInstrumentSave(); + afx_msg void OnInstrumentSaveOne() { SaveInstrument(false); } + afx_msg void OnInstrumentSaveAll() { SaveInstrument(true); } + afx_msg void OnInstrumentPlay(); + afx_msg void OnNameChanged(); + afx_msg void OnFileNameChanged(); + afx_msg void OnFadeOutVolChanged(); + afx_msg void OnGlobalVolChanged(); + afx_msg void OnSetPanningChanged(); + afx_msg void OnPanningChanged(); + afx_msg void OnNNAChanged(); + afx_msg void OnDCTChanged(); + afx_msg void OnDCAChanged(); + afx_msg void OnMPRChanged(); + afx_msg void OnMPRKillFocus(); + afx_msg void OnMBKChanged(); + afx_msg void OnMCHChanged(); + afx_msg void OnResamplingChanged(); + afx_msg void OnMixPlugChanged(); + afx_msg void OnPPSChanged(); + afx_msg void OnPPCChanged(); + afx_msg void OnFilterModeChanged(); + afx_msg void OnPluginVelocityHandlingChanged(); + afx_msg void OnPluginVolumeHandlingChanged(); + afx_msg void OnPitchWheelDepthChanged(); + afx_msg void OnOpenPluginList() { m_openendPluginListWithMouse = true; } + afx_msg void OnAttackChanged(); + afx_msg void OnEnableCutOff(); + afx_msg void OnEnableResonance(); + afx_msg void OnEditSampleMap(); + afx_msg void TogglePluginEditor(); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg void OnCbnSelchangeCombotuning(); + afx_msg void OnEnChangeEditPitchTempoLock(); + afx_msg void OnBnClickedCheckPitchtempolock(); + afx_msg void OnEnKillFocusEditPitchTempoLock(); + afx_msg void OnEnKillFocusEditFadeOut(); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.cpp new file mode 100644 index 00000000..7134fa83 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.cpp @@ -0,0 +1,1302 @@ +/* + * Ctrl_pat.cpp + * ------------ + * Purpose: Pattern tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "ImageLists.h" +#include "Childfrm.h" +#include "Moddoc.h" +#include "../soundlib/mod_specifications.h" +#include "Globals.h" +#include "Ctrl_pat.h" +#include "View_pat.h" +#include "PatternEditorDialogs.h" +#include "ChannelManagerDlg.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + + +////////////////////////////////////////////////////////////// +// CCtrlPatterns + + +BEGIN_MESSAGE_MAP(CCtrlPatterns, CModControlDlg) + //{{AFX_MSG_MAP(CCtrlPatterns) + ON_WM_KEYDOWN() + ON_WM_VSCROLL() + ON_WM_XBUTTONUP() + ON_COMMAND(IDC_BUTTON1, &CCtrlPatterns::OnSequenceNext) + ON_COMMAND(IDC_BUTTON2, &CCtrlPatterns::OnSequencePrev) + ON_COMMAND(ID_PLAYER_PAUSE, &CCtrlPatterns::OnPlayerPause) + ON_COMMAND(IDC_PATTERN_NEW, &CCtrlPatterns::OnPatternNew) + ON_COMMAND(IDC_PATTERN_STOP, &CCtrlPatterns::OnPatternStop) + ON_COMMAND(IDC_PATTERN_PLAY, &CCtrlPatterns::OnPatternPlay) + ON_COMMAND(IDC_PATTERN_PLAYFROMSTART, &CCtrlPatterns::OnPatternPlayFromStart) + ON_COMMAND(IDC_PATTERN_RECORD, &CCtrlPatterns::OnPatternRecord) + ON_COMMAND(IDC_PATTERN_LOOP, &CCtrlPatterns::OnChangeLoopStatus) + ON_COMMAND(ID_PATTERN_PLAYROW, &CCtrlPatterns::OnPatternPlayRow) + ON_COMMAND(ID_PATTERN_CHANNELMANAGER, &CCtrlPatterns::OnChannelManager) + ON_COMMAND(ID_PATTERN_VUMETERS, &CCtrlPatterns::OnPatternVUMeters) + ON_COMMAND(ID_VIEWPLUGNAMES, &CCtrlPatterns::OnPatternViewPlugNames) + ON_COMMAND(ID_NEXTINSTRUMENT, &CCtrlPatterns::OnNextInstrument) + ON_COMMAND(ID_PREVINSTRUMENT, &CCtrlPatterns::OnPrevInstrument) + ON_COMMAND(IDC_PATTERN_FOLLOWSONG, &CCtrlPatterns::OnFollowSong) + ON_COMMAND(ID_PATTERN_CHORDEDIT, &CCtrlPatterns::OnChordEditor) + ON_COMMAND(ID_PATTERN_PROPERTIES, &CCtrlPatterns::OnPatternProperties) + ON_COMMAND(ID_PATTERN_EXPAND, &CCtrlPatterns::OnPatternExpand) + ON_COMMAND(ID_PATTERN_SHRINK, &CCtrlPatterns::OnPatternShrink) + ON_COMMAND(ID_PATTERN_AMPLIFY, &CCtrlPatterns::OnPatternAmplify) + ON_COMMAND(ID_ORDERLIST_NEW, &CCtrlPatterns::OnPatternNew) + ON_COMMAND(ID_ORDERLIST_COPY, &CCtrlPatterns::OnPatternDuplicate) + ON_COMMAND(ID_ORDERLIST_MERGE, &CCtrlPatterns::OnPatternMerge) + ON_COMMAND(ID_PATTERNCOPY, &CCtrlPatterns::OnPatternCopy) + ON_COMMAND(ID_PATTERNPASTE, &CCtrlPatterns::OnPatternPaste) + ON_COMMAND(ID_EDIT_UNDO, &CCtrlPatterns::OnEditUndo) + ON_COMMAND(ID_PATTERNDETAIL_LO, &CCtrlPatterns::OnDetailLo) + ON_COMMAND(ID_PATTERNDETAIL_MED, &CCtrlPatterns::OnDetailMed) + ON_COMMAND(ID_PATTERNDETAIL_HI, &CCtrlPatterns::OnDetailHi) + ON_COMMAND(ID_OVERFLOWPASTE, &CCtrlPatterns::OnToggleOverflowPaste) + ON_CBN_SELCHANGE(IDC_COMBO_INSTRUMENT, &CCtrlPatterns::OnInstrumentChanged) + ON_COMMAND(IDC_PATINSTROPLUGGUI, &CCtrlPatterns::TogglePluginEditor) //rewbs.instroVST + ON_EN_CHANGE(IDC_EDIT_SPACING, &CCtrlPatterns::OnSpacingChanged) + ON_EN_CHANGE(IDC_EDIT_PATTERNNAME, &CCtrlPatterns::OnPatternNameChanged) + ON_EN_CHANGE(IDC_EDIT_SEQUENCE_NAME, &CCtrlPatterns::OnSequenceNameChanged) + ON_EN_CHANGE(IDC_EDIT_SEQNUM, &CCtrlPatterns::OnSequenceNumChanged) + ON_UPDATE_COMMAND_UI(IDC_PATTERN_RECORD,&CCtrlPatterns::OnUpdateRecord) + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CCtrlPatterns::OnToolTipText) + //}}AFX_MSG_MAP + ON_WM_MOUSEWHEEL() +END_MESSAGE_MAP() + +void CCtrlPatterns::DoDataExchange(CDataExchange *pDX) +{ + CModControlDlg::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CCtrlPatterns) + DDX_Control(pDX, IDC_BUTTON1, m_BtnNext); + DDX_Control(pDX, IDC_BUTTON2, m_BtnPrev); + DDX_Control(pDX, IDC_COMBO_INSTRUMENT, m_CbnInstrument); + DDX_Control(pDX, IDC_EDIT_SPACING, m_EditSpacing); + DDX_Control(pDX, IDC_EDIT_PATTERNNAME, m_EditPatName); + DDX_Control(pDX, IDC_EDIT_SEQNUM, m_EditSequence); + DDX_Control(pDX, IDC_SPIN_SPACING, m_SpinSpacing); + DDX_Control(pDX, IDC_SPIN_INSTRUMENT, m_SpinInstrument); + DDX_Control(pDX, IDC_SPIN_SEQNUM, m_SpinSequence); + DDX_Control(pDX, IDC_TOOLBAR1, m_ToolBar); + //}}AFX_DATA_MAP +} + + +const ModSequence &CCtrlPatterns::Order() const { return m_sndFile.Order(); } +ModSequence &CCtrlPatterns::Order() { return m_sndFile.Order(); } + + +CCtrlPatterns::CCtrlPatterns(CModControlView &parent, CModDoc &document) + : CModControlDlg(parent, document), m_OrderList(*this, document) +{ + m_bVUMeters = TrackerSettings::Instance().gbPatternVUMeters; + m_bPluginNames = TrackerSettings::Instance().gbPatternPluginNames; + m_bRecord = TrackerSettings::Instance().gbPatternRecord; +} + + +BOOL CCtrlPatterns::OnInitDialog() +{ + CRect rect, rcOrderList; + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModControlDlg::OnInitDialog(); + EnableToolTips(); + + if(!pMainFrm) + return TRUE; + SetRedraw(FALSE); + LockControls(); + // Order List + m_BtnNext.GetWindowRect(&rect); + ScreenToClient(&rect); + auto margins = Util::ScalePixels(4, m_hWnd); + rcOrderList.left = rect.right + margins; + rcOrderList.top = rect.top; + rcOrderList.bottom = rect.bottom + GetSystemMetrics(SM_CYHSCROLL); + GetClientRect(&rect); + rcOrderList.right = rect.right - margins; + m_OrderList.Init(rcOrderList, pMainFrm->GetGUIFont()); + // Toolbar buttons + m_ToolBar.Init(CMainFrame::GetMainFrame()->m_PatternIcons, CMainFrame::GetMainFrame()->m_PatternIconsDisabled); + m_ToolBar.AddButton(IDC_PATTERN_NEW, TIMAGE_PATTERN_NEW); + m_ToolBar.AddButton(IDC_PATTERN_PLAY, TIMAGE_PATTERN_PLAY); + m_ToolBar.AddButton(IDC_PATTERN_PLAYFROMSTART, TIMAGE_PATTERN_RESTART); + m_ToolBar.AddButton(IDC_PATTERN_STOP, TIMAGE_PATTERN_STOP); + m_ToolBar.AddButton(ID_PATTERN_PLAYROW, TIMAGE_PATTERN_PLAYROW); + m_ToolBar.AddButton(IDC_PATTERN_RECORD, TIMAGE_PATTERN_RECORD, TBSTYLE_CHECK, (m_bRecord ? TBSTATE_CHECKED : 0) | TBSTATE_ENABLED); + m_ToolBar.AddButton(ID_SEPARATOR, 0, TBSTYLE_SEP); + m_ToolBar.AddButton(ID_PATTERN_VUMETERS, TIMAGE_PATTERN_VUMETERS, TBSTYLE_CHECK, (m_bVUMeters ? TBSTATE_CHECKED : 0) | TBSTATE_ENABLED); + m_ToolBar.AddButton(ID_VIEWPLUGNAMES, TIMAGE_PATTERN_PLUGINS, TBSTYLE_CHECK, (m_bPluginNames ? TBSTATE_CHECKED : 0) | TBSTATE_ENABLED); + m_ToolBar.AddButton(ID_PATTERN_CHANNELMANAGER, TIMAGE_CHANNELMANAGER); + m_ToolBar.AddButton(ID_SEPARATOR, 0, TBSTYLE_SEP); + m_ToolBar.AddButton(ID_PATTERN_MIDIMACRO, TIMAGE_MACROEDITOR); + m_ToolBar.AddButton(ID_PATTERN_CHORDEDIT, TIMAGE_CHORDEDITOR); + m_ToolBar.AddButton(ID_SEPARATOR, 0, TBSTYLE_SEP); + m_ToolBar.AddButton(ID_EDIT_UNDO, TIMAGE_UNDO); + m_ToolBar.AddButton(ID_PATTERN_PROPERTIES, TIMAGE_PATTERN_PROPERTIES); + m_ToolBar.AddButton(ID_PATTERN_EXPAND, TIMAGE_PATTERN_EXPAND); + m_ToolBar.AddButton(ID_PATTERN_SHRINK, TIMAGE_PATTERN_SHRINK); + // m_ToolBar.AddButton(ID_PATTERN_AMPLIFY, TIMAGE_SAMPLE_AMPLIFY); + m_ToolBar.AddButton(ID_SEPARATOR, 0, TBSTYLE_SEP); + m_ToolBar.AddButton(ID_PATTERNDETAIL_LO, TIMAGE_PATTERN_DETAIL_LO, TBSTYLE_CHECK, TBSTATE_ENABLED); + m_ToolBar.AddButton(ID_PATTERNDETAIL_MED, TIMAGE_PATTERN_DETAIL_MED, TBSTYLE_CHECK, TBSTATE_ENABLED); + m_ToolBar.AddButton(ID_PATTERNDETAIL_HI, TIMAGE_PATTERN_DETAIL_HI, TBSTYLE_CHECK, TBSTATE_ENABLED | TBSTATE_CHECKED); + m_ToolBar.AddButton(ID_SEPARATOR, 0, TBSTYLE_SEP); + m_ToolBar.AddButton(ID_OVERFLOWPASTE, TIMAGE_PATTERN_OVERFLOWPASTE, TBSTYLE_CHECK, ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) ? TBSTATE_CHECKED : 0) | TBSTATE_ENABLED); + + // Special edit controls -> tab switch to view + m_EditSequence.SetParent(this); + m_EditSpacing.SetParent(this); + m_EditPatName.SetParent(this); + m_EditPatName.SetLimitText(MAX_PATTERNNAME - 1); + // Spin controls + m_SpinSpacing.SetRange32(0, MAX_SPACING); + m_SpinSpacing.SetPos(TrackerSettings::Instance().gnPatternSpacing); + + m_SpinInstrument.SetRange32(-1, 1); + m_SpinInstrument.SetPos(0); + + SetDlgItemInt(IDC_EDIT_SPACING, TrackerSettings::Instance().gnPatternSpacing); + CheckDlgButton(IDC_PATTERN_FOLLOWSONG, !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FOLLOWSONGOFF)); + + m_SpinSequence.SetRange32(1, m_sndFile.Order.GetNumSequences()); + m_SpinSequence.SetPos(m_sndFile.Order.GetCurrentSequenceIndex() + 1); + SetDlgItemText(IDC_EDIT_SEQUENCE_NAME, mpt::ToCString(Order().GetName())); + + m_OrderList.SetFocus(); + + UpdateView(PatternHint().Names().ModType(), NULL); + RecalcLayout(); + + m_bInitialized = TRUE; + UnlockControls(); + + SetRedraw(TRUE); + return FALSE; +} + + +void CCtrlPatterns::RecalcLayout() +{ + // Update Order List Position + if(m_OrderList.m_hWnd) + { + CRect rect; + int cx, cy, cellcx; + + m_BtnNext.GetWindowRect(&rect); + ScreenToClient(&rect); + cx = -(rect.right + 4); + cy = rect.bottom - rect.top + GetSystemMetrics(SM_CYHSCROLL); + GetClientRect(&rect); + cx += rect.right - 8; + cellcx = m_OrderList.GetFontWidth(); + if(cellcx > 0) + cx -= (cx % cellcx); + cx += 2; + if((cx > 0) && (cy > 0)) + { + m_OrderList.SetWindowPos(NULL, 0, 0, cx, cy, SWP_NOMOVE | SWP_NOZORDER | SWP_DRAWFRAME); + } + } +} + + +void CCtrlPatterns::UpdateView(UpdateHint hint, CObject *pObj) +{ + m_OrderList.UpdateView(hint, pObj); + FlagSet<HintType> hintType = hint.GetType(); + + const bool updateAll = hintType[HINT_MODTYPE]; + const bool updateSeq = hint.GetCategory() == HINTCAT_SEQUENCE; + const bool updatePlug = hint.GetCategory() == HINTCAT_PLUGINS && hintType[HINT_MIXPLUGINS]; + const PatternHint patternHint = hint.ToType<PatternHint>(); + + if(updateAll || (updateSeq && hintType[HINT_SEQNAMES])) + { + SetDlgItemText(IDC_EDIT_SEQUENCE_NAME, mpt::ToCString(Order().GetName())); + } + + if(updateAll || (updateSeq && hintType[HINT_MODSEQUENCE])) + { + m_SpinSequence.SetRange(1, m_sndFile.Order.GetNumSequences()); + m_SpinSequence.SetPos(m_sndFile.Order.GetCurrentSequenceIndex() + 1); + + // Enable/disable multisequence controls according the current modtype. + const BOOL isMultiSeqAvail = (m_sndFile.GetModSpecifications().sequencesMax > 1 || m_sndFile.Order.GetNumSequences() > 1) ? TRUE : FALSE; + GetDlgItem(IDC_STATIC_SEQUENCE_NAME)->EnableWindow(isMultiSeqAvail); + GetDlgItem(IDC_EDIT_SEQUENCE_NAME)->EnableWindow(isMultiSeqAvail); + GetDlgItem(IDC_EDIT_SEQNUM)->EnableWindow(isMultiSeqAvail); + GetDlgItem(IDC_SPIN_SEQNUM)->EnableWindow(isMultiSeqAvail); + } + + if(updateAll || updatePlug) + { + GetDlgItem(IDC_PATINSTROPLUGGUI)->EnableWindow(HasValidPlug(m_nInstrument) ? TRUE : FALSE); + } + + if(updateAll) + { + // Enable/disable pattern names + const BOOL isPatNameAvail = m_sndFile.GetModSpecifications().hasPatternNames ? TRUE : FALSE; + GetDlgItem(IDC_STATIC_PATTERNNAME)->EnableWindow(isPatNameAvail); + GetDlgItem(IDC_EDIT_PATTERNNAME)->EnableWindow(isPatNameAvail); + } + + if(hintType[HINT_MPTOPTIONS]) + { + m_ToolBar.UpdateStyle(); + m_ToolBar.SetState(ID_OVERFLOWPASTE, ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) ? TBSTATE_CHECKED : 0) | TBSTATE_ENABLED); + } + + bool instrPluginsChanged = false; + if(hint.GetCategory() == HINTCAT_PLUGINS && hintType[HINT_PLUGINNAMES]) + { + const auto changedPlug = hint.ToType<PluginHint>().GetPlugin(); + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + const auto ins = m_sndFile.Instruments[i]; + if(ins != nullptr && (!changedPlug && ins->nMixPlug != 0) || (changedPlug && ins->nMixPlug == changedPlug)) + { + instrPluginsChanged = true; + break; + } + } + } + + const bool updatePatNames = patternHint.GetType()[HINT_PATNAMES]; + const bool updateSmpNames = hint.GetCategory() == HINTCAT_SAMPLES && hintType[HINT_SMPNAMES]; + const bool updateInsNames = (hint.GetCategory() == HINTCAT_INSTRUMENTS && hintType[HINT_INSNAMES]) || instrPluginsChanged; + if(updateAll || updatePatNames || updateSmpNames || updateInsNames) + { + LockControls(); + CString s; + if(updateAll || updateSmpNames || updateInsNames) + { + constexpr TCHAR szSplitFormat[] = _T("%02u %s %02u: %s/%s"); + UINT nPos = 0; + m_CbnInstrument.SetRedraw(FALSE); + m_CbnInstrument.ResetContent(); + m_CbnInstrument.SetItemData(m_CbnInstrument.AddString(_T(" No Instrument")), 0); + const INSTRUMENTINDEX nSplitIns = m_modDoc.GetSplitKeyboardSettings().splitInstrument; + const ModCommand::NOTE noteSplit = 1 + m_modDoc.GetSplitKeyboardSettings().splitNote; + const CString sSplitInsName = m_modDoc.GetPatternViewInstrumentName(nSplitIns, true, false); + if(m_sndFile.GetNumInstruments()) + { + // Show instrument names + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + if(m_sndFile.Instruments[i] == nullptr) + continue; + + CString sDisplayName; + if(m_modDoc.GetSplitKeyboardSettings().IsSplitActive()) + { + s.Format(szSplitFormat, + nSplitIns, + mpt::ToCString(m_sndFile.GetNoteName(noteSplit, nSplitIns)).GetString(), + i, + sSplitInsName.GetString(), + m_modDoc.GetPatternViewInstrumentName(i, true, false).GetString()); + sDisplayName = s; + } + else + sDisplayName = m_modDoc.GetPatternViewInstrumentName(i); + + UINT n = m_CbnInstrument.AddString(sDisplayName); + if(n == m_nInstrument) nPos = n; + m_CbnInstrument.SetItemData(n, i); + } + } else + { + // Show sample names + SAMPLEINDEX nmax = m_sndFile.GetNumSamples(); + for(SAMPLEINDEX i = 1; i <= nmax; i++) if (m_sndFile.GetSample(i).HasSampleData() || m_sndFile.GetSample(i).uFlags[CHN_ADLIB]) + { + if (m_modDoc.GetSplitKeyboardSettings().IsSplitActive()) + s.Format(szSplitFormat, + nSplitIns, + mpt::ToCString(m_sndFile.GetNoteName(noteSplit, nSplitIns)).GetString(), + i, + mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[nSplitIns]).GetString(), + mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[i]).GetString()); + else + s.Format(_T("%02u: %s"), + i, + mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[i]).GetString()); + + UINT n = m_CbnInstrument.AddString(s); + if(n == m_nInstrument) nPos = n; + m_CbnInstrument.SetItemData(n, i); + } + } + m_CbnInstrument.SetCurSel(nPos); + m_CbnInstrument.SetRedraw(TRUE); + m_CbnInstrument.Invalidate(FALSE); + } + if(updateAll || updatePatNames) + { + PATTERNINDEX nPat; + if(patternHint.GetType()[HINT_PATNAMES]) + nPat = patternHint.GetPattern(); + else + nPat = (PATTERNINDEX)SendViewMessage(VIEWMSG_GETCURRENTPATTERN); + if(m_sndFile.Patterns.IsValidIndex(nPat)) + { + m_EditPatName.SetWindowText(mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.Patterns[nPat].GetName())); + } + + BOOL bXMIT = (m_sndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE; + m_ToolBar.EnableButton(ID_PATTERN_MIDIMACRO, bXMIT); + m_ToolBar.EnableButton(ID_PATTERN_PROPERTIES, bXMIT); + m_ToolBar.EnableButton(ID_PATTERN_EXPAND, bXMIT); + m_ToolBar.EnableButton(ID_PATTERN_SHRINK, bXMIT); + } + UnlockControls(); + } + if(hintType[HINT_MODTYPE | HINT_UNDO]) + { + m_ToolBar.EnableButton(ID_EDIT_UNDO, m_modDoc.GetPatternUndo().CanUndo()); + } +} + + +CRuntimeClass *CCtrlPatterns::GetAssociatedViewClass() +{ + return RUNTIME_CLASS(CViewPattern); +} + + +LRESULT CCtrlPatterns::OnModCtrlMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case CTRLMSG_GETCURRENTINSTRUMENT: + return m_nInstrument; + + case CTRLMSG_GETCURRENTPATTERN: + return m_OrderList.GetCurrentPattern(); + + case CTRLMSG_PATTERNCHANGED: + UpdateView(PatternHint(static_cast<PATTERNINDEX>(lParam)).Names()); + break; + + case CTRLMSG_PAT_PREVINSTRUMENT: + OnPrevInstrument(); + break; + + case CTRLMSG_PAT_NEXTINSTRUMENT: + OnNextInstrument(); + break; + + case CTRLMSG_NOTIFYCURRENTORDER: + if(m_OrderList.GetCurSel().GetSelCount() > 1 || m_OrderList.m_bDragging) + { + // Only update play cursor in case there's a selection + m_OrderList.Invalidate(FALSE); + break; + } + // Otherwise, just act the same as a normal selection change + [[fallthrough]]; + case CTRLMSG_SETCURRENTORDER: + // Set order list selection and refresh GUI if change successful + m_OrderList.SetCurSel(static_cast<ORDERINDEX>(lParam), false, false, true); + break; + + case CTRLMSG_FORCEREFRESH: + //refresh GUI + m_OrderList.InvalidateRect(NULL, FALSE); + break; + + case CTRLMSG_GETCURRENTORDER: + return m_OrderList.GetCurSel(true).firstOrd; + + case CTRLMSG_SETCURRENTINSTRUMENT: + case CTRLMSG_PAT_SETINSTRUMENT: + return SetCurrentInstrument(static_cast<uint32>(lParam)); + + case CTRLMSG_SETVIEWWND: + { + SendViewMessage(VIEWMSG_FOLLOWSONG, IsDlgButtonChecked(IDC_PATTERN_FOLLOWSONG)); + SendViewMessage(VIEWMSG_PATTERNLOOP, (m_sndFile.m_SongFlags & SONG_PATTERNLOOP) ? TRUE : FALSE); + OnSpacingChanged(); + SendViewMessage(VIEWMSG_SETDETAIL, m_nDetailLevel); + SendViewMessage(VIEWMSG_SETRECORD, m_bRecord); + SendViewMessage(VIEWMSG_SETVUMETERS, m_bVUMeters); + SendViewMessage(VIEWMSG_SETPLUGINNAMES, m_bPluginNames); + } + break; + + case CTRLMSG_SETSPACING: + SetDlgItemInt(IDC_EDIT_SPACING, static_cast<UINT>(lParam)); + break; + + case CTRLMSG_SETFOCUS: + GetParentFrame()->SetActiveView(&m_parent); + m_OrderList.SetFocus(); + break; + + case CTRLMSG_SETRECORD: + if (lParam >= 0) m_bRecord = (BOOL)(lParam); else m_bRecord = !m_bRecord; + m_ToolBar.SetState(IDC_PATTERN_RECORD, ((m_bRecord) ? TBSTATE_CHECKED : 0)|TBSTATE_ENABLED); + TrackerSettings::Instance().gbPatternRecord = (m_bRecord != 0); + SendViewMessage(VIEWMSG_SETRECORD, m_bRecord); + break; + + case CTRLMSG_PREVORDER: + m_OrderList.SetCurSel(Order().GetPreviousOrderIgnoringSkips(m_OrderList.GetCurSel(true).firstOrd), true); + break; + + case CTRLMSG_NEXTORDER: + m_OrderList.SetCurSel(Order().GetNextOrderIgnoringSkips(m_OrderList.GetCurSel(true).firstOrd), true); + break; + + //rewbs.customKeys + case CTRLMSG_PAT_FOLLOWSONG: + // parameters: 0 = turn off, 1 = toggle + { + UINT state = FALSE; + if(lParam == 1) // toggle + { + state = !IsDlgButtonChecked(IDC_PATTERN_FOLLOWSONG); + } + CheckDlgButton(IDC_PATTERN_FOLLOWSONG, state); + OnFollowSong(); + } + break; + + case CTRLMSG_PAT_LOOP: + { + bool setLoop = false; + if (lParam == -1) + { + //Toggle loop state + setLoop = !m_sndFile.m_SongFlags[SONG_PATTERNLOOP]; + } else + { + setLoop = (lParam != 0); + } + + m_sndFile.m_SongFlags.set(SONG_PATTERNLOOP, setLoop); + CheckDlgButton(IDC_PATTERN_LOOP, setLoop ? BST_CHECKED : BST_UNCHECKED); + break; + } + case CTRLMSG_PAT_NEWPATTERN: + OnPatternNew(); + break; + + case CTRLMSG_PAT_DUPPATTERN: + OnPatternDuplicate(); + break; + + case CTRLMSG_PAT_SETSEQUENCE: + m_OrderList.SelectSequence(static_cast<SEQUENCEINDEX>(lParam)); + UpdateView(SequenceHint(static_cast<SEQUENCEINDEX>(lParam)).Names(), nullptr); + break; + + default: + return CModControlDlg::OnModCtrlMsg(wParam, lParam); + } + return 0; +} + + +void CCtrlPatterns::SetCurrentPattern(PATTERNINDEX nPat) +{ + SendViewMessage(VIEWMSG_SETCURRENTPATTERN, (LPARAM)nPat); +} + + +BOOL CCtrlPatterns::SetCurrentInstrument(UINT nIns) +{ + if(nIns == m_nInstrument) + return TRUE; + int n = m_CbnInstrument.GetCount(); + for(int i = 0; i < n; i++) + { + if(m_CbnInstrument.GetItemData(i) == nIns) + { + m_CbnInstrument.SetCurSel(i); + m_nInstrument = static_cast<INSTRUMENTINDEX>(nIns); + GetDlgItem(IDC_PATINSTROPLUGGUI)->EnableWindow(HasValidPlug(m_nInstrument) ? TRUE : FALSE); + return TRUE; + } + } + return FALSE; +} + + +//////////////////////////////////////////////////////////// +// CCtrlPatterns messages + +void CCtrlPatterns::OnActivatePage(LPARAM lParam) +{ + int nIns = m_parent.GetInstrumentChange(); + if(nIns > 0) + { + SetCurrentInstrument(nIns); + } + + if(!(lParam & 0x80000000)) + { + // Pattern item + auto pat = static_cast<PATTERNINDEX>(lParam & 0xFFFF); + if(m_sndFile.Patterns.IsValidIndex(pat)) + { + for(SEQUENCEINDEX seq = 0; seq < m_sndFile.Order.GetNumSequences(); seq++) + { + if(ORDERINDEX ord = m_sndFile.Order(seq).FindOrder(pat); ord != ORDERINDEX_INVALID) + { + m_OrderList.SelectSequence(seq); + m_OrderList.SetCurSel(ord, true); + UpdateView(SequenceHint(seq).Names(), nullptr); + break; + } + } + } + SetCurrentPattern(pat); + } else if((lParam & 0x80000000)) + { + // Order item + auto ord = static_cast<ORDERINDEX>(lParam & 0xFFFF); + auto seq = static_cast<SEQUENCEINDEX>((lParam >> 16) & 0x7FFF); + if(seq < m_sndFile.Order.GetNumSequences()) + { + m_OrderList.SelectSequence(seq); + const auto &order = Order(); + if(ord < order.size()) + { + m_OrderList.SetCurSel(ord); + SetCurrentPattern(order[ord]); + } + UpdateView(SequenceHint(static_cast<SEQUENCEINDEX>(seq)).Names(), nullptr); + } + } + if(m_hWndView) + { + OnSpacingChanged(); + if(m_bRecord) + SendViewMessage(VIEWMSG_SETRECORD, m_bRecord); + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + + // Restore all save pattern state, except pattern number which we might have just set. + PATTERNVIEWSTATE &patternViewState = pFrame->GetPatternViewState(); + if(patternViewState.initialOrder != ORDERINDEX_INVALID) + { + if(CMainFrame::GetMainFrame()->GetModPlaying() != &m_modDoc) + m_OrderList.SetCurSel(patternViewState.initialOrder); + patternViewState.initialOrder = ORDERINDEX_INVALID; + } + + patternViewState.nPattern = static_cast<PATTERNINDEX>(SendViewMessage(VIEWMSG_GETCURRENTPATTERN)); + SendViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&patternViewState); + + SwitchToView(); + } + + // Combo boxes randomly disappear without this... why? + Invalidate(); +} + + +void CCtrlPatterns::OnDeactivatePage() +{ + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + if((pFrame) && (m_hWndView)) + SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&pFrame->GetPatternViewState()); +} + + +void CCtrlPatterns::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) +{ + CModControlDlg::OnVScroll(nSBCode, nPos, pScrollBar); + short int pos = (short int)m_SpinInstrument.GetPos(); + if(pos) + { + m_SpinInstrument.SetPos(0); + if(pos < 0) + OnPrevInstrument(); + else + OnNextInstrument(); + } +} + + +void CCtrlPatterns::OnSequencePrev() +{ + m_OrderList.SetCurSel(m_OrderList.GetCurSel(true).firstOrd - 1); + m_OrderList.SetFocus(); +} + + +void CCtrlPatterns::OnSequenceNext() +{ + m_OrderList.SetCurSel(m_OrderList.GetCurSel(true).firstOrd + 1); + m_OrderList.SetFocus(); +} + + +void CCtrlPatterns::OnChannelManager() +{ + m_modDoc.OnChannelManager(); +} + + +void CCtrlPatterns::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) +{ + CModControlDlg::OnKeyDown(nChar, nRepCnt, nFlags); +} + + +void CCtrlPatterns::OnSpacingChanged() +{ + if((m_EditSpacing.m_hWnd) && (m_EditSpacing.GetWindowTextLength() > 0)) + { + TrackerSettings::Instance().gnPatternSpacing = GetDlgItemInt(IDC_EDIT_SPACING); + if(TrackerSettings::Instance().gnPatternSpacing > MAX_SPACING) + { + TrackerSettings::Instance().gnPatternSpacing = MAX_SPACING; + SetDlgItemInt(IDC_EDIT_SPACING, TrackerSettings::Instance().gnPatternSpacing, FALSE); + } + SendViewMessage(VIEWMSG_SETSPACING, TrackerSettings::Instance().gnPatternSpacing); + } +} + + +void CCtrlPatterns::OnInstrumentChanged() +{ + int n = m_CbnInstrument.GetCurSel(); + if(n >= 0) + { + n = static_cast<int>(m_CbnInstrument.GetItemData(n)); + int nmax = (m_sndFile.m_nInstruments) ? m_sndFile.m_nInstruments : m_sndFile.m_nSamples; + if((n >= 0) && (n <= nmax) && (n != (int)m_nInstrument)) + { + m_nInstrument = static_cast<INSTRUMENTINDEX>(n); + m_parent.InstrumentChanged(m_nInstrument); + } + SwitchToView(); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_PATINSTROPLUGGUI), HasValidPlug(m_nInstrument)); + } +} + + +void CCtrlPatterns::OnPrevInstrument() +{ + int n = m_CbnInstrument.GetCount(); + if(n > 0) + { + int pos = m_CbnInstrument.GetCurSel(); + if(pos > 0) + pos--; + else + pos = n - 1; + m_CbnInstrument.SetCurSel(pos); + OnInstrumentChanged(); + } +} + + +void CCtrlPatterns::OnNextInstrument() +{ + int n = m_CbnInstrument.GetCount(); + if(n > 0) + { + int pos = m_CbnInstrument.GetCurSel() + 1; + if(pos >= n) + pos = 0; + m_CbnInstrument.SetCurSel(pos); + OnInstrumentChanged(); + } +} + + +void CCtrlPatterns::OnPlayerPause() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->PauseMod(); +} + + +void CCtrlPatterns::OnPatternNew() +{ + const auto &order = Order(); + ORDERINDEX curOrd = m_OrderList.GetCurSel(true).firstOrd; + PATTERNINDEX curPat = (curOrd < order.size()) ? order[curOrd] : 0; + ROWINDEX rows = 64; + if(m_sndFile.Patterns.IsValidPat(curPat)) + { + // Only if the current oder is already occupied, create a new pattern at the next position. + curOrd++; + } else + { + // Use currently edited pattern for new pattern length + curPat = static_cast<PATTERNINDEX>(SendViewMessage(VIEWMSG_GETCURRENTPATTERN)); + } + if(m_sndFile.Patterns.IsValidPat(curPat)) + { + rows = m_sndFile.Patterns[curPat].GetNumRows(); + } + rows = Clamp(rows, m_sndFile.GetModSpecifications().patternRowsMin, m_sndFile.GetModSpecifications().patternRowsMax); + const PATTERNINDEX newPat = m_modDoc.InsertPattern(rows, curOrd); + if(m_sndFile.Patterns.IsValidPat(newPat)) + { + // update time signature + if(m_sndFile.Patterns.IsValidIndex(curPat)) + { + if(m_sndFile.Patterns[curPat].GetOverrideSignature()) + m_sndFile.Patterns[newPat].SetSignature(m_sndFile.Patterns[curPat].GetRowsPerBeat(), m_sndFile.Patterns[curPat].GetRowsPerMeasure()); + if(m_sndFile.Patterns[curPat].HasTempoSwing()) + m_sndFile.Patterns[newPat].SetTempoSwing(m_sndFile.Patterns[curPat].GetTempoSwing()); + } + // move to new pattern + m_OrderList.SetCurSel(curOrd); + m_OrderList.Invalidate(FALSE); + SetCurrentPattern(newPat); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(NULL, PatternHint(newPat).Names(), this); + m_modDoc.UpdateAllViews(NULL, SequenceHint().Data(), this); + SwitchToView(); + } +} + + +// Duplicates one or more patterns. +void CCtrlPatterns::OnPatternDuplicate() +{ + OrdSelection selection = m_OrderList.GetCurSel(); + const ORDERINDEX insertFrom = selection.firstOrd; + const ORDERINDEX insertWhere = selection.lastOrd + 1u; + if(insertWhere >= m_sndFile.GetModSpecifications().ordersMax) + return; + const ORDERINDEX insertCount = std::min(selection.GetSelCount(), static_cast<ORDERINDEX>(m_sndFile.GetModSpecifications().ordersMax - insertWhere)); + if(!insertCount) + return; + + bool success = false, outOfPatterns = false; + // Has this pattern been duplicated already? (for multiselect) + std::vector<PATTERNINDEX> patReplaceIndex(m_sndFile.Patterns.Size(), PATTERNINDEX_INVALID); + + ModSequence &order = Order(); + for(ORDERINDEX i = 0; i < insertCount; i++) + { + PATTERNINDEX curPat = order[insertFrom + i]; + if(curPat < patReplaceIndex.size() && patReplaceIndex[curPat] == PATTERNINDEX_INVALID) + { + PATTERNINDEX newPat = m_sndFile.Patterns.Duplicate(curPat, true); + if(newPat != PATTERNINDEX_INVALID) + { + order.insert(insertWhere + i, 1, newPat); + success = true; + // Mark as duplicated, so if this pattern is to be duplicated again, the same new pattern number is inserted into the order list. + patReplaceIndex[curPat] = newPat; + } else + { + if(m_sndFile.Patterns.IsValidPat(curPat)) + outOfPatterns = true; + continue; + } + } else + { + // Invalid pattern, or it has been duplicated before (multiselect) + PATTERNINDEX newPat; + if(curPat < patReplaceIndex.size() && patReplaceIndex[curPat] != PATTERNINDEX_INVALID) + { + // Take care of patterns that have been duplicated before + newPat = patReplaceIndex[curPat]; + } else + { + newPat = order[insertFrom + i]; + } + + order.insert(insertWhere + i, 1, newPat); + + success = true; + } + } + if(success) + { + m_OrderList.InsertUpdatePlaystate(selection.firstOrd, selection.lastOrd); + + m_OrderList.Invalidate(FALSE); + m_OrderList.SetCurSel(insertWhere, true, false, true); + + // If the first duplicated order is e.g. a +++ item, we need to move the pattern display on or else we'll still edit the previously shown pattern. + ORDERINDEX showPattern = std::min(insertWhere, order.GetLastIndex()); + while(!order.IsValidPat(showPattern) && showPattern < order.GetLastIndex()) + { + showPattern++; + } + SetCurrentPattern(order[showPattern]); + + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); + m_modDoc.UpdateAllViews(nullptr, PatternHint(PATTERNINDEX_INVALID).Names(), this); + if(selection.lastOrd != selection.firstOrd) + m_OrderList.m_nScrollPos2nd = insertWhere + insertCount - 1u; + } + if(outOfPatterns) + { + const auto &specs = m_sndFile.GetModSpecifications(); + Reporting::Error(MPT_AFORMAT("Pattern limit of the {} format ({} patterns) has been reached.")(mpt::ToUpperCaseAscii(specs.fileExtension), specs.patternsMax), "Duplicate Patterns"); + } + SwitchToView(); +} + + +// Merges one or more patterns into a single pattern +void CCtrlPatterns::OnPatternMerge() +{ + const OrdSelection selection = m_OrderList.GetCurSel(); + const ORDERINDEX firstOrder = selection.firstOrd; + const ORDERINDEX numOrders = selection.GetSelCount(); + + // Get the total number of lines to be merged + ROWINDEX numRows = 0u; + ModSequence &order = Order(); + for(ORDERINDEX i = 0; i < numOrders; i++) + { + PATTERNINDEX pat = order[firstOrder + i]; + if(m_sndFile.Patterns.IsValidPat(pat)) + numRows += m_sndFile.Patterns[pat].GetNumRows(); + } + if(!numRows || numOrders < 2) + { + MessageBeep(MB_ICONWARNING); + SwitchToView(); + return; + } + + // Try to create a new pattern for the merge + const auto &specs = m_sndFile.GetModSpecifications(); + const auto format = mpt::ToUpperCaseAscii(specs.fileExtension); + if(numRows > specs.patternRowsMax) + { + Reporting::Error(MPT_AFORMAT("Merged pattern size ({} rows) exceeds the row limit ({} rows) of the {} format.")(numRows, specs.patternRowsMax, format), "Merge Patterns"); + SwitchToView(); + return; + } + + CriticalSection cs; + const PATTERNINDEX newPat = m_sndFile.Patterns.InsertAny(std::max(numRows, specs.patternRowsMin), true); + if(newPat == PATTERNINDEX_INVALID) + { + cs.Leave(); + Reporting::Error(MPT_AFORMAT("Pattern limit of the {} format ({} patterns) has been reached.")(format, specs.patternsMax), "Merge Patterns"); + SwitchToView(); + return; + } + + auto &pattern = m_sndFile.Patterns[newPat]; + auto it = pattern.begin(); + for(ORDERINDEX i = 0; i < numOrders; i++) + { + PATTERNINDEX pat = order[firstOrder + i]; + if(m_sndFile.Patterns.IsValidPat(pat)) + it = std::copy(m_sndFile.Patterns[pat].begin(), m_sndFile.Patterns[pat].end(), it); + } + + if(pattern.GetNumRows() > numRows) + pattern.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(numRows - 1).RetryNextRow()); + + // Remove the merged patterns... + order.Remove(selection.firstOrd, selection.lastOrd); + m_OrderList.DeleteUpdatePlaystate(selection.firstOrd, selection.lastOrd); + // ...and insert the new one + order.insert(firstOrder, 1, newPat); + m_OrderList.InsertUpdatePlaystate(firstOrder, firstOrder); + + m_OrderList.Invalidate(FALSE); + m_OrderList.SetSelection(firstOrder); + SetCurrentPattern(newPat); + + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); + m_modDoc.UpdateAllViews(nullptr, PatternHint(PATTERNINDEX_INVALID).Names(), this); + + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternStop() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->PauseMod(&m_modDoc); + m_sndFile.ResetChannels(); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternPlay() +{ + m_modDoc.OnPatternPlay(); + SwitchToView(); +} + +//rewbs.playSongFromCursor +void CCtrlPatterns::OnPatternPlayNoLoop() +{ + m_modDoc.OnPatternPlayNoLoop(); + SwitchToView(); +} +//end rewbs.playSongFromCursor + +void CCtrlPatterns::OnPatternPlayFromStart() +{ + m_modDoc.OnPatternRestart(); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternRecord() +{ + UINT nState = m_ToolBar.GetState(IDC_PATTERN_RECORD); + m_bRecord = ((nState & TBSTATE_CHECKED) != 0); + TrackerSettings::Instance().gbPatternRecord = (m_bRecord != 0); + SendViewMessage(VIEWMSG_SETRECORD, m_bRecord); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternVUMeters() +{ + UINT nState = m_ToolBar.GetState(ID_PATTERN_VUMETERS); + m_bVUMeters = ((nState & TBSTATE_CHECKED) != 0); + TrackerSettings::Instance().gbPatternVUMeters = (m_bVUMeters != 0); + SendViewMessage(VIEWMSG_SETVUMETERS, m_bVUMeters); + SwitchToView(); +} + +//rewbs.patPlugName +void CCtrlPatterns::OnPatternViewPlugNames() +{ + UINT nState = m_ToolBar.GetState(ID_VIEWPLUGNAMES); + m_bPluginNames = ((nState & TBSTATE_CHECKED) != 0); + TrackerSettings::Instance().gbPatternPluginNames = (m_bPluginNames != 0); + SendViewMessage(VIEWMSG_SETPLUGINNAMES, m_bPluginNames); + SwitchToView(); +} +//end rewbs.patPlugName + +void CCtrlPatterns::OnPatternProperties() +{ + SendViewMessage(VIEWMSG_PATTERNPROPERTIES, PATTERNINDEX_INVALID); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternExpand() +{ + SendViewMessage(VIEWMSG_EXPANDPATTERN); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternCopy() +{ + SendViewMessage(VIEWMSG_COPYPATTERN); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternPaste() +{ + SendViewMessage(VIEWMSG_PASTEPATTERN); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternShrink() +{ + SendViewMessage(VIEWMSG_SHRINKPATTERN); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternAmplify() +{ + SendViewMessage(VIEWMSG_AMPLIFYPATTERN); + SwitchToView(); +} + + +void CCtrlPatterns::OnPatternPlayRow() +{ + ::SendMessage(m_hWndView, WM_COMMAND, ID_PATTERN_PLAYROW, 0); + SwitchToView(); +} + + +void CCtrlPatterns::OnUpdateRecord(CCmdUI *pCmdUI) +{ + if(pCmdUI) + pCmdUI->SetCheck((m_bRecord) ? TRUE : FALSE); +} + + +void CCtrlPatterns::OnFollowSong() +{ + SendViewMessage(VIEWMSG_FOLLOWSONG, IsDlgButtonChecked(IDC_PATTERN_FOLLOWSONG)); + SwitchToView(); +} + + +void CCtrlPatterns::OnChangeLoopStatus() +{ + OnModCtrlMsg(CTRLMSG_PAT_LOOP, IsDlgButtonChecked(IDC_PATTERN_LOOP)); + SwitchToView(); +} + + +void CCtrlPatterns::OnEditUndo() +{ + if(m_hWndView) + ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_UNDO, 0); + SwitchToView(); +} + + +void CCtrlPatterns::OnSwitchToView() +{ + PostViewMessage(VIEWMSG_SETFOCUS); +} + + +void CCtrlPatterns::OnPatternNameChanged() +{ + if(!IsLocked()) + { + const PATTERNINDEX nPat = (PATTERNINDEX)SendViewMessage(VIEWMSG_GETCURRENTPATTERN); + + CString tmp; + m_EditPatName.GetWindowText(tmp); + const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp); + + if(m_sndFile.Patterns[nPat].GetName() != s) + { + if(m_sndFile.Patterns[nPat].SetName(s)) + { + if(m_sndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(NULL, PatternHint(nPat).Names(), this); + } + } + } +} + + +void CCtrlPatterns::OnSequenceNameChanged() +{ + CString tmp; + GetDlgItemText(IDC_EDIT_SEQUENCE_NAME, tmp); + const mpt::ustring str = mpt::ToUnicode(tmp); + auto &order = Order(); + if(str != order.GetName()) + { + order.SetName(str); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint(m_sndFile.Order.GetCurrentSequenceIndex()).Names(), this); + } +} + + +void CCtrlPatterns::OnChordEditor() +{ + CChordEditor dlg(this); + dlg.DoModal(); + SwitchToView(); +} + + +void CCtrlPatterns::OnDetailLo() +{ + m_ToolBar.SetState(ID_PATTERNDETAIL_LO, TBSTATE_CHECKED | TBSTATE_ENABLED); + if(m_nDetailLevel != PatternCursor::instrColumn) + { + m_nDetailLevel = PatternCursor::instrColumn; + m_ToolBar.SetState(ID_PATTERNDETAIL_MED, TBSTATE_ENABLED); + m_ToolBar.SetState(ID_PATTERNDETAIL_HI, TBSTATE_ENABLED); + SendViewMessage(VIEWMSG_SETDETAIL, m_nDetailLevel); + } + SwitchToView(); +} + + +void CCtrlPatterns::OnDetailMed() +{ + m_ToolBar.SetState(ID_PATTERNDETAIL_MED, TBSTATE_CHECKED | TBSTATE_ENABLED); + if(m_nDetailLevel != PatternCursor::volumeColumn) + { + m_nDetailLevel = PatternCursor::volumeColumn; + m_ToolBar.SetState(ID_PATTERNDETAIL_LO, TBSTATE_ENABLED); + m_ToolBar.SetState(ID_PATTERNDETAIL_HI, TBSTATE_ENABLED); + SendViewMessage(VIEWMSG_SETDETAIL, m_nDetailLevel); + } + SwitchToView(); +} + + +void CCtrlPatterns::OnDetailHi() +{ + m_ToolBar.SetState(ID_PATTERNDETAIL_HI, TBSTATE_CHECKED | TBSTATE_ENABLED); + if(m_nDetailLevel != PatternCursor::lastColumn) + { + m_nDetailLevel = PatternCursor::lastColumn; + m_ToolBar.SetState(ID_PATTERNDETAIL_LO, TBSTATE_ENABLED); + m_ToolBar.SetState(ID_PATTERNDETAIL_MED, TBSTATE_ENABLED); + SendViewMessage(VIEWMSG_SETDETAIL, m_nDetailLevel); + } + SwitchToView(); +} + +void CCtrlPatterns::OnToggleOverflowPaste() +{ + TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_OVERFLOWPASTE; + UpdateView(UpdateHint().MPTOptions()); + SwitchToView(); +} + + +void CCtrlPatterns::TogglePluginEditor() +{ + if(m_sndFile.GetInstrumentPlugin(m_nInstrument) != nullptr) + { + m_modDoc.TogglePluginEditor(m_sndFile.Instruments[m_nInstrument]->nMixPlug - 1, CMainFrame::GetInputHandler()->ShiftPressed()); + } +} + + +bool CCtrlPatterns::HasValidPlug(INSTRUMENTINDEX instr) const +{ + return m_sndFile.GetInstrumentPlugin(instr) != nullptr; +} + + +BOOL CCtrlPatterns::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + if(nFlags == 0) + { + PostViewMessage(VIEWMSG_DOSCROLL, zDelta); + } + return CModControlDlg::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CCtrlPatterns::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) + OnModCtrlMsg(CTRLMSG_PREVORDER, 0); + else if(nButton == XBUTTON2) + OnModCtrlMsg(CTRLMSG_NEXTORDER, 0); + CModControlDlg::OnXButtonUp(nFlags, nButton, point); +} + + +BOOL CCtrlPatterns::OnToolTip(UINT /*id*/, NMHDR *pNMHDR, LRESULT * /*pResult*/) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR; + UINT_PTR nID = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = ::GetDlgCtrlID((HWND)nID); + if(nID) + { + pTTT->lpszText = MAKEINTRESOURCE(nID); + pTTT->hinst = AfxGetResourceHandle(); + return TRUE; + } + } + + return FALSE; +} + + +BOOL CCtrlPatterns::GetToolTipText(UINT id, LPTSTR str) +{ + CString fmt; + const TCHAR *s = nullptr; + CommandID cmd = kcNull; + switch(id) + { + case IDC_PATTERN_NEW: s = _T("Insert Pattern"); cmd = kcNewPattern; break; + case IDC_PATTERN_PLAY: s = _T("Play Pattern"); cmd = kcPlayPatternFromCursor; break; + case IDC_PATTERN_PLAYFROMSTART: s = _T("Replay Pattern"); cmd = kcPlayPatternFromStart; break; + case IDC_PATTERN_STOP: s = _T("Stop"); cmd = kcPauseSong; break; + case ID_PATTERN_PLAYROW: s = _T("Play Row"); cmd = kcPatternPlayRow; break; + case IDC_PATTERN_RECORD: s = _T("Record"); cmd = kcPatternRecord; break; + case ID_PATTERN_VUMETERS: s = _T("VU-Meters"); break; + case ID_VIEWPLUGNAMES: s = _T("Show Plugins"); break; + case ID_PATTERN_CHANNELMANAGER: s = _T("Channel Manager"); cmd = kcViewChannelManager; break; + case ID_PATTERN_MIDIMACRO: s = _T("Zxx Macro Configuration"); cmd = kcShowMacroConfig; break; + case ID_PATTERN_CHORDEDIT: s = _T("Chord Editor"); cmd = kcChordEditor; break; + case ID_EDIT_UNDO: + fmt = _T("Undo"); + if(m_modDoc.GetPatternUndo().CanUndo()) + fmt += _T(" ") + m_modDoc.GetPatternUndo().GetUndoName(); + cmd = kcEditUndo; + break; + case ID_PATTERN_PROPERTIES: s = _T("Pattern Properties"); cmd = kcShowPatternProperties; break; + case ID_PATTERN_EXPAND: s = _T("Expand Pattern"); break; + case ID_PATTERN_SHRINK: s = _T("Shrink Pattern"); break; + case ID_PATTERNDETAIL_LO: s = _T("Low Pattern Detail Level"); break; + case ID_PATTERNDETAIL_MED: s = _T("Medium Pattern Detail Level"); break; + case ID_PATTERNDETAIL_HI: s = _T("High Pattern Detail Level"); break; + case ID_OVERFLOWPASTE: s = _T("Toggle Overflow Paste"); cmd = kcToggleOverflowPaste; break; + case IDC_PATTERN_LOOP: s = _T("Toggle Loop Pattern"); cmd = kcChangeLoopStatus; break; + case IDC_PATTERN_FOLLOWSONG: s = _T("Toggle Follow Song"); cmd = kcToggleFollowSong; break; + default: + return FALSE; + } + + if(s != nullptr) + fmt = s; + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if(!keyText.IsEmpty()) + fmt += MPT_CFORMAT(" ({})")(keyText); + } + _tcscpy(str, fmt.GetString()); + return TRUE; +} + + +void CCtrlPatterns::OnSequenceNumChanged() +{ + if((m_EditSequence.m_hWnd) && (m_EditSequence.GetWindowTextLength() > 0)) + { + SEQUENCEINDEX newSeq = static_cast<SEQUENCEINDEX>(GetDlgItemInt(IDC_EDIT_SEQNUM) - 1); + + if(newSeq == m_sndFile.Order.GetCurrentSequenceIndex()) + return; + + if(newSeq >= m_sndFile.Order.GetNumSequences()) + { + newSeq = m_sndFile.Order.GetNumSequences() - 1; + SetDlgItemInt(IDC_EDIT_SEQNUM, newSeq + 1, FALSE); + } + m_OrderList.SelectSequence(newSeq); + UpdateView(SequenceHint(newSeq).Names(), nullptr); + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.h b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.h new file mode 100644 index 00000000..0349728d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_pat.h @@ -0,0 +1,289 @@ +/* + * Ctrl_pat.h + * ---------- + * Purpose: Pattern tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Globals.h" +#include "PatternCursor.h" + +OPENMPT_NAMESPACE_BEGIN + +class COrderList; +class CCtrlPatterns; + +struct OrdSelection +{ + ORDERINDEX firstOrd = 0, lastOrd = 0; + ORDERINDEX GetSelCount() const { return lastOrd - firstOrd + 1; } +}; + +class COrderList: public CWnd +{ + friend class CCtrlPatterns; +protected: + HFONT m_hFont = nullptr; + int m_cxFont = 0, m_cyFont = 0; + //m_nXScroll : The order at the beginning of shown orderlist + //m_nScrollPos: The same as order + //m_nScrollPos2nd: 2nd selection point if multiple orders are selected + // (not neccessarily the higher order - GetCurSel() is taking care of that.) + ORDERINDEX m_nXScroll = 0, m_nScrollPos = 0, m_nScrollPos2nd = ORDERINDEX_INVALID, m_nDropPos, m_nMouseDownPos, m_playPos = ORDERINDEX_INVALID; + ORDERINDEX m_nDragOrder; + //To tell how many orders('orderboxes') to show at least + //on both sides of current order(when updating orderslist position). + int m_nOrderlistMargins; + CModDoc &m_modDoc; + CCtrlPatterns &m_pParent; + bool m_bScrolling = false, m_bDragging = false; + +public: + COrderList(CCtrlPatterns &parent, CModDoc &document); + +public: + BOOL Init(const CRect&, HFONT hFont); + void UpdateView(UpdateHint hint, CObject *pObj = nullptr); + void InvalidateSelection(); + PATTERNINDEX GetCurrentPattern() const; + // make the current selection the secondary selection (used for keyboard orderlist navigation) + inline void SetCurSelTo2ndSel(bool isSelectionKeyPressed) + { + if(isSelectionKeyPressed && m_nScrollPos2nd == ORDERINDEX_INVALID) m_nScrollPos2nd = m_nScrollPos; + else if(!isSelectionKeyPressed && m_nScrollPos2nd != ORDERINDEX_INVALID) m_nScrollPos2nd = ORDERINDEX_INVALID; + }; + void SetSelection(ORDERINDEX firstOrd, ORDERINDEX lastOrd = ORDERINDEX_INVALID); + // Why VC wants to inline this huge function is beyond my understanding... + MPT_NOINLINE bool SetCurSel(ORDERINDEX sel, bool setPlayPos = true, bool shiftClick = false, bool ignoreCurSel = false); + void UpdateScrollInfo(); + void UpdateInfoText(); + int GetFontWidth(); + void QueuePattern(CPoint pt); + + // Check if this module is currently playing + bool IsPlaying() const; + + ORDERINDEX GetOrderFromPoint(const CPoint &pt) const; + CRect GetRectFromOrder(ORDERINDEX ord) const; + + // Get the currently selected pattern(s). + // Set ignoreSelection to true if only the first selected point is important. + OrdSelection GetCurSel(bool ignoreSelection = false) const; + + // Sets target margin value and returns the effective margin value. + ORDERINDEX SetMargins(int); + + // Returns the effective margin value. + ORDERINDEX GetMargins() { return GetMargins(GetMarginsMax()); } + + // Returns the effective margin value. + ORDERINDEX GetMargins(const ORDERINDEX maxMargins) const { return std::min(maxMargins, static_cast<ORDERINDEX>(m_nOrderlistMargins)); } + + // Returns maximum margin value given current window width. + ORDERINDEX GetMarginsMax() { return GetMarginsMax(GetLength()); } + + // Returns maximum margin value when shown sequence has nLength orders. + // For example: If length is 4 orders -> maxMargins = 4/2 - 1 = 1; + // if maximum is 5 -> maxMargins = (int)5/2 = 2 + ORDERINDEX GetMarginsMax(const ORDERINDEX length) const { return (length > 0 && length % 2 == 0) ? length / 2 - 1 : length / 2; } + + // Returns the number of sequence items visible in the list. + ORDERINDEX GetLength(); + + // Return true if given order is in margins given that first shown order + // is 'startOrder'. Begin part of the whole sequence + // is not interpreted to be in margins regardless of the margin value. + bool IsOrderInMargins(int order, int startOrder); + + // Ensure that a given order index is visible in the orderlist view. + void EnsureVisible(ORDERINDEX order); + + // Set given sqeuence and update orderlist display. + void SelectSequence(const SEQUENCEINDEX nSeq); + + // Helper function for entering pattern number + void EnterPatternNum(int enterNum); + + void OnCopy(bool onlyOrders); + + // Update play state and order lock ranges after inserting order items. + void InsertUpdatePlaystate(ORDERINDEX first, ORDERINDEX last); + // Update play state and order lock ranges after deleting order items. + void DeleteUpdatePlaystate(ORDERINDEX first, ORDERINDEX last); + + //{{AFX_VIRTUAL(COrderList) + BOOL PreTranslateMessage(MSG *pMsg) override; + INT_PTR OnToolHitTest(CPoint point, TOOLINFO* pTI) const override; + HRESULT get_accName(VARIANT varChild, BSTR *pszName) override; + //}}AFX_VIRTUAL + +protected: + ModSequence& Order(); + const ModSequence& Order() const; + + void SetScrollPos(int pos); + int GetScrollPos(bool getTrackPos = false); + + // Resizes the order list if the specified order is past the order list length + bool EnsureEditable(ORDERINDEX ord); + + //{{AFX_MSG(COrderList) + afx_msg void OnPaint(); + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + afx_msg void OnSetFocus(CWnd *); + afx_msg void OnKillFocus(CWnd *); + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnLButtonDblClk(UINT, CPoint); + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnLButtonUp(UINT, CPoint); + afx_msg void OnMButtonDown(UINT, CPoint); + afx_msg void OnMouseMove(UINT, CPoint); + afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar*); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnSwitchToView(); + afx_msg void OnInsertOrder(); + afx_msg void OnInsertSeparatorPattern(); + afx_msg void OnDeleteOrder(); + afx_msg void OnRenderOrder(); + afx_msg void OnPatternProperties(); + afx_msg void OnPlayerPlay(); + afx_msg void OnPlayerPause(); + afx_msg void OnPlayerPlayFromStart(); + afx_msg void OnPatternPlayFromStart(); + afx_msg void OnCreateNewPattern(); + afx_msg void OnDuplicatePattern(); + afx_msg void OnMergePatterns(); + afx_msg void OnPatternCopy(); + afx_msg void OnPatternPaste(); + afx_msg void OnSetRestartPos(); + afx_msg void OnEditCopy() { OnCopy(false); } + afx_msg void OnEditCopyOrders() { OnCopy(true); } + afx_msg void OnEditCut(); + afx_msg LRESULT OnDragonDropping(WPARAM bDoDrop, LPARAM lParam); + afx_msg LRESULT OnHelpHitTest(WPARAM, LPARAM lParam); + afx_msg void OnSelectSequence(UINT nid); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg void OnLockPlayback(); + afx_msg void OnUnlockPlayback(); + afx_msg BOOL OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +// CPatEdit: Edit control that switches back to the pattern view if Tab key is pressed. + +class CPatEdit: public CEdit +{ +protected: + CCtrlPatterns *m_pParent = nullptr; + +public: + CPatEdit() = default; + void SetParent(CCtrlPatterns *parent) { m_pParent = parent; } + BOOL PreTranslateMessage(MSG *pMsg) override; +}; + + +class CCtrlPatterns: public CModControlDlg +{ + friend class COrderList; +protected: + COrderList m_OrderList; + CButton m_BtnPrev, m_BtnNext; + CComboBox m_CbnInstrument; + CPatEdit m_EditSpacing, m_EditPatName, m_EditSequence; + CSpinButtonCtrl m_SpinInstrument, m_SpinSpacing, m_SpinSequence; + CModControlBar m_ToolBar; + INSTRUMENTINDEX m_nInstrument = 0; + PatternCursor::Columns m_nDetailLevel = PatternCursor::lastColumn; // Visible Columns + bool m_bRecord = false, m_bVUMeters = false, m_bPluginNames = false; + +public: + CCtrlPatterns(CModControlView &parent, CModDoc &document); + Setting<LONG> &GetSplitPosRef() override { return TrackerSettings::Instance().glPatternWindowHeight; } + +public: + const ModSequence &Order() const; + ModSequence &Order(); + + void SetCurrentPattern(PATTERNINDEX nPat); + BOOL SetCurrentInstrument(UINT nIns); + BOOL GetFollowSong() { return IsDlgButtonChecked(IDC_PATTERN_FOLLOWSONG); } + BOOL GetLoopPattern() {return IsDlgButtonChecked(IDC_PATTERN_LOOP);} + COrderList &GetOrderList() { return m_OrderList; } + //{{AFX_VIRTUAL(CCtrlPatterns) + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + void RecalcLayout() override; + void UpdateView(UpdateHint hint = UpdateHint(), CObject *pObj = nullptr) override; + CRuntimeClass *GetAssociatedViewClass() override; + LRESULT OnModCtrlMsg(WPARAM wParam, LPARAM lParam) override; + void OnActivatePage(LPARAM) override; + void OnDeactivatePage() override; + BOOL GetToolTipText(UINT, LPTSTR) override; + //}}AFX_VIRTUAL +protected: + //{{AFX_MSG(CCtrlPatterns) + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnSequenceNext(); + afx_msg void OnSequencePrev(); + afx_msg void OnChannelManager(); + afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); + afx_msg void OnPlayerPause(); + afx_msg void OnPatternNew(); + afx_msg void OnPatternDuplicate(); + afx_msg void OnPatternMerge(); + afx_msg void OnPatternStop(); + afx_msg void OnPatternPlay(); + afx_msg void OnPatternPlayNoLoop(); + afx_msg void OnPatternPlayRow(); + afx_msg void OnPatternPlayFromStart(); + afx_msg void OnPatternRecord(); + afx_msg void OnPatternVUMeters(); + afx_msg void OnPatternViewPlugNames(); + afx_msg void OnPatternProperties(); + afx_msg void OnPatternExpand(); + afx_msg void OnPatternShrink(); + afx_msg void OnPatternAmplify(); + afx_msg void OnPatternCopy(); + afx_msg void OnPatternPaste(); + afx_msg void OnFollowSong(); + afx_msg void OnChangeLoopStatus(); + afx_msg void OnSwitchToView(); + afx_msg void OnInstrumentChanged(); + afx_msg void OnPrevInstrument(); + afx_msg void OnNextInstrument(); + afx_msg void OnSpacingChanged(); + afx_msg void OnPatternNameChanged(); + afx_msg void OnSequenceNameChanged(); + afx_msg void OnChordEditor(); + afx_msg void OnDetailLo(); + afx_msg void OnDetailMed(); + afx_msg void OnDetailHi(); + afx_msg void OnEditUndo(); + afx_msg void OnUpdateRecord(CCmdUI *pCmdUI); + afx_msg void TogglePluginEditor(); + afx_msg void OnToggleOverflowPaste(); + afx_msg void OnSequenceNumChanged(); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +private: + bool HasValidPlug(INSTRUMENTINDEX instr) const; +public: + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + afx_msg BOOL OnToolTip(UINT id, NMHDR *pTTTStruct, LRESULT *pResult); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_seq.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_seq.cpp new file mode 100644 index 00000000..7f27eae8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_seq.cpp @@ -0,0 +1,1675 @@ +/* + * Ctrl_seq.cpp + * ------------ + * Purpose: Order list for the pattern editor upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "Moddoc.h" +#include "../soundlib/mod_specifications.h" +#include "Globals.h" +#include "Ctrl_pat.h" +#include "PatternClipboard.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + +enum SequenceAction : SEQUENCEINDEX +{ + kAddSequence = MAX_SEQUENCES, + kDuplicateSequence, + kDeleteSequence, + kSplitSequence, + + kMaxSequenceActions +}; + +// Little helper function to avoid copypasta +static bool IsSelectionKeyPressed() { return CMainFrame::GetInputHandler()->SelectionPressed(); } +static bool IsCtrlKeyPressed() { return CMainFrame::GetInputHandler()->CtrlPressed(); } + + +////////////////////////////////////////////////////////////// +// CPatEdit + +BOOL CPatEdit::PreTranslateMessage(MSG *pMsg) +{ + if(((pMsg->message == WM_KEYDOWN) || (pMsg->message == WM_KEYUP)) && (pMsg->wParam == VK_TAB)) + { + if((pMsg->message == WM_KEYUP) && (m_pParent)) + { + m_pParent->SwitchToView(); + } + return TRUE; + } + return CEdit::PreTranslateMessage(pMsg); +} + + +////////////////////////////////////////////////////////////// +// COrderList + +BEGIN_MESSAGE_MAP(COrderList, CWnd) + //{{AFX_MSG_MAP(COrderList) + ON_WM_PAINT() + ON_WM_ERASEBKGND() + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONDBLCLK() + ON_WM_LBUTTONUP() + ON_WM_RBUTTONDOWN() + ON_WM_MBUTTONDOWN() + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_WM_HSCROLL() + ON_WM_SIZE() + + ON_COMMAND(ID_ORDERLIST_INSERT, &COrderList::OnInsertOrder) + ON_COMMAND(ID_ORDERLIST_INSERT_SEPARATOR, &COrderList::OnInsertSeparatorPattern) + ON_COMMAND(ID_ORDERLIST_DELETE, &COrderList::OnDeleteOrder) + ON_COMMAND(ID_ORDERLIST_RENDER, &COrderList::OnRenderOrder) + ON_COMMAND(ID_ORDERLIST_EDIT_COPY, &COrderList::OnEditCopy) + ON_COMMAND(ID_ORDERLIST_EDIT_CUT, &COrderList::OnEditCut) + ON_COMMAND(ID_ORDERLIST_EDIT_COPY_ORDERS, &COrderList::OnEditCopyOrders) + + ON_COMMAND(ID_PATTERN_PROPERTIES, &COrderList::OnPatternProperties) + ON_COMMAND(ID_PLAYER_PLAY, &COrderList::OnPlayerPlay) + ON_COMMAND(ID_PLAYER_PAUSE, &COrderList::OnPlayerPause) + ON_COMMAND(ID_PLAYER_PLAYFROMSTART, &COrderList::OnPlayerPlayFromStart) + ON_COMMAND(IDC_PATTERN_PLAYFROMSTART, &COrderList::OnPatternPlayFromStart) + ON_COMMAND(ID_ORDERLIST_NEW, &COrderList::OnCreateNewPattern) + ON_COMMAND(ID_ORDERLIST_COPY, &COrderList::OnDuplicatePattern) + ON_COMMAND(ID_ORDERLIST_MERGE, &COrderList::OnMergePatterns) + ON_COMMAND(ID_PATTERNCOPY, &COrderList::OnPatternCopy) + ON_COMMAND(ID_PATTERNPASTE, &COrderList::OnPatternPaste) + ON_COMMAND(ID_SETRESTARTPOS, &COrderList::OnSetRestartPos) + ON_COMMAND(ID_ORDERLIST_LOCKPLAYBACK, &COrderList::OnLockPlayback) + ON_COMMAND(ID_ORDERLIST_UNLOCKPLAYBACK, &COrderList::OnUnlockPlayback) + ON_COMMAND_RANGE(ID_SEQUENCE_ITEM, ID_SEQUENCE_ITEM + kMaxSequenceActions - 1, &COrderList::OnSelectSequence) + ON_MESSAGE(WM_MOD_DRAGONDROPPING, &COrderList::OnDragonDropping) + ON_MESSAGE(WM_HELPHITTEST, &COrderList::OnHelpHitTest) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &COrderList::OnCustomKeyMsg) + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &COrderList::OnToolTipText) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +COrderList::COrderList(CCtrlPatterns &parent, CModDoc &document) + : m_nOrderlistMargins(TrackerSettings::Instance().orderlistMargins) + , m_modDoc(document) + , m_pParent(parent) +{ + EnableActiveAccessibility(); +} + + +bool COrderList::EnsureEditable(ORDERINDEX ord) +{ + auto &sndFile = m_modDoc.GetSoundFile(); + if(ord >= Order().size()) + { + if(ord < sndFile.GetModSpecifications().ordersMax) + { + try + { + Order().resize(ord + 1); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + } else + { + return false; + } + } + return true; +} + + +ModSequence &COrderList::Order() { return m_modDoc.GetSoundFile().Order(); } +const ModSequence &COrderList::Order() const { return m_modDoc.GetSoundFile().Order(); } + + +void COrderList::SetScrollPos(int pos) +{ + // Work around 16-bit limitations of WM_HSCROLL + SCROLLINFO si; + MemsetZero(si); + si.cbSize = sizeof(si); + si.fMask = SIF_TRACKPOS; + GetScrollInfo(SB_HORZ, &si); + si.nPos = pos; + SetScrollInfo(SB_HORZ, &si); +} + + +int COrderList::GetScrollPos(bool getTrackPos) +{ + // Work around 16-bit limitations of WM_HSCROLL + SCROLLINFO si; + MemsetZero(si); + si.cbSize = sizeof(si); + si.fMask = SIF_TRACKPOS; + GetScrollInfo(SB_HORZ, &si); + return getTrackPos ? si.nTrackPos : si.nPos; +} + + +bool COrderList::IsOrderInMargins(int order, int startOrder) +{ + const ORDERINDEX nMargins = GetMargins(); + return ((startOrder != 0 && order - startOrder < nMargins) || + order - startOrder >= GetLength() - nMargins); +} + + +void COrderList::EnsureVisible(ORDERINDEX order) +{ + // nothing needs to be done + if(!IsOrderInMargins(order, m_nXScroll) || order == ORDERINDEX_INVALID) + return; + + if(order < m_nXScroll) + { + if(order < GetMargins()) + m_nXScroll = 0; + else + m_nXScroll = order - GetMargins(); + } else + { + m_nXScroll = order + 2 * GetMargins() - 1; + if(m_nXScroll < GetLength()) + m_nXScroll = 0; + else + m_nXScroll -= GetLength(); + } +} + + +bool COrderList::IsPlaying() const +{ + return (CMainFrame::GetMainFrame()->GetModPlaying() == &m_modDoc); +} + + +ORDERINDEX COrderList::GetOrderFromPoint(const CPoint &pt) const +{ + if(m_cxFont) + return mpt::saturate_cast<ORDERINDEX>(m_nXScroll + pt.x / m_cxFont); + return 0; +} + + +CRect COrderList::GetRectFromOrder(ORDERINDEX ord) const +{ + return CRect{CPoint{(ord - m_nXScroll) * m_cxFont, 0}, CSize{m_cxFont, m_cyFont}}; +} + + +BOOL COrderList::Init(const CRect &rect, HFONT hFont) +{ + CreateEx(WS_EX_STATICEDGE, NULL, _T(""), WS_CHILD | WS_VISIBLE, rect, &m_pParent, IDC_ORDERLIST); + m_hFont = hFont; + SendMessage(WM_SETFONT, (WPARAM)m_hFont); + SetScrollPos(0); + EnableScrollBarCtrl(SB_HORZ, TRUE); + SetCurSel(0); + EnableToolTips(); + return TRUE; +} + + +void COrderList::UpdateScrollInfo() +{ + CRect rcClient; + + GetClientRect(&rcClient); + if((m_cxFont > 0) && (rcClient.right > 0)) + { + CRect rect; + SCROLLINFO info; + UINT nPage; + + int nMax = Order().GetLengthTailTrimmed(); + + GetScrollInfo(SB_HORZ, &info, SIF_PAGE | SIF_RANGE); + info.fMask = SIF_PAGE | SIF_RANGE; + info.nMin = 0; + nPage = rcClient.right / m_cxFont; + if(nMax <= (int)nPage) + nMax = nPage + 1; + if((nMax != info.nMax) || (nPage != info.nPage)) + { + info.nPage = nPage; + info.nMax = nMax; + SetScrollInfo(SB_HORZ, &info, TRUE); + } + } +} + + +int COrderList::GetFontWidth() +{ + if((m_cxFont <= 0) && (m_hWnd) && (m_hFont)) + { + CClientDC dc(this); + HGDIOBJ oldfont = dc.SelectObject(m_hFont); + CSize sz = dc.GetTextExtent(_T("000+"), 4); + if(oldfont) + dc.SelectObject(oldfont); + return sz.cx; + } + return m_cxFont; +} + + +void COrderList::InvalidateSelection() +{ + ORDERINDEX ordLo = m_nScrollPos, count = 1; + static ORDERINDEX m_nScrollPos2Old = m_nScrollPos2nd; + if(m_nScrollPos2Old != ORDERINDEX_INVALID) + { + // there were multiple orders selected - remove them all + ORDERINDEX ordHi = m_nScrollPos; + if(m_nScrollPos2Old < m_nScrollPos) + ordLo = m_nScrollPos2Old; + else + ordHi = m_nScrollPos2Old; + count = ordHi - ordLo + 1; + } + m_nScrollPos2Old = m_nScrollPos2nd; + CRect rcClient, rect; + GetClientRect(&rcClient); + rect.left = rcClient.left + (ordLo - m_nXScroll) * m_cxFont; + rect.top = rcClient.top; + rect.right = rect.left + m_cxFont * count; + rect.bottom = rcClient.bottom; + rect &= rcClient; + if(rect.right > rect.left) + InvalidateRect(rect, FALSE); + if(m_playPos != ORDERINDEX_INVALID) + { + rect.left = rcClient.left + (m_playPos - m_nXScroll) * m_cxFont; + rect.top = rcClient.top; + rect.right = rect.left + m_cxFont; + rect &= rcClient; + if(rect.right > rect.left) + InvalidateRect(rect, FALSE); + m_playPos = ORDERINDEX_INVALID; + } +} + + +ORDERINDEX COrderList::GetLength() +{ + CRect rcClient; + GetClientRect(&rcClient); + if(m_cxFont > 0) + return mpt::saturate_cast<ORDERINDEX>(rcClient.right / m_cxFont); + else + { + const int fontWidth = GetFontWidth(); + return (fontWidth > 0) ? mpt::saturate_cast<ORDERINDEX>(rcClient.right / fontWidth) : 0; + } +} + + +OrdSelection COrderList::GetCurSel(bool ignoreSelection) const +{ + // returns the currently selected order(s) + OrdSelection result; + result.firstOrd = result.lastOrd = m_nScrollPos; + // ignoreSelection: true if only first selection marker is important. + if(!ignoreSelection && m_nScrollPos2nd != ORDERINDEX_INVALID) + { + if(m_nScrollPos2nd < m_nScrollPos) // ord2 < ord1 + result.firstOrd = m_nScrollPos2nd; + else + result.lastOrd = m_nScrollPos2nd; + } + ORDERINDEX lastIndex = std::max(Order().GetLengthTailTrimmed(), m_modDoc.GetSoundFile().GetModSpecifications().ordersMax) - 1u; + LimitMax(result.firstOrd, lastIndex); + LimitMax(result.lastOrd, lastIndex); + return result; +} + + +void COrderList::SetSelection(ORDERINDEX firstOrd, ORDERINDEX lastOrd) +{ + SetCurSel(firstOrd, true, false, true); + SetCurSel(lastOrd != ORDERINDEX_INVALID ? lastOrd : firstOrd, false, true, true); +} + + +bool COrderList::SetCurSel(ORDERINDEX sel, bool setPlayPos, bool shiftClick, bool ignoreCurSel) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + ORDERINDEX &ord = shiftClick ? m_nScrollPos2nd : m_nScrollPos; + const ORDERINDEX lastIndex = std::max(Order().GetLength(), sndFile.GetModSpecifications().ordersMax) - 1u; + + if((sel < 0) || (sel > lastIndex) || (!m_pParent) || (!pMainFrm)) + return false; + if(!ignoreCurSel && sel == ord && (sel == sndFile.m_PlayState.m_nCurrentOrder)) + return true; + const ORDERINDEX shownLength = GetLength(); + InvalidateSelection(); + ord = sel; + if(!EnsureEditable(ord)) + return false; + + if(!m_bScrolling) + { + const ORDERINDEX margins = GetMargins(GetMarginsMax(shownLength)); + if(ord < m_nXScroll + margins) + { + // Must move first shown sequence item to left in order to show the new active order. + m_nXScroll = (ord > margins) ? (ord - margins) : 0; + SetScrollPos(m_nXScroll); + Invalidate(FALSE); + } else + { + ORDERINDEX maxsel = shownLength; + if(maxsel) + maxsel--; + if(ord - m_nXScroll >= maxsel - margins) + { + // Must move first shown sequence item to right in order to show the new active order. + m_nXScroll = ord - (maxsel - margins); + SetScrollPos(m_nXScroll); + Invalidate(FALSE); + } + } + } + InvalidateSelection(); + PATTERNINDEX n = Order()[m_nScrollPos]; + if(setPlayPos && !shiftClick && sndFile.Patterns.IsValidPat(n)) + { + const bool isPlaying = IsPlaying(); + bool changedPos = false; + + if(isPlaying && sndFile.m_SongFlags[SONG_PATTERNLOOP]) + { + pMainFrm->ResetNotificationBuffer(); + + // Update channel parameters and play time + CriticalSection cs; + m_modDoc.SetElapsedTime(m_nScrollPos, 0, !sndFile.m_SongFlags[SONG_PAUSED | SONG_STEP]); + + changedPos = true; + } else if(m_pParent.GetFollowSong()) + { + FlagSet<SongFlags> pausedFlags = sndFile.m_SongFlags & (SONG_PAUSED | SONG_STEP | SONG_PATTERNLOOP); + // Update channel parameters and play time + CriticalSection cs; + sndFile.SetCurrentOrder(m_nScrollPos); + m_modDoc.SetElapsedTime(m_nScrollPos, 0, !sndFile.m_SongFlags[SONG_PAUSED | SONG_STEP]); + sndFile.m_SongFlags.set(pausedFlags); + + if(isPlaying) + pMainFrm->ResetNotificationBuffer(); + changedPos = true; + } + + if(changedPos && Order().IsPositionLocked(m_nScrollPos)) + { + // Users wants to go somewhere else, so let them do that. + OnUnlockPlayback(); + } + + m_pParent.SetCurrentPattern(n); + } else if(setPlayPos && !shiftClick && n != Order().GetIgnoreIndex() && n != Order().GetInvalidPatIndex()) + { + m_pParent.SetCurrentPattern(n); + } + UpdateInfoText(); + if(m_nScrollPos == m_nScrollPos2nd) + m_nScrollPos2nd = ORDERINDEX_INVALID; + return true; +} + + +PATTERNINDEX COrderList::GetCurrentPattern() const +{ + const ModSequence &order = Order(); + if(m_nScrollPos < order.size()) + { + return order[m_nScrollPos]; + } + return 0; +} + + +BOOL COrderList::PreTranslateMessage(MSG *pMsg) +{ + //handle Patterns View context keys that we want to take effect in the orderlist. + if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = (UINT)pMsg->wParam; + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + + if(ih->KeyEvent(kCtxCtrlOrderlist, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + + //HACK: masquerade as kCtxViewPatternsNote context until we implement appropriate + // command propagation to kCtxCtrlOrderlist context. + + if(ih->KeyEvent(kCtxViewPatternsNote, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + + // Handle Application (menu) key + if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS) + { + const auto selection = GetCurSel(); + auto pt = (GetRectFromOrder(selection.firstOrd) | GetRectFromOrder(selection.lastOrd)).CenterPoint(); + CRect clientRect; + GetClientRect(clientRect); + if(!clientRect.PtInRect(pt)) + pt = clientRect.CenterPoint(); + OnRButtonDown(0, pt); + } + } + + return CWnd::PreTranslateMessage(pMsg); +} + + +LRESULT COrderList::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) +{ + bool isPlaying = IsPlaying(); + switch(wParam) + { + case kcEditCopy: + OnEditCopy(); return wParam; + case kcEditCut: + OnEditCut(); return wParam; + case kcEditPaste: + OnPatternPaste(); return wParam; + case kcOrderlistEditCopyOrders: + OnEditCopyOrders(); return wParam; + + // Orderlist navigation + case kcOrderlistNavigateLeftSelect: + case kcOrderlistNavigateLeft: + SetCurSelTo2ndSel(wParam == kcOrderlistNavigateLeftSelect); SetCurSel(m_nScrollPos - 1, wParam == kcOrderlistNavigateLeft || !isPlaying); return wParam; + case kcOrderlistNavigateRightSelect: + case kcOrderlistNavigateRight: + SetCurSelTo2ndSel(wParam == kcOrderlistNavigateRightSelect); SetCurSel(m_nScrollPos + 1, wParam == kcOrderlistNavigateRight || !isPlaying); return wParam; + case kcOrderlistNavigateFirstSelect: + case kcOrderlistNavigateFirst: + SetCurSelTo2ndSel(wParam == kcOrderlistNavigateFirstSelect); SetCurSel(0, wParam == kcOrderlistNavigateFirst || !isPlaying); return wParam; + case kcEditSelectAll: + SetCurSel(0, !isPlaying); + [[fallthrough]]; + case kcOrderlistNavigateLastSelect: + case kcOrderlistNavigateLast: + { + SetCurSelTo2ndSel(wParam == kcOrderlistNavigateLastSelect || wParam == kcEditSelectAll); + ORDERINDEX nLast = Order().GetLengthTailTrimmed(); + if(nLast > 0) nLast--; + SetCurSel(nLast, wParam == kcOrderlistNavigateLast || !isPlaying); + } + return wParam; + + // Orderlist edit + case kcOrderlistEditDelete: + OnDeleteOrder(); return wParam; + case kcOrderlistEditInsert: + OnInsertOrder(); return wParam; + case kcOrderlistEditInsertSeparator: + OnInsertSeparatorPattern(); return wParam; + case kcOrderlistSwitchToPatternView: + OnSwitchToView(); return wParam; + case kcOrderlistEditPattern: + OnLButtonDblClk(0, CPoint(0, 0)); OnSwitchToView(); return wParam; + + // Enter pattern number + case kcOrderlistPat0: + case kcOrderlistPat1: + case kcOrderlistPat2: + case kcOrderlistPat3: + case kcOrderlistPat4: + case kcOrderlistPat5: + case kcOrderlistPat6: + case kcOrderlistPat7: + case kcOrderlistPat8: + case kcOrderlistPat9: + EnterPatternNum(static_cast<UINT>(wParam) - kcOrderlistPat0); return wParam; + case kcOrderlistPatMinus: + EnterPatternNum(10); return wParam; + case kcOrderlistPatPlus: + EnterPatternNum(11); return wParam; + case kcOrderlistPatIgnore: + EnterPatternNum(12); return wParam; + case kcOrderlistPatInvalid: + EnterPatternNum(13); return wParam; + + // kCtxViewPatternsNote messages + case kcSwitchToOrderList: + OnSwitchToView(); + return wParam; + case kcChangeLoopStatus: + m_pParent.OnModCtrlMsg(CTRLMSG_PAT_LOOP, -1); return wParam; + case kcToggleFollowSong: + m_pParent.OnModCtrlMsg(CTRLMSG_PAT_FOLLOWSONG, 1); return wParam; + + case kcChannelUnmuteAll: + case kcUnmuteAllChnOnPatTransition: + return m_pParent.SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); + + case kcOrderlistLockPlayback: + OnLockPlayback(); return wParam; + case kcOrderlistUnlockPlayback: + OnUnlockPlayback(); return wParam; + + case kcDuplicatePattern: + OnDuplicatePattern(); return wParam; + case kcMergePatterns: + OnMergePatterns(); return wParam; + case kcNewPattern: + OnCreateNewPattern(); return wParam; + } + + return kcNull; +} + + +// Helper function to enter pattern index into the orderlist. +// Call with param 0...9 (enter digit), 10 (decrease) or 11 (increase). +void COrderList::EnterPatternNum(int enterNum) +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + + if(!EnsureEditable(m_nScrollPos)) + return; + + PATTERNINDEX curIndex = Order()[m_nScrollPos]; + const PATTERNINDEX maxIndex = std::max(PATTERNINDEX(1), sndFile.Patterns.GetNumPatterns()) - 1; + const PATTERNINDEX firstInvalid = sndFile.GetModSpecifications().hasIgnoreIndex ? sndFile.Order.GetIgnoreIndex() : sndFile.Order.GetInvalidPatIndex(); + + if(enterNum >= 0 && enterNum <= 9) // enter 0...9 + { + if(curIndex >= sndFile.Patterns.Size()) + curIndex = 0; + + curIndex = curIndex * 10 + static_cast<PATTERNINDEX>(enterNum); + static_assert(MAX_PATTERNS < 10000); + if((curIndex >= 1000) && (curIndex > maxIndex)) curIndex %= 1000; + if((curIndex >= 100) && (curIndex > maxIndex)) curIndex %= 100; + if((curIndex >= 10) && (curIndex > maxIndex)) curIndex %= 10; + } else if(enterNum == 10) // decrease pattern index + { + if(curIndex == 0) + { + curIndex = sndFile.Order.GetInvalidPatIndex(); + } else if(curIndex > maxIndex && curIndex <= firstInvalid) + { + curIndex = maxIndex; + } else + { + do + { + curIndex--; + } while(curIndex > 0 && curIndex < firstInvalid && !sndFile.Patterns.IsValidPat(curIndex)); + } + } else if(enterNum == 11) // increase pattern index + { + if(curIndex >= sndFile.Order.GetInvalidPatIndex()) + { + curIndex = 0; + } else if(curIndex >= maxIndex && curIndex < firstInvalid) + { + curIndex = firstInvalid; + } else + { + do + { + curIndex++; + } while(curIndex <= maxIndex && !sndFile.Patterns.IsValidPat(curIndex)); + } + } else if(enterNum == 12) // ignore index (+++) + { + if(sndFile.GetModSpecifications().hasIgnoreIndex) + { + curIndex = sndFile.Order.GetIgnoreIndex(); + } + } else if(enterNum == 13) // invalid index (---) + { + curIndex = sndFile.Order.GetInvalidPatIndex(); + } + // apply + if(curIndex != Order()[m_nScrollPos]) + { + Order()[m_nScrollPos] = curIndex; + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); + InvalidateSelection(); + m_pParent.SetCurrentPattern(curIndex); + } +} + + +void COrderList::OnEditCut() +{ + OnEditCopy(); + OnDeleteOrder(); +} + + +void COrderList::OnCopy(bool onlyOrders) +{ + const OrdSelection ordsel = GetCurSel(); + BeginWaitCursor(); + PatternClipboard::Copy(m_modDoc.GetSoundFile(), ordsel.firstOrd, ordsel.lastOrd, onlyOrders); + PatternClipboardDialog::UpdateList(); + EndWaitCursor(); +} + + +void COrderList::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj != this && hint.ToType<SequenceHint>().GetType()[HINT_MODTYPE | HINT_MODSEQUENCE]) + { + Invalidate(FALSE); + UpdateInfoText(); + } + if(hint.GetType()[HINT_MPTOPTIONS]) + { + m_nOrderlistMargins = TrackerSettings::Instance().orderlistMargins; + } +} + + +void COrderList::OnSwitchToView() +{ + m_pParent.PostViewMessage(VIEWMSG_SETFOCUS); +} + + +void COrderList::UpdateInfoText() +{ + if(::GetFocus() != m_hWnd) + return; + + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + const auto &order = Order(); + + const ORDERINDEX length = order.GetLengthTailTrimmed(); + CString s; + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_HEXDISPLAY) + s.Format(_T("Position %02Xh of %02Xh"), m_nScrollPos, length); + else + s.Format(_T("Position %u of %u (%02Xh of %02Xh)"), m_nScrollPos, length, m_nScrollPos, length); + + if(order.IsValidPat(m_nScrollPos)) + { + if(const auto patName = sndFile.Patterns[order[m_nScrollPos]].GetName(); !patName.empty()) + s += _T(": ") + mpt::ToCString(sndFile.GetCharsetInternal(), patName); + } + CMainFrame::GetMainFrame()->SetInfoText(s); + + CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this); +} + + +// Accessible description for screen readers +HRESULT COrderList::get_accName(VARIANT, BSTR *pszName) +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + const auto &order = Order(); + + CString s; + const bool singleSel = m_nScrollPos2nd == ORDERINDEX_INVALID || m_nScrollPos2nd == m_nScrollPos; + const auto firstOrd = singleSel ? m_nScrollPos : std::min(m_nScrollPos, m_nScrollPos2nd), lastOrd = singleSel ? m_nScrollPos : std::max(m_nScrollPos, m_nScrollPos2nd); + if(singleSel) + s = MPT_CFORMAT("Order {}, ")(m_nScrollPos); + else + s = MPT_CFORMAT("Order selection {} to {}: ")(firstOrd, lastOrd); + bool first = true; + for(ORDERINDEX o = firstOrd; o <= lastOrd; o++) + { + if(!first) + s += _T(", "); + first = false; + PATTERNINDEX pat = order[o]; + if(pat == ModSequence::GetIgnoreIndex()) + s += _T(" Skip"); + else if(pat == ModSequence::GetInvalidPatIndex()) + s += _T(" Stop"); + else + s += MPT_CFORMAT("Pattern {}")(pat); + if(sndFile.Patterns.IsValidPat(pat)) + { + if(const auto patName = sndFile.Patterns[pat].GetName(); !patName.empty()) + s += _T(" (") + mpt::ToCString(sndFile.GetCharsetInternal(), patName) + _T(")"); + } + } + + *pszName = s.AllocSysString(); + return S_OK; +} + + +///////////////////////////////////////////////////////////////// +// COrderList messages + +void COrderList::OnPaint() +{ + TCHAR s[64]; + CPaintDC dc(this); + HGDIOBJ oldfont = dc.SelectObject(m_hFont); + HGDIOBJ oldpen = dc.SelectStockObject(DC_PEN); + const auto separatorColor = GetSysColor(COLOR_WINDOW) ^ 0x808080; + const auto colorText = GetSysColor(COLOR_WINDOWTEXT), colorInvalid = GetSysColor(COLOR_GRAYTEXT), colorTextSel = GetSysColor(COLOR_HIGHLIGHTTEXT); + const auto windowBrush = GetSysColorBrush(COLOR_WINDOW), highlightBrush = GetSysColorBrush(COLOR_HIGHLIGHT), faceBrush = GetSysColorBrush(COLOR_BTNFACE); + + SetDCPenColor(dc, separatorColor); + + // First time? + if(m_cxFont <= 0 || m_cyFont <= 0) + { + CSize sz = dc.GetTextExtent(_T("000+"), 4); + m_cxFont = sz.cx; + m_cyFont = sz.cy; + } + + if(m_cxFont > 0 && m_cyFont > 0) + { + CRect rcClient; + GetClientRect(&rcClient); + CRect rect = rcClient; + + UpdateScrollInfo(); + dc.SetBkMode(TRANSPARENT); + const OrdSelection selection = GetCurSel(); + + const int lineWidth1 = Util::ScalePixels(1, m_hWnd); + const int lineWidth2 = Util::ScalePixels(2, m_hWnd); + const bool isFocussed = (::GetFocus() == m_hWnd); + + const auto &order = Order(); + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + ORDERINDEX maxEntries = sndFile.GetModSpecifications().ordersMax; + if(order.size() > maxEntries) + { + // Only computed if potentially needed. + maxEntries = std::max(maxEntries, order.GetLengthTailTrimmed()); + } + + // Scrolling the shown orders(the showns rectangles)? + for(size_t pos = m_nXScroll; rect.left < rcClient.right; pos++, rect.left += m_cxFont) + { + const ORDERINDEX ord = mpt::saturate_cast<ORDERINDEX>(pos); + dc.SetTextColor(colorText); + const bool inSelection = (ord >= selection.firstOrd && ord <= selection.lastOrd); + const bool highLight = (isFocussed && inSelection); + if((rect.right = rect.left + m_cxFont) > rcClient.right) + rect.right = rcClient.right; + rect.right--; + + HBRUSH background; + if(highLight) + background = highlightBrush; // Currently selected order item + else if(order.IsPositionLocked(ord)) + background = faceBrush; // "Playback lock" indicator - grey out all order items which aren't played. + else + background = windowBrush; // Normal, unselected item. + ::FillRect(dc, &rect, background); + + // Drawing the shown pattern-indicator or drag position. + if(ord == (m_bDragging ? m_nDropPos : m_nScrollPos)) + { + rect.InflateRect(-1, -1); + dc.DrawFocusRect(&rect); + rect.InflateRect(1, 1); + } + MoveToEx(dc, rect.right, rect.top, NULL); + LineTo(dc, rect.right, rect.bottom); + + // Drawing the 'ctrl-transition' indicator + if(ord == sndFile.m_PlayState.m_nSeqOverride && sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID) + { + dc.FillSolidRect(CRect{rect.left + 4, rect.bottom - 4 - lineWidth1, rect.right - 4, rect.bottom - 4}, separatorColor); + } + + // Drawing 'playing'-indicator. + if(ord == sndFile.GetCurrentOrder() && CMainFrame::GetMainFrame()->IsPlaying()) + { + dc.FillSolidRect(CRect{rect.left + 4, rect.top + 2, rect.right - 4, rect.top + 2 + lineWidth1}, separatorColor); + m_playPos = ord; + } + + // Drawing drop indicator + if(m_bDragging && ord == m_nDropPos && !inSelection) + { + const bool dropLeft = (m_nDropPos < selection.firstOrd) || TrackerSettings::Instance().orderListOldDropBehaviour; + dc.FillSolidRect(CRect{dropLeft ? (rect.left + 2) : (rect.right - 2 - lineWidth2), rect.top + 2, dropLeft ? (rect.left + 2 + lineWidth2) : (rect.right - 2), rect.bottom - 2}, separatorColor); + } + + s[0] = _T('\0'); + const PATTERNINDEX pat = (ord < order.size()) ? order[ord] : PATTERNINDEX_INVALID; + if(ord < maxEntries && (rect.left + m_cxFont - 4) <= rcClient.right) + { + if(pat == order.GetInvalidPatIndex()) + _tcscpy(s, _T("---")); + else if(pat == order.GetIgnoreIndex()) + _tcscpy(s, _T("+++")); + else + wsprintf(s, _T("%u"), pat); + } + + COLORREF textCol; + if(highLight) + textCol = colorTextSel; // Highlighted pattern + else if(sndFile.Patterns.IsValidPat(pat)) + textCol = colorText; // Normal pattern + else + textCol = colorInvalid; // Non-existent pattern + dc.SetTextColor(textCol); + dc.DrawText(s, -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER); + } + } + if(oldpen) + dc.SelectObject(oldpen); + if(oldfont) + dc.SelectObject(oldfont); +} + + +void COrderList::OnSetFocus(CWnd *pWnd) +{ + CWnd::OnSetFocus(pWnd); + InvalidateSelection(); + UpdateInfoText(); + CMainFrame::GetMainFrame()->m_pOrderlistHasFocus = this; +} + + +void COrderList::OnKillFocus(CWnd *pWnd) +{ + CWnd::OnKillFocus(pWnd); + InvalidateSelection(); + CMainFrame::GetMainFrame()->m_pOrderlistHasFocus = nullptr; +} + + +void COrderList::OnLButtonDown(UINT nFlags, CPoint pt) +{ + CRect rect; + GetClientRect(&rect); + if(pt.y < rect.bottom) + { + SetFocus(); + + if(IsCtrlKeyPressed()) + { + // Queue pattern + QueuePattern(pt); + } else + { + // mark pattern (+skip to) + const int oldXScroll = m_nXScroll; + + ORDERINDEX ord = GetOrderFromPoint(pt); + OrdSelection selection = GetCurSel(); + + // check if cursor is in selection - if it is, only react on MouseUp as the user might want to drag those orders + if(m_nScrollPos2nd == ORDERINDEX_INVALID || ord < selection.firstOrd || ord > selection.lastOrd) + { + m_nScrollPos2nd = ORDERINDEX_INVALID; + SetCurSel(ord, true, IsSelectionKeyPressed()); + } + m_bDragging = !IsOrderInMargins(m_nScrollPos, oldXScroll) || !IsOrderInMargins(m_nScrollPos2nd, oldXScroll); + + m_nMouseDownPos = ord; + if(m_bDragging) + { + m_nDragOrder = m_nDropPos = GetCurSel(true).firstOrd; + SetCapture(); + } + } + } else + { + CWnd::OnLButtonDown(nFlags, pt); + } +} + + +void COrderList::OnLButtonUp(UINT nFlags, CPoint pt) +{ + CRect rect; + GetClientRect(&rect); + + // Copy or move orders? + const bool copyOrders = IsSelectionKeyPressed(); + + if(m_bDragging) + { + m_bDragging = false; + ReleaseCapture(); + if(rect.PtInRect(pt)) + { + ORDERINDEX n = GetOrderFromPoint(pt); + const OrdSelection selection = GetCurSel(); + if(n != ORDERINDEX_INVALID && n == m_nDropPos && (n < selection.firstOrd || n > selection.lastOrd)) + { + const bool multiSelection = (selection.firstOrd != selection.lastOrd); + const bool moveBack = m_nDropPos < m_nDragOrder; + ORDERINDEX moveCount = (selection.lastOrd - selection.firstOrd), movePos = selection.firstOrd; + + if(!moveBack && !TrackerSettings::Instance().orderListOldDropBehaviour) + m_nDropPos++; + + bool modified = false; + for(int i = 0; i <= moveCount; i++) + { + if(!m_modDoc.MoveOrder(movePos, m_nDropPos, true, copyOrders)) + break; + modified = true; + if(moveBack != copyOrders && multiSelection) + { + movePos++; + m_nDropPos++; + } + if(moveBack && copyOrders && multiSelection) + { + movePos += 2; + m_nDropPos++; + } + } + + if(multiSelection) + { + // adjust selection + m_nScrollPos2nd = m_nDropPos - 1; + m_nDropPos -= moveCount + (moveBack ? 0 : 1); + SetCurSel((moveBack && !copyOrders) ? m_nDropPos - 1 : m_nDropPos); + } else + { + SetCurSel((m_nDragOrder < m_nDropPos && !copyOrders) ? m_nDropPos - 1 : m_nDropPos); + } + // Did we actually change anything? + if(modified) + m_modDoc.SetModified(); + } else + { + if(pt.y < rect.bottom && n == m_nMouseDownPos && !copyOrders) + { + // Remove selection if we didn't drag anything but multiselect was active + m_nScrollPos2nd = ORDERINDEX_INVALID; + SetFocus(); + SetCurSel(n); + } + } + } + Invalidate(FALSE); + } else + { + CWnd::OnLButtonUp(nFlags, pt); + } +} + + +void COrderList::OnMouseMove(UINT nFlags, CPoint pt) +{ + if((m_bDragging) && (m_cxFont)) + { + CRect rect; + + GetClientRect(&rect); + ORDERINDEX n = ORDERINDEX_INVALID; + if(rect.PtInRect(pt)) + { + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + n = GetOrderFromPoint(pt); + if(n >= Order().size() && n >= sndFile.GetModSpecifications().ordersMax) + n = ORDERINDEX_INVALID; + } + if(n != m_nDropPos) + { + if(n != ORDERINDEX_INVALID) + { + m_nMouseDownPos = ORDERINDEX_INVALID; + m_nDropPos = n; + Invalidate(FALSE); + SetCursor(CMainFrame::curDragging); + } else + { + m_nDropPos = ORDERINDEX_INVALID; + SetCursor(CMainFrame::curNoDrop); + } + } + } else + { + CWnd::OnMouseMove(nFlags, pt); + } +} + + +void COrderList::OnSelectSequence(UINT nid) +{ + SelectSequence(static_cast<SEQUENCEINDEX>(nid - ID_SEQUENCE_ITEM)); +} + + +void COrderList::OnRButtonDown(UINT nFlags, CPoint pt) +{ + CRect rect; + GetClientRect(&rect); + if(m_bDragging) + { + m_nDropPos = ORDERINDEX_INVALID; + OnLButtonUp(nFlags, pt); + } + if(pt.y >= rect.bottom) + return; + + bool multiSelection = (m_nScrollPos2nd != ORDERINDEX_INVALID); + + if(!multiSelection) + SetCurSel(GetOrderFromPoint(pt), false, false, false); + SetFocus(); + HMENU hMenu = ::CreatePopupMenu(); + if(!hMenu) + return; + + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + + // Check if at least one pattern in the current selection exists + bool patExists = false; + OrdSelection selection = GetCurSel(); + LimitMax(selection.lastOrd, Order().GetLastIndex()); + for(ORDERINDEX ord = selection.firstOrd; ord <= selection.lastOrd && !patExists; ord++) + { + patExists = Order().IsValidPat(ord); + } + + const DWORD greyed = patExists ? 0 : MF_GRAYED; + + CInputHandler *ih = CMainFrame::GetInputHandler(); + + if(multiSelection) + { + // Several patterns are selected. + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT, ih->GetKeyTextFromCommand(kcOrderlistEditInsert, _T("&Insert Patterns"))); + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_DELETE, ih->GetKeyTextFromCommand(kcOrderlistEditDelete, _T("&Remove Patterns"))); + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy Patterns"))); + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_COPY_ORDERS, ih->GetKeyTextFromCommand(kcOrderlistEditCopyOrders, _T("&Copy Orders"))); + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("&C&ut Patterns"))); + AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERNPASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("P&aste Patterns"))); + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_COPY, ih->GetKeyTextFromCommand(kcDuplicatePattern, _T("&Duplicate Patterns"))); + AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_MERGE, ih->GetKeyTextFromCommand(kcMergePatterns, _T("&Merge Patterns"))); + } else + { + // Only one pattern is selected + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT, ih->GetKeyTextFromCommand(kcOrderlistEditInsert, _T("&Insert Pattern"))); + if(sndFile.GetModSpecifications().hasIgnoreIndex) + { + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_INSERT_SEPARATOR, ih->GetKeyTextFromCommand(kcOrderlistEditInsertSeparator, _T("&Insert Separator"))); + } + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_DELETE, ih->GetKeyTextFromCommand(kcOrderlistEditDelete, _T("&Remove Pattern"))); + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING, ID_ORDERLIST_NEW, ih->GetKeyTextFromCommand(kcNewPattern, _T("Create &New Pattern"))); + AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_COPY, ih->GetKeyTextFromCommand(kcDuplicatePattern, _T("&Duplicate Pattern"))); + AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERNCOPY, _T("&Copy Pattern")); + AppendMenu(hMenu, MF_STRING, ID_PATTERNPASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("P&aste Pattern"))); + const bool hasPatternProperties = sndFile.GetModSpecifications().patternRowsMin != sndFile.GetModSpecifications().patternRowsMax; + if(hasPatternProperties || sndFile.GetModSpecifications().hasRestartPos) + { + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + if(hasPatternProperties) + AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_PROPERTIES, _T("&Pattern Properties...")); + if(sndFile.GetModSpecifications().hasRestartPos) + AppendMenu(hMenu, MF_STRING | greyed | ((Order().GetRestartPos() == m_nScrollPos) ? MF_CHECKED : 0), ID_SETRESTARTPOS, _T("R&estart Position")); + } + if(sndFile.GetModSpecifications().sequencesMax > 1) + { + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + + HMENU menuSequence = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)menuSequence, _T("&Sequences")); + + const SEQUENCEINDEX numSequences = sndFile.Order.GetNumSequences(); + for(SEQUENCEINDEX i = 0; i < numSequences; i++) + { + CString str; + if(sndFile.Order(i).GetName().empty()) + str = MPT_CFORMAT("Sequence {}")(i + 1); + else + str = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(sndFile.Order(i).GetName())); + const UINT flags = (sndFile.Order.GetCurrentSequenceIndex() == i) ? MF_STRING | MF_CHECKED : MF_STRING; + AppendMenu(menuSequence, flags, ID_SEQUENCE_ITEM + i, str); + } + if(sndFile.Order.GetNumSequences() < sndFile.GetModSpecifications().sequencesMax) + { + AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kDuplicateSequence, _T("&Duplicate current sequence")); + AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kAddSequence, _T("&Create empty sequence")); + } + if(sndFile.Order.GetNumSequences() > 1) + AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kDeleteSequence, _T("D&elete current sequence")); + else + AppendMenu(menuSequence, MF_STRING, ID_SEQUENCE_ITEM + kSplitSequence, _T("&Split sub songs into sequences")); + } + } + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, ((selection.firstOrd == sndFile.m_lockOrderStart && selection.lastOrd == sndFile.m_lockOrderEnd) ? (MF_STRING | MF_CHECKED) : MF_STRING), ID_ORDERLIST_LOCKPLAYBACK, ih->GetKeyTextFromCommand(kcOrderlistLockPlayback, _T("&Lock Playback to Selection"))); + AppendMenu(hMenu, (sndFile.m_lockOrderStart == ORDERINDEX_INVALID ? (MF_STRING | MF_GRAYED) : MF_STRING), ID_ORDERLIST_UNLOCKPLAYBACK, ih->GetKeyTextFromCommand(kcOrderlistUnlockPlayback, _T("&Unlock Playback"))); + + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING | greyed, ID_ORDERLIST_RENDER, _T("Render to &Wave")); + + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); +} + + +void COrderList::OnLButtonDblClk(UINT, CPoint) +{ + auto &sndFile = m_modDoc.GetSoundFile(); + m_nScrollPos2nd = ORDERINDEX_INVALID; + SetFocus(); + if(!EnsureEditable(m_nScrollPos)) + return; + PATTERNINDEX pat = Order()[m_nScrollPos]; + if(sndFile.Patterns.IsValidPat(pat)) + m_pParent.SetCurrentPattern(pat); + else if(pat != sndFile.Order.GetIgnoreIndex()) + OnCreateNewPattern(); +} + + +void COrderList::OnMButtonDown(UINT nFlags, CPoint pt) +{ + MPT_UNREFERENCED_PARAMETER(nFlags); + QueuePattern(pt); +} + + +void COrderList::OnHScroll(UINT nSBCode, UINT /*nPos*/, CScrollBar *) +{ + UINT nNewPos = m_nXScroll; + UINT smin, smax; + + GetScrollRange(SB_HORZ, (LPINT)&smin, (LPINT)&smax); + m_bScrolling = true; + switch(nSBCode) + { + case SB_LINELEFT: if (nNewPos) nNewPos--; break; + case SB_LINERIGHT: if (nNewPos < smax) nNewPos++; break; + case SB_PAGELEFT: if (nNewPos > 4) nNewPos -= 4; else nNewPos = 0; break; + case SB_PAGERIGHT: if (nNewPos + 4 < smax) nNewPos += 4; else nNewPos = smax; break; + case SB_THUMBPOSITION: + case SB_THUMBTRACK: nNewPos = GetScrollPos(true); break; + case SB_LEFT: nNewPos = 0; break; + case SB_RIGHT: nNewPos = smax; break; + case SB_ENDSCROLL: m_bScrolling = false; break; + } + if (nNewPos > smax) nNewPos = smax; + if (nNewPos != m_nXScroll) + { + m_nXScroll = static_cast<ORDERINDEX>(nNewPos); + SetScrollPos(m_nXScroll); + Invalidate(FALSE); + } +} + + +void COrderList::OnSize(UINT nType, int cx, int cy) +{ + int nPos; + int smin, smax; + + CWnd::OnSize(nType, cx, cy); + UpdateScrollInfo(); + GetScrollRange(SB_HORZ, &smin, &smax); + nPos = GetScrollPos(); + if(nPos > smax) + nPos = smax; + if(m_nXScroll != nPos) + { + m_nXScroll = mpt::saturate_cast<ORDERINDEX>(nPos); + SetScrollPos(m_nXScroll); + Invalidate(FALSE); + } +} + + +void COrderList::OnInsertOrder() +{ + // insert the same order(s) after the currently selected order(s) + ModSequence &order = Order(); + + const OrdSelection selection = GetCurSel(); + const ORDERINDEX insertCount = order.insert(selection.lastOrd + 1, selection.lastOrd - selection.firstOrd + 1); + if(!insertCount) + return; + + std::copy(order.begin() + selection.firstOrd, order.begin() + selection.firstOrd + insertCount, order.begin() + selection.lastOrd + 1); + + InsertUpdatePlaystate(selection.firstOrd, selection.lastOrd); + + m_nScrollPos = std::min(ORDERINDEX(selection.lastOrd + 1), order.GetLastIndex()); + if(insertCount > 1) + m_nScrollPos2nd = std::min(ORDERINDEX(m_nScrollPos + insertCount - 1), order.GetLastIndex()); + else + m_nScrollPos2nd = ORDERINDEX_INVALID; + + InvalidateSelection(); + EnsureVisible(m_nScrollPos2nd); + // first inserted order has higher priority than the last one + EnsureVisible(m_nScrollPos); + + Invalidate(FALSE); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); +} + + +void COrderList::OnInsertSeparatorPattern() +{ + // Insert a separator pattern after the current pattern, don't move order list cursor + ModSequence &order = Order(); + + const OrdSelection selection = GetCurSel(true); + ORDERINDEX insertPos = selection.firstOrd; + + if(!EnsureEditable(insertPos)) + return; + if(order[insertPos] != order.GetInvalidPatIndex()) + { + // If we're not inserting at a stop (---) index, we move on by one position. + insertPos++; + order.insert(insertPos, 1, order.GetIgnoreIndex()); + } else + { + order[insertPos] = order.GetIgnoreIndex(); + } + + InsertUpdatePlaystate(insertPos, insertPos); + + Invalidate(FALSE); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); +} + + +void COrderList::OnRenderOrder() +{ + OrdSelection selection = GetCurSel(); + m_modDoc.OnFileWaveConvert(selection.firstOrd, selection.lastOrd); +} + + +void COrderList::OnDeleteOrder() +{ + OrdSelection selection = GetCurSel(); + // remove selection + m_nScrollPos2nd = ORDERINDEX_INVALID; + + Order().Remove(selection.firstOrd, selection.lastOrd); + + m_modDoc.SetModified(); + Invalidate(FALSE); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); + + DeleteUpdatePlaystate(selection.firstOrd, selection.lastOrd); + + SetCurSel(selection.firstOrd, true, false, true); +} + + +void COrderList::OnPatternProperties() +{ + ModSequence &order = Order(); + const auto ord = GetCurSel(true).firstOrd; + if(order.IsValidPat(ord)) + m_pParent.PostViewMessage(VIEWMSG_PATTERNPROPERTIES, order[ord]); +} + + +void COrderList::OnPlayerPlay() +{ + m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PLAY); +} + + +void COrderList::OnPlayerPause() +{ + m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PAUSE); +} + + +void COrderList::OnPlayerPlayFromStart() +{ + m_pParent.PostMessage(WM_COMMAND, ID_PLAYER_PLAYFROMSTART); +} + + +void COrderList::OnPatternPlayFromStart() +{ + m_pParent.PostMessage(WM_COMMAND, IDC_PATTERN_PLAYFROMSTART); +} + + +void COrderList::OnCreateNewPattern() +{ + m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_NEW); +} + + +void COrderList::OnDuplicatePattern() +{ + m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_COPY); +} + + +void COrderList::OnMergePatterns() +{ + m_pParent.PostMessage(WM_COMMAND, ID_ORDERLIST_MERGE); +} + + +void COrderList::OnPatternCopy() +{ + m_pParent.PostMessage(WM_COMMAND, ID_PATTERNCOPY); +} + + +void COrderList::OnPatternPaste() +{ + m_pParent.PostMessage(WM_COMMAND, ID_PATTERNPASTE); +} + + +void COrderList::OnSetRestartPos() +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + bool modified = false; + if(m_nScrollPos == Order().GetRestartPos()) + { + // Unset position + modified = (m_nScrollPos != 0); + Order().SetRestartPos(0); + } else if(sndFile.GetModSpecifications().hasRestartPos) + { + // Set new position + modified = true; + Order().SetRestartPos(m_nScrollPos); + } + if(modified) + { + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().RestartPos(), this); + } +} + + +LRESULT COrderList::OnHelpHitTest(WPARAM, LPARAM) +{ + return HID_BASE_COMMAND + IDC_ORDERLIST; +} + + +LRESULT COrderList::OnDragonDropping(WPARAM doDrop, LPARAM lParam) +{ + const DRAGONDROP *pDropInfo = (const DRAGONDROP *)lParam; + CPoint pt; + + if((!pDropInfo) || (&m_modDoc.GetSoundFile() != pDropInfo->sndFile) || (!m_cxFont)) + return FALSE; + BOOL canDrop = FALSE; + switch(pDropInfo->dropType) + { + case DRAGONDROP_ORDER: + if(pDropInfo->dropItem >= Order().size()) + break; + case DRAGONDROP_PATTERN: + canDrop = TRUE; + break; + } + if(!canDrop || !doDrop) + return canDrop; + GetCursorPos(&pt); + ScreenToClient(&pt); + if(pt.x < 0) + pt.x = 0; + ORDERINDEX posDest = mpt::saturate_cast<ORDERINDEX>(m_nXScroll + (pt.x / m_cxFont)); + if(posDest >= Order().size()) + return FALSE; + switch(pDropInfo->dropType) + { + case DRAGONDROP_PATTERN: + Order()[posDest] = static_cast<PATTERNINDEX>(pDropInfo->dropItem); + break; + + case DRAGONDROP_ORDER: + Order()[posDest] = Order()[pDropInfo->dropItem]; + break; + } + if(canDrop) + { + Invalidate(FALSE); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), this); + SetCurSel(posDest, true); + } + return canDrop; +} + + +ORDERINDEX COrderList::SetMargins(int i) +{ + m_nOrderlistMargins = i; + return GetMargins(); +} + + +void COrderList::SelectSequence(const SEQUENCEINDEX seq) +{ + CriticalSection cs; + + CMainFrame::GetMainFrame()->ResetNotificationBuffer(); + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + const bool editSequence = seq >= sndFile.Order.GetNumSequences(); + if(seq == kSplitSequence) + { + if(!sndFile.Order.CanSplitSubsongs()) + { + Reporting::Information(U_("No sub songs have been found in this sequence.")); + return; + } + if(Reporting::Confirm(U_("The order list contains separator items.\nDo you want to split the sequence at the separators into multiple song sequences?")) != cnfYes) + return; + if(!sndFile.Order.SplitSubsongsToMultipleSequences()) + return; + } else if(seq == kDeleteSequence) + { + SEQUENCEINDEX curSeq = sndFile.Order.GetCurrentSequenceIndex(); + mpt::ustring str = MPT_UFORMAT("Remove sequence {}: {}?")(curSeq + 1, mpt::ToUnicode(Order().GetName())); + if(Reporting::Confirm(str) == cnfYes) + sndFile.Order.RemoveSequence(curSeq); + else + return; + } else if(seq == kAddSequence || seq == kDuplicateSequence) + { + const bool duplicate = (seq == kDuplicateSequence); + const SEQUENCEINDEX newIndex = sndFile.Order.GetCurrentSequenceIndex() + 1u; + std::vector<SEQUENCEINDEX> newOrder(sndFile.Order.GetNumSequences()); + std::iota(newOrder.begin(), newOrder.end(), SEQUENCEINDEX(0)); + newOrder.insert(newOrder.begin() + newIndex, duplicate ? sndFile.Order.GetCurrentSequenceIndex() : SEQUENCEINDEX_INVALID); + if(m_modDoc.ReArrangeSequences(newOrder)) + { + sndFile.Order.SetSequence(newIndex); + if(const auto name = sndFile.Order().GetName(); duplicate && !name.empty()) + sndFile.Order().SetName(name + U_(" (Copy)")); + m_modDoc.UpdateAllViews(nullptr, SequenceHint(SEQUENCEINDEX_INVALID).Names().Data()); + } + } else if(seq == sndFile.Order.GetCurrentSequenceIndex()) + return; + else if(seq < sndFile.Order.GetNumSequences()) + sndFile.Order.SetSequence(seq); + ORDERINDEX posCandidate = Order().GetLengthTailTrimmed() - 1; + SetCurSel(std::min(m_nScrollPos, posCandidate), true, false, true); + m_pParent.SetCurrentPattern(Order()[m_nScrollPos]); + + UpdateScrollInfo(); + // This won't make sense anymore in the new sequence. + OnUnlockPlayback(); + + cs.Leave(); + + if(editSequence) + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, SequenceHint().Data(), nullptr); +} + + +void COrderList::QueuePattern(CPoint pt) +{ + CRect rect; + GetClientRect(&rect); + + if(!rect.PtInRect(pt)) + return; + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + + const PATTERNINDEX ignoreIndex = sndFile.Order.GetIgnoreIndex(); + const PATTERNINDEX stopIndex = sndFile.Order.GetInvalidPatIndex(); + const ORDERINDEX length = Order().GetLength(); + ORDERINDEX order = GetOrderFromPoint(pt); + + // If this is not a playable order item, find the next valid item. + while(order < length && (Order()[order] == ignoreIndex || Order()[order] == stopIndex)) + { + order++; + } + + if(order < length) + { + if(sndFile.m_PlayState.m_nSeqOverride == order) + { + // This item is already queued: Dequeue it. + sndFile.m_PlayState.m_nSeqOverride = ORDERINDEX_INVALID; + } else + { + if(Order().IsPositionLocked(order)) + { + // Users wants to go somewhere else, so let them do that. + OnUnlockPlayback(); + } + + sndFile.m_PlayState.m_nSeqOverride = order; + } + Invalidate(FALSE); + } +} + + +void COrderList::OnLockPlayback() +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + + OrdSelection selection = GetCurSel(); + if(selection.firstOrd == sndFile.m_lockOrderStart && selection.lastOrd == sndFile.m_lockOrderEnd) + { + OnUnlockPlayback(); + } else + { + sndFile.m_lockOrderStart = selection.firstOrd; + sndFile.m_lockOrderEnd = selection.lastOrd; + Invalidate(FALSE); + } +} + + +void COrderList::OnUnlockPlayback() +{ + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + sndFile.m_lockOrderStart = sndFile.m_lockOrderEnd = ORDERINDEX_INVALID; + Invalidate(FALSE); +} + + +void COrderList::InsertUpdatePlaystate(ORDERINDEX first, ORDERINDEX last) +{ + auto &sndFile = m_modDoc.GetSoundFile(); + Util::InsertItem(first, last, sndFile.m_PlayState.m_nNextOrder); + if(sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID) + Util::InsertItem(first, last, sndFile.m_PlayState.m_nSeqOverride); + // Adjust order lock position + if(sndFile.m_lockOrderStart != ORDERINDEX_INVALID) + Util::InsertRange(first, last, sndFile.m_lockOrderStart, sndFile.m_lockOrderEnd); +} + + +void COrderList::DeleteUpdatePlaystate(ORDERINDEX first, ORDERINDEX last) +{ + auto &sndFile = m_modDoc.GetSoundFile(); + Util::DeleteItem(first, last, sndFile.m_PlayState.m_nNextOrder); + if(sndFile.m_PlayState.m_nSeqOverride != ORDERINDEX_INVALID) + Util::DeleteItem(first, last, sndFile.m_PlayState.m_nSeqOverride); + // Adjust order lock position + if(sndFile.m_lockOrderStart != ORDERINDEX_INVALID) + Util::DeleteRange(first, last, sndFile.m_lockOrderStart, sndFile.m_lockOrderEnd); +} + + +INT_PTR COrderList::OnToolHitTest(CPoint point, TOOLINFO *pTI) const +{ + CRect rect; + GetClientRect(&rect); + + pTI->hwnd = m_hWnd; + pTI->uId = GetOrderFromPoint(point); + pTI->rect = rect; + pTI->lpszText = LPSTR_TEXTCALLBACK; + return pTI->uId; +} + + +BOOL COrderList::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR; + if(!(pTTT->uFlags & TTF_IDISHWND)) + { + CString text; + const CSoundFile &sndFile = m_modDoc.GetSoundFile(); + const ModSequence &order = Order(); + const ORDERINDEX ord = mpt::saturate_cast<ORDERINDEX>(pNMHDR->idFrom), ordLen = order.GetLengthTailTrimmed(); + text.Format(_T("Position %u of %u [%02Xh of %02Xh]"), ord, ordLen, ord, ordLen); + if(order.IsValidPat(ord)) + { + PATTERNINDEX pat = order[ord]; + const std::string name = sndFile.Patterns[pat].GetName(); + if(!name.empty()) + { + ::SendMessage(pNMHDR->hwndFrom, TTM_SETMAXTIPWIDTH, 0, int32_max); // Allow multiline tooltip + text += _T("\r\n") + mpt::ToCString(sndFile.GetCharsetInternal(), name); + } + } + mpt::String::WriteCStringBuf(pTTT->szText) = text; + return TRUE; + } + return FALSE; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.cpp new file mode 100644 index 00000000..e18237e3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.cpp @@ -0,0 +1,3792 @@ +/* + * Ctrl_smp.cpp + * ------------ + * Purpose: Sample tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Childfrm.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "../soundlib/mod_specifications.h" +#include "Globals.h" +#include "Ctrl_smp.h" +#include "View_smp.h" +#include "SampleEditorDialogs.h" +#include "dlg_misc.h" +#include "PSRatioCalc.h" +#include <soundtouch/include/SoundTouch.h> +#include <soundtouch/source/SoundTouchDLL/SoundTouchDLL.h> +#include <smbPitchShift/smbPitchShift.h> +#include "../tracklib/SampleEdit.h" +#include "Autotune.h" +#include "../common/mptStringBuffer.h" +#include "../common/mptFileIO.h" +#include "../common/FileReader.h" +#include "openmpt/soundbase/Copy.hpp" +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "../soundlib/SampleCopy.h" +#include "FileDialog.h" +#include "ProgressDialog.h" +#include "../include/r8brain/CDSPResampler.h" +#include "../soundlib/MixFuncTable.h" +#include "mpt/audio/span.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +#define BASENOTE_MIN (1*12) // C-1 +#define BASENOTE_MAX (10*12+11) // B-10 + +BEGIN_MESSAGE_MAP(CCtrlSamples, CModControlDlg) + //{{AFX_MSG_MAP(CCtrlSamples) + ON_WM_VSCROLL() + ON_WM_XBUTTONUP() + ON_NOTIFY(TBN_DROPDOWN, IDC_TOOLBAR1, &CCtrlSamples::OnTbnDropDownToolBar) + ON_COMMAND(IDC_SAMPLE_NEW, &CCtrlSamples::OnSampleNew) + ON_COMMAND(IDC_SAMPLE_DUPLICATE, &CCtrlSamples::OnSampleDuplicate) + ON_COMMAND(IDC_SAMPLE_OPEN, &CCtrlSamples::OnSampleOpen) + ON_COMMAND(IDC_SAMPLE_OPENKNOWN, &CCtrlSamples::OnSampleOpenKnown) + ON_COMMAND(IDC_SAMPLE_OPENRAW, &CCtrlSamples::OnSampleOpenRaw) + ON_COMMAND(IDC_SAMPLE_SAVEAS, &CCtrlSamples::OnSampleSave) + ON_COMMAND(IDC_SAVE_ONE, &CCtrlSamples::OnSampleSaveOne) + ON_COMMAND(IDC_SAVE_ALL, &CCtrlSamples::OnSampleSaveAll) + ON_COMMAND(IDC_SAMPLE_PLAY, &CCtrlSamples::OnSamplePlay) + ON_COMMAND(IDC_SAMPLE_NORMALIZE, &CCtrlSamples::OnNormalize) + ON_COMMAND(IDC_SAMPLE_AMPLIFY, &CCtrlSamples::OnAmplify) + ON_COMMAND(IDC_SAMPLE_RESAMPLE, &CCtrlSamples::OnResample) + ON_COMMAND(IDC_SAMPLE_REVERSE, &CCtrlSamples::OnReverse) + ON_COMMAND(IDC_SAMPLE_SILENCE, &CCtrlSamples::OnSilence) + ON_COMMAND(IDC_SAMPLE_INVERT, &CCtrlSamples::OnInvert) + ON_COMMAND(IDC_SAMPLE_SIGN_UNSIGN, &CCtrlSamples::OnSignUnSign) + ON_COMMAND(IDC_SAMPLE_DCOFFSET, &CCtrlSamples::OnRemoveDCOffset) + ON_COMMAND(IDC_SAMPLE_XFADE, &CCtrlSamples::OnXFade) + ON_COMMAND(IDC_SAMPLE_STEREOSEPARATION, &CCtrlSamples::OnStereoSeparation) + ON_COMMAND(IDC_SAMPLE_AUTOTUNE, &CCtrlSamples::OnAutotune) + ON_COMMAND(IDC_CHECK1, &CCtrlSamples::OnSetPanningChanged) + ON_COMMAND(IDC_CHECK2, &CCtrlSamples::OnKeepSampleOnDisk) + ON_COMMAND(ID_PREVINSTRUMENT, &CCtrlSamples::OnPrevInstrument) + ON_COMMAND(ID_NEXTINSTRUMENT, &CCtrlSamples::OnNextInstrument) + ON_COMMAND(IDC_BUTTON1, &CCtrlSamples::OnPitchShiftTimeStretch) + ON_COMMAND(IDC_BUTTON2, &CCtrlSamples::OnEstimateSampleSize) + ON_COMMAND(IDC_CHECK3, &CCtrlSamples::OnEnableStretchToSize) + ON_COMMAND(IDC_SAMPLE_INITOPL, &CCtrlSamples::OnInitOPLInstrument) + + ON_EN_CHANGE(IDC_SAMPLE_NAME, &CCtrlSamples::OnNameChanged) + ON_EN_CHANGE(IDC_SAMPLE_FILENAME, &CCtrlSamples::OnFileNameChanged) + ON_EN_CHANGE(IDC_EDIT_SAMPLE, &CCtrlSamples::OnSampleChanged) + ON_EN_CHANGE(IDC_EDIT1, &CCtrlSamples::OnLoopPointsChanged) + ON_EN_CHANGE(IDC_EDIT2, &CCtrlSamples::OnLoopPointsChanged) + ON_EN_CHANGE(IDC_EDIT3, &CCtrlSamples::OnSustainPointsChanged) + ON_EN_CHANGE(IDC_EDIT4, &CCtrlSamples::OnSustainPointsChanged) + ON_EN_CHANGE(IDC_EDIT5, &CCtrlSamples::OnFineTuneChanged) + ON_EN_CHANGE(IDC_EDIT7, &CCtrlSamples::OnVolumeChanged) + ON_EN_CHANGE(IDC_EDIT8, &CCtrlSamples::OnGlobalVolChanged) + ON_EN_CHANGE(IDC_EDIT9, &CCtrlSamples::OnPanningChanged) + ON_EN_CHANGE(IDC_EDIT14, &CCtrlSamples::OnVibSweepChanged) + ON_EN_CHANGE(IDC_EDIT15, &CCtrlSamples::OnVibDepthChanged) + ON_EN_CHANGE(IDC_EDIT16, &CCtrlSamples::OnVibRateChanged) + + ON_EN_SETFOCUS(IDC_SAMPLE_NAME, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_SAMPLE_FILENAME, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT1, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT2, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT3, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT4, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT5, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT7, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT8, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT9, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT14, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT15, &CCtrlSamples::OnEditFocus) + ON_EN_SETFOCUS(IDC_EDIT16, &CCtrlSamples::OnEditFocus) + + ON_EN_KILLFOCUS(IDC_EDIT5, &CCtrlSamples::OnFineTuneChangedDone) + + ON_CBN_SELCHANGE(IDC_COMBO_BASENOTE,&CCtrlSamples::OnBaseNoteChanged) + ON_CBN_SELCHANGE(IDC_COMBO_ZOOM, &CCtrlSamples::OnZoomChanged) + ON_CBN_SELCHANGE(IDC_COMBO1, &CCtrlSamples::OnLoopTypeChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CCtrlSamples::OnSustainTypeChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CCtrlSamples::OnVibTypeChanged) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CCtrlSamples::OnCustomKeyMsg) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void CCtrlSamples::DoDataExchange(CDataExchange* pDX) +{ + CModControlDlg::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CCtrlSamples) + DDX_Control(pDX, IDC_TOOLBAR1, m_ToolBar1); + DDX_Control(pDX, IDC_TOOLBAR2, m_ToolBar2); + DDX_Control(pDX, IDC_SAMPLE_NAME, m_EditName); + DDX_Control(pDX, IDC_SAMPLE_FILENAME, m_EditFileName); + DDX_Control(pDX, IDC_SAMPLE_NAME, m_EditName); + DDX_Control(pDX, IDC_SAMPLE_FILENAME, m_EditFileName); + DDX_Control(pDX, IDC_COMBO_ZOOM, m_ComboZoom); + DDX_Control(pDX, IDC_COMBO_BASENOTE, m_CbnBaseNote); + DDX_Control(pDX, IDC_SPIN_SAMPLE, m_SpinSample); + DDX_Control(pDX, IDC_EDIT_SAMPLE, m_EditSample); + DDX_Control(pDX, IDC_CHECK1, m_CheckPanning); + DDX_Control(pDX, IDC_SPIN1, m_SpinLoopStart); + DDX_Control(pDX, IDC_SPIN2, m_SpinLoopEnd); + DDX_Control(pDX, IDC_SPIN3, m_SpinSustainStart); + DDX_Control(pDX, IDC_SPIN4, m_SpinSustainEnd); + DDX_Control(pDX, IDC_SPIN5, m_SpinFineTune); + DDX_Control(pDX, IDC_SPIN7, m_SpinVolume); + DDX_Control(pDX, IDC_SPIN8, m_SpinGlobalVol); + DDX_Control(pDX, IDC_SPIN9, m_SpinPanning); + DDX_Control(pDX, IDC_SPIN11, m_SpinVibSweep); + DDX_Control(pDX, IDC_SPIN12, m_SpinVibDepth); + DDX_Control(pDX, IDC_SPIN13, m_SpinVibRate); + DDX_Control(pDX, IDC_COMBO1, m_ComboLoopType); + DDX_Control(pDX, IDC_COMBO2, m_ComboSustainType); + DDX_Control(pDX, IDC_COMBO3, m_ComboAutoVib); + DDX_Control(pDX, IDC_EDIT1, m_EditLoopStart); + DDX_Control(pDX, IDC_EDIT2, m_EditLoopEnd); + DDX_Control(pDX, IDC_EDIT3, m_EditSustainStart); + DDX_Control(pDX, IDC_EDIT4, m_EditSustainEnd); + DDX_Control(pDX, IDC_EDIT5, m_EditFineTune); + DDX_Control(pDX, IDC_EDIT7, m_EditVolume); + DDX_Control(pDX, IDC_EDIT8, m_EditGlobalVol); + DDX_Control(pDX, IDC_EDIT9, m_EditPanning); + DDX_Control(pDX, IDC_EDIT14, m_EditVibSweep); + DDX_Control(pDX, IDC_EDIT15, m_EditVibDepth); + DDX_Control(pDX, IDC_EDIT16, m_EditVibRate); + DDX_Control(pDX, IDC_COMBO4, m_ComboPitch); + DDX_Control(pDX, IDC_COMBO5, m_ComboQuality); + DDX_Control(pDX, IDC_COMBO6, m_ComboFFT); + DDX_Control(pDX, IDC_SPIN10, m_SpinSequenceMs); + DDX_Control(pDX, IDC_SPIN14, m_SpinSeekWindowMs); + DDX_Control(pDX, IDC_SPIN15, m_SpinOverlap); + DDX_Control(pDX, IDC_SPIN16, m_SpinStretchAmount); + DDX_Text(pDX, IDC_EDIT6, m_dTimeStretchRatio); + //}}AFX_DATA_MAP +} + + +CCtrlSamples::CCtrlSamples(CModControlView &parent, CModDoc &document) + : CModControlDlg(parent, document) +{ + m_nLockCount = 1; +} + + + +CCtrlSamples::~CCtrlSamples() +{ +} + + + +CRuntimeClass *CCtrlSamples::GetAssociatedViewClass() +{ + return RUNTIME_CLASS(CViewSample); +} + + +void CCtrlSamples::OnEditFocus() +{ + m_startedEdit = false; +} + + +BOOL CCtrlSamples::OnInitDialog() +{ + CModControlDlg::OnInitDialog(); + m_bInitialized = FALSE; + SetRedraw(FALSE); + + // Zoom Selection + static constexpr std::pair<const TCHAR *, int> ZoomLevels[] = + { + {_T("Auto"), 0}, + {_T("1:1"), 1}, + {_T("2:1"), -2}, + {_T("4:1"), -3}, + {_T("8:1"), -4}, + {_T("16:1"), -5}, + {_T("32:1"), -6}, + {_T("1:2"), 2}, + {_T("1:4"), 3}, + {_T("1:8"), 4}, + {_T("1:16"), 5}, + {_T("1:32"), 6}, + {_T("1:64"), 7}, + {_T("1:128"), 8}, + {_T("1:256"), 9}, + {_T("1:512"), 10}, + }; + m_ComboZoom.SetRedraw(FALSE); + m_ComboZoom.InitStorage(static_cast<int>(std::size(ZoomLevels)), 4); + for(const auto &[str, data] : ZoomLevels) + { + m_ComboZoom.SetItemData(m_ComboZoom.AddString(str), static_cast<DWORD_PTR>(data)); + } + m_ComboZoom.SetRedraw(TRUE); + m_ComboZoom.SetCurSel(0); + + // File ToolBar + m_ToolBar1.SetExtendedStyle(m_ToolBar1.GetExtendedStyle() | TBSTYLE_EX_DRAWDDARROWS); + m_ToolBar1.Init(CMainFrame::GetMainFrame()->m_PatternIcons,CMainFrame::GetMainFrame()->m_PatternIconsDisabled); + m_ToolBar1.AddButton(IDC_SAMPLE_NEW, TIMAGE_SAMPLE_NEW, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN); + m_ToolBar1.AddButton(IDC_SAMPLE_OPEN, TIMAGE_OPEN, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN); + m_ToolBar1.AddButton(IDC_SAMPLE_SAVEAS, TIMAGE_SAVE, TBSTYLE_BUTTON | TBSTYLE_DROPDOWN); + // Edit ToolBar + m_ToolBar2.Init(CMainFrame::GetMainFrame()->m_PatternIcons,CMainFrame::GetMainFrame()->m_PatternIconsDisabled); + m_ToolBar2.AddButton(IDC_SAMPLE_PLAY, TIMAGE_PREVIEW); + m_ToolBar2.AddButton(IDC_SAMPLE_NORMALIZE, TIMAGE_SAMPLE_NORMALIZE); + m_ToolBar2.AddButton(IDC_SAMPLE_AMPLIFY, TIMAGE_SAMPLE_AMPLIFY); + m_ToolBar2.AddButton(IDC_SAMPLE_DCOFFSET, TIMAGE_SAMPLE_DCOFFSET); + m_ToolBar2.AddButton(IDC_SAMPLE_STEREOSEPARATION, TIMAGE_SAMPLE_STEREOSEP); + m_ToolBar2.AddButton(IDC_SAMPLE_RESAMPLE, TIMAGE_SAMPLE_RESAMPLE); + m_ToolBar2.AddButton(IDC_SAMPLE_REVERSE, TIMAGE_SAMPLE_REVERSE); + m_ToolBar2.AddButton(IDC_SAMPLE_SILENCE, TIMAGE_SAMPLE_SILENCE); + m_ToolBar2.AddButton(IDC_SAMPLE_INVERT, TIMAGE_SAMPLE_INVERT); + m_ToolBar2.AddButton(IDC_SAMPLE_SIGN_UNSIGN, TIMAGE_SAMPLE_UNSIGN); + m_ToolBar2.AddButton(IDC_SAMPLE_XFADE, TIMAGE_SAMPLE_FIXLOOP); + m_ToolBar2.AddButton(IDC_SAMPLE_AUTOTUNE, TIMAGE_SAMPLE_AUTOTUNE); + // Setup Controls + m_SpinVolume.SetRange(0, 64); + m_SpinGlobalVol.SetRange(0, 64); + + m_CbnBaseNote.InitStorage(BASENOTE_MAX - BASENOTE_MIN, 4); + m_CbnBaseNote.SetRedraw(FALSE); + for(ModCommand::NOTE i = BASENOTE_MIN; i <= BASENOTE_MAX; i++) + { + CString noteName = mpt::ToCString(CSoundFile::GetDefaultNoteName(i % 12)) + mpt::cfmt::val(i / 12); + m_CbnBaseNote.SetItemData(m_CbnBaseNote.AddString(noteName), i - (NOTE_MIDDLEC - NOTE_MIN)); + } + m_CbnBaseNote.SetRedraw(TRUE); + + m_ComboFFT.ShowWindow(SW_SHOW); + m_ComboPitch.ShowWindow(SW_SHOW); + m_ComboQuality.ShowWindow(SW_SHOW); + m_ComboFFT.ShowWindow(SW_SHOW); + + // Pitch selection + // Allow pitch from -12 (1 octave down) to +12 (1 octave up) + m_ComboPitch.InitStorage(25, 4); + m_ComboPitch.SetRedraw(FALSE); + for(int i = -12 ; i <= 12 ; i++) + { + mpt::tstring str; + if(i == 0) + str = _T("none"); + else if(i < 0) + str = mpt::tfmt::dec(i); + else + str = _T("+") + mpt::tfmt::dec(i); + m_ComboPitch.SetItemData(m_ComboPitch.AddString(str.c_str()), i + 12); + } + m_ComboPitch.SetRedraw(TRUE); + // Set "none" as default pitch + m_ComboPitch.SetCurSel(12); + + // Quality selection + // Allow quality from 4 to 128 + m_ComboQuality.InitStorage(128 - 4, 4); + m_ComboQuality.SetRedraw(FALSE); + for(int i = 4; i <= 128; i++) + { + m_ComboQuality.SetItemData(m_ComboQuality.AddString(mpt::tfmt::dec(i).c_str()), i - 4); + } + m_ComboQuality.SetRedraw(TRUE); + // Set 32 as default quality + m_ComboQuality.SetCurSel(32 - 4); + + // FFT size selection + // Deduce exponent from equation : MAX_FRAME_LENGTH = 2^exponent + constexpr int exponent = mpt::bit_width(uint32(MAX_FRAME_LENGTH)) - 1; + // Allow FFT size from 2^8 (256) to 2^exponent (MAX_FRAME_LENGTH) + m_ComboFFT.InitStorage(exponent - 8, 4); + m_ComboFFT.SetRedraw(FALSE); + for(int i = 8 ; i <= exponent ; i++) + { + m_ComboFFT.SetItemData(m_ComboFFT.AddString(mpt::tfmt::dec(1 << i).c_str()), i - 8); + } + m_ComboFFT.SetRedraw(TRUE); + // Set 4096 as default FFT size + m_ComboFFT.SetCurSel(4); + + // Stretch to size check box + OnEnableStretchToSize(); + m_SpinSequenceMs.SetRange32(0, 9999); + m_SpinSeekWindowMs.SetRange32(0, 9999); + m_SpinOverlap.SetRange32(0, 9999); + m_SpinStretchAmount.SetRange32(50, 200); + + SetRedraw(TRUE); + return TRUE; +} + + +void CCtrlSamples::RecalcLayout() +{ +} + + +bool CCtrlSamples::SetCurrentSample(SAMPLEINDEX nSmp, LONG lZoom, bool bUpdNum) +{ + if(m_sndFile.GetNumSamples() < 1) + m_sndFile.m_nSamples = 1; + if((nSmp < 1) || (nSmp > m_sndFile.GetNumSamples())) + return FALSE; + + LockControls(); + if(m_nSample != nSmp) + { + m_nSample = nSmp; + UpdateView(SampleHint(m_nSample).Info()); + m_parent.SampleChanged(m_nSample); + } + if(bUpdNum) + { + SetDlgItemInt(IDC_EDIT_SAMPLE, m_nSample); + m_SpinSample.SetRange(1, m_sndFile.GetNumSamples()); + } + if(lZoom == -1) + { + lZoom = static_cast<int>(m_ComboZoom.GetItemData(m_ComboZoom.GetCurSel())); + } else + { + for(int i = 0; i< m_ComboZoom.GetCount(); i++) + { + if(static_cast<int>(m_ComboZoom.GetItemData(i)) == lZoom) + { + m_ComboZoom.SetCurSel(i); + break; + } + } + } + static_assert(MAX_SAMPLES < uint16_max); + SendViewMessage(VIEWMSG_SETCURRENTSAMPLE, (lZoom << 16) | m_nSample); + UnlockControls(); + return true; +} + + +void CCtrlSamples::OnActivatePage(LPARAM lParam) +{ + if (lParam < 0) + { + int nIns = m_parent.GetInstrumentChange(); + if (m_sndFile.GetNumInstruments()) + { + if ((nIns > 0) && (!m_modDoc.IsChildSample((INSTRUMENTINDEX)nIns, m_nSample))) + { + SAMPLEINDEX k = m_modDoc.FindInstrumentChild((INSTRUMENTINDEX)nIns); + if (k > 0) lParam = k; + } + } else + { + if (nIns > 0) lParam = nIns; + } + } else if (lParam > 0) + { + if (m_sndFile.GetNumInstruments()) + { + INSTRUMENTINDEX k = (INSTRUMENTINDEX)m_parent.GetInstrumentChange(); + if (!m_modDoc.IsChildSample(k, (SAMPLEINDEX)lParam)) + { + INSTRUMENTINDEX nins = m_modDoc.FindSampleParent((SAMPLEINDEX)lParam); + if(nins != INSTRUMENTINDEX_INVALID) + { + m_parent.InstrumentChanged(nins); + } + } + } else + { + m_parent.InstrumentChanged(static_cast<int>(lParam)); + } + } + + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + SAMPLEVIEWSTATE &sampleState = pFrame->GetSampleViewState(); + if(sampleState.initialSample != 0) + { + m_nSample = sampleState.initialSample; + sampleState.initialSample = 0; + } + + SetCurrentSample((lParam > 0) ? ((SAMPLEINDEX)lParam) : m_nSample); + + // Initial Update + if (!m_bInitialized) UpdateView(SampleHint(m_nSample).Info().ModType(), NULL); + if (m_hWndView) PostViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&sampleState); + SwitchToView(); + + // Combo boxes randomly disappear without this... why? + Invalidate(); +} + + +void CCtrlSamples::OnDeactivatePage() +{ + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + if ((pFrame) && (m_hWndView)) SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&pFrame->GetSampleViewState()); + m_modDoc.NoteOff(0, true); +} + + +LRESULT CCtrlSamples::OnModCtrlMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case CTRLMSG_GETCURRENTINSTRUMENT: + return m_nSample; + break; + + case CTRLMSG_SMP_PREVINSTRUMENT: + OnPrevInstrument(); + break; + + case CTRLMSG_SMP_NEXTINSTRUMENT: + OnNextInstrument(); + break; + + case CTRLMSG_SMP_OPENFILE: + if(lParam) + return OpenSample(*reinterpret_cast<const mpt::PathString *>(lParam)); + break; + + case CTRLMSG_SMP_SONGDROP: + if(lParam) + { + const auto &dropInfo = *reinterpret_cast<const DRAGONDROP *>(lParam); + if(dropInfo.sndFile) + return OpenSample(*dropInfo.sndFile, static_cast<SAMPLEINDEX>(dropInfo.dropItem)) ? TRUE : FALSE; + } + break; + + case CTRLMSG_SMP_SETZOOM: + SetCurrentSample(m_nSample, static_cast<int>(lParam), FALSE); + break; + + case CTRLMSG_SETCURRENTINSTRUMENT: + SetCurrentSample((SAMPLEINDEX)lParam, -1, TRUE); + break; + + case CTRLMSG_SMP_INITOPL: + OnInitOPLInstrument(); + break; + + case CTRLMSG_SMP_NEWSAMPLE: + return InsertSample(false) ? 1 : 0; + + case IDC_SAMPLE_REVERSE: + OnReverse(); + break; + + case IDC_SAMPLE_SILENCE: + OnSilence(); + break; + + case IDC_SAMPLE_INVERT: + OnInvert(); + break; + + case IDC_SAMPLE_XFADE: + OnXFade(); + break; + + case IDC_SAMPLE_STEREOSEPARATION: + OnStereoSeparation(); + break; + + case IDC_SAMPLE_AUTOTUNE: + OnAutotune(); + break; + + case IDC_SAMPLE_SIGN_UNSIGN: + OnSignUnSign(); + break; + + case IDC_SAMPLE_DCOFFSET: + RemoveDCOffset(false); + break; + + case IDC_SAMPLE_NORMALIZE: + Normalize(false); + break; + + case IDC_SAMPLE_AMPLIFY: + OnAmplify(); + break; + + case IDC_SAMPLE_QUICKFADE: + OnQuickFade(); + break; + + case IDC_SAMPLE_OPEN: + OnSampleOpen(); + break; + + case IDC_SAMPLE_SAVEAS: + OnSampleSave(); + break; + + case IDC_SAMPLE_NEW: + InsertSample(false); + break; + + default: + return CModControlDlg::OnModCtrlMsg(wParam, lParam); + } + return 0; +} + + +BOOL CCtrlSamples::GetToolTipText(UINT uId, LPTSTR pszText) +{ + if ((pszText) && (uId)) + { + UINT val = GetDlgItemInt(uId); + const TCHAR *s = nullptr; + CommandID cmd = kcNull; + switch(uId) + { + case IDC_SAMPLE_NEW: s = _T("Insert Sample"); cmd = kcSampleNew; break; + case IDC_SAMPLE_OPEN: s = _T("Import Sample"); cmd = kcSampleLoad; break; + case IDC_SAMPLE_SAVEAS: s = _T("Save Sample"); cmd = kcSampleSave; break; + case IDC_SAMPLE_PLAY: s = _T("Play Sample"); break; + case IDC_SAMPLE_NORMALIZE: s = _T("Normalize (hold shift to normalize all samples)"); cmd = kcSampleNormalize; break; + case IDC_SAMPLE_AMPLIFY: s = _T("Amplify"); cmd = kcSampleAmplify; break; + case IDC_SAMPLE_DCOFFSET: s = _T("Remove DC Offset and Normalize (hold shift to process all samples)"); cmd = kcSampleRemoveDCOffset; break; + case IDC_SAMPLE_STEREOSEPARATION: s = _T("Change Stereo Separation / Stereo Width of the sample"); cmd = kcSampleStereoSep; break; + case IDC_SAMPLE_RESAMPLE: s = _T("Resample"); cmd = kcSampleResample; break; + case IDC_SAMPLE_REVERSE: s = _T("Reverse"); cmd = kcSampleReverse; break; + case IDC_SAMPLE_SILENCE: s = _T("Silence"); cmd = kcSampleSilence; break; + case IDC_SAMPLE_INVERT: s = _T("Invert Phase"); cmd = kcSampleInvert; break; + case IDC_SAMPLE_SIGN_UNSIGN: s = _T("Signed/Unsigned Conversion"); cmd = kcSampleSignUnsign; break; + case IDC_SAMPLE_XFADE: s = _T("Crossfade Sample Loops"); cmd = kcSampleXFade; break; + case IDC_SAMPLE_AUTOTUNE: s = _T("Tune the sample to a given note"); cmd = kcSampleAutotune; break; + + case IDC_EDIT7: + case IDC_EDIT8: + // Volume to dB + if(IsOPLInstrument()) + _tcscpy(pszText, (mpt::tfmt::fix((static_cast<int32>(val) - 64) * 0.75, 2) + _T(" dB")).c_str()); + else + _tcscpy(pszText, CModDoc::LinearToDecibels(val, 64.0)); + return TRUE; + + case IDC_EDIT9: + // Panning + if(m_nSample) + { + const ModSample &sample = m_sndFile.GetSample(m_nSample); + _tcscpy(pszText, CModDoc::PanningToString(sample.nPan, 128)); + } + return TRUE; + + case IDC_EDIT5: + case IDC_SPIN5: + case IDC_COMBO_BASENOTE: + if(m_nSample) + { + const ModSample &sample = m_sndFile.GetSample(m_nSample); + const auto freqHz = sample.GetSampleRate(m_sndFile.GetType()); + if(sample.uFlags[CHN_ADLIB]) + { + // Translate to actual note frequency + _tcscpy(pszText, MPT_TFORMAT("{}Hz")(mpt::tfmt::flt(freqHz * (261.625 / 8363.0), 6)).c_str()); + return TRUE; + } + if(m_sndFile.UseFinetuneAndTranspose()) + { + // Transpose + Finetune to Frequency + _tcscpy(pszText, MPT_TFORMAT("{}Hz")(freqHz).c_str()); + return TRUE; + } + } + break; + + case IDC_EDIT14: + // Vibrato Sweep + if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + { + s = _T("Only available in IT / MPTM / XM format"); + break; + } else if(m_nSample) + { + const ModSample &sample = m_sndFile.GetSample(m_nSample); + int ticks = -1; + if(m_sndFile.m_playBehaviour[kITVibratoTremoloPanbrello]) + { + if(val > 0) + ticks = Util::muldivr_unsigned(sample.nVibDepth, 256, val); + } else if(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) + { + if(val > 0) + ticks = Util::muldivr_unsigned(sample.nVibDepth, 128, val); + } else + { + ticks = val; + } + if(ticks >= 0) + _stprintf(pszText, _T("%d ticks"), ticks); + else + _tcscpy(pszText, _T("No Vibrato")); + } + return TRUE; + case IDC_EDIT15: + // Vibrato Depth + if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + _tcscpy(pszText, _T("Only available in IT / MPTM / XM format")); + else + _stprintf(pszText, _T("%u cents"), Util::muldivr_unsigned(val, 100, 64)); + return TRUE; + case IDC_EDIT16: + // Vibrato Rate + if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + { + s = _T("Only available in IT / MPTM / XM format"); + break; + } else if(val == 0) + { + s = _T("Stopped"); + break; + } else + { + const double ticksPerCycle = 256.0 / val; + const uint32 ticksPerBeat = std::max(1u, m_sndFile.m_PlayState.m_nCurrentRowsPerBeat * m_sndFile.m_PlayState.m_nMusicSpeed); + _stprintf(pszText, _T("%.2f beats per cycle (%.2f ticks)"), ticksPerCycle / ticksPerBeat, ticksPerCycle); + } + return TRUE; + + case IDC_CHECK1: + case IDC_EDIT3: + case IDC_EDIT4: + case IDC_COMBO2: + if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) + s = _T("Only available in IT / MPTM format"); + break; + + case IDC_COMBO3: + if(!(m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM))) + s = _T("Only available in IT / MPTM / XM format"); + break; + + case IDC_CHECK2: + s = _T("Keep a reference to the original waveform instead of saving it in the module."); + break; + } + if(s != nullptr) + { + _tcscpy(pszText, s); + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if (!keyText.IsEmpty()) + _tcscat(pszText, MPT_TFORMAT(" ({})")(keyText).c_str()); + } + return TRUE; + } + } + return FALSE; +} + + +void CCtrlSamples::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj == this) return; + if (hint.GetType()[HINT_MPTOPTIONS]) + { + m_ToolBar1.UpdateStyle(); + m_ToolBar2.UpdateStyle(); + } + + const SampleHint sampleHint = hint.ToType<SampleHint>(); + FlagSet<HintType> hintType = sampleHint.GetType(); + if (!m_bInitialized) hintType.set(HINT_MODTYPE); + if(!hintType[HINT_SMPNAMES | HINT_SAMPLEINFO | HINT_MODTYPE]) return; + + const SAMPLEINDEX updateSmp = sampleHint.GetSample(); + if(updateSmp != m_nSample && updateSmp != 0 && !hintType[HINT_MODTYPE]) return; + + const CModSpecifications &specs = m_sndFile.GetModSpecifications(); + const bool isOPL = IsOPLInstrument(); + + LockControls(); + // Updating Ranges + if(hintType[HINT_MODTYPE]) + { + + // Limit text fields + m_EditName.SetLimitText(specs.sampleNameLengthMax); + m_EditFileName.SetLimitText(specs.sampleFilenameLengthMax); + + // Loop Type + m_ComboLoopType.ResetContent(); + m_ComboLoopType.AddString(_T("Off")); + m_ComboLoopType.AddString(_T("On")); + + // Sustain Loop Type + m_ComboSustainType.ResetContent(); + m_ComboSustainType.AddString(_T("Off")); + m_ComboSustainType.AddString(_T("On")); + + // Bidirectional Loops + if (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) + { + m_ComboLoopType.AddString(_T("Bidi")); + m_ComboSustainType.AddString(_T("Bidi")); + } + + // Loop Start + m_SpinLoopStart.SetRange(-1, 1); + m_SpinLoopStart.SetPos(0); + + // Loop End + m_SpinLoopEnd.SetRange(-1, 1); + m_SpinLoopEnd.SetPos(0); + + // Sustain Loop Start + m_SpinSustainStart.SetRange(-1, 1); + m_SpinSustainStart.SetPos(0); + + // Sustain Loop End + m_SpinSustainEnd.SetRange(-1, 1); + m_SpinSustainEnd.SetPos(0); + + // Finetune / C-5 Speed / BaseNote + BOOL b = m_sndFile.UseFinetuneAndTranspose() ? FALSE : TRUE; + SetDlgItemText(IDC_TEXT7, (b) ? _T("Freq. (Hz)") : _T("Finetune")); + m_SpinFineTune.SetRange(-1, 1); + m_EditFileName.EnableWindow(b); + + // AutoVibrato + b = (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE; + m_ComboAutoVib.EnableWindow(b); + m_SpinVibSweep.EnableWindow(b); + m_SpinVibDepth.EnableWindow(b); + m_SpinVibRate.EnableWindow(b); + m_EditVibSweep.EnableWindow(b); + m_EditVibDepth.EnableWindow(b); + m_EditVibRate.EnableWindow(b); + m_SpinVibSweep.SetRange(0, 255); + if(m_sndFile.GetType() & MOD_TYPE_XM) + { + m_SpinVibDepth.SetRange(0, 15); + m_SpinVibRate.SetRange(0, 63); + } else + { + m_SpinVibDepth.SetRange(0, 32); + m_SpinVibRate.SetRange(0, 64); + } + + // Global Volume + b = (m_sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE; + m_EditGlobalVol.EnableWindow(b); + m_SpinGlobalVol.EnableWindow(b); + + // Panning + b = (m_sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) ? TRUE : FALSE; + m_CheckPanning.EnableWindow(b && !(m_sndFile.GetType() & MOD_TYPE_XM)); + m_EditPanning.EnableWindow(b); + m_SpinPanning.EnableWindow(b); + m_SpinPanning.SetRange(0, (m_sndFile.GetType() == MOD_TYPE_XM) ? 255 : 64); + + b = (m_sndFile.GetType() & MOD_TYPE_MOD) ? FALSE : TRUE; + m_CbnBaseNote.EnableWindow(b); + } + // Updating Values + if (hintType[HINT_MODTYPE | HINT_SAMPLEINFO]) + { + if(m_nSample > m_sndFile.GetNumSamples()) + { + SetCurrentSample(m_sndFile.GetNumSamples()); + } + const ModSample &sample = m_sndFile.GetSample(m_nSample); + CString s; + DWORD d; + + m_SpinSample.SetRange(1, m_sndFile.GetNumSamples()); + m_SpinSample.Invalidate(FALSE); // In case the spin button was previously disabled + + // Length / Type + if(isOPL) + s = _T("OPL instrument"); + else + s = MPT_CFORMAT("{}-bit {}, len: {}")(sample.GetElementarySampleSize() * 8, CString(sample.uFlags[CHN_STEREO] ? _T("stereo") : _T("mono")), mpt::cfmt::dec(3, ',', sample.nLength)); + SetDlgItemText(IDC_TEXT5, s); + // File Name + s = mpt::ToCString(m_sndFile.GetCharsetInternal(), sample.filename); + if (specs.sampleFilenameLengthMax == 0) s.Empty(); + SetDlgItemText(IDC_SAMPLE_FILENAME, s); + // Volume + if(sample.uFlags[SMP_NODEFAULTVOLUME]) + SetDlgItemText(IDC_EDIT7, _T("none")); + else + SetDlgItemInt(IDC_EDIT7, sample.nVolume / 4u); + // Global Volume + SetDlgItemInt(IDC_EDIT8, sample.nGlobalVol); + // Panning + CheckDlgButton(IDC_CHECK1, (sample.uFlags[CHN_PANNING]) ? BST_CHECKED : BST_UNCHECKED); + if (m_sndFile.GetType() == MOD_TYPE_XM) + SetDlgItemInt(IDC_EDIT9, sample.nPan); //displayed panning with XM is 0-256, just like MPT's internal engine + else + SetDlgItemInt(IDC_EDIT9, sample.nPan / 4u); //displayed panning with anything but XM is 0-64 so we divide by 4 + // FineTune / C-4 Speed / BaseNote + int transp = 0; + if (!m_sndFile.UseFinetuneAndTranspose()) + { + s = mpt::cfmt::val(sample.nC5Speed); + m_EditFineTune.SetWindowText(s); + if(sample.nC5Speed != 0) + transp = ModSample::FrequencyToTranspose(sample.nC5Speed).first; + } else + { + int ftune = ((int)sample.nFineTune); + // MOD finetune range -8 to 7 translates to -128 to 112 + if(m_sndFile.GetType() & MOD_TYPE_MOD) ftune >>= 4; + SetDlgItemInt(IDC_EDIT5, ftune); + transp = (int)sample.RelativeTone; + } + int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp; + Limit(basenote, BASENOTE_MIN, BASENOTE_MAX); + basenote -= BASENOTE_MIN; + if (basenote != m_CbnBaseNote.GetCurSel()) m_CbnBaseNote.SetCurSel(basenote); + + // Auto vibrato + // Ramp up and ramp down are swapped in XM - probably because they ramp up the *period* instead of *frequency*. + const VibratoType rampUp = m_sndFile.GetType() == MOD_TYPE_XM ? VIB_RAMP_DOWN : VIB_RAMP_UP; + const VibratoType rampDown = m_sndFile.GetType() == MOD_TYPE_XM ? VIB_RAMP_UP : VIB_RAMP_DOWN; + m_ComboAutoVib.ResetContent(); + m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Sine")), VIB_SINE); + m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Square")), VIB_SQUARE); + if(m_sndFile.GetType() != MOD_TYPE_IT || sample.nVibType == VIB_RAMP_UP) + m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Ramp Up")), rampUp); + m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Ramp Down")), rampDown); + if(m_sndFile.GetType() != MOD_TYPE_XM || sample.nVibType == VIB_RANDOM) + m_ComboAutoVib.SetItemData(m_ComboAutoVib.AddString(_T("Random")), VIB_RANDOM); + + for(int i = 0; i < m_ComboAutoVib.GetCount(); i++) + { + if(m_ComboAutoVib.GetItemData(i) == sample.nVibType) + { + m_ComboAutoVib.SetCurSel(i); + break; + } + } + + SetDlgItemInt(IDC_EDIT14, sample.nVibSweep); + SetDlgItemInt(IDC_EDIT15, sample.nVibDepth); + SetDlgItemInt(IDC_EDIT16, sample.nVibRate); + // Loop + d = 0; + if (sample.uFlags[CHN_LOOP]) d = sample.uFlags[CHN_PINGPONGLOOP] ? 2 : 1; + if (sample.uFlags[CHN_REVERSE]) d |= 4; + m_ComboLoopType.SetCurSel(d); + s = mpt::cfmt::val(sample.nLoopStart); + m_EditLoopStart.SetWindowText(s); + s = mpt::cfmt::val(sample.nLoopEnd); + m_EditLoopEnd.SetWindowText(s); + // Sustain Loop + d = 0; + if (sample.uFlags[CHN_SUSTAINLOOP]) d = sample.uFlags[CHN_PINGPONGSUSTAIN] ? 2 : 1; + m_ComboSustainType.SetCurSel(d); + s = mpt::cfmt::val(sample.nSustainStart); + m_EditSustainStart.SetWindowText(s); + s = mpt::cfmt::val(sample.nSustainEnd); + m_EditSustainEnd.SetWindowText(s); + + // Disable certain buttons for OPL instruments + BOOL b = isOPL ? FALSE : TRUE; + static constexpr int sampleButtons[] = + { + IDC_SAMPLE_NORMALIZE, IDC_SAMPLE_AMPLIFY, + IDC_SAMPLE_DCOFFSET, IDC_SAMPLE_STEREOSEPARATION, + IDC_SAMPLE_RESAMPLE, IDC_SAMPLE_REVERSE, + IDC_SAMPLE_SILENCE, IDC_SAMPLE_INVERT, + IDC_SAMPLE_SIGN_UNSIGN, IDC_SAMPLE_XFADE, + }; + for(auto btn : sampleButtons) + { + m_ToolBar2.EnableButton(btn, b); + } + m_ComboLoopType.EnableWindow(b); + m_SpinLoopStart.EnableWindow(b); + m_SpinLoopEnd.EnableWindow(b); + m_EditLoopStart.EnableWindow(b); + m_EditLoopEnd.EnableWindow(b); + + const bool hasSustainLoop = !isOPL && ((m_sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || (m_nSample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(m_nSample).uFlags[CHN_SUSTAINLOOP])); + b = hasSustainLoop ? TRUE : FALSE; + m_ComboSustainType.EnableWindow(b); + m_SpinSustainStart.EnableWindow(b); + m_SpinSustainEnd.EnableWindow(b); + m_EditSustainStart.EnableWindow(b); + m_EditSustainEnd.EnableWindow(b); + + } + if(hintType[HINT_MODTYPE | HINT_SAMPLEINFO | HINT_SMPNAMES]) + { + // Name + SetDlgItemText(IDC_SAMPLE_NAME, mpt::ToWin(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[m_nSample]).c_str()); + + CheckDlgButton(IDC_CHECK2, m_sndFile.GetSample(m_nSample).uFlags[SMP_KEEPONDISK] ? BST_CHECKED : BST_UNCHECKED); + GetDlgItem(IDC_CHECK2)->EnableWindow((m_sndFile.SampleHasPath(m_nSample) && m_sndFile.GetType() == MOD_TYPE_MPT) ? TRUE : FALSE); + } + + if (!m_bInitialized) + { + // First update + m_bInitialized = TRUE; + UnlockControls(); + } + + m_ComboLoopType.Invalidate(FALSE); + m_ComboSustainType.Invalidate(FALSE); + m_ComboAutoVib.Invalidate(FALSE); + + UnlockControls(); +} + + +// updateAll: Update all views including this one. Otherwise, only update update other views. +void CCtrlSamples::SetModified(SampleHint hint, bool updateAll, bool waveformModified) +{ + m_modDoc.SetModified(); + + if(waveformModified) + { + // Update on-disk sample status in tree + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED]) hint.Names(); + sample.uFlags.set(SMP_MODIFIED); + } + m_modDoc.UpdateAllViews(nullptr, hint.SetData(m_nSample), updateAll ? nullptr : this); +} + + +void CCtrlSamples::PrepareUndo(const char *description, sampleUndoTypes type, SmpLength start, SmpLength end) +{ + m_startedEdit = true; + m_modDoc.GetSampleUndo().PrepareUndo(m_nSample, type, description, start, end); +} + + +bool CCtrlSamples::OpenSample(const mpt::PathString &fileName, FlagSet<OpenSampleTypes> types) +{ + BeginWaitCursor(); + InputFile f(fileName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(!f.IsValid()) + { + EndWaitCursor(); + return false; + } + + FileReader file = GetFileReader(f); + if(!file.IsValid()) + { + EndWaitCursor(); + return false; + } + + PrepareUndo("Replace", sundo_replace); + const auto parentIns = GetParentInstrumentWithSameName(); + bool bOk = false; + if(types[OpenSampleKnown]) + { + bOk = m_sndFile.ReadSampleFromFile(m_nSample, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad); + + if(!bOk) + { + // Try loading as module + bOk = CMainFrame::GetMainFrame()->SetTreeSoundfile(file); + if(bOk) + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + return true; + } + } + } + ModSample &sample = m_sndFile.GetSample(m_nSample); + + if(!bOk && types[OpenSampleRaw]) + { + CRawSampleDlg dlg(file, this); + EndWaitCursor(); + if(m_rememberRawFormat || dlg.DoModal() == IDOK) + { + SampleIO sampleIO = dlg.GetSampleFormat(); + m_rememberRawFormat = m_rememberRawFormat || dlg.GetRemeberFormat(); + + BeginWaitCursor(); + + file.Seek(dlg.GetOffset()); + + m_sndFile.DestroySampleThreadsafe(m_nSample); + const auto bytesPerSample = sampleIO.GetNumChannels() * sampleIO.GetBitDepth() / 8u; + sample.nLength = mpt::saturate_cast<SmpLength>(file.BytesLeft() / bytesPerSample); + + if(TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad) + { + sampleIO.MayNormalize(); + } + + if(sampleIO.ReadSample(sample, file)) + { + bOk = true; + + sample.nGlobalVol = 64; + sample.nVolume = 256; + sample.nPan = 128; + sample.uFlags.reset(CHN_LOOP | CHN_SUSTAINLOOP | SMP_MODIFIED); + sample.filename = ""; + m_sndFile.m_szNames[m_nSample] = ""; + if(!sample.nC5Speed) sample.nC5Speed = 22050; + sample.PrecomputeLoops(m_sndFile, false); + } else + { + m_modDoc.GetSampleUndo().Undo(m_nSample); + } + + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + } else + { + m_sndFile.SetSamplePath(m_nSample, fileName); + } + + EndWaitCursor(); + if (bOk) + { + TrackerSettings::Instance().PathSamples.SetWorkingDir(fileName, true); + if(sample.filename.empty()) + { + mpt::PathString name, ext; + fileName.SplitPath(nullptr, nullptr, &name, &ext); + + if(m_sndFile.m_szNames[m_nSample].empty()) m_sndFile.m_szNames[m_nSample] = name.ToLocale(); + + if(name.AsNative().length() < 9) name += ext; + sample.filename = name.ToLocale(); + } + if ((m_sndFile.GetType() & MOD_TYPE_XM) && !sample.uFlags[CHN_PANNING]) + { + sample.nPan = 128; + sample.uFlags.set(CHN_PANNING); + } + SetModified(SampleHint().Info().Data().Names(), true, false); + sample.uFlags.reset(SMP_KEEPONDISK); + + if(parentIns <= m_sndFile.GetNumInstruments()) + { + if(auto instr = m_sndFile.Instruments[parentIns]; instr != nullptr) + { + m_modDoc.GetInstrumentUndo().PrepareUndo(parentIns, "Set Name"); + instr->name = m_sndFile.m_szNames[m_nSample]; + m_modDoc.UpdateAllViews(nullptr, InstrumentHint(parentIns).Names(), this); + } + } + } + return true; +} + + +bool CCtrlSamples::OpenSample(const CSoundFile &sndFile, SAMPLEINDEX nSample) +{ + if(!nSample || nSample > sndFile.GetNumSamples()) return false; + + BeginWaitCursor(); + + PrepareUndo("Replace", sundo_replace); + const auto parentIns = GetParentInstrumentWithSameName(); + if(m_sndFile.ReadSampleFromSong(m_nSample, sndFile, nSample)) + { + SetModified(SampleHint().Info().Data().Names(), true, false); + if(parentIns <= m_sndFile.GetNumInstruments()) + { + if(auto instr = m_sndFile.Instruments[parentIns]; instr != nullptr) + { + m_modDoc.GetInstrumentUndo().PrepareUndo(parentIns, "Set Name"); + instr->name = m_sndFile.m_szNames[m_nSample]; + m_modDoc.UpdateAllViews(nullptr, InstrumentHint(parentIns).Names(), this); + } + } + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + + EndWaitCursor(); + + return true; +} + + +////////////////////////////////////////////////////////////////////////////////// +// CCtrlSamples messages + +void CCtrlSamples::OnSampleChanged() +{ + if(!IsLocked()) + { + SAMPLEINDEX n = (SAMPLEINDEX)GetDlgItemInt(IDC_EDIT_SAMPLE); + if ((n > 0) && (n <= m_sndFile.GetNumSamples()) && (n != m_nSample)) + { + SetCurrentSample(n, -1, FALSE); + m_parent.SampleChanged(m_nSample); + } + } +} + + +void CCtrlSamples::OnZoomChanged() +{ + if (!IsLocked()) SetCurrentSample(m_nSample); + SwitchToView(); +} + + +void CCtrlSamples::OnTbnDropDownToolBar(NMHDR *pNMHDR, LRESULT *pResult) +{ + CInputHandler *ih = CMainFrame::GetInputHandler(); + NMTOOLBAR *pToolBar = reinterpret_cast<NMTOOLBAR *>(pNMHDR); + ClientToScreen(&(pToolBar->rcButton)); // TrackPopupMenu uses screen coords + const int offset = Util::ScalePixels(4, m_hWnd); // Compared to the main toolbar, the offset seems to be a bit wrong here...? + int x = pToolBar->rcButton.left + offset, y = pToolBar->rcButton.bottom + offset; + CMenu menu; + switch(pToolBar->iItem) + { + case IDC_SAMPLE_NEW: + { + menu.CreatePopupMenu(); + menu.AppendMenu(MF_STRING, IDC_SAMPLE_DUPLICATE, ih->GetKeyTextFromCommand(kcSampleDuplicate, m_sndFile.GetSample(m_nSample).uFlags[CHN_ADLIB] ? _T("&Duplicate Instrument") : _T("&Duplicate Sample"))); + menu.AppendMenu(MF_STRING | (m_sndFile.SupportsOPL() ? 0 : MF_DISABLED), IDC_SAMPLE_INITOPL, ih->GetKeyTextFromCommand(kcSampleInitializeOPL, _T("Initialize &OPL Instrument"))); + menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this); + menu.DestroyMenu(); + } + break; + case IDC_SAMPLE_OPEN: + { + menu.CreatePopupMenu(); + menu.AppendMenu(MF_STRING, IDC_SAMPLE_OPENKNOWN, ih->GetKeyTextFromCommand(kcSampleLoad, _T("Import &Sample..."))); + menu.AppendMenu(MF_STRING, IDC_SAMPLE_OPENRAW, ih->GetKeyTextFromCommand(kcSampleLoadRaw, _T("Import &Raw Sample..."))); + menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this); + menu.DestroyMenu(); + } + break; + case IDC_SAMPLE_SAVEAS: + { + menu.CreatePopupMenu(); + menu.AppendMenu(MF_STRING, IDC_SAVE_ALL, _T("Save &All...")); + menu.TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, x, y, this); + menu.DestroyMenu(); + } + break; + } + *pResult = 0; +} + + +void CCtrlSamples::OnSampleNew() +{ + InsertSample(CMainFrame::GetInputHandler()->ShiftPressed()); + SwitchToView(); +} + + +bool CCtrlSamples::InsertSample(bool duplicate, int8 *confirm) +{ + const SAMPLEINDEX smp = m_modDoc.InsertSample(); + if(smp != SAMPLEINDEX_INVALID) + { + const SAMPLEINDEX oldSmp = m_nSample; + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + SetCurrentSample(smp); + + if(duplicate && oldSmp >= 1 && oldSmp <= sndFile.GetNumSamples()) + { + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, "Duplicate"); + sndFile.ReadSampleFromSong(smp, sndFile, oldSmp); + } + + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info().Data().Names()); + if(m_modDoc.GetNumInstruments() > 0 && m_modDoc.FindSampleParent(smp) == INSTRUMENTINDEX_INVALID) + { + bool insertInstrument; + if(confirm == nullptr || *confirm == -1) + { + insertInstrument = Reporting::Confirm("This sample is not used by any instrument. Do you want to create a new instrument using this sample?") == cnfYes; + if(confirm != nullptr) *confirm = insertInstrument; + } else + { + insertInstrument = (*confirm) != 0; + } + if(insertInstrument) + { + INSTRUMENTINDEX nins = m_modDoc.InsertInstrument(smp); + m_modDoc.UpdateAllViews(nullptr, InstrumentHint(nins).Info().Envelope().Names()); + m_parent.InstrumentChanged(nins); + } + } + } + return (smp != SAMPLEINDEX_INVALID); +} + + +static constexpr std::pair<const mpt::uchar *, const mpt::uchar *> SampleFormats[] +{ + { UL_("Wave Files (*.wav)"), UL_("*.wav") }, +#ifdef MPT_WITH_FLAC + { UL_("FLAC Files (*.flac,*.oga)"), UL_("*.flac;*.oga") }, +#endif // MPT_WITH_FLAC +#if defined(MPT_WITH_OPUSFILE) + { UL_("Opus Files (*.opus,*.oga)"), UL_("*.opus;*.oga") }, +#endif // MPT_WITH_OPUSFILE +#if defined(MPT_WITH_VORBISFILE) || defined(MPT_WITH_STBVORBIS) + { UL_("Ogg Vorbis Files (*.ogg,*.oga)"), UL_("*.ogg;*.oga") }, +#endif // VORBIS +#if defined(MPT_ENABLE_MP3_SAMPLES) + { UL_("MPEG Files (*.mp1,*.mp2,*.mp3)"), UL_("*.mp1;*.mp2;*.mp3") }, +#endif // MPT_ENABLE_MP3_SAMPLES + { UL_("XI Samples (*.xi)"), UL_("*.xi") }, + { UL_("Impulse Tracker Samples (*.its)"), UL_("*.its") }, + { UL_("Scream Tracker Samples (*.s3i,*.smp)"), UL_("*.s3i;*.smp") }, + { UL_("OPL Instruments (*.sb0,*.sb2,*.sbi)"), UL_("*.sb0;*.sb2;*.sbi") }, + { UL_("GF1 Patches (*.pat)"), UL_("*.pat") }, + { UL_("Wave64 Files (*.w64)"), UL_("*.w64") }, + { UL_("CAF Files (*.wav)"), UL_("*.caf") }, + { UL_("AIFF Files (*.aiff,*.8svx)"), UL_("*.aif;*.aiff;*.iff;*.8sv;*.8svx;*.svx") }, + { UL_("Sun Audio (*.au,*.snd)"), UL_("*.au;*.snd") }, + { UL_("SNES BRR Files (*.brr)"), UL_("*.brr") }, +}; + + +static mpt::ustring ConstructFileFilter(bool includeRaw) +{ + mpt::ustring s = U_("All Samples (*.wav,*.flac,*.xi,*.its,*.s3i,*.sbi,...)|"); + bool first = true; + for(const auto &[name, exts] : SampleFormats) + { + if(!first) + s += U_(";"); + else + first = false; + s += exts; + } +#if defined(MPT_WITH_MEDIAFOUNDATION) + std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes(); + s += ToFilterOnlyString(mediaFoundationTypes, true).ToUnicode(); +#endif + if(includeRaw) + { + s += U_(";*.raw;*.snd;*.pcm;*.sam"); + } + s += U_("|"); + for(const auto &[name, exts] : SampleFormats) + { + s += name + U_("|"); + s += exts + U_("|"); + } +#if defined(MPT_WITH_MEDIAFOUNDATION) + s += ToFilterString(mediaFoundationTypes, FileTypeFormatShowExtensions).ToUnicode(); +#endif + if(includeRaw) + { + s += U_("Raw Samples (*.raw,*.snd,*.pcm,*.sam)|*.raw;*.snd;*.pcm;*.sam|"); + } + s += U_("All Files (*.*)|*.*||"); + return s; +} + + +void CCtrlSamples::OnSampleOpen() +{ + static int nLastIndex = 0; + std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes(); + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .EnableAudioPreview() + .ExtensionFilter(ConstructFileFilter(true)) + .WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir()) + .FilterIndex(&nLastIndex); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + + OpenSamples(dlg.GetFilenames(), OpenSampleKnown | OpenSampleRaw); + SwitchToView(); +} + + +void CCtrlSamples::OnSampleOpenKnown() +{ + static int nLastIndex = 0; + std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes(); + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .EnableAudioPreview() + .ExtensionFilter(ConstructFileFilter(false)) + .WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir()) + .FilterIndex(&nLastIndex); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + + OpenSamples(dlg.GetFilenames(), OpenSampleKnown); +} + + +void CCtrlSamples::OnSampleOpenRaw() +{ + static int nLastIndex = 0; + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .EnableAudioPreview() + .ExtensionFilter("Raw Samples (*.raw,*.snd,*.pcm,*.sam)|*.raw;*.snd;*.pcm;*.sam|" + "All Files (*.*)|*.*||") + .WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir()) + .FilterIndex(&nLastIndex); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + + OpenSamples(dlg.GetFilenames(), OpenSampleRaw); +} + + +void CCtrlSamples::OpenSamples(const std::vector<mpt::PathString> &files, FlagSet<OpenSampleTypes> types) +{ + int8 confirm = -1; + bool first = true; + for(const auto &file : files) + { + // If loading multiple samples, create new slots for them + if(!first) + { + if(!InsertSample(false, &confirm)) + break; + } + + if(OpenSample(file, types)) + first = false; + else + ErrorBox(IDS_ERR_FILEOPEN, this); + } + SwitchToView(); +} + + +void CCtrlSamples::OnSampleSave() +{ + SaveSample(CMainFrame::GetInputHandler()->ShiftPressed()); +} + + +void CCtrlSamples::SaveSample(bool doBatchSave) +{ + mpt::PathString fileName, defaultPath = TrackerSettings::Instance().PathSamples.GetWorkingDir(); + SampleEditorDefaultFormat defaultFormat = TrackerSettings::Instance().m_defaultSampleFormat; + bool hasAdlib = false; + + if(!doBatchSave) + { + // Save this sample + const ModSample &sample = m_sndFile.GetSample(m_nSample); + if((!m_nSample) || (!sample.HasSampleData())) + { + SwitchToView(); + return; + } + if(m_sndFile.SampleHasPath(m_nSample)) + { + // For on-disk samples, propose their original filename and location + auto path = m_sndFile.GetSamplePath(m_nSample); + fileName = path.GetFullFileName(); + defaultPath = path.GetPath(); + } + if(fileName.empty()) fileName = mpt::PathString::FromLocale(sample.filename); + if(fileName.empty()) fileName = mpt::PathString::FromLocale(m_sndFile.m_szNames[m_nSample]); + if(fileName.empty()) fileName = P_("untitled"); + + const mpt::PathString ext = fileName.GetFileExt(); + if(!mpt::PathString::CompareNoCase(ext, P_(".flac"))) defaultFormat = dfFLAC; + else if(!mpt::PathString::CompareNoCase(ext, P_(".wav"))) defaultFormat = dfWAV; + else if(!mpt::PathString::CompareNoCase(ext, P_(".s3i"))) defaultFormat = dfS3I; + + hasAdlib = sample.uFlags[CHN_ADLIB]; + } else + { + // Save all samples + fileName = m_sndFile.GetpModDoc()->GetPathNameMpt().GetFileName(); + if(fileName.empty()) fileName = P_("untitled"); + + fileName += P_(" - %sample_number% - "); + if(m_sndFile.GetModSpecifications().sampleFilenameLengthMax == 0) + fileName += P_("%sample_name%"); + else + fileName += P_("%sample_filename%"); + } + SanitizeFilename(fileName); + + int filter; + switch(defaultFormat) + { + case dfWAV: + filter = 1; + break; + case dfFLAC: + default: + filter = 2; + break; + case dfS3I: + filter = 3; + break; + case dfRAW: + filter = 4; + break; + } + // Do we have to use a format that can save OPL instruments? + if(hasAdlib) + filter = 3; + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(ToSettingValue(defaultFormat).as<mpt::ustring>()) + .DefaultFilename(fileName) + .ExtensionFilter("Wave File (*.wav)|*.wav|" + "FLAC File (*.flac)|*.flac|" + "S3I Scream Tracker 3 Instrument (*.s3i)|*.s3i|" + "RAW Audio (*.raw)|*.raw||") + .WorkingDirectory(defaultPath) + .FilterIndex(&filter); + if(!dlg.Show(this)) return; + + BeginWaitCursor(); + + const auto saveFormat = FromSettingValue<SampleEditorDefaultFormat>(dlg.GetExtension().ToUnicode()); + + SAMPLEINDEX minSmp = m_nSample, maxSmp = m_nSample; + if(doBatchSave) + { + minSmp = 1; + maxSmp = m_sndFile.GetNumSamples(); + } + const auto numberFmt = mpt::FormatSpec().Dec().FillNul().Width(1 + static_cast<int>(std::log10(maxSmp))); + + bool ok = false; + CString sampleName, sampleFilename; + + for(SAMPLEINDEX smp = minSmp; smp <= maxSmp; smp++) + { + ModSample &sample = m_sndFile.GetSample(smp); + if(sample.HasSampleData()) + { + const bool isAdlib = sample.uFlags[CHN_ADLIB]; + + fileName = dlg.GetFirstFile(); + if(doBatchSave) + { + sampleName = mpt::ToCString(m_sndFile.GetCharsetInternal(), (!m_sndFile.m_szNames[smp].empty()) ? std::string(m_sndFile.m_szNames[smp]) : "untitled"); + sampleFilename = mpt::ToCString(m_sndFile.GetCharsetInternal(), (!sample.filename.empty()) ? sample.GetFilename() : m_sndFile.m_szNames[smp]); + SanitizeFilename(sampleName); + SanitizeFilename(sampleFilename); + + mpt::ustring fileNameU = fileName.ToUnicode(); + fileNameU = mpt::String::Replace(fileNameU, U_("%sample_number%"), mpt::ufmt::fmt(smp, numberFmt)); + fileNameU = mpt::String::Replace(fileNameU, U_("%sample_filename%"), mpt::ToUnicode(sampleFilename)); + fileNameU = mpt::String::Replace(fileNameU, U_("%sample_name%"), mpt::ToUnicode(sampleName)); + fileName = mpt::PathString::FromUnicode(fileNameU); + + // Need to enforce S3I for Adlib samples + if(isAdlib && saveFormat != dfS3I) + fileName = fileName.ReplaceExt(P_(".s3i")); + } + + try + { + mpt::SafeOutputFile sf(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + if(!f) + { + ok = false; + continue; + } + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + + // Need to enforce S3I for Adlib samples + const auto thisFormat = isAdlib ? dfS3I : saveFormat; + if(thisFormat == dfRAW) + ok = m_sndFile.SaveRAWSample(smp, f); + else if(thisFormat == dfFLAC) + ok = m_sndFile.SaveFLACSample(smp, f); + else if(thisFormat == dfS3I) + ok = m_sndFile.SaveS3ISample(smp, f); + else + ok = m_sndFile.SaveWAVSample(smp, f); + } catch(const std::exception &) + { + ok = false; + } + + if(ok) + { + m_sndFile.SetSamplePath(smp, fileName); + sample.uFlags.reset(SMP_MODIFIED); + UpdateView(SampleHint().Info()); + + // Check if any other samples refer to the same file - that would be dangerous. + if(sample.uFlags[SMP_KEEPONDISK]) + { + for(SAMPLEINDEX i = 1; i <= m_sndFile.GetNumSamples(); i++) + { + if(i != smp && m_sndFile.GetSample(i).uFlags[SMP_KEEPONDISK] && m_sndFile.GetSamplePath(i) == m_sndFile.GetSamplePath(smp)) + { + m_sndFile.GetSample(i).uFlags.reset(SMP_KEEPONDISK); + m_modDoc.UpdateAllViews(nullptr, SampleHint(i).Names().Info(), this); + } + } + } + } + } + } + EndWaitCursor(); + + if (!ok) + { + ErrorBox(IDS_ERR_SAVESMP, this); + } else + { + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + } + SwitchToView(); +} + + +void CCtrlSamples::OnSamplePlay() +{ + if (m_modDoc.IsNotePlaying(NOTE_NONE, m_nSample, 0)) + { + m_modDoc.NoteOff(0, true); + } else + { + m_modDoc.PlayNote(PlayNoteParam(NOTE_MIDDLEC).Sample(m_nSample)); + } + SwitchToView(); +} + + +template<typename T> +static bool DoNormalize(T *p, SmpLength selStart, SmpLength selEnd) +{ + auto [min, max] = CViewSample::FindMinMax(p + selStart, selEnd - selStart, 1); + max = std::max(-min, max); + if(max < std::numeric_limits<T>::max()) + { + max++; + for(SmpLength i = selStart; i < selEnd; i++) + { + p[i] = static_cast<T>((static_cast<int>(p[i]) << (sizeof(T) * 8 - 1)) / max); + } + return true; + } + return false; +} + + +void CCtrlSamples::Normalize(bool allSamples) +{ + //Default case: Normalize current sample + SAMPLEINDEX minSample = m_nSample, maxSample = m_nSample; + //If only one sample is selected, parts of it may be amplified + SmpLength selStart = 0, selEnd = 0; + + if(allSamples) + { + if(Reporting::Confirm(_T("This will normalize all samples independently. Continue?"), _T("Normalize")) == cnfNo) + return; + minSample = 1; + maxSample = m_sndFile.GetNumSamples(); + } else + { + SampleSelectionPoints selection = GetSelectionPoints(); + selStart = selection.nStart; + selEnd = selection.nEnd; + } + + + BeginWaitCursor(); + bool modified = false; + + for(SAMPLEINDEX smp = minSample; smp <= maxSample; smp++) + { + if(m_sndFile.GetSample(smp).HasSampleData()) + { + ModSample &sample = m_sndFile.GetSample(smp); + + if(minSample != maxSample) + { + // If more than one sample is selected, always amplify the whole sample. + selStart = 0; + selEnd = sample.nLength; + } else + { + // One sample: correct the boundaries, if needed + LimitMax(selEnd, sample.nLength); + LimitMax(selStart, selEnd); + if(selStart == selEnd) + { + selStart = 0; + selEnd = sample.nLength; + } + } + + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_update, "Normalize", selStart, selEnd); + + selStart *= sample.GetNumChannels(); + selEnd *= sample.GetNumChannels(); + + if(sample.uFlags[CHN_16BIT]) + { + modified |= DoNormalize(sample.sample16(), selStart, selEnd); + } else + { + modified |= DoNormalize(sample.sample8(), selStart, selEnd); + } + + if(modified) + { + sample.PrecomputeLoops(m_sndFile, false); + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Data()); + } + } + } + + if(modified) + { + SetModified(SampleHint().Data(), false, true); + } + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnNormalize() +{ + Normalize(CMainFrame::GetInputHandler()->ShiftPressed()); +} + + +void CCtrlSamples::RemoveDCOffset(bool allSamples) +{ + SAMPLEINDEX minSample = m_nSample, maxSample = m_nSample; + + //Shift -> Process all samples + if(allSamples) + { + if(Reporting::Confirm(_T("This will process all samples independently. Continue?"), _T("DC Offset Removal")) == cnfNo) + return; + minSample = 1; + maxSample = m_sndFile.GetNumSamples(); + } + + BeginWaitCursor(); + + // for report / SetModified + SAMPLEINDEX numModified = 0; + double reportOffset = 0; + + for(SAMPLEINDEX smp = minSample; smp <= maxSample; smp++) + { + SmpLength selStart, selEnd; + + if(!m_sndFile.GetSample(smp).HasSampleData()) + continue; + + if (minSample != maxSample) + { + selStart = 0; + selEnd = m_sndFile.GetSample(smp).nLength; + } else + { + SampleSelectionPoints selection = GetSelectionPoints(); + selStart = selection.nStart; + selEnd = selection.nEnd; + } + + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_update, "Remove DC Offset", selStart, selEnd); + + const double offset = SampleEdit::RemoveDCOffset(m_sndFile.GetSample(smp), selStart, selEnd, m_sndFile); + + if(offset == 0.0f) // No offset removed. + continue; + + reportOffset += offset; + numModified++; + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info().Data()); + } + + EndWaitCursor(); + SwitchToView(); + + // fill the statusbar with some nice information + + CString dcInfo; + if(numModified) + { + SetModified(SampleHint().Info().Data(), true, true); + if(numModified == 1) + { + dcInfo.Format(_T("Removed DC offset (%.1f%%)"), reportOffset * 100); + } else + { + dcInfo.Format(_T("Removed DC offset from %u samples (avg %0.1f%%)"), numModified, reportOffset / numModified * 100); + } + } else + { + dcInfo.SetString(_T("No DC offset found")); + } + + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + pMainFrm->SetXInfoText(dcInfo); + +} + + +void CCtrlSamples::OnRemoveDCOffset() +{ + RemoveDCOffset(CMainFrame::GetInputHandler()->ShiftPressed()); +} + + +void CCtrlSamples::ApplyAmplify(const double amp, const double fadeInStart, const double fadeOutEnd, const bool fadeIn, const bool fadeOut, const Fade::Law fadeLaw) +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) return; + + BeginWaitCursor(); + + SampleSelectionPoints selection = GetSelectionPoints(); + const auto start = selection.nStart, end = selection.nEnd, mid = (start + end) / 2; + + PrepareUndo("Amplify", sundo_update, start, end); + + if(fadeIn && fadeOut) + { + SampleEdit::AmplifySample(sample, start, mid, fadeInStart, amp, true, fadeLaw, m_sndFile); + SampleEdit::AmplifySample(sample, mid, end, amp, fadeOutEnd, false, fadeLaw, m_sndFile); + } else if(fadeIn) + { + SampleEdit::AmplifySample(sample, start, end, fadeInStart, amp, true, fadeLaw, m_sndFile); + } else if(fadeOut) + { + SampleEdit::AmplifySample(sample, start, end, amp, fadeOutEnd, false, fadeLaw, m_sndFile); + } else + { + SampleEdit::AmplifySample(sample, start, end, amp, amp, true, Fade::kLinear, m_sndFile); + } + + sample.PrecomputeLoops(m_sndFile, false); + SetModified(SampleHint().Data(), false, true); + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnAmplify() +{ + static CAmpDlg::AmpSettings settings { Fade::kLinear, 0, 0, 100, false, false }; + + CAmpDlg dlg(this, settings); + if (dlg.DoModal() != IDOK) return; + + ApplyAmplify(settings.factor / 100.0, settings.fadeInStart / 100.0, settings.fadeOutEnd / 100.0, settings.fadeIn, settings.fadeOut, settings.fadeLaw); +} + + +// Quickly fade the selection in/out without asking the user. +// Fade-In is applied if the selection starts at the beginning of the sample. +// Fade-Out is applied if the selection ends and the end of the sample. +void CCtrlSamples::OnQuickFade() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) return; + + SampleSelectionPoints sel = GetSelectionPoints(); + if(sel.selectionActive && (sel.nStart == 0 || sel.nEnd == sample.nLength)) + { + ApplyAmplify(1.0, (sel.nStart == 0) ? 0.0 : 1.0, (sel.nEnd == sample.nLength) ? 0.0 : 1.0, sel.nStart == 0, sel.nEnd == sample.nLength, Fade::kLinear); + } else + { + // Can't apply quick fade as no appropriate selection has been made, so ask the user to amplify the whole sample instead. + OnAmplify(); + } +} + + +void CCtrlSamples::OnResample() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + return; + + SAMPLEINDEX first = m_nSample, last = m_nSample; + if(CMainFrame::GetInputHandler()->ShiftPressed()) + { + first = 1; + last = m_sndFile.GetNumSamples(); + } + + const uint32 oldRate = sample.GetSampleRate(m_sndFile.GetType()); + CResamplingDlg dlg(this, oldRate, TrackerSettings::Instance().sampleEditorDefaultResampler, first != last); + if(dlg.DoModal() != IDOK) + return; + + TrackerSettings::Instance().sampleEditorDefaultResampler = dlg.GetFilter(); + for(SAMPLEINDEX smp = first; smp <= last; smp++) + { + const uint32 sampleFreq = m_sndFile.GetSample(smp).GetSampleRate(m_sndFile.GetType()); + uint32 newFreq = dlg.GetFrequency(); + if(dlg.GetResamplingOption() == CResamplingDlg::Upsample) + newFreq = sampleFreq * 2; + else if(dlg.GetResamplingOption() == CResamplingDlg::Downsample) + newFreq = sampleFreq / 2; + else if(newFreq == sampleFreq) + continue; + ApplyResample(smp, newFreq, dlg.GetFilter(), first != last, dlg.UpdatePatternCommands()); + } +} + + +void CCtrlSamples::ApplyResample(SAMPLEINDEX smp, uint32 newRate, ResamplingMode mode, bool ignoreSelection, bool updatePatternCommands) +{ + BeginWaitCursor(); + + ModSample &sample = m_sndFile.GetSample(smp); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + { + EndWaitCursor(); + return; + } + + SampleSelectionPoints selection = GetSelectionPoints(); + LimitMax(selection.nEnd, sample.nLength); + if(selection.nStart >= selection.nEnd || ignoreSelection) + { + selection.nStart = 0; + selection.nEnd = sample.nLength; + } + + const uint32 oldRate = sample.GetSampleRate(m_sndFile.GetType()); + if(newRate < 1 || oldRate < 1) + { + MessageBeep(MB_ICONWARNING); + EndWaitCursor(); + return; + } + const SmpLength oldLength = sample.nLength; + const SmpLength selLength = (selection.nEnd - selection.nStart); + const SmpLength newSelLength = Util::muldivr_unsigned(selLength, newRate, oldRate); + const SmpLength newSelEnd = selection.nStart + newSelLength; + const SmpLength newTotalLength = sample.nLength - selLength + newSelLength; + const uint8 numChannels = sample.GetNumChannels(); + + if(newTotalLength <= 1) + { + MessageBeep(MB_ICONWARNING); + EndWaitCursor(); + return; + } + + void *newSample = ModSample::AllocateSample(newTotalLength, sample.GetBytesPerSample()); + + if(newSample != nullptr) + { + // First, copy parts of the sample that are not affected by partial upsampling + const SmpLength bps = sample.GetBytesPerSample(); + std::memcpy(newSample, sample.sampleb(), selection.nStart * bps); + std::memcpy(static_cast<char *>(newSample) + newSelEnd * bps, sample.sampleb() + selection.nEnd * bps, (sample.nLength - selection.nEnd) * bps); + + if(mode == SRCMODE_DEFAULT) + { + // Resample using r8brain + const SmpLength bufferSize = std::min(std::max(selLength, SmpLength(oldRate)), SmpLength(1024 * 1024)); + std::vector<double> convBuffer(bufferSize); + r8b::CDSPResampler16 resampler(oldRate, newRate, bufferSize); + + for(uint8 chn = 0; chn < numChannels; chn++) + { + if(chn != 0) resampler.clear(); + + SmpLength readCount = selLength, writeCount = newSelLength; + SmpLength readOffset = selection.nStart * numChannels + chn, writeOffset = readOffset; + SmpLength outLatency = newRate; + double *outBuffer, lastVal = 0.0; + + { + // Pre-fill the resampler with the first sampling point. + // Otherwise, it will assume that all samples before the first sampling point are 0, + // which can lead to unwanted artefacts (ripples) if the sample doesn't start with a zero crossing. + double firstVal = 0.0; + switch(sample.GetElementarySampleSize()) + { + case 1: + firstVal = SC::Convert<double, int8>()(sample.sample8()[readOffset]); + lastVal = SC::Convert<double, int8>()(sample.sample8()[readOffset + selLength - numChannels]); + break; + case 2: + firstVal = SC::Convert<double, int16>()(sample.sample16()[readOffset]); + lastVal = SC::Convert<double, int16>()(sample.sample16()[readOffset + selLength - numChannels]); + break; + default: + // When higher bit depth is added, feel free to also replace CDSPResampler16 by CDSPResampler24 above. + MPT_ASSERT_MSG(false, "Bit depth not implemented"); + } + + // 10ms or less would probably be enough, but we will pre-fill the buffer with exactly "oldRate" samples + // to prevent any further rounding errors when using smaller buffers or when dividing oldRate or newRate. + uint32 remain = oldRate; + for(SmpLength i = 0; i < bufferSize; i++) convBuffer[i] = firstVal; + while(remain > 0) + { + uint32 procIn = std::min(remain, mpt::saturate_cast<uint32>(bufferSize)); + SmpLength procCount = resampler.process(convBuffer.data(), procIn, outBuffer); + MPT_ASSERT(procCount <= outLatency); + LimitMax(procCount, outLatency); + outLatency -= procCount; + remain -= procIn; + } + } + + // Now we can start with the actual resampling work... + while(writeCount > 0) + { + SmpLength smpCount = (SmpLength)convBuffer.size(); + if(readCount != 0) + { + LimitMax(smpCount, readCount); + + switch(sample.GetElementarySampleSize()) + { + case 1: + CopySample<SC::ConversionChain<SC::Convert<double, int8>, SC::DecodeIdentity<int8> > >(convBuffer.data(), smpCount, 1, sample.sample8() + readOffset, sample.GetSampleSizeInBytes(), sample.GetNumChannels()); + break; + case 2: + CopySample<SC::ConversionChain<SC::Convert<double, int16>, SC::DecodeIdentity<int16> > >(convBuffer.data(), smpCount, 1, sample.sample16() + readOffset, sample.GetSampleSizeInBytes(), sample.GetNumChannels()); + break; + } + readOffset += smpCount * numChannels; + readCount -= smpCount; + } else + { + // Nothing to read, but still to write (compensate for r8brain's output latency) + for(SmpLength i = 0; i < smpCount; i++) convBuffer[i] = lastVal; + } + + SmpLength procCount = resampler.process(convBuffer.data(), smpCount, outBuffer); + const SmpLength procLatency = std::min(outLatency, procCount); + procCount = std::min(procCount- procLatency, writeCount); + + switch(sample.GetElementarySampleSize()) + { + case 1: + CopySample<SC::ConversionChain<SC::Convert<int8, double>, SC::DecodeIdentity<double> > >(static_cast<int8 *>(newSample) + writeOffset, procCount, sample.GetNumChannels(), outBuffer + procLatency, procCount * sizeof(double), 1); + break; + case 2: + CopySample<SC::ConversionChain<SC::Convert<int16, double>, SC::DecodeIdentity<double> > >(static_cast<int16 *>(newSample) + writeOffset, procCount, sample.GetNumChannels(), outBuffer + procLatency, procCount * sizeof(double), 1); + break; + } + writeOffset += procCount * numChannels; + writeCount -= procCount; + outLatency -= procLatency; + } + } + } else + { + // Resample using built-in filters + uint32 functionNdx = MixFuncTable::ResamplingModeToMixFlags(mode); + if(sample.uFlags[CHN_16BIT]) functionNdx |= MixFuncTable::ndx16Bit; + if(sample.uFlags[CHN_STEREO]) functionNdx |= MixFuncTable::ndxStereo; + ModChannel chn{}; + chn.pCurrentSample = sample.samplev(); + chn.increment = SamplePosition::Ratio(oldRate, newRate); + chn.position.Set(selection.nStart); + chn.leftVol = chn.rightVol = (1 << 8); + chn.nLength = sample.nLength; + + SmpLength writeCount = newSelLength; + SmpLength writeOffset = selection.nStart * sample.GetNumChannels(); + while(writeCount > 0) + { + SmpLength procCount = std::min(static_cast<SmpLength>(MIXBUFFERSIZE), writeCount); + mixsample_t buffer[MIXBUFFERSIZE * 2]; + MemsetZero(buffer); + MixFuncTable::Functions[functionNdx](chn, m_sndFile.m_Resampler, buffer, procCount); + + for(uint8 c = 0; c < numChannels; c++) + { + switch(sample.GetElementarySampleSize()) + { + case 1: + CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int8, mixsample_t, 23>, SC::DecodeIdentity<mixsample_t> > >(static_cast<int8 *>(newSample) + writeOffset + c, procCount, sample.GetNumChannels(), buffer + c, sizeof(buffer), 2); + break; + case 2: + CopySample<SC::ConversionChain<SC::ConvertFixedPoint<int16, mixsample_t, 23>, SC::DecodeIdentity<mixsample_t> > >(static_cast<int16 *>(newSample) + writeOffset + c, procCount, sample.GetNumChannels(), buffer + c, sizeof(buffer), 2); + break; + } + } + + writeCount -= procCount; + writeOffset += procCount * sample.GetNumChannels(); + } + } + + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, (newRate > oldRate) ? "Upsample" : "Downsample"); + + // Adjust loops and cues + const auto oldCues = sample.cues; + for(SmpLength &point : SampleEdit::GetCuesAndLoops(sample)) + { + if(point >= oldLength) + point = newTotalLength; + else if(point >= selection.nEnd) + point += newSelLength - selLength; + else if(point > selection.nStart) + point = selection.nStart + Util::muldivr_unsigned(point - selection.nStart, newRate, oldRate); + LimitMax(point, newTotalLength); + } + + if(updatePatternCommands) + { + bool patternUndoCreated = false; + m_sndFile.Patterns.ForEachModCommand([&](ModCommand &m) + { + if(m.command != CMD_OFFSET && m.command != CMD_REVERSEOFFSET && m.command != CMD_OFFSETPERCENTAGE) + return; + if(m_sndFile.GetSampleIndex(m.note, m.instr) != smp) + return; + SmpLength point = m.param * 256u; + + if(m.command == CMD_OFFSETPERCENTAGE || (m.volcmd == VOLCMD_OFFSET && m.vol == 0)) + point = Util::muldivr_unsigned(point, oldLength, 65536); + else if(m.volcmd == VOLCMD_OFFSET && m.vol <= std::size(oldCues)) + point += oldCues[m.vol - 1]; + + if(point >= oldLength) + point = newTotalLength; + else if (point >= selection.nEnd) + point += newSelLength - selLength; + else if (point > selection.nStart) + point = selection.nStart + Util::muldivr_unsigned(point - selection.nStart, newRate, oldRate); + LimitMax(point, newTotalLength); + + if(m.command == CMD_OFFSETPERCENTAGE || (m.volcmd == VOLCMD_OFFSET && m.vol == 0)) + point = Util::muldivr_unsigned(point, 65536, newTotalLength); + else if(m.volcmd == VOLCMD_OFFSET && m.vol <= std::size(sample.cues)) + point -= sample.cues[m.vol - 1]; + if(!patternUndoCreated) + { + patternUndoCreated = true; + m_modDoc.PrepareUndoForAllPatterns(false, "Resample (Adjust Offsets)"); + } + m.param = mpt::saturate_cast<ModCommand::PARAM>(point / 256u); + }); + } + + if(!selection.selectionActive) + { + if(m_sndFile.GetType() != MOD_TYPE_MOD) + { + sample.nC5Speed = newRate; + sample.FrequencyToTranspose(); + } + } + + ctrlSmp::ReplaceSample(sample, newSample, newTotalLength, m_sndFile); + // Update loop wrap-around buffer + sample.PrecomputeLoops(m_sndFile); + + auto updateHint = SampleHint(smp).Info().Data(); + if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED]) + updateHint.Names(); + sample.uFlags.set(SMP_MODIFIED); + m_modDoc.SetModified(); + m_modDoc.UpdateAllViews(nullptr, updateHint, nullptr); + + if(selection.selectionActive && !ignoreSelection) + { + SetSelectionPoints(selection.nStart, newSelEnd); + } + } + + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::ReadTimeStretchParameters() +{ + m_nSequenceMs = GetDlgItemInt(IDC_EDIT10); + m_nSeekWindowMs = GetDlgItemInt(IDC_EDIT11); + m_nOverlapMs = GetDlgItemInt(IDC_EDIT12); +} + + +void CCtrlSamples::UpdateTimeStretchParameters() +{ + GetDlgItem(IDC_EDIT10)->SetWindowText(((m_nSequenceMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nSequenceMs)).c_str()); + GetDlgItem(IDC_EDIT11)->SetWindowText(((m_nSeekWindowMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nSeekWindowMs)).c_str()); + GetDlgItem(IDC_EDIT12)->SetWindowText(((m_nOverlapMs <= 0) ? _T("auto") : MPT_TFORMAT("{}ms")(m_nOverlapMs)).c_str()); +} + +void CCtrlSamples::OnEnableStretchToSize() +{ + // Enable time-stretching / disable unused pitch-shifting UI elements + bool timeStretch = IsDlgButtonChecked(IDC_CHECK3) != BST_UNCHECKED; + if(!timeStretch) ReadTimeStretchParameters(); + ((CComboBox *)GetDlgItem(IDC_COMBO4))->EnableWindow(timeStretch ? FALSE : TRUE); + ((CEdit *)GetDlgItem(IDC_EDIT6))->EnableWindow(timeStretch ? TRUE : FALSE); + ((CButton *)GetDlgItem(IDC_BUTTON2))->EnableWindow(timeStretch ? TRUE : FALSE); + + GetDlgItem(IDC_TEXT_PITCH)->SetWindowText(timeStretch ? _T("Sequence") : _T("Pitch")); + GetDlgItem(IDC_TEXT_QUALITY)->SetWindowText(timeStretch ? _T("Seek Window") : _T("Quality")); + GetDlgItem(IDC_TEXT_FFT)->SetWindowText(timeStretch ? _T("Overlap") : _T("FFT Size")); + + GetDlgItem(IDC_EDIT10)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + GetDlgItem(IDC_EDIT11)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + GetDlgItem(IDC_EDIT12)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + GetDlgItem(IDC_SPIN10)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + GetDlgItem(IDC_SPIN14)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + GetDlgItem(IDC_SPIN15)->ShowWindow(timeStretch ? SW_SHOW : SW_HIDE); + + GetDlgItem(IDC_COMBO4)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW); + GetDlgItem(IDC_COMBO5)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW); + GetDlgItem(IDC_COMBO6)->ShowWindow(timeStretch ? SW_HIDE : SW_SHOW); + + SetDlgItemText(IDC_BUTTON1, timeStretch ? _T("Time Stretch") : _T("Pitch Shift")); + if(timeStretch) + UpdateTimeStretchParameters(); +} + +void CCtrlSamples::OnEstimateSampleSize() +{ + if(!m_sndFile.GetSample(m_nSample).HasSampleData()) + return; + + //Ensure m_dTimeStretchRatio is up-to-date with textbox content + UpdateData(TRUE); + + //Open dialog + CPSRatioCalc dlg(m_sndFile, m_nSample, m_dTimeStretchRatio, this); + if (dlg.DoModal() != IDOK) return; + + //Update ratio value&textbox + m_dTimeStretchRatio = dlg.m_dRatio; + UpdateData(FALSE); +} + + +enum TimeStretchPitchShiftResult +{ + kUnknown, + kOK, + kAbort, + kInvalidRatio, + kStretchTooShort, + kStretchTooLong, + kOutOfMemory, + kSampleTooShort, + kStretchInvalidSampleRate, +}; + +class DoPitchShiftTimeStretch : public CProgressDialog +{ +public: + CCtrlSamples &m_parent; + CModDoc &m_modDoc; + const float m_ratio; + TimeStretchPitchShiftResult m_result = kUnknown; + uint32 m_updateInterval; + const SAMPLEINDEX m_sample; + const bool m_pitchShift; + + DoPitchShiftTimeStretch(CCtrlSamples &parent, CModDoc &modDoc, SAMPLEINDEX sample, float ratio, bool pitchShift) + : CProgressDialog(&parent) + , m_parent(parent) + , m_modDoc(modDoc) + , m_ratio(ratio) + , m_sample(sample) + , m_pitchShift(pitchShift) + { + m_updateInterval = TrackerSettings::Instance().GUIUpdateInterval; + if(m_updateInterval < 15) m_updateInterval = 15; + } + + void Run() override + { + SetTitle(m_pitchShift ? _T("Pitch Shift") : _T("Time Stretch")); + SetRange(0, 100); + if(m_pitchShift) + m_result = PitchShift(); + else + m_result = TimeStretch(); + EndDialog((m_result == kOK) ? IDOK : IDCANCEL); + } + + TimeStretchPitchShiftResult TimeStretch() + { + ModSample &sample = m_modDoc.GetSoundFile().GetSample(m_sample); + const uint32 sampleRate = sample.GetSampleRate(m_modDoc.GetModType()); + + if(!sample.HasSampleData()) return kAbort; + + if(m_ratio == 1.0) return kAbort; + if(m_ratio < 0.5f) return kStretchTooShort; + if(m_ratio > 2.0f) return kStretchTooLong; + if(sampleRate > 192000) return kStretchInvalidSampleRate; + + HANDLE handleSt = soundtouch_createInstance(); + if(handleSt == NULL) + { + mpt::throw_out_of_memory(); + } + + const uint8 smpSize = sample.GetElementarySampleSize(); + const uint8 numChannels = sample.GetNumChannels(); + + // Initialize soundtouch object. + soundtouch_setSampleRate(handleSt, sampleRate); + soundtouch_setChannels(handleSt, numChannels); + + // Given ratio is time stretch ratio, and must be converted to + // tempo change ratio: for example time stretch ratio 2 means + // tempo change ratio 0.5. + soundtouch_setTempoChange(handleSt, (1.0f / m_ratio - 1.0f) * 100.0f); + + // Read settings from GUI. + m_parent.ReadTimeStretchParameters(); + + // Set settings to soundtouch. Zero value means 'use default', and + // setting value is read back after setting because not all settings are accepted. + soundtouch_setSetting(handleSt, SETTING_SEQUENCE_MS, m_parent.m_nSequenceMs); + m_parent.m_nSequenceMs = soundtouch_getSetting(handleSt, SETTING_SEQUENCE_MS); + + soundtouch_setSetting(handleSt, SETTING_SEEKWINDOW_MS, m_parent.m_nSeekWindowMs); + m_parent.m_nSeekWindowMs = soundtouch_getSetting(handleSt, SETTING_SEEKWINDOW_MS); + + soundtouch_setSetting(handleSt, SETTING_OVERLAP_MS, m_parent.m_nOverlapMs); + m_parent.m_nOverlapMs = soundtouch_getSetting(handleSt, SETTING_OVERLAP_MS); + + // Update GUI with the actual SoundTouch parameters in effect. + m_parent.UpdateTimeStretchParameters(); + + const SmpLength inBatchSize = soundtouch_getSetting(handleSt, SETTING_NOMINAL_INPUT_SEQUENCE) + 1; // approximate value, add 1 to play safe + const SmpLength outBatchSize = soundtouch_getSetting(handleSt, SETTING_NOMINAL_OUTPUT_SEQUENCE) + 1; // approximate value, add 1 to play safe + + const auto selection = m_parent.GetSelectionPoints(); + const SmpLength selLength = selection.selectionActive ? selection.nEnd - selection.nStart : sample.nLength; + const SmpLength remainLength = sample.nLength - selLength; + + if(selLength < inBatchSize) + { + soundtouch_destroyInstance(handleSt); + return kSampleTooShort; + } + + if(static_cast<SmpLength>(std::ceil(static_cast<double>(m_ratio) * selLength)) < outBatchSize) + { + soundtouch_destroyInstance(handleSt); + return kSampleTooShort; + } + + const SmpLength stretchLength = mpt::saturate_round<SmpLength>(m_ratio * selLength); + const SmpLength stretchEnd = selection.nStart + stretchLength; + const SmpLength newSampleLength = remainLength + stretchLength; + void *pNewSample = nullptr; + if(newSampleLength <= MAX_SAMPLE_LENGTH) + { + pNewSample = ModSample::AllocateSample(newSampleLength, sample.GetBytesPerSample()); + } + if(pNewSample == nullptr) + { + soundtouch_destroyInstance(handleSt); + return kOutOfMemory; + } + + // Show wait mouse cursor + BeginWaitCursor(); + + memcpy(pNewSample, sample.sampleb(), selection.nStart * sample.GetBytesPerSample()); + memcpy(static_cast<std::byte *>(pNewSample) + stretchEnd * sample.GetBytesPerSample(), sample.sampleb() + selection.nEnd * sample.GetBytesPerSample(), (sample.nLength - selection.nEnd) * sample.GetBytesPerSample()); + + constexpr SmpLength MaxInputChunkSize = 1024; + + std::vector<float> buffer(MaxInputChunkSize * numChannels); + + SmpLength inPos = selection.nStart; + SmpLength outPos = selection.nStart; // Keeps count of the sample length received from stretching process. + + DWORD timeLast = 0; + + // Process sample in steps. + while(inPos < selection.nEnd) + { + // Current chunk size limit test + const SmpLength inChunkSize = std::min(MaxInputChunkSize, sample.nLength - inPos); + + DWORD timeNow = timeGetTime(); + if(timeNow - timeLast >= m_updateInterval) + { + // Show progress bar using process button painting & text label + TCHAR progress[32]; + uint32 percent = static_cast<uint32>(100 * (inPos + inChunkSize) / sample.nLength); + wsprintf(progress, _T("Time Stretch... %u%%"), percent); + SetText(progress); + SetProgress(percent); + ProcessMessages(); + if(m_abort) + break; + + timeLast = timeNow; + } + + // Send sampledata for processing. + switch(smpSize) + { + case 1: + CopyAudioChannelsInterleaved(buffer.data(), sample.sample8() + inPos * numChannels, numChannels, inChunkSize); + break; + case 2: + CopyAudioChannelsInterleaved(buffer.data(), sample.sample16() + inPos * numChannels, numChannels, inChunkSize); + break; + } + soundtouch_putSamples(handleSt, buffer.data(), inChunkSize); + + // Receive some processed samples (it's not guaranteed that there is any available). + { + SmpLength outChunkSize = std::min(static_cast<SmpLength>(soundtouch_numSamples(handleSt)), stretchLength - outPos); + if(outChunkSize > 0) + { + buffer.resize(outChunkSize * numChannels); + soundtouch_receiveSamples(handleSt, buffer.data(), outChunkSize); + switch(smpSize) + { + case 1: + CopyAudioChannelsInterleaved(static_cast<int8 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize); + break; + case 2: + CopyAudioChannelsInterleaved(static_cast<int16 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize); + break; + } + outPos += outChunkSize; + } + } + + // Next buffer chunk + inPos += inChunkSize; + } + + if(!m_abort) + { + // The input sample should now be processed. Receive remaining samples. + soundtouch_flush(handleSt); + SmpLength outChunkSize = std::min(static_cast<SmpLength>(soundtouch_numSamples(handleSt)), stretchLength - (outPos - selection.nStart)); + if(outChunkSize > 0) + { + buffer.resize(outChunkSize * numChannels); + soundtouch_receiveSamples(handleSt, buffer.data(), outChunkSize); + switch(smpSize) + { + case 1: + CopyAudioChannelsInterleaved(static_cast<int8 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize); + break; + case 2: + CopyAudioChannelsInterleaved(static_cast<int16 *>(pNewSample) + numChannels * outPos, buffer.data(), numChannels, outChunkSize); + break; + } + outPos += outChunkSize; + } + + soundtouch_clear(handleSt); + MPT_ASSERT(soundtouch_isEmpty(handleSt) != 0); + + CSoundFile &sndFile = m_modDoc.GetSoundFile(); + m_parent.PrepareUndo("Time Stretch", sundo_replace); + // Swap sample buffer pointer to new buffer, update song + sample data & free old sample buffer + ctrlSmp::ReplaceSample(sample, pNewSample, std::min(outPos + remainLength, newSampleLength), sndFile); + // Update loops and wrap-around buffer + sample.SetLoop( + mpt::saturate_round<SmpLength>(sample.nLoopStart * m_ratio), + mpt::saturate_round<SmpLength>(sample.nLoopEnd * m_ratio), + sample.uFlags[CHN_LOOP], + sample.uFlags[CHN_PINGPONGLOOP], + sndFile); + sample.SetSustainLoop( + mpt::saturate_round<SmpLength>(sample.nSustainStart * m_ratio), + mpt::saturate_round<SmpLength>(sample.nSustainEnd * m_ratio), + sample.uFlags[CHN_SUSTAINLOOP], + sample.uFlags[CHN_PINGPONGSUSTAIN], + sndFile); + } else + { + ModSample::FreeSample(pNewSample); + } + + soundtouch_destroyInstance(handleSt); + + // Restore mouse cursor + EndWaitCursor(); + + if(selection.selectionActive) + m_parent.SetSelectionPoints(selection.nStart, selection.nStart + stretchLength); + + return m_abort ? kAbort : kOK; + } + + TimeStretchPitchShiftResult PitchShift() + { + static constexpr SmpLength MAX_BUFFER_LENGTH = 8192; + ModSample &sample = m_modDoc.GetSoundFile().GetSample(m_sample); + + if(!sample.HasSampleData() || m_ratio < 0.5f || m_ratio > 2.0f) + { + return kAbort; + } + + // Get selected oversampling - quality - (also refered as FFT overlapping) factor + CComboBox *combo = (CComboBox *)m_parent.GetDlgItem(IDC_COMBO5); + long ovs = combo->GetCurSel() + 4; + + // Get selected FFT size (power of 2; should not exceed MAX_BUFFER_LENGTH - see smbPitchShift.h) + combo = (CComboBox *)m_parent.GetDlgItem(IDC_COMBO6); + UINT fft = 1 << (combo->GetCurSel() + 8); + while(fft > MAX_BUFFER_LENGTH) fft >>= 1; + + // Show wait mouse cursor + BeginWaitCursor(); + + // Get original sample rate + const float sampleRate = static_cast<float>(sample.GetSampleRate(m_modDoc.GetModType())); + + // Allocate working buffer + const size_t bufferSize = MAX_BUFFER_LENGTH + fft; + std::vector<float> buffer; + try + { + buffer.resize(bufferSize); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return kOutOfMemory; + } + + const auto smpSize = sample.GetElementarySampleSize(); + const auto numChans = sample.GetNumChannels(); + const auto bps = sample.GetBytesPerSample(); + int8 *pNewSample = static_cast<int8 *>(ModSample::AllocateSample(sample.nLength, bps)); + if(pNewSample == nullptr) + return kOutOfMemory; + + DWORD timeLast = 0; + + const auto selection = m_parent.GetSelectionPoints(); + + // Process each channel separately + for(uint8 chn = 0; chn < numChans; chn++) + { + // Process sample buffer using MAX_BUFFER_LENGTH (max) sized chunk steps (in order to allow + // the processing of BIG samples...) + for(SmpLength pos = selection.nStart; pos < selection.nEnd;) + { + DWORD timeNow = timeGetTime(); + if(timeNow - timeLast >= m_updateInterval) + { + TCHAR progress[32]; + uint32 percent = static_cast<uint32>(chn * 50.0 + (100.0 / numChans) * (pos - selection.nStart) / (selection.nEnd - selection.nStart)); + wsprintf(progress, _T("Pitch Shift... %u%%"), percent); + SetText(progress); + SetProgress(percent); + ProcessMessages(); + if(m_abort) + break; + + timeLast = timeNow; + } + + // TRICK : output buffer offset management + // as the pitch-shifter adds some blank signal in head of output buffer (matching FFT + // length - in short it needs a certain amount of data before being able to output some + // meaningful processed samples) , in order to avoid this behaviour , we will ignore + // the first FFT_length samples and process the same amount of extra blank samples + // (all 0.0f) at the end of the buffer (those extra samples will benefit from internal + // FFT data computed during the previous steps resulting in a correct and consistent + // signal output). + const SmpLength processLen = (pos + MAX_BUFFER_LENGTH <= selection.nEnd) ? MAX_BUFFER_LENGTH : (selection.nEnd - pos); + const bool bufStart = (pos == selection.nStart); + const bool bufEnd = (pos + processLen >= selection.nEnd); + const SmpLength startOffset = (bufStart ? fft : 0); + const SmpLength innerOffset = (bufStart ? 0 : fft); + const SmpLength finalOffset = (bufEnd ? fft : 0); + + // Re-initialize pitch-shifter with blank FFT before processing 1st chunk of current channel + if(bufStart) + { + std::fill(buffer.begin(), buffer.begin() + fft, 0.0f); + smbPitchShift(m_ratio, fft, fft, ovs, sampleRate, buffer.data(), buffer.data()); + } + + // Convert current channel's data chunk to float + SmpLength offset = pos * numChans + chn; + switch(smpSize) + { + case 1: + CopySample<SC::ConversionChain<SC::Convert<float, int8>, SC::DecodeIdentity<int8>>>(buffer.data(), processLen, 1, sample.sample8() + offset, sizeof(int8) * processLen * numChans, numChans); + break; + case 2: + CopySample<SC::ConversionChain<SC::Convert<float, int16>, SC::DecodeIdentity<int16>>>(buffer.data(), processLen, 1, sample.sample16() + offset, sizeof(int16) * processLen * numChans, numChans); + break; + } + + // Fills extra blank samples (read TRICK description comment above) + if(bufEnd) + std::fill(buffer.begin() + processLen, buffer.begin() + processLen + finalOffset, 0.0f); + + // Apply pitch shifting + smbPitchShift(m_ratio, static_cast<long>(processLen + finalOffset), fft, ovs, sampleRate, buffer.data(), buffer.data()); + + // Restore pitched-shifted float sample into original sample buffer + void *ptr = pNewSample + (pos - innerOffset) * smpSize * numChans + chn * smpSize; + const SmpLength copyLength = processLen + finalOffset - startOffset + 1; + + switch(smpSize) + { + case 1: + CopySample<SC::ConversionChain<SC::Convert<int8, float>, SC::DecodeIdentity<float>>>(static_cast<int8 *>(ptr), copyLength, numChans, buffer.data() + startOffset, sizeof(float) * bufferSize, 1); + break; + case 2: + CopySample<SC::ConversionChain<SC::Convert<int16, float>, SC::DecodeIdentity<float>>>(static_cast<int16 *>(ptr), copyLength, numChans, buffer.data() + startOffset, sizeof(float) * bufferSize, 1); + break; + } + + // Next buffer chunk + pos += processLen; + } + } + + if(!m_abort) + { + m_parent.PrepareUndo("Pitch Shift", sundo_replace); + memcpy(pNewSample, sample.sampleb(), selection.nStart * bps); + memcpy(pNewSample + selection.nEnd * bps, sample.sampleb() + selection.nEnd * bps, (sample.nLength - selection.nEnd) * bps); + ctrlSmp::ReplaceSample(sample, pNewSample, sample.nLength, m_modDoc.GetSoundFile()); + } else + { + ModSample::FreeSample(pNewSample); + } + + // Restore mouse cursor + EndWaitCursor(); + + return m_abort ? kAbort : kOK; + } +}; + + +void CCtrlSamples::OnPitchShiftTimeStretch() +{ + TimeStretchPitchShiftResult errorcode = kAbort; + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(!sample.HasSampleData()) goto error; + + if(IsDlgButtonChecked(IDC_CHECK3)) + { + // Time stretching + UpdateData(TRUE); //Ensure m_dTimeStretchRatio is up-to-date with textbox content + DoPitchShiftTimeStretch timeStretch(*this, m_modDoc, m_nSample, static_cast<float>(m_dTimeStretchRatio / 100.0), false); + timeStretch.DoModal(); + errorcode = timeStretch.m_result; + } else + { + // Pitch shifting + // Get selected pitch modifier [-12,+12] + CString text; + static_cast<CComboBox *>(GetDlgItem(IDC_COMBO4))->GetWindowText(text); + float pm = ConvertStrTo<float>(text); + if(pm == 0.0f) goto error; + + // Compute pitch ratio in range [0.5f ; 2.0f] (1.0f means output == input) + // * pitch up -> 1.0f + n / 12.0f -> (12.0f + n) / 12.0f , considering n : pitch modifier > 0 + // * pitch dn -> 1.0f - n / 24.0f -> (24.0f - n) / 24.0f , considering n : pitch modifier > 0 + float pitch = pm < 0 ? ((24.0f + pm) / 24.0f) : ((12.0f + pm) / 12.0f); + + // Apply pitch modifier + DoPitchShiftTimeStretch pitchShift(*this, m_modDoc, m_nSample, pitch, true); + pitchShift.DoModal(); + errorcode = pitchShift.m_result; + } + + if(errorcode == kOK) + { + // Update sample view + SetModified(SampleHint().Info().Data(), true, true); + return; + } + + // Error management +error: + + if(errorcode != kAbort) + { + CString str; + switch(errorcode) + { + case kInvalidRatio: + str = _T("Invalid stretch ratio!"); + break; + case kStretchTooShort: + case kStretchTooLong: + str = MPT_CFORMAT("Stretch ratio is too {}. Must be between 50% and 200%.")((errorcode == kStretchTooShort) ? CString(_T("low")) : CString(_T("high"))); + break; + case kOutOfMemory: + str = _T("Out of memory."); + break; + case kSampleTooShort: + str = _T("Sample too short."); + break; + case kStretchInvalidSampleRate: + str = _T("Sample rate must be 192,000 Hz or lower."); + break; + default: + str = _T("Unknown Error."); + break; + } + Reporting::Error(str); + } +} + + +void CCtrlSamples::OnReverse() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + + SampleSelectionPoints selection = GetSelectionPoints(); + + PrepareUndo("Reverse", sundo_reverse, selection.nStart, selection.nEnd); + if(SampleEdit::ReverseSample(sample, selection.nStart, selection.nEnd, m_sndFile)) + { + SetModified(SampleHint().Data(), false, true); + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnInvert() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + + SampleSelectionPoints selection = GetSelectionPoints(); + + PrepareUndo("Invert", sundo_invert, selection.nStart, selection.nEnd); + if(SampleEdit::InvertSample(sample, selection.nStart, selection.nEnd, m_sndFile) == true) + { + SetModified(SampleHint().Data(), false, true); + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnSignUnSign() +{ + if(!m_sndFile.GetSample(m_nSample).HasSampleData()) return; + + if(m_modDoc.IsNotePlaying(0, m_nSample, 0)) + MsgBoxHidable(ConfirmSignUnsignWhenPlaying); + + BeginWaitCursor(); + ModSample &sample = m_sndFile.GetSample(m_nSample); + SampleSelectionPoints selection = GetSelectionPoints(); + + PrepareUndo("Unsign", sundo_unsign, selection.nStart, selection.nEnd); + if(SampleEdit::UnsignSample(sample, selection.nStart, selection.nEnd, m_sndFile) == true) + { + SetModified(SampleHint().Data(), false, true); + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnSilence() +{ + if(!m_sndFile.GetSample(m_nSample).HasSampleData()) return; + BeginWaitCursor(); + SampleSelectionPoints selection = GetSelectionPoints(); + + // never apply silence to a sample that has no selection + const SmpLength len = selection.nEnd - selection.nStart; + if(selection.selectionActive && len > 1) + { + ModSample &sample = m_sndFile.GetSample(m_nSample); + PrepareUndo("Silence", sundo_update, selection.nStart, selection.nEnd); + if(SampleEdit::SilenceSample(sample, selection.nStart, selection.nEnd, m_sndFile)) + { + SetModified(SampleHint().Data(), false, true); + } + } + + EndWaitCursor(); + SwitchToView(); +} + + +void CCtrlSamples::OnPrevInstrument() +{ + if (m_nSample > 1) + SetCurrentSample(m_nSample - 1); + else + SetCurrentSample(m_sndFile.GetNumSamples()); +} + + +void CCtrlSamples::OnNextInstrument() +{ + if (m_nSample < m_sndFile.GetNumSamples()) + SetCurrentSample(m_nSample + 1); + else + SetCurrentSample(1); +} + + +void CCtrlSamples::OnNameChanged() +{ + if(IsLocked() || !m_nSample) return; + CString tmp; + m_EditName.GetWindowText(tmp); + const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp); + if(s != m_sndFile.m_szNames[m_nSample]) + { + if(!m_startedEdit) + { + PrepareUndo("Set Name"); + m_editInstrumentName = GetParentInstrumentWithSameName(); + if(m_editInstrumentName != INSTRUMENTINDEX_INVALID) + m_modDoc.GetInstrumentUndo().PrepareUndo(m_editInstrumentName, "Set Name"); + } + if(m_editInstrumentName <= m_sndFile.GetNumInstruments()) + { + if(auto instr = m_sndFile.Instruments[m_editInstrumentName]; instr != nullptr) + { + instr->name = s; + m_modDoc.UpdateAllViews(nullptr, InstrumentHint(m_editInstrumentName).Names(), this); + } + } + + m_sndFile.m_szNames[m_nSample] = s; + SetModified(SampleHint().Names(), false, false); + } +} + + +void CCtrlSamples::OnFileNameChanged() +{ + if(IsLocked()) return; + CString tmp; + m_EditFileName.GetWindowText(tmp); + const std::string s = mpt::ToCharset(m_sndFile.GetCharsetInternal(), tmp); + if(s != m_sndFile.GetSample(m_nSample).filename) + { + if(!m_startedEdit) PrepareUndo("Set Filename"); + m_sndFile.GetSample(m_nSample).filename = s; + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnVolumeChanged() +{ + if (IsLocked()) return; + int nVol = GetDlgItemInt(IDC_EDIT7); + Limit(nVol, 0, 64); + nVol *= 4; + ModSample &sample = m_sndFile.GetSample(m_nSample); + if (nVol != sample.nVolume) + { + if(!m_startedEdit) PrepareUndo("Set Default Volume"); + sample.nVolume = static_cast<uint16>(nVol); + sample.uFlags.reset(SMP_NODEFAULTVOLUME); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnGlobalVolChanged() +{ + if (IsLocked()) return; + int nVol = GetDlgItemInt(IDC_EDIT8); + Limit(nVol, 0, 64); + ModSample &sample = m_sndFile.GetSample(m_nSample); + if (nVol != sample.nGlobalVol) + { + if(!m_startedEdit) PrepareUndo("Set Global Volume"); + sample.nGlobalVol = static_cast<uint16>(nVol); + // Live-adjust volume + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if(chn.pModSample == &sample) + { + chn.UpdateInstrumentVolume(chn.pModSample, chn.pModInstrument); + } + } + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnSetPanningChanged() +{ + if (IsLocked()) return; + bool b = false; + if (m_sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) + { + b = IsDlgButtonChecked(IDC_CHECK1) != FALSE; + } + + ModSample &sample = m_sndFile.GetSample(m_nSample); + if(b != sample.uFlags[CHN_PANNING]) + { + PrepareUndo("Toggle Panning"); + sample.uFlags.set(CHN_PANNING, b); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnPanningChanged() +{ + if (IsLocked()) return; + int nPan = GetDlgItemInt(IDC_EDIT9); + if (nPan < 0) nPan = 0; + + if (m_sndFile.GetType() == MOD_TYPE_XM) + { + if (nPan > 255) nPan = 255; // displayed panning will be 0-255 with XM + } else + { + if (nPan > 64) nPan = 64; // displayed panning will be 0-64 with anything but XM. + nPan = nPan * 4; // so we x4 to get MPT's internal 0-256 range. + } + + ModSample &sample = m_sndFile.GetSample(m_nSample); + if (nPan != sample.nPan) + { + if(!m_startedEdit) PrepareUndo("Set Panning"); + sample.nPan = static_cast<uint16>(nPan); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnFineTuneChanged() +{ + if (IsLocked()) return; + int n = GetDlgItemInt(IDC_EDIT5); + if(!m_startedEdit) + PrepareUndo("Finetune"); + ModSample &sample = m_sndFile.GetSample(m_nSample); + if (!m_sndFile.UseFinetuneAndTranspose()) + { + if ((n > 0) && (n <= (m_sndFile.GetType() == MOD_TYPE_S3M ? 65535 : 9999999)) && (n != (int)m_sndFile.GetSample(m_nSample).nC5Speed)) + { + sample.nC5Speed = n; + int transp = ModSample::FrequencyToTranspose(n).first; + int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp; + Clamp(basenote, BASENOTE_MIN, BASENOTE_MAX); + basenote -= BASENOTE_MIN; + if (basenote != m_CbnBaseNote.GetCurSel()) + { + LockControls(); + m_CbnBaseNote.SetCurSel(basenote); + UnlockControls(); + } + SetModified(SampleHint().Info(), false, false); + } + } else + { + if(m_sndFile.GetType() & MOD_TYPE_MOD) + n = MOD2XMFineTune(n); + if((n >= -128) && (n <= 127)) + { + sample.nFineTune = static_cast<int8>(n); + SetModified(SampleHint().Info(), false, false); + } + } +} + + +void CCtrlSamples::OnFineTuneChangedDone() +{ + // Update all playing channels + ModSample &sample = m_sndFile.GetSample(m_nSample); + for(auto &chn : m_sndFile.m_PlayState.Chn) + { + if(chn.pModSample == &sample) + { + chn.nTranspose = sample.RelativeTone; + chn.nFineTune = sample.nFineTune; + if(chn.nC5Speed != 0 && sample.nC5Speed != 0) + { + if(m_sndFile.PeriodsAreFrequencies()) + chn.nPeriod = Util::muldivr(chn.nPeriod, sample.nC5Speed, chn.nC5Speed); + else if(!m_sndFile.m_SongFlags[SONG_LINEARSLIDES]) + chn.nPeriod = Util::muldivr(chn.nPeriod, chn.nC5Speed, sample.nC5Speed); + } + chn.nC5Speed = sample.nC5Speed; + } + } +} + + +void CCtrlSamples::OnBaseNoteChanged() +{ + if (IsLocked()) return; + int n = static_cast<int>(m_CbnBaseNote.GetItemData(m_CbnBaseNote.GetCurSel())); + + ModSample &sample = m_sndFile.GetSample(m_nSample); + PrepareUndo("Transpose"); + + if(!m_sndFile.UseFinetuneAndTranspose()) + { + const int oldTransp = ModSample::FrequencyToTranspose(sample.nC5Speed).first; + const uint32 newFreq = mpt::saturate_round<uint32>(sample.nC5Speed * std::pow(2.0, (n - oldTransp) / 12.0)); + if (newFreq > 0 && newFreq <= (m_sndFile.GetType() == MOD_TYPE_S3M ? 65535u : 9999999u) && newFreq != sample.nC5Speed) + { + sample.nC5Speed = newFreq; + LockControls(); + SetDlgItemInt(IDC_EDIT5, newFreq, FALSE); + + // Due to rounding imprecisions if the base note is below 0, we recalculate it here to make sure that the value stays consistent. + int basenote = (NOTE_MIDDLEC - NOTE_MIN) + ModSample::FrequencyToTranspose(newFreq).first; + Limit(basenote, BASENOTE_MIN, BASENOTE_MAX); + basenote -= BASENOTE_MIN; + if(basenote != m_CbnBaseNote.GetCurSel()) + m_CbnBaseNote.SetCurSel(basenote); + + OnFineTuneChangedDone(); + UnlockControls(); + SetModified(SampleHint().Info(), false, false); + } + } else + { + if ((n >= -128) && (n < 128)) + { + sample.RelativeTone = (int8)n; + OnFineTuneChangedDone(); + SetModified(SampleHint().Info(), false, false); + } + } +} + + +void CCtrlSamples::OnVibTypeChanged() +{ + if (IsLocked()) return; + int n = m_ComboAutoVib.GetCurSel(); + if (n >= 0) + { + PrepareUndo("Set Vibrato Type"); + m_sndFile.GetSample(m_nSample).nVibType = static_cast<VibratoType>(m_ComboAutoVib.GetItemData(n)); + + PropagateAutoVibratoChanges(); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnVibDepthChanged() +{ + if (IsLocked()) return; + int lmin = 0, lmax = 0; + m_SpinVibDepth.GetRange(lmin, lmax); + int n = GetDlgItemInt(IDC_EDIT15); + if ((n >= lmin) && (n <= lmax)) + { + if(!m_startedEdit) PrepareUndo("Set Vibrato Depth"); + m_sndFile.GetSample(m_nSample).nVibDepth = static_cast<uint8>(n); + + PropagateAutoVibratoChanges(); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnVibSweepChanged() +{ + if (IsLocked()) return; + int lmin = 0, lmax = 0; + m_SpinVibSweep.GetRange(lmin, lmax); + int n = GetDlgItemInt(IDC_EDIT14); + if ((n >= lmin) && (n <= lmax)) + { + if(!m_startedEdit) PrepareUndo("Set Vibrato Sweep"); + m_sndFile.GetSample(m_nSample).nVibSweep = static_cast<uint8>(n); + + PropagateAutoVibratoChanges(); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnVibRateChanged() +{ + if (IsLocked()) return; + int lmin = 0, lmax = 0; + m_SpinVibRate.GetRange(lmin, lmax); + int n = GetDlgItemInt(IDC_EDIT16); + if ((n >= lmin) && (n <= lmax)) + { + if(!m_startedEdit) PrepareUndo("Set Vibrato Rate"); + m_sndFile.GetSample(m_nSample).nVibRate = static_cast<uint8>(n); + + PropagateAutoVibratoChanges(); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnLoopTypeChanged() +{ + if(IsLocked()) return; + const int n = m_ComboLoopType.GetCurSel(); + ModSample &sample = m_sndFile.GetSample(m_nSample); + bool wasDisabled = !sample.uFlags[CHN_LOOP]; + + PrepareUndo("Set Loop Type"); + + // Loop type index: 0: Off, 1: On, 2: PingPong + sample.uFlags.set(CHN_LOOP, n > 0); + sample.uFlags.set(CHN_PINGPONGLOOP, n == 2); + + // set loop points if theren't any + if(wasDisabled && sample.uFlags[CHN_LOOP] && sample.nLoopStart == sample.nLoopEnd) + { + SampleSelectionPoints selection = GetSelectionPoints(); + if(selection.selectionActive) + { + sample.SetLoop(selection.nStart, selection.nEnd, true, n == 2, m_sndFile); + } else + { + sample.SetLoop(0, sample.nLength, true, n == 2, m_sndFile); + } + m_modDoc.UpdateAllViews(NULL, SampleHint(m_nSample).Info()); + } else + { + sample.PrecomputeLoops(m_sndFile); + } + ctrlSmp::UpdateLoopPoints(sample, m_sndFile); + SetModified(SampleHint().Info(), false, false); +} + + +void CCtrlSamples::OnLoopPointsChanged() +{ + if(IsLocked()) return; + ModSample &sample = m_sndFile.GetSample(m_nSample); + SmpLength start = GetDlgItemInt(IDC_EDIT1, NULL, FALSE), end = GetDlgItemInt(IDC_EDIT2, NULL, FALSE); + if(start < end || !sample.uFlags[CHN_LOOP]) + { + if(!m_startedEdit) PrepareUndo("Set Loop"); + const int n = m_ComboLoopType.GetCurSel(); + sample.SetLoop(start, end, n > 0, n == 2, m_sndFile); + SetModified(SampleHint().Info(), false, false); + } +} + + +void CCtrlSamples::OnSustainTypeChanged() +{ + if(IsLocked()) return; + const int n = m_ComboSustainType.GetCurSel(); + ModSample &sample = m_sndFile.GetSample(m_nSample); + bool wasDisabled = !sample.uFlags[CHN_SUSTAINLOOP]; + + PrepareUndo("Set Sustain Loop Type"); + + // Loop type index: 0: Off, 1: On, 2: PingPong + sample.uFlags.set(CHN_SUSTAINLOOP, n > 0); + sample.uFlags.set(CHN_PINGPONGSUSTAIN, n == 2); + + // set sustain loop points if theren't any + if(wasDisabled && sample.uFlags[CHN_SUSTAINLOOP] && sample.nSustainStart == sample.nSustainEnd) + { + SampleSelectionPoints selection = GetSelectionPoints(); + if(selection.selectionActive) + { + sample.SetSustainLoop(selection.nStart, selection.nEnd, true, n == 2, m_sndFile); + } else + { + sample.SetSustainLoop(0, sample.nLength, true, n == 2, m_sndFile); + } + m_modDoc.UpdateAllViews(NULL, SampleHint(m_nSample).Info()); + } else + { + sample.PrecomputeLoops(m_sndFile); + } + ctrlSmp::UpdateLoopPoints(sample, m_sndFile); + SetModified(SampleHint().Info(), false, false); +} + + +void CCtrlSamples::OnSustainPointsChanged() +{ + if(IsLocked()) return; + ModSample &sample = m_sndFile.GetSample(m_nSample); + SmpLength start = GetDlgItemInt(IDC_EDIT3, NULL, FALSE), end = GetDlgItemInt(IDC_EDIT4, NULL, FALSE); + if(start < end || !sample.uFlags[CHN_SUSTAINLOOP]) + { + if(!m_startedEdit) PrepareUndo("Set Sustain Loop"); + const int n = m_ComboSustainType.GetCurSel(); + sample.SetSustainLoop(start, end, n > 0, n == 2, m_sndFile); + SetModified(SampleHint().Info(), false, false); + } +} + + +#define SMPLOOP_ACCURACY 7 // 5% +#define BIDILOOP_ACCURACY 2 // 5% + + +bool MPT_LoopCheck(int sstart0, int sstart1, int send0, int send1) +{ + int dse0 = send0 - sstart0; + if ((dse0 < -SMPLOOP_ACCURACY) || (dse0 > SMPLOOP_ACCURACY)) return false; + int dse1 = send1 - sstart1; + if ((dse1 < -SMPLOOP_ACCURACY) || (dse1 > SMPLOOP_ACCURACY)) return false; + int dstart = sstart1 - sstart0; + int dend = send1 - send0; + if (!dstart) dstart = dend >> 7; + if (!dend) dend = dstart >> 7; + if ((dstart ^ dend) < 0) return false; + int delta = dend - dstart; + return ((delta > -SMPLOOP_ACCURACY) && (delta < SMPLOOP_ACCURACY)); +} + + +bool MPT_BidiEndCheck(int spos0, int spos1, int spos2) +{ + int delta0 = spos1 - spos0; + int delta1 = spos2 - spos1; + int delta2 = spos2 - spos0; + if (!delta0) delta0 = delta1 >> 7; + if (!delta1) delta1 = delta0 >> 7; + if ((delta1 ^ delta0) < 0) return false; + return ((delta0 >= -1) && (delta0 <= 0) && (delta1 >= -1) && (delta1 <= 0) && (delta2 >= -1) && (delta2 <= 0)); +} + + +bool MPT_BidiStartCheck(int spos0, int spos1, int spos2) +{ + int delta1 = spos1 - spos0; + int delta0 = spos2 - spos1; + int delta2 = spos2 - spos0; + if (!delta0) delta0 = delta1 >> 7; + if (!delta1) delta1 = delta0 >> 7; + if ((delta1 ^ delta0) < 0) return false; + return ((delta0 >= -1) && (delta0 <= 0) && (delta1 > -1) && (delta1 <= 0) && (delta2 >= -1) && (delta2 <= 0)); +} + + + +void CCtrlSamples::OnVScroll(UINT nCode, UINT, CScrollBar *scrollBar) +{ + TCHAR s[256]; + if(IsLocked()) return; + ModSample &sample = m_sndFile.GetSample(m_nSample); + const uint8 *pSample = mpt::byte_cast<const uint8 *>(sample.sampleb()); + const uint32 inc = sample.GetBytesPerSample(); + SmpLength i; + int pos; + bool redraw = false; + static CScrollBar *lastScrollbar = nullptr; + + LockControls(); + if ((!sample.nLength) || (!pSample)) goto NoSample; + if (sample.uFlags[CHN_16BIT]) + { + pSample++; + } + // Loop Start + if ((pos = m_SpinLoopStart.GetPos32()) != 0 && sample.nLoopEnd > 0) + { + bool bOk = false; + const uint8 *p = pSample + sample.nLoopStart * inc; + int find0 = (int)pSample[sample.nLoopEnd*inc-inc]; + int find1 = (int)pSample[sample.nLoopEnd*inc]; + // Find Next LoopStart Point + if (pos > 0) + { + for (i = sample.nLoopStart + 1; i + 16 < sample.nLoopEnd; i++) + { + p += inc; + bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } else + // Find Prev LoopStart Point + { + for (i = sample.nLoopStart; i; ) + { + i--; + p -= inc; + bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } + if (bOk) + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Loop Start"); + sample.nLoopStart = i; + wsprintf(s, _T("%u"), sample.nLoopStart); + m_EditLoopStart.SetWindowText(s); + redraw = true; + sample.PrecomputeLoops(m_sndFile); + } + m_SpinLoopStart.SetPos(0); + } + // Loop End + if ((pos = m_SpinLoopEnd.GetPos32()) != 0) + { + bool bOk = false; + const uint8 *p = pSample + sample.nLoopEnd * inc; + int find0 = (int)pSample[sample.nLoopStart*inc]; + int find1 = (int)pSample[sample.nLoopStart*inc+inc]; + // Find Next LoopEnd Point + if (pos > 0) + { + for (i = sample.nLoopEnd + 1; i <= sample.nLength; i++, p += inc) + { + bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } else + // Find Prev LoopEnd Point + { + for (i = sample.nLoopEnd; i > sample.nLoopStart + 16; ) + { + i--; + p -= inc; + bOk = sample.uFlags[CHN_PINGPONGLOOP] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } + if (bOk) + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Loop End"); + sample.nLoopEnd = i; + wsprintf(s, _T("%u"), sample.nLoopEnd); + m_EditLoopEnd.SetWindowText(s); + redraw = true; + sample.PrecomputeLoops(m_sndFile); + } + m_SpinLoopEnd.SetPos(0); + } + // Sustain Loop Start + if ((pos = m_SpinSustainStart.GetPos32()) != 0 && sample.nSustainEnd > 0) + { + bool bOk = false; + const uint8 *p = pSample + sample.nSustainStart * inc; + int find0 = (int)pSample[sample.nSustainEnd*inc-inc]; + int find1 = (int)pSample[sample.nSustainEnd*inc]; + // Find Next Sustain LoopStart Point + if (pos > 0) + { + for (i = sample.nSustainStart + 1; i + 16 < sample.nSustainEnd; i++) + { + p += inc; + bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } else + // Find Prev Sustain LoopStart Point + { + for (i = sample.nSustainStart; i; ) + { + i--; + p -= inc; + bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiStartCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } + if (bOk) + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Sustain Loop Start"); + sample.nSustainStart = i; + wsprintf(s, _T("%u"), sample.nSustainStart); + m_EditSustainStart.SetWindowText(s); + redraw = true; + sample.PrecomputeLoops(m_sndFile); + } + m_SpinSustainStart.SetPos(0); + } + // Sustain Loop End + if ((pos = m_SpinSustainEnd.GetPos32()) != 0) + { + bool bOk = false; + const uint8 *p = pSample + sample.nSustainEnd * inc; + int find0 = (int)pSample[sample.nSustainStart*inc]; + int find1 = (int)pSample[sample.nSustainStart*inc+inc]; + // Find Next LoopEnd Point + if (pos > 0) + { + for (i = sample.nSustainEnd + 1; i + 1 < sample.nLength; i++, p += inc) + { + bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } else + // Find Prev LoopEnd Point + { + for (i = sample.nSustainEnd; i > sample.nSustainStart + 16; ) + { + i--; + p -= inc; + bOk = sample.uFlags[CHN_PINGPONGSUSTAIN] ? MPT_BidiEndCheck(p[0], p[inc], p[inc*2]) : MPT_LoopCheck(find0, find1, p[0], p[inc]); + if (bOk) break; + } + } + if (bOk) + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Set Sustain Loop End"); + sample.nSustainEnd = i; + wsprintf(s, _T("%u"), sample.nSustainEnd); + m_EditSustainEnd.SetWindowText(s); + redraw = true; + sample.PrecomputeLoops(m_sndFile); + } + m_SpinSustainEnd.SetPos(0); + } +NoSample: + // FineTune / C-5 Speed + if ((pos = m_SpinFineTune.GetPos32()) != 0) + { + if (!m_sndFile.UseFinetuneAndTranspose()) + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Finetune"); + if(sample.nC5Speed < 1) + sample.nC5Speed = 8363; + auto oldFreq = sample.nC5Speed; + sample.Transpose((pos * TrackerSettings::Instance().m_nFinetuneStep) / 1200.0); + if(sample.nC5Speed == oldFreq) + sample.nC5Speed += pos; + Limit(sample.nC5Speed, 1u, 9999999u); // 9999999 is max. in Impulse Tracker + int transp = ModSample::FrequencyToTranspose(sample.nC5Speed).first; + int basenote = (NOTE_MIDDLEC - NOTE_MIN) + transp; + Clamp(basenote, BASENOTE_MIN, BASENOTE_MAX); + basenote -= BASENOTE_MIN; + if (basenote != m_CbnBaseNote.GetCurSel()) m_CbnBaseNote.SetCurSel(basenote); + SetDlgItemInt(IDC_EDIT5, sample.nC5Speed, FALSE); + } else + { + if(!m_startedEdit && lastScrollbar != scrollBar) PrepareUndo("Finetune"); + int ftune = (int)sample.nFineTune; + // MOD finetune range -8 to 7 translates to -128 to 112 + if(m_sndFile.GetType() & MOD_TYPE_MOD) + { + ftune = Clamp((ftune >> 4) + pos, -8, 7); + sample.nFineTune = MOD2XMFineTune((signed char)ftune); + } else + { + ftune = Clamp(ftune + pos, -128, 127); + sample.nFineTune = (signed char)ftune; + } + SetDlgItemInt(IDC_EDIT5, ftune, TRUE); + } + redraw = true; + m_SpinFineTune.SetPos(0); + OnFineTuneChangedDone(); + } + if(scrollBar->m_hWnd == m_SpinSequenceMs.m_hWnd || scrollBar->m_hWnd == m_SpinSeekWindowMs.m_hWnd || scrollBar->m_hWnd == m_SpinOverlap.m_hWnd) + { + ReadTimeStretchParameters(); + UpdateTimeStretchParameters(); + } + if(nCode == SB_ENDSCROLL) SwitchToView(); + if(redraw) + { + SetModified(SampleHint().Info().Data(), false, false); + } + lastScrollbar = scrollBar; + UnlockControls(); +} + + +BOOL CCtrlSamples::PreTranslateMessage(MSG *pMsg) +{ + if (pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler* ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = (UINT)pMsg->wParam; + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxViewSamples); + + if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } + + } + return CModControlDlg::PreTranslateMessage(pMsg); +} + + +LRESULT CCtrlSamples::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + int transpose = 0; + switch(wParam) + { + case kcSampleLoad: OnSampleOpen(); return wParam; + case kcSampleLoadRaw: OnSampleOpenRaw(); return wParam; + case kcSampleSave: OnSampleSaveOne(); return wParam; + case kcSampleNew: InsertSample(false); return wParam; + case kcSampleDuplicate: InsertSample(true); return wParam; + + case kcSampleTransposeUp: transpose = 1; break; + case kcSampleTransposeDown: transpose = -1; break; + case kcSampleTransposeOctUp: transpose = 12; break; + case kcSampleTransposeOctDown: transpose = -12; break; + + case kcSampleUpsample: + case kcSampleDownsample: + { + uint32 oldRate = m_sndFile.GetSample(m_nSample).GetSampleRate(m_sndFile.GetType()); + ApplyResample(m_nSample, wParam == kcSampleUpsample ? oldRate * 2 : oldRate / 2, TrackerSettings::Instance().sampleEditorDefaultResampler); + } + return wParam; + case kcSampleResample: + OnResample(); + return wParam; + case kcSampleStereoSep: + OnStereoSeparation(); + return wParam; + case kcSampleInitializeOPL: + OnInitOPLInstrument(); + return wParam; + } + + if(transpose) + { + if(m_CbnBaseNote.IsWindowEnabled()) + { + int sel = Clamp(m_CbnBaseNote.GetCurSel() + transpose, 0, m_CbnBaseNote.GetCount() - 1); + if(sel != m_CbnBaseNote.GetCurSel()) + { + m_CbnBaseNote.SetCurSel(sel); + OnBaseNoteChanged(); + } + } + return wParam; + } + + return kcNull; +} + + +// Return currently selected part of the sample. +// The whole sample size will be returned if no part of the sample is selected. +// However, point.bSelected indicates whether a sample selection exists or not. +CCtrlSamples::SampleSelectionPoints CCtrlSamples::GetSelectionPoints() +{ + SampleSelectionPoints points; + SAMPLEVIEWSTATE viewstate; + const ModSample &sample = m_sndFile.GetSample(m_nSample); + + Clear(viewstate); + SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&viewstate); + points.nStart = viewstate.dwBeginSel; + points.nEnd = viewstate.dwEndSel; + if(points.nEnd > sample.nLength) points.nEnd = sample.nLength; + if(points.nStart > points.nEnd) points.nStart = points.nEnd; + points.selectionActive = true; + if(points.nStart >= points.nEnd) + { + points.nStart = 0; + points.nEnd = sample.nLength; + points.selectionActive = false; + } + return points; +} + +// Set the currently selected part of the sample. +// To reset the selection, use nStart = nEnd = 0. +void CCtrlSamples::SetSelectionPoints(SmpLength nStart, SmpLength nEnd) +{ + const ModSample &sample = m_sndFile.GetSample(m_nSample); + + Limit(nStart, SmpLength(0), sample.nLength); + Limit(nEnd, SmpLength(0), sample.nLength); + + SAMPLEVIEWSTATE viewstate; + Clear(viewstate); + SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)&viewstate); + + viewstate.dwBeginSel = nStart; + viewstate.dwEndSel = nEnd; + SendViewMessage(VIEWMSG_LOADSTATE, (LPARAM)&viewstate); +} + + +// Crossfade loop to create smooth loop transitions +void CCtrlSamples::OnXFade() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + + if(!sample.HasSampleData()) + { + MessageBeep(MB_ICONWARNING); + SwitchToView(); + return; + } + bool resetLoopOnCancel = false; + if((sample.nLoopEnd <= sample.nLoopStart || sample.nLoopEnd > sample.nLength) + && (sample.nSustainEnd <= sample.nSustainStart || sample.nSustainEnd > sample.nLength)) + { + const auto selection = GetSelectionPoints(); + if(selection.nStart > 0 && selection.nEnd > selection.nStart) + { + sample.SetLoop(selection.nStart, selection.nEnd, true, false, m_sndFile); + resetLoopOnCancel = true; + } else + { + Reporting::Error("Crossfade requires a sample loop to work.", this); + SwitchToView(); + return; + } + } + if(sample.nLoopStart == 0 && sample.nSustainStart == 0) + { + Reporting::Error("Crossfade requires the sample to have data before the loop start.", this); + SwitchToView(); + return; + } + + CSampleXFadeDlg dlg(this, sample); + if(dlg.DoModal() == IDOK) + { + const SmpLength loopStart = dlg.m_useSustainLoop ? sample.nSustainStart: sample.nLoopStart; + const SmpLength loopEnd = dlg.m_useSustainLoop ? sample.nSustainEnd: sample.nLoopEnd; + const SmpLength maxSamples = std::min({ sample.nLength, loopStart, loopEnd / 2 }); + SmpLength fadeSamples = dlg.PercentToSamples(dlg.m_fadeLength); + LimitMax(fadeSamples, maxSamples); + if(fadeSamples < 2) return; + + PrepareUndo("Crossfade", sundo_update, + loopEnd - fadeSamples, + loopEnd + (dlg.m_afterloopFade ? std::min(sample.nLength - loopEnd, fadeSamples) : 0)); + + if(SampleEdit::XFadeSample(sample, fadeSamples, dlg.m_fadeLaw, dlg.m_afterloopFade, dlg.m_useSustainLoop, m_sndFile)) + { + SetModified(SampleHint().Info().Data(), true, true); + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + } else if(resetLoopOnCancel) + { + sample.SetLoop(0, 0, false, false, m_sndFile); + } + SwitchToView(); +} + + +void CCtrlSamples::OnStereoSeparation() +{ + ModSample &sample = m_sndFile.GetSample(m_nSample); + + if(!sample.HasSampleData() + || sample.GetNumChannels() != 2 + || sample.uFlags[CHN_ADLIB]) + { + MessageBeep(MB_ICONWARNING); + SwitchToView(); + return; + } + + static double separation = 100.0; + CInputDlg dlg(this, _T("Stereo separation amount\n0% = mono, 100% = no change, 200% = double separation\nNegative values swap channels"), -200.0, 200.0, separation); + if(dlg.DoModal() == IDOK) + { + separation = dlg.resultAsDouble; + + SampleSelectionPoints selection = GetSelectionPoints(); + PrepareUndo("Stereo Separation", sundo_update, + selection.nStart, selection.nEnd); + + if(SampleEdit::StereoSepSample(sample, selection.nStart, selection.nEnd, separation, m_sndFile)) + { + SetModified(SampleHint().Info().Data(), true, true); + } else + { + m_modDoc.GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + } + SwitchToView(); +} + + +void CCtrlSamples::OnAutotune() +{ + SampleSelectionPoints selection = GetSelectionPoints(); + if(!selection.selectionActive) + { + selection.nStart = selection.nEnd = 0; + } + + ModSample &sample = m_sndFile.GetSample(m_nSample); + Autotune at(sample, m_sndFile.GetType(), selection.nStart, selection.nEnd); + if(at.CanApply()) + { + CAutotuneDlg dlg(this); + if(dlg.DoModal() == IDOK) + { + BeginWaitCursor(); + PrepareUndo("Automatic Sample Tuning"); + bool modified = true; + if(IsOPLInstrument()) + { + const uint32 newFreq = mpt::saturate_round<uint32>(dlg.GetPitchReference() * (8363.0 / 440.0) * std::pow(2.0, dlg.GetTargetNote() / 12.0)); + modified = (newFreq != sample.nC5Speed); + sample.nC5Speed = newFreq; + } else + { + modified = at.Apply(static_cast<double>(dlg.GetPitchReference()), dlg.GetTargetNote()); + } + OnFineTuneChangedDone(); + if(modified) + SetModified(SampleHint().Info(), true, false); + EndWaitCursor(); + } + } + SwitchToView(); +} + + +void CCtrlSamples::OnKeepSampleOnDisk() +{ + SAMPLEINDEX first = m_nSample, last = m_nSample; + if(CMainFrame::GetInputHandler()->ShiftPressed()) + { + first = 1; + last = m_sndFile.GetNumSamples(); + } + + const bool enable = IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED; + for(SAMPLEINDEX i = first; i <= last; i++) + { + if(bool newState = enable && m_sndFile.SampleHasPath(i); newState != m_sndFile.GetSample(i).uFlags[SMP_KEEPONDISK]) + { + m_sndFile.GetSample(i).uFlags.set(SMP_KEEPONDISK, newState); + m_modDoc.UpdateAllViews(nullptr, SampleHint(i).Info().Names(), this); + } + } + m_modDoc.SetModified(); +} + + +// When changing auto vibrato properties, propagate them to other samples of the same instrument in XM edit mode. +void CCtrlSamples::PropagateAutoVibratoChanges() +{ + if(!(m_sndFile.GetType() & MOD_TYPE_XM)) + { + return; + } + + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + if(m_sndFile.IsSampleReferencedByInstrument(m_nSample, i)) + { + const auto referencedSamples = m_sndFile.Instruments[i]->GetSamples(); + + // Propagate changes to all samples that belong to this instrument. + const ModSample &it = m_sndFile.GetSample(m_nSample); + m_sndFile.PropagateXMAutoVibrato(i, it.nVibType, it.nVibSweep, it.nVibDepth, it.nVibRate); + for(auto smp : referencedSamples) + { + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info(), this); + } + } + } +} + + +void CCtrlSamples::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) OnPrevInstrument(); + else if(nButton == XBUTTON2) OnNextInstrument(); + CModControlDlg::OnXButtonUp(nFlags, nButton, point); + SwitchToView(); +} + + +bool CCtrlSamples::IsOPLInstrument() const +{ + return m_nSample >= 1 && m_nSample <= m_sndFile.GetNumSamples() && m_sndFile.GetSample(m_nSample).uFlags[CHN_ADLIB]; +} + + +void CCtrlSamples::OnInitOPLInstrument() +{ + if(m_sndFile.SupportsOPL()) + { + CriticalSection cs; + PrepareUndo("Initialize OPL Instrument", sundo_replace); + m_sndFile.DestroySample(m_nSample); + m_sndFile.InitOPL(); + ModSample &sample = m_sndFile.GetSample(m_nSample); + sample.nC5Speed = 8363; + // Initialize with instant attack, release and enabled sustain for carrier and instant attack for modulator + sample.SetAdlib(true, { 0x00, 0x20, 0x00, 0x00, 0xF0, 0xF0, 0x00, 0x0F, 0x00, 0x00, 0x00, 0x00 }); + SetModified(SampleHint().Info().Data().Names(), true, true); + SwitchToView(); + } +} + + +INSTRUMENTINDEX CCtrlSamples::GetParentInstrumentWithSameName() const +{ + auto ins = m_modDoc.FindSampleParent(m_nSample); + if(ins == INSTRUMENTINDEX_INVALID) + return INSTRUMENTINDEX_INVALID; + auto instr = m_sndFile.Instruments[ins]; + if(instr == nullptr) + return INSTRUMENTINDEX_INVALID; + if((!instr->name.empty() && instr->name != m_sndFile.m_szNames[m_nSample]) || instr->GetSamples().size() != 1) + return INSTRUMENTINDEX_INVALID; + + return ins; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.h b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.h new file mode 100644 index 00000000..7cdf04d0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Ctrl_smp.h @@ -0,0 +1,170 @@ +/* + * Ctrl_smp.h + * ---------- + * Purpose: Sample tab, upper panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/SampleIO.h" +#include "../tracklib/FadeLaws.h" + +OPENMPT_NAMESPACE_BEGIN + +enum OpenSampleTypes +{ + OpenSampleKnown = (1<<0), + OpenSampleRaw = (1<<1), +}; +MPT_DECLARE_ENUM(OpenSampleTypes) + +class CCtrlSamples: public CModControlDlg +{ +protected: + friend class DoPitchShiftTimeStretch; + + struct SampleSelectionPoints + { + SmpLength nStart; + SmpLength nEnd; + bool selectionActive; // does sample selection exist or not? + }; + + CModControlBar m_ToolBar1, m_ToolBar2; + CEdit m_EditSample, m_EditName, m_EditFileName, m_EditFineTune; + CEdit m_EditLoopStart, m_EditLoopEnd, m_EditSustainStart, m_EditSustainEnd; + CEdit m_EditVibSweep, m_EditVibDepth, m_EditVibRate; + CEdit m_EditVolume, m_EditGlobalVol, m_EditPanning; + CSpinButtonCtrl m_SpinVolume, m_SpinGlobalVol, m_SpinPanning, m_SpinVibSweep, m_SpinVibDepth, m_SpinVibRate; + CSpinButtonCtrl m_SpinLoopStart, m_SpinLoopEnd, m_SpinSustainStart, m_SpinSustainEnd; + CSpinButtonCtrl m_SpinFineTune, m_SpinSample; + CSpinButtonCtrl m_SpinSequenceMs, m_SpinSeekWindowMs, m_SpinOverlap, m_SpinStretchAmount; + CComboBox m_ComboAutoVib, m_ComboLoopType, m_ComboSustainType, m_ComboZoom, m_CbnBaseNote; + CButton m_CheckPanning; + double m_dTimeStretchRatio = 100; + uint32 m_nSequenceMs = 0; + uint32 m_nSeekWindowMs = 0; + uint32 m_nOverlapMs = 0; + SAMPLEINDEX m_nSample = 1; + INSTRUMENTINDEX m_editInstrumentName = INSTRUMENTINDEX_INVALID; + bool m_rememberRawFormat = false; + bool m_startedEdit = false; + + CComboBox m_ComboPitch, m_ComboQuality, m_ComboFFT; + + void UpdateTimeStretchParameters(); + void ReadTimeStretchParameters(); + + void ApplyAmplify(const double amp, const double fadeInStart, const double fadeOutEnd, const bool fadeIn, const bool fadeOut, const Fade::Law fadeLaw); + void ApplyResample(SAMPLEINDEX smp, uint32 newRate, ResamplingMode mode, bool ignoreSelection = false, bool updatePatternCommands = false); + + SampleSelectionPoints GetSelectionPoints(); + void SetSelectionPoints(SmpLength nStart, SmpLength nEnd); + + void PropagateAutoVibratoChanges(); + + bool IsOPLInstrument() const; + + INSTRUMENTINDEX GetParentInstrumentWithSameName() const; + +public: + CCtrlSamples(CModControlView &parent, CModDoc &document); + ~CCtrlSamples(); + + bool SetCurrentSample(SAMPLEINDEX nSmp, LONG lZoom = -1, bool bUpdNum = true); + bool InsertSample(bool duplicate, int8 *confirm = nullptr); + bool OpenSample(const mpt::PathString &fileName, FlagSet<OpenSampleTypes> types = OpenSampleKnown | OpenSampleRaw); + bool OpenSample(const CSoundFile &sndFile, SAMPLEINDEX nSample); + void OpenSamples(const std::vector<mpt::PathString> &files, FlagSet<OpenSampleTypes> types); + void SaveSample(bool doBatchSave); + + void Normalize(bool allSamples); + void RemoveDCOffset(bool allSamples); + + Setting<LONG> &GetSplitPosRef() override {return TrackerSettings::Instance().glSampleWindowHeight;} + +public: + //{{AFX_VIRTUAL(CCtrlSamples) + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + CRuntimeClass *GetAssociatedViewClass() override; + void RecalcLayout() override; + void OnActivatePage(LPARAM) override; + void OnDeactivatePage() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + LRESULT OnModCtrlMsg(WPARAM wParam, LPARAM lParam) override; + BOOL GetToolTipText(UINT uId, LPTSTR pszText) override; + BOOL PreTranslateMessage(MSG* pMsg) override; + //}}AFX_VIRTUAL +protected: + //{{AFX_MSG(CCtrlSamples) + afx_msg void OnEditFocus(); + afx_msg void OnSampleChanged(); + afx_msg void OnZoomChanged(); + afx_msg void OnPrevInstrument(); + afx_msg void OnNextInstrument(); + afx_msg void OnTbnDropDownToolBar(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnSampleNew(); + afx_msg void OnSampleDuplicate() { InsertSample(true); } + afx_msg void OnSampleOpen(); + afx_msg void OnSampleOpenKnown(); + afx_msg void OnSampleOpenRaw(); + afx_msg void OnSampleSave(); + afx_msg void OnSampleSaveOne() { SaveSample(false); } + afx_msg void OnSampleSaveAll() { SaveSample(true); } + afx_msg void OnSamplePlay(); + afx_msg void OnNormalize(); + afx_msg void OnAmplify(); + afx_msg void OnQuickFade(); + afx_msg void OnRemoveDCOffset(); + afx_msg void OnResample(); + afx_msg void OnReverse(); + afx_msg void OnSilence(); + afx_msg void OnInvert(); + afx_msg void OnSignUnSign(); + afx_msg void OnAutotune(); + afx_msg void OnNameChanged(); + afx_msg void OnFileNameChanged(); + afx_msg void OnVolumeChanged(); + afx_msg void OnGlobalVolChanged(); + afx_msg void OnSetPanningChanged(); + afx_msg void OnPanningChanged(); + afx_msg void OnFineTuneChanged(); + afx_msg void OnFineTuneChangedDone(); + afx_msg void OnBaseNoteChanged(); + afx_msg void OnLoopTypeChanged(); + afx_msg void OnLoopPointsChanged(); + afx_msg void OnSustainTypeChanged(); + afx_msg void OnSustainPointsChanged(); + afx_msg void OnVibTypeChanged(); + afx_msg void OnVibDepthChanged(); + afx_msg void OnVibSweepChanged(); + afx_msg void OnVibRateChanged(); + afx_msg void OnXFade(); + afx_msg void OnStereoSeparation(); + afx_msg void OnKeepSampleOnDisk(); + afx_msg void OnVScroll(UINT, UINT, CScrollBar *); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + + afx_msg void OnPitchShiftTimeStretch(); + afx_msg void OnEnableStretchToSize(); + afx_msg void OnEstimateSampleSize(); + + afx_msg void OnInitOPLInstrument(); + + MPT_NOINLINE void SetModified(SampleHint hint, bool updateAll, bool waveformModified); + void PrepareUndo(const char *description, sampleUndoTypes type = sundo_none, SmpLength start = 0, SmpLength end = 0); + + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.cpp new file mode 100644 index 00000000..1784a217 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.cpp @@ -0,0 +1,500 @@ +/* + * DefaultVstEditor.cpp + * -------------------- + * Purpose: Implementation of the default plugin editor that is used if a plugin does not provide an own editor GUI. + * 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 "DefaultVstEditor.h" +#include "../soundlib/Sndfile.h" +#include "../soundlib/plugins/PlugInterface.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#ifndef NO_PLUGINS + + +// Window proportions +struct Measurements +{ + enum + { + edSpacing = 5, // Spacing between elements + edLineHeight = 20, // Line of a single parameter line + edEditWidth = 45, // Width of the parameter edit box + edPerMilWidth = 30, // Width of the per mil label + edRightWidth = edEditWidth + edSpacing + edPerMilWidth, // Width of the right part of a parameter control set (edit box, param value) + edTotalHeight = 2 * edLineHeight + edSpacing, // Height of one set of controls + }; + + const int spacing; + const int lineHeight; + const int editWidth; + const int perMilWidth; + const int rightWidth; + const int totalHeight; + + Measurements(HWND hWnd) + : spacing(Util::ScalePixels(edSpacing, hWnd)) + , lineHeight(Util::ScalePixels(edLineHeight, hWnd)) + , editWidth(Util::ScalePixels(edEditWidth, hWnd)) + , perMilWidth(Util::ScalePixels(edPerMilWidth, hWnd)) + , rightWidth(Util::ScalePixels(edRightWidth, hWnd)) + , totalHeight(Util::ScalePixels(edTotalHeight, hWnd)) + { } +}; + + +// Create a set of parameter controls +ParamControlSet::ParamControlSet(CWnd *parent, const CRect &rect, int setID, const Measurements &m) +{ + // Offset of components on the right side + const int horizSplit = rect.left + rect.Width() - m.rightWidth; + + // Parameter name + nameLabel.Create(_T(""), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(rect.left, rect.top, horizSplit - m.spacing, rect.top + m.lineHeight), parent); + nameLabel.SetFont(parent->GetFont()); + + // Parameter value as reported by the plugin + valueLabel.Create(_T(""), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(horizSplit, rect.top, rect.right, rect.top + m.lineHeight), parent); + valueLabel.SetFont(parent->GetFont()); + + // Parameter value slider + valueSlider.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP /* | TBS_NOTICKS | TBS_BOTH */ | TBS_AUTOTICKS, CRect(rect.left, rect.bottom - m.lineHeight, horizSplit - m.spacing, rect.bottom), parent, ID_PLUGINEDITOR_SLIDERS_BASE + setID); + valueSlider.SetFont(parent->GetFont()); + valueSlider.SetRange(0, PARAM_RESOLUTION); + valueSlider.SetTicFreq(PARAM_RESOLUTION / 10); + + // Parameter value edit box + valueEdit.CreateEx(WS_EX_CLIENTEDGE, _T("EDIT"), NULL, WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER | ES_AUTOHSCROLL | ES_NUMBER, CRect(horizSplit, rect.bottom - m.lineHeight, horizSplit + m.editWidth, rect.bottom), parent, ID_PLUGINEDITOR_EDIT_BASE + setID); + valueEdit.SetFont(parent->GetFont()); + + // "Per mil" label + perMilLabel.Create(mpt::ToCString(mpt::Charset::UTF8, "\xE2\x80\xB0"), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(horizSplit + m.editWidth + m.spacing, rect.bottom - m.lineHeight, rect.right, rect.bottom), parent); + perMilLabel.SetFont(parent->GetFont()); +} + + +ParamControlSet::~ParamControlSet() +{ + nameLabel.DestroyWindow(); + valueLabel.DestroyWindow(); + valueSlider.DestroyWindow(); + valueEdit.DestroyWindow(); + perMilLabel.DestroyWindow(); +} + + +// Enable a set of parameter controls +void ParamControlSet::EnableControls(bool enable) +{ + const BOOL b = enable ? TRUE : FALSE; + nameLabel.EnableWindow(b); + valueLabel.EnableWindow(b); + valueSlider.EnableWindow(b); + valueEdit.EnableWindow(b); + perMilLabel.EnableWindow(b); +} + + +// Reset the content of a set of parameter controls +void ParamControlSet::ResetContent() +{ + nameLabel.SetWindowText(_T("")); + valueLabel.SetWindowText(_T("")); + valueSlider.SetPos(0); + valueEdit.SetWindowText(_T("")); +} + + +void ParamControlSet::SetParamName(const CString &name) +{ + nameLabel.SetWindowText(name); +} + + +void ParamControlSet::SetParamValue(int value, const CString &text) +{ + valueSlider.SetPos(value); + if (&valueEdit != valueEdit.GetFocus()) + { + // Don't update textbox when it has focus, else this will prevent user from changing the content. + CString paramValue; + paramValue.Format(_T("%0000d"), value); + valueEdit.SetWindowText(paramValue); + } + valueLabel.SetWindowText(text); +} + + +int ParamControlSet::GetParamValueFromSlider() const +{ + return valueSlider.GetPos(); +} + + +int ParamControlSet::GetParamValueFromEdit() const +{ + TCHAR s[16]; + valueEdit.GetWindowText(s, 16); + int val = _tstoi(s); + Limit(val, int(0), int(PARAM_RESOLUTION)); + return val; +} + + +BEGIN_MESSAGE_MAP(CDefaultVstEditor, CAbstractVstEditor) + //{{AFX_MSG_MAP(CDefaultVstEditor) + ON_CONTROL_RANGE(EN_CHANGE, ID_PLUGINEDITOR_EDIT_BASE, ID_PLUGINEDITOR_EDIT_BASE + NUM_PLUGINEDITOR_PARAMETERS - 1, &CDefaultVstEditor::OnParamTextboxChanged) + //}}AFX_MSG_MAP + ON_WM_HSCROLL() + ON_WM_VSCROLL() + ON_WM_MOUSEWHEEL() +END_MESSAGE_MAP() + + +void CDefaultVstEditor::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CDefaultVstEditor) + DDX_Control(pDX, IDC_SCROLLBAR1, paramScroller); + //}}AFX_DATA_MAP +} + + +CDefaultVstEditor::CDefaultVstEditor(IMixPlugin &plugin) : CAbstractVstEditor(plugin) +{ + m_nControlLock = 0; + paramOffset = 0; + + controls.clear(); +} + +CDefaultVstEditor::~CDefaultVstEditor() +{ + for(size_t i = 0; i < controls.size(); i++) + { + delete controls[i]; + } +} + + +void CDefaultVstEditor::CreateControls() +{ + // Already initialized. + if(!controls.empty()) + { + return; + } + Measurements m(m_hWnd); + + CRect window; + GetWindowRect(&window); + + CRect rect; + GetClientRect(&rect); + int origHeight = rect.bottom; + + // Get parameter scroll bar dimensions and move it to the right side of the window + CRect scrollRect; + paramScroller.GetClientRect(&scrollRect); + scrollRect.bottom = rect.bottom; + scrollRect.MoveToX(rect.right - scrollRect.right); + + // Ignore this space in our calculation from now on. + rect.right -= scrollRect.Width(); + + controls.clear(); + + // Create a bit of border space + rect.DeflateRect(m.spacing, m.spacing); + rect.bottom = m.totalHeight; + + for(int i = 0; i < NUM_PLUGINEDITOR_PARAMETERS; i++) + { + try + { + controls.push_back(new ParamControlSet(this, rect, i, m)); + rect.OffsetRect(0, m.totalHeight + m.spacing); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + + // Calculate new ideal window height. + const int heightChange = (rect.bottom - m.totalHeight + m.spacing) - origHeight; + + // Update parameter scroll bar height + scrollRect.bottom += heightChange; + paramScroller.MoveWindow(scrollRect, FALSE); + + // Resize window height + window.bottom += heightChange; + MoveWindow(&window); + + // Set scrollbar page size. + SCROLLINFO sbInfo; + paramScroller.GetScrollInfo(&sbInfo); + sbInfo.nPage = NUM_PLUGINEDITOR_PARAMETERS; + paramScroller.SetScrollInfo(&sbInfo); + + UpdateControls(true); +} + + +void CDefaultVstEditor::UpdateControls(bool updateParamNames) +{ + const PlugParamIndex numParams = m_VstPlugin.GetNumParameters(); + const PlugParamIndex scrollMax = numParams - std::min(numParams, static_cast<PlugParamIndex>(NUM_PLUGINEDITOR_PARAMETERS)); + LimitMax(paramOffset, scrollMax); + + int curScrollMin, curScrollMax; + paramScroller.GetScrollRange(&curScrollMin, &curScrollMax); + if(static_cast<PlugParamIndex>(curScrollMax) != scrollMax) + { + // Number of parameters changed - update scrollbar limits + paramScroller.SetScrollRange(0, scrollMax); + paramScroller.SetScrollPos(paramOffset); + paramScroller.EnableWindow(scrollMax > 0 ? TRUE : FALSE); + updateParamNames = true; + } + + m_VstPlugin.CacheParameterNames(paramOffset, std::min(paramOffset + NUM_PLUGINEDITOR_PARAMETERS, numParams)); + for(PlugParamIndex i = 0; i < NUM_PLUGINEDITOR_PARAMETERS; i++) + { + const PlugParamIndex param = paramOffset + i; + + if(param >= numParams) + { + // This param doesn't exist. + controls[i]->EnableControls(false); + controls[i]->ResetContent(); + continue; + } + + controls[i]->EnableControls(); + + if(updateParamNames) + { + // Update param name + controls[i]->SetParamName(m_VstPlugin.GetFormattedParamName(param)); + } + + UpdateParamDisplay(param); + } +} + + +void CDefaultVstEditor::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + CSliderCtrl* pScrolledSlider = reinterpret_cast<CSliderCtrl*>(pScrollBar); + // Check if any of the value sliders were affected. + for(size_t i = 0; i < controls.size(); i++) + { + if ((pScrolledSlider->GetDlgCtrlID() == controls[i]->GetSliderID()) && (nSBCode != SB_ENDSCROLL)) + { + OnParamSliderChanged(controls[i]->GetSliderID()); + break; + } + } + + CAbstractVstEditor::OnHScroll(nSBCode, nPos, pScrollBar); +} + + +void CDefaultVstEditor::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + if (pScrollBar == ¶mScroller) + { + // Get the minimum and maximum scrollbar positions. + int minpos; + int maxpos; + pScrollBar->GetScrollRange(&minpos, &maxpos); + //maxpos = pScrollBar->GetScrollLimit(); + + SCROLLINFO sbInfo; + paramScroller.GetScrollInfo(&sbInfo); + + // Get the current position of scroll box. + int curpos = pScrollBar->GetScrollPos(); + + // Determine the new position of scroll box. + switch(nSBCode) + { + case SB_LEFT: // Scroll to far left. + curpos = minpos; + break; + + case SB_RIGHT: // Scroll to far right. + curpos = maxpos; + break; + + case SB_ENDSCROLL: // End scroll. + break; + + case SB_LINELEFT: // Scroll left. + if(curpos > minpos) + curpos--; + break; + + case SB_LINERIGHT: // Scroll right. + if(curpos < maxpos) + curpos++; + break; + + case SB_PAGELEFT: // Scroll one page left. + if(curpos > minpos) + { + curpos = std::max(minpos, curpos - static_cast<int>(sbInfo.nPage)); + } + break; + + case SB_PAGERIGHT: // Scroll one page right. + if(curpos < maxpos) + { + curpos = std::min(maxpos, curpos + static_cast<int>(sbInfo.nPage)); + } + break; + + case SB_THUMBPOSITION: // Scroll to absolute position. nPos is the position + curpos = nPos; // of the scroll box at the end of the drag operation. + break; + + case SB_THUMBTRACK: // Drag scroll box to specified position. nPos is the + curpos = nPos; // position that the scroll box has been dragged to. + break; + } + + // Set the new position of the thumb (scroll box). + pScrollBar->SetScrollPos(curpos); + + paramOffset = curpos; + UpdateControls(true); + } + + CAbstractVstEditor::OnVScroll(nSBCode, nPos, pScrollBar); +} + + +BOOL CDefaultVstEditor::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + MPT_UNREFERENCED_PARAMETER(nFlags); + MPT_UNREFERENCED_PARAMETER(pt); + + // Mouse wheel - scroll parameter list + int minpos, maxpos; + paramScroller.GetScrollRange(&minpos, &maxpos); + if(minpos != maxpos) + { + paramOffset -= mpt::signum(zDelta); + Limit(paramOffset, PlugParamIndex(minpos), PlugParamIndex(maxpos)); + paramScroller.SetScrollPos(paramOffset); + + UpdateControls(true); + } + + return CAbstractVstEditor::OnMouseWheel(nFlags, zDelta, pt); +} + + +bool CDefaultVstEditor::OpenEditor(CWnd *parent) +{ + Create(IDD_DEFAULTPLUGINEDITOR, parent); + CreateControls(); + return CAbstractVstEditor::OpenEditor(parent); +} + + +// Called when a change occurs to the parameter textbox +// If the change is triggered by the user, we'll need to notify the plugin and update +// the other GUI controls +void CDefaultVstEditor::OnParamTextboxChanged(UINT id) +{ + if (m_nControlLock) + { + // Lock will be set if the GUI change was triggered internally (in UpdateParamDisplays). + // We're only interested in handling changes triggered by the user. + return; + } + + const PlugParamIndex param = paramOffset + id - ID_PLUGINEDITOR_EDIT_BASE; + + // Extract value and update + SetParam(param, controls[param - paramOffset]->GetParamValueFromEdit()); +} + + +// Called when a change occurs to the parameter slider +// If the change is triggered by the user, we'll need to notify the plugin and update +// the other GUI controls +void CDefaultVstEditor::OnParamSliderChanged(UINT id) +{ + if (m_nControlLock) + { + // Lock will be set if the GUI change was triggered internally (in UpdateParamDisplays). + // We're only interested in handling changes triggered by the user. + return; + } + + const PlugParamIndex param = paramOffset + id - ID_PLUGINEDITOR_SLIDERS_BASE; + + // Extract value and update + SetParam(param, controls[param - paramOffset]->GetParamValueFromSlider()); + +} + + +// Update a given parameter to a given value and notify plugin +void CDefaultVstEditor::SetParam(PlugParamIndex param, int value) +{ + if(param >= m_VstPlugin.GetNumParameters()) + { + return; + } + + m_VstPlugin.SetScaledUIParam(param, static_cast<PlugParamValue>(value) / static_cast<PlugParamValue>(PARAM_RESOLUTION)); + + // Update other GUI controls + UpdateParamDisplay(param); + + // Act as if an automation message has been sent by the plugin (record param changes, set document modified, etc...) + m_VstPlugin.AutomateParameter(param); + +} + + +//Update all GUI controls with the new param value +void CDefaultVstEditor::UpdateParamDisplay(PlugParamIndex param) +{ + if(m_nControlLock || param < paramOffset || param >= paramOffset + NUM_PLUGINEDITOR_PARAMETERS) + { + //Just to make sure we're not here as a consequence of an internal GUI change, and avoid modifying a parameter that doesn't exist on the GUI. + return; + } + + // Get the actual parameter value from the plugin + const int val = static_cast<int>(m_VstPlugin.GetScaledUIParam(param) * static_cast<float>(PARAM_RESOLUTION) + 0.5f); + + // Update the GUI controls + + // Set lock to indicate that the changes to the GUI are internal - no need to notify the plug and re-update GUI. + m_nControlLock++; + + controls[param - paramOffset]->SetParamValue(val, m_VstPlugin.GetFormattedParamValue(param)); + + // Unset lock - done with internal GUI updates. + m_nControlLock--; + +} + +#endif // NO_PLUGINS + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.h b/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.h new file mode 100644 index 00000000..fe5ca587 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/DefaultVstEditor.h @@ -0,0 +1,104 @@ +/* + * DefaultVstEditor.h + * ------------------ + * Purpose: Implementation of the default plugin editor that is used if a plugin does not provide an own editor GUI. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "Mptrack.h" +#include "AbstractVstEditor.h" + +OPENMPT_NAMESPACE_BEGIN + +enum +{ + PARAM_RESOLUTION = 1000, + NUM_PLUGINEDITOR_PARAMETERS = 8, // Parameters on screen +}; + +struct Measurements; + +class ParamControlSet +{ +protected: + CSliderCtrl valueSlider; + CEdit valueEdit; + CStatic nameLabel; + CStatic valueLabel; + CStatic perMilLabel; + +public: + ParamControlSet(CWnd *parent, const CRect &rect, int setID, const Measurements &m); + ~ParamControlSet(); + + void EnableControls(bool enable = true); + void ResetContent(); + + void SetParamName(const CString &name); + void SetParamValue(int value, const CString &text); + + int GetParamValueFromSlider() const; + int GetParamValueFromEdit() const; + + int GetSliderID() const { return valueSlider.GetDlgCtrlID(); }; +}; + + +class CDefaultVstEditor : public CAbstractVstEditor +{ +protected: + + std::vector<ParamControlSet *> controls; + + CScrollBar paramScroller; + PlugParamIndex paramOffset; + + int m_nControlLock; + +public: + + CDefaultVstEditor(IMixPlugin &plugin); + virtual ~CDefaultVstEditor(); + + virtual void UpdateParamDisplays() { CAbstractVstEditor::UpdateParamDisplays(); UpdateControls(false); }; + + virtual bool OpenEditor(CWnd *parent); + + // Plugins may not request to change the GUI size, since we use our own GUI. + virtual bool IsResizable() const { return false; }; + virtual bool SetSize(int, int) { return false; }; + +protected: + + virtual void DoDataExchange(CDataExchange* pDX); + + DECLARE_MESSAGE_MAP() + + afx_msg void OnParamTextboxChanged(UINT id); + afx_msg void OnParamSliderChanged(UINT id); + + afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + +protected: + + void CreateControls(); + void UpdateControls(bool updateParamNames); + void SetParam(PlugParamIndex param, int value); + void UpdateParamDisplay(PlugParamIndex param); + +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Draw_pat.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Draw_pat.cpp new file mode 100644 index 00000000..bd4d4772 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Draw_pat.cpp @@ -0,0 +1,1828 @@ +/* + * draw_pat.cpp + * ------------ + * Purpose: Code for drawing the pattern data. + * Notes : Also used for updating the status bar. + * Authors: Olivier Lapicque + * 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 "Moddoc.h" +#include "dlg_misc.h" +#include "Globals.h" +#include "View_pat.h" +#include "EffectVis.h" +#include "ChannelManagerDlg.h" +#include "../soundlib/tuning.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/Tables.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../common/mptStringBuffer.h" +#include "EffectInfo.h" +#include "PatternFont.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Headers +enum +{ + ROWHDR_WIDTH = 32, // Row header + COLHDR_HEIGHT = 16 + 4, // Column header (name + color) + VUMETERS_HEIGHT = 13, // Height of vu-meters + PLUGNAME_HEIGHT = 16, // Height of plugin names + VUMETERS_BMPWIDTH = 32, + VUMETERS_BMPHEIGHT = 10, + VUMETERS_MEDWIDTH = 24, + VUMETERS_LOWIDTH = 16, +}; + +enum +{ + COLUMN_BITS_NONE = 0x00, + COLUMN_BITS_NOTE = 0x01, + COLUMN_BITS_INSTRUMENT = 0x02, + COLUMN_BITS_VOLUME = 0x04, + COLUMN_BITS_FXCMD = 0x08, + COLUMN_BITS_FXPARAM = 0x10, + COLUMN_BITS_FXCMDANDPARAM = 0x18, + COLUMN_BITS_ALLCOLUMNS = 0x1F, + COLUMN_BITS_UNKNOWN = 0x20, // Appears to be unused + COLUMN_BITS_ALL = 0x3F, + COLUMN_BITS_SKIP = 0x40, + COLUMN_BITS_INVISIBLE = 0x80, +}; + + + + + +///////////////////////////////////////////////////////////////////////////// +// Effect colour codes + +// EffectType => ModColor mapping +static constexpr int effectColors[] = +{ + 0, + MODCOLOR_GLOBALS, + MODCOLOR_VOLUME, + MODCOLOR_PANNING, + MODCOLOR_PITCH, +}; + +static_assert(std::size(effectColors) == MAX_EFFECT_TYPE); + +///////////////////////////////////////////////////////////////////////////// +// CViewPattern Drawing Implementation + +static uint8 HighlightColor(int c0, int c1) +{ + int cf0 = 0xC0 - (c1 >> 2) - (c0 >> 3); + Limit(cf0, 0x40, 0xC0); + int cf1 = 0x100 - cf0; + return static_cast<uint8>((c0 * cf0 + c1 * cf1) >> 8); +} + + +static void MixColors(CFastBitmap &dib, ModColor target, ModColor src1, ModColor src2) +{ + const auto c1 = TrackerSettings::Instance().rgbCustomColors[src1], c2 = TrackerSettings::Instance().rgbCustomColors[src2]; + auto r = HighlightColor(GetRValue(c1), GetRValue(c2)); + auto g = HighlightColor(GetGValue(c1), GetGValue(c2)); + auto b = HighlightColor(GetBValue(c1), GetBValue(c2)); + dib.SetColor(target, RGB(r, g, b)); +} + + +void CViewPattern::UpdateColors() +{ + m_Dib.SetAllColors(0, MAX_MODCOLORS, TrackerSettings::Instance().rgbCustomColors.data()); + MixColors(m_Dib, MODCOLOR_2NDHIGHLIGHT, MODCOLOR_BACKHILIGHT, MODCOLOR_BACKNORMAL); + MixColors(m_Dib, MODCOLOR_DEFAULTVOLUME, MODCOLOR_VOLUME, MODCOLOR_BACKNORMAL); + MixColors(m_Dib, MODCOLOR_DUMMYCOMMAND, MODCOLOR_TEXTNORMAL, MODCOLOR_BACKNORMAL); + m_Dib.SetBlendColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BLENDCOLOR]); +} + + +bool CViewPattern::UpdateSizes() +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + int oldx = m_szCell.cx, oldy = m_szCell.cy; + m_szHeader.cx = ROWHDR_WIDTH; + m_szHeader.cy = COLHDR_HEIGHT; + m_szPluginHeader.cx = 0; + m_szPluginHeader.cy = m_Status[psShowPluginNames] ? MulDiv(PLUGNAME_HEIGHT, m_nDPIy, 96) : 0; + if(m_Status[psShowVUMeters]) m_szHeader.cy += VUMETERS_HEIGHT; + m_szCell.cx = 4 + pfnt->nEltWidths[0]; + if (m_nDetailLevel >= PatternCursor::instrColumn) m_szCell.cx += pfnt->nEltWidths[1]; + if (m_nDetailLevel >= PatternCursor::volumeColumn) m_szCell.cx += pfnt->nEltWidths[2]; + if (m_nDetailLevel >= PatternCursor::effectColumn) m_szCell.cx += pfnt->nEltWidths[3] + pfnt->nEltWidths[4]; + m_szCell.cy = pfnt->nHeight; + + m_szHeader.cx = MulDiv(m_szHeader.cx, m_nDPIx, 96); + m_szHeader.cy = MulDiv(m_szHeader.cy, m_nDPIy, 96); + m_szHeader.cy += m_szPluginHeader.cy; + + if(oldy != m_szCell.cy) + { + m_Dib.SetSize(m_Dib.GetWidth(), m_szCell.cy); + } + + return (oldx != m_szCell.cx || oldy != m_szCell.cy); +} + + +UINT CViewPattern::GetColumnOffset(PatternCursor::Columns column) const +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + LimitMax(column, PatternCursor::lastColumn); + UINT offset = 0; + for(int i = PatternCursor::firstColumn; i < column; i++) + offset += pfnt->nEltWidths[i]; + return offset; +} + + +int CViewPattern::GetSmoothScrollOffset() const +{ + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL) != 0 // Actually using the smooth scroll feature + && (m_Status & (psFollowSong | psDragActive)) == psFollowSong // Not drawing a selection during playback + && (m_nMidRow != 0 || GetYScrollPos() > 0) // If active row is not centered, only scroll when display position is actually not at the top + && IsLiveRecord() // Actually playing live (not paused or stepping) + && m_nNextPlayRow != m_nPlayRow) // Don't scroll if we stay on the same row + { + uint32 tick = m_nPlayTick; + // Avoid jerky animation with backwards-going patterns + if(m_nNextPlayRow == m_nPlayRow - 1) tick = m_nTicksOnRow - m_nPlayTick - 1; + return Util::muldivr_unsigned(m_szCell.cy, tick, std::max(1u, m_nTicksOnRow)); + } + return 0; +} + + +void CViewPattern::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj == this) + { + return; + } + if(hint.GetType()[HINT_MPTOPTIONS]) + { + PatternFont::UpdateFont(m_hWnd); + UpdateColors(); + UpdateSizes(); + UpdateScrollSize(); + InvalidatePattern(true, true); + return; + } + const auto generalHint = hint.ToType<GeneralHint>(); + if(generalHint.GetType()[HINT_MODTYPE | HINT_MODCHANNELS]) + { + InvalidateChannelsHeaders(); + UpdateScrollSize(); + } + if(generalHint.GetType()[HINT_MODTYPE]) + { + // If sequence and pattern view became inconsistent (e.g. due to rearranging patterns during cleanup), synchronize to order list again + const auto &order = Order(); + const ORDERINDEX ord = GetCurrentOrder(); + if(order.IsValidPat(ord) && order.at(ord) != m_nPattern) + SetCurrentPattern(order.at(ord)); + } + if(generalHint.GetType()[HINT_MODCHANNELS] + && m_quickChannelProperties.m_hWnd + && pObj != &m_quickChannelProperties + && (generalHint.GetChannel() >= GetDocument()->GetNumChannels() || generalHint.GetChannel() == m_quickChannelProperties.GetChannel())) + { + m_quickChannelProperties.UpdateDisplay(); + } + + const PatternHint patternHint = hint.ToType<PatternHint>(); + const PATTERNINDEX updatePat = patternHint.GetPattern(); + if(hint.GetType() == HINT_PATTERNDATA + && m_nPattern != updatePat + && updatePat != 0 + && updatePat != GetNextPattern() + && updatePat != GetPrevPattern()) + return; + + if(patternHint.GetType()[HINT_MODTYPE | HINT_PATTERNDATA]) + { + InvalidatePattern(false, true); + } else if(patternHint.GetType()[HINT_PATTERNROW]) + { + InvalidateRow(static_cast<const RowHint &>(hint).GetRow()); + } + +} + + +POINT CViewPattern::GetPointFromPosition(PatternCursor cursor) const +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + POINT pt; + int xofs = GetXScrollPos(); + int yofs = GetYScrollPos(); + + PatternCursor::Columns imax = cursor.GetColumnType(); + LimitMax(imax, PatternCursor::lastColumn); +// if(imax > m_nDetailLevel) +// { +// // Extend to next channel +// imax = PatternCursor::firstColumn; +// cursor.Move(0, 1, 0); +// } + + pt.x = (cursor.GetChannel() - xofs) * GetChannelWidth(); + + for(int i = 0; i < imax; i++) + { + pt.x += pfnt->nEltWidths[i]; + } + + if (pt.x < 0) pt.x = 0; + pt.x += Util::ScalePixels(ROWHDR_WIDTH, m_hWnd); + pt.y = (cursor.GetRow() - yofs + m_nMidRow) * m_szCell.cy; + + if (pt.y < 0) pt.y = 0; + pt.y += m_szHeader.cy; + + return pt; +} + + +PatternCursor CViewPattern::GetPositionFromPoint(POINT pt) const +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + int xofs = GetXScrollPos(); + int yofs = GetYScrollPos(); + int x = xofs + (pt.x - m_szHeader.cx) / GetChannelWidth(); + if (pt.x < m_szHeader.cx) x = (xofs) ? xofs - 1 : 0; + + int y = yofs - m_nMidRow + (pt.y - m_szHeader.cy + GetSmoothScrollOffset()) / m_szCell.cy; + if (y < 0) y = 0; + int xx = (pt.x - m_szHeader.cx) % GetChannelWidth(), dx = 0; + int imax = 4; + if (imax > (int)m_nDetailLevel + 1) imax = m_nDetailLevel + 1; + int i = 0; + for (i=0; i<imax; i++) + { + dx += pfnt->nEltWidths[i]; + if(xx < dx) + break; + } + return PatternCursor(static_cast<ROWINDEX>(y), static_cast<CHANNELINDEX>(x), static_cast<PatternCursor::Columns>(i)); +} + + +void CViewPattern::DrawLetter(int x, int y, char letter, int sizex, int ofsx) +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + + if(pfnt->dibASCII) + { + if(32 <= letter && letter <= 127) + { + m_Dib.TextBlt(x, y, sizex, pfnt->spacingY, (((unsigned char)letter) * pfnt->nNoteWidth[0]) + ofsx, 0, pfnt->dibASCII); + return; + } + } + + int srcx = pfnt->nSpaceX, srcy = pfnt->nSpaceY; + + if ((letter >= '0') && (letter <= '9')) + { + srcx = pfnt->nNumX; + srcy = pfnt->nNumY + (letter - '0') * pfnt->spacingY; + } else + if ((letter >= 'A') && (letter < 'N')) + { + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + (letter - 'A') * pfnt->spacingY; + } else + if ((letter >= 'N') && (letter <= 'Z')) + { + srcx = pfnt->nAlphaNZ_X; + srcy = pfnt->nAlphaNZ_Y + (letter - 'N') * pfnt->spacingY; + } else + switch(letter) + { + case '?': + srcx = pfnt->nAlphaNZ_X; + srcy = pfnt->nAlphaNZ_Y + 13 * pfnt->spacingY; + break; + case '#': + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + 13 * pfnt->spacingY; + break; + case '\\': + srcx = pfnt->nAlphaNZ_X; + srcy = pfnt->nAlphaNZ_Y + 14 * pfnt->spacingY; + break; + case ':': + srcx = pfnt->nAlphaNZ_X; + srcy = pfnt->nAlphaNZ_Y + 15 * pfnt->spacingY; + break; + case '*': + srcx = pfnt->nAlphaNZ_X; + srcy = pfnt->nAlphaNZ_Y + 16 * pfnt->spacingY; + break; + case ' ': + srcx = pfnt->nSpaceX; + srcy = pfnt->nSpaceY; + break; + case 'b': + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + 14 * pfnt->spacingY; + break; + case '-': + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + 15 * pfnt->spacingY; + break; + case '+': + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + 16 * pfnt->spacingY; + break; + case 'd': + srcx = pfnt->nAlphaAM_X; + srcy = pfnt->nAlphaAM_Y + 17 * pfnt->spacingY; + break; + case '.': + srcx = pfnt->nNoteX; + srcy = pfnt->nNoteY; + break; + } + m_Dib.TextBlt(x, y, sizex, pfnt->spacingY, srcx+ofsx, srcy, pfnt->dib); +} + +void CViewPattern::DrawLetter(int x, int y, wchar_t letter, int sizex, int ofsx) +{ + DrawLetter(x, y, mpt::unsafe_char_convert<char>(letter), sizex, ofsx); +} + +#if MPT_CXX_AT_LEAST(20) +void CViewPattern::DrawLetter(int x, int y, char8_t letter, int sizex, int ofsx) +{ + DrawLetter(x, y, mpt::unsafe_char_convert<char>(letter), sizex, ofsx); +} +#endif + +static MPT_FORCEINLINE void DrawPadding(CFastBitmap &dib, const PATTERNFONT *pfnt, int x, int y, int col) +{ + if(pfnt->padding[col]) + dib.TextBlt(x + pfnt->nEltWidths[col] - pfnt->padding[col], y, pfnt->padding[col], pfnt->spacingY, pfnt->nClrX + pfnt->nEltWidths[col] - pfnt->padding[col], pfnt->nClrY, pfnt->dib); +} + +void CViewPattern::DrawNote(int x, int y, UINT note, CTuning* pTuning) +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + + UINT xsrc = pfnt->nNoteX, ysrc = pfnt->nNoteY, dx = pfnt->nEltWidths[0]; + if (!note) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc, pfnt->dib); + } else + if (note == NOTE_NOTECUT) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc + 13*pfnt->spacingY, pfnt->dib); + } else + if (note == NOTE_KEYOFF) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc + 14*pfnt->spacingY, pfnt->dib); + } else + if(note == NOTE_FADE) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc + 17*pfnt->spacingY, pfnt->dib); + } else + if(note == NOTE_PC) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc + 15*pfnt->spacingY, pfnt->dib); + } else + if(note == NOTE_PCS) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, xsrc, ysrc + 16*pfnt->spacingY, pfnt->dib); + } else + { + if(pTuning) + { + // Drawing custom note names + std::wstring noteStr = mpt::ToWide(pTuning->GetNoteName(static_cast<Tuning::NOTEINDEXTYPE>(note - NOTE_MIDDLEC))); + if(noteStr.size() < 3) + noteStr.resize(3, L' '); + + DrawLetter(x, y, noteStr[0], pfnt->nNoteWidth[0], 0); + DrawLetter(x + pfnt->nNoteWidth[0], y, noteStr[1], pfnt->nNoteWidth[1], 0); + DrawLetter(x + pfnt->nNoteWidth[0] + pfnt->nNoteWidth[1], y, noteStr[2], pfnt->nOctaveWidth, 0); + } else + { + // Original + UINT o = (note - NOTE_MIN) / 12; //Octave + UINT n = (note - NOTE_MIN) % 12; //Note + + // Hack for default pattern font, allowing for sharps + if(TrackerSettings::Instance().accidentalFlats) + { + DrawLetter(x, y, NoteNamesFlat[n][0], pfnt->nNoteWidth[0], 0); + DrawLetter(x + pfnt->nNoteWidth[0], y, NoteNamesFlat[n][1], pfnt->nNoteWidth[1], 0); + } else + { + m_Dib.TextBlt(x, y, pfnt->nNoteWidth[0] + pfnt->nNoteWidth[1], pfnt->spacingY, xsrc, ysrc+(n+1)*pfnt->spacingY, pfnt->dib); + } + + if(o <= 15) + m_Dib.TextBlt(x + pfnt->nNoteWidth[0] + pfnt->nNoteWidth[1], y, pfnt->nOctaveWidth, pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY+o*pfnt->spacingY, pfnt->dib); + else + DrawLetter(x + pfnt->nNoteWidth[0] + pfnt->nNoteWidth[1], y, '?', pfnt->nOctaveWidth); + } + } + DrawPadding(m_Dib, pfnt, x, y, 0); +} + + +void CViewPattern::DrawInstrument(int x, int y, UINT instr) +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + if (instr) + { + UINT dx = pfnt->nInstrHiWidth; + if (instr < 100) + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, pfnt->nNumX+pfnt->nInstrOfs, pfnt->nNumY+(instr / 10)*pfnt->spacingY, pfnt->dib); + } else + { + m_Dib.TextBlt(x, y, dx, pfnt->spacingY, pfnt->nNum10X+pfnt->nInstr10Ofs, pfnt->nNum10Y+((instr-100) / 10)*pfnt->spacingY, pfnt->dib); + } + m_Dib.TextBlt(x+dx, y, pfnt->nEltWidths[1]-dx, pfnt->spacingY, pfnt->nNumX+pfnt->paramLoMargin, pfnt->nNumY+(instr % 10)*pfnt->spacingY, pfnt->dib); + } else + { + m_Dib.TextBlt(x, y, pfnt->nEltWidths[1], pfnt->spacingY, pfnt->nClrX+pfnt->nEltWidths[0], pfnt->nClrY, pfnt->dib); + } + DrawPadding(m_Dib, pfnt, x, y, 1); +} + + +void CViewPattern::DrawVolumeCommand(int x, int y, const ModCommand &mc, bool drawDefaultVolume) +{ + const PATTERNFONT *pfnt = PatternFont::currentFont; + + if(mc.IsPcNote()) + { + //If note is parameter control note, drawing volume command differently. + const int val = std::min(ModCommand::maxColumnValue, static_cast<int>(mc.GetValueVolCol())); + + if(pfnt->pcParamMargin) m_Dib.TextBlt(x, y, pfnt->pcParamMargin, pfnt->spacingY, pfnt->nClrX, pfnt->nClrY, pfnt->dib); + m_Dib.TextBlt(x + pfnt->pcParamMargin, y, pfnt->nVolCmdWidth, pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY+(val / 100)*pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(x+pfnt->nVolCmdWidth, y, pfnt->nVolHiWidth, pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY+((val / 10)%10)*pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(x+pfnt->nVolCmdWidth+pfnt->nVolHiWidth, y, pfnt->nEltWidths[2]-(pfnt->nVolCmdWidth+pfnt->nVolHiWidth), pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY+(val % 10)*pfnt->spacingY, pfnt->dib); + } else + { + ModCommand::VOLCMD volcmd = mc.volcmd; + int vol = (mc.vol & 0x7F); + + if(drawDefaultVolume) + { + // Displaying sample default volume if there is no volume command. + volcmd = VOLCMD_VOLUME; + vol = GetDefaultVolume(mc); + } + + if(volcmd != VOLCMD_NONE && volcmd < MAX_VOLCMDS) + { + m_Dib.TextBlt(x, y, pfnt->nVolCmdWidth, pfnt->spacingY, + pfnt->nVolX, pfnt->nVolY + volcmd * pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(x+pfnt->nVolCmdWidth, y, pfnt->nVolHiWidth, pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY + (vol / 10) * pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(x+pfnt->nVolCmdWidth + pfnt->nVolHiWidth, y, pfnt->nEltWidths[2] - (pfnt->nVolCmdWidth + pfnt->nVolHiWidth), pfnt->spacingY, + pfnt->nNumX, pfnt->nNumY + (vol % 10) * pfnt->spacingY, pfnt->dib); + } else + { + int srcx = pfnt->nEltWidths[0] + pfnt->nEltWidths[1]; + m_Dib.TextBlt(x, y, pfnt->nEltWidths[2], pfnt->spacingY, pfnt->nClrX+srcx, pfnt->nClrY, pfnt->dib); + } + } + DrawPadding(m_Dib, pfnt, x, y, 2); +} + + +void CViewPattern::OnDraw(CDC *pDC) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CHAR s[256]; + CRect rcClient, rect, rc; + const CModDoc *pModDoc; + + MPT_ASSERT(pDC); + UpdateSizes(); + if ((pModDoc = GetDocument()) == nullptr) return; + + const int vuHeight = MulDiv(VUMETERS_HEIGHT, m_nDPIy, 96); + const int colHeight = MulDiv(COLHDR_HEIGHT, m_nDPIy, 96); + const int chanColorHeight = MulDiv(4, m_nDPIy, 96); + const int chanColorOffset = MulDiv(2, m_nDPIy, 96); + const int recordInsX = MulDiv(3, m_nDPIx, 96); + const bool doSmoothScroll = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL) != 0; + + GetClientRect(&rcClient); + + HDC hdc; + HBITMAP oldBitmap = NULL; + if(doSmoothScroll) + { + if(rcClient != m_oldClient) + { + m_offScreenBitmap.DeleteObject(); + m_offScreenDC.DeleteDC(); + m_offScreenDC.CreateCompatibleDC(pDC); + m_offScreenBitmap.CreateCompatibleBitmap(pDC, rcClient.Width(), rcClient.Height()); + m_oldClient = rcClient; + } + hdc = m_offScreenDC; + oldBitmap = SelectBitmap(hdc, m_offScreenBitmap); + } else + { + hdc = pDC->m_hDC; + } + + const auto dcBrush = GetStockBrush(DC_BRUSH); + const auto faceColor = GetSysColor(COLOR_BTNFACE); + const auto shadowColor = GetSysColor(COLOR_BTNSHADOW); + const auto textColor = GetSysColor(COLOR_BTNTEXT); + + CHANNELINDEX xofs = static_cast<CHANNELINDEX>(GetXScrollPos()); + ROWINDEX yofs = static_cast<ROWINDEX>(GetYScrollPos()); + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + UINT nColumnWidth = m_szCell.cx; + UINT ncols = sndFile.GetNumChannels(); + int xpaint = m_szHeader.cx; + int ypaint = rcClient.top + m_szHeader.cy - GetSmoothScrollOffset(); + const auto &order = Order(); + const ORDERINDEX ordCount = Order().GetLength(); + + if (m_nMidRow) + { + if (yofs >= m_nMidRow) + { + yofs -= m_nMidRow; + } else + { + UINT nSkip = m_nMidRow - yofs; + PATTERNINDEX nPrevPat = PATTERNINDEX_INVALID; + + // Display previous pattern + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SHOWPREVIOUS) + { + if(m_nOrder > 0 && m_nOrder < ordCount) + { + ORDERINDEX prevOrder = order.GetPreviousOrderIgnoringSkips(m_nOrder); + //Skip +++ items + + if(m_nOrder < order.size() && order[m_nOrder] == m_nPattern) + { + nPrevPat = order[prevOrder]; + } + } + } + if(sndFile.Patterns.IsValidPat(nPrevPat)) + { + ROWINDEX nPrevRows = sndFile.Patterns[nPrevPat].GetNumRows(); + ROWINDEX n = std::min(static_cast<ROWINDEX>(nSkip), nPrevRows); + + ypaint += (nSkip - n) * m_szCell.cy; + rect.SetRect(0, m_szHeader.cy, nColumnWidth * ncols + m_szHeader.cx, ypaint - 1); + m_Dib.SetBlendMode(true); + DrawPatternData(hdc, nPrevPat, false, false, + nPrevRows - n, nPrevRows, xofs, rcClient, &ypaint); + m_Dib.SetBlendMode(false); + } else + { + ypaint += nSkip * m_szCell.cy; + rect.SetRect(0, m_szHeader.cy, nColumnWidth * ncols + m_szHeader.cx, ypaint - 1); + } + if ((rect.bottom > rect.top) && (rect.right > rect.left)) + { + ::SetDCBrushColor(hdc, faceColor); + ::FillRect(hdc, &rect, dcBrush); + auto shadowRect = rect; + shadowRect.top = shadowRect.bottom++; + ::SetDCBrushColor(hdc, shadowColor); + ::FillRect(hdc, &shadowRect, dcBrush); + } + yofs = 0; + } + } + + UINT nrows = sndFile.Patterns.IsValidPat(m_nPattern) ? sndFile.Patterns[m_nPattern].GetNumRows() : 0; + int ypatternend = ypaint + (nrows-yofs)*m_szCell.cy; + DrawPatternData(hdc, m_nPattern, true, (pMainFrm->GetModPlaying() == pModDoc), + yofs, nrows, xofs, rcClient, &ypaint); + // Display next pattern + if ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SHOWPREVIOUS) && (ypaint < rcClient.bottom) && (ypaint == ypatternend)) + { + int nVisRows = (rcClient.bottom - ypaint + m_szCell.cy - 1) / m_szCell.cy; + if ((nVisRows > 0) && (m_nMidRow)) + { + PATTERNINDEX nNextPat = PATTERNINDEX_INVALID; + ORDERINDEX nNextOrder = order.GetNextOrderIgnoringSkips(m_nOrder); + if(nNextOrder == m_nOrder) nNextOrder = ORDERINDEX_INVALID; + //Ignore skip items(+++) from sequence. + + if(m_nOrder < ordCount && nNextOrder < ordCount && order[m_nOrder] == m_nPattern) + { + nNextPat = order[nNextOrder]; + } + if(sndFile.Patterns.IsValidPat(nNextPat)) + { + ROWINDEX nNextRows = sndFile.Patterns[nNextPat].GetNumRows(); + ROWINDEX n = std::min(static_cast<ROWINDEX>(nVisRows), nNextRows); + + m_Dib.SetBlendMode(true); + DrawPatternData(hdc, nNextPat, false, false, + 0, n, xofs, rcClient, &ypaint); + m_Dib.SetBlendMode(false); + } + } + } + // Drawing outside pattern area + xpaint = m_szHeader.cx + (ncols - xofs) * nColumnWidth; + if ((xpaint < rcClient.right) && (ypaint > rcClient.top)) + { + rc.SetRect(xpaint, rcClient.top, rcClient.right, ypaint); + ::SetDCBrushColor(hdc, faceColor); + ::FillRect(hdc, &rc, dcBrush); + } + if (ypaint < rcClient.bottom) + { + int width = Util::ScalePixels(1, m_hWnd); + rc.SetRect(0, ypaint, rcClient.right + 1, rcClient.bottom + 1); + if(width == 1) + DrawButtonRect(hdc, &rc, _T("")); + else + DrawEdge(hdc, rc, EDGE_RAISED, BF_TOPLEFT | BF_MIDDLE); // Prevent lower edge from being drawn + } + // Drawing pattern selection + if(m_Status[psDragnDropping]) + { + DrawDragSel(hdc); + } + + const auto buttonBrush = GetSysColorBrush(COLOR_BTNFACE), blackBrush = GetStockBrush(BLACK_BRUSH); + UINT ncolhdr = xofs; + xpaint = m_szHeader.cx; + ypaint = rcClient.top; + rect.SetRect(0, rcClient.top, rcClient.right, rcClient.top + m_szHeader.cy); + if(::RectVisible(hdc, &rect)) + { + sprintf(s, "#%u", m_nPattern); + rect.right = m_szHeader.cx; + DrawButtonRect(hdc, &rect, s, FALSE, + (m_bInItemRect && m_nDragItem.Type() == DragItem::PatternHeader) ? TRUE : FALSE); + + const int dropWidth = Util::ScalePixels(2, m_hWnd); + + // Drawing Channel Headers + while (xpaint < rcClient.right) + { + rect.SetRect(xpaint, ypaint, xpaint + nColumnWidth, ypaint + m_szHeader.cy); + if (ncolhdr < ncols) + { + const auto &channel = sndFile.ChnSettings[ncolhdr]; + const auto recordGroup = pModDoc->GetChannelRecordGroup(static_cast<CHANNELINDEX>(ncolhdr)); + const char *pszfmt = sndFile.m_bChannelMuteTogglePending[ncolhdr]? "[Channel %u]" : "Channel %u"; + if(channel.szName[0] != 0) + pszfmt = sndFile.m_bChannelMuteTogglePending[ncolhdr] ? "%u: [%s]" : "%u: %s"; + else if(m_nDetailLevel < PatternCursor::volumeColumn) + pszfmt = sndFile.m_bChannelMuteTogglePending[ncolhdr] ? "[Ch%u]" : "Ch%u"; + else if(m_nDetailLevel < PatternCursor::effectColumn) + pszfmt = sndFile.m_bChannelMuteTogglePending[ncolhdr] ? "[Chn %u]" : "Chn %u"; + sprintf(s, pszfmt, ncolhdr + 1, channel.szName.buf); + DrawButtonRect(hdc, &rect, s, + channel.dwFlags[CHN_MUTE] ? TRUE : FALSE, + (m_bInItemRect && m_nDragItem.Type() == DragItem::ChannelHeader && m_nDragItem.Value() == ncolhdr) ? TRUE : FALSE, + recordGroup != RecordGroup::NoGroup ? DT_RIGHT : DT_CENTER, chanColorHeight); + + if(channel.color != ModChannelSettings::INVALID_COLOR) + { + // Channel color + CRect r; + r.top = rect.top + chanColorOffset; + r.bottom = r.top + chanColorHeight; + r.left = rect.left + chanColorOffset; + r.right = rect.right - chanColorOffset; + + ::SetDCBrushColor(hdc, channel.color); + ::FillRect(hdc, r, dcBrush); + } + + // When dragging around channel headers, mark insertion position + if(m_Status[psDragging] && !m_bInItemRect + && m_nDragItem.Type() == DragItem::ChannelHeader + && m_nDropItem.Type() == DragItem::ChannelHeader + && m_nDropItem.Value() == ncolhdr) + { + CRect r; + r.top = rect.top; + r.bottom = rect.bottom; + // Drop position depends on whether hovered channel is left or right of dragged item. + r.left = (m_nDropItem.Value() < m_nDragItem.Value() || m_Status[psShiftDragging]) ? rect.left : rect.right - dropWidth; + r.right = r.left + dropWidth; + + ::SetDCBrushColor(hdc, textColor); + ::FillRect(hdc, r, dcBrush); + } + + rect.bottom = rect.top + colHeight; + rect.top += chanColorHeight; + + if(recordGroup != RecordGroup::NoGroup) + { + CRect insRect; + insRect.SetRect(xpaint, ypaint + chanColorHeight, xpaint + nColumnWidth / 8 + recordInsX, ypaint + colHeight); + FrameRect(hdc, &rect, buttonBrush); + InvertRect(hdc, &rect); + s[0] = (recordGroup == RecordGroup::Group1) ? '1' : '2'; + s[1] = '\0'; + DrawButtonRect(hdc, &insRect, s, FALSE, FALSE, DT_CENTER); + FrameRect(hdc, &insRect, blackBrush); + } + + if(m_Status[psShowVUMeters]) + { + OldVUMeters[ncolhdr] = 0; + DrawChannelVUMeter(hdc, rect.left + 1, rect.bottom, ncolhdr); + rect.top += vuHeight; + rect.bottom += vuHeight; + } + if(m_Status[psShowPluginNames]) + { + rect.top = rect.bottom; + rect.bottom = rect.top + m_szPluginHeader.cy; + if(PLUGINDEX mixPlug = channel.nMixPlugin; mixPlug != 0) + sprintf(s, "%u: %s", mixPlug, (sndFile.m_MixPlugins[mixPlug - 1]).pMixPlugin ? sndFile.m_MixPlugins[mixPlug - 1].GetNameLocale() : "[empty]"); + else + sprintf(s, "---"); + DrawButtonRect(hdc, &rect, s, channel.dwFlags[CHN_NOFX] ? TRUE : FALSE, + ((m_bInItemRect) && (m_nDragItem.Type() == DragItem::PluginName) && (m_nDragItem.Value() == ncolhdr)) ? TRUE : FALSE, DT_CENTER); + } + + } else break; + ncolhdr++; + xpaint += nColumnWidth; + } + } + + if(doSmoothScroll) + { + CRect clipRect; + pDC->GetClipBox(clipRect); + pDC->BitBlt(clipRect.left, clipRect.top, clipRect.Width(), clipRect.Height(), &m_offScreenDC, clipRect.left, clipRect.top, SRCCOPY); + SelectBitmap(m_offScreenDC, oldBitmap); + } + + //rewbs.fxVis + if (m_pEffectVis) + { + //HACK: Update visualizer on every pattern redraw. Cleary there's space for opt here. + if (m_pEffectVis->m_hWnd) m_pEffectVis->Update(); + } +} + + +static constexpr UINT EncodeRowColor(int rowBkCol, int rowCol, bool rowSelected) +{ + return (rowBkCol << 16) | (rowCol << 8) | (rowSelected ? 1 : 0); +} + + +void CViewPattern::DrawPatternData(HDC hdc, PATTERNINDEX nPattern, bool selEnable, + bool isPlaying, ROWINDEX startRow, ROWINDEX numRows, CHANNELINDEX startChan, CRect &rcClient, int *pypaint) +{ + uint8 selectedCols[MAX_BASECHANNELS]; // Bit mask of selected channel components + static_assert(1 << PatternCursor::lastColumn <= Util::MaxValueOfType(selectedCols[0]) , "Columns are used as bitmasks."); + + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(nPattern)) + { + return; + } + const CPattern &pattern = sndFile.Patterns[nPattern]; + + const PATTERNFONT *pfnt = PatternFont::currentFont; + CRect rect; + int xpaint, ypaint = *pypaint; + UINT nColumnWidth; + + CHANNELINDEX ncols = sndFile.GetNumChannels(); + nColumnWidth = m_szCell.cx; + rect.SetRect(m_szHeader.cx, rcClient.top, m_szHeader.cx+nColumnWidth, rcClient.bottom); + for(CHANNELINDEX cmk = startChan; cmk < ncols; cmk++) + { + selectedCols[cmk] = selEnable ? m_Selection.GetSelectionBits(cmk) : 0; + if (!::RectVisible(hdc, &rect)) selectedCols[cmk] |= COLUMN_BITS_INVISIBLE; + rect.left += nColumnWidth; + rect.right += nColumnWidth; + } + // Max Visible Column + CHANNELINDEX maxcol = ncols; + while ((maxcol > startChan) && (selectedCols[maxcol-1] & COLUMN_BITS_INVISIBLE)) maxcol--; + // Init bitmap border + { + UINT maxndx = sndFile.GetNumChannels() * m_szCell.cx; + UINT ibmp = 0; + if (maxndx > (UINT)m_Dib.GetWidth()) maxndx = m_Dib.GetWidth(); + do + { + ibmp += nColumnWidth; + m_Dib.TextBlt(ibmp-4, 0, 4, m_szCell.cy, pfnt->nClrX+pfnt->nWidth-4, pfnt->nClrY, pfnt->dib); + } while (ibmp + nColumnWidth <= maxndx); + } + + const bool hexNumbers = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_HEXDISPLAY); + bool bRowSel = false; + int row_col = -1, row_bkcol = -1; + for(ROWINDEX row = startRow; row < numRows; row++) + { + UINT col, xbmp, nbmp, oldrowcolor; + const int compRow = row + TrackerSettings::Instance().rowDisplayOffset; + + rect.left = 0; + rect.top = ypaint; + rect.right = rcClient.right; + rect.bottom = rect.top + m_szCell.cy; + if (!::RectVisible(hdc, &rect)) + { + // No speedup for these columns next time + for(CHANNELINDEX iup = startChan; iup < maxcol; iup++) + selectedCols[iup] &= ~COLUMN_BITS_SKIP; + // skip row + ypaint += m_szCell.cy; + if(ypaint >= rcClient.bottom) + break; + continue; + } + rect.right = rect.left + m_szHeader.cx; + + bool rowDisabled = sndFile.m_lockRowStart != ROWINDEX_INVALID && (row < sndFile.m_lockRowStart || row > sndFile.m_lockRowEnd); + TCHAR s[32]; + if(hexNumbers) + wsprintf(s, _T("%s%02X"), compRow < 0 ? _T("-") : _T(""), std::abs(compRow)); + else + wsprintf(s, _T("%d"), compRow); + + DrawButtonRect(hdc, &rect, s, !selEnable || rowDisabled); + oldrowcolor = EncodeRowColor(row_bkcol, row_col, bRowSel); + bRowSel = (m_Selection.ContainsVertical(PatternCursor(row))); + row_col = MODCOLOR_TEXTNORMAL; + row_bkcol = MODCOLOR_BACKNORMAL; + + // time signature highlighting + ROWINDEX nBeat = sndFile.m_nDefaultRowsPerBeat, nMeasure = sndFile.m_nDefaultRowsPerMeasure; + if(sndFile.Patterns[nPattern].GetOverrideSignature()) + { + nBeat = sndFile.Patterns[nPattern].GetRowsPerBeat(); + nMeasure = sndFile.Patterns[nPattern].GetRowsPerMeasure(); + } + // secondary highlight (beats) + ROWINDEX highlightRow = compRow; + if(nMeasure > 0) + highlightRow %= nMeasure; + if ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_2NDHIGHLIGHT) + && nBeat > 0) + { + if((highlightRow % nBeat) == 0) + { + row_bkcol = MODCOLOR_2NDHIGHLIGHT; + } + } + // primary highlight (measures) + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_STDHIGHLIGHT) + && nMeasure > 0) + { + if(highlightRow == 0) + { + row_bkcol = MODCOLOR_BACKHILIGHT; + } + } + bool blendModeChanged = false; + if (selEnable) + { + if ((row == m_nPlayRow) && (nPattern == m_nPlayPat)) + { + row_col = MODCOLOR_TEXTPLAYCURSOR; + row_bkcol = MODCOLOR_BACKPLAYCURSOR; + } + if (row == GetCurrentRow()) + { + if(m_Status[psFocussed]) + { + row_col = MODCOLOR_TEXTCURROW; + row_bkcol = MODCOLOR_BACKCURROW; + } else + if(m_Status[psFollowSong] && isPlaying) + { + row_col = MODCOLOR_TEXTPLAYCURSOR; + row_bkcol = MODCOLOR_BACKPLAYCURSOR; + } + } + blendModeChanged = (rowDisabled != m_Dib.GetBlendMode()); + m_Dib.SetBlendMode(rowDisabled); + } + // Eliminate non-visible column + xpaint = m_szHeader.cx; + col = startChan; + while ((selectedCols[col] & COLUMN_BITS_INVISIBLE) && (col < maxcol)) + { + selectedCols[col] &= ~COLUMN_BITS_SKIP; + col++; + xpaint += nColumnWidth; + } + // Optimization: same row color ? + bool useSpeedUpMask = (oldrowcolor == EncodeRowColor(row_bkcol, row_col, bRowSel)) && !blendModeChanged; + xbmp = nbmp = 0; + do + { + int x, bk_col, tx_col, col_sel, fx_col; + + const ModCommand *m = pattern.GetpModCommand(row, static_cast<CHANNELINDEX>(col)); + + // Should empty volume commands be replaced with a volume command showing the default volume? + const bool drawDefaultVolume = DrawDefaultVolume(m); + + DWORD dwSpeedUpMask = 0; + if (useSpeedUpMask && (selectedCols[col] & COLUMN_BITS_SKIP) && (row)) + { + const ModCommand *mold = m - ncols; + const bool drawOldDefaultVolume = DrawDefaultVolume(mold); + + if (m->note == mold->note) dwSpeedUpMask |= COLUMN_BITS_NOTE; + if ((m->instr == mold->instr) || (m_nDetailLevel < PatternCursor::instrColumn)) dwSpeedUpMask |= COLUMN_BITS_INSTRUMENT; + if ( m->IsPcNote() || mold->IsPcNote() ) + { + // Handle speedup mask for PC notes. + if(m->note == mold->note) + { + if(m->GetValueVolCol() == mold->GetValueVolCol() || (m_nDetailLevel < PatternCursor::volumeColumn)) dwSpeedUpMask |= COLUMN_BITS_VOLUME; + if(m->GetValueEffectCol() == mold->GetValueEffectCol() || (m_nDetailLevel < PatternCursor::effectColumn)) dwSpeedUpMask |= COLUMN_BITS_FXCMDANDPARAM; + } + } else + { + if ((m->volcmd == mold->volcmd && (m->volcmd == VOLCMD_NONE || m->vol == mold->vol) && !drawDefaultVolume && !drawOldDefaultVolume) || (m_nDetailLevel < PatternCursor::volumeColumn)) dwSpeedUpMask |= COLUMN_BITS_VOLUME; + if ((m->command == mold->command) || (m_nDetailLevel < PatternCursor::effectColumn)) dwSpeedUpMask |= (m->command != CMD_NONE) ? COLUMN_BITS_FXCMD : COLUMN_BITS_FXCMDANDPARAM; + } + if (dwSpeedUpMask == COLUMN_BITS_ALLCOLUMNS) goto DoBlit; + } + selectedCols[col] |= COLUMN_BITS_SKIP; + col_sel = 0; + if (bRowSel) col_sel = selectedCols[col] & COLUMN_BITS_ALL; + tx_col = row_col; + bk_col = row_bkcol; + if (col_sel) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } + // Speedup: Empty command which is either not or fully selected + if (m->IsEmpty() && ((!col_sel) || (col_sel == COLUMN_BITS_ALLCOLUMNS))) + { + m_Dib.SetTextColor(tx_col, bk_col); + m_Dib.TextBlt(xbmp, 0, nColumnWidth-4, m_szCell.cy, pfnt->nClrX, pfnt->nClrY, pfnt->dib); + goto DoBlit; + } + x = 0; + // Note + if (!(dwSpeedUpMask & COLUMN_BITS_NOTE)) + { + tx_col = row_col; + bk_col = row_bkcol; + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_EFFECTHILIGHT) && m->IsNote()) + { + tx_col = MODCOLOR_NOTE; + + if(sndFile.m_SongFlags[SONG_AMIGALIMITS | SONG_PT_MODE]) + { + // Highlight notes that exceed the Amiga's frequency range. + if(sndFile.GetType() == MOD_TYPE_MOD && (m->note < NOTE_MIDDLEC - 12 || m->note >= NOTE_MIDDLEC + 2 * 12)) + { + tx_col = MODCOLOR_DODGY_COMMANDS; + } else if(sndFile.GetType() == MOD_TYPE_S3M && m->instr != 0 && m->instr <= sndFile.GetNumSamples()) + { + uint32 period = sndFile.GetPeriodFromNote(m->note, 0, sndFile.GetSample(m->instr).nC5Speed); + if(period < 113 * 4 || period > 856 * 4) + { + tx_col = MODCOLOR_DODGY_COMMANDS; + } + } + } + } + if (col_sel & COLUMN_BITS_NOTE) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } + // Drawing note + m_Dib.SetTextColor(tx_col, bk_col); + if(sndFile.GetType() == MOD_TYPE_MPT && m->instr < MAX_INSTRUMENTS && sndFile.Instruments[m->instr]) + DrawNote(xbmp+x, 0, m->note, sndFile.Instruments[m->instr]->pTuning); + else //Original + DrawNote(xbmp+x, 0, m->note); + } + x += pfnt->nEltWidths[0]; + // Instrument + if (m_nDetailLevel >= PatternCursor::instrColumn) + { + if (!(dwSpeedUpMask & COLUMN_BITS_INSTRUMENT)) + { + tx_col = row_col; + bk_col = row_bkcol; + if ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_EFFECTHILIGHT) && (m->instr)) + { + tx_col = MODCOLOR_INSTRUMENT; + } + if (col_sel & COLUMN_BITS_INSTRUMENT) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } + // Drawing instrument + m_Dib.SetTextColor(tx_col, bk_col); + DrawInstrument(xbmp+x, 0, m->instr); + } + x += pfnt->nEltWidths[1]; + } + // Volume + if (m_nDetailLevel >= PatternCursor::volumeColumn) + { + if (!(dwSpeedUpMask & COLUMN_BITS_VOLUME)) + { + tx_col = row_col; + bk_col = row_bkcol; + if (col_sel & COLUMN_BITS_VOLUME) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } else if (!m->IsPcNote() && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_EFFECTHILIGHT)) + { + if(m->volcmd != VOLCMD_NONE && m->volcmd < MAX_VOLCMDS && effectColors[m->GetVolumeEffectType()] != 0) + { + tx_col = effectColors[m->GetVolumeEffectType()]; + } else if(drawDefaultVolume) + { + tx_col = MODCOLOR_DEFAULTVOLUME; + } + } + // Drawing Volume + m_Dib.SetTextColor(tx_col, bk_col); + DrawVolumeCommand(xbmp + x, 0, *m, drawDefaultVolume); + } + x += pfnt->nEltWidths[2]; + } + // Command & param + if (m_nDetailLevel >= PatternCursor::effectColumn) + { + const bool isPCnote = m->IsPcNote(); + uint16 val = m->GetValueEffectCol(); + if(val > ModCommand::maxColumnValue) val = ModCommand::maxColumnValue; + fx_col = row_col; + if (!isPCnote && m->command != CMD_NONE && m->command < MAX_EFFECTS && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_EFFECTHILIGHT)) + { + if(effectColors[m->GetEffectType()] != 0) + fx_col = effectColors[m->GetEffectType()]; + else if(m->command == CMD_DUMMY) + fx_col = MODCOLOR_DUMMYCOMMAND; + } + if (!(dwSpeedUpMask & COLUMN_BITS_FXCMD)) + { + tx_col = fx_col; + bk_col = row_bkcol; + if (col_sel & COLUMN_BITS_FXCMD) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } + + // Drawing Command + m_Dib.SetTextColor(tx_col, bk_col); + if(isPCnote) + { + m_Dib.TextBlt(xbmp + x, 0, 2, pfnt->spacingY, pfnt->nClrX+x, pfnt->nClrY, pfnt->dib); + m_Dib.TextBlt(xbmp + x + pfnt->pcValMargin, 0, pfnt->nEltWidths[3], m_szCell.cy, pfnt->nNumX, pfnt->nNumY+(val / 100)*pfnt->spacingY, pfnt->dib); + } else + { + if(m->command != CMD_NONE) + { + char n = sndFile.GetModSpecifications().GetEffectLetter(m->command); + MPT_ASSERT(n >= ' '); + DrawLetter(xbmp+x, 0, n, pfnt->nEltWidths[3], pfnt->nCmdOfs); + } else + { + m_Dib.TextBlt(xbmp+x, 0, pfnt->nEltWidths[3], pfnt->spacingY, pfnt->nClrX+x, pfnt->nClrY, pfnt->dib); + } + } + DrawPadding(m_Dib, pfnt, xbmp + x, 0, 3); + } + x += pfnt->nEltWidths[3]; + // Param + if (!(dwSpeedUpMask & COLUMN_BITS_FXPARAM)) + { + tx_col = fx_col; + bk_col = row_bkcol; + if (col_sel & COLUMN_BITS_FXPARAM) + { + tx_col = MODCOLOR_TEXTSELECTED; + bk_col = MODCOLOR_BACKSELECTED; + } + + // Drawing param + m_Dib.SetTextColor(tx_col, bk_col); + if(isPCnote) + { + m_Dib.TextBlt(xbmp + x, 0, pfnt->nParamHiWidth, m_szCell.cy, pfnt->nNumX, pfnt->nNumY+((val / 10) % 10)*pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(xbmp + x + pfnt->nParamHiWidth, 0, pfnt->nEltWidths[4] - pfnt->padding[4] - pfnt->nParamHiWidth, m_szCell.cy, pfnt->nNumX+pfnt->paramLoMargin, pfnt->nNumY+(val % 10)*pfnt->spacingY, pfnt->dib); + } + else + { + if (m->command) + { + m_Dib.TextBlt(xbmp + x, 0, pfnt->nParamHiWidth, m_szCell.cy, pfnt->nNumX, pfnt->nNumY+(m->param >> 4)*pfnt->spacingY, pfnt->dib); + m_Dib.TextBlt(xbmp + x + pfnt->nParamHiWidth, 0, pfnt->nEltWidths[4] - pfnt->padding[4] - pfnt->nParamHiWidth, m_szCell.cy, pfnt->nNumX+pfnt->paramLoMargin, pfnt->nNumY+(m->param & 0x0F)*pfnt->spacingY, pfnt->dib); + } else + { + m_Dib.TextBlt(xbmp+x, 0, pfnt->nEltWidths[4], m_szCell.cy, pfnt->nClrX+x, pfnt->nClrY, pfnt->dib); + } + } + DrawPadding(m_Dib, pfnt, xbmp + x, 0, 4); + } + } + DoBlit: + nbmp++; + xbmp += nColumnWidth; + xpaint += nColumnWidth; + if ((xbmp + nColumnWidth >= (UINT)m_Dib.GetWidth()) || (xpaint >= rcClient.right)) break; + } while (++col < maxcol); + m_Dib.Blit(hdc, xpaint-xbmp, ypaint, xbmp, m_szCell.cy); + // skip row + ypaint += m_szCell.cy; + if (ypaint >= rcClient.bottom) break; + } + *pypaint = ypaint; + +} + + +void CViewPattern::DrawChannelVUMeter(HDC hdc, int x, int y, UINT nChn) +{ + if (ChnVUMeters[nChn] != OldVUMeters[nChn]) + { + UINT vul, vur; + vul = (ChnVUMeters[nChn] & 0xFF00) >> 8; + vur = ChnVUMeters[nChn] & 0xFF; + vul /= 15; + vur /= 15; + if (vul > 8) vul = 8; + if (vur > 8) vur = 8; + x += (m_szCell.cx / 2); + + const auto &channel = GetSoundFile()->m_PlayState.Chn[nChn]; + const bool isSynth = + channel.dwFlags[CHN_ADLIB] + || (channel.pModSample != nullptr && channel.pModSample->uFlags[CHN_ADLIB]) + || ((channel.pModSample == nullptr || !channel.pModSample->HasSampleData()) && channel.HasMIDIOutput()); + const auto bmp = isSynth ? CMainFrame::bmpPluginVUMeters : CMainFrame::bmpVUMeters; + + if (m_nDetailLevel <= PatternCursor::instrColumn) + { + DibBlt(hdc, x-VUMETERS_LOWIDTH-1, y, VUMETERS_LOWIDTH, VUMETERS_BMPHEIGHT, + VUMETERS_BMPWIDTH*2+VUMETERS_MEDWIDTH*2, vul * VUMETERS_BMPHEIGHT, bmp); + DibBlt(hdc, x-1, y, VUMETERS_LOWIDTH, VUMETERS_BMPHEIGHT, + VUMETERS_BMPWIDTH*2+VUMETERS_MEDWIDTH*2+VUMETERS_LOWIDTH, vur * VUMETERS_BMPHEIGHT, bmp); + } else + if (m_nDetailLevel <= PatternCursor::volumeColumn) + { + DibBlt(hdc, x - VUMETERS_MEDWIDTH-1, y, VUMETERS_MEDWIDTH, VUMETERS_BMPHEIGHT, + VUMETERS_BMPWIDTH*2, vul * VUMETERS_BMPHEIGHT, bmp); + DibBlt(hdc, x, y, VUMETERS_MEDWIDTH, VUMETERS_BMPHEIGHT, + VUMETERS_BMPWIDTH*2+VUMETERS_MEDWIDTH, vur * VUMETERS_BMPHEIGHT, bmp); + } else + { + DibBlt(hdc, x - VUMETERS_BMPWIDTH - 1, y, VUMETERS_BMPWIDTH, VUMETERS_BMPHEIGHT, + 0, vul * VUMETERS_BMPHEIGHT, bmp); + DibBlt(hdc, x + 1, y, VUMETERS_BMPWIDTH, VUMETERS_BMPHEIGHT, + VUMETERS_BMPWIDTH, vur * VUMETERS_BMPHEIGHT, bmp); + } + OldVUMeters[nChn] = ChnVUMeters[nChn]; + } +} + + +// Draw an inverted border around the dragged selection. +void CViewPattern::DrawDragSel(HDC hdc) +{ + const CSoundFile *pSndFile = GetSoundFile(); + CRect rect; + int x1, y1, x2, y2; + int nChannels, nRows; + + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) return; + + // Compute relative movement + int dx = (int)m_DragPos.GetChannel() - (int)m_StartSel.GetChannel(); + int dy = (int)m_DragPos.GetRow() - (int)m_StartSel.GetRow(); + + // Compute destination rect + PatternCursor begin(m_Selection.GetUpperLeft()), end(m_Selection.GetLowerRight()); + + // Check which selection lines need to be drawn. + bool drawLeft = ((int)begin.GetChannel() + dx >= GetXScrollPos()); + bool drawRight = ((int)end.GetChannel() + dx < (int)pSndFile->GetNumChannels()); + bool drawTop = ((int)begin.GetRow() + dy >= GetYScrollPos() - (int)m_nMidRow); + bool drawBottom = ((int)end.GetRow() + dy < (int)pSndFile->Patterns[m_nPattern].GetNumRows()); + + begin.Move(dy, dx, 0); + if(begin.GetChannel() >= pSndFile->GetNumChannels()) + { + // Moved outside pattern range. + return; + } + end.Move(dy, dx, 0); + begin.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + end.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + // We need to know the first pixel that's not part of our rect anymore, so we extend the selection. + end.Move(1, 0, 1); + PatternRect destination(begin, end); + + x1 = m_Selection.GetStartChannel(); + y1 = m_Selection.GetStartRow(); + x2 = m_Selection.GetEndChannel(); + y2 = m_Selection.GetEndRow(); + PatternCursor::Columns c1 = m_Selection.GetStartColumn(); + PatternCursor::Columns c2 = m_Selection.GetEndColumn(); + x1 += dx; + x2 += dx; + y1 += dy; + y2 += dy; + nChannels = pSndFile->m_nChannels; + nRows = pSndFile->Patterns[m_nPattern].GetNumRows(); + if (x1 < GetXScrollPos()) drawLeft = false; + if (x1 >= nChannels) x1 = nChannels - 1; + if (x1 < 0) { x1 = 0; c1 = PatternCursor::firstColumn; drawLeft = false; } + if (x2 >= nChannels) { x2 = nChannels - 1; c2 = PatternCursor::lastColumn; drawRight = false; } + if (x2 < 0) x2 = 0; + if (y1 < GetYScrollPos() - (int)m_nMidRow) drawTop = false; + if (y1 >= nRows) y1 = nRows-1; + if (y1 < 0) { y1 = 0; drawTop = false; } + if (y2 >= nRows) { y2 = nRows-1; drawBottom = false; } + if (y2 < 0) y2 = 0; + + POINT ptTopLeft = GetPointFromPosition(begin); + POINT ptBottomRight = GetPointFromPosition(end); + if ((ptTopLeft.x >= ptBottomRight.x) || (ptTopLeft.y >= ptBottomRight.y)) return; + + if(end.GetColumnType() == PatternCursor::firstColumn) + { + // Special case: If selection ends on the last column of a channel, subtract the channel separator width. + ptBottomRight.x -= 4; + } + + // invert the brush pattern (looks just like frame window sizing) + ::SetTextColor(hdc, RGB(255, 255, 255)); + ::SetBkColor(hdc, RGB(0, 0, 0)); + CBrush* pBrush = CDC::GetHalftoneBrush(); + if (pBrush != NULL) + { + HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, pBrush->m_hObject); + // Top + if (drawTop) + { + rect.SetRect(ptTopLeft.x + 4, ptTopLeft.y, ptBottomRight.x, ptTopLeft.y + 4); + if (!drawLeft) rect.left -= 4; + PatBlt(hdc, rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT); + } + // Bottom + if (drawBottom) + { + rect.SetRect(ptTopLeft.x, ptBottomRight.y - 4, ptBottomRight.x - 4, ptBottomRight.y); + if (!drawRight) rect.right += 4; + PatBlt(hdc, rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT); + } + // Left + if (drawLeft) + { + rect.SetRect(ptTopLeft.x, ptTopLeft.y, ptTopLeft.x + 4, ptBottomRight.y - 4); + if (!drawBottom) rect.bottom += 4; + PatBlt(hdc, rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT); + } + // Right + if (drawRight) + { + rect.SetRect(ptBottomRight.x - 4, ptTopLeft.y + 4, ptBottomRight.x, ptBottomRight.y); + if (!drawTop) rect.top -= 4; + PatBlt(hdc, rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT); + } + if (hOldBrush != NULL) SelectObject(hdc, hOldBrush); + } + +} + + +void CViewPattern::OnDrawDragSel() +{ + HDC hdc = ::GetDC(m_hWnd); + if (hdc != NULL) + { + DrawDragSel(hdc); + ::ReleaseDC(m_hWnd, hdc); + } +} + + +/////////////////////////////////////////////////////////////////////////////// +// CViewPattern Scrolling Functions + + +void CViewPattern::UpdateScrollSize() +{ + const CSoundFile *pSndFile = GetSoundFile(); + const CHANNELINDEX numChannels = pSndFile ? pSndFile->GetNumChannels() : 0; + const ROWINDEX numRows = (pSndFile && pSndFile->Patterns.IsValidPat(m_nPattern)) ? pSndFile->Patterns[m_nPattern].GetNumRows() : 0; + + CRect rect; + SIZE sizeTotal, sizePage, sizeLine; + sizeTotal.cx = m_szHeader.cx + numChannels * m_szCell.cx; + sizeTotal.cy = m_szHeader.cy + numRows * m_szCell.cy; + sizeLine.cx = m_szCell.cx; + sizeLine.cy = m_szCell.cy; + sizePage.cx = sizeLine.cx * 2; + sizePage.cy = sizeLine.cy * 8; + GetClientRect(&rect); + m_nMidRow = 0; + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CENTERROW) m_nMidRow = (rect.Height() - m_szHeader.cy) / (m_szCell.cy * 2); + if (m_nMidRow) sizeTotal.cy += m_nMidRow * m_szCell.cy * 2; + SetScrollSizes(MM_TEXT, sizeTotal, sizePage, sizeLine); + m_bWholePatternFitsOnScreen = (rect.Height() >= sizeTotal.cy); + if(m_bWholePatternFitsOnScreen) + m_nYScroll = 0; +} + + +void CViewPattern::UpdateScrollPos() +{ + CRect rect; + GetClientRect(&rect); + + int x = GetScrollPos(SB_HORZ); + if (x < 0) x = 0; + m_nXScroll = (x + m_szCell.cx - 1) / m_szCell.cx; + int y = GetScrollPos(SB_VERT); + if (y < 0) y = 0; + m_nYScroll = (y + m_szCell.cy - 1) / m_szCell.cy; + +} + + +BOOL CViewPattern::OnScrollBy(CSize sizeScroll, BOOL bDoScroll) +{ + int xOrig, xNew, x; + int yOrig, yNew, y; + + // don't scroll if there is no valid scroll range (ie. no scroll bar) + CScrollBar* pBar; + DWORD dwStyle = GetStyle(); + pBar = GetScrollBarCtrl(SB_VERT); + if ((pBar != NULL && !pBar->IsWindowEnabled()) || + (pBar == NULL && !(dwStyle & WS_VSCROLL))) + { + // vertical scroll bar not enabled + sizeScroll.cy = 0; + } + pBar = GetScrollBarCtrl(SB_HORZ); + if ((pBar != NULL && !pBar->IsWindowEnabled()) || + (pBar == NULL && !(dwStyle & WS_HSCROLL))) + { + // horizontal scroll bar not enabled + sizeScroll.cx = 0; + } + + // adjust current x position + xOrig = x = GetScrollPos(SB_HORZ); + int xMax = GetScrollLimit(SB_HORZ); + x += sizeScroll.cx; + if (x < 0) x = 0; else if (x > xMax) x = xMax; + + // adjust current y position + yOrig = y = GetScrollPos(SB_VERT); + int yMax = GetScrollLimit(SB_VERT); + y += sizeScroll.cy; + if (y < 0) y = 0; else if (y > yMax) y = yMax; + + if (!bDoScroll) return TRUE; + xNew = x; + yNew = y; + + if (x > 0) x = (x + m_szCell.cx - 1) / m_szCell.cx; else x = 0; + if (y > 0) y = (y + m_szCell.cy - 1) / m_szCell.cy; else y = 0; + if ((x != m_nXScroll) || (y != m_nYScroll)) + { + CRect rect; + GetClientRect(&rect); + // HACK: + // Wine handles ScrollWindow completely synchronously (using RedrawWindow). + // This causes the window update region to be repainted immediately + // before and immediately after the actual copying of the scrolled rect. + // Async and sync window painting generally do not mix well at all + // (not even on native Windows) and this causes inevitable flickering + // on Wine. + // Instead, just invalidate the whole scrolled window area and let + // WM_PAINT handle the whole mess without ever scrolling any already + // painted contents. This causes additional CPU usage (on Wine) but + // avoids totally annoying and distracting flickering of the current-row- + // highlight. + if (x != m_nXScroll) + { + rect.left = m_szHeader.cx; + rect.top = 0; + if(TrackerSettings::Instance().patternAlwaysDrawWholePatternOnScrollSlow || mpt::OS::Windows::IsWine()) + { + InvalidateRect(&rect, FALSE); + } else + { + ScrollWindow((m_nXScroll - x) * GetChannelWidth(), 0, &rect, &rect); + } + m_nXScroll = x; + } + if (y != m_nYScroll) + { + rect.left = 0; + rect.top = m_szHeader.cy; + if(TrackerSettings::Instance().patternAlwaysDrawWholePatternOnScrollSlow || mpt::OS::Windows::IsWine()) + { + InvalidateRect(&rect, FALSE); + } else + { + ScrollWindow(0, (m_nYScroll - y) * GetRowHeight(), &rect, &rect); + } + m_nYScroll = y; + } + } + if (xNew != xOrig) SetScrollPos(SB_HORZ, xNew); + if (yNew != yOrig) SetScrollPos(SB_VERT, yNew); + return TRUE; +} + + +void CViewPattern::OnSize(UINT nType, int cx, int cy) +{ + // Note: Switching between modules (when MDI childs are maximized) first calls this with the windowed size, then with the maximized size. + // Watch out for this odd behaviour when debugging this function. + CScrollView::OnSize(nType, cx, cy); + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) + { + UpdateSizes(); + UpdateScrollSize(); + UpdateScrollPos(); + m_Dib.SetSize(cx + m_szCell.cx, m_szCell.cy); + InvalidatePattern(); + } +} + + +void CViewPattern::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + if (nSBCode == SB_THUMBTRACK) m_Status.set(psDragVScroll); + CModScrollView::OnVScroll(nSBCode, nPos, pScrollBar); + if (nSBCode == SB_ENDSCROLL) m_Status.reset(psDragVScroll); +} + + +void CViewPattern::SetCurSel(PatternCursor beginSel, PatternCursor endSel) +{ + RECT rect1, rect2, rect, rcInt, rcUni; + POINT pt; + + // Get current selection area + PatternCursor endSel2(m_Selection.GetLowerRight()); + endSel2.Move(1, 0, 1); + + pt = GetPointFromPosition(m_Selection.GetUpperLeft()); + rect1.left = pt.x; + rect1.top = pt.y; + pt = GetPointFromPosition(endSel2); + rect1.right = pt.x; + rect1.bottom = pt.y; + if(rect1.left < m_szHeader.cx) rect1.left = m_szHeader.cx; + if(rect1.top < m_szHeader.cy) rect1.top = m_szHeader.cy; + + // Get new selection area + m_Selection = PatternRect(beginSel, endSel); + if(const CSoundFile *sndFile = GetSoundFile(); sndFile != nullptr && sndFile->Patterns.IsValidPat(m_nPattern)) + { + m_Selection.Sanitize(sndFile->Patterns[m_nPattern].GetNumRows(), sndFile->GetNumChannels()); + } + UpdateIndicator(); + + pt = GetPointFromPosition(m_Selection.GetUpperLeft()); + rect2.left = pt.x; + rect2.top = pt.y; + endSel2.Set(m_Selection.GetLowerRight()); + endSel2.Move(1, 0, 1); + pt = GetPointFromPosition(endSel2); + rect2.right = pt.x; + rect2.bottom = pt.y; + if (rect2.left < m_szHeader.cx) rect2.left = m_szHeader.cx; + if (rect2.top < m_szHeader.cy) rect2.top = m_szHeader.cy; + + // Compute area for invalidation + IntersectRect(&rcInt, &rect1, &rect2); + UnionRect(&rcUni, &rect1, &rect2); + SubtractRect(&rect, &rcUni, &rcInt); + if ((rect.left == rcUni.left) && (rect.top == rcUni.top) + && (rect.right == rcUni.right) && (rect.bottom == rcUni.bottom)) + { + InvalidateRect(&rect1, FALSE); + InvalidateRect(&rect2, FALSE); + } else + { + InvalidateRect(&rect, FALSE); + } +} + + +void CViewPattern::InvalidatePattern(bool invalidateChannelHeaders, bool invalidateRowHeaders) +{ + CRect rect; + GetClientRect(&rect); + if(!invalidateChannelHeaders) + { + rect.top += m_szHeader.cy; + } + if(!invalidateRowHeaders) + { + rect.left += m_szHeader.cx; + } + InvalidateRect(&rect, FALSE); + SanitizeCursor(); +} + + +void CViewPattern::InvalidateRow(ROWINDEX n) +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + int yofs = GetYScrollPos() - m_nMidRow; + if (n == ROWINDEX_INVALID) n = GetCurrentRow(); + if (((int)n < yofs) || (n >= pSndFile->Patterns[m_nPattern].GetNumRows())) return; + CRect rect; + GetClientRect(&rect); + rect.left = m_szHeader.cx; + rect.top = m_szHeader.cy - GetSmoothScrollOffset(); + rect.top += (n - yofs) * m_szCell.cy; + rect.bottom = rect.top + m_szCell.cy; + InvalidateRect(&rect, FALSE); + } + +} + + +void CViewPattern::InvalidateArea(PatternCursor begin, PatternCursor end) +{ + RECT rect; + POINT pt; + pt = GetPointFromPosition(begin); + rect.left = pt.x; + rect.top = pt.y; + end.Move(1, 0, 1); + pt = GetPointFromPosition(end); + rect.right = pt.x; + rect.bottom = pt.y; + InvalidateRect(&rect, FALSE); +} + + +void CViewPattern::InvalidateCell(PatternCursor cursor) +{ + cursor.RemoveColType(); + InvalidateArea(cursor, PatternCursor(cursor.GetRow(), cursor.GetChannel(), PatternCursor::lastColumn)); +} + + +void CViewPattern::InvalidateChannelsHeaders(CHANNELINDEX chn) +{ + CRect rect; + GetClientRect(&rect); + rect.bottom = rect.top + m_szHeader.cy; + if(chn != CHANNELINDEX_INVALID) + { + rect.left = GetPointFromPosition(PatternCursor{ 0u, chn }).x; + rect.right = rect.left + GetChannelWidth(); + } + InvalidateRect(&rect, FALSE); +} + + +void CViewPattern::UpdateIndicator(bool updateAccessibility) +{ + const CSoundFile *sndFile = GetSoundFile(); + CMainFrame *mainFrm = CMainFrame::GetMainFrame(); + if(mainFrm == nullptr || sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + return; + + mainFrm->SetUserText(MPT_CFORMAT("Row {}, Col {}")(GetCurrentRow(), GetCurrentChannel() + 1)); + if(::GetFocus() == m_hWnd) + { + const bool hasSelection = m_Selection.GetUpperLeft() != m_Selection.GetLowerRight(); + if(hasSelection) + mainFrm->SetInfoText(MPT_CFORMAT("Selection: {} row{}, {} channel{}")(m_Selection.GetNumRows(), CString(m_Selection.GetNumRows() != 1 ? _T("s") : _T("")), m_Selection.GetNumChannels(), CString(m_Selection.GetNumChannels() != 1 ? _T("s") : _T("")))); + if(GetCurrentRow() < sndFile->Patterns[m_nPattern].GetNumRows() && m_Cursor.GetChannel() < sndFile->GetNumChannels()) + { + if(!hasSelection) + mainFrm->SetInfoText(GetCursorDescription()); + UpdateXInfoText(); + } + if(updateAccessibility) + mainFrm->NotifyAccessibilityUpdate(*this); + } +} + + +CString CViewPattern::GetCursorDescription() const +{ + const CSoundFile &sndFile = *GetSoundFile(); + CString s; + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + { + return s; + } + ROWINDEX row = m_Cursor.GetRow(); + CHANNELINDEX channel = m_Cursor.GetChannel(); + const ModCommand *m = sndFile.Patterns[m_nPattern].GetpModCommand(row, channel); + + switch(m_Cursor.GetColumnType()) + { + case PatternCursor::noteColumn: + // display note + if(m->IsSpecialNote()) + s = szSpecialNoteShortDesc[m->note - NOTE_MIN_SPECIAL]; + else if(m->IsNote()) + s = mpt::ToCString(sndFile.GetNoteName(m->note, m->instr)); + break; + + case PatternCursor::instrColumn: + // display instrument + if(m->instr) + { + s.Format(_T("%u: "), m->instr); + if(m->IsPcNote()) + { + // display plugin name. + if(m->instr <= MAX_MIXPLUGINS) + { + s += mpt::ToCString(sndFile.m_MixPlugins[m->instr - 1].GetName()); + } + } else + { + // "normal" instrument + if(sndFile.GetNumInstruments()) + { + if((m->instr <= sndFile.GetNumInstruments()) && (sndFile.Instruments[m->instr])) + { + ModInstrument *pIns = sndFile.Instruments[m->instr]; + s += mpt::ToCString(sndFile.GetCharsetInternal(), pIns->name); + if((m->note) && (m->note <= NOTE_MAX)) + { + const SAMPLEINDEX nsmp = pIns->Keyboard[m->note - 1]; + if((nsmp) && (nsmp <= sndFile.GetNumSamples())) + { + if(sndFile.m_szNames[nsmp][0]) + { + s.AppendFormat(_T(" (%d: "), nsmp); + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[nsmp]); + s.AppendChar(_T(')')); + } + } + } + } + } else if(m->instr <= sndFile.GetNumSamples()) + { + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[m->instr]); + } + + } + } + break; + + case PatternCursor::volumeColumn: + // display volume command + if(m->IsPcNote()) + { + // display plugin param name. + if(m->instr > 0 && m->instr <= MAX_MIXPLUGINS) + { + const SNDMIXPLUGIN &plug = sndFile.m_MixPlugins[m->instr - 1]; + if(plug.pMixPlugin != nullptr) + { + s = plug.pMixPlugin->GetFormattedParamName(m->GetValueVolCol()); + } + } + } else if(m->volcmd != VOLCMD_NONE) + { + // "normal" volume command + EffectInfo effectInfo(sndFile); + effectInfo.GetVolCmdInfo(effectInfo.GetIndexFromVolCmd(m->volcmd), &s); + s += _T(": "); + CString tmp; + effectInfo.GetVolCmdParamInfo(*m, &tmp); + s += tmp; + } + break; + + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + // display effect command + if(m->IsPcNote()) + { + s.Format(_T("Parameter value: %u"), m->GetValueEffectCol()); + } else if(m->command != CMD_NONE) + { + EffectInfo effectInfo(sndFile); + CString sztmp; + if(effectInfo.GetIndexFromEffect(m->command, m->param) >= 0) + { + UINT xParam = 0, xMultiplier = 1; + getXParam(m->command, m_nPattern, row, channel, sndFile, xParam, xMultiplier); + + effectInfo.GetEffectNameEx(sztmp, *m, m->param * xMultiplier + xParam, channel); + } + //effectInfo.GetEffectName(sztmp, m->command, m->param, false, nChn); + if(!sztmp.IsEmpty()) + { + s.Format(_T("%c%02X: "), sndFile.GetModSpecifications().GetEffectLetter(m->command), m->param); + s += sztmp; + } + } + break; + } + return s; +} + + +void CViewPattern::UpdateXInfoText() +{ + const CSoundFile *sndFile = GetSoundFile(); + CMainFrame *mainFrm = CMainFrame::GetMainFrame(); + if(mainFrm == nullptr || sndFile == nullptr) + return; + + CHANNELINDEX chn = GetCurrentChannel(); + const auto &channel = sndFile->m_PlayState.Chn[chn]; + CString xtraInfo; + + xtraInfo.Format(_T("Chn:%d; Vol:%X; Mac:%X; Cut:%X%s; Res:%X; Pan:%X%s"), + chn + 1, + channel.nGlobalVol, + channel.nActiveMacro, + channel.nCutOff, + (channel.nFilterMode == FilterMode::HighPass) ? _T("-HP") : _T(""), + channel.nResonance, + channel.nPan, + channel.dwFlags[CHN_SURROUND] ? _T("-S") : _T("")); + + mainFrm->SetXInfoText(xtraInfo); +} + + +void CViewPattern::UpdateAllVUMeters(Notification *pnotify) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + const CModDoc *pModDoc = GetDocument(); + + if ((!pModDoc) || (!pMainFrm)) return; + CRect rcClient; + GetClientRect(&rcClient); + int xofs = GetXScrollPos(); + HDC hdc = ::GetDC(m_hWnd); + const bool isPlaying = (pMainFrm->GetFollowSong(pModDoc) == m_hWnd); + int x = m_szHeader.cx; + CHANNELINDEX nChn = static_cast<CHANNELINDEX>(xofs); + const int yPos = rcClient.top + MulDiv(COLHDR_HEIGHT, m_nDPIy, 96); + while ((nChn < pModDoc->GetNumChannels()) && (x < rcClient.right)) + { + ChnVUMeters[nChn] = static_cast<uint16>(pnotify->pos[nChn]); + if ((!isPlaying) || pnotify->type[Notification::Stop]) ChnVUMeters[nChn] = 0; + DrawChannelVUMeter(hdc, x + 1, rcClient.top + yPos, nChn); + nChn++; + x += m_szCell.cx; + } + ::ReleaseDC(m_hWnd, hdc); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.cpp new file mode 100644 index 00000000..294e6908 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.cpp @@ -0,0 +1,1052 @@ +/* + * EffectInfo.cpp + * -------------- + * Purpose: Provide information about effect names, parameter interpretation to the tracker interface. + * 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 "EffectInfo.h" +#include "Mptrack.h" // for szHexChar +#include "../soundlib/Sndfile.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/Tables.h" + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////////////////////// +// Effects description + +struct MPTEffectInfo +{ + EffectCommand effect; // CMD_XXXX + ModCommand::PARAM paramMask; // 0 = default + ModCommand::PARAM paramValue; // 0 = default + ModCommand::PARAM paramLimit; // Parameter Editor limit + FlagSet<MODTYPE> supportedFormats; // MOD_TYPE_XXX combo + const TCHAR *name; // e.g. "Tone Portamento" +}; + +static constexpr FlagSet<MODTYPE> MOD_TYPE_MODXM = MOD_TYPE_MOD | MOD_TYPE_XM; +static constexpr FlagSet<MODTYPE> MOD_TYPE_S3MIT = MOD_TYPE_S3M | MOD_TYPE_IT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_S3MITMPT = MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_NOMOD = MOD_TYPE_S3M | MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_XMIT = MOD_TYPE_XM | MOD_TYPE_IT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_XMITMPT = MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_ITMPT = MOD_TYPE_IT | MOD_TYPE_MPT; +static constexpr FlagSet<MODTYPE> MOD_TYPE_ALL = MODTYPE(~0); + +static constexpr MPTEffectInfo gFXInfo[] = +{ + {CMD_ARPEGGIO, 0,0, 0, MOD_TYPE_ALL, _T("Arpeggio")}, + {CMD_PORTAMENTOUP, 0,0, 0, MOD_TYPE_ALL, _T("Portamento Up")}, + {CMD_PORTAMENTODOWN,0,0, 0, MOD_TYPE_ALL, _T("Portamento Down")}, + {CMD_TONEPORTAMENTO,0,0, 0, MOD_TYPE_ALL, _T("Tone Portamento")}, + {CMD_VIBRATO, 0,0, 0, MOD_TYPE_ALL, _T("Vibrato")}, + {CMD_TONEPORTAVOL, 0,0, 0, MOD_TYPE_ALL, _T("Volslide+Toneporta")}, + {CMD_VIBRATOVOL, 0,0, 0, MOD_TYPE_ALL, _T("VolSlide+Vibrato")}, + {CMD_TREMOLO, 0,0, 0, MOD_TYPE_ALL, _T("Tremolo")}, + {CMD_PANNING8, 0,0, 0, MOD_TYPE_ALL, _T("Set Panning")}, + {CMD_OFFSET, 0,0, 0, MOD_TYPE_ALL, _T("Set Offset")}, + {CMD_VOLUMESLIDE, 0,0, 0, MOD_TYPE_ALL, _T("Volume Slide")}, + {CMD_POSITIONJUMP, 0,0, 0, MOD_TYPE_ALL, _T("Position Jump")}, + {CMD_VOLUME, 0,0, 0, MOD_TYPE_MODXM, _T("Set Volume")}, + {CMD_PATTERNBREAK, 0,0, 0, MOD_TYPE_ALL, _T("Pattern Break")}, + {CMD_RETRIG, 0,0, 0, MOD_TYPE_NOMOD, _T("Retrigger Note")}, + {CMD_SPEED, 0,0, 0, MOD_TYPE_ALL, _T("Set Speed")}, + {CMD_TEMPO, 0,0, 0, MOD_TYPE_ALL, _T("Set Tempo")}, + {CMD_TREMOR, 0,0, 0, MOD_TYPE_NOMOD, _T("Tremor")}, + {CMD_CHANNELVOLUME, 0,0, 0, MOD_TYPE_S3MITMPT, _T("Set Channel Volume")}, + {CMD_CHANNELVOLSLIDE,0,0, 0, MOD_TYPE_S3MITMPT, _T("Channel Volume Slide")}, + {CMD_GLOBALVOLUME, 0,0, 0, MOD_TYPE_NOMOD, _T("Set Global Volume")}, + {CMD_GLOBALVOLSLIDE,0,0, 0, MOD_TYPE_NOMOD, _T("Global Volume Slide")}, + {CMD_KEYOFF, 0,0, 0, MOD_TYPE_XM, _T("Key Off")}, + {CMD_FINEVIBRATO, 0,0, 0, MOD_TYPE_S3MITMPT, _T("Fine Vibrato")}, + {CMD_PANBRELLO, 0,0, 0, MOD_TYPE_NOMOD, _T("Panbrello")}, + {CMD_PANNINGSLIDE, 0,0, 0, MOD_TYPE_NOMOD, _T("Panning Slide")}, + {CMD_SETENVPOSITION,0,0, 0, MOD_TYPE_XM, _T("Envelope position")}, + {CMD_MIDI, 0,0, 0x7F, MOD_TYPE_NOMOD, _T("MIDI Macro")}, + {CMD_SMOOTHMIDI, 0,0, 0x7F, MOD_TYPE_XMITMPT, _T("Smooth MIDI Macro")}, + // Extended MOD/XM effects + {CMD_MODCMDEX, 0xF0,0x00, 0, MOD_TYPE_MOD, _T("Set Filter")}, + {CMD_MODCMDEX, 0xF0,0x10, 0, MOD_TYPE_MODXM, _T("Fine Porta Up")}, + {CMD_MODCMDEX, 0xF0,0x20, 0, MOD_TYPE_MODXM, _T("Fine Porta Down")}, + {CMD_MODCMDEX, 0xF0,0x30, 0, MOD_TYPE_MODXM, _T("Glissando Control")}, + {CMD_MODCMDEX, 0xF0,0x40, 0, MOD_TYPE_MODXM, _T("Vibrato Waveform")}, + {CMD_MODCMDEX, 0xF0,0x50, 0, MOD_TYPE_MODXM, _T("Set Finetune")}, + {CMD_MODCMDEX, 0xF0,0x60, 0, MOD_TYPE_MODXM, _T("Pattern Loop")}, + {CMD_MODCMDEX, 0xF0,0x70, 0, MOD_TYPE_MODXM, _T("Tremolo Waveform")}, + {CMD_MODCMDEX, 0xF0,0x80, 0, MOD_TYPE_MODXM, _T("Set Panning")}, + {CMD_MODCMDEX, 0xF0,0x90, 0, MOD_TYPE_MODXM, _T("Retrigger Note")}, + {CMD_MODCMDEX, 0xF0,0xA0, 0, MOD_TYPE_MODXM, _T("Fine Volslide Up")}, + {CMD_MODCMDEX, 0xF0,0xB0, 0, MOD_TYPE_MODXM, _T("Fine Volslide Down")}, + {CMD_MODCMDEX, 0xF0,0xC0, 0, MOD_TYPE_MODXM, _T("Note Cut")}, + {CMD_MODCMDEX, 0xF0,0xD0, 0, MOD_TYPE_MODXM, _T("Note Delay")}, + {CMD_MODCMDEX, 0xF0,0xE0, 0, MOD_TYPE_MODXM, _T("Pattern Delay")}, + {CMD_MODCMDEX, 0xF0,0xF0, 0, MOD_TYPE_XM, _T("Set Active Macro")}, + {CMD_MODCMDEX, 0xF0,0xF0, 0, MOD_TYPE_MOD, _T("Invert Loop")}, + // Extended S3M/IT effects + {CMD_S3MCMDEX, 0xF0,0x10, 0, MOD_TYPE_S3MITMPT, _T("Glissando Control")}, + {CMD_S3MCMDEX, 0xF0,0x20, 0, MOD_TYPE_S3M, _T("Set Finetune")}, + {CMD_S3MCMDEX, 0xF0,0x30, 0, MOD_TYPE_S3MITMPT, _T("Vibrato Waveform")}, + {CMD_S3MCMDEX, 0xF0,0x40, 0, MOD_TYPE_S3MITMPT, _T("Tremolo Waveform")}, + {CMD_S3MCMDEX, 0xF0,0x50, 0, MOD_TYPE_S3MITMPT, _T("Panbrello Waveform")}, + {CMD_S3MCMDEX, 0xF0,0x60, 0, MOD_TYPE_S3MITMPT, _T("Fine Pattern Delay")}, + {CMD_S3MCMDEX, 0xF0,0x80, 0, MOD_TYPE_S3MITMPT, _T("Set Panning")}, + {CMD_S3MCMDEX, 0xF0,0xA0, 0, MOD_TYPE_ITMPT, _T("Set High Offset")}, + {CMD_S3MCMDEX, 0xF0,0xB0, 0, MOD_TYPE_S3MITMPT, _T("Pattern Loop")}, + {CMD_S3MCMDEX, 0xF0,0xC0, 0, MOD_TYPE_S3MITMPT, _T("Note Cut")}, + {CMD_S3MCMDEX, 0xF0,0xD0, 0, MOD_TYPE_S3MITMPT, _T("Note Delay")}, + {CMD_S3MCMDEX, 0xF0,0xE0, 0, MOD_TYPE_S3MITMPT, _T("Pattern Delay")}, + {CMD_S3MCMDEX, 0xF0,0xF0, 0, MOD_TYPE_ITMPT, _T("Set Active Macro")}, + // MPT XM extensions and special effects + {CMD_XFINEPORTAUPDOWN,0xF0,0x10,0, MOD_TYPE_XM, _T("Extra Fine Porta Up")}, + {CMD_XFINEPORTAUPDOWN,0xF0,0x20,0, MOD_TYPE_XM, _T("Extra Fine Porta Down")}, + {CMD_XFINEPORTAUPDOWN,0xF0,0x50,0, MOD_TYPE_XM, _T("Panbrello Waveform")}, + {CMD_XFINEPORTAUPDOWN,0xF0,0x60,0, MOD_TYPE_XM, _T("Fine Pattern Delay")}, + {CMD_XFINEPORTAUPDOWN,0xF0,0x90,0, MOD_TYPE_XM, _T("Sound Control")}, + {CMD_XFINEPORTAUPDOWN,0xF0,0xA0,0, MOD_TYPE_XM, _T("Set High Offset")}, + // MPT IT extensions and special effects + {CMD_S3MCMDEX, 0xF0,0x90, 0, MOD_TYPE_S3MITMPT, _T("Sound Control")}, + {CMD_S3MCMDEX, 0xF0,0x70, 0, MOD_TYPE_ITMPT, _T("Instr. Control")}, + {CMD_DELAYCUT, 0x00,0x00, 0, MOD_TYPE_MPT, _T("Note Delay and Cut")}, + {CMD_XPARAM, 0,0, 0, MOD_TYPE_XMITMPT, _T("Parameter Extension")}, + {CMD_NOTESLIDEUP, 0,0, 0, MOD_TYPE_IMF | MOD_TYPE_PTM, _T("Note Slide Up")}, // IMF / PTM effect + {CMD_NOTESLIDEDOWN, 0,0, 0, MOD_TYPE_IMF | MOD_TYPE_PTM, _T("Note Slide Down")}, // IMF / PTM effect + {CMD_NOTESLIDEUPRETRIG, 0,0, 0, MOD_TYPE_PTM, _T("Note Slide Up + Retrigger Note")}, // PTM effect + {CMD_NOTESLIDEDOWNRETRIG,0,0, 0, MOD_TYPE_PTM, _T("Note Slide Down + Retrigger Note")}, // PTM effect + {CMD_REVERSEOFFSET, 0,0, 0, MOD_TYPE_PTM, _T("Revert Sample + Offset")}, // PTM effect + {CMD_DBMECHO, 0,0, 0, MOD_TYPE_DBM, _T("Echo Enable")}, // DBM effect + {CMD_OFFSETPERCENTAGE, 0,0, 0, MOD_TYPE_PLM, _T("Offset (Percentage)")}, // PLM effect + {CMD_FINETUNE, 0,0, 0, MOD_TYPE_MPT, _T("Finetune")}, + {CMD_FINETUNE_SMOOTH, 0,0, 0, MOD_TYPE_MPT, _T("Finetune (Smooth)")}, + {CMD_DUMMY, 0,0, 0, MOD_TYPE_NONE, _T("Empty") }, + {CMD_DIGIREVERSESAMPLE, 0, 0, 0, MOD_TYPE_NONE, _T("Reverse Sample")}, // DIGI effect +}; + + +UINT EffectInfo::GetNumEffects() const +{ + return static_cast<UINT>(std::size(gFXInfo)); +} + + +bool EffectInfo::IsExtendedEffect(UINT ndx) const +{ + return ((ndx < std::size(gFXInfo)) && (gFXInfo[ndx].paramMask)); +} + + +bool EffectInfo::GetEffectName(CString &pszDescription, ModCommand::COMMAND command, UINT param, bool bXX) const +{ + bool bSupported; + UINT fxndx = static_cast<UINT>(std::size(gFXInfo)); + pszDescription.Empty(); + for (UINT i = 0; i < std::size(gFXInfo); i++) + { + if ((command == gFXInfo[i].effect) // Effect + && ((param & gFXInfo[i].paramMask) == gFXInfo[i].paramValue)) // Value + { + fxndx = i; + // if format is compatible, everything is fine. if not, let's still search + // for another command. this fixes searching for the EFx command, which + // does different things in MOD format. + if((sndFile.GetType() & gFXInfo[i].supportedFormats)) + break; + } + } + if (fxndx == std::size(gFXInfo)) return false; + bSupported = ((sndFile.GetType() & gFXInfo[fxndx].supportedFormats)); + if (gFXInfo[fxndx].name) + { + if ((bXX) && (bSupported)) + { + pszDescription.Format(_T("%c%c%c: ") + , sndFile.GetModSpecifications().GetEffectLetter(command) + , ((gFXInfo[fxndx].paramMask & 0xF0) == 0xF0) ? szHexChar[gFXInfo[fxndx].paramValue >> 4] : 'x' + , ((gFXInfo[fxndx].paramMask & 0x0F) == 0x0F) ? szHexChar[gFXInfo[fxndx].paramValue & 0x0F] : 'x' + ); + } + pszDescription += gFXInfo[fxndx].name; + } + return bSupported; +} + + +LONG EffectInfo::GetIndexFromEffect(ModCommand::COMMAND command, ModCommand::PARAM param) const +{ + UINT ndx = static_cast<UINT>(std::size(gFXInfo)); + for (UINT i = 0; i < std::size(gFXInfo); i++) + { + if ((command == gFXInfo[i].effect) // Effect + && ((param & gFXInfo[i].paramMask) == gFXInfo[i].paramValue)) // Value + { + ndx = i; + if((sndFile.GetType() & gFXInfo[i].supportedFormats)) + break; // found fitting format; this is correct for sure + } + } + return ndx; +} + + +//Returns command and corrects parameter refParam if necessary +EffectCommand EffectInfo::GetEffectFromIndex(UINT ndx, ModCommand::PARAM &refParam) const +{ + if (ndx >= std::size(gFXInfo)) + { + refParam = 0; + return CMD_NONE; + } + + // Cap parameter to match FX if necessary. + if (gFXInfo[ndx].paramMask) + { + if (refParam < gFXInfo[ndx].paramValue) + { + refParam = gFXInfo[ndx].paramValue; // for example: delay with param < D0 becomes SD0 + } else if (refParam > gFXInfo[ndx].paramValue + 15) + { + refParam = gFXInfo[ndx].paramValue + 15; // for example: delay with param > DF becomes SDF + } + } + if (gFXInfo[ndx].paramLimit) + { + // used for Zxx macro control in parameter editor: limit to 7F max. + LimitMax(refParam, gFXInfo[ndx].paramLimit); + } + + return gFXInfo[ndx].effect; +} + + +EffectCommand EffectInfo::GetEffectFromIndex(UINT ndx) const +{ + if (ndx >= std::size(gFXInfo)) + { + return CMD_NONE; + } + + return gFXInfo[ndx].effect; +} + +UINT EffectInfo::GetEffectMaskFromIndex(UINT ndx) const +{ + if (ndx >= std::size(gFXInfo)) + { + return 0; + } + + return gFXInfo[ndx].paramValue; + +} + +bool EffectInfo::GetEffectInfo(UINT ndx, CString *s, bool bXX, ModCommand::PARAM *prangeMin, ModCommand::PARAM *prangeMax) const +{ + + if (s) s->Empty(); + if (prangeMin) *prangeMin = 0; + if (prangeMax) *prangeMax = 0; + if ((ndx >= std::size(gFXInfo)) || (!(sndFile.GetType() & gFXInfo[ndx].supportedFormats))) return FALSE; + if (s) GetEffectName(*s, gFXInfo[ndx].effect, gFXInfo[ndx].paramValue, bXX); + if ((prangeMin) && (prangeMax)) + { + ModCommand::PARAM nmin = 0, nmax = 0xFF; + if (gFXInfo[ndx].paramMask == 0xF0) + { + nmin = gFXInfo[ndx].paramValue; + nmax = nmin | 0x0F; + } + switch(gFXInfo[ndx].effect) + { + case CMD_ARPEGGIO: + if (sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) nmin = 1; + break; + case CMD_VOLUME: + case CMD_CHANNELVOLUME: + nmax = 0x40; + break; + case CMD_SPEED: + nmin = 1; + if (sndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) nmax = 0x1F; + else nmax = 0xFF; + break; + case CMD_TEMPO: + if (sndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) nmin = 0x20; + else nmin = 0; + break; + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + nmax = (sndFile.GetType() & MOD_TYPE_S3MITMPT) ? 59 : 30; + break; + case CMD_PANNING8: + if (sndFile.GetType() & (MOD_TYPE_S3M)) nmax = 0x81; + else nmax = 0xFF; + break; + case CMD_GLOBALVOLUME: + nmax = (sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? 128 : 64; + break; + + case CMD_MODCMDEX: + // adjust waveform types for XM/MOD + if(gFXInfo[ndx].paramValue == 0x40 || gFXInfo[ndx].paramValue == 0x70) nmax = gFXInfo[ndx].paramValue | 0x07; + if(gFXInfo[ndx].paramValue == 0x00) nmax = 1; + break; + case CMD_S3MCMDEX: + // adjust waveform types for IT/S3M + if(gFXInfo[ndx].paramValue >= 0x30 && gFXInfo[ndx].paramValue <= 0x50) nmax = gFXInfo[ndx].paramValue | ((sndFile.m_playBehaviour[kITVibratoTremoloPanbrello] || sndFile.GetType() == MOD_TYPE_S3M) ? 0x03 : 0x07); + break; + case CMD_PATTERNBREAK: + // no big patterns in MOD/S3M files, and FT2 disallows breaking to rows > 63 + if(sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_S3M | MOD_TYPE_XM)) + nmax = 63; + break; + } + *prangeMin = nmin; + *prangeMax = nmax; + } + return TRUE; +} + + +UINT EffectInfo::MapValueToPos(UINT ndx, UINT param) const +{ + UINT pos; + + if (ndx >= std::size(gFXInfo)) return 0; + pos = param; + if (gFXInfo[ndx].paramMask == 0xF0) + { + pos &= 0x0F; + pos |= gFXInfo[ndx].paramValue; + } + switch(gFXInfo[ndx].effect) + { + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + if (sndFile.GetType() & MOD_TYPE_S3MITMPT) + { + if (!param) + pos = 29; + else if (((param & 0x0F) == 0x0F) && (param & 0xF0)) + pos = 29 + (param >> 4); // Fine Up + else if (((param & 0xF0) == 0xF0) && (param & 0x0F)) + pos = 29 - (param & 0x0F); // Fine Down + else if (param & 0x0F) + pos = 15 - (param & 0x0F); // Down + else + pos = (param >> 4) + 44; // Up + } else + { + if (param & 0x0F) + pos = 15 - (param & 0x0F); + else + pos = (param >> 4) + 15; + } + break; + case CMD_PANNING8: + if(sndFile.GetType() == MOD_TYPE_S3M) + { + pos = Clamp(param, 0u, 0x80u); + if(param == 0xA4) + pos = 0x81; + } + break; + } + return pos; +} + + +UINT EffectInfo::MapPosToValue(UINT ndx, UINT pos) const +{ + UINT param; + + if (ndx >= std::size(gFXInfo)) return 0; + param = pos; + if (gFXInfo[ndx].paramMask == 0xF0) param |= gFXInfo[ndx].paramValue; + switch(gFXInfo[ndx].effect) + { + case CMD_VOLUMESLIDE: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + if (sndFile.GetType() & MOD_TYPE_S3MITMPT) + { + if (pos < 15) + param = 15 - pos; + else if (pos < 29) + param = (29 - pos) | 0xF0; + else if (pos == 29) + param = 0; + else if (pos <= 44) + param = ((pos - 29) << 4) | 0x0F; + else + if (pos <= 59) param = (pos - 44) << 4; + } else + { + if (pos < 15) + param = 15 - pos; + else + param = (pos - 15) << 4; + } + break; + case CMD_PANNING8: + if(sndFile.GetType() == MOD_TYPE_S3M) + param = (pos <= 0x80) ? pos : 0xA4; + break; + } + return param; +} + + +bool EffectInfo::GetEffectNameEx(CString &pszName, const ModCommand &m, uint32 param, CHANNELINDEX chn) const +{ + CString s; + const TCHAR *continueOrIgnore; + + auto ndx = GetIndexFromEffect(m.command, static_cast<ModCommand::PARAM>(param)); + + if(ndx < 0 || static_cast<std::size_t>(ndx) >= std::size(gFXInfo) || !gFXInfo[ndx].name) + return false; + pszName = CString{gFXInfo[ndx].name} + _T(": "); + + // for effects that don't have effect memory in MOD format. + if(sndFile.GetType() == MOD_TYPE_MOD) + continueOrIgnore = _T("ignore"); + else + continueOrIgnore = _T("continue"); + + const TCHAR *plusChar = _T("+"), *minusChar = _T("-"); + + switch(gFXInfo[ndx].effect) + { + case CMD_ARPEGGIO: + if(sndFile.GetType() == MOD_TYPE_XM) // XM also ignores this! + continueOrIgnore = _T("ignore"); + + if(param) + s.Format(_T("note+%d note+%d"), param >> 4, param & 0x0F); + else + s = continueOrIgnore; + break; + + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + if(param) + { + TCHAR sign = (gFXInfo[ndx].effect == CMD_PORTAMENTOUP) ? _T('+') : _T('-'); + + if((sndFile.GetType() & MOD_TYPE_S3MITMPT) && ((param & 0xF0) == 0xF0)) + s.Format(_T("fine %c%d"), sign, (param & 0x0F)); + else if((sndFile.GetType() & MOD_TYPE_S3MITMPT) && ((param & 0xF0) == 0xE0)) + s.Format(_T("extra fine %c%d"), sign, (param & 0x0F)); + else + s.Format(_T("%c%d"), sign, param); + } else + { + s = continueOrIgnore; + } + break; + + case CMD_TONEPORTAMENTO: + if (param) + s.Format(_T("speed %d"), param); + else + s = _T("continue"); + break; + + case CMD_VIBRATO: + case CMD_TREMOLO: + case CMD_PANBRELLO: + case CMD_FINEVIBRATO: + if (param) + s.Format(_T("speed=%d depth=%d"), param >> 4, param & 0x0F); + else + s = _T("continue"); + break; + + case CMD_SPEED: + s.Format(_T("%d ticks/row"), param); + break; + + case CMD_TEMPO: + if (param == 0) + s = _T("continue"); + else if (param < 0x10) + s.Format(_T("-%d bpm (slower)"), param & 0x0F); + else if (param < 0x20) + s.Format(_T("+%d bpm (faster)"), param & 0x0F); + else + s.Format(_T("%d bpm"), param); + break; + + case CMD_PANNING8: + if(sndFile.GetType() == MOD_TYPE_S3M && param == 0xA4) + s = _T("Surround"); + else + s.Format(_T("%d"), param); + break; + + case CMD_RETRIG: + switch(param >> 4) + { + case 0: + if(sndFile.GetType() & MOD_TYPE_XM) + s = _T("continue"); + else + s = _T("vol *1"); + break; + case 1: s = _T("vol -1"); break; + case 2: s = _T("vol -2"); break; + case 3: s = _T("vol -4"); break; + case 4: s = _T("vol -8"); break; + case 5: s = _T("vol -16"); break; + case 6: s = _T("vol *0.66"); break; + case 7: s = _T("vol *0.5"); break; + case 8: s = _T("vol *1"); break; + case 9: s = _T("vol +1"); break; + case 10: s = _T("vol +2"); break; + case 11: s = _T("vol +4"); break; + case 12: s = _T("vol +8"); break; + case 13: s = _T("vol +16"); break; + case 14: s = _T("vol *1.5"); break; + case 15: s = _T("vol *2"); break; + } + s.AppendFormat(_T(" speed %d"), param & 0x0F); + break; + + case CMD_VOLUMESLIDE: + if(sndFile.GetType() == MOD_TYPE_MOD && !param) + { + s = continueOrIgnore; + break; + } + [[fallthrough]]; + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_GLOBALVOLSLIDE: + case CMD_CHANNELVOLSLIDE: + case CMD_PANNINGSLIDE: + if(gFXInfo[ndx].effect == CMD_PANNINGSLIDE) + { + if(sndFile.GetType() == MOD_TYPE_XM) + { + plusChar = _T("-> "); + minusChar = _T("<- "); + } else + { + plusChar = _T("<- "); + minusChar = _T("-> "); + } + } + + if (!param) + { + s.Format(_T("continue")); + } else if ((sndFile.GetType() & MOD_TYPE_S3MITMPT) && ((param & 0x0F) == 0x0F) && (param & 0xF0)) + { + s.Format(_T("fine %s%d"), plusChar, param >> 4); + } else if ((sndFile.GetType() & MOD_TYPE_S3MITMPT) && ((param & 0xF0) == 0xF0) && (param & 0x0F)) + { + s.Format(_T("fine %s%d"), minusChar, param & 0x0F); + } else if ((param & 0x0F) != param && (param & 0xF0) != param) // both nibbles are set. + { + s = _T("undefined"); + } else if (param & 0x0F) + { + s.Format(_T("%s%d"), minusChar, param & 0x0F); + } else + { + s.Format(_T("%s%d"), plusChar, param >> 4); + } + break; + + case CMD_PATTERNBREAK: + pszName.Format(_T("Break to row %u"), param); + break; + + case CMD_POSITIONJUMP: + pszName.Format(_T("Jump to position %u"), param); + break; + + case CMD_OFFSET: + if (param) + pszName.Format(_T("Set Offset to %s"), mpt::cfmt::dec(3, ',', param).GetString()); + else + s = _T("continue"); + break; + + case CMD_CHANNELVOLUME: + case CMD_GLOBALVOLUME: + { + ModCommand::PARAM minVal = 0, maxVal = 128; + GetEffectInfo(ndx, nullptr, false, &minVal, &maxVal); + if((sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)) && param > maxVal) + s = _T("undefined"); + else + s.Format(_T("%u"), std::min(static_cast<uint32>(param), static_cast<uint32>(maxVal))); + } + break; + + case CMD_TREMOR: + if(param) + { + uint8 ontime = (uint8)(param >> 4), offtime = (uint8)(param & 0x0F); + if(sndFile.m_SongFlags[SONG_ITOLDEFFECTS] || (sndFile.GetType() & MOD_TYPE_XM)) + { + ontime++; + offtime++; + } else + { + if(ontime == 0) ontime = 1; + if(offtime == 0) offtime = 1; + } + s.Format(_T("ontime %u, offtime %u"), ontime, offtime); + } else + { + s = _T("continue"); + } + break; + + case CMD_SETENVPOSITION: + s.Format(_T("Tick %u"), param); + break; + + case CMD_MIDI: + case CMD_SMOOTHMIDI: + if (param < 0x80) + { + if(chn != CHANNELINDEX_INVALID) + { + const uint8 macroIndex = sndFile.m_PlayState.Chn[chn].nActiveMacro; + const PLUGINDEX plugin = sndFile.GetBestPlugin(sndFile.m_PlayState, chn, PrioritiseChannel, EvenIfMuted) - 1; + IMixPlugin *pPlugin = (plugin < MAX_MIXPLUGINS ? sndFile.m_MixPlugins[plugin].pMixPlugin : nullptr); + pszName.Format(_T("SFx MIDI Macro z=%d (SF%X: %s)"), param, macroIndex, sndFile.m_MidiCfg.GetParameteredMacroName(macroIndex, pPlugin).GetString()); + } else + { + pszName.Format(_T("SFx MIDI Macro z=%02X (%d)"), param, param); + } + } else + { + pszName.Format(_T("Fixed Macro Z%02X"), param); + } + break; + + case CMD_DELAYCUT: + pszName.Format(_T("Note delay: %d, cut after %d ticks"), (param >> 4), (param & 0x0F)); + break; + + case CMD_FINETUNE: + case CMD_FINETUNE_SMOOTH: + { + int8 pwd = 1; + const TCHAR *unit = _T(" cents"); + if(m.instr > 0 && m.instr <= sndFile.GetNumInstruments() && sndFile.Instruments[m.instr] != nullptr) + pwd = sndFile.Instruments[m.instr]->midiPWD; + else if(chn != CHANNELINDEX_INVALID && sndFile.m_PlayState.Chn[chn].pModInstrument != nullptr) + pwd = sndFile.m_PlayState.Chn[chn].pModInstrument->midiPWD; + else if(sndFile.GetNumInstruments()) + unit = _T(""); + + pszName = MPT_CFORMAT("Finetune{}: {}{}{}")( + CString(gFXInfo[ndx].effect == CMD_FINETUNE ? _T("") : _T(" (Smooth)")), + CString(param >= 0x8000 ? _T("+") : _T("")), + mpt::cfmt::val((static_cast<int32>(param) - 0x8000) * pwd / 327.68), + CString(unit)); + } + break; + + default: + if (gFXInfo[ndx].paramMask == 0xF0) + { + // Sound control names + if (((gFXInfo[ndx].effect == CMD_XFINEPORTAUPDOWN) || (gFXInfo[ndx].effect == CMD_S3MCMDEX)) + && ((gFXInfo[ndx].paramValue & 0xF0) == 0x90) && ((param & 0xF0) == 0x90)) + { + switch(param & 0x0F) + { + case 0x00: s = _T("90: Surround Off"); break; + case 0x01: s = _T("91: Surround On"); break; + case 0x08: s = _T("98: Reverb Off"); break; + case 0x09: s = _T("99: Reverb On"); break; + case 0x0A: s = _T("9A: Center surround"); break; + case 0x0B: s = _T("9B: Quad surround"); break; + case 0x0C: s = _T("9C: Global filters"); break; + case 0x0D: s = _T("9D: Local filters"); break; + case 0x0E: s = _T("9E: Play Forward"); break; + case 0x0F: s = _T("9F: Play Backward"); break; + default: s.Format(_T("%02X: undefined"), param); + } + } else + if (((gFXInfo[ndx].effect == CMD_XFINEPORTAUPDOWN) || (gFXInfo[ndx].effect == CMD_S3MCMDEX)) + && ((gFXInfo[ndx].paramValue & 0xF0) == 0x70) && ((param & 0xF0) == 0x70)) + { + switch(param & 0x0F) + { + case 0x00: s = _T("70: Past note cut"); break; + case 0x01: s = _T("71: Past note off"); break; + case 0x02: s = _T("72: Past note fade"); break; + case 0x03: s = _T("73: NNA note cut"); break; + case 0x04: s = _T("74: NNA continue"); break; + case 0x05: s = _T("75: NNA note off"); break; + case 0x06: s = _T("76: NNA note fade"); break; + case 0x07: s = _T("77: Volume Env Off"); break; + case 0x08: s = _T("78: Volume Env On"); break; + case 0x09: s = _T("79: Pan Env Off"); break; + case 0x0A: s = _T("7A: Pan Env On"); break; + case 0x0B: s = _T("7B: Pitch Env Off"); break; + case 0x0C: s = _T("7C: Pitch Env On"); break; + case 0x0D: if(sndFile.GetType() == MOD_TYPE_MPT) { s = _T("7D: Force Pitch Env"); break; } + [[fallthrough]]; + case 0x0E: if(sndFile.GetType() == MOD_TYPE_MPT) { s = _T("7E: Force Filter Env"); break; } + [[fallthrough]]; + default: s.Format(_T("%02X: undefined"), param); break; + } + } else + { + s.Format(_T("%d"), param & 0x0F); + if(gFXInfo[ndx].effect == CMD_S3MCMDEX) + { + switch(param & 0xF0) + { + case 0x10: // glissando control + if((param & 0x0F) == 0) + s = _T("smooth"); + else + s = _T("semitones"); + break; + case 0x20: // set finetune + s.Format(_T("%dHz"), S3MFineTuneTable[param & 0x0F]); + break; + case 0x30: // vibrato waveform + case 0x40: // tremolo waveform + case 0x50: // panbrello waveform + if(((param & 0x0F) > 0x03) && sndFile.m_playBehaviour[kITVibratoTremoloPanbrello]) + { + s = _T("ignore"); + break; + } + switch(param & 0x0F) + { + case 0x00: s = _T("sine wave"); break; + case 0x01: s = _T("ramp down"); break; + case 0x02: s = _T("square wave"); break; + case 0x03: s = _T("random"); break; + case 0x04: s = _T("sine wave (cont.)"); break; + case 0x05: s = _T("ramp down (cont.)"); break; + case 0x06: s = _T("square wave (cont.)"); break; + case 0x07: s = _T("random (cont.)"); break; + default: s = _T("ignore"); break; + } + break; + + case 0x60: // fine pattern delay (ticks) + s += _T(" ticks"); + break; + + case 0xA0: // high offset + s.Format(_T("+ %u samples"), (param & 0x0F) * 0x10000); + break; + + case 0xB0: // pattern loop + if((param & 0x0F) == 0x00) + s = _T("loop start"); + else + s += _T(" times"); + break; + case 0xC0: // note cut + case 0xD0: // note delay + //IT compatibility 22. SD0 == SD1, SC0 == SC1 + if(((param & 0x0F) == 1) || ((param & 0x0F) == 0 && (sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)))) + s = _T("1 tick"); + else + s += _T(" ticks"); + break; + case 0xE0: // pattern delay (rows) + s += _T(" rows"); + break; + case 0xF0: // macro + s = sndFile.m_MidiCfg.GetParameteredMacroName(param & 0x0F); + break; + default: + break; + } + } + if(gFXInfo[ndx].effect == CMD_MODCMDEX) + { + switch(param & 0xF0) + { + case 0x00: + // Filter + if(param & 1) + s = _T("LED Filter Off"); + else + s = _T("LED Filter On"); + break; + + case 0x30: // glissando control + if((param & 0x0F) == 0) + s = _T("smooth"); + else + s = _T("semitones"); + break; + case 0x40: // vibrato waveform + case 0x70: // tremolo waveform + switch(param & 0x0F) + { + case 0x00: case 0x08: s = _T("sine wave"); break; + case 0x01: case 0x09: s = _T("ramp down"); break; + case 0x02: case 0x0A: s = _T("square wave"); break; + case 0x03: case 0x0B: s = _T("square wave"); break; + + case 0x04: case 0x0C: s = _T("sine wave (cont.)"); break; + case 0x05: case 0x0D: s = _T("ramp down (cont.)"); break; + case 0x06: case 0x0E: s = _T("square wave (cont.)"); break; + case 0x07: case 0x0F: s = _T("square wave (cont.)"); break; + } + break; + case 0x50: // set finetune + { + int8 nFinetune = (param & 0x0F); + if(sndFile.GetType() & MOD_TYPE_XM) + { + // XM finetune + nFinetune = (nFinetune - 8) * 16; + } else + { + // MOD finetune + if(nFinetune > 7) nFinetune -= 16; + } + s.Format(_T("%d"), nFinetune); + } + break; + case 0x60: // pattern loop + if((param & 0x0F) == 0x00) + s = _T("loop start"); + else + s += _T(" times"); + break; + case 0x90: // retrigger + s.Format(_T("speed %d"), param & 0x0F); + break; + case 0xC0: // note cut + case 0xD0: // note delay + s += _T(" ticks"); + break; + case 0xE0: // pattern delay (rows) + s += _T(" rows"); + break; + case 0xF0: + if(sndFile.GetType() == MOD_TYPE_MOD) + { + // invert loop + if((param & 0x0F) == 0) + s = _T("Stop"); + else + s.Format(_T("Speed %d"), param & 0x0F); + } else + { + // macro + s = sndFile.m_MidiCfg.GetParameteredMacroName(param & 0x0F); + } + break; + default: + break; + } + } + } + + } else + { + s.Format(_T("%u"), param); + } + } + pszName += s; + return true; +} + + +//////////////////////////////////////////////////////////////////////////////////////// +// Volume column effects description + +struct MPTVolCmdInfo +{ + VolumeCommand volCmd; // VOLCMD_XXXX + FlagSet<MODTYPE> supportedFormats; // MOD_TYPE_XXX combo + const TCHAR *name; // e.g. "Set Volume" +}; + +static constexpr MPTVolCmdInfo gVolCmdInfo[] = +{ + {VOLCMD_VOLUME, MOD_TYPE_NOMOD, _T("Set Volume")}, + {VOLCMD_PANNING, MOD_TYPE_NOMOD, _T("Set Panning")}, + {VOLCMD_VOLSLIDEUP, MOD_TYPE_XMITMPT, _T("Volume slide up")}, + {VOLCMD_VOLSLIDEDOWN, MOD_TYPE_XMITMPT, _T("Volume slide down")}, + {VOLCMD_FINEVOLUP, MOD_TYPE_XMITMPT, _T("Fine volume up")}, + {VOLCMD_FINEVOLDOWN, MOD_TYPE_XMITMPT, _T("Fine volume down")}, + {VOLCMD_VIBRATOSPEED, MOD_TYPE_XM, _T("Vibrato speed")}, + {VOLCMD_VIBRATODEPTH, MOD_TYPE_XMITMPT, _T("Vibrato depth")}, + {VOLCMD_PANSLIDELEFT, MOD_TYPE_XM, _T("Pan slide left")}, + {VOLCMD_PANSLIDERIGHT, MOD_TYPE_XM, _T("Pan slide right")}, + {VOLCMD_TONEPORTAMENTO, MOD_TYPE_XMITMPT, _T("Tone portamento")}, + {VOLCMD_PORTAUP, MOD_TYPE_ITMPT, _T("Portamento up")}, + {VOLCMD_PORTADOWN, MOD_TYPE_ITMPT, _T("Portamento down")}, + {VOLCMD_PLAYCONTROL, MOD_TYPE_NONE, _T("Play Control")}, + {VOLCMD_OFFSET, MOD_TYPE_MPT, _T("Sample Cue")}, +}; + +static_assert(mpt::array_size<decltype(gVolCmdInfo)>::size == (MAX_VOLCMDS - 1)); + + +UINT EffectInfo::GetNumVolCmds() const +{ + return static_cast<UINT>(std::size(gVolCmdInfo)); +} + + +LONG EffectInfo::GetIndexFromVolCmd(ModCommand::VOLCMD volcmd) const +{ + for (UINT i = 0; i < std::size(gVolCmdInfo); i++) + { + if (gVolCmdInfo[i].volCmd == volcmd) return i; + } + return -1; +} + + +VolumeCommand EffectInfo::GetVolCmdFromIndex(UINT ndx) const +{ + return (ndx < std::size(gVolCmdInfo)) ? gVolCmdInfo[ndx].volCmd : VOLCMD_NONE; +} + + +bool EffectInfo::GetVolCmdInfo(UINT ndx, CString *s, ModCommand::VOL *prangeMin, ModCommand::VOL *prangeMax) const +{ + if (s) s->Empty(); + if (prangeMin) *prangeMin = 0; + if (prangeMax) *prangeMax = 0; + if (ndx >= std::size(gVolCmdInfo)) return false; + if (s) + { + s->Format(_T("%c: %s"), sndFile.GetModSpecifications().GetVolEffectLetter(GetVolCmdFromIndex(ndx)), gVolCmdInfo[ndx].name); + } + if ((prangeMin) && (prangeMax)) + { + switch(gVolCmdInfo[ndx].volCmd) + { + case VOLCMD_VOLUME: + case VOLCMD_PANNING: + *prangeMax = 64; + break; + + default: + *prangeMax = (sndFile.GetType() & MOD_TYPE_XM) ? 15 : 9; + } + } + return (sndFile.GetType() & gVolCmdInfo[ndx].supportedFormats); +} + + +bool EffectInfo::GetVolCmdParamInfo(const ModCommand &m, CString *s) const +{ + if(s == nullptr) return false; + s->Empty(); + + switch(m.volcmd) + { + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + case VOLCMD_FINEVOLUP: + case VOLCMD_FINEVOLDOWN: + if(m.vol > 0 || sndFile.GetType() == MOD_TYPE_XM) + { + s->Format(_T("%c%u"), + (m.volcmd == VOLCMD_VOLSLIDEUP || m.volcmd == VOLCMD_FINEVOLUP) ? _T('+') : _T('-'), + m.vol); + } else + { + *s = _T("continue"); + } + break; + + case VOLCMD_PORTAUP: + case VOLCMD_PORTADOWN: + case VOLCMD_TONEPORTAMENTO: + if(m.vol > 0) + { + ModCommand::PARAM param = m.vol << 2; + ModCommand::COMMAND cmd = CMD_PORTAMENTOUP; + if(m.volcmd == VOLCMD_PORTADOWN) + { + cmd = CMD_PORTAMENTODOWN; + } else if(m.volcmd == VOLCMD_TONEPORTAMENTO) + { + cmd = CMD_TONEPORTAMENTO; + if(sndFile.GetType() != MOD_TYPE_XM) param = ImpulseTrackerPortaVolCmd[m.vol & 0x0F]; + else param = m.vol << 4; + } + s->Format(_T("%u (%c%02X)"), + m.vol, + sndFile.GetModSpecifications().GetEffectLetter(cmd), + param); + } else + { + *s = _T("continue"); + } + break; + + case VOLCMD_OFFSET: + if(m.vol) + { + SAMPLEINDEX smp = m.instr; + if(smp > 0 && smp <= sndFile.GetNumInstruments() && m.IsNote() && sndFile.Instruments[smp] != nullptr) + { + smp = sndFile.Instruments[smp]->Keyboard[m.note - NOTE_MIN]; + } + s->Format(_T("Cue %u: "), m.vol); + if(smp > 0 && smp <= sndFile.GetNumSamples() && m.vol > 0 && m.vol <= std::size(sndFile.GetSample(smp).cues)) + { + auto cue = sndFile.GetSample(smp).cues[m.vol - 1]; + if(cue < sndFile.GetSample(smp).nLength) + s->Append(mpt::cfmt::dec(3, _T(','), sndFile.GetSample(smp).cues[m.vol - 1])); + else + s->Append(_T("unused")); + } else + s->Append(_T("unknown")); + } else + { + *s = _T("continue"); + } + break; + + case VOLCMD_PLAYCONTROL: + if(m.vol == 0) + *s = _T("Pause Playback"); + else if(m.vol == 1) + *s = _T("Continue Playback"); + break; + + default: + s->Format(_T("%u"), m.vol); + break; + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.h b/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.h new file mode 100644 index 00000000..b1432121 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/EffectInfo.h @@ -0,0 +1,67 @@ +/* + * EffectInfo.h + * ------------ + * Purpose: Provide information about effect names, parameter interpretation to the tracker interface. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "modcommand.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +class EffectInfo +{ +protected: + const CSoundFile &sndFile; + +public: + + EffectInfo(const CSoundFile &sf) : sndFile(sf) {}; + + // Effects Description + + bool GetEffectName(CString &pszDescription, ModCommand::COMMAND command, UINT param, bool bXX = false) const; // bXX: Nxx: ... + // Get size of list of known effect commands + UINT GetNumEffects() const; + // Get range information, effect name, etc... from a given effect. + bool GetEffectInfo(UINT ndx, CString *s, bool bXX = false, ModCommand::PARAM *prangeMin = nullptr, ModCommand::PARAM *prangeMax = nullptr) const; + // Get effect index in effect list from effect command + param + LONG GetIndexFromEffect(ModCommand::COMMAND command, ModCommand::PARAM param) const; + // Get effect command + param from effect index + EffectCommand GetEffectFromIndex(UINT ndx, ModCommand::PARAM &refParam) const; + EffectCommand GetEffectFromIndex(UINT ndx) const; + // Get parameter mask from effect (for extended effects) + UINT GetEffectMaskFromIndex(UINT ndx) const; + // Get precise effect name, also with explanation of effect parameter + bool GetEffectNameEx(CString &pszName, const ModCommand &m, uint32 param, CHANNELINDEX chn) const; + // Check whether an effect is extended (with parameter nibbles) + bool IsExtendedEffect(UINT ndx) const; + // Map an effect value to slider position + UINT MapValueToPos(UINT ndx, UINT param) const; + // Map slider position to an effect value + UINT MapPosToValue(UINT ndx, UINT pos) const; + + // Volume column effects description + + // Get size of list of known volume commands + UINT GetNumVolCmds() const; + // Get effect index in effect list from volume command + LONG GetIndexFromVolCmd(ModCommand::VOLCMD volcmd) const; + // Get volume command from effect index + VolumeCommand GetVolCmdFromIndex(UINT ndx) const; + // Get range information, effect name, etc... from a given effect. + bool GetVolCmdInfo(UINT ndx, CString *s, ModCommand::VOL *prangeMin = nullptr, ModCommand::VOL *prangeMax = nullptr) const; + // Get effect name and parameter description + bool GetVolCmdParamInfo(const ModCommand &m, CString *s) const; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.cpp new file mode 100644 index 00000000..1a6a2e22 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.cpp @@ -0,0 +1,873 @@ +/* + * EffectVis.cpp + * ------------- + * Purpose: Implementation of parameter visualisation dialog. + * Notes : (currenlty 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 "Childfrm.h" +#include "Moddoc.h" +#include "Globals.h" +#include "View_pat.h" +#include "EffectVis.h" + + +OPENMPT_NAMESPACE_BEGIN + +CEffectVis::EditAction CEffectVis::m_nAction = CEffectVis::kAction_OverwriteFX; + +IMPLEMENT_DYNAMIC(CEffectVis, CDialog) +CEffectVis::CEffectVis(CViewPattern *pViewPattern, ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, CModDoc &modDoc, PATTERNINDEX pat) + : effectInfo(modDoc.GetSoundFile()) + , m_ModDoc(modDoc) + , m_SndFile(modDoc.GetSoundFile()) + , m_pViewPattern(pViewPattern) +{ + m_nFillEffect = effectInfo.GetIndexFromEffect(CMD_SMOOTHMIDI, 0); + m_templatePCNote.Set(NOTE_PCS, 1, 0, 0); + UpdateSelection(startRow, endRow, nchn, pat); +} + +BEGIN_MESSAGE_MAP(CEffectVis, CDialog) + ON_WM_ERASEBKGND() + ON_WM_PAINT() + ON_WM_SIZE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONUP() + ON_WM_MOUSEMOVE() + ON_WM_RBUTTONDOWN() + ON_WM_RBUTTONUP() + ON_CBN_SELCHANGE(IDC_VISACTION, &CEffectVis::OnActionChanged) + ON_CBN_SELCHANGE(IDC_VISEFFECTLIST, &CEffectVis::OnEffectChanged) +END_MESSAGE_MAP() + +void CEffectVis::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_VISSTATUS, m_edVisStatus); + DDX_Control(pDX, IDC_VISEFFECTLIST, m_cmbEffectList); + DDX_Control(pDX, IDC_VISACTION, m_cmbActionList); +} + +void CEffectVis::OnActionChanged() +{ + m_nAction = static_cast<EditAction>(m_cmbActionList.GetItemData(m_cmbActionList.GetCurSel())); + if (m_nAction == kAction_FillPC + || m_nAction == kAction_OverwritePC + || m_nAction == kAction_Preserve) + m_cmbEffectList.EnableWindow(FALSE); + else + m_cmbEffectList.EnableWindow(TRUE); + +} + +void CEffectVis::OnEffectChanged() +{ + m_nFillEffect = static_cast<UINT>(m_cmbEffectList.GetItemData(m_cmbEffectList.GetCurSel())); +} + +void CEffectVis::OnPaint() +{ + CPaintDC dc(this); // device context for painting + ShowVis(&dc); + +} + +uint16 CEffectVis::GetParam(ROWINDEX row) const +{ + uint16 paramValue = 0; + + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + { + const ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + if (m.IsPcNote()) + { + paramValue = m.GetValueEffectCol(); + } else + { + paramValue = m.param; + } + } + + return paramValue; +} + +// Sets a row's param value based on the vertical cursor position. +// Sets either plain pattern effect parameter or PC note parameter +// as appropriate, depending on contents of row. +void CEffectVis::SetParamFromY(ROWINDEX row, int y) +{ + if(!m_SndFile.Patterns.IsValidPat(m_nPattern)) + return; + + ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + if (IsPcNote(row)) + { + uint16 param = ScreenYToPCParam(y); + m.SetValueEffectCol(param); + } else + { + ModCommand::PARAM param = ScreenYToFXParam(y); + // Cap the parameter value as appropriate, based on effect type (e.g. Zxx gets capped to [0x00,0x7F]) + effectInfo.GetEffectFromIndex(effectInfo.GetIndexFromEffect(m.command, param), param); + m.param = param; + } +} + + +EffectCommand CEffectVis::GetCommand(ROWINDEX row) const +{ + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + return static_cast<EffectCommand>(m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->command); + else + return CMD_NONE; +} + +void CEffectVis::SetCommand(ROWINDEX row, EffectCommand command) +{ + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + { + ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + if(m.IsPcNote()) + { + // Clear PC note + m.note = 0; + m.instr = 0; + m.volcmd = VOLCMD_NONE; + m.vol = 0; + } + m.command = command; + } +} + +int CEffectVis::RowToScreenX(ROWINDEX row) const +{ + if ((row >= m_startRow) || (row <= m_endRow)) + return mpt::saturate_round<int>(m_rcDraw.left + m_innerBorder + (row - m_startRow) * m_pixelsPerRow); + return -1; +} + + +int CEffectVis::RowToScreenY(ROWINDEX row) const +{ + int screenY = -1; + + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + { + const ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + if (m.IsPcNote()) + { + uint16 paramValue = m.GetValueEffectCol(); + screenY = PCParamToScreenY(paramValue); + } else + { + uint16 paramValue = m.param; + screenY = FXParamToScreenY(paramValue); + } + } + + return screenY; +} + +int CEffectVis::FXParamToScreenY(uint16 param) const +{ + if(param >= 0x00 && param <= 0xFF) + return mpt::saturate_round<int>(m_rcDraw.bottom - param * m_pixelsPerFXParam); + return -1; +} + +int CEffectVis::PCParamToScreenY(uint16 param) const +{ + if(param >= 0x00 && param <= ModCommand::maxColumnValue) + return mpt::saturate_round<int>(m_rcDraw.bottom - param*m_pixelsPerPCParam); + return -1; +} + +ModCommand::PARAM CEffectVis::ScreenYToFXParam(int y) const +{ + if(y <= FXParamToScreenY(0xFF)) + return 0xFF; + + if(y >= FXParamToScreenY(0x00)) + return 0x00; + + return mpt::saturate_round<ModCommand::PARAM>((m_rcDraw.bottom - y) / m_pixelsPerFXParam); +} + +uint16 CEffectVis::ScreenYToPCParam(int y) const +{ + if(y <= PCParamToScreenY(ModCommand::maxColumnValue)) + return ModCommand::maxColumnValue; + + if(y >= PCParamToScreenY(0x00)) + return 0x00; + + return mpt::saturate_round<uint16>((m_rcDraw.bottom - y) / m_pixelsPerPCParam); +} + +ROWINDEX CEffectVis::ScreenXToRow(int x) const +{ + if(x <= RowToScreenX(m_startRow)) + return m_startRow; + + if(x >= RowToScreenX(m_endRow)) + return m_endRow; + + return mpt::saturate_round<ROWINDEX>(m_startRow + (x - m_innerBorder) / m_pixelsPerRow); +} + + +void CEffectVis::DrawGrid() +{ + // Lots of room for optimisation here. + // Draw vertical grid lines + ROWINDEX nBeat = m_SndFile.m_nDefaultRowsPerBeat, nMeasure = m_SndFile.m_nDefaultRowsPerMeasure; + if(m_SndFile.Patterns[m_nPattern].GetOverrideSignature()) + { + nBeat = m_SndFile.Patterns[m_nPattern].GetRowsPerBeat(); + nMeasure = m_SndFile.Patterns[m_nPattern].GetRowsPerMeasure(); + } + + m_dcGrid.FillSolidRect(&m_rcDraw, 0); + auto oldPen = m_dcGrid.SelectStockObject(DC_PEN); + for(ROWINDEX row = m_startRow; row <= m_endRow; row++) + { + if(row % nMeasure == 0) + m_dcGrid.SetDCPenColor(RGB(0xFF, 0xFF, 0xFF)); + else if(row % nBeat == 0) + m_dcGrid.SetDCPenColor(RGB(0x99, 0x99, 0x99)); + else + m_dcGrid.SetDCPenColor(RGB(0x55, 0x55, 0x55)); + int x1 = RowToScreenX(row); + m_dcGrid.MoveTo(x1, m_rcDraw.top); + m_dcGrid.LineTo(x1, m_rcDraw.bottom); + } + + // Draw horizontal grid lines + constexpr UINT numHorizontalLines = 4; + for(UINT i = 0; i < numHorizontalLines; i++) + { + COLORREF c = 0; + switch(i % 4) + { + case 0: c = RGB(0x00, 0x00, 0x00); break; + case 1: c = RGB(0x40, 0x40, 0x40); break; + case 2: c = RGB(0x80, 0x80, 0x80); break; + case 3: c = RGB(0xCC, 0xCC, 0xCC); break; + } + m_dcGrid.SetDCPenColor(c); + int y1 = m_rcDraw.bottom / numHorizontalLines * i; + m_dcGrid.MoveTo(m_rcDraw.left + m_innerBorder, y1); + m_dcGrid.LineTo(m_rcDraw.right - m_innerBorder, y1); + } + m_dcGrid.SelectObject(oldPen); +} + + +void CEffectVis::SetPlayCursor(PATTERNINDEX nPat, ROWINDEX nRow) +{ + if(nPat == m_nPattern && nRow == m_nOldPlayPos) + return; + + if(m_nOldPlayPos >= m_startRow && m_nOldPlayPos <= m_endRow) + { + // erase current playpos + int x1 = RowToScreenX(m_nOldPlayPos); + m_dcPlayPos.SelectStockObject(BLACK_PEN); + m_dcPlayPos.MoveTo(x1,m_rcDraw.top); + m_dcPlayPos.LineTo(x1,m_rcDraw.bottom); + } + + if((nRow < m_startRow) || (nRow > m_endRow) || (nPat != m_nPattern)) + return; + + int x1 = RowToScreenX(nRow); + m_dcPlayPos.SelectStockObject(DC_PEN); + m_dcPlayPos.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_SAMPLE]); + m_dcPlayPos.MoveTo(x1,m_rcDraw.top); + m_dcPlayPos.LineTo(x1,m_rcDraw.bottom); + + m_nOldPlayPos = nRow; + InvalidateRect(NULL, FALSE); +} + + +void CEffectVis::ShowVis(CDC *pDC) +{ + if (m_forceRedraw) + { + m_forceRedraw = false; + + // if we already have a memory dc, destroy it (this occurs for a re-size) + if (m_dcGrid.m_hDC) + { + m_dcGrid.SelectObject(m_pbOldGrid); + m_dcGrid.DeleteDC(); + + m_dcNodes.SelectObject(m_pbOldNodes); + m_dcNodes.DeleteDC(); + + m_dcPlayPos.SelectObject(m_pbOldPlayPos); + m_dcPlayPos.DeleteDC(); + + m_bPlayPos.DeleteObject(); + m_bGrid.DeleteObject(); + m_bNodes.DeleteObject(); + } + + // create a memory based dc for drawing the grid + m_dcGrid.CreateCompatibleDC(pDC); + m_bGrid.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height()); + m_pbOldGrid = *m_dcGrid.SelectObject(&m_bGrid); + + // create a memory based dc for drawing the nodes + m_dcNodes.CreateCompatibleDC(pDC); + m_bNodes.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height()); + m_pbOldNodes = *m_dcNodes.SelectObject(&m_bNodes); + + // create a memory based dc for drawing the nodes + m_dcPlayPos.CreateCompatibleDC(pDC); + m_bPlayPos.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height()); + m_pbOldPlayPos = *m_dcPlayPos.SelectObject(&m_bPlayPos); + + SetPlayCursor(m_nPattern, m_nOldPlayPos); + DrawGrid(); + DrawNodes(); + } + + // display the new image, combining the nodes with the grid + ShowVisImage(pDC); + +} + + +void CEffectVis::ShowVisImage(CDC *pDC) +{ + // to avoid flicker, establish a memory dc, draw to it + // and then BitBlt it to the destination "pDC" + CDC memDC; + memDC.CreateCompatibleDC(pDC); + if (!memDC) + return; + + CBitmap memBitmap; + memBitmap.CreateCompatibleBitmap(pDC, m_rcDraw.Width(), m_rcDraw.Height()); + CBitmap *oldBitmap = memDC.SelectObject(&memBitmap); + + // make sure we have the bitmaps + if (!m_dcGrid.m_hDC) + return; + if (!m_dcNodes.m_hDC) + return; + if (!m_dcPlayPos.m_hDC) + return; + + if (memDC.m_hDC != nullptr) + { + // draw the grid + memDC.BitBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcGrid, 0, 0, SRCCOPY); + + // merge the nodes image with the grid + memDC.TransparentBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcNodes, 0, 0, m_rcDraw.Width(), m_rcDraw.Height(), 0x00000000); + // further merge the playpos + memDC.TransparentBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &m_dcPlayPos, 0, 0, m_rcDraw.Width(), m_rcDraw.Height(), 0x00000000); + + // copy the resulting bitmap to the destination + pDC->BitBlt(0, 0, m_rcDraw.Width(), m_rcDraw.Height(), &memDC, 0, 0, SRCCOPY); + } + + memDC.SelectObject(oldBitmap); + +} + + +void CEffectVis::DrawNodes() +{ + if(m_rcDraw.IsRectEmpty()) + return; + + //Draw + const int lineWidth = Util::ScalePixels(1, m_hWnd); + const int nodeSizeHalf = m_nodeSizeHalf; + const int nodeSizeHalf2 = nodeSizeHalf - lineWidth + 1; + const int nodeSize = 2 * nodeSizeHalf + 1; + + //erase + if ((ROWINDEX)m_nRowToErase < m_startRow || m_nParamToErase < 0) + { + m_dcNodes.FillSolidRect(&m_rcDraw, 0); + } else + { + int x = RowToScreenX(m_nRowToErase); + CRect r(x - nodeSizeHalf, m_rcDraw.top, x + nodeSizeHalf + 1, m_rcDraw.bottom); + m_dcNodes.FillSolidRect(&r, 0); + } + + for (ROWINDEX row = m_startRow; row <= m_endRow; row++) + { + COLORREF col = IsPcNote(row) ? RGB(0xFF, 0xFF, 0x00) : RGB(0xD0, 0xFF, 0xFF); + int x = RowToScreenX(row); + int y = RowToScreenY(row); + m_dcNodes.FillSolidRect(x - nodeSizeHalf, y - nodeSizeHalf, nodeSize, lineWidth, col); // Top + m_dcNodes.FillSolidRect(x + nodeSizeHalf2, y - nodeSizeHalf, lineWidth, nodeSize, col); // Right + m_dcNodes.FillSolidRect(x - nodeSizeHalf, y + nodeSizeHalf2, nodeSize, lineWidth, col); // Bottom + m_dcNodes.FillSolidRect(x - nodeSizeHalf, y - nodeSizeHalf, lineWidth, nodeSize, col); // Left + } + +} + +void CEffectVis::InvalidateRow(int row) +{ + if (((UINT)row < m_startRow) || ((UINT)row > m_endRow)) return; + +//It seems this optimisation doesn't work properly yet. Disable in Update() + + int x = RowToScreenX(row); + invalidated.bottom = m_rcDraw.bottom; + invalidated.top = m_rcDraw.top; + invalidated.left = x - m_nodeSizeHalf; + invalidated.right = x + m_nodeSizeHalf + 1; + InvalidateRect(&invalidated, FALSE); +} + + +void CEffectVis::OpenEditor(CWnd *parent) +{ + Create(IDD_EFFECTVISUALIZER, parent); + m_forceRedraw = true; + + if(TrackerSettings::Instance().effectVisWidth > 0 && TrackerSettings::Instance().effectVisHeight > 0) + { + WINDOWPLACEMENT wnd; + wnd.length = sizeof(wnd); + GetWindowPlacement(&wnd); + wnd.showCmd = SW_SHOWNOACTIVATE; + CRect rect = wnd.rcNormalPosition; + if(TrackerSettings::Instance().effectVisX > int32_min && TrackerSettings::Instance().effectVisY > int32_min) + { + CRect mainRect; + CMainFrame::GetMainFrame()->GetWindowRect(mainRect); + rect.left = mainRect.left + MulDiv(TrackerSettings::Instance().effectVisX, Util::GetDPIx(m_hWnd), 96); + rect.top = mainRect.top + MulDiv(TrackerSettings::Instance().effectVisY, Util::GetDPIx(m_hWnd), 96); + } + rect.right = rect.left + MulDiv(TrackerSettings::Instance().effectVisWidth, Util::GetDPIx(m_hWnd), 96); + rect.bottom = rect.top + MulDiv(TrackerSettings::Instance().effectVisHeight, Util::GetDPIx(m_hWnd), 96); + wnd.rcNormalPosition = rect; + SetWindowPlacement(&wnd); + } + + ShowWindow(SW_SHOW); +} + + +void CEffectVis::OnClose() +{ + DoClose(); +} + + +void CEffectVis::OnOK() +{ + OnClose(); +} + + +void CEffectVis::OnCancel() +{ + OnClose(); +} + + +void CEffectVis::DoClose() +{ + WINDOWPLACEMENT wnd; + wnd.length = sizeof(wnd); + GetWindowPlacement(&wnd); + CRect mainRect; + CMainFrame::GetMainFrame()->GetWindowRect(mainRect); + + CRect rect = wnd.rcNormalPosition; + rect.MoveToXY(rect.left - mainRect.left, rect.top - mainRect.top); + TrackerSettings::Instance().effectVisWidth = MulDiv(rect.Width(), 96, Util::GetDPIx(m_hWnd)); + TrackerSettings::Instance().effectVisHeight = MulDiv(rect.Height(), 96, Util::GetDPIy(m_hWnd)); + TrackerSettings::Instance().effectVisX = MulDiv(rect.left, 96, Util::GetDPIx(m_hWnd)); + TrackerSettings::Instance().effectVisY = MulDiv(rect.top, 96, Util::GetDPIy(m_hWnd)); + + m_dcGrid.SelectObject(m_pbOldGrid); + m_dcGrid.DeleteDC(); + m_dcNodes.SelectObject(m_pbOldNodes); + m_dcNodes.DeleteDC(); + m_dcPlayPos.SelectObject(m_pbOldPlayPos); + m_dcPlayPos.DeleteDC(); + + m_bGrid.DeleteObject(); + m_bNodes.DeleteObject(); + m_bPlayPos.DeleteObject(); + + DestroyWindow(); +} + + +void CEffectVis::PostNcDestroy() +{ + m_pViewPattern->m_pEffectVis = nullptr; +} + + +void CEffectVis::OnSize(UINT nType, int cx, int cy) +{ + MPT_UNREFERENCED_PARAMETER(nType); + MPT_UNREFERENCED_PARAMETER(cx); + MPT_UNREFERENCED_PARAMETER(cy); + GetClientRect(&m_rcFullWin); + m_rcDraw.SetRect(m_rcFullWin.left, m_rcFullWin.top, m_rcFullWin.right, m_rcFullWin.bottom - m_marginBottom); + + const int actionListWidth = Util::ScalePixels(170, m_hWnd); + const int commandListWidth = Util::ScalePixels(160, m_hWnd); + + if (IsWindow(m_edVisStatus.m_hWnd)) + m_edVisStatus.SetWindowPos(this, m_rcFullWin.left, m_rcDraw.bottom, m_rcFullWin.right-commandListWidth-actionListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER); + if (IsWindow(m_cmbActionList)) + m_cmbActionList.SetWindowPos(this, m_rcFullWin.right-commandListWidth-actionListWidth, m_rcDraw.bottom, actionListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER); + if (IsWindow(m_cmbEffectList)) + m_cmbEffectList.SetWindowPos(this, m_rcFullWin.right-commandListWidth, m_rcDraw.bottom, commandListWidth, m_rcFullWin.bottom-m_rcDraw.bottom, SWP_NOACTIVATE|SWP_NOCOPYBITS|SWP_SHOWWINDOW|SWP_NOZORDER); + + if(m_nRows) + m_pixelsPerRow = (float)(m_rcDraw.Width() - m_innerBorder * 2) / (float)m_nRows; + else + m_pixelsPerRow = 1; + m_pixelsPerFXParam = (float)(m_rcDraw.Height())/(float)0xFF; + m_pixelsPerPCParam = (float)(m_rcDraw.Height())/(float)ModCommand::maxColumnValue; + m_forceRedraw = true; + InvalidateRect(NULL, FALSE); //redraw everything +} + +void CEffectVis::Update() +{ + DrawNodes(); + if (::IsWindow(m_hWnd)) + { + OnPaint(); + if (m_nRowToErase<0) + InvalidateRect(NULL, FALSE); // redraw everything + else + { + InvalidateRow(m_nRowToErase); + m_nParamToErase=-1; + m_nRowToErase=-1; + } + + } +} + +void CEffectVis::UpdateSelection(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, PATTERNINDEX pat) +{ + m_startRow = startRow; + m_endRow = endRow; + m_nRows = endRow - startRow; + m_nChan = nchn; + m_nPattern = pat; + + //Check pattern, start row and channel exist + if(!m_SndFile.Patterns.IsValidPat(m_nPattern) || !m_SndFile.Patterns[m_nPattern].IsValidRow(m_startRow) || m_nChan >= m_SndFile.GetNumChannels()) + { + DoClose(); + return; + } + + //Check end exists + if(!m_SndFile.Patterns[m_nPattern].IsValidRow(m_endRow)) + { + m_endRow = m_SndFile.Patterns[m_nPattern].GetNumRows() - 1; + } + + if(m_nRows) + m_pixelsPerRow = (float)(m_rcDraw.Width() - m_innerBorder * 2) / (float)m_nRows; + else + m_pixelsPerRow = 1; + m_pixelsPerFXParam = (float)(m_rcDraw.Height())/(float)0xFF; + m_pixelsPerPCParam = (float)(m_rcDraw.Height())/(float)ModCommand::maxColumnValue; + + m_forceRedraw = true; + Update(); + +} + +void CEffectVis::OnRButtonDown(UINT nFlags, CPoint point) +{ + if (!(m_dwStatus & FXVSTATUS_LDRAGGING)) + { + SetFocus(); + SetCapture(); + + m_nDragItem = ScreenXToRow(point.x); + m_dwStatus |= FXVSTATUS_RDRAGGING; + m_ModDoc.GetPatternUndo().PrepareUndo(static_cast<PATTERNINDEX>(m_nPattern), m_nChan, m_nDragItem, 1, 1, "Parameter Editor entry"); + OnMouseMove(nFlags, point); + } + + CDialog::OnRButtonDown(nFlags, point); +} + +void CEffectVis::OnRButtonUp(UINT nFlags, CPoint point) +{ + ReleaseCapture(); + m_dwStatus = 0x00; + m_nDragItem = -1; + CDialog::OnRButtonUp(nFlags, point); +} + +void CEffectVis::OnMouseMove(UINT nFlags, CPoint point) +{ + CDialog::OnMouseMove(nFlags, point); + + ROWINDEX row = ScreenXToRow(point.x); + + if ((m_dwStatus & FXVSTATUS_RDRAGGING) && (m_nDragItem>=0) ) + { + m_nRowToErase = m_nDragItem; + m_nParamToErase = GetParam(m_nDragItem); + + MakeChange(m_nDragItem, point.y); + } else if ((m_dwStatus & FXVSTATUS_LDRAGGING)) + { + // Interpolate if we detect that rows have been skipped but the left mouse button was not released. + // This ensures we produce a smooth curve even when we are not notified of mouse movements at a high frequency (e.g. if CPU usage is high) + const int steps = std::abs((int)row - (int)m_nLastDrawnRow); + if (m_nLastDrawnRow != ROWINDEX_INVALID && m_nLastDrawnRow > m_startRow && steps > 1) + { + int direction = ((int)(row - m_nLastDrawnRow) > 0) ? 1 : -1; + float factor = (float)(point.y - m_nLastDrawnY)/(float)steps + 0.5f; + + int currentRow; + for (int i=1; i<=steps; i++) + { + currentRow = m_nLastDrawnRow+(direction*i); + int interpolatedY = mpt::saturate_round<int>(m_nLastDrawnY + ((float)i * factor)); + MakeChange(currentRow, interpolatedY); + } + + //Don't use single value update + m_nRowToErase = -1; + m_nParamToErase = -1; + } else + { + m_nRowToErase = -1; + m_nParamToErase = -1; + MakeChange(row, point.y); + } + + // Remember last modified point in case we need to interpolate + m_nLastDrawnRow = row; + m_nLastDrawnY = point.y; + } + //update status bar + CString status; + CString effectName; + uint16 paramValue; + + + if (IsPcNote(row)) + { + paramValue = ScreenYToPCParam(point.y); + effectName.Format(_T("%s"), _T("Param Control")); // TODO - show smooth & plug+param + } else + { + paramValue = ScreenYToFXParam(point.y); + effectInfo.GetEffectInfo(effectInfo.GetIndexFromEffect(GetCommand(row), ModCommand::PARAM(GetParam(row))), &effectName, true); + } + + status.Format(_T("Pat: %d\tChn: %d\tRow: %d\tVal: %02X (%03d) [%s]"), + m_nPattern, m_nChan+1, static_cast<signed int>(row), paramValue, paramValue, effectName.GetString()); + m_edVisStatus.SetWindowText(status); +} + +void CEffectVis::OnLButtonDown(UINT nFlags, CPoint point) +{ + if (!(m_dwStatus & FXVSTATUS_RDRAGGING)) + { + SetFocus(); + SetCapture(); + + m_nDragItem = ScreenXToRow(point.x); + m_dwStatus |= FXVSTATUS_LDRAGGING; + m_ModDoc.GetPatternUndo().PrepareUndo(static_cast<PATTERNINDEX>(m_nPattern), m_nChan, m_startRow, 1, m_endRow - m_startRow + 1, "Parameter Editor entry"); + OnMouseMove(nFlags, point); + } + + CDialog::OnLButtonDown(nFlags, point); +} + +void CEffectVis::OnLButtonUp(UINT nFlags, CPoint point) +{ + ReleaseCapture(); + m_dwStatus = 0x00; + CDialog::OnLButtonUp(nFlags, point); + m_nLastDrawnRow = ROWINDEX_INVALID; +} + + +BOOL CEffectVis::OnInitDialog() +{ + CDialog::OnInitDialog(); + + int dpi = Util::GetDPIx(m_hWnd); + m_nodeSizeHalf = MulDiv(3, dpi, 96); + m_marginBottom = MulDiv(20, dpi, 96); + m_innerBorder = MulDiv(4, dpi, 96); + + // If first selected row is a PC event (or some other row but there aren't any other effects), default to PC note overwrite mode + // and use it as a template for new PC notes that will be created via the visualiser. + bool isPCevent = IsPcNote(m_startRow); + if(!isPCevent) + { + for(ROWINDEX row = m_startRow; row <= m_endRow; row++) + { + if(IsPcNote(row)) + { + isPCevent = true; + } else if(GetCommand(row) != CMD_NONE) + { + isPCevent = false; + break; + } + } + } + + if(m_ModDoc.GetModType() == MOD_TYPE_MPT && isPCevent) + { + m_nAction = kAction_OverwritePC; + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + { + ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(m_startRow, m_nChan); + m_templatePCNote.Set(m.note, m.instr, m.GetValueVolCol(), 0); + } + m_cmbEffectList.EnableWindow(FALSE); + } else + { + // Otherwise, default to FX overwrite and + // use effect of first selected row as default effect type + m_nAction = kAction_OverwriteFX; + m_nFillEffect = effectInfo.GetIndexFromEffect(GetCommand(m_startRow), ModCommand::PARAM(GetParam(m_startRow))); + if (m_nFillEffect < 0 || m_nFillEffect >= MAX_EFFECTS) + m_nFillEffect = effectInfo.GetIndexFromEffect(CMD_SMOOTHMIDI, 0); + } + + + CString s; + UINT numfx = effectInfo.GetNumEffects(); + m_cmbEffectList.ResetContent(); + int k; + for (UINT i=0; i<numfx; i++) + { + if (effectInfo.GetEffectInfo(i, &s, true)) + { + k =m_cmbEffectList.AddString(s); + m_cmbEffectList.SetItemData(k, i); + if ((int)i == m_nFillEffect) + m_cmbEffectList.SetCurSel(k); + } + } + + m_cmbActionList.ResetContent(); + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite with effect:")), kAction_OverwriteFX); + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite effect next to note:")), kAction_OverwriteFXWithNote); + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Fill blanks with effect:")), kAction_FillFX); + if (m_ModDoc.GetModType() == MOD_TYPE_MPT) + { + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Overwrite with PC note")), kAction_OverwritePC); + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Fill blanks with PC note")), kAction_FillPC); + } + m_cmbActionList.SetItemData(m_cmbActionList.AddString(_T("Never change effect type")), kAction_Preserve); + + m_cmbActionList.SetCurSel(m_nAction); + return true; +} + +void CEffectVis::MakeChange(ROWINDEX row, int y) +{ + if(!m_SndFile.Patterns.IsValidPat(m_nPattern)) + return; + + ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + + switch (m_nAction) + { + case kAction_FillFX: + // Only set command if there isn't a command already at this row and it's not a PC note + if (GetCommand(row) == CMD_NONE && !IsPcNote(row)) + { + SetCommand(row, effectInfo.GetEffectFromIndex(m_nFillEffect)); + } + // Always set param + SetParamFromY(row, y); + break; + + case kAction_OverwriteFXWithNote: + if(!m_SndFile.Patterns.IsValidPat(m_nPattern)) + break; + if(!m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->IsNote()) + break; + [[fallthrough]]; + case kAction_OverwriteFX: + // Always set command and param. Blows away any PC notes. + SetCommand(row, effectInfo.GetEffectFromIndex(m_nFillEffect)); + SetParamFromY(row, y); + break; + + case kAction_FillPC: + // Fill only empty slots with PC notes - leave other slots alone. + if (m.IsEmpty()) + { + SetPcNote(row); + } + // Always set param + SetParamFromY(row, y); + break; + + case kAction_OverwritePC: + // Always convert to PC Note and set param value + SetPcNote(row); + SetParamFromY(row, y); + break; + + case kAction_Preserve: + if (GetCommand(row) != CMD_NONE || IsPcNote(row)) + { + // Only set param if we have an effect type or if this is a PC note. + // Never change the effect type. + SetParamFromY(row, y); + } + break; + + } + + m_ModDoc.SetModified(); + m_ModDoc.UpdateAllViews(nullptr, PatternHint(m_nPattern).Data()); +} + +void CEffectVis::SetPcNote(ROWINDEX row) +{ + if(!m_SndFile.Patterns.IsValidPat(m_nPattern)) + return; + + ModCommand &m = *m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan); + m.Set(m_templatePCNote.note, m_templatePCNote.instr, m_templatePCNote.GetValueVolCol(), 0); +} + +bool CEffectVis::IsPcNote(ROWINDEX row) const +{ + if(m_SndFile.Patterns.IsValidPat(m_nPattern)) + return m_SndFile.Patterns[m_nPattern].GetpModCommand(row, m_nChan)->IsPcNote(); + else + return false; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.h b/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.h new file mode 100644 index 00000000..8ecad009 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/EffectVis.h @@ -0,0 +1,145 @@ +/* + * EffectVis.h + * ----------- + * Purpose: Implementation of parameter visualisation dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "EffectInfo.h" + +OPENMPT_NAMESPACE_BEGIN + +class CViewPattern; +class CModDoc; +class CSoundFile; + +#define FXVSTATUS_LDRAGGING 0x01 +#define FXVSTATUS_RDRAGGING 0x02 + +// EffectVis dialog +class CEffectVis : public CDialog +{ + DECLARE_DYNAMIC(CEffectVis) + +public: + enum EditAction + { + kAction_OverwriteFX, + kAction_OverwriteFXWithNote, + kAction_FillFX, + kAction_OverwritePC, + kAction_FillPC, + kAction_Preserve + }; + + CEffectVis(CViewPattern *pViewPattern, ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, CModDoc &modDoc, PATTERNINDEX pat); + + void UpdateSelection(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX nchn, PATTERNINDEX pat); + void Update(); + void OpenEditor(CWnd *parent); + void SetPlayCursor(PATTERNINDEX nPat, ROWINDEX nRow); + void DoClose(); + + afx_msg void OnSize(UINT nType, int cx, int cy); + +protected: + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + void PostNcDestroy() override; + + EffectInfo effectInfo; + + CBitmap m_bGrid, m_bNodes, m_bPlayPos; + HBITMAP m_pbOldGrid = nullptr, m_pbOldNodes = nullptr, m_pbOldPlayPos = nullptr; + CDC m_dcGrid, m_dcNodes, m_dcPlayPos; + + void DrawNodes(); + void DrawGrid(); + + void ShowVis(CDC *pDC); + void ShowVisImage(CDC *pDC); + RECT invalidated; + + ROWINDEX m_nLastDrawnRow = ROWINDEX_INVALID; // for interpolation + int m_nLastDrawnY = -1; // for interpolation + int m_nRowToErase = -1; + int m_nParamToErase = -1; + + int m_nodeSizeHalf; // Half width of a node; + int m_marginBottom; + int m_innerBorder; + + ROWINDEX m_nOldPlayPos = ROWINDEX_INVALID; + ModCommand m_templatePCNote; + +protected: + ROWINDEX m_startRow; + ROWINDEX m_endRow; + ROWINDEX m_nRows; + CHANNELINDEX m_nChan; + PATTERNINDEX m_nPattern; + int m_nFillEffect; + static EditAction m_nAction; + + int m_nDragItem = -1; + UINT m_nBtnMouseOver; + DWORD m_dwStatus = 0; + + float m_pixelsPerRow = 1, m_pixelsPerFXParam = 1, m_pixelsPerPCParam = 1; + + bool m_forceRedraw = true; + + void InvalidateRow(int row); + int RowToScreenX(ROWINDEX row) const; + int RowToScreenY(ROWINDEX row) const; + int PCParamToScreenY(uint16 param) const; + int FXParamToScreenY(uint16 param) const; + uint16 GetParam(ROWINDEX row) const; + EffectCommand GetCommand(ROWINDEX row) const; + void SetParamFromY(ROWINDEX row, int y); + void SetCommand(ROWINDEX row, EffectCommand cmd); + ModCommand::PARAM ScreenYToFXParam(int y) const; + uint16 ScreenYToPCParam(int y) const; + ROWINDEX ScreenXToRow(int x) const; + bool IsPcNote(ROWINDEX row) const; + void SetPcNote(ROWINDEX row); + + CModDoc &m_ModDoc; + CSoundFile &m_SndFile; + CRect m_rcDraw; + CRect m_rcFullWin; + + CComboBox m_cmbEffectList, m_cmbActionList; + CEdit m_edVisStatus; + + void OnOK() override; + void OnCancel() override; + afx_msg void OnClose(); + + CViewPattern *m_pViewPattern; + + + DECLARE_MESSAGE_MAP() + BOOL OnInitDialog() override; + afx_msg void OnPaint(); + +protected: + afx_msg void OnLButtonDown(UINT nFlags, CPoint point); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnRButtonDown(UINT nFlags, CPoint point); + afx_msg void OnRButtonUp(UINT nFlags, CPoint point); + afx_msg void OnEffectChanged(); + afx_msg void OnActionChanged(); + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + + void MakeChange(ROWINDEX currentRow, int newY); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.cpp new file mode 100644 index 00000000..c4d8c5d5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.cpp @@ -0,0 +1,853 @@ +/* + * ExceptionHandler.cpp + * -------------------- + * Purpose: Code for handling crashes (unhandled exceptions) 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 "Mainfrm.h" +#include "Mptrack.h" +#include "AboutDialog.h" +#include "InputHandler.h" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "Moddoc.h" +#include <shlwapi.h> +#include "ExceptionHandler.h" +#include "../misc/WriteMemoryDump.h" +#include "../common/version.h" +#include "../common/mptFileIO.h" +#include "../soundlib/mod_specifications.h" + +#include <atomic> + + +OPENMPT_NAMESPACE_BEGIN + + +// Write full memory dump instead of minidump. +bool ExceptionHandler::fullMemDump = false; + +bool ExceptionHandler::stopSoundDeviceOnCrash = true; + +bool ExceptionHandler::stopSoundDeviceBeforeDump = false; + +// Delegate to system-specific crash processing once our own crash handler is +// finished. This is useful to allow attaching a debugger. +bool ExceptionHandler::delegateToWindowsHandler = false; + +// Allow debugging the unhandled exception filter. Normally, if a debugger is +// attached, no exceptions are unhandled because the debugger handles them. If +// debugExceptionHandler is true, an additional __try/__catch is inserted around +// InitInstance(), ExitInstance() and the main message loop, which will call our +// filter, which then can be stepped through in a debugger. +bool ExceptionHandler::debugExceptionHandler = false; + + +bool ExceptionHandler::useAnyCrashHandler = false; +bool ExceptionHandler::useImplicitFallbackSEH = false; +bool ExceptionHandler::useExplicitSEH = false; +bool ExceptionHandler::handleStdTerminate = false; +bool ExceptionHandler::handleMfcExceptions = false; + + +static thread_local ExceptionHandler::Context *g_Context = nullptr; + +static LPTOP_LEVEL_EXCEPTION_FILTER g_OriginalUnhandledExceptionFilter = nullptr; +static std::terminate_handler g_OriginalTerminateHandler = nullptr; + +static UINT g_OriginalErrorMode = 0; + + +ExceptionHandler::Context *ExceptionHandler::SetContext(Context *newContext) noexcept +{ + Context *oldContext = g_Context; + g_Context = newContext; + return oldContext; +} + + +static std::atomic<int> & g_CrashCount() +{ + static std::atomic<int> s_CrashCount(0); + return s_CrashCount; +} + + +static std::atomic<int> & g_TaintCountDriver() +{ + static std::atomic<int> s_TaintCountDriver(0); + return s_TaintCountDriver; +} + + +static std::atomic<int> & g_TaintCountPlugin() +{ + static std::atomic<int> s_TaintCountPlugin(0); + return s_TaintCountPlugin; +} + + +void ExceptionHandler::TaintProcess(ExceptionHandler::TaintReason reason) +{ + switch(reason) + { + case ExceptionHandler::TaintReason::Driver: + g_TaintCountDriver().fetch_add(1); + break; + case ExceptionHandler::TaintReason::Plugin: + g_TaintCountPlugin().fetch_add(1); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } +} + + +enum DumpMode +{ + DumpModeCrash = 0, // crash + DumpModeWarning = 1, // assert + DumpModeDebug = 2, // debug output (e.g. trace log) +}; + + +struct CrashOutputDirectory +{ + bool valid; + mpt::PathString path; + CrashOutputDirectory() + : valid(true) + { + const mpt::PathString timestampDir = mpt::PathString::FromCString((CTime::GetCurrentTime()).Format(_T("%Y-%m-%d %H.%M.%S\\"))); + // Create a crash directory + path = mpt::GetTempDirectory() + P_("OpenMPT Crash Files\\"); + if(!path.IsDirectory()) + { + CreateDirectory(path.AsNative().c_str(), nullptr); + } + // Set compressed attribute in order to save disk space. + // Debugging information should clutter the users computer as little as possible. + // Performance is not important here. + // Ignore any errors. + SetFilesystemCompression(path); + // Compression will be inherited by children directories and files automatically. + path += timestampDir; + if(!path.IsDirectory()) + { + if(!CreateDirectory(path.AsNative().c_str(), nullptr)) + { + valid = false; + } + } + } +}; + + +class DebugReporter +{ +private: + int crashCount; + int taintCountDriver; + int taintCountPlugin; + bool stateFrozen; + const DumpMode mode; + const CrashOutputDirectory crashDirectory; + bool writtenMiniDump; + bool writtenTraceLog; + int rescuedFiles; +private: + static bool FreezeState(DumpMode mode); + static bool Cleanup(DumpMode mode); + bool GenerateDump(_EXCEPTION_POINTERS *pExceptionInfo); + bool GenerateTraceLog(); + int RescueFiles(); + bool HasWrittenDebug() const { return writtenMiniDump || writtenTraceLog; } + static void StopSoundDevice(); +public: + DebugReporter(DumpMode mode, _EXCEPTION_POINTERS *pExceptionInfo); + ~DebugReporter(); + void ReportError(mpt::ustring errorMessage); +}; + + +DebugReporter::DebugReporter(DumpMode mode, _EXCEPTION_POINTERS *pExceptionInfo) + : crashCount(g_CrashCount().fetch_add(1) + 1) + , taintCountDriver(g_TaintCountDriver().load()) + , taintCountPlugin(g_TaintCountPlugin().load()) + , stateFrozen(FreezeState(mode)) + , mode(mode) + , writtenMiniDump(false) + , writtenTraceLog(false) + , rescuedFiles(0) +{ + if(mode == DumpModeCrash || mode == DumpModeWarning) + { + writtenMiniDump = GenerateDump(pExceptionInfo); + } + if(mode == DumpModeCrash || mode == DumpModeWarning || mode == DumpModeDebug) + { + writtenTraceLog = GenerateTraceLog(); + } + if(mode == DumpModeCrash || mode == DumpModeWarning) + { + rescuedFiles = RescueFiles(); + } +} + + +DebugReporter::~DebugReporter() +{ + Cleanup(mode); +} + + +bool DebugReporter::GenerateDump(_EXCEPTION_POINTERS *pExceptionInfo) +{ + return WriteMemoryDump(pExceptionInfo, (crashDirectory.path + P_("crash.dmp")).AsNative().c_str(), ExceptionHandler::fullMemDump); +} + + +bool DebugReporter::GenerateTraceLog() +{ + return mpt::log::Trace::Dump(crashDirectory.path + P_("trace.log")); +} + + +static void SaveDocumentSafe(CModDoc *pModDoc, const mpt::PathString &filename) +{ + __try + { + pModDoc->OnSaveDocument(filename); + } __except(EXCEPTION_EXECUTE_HANDLER) + { + // nothing + } +} + + +// Rescue modified files... +int DebugReporter::RescueFiles() +{ + int numFiles = 0; + auto documents = theApp.GetOpenDocuments(); + for(auto modDoc : documents) + { + if(modDoc->IsModified()) + { + if(numFiles == 0) + { + // Show the rescue directory in Explorer... + CTrackApp::OpenDirectory(crashDirectory.path); + } + + mpt::PathString filename; + filename += crashDirectory.path; + filename += mpt::PathString::FromUnicode(mpt::ufmt::val(++numFiles)); + filename += P_("_"); + filename += mpt::PathString::FromCString(modDoc->GetTitle()).SanitizeComponent(); + filename += P_("."); + filename += mpt::PathString::FromUTF8(modDoc->GetSoundFile().GetModSpecifications().fileExtension); + + try + { + SaveDocumentSafe(modDoc, filename); + } catch(...) + { + continue; + } + } + } + return numFiles; +} + + +void DebugReporter::ReportError(mpt::ustring errorMessage) +{ + + if(!crashDirectory.valid) + { + errorMessage += UL_("\n\n"); + errorMessage += UL_("Could not create the following directory for saving debug information and modified files to:\n"); + errorMessage += crashDirectory.path.ToUnicode(); + } + + if(HasWrittenDebug()) + { + errorMessage += UL_("\n\n"); + errorMessage += UL_("Debug information has been saved to\n"); + errorMessage += crashDirectory.path.ToUnicode(); + } + + if(rescuedFiles > 0) + { + errorMessage += UL_("\n\n"); + if(rescuedFiles == 1) + { + errorMessage += UL_("1 modified file has been rescued, but it cannot be guaranteed that it is still intact."); + } else + { + errorMessage += MPT_UFORMAT("{} modified files have been rescued, but it cannot be guaranteed that they are still intact.")(rescuedFiles); + } + } + + errorMessage += UL_("\n\n"); + errorMessage += MPT_UFORMAT("OpenMPT {} {} ({} ({}))") + ( Build::GetVersionStringExtended() + , mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()) + , SourceInfo::Current().GetUrlWithRevision() + , SourceInfo::Current().GetStateString() + ); + + errorMessage += UL_("\n\n"); + errorMessage += MPT_UFORMAT("Session error count: {}\n")(crashCount); + if(taintCountDriver > 0 || taintCountPlugin > 0) + { + errorMessage += UL_("Process is in tainted state!\n"); + errorMessage += MPT_UFORMAT("Previously masked driver crashes: {}\n")(taintCountDriver); + errorMessage += MPT_UFORMAT("Previously masked plugin crashes: {}\n")(taintCountPlugin); + } + + errorMessage += UL_("\n"); + + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("error.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& f = sf; + f.imbue(std::locale::classic()); + f << mpt::replace(mpt::ToCharset(mpt::Charset::UTF8, errorMessage), std::string("\n"), std::string("\r\n")); + } + + if(auto ih = CMainFrame::GetInputHandler(); ih != nullptr) + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("last-commands.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream &f = sf; + f.imbue(std::locale::classic()); + + const auto commandSet = ih->m_activeCommandSet.get(); + f << "Last commands:\n"; + for(size_t i = 0; i < ih->m_lastCommands.size(); i++) + { + CommandID id = ih->m_lastCommands[(ih->m_lastCommandPos + i) % ih->m_lastCommands.size()]; + if(id == kcNull) + continue; + f << mpt::afmt::val(id); + if(commandSet) + f << " (" << mpt::ToCharset(mpt::Charset::UTF8, commandSet->GetCommandText(id)) << ")"; + f << "\n"; + } + } + + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("threads.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& f = sf; + f.imbue(std::locale::classic()); + f << MPT_AFORMAT("current : {}")(mpt::afmt::hex0<8>(GetCurrentThreadId())) << "\r\n"; + f << MPT_AFORMAT("GUI : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindGUI))) << "\r\n"; + f << MPT_AFORMAT("Audio : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindAudio))) << "\r\n"; + f << MPT_AFORMAT("Notify : {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindNotify))) << "\r\n"; + f << MPT_AFORMAT("WatchDir: {}")(mpt::afmt::hex0<8>(mpt::log::Trace::GetThreadId(mpt::log::Trace::ThreadKindWatchdir))) << "\r\n"; + } + + static constexpr struct { const mpt::uchar * section; const mpt::uchar * key; } configAnonymize[] = + { + { UL_("Version"), UL_("InstallGUID") }, + { UL_("Recent File List"), nullptr }, + }; + + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("active-settings.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& f = sf; + f.imbue(std::locale::classic()); + if(theApp.GetpSettings()) + { + SettingsContainer &settings = theApp.GetSettings(); + for(const auto &it : settings) + { + bool skipPath = false; + for(const auto &path : configAnonymize) + { + if((path.key == nullptr && path.section == it.first.GetRefSection()) // Omit entire section + || (path.key != nullptr && it.first == SettingPath(path.section, path.key))) // Omit specific key + { + skipPath = true; + } + } + if(skipPath) + { + continue; + } + f + << mpt::ToCharset(mpt::Charset::UTF8, it.first.FormatAsString() + U_(" = ") + it.second.GetRefValue().FormatValueAsString()) + << std::endl; + } + } + } + + { + const mpt::PathString crashStoredSettingsFilename = crashDirectory.path + P_("stored-mptrack.ini"); + CopyFile + ( theApp.GetConfigFileName().AsNative().c_str() + , crashStoredSettingsFilename.AsNative().c_str() + , FALSE + ); + IniFileSettingsContainer crashStoredSettings{crashStoredSettingsFilename}; + for(const auto &path : configAnonymize) + { + if(path.key) + { + crashStoredSettings.Write(SettingPath(path.section, path.key), SettingValue(mpt::ustring())); + } else + { + crashStoredSettings.Remove(path.section); + } + } + crashStoredSettings.Flush(); + } + + /* + // This is very slow, we instead write active-settings.txt above. + { + IniFileSettingsBackend f(crashDirectory.path + P_("active-mptrack.ini")); + if(theApp.GetpSettings()) + { + SettingsContainer & settings = theApp.GetSettings(); + for(const auto &it : settings) + { + f.WriteSetting(it.first, it.second.GetRefValue()); + } + } + } + */ + + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("about-openmpt.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& f = sf; + f.imbue(std::locale::classic()); + f << mpt::ToCharset(mpt::Charset::UTF8, CAboutDlg::GetTabText(0)); + } + + { + mpt::SafeOutputFile sf(crashDirectory.path + P_("about-components.txt"), std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& f = sf; + f.imbue(std::locale::classic()); + f << mpt::ToCharset(mpt::Charset::UTF8, CAboutDlg::GetTabText(1)); + } + + Reporting::Error(errorMessage, (mode == DumpModeWarning) ? "OpenMPT Warning" : "OpenMPT Crash", CMainFrame::GetMainFrame()); + +} + + +// Freezes the state as much as possible in order to avoid further confusion by +// other (possibly still running) threads +bool DebugReporter::FreezeState(DumpMode mode) +{ + MPT_TRACE(); + + // seal the trace log as early as possible + mpt::log::Trace::Seal(); + + if(mode == DumpModeCrash || mode == DumpModeWarning) + { + if(CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->gpSoundDevice && CMainFrame::GetMainFrame()->gpSoundDevice->DebugIsFragileDevice()) + { + // For fragile devices, always stop the device. Stop before the dumping if not in realtime context. + if(!CMainFrame::GetMainFrame()->gpSoundDevice->DebugInRealtimeCallback()) + { + StopSoundDevice(); + } + } else + { + if(ExceptionHandler::stopSoundDeviceOnCrash && ExceptionHandler::stopSoundDeviceBeforeDump) + { + StopSoundDevice(); + } + } + } + + return true; +} + + +static void StopSoundDeviceSafe(CMainFrame *pMainFrame) +{ + __try + { + if(pMainFrame->gpSoundDevice) + { + pMainFrame->gpSoundDevice->Close(); + } + if(pMainFrame->m_NotifyTimer) + { + pMainFrame->KillTimer(pMainFrame->m_NotifyTimer); + pMainFrame->m_NotifyTimer = 0; + } + } __except(EXCEPTION_EXECUTE_HANDLER) + { + // nothing + } +} + + +void DebugReporter::StopSoundDevice() +{ + CMainFrame* pMainFrame = CMainFrame::GetMainFrame(); + if(pMainFrame) + { + try + { + StopSoundDeviceSafe(pMainFrame); + } catch(...) + { + // nothing + } + } +} + + +bool DebugReporter::Cleanup(DumpMode mode) +{ + MPT_TRACE(); + + if(mode == DumpModeCrash || mode == DumpModeWarning) + { + if(CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->gpSoundDevice && CMainFrame::GetMainFrame()->gpSoundDevice->DebugIsFragileDevice()) + { + // For fragile devices, always stop the device. Stop after the dumping if in realtime context. + if(CMainFrame::GetMainFrame()->gpSoundDevice->DebugInRealtimeCallback()) + { + StopSoundDevice(); + } + } else + { + if(ExceptionHandler::stopSoundDeviceOnCrash && !ExceptionHandler::stopSoundDeviceBeforeDump) + { + StopSoundDevice(); + } + } + } + + return true; +} + + +// Different entry points for different situations in which we want to dump some information + + +static bool IsCxxException(_EXCEPTION_POINTERS *pExceptionInfo) +{ + if (!pExceptionInfo) + return false; + if (!pExceptionInfo->ExceptionRecord) + return false; + if (pExceptionInfo->ExceptionRecord->ExceptionCode != 0xE06D7363u) + return false; + return true; +} + + +template <typename E> +static const E * GetCxxException(_EXCEPTION_POINTERS *pExceptionInfo) +{ + // https://blogs.msdn.microsoft.com/oldnewthing/20100730-00/?p=13273 + struct info_a_t + { + DWORD bitmask; // Probably: 1=Const, 2=Volatile + DWORD destructor; // RVA (Relative Virtual Address) of destructor for that exception object + DWORD unknown; + DWORD catchableTypesPtr; // RVA of instance of type "B" + }; + struct info_c_t + { + DWORD someBitmask; + DWORD typeInfo; // RVA of std::type_info for that type + DWORD memberDisplacement; // Add to ExceptionInformation[1] in EXCEPTION_RECORD to obtain 'this' pointer. + DWORD virtBaseRelated1; // -1 if no virtual base + DWORD virtBaseRelated2; // ? + DWORD objectSize; // Size of the object in bytes + DWORD probablyCopyCtr; // RVA of copy constructor (?) + }; + if(!pExceptionInfo) + return nullptr; + if (!pExceptionInfo->ExceptionRecord) + return nullptr; + if(pExceptionInfo->ExceptionRecord->ExceptionCode != 0xE06D7363u) + return nullptr; + #ifdef _WIN64 + if(pExceptionInfo->ExceptionRecord->NumberParameters != 4) + return nullptr; + #else + if(pExceptionInfo->ExceptionRecord->NumberParameters != 3) + return nullptr; + #endif + if(pExceptionInfo->ExceptionRecord->ExceptionInformation[0] != 0x19930520u) + return nullptr; + std::uintptr_t base_address = 0; + #ifdef _WIN64 + base_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[3]; + #else + base_address = 0; + #endif + std::uintptr_t obj_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[1]; + if(!obj_address) + return nullptr; + std::uintptr_t info_a_address = pExceptionInfo->ExceptionRecord->ExceptionInformation[2]; + if(!info_a_address) + return nullptr; + const info_a_t * info_a = reinterpret_cast<const info_a_t *>(info_a_address); + std::uintptr_t info_b_offset = info_a->catchableTypesPtr; + if(!info_b_offset) + return nullptr; + const DWORD * info_b = reinterpret_cast<const DWORD *>(base_address + info_b_offset); + for(DWORD type = 1; type <= info_b[0]; ++type) + { + std::uintptr_t info_c_offset = info_b[type]; + if(!info_c_offset) + continue; + const info_c_t * info_c = reinterpret_cast<const info_c_t *>(base_address + info_c_offset); + if(!info_c->typeInfo) + continue; + const std::type_info * ti = reinterpret_cast<const std::type_info *>(base_address + info_c->typeInfo); + if(*ti != typeid(E)) + continue; + const E * e = reinterpret_cast<const E *>(obj_address + info_c->memberDisplacement); + return e; + } + return nullptr; +} + + +void ExceptionHandler::UnhandledMFCException(CException * e, const MSG * pMsg) +{ + DebugReporter report(DumpModeCrash, nullptr); + mpt::ustring errorMessage; + if(e && dynamic_cast<CSimpleException*>(e)) + { + TCHAR tmp[1024 + 1]; + MemsetZero(tmp); + if(dynamic_cast<CSimpleException*>(e)->GetErrorMessage(tmp, static_cast<UINT>(std::size(tmp) - 1)) != 0) + { + tmp[1024] = 0; + errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}': {}.") + (mpt::ufmt::dec(pMsg ? pMsg->message : 0) + , mpt::ToUnicode(CString(tmp)) + ); + } else + { + errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}': {}.") + (mpt::ufmt::dec(pMsg ? pMsg->message : 0) + , mpt::ToUnicode(CString(tmp)) + ); + } + } + else + { + errorMessage = MPT_UFORMAT("Unhandled MFC exception occurred while processming window message '{}'.") + ( mpt::ufmt::dec(pMsg ? pMsg->message : 0) + ); + } + report.ReportError(errorMessage); +} + + +static void UnhandledExceptionFilterImpl(_EXCEPTION_POINTERS *pExceptionInfo) +{ + DebugReporter report(DumpModeCrash, pExceptionInfo); + + mpt::ustring errorMessage; + const std::exception * pE = GetCxxException<std::exception>(pExceptionInfo); + if(g_Context) + { + if(!g_Context->description.empty()) + { + errorMessage += MPT_UFORMAT("OpenMPT detected a crash in '{}'.\nThis is very likely not an OpenMPT bug. Please report the problem to the respective software author.\n")(g_Context->description); + } else + { + errorMessage += MPT_UFORMAT("OpenMPT detected a crash in unknown foreign code.\nThis is likely not an OpenMPT bug.\n")(); + } + } + if(pE) + { + const std::exception & e = *pE; + errorMessage += MPT_UFORMAT("Unhandled C++ exception '{}' occurred at address 0x{}: '{}'.") + ( mpt::ToUnicode(mpt::Charset::ASCII, typeid(e).name()) + , mpt::ufmt::hex0<mpt::pointer_size*2>(reinterpret_cast<std::uintptr_t>(pExceptionInfo->ExceptionRecord->ExceptionAddress)) + , mpt::get_exception_text<mpt::ustring>(e) + ); + } else + { + errorMessage += MPT_UFORMAT("Unhandled exception 0x{} at address 0x{} occurred.") + ( mpt::ufmt::HEX0<8>(pExceptionInfo->ExceptionRecord->ExceptionCode) + , mpt::ufmt::hex0<mpt::pointer_size*2>(reinterpret_cast<std::uintptr_t>(pExceptionInfo->ExceptionRecord->ExceptionAddress)) + ); + } + report.ReportError(errorMessage); + +} + + +LONG ExceptionHandler::UnhandledExceptionFilterContinue(_EXCEPTION_POINTERS *pExceptionInfo) +{ + + UnhandledExceptionFilterImpl(pExceptionInfo); + + // Disable the call to std::terminate() as that would re-renter the crash + // handler another time, but with less information available. +#if 0 + // MSVC implements calling std::terminate by its own UnhandledExeptionFilter. + // However, we do overwrite it here, thus we have to call std::terminate + // ourselves. + if (IsCxxException(pExceptionInfo)) + { + std::terminate(); + } +#endif + + // Let a potential debugger handle the exception... + return EXCEPTION_CONTINUE_SEARCH; + +} + + +LONG ExceptionHandler::ExceptionFilter(_EXCEPTION_POINTERS *pExceptionInfo) +{ + UnhandledExceptionFilterImpl(pExceptionInfo); + // Let a potential debugger handle the exception... + return EXCEPTION_EXECUTE_HANDLER; +} + + +static void mpt_unexpected_handler(); +static void mpt_terminate_handler(); + + +void ExceptionHandler::Register() +{ + if(useImplicitFallbackSEH) + { + g_OriginalUnhandledExceptionFilter = ::SetUnhandledExceptionFilter(&UnhandledExceptionFilterContinue); + } + if(handleStdTerminate) + { + g_OriginalTerminateHandler = std::set_terminate(&mpt_terminate_handler); + } +} + + +void ExceptionHandler::ConfigureSystemHandler() +{ +#if (_WIN32_WINNT >= 0x0600) + if(delegateToWindowsHandler) + { + //SetErrorMode(0); + g_OriginalErrorMode = ::GetErrorMode(); + } else + { + g_OriginalErrorMode = ::SetErrorMode(::GetErrorMode() | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + } +#else // _WIN32_WINNT < 0x0600 + if(delegateToWindowsHandler) + { + g_OriginalErrorMode = ::SetErrorMode(0); + } else + { + g_OriginalErrorMode = ::SetErrorMode(::SetErrorMode(0) | SEM_FAILCRITICALERRORS | SEM_NOGPFAULTERRORBOX); + } +#endif // _WIN32_WINNT +} + + +void ExceptionHandler::UnconfigureSystemHandler() +{ + ::SetErrorMode(g_OriginalErrorMode); + g_OriginalErrorMode = 0; +} + + +void ExceptionHandler::Unregister() +{ + if(handleStdTerminate) + { + std::set_terminate(g_OriginalTerminateHandler); + g_OriginalTerminateHandler = nullptr; + } + if(useImplicitFallbackSEH) + { + ::SetUnhandledExceptionFilter(g_OriginalUnhandledExceptionFilter); + g_OriginalUnhandledExceptionFilter = nullptr; + } +} + + +static void mpt_terminate_handler() +{ + DebugReporter(DumpModeCrash, nullptr).ReportError(U_("A C++ runtime crash occurred: std::terminate() called.")); +#if 1 + std::abort(); +#else + if(g_OriginalTerminateHandler) + { + g_OriginalTerminateHandler(); + } else + { + std::abort(); + } +#endif +} + + +#if defined(MPT_ASSERT_HANDLER_NEEDED) + +MPT_NOINLINE void AssertHandler(const mpt::source_location &loc, const char *expr, const char *msg) +{ + DebugReporter report(msg ? DumpModeWarning : DumpModeCrash, nullptr); + if(IsDebuggerPresent()) + { + OutputDebugString(_T("ASSERT(")); + OutputDebugString(mpt::ToWin(mpt::Charset::ASCII, expr).c_str()); + OutputDebugString(_T(") failed\n")); + DebugBreak(); + } else + { + mpt::ustring errorMessage; + if(msg) + { + errorMessage = MPT_UFORMAT("Internal state inconsistency detected at {}({}). This is just a warning that could potentially lead to a crash later on: {} [{}].") + ( mpt::ToUnicode(mpt::Charset::ASCII, loc.file_name() ? loc.file_name() : "") + , loc.line() + , mpt::ToUnicode(mpt::Charset::ASCII, msg) + , mpt::ToUnicode(mpt::Charset::ASCII, loc.function_name() ? loc.function_name() : "") + ); + } else + { + errorMessage = MPT_UFORMAT("Internal error occurred at {}({}): ASSERT({}) failed in [{}].") + ( mpt::ToUnicode(mpt::Charset::ASCII, loc.file_name() ? loc.file_name() : "") + , loc.line() + , mpt::ToUnicode(mpt::Charset::ASCII, expr) + , mpt::ToUnicode(mpt::Charset::ASCII, loc.function_name() ? loc.function_name() : "") + ); + } + report.ReportError(errorMessage); + } +} + +#endif // MPT_ASSERT_HANDLER_NEEDED + + +void DebugInjectCrash() +{ + DebugReporter(DumpModeCrash, nullptr).ReportError(U_("Injected crash.")); +} + + +void DebugTraceDump() +{ + DebugReporter report(DumpModeDebug, nullptr); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.h b/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.h new file mode 100644 index 00000000..28235001 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ExceptionHandler.h @@ -0,0 +1,95 @@ +/* + * ExceptionHandler.h + * ------------------ + * Purpose: Code for handling crashes (unhandled exceptions) in OpenMPT. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class ExceptionHandler +{ + +public: + + struct Context + { + void SetDescription(mpt::ustring desc) + { + description = std::move(desc); + } + mpt::ustring description; + }; + +public: + + static bool fullMemDump; + static bool stopSoundDeviceOnCrash; + static bool stopSoundDeviceBeforeDump; + static bool delegateToWindowsHandler; + static bool debugExceptionHandler; + + // these are expected to be set on startup and never changed again + static bool useAnyCrashHandler; + static bool useImplicitFallbackSEH; + static bool useExplicitSEH; + static bool handleStdTerminate; + static bool handleStdUnexpected; + static bool handleMfcExceptions; + + // Call this to activate unhandled exception filtering + // and register a std::terminate_handler. + static void Register(); + static void ConfigureSystemHandler(); + static void UnconfigureSystemHandler(); + static void Unregister(); + + enum class TaintReason + { + Driver, + Plugin, + }; + + static void TaintProcess(TaintReason reason); + +public: + + static Context *SetContext(Context *newContext) noexcept; + + class ContextSetter + { + private: + Context *m_OldContext; + public: + inline ContextSetter(Context *newContext) noexcept + : m_OldContext(SetContext(newContext)) + { + return; + } + ContextSetter(const ContextSetter &) = delete; + ContextSetter &operator=(const ContextSetter &) = delete; + inline ~ContextSetter() + { + SetContext(m_OldContext); + } + }; + + static LONG WINAPI UnhandledExceptionFilterContinue(_EXCEPTION_POINTERS *pExceptionInfo); + static LONG WINAPI ExceptionFilter(_EXCEPTION_POINTERS *pExceptionInfo); + + static void UnhandledMFCException(CException * e, const MSG * pMsg); + +}; + +void DebugInjectCrash(); + +void DebugTraceDump(); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.cpp new file mode 100644 index 00000000..c19a64db --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.cpp @@ -0,0 +1,367 @@ +/* + * ExternalSamples.cpp + * ------------------- + * Purpose: Dialogs for locating missing external samples and handling modified samples + * 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 "Moddoc.h" +#include "ExternalSamples.h" +#include "FileDialog.h" +#include "FolderScanner.h" +#include "TrackerSettings.h" +#include "Reporting.h" +#include "resource.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(MissingExternalSamplesDlg, ResizableDialog) + //{{AFX_MSG_MAP(ExternalSamplesDlg) + ON_NOTIFY(NM_DBLCLK, IDC_LIST1, &MissingExternalSamplesDlg::OnSetPath) + ON_COMMAND(IDC_BUTTON1, &MissingExternalSamplesDlg::OnScanFolder) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void MissingExternalSamplesDlg::DoDataExchange(CDataExchange *pDX) +{ + ResizableDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST1, m_List); +} + + +MissingExternalSamplesDlg::MissingExternalSamplesDlg(CModDoc &modDoc, CWnd *parent) + : ResizableDialog(IDD_MISSINGSAMPLES, parent) + , m_modDoc(modDoc) + , m_sndFile(modDoc.GetSoundFile()) +{ +} + + +BOOL MissingExternalSamplesDlg::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + + // Initialize table + const CListCtrlEx::Header headers[] = + { + { _T("Sample"), 128, LVCFMT_LEFT }, + { _T("External Filename"), 308, LVCFMT_LEFT }, + }; + m_List.SetHeaders(headers); + m_List.SetExtendedStyle(m_List.GetExtendedStyle() | LVS_EX_FULLROWSELECT); + + GenerateList(); + SetWindowText((_T("Missing External Samples - ") + m_modDoc.GetPathNameMpt().GetFullFileName().AsNative()).c_str()); + + return TRUE; +} + + +void MissingExternalSamplesDlg::GenerateList() +{ + m_List.SetRedraw(FALSE); + m_List.DeleteAllItems(); + CString s; + for(SAMPLEINDEX smp = 1; smp <= m_sndFile.GetNumSamples(); smp++) + { + if(m_sndFile.IsExternalSampleMissing(smp)) + { + s.Format(_T("%02u: "), smp); + s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.GetSampleName(smp)); + int insertAt = m_List.InsertItem(m_List.GetItemCount(), s); + if(insertAt == -1) + continue; + m_List.SetItemText(insertAt, 1, m_sndFile.GetSamplePath(smp).AsNative().c_str()); + m_List.SetItemData(insertAt, smp); + } + } + m_List.SetRedraw(TRUE); + + // Yay, we managed to find all samples! + if(!m_List.GetItemCount()) + OnOK(); +} + + +void MissingExternalSamplesDlg::OnSetPath(NMHDR *, LRESULT *) +{ + const int item = m_List.GetSelectionMark(); + if(item == -1) return; + const SAMPLEINDEX smp = static_cast<SAMPLEINDEX>(m_List.GetItemData(item)); + + const mpt::PathString path = m_modDoc.GetSoundFile().GetSamplePath(smp); + FileDialog dlg = OpenFileDialog() + .ExtensionFilter("All Samples|*.wav;*.flac|All files(*.*)|*.*||"); // Only show samples that we actually can save as well. + if(TrackerSettings::Instance().previewInFileDialogs) + dlg.EnableAudioPreview(); + if(path.empty()) + dlg.WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir()); + else + dlg.DefaultFilename(path); + if(!dlg.Show()) return; + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + + SetSample(smp, dlg.GetFirstFile()); + m_modDoc.UpdateAllViews(nullptr, SampleHint(smp).Info().Names().Data()); + GenerateList(); +} + + +void MissingExternalSamplesDlg::OnScanFolder() +{ + if(m_isScanning) + { + m_isScanning = false; + return; + } + + BrowseForFolder dlg(TrackerSettings::Instance().PathSamples.GetWorkingDir(), _T("Select a folder to search for missing samples...")); + if(dlg.Show()) + { + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetDirectory()); + + FolderScanner scan(dlg.GetDirectory(), FolderScanner::kOnlyFiles | FolderScanner::kFindInSubDirectories); + mpt::PathString fileName; + + m_isScanning = true; + SetDlgItemText(IDC_BUTTON1, _T("&Cancel")); + GetDlgItem(IDOK)->EnableWindow(FALSE); + BeginWaitCursor(); + + DWORD64 lastTick = Util::GetTickCount64(); + int foundFiles = 0; + + bool anyMissing = true; + while(scan.Next(fileName) && m_isScanning && anyMissing) + { + anyMissing = false; + for(SAMPLEINDEX smp = 1; smp <= m_sndFile.GetNumSamples(); smp++) + { + if(m_sndFile.IsExternalSampleMissing(smp)) + { + if(!mpt::PathString::CompareNoCase(m_sndFile.GetSamplePath(smp).GetFullFileName(), fileName.GetFullFileName())) + { + if(SetSample(smp, fileName)) + { + foundFiles++; + } + } else + { + anyMissing = true; + } + } + } + + const DWORD64 tick = Util::GetTickCount64(); + if(tick < lastTick || tick > lastTick + 100) + { + lastTick = tick; + SetDlgItemText(IDC_STATIC1, fileName.AsNative().c_str()); + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + } + } + EndWaitCursor(); + GetDlgItem(IDOK)->EnableWindow(TRUE); + SetDlgItemText(IDC_BUTTON1, _T("&Scan Folder...")); + + m_modDoc.UpdateAllViews(nullptr, SampleHint().Info().Data().Names()); + + if(foundFiles) + { + SetDlgItemText(IDC_STATIC1, MPT_CFORMAT("{} sample paths were relocated.")(foundFiles)); + } else + { + SetDlgItemText(IDC_STATIC1, _T("No matching sample names found.")); + } + m_isScanning = false; + GenerateList(); + } + +} + + +bool MissingExternalSamplesDlg::SetSample(SAMPLEINDEX smp, const mpt::PathString &fileName) +{ + m_modDoc.GetSampleUndo().PrepareUndo(smp, sundo_replace, "Replace"); + const mpt::PathString oldPath = m_sndFile.GetSamplePath(smp); + if(!m_sndFile.LoadExternalSample(smp, fileName)) + { + Reporting::Information(_T("Unable to load sample:\n") + fileName.AsNative()); + m_modDoc.GetSampleUndo().RemoveLastUndoStep(smp); + return false; + } else + { + // Maybe we just put the file into its regular place, in which case the module has not really been modified. + if(oldPath != fileName) + { + m_modDoc.SetModified(); + } + return true; + } +} + + +BEGIN_MESSAGE_MAP(ModifiedExternalSamplesDlg, ResizableDialog) + //{{AFX_MSG_MAP(ExternalSamplesDlg) + ON_COMMAND(IDC_SAVE, &ModifiedExternalSamplesDlg::OnSaveSelected) + ON_COMMAND(IDC_CHECK1, &ModifiedExternalSamplesDlg::OnCheckAll) + ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST1, &ModifiedExternalSamplesDlg::OnSelectionChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void ModifiedExternalSamplesDlg::DoDataExchange(CDataExchange *pDX) +{ + ResizableDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST1, m_List); +} + + +ModifiedExternalSamplesDlg::ModifiedExternalSamplesDlg(CModDoc &modDoc, CWnd *parent) + : ResizableDialog(IDD_MODIFIEDSAMPLES, parent) + , m_modDoc(modDoc) + , m_sndFile(modDoc.GetSoundFile()) +{ +} + + +BOOL ModifiedExternalSamplesDlg::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + + // Initialize table + const CListCtrlEx::Header headers[] = + { + {_T("Sample"), 120, LVCFMT_LEFT}, + {_T("Status"), 54, LVCFMT_LEFT}, + {_T("External Filename"), 262, LVCFMT_LEFT}, + }; + m_List.SetHeaders(headers); + m_List.SetExtendedStyle(m_List.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_CHECKBOXES); + + GenerateList(); + SetWindowText((_T("Modified External Samples - ") + m_modDoc.GetPathNameMpt().GetFullFileName().AsNative()).c_str()); + + return TRUE; +} + + +void ModifiedExternalSamplesDlg::GenerateList() +{ + m_List.SetRedraw(FALSE); + m_List.DeleteAllItems(); + CString s; + mpt::winstring status; + for(SAMPLEINDEX smp = 1; smp <= m_sndFile.GetNumSamples(); smp++) + { + if(!m_sndFile.GetSample(smp).uFlags[SMP_KEEPONDISK]) + continue; + + if(m_sndFile.GetSample(smp).uFlags[SMP_MODIFIED]) + status = _T("modified"); + else if(!m_sndFile.GetSamplePath(smp).IsFile()) + status = _T("missing"); + else + continue; + + s.Format(_T("%02u: "), smp); + s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.GetSampleName(smp)); + int insertAt = m_List.InsertItem(m_List.GetItemCount(), s); + if(insertAt == -1) + continue; + m_List.SetItemText(insertAt, 1, status.c_str()); + m_List.SetItemText(insertAt, 2, m_sndFile.GetSamplePath(smp).AsNative().c_str()); + m_List.SetCheck(insertAt, TRUE); + m_List.SetItemData(insertAt, smp); + } + m_List.SetRedraw(TRUE); + + CheckDlgButton(IDC_CHECK1, BST_CHECKED); + OnSelectionChanged(nullptr, nullptr); + + // Nothing modified? + if(!m_List.GetItemCount()) + OnOK(); +} + + +void ModifiedExternalSamplesDlg::OnCheckAll() +{ + const BOOL check = IsDlgButtonChecked(IDC_CHECK1) ? TRUE : FALSE; + const int count = m_List.GetItemCount(); + for(int i = 0; i < count; i++) + { + m_List.SetCheck(i, check); + } +} + + +void ModifiedExternalSamplesDlg::OnSelectionChanged(NMHDR *, LRESULT *) +{ + int numChecked = 0; + const int count = m_List.GetItemCount(); + for(int i = 0; i < count; i++) + { + if(m_List.GetCheck(i)) + numChecked++; + } + const TCHAR *embedText, *saveText; + if(numChecked == count) + { + embedText = _T("&Embed All"); + saveText = _T("&Save All"); + } else if(!numChecked) + { + embedText = _T("&Embed None"); + saveText = _T("&Save None"); + } else + { + embedText = _T("&Embed Selected"); + saveText = _T("&Save Selected"); + } + + GetDlgItem(IDOK)->SetWindowText(embedText); + GetDlgItem(IDC_SAVE)->SetWindowText(saveText); +} + + +void ModifiedExternalSamplesDlg::Execute(bool doSave) +{ + ScopedLogCapturer log(m_modDoc, _T("Modified Samples"), this); + + bool ok = true; + const int count = m_List.GetItemCount(); + for(int i = 0; i < count; i++) + { + if(!m_List.GetCheck(i)) + continue; + + SAMPLEINDEX smp = static_cast<SAMPLEINDEX>(m_List.GetItemData(i)); + if(doSave) + { + ok &= m_modDoc.SaveSample(smp); + } else + { + m_sndFile.GetSample(smp).uFlags.reset(SMP_KEEPONDISK); + m_modDoc.SetModified(); + } + } + + if(ok) + ResizableDialog::OnOK(); + else + ResizableDialog::OnCancel(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.h b/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.h new file mode 100644 index 00000000..2bb3cd67 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ExternalSamples.h @@ -0,0 +1,73 @@ +/* + * ExternalSamples.h + * ----------------- + * Purpose: Dialogs for locating missing external samples and handling modified samples + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "ResizableDialog.h" +#include "CListCtrl.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +class CSoundFile; + +class MissingExternalSamplesDlg : public ResizableDialog +{ +protected: + CModDoc &m_modDoc; + CSoundFile &m_sndFile; + CListCtrlEx m_List; + bool m_isScanning = false; + +public: + MissingExternalSamplesDlg(CModDoc &modDoc, CWnd *parent); + +protected: + void GenerateList(); + bool SetSample(SAMPLEINDEX smp, const mpt::PathString &fileName); + + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + + afx_msg void OnSetPath(NMHDR *, LRESULT *); + afx_msg void OnScanFolder(); + + DECLARE_MESSAGE_MAP() +}; + + +class ModifiedExternalSamplesDlg : public ResizableDialog +{ +protected: + CModDoc &m_modDoc; + CSoundFile &m_sndFile; + CListCtrlEx m_List; + +public: + ModifiedExternalSamplesDlg(CModDoc &modDoc, CWnd *parent); + +protected: + void GenerateList(); + void Execute(bool doSave); + + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override { Execute(false); } + + afx_msg void OnSaveSelected() { Execute(true); } + afx_msg void OnCheckAll(); + afx_msg void OnSelectionChanged(NMHDR *pNMHDR, LRESULT *pResult); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.cpp new file mode 100644 index 00000000..4adaa2ef --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.cpp @@ -0,0 +1,229 @@ +/* + * FileDialog.cpp + * -------------- + * Purpose: File and folder selection dialogs implementation. + * 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 "FileDialog.h" +#include "Mainfrm.h" +#include "InputHandler.h" + + +OPENMPT_NAMESPACE_BEGIN + +class CFileDialogEx : public CFileDialog +{ +public: + CFileDialogEx(bool bOpenFileDialog, + LPCTSTR lpszDefExt, + LPCTSTR lpszFileName, + DWORD dwFlags, + LPCTSTR lpszFilter, + CWnd *pParentWnd, + DWORD dwSize, + BOOL bVistaStyle, + bool preview) + : CFileDialog(bOpenFileDialog ? TRUE : FALSE, lpszDefExt, lpszFileName, dwFlags, lpszFilter, pParentWnd, dwSize, bVistaStyle) + , m_fileNameBuf(65536) + , doPreview(preview) + , played(false) + { + // MFC's filename buffer is way too small for multi-selections of a large number of files. + _tcsncpy(m_fileNameBuf.data(), lpszFileName, m_fileNameBuf.size()); + m_fileNameBuf.back() = '\0'; + m_ofn.lpstrFile = m_fileNameBuf.data(); + m_ofn.nMaxFile = mpt::saturate_cast<DWORD>(m_fileNameBuf.size()); + } + + ~CFileDialogEx() + { + if(played) + { + CMainFrame::GetMainFrame()->StopPreview(); + } + } + +#if NTDDI_VERSION >= NTDDI_VISTA + // MFC's AddPlace() is declared as throw() but can in fact throw if any of the COM calls fail, e.g. because the place does not exist. + // Avoid this by re-implementing our own version which doesn't throw. + void AddPlace(const mpt::PathString &path) + { + if(m_bVistaStyle && path.IsDirectory()) + { + CComPtr<IShellItem> shellItem; + HRESULT hr = SHCreateItemFromParsingName(path.ToWide().c_str(), nullptr, IID_IShellItem, reinterpret_cast<void **>(&shellItem)); + if(SUCCEEDED(hr)) + { + static_cast<IFileDialog*>(m_pIFileDialog)->AddPlace(shellItem, FDAP_TOP); + } + } + } +#endif + +protected: + std::vector<TCHAR> m_fileNameBuf; + CString oldName; + bool doPreview, played; + + void OnFileNameChange() override + { + if(doPreview) + { + CString name = GetPathName(); + if(!name.IsEmpty() && name != oldName) + { + oldName = name; + if(CMainFrame::GetMainFrame()->PlaySoundFile(mpt::PathString::FromCString(name), NOTE_MIDDLEC)) + { + played = true; + } + } + } + CFileDialog::OnFileNameChange(); + } +}; + + +// Display the file dialog. +bool FileDialog::Show(CWnd *parent) +{ + m_filenames.clear(); + + // First, set up the dialog... + CFileDialogEx dlg(m_load, + m_defaultExtension.empty() ? nullptr : m_defaultExtension.c_str(), + m_defaultFilename.c_str(), + OFN_EXPLORER | OFN_NOCHANGEDIR | OFN_HIDEREADONLY | OFN_ENABLESIZING | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | (m_multiSelect ? OFN_ALLOWMULTISELECT : 0) | (m_load ? 0 : (OFN_OVERWRITEPROMPT | OFN_NOREADONLYRETURN)), + m_extFilter.c_str(), + parent != nullptr ? parent : CMainFrame::GetMainFrame(), + 0, + (mpt::OS::Windows::IsWine() || mpt::OS::Windows::Version::Current().IsBefore(mpt::OS::Windows::Version::WinVista)) ? FALSE : TRUE, + m_preview && TrackerSettings::Instance().previewInFileDialogs); + OPENFILENAME &ofn = dlg.GetOFN(); + ofn.nFilterIndex = m_filterIndex != nullptr ? *m_filterIndex : 0; + if(!m_workingDirectory.empty()) + { + ofn.lpstrInitialDir = m_workingDirectory.c_str(); + } +#if NTDDI_VERSION >= NTDDI_VISTA + const auto places = + { + &TrackerSettings::Instance().PathPluginPresets, + &TrackerSettings::Instance().PathPlugins, + &TrackerSettings::Instance().PathSamples, + &TrackerSettings::Instance().PathInstruments, + &TrackerSettings::Instance().PathSongs, + }; + for(const auto place : places) + { + dlg.AddPlace(place->GetDefaultDir()); + } + for(const auto &place : m_places) + { + dlg.AddPlace(place); + } +#endif + + // Do it! + BypassInputHandler bih; + if(dlg.DoModal() != IDOK) + { + return false; + } + + // Retrieve variables + if(m_filterIndex != nullptr) + *m_filterIndex = ofn.nFilterIndex; + + if(m_multiSelect) + { +#if NTDDI_VERSION >= NTDDI_VISTA + // Multiple files might have been selected + if(CComPtr<IShellItemArray> shellItems = dlg.GetResults(); shellItems != nullptr) + { + // Using the old-style GetNextPathName doesn't work properly when the user performs a search and selects files from different folders. + // Hence we use that only as a fallback. + DWORD numItems = 0; + shellItems->GetCount(&numItems); + for(DWORD i = 0; i < numItems; i++) + { + CComPtr<IShellItem> shellItem; + shellItems->GetItemAt(i, &shellItem); + + LPWSTR filePath = nullptr; + if(HRESULT hr = shellItem->GetDisplayName(SIGDN_FILESYSPATH, &filePath); SUCCEEDED(hr)) + { + m_filenames.push_back(mpt::PathString::FromWide(filePath)); + ::CoTaskMemFree(filePath); + } + } + } else +#endif + { + POSITION pos = dlg.GetStartPosition(); + while(pos != nullptr) + { + m_filenames.push_back(mpt::PathString::FromCString(dlg.GetNextPathName(pos))); + } + } + } else + { + // Only one file + m_filenames.push_back(mpt::PathString::FromCString(dlg.GetPathName())); + } + + if(m_filenames.empty()) + { + return false; + } + + m_workingDirectory = m_filenames.front().AsNative().substr(0, ofn.nFileOffset); + m_extension = m_filenames.front().AsNative().substr(ofn.nFileExtension); + + return true; +} + + +// Helper callback to set start path. +int CALLBACK BrowseForFolder::BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM /*lParam*/, LPARAM lpData) +{ + if(uMsg == BFFM_INITIALIZED && lpData != NULL) + { + const BrowseForFolder *that = reinterpret_cast<BrowseForFolder *>(lpData); + SendMessage(hwnd, BFFM_SETSELECTION, TRUE, reinterpret_cast<LPARAM>(that->m_workingDirectory.AsNative().c_str())); + } + return 0; +} + + +// Display the folder dialog. +bool BrowseForFolder::Show(CWnd *parent) +{ + // Note: MFC's CFolderPickerDialog won't work on pre-Vista systems, as it tries to use OPENFILENAME. + BypassInputHandler bih; + TCHAR path[MAX_PATH]; + BROWSEINFO bi; + MemsetZero(bi); + bi.hwndOwner = (parent != nullptr ? parent : theApp.m_pMainWnd)->m_hWnd; + if(!m_caption.IsEmpty()) bi.lpszTitle = m_caption; + bi.pszDisplayName = path; + bi.ulFlags = BIF_RETURNONLYFSDIRS | BIF_USENEWUI; + bi.lpfn = BrowseCallbackProc; + bi.lParam = reinterpret_cast<LPARAM>(this); + LPITEMIDLIST pid = SHBrowseForFolder(&bi); + bool success = pid != nullptr && SHGetPathFromIDList(pid, path); + CoTaskMemFree(pid); + if(success) + { + m_workingDirectory = mpt::PathString::FromNative(path); + } + return success; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.h new file mode 100644 index 00000000..348020b2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/FileDialog.h @@ -0,0 +1,120 @@ +/* + * FileDialog.h + * ------------ + * Purpose: File and folder selection dialogs implementation. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <string> +#include <vector> + +OPENMPT_NAMESPACE_BEGIN + +// Generic open / save file dialog. Cannot be instanced by the user, use OpenFileDialog / SaveFileDialog instead. +class FileDialog +{ +public: + using PathList = std::vector<mpt::PathString>; + +protected: + mpt::RawPathString m_defaultExtension; + mpt::RawPathString m_defaultFilename; + mpt::RawPathString m_extFilter; + mpt::RawPathString m_lastPreviewFile; + mpt::RawPathString m_workingDirectory; + mpt::RawPathString m_extension; + PathList m_filenames; + PathList m_places; + int *m_filterIndex = nullptr; + bool m_load; + bool m_multiSelect = false; + bool m_preview = false; + +protected: + FileDialog(bool load) : m_load(load) { } + +public: + // Default extension to use if none is specified. + FileDialog &DefaultExtension(const mpt::PathString &ext) { m_defaultExtension = ext.AsNative(); return *this; } + FileDialog &DefaultExtension(const AnyStringLocale &ext) { m_defaultExtension = mpt::ToWin(ext); return *this; } + // Default suggested filename. + FileDialog &DefaultFilename(const mpt::PathString &name) { m_defaultFilename = name.AsNative(); return *this; } + FileDialog &DefaultFilename(const AnyStringLocale &name) { m_defaultFilename = mpt::ToWin(name); return *this; } + // List of possible extensions. Format: "description|extensions|...|description|extensions||" + FileDialog &ExtensionFilter(const mpt::PathString &filter) { m_extFilter = filter.AsNative(); return *this; } + FileDialog &ExtensionFilter(const AnyStringLocale &filter) { m_extFilter = mpt::ToWin(filter); return *this; } + // Default directory of the dialog. + FileDialog &WorkingDirectory(const mpt::PathString &dir) { m_workingDirectory = dir.AsNative(); return *this; } + // Pointer to a variable holding the index of the last extension filter to use. Holds the selected filter after the dialog has been closed. + FileDialog &FilterIndex(int *index) { m_filterIndex = index; return *this; } + // Enable preview of instrument files (if globally enabled). + FileDialog &EnableAudioPreview() { m_preview = true; return *this; } + // Add a directory to the application-specific quick-access directories in the file dialog + FileDialog &AddPlace(mpt::PathString path) { m_places.push_back(std::move(path)); return *this; } + + // Show the file selection dialog. + bool Show(CWnd *parent = nullptr); + + // Get some selected file. Mostly useful when only one selected file is possible anyway. + mpt::PathString GetFirstFile() const + { + if(!m_filenames.empty()) + return m_filenames.front(); + else + return {}; + } + // Gets a reference to all selected filenames. + const PathList &GetFilenames() const { return m_filenames; } + // Gets directory in which the selected files are placed. + mpt::PathString GetWorkingDirectory() const { return mpt::PathString::FromNative(m_workingDirectory); } + // Gets the extension of the first selected file, without dot. + mpt::PathString GetExtension() const { return mpt::PathString::FromNative(m_extension); } +}; + + +// Dialog for opening files +class OpenFileDialog : public FileDialog +{ +public: + OpenFileDialog() : FileDialog(true) { } + + // Enable selection of multiple files + OpenFileDialog &AllowMultiSelect() { m_multiSelect = true; return *this; } +}; + + +// Dialog for saving files +class SaveFileDialog : public FileDialog +{ +public: + SaveFileDialog() : FileDialog(false) { } +}; + + +// Folder browser. +class BrowseForFolder +{ +protected: + mpt::PathString m_workingDirectory; + CString m_caption; + +public: + BrowseForFolder(const mpt::PathString &dir, const CString &caption) : m_workingDirectory(dir), m_caption(caption) { } + + // Show the folder selection dialog. + bool Show(CWnd *parent = nullptr); + + // Gets selected directory. + mpt::PathString GetDirectory() const { return m_workingDirectory; } + +protected: + static int CALLBACK BrowseCallbackProc(HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.cpp new file mode 100644 index 00000000..3b0d320b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.cpp @@ -0,0 +1,90 @@ +/* + * FolderScanner.cpp + * ----------------- + * Purpose: Class for finding files in folders. + * 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 "FolderScanner.h" +#include <tchar.h> + +OPENMPT_NAMESPACE_BEGIN + +FolderScanner::FolderScanner(const mpt::PathString &path, FlagSet<ScanType> type, mpt::PathString filter) + : m_paths(1, path) + , m_filter(std::move(filter)) + , m_hFind(INVALID_HANDLE_VALUE) + , m_type(type) +{ + MemsetZero(m_wfd); +} + + +FolderScanner::~FolderScanner() +{ + FindClose(m_hFind); +} + + +bool FolderScanner::Next(mpt::PathString &file) +{ + bool found = false; + do + { + if(m_hFind == INVALID_HANDLE_VALUE) + { + if(m_paths.empty()) + { + return false; + } + + m_currentPath = m_paths.back(); + m_paths.pop_back(); + m_currentPath.EnsureTrailingSlash(); + m_hFind = FindFirstFile((m_currentPath + m_filter).AsNative().c_str(), &m_wfd); + } + + BOOL nextFile = FALSE; + if(m_hFind != INVALID_HANDLE_VALUE) + { + do + { + file = m_currentPath + mpt::PathString::FromNative(m_wfd.cFileName); + if(m_wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + if(_tcscmp(m_wfd.cFileName, _T("..")) && _tcscmp(m_wfd.cFileName, _T("."))) + { + if(m_type[kFindInSubDirectories]) + { + // Add sub directory + m_paths.push_back(file); + } + if(m_type[kOnlyDirectories]) + { + found = true; + } + } + } else if(m_type[kOnlyFiles]) + { + found = true; + } + } while((nextFile = FindNextFile(m_hFind, &m_wfd)) != FALSE && !found); + } + if(nextFile == FALSE) + { + // Done with this directory, advance to next + if(m_hFind != INVALID_HANDLE_VALUE) + { + FindClose(m_hFind); + } + m_hFind = INVALID_HANDLE_VALUE; + } + } while(!found); + return true; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.h b/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.h new file mode 100644 index 00000000..c55fcbfa --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/FolderScanner.h @@ -0,0 +1,48 @@ +/* + * FolderScanner.h + * --------------- + * Purpose: Class for finding files in folders. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/mptPathString.h" + +OPENMPT_NAMESPACE_BEGIN + +class FolderScanner +{ +public: + enum ScanType + { + kOnlyFiles = 0x01, + kOnlyDirectories = 0x02, + kFilesAndDirectories = kOnlyFiles | kOnlyDirectories, + kFindInSubDirectories = 0x04, + }; + +protected: + std::vector<mpt::PathString> m_paths; + mpt::PathString m_currentPath; + mpt::PathString m_filter; + HANDLE m_hFind; + WIN32_FIND_DATA m_wfd; + FlagSet<ScanType> m_type; + +public: + FolderScanner(const mpt::PathString &path, FlagSet<ScanType> type, mpt::PathString filter = MPT_PATHSTRING("*.*")); + ~FolderScanner(); + + // Return one file or directory at a time in parameter file. Returns true if a file was found (file parameter is valid), false if no more files can be found (file parameter is not touched). + bool Next(mpt::PathString &file); +}; + +MPT_DECLARE_ENUM(FolderScanner::ScanType) + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.cpp new file mode 100644 index 00000000..b1cb79a0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.cpp @@ -0,0 +1,227 @@ +/* + * GeneralConfigDlg.cpp + * -------------------- + * Purpose: Implementation of the general settings dialog. + * 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 "Mainfrm.h" +#include "GeneralConfigDlg.h" +#include "Settings.h" +#include "FileDialog.h" +#include "../common/mptStringBuffer.h" +#include "FolderScanner.h" + + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(COptionsGeneral, CPropertyPage) + ON_LBN_SELCHANGE(IDC_LIST1, &COptionsGeneral::OnOptionSelChanged) + ON_CLBN_CHKCHANGE(IDC_LIST1, &COptionsGeneral::OnSettingsChanged) + ON_COMMAND(IDC_RADIO1, &COptionsGeneral::OnSettingsChanged) + ON_COMMAND(IDC_RADIO2, &COptionsGeneral::OnSettingsChanged) + ON_COMMAND(IDC_RADIO3, &COptionsGeneral::OnSettingsChanged) + ON_EN_CHANGE(IDC_EDIT1, &COptionsGeneral::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO1, &COptionsGeneral::OnDefaultTypeChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &COptionsGeneral::OnTemplateChanged) + ON_CBN_EDITCHANGE(IDC_COMBO2, &COptionsGeneral::OnTemplateChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsGeneral::OnBrowseTemplate) +END_MESSAGE_MAP() + + +static constexpr struct GeneralOptionsDescriptions +{ + uint32 flag; + const char *name, *description; +} generalOptionsList[] = +{ + {PATTERN_PLAYNEWNOTE, "Play new notes while recording", "When this option is enabled, notes entered in the pattern editor will always be played (If not checked, notes won't be played in record mode)."}, + {PATTERN_PLAYEDITROW, "Play whole row while recording", "When this option is enabled, all notes on the current row are played when entering notes in the pattern editor."}, + {PATTERN_PLAYNAVIGATEROW, "Play whole row when navigating", "When this option is enabled, all notes on the current row are played when navigating vertically in the pattern editor."}, + {PATTERN_PLAYTRANSPOSE, "Play notes when transposing", "When transposing a single note, the new note is previewed."}, + {PATTERN_CENTERROW, "Always center active row", "Turn on this option to have the active row always centered in the pattern editor."}, + {PATTERN_SMOOTHSCROLL, "Smooth pattern scrolling", "Scroll patterns tick by tick rather than row by row at the expense of an increased CPU load."}, + {PATTERN_HEXDISPLAY, "Display rows in hex", "With this option enabled, row numbers and sequence numbers will be displayed in hexadecimal."}, + {PATTERN_WRAP, "Cursor wrap in pattern editor", "When this option is active, going past the end of a pattern row or channel will move the cursor to the beginning. When \"Continuous scroll\"-option is enabled, row wrap is disabled."}, + {PATTERN_DRAGNDROPEDIT, "Drag and Drop Editing", "Enable moving a selection in the pattern editor (copying if pressing shift while dragging)"}, + {PATTERN_FLATBUTTONS, "Flat Buttons", "Use flat buttons in toolbars"}, + {PATTERN_SINGLEEXPAND, "Single click to expand tree", "Single-clicking in the left tree view will expand a node."}, + {PATTERN_MUTECHNMODE, "Ignored muted channels", "Notes will not be played on muted channels (unmuting will only start on a new note)."}, + {PATTERN_NOEXTRALOUD, "No loud sample preview", "Disable loud playback of samples in the sample/instrument editor. Sample volume depends on the sample volume slider on the general tab when activated (if disabled, samples are previewed at 0 dB)."}, + {PATTERN_SHOWPREVIOUS, "Show Prev/Next patterns", "Displays grayed-out version of the previous/next patterns in the pattern editor. Does not work if \"always center active row\" is disabled."}, + {PATTERN_CONTSCROLL, "Continuous scroll", "Jumps to the next pattern when moving past the end of a pattern"}, + {PATTERN_KBDNOTEOFF, "Record note off", "Record note off when a key is released on the PC keyboard."}, + {PATTERN_FOLLOWSONGOFF, "Follow Song off by default", "Ensure Follow Song is off when opening or starting a new song."}, + {PATTERN_NOFOLLOWONCLICK,"Disable Follow Song on click", "Follow Song is deactivated when clicking into the pattern."}, + {PATTERN_OLDCTXMENUSTYLE, "Old style pattern context menu", "Check this option to hide unavailable items in the pattern editor context menu. Uncheck to grey-out unavailable items instead."}, + {PATTERN_SYNCMUTE, "Maintain sample sync on mute", "Samples continue to be processed when channels are muted (like in IT2 and FT2)"}, + {PATTERN_SYNCSAMPLEPOS, "Maintain sample sync on seek", "Sample that are still active from previous patterns are continued to be played after seeking.\nNote: Some pattern commands may prevent samples from being synced. This feature may slow down seeking."}, + {PATTERN_AUTODELAY, "Automatic delay commands", "Automatically insert appropriate note-delay commands when recording notes during live playback."}, + {PATTERN_NOTEFADE, "Note fade on key up", "Enable to fade / stop notes on key up in pattern tab."}, + {PATTERN_OVERFLOWPASTE, "Overflow paste mode", "Wrap pasted pattern data into next pattern. This is useful for creating echo channels."}, + {PATTERN_RESETCHANNELS, "Reset channels on loop", "If enabled, channels will be reset to their initial state when song looping is enabled.\nNote: This does not affect manual song loops (i.e. triggered by pattern commands) and is not recommended to be enabled."}, + {PATTERN_LIVEUPDATETREE,"Update sample status in tree", "If enabled, active samples and instruments will be indicated by a different icon in the treeview."}, + {PATTERN_NOCLOSEDIALOG, "Disable modern close dialog", "When closing the main window, a confirmation window is shown for every unsaved document instead of one single window with a list of unsaved documents."}, + {PATTERN_DBLCLICKSELECT, "Double-click to select channel", "Instead of showing the note properties, double-clicking a pattern cell selects the whole channel."}, + {PATTERN_SHOWDEFAULTVOLUME, "Show default volume commands", "If there is no volume command next to a note + instrument combination, the sample's default volume is shown."}, +}; + + +void COptionsGeneral::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CModTypeDlg) + DDX_Control(pDX, IDC_LIST1, m_CheckList); + DDX_Control(pDX, IDC_EDIT1, m_defaultArtist); + DDX_Control(pDX, IDC_COMBO2, m_defaultTemplate); + DDX_Control(pDX, IDC_COMBO1, m_defaultFormat); + //}}AFX_DATA_MAP +} + + +BOOL COptionsGeneral::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + m_defaultArtist.SetWindowText(mpt::ToCString(TrackerSettings::Instance().defaultArtist)); + + const struct + { + MODTYPE type; + const TCHAR *str; + } formats[] = + { + { MOD_TYPE_MOD, _T("MOD") }, + { MOD_TYPE_XM, _T("XM") }, + { MOD_TYPE_S3M, _T("S3M") }, + { MOD_TYPE_IT, _T("IT") }, + { MOD_TYPE_MPT, _T("MPTM") }, + }; + m_defaultFormat.SetCurSel(0); + for(const auto &fmt : formats) + { + auto idx = m_defaultFormat.AddString(fmt.str); + m_defaultFormat.SetItemData(idx, fmt.type); + if(fmt.type == TrackerSettings::Instance().defaultModType) + { + m_defaultFormat.SetCurSel(idx); + } + } + + const mpt::PathString basePath = theApp.GetConfigPath() + P_("TemplateModules\\"); + FolderScanner scanner(basePath, FolderScanner::kOnlyFiles | FolderScanner::kFindInSubDirectories); + mpt::PathString file; + while(scanner.Next(file)) + { + mpt::RawPathString fileW = file.AsNative(); + fileW = fileW.substr(basePath.Length()); + ::SendMessage(m_defaultTemplate.m_hWnd, CB_ADDSTRING, 0, (LPARAM)fileW.c_str()); + } + file = TrackerSettings::Instance().defaultTemplateFile; + if(file.GetPath() == basePath) + { + file = file.GetFullFileName(); + } + m_defaultTemplate.SetWindowText(file.AsNative().c_str()); + + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO1 + TrackerSettings::Instance().defaultNewFileAction); + + for(const auto &opt : generalOptionsList) + { + auto idx = m_CheckList.AddString(mpt::ToCString(mpt::Charset::ASCII, opt.name)); + const int check = (TrackerSettings::Instance().m_dwPatternSetup & opt.flag) != 0 ? BST_CHECKED : BST_UNCHECKED; + m_CheckList.SetCheck(idx, check); + } + m_CheckList.SetCurSel(0); + OnOptionSelChanged(); + + return TRUE; +} + + +void COptionsGeneral::OnOK() +{ + TrackerSettings::Instance().defaultArtist = GetWindowTextUnicode(m_defaultArtist); + TrackerSettings::Instance().defaultModType = static_cast<MODTYPE>(m_defaultFormat.GetItemData(m_defaultFormat.GetCurSel())); + TrackerSettings::Instance().defaultTemplateFile = mpt::PathString::FromCString(GetWindowTextString(m_defaultTemplate)); + + NewFileAction action = nfDefaultFormat; + int newActionRadio = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3); + if(newActionRadio == IDC_RADIO2) action = nfSameAsCurrent; + if(newActionRadio == IDC_RADIO3) action = nfDefaultTemplate; + if(action == nfDefaultTemplate && TrackerSettings::Instance().defaultTemplateFile.Get().empty()) + { + action = nfDefaultFormat; + ::MessageBeep(MB_ICONWARNING); + } + TrackerSettings::Instance().defaultNewFileAction = action; + + for(int i = 0; i < mpt::saturate_cast<int>(std::size(generalOptionsList)); i++) + { + const bool check = (m_CheckList.GetCheck(i) != BST_UNCHECKED); + + if(check) TrackerSettings::Instance().m_dwPatternSetup |= generalOptionsList[i].flag; + else TrackerSettings::Instance().m_dwPatternSetup &= ~generalOptionsList[i].flag; + } + + CMainFrame::GetMainFrame()->SetupMiscOptions(); + + CPropertyPage::OnOK(); +} + + +BOOL COptionsGeneral::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_GENERAL; + return CPropertyPage::OnSetActive(); +} + + +void COptionsGeneral::OnOptionSelChanged() +{ + const char *desc = ""; + const int sel = m_CheckList.GetCurSel(); + if ((sel >= 0) && (sel < mpt::saturate_cast<int>(std::size(generalOptionsList)))) + { + desc = generalOptionsList[sel].description; + } + SetDlgItemText(IDC_TEXT1, mpt::ToCString(mpt::Charset::ASCII, desc)); +} + + +void COptionsGeneral::OnBrowseTemplate() +{ + mpt::PathString basePath = theApp.GetInstallPath() + P_("TemplateModules\\"); + mpt::PathString defaultFile = mpt::PathString::FromCString(GetWindowTextString(m_defaultTemplate)); + if(defaultFile.empty()) defaultFile = TrackerSettings::Instance().defaultTemplateFile; + + OpenFileDialog dlg; + if(defaultFile.empty()) + { + dlg.WorkingDirectory(basePath); + } else + { + if(defaultFile.AsNative().find_first_of(_T("/\\")) == mpt::RawPathString::npos) + { + // Relative path + defaultFile = basePath + defaultFile; + } + dlg.DefaultFilename(defaultFile); + } + if(dlg.Show(this)) + { + defaultFile = dlg.GetFirstFile(); + if(defaultFile.GetPath() == basePath) + { + defaultFile = defaultFile.GetFullFileName(); + } + m_defaultTemplate.SetWindowText(defaultFile.AsNative().c_str()); + OnTemplateChanged(); + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.h new file mode 100644 index 00000000..ee3253c4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/GeneralConfigDlg.h @@ -0,0 +1,42 @@ +/* + * GeneralConfigDlg.h + * ------------------ + * Purpose: Implementation of the general settings dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class COptionsGeneral: public CPropertyPage +{ +protected: + CEdit m_defaultArtist; + CComboBox m_defaultTemplate, m_defaultFormat; + CCheckListBox m_CheckList; + +public: + COptionsGeneral() : CPropertyPage(IDD_OPTIONS_GENERAL) {} + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + + afx_msg void OnOptionSelChanged(); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnBrowseTemplate(); + afx_msg void OnDefaultTypeChanged() { CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO1); OnSettingsChanged(); } + afx_msg void OnTemplateChanged() { CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO3); OnSettingsChanged(); } + + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Globals.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Globals.cpp new file mode 100644 index 00000000..d59225b5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Globals.cpp @@ -0,0 +1,843 @@ +/* + * globals.cpp + * ----------- + * Purpose: Implementation of various views of the tracker interface. + * 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 "Moddoc.h" +#include "Childfrm.h" +#include "Globals.h" +#include "Ctrl_gen.h" +#include "Ctrl_pat.h" +#include "Ctrl_smp.h" +#include "Ctrl_ins.h" +#include "Ctrl_com.h" +#include "ImageLists.h" +#include "../soundlib/mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////////// +// CModControlDlg + +BEGIN_MESSAGE_MAP(CModControlDlg, CDialog) + //{{AFX_MSG_MAP(CModControlDlg) + ON_WM_SIZE() +#if !defined(MPT_BUILD_RETRO) + ON_MESSAGE(WM_DPICHANGED, &CModControlDlg::OnDPIChanged) +#endif + ON_MESSAGE(WM_MOD_UNLOCKCONTROLS, &CModControlDlg::OnUnlockControls) + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTW, 0, 0xFFFF, &CModControlDlg::OnToolTipText) + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXTA, 0, 0xFFFF, &CModControlDlg::OnToolTipText) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +CModControlDlg::CModControlDlg(CModControlView &parent, CModDoc &document) : m_modDoc(document), m_sndFile(document.GetSoundFile()), m_parent(parent) +{ + m_bInitialized = FALSE; + m_hWndView = NULL; + m_nLockCount = 0; +} + + +CModControlDlg::~CModControlDlg() +{ + ASSERT(m_hWnd == NULL); +} + + +BOOL CModControlDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + m_nDPIx = Util::GetDPIx(m_hWnd); + m_nDPIy = Util::GetDPIy(m_hWnd); + EnableToolTips(TRUE); + return TRUE; +} + + +LRESULT CModControlDlg::OnDPIChanged(WPARAM wParam, LPARAM) +{ + m_nDPIx = LOWORD(wParam); + m_nDPIy = HIWORD(wParam); + return 0; +} + + +void CModControlDlg::OnSize(UINT nType, int cx, int cy) +{ + CDialog::OnSize(nType, cx, cy); + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) + { + RecalcLayout(); + } +} + + +LRESULT CModControlDlg::OnModCtrlMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case CTRLMSG_SETVIEWWND: + m_hWndView = (HWND)lParam; + break; + + case CTRLMSG_ACTIVATEPAGE: + OnActivatePage(lParam); + break; + + case CTRLMSG_DEACTIVATEPAGE: + OnDeactivatePage(); + break; + + case CTRLMSG_SETFOCUS: + GetParentFrame()->SetActiveView(&m_parent); + SetFocus(); + break; + } + return 0; +} + + +LRESULT CModControlDlg::SendViewMessage(UINT uMsg, LPARAM lParam) const +{ + if (m_hWndView) return ::SendMessage(m_hWndView, WM_MOD_VIEWMSG, uMsg, lParam); + return 0; +} + + +BOOL CModControlDlg::PostViewMessage(UINT uMsg, LPARAM lParam) const +{ + if (m_hWndView) return ::PostMessage(m_hWndView, WM_MOD_VIEWMSG, uMsg, lParam); + return FALSE; +} + + +INT_PTR CModControlDlg::OnToolHitTest(CPoint point, TOOLINFO* pTI) const +{ + INT_PTR nHit = CDialog::OnToolHitTest(point, pTI); + if ((nHit >= 0) && (pTI)) + { + if ((pTI->lpszText == LPSTR_TEXTCALLBACK) && (pTI->hwnd == m_hWnd)) + { + CFrameWnd *pMDIParent = GetParentFrame(); + if (pMDIParent) pTI->hwnd = pMDIParent->m_hWnd; + } + } + return nHit; +} + + +BOOL CModControlDlg::OnToolTipText(UINT nID, NMHDR* pNMHDR, LRESULT* pResult) +{ + CChildFrame *pChildFrm = (CChildFrame *)GetParentFrame(); + if (pChildFrm) return pChildFrm->OnToolTipText(nID, pNMHDR, pResult); + if (pResult) *pResult = 0; + return FALSE; +} + + +///////////////////////////////////////////////////////////////////////////// +// CModControlView + +BOOL CModTabCtrl::Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (!pMainFrm) return FALSE; + if (!CTabCtrl::Create(dwStyle, rect, pParentWnd, nID)) return FALSE; + SendMessage(WM_SETFONT, (WPARAM)pMainFrm->GetGUIFont()); + SetImageList(&pMainFrm->m_MiscIcons); + return TRUE; +} + + +BOOL CModTabCtrl::InsertItem(int nIndex, LPCTSTR pszText, LPARAM lParam, int iImage) +{ + TC_ITEM tci; + tci.mask = TCIF_TEXT | TCIF_PARAM | TCIF_IMAGE; + tci.pszText = const_cast<LPTSTR>(pszText); + tci.lParam = lParam; + tci.iImage = iImage; + return CTabCtrl::InsertItem(nIndex, &tci); +} + + +LPARAM CModTabCtrl::GetItemData(int nIndex) +{ + TC_ITEM tci; + tci.mask = TCIF_PARAM; + tci.lParam = 0; + if (!GetItem(nIndex, &tci)) return 0; + return tci.lParam; +} + + +///////////////////////////////////////////////////////////////////////////////// +// CModControlView + +IMPLEMENT_DYNCREATE(CModControlView, CView) + +BEGIN_MESSAGE_MAP(CModControlView, CView) + //{{AFX_MSG_MAP(CModControlView) + ON_WM_SIZE() + ON_WM_DESTROY() + ON_NOTIFY(TCN_SELCHANGE, IDC_TABCTRL1, &CModControlView::OnTabSelchange) + ON_MESSAGE(WM_MOD_ACTIVATEVIEW, &CModControlView::OnActivateModView) + ON_MESSAGE(WM_MOD_CTRLMSG, &CModControlView::OnModCtrlMsg) + ON_MESSAGE(WM_MOD_GETTOOLTIPTEXT, &CModControlView::OnGetToolTipText) + ON_COMMAND(ID_EDIT_CUT, &CModControlView::OnEditCut) + ON_COMMAND(ID_EDIT_COPY, &CModControlView::OnEditCopy) + ON_COMMAND(ID_EDIT_PASTE, &CModControlView::OnEditPaste) + ON_COMMAND(ID_EDIT_MIXPASTE, &CModControlView::OnEditMixPaste) + ON_COMMAND(ID_EDIT_MIXPASTE_ITSTYLE, &CModControlView::OnEditMixPasteITStyle) + ON_COMMAND(ID_EDIT_FIND, &CModControlView::OnEditFind) + ON_COMMAND(ID_EDIT_FINDNEXT, &CModControlView::OnEditFindNext) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +CModControlView::CModControlView() +{ + MemsetZero(m_Pages); + m_nActiveDlg = -1; + m_nInstrumentChanged = -1; + m_hWndView = NULL; + m_hWndMDI = NULL; +} + + +BOOL CModControlView::PreCreateWindow(CREATESTRUCT& cs) +{ + return CView::PreCreateWindow(cs); +} + + +void CModControlView::OnInitialUpdate() // called first time after construct +{ + CView::OnInitialUpdate(); + CRect rect; + + CChildFrame *pParentFrame = (CChildFrame *)GetParentFrame(); + if (pParentFrame) m_hWndView = pParentFrame->GetHwndView(); + GetClientRect(&rect); + m_TabCtrl.Create(WS_CHILD|WS_VISIBLE|TCS_FOCUSNEVER|TCS_FORCELABELLEFT, rect, this, IDC_TABCTRL1); + UpdateView(UpdateHint().ModType()); + SetActivePage(0); +} + + +void CModControlView::OnSize(UINT nType, int cx, int cy) +{ + CView::OnSize(nType, cx, cy); + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) + { + RecalcLayout(); + } +} + + +void CModControlView::RecalcLayout() +{ + CRect rcClient; + + if (m_TabCtrl.m_hWnd == NULL) return; + GetClientRect(&rcClient); + if ((m_nActiveDlg >= 0) && (m_nActiveDlg < MAX_PAGES) && (m_Pages[m_nActiveDlg])) + { + CWnd *pDlg = m_Pages[m_nActiveDlg]; + CRect rect = rcClient; + m_TabCtrl.AdjustRect(FALSE, &rect); + HDWP hdwp = BeginDeferWindowPos(2); + DeferWindowPos(hdwp, m_TabCtrl.m_hWnd, NULL, rcClient.left, rcClient.top, rcClient.Width(), rcClient.Height(), SWP_NOZORDER); + DeferWindowPos(hdwp, pDlg->m_hWnd, NULL, rect.left, rect.top, rect.Width(), rect.Height(), SWP_NOZORDER); + EndDeferWindowPos(hdwp); + } else + { + m_TabCtrl.MoveWindow(&rcClient); + } +} + + +void CModControlView::OnUpdate(CView *, LPARAM lHint, CObject *pHint) +{ + UpdateView(UpdateHint::FromLPARAM(lHint), pHint); +} + + +void CModControlView::ForceRefresh() +{ + SetActivePage(GetActivePage()); +} + + +BOOL CModControlView::SetActivePage(int nIndex, LPARAM lParam) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModControlDlg *pDlg = NULL; + + + if (nIndex == -1) nIndex = m_TabCtrl.GetCurSel(); + + const UINT nID = static_cast<UINT>(m_TabCtrl.GetItemData(nIndex)); + if(nID == 0) return FALSE; + + switch(nID) + { + //rewbs.graph + case IDD_CONTROL_GRAPH: + nIndex = 5; + break; + //end rewbs.graph + case IDD_CONTROL_COMMENTS: + nIndex = 4; + break; + case IDD_CONTROL_GLOBALS: + nIndex = 0; + break; + case IDD_CONTROL_PATTERNS: + nIndex = 1; + break; + case IDD_CONTROL_SAMPLES: + nIndex = 2; + break; + case IDD_CONTROL_INSTRUMENTS: + nIndex = 3; + break; + default: + return FALSE; + } + + if ((nIndex < 0) || (nIndex >= MAX_PAGES) || (!pMainFrm)) return FALSE; + + if (m_Pages[m_nActiveDlg]) + m_Pages[m_nActiveDlg]->GetSplitPosRef() = ((CChildFrame *)GetParentFrame())->GetSplitterHeight(); + + if (nIndex == m_nActiveDlg) + { + pDlg = m_Pages[m_nActiveDlg]; + PostMessage(WM_MOD_CTRLMSG, CTRLMSG_ACTIVATEPAGE, lParam); + return TRUE; + } + if ((m_nActiveDlg >= 0) && (m_nActiveDlg < MAX_PAGES)) + { + if (m_Pages[m_nActiveDlg]) + { + OnModCtrlMsg(CTRLMSG_DEACTIVATEPAGE, 0); + m_Pages[m_nActiveDlg]->ShowWindow(SW_HIDE); + } + m_nActiveDlg = -1; + } + if (m_Pages[nIndex]) //Ctrl window already created? + { + m_nActiveDlg = nIndex; + pDlg = m_Pages[nIndex]; + } else //Ctrl window is not created yet - creating one. + { + MPT_ASSERT_ALWAYS(GetDocument() != nullptr); + switch(nID) + { + //rewbs.graph + case IDD_CONTROL_GRAPH: + //pDlg = new CCtrlGraph(); + break; + //end rewbs.graph + case IDD_CONTROL_COMMENTS: + pDlg = new CCtrlComments(*this, *GetDocument()); + break; + case IDD_CONTROL_GLOBALS: + pDlg = new CCtrlGeneral(*this, *GetDocument()); + break; + case IDD_CONTROL_PATTERNS: + pDlg = new CCtrlPatterns(*this, *GetDocument()); + break; + case IDD_CONTROL_SAMPLES: + pDlg = new CCtrlSamples(*this, *GetDocument()); + break; + case IDD_CONTROL_INSTRUMENTS: + pDlg = new CCtrlInstruments(*this, *GetDocument()); + break; + default: + return FALSE; + } + if (!pDlg) return FALSE; + pDlg->SetViewWnd(m_hWndView); + BOOL bStatus = pDlg->Create(nID, this); + if(bStatus == 0) // Creation failed. + { + delete pDlg; + return FALSE; + } + m_nActiveDlg = nIndex; + m_Pages[nIndex] = pDlg; + } + RecalcLayout(); + pMainFrm->SetUserText(_T("")); + pMainFrm->SetInfoText(_T("")); + pMainFrm->SetXInfoText(_T("")); //rewbs.xinfo + pDlg->ShowWindow(SW_SHOW); + ((CChildFrame *)GetParentFrame())->SetSplitterHeight(pDlg->GetSplitPosRef()); + if (m_hWndMDI) ::PostMessage(m_hWndMDI, WM_MOD_CHANGEVIEWCLASS, (WPARAM)lParam, (LPARAM)pDlg); + return TRUE; +} + + +void CModControlView::OnDestroy() +{ + m_nActiveDlg = -1; + for (UINT nIndex=0; nIndex<MAX_PAGES; nIndex++) + { + CModControlDlg *pDlg = m_Pages[nIndex]; + if (pDlg) + { + m_Pages[nIndex] = NULL; + pDlg->DestroyWindow(); + delete pDlg; + } + } + CView::OnDestroy(); +} + + +void CModControlView::UpdateView(UpdateHint lHint, CObject *pObject) +{ + CWnd *pActiveDlg = NULL; + CModDoc *pDoc = GetDocument(); + if (!pDoc) return; + // Module type changed: update tabs + if (lHint.GetType()[HINT_MODTYPE]) + { + UINT nCount = 4; + UINT mask = 1 | 2 | 4 | 16; + + if(pDoc->GetSoundFile().GetModSpecifications().instrumentsMax > 0 || pDoc->GetNumInstruments() > 0) + { + mask |= 8; + //mask |= 32; //rewbs.graph + nCount++; + } + if (nCount != (UINT)m_TabCtrl.GetItemCount()) + { + UINT count = 0; + if ((m_nActiveDlg >= 0) && (m_nActiveDlg < MAX_PAGES)) + { + pActiveDlg = m_Pages[m_nActiveDlg]; + if (pActiveDlg) pActiveDlg->ShowWindow(SW_HIDE); + } + m_TabCtrl.DeleteAllItems(); + if (mask & 1) m_TabCtrl.InsertItem(count++, _T("General"), IDD_CONTROL_GLOBALS, IMAGE_GENERAL); + if (mask & 2) m_TabCtrl.InsertItem(count++, _T("Patterns"), IDD_CONTROL_PATTERNS, IMAGE_PATTERNS); + if (mask & 4) m_TabCtrl.InsertItem(count++, _T("Samples"), IDD_CONTROL_SAMPLES, IMAGE_SAMPLES); + if (mask & 8) m_TabCtrl.InsertItem(count++, _T("Instruments"), IDD_CONTROL_INSTRUMENTS, IMAGE_INSTRUMENTS); + //if (mask & 32) m_TabCtrl.InsertItem(count++, _T("Graph"), IDD_CONTROL_GRAPH, IMAGE_GRAPH); //rewbs.graph + if (mask & 16) m_TabCtrl.InsertItem(count++, _T("Comments"), IDD_CONTROL_COMMENTS, IMAGE_COMMENTS); + } + } + // Update child dialogs + for (UINT nIndex=0; nIndex<MAX_PAGES; nIndex++) + { + CModControlDlg *pDlg = m_Pages[nIndex]; + if ((pDlg) && (pObject != pDlg)) pDlg->UpdateView(UpdateHint(lHint), pObject); + } + // Restore the displayed child dialog + if (pActiveDlg) pActiveDlg->ShowWindow(SW_SHOW); +} + + +void CModControlView::OnTabSelchange(NMHDR*, LRESULT* pResult) +{ + SetActivePage(m_TabCtrl.GetCurSel()); + if (pResult) *pResult = 0; +} + + +LRESULT CModControlView::OnActivateModView(WPARAM nIndex, LPARAM lParam) +{ + if(::GetActiveWindow() != CMainFrame::GetMainFrame()->m_hWnd) + { + // If we are in a dialog (e.g. Amplify Sample), do not allow to switch to a different tab. Otherwise, watch the tracker crash! + return 0; + } + + if (m_TabCtrl.m_hWnd) + { + if (nIndex < 100) + { + m_TabCtrl.SetCurSel(static_cast<int>(nIndex)); + SetActivePage(static_cast<int>(nIndex), lParam); + } else + // Might be a dialog id IDD_XXXX + { + int nItems = m_TabCtrl.GetItemCount(); + for (int i=0; i<nItems; i++) + { + if ((WPARAM)m_TabCtrl.GetItemData(i) == nIndex) + { + m_TabCtrl.SetCurSel(i); + SetActivePage(i, lParam); + break; + } + } + } + } + return 0; +} + + +LRESULT CModControlView::OnModCtrlMsg(WPARAM wParam, LPARAM lParam) +{ + if ((m_nActiveDlg >= 0) && (m_nActiveDlg < MAX_PAGES)) + { + CModControlDlg *pActiveDlg = m_Pages[m_nActiveDlg]; + if (pActiveDlg) + { + switch(wParam) + { + case CTRLMSG_SETVIEWWND: + { + m_hWndView = (HWND)lParam; + for (UINT i=0; i<MAX_PAGES; i++) + { + if (m_Pages[i]) m_Pages[i]->SetViewWnd(m_hWndView); + } + } + break; + } + return pActiveDlg->OnModCtrlMsg(wParam, lParam); + } + } + return 0; +} + + +LRESULT CModControlView::OnGetToolTipText(WPARAM uId, LPARAM pszText) +{ + if ((m_nActiveDlg >= 0) && (m_nActiveDlg < MAX_PAGES)) + { + CModControlDlg *pActiveDlg = m_Pages[m_nActiveDlg]; + if (pActiveDlg) return (LRESULT)pActiveDlg->GetToolTipText(static_cast<UINT>(uId), (LPTSTR)pszText); + } + return 0; +} + + +void CModControlView::SampleChanged(SAMPLEINDEX smp) +{ + const CModDoc *modDoc = GetDocument(); + if(modDoc && modDoc->GetNumInstruments()) + { + INSTRUMENTINDEX k = static_cast<INSTRUMENTINDEX>(GetInstrumentChange()); + if(!modDoc->IsChildSample(k, smp)) + { + INSTRUMENTINDEX nins = modDoc->FindSampleParent(smp); + if(nins != INSTRUMENTINDEX_INVALID) + { + InstrumentChanged(nins); + } + } + } else + { + InstrumentChanged(smp); + } +} + + +////////////////////////////////////////////////////////////////// +// CModScrollView + +#ifndef WM_MOUSEHWHEEL +#define WM_MOUSEHWHEEL 0x20E // Only available on Vista and newer +#endif + +IMPLEMENT_SERIAL(CModScrollView, CScrollView, 0) +BEGIN_MESSAGE_MAP(CModScrollView, CScrollView) + //{{AFX_MSG_MAP(CModScrollView) + ON_WM_DESTROY() + ON_WM_MOUSEWHEEL() + ON_WM_MOUSEHWHEEL() +#if !defined(MPT_BUILD_RETRO) + ON_MESSAGE(WM_DPICHANGED, &CModScrollView::OnDPIChanged) +#endif + ON_MESSAGE(WM_MOD_VIEWMSG, &CModScrollView::OnReceiveModViewMsg) + ON_MESSAGE(WM_MOD_DRAGONDROPPING, &CModScrollView::OnDragonDropping) + ON_MESSAGE(WM_MOD_UPDATEPOSITION, &CModScrollView::OnUpdatePosition) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +LRESULT CModScrollView::SendCtrlMessage(UINT uMsg, LPARAM lParam) const +{ + if (m_hWndCtrl) return ::SendMessage(m_hWndCtrl, WM_MOD_CTRLMSG, uMsg, lParam); + return 0; +} + + +void CModScrollView::SendCtrlCommand(int id) const +{ + ::SendMessage(m_hWndCtrl, WM_COMMAND, id, 0); +} + + +BOOL CModScrollView::PostCtrlMessage(UINT uMsg, LPARAM lParam) const +{ + if (m_hWndCtrl) return ::PostMessage(m_hWndCtrl, WM_MOD_CTRLMSG, uMsg, lParam); + return FALSE; +} + + +LRESULT CModScrollView::OnReceiveModViewMsg(WPARAM wParam, LPARAM lParam) +{ + return OnModViewMsg(wParam, lParam); +} + + +void CModScrollView::OnUpdate(CView* pView, LPARAM lHint, CObject*pHint) +{ + if (pView != this) UpdateView(UpdateHint::FromLPARAM(lHint), pHint); +} + + +LRESULT CModScrollView::OnModViewMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case VIEWMSG_SETCTRLWND: + m_hWndCtrl = (HWND)lParam; + break; + + case VIEWMSG_SETFOCUS: + case VIEWMSG_SETACTIVE: + GetParentFrame()->SetActiveView(this); + SetFocus(); + break; + } + return 0; +} + + +void CModScrollView::OnInitialUpdate() +{ + CScrollView::OnInitialUpdate(); + m_nDPIx = Util::GetDPIx(m_hWnd); + m_nDPIy = Util::GetDPIy(m_hWnd); +} + + +LRESULT CModScrollView::OnDPIChanged(WPARAM wParam, LPARAM) +{ + m_nDPIx = LOWORD(wParam); + m_nDPIy = HIWORD(wParam); + return 0; +} + + +void CModScrollView::UpdateIndicator(LPCTSTR lpszText) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->SetUserText((lpszText) ? lpszText : _T("")); +} + + +BOOL CModScrollView::OnMouseWheel(UINT fFlags, short zDelta, CPoint point) +{ + // we don't handle anything but scrolling just now + if (fFlags & (MK_SHIFT | MK_CONTROL)) return FALSE; + + //if the parent is a splitter, it will handle the message + //if (GetParentSplitter(this, TRUE)) return FALSE; + + // we can't get out of it--perform the scroll ourselves + return DoMouseWheel(fFlags, zDelta, point); +} + + +void CModScrollView::OnMouseHWheel(UINT fFlags, short zDelta, CPoint point) +{ + // we don't handle anything but scrolling just now + if (fFlags & (MK_SHIFT | MK_CONTROL)) + { + CScrollView::OnMouseHWheel(fFlags, zDelta, point); + return; + } + + if (OnScrollBy(CSize(zDelta * m_lineDev.cx / WHEEL_DELTA, 0), TRUE)) + UpdateWindow(); +} + + +void CModScrollView::OnDestroy() +{ + CModDoc *pModDoc = GetDocument(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if ((pMainFrm) && (pModDoc)) + { + if (pMainFrm->GetFollowSong(pModDoc) == m_hWnd) + { + pModDoc->SetNotifications(Notification::Default); + pModDoc->SetFollowWnd(NULL); + } + if (pMainFrm->GetMidiRecordWnd() == m_hWnd) + { + pMainFrm->SetMidiRecordWnd(NULL); + } + } + CScrollView::OnDestroy(); +} + + +LRESULT CModScrollView::OnUpdatePosition(WPARAM, LPARAM lParam) +{ + Notification *pnotify = (Notification *)lParam; + if (pnotify) return OnPlayerNotify(pnotify); + return 0; +} + + +BOOL CModScrollView::OnScroll(UINT nScrollCode, UINT nPos, BOOL bDoScroll) +{ + SCROLLINFO info; + if(LOBYTE(nScrollCode) == SB_THUMBTRACK) + { + if(GetScrollInfo(SB_HORZ, &info, SIF_TRACKPOS)) + nPos = info.nTrackPos; + m_nScrollPosX = nPos; + } else if(HIBYTE(nScrollCode) == SB_THUMBTRACK) + { + if(GetScrollInfo(SB_VERT, &info, SIF_TRACKPOS)) + nPos = info.nTrackPos; + m_nScrollPosY = nPos; + } + return CScrollView::OnScroll(nScrollCode, nPos, bDoScroll); +} + + +BOOL CModScrollView::OnScrollBy(CSize sizeScroll, BOOL bDoScroll) +{ + BOOL ret = CScrollView::OnScrollBy(sizeScroll, bDoScroll); + if(ret) + { + SCROLLINFO info; + if(sizeScroll.cx) + { + if(GetScrollInfo(SB_HORZ, &info, SIF_POS)) + m_nScrollPosX = info.nPos; + } + if(sizeScroll.cy) + { + if(GetScrollInfo(SB_VERT, &info, SIF_POS)) + m_nScrollPosY = info.nPos; + } + } + return ret; +} + + +int CModScrollView::SetScrollPos(int nBar, int nPos, BOOL bRedraw) +{ + if(nBar == SB_HORZ) + m_nScrollPosX = nPos; + else if(nBar == SB_VERT) + m_nScrollPosY = nPos; + return CScrollView::SetScrollPos(nBar, nPos, bRedraw); +} + + +void CModScrollView::SetScrollSizes(int nMapMode, SIZE sizeTotal, const SIZE& sizePage, const SIZE& sizeLine) +{ + CScrollView::SetScrollSizes(nMapMode, sizeTotal, sizePage, sizeLine); + // Fix scroll positions + SCROLLINFO info; + if(GetScrollInfo(SB_HORZ, &info, SIF_POS)) + m_nScrollPosX = info.nPos; + if(GetScrollInfo(SB_VERT, &info, SIF_POS)) + m_nScrollPosY = info.nPos; +} + + +BOOL CModScrollView::OnGesturePan(CPoint ptFrom, CPoint ptTo) +{ + // On Windows 8 and later, panning with touch gestures does not generate sensible WM_*SCROLL messages. + // OnScrollBy is only ever called with a size of 0/0 in this case. + // WM_GESTURE on the other hand gives us sensible data to work with. + OnScrollBy(ptTo - ptFrom, TRUE); + return TRUE; +} + + +//////////////////////////////////////////////////////////////////////////// +// CModControlBar + +BEGIN_MESSAGE_MAP(CModControlBar, CToolBarCtrl) + ON_MESSAGE(WM_HELPHITTEST, &CModControlBar::OnHelpHitTest) +END_MESSAGE_MAP() + + +BOOL CModControlBar::Init(CImageList &icons, CImageList &disabledIcons) +{ + const int imgSize = Util::ScalePixels(16, m_hWnd), btnSizeX = Util::ScalePixels(26, m_hWnd), btnSizeY = Util::ScalePixels(24, m_hWnd); + SetButtonStructSize(sizeof(TBBUTTON)); + SetBitmapSize(CSize(imgSize, imgSize)); + SetButtonSize(CSize(btnSizeX, btnSizeY)); + + // Add bitmaps + SetImageList(&icons); + SetDisabledImageList(&disabledIcons); + UpdateStyle(); + return TRUE; +} + + +BOOL CModControlBar::AddButton(UINT nID, int iImage, UINT nStyle, UINT nState) +{ + TBBUTTON btn; + + btn.iBitmap = iImage; + btn.idCommand = nID; + btn.fsStyle = (BYTE)nStyle; + btn.fsState = (BYTE)nState; + btn.dwData = 0; + btn.iString = 0; + return AddButtons(1, &btn); +} + + +void CModControlBar::UpdateStyle() +{ + if (m_hWnd) + { + LONG lStyleOld = GetWindowLong(m_hWnd, GWL_STYLE); + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS) + lStyleOld |= TBSTYLE_FLAT; + else + lStyleOld &= ~TBSTYLE_FLAT; + lStyleOld |= CCS_NORESIZE | CCS_NOPARENTALIGN | CCS_NODIVIDER | TBSTYLE_TOOLTIPS; + SetWindowLong(m_hWnd, GWL_STYLE, lStyleOld); + Invalidate(); + } +} + + +LRESULT CModControlBar::OnHelpHitTest(WPARAM, LPARAM lParam) +{ + TBBUTTON tbbn; + POINT point; + point.x = GET_X_LPARAM(lParam); + point.y = GET_Y_LPARAM(lParam); + int ndx = HitTest(&point); + if ((ndx >= 0) && (GetButton(ndx, &tbbn))) + { + return HID_BASE_COMMAND + tbbn.idCommand; + } + return 0; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Globals.h b/Src/external_dependencies/openmpt-trunk/mptrack/Globals.h new file mode 100644 index 00000000..8de5e17e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Globals.h @@ -0,0 +1,252 @@ +/* + * Globals.h + * --------- + * Purpose: Implementation of various views of the tracker interface. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +#ifndef WM_HELPHITTEST +#define WM_HELPHITTEST 0x366 +#endif + +#ifndef HID_BASE_COMMAND +#define HID_BASE_COMMAND 0x10000 +#endif + +#define ID_EDIT_MIXPASTE ID_EDIT_PASTE_SPECIAL //rewbs.mixPaste + +class CModControlView; +class CModControlBar; +class CModDoc; + +class CModControlBar: public CToolBarCtrl +{ +public: + BOOL Init(CImageList &icons, CImageList &disabledIcons); + void UpdateStyle(); + BOOL AddButton(UINT nID, int iImage=0, UINT nStyle=TBSTYLE_BUTTON, UINT nState=TBSTATE_ENABLED); + afx_msg LRESULT OnHelpHitTest(WPARAM, LPARAM); + DECLARE_MESSAGE_MAP() +}; + + +class CModControlDlg: public CDialog +{ +protected: + CModDoc &m_modDoc; + CSoundFile &m_sndFile; + CModControlView &m_parent; + HWND m_hWndView; + LONG m_nLockCount; + int m_nDPIx, m_nDPIy; // Cached DPI settings + BOOL m_bInitialized; + +public: + CModControlDlg(CModControlView &parent, CModDoc &document); + virtual ~CModControlDlg(); + +public: + void SetViewWnd(HWND hwndView) { m_hWndView = hwndView; } + HWND GetViewWnd() const { return m_hWndView; } + LRESULT SendViewMessage(UINT uMsg, LPARAM lParam=0) const; + BOOL PostViewMessage(UINT uMsg, LPARAM lParam=0) const; + LRESULT SwitchToView() const { return SendViewMessage(VIEWMSG_SETACTIVE); } + void LockControls() { m_nLockCount++; } + void UnlockControls() { PostMessage(WM_MOD_UNLOCKCONTROLS); } + bool IsLocked() const { return (m_nLockCount > 0); } + virtual Setting<LONG> &GetSplitPosRef() = 0; //rewbs.varWindowSize + + afx_msg void OnEditCut() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_CUT, 0); } + afx_msg void OnEditCopy() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_COPY, 0); } + afx_msg void OnEditPaste() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_PASTE, 0); } + afx_msg void OnEditMixPaste() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_MIXPASTE, 0); } + afx_msg void OnEditMixPasteITStyle() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_MIXPASTE_ITSTYLE, 0); } + afx_msg void OnEditPasteFlood() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_PASTEFLOOD, 0); } + afx_msg void OnEditPushForwardPaste() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_PUSHFORWARDPASTE, 0); } + afx_msg void OnEditFind() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_FIND, 0); } + afx_msg void OnEditFindNext() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_FINDNEXT, 0); } + afx_msg void OnSwitchToView() { if (m_hWndView) ::PostMessage(m_hWndView, WM_MOD_VIEWMSG, VIEWMSG_SETFOCUS, 0); } + + //{{AFX_VIRTUAL(CModControlDlg) + void OnOK() override {} + void OnCancel() override {} + virtual void RecalcLayout() = 0; + virtual void UpdateView(UpdateHint, CObject *) = 0; + virtual CRuntimeClass *GetAssociatedViewClass() { return nullptr; } + virtual LRESULT OnModCtrlMsg(WPARAM wParam, LPARAM lParam); + virtual void OnActivatePage(LPARAM) {} + virtual void OnDeactivatePage() {} + BOOL OnInitDialog() override; + INT_PTR OnToolHitTest(CPoint point, TOOLINFO *pTI) const override; + virtual BOOL GetToolTipText(UINT, LPTSTR) { return FALSE; } + //}}AFX_VIRTUAL + //{{AFX_MSG(CModControlDlg) + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg LRESULT OnUnlockControls(WPARAM, LPARAM) { if (m_nLockCount > 0) m_nLockCount--; return 0; } + afx_msg BOOL OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult); + afx_msg LRESULT OnDPIChanged(WPARAM = 0, LPARAM = 0); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +class CModTabCtrl: public CTabCtrl +{ +public: + BOOL InsertItem(int nIndex, LPCTSTR pszText, LPARAM lParam=0, int iImage=-1); + BOOL Create(DWORD dwStyle, const RECT& rect, CWnd* pParentWnd, UINT nID); + LPARAM GetItemData(int nIndex); +}; + + +class CModControlView: public CView +{ +public: + enum Views + { + VIEW_UNKNOWN = -1, + VIEW_GLOBALS = 0, + VIEW_PATTERNS, + VIEW_SAMPLES, + VIEW_INSTRUMENTS, + VIEW_COMMENTS, + VIEW_PLUGINS, + MAX_PAGES + }; + +protected: + CModTabCtrl m_TabCtrl; + CModControlDlg *m_Pages[MAX_PAGES]; + int m_nActiveDlg, m_nInstrumentChanged; + HWND m_hWndView, m_hWndMDI; + +protected: // create from serialization only + CModControlView(); + DECLARE_DYNCREATE(CModControlView) + +public: + virtual ~CModControlView() {} + CModDoc* GetDocument() const { return (CModDoc *)m_pDocument; } + void SampleChanged(SAMPLEINDEX smp); + void InstrumentChanged(int nInstr=-1) { m_nInstrumentChanged = nInstr; } + int GetInstrumentChange() const { return m_nInstrumentChanged; } + void SetMDIParentFrame(HWND hwnd) { m_hWndMDI = hwnd; } + void ForceRefresh(); + CModControlDlg *GetCurrentControlDlg() { return m_Pages[m_nActiveDlg]; } + +protected: + void RecalcLayout(); + void UpdateView(UpdateHint hint, CObject *pHint = nullptr); + BOOL SetActivePage(int nIndex = -1, LPARAM lParam=-1); +public: + int GetActivePage() const { return m_nActiveDlg; } + +protected: + //{{AFX_VIRTUAL(CModControlView) +public: + BOOL PreCreateWindow(CREATESTRUCT& cs) override; +protected: + void OnInitialUpdate() override; // called first time after construct + void OnDraw(CDC *) override {} + void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CModControlView) + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnDestroy(); + afx_msg void OnTabSelchange(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnEditCut() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_CUT, 0); } + afx_msg void OnEditCopy() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_COPY, 0); } + afx_msg void OnEditPaste() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_PASTE, 0); } + afx_msg void OnEditMixPaste() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_MIXPASTE, 0); } //rewbs.mixPaste + afx_msg void OnEditMixPasteITStyle() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_MIXPASTE_ITSTYLE, 0); } + afx_msg void OnEditFind() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_FIND, 0); } + afx_msg void OnEditFindNext() { if (m_hWndView) ::SendMessage(m_hWndView, WM_COMMAND, ID_EDIT_FINDNEXT, 0); } + afx_msg void OnSwitchToView() { if (m_hWndView) ::PostMessage(m_hWndView, WM_MOD_VIEWMSG, VIEWMSG_SETFOCUS, 0); } + afx_msg LRESULT OnActivateModView(WPARAM, LPARAM); + afx_msg LRESULT OnModCtrlMsg(WPARAM wParam, LPARAM lParam); + afx_msg LRESULT OnGetToolTipText(WPARAM, LPARAM); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + +// Non-client button attributes +#define NCBTNS_MOUSEOVER 0x01 +#define NCBTNS_CHECKED 0x02 +#define NCBTNS_DISABLED 0x04 +#define NCBTNS_PUSHED 0x08 + + +class CModScrollView: public CScrollView +{ +protected: + HWND m_hWndCtrl; + int m_nScrollPosX, m_nScrollPosY; + int m_nDPIx, m_nDPIy; // Cached DPI settings + +public: + DECLARE_SERIAL(CModScrollView) + CModScrollView() : m_hWndCtrl(nullptr), m_nScrollPosX(0), m_nScrollPosY(0) { } + virtual ~CModScrollView() {} + +public: + CModDoc* GetDocument() const { return (CModDoc *)m_pDocument; } + LRESULT SendCtrlMessage(UINT uMsg, LPARAM lParam=0) const; + void SendCtrlCommand(int id) const; + BOOL PostCtrlMessage(UINT uMsg, LPARAM lParam=0) const; + void UpdateIndicator(LPCTSTR lpszText = nullptr); + +public: + //{{AFX_VIRTUAL(CModScrollView) + void OnInitialUpdate() override; + void OnDraw(CDC *) override {} + void OnPrepareDC(CDC*, CPrintInfo*) override {} + void OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint) override; + virtual void UpdateView(UpdateHint, CObject *) {} + virtual LRESULT OnModViewMsg(WPARAM wParam, LPARAM lParam); + virtual BOOL OnDragonDrop(BOOL, const DRAGONDROP *) { return FALSE; } + virtual LRESULT OnPlayerNotify(Notification *) { return 0; } + //}}AFX_VIRTUAL + + CModControlDlg *GetControlDlg() { return static_cast<CModControlView *>(CWnd::FromHandle(m_hWndCtrl))->GetCurrentControlDlg(); } + +protected: + //{{AFX_MSG(CModScrollView) + afx_msg void OnDestroy(); + afx_msg LRESULT OnReceiveModViewMsg(WPARAM wParam, LPARAM lParam); + afx_msg BOOL OnMouseWheel(UINT fFlags, short zDelta, CPoint point); + afx_msg void OnMouseHWheel(UINT fFlags, short zDelta, CPoint point); + afx_msg LRESULT OnDragonDropping(WPARAM bDoDrop, LPARAM lParam) { return OnDragonDrop((BOOL)bDoDrop, (const DRAGONDROP *)lParam); } + afx_msg LRESULT OnUpdatePosition(WPARAM, LPARAM); + afx_msg LRESULT OnDPIChanged(WPARAM = 0, LPARAM = 0); + + // Fixes for 16-bit limitation in MFC's CScrollView + BOOL OnScroll(UINT nScrollCode, UINT nPos, BOOL bDoScroll = TRUE) override; + BOOL OnScrollBy(CSize sizeScroll, BOOL bDoScroll = TRUE) override; + int SetScrollPos(int nBar, int nPos, BOOL bRedraw = TRUE); + void SetScrollSizes(int nMapMode, SIZE sizeTotal, const SIZE& sizePage = CScrollView::sizeDefault, const SIZE& sizeLine = CScrollView::sizeDefault); + + BOOL OnGesturePan(CPoint ptFrom, CPoint ptTo) override; + + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////////// + +//{{AFX_INSERT_LOCATION}} +// Microsoft Developer Studio will insert additional declarations immediately before the previous line. + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.cpp new file mode 100644 index 00000000..732db274 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.cpp @@ -0,0 +1,519 @@ +/* + * HTTP.cpp + * -------- + * Purpose: Simple HTTP client interface. + * 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 "HTTP.h" +#include "mpt/system_error/system_error.hpp" +#include <WinInet.h> +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +URI ParseURI(mpt::ustring str) +{ + URI uri; + std::size_t scheme_delim_pos = str.find(':'); + if(scheme_delim_pos == mpt::ustring::npos) + { + throw bad_uri("no scheme delimiter"); + } + if(scheme_delim_pos == 0) + { + throw bad_uri("no scheme"); + } + uri.scheme = str.substr(0, scheme_delim_pos); + str = str.substr(scheme_delim_pos + 1); + if(str.substr(0, 2) == U_("//")) + { + str = str.substr(2); + std::size_t authority_delim_pos = str.find_first_of(U_("/?#")); + mpt::ustring authority = str.substr(0, authority_delim_pos); + std::size_t userinfo_delim_pos = authority.find(U_("@")); + if(userinfo_delim_pos != mpt::ustring::npos) + { + mpt::ustring userinfo = authority.substr(0, userinfo_delim_pos); + authority = authority.substr(userinfo_delim_pos + 1); + std::size_t username_delim_pos = userinfo.find(U_(":")); + uri.username = userinfo.substr(0, username_delim_pos); + if(username_delim_pos != mpt::ustring::npos) + { + uri.password = userinfo.substr(username_delim_pos + 1); + } + } + std::size_t beg_bracket_pos = authority.find(U_("[")); + std::size_t end_bracket_pos = authority.find(U_("]")); + std::size_t port_delim_pos = authority.find_last_of(U_(":")); + if(beg_bracket_pos != mpt::ustring::npos && end_bracket_pos != mpt::ustring::npos) + { + if(port_delim_pos != mpt::ustring::npos && port_delim_pos > end_bracket_pos) + { + uri.host = authority.substr(0, port_delim_pos); + uri.port = authority.substr(port_delim_pos + 1); + } else + { + uri.host = authority; + } + } else + { + uri.host = authority.substr(0, port_delim_pos); + if(port_delim_pos != mpt::ustring::npos) + { + uri.port = authority.substr(port_delim_pos + 1); + } + } + if(authority_delim_pos != mpt::ustring::npos) + { + str = str.substr(authority_delim_pos); + } else + { + str = U_(""); + } + } + std::size_t path_delim_pos = str.find_first_of(U_("?#")); + uri.path = str.substr(0, path_delim_pos); + if(path_delim_pos != mpt::ustring::npos) + { + str = str.substr(path_delim_pos); + std::size_t query_delim_pos = str.find(U_("#")); + if(query_delim_pos != mpt::ustring::npos) + { + if(query_delim_pos > 0) + { + uri.query = str.substr(1, query_delim_pos - 1); + uri.fragment = str.substr(query_delim_pos + 1); + } else + { + uri.fragment = str.substr(query_delim_pos + 1); + } + } else + { + uri.query = str.substr(1); + } + } + return uri; +} + + +namespace HTTP +{ + + +exception::exception(const mpt::ustring &m) + : std::runtime_error(std::string("HTTP error: ") + mpt::ToCharset(mpt::CharsetException, m)) +{ + message = m; +} + +mpt::ustring exception::GetMessage() const +{ + return message; +} + + +class LastErrorException + : public exception +{ +public: + LastErrorException() + : exception(mpt::windows::GetErrorMessage(GetLastError(), GetModuleHandle(TEXT("wininet.dll")))) + { + } +}; + + +struct NativeHandle +{ + HINTERNET native_handle; + NativeHandle(HINTERNET h) + : native_handle(h) + { + } + operator HINTERNET() const + { + return native_handle; + } +}; + + +Handle::Handle() + : handle(std::make_unique<NativeHandle>(HINTERNET(NULL))) +{ +} + +Handle::operator bool() const +{ + return handle->native_handle != HINTERNET(NULL); +} + +bool Handle::operator!() const +{ + return handle->native_handle == HINTERNET(NULL); +} + +Handle::Handle(NativeHandle h) + : handle(std::make_unique<NativeHandle>(HINTERNET(NULL))) +{ + handle->native_handle = h.native_handle; +} + +Handle & Handle::operator=(NativeHandle h) +{ + if(handle->native_handle) + { + InternetCloseHandle(handle->native_handle); + handle->native_handle = HINTERNET(NULL); + } + handle->native_handle = h.native_handle; + return *this; +} + +Handle::operator NativeHandle () +{ + return *handle; +} + +Handle::~Handle() +{ + if(handle->native_handle) + { + InternetCloseHandle(handle->native_handle); + handle->native_handle = HINTERNET(NULL); + } +} + + +InternetSession::InternetSession(mpt::ustring userAgent) +{ + internet = NativeHandle(InternetOpen(mpt::ToWin(userAgent).c_str(), INTERNET_OPEN_TYPE_PRECONFIG, NULL, NULL, 0)); + if(!internet) + { + throw HTTP::LastErrorException(); + } +} + +InternetSession::operator NativeHandle () +{ + return internet; +} + + +static mpt::winstring Verb(Method method) +{ + mpt::winstring result; + switch(method) + { + case Method::Get: + result = _T("GET"); + break; + case Method::Head: + result = _T("HEAD"); + break; + case Method::Post: + result = _T("POST"); + break; + case Method::Put: + result = _T("PUT"); + break; + case Method::Delete: + result = _T("DELETE"); + break; + case Method::Trace: + result = _T("TRACE"); + break; + case Method::Options: + result = _T("OPTIONS"); + break; + case Method::Connect: + result = _T("CONNECT"); + break; + case Method::Patch: + result = _T("PATCH"); + break; + } + return result; +} + +static bool IsCachable(Method method) +{ + return method == Method::Get || method == Method::Head; +} + + +namespace +{ +class AcceptMimeTypesWrapper +{ +private: + std::vector<mpt::winstring> strings; + std::vector<LPCTSTR> array; +public: + AcceptMimeTypesWrapper(AcceptMimeTypes acceptMimeTypes) + { + for(const auto &mimeType : acceptMimeTypes) + { + strings.push_back(mpt::ToWin(mpt::Charset::ASCII, mimeType)); + } + array.resize(strings.size() + 1); + for(std::size_t i = 0; i < strings.size(); ++i) + { + array[i] = strings[i].c_str(); + } + array[strings.size()] = NULL; + } + operator LPCTSTR*() + { + return strings.empty() ? NULL : array.data(); + } +}; +} + + +void Request::progress(Progress progress, uint64 transferred, std::optional<uint64> expectedSize) const +{ + if(progressCallback) + { + progressCallback(progress, transferred, expectedSize); + } +} + + +Result Request::operator()(InternetSession &internet) const +{ + progress(Progress::Start, 0, std::nullopt); + Port actualPort = port; + if(actualPort == Port::Default) + { + actualPort = (protocol != Protocol::HTTP) ? Port::HTTPS : Port::HTTP; + } + Handle connection = NativeHandle(InternetConnect( + NativeHandle(internet), + mpt::ToWin(host).c_str(), + static_cast<uint16>(actualPort), + !username.empty() ? mpt::ToWin(username).c_str() : NULL, + !password.empty() ? mpt::ToWin(password).c_str() : NULL, + INTERNET_SERVICE_HTTP, + 0, + 0)); + if(!connection) + { + throw HTTP::LastErrorException(); + } + progress(Progress::ConnectionEstablished, 0, std::nullopt); + mpt::ustring queryPath = path; + if(!query.empty()) + { + std::vector<mpt::ustring> arguments; + for(const auto &[key, value] : query) + { + if(!value.empty()) + { + arguments.push_back(MPT_UFORMAT("{}={}")(key, value)); + } else + { + arguments.push_back(MPT_UFORMAT("{}")(key)); + } + } + queryPath += U_("?") + mpt::String::Combine(arguments, U_("&")); + } + Handle request = NativeHandle(HttpOpenRequest( + NativeHandle(connection), + Verb(method).c_str(), + mpt::ToWin(path).c_str(), + NULL, + !referrer.empty() ? mpt::ToWin(referrer).c_str() : NULL, + AcceptMimeTypesWrapper(acceptMimeTypes), + 0 + | ((protocol != Protocol::HTTP) ? INTERNET_FLAG_SECURE : 0) + | (IsCachable(method) ? 0 : INTERNET_FLAG_NO_CACHE_WRITE) + | ((flags & NoCache) ? (INTERNET_FLAG_PRAGMA_NOCACHE | INTERNET_FLAG_RELOAD | INTERNET_FLAG_NO_CACHE_WRITE) : 0) + | ((flags & AutoRedirect) ? 0 : INTERNET_FLAG_NO_AUTO_REDIRECT) + , + NULL)); + if(!request) + { + throw HTTP::LastErrorException(); + } + progress(Progress::RequestOpened, 0, std::nullopt); + { + std::string headersString; + if(!dataMimeType.empty()) + { + headersString += MPT_AFORMAT("Content-type: {}\r\n")(dataMimeType); + } + if(!headers.empty()) + { + for(const auto &[key, value] : headers) + { + headersString += MPT_AFORMAT("{}: {}\r\n")(key, value); + } + } + if(HttpSendRequest( + NativeHandle(request), + !headersString.empty() ? mpt::ToWin(mpt::Charset::ASCII, headersString).c_str() : NULL, + !headersString.empty() ? mpt::saturate_cast<DWORD>(mpt::ToWin(mpt::Charset::ASCII, headersString).length()) : 0, + !data.empty() ? (LPVOID)data.data() : NULL, + !data.empty() ? mpt::saturate_cast<DWORD>(data.size()) : 0) + == FALSE) + { + throw HTTP::LastErrorException(); + } + } + progress(Progress::RequestSent, 0, std::nullopt); + Result result; + { + DWORD statusCode = 0; + DWORD length = sizeof(statusCode); + if(HttpQueryInfo(NativeHandle(request), HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &statusCode, &length, NULL) == FALSE) + { + throw HTTP::LastErrorException(); + } + result.Status = statusCode; + } + progress(Progress::ResponseReceived, 0, std::nullopt); + DWORD contentLength = static_cast<DWORD>(-1); + { + DWORD length = sizeof(contentLength); + if(HttpQueryInfo(NativeHandle(request), HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &contentLength, &length, NULL) == FALSE) + { + contentLength = static_cast<DWORD>(-1); + } + } + uint64 transferred = 0; + if(contentLength != static_cast<DWORD>(-1)) + { + result.ContentLength = contentLength; + } + std::optional<uint64> expectedSize = result.ContentLength; + progress(Progress::TransferBegin, transferred, expectedSize); + { + std::vector<std::byte> resultBuffer; + DWORD bytesRead = 0; + do + { + std::array<std::byte, mpt::IO::BUFFERSIZE_TINY> downloadBuffer; + DWORD availableSize = 0; + if(InternetQueryDataAvailable(NativeHandle(request), &availableSize, 0, NULL) == FALSE) + { + throw HTTP::LastErrorException(); + } + availableSize = std::clamp(availableSize, DWORD(0), mpt::saturate_cast<DWORD>(mpt::IO::BUFFERSIZE_TINY)); + if(InternetReadFile(NativeHandle(request), downloadBuffer.data(), availableSize, &bytesRead) == FALSE) + { + throw HTTP::LastErrorException(); + } + if(outputStream) + { + if(!mpt::IO::WriteRaw(*outputStream, mpt::as_span(downloadBuffer).first(bytesRead))) + { + throw HTTP::exception(U_("Writing output file failed.")); + } + } else + { + mpt::append(resultBuffer, downloadBuffer.data(), downloadBuffer.data() + bytesRead); + } + transferred += bytesRead; + progress(Progress::TransferRunning, transferred, expectedSize); + } while(bytesRead != 0); + result.Data = std::move(resultBuffer); + } + progress(Progress::TransferDone, transferred, expectedSize); + return result; +} + + +Request &Request::SetURI(const URI &uri) +{ + if(uri.scheme == U_("")) + { + throw bad_uri("no scheme"); + } else if(uri.scheme == U_("http")) + { + protocol = HTTP::Protocol::HTTP; + } else if(uri.scheme == U_("https")) + { + protocol = HTTP::Protocol::HTTPS; + } else + { + throw bad_uri("wrong scheme"); + } + host = uri.host; + if(!uri.port.empty()) + { + port = HTTP::Port(ConvertStrTo<uint16>(uri.port)); + } else + { + port = HTTP::Port::Default; + } + username = uri.username; + password = uri.password; + if(uri.path.empty()) + { + path = U_("/"); + } else + { + path = uri.path; + } + query.clear(); + auto keyvals = mpt::String::Split<mpt::ustring>(uri.query, U_("&")); + for(const auto &keyval : keyvals) + { + std::size_t delim_pos = keyval.find(U_("=")); + mpt::ustring key = keyval.substr(0, delim_pos); + mpt::ustring val; + if(delim_pos != mpt::ustring::npos) + { + val = keyval.substr(delim_pos + 1); + } + query.push_back(std::make_pair(key, val)); + } + // ignore fragment + return *this; +} + + +#if defined(MPT_BUILD_RETRO) +Request &Request::InsecureTLSDowngradeWindowsXP() +{ + if(mpt::OS::Windows::IsOriginal() && mpt::OS::Windows::Version::Current().IsBefore(mpt::OS::Windows::Version::WinVista)) + { + // TLS 1.0 is not enabled by default until IE7. Since WinInet won't let us override this setting, we cannot assume that HTTPS + // is going to work on older systems. Besides... Windows XP is already enough of a security risk by itself. :P + if(protocol == Protocol::HTTPS) + { + protocol = Protocol::HTTP; + } + if(port == Port::HTTPS) + { + port = Port::HTTP; + } + } + return *this; +} +#endif // MPT_BUILD_RETRO + + +Result SimpleGet(InternetSession &internet, Protocol protocol, const mpt::ustring &host, const mpt::ustring &path) +{ + HTTP::Request request; + request.protocol = protocol; + request.host = host; + request.method = HTTP::Method::Get; + request.path = path; + return internet(request); +} + + +} // namespace HTTP + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.h b/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.h new file mode 100644 index 00000000..91ae1afe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/HTTP.h @@ -0,0 +1,257 @@ +/* + * HTTP.h + * ------ + * Purpose: Simple HTTP client interface. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <functional> +#include <iosfwd> +#include <optional> + + +OPENMPT_NAMESPACE_BEGIN + + +struct URI +{ + mpt::ustring scheme; + mpt::ustring username; + mpt::ustring password; + mpt::ustring host; + mpt::ustring port; + mpt::ustring path; + mpt::ustring query; + mpt::ustring fragment; +}; + +class bad_uri + : public std::runtime_error +{ +public: + bad_uri(const std::string &msg) + : std::runtime_error(msg) + { + } +}; + +URI ParseURI(mpt::ustring str); + + +namespace HTTP +{ + + +class exception + : public std::runtime_error +{ +private: + mpt::ustring message; +public: + exception(const mpt::ustring &m); + mpt::ustring GetMessage() const; +}; + + +class status_exception + : public std::runtime_error +{ +public: + status_exception(uint64 status) + : std::runtime_error(MPT_AFORMAT("HTTP status {}")(status)) + { + return; + } +}; + + +class Abort + : public exception +{ +public: + Abort() : + exception(U_("Operation aborted.")) + { + return; + } +}; + + +struct NativeHandle; + + +class Handle +{ +private: + std::unique_ptr<NativeHandle> handle; +public: + Handle(); + Handle(const Handle &) = delete; + Handle & operator=(const Handle &) = delete; + explicit operator bool() const; + bool operator!() const; + Handle(NativeHandle h); + Handle & operator=(NativeHandle h); + operator NativeHandle (); + ~Handle(); +}; + + +class InternetSession +{ +private: + Handle internet; +public: + InternetSession(mpt::ustring userAgent); + operator NativeHandle (); + template <typename TRequest> + auto operator()(const TRequest &request) -> decltype(request(*this)) + { + return request(*this); + } + template <typename TRequest> + auto Request(const TRequest &request) -> decltype(request(*this)) + { + return request(*this); + } +}; + + +enum class Protocol +{ + HTTP, + HTTPS, +}; + + +enum class Port : uint16 +{ + Default = 0, + HTTP = 80, + HTTPS = 443, +}; + + +enum class Method +{ + Get, + Head, + Post, + Put, + Delete, + Trace, + Options, + Connect, + Patch, +}; + + +using Query = std::vector<std::pair<mpt::ustring, mpt::ustring>>; + + +namespace MimeType { + inline std::string Text() { return "text/plain"; } + inline std::string JSON() { return "application/json"; } + inline std::string Binary() { return "application/octet-stream"; } +} + + +using AcceptMimeTypes = std::vector<std::string>; +namespace MimeTypes { + inline AcceptMimeTypes Text() { return {"text/*"}; } + inline AcceptMimeTypes JSON() { return {MimeType::JSON()}; } + inline AcceptMimeTypes Binary() { return {MimeType::Binary()}; } +} + + +using Headers = std::vector<std::pair<std::string, std::string>>; + + +enum Flags +{ + None = 0x00u, + NoCache = 0x01u, + AutoRedirect = 0x02u, +}; + + +struct Result +{ + uint64 Status = 0; + std::optional<uint64> ContentLength; + std::vector<std::byte> Data; + + void CheckStatus(uint64 expected) const + { + if(Status != expected) + { + throw status_exception(Status); + } + } + +}; + + +enum class Progress +{ + Start = 1, + ConnectionEstablished = 2, + RequestOpened = 3, + RequestSent = 4, + ResponseReceived = 5, + TransferBegin = 6, + TransferRunning = 7, + TransferDone = 8, +}; + + +struct Request +{ + + Protocol protocol = Protocol::HTTPS; + mpt::ustring host; + Port port = Port::Default; + mpt::ustring username; + mpt::ustring password; + Method method = Method::Get; + mpt::ustring path = U_("/"); + Query query; + mpt::ustring referrer; + AcceptMimeTypes acceptMimeTypes; + Flags flags = None; + Headers headers; + std::string dataMimeType; + mpt::const_byte_span data; + + std::ostream *outputStream = nullptr; + std::function<void(Progress, uint64, std::optional<uint64>)> progressCallback = nullptr; + + Request &SetURI(const URI &uri); + +#if defined(MPT_BUILD_RETRO) + Request &InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + + Result operator()(InternetSession &internet) const; + +private: + + void progress(Progress progress, uint64 transferred, std::optional<uint64> expectedSize) const; + +}; + + +Result SimpleGet(InternetSession &internet, Protocol protocol, const mpt::ustring &host, const mpt::ustring &path); + + +} // namespace HTTP + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.cpp new file mode 100644 index 00000000..9ab159ac --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.cpp @@ -0,0 +1,235 @@ +/* +* IPCWindow.cpp +* ------------- +* Purpose: Hidden window to receive file open commands from another OpenMPT instance +* 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 "IPCWindow.h" + +#include "../common/version.h" +#include "Mptrack.h" + + +OPENMPT_NAMESPACE_BEGIN + +namespace IPCWindow +{ + + static constexpr TCHAR ClassName[] = _T("OpenMPT_IPC_Wnd"); + static HWND ipcWindow = nullptr; + + static LRESULT CALLBACK IPCWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) + { + if(uMsg == WM_COPYDATA) + { + const auto ©Data = *reinterpret_cast<const COPYDATASTRUCT *>(lParam); + LRESULT result = 0; + switch(static_cast<Function>(copyData.dwData)) + { + case Function::Open: + { + std::size_t count = copyData.cbData / sizeof(WCHAR); + const WCHAR* data = static_cast<const WCHAR *>(copyData.lpData); + const std::wstring name = std::wstring(data, data + count); + result = theApp.OpenDocumentFile(mpt::PathString::FromWide(name).AsNative().c_str()) ? 1 : 2; + } + break; + case Function::SetWindowForeground: + { + auto mainWnd = theApp.GetMainWnd(); + if(mainWnd) + { + if(mainWnd->IsIconic()) + { + mainWnd->ShowWindow(SW_RESTORE); + } + mainWnd->SetForegroundWindow(); + result = 1; + } else + { + result = 0; + } + } + break; + case Function::GetVersion: + { + result = Version::Current().GetRawVersion(); + } + break; + case Function::GetArchitecture: + { + #if MPT_OS_WINDOWS + result = static_cast<int32>(mpt::OS::Windows::GetProcessArchitecture()); + #else + result = -1; + #endif + } + break; + case Function::HasSameBinaryPath: + { + std::size_t count = copyData.cbData / sizeof(WCHAR); + const WCHAR* data = static_cast<const WCHAR *>(copyData.lpData); + const std::wstring path = std::wstring(data, data + count); + result = (theApp.GetInstallBinArchPath().ToWide() == path) ? 1 : 0; + } + break; + case Function::HasSameSettingsPath: + { + std::size_t count = copyData.cbData / sizeof(WCHAR); + const WCHAR* data = static_cast<const WCHAR *>(copyData.lpData); + const std::wstring path = std::wstring(data, data + count); + result = (theApp.GetConfigPath().ToWide() == path) ? 1 : 0; + } + break; + default: + result = 0; + break; + } + return result; + } + return ::DefWindowProc(hwnd, uMsg, wParam, lParam); + } + + void Open(HINSTANCE hInstance) + { + WNDCLASS ipcWindowClass = + { + 0, + IPCWindowProc, + 0, + 0, + hInstance, + nullptr, + nullptr, + nullptr, + nullptr, + ClassName + }; + auto ipcAtom = RegisterClass(&ipcWindowClass); + ipcWindow = CreateWindow(MAKEINTATOM(ipcAtom), _T("OpenMPT IPC Window"), 0, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, 0); + } + + void Close() + { + ::DestroyWindow(ipcWindow); + ipcWindow = nullptr; + } + + LRESULT SendIPC(HWND ipcWnd, Function function, mpt::const_byte_span data) + { + if(!ipcWnd) + { + return 0; + } + if(!mpt::in_range<DWORD>(data.size())) + { + return 0; + } + COPYDATASTRUCT copyData{}; + copyData.dwData = static_cast<ULONG>(function); + copyData.cbData = mpt::saturate_cast<DWORD>(data.size()); + copyData.lpData = const_cast<void*>(mpt::void_cast<const void*>(data.data())); + return ::SendMessage(ipcWnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(©Data)); + } + + HWND FindIPCWindow() + { + return ::FindWindow(ClassName, nullptr); + } + + struct EnumWindowState + { + FlagSet<InstanceRequirements> require; + HWND result = nullptr; + }; + + static BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) + { + EnumWindowState &state = *reinterpret_cast<EnumWindowState*>(lParam); + if(hwnd) + { + TCHAR className[256]; + MemsetZero(className); + if(::GetClassName(hwnd, className, 256) > 0) + { + if(!_tcscmp(className, IPCWindow::ClassName)) + { + if(state.require[SameVersion]) + { + if(Version(static_cast<uint32>(SendIPC(hwnd, Function::GetVersion))) != Version::Current()) + { + return TRUE; // continue + } + } + if(state.require[SameArchitecture]) + { + if(SendIPC(hwnd, Function::GetArchitecture) != static_cast<int>(mpt::OS::Windows::GetProcessArchitecture())) + { + return TRUE; // continue + } + } + if(state.require[SamePath]) + { + if(SendIPC(hwnd, Function::HasSameBinaryPath, mpt::as_span(theApp.GetInstallBinArchPath().ToWide())) != 1) + { + return TRUE; // continue + } + } + if(state.require[SameSettings]) + { + if(SendIPC(hwnd, Function::HasSameSettingsPath, mpt::as_span(theApp.GetConfigPath().ToWide())) != 1) + { + return TRUE; // continue + } + } + state.result = hwnd; + return TRUE; // continue + //return FALSE; // done + } + } + } + return TRUE; // continue + } + + HWND FindIPCWindow(FlagSet<InstanceRequirements> require) + { + EnumWindowState state; + state.require = require; + if(::EnumWindows(&EnumWindowsProc, reinterpret_cast<LPARAM>(&state)) == 0) + { + return nullptr; + } + return state.result; + } + + + + bool SendToIPC(const std::vector<mpt::PathString> &filenames) + { + HWND ipcWnd = FindIPCWindow(); + if(!ipcWnd) + { + return false; + } + DWORD processID = 0; + GetWindowThreadProcessId(ipcWnd, &processID); + AllowSetForegroundWindow(processID); + SendIPC(ipcWnd, Function::SetWindowForeground); + for(const auto &filename : filenames) + { + if(SendIPC(ipcWnd, Function::Open, mpt::as_span(filename.ToWide())) == 0) + { + return false; + } + } + return true; + } + +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.h b/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.h new file mode 100644 index 00000000..a9e547dc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/IPCWindow.h @@ -0,0 +1,55 @@ +/* +* IPCWindow.h +* ----------- +* Purpose: Hidden window to receive file open commands from another OpenMPT instance +* Notes : (currently none) +* Authors: OpenMPT Devs +* The OpenMPT source code is released under the BSD license. Read LICENSE for more details. +*/ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +namespace IPCWindow +{ + + enum class Function : ULONG + { + Open = 0x01, + SetWindowForeground = 0x02, + GetVersion = 0x03, // returns Version::GewRawVersion() + GetArchitecture = 0x04, // returns mpt::OS::Windows::Architecture + HasSameBinaryPath = 0x05, + HasSameSettingsPath = 0x06 + }; + + void Open(HINSTANCE hInstance); + + void Close(); + + LRESULT SendIPC(HWND ipcWnd, Function function, mpt::const_byte_span data = mpt::const_byte_span()); + + template <typename Tdata> LRESULT SendIPC(HWND ipcWnd, Function function, mpt::span<const Tdata> data) { return SendIPC(ipcWnd, function, mpt::const_byte_span(reinterpret_cast<const std::byte*>(data.data()), data.size() * sizeof(Tdata))); } + + enum InstanceRequirements + { + SamePath = 0x01u, + SameSettings = 0x02u, + SameArchitecture = 0x04u, + SameVersion = 0x08u + }; + MPT_DECLARE_ENUM(InstanceRequirements) + + HWND FindIPCWindow(); + + HWND FindIPCWindow(FlagSet<InstanceRequirements> require); + + // Send file open requests to other OpenMPT instance, if there is one + bool SendToIPC(const std::vector<mpt::PathString> &filenames); + +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Image.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Image.cpp new file mode 100644 index 00000000..fa5e9ced --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Image.cpp @@ -0,0 +1,337 @@ +/* + * Image.cpp + * --------- + * Purpose: Bitmap and Vector image file handling using GDI+. + * 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 "MPTrackUtil.h" +#include "Image.h" +#include "../common/FileReader.h" +#include "../common/ComponentManager.h" + +// GDI+ +#include <atlbase.h> +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#define min(a, b) (((a) < (b)) ? (a) : (b)) +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable : 4458) // declaration of 'x' hides class member +#endif +#include <gdiplus.h> +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif +#undef min +#undef max + + +OPENMPT_NAMESPACE_BEGIN + + +GdiplusRAII::GdiplusRAII() +{ + Gdiplus::GdiplusStartupInput gdiplusStartupInput; + Gdiplus::GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL); +} + + +GdiplusRAII::~GdiplusRAII() +{ + Gdiplus::GdiplusShutdown(gdiplusToken); + gdiplusToken = 0; +} + + +RawGDIDIB::RawGDIDIB(uint32 width, uint32 height) + : width(width) + , height(height) + , pixels(width * height) +{ + MPT_ASSERT(width > 0); + MPT_ASSERT(height > 0); +} + + +namespace GDIP +{ + + +static CComPtr<IStream> GetStream(mpt::const_byte_span data) +{ + CComPtr<IStream> stream; +#if (_WIN32_WINNT >= _WIN32_WINNT_VISTA) + stream.Attach(SHCreateMemStream(mpt::byte_cast<const unsigned char *>(data.data()), mpt::saturate_cast<UINT>(data.size()))); +#else + HGLOBAL hGlobal = GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, data.size()); + if(hGlobal == NULL) + { + throw bad_image(); + } + void * mem = GlobalLock(hGlobal); + if(!mem) + { + hGlobal = GlobalFree(hGlobal); + throw bad_image(); + } + std::memcpy(mem, data.data(), data.size()); + GlobalUnlock(hGlobal); + if(CreateStreamOnHGlobal(hGlobal, TRUE, &stream) != S_OK) + { + hGlobal = GlobalFree(hGlobal); + throw bad_image(); + } + hGlobal = NULL; +#endif + if(!stream) + { + throw bad_image(); + } + return stream; +} + + +std::unique_ptr<Gdiplus::Bitmap> LoadPixelImage(mpt::const_byte_span file) +{ + CComPtr<IStream> stream = GetStream(file); + std::unique_ptr<Gdiplus::Bitmap> result = std::make_unique<Gdiplus::Bitmap>(stream, FALSE); + if(result->GetLastStatus() != Gdiplus::Ok) + { + throw bad_image(); + } + if(result->GetWidth() == 0 || result->GetHeight() == 0) + { + throw bad_image(); + } + return result; +} + + +std::unique_ptr<Gdiplus::Bitmap> LoadPixelImage(FileReader file) +{ + FileReader::PinnedView view = file.GetPinnedView(); + return LoadPixelImage(view.span()); +} + + +std::unique_ptr<Gdiplus::Metafile> LoadVectorImage(mpt::const_byte_span file) +{ + CComPtr<IStream> stream = GetStream(file); + std::unique_ptr<Gdiplus::Metafile> result = std::make_unique<Gdiplus::Metafile>(stream); + if(result->GetLastStatus() != Gdiplus::Ok) + { + throw bad_image(); + } + if(result->GetWidth() == 0 || result->GetHeight() == 0) + { + throw bad_image(); + } + return result; +} + + +std::unique_ptr<Gdiplus::Metafile> LoadVectorImage(FileReader file) +{ + FileReader::PinnedView view = file.GetPinnedView(); + return LoadVectorImage(view.span()); +} + + +static std::unique_ptr<Gdiplus::Bitmap> DoResize(Gdiplus::Image &src, double scaling, int spriteWidth, int spriteHeight) +{ + const int width = src.GetWidth(), height = src.GetHeight(); + int newWidth = 0, newHeight = 0, newSpriteWidth = 0, newSpriteHeight = 0; + + if(spriteWidth <= 0 || spriteHeight <= 0) + { + newWidth = mpt::saturate_round<int>(width * scaling); + newHeight = mpt::saturate_round<int>(height * scaling); + } else + { + // Sprite mode: Source images consists of several sprites / icons that should be scaled individually + newSpriteWidth = mpt::saturate_round<int>(spriteWidth * scaling); + newSpriteHeight = mpt::saturate_round<int>(spriteHeight * scaling); + newWidth = width * newSpriteWidth / spriteWidth; + newHeight = height * newSpriteHeight / spriteHeight; + } + + std::unique_ptr<Gdiplus::Bitmap> resizedImage = std::make_unique<Gdiplus::Bitmap>(newWidth, newHeight, PixelFormat32bppARGB); + std::unique_ptr<Gdiplus::Graphics> resizedGraphics(Gdiplus::Graphics::FromImage(resizedImage.get())); + if(scaling >= 1.5) + { + // Prefer crisp look on real high-DPI devices + resizedGraphics->SetInterpolationMode(Gdiplus::InterpolationModeNearestNeighbor); + resizedGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHalf); + resizedGraphics->SetSmoothingMode(Gdiplus::SmoothingModeNone); + } else + { + resizedGraphics->SetInterpolationMode(Gdiplus::InterpolationModeHighQualityBicubic); + resizedGraphics->SetPixelOffsetMode(Gdiplus::PixelOffsetModeHighQuality); + resizedGraphics->SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); + } + if(spriteWidth <= 0 || spriteHeight <= 0) + { + resizedGraphics->DrawImage(&src, 0, 0, newWidth, newHeight); + } else + { + // Draw each source sprite individually into separate image to avoid neighbouring source sprites bleeding in + std::unique_ptr<Gdiplus::Bitmap> spriteImage = std::make_unique<Gdiplus::Bitmap>(spriteWidth, spriteHeight, PixelFormat32bppARGB); + std::unique_ptr<Gdiplus::Graphics> spriteGraphics(Gdiplus::Graphics::FromImage(spriteImage.get())); + + for(int srcY = 0, destY = 0; srcY < height; srcY += spriteHeight, destY += newSpriteHeight) + { + for(int srcX = 0, destX = 0; srcX < width; srcX += spriteWidth, destX += newSpriteWidth) + { + spriteGraphics->Clear({0, 0, 0, 0}); + spriteGraphics->DrawImage(&src, Gdiplus::Rect(0, 0, spriteWidth, spriteHeight), srcX, srcY, spriteWidth, spriteHeight, Gdiplus::UnitPixel); + resizedGraphics->DrawImage(spriteImage.get(), destX, destY, newSpriteWidth, newSpriteHeight); + } + } + } + return resizedImage; +} + + +std::unique_ptr<Gdiplus::Image> ResizeImage(Gdiplus::Image &src, double scaling, int spriteWidth, int spriteHeight) +{ + return DoResize(src, scaling, spriteWidth, spriteHeight); +} + + +std::unique_ptr<Gdiplus::Bitmap> ResizeImage(Gdiplus::Bitmap &src, double scaling, int spriteWidth, int spriteHeight) +{ + return DoResize(src, scaling, spriteWidth, spriteHeight); +} + + +} // namespace GDIP + + +std::unique_ptr<RawGDIDIB> ToRawGDIDIB(Gdiplus::Bitmap &bitmap) +{ + Gdiplus::BitmapData bitmapData; + Gdiplus::Rect rect{Gdiplus::Point{0, 0}, Gdiplus::Size{static_cast<INT>(bitmap.GetWidth()), static_cast<INT>(bitmap.GetHeight())}}; + std::unique_ptr<RawGDIDIB> result = std::make_unique<RawGDIDIB>(bitmap.GetWidth(), bitmap.GetHeight()); + if(bitmap.LockBits(&rect, Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bitmapData) != Gdiplus::Ok) + { + throw bad_image(); + } + RawGDIDIB::Pixel *dst = result->Pixels().data(); + for(uint32 y = 0; y < result->Height(); ++y) + { + const GDIP::Pixel *src = GDIP::GetScanline(bitmapData, y); + for(uint32 x = 0; x < result->Width(); ++x) + { + *dst = GDIP::ToRawGDIDIB(*src); + src++; + dst++; + } + } + bitmap.UnlockBits(&bitmapData); + return result; +} + + +std::unique_ptr<RawGDIDIB> LoadPixelImage(mpt::const_byte_span file, double scaling, int spriteWidth, int spriteHeight) +{ + auto bitmap = GDIP::LoadPixelImage(file); + if(scaling != 1.0) + bitmap = GDIP::ResizeImage(*bitmap, scaling, spriteWidth, spriteHeight); + return ToRawGDIDIB(*bitmap); +} + + +std::unique_ptr<RawGDIDIB> LoadPixelImage(FileReader file, double scaling, int spriteWidth, int spriteHeight) +{ + auto bitmap = GDIP::LoadPixelImage(file); + if(scaling != 1.0) + bitmap = GDIP::ResizeImage(*bitmap, scaling, spriteWidth, spriteHeight); + return ToRawGDIDIB(*bitmap); +} + + +// Create a DIB for the current device from our PNG. +bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, const RawGDIDIB &src) +{ + BITMAPINFOHEADER bi; + MemsetZero(bi); + bi.biSize = sizeof(BITMAPINFOHEADER); + bi.biWidth = src.Width(); + bi.biHeight = -static_cast<LONG>(src.Height()); + bi.biPlanes = 1; + bi.biBitCount = 32; + bi.biCompression = BI_RGB; + bi.biSizeImage = src.Width() * src.Height() * 4; + if(!dst.CreateCompatibleBitmap(&dc, src.Width(), src.Height())) + { + return false; + } + if(!SetDIBits(dc.GetSafeHdc(), dst, 0, src.Height(), src.Pixels().data(), reinterpret_cast<BITMAPINFO *>(&bi), DIB_RGB_COLORS)) + { + return false; + } + return true; +} + + +bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, Gdiplus::Image &src) +{ + if(!dst.CreateCompatibleBitmap(&dc, src.GetWidth(), src.GetHeight())) + { + return false; + } + CDC memdc; + if(!memdc.CreateCompatibleDC(&dc)) + { + return false; + } + memdc.SelectObject(dst); + Gdiplus::Graphics gfx(memdc); + if(gfx.DrawImage(&src, 0, 0) != Gdiplus::Ok) + { + return false; + } + return true; +} + + + +bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, mpt::const_byte_span file) +{ + try + { + std::unique_ptr<Gdiplus::Bitmap> pBitmap = GDIP::LoadPixelImage(file); + if(!CopyToCompatibleBitmap(dst, dc, *pBitmap)) + { + return false; + } + } catch(...) + { + return false; + } + return true; +} + + +bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, FileReader file) +{ + try + { + std::unique_ptr<Gdiplus::Bitmap> pBitmap = GDIP::LoadPixelImage(file); + if(!CopyToCompatibleBitmap(dst, dc, *pBitmap)) + { + return false; + } + } catch(...) + { + return false; + } + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Image.h b/Src/external_dependencies/openmpt-trunk/mptrack/Image.h new file mode 100644 index 00000000..8ac3c788 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Image.h @@ -0,0 +1,137 @@ +/* + * Image.h + * ------- + * Purpose: Bitmap and Vector image file handling. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/FileReaderFwd.h" + +// GDI+ +namespace Gdiplus { +#include <gdipluspixelformats.h> +class Image; +class Bitmap; +class Metafile; +} + + +OPENMPT_NAMESPACE_BEGIN + + +class bad_image : public std::runtime_error { public: bad_image() : std::runtime_error("") { } }; + + +class RawGDIDIB +{ +public: + struct Pixel + { + // Component order must be compatible with Microsoft DIBs! + uint8 b; + uint8 g; + uint8 r; + uint8 a; + constexpr Pixel() noexcept + : b(0), g(0), r(0), a(0) {} + constexpr Pixel(uint8 r, uint8 g, uint8 b, uint8 a) noexcept + : b(b), g(g), r(r), a(a) {} + constexpr Pixel(COLORREF color) noexcept + : b(GetBValue(color)), g(GetGValue(color)), r(GetRValue(color)), a(0) {} + }; +private: + uint32 width; + uint32 height; + std::vector<Pixel> pixels; +public: + RawGDIDIB(uint32 width, uint32 height); +public: + constexpr uint32 Width() const noexcept { return width; } + constexpr uint32 Height() const noexcept { return height; } + MPT_FORCEINLINE Pixel &operator()(uint32 x, uint32 y) noexcept { return pixels[y * width + x]; } + MPT_FORCEINLINE const Pixel &operator()(uint32 x, uint32 y) const noexcept { return pixels[y * width + x]; } + std::vector<Pixel> &Pixels() { return pixels; } + const std::vector<Pixel> &Pixels() const { return pixels; } +}; + + +class GdiplusRAII +{ +private: + ULONG_PTR gdiplusToken = 0; +public: + GdiplusRAII(); + ~GdiplusRAII(); +}; + + +namespace GDIP +{ + + std::unique_ptr<Gdiplus::Bitmap> LoadPixelImage(mpt::const_byte_span file); + std::unique_ptr<Gdiplus::Bitmap> LoadPixelImage(FileReader file); + + std::unique_ptr<Gdiplus::Metafile> LoadVectorImage(mpt::const_byte_span file); + std::unique_ptr<Gdiplus::Metafile> LoadVectorImage(FileReader file); + + std::unique_ptr<Gdiplus::Image> ResizeImage(Gdiplus::Image &src, double scaling, int spriteWidth = 0, int spriteHeight = 0); + std::unique_ptr<Gdiplus::Bitmap> ResizeImage(Gdiplus::Bitmap &src, double scaling, int spriteWidth = 0, int spriteHeight = 0); + + using Pixel = Gdiplus::ARGB; + + template <typename TBitmapData> + inline Pixel * GetScanline(const TBitmapData &bitmapData, std::size_t y) noexcept + { + if(bitmapData.Stride >= 0) + { + return reinterpret_cast<Pixel*>(mpt::void_cast<void*>(mpt::void_cast<std::byte*>(bitmapData.Scan0) + y * bitmapData.Stride)); + } else + { + return reinterpret_cast<Pixel*>(mpt::void_cast<void*>(mpt::void_cast<std::byte*>(bitmapData.Scan0) + (bitmapData.Height - 1 - y) * (-bitmapData.Stride))); + } + } + + constexpr Pixel AsPixel(uint8 r, uint8 g, uint8 b, uint8 a) noexcept + { + return Pixel(0) + | (static_cast<Pixel>(r) << RED_SHIFT) + | (static_cast<Pixel>(g) << GREEN_SHIFT) + | (static_cast<Pixel>(b) << BLUE_SHIFT) + | (static_cast<Pixel>(a) << ALPHA_SHIFT) + ; + } + + constexpr uint8 R(Pixel p) noexcept { return static_cast<uint8>(p >> RED_SHIFT); } + constexpr uint8 G(Pixel p) noexcept { return static_cast<uint8>(p >> GREEN_SHIFT); } + constexpr uint8 B(Pixel p) noexcept { return static_cast<uint8>(p >> BLUE_SHIFT); } + constexpr uint8 A(Pixel p) noexcept { return static_cast<uint8>(p >> ALPHA_SHIFT); } + + constexpr RawGDIDIB::Pixel ToRawGDIDIB(Pixel p) noexcept + { + return RawGDIDIB::Pixel(GDIP::R(p), GDIP::G(p), GDIP::B(p), GDIP::A(p)); + } + +} // namespace GDIP + + +std::unique_ptr<RawGDIDIB> ToRawGDIDIB(Gdiplus::Bitmap &bitmap); + +bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, const RawGDIDIB &src); + +bool CopyToCompatibleBitmap(CBitmap &dst, CDC &dc, Gdiplus::Image &src); + +std::unique_ptr<RawGDIDIB> LoadPixelImage(mpt::const_byte_span file, double scaling = 1.0, int spriteWidth = 0, int spriteHeight = 0); +std::unique_ptr<RawGDIDIB> LoadPixelImage(FileReader file, double scaling = 1.0, int spriteWidth = 0, int spriteHeight = 0); + +bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, mpt::const_byte_span file); +bool LoadCompatibleBitmapFromPixelImage(CBitmap &dst, CDC &dc, FileReader file); + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ImageLists.h b/Src/external_dependencies/openmpt-trunk/mptrack/ImageLists.h new file mode 100644 index 00000000..a52d31dd --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ImageLists.h @@ -0,0 +1,141 @@ +/* + * ImageLists.h + * ------------ + * Purpose: Enums for image lists + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +// Image List index +enum +{ + IMAGE_COMMENTS=0, + IMAGE_PATTERNS, + IMAGE_OPLINSTR = IMAGE_PATTERNS, + IMAGE_SAMPLES, + IMAGE_INSTRUMENTS, + IMAGE_PLUGININSTRUMENT = IMAGE_INSTRUMENTS, + IMAGE_GENERAL, + IMAGE_FOLDER, + IMAGE_OPENFOLDER, + IMAGE_PARTITION, + IMAGE_NOSAMPLE, + IMAGE_FOLDERPARENT, + IMAGE_FOLDERSONG, + IMAGE_DIRECTX, + IMAGE_WAVEOUT, + IMAGE_EFFECTPLUGIN = IMAGE_WAVEOUT, + IMAGE_ASIO, + IMAGE_CHIP, + IMAGE_SAMPLEMUTE, + IMAGE_INSTRMUTE, + IMAGE_SAMPLEACTIVE, + IMAGE_INSTRACTIVE, + IMAGE_NOPLUGIN, + IMAGE_TUX, + IMAGE_OPLINSTRACTIVE, + IMAGE_OPLINSTRMUTE, + IMAGE_EXTSAMPLEMISSING, + IMAGE_EXTSAMPLE, + IMAGE_EXTSAMPLEACTIVE, + IMAGE_EXTSAMPLEMUTE, + IMGLIST_NUMIMAGES +}; + + +// Toolbar Image List index +enum +{ + TIMAGE_PATTERN_NEW=0, + TIMAGE_PATTERN_STOP, + TIMAGE_PATTERN_PLAY, + TIMAGE_PATTERN_RESTART, + TIMAGE_PATTERN_RECORD, + TIMAGE_SAMPLE_FIXLOOP, + TIMAGE_SAMPLE_NEW, + TIMAGE_INSTR_NEW, + TIMAGE_SAMPLE_NORMALIZE, + TIMAGE_SAMPLE_AMPLIFY, + TIMAGE_SAMPLE_RESAMPLE, + TIMAGE_SAMPLE_REVERSE, + TIMAGE_OPEN, + TIMAGE_SAVE, + TIMAGE_PREVIEW, + TIMAGE_SAMPLE_AUTOTUNE, + TIMAGE_PATTERN_VUMETERS, + TIMAGE_MACROEDITOR, + TIMAGE_CHORDEDITOR, + TIMAGE_PATTERN_PROPERTIES, + TIMAGE_PATTERN_EXPAND, + TIMAGE_PATTERN_SHRINK, + TIMAGE_SAMPLE_SILENCE, + TIMAGE_PATTERN_OVERFLOWPASTE, + TIMAGE_UNDO, + TIMAGE_REDO, + TIMAGE_PATTERN_PLAYROW, + TIMAGE_SAMPLE_DOWNSAMPLE, + TIMAGE_PATTERN_DETAIL_LO, + TIMAGE_PATTERN_DETAIL_MED, + TIMAGE_PATTERN_DETAIL_HI, + TIMAGE_PATTERN_PLUGINS, + TIMAGE_CHANNELMANAGER, + TIMAGE_SAMPLE_INVERT, + TIMAGE_SAMPLE_UNSIGN, + TIMAGE_SAMPLE_DCOFFSET, + TIMAGE_SAMPLE_STEREOSEP, + PATTERNIMG_NUMIMAGES +}; + + +// Sample editor toolbar image list index +enum +{ + SIMAGE_CHECKED = 0, + SIMAGE_ZOOMUP, + SIMAGE_ZOOMDOWN, + SIMAGE_DRAW, + SIMAGE_RESIZE, + SIMAGE_GENERATE, + SIMAGE_GRID, + SAMPLEIMG_NUMIMAGES +}; + + +// Instrument editor toolbar image list index +enum +{ + IIMAGE_CHECKED = 0, + IIMAGE_VOLENV, + IIMAGE_PANENV, + IIMAGE_PITCHENV, + IIMAGE_NOPITCHENV, + IIMAGE_LOOP, + IIMAGE_SUSTAIN, + IIMAGE_CARRY, + IIMAGE_NOCARRY, + IIMAGE_VOLSWITCH, + IIMAGE_PANSWITCH, + IIMAGE_PITCHSWITCH, + IIMAGE_FILTERSWITCH, + IIMAGE_NOPITCHSWITCH, + IIMAGE_NOFILTERSWITCH, + IIMAGE_SAMPLEMAP, + IIMAGE_GRID, + IIMAGE_ZOOMIN, + IIMAGE_NOZOOMIN, + IIMAGE_ZOOMOUT, + IIMAGE_NOZOOMOUT, + IIMAGE_SAVE, + IIMAGE_LOAD, + ENVIMG_NUMIMAGES +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.cpp new file mode 100644 index 00000000..9a0d2d54 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.cpp @@ -0,0 +1,632 @@ +/* + * InputHandler.cpp + * ---------------- + * Purpose: Implementation of keyboard input handling, keymap loading, ... + * 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 "CommandSet.h" +#include "InputHandler.h" +#include "resource.h" +#include "Mainfrm.h" +#include "../soundlib/MIDIEvents.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#define TRANSITIONBIT 0x8000 +#define REPEATBIT 0x4000 + +CInputHandler::CInputHandler(CWnd *mainframe) +{ + m_pMainFrm = mainframe; + + //Init CommandSet and Load defaults + m_activeCommandSet = std::make_unique<CCommandSet>(); + m_lastCommands.fill(kcNull); + + mpt::PathString sDefaultPath = theApp.GetConfigPath() + P_("Keybindings.mkb"); + + const bool bNoExistingKbdFileSetting = TrackerSettings::Instance().m_szKbdFile.empty(); + + // 1. Try to load keybindings from the path saved in the settings. + // 2. If the setting doesn't exist or the loading fails, try to load from default location. + // 3. If neither one of these worked, load default keybindings from resources. + // 4. If there were no keybinding setting already, create a keybinding file to default location + // and set its path to settings. + + if (bNoExistingKbdFileSetting || !(m_activeCommandSet->LoadFile(TrackerSettings::Instance().m_szKbdFile))) + { + if (bNoExistingKbdFileSetting) + TrackerSettings::Instance().m_szKbdFile = sDefaultPath; + bool bSuccess = false; + if (sDefaultPath.IsFile()) + bSuccess = m_activeCommandSet->LoadFile(sDefaultPath); + if (!bSuccess) + { + // Load keybindings from resources. + MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("Loading keybindings from resources\n")); + bSuccess = m_activeCommandSet->LoadDefaultKeymap(); + if (bSuccess && bNoExistingKbdFileSetting) + { + m_activeCommandSet->SaveFile(TrackerSettings::Instance().m_szKbdFile); + } + } + if (!bSuccess) + ErrorBox(IDS_UNABLE_TO_LOAD_KEYBINDINGS); + } + // We will only overwrite the default Keybindings.mkb file from now on. + TrackerSettings::Instance().m_szKbdFile = sDefaultPath; + + //Get Keymap + m_activeCommandSet->GenKeyMap(m_keyMap); + SetupSpecialKeyInterception(); // Feature: use Windows keys as modifier keys, intercept special keys +} + + +CommandID CInputHandler::SendCommands(CWnd *wnd, const KeyMapRange &cmd) +{ + CommandID executeCommand = kcNull; + if(wnd != nullptr) + { + // Some commands (e.g. open/close/document switching) may invalidate the key map and thus its iterators. + // To avoid this problem, copy over the elements we are interested in before sending commands. + std::vector<KeyMap::value_type> commands; + commands.reserve(std::distance(cmd.first, cmd.second)); + for(auto i = cmd.first; i != cmd.second; i++) + { + commands.push_back(*i); + } + for(const auto &i : commands) + { + m_lastCommands[m_lastCommandPos] = i.second; + m_lastCommandPos = (m_lastCommandPos + 1) % m_lastCommands.size(); + if(wnd->SendMessage(WM_MOD_KEYCOMMAND, i.second, i.first.AsLPARAM()) != kcNull) + { + // Command was handled, no need to let the OS handle the key + executeCommand = i.second; + } + } + } + return executeCommand; +} + + +CommandID CInputHandler::GeneralKeyEvent(InputTargetContext context, int code, WPARAM wParam, LPARAM lParam) +{ + KeyMapRange cmd = { m_keyMap.end(), m_keyMap.end() }; + KeyEventType keyEventType; + + if(code == HC_ACTION) + { + //Get the KeyEventType (key up, key down, key repeat) + DWORD scancode = static_cast<LONG>(lParam) >> 16; + if((scancode & 0xC000) == 0xC000) + { + keyEventType = kKeyEventUp; + } else if((scancode & 0xC000) == 0x0000) + { + keyEventType = kKeyEventDown; + } else + { + keyEventType = kKeyEventRepeat; + } + + // Catch modifier change (ctrl, alt, shift) - Only check on keyDown or keyUp. + // NB: we want to catch modifiers even when the input handler is locked + if(keyEventType == kKeyEventUp || keyEventType == kKeyEventDown) + { + scancode = (static_cast<LONG>(lParam) >> 16) & 0x1FF; + CatchModifierChange(wParam, keyEventType, scancode); + } + + if(!InterceptSpecialKeys(static_cast<UINT>(wParam), static_cast<LONG>(lParam), true) && !IsBypassed()) + { + // only execute command when the input handler is not locked + // and the input is not a consequence of special key interception. + cmd = m_keyMap.equal_range(KeyCombination(context, m_modifierMask, static_cast<UINT>(wParam), keyEventType)); + } + } + if(code == HC_MIDI) + { + cmd = m_keyMap.equal_range(KeyCombination(context, ModMidi, static_cast<UINT>(wParam), static_cast<KeyEventType>(lParam))); + } + + return SendCommands(m_pMainFrm, cmd); +} + + +CommandID CInputHandler::KeyEvent(InputTargetContext context, UINT &nChar, UINT &/*nRepCnt*/, UINT &nFlags, KeyEventType keyEventType, CWnd *pSourceWnd) +{ + if(InterceptSpecialKeys(nChar, nFlags, false)) + return kcDummyShortcut; + KeyMapRange cmd = m_keyMap.equal_range(KeyCombination(context, m_modifierMask, nChar, keyEventType)); + + if(pSourceWnd == nullptr) + pSourceWnd = m_pMainFrm; // By default, send command message to main frame. + return SendCommands(pSourceWnd, cmd); +} + + +// Feature: use Windows keys as modifier keys, intercept special keys +bool CInputHandler::InterceptSpecialKeys(UINT nChar, UINT nFlags, bool generateMsg) +{ + KeyEventType keyEventType = GetKeyEventType(HIWORD(nFlags)); + enum { VK_NonExistentKey = VK_F24+1 }; + + if(nChar == VK_NonExistentKey) + { + return true; + } else if(m_bInterceptWindowsKeys && (nChar == VK_LWIN || nChar == VK_RWIN)) + { + if(keyEventType == kKeyEventDown) + { + INPUT inp[2]; + inp[0].type = inp[1].type = INPUT_KEYBOARD; + inp[0].ki.time = inp[1].ki.time = 0; + inp[0].ki.dwExtraInfo = inp[1].ki.dwExtraInfo = 0; + inp[0].ki.wVk = inp[1].ki.wVk = VK_NonExistentKey; + inp[0].ki.wScan = inp[1].ki.wScan = 0; + inp[0].ki.dwFlags = 0; + inp[1].ki.dwFlags = KEYEVENTF_KEYUP; + SendInput(2, inp, sizeof(INPUT)); + } + } + + if((nChar == VK_NUMLOCK && m_bInterceptNumLock) + || (nChar == VK_CAPITAL && m_bInterceptCapsLock) + || (nChar == VK_SCROLL && m_bInterceptScrollLock)) + { + if(GetMessageExtraInfo() == 0xC0FFEE) + { + SetMessageExtraInfo(0); + return true; + } else if(keyEventType == kKeyEventDown && generateMsg) + { + // Prevent keys from lighting up by simulating a second press. + INPUT inp[2]; + inp[0].type = inp[1].type = INPUT_KEYBOARD; + inp[0].ki.time = inp[1].ki.time = 0; + inp[0].ki.dwExtraInfo = inp[1].ki.dwExtraInfo = 0xC0FFEE; + inp[0].ki.wVk = inp[1].ki.wVk = static_cast<WORD>(nChar); + inp[0].ki.wScan = inp[1].ki.wScan = 0; + inp[0].ki.dwFlags = KEYEVENTF_KEYUP; + inp[1].ki.dwFlags = 0; + SendInput(2, inp, sizeof(INPUT)); + } + } + return false; +}; + + +void CInputHandler::SetupSpecialKeyInterception() +{ + m_bInterceptWindowsKeys = m_bInterceptNumLock = m_bInterceptCapsLock = m_bInterceptScrollLock = false; + for(const auto &i : m_keyMap) + { + ASSERT(i.second != kcNull); + if(i.first.Modifier() == ModWin) + m_bInterceptWindowsKeys = true; + if(i.first.KeyCode() == VK_NUMLOCK) + m_bInterceptNumLock = true; + if(i.first.KeyCode() == VK_CAPITAL) + m_bInterceptCapsLock = true; + if(i.first.KeyCode() == VK_SCROLL) + m_bInterceptScrollLock = true; + } +}; + + +//Deal with Modifier keypresses. Private surouting used above. +bool CInputHandler::CatchModifierChange(WPARAM wParam, KeyEventType keyEventType, int scancode) +{ + FlagSet<Modifiers> modifierMask = ModNone; + // Scancode for right modifier keys should have bit 8 set, but Right Shift is actually 0x36. + const bool isRight = ((scancode & 0x100) || scancode == 0x36) && TrackerSettings::Instance().MiscDistinguishModifiers; + switch(wParam) + { + case VK_CONTROL: + modifierMask.set(isRight ? ModRCtrl : ModCtrl); + break; + case VK_SHIFT: + modifierMask.set(isRight ? ModRShift : ModShift); + break; + case VK_MENU: + modifierMask.set(isRight ? ModRAlt : ModAlt); + break; + case VK_LWIN: case VK_RWIN: // Feature: use Windows keys as modifier keys + modifierMask.set(ModWin); + break; + } + + if (modifierMask) // This keypress just changed the modifier mask + { + if (keyEventType == kKeyEventDown) + { + m_modifierMask.set(modifierMask); + // Right Alt is registered as Ctrl+Alt. + // Left Ctrl + Right Alt seems like a pretty difficult to use key combination anyway, so just ignore Ctrl. + if(scancode == 0x138) + m_modifierMask.reset(ModCtrl); +#ifdef _DEBUG + LogModifiers(); +#endif + } else if (keyEventType == kKeyEventUp) + m_modifierMask.reset(modifierMask); + + return true; + } + + return false; +} + + +// Translate MIDI messages to shortcut commands +CommandID CInputHandler::HandleMIDIMessage(InputTargetContext context, uint32 message) +{ + auto byte1 = MIDIEvents::GetDataByte1FromEvent(message), byte2 = MIDIEvents::GetDataByte2FromEvent(message); + switch(MIDIEvents::GetTypeFromEvent(message)) + { + case MIDIEvents::evControllerChange: + if(byte2 != 0) + { + // Only capture MIDI CCs for now. Some controllers constantly send some MIDI CCs with value 0 + // (e.g. the Roland D-50 sends CC123 whenenver all notes have been released), so we will ignore those. + return GeneralKeyEvent(context, HC_MIDI, byte1, kKeyEventDown); + } + break; + + case MIDIEvents::evNoteOff: + byte2 = 0; + [[fallthrough]]; + case MIDIEvents::evNoteOn: + if(byte2 != 0) + { + return GeneralKeyEvent(context, HC_MIDI, byte1 | 0x80, kKeyEventDown); + } else + { + // If the key-down triggered a note, we still want that note to be stopped. So we always pretend that no key was assigned to this event + GeneralKeyEvent(context, HC_MIDI, byte1 | 0x80, kKeyEventUp); + } + break; + } + + return kcNull; +} + + +int CInputHandler::GetKeyListSize(CommandID cmd) const +{ + return m_activeCommandSet->GetKeyListSize(cmd); +} + + +//----------------------- Misc + + +void CInputHandler::LogModifiers() +{ + MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("----------------------------------\n")); + MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModCtrl] ? U_("Ctrl On") : U_("Ctrl --")); + MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModShift] ? U_("\tShft On") : U_("\tShft --")); + MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModAlt] ? U_("\tAlt On") : U_("\tAlt --")); + MPT_LOG_GLOBAL(LogDebug, "InputHandler", m_modifierMask[ModWin] ? U_("\tWin On\n") : U_("\tWin --\n")); +} + + +KeyEventType CInputHandler::GetKeyEventType(UINT nFlags) +{ + if (nFlags & TRANSITIONBIT) + { + // Key released + return kKeyEventUp; + } else if (nFlags & REPEATBIT) + { + // Key repeated + return kKeyEventRepeat; + } else + { + // New key down + return kKeyEventDown; + } +} + + +bool CInputHandler::SelectionPressed() const +{ + int nSelectionKeys = m_activeCommandSet->GetKeyListSize(kcSelect); + KeyCombination key; + + for (int k=0; k<nSelectionKeys; k++) + { + key = m_activeCommandSet->GetKey(kcSelect, k); + if (m_modifierMask & key.Modifier()) + { + return true; + } + } + return false; +} + + +bool CInputHandler::ShiftPressed() const +{ + return m_modifierMask[ModShift | ModRShift]; +} + + +bool CInputHandler::CtrlPressed() const +{ + return m_modifierMask[ModCtrl | ModRCtrl]; +} + + +bool CInputHandler::AltPressed() const +{ + return m_modifierMask[ModAlt | ModRAlt]; +} + + +void CInputHandler::Bypass(bool b) +{ + if(b) + m_bypassCount++; + else + m_bypassCount--; + ASSERT(m_bypassCount >= 0); +} + + +bool CInputHandler::IsBypassed() const +{ + return m_bypassCount > 0; +} + + +FlagSet<Modifiers> CInputHandler::GetModifierMask() const +{ + return m_modifierMask; +} + + +void CInputHandler::SetModifierMask(FlagSet<Modifiers> mask) +{ + m_modifierMask = mask; +} + + +CString CInputHandler::GetKeyTextFromCommand(CommandID c, const TCHAR *prependText) const +{ + CString s; + if(prependText != nullptr) + { + s = prependText; + s.AppendChar(_T('\t')); + } + s += m_activeCommandSet->GetKeyTextFromCommand(c, 0); + return s; +} + + +CString CInputHandler::GetMenuText(UINT id) const +{ + static constexpr std::tuple<UINT, CommandID, const TCHAR *> MenuItems[] = + { + { ID_FILE_NEW, kcFileNew, _T("&New") }, + { ID_FILE_OPEN, kcFileOpen, _T("&Open...") }, + { ID_FILE_OPENTEMPLATE, kcNull, _T("Open &Template") }, + { ID_FILE_CLOSE, kcFileClose, _T("&Close") }, + { ID_FILE_CLOSEALL, kcFileCloseAll, _T("C&lose All") }, + { ID_FILE_APPENDMODULE, kcFileAppend, _T("Appen&d Module...") }, + { ID_FILE_SAVE, kcFileSave, _T("&Save") }, + { ID_FILE_SAVE_AS, kcFileSaveAs, _T("Save &As...") }, + { ID_FILE_SAVE_COPY, kcFileSaveCopy, _T("Save Cop&y...") }, + { ID_FILE_SAVEASTEMPLATE, kcFileSaveTemplate, _T("Sa&ve as Template") }, + { ID_FILE_SAVEASWAVE, kcFileSaveAsWave, _T("Stream Export (&WAV, FLAC, MP3, etc.)...") }, + { ID_FILE_SAVEMIDI, kcFileSaveMidi, _T("Export as M&IDI...") }, + { ID_FILE_SAVEOPL, kcFileSaveOPL, _T("Export O&PL Register Dump...") }, + { ID_FILE_SAVECOMPAT, kcFileExportCompat, _T("Compatibility &Export...") }, + { ID_IMPORT_MIDILIB, kcFileImportMidiLib, _T("Import &MIDI Library...") }, + { ID_ADD_SOUNDBANK, kcFileAddSoundBank, _T("Add Sound &Bank...") }, + + { ID_PLAYER_PLAY, kcPlayPauseSong, _T("Pause / &Resume") }, + { ID_PLAYER_PLAYFROMSTART, kcPlaySongFromStart, _T("&Play from Start") }, + { ID_PLAYER_STOP, kcStopSong, _T("&Stop") }, + { ID_PLAYER_PAUSE, kcPauseSong, _T("P&ause") }, + { ID_MIDI_RECORD, kcMidiRecord, _T("&MIDI Record") }, + { ID_ESTIMATESONGLENGTH, kcEstimateSongLength, _T("&Estimate Song Length") }, + { ID_APPROX_BPM, kcApproxRealBPM, _T("Approximate Real &BPM") }, + + { ID_EDIT_UNDO, kcEditUndo, _T("&Undo") }, + { ID_EDIT_REDO, kcEditRedo, _T("&Redo") }, + { ID_EDIT_CUT, kcEditCut, _T("Cu&t") }, + { ID_EDIT_COPY, kcEditCopy, _T("&Copy") }, + { ID_EDIT_PASTE, kcEditPaste, _T("&Paste") }, + { ID_EDIT_SELECT_ALL, kcEditSelectAll, _T("Select &All") }, + { ID_EDIT_CLEANUP, kcNull, _T("C&leanup") }, + { ID_EDIT_FIND, kcEditFind, _T("&Find / Replace") }, + { ID_EDIT_FINDNEXT, kcEditFindNext, _T("Find &Next") }, + { ID_EDIT_GOTO_MENU, kcPatternGoto, _T("&Goto") }, + { ID_EDIT_SPLITKEYBOARDSETTINGS, kcShowSplitKeyboardSettings, _T("Split &Keyboard Settings") }, + // "Paste Special" sub menu + { ID_EDIT_PASTE_SPECIAL, kcEditMixPaste, _T("&Mix Paste") }, + { ID_EDIT_MIXPASTE_ITSTYLE, kcEditMixPasteITStyle, _T("M&ix Paste (IT Style)") }, + { ID_EDIT_PASTEFLOOD, kcEditPasteFlood, _T("Paste Fl&ood") }, + { ID_EDIT_PUSHFORWARDPASTE, kcEditPushForwardPaste, _T("&Push Forward Paste (Insert)") }, + + { ID_VIEW_GLOBALS, kcViewGeneral, _T("&General") }, + { ID_VIEW_SAMPLES, kcViewSamples, _T("&Samples") }, + { ID_VIEW_PATTERNS, kcViewPattern, _T("&Patterns") }, + { ID_VIEW_INSTRUMENTS, kcViewInstruments, _T("&Instruments") }, + { ID_VIEW_COMMENTS, kcViewComments, _T("&Comments") }, + { ID_VIEW_OPTIONS, kcViewOptions, _T("S&etup") }, + { ID_VIEW_TOOLBAR, kcViewMain, _T("&Main") }, + { IDD_TREEVIEW, kcViewTree, _T("&Tree") }, + { ID_PLUGIN_SETUP, kcViewAddPlugin, _T("Pl&ugin Manager") }, + { ID_CHANNEL_MANAGER, kcViewChannelManager, _T("Ch&annel Manager") }, + { ID_CLIPBOARD_MANAGER, kcToggleClipboardManager, _T("C&lipboard Manager") }, + { ID_VIEW_SONGPROPERTIES, kcViewSongProperties, _T("Song P&roperties") }, + { ID_PATTERN_MIDIMACRO, kcShowMacroConfig, _T("&Zxx Macro Configuration") }, + { ID_VIEW_MIDIMAPPING, kcViewMIDImapping, _T("&MIDI Mapping") }, + { ID_VIEW_EDITHISTORY, kcViewEditHistory, _T("Edit &History") }, + // Help submenu + { ID_HELPSHOW, kcHelp, _T("&Help") }, + { ID_EXAMPLE_MODULES, kcNull, _T("&Example Modules") }, + }; + + for(const auto & [cmdID, command, text] : MenuItems) + { + if(id == cmdID) + { + if(command != kcNull) + return GetKeyTextFromCommand(command, text); + else + return text; + } + } + MPT_ASSERT_NOTREACHED(); + return _T("Unknown Item"); +} + + +void CInputHandler::UpdateMainMenu() +{ + CMenu *pMenu = (CMainFrame::GetMainFrame())->GetMenu(); + if (!pMenu) return; + + pMenu->GetSubMenu(0)->ModifyMenu(0, MF_BYPOSITION | MF_STRING, 0, GetMenuText(ID_FILE_NEW)); + static constexpr int MenuItems[] = + { + ID_FILE_OPEN, + ID_FILE_APPENDMODULE, + ID_FILE_CLOSE, + ID_FILE_SAVE, + ID_FILE_SAVE_AS, + ID_FILE_SAVEASWAVE, + ID_FILE_SAVEMIDI, + ID_FILE_SAVECOMPAT, + ID_IMPORT_MIDILIB, + ID_ADD_SOUNDBANK, + + ID_PLAYER_PLAY, + ID_PLAYER_PLAYFROMSTART, + ID_PLAYER_STOP, + ID_PLAYER_PAUSE, + ID_MIDI_RECORD, + ID_ESTIMATESONGLENGTH, + ID_APPROX_BPM, + + ID_EDIT_UNDO, + ID_EDIT_REDO, + ID_EDIT_CUT, + ID_EDIT_COPY, + ID_EDIT_PASTE, + ID_EDIT_PASTE_SPECIAL, + ID_EDIT_MIXPASTE_ITSTYLE, + ID_EDIT_PASTEFLOOD, + ID_EDIT_PUSHFORWARDPASTE, + ID_EDIT_SELECT_ALL, + ID_EDIT_FIND, + ID_EDIT_FINDNEXT, + ID_EDIT_GOTO_MENU, + ID_EDIT_SPLITKEYBOARDSETTINGS, + + ID_VIEW_GLOBALS, + ID_VIEW_SAMPLES, + ID_VIEW_PATTERNS, + ID_VIEW_INSTRUMENTS, + ID_VIEW_COMMENTS, + ID_VIEW_TOOLBAR, + IDD_TREEVIEW, + ID_VIEW_OPTIONS, + ID_PLUGIN_SETUP, + ID_CHANNEL_MANAGER, + ID_CLIPBOARD_MANAGER, + ID_VIEW_SONGPROPERTIES, + ID_VIEW_SONGPROPERTIES, + ID_PATTERN_MIDIMACRO, + ID_VIEW_EDITHISTORY, + ID_HELPSHOW, + }; + for(const auto id : MenuItems) + { + pMenu->ModifyMenu(id, MF_BYCOMMAND | MF_STRING, id, GetMenuText(id)); + } +} + + +void CInputHandler::SetNewCommandSet(const CCommandSet *newSet) +{ + m_activeCommandSet->Copy(newSet); + m_activeCommandSet->GenKeyMap(m_keyMap); + SetupSpecialKeyInterception(); // Feature: use Windows keys as modifier keys, intercept special keys + UpdateMainMenu(); +} + + +bool CInputHandler::SetEffectLetters(const CModSpecifications &modSpecs) +{ + MPT_LOG_GLOBAL(LogDebug, "InputHandler", U_("Changing command set.")); + bool retval = m_activeCommandSet->QuickChange_SetEffects(modSpecs); + if(retval) m_activeCommandSet->GenKeyMap(m_keyMap); + return retval; +} + + +bool CInputHandler::IsKeyPressHandledByTextBox(DWORD key, HWND hWnd) const +{ + if(hWnd == nullptr) + return false; + + TCHAR activeWindowClassName[6]; + GetClassName(hWnd, activeWindowClassName, mpt::saturate_cast<int>(std::size(activeWindowClassName))); + const bool textboxHasFocus = _tcsicmp(activeWindowClassName, _T("Edit")) == 0; + if(!textboxHasFocus) + return false; + + //Alpha-numerics (only shift or no modifier): + if(!GetModifierMask().test_any_except(ModShift) + && ((key>='A'&&key<='Z') || (key>='0'&&key<='9') || + key==VK_DIVIDE || key==VK_MULTIPLY || key==VK_SPACE || key==VK_RETURN || + key==VK_CAPITAL || (key>=VK_OEM_1 && key<=VK_OEM_3) || (key>=VK_OEM_4 && key<=VK_OEM_8))) + return true; + + //navigation (any modifier): + if(key == VK_LEFT || key == VK_RIGHT || key == VK_UP || key == VK_DOWN || + key == VK_HOME || key == VK_END || key == VK_DELETE || key == VK_INSERT || key == VK_BACK) + return true; + + //Copy paste etc.. + if(GetModifierMask() == ModCtrl && + (key == 'Y' || key == 'Z' || key == 'X' || key == 'C' || key == 'V' || key == 'A')) + return true; + + return false; +} + + +BypassInputHandler::BypassInputHandler() +{ + if(CMainFrame::GetInputHandler()) + { + bypassed = true; + CMainFrame::GetInputHandler()->Bypass(true); + } +} + + +BypassInputHandler::~BypassInputHandler() +{ + if(bypassed) + { + CMainFrame::GetInputHandler()->Bypass(false); + bypassed = false; + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.h b/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.h new file mode 100644 index 00000000..37b21a67 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/InputHandler.h @@ -0,0 +1,84 @@ +/* + * InputHandler.h + * -------------- + * Purpose: Implementation of keyboard input handling, keymap loading, ... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CommandSet.h" + +OPENMPT_NAMESPACE_BEGIN + +// Hook codes +enum +{ + HC_MIDI = 0x8000, +}; + +class CInputHandler +{ +protected: + CWnd *m_pMainFrm; + KeyMap m_keyMap; + FlagSet<Modifiers> m_modifierMask = ModNone; + int m_bypassCount = 0; + bool m_bInterceptWindowsKeys : 1, m_bInterceptNumLock : 1, m_bInterceptCapsLock : 1, m_bInterceptScrollLock : 1; + +public: + std::unique_ptr<CCommandSet> m_activeCommandSet; + + std::array<CommandID, 10> m_lastCommands; + size_t m_lastCommandPos = 0; + +public: + CInputHandler(CWnd *mainframe); + CommandID GeneralKeyEvent(InputTargetContext context, int code, WPARAM wParam , LPARAM lParam); + CommandID KeyEvent(InputTargetContext context, UINT &nChar, UINT &nRepCnt, UINT &nFlags, KeyEventType keyEventType, CWnd *pSourceWnd = nullptr); + static KeyEventType GetKeyEventType(UINT nFlags); + bool IsKeyPressHandledByTextBox(DWORD wparam, HWND hWnd) const; + CommandID HandleMIDIMessage(InputTargetContext context, uint32 message); + + int GetKeyListSize(CommandID cmd) const; + +protected: + void LogModifiers(); + bool CatchModifierChange(WPARAM wParam, KeyEventType keyEventType, int scancode); + bool InterceptSpecialKeys(UINT nChar, UINT nFlags, bool generateMsg); + void SetupSpecialKeyInterception(); + CommandID SendCommands(CWnd *wnd, const KeyMapRange &cmd); + +public: + bool ShiftPressed() const; + bool SelectionPressed() const; + bool CtrlPressed() const; + bool AltPressed() const; + bool IsBypassed() const; + void Bypass(bool); + FlagSet<Modifiers> GetModifierMask() const; + void SetModifierMask(FlagSet<Modifiers> mask); + CString GetKeyTextFromCommand(CommandID c, const TCHAR *prependText = nullptr) const; + CString GetMenuText(UINT id) const; + void UpdateMainMenu(); + void SetNewCommandSet(const CCommandSet *newSet); + bool SetEffectLetters(const CModSpecifications &modSpecs); +}; + + +// RAII object for temporarily bypassing the input handler +class BypassInputHandler +{ +private: + bool bypassed = false; +public: + BypassInputHandler(); + ~BypassInputHandler(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.cpp new file mode 100644 index 00000000..b6706cc2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.cpp @@ -0,0 +1,816 @@ +/* + * KeyConfigDlg.cpp + * ---------------- + * Purpose: Implementation of OpenMPT's keyboard configuration dialog. + * 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 "KeyConfigDlg.h" +#include "FileDialog.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/MIDIEvents.h" + + +OPENMPT_NAMESPACE_BEGIN + + +//***************************************************************************************// +// CCustEdit: customised CEdit control to catch keypresses. +// (does what CHotKeyCtrl does,but better) +//***************************************************************************************// + +BEGIN_MESSAGE_MAP(CCustEdit, CEdit) + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_MESSAGE(WM_MOD_MIDIMSG, &CCustEdit::OnMidiMsg) +END_MESSAGE_MAP() + + +LRESULT CCustEdit::OnMidiMsg(WPARAM dwMidiDataParam, LPARAM) +{ + if(!m_isFocussed) + return 1; + + uint32 midiData = static_cast<uint32>(dwMidiDataParam); + const auto byte1 = MIDIEvents::GetDataByte1FromEvent(midiData), byte2 = MIDIEvents::GetDataByte2FromEvent(midiData); + switch(MIDIEvents::GetTypeFromEvent(midiData)) + { + case MIDIEvents::evControllerChange: + if(byte2 != 0) + { + SetKey(ModMidi, byte1); + if(!m_isDummy) + m_pOptKeyDlg->OnSetKeyChoice(); + } + break; + + case MIDIEvents::evNoteOn: + case MIDIEvents::evNoteOff: + SetKey(ModMidi, byte1 | 0x80); + if(!m_isDummy) + m_pOptKeyDlg->OnSetKeyChoice(); + break; + } + + return 1; +} + + +BOOL CCustEdit::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + if(pMsg->message == WM_KEYDOWN || pMsg->message == WM_SYSKEYDOWN) + { + SetKey(CMainFrame::GetInputHandler()->GetModifierMask(), static_cast<UINT>(pMsg->wParam)); + return -1; // Keypress handled, don't pass on message. + } else if(pMsg->message == WM_KEYUP || pMsg->message == WM_SYSKEYUP) + { + //if a key has been released but custom edit box is empty, we have probably just + //navigated into the box with TAB or SHIFT-TAB. No need to set keychoice. + if(code != 0 && !m_isDummy) + m_pOptKeyDlg->OnSetKeyChoice(); + } + } + return CEdit::PreTranslateMessage(pMsg); +} + + +void CCustEdit::SetKey(FlagSet<Modifiers> inMod, UINT inCode) +{ + mod = inMod; + code = inCode; + //Setup display + SetWindowText(KeyCombination::GetKeyText(mod, code)); +} + + +void CCustEdit::OnSetFocus(CWnd *pOldWnd) +{ + CEdit::OnSetFocus(pOldWnd); + // Lock the input handler + CMainFrame::GetInputHandler()->Bypass(true); + // Accept MIDI input + CMainFrame::GetMainFrame()->SetMidiRecordWnd(m_hWnd); + + m_isFocussed = true; +} + + +void CCustEdit::OnKillFocus(CWnd *pNewWnd) +{ + CEdit::OnKillFocus(pNewWnd); + //unlock the input handler + CMainFrame::GetInputHandler()->Bypass(false); + m_isFocussed = false; +} + + +//***************************************************************************************// +// COptionsKeyboard: +// +//***************************************************************************************// + +// Initialisation + +BEGIN_MESSAGE_MAP(COptionsKeyboard, CPropertyPage) + ON_LBN_SELCHANGE(IDC_CHOICECOMBO, &COptionsKeyboard::OnKeyChoiceSelect) + ON_LBN_SELCHANGE(IDC_COMMAND_LIST, &COptionsKeyboard::OnCommandKeySelChanged) + ON_LBN_SELCHANGE(IDC_KEYCATEGORY, &COptionsKeyboard::OnCategorySelChanged) + ON_EN_UPDATE(IDC_CHORDDETECTWAITTIME, &COptionsKeyboard::OnChordWaitTimeChanged) //rewbs.autochord + ON_COMMAND(IDC_DELETE, &COptionsKeyboard::OnDeleteKeyChoice) + ON_COMMAND(IDC_RESTORE, &COptionsKeyboard::OnRestoreKeyChoice) + ON_COMMAND(IDC_LOAD, &COptionsKeyboard::OnLoad) + ON_COMMAND(IDC_SAVE, &COptionsKeyboard::OnSave) + ON_COMMAND(IDC_CHECKKEYDOWN, &COptionsKeyboard::OnCheck) + ON_COMMAND(IDC_CHECKKEYHOLD, &COptionsKeyboard::OnCheck) + ON_COMMAND(IDC_CHECKKEYUP, &COptionsKeyboard::OnCheck) + ON_COMMAND(IDC_NOTESREPEAT, &COptionsKeyboard::OnNotesRepeat) + ON_COMMAND(IDC_NONOTESREPEAT, &COptionsKeyboard::OnNoNotesRepeat) + ON_COMMAND(IDC_CLEARLOG, &COptionsKeyboard::OnClearLog) + ON_COMMAND(IDC_RESTORE_KEYMAP, &COptionsKeyboard::OnRestoreDefaultKeymap) + ON_EN_CHANGE(IDC_FIND, &COptionsKeyboard::OnSearchTermChanged) + ON_EN_CHANGE(IDC_FINDHOTKEY, &COptionsKeyboard::OnFindHotKey) + ON_EN_SETFOCUS(IDC_FINDHOTKEY, &COptionsKeyboard::OnClearHotKey) + ON_WM_DESTROY() +END_MESSAGE_MAP() + +void COptionsKeyboard::DoDataExchange(CDataExchange *pDX) +{ + CPropertyPage::DoDataExchange(pDX); + DDX_Control(pDX, IDC_KEYCATEGORY, m_cmbCategory); + DDX_Control(pDX, IDC_COMMAND_LIST, m_lbnCommandKeys); + DDX_Control(pDX, IDC_CHOICECOMBO, m_cmbKeyChoice); + DDX_Control(pDX, IDC_CHORDDETECTWAITTIME, m_eChordWaitTime);//rewbs.autochord + DDX_Control(pDX, IDC_KEYREPORT, m_eReport); + DDX_Control(pDX, IDC_CUSTHOTKEY, m_eCustHotKey); + DDX_Control(pDX, IDC_FINDHOTKEY, m_eFindHotKey); + DDX_Control(pDX, IDC_CHECKKEYDOWN, m_bKeyDown); + DDX_Control(pDX, IDC_CHECKKEYHOLD, m_bKeyHold); + DDX_Control(pDX, IDC_CHECKKEYUP, m_bKeyUp); + DDX_Control(pDX, IDC_FIND, m_eFind); +} + + +BOOL COptionsKeyboard::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_KEYBOARD; + return CPropertyPage::OnSetActive(); +} + + + +BOOL COptionsKeyboard::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + m_fullPathName = TrackerSettings::Instance().m_szKbdFile; + + m_localCmdSet = std::make_unique<CCommandSet>(); + m_localCmdSet->Copy(CMainFrame::GetInputHandler()->m_activeCommandSet.get()); + + //Fill category combo and automatically selects first category + DefineCommandCategories(); + for(size_t c = 0; c < commandCategories.size(); c++) + { + if(commandCategories[c].name && !commandCategories[c].commands.empty()) + m_cmbCategory.SetItemData(m_cmbCategory.AddString(commandCategories[c].name), c); + } + m_cmbCategory.SetCurSel(0); + UpdateDialog(); + + m_eCustHotKey.SetParent(m_hWnd, IDC_CUSTHOTKEY, this); + m_eFindHotKey.SetParent(m_hWnd, IDC_FINDHOTKEY, this); + m_eReport.FmtLines(TRUE); + m_eReport.SetWindowText(_T("")); + + m_eChordWaitTime.SetWindowText(mpt::cfmt::val(TrackerSettings::Instance().gnAutoChordWaitTime)); + return TRUE; +} + + +void CommandCategory::AddCommands(CommandID first, CommandID last, bool addSeparatorAtEnd) +{ + int count = last - first + 1, val = first; + commands.insert(commands.end(), count, kcNull); + std::generate(commands.end() - count, commands.end(), [&val] { return static_cast<CommandID>(val++); }); + if(addSeparatorAtEnd) + separators.push_back(last); +} + + +// Filter commands: We only need user to see a select set off commands +// for each category +void COptionsKeyboard::DefineCommandCategories() +{ + { + CommandCategory newCat(_T("Global keys"), kCtxAllContexts); + + newCat.AddCommands(kcStartFile, kcEndFile, true); + newCat.AddCommands(kcStartPlayCommands, kcEndPlayCommands, true); + newCat.AddCommands(kcStartEditCommands, kcEndEditCommands, true); + newCat.AddCommands(kcStartView, kcEndView, true); + newCat.AddCommands(kcStartMisc, kcEndMisc, true); + newCat.commands.push_back(kcDummyShortcut); + + commandCategories.push_back(newCat); + } + + commandCategories.emplace_back(_T(" General [Top]"), kCtxCtrlGeneral); + commandCategories.emplace_back(_T(" General [Bottom]"), kCtxViewGeneral); + commandCategories.emplace_back(_T(" Pattern Editor [Top]"), kCtxCtrlPatterns); + + { + CommandCategory newCat(_T(" Pattern Editor - Order List"), kCtxCtrlOrderlist); + + newCat.AddCommands(kcStartOrderlistCommands, kcEndOrderlistCommands); + newCat.separators.push_back(kcEndOrderlistNavigation); + newCat.separators.push_back(kcEndOrderlistEdit); + newCat.separators.push_back(kcEndOrderlistNum); + + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Quick Channel Settings"), kCtxChannelSettings); + newCat.AddCommands(kcStartChnSettingsCommands, kcEndChnSettingsCommands); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - General"), kCtxViewPatterns); + + newCat.AddCommands(kcStartPlainNavigate, kcEndPlainNavigate, true); + newCat.AddCommands(kcStartJumpSnap, kcEndJumpSnap, true); + newCat.AddCommands(kcStartHomeEnd, kcEndHomeEnd, true); + newCat.AddCommands(kcPrevPattern, kcNextSequence, true); + newCat.AddCommands(kcStartSelect, kcEndSelect, true); + newCat.AddCommands(kcStartPatternClipboard, kcEndPatternClipboard, true); + newCat.AddCommands(kcClearRow, kcInsertWholeRowGlobal, true); + newCat.AddCommands(kcStartChannelKeys, kcEndChannelKeys, true); + newCat.AddCommands(kcBeginTranspose, kcEndTranspose, true); + newCat.AddCommands(kcPatternAmplify, kcPatternShrinkSelection, true); + newCat.AddCommands(kcStartPatternEditMisc, kcEndPatternEditMisc, true); + + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Note Column"), kCtxViewPatternsNote); + + newCat.AddCommands(kcVPStartNotes, kcVPEndNotes, true); + newCat.AddCommands(kcSetOctave0, kcSetOctave9, true); + newCat.AddCommands(kcStartNoteMisc, kcEndNoteMisc); + + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Instrument Column"), kCtxViewPatternsIns); + newCat.AddCommands(kcSetIns0, kcSetIns9); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Volume Column"), kCtxViewPatternsVol); + newCat.AddCommands(kcSetVolumeStart, kcSetVolumeEnd); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Effect Column"), kCtxViewPatternsFX); + newCat.AddCommands(kcSetFXStart, kcSetFXEnd); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Pattern Editor - Effect Parameter Column"), kCtxViewPatternsFXparam); + newCat.AddCommands(kcSetFXParam0, kcSetFXParamF); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Sample [Top]"), kCtxCtrlSamples); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Sample Editor"), kCtxViewSamples); + + newCat.AddCommands(kcStartSampleEditing, kcEndSampleEditing, true); + newCat.AddCommands(kcStartSampleMisc, kcEndSampleMisc, true); + newCat.AddCommands(kcStartSampleCues, kcEndSampleCueGroup); + + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Instrument Editor"), kCtxCtrlInstruments); + newCat.AddCommands(kcStartInstrumentCtrlMisc, kcEndInstrumentCtrlMisc); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Envelope Editor"), kCtxViewInstruments); + newCat.AddCommands(kcStartInstrumentMisc, kcEndInstrumentMisc); + commandCategories.push_back(newCat); + } + + commandCategories.emplace_back(_T(" Comments [Top]"), kCtxCtrlComments); + + { + CommandCategory newCat(_T(" Comments [Bottom]"), kCtxViewComments); + newCat.AddCommands(kcStartCommentsCommands, kcEndCommentsCommands); + commandCategories.push_back(newCat); + } + + { + CommandCategory newCat(_T(" Plugin Editor"), kCtxVSTGUI); + newCat.AddCommands(kcStartVSTGUICommands, kcEndVSTGUICommands); + commandCategories.push_back(newCat); + } +} + + +// Pure GUI methods + +void COptionsKeyboard::UpdateDialog() +{ + OnCategorySelChanged(); // Fills command list and automatically selects first command. + OnCommandKeySelChanged(); // Fills command key choice list for that command and automatically selects first choice. +} + + +void COptionsKeyboard::OnKeyboardChanged() +{ + OnSettingsChanged(); + UpdateDialog(); +} + + +void COptionsKeyboard::OnCategorySelChanged() +{ + int cat = static_cast<int>(m_cmbCategory.GetItemData(m_cmbCategory.GetCurSel())); + + if(cat >= 0 && cat != m_curCategory) + { + // Changed category + UpdateShortcutList(cat); + } +} + + +// Force last active category to be selected in dropdown menu. +void COptionsKeyboard::UpdateCategory() +{ + for(int i = 0; i < m_cmbCategory.GetCount(); i++) + { + if((int)m_cmbCategory.GetItemData(i) == m_curCategory) + { + m_cmbCategory.SetCurSel(i); + break; + } + } +} + +void COptionsKeyboard::OnSearchTermChanged() +{ + CString findString; + m_eFind.GetWindowText(findString); + + if(findString.IsEmpty()) + { + UpdateCategory(); + } + UpdateShortcutList(findString.IsEmpty() ? m_curCategory : -1); +} + + +void COptionsKeyboard::OnFindHotKey() +{ + if(m_eFindHotKey.code == 0) + { + UpdateCategory(); + } + UpdateShortcutList(m_eFindHotKey.code == 0 ? m_curCategory : -1); +} + + +void COptionsKeyboard::OnClearHotKey() +{ + // Focus key search: Clear input + m_eFindHotKey.SetKey(ModNone, 0); +} + + +// Fills command list and automatically selects first command. +void COptionsKeyboard::UpdateShortcutList(int category) +{ + CString findString; + m_eFind.GetWindowText(findString); + findString.MakeLower(); + + const bool searchByName = !findString.IsEmpty(), searchByKey = (m_eFindHotKey.code != 0); + const bool doSearch = (searchByName || searchByKey); + + int firstCat = category, lastCat = category; + if(category == -1) + { + // We will search in all categories + firstCat = 0; + lastCat = static_cast<int>(commandCategories.size()) - 1; + } + + CommandID curCommand = static_cast<CommandID>(m_lbnCommandKeys.GetItemData(m_lbnCommandKeys.GetCurSel())); + m_lbnCommandKeys.ResetContent(); + + for(int cat = firstCat; cat <= lastCat; cat++) + { + // When searching, we also add the category names to the list. + bool addCategoryName = (firstCat != lastCat); + + for(size_t cmd = 0; cmd < commandCategories[cat].commands.size(); cmd++) + { + CommandID com = (CommandID)commandCategories[cat].commands[cmd]; + + CString cmdText = m_localCmdSet->GetCommandText(com); + bool addKey = true; + + if(searchByKey) + { + addKey = false; + int numChoices = m_localCmdSet->GetKeyListSize(com); + for(int choice = 0; choice < numChoices; choice++) + { + const KeyCombination &kc = m_localCmdSet->GetKey(com, choice); + if(kc.KeyCode() == m_eFindHotKey.code && kc.Modifier() == m_eFindHotKey.mod) + { + addKey = true; + break; + } + } + } + if(searchByName && addKey) + { + addKey = (cmdText.MakeLower().Find(findString) >= 0); + } + + if(addKey) + { + m_curCategory = cat; + + if(!m_localCmdSet->isHidden(com)) + { + if(doSearch && addCategoryName) + { + const CString catName = _T("------ ") + commandCategories[cat].name.Trim() + _T(" ------"); + m_lbnCommandKeys.SetItemData(m_lbnCommandKeys.AddString(catName), DWORD_PTR(-1)); + addCategoryName = false; + } + + int item = m_lbnCommandKeys.AddString(m_localCmdSet->GetCommandText(com)); + m_lbnCommandKeys.SetItemData(item, com); + + if(curCommand == com) + { + // Keep selection on previously selected string + m_lbnCommandKeys.SetCurSel(item); + } + } + + if(commandCategories[cat].SeparatorAt(com)) + m_lbnCommandKeys.SetItemData(m_lbnCommandKeys.AddString(_T("------------------------------------------------------")), DWORD_PTR(-1)); + } + } + } + + if(m_lbnCommandKeys.GetCurSel() == -1) + { + m_lbnCommandKeys.SetCurSel(0); + } + OnCommandKeySelChanged(); +} + + +// Fills key choice list and automatically selects first key choice +void COptionsKeyboard::OnCommandKeySelChanged() +{ + const CommandID cmd = static_cast<CommandID>(m_lbnCommandKeys.GetItemData(m_lbnCommandKeys.GetCurSel())); + CString str; + + //Separator + if(cmd == kcNull) + { + m_cmbKeyChoice.SetWindowText(_T("")); + m_cmbKeyChoice.EnableWindow(FALSE); + m_eCustHotKey.SetWindowText(_T("")); + m_eCustHotKey.EnableWindow(FALSE); + m_bKeyDown.SetCheck(0); + m_bKeyDown.EnableWindow(FALSE); + m_bKeyHold.SetCheck(0); + m_bKeyHold.EnableWindow(FALSE); + m_bKeyUp.SetCheck(0); + m_bKeyUp.EnableWindow(FALSE); + m_curCommand = kcNull; + } + + //Fill "choice" list + else if((cmd >= 0) && (cmd != m_curCommand) || m_forceUpdate) // Have we changed command? + { + m_forceUpdate = false; + + m_cmbKeyChoice.EnableWindow(TRUE); + m_eCustHotKey.EnableWindow(TRUE); + m_bKeyDown.EnableWindow(TRUE); + m_bKeyHold.EnableWindow(TRUE); + m_bKeyUp.EnableWindow(TRUE); + + m_curCommand = cmd; + m_curCategory = GetCategoryFromCommandID(cmd); + + m_cmbKeyChoice.ResetContent(); + int numChoices = m_localCmdSet->GetKeyListSize(cmd); + if((cmd < kcNumCommands) && (numChoices > 0)) + { + for(int i = 0; i < numChoices; i++) + { + CString s = MPT_CFORMAT("Choice {} (of {})")(i + 1, numChoices); + m_cmbKeyChoice.SetItemData(m_cmbKeyChoice.AddString(s), i); + } + } + m_cmbKeyChoice.SetItemData(m_cmbKeyChoice.AddString(_T("<new>")), numChoices); + m_cmbKeyChoice.SetCurSel(0); + m_curKeyChoice = -1; + OnKeyChoiceSelect(); + } +} + +//Fills or clears key choice info +void COptionsKeyboard::OnKeyChoiceSelect() +{ + int choice = static_cast<int>(m_cmbKeyChoice.GetItemData(m_cmbKeyChoice.GetCurSel())); + CommandID cmd = m_curCommand; + + //If nothing there, clear + if(cmd == kcNull || choice >= m_localCmdSet->GetKeyListSize(cmd) || choice < 0) + { + m_curKeyChoice = choice; + m_forceUpdate = true; + m_eCustHotKey.SetKey(ModNone, 0); + m_bKeyDown.SetCheck(0); + m_bKeyHold.SetCheck(0); + m_bKeyUp.SetCheck(0); + return; + } + + //else, if changed, Fill + if(choice != m_curKeyChoice || m_forceUpdate) + { + m_curKeyChoice = choice; + m_forceUpdate = false; + KeyCombination kc = m_localCmdSet->GetKey(cmd, choice); + m_eCustHotKey.SetKey(kc.Modifier(), kc.KeyCode()); + + m_bKeyDown.SetCheck((kc.EventType() & kKeyEventDown) ? BST_CHECKED : BST_UNCHECKED); + m_bKeyHold.SetCheck((kc.EventType() & kKeyEventRepeat) ? BST_CHECKED : BST_UNCHECKED); + m_bKeyUp.SetCheck((kc.EventType() & kKeyEventUp) ? BST_CHECKED : BST_UNCHECKED); + } +} + +void COptionsKeyboard::OnChordWaitTimeChanged() +{ + CString s; + UINT val; + m_eChordWaitTime.GetWindowText(s); + val = _tstoi(s); + if(val > 5000) + { + val = 5000; + m_eChordWaitTime.SetWindowText(_T("5000")); + } + OnSettingsChanged(); +} + +// Change handling + +void COptionsKeyboard::OnRestoreKeyChoice() +{ + KeyCombination kc; + CommandID cmd = m_curCommand; + + CInputHandler *ih = CMainFrame::GetInputHandler(); + + // Do nothing if there's nothing to restore + if(cmd == kcNull || m_curKeyChoice < 0 || m_curKeyChoice >= ih->GetKeyListSize(cmd)) + { + ::MessageBeep(MB_ICONWARNING); + return; + } + + // Restore current key combination choice for currently selected command. + kc = ih->m_activeCommandSet->GetKey(cmd, m_curKeyChoice); + m_localCmdSet->Remove(m_curKeyChoice, cmd); + m_localCmdSet->Add(kc, cmd, true, m_curKeyChoice); + + ForceUpdateGUI(); + return; +} + +void COptionsKeyboard::OnDeleteKeyChoice() +{ + CommandID cmd = m_curCommand; + + // Do nothing if there's no key defined for this slot. + if(m_curCommand == kcNull || m_curKeyChoice < 0 || m_curKeyChoice >= m_localCmdSet->GetKeyListSize(cmd)) + { + ::MessageBeep(MB_ICONWARNING); + return; + } + + // Delete current key combination choice for currently selected command. + m_localCmdSet->Remove(m_curKeyChoice, cmd); + + ForceUpdateGUI(); + return; +} + + +void COptionsKeyboard::OnSetKeyChoice() +{ + CommandID cmd = m_curCommand; + if(cmd == kcNull) + { + Reporting::Warning("Invalid slot.", "Invalid key data", this); + return; + } + + FlagSet<KeyEventType> event = kKeyEventNone; + if(m_bKeyDown.GetCheck() != BST_UNCHECKED) + event |= kKeyEventDown; + if(m_bKeyHold.GetCheck() != BST_UNCHECKED) + event |= kKeyEventRepeat; + if(m_bKeyUp.GetCheck() != BST_UNCHECKED) + event |= kKeyEventUp; + + KeyCombination kc((commandCategories[m_curCategory]).id, m_eCustHotKey.mod, m_eCustHotKey.code, event); + //detect invalid input + if(!kc.KeyCode()) + { + Reporting::Warning("You need to say to which key you'd like to map this command to.", "Invalid key data", this); + return; + } + if(!kc.EventType()) + { + ::MessageBeep(MB_ICONWARNING); + kc.EventType(kKeyEventDown); + } + + bool add = true; + std::pair<CommandID, KeyCombination> conflictCmd; + if((conflictCmd = m_localCmdSet->IsConflicting(kc, cmd)).first != kcNull + && conflictCmd.first != cmd + && !m_localCmdSet->IsCrossContextConflict(kc, conflictCmd.second)) + { + ConfirmAnswer delOld = Reporting::Confirm(_T("New shortcut (") + kc.GetKeyText() + _T(") has the same key combination as ") + m_localCmdSet->GetCommandText(conflictCmd.first) + _T(" in ") + conflictCmd.second.GetContextText() + _T(".\nDo you want to delete the other shortcut, only keeping the new one?"), _T("Shortcut Conflict"), true, false, this); + if(delOld == cnfYes) + { + m_localCmdSet->Remove(conflictCmd.second, conflictCmd.first); + } else if(delOld == cnfCancel) + { + // Cancel altogther; restore original choice + add = false; + if(m_curKeyChoice >= 0 && m_curKeyChoice < m_localCmdSet->GetKeyListSize(cmd)) + { + KeyCombination origKc = m_localCmdSet->GetKey(cmd, m_curKeyChoice); + m_eCustHotKey.SetKey(origKc.Modifier(), origKc.KeyCode()); + } else + { + m_eCustHotKey.SetWindowText(_T("")); + } + } + } + + if(add) + { + CString report, reportHistory; + //process valid input + m_localCmdSet->Remove(m_curKeyChoice, cmd); + report = m_localCmdSet->Add(kc, cmd, true, m_curKeyChoice); + + //Update log + m_eReport.GetWindowText(reportHistory); + m_eReport.SetWindowText(report + reportHistory); + ForceUpdateGUI(); + } +} + + +void COptionsKeyboard::OnOK() +{ + CMainFrame::GetInputHandler()->SetNewCommandSet(m_localCmdSet.get()); + + CString cs; + m_eChordWaitTime.GetWindowText(cs); + TrackerSettings::Instance().gnAutoChordWaitTime = _tstoi(cs); + + CPropertyPage::OnOK(); +} + + +void COptionsKeyboard::OnDestroy() +{ + CPropertyPage::OnDestroy(); + m_localCmdSet = nullptr; +} + + +void COptionsKeyboard::OnLoad() +{ + auto dlg = OpenFileDialog() + .DefaultExtension("mkb") + .DefaultFilename(m_fullPathName) + .ExtensionFilter("OpenMPT Key Bindings (*.mkb)|*.mkb||") + .AddPlace(theApp.GetInstallPkgPath() + P_("extraKeymaps")) + .WorkingDirectory(TrackerSettings::Instance().m_szKbdFile); + if(!dlg.Show(this)) + return; + + m_fullPathName = dlg.GetFirstFile(); + m_localCmdSet->LoadFile(m_fullPathName); + ForceUpdateGUI(); +} + + +void COptionsKeyboard::OnSave() +{ + auto dlg = SaveFileDialog() + .DefaultExtension("mkb") + .DefaultFilename(m_fullPathName) + .ExtensionFilter("OpenMPT Key Bindings (*.mkb)|*.mkb||") + .WorkingDirectory(TrackerSettings::Instance().m_szKbdFile); + if(!dlg.Show(this)) + return; + + m_fullPathName = dlg.GetFirstFile(); + m_localCmdSet->SaveFile(m_fullPathName); +} + + +void COptionsKeyboard::OnNotesRepeat() +{ + m_localCmdSet->QuickChange_NotesRepeat(true); + ForceUpdateGUI(); +} + + +void COptionsKeyboard::OnNoNotesRepeat() +{ + m_localCmdSet->QuickChange_NotesRepeat(false); + ForceUpdateGUI(); +} + + +void COptionsKeyboard::ForceUpdateGUI() +{ + m_forceUpdate = true; // m_nCurKeyChoice and m_nCurHotKey haven't changed, yet we still want to update. + int ntmpChoice = m_curKeyChoice; // next call will overwrite m_nCurKeyChoice + OnCommandKeySelChanged(); // update keychoice list + m_cmbKeyChoice.SetCurSel(ntmpChoice); // select fresh keychoice (thus restoring m_nCurKeyChoice) + OnKeyChoiceSelect(); // update key data + OnSettingsChanged(); // Enable "apply" button +} + + +void COptionsKeyboard::OnClearLog() +{ + m_eReport.SetWindowText(_T("")); + ForceUpdateGUI(); +} + + +void COptionsKeyboard::OnRestoreDefaultKeymap() +{ + if(Reporting::Confirm("Discard all custom changes and restore default key configuration?", false, true, this) == cnfYes) + { + m_localCmdSet->LoadDefaultKeymap(); + ForceUpdateGUI(); + } +} + + +int COptionsKeyboard::GetCategoryFromCommandID(CommandID command) const +{ + for(size_t cat = 0; cat < commandCategories.size(); cat++) + { + const auto &cmds = commandCategories[cat].commands; + if(mpt::contains(cmds, command)) + return static_cast<int>(cat); + } + return -1; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.h new file mode 100644 index 00000000..dd1573c3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/KeyConfigDlg.h @@ -0,0 +1,133 @@ +/* + * KeyConfigDlg.h + * -------------- + * Purpose: Implementation of OpenMPT's keyboard configuration dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "Mainfrm.h" +#include "InputHandler.h" + +OPENMPT_NAMESPACE_BEGIN + +class COptionsKeyboard; + +// Might promote to class so we can add rules +// (eg automatically do note off stuff, generate chord keybindings from notes based just on modifier. +// Would need GUI rules too as options would be different for each category +class CommandCategory +{ +public: + CommandCategory(const TCHAR *n, InputTargetContext d) : name(n), id(d) { } + + bool SeparatorAt(CommandID c) const + { + return mpt::contains(separators, c); + } + + void AddCommands(CommandID first, CommandID last, bool addSeparatorAtEnd = false); + + CString name; + InputTargetContext id; + std::vector<CommandID> separators; + std::vector<CommandID> commands; +}; + + +class CCustEdit: public CEdit +{ +protected: + COptionsKeyboard *m_pOptKeyDlg; + HWND m_hParent = nullptr; + UINT m_nCtrlId = 0; + bool m_isFocussed = false, m_isDummy = false; + +public: + FlagSet<Modifiers> mod = ModNone; + UINT code = 0; + + CCustEdit(bool dummyField) : m_isDummy(dummyField) { } + void SetParent(HWND h, UINT nID, COptionsKeyboard *pOKD) + { + m_hParent = h; + m_nCtrlId = nID; + m_pOptKeyDlg = pOKD; + } + void SetKey(FlagSet<Modifiers> mod, UINT code); + + BOOL PreTranslateMessage(MSG *pMsg) override; + + afx_msg void OnSetFocus(CWnd* pOldWnd); + afx_msg void OnKillFocus(CWnd* pNewWnd); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + + DECLARE_MESSAGE_MAP() +}; + +class COptionsKeyboard: public CPropertyPage +{ +protected: + CListBox m_lbnHotKeys; + CListBox m_lbnCommandKeys; + CComboBox m_cmbKeyChoice; + CComboBox m_cmbCategory; + CButton m_bKeyDown, m_bKeyHold, m_bKeyUp; + CButton m_bnReset; + CCustEdit m_eCustHotKey, m_eFindHotKey; + CEdit m_eFind; + CEdit m_eReport, m_eChordWaitTime; + CommandID m_curCommand = kcNull; + int m_curCategory = -1, m_curKeyChoice = -1; + mpt::PathString m_fullPathName; + std::unique_ptr<CCommandSet> m_localCmdSet; + bool m_forceUpdate = false; + + void ForceUpdateGUI(); + void UpdateShortcutList(int category = -1); + void UpdateCategory(); + int GetCategoryFromCommandID(CommandID command) const; + +public: + COptionsKeyboard() : CPropertyPage(IDD_OPTIONS_KEYBOARD), m_eCustHotKey(false), m_eFindHotKey(true) { } + std::vector<CommandCategory> commandCategories; + void DefineCommandCategories(); + + void OnSetKeyChoice(); + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + + afx_msg void UpdateDialog(); + afx_msg void OnKeyboardChanged(); + afx_msg void OnKeyChoiceSelect(); + afx_msg void OnCommandKeySelChanged(); + afx_msg void OnCategorySelChanged(); + afx_msg void OnSearchTermChanged(); + afx_msg void OnChordWaitTimeChanged(); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnCheck() { OnSetKeyChoice(); }; + afx_msg void OnNotesRepeat(); + afx_msg void OnNoNotesRepeat(); + afx_msg void OnDeleteKeyChoice(); + afx_msg void OnRestoreKeyChoice(); + afx_msg void OnLoad(); + afx_msg void OnSave(); + afx_msg void OnClearLog(); + afx_msg void OnRestoreDefaultKeymap(); + afx_msg void OnClearHotKey(); + afx_msg void OnFindHotKey(); + afx_msg void OnDestroy(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.cpp new file mode 100644 index 00000000..af74514e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.cpp @@ -0,0 +1,62 @@ +/* + * LinkResolver.cpp + * ---------------- + * Purpose: Resolve Windows shell link targets + * 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 "LinkResolver.h" + +#include <atlconv.h> + +OPENMPT_NAMESPACE_BEGIN + +LinkResolver::LinkResolver() +{ + HRESULT result = CoCreateInstance(CLSID_ShellLink, + 0, + CLSCTX_INPROC_SERVER, + IID_IShellLink, + reinterpret_cast<LPVOID *>(&psl)); + if(SUCCEEDED(result) && psl != nullptr) + { + psl->QueryInterface(IID_IPersistFile, reinterpret_cast<LPVOID *>(&ppf)); + } +} + +LinkResolver::~LinkResolver() +{ + if(ppf != nullptr) + ppf->Release(); + if(psl != nullptr) + psl->Release(); +} + +mpt::PathString LinkResolver::Resolve(const TCHAR *inPath) +{ + if(ppf == nullptr) + return {}; + + SHFILEINFO info; + Clear(info); + if((SHGetFileInfo(inPath, 0, &info, sizeof(info), SHGFI_ATTRIBUTES) == 0) || !(info.dwAttributes & SFGAO_LINK)) + return {}; + + USES_CONVERSION; // T2COLE needs this + if(ppf != nullptr && SUCCEEDED(ppf->Load(T2COLE(inPath), STGM_READ))) + { + if(SUCCEEDED(psl->Resolve(AfxGetMainWnd()->m_hWnd, MAKELONG(SLR_ANY_MATCH | SLR_NO_UI | SLR_NOSEARCH, 100)))) + { + TCHAR outPath[MAX_PATH]; + psl->GetPath(outPath, mpt::saturate_cast<int>(std::size(outPath)), nullptr, 0); + return mpt::PathString::FromNative(outPath); + } + } + return {}; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.h b/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.h new file mode 100644 index 00000000..17cf42e0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/LinkResolver.h @@ -0,0 +1,33 @@ +/* + * LinkResolver.h + * -------------- + * Purpose: Resolve Windows shell link targets + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "../common/mptPathString.h" + +#include <ShObjIdl.h> + +OPENMPT_NAMESPACE_BEGIN + +class LinkResolver +{ + IShellLink *psl = nullptr; + IPersistFile *ppf = nullptr; + +public: + LinkResolver(); + ~LinkResolver(); + + + mpt::PathString Resolve(const TCHAR *inPath); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.cpp new file mode 100644 index 00000000..ef8bd8dc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.cpp @@ -0,0 +1,503 @@ +/* + * MIDIMacroDialog.cpp + * ------------------- + * Purpose: MIDI Macro Configuration Dialog + * 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/Reporting.h" +#include "../common/mptStringBuffer.h" +#include "Mainfrm.h" +#include "Mptrack.h" +#include "resource.h" +#include "MIDIMacroDialog.h" +#include "../soundlib/MIDIEvents.h" +#include "../soundlib/plugins/PlugInterface.h" + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(CMidiMacroSetup, CDialog) + ON_COMMAND(IDC_BUTTON1, &CMidiMacroSetup::OnSetAsDefault) + ON_COMMAND(IDC_BUTTON2, &CMidiMacroSetup::OnResetCfg) + ON_COMMAND(IDC_BUTTON3, &CMidiMacroSetup::OnMacroHelp) + ON_CBN_SELCHANGE(IDC_COMBO1, &CMidiMacroSetup::OnSFxChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CMidiMacroSetup::OnSFxPresetChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CMidiMacroSetup::OnZxxPresetChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &CMidiMacroSetup::UpdateZxxSelection) + ON_CBN_SELCHANGE(IDC_MACROPLUG, &CMidiMacroSetup::OnPlugChanged) + ON_CBN_SELCHANGE(IDC_MACROPARAM,&CMidiMacroSetup::OnPlugParamChanged) + ON_CBN_SELCHANGE(IDC_MACROCC, &CMidiMacroSetup::OnCCChanged) + ON_EN_CHANGE(IDC_EDIT1, &CMidiMacroSetup::OnSFxEditChanged) + ON_EN_CHANGE(IDC_EDIT2, &CMidiMacroSetup::OnZxxEditChanged) + ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT + kSFxMacros - 1, &CMidiMacroSetup::OnViewAllParams) + ON_COMMAND_RANGE(ID_PLUGSELECT + kSFxMacros, ID_PLUGSELECT + kSFxMacros + kSFxMacros - 1, &CMidiMacroSetup::OnSetSFx) +END_MESSAGE_MAP() + + +void CMidiMacroSetup::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO1, m_CbnSFx); + DDX_Control(pDX, IDC_COMBO2, m_CbnSFxPreset); + DDX_Control(pDX, IDC_COMBO3, m_CbnZxxPreset); + DDX_Control(pDX, IDC_COMBO4, m_CbnZxx); + DDX_Control(pDX, IDC_EDIT1, m_EditSFx); + DDX_Control(pDX, IDC_EDIT2, m_EditZxx); + DDX_Control(pDX, IDC_MACROPLUG, m_CbnMacroPlug); + DDX_Control(pDX, IDC_MACROPARAM, m_CbnMacroParam); + DDX_Control(pDX, IDC_MACROCC, m_CbnMacroCC); +} + + +BOOL CMidiMacroSetup::OnInitDialog() +{ + CString s; + CDialog::OnInitDialog(); + m_EditSFx.SetLimitText(kMacroLength - 1); + m_EditZxx.SetLimitText(kMacroLength - 1); + + // Parametered macro selection + for(int i = 0; i < 16; i++) + { + s.Format(_T("%d (SF%X)"), i, i); + m_CbnSFx.AddString(s); + } + + // Parametered macro presets + m_CbnSFx.SetCurSel(0); + for(int i = 0; i < kSFxMax; i++) + { + m_CbnSFxPreset.SetItemData(m_CbnSFxPreset.AddString(m_MidiCfg.GetParameteredMacroName(static_cast<ParameteredMacro>(i))), i); + } + OnSFxChanged(); + + // MIDI CC selection box + for (int cc = MIDIEvents::MIDICC_start; cc <= MIDIEvents::MIDICC_end; cc++) + { + s.Format(_T("CC %02d "), cc); + s += mpt::ToCString(mpt::Charset::UTF8, MIDIEvents::MidiCCNames[cc]); + m_CbnMacroCC.SetItemData(m_CbnMacroCC.AddString(s), cc); + } + + // Z80...ZFF box + for(int zxx = 0x80; zxx <= 0xFF; zxx++) + { + s.Format(_T("Z%02X"), zxx); + m_CbnZxx.AddString(s); + } + + // Fixed macro presets + m_CbnZxx.SetCurSel(0); + for(int i = 0; i < kZxxMax; i++) + { + m_CbnZxxPreset.SetItemData(m_CbnZxxPreset.AddString(m_MidiCfg.GetFixedMacroName(static_cast<FixedMacro>(i))), i); + } + m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType()); + + UpdateDialog(); + + auto ScalePixels = [&](auto x) { return Util::ScalePixels(x, m_hWnd); }; + int offsetx = ScalePixels(19), offsety = ScalePixels(30), separatorx = ScalePixels(4), separatory = ScalePixels(2); + int height = ScalePixels(18), widthMacro = ScalePixels(30), widthVal = ScalePixels(179), widthType = ScalePixels(135), widthBtn = ScalePixels(70); + + for(UINT m = 0; m < kSFxMacros; m++) + { + m_EditMacro[m].Create(_T(""), WS_CHILD | WS_VISIBLE | WS_TABSTOP, + CRect(offsetx, offsety + m * (separatory + height), offsetx + widthMacro, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m); + m_EditMacro[m].SetFont(GetFont()); + + m_EditMacroType[m].Create(ES_READONLY | WS_CHILD| WS_VISIBLE | WS_TABSTOP | WS_BORDER, + CRect(offsetx + separatorx + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m); + m_EditMacroType[m].SetFont(GetFont()); + + m_EditMacroValue[m].Create(ES_CENTER | ES_READONLY | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_BORDER, + CRect(offsetx + separatorx + widthType + widthMacro, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + kSFxMacros + m); + m_EditMacroValue[m].SetFont(GetFont()); + + m_BtnMacroShowAll[m].Create(_T("Show All..."), WS_CHILD | WS_TABSTOP | WS_VISIBLE, + CRect(offsetx + separatorx + widthType + widthMacro + widthVal, offsety + m * (separatory + height), offsetx + widthMacro + widthType + widthVal + widthBtn, offsety + m * (separatory + height) + height), this, ID_PLUGSELECT + m); + m_BtnMacroShowAll[m].SetFont(GetFont()); + } + UpdateMacroList(); + +#ifndef NO_PLUGINS + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + const SNDMIXPLUGIN &plugin = m_SndFile.m_MixPlugins[i]; + + if(plugin.IsValidPlugin()) + { + s.Format(_T("FX%d: "), i + 1); + s += mpt::ToCString(plugin.GetName()); + m_CbnMacroPlug.SetItemData(m_CbnMacroPlug.AddString(s), i); + } + } + m_CbnMacroPlug.SetCurSel(0); + OnPlugChanged(); +#endif // NO_PLUGINS + return FALSE; +} + + +// macro == -1 for updating all macros at once +void CMidiMacroSetup::UpdateMacroList(int macro) +{ + if(!m_EditMacro[0]) + { + // GUI not yet initialized + return; + } + + int start, end; + + if(macro >= 0 && macro < kSFxMacros) + { + start = end = macro; + } else + { + start = 0; + end = kSFxMacros - 1; + } + + CString s; + const int selectedMacro = m_CbnSFx.GetCurSel(); + + for(int m = start; m <= end; m++) + { + // SFx + s.Format(_T("SF%X"), static_cast<unsigned int>(m)); + m_EditMacro[m].SetWindowText(s); + + // Macro value: + m_EditMacroValue[m].SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[m])); + m_EditMacroValue[m].SetBackColor(m == selectedMacro ? RGB(200, 200, 225) : RGB(245, 245, 245)); + + // Macro Type: + const ParameteredMacro macroType = m_MidiCfg.GetParameteredMacroType(m); + switch(macroType) + { + case kSFxPlugParam: + s.Format(_T("Control Plugin Param %u"), static_cast<unsigned int>(m_MidiCfg.MacroToPlugParam(m))); + break; + + default: + s = m_MidiCfg.GetParameteredMacroName(m); + break; + } + m_EditMacroType[m].SetWindowText(s); + m_EditMacroType[m].SetBackColor(m == selectedMacro ? RGB(200,200,225) : RGB(245,245,245)); + + // Param details button: + m_BtnMacroShowAll[m].ShowWindow((macroType == kSFxPlugParam) ? SW_SHOW : SW_HIDE); + } +} + + +void CMidiMacroSetup::UpdateDialog() +{ + UINT sfx = m_CbnSFx.GetCurSel(); + UINT sfx_preset = static_cast<UINT>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel())); + if(sfx < m_MidiCfg.SFx.size()) + { + ToggleBoxes(sfx_preset, sfx); + m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.SFx[sfx])); + } + + UpdateZxxSelection(); + UpdateMacroList(); +} + + +void CMidiMacroSetup::OnSetAsDefault() +{ + theApp.SetDefaultMidiMacro(m_MidiCfg); +} + + +void CMidiMacroSetup::OnResetCfg() +{ + theApp.GetDefaultMidiMacro(m_MidiCfg); + m_CbnZxxPreset.SetCurSel(0); + OnSFxChanged(); +} + + +void CMidiMacroSetup::OnMacroHelp() +{ + Reporting::Information(_T("Valid characters in macros:\n\n" + "0-9, A-F - Raw hex data (4-Bit value)\n" + "c - MIDI channel (4-Bit value)\n" + "n - Note value\n\n" + "v - Note velocity\n" + "u - Computed note volume (including envelopes)\n\n" + "x - Note panning\n" + "y - Computed panning (including envelopes)\n\n" + "a - High byte of bank select\n" + "b - Low byte of bank select\n" + "p - Program select\n\n" + "h - Pattern channel\n" + "m - Sample loop direction\n" + "o - Last sample offset (Oxx / 9xx)\n" + "s - SysEx checksum (Roland)\n\n" + "z - Zxx parameter (00-7F)\n\n" + "Macros can be up to 31 characters long and contain multiple MIDI messages. SysEx messages are automatically terminated if not specified by the user."), + _T("OpenMPT MIDI Macro quick reference")); +} + + +void CMidiMacroSetup::OnSFxChanged() +{ + UINT sfx = m_CbnSFx.GetCurSel(); + if (sfx < 16) + { + int preset = m_MidiCfg.GetParameteredMacroType(sfx); + m_CbnSFxPreset.SetCurSel(preset); + } + UpdateDialog(); +} + + +void CMidiMacroSetup::OnSFxPresetChanged() +{ + UINT sfx = m_CbnSFx.GetCurSel(); + ParameteredMacro sfx_preset = static_cast<ParameteredMacro>(m_CbnSFxPreset.GetItemData(m_CbnSFxPreset.GetCurSel())); + + if (sfx < kSFxMacros) + { + if(sfx_preset != kSFxCustom) + { + m_MidiCfg.CreateParameteredMacro(sfx, sfx_preset); + } + UpdateDialog(); + } +} + + +void CMidiMacroSetup::OnZxxPresetChanged() +{ + FixedMacro zxxPreset = static_cast<FixedMacro>(m_CbnZxxPreset.GetItemData(m_CbnZxxPreset.GetCurSel())); + + if (zxxPreset != kZxxCustom) + { + m_MidiCfg.CreateFixedMacro(zxxPreset); + UpdateDialog(); + } +} + + +void CMidiMacroSetup::UpdateZxxSelection() +{ + UINT zxx = m_CbnZxx.GetCurSel(); + if(zxx < m_MidiCfg.Zxx.size()) + { + m_EditZxx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, m_MidiCfg.Zxx[zxx])); + } +} + + +void CMidiMacroSetup::OnSFxEditChanged() +{ + UINT sfx = m_CbnSFx.GetCurSel(); + if(sfx < m_MidiCfg.SFx.size()) + { + if(ValidateMacroString(m_EditSFx, m_MidiCfg.SFx[sfx], true)) + { + CString s; + m_EditSFx.GetWindowText(s); + m_MidiCfg.SFx[sfx] = mpt::ToCharset(mpt::Charset::ASCII, s); + + int sfx_preset = m_MidiCfg.GetParameteredMacroType(sfx); + m_CbnSFxPreset.SetCurSel(sfx_preset); + ToggleBoxes(sfx_preset, sfx); + UpdateMacroList(sfx); + } + } +} + + +void CMidiMacroSetup::OnZxxEditChanged() +{ + UINT zxx = m_CbnZxx.GetCurSel(); + if(zxx < m_MidiCfg.Zxx.size()) + { + if(ValidateMacroString(m_EditZxx, m_MidiCfg.Zxx[zxx], false)) + { + CString s; + m_EditZxx.GetWindowText(s); + m_MidiCfg.Zxx[zxx] = mpt::ToCharset(mpt::Charset::ASCII, s); + m_CbnZxxPreset.SetCurSel(m_MidiCfg.GetFixedMacroType()); + } + } +} + +void CMidiMacroSetup::OnSetSFx(UINT id) +{ + m_CbnSFx.SetCurSel(id - (ID_PLUGSELECT + kSFxMacros)); + OnSFxChanged(); +} + +void CMidiMacroSetup::OnViewAllParams(UINT id) +{ +#ifndef NO_PLUGINS + CString message, plugName; + int sfx = id - ID_PLUGSELECT; + PlugParamIndex param = m_MidiCfg.MacroToPlugParam(sfx); + message.Format(_T("These are the parameters that can be controlled by macro SF%X:\n\n"), sfx); + + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + IMixPlugin *pVstPlugin = m_SndFile.m_MixPlugins[plug].pMixPlugin; + if(pVstPlugin && param < pVstPlugin->GetNumParameters()) + { + plugName = mpt::ToCString(m_SndFile.m_MixPlugins[plug].GetName()); + message.AppendFormat(_T("FX%d: "), plug + 1); + message += plugName + _T("\t") + pVstPlugin->GetFormattedParamName(param) + _T("\n"); + } + } + + Reporting::Notification(message, _T("Macro -> Parameters")); +#endif // NO_PLUGINS +} + +void CMidiMacroSetup::OnPlugChanged() +{ +#ifndef NO_PLUGINS + DWORD_PTR plug = m_CbnMacroPlug.GetItemData(m_CbnMacroPlug.GetCurSel()); + + if(plug >= MAX_MIXPLUGINS) + return; + + IMixPlugin *pVstPlugin = m_SndFile.m_MixPlugins[plug].pMixPlugin; + if (pVstPlugin != nullptr) + { + m_CbnMacroParam.SetRedraw(FALSE); + m_CbnMacroParam.Clear(); + m_CbnMacroParam.ResetContent(); + AddPluginParameternamesToCombobox(m_CbnMacroParam, *pVstPlugin); + m_CbnMacroParam.SetRedraw(TRUE); + + int param = m_MidiCfg.MacroToPlugParam(m_CbnSFx.GetCurSel()); + m_CbnMacroParam.SetCurSel(param); + } +#endif // NO_PLUGINS +} + +void CMidiMacroSetup::OnPlugParamChanged() +{ + int param = static_cast<int>(m_CbnMacroParam.GetItemData(m_CbnMacroParam.GetCurSel())); + + if(param < 384) + { + const std::string macroText = m_MidiCfg.CreateParameteredMacro(kSFxPlugParam, param); + m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroText)); + } else + { + Reporting::Notification("Only parameters 0 to 383 can be controlled using MIDI Macros. Use Parameter Control Events to automate higher parameters."); + } +} + +void CMidiMacroSetup::OnCCChanged() +{ + int cc = static_cast<int>(m_CbnMacroCC.GetItemData(m_CbnMacroCC.GetCurSel())); + const std::string macroText = m_MidiCfg.CreateParameteredMacro(kSFxCC, cc); + m_EditSFx.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroText)); +} + +void CMidiMacroSetup::ToggleBoxes(UINT sfxPreset, UINT sfx) +{ + + if (sfxPreset == kSFxPlugParam) + { + m_CbnMacroCC.ShowWindow(FALSE); + m_CbnMacroPlug.ShowWindow(TRUE); + m_CbnMacroParam.ShowWindow(TRUE); + m_CbnMacroPlug.EnableWindow(TRUE); + m_CbnMacroParam.EnableWindow(TRUE); + SetDlgItemText(IDC_GENMACROLABEL, _T("Plugin/Param")); + m_CbnMacroParam.SetCurSel(m_MidiCfg.MacroToPlugParam(sfx)); + } else + { + m_CbnMacroPlug.EnableWindow(FALSE); + m_CbnMacroParam.EnableWindow(FALSE); + } + + if (sfxPreset == kSFxCC) + { + m_CbnMacroCC.EnableWindow(TRUE); + m_CbnMacroCC.ShowWindow(TRUE); + m_CbnMacroPlug.ShowWindow(FALSE); + m_CbnMacroParam.ShowWindow(FALSE); + SetDlgItemText(IDC_GENMACROLABEL, _T("MIDI CC")); + m_CbnMacroCC.SetCurSel(m_MidiCfg.MacroToMidiCC(sfx)); + } else + { + m_CbnMacroCC.EnableWindow(FALSE); + } +} + + +bool CMidiMacroSetup::ValidateMacroString(CEdit &wnd, const MIDIMacroConfig::Macro &prevMacro, bool isParametric) +{ + CString macroStrT; + wnd.GetWindowText(macroStrT); + std::string macroStr = mpt::ToCharset(mpt::Charset::ASCII, macroStrT); + + bool allowed = true, caseChange = false; + for(char &c : macroStr) + { + if(c == 'k' || c == 'K') // Previously, 'K' was used for MIDI channel + { + caseChange = true; + c = 'c'; + } else if(c >= 'd' && c <= 'f') // abc have special meanings, but def can be fixed + { + caseChange = true; + c = c - 'a' + 'A'; + } else if(c == 'M' || c == 'N' || c == 'O' || c == 'P' || c == 'S' || c == 'U' || c == 'V' || c == 'X' || c == 'Y' || c == 'Z') + { + caseChange = true; + c = c - 'A' + 'a'; + } else if(!( + (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'c') || + (c == 'h' || c == 'm' || c == 'n' || c == 'o' || c == 'p' || c == 's' ||c == 'u' || c == 'v' || c == 'x' || c == 'y' || c == ' ') || + (c == 'z' && isParametric))) + { + allowed = false; + break; + } + } + + if(!allowed) + { + // Replace text and keep cursor position if we just typed in an invalid character + if(prevMacro != std::string_view{macroStr}) + { + int start, end; + wnd.GetSel(start, end); + wnd.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, prevMacro)); + wnd.SetSel(start - 1, end - 1, true); + MessageBeep(MB_OK); + } + return false; + } else + { + if(caseChange) + { + // Replace text and keep cursor position if there was a case conversion + int start, end; + wnd.GetSel(start, end); + wnd.SetWindowText(mpt::ToCString(mpt::Charset::ASCII, macroStr)); + wnd.SetSel(start, end, true); + } + return true; + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.h new file mode 100644 index 00000000..cb842dc5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMacroDialog.h @@ -0,0 +1,66 @@ +/* + * MIDIMacroDialog.h + * ----------------- + * Purpose: MIDI Macro Configuration Dialog + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "ColourEdit.h" +#include "../common/misc_util.h" +#include "../soundlib/MIDIMacros.h" +#include "mpt/base/alloc.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CMidiMacroSetup: public CDialog +{ +protected: + CComboBox m_CbnSFx, m_CbnSFxPreset, m_CbnZxx, m_CbnZxxPreset, m_CbnMacroPlug, m_CbnMacroParam, m_CbnMacroCC; + CEdit m_EditSFx, m_EditZxx; + CColourEdit m_EditMacroValue[kSFxMacros], m_EditMacroType[kSFxMacros]; + CButton m_EditMacro[kSFxMacros], m_BtnMacroShowAll[kSFxMacros]; + + CSoundFile &m_SndFile; + +public: + CMidiMacroSetup(CSoundFile &sndFile, CWnd *parent = nullptr) : CDialog(IDD_MIDIMACRO, parent), m_SndFile(sndFile), m_vMidiCfg(sndFile.m_MidiCfg), m_MidiCfg(*m_vMidiCfg) { } +private: + mpt::heap_value<MIDIMacroConfig> m_vMidiCfg; +public: + MIDIMacroConfig & m_MidiCfg; + +protected: + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; + + bool ValidateMacroString(CEdit &wnd, const MIDIMacroConfig::Macro &prevMacro, bool isParametric); + + void UpdateMacroList(int macro=-1); + void ToggleBoxes(UINT preset, UINT sfx); + afx_msg void UpdateDialog(); + afx_msg void OnSetAsDefault(); + afx_msg void OnResetCfg(); + afx_msg void OnMacroHelp(); + afx_msg void OnSFxChanged(); + afx_msg void OnSFxPresetChanged(); + afx_msg void OnZxxPresetChanged(); + afx_msg void OnSFxEditChanged(); + afx_msg void OnZxxEditChanged(); + afx_msg void UpdateZxxSelection(); + afx_msg void OnPlugChanged(); + afx_msg void OnPlugParamChanged(); + afx_msg void OnCCChanged(); + + afx_msg void OnViewAllParams(UINT id); + afx_msg void OnSetSFx(UINT id); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.cpp new file mode 100644 index 00000000..0364fc4a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.cpp @@ -0,0 +1,172 @@ +/* + * MIDIMapping.cpp + * --------------- + * Purpose: MIDI Mapping management classes + * 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 "Moddoc.h" +#include "MIDIMapping.h" +#include "../common/FileReader.h" +#include "../soundlib/MIDIEvents.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +size_t CMIDIMapper::Serialize(std::ostream *file) const +{ + //Bytes: 1 Flags, 2 key, 1 plugindex, 1,2,4,8 plug/etc. + size_t size = 0; + for(const auto &d : m_Directives) + { + uint16 temp16 = (d.GetChnEvent() << 1) + (d.GetController() << 9); + if(d.GetAnyChannel()) temp16 |= 1; + uint32 temp32 = d.GetParamIndex(); + + uint8 temp8 = d.IsActive(); //bit 0 + if(d.GetCaptureMIDI()) temp8 |= (1 << 1); //bit 1 + //bits 2-4: Mapping type: 0 for plug param control. + //bit 5: + if(d.GetAllowPatternEdit()) temp8 |= (1 << 5); + //bits 6-7: Size: 5, 6, 8, 12 + + uint8 parambytes = 4; + if(temp32 <= uint16_max) + { + if(temp32 <= uint8_max) parambytes = 1; + else {parambytes = 2; temp8 |= (1 << 6);} + } + else temp8 |= (2 << 6); + + if(file) + { + std::ostream & f = *file; + mpt::IO::WriteIntLE<uint8>(f, temp8); + mpt::IO::WriteIntLE<uint16>(f, temp16); + mpt::IO::WriteIntLE<uint8>(f, d.GetPlugIndex()); + mpt::IO::WritePartial<uint32le>(f, mpt::as_le(temp32), parambytes); + } + size += sizeof(temp8) + sizeof(temp16) + sizeof(temp8) + parambytes; + } + return size; +} + + +bool CMIDIMapper::Deserialize(FileReader &file) +{ + m_Directives.clear(); + while(file.CanRead(1)) + { + uint8 i8 = file.ReadUint8(); + uint8 psize = 0; + // Determine size of this event (depends on size of plugin parameter index) + switch(i8 >> 6) + { + case 0: psize = 4; break; + case 1: psize = 5; break; + case 2: psize = 7; break; + case 3: default: psize = 11; break; + } + + if(!file.CanRead(psize)) return false; + if(((i8 >> 2) & 7) != 0) { file.Skip(psize); continue;} //Skipping unrecognised mapping types. + + CMIDIMappingDirective s; + s.SetActive((i8 & 1) != 0); + s.SetCaptureMIDI((i8 & (1 << 1)) != 0); + s.SetAllowPatternEdit((i8 & (1 << 5)) != 0); + uint16 i16 = file.ReadUint16LE(); //Channel, event, MIDIbyte1. + i8 = file.ReadUint8(); //Plugindex + uint32le i32; + file.ReadStructPartial(i32, psize - 3); + + s.SetChannel(((i16 & 1) != 0) ? 0 : 1 + ((i16 >> 1) & 0xF)); + s.SetEvent(static_cast<uint8>((i16 >> 5) & 0xF)); + s.SetController(i16 >> 9); + s.SetPlugIndex(i8); + s.SetParamIndex(i32); + AddDirective(s); + } + + return true; +} + + +bool CMIDIMapper::OnMIDImsg(const DWORD midimsg, PLUGINDEX &mappedIndex, PlugParamIndex ¶mindex, uint16 ¶mval) +{ + const MIDIEvents::EventType eventType = MIDIEvents::GetTypeFromEvent(midimsg); + const uint8 controller = MIDIEvents::GetDataByte1FromEvent(midimsg); + const uint8 channel = MIDIEvents::GetChannelFromEvent(midimsg) & 0x7F; + const uint8 controllerVal = MIDIEvents::GetDataByte2FromEvent(midimsg) & 0x7F; + + for(const auto &d : m_Directives) + { + if(!d.IsActive()) continue; + if(d.GetEvent() != eventType) continue; + if(eventType == MIDIEvents::evControllerChange + && d.GetController() != controller + && (d.GetController() >= 32 || d.GetController() + 32 != controller)) + continue; + if(!d.GetAnyChannel() && channel + 1 != d.GetChannel()) continue; + + const PLUGINDEX plugindex = d.GetPlugIndex(); + const uint32 param = d.GetParamIndex(); + uint16 val = (d.GetEvent() == MIDIEvents::evChannelAftertouch ? controller : controllerVal) << 7; + + if(eventType == MIDIEvents::evControllerChange) + { + // Fine (0...31) / Coarse (32...63) controller pairs - Fine should be sent first. + if(controller == m_lastCC + 32 && m_lastCC < 32) + { + val = (val >> 7) | m_lastCCvalue; + } + m_lastCC = controller; + m_lastCCvalue = val; + } + + if(d.GetAllowPatternEdit()) + { + mappedIndex = plugindex; + paramindex = param; + paramval = val; + } + + if(plugindex > 0 && plugindex <= MAX_MIXPLUGINS) + { +#ifndef NO_PLUGINS + IMixPlugin *pPlug = m_rSndFile.m_MixPlugins[plugindex - 1].pMixPlugin; + if(!pPlug) continue; + pPlug->SetParameter(param, val / 16383.0f); + if(m_rSndFile.GetpModDoc() != nullptr) + m_rSndFile.GetpModDoc()->SetModified(); +#endif // NO_PLUGINS + } + if(d.GetCaptureMIDI()) + { + return true; + } + } + + return false; +} + + +void CMIDIMapper::Swap(const size_t a, const size_t b) +{ + if(a < m_Directives.size() && b < m_Directives.size()) + { + std::swap(m_Directives[a], m_Directives[b]); + Sort(); + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.h b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.h new file mode 100644 index 00000000..b7d39c0e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMapping.h @@ -0,0 +1,128 @@ +/* + * MIDIMapping.h + * ------------- + * Purpose: MIDI Mapping management classes + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <vector> +#include <algorithm> + +#include "../common/FileReaderFwd.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class CMIDIMappingDirective +{ +public: + CMIDIMappingDirective() + : m_Active(true), m_CaptureMIDI(false), m_AllowPatternEdit(true), m_AnyChannel(true) + { + } + + void SetActive(const bool b) { m_Active = b; } + bool IsActive() const { return m_Active; } + + void SetCaptureMIDI(const bool b) { m_CaptureMIDI = b; } + bool GetCaptureMIDI() const { return m_CaptureMIDI; } + + void SetAllowPatternEdit(const bool b) { m_AllowPatternEdit = b; } + bool GetAllowPatternEdit() const { return m_AllowPatternEdit; } + + bool GetAnyChannel() const { return m_AnyChannel; } + + //Note: In these functions, channel value is in range [1,16], + //GetChannel() returns 0 on 'any channel'. + void SetChannel(const int c) { if(c < 1 || c > 16) m_AnyChannel = true; else { m_ChnEvent &= ~0x0F; m_ChnEvent |= c - 1; m_AnyChannel = false; } } + uint8 GetChannel() const {return (m_AnyChannel) ? 0 : (m_ChnEvent & 0xF) + 1;} + + void SetEvent(uint8 e) { if(e > 15) e = 15; m_ChnEvent &= ~0xF0; m_ChnEvent |= (e << 4); } + uint8 GetEvent() const {return (m_ChnEvent >> 4) & 0x0F;} + + void SetController(int controller) { if(controller > 127) controller = 127; m_MIDIByte1 = static_cast<uint8>(controller); } + uint8 GetController() const { return m_MIDIByte1; } + + //Note: Plug index starts from 1. + void SetPlugIndex(const int i) { m_PluginIndex = static_cast<PLUGINDEX>(i); } + PLUGINDEX GetPlugIndex() const { return m_PluginIndex; } + + void SetParamIndex(const int i) { m_Parameter = i; } + uint32 GetParamIndex() const { return m_Parameter; } + + bool IsDefault() const { return *this == CMIDIMappingDirective{}; } + + bool operator==(const CMIDIMappingDirective &other) const { return memcmp(this, &other, sizeof(*this)) == 0; } + bool operator<(const CMIDIMappingDirective &other) const { return GetController() < other.GetController(); } + + uint8 GetChnEvent() const {return m_ChnEvent;} + +private: + uint32 m_Parameter = 0; + PLUGINDEX m_PluginIndex = 1; + uint8 m_MIDIByte1 = 0; + uint8 m_ChnEvent = (0xB << 4); // 0-3 channel, 4-7 event + bool m_Active : 1; + bool m_CaptureMIDI : 1; // When true, MIDI data should not be processed beyond this directive + bool m_AllowPatternEdit : 1; // When true, the mapping can be used for modifying pattern. + bool m_AnyChannel : 1; +}; + +class CSoundFile; + + +class CMIDIMapper +{ +public: + CMIDIMapper(CSoundFile& sndfile) : m_rSndFile(sndfile) {} + + // If mapping found: + // - mappedIndex is set to mapped value(plug index) + // - paramindex to mapped parameter + // - paramvalue to parameter value. + // In case of multiple mappings, these get the values from the last mapping found. + // Returns true if MIDI was 'captured' by some directive, false otherwise. + bool OnMIDImsg(const DWORD midimsg, PLUGINDEX &mappedIndex, PlugParamIndex ¶mindex, uint16 ¶mvalue); + + // Swaps the positions of two elements. + void Swap(const size_t a, const size_t b); + + // Return the index after sorting for the added element + size_t SetDirective(const size_t i, const CMIDIMappingDirective& d) { m_Directives[i] = d; Sort(); return std::find(m_Directives.begin(), m_Directives.end(), d) - m_Directives.begin(); } + + // Return the index after sorting for the added element + size_t AddDirective(const CMIDIMappingDirective& d) { m_Directives.push_back(d); Sort(); return std::find(m_Directives.begin(), m_Directives.end(), d) - m_Directives.begin(); } + + void RemoveDirective(const size_t i) { m_Directives.erase(m_Directives.begin() + i); } + + const CMIDIMappingDirective &GetDirective(const size_t i) const { return m_Directives[i]; } + + size_t GetCount() const { return m_Directives.size(); } + + // Serialize to file, or just return the serialization size if no file handle is provided. + size_t Serialize(std::ostream *file = nullptr) const; + // Deserialize MIDI Mappings from file. Returns true if no errors were encountered. + bool Deserialize(FileReader &file); + + bool AreOrderEqual(const size_t a, const size_t b) const { return !(m_Directives[a] < m_Directives[b] || m_Directives[b] < m_Directives[a]); } + +private: + void Sort() { std::stable_sort(m_Directives.begin(), m_Directives.end()); } + +private: + CSoundFile &m_rSndFile; + std::vector<CMIDIMappingDirective> m_Directives; + uint16 m_lastCCvalue = 0; + uint8 m_lastCC = uint8_max; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.cpp new file mode 100644 index 00000000..c53f702b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.cpp @@ -0,0 +1,506 @@ +/* + * MIDIMappingDialog.cpp + * --------------------- + * Purpose: Implementation of OpenMPT's MIDI mapping dialog, for mapping incoming MIDI messages to plugin parameters. + * 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 "Mainfrm.h" +#include "Moddoc.h" +#include "MIDIMappingDialog.h" +#include "InputHandler.h" +#include "../soundlib/MIDIEvents.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../common/mptStringBuffer.h" + +#ifndef NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + + +CMIDIMappingDialog::CMIDIMappingDialog(CWnd *pParent, CSoundFile &rSndfile) + : CDialog(IDD_MIDIPARAMCONTROL, pParent) + , m_sndFile(rSndfile) + , m_rMIDIMapper(m_sndFile.GetMIDIMapper()) +{ + CMainFrame::GetInputHandler()->Bypass(true); + oldMIDIRecondWnd = CMainFrame::GetMainFrame()->GetMidiRecordWnd(); +} + + +CMIDIMappingDialog::~CMIDIMappingDialog() +{ + CMainFrame::GetMainFrame()->SetMidiRecordWnd(oldMIDIRecondWnd); + CMainFrame::GetInputHandler()->Bypass(false); +} + + +void CMIDIMappingDialog::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO_CONTROLLER, m_ControllerCBox); + DDX_Control(pDX, IDC_COMBO_PLUGIN, m_PluginCBox); + DDX_Control(pDX, IDC_COMBO_PARAM, m_PlugParamCBox); + DDX_Control(pDX, IDC_LIST1, m_List); + DDX_Control(pDX, IDC_COMBO_CHANNEL, m_ChannelCBox); + DDX_Control(pDX, IDC_COMBO_EVENT, m_EventCBox); + DDX_Control(pDX, IDC_SPINMOVEMAPPING, m_SpinMoveMapping); +} + + +BEGIN_MESSAGE_MAP(CMIDIMappingDialog, CDialog) + ON_NOTIFY(LVN_ITEMCHANGED, IDC_LIST1, &CMIDIMappingDialog::OnSelectionChanged) + ON_BN_CLICKED(IDC_CHECKACTIVE, &CMIDIMappingDialog::OnBnClickedCheckactive) + ON_BN_CLICKED(IDC_CHECKCAPTURE, &CMIDIMappingDialog::OnBnClickedCheckCapture) + ON_CBN_SELCHANGE(IDC_COMBO_CONTROLLER, &CMIDIMappingDialog::OnCbnSelchangeComboController) + ON_CBN_SELCHANGE(IDC_COMBO_CHANNEL, &CMIDIMappingDialog::OnCbnSelchangeComboChannel) + ON_CBN_SELCHANGE(IDC_COMBO_PLUGIN, &CMIDIMappingDialog::OnCbnSelchangeComboPlugin) + ON_CBN_SELCHANGE(IDC_COMBO_PARAM, &CMIDIMappingDialog::OnCbnSelchangeComboParam) + ON_CBN_SELCHANGE(IDC_COMBO_EVENT, &CMIDIMappingDialog::OnCbnSelchangeComboEvent) + ON_BN_CLICKED(IDC_BUTTON_ADD, &CMIDIMappingDialog::OnBnClickedButtonAdd) + ON_BN_CLICKED(IDC_BUTTON_REPLACE, &CMIDIMappingDialog::OnBnClickedButtonReplace) + ON_BN_CLICKED(IDC_BUTTON_REMOVE, &CMIDIMappingDialog::OnBnClickedButtonRemove) + ON_MESSAGE(WM_MOD_MIDIMSG, &CMIDIMappingDialog::OnMidiMsg) + ON_NOTIFY(UDN_DELTAPOS, IDC_SPINMOVEMAPPING, &CMIDIMappingDialog::OnDeltaposSpinmovemapping) + ON_BN_CLICKED(IDC_CHECK_PATRECORD, &CMIDIMappingDialog::OnBnClickedCheckPatRecord) + + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CMIDIMappingDialog::OnToolTipNotify) +END_MESSAGE_MAP() + + +LRESULT CMIDIMappingDialog::OnMidiMsg(WPARAM dwMidiDataParam, LPARAM) +{ + uint32 midiData = static_cast<uint32>(dwMidiDataParam); + if(IsDlgButtonChecked(IDC_CHECK_MIDILEARN)) + { + for(int i = 0; i < m_EventCBox.GetCount(); i++) + { + if(static_cast<MIDIEvents::EventType>(m_EventCBox.GetItemData(i)) == MIDIEvents::GetTypeFromEvent(midiData)) + { + m_ChannelCBox.SetCurSel(1 + MIDIEvents::GetChannelFromEvent(midiData)); + m_EventCBox.SetCurSel(i); + if(MIDIEvents::GetTypeFromEvent(midiData) == MIDIEvents::evControllerChange) + { + uint8 cc = MIDIEvents::GetDataByte1FromEvent(midiData); + if(m_lastCC >= 32 || cc != m_lastCC + 32) + { + // Ignore second CC message of 14-bit CC. + m_ControllerCBox.SetCurSel(cc); + } + m_lastCC = cc; + } + OnCbnSelchangeComboChannel(); + OnCbnSelchangeComboEvent(); + OnCbnSelchangeComboController(); + break; + } + } + } + return 1; +} + + +BOOL CMIDIMappingDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + + // Add events + m_EventCBox.SetItemData(m_EventCBox.AddString(_T("Controller Change")), MIDIEvents::evControllerChange); + m_EventCBox.SetItemData(m_EventCBox.AddString(_T("Polyphonic Aftertouch")), MIDIEvents::evPolyAftertouch); + m_EventCBox.SetItemData(m_EventCBox.AddString(_T("Channel Aftertouch")), MIDIEvents::evChannelAftertouch); + + // Add controller names + CString s; + for(uint8 i = MIDIEvents::MIDICC_start; i <= MIDIEvents::MIDICC_end; i++) + { + s.Format(_T("%3u "), i); + s += mpt::ToCString(mpt::Charset::UTF8, MIDIEvents::MidiCCNames[i]); + m_ControllerCBox.AddString(s); + } + + // Add plugin names + AddPluginNamesToCombobox(m_PluginCBox, m_sndFile.m_MixPlugins); + + // Initialize mapping table + static constexpr CListCtrlEx::Header headers[] = + { + { _T("Channel"), 58, LVCFMT_LEFT }, + { _T("Event / Controller"), 176, LVCFMT_LEFT }, + { _T("Plugin"), 120, LVCFMT_LEFT }, + { _T("Parameter"), 120, LVCFMT_LEFT }, + { _T("Capture"), 40, LVCFMT_LEFT }, + { _T("Pattern Record"), 40, LVCFMT_LEFT } + }; + m_List.SetHeaders(headers); + m_List.SetExtendedStyle(m_List.GetExtendedStyle() | LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT); + + // Add directives to list + for(size_t i = 0; i < m_rMIDIMapper.GetCount(); i++) + { + InsertItem(m_rMIDIMapper.GetDirective(i), int(i)); + } + + if(m_rMIDIMapper.GetCount() > 0 && m_Setting.IsDefault()) + { + SelectItem(0); + OnSelectionChanged(); + } else + { + UpdateDialog(); + } + + GetDlgItem(IDC_CHECK_PATRECORD)->EnableWindow((m_sndFile.GetType() == MOD_TYPE_MPT) ? TRUE : FALSE); + + CMainFrame::GetMainFrame()->SetMidiRecordWnd(GetSafeHwnd()); + + CheckDlgButton(IDC_CHECK_MIDILEARN, BST_CHECKED); + EnableToolTips(TRUE); + + return TRUE; // return TRUE unless you set the focus to a control +} + + +int CMIDIMappingDialog::InsertItem(const CMIDIMappingDirective &m, int insertAt) +{ + CString s; + if(m.GetAnyChannel()) + s = _T("Any"); + else + s.Format(_T("Ch %u"), m.GetChannel()); + + insertAt = m_List.InsertItem(insertAt, s); + if(insertAt == -1) + return -1; + m_List.SetCheck(insertAt, m.IsActive() ? TRUE : FALSE); + + switch(m.GetEvent()) + { + case MIDIEvents::evControllerChange: + s.Format(_T("CC %u: "), m.GetController()); + if(m.GetController() <= MIDIEvents::MIDICC_end) s += mpt::ToCString(mpt::Charset::UTF8, MIDIEvents::MidiCCNames[m.GetController()]); + break; + case MIDIEvents::evPolyAftertouch: + s = _T("Polyphonic Aftertouch"); break; + case MIDIEvents::evChannelAftertouch: + s = _T("Channel Aftertouch"); break; + default: + s.Format(_T("0x%02X"), m.GetEvent()); break; + } + m_List.SetItemText(insertAt, 1, s); + + const PLUGINDEX plugindex = m.GetPlugIndex(); + if(plugindex > 0 && plugindex < MAX_MIXPLUGINS) + { + const SNDMIXPLUGIN &plug = m_sndFile.m_MixPlugins[plugindex - 1]; + s.Format(_T("FX%u: "), plugindex); + s += mpt::ToCString(plug.GetName()); + m_List.SetItemText(insertAt, 2, s); + if(plug.pMixPlugin != nullptr) + s = plug.pMixPlugin->GetFormattedParamName(m.GetParamIndex()); + else + s.Empty(); + m_List.SetItemText(insertAt, 3, s); + } + m_List.SetItemText(insertAt, 4, m.GetCaptureMIDI() ? _T("Capt") : _T("")); + m_List.SetItemText(insertAt, 5, m.GetAllowPatternEdit() ? _T("Rec") : _T("")); + + return insertAt; +} + + +void CMIDIMappingDialog::SelectItem(int i) +{ + m_List.SetItemState(i, LVIS_SELECTED, LVIS_SELECTED); + m_List.SetSelectionMark(i); +} + + +void CMIDIMappingDialog::UpdateDialog(int selItem) +{ + CheckDlgButton(IDC_CHECKACTIVE, m_Setting.IsActive() ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECKCAPTURE, m_Setting.GetCaptureMIDI() ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK_PATRECORD, m_Setting.GetAllowPatternEdit() ? BST_CHECKED : BST_UNCHECKED); + + m_ChannelCBox.SetCurSel(m_Setting.GetChannel()); + + m_EventCBox.SetCurSel(-1); + for(int i = 0; i < m_EventCBox.GetCount(); i++) + { + if(m_EventCBox.GetItemData(i) == m_Setting.GetEvent()) + { + m_EventCBox.SetCurSel(i); + break; + } + } + + m_ControllerCBox.SetCurSel(m_Setting.GetController()); + m_PluginCBox.SetCurSel(m_Setting.GetPlugIndex() - 1); + m_PlugParamCBox.SetCurSel(m_Setting.GetParamIndex()); + + UpdateEvent(); + UpdateParameters(); + + bool enableMover = selItem >= 0; + if(enableMover) + { + const bool previousEqual = (selItem > 0 && m_rMIDIMapper.AreOrderEqual(selItem - 1, selItem)); + const bool nextEqual = (selItem + 1 < m_List.GetItemCount() && m_rMIDIMapper.AreOrderEqual(selItem, selItem + 1)); + enableMover = previousEqual || nextEqual; + } + m_SpinMoveMapping.EnableWindow(enableMover); +} + + +void CMIDIMappingDialog::UpdateEvent() +{ + m_ControllerCBox.EnableWindow(m_Setting.GetEvent() == MIDIEvents::evControllerChange ? TRUE : FALSE); + if(m_Setting.GetEvent() != MIDIEvents::evControllerChange) + m_ControllerCBox.SetCurSel(0); +} + + +void CMIDIMappingDialog::UpdateParameters() +{ + m_PlugParamCBox.SetRedraw(FALSE); + m_PlugParamCBox.ResetContent(); + AddPluginParameternamesToCombobox(m_PlugParamCBox, m_sndFile.m_MixPlugins[m_Setting.GetPlugIndex() - 1]); + m_PlugParamCBox.SetCurSel(m_Setting.GetParamIndex()); + m_PlugParamCBox.SetRedraw(TRUE); + m_PlugParamCBox.Invalidate(); +} + + +void CMIDIMappingDialog::OnSelectionChanged(NMHDR *pNMHDR, LRESULT * /*pResult*/) +{ + int i; + if(pNMHDR != nullptr) + { + NMLISTVIEW *nmlv = (NMLISTVIEW *)pNMHDR; + + if(((nmlv->uOldState ^ nmlv->uNewState) & INDEXTOSTATEIMAGEMASK(3)) != 0 && nmlv->uOldState != 0) + { + // Check box status changed + CMIDIMappingDirective m = m_rMIDIMapper.GetDirective(nmlv->iItem); + m.SetActive(nmlv->uNewState == INDEXTOSTATEIMAGEMASK(2)); + m_rMIDIMapper.SetDirective(nmlv->iItem, m); + SetModified(); + if(nmlv->iItem == m_List.GetSelectionMark()) + CheckDlgButton(IDC_CHECKACTIVE, nmlv->uNewState == INDEXTOSTATEIMAGEMASK(2) ? BST_CHECKED : BST_UNCHECKED); + } + + if(nmlv->uNewState & LVIS_SELECTED) + i = nmlv->iItem; + else + return; + } else + { + i = m_List.GetSelectionMark(); + } + if(i < 0 || (size_t)i >= m_rMIDIMapper.GetCount()) return; + m_Setting = m_rMIDIMapper.GetDirective(i); + UpdateDialog(i); +} + + +void CMIDIMappingDialog::OnBnClickedCheckactive() +{ + m_Setting.SetActive(IsDlgButtonChecked(IDC_CHECKACTIVE) == BST_CHECKED); +} + + +void CMIDIMappingDialog::OnBnClickedCheckCapture() +{ + m_Setting.SetCaptureMIDI(IsDlgButtonChecked(IDC_CHECKCAPTURE) == BST_CHECKED); +} + + +void CMIDIMappingDialog::OnBnClickedCheckPatRecord() +{ + m_Setting.SetAllowPatternEdit(IsDlgButtonChecked(IDC_CHECK_PATRECORD) == BST_CHECKED); +} + + +void CMIDIMappingDialog::OnCbnSelchangeComboController() +{ + m_Setting.SetController(m_ControllerCBox.GetCurSel()); +} + + +void CMIDIMappingDialog::OnCbnSelchangeComboChannel() +{ + m_Setting.SetChannel(m_ChannelCBox.GetCurSel()); +} + + +void CMIDIMappingDialog::OnCbnSelchangeComboPlugin() +{ + int i = m_PluginCBox.GetCurSel(); + if(i < 0 || i >= MAX_MIXPLUGINS) return; + m_Setting.SetPlugIndex(i+1); + UpdateParameters(); +} + + +void CMIDIMappingDialog::OnCbnSelchangeComboParam() +{ + m_Setting.SetParamIndex(m_PlugParamCBox.GetCurSel()); +} + + +void CMIDIMappingDialog::OnCbnSelchangeComboEvent() +{ + uint8 eventType = static_cast<uint8>(m_EventCBox.GetItemData(m_EventCBox.GetCurSel())); + m_Setting.SetEvent(eventType); + UpdateEvent(); +} + + +void CMIDIMappingDialog::OnBnClickedButtonAdd() +{ + if(m_sndFile.GetModSpecifications().MIDIMappingDirectivesMax <= m_rMIDIMapper.GetCount()) + { + Reporting::Information("Maximum amount of MIDI Mapping directives reached."); + } else + { + const size_t i = m_rMIDIMapper.AddDirective(m_Setting); + SetModified(); + + SelectItem(InsertItem(m_Setting, static_cast<int>(i))); + OnSelectionChanged(); + } +} + + +void CMIDIMappingDialog::OnBnClickedButtonReplace() +{ + const int i = m_List.GetSelectionMark(); + if(i >= 0 && (size_t)i < m_rMIDIMapper.GetCount()) + { + const size_t newIndex = m_rMIDIMapper.SetDirective(i, m_Setting); + SetModified(); + + m_List.DeleteItem(i); + SelectItem(InsertItem(m_Setting, static_cast<int>(newIndex))); + OnSelectionChanged(); + } +} + + +void CMIDIMappingDialog::OnBnClickedButtonRemove() +{ + int i = m_List.GetSelectionMark(); + if(i >= 0 && (size_t)i < m_rMIDIMapper.GetCount()) + { + m_rMIDIMapper.RemoveDirective(i); + SetModified(); + + m_List.DeleteItem(i); + if(m_List.GetItemCount() > 0) + { + if(i < m_List.GetItemCount()) + SelectItem(i); + else + SelectItem(i - 1); + } + i = m_List.GetSelectionMark(); + if(i >= 0 && (size_t)i < m_rMIDIMapper.GetCount()) + m_Setting = m_rMIDIMapper.GetDirective(i); + + OnSelectionChanged(); + } +} + + +void CMIDIMappingDialog::OnDeltaposSpinmovemapping(NMHDR *pNMHDR, LRESULT *pResult) +{ + const int index = m_List.GetSelectionMark(); + if(index < 0 || index >= m_List.GetItemCount()) return; + + LPNMUPDOWN pNMUpDown = reinterpret_cast<LPNMUPDOWN>(pNMHDR); + + int newIndex = -1; + if(pNMUpDown->iDelta < 0) //Up + { + if(index - 1 >= 0 && m_rMIDIMapper.AreOrderEqual(index-1, index)) + { + newIndex = index - 1; + } + } else //Down + { + if(index + 1 < m_List.GetItemCount() && m_rMIDIMapper.AreOrderEqual(index, index+1)) + { + newIndex = index + 1; + } + } + + if(newIndex != -1) + { + m_rMIDIMapper.Swap(size_t(newIndex), size_t(index)); + m_List.DeleteItem(index); + InsertItem(m_rMIDIMapper.GetDirective(newIndex), newIndex); + SelectItem(newIndex); + } + + *pResult = 0; +} + + +BOOL CMIDIMappingDialog::OnToolTipNotify(UINT, NMHDR * pNMHDR, LRESULT *) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT*)pNMHDR; + const TCHAR *text = _T(""); + UINT_PTR nID = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = ::GetDlgCtrlID((HWND)nID); + } + + switch(nID) + { + case IDC_CHECKCAPTURE: + text = _T("The event is not passed to any further MIDI mappings or recording facilities."); + break; + case IDC_CHECKACTIVE: + text = _T("The MIDI mapping is enabled and can be processed."); + break; + case IDC_CHECK_PATRECORD: + text = _T("Parameter changes are recorded into patterns as Parameter Control events."); + break; + case IDC_CHECK_MIDILEARN: + text = _T("Listens to incoming MIDI data to automatically fill in the appropriate data."); + break; + case IDC_SPINMOVEMAPPING: + text = _T("Change the processing order of the current selected MIDI mapping."); + break; + case IDC_COMBO_CHANNEL: + text = _T("The MIDI channel to listen on for this event."); + break; + case IDC_COMBO_EVENT: + text = _T("The MIDI event to listen for."); + break; + case IDC_COMBO_CONTROLLER: + text = _T("The MIDI controler to listen for."); + break; + } + + mpt::String::WriteWinBuf(pTTT->szText) = mpt::winstring(text); + return TRUE; +} + + +void CMIDIMappingDialog::SetModified() +{ + if(m_sndFile.GetpModDoc() != nullptr) + m_sndFile.GetpModDoc()->SetModified(); +} + + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.h new file mode 100644 index 00000000..d7a74934 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MIDIMappingDialog.h @@ -0,0 +1,85 @@ +/* + * MIDIMappingDialog.h + * ------------------- + * Purpose: Implementation of OpenMPT's MIDI mapping dialog, for mapping incoming MIDI messages to plugin parameters. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS + +#include "MIDIMapping.h" +#include "CListCtrl.h" + + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; +class CMIDIMapper; + +class CMIDIMappingDialog : public CDialog +{ +public: + CMIDIMappingDirective m_Setting; + +protected: + CSoundFile &m_sndFile; + CMIDIMapper &m_rMIDIMapper; + HWND oldMIDIRecondWnd; + + // Dialog Data + CComboBox m_ControllerCBox; + CComboBox m_PluginCBox; + CComboBox m_PlugParamCBox; + CComboBox m_ChannelCBox; + CComboBox m_EventCBox; + CListCtrlEx m_List; + CSpinButtonCtrl m_SpinMoveMapping; + + uint8 m_lastCC = uint8_max; + +public: + CMIDIMappingDialog(CWnd *pParent, CSoundFile &rSndfile); + ~CMIDIMappingDialog(); + +protected: + void UpdateDialog(int selItem = -1); + void UpdateEvent(); + void UpdateParameters(); + int InsertItem(const CMIDIMappingDirective &m, int insertAt); + void SelectItem(int i); + + void SetModified(); + + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + + DECLARE_MESSAGE_MAP() + + afx_msg void OnSelectionChanged(NMHDR *pNMHDR = nullptr, LRESULT *pResult = nullptr); + afx_msg BOOL OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult); + + afx_msg void OnBnClickedCheckactive(); + afx_msg void OnBnClickedCheckCapture(); + afx_msg void OnCbnSelchangeComboController(); + afx_msg void OnCbnSelchangeComboChannel(); + afx_msg void OnCbnSelchangeComboPlugin(); + afx_msg void OnCbnSelchangeComboParam(); + afx_msg void OnCbnSelchangeComboEvent(); + afx_msg void OnBnClickedButtonAdd(); + afx_msg void OnBnClickedButtonReplace(); + afx_msg void OnBnClickedButtonRemove(); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + afx_msg void OnDeltaposSpinmovemapping(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnBnClickedCheckPatRecord(); +}; + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp new file mode 100644 index 00000000..27446f92 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTHacks.cpp @@ -0,0 +1,482 @@ +/* + * MPTHacks.cpp + * ------------ + * Purpose: Find out if MOD/XM/S3M/IT modules have MPT-specific hacks and fix them. + * Notes : This is not finished yet. Still need to handle: + * - Out-of-range sample pre-amp settings + * - Comments in XM files + * - Many auto-fix actions (so that the auto-fix mode can actually be used at some point!) + * Maybe there should be two options if hacks are found: Convert the song to MPTM or remove hacks. + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Moddoc.h" +#include "../soundlib/modsmp_ctrl.h" +#include "../soundlib/mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Find and fix envelopes where two nodes are on the same tick. +bool FindIncompatibleEnvelopes(InstrumentEnvelope &env, bool autofix) +{ + bool found = false; + for(uint32 i = 1; i < env.size(); i++) + { + if(env[i].tick <= env[i - 1].tick) // "<=" so we can fix envelopes "on the fly" + { + found = true; + if(autofix) + { + env[i].tick = env[i - 1].tick + 1; + } + } + } + return found; +} + + +// Go through the module to find out if it contains any hacks introduced by (Open)MPT +bool CModDoc::HasMPTHacks(const bool autofix) +{ + const CModSpecifications *originalSpecs = &m_SndFile.GetModSpecifications(); + // retrieve original (not hacked) specs. + MODTYPE modType = m_SndFile.GetBestSaveFormat(); + switch(modType) + { + case MOD_TYPE_MOD: + originalSpecs = &ModSpecs::mod; + break; + case MOD_TYPE_XM: + originalSpecs = &ModSpecs::xm; + break; + case MOD_TYPE_S3M: + originalSpecs = &ModSpecs::s3m; + break; + case MOD_TYPE_IT: + originalSpecs = &ModSpecs::it; + break; + } + + bool foundHacks = false, foundHere = false; + ClearLog(); + + // Check for plugins +#ifndef NO_PLUGINS + foundHere = false; + for(const auto &plug : m_SndFile.m_MixPlugins) + { + if(plug.IsValidPlugin()) + { + foundHere = foundHacks = true; + break; + } + // REQUIRES AUTOFIX + } + if(foundHere) + AddToLog("Found plugins"); +#endif // NO_PLUGINS + + // Check for invalid order items + if(!originalSpecs->hasIgnoreIndex && mpt::contains(m_SndFile.Order(), m_SndFile.Order.GetIgnoreIndex())) + { + foundHacks = true; + AddToLog("This format does not support separator (+++) patterns"); + + if(autofix) + { + m_SndFile.Order().RemovePattern(m_SndFile.Order.GetIgnoreIndex()); + } + } + + if(!originalSpecs->hasStopIndex && m_SndFile.Order().GetLengthFirstEmpty() != m_SndFile.Order().GetLengthTailTrimmed()) + { + foundHacks = true; + AddToLog("The pattern sequence should end after the first stop (---) index in this format."); + + if(autofix) + { + m_SndFile.Order().RemovePattern(m_SndFile.Order.GetInvalidPatIndex()); + } + } + + // Global volume + if(modType == MOD_TYPE_XM && m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME) + { + foundHacks = true; + AddToLog("XM format does not support default global volume"); + if(autofix) + { + GlobalVolumeToPattern(); + } + } + + // Pattern count + if(m_SndFile.Patterns.GetNumPatterns() > originalSpecs->patternsMax) + { + AddToLog(MPT_AFORMAT("Found too many patterns ({} allowed)")(originalSpecs->patternsMax)); + foundHacks = true; + // REQUIRES (INTELLIGENT) AUTOFIX + } + + // Check for too big/small patterns + foundHere = false; + for(auto &pat : m_SndFile.Patterns) + { + if(pat.IsValid()) + { + const ROWINDEX patSize = pat.GetNumRows(); + if(patSize > originalSpecs->patternRowsMax) + { + foundHacks = foundHere = true; + if(autofix) + { + // REQUIRES (INTELLIGENT) AUTOFIX + } else + { + break; + } + } else if(patSize < originalSpecs->patternRowsMin) + { + foundHacks = foundHere = true; + if(autofix) + { + pat.Resize(originalSpecs->patternRowsMin); + pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(patSize - 1).RetryNextRow()); + } else + { + break; + } + } + } + } + if(foundHere) + { + AddToLog(MPT_AFORMAT("Found incompatible pattern lengths (must be between {} and {} rows)")(originalSpecs->patternRowsMin, originalSpecs->patternRowsMax)); + } + + // Check for invalid pattern commands + foundHere = false; + m_SndFile.Patterns.ForEachModCommand([originalSpecs, &foundHere, autofix, modType] (ModCommand &m) + { + // definitely not perfect yet. :) + // Probably missing: Some extended effect parameters + if(!originalSpecs->HasNote(m.note)) + { + foundHere = true; + if(autofix) + m.note = NOTE_NONE; + } + + if(!originalSpecs->HasCommand(m.command)) + { + foundHere = true; + if(autofix) + m.command = CMD_NONE; + } + + if(!originalSpecs->HasVolCommand(m.volcmd)) + { + foundHere = true; + if(autofix) + m.volcmd = VOLCMD_NONE; + } + + if(modType == MOD_TYPE_XM) // ModPlug XM extensions + { + if(m.command == CMD_XFINEPORTAUPDOWN && m.param >= 0x30) + { + foundHere = true; + if(autofix) + m.command = CMD_NONE; + } + } else if(modType == MOD_TYPE_IT) // ModPlug IT extensions + { + if((m.command == CMD_S3MCMDEX) && ((m.param & 0xF0) == 0x90) && (m.param != 0x91)) + { + foundHere = true; + if(autofix) + m.command = CMD_NONE; + } + } + }); + if(foundHere) + { + AddToLog("Found invalid pattern commands"); + foundHacks = true; + } + + // Check for pattern names + const PATTERNINDEX numNamedPatterns = m_SndFile.Patterns.GetNumNamedPatterns(); + if(numNamedPatterns > 0 && !originalSpecs->hasPatternNames) + { + AddToLog("Found pattern names"); + foundHacks = true; + if(autofix) + { + for(PATTERNINDEX i = 0; i < numNamedPatterns; i++) + { + m_SndFile.Patterns[i].SetName(""); + } + } + } + + // Check for too many channels + if(m_SndFile.GetNumChannels() > originalSpecs->channelsMax || m_SndFile.GetNumChannels() < originalSpecs->channelsMin) + { + AddToLog(MPT_AFORMAT("Found incompatible channel count (must be between {} and {} channels)")(originalSpecs->channelsMin, originalSpecs->channelsMax)); + foundHacks = true; + if(autofix) + { + std::vector<bool> usedChannels; + CheckUsedChannels(usedChannels); + RemoveChannels(usedChannels); + // REQUIRES (INTELLIGENT) AUTOFIX + } + } + + // Check for channel names + foundHere = false; + for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++) + { + if(!m_SndFile.ChnSettings[i].szName.empty()) + { + foundHere = foundHacks = true; + if(autofix) + m_SndFile.ChnSettings[i].szName = ""; + else + break; + } + } + if(foundHere) + AddToLog("Found channel names"); + + // Check for too many samples + if(m_SndFile.GetNumSamples() > originalSpecs->samplesMax) + { + AddToLog(MPT_AFORMAT("Found too many samples ({} allowed)")(originalSpecs->samplesMax)); + foundHacks = true; + // REQUIRES (INTELLIGENT) AUTOFIX + } + + // Check for sample extensions + foundHere = false; + for(SAMPLEINDEX i = 1; i <= m_SndFile.GetNumSamples(); i++) + { + ModSample &smp = m_SndFile.GetSample(i); + if(modType == MOD_TYPE_XM && smp.GetNumChannels() > 1) + { + foundHere = foundHacks = true; + if(autofix) + { + ctrlSmp::ConvertToMono(smp, m_SndFile, ctrlSmp::mixChannels); + } else + { + break; + } + } + } + if(foundHere) + AddToLog("Stereo samples are not supported in the original XM format"); + + // Check for too many instruments + if(m_SndFile.GetNumInstruments() > originalSpecs->instrumentsMax) + { + AddToLog(MPT_AFORMAT("Found too many instruments ({} allowed)")(originalSpecs->instrumentsMax)); + foundHacks = true; + // REQUIRES (INTELLIGENT) AUTOFIX + } + + // Check for instrument extensions + foundHere = false; + bool foundEnvelopes = false; + for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++) + { + ModInstrument *instr = m_SndFile.Instruments[i]; + if(instr == nullptr) continue; + + // Extended instrument attributes + if(instr->filterMode != FilterMode::Unchanged || instr->nVolRampUp != 0 || instr->resampling != SRCMODE_DEFAULT || + instr->nCutSwing != 0 || instr->nResSwing != 0 || instr->nMixPlug != 0 || instr->pitchToTempoLock.GetRaw() != 0 || + instr->nDCT == DuplicateCheckType::Plugin || + instr->VolEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET || + instr->PanEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET || + instr->PitchEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET + ) + { + foundHere = foundHacks = true; + if(autofix) + { + instr->filterMode = FilterMode::Unchanged; + instr->nVolRampUp = 0; + instr->resampling = SRCMODE_DEFAULT; + instr->nCutSwing = 0; + instr->nResSwing = 0; + instr->nMixPlug = 0; + instr->pitchToTempoLock.Set(0); + if(instr->nDCT == DuplicateCheckType::Plugin) instr->nDCT = DuplicateCheckType::None; + instr->VolEnv.nReleaseNode = instr->PanEnv.nReleaseNode = instr->PitchEnv.nReleaseNode = ENV_RELEASE_NODE_UNSET; + } + } + // Incompatible envelope shape + foundEnvelopes |= FindIncompatibleEnvelopes(instr->VolEnv, autofix); + foundEnvelopes |= FindIncompatibleEnvelopes(instr->PanEnv, autofix); + foundEnvelopes |= FindIncompatibleEnvelopes(instr->PitchEnv, autofix); + foundHacks |= foundEnvelopes; + } + if(foundHere) + AddToLog("Found MPT instrument extensions"); + if(foundEnvelopes) + AddToLog("Two envelope points may not share the same tick."); + + // Check for too many orders + if(m_SndFile.Order().GetLengthTailTrimmed() > originalSpecs->ordersMax) + { + AddToLog(MPT_AFORMAT("Found too many orders ({} allowed)")(originalSpecs->ordersMax)); + foundHacks = true; + if(autofix) + { + // Can we be more intelligent here and maybe remove stop patterns and such? + m_SndFile.Order().resize(originalSpecs->ordersMax); + } + } + + // Check for invalid default tempo + if(m_SndFile.m_nDefaultTempo > originalSpecs->GetTempoMax() || m_SndFile.m_nDefaultTempo < originalSpecs->GetTempoMin()) + { + AddToLog(MPT_AFORMAT("Found incompatible default tempo (must be between {} and {})")(originalSpecs->GetTempoMin().GetInt(), originalSpecs->GetTempoMax().GetInt())); + foundHacks = true; + if(autofix) + m_SndFile.m_nDefaultTempo = Clamp(m_SndFile.m_nDefaultTempo, originalSpecs->GetTempoMin(), originalSpecs->GetTempoMax()); + } + + // Check for invalid default speed + if(m_SndFile.m_nDefaultSpeed > originalSpecs->speedMax || m_SndFile.m_nDefaultSpeed < originalSpecs->speedMin) + { + AddToLog(MPT_AFORMAT("Found incompatible default speed (must be between {} and {})")(originalSpecs->speedMin, originalSpecs->speedMax)); + foundHacks = true; + if(autofix) + m_SndFile.m_nDefaultSpeed = Clamp(m_SndFile.m_nDefaultSpeed, originalSpecs->speedMin, originalSpecs->speedMax); + } + + // Check for invalid rows per beat / measure values + if(m_SndFile.m_nDefaultRowsPerBeat >= originalSpecs->patternRowsMax || m_SndFile.m_nDefaultRowsPerMeasure >= originalSpecs->patternRowsMax) + { + AddToLog("Found incompatible rows per beat / measure"); + foundHacks = true; + if(autofix) + { + m_SndFile.m_nDefaultRowsPerBeat = Clamp(m_SndFile.m_nDefaultRowsPerBeat, 1u, (originalSpecs->patternRowsMax - 1)); + m_SndFile.m_nDefaultRowsPerMeasure = Clamp(m_SndFile.m_nDefaultRowsPerMeasure, m_SndFile.m_nDefaultRowsPerBeat, (originalSpecs->patternRowsMax - 1)); + } + } + + // Find pattern-specific time signatures + if(!originalSpecs->hasPatternSignatures) + { + foundHere = false; + for(auto &pat : m_SndFile.Patterns) + { + if(pat.GetOverrideSignature()) + { + if(!foundHere) + AddToLog("Found pattern-specific time signatures"); + + if(autofix) + pat.RemoveSignature(); + + foundHacks = foundHere = true; + if(!autofix) + break; + } + } + } + + // Check for new tempo modes + if(m_SndFile.m_nTempoMode != TempoMode::Classic) + { + AddToLog("Found incompatible tempo mode (only classic tempo mode allowed)"); + foundHacks = true; + if(autofix) + m_SndFile.m_nTempoMode = TempoMode::Classic; + } + + // Check for extended filter range flag + if(m_SndFile.m_SongFlags[SONG_EXFILTERRANGE]) + { + AddToLog("Found extended filter range"); + foundHacks = true; + if(autofix) + m_SndFile.m_SongFlags.reset(SONG_EXFILTERRANGE); + } + + // Player flags + if((modType & (MOD_TYPE_XM|MOD_TYPE_IT)) && !m_SndFile.m_playBehaviour[MSF_COMPATIBLE_PLAY]) + { + AddToLog("Compatible play is deactivated"); + foundHacks = true; + if(autofix) + m_SndFile.SetDefaultPlaybackBehaviour(modType); + } + + // Check for restart position where it should not be + for(SEQUENCEINDEX seq = 0; seq < m_SndFile.Order.GetNumSequences(); seq++) + { + if(m_SndFile.Order(seq).GetRestartPos() > 0 && !originalSpecs->hasRestartPos) + { + AddToLog("Found restart position"); + foundHacks = true; + if(autofix) + { + m_SndFile.Order.RestartPosToPattern(seq); + } + } + } + + if(!originalSpecs->hasArtistName && !m_SndFile.m_songArtist.empty() && !(modType & (MOD_TYPE_MOD | MOD_TYPE_S3M))) + { + AddToLog("Found artist name"); + foundHacks = true; + if(autofix) + { + m_SndFile.m_songArtist.clear(); + } + } + + if(m_SndFile.GetMixLevels() != MixLevels::Compatible && m_SndFile.GetMixLevels() != MixLevels::CompatibleFT2) + { + AddToLog("Found incorrect mix levels (only compatible mix levels allowed)"); + foundHacks = true; + if(autofix) + m_SndFile.SetMixLevels(modType == MOD_TYPE_XM ? MixLevels::CompatibleFT2 : MixLevels::Compatible); + } + + // Check for extended MIDI macros + if(modType == MOD_TYPE_IT) + { + for(const auto ¯o : m_SndFile.m_MidiCfg) + { + for(const auto c : std::string_view{macro}) + { + if(c == 's') + { + foundHacks = true; + AddToLog("Found SysEx checksum variable in MIDI macro"); + break; + } + } + } + } + + if(autofix && foundHacks) + SetModified(); + + return foundHacks; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackLink.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackLink.cpp new file mode 100644 index 00000000..161f3839 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackLink.cpp @@ -0,0 +1,71 @@ +/* + * MPTrackLink.cpp + * --------------- + * Purpose: Consolidated linking against MSVC/Windows libraries. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#if defined(MPT_BUILD_MSVC) +#if MPT_COMPILER_MSVC || MPT_COMPILER_CLANG + +#if !defined(MPT_BUILD_RETRO) +#pragma comment(lib, "delayimp.lib") +#endif // !MPT_BUILD_RETRO + +#pragma comment(lib, "version.lib") +#pragma comment(lib, "rpcrt4.lib") +#pragma comment(lib, "shlwapi.lib") +#pragma comment(lib, "wininet.lib") +#pragma comment(lib, "htmlhelp.lib") +#pragma comment(lib, "uxtheme.lib") + +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "ncrypt.lib") + +#pragma comment(lib, "gdiplus.lib") + +#pragma comment(lib, "dmoguids.lib") +#pragma comment(lib, "strmiids.lib") + +#if (_WIN32_WINNT >= 0x600) +#pragma comment(lib, "avrt.lib") +#endif +#if defined(MPT_WITH_DIRECTSOUND) +#pragma comment(lib, "dsound.lib") +#endif // MPT_WITH_DIRECTSOUND +#pragma comment(lib, "winmm.lib") + +#pragma comment(lib, "ksuser.lib") + +#ifdef MPT_WITH_MEDIAFOUNDATION +#pragma comment(lib, "mf.lib") +#pragma comment(lib, "mfplat.lib") +#pragma comment(lib, "mfreadwrite.lib") +#pragma comment(lib, "mfuuid.lib") // static lib +#pragma comment(lib, "propsys.lib") +#endif + +// work-around VS2019 16.8.1 bug on ARM and ARM64 +#if MPT_COMPILER_MSVC +#if defined(_M_ARM) || defined(_M_ARM64) +#pragma comment(lib, "Synchronization.lib") +#endif +#endif + +#if MPT_COMPILER_MSVC +#pragma comment( linker, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df'\"" ) +#endif // MPT_COMPILER_MSVC + +#endif // MPT_COMPILER_MSVC || MPT_COMPILER_CLANG +#endif // MPT_BUILD_MSVC + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.cpp new file mode 100644 index 00000000..dbc00d9d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.cpp @@ -0,0 +1,85 @@ +/* + * MPTrackUtil.cpp + * --------------- + * Purpose: Various useful utility functions. + * 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 "MPTrackUtil.h" + + +OPENMPT_NAMESPACE_BEGIN + + +static bool CreateShellLink(const IID &type, const mpt::PathString &path, const mpt::PathString &target, const mpt::ustring &description) +{ + HRESULT hres = 0; + IShellLink *psl = nullptr; + hres = CoCreateInstance(type, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&psl); + if(SUCCEEDED(hres)) + { + IPersistFile *ppf = nullptr; + psl->SetPath(target.AsNative().c_str()); + psl->SetDescription(mpt::ToWin(description).c_str()); + hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf); + if(SUCCEEDED(hres)) + { + hres = ppf->Save(path.ToWide().c_str(), TRUE); + ppf->Release(); + ppf = nullptr; + } + psl->Release(); + psl = nullptr; + } + return SUCCEEDED(hres); +} + + +bool CreateShellFolderLink(const mpt::PathString &path, const mpt::PathString &target, const mpt::ustring &description) +{ + return CreateShellLink(CLSID_FolderShortcut, path, target, description); +} + + +bool CreateShellFileLink(const mpt::PathString &path, const mpt::PathString &target, const mpt::ustring &description) +{ + return CreateShellLink(CLSID_ShellLink, path, target, description); +} + + +mpt::const_byte_span GetResource(LPCTSTR lpName, LPCTSTR lpType) +{ + HINSTANCE hInstance = AfxGetInstanceHandle(); + HRSRC hRsrc = FindResource(hInstance, lpName, lpType); + if(hRsrc == NULL) + { + return mpt::const_byte_span(); + } + HGLOBAL hGlob = LoadResource(hInstance, hRsrc); + if(hGlob == NULL) + { + return mpt::const_byte_span(); + } + return mpt::const_byte_span(mpt::void_cast<const std::byte *>(LockResource(hGlob)), SizeofResource(hInstance, hRsrc)); + // no need to call FreeResource(hGlob) or free hRsrc, according to MSDN +} + + +CString LoadResourceString(UINT nID) +{ + CString str; + BOOL resourceLoaded = str.LoadString(nID); + MPT_ASSERT(resourceLoaded); + if(!resourceLoaded) + { + return _T(""); + } + return str; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.h b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.h new file mode 100644 index 00000000..ff0eab59 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtil.h @@ -0,0 +1,85 @@ +/* + * MPTrackUtil.h + * ------------- + * Purpose: Various useful utility functions. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include <string> + + +OPENMPT_NAMESPACE_BEGIN + + +bool CreateShellFolderLink(const mpt::PathString &path, const mpt::PathString &target, const mpt::ustring &description = mpt::ustring()); + +bool CreateShellFileLink(const mpt::PathString &path, const mpt::PathString &target, const mpt::ustring &description = mpt::ustring()); + + +/* + * Gets resource as raw byte data. + * [in] lpName and lpType: parameters passed to FindResource(). + * Return: span representing the resource data, valid as long as hInstance is valid. + */ +mpt::const_byte_span GetResource(LPCTSTR lpName, LPCTSTR lpType); + + +CString LoadResourceString(UINT nID); + + +namespace Util +{ + // Get horizontal DPI resolution + MPT_FORCEINLINE int GetDPIx(HWND hwnd) + { + HDC dc = ::GetDC(hwnd); + int dpi = ::GetDeviceCaps(dc, LOGPIXELSX); + ::ReleaseDC(hwnd, dc); + return dpi; + } + + // Get vertical DPI resolution + MPT_FORCEINLINE int GetDPIy(HWND hwnd) + { + HDC dc = ::GetDC(hwnd); + int dpi = ::GetDeviceCaps(dc, LOGPIXELSY); + ::ReleaseDC(hwnd, dc); + return dpi; + } + + // Applies DPI scaling factor to some given size + MPT_FORCEINLINE int ScalePixels(int pixels, HWND hwnd) + { + return MulDiv(pixels, GetDPIx(hwnd), 96); + } + + // Removes DPI scaling factor from some given size + MPT_FORCEINLINE int ScalePixelsInv(int pixels, HWND hwnd) + { + return MulDiv(pixels, 96, GetDPIx(hwnd)); + } +} + + +namespace Util +{ + inline DWORD64 GetTickCount64() + { +#if (_WIN32_WINNT >= _WIN32_WINNT_VISTA) + return ::GetTickCount64(); +#else + return ::GetTickCount(); +#endif + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.cpp new file mode 100644 index 00000000..6d01d111 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.cpp @@ -0,0 +1,313 @@ +/* + * MPTrackUtilWine.cpp + * ------------------- + * Purpose: Wine utility functions. + * 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 "MPTrackUtilWine.h" +#include "Mptrack.h" +#include "../common/misc_util.h" +#include "../misc/mptWine.h" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Util +{ + + +namespace Wine +{ + + +class CExecutePosixShellScriptProgressDialog + : public CDialog +{ + +protected: + + mpt::Wine::Context & wine; + std::string m_Title; + std::string m_Status; + bool m_bAbort; + std::string m_script; + FlagSet<mpt::Wine::ExecFlags> m_Flags; + std::map<std::string, std::vector<char> > m_Filetree; + mpt::Wine::ExecResult m_ExecResult; + std::string m_ExceptionString; + +public: + + CExecutePosixShellScriptProgressDialog(mpt::Wine::Context & wine, std::string script, FlagSet<mpt::Wine::ExecFlags> flags, std::map<std::string, std::vector<char> > filetree, std::string title, std::string status, CWnd *parent = NULL); + + BOOL OnInitDialog(); + + void OnCancel(); + + afx_msg void OnButton1(); + + static mpt::Wine::ExecuteProgressResult ProgressCancelCallback(void *userdata); + + static void ProgressCallback(void *userdata); + + mpt::Wine::ExecuteProgressResult Progress(bool allowCancel); + + void MessageLoop(); + + mpt::Wine::ExecResult GetExecResult() const; + std::string GetExceptionString() const; + +private: + + DECLARE_MESSAGE_MAP() + +}; + + +BEGIN_MESSAGE_MAP(CExecutePosixShellScriptProgressDialog, CDialog) + ON_COMMAND(IDC_BUTTON1, &CExecutePosixShellScriptProgressDialog::OnButton1) +END_MESSAGE_MAP() + + +CExecutePosixShellScriptProgressDialog::CExecutePosixShellScriptProgressDialog(mpt::Wine::Context & wine, std::string script, FlagSet<mpt::Wine::ExecFlags> flags, std::map<std::string, std::vector<char> > filetree, std::string title, std::string status, CWnd *parent) + : CDialog(IDD_PROGRESS, parent) + , wine(wine) + , m_Title(title) + , m_Status(status) + , m_bAbort(false) + , m_script(script) + , m_Flags(flags) + , m_Filetree(filetree) + , m_ExecResult(mpt::Wine::ExecResult::Error()) +{ + return; +} + + +void CExecutePosixShellScriptProgressDialog::OnCancel() +{ + m_bAbort = true; +} + + +mpt::Wine::ExecuteProgressResult CExecutePosixShellScriptProgressDialog::ProgressCancelCallback(void *userdata) +{ + return reinterpret_cast<CExecutePosixShellScriptProgressDialog*>(userdata)->Progress(true); +} + + +void CExecutePosixShellScriptProgressDialog::ProgressCallback(void *userdata) +{ + reinterpret_cast<CExecutePosixShellScriptProgressDialog*>(userdata)->Progress(false); +} + + +void CExecutePosixShellScriptProgressDialog::MessageLoop() +{ + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + + +mpt::Wine::ExecResult CExecutePosixShellScriptProgressDialog::GetExecResult() const +{ + return m_ExecResult; +} + + +std::string CExecutePosixShellScriptProgressDialog::GetExceptionString() const +{ + return m_ExceptionString; +} + + +BOOL CExecutePosixShellScriptProgressDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetWindowText(mpt::ToCString(mpt::Charset::UTF8, m_Title)); + SetDlgItemText(IDCANCEL, _T("Cancel")); + SetWindowLong(::GetDlgItem(m_hWnd, IDC_PROGRESS1), GWL_STYLE, GetWindowLong(::GetDlgItem(m_hWnd, IDC_PROGRESS1), GWL_STYLE) | PBS_MARQUEE); + ::SendMessage(::GetDlgItem(m_hWnd, IDC_PROGRESS1), PBM_SETMARQUEE, 1, 30); // 30 is Windows default, but Wine < 1.7.15 defaults to 0 + PostMessage(WM_COMMAND, IDC_BUTTON1); + return TRUE; +} + + +mpt::Wine::ExecuteProgressResult CExecutePosixShellScriptProgressDialog::Progress(bool allowCancel) +{ + if(m_bAbort) + { + return mpt::Wine::ExecuteProgressAsyncCancel; + } + ::ShowWindow(::GetDlgItem(m_hWnd, IDCANCEL), allowCancel ? SW_SHOW : SW_HIDE); + MessageLoop(); + if(m_bAbort) + { + return mpt::Wine::ExecuteProgressAsyncCancel; + } + ::Sleep(10); + return mpt::Wine::ExecuteProgressContinueWaiting; +} + + +void CExecutePosixShellScriptProgressDialog::OnButton1() +{ + if(m_script.empty()) + { + EndDialog(IDCANCEL); + return; + } + + SetDlgItemText(IDC_TEXT1, mpt::ToCString(mpt::Charset::UTF8, m_Status)); + MessageLoop(); + if(m_bAbort) + { + EndDialog(IDCANCEL); + return; + } + ::Sleep(10); + + try + { + m_ExecResult = wine.ExecutePosixShellScript(m_script, m_Flags, m_Filetree, m_Title, &ProgressCallback, &ProgressCancelCallback, this); + } catch(const mpt::Wine::Exception &e) + { + m_ExceptionString = mpt::get_exception_text<std::string>(e); + EndDialog(IDCANCEL); + return; + } + + MessageLoop(); + if(m_bAbort) + { + EndDialog(IDCANCEL); + return; + } + + SetDlgItemText(IDC_TEXT1, _T("Done.")); + ::ShowWindow(::GetDlgItem(m_hWnd, IDCANCEL), SW_HIDE); + for(int i = 0; i < 10; ++i) + { + MessageLoop(); + ::Sleep(10); + } + + MessageLoop(); + + EndDialog(IDOK); +} + + +static void ProgressCallback(void * /*userdata*/ ) +{ + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + ::Sleep(10); +} + + +static mpt::Wine::ExecuteProgressResult ProgressCancelCallback(void *userdata) +{ + ProgressCallback(userdata); + return mpt::Wine::ExecuteProgressContinueWaiting; +} + + +mpt::Wine::ExecResult ExecutePosixShellScript(mpt::Wine::Context & wine, std::string script, FlagSet<mpt::Wine::ExecFlags> flags, std::map<std::string, std::vector<char> > filetree, std::string title, std::string status) +{ + if(flags[mpt::Wine::ExecFlagProgressWindow]) + { + CExecutePosixShellScriptProgressDialog dlg(wine, script, flags, filetree, title, status, theApp.GetMainWnd()); + if(dlg.DoModal() != IDOK) + { + if(!dlg.GetExceptionString().empty()) + { + throw mpt::Wine::Exception(dlg.GetExceptionString()); + } + throw mpt::Wine::Exception("Canceled."); + } + return dlg.GetExecResult(); + } else + { + return wine.ExecutePosixShellScript(script, flags, filetree, title, &ProgressCallback, &ProgressCancelCallback, nullptr); + } +} + + +Dialog::Dialog(std::string title, bool terminal) + : m_Terminal(terminal) + , m_Title(title) +{ + return; +} + +std::string Dialog::Detect() const +{ + std::string script; + script += std::string() + "chmod u+x ./build/wine/dialog.sh" + "\n"; + return script; +} + +std::string Dialog::DialogVar() const +{ + if(m_Terminal) + { + return "./build/wine/dialog.sh tui"; + } else + { + return "./build/wine/dialog.sh gui"; + } +} + +std::string Dialog::Title() const +{ + return m_Title; +} + +std::string Dialog::Status(std::string text) const +{ + return std::string() + DialogVar() + " --infobox \"" + Title() + "\" \"" + text + "\""; +} + +std::string Dialog::MessageBox(std::string text) const +{ + return std::string() + DialogVar() + " --msgbox \"" + Title() + "\" \"" + text + "\""; +} + +std::string Dialog::YesNo(std::string text) const +{ + return std::string() + DialogVar() + " --yesno \"" + Title() + "\" \"" + text + "\""; +} + +std::string Dialog::TextBox(std::string text) const +{ + return std::string() + DialogVar() + " --textbox \"" + Title() + "\" \"" + text + "\""; +} + +std::string Dialog::Progress(std::string text) const +{ + return std::string() + DialogVar() + " --gauge \"" + Title() + "\" \"" + text + "\""; +} + + +} // namespace Wine + + +} // namespace Util + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.h b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.h new file mode 100644 index 00000000..bae1d38e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackUtilWine.h @@ -0,0 +1,58 @@ +/* + * MPTrackUtilWine.h + * ----------------- + * Purpose: Wine utility functions. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "../misc/mptWine.h" + + +OPENMPT_NAMESPACE_BEGIN + + +namespace Util +{ + + +namespace Wine +{ + + +mpt::Wine::ExecResult ExecutePosixShellScript(mpt::Wine::Context & wine, std::string script, FlagSet<mpt::Wine::ExecFlags> flags, std::map<std::string, std::vector<char> > filetree, std::string title, std::string status); + + +class Dialog +{ +private: + bool m_Terminal; + std::string m_Title; +private: + std::string DialogVar() const; +public: + Dialog(std::string title, bool terminal); + std::string Title() const; + std::string Detect() const; + std::string Status(std::string text) const; + std::string MessageBox(std::string text) const; + std::string YesNo(std::string text) const; + std::string TextBox(std::string filename) const; + std::string Progress(std::string text) const; +}; + + +} // namespace Wine + + +} // namespace Util + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.cpp new file mode 100644 index 00000000..856dee2d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.cpp @@ -0,0 +1,847 @@ +/* + * MPTrackWine.cpp + * --------------- + * Purpose: OpenMPT Wine support functions. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#if MPT_COMPILER_MSVC +#pragma warning(disable:4800) // 'T' : forcing value to bool 'true' or 'false' (performance warning) +#endif // MPT_COMPILER_MSVC + +#include "MPTrackWine.h" + +#include "mpt/uuid/uuid.hpp" + +#include "Mptrack.h" +#include "Mainfrm.h" +#include "AboutDialog.h" +#include "TrackerSettings.h" +#include "../common/ComponentManager.h" +#include "../common/mptFileIO.h" +#include "../misc/mptOS.h" +#include "mpt/crc/crc.hpp" +#include "../common/FileReader.h" +#include "../misc/mptWine.h" +#include "MPTrackUtilWine.h" + +#include "wine/NativeSoundDevice.h" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "wine/NativeSoundDeviceMarshalling.h" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" + +#include <ios> + +#include <contrib/minizip/unzip.h> +#include <contrib/minizip/iowin32.h> + + +OPENMPT_NAMESPACE_BEGIN + + +static mpt::ustring WineGetWindowTitle() +{ + return U_("OpenMPT Wine integration"); +} + + +static std::string WineGetWindowTitleUTF8() +{ + return mpt::ToCharset(mpt::Charset::UTF8, WineGetWindowTitle()); +} + + +static mpt::PathString WineGetSupportZipFilename() +{ + return P_("openmpt-wine-support.zip"); +} + + +static char SanitizeBuildIdChar(char c) +{ + char result = c; + if (c == '\0') result = '_'; + else if (c >= 'a' && c <= 'z') result = c; + else if (c >= 'A' && c <= 'Z') result = c; + else if (c >= '0' && c <= '9') result = c; + else if (c == '!') result = c; + else if (c == '+') result = c; + else if (c == '-') result = c; + else if (c == '.') result = c; + else if (c == '~') result = c; + else if (c == '_') result = c; + else result = '_'; + return result; +} + + +static std::string SanitizeBuildID(std::string id) +{ + for(auto & c : id) + { + c = SanitizeBuildIdChar(c); + } + return id; +} + + +namespace WineIntegration { + + +static mpt::crc64_jones WineHashVersion(mpt::crc64_jones crc) +{ + std::string s; + s += mpt::ToCharset(mpt::Charset::UTF8, Build::GetVersionStringExtended()); + s += " "; + s += mpt::ToCharset(mpt::Charset::UTF8, mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())); + s += " "; + s += mpt::ToCharset(mpt::Charset::UTF8, SourceInfo::Current().GetUrlWithRevision()); + s += " "; + s += mpt::ToCharset(mpt::Charset::UTF8, SourceInfo::Current().GetStateString()); + crc(s.begin(), s.end()); + return crc; +} + + +static mpt::crc64_jones WineHashFile(mpt::crc64_jones crc, mpt::PathString filename) +{ + InputFile file(filename, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(!file.IsValid()) + { + return crc; + } + FileReader f = GetFileReader(file); + FileReader::PinnedView view = f.ReadPinnedView(); + crc(view.begin(), view.end()); + return crc; +} + + +static mpt::crc64_jones WineHashSettings(mpt::crc64_jones crc) +{ + std::string result; + result += std::string() + "-c"; + result += std::string() + "-" + mpt::afmt::dec(TrackerSettings::Instance().WineSupportEnablePulseAudio.Get()); + result += std::string() + "-" + mpt::afmt::dec(TrackerSettings::Instance().WineSupportEnablePortAudio.Get()); + crc(result.begin(), result.end()); + return crc; +} + + +mpt::ustring WineGetSystemInfoString(mpt::OS::Wine::VersionContext & wineVersion) +{ + mpt::ustring msg; + + msg += CAboutDlg::GetTabText(5); + + msg += U_("\n"); + + msg += MPT_UFORMAT("OpenMPT detected Wine {} running on {}.\n") + ( wineVersion.Version().AsString() + , wineVersion.HostClass() == mpt::osinfo::osclass::Linux ? U_("Linux") : U_("unknown system") + ); + + return msg; + +} + + +bool WineSetupIsSupported(mpt::OS::Wine::VersionContext & wineVersion) +{ + bool supported = true; + if(wineVersion.RawBuildID().empty()) supported = false; + if(!TrackerSettings::Instance().WineSupportAllowUnknownHost) + { + if((wineVersion.HostClass() == mpt::osinfo::osclass::Linux) || ((wineVersion.HostClass() == mpt::osinfo::osclass::BSD) && wineVersion.RawHostSysName() == "FreeBSD")) + { + // ok + } else + { + supported = false; + } + } + if(!wineVersion.Version().IsValid()) supported = false; + return supported; +} + +bool WineSetupIsSupported(mpt::Wine::Context & wine) +{ + bool supported = true; + if(theApp.GetInstallPath().empty()) supported = false; + if(wine.PathToPosix(theApp.GetInstallPath()).empty()) supported = false; + if(wine.PathToPosix(theApp.GetConfigPath()).empty()) supported = false; + if(wine.PathToWindows("/").empty()) supported = false; + if(supported) + { + if(wine.HOME().empty()) supported = false; + } + if(supported) + { + if(wine.Uname_m() == "x86_64" && mpt::pointer_size != 8) supported = false; + } + return supported; +} + + +static std::map<std::string, std::vector<char> > UnzipToMap(mpt::PathString filename) +{ + std::map<std::string, std::vector<char> > filetree; + { + zlib_filefunc64_def zipfilefuncs; + MemsetZero(zipfilefuncs); + fill_win32_filefunc64W(&zipfilefuncs); + unzFile zipfile = unzOpen2_64(filename.ToWide().c_str(), &zipfilefuncs); + if(!zipfile) + { + throw mpt::Wine::Exception("Archive is not a zip file"); + } + for(int status = unzGoToFirstFile(zipfile); status == UNZ_OK; status = unzGoToNextFile(zipfile)) + { + int openstatus = UNZ_OK; + openstatus = unzOpenCurrentFile(zipfile); + if(openstatus != UNZ_OK) + { + unzClose(zipfile); + throw mpt::Wine::Exception("Archive is corrupted."); + } + unz_file_info info; + MemsetZero(info); + char name[1024]; + MemsetZero(name); + openstatus = unzGetCurrentFileInfo(zipfile, &info, name, sizeof(name) - 1, nullptr, 0, nullptr, 0); + if(openstatus != UNZ_OK) + { + unzCloseCurrentFile(zipfile); + unzClose(zipfile); + throw mpt::Wine::Exception("Archive is corrupted."); + } + std::vector<char> data(info.uncompressed_size); + unzReadCurrentFile(zipfile, &data[0], info.uncompressed_size); + unzCloseCurrentFile(zipfile); + + data = mpt::buffer_cast<std::vector<char>>(mpt::replace(mpt::buffer_cast<std::string>(data), std::string("\r\n"), std::string("\n"))); + + filetree[mpt::replace(mpt::ToCharset(mpt::Charset::UTF8, mpt::Charset::CP437, name), std::string("\\"), std::string("/"))] = data; + } + unzClose(zipfile); + } + return filetree; +} + + +bool IsSupported() +{ + return theApp.GetWine() ? true : false; +} + + +bool IsCompiled() +{ + return !theApp.GetWineWrapperDllFilename().empty(); +} + + +void Initialize() +{ + + if(!mpt::OS::Windows::IsWine()) + { + return; + } + + mpt::ustring lf = U_("\n"); + + if(!TrackerSettings::Instance().WineSupportEnabled) + { + return; + } + + mpt::OS::Wine::VersionContext wineVersion = *theApp.GetWineVersion(); + if(!WineSetupIsSupported(wineVersion)) + { + mpt::ustring msg; + msg += U_("OpenMPT does not support Wine integration on your current Wine setup.") + lf; + Reporting::Notification(msg, WineGetWindowTitle()); + return; + } + + try + { + mpt::Wine::Context wine = mpt::Wine::Context(wineVersion); + if(!WineSetupIsSupported(wine)) + { + mpt::ustring msg; + msg += U_("OpenMPT does not support Wine integration on your current Wine setup.") + lf; + Reporting::Notification(msg, WineGetWindowTitle()); + return; + } + theApp.SetWine(std::make_shared<mpt::Wine::Context>(wine)); + } catch(const std::exception & e) + { + mpt::ustring msg; + msg += U_("OpenMPT was not able to determine Wine configuration details on your current Wine setup:") + lf; + msg += mpt::get_exception_text<mpt::ustring>(e) + lf; + msg += U_("OpenMPT native Wine Integration will not be available.") + lf; + Reporting::Error(msg, WineGetWindowTitle()); + return; + } + mpt::Wine::Context wine = *theApp.GetWine(); + + try + { + + struct Paths + { + mpt::PathString AppData; + mpt::PathString AppData_Wine; + mpt::PathString AppData_Wine_WineVersion; + mpt::PathString AppData_Wine_WineVersion_OpenMPTVersion; + std::string Host_AppData; + std::string Host_AppData_Wine; + std::string Host_AppData_Wine_WineVersion; + std::string Host_AppData_Wine_WineVersion_OpenMPTVersion; + std::string Host_Native_OpenMPT_Wine_WineVersion_OpenMPTVersion; + static void CreatePath(mpt::PathString path) + { + if(path.IsDirectory()) + { + return; + } + if(CreateDirectory(path.AsNative().c_str(), NULL) == 0) + { + throw mpt::Wine::Exception(std::string() + "Failed to create directory: " + path.ToUTF8()); + } + } + std::string GetOpenMPTVersion() const + { + std::string ver; + ver += mpt::ToCharset(mpt::Charset::UTF8, Build::GetVersionStringPure() + U_("_") + mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())); + mpt::crc64_jones crc; + crc = WineHashVersion(crc); + crc = WineHashFile(crc, theApp.GetInstallPath() + WineGetSupportZipFilename()); + crc = WineHashSettings(crc); + ver += std::string("-") + mpt::afmt::hex0<16>(crc.result()); + return ver; + } + Paths(mpt::Wine::Context & wine) + { + AppData = theApp.GetConfigPath().WithoutTrailingSlash(); + AppData_Wine = AppData.WithTrailingSlash() + P_("Wine"); + AppData_Wine_WineVersion = AppData_Wine.WithTrailingSlash() + mpt::PathString::FromUTF8(SanitizeBuildID(wine.VersionContext().RawBuildID())); + AppData_Wine_WineVersion_OpenMPTVersion = AppData_Wine_WineVersion.WithTrailingSlash() + mpt::PathString::FromUTF8(GetOpenMPTVersion()); + CreatePath(AppData); + CreatePath(AppData_Wine); + CreatePath(AppData_Wine_WineVersion); + CreatePath(AppData_Wine_WineVersion_OpenMPTVersion); + Host_AppData = wine.PathToPosixCanonical(AppData); + Host_AppData_Wine = wine.PathToPosixCanonical(AppData_Wine); + Host_AppData_Wine_WineVersion = wine.PathToPosixCanonical(AppData_Wine_WineVersion); + Host_AppData_Wine_WineVersion_OpenMPTVersion = wine.PathToPosixCanonical(AppData_Wine_WineVersion_OpenMPTVersion); + Host_Native_OpenMPT_Wine_WineVersion_OpenMPTVersion = wine.XDG_DATA_HOME() + "/OpenMPT/Wine/" + SanitizeBuildID(wine.VersionContext().RawBuildID()) + "/" + GetOpenMPTVersion(); + } + }; + + const Paths paths(wine); + + const std::string nativeSearchPath = paths.Host_Native_OpenMPT_Wine_WineVersion_OpenMPTVersion; + + if(!TrackerSettings::Instance().WineSupportAlwaysRecompile) + { + if((paths.AppData_Wine_WineVersion_OpenMPTVersion.WithTrailingSlash() + P_("success.txt")).IsFile()) + { + theApp.SetWineWrapperDllFilename(paths.AppData_Wine_WineVersion_OpenMPTVersion.WithTrailingSlash() + P_("openmpt_wine_wrapper.dll")); + return; + } + } + + if(TrackerSettings::Instance().WineSupportAskCompile) + { + mpt::ustring msg; + msg += U_("OpenMPT Wine integration requires recompilation and will not work otherwise.\n"); + msg += U_("Recompile now?\n"); + if(Reporting::Confirm(msg, WineGetWindowTitle(), false, false) != cnfYes) + { + return; + } + } + + std::map<std::string, std::vector<char> > filetree; + filetree = UnzipToMap(theApp.GetInstallPath() + WineGetSupportZipFilename()); + + Util::Wine::Dialog dialog(WineGetWindowTitleUTF8(), TrackerSettings::Instance().WineSupportCompileVerbosity < 6); + + std::string script; + + script += std::string() + "#!/usr/bin/env sh" + "\n"; + script += std::string() + "\n"; + script += std::string() + "touch message.txt" + "\n"; + script += std::string() + "\n"; + + script += dialog.Detect(); + + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 6) + { + script += std::string() + "echo Working directory:" + "\n"; + script += std::string() + "pwd" + "\n"; + } + + script += std::string() + "\n"; + + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 3) + { + script += std::string() + "echo " + WineGetWindowTitleUTF8() + "\n"; + } else + { + if(TrackerSettings::Instance().WineSupportCompileVerbosity == 2) + { + script += std::string() + "{" + "\n"; + script += std::string() + " echo 0" + "\n"; + script += std::string() + " echo 100" + "\n"; + script += std::string() + "} | " + dialog.Progress("[>>] Prepare OpenMPT Wine Integration\\n[ ] Compile native support\\n[ ] Compile Wine wrapper\\n\\n[1/3] Preparing OpenMPT Wine Integration ...") + "\n"; + } else + { + script += std::string() + dialog.Status("Preparing OpenMPT Wine Integration.") + "\n"; + } + } + + script += std::string() + "\n"; + + script += std::string() + "printf \"#pragma once\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_URL \\\"" + mpt::ToCharset(mpt::Charset::ASCII, SourceInfo::Current().Url()) + "\\\"\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_DATE \\\"" + mpt::ToCharset(mpt::Charset::ASCII, SourceInfo::Current().Date()) + "\\\"\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_REVISION " + mpt::afmt::dec(SourceInfo::Current().Revision()) + "\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_DIRTY " + mpt::afmt::dec(SourceInfo::Current().IsDirty()) + "\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_MIXEDREVISIONS " + mpt::afmt::dec(SourceInfo::Current().HasMixedRevisions()) + "\\n\" >> common/svn_version.h" + "\n"; + script += std::string() + "printf \"#define OPENMPT_VERSION_IS_PACKAGE " + mpt::afmt::dec(SourceInfo::Current().IsPackage()) + "\\n\" >> common/svn_version.h" + "\n"; + + script += std::string() + "\n"; + + script += std::string() + "missing=" + "\n"; + + script += std::string() + "\n"; + + const std::string make = ((wineVersion.HostClass() == mpt::osinfo::osclass::BSD) ? "gmake" : "make"); + + std::vector<std::string> commands; + commands.push_back(make); + commands.push_back("pkg-config"); + commands.push_back("cpp"); + commands.push_back("cc"); + commands.push_back("c++"); + commands.push_back("ld"); + commands.push_back("ccache"); + for(const auto &command : commands) + { + script += std::string() + "command -v " + command + " 2>/dev/null 1>/dev/null" + "\n"; + script += std::string() + "if [ \"$?\" -ne \"0\" ] ; then" + "\n"; + script += std::string() + " missing=\"$missing " + command + "\"" + "\n"; + script += std::string() + "fi" + "\n"; + } + + script += std::string() + "if [ \"x$missing\" = \"x\" ] ; then" + "\n"; + script += std::string() + " printf \"\"" + "\n"; + script += std::string() + "else" + "\n"; +#if 0 + if(!TrackerSettings::Instance().WineSupportSilentCompile >= 1) + { + script += std::string() + " " + dialog.YesNo("The following commands are missing:\\n\\n$missing\\n\\nDo you want OpenMPT to try installing those now?") + "\n"; + } + script += std::string() + " if [ \"$?\" -ne \"0\" ] ; then" + "\n"; + script += std::string() + " exit 1" + "\n"; + script += std::string() + " fi" + "\n"; +#else + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("The following commands are missing:\\n\\n$missing\\n\\nPlease install them with your system package installer.") + "\n"; + } + script += std::string() + " exit 1" + "\n"; +#endif + script += std::string() + "fi" + "\n"; + + script += std::string() + "\n"; + + script += std::string() + "mkdir -p " + wine.EscapePosixShell(wine.XDG_DATA_HOME()) + "/OpenMPT/Wine" + "\n"; + script += std::string() + "mkdir -p " + wine.EscapePosixShell(wine.XDG_CACHE_HOME()) + "/OpenMPT/Wine" + "\n"; + script += std::string() + "mkdir -p " + wine.EscapePosixShell(wine.XDG_CONFIG_HOME()) + "/OpenMPT/Wine" + "\n"; + + script += std::string() + "mkdir -p " + wine.EscapePosixShell(paths.Host_Native_OpenMPT_Wine_WineVersion_OpenMPTVersion) + "\n"; + + script += std::string() + "\n"; + + script += std::string() + "CCACHE_DIR=" + wine.EscapePosixShell(wine.XDG_CACHE_HOME()) + "/OpenMPT/Wine/ccache" + "\n"; + script += std::string() + "CCACHE_COMPRESS=1" + " \n"; + script += std::string() + "export CCACHE_DIR" + " \n"; + script += std::string() + "export CCACHE_COMPRESS" + " \n"; + + script += std::string() + "\n"; + + std::vector<std::string> winegcc; + if constexpr(mpt::arch_bits == 32) + { // 32bit winegcc probably cannot compile to 64bit + winegcc.push_back("winegcc32-development"); + } + MPT_MAYBE_CONSTANT_IF(TrackerSettings::Instance().WineSupportForeignOpenMPT || (mpt::arch_bits == 64)) + { + winegcc.push_back("winegcc64-development"); + } + winegcc.push_back("winegcc-development"); + if(wineVersion.HostClass() != mpt::osinfo::osclass::BSD) + { // avoid C++ compiler on *BSD because libc++ Win32 support tends to be missing there. + if constexpr(mpt::arch_bits == 32) + { // 32bit winegcc probably cannot compile to 64bit + winegcc.push_back("wineg++32-development"); + } + MPT_MAYBE_CONSTANT_IF(TrackerSettings::Instance().WineSupportForeignOpenMPT || (mpt::arch_bits == 64)) + { + winegcc.push_back("wineg++64-development"); + } + winegcc.push_back("wineg++-development"); + } + if constexpr(mpt::arch_bits == 32) + { // 32bit winegcc probably cannot compile to 64bit + winegcc.push_back("winegcc32"); + } + MPT_MAYBE_CONSTANT_IF(TrackerSettings::Instance().WineSupportForeignOpenMPT || (mpt::arch_bits == 64)) + { + winegcc.push_back("winegcc64"); + } + winegcc.push_back("winegcc"); + if(wineVersion.HostClass() != mpt::osinfo::osclass::BSD) + { // avoid C++ compiler on *BSD because libc++ Win32 support tends to be missing there. + if constexpr(mpt::arch_bits == 32) + { // 32bit winegcc probably cannot compile to 64bit + winegcc.push_back("wineg++32"); + } + MPT_MAYBE_CONSTANT_IF(TrackerSettings::Instance().WineSupportForeignOpenMPT || (mpt::arch_bits == 64)) + { + winegcc.push_back("wineg++64"); + } + winegcc.push_back("wineg++"); + } + for(const auto &c : winegcc) + { + script += std::string() + "if command -v " + c + " 2>/dev/null 1>/dev/null ; then" + "\n"; + script += std::string() + " MPT_WINEGXX=" + c + "\n"; + script += std::string() + "fi" + "\n"; + } + script += std::string() + "if [ -z $MPT_WINEGXX ] ; then" + "\n"; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("WineGCC not found.\\nPlease install it with your system package installer.") + "\n"; + } + script += std::string() + " exit 1" + "\n"; + script += std::string() + "fi" + "\n"; + + // Work-around for Debian 8, Wine 1.6.2 + MPT_MAYBE_CONSTANT_IF(TrackerSettings::Instance().WineSupportForeignOpenMPT || (mpt::arch_bits == 64)) + { + script += std::string() + "if [ `$MPT_WINEGXX > /dev/null 2>&1 ; echo $?` -eq 127 ] ; then" + "\n"; + script += std::string() + " if command -v /usr/lib/x86_64-linux-gnu/wine/bin/winegcc 2>/dev/null 1>/dev/null ; then" + "\n"; + script += std::string() + " MPT_WINEGXX=/usr/lib/x86_64-linux-gnu/wine/bin/winegcc"+ "\n"; + script += std::string() + " PATH=/usr/lib/x86_64-linux-gnu/wine/bin:\"${PATH}\"" + "\n"; + script += std::string() + " export PATH" + "\n"; + script += std::string() + " fi" + "\n"; + script += std::string() + "fi" + "\n"; + } + if constexpr(mpt::arch_bits == 32) + { + script += std::string() + "if [ `$MPT_WINEGXX > /dev/null 2>&1 ; echo $?` -eq 127 ] ; then" + "\n"; + script += std::string() + " if command -v /usr/lib/i386-linux-gnu/wine/bin/winegcc 2>/dev/null 1>/dev/null ; then" + "\n"; + script += std::string() + " MPT_WINEGXX=/usr/lib/i386-linux-gnu/wine/bin/winegcc" + "\n"; + script += std::string() + " PATH=/usr/lib/i386-linux-gnu/wine/bin:\"${PATH}\"" + "\n"; + script += std::string() + " export PATH" + "\n"; + script += std::string() + " fi" + "\n"; + script += std::string() + "fi" + "\n"; + } + + std::string features; + if(TrackerSettings::Instance().WineSupportForeignOpenMPT) + { + features += std::string() + " " + "MPT_ARCH_BITS=" + mpt::afmt::dec(mpt::arch_bits); + if constexpr(mpt::arch_bits == 64) + { + features += std::string() + " " + "MPT_TARGET=" + "x86_64-linux-gnu-"; + } else + { + features += std::string() + " " + "MPT_TARGET=" + "i686-linux-gnu-"; + } + } + features += std::string() + " " + "MPT_TRY_PORTAUDIO=" + mpt::afmt::dec(TrackerSettings::Instance().WineSupportEnablePortAudio.Get()); + features += std::string() + " " + "MPT_TRY_PULSEAUDIO=" + mpt::afmt::dec(TrackerSettings::Instance().WineSupportEnablePulseAudio.Get()); + features += std::string() + " " + "MPT_TRY_RTAUDIO=" + mpt::afmt::dec(TrackerSettings::Instance().WineSupportEnableRtAudio.Get()); + + int makeverbosity = Clamp(TrackerSettings::Instance().WineSupportCompileVerbosity.Get(), 0, 6); + + if(TrackerSettings::Instance().WineSupportCompileVerbosity == 2) + { + + script += std::string() + "{" + "\n"; + script += std::string() + " echo 0" + "\n"; + script += std::string() + " " + make + " -j " + mpt::afmt::dec(std::max(std::thread::hardware_concurrency(), static_cast<unsigned int>(1))) + " -f build/wine/native_support.mk" + " V=" + mpt::afmt::dec(makeverbosity) + " " + features + " all MPT_PROGRESS_FILE=\"&4\" 4>&1 1>stdout.txt 2>stderr.txt" + "\n"; + script += std::string() + " echo -n $? > stdexit.txt" + "\n"; + script += std::string() + " echo 100" + "\n"; + script += std::string() + "} | " + dialog.Progress("[OK] Prepare OpenMPT Wine Integration\\n[>>] Compile native support\\n[ ] Compile Wine wrapper\\n\\n[2/3] Compiling native support ...") + "\n"; + script += std::string() + "MPT_EXITCODE=`cat stdexit.txt`" + "\n"; + script += std::string() + "if [ \"$MPT_EXITCODE\" -ne \"0\" ] ; then" + "\n"; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("OpenMPT Wine integration failed to compile.") + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + } + script += std::string() + " exit 1" + "\n"; + script += std::string() + "fi" + "\n"; + script += std::string() + "if [ -s stderr.txt ] ; then" + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + script += std::string() + "fi" + "\n"; + + script += std::string() + "{" + "\n"; + script += std::string() + " echo 0" + "\n"; + script += std::string() + " " + make + " -j " + mpt::afmt::dec(std::max(std::thread::hardware_concurrency(), static_cast<unsigned int>(1))) + " -f build/wine/wine_wrapper.mk" + " V=" + mpt::afmt::dec(makeverbosity) + " WINEGXX=$MPT_WINEGXX " + "MPT_WINEGCC_LANG=" + ((wineVersion.HostClass() == mpt::osinfo::osclass::BSD) ? "C" : "CPLUSPLUS") + " MPT_WINE_SEARCHPATH=" + wine.EscapePosixShell(nativeSearchPath) + " all MPT_PROGRESS_FILE=\"&4\" 4>&1 1>stdout.txt 2>stderr.txt" + "\n"; + script += std::string() + " echo -n $? > stdexit.txt" + "\n"; + script += std::string() + " echo 100" + "\n"; + script += std::string() + "} | " + dialog.Progress("[OK] Prepare OpenMPT Wine Integration\\n[OK] Compile native support\\n[>>] Compile Wine wrapper\\n\\n[3/3] Compiling Wine wrapper ...") + "\n"; + script += std::string() + "MPT_EXITCODE=`cat stdexit.txt`" + "\n"; + script += std::string() + "if [ \"$MPT_EXITCODE\" -ne \"0\" ] ; then" + "\n"; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("OpenMPT Wine integration failed to compile.") + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + } + script += std::string() + " exit 1" + "\n"; + script += std::string() + "fi" + "\n"; + script += std::string() + "if [ -s stderr.txt ] ; then" + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + script += std::string() + "fi" + "\n"; + + } else + { + + script += std::string() + "" + make + " -j " + mpt::afmt::dec(std::max(std::thread::hardware_concurrency(), static_cast<unsigned int>(1))) + " -f build/wine/native_support.mk" + " V=" + mpt::afmt::dec(makeverbosity) + " " + features + " all" + "\n"; + script += std::string() + "if [ \"$?\" -ne \"0\" ] ; then" + "\n"; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("OpenMPT Wine integration failed to compile.") + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + } + script += std::string() + " exit 1" + "\n"; + script += std::string() + "fi" + "\n"; + script += std::string() + "if [ -s stderr.txt ] ; then" + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + script += std::string() + "fi" + "\n"; + + script += std::string() + "" + make + " -j " + mpt::afmt::dec(std::max(std::thread::hardware_concurrency(), static_cast<unsigned int>(1))) + " -f build/wine/wine_wrapper.mk" + " V=" + mpt::afmt::dec(makeverbosity) + " WINEGXX=$MPT_WINEGXX " + "MPT_WINEGCC_LANG=" + ((wineVersion.HostClass() == mpt::osinfo::osclass::BSD) ? "C" : "CPLUSPLUS") + " MPT_WINE_SEARCHPATH=" + wine.EscapePosixShell(nativeSearchPath) + " all" + "\n"; + script += std::string() + "if [ \"$?\" -ne \"0\" ] ; then" + "\n"; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + script += std::string() + " " + dialog.MessageBox("OpenMPT Wine integration failed to compile.") + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + } + script += std::string() + " exit 1" + "\n"; + script += std::string() + "fi" + "\n"; + script += std::string() + "if [ -s stderr.txt ] ; then" + "\n"; + script += std::string() + " " + dialog.TextBox("stderr.txt") + "\n"; + script += std::string() + "fi" + "\n"; + + } + + script += std::string() + "\n"; + + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 6) + { + script += std::string() + dialog.MessageBox("OpenMPT Wine integration compiled successfully.") + "\n"; + } + + script += std::string() + "\n"; + + script += "exit 0" "\n"; + + CMainFrame::GetMainFrame()->EnableWindow(FALSE); + mpt::Wine::ExecResult result; + try + { + FlagSet<mpt::Wine::ExecFlags> flags = mpt::Wine::ExecFlagNone; + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 1) + { + flags = (mpt::Wine::ExecFlagProgressWindow | mpt::Wine::ExecFlagInteractive); + } else if(TrackerSettings::Instance().WineSupportCompileVerbosity == 0) + { + flags = (mpt::Wine::ExecFlagProgressWindow | mpt::Wine::ExecFlagSilent); + } else + { + flags = (mpt::Wine::ExecFlagSilent); + } + result = Util::Wine::ExecutePosixShellScript + ( wine + , script + , flags + , filetree + , WineGetWindowTitleUTF8() + , "Compiling Wine support ..." + ); + } catch(const mpt::Wine::Exception & /* e */ ) + { + CMainFrame::GetMainFrame()->EnableWindow(TRUE); + throw; + } + CMainFrame::GetMainFrame()->EnableWindow(TRUE); + if(result.exitcode != 0) + { + if(result.filetree["message.txt"].size() > 0) + { + throw mpt::Wine::Exception(std::string(result.filetree["message.txt"].begin(), result.filetree["message.txt"].end())); + } else + { + throw mpt::Wine::Exception("Executing Wine integration build script failed."); + } + } + + { + std::string fn = "libopenmpt_native_support.so"; + mpt::ofstream f(wine.PathToWindows(nativeSearchPath) + P_("\\") + mpt::PathString::FromUTF8(fn), std::ios::binary); + f.write(&result.filetree[fn][0], result.filetree[fn].size()); + f.flush(); + if(!f) + { + throw mpt::Wine::Exception("Writing libopenmpt_native_support.so failed."); + } + } + { + std::string fn = "openmpt_wine_wrapper.dll"; + mpt::ofstream f(paths.AppData_Wine_WineVersion_OpenMPTVersion + P_("\\") + mpt::PathString::FromUTF8(fn), std::ios::binary); + f.write(&result.filetree[fn][0], result.filetree[fn].size()); + f.flush(); + if(!f) + { + throw mpt::Wine::Exception("Writing openmpt_wine_wrapper.dll failed."); + } + } + { + std::string fn = "success.txt"; + mpt::ofstream f(paths.AppData_Wine_WineVersion_OpenMPTVersion + P_("\\") + mpt::PathString::FromUTF8(fn), std::ios::binary); + f.imbue(std::locale::classic()); + f << std::string("1"); + f.flush(); + if(!f) + { + throw mpt::Wine::Exception("Writing success.txt failed."); + } + } + + theApp.SetWineWrapperDllFilename(paths.AppData_Wine_WineVersion_OpenMPTVersion + P_("\\") + P_("openmpt_wine_wrapper.dll")); + + } catch(const mpt::Wine::Exception &e) + { + Reporting::Error(U_("Setting up OpenMPT Wine integration failed: ") + mpt::get_exception_text<mpt::ustring>(e), WineGetWindowTitle()); + } + +} + + +} // namespace WineIntegration + + +std::string ComponentWineWrapper::result_as_string(char * str) const +{ + std::string result = str; + OpenMPT_Wine_Wrapper_String_Free(str); + return result; +} + +ComponentWineWrapper::ComponentWineWrapper() + : ComponentLibrary(ComponentTypeBundled) +{ + return; +} + +bool ComponentWineWrapper::DoInitialize() +{ + if(theApp.GetWineWrapperDllFilename().empty()) + { + return false; + } + AddLibrary("WineWrapper", mpt::LibraryPath::FullPath(theApp.GetWineWrapperDllFilename())); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_Init); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_Fini); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_String_Free); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_EnumerateDevices); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Construct); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Destruct); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_SetMessageReceiver); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_SetCallback); + + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceInfo); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceCaps); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceDynamicCaps); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Init); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Open); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Close); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Start); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_Stop); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetRequestFlags); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_IsInited); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_IsOpen); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_IsAvailable); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_IsPlaying); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_IsPlayingSilence); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_StopAndAvoidPlayingSilence); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_EndPlayingSilence); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_OnIdle); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetSettings); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetActualSampleFormat); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetEffectiveBufferAttributes); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetTimeInfo); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetStreamPosition); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_DebugIsFragileDevice); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_DebugInRealtimeCallback); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_GetStatistics); + MPT_COMPONENT_BIND("WineWrapper", OpenMPT_Wine_Wrapper_SoundDevice_OpenDriverSettings); + + if(HasBindFailed()) + { + Reporting::Error("OpenMPT Wine integration failed loading.", WineGetWindowTitle()); + return false; + } + if(OpenMPT_Wine_Wrapper_Init() != 0) + { + Reporting::Error("OpenMPT Wine integration initialization failed.", WineGetWindowTitle()); + return false; + } + if(TrackerSettings::Instance().WineSupportCompileVerbosity >= 6) + { + Reporting::Notification(MPT_AFORMAT("OpenMPT Wine integration loaded successfully.")(), WineGetWindowTitle()); + } + return true; +} + +ComponentWineWrapper::~ComponentWineWrapper() +{ + if(IsAvailable()) + { + OpenMPT_Wine_Wrapper_Fini(); + } +} + + +namespace WineIntegration { + + +void Load() +{ + ReloadComponent<ComponentWineWrapper>(); +} + + +} // namespace WineIntegration + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.h b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.h new file mode 100644 index 00000000..efdd9afc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MPTrackWine.h @@ -0,0 +1,135 @@ +/* + * MPTrackWine.h + * ------------- + * Purpose: OpenMPT Wine support functions. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "../common/ComponentManager.h" + + +extern "C" { + +typedef void OpenMPT_int24; + +typedef struct OpenMPT_SoundDevice_StreamPosition OpenMPT_SoundDevice_StreamPosition; +typedef struct OpenMPT_SoundDevice_TimeInfo OpenMPT_SoundDevice_TimeInfo; +typedef struct OpenMPT_SoundDevice_Flags OpenMPT_SoundDevice_Flags; +typedef struct OpenMPT_SoundDevice_BufferFormat OpenMPT_SoundDevice_BufferFormat; +typedef struct OpenMPT_SoundDevice_BufferAttributes OpenMPT_SoundDevice_BufferAttributes; +typedef struct OpenMPT_SoundDevice_RequestFlags OpenMPT_SoundDevice_RequestFlags; +typedef struct OpenMPT_SoundDevice OpenMPT_SoundDevice; + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver { + void * inst; + void (__cdecl * SoundDeviceMessageFunc)( void * inst, uintptr_t level, const char * message ); +} OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver; + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice_ICallback { + void * inst; + // main thread + void (__cdecl * SoundCallbackGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (__cdecl * SoundCallbackPreStartFunc)( void * inst ); + void (__cdecl * SoundCallbackPostStopFunc)( void * inst ); + void (__cdecl * SoundCallbackIsLockedByCurrentThreadFunc)( void * inst, uintptr_t * result ); + // audio thread + void (__cdecl * SoundCallbackLockFunc)( void * inst ); + void (__cdecl * SoundCallbackLockedGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (__cdecl * SoundCallbackLockedProcessPrepareFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (__cdecl * SoundCallbackLockedProcessUint8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, uint8_t * buffer, const uint8_t * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessInt8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int8_t * buffer, const int8_t * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessInt16Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int16_t * buffer, const int16_t * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessInt24Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, OpenMPT_int24 * buffer, const OpenMPT_int24 * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessInt32Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int32_t * buffer, const int32_t * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessFloatFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, float * buffer, const float * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessDoubleFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, double * buffer, const double * inputBuffer ); + void (__cdecl * SoundCallbackLockedProcessDoneFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (__cdecl * SoundCallbackUnlockFunc)( void * inst ); +} OpenMPT_Wine_Wrapper_SoundDevice_ICallback; + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice OpenMPT_Wine_Wrapper_SoundDevice; + +}; + +OPENMPT_NAMESPACE_BEGIN + + +namespace WineIntegration { + +void Initialize(); + +bool IsSupported(); + +bool IsCompiled(); + +void Load(); + +} // namespace WineIntegration + + +class ComponentWineWrapper + : public ComponentLibrary +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentWineWrapper, "WineWrapper") + +public: + +private: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_Init)(void) = nullptr; +private: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_Fini)(void) = nullptr; + +private: void (__cdecl * OpenMPT_Wine_Wrapper_String_Free)(char * str) = nullptr; + +private: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_EnumerateDevices)(void) = nullptr; +public: std::string SoundDevice_EnumerateDevices() const { return result_as_string(OpenMPT_Wine_Wrapper_SoundDevice_EnumerateDevices()); } + +public: OpenMPT_Wine_Wrapper_SoundDevice * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Construct)( const char * info ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Destruct)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; + +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_SetMessageReceiver)( OpenMPT_Wine_Wrapper_SoundDevice * sd, const OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver * receiver ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_SetCallback)( OpenMPT_Wine_Wrapper_SoundDevice * sd, const OpenMPT_Wine_Wrapper_SoundDevice_ICallback * callback ) = nullptr; + +public: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceInfo)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceCaps)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceDynamicCaps)( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * baseSampleRates ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Init)( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * appInfo ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Open)( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * settings ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Close)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Start)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_Stop)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetRequestFlags)( const OpenMPT_Wine_Wrapper_SoundDevice * sd, uint32_t * result ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_IsInited)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_IsOpen)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_IsAvailable)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_IsPlaying)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_IsPlayingSilence)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_StopAndAvoidPlayingSilence)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_EndPlayingSilence)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_OnIdle)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetSettings)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetActualSampleFormat)( const OpenMPT_Wine_Wrapper_SoundDevice * sd, int32_t * result ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetEffectiveBufferAttributes)( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_BufferAttributes * result ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetTimeInfo)( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_TimeInfo * result ) = nullptr; +public: void (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetStreamPosition)( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_StreamPosition * result ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_DebugIsFragileDevice)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_DebugInRealtimeCallback)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: char * (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_GetStatistics)( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; +public: uintptr_t (__cdecl * OpenMPT_Wine_Wrapper_SoundDevice_OpenDriverSettings)( OpenMPT_Wine_Wrapper_SoundDevice * sd ) = nullptr; + +public: std::string result_as_string(char * str) const; + +public: + ComponentWineWrapper(); + bool DoInitialize(); + virtual ~ComponentWineWrapper(); +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MainFrm.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MainFrm.cpp new file mode 100644 index 00000000..52686070 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MainFrm.cpp @@ -0,0 +1,3257 @@ +/* + * MainFrm.cpp + * ----------- + * Purpose: Implementation of OpenMPT's main window code. + * 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 "ModDocTemplate.h" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "openmpt/sounddevice/SoundDeviceBuffer.hpp" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "../soundlib/AudioReadTarget.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "Childfrm.h" +#include "Dlsbank.h" +#include "Mpdlgs.h" +#include "FolderScanner.h" +#include "KeyConfigDlg.h" +#include "PathConfigDlg.h" +#include "GeneralConfigDlg.h" +#include "ColorConfigDlg.h" +#include "SampleConfigDlg.h" +#include "AdvancedConfigDlg.h" +#include "AutoSaver.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "Globals.h" +#include "ChannelManagerDlg.h" +#include "../common/version.h" +#include "UpdateCheck.h" +#include "CloseMainDialog.h" +#include "SelectPluginDialog.h" +#include "PatternClipboard.h" +#include "PatternFont.h" +#include "../common/mptFileIO.h" +#include "../common/FileReader.h" +#include "../common/Profiler.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../soundlib/plugins/PluginManager.h" +#include "Vstplug.h" +#include "FileDialog.h" +#include "ProgressDialog.h" +#include <HtmlHelp.h> +#include <Dbt.h> // device change messages +#include "mpt/audio/span.hpp" + + +OPENMPT_NAMESPACE_BEGIN + +#define TIMERID_GUI 1 +#define TIMERID_NOTIFY 2 + +#define MPTTIMER_PERIOD 200 + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame + +IMPLEMENT_DYNAMIC(CMainFrame, CMDIFrameWnd) + +BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd) + //{{AFX_MSG_MAP(CMainFrame) + ON_WM_TIMER() + ON_WM_CLOSE() + ON_WM_CREATE() + ON_WM_RBUTTONDOWN() + ON_WM_DEVICECHANGE() + ON_WM_DROPFILES() + ON_WM_QUERYENDSESSION() + ON_COMMAND(ID_VIEW_OPTIONS, &CMainFrame::OnViewOptions) + + ON_COMMAND(ID_PLUGIN_SETUP, &CMainFrame::OnPluginManager) + ON_COMMAND(ID_CLIPBOARD_MANAGER, &CMainFrame::OnClipboardManager) + //ON_COMMAND(ID_HELP, CMDIFrameWnd::OnHelp) + ON_COMMAND(ID_REPORT_BUG, &CMainFrame::OnReportBug) + ON_COMMAND(ID_NEXTOCTAVE, &CMainFrame::OnNextOctave) + ON_COMMAND(ID_PREVOCTAVE, &CMainFrame::OnPrevOctave) + ON_COMMAND_RANGE(ID_FILE_OPENTEMPLATE, ID_FILE_OPENTEMPLATE_LASTINRANGE, &CMainFrame::OnOpenTemplateModule) + ON_COMMAND(ID_ADD_SOUNDBANK, &CMainFrame::OnAddDlsBank) + ON_COMMAND(ID_IMPORT_MIDILIB, &CMainFrame::OnImportMidiLib) + ON_COMMAND(ID_MIDI_RECORD, &CMainFrame::OnMidiRecord) + ON_COMMAND(ID_PANIC, &CMainFrame::OnPanic) + ON_COMMAND(ID_PLAYER_PAUSE, &CMainFrame::OnPlayerPause) + ON_COMMAND_RANGE(ID_EXAMPLE_MODULES, ID_EXAMPLE_MODULES_LASTINRANGE, &CMainFrame::OnExampleSong) + ON_COMMAND_EX(IDD_TREEVIEW, &CMainFrame::OnBarCheck) + ON_COMMAND_EX(ID_NETLINK_MODPLUG, &CMainFrame::OnInternetLink) + ON_COMMAND_EX(ID_NETLINK_TOP_PICKS, &CMainFrame::OnInternetLink) + ON_UPDATE_COMMAND_UI(ID_MIDI_RECORD, &CMainFrame::OnUpdateMidiRecord) + ON_UPDATE_COMMAND_UI(ID_INDICATOR_TIME, &CMainFrame::OnUpdateTime) + ON_UPDATE_COMMAND_UI(ID_INDICATOR_USER, &CMainFrame::OnUpdateUser) + ON_UPDATE_COMMAND_UI(ID_INDICATOR_INFO, &CMainFrame::OnUpdateInfo) + ON_UPDATE_COMMAND_UI(ID_INDICATOR_XINFO,&CMainFrame::OnUpdateXInfo) + ON_UPDATE_COMMAND_UI(IDD_TREEVIEW, &CMainFrame::OnUpdateControlBarMenu) + ON_MESSAGE(WM_MOD_UPDATEPOSITION, &CMainFrame::OnUpdatePosition) + ON_MESSAGE(WM_MOD_INVALIDATEPATTERNS, &CMainFrame::OnInvalidatePatterns) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CMainFrame::OnCustomKeyMsg) + ON_MESSAGE(WM_MOD_MIDIMAPPING, &CMainFrame::OnViewMIDIMapping) + ON_MESSAGE(WM_MOD_UPDATEVIEWS, &CMainFrame::OnUpdateViews) + ON_MESSAGE(WM_MOD_SETMODIFIED, &CMainFrame::OnSetModified) +#if defined(MPT_ENABLE_UPDATE) + ON_MESSAGE(WM_MOD_UPDATENOTIFY, &CMainFrame::OnToolbarUpdateIndicatorClick) +#endif // MPT_ENABLE_UPDATE + ON_COMMAND(ID_INTERNETUPDATE, &CMainFrame::OnInternetUpdate) + ON_COMMAND(ID_UPDATE_AVAILABLE, &CMainFrame::OnUpdateAvailable) + ON_COMMAND(ID_HELP_SHOWSETTINGSFOLDER, &CMainFrame::OnShowSettingsFolder) +#if defined(MPT_ENABLE_UPDATE) + ON_MESSAGE(MPT_WM_APP_UPDATECHECK_START, &CMainFrame::OnUpdateCheckStart) + ON_MESSAGE(MPT_WM_APP_UPDATECHECK_PROGRESS, &CMainFrame::OnUpdateCheckProgress) + ON_MESSAGE(MPT_WM_APP_UPDATECHECK_CANCELED, &CMainFrame::OnUpdateCheckCanceled) + ON_MESSAGE(MPT_WM_APP_UPDATECHECK_FAILURE, &CMainFrame::OnUpdateCheckFailure) + ON_MESSAGE(MPT_WM_APP_UPDATECHECK_SUCCESS, &CMainFrame::OnUpdateCheckSuccess) +#endif // MPT_ENABLE_UPDATE + ON_COMMAND(ID_HELPSHOW, &CMainFrame::OnHelp) + + ON_COMMAND_RANGE(ID_MRU_LIST_FIRST, ID_MRU_LIST_LAST, &CMainFrame::OnOpenMRUItem) + ON_UPDATE_COMMAND_UI(ID_MRU_LIST_FIRST, &CMainFrame::OnUpdateMRUItem) + //}}AFX_MSG_MAP + ON_WM_INITMENU() + ON_WM_KILLFOCUS() + ON_WM_MOUSEWHEEL() + ON_WM_SHOWWINDOW() + ON_WM_ACTIVATEAPP() +END_MESSAGE_MAP() + +// Globals +OptionsPage CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_DEFAULT; +HHOOK CMainFrame::ghKbdHook = NULL; + +// GDI +HICON CMainFrame::m_hIcon = NULL; +HFONT CMainFrame::m_hGUIFont = NULL; +HFONT CMainFrame::m_hFixedFont = NULL; +HPEN CMainFrame::penDarkGray = NULL; +HPEN CMainFrame::penGray99 = NULL; +HPEN CMainFrame::penHalfDarkGray = NULL; + +HCURSOR CMainFrame::curDragging = NULL; +HCURSOR CMainFrame::curArrow = NULL; +HCURSOR CMainFrame::curNoDrop = NULL; +HCURSOR CMainFrame::curNoDrop2 = NULL; +HCURSOR CMainFrame::curVSplit = NULL; +MODPLUGDIB *CMainFrame::bmpNotes = nullptr; +MODPLUGDIB *CMainFrame::bmpVUMeters = nullptr; +MODPLUGDIB *CMainFrame::bmpPluginVUMeters = nullptr; +COLORREF CMainFrame::gcolrefVuMeter[NUM_VUMETER_PENS*2]; + +CInputHandler *CMainFrame::m_InputHandler = nullptr; + +static UINT indicators[] = +{ + ID_SEPARATOR, // status line indicator + ID_INDICATOR_XINFO, + ID_INDICATOR_INFO, + ID_INDICATOR_USER, + ID_INDICATOR_TIME, +}; + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame construction/destruction +CMainFrame::CMainFrame() + : SoundDevice::CallbackBufferHandler<DithersOpenMPT>(theApp.PRNG()) + , m_SoundDeviceFillBufferCriticalSection(CriticalSection::InitialState::Unlocked) +{ + m_szUserText[0] = 0; + m_szInfoText[0] = 0; + m_szXInfoText[0]= 0; + + MemsetZero(gcolrefVuMeter); + + m_InputHandler = new CInputHandler(this); + +} + + +void CMainFrame::Initialize() +{ + //Adding version number to the frame title + CString title = GetTitle(); + title += _T(" ") + mpt::cfmt::val(Version::Current()); + #if defined(MPT_BUILD_RETRO) + title += _T(" RETRO"); + #endif // MPT_BUILD_RETRO + if(Build::IsDebugBuild()) + { + title += _T(" DEBUG"); + } + #ifndef MPT_WITH_VST + title += _T(" NO_VST"); + #endif + #ifndef MPT_WITH_DMO + title += _T(" NO_DMO"); + #endif + #ifdef NO_PLUGINS + title += _T(" NO_PLUGINS"); + #endif + SetTitle(title); + OnUpdateFrameTitle(false); + + // Check for valid sound device + SoundDevice::Identifier dev = TrackerSettings::Instance().GetSoundDeviceIdentifier(); + if(!theApp.GetSoundDevicesManager()->FindDeviceInfo(dev).IsValid()) + { + dev = theApp.GetSoundDevicesManager()->FindDeviceInfoBestMatch(dev).GetIdentifier(); + TrackerSettings::Instance().SetSoundDeviceIdentifier(dev); + } + + // Setup timer + OnUpdateUser(NULL); + m_nTimer = SetTimer(TIMERID_GUI, MPTTIMER_PERIOD, NULL); + + // Setup Keyboard Hook + ghKbdHook = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, AfxGetInstanceHandle(), GetCurrentThreadId()); + + // Update the tree + m_wndTree.Init(); + + CreateExampleModulesMenu(); + CreateTemplateModulesMenu(); + UpdateMRUList(); +} + + +CMainFrame::~CMainFrame() +{ + delete m_InputHandler; + CChannelManagerDlg::DestroySharedInstance(); +} + + +int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) +{ + if(CMDIFrameWnd::OnCreate(lpCreateStruct) == -1) + return -1; + // Load resources + m_hIcon = theApp.LoadIcon(IDR_MAINFRAME); + + // Toolbar and other icons + CDC *dc = GetDC(); + const double scaling = Util::GetDPIx(m_hWnd) / 96.0; + static constexpr int miscIconsInvert[] = {IMAGE_PATTERNS, IMAGE_OPLINSTRACTIVE, IMAGE_OPLINSTRMUTE}; + static constexpr int patternIconsInvert[] = {TIMAGE_PREVIEW, TIMAGE_MACROEDITOR, TIMAGE_PATTERN_OVERFLOWPASTE, TIMAGE_PATTERN_DETAIL_LO, TIMAGE_PATTERN_DETAIL_MED, TIMAGE_PATTERN_DETAIL_HI, TIMAGE_PATTERN_PLUGINS, TIMAGE_SAMPLE_UNSIGN}; + static constexpr int envelopeIconsInvert[] = {IIMAGE_CHECKED, IIMAGE_VOLSWITCH, IIMAGE_PANSWITCH, IIMAGE_PITCHSWITCH, IIMAGE_FILTERSWITCH, IIMAGE_NOPITCHSWITCH, IIMAGE_NOFILTERSWITCH}; + m_MiscIcons.Create(IDB_IMAGELIST, 16, 16, IMGLIST_NUMIMAGES, 1, dc, scaling, false, miscIconsInvert); + m_MiscIconsDisabled.Create(IDB_IMAGELIST, 16, 16, IMGLIST_NUMIMAGES, 1, dc, scaling, true, miscIconsInvert); + m_PatternIcons.Create(IDB_PATTERNS, 16, 16, PATTERNIMG_NUMIMAGES, 1, dc, scaling, false, patternIconsInvert); + m_PatternIconsDisabled.Create(IDB_PATTERNS, 16, 16, PATTERNIMG_NUMIMAGES, 1, dc, scaling, true, patternIconsInvert); + m_EnvelopeIcons.Create(IDB_ENVTOOLBAR, 20, 18, ENVIMG_NUMIMAGES, 1, dc, scaling, false, envelopeIconsInvert); + m_SampleIcons.Create(IDB_SMPTOOLBAR, 20, 18, SAMPLEIMG_NUMIMAGES, 1, dc, scaling, false); + ReleaseDC(dc); + + NONCLIENTMETRICS metrics; + metrics.cbSize = sizeof(metrics); + SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0); + m_hGUIFont = CreateFontIndirect(&metrics.lfMessageFont); + + penDarkGray = ::CreatePen(PS_SOLID, 0, GetSysColor(COLOR_BTNSHADOW)); + penGray99 = ::CreatePen(PS_SOLID,0, RGB(0x99, 0x99, 0x99)); + penHalfDarkGray = ::CreatePen(PS_DOT, 0, GetSysColor(COLOR_BTNSHADOW)); + + // Cursors + curDragging = theApp.LoadCursor(IDC_DRAGGING); + curArrow = theApp.LoadStandardCursor(IDC_ARROW); + curNoDrop = theApp.LoadStandardCursor(IDC_NO); + curNoDrop2 = theApp.LoadCursor(IDC_NODRAG); + curVSplit = theApp.LoadCursor(AFX_IDC_HSPLITBAR); + // bitmaps + bmpNotes = LoadDib(MAKEINTRESOURCE(IDB_PATTERNVIEW)); + bmpVUMeters = LoadDib(MAKEINTRESOURCE(IDB_VUMETERS)); + bmpPluginVUMeters = LoadDib(MAKEINTRESOURCE(IDB_VUMETERS)); + // Toolbars + EnableDocking(CBRS_ALIGN_ANY); + if (!m_wndToolBar.Create(this)) return -1; + if (!m_wndStatusBar.Create(this)) return -1; + if (!m_wndTree.Create(this, IDD_TREEVIEW, CBRS_LEFT|CBRS_BORDER_RIGHT, IDD_TREEVIEW)) return -1; + m_wndStatusBar.SetIndicators(indicators, mpt::saturate_cast<int>(std::size(indicators))); + m_wndStatusBar.SetPaneInfo(0, ID_SEPARATOR, SBPS_STRETCH, 256); + m_wndToolBar.Init(this); + m_wndTree.RecalcLayout(); + + RemoveControlBar(&m_wndStatusBar); //Removing statusbar because corrupted statusbar inifiledata can crash the program. + m_wndTree.EnableDocking(0); //To prevent a crash when there's "Docking=1" for treebar in ini-settings. + // Restore toobar positions + LoadBarState(_T("Toolbars")); + + AddControlBar(&m_wndStatusBar); //Restore statusbar to mainframe. + + UpdateColors(); + + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_ENABLE_RECORD_DEFAULT) midiOpenDevice(false); + +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable:6387) // '_Param_(2)' could be '0': this does not adhere to the specification for the function 'HtmlHelpW' +#endif + ::HtmlHelp(m_hWnd, nullptr, HH_INITIALIZE, reinterpret_cast<DWORD_PTR>(&helpCookie)); +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif + + return 0; +} + + +BOOL CMainFrame::PreCreateWindow(CREATESTRUCT& cs) +{ + return CMDIFrameWnd::PreCreateWindow(cs); +} + + +BOOL CMainFrame::DestroyWindow() +{ +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable:6387) // '_Param_(2)' could be '0': this does not adhere to the specification for the function 'HtmlHelpW' +#endif + ::HtmlHelp(m_hWnd, nullptr, HH_UNINITIALIZE, reinterpret_cast<DWORD_PTR>(&helpCookie)); +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif + + // Uninstall Keyboard Hook + if (ghKbdHook) + { + UnhookWindowsHookEx(ghKbdHook); + ghKbdHook = NULL; + } + // Kill Timer + if (m_nTimer) + { + KillTimer(m_nTimer); + m_nTimer = 0; + } + if (shMidiIn) midiCloseDevice(); + // Delete bitmaps + delete bmpNotes; + bmpNotes = nullptr; + + delete bmpVUMeters; + bmpVUMeters = nullptr; + + delete bmpPluginVUMeters; + bmpPluginVUMeters = nullptr; + + PatternFont::DeleteFontData(); + + // Kill GDI Objects +#define DeleteGDIObject(h) ::DeleteObject(h); h = NULL; + DeleteGDIObject(penDarkGray); + DeleteGDIObject(m_hGUIFont); + DeleteGDIObject(m_hFixedFont); + DeleteGDIObject(penGray99); +#undef DeleteGDIObject + + return CMDIFrameWnd::DestroyWindow(); +} + + +void CMainFrame::OnClose() +{ + MPT_TRACE_SCOPE(); + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOCLOSEDIALOG)) + { + // Show modified documents window + CloseMainDialog dlg; + if(dlg.DoModal() != IDOK) + { + return; + } + } + + CChildFrame *pMDIActive = (CChildFrame *)MDIGetActive(); + + BeginWaitCursor(); + if (IsPlaying()) PauseMod(); + if (pMDIActive) pMDIActive->SavePosition(TRUE); + + if(gpSoundDevice) + { + gpSoundDevice->Stop(); + gpSoundDevice->Close(); + delete gpSoundDevice; + gpSoundDevice = nullptr; + } + + // Save Settings + RemoveControlBar(&m_wndStatusBar); // Remove statusbar so that its state won't get saved. + SaveBarState(_T("Toolbars")); + AddControlBar(&m_wndStatusBar); // Restore statusbar to mainframe. + TrackerSettings::Instance().SaveSettings(); + + if(m_InputHandler && m_InputHandler->m_activeCommandSet) + { + m_InputHandler->m_activeCommandSet->SaveFile(TrackerSettings::Instance().m_szKbdFile); + } + + EndWaitCursor(); + CMDIFrameWnd::OnClose(); +} + + +BOOL CMainFrame::OnDeviceChange(UINT nEventType, DWORD_PTR dwData) +{ + if(nEventType == DBT_DEVNODES_CHANGED && shMidiIn) + { + // Calling this (or most other MIDI input related functions) makes the MIDI driver realize + // that the connection to USB MIDI devices was lost and send a MIM_CLOSE message. + // Otherwise, after disconnecting a USB MIDI device, the MIDI callback will stay alive forever but return no data. + midiInGetNumDevs(); + } + return CMDIFrameWnd::OnDeviceChange(nEventType, dwData); +} + + +// Drop files from Windows +void CMainFrame::OnDropFiles(HDROP hDropInfo) +{ + const UINT nFiles = ::DragQueryFileW(hDropInfo, (UINT)-1, NULL, 0); + CMainFrame::GetMainFrame()->SetForegroundWindow(); + for(UINT f = 0; f < nFiles; f++) + { + UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, L'\0'); + if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) + { + const mpt::PathString file = mpt::PathString::FromNative(fileName.data()); +#ifdef MPT_BUILD_DEBUG + // Debug Hack: Quickly scan a folder containing module files (without running out of window handles ;) + if(m_InputHandler->CtrlPressed() && m_InputHandler->AltPressed() && m_InputHandler->ShiftPressed() && file.IsDirectory()) + { + FolderScanner scanner(file, FolderScanner::kOnlyFiles | FolderScanner::kFindInSubDirectories); + mpt::PathString scanName; + size_t failed = 0, total = 0; + while(scanner.Next(scanName)) + { + InputFile inputFile(scanName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(!inputFile.IsValid()) + continue; + auto sndFile = std::make_unique<CSoundFile>(); + MPT_LOG_GLOBAL(LogDebug, "info", U_("Loading ") + scanName.ToUnicode()); + if(!sndFile->Create(GetFileReader(inputFile), CSoundFile::loadCompleteModule, nullptr)) + { + MPT_LOG_GLOBAL(LogDebug, "info", U_("FAILED: ") + scanName.ToUnicode()); + failed++; + } + total++; + } + Reporting::Information(MPT_UFORMAT("Scanned {} files, {} failed")(total, failed)); + break; + } +#endif + theApp.OpenDocumentFile(file.ToCString()); + } + } + ::DragFinish(hDropInfo); +} + + +void CMainFrame::OnActivateApp(BOOL active, DWORD /*threadID*/) +{ + // Ensure modifiers are reset when we leave the window (e.g. Alt-Tab) + if(!active) + m_InputHandler->SetModifierMask(ModNone); +} + + +LRESULT CALLBACK CMainFrame::KeyboardProc(int code, WPARAM wParam, LPARAM lParam) +{ + + static bool s_KeyboardHookReentryFlag = false; // work-around for https://bugs.openmpt.org/view.php?id=713 + + if(mpt::OS::Windows::IsWine()) // work-around for https://bugs.openmpt.org/view.php?id=713 + { + // TODO: Properly fix this in same way or another. + if(code < 0) + { + return CallNextHookEx(ghKbdHook, code, wParam, lParam); // required by spec + } + if(theApp.InGuiThread()) + { + if(s_KeyboardHookReentryFlag) + { + // Exit early without calling our hook when re-entering. + // We still need to call other hooks though. + return CallNextHookEx(ghKbdHook, code, wParam, lParam); // required by spec + } + s_KeyboardHookReentryFlag = true; + } + } + + bool skipFurtherProcessing = false; + + if(code >= 0) + { + // Check if textbox has focus + const bool handledByTextBox = m_InputHandler->IsKeyPressHandledByTextBox(static_cast<DWORD>(wParam), ::GetFocus()); + if(!handledByTextBox && m_InputHandler->GeneralKeyEvent(kCtxAllContexts, code, wParam, lParam) != kcNull) + { + if(wParam != VK_ESCAPE) + { + // We've handled the keypress. No need to take it further. + // Unless it was esc, in which case we need it to close Windows + // (there might be other special cases, we'll see.. ) + skipFurtherProcessing = true; + } + } + } + + LRESULT result = 0; + if(skipFurtherProcessing) + { + result = -1; + } else + { + result = CallNextHookEx(ghKbdHook, code, wParam, lParam); + } + + if(mpt::OS::Windows::IsWine()) // work-around for https://bugs.openmpt.org/view.php?id=713 + { + if(theApp.InGuiThread()) + { + s_KeyboardHookReentryFlag = false; + } + } + + return result; +} + + +BOOL CMainFrame::PreTranslateMessage(MSG* pMsg) +{ + if((pMsg->message == WM_RBUTTONDOWN) || (pMsg->message == WM_NCRBUTTONDOWN)) + { + CWnd* pWnd = CWnd::FromHandlePermanent(pMsg->hwnd); + CControlBar* pBar = NULL; + HWND hwnd = (pWnd) ? pWnd->m_hWnd : NULL; + + if ((hwnd) && (pMsg->message == WM_RBUTTONDOWN)) pBar = DYNAMIC_DOWNCAST(CControlBar, pWnd); + if ((pBar != NULL) || ((pMsg->message == WM_NCRBUTTONDOWN) && (pMsg->wParam == HTMENU))) + { + CMenu Menu; + CPoint pt; + + GetCursorPos(&pt); + if (Menu.LoadMenu(IDR_TOOLBARS)) + { + CMenu* pSubMenu = Menu.GetSubMenu(0); + if (pSubMenu!=NULL) pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON,pt.x,pt.y,this); + } + } + } + return CMDIFrameWnd::PreTranslateMessage(pMsg); +} + + +void CMainFrame::OnUpdateFrameTitle(BOOL bAddToTitle) +{ + if ((GetStyle() & FWS_ADDTOTITLE) == 0) return; // leave it alone! + + CMDIChildWnd* pActiveChild = nullptr; + CDocument* pDocument = GetActiveDocument(); + if (bAddToTitle && + (pActiveChild = MDIGetActive()) != nullptr && + (pActiveChild->GetStyle() & WS_MAXIMIZE) == 0 && + (pDocument != nullptr || + (pDocument = pActiveChild->GetActiveDocument()) != nullptr)) + { + CString title = pDocument->GetTitle(); + if(pDocument->IsModified()) + title.AppendChar(_T('*')); + UpdateFrameTitleForDocument(title); + } else + { + LPCTSTR lpstrTitle = nullptr; + CString strTitle; + + if (pActiveChild != nullptr) + { + strTitle = pActiveChild->GetTitle(); + if (!strTitle.IsEmpty()) + lpstrTitle = strTitle; + } + UpdateFrameTitleForDocument(lpstrTitle); + } +} + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame Sound Library + + +void CMainFrame::OnTimerNotify() +{ + MPT_TRACE_SCOPE(); + ASSERT(InGuiThread()); + ASSERT(!InNotifyHandler()); + m_InNotifyHandler = true; + Notification PendingNotification; + bool found = false; + int64 currenttotalsamples = 0; + if(gpSoundDevice) + { + currenttotalsamples = gpSoundDevice->GetStreamPosition().Frames; + } + { + // advance to the newest notification, drop the obsolete ones + mpt::lock_guard<mpt::mutex> lock(m_NotificationBufferMutex); + const Notification * pnotify = nullptr; + const Notification * p = m_NotifyBuffer.peek_p(); + if(p && currenttotalsamples >= p->timestampSamples) + { + pnotify = p; + while(m_NotifyBuffer.peek_next_p() && currenttotalsamples >= m_NotifyBuffer.peek_next_p()->timestampSamples) + { + m_NotifyBuffer.pop(); + p = m_NotifyBuffer.peek_p(); + pnotify = p; + } + } + if(pnotify) + { + PendingNotification = *pnotify; // copy notification so that we can free the buffer + found = true; + { + m_NotifyBuffer.pop(); + } + } + } + if(found) + { + OnUpdatePosition(0, (LPARAM)&PendingNotification); + } + m_InNotifyHandler = false; + ASSERT(!InNotifyHandler()); +} + + +void CMainFrame::SoundDeviceMessage(LogLevel level, const mpt::ustring &str) +{ + MPT_TRACE(); + Reporting::Message(level, str); +} + + +void CMainFrame::SoundCallbackPreStart() +{ + MPT_TRACE(); + m_SoundDeviceClock.SetResolution(1); +} + + +void CMainFrame::SoundCallbackPostStop() +{ + MPT_TRACE(); + m_SoundDeviceClock.SetResolution(0); +} + + +uint64 CMainFrame::SoundCallbackGetReferenceClockNowNanoseconds() const +{ + MPT_TRACE(); + MPT_ASSERT(!InAudioThread()); + return m_SoundDeviceClock.NowNanoseconds(); +} + + +uint64 CMainFrame::SoundCallbackLockedGetReferenceClockNowNanoseconds() const +{ + MPT_TRACE(); + MPT_ASSERT(InAudioThread()); + return m_SoundDeviceClock.NowNanoseconds(); +} + + +bool CMainFrame::SoundCallbackIsLockedByCurrentThread() const +{ + MPT_TRACE(); + return theApp.GetGlobalMutexRef().IsLockedByCurrentThread(); +} + + +void CMainFrame::SoundCallbackLock() +{ + MPT_TRACE_SCOPE(); + m_SoundDeviceFillBufferCriticalSection.Enter(); + MPT_ASSERT_ALWAYS(m_pSndFile != nullptr); + m_AudioThreadId = GetCurrentThreadId(); + mpt::log::Trace::SetThreadId(mpt::log::Trace::ThreadKindAudio, m_AudioThreadId); +} + + +void CMainFrame::SoundCallbackUnlock() +{ + MPT_TRACE_SCOPE(); + MPT_ASSERT_ALWAYS(m_pSndFile != nullptr); + m_AudioThreadId = 0; + m_SoundDeviceFillBufferCriticalSection.Leave(); +} + + +class BufferInputWrapper + : public IAudioSource +{ +private: + SoundDevice::CallbackBuffer<DithersOpenMPT> &callbackBuffer; +public: + inline BufferInputWrapper(SoundDevice::CallbackBuffer<DithersOpenMPT> &callbackBuffer_) + : callbackBuffer(callbackBuffer_) + { + return; + } + inline void Process(mpt::audio_span_planar<MixSampleInt> buffer) override + { + callbackBuffer.template ReadFixedPoint<MixSampleIntTraits::mix_fractional_bits>(buffer); + } + inline void Process(mpt::audio_span_planar<MixSampleFloat> buffer) override + { + callbackBuffer.Read(buffer); + } +}; + + +class BufferOutputWrapper + : public IAudioTarget +{ +private: + SoundDevice::CallbackBuffer<DithersOpenMPT> &callbackBuffer; +public: + inline BufferOutputWrapper(SoundDevice::CallbackBuffer<DithersOpenMPT> &callbackBuffer_) + : callbackBuffer(callbackBuffer_) + { + return; + } + inline void Process(mpt::audio_span_interleaved<MixSampleInt> buffer) override + { + callbackBuffer.template WriteFixedPoint<MixSampleIntTraits::mix_fractional_bits>(buffer); + } + inline void Process(mpt::audio_span_interleaved<MixSampleFloat> buffer) override + { + callbackBuffer.Write(buffer); + } +}; + + +void CMainFrame::SoundCallbackLockedProcessPrepare(SoundDevice::TimeInfo timeInfo) +{ + MPT_TRACE_SCOPE(); + MPT_ASSERT(InAudioThread()); + TimingInfo timingInfo; + timingInfo.OutputLatency = timeInfo.Latency; + timingInfo.StreamFrames = timeInfo.SyncPointStreamFrames; + timingInfo.SystemTimestamp = timeInfo.SyncPointSystemTimestamp; + timingInfo.Speed = timeInfo.Speed; + m_pSndFile->m_TimingInfo = timingInfo; +} + + +void CMainFrame::SoundCallbackLockedCallback(SoundDevice::CallbackBuffer<DithersOpenMPT> &buffer) +{ + MPT_TRACE_SCOPE(); + MPT_ASSERT(InAudioThread()); + OPENMPT_PROFILE_FUNCTION(Profiler::Audio); + BufferInputWrapper source(buffer); + BufferOutputWrapper target(buffer); + MPT_ASSERT(buffer.GetNumFrames() <= std::numeric_limits<CSoundFile::samplecount_t>::max()); + CSoundFile::samplecount_t framesToRender = static_cast<CSoundFile::samplecount_t>(buffer.GetNumFrames()); + MPT_ASSERT(framesToRender > 0); + CSoundFile::samplecount_t renderedFrames = m_pSndFile->Read(framesToRender, target, source, std::ref(m_VUMeterOutput), std::ref(m_VUMeterInput)); + MPT_ASSERT(renderedFrames <= framesToRender); + [[maybe_unused]] CSoundFile::samplecount_t remainingFrames = framesToRender - renderedFrames; + MPT_ASSERT(remainingFrames >= 0); // remaining buffer is filled with silence automatically +} + + +void CMainFrame::SoundCallbackLockedProcessDone(SoundDevice::TimeInfo timeInfo) +{ + MPT_TRACE_SCOPE(); + MPT_ASSERT(InAudioThread()); + OPENMPT_PROFILE_FUNCTION(Profiler::Notify); + MPT_ASSERT((timeInfo.RenderStreamPositionAfter.Frames - timeInfo.RenderStreamPositionBefore.Frames) < std::numeric_limits<CSoundFile::samplecount_t>::max()); + CSoundFile::samplecount_t framesRendered = static_cast<CSoundFile::samplecount_t>(timeInfo.RenderStreamPositionAfter.Frames - timeInfo.RenderStreamPositionBefore.Frames); + int64 streamPosition = timeInfo.RenderStreamPositionAfter.Frames; + DoNotification(framesRendered, streamPosition); + //m_pSndFile->m_TimingInfo = TimingInfo(); // reset +} + + +bool CMainFrame::IsAudioDeviceOpen() const +{ + MPT_TRACE_SCOPE(); + return gpSoundDevice && gpSoundDevice->IsOpen(); +} + + +bool CMainFrame::audioOpenDevice() +{ + MPT_TRACE_SCOPE(); + const SoundDevice::Identifier deviceIdentifier = TrackerSettings::Instance().GetSoundDeviceIdentifier(); + if(!TrackerSettings::Instance().GetMixerSettings().IsValid()) + { + Reporting::Error(MPT_UFORMAT("Unable to open sound device '{}': Invalid mixer settings.")(deviceIdentifier)); + return false; + } + if(gpSoundDevice && (gpSoundDevice->GetDeviceInfo().GetIdentifier() != deviceIdentifier)) + { + gpSoundDevice->Stop(); + gpSoundDevice->Close(); + delete gpSoundDevice; + gpSoundDevice = nullptr; + } + if(IsAudioDeviceOpen()) + { + return true; + } + if(!gpSoundDevice) + { + gpSoundDevice = theApp.GetSoundDevicesManager()->CreateSoundDevice(deviceIdentifier); + } + if(!gpSoundDevice) + { + Reporting::Error(MPT_UFORMAT("Unable to open sound device '{}': Could not find sound device.")(deviceIdentifier)); + return false; + } + gpSoundDevice->SetMessageReceiver(this); + gpSoundDevice->SetCallback(this); + SoundDevice::Settings deviceSettings = TrackerSettings::Instance().GetSoundDeviceSettings(deviceIdentifier); + if(!gpSoundDevice->Open(deviceSettings)) + { + if(!gpSoundDevice->IsAvailable()) + { + Reporting::Error(MPT_UFORMAT("Unable to open sound device '{}': Device not available.")(gpSoundDevice->GetDeviceInfo().GetDisplayName())); + } else + { + Reporting::Error(MPT_UFORMAT("Unable to open sound device '{}'.")(gpSoundDevice->GetDeviceInfo().GetDisplayName())); + } + return false; + } + SampleFormat actualSampleFormat = gpSoundDevice->GetActualSampleFormat(); + if(!actualSampleFormat.IsValid()) + { + Reporting::Error(MPT_UFORMAT("Unable to open sound device '{}': Unknown sample format.")(gpSoundDevice->GetDeviceInfo().GetDisplayName())); + return false; + } + deviceSettings.sampleFormat = actualSampleFormat; + Dithers().SetMode(deviceSettings.DitherType, deviceSettings.Channels); + TrackerSettings::Instance().MixerSamplerate = gpSoundDevice->GetSettings().Samplerate; + TrackerSettings::Instance().SetSoundDeviceSettings(deviceIdentifier, deviceSettings); + return true; +} + + +void CMainFrame::audioCloseDevice() +{ + MPT_TRACE_SCOPE(); + if(gpSoundDevice) + { + gpSoundDevice->Close(); + } + if(m_NotifyTimer) + { + KillTimer(m_NotifyTimer); + m_NotifyTimer = 0; + } + ResetNotificationBuffer(); +} + + +void VUMeter::Process(Channel &c, MixSampleInt sample) +{ + c.peak = std::max(c.peak, std::abs(sample)); + if(sample < MixSampleIntTraits::mix_clip_min || MixSampleIntTraits::mix_clip_max < sample) + { + c.clipped = true; + } +} + + +void VUMeter::Process(Channel &c, MixSampleFloat sample) +{ + Process(c, SC::ConvertToFixedPoint<MixSampleInt, MixSampleFloat, MixSampleIntTraits::mix_fractional_bits>()(sample)); +} + + +void VUMeter::Process(mpt::audio_span_interleaved<const MixSampleInt> buffer) +{ + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < std::min(buffer.size_channels(), maxChannels); ++channel) + { + Process(channels[channel], buffer(channel, frame)); + } + } + for(std::size_t channel = std::min(buffer.size_channels(), maxChannels); channel < maxChannels; ++channel) + { + channels[channel] = Channel(); + } +} + + +void VUMeter::Process(mpt::audio_span_interleaved<const MixSampleFloat> buffer) +{ + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < std::min(buffer.size_channels(), maxChannels); ++channel) + { + Process(channels[channel], buffer(channel, frame)); + } + } + for(std::size_t channel = std::min(buffer.size_channels(), maxChannels); channel < maxChannels; ++channel) + { + channels[channel] = Channel(); + } +} + + +void VUMeter::Process(mpt::audio_span_planar<const MixSampleInt> buffer) +{ + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < std::min(buffer.size_channels(), maxChannels); ++channel) + { + Process(channels[channel], buffer(channel, frame)); + } + } + for(std::size_t channel = std::min(buffer.size_channels(), maxChannels); channel < maxChannels; ++channel) + { + channels[channel] = Channel(); + } +} + + +void VUMeter::Process(mpt::audio_span_planar<const MixSampleFloat> buffer) +{ + for(std::size_t frame = 0; frame < buffer.size_frames(); ++frame) + { + for(std::size_t channel = 0; channel < std::min(buffer.size_channels(), maxChannels); ++channel) + { + Process(channels[channel], buffer(channel, frame)); + } + } + for(std::size_t channel = std::min(buffer.size_channels(), maxChannels); channel < maxChannels; ++channel) + { + channels[channel] = Channel(); + } +} + + +const float VUMeter::dynamicRange = 48.0f; // corresponds to the current implementation of the UI widget displaying the result + + +void VUMeter::SetDecaySpeedDecibelPerSecond(float decibelPerSecond) +{ + float linearDecayRate = decibelPerSecond / dynamicRange; + decayParam = mpt::saturate_round<int32>(linearDecayRate * static_cast<float>(MixSampleIntTraits::mix_clip_max)); +} + + +void VUMeter::Decay(int32 secondsNum, int32 secondsDen) +{ + int32 decay = Util::muldivr(decayParam, secondsNum, secondsDen); + for(std::size_t channel = 0; channel < maxChannels; ++channel) + { + channels[channel].peak = std::max(channels[channel].peak - decay, 0); + } +} + + +void VUMeter::ResetClipped() +{ + for(std::size_t channel = 0; channel < maxChannels; ++channel) + { + channels[channel].clipped = false; + } +} + + +static void SetVUMeter(std::array<uint32, 4> &masterVU, const VUMeter &vumeter) +{ + for(std::size_t channel = 0; channel < VUMeter::maxChannels; ++channel) + { + masterVU[channel] = Clamp(vumeter[channel].peak >> 11, 0, 0x10000); + if(vumeter[channel].clipped) + { + masterVU[channel] |= Notification::ClipVU; + } + } +} + + +bool CMainFrame::DoNotification(DWORD dwSamplesRead, int64 streamPosition) +{ + MPT_TRACE_SCOPE(); + ASSERT(InAudioThread()); + if(!m_pSndFile) return false; + + FlagSet<Notification::Type> notifyType(Notification::Default); + Notification::Item notifyItem = 0; + + if(m_pSndFile->m_pModDoc) + { + notifyType = m_pSndFile->m_pModDoc->GetNotificationType(); + notifyItem = m_pSndFile->m_pModDoc->GetNotificationItem(); + } + + // Add an entry to the notification history + + Notification notification(notifyType, notifyItem, streamPosition, m_pSndFile->m_PlayState.m_nRow, m_pSndFile->m_PlayState.m_nTickCount, m_pSndFile->m_PlayState.TicksOnRow(), m_pSndFile->m_PlayState.m_nCurrentOrder, m_pSndFile->m_PlayState.m_nPattern, m_pSndFile->GetMixStat(), static_cast<uint8>(m_pSndFile->m_MixerSettings.gnChannels), static_cast<uint8>(m_pSndFile->m_MixerSettings.NumInputChannels)); + + m_pSndFile->ResetMixStat(); + + if(m_pSndFile->m_SongFlags[SONG_ENDREACHED]) notification.type.set(Notification::EOS); + + if(notifyType[Notification::Sample]) + { + // Sample positions + const SAMPLEINDEX smp = notifyItem; + if(smp > 0 && smp <= m_pSndFile->GetNumSamples() && m_pSndFile->GetSample(smp).HasSampleData()) + { + for(CHANNELINDEX k = 0; k < MAX_CHANNELS; k++) + { + const ModChannel &chn = m_pSndFile->m_PlayState.Chn[k]; + if(chn.pModSample == &m_pSndFile->GetSample(smp) && chn.nLength != 0 // Corrent sample is set up on this channel + && (!chn.dwFlags[CHN_NOTEFADE] || chn.nFadeOutVol)) // And it hasn't completely faded out yet, so it's still playing + { + notification.pos[k] = chn.position.GetInt(); + } else + { + notification.pos[k] = Notification::PosInvalid; + } + } + } else + { + // Can't generate a valid notification. + notification.type.reset(Notification::Sample); + } + } else if(notifyType[Notification::VolEnv | Notification::PanEnv | Notification::PitchEnv]) + { + // Instrument envelopes + const INSTRUMENTINDEX ins = notifyItem; + + EnvelopeType notifyEnv = ENV_VOLUME; + if(notifyType[Notification::PitchEnv]) + notifyEnv = ENV_PITCH; + else if(notifyType[Notification::PanEnv]) + notifyEnv = ENV_PANNING; + + if(ins > 0 && ins <= m_pSndFile->GetNumInstruments() && m_pSndFile->Instruments[ins] != nullptr) + { + for(CHANNELINDEX k = 0; k < MAX_CHANNELS; k++) + { + const ModChannel &chn = m_pSndFile->m_PlayState.Chn[k]; + SmpLength pos = Notification::PosInvalid; + + if(chn.pModInstrument == m_pSndFile->Instruments[ins] // Correct instrument is set up on this channel + && (chn.nLength || chn.pModInstrument->HasValidMIDIChannel()) // And it's playing something (sample or instrument plugin) + && (!chn.dwFlags[CHN_NOTEFADE] || chn.nFadeOutVol)) // And it hasn't completely faded out yet, so it's still playing + { + const ModChannel::EnvInfo &chnEnv = chn.GetEnvelope(notifyEnv); + if(chnEnv.flags[ENV_ENABLED]) + { + pos = chnEnv.nEnvPosition; + if(m_pSndFile->m_playBehaviour[kITEnvelopePositionHandling]) + { + // Impulse Tracker envelope handling (see e.g. CSoundFile::IncrementEnvelopePosition in SndMix.cpp for details) + if(pos > 0) + pos--; + else + pos = Notification::PosInvalid; // Envelope isn't playing yet (e.g. when disabling it right when triggering a note) + } + } + } + notification.pos[k] = pos; + } + } else + { + // Can't generate a valid notification. + notification.type.reset(Notification::VolEnv | Notification::PanEnv | Notification::PitchEnv); + } + } else if(notifyType[Notification::VUMeters]) + { + // Pattern channel VU meters + for(CHANNELINDEX k = 0; k < m_pSndFile->GetNumChannels(); k++) + { + uint32 vul = m_pSndFile->m_PlayState.Chn[k].nLeftVU; + uint32 vur = m_pSndFile->m_PlayState.Chn[k].nRightVU; + notification.pos[k] = (vul << 8) | (vur); + } + } + + { + // Master VU meter + SetVUMeter(notification.masterVUin, m_VUMeterInput); + SetVUMeter(notification.masterVUout, m_VUMeterOutput); + m_VUMeterInput.Decay(dwSamplesRead, m_pSndFile->m_MixerSettings.gdwMixingFreq); + m_VUMeterOutput.Decay(dwSamplesRead, m_pSndFile->m_MixerSettings.gdwMixingFreq); + + } + + { + mpt::lock_guard<mpt::mutex> lock(m_NotificationBufferMutex); + if(m_NotifyBuffer.write_size() == 0) + { + ASSERT(0); + return FALSE; // drop notification + } + m_NotifyBuffer.push(notification); + } + + return true; +} + + +void CMainFrame::UpdateDspEffects(CSoundFile &sndFile, bool reset) +{ + CriticalSection cs; +#ifndef NO_REVERB + sndFile.m_Reverb.m_Settings = TrackerSettings::Instance().m_ReverbSettings; +#endif +#ifndef NO_DSP + sndFile.m_Surround.m_Settings = TrackerSettings::Instance().m_SurroundSettings; +#endif +#ifndef NO_DSP + sndFile.m_MegaBass.m_Settings = TrackerSettings::Instance().m_MegaBassSettings; +#endif +#ifndef NO_EQ + sndFile.SetEQGains(TrackerSettings::Instance().m_EqSettings.Gains, TrackerSettings::Instance().m_EqSettings.Freqs, reset); +#endif +#ifndef NO_DSP + sndFile.m_BitCrush.m_Settings = TrackerSettings::Instance().m_BitCrushSettings; +#endif + sndFile.SetDspEffects(TrackerSettings::Instance().MixerDSPMask); + sndFile.InitPlayer(reset); +} + + +void CMainFrame::UpdateAudioParameters(CSoundFile &sndFile, bool reset) +{ + CriticalSection cs; + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_MUTECHNMODE) + TrackerSettings::Instance().MixerFlags |= SNDMIX_MUTECHNMODE; + else + TrackerSettings::Instance().MixerFlags &= ~SNDMIX_MUTECHNMODE; + sndFile.SetMixerSettings(TrackerSettings::Instance().GetMixerSettings()); + sndFile.SetResamplerSettings(TrackerSettings::Instance().GetResamplerSettings()); + UpdateDspEffects(sndFile, false); // reset done in next line + sndFile.InitPlayer(reset); +} + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame diagnostics + +#ifdef _DEBUG +void CMainFrame::AssertValid() const +{ + CMDIFrameWnd::AssertValid(); +} + +void CMainFrame::Dump(CDumpContext& dc) const +{ + CMDIFrameWnd::Dump(dc); +} + +#endif //_DEBUG + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame static helpers + + +void CMainFrame::UpdateColors() +{ + const auto &colors = TrackerSettings::Instance().rgbCustomColors; + const struct { MODPLUGDIB *bitmap; uint32 lo, med, hi; } meters[] = + { + { bmpVUMeters, MODCOLOR_VUMETER_LO, MODCOLOR_VUMETER_MED, MODCOLOR_VUMETER_HI }, + { bmpPluginVUMeters, MODCOLOR_VUMETER_LO_VST, MODCOLOR_VUMETER_MED_VST, MODCOLOR_VUMETER_HI_VST }, + }; + for(auto &meter : meters) if(meter.bitmap != nullptr) + { + meter.bitmap->bmiColors[7] = rgb2quad(GetSysColor(COLOR_BTNFACE)); + meter.bitmap->bmiColors[8] = rgb2quad(GetSysColor(COLOR_BTNSHADOW)); + meter.bitmap->bmiColors[15] = rgb2quad(GetSysColor(COLOR_BTNHIGHLIGHT)); + meter.bitmap->bmiColors[10] = rgb2quad(colors[meter.lo]); + meter.bitmap->bmiColors[11] = rgb2quad(colors[meter.med]); + meter.bitmap->bmiColors[9] = rgb2quad(colors[meter.hi]); + meter.bitmap->bmiColors[2] = rgb2quad((colors[meter.lo] >> 1) & 0x7F7F7F); + meter.bitmap->bmiColors[3] = rgb2quad((colors[meter.med] >> 1) & 0x7F7F7F); + meter.bitmap->bmiColors[1] = rgb2quad((colors[meter.hi] >> 1) & 0x7F7F7F); + } + + // Generel tab VU meters + for(UINT i = 0; i < NUM_VUMETER_PENS * 2; i++) + { + int r0,g0,b0, r1,g1,b1; + int r, g, b; + int y; + + y = (i >= NUM_VUMETER_PENS) ? (i-NUM_VUMETER_PENS) : i; + if (y < (NUM_VUMETER_PENS/2)) + { + r0 = GetRValue(colors[MODCOLOR_VUMETER_LO]); + g0 = GetGValue(colors[MODCOLOR_VUMETER_LO]); + b0 = GetBValue(colors[MODCOLOR_VUMETER_LO]); + r1 = GetRValue(colors[MODCOLOR_VUMETER_MED]); + g1 = GetGValue(colors[MODCOLOR_VUMETER_MED]); + b1 = GetBValue(colors[MODCOLOR_VUMETER_MED]); + } else + { + y -= (NUM_VUMETER_PENS/2); + r0 = GetRValue(colors[MODCOLOR_VUMETER_MED]); + g0 = GetGValue(colors[MODCOLOR_VUMETER_MED]); + b0 = GetBValue(colors[MODCOLOR_VUMETER_MED]); + r1 = GetRValue(colors[MODCOLOR_VUMETER_HI]); + g1 = GetGValue(colors[MODCOLOR_VUMETER_HI]); + b1 = GetBValue(colors[MODCOLOR_VUMETER_HI]); + } + r = r0 + ((r1 - r0) * y) / (NUM_VUMETER_PENS/2); + g = g0 + ((g1 - g0) * y) / (NUM_VUMETER_PENS/2); + b = b0 + ((b1 - b0) * y) / (NUM_VUMETER_PENS/2); + if (i >= NUM_VUMETER_PENS) + { + r = (r*2)/5; + g = (g*2)/5; + b = (b*2)/5; + } + gcolrefVuMeter[i] = RGB(r, g, b); + } + CMainFrame *mainFrm = GetMainFrame(); + if(mainFrm != nullptr) + { + mainFrm->m_wndToolBar.m_VuMeter.Invalidate(); + } +} + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame operations + + +UINT CMainFrame::GetBaseOctave() const +{ + return m_wndToolBar.GetBaseOctave(); +} + + +void CMainFrame::ResetNotificationBuffer() +{ + MPT_TRACE(); + mpt::lock_guard<mpt::mutex> lock(m_NotificationBufferMutex); + m_NotifyBuffer.clear(); +} + + +bool CMainFrame::PreparePlayback() +{ + MPT_TRACE_SCOPE(); + // open the audio device to update needed TrackerSettings mixer parameters + if(!audioOpenDevice()) return false; + return true; +} + + +bool CMainFrame::StartPlayback() +{ + MPT_TRACE_SCOPE(); + if(!m_pSndFile) return false; // nothing to play + if(!IsAudioDeviceOpen()) return false; + Dithers().Reset(); + if(!gpSoundDevice->Start()) return false; + if(!m_NotifyTimer) + { + if(TrackerSettings::Instance().GUIUpdateInterval.Get() > 0) + { + m_NotifyTimer = SetTimer(TIMERID_NOTIFY, TrackerSettings::Instance().GUIUpdateInterval, NULL); + } else + { + m_NotifyTimer = SetTimer(TIMERID_NOTIFY, std::max(int(1), mpt::saturate_round<int>(gpSoundDevice->GetEffectiveBufferAttributes().UpdateInterval * 1000.0)), NULL); + } + } + return true; +} + + +void CMainFrame::StopPlayback() +{ + MPT_TRACE_SCOPE(); + if(!IsAudioDeviceOpen()) return; + gpSoundDevice->Stop(); + if(m_NotifyTimer) + { + KillTimer(m_NotifyTimer); + m_NotifyTimer = 0; + } + ResetNotificationBuffer(); + if(!gpSoundDevice->GetDeviceCaps().CanKeepDeviceRunning || TrackerSettings::Instance().m_SoundSettingsStopMode == SoundDeviceStopModeClosed) + { + audioCloseDevice(); + } +} + + +bool CMainFrame::RestartPlayback() +{ + MPT_TRACE_SCOPE(); + if(!m_pSndFile) return false; // nothing to play + if(!IsAudioDeviceOpen()) return false; + if(!gpSoundDevice->IsPlaying()) return false; + gpSoundDevice->StopAndAvoidPlayingSilence(); + if(m_NotifyTimer) + { + KillTimer(m_NotifyTimer); + m_NotifyTimer = 0; + } + ResetNotificationBuffer(); + return StartPlayback(); +} + + +bool CMainFrame::PausePlayback() +{ + MPT_TRACE_SCOPE(); + if(!IsAudioDeviceOpen()) return false; + gpSoundDevice->Stop(); + if(m_NotifyTimer) + { + KillTimer(m_NotifyTimer); + m_NotifyTimer = 0; + } + ResetNotificationBuffer(); + return true; +} + + +void CMainFrame::GenerateStopNotification() +{ + Notification mn(Notification::Stop); + SendMessage(WM_MOD_UPDATEPOSITION, 0, (LPARAM)&mn); +} + + +void CMainFrame::UnsetPlaybackSoundFile() +{ + MPT_ASSERT_ALWAYS(!gpSoundDevice || !gpSoundDevice->IsPlaying()); + if(m_pSndFile) + { + m_pSndFile->SuspendPlugins(); + if(m_pSndFile->GetpModDoc()) + { + m_wndTree.UpdatePlayPos(m_pSndFile->GetpModDoc(), nullptr); + } + m_pSndFile->m_SongFlags.reset(SONG_PAUSED); + if(m_pSndFile == &m_WaveFile) + { + // Unload previewed instrument + m_WaveFile.Destroy(); + } else + { + // Stop sample preview channels + for(CHANNELINDEX i = m_pSndFile->m_nChannels; i < MAX_CHANNELS; i++) + { + if(m_pSndFile->m_PlayState.Chn[i].isPreviewNote) + { + m_pSndFile->m_PlayState.Chn[i].nLength = 0; + m_pSndFile->m_PlayState.Chn[i].position.Set(0); + } + } + } + } + m_pSndFile = nullptr; + m_wndToolBar.SetCurrentSong(nullptr); + ResetNotificationBuffer(); +} + + +void CMainFrame::SetPlaybackSoundFile(CSoundFile *pSndFile) +{ + MPT_ASSERT_ALWAYS(pSndFile); + m_pSndFile = pSndFile; +} + + +bool CMainFrame::PlayMod(CModDoc *pModDoc) +{ + MPT_ASSERT_ALWAYS(!theApp.GetGlobalMutexRef().IsLockedByCurrentThread()); + if(!pModDoc) return false; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!IsValidSoundFile(sndFile)) return false; + + // if something is playing, pause it + PausePlayback(); + GenerateStopNotification(); + + UnsetPlaybackSoundFile(); + + // open audio device if not already open + if (!PreparePlayback()) return false; + + // set mixing parameters in CSoundFile + UpdateAudioParameters(sndFile); + + SetPlaybackSoundFile(&sndFile); + + const bool bPaused = m_pSndFile->IsPaused(); + const bool bPatLoop = m_pSndFile->m_SongFlags[SONG_PATTERNLOOP]; + + m_pSndFile->m_SongFlags.reset(SONG_FADINGSONG | SONG_ENDREACHED); + + if(!bPatLoop && bPaused) sndFile.m_SongFlags.set(SONG_PAUSED); + sndFile.SetRepeatCount((TrackerSettings::Instance().gbLoopSong) ? -1 : 0); + + sndFile.InitPlayer(true); + sndFile.ResumePlugins(); + + m_wndToolBar.SetCurrentSong(m_pSndFile); + + m_VUMeterInput = VUMeter(); + m_VUMeterOutput = VUMeter(); + + if(!StartPlayback()) + { + UnsetPlaybackSoundFile(); + return false; + } + + return true; +} + + +bool CMainFrame::PauseMod(CModDoc *pModDoc) +{ + MPT_ASSERT_ALWAYS(!theApp.GetGlobalMutexRef().IsLockedByCurrentThread()); + if(pModDoc && (pModDoc != GetModPlaying())) return false; + if(!IsPlaying()) return true; + + PausePlayback(); + GenerateStopNotification(); + + UnsetPlaybackSoundFile(); + + StopPlayback(); + + return true; +} + + +bool CMainFrame::StopMod(CModDoc *pModDoc) +{ + MPT_ASSERT_ALWAYS(!theApp.GetGlobalMutexRef().IsLockedByCurrentThread()); + if(pModDoc && (pModDoc != GetModPlaying())) return false; + if(!IsPlaying()) return true; + + PausePlayback(); + SetElapsedTime(0); + GenerateStopNotification(); + + m_pSndFile->ResetPlayPos(); + m_pSndFile->ResetChannels(); + UnsetPlaybackSoundFile(); + + StopPlayback(); + + return true; +} + + +bool CMainFrame::StopSoundFile(CSoundFile *pSndFile) +{ + MPT_ASSERT_ALWAYS(!theApp.GetGlobalMutexRef().IsLockedByCurrentThread()); + if(!IsValidSoundFile(pSndFile)) return false; + if(pSndFile != m_pSndFile) return false; + if(!IsPlaying()) return true; + + PausePlayback(); + GenerateStopNotification(); + + UnsetPlaybackSoundFile(); + + StopPlayback(); + + return true; +} + + +bool CMainFrame::PlaySoundFile(CSoundFile *pSndFile) +{ + MPT_ASSERT_ALWAYS(!theApp.GetGlobalMutexRef().IsLockedByCurrentThread()); + if(!IsValidSoundFile(pSndFile)) return false; + + PausePlayback(); + GenerateStopNotification(); + + if(m_pSndFile != pSndFile) + { + UnsetPlaybackSoundFile(); + } + + if(!PreparePlayback()) + { + UnsetPlaybackSoundFile(); + return false; + } + + UpdateAudioParameters(*pSndFile); + + SetPlaybackSoundFile(pSndFile); + + m_pSndFile->InitPlayer(true); + + if(!StartPlayback()) + { + UnsetPlaybackSoundFile(); + return false; + } + + return true; +} + + +bool CMainFrame::PlayDLSInstrument(const CDLSBank &bank, UINT instr, UINT region, ModCommand::NOTE note, int volume) +{ + bool ok = false; + BeginWaitCursor(); + { + CriticalSection cs; + InitPreview(); + if(bank.ExtractInstrument(m_WaveFile, 1, instr, region)) + { + PreparePreview(note, volume); + ok = true; + } + } + EndWaitCursor(); + if(!ok) + { + PausePlayback(); + UnsetPlaybackSoundFile(); + StopPlayback(); + return false; + } + if(IsPlaying() && (m_pSndFile == &m_WaveFile)) + { + return true; + } + return PlaySoundFile(&m_WaveFile); +} + + +bool CMainFrame::PlaySoundFile(const mpt::PathString &filename, ModCommand::NOTE note, int volume) +{ + bool ok = false; + BeginWaitCursor(); + { + CriticalSection cs; + static mpt::PathString prevFile; + // Did we already load this file for previewing? Don't load it again if the preview is still running. + ok = (prevFile == filename && m_pSndFile == &m_WaveFile); + + if(!ok && !filename.empty()) + { + InputFile f(filename, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(f.IsValid()) + { + FileReader file = GetFileReader(f); + if(file.IsValid()) + { + InitPreview(); + m_WaveFile.m_SongFlags.set(SONG_PAUSED); + // Avoid hanging audio while reading file - we have removed all sample and instrument references before, + // so it's safe to replace the sample / instrument now. + cs.Leave(); + ok = m_WaveFile.ReadInstrumentFromFile(1, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad); + cs.Enter(); + if(!ok) + { + // Try reading as sample if reading as instrument fails + ok = m_WaveFile.ReadSampleFromFile(1, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad); + m_WaveFile.AllocateInstrument(1, 1); + } + } + } + } + if(ok) + { + // Write notes to pattern. Also done if we have previously loaded this file, since we might be previewing another note now. + PreparePreview(note, volume); + prevFile = filename; + } + } + EndWaitCursor(); + if(!ok) + { + PausePlayback(); + UnsetPlaybackSoundFile(); + StopPlayback(); + return false; + } + if(IsPlaying() && (m_pSndFile == &m_WaveFile)) + { + return true; + } + return PlaySoundFile(&m_WaveFile); +} + + +bool CMainFrame::PlaySoundFile(CSoundFile &sndFile, INSTRUMENTINDEX nInstrument, SAMPLEINDEX nSample, ModCommand::NOTE note, int volume) +{ + bool ok = false; + BeginWaitCursor(); + { + CriticalSection cs; + InitPreview(); + m_WaveFile.m_nType = sndFile.GetType(); + if ((nInstrument) && (nInstrument <= sndFile.GetNumInstruments())) + { + m_WaveFile.m_nInstruments = 1; + m_WaveFile.m_nSamples = 32; + } else + { + m_WaveFile.m_nInstruments = 0; + m_WaveFile.m_nSamples = 1; + } + if (nInstrument != INSTRUMENTINDEX_INVALID && nInstrument <= sndFile.GetNumInstruments()) + { + m_WaveFile.ReadInstrumentFromSong(1, sndFile, nInstrument); + } else if(nSample != SAMPLEINDEX_INVALID && nSample <= sndFile.GetNumSamples()) + { + m_WaveFile.ReadSampleFromSong(1, sndFile, nSample); + } + PreparePreview(note, volume); + ok = true; + } + EndWaitCursor(); + if(!ok) + { + PausePlayback(); + UnsetPlaybackSoundFile(); + StopPlayback(); + return false; + } + if(IsPlaying() && (m_pSndFile == &m_WaveFile)) + { + return true; + } + return PlaySoundFile(&m_WaveFile); +} + + +void CMainFrame::InitPreview() +{ + m_WaveFile.Destroy(); + m_WaveFile.Create(FileReader()); + m_WaveFile.m_nDefaultTempo.Set(125); + m_WaveFile.m_nDefaultSpeed = 6; + m_WaveFile.m_nType = MOD_TYPE_MPT; + m_WaveFile.m_nChannels = 2; + m_WaveFile.m_nInstruments = 1; + m_WaveFile.m_nTempoMode = TempoMode::Classic; + m_WaveFile.Order().assign(1, 0); + m_WaveFile.Patterns.Insert(0, 2); + m_WaveFile.m_SongFlags = SONG_LINEARSLIDES; +} + + +void CMainFrame::PreparePreview(ModCommand::NOTE note, int volume) +{ + m_WaveFile.m_SongFlags.reset(SONG_PAUSED); + m_WaveFile.SetRepeatCount(-1); + m_WaveFile.ResetPlayPos(); + + const CModDoc *activeDoc = GetActiveDoc(); + if(activeDoc != nullptr && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOEXTRALOUD)) + { + m_WaveFile.SetMixLevels(activeDoc->GetSoundFile().GetMixLevels()); + m_WaveFile.m_nSamplePreAmp = activeDoc->GetSoundFile().m_nSamplePreAmp; + } else + { + // Preview at 0dB + m_WaveFile.SetMixLevels(MixLevels::v1_17RC3); + m_WaveFile.m_nSamplePreAmp = static_cast<uint32>(m_WaveFile.GetPlayConfig().getNormalSamplePreAmp()); + } + + // Avoid global volume ramping when trying samples in the treeview. + m_WaveFile.m_nDefaultGlobalVolume = m_WaveFile.m_PlayState.m_nGlobalVolume = (volume > 0) ? volume : MAX_GLOBAL_VOLUME; + + if(m_WaveFile.Patterns.IsValidPat(0)) + { + auto m = m_WaveFile.Patterns[0].GetRow(0); + if(m_WaveFile.GetNumSamples() > 0) + { + m[0].note = note; + m[0].instr = 1; + } + // Infinite loop on second row + m[1 * 2].command = CMD_POSITIONJUMP; + m[1 * 2 + 1].command = CMD_PATTERNBREAK; + m[1 * 2 + 1].param = 1; + } + m_WaveFile.InitPlayer(true); +} + + +HWND CMainFrame::GetFollowSong() const +{ + return GetModPlaying() ? GetModPlaying()->GetFollowWnd() : NULL; +} + + +void CMainFrame::IdleHandlerSounddevice() +{ + MPT_TRACE_SCOPE(); + if(gpSoundDevice) + { + const FlagSet<SoundDevice::RequestFlags> requestFlags = gpSoundDevice->GetRequestFlags(); + if(requestFlags[SoundDevice::RequestFlagClose]) + { + StopPlayback(); + audioCloseDevice(); + } else if(requestFlags[SoundDevice::RequestFlagReset]) + { + ResetSoundCard(); + } else if(requestFlags[SoundDevice::RequestFlagRestart]) + { + RestartPlayback(); + } else + { + gpSoundDevice->OnIdle(); + } + } +} + + +BOOL CMainFrame::ResetSoundCard() +{ + MPT_TRACE_SCOPE(); + return CMainFrame::SetupSoundCard(TrackerSettings::Instance().GetSoundDeviceSettings(TrackerSettings::Instance().GetSoundDeviceIdentifier()), TrackerSettings::Instance().GetSoundDeviceIdentifier(), TrackerSettings::Instance().m_SoundSettingsStopMode, true); +} + + +BOOL CMainFrame::SetupSoundCard(SoundDevice::Settings deviceSettings, SoundDevice::Identifier deviceIdentifier, SoundDeviceStopMode stoppedMode, bool forceReset) +{ + MPT_TRACE_SCOPE(); + if(forceReset + || (TrackerSettings::Instance().GetSoundDeviceIdentifier() != deviceIdentifier) + || (TrackerSettings::Instance().GetSoundDeviceSettings(deviceIdentifier) != deviceSettings) + || (TrackerSettings::Instance().m_SoundSettingsStopMode != stoppedMode) + ) + { + CModDoc *pActiveMod = nullptr; + if(IsPlaying()) + { + if ((m_pSndFile) && (!m_pSndFile->IsPaused())) pActiveMod = GetModPlaying(); + PauseMod(); + } + if(gpSoundDevice) + { + gpSoundDevice->Close(); + } + TrackerSettings::Instance().m_SoundSettingsStopMode = stoppedMode; + switch(stoppedMode) + { + case SoundDeviceStopModeClosed: + deviceSettings.KeepDeviceRunning = true; + break; + case SoundDeviceStopModeStopped: + deviceSettings.KeepDeviceRunning = false; + break; + case SoundDeviceStopModePlaying: + deviceSettings.KeepDeviceRunning = true; + break; + } + TrackerSettings::Instance().SetSoundDeviceIdentifier(deviceIdentifier); + TrackerSettings::Instance().SetSoundDeviceSettings(deviceIdentifier, deviceSettings); + TrackerSettings::Instance().MixerOutputChannels = deviceSettings.Channels; + TrackerSettings::Instance().MixerNumInputChannels = deviceSettings.InputChannels; + TrackerSettings::Instance().MixerSamplerate = deviceSettings.Samplerate; + if(pActiveMod) + { + PlayMod(pActiveMod); + } + UpdateWindow(); + } else + { + // No need to restart playback + CriticalSection cs; + if(GetSoundFilePlaying()) UpdateAudioParameters(*GetSoundFilePlaying(), FALSE); + } + return TRUE; +} + + +BOOL CMainFrame::SetupPlayer() +{ + CriticalSection cs; + if(GetSoundFilePlaying()) UpdateAudioParameters(*GetSoundFilePlaying(), FALSE); + return TRUE; +} + + +BOOL CMainFrame::SetupMiscOptions() +{ + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_MUTECHNMODE) + TrackerSettings::Instance().MixerFlags |= SNDMIX_MUTECHNMODE; + else + TrackerSettings::Instance().MixerFlags &= ~SNDMIX_MUTECHNMODE; + { + CriticalSection cs; + if(GetSoundFilePlaying()) UpdateAudioParameters(*GetSoundFilePlaying()); + } + + m_wndToolBar.EnableFlatButtons(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS); + + UpdateTree(nullptr, UpdateHint().MPTOptions()); + UpdateAllViews(UpdateHint().MPTOptions()); + return true; +} + + +void CMainFrame::SetupMidi(DWORD d, UINT n) +{ + bool deviceChanged = (TrackerSettings::Instance().m_nMidiDevice != n); + TrackerSettings::Instance().m_dwMidiSetup = d; + TrackerSettings::Instance().SetMIDIDevice(n); + if(deviceChanged && shMidiIn) + { + // Device has changed, close the old one. + midiCloseDevice(); + midiOpenDevice(); + } +} + + +void CMainFrame::UpdateAllViews(UpdateHint hint, CObject *pHint) +{ + CModDocTemplate *pDocTmpl = theApp.GetModDocTemplate(); + if (pDocTmpl) + { + for(auto &doc : *pDocTmpl) + { + doc->UpdateAllViews(nullptr, hint, pHint); + } + } +} + + +void CMainFrame::SetUserText(LPCTSTR lpszText) +{ + if (lpszText[0] | m_szUserText[0]) + { + _tcscpy(m_szUserText, lpszText); + OnUpdateUser(nullptr); + } +} + + +void CMainFrame::SetInfoText(LPCTSTR lpszText) +{ + if (lpszText[0] | m_szInfoText[0]) + { + _tcscpy(m_szInfoText, lpszText); + OnUpdateInfo(nullptr); + } +} + + +void CMainFrame::SetXInfoText(LPCTSTR lpszText) +{ + if (lpszText[0] | m_szXInfoText[0]) + { + _tcscpy(m_szXInfoText, lpszText); + OnUpdateInfo(nullptr); + } +} + + +void CMainFrame::SetHelpText(LPCTSTR lpszText) +{ + m_wndStatusBar.SetPaneText(0, lpszText); +} + + +void CMainFrame::OnDocumentCreated(CModDoc *pModDoc) +{ + m_wndTree.OnDocumentCreated(pModDoc); + UpdateMRUList(); +} + + +void CMainFrame::OnDocumentClosed(CModDoc *pModDoc) +{ + if (pModDoc == GetModPlaying()) PauseMod(); + + m_wndTree.OnDocumentClosed(pModDoc); +} + + +void CMainFrame::UpdateTree(CModDoc *pModDoc, UpdateHint hint, CObject *pHint) +{ + m_wndTree.OnUpdate(pModDoc, hint, pHint); +} + + +void CMainFrame::RefreshDlsBanks() +{ + m_wndTree.RefreshDlsBanks(); +} + + +///////////////////////////////////////////////////////////////////////////// +// CMainFrame message handlers + + +void CMainFrame::OnViewOptions() +{ + if (m_bOptionsLocked) + return; + + CPropertySheet dlg(_T("OpenMPT Setup"), this, m_nLastOptionsPage); + COptionsGeneral general; + COptionsSoundcard sounddlg(TrackerSettings::Instance().m_SoundDeviceIdentifier); + COptionsSampleEditor smpeditor; + COptionsKeyboard keyboard; + COptionsColors colors; + COptionsMixer mixerdlg; + CMidiSetupDlg mididlg(TrackerSettings::Instance().m_dwMidiSetup, TrackerSettings::Instance().GetCurrentMIDIDevice()); + PathConfigDlg pathsdlg; +#if defined(MPT_ENABLE_UPDATE) + CUpdateSetupDlg updatedlg; +#endif // MPT_ENABLE_UPDATE + COptionsAdvanced advanced; + COptionsWine winedlg; + dlg.AddPage(&general); + dlg.AddPage(&sounddlg); + dlg.AddPage(&mixerdlg); +#if !defined(NO_REVERB) || !defined(NO_DSP) || !defined(NO_EQ) || !defined(NO_AGC) + COptionsPlayer dspdlg; + dlg.AddPage(&dspdlg); +#endif + dlg.AddPage(&smpeditor); + dlg.AddPage(&keyboard); + dlg.AddPage(&colors); + dlg.AddPage(&mididlg); + dlg.AddPage(&pathsdlg); +#if defined(MPT_ENABLE_UPDATE) + dlg.AddPage(&updatedlg); +#endif // MPT_ENABLE_UPDATE + dlg.AddPage(&advanced); + if(mpt::OS::Windows::IsWine()) dlg.AddPage(&winedlg); + m_bOptionsLocked = true; + m_SoundCardOptionsDialog = &sounddlg; +#if defined(MPT_ENABLE_UPDATE) + m_UpdateOptionsDialog = &updatedlg; +#endif // MPT_ENABLE_UPDATE + dlg.DoModal(); + m_SoundCardOptionsDialog = nullptr; +#if defined(MPT_ENABLE_UPDATE) + m_UpdateOptionsDialog = nullptr; +#endif // MPT_ENABLE_UPDATE + m_bOptionsLocked = false; + m_wndTree.OnOptionsChanged(); +} + + +void CMainFrame::OnPluginManager() +{ +#ifndef NO_PLUGINS + PLUGINDEX nPlugslot = PLUGINDEX_INVALID; + CModDoc* pModDoc = GetActiveDoc(); + + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + //Find empty plugin slot + for (PLUGINDEX nPlug = 0; nPlug < MAX_MIXPLUGINS; nPlug++) + { + if (sndFile.m_MixPlugins[nPlug].pMixPlugin == nullptr) + { + nPlugslot = nPlug; + break; + } + } + } + + CSelectPluginDlg dlg(GetActiveDoc(), nPlugslot, this); + if(dlg.DoModal() == IDOK && pModDoc) + { + pModDoc->SetModified(); + //Refresh views + pModDoc->UpdateAllViews(nullptr, PluginHint().Info().Names().ModType()); + //Refresh Controls + CChildFrame *pActiveChild = (CChildFrame *)MDIGetActive(); + pActiveChild->ForceRefresh(); + } +#endif // NO_PLUGINS +} + + +void CMainFrame::OnClipboardManager() +{ + PatternClipboardDialog::Show(); +} + + +void CMainFrame::OnAddDlsBank() +{ + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .ExtensionFilter("All Sound Banks|*.dls;*.sbk;*.sf2;*.sf3;*.sf4;*.mss|" + "Downloadable Sounds Banks (*.dls)|*.dls;*.mss|" + "SoundFont 2.0 Banks (*.sf2)|*.sbk;*.sf2;*.sf3;*.sf4|" + "All Files (*.*)|*.*||"); + if(!dlg.Show()) return; + + BeginWaitCursor(); + bool ok = true; + for(const auto &file : dlg.GetFilenames()) + { + ok &= CTrackApp::AddDLSBank(file); + } + if(!ok) + { + Reporting::Error("At least one selected file was not a valid sound bank."); + } + m_wndTree.RefreshDlsBanks(); + EndWaitCursor(); +} + + +void CMainFrame::OnImportMidiLib() +{ + FileDialog dlg = OpenFileDialog() + .ExtensionFilter("Text and INI files (*.txt,*.ini)|*.txt;*.ini;*.dls;*.sf2;*.sf3;*.sf4;*.sbk|" + "Downloadable Sound Banks (*.dls)|*.dls;*.mss|" + "SoundFont 2.0 banks (*.sf2)|*.sbk;*.sf2;*.sf3;*.sf4|" + "Gravis UltraSound (ultrasnd.ini)|ultrasnd.ini|" + "All Files (*.*)|*.*||"); + if(!dlg.Show()) return; + + BeginWaitCursor(); + CTrackApp::ImportMidiConfig(dlg.GetFirstFile()); + m_wndTree.RefreshMidiLibrary(); + EndWaitCursor(); +} + + +void CMainFrame::OnTimer(UINT_PTR timerID) +{ + switch(timerID) + { + case TIMERID_GUI: + OnTimerGUI(); + break; + case TIMERID_NOTIFY: + OnTimerNotify(); + break; + } +} + + +void CMainFrame::OnTimerGUI() +{ + IdleHandlerSounddevice(); + + // Display Time in status bar + CSoundFile::samplecount_t time = 0; + if(m_pSndFile != nullptr && m_pSndFile->GetSampleRate() != 0) + { + time = m_pSndFile->GetTotalSampleCount() / m_pSndFile->GetSampleRate(); + if(time != m_dwTimeSec) + { + m_dwTimeSec = time; + m_nAvgMixChn = m_nMixChn; + OnUpdateTime(NULL); + } + } + + // Idle Time Check + DWORD curTime = timeGetTime(); + + if(m_AutoSaver.IsEnabled()) + { + bool success = m_AutoSaver.DoSave(curTime); + if (!success) // autosave failure; bring up options. + { + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_PATHS; + OnViewOptions(); + } + } + + if(m_SoundCardOptionsDialog) + { + m_SoundCardOptionsDialog->UpdateStatistics(); + } + +#ifdef USE_PROFILER + { + Profiler::Update(); + + CWnd * cwnd = CWnd::FromHandle(this->m_hWndMDIClient); + CClientDC dc(cwnd); + + int height = 16; + int width = 256; + + std::vector<std::string> catnames = Profiler::GetCategoryNames(); + std::vector<double> cats = Profiler::DumpCategories(); + + for(int i=0; i<Profiler::CategoriesCount; i++) + { + dc.FillSolidRect(0, i * height, (int)(width * cats[i]), height, RGB(255,0,0)); + dc.FillSolidRect((int)(width * cats[i]), i * height, width - (int)(width * cats[i]), height, RGB(192,192,192)); + RECT rect; + cwnd->GetClientRect(&rect); + rect.left += width; + rect.top += i * height; + auto s = mpt::ToCString(mpt::Charset::ASCII, mpt::afmt::right(6, mpt::afmt::fix(cats[i] * 100.0, 3)) + "% " + catnames[i]); + dc.DrawText(s, s.GetLength(), &rect, DT_LEFT); + } + + RECT rect; + cwnd->GetClientRect(&rect); + rect.top += Profiler::CategoriesCount * height; + auto s = mpt::ToCString(mpt::Charset::ASCII, Profiler::DumpProfiles()); + dc.DrawText(s, s.GetLength(), &rect, DT_LEFT); + + cwnd->Detach(); + } +#endif + +} + + +CModDoc *CMainFrame::GetActiveDoc() const +{ + CMDIChildWnd *pMDIActive = MDIGetActive(); + if (pMDIActive) + { + return static_cast<CModDoc *>(pMDIActive->GetActiveDocument()); + } + return nullptr; +} + + +CView *CMainFrame::GetActiveView() const +{ + CMDIChildWnd *pMDIActive = MDIGetActive(); + if (pMDIActive) + { + return pMDIActive->GetActiveView(); + } + + return nullptr; +} + + +void CMainFrame::SwitchToActiveView() +{ + CWnd *wnd = GetActiveView(); + if (wnd) + { + // Hack: If the upper view is active, we only get the "container" (the dialog view with the tabs), not the view itself. + if(!strcmp(wnd->GetRuntimeClass()->m_lpszClassName, "CModControlView")) + { + wnd = static_cast<CModControlView *>(wnd)->GetCurrentControlDlg(); + } + wnd->SetFocus(); + } +} + + +void CMainFrame::OnUpdateTime(CCmdUI *) +{ + TCHAR s[64]; + wsprintf(s, _T("%u:%02u:%02u"), + m_dwTimeSec / 3600, (m_dwTimeSec / 60) % 60, m_dwTimeSec % 60); + + if(m_pSndFile != nullptr && m_pSndFile != &m_WaveFile && !m_pSndFile->IsPaused()) + { + PATTERNINDEX nPat = m_pSndFile->m_PlayState.m_nPattern; + if(m_pSndFile->Patterns.IsValidIndex(nPat)) + { + if(nPat < 10) _tcscat(s, _T(" ")); + if(nPat < 100) _tcscat(s, _T(" ")); + wsprintf(s + _tcslen(s), _T(" [%d]"), nPat); + } + wsprintf(s + _tcslen(s), _T(" %dch"), m_nAvgMixChn); + } + m_wndStatusBar.SetPaneText(m_wndStatusBar.CommandToIndex(ID_INDICATOR_TIME), s, TRUE); +} + + +void CMainFrame::OnUpdateUser(CCmdUI *) +{ + m_wndStatusBar.SetPaneText(m_wndStatusBar.CommandToIndex(ID_INDICATOR_USER), m_szUserText, TRUE); +} + + +void CMainFrame::OnUpdateInfo(CCmdUI *) +{ + m_wndStatusBar.SetPaneText(m_wndStatusBar.CommandToIndex(ID_INDICATOR_INFO), m_szInfoText, TRUE); +} + + +void CMainFrame::OnUpdateXInfo(CCmdUI *) +{ + m_wndStatusBar.SetPaneText(m_wndStatusBar.CommandToIndex(ID_INDICATOR_XINFO), m_szXInfoText, TRUE); +} + +void CMainFrame::OnPlayerPause() +{ + if (GetModPlaying()) + { + GetModPlaying()->OnPlayerPause(); + } else + { + PauseMod(); + } +} + + +void CMainFrame::OpenMenuItemFile(const UINT nId, const bool isTemplateFile) +{ + const UINT nIdBegin = (isTemplateFile) ? ID_FILE_OPENTEMPLATE : ID_EXAMPLE_MODULES; + const std::vector<mpt::PathString> &vecFilePaths = (isTemplateFile) ? m_TemplateModulePaths : m_ExampleModulePaths; + + const UINT nIndex = nId - nIdBegin; + if (nIndex < vecFilePaths.size()) + { + const mpt::PathString& sPath = vecFilePaths[nIndex]; + const bool bExists = sPath.IsFile(); + CDocument *pDoc = nullptr; + if(bExists) + { + pDoc = theApp.GetModDocTemplate()->OpenTemplateFile(sPath, !isTemplateFile); + } + if(!pDoc) + { + Reporting::Notification(_T("The file '") + sPath.ToCString() + _T("' ") + (bExists ? _T("exists but can't be read.") : _T("does not exist."))); + } + } else + { + MPT_ASSERT(nId == UINT(isTemplateFile ? ID_FILE_OPENTEMPLATE_LASTINRANGE : ID_EXAMPLE_MODULES_LASTINRANGE)); + FileDialog::PathList files; + theApp.OpenModulesDialog(files, isTemplateFile ? theApp.GetConfigPath() + P_("TemplateModules") : theApp.GetInstallPath() + P_("ExampleSongs")); + for(const auto &file : files) + { + theApp.OpenDocumentFile(file.ToCString()); + } + } +} + + +void CMainFrame::OnOpenTemplateModule(UINT nId) +{ + OpenMenuItemFile(nId, true/*open template menu file*/); +} + + +void CMainFrame::OnExampleSong(UINT nId) +{ + OpenMenuItemFile(nId, false/*open example menu file*/); +} + + +void CMainFrame::OnOpenMRUItem(UINT nId) +{ + theApp.OpenDocumentFile(TrackerSettings::Instance().mruFiles[nId - ID_MRU_LIST_FIRST].ToCString()); +} + + +void CMainFrame::OnUpdateMRUItem(CCmdUI *cmd) +{ + cmd->Enable(!TrackerSettings::Instance().mruFiles.empty()); +} + + +LRESULT CMainFrame::OnInvalidatePatterns(WPARAM, LPARAM) +{ + UpdateAllViews(UpdateHint().MPTOptions()); + return TRUE; +} + + +LRESULT CMainFrame::OnUpdatePosition(WPARAM, LPARAM lParam) +{ + OPENMPT_PROFILE_FUNCTION(Profiler::GUI); + m_VUMeterOutput.SetDecaySpeedDecibelPerSecond(TrackerSettings::Instance().VuMeterDecaySpeedDecibelPerSecond); // update in notification update in order to avoid querying the settings framework from inside audio thread + m_VUMeterInput.SetDecaySpeedDecibelPerSecond(TrackerSettings::Instance().VuMeterDecaySpeedDecibelPerSecond); // update in notification update in order to avoid querying the settings framework from inside audio thread + Notification *pnotify = (Notification *)lParam; + if (pnotify) + { + if(pnotify->type[Notification::EOS]) + { + PostMessage(WM_COMMAND, ID_PLAYER_STOP); + } + //Log("OnUpdatePosition: row=%d time=%lu\n", pnotify->nRow, pnotify->TimestampSamples); + if(CModDoc *modDoc = GetModPlaying(); modDoc != nullptr) + { + m_wndTree.UpdatePlayPos(modDoc, pnotify); + if (GetFollowSong()) + ::SendMessage(GetFollowSong(), WM_MOD_UPDATEPOSITION, 0, lParam); + if(m_pSndFile->m_pluginDryWetRatioChanged.any()) + { + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + if(m_pSndFile->m_pluginDryWetRatioChanged[i]) + modDoc->PostMessageToAllViews(WM_MOD_PLUGINDRYWETRATIOCHANGED, i); + } + m_pSndFile->m_pluginDryWetRatioChanged.reset(); + } + } + m_nMixChn = pnotify->mixedChannels; + + bool duplicateMono = false; + if(pnotify->masterVUinChannels == 1 && pnotify->masterVUoutChannels > 1) + { + duplicateMono = true; + } else if(pnotify->masterVUoutChannels == 1 && pnotify->masterVUinChannels > 1) + { + duplicateMono = true; + } + uint8 countChan = 0; + uint32 vu[VUMeter::maxChannels * 2]; + MemsetZero(vu); + std::copy(pnotify->masterVUin.begin(), pnotify->masterVUin.begin() + pnotify->masterVUinChannels, vu + countChan); + + countChan += pnotify->masterVUinChannels; + if(pnotify->masterVUinChannels == 1 && duplicateMono) + { + std::copy(pnotify->masterVUin.begin(), pnotify->masterVUin.begin() + 1, vu + countChan); + countChan += 1; + } + + std::copy(pnotify->masterVUout.begin(), pnotify->masterVUout.begin() + pnotify->masterVUoutChannels, vu + countChan); + countChan += pnotify->masterVUoutChannels; + if(pnotify->masterVUoutChannels == 1 && duplicateMono) + { + std::copy(pnotify->masterVUout.begin(), pnotify->masterVUout.begin() + 1, vu + countChan); + countChan += 1; + } + + m_wndToolBar.m_VuMeter.SetVuMeter(countChan, vu, pnotify->type[Notification::Stop]); + + m_wndToolBar.SetCurrentSong(m_pSndFile); + } + return 0; +} + + +LRESULT CMainFrame::OnUpdateViews(WPARAM modDoc, LPARAM hint) +{ + CModDoc *doc = reinterpret_cast<CModDoc *>(modDoc); + CModDocTemplate *pDocTmpl = theApp.GetModDocTemplate(); + if(pDocTmpl && pDocTmpl->DocumentExists(doc)) + { + // Since this message is potentially posted, we first need to verify if the document still exists + doc->UpdateAllViews(nullptr, UpdateHint::FromLPARAM(hint)); + } + return 0; +} + + +LRESULT CMainFrame::OnSetModified(WPARAM modDoc, LPARAM) +{ + CModDoc *doc = reinterpret_cast<CModDoc *>(modDoc); + CModDocTemplate *pDocTmpl = theApp.GetModDocTemplate(); + if(pDocTmpl && pDocTmpl->DocumentExists(doc)) + { + // Since this message is potentially posted, we first need to verify if the document still exists + doc->UpdateFrameCounts(); + } + return 0; +} + + +void CMainFrame::OnPanic() +{ + // "Panic button." At the moment, it just resets all VSTi and sample notes. + if(GetModPlaying()) + GetModPlaying()->OnPanic(); +} + + +void CMainFrame::OnPrevOctave() +{ + UINT n = GetBaseOctave(); + if (n > MIN_BASEOCTAVE) m_wndToolBar.SetBaseOctave(n-1); +} + + +void CMainFrame::OnNextOctave() +{ + UINT n = GetBaseOctave(); + if (n < MAX_BASEOCTAVE) m_wndToolBar.SetBaseOctave(n+1); +} + + +void CMainFrame::OnReportBug() +{ + CTrackApp::OpenURL(Build::GetURL(Build::Url::Bugtracker)); +} + + +BOOL CMainFrame::OnInternetLink(UINT nID) +{ + mpt::ustring url; + switch(nID) + { + case ID_NETLINK_MODPLUG: url = Build::GetURL(Build::Url::Website); break; + case ID_NETLINK_TOP_PICKS: url = Build::GetURL(Build::Url::TopPicks); break; + } + if(!url.empty()) + { + return CTrackApp::OpenURL(url) ? TRUE : FALSE; + } + return FALSE; +} + + +void CMainFrame::OnRButtonDown(UINT, CPoint pt) +{ + CMenu Menu; + ClientToScreen(&pt); + if (Menu.LoadMenu(IDR_TOOLBARS)) + { + CMenu *pSubMenu = Menu.GetSubMenu(0); + if (pSubMenu != nullptr) pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this); + } +} + + +LRESULT CMainFrame::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case kcViewTree: OnBarCheck(IDD_TREEVIEW); break; + case kcViewOptions: OnViewOptions(); break; + case kcViewMain: OnBarCheck(59392 /* MAINVIEW */); break; + case kcFileImportMidiLib: OnImportMidiLib(); break; + case kcFileAddSoundBank: OnAddDlsBank(); break; + case kcPauseSong: OnPlayerPause(); break; + case kcPrevOctave: OnPrevOctave(); break; + case kcNextOctave: OnNextOctave(); break; + case kcFileNew: theApp.OnFileNew(); break; + case kcFileOpen: theApp.OnFileOpen(); break; + case kcMidiRecord: OnMidiRecord(); break; + case kcHelp: OnHelp(); break; + case kcViewAddPlugin: OnPluginManager(); break; + case kcNextDocument: MDINext(); break; + case kcPrevDocument: MDIPrev(); break; + case kcFileCloseAll: theApp.OnFileCloseAll(); break; + + + //D'oh!! moddoc isn't a CWnd so we have to handle its messages and pass them on. + + case kcFileSaveAs: + case kcFileSaveCopy: + case kcFileSaveAsWave: + case kcFileSaveMidi: + case kcFileSaveOPL: + case kcFileExportCompat: + case kcFileClose: + case kcFileSave: + case kcViewGeneral: + case kcViewPattern: + case kcViewSamples: + case kcViewInstruments: + case kcViewComments: + case kcViewGraph: //rewbs.graph + case kcViewSongProperties: + case kcViewTempoSwing: + case kcViewMIDImapping: + case kcViewEditHistory: + case kcViewChannelManager: + case kcPlayPatternFromCursor: + case kcPlayPatternFromStart: + case kcPlaySongFromCursor: + case kcPlaySongFromStart: + case kcPlayPauseSong: + case kcPlaySongFromPattern: + case kcStopSong: + case kcToggleLoopSong: + case kcPanic: + case kcEstimateSongLength: + case kcApproxRealBPM: + case kcTempoIncrease: + case kcTempoDecrease: + case kcTempoIncreaseFine: + case kcTempoDecreaseFine: + case kcSpeedIncrease: + case kcSpeedDecrease: + case kcViewToggle: + { + CModDoc *modDoc = GetActiveDoc(); + if (modDoc) + return GetActiveDoc()->OnCustomKeyMsg(wParam, lParam); + else if(wParam == kcPlayPauseSong || wParam == kcStopSong) + StopPreview(); + break; + } + + case kcSwitchToInstrLibrary: + if(m_bModTreeHasFocus) + SwitchToActiveView(); + else + m_wndTree.SetFocus(); + break; + + //if handled neither by MainFrame nor by ModDoc... + default: + //If the treeview has focus, post command to the tree view + if (m_bModTreeHasFocus) + return m_wndTree.SendMessageToModTree(WM_MOD_KEYCOMMAND, wParam, lParam); + if (m_pNoteMapHasFocus) + return m_pNoteMapHasFocus->SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); + if (m_pOrderlistHasFocus) + return m_pOrderlistHasFocus->SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); + + //Else send it to the active view + CMDIChildWnd *pMDIActive = MDIGetActive(); + CWnd *wnd = nullptr; + if(pMDIActive) + { + wnd = pMDIActive->GetActiveView(); + // Hack: If the upper view is active, we only get the "container" (the dialog view with the tabs), not the view itself. + if(!strcmp(wnd->GetRuntimeClass()->m_lpszClassName, "CModControlView")) + { + wnd = static_cast<CModControlView *>(wnd)->GetCurrentControlDlg(); + } + } + if(wnd) + return wnd->SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); + return kcNull; + } + + return wParam; +} + + +void CMainFrame::OnInitMenu(CMenu* pMenu) +{ + m_InputHandler->SetModifierMask(ModNone); + CMDIFrameWnd::OnInitMenu(pMenu); + +} + + +BOOL CMainFrame::InitRenderer(CSoundFile* pSndFile) +{ + CriticalSection cs; + pSndFile->m_bIsRendering = true; + pSndFile->SuspendPlugins(); + pSndFile->ResumePlugins(); + return true; +} + + +BOOL CMainFrame::StopRenderer(CSoundFile* pSndFile) +{ + CriticalSection cs; + pSndFile->SuspendPlugins(); + pSndFile->m_bIsRendering = false; + return true; +} + + +// We have switched focus to a new module - might need to update effect keys to reflect module type +bool CMainFrame::UpdateEffectKeys(const CModDoc *modDoc) +{ + if(modDoc != nullptr) + { + return m_InputHandler->SetEffectLetters(modDoc->GetSoundFile().GetModSpecifications()); + } + return false; +} + + +void CMainFrame::OnKillFocus(CWnd* pNewWnd) +{ + CMDIFrameWnd::OnKillFocus(pNewWnd); + + //rewbs: ensure modifiers are reset when we leave the window (e.g. alt-tab) + m_InputHandler->SetModifierMask(ModNone); +} + + +void CMainFrame::OnShowWindow(BOOL bShow, UINT /*nStatus*/) +{ + static bool firstShow = true; + if (bShow && !IsWindowVisible() && firstShow) + { + firstShow = false; + WINDOWPLACEMENT wpl; + GetWindowPlacement(&wpl); + wpl = theApp.GetSettings().Read<WINDOWPLACEMENT>(U_("Display"), U_("WindowPlacement"), wpl); + SetWindowPlacement(&wpl); + } +} + + +void CMainFrame::OnInternetUpdate() +{ +#if defined(MPT_ENABLE_UPDATE) + CUpdateCheck::DoManualUpdateCheck(); +#endif // MPT_ENABLE_UPDATE +} + + +void CMainFrame::OnUpdateAvailable() +{ +#if defined(MPT_ENABLE_UPDATE) + if(m_updateCheckResult) + CUpdateCheck::ShowSuccessGUI(false, *m_updateCheckResult); + else + CUpdateCheck::DoManualUpdateCheck(); +#endif // MPT_ENABLE_UPDATE +} + + +void CMainFrame::OnShowSettingsFolder() +{ + theApp.OpenDirectory(theApp.GetConfigPath()); +} + + + +class CUpdateCheckProgressDialog + : public CProgressDialog +{ +public: + CUpdateCheckProgressDialog(CWnd *parent) + : CProgressDialog(parent) + { + return; + } + void Run() override + { + } +}; + +static std::unique_ptr<CUpdateCheckProgressDialog> g_UpdateCheckProgressDialog = nullptr; + + +#if defined(MPT_ENABLE_UPDATE) + + +bool CMainFrame::ShowUpdateIndicator(const UpdateCheckResult &result, const CString &releaseVersion, const CString &infoURL, bool showHighlight) +{ + m_updateCheckResult = std::make_unique<UpdateCheckResult>(result); + if(m_wndToolBar.IsVisible()) + { + return m_wndToolBar.ShowUpdateInfo(releaseVersion, infoURL, showHighlight); + } else + { + GetMenu()->RemoveMenu(ID_UPDATE_AVAILABLE, MF_BYCOMMAND); + GetMenu()->AppendMenu(MF_STRING, ID_UPDATE_AVAILABLE, _T("[Update Available]")); + DrawMenuBar(); + return true; + } +} + + +LRESULT CMainFrame::OnUpdateCheckStart(WPARAM wparam, LPARAM lparam) +{ + GetMenu()->RemoveMenu(ID_UPDATE_AVAILABLE, MF_BYCOMMAND); + m_wndToolBar.RemoveUpdateInfo(); + + const bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam); + CString updateText = _T("Checking for updates..."); + if(isAutoUpdate) + { + SetHelpText(updateText); + } else if(m_UpdateOptionsDialog) + { + m_UpdateOptionsDialog->SetDlgItemText(IDC_LASTUPDATE, updateText); + } else + { + if(!g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog = std::make_unique<CUpdateCheckProgressDialog>(CMainFrame::GetMainFrame()); + g_UpdateCheckProgressDialog->Create(IDD_PROGRESS, CMainFrame::GetMainFrame()); + g_UpdateCheckProgressDialog->SetTitle(_T("Checking for updates...")); + g_UpdateCheckProgressDialog->SetText(_T("Checking for updates...")); + g_UpdateCheckProgressDialog->SetAbortText(_T("&Cancel")); + g_UpdateCheckProgressDialog->SetRange(0, 100); + g_UpdateCheckProgressDialog->ShowWindow(SW_SHOWDEFAULT); + } + if(g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog->SetProgress(0); + } + } + return TRUE; +} + + +LRESULT CMainFrame::OnUpdateCheckProgress(WPARAM wparam, LPARAM lparam) +{ + bool isAutoUpdate = wparam != 0; + CString updateText = MPT_CFORMAT("Checking for updates... {}%")(lparam); + if(isAutoUpdate) + { + SetHelpText(updateText); + } else if(m_UpdateOptionsDialog) + { + m_UpdateOptionsDialog->SetDlgItemText(IDC_LASTUPDATE, updateText); + } else + { + if(g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog->SetProgress(lparam); + if(g_UpdateCheckProgressDialog->m_abort) + { + return FALSE; + } + } + } + return TRUE; +} + + +LRESULT CMainFrame::OnUpdateCheckCanceled(WPARAM wparam, LPARAM lparam) +{ + m_updateCheckResult.reset(); + bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam); + CString updateText = _T("Checking for updates... Canceled."); + if(isAutoUpdate) + { + SetHelpText(updateText); + } else if(m_UpdateOptionsDialog) + { + m_UpdateOptionsDialog->SetDlgItemText(IDC_LASTUPDATE, updateText); + m_UpdateOptionsDialog->SettingChanged(TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath()); + } else + { + if(g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog->DestroyWindow(); + g_UpdateCheckProgressDialog = nullptr; + } + } + if(isAutoUpdate) + { + SetHelpText(_T("")); + } + return TRUE; +} + + +LRESULT CMainFrame::OnUpdateCheckFailure(WPARAM wparam, LPARAM lparam) +{ + m_updateCheckResult.reset(); + const bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam); + const CUpdateCheck::Error &error = CUpdateCheck::MessageAsError(wparam, lparam); + CString updateText = MPT_CFORMAT("Checking for updates failed: {}")(mpt::get_exception_text<mpt::ustring>(error)); + if(isAutoUpdate) + { + SetHelpText(updateText); + } else if(m_UpdateOptionsDialog) + { + m_UpdateOptionsDialog->SetDlgItemText(IDC_LASTUPDATE, updateText); + m_UpdateOptionsDialog->SettingChanged(TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath()); + } else + { + if(g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog->DestroyWindow(); + g_UpdateCheckProgressDialog = nullptr; + } + } + CUpdateCheck::ShowFailureGUI(isAutoUpdate, error); + if(isAutoUpdate) + { + SetHelpText(_T("")); + } + return TRUE; +} + + +LRESULT CMainFrame::OnUpdateCheckSuccess(WPARAM wparam, LPARAM lparam) +{ + m_updateCheckResult.reset(); + const bool isAutoUpdate = CUpdateCheck::IsAutoUpdateFromMessage(wparam, lparam); + const UpdateCheckResult &result = CUpdateCheck::MessageAsResult(wparam, lparam); + CUpdateCheck::AcknowledgeSuccess(result); + if(result.CheckTime != time_t{}) + TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime); + if(!isAutoUpdate) + { + if(m_UpdateOptionsDialog) + { + m_UpdateOptionsDialog->SetDlgItemText(IDC_LASTUPDATE, _T("Checking for updates... Done.")); + } else + { + if(g_UpdateCheckProgressDialog) + { + g_UpdateCheckProgressDialog->DestroyWindow(); + g_UpdateCheckProgressDialog = nullptr; + } + SetHelpText(_T("")); + } + } + CUpdateCheck::ShowSuccessGUI(isAutoUpdate, result); + if(isAutoUpdate) + { + SetHelpText(_T("")); + } + return TRUE; +} + + +LPARAM CMainFrame::OnToolbarUpdateIndicatorClick(WPARAM action, LPARAM) +{ + const auto popAction = static_cast<UpdateToolTip::PopAction>(action); + if(popAction != UpdateToolTip::PopAction::ClickBubble) + return 0; + + if(m_updateCheckResult) + CUpdateCheck::ShowSuccessGUI(false, *m_updateCheckResult); + else + CUpdateCheck::DoManualUpdateCheck(); + return 0; +} + + +#endif // MPT_ENABLE_UPDATE + + +void CMainFrame::OnHelp() +{ + CView *view = GetActiveView(); + const char *page = ""; + if(m_bOptionsLocked) + { + switch(m_nLastOptionsPage) + { + case OPTIONS_PAGE_GENERAL: page = "::/Setup_General.html"; break; + case OPTIONS_PAGE_SOUNDCARD: page = "::/Setup_Soundcard.html"; break; + case OPTIONS_PAGE_MIXER: page = "::/Setup_Mixer.html"; break; + case OPTIONS_PAGE_PLAYER: page = "::/Setup_DSP.html"; break; + case OPTIONS_PAGE_SAMPLEDITOR: page = "::/Setup_Samples.html"; break; + case OPTIONS_PAGE_KEYBOARD: page = "::/Setup_Keyboard.html"; break; + case OPTIONS_PAGE_COLORS: page = "::/Setup_Display.html"; break; + case OPTIONS_PAGE_MIDI: page = "::/Setup_MIDI.html"; break; + case OPTIONS_PAGE_PATHS: page = "::/Setup_Paths_Auto_Save.html"; break; + case OPTIONS_PAGE_UPDATE: page = "::/Setup_Update.html"; break; + case OPTIONS_PAGE_ADVANCED: page = "::/Setup_Advanced.html"; break; + case OPTIONS_PAGE_WINE: page = "::/Setup_Wine.html"; break; + } + } else if(view != nullptr) + { + const char *className = view->GetRuntimeClass()->m_lpszClassName; + if(!strcmp("CViewGlobals", className)) + page = "::/General.html"; + else if(!strcmp("CViewPattern", className)) + page = "::/Patterns.html"; + else if(!strcmp("CViewSample", className)) + page = "::/Samples.html"; + else if(!strcmp("CViewInstrument", className)) + page = "::/Instruments.html"; + else if(!strcmp("CViewComments", className)) + page = "::/Comments.html"; + else if(!strcmp("CModControlView", className)) + { + switch(static_cast<CModControlView*>(view)->GetActivePage()) + { + case CModControlView::VIEW_GLOBALS: page = "::/General.html"; break; + case CModControlView::VIEW_PATTERNS: page = "::/Patterns.html"; break; + case CModControlView::VIEW_SAMPLES: page = "::/Samples.html"; break; + case CModControlView::VIEW_INSTRUMENTS: page = "::/Instruments.html"; break; + case CModControlView::VIEW_COMMENTS: page = "::/Comments.html"; break; + } + } + } + + const mpt::PathString helpFile = theApp.GetInstallPath() + P_("OpenMPT Manual.chm") + mpt::PathString::FromUTF8(page) + P_(">OpenMPT"); + if(!::HtmlHelp(m_hWnd, helpFile.AsNative().c_str(), strcmp(page, "") ? HH_DISPLAY_TOC : HH_DISPLAY_TOPIC, NULL)) + { + Reporting::Error(_T("Could not find help file:\n") + helpFile.ToCString()); + return; + } + //::ShowWindow(hwndHelp, SW_SHOWMAXIMIZED); +} + + +LRESULT CMainFrame::OnViewMIDIMapping(WPARAM wParam, LPARAM lParam) +{ + static bool inMapper = false; + if(!inMapper) + { + inMapper = true; + CModDoc *doc = GetActiveDoc(); + if(doc != nullptr) + doc->ViewMIDIMapping(static_cast<PLUGINDEX>(wParam), static_cast<PlugParamIndex>(lParam)); + inMapper = false; + } + return 0; +} + + +HMENU CMainFrame::CreateFileMenu(const size_t maxCount, std::vector<mpt::PathString>& paths, const mpt::PathString &folderName, const uint16 idRangeBegin) +{ + paths.clear(); + HMENU hMenu = ::CreatePopupMenu(); + ASSERT(hMenu != NULL); + if (hMenu != NULL) + { + UINT_PTR filesAdded = 0; + for(size_t i = 0; i < 2; i++) // 0: app items, 1: user items + { + // To avoid duplicates, check whether app path and config path are the same. + if (i == 1 && mpt::PathString::CompareNoCase(theApp.GetInstallPath(), theApp.GetConfigPath()) == 0) + break; + + mpt::PathString basePath; + basePath = (i == 0) ? theApp.GetInstallPath() : theApp.GetConfigPath(); + basePath += folderName; + if(!basePath.IsDirectory()) + continue; + + FolderScanner scanner(basePath, FolderScanner::kOnlyFiles); + mpt::PathString fileName; + while(filesAdded < maxCount && scanner.Next(fileName)) + { + paths.push_back(fileName); + CString file = fileName.GetFullFileName().ToCString(); + file.Replace(_T("&"), _T("&&")); + AppendMenu(hMenu, MF_STRING, idRangeBegin + filesAdded, file); + filesAdded++; + } + } + + if(filesAdded == 0) + { + AppendMenu(hMenu, MF_STRING | MF_GRAYED | MF_DISABLED, 0, _T("No items found")); + } else + { + AppendMenu(hMenu, MF_SEPARATOR, 0, 0); + AppendMenu(hMenu, MF_STRING, idRangeBegin + maxCount, _T("Browse...")); + } + } + + return hMenu; +} + + +void CMainFrame::CreateExampleModulesMenu() +{ + static_assert(nMaxItemsInExampleModulesMenu == ID_EXAMPLE_MODULES_LASTINRANGE - ID_EXAMPLE_MODULES, + "Make sure that there's a proper range for menu commands in resources."); + HMENU hMenu = CreateFileMenu(nMaxItemsInExampleModulesMenu, m_ExampleModulePaths, P_("ExampleSongs\\"), ID_EXAMPLE_MODULES); + CMenu* const pMainMenu = GetMenu(); + if (hMenu && pMainMenu && m_InputHandler) + VERIFY(pMainMenu->ModifyMenu(ID_EXAMPLE_MODULES, MF_BYCOMMAND | MF_POPUP, (UINT_PTR)hMenu, m_InputHandler->GetMenuText(ID_EXAMPLE_MODULES))); + else + ASSERT(false); +} + + +// Hack-ish way to get the file menu (this is necessary because the MDI document icon next to the File menu is a sub menu, too). +CMenu *CMainFrame::GetFileMenu() const +{ + CMenu *mainMenu = GetMenu(); + CMenu *fileMenu = mainMenu ? mainMenu->GetSubMenu(0) : nullptr; + if(fileMenu) + { + if(fileMenu->GetMenuItemID(1) != ID_FILE_OPEN) + fileMenu = mainMenu->GetSubMenu(1); + ASSERT(fileMenu->GetMenuItemID(1) == ID_FILE_OPEN); + } + ASSERT(fileMenu); + return fileMenu; +} + + +void CMainFrame::CreateTemplateModulesMenu() +{ + static_assert(nMaxItemsInTemplateModulesMenu == ID_FILE_OPENTEMPLATE_LASTINRANGE - ID_FILE_OPENTEMPLATE, + "Make sure that there's a proper range for menu commands in resources."); + HMENU hMenu = CreateFileMenu(nMaxItemsInTemplateModulesMenu, m_TemplateModulePaths, P_("TemplateModules\\"), ID_FILE_OPENTEMPLATE); + CMenu *pFileMenu = GetFileMenu(); + if (hMenu && pFileMenu && m_InputHandler) + { + VERIFY(pFileMenu->RemoveMenu(2, MF_BYPOSITION)); + VERIFY(pFileMenu->InsertMenu(2, MF_BYPOSITION | MF_POPUP, (UINT_PTR)hMenu, m_InputHandler->GetMenuText(ID_FILE_OPENTEMPLATE))); + } + else + ASSERT(false); +} + + +void CMainFrame::UpdateMRUList() +{ + CMenu *pMenu = GetFileMenu(); + if(!pMenu) return; + + static int firstMenu = -1; + if(firstMenu == -1) + { + int numMenus = pMenu->GetMenuItemCount(); + for(int i = 0; i < numMenus; i++) + { + if(pMenu->GetMenuItemID(i) == ID_MRU_LIST_FIRST) + { + firstMenu = i; + break; + } + } + } + + for(int i = ID_MRU_LIST_FIRST; i <= ID_MRU_LIST_LAST; i++) + { + pMenu->DeleteMenu(i, MF_BYCOMMAND); + } + + if(TrackerSettings::Instance().mruFiles.empty()) + { + // MFC will automatically ignore if we set MF_GRAYED here because of CFrameWnd::m_bAutoMenuEnable. + // So we will have to install a ON_UPDATE_COMMAND_UI callback... + pMenu->InsertMenu(firstMenu, MF_STRING | MF_BYPOSITION, ID_MRU_LIST_FIRST, _T("Recent File")); + } else + { + const mpt::PathString workDir = TrackerSettings::Instance().PathSongs.GetWorkingDir(); + const int entries = mpt::saturate_cast<int>(TrackerSettings::Instance().mruFiles.size()); + for(int i = 0; i < entries; i++) + { + mpt::winstring s = mpt::tfmt::val(i + 1) + _T(" "); + // Add mnemonics + if(i < 9) + { + s = _T("&") + s; + } else if(i == 9) + { + s = _T("1&0 "); + } + + const mpt::PathString &pathMPT = TrackerSettings::Instance().mruFiles[i]; + mpt::winstring path = pathMPT.AsNative(); + if(!mpt::PathString::CompareNoCase(workDir, pathMPT.GetPath())) + { + // Only show filename + path = path.substr(workDir.AsNative().length()); + } else if(path.length() > 30) // Magic number experimentally determined to be equal to MFC's behaviour + { + // Shorten path ("C:\Foo\VeryLongString...\Bar.it" => "C:\Foo\...\Bar.it") + size_t start = path.find_first_of(_T("\\/"), path.find_first_of(_T("\\/")) + 1); + size_t end = path.find_last_of(_T("\\/")); + if(start < end) + { + path = path.substr(0, start + 1) + _T("...") + path.substr(end); + } + } + path = mpt::String::Replace(path, mpt::winstring(_T("&")), mpt::winstring(_T("&&"))); + s += path; + pMenu->InsertMenu(firstMenu + i, MF_STRING | MF_BYPOSITION, ID_MRU_LIST_FIRST + i, mpt::ToCString(s)); + } + } +} + + +BOOL CMainFrame::OnQueryEndSession() +{ + int modifiedCount = 0; + for(const auto &modDoc : theApp.GetOpenDocuments()) + { + if(modDoc->IsModified()) + modifiedCount++; + } +#if (_WIN32_WINNT >= _WIN32_WINNT_VISTA) + ShutdownBlockReasonDestroy(m_hWnd); + if(modifiedCount > 0) + { + ShutdownBlockReasonCreate(m_hWnd, + MPT_WFORMAT("There {} {} unsaved file{}.")(modifiedCount == 1 ? L"is" : L"are", modifiedCount, modifiedCount == 1 ? L"" : L"s").c_str()); + } +#endif + return modifiedCount ? FALSE : TRUE; +} + + +void CMainFrame::NotifyAccessibilityUpdate(CWnd &source) +{ + if(!IsPlaying() || m_pSndFile->m_SongFlags[SONG_PAUSED]) + source.NotifyWinEvent(EVENT_OBJECT_NAMECHANGE, OBJID_CLIENT, CHILDID_SELF); +} + +// ITfLanguageProfileNotifySink implementation + +TfLanguageProfileNotifySink::TfLanguageProfileNotifySink() +{ + HRESULT hr = CoCreateInstance(CLSID_TF_InputProcessorProfiles, NULL, CLSCTX_INPROC_SERVER, IID_ITfInputProcessorProfiles, (void**)&m_pProfiles); + if(SUCCEEDED(hr)) + { + hr = m_pProfiles->QueryInterface(IID_ITfSource, (void**)&m_pSource); + if(SUCCEEDED(hr)) + { + hr = m_pSource->AdviseSink(IID_ITfLanguageProfileNotifySink, + static_cast<ITfLanguageProfileNotifySink *>(this), + &m_dwCookie); + MPT_ASSERT(SUCCEEDED(hr)); + MPT_ASSERT(m_dwCookie != TF_INVALID_COOKIE); + } + } +} + + +TfLanguageProfileNotifySink::~TfLanguageProfileNotifySink() +{ + if(mpt::OS::Windows::IsWine()) + { + // Calling UnadviseSink causes a random crash in Wine when computing its function pointer for some reason. + // Probably a race condition I don't understand, and probably a bug in Wine. + return; + } + if(m_pSource && (m_dwCookie != TF_INVALID_COOKIE)) + { + m_pSource->UnadviseSink(m_dwCookie); + } + m_dwCookie = TF_INVALID_COOKIE; + if(m_pSource) + { + m_pSource->Release(); + } + m_pSource = nullptr; + if(m_pProfiles) m_pProfiles->Release(); + m_pProfiles = nullptr; +} + + +HRESULT TfLanguageProfileNotifySink::OnLanguageChange(LANGID /*langid*/, __RPC__out BOOL *pfAccept) +{ + *pfAccept = TRUE; + return ResultFromScode(S_OK); +} + + +HRESULT TfLanguageProfileNotifySink::OnLanguageChanged() +{ + // Input language has changed, so key positions might have changed too. + CMainFrame *mainFrm = CMainFrame::GetMainFrame(); + if(mainFrm != nullptr) + { + mainFrm->UpdateEffectKeys(mainFrm->GetActiveDoc()); + mainFrm->m_InputHandler->SetModifierMask(ModNone); + } + return ResultFromScode(S_OK); +} + + +HRESULT TfLanguageProfileNotifySink::QueryInterface(REFIID riid, void **ppvObject) +{ + if(riid == IID_ITfLanguageProfileNotifySink || riid == IID_IUnknown) + { + *ppvObject = static_cast<ITfLanguageProfileNotifySink *>(this); + AddRef(); + return ResultFromScode(S_OK); + } + *ppvObject = nullptr; + return ResultFromScode(E_NOINTERFACE); +} + + +ULONG TfLanguageProfileNotifySink::AddRef() +{ + // Don't let COM do anything to this object + return 1; +} + +ULONG TfLanguageProfileNotifySink::Release() +{ + // Don't let COM do anything to this object + return 1; +} + + +///////////////////////////////////////////// +//Misc helper functions +///////////////////////////////////////////// + +void AddPluginNamesToCombobox(CComboBox &CBox, const SNDMIXPLUGIN *plugarray, const bool libraryName, const PLUGINDEX updatePlug) +{ +#ifndef NO_PLUGINS + int insertAt = CBox.GetCount(); + if(updatePlug != PLUGINDEX_INVALID) + { + const int items = insertAt; + for(insertAt = 0; insertAt < items; insertAt++) + { + auto thisPlugin = static_cast<PLUGINDEX>(CBox.GetItemData(insertAt)); + if(thisPlugin == (updatePlug + 1)) + CBox.DeleteString(insertAt); + if(thisPlugin >= (updatePlug + 1)) + break; + } + } + + mpt::tstring str; + str.reserve(80); + for(PLUGINDEX plug = 0; plug < MAX_MIXPLUGINS; plug++) + { + if(updatePlug != PLUGINDEX_INVALID && plug != updatePlug) + continue; + const SNDMIXPLUGIN &plugin = plugarray[plug]; + str.clear(); + str += MPT_TFORMAT("FX{}: ")(plug + 1); + const auto plugName = plugin.GetName(), libName = plugin.GetLibraryName(); + str += mpt::ToWin(plugName); + if(libraryName && plugName != libName && !libName.empty()) + str += _T(" (") + mpt::ToWin(libName) + _T(")"); + else if(plugName.empty() && (!libraryName || libName.empty())) + str += _T("--"); +#ifdef MPT_WITH_VST + auto *vstPlug = dynamic_cast<const CVstPlugin *>(plugin.pMixPlugin); + if(vstPlug != nullptr && vstPlug->isBridged) + { + VSTPluginLib &lib = vstPlug->GetPluginFactory(); + str += MPT_TFORMAT(" ({} Bridged)")(lib.GetDllArchNameUser()); + } +#endif // MPT_WITH_VST + + insertAt = CBox.InsertString(insertAt, str.c_str()); + CBox.SetItemData(insertAt, plug + 1); + insertAt++; + } +#endif // NO_PLUGINS +} + + +void AddPluginParameternamesToCombobox(CComboBox& CBox, SNDMIXPLUGIN& plug) +{ +#ifndef NO_PLUGINS + if(plug.pMixPlugin) + AddPluginParameternamesToCombobox(CBox, *plug.pMixPlugin); +#endif // NO_PLUGINS +} + + +void AddPluginParameternamesToCombobox(CComboBox& CBox, IMixPlugin& plug) +{ +#ifndef NO_PLUGINS + const PlugParamIndex numParams = plug.GetNumParameters(); + plug.CacheParameterNames(0, numParams); + for(PlugParamIndex i = 0; i < numParams; i++) + { + CBox.SetItemData(CBox.AddString(plug.GetFormattedParamName(i)), i); + } +#endif // NO_PLUGINS +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.cpp new file mode 100644 index 00000000..462109b2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.cpp @@ -0,0 +1,1410 @@ +/* + * Mainbar.cpp + * ----------- + * Purpose: Implementation of OpenMPT's window toolbar. + * 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 "View_tre.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "../soundlib/mod_specifications.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////// +// CToolBarEx: custom toolbar base class + +void CToolBarEx::SetHorizontal() +{ + m_bVertical = false; + SetBarStyle(GetBarStyle() | CBRS_ALIGN_TOP); +} + + +void CToolBarEx::SetVertical() +{ + m_bVertical = true; +} + + +CSize CToolBarEx::CalcDynamicLayout(int nLength, DWORD dwMode) +{ + CSize sizeResult; + // if we're committing set the buttons appropriately + if(dwMode & LM_COMMIT) + { + if(dwMode & LM_VERTDOCK) + { + if(!m_bVertical) + SetVertical(); + } else + { + if(m_bVertical) + SetHorizontal(); + } + sizeResult = CToolBar::CalcDynamicLayout(nLength, dwMode); + } else + { + const bool wasVertical = m_bVertical; + const bool doSwitch = (dwMode & LM_HORZ) ? wasVertical : !wasVertical; + + if(doSwitch) + { + if(wasVertical) + SetHorizontal(); + else + SetVertical(); + } + + sizeResult = CToolBar::CalcDynamicLayout(nLength, dwMode); + + if(doSwitch) + { + if(wasVertical) + SetHorizontal(); + else + SetVertical(); + } + } + + return sizeResult; +} + + +BOOL CToolBarEx::EnableControl(CWnd &wnd, UINT nIndex, UINT nHeight) +{ + if(wnd.m_hWnd != NULL) + { + CRect rect; + GetItemRect(nIndex, rect); + if(nHeight) + { + int n = (rect.bottom + rect.top - nHeight) / 2; + if(n > rect.top) rect.top = n; + } + wnd.SetWindowPos(NULL, rect.left, rect.top, 0, 0, SWP_NOZORDER|SWP_NOACTIVATE|SWP_NOSIZE|SWP_NOCOPYBITS); + wnd.ShowWindow(SW_SHOW); + } + return TRUE; +} + + +void CToolBarEx::ChangeCtrlStyle(LONG lStyle, BOOL bSetStyle) +{ + if(m_hWnd) + { + LONG lStyleOld = GetWindowLong(m_hWnd, GWL_STYLE); + if(bSetStyle) + lStyleOld |= lStyle; + else + lStyleOld &= ~lStyle; + SetWindowLong(m_hWnd, GWL_STYLE, lStyleOld); + SetWindowPos(NULL, 0, 0, 0, 0, SWP_NOZORDER | SWP_NOMOVE | SWP_NOSIZE); + Invalidate(); + } +} + + +void CToolBarEx::EnableFlatButtons(BOOL bFlat) +{ + m_bFlatButtons = bFlat ? true : false; + ChangeCtrlStyle(TBSTYLE_FLAT, bFlat); +} + + +///////////////////////////////////////////////////////////////////// +// CMainToolBar + +#define SCALEWIDTH(x) (Util::ScalePixels(x, m_hWnd)) +#define SCALEHEIGHT(x) (Util::ScalePixels(x, m_hWnd)) + +// Play Command +#define PLAYCMD_INDEX 10 +#define TOOLBAR_IMAGE_PAUSE 8 +#define TOOLBAR_IMAGE_PLAY 13 +// Base octave +#define EDITOCTAVE_INDEX 13 +#define EDITOCTAVE_WIDTH SCALEWIDTH(55) +#define EDITOCTAVE_HEIGHT SCALEHEIGHT(20) +#define SPINOCTAVE_INDEX (EDITOCTAVE_INDEX+1) +#define SPINOCTAVE_WIDTH SCALEWIDTH(16) +#define SPINOCTAVE_HEIGHT (EDITOCTAVE_HEIGHT) +// Static "Tempo:" +#define TEMPOTEXT_INDEX 16 +#define TEMPOTEXT_WIDTH SCALEWIDTH(45) +#define TEMPOTEXT_HEIGHT SCALEHEIGHT(20) +// Edit Tempo +#define EDITTEMPO_INDEX (TEMPOTEXT_INDEX+1) +#define EDITTEMPO_WIDTH SCALEWIDTH(48) +#define EDITTEMPO_HEIGHT SCALEHEIGHT(20) +// Spin Tempo +#define SPINTEMPO_INDEX (EDITTEMPO_INDEX+1) +#define SPINTEMPO_WIDTH SCALEWIDTH(16) +#define SPINTEMPO_HEIGHT (EDITTEMPO_HEIGHT) +// Static "Speed:" +#define SPEEDTEXT_INDEX 20 +#define SPEEDTEXT_WIDTH SCALEWIDTH(57) +#define SPEEDTEXT_HEIGHT (TEMPOTEXT_HEIGHT) +// Edit Speed +#define EDITSPEED_INDEX (SPEEDTEXT_INDEX+1) +#define EDITSPEED_WIDTH SCALEWIDTH(28) +#define EDITSPEED_HEIGHT (EDITTEMPO_HEIGHT) +// Spin Speed +#define SPINSPEED_INDEX (EDITSPEED_INDEX+1) +#define SPINSPEED_WIDTH SCALEWIDTH(16) +#define SPINSPEED_HEIGHT (EDITSPEED_HEIGHT) +// Static "Rows/Beat:" +#define RPBTEXT_INDEX 24 +#define RPBTEXT_WIDTH SCALEWIDTH(63) +#define RPBTEXT_HEIGHT (TEMPOTEXT_HEIGHT) +// Edit Speed +#define EDITRPB_INDEX (RPBTEXT_INDEX+1) +#define EDITRPB_WIDTH SCALEWIDTH(28) +#define EDITRPB_HEIGHT (EDITTEMPO_HEIGHT) +// Spin Speed +#define SPINRPB_INDEX (EDITRPB_INDEX+1) +#define SPINRPB_WIDTH SCALEWIDTH(16) +#define SPINRPB_HEIGHT (EDITRPB_HEIGHT) +// VU Meters +#define VUMETER_INDEX (SPINRPB_INDEX+6) +#define VUMETER_WIDTH SCALEWIDTH(255) +#define VUMETER_HEIGHT SCALEHEIGHT(19) + +static UINT MainButtons[] = +{ + // same order as in the bitmap 'mainbar.bmp' + ID_FILE_NEW, + ID_FILE_OPEN, + ID_FILE_SAVE, + ID_SEPARATOR, + ID_EDIT_CUT, + ID_EDIT_COPY, + ID_EDIT_PASTE, + ID_SEPARATOR, + ID_MIDI_RECORD, + ID_PLAYER_STOP, + ID_PLAYER_PAUSE, + ID_PLAYER_PLAYFROMSTART, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_SEPARATOR, + ID_VIEW_OPTIONS, + ID_PANIC, + ID_UPDATE_AVAILABLE, + ID_SEPARATOR, + ID_SEPARATOR, // VU Meter +}; + + +enum { MAX_MIDI_DEVICES = 256 }; + +BEGIN_MESSAGE_MAP(CMainToolBar, CToolBarEx) + ON_WM_VSCROLL() + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &CMainToolBar::OnToolTipText) + ON_NOTIFY_REFLECT(TBN_DROPDOWN, &CMainToolBar::OnTbnDropDownToolBar) + ON_COMMAND_RANGE(ID_SELECT_MIDI_DEVICE, ID_SELECT_MIDI_DEVICE + MAX_MIDI_DEVICES, &CMainToolBar::OnSelectMIDIDevice) +END_MESSAGE_MAP() + + +template<typename TWnd> +static bool CreateTextWnd(TWnd &wnd, const TCHAR *text, DWORD style, CWnd *parent, UINT id) +{ + auto dc = parent->GetDC(); + auto oldFont = dc->SelectObject(CMainFrame::GetGUIFont()); + const auto size = dc->GetTextExtent(text); + dc->SelectObject(oldFont); + parent->ReleaseDC(dc); + CRect rect{0, 0, size.cx + Util::ScalePixels(10, *parent), std::max(static_cast<int>(size.cy) + Util::ScalePixels(4, *parent), Util::ScalePixels(20, *parent))}; + return wnd.Create(text, style, rect, parent, id) != FALSE; +} + +BOOL CMainToolBar::Create(CWnd *parent) +{ + CRect rect; + DWORD dwStyle = WS_CHILD | WS_VISIBLE | CBRS_TOP | CBRS_SIZE_DYNAMIC | CBRS_TOOLTIPS | CBRS_FLYBY; + + if(!CToolBar::Create(parent, dwStyle)) + return FALSE; + + CDC *dc = GetDC(); + const auto hFont = reinterpret_cast<WPARAM>(CMainFrame::GetGUIFont()); + const double scaling = Util::GetDPIx(m_hWnd) / 96.0; + const int imgSize = mpt::saturate_round<int>(16 * scaling), btnSizeX = mpt::saturate_round<int>(23 * scaling), btnSizeY = mpt::saturate_round<int>(22 * scaling); + m_ImageList.Create(IDB_MAINBAR, 16, 16, IMGLIST_NUMIMAGES, 1, dc, scaling, false); + m_ImageListDisabled.Create(IDB_MAINBAR, 16, 16, IMGLIST_NUMIMAGES, 1, dc, scaling, true); + ReleaseDC(dc); + GetToolBarCtrl().SetBitmapSize(CSize(imgSize, imgSize)); + GetToolBarCtrl().SetButtonSize(CSize(btnSizeX, btnSizeY)); + GetToolBarCtrl().SetImageList(&m_ImageList); + GetToolBarCtrl().SetDisabledImageList(&m_ImageListDisabled); + SendMessage(WM_SETFONT, hFont, TRUE); + + if(!SetButtons(MainButtons, mpt::saturate_cast<int>(std::size(MainButtons)))) return FALSE; + + CRect temp; + GetItemRect(0, temp); + SetSizes(CSize(temp.Width(), temp.Height()), CSize(imgSize, imgSize)); + + // Dropdown menus for New and MIDI buttons + LPARAM dwExStyle = GetToolBarCtrl().SendMessage(TB_GETEXTENDEDSTYLE) | TBSTYLE_EX_DRAWDDARROWS; + GetToolBarCtrl().SendMessage(TB_SETEXTENDEDSTYLE, 0, dwExStyle); + SetButtonStyle(CommandToIndex(ID_FILE_NEW), GetButtonStyle(CommandToIndex(ID_FILE_NEW)) | TBSTYLE_DROPDOWN); + SetButtonStyle(CommandToIndex(ID_MIDI_RECORD), GetButtonStyle(CommandToIndex(ID_MIDI_RECORD)) | TBSTYLE_DROPDOWN); + + nCurrentSpeed = 6; + nCurrentTempo.Set(125); + nCurrentRowsPerBeat = 4; + nCurrentOctave = -1; + + // Octave Edit Box + if(!CreateTextWnd(m_EditOctave, _T("Octave 9"), WS_CHILD | WS_BORDER | SS_LEFT | SS_CENTERIMAGE, this, IDC_EDIT_BASEOCTAVE)) return FALSE; + rect.SetRect(0, 0, SPINOCTAVE_WIDTH, SPINOCTAVE_HEIGHT); + m_SpinOctave.Create(WS_CHILD | UDS_ALIGNRIGHT, rect, this, IDC_SPIN_BASEOCTAVE); + + // Tempo Text + if(!CreateTextWnd(m_StaticTempo, _T("Tempo:"), WS_CHILD | SS_CENTER | SS_CENTERIMAGE, this, IDC_TEXT_CURRENTTEMPO)) return FALSE; + // Tempo EditBox + if(!CreateTextWnd(m_EditTempo, _T("999.999"), WS_CHILD | WS_BORDER | SS_LEFT | SS_CENTERIMAGE , this, IDC_EDIT_CURRENTTEMPO)) return FALSE; + // Tempo Spin + rect.SetRect(0, 0, SPINTEMPO_WIDTH, SPINTEMPO_HEIGHT); + m_SpinTempo.Create(WS_CHILD | UDS_ALIGNRIGHT, rect, this, IDC_SPIN_CURRENTTEMPO); + + // Speed Text + if(!CreateTextWnd(m_StaticSpeed, _T("Ticks/Row:"), WS_CHILD | SS_CENTER | SS_CENTERIMAGE, this, IDC_TEXT_CURRENTSPEED)) return FALSE; + // Speed EditBox + if(!CreateTextWnd(m_EditSpeed, _T("999"), WS_CHILD | WS_BORDER | SS_LEFT | SS_CENTERIMAGE , this, IDC_EDIT_CURRENTSPEED)) return FALSE; + // Speed Spin + rect.SetRect(0, 0, SPINSPEED_WIDTH, SPINSPEED_HEIGHT); + m_SpinSpeed.Create(WS_CHILD | UDS_ALIGNRIGHT, rect, this, IDC_SPIN_CURRENTSPEED); + + // Rows per Beat Text + if(!CreateTextWnd(m_StaticRowsPerBeat, _T("Rows/Beat:"), WS_CHILD | SS_CENTER | SS_CENTERIMAGE, this, IDC_TEXT_RPB)) return FALSE; + // Rows per Beat EditBox + if(!CreateTextWnd(m_EditRowsPerBeat, _T("9999"), WS_CHILD | WS_BORDER | SS_LEFT | SS_CENTERIMAGE , this, IDC_EDIT_RPB)) return FALSE; + // Rows per Beat Spin + rect.SetRect(0, 0, SPINRPB_WIDTH, SPINRPB_HEIGHT); + m_SpinRowsPerBeat.Create(WS_CHILD | UDS_ALIGNRIGHT, rect, this, IDC_SPIN_RPB); + + // VU Meter + rect.SetRect(0, 0, VUMETER_WIDTH, VUMETER_HEIGHT); + //m_VuMeter.CreateEx(WS_EX_STATICEDGE, "STATIC", "", WS_CHILD | WS_BORDER | SS_NOTIFY, rect, this, IDC_VUMETER); + m_VuMeter.Create(_T(""), WS_CHILD | WS_BORDER | SS_NOTIFY, rect, this, IDC_VUMETER); + + // Adjust control styles + m_EditOctave.SendMessage(WM_SETFONT, hFont); + m_EditOctave.ModifyStyleEx(0, WS_EX_STATICEDGE, SWP_NOACTIVATE); + m_StaticTempo.SendMessage(WM_SETFONT, hFont); + m_EditTempo.SendMessage(WM_SETFONT, hFont); + m_EditTempo.ModifyStyleEx(0, WS_EX_STATICEDGE, SWP_NOACTIVATE); + m_StaticSpeed.SendMessage(WM_SETFONT, hFont); + m_EditSpeed.SendMessage(WM_SETFONT, hFont); + m_EditSpeed.ModifyStyleEx(0, WS_EX_STATICEDGE, SWP_NOACTIVATE); + m_StaticRowsPerBeat.SendMessage(WM_SETFONT, hFont); + m_EditRowsPerBeat.SendMessage(WM_SETFONT, hFont); + m_EditRowsPerBeat.ModifyStyleEx(0, WS_EX_STATICEDGE, SWP_NOACTIVATE); + m_SpinOctave.SetRange(MIN_BASEOCTAVE, MAX_BASEOCTAVE); + m_SpinOctave.SetPos(4); + m_SpinTempo.SetRange(-1, 1); + m_SpinTempo.SetPos(0); + m_SpinSpeed.SetRange(-1, 1); + m_SpinSpeed.SetPos(0); + m_SpinRowsPerBeat.SetRange(-1, 1); + m_SpinRowsPerBeat.SetPos(0); + // Display everything + SetWindowText(_T("Main")); + SetBaseOctave(4); + SetCurrentSong(nullptr); + EnableDocking(CBRS_ALIGN_ANY); + + GetToolBarCtrl().SetState(ID_UPDATE_AVAILABLE, TBSTATE_HIDDEN); + + return TRUE; +} + + +void CMainToolBar::Init(CMainFrame *pMainFrm) +{ + EnableFlatButtons(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS); + SetHorizontal(); + pMainFrm->DockControlBar(this); +} + + +static int GetWndWidth(const CWnd &wnd) +{ + CRect rect; + wnd.GetClientRect(rect); + return rect.right; +} + + +void CMainToolBar::SetHorizontal() +{ + CToolBarEx::SetHorizontal(); + m_VuMeter.SetOrientation(true); + SetButtonInfo(EDITOCTAVE_INDEX, IDC_EDIT_BASEOCTAVE, TBBS_SEPARATOR, GetWndWidth(m_EditOctave)); + SetButtonInfo(SPINOCTAVE_INDEX, IDC_SPIN_BASEOCTAVE, TBBS_SEPARATOR, SPINOCTAVE_WIDTH); + SetButtonInfo(TEMPOTEXT_INDEX, IDC_TEXT_CURRENTTEMPO, TBBS_SEPARATOR, GetWndWidth(m_StaticTempo)); + SetButtonInfo(EDITTEMPO_INDEX, IDC_EDIT_CURRENTTEMPO, TBBS_SEPARATOR, GetWndWidth(m_EditTempo)); + SetButtonInfo(SPINTEMPO_INDEX, IDC_SPIN_CURRENTTEMPO, TBBS_SEPARATOR, SPINTEMPO_WIDTH); + SetButtonInfo(SPEEDTEXT_INDEX, IDC_TEXT_CURRENTSPEED, TBBS_SEPARATOR, GetWndWidth(m_StaticSpeed)); + SetButtonInfo(EDITSPEED_INDEX, IDC_EDIT_CURRENTSPEED, TBBS_SEPARATOR, GetWndWidth(m_EditSpeed)); + SetButtonInfo(SPINSPEED_INDEX, IDC_SPIN_CURRENTSPEED, TBBS_SEPARATOR, SPINSPEED_WIDTH); + SetButtonInfo(RPBTEXT_INDEX, IDC_TEXT_RPB, TBBS_SEPARATOR, GetWndWidth(m_StaticRowsPerBeat)); + SetButtonInfo(EDITRPB_INDEX, IDC_EDIT_RPB, TBBS_SEPARATOR, GetWndWidth(m_EditRowsPerBeat)); + SetButtonInfo(SPINRPB_INDEX, IDC_SPIN_RPB, TBBS_SEPARATOR, SPINRPB_WIDTH); + SetButtonInfo(VUMETER_INDEX, IDC_VUMETER, TBBS_SEPARATOR, VUMETER_WIDTH); + + //SetButtonInfo(SPINSPEED_INDEX+1, IDC_TEXT_BPM, TBBS_SEPARATOR, SPEEDTEXT_WIDTH); + // Octave Box + EnableControl(m_EditOctave, EDITOCTAVE_INDEX); + EnableControl(m_SpinOctave, SPINOCTAVE_INDEX); + // Tempo + EnableControl(m_StaticTempo, TEMPOTEXT_INDEX, TEMPOTEXT_HEIGHT); + EnableControl(m_EditTempo, EDITTEMPO_INDEX, EDITTEMPO_HEIGHT); + EnableControl(m_SpinTempo, SPINTEMPO_INDEX, SPINTEMPO_HEIGHT); + // Speed + EnableControl(m_StaticSpeed, SPEEDTEXT_INDEX, SPEEDTEXT_HEIGHT); + EnableControl(m_EditSpeed, EDITSPEED_INDEX, EDITSPEED_HEIGHT); + EnableControl(m_SpinSpeed, SPINSPEED_INDEX, SPINSPEED_HEIGHT); + // Rows per Beat + EnableControl(m_StaticRowsPerBeat, RPBTEXT_INDEX, RPBTEXT_HEIGHT); + EnableControl(m_EditRowsPerBeat, EDITRPB_INDEX, EDITRPB_HEIGHT); + EnableControl(m_SpinRowsPerBeat, SPINRPB_INDEX, SPINRPB_HEIGHT); + EnableControl(m_VuMeter, VUMETER_INDEX, VUMETER_HEIGHT); +} + + +void CMainToolBar::SetVertical() +{ + CToolBarEx::SetVertical(); + m_VuMeter.SetOrientation(false); + // Change Buttons + SetButtonInfo(EDITOCTAVE_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(SPINOCTAVE_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(TEMPOTEXT_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(EDITTEMPO_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(SPINTEMPO_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(SPEEDTEXT_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(EDITSPEED_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(SPINSPEED_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(RPBTEXT_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(EDITRPB_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(SPINRPB_INDEX, ID_SEPARATOR, TBBS_SEPARATOR, 1); + SetButtonInfo(VUMETER_INDEX, IDC_VUMETER, TBBS_SEPARATOR, VUMETER_HEIGHT); + + // Hide Controls + if(m_EditOctave.m_hWnd) m_EditOctave.ShowWindow(SW_HIDE); + if(m_SpinOctave.m_hWnd) m_SpinOctave.ShowWindow(SW_HIDE); + if(m_StaticTempo.m_hWnd) m_StaticTempo.ShowWindow(SW_HIDE); + if(m_EditTempo.m_hWnd) m_EditTempo.ShowWindow(SW_HIDE); + if(m_SpinTempo.m_hWnd) m_SpinTempo.ShowWindow(SW_HIDE); + if(m_StaticSpeed.m_hWnd) m_StaticSpeed.ShowWindow(SW_HIDE); + if(m_EditSpeed.m_hWnd) m_EditSpeed.ShowWindow(SW_HIDE); + if(m_SpinSpeed.m_hWnd) m_SpinSpeed.ShowWindow(SW_HIDE); + if(m_StaticRowsPerBeat.m_hWnd) m_StaticRowsPerBeat.ShowWindow(SW_HIDE); + if(m_EditRowsPerBeat.m_hWnd) m_EditRowsPerBeat.ShowWindow(SW_HIDE); + if(m_SpinRowsPerBeat.m_hWnd) m_SpinRowsPerBeat.ShowWindow(SW_HIDE); + EnableControl(m_VuMeter, VUMETER_INDEX, VUMETER_HEIGHT); + //if(m_StaticBPM.m_hWnd) m_StaticBPM.ShowWindow(SW_HIDE); +} + + +UINT CMainToolBar::GetBaseOctave() const +{ + if(nCurrentOctave >= MIN_BASEOCTAVE) return (UINT)nCurrentOctave; + return 4; +} + + +BOOL CMainToolBar::SetBaseOctave(UINT nOctave) +{ + TCHAR s[64]; + + if((nOctave < MIN_BASEOCTAVE) || (nOctave > MAX_BASEOCTAVE)) return FALSE; + if(nOctave != (UINT)nCurrentOctave) + { + nCurrentOctave = nOctave; + wsprintf(s, _T(" Octave %d"), nOctave); + m_EditOctave.SetWindowText(s); + m_SpinOctave.SetPos(nOctave); + } + return TRUE; +} + + +bool CMainToolBar::ShowUpdateInfo(const CString &newVersion, const CString &infoURL, bool showHighLight) +{ + GetToolBarCtrl().SetState(ID_UPDATE_AVAILABLE, TBSTATE_ENABLED); + if(m_bVertical) + SetVertical(); + else + SetHorizontal(); + + CRect rect; + GetToolBarCtrl().GetRect(ID_UPDATE_AVAILABLE, &rect); + CPoint pt = rect.CenterPoint(); + ClientToScreen(&pt); + CMainFrame::GetMainFrame()->GetWindowRect(rect); + LimitMax(pt.x, rect.right); + + if(showHighLight) + { + return m_tooltip.ShowUpdate(*this, newVersion, infoURL, rect, pt, ID_UPDATE_AVAILABLE); + } else + { + return true; + } +} + + +void CMainToolBar::RemoveUpdateInfo() +{ + if(m_tooltip) + m_tooltip.Pop(); + GetToolBarCtrl().SetState(ID_UPDATE_AVAILABLE, TBSTATE_HIDDEN); +} + + +BOOL CMainToolBar::SetCurrentSong(CSoundFile *pSndFile) +{ + static CSoundFile *sndFile = nullptr; + if(pSndFile != sndFile) + { + sndFile = pSndFile; + } + + // Update Info + if(pSndFile) + { + TCHAR s[32]; + // Update play/pause button + if(nCurrentTempo == TEMPO(0, 0)) SetButtonInfo(PLAYCMD_INDEX, ID_PLAYER_PAUSE, TBBS_BUTTON, TOOLBAR_IMAGE_PAUSE); + // Update Speed + int nSpeed = pSndFile->m_PlayState.m_nMusicSpeed; + if(nSpeed != nCurrentSpeed) + { + CModDoc *modDoc = pSndFile->GetpModDoc(); + if(modDoc != nullptr) + { + // Update envelope views if speed has changed + modDoc->UpdateAllViews(InstrumentHint().Envelope()); + } + + if(nCurrentSpeed < 0) m_SpinSpeed.EnableWindow(TRUE); + nCurrentSpeed = nSpeed; + wsprintf(s, _T("%u"), static_cast<unsigned int>(nCurrentSpeed)); + m_EditSpeed.SetWindowText(s); + } + TEMPO nTempo = pSndFile->m_PlayState.m_nMusicTempo; + if(nTempo != nCurrentTempo) + { + if(nCurrentTempo <= TEMPO(0, 0)) m_SpinTempo.EnableWindow(TRUE); + nCurrentTempo = nTempo; + if(nCurrentTempo.GetFract() == 0) + _stprintf(s, _T("%u"), nCurrentTempo.GetInt()); + else + _stprintf(s, _T("%.4f"), nCurrentTempo.ToDouble()); + m_EditTempo.SetWindowText(s); + } + int nRowsPerBeat = pSndFile->m_PlayState.m_nCurrentRowsPerBeat; + if(nRowsPerBeat != nCurrentRowsPerBeat) + { + if(nCurrentRowsPerBeat < 0) m_SpinRowsPerBeat.EnableWindow(TRUE); + nCurrentRowsPerBeat = nRowsPerBeat; + wsprintf(s, _T("%u"), static_cast<unsigned int>(nCurrentRowsPerBeat)); + m_EditRowsPerBeat.SetWindowText(s); + } + } else + { + if(nCurrentTempo > TEMPO(0, 0)) + { + nCurrentTempo.Set(0); + m_EditTempo.SetWindowText(_T("---")); + m_SpinTempo.EnableWindow(FALSE); + SetButtonInfo(PLAYCMD_INDEX, ID_PLAYER_PLAY, TBBS_BUTTON, TOOLBAR_IMAGE_PLAY); + } + if(nCurrentSpeed != -1) + { + nCurrentSpeed = -1; + m_EditSpeed.SetWindowText(_T("---")); + m_SpinSpeed.EnableWindow(FALSE); + } + if(nCurrentRowsPerBeat != -1) + { + nCurrentRowsPerBeat = -1; + m_EditRowsPerBeat.SetWindowText(_T("---")); + m_SpinRowsPerBeat.EnableWindow(FALSE); + } + } + return TRUE; +} + + +void CMainToolBar::OnVScroll(UINT nCode, UINT nPos, CScrollBar *pScrollBar) +{ + CMainFrame *pMainFrm; + + CToolBarEx::OnVScroll(nCode, nPos, pScrollBar); + short int oct = (short int)m_SpinOctave.GetPos(); + if((oct >= MIN_BASEOCTAVE) && ((int)oct != nCurrentOctave)) + { + SetBaseOctave(oct); + } + if((nCurrentSpeed < 0) || (nCurrentTempo <= TEMPO(0, 0))) return; + if((pMainFrm = CMainFrame::GetMainFrame()) != nullptr) + { + CSoundFile *pSndFile = pMainFrm->GetSoundFilePlaying(); + if(pSndFile) + { + const auto &specs = pSndFile->GetModSpecifications(); + int n; + if((n = mpt::signum(m_SpinTempo.GetPos32())) != 0) + { + TEMPO newTempo; + if(specs.hasFractionalTempo) + { + n *= TEMPO::fractFact; + if(CMainFrame::GetMainFrame()->GetInputHandler()->CtrlPressed()) + n /= 100; + else + n /= 10; + newTempo.SetRaw(n); + } else + { + newTempo = TEMPO(n, 0); + } + newTempo += nCurrentTempo; + pSndFile->SetTempo(Clamp(newTempo, specs.GetTempoMin(), specs.GetTempoMax()), true); + m_SpinTempo.SetPos(0); + } + if((n = mpt::signum(m_SpinSpeed.GetPos32())) != 0) + { + pSndFile->m_PlayState.m_nMusicSpeed = Clamp(uint32(nCurrentSpeed + n), specs.speedMin, specs.speedMax); + m_SpinSpeed.SetPos(0); + } + if((n = m_SpinRowsPerBeat.GetPos32()) != 0) + { + if(n < 0) + { + if(nCurrentRowsPerBeat > 1) + SetRowsPerBeat(nCurrentRowsPerBeat - 1); + } else if(static_cast<ROWINDEX>(nCurrentRowsPerBeat) < pSndFile->m_PlayState.m_nCurrentRowsPerMeasure) + { + SetRowsPerBeat(nCurrentRowsPerBeat + 1); + } + m_SpinRowsPerBeat.SetPos(0); + + // Update pattern editor + pMainFrm->PostMessage(WM_MOD_INVALIDATEPATTERNS, HINT_MPTOPTIONS); + } + + SetCurrentSong(pSndFile); + } + } +} + + +void CMainToolBar::OnTbnDropDownToolBar(NMHDR *pNMHDR, LRESULT *pResult) +{ + NMTOOLBAR *pToolBar = reinterpret_cast<NMTOOLBAR *>(pNMHDR); + ClientToScreen(&(pToolBar->rcButton)); + + switch(pToolBar->iItem) + { + case ID_FILE_NEW: + CMainFrame::GetMainFrame()->GetFileMenu()->GetSubMenu(0)->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pToolBar->rcButton.left, pToolBar->rcButton.bottom, this); + break; + case ID_MIDI_RECORD: + // Show a list of MIDI devices + { + HMENU hMenu = ::CreatePopupMenu(); + MIDIINCAPS mic; + UINT numDevs = midiInGetNumDevs(); + if(numDevs > MAX_MIDI_DEVICES) numDevs = MAX_MIDI_DEVICES; + UINT current = TrackerSettings::Instance().GetCurrentMIDIDevice(); + for(UINT i = 0; i < numDevs; i++) + { + mic.szPname[0] = 0; + if(midiInGetDevCaps(i, &mic, sizeof(mic)) == MMSYSERR_NOERROR) + { + ::AppendMenu(hMenu, MF_STRING | (i == current ? MF_CHECKED : 0), ID_SELECT_MIDI_DEVICE + i, theApp.GetFriendlyMIDIPortName(mpt::String::ReadCStringBuf(mic.szPname), true)); + } + } + if(!numDevs) + { + ::AppendMenu(hMenu, MF_STRING | MF_GRAYED, 0, _T("No MIDI input devices found")); + } + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pToolBar->rcButton.left, pToolBar->rcButton.bottom, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + } + break; + } + + *pResult = 0; +} + + +void CMainToolBar::OnSelectMIDIDevice(UINT id) +{ + CMainFrame::GetMainFrame()->midiCloseDevice(); + TrackerSettings::Instance().SetMIDIDevice(id - ID_SELECT_MIDI_DEVICE); + CMainFrame::GetMainFrame()->midiOpenDevice(); +} + + +void CMainToolBar::SetRowsPerBeat(ROWINDEX nNewRPB) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm == nullptr) + return; + CModDoc *pModDoc = pMainFrm->GetModPlaying(); + CSoundFile *pSndFile = pMainFrm->GetSoundFilePlaying(); + if(pModDoc == nullptr || pSndFile == nullptr) + return; + + pSndFile->m_PlayState.m_nCurrentRowsPerBeat = nNewRPB; + PATTERNINDEX nPat = pSndFile->GetCurrentPattern(); + if(pSndFile->Patterns[nPat].GetOverrideSignature()) + { + if(nNewRPB <= pSndFile->Patterns[nPat].GetRowsPerMeasure()) + { + pSndFile->Patterns[nPat].SetSignature(nNewRPB, pSndFile->Patterns[nPat].GetRowsPerMeasure()); + TempoSwing swing = pSndFile->Patterns[nPat].GetTempoSwing(); + if(!swing.empty()) + { + swing.resize(nNewRPB); + pSndFile->Patterns[nPat].SetTempoSwing(swing); + } + pModDoc->SetModified(); + } + } else + { + if(nNewRPB <= pSndFile->m_nDefaultRowsPerMeasure) + { + pSndFile->m_nDefaultRowsPerBeat = nNewRPB; + if(!pSndFile->m_tempoSwing.empty()) pSndFile->m_tempoSwing.resize(nNewRPB); + pModDoc->SetModified(); + } + } +} + + +BOOL CMainToolBar::OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult) +{ + auto pTTT = reinterpret_cast<TOOLTIPTEXT *>(pNMHDR); + UINT_PTR id = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + id = (UINT_PTR)::GetDlgCtrlID((HWND)id); + } + + const TCHAR *s = nullptr; + CommandID cmd = kcNull; + switch(id) + { + case ID_FILE_NEW: s = _T("New"); cmd = kcFileNew; break; + case ID_FILE_OPEN: s = _T("Open"); cmd = kcFileOpen; break; + case ID_FILE_SAVE: s = _T("Save"); cmd = kcFileSave; break; + case ID_EDIT_CUT: s = _T("Cut"); cmd = kcEditCut; break; + case ID_EDIT_COPY: s = _T("Copy"); cmd = kcEditCopy; break; + case ID_EDIT_PASTE: s = _T("Paste"); cmd = kcEditPaste; break; + case ID_MIDI_RECORD: s = _T("MIDI Record"); cmd = kcMidiRecord; break; + case ID_PLAYER_STOP: s = _T("Stop"); cmd = kcStopSong; break; + case ID_PLAYER_PLAY: s = _T("Play"); cmd = kcPlayPauseSong; break; + case ID_PLAYER_PAUSE: s = _T("Pause"); cmd = kcPlayPauseSong; break; + case ID_PLAYER_PLAYFROMSTART: s = _T("Play From Start"); cmd = kcPlaySongFromStart; break; + case ID_VIEW_OPTIONS: s = _T("Setup"); cmd = kcViewOptions; break; + case ID_PANIC: s = _T("Stop all hanging plugin and sample voices"); cmd = kcPanic; break; + case ID_UPDATE_AVAILABLE: s = _T("A new update is available."); break; + } + + if(s == nullptr) + return FALSE; + + mpt::tstring fmt = s; + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if(!keyText.IsEmpty()) + fmt += MPT_TFORMAT(" ({})")(keyText); + } + mpt::String::WriteWinBuf(pTTT->szText) = fmt; + *pResult = 0; + + // bring the tooltip window above other popup windows + ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, + SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOOWNERZORDER); + + return TRUE; // message was handled +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// CModTreeBar + +BEGIN_MESSAGE_MAP(CModTreeBar, CDialogBar) + //{{AFX_MSG_MAP(CModTreeBar) + ON_WM_NCCALCSIZE() + ON_WM_NCPAINT() + ON_WM_NCHITTEST() + ON_WM_SIZE() + ON_WM_NCMOUSEMOVE() + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_NCLBUTTONDOWN() + ON_WM_LBUTTONUP() + ON_WM_NCLBUTTONUP() + ON_MESSAGE(WM_INITDIALOG, &CModTreeBar::OnInitDialog) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +CModTreeBar::CModTreeBar() +{ + m_nTreeSplitRatio = TrackerSettings::Instance().glTreeSplitRatio; +} + + +LRESULT CModTreeBar::OnInitDialog(WPARAM wParam, LPARAM lParam) +{ + LRESULT l = CDialogBar::HandleInitDialog(wParam, lParam); + m_pModTreeData = new CModTree(nullptr); + if(m_pModTreeData) m_pModTreeData->SubclassDlgItem(IDC_TREEDATA, this); + m_pModTree = new CModTree(m_pModTreeData); + if(m_pModTree) m_pModTree->SubclassDlgItem(IDC_TREEVIEW, this); + m_dwStatus = 0; + m_sizeDefault.cx = Util::ScalePixels(TrackerSettings::Instance().glTreeWindowWidth, m_hWnd) + 3; + m_sizeDefault.cy = 32767; + return l; +} + + +CModTreeBar::~CModTreeBar() +{ + if(m_pModTree) + { + delete m_pModTree; + m_pModTree = nullptr; + } + if(m_pModTreeData) + { + delete m_pModTreeData; + m_pModTreeData = nullptr; + } +} + + +void CModTreeBar::Init() +{ + m_nTreeSplitRatio = TrackerSettings::Instance().glTreeSplitRatio; + if(m_pModTree) + { + m_pModTreeData->Init(); + m_pModTree->Init(); + } +} + + +void CModTreeBar::RefreshDlsBanks() +{ + if(m_pModTree) m_pModTree->RefreshDlsBanks(); +} + + +void CModTreeBar::RefreshMidiLibrary() +{ + if(m_pModTree) m_pModTree->RefreshMidiLibrary(); +} + + +void CModTreeBar::OnOptionsChanged() +{ + if(m_pModTree) m_pModTree->OnOptionsChanged(); +} + + +void CModTreeBar::RecalcLayout() +{ + CRect rect; + + if((m_pModTree) && (m_pModTreeData)) + { + int cytree, cydata, cyavail; + + GetClientRect(&rect); + cyavail = rect.Height() - 3; + if(cyavail < 0) cyavail = 0; + cytree = (cyavail * m_nTreeSplitRatio) >> 8; + cydata = cyavail - cytree; + m_pModTree->SetWindowPos(NULL, 0,0, rect.Width(), cytree, SWP_NOZORDER|SWP_NOACTIVATE); + m_pModTreeData->SetWindowPos(NULL, 0,cytree+3, rect.Width(), cydata, SWP_NOZORDER|SWP_NOACTIVATE); + } +} + + +CSize CModTreeBar::CalcFixedLayout(BOOL, BOOL) +{ + int width = Util::ScalePixels(TrackerSettings::Instance().glTreeWindowWidth, m_hWnd); + CSize sz; + m_sizeDefault.cx = width; + m_sizeDefault.cy = 32767; + sz.cx = width + 3; + if(sz.cx < 4) sz.cx = 4; + sz.cy = 32767; + return sz; +} + + +void CModTreeBar::DoMouseMove(CPoint pt) +{ + CRect rect; + + if((m_dwStatus & (MTB_CAPTURE|MTB_DRAGGING)) && (::GetCapture() != m_hWnd)) + { + CancelTracking(); + } + if(m_dwStatus & MTB_DRAGGING) + { + if(m_dwStatus & MTB_VERTICAL) + { + if(m_pModTree) + { + m_pModTree->GetWindowRect(&rect); + pt.y += rect.Height(); + } + GetClientRect(&rect); + pt.y -= ptDragging.y; + if(pt.y < 0) pt.y = 0; + if(pt.y > rect.Height()) pt.y = rect.Height(); + if((!(m_dwStatus & MTB_TRACKER)) || (pt.y != (int)m_nTrackPos)) + { + if(m_dwStatus & MTB_TRACKER) OnInvertTracker(m_nTrackPos); + m_nTrackPos = pt.y; + OnInvertTracker(m_nTrackPos); + m_dwStatus |= MTB_TRACKER; + } + } else + { + pt.x -= ptDragging.x - m_cxOriginal + 3; + if(pt.x < 0) pt.x = 0; + if((!(m_dwStatus & MTB_TRACKER)) || (pt.x != (int)m_nTrackPos)) + { + if(m_dwStatus & MTB_TRACKER) OnInvertTracker(m_nTrackPos); + m_nTrackPos = pt.x; + OnInvertTracker(m_nTrackPos); + m_dwStatus |= MTB_TRACKER; + } + } + } else + { + UINT nCursor = 0; + + GetClientRect(&rect); + rect.left = rect.right - 2; + rect.right = rect.left + 5; + if(rect.PtInRect(pt)) + { + nCursor = AFX_IDC_HSPLITBAR; + } else + if(m_pModTree) + { + m_pModTree->GetWindowRect(&rect); + rect.right = rect.Width(); + rect.left = 0; + rect.top = rect.Height()-1; + rect.bottom = rect.top + 5; + if(rect.PtInRect(pt)) + { + nCursor = AFX_IDC_VSPLITBAR; + } + } + if(nCursor) + { + UINT nDir = (nCursor == AFX_IDC_VSPLITBAR) ? MTB_VERTICAL : 0; + BOOL bLoad = FALSE; + if(!(m_dwStatus & MTB_CAPTURE)) + { + m_dwStatus |= MTB_CAPTURE; + SetCapture(); + bLoad = TRUE; + } else + { + if(nDir != (m_dwStatus & MTB_VERTICAL)) bLoad = TRUE; + } + m_dwStatus &= ~MTB_VERTICAL; + m_dwStatus |= nDir; + if(bLoad) SetCursor(theApp.LoadCursor(nCursor)); + } else + { + if(m_dwStatus & MTB_CAPTURE) + { + m_dwStatus &= ~MTB_CAPTURE; + ReleaseCapture(); + SetCursor(LoadCursor(NULL, IDC_ARROW)); + } + } + } +} + + +void CModTreeBar::DoLButtonDown(CPoint pt) +{ + if((m_dwStatus & MTB_CAPTURE) && (!(m_dwStatus & MTB_DRAGGING))) + { + CRect rect; + GetWindowRect(&rect); + m_cxOriginal = rect.Width(); + m_cyOriginal = rect.Height(); + ptDragging = pt; + m_dwStatus |= MTB_DRAGGING; + DoMouseMove(pt); + } +} + + +void CModTreeBar::DoLButtonUp() +{ + if(m_dwStatus & MTB_DRAGGING) + { + CRect rect; + + m_dwStatus &= ~MTB_DRAGGING; + if(m_dwStatus & MTB_TRACKER) + { + OnInvertTracker(m_nTrackPos); + m_dwStatus &= ~MTB_TRACKER; + } + if(m_dwStatus & MTB_VERTICAL) + { + GetClientRect(&rect); + int cyavail = rect.Height() - 3; + if(cyavail < 4) cyavail = 4; + int ratio = (m_nTrackPos << 8) / cyavail; + if(ratio < 0) ratio = 0; + if(ratio > 256) ratio = 256; + m_nTreeSplitRatio = ratio; + TrackerSettings::Instance().glTreeSplitRatio = ratio; + RecalcLayout(); + } else + { + GetWindowRect(&rect); + m_nTrackPos += 3; + if(m_nTrackPos < 4) m_nTrackPos = 4; + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if((m_nTrackPos != (UINT)rect.Width()) && (pMainFrm)) + { + TrackerSettings::Instance().glTreeWindowWidth = Util::ScalePixelsInv(m_nTrackPos - 3, m_hWnd); + m_sizeDefault.cx = m_nTrackPos; + m_sizeDefault.cy = 32767; + pMainFrm->RecalcLayout(); + } + } + } +} + + +void CModTreeBar::CancelTracking() +{ + if(m_dwStatus & MTB_TRACKER) + { + OnInvertTracker(m_nTrackPos); + m_dwStatus &= ~MTB_TRACKER; + } + m_dwStatus &= ~MTB_DRAGGING; + if(m_dwStatus & MTB_CAPTURE) + { + m_dwStatus &= ~MTB_CAPTURE; + ReleaseCapture(); + } +} + + +void CModTreeBar::OnInvertTracker(UINT x) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + + if(pMainFrm) + { + CRect rect; + + GetClientRect(&rect); + if(m_dwStatus & MTB_VERTICAL) + { + rect.top = x; + rect.bottom = rect.top + 4; + } else + { + rect.left = x; + rect.right = rect.left + 4; + } + ClientToScreen(&rect); + pMainFrm->ScreenToClient(&rect); + + // pat-blt without clip children on + CDC* pDC = pMainFrm->GetDC(); + // invert the brush pattern (looks just like frame window sizing) + CBrush* pBrush = CDC::GetHalftoneBrush(); + HBRUSH hOldBrush = NULL; + if(pBrush != NULL) + hOldBrush = (HBRUSH)SelectObject(pDC->m_hDC, pBrush->m_hObject); + pDC->PatBlt(rect.left, rect.top, rect.Width(), rect.Height(), PATINVERT); + if(hOldBrush != NULL) + SelectObject(pDC->m_hDC, hOldBrush); + ReleaseDC(pDC); + } +} + + +void CModTreeBar::OnDocumentCreated(CModDoc *pModDoc) +{ + if(m_pModTree && pModDoc) m_pModTree->AddDocument(*pModDoc); +} + + +void CModTreeBar::OnDocumentClosed(CModDoc *pModDoc) +{ + if(m_pModTree && pModDoc) m_pModTree->RemoveDocument(*pModDoc); +} + + +void CModTreeBar::OnUpdate(CModDoc *pModDoc, UpdateHint hint, CObject *pHint) +{ + if(m_pModTree) m_pModTree->OnUpdate(pModDoc, hint, pHint); +} + + +void CModTreeBar::UpdatePlayPos(CModDoc *pModDoc, Notification *pNotify) +{ + if(m_pModTree && pModDoc) m_pModTree->UpdatePlayPos(*pModDoc, pNotify); +} + + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// CModTreeBar message handlers + +void CModTreeBar::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp) +{ + CDialogBar::OnNcCalcSize(bCalcValidRects, lpncsp); + if(lpncsp) + { + lpncsp->rgrc[0].right -= 3; + if(lpncsp->rgrc[0].right < lpncsp->rgrc[0].left) lpncsp->rgrc[0].right = lpncsp->rgrc[0].left; + } +} + + +LRESULT CModTreeBar::OnNcHitTest(CPoint point) +{ + CRect rect; + + GetWindowRect(&rect); + rect.DeflateRect(1,1); + rect.right -= 3; + if(!rect.PtInRect(point)) return HTBORDER; + return CDialogBar::OnNcHitTest(point); +} + + +void CModTreeBar::OnNcPaint() +{ + RECT rect; + CDialogBar::OnNcPaint(); + + GetWindowRect(&rect); + // Assumes there is no other non-client items + rect.right -= rect.left; + rect.bottom -= rect.top; + rect.top = 0; + rect.left = rect.right - 3; + if((rect.left < rect.right) && (rect.top < rect.bottom)) + { + CDC *pDC = GetWindowDC(); + HDC hdc = pDC->m_hDC; + FillRect(hdc, &rect, GetSysColorBrush(COLOR_BTNFACE)); + ReleaseDC(pDC); + } +} + + +void CModTreeBar::OnSize(UINT nType, int cx, int cy) +{ + CDialogBar::OnSize(nType, cx, cy); + RecalcLayout(); +} + + +void CModTreeBar::OnNcMouseMove(UINT, CPoint point) +{ + CRect rect; + CPoint pt = point; + + GetWindowRect(&rect); + pt.x -= rect.left; + pt.y -= rect.top; + DoMouseMove(pt); +} + + +void CModTreeBar::OnMouseMove(UINT, CPoint point) +{ + DoMouseMove(point); +} + + +void CModTreeBar::OnNcLButtonDown(UINT, CPoint point) +{ + CRect rect; + CPoint pt = point; + + GetWindowRect(&rect); + pt.x -= rect.left; + pt.y -= rect.top; + DoLButtonDown(pt); +} + + +void CModTreeBar::OnLButtonDown(UINT, CPoint point) +{ + DoLButtonDown(point); +} + + +void CModTreeBar::OnNcLButtonUp(UINT, CPoint) +{ + DoLButtonUp(); +} + + +void CModTreeBar::OnLButtonUp(UINT, CPoint) +{ + DoLButtonUp(); +} + + +HWND CModTreeBar::GetModTreeHWND() +{ + return m_pModTree->m_hWnd; +} + + +LRESULT CModTreeBar::SendMessageToModTree(UINT cmdID, WPARAM wParam, LPARAM lParam) +{ + if(::GetFocus() == m_pModTree->m_hWnd) + return m_pModTree->SendMessage(cmdID, wParam, lParam); + if(::GetFocus() == m_pModTreeData->m_hWnd) + return m_pModTreeData->SendMessage(cmdID, wParam, lParam); + return 0; +} + + +bool CModTreeBar::SetTreeSoundfile(FileReader &file) +{ + return m_pModTree->SetSoundFile(file); +} + + + + +//////////////////////////////////////////////////////////////////////////////// +// +// Stereo VU Meter for toolbar +// + +BEGIN_MESSAGE_MAP(CStereoVU, CStatic) + ON_WM_PAINT() + ON_WM_LBUTTONDOWN() +END_MESSAGE_MAP() + + +void CStereoVU::OnPaint() +{ + CRect rect; + CPaintDC dc(this); + DrawVuMeters(dc, true); +} + + +void CStereoVU::SetVuMeter(uint8 validChannels, const uint32 channels[4], bool force) +{ + bool changed = false; + if(validChannels == 0) + { + // reset + validChannels = numChannels; + } else if(validChannels != numChannels) + { + changed = true; + force = true; + numChannels = validChannels; + allowRightToLeft = (numChannels > 2); + } + for(uint8 c = 0; c < validChannels; ++c) + { + if(vuMeter[c] != channels[c]) + { + changed = true; + } + } + if(changed) + { + DWORD curTime = timeGetTime(); + if(curTime - lastVuUpdateTime >= TrackerSettings::Instance().VuMeterUpdateInterval || force) + { + for(uint8 c = 0; c < validChannels; ++c) + { + vuMeter[c] = channels[c]; + } + CClientDC dc(this); + DrawVuMeters(dc, force); + lastVuUpdateTime = curTime; + } + } +} + + +// Draw stereo VU +void CStereoVU::DrawVuMeters(CDC &dc, bool redraw) +{ + CRect rect; + GetClientRect(&rect); + + if(redraw) + { + dc.FillSolidRect(rect.left, rect.top, rect.Width(), rect.Height(), RGB(0,0,0)); + } + + for(uint8 channel = 0; channel < numChannels; ++channel) + { + CRect chanrect = rect; + if(horizontal) + { + if(allowRightToLeft) + { + const int col = channel % 2; + const int row = channel / 2; + + float width = (rect.Width() - 2.0f) / 2.0f; + float height = rect.Height() / float(numChannels/2); + + chanrect.top = mpt::saturate_round<int32>(rect.top + height * row); + chanrect.bottom = mpt::saturate_round<int32>(chanrect.top + height) - 1; + + chanrect.left = mpt::saturate_round<int32>(rect.left + width * col) + ((col == 1) ? 2 : 0); + chanrect.right = mpt::saturate_round<int32>(chanrect.left + width) - 1; + + } else + { + float height = rect.Height() / float(numChannels); + chanrect.top = mpt::saturate_round<int32>(rect.top + height * channel); + chanrect.bottom = mpt::saturate_round<int32>(chanrect.top + height) - 1; + } + } else + { + float width = rect.Width() / float(numChannels); + chanrect.left = mpt::saturate_round<int32>(rect.left + width * channel); + chanrect.right = mpt::saturate_round<int32>(chanrect.left + width) - 1; + } + DrawVuMeter(dc, chanrect, channel, redraw); + } + +} + + +// Draw a single VU Meter +void CStereoVU::DrawVuMeter(CDC &dc, const CRect &rect, int index, bool redraw) +{ + uint32 vu = vuMeter[index]; + + if(CMainFrame::GetMainFrame()->GetSoundFilePlaying() == nullptr) + { + vu = 0; + } + + const bool clip = (vu & Notification::ClipVU) != 0; + vu = (vu & (~Notification::ClipVU)) >> 8; + + if(horizontal) + { + const bool rtl = allowRightToLeft && ((index % 2) == 0); + + const int cx = std::max(1, rect.Width()); + int v = (vu * cx) >> 8; + + for(int x = 0; x <= cx; x += 2) + { + int pen = Clamp((x * NUM_VUMETER_PENS) / cx, 0, NUM_VUMETER_PENS - 1); + const bool last = (x == (cx & ~0x1)); + + // Darken everything above volume, unless it's the clip indicator + if(v <= x && (!last || !clip)) + pen += NUM_VUMETER_PENS; + + bool draw = redraw || (v < lastV[index] && v<=x && x<=lastV[index]) || (lastV[index] < v && lastV[index]<=x && x<=v); + draw = draw || (last && clip != lastClip[index]); + if(draw) dc.FillSolidRect( + ((!rtl) ? (rect.left + x) : (rect.right - x)), + rect.top, 1, rect.Height(), CMainFrame::gcolrefVuMeter[pen]); + if(last) lastClip[index] = clip; + } + lastV[index] = v; + } else + { + const int cy = std::max(1, rect.Height()); + int v = (vu * cy) >> 8; + + for(int ry = rect.bottom - 1; ry > rect.top; ry -= 2) + { + const int y0 = rect.bottom - ry; + int pen = Clamp((y0 * NUM_VUMETER_PENS) / cy, 0, NUM_VUMETER_PENS - 1); + const bool last = (ry == rect.top + 1); + + // Darken everything above volume, unless it's the clip indicator + if(v <= y0 && (!last || !clip)) + pen += NUM_VUMETER_PENS; + + bool draw = redraw || (v < lastV[index] && v<=ry && ry<=lastV[index]) || (lastV[index] < v && lastV[index]<=ry && ry<=v); + draw = draw || (last && clip != lastClip[index]); + if(draw) dc.FillSolidRect(rect.left, ry, rect.Width(), 1, CMainFrame::gcolrefVuMeter[pen]); + if(last) lastClip[index] = clip; + } + lastV[index] = v; + } +} + + +void CStereoVU::OnLButtonDown(UINT, CPoint) +{ + // Reset clip indicator. + CMainFrame::GetMainFrame()->m_VUMeterInput.ResetClipped(); + CMainFrame::GetMainFrame()->m_VUMeterOutput.ResetClipped(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.h b/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.h new file mode 100644 index 00000000..65cb9188 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mainbar.h @@ -0,0 +1,197 @@ +/* + * Mainbar.h + * --------- + * Purpose: Implementation of OpenMPT's window toolbar. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "UpdateToolTip.h" + +OPENMPT_NAMESPACE_BEGIN + +class CStereoVU: public CStatic +{ +protected: + uint8 numChannels; + uint32 vuMeter[4]; + DWORD lastVuUpdateTime; + int lastV[4]; + bool lastClip[4]; + bool horizontal; + bool allowRightToLeft; + +public: + CStereoVU() { numChannels = 2; MemsetZero(vuMeter); lastVuUpdateTime = timeGetTime(); horizontal = true; MemsetZero(lastV); MemsetZero(lastClip); allowRightToLeft = false; } + void SetVuMeter(uint8 validChannels, const uint32 channels[4], bool force=false); + void SetOrientation(bool h) { horizontal = h; } + +protected: + void DrawVuMeters(CDC &dc, bool redraw=false); + void DrawVuMeter(CDC &dc, const CRect &rect, int index, bool redraw=false); + +protected: + afx_msg void OnPaint(); + afx_msg void OnLButtonDown(UINT, CPoint); + DECLARE_MESSAGE_MAP(); +}; + +#define MIN_BASEOCTAVE 0 +#define MAX_BASEOCTAVE 8 + +class CSoundFile; +class CModDoc; +class CModTree; +class CMainFrame; + +class CToolBarEx: public CToolBar +{ +protected: + bool m_bVertical = false, m_bFlatButtons = false; + +public: + CToolBarEx() {} + ~CToolBarEx() override {} + +public: + BOOL EnableControl(CWnd &wnd, UINT nIndex, UINT nHeight=0); + void ChangeCtrlStyle(LONG lStyle, BOOL bSetStyle); + void EnableFlatButtons(BOOL bFlat); + +public: + //{{AFX_VIRTUAL(CToolBarEx) + CSize CalcDynamicLayout(int nLength, DWORD dwMode) override; + virtual void SetHorizontal(); + virtual void SetVertical(); + //}}AFX_VIRTUAL +}; + + +class CMainToolBar: public CToolBarEx +{ +protected: + UpdateToolTip m_tooltip; + CImageListEx m_ImageList, m_ImageListDisabled; + CStatic m_EditTempo, m_EditSpeed, m_EditOctave, m_EditRowsPerBeat; + CStatic m_StaticTempo, m_StaticSpeed, m_StaticRowsPerBeat; + CSpinButtonCtrl m_SpinTempo, m_SpinSpeed, m_SpinOctave, m_SpinRowsPerBeat; + int nCurrentSpeed, nCurrentOctave, nCurrentRowsPerBeat; + TEMPO nCurrentTempo; +public: + CStereoVU m_VuMeter; + +public: + CMainToolBar() {} + ~CMainToolBar() override {} + +protected: + void SetRowsPerBeat(ROWINDEX nNewRPB); + +public: + //{{AFX_VIRTUAL(CMainToolBar) + void SetHorizontal() override; + void SetVertical() override; + //}}AFX_VIRTUAL + +public: +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Woverloaded-virtual" +#endif // MPT_COMPILER_CLANG + BOOL Create(CWnd *parent); +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG + void Init(CMainFrame *); + UINT GetBaseOctave() const; + BOOL SetBaseOctave(UINT nOctave); + BOOL SetCurrentSong(CSoundFile *pModDoc); + + bool ShowUpdateInfo(const CString &newVersion, const CString &infoURL, bool showHighLight); + void RemoveUpdateInfo(); + +protected: + //{{AFX_MSG(CMainToolBar) + afx_msg void OnVScroll(UINT, UINT, CScrollBar *); + afx_msg void OnTbnDropDownToolBar(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg BOOL OnToolTipText(UINT, NMHDR* pNMHDR, LRESULT* pResult); + afx_msg void OnSelectMIDIDevice(UINT id); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +class CModTreeBar: public CDialogBar +{ +protected: + enum Status + { + MTB_VERTICAL = 0x01, + MTB_CAPTURE = 0x02, + MTB_DRAGGING = 0x04, + MTB_TRACKER = 0x08, + }; + + DWORD m_dwStatus = 0; // MTB_XXXX + UINT m_nCursorDrag = 0; + CPoint ptDragging; + UINT m_cxOriginal = 0, m_cyOriginal = 0, m_nTrackPos = 0; + UINT m_nTreeSplitRatio = 0; + +public: + CModTree *m_pModTree = nullptr, *m_pModTreeData = nullptr; + + CModTreeBar(); + ~CModTreeBar() override; + +public: + void Init(); + void RecalcLayout(); + void DoMouseMove(CPoint point); + void DoLButtonDown(CPoint point); + void DoLButtonUp(); + void CancelTracking(); + void OnInvertTracker(UINT x); + void RefreshDlsBanks(); + void RefreshMidiLibrary(); + void OnOptionsChanged(); + void OnDocumentCreated(CModDoc *pModDoc); + void OnDocumentClosed(CModDoc *pModDoc); + void OnUpdate(CModDoc *pModDoc, UpdateHint hint, CObject *pHint = nullptr); + void UpdatePlayPos(CModDoc *pModDoc, Notification *pNotify); + HWND GetModTreeHWND(); //rewbs.customKeys + LRESULT SendMessageToModTree(UINT cmdID, WPARAM wParam, LPARAM lParam); + bool SetTreeSoundfile(FileReader &file); + + +protected: + //{{AFX_VIRTUAL(CModTreeBar) + CSize CalcFixedLayout(BOOL bStretch, BOOL bHorz) override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CModTreeBar) + afx_msg void OnNcPaint(); + afx_msg LRESULT OnNcHitTest(CPoint point); + afx_msg void OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnNcMouseMove(UINT nHitTest, CPoint point); + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnNcLButtonDown(UINT, CPoint); + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnNcLButtonUp(UINT, CPoint); + afx_msg void OnLButtonUp(UINT, CPoint); + afx_msg void OnNcRButtonDown(UINT, CPoint) { CancelTracking(); } + afx_msg void OnRButtonDown(UINT, CPoint) { CancelTracking(); } + afx_msg LRESULT OnInitDialog(WPARAM, LPARAM); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mainfrm.h b/Src/external_dependencies/openmpt-trunk/mptrack/Mainfrm.h new file mode 100644 index 00000000..4e3aa33b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mainfrm.h @@ -0,0 +1,588 @@ +/* + * Mainfrm.h + * --------- + * Purpose: Implementation of OpenMPT's main window code. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <Msctf.h> +#include "Mptrack.h" +#include "AutoSaver.h" +#include "UpdateHints.h" +#include "../soundlib/AudioCriticalSection.h" +#include "mpt/mutex/mutex.hpp" +#include "../soundlib/Sndfile.h" +#include "openmpt/soundbase/Dither.hpp" +#include "../common/Dither.h" +#include "mpt/audio/span.hpp" +#include "openmpt/sounddevice/SoundDeviceBuffer.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CDLSBank; +class CInputHandler; +class CModDoc; +class CAutoSaver; +struct UpdateCheckResult; +namespace SoundDevice { +class Base; +class ICallback; +} // namerspace SoundDevice + +#define MAINFRAME_TITLE _T("Open ModPlug Tracker") + +// Custom window messages +enum +{ + WM_MOD_UPDATEPOSITION = (WM_USER+1973), + WM_MOD_INVALIDATEPATTERNS, + WM_MOD_ACTIVATEVIEW, + WM_MOD_CHANGEVIEWCLASS, + WM_MOD_UNLOCKCONTROLS, + WM_MOD_CTRLMSG, + WM_MOD_VIEWMSG, + WM_MOD_MIDIMSG, + WM_MOD_GETTOOLTIPTEXT, + WM_MOD_DRAGONDROPPING, + WM_MOD_KBDNOTIFY, + WM_MOD_INSTRSELECTED, + WM_MOD_KEYCOMMAND, + WM_MOD_RECORDPARAM, + WM_MOD_PLUGPARAMAUTOMATE, + WM_MOD_MIDIMAPPING, + WM_MOD_UPDATEVIEWS, + WM_MOD_SETMODIFIED, + WM_MOD_MDIACTIVATE, + WM_MOD_MDIDEACTIVATE, + WM_MOD_UPDATENOTIFY, + WM_MOD_PLUGINDRYWETRATIOCHANGED, +}; + +enum +{ + MPT_WM_APP_UPDATECHECK_START = WM_APP + 1, + MPT_WM_APP_UPDATECHECK_PROGRESS = WM_APP + 2, + MPT_WM_APP_UPDATECHECK_CANCELED = WM_APP + 3, + MPT_WM_APP_UPDATECHECK_FAILURE = WM_APP + 4, + MPT_WM_APP_UPDATECHECK_SUCCESS = WM_APP + 5, +}; + +enum +{ + CTRLMSG_BASE = 0, + CTRLMSG_SETVIEWWND, + CTRLMSG_ACTIVATEPAGE, + CTRLMSG_DEACTIVATEPAGE, + CTRLMSG_SETFOCUS, + // Pattern-Specific + CTRLMSG_GETCURRENTPATTERN, + CTRLMSG_NOTIFYCURRENTORDER, + CTRLMSG_SETCURRENTORDER, + CTRLMSG_GETCURRENTORDER, + CTRLMSG_FORCEREFRESH, + CTRLMSG_PAT_PREVINSTRUMENT, + CTRLMSG_PAT_NEXTINSTRUMENT, + CTRLMSG_PAT_SETINSTRUMENT, + CTRLMSG_PAT_FOLLOWSONG, + CTRLMSG_PAT_LOOP, + CTRLMSG_PAT_NEWPATTERN, + CTRLMSG_PAT_SETSEQUENCE, + CTRLMSG_GETCURRENTINSTRUMENT, + CTRLMSG_SETCURRENTINSTRUMENT, + CTRLMSG_SETSPACING, + CTRLMSG_PATTERNCHANGED, + CTRLMSG_PREVORDER, + CTRLMSG_NEXTORDER, + CTRLMSG_SETRECORD, + CTRLMSG_PAT_DUPPATTERN, + // Sample-Specific + CTRLMSG_SMP_PREVINSTRUMENT, + CTRLMSG_SMP_NEXTINSTRUMENT, + CTRLMSG_SMP_OPENFILE, + CTRLMSG_SMP_SETZOOM, + CTRLMSG_SMP_GETZOOM, + CTRLMSG_SMP_SONGDROP, + CTRLMSG_SMP_INITOPL, + CTRLMSG_SMP_NEWSAMPLE, + // Instrument-Specific + CTRLMSG_INS_PREVINSTRUMENT, + CTRLMSG_INS_NEXTINSTRUMENT, + CTRLMSG_INS_OPENFILE, + CTRLMSG_INS_NEWINSTRUMENT, + CTRLMSG_INS_SONGDROP, + CTRLMSG_INS_SAMPLEMAP, +}; + +enum +{ + VIEWMSG_BASE=0, + VIEWMSG_SETCTRLWND, + VIEWMSG_SETACTIVE, + VIEWMSG_SETFOCUS, + VIEWMSG_SAVESTATE, + VIEWMSG_LOADSTATE, + // Pattern-Specific + VIEWMSG_SETCURRENTPATTERN, + VIEWMSG_GETCURRENTPATTERN, + VIEWMSG_FOLLOWSONG, + VIEWMSG_PATTERNLOOP, + VIEWMSG_GETCURRENTPOS, + VIEWMSG_SETRECORD, + VIEWMSG_SETSPACING, + VIEWMSG_PATTERNPROPERTIES, + VIEWMSG_SETVUMETERS, + VIEWMSG_SETPLUGINNAMES, //rewbs.patPlugNames + VIEWMSG_DOMIDISPACING, + VIEWMSG_EXPANDPATTERN, + VIEWMSG_SHRINKPATTERN, + VIEWMSG_COPYPATTERN, + VIEWMSG_PASTEPATTERN, + VIEWMSG_AMPLIFYPATTERN, + VIEWMSG_SETDETAIL, + // Sample-Specific + VIEWMSG_SETCURRENTSAMPLE, + VIEWMSG_SETMODIFIED, + VIEWMSG_PREPAREUNDO, + // Instrument-Specific + VIEWMSG_SETCURRENTINSTRUMENT, + VIEWMSG_DOSCROLL, +}; + + +#define NUM_VUMETER_PENS 32 + + +// Tab Order +enum OptionsPage +{ + OPTIONS_PAGE_DEFAULT = 0, + OPTIONS_PAGE_GENERAL = OPTIONS_PAGE_DEFAULT, + OPTIONS_PAGE_SOUNDCARD, + OPTIONS_PAGE_MIXER, + OPTIONS_PAGE_PLAYER, + OPTIONS_PAGE_SAMPLEDITOR, + OPTIONS_PAGE_KEYBOARD, + OPTIONS_PAGE_COLORS, + OPTIONS_PAGE_MIDI, + OPTIONS_PAGE_PATHS, + OPTIONS_PAGE_UPDATE, + OPTIONS_PAGE_ADVANCED, + OPTIONS_PAGE_WINE, +}; + + +///////////////////////////////////////////////////////////////////////// +// Player position notification + +#define MAX_UPDATE_HISTORY 2000 // 2 seconds with 1 ms updates +OPENMPT_NAMESPACE_END +#include "Notification.h" +OPENMPT_NAMESPACE_BEGIN + +OPENMPT_NAMESPACE_END +#include "CImageListEx.h" +#include "Mainbar.h" +#include "TrackerSettings.h" +OPENMPT_NAMESPACE_BEGIN +struct MODPLUGDIB; + +template<> inline SettingValue ToSettingValue(const WINDOWPLACEMENT &val) +{ + return SettingValue(EncodeBinarySetting<WINDOWPLACEMENT>(val), "WINDOWPLACEMENT"); +} +template<> inline WINDOWPLACEMENT FromSettingValue(const SettingValue &val) +{ + ASSERT(val.GetTypeTag() == "WINDOWPLACEMENT"); + return DecodeBinarySetting<WINDOWPLACEMENT>(val.as<std::vector<std::byte> >()); +} + + +class VUMeter + : public IMonitorInput + , public IMonitorOutput +{ +public: + static constexpr std::size_t maxChannels = 4; + static const float dynamicRange; // corresponds to the current implementation of the UI widget diplaying the result + struct Channel + { + int32 peak = 0; + bool clipped = false; + }; +private: + Channel channels[maxChannels]; + int32 decayParam; + void Process(Channel &channel, MixSampleInt sample); + void Process(Channel &channel, MixSampleFloat sample); +public: + VUMeter() : decayParam(0) { SetDecaySpeedDecibelPerSecond(88.0f); } + void SetDecaySpeedDecibelPerSecond(float decibelPerSecond); +public: + const Channel & operator [] (std::size_t channel) const { return channels[channel]; } + void Process(mpt::audio_span_interleaved<const MixSampleInt> buffer); + void Process(mpt::audio_span_planar<const MixSampleInt> buffer); + void Process(mpt::audio_span_interleaved<const MixSampleFloat> buffer); + void Process(mpt::audio_span_planar<const MixSampleFloat> buffer); + void Decay(int32 secondsNum, int32 secondsDen); + void ResetClipped(); +}; + + +class TfLanguageProfileNotifySink : public ITfLanguageProfileNotifySink +{ +public: + TfLanguageProfileNotifySink(); + ~TfLanguageProfileNotifySink(); + + HRESULT STDMETHODCALLTYPE OnLanguageChange(LANGID langid, __RPC__out BOOL *pfAccept) override; + HRESULT STDMETHODCALLTYPE OnLanguageChanged() override; + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override; + ULONG STDMETHODCALLTYPE AddRef() override; + ULONG STDMETHODCALLTYPE Release() override; + +protected: + ITfInputProcessorProfiles *m_pProfiles = nullptr; + ITfSource *m_pSource = nullptr; + DWORD m_dwCookie = TF_INVALID_COOKIE; +}; + + +class CMainFrame + : public CMDIFrameWnd + , public SoundDevice::CallbackBufferHandler<DithersOpenMPT> + , public SoundDevice::IMessageReceiver + , public TfLanguageProfileNotifySink +{ + DECLARE_DYNAMIC(CMainFrame) + // static data +public: + + // Globals + static OptionsPage m_nLastOptionsPage; + static HHOOK ghKbdHook; + + // GDI + static HICON m_hIcon; + static HFONT m_hGUIFont, m_hFixedFont; + static HPEN penDarkGray, penHalfDarkGray, penGray99; + static HCURSOR curDragging, curNoDrop, curArrow, curNoDrop2, curVSplit; + static MODPLUGDIB *bmpNotes, *bmpVUMeters, *bmpPluginVUMeters; + static COLORREF gcolrefVuMeter[NUM_VUMETER_PENS * 2]; // General tab VU meters + +public: + + // Low-Level Audio + CriticalSection m_SoundDeviceFillBufferCriticalSection; + Util::MultimediaClock m_SoundDeviceClock; + SoundDevice::IBase *gpSoundDevice = nullptr; + UINT_PTR m_NotifyTimer = 0; + VUMeter m_VUMeterInput; + VUMeter m_VUMeterOutput; + + DWORD m_AudioThreadId = 0; + bool m_InNotifyHandler = false; + + // Midi Input +public: + static HMIDIIN shMidiIn; + +public: + CImageListEx m_MiscIcons, m_MiscIconsDisabled; // Misc Icons + CImageListEx m_PatternIcons, m_PatternIconsDisabled; // Pattern icons (includes some from sample editor as well...) + CImageListEx m_EnvelopeIcons; // Instrument editor icons + CImageListEx m_SampleIcons; // Sample editor icons + +protected: + CModTreeBar m_wndTree; + CStatusBar m_wndStatusBar; + CMainToolBar m_wndToolBar; + CSoundFile *m_pSndFile = nullptr; // != NULL only when currently playing or rendering + HWND m_hWndMidi = nullptr; + CSoundFile::samplecount_t m_dwTimeSec = 0; + UINT_PTR m_nTimer = 0; + UINT m_nAvgMixChn = 0, m_nMixChn = 0; + // Misc + class COptionsSoundcard *m_SoundCardOptionsDialog = nullptr; +#if defined(MPT_ENABLE_UPDATE) + class CUpdateSetupDlg *m_UpdateOptionsDialog = nullptr; + std::unique_ptr<UpdateCheckResult> m_updateCheckResult; +#endif // MPT_ENABLE_UPDATE + DWORD helpCookie = 0; + bool m_bOptionsLocked = false; + + // Notification Buffer + mpt::mutex m_NotificationBufferMutex; // to avoid deadlocks, this mutex should only be taken as a innermost lock, i.e. do not block on anything while holding this mutex + Util::fixed_size_queue<Notification,MAX_UPDATE_HISTORY> m_NotifyBuffer; + + // Instrument preview in tree view + CSoundFile m_WaveFile; + + TCHAR m_szUserText[512], m_szInfoText[512], m_szXInfoText[512]; + + CAutoSaver m_AutoSaver; + +public: + CWnd *m_pNoteMapHasFocus = nullptr; + CWnd *m_pOrderlistHasFocus = nullptr; + bool m_bModTreeHasFocus = false; + +public: + CMainFrame(/*CString regKeyExtension*/); + void Initialize(); + + +// Low-Level Audio +public: + static void UpdateDspEffects(CSoundFile &sndFile, bool reset=false); + static void UpdateAudioParameters(CSoundFile &sndFile, bool reset=false); + + // from SoundDevice::IBufferHandler + uint64 SoundCallbackGetReferenceClockNowNanoseconds() const override; + void SoundCallbackPreStart() override; + void SoundCallbackPostStop() override; + bool SoundCallbackIsLockedByCurrentThread() const override; + void SoundCallbackLock() override; + uint64 SoundCallbackLockedGetReferenceClockNowNanoseconds() const override; + void SoundCallbackLockedProcessPrepare(SoundDevice::TimeInfo timeInfo) override; + void SoundCallbackLockedCallback(SoundDevice::CallbackBuffer<DithersOpenMPT> &buffer) override; + void SoundCallbackLockedProcessDone(SoundDevice::TimeInfo timeInfo) override; + void SoundCallbackUnlock() override; + + // from SoundDevice::IMessageReceiver + void SoundDeviceMessage(LogLevel level, const mpt::ustring &str) override; + + bool InGuiThread() const { return theApp.InGuiThread(); } + bool InAudioThread() const { return GetCurrentThreadId() == m_AudioThreadId; } + bool InNotifyHandler() const { return m_InNotifyHandler; } + + bool audioOpenDevice(); + void audioCloseDevice(); + bool IsAudioDeviceOpen() const; + bool DoNotification(DWORD dwSamplesRead, int64 streamPosition); + +// Midi Input Functions +public: + bool midiOpenDevice(bool showSettings = true); + void midiCloseDevice(); + void SetMidiRecordWnd(HWND hwnd) { m_hWndMidi = hwnd; } + HWND GetMidiRecordWnd() const { return m_hWndMidi; } + + static int ApplyVolumeRelatedSettings(const DWORD &dwParam1, const BYTE midivolume); + +// static functions +public: + static CMainFrame *GetMainFrame() { return (CMainFrame *)theApp.m_pMainWnd; } + static void UpdateColors(); + static HICON GetModIcon() { return m_hIcon; } + static HFONT GetGUIFont() { return m_hGUIFont; } + static HFONT &GetCommentsFont() { return m_hFixedFont; } + static void UpdateAllViews(UpdateHint hint, CObject *pHint=NULL); + static LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam); + static CInputHandler *m_InputHandler; + + // Misc functions +public: + void SetUserText(LPCTSTR lpszText); + void SetInfoText(LPCTSTR lpszText); + void SetXInfoText(LPCTSTR lpszText); + void SetHelpText(LPCTSTR lpszText); + UINT GetBaseOctave() const; + CModDoc *GetActiveDoc() const; + CView *GetActiveView() const; + void OnDocumentCreated(CModDoc *pModDoc); + void OnDocumentClosed(CModDoc *pModDoc); + void UpdateTree(CModDoc *pModDoc, UpdateHint hint, CObject *pHint = nullptr); + void RefreshDlsBanks(); + static CInputHandler* GetInputHandler() { return m_InputHandler; } + void SetElapsedTime(double t) { m_dwTimeSec = static_cast<CSoundFile::samplecount_t>(t); } + +#if defined(MPT_ENABLE_UPDATE) + bool ShowUpdateIndicator(const UpdateCheckResult &result, const CString &releaseVersion, const CString &infoURL, bool showHighlight); +#endif // MPT_ENABLE_UPDATE + + CModTree *GetUpperTreeview() { return m_wndTree.m_pModTree; } + CModTree *GetLowerTreeview() { return m_wndTree.m_pModTreeData; } + bool SetTreeSoundfile(FileReader &file) { return m_wndTree.SetTreeSoundfile(file); } + + void CreateExampleModulesMenu(); + void CreateTemplateModulesMenu(); + CMenu *GetFileMenu() const; + + // Creates submenu whose items are filenames of files in both + // AppDirectory\folderName\ (usually C:\Program Files\OpenMPT\folderName\) + // and + // ConfigDirectory\folderName (usually %appdata%\OpenMPT\folderName\) + // [in] maxCount: Maximum number of items allowed in the menu + // [out] paths: Receives the full paths of the files added to the menu. + // [in] folderName: Name of the folder + // [in] idRangeBegin: First ID for the menu item. + static HMENU CreateFileMenu(const size_t maxCount, std::vector<mpt::PathString>& paths, const mpt::PathString &folderName, const uint16 idRangeBegin); + +// Player functions +public: + + // High-level synchronous playback functions, do not hold AudioCriticalSection while calling these + bool PreparePlayback(); + bool StartPlayback(); + void StopPlayback(); + bool RestartPlayback(); + bool PausePlayback(); + static bool IsValidSoundFile(CSoundFile &sndFile) { return sndFile.GetType() ? true : false; } + static bool IsValidSoundFile(CSoundFile *pSndFile) { return pSndFile && pSndFile->GetType(); } + void SetPlaybackSoundFile(CSoundFile *pSndFile); + void UnsetPlaybackSoundFile(); + void GenerateStopNotification(); + + bool PlayMod(CModDoc *); + bool StopMod(CModDoc *pDoc = nullptr); + bool PauseMod(CModDoc *pDoc = nullptr); + + bool StopSoundFile(CSoundFile *); + bool PlaySoundFile(CSoundFile *); + bool PlaySoundFile(const mpt::PathString &filename, ModCommand::NOTE note, int volume = -1); + bool PlaySoundFile(CSoundFile &sndFile, INSTRUMENTINDEX nInstrument, SAMPLEINDEX nSample, ModCommand::NOTE note, int volume = -1); + bool PlayDLSInstrument(const CDLSBank &bank, UINT instr, UINT region, ModCommand::NOTE note, int volume = -1); + + void InitPreview(); + void PreparePreview(ModCommand::NOTE note, int volume); + void StopPreview() { StopSoundFile(&m_WaveFile); } + void PlayPreview() { PlaySoundFile(&m_WaveFile); } + + inline bool IsPlaying() const { return m_pSndFile != nullptr; } + // Return currently playing module (nullptr if none is playing) + inline CModDoc *GetModPlaying() const { return m_pSndFile ? m_pSndFile->GetpModDoc() : nullptr; } + // Return currently playing module (nullptr if none is playing) + inline CSoundFile *GetSoundFilePlaying() const { return m_pSndFile; } + BOOL InitRenderer(CSoundFile*); + BOOL StopRenderer(CSoundFile*); + void SwitchToActiveView(); + + void IdleHandlerSounddevice(); + + BOOL ResetSoundCard(); + BOOL SetupSoundCard(SoundDevice::Settings deviceSettings, SoundDevice::Identifier deviceIdentifier, SoundDeviceStopMode stoppedMode, bool forceReset = false); + BOOL SetupMiscOptions(); + BOOL SetupPlayer(); + + void SetupMidi(DWORD d, UINT n); + HWND GetFollowSong() const; + HWND GetFollowSong(const CModDoc *pDoc) const { return (pDoc == GetModPlaying()) ? GetFollowSong() : nullptr; } + void ResetNotificationBuffer(); + + // Notify accessbility software that it should read out updated UI elements + void NotifyAccessibilityUpdate(CWnd &source); + +// Overrides +protected: + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(CMainFrame) + BOOL PreCreateWindow(CREATESTRUCT& cs) override; + BOOL PreTranslateMessage(MSG *pMsg) override; + BOOL DestroyWindow() override; + void OnUpdateFrameTitle(BOOL bAddToTitle) override; + //}}AFX_VIRTUAL + + /// Opens either template or example menu item. + void OpenMenuItemFile(const UINT nId, const bool isTemplateFile); + +public: + void UpdateMRUList(); + +// Implementation +public: + ~CMainFrame() override; +#ifdef _DEBUG + void AssertValid() const override; + void Dump(CDumpContext& dc) const override; +#endif + + void OnTimerGUI(); + void OnTimerNotify(); + +// Message map functions + //{{AFX_MSG(CMainFrame) +public: + afx_msg void OnAddDlsBank(); + afx_msg void OnImportMidiLib(); + afx_msg void OnViewOptions(); +protected: + afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct); + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnClose(); + afx_msg void OnTimer(UINT_PTR); + + afx_msg void OnPluginManager(); + afx_msg void OnClipboardManager(); + + afx_msg LRESULT OnViewMIDIMapping(WPARAM wParam, LPARAM lParam); + afx_msg void OnUpdateTime(CCmdUI *pCmdUI); + afx_msg void OnUpdateUser(CCmdUI *pCmdUI); + afx_msg void OnUpdateInfo(CCmdUI *pCmdUI); + afx_msg void OnUpdateXInfo(CCmdUI *pCmdUI); + afx_msg void OnUpdateMidiRecord(CCmdUI *pCmdUI); + afx_msg void OnPlayerPause(); + afx_msg void OnMidiRecord(); + afx_msg void OnPrevOctave(); + afx_msg void OnNextOctave(); + afx_msg void OnPanic(); + afx_msg void OnReportBug(); + afx_msg BOOL OnInternetLink(UINT nID); + afx_msg LRESULT OnUpdatePosition(WPARAM, LPARAM lParam); + afx_msg LRESULT OnUpdateViews(WPARAM modDoc, LPARAM hint); + afx_msg LRESULT OnSetModified(WPARAM modDoc, LPARAM); + afx_msg void OnOpenTemplateModule(UINT nId); + afx_msg void OnExampleSong(UINT nId); + afx_msg void OnOpenMRUItem(UINT nId); + afx_msg void OnUpdateMRUItem(CCmdUI *cmd); + afx_msg LRESULT OnInvalidatePatterns(WPARAM, LPARAM); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg void OnInternetUpdate(); + afx_msg void OnUpdateAvailable(); + afx_msg void OnShowSettingsFolder(); +#if defined(MPT_ENABLE_UPDATE) + afx_msg LRESULT OnUpdateCheckStart(WPARAM wparam, LPARAM lparam); + afx_msg LRESULT OnUpdateCheckProgress(WPARAM wparam, LPARAM lparam); + afx_msg LRESULT OnUpdateCheckCanceled(WPARAM wparam, LPARAM lparam); + afx_msg LRESULT OnUpdateCheckFailure(WPARAM wparam, LPARAM lparam); + afx_msg LRESULT OnUpdateCheckSuccess(WPARAM wparam, LPARAM lparam); + afx_msg LRESULT OnToolbarUpdateIndicatorClick(WPARAM wparam, LPARAM lparam); +#endif // MPT_ENABLE_UPDATE + afx_msg void OnHelp(); + afx_msg BOOL OnDeviceChange(UINT nEventType, DWORD_PTR dwData); + afx_msg void OnDropFiles(HDROP hDropInfo); + afx_msg BOOL OnQueryEndSession(); + afx_msg void OnActivateApp(BOOL active, DWORD threadID); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnInitMenu(CMenu *pMenu); + bool UpdateEffectKeys(const CModDoc *modDoc); + afx_msg void OnKillFocus(CWnd* pNewWnd); + afx_msg void OnShowWindow(BOOL bShow, UINT nStatus); + + // Defines maximum number of items in example modules menu. + static constexpr size_t nMaxItemsInExampleModulesMenu = 50; + static constexpr size_t nMaxItemsInTemplateModulesMenu = 50; + +private: + /// Array of paths of example modules that are available from help menu. + std::vector<mpt::PathString> m_ExampleModulePaths; + /// Array of paths of template modules that are available from file menu. + std::vector<mpt::PathString> m_TemplateModulePaths; +}; + + +///////////////////////////////////////////////////////////////////////////// + +//{{AFX_INSERT_LOCATION}} +// Microsoft Developer Studio will insert additional declarations immediately before the previous line. + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mod2wave.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Mod2wave.cpp new file mode 100644 index 00000000..20470260 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mod2wave.cpp @@ -0,0 +1,1401 @@ +/* + * mod2wave.cpp + * ------------ + * Purpose: Module to WAV conversion (dialog + conversion code). + * 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 "Sndfile.h" +#include "Dlsbank.h" +#include "Mainfrm.h" +#include "Mpdlgs.h" +#include "mod2wave.h" +#include "WAVTools.h" +#include "../common/mptString.h" +#include "../common/version.h" +#include "../soundlib/MixerLoops.h" +#include "openmpt/soundbase/Dither.hpp" +#include "../common/Dither.h" +#include "../soundlib/AudioReadTarget.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../common/mptFileIO.h" +#include "mpt/audio/span.hpp" +#include <variant> +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +extern const TCHAR *gszChnCfgNames[3]; + + +template <typename Tsample> +static CSoundFile::samplecount_t ReadInterleaved(CSoundFile &sndFile, Tsample *outputBuffer, std::size_t channels, CSoundFile::samplecount_t count, DithersOpenMPT &dithers) +{ + sndFile.ResetMixStat(); + MPT_ASSERT(sndFile.m_MixerSettings.gnChannels == channels); + AudioTargetBuffer<mpt::audio_span_interleaved<Tsample>, DithersOpenMPT> target(mpt::audio_span_interleaved<Tsample>(outputBuffer, channels, count), dithers); + return sndFile.Read(count, target); +} + + +/////////////////////////////////////////////////// +// CWaveConvert - setup for converting a wave file + +BEGIN_MESSAGE_MAP(CWaveConvert, CDialog) + ON_COMMAND(IDC_CHECK2, &CWaveConvert::OnCheckTimeLimit) + ON_COMMAND(IDC_CHECK4, &CWaveConvert::OnCheckChannelMode) + ON_COMMAND(IDC_CHECK6, &CWaveConvert::OnCheckInstrMode) + ON_COMMAND(IDC_RADIO1, &CWaveConvert::UpdateDialog) + ON_COMMAND(IDC_RADIO2, &CWaveConvert::UpdateDialog) + ON_COMMAND(IDC_RADIO3, &CWaveConvert::UpdateDialog) + ON_COMMAND(IDC_RADIO4, &CWaveConvert::OnExportModeChanged) + ON_COMMAND(IDC_RADIO5, &CWaveConvert::OnExportModeChanged) + ON_COMMAND(IDC_PLAYEROPTIONS, &CWaveConvert::OnPlayerOptions) + ON_CBN_SELCHANGE(IDC_COMBO5, &CWaveConvert::OnFileTypeChanged) + ON_CBN_SELCHANGE(IDC_COMBO1, &CWaveConvert::OnSamplerateChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &CWaveConvert::OnChannelsChanged) + ON_CBN_SELCHANGE(IDC_COMBO6, &CWaveConvert::OnDitherChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CWaveConvert::OnFormatChanged) + ON_CBN_SELCHANGE(IDC_COMBO9, &CWaveConvert::OnSampleSlotChanged) +END_MESSAGE_MAP() + + +CWaveConvert::CWaveConvert(CWnd *parent, ORDERINDEX minOrder, ORDERINDEX maxOrder, ORDERINDEX numOrders, CSoundFile &sndFile, const std::vector<EncoderFactoryBase*> &encFactories) + : CDialog(IDD_WAVECONVERT, parent) + , m_Settings(theApp.GetSettings(), encFactories) + , m_SndFile(sndFile) +{ + ASSERT(!encFactories.empty()); + encTraits = m_Settings.GetTraits(); + m_bGivePlugsIdleTime = false; + if(minOrder != ORDERINDEX_INVALID && maxOrder != ORDERINDEX_INVALID) + { + // render selection + m_Settings.minOrder = minOrder; + m_Settings.maxOrder = maxOrder; + } + m_Settings.repeatCount = 1; + m_Settings.minSequence = m_Settings.maxSequence = m_SndFile.Order.GetCurrentSequenceIndex(); + m_nNumOrders = numOrders; + + m_dwSongLimit = 0; +} + + +void CWaveConvert::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO5, m_CbnFileType); + DDX_Control(pDX, IDC_COMBO1, m_CbnSampleRate); + DDX_Control(pDX, IDC_COMBO4, m_CbnChannels); + DDX_Control(pDX, IDC_COMBO6, m_CbnDither); + DDX_Control(pDX, IDC_COMBO2, m_CbnSampleFormat); + DDX_Control(pDX, IDC_SPIN3, m_SpinMinOrder); + DDX_Control(pDX, IDC_SPIN4, m_SpinMaxOrder); + DDX_Control(pDX, IDC_SPIN5, m_SpinLoopCount); + DDX_Control(pDX, IDC_SPIN6, m_SpinMinSequence); + DDX_Control(pDX, IDC_SPIN7, m_SpinMaxSequence); + DDX_Control(pDX, IDC_COMBO9, m_CbnSampleSlot); + + DDX_Control(pDX, IDC_COMBO3, m_CbnGenre); + DDX_Control(pDX, IDC_EDIT10, m_EditGenre); + DDX_Control(pDX, IDC_EDIT11, m_EditTitle); + DDX_Control(pDX, IDC_EDIT6, m_EditAuthor); + DDX_Control(pDX, IDC_EDIT7, m_EditAlbum); + DDX_Control(pDX, IDC_EDIT8, m_EditURL); + DDX_Control(pDX, IDC_EDIT9, m_EditYear); +} + + +BOOL CWaveConvert::OnInitDialog() +{ + CDialog::OnInitDialog(); + + CheckDlgButton(IDC_CHECK5, BST_UNCHECKED); // Normalize + CheckDlgButton(IDC_CHECK3, BST_CHECKED); // Cue points + + CheckDlgButton(IDC_CHECK4, BST_UNCHECKED); + CheckDlgButton(IDC_CHECK6, BST_UNCHECKED); + + const bool selection = (m_Settings.minOrder != ORDERINDEX_INVALID && m_Settings.maxOrder != ORDERINDEX_INVALID); + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, selection ? IDC_RADIO2 : IDC_RADIO1); + if(selection) + { + SetDlgItemInt(IDC_EDIT3, m_Settings.minOrder); + SetDlgItemInt(IDC_EDIT4, m_Settings.maxOrder); + } + m_SpinMinOrder.SetRange32(0, m_nNumOrders); + m_SpinMaxOrder.SetRange32(0, m_nNumOrders); + + const SEQUENCEINDEX numSequences = m_SndFile.Order.GetNumSequences(); + const BOOL enableSeq = numSequences > 1 ? TRUE : FALSE; + GetDlgItem(IDC_RADIO3)->EnableWindow(enableSeq); + m_SpinMinSequence.SetRange32(1, numSequences); + m_SpinMaxSequence.SetRange32(1, numSequences); + SetDlgItemInt(IDC_EDIT12, m_Settings.minSequence + 1); + SetDlgItemInt(IDC_EDIT13, m_Settings.maxSequence + 1); + + SetDlgItemInt(IDC_EDIT5, m_Settings.repeatCount, FALSE); + m_SpinLoopCount.SetRange32(1, int16_max); + + FillFileTypes(); + FillSamplerates(); + FillChannels(); + FillFormats(); + FillDither(); + + LoadTags(); + + m_EditYear.SetLimitText(4); + + m_EditTitle.SetWindowText(mpt::ToCString(m_Settings.Tags.title)); + m_EditAuthor.SetWindowText(mpt::ToCString(m_Settings.Tags.artist)); + m_EditURL.SetWindowText(mpt::ToCString(m_Settings.Tags.url)); + m_EditAlbum.SetWindowText(mpt::ToCString(m_Settings.Tags.album)); + m_EditYear.SetWindowText(mpt::ToCString(m_Settings.Tags.year)); + m_EditGenre.SetWindowText(mpt::ToCString(m_Settings.Tags.genre)); + + FillTags(); + + // Plugin quirk options are only available if there are any plugins loaded. + GetDlgItem(IDC_GIVEPLUGSIDLETIME)->EnableWindow(FALSE); + GetDlgItem(IDC_RENDERSILENCE)->EnableWindow(FALSE); +#ifndef NO_PLUGINS + for(const auto &plug : m_SndFile.m_MixPlugins) + { + if(plug.pMixPlugin != nullptr) + { + GetDlgItem(IDC_GIVEPLUGSIDLETIME)->EnableWindow(TRUE); + GetDlgItem(IDC_RENDERSILENCE)->EnableWindow(TRUE); + break; + } + } +#endif // NO_PLUGINS + + // Fill list of sample slots to render into + if(m_SndFile.GetNextFreeSample() != SAMPLEINDEX_INVALID) + { + m_CbnSampleSlot.SetItemData(m_CbnSampleSlot.AddString(_T("<empty slot>")), 0); + } + CString s; + for(SAMPLEINDEX smp = 1; smp <= m_SndFile.GetNumSamples(); smp++) + { + s.Format(_T("%02u: %s%s"), smp, m_SndFile.GetSample(smp).HasSampleData() ? _T("*") : _T(""), mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetSampleName(smp)).GetString()); + m_CbnSampleSlot.SetItemData(m_CbnSampleSlot.AddString(s), smp); + } + if(m_Settings.sampleSlot > m_SndFile.GetNumSamples()) m_Settings.sampleSlot = 0; + m_CbnSampleSlot.SetCurSel(m_Settings.sampleSlot); + + CheckRadioButton(IDC_RADIO4, IDC_RADIO5, m_Settings.outputToSample ? IDC_RADIO5 : IDC_RADIO4); + + UpdateDialog(); + return TRUE; +} + + +void CWaveConvert::LoadTags() +{ + m_Settings.Tags.title = mpt::ToUnicode(mpt::Charset::Locale, m_SndFile.GetTitle()); + m_Settings.Tags.comments = mpt::ToUnicode(mpt::Charset::Locale, m_SndFile.m_songMessage.GetFormatted(SongMessage::leLF)); + m_Settings.Tags.artist = m_SndFile.m_songArtist; + m_Settings.Tags.album = m_Settings.storedTags.album; + m_Settings.Tags.trackno = m_Settings.storedTags.trackno; + m_Settings.Tags.year = m_Settings.storedTags.year; + m_Settings.Tags.url = m_Settings.storedTags.url; + m_Settings.Tags.genre = m_Settings.storedTags.genre; +} + + +void CWaveConvert::SaveTags() +{ + m_Settings.storedTags.artist = m_Settings.Tags.artist; + m_Settings.storedTags.album = m_Settings.Tags.album; + m_Settings.storedTags.trackno = m_Settings.Tags.trackno; + m_Settings.storedTags.year = m_Settings.Tags.year; + m_Settings.storedTags.url = m_Settings.Tags.url; + m_Settings.storedTags.genre = m_Settings.Tags.genre; +} + + +void CWaveConvert::FillTags() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + + DWORD_PTR dwFormat = m_CbnSampleFormat.GetItemData(m_CbnSampleFormat.GetCurSel()); + Encoder::Mode mode = (Encoder::Mode)((dwFormat >> 24) & 0xff); + + CheckDlgButton(IDC_CHECK3, encTraits->canCues?encSettings.Cues?TRUE:FALSE:FALSE); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_CHECK3), encTraits->canCues?TRUE:FALSE); + + const BOOL canTags = encTraits->canTags ? TRUE : FALSE; + CheckDlgButton(IDC_CHECK7, encSettings.Tags ? canTags : FALSE); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_CHECK7), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_COMBO3), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT11), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT6), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT7), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT8), canTags); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT9), canTags); + m_CbnGenre.EnableWindow(canTags?TRUE:FALSE); + m_EditGenre.EnableWindow(canTags?TRUE:FALSE); + + if((encTraits->modesWithFixedGenres & mode) && !encTraits->genres.empty()) + { + m_EditGenre.ShowWindow(SW_HIDE); + m_CbnGenre.ShowWindow(SW_SHOW); + m_EditGenre.Clear(); + m_CbnGenre.ResetContent(); + m_CbnGenre.AddString(_T("")); + for(const auto &genre : encTraits->genres) + { + m_CbnGenre.AddString(mpt::ToCString(genre)); + } + } else + { + m_CbnGenre.ShowWindow(SW_HIDE); + m_EditGenre.ShowWindow(SW_SHOW); + m_CbnGenre.ResetContent(); + m_EditGenre.Clear(); + } + +} + + +void CWaveConvert::FillFileTypes() +{ + m_CbnFileType.ResetContent(); + int sel = 0; + for(std::size_t i = 0; i < m_Settings.EncoderFactories.size(); ++i) + { + int ndx = m_CbnFileType.AddString(MPT_CFORMAT("{} ({})")(mpt::ToCString(m_Settings.EncoderFactories[i]->GetTraits().fileShortDescription), mpt::ToCString(m_Settings.EncoderFactories[i]->GetTraits().fileDescription))); + m_CbnFileType.SetItemData(ndx, i); + if(m_Settings.EncoderIndex == i) + { + sel = ndx; + } + } + m_CbnFileType.SetCurSel(sel); +} + + +void CWaveConvert::FillSamplerates() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + m_CbnSampleRate.CComboBox::ResetContent(); + int sel = -1; + if(TrackerSettings::Instance().ExportDefaultToSoundcardSamplerate) + { + for(auto samplerate : encTraits->samplerates) + { + if(samplerate == TrackerSettings::Instance().MixerSamplerate) + { + encSettings.Samplerate = samplerate; + } + } + } + for(auto samplerate : encTraits->samplerates) + { + int ndx = m_CbnSampleRate.AddString(MPT_CFORMAT("{} Hz")(samplerate)); + m_CbnSampleRate.SetItemData(ndx, samplerate); + if(samplerate == encSettings.Samplerate) + { + sel = ndx; + } + } + if(sel == -1) + { + sel = 0; + } + m_CbnSampleRate.SetCurSel(sel); +} + + +void CWaveConvert::FillChannels() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + m_CbnChannels.CComboBox::ResetContent(); + int sel = 0; + for(int channels = 4; channels >= 1; channels /= 2) + { + if(channels > encTraits->maxChannels) + { + continue; + } + if(IsDlgButtonChecked(IDC_RADIO5) != BST_UNCHECKED) + { + if(channels > 2) + { + // sample export only supports 2 channels max + continue; + } + } + int ndx = m_CbnChannels.AddString(gszChnCfgNames[(channels+2)/2-1]); + m_CbnChannels.SetItemData(ndx, channels); + if(channels == encSettings.Channels) + { + sel = ndx; + } + } + m_CbnChannels.SetCurSel(sel); +} + + +void CWaveConvert::FillFormats() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + m_CbnSampleFormat.CComboBox::ResetContent(); + int sel = -1; + int samplerate = static_cast<int>(m_CbnSampleRate.GetItemData(m_CbnSampleRate.GetCurSel())); + int channels = static_cast<int>(m_CbnChannels.GetItemData(m_CbnChannels.GetCurSel())); + if(encTraits->modes & Encoder::ModeQuality) + { + for(int quality = 100; quality >= 0; quality -= 10) + { + int ndx = m_CbnSampleFormat.AddString(mpt::ToCString(m_Settings.GetEncoderFactory()->DescribeQuality(quality * 0.01f))); + m_CbnSampleFormat.SetItemData(ndx, (Encoder::ModeQuality<<24) | (quality<<0)); + if(encSettings.Mode == Encoder::ModeQuality && mpt::saturate_round<int>(encSettings.Quality*100.0f) == quality) + { + sel = ndx; + } + } + } + if(encTraits->modes & Encoder::ModeVBR) + { + for(int bitrate = static_cast<int>(encTraits->bitrates.size()-1); bitrate >= 0; --bitrate) + { + if(!m_Settings.GetEncoderFactory()->IsBitrateSupported(samplerate, channels, encTraits->bitrates[bitrate])) + { + continue; + } + int ndx = m_CbnSampleFormat.AddString(mpt::ToCString(m_Settings.GetEncoderFactory()->DescribeBitrateVBR(encTraits->bitrates[bitrate]))); + m_CbnSampleFormat.SetItemData(ndx, (Encoder::ModeVBR<<24) | (encTraits->bitrates[bitrate]<<0)); + if(encSettings.Mode == Encoder::ModeVBR && static_cast<int>(encSettings.Bitrate) == encTraits->bitrates[bitrate]) + { + sel = ndx; + } + } + } + if(encTraits->modes & Encoder::ModeABR) + { + for(int bitrate = static_cast<int>(encTraits->bitrates.size()-1); bitrate >= 0; --bitrate) + { + if(!m_Settings.GetEncoderFactory()->IsBitrateSupported(samplerate, channels, encTraits->bitrates[bitrate])) + { + continue; + } + int ndx = m_CbnSampleFormat.AddString(mpt::ToCString(m_Settings.GetEncoderFactory()->DescribeBitrateABR(encTraits->bitrates[bitrate]))); + m_CbnSampleFormat.SetItemData(ndx, (Encoder::ModeABR<<24) | (encTraits->bitrates[bitrate]<<0)); + if(encSettings.Mode == Encoder::ModeABR && static_cast<int>(encSettings.Bitrate) == encTraits->bitrates[bitrate]) + { + sel = ndx; + } + } + } + if(encTraits->modes & Encoder::ModeCBR) + { + for(int bitrate = static_cast<int>(encTraits->bitrates.size()-1); bitrate >= 0; --bitrate) + { + if(!m_Settings.GetEncoderFactory()->IsBitrateSupported(samplerate, channels, encTraits->bitrates[bitrate])) + { + continue; + } + int ndx = m_CbnSampleFormat.AddString(mpt::ToCString(m_Settings.GetEncoderFactory()->DescribeBitrateCBR(encTraits->bitrates[bitrate]))); + m_CbnSampleFormat.SetItemData(ndx, (Encoder::ModeCBR<<24) | (encTraits->bitrates[bitrate]<<0)); + if(encSettings.Mode == Encoder::ModeCBR && static_cast<int>(encSettings.Bitrate) == encTraits->bitrates[bitrate]) + { + sel = ndx; + } + } + } + if(encTraits->modes & Encoder::ModeLossless) + { + bool allBig = true; + bool allLittle = true; + for(const auto &format : encTraits->formats) + { + if(format.endian != mpt::endian::little) + { + allLittle = false; + } + if(format.endian != mpt::endian::big) + { + allBig = false; + } + } + bool showEndian = !(allBig || allLittle); + for(std::size_t i = 0; i < encTraits->formats.size(); ++i) + { + const Encoder::Format &format = encTraits->formats[i]; + mpt::ustring description; + switch(format.encoding) + { + case Encoder::Format::Encoding::Float: + description = MPT_UFORMAT("{} Bit Floating Point")(format.bits); + break; + case Encoder::Format::Encoding::Integer: + description = MPT_UFORMAT("{} Bit")(format.bits); + break; + case Encoder::Format::Encoding::Alaw: + description = U_("A-law"); + break; + case Encoder::Format::Encoding::ulaw: + description = MPT_UTF8("\xce\xbc-law"); + break; + case Encoder::Format::Encoding::Unsigned: + description = MPT_UFORMAT("{} Bit (unsigned)")(format.bits); + break; + } + if(showEndian && format.bits != 8 && format.encoding != Encoder::Format::Encoding::Alaw && format.encoding != Encoder::Format::Encoding::ulaw) + { + switch(format.endian) + { + case mpt::endian::big: + description += U_(" Big-Endian"); + break; + case mpt::endian::little: + description += U_(" Little-Endian"); + break; + } + } + int ndx = m_CbnSampleFormat.AddString(mpt::ToCString(description)); + m_CbnSampleFormat.SetItemData(ndx, format.AsInt()); + if(encSettings.Mode & Encoder::ModeLossless && format == encSettings.Format2) + { + sel = ndx; + } + } + } + if(sel == -1) + { + sel = 0; + } + m_CbnSampleFormat.SetCurSel(sel); +} + + +void CWaveConvert::FillDither() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + m_CbnDither.CComboBox::ResetContent(); + int format = m_CbnSampleFormat.GetItemData(m_CbnSampleFormat.GetCurSel()) & 0xffffff; + if((encTraits->modes & Encoder::ModeLossless) && Encoder::Format::FromInt(format).GetSampleFormat() != SampleFormat::Invalid && !Encoder::Format::FromInt(format).GetSampleFormat().IsFloat()) + { + m_CbnDither.EnableWindow(TRUE); + for(std::size_t dither = 0; dither < DithersOpenMPT::GetNumDithers(); ++dither) + { + int ndx = m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(dither) + U_(" dither"))); + m_CbnDither.SetItemData(ndx, dither); + } + } else + { + m_CbnDither.EnableWindow(FALSE); + for(std::size_t dither = 0; dither < DithersOpenMPT::GetNumDithers(); ++dither) + { + int ndx = m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(DithersOpenMPT::GetNoDither()) + U_(" dither"))); + m_CbnDither.SetItemData(ndx, dither); + } + } + m_CbnDither.SetCurSel(encSettings.Dither); +} + + +void CWaveConvert::OnFileTypeChanged() +{ + SaveEncoderSettings(); + DWORD_PTR dwFileType = m_CbnFileType.GetItemData(m_CbnFileType.GetCurSel()); + m_Settings.SelectEncoder(dwFileType); + encTraits = m_Settings.GetTraits(); + FillSamplerates(); + FillChannels(); + FillFormats(); + FillDither(); + FillTags(); +} + + +void CWaveConvert::OnSamplerateChanged() +{ + SaveEncoderSettings(); + FillFormats(); + FillDither(); +} + + +void CWaveConvert::OnChannelsChanged() +{ + SaveEncoderSettings(); + FillFormats(); + FillDither(); +} + + +void CWaveConvert::OnDitherChanged() +{ + SaveEncoderSettings(); +} + + +void CWaveConvert::OnFormatChanged() +{ + SaveEncoderSettings(); + FillDither(); + FillTags(); +} + + +void CWaveConvert::UpdateDialog() +{ + CheckDlgButton(IDC_CHECK2, (m_dwSongLimit) ? BST_CHECKED : 0); + GetDlgItem(IDC_EDIT2)->EnableWindow(m_dwSongLimit ? TRUE : FALSE); + + // Repeat / selection play + int sel = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3); + + GetDlgItem(IDC_EDIT3)->EnableWindow(sel == IDC_RADIO2); + GetDlgItem(IDC_EDIT4)->EnableWindow(sel == IDC_RADIO2); + m_SpinMinOrder.EnableWindow(sel == IDC_RADIO2); + m_SpinMaxOrder.EnableWindow(sel == IDC_RADIO2); + + GetDlgItem(IDC_EDIT5)->EnableWindow(sel == IDC_RADIO1); + m_SpinLoopCount.EnableWindow(sel == IDC_RADIO1); + + const SEQUENCEINDEX numSequences = m_SndFile.Order.GetNumSequences(); + const BOOL enableSeq = (numSequences > 1 && sel == IDC_RADIO3) ? TRUE : FALSE; + GetDlgItem(IDC_EDIT12)->EnableWindow(enableSeq); + GetDlgItem(IDC_EDIT13)->EnableWindow(enableSeq); + m_SpinMinSequence.EnableWindow(enableSeq); + m_SpinMaxSequence.EnableWindow(enableSeq); + + // No free slots => Cannot do instrument- or channel-based export to sample + BOOL canDoMultiExport = (IsDlgButtonChecked(IDC_RADIO4) != BST_UNCHECKED /* normal export */ || m_CbnSampleSlot.GetItemData(0) == 0 /* "free slot" is in list */) ? TRUE : FALSE; + GetDlgItem(IDC_CHECK4)->EnableWindow(canDoMultiExport); + GetDlgItem(IDC_CHECK6)->EnableWindow(canDoMultiExport); +} + + +void CWaveConvert::OnExportModeChanged() +{ + SaveEncoderSettings(); + bool sampleExport = (IsDlgButtonChecked(IDC_RADIO5) != BST_UNCHECKED); + m_CbnFileType.EnableWindow(sampleExport ? FALSE : TRUE); + m_CbnSampleSlot.EnableWindow(sampleExport && !IsDlgButtonChecked(IDC_CHECK4) && !IsDlgButtonChecked(IDC_CHECK6)); + if(sampleExport) + { + // Render to sample: Always use WAV + if(m_CbnFileType.GetCurSel() != 0) + { + m_CbnFileType.SetCurSel(0); + OnFileTypeChanged(); + } + } + FillChannels(); + FillFormats(); + FillDither(); + FillTags(); +} + + +void CWaveConvert::OnSampleSlotChanged() +{ + CheckRadioButton(IDC_RADIO4, IDC_RADIO5, IDC_RADIO5); + // When choosing a specific sample slot, we cannot use per-channel or per-instrument export + int sel = m_CbnSampleSlot.GetCurSel(); + if(sel >= 0 && m_CbnSampleSlot.GetItemData(sel) > 0) + { + CheckDlgButton(IDC_CHECK4, BST_UNCHECKED); + CheckDlgButton(IDC_CHECK6, BST_UNCHECKED); + } + UpdateDialog(); +} + + +void CWaveConvert::OnPlayerOptions() +{ + CPropertySheet dlg(_T("Mixer Settings"), this); + COptionsMixer mixerpage; + dlg.AddPage(&mixerpage); +#if !defined(NO_REVERB) || !defined(NO_DSP) || !defined(NO_EQ) || !defined(NO_AGC) + COptionsPlayer dsppage; + dlg.AddPage(&dsppage); +#endif + dlg.DoModal(); +} + + +void CWaveConvert::OnCheckTimeLimit() +{ + if (IsDlgButtonChecked(IDC_CHECK2)) + { + m_dwSongLimit = GetDlgItemInt(IDC_EDIT2, NULL, FALSE); + if (!m_dwSongLimit) + { + m_dwSongLimit = 600; + SetDlgItemText(IDC_EDIT2, _T("600")); + } + } else m_dwSongLimit = 0; + UpdateDialog(); +} + + +// Channel render is mutually exclusive with instrument render +void CWaveConvert::OnCheckChannelMode() +{ + if(IsDlgButtonChecked(IDC_CHECK4) != BST_UNCHECKED) + { + CheckDlgButton(IDC_CHECK6, BST_UNCHECKED); + m_CbnSampleSlot.SetCurSel(0); + } + UpdateDialog(); +} + + +// Channel render is mutually exclusive with instrument render +void CWaveConvert::OnCheckInstrMode() +{ + if(IsDlgButtonChecked(IDC_CHECK6) != BST_UNCHECKED) + { + CheckDlgButton(IDC_CHECK4, BST_UNCHECKED); + m_CbnSampleSlot.SetCurSel(0); + } + UpdateDialog(); +} + + +void CWaveConvert::OnOK() +{ + if (m_dwSongLimit) m_dwSongLimit = GetDlgItemInt(IDC_EDIT2, NULL, FALSE); + + const bool selection = IsDlgButtonChecked(IDC_RADIO2) != BST_UNCHECKED; + if(selection) + { + // Play selection + m_Settings.minOrder = static_cast<ORDERINDEX>(GetDlgItemInt(IDC_EDIT3, NULL, FALSE)); + m_Settings.maxOrder = static_cast<ORDERINDEX>(GetDlgItemInt(IDC_EDIT4, NULL, FALSE)); + if(m_Settings.minOrder > m_Settings.maxOrder) + std::swap(m_Settings.minOrder, m_Settings.maxOrder); + } else + { + m_Settings.minOrder = m_Settings.maxOrder = ORDERINDEX_INVALID; + } + if(IsDlgButtonChecked(IDC_RADIO3)) + { + const UINT maxSequence = m_SndFile.Order.GetNumSequences(); + m_Settings.minSequence = static_cast<SEQUENCEINDEX>(std::clamp(GetDlgItemInt(IDC_EDIT12, NULL, FALSE), 1u, maxSequence) - 1u); + m_Settings.maxSequence = static_cast<SEQUENCEINDEX>(std::clamp(GetDlgItemInt(IDC_EDIT13, NULL, FALSE), 1u, maxSequence) - 1u); + if(m_Settings.minSequence > m_Settings.maxSequence) + std::swap(m_Settings.minSequence, m_Settings.maxSequence); + } else + { + m_Settings.minSequence = m_Settings.maxSequence = m_SndFile.Order.GetCurrentSequenceIndex(); + } + + m_Settings.repeatCount = static_cast<uint16>(GetDlgItemInt(IDC_EDIT5, NULL, FALSE)); + m_Settings.normalize = IsDlgButtonChecked(IDC_CHECK5) != BST_UNCHECKED; + m_Settings.silencePlugBuffers = IsDlgButtonChecked(IDC_RENDERSILENCE) != BST_UNCHECKED; + m_Settings.outputToSample = IsDlgButtonChecked(IDC_RADIO5) != BST_UNCHECKED; + m_bGivePlugsIdleTime = IsDlgButtonChecked(IDC_GIVEPLUGSIDLETIME) != BST_UNCHECKED; + if (m_bGivePlugsIdleTime) + { + static bool showWarning = true; + if(showWarning && Reporting::Confirm("You only need slow render if you are experiencing dropped notes with a Kontakt based sampler with Direct-From-Disk enabled, or buggy plugins that use the system time for parameter automation.\nIt will make rendering *very* slow.\n\nAre you sure you want to enable slow render?", + "Really enable slow render?") == cnfNo) + { + m_bGivePlugsIdleTime = false; + } else + { + showWarning = false; + } + } + + m_bChannelMode = IsDlgButtonChecked(IDC_CHECK4) != BST_UNCHECKED; + m_bInstrumentMode= IsDlgButtonChecked(IDC_CHECK6) != BST_UNCHECKED; + + m_Settings.sampleSlot = static_cast<SAMPLEINDEX>(m_CbnSampleSlot.GetItemData(m_CbnSampleSlot.GetCurSel())); + + SaveEncoderSettings(); + + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + + m_Settings.Tags = FileTags(); + + m_Settings.Tags.SetEncoder(); + + if(encSettings.Tags) + { + CString tmp; + + m_EditTitle.GetWindowText(tmp); + m_Settings.Tags.title = mpt::ToUnicode(tmp); + + m_EditAuthor.GetWindowText(tmp); + m_Settings.Tags.artist = mpt::ToUnicode(tmp); + + m_EditAlbum.GetWindowText(tmp); + m_Settings.Tags.album = mpt::ToUnicode(tmp); + + m_EditURL.GetWindowText(tmp); + m_Settings.Tags.url = mpt::ToUnicode(tmp); + + if((encTraits->modesWithFixedGenres & encSettings.Mode) && !encTraits->genres.empty()) + { + m_CbnGenre.GetWindowText(tmp); + m_Settings.Tags.genre = mpt::ToUnicode(tmp); + } else + { + m_EditGenre.GetWindowText(tmp); + m_Settings.Tags.genre = mpt::ToUnicode(tmp); + } + + m_EditYear.GetWindowText(tmp); + m_Settings.Tags.year = mpt::ToUnicode(tmp); + if(m_Settings.Tags.year == U_("0")) + { + m_Settings.Tags.year = mpt::ustring(); + } + + if(!m_SndFile.m_songMessage.empty()) + { + m_Settings.Tags.comments = mpt::ToUnicode(mpt::Charset::Locale, m_SndFile.m_songMessage.GetFormatted(SongMessage::leLF)); + } + + m_Settings.Tags.bpm = mpt::ufmt::val(m_SndFile.GetCurrentBPM()); + + SaveTags(); + + } + + CDialog::OnOK(); + +} + + +void CWaveConvert::SaveEncoderSettings() +{ + EncoderSettingsConf &encSettings = m_Settings.GetEncoderSettings(); + + encSettings.Samplerate = static_cast<uint32>(m_CbnSampleRate.GetItemData(m_CbnSampleRate.GetCurSel())); + encSettings.Channels = static_cast<uint16>(m_CbnChannels.GetItemData(m_CbnChannels.GetCurSel())); + DWORD_PTR dwFormat = m_CbnSampleFormat.GetItemData(m_CbnSampleFormat.GetCurSel()); + + if(encTraits->modes & Encoder::ModeLossless) + { + int format = (int)((dwFormat >> 0) & 0xffffff); + encSettings.Dither = static_cast<int>(m_CbnDither.GetItemData(m_CbnDither.GetCurSel())); + encSettings.Format2 = Encoder::Format::FromInt(format); + encSettings.Mode = Encoder::ModeLossless; + encSettings.Bitrate = 0; + encSettings.Quality = encTraits->defaultQuality; + } else + { + encSettings.Dither = static_cast<int>(m_CbnDither.GetItemData(m_CbnDither.GetCurSel())); + Encoder::Mode mode = (Encoder::Mode)((dwFormat >> 24) & 0xff); + int quality = (int)((dwFormat >> 0) & 0xff); + int bitrate = (int)((dwFormat >> 0) & 0xffff); + encSettings.Mode = mode; + encSettings.Bitrate = bitrate; + encSettings.Quality = static_cast<float>(quality) * 0.01f; + encSettings.Format2 = { Encoder::Format::Encoding::Float, 32, mpt::get_endian() }; + } + + encSettings.Cues = IsDlgButtonChecked(IDC_CHECK3) ? true : false; + + encSettings.Tags = IsDlgButtonChecked(IDC_CHECK7) ? true : false; + +} + + +std::size_t CWaveConvertSettings::FindEncoder(const mpt::ustring &name) const +{ + for(std::size_t i = 0; i < EncoderFactories.size(); ++i) + { + if(EncoderFactories[i]->GetTraits().encoderSettingsName == name) + { + return i; + } + } + return 0; +} + + +void CWaveConvertSettings::SelectEncoder(std::size_t index) +{ + MPT_ASSERT(!EncoderFactories.empty()); + MPT_ASSERT(index < EncoderFactories.size()); + EncoderIndex = index; + EncoderName = EncoderFactories[EncoderIndex]->GetTraits().encoderSettingsName; +} + + +EncoderFactoryBase *CWaveConvertSettings::GetEncoderFactory() const +{ + MPT_ASSERT(!EncoderFactories.empty()); + return EncoderFactories[EncoderIndex]; +} + + +const Encoder::Traits *CWaveConvertSettings::GetTraits() const +{ + MPT_ASSERT(!EncoderFactories.empty()); + return &EncoderFactories[EncoderIndex]->GetTraits(); +} + + +EncoderSettingsConf &CWaveConvertSettings::GetEncoderSettings() const +{ + MPT_ASSERT(!EncoderSettings.empty()); + return *(EncoderSettings[EncoderIndex]); +} + + +Encoder::Settings CWaveConvertSettings::GetEncoderSettingsWithDetails() const +{ + MPT_ASSERT(!EncoderSettings.empty()); + Encoder::Settings settings = static_cast<Encoder::Settings>(*(EncoderSettings[EncoderIndex])); + settings.Details = static_cast<Encoder::StreamSettings>(TrackerSettings::Instance().ExportStreamEncoderSettings); + return settings; +} + + +CWaveConvertSettings::CWaveConvertSettings(SettingsContainer &conf, const std::vector<EncoderFactoryBase*> &encFactories) + : EncoderFactories(encFactories) + , EncoderName(conf, U_("Export"), U_("Encoder"), U_("")) + , EncoderIndex(FindEncoder(EncoderName)) + , storedTags(conf) + , repeatCount(0) + , minOrder(ORDERINDEX_INVALID), maxOrder(ORDERINDEX_INVALID) + , sampleSlot(0) + , normalize(false) + , silencePlugBuffers(false) + , outputToSample(false) +{ + Tags.SetEncoder(); + for(const auto & factory : EncoderFactories) + { + const Encoder::Traits &encTraits = factory->GetTraits(); + EncoderSettings.push_back( + std::make_unique<EncoderSettingsConf>( + conf, + encTraits.encoderSettingsName, + encTraits.canCues, + encTraits.canTags, + encTraits.defaultSamplerate, + encTraits.defaultChannels, + encTraits.defaultMode, + encTraits.defaultBitrate, + encTraits.defaultQuality, + encTraits.defaultFormat, + encTraits.defaultDitherType + ) + ); + } + SelectEncoder(EncoderIndex); +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// CDoWaveConvert: save a mod as a wave file + +void CDoWaveConvert::Run() +{ + + UINT ok = IDOK; + uint64 ullSamples = 0; + + std::vector<float> normalizeBufferData; + float *normalizeBuffer = nullptr; + float normalizePeak = 0.0f; + const mpt::PathString normalizeFileName = mpt::CreateTempFileName(P_("OpenMPT")); + std::optional<mpt::fstream> normalizeFile; + if(m_Settings.normalize) + { + normalizeBufferData.resize(MIXBUFFERSIZE * 4); + normalizeBuffer = normalizeBufferData.data(); + // Ensure this temporary file is marked as temporary in the file system, to increase the chance it will never be written to disk + if(HANDLE hFile = ::CreateFile(normalizeFileName.AsNative().c_str(), GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL); hFile != INVALID_HANDLE_VALUE) + { + ::CloseHandle(hFile); + } + normalizeFile.emplace(normalizeFileName, std::ios::binary | std::ios::in | std::ios::out | std::ios::trunc); + } + + const Encoder::Settings encSettings = m_Settings.GetEncoderSettingsWithDetails(); + const uint32 samplerate = encSettings.Samplerate; + const uint16 channels = encSettings.Channels; + + ASSERT(m_Settings.GetEncoderFactory() && m_Settings.GetEncoderFactory()->IsAvailable()); + + // Silence mix buffer of plugins, for plugins that don't clear their reverb buffers and similar stuff when they are reset +#ifndef NO_PLUGINS + if(m_Settings.silencePlugBuffers) + { + SetText(_T("Clearing plugin buffers")); + for(auto &plug : m_SndFile.m_MixPlugins) + { + if(plug.pMixPlugin != nullptr) + { + // Render up to 20 seconds per plugin + for(int j = 0; j < 20; j++) + { + const float maxVal = plug.pMixPlugin->RenderSilence(samplerate); + if(maxVal <= FLT_EPSILON) + { + break; + } + } + + ProcessMessages(); + if(m_abort) + { + m_abort = false; + break; + } + } + } + } +#endif // NO_PLUGINS + + MixerSettings oldmixersettings = m_SndFile.m_MixerSettings; + MixerSettings mixersettings = TrackerSettings::Instance().GetMixerSettings(); + mixersettings.m_nMaxMixChannels = MAX_CHANNELS; // always use max mixing channels when rendering + mixersettings.gdwMixingFreq = samplerate; + mixersettings.gnChannels = channels; + m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP); + if(m_Settings.normalize) + { +#ifndef NO_AGC + mixersettings.DSPMask &= ~SNDDSP_AGC; +#endif + } + + DithersOpenMPT dithers(theApp.PRNG(), encSettings.Dither, encSettings.Channels); + + m_SndFile.ResetChannels(); + m_SndFile.SetMixerSettings(mixersettings); + m_SndFile.SetResamplerSettings(TrackerSettings::Instance().GetResamplerSettings()); + m_SndFile.InitPlayer(true); + + // Tags must be known at the stream start, + // so that the encoder class could write them before audio data if mandated by the format, + // otherwise they should just be cached by the encoder. + std::unique_ptr<IAudioStreamEncoder> fileEnc = m_Settings.GetEncoderFactory()->ConstructStreamEncoder(fileStream, encSettings, m_Settings.Tags); + + std::variant< + std::unique_ptr<std::array<double, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<float, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<int32, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<int24, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<int16, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<int8, MIXBUFFERSIZE * 4>>, + std::unique_ptr<std::array<uint8, MIXBUFFERSIZE * 4>>> bufferData; + union AnyBufferSamplePointer + { + double *float64; + float *float32; + int32 *int32; + int24 *int24; + int16 *int16; + int8 *int8; + uint8 *uint8; + void *any; + }; + AnyBufferSamplePointer buffer; + buffer.any = nullptr; + switch(fileEnc->GetSampleFormat()) + { + case SampleFormat::Float64: + bufferData = std::make_unique<std::array<double, MIXBUFFERSIZE * 4>>(); + buffer.float64 = std::get<0>(bufferData)->data(); + break; + case SampleFormat::Float32: + bufferData = std::make_unique<std::array<float, MIXBUFFERSIZE * 4>>(); + buffer.float32 = std::get<1>(bufferData)->data(); + break; + case SampleFormat::Int32: + bufferData = std::make_unique<std::array<int32, MIXBUFFERSIZE * 4>>(); + buffer.int32 = std::get<2>(bufferData)->data(); + break; + case SampleFormat::Int24: + bufferData = std::make_unique<std::array<int24, MIXBUFFERSIZE * 4>>(); + buffer.int24 = std::get<3>(bufferData)->data(); + break; + case SampleFormat::Int16: + bufferData = std::make_unique<std::array<int16, MIXBUFFERSIZE * 4>>(); + buffer.int16 = std::get<4>(bufferData)->data(); + break; + case SampleFormat::Int8: + bufferData = std::make_unique<std::array<int8, MIXBUFFERSIZE * 4>>(); + buffer.int8 = std::get<5>(bufferData)->data(); + break; + case SampleFormat::Unsigned8: + bufferData = std::make_unique<std::array<uint8, MIXBUFFERSIZE * 4>>(); + buffer.uint8 = std::get<6>(bufferData)->data(); + break; + } + + uint64 ullMaxSamples = uint64_max / (channels * ((fileEnc->GetSampleFormat().GetBitsPerSample()+7) / 8)); + if (m_dwSongLimit) + { + LimitMax(ullMaxSamples, m_dwSongLimit * samplerate); + } + + // Calculate maximum samples + uint64 max = m_dwSongLimit ? ullMaxSamples : uint64_max; + + // Reset song position tracking + m_SndFile.ResetPlayPos(); + m_SndFile.m_SongFlags.reset(SONG_PATTERNLOOP); + ORDERINDEX startOrder = 0; + GetLengthTarget target; + if(m_Settings.minOrder != ORDERINDEX_INVALID && m_Settings.maxOrder != ORDERINDEX_INVALID) + { + m_SndFile.SetRepeatCount(0); + startOrder = m_Settings.minOrder; + ORDERINDEX endOrder = m_Settings.maxOrder; + while(!m_SndFile.Order().IsValidPat(endOrder) && endOrder > startOrder) + { + endOrder--; + } + if(m_SndFile.Order().IsValidPat(endOrder)) + { + target = GetLengthTarget(endOrder, m_SndFile.Patterns[m_SndFile.Order()[endOrder]].GetNumRows() - 1); + } + target.StartPos(m_SndFile.Order.GetCurrentSequenceIndex(), startOrder, 0); + m_SndFile.m_nMaxOrderPosition = endOrder + 1; + } else + { + m_SndFile.SetRepeatCount(std::max(0, m_Settings.repeatCount - 1)); + } + uint64 l = mpt::saturate_round<uint64>(m_SndFile.GetLength(eNoAdjust, target).front().duration * samplerate * (1 + m_SndFile.GetRepeatCount())); + + m_SndFile.SetCurrentOrder(startOrder); + m_SndFile.GetLength(eAdjust, GetLengthTarget(startOrder, 0)); // adjust playback variables / visited rows vector + m_SndFile.m_PlayState.m_nCurrentOrder = startOrder; + + if (l < max) max = l; + + SetRange(0, max); + EnableTaskbarProgress(); + + // No pattern cue points yet + std::vector<PatternCuePoint> patternCuePoints; + patternCuePoints.reserve(m_SndFile.Order().size()); + m_SndFile.m_PatternCuePoints = &patternCuePoints; + + // Process the conversion + + // For calculating the remaining time + auto dwStartTime = timeGetTime(), prevTime = dwStartTime; + uint32 timeRemaining = 0; + + uint64 bytesWritten = 0; + + auto mainFrame = CMainFrame::GetMainFrame(); + mainFrame->PauseMod(); + m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PATTERNLOOP); + mainFrame->InitRenderer(&m_SndFile); + + for (UINT n = 0; ; n++) + { + UINT lRead = 0; + if(m_Settings.normalize) + { + lRead = ReadInterleaved(m_SndFile, normalizeBuffer, channels, MIXBUFFERSIZE, dithers); + } else + { + switch(fileEnc->GetSampleFormat()) + { + case SampleFormat::Float64: + lRead = ReadInterleaved(m_SndFile, buffer.float64, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Float32: + lRead = ReadInterleaved(m_SndFile, buffer.float32, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Int32: + lRead = ReadInterleaved(m_SndFile, buffer.int32, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Int24: + lRead = ReadInterleaved(m_SndFile, buffer.int24, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Int16: + lRead = ReadInterleaved(m_SndFile, buffer.int16, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Int8: + lRead = ReadInterleaved(m_SndFile, buffer.int8, channels, MIXBUFFERSIZE, dithers); + break; + case SampleFormat::Unsigned8: + lRead = ReadInterleaved(m_SndFile, buffer.uint8, channels, MIXBUFFERSIZE, dithers); + break; + } + } + + // Process cue points (add base offset), if there are any to process. + for(auto iter = patternCuePoints.rbegin(); iter != patternCuePoints.rend(); ++iter) + { + if(iter->processed) + { + // From this point, all cues have already been processed. + break; + } + iter->offset += ullSamples; + iter->processed = true; + } + + if (m_bGivePlugsIdleTime) + { + Sleep(20); + } + + if (!lRead) + break; + ullSamples += lRead; + + if(m_Settings.normalize) + { + + std::size_t countSamples = lRead * m_SndFile.m_MixerSettings.gnChannels; + const float *src = normalizeBuffer; + while(countSamples--) + { + const float val = *src; + if(val > normalizePeak) normalizePeak = val; + else if(0.0f - val >= normalizePeak) normalizePeak = 0.0f - val; + src++; + } + + if(!mpt::IO::WriteRaw(*normalizeFile, mpt::as_span(reinterpret_cast<const std::byte*>(normalizeBuffer), lRead * m_SndFile.m_MixerSettings.gnChannels * sizeof(float)))) + { + break; + } + + } else + { + + const std::streampos oldPos = fileStream.tellp(); + switch(fileEnc->GetSampleFormat()) + { + case SampleFormat::Float64: + fileEnc->WriteInterleaved(lRead, buffer.float64); + break; + case SampleFormat::Float32: + fileEnc->WriteInterleaved(lRead, buffer.float32); + break; + case SampleFormat::Int32: + fileEnc->WriteInterleaved(lRead, buffer.int32); + break; + case SampleFormat::Int24: + fileEnc->WriteInterleaved(lRead, buffer.int24); + break; + case SampleFormat::Int16: + fileEnc->WriteInterleaved(lRead, buffer.int16); + break; + case SampleFormat::Int8: + fileEnc->WriteInterleaved(lRead, buffer.int8); + break; + case SampleFormat::Unsigned8: + fileEnc->WriteInterleaved(lRead, buffer.uint8); + break; + } + const std::streampos newPos = fileStream.tellp(); + bytesWritten += static_cast<uint64>(newPos - oldPos); + + if(!fileStream) + { + break; + } + + } + if(m_dwSongLimit && (ullSamples >= ullMaxSamples)) + { + break; + } + + auto currentTime = timeGetTime(); + if((currentTime - prevTime) >= 16) + { + prevTime = currentTime; + + DWORD seconds = (DWORD)(ullSamples / m_SndFile.m_MixerSettings.gdwMixingFreq); + + if((ullSamples > 0) && (ullSamples < max)) + { + timeRemaining = static_cast<uint32>((timeRemaining + ((currentTime - dwStartTime) * (max - ullSamples) / ullSamples) / 1000) / 2); + } + + if(m_Settings.normalize) + { + SetText(MPT_CFORMAT("Rendering {}... ({}mn{}s, {}mn{}s remaining)")(caption, seconds / 60, mpt::ufmt::dec0<2>(seconds % 60u), timeRemaining / 60, mpt::ufmt::dec0<2>(timeRemaining % 60u))); + } else + { + SetText(MPT_CFORMAT("Writing {}... ({}kB, {}mn{}s, {}mn{}s remaining)")(caption, bytesWritten >> 10, seconds / 60, mpt::ufmt::dec0<2>(seconds % 60u), timeRemaining / 60, mpt::ufmt::dec0<2>(timeRemaining % 60u))); + } + + SetProgress(ullSamples); + } + ProcessMessages(); + + if (m_abort) + { + ok = IDCANCEL; + break; + } + } + + m_SndFile.m_nMaxOrderPosition = 0; + + mainFrame->StopRenderer(&m_SndFile); + + if(m_Settings.normalize) + { + const float normalizeFactor = (normalizePeak != 0.0f) ? (1.0f / normalizePeak) : 1.0f; + + const uint64 framesTotal = ullSamples; + int lastPercent = -1; + + mpt::IO::SeekAbsolute(*normalizeFile, 0); + + uint64 framesProcessed = 0; + uint64 framesToProcess = framesTotal; + + SetRange(0, framesTotal); + + while(framesToProcess) + { + const std::size_t framesChunk = std::min(mpt::saturate_cast<std::size_t>(framesToProcess), std::size_t(MIXBUFFERSIZE)); + const uint32 samplesChunk = static_cast<uint32>(framesChunk * channels); + + const std::size_t bytes = samplesChunk * sizeof(float); + if(mpt::IO::ReadRaw(*normalizeFile, mpt::as_span(reinterpret_cast<std::byte*>(normalizeBuffer), bytes)).size() != bytes) + { + break; + } + + for(std::size_t i = 0; i < samplesChunk; ++i) + { + normalizeBuffer[i] *= normalizeFactor; + } + + const std::streampos oldPos = fileStream.tellp(); + std::visit( + [&](auto& ditherInstance) + { + switch(fileEnc->GetSampleFormat()) + { + case SampleFormat::Unsigned8: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<uint8>(buffer.uint8, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Int8: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<int8>(buffer.int8, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Int16: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<int16>(buffer.int16, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Int24: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<int24>(buffer.int24, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Int32: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<int32>(buffer.int32, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Float32: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<float>(buffer.float32, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + case SampleFormat::Float64: + ConvertBufferMixInternalToBuffer<false>(mpt::audio_span_interleaved<double>(buffer.float64, channels, framesChunk), mpt::audio_span_interleaved<const MixSampleFloat>(normalizeBuffer, channels, framesChunk), ditherInstance, channels, framesChunk); + break; + default: MPT_ASSERT_NOTREACHED(); break; + } + }, + dithers.Variant() + ); + switch(fileEnc->GetSampleFormat()) + { + case SampleFormat::Float64: + fileEnc->WriteInterleaved(framesChunk, buffer.float64); + break; + case SampleFormat::Float32: + fileEnc->WriteInterleaved(framesChunk, buffer.float32); + break; + case SampleFormat::Int32: + fileEnc->WriteInterleaved(framesChunk, buffer.int32); + break; + case SampleFormat::Int24: + fileEnc->WriteInterleaved(framesChunk, buffer.int24); + break; + case SampleFormat::Int16: + fileEnc->WriteInterleaved(framesChunk, buffer.int16); + break; + case SampleFormat::Int8: + fileEnc->WriteInterleaved(framesChunk, buffer.int8); + break; + case SampleFormat::Unsigned8: + fileEnc->WriteInterleaved(framesChunk, buffer.uint8); + break; + } + const std::streampos newPos = fileStream.tellp(); + bytesWritten += static_cast<std::size_t>(newPos - oldPos); + + auto currentTime = timeGetTime(); + if((currentTime - prevTime) >= 16) + { + prevTime = currentTime; + + int percent = static_cast<int>(100 * framesProcessed / framesTotal); + if(percent != lastPercent) + { + SetText(MPT_CFORMAT("Normalizing... ({}%)")(percent)); + SetProgress(framesProcessed); + lastPercent = percent; + } + ProcessMessages(); + } + + framesProcessed += framesChunk; + framesToProcess -= framesChunk; + } + + mpt::IO::Flush(*normalizeFile); + normalizeFile.reset(); + for(int retry=0; retry<10; retry++) + { + // stupid virus scanners + if(DeleteFile(normalizeFileName.AsNative().c_str()) != EACCES) + { + break; + } + Sleep(10); + } + + } + + if(!patternCuePoints.empty()) + { + if(encSettings.Cues) + { + std::vector<uint64> cues; + cues.reserve(patternCuePoints.size()); + for(const auto &cue : patternCuePoints) + { + cues.push_back(static_cast<uint32>(cue.offset)); + } + fileEnc->WriteCues(cues); + } + } + m_SndFile.m_PatternCuePoints = nullptr; + + fileEnc->WriteFinalize(); + + fileEnc = nullptr; + + CMainFrame::UpdateAudioParameters(m_SndFile, TRUE); + EndDialog(ok); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.cpp new file mode 100644 index 00000000..37757b7d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.cpp @@ -0,0 +1,675 @@ +/* + * ModConvert.cpp + * -------------- + * Purpose: Converting between various module formats. + * Notes : Incomplete list of MPTm-only features and extensions in the old formats: + * Features only available for MPTm: + * - User definable tunings. + * - Extended pattern range + * - Extended sequence + * - Multiple sequences ("songs") + * - Pattern-specific time signatures + * - Pattern effects :xy, S7D, S7E + * - Long instrument envelopes + * - Envelope release node (this was previously also usable in the IT format, but is now deprecated in that format) + * - Fractional tempo + * - Song-specific resampling + * - Alternative tempo modes (only usable in legacy XM / IT files) + * + * Extended features in IT/XM/S3M (not all listed below are available in all of those formats): + * - Plugins + * - Extended ranges for + * - Sample count + * - Instrument count + * - Pattern count + * - Sequence size + * - Row count + * - Channel count + * - Tempo limits + * - Extended sample/instrument properties. + * - MIDI mapping directives + * - Version info + * - Channel names + * - Pattern names + * - For more info, see e.g. SaveExtendedSongProperties(), SaveExtendedInstrumentProperties() + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Moddoc.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "../tracklib/SampleEdit.h" +#include "../soundlib/modsmp_ctrl.h" +#include "../soundlib/mod_specifications.h" +#include "ModConvert.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// Trim envelopes and remove release nodes. +static void UpdateEnvelopes(InstrumentEnvelope &mptEnv, const CModSpecifications &specs, std::bitset<wNumWarnings> &warnings) +{ + // shorten instrument envelope if necessary (for mod conversion) + const uint8 envMax = specs.envelopePointsMax; + +#define TRIMENV(envLen) if(envLen >= envMax) { envLen = envMax - 1; warnings.set(wTrimmedEnvelopes); } + + if(mptEnv.size() > envMax) + { + mptEnv.resize(envMax); + warnings.set(wTrimmedEnvelopes); + } + TRIMENV(mptEnv.nLoopStart); + TRIMENV(mptEnv.nLoopEnd); + TRIMENV(mptEnv.nSustainStart); + TRIMENV(mptEnv.nSustainEnd); + if(mptEnv.nReleaseNode != ENV_RELEASE_NODE_UNSET) + { + if(specs.hasReleaseNode) + { + TRIMENV(mptEnv.nReleaseNode); + } else + { + mptEnv.nReleaseNode = ENV_RELEASE_NODE_UNSET; + warnings.set(wReleaseNode); + } + } + +#undef TRIMENV +} + + +bool CModDoc::ChangeModType(MODTYPE nNewType) +{ + std::bitset<wNumWarnings> warnings; + warnings.reset(); + PATTERNINDEX nResizedPatterns = 0; + + const MODTYPE nOldType = m_SndFile.GetType(); + + if(nNewType == nOldType) + return true; + + const bool oldTypeIsXM = (nOldType == MOD_TYPE_XM), + oldTypeIsS3M = (nOldType == MOD_TYPE_S3M), oldTypeIsIT = (nOldType == MOD_TYPE_IT), + oldTypeIsMPT = (nOldType == MOD_TYPE_MPT), + oldTypeIsS3M_IT_MPT = (oldTypeIsS3M || oldTypeIsIT || oldTypeIsMPT), + oldTypeIsIT_MPT = (oldTypeIsIT || oldTypeIsMPT); + + const bool newTypeIsMOD = (nNewType == MOD_TYPE_MOD), newTypeIsXM = (nNewType == MOD_TYPE_XM), + newTypeIsS3M = (nNewType == MOD_TYPE_S3M), newTypeIsIT = (nNewType == MOD_TYPE_IT), + newTypeIsMPT = (nNewType == MOD_TYPE_MPT), newTypeIsMOD_XM = (newTypeIsMOD || newTypeIsXM), + newTypeIsIT_MPT = (newTypeIsIT || newTypeIsMPT); + + const CModSpecifications &specs = m_SndFile.GetModSpecifications(nNewType); + + // Check if conversion to 64 rows is necessary + for(const auto &pat : m_SndFile.Patterns) + { + if(pat.IsValid() && pat.GetNumRows() != 64) + nResizedPatterns++; + } + + if((m_SndFile.GetNumInstruments() || nResizedPatterns) && (nNewType & (MOD_TYPE_MOD|MOD_TYPE_S3M))) + { + if(Reporting::Confirm( + "This operation will convert all instruments to samples,\n" + "and resize all patterns to 64 rows.\n" + "Do you want to continue?", "Warning") != cnfYes) return false; + BeginWaitCursor(); + CriticalSection cs; + + // Converting instruments to samples + if(m_SndFile.GetNumInstruments()) + { + ConvertInstrumentsToSamples(); + warnings.set(wInstrumentsToSamples); + } + + // Resizing all patterns to 64 rows + for(auto &pat : m_SndFile.Patterns) if(pat.IsValid() && pat.GetNumRows() != 64) + { + ROWINDEX origRows = pat.GetNumRows(); + pat.Resize(64); + + if(origRows < 64) + { + // Try to save short patterns by inserting a pattern break. + pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(origRows - 1).RetryNextRow()); + } + + warnings.set(wResizedPatterns); + } + + // Removing all instrument headers from channels + for(auto &chn : m_SndFile.m_PlayState.Chn) + { + chn.pModInstrument = nullptr; + } + + for(INSTRUMENTINDEX nIns = 0; nIns <= m_SndFile.GetNumInstruments(); nIns++) + { + delete m_SndFile.Instruments[nIns]; + m_SndFile.Instruments[nIns] = nullptr; + } + m_SndFile.m_nInstruments = 0; + + EndWaitCursor(); + } //End if (((m_SndFile.m_nInstruments) || (b64)) && (nNewType & (MOD_TYPE_MOD|MOD_TYPE_S3M))) + BeginWaitCursor(); + + + ///////////////////////////// + // Converting pattern data + + // When converting to MOD, get the new sample transpose setting right here so that we can compensate notes in the pattern. + if(newTypeIsMOD && !oldTypeIsXM) + { + for(SAMPLEINDEX smp = 1; smp <= m_SndFile.GetNumSamples(); smp++) + { + m_SndFile.GetSample(smp).FrequencyToTranspose(); + } + } + + bool onlyAmigaNotes = true; + for(auto &pat : m_SndFile.Patterns) if(pat.IsValid()) + { + // This is used for -> MOD/XM conversion + std::vector<std::array<ModCommand::PARAM, MAX_EFFECTS>> effMemory(GetNumChannels()); + std::vector<ModCommand::VOL> volMemory(GetNumChannels(), 0); + std::vector<ModCommand::INSTR> instrMemory(GetNumChannels(), 0); + + bool addBreak = false; // When converting to XM, avoid the E60 bug. + CHANNELINDEX chn = 0; + ROWINDEX row = 0; + + for(auto m = pat.begin(); m != pat.end(); m++, chn++) + { + if(chn >= GetNumChannels()) + { + chn = 0; + row++; + } + + ModCommand::INSTR instr = m->instr; + if(m->instr) instrMemory[chn] = instr; + else instr = instrMemory[chn]; + + // Deal with volume column slide memory (it's not shared with the effect column) + if(oldTypeIsIT_MPT && (newTypeIsMOD_XM || newTypeIsS3M)) + { + switch(m->volcmd) + { + case VOLCMD_VOLSLIDEUP: + case VOLCMD_VOLSLIDEDOWN: + case VOLCMD_FINEVOLUP: + case VOLCMD_FINEVOLDOWN: + if(m->vol == 0) + m->vol = volMemory[chn]; + else + volMemory[chn] = m->vol; + break; + } + } + + // Deal with MOD/XM commands without effect memory + if(oldTypeIsS3M_IT_MPT && newTypeIsMOD_XM) + { + switch(m->command) + { + // No effect memory in XM / MOD + case CMD_ARPEGGIO: + case CMD_S3MCMDEX: + case CMD_MODCMDEX: + + // These have effect memory in XM, but it is spread over several commands (for fine and extra-fine slides), so the easiest way to fix this is to just always use the previous value. + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + case CMD_VOLUMESLIDE: + if(m->param == 0) + m->param = effMemory[chn][m->command]; + else + effMemory[chn][m->command] = m->param; + break; + } + } + + // Adjust effect memory for MOD files + if(newTypeIsMOD) + { + switch(m->command) + { + case CMD_PORTAMENTOUP: + case CMD_PORTAMENTODOWN: + case CMD_TONEPORTAVOL: + case CMD_VIBRATOVOL: + case CMD_VOLUMESLIDE: + // ProTracker doesn't have effect memory for these commands, so let's try to fix them + if(m->param == 0) + m->param = effMemory[chn][m->command]; + else + effMemory[chn][m->command] = m->param; + break; + + } + + // Compensate for loss of transpose information + if(m->IsNote() && instr && instr <= GetNumSamples()) + { + const int newNote = m->note + m_SndFile.GetSample(instr).RelativeTone; + m->note = static_cast<ModCommand::NOTE>(Clamp(newNote, specs.noteMin, specs.noteMax)); + } + if(!m->IsAmigaNote()) + { + onlyAmigaNotes = false; + } + } + + m->Convert(nOldType, nNewType, m_SndFile); + + // When converting to XM, avoid the E60 bug. + if(newTypeIsXM) + { + switch(m->command) + { + case CMD_MODCMDEX: + if(m->param == 0x60 && row > 0) + { + addBreak = true; + } + break; + case CMD_POSITIONJUMP: + case CMD_PATTERNBREAK: + addBreak = false; + break; + } + } + + // Fix Row Delay commands when converting between MOD/XM and S3M/IT. + // FT2 only considers the rightmost command, ST3/IT only the leftmost... + if((nOldType & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) && (nNewType & (MOD_TYPE_MOD | MOD_TYPE_XM)) + && m->command == CMD_MODCMDEX && (m->param & 0xF0) == 0xE0) + { + if(oldTypeIsIT_MPT || m->param != 0xE0) + { + // If the leftmost row delay command is SE0, ST3 ignores it, IT doesn't. + + // Delete all commands right of the first command + auto p = m + 1; + for(CHANNELINDEX c = chn + 1; c < m_SndFile.GetNumChannels(); c++, p++) + { + if(p->command == CMD_S3MCMDEX && (p->param & 0xF0) == 0xE0) + { + p->command = CMD_NONE; + } + } + } + } else if((nOldType & (MOD_TYPE_MOD | MOD_TYPE_XM)) && (nNewType & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) + && m->command == CMD_S3MCMDEX && (m->param & 0xF0) == 0xE0) + { + // Delete all commands left of the last command + auto p = m - 1; + for(CHANNELINDEX c = 0; c < chn; c++, p--) + { + if(p->command == CMD_S3MCMDEX && (p->param & 0xF0) == 0xE0) + { + p->command = CMD_NONE; + } + } + } + + } + if(addBreak) + { + pat.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(pat.GetNumRows() - 1)); + } + } + + //////////////////////////////////////////////// + // Converting instrument / sample / etc. data + + + // Do some sample conversion + const bool newTypeHasPingPongLoops = !(newTypeIsMOD || newTypeIsS3M); + for(SAMPLEINDEX smp = 1; smp <= m_SndFile.GetNumSamples(); smp++) + { + ModSample &sample = m_SndFile.GetSample(smp); + GetSampleUndo().PrepareUndo(smp, sundo_none, "Song Conversion"); + + // Too many samples? Only 31 samples allowed in MOD format... + if(newTypeIsMOD && smp > 31 && sample.nLength > 0) + { + warnings.set(wMOD31Samples); + } + + // No auto-vibrato in MOD/S3M + if((newTypeIsMOD || newTypeIsS3M) && (sample.nVibDepth | sample.nVibRate | sample.nVibSweep) != 0) + { + warnings.set(wSampleAutoVibrato); + } + + // No sustain loops for MOD/S3M/XM + bool ignoreLoopConversion = false; + if(newTypeIsMOD_XM || newTypeIsS3M) + { + // Sustain loops - convert to normal loops + if(sample.uFlags[CHN_SUSTAINLOOP]) + { + warnings.set(wSampleSustainLoops); + // Prepare conversion to regular loop + if(!newTypeHasPingPongLoops) + { + ignoreLoopConversion = true; + if(!SampleEdit::ConvertPingPongLoop(sample, m_SndFile, true)) + warnings.set(wSampleBidiLoops); + } + } + } + + // No ping-pong loops in MOD/S3M + if(!ignoreLoopConversion && !newTypeHasPingPongLoops && sample.HasPingPongLoop()) + { + if(!SampleEdit::ConvertPingPongLoop(sample, m_SndFile, false)) + warnings.set(wSampleBidiLoops); + } + + if(newTypeIsMOD && sample.RelativeTone != 0) + { + warnings.set(wMODSampleFrequency); + } + + if(!CSoundFile::SupportsOPL(nNewType) && sample.uFlags[CHN_ADLIB]) + { + warnings.set(wAdlibInstruments); + } + + sample.Convert(nOldType, nNewType); + } + + for(INSTRUMENTINDEX ins = 1; ins <= m_SndFile.GetNumInstruments(); ins++) + { + ModInstrument *pIns = m_SndFile.Instruments[ins]; + if(pIns == nullptr) + { + continue; + } + + // Convert IT/MPT to XM (fix instruments) + if(newTypeIsXM) + { + for(size_t i = 0; i < std::size(pIns->NoteMap); i++) + { + if (pIns->NoteMap[i] && pIns->NoteMap[i] != (i + 1)) + { + warnings.set(wBrokenNoteMap); + break; + } + } + // Convert sustain loops to sustain "points" + if(pIns->VolEnv.nSustainStart != pIns->VolEnv.nSustainEnd) + { + warnings.set(wInstrumentSustainLoops); + } + if(pIns->PanEnv.nSustainStart != pIns->PanEnv.nSustainEnd) + { + warnings.set(wInstrumentSustainLoops); + } + } + + + // Convert MPT to anything - remove instrument tunings, Pitch/Tempo Lock, filter variation + if(oldTypeIsMPT) + { + if(pIns->pTuning != nullptr) + { + warnings.set(wInstrumentTuning); + } + if(pIns->pitchToTempoLock.GetRaw() != 0) + { + warnings.set(wPitchToTempoLock); + } + if((pIns->nCutSwing | pIns->nResSwing) != 0) + { + warnings.set(wFilterVariation); + } + } + + pIns->Convert(nOldType, nNewType); + } + + if(newTypeIsMOD) + { + // Not supported in MOD format + auto firstPat = std::find_if(m_SndFile.Order().cbegin(), m_SndFile.Order().cend(), [this](PATTERNINDEX pat) { return m_SndFile.Patterns.IsValidPat(pat); }); + bool firstPatValid = firstPat != m_SndFile.Order().cend(); + bool lossy = false; + + if(m_SndFile.m_nDefaultSpeed != 6) + { + if(firstPatValid) + { + m_SndFile.Patterns[*firstPat].WriteEffect(EffectWriter(CMD_SPEED, ModCommand::PARAM(m_SndFile.m_nDefaultSpeed)).RetryNextRow()); + } + m_SndFile.m_nDefaultSpeed = 6; + lossy = true; + } + if(m_SndFile.m_nDefaultTempo != TEMPO(125, 0)) + { + if(firstPatValid) + { + m_SndFile.Patterns[*firstPat].WriteEffect(EffectWriter(CMD_TEMPO, ModCommand::PARAM(m_SndFile.m_nDefaultTempo.GetInt())).RetryNextRow()); + } + m_SndFile.m_nDefaultTempo.Set(125); + lossy = true; + } + if(m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME || m_SndFile.m_nSamplePreAmp != 48 || m_SndFile.m_nVSTiVolume != 48) + { + m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + m_SndFile.m_nSamplePreAmp = 48; + m_SndFile.m_nVSTiVolume = 48; + lossy = true; + } + if(lossy) + { + warnings.set(wMODGlobalVars); + } + } + + // Is the "restart position" value allowed in this format? + for(SEQUENCEINDEX seq = 0; seq < m_SndFile.Order.GetNumSequences(); seq++) + { + if(m_SndFile.Order(seq).GetRestartPos() > 0 && !specs.hasRestartPos) + { + // Try to fix it by placing a pattern jump command in the pattern. + if(!m_SndFile.Order.RestartPosToPattern(seq)) + { + // Couldn't fix it! :( + warnings.set(wRestartPos); + } + } + } + + // Fix channel settings (pan/vol) + for(CHANNELINDEX nChn = 0; nChn < GetNumChannels(); nChn++) + { + if(newTypeIsMOD_XM || newTypeIsS3M) + { + if(m_SndFile.ChnSettings[nChn].nVolume != 64 || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND]) + { + m_SndFile.ChnSettings[nChn].nVolume = 64; + m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND); + warnings.set(wChannelVolSurround); + } + } + if(newTypeIsXM) + { + if(m_SndFile.ChnSettings[nChn].nPan != 128) + { + m_SndFile.ChnSettings[nChn].nPan = 128; + warnings.set(wChannelPanning); + } + } + } + + // Check for patterns with custom time signatures (fixing will be applied in the pattern container) + if(!specs.hasPatternSignatures) + { + for(const auto &pat: m_SndFile.Patterns) + { + if(pat.GetOverrideSignature()) + { + warnings.set(wPatternSignatures); + break; + } + } + } + + // Check whether the new format supports embedding the edit history in the file. + if(oldTypeIsIT_MPT && !newTypeIsIT_MPT && GetSoundFile().GetFileHistory().size() > 0) + { + warnings.set(wEditHistory); + } + + if((nOldType & MOD_TYPE_XM) && m_SndFile.m_playBehaviour[kFT2VolumeRamping]) + { + warnings.set(wVolRamp); + } + + CriticalSection cs; + m_SndFile.ChangeModTypeTo(nNewType); + + // In case we need to update IT bidi loop handling pre-computation or loops got changed... + m_SndFile.PrecomputeSampleLoops(false); + + // Song flags + if(!(specs.songFlags & SONG_LINEARSLIDES) && m_SndFile.m_SongFlags[SONG_LINEARSLIDES]) + { + warnings.set(wLinearSlides); + } + if(oldTypeIsXM && newTypeIsIT_MPT) + { + m_SndFile.m_SongFlags.set(SONG_ITCOMPATGXX); + } else if(newTypeIsMOD && GetNumChannels() == 4 && onlyAmigaNotes) + { + m_SndFile.m_SongFlags.set(SONG_ISAMIGA); + m_SndFile.InitAmigaResampler(); + } + m_SndFile.m_SongFlags &= (specs.songFlags | SONG_PLAY_FLAGS); + + // Adjust mix levels + if(newTypeIsMOD || newTypeIsS3M) + { + m_SndFile.SetMixLevels(MixLevels::Compatible); + } + if(oldTypeIsMPT && m_SndFile.GetMixLevels() != MixLevels::Compatible && m_SndFile.GetMixLevels() != MixLevels::CompatibleFT2) + { + warnings.set(wMixmode); + } + + if(!specs.hasFractionalTempo && m_SndFile.m_nDefaultTempo.GetFract() != 0) + { + m_SndFile.m_nDefaultTempo.Set(m_SndFile.m_nDefaultTempo.GetInt(), 0); + warnings.set(wFractionalTempo); + } + + ChangeFileExtension(nNewType); + + // Check mod specifications + Limit(m_SndFile.m_nDefaultTempo, specs.GetTempoMin(), specs.GetTempoMax()); + Limit(m_SndFile.m_nDefaultSpeed, specs.speedMin, specs.speedMax); + + for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++) if(m_SndFile.Instruments[i] != nullptr) + { + UpdateEnvelopes(m_SndFile.Instruments[i]->VolEnv, specs, warnings); + UpdateEnvelopes(m_SndFile.Instruments[i]->PanEnv, specs, warnings); + UpdateEnvelopes(m_SndFile.Instruments[i]->PitchEnv, specs, warnings); + } + + // XM requires instruments, so we create them right away. + if(newTypeIsXM && GetNumInstruments() == 0) + { + ConvertSamplesToInstruments(); + } + + // XM has no global volume + if(newTypeIsXM && m_SndFile.m_nDefaultGlobalVolume != MAX_GLOBAL_VOLUME) + { + if(!GlobalVolumeToPattern()) + { + warnings.set(wGlobalVolumeNotSupported); + } + } + + // Resampling is only saved in MPTM + if(!newTypeIsMPT && m_SndFile.m_nResampling != SRCMODE_DEFAULT) + { + warnings.set(wResamplingMode); + m_SndFile.m_nResampling = SRCMODE_DEFAULT; + } + + cs.Leave(); + + if(warnings[wResizedPatterns]) + { + AddToLog(LogInformation, MPT_UFORMAT("{} patterns have been resized to 64 rows")(nResizedPatterns)); + } + static constexpr struct + { + ConversionWarning warning; + const char *mesage; + } messages[] = + { + // Pattern warnings + { wRestartPos, "Restart position is not supported by the new format." }, + { wPatternSignatures, "Pattern-specific time signatures are not supported by the new format." }, + { wChannelVolSurround, "Channel volume and surround are not supported by the new format." }, + { wChannelPanning, "Channel panning is not supported by the new format." }, + // Sample warnings + { wSampleBidiLoops, "Sample bidi loops are not supported by the new format." }, + { wSampleSustainLoops, "New format doesn't support sample sustain loops." }, + { wSampleAutoVibrato, "New format doesn't support sample autovibrato." }, + { wMODSampleFrequency, "Sample C-5 frequencies will be lost." }, + { wMOD31Samples, "Samples above 31 will be lost when saving as MOD. Consider rearranging samples if there are unused slots available." }, + { wAdlibInstruments, "OPL instruments are not supported by this format." }, + // Instrument warnings + { wInstrumentsToSamples, "All instruments have been converted to samples." }, + { wTrimmedEnvelopes, "Instrument envelopes have been shortened." }, + { wInstrumentSustainLoops, "Sustain loops were converted to sustain points." }, + { wInstrumentTuning, "Instrument tunings will be lost." }, + { wPitchToTempoLock, "Pitch / Tempo Lock instrument property is not supported by the new format." }, + { wBrokenNoteMap, "Instrument Note Mapping is not supported by the new format." }, + { wReleaseNode, "Instrument envelope release nodes are not supported by the new format." }, + { wFilterVariation, "Random filter variation is not supported by the new format." }, + // General warnings + { wMODGlobalVars, "Default speed, tempo and global volume will be lost." }, + { wLinearSlides, "Linear Frequency Slides not supported by the new format." }, + { wEditHistory, "Edit history will not be saved in the new format." }, + { wMixmode, "Consider setting the mix levels to \"Compatible\" in the song properties when working with legacy formats." }, + { wVolRamp, "Fasttracker 2 compatible super soft volume ramping gets lost when converting XM files to another type." }, + { wGlobalVolumeNotSupported, "Default global volume is not supported by the new format." }, + { wResamplingMode, "Song-specific resampling mode is not supported by the new format." }, + { wFractionalTempo, "Fractional tempo is not supported by the new format." }, + }; + for(const auto &msg : messages) + { + if(warnings[msg.warning]) + AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::UTF8, msg.mesage)); + } + + SetModified(); + GetPatternUndo().ClearUndo(); + UpdateAllViews(nullptr, GeneralHint().General().ModType()); + EndWaitCursor(); + + // Update effect key commands + CMainFrame::GetInputHandler()->SetEffectLetters(m_SndFile.GetModSpecifications()); + + return true; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.h b/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.h new file mode 100644 index 00000000..655e3d18 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ModConvert.h @@ -0,0 +1,50 @@ +/* + * ModConvert.h + * ------------ + * Purpose: Converting between various module formats. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +// Warning types +enum ConversionWarning +{ + wInstrumentsToSamples = 0, + wResizedPatterns, + wSampleBidiLoops, + wSampleSustainLoops, + wSampleAutoVibrato, + wMODSampleFrequency, + wBrokenNoteMap, + wInstrumentSustainLoops, + wInstrumentTuning, + wMODGlobalVars, + wMOD31Samples, + wAdlibInstruments, + wRestartPos, + wChannelVolSurround, + wChannelPanning, + wPatternSignatures, + wLinearSlides, + wTrimmedEnvelopes, + wReleaseNode, + wEditHistory, + wMixmode, + wVolRamp, + wPitchToTempoLock, + wGlobalVolumeNotSupported, + wFilterVariation, + wResamplingMode, + wFractionalTempo, + wNumWarnings +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.cpp new file mode 100644 index 00000000..67cf4745 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.cpp @@ -0,0 +1,240 @@ +/* + * ModDocTemplate.cpp + * ------------------ + * Purpose: CDocTemplate and CModDocManager specialization for CModDoc. + * 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 "FolderScanner.h" +#include "Mainfrm.h" +#include "Moddoc.h" +#include "ModDocTemplate.h" +#include "Reporting.h" +#include "SelectPluginDialog.h" +#include "../soundlib/plugins/PluginManager.h" + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MPT_ALL_LOGGING +#define DDEDEBUG +#endif + + +CDocument *CModDocTemplate::OpenDocumentFile(LPCTSTR lpszPathName, BOOL addToMru, BOOL makeVisible) +{ + const mpt::PathString filename = (lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString()); + + // First, remove document from MRU list. + if(addToMru) + { + theApp.RemoveMruItem(filename); + } + + CDocument *pDoc = CMultiDocTemplate::OpenDocumentFile(filename.empty() ? nullptr : filename.ToCString().GetString(), addToMru, makeVisible); + if(pDoc) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->OnDocumentCreated(static_cast<CModDoc *>(pDoc)); + } else if(!filename.empty() && CMainFrame::GetMainFrame() && addToMru) + { + // Opening the document failed + CMainFrame::GetMainFrame()->UpdateMRUList(); + } + return pDoc; +} + + +CDocument *CModDocTemplate::OpenTemplateFile(const mpt::PathString &filename, bool isExampleTune) +{ + CDocument *doc = OpenDocumentFile(filename.ToCString(), isExampleTune ? TRUE : FALSE, TRUE); + if(doc) + { + CModDoc *modDoc = static_cast<CModDoc *>(doc); + // Clear path so that saving will not take place in templates/examples folder. + modDoc->ClearFilePath(); + if(!isExampleTune) + { + CMultiDocTemplate::SetDefaultTitle(modDoc); + m_nUntitledCount++; + // Name has changed... + CMainFrame::GetMainFrame()->UpdateTree(modDoc, GeneralHint().General()); + + // Reset edit history for template files + CSoundFile &sndFile = modDoc->GetSoundFile(); + sndFile.GetFileHistory().clear(); + sndFile.m_dwCreatedWithVersion = Version::Current(); + sndFile.m_dwLastSavedWithVersion = Version(); + sndFile.m_modFormat = ModFormatDetails(); + sndFile.m_songArtist = TrackerSettings::Instance().defaultArtist; + if(sndFile.GetType() != MOD_TYPE_MPT) + { + // Always enforce most compatible playback for legacy module types + sndFile.m_playBehaviour = sndFile.GetDefaultPlaybackBehaviour(sndFile.GetType()); + } + doc->UpdateAllViews(nullptr, UpdateHint().ModType().AsLPARAM()); + } else + { + // Remove extension from title, so that saving the file will not suggest a filename like e.g. "example.it.it". + const CString title = modDoc->GetTitle(); + const int dotPos = title.ReverseFind(_T('.')); + if(dotPos >= 0) + { + modDoc->SetTitle(title.Left(dotPos)); + } + } + } + return doc; +} + + +void CModDocTemplate::AddDocument(CDocument *doc) +{ + CMultiDocTemplate::AddDocument(doc); + m_documents.insert(static_cast<CModDoc *>(doc)); +} + + +void CModDocTemplate::RemoveDocument(CDocument *doc) +{ + CMultiDocTemplate::RemoveDocument(doc); + m_documents.erase(static_cast<CModDoc *>(doc)); +} + + +bool CModDocTemplate::DocumentExists(const CModDoc *doc) const +{ + return m_documents.count(const_cast<CModDoc *>(doc)) != 0; +} + + +CDocument *CModDocManager::OpenDocumentFile(LPCTSTR lpszFileName, BOOL bAddToMRU) +{ + const mpt::PathString filename = (lpszFileName ? mpt::PathString::FromCString(lpszFileName) : mpt::PathString()); + + if(filename.IsDirectory()) + { + FolderScanner scanner(filename, FolderScanner::kOnlyFiles | FolderScanner::kFindInSubDirectories); + mpt::PathString file; + CDocument *pDoc = nullptr; + while(scanner.Next(file)) + { + pDoc = OpenDocumentFile(file.ToCString(), bAddToMRU); + } + return pDoc; + } + + if(const auto fileExt = filename.GetFileExt(); !mpt::PathString::CompareNoCase(fileExt, P_(".dll")) || !mpt::PathString::CompareNoCase(fileExt, P_(".vst3"))) + { + if(auto plugManager = theApp.GetPluginManager(); plugManager != nullptr) + { + if(auto plugLib = plugManager->AddPlugin(filename, TrackerSettings::Instance().BrokenPluginsWorkaroundVSTMaskAllCrashes); plugLib != nullptr) + { + if(!CSelectPluginDlg::VerifyPlugin(plugLib, nullptr)) + { + plugManager->RemovePlugin(plugLib); + } + return nullptr; + } + } + } + + CDocument *pDoc = CDocManager::OpenDocumentFile(lpszFileName, bAddToMRU); + if(pDoc == nullptr && !filename.empty()) + { + if(!filename.IsFile()) + { + Reporting::Error(MPT_CFORMAT("Unable to open \"{}\": file does not exist.")(filename.ToCString())); + theApp.RemoveMruItem(filename); + CMainFrame::GetMainFrame()->UpdateMRUList(); + } else + { + // Case: Valid path but opening failed. + const int numDocs = theApp.GetOpenDocumentCount(); + Reporting::Notification(MPT_CFORMAT("Opening \"{}\" failed. This can happen if " + "no more modules can be opened or if the file type was not " + "recognised (currently there {} {} document{} open).")( + filename.ToCString(), (numDocs == 1) ? CString(_T("is")) : CString(_T("are")), numDocs, (numDocs == 1) ? CString(_T("")) : CString(_T("s")))); + } + } + return pDoc; +} + + +BOOL CModDocManager::OnDDECommand(LPTSTR lpszCommand) +{ + BOOL bResult, bActivate; +#ifdef DDEDEBUG + MPT_LOG_GLOBAL(LogDebug, "DDE", U_("OnDDECommand: ") + mpt::ToUnicode(mpt::winstring(lpszCommand))); +#endif + // Handle any DDE commands recognized by your application + // and return TRUE. See implementation of CWinApp::OnDDEComand + // for example of parsing the DDE command string. + bResult = FALSE; + bActivate = FALSE; + if ((lpszCommand) && lpszCommand[0] && (theApp.m_pMainWnd)) + { + std::size_t len = _tcslen(lpszCommand); + std::vector<TCHAR> s(lpszCommand, lpszCommand + len + 1); + + len--; + while((len > 0) && _tcschr(_T("(){}[]\'\" "), s[len])) + { + s[len--] = 0; + } + TCHAR *pszCmd = s.data(); + while (pszCmd[0] == _T('[')) pszCmd++; + TCHAR *pszData = pszCmd; + while ((pszData[0] != _T('(')) && (pszData[0])) + { + if (((BYTE)pszData[0]) <= (BYTE)' ') *pszData = 0; + pszData++; + } + while ((*pszData) && (_tcschr(_T("(){}[]\'\" "), *pszData))) + { + *pszData = 0; + pszData++; + } + // Edit/Open + if ((!lstrcmpi(pszCmd, _T("Edit"))) + || (!lstrcmpi(pszCmd, _T("Open")))) + { + if (pszData[0]) + { + bResult = TRUE; + bActivate = TRUE; + OpenDocumentFile(pszData); + } + } else + // New + if (!lstrcmpi(pszCmd, _T("New"))) + { + OpenDocumentFile(_T("")); + bResult = TRUE; + bActivate = TRUE; + } + #ifdef DDEDEBUG + MPT_LOG_GLOBAL(LogDebug, "DDE", MPT_UFORMAT("{}({})")(mpt::winstring(pszCmd), mpt::winstring(pszData))); + #endif + if ((bActivate) && (theApp.m_pMainWnd->m_hWnd)) + { + if (theApp.m_pMainWnd->IsIconic()) theApp.m_pMainWnd->ShowWindow(SW_RESTORE); + theApp.m_pMainWnd->SetActiveWindow(); + } + } + // Return FALSE for any DDE commands you do not handle. +#ifdef DDEDEBUG + if (!bResult) + { + MPT_LOG_GLOBAL(LogDebug, "DDE", U_("WARNING: failure in CModDocManager::OnDDECommand()")); + } +#endif + return bResult; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.h b/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.h new file mode 100644 index 00000000..53f7ae61 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ModDocTemplate.h @@ -0,0 +1,57 @@ +/* + * ModDocTemplate.h + * ---------------- + * Purpose: CDocTemplate and CModDocManager specializations for CModDoc. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <unordered_set> + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +namespace mpt { class PathString; } + +class CModDocTemplate: public CMultiDocTemplate +{ + std::unordered_set<CModDoc *> m_documents; // Allow faster lookup of open documents than MFC's linear search allows for + +public: + CModDocTemplate(UINT nIDResource, CRuntimeClass* pDocClass, CRuntimeClass* pFrameClass, CRuntimeClass* pViewClass): + CMultiDocTemplate(nIDResource, pDocClass, pFrameClass, pViewClass) {} + + CDocument* OpenTemplateFile(const mpt::PathString &filename, bool isExampleTune = false); + + CDocument* OpenDocumentFile(LPCTSTR lpszPathName, BOOL addToMru = TRUE, BOOL makeVisible = TRUE) override; + + void AddDocument(CDocument *doc) override; + void RemoveDocument(CDocument *doc) override; + bool DocumentExists(const CModDoc *doc) const; + + size_t size() const { return m_documents.size(); } + bool empty() const { return m_documents.empty(); } + auto begin() { return m_documents.begin(); } + auto begin() const { return m_documents.begin(); } + auto cbegin() const { return m_documents.cbegin(); } + auto end() { return m_documents.end(); } + auto end() const { return m_documents.end(); } + auto cend() const { return m_documents.cend(); } +}; + + +class CModDocManager : public CDocManager +{ +public: + CDocument *OpenDocumentFile(LPCTSTR lpszFileName, BOOL bAddToMRU = TRUE) override; + BOOL OnDDECommand(LPTSTR lpszCommand) override; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp new file mode 100644 index 00000000..f2c7ac44 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.cpp @@ -0,0 +1,3361 @@ +/* + * Moddoc.cpp + * ---------- + * Purpose: Module document handling in OpenMPT. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Mptrack.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "Moddoc.h" +#include "ModDocTemplate.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "Childfrm.h" +#include "Mpdlgs.h" +#include "dlg_misc.h" +#include "TempoSwingDialog.h" +#include "mod2wave.h" +#include "ChannelManagerDlg.h" +#include "MIDIMacroDialog.h" +#include "MIDIMappingDialog.h" +#include "StreamEncoderAU.h" +#include "StreamEncoderFLAC.h" +#include "StreamEncoderMP3.h" +#include "StreamEncoderOpus.h" +#include "StreamEncoderRAW.h" +#include "StreamEncoderVorbis.h" +#include "StreamEncoderWAV.h" +#include "mod2midi.h" +#include "../common/version.h" +#include "../tracklib/SampleEdit.h" +#include "../soundlib/modsmp_ctrl.h" +#include "CleanupSong.h" +#include "../common/mptStringBuffer.h" +#include "../common/mptFileIO.h" +#include <sstream> +#include "../common/FileReader.h" +#include "FileDialog.h" +#include "ExternalSamples.h" +#include "Globals.h" +#include "../soundlib/OPL.h" +#ifndef NO_PLUGINS +#include "AbstractVstEditor.h" +#endif +#include "mpt/binary/hex.hpp" +#include "mpt/base/numbers.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +const TCHAR FileFilterMOD[] = _T("ProTracker Modules (*.mod)|*.mod||"); +const TCHAR FileFilterXM[] = _T("FastTracker Modules (*.xm)|*.xm||"); +const TCHAR FileFilterS3M[] = _T("Scream Tracker Modules (*.s3m)|*.s3m||"); +const TCHAR FileFilterIT[] = _T("Impulse Tracker Modules (*.it)|*.it||"); +const TCHAR FileFilterMPT[] = _T("OpenMPT Modules (*.mptm)|*.mptm||"); +const TCHAR FileFilterNone[] = _T(""); + +const CString ModTypeToFilter(const CSoundFile& sndFile) +{ + const MODTYPE modtype = sndFile.GetType(); + switch(modtype) + { + case MOD_TYPE_MOD: return FileFilterMOD; + case MOD_TYPE_XM: return FileFilterXM; + case MOD_TYPE_S3M: return FileFilterS3M; + case MOD_TYPE_IT: return FileFilterIT; + case MOD_TYPE_MPT: return FileFilterMPT; + default: return FileFilterNone; + } +} + +///////////////////////////////////////////////////////////////////////////// +// CModDoc + +IMPLEMENT_DYNCREATE(CModDoc, CDocument) + +BEGIN_MESSAGE_MAP(CModDoc, CDocument) + //{{AFX_MSG_MAP(CModDoc) + ON_COMMAND(ID_FILE_SAVE_COPY, &CModDoc::OnSaveCopy) + ON_COMMAND(ID_FILE_SAVEASTEMPLATE, &CModDoc::OnSaveTemplateModule) + ON_COMMAND(ID_FILE_SAVEASWAVE, &CModDoc::OnFileWaveConvert) + ON_COMMAND(ID_FILE_SAVEMIDI, &CModDoc::OnFileMidiConvert) + ON_COMMAND(ID_FILE_SAVEOPL, &CModDoc::OnFileOPLExport) + ON_COMMAND(ID_FILE_SAVECOMPAT, &CModDoc::OnFileCompatibilitySave) + ON_COMMAND(ID_FILE_APPENDMODULE, &CModDoc::OnAppendModule) + ON_COMMAND(ID_PLAYER_PLAY, &CModDoc::OnPlayerPlay) + ON_COMMAND(ID_PLAYER_PAUSE, &CModDoc::OnPlayerPause) + ON_COMMAND(ID_PLAYER_STOP, &CModDoc::OnPlayerStop) + ON_COMMAND(ID_PLAYER_PLAYFROMSTART, &CModDoc::OnPlayerPlayFromStart) + ON_COMMAND(ID_VIEW_SONGPROPERTIES, &CModDoc::OnSongProperties) + ON_COMMAND(ID_VIEW_GLOBALS, &CModDoc::OnEditGlobals) + ON_COMMAND(ID_VIEW_PATTERNS, &CModDoc::OnEditPatterns) + ON_COMMAND(ID_VIEW_SAMPLES, &CModDoc::OnEditSamples) + ON_COMMAND(ID_VIEW_INSTRUMENTS, &CModDoc::OnEditInstruments) + ON_COMMAND(ID_VIEW_COMMENTS, &CModDoc::OnEditComments) + ON_COMMAND(ID_VIEW_EDITHISTORY, &CModDoc::OnViewEditHistory) + ON_COMMAND(ID_VIEW_MIDIMAPPING, &CModDoc::OnViewMIDIMapping) + ON_COMMAND(ID_VIEW_MPTHACKS, &CModDoc::OnViewMPTHacks) + ON_COMMAND(ID_EDIT_CLEANUP, &CModDoc::OnShowCleanup) + ON_COMMAND(ID_EDIT_SAMPLETRIMMER, &CModDoc::OnShowSampleTrimmer) + ON_COMMAND(ID_PATTERN_MIDIMACRO, &CModDoc::OnSetupZxxMacros) + ON_COMMAND(ID_CHANNEL_MANAGER, &CModDoc::OnChannelManager) + + ON_COMMAND(ID_ESTIMATESONGLENGTH, &CModDoc::OnEstimateSongLength) + ON_COMMAND(ID_APPROX_BPM, &CModDoc::OnApproximateBPM) + ON_COMMAND(ID_PATTERN_PLAY, &CModDoc::OnPatternPlay) + ON_COMMAND(ID_PATTERN_PLAYNOLOOP, &CModDoc::OnPatternPlayNoLoop) + ON_COMMAND(ID_PATTERN_RESTART, &CModDoc::OnPatternRestart) + ON_UPDATE_COMMAND_UI(ID_VIEW_INSTRUMENTS, &CModDoc::OnUpdateXMITMPTOnly) + ON_UPDATE_COMMAND_UI(ID_PATTERN_MIDIMACRO, &CModDoc::OnUpdateXMITMPTOnly) + ON_UPDATE_COMMAND_UI(ID_VIEW_MIDIMAPPING, &CModDoc::OnUpdateHasMIDIMappings) + ON_UPDATE_COMMAND_UI(ID_VIEW_EDITHISTORY, &CModDoc::OnUpdateHasEditHistory) + ON_UPDATE_COMMAND_UI(ID_FILE_SAVECOMPAT, &CModDoc::OnUpdateCompatExportableOnly) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +///////////////////////////////////////////////////////////////////////////// +// CModDoc construction/destruction + +CModDoc::CModDoc() + : m_notifyType(Notification::Default) + , m_PatternUndo(*this) + , m_SampleUndo(*this) + , m_InstrumentUndo(*this) +{ + // Set the creation date of this file (or the load time if we're loading an existing file) + time(&m_creationTime); + + ReinitRecordState(); + + CMainFrame::UpdateAudioParameters(m_SndFile, true); +} + + +CModDoc::~CModDoc() +{ + ClearLog(); +} + + +void CModDoc::SetModified(bool modified) +{ + static_assert(sizeof(long) == sizeof(m_bModified)); + m_modifiedAutosave = modified; + if(!!InterlockedExchange(reinterpret_cast<long *>(&m_bModified), modified ? TRUE : FALSE) != modified) + { + // Update window titles in GUI thread + CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_SETMODIFIED, reinterpret_cast<WPARAM>(this), 0); + } +} + + +// Return "modified since last autosave" status and reset it until the next SetModified() (as this is only used for polling during autosave) +bool CModDoc::ModifiedSinceLastAutosave() +{ + return m_modifiedAutosave.exchange(false); +} + + +BOOL CModDoc::OnNewDocument() +{ + if (!CDocument::OnNewDocument()) return FALSE; + + m_SndFile.Create(FileReader(), CSoundFile::loadCompleteModule, this); + m_SndFile.ChangeModTypeTo(CTrackApp::GetDefaultDocType()); + + theApp.GetDefaultMidiMacro(m_SndFile.m_MidiCfg); + m_SndFile.m_SongFlags.set((SONG_LINEARSLIDES | SONG_ISAMIGA) & m_SndFile.GetModSpecifications().songFlags); + + ReinitRecordState(); + InitializeMod(); + SetModified(false); + return TRUE; +} + + +BOOL CModDoc::OnOpenDocument(LPCTSTR lpszPathName) +{ + const mpt::PathString filename = lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString(); + + ScopedLogCapturer logcapturer(*this); + + if(filename.empty()) return OnNewDocument(); + + BeginWaitCursor(); + + { + + MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open...")); + + InputFile f(filename, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if (f.IsValid()) + { + FileReader file = GetFileReader(f); + MPT_ASSERT(GetPathNameMpt().empty()); + SetPathName(filename, FALSE); // Path is not set yet, but loaders processing external samples/instruments (ITP/MPTM) need this for relative paths. + try + { + if(!m_SndFile.Create(file, CSoundFile::loadCompleteModule, this)) + { + EndWaitCursor(); + return FALSE; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + EndWaitCursor(); + AddToLog(LogError, U_("Out of Memory")); + return FALSE; + } catch(const std::exception &) + { + EndWaitCursor(); + return FALSE; + } + } + + MPT_LOG_GLOBAL(LogDebug, "Loader", U_("Open.")); + + } + + EndWaitCursor(); + + logcapturer.ShowLog( + MPT_CFORMAT("File: {}\nLast saved with: {}, you are using OpenMPT {}\n\n") + (filename, m_SndFile.m_modFormat.madeWithTracker, Version::Current())); + + if((m_SndFile.m_nType == MOD_TYPE_NONE) || (!m_SndFile.GetNumChannels())) + return FALSE; + + const bool noColors = std::find_if(std::begin(m_SndFile.ChnSettings), std::begin(m_SndFile.ChnSettings) + GetNumChannels(), [](const auto &settings) { + return settings.color != ModChannelSettings::INVALID_COLOR; + }) == std::begin(m_SndFile.ChnSettings) + GetNumChannels(); + if(noColors) + { + SetDefaultChannelColors(); + } + + // Convert to MOD/S3M/XM/IT + switch(m_SndFile.GetType()) + { + case MOD_TYPE_MOD: + case MOD_TYPE_S3M: + case MOD_TYPE_XM: + case MOD_TYPE_IT: + case MOD_TYPE_MPT: + break; + default: + m_SndFile.ChangeModTypeTo(m_SndFile.GetBestSaveFormat(), false); + m_SndFile.m_SongFlags.set(SONG_IMPORTED); + break; + } + // If the file was packed in some kind of container (e.g. ZIP, or simply a format like MO3), prompt for new file extension as well + // Same if MOD_TYPE_XXX does not indicate actual song format + if(m_SndFile.GetContainerType() != MOD_CONTAINERTYPE_NONE || m_SndFile.m_SongFlags[SONG_IMPORTED]) + { + m_ShowSavedialog = true; + } + + ReinitRecordState(); + + if(TrackerSettings::Instance().rememberSongWindows) + DeserializeViews(); + + // This is only needed when opening a module with stored window positions. + // The MDI child is activated before it has an active view and thus there is no CModDoc associated with it. + CMainFrame::GetMainFrame()->UpdateEffectKeys(this); + auto instance = CChannelManagerDlg::sharedInstance(); + if(instance != nullptr) + { + instance->SetDocument(this); + } + + // Show warning if file was made with more recent version of OpenMPT except + if(m_SndFile.m_dwLastSavedWithVersion.WithoutTestNumber() > Version::Current()) + { + Reporting::Notification(MPT_UFORMAT("Warning: this song was last saved with a more recent version of OpenMPT.\r\nSong saved with: v{}. Current version: v{}.\r\n")( + m_SndFile.m_dwLastSavedWithVersion, + Version::Current())); + } + + SetModified(false); + m_bHasValidPath = true; + + // Check if there are any missing samples, and if there are, show a dialog to relocate them. + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + if(m_SndFile.IsExternalSampleMissing(smp)) + { + MissingExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame()); + dlg.DoModal(); + break; + } + } + + return TRUE; +} + + +bool CModDoc::OnSaveDocument(const mpt::PathString &filename, const bool setPath) +{ + ScopedLogCapturer logcapturer(*this); + if(filename.empty()) + return false; + + bool ok = false; + BeginWaitCursor(); + m_SndFile.m_dwLastSavedWithVersion = Version::Current(); + try + { + mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + if(f) + { + if(m_SndFile.m_SongFlags[SONG_IMPORTED] && !(GetModType() & (MOD_TYPE_MOD | MOD_TYPE_S3M))) + { + // Check if any non-supported playback behaviours are enabled due to being imported from a different format + const auto supportedBehaviours = m_SndFile.GetSupportedPlaybackBehaviour(GetModType()); + bool showWarning = true; + for(size_t i = 0; i < kMaxPlayBehaviours; i++) + { + if(m_SndFile.m_playBehaviour[i] && !supportedBehaviours[i]) + { + if(showWarning) + { + AddToLog(LogWarning, mpt::ToUnicode(mpt::Charset::ASCII, MPT_AFORMAT("Some imported Compatibility Settings that are not supported by the {} format have been disabled. Verify that the module still sounds as intended.") + (mpt::ToUpperCaseAscii(m_SndFile.GetModSpecifications().fileExtension)))); + showWarning = false; + } + m_SndFile.m_playBehaviour.reset(i); + } + } + } + + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + FixNullStrings(); + switch(m_SndFile.GetType()) + { + case MOD_TYPE_MOD: ok = m_SndFile.SaveMod(f); break; + case MOD_TYPE_S3M: ok = m_SndFile.SaveS3M(f); break; + case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f); break; + case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename); break; + case MOD_TYPE_MPT: ok = m_SndFile.SaveIT(f, filename); break; + default: MPT_ASSERT_NOTREACHED(); + } + } + } catch(const std::exception &) + { + ok = false; + } + EndWaitCursor(); + + if(ok) + { + if(setPath) + { + // Set new path for this file, unless we are saving a template or a copy, in which case we want to keep the old file path. + SetPathName(filename); + } + logcapturer.ShowLog(true); + if(TrackerSettings::Instance().rememberSongWindows) + SerializeViews(); + } else + { + ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame()); + } + return ok; +} + + +BOOL CModDoc::SaveModified() +{ + if(m_SndFile.GetType() == MOD_TYPE_MPT && !SaveAllSamples()) + return FALSE; + return CDocument::SaveModified(); +} + + +bool CModDoc::SaveAllSamples(bool showPrompt) +{ + if(showPrompt) + { + ModifiedExternalSamplesDlg dlg(*this, CMainFrame::GetMainFrame()); + return dlg.DoModal() == IDOK; + } else + { + bool ok = true; + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + ok &= SaveSample(smp); + } + return ok; + } +} + + +bool CModDoc::SaveSample(SAMPLEINDEX smp) +{ + bool success = false; + if(smp > 0 && smp <= GetNumSamples()) + { + const mpt::PathString filename = m_SndFile.GetSamplePath(smp); + if(!filename.empty()) + { + auto &sample = m_SndFile.GetSample(smp); + const auto ext = filename.GetFileExt().ToUnicode().substr(1); + const auto format = FromSettingValue<SampleEditorDefaultFormat>(ext); + + try + { + mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + if(sf) + { + mpt::ofstream &f = sf; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + + if(sample.uFlags[CHN_ADLIB] || format == dfS3I) + success = m_SndFile.SaveS3ISample(smp, f); + else if(format != dfWAV) + success = m_SndFile.SaveFLACSample(smp, f); + else + success = m_SndFile.SaveWAVSample(smp, f); + } + } catch(const std::exception &) + { + success = false; + } + + if(success) + sample.uFlags.reset(SMP_MODIFIED); + else + AddToLog(LogError, MPT_UFORMAT("Unable to save sample {}: {}")(smp, filename)); + } + } + return success; +} + + +void CModDoc::OnCloseDocument() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) pMainFrm->OnDocumentClosed(this); + CDocument::OnCloseDocument(); +} + + +void CModDoc::DeleteContents() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->StopMod(this); + m_SndFile.Destroy(); + ReinitRecordState(); +} + + +BOOL CModDoc::DoSave(const mpt::PathString &filename, bool setPath) +{ + const mpt::PathString docFileName = GetPathNameMpt(); + const std::string defaultExtension = m_SndFile.GetModSpecifications().fileExtension; + + switch(m_SndFile.GetBestSaveFormat()) + { + case MOD_TYPE_MOD: + MsgBoxHidable(ModSaveHint); + break; + case MOD_TYPE_S3M: + break; + case MOD_TYPE_XM: + MsgBoxHidable(XMCompatibilityExportTip); + break; + case MOD_TYPE_IT: + MsgBoxHidable(ItCompatibilityExportTip); + break; + case MOD_TYPE_MPT: + break; + default: + ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame()); + return FALSE; + } + + mpt::PathString ext = P_(".") + mpt::PathString::FromUTF8(defaultExtension); + + mpt::PathString saveFileName; + + if(filename.empty() || m_ShowSavedialog) + { + mpt::PathString drive = docFileName.GetDrive(); + mpt::PathString dir = docFileName.GetDir(); + mpt::PathString fileName = docFileName.GetFileName(); + if(fileName.empty()) + { + fileName = mpt::PathString::FromCString(GetTitle()).SanitizeComponent(); + } + mpt::PathString defaultSaveName = drive + dir + fileName + ext; + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(defaultExtension) + .DefaultFilename(defaultSaveName) + .ExtensionFilter(ModTypeToFilter(m_SndFile)) + .WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir()); + if(!dlg.Show()) return FALSE; + + TrackerSettings::Instance().PathSongs.SetWorkingDir(dlg.GetWorkingDirectory()); + + saveFileName = dlg.GetFirstFile(); + } else + { + saveFileName = filename; + } + + // Do we need to create a backup file ? + if((TrackerSettings::Instance().CreateBackupFiles) + && (IsModified()) && (!mpt::PathString::CompareNoCase(saveFileName, docFileName))) + { + if(saveFileName.IsFile()) + { + mpt::PathString backupFileName = saveFileName.ReplaceExt(P_(".bak")); + if(backupFileName.IsFile()) + { + DeleteFile(backupFileName.AsNative().c_str()); + } + MoveFile(saveFileName.AsNative().c_str(), backupFileName.AsNative().c_str()); + } + } + if(OnSaveDocument(saveFileName, setPath)) + { + SetModified(false); + m_SndFile.m_SongFlags.reset(SONG_IMPORTED); + m_bHasValidPath = true; + m_ShowSavedialog = false; + CMainFrame::GetMainFrame()->UpdateTree(this, GeneralHint().General()); // Update treeview (e.g. filename might have changed) + return TRUE; + } else + { + return FALSE; + } +} + + +void CModDoc::OnAppendModule() +{ + FileDialog::PathList files; + CTrackApp::OpenModulesDialog(files); + + ScopedLogCapturer logcapture(*this, _T("Append Failures")); + try + { + auto source = std::make_unique<CSoundFile>(); + for(const auto &file : files) + { + InputFile f(file, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(!f.IsValid()) + { + AddToLog("Unable to open source file!"); + continue; + } + try + { + if(!source->Create(GetFileReader(f), CSoundFile::loadCompleteModule)) + { + AddToLog("Unable to open source file!"); + continue; + } + } catch(const std::exception &) + { + AddToLog("Unable to open source file!"); + continue; + } + AppendModule(*source); + source->Destroy(); + SetModified(); + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + AddToLog("Out of memory."); + return; + } + + UpdateAllViews(nullptr, SequenceHint().Data().ModType()); +} + + +void CModDoc::InitializeMod() +{ + // New module ? + if (!m_SndFile.m_nChannels) + { + switch(GetModType()) + { + case MOD_TYPE_MOD: + m_SndFile.m_nChannels = 4; + break; + case MOD_TYPE_S3M: + m_SndFile.m_nChannels = 16; + break; + default: + m_SndFile.m_nChannels = 32; + break; + } + + SetDefaultChannelColors(); + + if(GetModType() == MOD_TYPE_MPT) + { + m_SndFile.m_nTempoMode = TempoMode::Modern; + m_SndFile.m_SongFlags.set(SONG_EXFILTERRANGE); + } + m_SndFile.SetDefaultPlaybackBehaviour(GetModType()); + + // Refresh mix levels now that the correct mod type has been set + m_SndFile.SetMixLevels(m_SndFile.GetModSpecifications().defaultMixLevels); + + m_SndFile.Order().assign(1, 0); + if (!m_SndFile.Patterns.IsValidPat(0)) + { + m_SndFile.Patterns.Insert(0, 64); + } + + Clear(m_SndFile.m_szNames); + + m_SndFile.m_PlayState.m_nMusicTempo.Set(125); + m_SndFile.m_nDefaultTempo.Set(125); + m_SndFile.m_PlayState.m_nMusicSpeed = m_SndFile.m_nDefaultSpeed = 6; + + // Set up mix levels + m_SndFile.m_PlayState.m_nGlobalVolume = m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + m_SndFile.m_nSamplePreAmp = m_SndFile.m_nVSTiVolume = 48; + + for (CHANNELINDEX nChn = 0; nChn < MAX_BASECHANNELS; nChn++) + { + m_SndFile.ChnSettings[nChn].dwFlags.reset(); + m_SndFile.ChnSettings[nChn].nVolume = 64; + m_SndFile.ChnSettings[nChn].nPan = 128; + m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = 64; + } + // Setup LRRL panning scheme for MODs + m_SndFile.SetupMODPanning(); + } + if (!m_SndFile.m_nSamples) + { + m_SndFile.m_szNames[1] = "untitled"; + m_SndFile.m_nSamples = (GetModType() == MOD_TYPE_MOD) ? 31 : 1; + + SampleEdit::ResetSamples(m_SndFile, SampleEdit::SmpResetInit); + + m_SndFile.GetSample(1).Initialize(m_SndFile.GetType()); + + if ((!m_SndFile.m_nInstruments) && (m_SndFile.GetType() & MOD_TYPE_XM)) + { + if(m_SndFile.AllocateInstrument(1, 1)) + { + m_SndFile.m_nInstruments = 1; + InitializeInstrument(m_SndFile.Instruments[1]); + } + } + if (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) + { + m_SndFile.m_SongFlags.set(SONG_LINEARSLIDES); + } + } + m_SndFile.ResetPlayPos(); + m_SndFile.m_songArtist = TrackerSettings::Instance().defaultArtist; +} + + +bool CModDoc::SetDefaultChannelColors(CHANNELINDEX minChannel, CHANNELINDEX maxChannel) +{ + LimitMax(minChannel, GetNumChannels()); + LimitMax(maxChannel, GetNumChannels()); + if(maxChannel < minChannel) + std::swap(minChannel, maxChannel); + bool modified = false; + if(TrackerSettings::Instance().defaultRainbowChannelColors != DefaultChannelColors::NoColors) + { + const bool rainbow = TrackerSettings::Instance().defaultRainbowChannelColors == DefaultChannelColors::Rainbow; + CHANNELINDEX numGroups = 0; + if(rainbow) + { + for(CHANNELINDEX i = minChannel + 1u; i < maxChannel; i++) + { + if(m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName) + numGroups++; + } + } + const double hueFactor = rainbow ? (1.5 * mpt::numbers::pi) / std::max(1, numGroups - 1) : 1000.0; // Three quarters of the color wheel, red to purple + for(CHANNELINDEX i = minChannel, group = minChannel; i < maxChannel; i++) + { + if(i > minChannel && (m_SndFile.ChnSettings[i].szName.empty() || m_SndFile.ChnSettings[i].szName != m_SndFile.ChnSettings[i - 1].szName)) + group++; + const double hue = group * hueFactor; // 0...2pi + const double saturation = 0.3; // 0...2/3 + const double brightness = 1.2; // 0...4/3 + const double r = brightness * (1 + saturation * (std::cos(hue) - 1.0)); + const double g = brightness * (1 + saturation * (std::cos(hue - 2.09439) - 1.0)); + const double b = brightness * (1 + saturation * (std::cos(hue + 2.09439) - 1.0)); + const auto color = RGB(mpt::saturate_round<uint8>(r * 255), mpt::saturate_round<uint8>(g * 255), mpt::saturate_round<uint8>(b * 255)); + if(m_SndFile.ChnSettings[i].color != color) + { + m_SndFile.ChnSettings[i].color = color; + modified = true; + } + } + } else + { + for(CHANNELINDEX i = minChannel; i < maxChannel; i++) + { + if(m_SndFile.ChnSettings[i].color != ModChannelSettings::INVALID_COLOR) + { + m_SndFile.ChnSettings[i].color = ModChannelSettings::INVALID_COLOR; + modified = true; + } + } + } + return modified; +} + + +void CModDoc::PostMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + POSITION pos = GetFirstViewPosition(); + while(pos != nullptr) + { + if(CView *pView = GetNextView(pos); pView != nullptr) + pView->PostMessage(uMsg, wParam, lParam); + } +} + + +void CModDoc::SendNotifyMessageToAllViews(UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + POSITION pos = GetFirstViewPosition(); + while(pos != nullptr) + { + if(CView *pView = GetNextView(pos); pView != nullptr) + pView->SendNotifyMessage(uMsg, wParam, lParam); + } +} + + +void CModDoc::SendMessageToActiveView(UINT uMsg, WPARAM wParam, LPARAM lParam) +{ + if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr) + { + lastActiveFrame->SendMessageToDescendants(uMsg, wParam, lParam); + } +} + + +void CModDoc::ViewPattern(UINT nPat, UINT nOrd) +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, ((nPat+1) << 16) | nOrd); +} + + +void CModDoc::ViewSample(UINT nSmp) +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, nSmp); +} + + +void CModDoc::ViewInstrument(UINT nIns) +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, nIns); +} + + +ScopedLogCapturer::ScopedLogCapturer(CModDoc &modDoc, const CString &title, CWnd *parent, bool showLog) : +m_modDoc(modDoc), m_oldLogMode(m_modDoc.GetLogMode()), m_title(title), m_pParent(parent), m_showLog(showLog) +{ + m_modDoc.SetLogMode(LogModeGather); +} + + +void ScopedLogCapturer::ShowLog(bool force) +{ + if(force || m_oldLogMode == LogModeInstantReporting) + { + m_modDoc.ShowLog(m_title, m_pParent); + m_modDoc.ClearLog(); + } +} + + +void ScopedLogCapturer::ShowLog(const std::string &preamble, bool force) +{ + if(force || m_oldLogMode == LogModeInstantReporting) + { + m_modDoc.ShowLog(mpt::ToCString(mpt::Charset::Locale, preamble), m_title, m_pParent); + m_modDoc.ClearLog(); + } +} + + +void ScopedLogCapturer::ShowLog(const CString &preamble, bool force) +{ + if(force || m_oldLogMode == LogModeInstantReporting) + { + m_modDoc.ShowLog(preamble, m_title, m_pParent); + m_modDoc.ClearLog(); + } +} + + +void ScopedLogCapturer::ShowLog(const mpt::ustring &preamble, bool force) +{ + if(force || m_oldLogMode == LogModeInstantReporting) + { + m_modDoc.ShowLog(mpt::ToCString(preamble), m_title, m_pParent); + m_modDoc.ClearLog(); + } +} + + +ScopedLogCapturer::~ScopedLogCapturer() +{ + if(m_showLog) + ShowLog(); + else + m_modDoc.ClearLog(); + m_modDoc.SetLogMode(m_oldLogMode); +} + + +void CModDoc::AddToLog(LogLevel level, const mpt::ustring &text) const +{ + if(m_LogMode == LogModeGather) + { + m_Log.push_back(LogEntry(level, text)); + } else + { + if(level < LogDebug) + { + Reporting::Message(level, text); + } + } +} + + +mpt::ustring CModDoc::GetLogString() const +{ + mpt::ustring ret; + for(const auto &i : m_Log) + { + ret += i.message; + ret += U_("\r\n"); + } + return ret; +} + + +LogLevel CModDoc::GetMaxLogLevel() const +{ + LogLevel retval = LogInformation; + // find the most severe loglevel + for(const auto &i : m_Log) + { + retval = std::min(retval, i.level); + } + return retval; +} + + +void CModDoc::ClearLog() +{ + m_Log.clear(); +} + + +UINT CModDoc::ShowLog(const CString &preamble, const CString &title, CWnd *parent) +{ + if(!parent) parent = CMainFrame::GetMainFrame(); + if(GetLog().size() > 0) + { + LogLevel level = GetMaxLogLevel(); + if(level < LogDebug) + { + CString text = preamble + mpt::ToCString(GetLogString()); + CString actualTitle = (title.GetLength() == 0) ? CString(MAINFRAME_TITLE) : title; + Reporting::Message(level, text, actualTitle, parent); + return IDOK; + } + } + return IDCANCEL; +} + + +void CModDoc::ProcessMIDI(uint32 midiData, INSTRUMENTINDEX ins, IMixPlugin *plugin, InputTargetContext ctx) +{ + static uint8 midiVolume = 127; + + MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData); + const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData); + const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); + const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData); + uint8 note = midiByte1 + NOTE_MIN; + int vol = midiByte2; + + if((event == MIDIEvents::evNoteOn) && !vol) + event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd + + PLUGINDEX mappedIndex = 0; + PlugParamIndex paramIndex = 0; + uint16 paramValue = 0; + bool captured = m_SndFile.GetMIDIMapper().OnMIDImsg(midiData, mappedIndex, paramIndex, paramValue); + + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + if(ih->HandleMIDIMessage(ctx, midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull) + { + // Mapped to a command, no need to pass message on. + captured = true; + } + + if(captured) + { + // Event captured by MIDI mapping or shortcut, no need to pass message on. + return; + } + + switch(event) + { + case MIDIEvents::evNoteOff: + if(m_midiSustainActive[channel]) + { + m_midiSustainBuffer[channel].push_back(midiData); + return; + } + if(ins > 0 && ins <= GetNumInstruments()) + { + LimitMax(note, NOTE_MAX); + if(m_midiPlayingNotes[channel][note]) + m_midiPlayingNotes[channel][note] = false; + NoteOff(note, false, ins, m_noteChannel[note - NOTE_MIN]); + return; + } else if(plugin != nullptr) + { + plugin->MidiSend(midiData); + } + break; + + case MIDIEvents::evNoteOn: + if(ins > 0 && ins <= GetNumInstruments()) + { + LimitMax(note, NOTE_MAX); + vol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume); + PlayNote(PlayNoteParam(note).Instrument(ins).Volume(vol).CheckNNA(m_midiPlayingNotes[channel]), &m_noteChannel); + return; + } else if(plugin != nullptr) + { + plugin->MidiSend(midiData); + } + break; + + case MIDIEvents::evControllerChange: + switch(midiByte1) + { + case MIDIEvents::MIDICC_Volume_Coarse: + midiVolume = midiByte2; + break; + case MIDIEvents::MIDICC_HoldPedal_OnOff: + m_midiSustainActive[channel] = (midiByte2 >= 0x40); + if(!m_midiSustainActive[channel]) + { + // Release all notes + for(const auto offEvent : m_midiSustainBuffer[channel]) + { + ProcessMIDI(offEvent, ins, plugin, ctx); + } + m_midiSustainBuffer[channel].clear(); + } + break; + } + break; + } + + if((TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDITOPLUG) && CMainFrame::GetMainFrame()->GetModPlaying() == this && plugin != nullptr) + { + plugin->MidiSend(midiData); + // Sending midi may modify the plug. For now, if MIDI data is not active sensing or aftertouch messages, set modified. + if(midiData != MIDIEvents::System(MIDIEvents::sysActiveSense) + && event != MIDIEvents::evPolyAftertouch && event != MIDIEvents::evChannelAftertouch + && event != MIDIEvents::evPitchBend + && m_SndFile.GetModSpecifications().supportsPlugins) + { + SetModified(); + } + } +} + + +CHANNELINDEX CModDoc::PlayNote(PlayNoteParam ¶ms, NoteToChannelMap *noteChannel) +{ + CHANNELINDEX channel = GetNumChannels(); + + ModCommand::NOTE note = params.m_note; + if(ModCommand::IsNote(ModCommand::NOTE(note))) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm == nullptr || note == NOTE_NONE) return CHANNELINDEX_INVALID; + if (pMainFrm->GetModPlaying() != this) + { + // All notes off when resuming paused playback + m_SndFile.ResetChannels(); + + m_SndFile.m_SongFlags.set(SONG_PAUSED); + pMainFrm->PlayMod(this); + } + + CriticalSection cs; + + if(params.m_notesPlaying) + CheckNNA(note, params.m_instr, *params.m_notesPlaying); + + // Find a channel to play on + channel = FindAvailableChannel(); + ModChannel &chn = m_SndFile.m_PlayState.Chn[channel]; + + // reset channel properties; in theory the chan is completely unused anyway. + chn.Reset(ModChannel::resetTotal, m_SndFile, CHANNELINDEX_INVALID, CHN_MUTE); + chn.nNewNote = chn.nLastNote = static_cast<uint8>(note); + chn.nVolume = 256; + + if(params.m_instr) + { + // Set instrument (or sample if there are no instruments) + chn.ResetEnvelopes(); + m_SndFile.InstrumentChange(chn, params.m_instr); + } else if(params.m_sample > 0 && params.m_sample <= GetNumSamples()) // Or set sample explicitely + { + ModSample &sample = m_SndFile.GetSample(params.m_sample); + chn.pCurrentSample = sample.samplev(); + chn.pModInstrument = nullptr; + chn.pModSample = &sample; + chn.nFineTune = sample.nFineTune; + chn.nC5Speed = sample.nC5Speed; + chn.nLoopStart = sample.nLoopStart; + chn.nLoopEnd = sample.nLoopEnd; + chn.dwFlags = (sample.uFlags & (CHN_SAMPLEFLAGS & ~CHN_MUTE)); + chn.nPan = 128; + if(sample.uFlags[CHN_PANNING]) chn.nPan = sample.nPan; + chn.UpdateInstrumentVolume(&sample, nullptr); + } + chn.nFadeOutVol = 0x10000; + chn.isPreviewNote = true; + if(params.m_currentChannel != CHANNELINDEX_INVALID) + chn.nMasterChn = params.m_currentChannel + 1; + else + chn.nMasterChn = 0; + + if(chn.dwFlags[CHN_ADLIB] && chn.pModSample && m_SndFile.m_opl) + { + m_SndFile.m_opl->Patch(channel, chn.pModSample->adlib); + } + + m_SndFile.NoteChange(chn, note, false, true, true, channel); + if(params.m_volume >= 0) chn.nVolume = std::min(params.m_volume, 256); + + // Handle sample looping. + // Changed line to fix http://forum.openmpt.org/index.php?topic=1700.0 + //if ((loopstart + 16 < loopend) && (loopstart >= 0) && (loopend <= (LONG)pchn.nLength)) + if ((params.m_loopStart + 16 < params.m_loopEnd) && (params.m_loopStart >= 0) && (chn.pModSample != nullptr)) + { + chn.position.Set(params.m_loopStart); + chn.nLoopStart = params.m_loopStart; + chn.nLoopEnd = params.m_loopEnd; + chn.nLength = std::min(params.m_loopEnd, chn.pModSample->nLength); + } + + // Handle extra-loud flag + chn.dwFlags.set(CHN_EXTRALOUD, !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOEXTRALOUD) && params.m_sample); + + // Handle custom start position + if(params.m_sampleOffset > 0 && chn.pModSample) + { + chn.position.Set(params.m_sampleOffset); + // If start position is after loop end, set loop end to sample end so that the sample starts + // playing. + if(chn.nLoopEnd < params.m_sampleOffset) + chn.nLength = chn.nLoopEnd = chn.pModSample->nLength; + } + + // VSTi preview + if(params.m_instr > 0 && params.m_instr <= m_SndFile.GetNumInstruments()) + { + const ModInstrument *pIns = m_SndFile.Instruments[params.m_instr]; + if (pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan + { + PLUGINDEX nPlugin = 0; + if (chn.pModInstrument) + nPlugin = chn.pModInstrument->nMixPlug; // First try instrument plugin + if ((!nPlugin || nPlugin > MAX_MIXPLUGINS) && params.m_currentChannel != CHANNELINDEX_INVALID) + nPlugin = m_SndFile.ChnSettings[params.m_currentChannel].nMixPlugin; // Then try channel plugin + + if ((nPlugin) && (nPlugin <= MAX_MIXPLUGINS)) + { + IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[nPlugin - 1].pMixPlugin; + if(pPlugin != nullptr) + { + pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN], static_cast<uint16>(chn.nVolume), channel); + } + } + } + } + + // Remove channel from list of mixed channels to fix https://bugs.openmpt.org/view.php?id=209 + // This is required because a previous note on the same channel might have just stopped playing, + // but the channel is still in the mix list. + // Since the channel volume / etc is only updated every tick in CSoundFile::ReadNote, and we + // do not want to duplicate mixmode-dependant logic here, CSoundFile::CreateStereoMix may already + // try to mix our newly set up channel at volume 0 if we don't remove it from the list. + auto mixBegin = std::begin(m_SndFile.m_PlayState.ChnMix); + auto mixEnd = std::remove(mixBegin, mixBegin + m_SndFile.m_nMixChannels, channel); + m_SndFile.m_nMixChannels = static_cast<CHANNELINDEX>(std::distance(mixBegin, mixEnd)); + + if(noteChannel) + { + noteChannel->at(note - NOTE_MIN) = channel; + } + } else + { + CriticalSection cs; + // Apply note cut / off / fade (also on preview channels) + m_SndFile.NoteChange(m_SndFile.m_PlayState.Chn[channel], note); + for(CHANNELINDEX c = m_SndFile.GetNumChannels(); c < MAX_CHANNELS; c++) + { + ModChannel &chn = m_SndFile.m_PlayState.Chn[c]; + if(chn.isPreviewNote && (chn.pModSample || chn.pModInstrument)) + { + m_SndFile.NoteChange(chn, note); + } + } + } + return channel; +} + + +bool CModDoc::NoteOff(UINT note, bool fade, INSTRUMENTINDEX ins, CHANNELINDEX currentChn) +{ + CriticalSection cs; + + if(ins != INSTRUMENTINDEX_INVALID && ins <= m_SndFile.GetNumInstruments() && ModCommand::IsNote(ModCommand::NOTE(note))) + { + const ModInstrument *pIns = m_SndFile.Instruments[ins]; + if(pIns && pIns->HasValidMIDIChannel()) // instro sends to a midi chan + { + PLUGINDEX plug = pIns->nMixPlug; // First try intrument VST + if((!plug || plug > MAX_MIXPLUGINS) // No good plug yet + && currentChn < MAX_BASECHANNELS) // Chan OK + { + plug = m_SndFile.ChnSettings[currentChn].nMixPlugin;// Then try Channel VST + } + + if(plug && plug <= MAX_MIXPLUGINS) + { + IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plug - 1].pMixPlugin; + if(pPlugin) + { + pPlugin->MidiCommand(*pIns, pIns->NoteMap[note - NOTE_MIN] + NOTE_KEYOFF, 0, currentChn); + } + } + } + } + + const FlagSet<ChannelFlags> mask = (fade ? CHN_NOTEFADE : (CHN_NOTEFADE | CHN_KEYOFF)); + const CHANNELINDEX startChn = currentChn != CHANNELINDEX_INVALID ? currentChn : m_SndFile.m_nChannels; + const CHANNELINDEX endChn = currentChn != CHANNELINDEX_INVALID ? currentChn + 1 : MAX_CHANNELS; + ModChannel *pChn = &m_SndFile.m_PlayState.Chn[startChn]; + for(CHANNELINDEX i = startChn; i < endChn; i++, pChn++) + { + // Fade all channels > m_nChannels which are playing this note and aren't NNA channels. + if((pChn->isPreviewNote || i < m_SndFile.GetNumChannels()) + && !pChn->dwFlags[mask] + && (pChn->nLength || pChn->dwFlags[CHN_ADLIB]) + && (note == pChn->nNewNote || note == NOTE_NONE)) + { + m_SndFile.KeyOff(*pChn); + if (!m_SndFile.m_nInstruments) pChn->dwFlags.reset(CHN_LOOP | CHN_PINGPONGFLAG); + if (fade) pChn->dwFlags.set(CHN_NOTEFADE); + // Instantly stop samples that would otherwise play forever + if (pChn->pModInstrument && !pChn->pModInstrument->nFadeOut) + pChn->nFadeOutVol = 0; + if(pChn->dwFlags[CHN_ADLIB] && m_SndFile.m_opl) + { + m_SndFile.m_opl->NoteOff(i); + } + if (note) break; + } + } + + return true; +} + + +// Apply DNA/NNA settings for note preview. It will also set the specified note to be playing in the playingNotes set. +void CModDoc::CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes) +{ + if(ins > GetNumInstruments() || m_SndFile.Instruments[ins] == nullptr || note >= playingNotes.size()) + { + return; + } + const ModInstrument *pIns = m_SndFile.Instruments[ins]; + for(CHANNELINDEX chn = GetNumChannels(); chn < MAX_CHANNELS; chn++) + { + const ModChannel &channel = m_SndFile.m_PlayState.Chn[chn]; + if(channel.pModInstrument == pIns && channel.isPreviewNote && ModCommand::IsNote(channel.nLastNote) + && (channel.nLength || pIns->HasValidMIDIChannel()) && !playingNotes[channel.nLastNote]) + { + CHANNELINDEX nnaChn = m_SndFile.CheckNNA(chn, ins, note, false); + if(nnaChn != CHANNELINDEX_INVALID) + { + // Keep the new NNA channel playing in the same channel slot. + // That way, we do not need to touch the ChnMix array, and we avoid the same channel being checked twice. + if(nnaChn != chn) + { + m_SndFile.m_PlayState.Chn[chn] = std::move(m_SndFile.m_PlayState.Chn[nnaChn]); + m_SndFile.m_PlayState.Chn[nnaChn] = {}; + } + // Avoid clicks if the channel wasn't ramping before. + m_SndFile.m_PlayState.Chn[chn].dwFlags.set(CHN_FASTVOLRAMP); + m_SndFile.ProcessRamping(m_SndFile.m_PlayState.Chn[chn]); + } + } + } + playingNotes.set(note); +} + + +// Check if a given note of an instrument or sample is playing from the editor. +// If note == 0, just check if an instrument or sample is playing. +bool CModDoc::IsNotePlaying(UINT note, SAMPLEINDEX nsmp, INSTRUMENTINDEX nins) +{ + ModChannel *pChn = &m_SndFile.m_PlayState.Chn[m_SndFile.GetNumChannels()]; + for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++, pChn++) if (pChn->isPreviewNote) + { + if(pChn->nLength != 0 && !pChn->dwFlags[CHN_NOTEFADE | CHN_KEYOFF| CHN_MUTE] + && (note == pChn->nNewNote || note == NOTE_NONE) + && (pChn->pModSample == &m_SndFile.GetSample(nsmp) || !nsmp) + && (pChn->pModInstrument == m_SndFile.Instruments[nins] || !nins)) return true; + } + return false; +} + + +bool CModDoc::MuteToggleModifiesDocument() const +{ + return (m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)) && TrackerSettings::Instance().MiscSaveChannelMuteStatus; +} + + +bool CModDoc::MuteChannel(CHANNELINDEX nChn, bool doMute) +{ + if (nChn >= m_SndFile.GetNumChannels()) + { + return false; + } + + // Mark channel as muted in channel settings + m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_MUTE, doMute); + + const bool success = UpdateChannelMuteStatus(nChn); + if(success && MuteToggleModifiesDocument()) + { + SetModified(); + } + + return success; +} + + +bool CModDoc::UpdateChannelMuteStatus(CHANNELINDEX nChn) +{ + const ChannelFlags muteType = CSoundFile::GetChannelMuteFlag(); + + if (nChn >= m_SndFile.GetNumChannels()) + { + return false; + } + + const bool doMute = m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE]; + + // Mute pattern channel + if (doMute) + { + m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(muteType); + if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(nChn); + // Kill VSTi notes on muted channel. + PLUGINDEX nPlug = m_SndFile.GetBestPlugin(m_SndFile.m_PlayState, nChn, PrioritiseInstrument, EvenIfMuted); + if ((nPlug) && (nPlug<=MAX_MIXPLUGINS)) + { + IMixPlugin *pPlug = m_SndFile.m_MixPlugins[nPlug - 1].pMixPlugin; + const ModInstrument* pIns = m_SndFile.m_PlayState.Chn[nChn].pModInstrument; + if (pPlug && pIns) + { + pPlug->MidiCommand(*pIns, NOTE_KEYOFF, 0, nChn); + } + } + } else + { + // On unmute alway cater for both mute types - this way there's no probs if user changes mute mode. + m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE); + } + + // Mute any NNA'd channels + for (CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) + { + if (m_SndFile.m_PlayState.Chn[i].nMasterChn == nChn + 1u) + { + if (doMute) + { + m_SndFile.m_PlayState.Chn[i].dwFlags.set(muteType); + if(m_SndFile.m_opl) m_SndFile.m_opl->NoteCut(i); + } else + { + // On unmute alway cater for both mute types - this way there's no probs if user changes mute mode. + m_SndFile.m_PlayState.Chn[i].dwFlags.reset(CHN_SYNCMUTE | CHN_MUTE); + } + } + } + + return true; +} + + +bool CModDoc::IsChannelSolo(CHANNELINDEX nChn) const +{ + if (nChn >= m_SndFile.m_nChannels) return true; + return m_SndFile.ChnSettings[nChn].dwFlags[CHN_SOLO]; +} + +bool CModDoc::SoloChannel(CHANNELINDEX nChn, bool bSolo) +{ + if (nChn >= m_SndFile.m_nChannels) return false; + if (MuteToggleModifiesDocument()) SetModified(); + m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SOLO, bSolo); + return true; +} + + +bool CModDoc::IsChannelNoFx(CHANNELINDEX nChn) const +{ + if (nChn >= m_SndFile.m_nChannels) return true; + return m_SndFile.ChnSettings[nChn].dwFlags[CHN_NOFX]; +} + + +bool CModDoc::NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix) +{ + if (nChn >= m_SndFile.m_nChannels) return false; + m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_NOFX, bNoFx); + if(updateMix) m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_NOFX, bNoFx); + return true; +} + + +RecordGroup CModDoc::GetChannelRecordGroup(CHANNELINDEX channel) const +{ + if(channel >= GetNumChannels()) + return RecordGroup::NoGroup; + if(m_bsMultiRecordMask[channel]) + return RecordGroup::Group1; + if(m_bsMultiSplitRecordMask[channel]) + return RecordGroup::Group2; + return RecordGroup::NoGroup; +} + + +void CModDoc::SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup) +{ + if(channel >= GetNumChannels()) + return; + m_bsMultiRecordMask.set(channel, recordGroup == RecordGroup::Group1); + m_bsMultiSplitRecordMask.set(channel, recordGroup == RecordGroup::Group2); +} + + +void CModDoc::ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup) +{ + if(channel >= GetNumChannels()) + return; + if(recordGroup == RecordGroup::Group1) + { + m_bsMultiRecordMask.flip(channel); + m_bsMultiSplitRecordMask.reset(channel); + } else if(recordGroup == RecordGroup::Group2) + { + m_bsMultiRecordMask.reset(channel); + m_bsMultiSplitRecordMask.flip(channel); + } +} + + +void CModDoc::ReinitRecordState(bool unselect) +{ + if(unselect) + { + m_bsMultiRecordMask.reset(); + m_bsMultiSplitRecordMask.reset(); + } else + { + m_bsMultiRecordMask.set(); + m_bsMultiSplitRecordMask.set(); + } +} + + +bool CModDoc::MuteSample(SAMPLEINDEX nSample, bool bMute) +{ + if ((nSample < 1) || (nSample > m_SndFile.GetNumSamples())) return false; + m_SndFile.GetSample(nSample).uFlags.set(CHN_MUTE, bMute); + return true; +} + + +bool CModDoc::MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute) +{ + if ((nInstr < 1) || (nInstr > m_SndFile.GetNumInstruments()) || (!m_SndFile.Instruments[nInstr])) return false; + m_SndFile.Instruments[nInstr]->dwFlags.set(INS_MUTE, bMute); + return true; +} + + +bool CModDoc::SurroundChannel(CHANNELINDEX nChn, bool surround) +{ + if(nChn >= m_SndFile.GetNumChannels()) return false; + + if(!(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT))) surround = false; + + if(surround != m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND]) + { + // Update channel configuration + if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified(); + + m_SndFile.ChnSettings[nChn].dwFlags.set(CHN_SURROUND, surround); + if(surround) + { + m_SndFile.ChnSettings[nChn].nPan = 128; + } + } + + // Update playing channel + m_SndFile.m_PlayState.Chn[nChn].dwFlags.set(CHN_SURROUND, surround); + if(surround) + { + m_SndFile.m_PlayState.Chn[nChn].nPan = 128; + } + return true; +} + + +bool CModDoc::SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume) +{ + bool ok = false; + if(nChn >= m_SndFile.GetNumChannels() || nVolume > 64) return false; + if(m_SndFile.ChnSettings[nChn].nVolume != nVolume) + { + m_SndFile.ChnSettings[nChn].nVolume = nVolume; + if(m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified(); + ok = true; + } + m_SndFile.m_PlayState.Chn[nChn].nGlobalVol = nVolume; + return ok; +} + + +bool CModDoc::SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan) +{ + bool ok = false; + if(nChn >= m_SndFile.GetNumChannels() || nPan > 256) return false; + if(m_SndFile.ChnSettings[nChn].nPan != nPan || m_SndFile.ChnSettings[nChn].dwFlags[CHN_SURROUND]) + { + m_SndFile.ChnSettings[nChn].nPan = nPan; + m_SndFile.ChnSettings[nChn].dwFlags.reset(CHN_SURROUND); + if(m_SndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT)) SetModified(); + ok = true; + } + m_SndFile.m_PlayState.Chn[nChn].nPan = nPan; + m_SndFile.m_PlayState.Chn[nChn].dwFlags.reset(CHN_SURROUND); + return ok; +} + + +bool CModDoc::IsChannelMuted(CHANNELINDEX nChn) const +{ + if(nChn >= m_SndFile.GetNumChannels()) return true; + return m_SndFile.ChnSettings[nChn].dwFlags[CHN_MUTE]; +} + + +bool CModDoc::IsSampleMuted(SAMPLEINDEX nSample) const +{ + if(!nSample || nSample > m_SndFile.GetNumSamples()) return false; + return m_SndFile.GetSample(nSample).uFlags[CHN_MUTE]; +} + + +bool CModDoc::IsInstrumentMuted(INSTRUMENTINDEX nInstr) const +{ + if(!nInstr || nInstr > m_SndFile.GetNumInstruments() || !m_SndFile.Instruments[nInstr]) return false; + return m_SndFile.Instruments[nInstr]->dwFlags[INS_MUTE]; +} + + +UINT CModDoc::GetPatternSize(PATTERNINDEX nPat) const +{ + if(m_SndFile.Patterns.IsValidIndex(nPat)) return m_SndFile.Patterns[nPat].GetNumRows(); + return 0; +} + + +void CModDoc::SetFollowWnd(HWND hwnd) +{ + m_hWndFollow = hwnd; +} + + +bool CModDoc::IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const +{ + return m_SndFile.IsSampleReferencedByInstrument(nSmp, nIns); +} + + +// Find an instrument that references the given sample. +// If no such instrument is found, INSTRUMENTINDEX_INVALID is returned. +INSTRUMENTINDEX CModDoc::FindSampleParent(SAMPLEINDEX sample) const +{ + if(sample == 0) + { + return INSTRUMENTINDEX_INVALID; + } + for(INSTRUMENTINDEX i = 1; i <= m_SndFile.GetNumInstruments(); i++) + { + const ModInstrument *pIns = m_SndFile.Instruments[i]; + if(pIns != nullptr) + { + for(size_t j = 0; j < NOTE_MAX; j++) + { + if(pIns->Keyboard[j] == sample) + { + return i; + } + } + } + } + return INSTRUMENTINDEX_INVALID; +} + + +SAMPLEINDEX CModDoc::FindInstrumentChild(INSTRUMENTINDEX nIns) const +{ + if ((!nIns) || (nIns > m_SndFile.GetNumInstruments())) return 0; + const ModInstrument *pIns = m_SndFile.Instruments[nIns]; + if (pIns) + { + for (auto n : pIns->Keyboard) + { + if ((n) && (n <= m_SndFile.GetNumSamples())) return n; + } + } + return 0; +} + + +LRESULT CModDoc::ActivateView(UINT nIdView, DWORD dwParam) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (!pMainFrm) return 0; + CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive(); + if (pMDIActive) + { + CView *pView = pMDIActive->GetActiveView(); + if ((pView) && (pView->GetDocument() == this)) + { + return ((CChildFrame *)pMDIActive)->ActivateView(nIdView, dwParam); + } + } + POSITION pos = GetFirstViewPosition(); + while (pos != NULL) + { + CView *pView = GetNextView(pos); + if ((pView) && (pView->GetDocument() == this)) + { + CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame(); + pChildFrm->MDIActivate(); + return pChildFrm->ActivateView(nIdView, dwParam); + } + } + return 0; +} + + +// Activate document's window. +void CModDoc::ActivateWindow() +{ + + CChildFrame *pChildFrm = GetChildFrame(); + if(pChildFrm) pChildFrm->MDIActivate(); +} + + +void CModDoc::UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint) +{ + // Tunnel our UpdateHint into an LPARAM + CDocument::UpdateAllViews(pSender, hint.AsLPARAM(), pHint); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->UpdateTree(this, hint, pHint); + + if(hint.GetType()[HINT_MODCHANNELS | HINT_MODTYPE]) + { + auto instance = CChannelManagerDlg::sharedInstance(); + if(instance != nullptr && pHint != instance && instance->GetDocument() == this) + instance->Update(hint, pHint); + } +#ifndef NO_PLUGINS + if(hint.GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES]) + { + for(auto &plug : m_SndFile.m_MixPlugins) + { + auto mixPlug = plug.pMixPlugin; + if(mixPlug != nullptr && mixPlug->GetEditor()) + { + mixPlug->GetEditor()->UpdateView(hint); + } + } + } +#endif +} + + +void CModDoc::UpdateAllViews(UpdateHint hint) +{ + CMainFrame::GetMainFrame()->SendNotifyMessage(WM_MOD_UPDATEVIEWS, reinterpret_cast<WPARAM>(this), hint.AsLPARAM()); +} + + +///////////////////////////////////////////////////////////////////////////// +// CModDoc commands + +void CModDoc::OnFileWaveConvert() +{ + OnFileWaveConvert(ORDERINDEX_INVALID, ORDERINDEX_INVALID); +} + + +void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories) +{ + ASSERT(!encFactories.empty()); + + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + + if ((!pMainFrm) || (!m_SndFile.GetType()) || encFactories.empty()) return; + + CWaveConvert wsdlg(pMainFrm, nMinOrder, nMaxOrder, m_SndFile.Order().GetLengthTailTrimmed() - 1, m_SndFile, encFactories); + { + BypassInputHandler bih; + if (wsdlg.DoModal() != IDOK) return; + } + + EncoderFactoryBase *encFactory = wsdlg.m_Settings.GetEncoderFactory(); + + const mpt::PathString extension = encFactory->GetTraits().fileExtension; + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(extension) + .DefaultFilename(GetPathNameMpt().GetFileName() + P_(".") + extension) + .ExtensionFilter(encFactory->GetTraits().fileDescription + U_(" (*.") + extension.ToUnicode() + U_(")|*.") + extension.ToUnicode() + U_("||")) + .WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir()); + if(!wsdlg.m_Settings.outputToSample && !dlg.Show()) return; + + // will set default dir here because there's no setup option for export dir yet (feel free to add one...) + TrackerSettings::Instance().PathExport.SetDefaultDir(dlg.GetWorkingDirectory(), true); + + mpt::PathString drive, dir, name, ext; + dlg.GetFirstFile().SplitPath(&drive, &dir, &name, &ext); + const mpt::PathString fileName = drive + dir + name; + const mpt::PathString fileExt = ext; + + const ORDERINDEX currentOrd = m_SndFile.m_PlayState.m_nCurrentOrder; + const ROWINDEX currentRow = m_SndFile.m_PlayState.m_nRow; + + int nRenderPasses = 1; + // Channel mode + std::vector<bool> usedChannels; + std::vector<FlagSet<ChannelFlags>> channelFlags; + // Instrument mode + std::vector<bool> instrMuteState; + + // CHN_SYNCMUTE is used with formats where CHN_MUTE would stop processing global effects and could thus mess synchronization between exported channels + const ChannelFlags muteFlag = m_SndFile.m_playBehaviour[kST3NoMutedChannels] ? CHN_SYNCMUTE : CHN_MUTE; + + // Channel mode: save song in multiple wav files (one for each enabled channels) + if(wsdlg.m_bChannelMode) + { + // Don't save empty channels + CheckUsedChannels(usedChannels); + + nRenderPasses = m_SndFile.GetNumChannels(); + channelFlags.resize(nRenderPasses, ChannelFlags(0)); + for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++) + { + // Save channels' flags + channelFlags[i] = m_SndFile.ChnSettings[i].dwFlags; + // Ignore muted channels + if(channelFlags[i][CHN_MUTE]) usedChannels[i] = false; + // Mute each channel + m_SndFile.ChnSettings[i].dwFlags.set(muteFlag); + } + } + // Instrument mode: Same as channel mode, but renders per instrument (or sample) + if(wsdlg.m_bInstrumentMode) + { + if(m_SndFile.GetNumInstruments() == 0) + { + nRenderPasses = m_SndFile.GetNumSamples(); + instrMuteState.resize(nRenderPasses, false); + for(SAMPLEINDEX i = 0; i < m_SndFile.GetNumSamples(); i++) + { + instrMuteState[i] = IsSampleMuted(i + 1); + MuteSample(i + 1, true); + } + } else + { + nRenderPasses = m_SndFile.GetNumInstruments(); + instrMuteState.resize(nRenderPasses, false); + for(INSTRUMENTINDEX i = 0; i < m_SndFile.GetNumInstruments(); i++) + { + instrMuteState[i] = IsInstrumentMuted(i + 1); + MuteInstrument(i + 1, true); + } + } + } + + pMainFrm->PauseMod(this); + int oldRepeat = m_SndFile.GetRepeatCount(); + + const SEQUENCEINDEX currentSeq = m_SndFile.Order.GetCurrentSequenceIndex(); + for(SEQUENCEINDEX seq = wsdlg.m_Settings.minSequence; seq <= wsdlg.m_Settings.maxSequence; seq++) + { + m_SndFile.Order.SetSequence(seq); + mpt::ustring fileNameAdd; + for(int i = 0; i < nRenderPasses; i++) + { + mpt::PathString thisName = fileName; + CString caption = _T("file"); + fileNameAdd.clear(); + if(wsdlg.m_Settings.minSequence != wsdlg.m_Settings.maxSequence) + { + fileNameAdd = MPT_UFORMAT("-{}")(mpt::ufmt::dec0<2>(seq + 1)); + mpt::ustring seqName = m_SndFile.Order(seq).GetName(); + if(!seqName.empty()) + { + fileNameAdd += UL_("-") + seqName; + } + } + + // Channel mode + if(wsdlg.m_bChannelMode) + { + // Re-mute previously processed channel + if(i > 0) + m_SndFile.ChnSettings[i - 1].dwFlags.set(muteFlag); + + // Was this channel actually muted? Don't process it then. + if(!usedChannels[i]) + continue; + + // Add channel number & name (if available) to path string + if(!m_SndFile.ChnSettings[i].szName.empty()) + { + fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName)); + caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.ChnSettings[i].szName)); + } else + { + fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1)); + caption = MPT_CFORMAT("channel {}")(i + 1); + } + // Unmute channel to process + m_SndFile.ChnSettings[i].dwFlags.reset(muteFlag); + } + // Instrument mode + if(wsdlg.m_bInstrumentMode) + { + if(m_SndFile.GetNumInstruments() == 0) + { + // Re-mute previously processed sample + if(i > 0) MuteSample(static_cast<SAMPLEINDEX>(i), true); + + if(!m_SndFile.GetSample(static_cast<SAMPLEINDEX>(i + 1)).HasSampleData() || !IsSampleUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i]) + continue; + + // Add sample number & name (if available) to path string + if(!m_SndFile.m_szNames[i + 1].empty()) + { + fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1])); + caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.m_szNames[i + 1])); + } else + { + fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1)); + caption = MPT_CFORMAT("sample {}")(i + 1); + } + // Unmute sample to process + MuteSample(static_cast<SAMPLEINDEX>(i + 1), false); + } else + { + // Re-mute previously processed instrument + if(i > 0) MuteInstrument(static_cast<INSTRUMENTINDEX>(i), true); + + if(m_SndFile.Instruments[i + 1] == nullptr || !IsInstrumentUsed(static_cast<SAMPLEINDEX>(i + 1), false) || instrMuteState[i]) + continue; + + if(!m_SndFile.Instruments[i + 1]->name.empty()) + { + fileNameAdd += MPT_UFORMAT("-{}_{}")(mpt::ufmt::dec0<3>(i + 1), mpt::ToUnicode(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name)); + caption = MPT_CFORMAT("{}: {}")(i + 1, mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.Instruments[i + 1]->name)); + } else + { + fileNameAdd += MPT_UFORMAT("-{}")(mpt::ufmt::dec0<3>(i + 1)); + caption = MPT_CFORMAT("instrument {}")(i + 1); + } + // Unmute instrument to process + MuteInstrument(static_cast<SAMPLEINDEX>(i + 1), false); + } + } + + if(!fileNameAdd.empty()) + { + SanitizeFilename(fileNameAdd); + thisName += mpt::PathString::FromUnicode(fileNameAdd); + } + thisName += fileExt; + if(wsdlg.m_Settings.outputToSample) + { + thisName = mpt::CreateTempFileName(P_("OpenMPT")); + // Ensure this temporary file is marked as temporary in the file system, to increase the chance it will never be written to disk + HANDLE hFile = ::CreateFile(thisName.AsNative().c_str(), GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_TEMPORARY, NULL); + if(hFile != INVALID_HANDLE_VALUE) + { + ::CloseHandle(hFile); + } + } + + // Render song (or current channel, or current sample/instrument) + bool cancel = true; + try + { + mpt::SafeOutputFile safeFileStream(thisName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = safeFileStream; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + + if(!f) + { + Reporting::Error("Could not open file for writing. Is it open in another application?"); + } else + { + BypassInputHandler bih; + CDoWaveConvert dwcdlg(m_SndFile, f, caption, wsdlg.m_Settings, pMainFrm); + dwcdlg.m_bGivePlugsIdleTime = wsdlg.m_bGivePlugsIdleTime; + dwcdlg.m_dwSongLimit = wsdlg.m_dwSongLimit; + cancel = dwcdlg.DoModal() != IDOK; + } + } catch(const std::exception &) + { + Reporting::Error(_T("Error while writing file!")); + } + + if(wsdlg.m_Settings.outputToSample) + { + if(!cancel) + { + InputFile f(thisName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(f.IsValid()) + { + FileReader file = GetFileReader(f); + SAMPLEINDEX smp = wsdlg.m_Settings.sampleSlot; + if(smp == 0 || smp > GetNumSamples()) smp = m_SndFile.GetNextFreeSample(); + if(smp == SAMPLEINDEX_INVALID) + { + Reporting::Error(_T("Too many samples!")); + cancel = true; + } + if(!cancel) + { + if(GetNumSamples() < smp) m_SndFile.m_nSamples = smp; + GetSampleUndo().PrepareUndo(smp, sundo_replace, "Render To Sample"); + if(m_SndFile.ReadSampleFromFile(smp, file, false)) + { + m_SndFile.m_szNames[smp] = "Render To Sample" + mpt::ToCharset(m_SndFile.GetCharsetInternal(), fileNameAdd); + UpdateAllViews(nullptr, SampleHint().Info().Data().Names()); + if(m_SndFile.GetNumInstruments() && !IsSampleUsed(smp)) + { + // Insert new instrument for the generated sample in case it is not referenced by any instruments yet. + // It should only be already referenced if the user chose to export to an existing sample slot. + InsertInstrument(smp); + UpdateAllViews(nullptr, InstrumentHint().Info().Names()); + } + SetModified(); + } else + { + GetSampleUndo().RemoveLastUndoStep(smp); + } + } + } + } + + // Always clean up after ourselves + for(int retry = 0; retry < 10; retry++) + { + // stupid virus scanners + if(DeleteFile(thisName.AsNative().c_str()) != EACCES) + { + break; + } + Sleep(10); + } + } + + if(cancel) break; + } + } + + // Restore channels' flags + if(wsdlg.m_bChannelMode) + { + for(CHANNELINDEX i = 0; i < m_SndFile.GetNumChannels(); i++) + { + m_SndFile.ChnSettings[i].dwFlags = channelFlags[i]; + } + } + // Restore instruments' / samples' flags + if(wsdlg.m_bInstrumentMode) + { + for(size_t i = 0; i < instrMuteState.size(); i++) + { + if(m_SndFile.GetNumInstruments() == 0) + MuteSample(static_cast<SAMPLEINDEX>(i + 1), instrMuteState[i]); + else + MuteInstrument(static_cast<INSTRUMENTINDEX>(i + 1), instrMuteState[i]); + } + } + + m_SndFile.Order.SetSequence(currentSeq); + m_SndFile.SetRepeatCount(oldRepeat); + m_SndFile.GetLength(eAdjust, GetLengthTarget(currentOrd, currentRow)); + m_SndFile.m_PlayState.m_nNextOrder = currentOrd; + m_SndFile.m_PlayState.m_nNextRow = currentRow; + CMainFrame::UpdateAudioParameters(m_SndFile, true); +} + + +void CModDoc::OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder) +{ + WAVEncoder wavencoder; + FLACEncoder flacencoder; + AUEncoder auencoder; + OggOpusEncoder opusencoder; + VorbisEncoder vorbisencoder; + MP3Encoder mp3lame(MP3EncoderLame); + MP3Encoder mp3lamecompatible(MP3EncoderLameCompatible); + RAWEncoder rawencoder; + std::vector<EncoderFactoryBase*> encoders; + if(wavencoder.IsAvailable()) encoders.push_back(&wavencoder); + if(flacencoder.IsAvailable()) encoders.push_back(&flacencoder); + if(auencoder.IsAvailable()) encoders.push_back(&auencoder); + if(rawencoder.IsAvailable()) encoders.push_back(&rawencoder); + if(opusencoder.IsAvailable()) encoders.push_back(&opusencoder); + if(vorbisencoder.IsAvailable()) encoders.push_back(&vorbisencoder); + if(mp3lame.IsAvailable()) + { + encoders.push_back(&mp3lame); + } + if(mp3lamecompatible.IsAvailable()) encoders.push_back(&mp3lamecompatible); + OnFileWaveConvert(nMinOrder, nMaxOrder, encoders); +} + + +void CModDoc::OnFileMidiConvert() +{ +#ifndef NO_PLUGINS + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + + if ((!pMainFrm) || (!m_SndFile.GetType())) return; + + mpt::PathString filename = GetPathNameMpt().ReplaceExt(P_(".mid")); + + FileDialog dlg = SaveFileDialog() + .DefaultExtension("mid") + .DefaultFilename(filename) + .ExtensionFilter("MIDI Files (*.mid)|*.mid||"); + if(!dlg.Show()) return; + + CModToMidi mididlg(m_SndFile, pMainFrm); + BypassInputHandler bih; + if(mididlg.DoModal() == IDOK) + { + try + { + mpt::SafeOutputFile sf(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + + if(!f.good()) + { + Reporting::Error("Could not open file for writing. Is it open in another application?"); + return; + } + + CDoMidiConvert doconv(m_SndFile, f, mididlg.m_instrMap); + doconv.DoModal(); + } catch(const std::exception &) + { + Reporting::Error(_T("Error while writing file!")); + } + } +#else + Reporting::Error("In order to use MIDI export, OpenMPT must be built with plugin support."); +#endif // NO_PLUGINS +} + +//HACK: This is a quick fix. Needs to be better integrated into player and GUI. +void CModDoc::OnFileCompatibilitySave() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (!pMainFrm) return; + + CString pattern; + + const MODTYPE type = m_SndFile.GetType(); + switch(type) + { + case MOD_TYPE_IT: + pattern = FileFilterIT; + MsgBoxHidable(CompatExportDefaultWarning); + break; + case MOD_TYPE_XM: + pattern = FileFilterXM; + MsgBoxHidable(CompatExportDefaultWarning); + break; + default: + // Not available for this format. + return; + } + + const std::string ext = m_SndFile.GetModSpecifications().fileExtension; + + mpt::PathString filename; + + { + mpt::PathString drive; + mpt::PathString dir; + mpt::PathString fileName; + GetPathNameMpt().SplitPath(&drive, &dir, &fileName, nullptr); + + filename = drive; + filename += dir; + filename += fileName; + if(!strstr(fileName.ToUTF8().c_str(), "compat")) + filename += P_(".compat."); + else + filename += P_("."); + filename += mpt::PathString::FromUTF8(ext); + } + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(ext) + .DefaultFilename(filename) + .ExtensionFilter(pattern) + .WorkingDirectory(TrackerSettings::Instance().PathSongs.GetWorkingDir()); + if(!dlg.Show()) return; + + filename = dlg.GetFirstFile(); + + bool ok = false; + BeginWaitCursor(); + try + { + mpt::SafeOutputFile sf(filename, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + if(f) + { + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + ScopedLogCapturer logcapturer(*this); + FixNullStrings(); + switch(type) + { + case MOD_TYPE_XM: ok = m_SndFile.SaveXM(f, true); break; + case MOD_TYPE_IT: ok = m_SndFile.SaveIT(f, filename, true); break; + default: MPT_ASSERT_NOTREACHED(); + } + } + } catch(const std::exception &) + { + ok = false; + } + EndWaitCursor(); + + if(!ok) + { + ErrorBox(IDS_ERR_SAVESONG, CMainFrame::GetMainFrame()); + } +} + + +void CModDoc::OnPlayerPlay() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) + { + CChildFrame *pChildFrm = GetChildFrame(); + if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) + { + //User has sent play song command: set loop pattern checkbox to false. + pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0); + } + + bool isPlaying = (pMainFrm->GetModPlaying() == this); + if(isPlaying && !m_SndFile.m_SongFlags[SONG_PAUSED | SONG_STEP/*|SONG_PATTERNLOOP*/]) + { + OnPlayerPause(); + return; + } + + CriticalSection cs; + + // Kill editor voices + for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) if (m_SndFile.m_PlayState.Chn[i].isPreviewNote) + { + m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + if (!isPlaying) m_SndFile.m_PlayState.Chn[i].nLength = 0; + } + + m_SndFile.m_PlayState.m_bPositionChanged = true; + + if(isPlaying) + { + m_SndFile.StopAllVsti(); + } + + cs.Leave(); + + m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PAUSED | SONG_PATTERNLOOP); + pMainFrm->PlayMod(this); + } +} + + +void CModDoc::OnPlayerPause() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) + { + if (pMainFrm->GetModPlaying() == this) + { + bool isLooping = m_SndFile.m_SongFlags[SONG_PATTERNLOOP]; + PATTERNINDEX nPat = m_SndFile.m_PlayState.m_nPattern; + ROWINDEX nRow = m_SndFile.m_PlayState.m_nRow; + ROWINDEX nNextRow = m_SndFile.m_PlayState.m_nNextRow; + pMainFrm->PauseMod(); + + if ((isLooping) && (nPat < m_SndFile.Patterns.Size())) + { + CriticalSection cs; + + if ((m_SndFile.m_PlayState.m_nCurrentOrder < m_SndFile.Order().GetLength()) && (m_SndFile.Order()[m_SndFile.m_PlayState.m_nCurrentOrder] == nPat)) + { + m_SndFile.m_PlayState.m_nNextOrder = m_SndFile.m_PlayState.m_nCurrentOrder; + m_SndFile.m_PlayState.m_nNextRow = nNextRow; + m_SndFile.m_PlayState.m_nRow = nRow; + } else + { + for (ORDERINDEX nOrd = 0; nOrd < m_SndFile.Order().GetLength(); nOrd++) + { + if (m_SndFile.Order()[nOrd] == m_SndFile.Order.GetInvalidPatIndex()) break; + if (m_SndFile.Order()[nOrd] == nPat) + { + m_SndFile.m_PlayState.m_nCurrentOrder = nOrd; + m_SndFile.m_PlayState.m_nNextOrder = nOrd; + m_SndFile.m_PlayState.m_nNextRow = nNextRow; + m_SndFile.m_PlayState.m_nRow = nRow; + break; + } + } + } + } + } else + { + pMainFrm->PauseMod(); + } + } +} + + +void CModDoc::OnPlayerStop() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->StopMod(); +} + + +void CModDoc::OnPlayerPlayFromStart() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) + { + CChildFrame *pChildFrm = GetChildFrame(); + if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) + { + //User has sent play song command: set loop pattern checkbox to false. + pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0); + } + + pMainFrm->PauseMod(); + CriticalSection cs; + m_SndFile.m_SongFlags.reset(SONG_STEP | SONG_PATTERNLOOP); + m_SndFile.ResetPlayPos(); + //m_SndFile.visitedSongRows.Initialize(true); + + m_SndFile.m_PlayState.m_bPositionChanged = true; + + cs.Leave(); + + pMainFrm->PlayMod(this); + } +} + + +void CModDoc::OnEditGlobals() +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_GLOBALS); +} + + +void CModDoc::OnEditPatterns() +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_PATTERNS, -1); +} + + +void CModDoc::OnEditSamples() +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_SAMPLES, -1); +} + + +void CModDoc::OnEditInstruments() +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_INSTRUMENTS, -1); +} + + +void CModDoc::OnEditComments() +{ + SendMessageToActiveView(WM_MOD_ACTIVATEVIEW, IDD_CONTROL_COMMENTS); +} + + +void CModDoc::OnShowCleanup() +{ + CModCleanupDlg dlg(*this, CMainFrame::GetMainFrame()); + dlg.DoModal(); +} + + +void CModDoc::OnSetupZxxMacros() +{ + CMidiMacroSetup dlg(m_SndFile); + if(dlg.DoModal() == IDOK) + { + if(m_SndFile.m_MidiCfg != dlg.m_MidiCfg) + { + m_SndFile.m_MidiCfg = dlg.m_MidiCfg; + SetModified(); + } + } +} + + +// Enable menu item only module types that support MIDI Mappings +void CModDoc::OnUpdateHasMIDIMappings(CCmdUI *p) +{ + if(p) + p->Enable((m_SndFile.GetModSpecifications().MIDIMappingDirectivesMax > 0) ? TRUE : FALSE); +} + + +// Enable menu item only for IT / MPTM / XM files +void CModDoc::OnUpdateXMITMPTOnly(CCmdUI *p) +{ + if (p) + p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE); +} + + +// Enable menu item only for IT / MPTM files +void CModDoc::OnUpdateHasEditHistory(CCmdUI *p) +{ + if (p) + p->Enable(((m_SndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) || !m_SndFile.GetFileHistory().empty()) ? TRUE : FALSE); +} + + +// Enable menu item if current module type supports compatibility export +void CModDoc::OnUpdateCompatExportableOnly(CCmdUI *p) +{ + if(p) + p->Enable((m_SndFile.GetType() & (MOD_TYPE_XM | MOD_TYPE_IT)) ? TRUE : FALSE); +} + + +static CString FormatSongLength(double length) +{ + length = mpt::round(length); + double minutes = std::floor(length / 60.0), seconds = std::fmod(length, 60.0); + CString s; + s.Format(_T("%.0fmn%02.0fs"), minutes, seconds); + return s; +} + + +void CModDoc::OnEstimateSongLength() +{ + CString s = _T("Approximate song length: "); + const auto subSongs = m_SndFile.GetAllSubSongs(); + if (subSongs.empty()) + { + Reporting::Information(_T("No patterns found!")); + return; + } + + std::vector<uint32> songsPerSequence(m_SndFile.Order.GetNumSequences(), 0); + SEQUENCEINDEX prevSeq = subSongs[0].sequence; + for(const auto &song : subSongs) + { + songsPerSequence[song.sequence]++; + if(prevSeq != song.sequence) + prevSeq = SEQUENCEINDEX_INVALID; + } + + double totalLength = 0.0; + uint32 songCount = 0; + // If there are multiple sequences, indent their subsongs + const TCHAR *indent = (prevSeq == SEQUENCEINDEX_INVALID) ? _T("\t") : _T(""); + for(const auto &song : subSongs) + { + double songLength = song.duration; + if(subSongs.size() > 1) + { + totalLength += songLength; + if(prevSeq != song.sequence) + { + songCount = 0; + prevSeq = song.sequence; + if(m_SndFile.Order(prevSeq).GetName().empty()) + s.AppendFormat(_T("\nSequence %u:"), prevSeq + 1u); + else + s.AppendFormat(_T("\nSequence %u (%s):"), prevSeq + 1u, mpt::ToWin(m_SndFile.Order(prevSeq).GetName()).c_str()); + } + songCount++; + if(songsPerSequence[song.sequence] > 1) + s.AppendFormat(_T("\n%sSong %u, starting at order %u:\t"), indent, songCount, song.startOrder); + else + s.AppendChar(_T('\t')); + } + if(songLength != std::numeric_limits<double>::infinity()) + { + songLength = mpt::round(songLength); + s += FormatSongLength(songLength); + } else + { + s += _T("Song too long!"); + } + } + if(subSongs.size() > 1 && totalLength != std::numeric_limits<double>::infinity()) + { + s += _T("\n\nTotal length:\t") + FormatSongLength(totalLength); + } + + Reporting::Information(s); +} + + +void CModDoc::OnApproximateBPM() +{ + if(CMainFrame::GetMainFrame()->GetModPlaying() != this) + { + m_SndFile.m_PlayState.m_nCurrentRowsPerBeat = m_SndFile.m_nDefaultRowsPerBeat; + m_SndFile.m_PlayState.m_nCurrentRowsPerMeasure = m_SndFile.m_nDefaultRowsPerMeasure; + } + m_SndFile.RecalculateSamplesPerTick(); + const double bpm = m_SndFile.GetCurrentBPM(); + + CString s; + switch(m_SndFile.m_nTempoMode) + { + case TempoMode::Alternative: + s.Format(_T("Using alternative tempo interpretation.\n\nAssuming:\n. %.8g ticks per second\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"), + m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm); + break; + + case TempoMode::Modern: + s.Format(_T("Using modern tempo interpretation.\n\nThe tempo is: %.8g BPM"), bpm); + break; + + case TempoMode::Classic: + default: + s.Format(_T("Using standard tempo interpretation.\n\nAssuming:\n. A mod tempo (tick duration factor) of %.8g\n. %u ticks per row\n. %u rows per beat\nthe tempo is approximately: %.8g BPM"), + m_SndFile.m_PlayState.m_nMusicTempo.ToDouble(), m_SndFile.m_PlayState.m_nMusicSpeed, m_SndFile.m_PlayState.m_nCurrentRowsPerBeat, bpm); + break; + } + + Reporting::Information(s); +} + + +CChildFrame *CModDoc::GetChildFrame() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (!pMainFrm) return nullptr; + CMDIChildWnd *pMDIActive = pMainFrm->MDIGetActive(); + if (pMDIActive) + { + CView *pView = pMDIActive->GetActiveView(); + if ((pView) && (pView->GetDocument() == this)) + return static_cast<CChildFrame *>(pMDIActive); + } + POSITION pos = GetFirstViewPosition(); + while (pos != NULL) + { + CView *pView = GetNextView(pos); + if ((pView) && (pView->GetDocument() == this)) + return static_cast<CChildFrame *>(pView->GetParentFrame()); + } + + return nullptr; +} + + +// Get the currently edited pattern position. Note that ord might be ORDERINDEX_INVALID when editing a pattern that is not present in the order list. +void CModDoc::GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord) +{ + CChildFrame *pChildFrm = GetChildFrame(); + + if(strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) // dirty HACK + { + PATTERNVIEWSTATE patternViewState; + pChildFrm->SendViewMessage(VIEWMSG_SAVESTATE, (LPARAM)(&patternViewState)); + + pat = patternViewState.nPattern; + row = patternViewState.cursor.GetRow(); + ord = patternViewState.nOrder; + } else + { + //patern editor object does not exist (i.e. is not active) - use saved state. + PATTERNVIEWSTATE &patternViewState = pChildFrm->GetPatternViewState(); + + pat = patternViewState.nPattern; + row = patternViewState.cursor.GetRow(); + ord = patternViewState.nOrder; + } + + const auto &order = m_SndFile.Order(); + if(order.empty()) + { + ord = ORDERINDEX_INVALID; + pat = 0; + row = 0; + } else if(ord >= order.size()) + { + ord = 0; + pat = m_SndFile.Order()[ord]; + } + if(!m_SndFile.Patterns.IsValidPat(pat)) + { + pat = 0; + row = 0; + } else if(row >= m_SndFile.Patterns[pat].GetNumRows()) + { + row = 0; + } + + //ensure order correlates with pattern. + if(ord >= order.size() || order[ord] != pat) + { + ord = order.FindOrder(pat); + } +} + + +//////////////////////////////////////////////////////////////////////////////////////// +// Playback + + +void CModDoc::OnPatternRestart(bool loop) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CChildFrame *pChildFrm = GetChildFrame(); + + if ((pMainFrm) && (pChildFrm)) + { + if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) + { + //User has sent play pattern command: set loop pattern checkbox to true. + pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, loop ? 1 : 0); + } + + ROWINDEX nRow; + PATTERNINDEX nPat; + ORDERINDEX nOrd; + GetEditPosition(nRow, nPat, nOrd); + CModDoc *pModPlaying = pMainFrm->GetModPlaying(); + + CriticalSection cs; + + // Cut instruments/samples + for(auto &chn : m_SndFile.m_PlayState.Chn) + { + chn.nPatternLoopCount = 0; + chn.nPatternLoop = 0; + chn.nFadeOutVol = 0; + chn.dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + } + if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd; + m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP); + if(loop) + m_SndFile.LoopPattern(nPat); + else + m_SndFile.LoopPattern(PATTERNINDEX_INVALID); + + // set playback timer in the status bar (and update channel status) + SetElapsedTime(nOrd, 0, true); + + if(pModPlaying == this) + { + m_SndFile.StopAllVsti(); + } + + cs.Leave(); + + if(pModPlaying != this) + { + SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem); + SetFollowWnd(pChildFrm->GetHwndView()); + pMainFrm->PlayMod(this); //rewbs.fix2977 + } + } + //SwitchToView(); +} + +void CModDoc::OnPatternPlay() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CChildFrame *pChildFrm = GetChildFrame(); + + if ((pMainFrm) && (pChildFrm)) + { + if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) + { + //User has sent play pattern command: set loop pattern checkbox to true. + pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 1); + } + + ROWINDEX nRow; + PATTERNINDEX nPat; + ORDERINDEX nOrd; + GetEditPosition(nRow, nPat, nOrd); + CModDoc *pModPlaying = pMainFrm->GetModPlaying(); + + CriticalSection cs; + + // Cut instruments/samples + for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) + { + m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + } + if ((nOrd < m_SndFile.Order().size()) && (m_SndFile.Order()[nOrd] == nPat)) m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd; + m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP); + m_SndFile.LoopPattern(nPat); + + // set playback timer in the status bar (and update channel status) + SetElapsedTime(nOrd, nRow, true); + + if(pModPlaying == this) + { + m_SndFile.StopAllVsti(); + } + + cs.Leave(); + + if(pModPlaying != this) + { + SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem); + SetFollowWnd(pChildFrm->GetHwndView()); + pMainFrm->PlayMod(this); //rewbs.fix2977 + } + } + //SwitchToView(); + +} + +void CModDoc::OnPatternPlayNoLoop() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CChildFrame *pChildFrm = GetChildFrame(); + + if ((pMainFrm) && (pChildFrm)) + { + if (strcmp("CViewPattern", pChildFrm->GetCurrentViewClassName()) == 0) + { + //User has sent play song command: set loop pattern checkbox to false. + pChildFrm->SendViewMessage(VIEWMSG_PATTERNLOOP, 0); + } + + ROWINDEX nRow; + PATTERNINDEX nPat; + ORDERINDEX nOrd; + GetEditPosition(nRow, nPat, nOrd); + CModDoc *pModPlaying = pMainFrm->GetModPlaying(); + + CriticalSection cs; + // Cut instruments/samples + for(CHANNELINDEX i = m_SndFile.GetNumChannels(); i < MAX_CHANNELS; i++) + { + m_SndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + } + m_SndFile.m_SongFlags.reset(SONG_PAUSED | SONG_STEP); + m_SndFile.SetCurrentOrder(nOrd); + if(nOrd < m_SndFile.Order().size() && m_SndFile.Order()[nOrd] == nPat) + m_SndFile.DontLoopPattern(nPat, nRow); + else + m_SndFile.LoopPattern(nPat); + + // set playback timer in the status bar (and update channel status) + SetElapsedTime(nOrd, nRow, true); + + if(pModPlaying == this) + { + m_SndFile.StopAllVsti(); + } + + cs.Leave(); + + if(pModPlaying != this) + { + SetNotifications(m_notifyType | Notification::Position | Notification::VUMeters, m_notifyItem); + SetFollowWnd(pChildFrm->GetHwndView()); + pMainFrm->PlayMod(this); //rewbs.fix2977 + } + } + //SwitchToView(); +} + + +void CModDoc::OnViewEditHistory() +{ + CEditHistoryDlg dlg(CMainFrame::GetMainFrame(), *this); + dlg.DoModal(); +} + + +void CModDoc::OnViewMPTHacks() +{ + ScopedLogCapturer logcapturer(*this); + if(!HasMPTHacks()) + { + AddToLog("No hacks found."); + } +} + + +void CModDoc::OnViewTempoSwingSettings() +{ + if(m_SndFile.m_nDefaultRowsPerBeat > 0 && m_SndFile.m_nTempoMode == TempoMode::Modern) + { + TempoSwing tempoSwing = m_SndFile.m_tempoSwing; + tempoSwing.resize(m_SndFile.m_nDefaultRowsPerBeat, TempoSwing::Unity); + CTempoSwingDlg dlg(CMainFrame::GetMainFrame(), tempoSwing, m_SndFile); + if(dlg.DoModal() == IDOK) + { + SetModified(); + m_SndFile.m_tempoSwing = dlg.m_tempoSwing; + } + } else if(GetModType() == MOD_TYPE_MPT) + { + Reporting::Error(_T("Modern tempo mode needs to be enabled in order to edit tempo swing settings.")); + OnSongProperties(); + } +} + + +LRESULT CModDoc::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + const auto &modSpecs = m_SndFile.GetModSpecifications(); + switch(wParam) + { + case kcViewGeneral: OnEditGlobals(); break; + case kcViewPattern: OnEditPatterns(); break; + case kcViewSamples: OnEditSamples(); break; + case kcViewInstruments: OnEditInstruments(); break; + case kcViewComments: OnEditComments(); break; + case kcViewSongProperties: OnSongProperties(); break; + case kcViewTempoSwing: OnViewTempoSwingSettings(); break; + case kcShowMacroConfig: OnSetupZxxMacros(); break; + case kcViewMIDImapping: OnViewMIDIMapping(); break; + case kcViewEditHistory: OnViewEditHistory(); break; + case kcViewChannelManager: OnChannelManager(); break; + + case kcFileSaveAsWave: OnFileWaveConvert(); break; + case kcFileSaveMidi: OnFileMidiConvert(); break; + case kcFileSaveOPL: OnFileOPLExport(); break; + case kcFileExportCompat: OnFileCompatibilitySave(); break; + case kcEstimateSongLength: OnEstimateSongLength(); break; + case kcApproxRealBPM: OnApproximateBPM(); break; + case kcFileSave: DoSave(GetPathNameMpt()); break; + case kcFileSaveAs: DoSave(mpt::PathString()); break; + case kcFileSaveCopy: OnSaveCopy(); break; + case kcFileSaveTemplate: OnSaveTemplateModule(); break; + case kcFileClose: SafeFileClose(); break; + case kcFileAppend: OnAppendModule(); break; + + case kcPlayPatternFromCursor: OnPatternPlay(); break; + case kcPlayPatternFromStart: OnPatternRestart(); break; + case kcPlaySongFromCursor: OnPatternPlayNoLoop(); break; + case kcPlaySongFromStart: OnPlayerPlayFromStart(); break; + case kcPlayPauseSong: OnPlayerPlay(); break; + case kcPlaySongFromPattern: OnPatternRestart(false); break; + case kcStopSong: OnPlayerStop(); break; + case kcPanic: OnPanic(); break; + case kcToggleLoopSong: SetLoopSong(!TrackerSettings::Instance().gbLoopSong); break; + + case kcTempoIncreaseFine: + if(!modSpecs.hasFractionalTempo) + break; + [[fallthrough]]; + case kcTempoIncrease: + if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo < modSpecs.GetTempoMax()) + m_SndFile.m_PlayState.m_nMusicTempo = std::min(modSpecs.GetTempoMax(), tempo + TEMPO(wParam == kcTempoIncrease ? 1.0 : 0.1)); + break; + case kcTempoDecreaseFine: + if(!modSpecs.hasFractionalTempo) + break; + [[fallthrough]]; + case kcTempoDecrease: + if(auto tempo = m_SndFile.m_PlayState.m_nMusicTempo; tempo > modSpecs.GetTempoMin()) + m_SndFile.m_PlayState.m_nMusicTempo = std::max(modSpecs.GetTempoMin(), tempo - TEMPO(wParam == kcTempoDecrease ? 1.0 : 0.1)); + break; + case kcSpeedIncrease: + if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed < modSpecs.speedMax) + m_SndFile.m_PlayState.m_nMusicSpeed = speed + 1; + break; + case kcSpeedDecrease: + if(auto speed = m_SndFile.m_PlayState.m_nMusicSpeed; speed > modSpecs.speedMin) + m_SndFile.m_PlayState.m_nMusicSpeed = speed - 1; + break; + + case kcViewToggle: + if(auto *lastActiveFrame = CChildFrame::LastActiveFrame(); lastActiveFrame != nullptr) + lastActiveFrame->ToggleViews(); + break; + + default: return kcNull; + } + + return wParam; +} + + +void CModDoc::TogglePluginEditor(UINT plugin, bool onlyThisEditor) +{ + if(plugin < MAX_MIXPLUGINS) + { + IMixPlugin *pPlugin = m_SndFile.m_MixPlugins[plugin].pMixPlugin; + if(pPlugin != nullptr) + { + if(onlyThisEditor) + { + int32 posX = int32_min, posY = int32_min; + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + SNDMIXPLUGIN &otherPlug = m_SndFile.m_MixPlugins[i]; + if(i != plugin && otherPlug.pMixPlugin != nullptr && otherPlug.pMixPlugin->GetEditor() != nullptr) + { + otherPlug.pMixPlugin->CloseEditor(); + if(otherPlug.editorX != int32_min) + { + posX = otherPlug.editorX; + posY = otherPlug.editorY; + } + } + } + if(posX != int32_min) + { + m_SndFile.m_MixPlugins[plugin].editorX = posX; + m_SndFile.m_MixPlugins[plugin].editorY = posY; + } + } + + pPlugin->ToggleEditor(); + } + } +} + + +void CModDoc::SetLoopSong(bool loop) +{ + TrackerSettings::Instance().gbLoopSong = loop; + m_SndFile.SetRepeatCount(loop ? -1 : 0); + CMainFrame::GetMainFrame()->UpdateAllViews(UpdateHint().MPTOptions()); +} + + +void CModDoc::ChangeFileExtension(MODTYPE nNewType) +{ + //Not making path if path is empty(case only(?) for new file) + if(!GetPathNameMpt().empty()) + { + mpt::PathString drive; + mpt::PathString dir; + mpt::PathString fname; + mpt::PathString fext; + GetPathNameMpt().SplitPath(&drive, &dir, &fname, &fext); + + mpt::PathString newPath = drive + dir; + + // Catch case where we don't have a filename yet. + if(fname.empty()) + { + newPath += mpt::PathString::FromCString(GetTitle()).SanitizeComponent(); + } else + { + newPath += fname; + } + + newPath += P_(".") + mpt::PathString::FromUTF8(CSoundFile::GetModSpecifications(nNewType).fileExtension); + + // Forcing save dialog to appear after extension change - otherwise unnotified file overwriting may occur. + m_ShowSavedialog = true; + + SetPathName(newPath, FALSE); + } + + UpdateAllViews(NULL, UpdateHint().ModType()); +} + + +CHANNELINDEX CModDoc::FindAvailableChannel() const +{ + CHANNELINDEX chn = m_SndFile.GetNNAChannel(CHANNELINDEX_INVALID); + if(chn != CHANNELINDEX_INVALID) + return chn; + else + return GetNumChannels(); +} + + +void CModDoc::RecordParamChange(PLUGINDEX plugSlot, PlugParamIndex paramIndex) +{ + ::SendNotifyMessage(m_hWndFollow, WM_MOD_RECORDPARAM, plugSlot, paramIndex); +} + + +void CModDoc::LearnMacro(int macroToSet, PlugParamIndex paramToUse) +{ + if(macroToSet < 0 || macroToSet > kSFxMacros) + { + return; + } + + // If macro already exists for this param, inform user and return + if(auto macro = m_SndFile.m_MidiCfg.FindMacroForParam(paramToUse); macro >= 0) + { + CString message; + message.Format(_T("Parameter %i can already be controlled with macro %X."), static_cast<int>(paramToUse), macro); + Reporting::Information(message, _T("Macro exists for this parameter")); + return; + } + + // Set new macro + if(paramToUse < 384) + { + m_SndFile.m_MidiCfg.CreateParameteredMacro(macroToSet, kSFxPlugParam, paramToUse); + } else + { + CString message; + message.Format(_T("Parameter %i beyond controllable range. Use Parameter Control Events to automate this parameter."), static_cast<int>(paramToUse)); + Reporting::Information(message, _T("Macro not assigned for this parameter")); + return; + } + + CString message; + message.Format(_T("Parameter %i can now be controlled with macro %X."), static_cast<int>(paramToUse), macroToSet); + Reporting::Information(message, _T("Macro assigned for this parameter")); + + return; +} + + +void CModDoc::OnSongProperties() +{ + const bool wasUsingFrequencies = m_SndFile.PeriodsAreFrequencies(); + CModTypeDlg dlg(m_SndFile, CMainFrame::GetMainFrame()); + if(dlg.DoModal() == IDOK) + { + UpdateAllViews(nullptr, GeneralHint().General()); + ScopedLogCapturer logcapturer(*this, _T("Conversion Status")); + bool showLog = false; + if(dlg.m_nType != GetModType()) + { + if(!ChangeModType(dlg.m_nType)) + return; + showLog = true; + } + + CHANNELINDEX newChannels = Clamp(dlg.m_nChannels, m_SndFile.GetModSpecifications().channelsMin, m_SndFile.GetModSpecifications().channelsMax); + if(newChannels != GetNumChannels()) + { + const bool showCancelInRemoveDlg = m_SndFile.GetModSpecifications().channelsMax >= m_SndFile.GetNumChannels(); + if(ChangeNumChannels(newChannels, showCancelInRemoveDlg)) + showLog = true; + + // Force update of pattern highlights / num channels + UpdateAllViews(nullptr, PatternHint().Data()); + UpdateAllViews(nullptr, GeneralHint().Channels()); + } + + if(wasUsingFrequencies != m_SndFile.PeriodsAreFrequencies()) + { + for(auto &chn : m_SndFile.m_PlayState.Chn) + { + chn.nPeriod = 0; + } + } + + SetModified(); + } +} + + +void CModDoc::ViewMIDIMapping(PLUGINDEX plugin, PlugParamIndex param) +{ + CMIDIMappingDialog dlg(CMainFrame::GetMainFrame(), m_SndFile); + if(plugin != PLUGINDEX_INVALID) + { + dlg.m_Setting.SetPlugIndex(plugin + 1); + dlg.m_Setting.SetParamIndex(param); + } + dlg.DoModal(); +} + + +void CModDoc::OnChannelManager() +{ + CChannelManagerDlg *instance = CChannelManagerDlg::sharedInstanceCreate(); + if(instance != nullptr) + { + if(instance->IsDisplayed()) + instance->Hide(); + else + { + instance->SetDocument(this); + instance->Show(); + } + } +} + + +// Sets playback timer to playback time at given position. +// At the same time, the playback parameters (global volume, channel volume and stuff like that) are calculated for this position. +// Sample channels positions are only updated if setSamplePos is true *and* the user has chosen to update sample play positions on seek. +void CModDoc::SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos) +{ + if(nOrd == ORDERINDEX_INVALID) return; + + double t = m_SndFile.GetPlaybackTimeAt(nOrd, nRow, true, setSamplePos && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SYNCSAMPLEPOS) != 0); + if(t < 0) + { + // Position is never played regularly, but we may want to continue playing from here nevertheless. + m_SndFile.m_PlayState.m_nCurrentOrder = m_SndFile.m_PlayState.m_nNextOrder = nOrd; + m_SndFile.m_PlayState.m_nRow = m_SndFile.m_PlayState.m_nNextRow = nRow; + } + + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm != nullptr) pMainFrm->SetElapsedTime(std::max(0.0, t)); +} + + +CString CModDoc::GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr, + bool bEmptyInsteadOfNoName /* = false*/, + bool bIncludeIndex /* = true*/) const +{ + if(nInstr >= MAX_INSTRUMENTS || m_SndFile.GetNumInstruments() == 0 || m_SndFile.Instruments[nInstr] == nullptr) + return CString(); + + CString displayName, instrumentName, pluginName; + + // Get instrument name. + instrumentName = mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetInstrumentName(nInstr)); + + // If instrument name is empty, use name of the sample mapped to C-5. + if (instrumentName.IsEmpty()) + { + const SAMPLEINDEX nSmp = m_SndFile.Instruments[nInstr]->Keyboard[NOTE_MIDDLEC - 1]; + if (nSmp <= m_SndFile.GetNumSamples() && m_SndFile.GetSample(nSmp).HasSampleData()) + instrumentName = _T("s: ") + mpt::ToCString(m_SndFile.GetCharsetInternal(), m_SndFile.GetSampleName(nSmp)); + } + + // Get plugin name. + const PLUGINDEX nPlug = m_SndFile.Instruments[nInstr]->nMixPlug; + if (nPlug > 0 && nPlug < MAX_MIXPLUGINS) + pluginName = mpt::ToCString(m_SndFile.m_MixPlugins[nPlug-1].GetName()); + + if (pluginName.IsEmpty()) + { + if(bEmptyInsteadOfNoName && instrumentName.IsEmpty()) + return TEXT(""); + if(instrumentName.IsEmpty()) + instrumentName = _T("(no name)"); + if (bIncludeIndex) + displayName.Format(_T("%02d: %s"), nInstr, instrumentName.GetString()); + else + displayName = instrumentName; + } else + { + if (bIncludeIndex) + displayName.Format(TEXT("%02d: %s (%s)"), nInstr, instrumentName.GetString(), pluginName.GetString()); + else + displayName.Format(TEXT("%s (%s)"), instrumentName.GetString(), pluginName.GetString()); + } + return displayName; +} + + +void CModDoc::SafeFileClose() +{ + // Verify that the main window has the focus. This saves us a lot of trouble because active modal dialogs cannot know if their pSndFile pointers are still valid. + if(GetActiveWindow() == CMainFrame::GetMainFrame()->m_hWnd) + OnFileClose(); +} + + +// "Panic button". This resets all VSTi, OPL and sample notes. +void CModDoc::OnPanic() +{ + CriticalSection cs; + m_SndFile.ResetChannels(); + m_SndFile.StopAllVsti(); +} + + +// Before saving, make sure that every char after the terminating null char is also null. +// Else, garbage might end up in various text strings that wasn't supposed to be there. +void CModDoc::FixNullStrings() +{ + // Macros + m_SndFile.m_MidiCfg.Sanitize(); +} + + +void CModDoc::OnSaveCopy() +{ + DoSave(mpt::PathString(), false); +} + + +void CModDoc::OnSaveTemplateModule() +{ + // Create template folder if doesn't exist already. + const mpt::PathString templateFolder = TrackerSettings::Instance().PathUserTemplates.GetDefaultDir(); + if (!templateFolder.IsDirectory()) + { + if (!CreateDirectory(templateFolder.AsNative().c_str(), nullptr)) + { + Reporting::Notification(MPT_CFORMAT("Error: Unable to create template folder '{}'")( templateFolder)); + return; + } + } + + // Generate file name candidate. + mpt::PathString sName; + for(size_t i = 0; i < 1000; ++i) + { + sName += P_("newTemplate") + mpt::PathString::FromUnicode(mpt::ufmt::val(i)); + sName += P_(".") + mpt::PathString::FromUTF8(m_SndFile.GetModSpecifications().fileExtension); + if (!(templateFolder + sName).FileOrDirectoryExists()) + break; + } + + // Ask file name from user. + FileDialog dlg = SaveFileDialog() + .DefaultExtension(m_SndFile.GetModSpecifications().fileExtension) + .DefaultFilename(sName) + .ExtensionFilter(ModTypeToFilter(m_SndFile)) + .WorkingDirectory(templateFolder); + if(!dlg.Show()) + return; + + if (OnSaveDocument(dlg.GetFirstFile(), false)) + { + // Update template menu. + CMainFrame::GetMainFrame()->CreateTemplateModulesMenu(); + } +} + + +// Create an undo point that stores undo data for all existing patterns +void CModDoc::PrepareUndoForAllPatterns(bool storeChannelInfo, const char *description) +{ + bool linkUndo = false; + + PATTERNINDEX lastPat = 0; + for(PATTERNINDEX pat = 0; pat < m_SndFile.Patterns.Size(); pat++) + { + if(m_SndFile.Patterns.IsValidPat(pat)) lastPat = pat; + } + + for(PATTERNINDEX pat = 0; pat <= lastPat; pat++) + { + if(m_SndFile.Patterns.IsValidPat(pat)) + { + GetPatternUndo().PrepareUndo(pat, 0, 0, GetNumChannels(), m_SndFile.Patterns[pat].GetNumRows(), description, linkUndo, storeChannelInfo && pat == lastPat); + linkUndo = true; + } + } +} + + +CString CModDoc::LinearToDecibels(double value, double valueAtZeroDB) +{ + if (value == 0) return _T("-inf"); + + double changeFactor = value / valueAtZeroDB; + double dB = 20.0 * std::log10(changeFactor); + + CString s = (dB >= 0) ? _T("+") : _T(""); + s.AppendFormat(_T("%.2f dB"), dB); + return s; +} + + +CString CModDoc::PanningToString(int32 value, int32 valueAtCenter) +{ + if(value == valueAtCenter) + return _T("Center"); + + CString s; + s.Format(_T("%i%% %s"), (std::abs(static_cast<int>(value) - valueAtCenter) * 100) / valueAtCenter, value < valueAtCenter ? _T("Left") : _T("Right")); + return s; +} + + +// Apply OPL patch changes to live playback +void CModDoc::UpdateOPLInstrument(SAMPLEINDEX smp) +{ + const ModSample &sample = m_SndFile.GetSample(smp); + if(!sample.uFlags[CHN_ADLIB] || !m_SndFile.m_opl || CMainFrame::GetMainFrame()->GetModPlaying() != this) + return; + + CriticalSection cs; + const auto &patch = sample.adlib; + for(CHANNELINDEX chn = 0; chn < MAX_CHANNELS; chn++) + { + const auto &c = m_SndFile.m_PlayState.Chn[chn]; + if(c.pModSample == &sample && c.IsSamplePlaying()) + { + m_SndFile.m_opl->Patch(chn, patch); + } + } +} + + +// Store all view positions t settings file +void CModDoc::SerializeViews() const +{ + const mpt::PathString pathName = theApp.IsPortableMode() ? GetPathNameMpt().AbsolutePathToRelative(theApp.GetInstallPath()) : GetPathNameMpt(); + if(pathName.empty()) + { + return; + } + std::ostringstream f(std::ios::out | std::ios::binary); + + CRect mdiRect; + ::GetClientRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect); + const int width = mdiRect.Width(); + const int height = mdiRect.Height(); + + const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + // Document view positions and sizes + POSITION pos = GetFirstViewPosition(); + while(pos != nullptr && !mdiRect.IsRectEmpty()) + { + CModControlView *pView = dynamic_cast<CModControlView *>(GetNextView(pos)); + if(pView) + { + CChildFrame *pChildFrm = (CChildFrame *)pView->GetParentFrame(); + WINDOWPLACEMENT wnd; + wnd.length = sizeof(WINDOWPLACEMENT); + pChildFrm->GetWindowPlacement(&wnd); + const CRect rect = wnd.rcNormalPosition; + + // Write size information + uint8 windowState = 0; + if(wnd.showCmd == SW_SHOWMAXIMIZED) windowState = 1; + else if(wnd.showCmd == SW_SHOWMINIMIZED) windowState = 2; + mpt::IO::WriteIntLE<uint8>(f, 0); // Window type + mpt::IO::WriteIntLE<uint8>(f, windowState); + mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.left, 1 << 30, width)); + mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.top, 1 << 30, height)); + mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Width(), 1 << 30, width)); + mpt::IO::WriteIntLE<int32>(f, Util::muldivr(rect.Height(), 1 << 30, height)); + + std::string s = pChildFrm->SerializeView(); + mpt::IO::WriteVarInt(f, s.size()); + f << s; + } + } + // Plugin window positions + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + if(m_SndFile.m_MixPlugins[i].IsValidPlugin() && m_SndFile.m_MixPlugins[i].editorX != int32_min && cxScreen && cyScreen) + { + // Translate screen position into percentage (to make it independent of the actual screen resolution) + int32 editorX = Util::muldivr(m_SndFile.m_MixPlugins[i].editorX, 1 << 30, cxScreen); + int32 editorY = Util::muldivr(m_SndFile.m_MixPlugins[i].editorY, 1 << 30, cyScreen); + + mpt::IO::WriteIntLE<uint8>(f, 1); // Window type + mpt::IO::WriteIntLE<uint8>(f, 0); // Version + mpt::IO::WriteVarInt(f, i); + mpt::IO::WriteIntLE<int32>(f, editorX); + mpt::IO::WriteIntLE<int32>(f, editorY); + } + } + + SettingsContainer &settings = theApp.GetSongSettings(); + const std::string s = f.str(); + settings.Write(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode(), pathName); + settings.Write(U_("WindowSettings"), pathName.ToUnicode(), mpt::encode_hex(mpt::as_span(s))); +} + + +// Restore all view positions from settings file +void CModDoc::DeserializeViews() +{ + mpt::PathString pathName = GetPathNameMpt(); + if(pathName.empty()) return; + + SettingsContainer &settings = theApp.GetSongSettings(); + mpt::ustring s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode()); + if(s.size() < 2) + { + // Try relative path + pathName = pathName.RelativePathToAbsolute(theApp.GetInstallPath()); + s = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.ToUnicode()); + if(s.size() < 2) + { + // Try searching for filename instead of full path name + const mpt::ustring altName = settings.Read<mpt::ustring>(U_("WindowSettings"), pathName.GetFullFileName().ToUnicode()); + s = settings.Read<mpt::ustring>(U_("WindowSettings"), altName); + if(s.size() < 2) return; + } + } + std::vector<std::byte> bytes = mpt::decode_hex(s); + + FileReader file(mpt::as_span(bytes)); + + CRect mdiRect; + ::GetWindowRect(CMainFrame::GetMainFrame()->m_hWndMDIClient, &mdiRect); + const int width = mdiRect.Width(); + const int height = mdiRect.Height(); + + const int cxScreen = GetSystemMetrics(SM_CXVIRTUALSCREEN), cyScreen = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + POSITION pos = GetFirstViewPosition(); + CChildFrame *pChildFrm = nullptr; + if(pos != nullptr) pChildFrm = dynamic_cast<CChildFrame *>(GetNextView(pos)->GetParentFrame()); + + bool anyMaximized = false; + while(file.CanRead(1)) + { + const uint8 windowType = file.ReadUint8(); + if(windowType == 0) + { + // Document view positions and sizes + const uint8 windowState = file.ReadUint8(); + CRect rect; + rect.left = Util::muldivr(file.ReadInt32LE(), width, 1 << 30); + rect.top = Util::muldivr(file.ReadInt32LE(), height, 1 << 30); + rect.right = rect.left + Util::muldivr(file.ReadInt32LE(), width, 1 << 30); + rect.bottom = rect.top + Util::muldivr(file.ReadInt32LE(), height, 1 << 30); + size_t dataSize; + file.ReadVarInt(dataSize); + FileReader data = file.ReadChunk(dataSize); + + if(pChildFrm == nullptr) + { + CModDocTemplate *pTemplate = static_cast<CModDocTemplate *>(GetDocTemplate()); + ASSERT_VALID(pTemplate); + pChildFrm = static_cast<CChildFrame *>(pTemplate->CreateNewFrame(this, nullptr)); + if(pChildFrm != nullptr) + { + pTemplate->InitialUpdateFrame(pChildFrm, this); + } + } + if(pChildFrm != nullptr) + { + if(!mdiRect.IsRectEmpty()) + { + WINDOWPLACEMENT wnd; + wnd.length = sizeof(wnd); + pChildFrm->GetWindowPlacement(&wnd); + wnd.showCmd = SW_SHOWNOACTIVATE; + if(windowState == 1 || anyMaximized) + { + // Once a window has been maximized, all following windows have to be marked as maximized as well. + wnd.showCmd = SW_MAXIMIZE; + anyMaximized = true; + } else if(windowState == 2) + { + wnd.showCmd = SW_MINIMIZE; + } + if(rect.left < width && rect.right > 0 && rect.top < height && rect.bottom > 0) + { + wnd.rcNormalPosition = CRect(rect.left, rect.top, rect.right, rect.bottom); + } + pChildFrm->SetWindowPlacement(&wnd); + } + pChildFrm->DeserializeView(data); + pChildFrm = nullptr; + } + } else if(windowType == 1) + { + if(file.ReadUint8() != 0) + break; + // Plugin window positions + PLUGINDEX plug = 0; + if(file.ReadVarInt(plug) && plug < MAX_MIXPLUGINS) + { + int32 editorX = file.ReadInt32LE(); + int32 editorY = file.ReadInt32LE(); + if(editorX != int32_min && editorY != int32_min) + { + m_SndFile.m_MixPlugins[plug].editorX = Util::muldivr(editorX, cxScreen, 1 << 30); + m_SndFile.m_MixPlugins[plug].editorY = Util::muldivr(editorY, cyScreen, 1 << 30); + } + } + } else + { + // Unknown type + break; + } + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.h b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.h new file mode 100644 index 00000000..e3fefa96 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Moddoc.h @@ -0,0 +1,475 @@ +/* + * ModDoc.h + * -------- + * Purpose: Converting between various module formats. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Sndfile.h" +#include "../common/misc_util.h" +#include "Undo.h" +#include "Notification.h" +#include "UpdateHints.h" +#include <time.h> + +OPENMPT_NAMESPACE_BEGIN + +class EncoderFactoryBase; +class CChildFrame; + +///////////////////////////////////////////////////////////////////////// +// Split Keyboard Settings (pattern editor) + +struct SplitKeyboardSettings +{ + enum + { + splitOctaveRange = 9, + }; + + bool IsSplitActive() const { return (octaveLink && (octaveModifier != 0)) || (splitInstrument != 0) || (splitVolume != 0); } + + int octaveModifier = 0; // determines by how many octaves the notes should be transposed up or down + ModCommand::NOTE splitNote = NOTE_MIDDLEC - 1; + ModCommand::INSTR splitInstrument = 0; + ModCommand::VOL splitVolume = 0; + bool octaveLink = false; // apply octaveModifier +}; + +enum InputTargetContext : int8; + + +struct LogEntry +{ + LogLevel level; + mpt::ustring message; + LogEntry() : level(LogInformation) {} + LogEntry(LogLevel l, const mpt::ustring &m) : level(l), message(m) {} +}; + + +enum LogMode +{ + LogModeInstantReporting, + LogModeGather, +}; + + +class ScopedLogCapturer +{ +private: + CModDoc &m_modDoc; + LogMode m_oldLogMode; + CString m_title; + CWnd *m_pParent; + bool m_showLog; +public: + ScopedLogCapturer(CModDoc &modDoc, const CString &title = {}, CWnd *parent = nullptr, bool showLog = true); + ~ScopedLogCapturer(); + void ShowLog(bool force = false); + void ShowLog(const CString &preamble, bool force = false); + [[deprecated]] void ShowLog(const std::string &preamble, bool force = false); + void ShowLog(const mpt::ustring &preamble, bool force = false); +}; + + +struct PlayNoteParam +{ + std::bitset<128> *m_notesPlaying = nullptr; + SmpLength m_loopStart = 0, m_loopEnd = 0, m_sampleOffset = 0; + int32 m_volume = -1; + SAMPLEINDEX m_sample = 0; + INSTRUMENTINDEX m_instr = 0; + CHANNELINDEX m_currentChannel = CHANNELINDEX_INVALID; + ModCommand::NOTE m_note; + + PlayNoteParam(ModCommand::NOTE note) : m_note(note) { } + + PlayNoteParam& LoopStart(SmpLength loopStart) { m_loopStart = loopStart; return *this; } + PlayNoteParam& LoopEnd(SmpLength loopEnd) { m_loopEnd = loopEnd; return *this; } + PlayNoteParam& Offset(SmpLength sampleOffset) { m_sampleOffset = sampleOffset; return *this; } + + PlayNoteParam& Volume(int32 volume) { m_volume = volume; return *this; } + PlayNoteParam& Sample(SAMPLEINDEX sample) { m_sample = sample; return *this; } + PlayNoteParam& Instrument(INSTRUMENTINDEX instr) { m_instr = instr; return *this; } + PlayNoteParam& Channel(CHANNELINDEX channel) { m_currentChannel = channel; return *this; } + + PlayNoteParam& CheckNNA(std::bitset<128> ¬esPlaying) { m_notesPlaying = ¬esPlaying; return *this; } +}; + + +enum class RecordGroup : uint8 +{ + NoGroup = 0, + Group1 = 1, + Group2 = 2, +}; + + +class CModDoc final : public CDocument +{ +protected: + friend ScopedLogCapturer; + mutable std::vector<LogEntry> m_Log; + LogMode m_LogMode = LogModeInstantReporting; + CSoundFile m_SndFile; + + HWND m_hWndFollow = nullptr; + FlagSet<Notification::Type, uint16> m_notifyType; + Notification::Item m_notifyItem = 0; + CSize m_szOldPatternScrollbarsPos = { -10, -10 }; + + CPatternUndo m_PatternUndo; + CSampleUndo m_SampleUndo; + CInstrumentUndo m_InstrumentUndo; + SplitKeyboardSettings m_SplitKeyboardSettings; // this is maybe not the best place to keep them, but it should do the job + time_t m_creationTime; + + std::atomic<bool> m_modifiedAutosave = false; // Modified since last autosave? + +public: + class NoteToChannelMap : public std::array<CHANNELINDEX, NOTE_MAX - NOTE_MIN + 1> + { + public: + NoteToChannelMap() { fill(CHANNELINDEX_INVALID); } + }; + NoteToChannelMap m_noteChannel; // Note -> Preview channel assignment + + bool m_ShowSavedialog = false; + bool m_bHasValidPath = false; //becomes true if document is loaded or saved. + +protected: + // Note-off event buffer for MIDI sustain pedal + std::array<std::vector<uint32>, 16> m_midiSustainBuffer; + std::array<std::bitset<128>, 16> m_midiPlayingNotes; + std::bitset<16> m_midiSustainActive; + + std::bitset<MAX_BASECHANNELS> m_bsMultiRecordMask; + std::bitset<MAX_BASECHANNELS> m_bsMultiSplitRecordMask; + +protected: // create from serialization only + CModDoc(); + DECLARE_DYNCREATE(CModDoc) + +// public members +public: + CSoundFile &GetSoundFile() { return m_SndFile; } + const CSoundFile &GetSoundFile() const { return m_SndFile; } + +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Woverloaded-virtual" +#endif // MPT_COMPILER_CLANG + bool IsModified() const { return m_bModified != FALSE; } // Work-around: CDocument::IsModified() is not const... +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG + void SetModified(bool modified = true); + bool ModifiedSinceLastAutosave(); + void SetShowSaveDialog(bool b) { m_ShowSavedialog = b; } + void PostMessageToAllViews(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0); + void SendNotifyMessageToAllViews(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0); + void SendMessageToActiveView(UINT uMsg, WPARAM wParam = 0, LPARAM lParam = 0); + MODTYPE GetModType() const { return m_SndFile.m_nType; } + INSTRUMENTINDEX GetNumInstruments() const { return m_SndFile.m_nInstruments; } + SAMPLEINDEX GetNumSamples() const { return m_SndFile.m_nSamples; } + + // Logging for general progress and error events. + void AddToLog(LogLevel level, const mpt::ustring &text) const; + /*[[deprecated]]*/ void AddToLog(const CString &text) const { AddToLog(LogInformation, mpt::ToUnicode(text)); } + /*[[deprecated]]*/ void AddToLog(const std::string &text) const { AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::Locale, text)); } + /*[[deprecated]]*/ void AddToLog(const char *text) const { AddToLog(LogInformation, mpt::ToUnicode(mpt::Charset::Locale, text ? text : "")); } + + const std::vector<LogEntry> & GetLog() const { return m_Log; } + mpt::ustring GetLogString() const; + LogLevel GetMaxLogLevel() const; +protected: + LogMode GetLogMode() const { return m_LogMode; } + void SetLogMode(LogMode mode) { m_LogMode = mode; } + void ClearLog(); + UINT ShowLog(const CString &preamble, const CString &title = {}, CWnd *parent = nullptr); + UINT ShowLog(const CString &title = {}, CWnd *parent = nullptr) { return ShowLog(_T(""), title, parent); } + +public: + + void ClearFilePath() { m_strPathName.Empty(); } + + void ViewPattern(UINT nPat, UINT nOrd); + void ViewSample(UINT nSmp); + void ViewInstrument(UINT nIns); + HWND GetFollowWnd() const { return m_hWndFollow; } + void SetFollowWnd(HWND hwnd); + + void SetNotifications(FlagSet<Notification::Type> type, Notification::Item item = 0) { m_notifyType = type; m_notifyItem = item; } + FlagSet<Notification::Type, uint16> GetNotificationType() const { return m_notifyType; } + Notification::Item GetNotificationItem() const { return m_notifyItem; } + + void ActivateWindow(); + + void OnSongProperties(); + + void PrepareUndoForAllPatterns(bool storeChannelInfo = false, const char *description = ""); + CPatternUndo &GetPatternUndo() { return m_PatternUndo; } + CSampleUndo &GetSampleUndo() { return m_SampleUndo; } + CInstrumentUndo &GetInstrumentUndo() { return m_InstrumentUndo; } + SplitKeyboardSettings &GetSplitKeyboardSettings() { return m_SplitKeyboardSettings; } + + time_t GetCreationTime() const { return m_creationTime; } + +// operations +public: + bool ChangeModType(MODTYPE wType); + + bool ChangeNumChannels(CHANNELINDEX nNewChannels, const bool showCancelInRemoveDlg = true); + bool RemoveChannels(const std::vector<bool> &keepMask, bool verbose = false); + void CheckUsedChannels(std::vector<bool> &usedMask, CHANNELINDEX maxRemoveCount = MAX_BASECHANNELS) const; + + CHANNELINDEX ReArrangeChannels(const std::vector<CHANNELINDEX> &fromToArray, const bool createUndoPoint = true); + SAMPLEINDEX ReArrangeSamples(const std::vector<SAMPLEINDEX> &newOrder); + INSTRUMENTINDEX ReArrangeInstruments(const std::vector<INSTRUMENTINDEX> &newOrder, deleteInstrumentSamples removeSamples = doNoDeleteAssociatedSamples); + SEQUENCEINDEX ReArrangeSequences(const std::vector<SEQUENCEINDEX> &newOrder); + + bool ConvertInstrumentsToSamples(); + bool ConvertSamplesToInstruments(); + PLUGINDEX RemovePlugs(const std::vector<bool> &keepMask); + bool RemovePlugin(PLUGINDEX plugin); + + void ClonePlugin(SNDMIXPLUGIN &target, const SNDMIXPLUGIN &source); + void AppendModule(const CSoundFile &source); + + // Create a new pattern and, if order position is specified, inserts it into the order list. + PATTERNINDEX InsertPattern(ROWINDEX rows, ORDERINDEX ord = ORDERINDEX_INVALID); + SAMPLEINDEX InsertSample(); + INSTRUMENTINDEX InsertInstrument(SAMPLEINDEX sample = SAMPLEINDEX_INVALID, INSTRUMENTINDEX duplicateSource = INSTRUMENTINDEX_INVALID, bool silent = false); + INSTRUMENTINDEX InsertInstrumentForPlugin(PLUGINDEX plug); + INSTRUMENTINDEX HasInstrumentForPlugin(PLUGINDEX plug) const; + void InitializeInstrument(ModInstrument *pIns); + bool RemoveOrder(SEQUENCEINDEX nSeq, ORDERINDEX nOrd); + bool RemovePattern(PATTERNINDEX nPat); + bool RemoveSample(SAMPLEINDEX nSmp); + bool RemoveInstrument(INSTRUMENTINDEX nIns); + + void ProcessMIDI(uint32 midiData, INSTRUMENTINDEX ins, IMixPlugin *plugin, InputTargetContext ctx); + CHANNELINDEX PlayNote(PlayNoteParam ¶ms, NoteToChannelMap *noteChannel = nullptr); + bool NoteOff(UINT note, bool fade = false, INSTRUMENTINDEX ins = INSTRUMENTINDEX_INVALID, CHANNELINDEX currentChn = CHANNELINDEX_INVALID); + void CheckNNA(ModCommand::NOTE note, INSTRUMENTINDEX ins, std::bitset<128> &playingNotes); + void UpdateOPLInstrument(SAMPLEINDEX smp); + + bool IsNotePlaying(UINT note, SAMPLEINDEX nsmp = 0, INSTRUMENTINDEX nins = 0); + bool MuteChannel(CHANNELINDEX nChn, bool bMute); + bool UpdateChannelMuteStatus(CHANNELINDEX nChn); + bool MuteSample(SAMPLEINDEX nSample, bool bMute); + bool MuteInstrument(INSTRUMENTINDEX nInstr, bool bMute); + // Returns true if toggling the mute status of a channel should set the document as modified given the current module format and settings. + bool MuteToggleModifiesDocument() const; + + bool SoloChannel(CHANNELINDEX nChn, bool bSolo); + bool IsChannelSolo(CHANNELINDEX nChn) const; + + bool SurroundChannel(CHANNELINDEX nChn, bool bSurround); + bool SetChannelGlobalVolume(CHANNELINDEX nChn, uint16 nVolume); + bool SetChannelDefaultPan(CHANNELINDEX nChn, uint16 nPan); + bool IsChannelMuted(CHANNELINDEX nChn) const; + bool IsSampleMuted(SAMPLEINDEX nSample) const; + bool IsInstrumentMuted(INSTRUMENTINDEX nInstr) const; + + bool NoFxChannel(CHANNELINDEX nChn, bool bNoFx, bool updateMix = true); + bool IsChannelNoFx(CHANNELINDEX nChn) const; + + RecordGroup GetChannelRecordGroup(CHANNELINDEX channel) const; + void SetChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup); + void ToggleChannelRecordGroup(CHANNELINDEX channel, RecordGroup recordGroup); + void ReinitRecordState(bool unselect = true); + + CHANNELINDEX GetNumChannels() const { return m_SndFile.m_nChannels; } + UINT GetPatternSize(PATTERNINDEX nPat) const; + bool IsChildSample(INSTRUMENTINDEX nIns, SAMPLEINDEX nSmp) const; + INSTRUMENTINDEX FindSampleParent(SAMPLEINDEX sample) const; + SAMPLEINDEX FindInstrumentChild(INSTRUMENTINDEX nIns) const; + bool MoveOrder(ORDERINDEX nSourceNdx, ORDERINDEX nDestNdx, bool bUpdate = true, bool bCopy = false, SEQUENCEINDEX nSourceSeq = SEQUENCEINDEX_INVALID, SEQUENCEINDEX nDestSeq = SEQUENCEINDEX_INVALID); + BOOL ExpandPattern(PATTERNINDEX nPattern); + BOOL ShrinkPattern(PATTERNINDEX nPattern); + + bool SetDefaultChannelColors() { return SetDefaultChannelColors(0, GetNumChannels()); } + bool SetDefaultChannelColors(CHANNELINDEX channel) { return SetDefaultChannelColors(channel, channel + 1u); } + bool SetDefaultChannelColors(CHANNELINDEX minChannel, CHANNELINDEX maxChannel); + bool SupportsChannelColors() const { return GetModType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT); } + + bool CopyEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv); + bool SaveEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName); + bool PasteEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv); + bool LoadEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName); + + LRESULT ActivateView(UINT nIdView, DWORD dwParam); + // Notify all views of document updates (GUI thread only) + void UpdateAllViews(CView *pSender, UpdateHint hint, CObject *pHint=NULL); + // Notify all views of document updates (for non-GUI threads) + void UpdateAllViews(UpdateHint hint); + void GetEditPosition(ROWINDEX &row, PATTERNINDEX &pat, ORDERINDEX &ord); + LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + void TogglePluginEditor(UINT m_nCurrentPlugin, bool onlyThisEditor = false); + void RecordParamChange(PLUGINDEX slot, PlugParamIndex param); + void LearnMacro(int macro, PlugParamIndex param); + void SetElapsedTime(ORDERINDEX nOrd, ROWINDEX nRow, bool setSamplePos); + void SetLoopSong(bool loop); + + // Global settings to pattern effect conversion + bool GlobalVolumeToPattern(); + + bool HasMPTHacks(const bool autofix = false); + + void FixNullStrings(); + +// Fix: save pattern scrollbar position when switching to other tab + CSize GetOldPatternScrollbarsPos() const { return m_szOldPatternScrollbarsPos; }; + void SetOldPatternScrollbarsPos( CSize s ){ m_szOldPatternScrollbarsPos = s; }; + + void OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder); + void OnFileWaveConvert(ORDERINDEX nMinOrder, ORDERINDEX nMaxOrder, const std::vector<EncoderFactoryBase*> &encFactories); + + // Returns formatted ModInstrument name. + // [in] bEmptyInsteadOfNoName: In case of unnamed instrument string, "(no name)" is returned unless this + // parameter is true is case which an empty name is returned. + // [in] bIncludeIndex: True to include instrument index in front of the instrument name, false otherwise. + CString GetPatternViewInstrumentName(INSTRUMENTINDEX nInstr, bool bEmptyInsteadOfNoName = false, bool bIncludeIndex = true) const; + + // Check if a given channel contains data. + bool IsChannelUnused(CHANNELINDEX nChn) const; + // Check whether a sample is used. + // In sample mode, the sample numbers in all patterns are checked. + // In instrument mode, it is only checked if a sample is referenced by an instrument (but not if the sample is actually played anywhere) + bool IsSampleUsed(SAMPLEINDEX sample, bool searchInMutedChannels = true) const; + // Check whether an instrument is used (only for instrument mode). + bool IsInstrumentUsed(INSTRUMENTINDEX instr, bool searchInMutedChannels = true) const; + +// protected members +protected: + + void InitializeMod(); + + CChildFrame *GetChildFrame(); //rewbs.customKeys + +// Overrides + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(CModDoc) + public: + BOOL OnNewDocument() override; + BOOL OnOpenDocument(LPCTSTR lpszPathName) override; + BOOL OnSaveDocument(LPCTSTR lpszPathName) override + { + return OnSaveDocument(lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString()) ? TRUE : FALSE; + } + void OnCloseDocument() override; + void SafeFileClose(); + bool OnSaveDocument(const mpt::PathString &filename, const bool setPath = true); + +#if MPT_COMPILER_CLANG +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Woverloaded-virtual" +#endif // MPT_COMPILER_CLANG + void SetPathName(const mpt::PathString &filename, BOOL bAddToMRU = TRUE) + { + CDocument::SetPathName(filename.ToCString(), bAddToMRU); + } +#if MPT_COMPILER_CLANG +#pragma clang diagnostic pop +#endif // MPT_COMPILER_CLANG + mpt::PathString GetPathNameMpt() const + { + return mpt::PathString::FromCString(GetPathName()); + } + + BOOL SaveModified() override; + bool SaveAllSamples(bool showPrompt = true); + bool SaveSample(SAMPLEINDEX smp); + + BOOL DoSave(LPCTSTR lpszPathName, BOOL /*bSaveAs*/ = TRUE) override + { + return DoSave(lpszPathName ? mpt::PathString::FromCString(lpszPathName) : mpt::PathString()); + } + BOOL DoSave(const mpt::PathString &filename, bool setPath = true); + void DeleteContents() override; + //}}AFX_VIRTUAL + + // Get the sample index for the current pattern cell (resolves instrument note maps, etc) + SAMPLEINDEX GetSampleIndex(const ModCommand &m, ModCommand::INSTR lastInstr = 0) const; + // Get group (octave) size from given instrument (or sample in sample mode) + int GetInstrumentGroupSize(INSTRUMENTINDEX instr) const; + int GetBaseNote(INSTRUMENTINDEX instr) const; + ModCommand::NOTE GetNoteWithBaseOctave(int noteOffset, INSTRUMENTINDEX instr) const; + + // Convert a linear volume property to decibels + static CString LinearToDecibels(double value, double valueAtZeroDB); + // Convert a panning value to a more readable string + static CString PanningToString(int32 value, int32 valueAtCenter); + + void SerializeViews() const; + void DeserializeViews(); + + // View MIDI Mapping dialog for given plugin and parameter combination. + void ViewMIDIMapping(PLUGINDEX plugin = PLUGINDEX_INVALID, PlugParamIndex param = 0); + +// Implementation +public: + virtual ~CModDoc(); + +// Generated message map functions +public: + //{{AFX_MSG(CModDoc) + afx_msg void OnFileWaveConvert(); + afx_msg void OnFileMidiConvert(); + afx_msg void OnFileOPLExport(); + afx_msg void OnFileCompatibilitySave(); + afx_msg void OnPlayerPlay(); + afx_msg void OnPlayerStop(); + afx_msg void OnPlayerPause(); + afx_msg void OnPlayerPlayFromStart(); + afx_msg void OnPanic(); + afx_msg void OnEditGlobals(); + afx_msg void OnEditPatterns(); + afx_msg void OnEditSamples(); + afx_msg void OnEditInstruments(); + afx_msg void OnEditComments(); + afx_msg void OnShowCleanup(); + afx_msg void OnShowSampleTrimmer(); + afx_msg void OnSetupZxxMacros(); + afx_msg void OnEstimateSongLength(); + afx_msg void OnApproximateBPM(); + afx_msg void OnUpdateXMITMPTOnly(CCmdUI *p); + afx_msg void OnUpdateHasEditHistory(CCmdUI *p); + afx_msg void OnUpdateHasMIDIMappings(CCmdUI *p); + afx_msg void OnUpdateCompatExportableOnly(CCmdUI *p); + afx_msg void OnPatternRestart() { OnPatternRestart(true); } //rewbs.customKeys + afx_msg void OnPatternRestart(bool loop); //rewbs.customKeys + afx_msg void OnPatternPlay(); //rewbs.customKeys + afx_msg void OnPatternPlayNoLoop(); //rewbs.customKeys + afx_msg void OnViewEditHistory(); + afx_msg void OnViewMPTHacks(); + afx_msg void OnViewTempoSwingSettings(); + afx_msg void OnSaveCopy(); + afx_msg void OnSaveTemplateModule(); + afx_msg void OnAppendModule(); + afx_msg void OnViewMIDIMapping() { ViewMIDIMapping(); } + afx_msg void OnChannelManager(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +private: + + void ChangeFileExtension(MODTYPE nNewType); + CHANNELINDEX FindAvailableChannel() const; +}; + +///////////////////////////////////////////////////////////////////////////// + +//{{AFX_INSERT_LOCATION}} +// Microsoft Developer Studio will insert additional declarations immediately before the previous line. + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Modedit.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Modedit.cpp new file mode 100644 index 00000000..97154ed3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Modedit.cpp @@ -0,0 +1,1383 @@ +/* + * ModEdit.cpp + * ----------- + * Purpose: Song (pattern, samples, instruments) editing functions + * 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 "Moddoc.h" +#include "Clipboard.h" +#include "dlg_misc.h" +#include "Dlsbank.h" +#include "../soundlib/modsmp_ctrl.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/tuning.h" +#include "../soundlib/OPL.h" +#include "../common/misc_util.h" +#include "../common/mptStringBuffer.h" +#include "../common/mptFileIO.h" +#include <sstream> +// Plugin cloning +#include "../soundlib/plugins/PluginManager.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "VstPresets.h" +#include "../common/FileReader.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// Change the number of channels. +// Return true on success. +bool CModDoc::ChangeNumChannels(CHANNELINDEX nNewChannels, const bool showCancelInRemoveDlg) +{ + const CHANNELINDEX maxChans = m_SndFile.GetModSpecifications().channelsMax; + + if(nNewChannels > maxChans) + { + CString error; + error.Format(_T("Error: Max number of channels for this file type is %u"), maxChans); + Reporting::Warning(error); + return false; + } + + if(nNewChannels == GetNumChannels()) + return false; + + if(nNewChannels < GetNumChannels()) + { + // Remove channels + CHANNELINDEX chnsToRemove = 0, maxRemoveCount = 0; + + //nNewChannels = 0 means user can choose how many channels to remove + if(nNewChannels > 0) + { + chnsToRemove = GetNumChannels() - nNewChannels; + maxRemoveCount = chnsToRemove; + } else + { + chnsToRemove = 0; + maxRemoveCount = GetNumChannels(); + } + + CRemoveChannelsDlg rem(m_SndFile, chnsToRemove, showCancelInRemoveDlg); + CheckUsedChannels(rem.m_bKeepMask, maxRemoveCount); + if(rem.DoModal() != IDOK) + return false; + + // Removing selected channels + return RemoveChannels(rem.m_bKeepMask, true); + } else + { + // Increasing number of channels + BeginWaitCursor(); + std::vector<CHANNELINDEX> channels(nNewChannels, CHANNELINDEX_INVALID); + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + channels[chn] = chn; + } + + bool success = (ReArrangeChannels(channels) == nNewChannels); + if(success) + { + SetModified(); + UpdateAllViews(nullptr, UpdateHint().ModType()); + } + EndWaitCursor(); + return success; + } +} + + +// To remove all channels whose index corresponds to false in the keepMask vector. +// Return true on success. +bool CModDoc::RemoveChannels(const std::vector<bool> &keepMask, bool verbose) +{ + CHANNELINDEX nRemainingChannels = 0; + //First calculating how many channels are to be left + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + if(keepMask[chn]) + nRemainingChannels++; + } + if(nRemainingChannels == GetNumChannels() || nRemainingChannels < m_SndFile.GetModSpecifications().channelsMin) + { + if(verbose) + { + CString str; + if(nRemainingChannels == GetNumChannels()) + str = _T("No channels chosen to be removed."); + else + str = _T("No removal done - channel number is already at minimum."); + Reporting::Information(str, _T("Remove Channels")); + } + return false; + } + + BeginWaitCursor(); + // Create new channel order, with only channels from m_bChnMask left. + std::vector<CHANNELINDEX> channels(nRemainingChannels, 0); + CHANNELINDEX i = 0; + for(CHANNELINDEX chn = 0; chn < GetNumChannels(); chn++) + { + if(keepMask[chn]) + { + channels[i++] = chn; + } + } + const bool success = (ReArrangeChannels(channels) == nRemainingChannels); + if(success) + { + SetModified(); + UpdateAllViews(nullptr, UpdateHint().ModType()); + } + EndWaitCursor(); + return success; +} + + +// Base code for adding, removing, moving and duplicating channels. Returns new number of channels on success, CHANNELINDEX_INVALID otherwise. +// The new channel vector can contain CHANNELINDEX_INVALID for adding new (empty) channels. +CHANNELINDEX CModDoc::ReArrangeChannels(const std::vector<CHANNELINDEX> &newOrder, const bool createUndoPoint) +{ + //newOrder[i] tells which current channel should be placed to i:th position in + //the new order, or if i is not an index of current channels, then new channel is + //added to position i. If index of some current channel is missing from the + //newOrder-vector, then the channel gets removed. + + const CHANNELINDEX newNumChannels = static_cast<CHANNELINDEX>(newOrder.size()), oldNumChannels = GetNumChannels(); + auto &specs = m_SndFile.GetModSpecifications(); + + if(newNumChannels > specs.channelsMax || newNumChannels < specs.channelsMin) + { + CString str; + str.Format(_T("Can't apply change: Number of channels should be between %u and %u."), specs.channelsMin, specs.channelsMax); + Reporting::Error(str, _T("Rearrange Channels")); + return CHANNELINDEX_INVALID; + } + + if(createUndoPoint) + { + PrepareUndoForAllPatterns(true, "Rearrange Channels"); + } + + CriticalSection cs; + if(oldNumChannels == newNumChannels) + { + // Optimization with no pattern re-allocation + std::vector<ModCommand> oldRow(oldNumChannels); + for(auto &pat : m_SndFile.Patterns) + { + auto m = pat.begin(); + for(ROWINDEX row = 0; row < pat.GetNumRows(); row++) + { + oldRow.assign(m, m + oldNumChannels); + for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++, m++) + { + if(newOrder[chn] < oldNumChannels) // Case: getting old channel to the new channel order. + *m = oldRow[newOrder[chn]]; + else + *m = ModCommand::Empty(); + } + } + } + } else + { + // Create all patterns first so that we can exit cleanly in case of OOM + std::vector<std::vector<ModCommand>> newPatterns; + try + { + newPatterns.resize(m_SndFile.Patterns.Size()); + for(PATTERNINDEX i = 0; i < m_SndFile.Patterns.Size(); i++) + { + newPatterns[i].resize(m_SndFile.Patterns[i].GetNumRows() * newNumChannels); + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + Reporting::Error("Out of memory!", "Rearrange Channels"); + return oldNumChannels; + } + + m_SndFile.m_nChannels = newNumChannels; + for(PATTERNINDEX i = 0; i < m_SndFile.Patterns.Size(); i++) + { + CPattern &pat = m_SndFile.Patterns[i]; + if(pat.IsValid()) + { + auto mNew = newPatterns[i].begin(), mOld = pat.begin(); + for(ROWINDEX row = 0; row < pat.GetNumRows(); row++, mOld += oldNumChannels) + { + for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++, mNew++) + { + if(newOrder[chn] < oldNumChannels) // Case: getting old channel to the new channel order. + *mNew = mOld[newOrder[chn]]; + } + } + pat.SetData(std::move(newPatterns[i])); + } + } + } + + // Reverse mapping: One of the new indices of an old channel + std::vector<CHANNELINDEX> newIndex(oldNumChannels, CHANNELINDEX_INVALID); + for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++) + { + if(newOrder[chn] < oldNumChannels) + { + newIndex[newOrder[chn]] = chn; + } + } + + // Reassign NNA channels (note: if we increase the number of channels, the lowest-indexed NNA channels will still be lost) + const auto muteFlag = CSoundFile::GetChannelMuteFlag(); + for(CHANNELINDEX chn = oldNumChannels; chn < MAX_CHANNELS; chn++) + { + auto &channel = m_SndFile.m_PlayState.Chn[chn]; + CHANNELINDEX masterChn = channel.nMasterChn, newMasterChn; + if(masterChn > 0 && masterChn <= newIndex.size() && (newMasterChn = newIndex[masterChn - 1]) != CHANNELINDEX_INVALID) + channel.nMasterChn = newMasterChn + 1; + else + channel.Reset(ModChannel::resetTotal, m_SndFile, chn, muteFlag); + } + + std::vector<ModChannel> chns(std::begin(m_SndFile.m_PlayState.Chn), std::begin(m_SndFile.m_PlayState.Chn) + oldNumChannels); + std::vector<ModChannelSettings> settings(std::begin(m_SndFile.ChnSettings), std::begin(m_SndFile.ChnSettings) + oldNumChannels); + std::vector<RecordGroup> recordStates(oldNumChannels); + auto chnMutePendings = m_SndFile.m_bChannelMuteTogglePending; + for(CHANNELINDEX chn = 0; chn < oldNumChannels; chn++) + { + recordStates[chn] = GetChannelRecordGroup(chn); + } + ReinitRecordState(); + + for(CHANNELINDEX chn = 0; chn < newNumChannels; chn++) + { + CHANNELINDEX srcChn = newOrder[chn]; + if(srcChn < oldNumChannels) + { + m_SndFile.ChnSettings[chn] = settings[srcChn]; + m_SndFile.m_PlayState.Chn[chn] = chns[srcChn]; + SetChannelRecordGroup(chn, recordStates[srcChn]); + m_SndFile.m_bChannelMuteTogglePending[chn] = chnMutePendings[srcChn]; + if(m_SndFile.m_opl) + m_SndFile.m_opl->MoveChannel(srcChn, chn); + } else + { + m_SndFile.InitChannel(chn); + SetDefaultChannelColors(chn); + } + } + // Reset MOD panning (won't affect other module formats) + m_SndFile.SetupMODPanning(); + m_SndFile.InitAmigaResampler(); + return newNumChannels; +} + + +// Base code for adding, removing, moving and duplicating samples. Returns new number of samples on success, SAMPLEINDEX_INVALID otherwise. +// The new sample vector can contain SAMPLEINDEX_INVALID for adding new (empty) samples. +// newOrder indices are zero-based, i.e. newOrder[0] will define the contents of the first sample slot. +SAMPLEINDEX CModDoc::ReArrangeSamples(const std::vector<SAMPLEINDEX> &newOrder) +{ + if(newOrder.size() > m_SndFile.GetModSpecifications().samplesMax) + { + return SAMPLEINDEX_INVALID; + } + + CriticalSection cs; + + const SAMPLEINDEX oldNumSamples = m_SndFile.GetNumSamples(), newNumSamples = static_cast<SAMPLEINDEX>(newOrder.size()); + + std::vector<int> sampleCount(oldNumSamples + 1, 0); + std::vector<ModSample> sampleHeaders(oldNumSamples + 1); + std::vector<SAMPLEINDEX> newIndex(oldNumSamples + 1, 0); // One of the new indexes for the old sample + std::vector<std::string> sampleNames(oldNumSamples + 1); + std::vector<mpt::PathString> samplePaths(oldNumSamples + 1); + + for(SAMPLEINDEX i = 0; i < newNumSamples; i++) + { + const SAMPLEINDEX origSlot = newOrder[i]; + if(origSlot > 0 && origSlot <= oldNumSamples) + { + sampleCount[origSlot]++; + sampleHeaders[origSlot] = m_SndFile.GetSample(origSlot); + if(!newIndex[origSlot]) + newIndex[origSlot] = i + 1; + } + } + + // First, delete all samples that will be removed anyway. + for(SAMPLEINDEX i = 1; i < sampleCount.size(); i++) + { + if(sampleCount[i] == 0) + { + m_SndFile.DestroySample(i); + GetSampleUndo().ClearUndo(i); + } + sampleNames[i] = m_SndFile.m_szNames[i]; + samplePaths[i] = m_SndFile.GetSamplePath(i); + } + + // Remove sample data references from now unused slots. + for(SAMPLEINDEX i = newNumSamples + 1; i <= oldNumSamples; i++) + { + m_SndFile.GetSample(i).pData.pSample = nullptr; + m_SndFile.GetSample(i).nLength = 0; + m_SndFile.m_szNames[i] = ""; + } + + // Now, create new sample list. + m_SndFile.m_nSamples = std::max(m_SndFile.m_nSamples, newNumSamples); // Avoid assertions when using GetSample()... + for(SAMPLEINDEX i = 0; i < newNumSamples; i++) + { + const SAMPLEINDEX origSlot = newOrder[i]; + ModSample &target = m_SndFile.GetSample(i + 1); + if(origSlot > 0 && origSlot <= oldNumSamples) + { + // Copy an original sample. + target = sampleHeaders[origSlot]; + if(--sampleCount[origSlot] > 0 && sampleHeaders[origSlot].HasSampleData()) + { + // This sample slot is referenced multiple times, so we have to copy the actual sample. + if(target.CopyWaveform(sampleHeaders[origSlot])) + { + target.PrecomputeLoops(m_SndFile, false); + } else + { + Reporting::Error("Cannot duplicate sample - out of memory!"); + } + } + m_SndFile.m_szNames[i + 1] = sampleNames[origSlot]; + m_SndFile.SetSamplePath(i + 1, samplePaths[origSlot]); + } else + { + // Invalid sample reference. + target.pData.pSample = nullptr; + target.Initialize(m_SndFile.GetType()); + m_SndFile.m_szNames[i + 1] = ""; + m_SndFile.ResetSamplePath(i + 1); + } + } + + GetSampleUndo().RearrangeSamples(newIndex); + + const auto muteFlag = CSoundFile::GetChannelMuteFlag(); + for(CHANNELINDEX c = 0; c < std::size(m_SndFile.m_PlayState.Chn); c++) + { + ModChannel &chn = m_SndFile.m_PlayState.Chn[c]; + for(SAMPLEINDEX i = 1; i <= oldNumSamples; i++) + { + if(chn.pModSample == &m_SndFile.GetSample(i)) + { + chn.pModSample = &m_SndFile.GetSample(newIndex[i]); + if(i == 0 || i > newNumSamples) + { + chn.Reset(ModChannel::resetTotal, m_SndFile, c, muteFlag); + } + break; + } + } + } + + m_SndFile.m_nSamples = newNumSamples; + + if(m_SndFile.GetNumInstruments()) + { + // Instrument mode: Update sample maps. + for(INSTRUMENTINDEX i = 0; i <= m_SndFile.GetNumInstruments(); i++) + { + ModInstrument *ins = m_SndFile.Instruments[i]; + if(ins == nullptr) + { + continue; + } + GetInstrumentUndo().RearrangeSamples(i, newIndex); + for(auto &sample : ins->Keyboard) + { + if(sample < newIndex.size()) + sample = newIndex[sample]; + else + sample = 0; + } + } + } else + { + PrepareUndoForAllPatterns(false, "Rearrange Samples"); + m_SndFile.Patterns.ForEachModCommand([&newIndex] (ModCommand &m) + { + if(!m.IsPcNote() && m.instr < newIndex.size()) + { + m.instr = static_cast<ModCommand::INSTR>(newIndex[m.instr]); + } + }); + } + + return GetNumSamples(); +} + + +// Base code for adding, removing, moving and duplicating instruments. Returns new number of instruments on success, INSTRUMENTINDEX_INVALID otherwise. +// The new instrument vector can contain INSTRUMENTINDEX_INVALID for adding new (empty) instruments. +// newOrder indices are zero-based, i.e. newOrder[0] will define the contents of the first instrument slot. +INSTRUMENTINDEX CModDoc::ReArrangeInstruments(const std::vector<INSTRUMENTINDEX> &newOrder, deleteInstrumentSamples removeSamples) +{ + if(newOrder.size() > m_SndFile.GetModSpecifications().instrumentsMax || GetNumInstruments() == 0) + { + return INSTRUMENTINDEX_INVALID; + } + + CriticalSection cs; + + const INSTRUMENTINDEX oldNumInstruments = m_SndFile.GetNumInstruments(), newNumInstruments = static_cast<INSTRUMENTINDEX>(newOrder.size()); + + std::vector<ModInstrument> instrumentHeaders(oldNumInstruments + 1); + std::vector<INSTRUMENTINDEX> newIndex(oldNumInstruments + 1, 0); // One of the new indexes for the old instrument + for(INSTRUMENTINDEX i = 0; i < newNumInstruments; i++) + { + const INSTRUMENTINDEX origSlot = newOrder[i]; + if(origSlot > 0 && origSlot <= oldNumInstruments) + { + if(m_SndFile.Instruments[origSlot] != nullptr) + instrumentHeaders[origSlot] = *m_SndFile.Instruments[origSlot]; + newIndex[origSlot] = i + 1; + } + } + + // Delete unused instruments first. + for(INSTRUMENTINDEX i = 1; i <= oldNumInstruments; i++) + { + if(newIndex[i] == 0) + { + m_SndFile.DestroyInstrument(i, removeSamples); + } + } + + m_SndFile.m_nInstruments = newNumInstruments; + + // Now, create new instrument list. + for(INSTRUMENTINDEX i = 0; i < newNumInstruments; i++) + { + ModInstrument *ins = m_SndFile.AllocateInstrument(i + 1); + if(ins == nullptr) + { + continue; + } + + const INSTRUMENTINDEX origSlot = newOrder[i]; + if(origSlot > 0 && origSlot <= oldNumInstruments) + { + // Copy an original instrument. + *ins = instrumentHeaders[origSlot]; + } + } + + // Free unused instruments + for(INSTRUMENTINDEX i = newNumInstruments + 1; i <= oldNumInstruments; i++) + { + m_SndFile.DestroyInstrument(i, doNoDeleteAssociatedSamples); + } + + PrepareUndoForAllPatterns(false, "Rearrange Instruments"); + GetInstrumentUndo().RearrangeInstruments(newIndex); + m_SndFile.Patterns.ForEachModCommand([&newIndex] (ModCommand &m) + { + if(!m.IsPcNote() && m.instr < newIndex.size()) + { + m.instr = static_cast<ModCommand::INSTR>(newIndex[m.instr]); + } + }); + + return GetNumInstruments(); +} + + +SEQUENCEINDEX CModDoc::ReArrangeSequences(const std::vector<SEQUENCEINDEX> &newOrder) +{ + CriticalSection cs; + return m_SndFile.Order.Rearrange(newOrder) ? m_SndFile.Order.GetNumSequences() : SEQUENCEINDEX_INVALID; +} + + +bool CModDoc::ConvertInstrumentsToSamples() +{ + if(!m_SndFile.GetNumInstruments()) + return false; + GetInstrumentUndo().ClearUndo(); + m_SndFile.Patterns.ForEachModCommand([&] (ModCommand &m) + { + if(m.instr && !m.IsPcNote()) + { + ModCommand::INSTR instr = m.instr, newinstr = 0; + ModCommand::NOTE note = m.note, newnote = note; + if(ModCommand::IsNote(note)) + note = note - NOTE_MIN; + else + note = NOTE_MIDDLEC - NOTE_MIN; + + if((instr < MAX_INSTRUMENTS) && (m_SndFile.Instruments[instr])) + { + const ModInstrument *pIns = m_SndFile.Instruments[instr]; + newinstr = static_cast<ModCommand::INSTR>(pIns->Keyboard[note]); + newnote = pIns->NoteMap[note]; + if(pIns->Keyboard[note] > Util::MaxValueOfType(m.instr)) + newinstr = 0; + } + m.instr = newinstr; + if(m.IsNote()) + { + m.note = newnote; + } + } + }); + return true; +} + + +bool CModDoc::ConvertSamplesToInstruments() +{ + const INSTRUMENTINDEX instrumentMax = m_SndFile.GetModSpecifications().instrumentsMax; + if(GetNumInstruments() > 0 || instrumentMax == 0) + return false; + + // If there is no actual sample data, don't bother creating any instruments + bool anySamples = false; + for(SAMPLEINDEX smp = 1; smp <= m_SndFile.m_nSamples; smp++) + { + if(m_SndFile.GetSample(smp).HasSampleData()) + { + anySamples = true; + break; + } + } + if(!anySamples) + return true; + + m_SndFile.m_nInstruments = std::min(m_SndFile.GetNumSamples(), instrumentMax); + for(SAMPLEINDEX smp = 1; smp <= m_SndFile.m_nInstruments; smp++) + { + const bool muted = IsSampleMuted(smp); + MuteSample(smp, false); + + ModInstrument *instrument = m_SndFile.AllocateInstrument(smp, smp); + if(instrument == nullptr) + { + ErrorBox(IDS_ERR_OUTOFMEMORY, CMainFrame::GetMainFrame()); + return false; + } + + InitializeInstrument(instrument); + instrument->name = m_SndFile.m_szNames[smp]; + MuteInstrument(smp, muted); + } + + return true; +} + + +PLUGINDEX CModDoc::RemovePlugs(const std::vector<bool> &keepMask) +{ + // Remove all plugins whose keepMask[plugindex] is false. + PLUGINDEX nRemoved = 0; + const PLUGINDEX maxPlug = std::min(MAX_MIXPLUGINS, static_cast<PLUGINDEX>(keepMask.size())); + + CriticalSection cs; + for(PLUGINDEX nPlug = 0; nPlug < maxPlug; nPlug++) + { + SNDMIXPLUGIN &plug = m_SndFile.m_MixPlugins[nPlug]; + if(keepMask[nPlug]) + { + continue; + } + + if(plug.pMixPlugin || plug.IsValidPlugin()) + { + nRemoved++; + } + + plug.Destroy(); + plug = {}; + + for(PLUGINDEX srcPlugSlot = 0; srcPlugSlot < nPlug; srcPlugSlot++) + { + SNDMIXPLUGIN &srcPlug = GetSoundFile().m_MixPlugins[srcPlugSlot]; + if(srcPlug.GetOutputPlugin() == nPlug) + { + srcPlug.SetOutputToMaster(); + UpdateAllViews(nullptr, PluginHint(static_cast<PLUGINDEX>(srcPlugSlot + 1)).Info()); + } + } + UpdateAllViews(nullptr, PluginHint(static_cast<PLUGINDEX>(nPlug + 1)).Info().Names()); + } + + if(nRemoved && m_SndFile.GetModSpecifications().supportsPlugins) + SetModified(); + + return nRemoved; +} + + +bool CModDoc::RemovePlugin(PLUGINDEX plugin) +{ + if(plugin >= MAX_MIXPLUGINS) + return false; + std::vector<bool> keepMask(MAX_MIXPLUGINS, true); + keepMask[plugin] = false; + return RemovePlugs(keepMask) == 1; +} + + +// Clone a plugin slot (source does not necessarily have to be from the current module) +void CModDoc::ClonePlugin(SNDMIXPLUGIN &target, const SNDMIXPLUGIN &source) +{ + IMixPlugin *srcVstPlug = source.pMixPlugin; + target.Destroy(); + target = source; + // Don't want this plugin to be accidentally erased again... + target.pMixPlugin = nullptr; + if(target.editorX != int32_min) + { + // Move target editor a bit to visually distinguish it from the original editor + int addPixels = Util::ScalePixels(16, CMainFrame::GetMainFrame()->m_hWnd); + target.editorX += addPixels; + target.editorY += addPixels; + } +#ifndef NO_PLUGINS + if(theApp.GetPluginManager()->CreateMixPlugin(target, GetSoundFile())) + { + IMixPlugin *newVstPlug = target.pMixPlugin; + newVstPlug->SetCurrentProgram(srcVstPlug->GetCurrentProgram()); + + std::ostringstream f(std::ios::out | std::ios::binary); + if(VSTPresets::SaveFile(f, *srcVstPlug, false)) + { + const std::string data = f.str(); + FileReader file(mpt::as_span(data)); + VSTPresets::LoadFile(file, *newVstPlug); + } + } +#endif // !NO_PLUGINS +} + + +PATTERNINDEX CModDoc::InsertPattern(ROWINDEX rows, ORDERINDEX ord) +{ + if(ord != ORDERINDEX_INVALID && m_SndFile.Order().GetLengthTailTrimmed() >= m_SndFile.GetModSpecifications().ordersMax) + return PATTERNINDEX_INVALID; + + PATTERNINDEX pat = m_SndFile.Patterns.InsertAny(rows, true); + if(pat != PATTERNINDEX_INVALID) + { + if(ord != ORDERINDEX_INVALID) + { + m_SndFile.Order().insert(ord, 1, pat); + } + SetModified(); + } + return pat; +} + + +SAMPLEINDEX CModDoc::InsertSample() +{ + SAMPLEINDEX i = m_SndFile.GetNextFreeSample(); + + if((i > std::numeric_limits<ModCommand::INSTR>::max() && !m_SndFile.GetNumInstruments()) || i == SAMPLEINDEX_INVALID) + { + ErrorBox(IDS_ERR_TOOMANYSMP, CMainFrame::GetMainFrame()); + return SAMPLEINDEX_INVALID; + } + const bool newSlot = (i > m_SndFile.GetNumSamples()); + if(newSlot || !m_SndFile.m_szNames[i][0]) + m_SndFile.m_szNames[i] = "untitled"; + if(newSlot) + m_SndFile.m_nSamples = i; + m_SndFile.GetSample(i).Initialize(m_SndFile.GetType()); + + m_SndFile.ResetSamplePath(i); + + SetModified(); + return i; +} + + +// Insert a new instrument assigned to sample nSample or duplicate instrument "duplicateSource". +// If "sample" is invalid, an appropriate sample slot is selected. 0 means "no sample". +INSTRUMENTINDEX CModDoc::InsertInstrument(SAMPLEINDEX sample, INSTRUMENTINDEX duplicateSource, bool silent) +{ + if(m_SndFile.GetModSpecifications().instrumentsMax == 0) + return INSTRUMENTINDEX_INVALID; + + ModInstrument *pDup = nullptr; + if(duplicateSource > 0 && duplicateSource <= m_SndFile.m_nInstruments) + { + pDup = m_SndFile.Instruments[duplicateSource]; + } + + if(!m_SndFile.GetNumInstruments() && (m_SndFile.GetNumSamples() > 1 || m_SndFile.GetSample(1).HasSampleData())) + { + bool doConvert = true; + if(!silent) + { + ConfirmAnswer result = Reporting::Confirm("Convert existing samples to instruments first?", true); + if(result == cnfCancel) + { + return INSTRUMENTINDEX_INVALID; + } + doConvert = (result == cnfYes); + } + if(doConvert) + { + if(!ConvertSamplesToInstruments()) + { + return INSTRUMENTINDEX_INVALID; + } + } + } + + const INSTRUMENTINDEX newins = m_SndFile.GetNextFreeInstrument(); + if(newins == INSTRUMENTINDEX_INVALID) + { + if(!silent) + { + ErrorBox(IDS_ERR_TOOMANYINS, CMainFrame::GetMainFrame()); + } + return INSTRUMENTINDEX_INVALID; + } else if(newins > m_SndFile.GetNumInstruments()) + { + m_SndFile.m_nInstruments = newins; + } + + // Determine which sample slot to use + SAMPLEINDEX newsmp = 0; + if(sample < m_SndFile.GetModSpecifications().samplesMax) + { + // Use specified slot + newsmp = sample; + } else if(!pDup) + { + newsmp = m_SndFile.GetNextFreeSample(newins); + if(newsmp > m_SndFile.GetNumSamples()) + { + // Add a new sample + const SAMPLEINDEX inssmp = InsertSample(); + if(inssmp != SAMPLEINDEX_INVALID) + newsmp = inssmp; + } + } + + CriticalSection cs; + + ModInstrument *pIns = m_SndFile.AllocateInstrument(newins, newsmp); + if(pIns == nullptr) + { + if(!silent) + { + cs.Leave(); + ErrorBox(IDS_ERR_OUTOFMEMORY, CMainFrame::GetMainFrame()); + } + return INSTRUMENTINDEX_INVALID; + } + InitializeInstrument(pIns); + + if(pDup) + { + *pIns = *pDup; + } + + SetModified(); + + return newins; +} + + +// Load default instrument values for inserting new instrument during editing +void CModDoc::InitializeInstrument(ModInstrument *pIns) +{ + pIns->pluginVolumeHandling = TrackerSettings::Instance().DefaultPlugVolumeHandling; +} + + +// Try to set up a new instrument that is linked to a given plugin +INSTRUMENTINDEX CModDoc::InsertInstrumentForPlugin(PLUGINDEX plug) +{ +#ifndef NO_PLUGINS + const bool first = (GetNumInstruments() == 0); + INSTRUMENTINDEX instr = InsertInstrument(0, INSTRUMENTINDEX_INVALID, true); + if(instr == INSTRUMENTINDEX_INVALID) + return INSTRUMENTINDEX_INVALID; + + ModInstrument &ins = *m_SndFile.Instruments[instr]; + ins.name = mpt::ToCharset(m_SndFile.GetCharsetInternal(), MPT_UFORMAT("{}: {}")(plug + 1, m_SndFile.m_MixPlugins[plug].GetName())); + ins.filename = mpt::ToCharset(m_SndFile.GetCharsetInternal(), m_SndFile.m_MixPlugins[plug].GetLibraryName()); + ins.nMixPlug = plug + 1; + ins.nMidiChannel = 1; + + InstrumentHint hint = InstrumentHint(instr).Info().Envelope().Names(); + if(first) + hint.ModType(); + UpdateAllViews(nullptr, hint); + if(m_SndFile.GetModSpecifications().supportsPlugins) + { + SetModified(); + } + + return instr; +#else + return INSTRUMENTINDEX_INVALID; +#endif +} + + +INSTRUMENTINDEX CModDoc::HasInstrumentForPlugin(PLUGINDEX plug) const +{ + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(m_SndFile.Instruments[i] != nullptr && m_SndFile.Instruments[i]->nMixPlug == plug + 1) + { + return i; + } + } + return INSTRUMENTINDEX_INVALID; +} + + +bool CModDoc::RemoveOrder(SEQUENCEINDEX nSeq, ORDERINDEX nOrd) +{ + if(nSeq >= m_SndFile.Order.GetNumSequences() || nOrd >= m_SndFile.Order(nSeq).size()) + return false; + + CriticalSection cs; + m_SndFile.Order(nSeq).Remove(nOrd, nOrd); + SetModified(); + + return true; +} + + + +bool CModDoc::RemovePattern(PATTERNINDEX nPat) +{ + if(m_SndFile.Patterns.IsValidPat(nPat)) + { + CriticalSection cs; + GetPatternUndo().PrepareUndo(nPat, 0, 0, GetNumChannels(), m_SndFile.Patterns[nPat].GetNumRows(), "Remove Pattern"); + m_SndFile.Patterns.Remove(nPat); + SetModified(); + + return true; + } + return false; +} + + +bool CModDoc::RemoveSample(SAMPLEINDEX nSmp) +{ + if((nSmp) && (nSmp <= m_SndFile.GetNumSamples())) + { + CriticalSection cs; + + m_SndFile.DestroySample(nSmp); + m_SndFile.m_szNames[nSmp] = ""; + while((m_SndFile.GetNumSamples() > 1) + && (!m_SndFile.m_szNames[m_SndFile.GetNumSamples()][0]) + && (!m_SndFile.GetSample(m_SndFile.GetNumSamples()).HasSampleData())) + { + m_SndFile.m_nSamples--; + } + SetModified(); + + return true; + } + return false; +} + + +bool CModDoc::RemoveInstrument(INSTRUMENTINDEX nIns) +{ + if((nIns) && (nIns <= m_SndFile.GetNumInstruments()) && (m_SndFile.Instruments[nIns])) + { + ConfirmAnswer result = cnfNo; + if(!m_SndFile.Instruments[nIns]->GetSamples().empty()) + result = Reporting::Confirm("Remove samples associated with an instrument if they are unused?", "Removing instrument", true); + if(result == cnfCancel) + { + return false; + } + if(m_SndFile.DestroyInstrument(nIns, (result == cnfYes) ? deleteAssociatedSamples : doNoDeleteAssociatedSamples)) + { + CriticalSection cs; + if(nIns == m_SndFile.m_nInstruments) + m_SndFile.m_nInstruments--; + bool instrumentsLeft = std::find_if(std::begin(m_SndFile.Instruments), std::end(m_SndFile.Instruments), [](ModInstrument *ins) { return ins != nullptr; }) != std::end(m_SndFile.Instruments); + if(!instrumentsLeft) + m_SndFile.m_nInstruments = 0; + SetModified(); + + return true; + } + } + return false; +} + + +bool CModDoc::MoveOrder(ORDERINDEX sourceOrd, ORDERINDEX destOrd, bool update, bool copy, SEQUENCEINDEX sourceSeq, SEQUENCEINDEX destSeq) +{ + if(sourceSeq == SEQUENCEINDEX_INVALID) + sourceSeq = m_SndFile.Order.GetCurrentSequenceIndex(); + if(destSeq == SEQUENCEINDEX_INVALID) + destSeq = m_SndFile.Order.GetCurrentSequenceIndex(); + if(std::max(sourceSeq, destSeq) >= m_SndFile.Order.GetNumSequences()) + return false; + + const ORDERINDEX maxOrders = m_SndFile.GetModSpecifications().ordersMax; + if(destOrd > maxOrders) + return false; + if(destOrd == maxOrders && (sourceSeq != destSeq || copy)) + return false; + + auto &sourceSequence = m_SndFile.Order(sourceSeq); + const PATTERNINDEX sourcePat = sourceOrd < sourceSequence.size() ? sourceSequence[sourceOrd] : sourceSequence.GetInvalidPatIndex(); + + // Delete source + if(!copy) + { + sourceSequence.Remove(sourceOrd, sourceOrd); + if(sourceOrd < destOrd && sourceSeq == destSeq) + destOrd--; + } + // Insert at dest + m_SndFile.Order(destSeq).insert(destOrd, 1, sourcePat); + + if(update) + { + UpdateAllViews(nullptr, SequenceHint().Data()); + } + return true; +} + + +BOOL CModDoc::ExpandPattern(PATTERNINDEX nPattern) +{ + ROWINDEX numRows; + + if(!m_SndFile.Patterns.IsValidPat(nPattern) + || (numRows = m_SndFile.Patterns[nPattern].GetNumRows()) > m_SndFile.GetModSpecifications().patternRowsMax / 2) + { + return false; + } + + BeginWaitCursor(); + CriticalSection cs; + GetPatternUndo().PrepareUndo(nPattern, 0, 0, GetNumChannels(), numRows, "Expand Pattern"); + bool success = m_SndFile.Patterns[nPattern].Expand(); + cs.Leave(); + EndWaitCursor(); + + if(success) + { + SetModified(); + UpdateAllViews(NULL, PatternHint(nPattern).Data(), NULL); + } else + { + GetPatternUndo().RemoveLastUndoStep(); + } + return success; +} + + +BOOL CModDoc::ShrinkPattern(PATTERNINDEX nPattern) +{ + ROWINDEX numRows; + + if(!m_SndFile.Patterns.IsValidPat(nPattern) + || (numRows = m_SndFile.Patterns[nPattern].GetNumRows()) < m_SndFile.GetModSpecifications().patternRowsMin * 2) + { + return false; + } + + BeginWaitCursor(); + CriticalSection cs; + GetPatternUndo().PrepareUndo(nPattern, 0, 0, GetNumChannels(), numRows, "Shrink Pattern"); + bool success = m_SndFile.Patterns[nPattern].Shrink(); + cs.Leave(); + EndWaitCursor(); + + if(success) + { + SetModified(); + UpdateAllViews(NULL, PatternHint(nPattern).Data(), NULL); + } else + { + GetPatternUndo().RemoveLastUndoStep(); + } + return success; +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// Copy/Paste envelope + +static constexpr const char pszEnvHdr[] = "ModPlug Tracker Envelope\r\n"; +static constexpr const char pszEnvFmt[] = "%d,%d,%d,%d,%d,%d,%d,%d\r\n"; + +static bool EnvelopeToString(CStringA &s, const InstrumentEnvelope &env) +{ + // We don't want to copy empty envelopes + if(env.empty()) + { + return false; + } + + s.Preallocate(2048); + s = pszEnvHdr; + s.AppendFormat(pszEnvFmt, env.size(), env.nSustainStart, env.nSustainEnd, env.nLoopStart, env.nLoopEnd, env.dwFlags[ENV_SUSTAIN] ? 1 : 0, env.dwFlags[ENV_LOOP] ? 1 : 0, env.dwFlags[ENV_CARRY] ? 1 : 0); + for(auto &p : env) + { + s.AppendFormat("%d,%d\r\n", p.tick, p.value); + } + + // Writing release node + s.AppendFormat("%u\r\n", env.nReleaseNode); + return true; +} + + +static bool StringToEnvelope(const std::string_view &s, InstrumentEnvelope &env, const CModSpecifications &specs) +{ + uint32 susBegin = 0, susEnd = 0, loopBegin = 0, loopEnd = 0, bSus = 0, bLoop = 0, bCarry = 0, nPoints = 0, releaseNode = ENV_RELEASE_NODE_UNSET; + size_t length = s.size(), pos = std::size(pszEnvHdr) - 1; + if(length <= pos || mpt::CompareNoCaseAscii(s.data(), pszEnvHdr, pos - 2)) + { + return false; + } + sscanf(&s[pos], pszEnvFmt, &nPoints, &susBegin, &susEnd, &loopBegin, &loopEnd, &bSus, &bLoop, &bCarry); + while(pos < length && s[pos] != '\r' && s[pos] != '\n') + pos++; + + nPoints = std::min(nPoints, static_cast<uint32>(specs.envelopePointsMax)); + if(susEnd >= nPoints) + susEnd = 0; + if(susBegin > susEnd) + susBegin = susEnd; + if(loopEnd >= nPoints) + loopEnd = 0; + if(loopBegin > loopEnd) + loopBegin = loopEnd; + + try + { + env.resize(nPoints); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + env.nSustainStart = static_cast<decltype(env.nSustainStart)>(susBegin); + env.nSustainEnd = static_cast<decltype(env.nSustainEnd)>(susEnd); + env.nLoopStart = static_cast<decltype(env.nLoopStart)>(loopBegin); + env.nLoopEnd = static_cast<decltype(env.nLoopEnd)>(loopEnd); + env.nReleaseNode = static_cast<decltype(env.nReleaseNode)>(releaseNode); + env.dwFlags.set(ENV_LOOP, bLoop != 0); + env.dwFlags.set(ENV_SUSTAIN, bSus != 0); + env.dwFlags.set(ENV_CARRY, bCarry != 0); + env.dwFlags.set(ENV_ENABLED, nPoints > 0); + + int oldn = 0; + for(auto &p : env) + { + while(pos < length && (s[pos] < '0' || s[pos] > '9')) + pos++; + if(pos >= length) + break; + int n1 = atoi(&s[pos]); + while(pos < length && s[pos] != ',') + pos++; + while(pos < length && (s[pos] < '0' || s[pos] > '9')) + pos++; + if(pos >= length) + break; + int n2 = atoi(&s[pos]); + if(n1 < oldn) + n1 = oldn + 1; + Limit(n2, ENVELOPE_MIN, ENVELOPE_MAX); + p.tick = (uint16)n1; + p.value = (uint8)n2; + oldn = n1; + while(pos < length && s[pos] != '\r' && s[pos] != '\n') + pos++; + if(pos >= length) + break; + } + env.Sanitize(); + + // Read release node information. + env.nReleaseNode = ENV_RELEASE_NODE_UNSET; + if(pos < length) + { + auto r = static_cast<decltype(env.nReleaseNode)>(atoi(&s[pos])); + if(r == 0 || r >= nPoints || !specs.hasReleaseNode) + r = ENV_RELEASE_NODE_UNSET; + env.nReleaseNode = r; + } + return true; +} + + +bool CModDoc::CopyEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + + if((nIns < 1) || (nIns > m_SndFile.m_nInstruments) || (!m_SndFile.Instruments[nIns]) || (!pMainFrm)) + return false; + BeginWaitCursor(); + const ModInstrument *pIns = m_SndFile.Instruments[nIns]; + if(pIns == nullptr) + return false; + + CStringA s; + EnvelopeToString(s, pIns->GetEnvelope(nEnv)); + + int memSize = s.GetLength() + 1; + Clipboard clipboard(CF_TEXT, memSize); + if(auto p = clipboard.As<char>()) + { + memcpy(p, s.GetString(), memSize); + } + EndWaitCursor(); + return true; +} + + +bool CModDoc::SaveEnvelope(INSTRUMENTINDEX ins, EnvelopeType env, const mpt::PathString &fileName) +{ + if(ins < 1 || ins > m_SndFile.m_nInstruments || !m_SndFile.Instruments[ins]) + return false; + BeginWaitCursor(); + const ModInstrument *pIns = m_SndFile.Instruments[ins]; + if(pIns == nullptr) + return false; + + CStringA s; + EnvelopeToString(s, pIns->GetEnvelope(env)); + + bool ok = false; + try + { + mpt::SafeOutputFile sf(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + if(f) + ok = mpt::IO::WriteRaw(f, s.GetString(), s.GetLength()); + else + ok = false; + } catch(const std::exception &) + { + ok = false; + } + EndWaitCursor(); + return ok; +} + + +bool CModDoc::PasteEnvelope(INSTRUMENTINDEX ins, EnvelopeType env) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(ins < 1 || ins > m_SndFile.m_nInstruments || !m_SndFile.Instruments[ins] || !pMainFrm) + return false; + BeginWaitCursor(); + Clipboard clipboard(CF_TEXT); + auto data = clipboard.GetString(); + if(!data.length()) + { + EndWaitCursor(); + return false; + } + bool result = StringToEnvelope(data, m_SndFile.Instruments[ins]->GetEnvelope(env), m_SndFile.GetModSpecifications()); + EndWaitCursor(); + return result; +} + + +bool CModDoc::LoadEnvelope(INSTRUMENTINDEX nIns, EnvelopeType nEnv, const mpt::PathString &fileName) +{ + InputFile f(fileName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(nIns < 1 || nIns > m_SndFile.m_nInstruments || !m_SndFile.Instruments[nIns] || !f.IsValid()) + return false; + BeginWaitCursor(); + FileReader file = GetFileReader(f); + std::string data; + file.ReadNullString(data, 1 << 16); + bool result = StringToEnvelope(data, m_SndFile.Instruments[nIns]->GetEnvelope(nEnv), m_SndFile.GetModSpecifications()); + EndWaitCursor(); + return result; +} + + +// Check which channels contain note data. maxRemoveCount specified how many empty channels are reported at max. +void CModDoc::CheckUsedChannels(std::vector<bool> &usedMask, CHANNELINDEX maxRemoveCount) const +{ + // Checking for unused channels + CHANNELINDEX chn = GetNumChannels(); + usedMask.assign(chn, true); + while(chn-- > 0) + { + if(IsChannelUnused(chn)) + { + usedMask[chn] = false; + // Found enough empty channels yet? + if((--maxRemoveCount) == 0) + break; + } + } +} + + +// Check if a given channel contains note data or global effects. +bool CModDoc::IsChannelUnused(CHANNELINDEX nChn) const +{ + const CHANNELINDEX nChannels = GetNumChannels(); + if(nChn >= nChannels) + { + return true; + } + for(auto &pat : m_SndFile.Patterns) + { + if(pat.IsValid()) + { + const ModCommand *p = pat.GetpModCommand(0, nChn); + for(ROWINDEX row = pat.GetNumRows(); row > 0; row--, p += nChannels) + { + if(p->IsNote() || p->IsInstrPlug() || p->IsGlobalCommand()) + return false; + } + } + } + return true; +} + + +bool CModDoc::IsSampleUsed(SAMPLEINDEX sample, bool searchInMutedChannels) const +{ + if(!sample || sample > GetNumSamples()) + return false; + if(GetNumInstruments()) + { + for(INSTRUMENTINDEX i = 1; i <= GetNumInstruments(); i++) + { + if(m_SndFile.IsSampleReferencedByInstrument(sample, i)) + return true; + } + } else + { + for(const auto &pattern : m_SndFile.Patterns) + { + if(!pattern.IsValid()) + continue; + auto m = pattern.cbegin(); + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels(); chn++, m++) + { + if(searchInMutedChannels || !m_SndFile.ChnSettings[chn].dwFlags[CHN_MUTE]) + { + if(m->instr == sample && !m->IsPcNote()) + return true; + } + } + } + } + } + return false; +} + + +bool CModDoc::IsInstrumentUsed(INSTRUMENTINDEX instr, bool searchInMutedChannels) const +{ + if(instr < 1 || instr > GetNumInstruments()) + return false; + for(const auto &pattern : m_SndFile.Patterns) + { + if(!pattern.IsValid()) + continue; + auto m = pattern.cbegin(); + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + for(CHANNELINDEX chn = 0; chn < pattern.GetNumChannels(); chn++, m++) + { + if(searchInMutedChannels || !m_SndFile.ChnSettings[chn].dwFlags[CHN_MUTE]) + { + if(m->instr == instr && !m->IsPcNote()) + return true; + } + } + } + } + return false; +} + + +// Convert module's default global volume to a pattern command. +bool CModDoc::GlobalVolumeToPattern() +{ + bool result = false; + if(m_SndFile.GetModSpecifications().HasCommand(CMD_GLOBALVOLUME)) + { + for(PATTERNINDEX pat : m_SndFile.Order()) + { + if(m_SndFile.Patterns[pat].WriteEffect(EffectWriter(CMD_GLOBALVOLUME, mpt::saturate_cast<ModCommand::PARAM>(m_SndFile.m_nDefaultGlobalVolume * 64 / MAX_GLOBAL_VOLUME)).RetryNextRow())) + { + result = true; + break; + } + } + } + + m_SndFile.m_nDefaultGlobalVolume = MAX_GLOBAL_VOLUME; + return result; +} + + +SAMPLEINDEX CModDoc::GetSampleIndex(const ModCommand &m, ModCommand::INSTR lastInstr) const +{ + if(m.IsPcNote()) + return 0; + return m_SndFile.GetSampleIndex(m.note, m.instr > 0 ? m.instr : lastInstr); +} + + +int CModDoc::GetInstrumentGroupSize(INSTRUMENTINDEX instr) const +{ + const ModInstrument *ins; + if(instr > 0 && instr <= GetNumInstruments() + && (ins = m_SndFile.Instruments[instr]) != nullptr && ins->pTuning != nullptr + && ins->pTuning->GetGroupSize() != 0) + { + return ins->pTuning->GetGroupSize(); + } + return 12; +} + + +int CModDoc::GetBaseNote(INSTRUMENTINDEX instr) const +{ + // This may look a bit strange (using -12 and -4 instead of just -5 in the second part) but this is to keep custom tunings centered around middle-C on the keyboard. + return NOTE_MIDDLEC - 12 + (CMainFrame::GetMainFrame()->GetBaseOctave() - 4) * GetInstrumentGroupSize(instr); +} + + +ModCommand::NOTE CModDoc::GetNoteWithBaseOctave(int noteOffset, INSTRUMENTINDEX instr) const +{ + return static_cast<ModCommand::NOTE>(Clamp(GetBaseNote(instr) + noteOffset, NOTE_MIN, NOTE_MAX)); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.cpp new file mode 100644 index 00000000..6f9ec2aa --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.cpp @@ -0,0 +1,90 @@ +/* + * MoveFXSlotDialog.h + * ------------------ + * Purpose: Implementationof OpenMPT's move plugin dialog. + * 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 "MoveFXSlotDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +void CMoveFXSlotDialog::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO1, m_CbnEmptySlots); +} + + +CMoveFXSlotDialog::CMoveFXSlotDialog(CWnd *pParent, PLUGINDEX currentSlot, const std::vector<PLUGINDEX> &emptySlots, PLUGINDEX defaultIndex, bool clone, bool hasChain) : + CDialog(CMoveFXSlotDialog::IDD, pParent), + m_EmptySlots(emptySlots), + m_nDefaultSlot(defaultIndex), + moveChain(hasChain) +{ + if(clone) + { + m_csPrompt.Format(_T("Clone plugin in slot %d to the following empty slot:"), currentSlot + 1); + m_csTitle = _T("Clone To Slot..."); + m_csChain = _T("&Clone follow-up plugin chain if possible"); + } else + { + m_csPrompt.Format(_T("Move plugin in slot %d to the following empty slot:"), currentSlot + 1); + m_csTitle = _T("Move To Slot..."); + m_csChain = _T("&Move follow-up plugin chain if possible"); + } +} + + +BOOL CMoveFXSlotDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetDlgItemText(IDC_STATIC1, m_csPrompt); + SetDlgItemText(IDC_CHECK1, m_csChain); + SetWindowText(m_csTitle); + + if(m_EmptySlots.empty()) + { + Reporting::Error("No empty plugin slots are availabe."); + OnCancel(); + return TRUE; + } + + CString slotText; + std::size_t defaultSlot = 0; + bool foundDefault = false; + for(size_t nSlot = 0; nSlot < m_EmptySlots.size(); nSlot++) + { + slotText.Format(_T("FX%d"), m_EmptySlots[nSlot] + 1); + m_CbnEmptySlots.SetItemData(m_CbnEmptySlots.AddString(slotText), nSlot); + if(m_EmptySlots[nSlot] >= m_nDefaultSlot && !foundDefault) + { + defaultSlot = nSlot; + foundDefault = true; + } + } + m_CbnEmptySlots.SetCurSel(static_cast<int>(defaultSlot)); + + GetDlgItem(IDC_CHECK1)->EnableWindow(moveChain ? TRUE : FALSE); + CheckDlgButton(IDC_CHECK1, moveChain ? BST_CHECKED : BST_UNCHECKED); + + return TRUE; +} + + +void CMoveFXSlotDialog::OnOK() +{ + m_nToSlot = m_CbnEmptySlots.GetItemData(m_CbnEmptySlots.GetCurSel()); + moveChain = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + CDialog::OnOK(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.h new file mode 100644 index 00000000..b0e49b54 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/MoveFXSlotDialog.h @@ -0,0 +1,43 @@ +/* + * MoveFXSlotDialog.h + * ------------------ + * Purpose: Implementationof OpenMPT's move plugin dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CMoveFXSlotDialog : public CDialog +{ +protected: + const std::vector<PLUGINDEX> &m_EmptySlots; + CString m_csPrompt, m_csTitle, m_csChain; + size_t m_nToSlot; + PLUGINDEX m_nDefaultSlot; + bool moveChain; + + CComboBox m_CbnEmptySlots; + + enum { IDD = IDD_MOVEFXSLOT }; + +public: + CMoveFXSlotDialog(CWnd *pParent, PLUGINDEX currentSlot, const std::vector<PLUGINDEX> &emptySlots, PLUGINDEX defaultIndex, bool clone, bool hasChain); + PLUGINDEX GetSlot() const { return m_EmptySlots[m_nToSlot]; } + size_t GetSlotIndex() const { return m_nToSlot; } + bool DoMoveChain() const { return moveChain; } + +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + + virtual void OnOK(); + virtual BOOL OnInitDialog(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.cpp new file mode 100644 index 00000000..26e0b2aa --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.cpp @@ -0,0 +1,1991 @@ +/* + * MPDlgs.cpp + * ---------- + * Purpose: Implementation of various player setup dialogs. + * 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 "Sndfile.h" +#include "Mainfrm.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "Mpdlgs.h" +#include "dlg_misc.h" +#include "../common/mptStringBuffer.h" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "../common/Dither.h" + + +OPENMPT_NAMESPACE_BEGIN + + +const TCHAR *gszChnCfgNames[3] = +{ + _T("Mono"), + _T("Stereo"), + _T("Quad") +}; + + +static double ParseTime(CString str) +{ + return ConvertStrTo<double>(mpt::ToCharset(mpt::Charset::ASCII, str)) / 1000.0; +} + + +static CString PrintTime(double seconds) +{ + int32 microseconds = mpt::saturate_round<int32>(seconds * 1000000.0); + int precision = 0; + if(microseconds < 1000) + { + precision = 3; + } else if(microseconds < 10000) + { + precision = 2; + } else if(microseconds < 100000) + { + precision = 1; + } else + { + precision = 0; + } + return MPT_CFORMAT("{} ms")(mpt::cfmt::fix(seconds * 1000.0, precision)); +} + + +BEGIN_MESSAGE_MAP(COptionsSoundcard, CPropertyPage) + ON_WM_HSCROLL() + ON_COMMAND(IDC_CHECK4, &COptionsSoundcard::OnExclusiveModeChanged) + ON_COMMAND(IDC_CHECK5, &COptionsSoundcard::OnSettingsChanged) + ON_COMMAND(IDC_CHECK7, &COptionsSoundcard::OnSettingsChanged) + ON_COMMAND(IDC_CHECK9, &COptionsSoundcard::OnSettingsChanged) + ON_COMMAND(IDC_CHECK_SOUNDCARD_SHOWALL, &COptionsSoundcard::OnSoundCardShowAll) + ON_CBN_SELCHANGE(IDC_COMBO1, &COptionsSoundcard::OnDeviceChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO_UPDATEINTERVAL, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO5, &COptionsSoundcard::OnChannelsChanged) + ON_CBN_SELCHANGE(IDC_COMBO6, &COptionsSoundcard::OnSampleFormatChanged) + ON_CBN_SELCHANGE(IDC_COMBO10, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_EDITCHANGE(IDC_COMBO2, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_EDITCHANGE(IDC_COMBO_UPDATEINTERVAL, &COptionsSoundcard::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO11, &COptionsSoundcard::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsSoundcard::OnSoundCardRescan) + ON_COMMAND(IDC_BUTTON2, &COptionsSoundcard::OnSoundCardDriverPanel) + ON_CBN_SELCHANGE(IDC_COMBO_CHANNEL_FRONTLEFT, &COptionsSoundcard::OnChannel1Changed) + ON_CBN_SELCHANGE(IDC_COMBO_CHANNEL_FRONTRIGHT, &COptionsSoundcard::OnChannel2Changed) + ON_CBN_SELCHANGE(IDC_COMBO_CHANNEL_REARLEFT, &COptionsSoundcard::OnChannel3Changed) + ON_CBN_SELCHANGE(IDC_COMBO_CHANNEL_REARRIGHT, &COptionsSoundcard::OnChannel4Changed) + ON_CBN_SELCHANGE(IDC_COMBO_RECORDING_CHANNELS, &COptionsSoundcard::OnRecordingChanged) + ON_CBN_SELCHANGE(IDC_COMBO_RECORDING_SOURCE, &COptionsSoundcard::OnSettingsChanged) +END_MESSAGE_MAP() + + +void COptionsSoundcard::OnSampleFormatChanged() +{ + OnSettingsChanged(); + UpdateDither(); +} + + +void COptionsSoundcard::OnRecordingChanged() +{ + DWORD_PTR inputChannels = m_CbnRecordingChannels.GetItemData(m_CbnRecordingChannels.GetCurSel()); + m_CbnRecordingSource.EnableWindow((m_CurrentDeviceCaps.HasNamedInputSources && inputChannels > 0) ? TRUE : FALSE); + OnSettingsChanged(); +} + + +void COptionsSoundcard::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsSoundcard) + DDX_Control(pDX, IDC_COMBO1, m_CbnDevice); + DDX_Control(pDX, IDC_COMBO2, m_CbnLatencyMS); + DDX_Control(pDX, IDC_COMBO_UPDATEINTERVAL, m_CbnUpdateIntervalMS); + DDX_Control(pDX, IDC_COMBO3, m_CbnMixingFreq); + DDX_Control(pDX, IDC_COMBO5, m_CbnChannels); + DDX_Control(pDX, IDC_COMBO6, m_CbnSampleFormat); + DDX_Control(pDX, IDC_COMBO10, m_CbnDither); + DDX_Control(pDX, IDC_BUTTON2, m_BtnDriverPanel); + DDX_Control(pDX, IDC_COMBO6, m_CbnSampleFormat); + DDX_Control(pDX, IDC_COMBO11, m_CbnStoppedMode); + DDX_Control(pDX, IDC_COMBO_CHANNEL_FRONTLEFT , m_CbnChannelMapping[0]); + DDX_Control(pDX, IDC_COMBO_CHANNEL_FRONTRIGHT, m_CbnChannelMapping[1]); + DDX_Control(pDX, IDC_COMBO_CHANNEL_REARLEFT , m_CbnChannelMapping[2]); + DDX_Control(pDX, IDC_COMBO_CHANNEL_REARRIGHT , m_CbnChannelMapping[3]); + DDX_Control(pDX, IDC_COMBO_RECORDING_CHANNELS, m_CbnRecordingChannels); + DDX_Control(pDX, IDC_COMBO_RECORDING_SOURCE, m_CbnRecordingSource); + DDX_Control(pDX, IDC_EDIT_STATISTICS, m_EditStatistics); + //}}AFX_DATA_MAP +} + + +COptionsSoundcard::COptionsSoundcard(SoundDevice::Identifier deviceIdentifier) + : CPropertyPage(IDD_OPTIONS_SOUNDCARD) + , m_InitialDeviceIdentifier(deviceIdentifier) +{ + return; +} + + +void COptionsSoundcard::SetInitialDevice() +{ + SetDevice(m_InitialDeviceIdentifier, true); +} + + +void COptionsSoundcard::SetDevice(SoundDevice::Identifier dev, bool forceReload) +{ + SoundDevice::Identifier olddev = m_CurrentDeviceInfo.GetIdentifier(); + SoundDevice::Info newInfo; + SoundDevice::Caps newCaps; + SoundDevice::DynamicCaps newDynamicCaps; + SoundDevice::Settings newSettings; + newInfo = theApp.GetSoundDevicesManager()->FindDeviceInfo(dev); + newCaps = theApp.GetSoundDevicesManager()->GetDeviceCaps(dev, CMainFrame::GetMainFrame()->gpSoundDevice); + newDynamicCaps = theApp.GetSoundDevicesManager()->GetDeviceDynamicCaps(dev, TrackerSettings::Instance().GetSampleRates(), CMainFrame::GetMainFrame(), CMainFrame::GetMainFrame()->gpSoundDevice, true); + bool deviceChanged = (dev != olddev); + if(deviceChanged || forceReload) + { + newSettings = TrackerSettings::Instance().GetSoundDeviceSettings(dev); + } else + { + newSettings = m_Settings; + } + m_CurrentDeviceInfo = newInfo; + m_CurrentDeviceCaps = newCaps; + m_CurrentDeviceDynamicCaps = newDynamicCaps; + m_Settings = newSettings; +} + + +void COptionsSoundcard::OnSoundCardShowAll() +{ + TrackerSettings::Instance().m_SoundShowDeprecatedDevices = (IsDlgButtonChecked(IDC_CHECK_SOUNDCARD_SHOWALL) == BST_CHECKED); + SetDevice(m_CurrentDeviceInfo.GetIdentifier(), true); + UpdateEverything(); +} + + +void COptionsSoundcard::OnSoundCardRescan() +{ + { + // Close sound device because IDs might change when re-enumerating which could cause all kinds of havoc. + CMainFrame::GetMainFrame()->audioCloseDevice(); + delete CMainFrame::GetMainFrame()->gpSoundDevice; + CMainFrame::GetMainFrame()->gpSoundDevice = nullptr; + } + theApp.GetSoundDevicesManager()->ReEnumerate(); + SetDevice(m_CurrentDeviceInfo.GetIdentifier(), true); + UpdateEverything(); +} + + +BOOL COptionsSoundcard::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + SetInitialDevice(); + UpdateEverything(); + return TRUE; +} + + +void COptionsSoundcard::UpdateLatency() +{ + { + GetDlgItem(IDC_STATIC_LATENCY)->EnableWindow(TRUE); + m_CbnLatencyMS.EnableWindow(TRUE); + } + // latency + { + static constexpr double latencies [] = { + 0.001, + 0.002, + 0.003, + 0.004, + 0.005, + 0.010, + 0.015, + 0.020, + 0.025, + 0.030, + 0.040, + 0.050, + 0.075, + 0.100, + 0.150, + 0.200, + 0.250 + }; + m_CbnLatencyMS.ResetContent(); + m_CbnLatencyMS.SetWindowText(PrintTime(m_Settings.Latency)); + for(auto lat : latencies) + { + if(m_CurrentDeviceCaps.LatencyMin <= lat && lat <= m_CurrentDeviceCaps.LatencyMax) + { + m_CbnLatencyMS.AddString(PrintTime(lat)); + } + } + } + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + GetDlgItem(IDC_STATIC_LATENCY)->EnableWindow(FALSE); + m_CbnLatencyMS.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateUpdateInterval() +{ + { + m_CbnUpdateIntervalMS.EnableWindow(TRUE); + } + // update interval + { + static constexpr double updateIntervals [] = { + 0.001, + 0.002, + 0.005, + 0.010, + 0.015, + 0.020, + 0.025, + 0.050 + }; + m_CbnUpdateIntervalMS.ResetContent(); + m_CbnUpdateIntervalMS.SetWindowText(PrintTime(m_Settings.UpdateInterval)); + for(auto upd : updateIntervals) + { + if(m_CurrentDeviceCaps.UpdateIntervalMin <= upd && upd <= m_CurrentDeviceCaps.UpdateIntervalMax) + { + m_CbnUpdateIntervalMS.AddString(PrintTime(upd)); + } + } + } + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier()) || !m_CurrentDeviceCaps.CanUpdateInterval) + { + m_CbnUpdateIntervalMS.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateGeneral() +{ + // General + { + if(m_CurrentDeviceCaps.CanKeepDeviceRunning) + { + m_CbnStoppedMode.ResetContent(); + m_CbnStoppedMode.AddString(_T("Close driver")); + m_CbnStoppedMode.AddString(_T("Pause driver")); + m_CbnStoppedMode.AddString(_T("Play silence")); + m_CbnStoppedMode.SetCurSel(TrackerSettings::Instance().m_SoundSettingsStopMode); + } else + { + m_CbnStoppedMode.ResetContent(); + m_CbnStoppedMode.AddString(_T("Close driver")); + m_CbnStoppedMode.AddString(_T("Close driver")); + m_CbnStoppedMode.AddString(_T("Close driver")); + m_CbnStoppedMode.SetCurSel(TrackerSettings::Instance().m_SoundSettingsStopMode); + } + CheckDlgButton(IDC_CHECK7, TrackerSettings::Instance().m_SoundSettingsOpenDeviceAtStartup ? BST_CHECKED : BST_UNCHECKED); + } + bool isUnavailble = theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier()); + m_CbnStoppedMode.EnableWindow(isUnavailble ? FALSE : (m_CurrentDeviceCaps.CanKeepDeviceRunning ? TRUE : FALSE)); + CPropertySheet *sheet = dynamic_cast<CPropertySheet *>(GetParent()); + if(sheet) sheet->GetDlgItem(IDOK)->EnableWindow(isUnavailble ? FALSE : TRUE); +} + + +void COptionsSoundcard::UpdateEverything() +{ + // Sound Device + { + if(m_CurrentDeviceInfo.IsDeprecated()) + { + TrackerSettings::Instance().m_SoundShowDeprecatedDevices = true; + } + CheckDlgButton(IDC_CHECK_SOUNDCARD_SHOWALL, TrackerSettings::Instance().m_SoundShowDeprecatedDevices ? BST_CHECKED : BST_UNCHECKED); + + m_CbnDevice.ResetContent(); + m_CbnDevice.SetImageList(&CMainFrame::GetMainFrame()->m_MiscIcons); + + UINT iItem = 0; + + for(const auto &it : *theApp.GetSoundDevicesManager()) + { + + if(!TrackerSettings::Instance().m_SoundShowDeprecatedDevices) + { + if(it.IsDeprecated()) + { + continue; + } + } + + { + COMBOBOXEXITEM cbi; + MemsetZero(cbi); + cbi.iItem = iItem; + cbi.cchTextMax = 0; + cbi.mask = CBEIF_LPARAM | CBEIF_TEXT; + cbi.lParam = theApp.GetSoundDevicesManager()->GetGlobalID(it.GetIdentifier()); + mpt::ustring TypeWineNative = U_("Wine-Native"); + if(it.type == SoundDevice::TypeWAVEOUT || it.type == SoundDevice::TypePORTAUDIO_WMME) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_WAVEOUT; + } else if(it.type == SoundDevice::TypeDSOUND || it.type == SoundDevice::TypePORTAUDIO_DS || it.type == U_("RtAudio-ds")) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_DIRECTX; + } else if(it.type == SoundDevice::TypeASIO || it.type == U_("RtAudio-asio")) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_ASIO; + } else if(it.type == SoundDevice::TypePORTAUDIO_WASAPI || it.type == U_("RtAudio-wasapi")) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_SAMPLEMUTE; // // No real image available for now, + } else if(it.type == SoundDevice::TypePORTAUDIO_WDMKS) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_CHIP; // No real image available for now, + } else if(it.type.find(TypeWineNative + U_("-")) == 0) + { + if(theApp.GetWineVersion() && (theApp.GetWineVersion()->HostClass() == mpt::osinfo::osclass::Linux)) + { + cbi.mask |= CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_OVERLAY; + cbi.iImage = IMAGE_TUX; + } else + { + cbi.iImage = 0; + } + } else + { + cbi.iImage = 0; + } + cbi.iSelectedImage = cbi.iImage; + cbi.iOverlay = cbi.iImage; + CString tmp = mpt::ToCString(it.GetDisplayName()); + cbi.pszText = const_cast<TCHAR *>(tmp.GetString()); + cbi.iIndent = 0; + int pos = m_CbnDevice.InsertItem(&cbi); + if(static_cast<SoundDevice::Manager::GlobalID>(cbi.lParam) == theApp.GetSoundDevicesManager()->GetGlobalID(m_CurrentDeviceInfo.GetIdentifier())) + { + m_CbnDevice.SetCurSel(pos); + } + iItem++; + } + } + } + + UpdateDevice(); + +} + + +void COptionsSoundcard::UpdateDevice() +{ + GetDlgItem(IDC_CHECK_SOUNDCARD_SHOWALL)->EnableWindow(m_CurrentDeviceInfo.IsDeprecated() ? FALSE : TRUE); + UpdateGeneral(); + UpdateControls(); + UpdateLatency(); + UpdateUpdateInterval(); + UpdateSampleRates(); + UpdateChannels(); + UpdateSampleFormat(); + UpdateDither(); + UpdateChannelMapping(); + UpdateRecording(); +} + + +void COptionsSoundcard::UpdateChannels() +{ + { + m_CbnChannels.EnableWindow(TRUE); + } + m_CbnChannels.ResetContent(); + int maxChannels = 0; + if(m_CurrentDeviceDynamicCaps.channelNames.size() > 0) + { + maxChannels = static_cast<int>(std::min(std::size_t(4), m_CurrentDeviceDynamicCaps.channelNames.size())); + } else + { + maxChannels = 4; + } + int sel = 0; + for(int channels = maxChannels; channels >= 1; channels /= 2) + { + int ndx = m_CbnChannels.AddString(gszChnCfgNames[(channels+2)/2-1]); + m_CbnChannels.SetItemData(ndx, channels); + if(channels == m_Settings.Channels) + { + sel = ndx; + } + } + m_CbnChannels.SetCurSel(sel); + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + m_CbnChannels.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateRecording() +{ + GetDlgItem(IDC_STATIC_RECORDING)->ShowWindow(TrackerSettings::Instance().m_SoundShowRecordingSettings ? SW_SHOW : SW_HIDE); + m_CbnRecordingChannels.ShowWindow(TrackerSettings::Instance().m_SoundShowRecordingSettings ? SW_SHOW : SW_HIDE); + m_CbnRecordingSource.ShowWindow(TrackerSettings::Instance().m_SoundShowRecordingSettings ? SW_SHOW : SW_HIDE); + m_CbnRecordingChannels.ResetContent(); + m_CbnRecordingSource.ResetContent(); + if(m_CurrentDeviceCaps.CanInput && ((m_CurrentDeviceCaps.HasNamedInputSources && m_CurrentDeviceDynamicCaps.inputSourceNames.size() > 0) || !m_CurrentDeviceCaps.HasNamedInputSources)) + { + GetDlgItem(IDC_STATIC_RECORDING)->EnableWindow(TRUE); + m_CbnRecordingChannels.EnableWindow(TRUE); + int sel = 0; + { + int ndx = m_CbnRecordingChannels.AddString(_T("off")); + m_CbnRecordingChannels.SetItemData(ndx, 0); + if(0 == m_Settings.InputChannels) + { + sel = ndx; + } + } + for(int channels = 4; channels >= 1; channels /= 2) + { + int ndx = m_CbnRecordingChannels.AddString(gszChnCfgNames[(channels+2)/2-1]); + m_CbnRecordingChannels.SetItemData(ndx, channels); + if(channels == m_Settings.InputChannels) + { + sel = ndx; + } + } + m_CbnRecordingChannels.SetCurSel(sel); + if(m_CurrentDeviceCaps.HasNamedInputSources) + { + m_CbnRecordingSource.EnableWindow((m_Settings.InputChannels > 0) ? TRUE : FALSE); + sel = -1; + for(size_t ch = 0; ch < m_CurrentDeviceDynamicCaps.inputSourceNames.size(); ch++) + { + const int pos = (int)::SendMessageW(m_CbnRecordingSource.m_hWnd, CB_ADDSTRING, 0, (LPARAM)m_CurrentDeviceDynamicCaps.inputSourceNames[ch].second.c_str()); + m_CbnRecordingSource.SetItemData(pos, (DWORD_PTR)m_CurrentDeviceDynamicCaps.inputSourceNames[ch].first); + if(m_CurrentDeviceDynamicCaps.inputSourceNames[ch].first == m_Settings.InputSourceID) + { + sel = pos; + } + } + if(sel == -1 ) sel = 0; + m_CbnRecordingSource.SetCurSel(sel); + } else + { + m_CbnRecordingSource.EnableWindow(FALSE); + } + } else + { + GetDlgItem(IDC_STATIC_RECORDING)->EnableWindow(FALSE); + m_CbnRecordingChannels.EnableWindow(FALSE); + int ndx = m_CbnRecordingChannels.AddString(_T("off")); + m_CbnRecordingChannels.SetItemData(ndx, 0); + m_CbnRecordingChannels.SetCurSel(ndx); + m_CbnRecordingSource.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateSampleFormat() +{ + { + m_CbnSampleFormat.EnableWindow(TRUE); + } + UINT n = 0; + m_CbnSampleFormat.ResetContent(); + std::vector<SampleFormat> sampleformats; + if(IsDlgButtonChecked(IDC_CHECK4)) + { + sampleformats = m_CurrentDeviceDynamicCaps.supportedExclusiveModeSampleFormats; + } else + { + sampleformats = m_CurrentDeviceDynamicCaps.supportedSampleFormats; + } + m_CbnSampleFormat.EnableWindow(m_CurrentDeviceCaps.CanSampleFormat && (sampleformats.size() != 1) ? TRUE : FALSE); + const std::vector<SampleFormat> allSampleFormats = AllSampleFormats<std::vector<SampleFormat>>(); + for(const auto sampleFormat : allSampleFormats) + { + if(!sampleformats.empty() && !mpt::contains(sampleformats, sampleFormat)) + { + continue; + } + CString name; + if(sampleFormat.IsFloat()) + { + name = MPT_CFORMAT("Float {} bit")(sampleFormat.GetBitsPerSample()); + } else if(sampleFormat.IsUnsigned()) + { + name = MPT_CFORMAT("{} Bit uint")(sampleFormat.GetBitsPerSample()); + } else + { + name = MPT_CFORMAT("{} Bit")(sampleFormat.GetBitsPerSample()); + } + UINT ndx = m_CbnSampleFormat.AddString(name); + m_CbnSampleFormat.SetItemData(ndx, mpt::to_underlying<SampleFormat::Enum>(sampleFormat)); + if(sampleFormat == m_Settings.sampleFormat) + { + n = ndx; + } + } + m_CbnSampleFormat.SetCurSel(n); + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + m_CbnSampleFormat.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateDither() +{ + { + m_CbnDither.EnableWindow(TRUE); + } + m_CbnDither.ResetContent(); + SampleFormat sampleFormat = SampleFormat::FromInt(static_cast<int>(m_CbnSampleFormat.GetItemData(m_CbnSampleFormat.GetCurSel()))); + if(sampleFormat.IsInt() && sampleFormat.GetBitsPerSample() < 32) + { + m_CbnDither.EnableWindow(TRUE); + for(std::size_t i = 0; i < DithersOpenMPT::GetNumDithers(); ++i) + { + m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(i) + U_(" dither"))); + } + } else if(m_CurrentDeviceCaps.HasInternalDither) + { + m_CbnDither.EnableWindow(TRUE); + m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(DithersOpenMPT::GetNoDither()) + U_(" dither"))); + m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(DithersOpenMPT::GetDefaultDither()) + U_(" dither"))); + } else + { + m_CbnDither.EnableWindow(FALSE); + for(std::size_t i = 0; i < DithersOpenMPT::GetNumDithers(); ++i) + { + m_CbnDither.AddString(mpt::ToCString(DithersOpenMPT::GetModeName(DithersOpenMPT::GetNoDither()) + U_(" dither"))); + } + } + if(m_Settings.DitherType < 0 || m_Settings.DitherType >= m_CbnDither.GetCount()) + { + m_CbnDither.SetCurSel(1); + } else + { + m_CbnDither.SetCurSel(m_Settings.DitherType); + } + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + m_CbnDither.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateChannelMapping() +{ + { + GetDlgItem(IDC_STATIC_CHANNELMAPPING)->EnableWindow(TRUE); + GetDlgItem(IDC_STATIC_CHANNEL_FRONT)->EnableWindow(TRUE); + GetDlgItem(IDC_STATIC_CHANNEL_REAR)->EnableWindow(TRUE); + for(int mch = 0; mch < NUM_CHANNELCOMBOBOXES; mch++) + { + CComboBox *combo = &m_CbnChannelMapping[mch]; + combo->EnableWindow(TRUE); + } + } + int usedChannels = static_cast<int>(m_CbnChannels.GetItemData(m_CbnChannels.GetCurSel())); + if(m_Settings.Channels.GetNumHostChannels() != static_cast<uint32>(usedChannels)) + { + // If the channel mapping is not valid for the selected number of channels, reset it to default identity mapping. + m_Settings.Channels = SoundDevice::ChannelMapping(usedChannels); + } + GetDlgItem(IDC_STATIC_CHANNELMAPPING)->EnableWindow(m_CurrentDeviceCaps.CanChannelMapping ? TRUE : FALSE); + if(m_CurrentDeviceCaps.CanChannelMapping && usedChannels > 2) + { + GetDlgItem(IDC_STATIC_CHANNEL_FRONT)->EnableWindow(TRUE); + GetDlgItem(IDC_STATIC_CHANNEL_REAR)->EnableWindow(TRUE); + } else + { + GetDlgItem(IDC_STATIC_CHANNEL_FRONT)->EnableWindow(FALSE); + GetDlgItem(IDC_STATIC_CHANNEL_REAR)->EnableWindow(FALSE); + } + for(int mch = 0; mch < NUM_CHANNELCOMBOBOXES; mch++) // Host channels + { + CComboBox *combo = &m_CbnChannelMapping[mch]; + combo->EnableWindow((m_CurrentDeviceCaps.CanChannelMapping && mch < usedChannels) ? TRUE : FALSE); + combo->ResetContent(); + if(m_CurrentDeviceCaps.CanChannelMapping) + { + combo->SetItemData(combo->AddString(_T("Unassigned")), (DWORD_PTR)-1); + combo->SetCurSel(0); + if(mch < usedChannels) + { + for(size_t dch = 0; dch < m_CurrentDeviceDynamicCaps.channelNames.size(); dch++) // Device channels + { + const int pos = (int)::SendMessageW(combo->m_hWnd, CB_ADDSTRING, 0, (LPARAM)m_CurrentDeviceDynamicCaps.channelNames[dch].c_str()); + combo->SetItemData(pos, (DWORD_PTR)dch); + if(static_cast<int32>(dch) == m_Settings.Channels.ToDevice(mch)) + { + combo->SetCurSel(pos); + } + } + } + } + } + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + GetDlgItem(IDC_STATIC_CHANNELMAPPING)->EnableWindow(FALSE); + GetDlgItem(IDC_STATIC_CHANNEL_FRONT)->EnableWindow(FALSE); + GetDlgItem(IDC_STATIC_CHANNEL_REAR)->EnableWindow(FALSE); + for(int mch = 0; mch < NUM_CHANNELCOMBOBOXES; mch++) + { + CComboBox *combo = &m_CbnChannelMapping[mch]; + combo->EnableWindow(FALSE); + } + } +} + + +void COptionsSoundcard::OnDeviceChanged() +{ + int n = m_CbnDevice.GetCurSel(); + if(n >= 0) + { + SetDevice(theApp.GetSoundDevicesManager()->FindDeviceInfo(static_cast<SoundDevice::Manager::GlobalID>(m_CbnDevice.GetItemData(n))).GetIdentifier()); + UpdateDevice(); + OnSettingsChanged(); + } +} + + +void COptionsSoundcard::OnExclusiveModeChanged() +{ + UpdateSampleRates(); + UpdateSampleFormat(); + UpdateDither(); + OnSettingsChanged(); +} + + +void COptionsSoundcard::OnChannelsChanged() +{ + UpdateChannelMapping(); + OnSettingsChanged(); +} + + +void COptionsSoundcard::OnSoundCardDriverPanel() +{ + theApp.GetSoundDevicesManager()->OpenDriverSettings( + theApp.GetSoundDevicesManager()->FindDeviceInfo(static_cast<SoundDevice::Manager::GlobalID>(m_CbnDevice.GetItemData(m_CbnDevice.GetCurSel()))).GetIdentifier(), + CMainFrame::GetMainFrame(), + CMainFrame::GetMainFrame()->gpSoundDevice + ); +} + + +void COptionsSoundcard::OnChannelChanged(int channel) +{ + CComboBox *combo = &m_CbnChannelMapping[channel]; + const LONG_PTR newChn = combo->GetItemData(combo->GetCurSel()); + if(newChn == -1) + { + return; + } + // Ensure that no channel is used twice + for(int mch = 0; mch < NUM_CHANNELCOMBOBOXES; mch++) // Host channels + { + if(mch != channel) + { + combo = &m_CbnChannelMapping[mch]; + if((int)combo->GetItemData(combo->GetCurSel()) == newChn) + { + // find an unused channel + bool found = false; + int deviceChannel = 0; + for(; deviceChannel < static_cast<int>(m_CurrentDeviceDynamicCaps.channelNames.size()); ++deviceChannel) + { + bool used = false; + for(int hostChannel = 0; hostChannel < NUM_CHANNELCOMBOBOXES; ++hostChannel) + { + if(static_cast<int>(m_CbnChannelMapping[hostChannel].GetItemData(m_CbnChannelMapping[hostChannel].GetCurSel())) == deviceChannel) + { + used = true; + break; + } + } + if(!used) + { + found = true; + break; + } + } + if(found) + { + combo->SetCurSel(deviceChannel+1); + } else + { + combo->SetCurSel(0); + } + break; + } + } + } + OnSettingsChanged(); +} + + +// Fill the dropdown box with a list of valid sample rates, depending on the selected sound device. +void COptionsSoundcard::UpdateSampleRates() +{ + { + GetDlgItem(IDC_STATIC_FORMAT)->EnableWindow(TRUE); + m_CbnMixingFreq.EnableWindow(TRUE); + } + + m_CbnMixingFreq.ResetContent(); + + std::vector<uint32> samplerates; + + if(IsDlgButtonChecked(IDC_CHECK4)) + { + samplerates = m_CurrentDeviceDynamicCaps.supportedExclusiveSampleRates; + } else + { + samplerates = m_CurrentDeviceDynamicCaps.supportedSampleRates; + } + + if(samplerates.empty()) + { + // We have no valid list of supported playback rates! Assume all rates supported by OpenMPT are possible... + samplerates = TrackerSettings::Instance().GetSampleRates(); + } + + int n = 0; + for(size_t i = 0; i < samplerates.size(); i++) + { + int pos = m_CbnMixingFreq.AddString(MPT_CFORMAT("{} Hz")(samplerates[i])); + m_CbnMixingFreq.SetItemData(pos, samplerates[i]); + if(m_Settings.Samplerate == samplerates[i]) + { + n = pos; + } + } + m_CbnMixingFreq.SetCurSel(n); + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + GetDlgItem(IDC_STATIC_FORMAT)->EnableWindow(FALSE); + m_CbnMixingFreq.EnableWindow(FALSE); + } +} + + +void COptionsSoundcard::UpdateControls() +{ + { + m_BtnDriverPanel.EnableWindow(TRUE); + GetDlgItem(IDC_CHECK4)->EnableWindow(TRUE); + GetDlgItem(IDC_CHECK5)->EnableWindow(TRUE); + GetDlgItem(IDC_CHECK9)->EnableWindow(TRUE); + GetDlgItem(IDC_STATIC_UPDATEINTERVAL)->EnableWindow(TRUE); + GetDlgItem(IDC_COMBO_UPDATEINTERVAL)->EnableWindow(TRUE); + } + if(!m_CurrentDeviceCaps.CanKeepDeviceRunning) + { + m_Settings.KeepDeviceRunning = false; + } + m_BtnDriverPanel.EnableWindow(m_CurrentDeviceCaps.CanDriverPanel ? TRUE : FALSE); + GetDlgItem(IDC_CHECK4)->EnableWindow(m_CurrentDeviceCaps.CanExclusiveMode ? TRUE : FALSE); + GetDlgItem(IDC_CHECK5)->EnableWindow(m_CurrentDeviceCaps.CanBoostThreadPriority ? TRUE : FALSE); + GetDlgItem(IDC_CHECK9)->EnableWindow(m_CurrentDeviceCaps.CanUseHardwareTiming ? TRUE : FALSE); + GetDlgItem(IDC_STATIC_UPDATEINTERVAL)->EnableWindow(m_CurrentDeviceCaps.CanUpdateInterval ? TRUE : FALSE); + GetDlgItem(IDC_COMBO_UPDATEINTERVAL)->EnableWindow(m_CurrentDeviceCaps.CanUpdateInterval ? TRUE : FALSE); + GetDlgItem(IDC_CHECK4)->SetWindowText(mpt::ToCString(m_CurrentDeviceCaps.ExclusiveModeDescription)); + CheckDlgButton(IDC_CHECK4, m_CurrentDeviceCaps.CanExclusiveMode && m_Settings.ExclusiveMode ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK5, m_CurrentDeviceCaps.CanBoostThreadPriority && m_Settings.BoostThreadPriority ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK9, m_CurrentDeviceCaps.CanUseHardwareTiming && m_Settings.UseHardwareTiming ? BST_CHECKED : BST_UNCHECKED); + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + m_BtnDriverPanel.EnableWindow(FALSE); + GetDlgItem(IDC_CHECK4)->EnableWindow(FALSE); + GetDlgItem(IDC_CHECK5)->EnableWindow(FALSE); + GetDlgItem(IDC_CHECK9)->EnableWindow(FALSE); + GetDlgItem(IDC_STATIC_UPDATEINTERVAL)->EnableWindow(FALSE); + GetDlgItem(IDC_COMBO_UPDATEINTERVAL)->EnableWindow(FALSE); + } +} + + +BOOL COptionsSoundcard::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_SOUNDCARD; + return CPropertyPage::OnSetActive(); +} + + +void COptionsSoundcard::OnOK() +{ + if(!theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + + // General + { + TrackerSettings::Instance().m_SoundSettingsOpenDeviceAtStartup = IsDlgButtonChecked(IDC_CHECK7) != BST_UNCHECKED; + } + m_Settings.ExclusiveMode = IsDlgButtonChecked(IDC_CHECK4) != BST_UNCHECKED; + m_Settings.BoostThreadPriority = IsDlgButtonChecked(IDC_CHECK5) != BST_UNCHECKED; + m_Settings.UseHardwareTiming = IsDlgButtonChecked(IDC_CHECK9) != BST_UNCHECKED; + // Mixing Freq + { + m_Settings.Samplerate = static_cast<uint32>(m_CbnMixingFreq.GetItemData(m_CbnMixingFreq.GetCurSel())); + } + // Channels + { + DWORD_PTR n = m_CbnChannels.GetItemData(m_CbnChannels.GetCurSel()); + m_Settings.Channels = static_cast<int>(n); + if((m_Settings.Channels != 1) && (m_Settings.Channels != 4)) + { + m_Settings.Channels = 2; + } + } + // SampleFormat + { + DWORD_PTR n = m_CbnSampleFormat.GetItemData(m_CbnSampleFormat.GetCurSel()); + m_Settings.sampleFormat = SampleFormat::FromInt(static_cast<int>(n)); + } + // Dither + { + m_Settings.DitherType = m_CbnDither.GetCurSel(); + } + // Latency + { + CString s; + m_CbnLatencyMS.GetWindowText(s); + m_Settings.Latency = ParseTime(s); + //Check given value. + if(m_Settings.Latency == 0.0) m_Settings.Latency = m_CurrentDeviceCaps.DefaultSettings.Latency; + m_Settings.Latency = Clamp(m_Settings.Latency, m_CurrentDeviceCaps.LatencyMin, m_CurrentDeviceCaps.LatencyMax); + m_CbnLatencyMS.SetWindowText(PrintTime(m_Settings.Latency)); + } + // Update Interval + { + CString s; + m_CbnUpdateIntervalMS.GetWindowText(s); + m_Settings.UpdateInterval = ParseTime(s); + //Check given value. + if(m_Settings.UpdateInterval == 0.0) m_Settings.UpdateInterval = m_CurrentDeviceCaps.DefaultSettings.UpdateInterval; + m_Settings.UpdateInterval = Clamp(m_Settings.UpdateInterval, m_CurrentDeviceCaps.UpdateIntervalMin, m_CurrentDeviceCaps.UpdateIntervalMax); + m_CbnUpdateIntervalMS.SetWindowText(PrintTime(m_Settings.UpdateInterval)); + } + // Channel Mapping + { + if(m_CurrentDeviceCaps.CanChannelMapping) + { + int numChannels = std::min(static_cast<int>(m_Settings.Channels), static_cast<int>(NUM_CHANNELCOMBOBOXES)); + std::vector<int32> channels(numChannels); + for(int mch = 0; mch < numChannels; mch++) // Host channels + { + CComboBox *combo = &m_CbnChannelMapping[mch]; + channels[mch] = static_cast<int32>(combo->GetItemData(combo->GetCurSel())); + } + m_Settings.Channels = channels; + } + } + // Recording + { + if(TrackerSettings::Instance().m_SoundShowRecordingSettings && m_CurrentDeviceCaps.CanInput && ((m_CurrentDeviceCaps.HasNamedInputSources && m_CurrentDeviceDynamicCaps.inputSourceNames.size() > 0) || !m_CurrentDeviceCaps.HasNamedInputSources)) + { + DWORD_PTR n = m_CbnRecordingChannels.GetItemData(m_CbnRecordingChannels.GetCurSel()); + m_Settings.InputChannels = static_cast<uint8>(n); + if((m_Settings.InputChannels != 1) && (m_Settings.InputChannels != 2) && (m_Settings.InputChannels != 4)) + { + m_Settings.InputChannels = 0; + } + if(m_CurrentDeviceCaps.HasNamedInputSources) + { + DWORD_PTR sourceID = m_CbnRecordingSource.GetItemData(m_CbnRecordingSource.GetCurSel()); + m_Settings.InputSourceID = static_cast<uint32>(sourceID); + } else + { + m_Settings.InputSourceID = 0; + } + } else + { + m_Settings.InputChannels = 0; + m_Settings.InputSourceID = 0; + } + } + CMainFrame::GetMainFrame()->SetupSoundCard(m_Settings, m_CurrentDeviceInfo.GetIdentifier(), (SoundDeviceStopMode)m_CbnStoppedMode.GetCurSel()); + SetDevice(m_CurrentDeviceInfo.GetIdentifier(), true); // Poll changed ASIO sample format and channel names + UpdateDevice(); + UpdateStatistics(); + + } else + { + + Reporting::Error("Sound card currently not available."); + + } + + CPropertyPage::OnOK(); +} + + +void COptionsSoundcard::UpdateStatistics() +{ + if (!m_EditStatistics) return; + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm->gpSoundDevice && pMainFrm->IsPlaying()) + { + const SoundDevice::BufferAttributes bufferAttributes = pMainFrm->gpSoundDevice->GetEffectiveBufferAttributes(); + const SoundDevice::Statistics stats = pMainFrm->gpSoundDevice->GetStatistics(); +#if 0 + const SoundDevice::TimeInfo timeInfo = pMainFrm->gpSoundDevice->GetTimeInfo(); + const SoundDevice::StreamPosition streamPosition = pMainFrm->gpSoundDevice->GetStreamPosition(); +#endif + const uint32 samplerate = pMainFrm->gpSoundDevice->GetSettings().Samplerate; + mpt::ustring s; + if(bufferAttributes.NumBuffers > 2) + { + s += MPT_UFORMAT("Buffer: {}% ({}/{})\r\n")((bufferAttributes.Latency > 0.0) ? mpt::saturate_round<int64>(stats.InstantaneousLatency / bufferAttributes.Latency * 100.0) : 0, (stats.LastUpdateInterval > 0.0) ? mpt::saturate_round<int64>(bufferAttributes.Latency / stats.LastUpdateInterval) : 0, bufferAttributes.NumBuffers); + } else + { + s += MPT_UFORMAT("Buffer: {}%\r\n")((bufferAttributes.Latency > 0.0) ? mpt::saturate_round<int64>(stats.InstantaneousLatency / bufferAttributes.Latency * 100.0) : 0); + } + s += MPT_UFORMAT("Latency: {} ms (current: {} ms, {} frames)\r\n")(mpt::ufmt::fix(bufferAttributes.Latency * 1000.0, 1), mpt::ufmt::fix(stats.InstantaneousLatency * 1000.0, 1), mpt::saturate_round<int64>(stats.InstantaneousLatency * samplerate)); + s += MPT_UFORMAT("Period: {} ms (current: {} ms, {} frames)\r\n")(mpt::ufmt::fix(bufferAttributes.UpdateInterval * 1000.0, 1), mpt::ufmt::fix(stats.LastUpdateInterval * 1000.0, 1), mpt::saturate_round<int64>(stats.LastUpdateInterval * samplerate)); +#if 0 + s += MPT_UFORMAT("TimeInfo: latency = {} ms / speed = {} / latency = {} ms\r\n")( + mpt::ufmt::fix(timeInfo.Latency * 1000.0, 1), + mpt::ufmt::flt(timeInfo.Speed, 4), + mpt::ufmt::fix((timeInfo.RenderStreamPositionBefore.Seconds - streamPosition.Seconds) * 1000.0, 1)); +#endif + s += stats.text; + m_EditStatistics.SetWindowText(mpt::ToCString(s)); + } else + { + if(theApp.GetSoundDevicesManager()->IsDeviceUnavailable(m_CurrentDeviceInfo.GetIdentifier())) + { + m_EditStatistics.SetWindowText(_T("Device currently unavailable.")); + } else + { + m_EditStatistics.SetWindowText(_T("")); + } + } +} + + +////////////////// +// COptionsMixer + +BEGIN_MESSAGE_MAP(COptionsMixer, CPropertyPage) + ON_WM_HSCROLL() + ON_WM_VSCROLL() + ON_CBN_SELCHANGE(IDC_COMBO_FILTER, &COptionsMixer::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO_AMIGA_TYPE, &COptionsMixer::OnSettingsChanged) + ON_EN_UPDATE(IDC_RAMPING_IN, &COptionsMixer::OnRampingChanged) + ON_EN_UPDATE(IDC_RAMPING_OUT, &COptionsMixer::OnRampingChanged) + ON_COMMAND(IDC_CHECK_SOFTPAN, &COptionsMixer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK1, &COptionsMixer::OnAmigaChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsMixer::OnDefaultRampSettings) +END_MESSAGE_MAP() + + +void COptionsMixer::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsSoundcard) + DDX_Control(pDX, IDC_COMBO_FILTER, m_CbnResampling); + DDX_Control(pDX, IDC_COMBO_AMIGA_TYPE, m_CbnAmigaType); + DDX_Control(pDX, IDC_RAMPING_IN, m_CEditRampUp); + DDX_Control(pDX, IDC_RAMPING_OUT, m_CEditRampDown); + DDX_Control(pDX, IDC_EDIT_VOLRAMP_SAMPLES_UP, m_CInfoRampUp); + DDX_Control(pDX, IDC_EDIT_VOLRAMP_SAMPLES_DOWN, m_CInfoRampDown); + DDX_Control(pDX, IDC_SLIDER_STEREOSEP, m_SliderStereoSep); + // check box soft pan + DDX_Control(pDX, IDC_SLIDER_PREAMP, m_SliderPreAmp); + //}}AFX_DATA_MAP +} + + +BOOL COptionsMixer::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + // Resampling type + { + const auto resamplingModes = Resampling::AllModes(); + for(auto mode : resamplingModes) + { + int index = m_CbnResampling.AddString(CTrackApp::GetResamplingModeName(mode, 2, true)); + m_CbnResampling.SetItemData(index, mode); + if(TrackerSettings::Instance().ResamplerMode == mode) + { + m_CbnResampling.SetCurSel(index); + } + } + } + + // Amiga Resampler + const bool enableAmigaResampler = TrackerSettings::Instance().ResamplerEmulateAmiga != Resampling::AmigaFilter::Off; + CheckDlgButton(IDC_CHECK1, enableAmigaResampler ? BST_CHECKED : BST_UNCHECKED); + m_CbnAmigaType.EnableWindow(enableAmigaResampler ? TRUE : FALSE); + static constexpr std::pair<const TCHAR *, Resampling::AmigaFilter> Filters[] = + { + {_T("A500 Filter"), Resampling::AmigaFilter::A500}, + {_T("A1200 Filter"), Resampling::AmigaFilter::A1200}, + {_T("Unfiltered"), Resampling::AmigaFilter::Unfiltered}, + }; + int sel = 0; + for(const auto & [name, filter] : Filters) + { + const int item = m_CbnAmigaType.AddString(name); + m_CbnAmigaType.SetItemData(item, static_cast<DWORD_PTR>(filter)); + if(filter == TrackerSettings::Instance().ResamplerEmulateAmiga) + sel = item; + } + m_CbnAmigaType.SetCurSel(sel); + + // volume ramping + { + m_CEditRampUp.SetWindowText(mpt::ToCString(mpt::ufmt::val(TrackerSettings::Instance().GetMixerSettings().GetVolumeRampUpMicroseconds()))); + m_CEditRampDown.SetWindowText(mpt::ToCString(mpt::ufmt::val(TrackerSettings::Instance().GetMixerSettings().GetVolumeRampDownMicroseconds()))); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN2))->SetRange32(0, int32_max); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN3))->SetRange32(0, int32_max); + UpdateRamping(); + } + + // Stereo Separation + { + m_SliderStereoSep.SetRange(0, 32); + m_SliderStereoSep.SetPos(16); + for (int n = 0; n <= 32; n++) + { + if ((int)TrackerSettings::Instance().MixerStereoSeparation <= 8 * n) + { + m_SliderStereoSep.SetPos(n); + break; + } + } + UpdateStereoSep(); + } + + // soft pan + { + CheckDlgButton(IDC_CHECK_SOFTPAN, (TrackerSettings::Instance().MixerFlags & SNDMIX_SOFTPANNING) ? BST_CHECKED : BST_UNCHECKED); + } + + // Pre-Amplification + { + m_SliderPreAmp.SetTicFreq(5); + m_SliderPreAmp.SetRange(0, 40); + int n = (TrackerSettings::Instance().MixerPreAmp - 64) / 8; + if ((n < 0) || (n > 40)) n = 16; + m_SliderPreAmp.SetPos(n); + } + + m_initialized = true; + + return TRUE; +} + + +BOOL COptionsMixer::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_MIXER; + return CPropertyPage::OnSetActive(); +} + + +void COptionsMixer::OnAmigaChanged() +{ + const bool enableAmigaResampler = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + m_CbnAmigaType.EnableWindow(enableAmigaResampler ? TRUE : FALSE); + OnSettingsChanged(); +} + + +void COptionsMixer::OnRampingChanged() +{ + if(!m_initialized) + return; + UpdateRamping(); + OnSettingsChanged(); +} + + +void COptionsMixer::OnDefaultRampSettings() +{ + m_CEditRampUp.SetWindowText(mpt::ToCString(mpt::ufmt::val(MixerSettings().GetVolumeRampUpMicroseconds()))); + m_CEditRampDown.SetWindowText(mpt::ToCString(mpt::ufmt::val(MixerSettings().GetVolumeRampDownMicroseconds()))); + OnRampingChanged(); +} + + +void COptionsMixer::OnHScroll(UINT n, UINT pos, CScrollBar *p) +{ + CPropertyPage::OnHScroll(n, pos, p); + if(p == (CScrollBar *)&m_SliderStereoSep) + { + UpdateStereoSep(); + OnSettingsChanged(); + } +} + + +void COptionsMixer::UpdateRamping() +{ + MixerSettings settings = TrackerSettings::Instance().GetMixerSettings(); + CString s; + m_CEditRampUp.GetWindowText(s); + settings.SetVolumeRampUpMicroseconds(ConvertStrTo<int32>(s)); + m_CEditRampDown.GetWindowText(s); + settings.SetVolumeRampDownMicroseconds(ConvertStrTo<int32>(s)); + s.Format(_T("%i samples at %i Hz"), (int)settings.GetVolumeRampUpSamples(), (int)settings.gdwMixingFreq); + m_CInfoRampUp.SetWindowText(s); + s.Format(_T("%i samples at %i Hz"), (int)settings.GetVolumeRampDownSamples(), (int)settings.gdwMixingFreq); + m_CInfoRampDown.SetWindowText(s); +} + + +void COptionsMixer::UpdateStereoSep() +{ + CString s; + s.Format(_T("%d%%"), ((8 * m_SliderStereoSep.GetPos()) * 100) / 128); + SetDlgItemText(IDC_TEXT_STEREOSEP, s); +} + + +void COptionsMixer::OnOK() +{ + // resampler mode + { + TrackerSettings::Instance().ResamplerMode = static_cast<ResamplingMode>(m_CbnResampling.GetItemData(m_CbnResampling.GetCurSel())); + } + + // Amiga Resampler + if(IsDlgButtonChecked(IDC_CHECK1) == BST_UNCHECKED) + TrackerSettings::Instance().ResamplerEmulateAmiga = Resampling::AmigaFilter::Off; + else + TrackerSettings::Instance().ResamplerEmulateAmiga = static_cast<Resampling::AmigaFilter>(m_CbnAmigaType.GetItemData(m_CbnAmigaType.GetCurSel())); + + // volume ramping + { + MixerSettings settings = TrackerSettings::Instance().GetMixerSettings(); + CString s; + m_CEditRampUp.GetWindowText(s); + settings.SetVolumeRampUpMicroseconds(ConvertStrTo<int>(s)); + m_CEditRampDown.GetWindowText(s); + settings.SetVolumeRampDownMicroseconds(ConvertStrTo<int>(s)); + TrackerSettings::Instance().SetMixerSettings(settings); + } + + // stereo sep + { + TrackerSettings::Instance().MixerStereoSeparation = 8 * m_SliderStereoSep.GetPos(); + } + + // soft pan + { + if(IsDlgButtonChecked(IDC_CHECK_SOFTPAN)) + { + TrackerSettings::Instance().MixerFlags = TrackerSettings::Instance().MixerFlags | SNDMIX_SOFTPANNING; + } else + { + TrackerSettings::Instance().MixerFlags = TrackerSettings::Instance().MixerFlags & ~SNDMIX_SOFTPANNING; + } + } + + // pre amp + { + int n = m_SliderPreAmp.GetPos(); + if ((n >= 0) && (n <= 40)) // approximately +/- 10dB + { + TrackerSettings::Instance().MixerPreAmp = 64 + (n * 8); + } + } + + CMainFrame::GetMainFrame()->SetupPlayer(); + CMainFrame::GetMainFrame()->PostMessage(WM_MOD_INVALIDATEPATTERNS, HINT_MPTOPTIONS); + + CPropertyPage::OnOK(); +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// CEQSavePresetDlg +// + +#ifndef NO_EQ + +class CEQSavePresetDlg: public CDialog +{ +protected: + EQPreset &m_EQ; + +public: + CEQSavePresetDlg(EQPreset &eq, CWnd *parent = nullptr) : CDialog(IDD_SAVEPRESET, parent), m_EQ(eq) { } + BOOL OnInitDialog(); + void OnOK(); +}; + + +BOOL CEQSavePresetDlg::OnInitDialog() +{ + CComboBox *pCombo = (CComboBox *)GetDlgItem(IDC_COMBO1); + if (pCombo) + { + int ndx = 0; + for (UINT i=0; i<4; i++) + { + int n = pCombo->AddString(mpt::ToCString(mpt::Charset::Locale, TrackerSettings::Instance().m_EqUserPresets[i].szName)); + pCombo->SetItemData( n, i); + if (!lstrcmpiA(TrackerSettings::Instance().m_EqUserPresets[i].szName, m_EQ.szName)) ndx = n; + } + pCombo->SetCurSel(ndx); + } + SetDlgItemText(IDC_EDIT1, mpt::ToCString(mpt::Charset::Locale, m_EQ.szName)); + return TRUE; +} + + +void CEQSavePresetDlg::OnOK() +{ + CComboBox *pCombo = (CComboBox *)GetDlgItem(IDC_COMBO1); + if (pCombo) + { + int n = pCombo->GetCurSel(); + if ((n < 0) || (n >= 4)) n = 0; + CString s; + GetDlgItemText(IDC_EDIT1, s); + mpt::String::WriteAutoBuf(m_EQ.szName) = mpt::ToCharset(mpt::Charset::Locale, s); + TrackerSettings::Instance().m_EqUserPresets[n] = m_EQ; + } + CDialog::OnOK(); +} + + +void CEQSlider::Init(UINT nID, UINT n, CWnd *parent) +{ + m_nSliderNo = n; + m_pParent = parent; + SubclassDlgItem(nID, parent); +} + + +BOOL CEQSlider::PreTranslateMessage(MSG *pMsg) +{ + if ((pMsg) && (pMsg->message == WM_RBUTTONDOWN) && (m_pParent)) + { + m_x = LOWORD(pMsg->lParam); + m_y = HIWORD(pMsg->lParam); + m_pParent->PostMessage(WM_COMMAND, ID_EQSLIDER_BASE+m_nSliderNo, 0); + } + return CSliderCtrl::PreTranslateMessage(pMsg); +} + +#endif // !NO_EQ + + +////////////////////////////////////////////////////////// +// COptionsPlayer - DSP / EQ settings + + +#ifndef NO_EQ +#define EQ_MAX_FREQS 5 + +const UINT gEqBandFreqs[MAX_EQ_BANDS][EQ_MAX_FREQS] = +{ + { 100, 125, 150, 200, 250 }, + { 300, 350, 400, 450, 500 }, + { 600, 700, 800, 900, 1000 }, + { 1250, 1500, 1750, 2000, 2500 }, + { 3000, 3500, 4000, 4500, 5000 }, + { 6000, 7000, 8000, 9000, 10000 }, +}; +#endif // !NO_EQ + +BEGIN_MESSAGE_MAP(COptionsPlayer, CPropertyPage) +#ifndef NO_EQ + // EQ + ON_WM_VSCROLL() + ON_COMMAND(IDC_CHECK3, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON1, &COptionsPlayer::OnEqUser1) + ON_COMMAND(IDC_BUTTON2, &COptionsPlayer::OnEqUser2) + ON_COMMAND(IDC_BUTTON3, &COptionsPlayer::OnEqUser3) + ON_COMMAND(IDC_BUTTON4, &COptionsPlayer::OnEqUser4) + ON_COMMAND(IDC_BUTTON5, &COptionsPlayer::OnSavePreset) + ON_COMMAND_RANGE(ID_EQSLIDER_BASE, ID_EQSLIDER_BASE + MAX_EQ_BANDS, &COptionsPlayer::OnSliderMenu) + ON_COMMAND_RANGE(ID_EQMENU_BASE, ID_EQMENU_BASE + EQ_MAX_FREQS, &COptionsPlayer::OnSliderFreq) +#endif // !NO_EQ + + // DSP + ON_WM_HSCROLL() + ON_CBN_SELCHANGE(IDC_COMBO2, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK1, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK2, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK4, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK5, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK6, &COptionsPlayer::OnSettingsChanged) + ON_COMMAND(IDC_CHECK7, &COptionsPlayer::OnSettingsChanged) +END_MESSAGE_MAP() + + +void COptionsPlayer::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsPlayer) + DDX_Control(pDX, IDC_COMBO2, m_CbnReverbPreset); + DDX_Control(pDX, IDC_SLIDER1, m_SbXBassDepth); + DDX_Control(pDX, IDC_SLIDER2, m_SbXBassRange); + DDX_Control(pDX, IDC_SLIDER3, m_SbReverbDepth); + DDX_Control(pDX, IDC_SLIDER5, m_SbSurroundDepth); + DDX_Control(pDX, IDC_SLIDER6, m_SbSurroundDelay); + DDX_Control(pDX, IDC_SLIDER4, m_SbBitCrushBits); + //}}AFX_DATA_MAP +} + + +BOOL COptionsPlayer::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + uint32 dwQuality = TrackerSettings::Instance().MixerDSPMask; + +#ifndef NO_EQ + for (UINT i = 0; i < MAX_EQ_BANDS; i++) + { + m_Sliders[i].Init(IDC_SLIDER7 + i, i, this); + m_Sliders[i].SetRange(0, 32); + m_Sliders[i].SetTicFreq(4); + } + + UpdateDialog(); + + if (dwQuality & SNDDSP_EQ) CheckDlgButton(IDC_CHECK3, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK3)->EnableWindow(FALSE); +#endif + + // Effects +#ifndef NO_DSP + if (dwQuality & SNDDSP_MEGABASS) CheckDlgButton(IDC_CHECK1, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK1)->EnableWindow(FALSE); +#endif +#ifndef NO_AGC + if (dwQuality & SNDDSP_AGC) CheckDlgButton(IDC_CHECK2, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK2)->EnableWindow(FALSE); +#endif +#ifndef NO_DSP + if (dwQuality & SNDDSP_SURROUND) CheckDlgButton(IDC_CHECK4, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK4)->EnableWindow(FALSE); +#endif +#ifndef NO_DSP + if (dwQuality & SNDDSP_BITCRUSH) CheckDlgButton(IDC_CHECK5, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK5)->EnableWindow(FALSE); +#endif + +#ifndef NO_DSP + m_SbBitCrushBits.SetRange(1, 24); + m_SbBitCrushBits.SetPos(TrackerSettings::Instance().m_BitCrushSettings.m_Bits); +#else + m_SbBitCurshBits.EnableWindow(FALSE); +#endif + +#ifndef NO_DSP + // Bass Expansion + m_SbXBassDepth.SetRange(0,4); + m_SbXBassDepth.SetPos(8-TrackerSettings::Instance().m_MegaBassSettings.m_nXBassDepth); + m_SbXBassRange.SetRange(0,4); + m_SbXBassRange.SetPos(4 - (TrackerSettings::Instance().m_MegaBassSettings.m_nXBassRange - 1) / 5); +#else + m_SbXBassDepth.EnableWindow(FALSE); + m_SbXBassRange.EnableWindow(FALSE); +#endif + +#ifndef NO_REVERB + // Reverb + m_SbReverbDepth.SetRange(1, 16); + m_SbReverbDepth.SetPos(TrackerSettings::Instance().m_ReverbSettings.m_nReverbDepth); + UINT nSel = 0; + for (UINT iRvb=0; iRvb<NUM_REVERBTYPES; iRvb++) + { + CString pszName = mpt::ToCString(GetReverbPresetName(iRvb)); + if(!pszName.IsEmpty()) + { + UINT n = m_CbnReverbPreset.AddString(pszName); + m_CbnReverbPreset.SetItemData(n, iRvb); + if (iRvb == TrackerSettings::Instance().m_ReverbSettings.m_nReverbType) nSel = n; + } + } + m_CbnReverbPreset.SetCurSel(nSel); + if (dwQuality & SNDDSP_REVERB) CheckDlgButton(IDC_CHECK6, BST_CHECKED); +#else + GetDlgItem(IDC_CHECK6)->EnableWindow(FALSE); + m_SbReverbDepth.EnableWindow(FALSE); + m_CbnReverbPreset.EnableWindow(FALSE); +#endif + +#ifndef NO_DSP + // Surround + { + UINT n = TrackerSettings::Instance().m_SurroundSettings.m_nProLogicDepth; + if (n < 1) n = 1; + if (n > 16) n = 16; + m_SbSurroundDepth.SetRange(1, 16); + m_SbSurroundDepth.SetPos(n); + m_SbSurroundDelay.SetRange(0, 8); + m_SbSurroundDelay.SetPos((TrackerSettings::Instance().m_SurroundSettings.m_nProLogicDelay-5)/5); + } +#else + m_SbSurroundDepth.EnableWindow(FALSE); + m_SbSurroundDelay.EnableWindow(FALSE); +#endif + + return TRUE; +} + + +BOOL COptionsPlayer::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_PLAYER; + + SetDlgItemText(IDC_EQ_WARNING, + _T("Note: This EQ is applied to any and all of the modules ") + _T("that you load in OpenMPT; its settings are stored globally, ") + _T("rather than in each file. This means that you should avoid ") + _T("using it as part of your production process, and instead only ") + _T("use it to correct deficiencies in your audio hardware.")); + + return CPropertyPage::OnSetActive(); +} + + +void COptionsPlayer::OnHScroll(UINT nSBCode, UINT, CScrollBar *psb) +{ + if (nSBCode == SB_ENDSCROLL) return; + if ((psb) && (psb->m_hWnd == m_SbReverbDepth.m_hWnd)) + { +#ifndef NO_REVERB + UINT n = m_SbReverbDepth.GetPos(); + if ((n) && (n <= 16)) TrackerSettings::Instance().m_ReverbSettings.m_nReverbDepth = n; + //if ((n) && (n <= 16)) CSoundFile::m_Reverb.m_Settings.m_nReverbDepth = n; + CMainFrame::GetMainFrame()->SetupPlayer(); +#endif + } else + { + OnSettingsChanged(); + } +} + + +void COptionsPlayer::OnOK() +{ + DWORD dwQuality = 0; + + DWORD dwQualityMask = 0; + +#ifndef NO_DSP + dwQualityMask |= SNDDSP_MEGABASS; + if (IsDlgButtonChecked(IDC_CHECK1)) dwQuality |= SNDDSP_MEGABASS; +#endif +#ifndef NO_AGC + dwQualityMask |= SNDDSP_AGC; + if (IsDlgButtonChecked(IDC_CHECK2)) dwQuality |= SNDDSP_AGC; +#endif +#ifndef NO_EQ + dwQualityMask |= SNDDSP_EQ; + if (IsDlgButtonChecked(IDC_CHECK3)) dwQuality |= SNDDSP_EQ; +#endif +#ifndef NO_DSP + dwQualityMask |= SNDDSP_SURROUND; + if (IsDlgButtonChecked(IDC_CHECK4)) dwQuality |= SNDDSP_SURROUND; +#endif +#ifndef NO_REVERB + dwQualityMask |= SNDDSP_REVERB; + if (IsDlgButtonChecked(IDC_CHECK6)) dwQuality |= SNDDSP_REVERB; +#endif +#ifndef NO_DSP + dwQualityMask |= SNDDSP_BITCRUSH; + if (IsDlgButtonChecked(IDC_CHECK5)) dwQuality |= SNDDSP_BITCRUSH; +#endif + +#ifndef NO_DSP + { + TrackerSettings::Instance().m_BitCrushSettings.m_Bits = m_SbBitCrushBits.GetPos(); + } +#endif + +#ifndef NO_DSP + // Bass Expansion + { + UINT nXBassDepth = 8-m_SbXBassDepth.GetPos(); + if (nXBassDepth < 4) nXBassDepth = 4; + if (nXBassDepth > 8) nXBassDepth = 8; + UINT nXBassRange = (4-m_SbXBassRange.GetPos()) * 5 + 1; + if (nXBassRange < 5) nXBassRange = 5; + if (nXBassRange > 21) nXBassRange = 21; + TrackerSettings::Instance().m_MegaBassSettings.m_nXBassDepth = nXBassDepth; + TrackerSettings::Instance().m_MegaBassSettings.m_nXBassRange = nXBassRange; + } +#endif +#ifndef NO_REVERB + // Reverb + { + // Reverb depth is dynamically changed + uint32 nReverbType = static_cast<uint32>(m_CbnReverbPreset.GetItemData(m_CbnReverbPreset.GetCurSel())); + if (nReverbType < NUM_REVERBTYPES) TrackerSettings::Instance().m_ReverbSettings.m_nReverbType = nReverbType; + } +#endif +#ifndef NO_DSP + // Surround + { + UINT nProLogicDepth = m_SbSurroundDepth.GetPos(); + UINT nProLogicDelay = 5 + (m_SbSurroundDelay.GetPos() * 5); + TrackerSettings::Instance().m_SurroundSettings.m_nProLogicDepth = nProLogicDepth; + TrackerSettings::Instance().m_SurroundSettings.m_nProLogicDelay = nProLogicDelay; + } +#endif + + TrackerSettings::Instance().MixerDSPMask = dwQuality; + + CMainFrame::GetMainFrame()->SetupPlayer(); + CPropertyPage::OnOK(); +} + + +#ifndef NO_EQ + +void COptionsPlayer::UpdateEQ(bool bReset) +{ + CriticalSection cs; + if(CMainFrame::GetMainFrame()->GetSoundFilePlaying()) + CMainFrame::GetMainFrame()->GetSoundFilePlaying()->SetEQGains(m_EQPreset.Gains, m_EQPreset.Freqs, bReset); +} + + +void COptionsPlayer::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) +{ + CDialog::OnVScroll(nSBCode, nPos, pScrollBar); + for (UINT i=0; i<MAX_EQ_BANDS; i++) + { + int n = 32 - m_Sliders[i].GetPos(); + if ((n >= 0) && (n <= 32)) m_EQPreset.Gains[i] = n; + } + UpdateEQ(FALSE); +} + + +void COptionsPlayer::LoadEQPreset(const EQPreset &preset) +{ + m_EQPreset = preset; + UpdateEQ(TRUE); + UpdateDialog(); +} + + +void COptionsPlayer::OnSavePreset() +{ + CEQSavePresetDlg dlg(m_EQPreset, this); + if (dlg.DoModal() == IDOK) + { + UpdateDialog(); + } +} + + +static CString f2s(UINT f) +{ + if (f < 1000) + { + return MPT_CFORMAT("{}Hz")(f); + } else + { + UINT fHi = f / 1000u; + UINT fLo = f % 1000u; + if(fLo) + { + return MPT_CFORMAT("{}.{}kHz")(fHi, mpt::cfmt::dec0<1>(fLo/100)); + } else + { + return MPT_CFORMAT("{}kHz")(fHi); + } + } +} + + +void COptionsPlayer::UpdateDialog() +{ + for (UINT i=0; i<MAX_EQ_BANDS; i++) + { + int n = 32 - m_EQPreset.Gains[i]; + if (n < 0) n = 0; + if (n > 32) n = 32; + if (n != (m_Sliders[i].GetPos() & 0xFFFF)) m_Sliders[i].SetPos(n); + SetDlgItemText(IDC_TEXT1 + i, f2s(m_EQPreset.Freqs[i])); + } + for(unsigned int i = 0; i < std::size(TrackerSettings::Instance().m_EqUserPresets); i++) + { + SetDlgItemText(IDC_BUTTON1 + i, mpt::ToCString(mpt::Charset::Locale, TrackerSettings::Instance().m_EqUserPresets[i].szName)); + } +} + + +void COptionsPlayer::OnSliderMenu(UINT nID) +{ + UINT n = nID - ID_EQSLIDER_BASE; + if (n < MAX_EQ_BANDS) + { + HMENU hMenu = ::CreatePopupMenu(); + m_nSliderMenu = n; + if (!hMenu) return; + const UINT *pFreqs = gEqBandFreqs[m_nSliderMenu]; + for (UINT i = 0; i < EQ_MAX_FREQS; i++) + { + DWORD d = MF_STRING; + if (m_EQPreset.Freqs[m_nSliderMenu] == pFreqs[i]) d |= MF_CHECKED; + ::AppendMenu(hMenu, d, ID_EQMENU_BASE+i, f2s(pFreqs[i])); + } + CPoint pt(m_Sliders[m_nSliderMenu].m_x, m_Sliders[m_nSliderMenu].m_y); + m_Sliders[m_nSliderMenu].ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + } +} + + +void COptionsPlayer::OnSliderFreq(UINT nID) +{ + UINT n = nID - ID_EQMENU_BASE; + if ((m_nSliderMenu < MAX_EQ_BANDS) && (n < EQ_MAX_FREQS)) + { + UINT f = gEqBandFreqs[m_nSliderMenu][n]; + if (f != m_EQPreset.Freqs[m_nSliderMenu]) + { + m_EQPreset.Freqs[m_nSliderMenu] = f; + UpdateEQ(TRUE); + UpdateDialog(); + } + } +} + +#endif // !NO_EQ + + +///////////////////////////////////////////////////////////// +// CMidiSetupDlg + +BEGIN_MESSAGE_MAP(CMidiSetupDlg, CPropertyPage) + ON_CBN_SELCHANGE(IDC_COMBO1, &CMidiSetupDlg::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CMidiSetupDlg::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON1, &CMidiSetupDlg::OnRenameDevice) + ON_COMMAND(IDC_CHECK1, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK2, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK3, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK4, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK5, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_MIDI_TO_PLUGIN, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_MIDI_MACRO_CONTROL, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_MIDIVOL_TO_NOTEVOL, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_MIDIPLAYCONTROL, &CMidiSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_MIDIPLAYPATTERNONMIDIIN, &CMidiSetupDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_EDIT1, &CMidiSetupDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_EDIT2, &CMidiSetupDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_EDIT3, &CMidiSetupDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_EDIT4, &CMidiSetupDlg::OnSettingsChanged) +END_MESSAGE_MAP() + + +void CMidiSetupDlg::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsSoundcard) + DDX_Control(pDX, IDC_SPIN1, m_SpinSpd); + DDX_Control(pDX, IDC_SPIN2, m_SpinPat); + DDX_Control(pDX, IDC_SPIN3, m_SpinAmp); + DDX_Control(pDX, IDC_COMBO1, m_InputDevice); + DDX_Control(pDX, IDC_COMBO2, m_ATBehaviour); + DDX_Control(pDX, IDC_COMBO3, m_Quantize); + //}}AFX_DATA_MAP +} + + +BOOL CMidiSetupDlg::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + // Flags + if (m_dwMidiSetup & MIDISETUP_RECORDVELOCITY) CheckDlgButton(IDC_CHECK1, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_RECORDNOTEOFF) CheckDlgButton(IDC_CHECK2, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_ENABLE_RECORD_DEFAULT) CheckDlgButton(IDC_CHECK3, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_TRANSPOSEKEYBOARD) CheckDlgButton(IDC_CHECK4, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_MIDITOPLUG) CheckDlgButton(IDC_MIDI_TO_PLUGIN, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_MIDIMACROCONTROL) CheckDlgButton(IDC_MIDI_MACRO_CONTROL, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_MIDIVOL_TO_NOTEVOL) CheckDlgButton(IDC_MIDIVOL_TO_NOTEVOL, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_RESPONDTOPLAYCONTROLMSGS) CheckDlgButton(IDC_MIDIPLAYCONTROL, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN) CheckDlgButton(IDC_MIDIPLAYPATTERNONMIDIIN, BST_CHECKED); + if (m_dwMidiSetup & MIDISETUP_MIDIMACROPITCHBEND) CheckDlgButton(IDC_CHECK5, BST_CHECKED); + + // Midi In Device + RefreshDeviceList(m_nMidiDevice); + + // Aftertouch behaviour + m_ATBehaviour.ResetContent(); + static constexpr std::pair<const TCHAR *, RecordAftertouchOptions> aftertouchOptions[] = + { + { _T("Do not record Aftertouch"), atDoNotRecord }, + { _T("Record as Volume Commands"), atRecordAsVolume }, + { _T("Record as MIDI Macros"), atRecordAsMacro }, + }; + + for(const auto & [str, value] : aftertouchOptions) + { + int item = m_ATBehaviour.AddString(str); + m_ATBehaviour.SetItemData(item, value); + if(value == TrackerSettings::Instance().aftertouchBehaviour) + { + m_ATBehaviour.SetCurSel(item); + } + } + + // Note Velocity amp + SetDlgItemInt(IDC_EDIT3, TrackerSettings::Instance().midiVelocityAmp); + m_SpinAmp.SetRange(1, 10000); + + SetDlgItemText(IDC_EDIT4, mpt::ToCString(IgnoredCCsToString(TrackerSettings::Instance().midiIgnoreCCs))); + + // Midi Import settings + SetDlgItemInt(IDC_EDIT1, TrackerSettings::Instance().midiImportTicks); + SetDlgItemInt(IDC_EDIT2, TrackerSettings::Instance().midiImportPatternLen); + + // Note quantization + m_Quantize.ResetContent(); + static constexpr std::pair<const TCHAR *, uint32> quantizeOptions[] = + { + { _T("1/4th Notes"), 4 }, { _T("1/6th Notes"), 6 }, + { _T("1/8th Notes"), 8 }, { _T("1/12th Notes"), 12 }, + { _T("1/16th Notes"), 16 }, { _T("1/24th Notes"), 24 }, + { _T("1/32nd Notes"), 32 }, { _T("1/48th Notes"), 48 }, + { _T("1/64th Notes"), 64 }, { _T("1/96th Notes"), 96 }, + }; + + for(const auto & [str, value]: quantizeOptions) + { + int item = m_Quantize.AddString(str); + m_Quantize.SetItemData(item, value); + if(value == TrackerSettings::Instance().midiImportQuantize) + { + m_Quantize.SetCurSel(item); + } + } + m_SpinSpd.SetRange(2, 16); + m_SpinPat.SetRange(1, MAX_PATTERN_ROWS); + return TRUE; +} + + +void CMidiSetupDlg::RefreshDeviceList(UINT currentDevice) +{ + m_InputDevice.SetRedraw(FALSE); + m_InputDevice.ResetContent(); + UINT ndevs = midiInGetNumDevs(); + for(UINT i = 0; i < ndevs; i++) + { + MIDIINCAPS mic; + mic.szPname[0] = 0; + if(midiInGetDevCaps(i, &mic, sizeof(mic)) == MMSYSERR_NOERROR) + { + int item = m_InputDevice.AddString(theApp.GetFriendlyMIDIPortName(mpt::ToCString(mpt::String::ReadWinBuf(mic.szPname)), true)); + m_InputDevice.SetItemData(item, i); + if(i == currentDevice) + { + m_InputDevice.SetCurSel(item); + } + } + } + m_InputDevice.SetRedraw(TRUE); + m_InputDevice.Invalidate(FALSE); +} + + +void CMidiSetupDlg::OnRenameDevice() +{ + int n = m_InputDevice.GetCurSel(); + if(n >= 0) + { + UINT device = static_cast<UINT>(m_InputDevice.GetItemData(n)); + MIDIINCAPS mic; + mic.szPname[0] = 0; + midiInGetDevCaps(device, &mic, sizeof(mic)); + CString name = mic.szPname; + CString friendlyName = theApp.GetSettings().Read(U_("MIDI Input Ports"), mpt::ToUnicode(name), name); + CInputDlg dlg(this, _T("New name for ") + name + _T(":"), friendlyName); + if(dlg.DoModal() == IDOK) + { + if(dlg.resultAsString.IsEmpty() || dlg.resultAsString == name) + theApp.GetSettings().Remove(U_("MIDI Input Ports"), mpt::ToUnicode(name)); + else + theApp.GetSettings().Write(U_("MIDI Input Ports"), mpt::ToUnicode(name), dlg.resultAsString); + RefreshDeviceList(device); + } + } +} + + +void CMidiSetupDlg::OnOK() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + m_dwMidiSetup = 0; + m_nMidiDevice = MIDI_MAPPER; + if (IsDlgButtonChecked(IDC_CHECK1)) m_dwMidiSetup |= MIDISETUP_RECORDVELOCITY; + if (IsDlgButtonChecked(IDC_CHECK2)) m_dwMidiSetup |= MIDISETUP_RECORDNOTEOFF; + if (IsDlgButtonChecked(IDC_CHECK3)) m_dwMidiSetup |= MIDISETUP_ENABLE_RECORD_DEFAULT; + if (IsDlgButtonChecked(IDC_CHECK4)) m_dwMidiSetup |= MIDISETUP_TRANSPOSEKEYBOARD; + if (IsDlgButtonChecked(IDC_MIDI_TO_PLUGIN)) m_dwMidiSetup |= MIDISETUP_MIDITOPLUG; + if (IsDlgButtonChecked(IDC_MIDI_MACRO_CONTROL)) m_dwMidiSetup |= MIDISETUP_MIDIMACROCONTROL; + if (IsDlgButtonChecked(IDC_MIDIVOL_TO_NOTEVOL)) m_dwMidiSetup |= MIDISETUP_MIDIVOL_TO_NOTEVOL; + if (IsDlgButtonChecked(IDC_MIDIPLAYCONTROL)) m_dwMidiSetup |= MIDISETUP_RESPONDTOPLAYCONTROLMSGS; + if (IsDlgButtonChecked(IDC_MIDIPLAYPATTERNONMIDIIN)) m_dwMidiSetup |= MIDISETUP_PLAYPATTERNONMIDIIN; + if (IsDlgButtonChecked(IDC_CHECK5)) m_dwMidiSetup |= MIDISETUP_MIDIMACROPITCHBEND; + + int n = m_InputDevice.GetCurSel(); + if (n >= 0) m_nMidiDevice = static_cast<UINT>(m_InputDevice.GetItemData(n)); + + TrackerSettings::Instance().aftertouchBehaviour = static_cast<RecordAftertouchOptions>(m_ATBehaviour.GetItemData(m_ATBehaviour.GetCurSel())); + + TrackerSettings::Instance().midiVelocityAmp = static_cast<uint16>(Clamp(GetDlgItemInt(IDC_EDIT3), 1u, 10000u)); + + CString cc; + GetDlgItemText(IDC_EDIT4, cc); + TrackerSettings::Instance().midiIgnoreCCs = StringToIgnoredCCs(mpt::ToUnicode(cc)); + + TrackerSettings::Instance().midiImportTicks = static_cast<uint8>(Clamp(GetDlgItemInt(IDC_EDIT1), uint8(2), uint8(16))); + TrackerSettings::Instance().midiImportPatternLen = Clamp(GetDlgItemInt(IDC_EDIT2), ROWINDEX(1), MAX_PATTERN_ROWS); + if(m_Quantize.GetCurSel() != -1) + { + TrackerSettings::Instance().midiImportQuantize = static_cast<uint32>(m_Quantize.GetItemData(m_Quantize.GetCurSel())); + } + + if (pMainFrm) pMainFrm->SetupMidi(m_dwMidiSetup, m_nMidiDevice); + CPropertyPage::OnOK(); +} + + +BOOL CMidiSetupDlg::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_MIDI; + return CPropertyPage::OnSetActive(); +} + + +// Wine + + +BEGIN_MESSAGE_MAP(COptionsWine, CPropertyPage) + ON_COMMAND(IDC_CHECK_WINE_ENABLE, &COptionsWine::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO_WINE_PULSEAUDIO, &COptionsWine::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO_WINE_PORTAUDIO, &COptionsWine::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_COMBO_WINE_RTAUDIO, &COptionsWine::OnSettingsChanged) +END_MESSAGE_MAP() + + +COptionsWine::COptionsWine() + : CPropertyPage(IDD_OPTIONS_WINE) +{ + return; +} + + +void COptionsWine::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsWine) + DDX_Control(pDX, IDC_COMBO_WINE_PULSEAUDIO, m_CbnPulseAudio); + DDX_Control(pDX, IDC_COMBO_WINE_PORTAUDIO, m_CbnPortAudio); + DDX_Control(pDX, IDC_COMBO_WINE_RTAUDIO, m_CbnRtAudio); + //}}AFX_DATA_MAP +} + + +BOOL COptionsWine::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + GetDlgItem(IDC_CHECK_WINE_ENABLE)->EnableWindow(mpt::OS::Windows::IsWine() ? TRUE : FALSE); + CheckDlgButton(IDC_CHECK_WINE_ENABLE, TrackerSettings::Instance().WineSupportEnabled ? BST_CHECKED : BST_UNCHECKED); + int index; + index = m_CbnPulseAudio.AddString(_T("Auto" )); m_CbnPulseAudio.SetItemData(index, 1); + index = m_CbnPulseAudio.AddString(_T("Enabled" )); m_CbnPulseAudio.SetItemData(index, 2); + index = m_CbnPulseAudio.AddString(_T("Disabled")); m_CbnPulseAudio.SetItemData(index, 0); + m_CbnPulseAudio.SetCurSel(0); + for(index = 0; index < 3; ++index) + { + if(m_CbnPulseAudio.GetItemData(index) == static_cast<uint32>(TrackerSettings::Instance().WineSupportEnablePulseAudio)) + { + m_CbnPulseAudio.SetCurSel(index); + } + } + index = m_CbnPortAudio.AddString(_T("Auto" )); m_CbnPortAudio.SetItemData(index, 1); + index = m_CbnPortAudio.AddString(_T("Enabled" )); m_CbnPortAudio.SetItemData(index, 2); + index = m_CbnPortAudio.AddString(_T("Disabled")); m_CbnPortAudio.SetItemData(index, 0); + for(index = 0; index < 3; ++index) + { + if(m_CbnPortAudio.GetItemData(index) == static_cast<uint32>(TrackerSettings::Instance().WineSupportEnablePortAudio)) + { + m_CbnPortAudio.SetCurSel(index); + } + } + index = m_CbnRtAudio.AddString(_T("Auto" )); m_CbnRtAudio.SetItemData(index, 1); + index = m_CbnRtAudio.AddString(_T("Enabled" )); m_CbnRtAudio.SetItemData(index, 2); + index = m_CbnRtAudio.AddString(_T("Disabled")); m_CbnRtAudio.SetItemData(index, 0); + for(index = 0; index < 3; ++index) + { + if(m_CbnRtAudio.GetItemData(index) == static_cast<uint32>(TrackerSettings::Instance().WineSupportEnableRtAudio)) + { + m_CbnRtAudio.SetCurSel(index); + } + } + return TRUE; +} + + +void COptionsWine::OnSettingsChanged() +{ + SetModified(TRUE); +} + + +void COptionsWine::OnOK() +{ + TrackerSettings::Instance().WineSupportEnabled = IsDlgButtonChecked(IDC_CHECK_WINE_ENABLE) ? true : false; + TrackerSettings::Instance().WineSupportEnablePulseAudio = static_cast<int32>(m_CbnPulseAudio.GetItemData(m_CbnPulseAudio.GetCurSel())); + TrackerSettings::Instance().WineSupportEnablePortAudio = static_cast<int32>(m_CbnPortAudio.GetItemData(m_CbnPortAudio.GetCurSel())); + TrackerSettings::Instance().WineSupportEnableRtAudio = static_cast<int32>(m_CbnRtAudio.GetItemData(m_CbnRtAudio.GetCurSel())); + CPropertyPage::OnOK(); +} + + +BOOL COptionsWine::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_WINE; + return CPropertyPage::OnSetActive(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.h b/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.h new file mode 100644 index 00000000..93647489 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mpdlgs.h @@ -0,0 +1,256 @@ +/* + * MPDlgs.h + * -------- + * Purpose: Implementation of various player setup dialogs. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; +class CMainFrame; + +#define NUM_CHANNELCOMBOBOXES 4 + +class COptionsSoundcard: public CPropertyPage +{ +protected: + CComboBoxEx m_CbnDevice; + CComboBox m_CbnLatencyMS, m_CbnUpdateIntervalMS, m_CbnMixingFreq, m_CbnChannels, m_CbnSampleFormat, m_CbnDither, m_CbnRecordingChannels, m_CbnRecordingSource; + CEdit m_EditStatistics; + CButton m_BtnDriverPanel; + + CComboBox m_CbnStoppedMode; + + CComboBox m_CbnChannelMapping[NUM_CHANNELCOMBOBOXES]; + + SoundDevice::Identifier m_InitialDeviceIdentifier; + + void SetInitialDevice(); + + void SetDevice(SoundDevice::Identifier dev, bool forceReload=false); + SoundDevice::Info m_CurrentDeviceInfo; + SoundDevice::Caps m_CurrentDeviceCaps; + SoundDevice::DynamicCaps m_CurrentDeviceDynamicCaps; + SoundDevice::Settings m_Settings; + +public: + COptionsSoundcard(SoundDevice::Identifier deviceIdentifier); + + void UpdateStatistics(); + +private: + void UpdateEverything(); + void UpdateDevice(); + void UpdateGeneral(); + void UpdateLatency(); + void UpdateUpdateInterval(); + void UpdateSampleRates(); + void UpdateChannels(); + void UpdateSampleFormat(); + void UpdateDither(); + void UpdateChannelMapping(); + void UpdateRecording(); + void UpdateControls(); + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + void UpdateStereoSep(); + + afx_msg void OnDeviceChanged(); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnExclusiveModeChanged(); + afx_msg void OnChannelsChanged(); + afx_msg void OnSampleFormatChanged(); + afx_msg void OnRecordingChanged(); + afx_msg void OnSoundCardShowAll(); + afx_msg void OnSoundCardRescan(); + afx_msg void OnSoundCardDriverPanel(); + + void OnChannelChanged(int channel); + afx_msg void OnChannel1Changed() { OnChannelChanged(0); }; + afx_msg void OnChannel2Changed() { OnChannelChanged(1); }; + afx_msg void OnChannel3Changed() { OnChannelChanged(2); }; + afx_msg void OnChannel4Changed() { OnChannelChanged(3); }; + + DECLARE_MESSAGE_MAP() +}; + + +class COptionsMixer: public CPropertyPage +{ +protected: + + CComboBox m_CbnResampling, m_CbnAmigaType; + + CEdit m_CEditRampUp; + CEdit m_CEditRampDown; + CEdit m_CInfoRampUp; + CEdit m_CInfoRampDown; + + CSliderCtrl m_SliderStereoSep; + + // check box soft pan + + CSliderCtrl m_SliderPreAmp; + + bool m_initialized : 1; + +public: + COptionsMixer() + : CPropertyPage(IDD_OPTIONS_MIXER) + , m_initialized(false) + {} + +protected: + void UpdateRamping(); + void UpdateStereoSep(); + + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnAmigaChanged(); + afx_msg void OnRampingChanged(); + afx_msg void OnDefaultRampSettings(); + + afx_msg void OnHScroll(UINT n, UINT pos, CScrollBar *p); + + DECLARE_MESSAGE_MAP() + +}; + + +#ifndef NO_EQ + +class CEQSlider: public CSliderCtrl +{ +public: + CWnd *m_pParent; + UINT m_nSliderNo; + short int m_x, m_y; +public: + CEQSlider() {} + void Init(UINT nID, UINT n, CWnd *parent); + BOOL PreTranslateMessage(MSG *pMsg); +}; + +#endif // !NO_EQ + + +class COptionsPlayer: public CPropertyPage +{ +protected: + CComboBox m_CbnReverbPreset; + CSliderCtrl m_SbXBassDepth, m_SbXBassRange; + CSliderCtrl m_SbSurroundDepth, m_SbSurroundDelay; + CSliderCtrl m_SbReverbDepth; + CSliderCtrl m_SbBitCrushBits; + +#ifndef NO_EQ + CEQSlider m_Sliders[MAX_EQ_BANDS]; + EQPreset &m_EQPreset; + UINT m_nSliderMenu; +#endif // !NO_EQ + +public: + COptionsPlayer() : CPropertyPage(IDD_OPTIONS_PLAYER) +#ifndef NO_EQ + , m_EQPreset(TrackerSettings::Instance().m_EqSettings) +#endif + { } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + afx_msg void OnHScroll(UINT, UINT, CScrollBar *); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + +#ifndef NO_EQ + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnEqUser1() { LoadEQPreset(TrackerSettings::Instance().m_EqUserPresets[0]); }; + afx_msg void OnEqUser2() { LoadEQPreset(TrackerSettings::Instance().m_EqUserPresets[1]); }; + afx_msg void OnEqUser3() { LoadEQPreset(TrackerSettings::Instance().m_EqUserPresets[2]); }; + afx_msg void OnEqUser4() { LoadEQPreset(TrackerSettings::Instance().m_EqUserPresets[3]); }; + afx_msg void OnSavePreset(); + afx_msg void OnSliderMenu(UINT); + afx_msg void OnSliderFreq(UINT); + + void UpdateDialog(); + void UpdateEQ(bool bReset); + void LoadEQPreset(const EQPreset &preset); +#endif // !NO_EQ + + DECLARE_MESSAGE_MAP() +}; + + +class CMidiSetupDlg: public CPropertyPage +{ +public: + DWORD m_dwMidiSetup; + UINT m_nMidiDevice; + +protected: + CSpinButtonCtrl m_SpinSpd, m_SpinPat, m_SpinAmp; + CComboBox m_InputDevice, m_ATBehaviour, m_Quantize; + +public: + CMidiSetupDlg(DWORD d, UINT n) + : CPropertyPage(IDD_OPTIONS_MIDI) + , m_dwMidiSetup(d) + , m_nMidiDevice(n) + { } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + void RefreshDeviceList(UINT currentDevice); + afx_msg void OnRenameDevice(); + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + DECLARE_MESSAGE_MAP() +}; + + + +class COptionsWine: public CPropertyPage +{ + +protected: + CComboBox m_CbnPulseAudio; + CComboBox m_CbnPortAudio; + CComboBox m_CbnRtAudio; + +public: + COptionsWine(); + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + void DoDataExchange(CDataExchange* pDX) override; + + afx_msg void OnSettingsChanged(); + + DECLARE_MESSAGE_MAP() +}; + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mpt_midi.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Mpt_midi.cpp new file mode 100644 index 00000000..6463ede0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mpt_midi.cpp @@ -0,0 +1,187 @@ +/* + * MPT_MIDI.cpp + * ------------ + * Purpose: MIDI Input handling code. + * 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 <mmsystem.h> +#include "Mainfrm.h" +#include "InputHandler.h" +#include "Dlsbank.h" +#include "../soundlib/MIDIEvents.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MPT_ALL_LOGGING +#define MPTMIDI_RECORDLOG +#endif + + +// Midi Input globals +HMIDIIN CMainFrame::shMidiIn = nullptr; + +//Get Midi message(dwParam1), apply MIDI settings having effect on volume, and return +//the volume value [0, 256]. In addition value -1 is used as 'use default value'-indicator. +int CMainFrame::ApplyVolumeRelatedSettings(const DWORD &dwParam1, const BYTE midivolume) +{ + int nVol = MIDIEvents::GetDataByte2FromEvent(dwParam1); + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_RECORDVELOCITY) + { + nVol = (CDLSBank::DLSMidiVolumeToLinear(nVol)+255) >> 8; + nVol *= TrackerSettings::Instance().midiVelocityAmp / 100; + Limit(nVol, 1, 256); + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIVOL_TO_NOTEVOL) + nVol = static_cast<int>((midivolume / 127.0) * nVol); + } else + { + // Case: No velocity record. + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIVOL_TO_NOTEVOL) + nVol = 4*((midivolume+1)/2); + else //Use default volume + nVol = -1; + } + + return nVol; +} + + +void ApplyTransposeKeyboardSetting(CMainFrame &rMainFrm, uint32 &dwParam1) +{ + if ( (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_TRANSPOSEKEYBOARD) + && (MIDIEvents::GetChannelFromEvent(dwParam1) != 9) ) + { + int nTranspose = rMainFrm.GetBaseOctave() - 4; + if (nTranspose) + { + int note = MIDIEvents::GetDataByte1FromEvent(dwParam1); + if (note < 0x80) + { + note += nTranspose * 12; + Limit(note, 0, NOTE_MAX - NOTE_MIN); + + dwParam1 &= 0xffff00ff; + + dwParam1 |= (note << 8); + } + } + } +} + + +///////////////////////////////////////////////////////////////////////////// +// MMSYSTEM Midi Record + +void CALLBACK MidiInCallBack(HMIDIIN, UINT wMsg, DWORD_PTR, DWORD_PTR dwParam1, DWORD_PTR dwParam2) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + HWND hWndMidi; + + if (!pMainFrm) return; +#ifdef MPTMIDI_RECORDLOG + DWORD dwMidiStatus = dwParam1 & 0xFF; + DWORD dwMidiByte1 = (dwParam1 >> 8) & 0xFF; + DWORD dwMidiByte2 = (dwParam1 >> 16) & 0xFF; + DWORD dwTimeStamp = (DWORD)dwParam2; + MPT_LOG_GLOBAL(LogDebug, "MIDI", MPT_UFORMAT("time={}ms status={} data={}.{}")(mpt::ufmt::dec(dwTimeStamp), mpt::ufmt::HEX0<2>(dwMidiStatus), mpt::ufmt::HEX0<2>(dwMidiByte1), mpt::ufmt::HEX0<2>(dwMidiByte2))); +#endif + + hWndMidi = pMainFrm->GetMidiRecordWnd(); + if(wMsg == MIM_DATA || wMsg == MIM_MOREDATA) + { + uint32 data = static_cast<uint32>(dwParam1); + if(::IsWindow(hWndMidi)) + { + switch(MIDIEvents::GetTypeFromEvent(data)) + { + case MIDIEvents::evNoteOff: // Note Off + case MIDIEvents::evNoteOn: // Note On + ApplyTransposeKeyboardSetting(*pMainFrm, data); + [[fallthrough]]; + default: + if(::PostMessage(hWndMidi, WM_MOD_MIDIMSG, data, dwParam2)) + return; // Message has been handled + break; + } + } + // Pass MIDI to keyboard handler + CMainFrame::GetInputHandler()->HandleMIDIMessage(kCtxAllContexts, data); + } else if(wMsg == MIM_LONGDATA) + { + // SysEx... + } else if (wMsg == MIM_CLOSE) + { + // midiInClose will trigger this, but also disconnecting a USB MIDI device (although delayed, seems to be coupled to calling something like midiInGetNumDevs). + // In the latter case, we need to inform the UI. + if(CMainFrame::shMidiIn != nullptr) + { + pMainFrm->SendMessage(WM_COMMAND, ID_MIDI_RECORD); + } + } +} + + +bool CMainFrame::midiOpenDevice(bool showSettings) +{ + if (shMidiIn) return true; + + if (midiInOpen(&shMidiIn, TrackerSettings::Instance().GetCurrentMIDIDevice(), (DWORD_PTR)MidiInCallBack, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) + { + shMidiIn = nullptr; + + // Show MIDI configuration on fail. + if(showSettings) + { + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_MIDI; + CMainFrame::GetMainFrame()->OnViewOptions(); + } + + // Let's see if the user updated the settings. + if(midiInOpen(&shMidiIn, TrackerSettings::Instance().GetCurrentMIDIDevice(), (DWORD_PTR)MidiInCallBack, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR) + { + shMidiIn = nullptr; + return false; + } + } + midiInStart(shMidiIn); + return true; +} + + +void CMainFrame::midiCloseDevice() +{ + if (shMidiIn) + { + // Prevent infinite loop in MIM_CLOSE + auto handle = shMidiIn; + shMidiIn = nullptr; + midiInClose(handle); + } +} + + +void CMainFrame::OnMidiRecord() +{ + if (shMidiIn) + { + midiCloseDevice(); + } else + { + midiOpenDevice(); + } +} + + +void CMainFrame::OnUpdateMidiRecord(CCmdUI *pCmdUI) +{ + if (pCmdUI) pCmdUI->SetCheck((shMidiIn) ? TRUE : FALSE); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.cpp new file mode 100644 index 00000000..2e84bc81 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.cpp @@ -0,0 +1,2413 @@ +/* + * MPTrack.cpp + * ----------- + * Purpose: OpenMPT core application class. + * 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 "IPCWindow.h" +#include "InputHandler.h" +#include "Childfrm.h" +#include "Moddoc.h" +#include "ModDocTemplate.h" +#include "Globals.h" +#include "../soundlib/Dlsbank.h" +#include "../common/version.h" +#include "../test/test.h" +#include "UpdateCheck.h" +#include "../common/mptStringBuffer.h" +#include "ExceptionHandler.h" +#include "CloseMainDialog.h" +#include "PlugNotFoundDlg.h" +#include "AboutDialog.h" +#include "AutoSaver.h" +#include "FileDialog.h" +#include "Image.h" +#include "BuildVariants.h" +#include "../common/ComponentManager.h" +#include "WelcomeDialog.h" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "WineSoundDeviceStub.h" +#include "../soundlib/plugins/PluginManager.h" +#include "MPTrackWine.h" +#include "MPTrackUtil.h" +#if MPT_MSVC_AT_LEAST(2022, 2) && MPT_MSVC_BEFORE(2022, 3) +// Work-around <https://developercommunity.visualstudio.com/t/warning-C4311-in-MFC-header-afxrecovery/10041328>, +// see <https://developercommunity.visualstudio.com/t/Compiler-warnings-after-upgrading-to-17/10036311#T-N10061908>. +template <class ARG_KEY> +AFX_INLINE UINT AFXAPI HashKey(ARG_KEY key); +template <> +AFX_INLINE UINT AFXAPI HashKey<CDocument*>(CDocument *key) +{ + // (algorithm copied from STL hash in xfunctional) +#pragma warning(suppress: 4302) // 'type cast' : truncation +#pragma warning(suppress: 4311) // pointer truncation + ldiv_t HashVal = ldiv((long)(CDocument*)key, 127773); + HashVal.rem = 16807 * HashVal.rem - 2836 * HashVal.quot; + if(HashVal.rem < 0) + HashVal.rem += 2147483647; + return ((UINT)HashVal.rem); +} +#endif +#include <afxdatarecovery.h> + +// GDI+ +#include <atlbase.h> +#define max(a, b) (((a) > (b)) ? (a) : (b)) +#define min(a, b) (((a) < (b)) ? (a) : (b)) +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable : 4458) // declaration of 'x' hides class member +#endif +#include <gdiplus.h> +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif +#undef min +#undef max + +#if MPT_COMPILER_MSVC +#define _CRTDBG_MAP_ALLOC +#include <stdlib.h> +#include <crtdbg.h> +#endif + + +OPENMPT_NAMESPACE_BEGIN + +///////////////////////////////////////////////////////////////////////////// +// The one and only CTrackApp object + +CTrackApp theApp; + +const TCHAR *szSpecialNoteNamesMPT[] = {_T("PCs"), _T("PC"), _T("~~ (Note Fade)"), _T("^^ (Note Cut)"), _T("== (Note Off)")}; +const TCHAR *szSpecialNoteShortDesc[] = {_T("Param Control (Smooth)"), _T("Param Control"), _T("Note Fade"), _T("Note Cut"), _T("Note Off")}; + +// Make sure that special note arrays include string for every note. +static_assert(NOTE_MAX_SPECIAL - NOTE_MIN_SPECIAL + 1 == mpt::array_size<decltype(szSpecialNoteNamesMPT)>::size); +static_assert(mpt::array_size<decltype(szSpecialNoteShortDesc)>::size == mpt::array_size<decltype(szSpecialNoteNamesMPT)>::size); + +const char *szHexChar = "0123456789ABCDEF"; + + +#ifdef MPT_WITH_ASIO +class ComponentASIO + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentASIO, "ASIO") +public: + ComponentASIO() = default; + virtual ~ComponentASIO() = default; +}; +#endif // MPT_WITH_ASIO + +#if defined(MPT_WITH_DIRECTSOUND) +class ComponentDirectSound + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentDirectSound, "DirectSound") +public: + ComponentDirectSound() = default; + virtual ~ComponentDirectSound() = default; +}; +#endif // MPT_WITH_DIRECTSOUND + +#if defined(MPT_WITH_PORTAUDIO) +class ComponentPortAudio + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentPortAudio, "PortAudio") +public: + ComponentPortAudio() = default; + virtual ~ComponentPortAudio() = default; +}; +#endif // MPT_WITH_PORTAUDIO + +#if defined(MPT_WITH_PULSEAUDIO) +class ComponentPulseaudio + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentPulseaudio, "Pulseaudio") +public: + ComponentPulseaudio() = default; + virtual ~ComponentPulseaudio() = default; +}; +#endif // MPT_WITH_PULSEAUDIO + +#if defined(MPT_WITH_PULSEAUDIO) && defined(MPT_WITH_PULSEAUDIOSIMPLE) +class ComponentPulseaudioSimple + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentPulseaudioSimple, "PulseaudioSimple") +public: + ComponentPulseaudioSimple() = default; + virtual ~ComponentPulseaudioSimple() = default; +}; +#endif // MPT_WITH_PULSEAUDIO && MPT_WITH_PULSEAUDIOSIMPLE + +#if defined(MPT_WITH_RTAUDIO) +class ComponentRtAudio + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentRtAudio, "RtAudio") +public: + ComponentRtAudio() = default; + virtual ~ComponentRtAudio() = default; +}; +#endif // MPT_WITH_RTAUDIO + +#if MPT_OS_WINDOWS +class ComponentWaveOut + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentWaveOut, "WaveOut") +public: + ComponentWaveOut() = default; + virtual ~ComponentWaveOut() = default; +}; +#endif // MPT_OS_WINDOWS + +struct AllSoundDeviceComponents +{ +#if defined(MPT_WITH_PULSEAUDIO) && defined(MPT_ENABLE_PULSEAUDIO_FULL) + ComponentHandle<ComponentPulseaudio> m_Pulseaudio; +#endif // MPT_WITH_PULSEAUDIO && MPT_ENABLE_PULSEAUDIO_FULL +#if defined(MPT_WITH_PULSEAUDIO) && defined(MPT_WITH_PULSEAUDIOSIMPLE) + ComponentHandle<ComponentPulseaudioSimple> m_PulseaudioSimple; +#endif // MPT_WITH_PULSEAUDIO && MPT_WITH_PULSEAUDIOSIMPLE +#if MPT_OS_WINDOWS + ComponentHandle<ComponentWaveOut> m_WaveOut; +#endif // MPT_OS_WINDOWS +#if defined(MPT_WITH_DIRECTSOUND) + ComponentHandle<ComponentDirectSound> m_DirectSound; +#endif // MPT_WITH_DIRECTSOUND +#ifdef MPT_WITH_ASIO + ComponentHandle<ComponentASIO> m_ASIO; +#endif // MPT_WITH_ASIO +#ifdef MPT_WITH_PORTAUDIO + ComponentHandle<ComponentPortAudio> m_PortAudio; +#endif // MPT_WITH_PORTAUDIO +#ifdef MPT_WITH_RTAUDIO + ComponentHandle<ComponentRtAudio> m_RtAudio; +#endif // MPT_WITH_RTAUDIO + operator SoundDevice::EnabledBackends() const + { + SoundDevice::EnabledBackends result; +#if defined(MPT_WITH_PULSEAUDIO) && defined(MPT_ENABLE_PULSEAUDIO_FULL) + result.Pulseaudio = IsComponentAvailable(m_PulseAudio); +#endif // MPT_WITH_PULSEAUDIO && MPT_ENABLE_PULSEAUDIO_FULL +#if defined(MPT_WITH_PULSEAUDIO) && defined(MPT_WITH_PULSEAUDIOSIMPLE) + result.PulseaudioSimple = IsComponentAvailable(m_PulseAudioSimple); +#endif // MPT_WITH_PULSEAUDIO && MPT_WITH_PULSEAUDIOSIMPLE +#if MPT_OS_WINDOWS + result.WaveOut = IsComponentAvailable(m_WaveOut); +#endif // MPT_OS_WINDOWS +#if defined(MPT_WITH_DIRECTSOUND) + result.DirectSound = IsComponentAvailable(m_DirectSound); +#endif // MPT_WITH_DIRECTSOUND +#ifdef MPT_WITH_ASIO + result.ASIO = IsComponentAvailable(m_ASIO); +#endif // MPT_WITH_ASIO +#ifdef MPT_WITH_PORTAUDIO + result.PortAudio = IsComponentAvailable(m_PortAudio); +#endif // MPT_WITH_PORTAUDIO +#ifdef MPT_WITH_RTAUDIO + result.RtAudio = IsComponentAvailable(m_RtAudio); +#endif // MPT_WITH_RTAUDIO + return result; + } +}; + + +void CTrackApp::OnFileCloseAll() +{ + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOCLOSEDIALOG)) + { + // Show modified documents window + CloseMainDialog dlg; + if(dlg.DoModal() != IDOK) + { + return; + } + } + + for(auto &doc : GetOpenDocuments()) + { + doc->SafeFileClose(); + } +} + + +void CTrackApp::OnUpdateAnyDocsOpen(CCmdUI *cmd) +{ + cmd->Enable(!GetModDocTemplate()->empty()); +} + + +int CTrackApp::GetOpenDocumentCount() const +{ + return static_cast<int>(GetModDocTemplate()->size()); +} + + +// Retrieve a list of all open modules. +std::vector<CModDoc *> CTrackApp::GetOpenDocuments() const +{ + std::vector<CModDoc *> documents; + if(auto *pDocTmpl = GetModDocTemplate()) + { + POSITION pos = pDocTmpl->GetFirstDocPosition(); + CDocument *pDoc; + while((pos != nullptr) && ((pDoc = pDocTmpl->GetNextDoc(pos)) != nullptr)) + { + documents.push_back(dynamic_cast<CModDoc *>(pDoc)); + } + } + + return documents; +} + + +///////////////////////////////////////////////////////////////////////////// +// Command Line options + +class CMPTCommandLineInfo: public CCommandLineInfo +{ +public: + std::vector<mpt::PathString> m_fileNames; + bool m_noDls = false, m_noPlugins = false, m_noAssembly = false, m_noSysCheck = false, m_noWine = false, + m_portable = false, m_noCrashHandler = false, m_debugCrashHandler = false, m_sharedInstance = false; +#ifdef ENABLE_TESTS + bool m_noTests = false; +#endif + +public: + void ParseParam(LPCTSTR param, BOOL isFlag, BOOL isLast) override + { + if(isFlag) + { + if(!lstrcmpi(param, _T("nologo"))) { m_bShowSplash = FALSE; return; } + if(!lstrcmpi(param, _T("nodls"))) { m_noDls = true; return; } + if(!lstrcmpi(param, _T("noplugs"))) { m_noPlugins = true; return; } + if(!lstrcmpi(param, _T("portable"))) { m_portable = true; return; } + if(!lstrcmpi(param, _T("fullMemDump"))) { ExceptionHandler::fullMemDump = true; return; } + if(!lstrcmpi(param, _T("noAssembly"))) { m_noAssembly = true; return; } + if(!lstrcmpi(param, _T("noSysCheck"))) { m_noSysCheck = true; return; } + if(!lstrcmpi(param, _T("noWine"))) { m_noWine = true; return; } + if(!lstrcmpi(param, _T("noCrashHandler"))) { m_noCrashHandler = true; return; } + if(!lstrcmpi(param, _T("DebugCrashHandler"))) { m_debugCrashHandler = true; return; } + if(!lstrcmpi(param, _T("shared"))) { m_sharedInstance = true; return; } +#ifdef ENABLE_TESTS + if (!lstrcmpi(param, _T("noTests"))) { m_noTests = true; return; } +#endif + } else + { + m_fileNames.push_back(mpt::PathString::FromNative(param)); + if(m_nShellCommand == FileNew) m_nShellCommand = FileOpen; + } + CCommandLineInfo::ParseParam(param, isFlag, isLast); + } +}; + + +// Splash Screen + +static void StartSplashScreen(); +static void StopSplashScreen(); +static void TimeoutSplashScreen(); + + +///////////////////////////////////////////////////////////////////////////// +// Midi Library + +MidiLibrary CTrackApp::midiLibrary; + +void CTrackApp::ImportMidiConfig(const mpt::PathString &filename, bool hideWarning) +{ + if(filename.empty()) return; + + if(CDLSBank::IsDLSBank(filename)) + { + ConfirmAnswer result = cnfYes; + if(!hideWarning) + { + result = Reporting::Confirm("You are about to replace the current MIDI library:\n" + "Do you want to replace only the missing instruments? (recommended)", + "Warning", true); + } + if(result == cnfCancel) return; + const bool replaceAll = (result == cnfNo); + CDLSBank dlsbank; + if (dlsbank.Open(filename)) + { + for(uint32 ins = 0; ins < 256; ins++) + { + if(replaceAll || midiLibrary[ins].empty()) + { + uint32 prog = (ins < 128) ? ins : 0xFF; + uint32 key = (ins < 128) ? 0xFF : ins & 0x7F; + uint32 bank = (ins < 128) ? 0 : F_INSTRUMENT_DRUMS; + if (dlsbank.FindInstrument(ins >= 128, bank, prog, key)) + { + midiLibrary[ins] = filename; + } + } + } + } + return; + } + + IniFileSettingsContainer file(filename); + ImportMidiConfig(file, filename.GetPath()); +} + + +static mpt::PathString GetUltraSoundPatchDir(SettingsContainer &file, const mpt::ustring &iniSection, const mpt::PathString &path, bool forgetSettings) +{ + mpt::PathString patchDir = file.Read<mpt::PathString>(iniSection, U_("PatchDir"), {}); + if(forgetSettings) + file.Forget(U_("Ultrasound"), U_("PatchDir")); + if(patchDir.empty() || patchDir == P_(".\\")) + patchDir = path; + if(!patchDir.empty()) + patchDir.EnsureTrailingSlash(); + return patchDir; +} + +void CTrackApp::ImportMidiConfig(SettingsContainer &file, const mpt::PathString &path, bool forgetSettings) +{ + const mpt::PathString patchDir = GetUltraSoundPatchDir(file, U_("Ultrasound"), path, forgetSettings); + for(uint32 prog = 0; prog < 256; prog++) + { + mpt::ustring key = MPT_UFORMAT("{}{}")((prog < 128) ? U_("Midi") : U_("Perc"), prog & 0x7F); + mpt::PathString filename = file.Read<mpt::PathString>(U_("Midi Library"), key, mpt::PathString()); + // Check for ULTRASND.INI + if(filename.empty()) + { + mpt::ustring section = (prog < 128) ? UL_("Melodic Patches") : UL_("Drum Patches"); + key = mpt::ufmt::val(prog & 0x7f); + filename = file.Read<mpt::PathString>(section, key, mpt::PathString()); + if(forgetSettings) file.Forget(section, key); + if(filename.empty()) + { + section = (prog < 128) ? UL_("Melodic Bank 0") : UL_("Drum Bank 0"); + filename = file.Read<mpt::PathString>(section, key, mpt::PathString()); + if(forgetSettings) file.Forget(section, key); + } + const mpt::PathString localPatchDir = GetUltraSoundPatchDir(file, section, patchDir, forgetSettings); + if(!filename.empty()) + { + filename = localPatchDir + filename + P_(".pat"); + } + } + if(!filename.empty()) + { + filename = theApp.PathInstallRelativeToAbsolute(filename); + midiLibrary[prog] = filename; + } + } +} + + +void CTrackApp::ExportMidiConfig(const mpt::PathString &filename) +{ + if(filename.empty()) return; + IniFileSettingsContainer file(filename); + ExportMidiConfig(file); +} + +void CTrackApp::ExportMidiConfig(SettingsContainer &file) +{ + for(uint32 prog = 0; prog < 256; prog++) if (!midiLibrary[prog].empty()) + { + mpt::PathString szFileName = midiLibrary[prog]; + + if(!szFileName.empty()) + { + if(theApp.IsPortableMode()) + szFileName = theApp.PathAbsoluteToInstallRelative(szFileName); + + mpt::ustring key = MPT_UFORMAT("{}{}")((prog < 128) ? U_("Midi") : U_("Perc"), prog & 0x7F); + file.Write<mpt::PathString>(U_("Midi Library"), key, szFileName); + } + } +} + + +///////////////////////////////////////////////////////////////////////////// +// DLS Banks support + +std::vector<std::unique_ptr<CDLSBank>> CTrackApp::gpDLSBanks; + + +struct CompareLessPathStringNoCase +{ + inline bool operator()(const mpt::PathString &l, const mpt::PathString &r) const + { + return mpt::PathString::CompareNoCase(l, r) < 0; + } +}; + +std::future<std::vector<std::unique_ptr<CDLSBank>>> CTrackApp::LoadDefaultDLSBanks() +{ + std::set<mpt::PathString, CompareLessPathStringNoCase> paths; + + uint32 numBanks = theApp.GetSettings().Read<uint32>(U_("DLS Banks"), U_("NumBanks"), 0); + for(uint32 i = 0; i < numBanks; i++) + { + mpt::PathString path = theApp.GetSettings().Read<mpt::PathString>(U_("DLS Banks"), MPT_UFORMAT("Bank{}")(i + 1), mpt::PathString()); + paths.insert(theApp.PathInstallRelativeToAbsolute(path)); + } + + HKEY key; + if(RegOpenKeyEx(HKEY_LOCAL_MACHINE, _T("Software\\Microsoft\\DirectMusic"), 0, KEY_READ, &key) == ERROR_SUCCESS) + { + DWORD dwRegType = REG_SZ; + DWORD dwSize = 0; + if(RegQueryValueEx(key, _T("GMFilePath"), NULL, &dwRegType, nullptr, &dwSize) == ERROR_SUCCESS && dwSize > 0) + { + std::vector<TCHAR> filenameT(dwSize / sizeof(TCHAR)); + if(RegQueryValueEx(key, _T("GMFilePath"), NULL, &dwRegType, reinterpret_cast<LPBYTE>(filenameT.data()), &dwSize) == ERROR_SUCCESS) + { + mpt::winstring filenamestr = ParseMaybeNullTerminatedStringFromBufferWithSizeInBytes<mpt::winstring>(filenameT.data(), dwSize); + std::vector<TCHAR> filenameExpanded(::ExpandEnvironmentStrings(filenamestr.c_str(), nullptr, 0)); + ::ExpandEnvironmentStrings(filenamestr.c_str(), filenameExpanded.data(), static_cast<DWORD>(filenameExpanded.size())); + auto filename = mpt::PathString::FromNative(filenameExpanded.data()); + ImportMidiConfig(filename, true); + paths.insert(std::move(filename)); + } + } + RegCloseKey(key); + } + + if(paths.empty()) + return {}; + + return std::async(std::launch::async, [paths = std::move(paths)]() + { + std::vector<std::unique_ptr<CDLSBank>> banks; + banks.reserve(paths.size()); + for(const auto &filename : paths) + { + if(filename.empty() || !CDLSBank::IsDLSBank(filename)) + continue; + try + { + auto bank = std::make_unique<CDLSBank>(); + if(bank->Open(filename)) + { + banks.push_back(std::move(bank)); + continue; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } catch(const std::exception &) + { + } + } + // Avoid the overhead of future::wait_for(0) until future::is_ready is finally non-experimental + theApp.m_scannedDlsBanksAvailable = true; + return banks; + }); +} + + +void CTrackApp::SaveDefaultDLSBanks() +{ + uint32 nBanks = 0; + for(const auto &bank : gpDLSBanks) + { + if(!bank || bank->GetFileName().empty()) + continue; + + mpt::PathString path = bank->GetFileName(); + if(theApp.IsPortableMode()) + { + path = theApp.PathAbsoluteToInstallRelative(path); + } + + mpt::ustring key = MPT_UFORMAT("Bank{}")(nBanks + 1); + theApp.GetSettings().Write<mpt::PathString>(U_("DLS Banks"), key, path); + nBanks++; + + } + theApp.GetSettings().Write<uint32>(U_("DLS Banks"), U_("NumBanks"), nBanks); +} + + +void CTrackApp::RemoveDLSBank(UINT nBank) +{ + if(nBank < gpDLSBanks.size()) + gpDLSBanks[nBank] = nullptr; +} + + +bool CTrackApp::AddDLSBank(const mpt::PathString &filename) +{ + if(filename.empty() || !CDLSBank::IsDLSBank(filename)) return false; + // Check for dupes + for(const auto &bank : gpDLSBanks) + { + if(bank && !mpt::PathString::CompareNoCase(filename, bank->GetFileName())) + return true; + } + try + { + auto bank = std::make_unique<CDLSBank>(); + if(bank->Open(filename)) + { + gpDLSBanks.push_back(std::move(bank)); + return true; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } catch(const std::exception &) + { + } + return false; +} + + +size_t CTrackApp::AddScannedDLSBanks() +{ + if(!m_scannedDlsBanks.valid()) + return 0; + + size_t numAdded = 0; + auto scannedBanks = m_scannedDlsBanks.get(); + gpDLSBanks.reserve(gpDLSBanks.size() + scannedBanks.size()); + const size_t existingBanks = gpDLSBanks.size(); + for(auto &bank : scannedBanks) + { + if(std::find_if(gpDLSBanks.begin(), gpDLSBanks.begin() + existingBanks, [&bank](const auto &other) { return other && *bank == *other; }) == gpDLSBanks.begin() + existingBanks) + { + gpDLSBanks.push_back(std::move(bank)); + numAdded++; + } + } + return numAdded; +} + + +///////////////////////////////////////////////////////////////////////////// +// CTrackApp + +MODTYPE CTrackApp::m_nDefaultDocType = MOD_TYPE_IT; + +BEGIN_MESSAGE_MAP(CTrackApp, CWinApp) + //{{AFX_MSG_MAP(CTrackApp) + ON_COMMAND(ID_FILE_NEW, &CTrackApp::OnFileNew) + ON_COMMAND(ID_FILE_NEWMOD, &CTrackApp::OnFileNewMOD) + ON_COMMAND(ID_FILE_NEWS3M, &CTrackApp::OnFileNewS3M) + ON_COMMAND(ID_FILE_NEWXM, &CTrackApp::OnFileNewXM) + ON_COMMAND(ID_FILE_NEWIT, &CTrackApp::OnFileNewIT) + ON_COMMAND(ID_NEW_MPT, &CTrackApp::OnFileNewMPT) + ON_COMMAND(ID_FILE_OPEN, &CTrackApp::OnFileOpen) + ON_COMMAND(ID_FILE_CLOSEALL, &CTrackApp::OnFileCloseAll) + ON_COMMAND(ID_APP_ABOUT, &CTrackApp::OnAppAbout) + ON_UPDATE_COMMAND_UI(ID_FILE_CLOSEALL, &CTrackApp::OnUpdateAnyDocsOpen) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +///////////////////////////////////////////////////////////////////////////// +// CTrackApp construction + +CTrackApp::CTrackApp() +{ + m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART | AFX_RESTART_MANAGER_REOPEN_PREVIOUS_FILES; +} + + +class OpenMPTDataRecoveryHandler + : public CDataRecoveryHandler +{ +public: + OpenMPTDataRecoveryHandler(_In_ DWORD dwRestartManagerSupportFlags, _In_ int nAutosaveInterval) + : CDataRecoveryHandler(dwRestartManagerSupportFlags, nAutosaveInterval) + { + return; + } + ~OpenMPTDataRecoveryHandler() override = default; + + BOOL SaveOpenDocumentList() override + { + BOOL bRet = TRUE; // return FALSE if document list non-empty and not saved + + POSITION posAutosave = m_mapDocNameToAutosaveName.GetStartPosition(); + if (posAutosave != NULL) + { + bRet = FALSE; + + // Save the open document list and associated autosave info to the registry + IniFileSettingsBackend ini(theApp.GetConfigPath() + P_("restart.") + mpt::PathString::FromCString(GetRestartIdentifier()) + P_(".ini")); + ini.ConvertToUnicode(); + int32 count = 0; + while (posAutosave != NULL) + { + CString strDocument, strAutosave; + m_mapDocNameToAutosaveName.GetNextAssoc(posAutosave, strDocument, strAutosave); + + ini.WriteSetting({ U_("RestartDocument"), mpt::ufmt::val(count) }, SettingValue(mpt::ToUnicode(strDocument))); + ini.WriteSetting({ U_("RestartAutosave"), mpt::ufmt::val(count) }, SettingValue(mpt::ToUnicode(strAutosave))); + count++; + } + ini.WriteSetting({ U_("Restart"), U_("Count") }, SettingValue(count)); + + return TRUE; + } + + return bRet; + } + + BOOL ReadOpenDocumentList() override + { + BOOL bRet = FALSE; // return TRUE only if at least one document was found + + { + IniFileSettingsBackend ini(theApp.GetConfigPath() + P_("restart.") + mpt::PathString::FromCString(GetRestartIdentifier()) + P_(".ini")); + int32 count = ini.ReadSetting({ U_("Restart"), U_("Count") }, SettingValue(0)); + + for(int32 index = 0; index < count; ++index) + { + mpt::ustring document = ini.ReadSetting({ U_("RestartDocument"), mpt::ufmt::val(index) }, SettingValue(U_(""))); + mpt::ustring autosave = ini.ReadSetting({ U_("RestartAutosave"), mpt::ufmt::val(index) }, SettingValue(U_(""))); + if(!document.empty()) + { + m_mapDocNameToAutosaveName[mpt::ToCString(document)] = mpt::ToCString(autosave); + bRet = TRUE; + } + } + + ini.RemoveSection(U_("Restart")); + ini.RemoveSection(U_("RestartDocument")); + ini.RemoveSection(U_("RestartAutosave")); + + } + + DeleteFile((theApp.GetConfigPath() + P_("restart.") + mpt::PathString::FromCString(GetRestartIdentifier()) + P_(".ini")).AsNative().c_str()); + + return bRet; + } + +}; + + +CDataRecoveryHandler *CTrackApp::GetDataRecoveryHandler() +{ + static BOOL bTriedOnce = FALSE; + + // Since the application restart and application recovery are supported only on Windows + // Vista and above, we don't need a recovery handler on Windows versions less than Vista. + if (SupportsRestartManager() || SupportsApplicationRecovery()) + { + if (!bTriedOnce && m_pDataRecoveryHandler == NULL) + { + m_pDataRecoveryHandler = new OpenMPTDataRecoveryHandler(m_dwRestartManagerSupportFlags, m_nAutosaveInterval); + if (!m_pDataRecoveryHandler->Initialize()) + { + delete m_pDataRecoveryHandler; + m_pDataRecoveryHandler = NULL; + } + } + } + + bTriedOnce = TRUE; + return m_pDataRecoveryHandler; +} + + +void CTrackApp::AddToRecentFileList(LPCTSTR lpszPathName) +{ + AddToRecentFileList(mpt::PathString::FromCString(lpszPathName)); +} + + +void CTrackApp::AddToRecentFileList(const mpt::PathString &path) +{ + RemoveMruItem(path); + TrackerSettings::Instance().mruFiles.insert(TrackerSettings::Instance().mruFiles.begin(), path); + if(TrackerSettings::Instance().mruFiles.size() > TrackerSettings::Instance().mruListLength) + { + TrackerSettings::Instance().mruFiles.resize(TrackerSettings::Instance().mruListLength); + } + CMainFrame::GetMainFrame()->UpdateMRUList(); +} + + +void CTrackApp::RemoveMruItem(const size_t item) +{ + if(item < TrackerSettings::Instance().mruFiles.size()) + { + TrackerSettings::Instance().mruFiles.erase(TrackerSettings::Instance().mruFiles.begin() + item); + CMainFrame::GetMainFrame()->UpdateMRUList(); + } +} + + +void CTrackApp::RemoveMruItem(const mpt::PathString &path) +{ + auto &mruFiles = TrackerSettings::Instance().mruFiles; + for(auto i = mruFiles.begin(); i != mruFiles.end(); i++) + { + if(!mpt::PathString::CompareNoCase(*i, path)) + { + mruFiles.erase(i); + break; + } + } +} + + +///////////////////////////////////////////////////////////////////////////// +// CTrackApp initialization + + +namespace Tracker +{ +mpt::recursive_mutex_with_lock_count & GetGlobalMutexRef() +{ + return theApp.GetGlobalMutexRef(); +} +} // namespace Tracker + + +class ComponentManagerSettings + : public IComponentManagerSettings +{ +private: + TrackerSettings &conf; + mpt::PathString configPath; +public: + ComponentManagerSettings(TrackerSettings &conf, const mpt::PathString &configPath) + : conf(conf) + , configPath(configPath) + { + return; + } + bool LoadOnStartup() const override + { + return conf.ComponentsLoadOnStartup; + } + bool KeepLoaded() const override + { + return conf.ComponentsKeepLoaded; + } + bool IsBlocked(const std::string &key) const override + { + return conf.IsComponentBlocked(key); + } + mpt::PathString Path() const override + { + if(mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()).empty()) + { + return mpt::PathString(); + } + return configPath + P_("Components\\") + mpt::PathString::FromUnicode(mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())) + P_("\\"); + } +}; + + +// Move a config file called fileName from the App's directory (or one of its sub directories specified by subDir) to +// %APPDATA%. If specified, it will be renamed to newFileName. Existing files are never overwritten. +// Returns true on success. +bool CTrackApp::MoveConfigFile(const mpt::PathString &fileName, mpt::PathString subDir, mpt::PathString newFileName) +{ + const mpt::PathString oldPath = GetInstallPath() + subDir + fileName; + mpt::PathString newPath = GetConfigPath() + subDir; + if(!newFileName.empty()) + newPath += newFileName; + else + newPath += fileName; + + if(!newPath.IsFile() && oldPath.IsFile()) + { + return MoveFile(oldPath.AsNative().c_str(), newPath.AsNative().c_str()) != 0; + } + return false; +} + + +// Set up paths were configuration data is written to. Set overridePortable to true if application's own directory should always be used. +void CTrackApp::SetupPaths(bool overridePortable) +{ + + // First, determine if the executable is installed in multi-arch mode or in the old standard mode. + bool modeMultiArch = false; + bool modeSourceProject = false; + const mpt::PathString exePath = mpt::GetExecutablePath(); + auto exePathComponents = mpt::String::Split<mpt::ustring>(exePath.GetDir().WithoutTrailingSlash().ToUnicode(), P_("\\").ToUnicode()); + if(exePathComponents.size() >= 2) + { + if(exePathComponents[exePathComponents.size()-1] == mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())) + { + if(exePathComponents[exePathComponents.size()-2] == U_("bin")) + { + modeMultiArch = true; + } + } + } + // Check if we are running from the source tree. + if(!modeMultiArch && exePathComponents.size() >= 4) + { + if(exePathComponents[exePathComponents.size()-1] == mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture())) + { + if(exePathComponents[exePathComponents.size()-4] == U_("bin")) + { + modeSourceProject = true; + } + } + } + if(modeSourceProject) + { + m_InstallPath = mpt::GetAbsolutePath(exePath + P_("..\\") + P_("..\\") + P_("..\\") + P_("..\\")); + m_InstallBinPath = mpt::GetAbsolutePath(exePath + P_("..\\")); + m_InstallBinArchPath = exePath; + m_InstallPkgPath = mpt::GetAbsolutePath(exePath + P_("..\\") + P_("..\\") + P_("..\\") + P_("..\\packageTemplate\\")); + } else if(modeMultiArch) + { + m_InstallPath = mpt::GetAbsolutePath(exePath + P_("..\\") + P_("..\\")); + m_InstallBinPath = mpt::GetAbsolutePath(exePath + P_("..\\")); + m_InstallBinArchPath = exePath; + m_InstallPkgPath = mpt::GetAbsolutePath(exePath + P_("..\\") + P_("..\\")); + } else + { + m_InstallPath = exePath; + m_InstallBinPath = exePath; + m_InstallBinArchPath = exePath; + m_InstallPkgPath = exePath; + } + + // Determine paths, portable mode, first run. Do not yet update any state. + mpt::PathString configPathPortable = (modeSourceProject ? exePath : m_InstallPath); // config path in portable mode + mpt::PathString configPathUser; // config path in default non-portable mode + { + // Try to find a nice directory where we should store our settings (default: %APPDATA%) + TCHAR dir[MAX_PATH] = { 0 }; + if((SHGetFolderPath(NULL, CSIDL_APPDATA, NULL, SHGFP_TYPE_CURRENT, dir) == S_OK) + || (SHGetFolderPath(NULL, CSIDL_MYDOCUMENTS, NULL, SHGFP_TYPE_CURRENT, dir) == S_OK)) + { + // Store our app settings in %APPDATA% or "My Documents" + configPathUser = mpt::PathString::FromNative(dir) + P_("\\OpenMPT\\"); + } + } + + // Check if the user has configured portable mode. + bool configInstallPortable = false; + mpt::PathString portableFlagFilename = (configPathPortable + P_("OpenMPT.portable")); + bool configPortableFlag = portableFlagFilename.IsFile(); + configInstallPortable = configInstallPortable || configPortableFlag; + // before 1.29.00.13: + configInstallPortable = configInstallPortable || (GetPrivateProfileInt(_T("Paths"), _T("UseAppDataDirectory"), 1, (configPathPortable + P_("mptrack.ini")).AsNative().c_str()) == 0); + // convert to new style + if(configInstallPortable && !configPortableFlag) + { + mpt::SafeOutputFile f(portableFlagFilename); + } + + // Determine portable mode. + bool portableMode = overridePortable || configInstallPortable || configPathUser.empty(); + + // Update config dir + m_ConfigPath = portableMode ? configPathPortable : configPathUser; + + // Set up default file locations + m_szConfigFileName = m_ConfigPath + P_("mptrack.ini"); // config file + m_szPluginCacheFileName = m_ConfigPath + P_("plugin.cache"); // plugin cache + + // Force use of custom ini file rather than windowsDir\executableName.ini + if(m_pszProfileName) + { + free((void *)m_pszProfileName); + } + m_pszProfileName = _tcsdup(m_szConfigFileName.ToCString()); + + m_bInstallerMode = !modeSourceProject && !portableMode; + m_bPortableMode = portableMode; + m_bSourceTreeMode = modeSourceProject; + +} + + +void CTrackApp::CreatePaths() +{ + // Create missing diretories + if(!IsPortableMode()) + { + if(!m_ConfigPath.IsDirectory()) + { + CreateDirectory(m_ConfigPath.AsNative().c_str(), 0); + } + } + if(!(GetConfigPath() + P_("Components")).IsDirectory()) + { + CreateDirectory((GetConfigPath() + P_("Components")).AsNative().c_str(), 0); + } + if(!(GetConfigPath() + P_("Components\\") + mpt::PathString::FromUnicode(mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()))).IsDirectory()) + { + CreateDirectory((GetConfigPath() + P_("Components\\") + mpt::PathString::FromUnicode(mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()))).AsNative().c_str(), 0); + } + + // Handle updates from old versions. + + if(!IsPortableMode()) + { + + // Move the config files if they're still in the old place. + MoveConfigFile(P_("mptrack.ini")); + MoveConfigFile(P_("plugin.cache")); + + // Import old tunings + const mpt::PathString oldTunings = GetInstallPath() + P_("tunings\\"); + + if(oldTunings.IsDirectory()) + { + const mpt::PathString searchPattern = oldTunings + P_("*.*"); + WIN32_FIND_DATA FindFileData; + HANDLE hFind; + hFind = FindFirstFile(searchPattern.AsNative().c_str(), &FindFileData); + if(hFind != INVALID_HANDLE_VALUE) + { + do + { + MoveConfigFile(mpt::PathString::FromNative(FindFileData.cFileName), P_("tunings\\")); + } while(FindNextFile(hFind, &FindFileData) != 0); + } + FindClose(hFind); + RemoveDirectory(oldTunings.AsNative().c_str()); + } + + } + +} + + +#if !defined(MPT_BUILD_RETRO) + +bool CTrackApp::CheckSystemSupport() +{ + const mpt::ustring lf = U_("\n"); + const mpt::ustring url = Build::GetURL(Build::Url::Download); + if(!BuildVariants::ProcessorCanRunCurrentBuild()) + { + mpt::ustring text; + text += U_("Your CPU is too old to run this variant of OpenMPT.") + lf; + text += U_("OpenMPT will exit now.") + lf; + Reporting::Error(text, "OpenMPT"); + return false; + } + if(BuildVariants::IsKnownSystem() && !BuildVariants::SystemCanRunCurrentBuild()) + { + mpt::ustring text; + text += U_("Your system does not meet the minimum requirements for this variant of OpenMPT.") + lf; + if(mpt::OS::Windows::IsOriginal()) + { + text += U_("OpenMPT will exit now.") + lf; + } + Reporting::Error(text, "OpenMPT"); + if(mpt::OS::Windows::IsOriginal()) + { + return false; + } else + { + return true; // may work though + } + } + return true; +} + +#endif // !MPT_BUILD_RETRO + + +BOOL CTrackApp::InitInstanceEarly(CMPTCommandLineInfo &cmdInfo) +{ + // The first step of InitInstance, always executed without any crash handler. + + #ifndef UNICODE + if(MessageBox(NULL, + _T("STOP!!!") _T("\n") + _T("This is an ANSI (as opposed to a UNICODE) build of OpenMPT.") _T("\n") + _T("\n") + _T("ANSI builds are NOT SUPPORTED and WILL CAUSE CORRUPTION of the OpenMPT configuration and exhibit other unintended behaviour.") _T("\n") + _T("\n") + _T("Please use an official build of OpenMPT or compile 'OpenMPT.sln' instead of 'OpenMPT-ANSI.sln'.") _T("\n") + _T("\n") + _T("Continue starting OpenMPT anyway?") _T("\n"), + _T("OpenMPT"), MB_ICONSTOP | MB_YESNO| MB_DEFBUTTON2) + != IDYES) + { + ExitProcess(1); + } + #endif + + // Call the base class. + // This is required for MFC RestartManager integration. + if(!CWinApp::InitInstance()) + { + return FALSE; + } + + #if MPT_COMPILER_MSVC + _CrtSetDebugFillThreshold(0); // Disable buffer filling in secure enhanced CRT functions. + #endif + + // Avoid e.g. audio APIs trying to load wdmaud.drv from arbitrary working directory + ::SetCurrentDirectory(mpt::GetExecutablePath().AsNative().c_str()); + + // Initialize OLE MFC support + BOOL oleinit = AfxOleInit(); + ASSERT(oleinit != FALSE); // no MPT_ASSERT here! + + // Parse command line for standard shell commands, DDE, file open + ParseCommandLine(cmdInfo); + + // Set up paths to store configuration in + SetupPaths(cmdInfo.m_portable); + + if(cmdInfo.m_sharedInstance && IPCWindow::SendToIPC(cmdInfo.m_fileNames)) + { + ExitProcess(0); + } + + // Initialize DocManager (for DDE) + // requires mpt::PathString + ASSERT(nullptr == m_pDocManager); // no MPT_ASSERT here! + m_pDocManager = new CModDocManager(); + + if(IsDebuggerPresent() && cmdInfo.m_debugCrashHandler) + { + ExceptionHandler::useAnyCrashHandler = true; + ExceptionHandler::useImplicitFallbackSEH = false; + ExceptionHandler::useExplicitSEH = true; + ExceptionHandler::handleStdTerminate = true; + ExceptionHandler::handleMfcExceptions = true; + ExceptionHandler::debugExceptionHandler = true; + } else if(IsDebuggerPresent() || cmdInfo.m_noCrashHandler) + { + ExceptionHandler::useAnyCrashHandler = false; + ExceptionHandler::useImplicitFallbackSEH = false; + ExceptionHandler::useExplicitSEH = false; + ExceptionHandler::handleStdTerminate = false; + ExceptionHandler::handleMfcExceptions = false; + ExceptionHandler::debugExceptionHandler = false; + } else + { + ExceptionHandler::useAnyCrashHandler = true; + ExceptionHandler::useImplicitFallbackSEH = true; + ExceptionHandler::useExplicitSEH = true; + ExceptionHandler::handleStdTerminate = true; + ExceptionHandler::handleMfcExceptions = true; + ExceptionHandler::debugExceptionHandler = false; + } + + return TRUE; +} + + +BOOL CTrackApp::InitInstanceImpl(CMPTCommandLineInfo &cmdInfo) +{ + + m_GuiThreadId = GetCurrentThreadId(); + + mpt::log::Trace::SetThreadId(mpt::log::Trace::ThreadKindGUI, m_GuiThreadId); + + if(ExceptionHandler::useAnyCrashHandler) + { + ExceptionHandler::Register(); + } + + // Start loading + BeginWaitCursor(); + + MPT_LOG_GLOBAL(LogInformation, "", U_("OpenMPT Start")); + + // create the tracker-global random device + m_RD = std::make_unique<mpt::random_device>(); + // make the device available to non-tracker-only code + mpt::set_global_random_device(m_RD.get()); + // create and seed the traker-global best PRNG with the random device + m_PRNG = std::make_unique<mpt::thread_safe_prng<mpt::default_prng> >(mpt::make_prng<mpt::default_prng>(RandomDevice())); + // make the best PRNG available to non-tracker-only code + mpt::set_global_prng(m_PRNG.get()); + // additionally, seed the C rand() PRNG, just in case any third party library calls rand() + mpt::crand::reseed(RandomDevice()); + + m_Gdiplus = std::make_unique<GdiplusRAII>(); + + if(cmdInfo.m_noWine) + { + mpt::OS::Windows::PreventWineDetection(); + } + + #ifdef MPT_ENABLE_ARCH_INTRINSICS + if(!cmdInfo.m_noAssembly) + { + CPU::EnableAvailableFeatures(); + } + #endif // MPT_ENABLE_ARCH_INTRINSICS + + if(mpt::OS::Windows::IsWine()) + { + SetWineVersion(std::make_shared<mpt::OS::Wine::VersionContext>()); + } + + // Create paths to store configuration in + CreatePaths(); + + m_pSettingsIniFile = new IniFileSettingsBackend(m_szConfigFileName); + m_pSettings = new SettingsContainer(m_pSettingsIniFile); + + m_pDebugSettings = new DebugSettings(*m_pSettings); + + m_pTrackerSettings = new TrackerSettings(*m_pSettings); + + MPT_LOG_GLOBAL(LogInformation, "", U_("OpenMPT settings initialized.")); + + if(ExceptionHandler::useAnyCrashHandler) + { + ExceptionHandler::ConfigureSystemHandler(); + } + + if(TrackerSettings::Instance().MiscUseSingleInstance && IPCWindow::SendToIPC(cmdInfo.m_fileNames)) + { + ExitProcess(0); + } + + IPCWindow::Open(m_hInstance); + + m_pSongSettingsIniFile = new IniFileSettingsBackend(GetConfigPath() + P_("SongSettings.ini")); + m_pSongSettings = new SettingsContainer(m_pSongSettingsIniFile); + + m_pComponentManagerSettings = new ComponentManagerSettings(TrackerSettings::Instance(), GetConfigPath()); + + m_pPluginCache = new IniFileSettingsContainer(m_szPluginCacheFileName); + + // Load standard INI file options (without MRU) + // requires SetupPaths+CreatePaths called + LoadStdProfileSettings(0); + + // Set process priority class + #ifndef _DEBUG + SetPriorityClass(GetCurrentProcess(), TrackerSettings::Instance().MiscProcessPriorityClass); + #endif + + // Dynamic DPI-awareness. Some users might want to disable DPI-awareness because of their DPI-unaware VST plugins. + bool setDPI = false; + // For Windows 10, Creators Update (1703) and newer + { + mpt::Library user32(mpt::LibraryPath::System(P_("user32"))); + if (user32.IsValid()) + { + enum MPT_DPI_AWARENESS_CONTEXT + { + MPT_DPI_AWARENESS_CONTEXT_UNAWARE = -1, + MPT_DPI_AWARENESS_CONTEXT_SYSTEM_AWARE = -2, + MPT_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE = -3, + MPT_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2 = -4, + MPT_DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED = -5, // 1809 update and newer + }; + using PSETPROCESSDPIAWARENESSCONTEXT = BOOL(WINAPI *)(HANDLE); + PSETPROCESSDPIAWARENESSCONTEXT SetProcessDpiAwarenessContext = nullptr; + if(user32.Bind(SetProcessDpiAwarenessContext, "SetProcessDpiAwarenessContext")) + { + if (TrackerSettings::Instance().highResUI) + { + setDPI = (SetProcessDpiAwarenessContext(HANDLE(MPT_DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)) == TRUE); + } else + { + if (SetProcessDpiAwarenessContext(HANDLE(MPT_DPI_AWARENESS_CONTEXT_UNAWARE_GDISCALED)) == TRUE) + setDPI = true; + else + setDPI = (SetProcessDpiAwarenessContext(HANDLE(MPT_DPI_AWARENESS_CONTEXT_UNAWARE)) == TRUE); + } + } + } + } + // For Windows 8.1 and newer + if(!setDPI) + { + mpt::Library shcore(mpt::LibraryPath::System(P_("SHCore"))); + if(shcore.IsValid()) + { + using PSETPROCESSDPIAWARENESS = HRESULT (WINAPI *)(int); + PSETPROCESSDPIAWARENESS SetProcessDPIAwareness = nullptr; + if(shcore.Bind(SetProcessDPIAwareness, "SetProcessDpiAwareness")) + { + setDPI = (SetProcessDPIAwareness(TrackerSettings::Instance().highResUI ? 2 : 0) == S_OK); + } + } + } + // For Vista and newer + if(!setDPI && TrackerSettings::Instance().highResUI) + { + mpt::Library user32(mpt::LibraryPath::System(P_("user32"))); + if(user32.IsValid()) + { + using PSETPROCESSDPIAWARE = BOOL (WINAPI *)(); + PSETPROCESSDPIAWARE SetProcessDPIAware = nullptr; + if(user32.Bind(SetProcessDPIAware, "SetProcessDPIAware")) + { + SetProcessDPIAware(); + } + } + } + + // create main MDI Frame window + CMainFrame* pMainFrame = new CMainFrame(); + if(!pMainFrame->LoadFrame(IDR_MAINFRAME)) return FALSE; + m_pMainWnd = pMainFrame; + + // Show splash screen + if(cmdInfo.m_bShowSplash && TrackerSettings::Instance().m_ShowSplashScreen) + { + StartSplashScreen(); + } + + // create component manager + ComponentManager::Init(*m_pComponentManagerSettings); + + // load components + ComponentManager::Instance()->Startup(); + + // Wine Support + if(mpt::OS::Windows::IsWine()) + { + WineIntegration::Initialize(); + WineIntegration::Load(); + } + + // Register document templates + m_pModTemplate = new CModDocTemplate( + IDR_MODULETYPE, + RUNTIME_CLASS(CModDoc), + RUNTIME_CLASS(CChildFrame), // custom MDI child frame + RUNTIME_CLASS(CModControlView)); + AddDocTemplate(m_pModTemplate); + + // Load Midi Library + ImportMidiConfig(theApp.GetSettings(), {}, true); + + // Enable DDE Execute open + // requires m_pDocManager + EnableShellOpen(); + + // Enable drag/drop open + m_pMainWnd->DragAcceptFiles(); + + // Load sound APIs + // requires TrackerSettings + m_pAllSoundDeviceComponents = std::make_unique<AllSoundDeviceComponents>(); + auto GetSysInfo = [&]() + { + if(mpt::OS::Windows::IsWine()) + { + return SoundDevice::SysInfo(mpt::osinfo::get_class(), mpt::OS::Windows::Version::Current(), mpt::OS::Windows::IsWine(), GetWineVersion()->HostClass(), GetWineVersion()->Version()); + } + return SoundDevice::SysInfo(mpt::osinfo::get_class(), mpt::OS::Windows::Version::Current(), mpt::OS::Windows::IsWine(), mpt::osinfo::osclass::Unknown, mpt::osinfo::windows::wine::version()); + }; + SoundDevice::SysInfo sysInfo = GetSysInfo(); + SoundDevice::AppInfo appInfo; + appInfo.SetName(U_("OpenMPT")); + appInfo.SetHWND(*m_pMainWnd); + appInfo.BoostedThreadPriorityXP = TrackerSettings::Instance().SoundBoostedThreadPriority; + appInfo.BoostedThreadMMCSSClassVista = TrackerSettings::Instance().SoundBoostedThreadMMCSSClass; + appInfo.BoostedThreadRealtimePosix = TrackerSettings::Instance().SoundBoostedThreadRealtimePosix; + appInfo.BoostedThreadNicenessPosix = TrackerSettings::Instance().SoundBoostedThreadNicenessPosix; + appInfo.BoostedThreadRtprioPosix = TrackerSettings::Instance().SoundBoostedThreadRtprioPosix; + appInfo.MaskDriverCrashes = TrackerSettings::Instance().SoundMaskDriverCrashes; + appInfo.AllowDeferredProcessing = TrackerSettings::Instance().SoundAllowDeferredProcessing; + std::vector<std::shared_ptr<SoundDevice::IDevicesEnumerator>> deviceEnumerators = SoundDevice::Manager::GetEnabledEnumerators(*m_pAllSoundDeviceComponents); + deviceEnumerators.push_back(std::static_pointer_cast<SoundDevice::IDevicesEnumerator>(std::make_shared<SoundDevice::DevicesEnumerator<SoundDevice::SoundDeviceStub>>())); + m_pSoundDevicesManager = std::make_unique<SoundDevice::Manager>(m_GlobalLogger, sysInfo, appInfo, std::move(deviceEnumerators)); + m_pTrackerSettings->MigrateOldSoundDeviceSettings(*m_pSoundDevicesManager); + + // Set default note names + CSoundFile::SetDefaultNoteNames(); + + // Load DLS Banks + if (!cmdInfo.m_noDls) + m_scannedDlsBanks = LoadDefaultDLSBanks(); + + // Initialize Plugins + if (!cmdInfo.m_noPlugins) InitializeDXPlugins(); + + // Initialize CMainFrame + pMainFrame->Initialize(); + InitCommonControls(); + pMainFrame->m_InputHandler->UpdateMainMenu(); + + // Dispatch commands specified on the command line + if(cmdInfo.m_nShellCommand == CCommandLineInfo::FileNew) + { + // When not asked to open any existing file, + // we do not want to open an empty new one on startup. + cmdInfo.m_nShellCommand = CCommandLineInfo::FileNothing; + } + bool shellSuccess = false; + if(cmdInfo.m_fileNames.empty()) + { + shellSuccess = ProcessShellCommand(cmdInfo) != FALSE; + } else + { + cmdInfo.m_nShellCommand = CCommandLineInfo::FileOpen; + for(const auto &filename : cmdInfo.m_fileNames) + { + cmdInfo.m_strFileName = filename.ToCString(); + shellSuccess |= ProcessShellCommand(cmdInfo) != FALSE; + } + } + if(!shellSuccess) + { + EndWaitCursor(); + StopSplashScreen(); + return FALSE; + } + + pMainFrame->ShowWindow(m_nCmdShow); + pMainFrame->UpdateWindow(); + + EndWaitCursor(); + + + // Perform startup tasks. + +#if !defined(MPT_BUILD_RETRO) + // Check whether we are running the best build for the given system. + if(!cmdInfo.m_noSysCheck) + { + if(!CheckSystemSupport()) + { + StopSplashScreen(); + return FALSE; + } + } +#endif // !MPT_BUILD_RETRO + + if(TrackerSettings::Instance().FirstRun) + { + // On high-DPI devices, automatically upscale pattern font + FontSetting font = TrackerSettings::Instance().patternFont; + font.size = Clamp(Util::GetDPIy(m_pMainWnd->m_hWnd) / 96 - 1, 0, 9); + TrackerSettings::Instance().patternFont = font; + new WelcomeDlg(m_pMainWnd); + } else + { +#if !defined(MPT_BUILD_RETRO) + bool deprecatedSoundDevice = GetSoundDevicesManager()->FindDeviceInfo(TrackerSettings::Instance().GetSoundDeviceIdentifier()).IsDeprecated(); + bool showSettings = deprecatedSoundDevice && !TrackerSettings::Instance().m_SoundDeprecatedDeviceWarningShown && (Reporting::Confirm( + U_("You have currently selected a sound device which is deprecated. MME/WaveOut support will be removed in a future OpenMPT version.\n") + + U_("The recommended sound device type is WASAPI.\n") + + U_("Do you want to change your sound device settings now?"), + U_("OpenMPT - Deprecated sound device") + ) == cnfYes); + if(showSettings) + { + TrackerSettings::Instance().m_SoundDeprecatedDeviceWarningShown = true; + m_pMainWnd->PostMessage(WM_COMMAND, ID_VIEW_OPTIONS); + } +#endif // !MPT_BUILD_RETRO + } + +#ifdef ENABLE_TESTS + if(!cmdInfo.m_noTests) + Test::DoTests(); +#endif + + if(TrackerSettings::Instance().m_SoundSettingsOpenDeviceAtStartup) + { + pMainFrame->InitPreview(); + pMainFrame->PreparePreview(NOTE_NOTECUT, 0); + pMainFrame->PlayPreview(); + } + + if(!TrackerSettings::Instance().FirstRun) + { +#if defined(MPT_ENABLE_UPDATE) + if(CUpdateCheck::IsSuitableUpdateMoment()) + { + CUpdateCheck::DoAutoUpdateCheck(); + } +#endif // MPT_ENABLE_UPDATE + } + + return TRUE; +} + + +BOOL CTrackApp::InitInstance() +{ + CMPTCommandLineInfo cmdInfo; + if(!InitInstanceEarly(cmdInfo)) + { + return FALSE; + } + return InitInstanceLate(cmdInfo); +} + + +BOOL CTrackApp::InitInstanceLate(CMPTCommandLineInfo &cmdInfo) +{ + BOOL result = FALSE; + if(ExceptionHandler::useExplicitSEH) + { + // https://support.microsoft.com/en-us/kb/173652 + __try + { + result = InitInstanceImpl(cmdInfo); + } __except(ExceptionHandler::ExceptionFilter(GetExceptionInformation())) + { + std::abort(); + } + } else + { + result = InitInstanceImpl(cmdInfo); + } + return result; +} + + +int CTrackApp::Run() +{ + int result = 255; + if(ExceptionHandler::useExplicitSEH) + { + // https://support.microsoft.com/en-us/kb/173652 + __try + { + result = CWinApp::Run(); + } __except(ExceptionHandler::ExceptionFilter(GetExceptionInformation())) + { + std::abort(); + } + } else + { + result = CWinApp::Run(); + } + return result; +} + + +LRESULT CTrackApp::ProcessWndProcException(CException * e, const MSG * pMsg) +{ + if(ExceptionHandler::handleMfcExceptions) + { + LRESULT result = 0L; // as per documentation + if(pMsg) + { + if(pMsg->message == WM_COMMAND) + { + result = (LRESULT)TRUE; // as per documentation + } + } + if(dynamic_cast<CMemoryException*>(e)) + { + e->ReportError(); + //ExceptionHandler::UnhandledMFCException(e, pMsg); + } else + { + ExceptionHandler::UnhandledMFCException(e, pMsg); + } + return result; + } else + { + return CWinApp::ProcessWndProcException(e, pMsg); + } +} + + +int CTrackApp::ExitInstance() +{ + int result = 0; + if(ExceptionHandler::useExplicitSEH) + { + // https://support.microsoft.com/en-us/kb/173652 + __try + { + result = ExitInstanceImpl(); + } __except(ExceptionHandler::ExceptionFilter(GetExceptionInformation())) + { + std::abort(); + } + } else + { + result = ExitInstanceImpl(); + } + return result; +} + + +int CTrackApp::ExitInstanceImpl() +{ + IPCWindow::Close(); + + m_pSoundDevicesManager = nullptr; + m_pAllSoundDeviceComponents = nullptr; + ExportMidiConfig(theApp.GetSettings()); + AddScannedDLSBanks(); + SaveDefaultDLSBanks(); + gpDLSBanks.clear(); + + // Uninitialize Plugins + UninitializeDXPlugins(); + + ComponentManager::Release(); + + delete m_pPluginCache; + m_pPluginCache = nullptr; + delete m_pComponentManagerSettings; + m_pComponentManagerSettings = nullptr; + delete m_pTrackerSettings; + m_pTrackerSettings = nullptr; + delete m_pDebugSettings; + m_pDebugSettings = nullptr; + delete m_pSettings; + m_pSettings = nullptr; + delete m_pSettingsIniFile; + m_pSettingsIniFile = nullptr; + delete m_pSongSettings; + m_pSongSettings = nullptr; + delete m_pSongSettingsIniFile; + m_pSongSettingsIniFile = nullptr; + + if(mpt::OS::Windows::IsWine()) + { + SetWineVersion(nullptr); + } + + m_Gdiplus.reset(); + + mpt::set_global_prng(nullptr); + m_PRNG.reset(); + mpt::set_global_random_device(nullptr); + m_RD.reset(); + + if(ExceptionHandler::useAnyCrashHandler) + { + ExceptionHandler::UnconfigureSystemHandler(); + ExceptionHandler::Unregister(); + } + + return CWinApp::ExitInstance(); +} + + +//////////////////////////////////////////////////////////////////////////////// +// App Messages + + +CModDoc *CTrackApp::NewDocument(MODTYPE newType) +{ + // Build from template + if(newType == MOD_TYPE_NONE) + { + const mpt::PathString templateFile = TrackerSettings::Instance().defaultTemplateFile; + if(TrackerSettings::Instance().defaultNewFileAction == nfDefaultTemplate && !templateFile.empty()) + { + // Template file can be either a filename inside one of the preset and user TemplateModules folders, or a full path. + const mpt::PathString dirs[] = { GetConfigPath() + P_("TemplateModules\\"), GetInstallPath() + P_("TemplateModules\\"), mpt::PathString() }; + for(const auto &dir : dirs) + { + if((dir + templateFile).IsFile()) + { + if(CModDoc *modDoc = static_cast<CModDoc *>(m_pModTemplate->OpenTemplateFile(dir + templateFile))) + { + return modDoc; + } + } + } + } + + + // Default module type + newType = TrackerSettings::Instance().defaultModType; + + // Get active document to make the new module of the same type + CModDoc *pModDoc = CMainFrame::GetMainFrame()->GetActiveDoc(); + if(pModDoc != nullptr && TrackerSettings::Instance().defaultNewFileAction == nfSameAsCurrent) + { + newType = pModDoc->GetSoundFile().GetBestSaveFormat(); + } + } + + SetDefaultDocType(newType); + return static_cast<CModDoc *>(m_pModTemplate->OpenDocumentFile(_T(""))); +} + + +void CTrackApp::OpenModulesDialog(std::vector<mpt::PathString> &files, const mpt::PathString &overridePath) +{ + files.clear(); + + static constexpr std::string_view commonExts[] = {"mod", "s3m", "xm", "it", "mptm", "mo3", "oxm", "nst", "stk", "m15", "pt36", "mid", "rmi", "smf", "wav", "mdz", "s3z", "xmz", "itz", "mdr"}; + std::string exts, extsWithoutCommon; + for(const auto &ext : CSoundFile::GetSupportedExtensions(true)) + { + const auto filter = std::string("*.") + ext + std::string(";"); + exts += filter; + if(!mpt::contains(commonExts, ext)) + extsWithoutCommon += filter; + } + + static int nFilterIndex = 0; + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .ExtensionFilter("All Modules (*.mptm,*.mod,*.xm,*.s3m,*.it,...)|" + exts + ";mod.*" + "|" + "Compressed Modules (*.mdz,*.s3z,*.xmz,*.itz,*.mo3,*.oxm,...)|*.mdz;*.s3z;*.xmz;*.itz;*.mdr;*.zip;*.rar;*.lha;*.pma;*.lzs;*.gz;*.mo3;*.oxm" + "|" + "ProTracker Modules (*.mod,*.nst)|*.mod;mod.*;*.mdz;*.nst;*.m15;*.stk;*.pt36|" + "Scream Tracker Modules (*.s3m,*.stm)|*.s3m;*.stm;*.s3z;*.stx|" + "FastTracker Modules (*.xm)|*.xm;*.xmz|" + "Impulse Tracker Modules (*.it)|*.it;*.itz|" + "OpenMPT Modules (*.mptm)|*.mptm;*.mptmz|" + "Other Modules (*.mtm,*.okt,*.mdl,*.669,*.far,...)|" + extsWithoutCommon + "|" + "Wave Files (*.wav)|*.wav|" + "MIDI Files (*.mid,*.rmi)|*.mid;*.rmi;*.smf|" + "All Files (*.*)|*.*||") + .WorkingDirectory(overridePath.empty() ? TrackerSettings::Instance().PathSongs.GetWorkingDir() : overridePath) + .FilterIndex(&nFilterIndex); + if(!dlg.Show()) return; + + if(overridePath.empty()) + TrackerSettings::Instance().PathSongs.SetWorkingDir(dlg.GetWorkingDirectory()); + + files = dlg.GetFilenames(); +} + + +void CTrackApp::OnFileOpen() +{ + FileDialog::PathList files; + OpenModulesDialog(files); + for(const auto &file : files) + { + OpenDocumentFile(file.ToCString()); + } +} + + +// App command to run the dialog +void CTrackApp::OnAppAbout() +{ + if (CAboutDlg::instance) return; + CAboutDlg::instance = new CAboutDlg(); + CAboutDlg::instance->Create(IDD_ABOUTBOX, m_pMainWnd); +} + + +///////////////////////////////////////////////////////////////////////////// +// Splash Screen + +class CSplashScreen: public CDialog +{ +protected: + std::unique_ptr<Gdiplus::Image> m_Image; + +public: + ~CSplashScreen(); + BOOL OnInitDialog() override; + void OnOK() override; + void OnCancel() override { OnOK(); } + void OnPaint(); + BOOL OnEraseBkgnd(CDC *) { return TRUE; } + + DECLARE_MESSAGE_MAP() +}; + +BEGIN_MESSAGE_MAP(CSplashScreen, CDialog) + ON_WM_PAINT() + ON_WM_ERASEBKGND() +END_MESSAGE_MAP() + +static CSplashScreen *gpSplashScreen = NULL; + +static DWORD64 gSplashScreenStartTime = 0; + + +CSplashScreen::~CSplashScreen() +{ + gpSplashScreen = nullptr; +} + + +void CSplashScreen::OnPaint() +{ + CPaintDC dc(this); + Gdiplus::Graphics gfx(dc); + + CRect rect; + GetClientRect(&rect); + gfx.SetInterpolationMode(Gdiplus::InterpolationModeHighQuality); + gfx.SetSmoothingMode(Gdiplus::SmoothingModeHighQuality); + gfx.DrawImage(m_Image.get(), 0, 0, rect.right, rect.bottom); + + CDialog::OnPaint(); +} + + +BOOL CSplashScreen::OnInitDialog() +{ + CDialog::OnInitDialog(); + + try + { + m_Image = GDIP::LoadPixelImage(GetResource(MAKEINTRESOURCE(IDB_SPLASHNOFOLDFIN), _T("PNG"))); + } catch(const bad_image &) + { + return FALSE; + } + + CRect rect; + GetWindowRect(&rect); + const int width = Util::ScalePixels(m_Image->GetWidth(), m_hWnd) / 2; + const int height = Util::ScalePixels(m_Image->GetHeight(), m_hWnd) / 2; + SetWindowPos(nullptr, + rect.left - ((width - rect.Width()) / 2), + rect.top - ((height - rect.Height()) / 2), + width, + height, + SWP_NOZORDER | SWP_NOCOPYBITS); + + return TRUE; +} + + +void CSplashScreen::OnOK() +{ + StopSplashScreen(); +} + + +static void StartSplashScreen() +{ + if(!gpSplashScreen) + { + gpSplashScreen = new CSplashScreen(); + gpSplashScreen->Create(IDD_SPLASHSCREEN, theApp.m_pMainWnd); + gpSplashScreen->ShowWindow(SW_SHOW); + gpSplashScreen->UpdateWindow(); + gpSplashScreen->BeginWaitCursor(); + gSplashScreenStartTime = Util::GetTickCount64(); + } +} + + +static void StopSplashScreen() +{ + if(gpSplashScreen) + { + gpSplashScreen->EndWaitCursor(); + gpSplashScreen->DestroyWindow(); + delete gpSplashScreen; + gpSplashScreen = nullptr; + } +} + + +static void TimeoutSplashScreen() +{ + if(gpSplashScreen) + { + if(Util::GetTickCount64() - gSplashScreenStartTime > 100) + { + StopSplashScreen(); + } + } +} + + +///////////////////////////////////////////////////////////////////////////// +// Idle-time processing + +BOOL CTrackApp::OnIdle(LONG lCount) +{ + BOOL b = CWinApp::OnIdle(lCount); + + TimeoutSplashScreen(); + + if(CMainFrame::GetMainFrame()) + { + CMainFrame::GetMainFrame()->IdleHandlerSounddevice(); + + if(m_scannedDlsBanksAvailable) + { + if(AddScannedDLSBanks()) + CMainFrame::GetMainFrame()->RefreshDlsBanks(); + } + } + + // Call plugins idle routine for open editor + if (m_pPluginManager) + { + DWORD curTime = timeGetTime(); + //rewbs.vstCompliance: call @ 50Hz + if (curTime - m_dwLastPluginIdleCall > 20 || curTime < m_dwLastPluginIdleCall) + { + m_pPluginManager->OnIdle(); + m_dwLastPluginIdleCall = curTime; + } + } + + return b; +} + + +///////////////////////////////////////////////////////////////////////////// +// DIB + + +RGBQUAD rgb2quad(COLORREF c) +{ + RGBQUAD r; + r.rgbBlue = GetBValue(c); + r.rgbGreen = GetGValue(c); + r.rgbRed = GetRValue(c); + r.rgbReserved = 0; + return r; +} + + +void DibBlt(HDC hdc, int x, int y, int sizex, int sizey, int srcx, int srcy, MODPLUGDIB *lpdib) +{ + if (!lpdib) return; + SetDIBitsToDevice( hdc, + x, + y, + sizex, + sizey, + srcx, + lpdib->bmiHeader.biHeight - srcy - sizey, + 0, + lpdib->bmiHeader.biHeight, + lpdib->lpDibBits, + (LPBITMAPINFO)lpdib, + DIB_RGB_COLORS); +} + + +MODPLUGDIB *LoadDib(LPCTSTR lpszName) +{ + mpt::const_byte_span data = GetResource(lpszName, RT_BITMAP); + if(!data.data()) + { + return nullptr; + } + LPBITMAPINFO p = (LPBITMAPINFO)data.data(); + MODPLUGDIB *pmd = new MODPLUGDIB; + pmd->bmiHeader = p->bmiHeader; + for (int i=0; i<16; i++) pmd->bmiColors[i] = p->bmiColors[i]; + LPBYTE lpDibBits = (LPBYTE)p; + lpDibBits += p->bmiHeader.biSize + 16 * sizeof(RGBQUAD); + pmd->lpDibBits = lpDibBits; + return pmd; +} + +int DrawTextT(HDC hdc, const wchar_t *lpchText, int cchText, LPRECT lprc, UINT format) +{ + return ::DrawTextW(hdc, lpchText, cchText, lprc, format); +} + +int DrawTextT(HDC hdc, const char *lpchText, int cchText, LPRECT lprc, UINT format) +{ + return ::DrawTextA(hdc, lpchText, cchText, lprc, format); +} + +template<typename Tchar> +static void DrawButtonRectImpl(HDC hdc, CRect rect, const Tchar *lpszText, bool disabled, bool pushed, DWORD textFlags, uint32 topMargin) +{ + int width = Util::ScalePixels(1, WindowFromDC(hdc)); + if(width != 1) + { + // Draw "real" buttons in Hi-DPI mode + DrawFrameControl(hdc, rect, DFC_BUTTON, pushed ? (DFCS_PUSHED | DFCS_BUTTONPUSH) : DFCS_BUTTONPUSH); + } else + { + const auto colorHighlight = GetSysColor(COLOR_BTNHIGHLIGHT), colorShadow = GetSysColor(COLOR_BTNSHADOW); + auto oldpen = SelectPen(hdc, GetStockObject(DC_PEN)); + ::SetDCPenColor(hdc, pushed ? colorShadow : colorHighlight); + ::FillRect(hdc, rect, GetSysColorBrush(COLOR_BTNFACE)); + ::MoveToEx(hdc, rect.left, rect.bottom - 1, nullptr); + ::LineTo(hdc, rect.left, rect.top); + ::LineTo(hdc, rect.right - 1, rect.top); + ::SetDCPenColor(hdc, pushed ? colorHighlight : colorShadow); + ::LineTo(hdc, rect.right - 1, rect.bottom - 1); + ::LineTo(hdc, rect.left, rect.bottom - 1); + SelectPen(hdc, oldpen); + } + + if(lpszText && lpszText[0]) + { + rect.DeflateRect(width, width); + if(pushed) + { + rect.top += width; + rect.left += width; + } + ::SetTextColor(hdc, GetSysColor(disabled ? COLOR_GRAYTEXT : COLOR_BTNTEXT)); + ::SetBkMode(hdc, TRANSPARENT); + rect.top += topMargin; + auto oldfont = SelectFont(hdc, CMainFrame::GetGUIFont()); + DrawTextT(hdc, lpszText, -1, &rect, textFlags | DT_SINGLELINE | DT_NOPREFIX); + SelectFont(hdc, oldfont); + } +} + + +void DrawButtonRect(HDC hdc, const RECT *lpRect, LPCSTR lpszText, BOOL bDisabled, BOOL bPushed, DWORD dwFlags, uint32 topMargin) +{ + DrawButtonRectImpl(hdc, *lpRect, lpszText, bDisabled, bPushed, dwFlags, topMargin); +} + + +void DrawButtonRect(HDC hdc, const RECT *lpRect, LPCWSTR lpszText, BOOL bDisabled, BOOL bPushed, DWORD dwFlags, uint32 topMargin) +{ + DrawButtonRectImpl(hdc, *lpRect, lpszText, bDisabled, bPushed, dwFlags, topMargin); +} + + + +////////////////////////////////////////////////////////////////////////////////// +// Misc functions + + +void ErrorBox(UINT nStringID, CWnd *parent) +{ + CString str; + BOOL resourceLoaded = str.LoadString(nStringID); + if(!resourceLoaded) + { + str.Format(_T("Resource string %u not found."), nStringID); + } + MPT_ASSERT(resourceLoaded); + Reporting::CustomNotification(str, _T("Error!"), MB_OK | MB_ICONERROR, parent); +} + + +CString GetWindowTextString(const CWnd &wnd) +{ + CString result; + wnd.GetWindowText(result); + return result; +} + + +mpt::ustring GetWindowTextUnicode(const CWnd &wnd) +{ + return mpt::ToUnicode(GetWindowTextString(wnd)); +} + + +//////////////////////////////////////////////////////////////////////////////// +// CFastBitmap 8-bit output / 4-bit input +// useful for lots of small blits with color mapping +// combined in one big blit + +void CFastBitmap::Init(MODPLUGDIB *lpTextDib) +{ + m_nBlendOffset = 0; + m_pTextDib = lpTextDib; + MemsetZero(m_Dib.bmiHeader); + m_nTextColor = 0; + m_nBkColor = 1; + m_Dib.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + m_Dib.bmiHeader.biWidth = 0; // Set later + m_Dib.bmiHeader.biHeight = 0; // Ditto + m_Dib.bmiHeader.biPlanes = 1; + m_Dib.bmiHeader.biBitCount = 8; + m_Dib.bmiHeader.biCompression = BI_RGB; + m_Dib.bmiHeader.biSizeImage = 0; + m_Dib.bmiHeader.biXPelsPerMeter = 96; + m_Dib.bmiHeader.biYPelsPerMeter = 96; + m_Dib.bmiHeader.biClrUsed = 0; + m_Dib.bmiHeader.biClrImportant = 256; // MAX_MODPALETTECOLORS; + m_n4BitPalette[0] = (BYTE)m_nTextColor; + m_n4BitPalette[4] = MODCOLOR_SEPSHADOW; + m_n4BitPalette[12] = MODCOLOR_SEPFACE; + m_n4BitPalette[14] = MODCOLOR_SEPHILITE; + m_n4BitPalette[15] = (BYTE)m_nBkColor; +} + + +void CFastBitmap::Blit(HDC hdc, int x, int y, int cx, int cy) +{ + SetDIBitsToDevice( hdc, + x, + y, + cx, + cy, + 0, + m_Dib.bmiHeader.biHeight - cy, + 0, + m_Dib.bmiHeader.biHeight, + &m_Dib.DibBits[0], + (LPBITMAPINFO)&m_Dib, + DIB_RGB_COLORS); +} + + +void CFastBitmap::SetColor(UINT nIndex, COLORREF cr) +{ + if (nIndex < 256) + { + m_Dib.bmiColors[nIndex].rgbRed = GetRValue(cr); + m_Dib.bmiColors[nIndex].rgbGreen = GetGValue(cr); + m_Dib.bmiColors[nIndex].rgbBlue = GetBValue(cr); + } +} + + +void CFastBitmap::SetAllColors(UINT nBaseIndex, UINT nColors, COLORREF *pcr) +{ + for (UINT i=0; i<nColors; i++) + { + SetColor(nBaseIndex+i, pcr[i]); + } +} + + +void CFastBitmap::SetBlendColor(COLORREF cr) +{ + UINT r = GetRValue(cr); + UINT g = GetGValue(cr); + UINT b = GetBValue(cr); + for (UINT i=0; i<BLEND_OFFSET; i++) + { + UINT m = (m_Dib.bmiColors[i].rgbRed >> 2) + + (m_Dib.bmiColors[i].rgbGreen >> 1) + + (m_Dib.bmiColors[i].rgbBlue >> 2); + m_Dib.bmiColors[i|BLEND_OFFSET].rgbRed = static_cast<BYTE>((m + r)>>1); + m_Dib.bmiColors[i|BLEND_OFFSET].rgbGreen = static_cast<BYTE>((m + g)>>1); + m_Dib.bmiColors[i|BLEND_OFFSET].rgbBlue = static_cast<BYTE>((m + b)>>1); + } +} + + +// Monochrome 4-bit bitmap (0=text, !0 = back) +void CFastBitmap::TextBlt(int x, int y, int cx, int cy, int srcx, int srcy, MODPLUGDIB *lpdib) +{ + const uint8 *psrc; + BYTE *pdest; + UINT x1, x2; + int srcwidth, srcinc; + + m_n4BitPalette[0] = (BYTE)m_nTextColor; + m_n4BitPalette[15] = (BYTE)m_nBkColor; + if (x < 0) + { + cx += x; + x = 0; + } + if (y < 0) + { + cy += y; + y = 0; + } + if ((x >= m_Dib.bmiHeader.biWidth) || (y >= m_Dib.bmiHeader.biHeight)) return; + if (x+cx >= m_Dib.bmiHeader.biWidth) cx = m_Dib.bmiHeader.biWidth - x; + if (y+cy >= m_Dib.bmiHeader.biHeight) cy = m_Dib.bmiHeader.biHeight - y; + if (!lpdib) lpdib = m_pTextDib; + if ((cx <= 0) || (cy <= 0) || (!lpdib)) return; + srcwidth = (lpdib->bmiHeader.biWidth+1) >> 1; + srcinc = srcwidth; + if (((int)lpdib->bmiHeader.biHeight) > 0) + { + srcy = lpdib->bmiHeader.biHeight - 1 - srcy; + srcinc = -srcinc; + } + x1 = srcx & 1; + x2 = x1 + cx; + pdest = &m_Dib.DibBits[((m_Dib.bmiHeader.biHeight - 1 - y) << m_nXShiftFactor) + x]; + psrc = lpdib->lpDibBits + (srcx >> 1) + (srcy * srcwidth); + for (int iy=0; iy<cy; iy++) + { + uint8 *p = pdest; + UINT ix = x1; + if (ix&1) + { + UINT b = psrc[ix >> 1]; + *p++ = m_n4BitPalette[b & 0x0F]+m_nBlendOffset; + ix++; + } + while (ix+1 < x2) + { + UINT b = psrc[ix >> 1]; + p[0] = m_n4BitPalette[b >> 4]+m_nBlendOffset; + p[1] = m_n4BitPalette[b & 0x0F]+m_nBlendOffset; + ix+=2; + p+=2; + } + if (x2&1) + { + UINT b = psrc[ix >> 1]; + *p++ = m_n4BitPalette[b >> 4]+m_nBlendOffset; + } + pdest -= m_Dib.bmiHeader.biWidth; + psrc += srcinc; + } +} + + +void CFastBitmap::SetSize(int x, int y) +{ + if(x > 4) + { + // Compute the required shift factor for obtaining a power-of-two bitmap width + m_nXShiftFactor = 1; + x--; + while(x >>= 1) + { + m_nXShiftFactor++; + } + } else + { + // Bitmaps rows are aligned to 4 bytes, so let this bitmap be exactly 4 pixels wide. + m_nXShiftFactor = 2; + } + + x = (1 << m_nXShiftFactor); + if(m_Dib.DibBits.size() != static_cast<size_t>(y << m_nXShiftFactor)) m_Dib.DibBits.resize(y << m_nXShiftFactor); + m_Dib.bmiHeader.biWidth = x; + m_Dib.bmiHeader.biHeight = y; +} + + +/////////////////////////////////////////////////////////////////////////////////// +// +// DirectX Plugins +// + +void CTrackApp::InitializeDXPlugins() +{ + m_pPluginManager = new CVstPluginManager; + const size_t numPlugins = GetSettings().Read<int32>(U_("VST Plugins"), U_("NumPlugins"), 0); + + bool maskCrashes = TrackerSettings::Instance().BrokenPluginsWorkaroundVSTMaskAllCrashes; + + std::vector<VSTPluginLib *> nonFoundPlugs; + const mpt::PathString failedPlugin = GetSettings().Read<mpt::PathString>(U_("VST Plugins"), U_("FailedPlugin"), P_("")); + + CDialog pluginScanDlg; + CWnd *textWnd = nullptr; + DWORD64 scanStart = Util::GetTickCount64(); + + // Read tags for built-in plugins + for(auto plug : *m_pPluginManager) + { + mpt::ustring key = MPT_UFORMAT("Plugin{}{}.Tags")(mpt::ufmt::HEX0<8>(plug->pluginId1), mpt::ufmt::HEX0<8>(plug->pluginId2)); + plug->tags = GetSettings().Read<mpt::ustring>(U_("VST Plugins"), key, mpt::ustring()); + } + + // Restructured plugin cache + if(TrackerSettings::Instance().PreviousSettingsVersion < MPT_V("1.27.00.15")) + { + DeleteFile(m_szPluginCacheFileName.AsNative().c_str()); + GetPluginCache().ForgetAll(); + } + + m_pPluginManager->reserve(numPlugins); + auto plugIDFormat = MPT_UFORMAT("Plugin{}"); + auto scanFormat = MPT_CFORMAT("Scanning Plugin {} / {}...\n{}"); + auto tagFormat = MPT_UFORMAT("Plugin{}.Tags"); + for(size_t plug = 0; plug < numPlugins; plug++) + { + mpt::PathString plugPath = GetSettings().Read<mpt::PathString>(U_("VST Plugins"), plugIDFormat(plug), mpt::PathString()); + if(!plugPath.empty()) + { + plugPath = PathInstallRelativeToAbsolute(plugPath); + + if(!pluginScanDlg.m_hWnd && Util::GetTickCount64() >= scanStart + 2000) + { + // If this is taking too long, show the user what they're waiting for. + pluginScanDlg.Create(IDD_SCANPLUGINS, gpSplashScreen); + pluginScanDlg.ShowWindow(SW_SHOW); + pluginScanDlg.CenterWindow(gpSplashScreen); + textWnd = pluginScanDlg.GetDlgItem(IDC_SCANTEXT); + } else if(pluginScanDlg.m_hWnd && Util::GetTickCount64() >= scanStart + 30) + { + textWnd->SetWindowText(scanFormat(plug + 1, numPlugins + 1, plugPath)); + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + scanStart = Util::GetTickCount64(); + } + + if(plugPath == failedPlugin) + { + GetSettings().Remove(U_("VST Plugins"), U_("FailedPlugin")); + const CString text = _T("The following plugin has previously crashed OpenMPT during initialisation:\n\n") + failedPlugin.ToCString() + _T("\n\nDo you still want to load it?"); + if(Reporting::Confirm(text, false, true, &pluginScanDlg) == cnfNo) + { + continue; + } + } + + mpt::ustring plugTags = GetSettings().Read<mpt::ustring>(U_("VST Plugins"), tagFormat(plug), mpt::ustring()); + + bool plugFound = true; + VSTPluginLib *lib = m_pPluginManager->AddPlugin(plugPath, maskCrashes, plugTags, true, &plugFound); + if(!plugFound && lib != nullptr) + { + nonFoundPlugs.push_back(lib); + } + if(lib != nullptr && lib->libraryName == P_("MIDI Input Output") && lib->pluginId1 == PLUGMAGIC('V','s','t','P') && lib->pluginId2 == PLUGMAGIC('M','M','I','D')) + { + // This appears to be an old version of our MIDI I/O plugin, which is now built right into the main executable. + m_pPluginManager->RemovePlugin(lib); + } + } + } + GetPluginCache().Flush(); + if(pluginScanDlg.m_hWnd) + { + pluginScanDlg.DestroyWindow(); + } + if(!nonFoundPlugs.empty()) + { + PlugNotFoundDialog(nonFoundPlugs, nullptr).DoModal(); + } +} + + +void CTrackApp::UninitializeDXPlugins() +{ + if(!m_pPluginManager) return; + +#ifndef NO_PLUGINS + + size_t plugIndex = 0; + for(auto plug : *m_pPluginManager) + { + if(!plug->isBuiltIn) + { + mpt::PathString plugPath = plug->dllPath; + if(theApp.IsPortableMode()) + { + plugPath = PathAbsoluteToInstallRelative(plugPath); + } + theApp.GetSettings().Write<mpt::PathString>(U_("VST Plugins"), MPT_UFORMAT("Plugin{}")(plugIndex), plugPath); + + theApp.GetSettings().Write(U_("VST Plugins"), MPT_UFORMAT("Plugin{}.Tags")(plugIndex), plug->tags); + + plugIndex++; + } else + { + mpt::ustring key = MPT_UFORMAT("Plugin{}{}.Tags")(mpt::ufmt::HEX0<8>(plug->pluginId1), mpt::ufmt::HEX0<8>(plug->pluginId2)); + theApp.GetSettings().Write(U_("VST Plugins"), key, plug->tags); + } + } + theApp.GetSettings().Write(U_("VST Plugins"), U_("NumPlugins"), static_cast<uint32>(plugIndex)); +#endif // NO_PLUGINS + + delete m_pPluginManager; + m_pPluginManager = nullptr; +} + + +/////////////////////////////////////////////////////////////////////////////////// +// Internet-related functions + +bool CTrackApp::OpenURL(const char *url) +{ + if(!url) return false; + return OpenURL(mpt::PathString::FromUTF8(url)); +} + +bool CTrackApp::OpenURL(const std::string &url) +{ + return OpenURL(mpt::PathString::FromUTF8(url)); +} + +bool CTrackApp::OpenURL(const CString &url) +{ + return OpenURL(mpt::ToUnicode(url)); +} + +bool CTrackApp::OpenURL(const mpt::ustring &url) +{ + return OpenURL(mpt::PathString::FromUnicode(url)); +} + +bool CTrackApp::OpenURL(const mpt::PathString &lpszURL) +{ + if(!lpszURL.empty() && theApp.m_pMainWnd) + { + if(reinterpret_cast<INT_PTR>(ShellExecute( + theApp.m_pMainWnd->m_hWnd, + _T("open"), + lpszURL.AsNative().c_str(), + NULL, + NULL, + SW_SHOW)) >= 32) + { + return true; + } + } + return false; +} + + +CString CTrackApp::GetResamplingModeName(ResamplingMode mode, int length, bool addTaps) +{ + CString result; + switch(mode) + { + case SRCMODE_NEAREST: + result = (length > 1) ? _T("No Interpolation") : _T("None") ; + break; + case SRCMODE_LINEAR: + result = _T("Linear"); + break; + case SRCMODE_CUBIC: + result = _T("Cubic"); + break; + case SRCMODE_SINC8: + result = _T("Sinc"); + break; + case SRCMODE_SINC8LP: + result = _T("Sinc"); + break; + default: + MPT_ASSERT_NOTREACHED(); + break; + } + if(Resampling::HasAA(mode)) + { + result += (length > 1) ? _T(" + Low-Pass") : _T(" + LP"); + } + if(addTaps) + { + result += MPT_CFORMAT(" ({} tap{})")(Resampling::Length(mode), (Resampling::Length(mode) != 1) ? CString(_T("s")) : CString(_T(""))); + } + return result; +} + + +mpt::ustring CTrackApp::GetFriendlyMIDIPortName(const mpt::ustring &deviceName, bool isInputPort, bool addDeviceName) +{ + auto friendlyName = GetSettings().Read<mpt::ustring>(isInputPort ? U_("MIDI Input Ports") : U_("MIDI Output Ports"), deviceName, deviceName); + if(friendlyName.empty()) + return deviceName; + else if(addDeviceName && friendlyName != deviceName) + return friendlyName + UL_(" (") + deviceName + UL_(")"); + else + return friendlyName; +} + + +CString CTrackApp::GetFriendlyMIDIPortName(const CString &deviceName, bool isInputPort, bool addDeviceName) +{ + return mpt::ToCString(GetFriendlyMIDIPortName(mpt::ToUnicode(deviceName), isInputPort, addDeviceName)); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.h b/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.h new file mode 100644 index 00000000..07eb9287 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Mptrack.h @@ -0,0 +1,467 @@ +/* + * MPTrack.h + * --------- + * Purpose: OpenMPT core application class. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "resource.h" // main symbols +#include "Settings.h" +#include "MPTrackUtil.h" +#include "Reporting.h" +#include "../soundlib/MIDIMacros.h" +#include "../soundlib/modcommand.h" +#include "../common/ComponentManager.h" +#include "../misc/mptMutex.h" +#include "../common/mptRandom.h" + +#include <future> + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +class CModDocTemplate; +class CVstPluginManager; +namespace SoundDevice +{ +class Manager; +} // namespace SoundDevice +struct AllSoundDeviceComponents; +class CDLSBank; +class DebugSettings; +class TrackerSettings; +class ComponentManagerSettings; +namespace mpt +{ +namespace Wine +{ +class VersionContext; +class Context; +} // namespace Wine +} // namespace mpt +class GdiplusRAII; + + +///////////////////////////////////////////////////////////////////////////// +// 16-colors DIB +struct MODPLUGDIB +{ + BITMAPINFOHEADER bmiHeader; + RGBQUAD bmiColors[16]; + LPBYTE lpDibBits; +}; + + +///////////////////////////////////////////////////////////////////////////// +// Midi Library + +using MidiLibrary = std::array<mpt::PathString, 128 * 2>; // 128 instruments + 128 percussions + + +////////////////////////////////////////////////////////////////////////// +// Dragon Droppings + +enum DragonDropType +{ + DRAGONDROP_NOTHING = 0, // |------< Drop Type >-------------|---< dropItem >----|---< dropParam >---| + DRAGONDROP_DLS, // | Instrument from a DLS bank | DLS Bank # | DLS Instrument | + DRAGONDROP_SAMPLE, // | Sample from a song | Sample # | NULL | + DRAGONDROP_INSTRUMENT, // | Instrument from a song | Instrument # | NULL | + DRAGONDROP_SOUNDFILE, // | File from instrument library | ? | File Name | + DRAGONDROP_MIDIINSTR, // | File from midi library | Midi Program/Perc | File Name | + DRAGONDROP_PATTERN, // | Pattern from a song | Pattern # | NULL | + DRAGONDROP_ORDER, // | Pattern index in a song | Order # | NULL | + DRAGONDROP_SONG, // | Song file (mod/s3m/xm/it) | 0 | File Name | + DRAGONDROP_SEQUENCE // | Sequence (a set of orders) | Sequence # | NULL | +}; + +struct DRAGONDROP +{ + const CSoundFile *sndFile = nullptr; + DragonDropType dropType = DRAGONDROP_NOTHING; + uint32 dropItem = 0; + LPARAM dropParam = 0; + + mpt::PathString GetPath() const + { + const mpt::PathString *const path = reinterpret_cast<const mpt::PathString *>(dropParam); + MPT_ASSERT(path); + return path ? *path : mpt::PathString(); + } +}; + + +///////////////////////////////////////////////////////////////////////////// +// CTrackApp: +// See mptrack.cpp for the implementation of this class +// + +class CMPTCommandLineInfo; + +class CTrackApp : public CWinApp +{ + friend class CMainFrame; + // static data +protected: + static MODTYPE m_nDefaultDocType; + static MidiLibrary midiLibrary; + +public: + static std::vector<std::unique_ptr<CDLSBank>> gpDLSBanks; + +protected: + mpt::recursive_mutex_with_lock_count m_GlobalMutex; + + DWORD m_GuiThreadId = 0; + + std::future<std::vector<std::unique_ptr<CDLSBank>>> m_scannedDlsBanks; + std::atomic<bool> m_scannedDlsBanksAvailable = false; + + std::unique_ptr<mpt::random_device> m_RD; + std::unique_ptr<mpt::thread_safe_prng<mpt::default_prng>> m_PRNG; + + std::unique_ptr<GdiplusRAII> m_Gdiplus; + + std::shared_ptr<mpt::OS::Wine::VersionContext> m_WineVersion; + + IniFileSettingsBackend *m_pSettingsIniFile; + SettingsContainer *m_pSettings = nullptr; + DebugSettings *m_pDebugSettings = nullptr; + TrackerSettings *m_pTrackerSettings = nullptr; + IniFileSettingsBackend *m_pSongSettingsIniFile = nullptr; + SettingsContainer *m_pSongSettings = nullptr; + ComponentManagerSettings *m_pComponentManagerSettings = nullptr; + IniFileSettingsContainer *m_pPluginCache = nullptr; + CModDocTemplate *m_pModTemplate = nullptr; + CVstPluginManager *m_pPluginManager = nullptr; + mpt::log::GlobalLogger m_GlobalLogger{}; + std::unique_ptr<AllSoundDeviceComponents> m_pAllSoundDeviceComponents; + std::unique_ptr<SoundDevice::Manager> m_pSoundDevicesManager; + + mpt::PathString m_InstallPath; // i.e. "C:\Program Files\OpenMPT\" (installer mode) or "G:\OpenMPT\" (portable mode) + mpt::PathString m_InstallBinPath; // i.e. "C:\Program Files\OpenMPT\bin\" (multi-arch mode) or InstallPath (legacy mode) + mpt::PathString m_InstallBinArchPath; // i.e. "C:\Program Files\OpenMPT\bin\amd64\" (multi-arch mode) or InstallPath (legacy mode) + mpt::PathString m_InstallPkgPath; // i.e. "C:\Program Files\OpenMPT\" (installer mode) or "G:\OpenMPT\" (portable mode) + + mpt::PathString m_ConfigPath; // InstallPath (portable mode) or "%AppData%\OpenMPT\" + + mpt::PathString m_szConfigFileName; + mpt::PathString m_szPluginCacheFileName; + + std::shared_ptr<mpt::Wine::Context> m_Wine; + mpt::PathString m_WineWrapperDllName; + // Default macro configuration + MIDIMacroConfig m_MidiCfg; + DWORD m_dwLastPluginIdleCall = 0; + bool m_bInstallerMode = false; + bool m_bPortableMode = false; + bool m_bSourceTreeMode = false; + +public: + CTrackApp(); + + CDataRecoveryHandler *GetDataRecoveryHandler() override; + void AddToRecentFileList(LPCTSTR lpszPathName) override; + void AddToRecentFileList(const mpt::PathString &path); + /// Removes item from MRU-list; most recent item has index zero. + void RemoveMruItem(const size_t item); + void RemoveMruItem(const mpt::PathString &path); + +public: + bool IsMultiArchInstall() const { return m_InstallPath == m_InstallBinArchPath; } + mpt::PathString GetInstallPath() const { return m_InstallPath; } // i.e. "C:\Program Files\OpenMPT\" (installer mode) or "G:\OpenMPT\" (portable mode) + mpt::PathString GetInstallBinPath() const { return m_InstallBinPath; } // i.e. "C:\Program Files\OpenMPT\bin\" (multi-arch mode) or InstallPath (legacy mode) + mpt::PathString GetInstallBinArchPath() const { return m_InstallBinArchPath; } // i.e. "C:\Program Files\OpenMPT\bin\amd64\" (multi-arch mode) or InstallPath (legacy mode) + mpt::PathString GetInstallPkgPath() const { return m_InstallPkgPath; } // i.e. "C:\Program Files\OpenMPT\" (installer mode) or "G:\OpenMPT\" (portable mode) + + static MODTYPE GetDefaultDocType() { return m_nDefaultDocType; } + static void SetDefaultDocType(MODTYPE n) { m_nDefaultDocType = n; } + static MidiLibrary &GetMidiLibrary() { return midiLibrary; } + static void ImportMidiConfig(const mpt::PathString &filename, bool hideWarning = false); + static void ExportMidiConfig(const mpt::PathString &filename); + static void ImportMidiConfig(SettingsContainer &file, const mpt::PathString &path, bool forgetSettings = false); + static void ExportMidiConfig(SettingsContainer &file); + static std::future<std::vector<std::unique_ptr<CDLSBank>>> LoadDefaultDLSBanks(); + static void SaveDefaultDLSBanks(); + static void RemoveDLSBank(UINT nBank); + static bool AddDLSBank(const mpt::PathString &filename); + static bool OpenURL(const char *url); // UTF8 + static bool OpenURL(const std::string &url); // UTF8 + static bool OpenURL(const CString &url); + static bool OpenURL(const mpt::ustring &url); + static bool OpenURL(const mpt::PathString &lpszURL); + static bool OpenFile(const mpt::PathString &file) { return OpenURL(file); }; + static bool OpenDirectory(const mpt::PathString &directory) { return OpenURL(directory); }; + + // Retrieve the user-supplied MIDI port name for a MIDI input or output port. + mpt::ustring GetFriendlyMIDIPortName(const mpt::ustring &deviceName, bool isInputPort, bool addDeviceName = true); + CString GetFriendlyMIDIPortName(const CString &deviceName, bool isInputPort, bool addDeviceName = true); + + int GetOpenDocumentCount() const; + std::vector<CModDoc *> GetOpenDocuments() const; + +public: + inline mpt::recursive_mutex_with_lock_count &GetGlobalMutexRef() { return m_GlobalMutex; } + bool InGuiThread() const { return GetCurrentThreadId() == m_GuiThreadId; } + mpt::random_device &RandomDevice() { return *m_RD; } + mpt::thread_safe_prng<mpt::default_prng> &PRNG() { return *m_PRNG; } + CModDocTemplate *GetModDocTemplate() const { return m_pModTemplate; } + CVstPluginManager *GetPluginManager() const { return m_pPluginManager; } + SoundDevice::Manager *GetSoundDevicesManager() const { return m_pSoundDevicesManager.get(); } + void GetDefaultMidiMacro(MIDIMacroConfig &cfg) const { cfg = m_MidiCfg; } + void SetDefaultMidiMacro(const MIDIMacroConfig &cfg) { m_MidiCfg = cfg; } + mpt::PathString GetConfigFileName() const { return m_szConfigFileName; } + SettingsContainer *GetpSettings() + { + return m_pSettings; + } + SettingsContainer &GetSettings() + { + ASSERT(m_pSettings); + return *m_pSettings; + } + TrackerSettings &GetTrackerSettings() + { + ASSERT(m_pTrackerSettings); + return *m_pTrackerSettings; + } + bool IsInstallerMode() const + { + return m_bInstallerMode; + } + bool IsPortableMode() const + { + return m_bPortableMode; + } + bool IsSourceTreeMode() const + { + return m_bSourceTreeMode; + } + + SettingsContainer &GetPluginCache() + { + ASSERT(m_pPluginCache); + return *m_pPluginCache; + } + + SettingsContainer &GetSongSettings() + { + ASSERT(m_pSongSettings); + return *m_pSongSettings; + } + const mpt::PathString &GetSongSettingsFilename() const + { + return m_pSongSettingsIniFile->GetFilename(); + } + + void SetWineVersion(std::shared_ptr<mpt::OS::Wine::VersionContext> wineVersion) + { + MPT_ASSERT_ALWAYS(mpt::OS::Windows::IsWine()); + m_WineVersion = wineVersion; + } + std::shared_ptr<mpt::OS::Wine::VersionContext> GetWineVersion() const + { + MPT_ASSERT_ALWAYS(mpt::OS::Windows::IsWine()); + MPT_ASSERT_ALWAYS(m_WineVersion); // Verify initialization order. We should not should reach this until after Wine is detected. + return m_WineVersion; + } + + void SetWine(std::shared_ptr<mpt::Wine::Context> wine) + { + m_Wine = wine; + } + std::shared_ptr<mpt::Wine::Context> GetWine() const + { + return m_Wine; + } + + void SetWineWrapperDllFilename(mpt::PathString filename) + { + m_WineWrapperDllName = filename; + } + mpt::PathString GetWineWrapperDllFilename() const + { + return m_WineWrapperDllName; + } + + /// Returns path to config folder including trailing '\'. + mpt::PathString GetConfigPath() const { return m_ConfigPath; } + void SetupPaths(bool overridePortable); + void CreatePaths(); + +#if !defined(MPT_BUILD_RETRO) + bool CheckSystemSupport(); +#endif // !MPT_BUILD_RETRO + + // Relative / absolute paths conversion + mpt::PathString PathAbsoluteToInstallRelative(const mpt::PathString &path) { return path.AbsolutePathToRelative(GetInstallPath()); } + mpt::PathString PathInstallRelativeToAbsolute(const mpt::PathString &path) { return path.RelativePathToAbsolute(GetInstallPath()); } + mpt::PathString PathAbsoluteToInstallBinArchRelative(const mpt::PathString &path) { return path.AbsolutePathToRelative(GetInstallBinArchPath()); } + mpt::PathString PathInstallBinArchRelativeToAbsolute(const mpt::PathString &path) { return path.RelativePathToAbsolute(GetInstallBinArchPath()); } + + static void OpenModulesDialog(std::vector<mpt::PathString> &files, const mpt::PathString &overridePath = mpt::PathString()); + +public: + // Get name of resampling mode. addTaps = true also adds the number of taps the filter uses. + static CString GetResamplingModeName(ResamplingMode mode, int length, bool addTaps); + + // Overrides +public: + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(CTrackApp) +public: + BOOL InitInstance() override; + BOOL InitInstanceEarly(CMPTCommandLineInfo &cmdInfo); + BOOL InitInstanceLate(CMPTCommandLineInfo &cmdInfo); + BOOL InitInstanceImpl(CMPTCommandLineInfo &cmdInfo); + int Run() override; + LRESULT ProcessWndProcException(CException *e, const MSG *pMsg) override; + int ExitInstance() override; + int ExitInstanceImpl(); + BOOL OnIdle(LONG lCount) override; + //}}AFX_VIRTUAL + + // Implementation + + //{{AFX_MSG(CTrackApp) + CModDoc *NewDocument(MODTYPE newType = MOD_TYPE_NONE); + + afx_msg void OnFileNew() { NewDocument(); } + afx_msg void OnFileNewMOD() { NewDocument(MOD_TYPE_MOD); } + afx_msg void OnFileNewS3M() { NewDocument(MOD_TYPE_S3M); } + afx_msg void OnFileNewXM() { NewDocument(MOD_TYPE_XM); } + afx_msg void OnFileNewIT() { NewDocument(MOD_TYPE_IT); } + afx_msg void OnFileNewMPT() { NewDocument(MOD_TYPE_MPT); } + + afx_msg void OnFileOpen(); + afx_msg void OnAppAbout(); + + afx_msg void OnFileCloseAll(); + afx_msg void OnUpdateAnyDocsOpen(CCmdUI *cmd); + + //}}AFX_MSG + DECLARE_MESSAGE_MAP() + +protected: + size_t AddScannedDLSBanks(); + + void InitializeDXPlugins(); + void UninitializeDXPlugins(); + + bool MoveConfigFile(const mpt::PathString &fileName, mpt::PathString subDir = {}, mpt::PathString newFileName = {}); +}; + + +extern CTrackApp theApp; + + +////////////////////////////////////////////////////////////////// +// More Bitmap Helpers + +class CFastBitmap +{ +protected: + static constexpr uint8 BLEND_OFFSET = 0x80; + + struct MODPLUGFASTDIB + { + BITMAPINFOHEADER bmiHeader; + RGBQUAD bmiColors[256]; + std::vector<uint8> DibBits; + }; + + MODPLUGFASTDIB m_Dib; + UINT m_nTextColor, m_nBkColor; + MODPLUGDIB *m_pTextDib; + uint8 m_nBlendOffset; + uint8 m_n4BitPalette[16]; + uint8 m_nXShiftFactor; + +public: + CFastBitmap() {} + +public: + void Init(MODPLUGDIB *lpTextDib = nullptr); + void Blit(HDC hdc, int x, int y, int cx, int cy); + void Blit(HDC hdc, LPCRECT lprc) { Blit(hdc, lprc->left, lprc->top, lprc->right - lprc->left, lprc->bottom - lprc->top); } + void SetTextColor(int nText, int nBk = -1) + { + m_nTextColor = nText; + if(nBk >= 0) + m_nBkColor = nBk; + } + void SetTextBkColor(UINT nBk) { m_nBkColor = nBk; } + void SetColor(UINT nIndex, COLORREF cr); + void SetAllColors(UINT nBaseIndex, UINT nColors, COLORREF *pcr); + void TextBlt(int x, int y, int cx, int cy, int srcx, int srcy, MODPLUGDIB *lpdib = nullptr); + void SetBlendMode(bool enable) { m_nBlendOffset = enable ? BLEND_OFFSET : 0; } + bool GetBlendMode() const { return m_nBlendOffset != 0; } + void SetBlendColor(COLORREF cr); + void SetSize(int x, int y); + int GetWidth() const { return m_Dib.bmiHeader.biWidth; } +}; + + +/////////////////////////////////////////////////// +// 4-bit DIB Drawing functions +void DibBlt(HDC hdc, int x, int y, int sizex, int sizey, int srcx, int srcy, MODPLUGDIB *lpdib); +MODPLUGDIB *LoadDib(LPCTSTR lpszName); +RGBQUAD rgb2quad(COLORREF c); + +// Other bitmap functions +int DrawTextT(HDC hdc, const wchar_t *lpchText, int cchText, LPRECT lprc, UINT format); +int DrawTextT(HDC hdc, const char *lpchText, int cchText, LPRECT lprc, UINT format); +void DrawButtonRect(HDC hdc, const RECT *lpRect, LPCSTR lpszText = nullptr, BOOL bDisabled = FALSE, BOOL bPushed = FALSE, DWORD dwFlags = (DT_CENTER | DT_VCENTER), uint32 topMargin = 0); +void DrawButtonRect(HDC hdc, const RECT *lpRect, LPCWSTR lpszText = nullptr, BOOL bDisabled = FALSE, BOOL bPushed = FALSE, DWORD dwFlags = (DT_CENTER | DT_VCENTER), uint32 topMargin = 0); + +// Misc functions +void ErrorBox(UINT nStringID, CWnd *p = nullptr); + +// Helper function declarations. +struct SNDMIXPLUGIN; +class IMixPlugin; +void AddPluginNamesToCombobox(CComboBox &CBox, const SNDMIXPLUGIN *plugarray, const bool libraryName = false, const PLUGINDEX updatePlug = PLUGINDEX_INVALID); +void AddPluginParameternamesToCombobox(CComboBox &CBox, SNDMIXPLUGIN &plugarray); +void AddPluginParameternamesToCombobox(CComboBox &CBox, IMixPlugin &plug); + +// Append note names in range [noteStart, noteEnd] to given combobox. Index starts from 0. +void AppendNotesToControl(CComboBox &combobox, ModCommand::NOTE noteStart, ModCommand::NOTE noteEnd); + +// Append note names to combo box. +// If nInstr is given, instrument-specific note names are used instead of default note names. +// A custom note range may also be specified using the noteStart and noteEnd parameters. +// If they are left out, only notes that are available in the module type, plus any supported "special notes" are added. +void AppendNotesToControlEx(CComboBox &combobox, const CSoundFile &sndFile, INSTRUMENTINDEX nInstr = MAX_INSTRUMENTS, ModCommand::NOTE noteStart = 0, ModCommand::NOTE noteEnd = 0); + +// Get window text (e.g. edit box content) as a CString +CString GetWindowTextString(const CWnd &wnd); + +// Get window text (e.g. edit box content) as a unicode string +mpt::ustring GetWindowTextUnicode(const CWnd &wnd); + +/////////////////////////////////////////////////// +// Tables + +extern const TCHAR *szSpecialNoteNamesMPT[]; +extern const TCHAR *szSpecialNoteShortDesc[]; +extern const char *szHexChar; + +// Defined in load_mid.cpp +extern const char *szMidiProgramNames[128]; +extern const char *szMidiPercussionNames[61]; // notes 25..85 +extern const char *szMidiGroupNames[17]; // 16 groups + Percussions + +///////////////////////////////////////////////////////////////////////////// + +//{{AFX_INSERT_LOCATION}} +// Microsoft Developer Studio will insert additional declarations immediately before the previous line. + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Notification.h b/Src/external_dependencies/openmpt-trunk/mptrack/Notification.h new file mode 100644 index 00000000..96a8c451 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Notification.h @@ -0,0 +1,63 @@ +/* + * Notification.h + * -------------- + * Purpose: GUI update notification struct + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +// struct Notification requires working copy constructor / copy assignment, keep in mind when extending +struct Notification +{ + enum Type + { + GlobalVU = 0x00, // Global VU meters (always enabled) + Position = 0x01, // Pattern playback position + Sample = 0x02, // pos[i] contains sample position on this channel + VolEnv = 0x04, // pos[i] contains volume envelope position + PanEnv = 0x08, // pos[i] contains panning envelope position + PitchEnv = 0x10, // pos[i] contains pitch envelope position + VUMeters = 0x20, // pos[i] contains pattern VU meter for this channel + EOS = 0x40, // End of stream reached, the GUI should stop the audio device + Stop = 0x80, // Audio device has been stopped -> reset GUI + + Default = GlobalVU, + }; + + typedef uint16 Item; + + static constexpr SmpLength PosInvalid = SmpLength(-1); // pos[i] is not valid (if it contains sample or envelope position) + static constexpr uint32 ClipVU = 0x80000000; // Master VU clip indicator bit (sound output has previously clipped) + + int64 timestampSamples; + FlagSet<Notification::Type> type; + Item item; // Sample or instrument number, depending on type + ROWINDEX row; // Always valid + uint32 tick, ticksOnRow; // ditto + ORDERINDEX order; // ditto + PATTERNINDEX pattern; // ditto + uint32 mixedChannels; // ditto + std::array<uint32, 4> masterVUin; // ditto + std::array<uint32, 4> masterVUout; // ditto + uint8 masterVUinChannels; // ditto + uint8 masterVUoutChannels; // ditto + std::array<SmpLength, MAX_CHANNELS> pos; // Sample / envelope pos for each channel if != PosInvalid, or pattern channel VUs + + Notification(FlagSet<Notification::Type> t = Default, Item i = 0, int64 s = 0, ROWINDEX r = 0, uint32 ti = 0, uint32 tir = 0, ORDERINDEX o = 0, PATTERNINDEX p = 0, uint32 x = 0, uint8 outChannels = 0, uint8 inChannels = 0) : timestampSamples(s), type(t), item(i), row(r), tick(ti), ticksOnRow(tir), order(o), pattern(p), mixedChannels(x), masterVUinChannels(inChannels), masterVUoutChannels(outChannels) + { + masterVUin.fill(0); + masterVUout.fill(0); + pos.fill(0); + } +}; + +DECLARE_FLAGSET(Notification::Type); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/OPLExport.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/OPLExport.cpp new file mode 100644 index 00000000..720300d5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/OPLExport.cpp @@ -0,0 +1,640 @@ +/* + * OPLExport.cpp + * ------------- + * Purpose: Export of OPL register dumps as VGM/VGZ or DRO files + * 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 "FileDialog.h" +#include "InputHandler.h" +#include "Mainfrm.h" +#include "Moddoc.h" +#include "ProgressDialog.h" +#include "../soundlib/OPL.h" +#include "../soundlib/Tagging.h" + +#include <zlib/zlib.h> + +OPENMPT_NAMESPACE_BEGIN + +// DRO file header +struct DROHeaderV1 +{ + static constexpr char droMagic[] = "DBRAWOPL"; + + char magic[8]; + uint16le verHi; + uint16le verLo; + uint32le lengthMs; + uint32le lengthBytes; + uint32le hardwareType; +}; + +MPT_BINARY_STRUCT(DROHeaderV1, 24); + + +// VGM file header +struct VGMHeader +{ + static constexpr char VgmMagic[] = "Vgm "; + + char magic[4]; + uint32le eofOffset; + uint32le version; + uint32le sn76489clock; + uint32le ym2413clock; + uint32le gd3Offset; + uint32le totalNumSamples; + uint32le loopOffset; + uint32le loopNumSamples; + uint32le rate; + uint32le someChipClocks[3]; + uint32le vgmDataOffset; + uint32le variousChipClocks[9]; + uint32le ymf262clock; // 14318180 + uint32le evenMoreChipClocks[7]; + uint8 volumeModifier; + uint8 reserved[131]; // Various other fields we're not interested in +}; + +MPT_BINARY_STRUCT(VGMHeader, 256); + + +// VGM metadata header +struct Gd3Header +{ + static constexpr char Gd3Magic[] = "Gd3 "; + + char magic[4]; + uint32le version; + uint32le size; +}; + +MPT_BINARY_STRUCT(Gd3Header, 12); + + +// The OPL register logger and serializer for VGM/VGZ/DRO files +class OPLCapture final : public OPL::IRegisterLogger +{ + struct RegisterDump + { + CSoundFile::samplecount_t sampleOffset; + uint8 regLo; + uint8 regHi; + uint8 value; + }; + +public: + OPLCapture(CSoundFile &sndFile) : m_sndFile{sndFile} {} + + void Reset() + { + m_registerDump.clear(); + m_prevRegisters.clear(); + } + + void CaptureAllVoiceRegisters() + { + for(const auto reg : OPL::AllVoiceRegisters()) + { + uint8 value = 0; + if(const auto prevValue = m_prevRegisters.find(reg); prevValue != m_prevRegisters.end()) + value = prevValue->second; + m_registerDumpAtLoopStart[reg] = value; + } + } + + void WriteDRO(std::ostream &f) const + { + DROHeaderV1 header{}; + memcpy(header.magic, DROHeaderV1::droMagic, 8); + header.verHi = 0; + header.verLo = 1; + header.lengthMs = Util::muldivr_unsigned(m_sndFile.GetTotalSampleCount(), 1000, m_sndFile.GetSampleRate()); + header.lengthBytes = 0; + header.hardwareType = 1; // OPL3 + + mpt::IO::Write(f, header); + + CSoundFile::samplecount_t prevOffset = 0, prevOffsetMs = 0; + bool prevHigh = false; + for(const auto ® : m_registerDump) + { + if(reg.sampleOffset > prevOffset) + { + uint32 offsetMs = Util::muldivr_unsigned(reg.sampleOffset, 1000, m_sndFile.GetSampleRate()); + header.lengthBytes += WriteDRODelay(f, offsetMs - prevOffsetMs); + prevOffset = reg.sampleOffset; + prevOffsetMs = offsetMs; + } + if(const bool isHigh = (reg.regHi == 1); isHigh != prevHigh) + { + prevHigh = isHigh; + mpt::IO::Write(f, mpt::as_byte(2 + reg.regHi)); + header.lengthBytes++; + } + if(reg.regLo <= 4) + { + mpt::IO::Write(f, mpt::as_byte(4)); + header.lengthBytes++; + } + const uint8 regValue[] = {reg.regLo, reg.value}; + mpt::IO::Write(f, regValue); + header.lengthBytes += 2; + } + if(header.lengthMs > prevOffsetMs) + header.lengthBytes += WriteDRODelay(f, header.lengthMs - prevOffsetMs); + + MPT_ASSERT(mpt::IO::TellWrite(f) == static_cast<mpt::IO::Offset>(header.lengthBytes + sizeof(header))); + // AdPlug can read some metadata following the register dump, but DroTrimmer panics if it see that data. + // As the metadata is very limited (40 characters per field, unknown 8-bit encoding) we'll leave that feature to the VGM export. +#if 0 + mpt::IO::Write(f, mpt::as_byte(0xFF)); + mpt::IO::Write(f, mpt::as_byte(0xFF)); + + char name[40]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = m_sndFile.m_songName; + mpt::IO::Write(f, mpt::as_byte(0x1A)); + mpt::IO::Write(f, name); + + if(!m_sndFile.m_songArtist.empty()) + { + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = mpt::ToCharset(mpt::Charset::ISO8859_1, m_sndFile.m_songArtist); + mpt::IO::Write(f, mpt::as_byte(0x1B)); + mpt::IO::Write(f, name); + } +#endif + + mpt::IO::SeekAbsolute(f, 0); + mpt::IO::Write(f, header); + } + + void WriteVGZ(std::ostream &f, const CSoundFile::samplecount_t loopStart, const FileTags &fileTags, const mpt::ustring &filename) const + { + std::ostringstream outStream; + WriteVGM(outStream, loopStart, fileTags); + + std::string outData = std::move(outStream).str(); + z_stream strm{}; + strm.avail_in = static_cast<uInt>(outData.size()); + strm.next_in = reinterpret_cast<Bytef *>(outData.data()); + if(deflateInit2(&strm, Z_BEST_COMPRESSION, Z_DEFLATED, 15 | 16, 9, Z_DEFAULT_STRATEGY) != Z_OK) + throw std::runtime_error{"zlib init failed"}; + gz_header gzHeader{}; + gzHeader.time = static_cast<uLong>(time(nullptr)); + std::string filenameISO = mpt::ToCharset(mpt::Charset::ISO8859_1, filename); + gzHeader.name = reinterpret_cast<Bytef *>(filenameISO.data()); + deflateSetHeader(&strm, &gzHeader); + do + { + std::array<Bytef, mpt::IO::BUFFERSIZE_TINY> buffer; + strm.avail_out = static_cast<uInt>(buffer.size()); + strm.next_out = buffer.data(); + deflate(&strm, Z_FINISH); + mpt::IO::WritePartial(f, buffer, buffer.size() - strm.avail_out); + } while(strm.avail_out == 0); + deflateEnd(&strm); + } + + void WriteVGM(std::ostream &f, const CSoundFile::samplecount_t loopStart, const FileTags &fileTags) const + { + VGMHeader header{}; + memcpy(header.magic, VGMHeader::VgmMagic, 4); + header.version = 0x160; + header.vgmDataOffset = sizeof(header) - offsetof(VGMHeader, vgmDataOffset); + header.ymf262clock = 14318180; + header.totalNumSamples = static_cast<uint32>(m_sndFile.GetTotalSampleCount()); + if(loopStart != Util::MaxValueOfType(loopStart)) + header.loopNumSamples = static_cast<uint32>(m_sndFile.GetTotalSampleCount() - loopStart); + + mpt::IO::Write(f, header); + + bool wroteLoopStart = (header.loopNumSamples == 0); + CSoundFile::samplecount_t prevOffset = 0; + for(const auto ® : m_registerDump) + { + if(reg.sampleOffset >= loopStart && !wroteLoopStart) + { + WriteVGMDelay(f, loopStart - prevOffset); + prevOffset = loopStart; + header.loopOffset = static_cast<uint32>(mpt::IO::TellWrite(f) - 0x1C); + wroteLoopStart = true; + for(const auto & [loopReg, value] : m_registerDumpAtLoopStart) + { + if(m_prevRegisters.count(loopReg)) + { + const uint8 data[] = {static_cast<uint8>(0x5E + (loopReg >> 8)), static_cast<uint8>(loopReg & 0xFF), value}; + mpt::IO::Write(f, data); + } + } + } + + WriteVGMDelay(f, reg.sampleOffset - prevOffset); + prevOffset = reg.sampleOffset; + const uint8 data[] = {static_cast<uint8>(0x5E + reg.regHi), reg.regLo, reg.value}; + mpt::IO::Write(f, data); + } + WriteVGMDelay(f, m_sndFile.GetTotalSampleCount() - prevOffset); + mpt::IO::Write(f, mpt::as_byte(0x66)); + + header.gd3Offset = static_cast<uint32>(mpt::IO::TellWrite(f) - offsetof(VGMHeader, gd3Offset)); + + const mpt::ustring tags[] = + { + fileTags.title, + {}, // Song name JP + {}, // Game name EN + {}, // Game name JP + Version::Current().GetOpenMPTVersionString(), + {}, // System name JP + fileTags.artist, + {}, // Author name JP + fileTags.year, + {}, // Person who created the VGM file + mpt::String::Replace(fileTags.comments, U_("\r\n"), U_("\n")), + }; + std::ostringstream tagStream; + for(const auto &tag : tags) + { + WriteVGMString(tagStream, mpt::ToWide(tag)); + } + const auto tagsData = std::move(tagStream).str(); + + Gd3Header gd3Header{}; + memcpy(gd3Header.magic, Gd3Header::Gd3Magic, 4); + gd3Header.version = 0x100; + gd3Header.size = static_cast<uint32>(tagsData.size()); + mpt::IO::Write(f, gd3Header); + mpt::IO::WriteRaw(f, mpt::as_span(tagsData)); + + header.eofOffset = static_cast<uint32>(mpt::IO::TellWrite(f) - offsetof(VGMHeader, eofOffset)); + + mpt::IO::SeekAbsolute(f, 0); + mpt::IO::Write(f, header); + } + +private: + static uint32 WriteDRODelay(std::ostream &f, uint32 delay) + { + uint32 bytesWritten = 0; + while(delay > 256) + { + uint32 subDelay = std::min(delay, 65536u); + mpt::IO::Write(f, mpt::as_byte(1)); + mpt::IO::WriteIntLE(f, static_cast<uint16>(subDelay - 1)); + bytesWritten += 3; + delay -= subDelay; + } + if(delay) + { + mpt::IO::Write(f, mpt::as_byte(0)); + mpt::IO::WriteIntLE(f, static_cast<uint8>(delay - 1)); + bytesWritten += 2; + } + return bytesWritten; + } + + static void WriteVGMDelay(std::ostream &f, CSoundFile::samplecount_t delay) + { + while(delay) + { + uint16 subDelay = mpt::saturate_cast<uint16>(delay); + if(subDelay <= 16) + { + mpt::IO::Write(f, mpt::as_byte(0x6F + subDelay)); + } else if(subDelay == 735) + { + mpt::IO::Write(f, mpt::as_byte(0x62)); // 1/60th of a second + } else if(subDelay == 882) + { + mpt::IO::Write(f, mpt::as_byte(0x63)); // 1/50th of a second + } else + { + mpt::IO::Write(f, mpt::as_byte(0x61)); + mpt::IO::WriteIntLE(f, subDelay); + } + delay -= subDelay; + } + } + + static void WriteVGMString(std::ostream &f, const std::wstring &s) + { + std::vector<uint16le> s16le(s.length() + 1); + for(size_t i = 0; i < s.length(); i++) + { + s16le[i] = s[i] ? s[i] : L' '; + } + mpt::IO::Write(f, s16le); + } + + void Port(CHANNELINDEX, uint16 reg, uint8 value) override + { + if(const auto prevValue = m_prevRegisters.find(reg); prevValue != m_prevRegisters.end() && prevValue->second == value) + return; + m_registerDump.push_back({m_sndFile.GetTotalSampleCount(), static_cast<uint8>(reg & 0xFF), static_cast<uint8>(reg >> 8), value}); + m_prevRegisters[reg] = value; + } + + std::vector<RegisterDump> m_registerDump; + std::map<uint16, uint8> m_prevRegisters, m_registerDumpAtLoopStart; + CSoundFile &m_sndFile; +}; + + +class OPLExportDlg : public CProgressDialog +{ +private: + enum class ExportFormat + { + VGZ = IDC_RADIO1, + VGM = IDC_RADIO2, + DRO = IDC_RADIO3, + }; + + static ExportFormat s_format; + + OPLCapture m_oplLogger; + CSoundFile &m_sndFile; + CModDoc &m_modDoc; + + std::vector<SubSong> m_subSongs; + size_t m_selectedSong = 0; + bool m_conversionRunning = false; + bool m_locked = true; + +public: + OPLExportDlg(CModDoc &modDoc, CWnd *parent = nullptr) + : CProgressDialog{parent, IDD_OPLEXPORT} + , m_oplLogger{modDoc.GetSoundFile()} + , m_sndFile{modDoc.GetSoundFile()} + , m_modDoc{modDoc} + , m_subSongs{modDoc.GetSoundFile().GetAllSubSongs()} + { + } + + BOOL OnInitDialog() override + { + CProgressDialog::OnInitDialog(); + + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, static_cast<int>(s_format)); + CheckRadioButton(IDC_RADIO4, IDC_RADIO5, IDC_RADIO4); + + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1))->SetRange32(1, static_cast<int>(m_subSongs.size())); + SetDlgItemInt(IDC_EDIT1, static_cast<UINT>(m_selectedSong + 1), FALSE); + if(m_subSongs.size() <= 1) + { + const int controls[] = {IDC_RADIO4, IDC_RADIO5, IDC_EDIT1, IDC_SPIN1}; + for(int control : controls) + GetDlgItem(control)->EnableWindow(FALSE); + } + UpdateSubsongName(); + OnFormatChanged(); + + SetDlgItemText(IDC_EDIT2, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.GetTitle()).c_str()); + SetDlgItemText(IDC_EDIT3, mpt::ToWin(m_sndFile.m_songArtist).c_str()); + if(!m_sndFile.GetFileHistory().empty()) + SetDlgItemText(IDC_EDIT4, mpt::ToWin(mpt::String::Replace(m_sndFile.GetFileHistory().back().AsISO8601().substr(0, 10), U_("-"), U_("/"))).c_str()); + SetDlgItemText(IDC_EDIT5, mpt::ToWin(m_sndFile.GetCharsetFile(), m_sndFile.m_songMessage.GetFormatted(SongMessage::leCRLF)).c_str()); + + m_locked = false; + return TRUE; + } + + void OnOK() override + { + mpt::PathString extension = P_("vgz"); + s_format = static_cast<ExportFormat>(GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3)); + if(s_format == ExportFormat::DRO) + extension = P_("dro"); + else if(s_format == ExportFormat::VGM) + extension = P_("vgm"); + + FileDialog dlg = SaveFileDialog() + .DefaultExtension(extension) + .DefaultFilename(m_modDoc.GetPathNameMpt().GetFileName().ReplaceExt(P_(".") + extension)) + .ExtensionFilter(MPT_UFORMAT("{} Files|*.{}||")(mpt::ToUpperCase(extension.ToUnicode()), extension)) + .WorkingDirectory(TrackerSettings::Instance().PathExport.GetWorkingDir()); + if(!dlg.Show()) + { + OnCancel(); + return; + } + TrackerSettings::Instance().PathExport.SetWorkingDir(dlg.GetWorkingDirectory()); + + DoConversion(dlg.GetFirstFile()); + + CProgressDialog::OnOK(); + } + + void OnCancel() override + { + if(m_conversionRunning) + CProgressDialog::OnCancel(); + else + CDialog::OnCancel(); + } + + void Run() override {} + + afx_msg void OnFormatChanged() + { + const int controls[] = {IDC_EDIT2, IDC_EDIT3, IDC_EDIT4, IDC_EDIT5}; + for(int control : controls) + GetDlgItem(control)->EnableWindow(GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3) == static_cast<int>(ExportFormat::DRO) ? FALSE : TRUE); + } + + afx_msg void OnSubsongChanged() + { + if(m_locked) + return; + CheckRadioButton(IDC_RADIO4, IDC_RADIO5, IDC_RADIO5); + BOOL ok = FALSE; + const auto newSubSong = std::clamp(static_cast<size_t>(GetDlgItemInt(IDC_EDIT1, &ok, FALSE)), size_t(1), m_subSongs.size()) - 1; + if(m_selectedSong == newSubSong || !ok) + return; + m_selectedSong = newSubSong; + UpdateSubsongName(); + } + + void UpdateSubsongName() + { + const auto subsongText = GetDlgItem(IDC_SUBSONG); + if(subsongText == nullptr || m_selectedSong >= m_subSongs.size()) + return; + const auto &song = m_subSongs[m_selectedSong]; + const auto sequenceName = m_sndFile.Order(song.sequence).GetName(); + const auto startPattern = m_sndFile.Order(song.sequence).PatternAt(song.startOrder); + const auto orderName = startPattern ? startPattern->GetName() : std::string{}; + subsongText->SetWindowText(MPT_TFORMAT("Sequence {}{}\nOrder {} to {}{}")( + song.sequence + 1, + sequenceName.empty() ? mpt::tstring{} : MPT_TFORMAT(" ({})")(sequenceName), + song.startOrder, + song.endOrder, + orderName.empty() ? mpt::tstring{} : MPT_TFORMAT(" ({})")(mpt::ToWin(m_sndFile.GetCharsetInternal(), orderName))) + .c_str()); + } + + void DoConversion(const mpt::PathString &fileName) + { + const int controls[] = {IDC_RADIO1, IDC_RADIO2, IDC_RADIO3, IDC_RADIO4, IDC_RADIO5, IDC_EDIT1, IDC_EDIT2, IDC_EDIT3, IDC_EDIT4, IDC_EDIT5, IDC_SPIN1, IDOK}; + for(int control : controls) + GetDlgItem(control)->EnableWindow(FALSE); + + BypassInputHandler bih; + CMainFrame::GetMainFrame()->StopMod(&m_modDoc); + + FileTags fileTags; + { + CString title, artist, date, notes; + GetDlgItemText(IDC_EDIT2, title); + GetDlgItemText(IDC_EDIT3, artist); + GetDlgItemText(IDC_EDIT4, date); + GetDlgItemText(IDC_EDIT5, notes); + fileTags.title = mpt::ToUnicode(title); + fileTags.artist = mpt::ToUnicode(artist); + fileTags.year = mpt::ToUnicode(date); + fileTags.comments = mpt::ToUnicode(notes); + } + + if(IsDlgButtonChecked(IDC_RADIO5)) + m_subSongs = {m_subSongs[m_selectedSong]}; + + SetRange(0, mpt::saturate_round<uint64>(std::accumulate(m_subSongs.begin(), m_subSongs.end(), 0.0, [](double acc, const auto &song) { return acc + song.duration; }) * m_sndFile.GetSampleRate())); + GetDlgItem(IDC_PROGRESS1)->ShowWindow(SW_SHOW); + + m_sndFile.m_bIsRendering = true; + + const auto origSettings = m_sndFile.m_MixerSettings; + auto newSettings = m_sndFile.m_MixerSettings; + if(s_format != ExportFormat::DRO) + newSettings.gdwMixingFreq = 44100; // required for VGM, DRO doesn't care + m_sndFile.SetMixerSettings(newSettings); + + const auto origSequence = m_sndFile.Order.GetCurrentSequenceIndex(); + const auto origRepeatCount = m_sndFile.GetRepeatCount(); + m_sndFile.SetRepeatCount(0); + + auto opl = std::move(m_sndFile.m_opl); + + const auto songIndexFmt = mpt::FormatSpec{}.Dec().FillNul().Width(1 + static_cast<int>(std::log10(m_subSongs.size()))); + + size_t totalSamples = 0; + for(size_t i = 0; i < m_subSongs.size() && !m_abort; i++) + { + const auto &song = m_subSongs[i]; + + m_sndFile.ResetPlayPos(); + m_sndFile.GetLength(eAdjust, GetLengthTarget(song.startOrder, song.startRow).StartPos(song.sequence, 0, 0)); + m_sndFile.m_SongFlags.reset(SONG_PLAY_FLAGS); + + m_oplLogger.Reset(); + m_sndFile.m_opl = std::make_unique<OPL>(m_oplLogger); + + auto prevTime = timeGetTime(); + CSoundFile::samplecount_t loopStart = std::numeric_limits<CSoundFile::samplecount_t>::max(), subsongSamples = 0; + while(!m_abort) + { + auto count = m_sndFile.ReadOneTick(); + if(count == 0) + break; + + if(loopStart == Util::MaxValueOfType(loopStart) + && m_sndFile.m_PlayState.m_nCurrentOrder == song.loopStartOrder && m_sndFile.m_PlayState.m_nRow == song.loopStartRow + && (song.loopStartOrder != song.startOrder || song.loopStartRow != song.startRow)) + { + loopStart = subsongSamples; + m_oplLogger.CaptureAllVoiceRegisters(); // Make sure all registers are in the correct state when looping back + } + + totalSamples += count; + subsongSamples += count; + + auto currentTime = timeGetTime(); + if(currentTime - prevTime >= 16) + { + prevTime = currentTime; + auto timeSec = subsongSamples / m_sndFile.GetSampleRate(); + SetWindowText(MPT_TFORMAT("Exporting Song {} / {}... {}:{}:{}")(i + 1, m_subSongs.size(), timeSec / 3600, mpt::cfmt::dec0<2>((timeSec / 60) % 60), mpt::cfmt::dec0<2>(timeSec % 60)).c_str()); + + SetProgress(totalSamples); + ProcessMessages(); + } + } + + if(m_sndFile.m_SongFlags[SONG_BREAKTOROW] && loopStart == Util::MaxValueOfType(loopStart) && song.loopStartOrder == song.startOrder && song.loopStartRow == song.startRow) + loopStart = 0; + + mpt::PathString currentFileName = fileName; + if(m_subSongs.size() > 1) + currentFileName = fileName.ReplaceExt(mpt::PathString::FromNative(MPT_TFORMAT(" ({})")(mpt::ufmt::fmt(i + 1, songIndexFmt))) + fileName.GetFileExt()); + mpt::SafeOutputFile sf(currentFileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &f = sf; + try + { + if(!f) + throw std::exception{}; + f.exceptions(f.exceptions() | std::ios::badbit | std::ios::failbit); + if(s_format == ExportFormat::DRO) + m_oplLogger.WriteDRO(f); + else if(s_format == ExportFormat::VGM) + m_oplLogger.WriteVGM(f, loopStart, fileTags); + else + m_oplLogger.WriteVGZ(f, loopStart, fileTags, currentFileName.ReplaceExt(P_(".vgm")).GetFullFileName().ToUnicode()); + } catch(const std::exception &) + { + Reporting::Error(MPT_UFORMAT("Unable to write to file {}!")(currentFileName)); + break; + } + } + + // Reset globals to previous values + m_sndFile.m_opl = std::move(opl); + m_sndFile.SetRepeatCount(origRepeatCount); + m_sndFile.Order.SetSequence(origSequence); + m_sndFile.ResetPlayPos(); + m_sndFile.SetMixerSettings(origSettings); + m_sndFile.m_bIsRendering = false; + } + + DECLARE_MESSAGE_MAP() +}; + +OPLExportDlg::ExportFormat OPLExportDlg::s_format = OPLExportDlg::ExportFormat::VGZ; + + +BEGIN_MESSAGE_MAP(OPLExportDlg, CDialog) + //{{AFX_MSG_MAP(OPLExportDlg) + ON_COMMAND(IDC_RADIO1, &OPLExportDlg::OnFormatChanged) + ON_COMMAND(IDC_RADIO2, &OPLExportDlg::OnFormatChanged) + ON_COMMAND(IDC_RADIO3, &OPLExportDlg::OnFormatChanged) + ON_EN_CHANGE(IDC_EDIT1, &OPLExportDlg::OnSubsongChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void CModDoc::OnFileOPLExport() +{ + bool anyOPL = false; + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + if(m_SndFile.GetSample(smp).uFlags[CHN_ADLIB]) + { + anyOPL = true; + break; + } + } + if(!anyOPL) + { + Reporting::Information(_T("This module does not use any OPL instruments."), _T("No OPL Instruments Found")); + return; + } + + OPLExportDlg dlg{*this, CMainFrame::GetMainFrame()}; + dlg.DoModal(); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.cpp new file mode 100644 index 00000000..782dae3c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.cpp @@ -0,0 +1,277 @@ +/* + * OPLInstrDlg.cpp + * --------------- + * Purpose: Editor for OPL-based synth instruments + * 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 "OPLInstrDlg.h" +#include "../soundlib/OPL.h" +#include "../soundlib/Sndfile.h" +#include "resource.h" +#include "Mainfrm.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(OPLInstrDlg, CDialog) + ON_WM_HSCROLL() + ON_MESSAGE(WM_MOD_DRAGONDROPPING, &OPLInstrDlg::OnDragonDropping) + ON_COMMAND(IDC_CHECK1, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK2, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK3, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK4, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK5, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK6, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK7, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK8, &OPLInstrDlg::ParamsChanged) + ON_COMMAND(IDC_CHECK9, &OPLInstrDlg::ParamsChanged) + ON_CBN_SELCHANGE(IDC_COMBO1, &OPLInstrDlg::ParamsChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &OPLInstrDlg::ParamsChanged) + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &OPLInstrDlg::OnToolTip) +END_MESSAGE_MAP() + + +void OPLInstrDlg::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_CHECK1, m_additive); + DDX_Control(pDX, IDC_SLIDER1, m_feedback); + + for(int op = 0; op < 2; op++) + { + const int slider = op * 7; + const int check = op * 4; + DDX_Control(pDX, IDC_SLIDER2 + slider, m_attackRate[op]); + DDX_Control(pDX, IDC_SLIDER3 + slider, m_decayRate[op]); + DDX_Control(pDX, IDC_SLIDER4 + slider, m_sustainLevel[op]); + DDX_Control(pDX, IDC_SLIDER5 + slider, m_releaseRate[op]); + DDX_Control(pDX, IDC_CHECK2 + check, m_sustain[op]); + DDX_Control(pDX, IDC_SLIDER6 + slider, m_volume[op]); + DDX_Control(pDX, IDC_CHECK3 + check, m_scaleEnv[op]); + DDX_Control(pDX, IDC_SLIDER7 + slider, m_levelScaling[op]); + DDX_Control(pDX, IDC_SLIDER8 + slider, m_freqMultiplier[op]); + DDX_Control(pDX, IDC_COMBO1 + op, m_waveform[op]); + DDX_Control(pDX, IDC_CHECK4 + check, m_vibrato[op]); + DDX_Control(pDX, IDC_CHECK5 + check, m_tremolo[op]); + } +} + + +OPLInstrDlg::OPLInstrDlg(CWnd &parent, const CSoundFile &sndFile) + : m_parent(parent) + , m_sndFile(sndFile) +{ + Create(IDD_OPL_PARAMS, &parent); + CRect rect; + GetClientRect(rect); + m_windowSize = rect.BottomRight(); +} + + +OPLInstrDlg::~OPLInstrDlg() +{ + DestroyWindow(); +} + + +BOOL OPLInstrDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + EnableToolTips(); + m_feedback.SetRange(0, 7); + for(int op = 0; op < 2; op++) + { + m_attackRate[op].SetRange(0, 15); + m_decayRate[op].SetRange(0, 15); + m_sustainLevel[op].SetRange(0, 15); + m_releaseRate[op].SetRange(0, 15); + m_volume[op].SetRange(0, 63); + m_volume[op].SetTicFreq(4); + m_levelScaling[op].SetRange(0, 3); + m_freqMultiplier[op].SetRange(0, 15); + } + + return TRUE; +} + + +BOOL OPLInstrDlg::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + // Forward key presses and drag&drop support to parent editor + if(pMsg->message == WM_SYSKEYUP || pMsg->message == WM_KEYUP || + pMsg->message == WM_SYSKEYDOWN || pMsg->message == WM_KEYDOWN || + pMsg->message == WM_DROPFILES) + { + if(pMsg->hwnd == m_hWnd) + { + pMsg->hwnd = m_parent.m_hWnd; + } + if(m_parent.PreTranslateMessage(pMsg)) + { + return TRUE; + } + } else if(pMsg->message == WM_CHAR) + { + // Avoid Windows sounds on note key repeats + if(HIWORD(pMsg->lParam) & 0x4000) + return TRUE; + } + } + return CDialog::PreTranslateMessage(pMsg); +} + + +LRESULT OPLInstrDlg::OnDragonDropping(WPARAM wParam, LPARAM lParam) +{ + return m_parent.SendMessage(WM_MOD_DRAGONDROPPING, wParam, lParam); +} + + +// Swap OPL Key Scale Level bits for a "human-readable" value. +static uint8 KeyScaleLevel(uint8 kslVolume) +{ + static constexpr uint8 KSLFix[4] = { 0x00, 0x80, 0x40, 0xC0 }; + return KSLFix[kslVolume >> 6]; +} + + +void OPLInstrDlg::SetPatch(OPLPatch &patch) +{ + SetRedraw(FALSE); + + m_additive.SetCheck((patch[10] & OPL::CONNECTION_BIT) ? BST_CHECKED : BST_UNCHECKED); + m_feedback.SetPos((patch[10] & OPL::FEEDBACK_MASK) >> 1); + for(int op = 0; op < 2; op++) + { + m_attackRate[op].SetPos(15 - (patch[4 + op] >> 4)); + m_decayRate[op].SetPos(15 - (patch[4 + op] & 0x0F)); + m_sustainLevel[op].SetPos(15 - (patch[6 + op] >> 4)); + m_releaseRate[op].SetPos(15 - (patch[6 + op] & 0x0F)); + m_volume[op].SetPos(63 - (patch[2 + op] & OPL::TOTAL_LEVEL_MASK)); + m_levelScaling[op].SetPos(KeyScaleLevel(patch[2 + op]) >> 6); + m_freqMultiplier[op].SetPos(patch[0 + op] & OPL::MULTIPLE_MASK); + + m_sustain[op].SetCheck((patch[0 + op] & OPL::SUSTAIN_ON) ? BST_CHECKED : BST_UNCHECKED); + m_scaleEnv[op].SetCheck((patch[0 + op] & OPL::KSR) ? BST_CHECKED : BST_UNCHECKED); + m_vibrato[op].SetCheck((patch[0 + op] & OPL::VIBRATO_ON) ? BST_CHECKED : BST_UNCHECKED); + m_tremolo[op].SetCheck((patch[0 + op] & OPL::TREMOLO_ON) ? BST_CHECKED : BST_UNCHECKED); + + const auto waveform = patch[8 + op]; + const int numWaveforms = (m_sndFile.GetType() == MOD_TYPE_S3M && waveform < 4) ? 4 : 8; + if(numWaveforms != m_waveform[op].GetCount()) + { + m_waveform[op].ResetContent(); + static constexpr const TCHAR *waveformNames[] = + { + _T("Sine"), _T("Half Sine"), _T("Absolute Sine"), _T("Pulse Sine"), + _T("Sine (Even Periods)"), _T("Absolute Sine (Even Periods)"), _T("Square"), _T("Derived Square") + }; + for(int i = 0; i < numWaveforms; i++) + { + m_waveform[op].AddString(waveformNames[i]); + } + } + m_waveform[op].SetCurSel(waveform); + } + SetRedraw(TRUE); + m_patch = &patch; +} + + +void OPLInstrDlg::ParamsChanged() +{ + OPLPatch patch{{}}; + if(m_additive.GetCheck() != BST_UNCHECKED) patch[10] |= OPL::CONNECTION_BIT; + patch[10] |= static_cast<uint8>(m_feedback.GetPos() << 1); + for(int op = 0; op < 2; op++) + { + patch[op] = static_cast<uint8>(m_freqMultiplier[op].GetPos()); + if(m_sustain[op].GetCheck() != BST_UNCHECKED) patch[op] |= OPL::SUSTAIN_ON; + if(m_scaleEnv[op].GetCheck() != BST_UNCHECKED) patch[op] |= OPL::KSR; + if(m_vibrato[op].GetCheck() != BST_UNCHECKED) patch[op] |= OPL::VIBRATO_ON; + if(m_tremolo[op].GetCheck() != BST_UNCHECKED) patch[op] |= OPL::TREMOLO_ON; + patch[2 + op] = static_cast<uint8>((63 - m_volume[op].GetPos()) | KeyScaleLevel(static_cast<uint8>(m_levelScaling[op].GetPos() << 6))); + patch[4 + op] = static_cast<uint8>(((15 - m_attackRate[op].GetPos()) << 4) | (15 - m_decayRate[op].GetPos())); + patch[6 + op] = static_cast<uint8>(((15 - m_sustainLevel[op].GetPos()) << 4) | (15 - m_releaseRate[op].GetPos())); + patch[8 + op] = static_cast<uint8>(m_waveform[op].GetCurSel()); + } + + if(*m_patch != patch) + { + m_parent.SendMessage(WM_MOD_VIEWMSG, VIEWMSG_PREPAREUNDO); + *m_patch = patch; + m_parent.SendMessage(WM_MOD_VIEWMSG, VIEWMSG_SETMODIFIED, SampleHint().Data().AsLPARAM()); + } +} + + +BOOL OPLInstrDlg::OnToolTip(UINT /*id*/, NMHDR *pNMHDR, LRESULT* /*pResult*/) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR; + UINT_PTR nID = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = ::GetDlgCtrlID((HWND)nID); + } + + static constexpr const char *feedback[] = {"disabled", "\xCF\x80/16", "\xCF\x80/8", "\xCF\x80/4", "\xCF\x80/2", "\xCF\x80", "2\xCF\x80", "4\xCF\x80"}; + static constexpr const TCHAR *ksl[] = {_T("disabled"), _T("1.5 dB / octave"), _T("3 dB / octave"), _T("6 dB / octave")}; + + mpt::tstring text; + const CWnd *wnd = GetDlgItem(static_cast<int>(nID)); + const CSliderCtrl *slider = static_cast<const CSliderCtrl *>(wnd); + switch(nID) + { + case IDC_SLIDER1: + // Feedback + text = mpt::ToWin(mpt::Charset::UTF8, feedback[slider->GetPos() & 7]); + break; + + case IDC_SLIDER2: + case IDC_SLIDER3: + case IDC_SLIDER5: + case IDC_SLIDER9: + case IDC_SLIDER10: + case IDC_SLIDER12: + // Attack / Decay / Release + text = _T("faster < ") + mpt::tfmt::val(slider->GetPos()) + _T(" > slower"); + break; + case IDC_SLIDER4: + case IDC_SLIDER11: + // Sustain Level + { + const int pos = slider->GetPos(); + text = mpt::tfmt::val((pos == 0) ? -93 : ((-15 + pos) * 3)) + _T(" dB"); + } + break; + case IDC_SLIDER6: + case IDC_SLIDER13: + // Volume Level + text = mpt::tfmt::fix((-63 + slider->GetPos()) * 0.75, 2) + _T(" dB"); + break; + case IDC_SLIDER7: + case IDC_SLIDER14: + // Key Scale Level + text = ksl[slider->GetPos() & 3]; + break; + case IDC_SLIDER8: + case IDC_SLIDER15: + // Frequency Multiplier + if(slider->GetPos() == 0) + text = _T("0.5"); + else + text = mpt::tfmt::val(slider->GetPos()); + break; + } + + mpt::String::WriteWinBuf(pTTT->szText) = text.c_str(); + return TRUE; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.h new file mode 100644 index 00000000..a784a7f2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/OPLInstrDlg.h @@ -0,0 +1,49 @@ +/* + * OPLInstrDlg.h + * ------------- + * Purpose: Editor for OPL-based synth instruments + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +class OPLInstrDlg : public CDialog +{ + CButton m_additive, m_sustain[2], m_scaleEnv[2], m_vibrato[2], m_tremolo[2]; + CSliderCtrl m_feedback, m_attackRate[2], m_decayRate[2], m_sustainLevel[2], m_releaseRate[2], m_volume[2], m_levelScaling[2], m_freqMultiplier[2]; + CComboBox m_waveform[2]; + CSize m_windowSize; + CWnd &m_parent; + const CSoundFile &m_sndFile; + OPLPatch *m_patch; + +public: + OPLInstrDlg(CWnd &parent, const CSoundFile &sndFile); + ~OPLInstrDlg(); + void SetPatch(OPLPatch &patch); + CSize GetMinimumSize() const { return m_windowSize; } + +protected: + void ParamsChanged(); + + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + BOOL PreTranslateMessage(MSG *pMsg) override; + void OnOK() override { } + void OnCancel() override { } + void OnHScroll(UINT, UINT, CScrollBar *) { ParamsChanged(); } + LRESULT OnDragonDropping(WPARAM wParam, LPARAM lParam); + BOOL OnToolTip(UINT id, NMHDR *pNMHDR, LRESULT *pResult); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.cpp new file mode 100644 index 00000000..6bfff1f5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.cpp @@ -0,0 +1,183 @@ +/* + * PSRatioCalc.cpp + * --------------- + * Purpose: Dialog for calculating sample pitch shift ratios in the sample editor. + * 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 "PSRatioCalc.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// CPSRatioCalc dialog + +IMPLEMENT_DYNAMIC(CPSRatioCalc, CDialog) +CPSRatioCalc::CPSRatioCalc(const CSoundFile &sndFile, SAMPLEINDEX sample, double ratio, CWnd* pParent /*=NULL*/) + : CDialog(IDD_PITCHSHIFT, pParent) + , m_dRatio(ratio) + , sndFile(sndFile) + , sampleIndex(sample) +{ + // Calculate/verify samplerate at C5. + const ModSample &smp = sndFile.GetSample(sampleIndex); + uint32 sampleRate = smp.GetSampleRate(sndFile.GetType()); + if(sampleRate <= 0) + sampleRate = 8363; + + m_nSpeed = sndFile.m_PlayState.m_nMusicSpeed; + m_nTempo = sndFile.m_PlayState.m_nMusicTempo; + + // Sample rate will not change. We can calculate original duration once and disgard sampleRate. + m_lMsOrig = static_cast<ULONGLONG>(1000.0 * ((double)smp.nLength / sampleRate)); + CalcSamples(); + CalcMs(); + CalcRows(); +} + + +void CPSRatioCalc::DoDataExchange(CDataExchange* pDX) +{ + CWnd* hasFocus = GetFocus(); + + CDialog::DoDataExchange(pDX); + SmpLength origLength = sndFile.GetSample(sampleIndex).nLength; + DDX_Text(pDX, IDC_SAMPLE_LENGTH_ORIGINAL, origLength); + DDX_Text(pDX, IDC_SAMPLE_LENGTH_NEW, m_lSamplesNew); + DDX_Text(pDX, IDC_MS_LENGTH_ORIGINAL2, m_lMsOrig); + DDX_Text(pDX, IDC_MS_LENGTH_NEW, m_lMsNew); + DDX_Text(pDX, IDC_SPEED, m_nSpeed); + DDX_Text(pDX, IDC_ROW_LENGTH_ORIGINAL, m_dRowsOrig); + + //These 2 CEdits must only be updated if they don't have focus (to preserve trailing . and 0s etc..) + if (pDX->m_bSaveAndValidate || hasFocus != GetDlgItem(IDC_ROW_LENGTH_NEW2)) + DDX_Text(pDX, IDC_ROW_LENGTH_NEW2, m_dRowsNew); + if (pDX->m_bSaveAndValidate || hasFocus != GetDlgItem(IDC_PSRATIO)) + DDX_Text(pDX, IDC_PSRATIO, m_dRatio); +} + + +BEGIN_MESSAGE_MAP(CPSRatioCalc, CDialog) + ON_EN_UPDATE(IDC_SAMPLE_LENGTH_NEW, &CPSRatioCalc::OnEnChangeSamples) + ON_EN_UPDATE(IDC_MS_LENGTH_NEW, &CPSRatioCalc::OnEnChangeMs) + ON_EN_UPDATE(IDC_SPEED, &CPSRatioCalc::OnEnChangeSpeed) + ON_EN_UPDATE(IDC_TEMPO, &CPSRatioCalc::OnEnChangeSpeed) + ON_EN_UPDATE(IDC_ROW_LENGTH_NEW2, &CPSRatioCalc::OnEnChangeRows) + ON_EN_UPDATE(IDC_PSRATIO, &CPSRatioCalc::OnEnChangeratio) + ON_BN_CLICKED(IDOK, &CPSRatioCalc::OnBnClickedOk) +END_MESSAGE_MAP() + + +BOOL CPSRatioCalc::OnInitDialog() +{ + CDialog::OnInitDialog(); + m_EditTempo.SubclassDlgItem(IDC_TEMPO, this); + m_EditTempo.AllowNegative(false); + m_EditTempo.SetTempoValue(m_nTempo); + return TRUE; +} + +// CPSRatioCalc message handlers +void CPSRatioCalc::OnEnChangeSamples() +{ + UpdateData(); + if (m_lSamplesNew && sndFile.GetSample(sampleIndex).nLength) + { + m_dRatio = (double)m_lSamplesNew / (double)sndFile.GetSample(sampleIndex).nLength * 100; + CalcMs(); + CalcRows(); + UpdateData(FALSE); + } +} + +void CPSRatioCalc::OnEnChangeMs() +{ + UpdateData(); + if (m_lMsOrig && m_lMsNew) + { + m_dRatio = (double)m_lMsNew / (double)m_lMsOrig * 100; + CalcSamples(); + CalcRows(); + UpdateData(FALSE); + } + +} + +void CPSRatioCalc::OnEnChangeRows() +{ + UpdateData(); + if (m_dRowsOrig && m_dRowsNew) + { + m_dRatio = m_dRowsNew / m_dRowsOrig * 100.0; + CalcSamples(); + CalcMs(); + UpdateData(FALSE); + } + +} + +void CPSRatioCalc::OnEnChangeSpeed() +{ + UpdateData(); + m_nTempo = m_EditTempo.GetTempoValue(); + if (m_nTempo < TEMPO(1, 0)) m_nTempo = TEMPO(1, 0); + if (m_nSpeed < 1) m_nSpeed = 1; + CalcRows(); + UpdateData(FALSE); +} + +void CPSRatioCalc::OnEnChangeratio() +{ + UpdateData(); + if (m_dRatio) + { + CalcSamples(); + CalcMs(); + CalcRows(); + UpdateData(FALSE); + } +} + +// CPSRatioCalc Internal Calculations: + +void CPSRatioCalc::CalcSamples() +{ + m_lSamplesNew = static_cast<ULONGLONG>(sndFile.GetSample(sampleIndex).nLength * (m_dRatio / 100.0)); + return; +} + +void CPSRatioCalc::CalcMs() +{ + m_lMsNew = static_cast<ULONGLONG>(m_lMsOrig * (m_dRatio / 100.0)); + return; +} + +void CPSRatioCalc::CalcRows() +{ + double rowTime = sndFile.GetRowDuration(m_nTempo, m_nSpeed); + + m_dRowsOrig = (double)m_lMsOrig / rowTime; + m_dRowsNew = m_dRowsOrig * (m_dRatio / 100); + + return; +} + +void CPSRatioCalc::OnBnClickedOk() +{ + if (m_dRatio<50.0 || m_dRatio>200.0) + { + Reporting::Error("Error: ratio must be between 50% and 200%."); + return; + } + OnOK(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.h b/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.h new file mode 100644 index 00000000..2c70106c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PSRatioCalc.h @@ -0,0 +1,56 @@ +/* + * PSRatioCalc.h + * ------------- + * Purpose: Dialog for calculating sample pitch shift ratios in the sample editor. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +class CPSRatioCalc : public CDialog +{ + DECLARE_DYNAMIC(CPSRatioCalc) + +public: + CPSRatioCalc(const CSoundFile &sndFile, SAMPLEINDEX sample, double ratio, CWnd* pParent = NULL); // standard constructor + double m_dRatio; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + virtual BOOL OnInitDialog(); + + CNumberEdit m_EditTempo; + const CSoundFile &sndFile; + SAMPLEINDEX sampleIndex; + + ULONGLONG m_lSamplesNew; + ULONGLONG m_lMsNew, m_lMsOrig; + double m_dRowsOrig, m_dRowsNew; + uint32 m_nSpeed; + TEMPO m_nTempo; + + afx_msg void OnEnChangeSamples(); + afx_msg void OnEnChangeMs(); + afx_msg void OnEnChangeSpeed(); + afx_msg void OnEnChangeRows(); + afx_msg void OnEnChangeratio(); + + void CalcSamples(); + void CalcMs(); + void CalcRows(); + + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnBnClickedOk(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.cpp new file mode 100644 index 00000000..1e12c19b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.cpp @@ -0,0 +1,192 @@ +/* + * PathConfigDlg.cpp + * ----------------- + * Purpose: Default paths and auto save setup dialog. + * 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 "resource.h" +#include "PathConfigDlg.h" +#include "FileDialog.h" +#include "Mainfrm.h" + +OPENMPT_NAMESPACE_BEGIN + +static constexpr std::pair<ConfigurableDirectory TrackerSettings::*, int> PathSettings[] = +{ + { &TrackerSettings::PathSongs, IDC_OPTIONS_DIR_MODS }, + { &TrackerSettings::PathSamples, IDC_OPTIONS_DIR_SAMPS }, + { &TrackerSettings::PathInstruments, IDC_OPTIONS_DIR_INSTS }, + { &TrackerSettings::PathPlugins, IDC_OPTIONS_DIR_VSTS }, + { &TrackerSettings::PathPluginPresets, IDC_OPTIONS_DIR_VSTPRESETS }, + { &TrackerSettings::AutosavePath, IDC_AUTOSAVE_PATH }, +}; + +IMPLEMENT_DYNAMIC(PathConfigDlg, CPropertyPage) + +PathConfigDlg::PathConfigDlg() + : CPropertyPage(IDD_OPTIONS_AUTOSAVE) +{ +} + + +void PathConfigDlg::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); +} + +BEGIN_MESSAGE_MAP(PathConfigDlg, CPropertyPage) + // Paths + ON_EN_CHANGE(IDC_OPTIONS_DIR_MODS, &PathConfigDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_OPTIONS_DIR_SAMPS, &PathConfigDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_OPTIONS_DIR_INSTS, &PathConfigDlg::OnSettingsChanged) + ON_EN_CHANGE(IDC_OPTIONS_DIR_VSTPRESETS, &PathConfigDlg::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON_CHANGE_MODDIR, &PathConfigDlg::OnBrowseSongs) + ON_COMMAND(IDC_BUTTON_CHANGE_SAMPDIR, &PathConfigDlg::OnBrowseSamples) + ON_COMMAND(IDC_BUTTON_CHANGE_INSTRDIR, &PathConfigDlg::OnBrowseInstruments) + ON_COMMAND(IDC_BUTTON_CHANGE_VSTDIR, &PathConfigDlg::OnBrowsePlugins) + ON_COMMAND(IDC_BUTTON_CHANGE_VSTPRESETSDIR, &PathConfigDlg::OnBrowsePresets) + + // Autosave + ON_COMMAND(IDC_CHECK1, &PathConfigDlg::OnSettingsChanged) + ON_BN_CLICKED(IDC_AUTOSAVE_BROWSE, &PathConfigDlg::OnBrowseAutosavePath) + ON_BN_CLICKED(IDC_AUTOSAVE_ENABLE, &PathConfigDlg::OnAutosaveEnable) + ON_BN_CLICKED(IDC_AUTOSAVE_USEORIGDIR, &PathConfigDlg::OnAutosaveUseOrigDir) + ON_BN_CLICKED(IDC_AUTOSAVE_USECUSTOMDIR, &PathConfigDlg::OnAutosaveUseOrigDir) + ON_EN_UPDATE(IDC_AUTOSAVE_PATH, &PathConfigDlg::OnSettingsChanged) + ON_EN_UPDATE(IDC_AUTOSAVE_HISTORY, &PathConfigDlg::OnSettingsChanged) + ON_EN_UPDATE(IDC_AUTOSAVE_INTERVAL, &PathConfigDlg::OnSettingsChanged) +END_MESSAGE_MAP() + + +BOOL PathConfigDlg::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + const auto &settings = TrackerSettings::Instance(); + // Default paths + for(const auto & [path, id] : PathSettings) + { + SetDlgItemText(id, (settings.*path).GetDefaultDir().AsNative().c_str()); + } + + // Autosave + CheckDlgButton(IDC_CHECK1, settings.CreateBackupFiles ? BST_CHECKED : BST_UNCHECKED); + + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1))->SetRange32(1, int32_max); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN2))->SetRange32(1, int32_max); + CheckDlgButton(IDC_AUTOSAVE_ENABLE, settings.AutosaveEnabled ? BST_CHECKED : BST_UNCHECKED); + SetDlgItemInt(IDC_AUTOSAVE_HISTORY, settings.AutosaveHistoryDepth); + SetDlgItemInt(IDC_AUTOSAVE_INTERVAL, settings.AutosaveIntervalMinutes); + CheckDlgButton(IDC_AUTOSAVE_USEORIGDIR, settings.AutosaveUseOriginalPath ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_AUTOSAVE_USECUSTOMDIR, settings.AutosaveUseOriginalPath ? BST_UNCHECKED : BST_CHECKED); + //enable/disable stuff as appropriate + OnAutosaveEnable(); + OnAutosaveUseOrigDir(); + + return TRUE; +} + + +mpt::PathString PathConfigDlg::GetPath(int id) +{ + return mpt::PathString::FromCString(GetWindowTextString(*GetDlgItem(id))).EnsureTrailingSlash(); +} + + +void PathConfigDlg::OnOK() +{ + auto &settings = TrackerSettings::Instance(); + // Default paths + for(const auto &path : PathSettings) + { + (settings.*path.first).SetDefaultDir(GetPath(path.second)); + } + + // Autosave + settings.CreateBackupFiles = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + + settings.AutosaveEnabled = (IsDlgButtonChecked(IDC_AUTOSAVE_ENABLE) != BST_UNCHECKED); + settings.AutosaveHistoryDepth = (GetDlgItemInt(IDC_AUTOSAVE_HISTORY)); + settings.AutosaveIntervalMinutes = (GetDlgItemInt(IDC_AUTOSAVE_INTERVAL)); + settings.AutosaveUseOriginalPath = (IsDlgButtonChecked(IDC_AUTOSAVE_USEORIGDIR) == BST_CHECKED); + + CPropertyPage::OnOK(); +} + + +void PathConfigDlg::BrowseFolder(UINT nID) +{ + const TCHAR *prompt = (nID == IDC_AUTOSAVE_PATH) + ? _T("Select a folder to store autosaved files in...") + : _T("Select a default folder..."); + BrowseForFolder dlg(GetPath(nID), prompt); + if(dlg.Show(this)) + { + SetDlgItemText(nID, dlg.GetDirectory().AsNative().c_str()); + OnSettingsChanged(); + } +} + + +void PathConfigDlg::OnAutosaveEnable() +{ + BOOL enabled = IsDlgButtonChecked(IDC_AUTOSAVE_ENABLE); + GetDlgItem(IDC_AUTOSAVE_INTERVAL)->EnableWindow(enabled); + GetDlgItem(IDC_SPIN1)->EnableWindow(enabled); + GetDlgItem(IDC_AUTOSAVE_HISTORY)->EnableWindow(enabled); + GetDlgItem(IDC_SPIN2)->EnableWindow(enabled); + GetDlgItem(IDC_AUTOSAVE_USEORIGDIR)->EnableWindow(enabled); + GetDlgItem(IDC_AUTOSAVE_USECUSTOMDIR)->EnableWindow(enabled); + GetDlgItem(IDC_AUTOSAVE_PATH)->EnableWindow(enabled); + GetDlgItem(IDC_AUTOSAVE_BROWSE)->EnableWindow(enabled); + OnSettingsChanged(); + return; +} + + +void PathConfigDlg::OnAutosaveUseOrigDir() +{ + if (IsDlgButtonChecked(IDC_AUTOSAVE_ENABLE)) + { + BOOL enabled = IsDlgButtonChecked(IDC_AUTOSAVE_USEORIGDIR); + GetDlgItem(IDC_AUTOSAVE_PATH)->EnableWindow(!enabled); + GetDlgItem(IDC_AUTOSAVE_BROWSE)->EnableWindow(!enabled); + OnSettingsChanged(); + } + return; +} + + +void PathConfigDlg::OnSettingsChanged() +{ + SetModified(TRUE); +} + + +BOOL PathConfigDlg::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_PATHS; + return CPropertyPage::OnSetActive(); +} + + +BOOL PathConfigDlg::OnKillActive() +{ + mpt::PathString path = GetPath(IDC_AUTOSAVE_PATH); + + if (!path.IsDirectory() && IsDlgButtonChecked(IDC_AUTOSAVE_ENABLE) && !IsDlgButtonChecked(IDC_AUTOSAVE_USEORIGDIR)) + { + Reporting::Error("Backup path does not exist."); + GetDlgItem(IDC_AUTOSAVE_PATH)->SetFocus(); + return 0; + } + + return CPropertyPage::OnKillActive(); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.h new file mode 100644 index 00000000..e07a52b0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PathConfigDlg.h @@ -0,0 +1,50 @@ +/* + * PathConfigDlg.h + * --------------- + * Purpose: Default paths and auto save setup dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class PathConfigDlg : public CPropertyPage +{ + DECLARE_DYNAMIC(PathConfigDlg) + +public: + PathConfigDlg(); + +protected: + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + + void OnOK() override; + BOOL OnInitDialog() override; + BOOL OnKillActive() override; + + afx_msg void OnAutosaveEnable(); + afx_msg void OnAutosaveUseOrigDir(); + afx_msg void OnBrowseAutosavePath() { BrowseFolder(IDC_AUTOSAVE_PATH); } + afx_msg void OnBrowseSongs() { BrowseFolder(IDC_OPTIONS_DIR_MODS); } + afx_msg void OnBrowseSamples() { BrowseFolder(IDC_OPTIONS_DIR_SAMPS); } + afx_msg void OnBrowseInstruments() { BrowseFolder(IDC_OPTIONS_DIR_INSTS); } + afx_msg void OnBrowsePlugins() { BrowseFolder(IDC_OPTIONS_DIR_VSTS); } + afx_msg void OnBrowsePresets() { BrowseFolder(IDC_OPTIONS_DIR_VSTPRESETS); } + + void OnSettingsChanged(); + BOOL OnSetActive() override; + + void BrowseFolder(UINT nID); + + mpt::PathString GetPath(int id); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.cpp new file mode 100644 index 00000000..930786ea --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.cpp @@ -0,0 +1,1227 @@ +/* + * PatternClipboard.cpp + * -------------------- + * Purpose: Implementation of the pattern clipboard mechanism + * 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 "PatternClipboard.h" +#include "PatternCursor.h" +#include "Mainfrm.h" +#include "Moddoc.h" +#include "Clipboard.h" +#include "View_pat.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/Tables.h" + + +OPENMPT_NAMESPACE_BEGIN + + +/* Clipboard format: + * Hdr: "ModPlug Tracker S3M\r\n" + * Full: '|C#401v64A06' + * Reset: '|...........' + * Empty: '| ' + * End of row: '\r\n' + * + * When pasting multiple patterns, the header line is followed by the order list: + * Orders: 0,1,2,+,-,1\r\n + * After that, the individual pattern headers and pattern data follows: + * 'Rows: 64\r\n' (must be first) + * 'Name: Pattern Name\r\n' (optional) + * 'Signature: 4/16\r\n' (optional) + * 'Swing: 16777216,16777216,16777216,16777216\r\n' (optional) + * Pattern data... + */ + +PatternClipboard PatternClipboard::instance; + +std::string PatternClipboard::GetFileExtension(const char *ext, bool addPadding) +{ + std::string format(ext); + if(format.size() > 3) + { + format.resize(3); + } + format = mpt::ToUpperCaseAscii(format); + if(addPadding) + { + format.insert(0, 3 - format.size(), ' '); + } + return format; +} + + +std::string PatternClipboard::FormatClipboardHeader(const CSoundFile &sndFile) +{ + return "ModPlug Tracker " + GetFileExtension(sndFile.GetModSpecifications().fileExtension, true) + "\r\n"; +} + + +// Copy a range of patterns to both the system clipboard and the internal clipboard. +bool PatternClipboard::Copy(const CSoundFile &sndFile, ORDERINDEX first, ORDERINDEX last, bool onlyOrders) +{ + const ModSequence &order = sndFile.Order(); + LimitMax(first, order.GetLength()); + LimitMax(last, order.GetLength()); + + // Set up clipboard header. + std::string data = FormatClipboardHeader(sndFile) + "Orders: "; + std::string patternData; + + // Pattern => Order list assignment + std::vector<PATTERNINDEX> patList(sndFile.Patterns.Size(), PATTERNINDEX_INVALID); + PATTERNINDEX insertedPats = 0; + + // Add order list and pattern information to header. + for(ORDERINDEX ord = first; ord <= last; ord++) + { + PATTERNINDEX pattern = order[ord]; + + if(ord != first) + data += ','; + + if(pattern == order.GetInvalidPatIndex()) + { + data += '-'; + } else if(pattern == order.GetIgnoreIndex()) + { + data += '+'; + } else if(sndFile.Patterns.IsValidPat(pattern)) + { + if(onlyOrders) + { + patList[pattern] = pattern; + } else if(patList[pattern] == PATTERNINDEX_INVALID) + { + // New pattern + patList[pattern] = insertedPats++; + + const CPattern &pat = sndFile.Patterns[pattern]; + patternData += MPT_AFORMAT("Rows: {}\r\n")(pat.GetNumRows()); + std::string name = pat.GetName(); + if(!name.empty()) + { + patternData += "Name: " + name + "\r\n"; + } + if(pat.GetOverrideSignature()) + { + patternData += MPT_AFORMAT("Signature: {}/{}\r\n")(pat.GetRowsPerBeat(), pat.GetRowsPerMeasure()); + } + if(pat.HasTempoSwing()) + { + patternData += "Swing: "; + const TempoSwing &swing = pat.GetTempoSwing(); + for(size_t i = 0; i < swing.size(); i++) + { + if(i == 0) + { + patternData += MPT_AFORMAT("{}")(swing[i]); + } else + { + patternData += MPT_AFORMAT(",{}")(swing[i]); + } + } + patternData += "\r\n"; + } + patternData += CreateClipboardString(sndFile, pattern, PatternRect(PatternCursor(), PatternCursor(sndFile.Patterns[pattern].GetNumRows() - 1, sndFile.GetNumChannels() - 1, PatternCursor::lastColumn))); + } + + data += mpt::afmt::val(patList[pattern]); + } + } + if(!onlyOrders) + { + data += "\r\n" + patternData; + } + + if(instance.m_activeClipboard < instance.m_clipboards.size()) + { + // Copy to internal clipboard + CString desc = MPT_CFORMAT("{} {} ({} to {})")(last - first + 1, onlyOrders ? CString(_T("Orders")) : CString(_T("Patterns")), first, last); + instance.m_clipboards[instance.m_activeClipboard] = {data, desc}; + } + + return ToSystemClipboard(data); +} + + +// Copy a pattern selection to both the system clipboard and the internal clipboard. +bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection) +{ + std::string data = CreateClipboardString(sndFile, pattern, selection); + if(data.empty()) + return false; + + // Set up clipboard header + data.insert(0, FormatClipboardHeader(sndFile)); + + if(instance.m_activeClipboard < instance.m_clipboards.size()) + { + // Copy to internal clipboard + CString desc; + desc.Format(_T("%u rows, %u channels (pattern %u)"), selection.GetNumRows(), selection.GetNumChannels(), pattern); + instance.m_clipboards[instance.m_activeClipboard] = {data, desc}; + } + + return ToSystemClipboard(data); +} + + +// Copy a pattern or pattern channel to the internal pattern or channel clipboard. +bool PatternClipboard::Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel) +{ + if(!sndFile.Patterns.IsValidPat(pattern)) + return false; + + const bool patternCopy = (channel == CHANNELINDEX_INVALID); + const CPattern &pat = sndFile.Patterns[pattern]; + PatternRect selection; + if(patternCopy) + selection = {PatternCursor(0, 0, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, pat.GetNumChannels() - 1, PatternCursor::lastColumn)}; + else + selection = {PatternCursor(0, channel, PatternCursor::firstColumn), PatternCursor(pat.GetNumRows() - 1, channel, PatternCursor::lastColumn)}; + + std::string data = CreateClipboardString(sndFile, pattern, selection); + if(data.empty()) + return false; + + // Set up clipboard header + data.insert(0, FormatClipboardHeader(sndFile)); + + // Copy to internal clipboard + (patternCopy ? instance.m_patternClipboard : instance.m_channelClipboard) = {data, {}}; + return true; +} + + +// Create the clipboard text for a pattern selection +std::string PatternClipboard::CreateClipboardString(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection) +{ + if(!sndFile.Patterns.IsValidPat(pattern)) + return ""; + + if(selection.GetStartColumn() == PatternCursor::paramColumn) + { + // Special case: If selection starts with a parameter column, extend it to include the effect letter as well. + PatternCursor upper(selection.GetUpperLeft()); + upper.Move(0, 0, -1); + selection = PatternRect(upper, selection.GetLowerRight()); + } + + const ROWINDEX startRow = selection.GetStartRow(), numRows = selection.GetNumRows(); + const CHANNELINDEX startChan = selection.GetStartChannel(), numChans = selection.GetNumChannels(); + + std::string data; + data.reserve(numRows * (numChans * 12 + 2)); + + for(ROWINDEX row = 0; row < numRows; row++) + { + if(row + startRow >= sndFile.Patterns[pattern].GetNumRows()) + break; + + const ModCommand *m = sndFile.Patterns[pattern].GetpModCommand(row + startRow, startChan); + + for(CHANNELINDEX chn = 0; chn < numChans; chn++, m++) + { + PatternCursor cursor(0, startChan + chn); + data += '|'; + + // Note + if(selection.ContainsHorizontal(cursor)) + { + if(m->IsNote()) + { + // Need to guarantee that sharps are used for the clipboard. + data += mpt::ToCharset(mpt::Charset::Locale, mpt::ustring(NoteNamesSharp[(m->note - NOTE_MIN) % 12])); + data += ('0' + (m->note - NOTE_MIN) / 12); + } else + { + data += mpt::ToCharset(mpt::Charset::Locale, sndFile.GetNoteName(m->note)); + } + } else + { + // No note + data += " "; + } + + // Instrument + cursor.Move(0, 0, 1); + if(selection.ContainsHorizontal(cursor)) + { + if(m->instr) + { + data += ('0' + (m->instr / 10)); + data += ('0' + (m->instr % 10)); + } else + { + data += ".."; + } + } else + { + data += " "; + } + + // Volume + cursor.Move(0, 0, 1); + if(selection.ContainsHorizontal(cursor)) + { + if(m->IsPcNote()) + { + data += mpt::afmt::dec0<3>(m->GetValueVolCol()); + } + else + { + if(m->volcmd != VOLCMD_NONE && m->vol <= 99) + { + data += sndFile.GetModSpecifications().GetVolEffectLetter(m->volcmd); + data += mpt::afmt::dec0<2>(m->vol); + } else + { + data += "..."; + } + } + } else + { + data += " "; + } + + // Effect + cursor.Move(0, 0, 1); + if(selection.ContainsHorizontal(cursor)) + { + if(m->IsPcNote()) + { + data += mpt::afmt::dec0<3>(m->GetValueEffectCol()); + } + else + { + if(m->command != CMD_NONE) + { + data += sndFile.GetModSpecifications().GetEffectLetter(m->command); + } else + { + data += '.'; + } + + if(m->param != 0 && m->command != CMD_NONE) + { + data += mpt::afmt::HEX0<2>(m->param); + } else + { + data += ".."; + } + } + } else + { + data += " "; + } + } + + // Next Row + data += "\r\n"; + } + + return data; +} + + +// Try pasting a pattern selection from the system clipboard. +bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, bool &orderChanged) +{ + std::string data; + if(!FromSystemClipboard(data) || !HandlePaste(sndFile, pastePos, mode, data, pasteRect, orderChanged)) + { + // Fall back to internal clipboard if there's no valid pattern data in the system clipboard. + return Paste(sndFile, pastePos, mode, pasteRect, instance.m_activeClipboard, orderChanged); + } + return true; +} + + +// Try pasting a pattern selection from an internal clipboard. +bool PatternClipboard::Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, clipindex_t internalClipboard, bool &orderChanged) +{ + if(internalClipboard >= instance.m_clipboards.size()) + return false; + + return HandlePaste(sndFile, pastePos, mode, instance.m_clipboards[internalClipboard].content, pasteRect, orderChanged); +} + + +// Paste from pattern or channel clipboard. +bool PatternClipboard::Paste(CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel) +{ + PatternEditPos pastePos{0, ORDERINDEX_INVALID, pattern, channel != CHANNELINDEX_INVALID ? channel : CHANNELINDEX(0)}; + PatternRect pasteRect; + bool orderChanged = false; + return HandlePaste(sndFile, pastePos, pmOverwrite, (channel == CHANNELINDEX_INVALID ? instance.m_patternClipboard : instance.m_channelClipboard).content, pasteRect, orderChanged); +} + + +// Parse clipboard string and perform the pasting operation. +bool PatternClipboard::HandlePaste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, const std::string &data, PatternRect &pasteRect, bool &orderChanged) +{ + const std::string whitespace(" \n\r\t"); + PATTERNINDEX pattern = pastePos.pattern; + ORDERINDEX &curOrder = pastePos.order; + orderChanged = false; + if(sndFile.GetpModDoc() == nullptr) + return false; + + CModDoc &modDoc = *(sndFile.GetpModDoc()); + ModSequence &order = sndFile.Order(); + + bool success = false; + bool prepareUndo = true; // Prepare pattern for undo next time + bool firstUndo = true; // For chaining undos (see overflow / multi-pattern paste) + + // Search for signature + std::string::size_type pos, startPos = 0; + MODTYPE pasteFormat = MOD_TYPE_NONE; + while(pasteFormat == MOD_TYPE_NONE && (startPos = data.find("ModPlug Tracker ", startPos)) != std::string::npos) + { + startPos += 16; + // Check paste format + const std::string format = mpt::ToUpperCaseAscii(mpt::trim(data.substr(startPos, 3))); + + for(const auto &spec : ModSpecs::Collection) + { + if(format == GetFileExtension(spec->fileExtension, false)) + { + pasteFormat = spec->internalType; + startPos += 3; + break; + } + } + } + + // What is this I don't even + if(startPos == std::string::npos) + return false; + // Skip whitespaces + startPos = data.find_first_not_of(whitespace, startPos); + if(startPos == std::string::npos) + return false; + + // Multi-order stuff + std::vector<PATTERNINDEX> patList; + // Multi-order mix-paste stuff + std::vector<ORDERINDEX> ordList; + std::vector<std::string::size_type> patOffset; + + enum { kSinglePaste, kMultiInsert, kMultiOverwrite } patternMode = kSinglePaste; + if(data.substr(startPos, 8) == "Orders: ") + { + // Pasting several patterns at once. + patternMode = (mode == pmOverwrite) ? kMultiInsert : kMultiOverwrite; + + // Put new patterns after current pattern, if it exists + if(order.IsValidPat(curOrder) && patternMode == kMultiInsert) + curOrder++; + + pos = startPos + 8; + startPos = data.find('\n', pos); + ORDERINDEX writeOrder = curOrder; + const bool onlyOrders = (startPos == std::string::npos); + if(onlyOrders) + { + // Only create order list, no patterns + startPos = data.size(); + } else + { + startPos++; + } + + while(pos < startPos && pos != std::string::npos) + { + PATTERNINDEX insertPat; + auto curPos = pos; + // Next order item, please + pos = data.find(',', pos + 1); + if(pos != std::string::npos) + pos++; + + if(data[curPos] == '+') + { + insertPat = order.GetIgnoreIndex(); + } else if(data[curPos] == '-') + { + insertPat = order.GetInvalidPatIndex(); + } else + { + insertPat = ConvertStrTo<PATTERNINDEX>(data.substr(curPos, 10)); + if(patternMode == kMultiOverwrite) + { + // We only want the order of pasted patterns now, do not create any new patterns + ordList.push_back(insertPat); + continue; + } + + if(insertPat < patList.size() && patList[insertPat] != PATTERNINDEX_INVALID) + { + // Duplicate pattern + insertPat = patList[insertPat]; + } else if(!onlyOrders) + { + // New pattern + if(insertPat >= patList.size()) + { + patList.resize(insertPat + 1, PATTERNINDEX_INVALID); + } + + patList[insertPat] = modDoc.InsertPattern(64); + insertPat = patList[insertPat]; + } + } + + if((insertPat == order.GetIgnoreIndex() && !sndFile.GetModSpecifications().hasIgnoreIndex) + || (insertPat == order.GetInvalidPatIndex() && !sndFile.GetModSpecifications().hasStopIndex) + || insertPat == PATTERNINDEX_INVALID + || patternMode == kMultiOverwrite) + { + continue; + } + + if(order.insert(writeOrder, 1) == 0) + { + break; + } + order[writeOrder++] = insertPat; + orderChanged = true; + } + + if(patternMode == kMultiInsert) + { + if(!patList.empty()) + { + // First pattern we're going to paste in. + pattern = patList[0]; + } + + // We already modified the order list... + success = true; + pastePos.pattern = pattern; + pastePos.row = 0; + pastePos.channel = 0; + } else + { + if(ordList.empty()) + return success; + // Find pattern offsets + pos = startPos; + patOffset.reserve(ordList.size()); + bool patStart = false; + while((pos = data.find_first_not_of(whitespace, pos)) != std::string::npos) + { + auto eol = data.find('\n', pos + 1); + if(eol == std::string::npos) + eol = data.size(); + if(data.substr(pos, 6) == "Rows: ") + { + patStart = true; + } else if(data.substr(pos, 1) == "|" && patStart) + { + patOffset.push_back(pos); + patStart = false; + } + pos = eol; + } + if(patOffset.empty()) + return success; + startPos = patOffset[0]; + } + } + + size_t curPattern = 0; // Currently pasted pattern for multi-paste + ROWINDEX startRow = pastePos.row; + ROWINDEX curRow = startRow; + CHANNELINDEX startChan = pastePos.channel, col; + + // Can we actually paste at this position? + if(!sndFile.Patterns.IsValidPat(pattern) || startRow >= sndFile.Patterns[pattern].GetNumRows() || startChan >= sndFile.GetNumChannels()) + { + return success; + } + + const CModSpecifications &sourceSpecs = CSoundFile::GetModSpecifications(pasteFormat); + const bool overflowPaste = (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OVERFLOWPASTE) && mode != pmPasteFlood && mode != pmPushForward && patternMode != kMultiInsert && curOrder != ORDERINDEX_INVALID; + const bool doITStyleMix = (mode == pmMixPasteIT); + const bool doMixPaste = (mode == pmMixPaste) || doITStyleMix; + const bool clipboardHasS3MCommands = (pasteFormat & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_S3M)); + const bool insertNewPatterns = overflowPaste && (patternMode == kMultiOverwrite); + + PatternCursor startPoint(startRow, startChan, PatternCursor::lastColumn), endPoint(startRow, startChan, PatternCursor::firstColumn); + ModCommand *patData = sndFile.Patterns[pattern].GetpModCommand(startRow, 0); + + auto multiPastePos = ordList.cbegin(); + pos = startPos; + + while(curRow < sndFile.Patterns[pattern].GetNumRows() || overflowPaste || patternMode == kMultiInsert) + { + // Parse next line + pos = data.find_first_not_of(whitespace, pos); + if(pos == std::string::npos) + { + // End of paste + if(mode == pmPasteFlood && curRow != startRow && curRow < sndFile.Patterns[pattern].GetNumRows()) + { + // Restarting pasting from beginning. + pos = startPos; + multiPastePos = ordList.cbegin(); + continue; + } else + { + // Prevent infinite loop with malformed clipboard data. + break; + } + } + auto eol = data.find('\n', pos + 1); + if(eol == std::string::npos) + eol = data.size(); + + // Handle multi-paste: Read pattern information + if(patternMode != kSinglePaste) + { + // Parse pattern header lines + bool parsedLine = true; + if(data.substr(pos, 6) == "Rows: ") + { + pos += 6; + // Advance to next pattern + if(patternMode == kMultiOverwrite) + { + // In case of multi-pattern mix-paste, we know that we reached the end of the previous pattern and need to parse the next order now. + multiPastePos++; + if(multiPastePos == ordList.cend() || *multiPastePos >= patOffset.size()) + pos = data.size(); + else + pos = patOffset[*multiPastePos]; + continue; + } + + // Otherwise, parse this pattern header normally. + do + { + if(curPattern >= patList.size()) + { + return success; + } + pattern = patList[curPattern++]; + } while (pattern == PATTERNINDEX_INVALID); + ROWINDEX numRows = ConvertStrTo<ROWINDEX>(data.substr(pos, 10)); + sndFile.Patterns[pattern].Resize(numRows); + patData = sndFile.Patterns[pattern].GetpModCommand(0, 0); + curRow = 0; + prepareUndo = true; + } else if(data.substr(pos, 6) == "Name: ") + { + pos += 6; + auto name = mpt::trim_right(data.substr(pos, eol - pos - 1)); + sndFile.Patterns[pattern].SetName(name); + } else if(data.substr(pos, 11) == "Signature: ") + { + pos += 11; + auto pos2 = data.find("/", pos + 1); + if(pos2 != std::string::npos) + { + pos2++; + ROWINDEX rpb = ConvertStrTo<ROWINDEX>(data.substr(pos, pos2 - pos)); + ROWINDEX rpm = ConvertStrTo<ROWINDEX>(data.substr(pos2, eol - pos2)); + sndFile.Patterns[pattern].SetSignature(rpb, rpm); + } + } else if(data.substr(pos, 7) == "Swing: ") + { + pos += 7; + TempoSwing swing; + swing.resize(sndFile.Patterns[pattern].GetRowsPerBeat(), TempoSwing::Unity); + size_t i = 0; + while(pos != std::string::npos && pos < eol && i < swing.size()) + { + swing[i++] = ConvertStrTo<TempoSwing::value_type>(data.substr(pos, eol - pos)); + pos = data.find(',', pos + 1); + if(pos != std::string::npos) + pos++; + } + sndFile.Patterns[pattern].SetTempoSwing(swing); + } else + { + parsedLine = false; + } + if(parsedLine) + { + pos = eol; + continue; + } + } + if(data[pos] != '|') + { + // Not a valid line? + pos = eol; + continue; + } + + if(overflowPaste) + { + // Handle overflow paste. Continue pasting in next pattern if enabled. + // If Paste Flood is enabled, this won't be called due to obvious reasons. + while(curRow >= sndFile.Patterns[pattern].GetNumRows()) + { + curRow = 0; + ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder); + if(nextOrder <= curOrder || !order.IsValidPat(nextOrder)) + { + PATTERNINDEX newPat; + if(!insertNewPatterns + || curOrder >= sndFile.GetModSpecifications().ordersMax + || (newPat = modDoc.InsertPattern(sndFile.Patterns[pattern].GetNumRows())) == PATTERNINDEX_INVALID + || order.insert(curOrder + 1, 1, newPat) == 0) + { + return success; + } + orderChanged = true; + nextOrder = curOrder + 1; + } + pattern = order[nextOrder]; + if(!sndFile.Patterns.IsValidPat(pattern)) return success; + patData = sndFile.Patterns[pattern].GetpModCommand(0, 0); + curOrder = nextOrder; + prepareUndo = true; + startRow = 0; + } + } + + success = true; + col = startChan; + // Paste columns + while((pos + 11 < data.size()) && (data[pos] == '|')) + { + pos++; + // Handle pasting large pattern into smaller pattern (e.g. 128-row pattern into MOD, which only allows 64 rows) + ModCommand dummy; + ModCommand &m = curRow < sndFile.Patterns[pattern].GetNumRows() ? patData[col] : dummy; + + // Check valid paste condition. Paste will be skipped if + // - col is not a valid channelindex or + // - doing mix paste and paste destination modcommand is a PCnote or + // - doing mix paste and trying to paste PCnote on non-empty modcommand. + const bool skipPaste = + col >= sndFile.GetNumChannels() || + (doMixPaste && m.IsPcNote()) || + (doMixPaste && data[pos] == 'P' && !m.IsEmpty()); + + if(skipPaste == false) + { + // Before changing anything in this pattern, we have to create an undo point. + if(prepareUndo) + { + modDoc.GetPatternUndo().PrepareUndo(pattern, startChan, startRow, sndFile.GetNumChannels(), sndFile.Patterns[pattern].GetNumRows(), "Paste", !firstUndo); + prepareUndo = false; + firstUndo = false; + } + + // ITSyle mixpaste requires that we keep a copy of the thing we are about to paste on + // so that we can refer back to check if there was anything in e.g. the note column before we pasted. + const ModCommand origModCmd = m; + + // push channel data below paste point first. + if(mode == pmPushForward) + { + for(ROWINDEX pushRow = sndFile.Patterns[pattern].GetNumRows() - 1 - curRow; pushRow > 0; pushRow--) + { + patData[col + pushRow * sndFile.GetNumChannels()] = patData[col + (pushRow - 1) * sndFile.GetNumChannels()]; + } + m.Clear(); + } + + PatternCursor::Columns firstCol = PatternCursor::lastColumn, lastCol = PatternCursor::firstColumn; + + // Note + if(data[pos] != ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.note == NOTE_NONE) || + (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE)))) + { + firstCol = PatternCursor::noteColumn; + m.note = NOTE_NONE; + if(data[pos] == '=') + m.note = NOTE_KEYOFF; + else if(data[pos] == '^') + m.note = NOTE_NOTECUT; + else if(data[pos] == '~') + m.note = NOTE_FADE; + else if(data[pos] == 'P') + { + if(data[pos + 2] == 'S' || data[pos + 2] == 's') + m.note = NOTE_PCS; + else + m.note = NOTE_PC; + } else if (data[pos] != '.') + { + // Check note names + for(uint8 i = 0; i < 12; i++) + { + if(data[pos] == NoteNamesSharp[i][0] && data[pos + 1] == NoteNamesSharp[i][1]) + { + m.note = ModCommand::NOTE(i + NOTE_MIN); + break; + } + } + if(m.note != NOTE_NONE) + { + // Check octave + m.note += (data[pos + 2] - '0') * 12; + if(!m.IsNote()) + { + // Invalid octave + m.note = NOTE_NONE; + } + } + } + } + + // Instrument + if(data[pos + 3] > ' ' && (!doMixPaste || ( (!doITStyleMix && origModCmd.instr == 0) || + (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE) ) )) + { + firstCol = std::min(firstCol, PatternCursor::instrColumn); + lastCol = std::max(lastCol, PatternCursor::instrColumn); + if(data[pos + 3] >= '0' && data[pos + 3] <= ('0' + (MAX_INSTRUMENTS / 10))) + { + m.instr = (data[pos + 3] - '0') * 10 + (data[pos + 4] - '0'); + } else m.instr = 0; + } + + // Volume + if(data[pos + 5] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.volcmd == VOLCMD_NONE) || + (doITStyleMix && origModCmd.note == NOTE_NONE && origModCmd.instr == 0 && origModCmd.volcmd == VOLCMD_NONE)))) + { + firstCol = std::min(firstCol, PatternCursor::volumeColumn); + lastCol = std::max(lastCol, PatternCursor::volumeColumn); + if(data[pos + 5] != '.') + { + if(m.IsPcNote()) + { + m.SetValueVolCol(ConvertStrTo<uint16>(data.substr(pos + 5, 3))); + } else + { + m.volcmd = VOLCMD_NONE; + for(int i = VOLCMD_NONE + 1; i < MAX_VOLCMDS; i++) + { + const char cmd = sourceSpecs.GetVolEffectLetter(static_cast<VolumeCommand>(i)); + if(data[pos + 5] == cmd && cmd != '?') + { + m.volcmd = static_cast<VolumeCommand>(i); + break; + } + } + m.vol = (data[pos + 6] - '0') * 10 + (data[pos + 7] - '0'); + } + } else + { + m.volcmd = VOLCMD_NONE; + m.vol = 0; + } + } + + // Effect + if(m.IsPcNote()) + { + if(data[pos + 8] != '.' && data[pos + 8] > ' ') + { + firstCol = std::min(firstCol, PatternCursor::paramColumn); + lastCol = std::max(lastCol, PatternCursor::paramColumn); + m.SetValueEffectCol(ConvertStrTo<uint16>(data.substr(pos + 8, 3))); + } else if(!origModCmd.IsPcNote()) + { + // No value provided in clipboard + if((m.command == CMD_MIDI || m.command == CMD_SMOOTHMIDI) && m.param < 128) + m.SetValueEffectCol(static_cast<uint16>(Util::muldivr(m.param, ModCommand::maxColumnValue, 127))); + else + m.SetValueEffectCol(0); + } + } else + { + if(data[pos + 8] > ' ' && (!doMixPaste || ((!doITStyleMix && origModCmd.command == CMD_NONE) || + (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0)))) + { + firstCol = std::min(firstCol, PatternCursor::effectColumn); + lastCol = std::max(lastCol, PatternCursor::effectColumn); + m.command = CMD_NONE; + if(data[pos + 8] != '.') + { + for(int i = CMD_NONE + 1; i < MAX_EFFECTS; i++) + { + const char cmd = sourceSpecs.GetEffectLetter(static_cast<EffectCommand>(i)); + if(data[pos + 8] == cmd && cmd != '?') + { + m.command = static_cast<EffectCommand>(i); + break; + } + } + } + } + + // Effect value + if(data[pos + 9] > ' ' && (!doMixPaste || ((!doITStyleMix && (origModCmd.command == CMD_NONE || origModCmd.param == 0)) || + (doITStyleMix && origModCmd.command == CMD_NONE && origModCmd.param == 0)))) + { + firstCol = std::min(firstCol, PatternCursor::paramColumn); + lastCol = std::max(lastCol, PatternCursor::paramColumn); + m.param = 0; + if(data[pos + 9] != '.') + { + for(uint8 i = 0; i < 16; i++) + { + if(data[pos + 9] == szHexChar[i]) m.param |= (i << 4); + if(data[pos + 10] == szHexChar[i]) m.param |= i; + } + } + } + + // Speed / tempo command conversion + if (sndFile.GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) + { + switch(m.command) + { + case CMD_SPEED: + case CMD_TEMPO: + if(!clipboardHasS3MCommands) + { + if(m.param < 32) + m.command = CMD_SPEED; + else + m.command = CMD_TEMPO; + } else + { + if(m.command == CMD_SPEED && m.param >= 32) + m.param = CMD_TEMPO; + else if(m.command == CMD_TEMPO && m.param < 32) + m.param = CMD_SPEED; + } + break; + } + } else + { + switch(m.command) + { + case CMD_SPEED: + case CMD_TEMPO: + if(!clipboardHasS3MCommands) + { + if(m.param < 32) + m.command = CMD_SPEED; + else + m.command = CMD_TEMPO; + } + break; + } + } + } + + // Convert some commands, if necessary. With mix paste convert only + // if the original modcommand was empty as otherwise the unchanged parts + // of the old modcommand would falsely be interpreted being of type + // origFormat and ConvertCommand could change them. + if(pasteFormat != sndFile.GetType() && (!doMixPaste || origModCmd.IsEmpty())) + m.Convert(pasteFormat, sndFile.GetType(), sndFile); + + // Sanitize PC events + if(m.IsPcNote()) + { + m.SetValueEffectCol(std::min(m.GetValueEffectCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue))); + m.SetValueVolCol(std::min(m.GetValueVolCol(), static_cast<decltype(m.GetValueEffectCol())>(ModCommand::maxColumnValue))); + } + + // Adjust pattern selection + if(col == startChan) startPoint.SetColumn(startChan, firstCol); + if(endPoint.CompareColumn(PatternCursor(0, col, lastCol)) < 0) endPoint.SetColumn(col, lastCol); + if(curRow > endPoint.GetRow()) endPoint.SetRow(curRow); + pasteRect = PatternRect(startPoint, endPoint); + } + + pos += 11; + col++; + } + // Next row + patData += sndFile.GetNumChannels(); + curRow++; + pos = eol; + } + + return success; +} + + +// Copy one of the internal clipboards to the system clipboard. +bool PatternClipboard::SelectClipboard(clipindex_t which) +{ + instance.m_activeClipboard = which; + return ToSystemClipboard(instance.m_clipboards[instance.m_activeClipboard]); +} + + +// Switch to the next internal clipboard. +bool PatternClipboard::CycleForward() +{ + instance.m_activeClipboard++; + if(instance.m_activeClipboard >= instance.m_clipboards.size()) + instance.m_activeClipboard = 0; + + return SelectClipboard(instance.m_activeClipboard); +} + + +// Switch to the previous internal clipboard. +bool PatternClipboard::CycleBackward() +{ + if(instance.m_activeClipboard == 0) + instance.m_activeClipboard = instance.m_clipboards.size() - 1; + else + instance.m_activeClipboard--; + + return SelectClipboard(instance.m_activeClipboard); +} + + +// Set the maximum number of internal clipboards. +void PatternClipboard::SetClipboardSize(clipindex_t maxEntries) +{ + instance.m_clipboards.resize(maxEntries, {"", _T("unused")}); + LimitMax(instance.m_activeClipboard, maxEntries - 1); +} + + +// Check whether patterns can be pasted from clipboard +bool PatternClipboard::CanPaste() +{ + return !!IsClipboardFormatAvailable(CF_TEXT); +} + + + +// System-specific clipboard functions +bool PatternClipboard::ToSystemClipboard(const std::string_view &data) +{ + Clipboard clipboard(CF_TEXT, data.size() + 1); + if(auto dst = clipboard.As<char>()) + { + std::copy(data.begin(), data.end(), dst); + dst[data.size()] = '\0'; + return true; + } + return false; +} + + +// System-specific clipboard functions +bool PatternClipboard::FromSystemClipboard(std::string &data) +{ + Clipboard clipboard(CF_TEXT); + if(auto cbdata = clipboard.Get(); cbdata.data()) + { + if(cbdata.size() > 0) + data.assign(mpt::byte_cast<char *>(cbdata.data()), cbdata.size() - 1); + return !data.empty(); + } + return false; +} + + +BEGIN_MESSAGE_MAP(PatternClipboardDialog, ResizableDialog) + ON_EN_UPDATE(IDC_EDIT1, &PatternClipboardDialog::OnNumClipboardsChanged) + ON_LBN_SELCHANGE(IDC_LIST1, &PatternClipboardDialog::OnSelectClipboard) + ON_LBN_DBLCLK(IDC_LIST1, &PatternClipboardDialog::OnEditName) +END_MESSAGE_MAP() + +PatternClipboardDialog PatternClipboardDialog::instance; + +void PatternClipboardDialog::DoDataExchange(CDataExchange *pDX) +{ + DDX_Control(pDX, IDC_SPIN1, m_numClipboardsSpin); + DDX_Control(pDX, IDC_LIST1, m_clipList); +} + + +PatternClipboardDialog::PatternClipboardDialog() : m_editNameBox(*this) +{ +} + + +void PatternClipboardDialog::Show() +{ + instance.m_isLocked = true; + if(!instance.m_isCreated) + { + instance.Create(IDD_CLIPBOARD, CMainFrame::GetMainFrame()); + instance.m_numClipboardsSpin.SetRange(0, int16_max); + } + instance.SetDlgItemInt(IDC_EDIT1, mpt::saturate_cast<UINT>(PatternClipboard::GetClipboardSize()), FALSE); + instance.m_isLocked = false; + instance.m_isCreated = true; + instance.UpdateList(); + + instance.SetWindowPos(nullptr, instance.m_posX, instance.m_posY, 0, 0, SWP_SHOWWINDOW | SWP_NOOWNERZORDER | SWP_NOSIZE | SWP_NOZORDER | (instance.m_posX == -1 ? SWP_NOMOVE : 0)); +} + + +void PatternClipboardDialog::OnNumClipboardsChanged() +{ + if(m_isLocked) + { + return; + } + OnEndEdit(); + PatternClipboard::SetClipboardSize(GetDlgItemInt(IDC_EDIT1, nullptr, FALSE)); + UpdateList(); +} + + +void PatternClipboardDialog::UpdateList() +{ + if(instance.m_isLocked) + { + return; + } + instance.m_clipList.ResetContent(); + PatternClipboard::clipindex_t i = 0; + for(const auto &clip : PatternClipboard::instance.m_clipboards) + { + const int item = instance.m_clipList.AddString(clip.description); + instance.m_clipList.SetItemDataPtr(item, reinterpret_cast<void *>(i)); + if(PatternClipboard::instance.m_activeClipboard == i) + { + instance.m_clipList.SetCurSel(item); + } + i++; + } +} + + +void PatternClipboardDialog::OnSelectClipboard() +{ + if(m_isLocked) + { + return; + } + PatternClipboard::clipindex_t item = reinterpret_cast<PatternClipboard::clipindex_t>(m_clipList.GetItemDataPtr(m_clipList.GetCurSel())); + PatternClipboard::SelectClipboard(item); + OnEndEdit(); +} + + +void PatternClipboardDialog::OnOK() +{ + const CWnd *focus = GetFocus(); + if(focus == &m_editNameBox) + { + // User pressed enter in clipboard name edit box => cancel editing + OnEndEdit(); + } else if(focus == &m_clipList) + { + // User pressed enter in the clipboard name list => start editing + OnEditName(); + } else + { + ResizableDialog::OnOK(); + } +} + + +void PatternClipboardDialog::OnCancel() +{ + if(GetFocus() == &m_editNameBox) + { + // User pressed enter in clipboard name edit box => just cancel editing + m_editNameBox.DestroyWindow(); + return; + } + + OnEndEdit(false); + + m_isCreated = false; + m_isLocked = true; + + RECT rect; + GetWindowRect(&rect); + m_posX = rect.left; + m_posY = rect.top; + + DestroyWindow(); +} + + +void PatternClipboardDialog::OnEditName() +{ + OnEndEdit(); + + const int sel = m_clipList.GetCurSel(); + if(sel == LB_ERR) + { + return; + } + + CRect rect; + m_clipList.GetItemRect(sel, rect); + rect.InflateRect(0, 2, 0, 2); + + // Create the edit control + m_editNameBox.Create(WS_VISIBLE | WS_CHILD | WS_BORDER | ES_LEFT | ES_AUTOHSCROLL, rect, &m_clipList, 1); + m_editNameBox.SetFont(m_clipList.GetFont()); + m_editNameBox.SetWindowText(PatternClipboard::instance.m_clipboards[sel].description); + m_editNameBox.SetSel(0, -1, TRUE); + m_editNameBox.SetFocus(); + SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, (LONG_PTR)m_clipList.GetItemDataPtr(sel)); +} + + +void PatternClipboardDialog::OnEndEdit(bool apply) +{ + if(m_editNameBox.GetSafeHwnd() == NULL) + { + return; + } + + if(apply) + { + size_t sel = GetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA); + if(sel >= PatternClipboard::instance.m_clipboards.size()) + { + // What happened? + return; + } + + CString newName; + m_editNameBox.GetWindowText(newName); + + PatternClipboard::instance.m_clipboards[sel].description = newName; + } + + SetWindowLongPtr(m_editNameBox.m_hWnd, GWLP_USERDATA, LONG_PTR(-1)); + m_editNameBox.DestroyWindow(); + + UpdateList(); +} + + +BEGIN_MESSAGE_MAP(PatternClipboardDialog::CInlineEdit, CEdit) + ON_WM_KILLFOCUS() +END_MESSAGE_MAP() + + +PatternClipboardDialog::CInlineEdit::CInlineEdit(PatternClipboardDialog &dlg) : parent(dlg) +{ +} + + +void PatternClipboardDialog::CInlineEdit::OnKillFocus(CWnd *newWnd) +{ + parent.OnEndEdit(true); + CEdit::OnKillFocus(newWnd); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.h new file mode 100644 index 00000000..0054ca29 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternClipboard.h @@ -0,0 +1,160 @@ +/* + * PatternClipboard.h + * ------------------ + * Purpose: Implementation of the pattern clipboard mechanism + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "ResizableDialog.h" +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +struct PatternEditPos; +class PatternRect; +class CSoundFile; + +struct PatternClipboardElement +{ + std::string content; + CString description; +}; + + +class PatternClipboard +{ + friend class PatternClipboardDialog; + +public: + enum PasteModes + { + pmOverwrite, + pmMixPaste, + pmMixPasteIT, + pmPasteFlood, + pmPushForward, + }; + + // Clipboard index type + using clipindex_t = size_t; + +protected: + // The one and only pattern clipboard object + static PatternClipboard instance; + + // Active internal clipboard index + clipindex_t m_activeClipboard = 0; + // Internal clipboard collection + std::vector<PatternClipboardElement> m_clipboards; + PatternClipboardElement m_channelClipboard; + PatternClipboardElement m_patternClipboard; + +public: + // Copy a range of patterns to both the system clipboard and the internal clipboard. + static bool Copy(const CSoundFile &sndFile, ORDERINDEX first, ORDERINDEX last, bool onlyOrders); + // Copy a pattern selection to both the system clipboard and the internal clipboard. + static bool Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection); + // Copy a pattern or pattern channel to the internal pattern or channel clipboard. + static bool Copy(const CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel = CHANNELINDEX_INVALID); + // Try pasting a pattern selection from the system clipboard. + static bool Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, bool &orderChanged); + // Try pasting a pattern selection from an internal clipboard. + static bool Paste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, PatternRect &pasteRect, clipindex_t internalClipboard, bool &orderChanged); + // Paste from pattern or channel clipboard. + static bool Paste(CSoundFile &sndFile, PATTERNINDEX pattern, CHANNELINDEX channel = CHANNELINDEX_INVALID); + // Copy one of the internal clipboards to the system clipboard. + static bool SelectClipboard(clipindex_t which); + // Switch to the next internal clipboard. + static bool CycleForward(); + // Switch to the previous internal clipboard. + static bool CycleBackward(); + // Set the maximum number of internal clipboards. + static void SetClipboardSize(clipindex_t maxEntries); + // Return the current number of clipboards. + static clipindex_t GetClipboardSize() { return instance.m_clipboards.size(); }; + // Check whether patterns can be pasted from clipboard + static bool CanPaste(); + +protected: + PatternClipboard() { SetClipboardSize(1); }; + + static std::string GetFileExtension(const char *ext, bool addPadding); + + static std::string FormatClipboardHeader(const CSoundFile &sndFile); + + // Create the clipboard text for a pattern selection + static std::string CreateClipboardString(const CSoundFile &sndFile, PATTERNINDEX pattern, PatternRect selection); + + // Parse clipboard string and perform the pasting operation. + static bool HandlePaste(CSoundFile &sndFile, PatternEditPos &pastePos, PasteModes mode, const std::string &data, PatternRect &pasteRect, bool &orderChanged); + + // System-specific clipboard functions + static bool ToSystemClipboard(const PatternClipboardElement &clipboard) { return ToSystemClipboard(clipboard.content); }; + static bool ToSystemClipboard(const std::string_view &data); + static bool FromSystemClipboard(std::string &data); +}; + + +class PatternClipboardDialog : public ResizableDialog +{ +protected: + // The one and only pattern clipboard dialog object + static PatternClipboardDialog instance; + + // Edit class for clipboard name editing + class CInlineEdit : public CEdit + { + protected: + PatternClipboardDialog &parent; + + public: + CInlineEdit(PatternClipboardDialog &dlg); + + DECLARE_MESSAGE_MAP(); + + afx_msg void OnKillFocus(CWnd *newWnd); + }; + + CSpinButtonCtrl m_numClipboardsSpin; + CListBox m_clipList; + CInlineEdit m_editNameBox; + int m_posX = -1, m_posY = -1; + bool m_isLocked = true, m_isCreated = false; + +public: + static void UpdateList(); + static void Show(); + static void Toggle() + { + if(instance.m_isCreated) + instance.OnCancel(); + else + instance.Show(); + } + +protected: + PatternClipboardDialog(); + + void DoDataExchange(CDataExchange *pDX) override; + + void OnOK() override; + void OnCancel() override; + + afx_msg void OnNumClipboardsChanged(); + afx_msg void OnSelectClipboard(); + + // Edit clipboard name + afx_msg void OnEditName(); + void OnEndEdit(bool apply = true); + + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternCursor.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternCursor.h new file mode 100644 index 00000000..b7d5ce43 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternCursor.h @@ -0,0 +1,369 @@ +/* + * PatternCursor.h + * --------------- + * Purpose: Class for storing pattern cursor information. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +class PatternCursor +{ + friend class PatternRect; + +public: + + enum Columns + { + firstColumn = 0, + noteColumn = firstColumn, + instrColumn, + volumeColumn, + effectColumn, + paramColumn, + lastColumn = paramColumn, + }; + +protected: + // Pattern cursor structure (MSB to LSB): + // | 16 bits - row | 13 bits - channel | 3 bits - channel component | + // As you can see, the highest 16 bits contain a row index. + // It is followed by a channel index, which is 13 bits wide. + // The lowest 3 bits are used for addressing the components of a channel. + // They are *not* used as a bit set, but treated as one of the Columns enum entries that can be found above. + uint32 cursor; + + static_assert(MAX_BASECHANNELS <= 0x1FFF, "Check: Channel index in class PatternCursor is only 13 bits wide!"); + static_assert(MAX_PATTERN_ROWS <= 0xFFFF, "Check: Row index in class PatternCursor is only 16 bits wide!"); + +public: + + // Construct cursor from given coordinates. + PatternCursor(ROWINDEX row = 0, CHANNELINDEX channel = 0, Columns column = firstColumn) + { + Set(row, channel, column); + }; + + // Construct cursor from a given row and another cursor's horizontal position (channel + column). + PatternCursor(ROWINDEX row, const PatternCursor &other) + { + Set(row, other); + }; + + // Get the pattern row the cursor is located in. + ROWINDEX GetRow() const + { + return static_cast<ROWINDEX>(cursor >> 16); + }; + + // Get the pattern channel the cursor is located in. + CHANNELINDEX GetChannel() const + { + return static_cast<CHANNELINDEX>((cursor & 0xFFFF) >> 3); + }; + + // Get the pattern channel column the cursor is located in. + Columns GetColumnType() const + { + return static_cast<Columns>((cursor & 0x07)); + }; + + // Set the cursor to given coordinates. + void Set(ROWINDEX row, CHANNELINDEX channel, Columns column = firstColumn) + { + cursor = (row << 16) | ((channel << 3) & 0x1FFF) | (column & 0x07); + }; + + // Set the cursor to the same position as another curosr. + void Set(const PatternCursor &other) + { + *this = other; + }; + + // Set the cursor to a given row and another cursor's horizontal position (channel + column). + void Set(ROWINDEX row, const PatternCursor &other) + { + cursor = (row << 16) | (other.cursor & 0xFFFF); + }; + + // Only update the row of a cursor. + void SetRow(ROWINDEX row) + { + Set(row, *this); + }; + + // Only update the horizontal position of a cursor. + void SetColumn(CHANNELINDEX chn, Columns col) + { + Set(GetRow(), chn, col); + }; + + // Move the cursor relatively. + void Move(int rows, int channels, int columns) + { + int row = std::max(int(0), static_cast<int>(GetRow()) + rows); + int chn = static_cast<int>(GetChannel()) + channels; + int col = static_cast<int>(GetColumnType() + columns); + + // Boundary checking + if(chn < 0) + { + // Moving too far left + chn = 0; + col = firstColumn; + } + + if(col < firstColumn) + { + // Extending beyond first column + col = lastColumn; + chn--; + } else if(col > lastColumn) + { + // Extending beyond last column + col = firstColumn; + chn++; + } + + Set(static_cast<ROWINDEX>(row), static_cast<CHANNELINDEX>(chn), static_cast<Columns>(col)); + } + + + // Remove the channel column type from the cursor position. + void RemoveColType() + { + cursor &= ~0x07; + } + + // Compare the row of two cursors. + // Returns -1 if this cursor is above of the other cursor, + // 1 if it is below of the other cursor and 0 if they are at the same vertical position. + int CompareRow(const PatternCursor &other) const + { + if(GetRow() < other.GetRow()) return -1; + if(GetRow() > other.GetRow()) return 1; + return 0; + } + + // Compare the column (channel + sub column) of two cursors. + // Returns -1 if this cursor is left of the other cursor, + // 1 if it is right of the other cursor and 0 if they are at the same horizontal position. + int CompareColumn(const PatternCursor &other) const + { + if((cursor & 0xFFFF) < (other.cursor & 0xFFFF)) return -1; + if((cursor & 0xFFFF) > (other.cursor & 0xFFFF)) return 1; + return 0; + } + + + // Check whether the cursor is placed on the first channel and first column. + bool IsInFirstColumn() const + { + return (cursor & 0xFFFF) == 0; + } + + + // Ensure that the point lies within a given pattern size. + void Sanitize(ROWINDEX maxRows, CHANNELINDEX maxChans) + { + ROWINDEX row = std::min(GetRow(), static_cast<ROWINDEX>(maxRows - 1)); + CHANNELINDEX chn = GetChannel(); + Columns col = GetColumnType(); + + if(chn >= maxChans) + { + chn = maxChans - 1; + col = lastColumn; + } else if(col > lastColumn) + { + col = lastColumn; + } + Set(row, chn, col); + }; + + bool operator == (const PatternCursor &other) const + { + return cursor == other.cursor; + } + + bool operator != (const PatternCursor &other) const + { + return !(*this == other); + } + +}; + + +class PatternRect +{ +protected: + PatternCursor upperLeft, lowerRight; + +public: + + // Construct selection from two pattern cursors. + PatternRect(const PatternCursor &p1, const PatternCursor &p2) + { + if(p1.CompareColumn(p2) <= 0) + { + upperLeft.cursor = (p1.cursor & 0xFFFF); + lowerRight.cursor = (p2.cursor & 0xFFFF); + } else + { + upperLeft.cursor = (p2.cursor & 0xFFFF); + lowerRight.cursor = (p1.cursor & 0xFFFF); + } + + if(p1.CompareRow(p2) <= 0) + { + upperLeft.cursor |= (p1.GetRow() << 16); + lowerRight.cursor |= (p2.GetRow() << 16); + } else + { + upperLeft.cursor |= (p2.GetRow() << 16); + lowerRight.cursor |= (p1.GetRow() << 16); + } + }; + + // Construct empty rect. + PatternRect() + { + upperLeft.Set(0); + lowerRight.Set(0); + } + + // Return upper-left corner of selection. + PatternCursor GetUpperLeft() const + { + return upperLeft; + }; + + // Return lower-right corner of selection. + PatternCursor GetLowerRight() const + { + return lowerRight; + }; + + // Check if a given point is within the horizontal boundaries of the rect + bool ContainsHorizontal(const PatternCursor &point) const + { + return point.CompareColumn(upperLeft) >= 0 && point.CompareColumn(lowerRight) <= 0; + } + + // Check if a given point is within the vertical boundaries of the rect + bool ContainsVertical(const PatternCursor &point) const + { + return point.CompareRow(upperLeft) >= 0 && point.CompareRow(lowerRight) <= 0; + } + + // Check if a given point is within the rect + bool Contains(const PatternCursor &point) const + { + return ContainsHorizontal(point) && ContainsVertical(point); + } + + // Ensure that the selection doesn't exceed a given pattern size. + void Sanitize(ROWINDEX maxRows, CHANNELINDEX maxChans) + { + upperLeft.Sanitize(maxRows, maxChans); + lowerRight.Sanitize(maxRows, maxChans); + }; + + + // Get first row of selection + ROWINDEX GetStartRow() const + { + ASSERT(upperLeft.GetRow() <= lowerRight.GetRow()); + return upperLeft.GetRow(); + } + + // Get last row of selection + ROWINDEX GetEndRow() const + { + ASSERT(upperLeft.GetRow() <= lowerRight.GetRow()); + return lowerRight.GetRow(); + } + + // Get first channel of selection + CHANNELINDEX GetStartChannel() const + { + ASSERT(upperLeft.GetChannel() <= lowerRight.GetChannel()); + return upperLeft.GetChannel(); + } + + // Get last channel of selection + CHANNELINDEX GetEndChannel() const + { + ASSERT(upperLeft.GetChannel() <= lowerRight.GetChannel()); + return lowerRight.GetChannel(); + } + + PatternCursor::Columns GetStartColumn() const + { + ASSERT((upperLeft.cursor & 0xFFFF) <= (lowerRight.cursor & 0xFFFF)); + return upperLeft.GetColumnType(); + } + + PatternCursor::Columns GetEndColumn() const + { + ASSERT((upperLeft.cursor & 0xFFFF) <= (lowerRight.cursor & 0xFFFF)); + return lowerRight.GetColumnType(); + } + + // Create a bitset of the selected columns of a channel. If a column is selected, the corresponding bit is set. + // Example: If the first and second column of the channel are selected, the bits 00000011 would be returned. + uint8 GetSelectionBits(CHANNELINDEX chn) const + { + const CHANNELINDEX startChn = GetStartChannel(), endChn = GetEndChannel(); + uint8 bits = 0; + + if(chn >= startChn && chn <= endChn) + { + // All columns could be selected (unless this is the first or last channel). + bits = uint8_max; + + if(chn == startChn) + { + // First channel: Remove columns left of the start column type. + bits <<= GetUpperLeft().GetColumnType(); + } + if(chn == endChn) + { + // Last channel: Remove columns right of the end column type. + bits &= (2 << GetLowerRight().GetColumnType()) - 1; + } + } + return (bits & 0x1F); + } + + // Get number of rows in selection + ROWINDEX GetNumRows() const + { + ASSERT(upperLeft.GetRow() <= lowerRight.GetRow()); + return 1 + GetEndRow() - GetStartRow(); + } + + // Get number of channels in selection + CHANNELINDEX GetNumChannels() const + { + ASSERT(upperLeft.GetChannel() <= lowerRight.GetChannel()); + return 1 + GetEndChannel() - GetStartChannel(); + } + + bool operator == (const PatternRect &other) const + { + return upperLeft == other.upperLeft && lowerRight == other.lowerRight; + } + +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.cpp new file mode 100644 index 00000000..f3babd7f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.cpp @@ -0,0 +1,1695 @@ +/* + * PatternEditorDialogs.cpp + * ------------------------ + * Purpose: Code for various dialogs that are used in the pattern editor. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "View_pat.h" +#include "PatternEditorDialogs.h" +#include "TempoSwingDialog.h" +#include "../soundlib/mod_specifications.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + + +static constexpr EffectCommand ExtendedCommands[] = {CMD_OFFSET, CMD_PATTERNBREAK, CMD_POSITIONJUMP, CMD_TEMPO, CMD_FINETUNE, CMD_FINETUNE_SMOOTH}; + +// For a given pattern cell, check if it contains a command supported by the X-Param mechanism. +// If so, calculate the multipler for this cell and the value of all the other cells belonging to this param. +void getXParam(ModCommand::COMMAND command, PATTERNINDEX nPat, ROWINDEX nRow, CHANNELINDEX nChannel, const CSoundFile &sndFile, UINT &xparam, UINT &multiplier) +{ + UINT xp = 0, mult = 1; + int cmdRow = static_cast<int>(nRow); + const auto &pattern = sndFile.Patterns[nPat]; + + if(command == CMD_XPARAM) + { + // current command is a parameter extension command + cmdRow--; + + // Try to find previous command parameter to be extended + while(cmdRow >= 0) + { + const ModCommand &m = *pattern.GetpModCommand(cmdRow, nChannel); + if(mpt::contains(ExtendedCommands, m.command)) + break; + if(m.command != CMD_XPARAM) + { + cmdRow = -1; + break; + } + cmdRow--; + } + } else if(!mpt::contains(ExtendedCommands, command)) + { + // If current row do not own any satisfying command parameter to extend, set return state + cmdRow = -1; + } + + if(cmdRow >= 0) + { + // An 'extendable' command parameter has been found + const ModCommand &m = *pattern.GetpModCommand(cmdRow, nChannel); + + // Find extension resolution (8 to 24 bits) + uint32 n = 1; + while(n < 4 && cmdRow + n < pattern.GetNumRows()) + { + if(pattern.GetpModCommand(cmdRow + n, nChannel)->command != CMD_XPARAM) + break; + n++; + } + + // Parameter extension found (above 8 bits non-standard parameters) + if(n > 1) + { + // Limit offset command to 24 bits, other commands to 16 bits + n = m.command == CMD_OFFSET ? n : (n > 2 ? 2 : n); + + // Compute extended value WITHOUT current row parameter value : this parameter + // is being currently edited (this is why this function is being called) so we + // only need to compute a multiplier so that we can add its contribution while + // its value is changed by user + for(uint32 j = 0; j < n; j++) + { + const ModCommand &mx = *pattern.GetpModCommand(cmdRow + j, nChannel); + + uint32 k = 8 * (n - j - 1); + if(cmdRow + j == nRow) + mult = 1 << k; + else + xp += (mx.param << k); + } + } else if(m.command == CMD_OFFSET || m.command == CMD_FINETUNE || m.command == CMD_FINETUNE_SMOOTH) + { + // No parameter extension to perform (8 bits standard parameter), + // just care about offset command special case (16 bits, fake) + mult <<= 8; + } + + const auto modDoc = sndFile.GetpModDoc(); + if(m.command == CMD_OFFSET && m.volcmd == VOLCMD_OFFSET && modDoc != nullptr) + { + SAMPLEINDEX smp = modDoc->GetSampleIndex(m); + if(m.vol == 0 && smp != 0) + { + xp = Util::muldivr_unsigned(sndFile.GetSample(smp).nLength, pattern.GetpModCommand(nRow, nChannel)->param * mult + xp, 256u << (8u * (std::max(uint32(2), n) - 1u))); + mult = 0; + } else if(m.vol > 0 && smp != 0) + { + xp += sndFile.GetSample(smp).cues[m.vol - 1]; + } + } + } + + // Return x-parameter + multiplier = mult; + xparam = xp; +} + + +///////////////////////////////////////////////////////////////////////////////////////////// +// CPatternPropertiesDlg + +BEGIN_MESSAGE_MAP(CPatternPropertiesDlg, CDialog) + ON_COMMAND(IDC_BUTTON_HALF, &CPatternPropertiesDlg::OnHalfRowNumber) + ON_COMMAND(IDC_BUTTON_DOUBLE, &CPatternPropertiesDlg::OnDoubleRowNumber) + ON_COMMAND(IDC_CHECK1, &CPatternPropertiesDlg::OnOverrideSignature) + ON_COMMAND(IDC_BUTTON1, &CPatternPropertiesDlg::OnTempoSwing) +END_MESSAGE_MAP() + +BOOL CPatternPropertiesDlg::OnInitDialog() +{ + CComboBox *combo; + CDialog::OnInitDialog(); + combo = (CComboBox *)GetDlgItem(IDC_COMBO1); + const CSoundFile &sndFile = modDoc.GetSoundFile(); + + if(m_nPattern < sndFile.Patterns.Size() && combo) + { + CString s; + const CPattern &pattern = sndFile.Patterns[m_nPattern]; + ROWINDEX nrows = pattern.GetNumRows(); + + const CModSpecifications &specs = sndFile.GetModSpecifications(); + combo->SetRedraw(FALSE); + for(UINT irow = specs.patternRowsMin; irow <= specs.patternRowsMax; irow++) + { + combo->AddString(mpt::cfmt::dec(irow)); + } + combo->SetCurSel(nrows - specs.patternRowsMin); + combo->SetRedraw(TRUE); + + CheckRadioButton(IDC_RADIO1, IDC_RADIO2, IDC_RADIO2); + + s = MPT_CFORMAT("Pattern #{}: {} row{} ({}K)")( + m_nPattern, + pattern.GetNumRows(), + (pattern.GetNumRows() == 1) ? CString(_T("")) : CString(_T("s")), + static_cast<int>((pattern.GetNumRows() * sndFile.GetNumChannels() * sizeof(ModCommand)) / 1024)); + SetDlgItemText(IDC_TEXT1, s); + + // Window title + const CString patternName = mpt::ToCString(sndFile.GetCharsetInternal(), pattern.GetName()); + s = MPT_CFORMAT("Pattern Properties for Pattern #{}")(m_nPattern); + if(!patternName.IsEmpty()) + { + s += _T(" ("); + s += patternName; + s += _T(")"); + } + SetWindowText(s); + + // Pattern time signature + const bool bOverride = pattern.GetOverrideSignature(); + ROWINDEX nRPB = pattern.GetRowsPerBeat(), nRPM = pattern.GetRowsPerMeasure(); + if(nRPB == 0 || !bOverride) + nRPB = sndFile.m_nDefaultRowsPerBeat; + if(nRPM == 0 || !bOverride) + nRPM = sndFile.m_nDefaultRowsPerMeasure; + + m_tempoSwing = pattern.HasTempoSwing() ? pattern.GetTempoSwing() : sndFile.m_tempoSwing; + + GetDlgItem(IDC_CHECK1)->EnableWindow(sndFile.GetModSpecifications().hasPatternSignatures ? TRUE : FALSE); + CheckDlgButton(IDC_CHECK1, bOverride ? BST_CHECKED : BST_UNCHECKED); + SetDlgItemInt(IDC_ROWSPERBEAT, nRPB, FALSE); + SetDlgItemInt(IDC_ROWSPERMEASURE, nRPM, FALSE); + OnOverrideSignature(); + } + return TRUE; +} + + +void CPatternPropertiesDlg::OnHalfRowNumber() +{ + const CSoundFile &sndFile = modDoc.GetSoundFile(); + + UINT nRows = GetDlgItemInt(IDC_COMBO1, NULL, FALSE); + nRows /= 2; + if(nRows < sndFile.GetModSpecifications().patternRowsMin) + nRows = sndFile.GetModSpecifications().patternRowsMin; + SetDlgItemInt(IDC_COMBO1, nRows, FALSE); +} + + +void CPatternPropertiesDlg::OnDoubleRowNumber() +{ + const CSoundFile &sndFile = modDoc.GetSoundFile(); + + UINT nRows = GetDlgItemInt(IDC_COMBO1, NULL, FALSE); + nRows *= 2; + if(nRows > sndFile.GetModSpecifications().patternRowsMax) + nRows = sndFile.GetModSpecifications().patternRowsMax; + SetDlgItemInt(IDC_COMBO1, nRows, FALSE); +} + + +void CPatternPropertiesDlg::OnOverrideSignature() +{ + GetDlgItem(IDC_ROWSPERBEAT)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1)); + GetDlgItem(IDC_ROWSPERMEASURE)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1)); + GetDlgItem(IDC_BUTTON1)->EnableWindow(IsDlgButtonChecked(IDC_CHECK1) && modDoc.GetSoundFile().m_nTempoMode == TempoMode::Modern); +} + + +void CPatternPropertiesDlg::OnTempoSwing() +{ + CPattern &pat = modDoc.GetSoundFile().Patterns[m_nPattern]; + const ROWINDEX oldRPB = pat.GetRowsPerBeat(); + const ROWINDEX oldRPM = pat.GetRowsPerMeasure(); + + // Temporarily apply new tempo signature for preview + const ROWINDEX newRPB = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT)), ROWINDEX(1), MAX_ROWS_PER_BEAT); + const ROWINDEX newRPM = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE)), newRPB, MAX_ROWS_PER_BEAT); + pat.SetSignature(newRPB, newRPM); + + m_tempoSwing.resize(newRPB, TempoSwing::Unity); + CTempoSwingDlg dlg(this, m_tempoSwing, modDoc.GetSoundFile(), m_nPattern); + if(dlg.DoModal() == IDOK) + { + m_tempoSwing = dlg.m_tempoSwing; + } + pat.SetSignature(oldRPB, oldRPM); +} + + +void CPatternPropertiesDlg::OnOK() +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + CPattern &pattern = sndFile.Patterns[m_nPattern]; + // Update pattern signature if necessary + if(sndFile.GetModSpecifications().hasPatternSignatures) + { + if(IsDlgButtonChecked(IDC_CHECK1)) + { + // Enable signature + const ROWINDEX newRPB = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT, NULL, FALSE)), MAX_ROWS_PER_BEAT); + const ROWINDEX newRPM = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE, NULL, FALSE)), MAX_ROWS_PER_BEAT); + + if(newRPB != pattern.GetRowsPerBeat() || newRPM != pattern.GetRowsPerMeasure() || m_tempoSwing != pattern.GetTempoSwing()) + { + if(!pattern.SetSignature(newRPB, newRPM)) + { + Reporting::Error("Invalid time signature!", "Pattern Properties"); + GetDlgItem(IDC_ROWSPERBEAT)->SetFocus(); + return; + } + m_tempoSwing.resize(newRPB, TempoSwing::Unity); + pattern.SetTempoSwing(m_tempoSwing); + modDoc.SetModified(); + } + } else + { + // Disable signature + if(pattern.GetOverrideSignature() || pattern.HasTempoSwing()) + { + pattern.RemoveSignature(); + pattern.RemoveTempoSwing(); + modDoc.SetModified(); + } + } + } + + + const ROWINDEX newSize = (ROWINDEX)GetDlgItemInt(IDC_COMBO1, NULL, FALSE); + + // Check if any pattern data would be removed. + bool resize = (newSize != sndFile.Patterns[m_nPattern].GetNumRows()); + bool resizeAtEnd = IsDlgButtonChecked(IDC_RADIO2) != BST_UNCHECKED; + if(newSize < sndFile.Patterns[m_nPattern].GetNumRows()) + { + ROWINDEX firstRow = resizeAtEnd ? newSize : 0; + ROWINDEX lastRow = resizeAtEnd ? sndFile.Patterns[m_nPattern].GetNumRows() : sndFile.Patterns[m_nPattern].GetNumRows() - newSize; + for(ROWINDEX row = firstRow; row < lastRow; row++) + { + if(!sndFile.Patterns[m_nPattern].IsEmptyRow(row)) + { + resize = (Reporting::Confirm(MPT_AFORMAT("Data at the {} of the pattern will be lost.\nDo you want to continue?")(resizeAtEnd ? "end" : "start"), "Shrink Pattern") == cnfYes); + break; + } + } + } + + if(resize) + { + modDoc.BeginWaitCursor(); + modDoc.GetPatternUndo().PrepareUndo(m_nPattern, 0, 0, sndFile.Patterns[m_nPattern].GetNumChannels(), sndFile.Patterns[m_nPattern].GetNumRows(), "Resize"); + if(sndFile.Patterns[m_nPattern].Resize(newSize, true, resizeAtEnd)) + { + modDoc.SetModified(); + } + modDoc.EndWaitCursor(); + } + CDialog::OnOK(); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// CEditCommand + +BEGIN_MESSAGE_MAP(CEditCommand, CDialog) + ON_WM_ACTIVATE() + ON_WM_CLOSE() + + ON_CBN_SELCHANGE(IDC_COMBO1, &CEditCommand::OnNoteChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CEditCommand::OnNoteChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CEditCommand::OnVolCmdChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &CEditCommand::OnCommandChanged) + ON_CBN_SELCHANGE(IDC_COMBO5, &CEditCommand::OnPlugParamChanged) + ON_WM_HSCROLL() +END_MESSAGE_MAP() + + +void CEditCommand::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CSplitKeyboadSettings) + DDX_Control(pDX, IDC_COMBO1, cbnNote); + DDX_Control(pDX, IDC_COMBO2, cbnInstr); + DDX_Control(pDX, IDC_COMBO3, cbnVolCmd); + DDX_Control(pDX, IDC_COMBO4, cbnCommand); + DDX_Control(pDX, IDC_COMBO5, cbnPlugParam); + DDX_Control(pDX, IDC_SLIDER1, sldVolParam); + DDX_Control(pDX, IDC_SLIDER2, sldParam); + //}}AFX_DATA_MAP +} + + +CEditCommand::CEditCommand(CSoundFile &sndFile) + : sndFile(sndFile), effectInfo(sndFile) +{ + CDialog::Create(IDD_PATTERN_EDITCOMMAND); +} + + +BOOL CEditCommand::PreTranslateMessage(MSG *pMsg) +{ + if((pMsg) && (pMsg->message == WM_KEYDOWN)) + { + if((pMsg->wParam == VK_ESCAPE) || (pMsg->wParam == VK_RETURN) || (pMsg->wParam == VK_APPS)) + { + OnClose(); + return TRUE; + } + } + return CDialog::PreTranslateMessage(pMsg); +} + + +bool CEditCommand::ShowEditWindow(PATTERNINDEX pat, const PatternCursor &cursor, CWnd *parent) +{ + editPattern = pat; + const ROWINDEX row = editRow = cursor.GetRow(); + const CHANNELINDEX chn = editChannel = cursor.GetChannel(); + + if(!sndFile.Patterns.IsValidPat(pat) + || !sndFile.Patterns[pat].IsValidRow(row) + || chn >= sndFile.GetNumChannels()) + { + ShowWindow(SW_HIDE); + return false; + } + + m = sndFile.Patterns[pat].GetpModCommand(row, chn); + modified = false; + + InitAll(); + + switch(cursor.GetColumnType()) + { + case PatternCursor::noteColumn: + cbnNote.SetFocus(); + break; + case PatternCursor::instrColumn: + cbnInstr.SetFocus(); + break; + case PatternCursor::volumeColumn: + if(m->IsPcNote()) + cbnPlugParam.SetFocus(); + else + cbnVolCmd.SetFocus(); + break; + case PatternCursor::effectColumn: + if(m->IsPcNote()) + sldParam.SetFocus(); + else + cbnCommand.SetFocus(); + break; + case PatternCursor::paramColumn: + sldParam.SetFocus(); + break; + } + + // Update Window Title + SetWindowText(MPT_CFORMAT("Note Properties - Row {}, Channel {}")(row, chn + 1)); + + CRect rectParent, rectWnd; + parent->GetWindowRect(&rectParent); + GetClientRect(&rectWnd); + SetWindowPos(CMainFrame::GetMainFrame(), + rectParent.left + (rectParent.Width() - rectWnd.right) / 2, + rectParent.top + (rectParent.Height() - rectWnd.bottom) / 2, + -1, -1, SWP_NOSIZE | SWP_NOACTIVATE); + ShowWindow(SW_RESTORE); + return true; +} + + +void CEditCommand::InitNote() +{ + // Note + cbnNote.SetRedraw(FALSE); + if(oldSpecs != &sndFile.GetModSpecifications()) + { + cbnNote.ResetContent(); + cbnNote.SetItemData(cbnNote.AddString(_T("No Note")), 0); + AppendNotesToControlEx(cbnNote, sndFile, m->instr); + oldSpecs = &sndFile.GetModSpecifications(); + } + + if(m->IsNote()) + { + // Normal note / no note + const ModCommand::NOTE noteStart = sndFile.GetModSpecifications().noteMin; + cbnNote.SetCurSel(m->note - (noteStart - 1)); + } else if(m->note == NOTE_NONE) + { + cbnNote.SetCurSel(0); + } else + { + // Special notes + for(int i = cbnNote.GetCount() - 1; i >= 0; --i) + { + if(cbnNote.GetItemData(i) == m->note) + { + cbnNote.SetCurSel(i); + break; + } + } + } + cbnNote.SetRedraw(TRUE); + + // Instrument + cbnInstr.SetRedraw(FALSE); + cbnInstr.ResetContent(); + + if(m->IsPcNote()) + { + // control plugin param note + cbnInstr.SetItemData(cbnInstr.AddString(_T("No Effect")), 0); + AddPluginNamesToCombobox(cbnInstr, sndFile.m_MixPlugins, false); + } else + { + // instrument / sample + cbnInstr.SetItemData(cbnInstr.AddString(_T("No Instrument")), 0); + const uint32 nmax = sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples(); + for(uint32 i = 1; i <= nmax; i++) + { + CString s = mpt::cfmt::val(i) + _T(": "); + // instrument / sample + if(sndFile.GetNumInstruments()) + { + if(sndFile.Instruments[i]) + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.Instruments[i]->name); + } else + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[i]); + cbnInstr.SetItemData(cbnInstr.AddString(s), i); + } + } + cbnInstr.SetCurSel(m->instr); + cbnInstr.SetRedraw(TRUE); +} + + +void CEditCommand::InitVolume() +{ + cbnVolCmd.SetRedraw(FALSE); + cbnVolCmd.ResetContent(); + if(sndFile.GetType() == MOD_TYPE_MOD || m->IsPcNote()) + { + cbnVolCmd.EnableWindow(FALSE); + sldVolParam.EnableWindow(FALSE); + } else + { + // Normal volume column effect + cbnVolCmd.EnableWindow(TRUE); + sldVolParam.EnableWindow(TRUE); + uint32 count = effectInfo.GetNumVolCmds(); + cbnVolCmd.SetItemData(cbnVolCmd.AddString(_T(" None")), (DWORD_PTR)-1); + cbnVolCmd.SetCurSel(0); + UINT fxndx = effectInfo.GetIndexFromVolCmd(m->volcmd); + for(uint32 i = 0; i < count; i++) + { + CString s; + if(effectInfo.GetVolCmdInfo(i, &s)) + { + int k = cbnVolCmd.AddString(s); + cbnVolCmd.SetItemData(k, i); + if(i == fxndx) + cbnVolCmd.SetCurSel(k); + } + } + UpdateVolCmdRange(); + } + cbnVolCmd.SetRedraw(TRUE); +} + + +void CEditCommand::InitEffect() +{ + if(m->IsPcNote()) + { + cbnCommand.ShowWindow(SW_HIDE); + return; + } + cbnCommand.ShowWindow(SW_SHOW); + xParam = 0; + xMultiplier = 1; + getXParam(m->command, editPattern, editRow, editChannel, sndFile, xParam, xMultiplier); + + cbnCommand.SetRedraw(FALSE); + cbnCommand.ResetContent(); + uint32 numfx = effectInfo.GetNumEffects(); + uint32 fxndx = effectInfo.GetIndexFromEffect(m->command, m->param); + cbnCommand.SetItemData(cbnCommand.AddString(_T(" None")), (DWORD_PTR)-1); + if(m->command == CMD_NONE) + cbnCommand.SetCurSel(0); + + CString s; + for(uint32 i = 0; i < numfx; i++) + { + if(effectInfo.GetEffectInfo(i, &s, true)) + { + int k = cbnCommand.AddString(s); + cbnCommand.SetItemData(k, i); + if(i == fxndx) + cbnCommand.SetCurSel(k); + } + } + UpdateEffectRange(false); + cbnCommand.SetRedraw(TRUE); + cbnCommand.Invalidate(); +} + + +void CEditCommand::InitPlugParam() +{ + if(!m->IsPcNote()) + { + cbnPlugParam.ShowWindow(SW_HIDE); + return; + } + cbnPlugParam.ShowWindow(SW_SHOW); + + cbnPlugParam.SetRedraw(FALSE); + cbnPlugParam.ResetContent(); + + if(m->instr > 0 && m->instr <= MAX_MIXPLUGINS) + { + AddPluginParameternamesToCombobox(cbnPlugParam, sndFile.m_MixPlugins[m->instr - 1]); + cbnPlugParam.SetCurSel(m->GetValueVolCol()); + } + UpdateEffectRange(false); + + cbnPlugParam.SetRedraw(TRUE); + cbnPlugParam.Invalidate(); +} + + +void CEditCommand::UpdateVolCmdRange() +{ + ModCommand::VOL rangeMin = 0, rangeMax = 0; + LONG fxndx = effectInfo.GetIndexFromVolCmd(m->volcmd); + bool ok = effectInfo.GetVolCmdInfo(fxndx, NULL, &rangeMin, &rangeMax); + if(ok && rangeMax > rangeMin) + { + sldVolParam.EnableWindow(TRUE); + sldVolParam.SetRange(rangeMin, rangeMax); + Limit(m->vol, rangeMin, rangeMax); + sldVolParam.SetPos(m->vol); + } else + { + // Why does this not update the display at all? + sldVolParam.SetRange(0, 0); + sldVolParam.SetPos(0); + sldVolParam.EnableWindow(FALSE); + } + UpdateVolCmdValue(); +} + + +void CEditCommand::UpdateEffectRange(bool set) +{ + DWORD pos; + bool enable = true; + + if(m->IsPcNote()) + { + // plugin param control note + sldParam.SetRange(0, ModCommand::maxColumnValue); + pos = m->GetValueEffectCol(); + } else + { + // process as effect + ModCommand::PARAM rangeMin = 0, rangeMax = 0; + LONG fxndx = effectInfo.GetIndexFromEffect(m->command, m->param); + enable = ((fxndx >= 0) && (effectInfo.GetEffectInfo(fxndx, NULL, false, &rangeMin, &rangeMax))); + + pos = effectInfo.MapValueToPos(fxndx, m->param); + if(pos > rangeMax) + pos = rangeMin | (pos & 0x0F); + Limit(pos, rangeMin, rangeMax); + + sldParam.SetRange(rangeMin, rangeMax); + } + + if(enable) + { + sldParam.EnableWindow(TRUE); + sldParam.SetPageSize(1); + sldParam.SetPos(pos); + } else + { + // Why does this not update the display at all? + sldParam.SetRange(0, 0); + sldParam.SetPos(0); + sldParam.EnableWindow(FALSE); + } + UpdateEffectValue(set); +} + + +void CEditCommand::OnNoteChanged() +{ + const bool wasParamControl = m->IsPcNote(); + ModCommand::NOTE newNote = m->note; + ModCommand::INSTR newInstr = m->instr; + + int n = cbnNote.GetCurSel(); + if(n >= 0) + newNote = static_cast<ModCommand::NOTE>(cbnNote.GetItemData(n)); + + n = cbnInstr.GetCurSel(); + if(n >= 0) + newInstr = static_cast<ModCommand::INSTR>(cbnInstr.GetItemData(n)); + + if(m->note != newNote || m->instr != newInstr) + { + PrepareUndo("Note Entry"); + CModDoc *modDoc = sndFile.GetpModDoc(); + m->note = newNote; + m->instr = newInstr; + + modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr); + + if(wasParamControl != m->IsPcNote()) + { + InitAll(); + } else if(!m->IsPcNote() + && m->instr <= sndFile.GetNumInstruments() + && newInstr <= sndFile.GetNumInstruments() + && sndFile.Instruments[m->instr] != nullptr + && sndFile.Instruments[newInstr] != nullptr + && sndFile.Instruments[newInstr]->pTuning != sndFile.Instruments[m->instr]->pTuning) + { + //Checking whether note names should be recreated. + InitNote(); + } else if(m->IsPcNote()) + { + // Update parameter list + InitPlugParam(); + } + } +} + + +void CEditCommand::OnVolCmdChanged() +{ + ModCommand::VOLCMD newVolCmd = m->volcmd; + ModCommand::VOL newVol = m->vol; + + int n = cbnVolCmd.GetCurSel(); + if(n >= 0) + { + newVolCmd = effectInfo.GetVolCmdFromIndex(static_cast<UINT>(cbnVolCmd.GetItemData(n))); + } + + newVol = static_cast<ModCommand::VOL>(sldVolParam.GetPos()); + + const bool volCmdChanged = m->volcmd != newVolCmd; + if(volCmdChanged || m->vol != newVol) + { + PrepareUndo("Volume Entry"); + CModDoc *modDoc = sndFile.GetpModDoc(); + m->volcmd = newVolCmd; + m->vol = newVol; + + modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr); + + if(volCmdChanged) + UpdateVolCmdRange(); + else + UpdateVolCmdValue(); + } +} + + +void CEditCommand::OnCommandChanged() +{ + ModCommand::COMMAND newCommand = m->command; + ModCommand::PARAM newParam = m->param; + + int n = cbnCommand.GetCurSel(); + if(n >= 0) + { + int ndx = static_cast<int>(cbnCommand.GetItemData(n)); + newCommand = static_cast<ModCommand::COMMAND>((ndx >= 0) ? effectInfo.GetEffectFromIndex(ndx, newParam) : CMD_NONE); + } + + if(m->command != newCommand || m->param != newParam) + { + PrepareUndo("Effect Entry"); + + m->command = newCommand; + if(newCommand != CMD_NONE) + { + m->param = newParam; + } + + xParam = 0; + xMultiplier = 1; + if(newCommand == CMD_XPARAM || mpt::contains(ExtendedCommands, newCommand)) + { + getXParam(newCommand, editPattern, editRow, editChannel, sndFile, xParam, xMultiplier); + } + + UpdateEffectRange(true); + + sndFile.GetpModDoc()->UpdateAllViews(nullptr, RowHint(editRow), nullptr); + } +} + + +void CEditCommand::OnPlugParamChanged() +{ + uint16 newPlugParam = m->GetValueVolCol(); + + int n = cbnPlugParam.GetCurSel(); + if(n >= 0) + { + newPlugParam = static_cast<uint16>(cbnPlugParam.GetItemData(n)); + } + + if(m->GetValueVolCol() != newPlugParam) + { + PrepareUndo("Effect Entry"); + m->SetValueVolCol(newPlugParam); + sndFile.GetpModDoc()->UpdateAllViews(nullptr, RowHint(editRow), nullptr); + } +} + + +void CEditCommand::UpdateVolCmdValue() +{ + CString s; + if(m->IsPcNote()) + { + // plugin param control note + uint16 plugParam = static_cast<uint16>(sldVolParam.GetPos()); + s.Format(_T("Value: %u"), plugParam); + } else + { + // process as effect + effectInfo.GetVolCmdParamInfo(*m, &s); + } + SetDlgItemText(IDC_TEXT2, s); +} + + +void CEditCommand::UpdateEffectValue(bool set) +{ + CString s; + + uint16 newPlugParam = 0; + ModCommand::PARAM newParam = 0; + + if(m->IsPcNote()) + { + // plugin param control note + newPlugParam = static_cast<uint16>(sldParam.GetPos()); + s.Format(_T("Value: %u"), newPlugParam); + } else + { + // process as effect + LONG fxndx = effectInfo.GetIndexFromEffect(m->command, m->param); + if(fxndx >= 0) + { + newParam = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(fxndx, sldParam.GetPos())); + effectInfo.GetEffectNameEx(s, *m, newParam * xMultiplier + xParam, editChannel); + } + } + SetDlgItemText(IDC_TEXT1, s); + + if(set) + { + if((!m->IsPcNote() && m->param != newParam) + || (m->IsPcNote() && m->GetValueVolCol() != newPlugParam)) + { + PrepareUndo("Effect Entry"); + CModDoc *modDoc = sndFile.GetpModDoc(); + if(m->IsPcNote()) + { + m->SetValueEffectCol(newPlugParam); + } else + { + m->param = newParam; + } + + modDoc->UpdateAllViews(nullptr, RowHint(editRow), nullptr); + } + } +} + + +void CEditCommand::PrepareUndo(const char *description) +{ + CModDoc *modDoc = sndFile.GetpModDoc(); + if(!modified) + { + // Let's create just one undo step. + modDoc->GetPatternUndo().PrepareUndo(editPattern, editChannel, editRow, 1, 1, description); + modified = true; + } + modDoc->SetModified(); +} + + +void CEditCommand::OnHScroll(UINT, UINT, CScrollBar *bar) +{ + if(bar == static_cast<CWnd *>(&sldVolParam)) + { + OnVolCmdChanged(); + } else if(bar == static_cast<CWnd *>(&sldParam)) + { + UpdateEffectValue(true); + } +} + + +void CEditCommand::OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized) +{ + CDialog::OnActivate(nState, pWndOther, bMinimized); + if(nState == WA_INACTIVE) + ShowWindow(SW_HIDE); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Chord Editor + +BEGIN_MESSAGE_MAP(CChordEditor, ResizableDialog) + ON_MESSAGE(WM_MOD_KBDNOTIFY, &CChordEditor::OnKeyboardNotify) + ON_CBN_SELCHANGE(IDC_COMBO1, &CChordEditor::OnChordChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CChordEditor::OnBaseNoteChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CChordEditor::OnNote1Changed) + ON_CBN_SELCHANGE(IDC_COMBO4, &CChordEditor::OnNote2Changed) + ON_CBN_SELCHANGE(IDC_COMBO5, &CChordEditor::OnNote3Changed) +END_MESSAGE_MAP() + + +void CChordEditor::DoDataExchange(CDataExchange *pDX) +{ + ResizableDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CChordEditor) + DDX_Control(pDX, IDC_KEYBOARD1, m_Keyboard); + DDX_Control(pDX, IDC_COMBO1, m_CbnShortcut); + DDX_Control(pDX, IDC_COMBO2, m_CbnBaseNote); + DDX_Control(pDX, IDC_COMBO3, m_CbnNote[0]); + DDX_Control(pDX, IDC_COMBO4, m_CbnNote[1]); + DDX_Control(pDX, IDC_COMBO5, m_CbnNote[2]); + static_assert(mpt::array_size<decltype(m_CbnNote)>::size == 3); + //}}AFX_DATA_MAP +} + + +CChordEditor::CChordEditor(CWnd *parent) + : ResizableDialog(IDD_CHORDEDIT, parent) +{ + m_chords = TrackerSettings::GetChords(); +} + +BOOL CChordEditor::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + m_Keyboard.Init(this, (CHORD_MAX - CHORD_MIN) / 12, true); + + m_CbnShortcut.SetRedraw(FALSE); + m_CbnBaseNote.SetRedraw(FALSE); + for(auto &combo : m_CbnNote) + combo.SetRedraw(FALSE); + + // Shortcut key combo box + AppendNotesToControl(m_CbnShortcut, NOTE_MIN, NOTE_MIN + static_cast<int>(kcVPEndChords) - static_cast<int>(kcVPStartChords)); + + m_CbnShortcut.SetCurSel(0); + // Base Note combo box + m_CbnBaseNote.SetItemData(m_CbnBaseNote.AddString(_T("Relative")), MPTChord::relativeMode); + AppendNotesToControl(m_CbnBaseNote, NOTE_MIN, NOTE_MIN + 3 * 12 - 1); + + // Chord Note combo boxes + CString s; + for(int note = CHORD_MIN - 1; note < CHORD_MAX; note++) + { + int noteVal = note; + if(note == CHORD_MIN - 1) + { + s = _T("--"); + noteVal = MPTChord::noNote; + } else + { + s = mpt::ToCString(CSoundFile::GetDefaultNoteName(mpt::wrapping_modulo(note, 12))); + const int octave = mpt::wrapping_divide(note, 12); + if(octave > 0) + s.AppendFormat(_T(" (+%d)"), octave); + else if(octave < 0) + s.AppendFormat(_T(" (%d)"), octave); + } + for(auto &combo : m_CbnNote) + combo.SetItemData(combo.AddString(s), noteVal); + } + + m_CbnShortcut.SetRedraw(TRUE); + m_CbnBaseNote.SetRedraw(TRUE); + for(auto &combo : m_CbnNote) + combo.SetRedraw(TRUE); + + // Update Dialog + OnChordChanged(); + return TRUE; +} + + +void CChordEditor::OnOK() +{ + TrackerSettings::GetChords() = m_chords; + ResizableDialog::OnOK(); +} + + +MPTChord &CChordEditor::GetChord() +{ + int chord = m_CbnShortcut.GetCurSel(); + if(chord >= 0) + chord = static_cast<int>(m_CbnShortcut.GetItemData(chord)) - NOTE_MIN; + if(chord < 0 || chord >= static_cast<int>(m_chords.size())) + chord = 0; + return m_chords[chord]; +} + + +LRESULT CChordEditor::OnKeyboardNotify(WPARAM cmd, LPARAM nKey) +{ + const bool outside = static_cast<int>(nKey) == -1; + if(cmd == KBDNOTIFY_LBUTTONUP && outside) + { + // Stopped dragging ouside of keyboard area + m_mouseDownKey = m_dragKey = MPTChord::noNote; + return 0; + } else if (cmd == KBDNOTIFY_MOUSEMOVE || outside) + { + return 0; + } + + MPTChord &chord = GetChord(); + const MPTChord::NoteType key = static_cast<MPTChord::NoteType>(nKey) + CHORD_MIN; + bool update = false; + + if(cmd == KBDNOTIFY_LBUTTONDOWN && m_mouseDownKey == MPTChord::noNote) + { + // Initial mouse down + m_mouseDownKey = key; + m_dragKey = MPTChord::noNote; + return 0; + } + if(cmd == KBDNOTIFY_LBUTTONDOWN && m_dragKey == MPTChord::noNote && key != m_mouseDownKey) + { + // Start dragging + m_dragKey = m_mouseDownKey; + } + + // Remove dragged note or toggle + bool noteIsSet = false; + for(auto ¬e : chord.notes) + { + if((m_dragKey != MPTChord::noNote && note == m_dragKey) + || (m_dragKey == MPTChord::noNote && note == m_mouseDownKey)) + { + note = MPTChord::noNote; + noteIsSet = update = true; + break; + } + } + + // Move or toggle note + if(cmd != KBDNOTIFY_LBUTTONUP || m_dragKey != MPTChord::noNote || !noteIsSet) + { + for(auto ¬e : chord.notes) + { + if(note == MPTChord::noNote) + { + note = key; + update = true; + break; + } + } + } + + if(cmd == KBDNOTIFY_LBUTTONUP) + m_mouseDownKey = m_dragKey = MPTChord::noNote; + else + m_dragKey = key; + + if(update) + { + std::sort(chord.notes.begin(), chord.notes.end(), [](MPTChord::NoteType left, MPTChord::NoteType right) + { + return (left == MPTChord::noNote) ? false : (left < right); + }); + OnChordChanged(); + } + return 0; +} + + +void CChordEditor::OnChordChanged() +{ + const MPTChord &chord = GetChord(); + if(chord.key != MPTChord::relativeMode) + m_CbnBaseNote.SetCurSel(chord.key + 1); + else + m_CbnBaseNote.SetCurSel(0); + for(int i = 0; i < MPTChord::notesPerChord - 1; i++) + { + int note = chord.notes[i]; + if(note == MPTChord::noNote) + note = 0; + else + note += 1 - CHORD_MIN; + m_CbnNote[i].SetCurSel(note); + } + UpdateKeyboard(); +} + + +void CChordEditor::UpdateKeyboard() +{ + MPTChord &chord = GetChord(); + const int baseNote = (chord.key == MPTChord::relativeMode) ? 0 : (chord.key % 12); + for(int i = CHORD_MIN; i < CHORD_MAX; i++) + { + uint8 b = CKeyboardControl::KEYFLAG_NORMAL; + for(const auto note : chord.notes) + { + if(i == note) + b |= CKeyboardControl::KEYFLAG_REDDOT; + } + if(i == baseNote) + b |= CKeyboardControl::KEYFLAG_BRIGHTDOT; + m_Keyboard.SetFlags(i - CHORD_MIN, b); + } + m_Keyboard.InvalidateRect(nullptr, FALSE); +} + + +void CChordEditor::OnBaseNoteChanged() +{ + MPTChord &chord = GetChord(); + int basenote = static_cast<int>(m_CbnBaseNote.GetItemData(m_CbnBaseNote.GetCurSel())); + if(basenote != MPTChord::relativeMode) + basenote -= NOTE_MIN; + chord.key = (uint8)basenote; + UpdateKeyboard(); +} + + +void CChordEditor::OnNoteChanged(int noteIndex) +{ + MPTChord &chord = GetChord(); + int note = m_CbnNote[noteIndex].GetCurSel(); + if(note < 0) + return; + chord.notes[noteIndex] = static_cast<int8>(m_CbnNote[noteIndex].GetItemData(note)); + UpdateKeyboard(); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Keyboard Split Settings (pattern editor) + +BEGIN_MESSAGE_MAP(CSplitKeyboardSettings, CDialog) + ON_CBN_SELCHANGE(IDC_COMBO_OCTAVEMODIFIER, &CSplitKeyboardSettings::OnOctaveModifierChanged) +END_MESSAGE_MAP() + + +void CSplitKeyboardSettings::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CSplitKeyboadSettings) + DDX_Control(pDX, IDC_COMBO_SPLITINSTRUMENT, m_CbnSplitInstrument); + DDX_Control(pDX, IDC_COMBO_SPLITNOTE, m_CbnSplitNote); + DDX_Control(pDX, IDC_COMBO_OCTAVEMODIFIER, m_CbnOctaveModifier); + DDX_Control(pDX, IDC_COMBO_SPLITVOLUME, m_CbnSplitVolume); + //}}AFX_DATA_MAP +} + + +BOOL CSplitKeyboardSettings::OnInitDialog() +{ + if(sndFile.GetpModDoc() == nullptr) + return TRUE; + + CDialog::OnInitDialog(); + + CString s; + + // Split Notes + AppendNotesToControl(m_CbnSplitNote, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax); + m_CbnSplitNote.SetCurSel(m_Settings.splitNote - (sndFile.GetModSpecifications().noteMin - NOTE_MIN)); + + // Octave modifier + m_CbnOctaveModifier.SetRedraw(FALSE); + m_CbnSplitVolume.InitStorage(SplitKeyboardSettings::splitOctaveRange * 2 + 1, 9); + for(int i = -SplitKeyboardSettings::splitOctaveRange; i < SplitKeyboardSettings::splitOctaveRange + 1; i++) + { + s.Format(i < 0 ? _T("Octave -%d") : i > 0 ? _T("Octave +%d") : _T("No Change"), std::abs(i)); + int n = m_CbnOctaveModifier.AddString(s); + m_CbnOctaveModifier.SetItemData(n, i); + } + m_CbnOctaveModifier.SetRedraw(TRUE); + m_CbnOctaveModifier.SetCurSel(m_Settings.octaveModifier + SplitKeyboardSettings::splitOctaveRange); + CheckDlgButton(IDC_PATTERN_OCTAVELINK, (m_Settings.octaveLink && m_Settings.octaveModifier != 0) ? BST_CHECKED : BST_UNCHECKED); + + // Volume + m_CbnSplitVolume.SetRedraw(FALSE); + m_CbnSplitVolume.InitStorage(65, 4); + m_CbnSplitVolume.AddString(_T("No Change")); + m_CbnSplitVolume.SetItemData(0, 0); + for(int i = 1; i <= 64; i++) + { + s.Format(_T("%d"), i); + int n = m_CbnSplitVolume.AddString(s); + m_CbnSplitVolume.SetItemData(n, i); + } + m_CbnSplitVolume.SetRedraw(TRUE); + m_CbnSplitVolume.SetCurSel(m_Settings.splitVolume); + + // Instruments + m_CbnSplitInstrument.SetRedraw(FALSE); + m_CbnSplitInstrument.InitStorage(1 + (sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples()), 16); + m_CbnSplitInstrument.SetItemData(m_CbnSplitInstrument.AddString(_T("No Change")), 0); + + if(sndFile.GetNumInstruments()) + { + for(INSTRUMENTINDEX nIns = 1; nIns <= sndFile.GetNumInstruments(); nIns++) + { + if(sndFile.Instruments[nIns] == nullptr) + continue; + + CString displayName = sndFile.GetpModDoc()->GetPatternViewInstrumentName(nIns); + int n = m_CbnSplitInstrument.AddString(displayName); + m_CbnSplitInstrument.SetItemData(n, nIns); + } + } else + { + for(SAMPLEINDEX nSmp = 1; nSmp <= sndFile.GetNumSamples(); nSmp++) + { + if(sndFile.GetSample(nSmp).HasSampleData()) + { + s.Format(_T("%02d: "), nSmp); + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[nSmp]); + int n = m_CbnSplitInstrument.AddString(s); + m_CbnSplitInstrument.SetItemData(n, nSmp); + } + } + } + m_CbnSplitInstrument.SetRedraw(TRUE); + m_CbnSplitInstrument.SetCurSel(m_Settings.splitInstrument); + + return TRUE; +} + + +void CSplitKeyboardSettings::OnOK() +{ + CDialog::OnOK(); + + m_Settings.splitNote = static_cast<ModCommand::NOTE>(m_CbnSplitNote.GetItemData(m_CbnSplitNote.GetCurSel()) - 1); + m_Settings.octaveModifier = m_CbnOctaveModifier.GetCurSel() - SplitKeyboardSettings::splitOctaveRange; + m_Settings.octaveLink = (IsDlgButtonChecked(IDC_PATTERN_OCTAVELINK) != BST_UNCHECKED); + m_Settings.splitVolume = static_cast<ModCommand::VOL>(m_CbnSplitVolume.GetCurSel()); + m_Settings.splitInstrument = static_cast<ModCommand::INSTR>(m_CbnSplitInstrument.GetItemData(m_CbnSplitInstrument.GetCurSel())); +} + + +void CSplitKeyboardSettings::OnCancel() +{ + CDialog::OnCancel(); +} + + +void CSplitKeyboardSettings::OnOctaveModifierChanged() +{ + CheckDlgButton(IDC_PATTERN_OCTAVELINK, (m_CbnOctaveModifier.GetCurSel() != 9) ? BST_CHECKED : BST_UNCHECKED); +} + + +///////////////////////////////////////////////////////////////////////// +// Show channel properties from pattern editor + +BEGIN_MESSAGE_MAP(QuickChannelProperties, CDialog) + ON_WM_HSCROLL() // Sliders + ON_WM_ACTIVATE() // Catch Window focus change + ON_EN_UPDATE(IDC_EDIT1, &QuickChannelProperties::OnVolChanged) + ON_EN_UPDATE(IDC_EDIT2, &QuickChannelProperties::OnPanChanged) + ON_EN_UPDATE(IDC_EDIT3, &QuickChannelProperties::OnNameChanged) + ON_COMMAND(IDC_CHECK1, &QuickChannelProperties::OnMuteChanged) + ON_COMMAND(IDC_CHECK2, &QuickChannelProperties::OnSurroundChanged) + ON_COMMAND(IDC_BUTTON1, &QuickChannelProperties::OnPrevChannel) + ON_COMMAND(IDC_BUTTON2, &QuickChannelProperties::OnNextChannel) + ON_COMMAND(IDC_BUTTON3, &QuickChannelProperties::OnChangeColor) + ON_COMMAND(IDC_BUTTON4, &QuickChannelProperties::OnChangeColor) + ON_COMMAND(IDC_BUTTON5, &QuickChannelProperties::OnPickPrevColor) + ON_COMMAND(IDC_BUTTON6, &QuickChannelProperties::OnPickNextColor) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &QuickChannelProperties::OnCustomKeyMsg) + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &QuickChannelProperties::OnToolTipText) +END_MESSAGE_MAP() + + +void QuickChannelProperties::DoDataExchange(CDataExchange *pDX) +{ + DDX_Control(pDX, IDC_SLIDER1, m_volSlider); + DDX_Control(pDX, IDC_SLIDER2, m_panSlider); + DDX_Control(pDX, IDC_SPIN1, m_volSpin); + DDX_Control(pDX, IDC_SPIN2, m_panSpin); + DDX_Control(pDX, IDC_EDIT3, m_nameEdit); +} + + +QuickChannelProperties::~QuickChannelProperties() +{ + DestroyWindow(); +} + + +void QuickChannelProperties::OnActivate(UINT nState, CWnd *, BOOL) +{ + if(nState == WA_INACTIVE && !m_settingColor) + { + // Hide window when changing focus to another window. + m_visible = false; + ShowWindow(SW_HIDE); + } +} + + +// Show channel properties for a given channel at a given screen position. +void QuickChannelProperties::Show(CModDoc *modDoc, CHANNELINDEX chn, CPoint position) +{ + if(!m_hWnd) + { + Create(IDD_CHANNELSETTINGS, nullptr); + EnableToolTips(); + m_colorBtn.SubclassDlgItem(IDC_BUTTON4, this); + m_colorBtnPrev.SubclassDlgItem(IDC_BUTTON5, this); + m_colorBtnNext.SubclassDlgItem(IDC_BUTTON6, this); + + m_volSlider.SetRange(0, 64); + m_volSlider.SetTicFreq(8); + m_volSpin.SetRange(0, 64); + + m_panSlider.SetRange(0, 64); + m_panSlider.SetTicFreq(8); + m_panSpin.SetRange(0, 256); + + m_nameEdit.SetFocus(); + } + m_document = modDoc; + m_channel = chn; + + SetParent(nullptr); + + // Center window around point where user clicked. + CRect rect, screenRect; + GetWindowRect(rect); + ::GetWindowRect(::GetDesktopWindow(), &screenRect); + rect.MoveToXY( + Clamp(static_cast<int>(position.x) - rect.Width() / 2, 0, static_cast<int>(screenRect.right) - rect.Width()), + Clamp(static_cast<int>(position.y) - rect.Height() / 2, 0, static_cast<int>(screenRect.bottom) - rect.Height())); + MoveWindow(rect); + + SetWindowText(MPT_TFORMAT("Settings for Channel {}")(chn + 1).c_str()); + + UpdateDisplay(); + + const BOOL enablePan = (m_document->GetModType() & (MOD_TYPE_XM | MOD_TYPE_MOD)) ? FALSE : TRUE; + const BOOL itOnly = (m_document->GetModType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? TRUE : FALSE; + + // Volume controls + m_volSlider.EnableWindow(itOnly); + m_volSpin.EnableWindow(itOnly); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT1), itOnly); + + // Pan controls + m_panSlider.EnableWindow(enablePan); + m_panSpin.EnableWindow(enablePan); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_EDIT2), enablePan); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_CHECK2), itOnly); + + // Channel name + m_nameEdit.EnableWindow((m_document->GetModType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) ? TRUE : FALSE); + + ShowWindow(SW_SHOW); + m_visible = true; +} + + +void QuickChannelProperties::UpdateDisplay() +{ + // Set up channel properties + m_visible = false; + const ModChannelSettings &settings = m_document->GetSoundFile().ChnSettings[m_channel]; + SetDlgItemInt(IDC_EDIT1, settings.nVolume, FALSE); + SetDlgItemInt(IDC_EDIT2, settings.nPan, FALSE); + m_volSlider.SetPos(settings.nVolume); + m_panSlider.SetPos(settings.nPan / 4u); + CheckDlgButton(IDC_CHECK1, (settings.dwFlags[CHN_MUTE]) ? TRUE : FALSE); + CheckDlgButton(IDC_CHECK2, (settings.dwFlags[CHN_SURROUND]) ? TRUE : FALSE); + + TCHAR description[16]; + wsprintf(description, _T("Channel %d:"), m_channel + 1); + SetDlgItemText(IDC_STATIC_CHANNEL_NAME, description); + m_nameEdit.LimitText(MAX_CHANNELNAME - 1); + m_nameEdit.SetWindowText(mpt::ToCString(m_document->GetSoundFile().GetCharsetInternal(), settings.szName)); + + const bool isFirst = (m_channel <= 0), isLast = (m_channel >= m_document->GetNumChannels() - 1); + + m_colorBtn.SetColor(settings.color); + m_colorBtnPrev.EnableWindow(isFirst ? FALSE : TRUE); + if(!isFirst) + m_colorBtnPrev.SetColor(m_document->GetSoundFile().ChnSettings[m_channel - 1].color); + m_colorBtnNext.EnableWindow(isLast ? FALSE : TRUE); + if(!isLast) + m_colorBtnNext.SetColor(m_document->GetSoundFile().ChnSettings[m_channel + 1].color); + + m_settingsChanged = false; + m_visible = true; + + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_BUTTON1), isFirst ? FALSE : TRUE); + ::EnableWindow(::GetDlgItem(m_hWnd, IDC_BUTTON2), isLast ? FALSE : TRUE); +} + +void QuickChannelProperties::PrepareUndo() +{ + if(!m_settingsChanged) + { + // Backup old channel settings through pattern undo. + m_settingsChanged = true; + m_document->GetPatternUndo().PrepareChannelUndo(m_channel, 1, "Channel Settings"); + } +} + + +void QuickChannelProperties::OnVolChanged() +{ + if(!m_visible) + { + return; + } + + uint16 volume = static_cast<uint16>(GetDlgItemInt(IDC_EDIT1)); + if(volume >= 0 && volume <= 64) + { + PrepareUndo(); + m_document->SetChannelGlobalVolume(m_channel, volume); + m_volSlider.SetPos(volume); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + } +} + + +void QuickChannelProperties::OnPanChanged() +{ + if(!m_visible) + { + return; + } + + uint16 panning = static_cast<uint16>(GetDlgItemInt(IDC_EDIT2)); + if(panning >= 0 && panning <= 256) + { + PrepareUndo(); + m_document->SetChannelDefaultPan(m_channel, panning); + m_panSlider.SetPos(panning / 4u); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + // Surround is forced off when changing pan, so uncheck the checkbox. + CheckDlgButton(IDC_CHECK2, BST_UNCHECKED); + } +} + + +void QuickChannelProperties::OnHScroll(UINT, UINT, CScrollBar *bar) +{ + if(!m_visible) + { + return; + } + + bool update = false; + + // Volume slider + if(bar == reinterpret_cast<CScrollBar *>(&m_volSlider)) + { + uint16 pos = static_cast<uint16>(m_volSlider.GetPos()); + PrepareUndo(); + if(m_document->SetChannelGlobalVolume(m_channel, pos)) + { + SetDlgItemInt(IDC_EDIT1, pos); + update = true; + } + } + // Pan slider + if(bar == reinterpret_cast<CScrollBar *>(&m_panSlider)) + { + uint16 pos = static_cast<uint16>(m_panSlider.GetPos()); + PrepareUndo(); + if(m_document->SetChannelDefaultPan(m_channel, pos * 4u)) + { + SetDlgItemInt(IDC_EDIT2, pos * 4u); + CheckDlgButton(IDC_CHECK2, BST_UNCHECKED); + update = true; + } + } + + if(update) + { + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + } +} + + +void QuickChannelProperties::OnMuteChanged() +{ + if(!m_visible) + { + return; + } + + m_document->MuteChannel(m_channel, IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); +} + + +void QuickChannelProperties::OnSurroundChanged() +{ + if(!m_visible) + { + return; + } + + PrepareUndo(); + m_document->SurroundChannel(m_channel, IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + UpdateDisplay(); +} + + +void QuickChannelProperties::OnNameChanged() +{ + if(!m_visible) + { + return; + } + + ModChannelSettings &settings = m_document->GetSoundFile().ChnSettings[m_channel]; + CString newNameTmp; + m_nameEdit.GetWindowText(newNameTmp); + std::string newName = mpt::ToCharset(m_document->GetSoundFile().GetCharsetInternal(), newNameTmp); + + if(newName != settings.szName) + { + PrepareUndo(); + settings.szName = newName; + m_document->SetModified(); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + } +} + + +void QuickChannelProperties::OnChangeColor() +{ + m_settingColor = true; + if(auto color = m_colorBtn.PickColor(m_document->GetSoundFile(), m_channel); color.has_value()) + { + PrepareUndo(); + m_document->GetSoundFile().ChnSettings[m_channel].color = *color; + if(m_document->SupportsChannelColors()) + m_document->SetModified(); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + } + m_settingColor = false; +} + + +void QuickChannelProperties::OnPickPrevColor() +{ + if(m_channel > 0) + PickColorFromChannel(m_channel - 1); +} + + +void QuickChannelProperties::OnPickNextColor() +{ + if(m_channel < m_document->GetNumChannels() - 1) + PickColorFromChannel(m_channel + 1); +} + + +void QuickChannelProperties::PickColorFromChannel(CHANNELINDEX channel) +{ + auto &channels = m_document->GetSoundFile().ChnSettings; + if(channels[channel].color != channels[m_channel].color) + { + PrepareUndo(); + channels[m_channel].color = channels[channel].color; + m_colorBtn.SetColor(channels[m_channel].color); + if(m_document->SupportsChannelColors()) + m_document->SetModified(); + m_document->UpdateAllViews(nullptr, GeneralHint(m_channel).Channels(), this); + } +} + + +void QuickChannelProperties::OnPrevChannel() +{ + if(m_channel > 0) + { + m_channel--; + UpdateDisplay(); + } +} + + +void QuickChannelProperties::OnNextChannel() +{ + if(m_channel < m_document->GetNumChannels() - 1) + { + m_channel++; + UpdateDisplay(); + } +} + + +BOOL QuickChannelProperties::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + + if(ih->KeyEvent(kCtxChannelSettings, nChar, nRepCnt, nFlags, kT, this) != kcNull) + { + return TRUE; // Mapped to a command, no need to pass message on. + } + } + } + + return CDialog::PreTranslateMessage(pMsg); +} + + +LRESULT QuickChannelProperties::OnCustomKeyMsg(WPARAM wParam, LPARAM) +{ + switch(wParam) + { + case kcChnSettingsPrev: + OnPrevChannel(); + return wParam; + case kcChnSettingsNext: + OnNextChannel(); + return wParam; + case kcChnColorFromPrev: + OnPickPrevColor(); + return wParam; + case kcChnColorFromNext: + OnPickNextColor(); + return wParam; + case kcChnSettingsClose: + OnActivate(WA_INACTIVE, nullptr, FALSE); + return wParam; + } + + return kcNull; +} + + +BOOL QuickChannelProperties::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult) +{ + auto pTTT = reinterpret_cast<TOOLTIPTEXT *>(pNMHDR); + UINT_PTR id = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + id = static_cast<UINT_PTR>(::GetDlgCtrlID(reinterpret_cast<HWND>(id))); + } + + mpt::tstring text; + CommandID cmd = kcNull; + switch (id) + { + case IDC_EDIT1: + case IDC_SLIDER1: + text = CModDoc::LinearToDecibels(m_document->GetSoundFile().ChnSettings[m_channel].nVolume, 64.0); + break; + case IDC_EDIT2: + case IDC_SLIDER2: + text = CModDoc::PanningToString(m_document->GetSoundFile().ChnSettings[m_channel].nPan, 128); + break; + case IDC_BUTTON1: + text = _T("Previous Channel"); + cmd = kcChnSettingsPrev; + break; + case IDC_BUTTON2: + text = _T("Next Channel"); + cmd = kcChnSettingsNext; + break; + case IDC_BUTTON5: + text = _T("Take color from previous channel"); + cmd = kcChnColorFromPrev; + break; + case IDC_BUTTON6: + text = _T("Take color from next channel"); + cmd = kcChnColorFromNext; + break; + default: + return FALSE; + } + + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if(!keyText.IsEmpty()) + text += MPT_TFORMAT(" ({})")(keyText); + } + + mpt::String::WriteWinBuf(pTTT->szText) = text; + *pResult = 0; + + // bring the tooltip window above other popup windows + ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOOWNERZORDER); + + return TRUE; // message was handled +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.h new file mode 100644 index 00000000..49022171 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternEditorDialogs.h @@ -0,0 +1,222 @@ +/* + * PatternEditorDialogs.h + * ---------------------- + * Purpose: Code for various dialogs that are used in the pattern editor. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "dlg_misc.h" // for keyboard control +#include "EffectInfo.h" +#include "PatternCursor.h" +#include "TrackerSettings.h" +#include "ResizableDialog.h" +#include "ColorPickerButton.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +struct SplitKeyboardSettings; + +class CPatternPropertiesDlg: public CDialog +{ +protected: + CModDoc &modDoc; + TempoSwing m_tempoSwing; + PATTERNINDEX m_nPattern; + +public: + CPatternPropertiesDlg(CModDoc &modParent, PATTERNINDEX nPat, CWnd *parent=NULL) + : CDialog(IDD_PATTERN_PROPERTIES, parent) + , modDoc(modParent) + , m_nPattern(nPat) + { } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + afx_msg void OnHalfRowNumber(); + afx_msg void OnDoubleRowNumber(); + afx_msg void OnOverrideSignature(); + afx_msg void OnTempoSwing(); + DECLARE_MESSAGE_MAP() +}; + + +////////////////////////////////////////////////////////////////////////// +// Command Editing + + +class CEditCommand: public CDialog +{ +protected: + CComboBox cbnNote, cbnInstr, cbnVolCmd, cbnCommand, cbnPlugParam; + CSliderCtrl sldVolParam, sldParam; + CSoundFile &sndFile; + const CModSpecifications *oldSpecs = nullptr; + ModCommand *m = nullptr; + EffectInfo effectInfo; + PATTERNINDEX editPattern = PATTERNINDEX_INVALID; + CHANNELINDEX editChannel = CHANNELINDEX_INVALID; + ROWINDEX editRow = ROWINDEX_INVALID; + UINT xParam, xMultiplier; + bool modified = false; + +public: + CEditCommand(CSoundFile &sndFile); + +public: + bool ShowEditWindow(PATTERNINDEX pat, const PatternCursor &cursor, CWnd *parent); + +protected: + void InitAll() { InitNote(); InitVolume(); InitEffect(); InitPlugParam(); } + void InitNote(); + void InitVolume(); + void InitEffect(); + void InitPlugParam(); + + void UpdateVolCmdRange(); + void UpdateVolCmdValue(); + void UpdateEffectRange(bool set); + void UpdateEffectValue(bool set); + + void PrepareUndo(const char *description); + + //{{AFX_VIRTUAL(CEditCommand) + void DoDataExchange(CDataExchange *pDX) override; + void OnOK() override { ShowWindow(SW_HIDE); } + void OnCancel() override { ShowWindow(SW_HIDE); } + BOOL PreTranslateMessage(MSG *pMsg) override; + afx_msg void OnActivate(UINT nState, CWnd *pWndOther, BOOL bMinimized); + afx_msg void OnClose() { ShowWindow(SW_HIDE); } + + afx_msg void OnNoteChanged(); + afx_msg void OnVolCmdChanged(); + afx_msg void OnCommandChanged(); + afx_msg void OnPlugParamChanged(); + afx_msg void OnHScroll(UINT, UINT, CScrollBar *); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Chord Editor + +class CChordEditor : public ResizableDialog +{ +protected: + CKeyboardControl m_Keyboard; + CComboBox m_CbnShortcut, m_CbnBaseNote, m_CbnNote[MPTChord::notesPerChord - 1]; + MPTChords m_chords; + MPTChord::NoteType m_mouseDownKey = MPTChord::noNote, m_dragKey = MPTChord::noNote; + + static constexpr MPTChord::NoteType CHORD_MIN = -24; + static constexpr MPTChord::NoteType CHORD_MAX = 24; + +public: + CChordEditor(CWnd *parent = nullptr); + +protected: + MPTChord &GetChord(); + + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + + void UpdateKeyboard(); + afx_msg LRESULT OnKeyboardNotify(WPARAM, LPARAM); + afx_msg void OnChordChanged(); + afx_msg void OnBaseNoteChanged(); + afx_msg void OnNote1Changed() { OnNoteChanged(0); } + afx_msg void OnNote2Changed() { OnNoteChanged(1); } + afx_msg void OnNote3Changed() { OnNoteChanged(2); } + void OnNoteChanged(int noteIndex); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Keyboard Split Settings (pattern editor) + +class CSplitKeyboardSettings : public CDialog +{ +protected: + CComboBox m_CbnSplitInstrument, m_CbnSplitNote, m_CbnOctaveModifier, m_CbnSplitVolume; + CSoundFile &sndFile; + +public: + SplitKeyboardSettings &m_Settings; + + CSplitKeyboardSettings(CWnd *parent, CSoundFile &sf, SplitKeyboardSettings &settings) : CDialog(IDD_KEYBOARD_SPLIT, parent), sndFile(sf), m_Settings(settings) { } + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + void OnCancel() override; + + afx_msg void OnOctaveModifierChanged(); + + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Show channel properties from pattern editor + +class QuickChannelProperties : public CDialog +{ +protected: + CModDoc *m_document = nullptr; + CHANNELINDEX m_channel = 0; + bool m_visible = false; + bool m_settingsChanged = false; + bool m_settingColor = false; + + ColorPickerButton m_colorBtn, m_colorBtnPrev, m_colorBtnNext; + CSliderCtrl m_volSlider, m_panSlider; + CSpinButtonCtrl m_volSpin, m_panSpin; + CEdit m_nameEdit; + +public: + QuickChannelProperties() = default; + ~QuickChannelProperties(); + + void Show(CModDoc *modDoc, CHANNELINDEX chn, CPoint position); + void UpdateDisplay(); + CHANNELINDEX GetChannel() const { return m_channel; } + +protected: + void DoDataExchange(CDataExchange *pDX) override; + + void PrepareUndo(); + void PickColorFromChannel(CHANNELINDEX channel); + + afx_msg void OnActivate(UINT nState, CWnd *, BOOL); + afx_msg void OnVolChanged(); + afx_msg void OnPanChanged(); + afx_msg void OnHScroll(UINT, UINT, CScrollBar *); + afx_msg void OnMuteChanged(); + afx_msg void OnSurroundChanged(); + afx_msg void OnNameChanged(); + afx_msg void OnPrevChannel(); + afx_msg void OnNextChannel(); + afx_msg void OnChangeColor(); + afx_msg void OnPickPrevColor(); + afx_msg void OnPickNextColor(); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg BOOL OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult); + + BOOL PreTranslateMessage(MSG *pMsg); + + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.cpp new file mode 100644 index 00000000..a87b9a53 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.cpp @@ -0,0 +1,597 @@ +/* + * PatternFindReplace.cpp + * ---------------------- + * Purpose: Implementation of the pattern search. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "Mainfrm.h" +#include "Moddoc.h" +#include "resource.h" +#include "View_pat.h" +#include "PatternEditorDialogs.h" +#include "PatternFindReplace.h" +#include "PatternFindReplaceDlg.h" +#include "../soundlib/mod_specifications.h" + +OPENMPT_NAMESPACE_BEGIN + +FindReplace FindReplace::instance; + +FindReplace::FindReplace() + : findFlags(FullSearch), replaceFlags(ReplaceAll) + , replaceNoteAction(ReplaceValue), replaceInstrAction(ReplaceValue), replaceVolumeAction(ReplaceValue), replaceParamAction(ReplaceValue) + , replaceNote(NOTE_NONE), replaceInstr(0), replaceVolume(0), replaceParam(0) + , replaceVolCmd(VOLCMD_NONE), replaceCommand(CMD_NONE) + , findNoteMin(NOTE_NONE), findNoteMax(NOTE_NONE) + , findInstrMin(0), findInstrMax(0) + , findVolCmd(VOLCMD_NONE) + , findVolumeMin(0), findVolumeMax(0) + , findCommand(CMD_NONE) + , findParamMin(0), findParamMax(0) + , selection(PatternRect()) + , findChnMin(0), findChnMax(0) +{ } + + +void CViewPattern::OnEditFind() +{ + static bool dialogOpen = false; + CModDoc *pModDoc = GetDocument(); + if (pModDoc && !dialogOpen) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + FindReplace settings = FindReplace::instance; + ModCommand m = ModCommand::Empty(); + if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) + { + settings.findFlags.set(FindReplace::InPatSelection); + settings.findFlags.reset(FindReplace::FullSearch); + } else if(sndFile.Patterns.IsValidPat(m_nPattern)) + { + const CPattern &pat = sndFile.Patterns[m_nPattern]; + m_Cursor.Sanitize(pat.GetNumRows(), pat.GetNumChannels()); + m = *pat.GetpModCommand(m_Cursor.GetRow(), m_Cursor.GetChannel()); + } + + CFindReplaceTab pageFind(IDD_EDIT_FIND, false, sndFile, settings, m); + CFindReplaceTab pageReplace(IDD_EDIT_REPLACE, true, sndFile, settings, m); + CPropertySheet dlg(_T("Find/Replace")); + + dlg.AddPage(&pageFind); + dlg.AddPage(&pageReplace); + dialogOpen = true; + if(dlg.DoModal() == IDOK) + { + FindReplace::instance = settings; + FindReplace::instance.selection = m_Selection; + m_bContinueSearch = false; + OnEditFindNext(); + } + dialogOpen = false; + } +} + + +void CViewPattern::OnEditFindNext() +{ + CSoundFile &sndFile = *GetSoundFile(); + const CModSpecifications &specs = sndFile.GetModSpecifications(); + uint32 nFound = 0; + + if(!FindReplace::instance.findFlags[~FindReplace::FullSearch]) + { + PostMessage(WM_COMMAND, ID_EDIT_FIND); + return; + } + BeginWaitCursor(); + + EffectInfo effectInfo(sndFile); + + PATTERNINDEX patStart = m_nPattern; + PATTERNINDEX patEnd = m_nPattern + 1; + + if(FindReplace::instance.findFlags[FindReplace::FullSearch]) + { + patStart = 0; + patEnd = sndFile.Patterns.Size(); + } else if(FindReplace::instance.findFlags[FindReplace::InPatSelection]) + { + patStart = m_nPattern; + patEnd = patStart + 1; + } + + if(m_bContinueSearch) + { + patStart = m_nPattern; + } + + // Do we search for an extended effect? + bool isExtendedEffect = false; + if(FindReplace::instance.findFlags[FindReplace::Command]) + { + UINT fxndx = effectInfo.GetIndexFromEffect(FindReplace::instance.findCommand, static_cast<ModCommand::PARAM>(FindReplace::instance.findParamMin)); + isExtendedEffect = effectInfo.IsExtendedEffect(fxndx); + } + + CHANNELINDEX firstChannel = 0; + CHANNELINDEX lastChannel = sndFile.GetNumChannels() - 1; + + if(FindReplace::instance.findFlags[FindReplace::InChannels]) + { + // Limit search to given channels + firstChannel = std::min(FindReplace::instance.findChnMin, lastChannel); + lastChannel = std::min(FindReplace::instance.findChnMax, lastChannel); + } + + if(FindReplace::instance.findFlags[FindReplace::InPatSelection]) + { + // Limit search to pattern selection + firstChannel = std::min(FindReplace::instance.selection.GetStartChannel(), lastChannel); + lastChannel = std::min(FindReplace::instance.selection.GetEndChannel(), lastChannel); + } + + for(PATTERNINDEX pat = patStart; pat < patEnd; pat++) + { + if(!sndFile.Patterns.IsValidPat(pat)) + { + continue; + } + + ROWINDEX row = 0; + CHANNELINDEX chn = firstChannel; + if(m_bContinueSearch && pat == patStart && pat == m_nPattern) + { + // Continue search from cursor position + row = GetCurrentRow(); + chn = GetCurrentChannel() + 1; + if(chn > lastChannel) + { + row++; + chn = firstChannel; + } else if(chn < firstChannel) + { + chn = firstChannel; + } + } + + bool firstInPat = true; + const ROWINDEX numRows = sndFile.Patterns[pat].GetNumRows(); + std::vector<ModCommand::INSTR> lastInstr(sndFile.GetNumChannels(), 0); + + for(; row < numRows; row++) + { + ModCommand *m = sndFile.Patterns[pat].GetpModCommand(row, chn); + + for(; chn <= lastChannel; chn++, m++) + { + RowMask findWhere; + + if(FindReplace::instance.findFlags[FindReplace::InPatSelection]) + { + // Limit search to pattern selection + if((chn == FindReplace::instance.selection.GetStartChannel() || chn == FindReplace::instance.selection.GetEndChannel()) + && row >= FindReplace::instance.selection.GetStartRow() && row <= FindReplace::instance.selection.GetEndRow()) + { + // For channels that are on the left and right boundaries of the selection, we need to check + // columns are actually selected a bit more thoroughly. + for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++) + { + PatternCursor cursor(row, chn, static_cast<PatternCursor::Columns>(i)); + if(!FindReplace::instance.selection.Contains(cursor)) + { + switch(i) + { + case PatternCursor::noteColumn: findWhere.note = false; break; + case PatternCursor::instrColumn: findWhere.instrument = false; break; + case PatternCursor::volumeColumn: findWhere.volume = false; break; + case PatternCursor::effectColumn: findWhere.command = false; break; + case PatternCursor::paramColumn: findWhere.parameter = false; break; + } + } + } + } else + { + // For channels inside the selection, we have an easier job to solve. + if(!FindReplace::instance.selection.Contains(PatternCursor(row, chn))) + { + findWhere.Clear(); + } + } + } + + if(m->instr > 0) + lastInstr[chn] = m->instr; + + if((FindReplace::instance.findFlags[FindReplace::Note] && (!findWhere.note || m->note < FindReplace::instance.findNoteMin || m->note > FindReplace::instance.findNoteMax)) + || (FindReplace::instance.findFlags[FindReplace::Instr] && (!findWhere.instrument || m->instr < FindReplace::instance.findInstrMin || m->instr > FindReplace::instance.findInstrMax))) + { + continue; + } + + if(!m->IsPcNote()) + { + if((FindReplace::instance.findFlags[FindReplace::VolCmd] && (!findWhere.volume || m->volcmd != FindReplace::instance.findVolCmd)) + || (FindReplace::instance.findFlags[FindReplace::Volume] && (!findWhere.volume || m->volcmd == VOLCMD_NONE || m->vol < FindReplace::instance.findVolumeMin || m->vol > FindReplace::instance.findVolumeMax)) + || (FindReplace::instance.findFlags[FindReplace::Command] && (!findWhere.command || m->command != FindReplace::instance.findCommand)) + || (FindReplace::instance.findFlags[FindReplace::Param] && (!findWhere.parameter || m->command == CMD_NONE || m->param < FindReplace::instance.findParamMin || m->param > FindReplace::instance.findParamMax)) + || FindReplace::instance.findFlags[FindReplace::PCParam] + || FindReplace::instance.findFlags[FindReplace::PCValue]) + { + continue; + } + } else + { + if((FindReplace::instance.findFlags[FindReplace::PCParam] && (!findWhere.volume || m->GetValueVolCol() < FindReplace::instance.findParamMin || m->GetValueVolCol() > FindReplace::instance.findParamMax)) + || (FindReplace::instance.findFlags[FindReplace::PCValue] && (!(findWhere.command || findWhere.parameter) || m->GetValueEffectCol() < FindReplace::instance.findVolumeMin || m->GetValueEffectCol() > FindReplace::instance.findVolumeMax)) + || FindReplace::instance.findFlags[FindReplace::VolCmd] + || FindReplace::instance.findFlags[FindReplace::Volume] + || FindReplace::instance.findFlags[FindReplace::Command] + || FindReplace::instance.findFlags[FindReplace::Param]) + { + continue; + } + } + + if((FindReplace::instance.findFlags & (FindReplace::Command | FindReplace::Param)) == FindReplace::Command && isExtendedEffect) + { + if((m->param & 0xF0) != (FindReplace::instance.findParamMin & 0xF0)) + continue; + } + + // Found! + + // Do we want to jump to the finding in this pattern? + const bool updatePos = !FindReplace::instance.replaceFlags.test_all(FindReplace::ReplaceAll | FindReplace::Replace); + nFound++; + + if(updatePos) + { + if(IsLiveRecord()) + { + // turn off "follow song" + m_Status.reset(psFollowSong); + SendCtrlMessage(CTRLMSG_PAT_FOLLOWSONG, 0); + } + + // Find sequence and order where this pattern is used + const auto numSequences = sndFile.Order.GetNumSequences(); + auto seq = sndFile.Order.GetCurrentSequenceIndex(); + for(SEQUENCEINDEX i = 0; i < numSequences; i++) + { + const bool isCurrentSeq = (i == 0); + ORDERINDEX matchingOrder = sndFile.Order(seq).FindOrder(pat, isCurrentSeq ? GetCurrentOrder() : 0); + if(matchingOrder != ORDERINDEX_INVALID) + { + if(!isCurrentSeq) + SendCtrlMessage(CTRLMSG_PAT_SETSEQUENCE, seq); + SetCurrentOrder(matchingOrder); + break; + } + if(++seq >= numSequences) + seq = 0; + } + // go to place of finding + SetCurrentPattern(pat); + } + + PatternCursor::Columns foundCol = PatternCursor::firstColumn; + if(FindReplace::instance.findFlags[FindReplace::Note]) + foundCol = PatternCursor::noteColumn; + else if(FindReplace::instance.findFlags[FindReplace::Instr]) + foundCol = PatternCursor::instrColumn; + else if(FindReplace::instance.findFlags[FindReplace::VolCmd | FindReplace::Volume | FindReplace::PCParam]) + foundCol = PatternCursor::volumeColumn; + else if(FindReplace::instance.findFlags[FindReplace::Command | FindReplace::PCValue]) + foundCol = PatternCursor::effectColumn; + else if(FindReplace::instance.findFlags[FindReplace::Param]) + foundCol = PatternCursor::paramColumn; + + if(updatePos) + { + // Jump to pattern cell + SetCursorPosition(PatternCursor(row, chn, foundCol)); + } + + if(!FindReplace::instance.replaceFlags[FindReplace::Replace]) goto EndSearch; + + bool replace = true; + + if(!FindReplace::instance.replaceFlags[FindReplace::ReplaceAll]) + { + ConfirmAnswer result = Reporting::Confirm("Replace this occurrence?", "Replace", true); + if(result == cnfCancel) + { + goto EndSearch; // Yuck! + } else + { + replace = (result == cnfYes); + } + } + if(replace) + { + if(FindReplace::instance.replaceFlags[FindReplace::ReplaceAll]) + { + // Just create one logic undo step per pattern when auto-replacing all occurences. + if(firstInPat) + { + GetDocument()->GetPatternUndo().PrepareUndo(pat, firstChannel, row, lastChannel - firstChannel + 1, numRows - row + 1, "Find / Replace", (nFound > 1)); + firstInPat = false; + } + } else + { + // Create separately undo-able items when replacing manually. + GetDocument()->GetPatternUndo().PrepareUndo(pat, chn, row, 1, 1, "Find / Replace"); + } + + if(FindReplace::instance.replaceFlags[FindReplace::Instr]) + { + int instrReplace = FindReplace::instance.replaceInstr; + int instr = m->instr; + if(FindReplace::instance.replaceInstrAction == FindReplace::ReplaceRelative && instr > 0) + instr += instrReplace; + else if(FindReplace::instance.replaceInstrAction == FindReplace::ReplaceValue) + instr = instrReplace; + + m->instr = mpt::saturate_cast<ModCommand::INSTR>(instr); + if(m->instr > 0) + lastInstr[chn] = m->instr; + } + + if(FindReplace::instance.replaceFlags[FindReplace::Note]) + { + int noteReplace = FindReplace::instance.replaceNote; + if(FindReplace::instance.replaceNoteAction == FindReplace::ReplaceRelative && m->IsNote()) + { + if(noteReplace == FindReplace::ReplaceOctaveUp || noteReplace == FindReplace::ReplaceOctaveDown) + { + noteReplace = GetDocument()->GetInstrumentGroupSize(lastInstr[chn]) * mpt::signum(noteReplace); + } + int note = Clamp(m->note + noteReplace, specs.noteMin, specs.noteMax); + m->note = static_cast<ModCommand::NOTE>(note); + } else if(FindReplace::instance.replaceNoteAction == FindReplace::ReplaceValue) + { + // Replace with another note + // If we're going to remove a PC Note or replace a normal note by a PC note, wipe out the complete column. + if(m->IsPcNote() != ModCommand::IsPcNote(static_cast<ModCommand::NOTE>(noteReplace))) + { + m->Clear(); + } + m->note = static_cast<ModCommand::NOTE>(noteReplace); + } + } + + bool hadVolume = (m->volcmd == VOLCMD_VOLUME); + if(FindReplace::instance.replaceFlags[FindReplace::VolCmd]) + { + m->volcmd = FindReplace::instance.replaceVolCmd; + } + + if(FindReplace::instance.replaceFlags[FindReplace::Volume]) + { + int volReplace = FindReplace::instance.replaceVolume; + int vol = m->vol; + if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceRelative || FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceMultiply) + { + if(!hadVolume && m->volcmd == VOLCMD_VOLUME) + vol = GetDefaultVolume(*m, lastInstr[chn]); + + if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceRelative) + vol += volReplace; + else + vol = Util::muldivr(vol, volReplace, 100); + } else if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceValue) + { + vol = volReplace; + } + m->vol = mpt::saturate_cast<ModCommand::VOL>(vol); + } + + if(FindReplace::instance.replaceFlags[FindReplace::VolCmd | FindReplace::Volume] && m->volcmd != VOLCMD_NONE) + { + // Fix volume command parameters if necessary. This is necesary e.g. + // when there was a command "v24" and the user searched for v and replaced it by d. + // In that case, d24 wouldn't be a valid command. + ModCommand::VOL minVal = 0, maxVal = 64; + if(effectInfo.GetVolCmdInfo(effectInfo.GetIndexFromVolCmd(m->volcmd), nullptr, &minVal, &maxVal)) + { + Limit(m->vol, minVal, maxVal); + } + } + + hadVolume = (m->command == CMD_VOLUME); + if(FindReplace::instance.replaceFlags[FindReplace::Command]) + { + m->command = FindReplace::instance.replaceCommand; + } + + if(FindReplace::instance.replaceFlags[FindReplace::Param]) + { + int paramReplace = FindReplace::instance.replaceParam; + int param = m->param; + if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceRelative || FindReplace::instance.replaceParamAction == FindReplace::ReplaceMultiply) + { + if(isExtendedEffect) + param &= 0x0F; + + if(!hadVolume && m->command == CMD_VOLUME) + param = GetDefaultVolume(*m, lastInstr[chn]); + + if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceRelative) + param += paramReplace; + else + param = Util::muldivr(param, paramReplace, 100); + + if(isExtendedEffect) + param = Clamp(param, 0, 15) | (m->param & 0xF0); + } else if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceValue) + { + param = paramReplace; + } + + if(isExtendedEffect && !FindReplace::instance.replaceFlags[FindReplace::Command]) + m->param = static_cast<ModCommand::PARAM>((m->param & 0xF0) | (param & 0x0F)); + else + m->param = mpt::saturate_cast<ModCommand::PARAM>(param); + } + + if(FindReplace::instance.replaceFlags[FindReplace::PCParam]) + { + int paramReplace = FindReplace::instance.replaceParam; + int param = m->GetValueVolCol(); + if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceRelative) + param += paramReplace; + else if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceMultiply) + param = Util::muldivr(param, paramReplace, 100); + else if(FindReplace::instance.replaceParamAction == FindReplace::ReplaceValue) + param = paramReplace; + + m->SetValueVolCol(static_cast<uint16>(Clamp(param, 0, ModCommand::maxColumnValue))); + } + + if(FindReplace::instance.replaceFlags[FindReplace::PCValue]) + { + int valueReplace = FindReplace::instance.replaceVolume; + int value = m->GetValueEffectCol(); + if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceRelative) + value += valueReplace; + else if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceMultiply) + value = Util::muldivr(value, valueReplace, 100); + else if(FindReplace::instance.replaceVolumeAction == FindReplace::ReplaceValue) + value = valueReplace; + + m->SetValueEffectCol(static_cast<uint16>(Clamp(value, 0, ModCommand::maxColumnValue))); + } + + SetModified(false); + if(updatePos) + InvalidateRow(); + } + } + chn = firstChannel; + } + + } +EndSearch: + + if(FindReplace::instance.replaceFlags[FindReplace::ReplaceAll]) + { + InvalidatePattern(); + } + + if(FindReplace::instance.findFlags[FindReplace::InPatSelection] && (nFound == 0 || (FindReplace::instance.replaceFlags & (FindReplace::Replace | FindReplace::ReplaceAll)) == FindReplace::Replace)) + { + // Restore original selection if we didn't find anything or just replaced stuff manually. + m_Selection = FindReplace::instance.selection; + InvalidatePattern(); + } + + m_bContinueSearch = true; + + EndWaitCursor(); + + // Display search results + if(nFound == 0) + { + CString result; + result.Preallocate(14 + 16); + result = _T("Cannot find \""); + + // Note + if(FindReplace::instance.findFlags[FindReplace::Note]) + { + result += mpt::ToCString(sndFile.GetNoteName(FindReplace::instance.findNoteMin)); + if(FindReplace::instance.findNoteMax > FindReplace::instance.findNoteMin) + { + result.AppendChar(_T('-')); + result += mpt::ToCString(sndFile.GetNoteName(FindReplace::instance.findNoteMax)); + } + } else + { + result += _T("???"); + } + result.AppendChar(_T(' ')); + + // Instrument + if(FindReplace::instance.findFlags[FindReplace::Instr]) + { + if(FindReplace::instance.findInstrMin) + result.AppendFormat(_T("%03d"), FindReplace::instance.findInstrMin); + else + result.Append(_T(" ..")); + if(FindReplace::instance.findInstrMax > FindReplace::instance.findInstrMin) + result.AppendFormat(_T("-%03d"), FindReplace::instance.findInstrMax); + } else + { + result.Append(_T(" ??")); + } + result.AppendChar(_T(' ')); + + // Volume Command + if(FindReplace::instance.findFlags[FindReplace::VolCmd]) + { + if(FindReplace::instance.findVolCmd != VOLCMD_NONE) + result.AppendChar(specs.GetVolEffectLetter(FindReplace::instance.findVolCmd)); + else + result.AppendChar(_T('.')); + } else if(FindReplace::instance.findFlags[FindReplace::PCParam]) + { + result.AppendFormat(_T("%03d"), FindReplace::instance.findParamMin); + if(FindReplace::instance.findParamMax > FindReplace::instance.findParamMin) + result.AppendFormat(_T("-%03d"), FindReplace::instance.findParamMax); + } else + { + result.AppendChar(_T('?')); + } + + // Volume Parameter + if(FindReplace::instance.findFlags[FindReplace::Volume]) + { + result.AppendFormat(_T("%02d"), FindReplace::instance.findVolumeMin); + if(FindReplace::instance.findVolumeMax > FindReplace::instance.findVolumeMin) + result.AppendFormat(_T("-%02d"), FindReplace::instance.findVolumeMax); + } else if(!FindReplace::instance.findFlags[FindReplace::PCParam]) + { + result.AppendFormat(_T("??")); + } + result.AppendChar(_T(' ')); + + // Effect Command + if(FindReplace::instance.findFlags[FindReplace::Command]) + { + if(FindReplace::instance.findCommand != CMD_NONE) + result.AppendChar(specs.GetEffectLetter(FindReplace::instance.findCommand)); + else + result.AppendChar(_T('.')); + } else if(FindReplace::instance.findFlags[FindReplace::PCValue]) + { + result.AppendFormat(_T("%03d"), FindReplace::instance.findVolumeMin); + if(FindReplace::instance.findVolumeMax > FindReplace::instance.findVolumeMin) + result.AppendFormat(_T("-%03d"), FindReplace::instance.findVolumeMax); + } else + { + result.AppendChar(_T('?')); + } + + // Effect Parameter + if(FindReplace::instance.findFlags[FindReplace::Param]) + { + result.AppendFormat(_T("%02X"), FindReplace::instance.findParamMin); + if(FindReplace::instance.findParamMax > FindReplace::instance.findParamMin) + result.AppendFormat(_T("-%02X"), FindReplace::instance.findParamMax); + } else if(!FindReplace::instance.findFlags[FindReplace::PCValue]) + { + result.AppendFormat(_T("??")); + } + + result.AppendChar(_T('"')); + + Reporting::Information(result, _T("Find/Replace")); + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.h new file mode 100644 index 00000000..6266213d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplace.h @@ -0,0 +1,73 @@ +/* + * PatternFindReplace.h + * -------------------- + * Purpose: Implementation of the pattern search. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +// Find/Replace data +struct FindReplace +{ + static FindReplace instance; + + enum Flags + { + Note = 0x01, // Search for note + Instr = 0x02, // Search for instrument + VolCmd = 0x04, // Search for volume effect + Volume = 0x08, // Search for volume + Command = 0x10, // Search for effect + Param = 0x20, // Search for effect parameter + PCParam = 0x40, // Parameter of PC event + PCValue = 0x80, // Value of PC event + + InChannels = 0x100, // Limit search to channels + FullSearch = 0x200, // Search whole song + InPatSelection = 0x400, // Search in current pattern selection + Replace = 0x800, // Replace + ReplaceAll = 0x1000, // Replace all + }; + + enum ReplaceMode + { + ReplaceValue, + ReplaceRelative, + ReplaceMultiply, + }; + + enum + { + ReplaceOctaveUp = 12000, + ReplaceOctaveDown = -12000, + }; + + FlagSet<Flags> findFlags, replaceFlags; // See Flags + + // Data to replace with + ReplaceMode replaceNoteAction, replaceInstrAction, replaceVolumeAction, replaceParamAction; + int replaceNote, replaceInstr, replaceVolume, replaceParam; + ModCommand::VOLCMD replaceVolCmd; + ModCommand::COMMAND replaceCommand; + + // Data to find + ModCommand::NOTE findNoteMin, findNoteMax; + ModCommand::INSTR findInstrMin, findInstrMax; + ModCommand::VOLCMD findVolCmd; + int findVolumeMin, findVolumeMax; + ModCommand::COMMAND findCommand; + int findParamMin, findParamMax; + + PatternRect selection; // Find in this selection (if FindReplace::InPatSelection is set) + CHANNELINDEX findChnMin, findChnMax; // Find in these channels (if FindReplace::InChannels is set) + + FindReplace(); +}; + +DECLARE_FLAGSET(FindReplace::Flags); diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.cpp new file mode 100644 index 00000000..ff1977c8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.cpp @@ -0,0 +1,952 @@ +/* + * PatternFindReplaceDlg.cpp + * ------------------------- + * Purpose: The find/replace dialog for pattern data. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "View_pat.h" +#include "PatternFindReplace.h" +#include "PatternFindReplaceDlg.h" + + +OPENMPT_NAMESPACE_BEGIN + +// CFindRangeDlg: Find a range of values. + +class CFindRangeDlg : public CDialog +{ +public: + enum DisplayMode + { + kDecimal, + kHex, + kNotes, + }; +protected: + CComboBox m_cbnMin, m_cbnMax; + int m_minVal, m_minDefault; + int m_maxVal, m_maxDefault; + DisplayMode m_displayMode; + +public: + CFindRangeDlg(CWnd *parent, int minVal, int minDefault, int maxVal, int maxDefault, DisplayMode displayMode) : CDialog(IDD_FIND_RANGE, parent) + , m_minVal(minVal) + , m_minDefault(minDefault) + , m_maxVal(maxVal) + , m_maxDefault(maxDefault) + , m_displayMode(displayMode) + { } + + int GetMinVal() const { return m_minVal; } + int GetMaxVal() const { return m_maxVal; } + +protected: + virtual void DoDataExchange(CDataExchange* pDX) + { + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO1, m_cbnMin); + DDX_Control(pDX, IDC_COMBO2, m_cbnMax); + } + + virtual BOOL OnInitDialog() + { + CDialog::OnInitDialog(); + + if(m_displayMode == kNotes) + { + AppendNotesToControl(m_cbnMin, static_cast<ModCommand::NOTE>(m_minVal), static_cast<ModCommand::NOTE>(m_maxVal)); + AppendNotesToControl(m_cbnMax, static_cast<ModCommand::NOTE>(m_minVal), static_cast<ModCommand::NOTE>(m_maxVal)); + } else + { + m_cbnMin.InitStorage(m_minVal - m_maxVal + 1, 4); + m_cbnMax.InitStorage(m_minVal - m_maxVal + 1, 4); + const TCHAR *formatString; + if(m_displayMode == kHex && m_maxVal <= 0x0F) + formatString = _T("%01X"); + else if(m_displayMode == kHex) + formatString = _T("%02X"); + else + formatString = _T("%d"); + for(int i = m_minVal; i <= m_maxVal; i++) + { + TCHAR s[16]; + wsprintf(s, formatString, i); + m_cbnMin.SetItemData(m_cbnMin.AddString(s), i); + m_cbnMax.SetItemData(m_cbnMax.AddString(s), i); + } + } + if(m_minDefault < m_minVal || m_minDefault > m_maxDefault) + { + m_minDefault = m_minVal; + m_maxDefault = m_maxVal; + } + m_cbnMin.SetCurSel(m_minDefault - m_minVal); + m_cbnMax.SetCurSel(m_maxDefault - m_minVal); + + return TRUE; + } + + virtual void OnOK() + { + CDialog::OnOK(); + m_minVal = static_cast<int>(m_cbnMin.GetItemData(m_cbnMin.GetCurSel())); + m_maxVal = static_cast<int>(m_cbnMax.GetItemData(m_cbnMax.GetCurSel())); + if(m_maxVal < m_minVal) + std::swap(m_minVal, m_maxVal); + } +}; + + +BEGIN_MESSAGE_MAP(CFindReplaceTab, CPropertyPage) + ON_CBN_SELCHANGE(IDC_COMBO1, &CFindReplaceTab::OnNoteChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &CFindReplaceTab::OnInstrChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CFindReplaceTab::OnVolCmdChanged) + ON_CBN_SELCHANGE(IDC_COMBO4, &CFindReplaceTab::OnVolumeChanged) + ON_CBN_SELCHANGE(IDC_COMBO5, &CFindReplaceTab::OnEffectChanged) + ON_CBN_SELCHANGE(IDC_COMBO6, &CFindReplaceTab::OnParamChanged) + ON_CBN_SELCHANGE(IDC_COMBO7, &CFindReplaceTab::OnPCParamChanged) + + ON_CBN_EDITCHANGE(IDC_COMBO4, &CFindReplaceTab::OnVolumeChanged) + ON_CBN_EDITCHANGE(IDC_COMBO6, &CFindReplaceTab::OnParamChanged) + + ON_COMMAND(IDC_CHECK1, &CFindReplaceTab::OnCheckNote) + ON_COMMAND(IDC_CHECK2, &CFindReplaceTab::OnCheckInstr) + ON_COMMAND(IDC_CHECK3, &CFindReplaceTab::OnCheckVolCmd) + ON_COMMAND(IDC_CHECK4, &CFindReplaceTab::OnCheckVolume) + ON_COMMAND(IDC_CHECK5, &CFindReplaceTab::OnCheckEffect) + ON_COMMAND(IDC_CHECK6, &CFindReplaceTab::OnCheckParam) + + ON_COMMAND(IDC_CHECK7, &CFindReplaceTab::OnCheckChannelSearch) +END_MESSAGE_MAP() + + +void CFindReplaceTab::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO1, m_cbnNote); + DDX_Control(pDX, IDC_COMBO2, m_cbnInstr); + DDX_Control(pDX, IDC_COMBO3, m_cbnVolCmd); + DDX_Control(pDX, IDC_COMBO4, m_cbnVolume); + DDX_Control(pDX, IDC_COMBO5, m_cbnCommand); + DDX_Control(pDX, IDC_COMBO6, m_cbnParam); + DDX_Control(pDX, IDC_COMBO7, m_cbnPCParam); +} + + +BOOL CFindReplaceTab::OnInitDialog() +{ + CString s; + + CPropertyPage::OnInitDialog(); + // Search flags + FlagSet<FindReplace::Flags> flags = m_isReplaceTab ? m_settings.replaceFlags : m_settings.findFlags; + + COMBOBOXINFO info; + info.cbSize = sizeof(info); + if(m_cbnVolume.GetComboBoxInfo(&info)) + { + ::SetWindowLong(info.hwndItem, GWL_STYLE, ::GetWindowLong(info.hwndItem, GWL_STYLE) | ES_NUMBER); + ::SendMessage(info.hwndItem, EM_SETLIMITTEXT, 4, 0); + } + if(m_cbnParam.GetComboBoxInfo(&info)) + { + // Might need to enter hex values + //::SetWindowLong(info.hwndItem, GWL_STYLE, ::GetWindowLong(info.hwndItem, GWL_STYLE) | ES_NUMBER); + ::SendMessage(info.hwndItem, EM_SETLIMITTEXT, 4, 0); + } + + CheckDlgButton(IDC_CHECK1, flags[FindReplace::Note] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK2, flags[FindReplace::Instr] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK3, flags[FindReplace::VolCmd | FindReplace::PCParam] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK4, flags[FindReplace::Volume | FindReplace::PCValue] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK5, flags[FindReplace::Command] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK6, flags[FindReplace::Param] ? BST_CHECKED : BST_UNCHECKED); + if(m_isReplaceTab) + { + CheckDlgButton(IDC_CHECK7, flags[FindReplace::Replace] ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK8, flags[FindReplace::ReplaceAll] ? BST_CHECKED : BST_UNCHECKED); + } else + { + CheckDlgButton(IDC_CHECK7, flags[FindReplace::InChannels] ? BST_CHECKED : BST_UNCHECKED); + int nButton = IDC_RADIO1; + if(flags[FindReplace::FullSearch]) + nButton = IDC_RADIO2; + else if(flags[FindReplace::InPatSelection]) + nButton = IDC_RADIO3; + + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, nButton); + GetDlgItem(IDC_RADIO3)->EnableWindow(flags[FindReplace::InPatSelection] ? TRUE : FALSE); + SetDlgItemInt(IDC_EDIT1, m_settings.findChnMin + 1); + SetDlgItemInt(IDC_EDIT2, m_settings.findChnMax + 1); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1))->SetRange32(1, m_sndFile.GetNumChannels()); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN2))->SetRange32(1, m_sndFile.GetNumChannels()); + + // Pre-fill with selected pattern data + if(!flags[FindReplace::Note] && m_initialValues.note != NOTE_NONE) + { + m_settings.findNoteMin = m_settings.findNoteMax = m_initialValues.note; + } + if(!flags[FindReplace::Instr] && m_initialValues.instr != 0) + { + m_settings.findInstrMin = m_settings.findInstrMax = m_initialValues.instr; + } + if(IsPCEvent()) + { + if(!flags[FindReplace::PCParam] && m_initialValues.GetValueVolCol() != 0) + m_settings.findParamMin = m_settings.findParamMax = m_initialValues.GetValueVolCol(); + if(!flags[FindReplace::PCValue] && m_initialValues.GetValueEffectCol() != 0) + m_settings.findVolumeMin = m_settings.findVolumeMax = m_initialValues.GetValueEffectCol(); + } else + { + if(!flags[FindReplace::VolCmd] && m_initialValues.volcmd != VOLCMD_NONE) + m_settings.findVolCmd = m_initialValues.volcmd; + if(!flags[FindReplace::Volume] && m_initialValues.volcmd != VOLCMD_NONE) + m_settings.findVolumeMin = m_settings.findVolumeMax = m_initialValues.vol; + if(!flags[FindReplace::Command] && m_initialValues.command != CMD_NONE) + m_settings.findCommand = m_initialValues.command; + if(!flags[FindReplace::Param] && m_initialValues.command != CMD_NONE) + m_settings.findParamMin = m_settings.findParamMax = m_initialValues.param; + } + } + + // Note + { + int sel = -1; + m_cbnNote.SetRedraw(FALSE); + m_cbnNote.InitStorage(150, 6); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("...")), NOTE_NONE); + if (m_isReplaceTab) + { + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("note -1")), kReplaceNoteMinusOne); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("note +1")), kReplaceNotePlusOne); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("-1 oct")), kReplaceNoteMinusOctave); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("+1 oct")), kReplaceNotePlusOctave); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("Transpose...")), kReplaceRelative); + if(m_settings.replaceNoteAction == FindReplace::ReplaceRelative) + { + switch(m_settings.replaceNote) + { + case -1: sel = 1; break; + case 1: sel = 2; break; + case FindReplace::ReplaceOctaveDown: sel = 3; break; + case FindReplace::ReplaceOctaveUp : sel = 4; break; + default: sel = 5; break; + } + } + } else + { + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("any")), kFindAny); + m_cbnNote.SetItemData(m_cbnNote.AddString(_T("Range...")), kFindRange); + if(m_settings.findNoteMin == NOTE_MIN && m_settings.findNoteMax == NOTE_MAX) + sel = 1; + else if(m_settings.findNoteMin < m_settings.findNoteMax) + sel = 2; + } + AppendNotesToControlEx(m_cbnNote, m_sndFile); + + if(sel == -1) + { + DWORD_PTR searchNote = m_isReplaceTab ? m_settings.replaceNote : m_settings.findNoteMin; + int ncount = m_cbnNote.GetCount(); + for(int i = 0; i < ncount; i++) if(searchNote == m_cbnNote.GetItemData(i)) + { + sel = i; + break; + } + } + m_cbnNote.SetCurSel(sel); + m_cbnNote.SetRedraw(TRUE); + } + + // Volume Command + m_cbnVolCmd.SetRedraw(FALSE); + m_cbnVolCmd.InitStorage(m_effectInfo.GetNumVolCmds(), 15); + m_cbnVolCmd.SetItemData(m_cbnVolCmd.AddString(_T(" None")), (DWORD_PTR)-1); + UINT count = m_effectInfo.GetNumVolCmds(); + for (UINT n=0; n<count; n++) + { + if(m_effectInfo.GetVolCmdInfo(n, &s) && !s.IsEmpty()) + { + m_cbnVolCmd.SetItemData(m_cbnVolCmd.AddString(s), n); + } + } + m_cbnVolCmd.SetCurSel(0); + UINT fxndx = m_effectInfo.GetIndexFromVolCmd(m_isReplaceTab ? m_settings.replaceVolCmd : m_settings.findVolCmd); + for (UINT i=0; i<=count; i++) if (fxndx == m_cbnVolCmd.GetItemData(i)) + { + m_cbnVolCmd.SetCurSel(i); + break; + } + m_cbnVolCmd.SetRedraw(TRUE); + m_cbnVolCmd.ShowWindow(SW_SHOW); + m_cbnPCParam.ShowWindow(SW_HIDE); + + // Command + { + m_cbnCommand.SetRedraw(FALSE); + m_cbnCommand.InitStorage(m_effectInfo.GetNumEffects(), 20); + m_cbnCommand.SetItemData(m_cbnCommand.AddString(_T(" None")), (DWORD_PTR)-1); + count = m_effectInfo.GetNumEffects(); + for (UINT n=0; n<count; n++) + { + if(m_effectInfo.GetEffectInfo(n, &s, true) && !s.IsEmpty()) + { + m_cbnCommand.SetItemData(m_cbnCommand.AddString(s), n); + } + } + m_cbnCommand.SetCurSel(0); + fxndx = m_effectInfo.GetIndexFromEffect(m_isReplaceTab ? m_settings.replaceCommand : m_settings.findCommand, static_cast<ModCommand::PARAM>(m_isReplaceTab ? m_settings.replaceParam : m_settings.findParamMin)); + for (UINT i=0; i<=count; i++) if (fxndx == m_cbnCommand.GetItemData(i)) + { + m_cbnCommand.SetCurSel(i); + break; + } + m_cbnCommand.SetRedraw(TRUE); + } + UpdateInstrumentList(); + UpdateVolumeList(); + UpdateParamList(); + OnCheckChannelSearch(); + return TRUE; +} + + +bool CFindReplaceTab::IsPCEvent() const +{ + if(m_isReplaceTab) + { + if(ModCommand::IsPcNote(static_cast<ModCommand::NOTE>(m_settings.replaceNote))) + return true; + else if(m_settings.replaceFlags[FindReplace::Note]) + return false; + // If we don't replace the note, still show the PC-related settings if we search for PC events. + } + return ModCommand::IsPcNote(m_settings.findNoteMin); +} + + +void CFindReplaceTab::UpdateInstrumentList() +{ + const bool isPCEvent = IsPCEvent(); + if(m_cbnInstr.GetCount() != 0 && !!GetWindowLongPtr(m_cbnInstr.m_hWnd, GWLP_USERDATA) == isPCEvent) + return; + SetWindowLongPtr(m_cbnInstr.m_hWnd, GWLP_USERDATA, isPCEvent); + + int oldSelection = (m_isReplaceTab ? m_settings.replaceInstr : m_settings.findInstrMin); + int sel = (oldSelection == 0) ? 0 : -1; + m_cbnInstr.SetRedraw(FALSE); + m_cbnInstr.ResetContent(); + m_cbnInstr.InitStorage((isPCEvent ? MAX_MIXPLUGINS : MAX_INSTRUMENTS) + 3, 32); + m_cbnInstr.SetItemData(m_cbnInstr.AddString(_T("..")), 0); + if (m_isReplaceTab) + { + m_cbnInstr.SetItemData(m_cbnInstr.AddString(isPCEvent ? _T("Plugin -1") : _T("Instrument -1")), kReplaceInstrumentMinusOne); + m_cbnInstr.SetItemData(m_cbnInstr.AddString(isPCEvent ? _T("Plugin +1") : _T("Instrument +1")), kReplaceInstrumentPlusOne); + m_cbnInstr.SetItemData(m_cbnInstr.AddString(_T("Other...")), kReplaceRelative); + if(m_settings.replaceInstrAction == FindReplace::ReplaceRelative) + { + switch(m_settings.replaceInstr) + { + case -1: sel = 1; break; + case 1: sel = 2; break; + default: sel = 3; break; + } + } + } else + { + m_cbnInstr.SetItemData(m_cbnInstr.AddString(_T("Range...")), kFindRange); + if(m_settings.findInstrMin < m_settings.findInstrMax) + sel = 1; + } + if(sel == -1) + sel = m_cbnInstr.GetCount() + oldSelection - 1; + if(isPCEvent) + { + AddPluginNamesToCombobox(m_cbnInstr, m_sndFile.m_MixPlugins, false); + } else + { + CString s; + for(INSTRUMENTINDEX n = 1; n < MAX_INSTRUMENTS; n++) + { + s.Format(_T("%03d:"), n); + if(m_sndFile.GetNumInstruments()) + s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.GetInstrumentName(n)); + else + s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[n]); + m_cbnInstr.SetItemData(m_cbnInstr.AddString(s), n); + } + } + m_cbnInstr.SetCurSel(sel); + m_cbnInstr.SetRedraw(TRUE); + m_cbnInstr.Invalidate(FALSE); +} + + +void CFindReplaceTab::UpdateParamList() +{ + const bool isPCEvent = IsPCEvent(); + if(m_cbnInstr.GetCount() == 0 || !!GetWindowLongPtr(m_cbnInstr.m_hWnd, GWLP_USERDATA) != isPCEvent) + { + SetWindowLongPtr(m_cbnInstr.m_hWnd, GWLP_USERDATA, isPCEvent); + } + + int effectIndex = static_cast<int>(m_cbnCommand.GetItemData(m_cbnCommand.GetCurSel())); + ModCommand::PARAM n = 0; // unused parameter adjustment + ModCommand::COMMAND cmd = m_effectInfo.GetEffectFromIndex(effectIndex, n); + const UINT mask = m_effectInfo.GetEffectMaskFromIndex(effectIndex); + if(m_isReplaceTab) + m_settings.replaceCommand = cmd; + else + m_settings.findCommand = cmd; + + // Update Param range + const bool isExtended = m_effectInfo.IsExtendedEffect(effectIndex); + int sel = -1; + int oldcount = m_cbnParam.GetCount(); + int newcount = isExtended ? 16 : 256; + if(oldcount) + oldcount -= m_isReplaceTab ? 2 : 1; + + auto findParam = m_isReplaceTab ? m_settings.replaceParam : m_settings.findParamMin; + if(isExtended) + { + findParam &= 0x0F; + if(!m_isReplaceTab && !IsDlgButtonChecked(IDC_CHECK6)) + { + m_settings.findParamMin = (m_settings.findParamMin & 0x0F) | mask; + m_settings.findParamMax = (m_settings.findParamMax & 0x0F) | mask; + } else if(m_isReplaceTab) + { + m_settings.replaceParam |= mask; + } + } + + if(oldcount != newcount) + { + TCHAR s[16]; + int newpos; + if(oldcount && m_cbnParam.GetCurSel() != CB_ERR) + newpos = static_cast<int>(m_cbnParam.GetItemData(m_cbnParam.GetCurSel())); + else + newpos = findParam; + Limit(newpos, 0, newcount - 1); + m_cbnParam.SetRedraw(FALSE); + m_cbnParam.ResetContent(); + m_cbnParam.InitStorage(newcount + 2, 4); + + if(m_isReplaceTab) + { + wsprintf(s, _T("+ %d"), m_settings.replaceParam); + m_cbnParam.SetItemData(m_cbnParam.AddString(s), kReplaceRelative); + wsprintf(s, _T("* %d%%"), m_settings.replaceParam); + m_cbnParam.SetItemData(m_cbnParam.AddString(s), kReplaceMultiply); + if(m_settings.replaceParamAction == FindReplace::ReplaceRelative) + sel = 0; + else if(m_settings.replaceParamAction == FindReplace::ReplaceMultiply) + sel = 1; + + m_settings.replaceParam = newpos; + if(isExtended) + { + m_settings.replaceParam = (m_settings.replaceParam & 0x0F) | mask; + } + } else + { + m_cbnParam.SetItemData(m_cbnParam.AddString(_T("Range")), kFindRange); + if(m_settings.findParamMin < m_settings.findParamMax) + sel = 0; + } + + if(sel == -1) + sel = m_cbnParam.GetCount() + newpos; + for(int param = 0; param < newcount; param++) + { + wsprintf(s, (newcount == 256) ? _T("%02X") : _T("%X"), param); + int i = m_cbnParam.AddString(s); + m_cbnParam.SetItemData(i, param); + } + m_cbnParam.SetCurSel(sel); + m_cbnParam.SetRedraw(TRUE); + m_cbnParam.Invalidate(FALSE); + } +} + + +void CFindReplaceTab::UpdateVolumeList() +{ + TCHAR s[256]; + const bool isPCEvent = IsPCEvent(); + + BOOL enable = isPCEvent ? FALSE : TRUE; + GetDlgItem(IDC_CHECK5)->EnableWindow(enable); + GetDlgItem(IDC_CHECK6)->EnableWindow(enable); + m_cbnCommand.EnableWindow(enable); + m_cbnParam.EnableWindow(enable); + + // Update plugin parameter list + int plug = static_cast<int>(m_cbnInstr.GetItemData(m_cbnInstr.GetCurSel())); + if(isPCEvent && (m_cbnPCParam.GetCount() == 0 || GetWindowLongPtr(m_cbnPCParam.m_hWnd, GWLP_USERDATA) != plug)) + { + SetWindowLongPtr(m_cbnPCParam.m_hWnd, GWLP_USERDATA, plug); + + CheckDlgButton(IDC_CHECK5, BST_UNCHECKED); + CheckDlgButton(IDC_CHECK6, BST_UNCHECKED); + + int sel = m_isReplaceTab ? m_settings.replaceParam : m_settings.findParamMin; + plug--; + m_cbnPCParam.SetRedraw(FALSE); + m_cbnPCParam.ResetContent(); + if(plug >= 0 && plug < MAX_MIXPLUGINS && m_sndFile.m_MixPlugins[plug].pMixPlugin != nullptr) + { + AddPluginParameternamesToCombobox(m_cbnPCParam, *m_sndFile.m_MixPlugins[plug].pMixPlugin); + } else + { + m_cbnPCParam.InitStorage(ModCommand::maxColumnValue, 20); + for(int i = 0; i < ModCommand::maxColumnValue; i++) + { + wsprintf(s, _T("%02u: Parameter %02u"), static_cast<unsigned int>(i), static_cast<unsigned int>(i)); + m_cbnPCParam.SetItemData(m_cbnPCParam.AddString(s), i); + } + } + m_cbnPCParam.SetCurSel(sel); + m_cbnPCParam.SetRedraw(TRUE); + m_cbnPCParam.Invalidate(FALSE); + } + + m_cbnVolCmd.ShowWindow(isPCEvent ? SW_HIDE : SW_SHOW); + m_cbnPCParam.ShowWindow(isPCEvent ? SW_SHOW : SW_HIDE); + + int rangeMin, rangeMax, curVal; + + if(isPCEvent) + { + rangeMin = 0; + rangeMax = ModCommand::maxColumnValue; + curVal = (m_isReplaceTab ? m_settings.replaceVolume : m_settings.findVolumeMin); + } else + { + int effectIndex = static_cast<int>(m_cbnVolCmd.GetItemData(m_cbnVolCmd.GetCurSel())); + ModCommand::VOLCMD cmd = m_effectInfo.GetVolCmdFromIndex(effectIndex); + if(m_isReplaceTab) + m_settings.replaceVolCmd = cmd; + else + m_settings.findVolCmd = cmd; + + // Update Param range + ModCommand::VOL volMin, volMax; + if(!m_effectInfo.GetVolCmdInfo(effectIndex, nullptr, &volMin, &volMax)) + { + volMin = 0; + volMax = 64; + } + rangeMin = volMin; + rangeMax = volMax; + curVal = (m_isReplaceTab ? m_settings.replaceVolume : m_settings.findVolumeMin); + } + + int oldcount = m_cbnVolume.GetCount(); + int newcount = rangeMax - rangeMin + 1; + if (oldcount != newcount) + { + int sel = -1; + int newpos; + if (oldcount) + newpos = static_cast<int>(m_cbnVolume.GetItemData(m_cbnVolume.GetCurSel())); + else + newpos = curVal; + Limit(newpos, 0, newcount - 1); + m_cbnVolume.SetRedraw(FALSE); + m_cbnVolume.ResetContent(); + m_cbnVolume.InitStorage(newcount + 2, 4); + if(m_isReplaceTab) + { + wsprintf(s, _T("+ %d"), m_settings.replaceVolume); + m_cbnVolume.SetItemData(m_cbnVolume.AddString(s), kReplaceRelative); + wsprintf(s, _T("* %d%%"), m_settings.replaceVolume); + m_cbnVolume.SetItemData(m_cbnVolume.AddString(s), kReplaceMultiply); + if(m_settings.replaceVolumeAction == FindReplace::ReplaceRelative) + sel = 0; + else if(m_settings.replaceVolumeAction == FindReplace::ReplaceMultiply) + sel = 1; + } else + { + m_cbnVolume.SetItemData(m_cbnVolume.AddString(_T("Range...")), kFindRange); + if(m_settings.findVolumeMin < m_settings.findVolumeMax) + sel = 0; + } + + if(sel == -1) + sel = m_cbnVolume.GetCount() + newpos - rangeMin; + for (int vol = rangeMin; vol <= rangeMax; vol++) + { + wsprintf(s, (rangeMax < 10 || rangeMax > 99) ? _T("%d") : _T("%02d"), vol); + int i = m_cbnVolume.AddString(s); + m_cbnVolume.SetItemData(i, vol); + } + m_cbnVolume.SetCurSel(sel); + m_cbnVolume.SetRedraw(TRUE); + m_cbnVolume.Invalidate(FALSE); + } +} + + +void CFindReplaceTab::OnNoteChanged() +{ + CheckOnChange(IDC_CHECK1); + int item = static_cast<int>(m_cbnNote.GetItemData(m_cbnNote.GetCurSel())); + if(m_isReplaceTab) + { + m_settings.replaceNoteAction = FindReplace::ReplaceRelative; + switch(item) + { + case kReplaceNoteMinusOne: m_settings.replaceNote = -1; break; + case kReplaceNotePlusOne: m_settings.replaceNote = 1; break; + case kReplaceNoteMinusOctave: m_settings.replaceNote = FindReplace::ReplaceOctaveDown; break; + case kReplaceNotePlusOctave: m_settings.replaceNote = FindReplace::ReplaceOctaveUp; break; + + case kReplaceRelative: + { + CInputDlg dlg(this, _T("Custom Transpose Amount:"), -120, 120, m_settings.replaceNote); + if(dlg.DoModal() == IDOK) + { + m_settings.replaceNote = dlg.resultAsInt; + } else + { + // TODO undo selection + } + } + break; + + default: + m_settings.replaceNote = item; + m_settings.replaceNoteAction = FindReplace::ReplaceValue; + } + } else + { + if(item == kFindRange) + { + CFindRangeDlg dlg(this, NOTE_MIN, m_settings.findNoteMin, NOTE_MAX, m_settings.findNoteMax, CFindRangeDlg::kNotes); + if(dlg.DoModal() == IDOK) + { + m_settings.findNoteMin = static_cast<ModCommand::NOTE>(dlg.GetMinVal()); + m_settings.findNoteMax = static_cast<ModCommand::NOTE>(dlg.GetMaxVal()); + } + } else if(item == kFindAny) + { + m_settings.findNoteMin = NOTE_MIN; + m_settings.findNoteMax = NOTE_MAX; + } else + { + m_settings.findNoteMin = m_settings.findNoteMax = static_cast<ModCommand::NOTE>(item); + } + } + UpdateInstrumentList(); + UpdateVolumeList(); +} + + +void CFindReplaceTab::OnInstrChanged() +{ + CheckOnChange(IDC_CHECK2); + int item = static_cast<int>(m_cbnInstr.GetItemData(m_cbnInstr.GetCurSel())); + if(m_isReplaceTab) + { + m_settings.replaceInstrAction = FindReplace::ReplaceRelative; + switch(item) + { + case kReplaceInstrumentMinusOne: m_settings.replaceInstr = -1; break; + case kReplaceInstrumentPlusOne: m_settings.replaceInstr = 1; break; + + case kReplaceRelative: + { + CInputDlg dlg(this, _T("Custom Replacement Amount:"), -255, 255, m_settings.replaceInstr); + if(dlg.DoModal() == IDOK) + { + m_settings.replaceInstrAction = FindReplace::ReplaceRelative; + m_settings.replaceInstr = dlg.resultAsInt; + } else + { + // TODO undo selection + } + } + break; + + default: + m_settings.replaceInstrAction = FindReplace::ReplaceValue; + m_settings.replaceInstr = item; + break; + } + } else + { + if(item == kFindRange) + { + CFindRangeDlg dlg(this, 1, m_settings.findInstrMin, MAX_INSTRUMENTS - 1, m_settings.findInstrMax, CFindRangeDlg::kDecimal); + if(dlg.DoModal() == IDOK) + { + m_settings.findInstrMin = static_cast<ModCommand::INSTR>(dlg.GetMinVal()); + m_settings.findInstrMax = static_cast<ModCommand::INSTR>(dlg.GetMaxVal()); + } + } else + { + m_settings.findInstrMin = m_settings.findInstrMax = static_cast<ModCommand::INSTR>(item); + } + } + if(IsPCEvent()) + UpdateVolumeList(); +} + + +void CFindReplaceTab::RelativeOrMultiplyPrompt(CComboBox &comboBox, FindReplace::ReplaceMode &action, int &value, int range, bool isHex) +{ + int sel = comboBox.GetCurSel(); + int item = static_cast<int>(comboBox.GetItemData(sel)); + + if(sel == CB_ERR) + { + item = 0; + CString s; + comboBox.GetWindowText(s); + s.TrimLeft(); + if(s.GetLength() >= 1) + { + TCHAR first = s[0]; + if(first == _T('+')) + { + item = kReplaceRelative; + sel = 0; + } else if(first == _T('*')) + { + item = kReplaceMultiply; + sel = 1; + } + } + if(!item) + { + if(isHex) + { + int len = ::GetWindowTextLengthA(m_cbnParam); + std::string sHex(len, 0); + ::GetWindowTextA(m_cbnParam, &sHex[0], len + 1); + item = mpt::String::Parse::HexToUnsignedInt(sHex); + } else + { + item = ConvertStrTo<int>(s); + } + } + } + + if(item == kReplaceRelative || item == kReplaceMultiply) + { + const TCHAR *prompt, *format; + FindReplace::ReplaceMode act; + if(item == kReplaceRelative) + { + act = FindReplace::ReplaceRelative; + prompt = _T("Amount to add or subtract:"); + format = _T("+ %d"); + } else + { + act = FindReplace::ReplaceMultiply; + prompt = _T("Multiply by percentage:"); + format = _T("* %d%%"); + } + + range *= 100; + CInputDlg dlg(this, prompt, -range, range, value); + if(dlg.DoModal() == IDOK) + { + value = dlg.resultAsInt; + action = act; + + TCHAR s[32]; + wsprintf(s, format, value); + comboBox.DeleteString(sel); + comboBox.InsertString(sel, s); + comboBox.SetItemData(sel, item); + comboBox.SetCurSel(sel); + } else + { + // TODO undo selection + } + } else + { + action = FindReplace::ReplaceValue; + value = item; + } +} + + +void CFindReplaceTab::OnVolumeChanged() +{ + CheckOnChange(IDC_CHECK4); + int item = m_cbnVolume.GetCurSel(); + if(item != CB_ERR) + item = static_cast<int>(m_cbnVolume.GetItemData(item)); + else + item = GetDlgItemInt(IDC_COMBO4); + + int rangeMax = IsPCEvent() ? ModCommand::maxColumnValue : 64; + if(m_isReplaceTab) + { + RelativeOrMultiplyPrompt(m_cbnVolume, m_settings.replaceVolumeAction, m_settings.replaceVolume, rangeMax, false); + } else + { + if(item == kFindRange) + { + CFindRangeDlg dlg(this, 0, m_settings.findVolumeMin, rangeMax, m_settings.findVolumeMax, CFindRangeDlg::kDecimal); + if(dlg.DoModal() == IDOK) + { + m_settings.findVolumeMin = dlg.GetMinVal(); + m_settings.findVolumeMax = dlg.GetMaxVal(); + } else + { + // TODO undo selection + } + } else + { + m_settings.findVolumeMin = m_settings.findVolumeMax = item; + } + } +} + + +void CFindReplaceTab::OnParamChanged() +{ + CheckOnChange(IDC_CHECK6); + int item = m_cbnParam.GetCurSel(); + if(item != CB_ERR) + { + item = static_cast<int>(m_cbnParam.GetItemData(item)); + } else + { + int len = ::GetWindowTextLengthA(m_cbnParam); + std::string s(len, 0); + ::GetWindowTextA(m_cbnParam, &s[0], len + 1); + item = mpt::String::Parse::HexToUnsignedInt(s); + } + + // Apply parameter value mask if required (e.g. SDx has mask D0). + int effectIndex = static_cast<int>(m_cbnCommand.GetItemData(m_cbnCommand.GetCurSel())); + UINT mask = (effectIndex > -1) ? m_effectInfo.GetEffectMaskFromIndex(effectIndex) : 0; + + if(m_isReplaceTab) + { + RelativeOrMultiplyPrompt(m_cbnParam, m_settings.replaceParamAction, m_settings.replaceParam, 256, true); + if(m_settings.replaceParamAction == FindReplace::ReplaceValue && effectIndex > -1) + { + m_settings.replaceParam |= mask; + } + } else + { + if(item == kFindRange) + { + CFindRangeDlg dlg(this, 0, m_settings.findParamMin & ~mask, m_cbnParam.GetCount() - 2, m_settings.findParamMax & ~mask, CFindRangeDlg::kHex); + if(dlg.DoModal() == IDOK) + { + m_settings.findParamMin = dlg.GetMinVal() | mask; + m_settings.findParamMax = dlg.GetMaxVal() | mask; + } else + { + // TODO undo selection + } + } else + { + m_settings.findParamMin = m_settings.findParamMax = (item | mask); + } + } +} + + +void CFindReplaceTab::OnPCParamChanged() +{ + CheckOnChange(IDC_CHECK3); + int item = static_cast<int>(m_cbnPCParam.GetItemData(m_cbnPCParam.GetCurSel())); + + if(m_isReplaceTab) + { + RelativeOrMultiplyPrompt(m_cbnPCParam, m_settings.replaceParamAction, m_settings.replaceParam, 256, false); + } else + { + if(item == kFindRange) + { + CFindRangeDlg dlg(this, 0, m_settings.findParamMin, ModCommand::maxColumnValue, m_settings.findParamMax, CFindRangeDlg::kDecimal); + if(dlg.DoModal() == IDOK) + { + m_settings.findParamMin = dlg.GetMinVal(); + m_settings.findParamMax = dlg.GetMaxVal(); + } else + { + // TODO undo selection + } + } else + { + m_settings.findParamMin = m_settings.findParamMax = item; + } + } +} + + +void CFindReplaceTab::OnCheckChannelSearch() +{ + if (!m_isReplaceTab) + { + BOOL b = IsDlgButtonChecked(IDC_CHECK7); + GetDlgItem(IDC_EDIT1)->EnableWindow(b); + GetDlgItem(IDC_SPIN1)->EnableWindow(b); + GetDlgItem(IDC_EDIT2)->EnableWindow(b); + GetDlgItem(IDC_SPIN2)->EnableWindow(b); + } +} + + +void CFindReplaceTab::OnOK() +{ + // Search flags + FlagSet<FindReplace::Flags> &flags = m_isReplaceTab ? m_settings.replaceFlags : m_settings.findFlags; + flags.reset(); + flags.set(FindReplace::Note, !!IsDlgButtonChecked(IDC_CHECK1)); + flags.set(FindReplace::Instr, !!IsDlgButtonChecked(IDC_CHECK2)); + if(IsPCEvent()) + { + flags.set(FindReplace::PCParam, !!IsDlgButtonChecked(IDC_CHECK3)); + flags.set(FindReplace::PCValue, !!IsDlgButtonChecked(IDC_CHECK4)); + } else + { + flags.set(FindReplace::VolCmd, !!IsDlgButtonChecked(IDC_CHECK3)); + flags.set(FindReplace::Volume, !!IsDlgButtonChecked(IDC_CHECK4)); + flags.set(FindReplace::Command, !!IsDlgButtonChecked(IDC_CHECK5)); + flags.set(FindReplace::Param, !!IsDlgButtonChecked(IDC_CHECK6)); + } + if(m_isReplaceTab) + { + flags.set(FindReplace::Replace, !!IsDlgButtonChecked(IDC_CHECK7)); + flags.set(FindReplace::ReplaceAll, !!IsDlgButtonChecked(IDC_CHECK8)); + } else + { + flags.set(FindReplace::InChannels, !!IsDlgButtonChecked(IDC_CHECK7)); + flags.set(FindReplace::FullSearch, !!IsDlgButtonChecked(IDC_RADIO2)); + flags.set(FindReplace::InPatSelection, !!IsDlgButtonChecked(IDC_RADIO3)); + } + + // Min/Max channels + if (!m_isReplaceTab) + { + m_settings.findChnMin = static_cast<CHANNELINDEX>(GetDlgItemInt(IDC_EDIT1) - 1); + m_settings.findChnMax = static_cast<CHANNELINDEX>(GetDlgItemInt(IDC_EDIT2) - 1); + if (m_settings.findChnMax < m_settings.findChnMin) + { + std::swap(m_settings.findChnMin, m_settings.findChnMax); + } + } + CPropertyPage::OnOK(); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.h new file mode 100644 index 00000000..afd640b8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFindReplaceDlg.h @@ -0,0 +1,100 @@ +/* + * PatternFindReplaceDlg.h + * ----------------------- + * Purpose: The find/replace dialog for pattern data. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "EffectInfo.h" +#include "PatternCursor.h" + +OPENMPT_NAMESPACE_BEGIN + +///////////////////////////////////////////////////////////////////////// +// Search/Replace + +struct FindReplace; + +class CFindReplaceTab: public CPropertyPage +{ +protected: + CComboBox m_cbnNote, m_cbnInstr, m_cbnVolCmd, m_cbnVolume, m_cbnCommand, m_cbnParam, m_cbnPCParam; + + CSoundFile &m_sndFile; + FindReplace &m_settings; + EffectInfo m_effectInfo; + ModCommand m_initialValues; + bool m_isReplaceTab; + + // Special ItemData values + enum + { + kFindAny = INT_MAX - 1, + kFindRange = INT_MAX - 2, + + kReplaceRelative = INT_MAX - 3, + kReplaceMultiply = INT_MAX - 4, + + kReplaceNoteMinusOne = INT_MAX - 5, + kReplaceNotePlusOne = INT_MAX - 6, + kReplaceNoteMinusOctave = INT_MAX - 7, + kReplaceNotePlusOctave = INT_MAX - 8, + + kReplaceInstrumentMinusOne = INT_MAX - 5, + kReplaceInstrumentPlusOne = INT_MAX - 6, + }; + +public: + CFindReplaceTab(UINT nIDD, bool isReplaceTab, CSoundFile &sf, FindReplace &settings, const ModCommand &initialValues) + : CPropertyPage(nIDD) + , m_sndFile(sf) + , m_settings(settings) + , m_effectInfo(sf) + , m_initialValues(initialValues) + , m_isReplaceTab(isReplaceTab) + { } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + void DoDataExchange(CDataExchange* pDX) override; + + bool IsPCEvent() const; + + void UpdateInstrumentList(); + void UpdateVolumeList(); + void UpdateParamList(); + + // When a combobox is focussed, check the corresponding checkbox. + void CheckOnChange(int nIDButton) { CheckDlgButton(nIDButton, BST_CHECKED); CheckReplace(nIDButton); }; + afx_msg void OnNoteChanged(); + afx_msg void OnInstrChanged(); + afx_msg void OnVolCmdChanged() { CheckOnChange(IDC_CHECK3); UpdateVolumeList(); }; + afx_msg void OnVolumeChanged(); + afx_msg void OnEffectChanged() { CheckOnChange(IDC_CHECK5); UpdateParamList(); }; + afx_msg void OnParamChanged(); + afx_msg void OnPCParamChanged(); + // When a checkbox is checked, also check "Replace By". + afx_msg void OnCheckNote() { CheckReplace(IDC_CHECK1); }; + afx_msg void OnCheckInstr() { CheckReplace(IDC_CHECK2); }; + afx_msg void OnCheckVolCmd() { CheckReplace(IDC_CHECK3); }; + afx_msg void OnCheckVolume() { CheckReplace(IDC_CHECK4); }; + afx_msg void OnCheckEffect() { CheckReplace(IDC_CHECK5); }; + afx_msg void OnCheckParam() { CheckReplace(IDC_CHECK6); }; + // Check "Replace By" + afx_msg void CheckReplace(int nIDButton) { if(m_isReplaceTab && IsDlgButtonChecked(nIDButton)) CheckDlgButton(IDC_CHECK7, BST_CHECKED); }; + + afx_msg void OnCheckChannelSearch(); + + void RelativeOrMultiplyPrompt(CComboBox &comboBox, FindReplace::ReplaceMode &action, int &value, int range, bool isHex); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.cpp new file mode 100644 index 00000000..71dd290f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.cpp @@ -0,0 +1,480 @@ +/* + * PatternFont.cpp + * --------------- + * Purpose: Code for creating pattern font bitmaps + * 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 "PatternFont.h" +#include "Mptrack.h" +#include "Mainfrm.h" +#include "TrackerSettings.h" +#include "../soundlib/Tables.h" + +OPENMPT_NAMESPACE_BEGIN + +////////////////////////////////////////////// +// Font Definitions + +// Medium Font (Default) +static constexpr PATTERNFONT gDefaultPatternFont = +{ + nullptr, + nullptr, + 92,13, // Column Width & Height + 0,0, // Clear location + 130,8, // Space Location. + {20, 20, 24, 9, 15}, // Element Widths + {0, 0, 0, 0, 0}, // Padding pixels contained in element width + 20,13, // Numbers 0-F (hex) + 30,13, // Numbers 10-29 (dec) + 64,26, // A-M# + 78,26, // N-Z? // MEDIUM FONT !!! + 0, 0, + { 7, 5 }, 8, // Note & Octave Width + 42,13, // Volume Column Effects + 8,8, + -1, + 8, // 8+7 = 15 + -3, -1, 12, + 1, // Margin for first digit of PC event parameter number + 2, // Margin for first digit of PC event parameter value + 1, // Margin for second digit of parameter + 13, // Vertical spacing between letters in the bitmap +}; + + +////////////////////////////////////////////////// +// Small Font + +static constexpr PATTERNFONT gSmallPatternFont = +{ + nullptr, + nullptr, + 70,11, // Column Width & Height + 92,0, // Clear location + 130,8, // Space Location. + {16, 14, 18, 7, 11}, // Element Widths + {0, 0, 0, 0, 0}, // Padding pixels contained in element width + 108,13, // Numbers 0-F (hex) + 120,13, // Numbers 10-29 (dec) + 142,26, // A-M# + 150,26, // N-Z? // SMALL FONT !!! + 92, 0, // Notes + { 5, 5 }, 6, // Note & Octave Width + 132,13, // Volume Column Effects + 6,5, + -1, + 6, // 8+7 = 15 + -3, 1, 9, // InstrOfs + nInstrHiWidth + 1, // Margin for first digit of PC event parameter number + 2, // Margin for first digit of PC event parameter value + 1, // Margin for second digit of parameter + 13, // // Vertical spacing between letters in the bitmap +}; + +// NOTE: See also CViewPattern::DrawNote() when changing stuff here +// or adding new fonts - The custom tuning note names might require +// some additions there. + +const PATTERNFONT *PatternFont::currentFont = nullptr; + +static MODPLUGDIB customFontBitmap; +static MODPLUGDIB customFontBitmapASCII; + +static void DrawChar(HDC hDC, WCHAR ch, int x, int y, int w, int h) +{ + CRect rect(x, y, x + w, y + h); + ::DrawTextW(hDC, &ch, 1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); +} + +static void DrawChar(HDC hDC, CHAR ch, int x, int y, int w, int h) +{ + CRect rect(x, y, x + w, y + h); + ::DrawTextA(hDC, &ch, 1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); +} + +#if MPT_CXX_AT_LEAST(20) +static void DrawChar(HDC hDC, char8_t ch8, int x, int y, int w, int h) +{ + CRect rect(x, y, x + w, y + h); + CHAR ch = mpt::unsafe_char_convert<char>(ch8); + ::DrawTextA(hDC, &ch, 1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); +} +#endif + +template<typename char_t> +static void DrawString(HDC hDC, const char_t *text, int len, int x, int y, int w, int h) +{ + for(int i = 0; i < len; i++) + { + DrawChar(hDC, text[i], x, y, w, h); + x += w; + } +} + +void PatternFont::UpdateFont(HWND hwnd) +{ + FontSetting font = TrackerSettings::Instance().patternFont; + const PATTERNFONT *builtinFont = nullptr; + if(font.name == PATTERNFONT_SMALL || font.name.empty()) + { + builtinFont = &gSmallPatternFont; + } else if(font.name == PATTERNFONT_LARGE) + { + builtinFont = &gDefaultPatternFont; + } + + if(builtinFont != nullptr && font.size < 1) + { + currentFont = builtinFont; + return; + } + + static PATTERNFONT pf = { 0 }; + currentFont = &pf; + + static FontSetting previousFont; + if(previousFont == font) + { + // Nothing to do + return; + } + previousFont = font; + DeleteFontData(); + pf.dib = &customFontBitmap; + pf.dibASCII = nullptr; + + // Upscale built-in font? + if(builtinFont != nullptr) + { + // Copy and scale original 4-bit bitmap + LimitMax(font.size, 10); + font.size++; + customFontBitmap.bmiHeader = CMainFrame::bmpNotes->bmiHeader; + customFontBitmap.bmiHeader.biWidth *= font.size; + customFontBitmap.bmiHeader.biHeight *= font.size; + customFontBitmap.bmiHeader.biSizeImage = customFontBitmap.bmiHeader.biWidth * customFontBitmap.bmiHeader.biHeight / 2; + customFontBitmap.lpDibBits = new uint8[customFontBitmap.bmiHeader.biSizeImage]; + + // Upscale the image (ugly code ahead) + const uint8 *origPixels = CMainFrame::bmpNotes->lpDibBits; + uint8 *scaledPixels = customFontBitmap.lpDibBits; + const int bytesPerLine = customFontBitmap.bmiHeader.biWidth / 2, scaleBytes = bytesPerLine * font.size; + bool outPos = false; + for(int y = 0; y < CMainFrame::bmpNotes->bmiHeader.biHeight; y++, scaledPixels += scaleBytes - bytesPerLine) + { + for(int x = 0; x < CMainFrame::bmpNotes->bmiHeader.biWidth; x++) + { + uint8 pixel = *origPixels; + if(x % 2u == 0) + { + pixel >>= 4; + } else + { + pixel &= 0x0F; + origPixels++; + } + for(int scaleX = 0; scaleX < font.size; scaleX++) + { + if(!outPos) + { + for(int scaleY = 0; scaleY < scaleBytes; scaleY += bytesPerLine) + { + scaledPixels[scaleY] = pixel << 4; + } + } else + { + for(int scaleY = 0; scaleY < scaleBytes; scaleY += bytesPerLine) + { + scaledPixels[scaleY] |= pixel; + } + scaledPixels++; + } + outPos = !outPos; + } + } + } + pf.nWidth = (builtinFont->nWidth - 4) * font.size + 4; + pf.nHeight = builtinFont->nHeight * font.size; + pf.nClrX = builtinFont->nClrX * font.size; + pf.nClrY = builtinFont->nClrY * font.size; + pf.nSpaceX = builtinFont->nSpaceX * font.size; + pf.nSpaceY = builtinFont->nSpaceY * font.size; + for(std::size_t i = 0; i < std::size(pf.nEltWidths); i++) + { + pf.nEltWidths[i] = builtinFont->nEltWidths[i] * font.size; + pf.padding[i] = builtinFont->padding[i] * font.size; + } + pf.nNumX = builtinFont->nNumX * font.size; + pf.nNumY = builtinFont->nNumY * font.size; + pf.nNum10X = builtinFont->nNum10X * font.size; + pf.nNum10Y = builtinFont->nNum10Y * font.size; + pf.nAlphaAM_X = builtinFont->nAlphaAM_X * font.size; + pf.nAlphaAM_Y = builtinFont->nAlphaAM_Y * font.size; + pf.nAlphaNZ_X = builtinFont->nAlphaNZ_X * font.size; + pf.nAlphaNZ_Y = builtinFont->nAlphaNZ_Y * font.size; + pf.nNoteX = builtinFont->nNoteX * font.size; + pf.nNoteY = builtinFont->nNoteY * font.size; + pf.nNoteWidth[0] = builtinFont->nNoteWidth[0] * font.size; + pf.nNoteWidth[1] = builtinFont->nNoteWidth[1] * font.size; + pf.nOctaveWidth = builtinFont->nOctaveWidth * font.size; + pf.nVolX = builtinFont->nVolX * font.size; + pf.nVolY = builtinFont->nVolY * font.size; + pf.nVolCmdWidth = builtinFont->nVolCmdWidth * font.size; + pf.nVolHiWidth = builtinFont->nVolHiWidth * font.size; + pf.nCmdOfs = builtinFont->nCmdOfs * font.size; + pf.nParamHiWidth = builtinFont->nParamHiWidth * font.size; + pf.nInstrOfs = builtinFont->nInstrOfs * font.size; + pf.nInstr10Ofs = builtinFont->nInstr10Ofs * font.size; + pf.nInstrHiWidth = builtinFont->nInstrHiWidth * font.size; + pf.pcParamMargin = builtinFont->pcParamMargin * font.size; + pf.pcValMargin = builtinFont->pcValMargin * font.size; + pf.paramLoMargin = builtinFont->paramLoMargin * font.size; + pf.spacingY = builtinFont->spacingY * font.size; + + // Create 4-pixel border + const int bmWidth2 = pf.dib->bmiHeader.biWidth / 2; + for(int y = 0, x = (customFontBitmap.bmiHeader.biHeight - pf.nClrY - pf.nHeight) * bmWidth2 + (pf.nClrX + pf.nWidth - 4) / 2; y < pf.nHeight; y++, x += bmWidth2) + { + pf.dib->lpDibBits[x] = 0xEC; + pf.dib->lpDibBits[x + 1] = 0xC4; + } + + return; + } + + // Create our own font! + + CDC hDC; + hDC.CreateCompatibleDC(CDC::FromHandle(::GetDC(hwnd))); + // Point size to pixels + font.size = -MulDiv(font.size, Util::GetDPIy(hwnd), 720); + + CFont gdiFont; + gdiFont.CreateFont(font.size, 0, 0, 0, font.flags[FontSetting::Bold] ? FW_BOLD : FW_NORMAL, font.flags[FontSetting::Italic] ? TRUE : FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_RASTER_PRECIS, CLIP_DEFAULT_PRECIS, NONANTIALIASED_QUALITY, FIXED_PITCH | FF_DONTCARE, mpt::ToCString(font.name)); + CFont *oldFont = hDC.SelectObject(&gdiFont); + + CPoint pt = hDC.GetTextExtent(_T("W")); + const int charWidth = pt.x, charHeight = pt.y; + const int spacing = charWidth / 4; + const int width = charWidth * 12 + spacing * 2 + 4, height = charHeight * 21; + + pf.nWidth = width; // Column Width & Height, including 4-pixels border + pf.nHeight = charHeight; + pf.nClrX = pf.nClrY = 0; // Clear (empty note) location + pf.nSpaceX = charWidth * 7; // White location (must be big enough) + pf.nSpaceY = charHeight; + pf.nEltWidths[0] = charWidth * 3; // Note + pf.padding[0] = 0; + pf.nEltWidths[1] = charWidth * 3 + spacing; // Instr + pf.padding[1] = spacing; + pf.nEltWidths[2] = charWidth * 3 + spacing; // Volume + pf.padding[2] = spacing; + pf.nEltWidths[3] = charWidth; // Command letter + pf.padding[3] = 0; + pf.nEltWidths[4] = charWidth * 2; // Command param + pf.padding[4] = 0; + pf.nNumX = charWidth * 3; // Vertically-oriented numbers 0x00-0x0F + pf.nNumY = charHeight; + pf.nNum10X = charWidth * 4; // Numbers 10-29 + pf.nNum10Y = charHeight; + pf.nAlphaAM_X = charWidth * 6; // Letters A-M +# + pf.nAlphaAM_Y = charHeight * 2; + pf.nAlphaNZ_X = charWidth * 7; // Letters N-Z +? + pf.nAlphaNZ_Y = charHeight * 2; + pf.nNoteX = 0; // Notes ..., C-, C#, ... + pf.nNoteY = 0; + pf.nNoteWidth[0] = charWidth; // Total width of note (C#) + pf.nNoteWidth[1] = charWidth; // Total width of note (C#) + pf.nOctaveWidth = charWidth; // Octave Width + pf.nVolX = charWidth * 8; // Volume Column Effects + pf.nVolY = charHeight; + pf.nVolCmdWidth = charWidth; // Width of volume effect + pf.nVolHiWidth = charWidth; // Width of first character in volume parameter + pf.nCmdOfs = 0; // XOffset (-xxx) around the command letter + pf.nParamHiWidth = charWidth; + pf.nInstrOfs = -charWidth; + pf.nInstr10Ofs = 0; + pf.nInstrHiWidth = charWidth * 2; + pf.pcParamMargin = 0; + pf.pcValMargin = 0; + pf.paramLoMargin = 0; // Margin for second digit of parameter + pf.spacingY = charHeight; + + { + + pf.dib->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + pf.dib->bmiHeader.biWidth = ((width + 7) & ~7); // 4-byte alignment + pf.dib->bmiHeader.biHeight = -(int32)height; + pf.dib->bmiHeader.biSizeImage = pf.dib->bmiHeader.biWidth * height / 2; + pf.dib->bmiHeader.biPlanes = 1; + pf.dib->bmiHeader.biBitCount = 4; + pf.dib->bmiHeader.biCompression = BI_RGB; + pf.dib->lpDibBits = new uint8[pf.dib->bmiHeader.biSizeImage]; + pf.dib->bmiColors[0] = rgb2quad(RGB(0x00, 0x00, 0x00)); + pf.dib->bmiColors[15] = rgb2quad(RGB(0xFF, 0xFF, 0xFF)); + + uint8 *data = nullptr; + HBITMAP bitmap = ::CreateDIBSection(hDC, (BITMAPINFO *)&pf.dib->bmiHeader, DIB_RGB_COLORS, (void **)&data, nullptr, 0); + if(!bitmap) + { + hDC.SelectObject(oldFont); + gdiFont.DeleteObject(); + hDC.DeleteDC(); + currentFont = &gDefaultPatternFont; + return; + } + HGDIOBJ oldBitmap = hDC.SelectObject(bitmap); + + hDC.FillSolidRect(0, 0, width - 4, height, RGB(0xFF, 0xFF, 0xFF)); + hDC.SetTextColor(RGB(0x00, 0x00, 0x00)); + hDC.SetBkColor(RGB(0xFF, 0xFF, 0xFF)); + hDC.SetTextAlign(TA_TOP | TA_LEFT); + + // Empty cells (dots - i-th bit set = dot in the i-th column of a cell) + const uint8 dots[5] = { 1 | 2 | 4, 2 | 4, 2 | 4, 1, 1 | 2 }; + const auto dotStr = TrackerSettings::Instance().patternFontDot.Get(); + auto dotChar = dotStr.empty() ? UC_(' ') : dotStr[0]; + for(int cell = 0, offset = 0; cell < static_cast<int>(std::size(dots)); cell++) + { + uint8 dot = dots[cell]; + for(int i = 0; dot != 0; i++) + { + if(dot & 1) DrawChar(hDC, dotChar, pf.nClrX + offset + i * charWidth, pf.nClrY, charWidth, charHeight); + dot >>= 1; + } + offset += pf.nEltWidths[cell]; + } + + // Notes + for(int i = 0; i < 12; i++) + { + DrawString(hDC, NoteNamesSharp[i], 2, pf.nNoteX, pf.nNoteY + (i + 1) * charHeight, charWidth, charHeight); + } + DrawString(hDC, "^^", 2, pf.nNoteX, pf.nNoteY + 13 * charHeight, charWidth, charHeight); + DrawString(hDC, "==", 2, pf.nNoteX, pf.nNoteY + 14 * charHeight, charWidth, charHeight); + DrawString(hDC, "PC", 2, pf.nNoteX, pf.nNoteY + 15 * charHeight, charWidth, charHeight); + DrawString(hDC, "PCs", 3, pf.nNoteX, pf.nNoteY + 16 * charHeight, charWidth, charHeight); + DrawString(hDC, "~~", 2, pf.nNoteX, pf.nNoteY + 17 * charHeight, charWidth, charHeight); + + // Hex chars + const char hexChars[] = "0123456789ABCDEF"; + for(int i = 0; i < 16; i++) + { + DrawChar(hDC, hexChars[i], pf.nNumX, pf.nNumY + i * charHeight, charWidth, charHeight); + } + // Double-digit numbers + for(int i = 0; i < 20; i++) + { + char s[2]; + s[0] = char('1' + i / 10); + s[1] = char('0' + i % 10); + DrawString(hDC, s, 2, pf.nNum10X, pf.nNum10Y + i * charHeight, charWidth, charHeight); + } + + // Volume commands + const char volEffects[]= " vpcdabuhlrgfe:o"; + static_assert(mpt::array_size<decltype(volEffects)>::size - 1 == MAX_VOLCMDS); + for(int i = 0; i < MAX_VOLCMDS; i++) + { + DrawChar(hDC, volEffects[i], pf.nVolX, pf.nVolY + i * charHeight, charWidth, charHeight); + } + + // Letters A-Z + for(int i = 0; i < 13; i++) + { + DrawChar(hDC, char('A' + i), pf.nAlphaAM_X, pf.nAlphaAM_Y + i * charHeight, charWidth, charHeight); + DrawChar(hDC, char('N' + i), pf.nAlphaNZ_X, pf.nAlphaNZ_Y + i * charHeight, charWidth, charHeight); + } + // Special chars + DrawChar(hDC, '#', pf.nAlphaAM_X, pf.nAlphaAM_Y + 13 * charHeight, charWidth, charHeight); + DrawChar(hDC, '?', pf.nAlphaNZ_X, pf.nAlphaNZ_Y + 13 * charHeight, charWidth, charHeight); + DrawChar(hDC, 'b', pf.nAlphaAM_X, pf.nAlphaAM_Y + 14 * charHeight, charWidth, charHeight); + DrawChar(hDC, '\\', pf.nAlphaNZ_X, pf.nAlphaNZ_Y + 14 * charHeight, charWidth, charHeight); + DrawChar(hDC, '-', pf.nAlphaAM_X, pf.nAlphaAM_Y + 15 * charHeight, charWidth, charHeight); + DrawChar(hDC, ':', pf.nAlphaNZ_X, pf.nAlphaNZ_Y + 15 * charHeight, charWidth, charHeight); + DrawChar(hDC, '+', pf.nAlphaAM_X, pf.nAlphaAM_Y + 16 * charHeight, charWidth, charHeight); + DrawChar(hDC, '*', pf.nAlphaNZ_X, pf.nAlphaNZ_Y + 16 * charHeight, charWidth, charHeight); + DrawChar(hDC, 'd', pf.nAlphaAM_X, pf.nAlphaAM_Y + 17 * charHeight, charWidth, charHeight); + + ::GdiFlush(); + std::memcpy(pf.dib->lpDibBits, data, pf.dib->bmiHeader.biSizeImage); + // Create 4-pixel border + const int bmWidth2 = pf.dib->bmiHeader.biWidth / 2; + for(int y = 0, x = width / 2 - 2; y < height; y++, x += bmWidth2) + { + pf.dib->lpDibBits[x] = 0xEC; + pf.dib->lpDibBits[x + 1] = 0xC4; + } + + hDC.SelectObject(oldBitmap); + DeleteBitmap(bitmap); + + } + + { + + pf.dibASCII = &customFontBitmapASCII; + + pf.dibASCII->bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + pf.dibASCII->bmiHeader.biWidth = ((charWidth * 128 + 7) & ~7); // 4-byte alignment + pf.dibASCII->bmiHeader.biHeight = -(int32)charHeight; + pf.dibASCII->bmiHeader.biSizeImage = pf.dibASCII->bmiHeader.biWidth * charHeight / 2; + pf.dibASCII->bmiHeader.biPlanes = 1; + pf.dibASCII->bmiHeader.biBitCount = 4; + pf.dibASCII->bmiHeader.biCompression = BI_RGB; + pf.dibASCII->lpDibBits = new uint8[pf.dibASCII->bmiHeader.biSizeImage]; + pf.dibASCII->bmiColors[0] = rgb2quad(RGB(0x00, 0x00, 0x00)); + pf.dibASCII->bmiColors[15] = rgb2quad(RGB(0xFF, 0xFF, 0xFF)); + + uint8 *data = nullptr; + HBITMAP bitmap = ::CreateDIBSection(hDC, (BITMAPINFO *)&pf.dibASCII->bmiHeader, DIB_RGB_COLORS, (void **)&data, nullptr, 0); + if(!bitmap) + { + hDC.SelectObject(oldFont); + gdiFont.DeleteObject(); + hDC.DeleteDC(); + currentFont = &gDefaultPatternFont; + return; + } + HGDIOBJ oldBitmap = hDC.SelectObject(bitmap); + + hDC.FillSolidRect(0, 0, pf.dibASCII->bmiHeader.biWidth, -pf.dibASCII->bmiHeader.biHeight, RGB(0xFF, 0xFF, 0xFF)); + hDC.SetTextColor(RGB(0x00, 0x00, 0x00)); + hDC.SetBkColor(RGB(0xFF, 0xFF, 0xFF)); + hDC.SetTextAlign(TA_TOP | TA_LEFT); + + for(uint32 c = 32; c < 128; ++c) + { + DrawChar(hDC, (char)(unsigned char)c, charWidth * c, 0, charWidth, charHeight); + } + ::GdiFlush(); + + std::memcpy(pf.dibASCII->lpDibBits, data, pf.dibASCII->bmiHeader.biSizeImage); + + hDC.SelectObject(oldBitmap); + DeleteBitmap(bitmap); + + } + + hDC.SelectObject(oldFont); + gdiFont.DeleteObject(); + + hDC.DeleteDC(); +} + + +void PatternFont::DeleteFontData() +{ + delete[] customFontBitmap.lpDibBits; + customFontBitmap.lpDibBits = nullptr; + delete[] customFontBitmapASCII.lpDibBits; + customFontBitmapASCII.lpDibBits = nullptr; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.h new file mode 100644 index 00000000..0dce0d31 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternFont.h @@ -0,0 +1,56 @@ +/* + * PatternFont.h + * ------------- + * Purpose: Code for creating pattern font bitmaps + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +struct MODPLUGDIB; + +struct PATTERNFONT +{ + MODPLUGDIB *dib; + MODPLUGDIB *dibASCII; // optional + int nWidth, nHeight; // Column Width & Height, including 4-pixels border + int nClrX, nClrY; // Clear (empty note) location + int nSpaceX, nSpaceY; // White location (must be big enough) + UINT nEltWidths[5]; // Elements Sizes + UINT padding[5]; // Padding pixels contained in element width + int nNumX, nNumY; // Vertically-oriented numbers 0x00-0x0F + int nNum10X, nNum10Y; // Numbers 10-29 + int nAlphaAM_X,nAlphaAM_Y; // Letters A-M +# + int nAlphaNZ_X,nAlphaNZ_Y; // Letters N-Z +? + int nNoteX, nNoteY; // Notes ..., C-, C#, ... + int nNoteWidth[2]; // Total width of note (C#) + int nOctaveWidth; // Octave Width + int nVolX, nVolY; // Volume Column Effects + int nVolCmdWidth; // Width of volume effect + int nVolHiWidth; // Width of first character in volume parameter + int nCmdOfs; // XOffset (-xxx) around the command letter + int nParamHiWidth; + int nInstrOfs, nInstr10Ofs, nInstrHiWidth; + int pcParamMargin; // Margin for first digit of PC event parameter number + int pcValMargin; // Margin for first digit of PC event parameter value + int paramLoMargin; // Margin for second digit of parameter + int spacingY; // Vertical spacing between letters in the bitmap +}; + +class PatternFont +{ +public: + static const PATTERNFONT *currentFont; + + static void UpdateFont(HWND hwnd); + static void DeleteFontData(); +}; + +OPENMPT_NAMESPACE_END
\ No newline at end of file diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.cpp new file mode 100644 index 00000000..0fd1d6e4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.cpp @@ -0,0 +1,204 @@ +/* + * PatternGotoDialog.cpp + * --------------------- + * Purpose: Implementation of pattern "go to" dialog. + * 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 "PatternGotoDialog.h" +#include "Sndfile.h" + + +OPENMPT_NAMESPACE_BEGIN + + +// CPatternGotoDialog dialog + +CPatternGotoDialog::CPatternGotoDialog(CWnd *pParent, ROWINDEX row, CHANNELINDEX chan, PATTERNINDEX pat, ORDERINDEX ord, CSoundFile &sndFile) + : CDialog(IDD_EDIT_GOTO, pParent) + , m_SndFile(sndFile) + , m_nRow(row) + , m_nChannel(chan) + , m_nPattern(pat) + , m_nOrder(ord) + , m_nActiveOrder(ord) +{ } + + +void CPatternGotoDialog::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_SPIN1, m_SpinRow); + DDX_Control(pDX, IDC_SPIN2, m_SpinChannel); + DDX_Control(pDX, IDC_SPIN3, m_SpinPattern); + DDX_Control(pDX, IDC_SPIN4, m_SpinOrder); +} + + +BEGIN_MESSAGE_MAP(CPatternGotoDialog, CDialog) + ON_EN_CHANGE(IDC_GOTO_PAT, &CPatternGotoDialog::OnPatternChanged) + ON_EN_CHANGE(IDC_GOTO_ORD, &CPatternGotoDialog::OnOrderChanged) + ON_EN_CHANGE(IDC_GOTO_ROW, &CPatternGotoDialog::OnRowChanged) + ON_EN_CHANGE(IDC_EDIT5, &CPatternGotoDialog::OnTimeChanged) + ON_EN_CHANGE(IDC_EDIT6, &CPatternGotoDialog::OnTimeChanged) +END_MESSAGE_MAP() + + +BOOL CPatternGotoDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + HICON icon = ::LoadIcon(AfxGetResourceHandle(), MAKEINTRESOURCE(IDR_MODULETYPE)); + SetIcon(icon, FALSE); + SetIcon(icon, TRUE); + + UpdateNumRows(); + m_SpinChannel.SetRange32(1, m_SndFile.GetNumChannels()); + m_SpinPattern.SetRange32(0, std::max(m_SndFile.Patterns.GetNumPatterns(), PATTERNINDEX(1)) - 1); + m_SpinOrder.SetRange32(0, std::max(m_SndFile.Order().GetLengthTailTrimmed(), ORDERINDEX(1)) - 1); + SetDlgItemInt(IDC_GOTO_ROW, m_nRow); + SetDlgItemInt(IDC_GOTO_CHAN, m_nChannel); + SetDlgItemInt(IDC_GOTO_PAT, m_nPattern); + SetDlgItemInt(IDC_GOTO_ORD, m_nOrder); + UpdateTime(); + UnlockControls(); + return TRUE; +} + +void CPatternGotoDialog::OnOK() +{ + const auto &Order = m_SndFile.Order(); + m_nRow = mpt::saturate_cast<ROWINDEX>(GetDlgItemInt(IDC_GOTO_ROW)); + m_nChannel = mpt::saturate_cast<CHANNELINDEX>(GetDlgItemInt(IDC_GOTO_CHAN)); + + // Valid order item? + if(m_nOrder >= Order.size()) + { + MessageBeep(MB_ICONWARNING); + GetDlgItem(IDC_GOTO_ORD)->SetFocus(); + return; + } + // Does order match pattern? Does pattern exist? + if(Order[m_nOrder] != m_nPattern || !Order.IsValidPat(m_nOrder)) + { + MessageBeep(MB_ICONWARNING); + GetDlgItem(IDC_GOTO_PAT)->SetFocus(); + return; + } + + LimitMax(m_nRow, m_SndFile.Patterns[m_nPattern].GetNumRows() - ROWINDEX(1)); + Limit(m_nChannel, CHANNELINDEX(1), m_SndFile.GetNumChannels()); + + CDialog::OnOK(); +} + +void CPatternGotoDialog::OnPatternChanged() +{ + if(ControlsLocked()) + return; // The change to textbox did not come from user. + + m_nPattern = mpt::saturate_cast<PATTERNINDEX>(GetDlgItemInt(IDC_GOTO_PAT)); + m_nOrder = m_SndFile.Order().FindOrder(m_nPattern, m_nActiveOrder); + + if(m_nOrder == ORDERINDEX_INVALID) + { + m_nOrder = 0; + } + + LockControls(); + SetDlgItemInt(IDC_GOTO_ORD, m_nOrder); + UpdateNumRows(); + UpdateTime(); + UnlockControls(); +} + + +void CPatternGotoDialog::OnOrderChanged() +{ + if(ControlsLocked()) + return; // The change to textbox did not come from user. + + m_nOrder = mpt::saturate_cast<ORDERINDEX>(GetDlgItemInt(IDC_GOTO_ORD)); + if(m_SndFile.Order().IsValidPat(m_nOrder)) + { + m_nPattern = m_SndFile.Order()[m_nOrder]; + } + + LockControls(); + SetDlgItemInt(IDC_GOTO_PAT, m_nPattern); + UpdateNumRows(); + UpdateTime(); + UnlockControls(); +} + + +void CPatternGotoDialog::OnRowChanged() +{ + if(ControlsLocked()) + return; // The change to textbox did not come from user. + + m_nRow = mpt::saturate_cast<ROWINDEX>(GetDlgItemInt(IDC_GOTO_ROW)); + UpdateTime(); +} + + +void CPatternGotoDialog::OnTimeChanged() +{ + if(ControlsLocked()) + return; // The change to textbox did not come from user. + + BOOL success = TRUE; + auto minutes = GetDlgItemInt(IDC_EDIT5, &success); + if(!success) + return; + auto seconds = GetDlgItemInt(IDC_EDIT6, &success); + if(!success) + return; + + auto result = m_SndFile.GetLength(eNoAdjust, GetLengthTarget(minutes * 60.0 + seconds)).back(); + if(!result.targetReached) + return; + + m_nOrder = result.lastOrder; + m_nRow = result.lastRow; + if(m_SndFile.Order().IsValidPat(m_nOrder)) + m_nPattern = m_SndFile.Order()[m_nOrder]; + + LockControls(); + SetDlgItemInt(IDC_GOTO_ORD, m_nOrder); + SetDlgItemInt(IDC_GOTO_ROW, m_nRow); + SetDlgItemInt(IDC_GOTO_PAT, m_nPattern); + UnlockControls(); +} + + +void CPatternGotoDialog::UpdateNumRows() +{ + const ROWINDEX maxRow = (m_SndFile.Patterns.IsValidPat(m_nPattern) ? m_SndFile.Patterns[m_nPattern].GetNumRows() : MAX_PATTERN_ROWS) - 1; + m_SpinRow.SetRange32(0, maxRow); + if(m_nRow > maxRow) + { + m_nRow = maxRow; + SetDlgItemInt(IDC_GOTO_ROW, m_nRow); + } +} + + +void CPatternGotoDialog::UpdateTime() +{ + const double length = m_SndFile.GetPlaybackTimeAt(m_nOrder, m_nRow, false, false); + if(length < 0.0) + return; + const double minutes = std::floor(length / 60.0), seconds = std::fmod(length, 60.0); + LockControls(); + SetDlgItemInt(IDC_EDIT5, static_cast<int>(minutes)); + SetDlgItemInt(IDC_EDIT6, static_cast<int>(seconds)); + UnlockControls(); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.h new file mode 100644 index 00000000..45e322a3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PatternGotoDialog.h @@ -0,0 +1,57 @@ +/* + * PatternGotoDialog.h + * ------------------- + * Purpose: Implementation of pattern "go to" dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CSoundFile; + +// CPatternGotoDialog dialog + +class CPatternGotoDialog : public CDialog +{ + CSoundFile &m_SndFile; + CSpinButtonCtrl m_SpinRow, m_SpinChannel, m_SpinPattern, m_SpinOrder; + +public: + ROWINDEX m_nRow; + CHANNELINDEX m_nChannel; + PATTERNINDEX m_nPattern; + ORDERINDEX m_nOrder, m_nActiveOrder; + +public: + CPatternGotoDialog(CWnd *pParent, ROWINDEX row, CHANNELINDEX chan, PATTERNINDEX pat, ORDERINDEX ord, CSoundFile &sndFile); + BOOL OnInitDialog() override; + +protected: + bool m_controlLock = true; + + inline bool ControlsLocked() const { return m_controlLock; } + inline void LockControls() { m_controlLock = true; } + inline void UnlockControls() { m_controlLock = false; } + + void UpdateNumRows(); + void UpdateTime(); + + void DoDataExchange(CDataExchange* pDX) override; + void OnOK() override; + + afx_msg void OnPatternChanged(); + afx_msg void OnOrderChanged(); + afx_msg void OnRowChanged(); + afx_msg void OnTimeChanged(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.cpp new file mode 100644 index 00000000..076f8be2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.cpp @@ -0,0 +1,79 @@ +/* + * PlugNotFoundDlg.cpp + * ------------------- + * Purpose: Dialog for handling missing plugins + * 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 "PlugNotFoundDlg.h" +#include "../soundlib/plugins/PluginManager.h" +#include "Mptrack.h" +#include "resource.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(PlugNotFoundDialog, ResizableDialog) + ON_COMMAND(IDC_BUTTON_REMOVE, &PlugNotFoundDialog::OnRemove) +END_MESSAGE_MAP() + + +void PlugNotFoundDialog::DoDataExchange(CDataExchange *pDX) +{ + ResizableDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST1, m_List); +} + + +PlugNotFoundDialog::PlugNotFoundDialog(std::vector<VSTPluginLib *> &plugins, CWnd *parent) + : ResizableDialog(IDD_MISSINGPLUGS, parent) + , m_plugins(plugins) +{ +} + + +BOOL PlugNotFoundDialog::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + + // Initialize table + const CListCtrlEx::Header headers[] = + { + { _T("Plugin"), 128, LVCFMT_LEFT }, + { _T("File Path"), 308, LVCFMT_LEFT }, + }; + m_List.SetHeaders(headers); + m_List.SetExtendedStyle(m_List.GetExtendedStyle() | LVS_EX_FULLROWSELECT | LVS_EX_CHECKBOXES); + + for(const auto &plug : m_plugins) + { + const int insertAt = m_List.InsertItem(m_List.GetItemCount(), plug->libraryName.AsNative().c_str()); + if(insertAt == -1) + continue; + m_List.SetItemText(insertAt, 1, plug->dllPath.AsNative().c_str()); + m_List.SetItemDataPtr(insertAt, plug); + m_List.SetCheck(insertAt); + } + m_List.SetFocus(); + + return TRUE; +} + + +void PlugNotFoundDialog::OnRemove() +{ + const int plugs = m_List.GetItemCount(); + for(int i = 0; i < plugs; i++) + { + if(m_List.GetCheck(i)) + { + theApp.GetPluginManager()->RemovePlugin(static_cast<VSTPluginLib *>(m_List.GetItemDataPtr(i))); + } + } + OnOK(); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.h new file mode 100644 index 00000000..13e6157e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/PlugNotFoundDlg.h @@ -0,0 +1,38 @@ +/* + * PlugNotFoundDlg.h + * ----------------- + * Purpose: Dialog for handling missing plugins + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "ResizableDialog.h" +#include "CListCtrl.h" + +OPENMPT_NAMESPACE_BEGIN + +struct VSTPluginLib; + +class PlugNotFoundDialog : public ResizableDialog +{ + std::vector<VSTPluginLib *> &m_plugins; + CListCtrlEx m_List; + +public: + PlugNotFoundDialog(std::vector<VSTPluginLib *> &plugins, CWnd *parent); + + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + + afx_msg void OnRemove(); + + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.cpp new file mode 100644 index 00000000..7fc12ce3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.cpp @@ -0,0 +1,120 @@ +/* + * ProgressDialog.cpp + * ------------------ + * Purpose: An abortable, progress-indicating dialog, e.g. for showing conversion progress. + * 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 "resource.h" +#include "ProgressDialog.h" +#include "Mptrack.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(CProgressDialog, CDialog) + ON_COMMAND(IDC_BUTTON1, &CProgressDialog::Run) +END_MESSAGE_MAP() + +CProgressDialog::CProgressDialog(CWnd *parent, UINT resourceID) + : CDialog{resourceID <= 0 ? IDD_PROGRESS : resourceID, parent} + , m_customDialog{resourceID > 0} +{ } + +CProgressDialog::~CProgressDialog() +{ + if(m_taskBarList) + { + m_taskBarList->SetProgressState(*theApp.m_pMainWnd, TBPF_NOPROGRESS); + m_taskBarList->Release(); + } + + if(IsWindow(m_hWnd)) + { + // This should only happen if this dialog gets destroyed as part of stack unwinding + EndDialog(IDCANCEL); + DestroyWindow(); + } +} + +BOOL CProgressDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + if(!m_customDialog) + PostMessage(WM_COMMAND, IDC_BUTTON1); + return TRUE; +} + + +void CProgressDialog::SetTitle(const TCHAR *title) +{ + SetWindowText(title); +} + + + +void CProgressDialog::SetAbortText(const TCHAR *abort) +{ + SetDlgItemText(IDCANCEL, abort); +} + + +void CProgressDialog::SetText(const TCHAR *text) +{ + SetDlgItemText(IDC_TEXT1, text); +} + + +void CProgressDialog::SetRange(uint64 min, uint64 max) +{ + MPT_ASSERT(min <= max); + m_min = min; + m_max = max; + m_shift = 0; + // Is the range too big for 32-bit values? + while(max > int32_max) + { + m_shift++; + max >>= 1; + } + ::SendMessage(::GetDlgItem(m_hWnd, IDC_PROGRESS1), PBM_SETRANGE32, static_cast<uint32>(m_min >> m_shift), static_cast<uint32>(m_max >> m_shift)); +} + + +void CProgressDialog::SetProgress(uint64 progress) +{ + ::SendMessage(::GetDlgItem(m_hWnd, IDC_PROGRESS1), PBM_SETPOS, static_cast<uint32>(progress >> m_shift), 0); + if(m_taskBarList != nullptr) + { + m_taskBarList->SetProgressValue(*theApp.m_pMainWnd, progress - m_min, m_max - m_min); + } +} + + +void CProgressDialog::EnableTaskbarProgress() +{ + if(CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_ALL, IID_ITaskbarList3, (void**)&m_taskBarList) != S_OK) + { + return; + } + if(m_taskBarList != nullptr) + { + m_taskBarList->SetProgressState(*theApp.m_pMainWnd, TBPF_NORMAL); + } +} + + +void CProgressDialog::ProcessMessages() +{ + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.h new file mode 100644 index 00000000..a476f788 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ProgressDialog.h @@ -0,0 +1,55 @@ +/* + * ProgressDialog.h + * ---------------- + * Purpose: An abortable, progress-indicating dialog, e.g. for showing conversion progress. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class CProgressDialog : public CDialog +{ +private: + ITaskbarList3 *m_taskBarList = nullptr; + uint64 m_min = 0, m_max = 0, m_shift = 0; + const bool m_customDialog; + +public: + bool m_abort = false; + + CProgressDialog(CWnd *parent = nullptr, UINT resourceID = 0); + ~CProgressDialog(); + + // Set the window title + void SetTitle(const TCHAR *title); + // Set the text on abort button + void SetAbortText(const TCHAR *abort); + // Set the text to be displayed along the progress bar. + void SetText(const TCHAR *text); + // Set the minimum and maximum value of the progress bar. + void SetRange(uint64 min, uint64 max); + // Set the current progress. + void SetProgress(uint64 progress); + // Show the progress in the task bar as well + void EnableTaskbarProgress(); + // Process all queued Windows messages + void ProcessMessages(); + // Run method for this dialog that must implement the action to be carried out - unless a custom resourceID was provided, + // in which case the user is responsible for running the dialog and the implementation of this function may be a no-op. + virtual void Run() = 0; + +protected: + BOOL OnInitDialog() override; + void OnCancel() override { m_abort = true; } + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.cpp new file mode 100644 index 00000000..32d337e0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.cpp @@ -0,0 +1,180 @@ +/* + * Reporting.cpp + * ------------- + * Purpose: A class for showing notifications, prompts, etc... + * 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 "Reporting.h" +#include "../mptrack/Mainfrm.h" +#include "../mptrack/InputHandler.h" + + +OPENMPT_NAMESPACE_BEGIN + + +static inline UINT LogLevelToFlags(LogLevel level) +{ + switch(level) + { + case LogDebug: return MB_OK; break; + case LogNotification: return MB_OK; break; + case LogInformation: return MB_OK | MB_ICONINFORMATION; break; + case LogWarning: return MB_OK | MB_ICONWARNING; break; + case LogError: return MB_OK | MB_ICONERROR; break; + } + return MB_OK; +} + + +static CString GetTitle() +{ + return MAINFRAME_TITLE; +} + + +static CString FillEmptyCaption(const CString &caption, LogLevel level) +{ + CString result = caption; + if(result.IsEmpty()) + { + result = GetTitle() + _T(" - "); + switch(level) + { + case LogDebug: result += _T("Debug"); break; + case LogNotification: result += _T("Notification"); break; + case LogInformation: result += _T("Information"); break; + case LogWarning: result += _T("Warning"); break; + case LogError: result += _T("Error"); break; + } + } + return result; +} + + +static CString FillEmptyCaption(const CString &caption) +{ + CString result = caption; + if(result.IsEmpty()) + { + result = GetTitle(); + } + return result; +} + + +static UINT ShowNotificationImpl(const CString &text, const CString &caption, UINT flags, const CWnd *parent) +{ + if(parent == nullptr) + { + parent = CMainFrame::GetActiveWindow(); + } + BypassInputHandler bih; + UINT result = ::MessageBox(parent->GetSafeHwnd(), text, caption.IsEmpty() ? CString(MAINFRAME_TITLE) : caption, flags); + return result; +} + + +UINT Reporting::CustomNotification(const AnyStringLocale &text, const AnyStringLocale &caption, UINT flags, const CWnd *parent) +{ + return ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption)), flags, parent); +} + + +void Reporting::Notification(const AnyStringLocale &text, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(CString(), LogNotification), LogLevelToFlags(LogNotification), parent); +} +void Reporting::Notification(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption), LogNotification), LogLevelToFlags(LogNotification), parent); +} + + +void Reporting::Information(const AnyStringLocale &text, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(CString(), LogInformation), LogLevelToFlags(LogInformation), parent); +} +void Reporting::Information(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption), LogInformation), LogLevelToFlags(LogInformation), parent); +} + + +void Reporting::Warning(const AnyStringLocale &text, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(CString(), LogWarning), LogLevelToFlags(LogWarning), parent); +} +void Reporting::Warning(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption), LogWarning), LogLevelToFlags(LogWarning), parent); +} + + +void Reporting::Error(const AnyStringLocale &text, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(CString(), LogError), LogLevelToFlags(LogError), parent); +} +void Reporting::Error(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption), LogError), LogLevelToFlags(LogError), parent); +} + + +void Reporting::Message(LogLevel level, const AnyStringLocale &text, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(CString(), level), LogLevelToFlags(level), parent); +} +void Reporting::Message(LogLevel level, const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption), level), LogLevelToFlags(level), parent); +} + + +ConfirmAnswer Reporting::Confirm(const AnyStringLocale &text, bool showCancel, bool defaultNo, const CWnd *parent) +{ + return Confirm(mpt::ToCString(text), GetTitle() + _T(" - Confirmation"), showCancel, defaultNo, parent); +} + + +ConfirmAnswer Reporting::Confirm(const AnyStringLocale &text, const AnyStringLocale &caption, bool showCancel, bool defaultNo, const CWnd *parent) +{ + UINT result = ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption)), (showCancel ? MB_YESNOCANCEL : MB_YESNO) | MB_ICONQUESTION | (defaultNo ? MB_DEFBUTTON2 : 0), parent); + switch(result) + { + case IDYES: + return cnfYes; + case IDNO: + return cnfNo; + default: + case IDCANCEL: + return cnfCancel; + } +} + + +RetryAnswer Reporting::RetryCancel(const AnyStringLocale &text, const CWnd *parent) +{ + return RetryCancel(mpt::ToCString(text), GetTitle(), parent); +} + + +RetryAnswer Reporting::RetryCancel(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent) +{ + UINT result = ShowNotificationImpl(mpt::ToCString(text), FillEmptyCaption(mpt::ToCString(caption)), MB_RETRYCANCEL, parent); + switch(result) + { + case IDRETRY: + return rtyRetry; + default: + case IDCANCEL: + return rtyCancel; + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.h b/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.h new file mode 100644 index 00000000..649142b9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Reporting.h @@ -0,0 +1,77 @@ +/* + * Reporting.h + * ----------- + * Purpose: A class for showing notifications, prompts, etc... + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +enum ConfirmAnswer +{ + cnfYes, + cnfNo, + cnfCancel, +}; + + +enum RetryAnswer +{ + rtyRetry, + rtyCancel, +}; + + +class Reporting +{ + +public: + + // TODO Quick'n'dirty workaround for MsgBox(). Shouldn't be public. + static UINT CustomNotification(const AnyStringLocale &text, const AnyStringLocale &caption, UINT flags, const CWnd *parent); + + // Show a simple notification + static void Notification(const AnyStringLocale &text, const CWnd *parent = nullptr); + static void Notification(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + + // Show a simple information + static void Information(const AnyStringLocale &text, const CWnd *parent = nullptr); + static void Information(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + + // Show a simple warning + static void Warning(const AnyStringLocale &text, const CWnd *parent = nullptr); + static void Warning(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + + // Show an error box. + static void Error(const AnyStringLocale &text, const CWnd *parent = nullptr); + static void Error(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + + // Simplified version of the above + static void Message(LogLevel level, const AnyStringLocale &text, const CWnd *parent = nullptr); + static void Message(LogLevel level, const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + + // Show a confirmation dialog. + static ConfirmAnswer Confirm(const AnyStringLocale &text, bool showCancel = false, bool defaultNo = false, const CWnd *parent = nullptr); + static ConfirmAnswer Confirm(const AnyStringLocale &text, const AnyStringLocale &caption, bool showCancel = false, bool defaultNo = false, const CWnd *parent = nullptr); + // work-around string literals for caption decaying to bool and catching the wrong overload instead of converting to a string. + static ConfirmAnswer Confirm(const AnyStringLocale &text, const char *caption, bool showCancel = false, bool defaultNo = false, const CWnd *parent = nullptr) { return Confirm(text, AnyStringLocale(caption), showCancel, defaultNo, parent); } + static ConfirmAnswer Confirm(const AnyStringLocale &text, const wchar_t *caption, bool showCancel = false, bool defaultNo = false, const CWnd *parent = nullptr) { return Confirm(text, AnyStringLocale(caption), showCancel, defaultNo, parent); } + static ConfirmAnswer Confirm(const AnyStringLocale &text, const CString &caption, bool showCancel = false, bool defaultNo = false, const CWnd *parent = nullptr) { return Confirm(text, AnyStringLocale(caption), showCancel, defaultNo, parent); } + + // Show a confirmation dialog. + static RetryAnswer RetryCancel(const AnyStringLocale &text, const CWnd *parent = nullptr); + static RetryAnswer RetryCancel(const AnyStringLocale &text, const AnyStringLocale &caption, const CWnd *parent = nullptr); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.cpp new file mode 100644 index 00000000..492a4ad2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.cpp @@ -0,0 +1,47 @@ +/* + * ResizableDialog.cpp + * ------------------- + * Purpose: A wrapper for resizable MFC dialogs that fixes the dialog's minimum size + * (as MFC does not scale controls properly if the user makes the dialog smaller than it originally was) + * 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 "ResizableDialog.h" +#include "resource.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(ResizableDialog, CDialog) + ON_WM_GETMINMAXINFO() +END_MESSAGE_MAP() + +ResizableDialog::ResizableDialog(UINT nIDTemplate, CWnd *pParentWnd) + : CDialog(nIDTemplate, pParentWnd) +{ } + + +BOOL ResizableDialog::OnInitDialog() +{ + CRect rect; + GetWindowRect(rect); + m_minSize = rect.Size(); + + HICON icon = ::LoadIcon(AfxGetResourceHandle(), MAKEINTRESOURCE(IDR_MODULETYPE)); + SetIcon(icon, FALSE); + SetIcon(icon, TRUE); + + return CDialog::OnInitDialog(); +} + + +void ResizableDialog::OnGetMinMaxInfo(MINMAXINFO *mmi) +{ + mmi->ptMinTrackSize = m_minSize; + CDialog::OnGetMinMaxInfo(mmi); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.h new file mode 100644 index 00000000..2771dba2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ResizableDialog.h @@ -0,0 +1,33 @@ +/* + * ResizableDialog.h + * ----------------- + * Purpose: A wrapper for resizable MFC dialogs that fixes the dialog's minimum size + * (as MFC does not scale controls properly if the user makes the dialog smaller than it originally was) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class ResizableDialog : public CDialog +{ +private: + CPoint m_minSize; + +public: + ResizableDialog() = default; + explicit ResizableDialog(UINT nIDTemplate, CWnd *pParentWnd = nullptr); + +protected: + BOOL OnInitDialog() override; + afx_msg void OnGetMinMaxInfo(MINMAXINFO *mmi); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.cpp new file mode 100644 index 00000000..01bf61f0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.cpp @@ -0,0 +1,132 @@ +/* + * SampleConfigDlg.cpp + * ------------------- + * Purpose: Implementation of the sample/instrument editor settings dialog. + * 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 "Mainfrm.h" +#include "Moddoc.h" +#include "SampleConfigDlg.h" + + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(COptionsSampleEditor, CPropertyPage) + ON_WM_HSCROLL() + ON_EN_CHANGE(IDC_EDIT_UNDOSIZE, &COptionsSampleEditor::OnUndoSizeChanged) + ON_EN_CHANGE(IDC_EDIT_FINETUNE, &COptionsSampleEditor::OnSettingsChanged) + ON_EN_CHANGE(IDC_FLAC_COMPRESSION, &COptionsSampleEditor::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_DEFAULT_FORMAT, &COptionsSampleEditor::OnSettingsChanged) + ON_CBN_SELCHANGE(IDC_VOLUME_HANDLING, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_RADIO1, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_RADIO2, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_RADIO3, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_COMPRESS_ITI, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_PREVIEW_SAMPLES, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_NORMALIZE, &COptionsSampleEditor::OnSettingsChanged) + ON_COMMAND(IDC_CURSORINHEX, &COptionsSampleEditor::OnSettingsChanged) +END_MESSAGE_MAP() + + +void COptionsSampleEditor::DoDataExchange(CDataExchange* pDX) +{ + CPropertyPage::DoDataExchange(pDX); + //{{AFX_DATA_MAP(COptionsSampleEditor) + DDX_Control(pDX, IDC_DEFAULT_FORMAT, m_cbnDefaultSampleFormat); + DDX_Control(pDX, IDC_VOLUME_HANDLING, m_cbnDefaultVolumeHandling); + //}}AFX_DATA_MAP +} + + +BOOL COptionsSampleEditor::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + SetDlgItemInt(IDC_EDIT_UNDOSIZE, TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInPercent()); + SetDlgItemInt(IDC_EDIT_FINETUNE, TrackerSettings::Instance().m_nFinetuneStep); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1))->SetRange32(0, 100); + static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN2))->SetRange32(1, 200); + RecalcUndoSize(); + + m_cbnDefaultSampleFormat.SetItemData(m_cbnDefaultSampleFormat.AddString(_T("FLAC")), dfFLAC); + m_cbnDefaultSampleFormat.SetItemData(m_cbnDefaultSampleFormat.AddString(_T("WAV")), dfWAV); + m_cbnDefaultSampleFormat.SetItemData(m_cbnDefaultSampleFormat.AddString(_T("RAW")), dfRAW); + m_cbnDefaultSampleFormat.SetItemData(m_cbnDefaultSampleFormat.AddString(_T("S3I")), dfS3I); + m_cbnDefaultSampleFormat.SetCurSel(TrackerSettings::Instance().m_defaultSampleFormat); + + CSliderCtrl *slider = static_cast<CSliderCtrl *>(GetDlgItem(IDC_SLIDER1)); + slider->SetRange(0, 8); + slider->SetTicFreq(1); + slider->SetPos(TrackerSettings::Instance().m_FLACCompressionLevel); + + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO1 + TrackerSettings::Instance().sampleEditorKeyBehaviour); + + CheckDlgButton(IDC_COMPRESS_ITI, TrackerSettings::Instance().compressITI ? BST_CHECKED : BST_UNCHECKED); + + m_cbnDefaultVolumeHandling.SetItemData(m_cbnDefaultVolumeHandling.AddString(_T("MIDI volume")), PLUGIN_VOLUMEHANDLING_MIDI); + m_cbnDefaultVolumeHandling.SetItemData(m_cbnDefaultVolumeHandling.AddString(_T("Dry/Wet ratio")), PLUGIN_VOLUMEHANDLING_DRYWET); + m_cbnDefaultVolumeHandling.SetItemData(m_cbnDefaultVolumeHandling.AddString(_T("None")), PLUGIN_VOLUMEHANDLING_IGNORE); + m_cbnDefaultVolumeHandling.SetCurSel(TrackerSettings::Instance().DefaultPlugVolumeHandling); + + CheckDlgButton(IDC_PREVIEW_SAMPLES, TrackerSettings::Instance().previewInFileDialogs ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_NORMALIZE, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CURSORINHEX, TrackerSettings::Instance().cursorPositionInHex ? BST_CHECKED : BST_UNCHECKED); + + return TRUE; +} + + +void COptionsSampleEditor::OnOK() +{ + CPropertyPage::OnOK(); + + TrackerSettings::Instance().m_nFinetuneStep = GetDlgItemInt(IDC_EDIT_FINETUNE); + TrackerSettings::Instance().m_SampleUndoBufferSize = SampleUndoBufferSize(GetDlgItemInt(IDC_EDIT_UNDOSIZE)); + TrackerSettings::Instance().m_defaultSampleFormat = static_cast<SampleEditorDefaultFormat>(m_cbnDefaultSampleFormat.GetItemData(m_cbnDefaultSampleFormat.GetCurSel())); + TrackerSettings::Instance().m_FLACCompressionLevel = static_cast<CSliderCtrl *>(GetDlgItem(IDC_SLIDER1))->GetPos(); + TrackerSettings::Instance().sampleEditorKeyBehaviour = static_cast<SampleEditorKeyBehaviour>(GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3) - IDC_RADIO1); + TrackerSettings::Instance().compressITI = IsDlgButtonChecked(IDC_COMPRESS_ITI) != BST_UNCHECKED; + TrackerSettings::Instance().DefaultPlugVolumeHandling = static_cast<PlugVolumeHandling>(m_cbnDefaultVolumeHandling.GetItemData(m_cbnDefaultVolumeHandling.GetCurSel())); + TrackerSettings::Instance().previewInFileDialogs = IsDlgButtonChecked(IDC_PREVIEW_SAMPLES) != BST_UNCHECKED; + TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad = IsDlgButtonChecked(IDC_NORMALIZE) != BST_UNCHECKED; + TrackerSettings::Instance().cursorPositionInHex = IsDlgButtonChecked(IDC_CURSORINHEX) != BST_UNCHECKED; + + auto docs = theApp.GetOpenDocuments(); + for(auto modDoc : docs) + { + modDoc->GetSampleUndo().RestrictBufferSize(); + } +} + + +BOOL COptionsSampleEditor::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_SAMPLEDITOR; + return CPropertyPage::OnSetActive(); +} + + +void COptionsSampleEditor::OnUndoSizeChanged() +{ + RecalcUndoSize(); + OnSettingsChanged(); +} + + +void COptionsSampleEditor::RecalcUndoSize() +{ + UINT sizePercent = GetDlgItemInt(IDC_EDIT_UNDOSIZE); + uint32 sizeMB = mpt::saturate_cast<uint32>(SampleUndoBufferSize(sizePercent).GetSizeInBytes() >> 20); + CString text = _T("% of physical memory ("); + if(sizePercent) + text.AppendFormat(_T("%u MiB)"), sizeMB); + else + text.Append(_T("disabled)")); + SetDlgItemText(IDC_UNDOSIZE, text); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.h new file mode 100644 index 00000000..2c09f9c4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleConfigDlg.h @@ -0,0 +1,40 @@ +/* + * SampleConfigDlg.h + * ------------------- + * Purpose: Implementation of the sample/instrument editor settings dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class COptionsSampleEditor : public CPropertyPage +{ +protected: + CComboBox m_cbnDefaultSampleFormat, m_cbnDefaultVolumeHandling; + +public: + COptionsSampleEditor() : CPropertyPage(IDD_OPTIONS_SAMPLEEDITOR) { } + +protected: + + BOOL OnInitDialog() override; + void OnOK() override; + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnSetActive() override; + + void RecalcUndoSize(); + + afx_msg void OnHScroll(UINT /*nSBCode*/, UINT /*nPos*/, CScrollBar* /*pScrollBar*/) { OnSettingsChanged(); } + afx_msg void OnSettingsChanged() { SetModified(TRUE); } + afx_msg void OnUndoSizeChanged(); + DECLARE_MESSAGE_MAP(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.cpp new file mode 100644 index 00000000..18885a31 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.cpp @@ -0,0 +1,901 @@ +/* + * SampleEditorDialogs.cpp + * ----------------------- + * Purpose: Code for various dialogs that are used in the sample editor. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "resource.h" +#include "Reporting.h" +#include "MPTrackUtil.h" +#include "Mptrack.h" +#include "../common/misc_util.h" +#include "../soundlib/Snd_defs.h" +#include "../soundlib/ModSample.h" +#include "SampleEditorDialogs.h" +#include "ProgressDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +////////////////////////////////////////////////////////////////////////// +// Sample amplification dialog + +BEGIN_MESSAGE_MAP(CAmpDlg, CDialog) + ON_WM_DESTROY() + ON_EN_CHANGE(IDC_EDIT2, &CAmpDlg::EnableFadeIn) + ON_EN_CHANGE(IDC_EDIT3, &CAmpDlg::EnableFadeOut) +END_MESSAGE_MAP() + +void CAmpDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CAmpDlg) + DDX_Control(pDX, IDC_COMBO1, m_fadeBox); + //}}AFX_DATA_MAP +} + +CAmpDlg::CAmpDlg(CWnd *parent, AmpSettings &settings, int16 factorMin, int16 factorMax) + : CDialog(IDD_SAMPLE_AMPLIFY, parent) + , m_settings(settings) + , m_nFactorMin(factorMin) + , m_nFactorMax(factorMax) +{} + +BOOL CAmpDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + CSpinButtonCtrl *spin = (CSpinButtonCtrl *)GetDlgItem(IDC_SPIN1); + spin->SetRange32(m_nFactorMin, m_nFactorMax); + spin->SetPos32(m_settings.factor); + spin = (CSpinButtonCtrl *)GetDlgItem(IDC_SPIN2); + spin->SetRange32(0, 100); + spin->SetPos32(m_settings.fadeInStart); + spin = (CSpinButtonCtrl *)GetDlgItem(IDC_SPIN3); + spin->SetRange32(0, 100); + spin->SetPos32(m_settings.fadeOutEnd); + + SetDlgItemInt(IDC_EDIT1, m_settings.factor); + SetDlgItemInt(IDC_EDIT2, m_settings.fadeInStart); + SetDlgItemInt(IDC_EDIT3, m_settings.fadeOutEnd); + m_edit.SubclassDlgItem(IDC_EDIT1, this); + m_edit.AllowFractions(false); + m_edit.AllowNegative(m_nFactorMin < 0); + m_editFadeIn.SubclassDlgItem(IDC_EDIT2, this); + m_editFadeIn.AllowFractions(false); + m_editFadeIn.AllowNegative(m_nFactorMin < 0); + m_editFadeOut.SubclassDlgItem(IDC_EDIT3, this); + m_editFadeOut.AllowFractions(false); + m_editFadeOut.AllowNegative(m_nFactorMin < 0); + + const struct + { + const TCHAR *name; + Fade::Law id; + } fadeLaws[] = + { + { _T("Linear"), Fade::kLinear }, + { _T("Exponential"), Fade::kPow }, + { _T("Square Root"), Fade::kSqrt }, + { _T("Logarithmic"), Fade::kLog }, + { _T("Quarter Sine"), Fade::kQuarterSine }, + { _T("Half Sine"), Fade::kHalfSine }, + }; + // Create icons for fade laws + const int cx = Util::ScalePixels(16, m_hWnd); + const int cy = Util::ScalePixels(16, m_hWnd); + const int imgWidth = cx * static_cast<int>(std::size(fadeLaws)); + m_list.Create(cx, cy, ILC_COLOR32 | ILC_MASK, 0, 1); + std::vector<COLORREF> bits(imgWidth * cy, RGB(255, 0, 255)); + const COLORREF col = GetSysColor(COLOR_WINDOWTEXT); + for(int i = 0, baseX = 0; i < static_cast<int>(std::size(fadeLaws)); i++, baseX += cx) + { + Fade::Func fadeFunc = Fade::GetFadeFunc(fadeLaws[i].id); + int oldVal = cy - 1; + for(int x = 0; x < cx; x++) + { + int val = cy - 1 - mpt::saturate_round<int>(cy * fadeFunc(static_cast<double>(x) / cx)); + Limit(val, 0, cy - 1); + if(oldVal > val && x > 0) + { + int dy = (oldVal - val) / 2; + for(int y = oldVal * imgWidth; dy != 0; y -= imgWidth, dy--) + { + bits[baseX + (x - 1) + y] = col; + } + oldVal -= dy + 1; + } + for(int y = oldVal * imgWidth; y >= val * imgWidth; y -= imgWidth) + { + bits[baseX + x + y] = col; + } + oldVal = val; + } + } + CBitmap bitmap; + bitmap.CreateBitmap(cx * static_cast<int>(std::size(fadeLaws)), cy, 1, 32, bits.data()); + m_list.Add(&bitmap, RGB(255, 0, 255)); + bitmap.DeleteObject(); + m_fadeBox.SetImageList(&m_list); + + // Add fade laws to list + COMBOBOXEXITEM cbi; + MemsetZero(cbi); + cbi.mask = CBEIF_IMAGE | CBEIF_SELECTEDIMAGE | CBEIF_TEXT | CBEIF_LPARAM; + for(int i = 0; i < static_cast<int>(std::size(fadeLaws)); i++) + { + cbi.iItem = i; + cbi.pszText = const_cast<LPTSTR>(fadeLaws[i].name); + cbi.iImage = cbi.iSelectedImage = i; + cbi.lParam = fadeLaws[i].id; + m_fadeBox.InsertItem(&cbi); + if(fadeLaws[i].id == m_settings.fadeLaw) m_fadeBox.SetCurSel(i); + } + + m_locked = false; + + return TRUE; +} + + +void CAmpDlg::OnDestroy() +{ + m_list.DeleteImageList(); +} + + +void CAmpDlg::OnOK() +{ + m_settings.factor = static_cast<int16>(Clamp(static_cast<int>(GetDlgItemInt(IDC_EDIT1)), m_nFactorMin, m_nFactorMax)); + m_settings.fadeInStart = Clamp(static_cast<int>(GetDlgItemInt(IDC_EDIT2)), m_nFactorMin, m_nFactorMax); + m_settings.fadeOutEnd = Clamp(static_cast<int>(GetDlgItemInt(IDC_EDIT3)), m_nFactorMin, m_nFactorMax); + m_settings.fadeIn = (IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED); + m_settings.fadeOut = (IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED); + m_settings.fadeLaw = static_cast<Fade::Law>(m_fadeBox.GetItemData(m_fadeBox.GetCurSel())); + CDialog::OnOK(); +} + + +////////////////////////////////////////////////////////////// +// Sample import dialog + +SampleIO CRawSampleDlg::m_format(SampleIO::_8bit, SampleIO::mono, SampleIO::littleEndian, SampleIO::signedPCM); +SmpLength CRawSampleDlg::m_offset = 0; + +BEGIN_MESSAGE_MAP(CRawSampleDlg, CDialog) + ON_COMMAND_RANGE(IDC_RADIO1, IDC_RADIO4, &CRawSampleDlg::OnBitDepthChanged) + ON_COMMAND_RANGE(IDC_RADIO7, IDC_RADIO10, &CRawSampleDlg::OnEncodingChanged) + ON_COMMAND(IDC_BUTTON1, &CRawSampleDlg::OnAutodetectFormat) +END_MESSAGE_MAP() + + +void CRawSampleDlg::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CRawSampleDlg) + DDX_Control(pDX, IDC_SPIN1, m_SpinOffset); + //}}AFX_DATA_MAP +} + + +BOOL CRawSampleDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + if(const auto filename = m_file.GetOptionalFileName(); filename) + { + CString title; + GetWindowText(title); + title += _T(" - ") + filename->GetFullFileName().ToCString(); + SetWindowText(title); + } + m_SpinOffset.SetRange32(0, mpt::saturate_cast<int>(m_file.GetLength() - 1u)); + UpdateDialog(); + return TRUE; +} + + +void CRawSampleDlg::OnOK() +{ + const int bitDepth = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO4); + const int channels = GetCheckedRadioButton(IDC_RADIO5, IDC_RADIO6); + const int encoding = GetCheckedRadioButton(IDC_RADIO7, IDC_RADIO10); + const int endianness = GetCheckedRadioButton(IDC_RADIO11, IDC_RADIO12); + if(bitDepth == IDC_RADIO1) + m_format |= SampleIO::_8bit; + else if(bitDepth == IDC_RADIO2) + m_format |= SampleIO::_16bit; + else if(bitDepth == IDC_RADIO3) + m_format |= SampleIO::_24bit; + else if(bitDepth == IDC_RADIO4) + m_format |= SampleIO::_32bit; + if(channels == IDC_RADIO5) + m_format |= SampleIO::mono; + else if(channels == IDC_RADIO6) + m_format |= SampleIO::stereoInterleaved; + if(encoding == IDC_RADIO7) + m_format |= SampleIO::signedPCM; + else if(encoding == IDC_RADIO8) + m_format |= SampleIO::unsignedPCM; + else if(encoding == IDC_RADIO9) + m_format |= SampleIO::deltaPCM; + else if(encoding == IDC_RADIO10) + m_format |= SampleIO::floatPCM; + if(endianness == IDC_RADIO11) + m_format |= SampleIO::littleEndian; + else if(endianness == IDC_RADIO12) + m_format |= SampleIO::bigEndian; + m_rememberFormat = IsDlgButtonChecked(IDC_CHK_REMEMBERSETTINGS) != BST_UNCHECKED; + m_offset = GetDlgItemInt(IDC_EDIT1, nullptr, FALSE); + CDialog::OnOK(); +} + + +void CRawSampleDlg::UpdateDialog() +{ + const int bitDepthID = IDC_RADIO1 + m_format.GetBitDepth() / 8 - 1; + CheckRadioButton(IDC_RADIO1, IDC_RADIO4, bitDepthID); + CheckRadioButton(IDC_RADIO5, IDC_RADIO6, (m_format.GetChannelFormat() == SampleIO::mono) ? IDC_RADIO5 : IDC_RADIO6); + int encodingID = IDC_RADIO7; + switch(m_format.GetEncoding()) + { + case SampleIO::signedPCM: encodingID = IDC_RADIO7; break; + case SampleIO::unsignedPCM: encodingID = IDC_RADIO8; break; + case SampleIO::deltaPCM: encodingID = IDC_RADIO9; break; + case SampleIO::floatPCM: encodingID = IDC_RADIO10; break; + default: MPT_ASSERT_NOTREACHED(); + } + CheckRadioButton(IDC_RADIO7, IDC_RADIO10, encodingID); + CheckRadioButton(IDC_RADIO11, IDC_RADIO12, (m_format.GetEndianness() == SampleIO::littleEndian) ? IDC_RADIO11 : IDC_RADIO12); + CheckDlgButton(IDC_CHK_REMEMBERSETTINGS, (m_rememberFormat ? BST_CHECKED : BST_UNCHECKED)); + SetDlgItemInt(IDC_EDIT1, m_offset, FALSE); + + OnBitDepthChanged(bitDepthID); + OnEncodingChanged(encodingID); +} + + +void CRawSampleDlg::OnBitDepthChanged(UINT id) +{ + const auto bits = (id - IDC_RADIO1 + 1) * 8; + // 8-bit: endianness doesn't matter + BOOL enableEndianness = (bits == 8) ? FALSE : TRUE; + GetDlgItem(IDC_RADIO11)->EnableWindow(enableEndianness); + GetDlgItem(IDC_RADIO12)->EnableWindow(enableEndianness); + if(bits == 8) + CheckRadioButton(IDC_RADIO11, IDC_RADIO12, IDC_RADIO11); + + const BOOL hasUnsignedDelta = (bits <= 16) ? TRUE : FALSE; + const BOOL hasFloat = (bits == 32) ? TRUE : FALSE; + + GetDlgItem(IDC_RADIO8)->EnableWindow(hasUnsignedDelta); + GetDlgItem(IDC_RADIO9)->EnableWindow(hasUnsignedDelta); + GetDlgItem(IDC_RADIO10)->EnableWindow(hasFloat); + + const int encoding = GetCheckedRadioButton(IDC_RADIO7, IDC_RADIO10); + if((encoding == IDC_RADIO8 && !hasUnsignedDelta) + || (encoding == IDC_RADIO9 && !hasUnsignedDelta) + || (encoding == IDC_RADIO10 && !hasFloat)) + CheckRadioButton(IDC_RADIO7, IDC_RADIO10, IDC_RADIO7); +} + + +void CRawSampleDlg::OnEncodingChanged(UINT id) +{ + const bool isUnsignedDelta = (id == IDC_RADIO8) || (id == IDC_RADIO9); + const bool isFloat = (id == IDC_RADIO10); + + GetDlgItem(IDC_RADIO1)->EnableWindow(isFloat ? FALSE : TRUE); + GetDlgItem(IDC_RADIO2)->EnableWindow(isFloat ? FALSE : TRUE); + GetDlgItem(IDC_RADIO3)->EnableWindow((isFloat || isUnsignedDelta) ? FALSE : TRUE); + GetDlgItem(IDC_RADIO4)->EnableWindow(isUnsignedDelta ? FALSE : TRUE); + + const int bitDepth = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO4); + if(bitDepth != IDC_RADIO4 && isFloat) + CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO4); + if((bitDepth == IDC_RADIO3 || bitDepth == IDC_RADIO4) && isUnsignedDelta) + CheckRadioButton(IDC_RADIO1, IDC_RADIO4, IDC_RADIO1); +} + + +class AutodetectFormatDlg : public CProgressDialog +{ + CRawSampleDlg &m_parent; + +public: + SampleIO m_bestFormat; + + AutodetectFormatDlg(CRawSampleDlg &parent) + : CProgressDialog(&parent) + , m_parent(parent) {} + + void Run() override + { + // Probed raw formats... little-endian and stereo versions are automatically checked as well. + static constexpr SampleIO ProbeFormats[] = + { + // 8-Bit + {SampleIO::_8bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM}, + {SampleIO::_8bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::unsignedPCM}, + {SampleIO::_8bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::deltaPCM}, + // 16-Bit + {SampleIO::_16bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM}, + {SampleIO::_16bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::unsignedPCM}, + {SampleIO::_16bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::deltaPCM}, + // 24-Bit + {SampleIO::_24bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM}, + // 32-Bit + {SampleIO::_32bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::signedPCM}, + {SampleIO::_32bit, SampleIO::mono, SampleIO::bigEndian, SampleIO::floatPCM}, + }; + + SetTitle(_T("Raw Import")); + SetText(_T("Determining raw format...")); + SetRange(0, std::size(ProbeFormats) * 4); + + double bestError = DBL_MAX; + uint64 progress = 0; + + for(SampleIO format : ProbeFormats) + { + for(const auto endianness : {SampleIO::littleEndian, SampleIO::bigEndian}) + { + if(endianness == SampleIO::bigEndian && format.GetBitDepth() == SampleIO::_8bit) + continue; + format |= endianness; + for(const auto channels : {SampleIO::mono, SampleIO::stereoInterleaved}) + { + format |= channels; + + ModSample sample; + m_parent.m_file.Seek(m_parent.m_offset); + const auto bytesPerSample = format.GetNumChannels() * format.GetBitDepth() / 8u; + sample.nLength = mpt::saturate_cast<SmpLength>(m_parent.m_file.BytesLeft() / bytesPerSample); + if(!format.ReadSample(sample, m_parent.m_file)) + continue; + + const uint8 numChannels = sample.GetNumChannels(); + double error = 0.0; + for(uint8 chn = 0; chn < numChannels; chn++) + { + const auto ComputeSampleError = [](auto *v, SmpLength length, uint8 numChannels) + { + const double factor = 1.0 / (1u << (sizeof(*v) * 8u - 1u)); + double error = 0.0; + int32 prev = 0; + for(SmpLength i = length; i != 0; i--, v += numChannels) + { + auto diff = (*v - prev) * factor; + error += diff * diff; + prev = *v; + } + return error; + }; + + if(sample.uFlags[CHN_16BIT]) + error += ComputeSampleError(sample.sample16() + chn, sample.nLength, numChannels); + else + error += ComputeSampleError(sample.sample8() + chn, sample.nLength, numChannels); + } + sample.FreeSample(); + + double errorFactor = format.GetBitDepth() * format.GetBitDepth() / 64; + // Delta PCM often produces slightly worse error compared to signed PCM for real delta samples, so give it a bit of an advantage. + if(format.GetEncoding() == SampleIO::deltaPCM) + errorFactor *= 0.75; + + error *= errorFactor; + + if(error < bestError) + { + bestError = error; + m_bestFormat = format; + } + + SetProgress(++progress); + ProcessMessages(); + if(m_abort) + { + EndDialog(IDCANCEL); + return; + } + } + } + } + + EndDialog(IDOK); + } +}; + + +void CRawSampleDlg::OnAutodetectFormat() +{ + m_offset = GetDlgItemInt(IDC_EDIT1, nullptr, FALSE); + AutodetectFormatDlg dlg(*this); + if(dlg.DoModal() == IDOK) + { + m_format = dlg.m_bestFormat; + UpdateDialog(); + } +} + + +///////////////////////////////////////////////////////////////////////// +// Add silence / resize sample dialog + +BEGIN_MESSAGE_MAP(AddSilenceDlg, CDialog) + ON_CBN_SELCHANGE(IDC_COMBO1, &AddSilenceDlg::OnUnitChanged) + ON_COMMAND(IDC_RADIO_ADDSILENCE_BEGIN, &AddSilenceDlg::OnEditModeChanged) + ON_COMMAND(IDC_RADIO_ADDSILENCE_END, &AddSilenceDlg::OnEditModeChanged) + ON_COMMAND(IDC_RADIO_RESIZETO, &AddSilenceDlg::OnEditModeChanged) + ON_COMMAND(IDC_RADIO1, &AddSilenceDlg::OnEditModeChanged) +END_MESSAGE_MAP() + +SmpLength AddSilenceDlg::m_addSamples = 32; +SmpLength AddSilenceDlg::m_createSamples = 64; + +AddSilenceDlg::AddSilenceDlg(CWnd *parent, SmpLength origLength, uint32 sampleRate, bool allowOPL) + : CDialog(IDD_ADDSILENCE, parent) + , m_numSamples(m_addSamples) + , m_sampleRate(sampleRate) + , m_allowOPL(allowOPL) +{ + if(origLength > 0) + { + m_length = origLength; + m_editOption = kSilenceAtEnd; + } else + { + m_length = m_createSamples; + m_editOption = kResize; + } +} + + +BOOL AddSilenceDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + CSpinButtonCtrl *spin = (CSpinButtonCtrl *)GetDlgItem(IDC_SPIN_ADDSILENCE); + if(spin) + { + spin->SetRange32(0, int32_max); + spin->SetPos32(m_numSamples); + } + + CComboBox *box = (CComboBox *)GetDlgItem(IDC_COMBO1); + if(box) + { + box->AddString(_T("samples")); + box->AddString(_T("ms")); + box->SetCurSel(m_unit); + if(m_sampleRate == 0) + { + // Can't do any conversions if samplerate is unknown + box->EnableWindow(FALSE); + } + } + + int buttonID = IDC_RADIO_ADDSILENCE_END; + switch(m_editOption) + { + case kSilenceAtBeginning: buttonID = IDC_RADIO_ADDSILENCE_BEGIN; break; + case kSilenceAtEnd: buttonID = IDC_RADIO_ADDSILENCE_END; break; + case kResize: buttonID = IDC_RADIO_RESIZETO; break; + } + CheckDlgButton(buttonID, BST_CHECKED); + + SetDlgItemInt(IDC_EDIT_ADDSILENCE, (m_editOption == kResize) ? m_length : m_numSamples, FALSE); + GetDlgItem(IDC_RADIO1)->EnableWindow(m_allowOPL ? TRUE : FALSE); + + return TRUE; +} + + +void AddSilenceDlg::OnOK() +{ + m_numSamples = GetDlgItemInt(IDC_EDIT_ADDSILENCE, nullptr, FALSE); + if(m_unit == kMilliseconds) + { + m_numSamples = Util::muldivr_unsigned(m_numSamples, m_sampleRate, 1000); + } + switch(m_editOption = GetEditMode()) + { + case kSilenceAtBeginning: + case kSilenceAtEnd: + m_addSamples = m_numSamples; + break; + case kResize: + m_createSamples = m_numSamples; + break; + } + CDialog::OnOK(); +} + + +void AddSilenceDlg::OnEditModeChanged() +{ + AddSilenceOptions newEditOption = GetEditMode(); + GetDlgItem(IDC_EDIT_ADDSILENCE)->EnableWindow((newEditOption == kOPLInstrument) ? FALSE : TRUE); + if(newEditOption != kResize && m_editOption == kResize) + { + // Switch to "add silence" + m_length = GetDlgItemInt(IDC_EDIT_ADDSILENCE); + SetDlgItemInt(IDC_EDIT_ADDSILENCE, m_numSamples); + } else if(newEditOption == kResize && m_editOption != kResize) + { + // Switch to "resize" + m_numSamples = GetDlgItemInt(IDC_EDIT_ADDSILENCE); + SetDlgItemInt(IDC_EDIT_ADDSILENCE, m_length); + } + m_editOption = newEditOption; +} + + +void AddSilenceDlg::OnUnitChanged() +{ + const auto unit = static_cast<Unit>(static_cast<CComboBox*>(GetDlgItem(IDC_COMBO1))->GetCurSel()); + if(m_unit == unit) + return; + + m_unit = unit; + SmpLength duration = GetDlgItemInt(IDC_EDIT_ADDSILENCE); + if(m_unit == kSamples) + { + // Convert from milliseconds + duration = Util::muldivr_unsigned(duration, m_sampleRate, 1000); + } else + { + // Convert from samples + duration = Util::muldivr_unsigned(duration, 1000, m_sampleRate); + } + SetDlgItemInt(IDC_EDIT_ADDSILENCE, duration); +} + + +AddSilenceDlg::AddSilenceOptions AddSilenceDlg::GetEditMode() const +{ + if(IsDlgButtonChecked(IDC_RADIO_ADDSILENCE_BEGIN)) return kSilenceAtBeginning; + else if(IsDlgButtonChecked(IDC_RADIO_ADDSILENCE_END)) return kSilenceAtEnd; + else if(IsDlgButtonChecked(IDC_RADIO_RESIZETO)) return kResize; + else if(IsDlgButtonChecked(IDC_RADIO1)) return kOPLInstrument; + MPT_ASSERT_NOTREACHED(); + return kSilenceAtEnd; +} + + +///////////////////////////////////////////////////////////////////////// +// Sample grid dialog + +void CSampleGridDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CSampleGridDlg) + DDX_Control(pDX, IDC_EDIT1, m_EditSegments); + DDX_Control(pDX, IDC_SPIN1, m_SpinSegments); + //}}AFX_DATA_MAP +} + + +BOOL CSampleGridDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + m_SpinSegments.SetRange32(0, m_nMaxSegments); + m_SpinSegments.SetPos(m_nSegments); + SetDlgItemInt(IDC_EDIT1, m_nSegments, FALSE); + GetDlgItem(IDC_EDIT1)->SetFocus(); + return TRUE; +} + + +void CSampleGridDlg::OnOK() +{ + m_nSegments = GetDlgItemInt(IDC_EDIT1, NULL, FALSE); + CDialog::OnOK(); +} + + +///////////////////////////////////////////////////////////////////////// +// Sample cross-fade dialog + +uint32 CSampleXFadeDlg::m_fadeLength = 20000; +uint32 CSampleXFadeDlg::m_fadeLaw = 50000; +bool CSampleXFadeDlg::m_afterloopFade = true; +bool CSampleXFadeDlg::m_useSustainLoop = false; + +BEGIN_MESSAGE_MAP(CSampleXFadeDlg, CDialog) + ON_WM_HSCROLL() + ON_COMMAND(IDC_RADIO1, &CSampleXFadeDlg::OnLoopTypeChanged) + ON_COMMAND(IDC_RADIO2, &CSampleXFadeDlg::OnLoopTypeChanged) + ON_EN_CHANGE(IDC_EDIT1, &CSampleXFadeDlg::OnFadeLengthChanged) + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CSampleXFadeDlg::OnToolTipText) +END_MESSAGE_MAP() + + +void CSampleXFadeDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CSampleGridDlg) + DDX_Control(pDX, IDC_EDIT1, m_EditSamples); + DDX_Control(pDX, IDC_SPIN1, m_SpinSamples); + DDX_Control(pDX, IDC_SLIDER1, m_SliderLength); + DDX_Control(pDX, IDC_SLIDER2, m_SliderFadeLaw); + DDX_Control(pDX, IDC_RADIO1, m_RadioNormalLoop); + DDX_Control(pDX, IDC_RADIO2, m_RadioSustainLoop); + //}}AFX_DATA_MAP +} + + +BOOL CSampleXFadeDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + const bool hasNormal = m_sample.uFlags[CHN_LOOP] && m_sample.nLoopStart > 0; + const bool hasSustain = m_sample.uFlags[CHN_SUSTAINLOOP] && m_sample.nSustainStart > 0; + const bool hasBothLoops = hasNormal && hasSustain; + m_RadioNormalLoop.EnableWindow(hasBothLoops); + m_RadioSustainLoop.EnableWindow(hasBothLoops); + CheckRadioButton(IDC_RADIO1, IDC_RADIO2, ((m_useSustainLoop && hasSustain) || !hasNormal) ? IDC_RADIO2 : IDC_RADIO1); + + m_SliderLength.SetRange(0, 100000); + m_SliderLength.SetPos(m_fadeLength); + m_SliderFadeLaw.SetRange(0, 100000); + m_SliderFadeLaw.SetPos(m_fadeLaw); + + OnLoopTypeChanged(); + + return TRUE; +} + + +void CSampleXFadeDlg::OnOK() +{ + m_fadeLength = m_SliderLength.GetPos(); + m_fadeLaw = m_SliderFadeLaw.GetPos(); + m_afterloopFade = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + m_useSustainLoop = IsDlgButtonChecked(IDC_RADIO2) != BST_UNCHECKED; + Limit(m_fadeLength, uint32(0), uint32(100000)); + CDialog::OnOK(); +} + + +void CSampleXFadeDlg::OnLoopTypeChanged() +{ + SmpLength loopStart = m_sample.nLoopStart, loopEnd = m_sample.nLoopEnd; + if(IsDlgButtonChecked(IDC_RADIO2)) + { + loopStart = m_sample.nSustainStart; + loopEnd = m_sample.nSustainEnd; + } + m_maxLength = std::min({ m_sample.nLength, loopStart, loopEnd / 2u }); + m_loopLength = loopEnd - loopStart; + + m_editLocked = true; + m_SpinSamples.SetRange32(0, std::min(m_loopLength, m_maxLength)); + GetDlgItem(IDC_EDIT1)->SetFocus(); + CheckDlgButton(IDC_CHECK1, m_afterloopFade ? BST_CHECKED : BST_UNCHECKED); + + SmpLength numSamples = PercentToSamples(m_SliderLength.GetPos()); + numSamples = std::min({ numSamples, m_loopLength, m_maxLength }); + m_SpinSamples.SetPos(numSamples); + SetDlgItemInt(IDC_EDIT1, numSamples, FALSE); + + m_editLocked = false; +} + + +void CSampleXFadeDlg::OnFadeLengthChanged() +{ + if(m_editLocked) return; + SmpLength numSamples = GetDlgItemInt(IDC_EDIT1, NULL, FALSE); + numSamples = std::min({ numSamples, m_loopLength, m_maxLength }); + m_SliderLength.SetPos(SamplesToPercent(numSamples)); +} + + +void CSampleXFadeDlg::OnHScroll(UINT, UINT, CScrollBar *sb) +{ + if(sb == (CScrollBar *)(&m_SliderLength)) + { + m_editLocked = true; + SmpLength numSamples = PercentToSamples(m_SliderLength.GetPos()); + if(numSamples > m_maxLength) + { + numSamples = m_maxLength; + m_SliderLength.SetPos(SamplesToPercent(numSamples)); + } + m_SpinSamples.SetPos(numSamples); + SetDlgItemInt(IDC_EDIT1, numSamples, FALSE); + m_editLocked = false; + } +} + + +BOOL CSampleXFadeDlg::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT *)pNMHDR; + UINT_PTR nID = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = (UINT_PTR)::GetDlgCtrlID((HWND)nID); + } + switch(nID) + { + case IDC_SLIDER1: + { + uint32 percent = m_SliderLength.GetPos(); + wsprintf(pTTT->szText, _T("%u.%03u%% of the loop (%u samples)"), percent / 1000, percent % 1000, PercentToSamples(percent)); + } + break; + case IDC_SLIDER2: + _tcscpy(pTTT->szText, _T("Slide towards constant power for fixing badly looped samples.")); + break; + default: + return FALSE; + } + *pResult = 0; + + // bring the tooltip window above other popup windows + ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, + SWP_NOACTIVATE|SWP_NOSIZE|SWP_NOMOVE|SWP_NOOWNERZORDER); + return TRUE; +} + + +///////////////////////////////////////////////////////////////////////// +// Resampling dialog + +CResamplingDlg::ResamplingOption CResamplingDlg::m_lastChoice = CResamplingDlg::Upsample; +uint32 CResamplingDlg::m_lastFrequency = 0; +bool CResamplingDlg::m_updatePatterns = false; + +BEGIN_MESSAGE_MAP(CResamplingDlg, CDialog) + ON_EN_SETFOCUS(IDC_EDIT1, &CResamplingDlg::OnFocusEdit) +END_MESSAGE_MAP() + +BOOL CResamplingDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetWindowText(m_resampleAll ? _T("Resample All") : _T("Resample")); + + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO1 + m_lastChoice); + if(m_frequency > 0) + { + TCHAR s[32]; + wsprintf(s, _T("&Upsample (%u Hz)"), m_frequency * 2); + SetDlgItemText(IDC_RADIO1, s); + wsprintf(s, _T("&Downsample (%u Hz)"), m_frequency / 2); + SetDlgItemText(IDC_RADIO2, s); + + if(!m_lastFrequency) + m_lastFrequency = m_frequency; + } + if(!m_lastFrequency) + m_lastFrequency = 48000; + + + SetDlgItemInt(IDC_EDIT1, m_lastFrequency, FALSE); + CSpinButtonCtrl *spin = static_cast<CSpinButtonCtrl *>(GetDlgItem(IDC_SPIN1)); + spin->SetRange32(1, 999999); + spin->SetPos32(m_lastFrequency); + + CComboBox *cbnResampling = static_cast<CComboBox *>(GetDlgItem(IDC_COMBO_FILTER)); + cbnResampling->SetRedraw(FALSE); + const auto resamplingModes = Resampling::AllModesWithDefault(); + for(auto mode : resamplingModes) + { + CString desc = _T("r8brain (High Quality)"); + if(mode != SRCMODE_DEFAULT) + desc = CTrackApp::GetResamplingModeName(mode, 1, true); + + int index = cbnResampling->AddString(desc); + cbnResampling->SetItemData(index, mode); + if(m_srcMode == mode) + cbnResampling->SetCurSel(index); + } + cbnResampling->SetRedraw(TRUE); + + CheckDlgButton(IDC_CHECK1, m_updatePatterns ? BST_CHECKED : BST_UNCHECKED); + + return TRUE; +} + + +void CResamplingDlg::OnOK() +{ + const int choice = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3); + if(choice == IDC_RADIO1) + { + m_lastChoice = Upsample; + m_frequency *= 2; + } else if(choice == IDC_RADIO2) + { + m_lastChoice = Downsample; + m_frequency /= 2; + } else + { + m_lastChoice = Custom; + uint32 newFrequency = GetDlgItemInt(IDC_EDIT1, NULL, FALSE); + if(newFrequency > 0) + { + m_lastFrequency = m_frequency = newFrequency; + } else + { + MessageBeep(MB_ICONWARNING); + GetDlgItem(IDC_EDIT1)->SetFocus(); + return; + } + } + + CComboBox *cbnResampling = static_cast<CComboBox *>(GetDlgItem(IDC_COMBO_FILTER)); + m_srcMode = static_cast<ResamplingMode>(cbnResampling->GetItemData(cbnResampling->GetCurSel())); + + m_updatePatterns = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + + CDialog::OnOK(); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Sample mix dialog + +SmpLength CMixSampleDlg::sampleOffset = 0; +int CMixSampleDlg::amplifyOriginal = 50; +int CMixSampleDlg::amplifyMix = 50; + + +void CMixSampleDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CMixSampleDlg) + DDX_Control(pDX, IDC_EDIT_OFFSET, m_EditOffset); + DDX_Control(pDX, IDC_SPIN_OFFSET, m_SpinOffset); + DDX_Control(pDX, IDC_SPIN_SAMPVOL1, m_SpinVolOriginal); + DDX_Control(pDX, IDC_SPIN_SAMPVOL2, m_SpinVolMix); + //}}AFX_DATA_MAP +} + + +CMixSampleDlg::CMixSampleDlg(CWnd *parent) + : CDialog(IDD_MIXSAMPLES, parent) +{ } + + +BOOL CMixSampleDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + // Offset + m_SpinOffset.SetRange32(0, MAX_SAMPLE_LENGTH); + SetDlgItemInt(IDC_EDIT_OFFSET, sampleOffset); + + // Volumes + m_SpinVolOriginal.SetRange(-10000, 10000); + m_SpinVolMix.SetRange(-10000, 10000); + + m_EditVolOriginal.SubclassDlgItem(IDC_EDIT_SAMPVOL1, this); + m_EditVolOriginal.AllowNegative(true); + m_EditVolMix.SubclassDlgItem(IDC_EDIT_SAMPVOL2, this); + m_EditVolMix.AllowNegative(true); + + SetDlgItemInt(IDC_EDIT_SAMPVOL1, amplifyOriginal); + SetDlgItemInt(IDC_EDIT_SAMPVOL2, amplifyMix); + + return TRUE; +} + + +void CMixSampleDlg::OnOK() +{ + CDialog::OnOK(); + sampleOffset = Clamp<SmpLength, SmpLength>(GetDlgItemInt(IDC_EDIT_OFFSET), 0, MAX_SAMPLE_LENGTH); + amplifyOriginal = Clamp<int, int>(GetDlgItemInt(IDC_EDIT_SAMPVOL1), -10000, 10000); + amplifyMix = Clamp<int, int>(GetDlgItemInt(IDC_EDIT_SAMPVOL2), -10000, 10000); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.h b/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.h new file mode 100644 index 00000000..32f6c9ef --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleEditorDialogs.h @@ -0,0 +1,278 @@ +/* + * SampleEditorDialogs.h + * --------------------- + * Purpose: Code for various dialogs that are used in the sample editor. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/FileReaderFwd.h" +#include "../soundlib/SampleIO.h" +#include "../tracklib/FadeLaws.h" +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +////////////////////////////////////////////////////////////////////////// +// Sample amplification dialog + +class CAmpDlg: public CDialog +{ +public: + struct AmpSettings + { + Fade::Law fadeLaw; + int fadeInStart, fadeOutEnd; + int16 factor; + bool fadeIn, fadeOut; + }; + + AmpSettings &m_settings; + int16 m_nFactorMin, m_nFactorMax; + +protected: + CComboBoxEx m_fadeBox; + CImageList m_list; + CNumberEdit m_edit, m_editFadeIn, m_editFadeOut; + bool m_locked = true; + +public: + CAmpDlg(CWnd *parent, AmpSettings &settings, int16 factorMin = int16_min, int16 factorMax = int16_max); + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + void OnDestroy(); + + afx_msg void EnableFadeIn() { if(!m_locked) CheckDlgButton(IDC_CHECK1, BST_CHECKED); } + afx_msg void EnableFadeOut() { if(!m_locked) CheckDlgButton(IDC_CHECK2, BST_CHECKED); } + + DECLARE_MESSAGE_MAP() +}; + + +////////////////////////////////////////////////////////////////////////// +// Sample import dialog + +class CRawSampleDlg: public CDialog +{ + friend class AutodetectFormatDlg; + +protected: + static SampleIO m_format; + static SmpLength m_offset; + + CSpinButtonCtrl m_SpinOffset; + FileReader &m_file; + bool m_rememberFormat = false; + +public: + SampleIO GetSampleFormat() const { return m_format; } + void SetSampleFormat(SampleIO nFormat) { m_format = nFormat; } + + bool GetRemeberFormat() const { return m_rememberFormat; }; + void SetRememberFormat(bool remember) { m_rememberFormat = remember; }; + + SmpLength GetOffset() const { return m_offset; } + void SetOffset(SmpLength offset) { m_offset = offset; } + +public: + CRawSampleDlg(FileReader &file, CWnd *parent = nullptr) + : CDialog(IDD_LOADRAWSAMPLE, parent) + , m_file(file) {} + +protected: + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + void UpdateDialog(); + + void OnBitDepthChanged(UINT id); + void OnEncodingChanged(UINT id); + void OnAutodetectFormat(); + + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Add silence dialog - add silence to a sample + +class AddSilenceDlg: public CDialog +{ +public: + enum AddSilenceOptions + { + kSilenceAtBeginning, // Add at beginning of sample + kSilenceAtEnd, // Add at end of sample + kResize, // Resize sample + kOPLInstrument, // Initialize as OPL instrument + }; + + enum Unit + { + kSamples = 0, + kMilliseconds, + }; + + SmpLength m_numSamples; // Add x samples (also containes the return value in all cases) + SmpLength m_length; // Set size to x samples (init value: current sample size) + AddSilenceOptions m_editOption; // See above + +protected: + static SmpLength m_addSamples; + static SmpLength m_createSamples; + uint32 m_sampleRate; + Unit m_unit = kSamples; + bool m_allowOPL; + +public: + AddSilenceDlg(CWnd *parent, SmpLength origLength, uint32 sampleRate, bool allowOPL); + + BOOL OnInitDialog() override; + void OnOK() override; + +protected: + AddSilenceOptions GetEditMode() const; + afx_msg void OnEditModeChanged(); + afx_msg void OnUnitChanged(); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Sample grid dialog + +class CSampleGridDlg: public CDialog +{ +public: + SmpLength m_nSegments, m_nMaxSegments; + +protected: + CEdit m_EditSegments; + CSpinButtonCtrl m_SpinSegments; + +public: + CSampleGridDlg(CWnd *parent, SmpLength nSegments, SmpLength nMaxSegments) : CDialog(IDD_SAMPLE_GRID_SIZE, parent) { m_nSegments = nSegments; m_nMaxSegments = nMaxSegments; }; + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; +}; + + +///////////////////////////////////////////////////////////////////////// +// Sample cross-fade dialog + +class CSampleXFadeDlg: public CDialog +{ +public: + static uint32 m_fadeLength; + static uint32 m_fadeLaw; + static bool m_afterloopFade; + static bool m_useSustainLoop; + SmpLength m_loopLength = 0, m_maxLength = 0; + +protected: + CSliderCtrl m_SliderLength, m_SliderFadeLaw; + CEdit m_EditSamples; + CSpinButtonCtrl m_SpinSamples; + CButton m_RadioNormalLoop, m_RadioSustainLoop; + ModSample &m_sample; + bool m_editLocked = true; + +public: + CSampleXFadeDlg(CWnd *parent, ModSample &sample) + : CDialog(IDD_SAMPLE_XFADE, parent) + , m_sample(sample) {} + + SmpLength PercentToSamples(uint32 percent) const { return Util::muldivr_unsigned(percent, m_loopLength, 100000); } + uint32 SamplesToPercent(SmpLength samples) const { return Util::muldivr_unsigned(samples, 100000, m_loopLength); } + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + + afx_msg void OnLoopTypeChanged(); + afx_msg void OnFadeLengthChanged(); + afx_msg void OnHScroll(UINT, UINT, CScrollBar *); + afx_msg BOOL OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Resampling dialog + +class CResamplingDlg: public CDialog +{ +public: + enum ResamplingOption + { + Upsample, + Downsample, + Custom + }; + +protected: + ResamplingMode m_srcMode; + uint32 m_frequency; + bool m_resampleAll; + static uint32 m_lastFrequency; + static ResamplingOption m_lastChoice; + static bool m_updatePatterns; + +public: + CResamplingDlg(CWnd *parent, uint32 frequency, ResamplingMode srcMode, bool resampleAll) : CDialog(IDD_RESAMPLE, parent), m_srcMode(srcMode), m_frequency(frequency), m_resampleAll(resampleAll) { }; + uint32 GetFrequency() const { return m_frequency; } + ResamplingMode GetFilter() const { return m_srcMode; } + static ResamplingOption GetResamplingOption() { return m_lastChoice; } + static bool UpdatePatternCommands() { return m_updatePatterns; } + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + + afx_msg void OnFocusEdit() { CheckRadioButton(IDC_RADIO1, IDC_RADIO3, IDC_RADIO3); } + + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Sample mix dialog + +class CMixSampleDlg: public CDialog +{ +protected: + // Dialog controls + CEdit m_EditOffset; + CNumberEdit m_EditVolOriginal, m_EditVolMix; + CSpinButtonCtrl m_SpinOffset, m_SpinVolOriginal, m_SpinVolMix; + +public: + static SmpLength sampleOffset; + static int amplifyOriginal; + static int amplifyMix; + +public: + CMixSampleDlg(CWnd *parent); + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.cpp new file mode 100644 index 00000000..83c8a999 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.cpp @@ -0,0 +1,737 @@ +/* + * SampleGenerator.cpp + * ------------------- + * Purpose: Generate samples from math formulas using muParser + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#if MPT_DISABLED_CODE + +#include "SampleGenerator.h" +#include "modsmp_ctrl.h" + +int CSampleGenerator::sample_frequency = 44100; +int CSampleGenerator::sample_length = CSampleGenerator::sample_frequency; +mu::string_type CSampleGenerator::expression = _T("sin(xp * _pi)"); +smpgen_clip_methods CSampleGenerator::sample_clipping = smpgen_normalize; + +mu::value_type *CSampleGenerator::sample_buffer = nullptr; +size_t CSampleGenerator::samples_written = 0; + + +CSampleGenerator::CSampleGenerator() +{ + + // Setup function callbacks + muParser.DefineFun(_T("clip"), &ClipCallback, false); + muParser.DefineFun(_T("pwm"), &PWMCallback, false); + muParser.DefineFun(_T("rnd"), &RndCallback, false); + muParser.DefineFun(_T("smp"), &SampleDataCallback, false); + muParser.DefineFun(_T("tri"), &TriangleCallback, false); + + // Setup binary operator callbacks + muParser.DefineOprt(_T("mod"), &ModuloCallback, 0); + + //muParser.DefineConst("pi", (mu::value_type)PARSER_CONST_PI); + +} + + +// Open the smpgen dialog +bool CSampleGenerator::ShowDialog() +{ + bool isDone = false, result = false; + while(!isDone) + { + CSmpGenDialog dlg(sample_frequency, sample_length, sample_clipping, expression); + dlg.DoModal(); + + // pressed "OK" button? + if(dlg.CanApply()) + { + sample_frequency = dlg.GetFrequency(); + sample_length = dlg.GetLength(); + sample_clipping = dlg.GetClipping(); + expression = dlg.GetExpression(); + isDone = CanRenderSample(); + if(isDone) isDone = TestExpression(); // show dialog again if the formula can't be parsed. + result = true; + } else + { + isDone = true; // just quit. + result = false; + } + } + return result; +} + + +// Check if the currently select expression can be parsed by muParser. +bool CSampleGenerator::TestExpression() +{ + // reset helper variables + samples_written = 0; + sample_buffer = nullptr; + + muParser.SetExpr(expression); + mu::value_type x = 0; + muParser.DefineVar(_T("x"), &x); + muParser.DefineVar(_T("xp"), &x); + muParser.DefineVar(_T("len"), &x); + muParser.DefineVar(_T("lens"), &x); + muParser.DefineVar(_T("freq"), &x); + + try + { + muParser.Eval(); + } + catch (mu::Parser::exception_type &e) + { + ShowError(&e); + return false; + } + return true; +} + + +// Check if sample parameters are valid. +bool CSampleGenerator::CanRenderSample() const +{ + if(sample_frequency < SMPGEN_MINFREQ || sample_frequency > SMPGEN_MAXFREQ || sample_length < SMPGEN_MINLENGTH || sample_length > SMPGEN_MAXLENGTH) return false; + return true; +} + + +// Actual render loop. +bool CSampleGenerator::RenderSample(CSoundFile *pSndFile, SAMPLEINDEX nSample) +{ + if(!CanRenderSample() || !TestExpression() || (pSndFile == nullptr) || (nSample < 1) || (nSample > pSndFile->m_nSamples)) return false; + + // allocate a new buffer + sample_buffer = (mu::value_type *)malloc(sample_length * sizeof(mu::value_type)); + if(sample_buffer == nullptr) return false; + memset(sample_buffer, 0, sample_length * sizeof(mu::value_type)); + + mu::value_type x = 0, xp = 0; + mu::value_type v_len = sample_length, v_freq = sample_frequency, v_lens = v_len / v_freq; + muParser.DefineVar(_T("x"), &x); + muParser.DefineVar(_T("xp"), &xp); + muParser.DefineVar(_T("len"), &v_len); + muParser.DefineVar(_T("lens"), &v_lens); + muParser.DefineVar(_T("freq"), &v_freq); + + bool success = true; + mu::value_type minmax = 0; + + for(size_t i = 0; i < (size_t)sample_length; i++) + { + samples_written = i; + x = (mu::value_type)i; + xp = x * 100 / sample_length; + + try + { + sample_buffer[i] = muParser.Eval(); + } + catch (mu::Parser::exception_type &e) + { + // let's just ignore div by zero errors (note: this error code is currently unused (muParser 1.30)) + if(e.GetCode() != mu::ecDIV_BY_ZERO) + { + ShowError(&e); + success = false; + break; + } + sample_buffer[i] = 0; + } + // new maximum value? + if(std::abs(sample_buffer[i]) > minmax) minmax = std::abs(sample_buffer[i]); + + } + + if(success) + { + MODSAMPLE *pModSample = &pSndFile->Samples[nSample]; + + BEGIN_CRITICAL(); + + // first, save some memory... (leads to crashes) + //CSoundFile::FreeSample(pModSample->pSample); + //pModSample->pSample = nullptr; + + if(minmax == 0) minmax = 1; // avoid division by 0 + + // convert sample to 16-bit (or whateve rhas been specified) + int16 *pSample = (sampling_type *)CSoundFile::AllocateSample((sample_length + 4) * SMPGEN_MIXBYTES); + for(size_t i = 0; i < (size_t)sample_length; i++) + { + switch(sample_clipping) + { + case smpgen_clip: sample_buffer[i] = CLAMP(sample_buffer[i], -1, 1); break; // option 1: clip + case smpgen_normalize: sample_buffer[i] /= minmax; break; // option 3: normalize + } + + pSample[i] = (sampling_type)(sample_buffer[i] * sample_maxvalue); + } + + // set new sample proprerties + pModSample->nC5Speed = sample_frequency; + CSoundFile::FrequencyToTranspose(pModSample); + pModSample->uFlags |= CHN_16BIT; // has to be adjusted if SMPGEN_MIXBYTES changes! + pModSample->uFlags &= ~(CHN_STEREO|CHN_SUSTAINLOOP|CHN_PINGPONGSUSTAIN); + pModSample->nLoopStart = 0; + pModSample->nLoopEnd = sample_length; + pModSample->nSustainStart = pModSample->nSustainEnd = 0; + if(sample_length / sample_frequency < 5) // arbitrary limit for automatic sample loop (5 seconds) + pModSample->uFlags |= CHN_LOOP; + else + pModSample->uFlags &= ~(CHN_LOOP|CHN_PINGPONGLOOP); + + ctrlSmp::ReplaceSample(*pModSample, (LPSTR)pSample, sample_length, pSndFile); + + END_CRITICAL(); + } + + free(sample_buffer); + sample_buffer = nullptr; + + return success; +} + + +// Callback function to access sample data +mu::value_type CSampleGenerator::SampleDataCallback(mu::value_type v) +{ + if(sample_buffer == nullptr) return 0; + v = CLAMP(v, 0, samples_written); + size_t pos = static_cast<size_t>(v); + return sample_buffer[pos]; +} + + +void CSampleGenerator::ShowError(mu::Parser::exception_type *e) +{ + std::string errmsg; + errmsg = "The expression\n " + e->GetExpr() + "\ncontains an error "; + if(!e->GetToken().empty()) + errmsg += "in the token\n " + e->GetToken() + "\n"; + errmsg += "at position " + Stringify(e->GetPos()) + ".\nThe error message was: " + e->GetMsg(); + ::MessageBox(0, errmsg.c_str(), _T("muParser Sample Generator"), 0); +} + + +////////////////////////////////////////////////////////////////////////// +// Sample Generator Dialog implementation + +#define MAX_SAMPLEGEN_EXPRESSIONS 61 + +BEGIN_MESSAGE_MAP(CSmpGenDialog, CDialog) + ON_EN_CHANGE(IDC_EDIT_SAMPLE_LENGTH, &CSmpGenDialog::OnSampleLengthChanged) + ON_EN_CHANGE(IDC_EDIT_SAMPLE_LENGTH_SEC, &CSmpGenDialog::OnSampleSecondsChanged) + ON_EN_CHANGE(IDC_EDIT_SAMPLE_FREQ, &CSmpGenDialog::OnSampleFreqChanged) + ON_EN_CHANGE(IDC_EDIT_FORMULA, &CSmpGenDialog::OnExpressionChanged) + ON_COMMAND(IDC_BUTTON_SHOW_EXPRESSIONS, &CSmpGenDialog::OnShowExpressions) + ON_COMMAND(IDC_BUTTON_SAMPLEGEN_PRESETS, &CSmpGenDialog::OnShowPresets) + ON_COMMAND_RANGE(ID_SAMPLE_GENERATOR_MENU, ID_SAMPLE_GENERATOR_MENU + MAX_SAMPLEGEN_EXPRESSIONS - 1, &CSmpGenDialog::OnInsertExpression) + ON_COMMAND_RANGE(ID_SAMPLE_GENERATOR_PRESET_MENU, ID_SAMPLE_GENERATOR_PRESET_MENU + MAX_SAMPLEGEN_PRESETS + 1, &CSmpGenDialog::OnSelectPreset) +END_MESSAGE_MAP() + + +// List of all possible expression for expression menu +const samplegen_expression menu_descriptions[MAX_SAMPLEGEN_EXPRESSIONS] = +{ + {"Variables", ""}, + {"Current position (sampling point)", "x"}, + {"Current position (percentage)", "xp"}, + {"Sample length", "len"}, + {"Sample length (seconds)", "lens"}, + {"Sampling frequency", "freq"}, + {"Constants", ""}, + {"Pi", "_pi"}, + {"e", "_e"}, + {"Trigonometric functions", ""}, + {"Sine", "sin(x)"}, + {"Cosine", "cos(x)"}, + {"Tangens", "tan(x)"}, + {"Arcus Sine", "asin(x)"}, + {"Arcus Cosine", "acos(x)"}, + {"Arcus Tangens", "atan(x)"}, + {"Hyperbolic Sine", "sinh(x)"}, + {"Hyperbolic Cosine", "cosh(x)"}, + {"Hyperbolic Tangens", "tanh(x)"}, + {"Hyperbolic Arcus Sine", "asinh(x)"}, + {"Hyperbolic Arcus Cosine", "acosh(x)"}, + {"Hyperbolic Arcus Tangens", "atanh(x)"}, + {"Log, Exp, Root", ""}, + {"Logarithm (base 2)", "log2(x)"}, + {"Logarithm (base 10)", "log(x)"}, + {"Natural Logarithm (base e)", "ln(x)"}, + {"e^x", "exp(x)"}, + {"Square Root", "sqrt(x)"}, + {"Sign and rounding", ""}, + {"Sign", "sign(x)"}, + {"Absolute value", "abs(x)"}, + {"Round to nearest integer", "rint(x)"}, + {"Sets", ""}, + {"Minimum", "min(x, y, ...)"}, + {"Maximum", "max(x, y, ...)"}, + {"Sum", "sum(x, y, ...)"}, + {"Mean value", "avg(x, y, ...)"}, + {"Misc functions", ""}, + {"Pulse generator", "pwm(position, duty%, width)"}, + {"Triangle", "tri(position, width)"}, + {"Random value between 0 and x", "rnd(x)"}, + {"Access previous sampling point", "smp(position)"}, + {"Clip between values", "clip(value, minclip, maxclip)"}, + {"If...Then...Else", "if(condition, statement1, statement2)"}, + {"Operators", ""}, + {"Assignment", "x = y"}, + {"Logical And", "x abd y"}, + {"Logical Or", "x or y"}, + {"Logical Xor", "x xor y"}, + {"Less or equal", "x <= y"}, + {"Greater or equal", "x >= y"}, + {"Not equal", "x != y"}, + {"Equal", "x == y"}, + {"Greater than", "x > y"}, + {"Less than", "x < y"}, + {"Addition", "x + y"}, + {"Subtraction", "x - y"}, + {"Multiplication", "x * y"}, + {"Division", "x / y"}, + {"x^y", "x ^ y"}, + {"Modulo", "x mod y"}, +}; + + +BOOL CSmpGenDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + RecalcParameters(false, true); + SetDlgItemText(IDC_EDIT_FORMULA, expression.c_str()); + + int check = IDC_RADIO_SMPCLIP1; + switch(sample_clipping) + { + case smpgen_clip: check = IDC_RADIO_SMPCLIP1; break; + case smpgen_overflow: check = IDC_RADIO_SMPCLIP2; break; + case smpgen_normalize: check = IDC_RADIO_SMPCLIP3; break; + } + CheckRadioButton(IDC_RADIO_SMPCLIP1, IDC_RADIO_SMPCLIP3, check); + + if(presets.GetNumPresets() == 0) + { + CreateDefaultPresets(); + } + + // Create font for "dropdown" button (Marlett system font) + hButtonFont = CreateFont(14, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FF_DONTCARE, TEXT("Marlett")); + ::SendMessage(GetDlgItem(IDC_BUTTON_SHOW_EXPRESSIONS)->m_hWnd, WM_SETFONT, (WPARAM)hButtonFont, MAKELPARAM(TRUE, 0)); + + return TRUE; +} + + +void CSmpGenDialog::OnOK() +{ + CDialog::OnOK(); + apply = true; + + int check = GetCheckedRadioButton(IDC_RADIO_SMPCLIP1, IDC_RADIO_SMPCLIP3); + switch(check) + { + case IDC_RADIO_SMPCLIP1: sample_clipping = smpgen_clip; break; + case IDC_RADIO_SMPCLIP2: sample_clipping = smpgen_overflow; break; + case IDC_RADIO_SMPCLIP3: sample_clipping = smpgen_normalize; break; + } + + DeleteObject(hButtonFont); +} + + +void CSmpGenDialog::OnCancel() +{ + CDialog::OnCancel(); + apply = false; +} + + +// User changed formula +void CSmpGenDialog::OnExpressionChanged() +{ + CString result; + GetDlgItemText(IDC_EDIT_FORMULA, result); + expression = result; +} + + +// User changed sample length field +void CSmpGenDialog::OnSampleLengthChanged() +{ + int temp_length = GetDlgItemInt(IDC_EDIT_SAMPLE_LENGTH); + if(temp_length >= SMPGEN_MINLENGTH && temp_length <= SMPGEN_MAXLENGTH) + { + sample_length = temp_length; + RecalcParameters(false); + } +} + + +// User changed sample length (seconds) field +void CSmpGenDialog::OnSampleSecondsChanged() +{ + CString str; + GetDlgItemText(IDC_EDIT_SAMPLE_LENGTH_SEC, str); + double temp_seconds = atof(str); + if(temp_seconds > 0) + { + sample_seconds = temp_seconds; + RecalcParameters(true); + } +} + + +// User changed sample frequency field +void CSmpGenDialog::OnSampleFreqChanged() +{ + int temp_freq = GetDlgItemInt(IDC_EDIT_SAMPLE_FREQ); + if(temp_freq >= SMPGEN_MINFREQ && temp_freq <= SMPGEN_MAXFREQ) + { + sample_frequency = temp_freq; + RecalcParameters(false); + } +} + + +// Show all expressions that can be input +void CSmpGenDialog::OnShowExpressions() +{ + HMENU hMenu = ::CreatePopupMenu(), hSubMenu = NULL; + if(!hMenu) return; + + for(int i = 0; i < MAX_SAMPLEGEN_EXPRESSIONS; i++) + { + if(menu_descriptions[i].expression == "") + { + // add sub menu + if(hSubMenu != NULL) ::DestroyMenu(hSubMenu); + hSubMenu = ::CreatePopupMenu(); + + AppendMenu(hMenu, MF_POPUP, (UINT_PTR)hSubMenu, menu_descriptions[i].description.c_str()); + } else + { + // add sub menu entry (formula) + AppendMenu(hSubMenu, MF_STRING, ID_SAMPLE_GENERATOR_MENU + i, menu_descriptions[i].description.c_str()); + } + } + + // place popup menu below button + RECT button; + GetDlgItem(IDC_BUTTON_SHOW_EXPRESSIONS)->GetWindowRect(&button); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, button.left, button.bottom, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + ::DestroyMenu(hSubMenu); +} + + +// Show all expression presets +void CSmpGenDialog::OnShowPresets() +{ + HMENU hMenu = ::CreatePopupMenu(); + if(!hMenu) return; + + bool prestsExist = false; + for(size_t i = 0; i < presets.GetNumPresets(); i++) + { + if(presets.GetPreset(i)->expression != "") + { + AppendMenu(hMenu, MF_STRING, ID_SAMPLE_GENERATOR_PRESET_MENU + i, presets.GetPreset(i)->description.c_str()); + prestsExist = true; + } + } + + if(prestsExist) AppendMenu(hMenu, MF_SEPARATOR, 0, NULL); + + AppendMenu(hMenu, MF_STRING, ID_SAMPLE_GENERATOR_PRESET_MENU + MAX_SAMPLEGEN_PRESETS, _TEXT("Manage...")); + + CString result; + GetDlgItemText(IDC_EDIT_FORMULA, result); + if((!result.IsEmpty()) && (presets.GetNumPresets() < MAX_SAMPLEGEN_PRESETS)) + { + AppendMenu(hMenu, MF_STRING, ID_SAMPLE_GENERATOR_PRESET_MENU + MAX_SAMPLEGEN_PRESETS + 1, _TEXT("Add current...")); + } + + // place popup menu below button + RECT button; + GetDlgItem(IDC_BUTTON_SAMPLEGEN_PRESETS)->GetWindowRect(&button); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, button.left, button.bottom, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); +} + + + +// Insert expression from context menu +void CSmpGenDialog::OnInsertExpression(UINT nId) +{ + if((nId < ID_SAMPLE_GENERATOR_MENU) || (nId >= ID_SAMPLE_GENERATOR_MENU + MAX_SAMPLEGEN_EXPRESSIONS)) return; + + expression += " " + menu_descriptions[nId - ID_SAMPLE_GENERATOR_MENU].expression; + + SetDlgItemText(IDC_EDIT_FORMULA, expression.c_str()); +} + + +// Select a preset (or manage them, or add one) +void CSmpGenDialog::OnSelectPreset(UINT nId) +{ + if((nId < ID_SAMPLE_GENERATOR_PRESET_MENU) || (nId >= ID_SAMPLE_GENERATOR_PRESET_MENU + MAX_SAMPLEGEN_PRESETS + 2)) return; + + if(nId - ID_SAMPLE_GENERATOR_PRESET_MENU >= MAX_SAMPLEGEN_PRESETS) + { + // add... + if((nId - ID_SAMPLE_GENERATOR_PRESET_MENU == MAX_SAMPLEGEN_PRESETS + 1)) + { + samplegen_expression newPreset; + newPreset.description = newPreset.expression = expression; + presets.AddPreset(newPreset); + // call preset manager now. + } + + // manage... + CSmpGenPresetDlg dlg(&presets); + dlg.DoModal(); + } else + { + expression = presets.GetPreset(nId - ID_SAMPLE_GENERATOR_PRESET_MENU)->expression; + SetDlgItemText(IDC_EDIT_FORMULA, expression.c_str()); + } + +} + + +// Update input fields, depending on what has been chagned +void CSmpGenDialog::RecalcParameters(bool secondsChanged, bool forceRefresh) +{ + static bool isLocked = false; + if(isLocked) return; + isLocked = true; // avoid deadlock + + if(secondsChanged) + { + // seconds changed => recalc length + sample_length = (int)(sample_seconds * sample_frequency); + if(sample_length < SMPGEN_MINLENGTH || sample_length > SMPGEN_MAXLENGTH) sample_length = SMPGEN_MAXLENGTH; + } else + { + // length/freq changed => recalc seconds + sample_seconds = ((double)sample_length) / ((double)sample_frequency); + } + + if(secondsChanged || forceRefresh) SetDlgItemInt(IDC_EDIT_SAMPLE_LENGTH, sample_length); + if(secondsChanged || forceRefresh) SetDlgItemInt(IDC_EDIT_SAMPLE_FREQ, sample_frequency); + CString str; + str.Format("%.4f", sample_seconds); + if(!secondsChanged || forceRefresh) SetDlgItemText(IDC_EDIT_SAMPLE_LENGTH_SEC, str); + + int smpsize = sample_length * SMPGEN_MIXBYTES; + if(smpsize < 1024) + { + str.Format("Sample Size: %d Bytes", smpsize); + } else if((smpsize >> 10) < 1024) + { + str.Format("Sample Size: %d KB", smpsize >> 10); + } else + { + str.Format("Sample Size: %d MB", smpsize >> 20); + } + SetDlgItemText(IDC_STATIC_SMPSIZE_KB, str); + + isLocked = false; +} + + +// Create a set of default formla presets +void CSmpGenDialog::CreateDefaultPresets() +{ + samplegen_expression preset; + + preset.description = "A440"; + preset.expression = "sin(xp * _pi / 50 * 440 * len / freq)"; + presets.AddPreset(preset); + + preset.description = "Brown Noise (kind of)"; + preset.expression = "rnd(1) * 0.1 + smp(x - 1) * 0.9"; + presets.AddPreset(preset); + + preset.description = "Noisy Saw"; + preset.expression = "(x mod 800) / 800 - 0.5 + rnd(0.1)"; + presets.AddPreset(preset); + + preset.description = "PWM Filter"; + preset.expression = "pwm(x, 50 + sin(xp * _pi / 100) * 40, 100) + tri(x, 50)"; + presets.AddPreset(preset); + + preset.description = "Fat PWM Pad"; + preset.expression = "pwm(x, xp, 500) + pwm(x, abs(50 - xp), 1000)"; + presets.AddPreset(preset); + + preset.description = "Dual Square"; + preset.expression = "if((x mod 100) < 50, (x mod 200), -x mod 200)"; + presets.AddPreset(preset); + + preset.description = "Noise Hit"; + preset.expression = "exp(-xp) * (rnd(x) - x / 2)"; + presets.AddPreset(preset); + + preset.description = "Laser"; + preset.expression = "sin(xp * _pi * 100 /(xp ^ 2)) * 100 / sqrt(xp)"; + presets.AddPreset(preset); + + preset.description = "Noisy Laser Hit"; + preset.expression = "(sin(sqrt(xp) * 100) + rnd(1) - 0.5) * exp(-xp / 10)"; + presets.AddPreset(preset); + + preset.description = "Twinkle, Twinkle..."; + preset.expression = "sin(xp * _pi * 100 / xp) * 100 / sqrt(xp)"; + presets.AddPreset(preset); + + preset.description = "FM Tom"; + preset.expression = "sin(xp * _pi * 2 + (xp / 5 - 50) ^ 2) * exp(-xp / 10)"; + presets.AddPreset(preset); + + preset.description = "FM Warp"; + preset.expression = "sin(_pi * xp / 2 * (1 + (1 + sin(_pi * xp / 4 * 50)) / 4)) * exp(-(xp / 8) * .6)"; + presets.AddPreset(preset); + + preset.description = "Weird Noise"; + preset.expression = "rnd(1) * 0.1 + smp(x - rnd(xp)) * 0.9"; + presets.AddPreset(preset); + +} + + + +////////////////////////////////////////////////////////////////////////// +// Sample Generator Preset Dialog implementation + + +BEGIN_MESSAGE_MAP(CSmpGenPresetDlg, CDialog) + ON_COMMAND(IDC_BUTTON_ADD, &CSmpGenPresetDlg::OnAddPreset) + ON_COMMAND(IDC_BUTTON_REMOVE, &CSmpGenPresetDlg::OnRemovePreset) + ON_EN_CHANGE(IDC_EDIT_PRESET_NAME, &CSmpGenPresetDlg::OnTextChanged) + ON_EN_CHANGE(IDC_EDIT_PRESET_EXPR, &CSmpGenPresetDlg::OnExpressionChanged) + ON_LBN_SELCHANGE(IDC_LIST_SAMPLEGEN_PRESETS, &CSmpGenPresetDlg::OnListSelChange) +END_MESSAGE_MAP() + + +BOOL CSmpGenPresetDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + + RefreshList(); + + return TRUE; +} + + +void CSmpGenPresetDlg::OnOK() +{ + // remove empty presets + for(size_t i = 0; i < presets->GetNumPresets(); i++) + { + if(presets->GetPreset(i)->expression.empty()) + { + presets->RemovePreset(i); + } + } + CDialog::OnOK(); +} + + +void CSmpGenPresetDlg::OnListSelChange() +{ + currentItem = ((CListBox *)GetDlgItem(IDC_LIST_SAMPLEGEN_PRESETS))->GetCurSel() + 1; + if(currentItem == 0 || currentItem > presets->GetNumPresets()) return; + samplegen_expression *preset = presets->GetPreset(currentItem - 1); + if(preset == nullptr) return; + SetDlgItemText(IDC_EDIT_PRESET_NAME, preset->description.c_str()); + SetDlgItemText(IDC_EDIT_PRESET_EXPR, preset->expression.c_str()); +} + + +void CSmpGenPresetDlg::OnTextChanged() +{ + if(currentItem == 0 || currentItem > presets->GetNumPresets()) return; + CString result; + GetDlgItemText(IDC_EDIT_PRESET_NAME, result); + + samplegen_expression *preset = presets->GetPreset(currentItem - 1); + if(preset == nullptr) return; + preset->description = result; + + CListBox *clist = (CListBox *)GetDlgItem(IDC_LIST_SAMPLEGEN_PRESETS); + clist->DeleteString(currentItem - 1); + clist->InsertString(currentItem - 1, (preset->description).c_str()); + clist->SetCurSel(currentItem - 1); +} + + +void CSmpGenPresetDlg::OnExpressionChanged() +{ + if(currentItem == 0 || currentItem > presets->GetNumPresets()) return; + CString result; + GetDlgItemText(IDC_EDIT_PRESET_EXPR, result); + + samplegen_expression *preset = presets->GetPreset(currentItem - 1); + if(preset != nullptr) preset->expression = result; + +} + + +void CSmpGenPresetDlg::OnAddPreset() +{ + samplegen_expression newPreset; + newPreset.description = "New Preset"; + newPreset.expression = ""; + if(presets->AddPreset(newPreset)) + { + currentItem = presets->GetNumPresets(); + RefreshList(); + } +} + + +void CSmpGenPresetDlg::OnRemovePreset() +{ + if(currentItem == 0 || currentItem > presets->GetNumPresets()) return; + if(presets->RemovePreset(currentItem - 1)) + RefreshList(); +} + + +void CSmpGenPresetDlg::RefreshList() +{ + CListBox *clist = (CListBox *)GetDlgItem(IDC_LIST_SAMPLEGEN_PRESETS); + clist->SetRedraw(FALSE); //disable lisbox refreshes during fill to avoid flicker + clist->ResetContent(); + for(size_t i = 0; i < presets->GetNumPresets(); i++) + { + samplegen_expression *preset = presets->GetPreset(i); + if(preset != nullptr) + clist->AddString((preset->description).c_str()); + } + clist->SetRedraw(TRUE); //re-enable lisbox refreshes + if(currentItem == 0 || currentItem > presets->GetNumPresets()) + { + currentItem = presets->GetNumPresets(); + } + if(currentItem != 0) clist->SetCurSel(currentItem - 1); + OnListSelChange(); +} + +#endif // MPT_DISABLED_CODE diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.h b/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.h new file mode 100644 index 00000000..fb9a84ab --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleGenerator.h @@ -0,0 +1,208 @@ +/* + * SampleGenerator.h + * ----------------- + * Purpose: Generate samples from math formulas using muParser + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifdef MPT_DISABLED_CODE + +#include "Mptrack.h" +#include "Mainfrm.h" +#include "Sndfile.h" +#include "../muParser/include/muParser.h" + +// sample length +#define SMPGEN_MINLENGTH 1 +#define SMPGEN_MAXLENGTH MAX_SAMPLE_LENGTH +// sample frequency +#define SMPGEN_MINFREQ 1 +#define SMPGEN_MAXFREQ 96000 // MAX_SAMPLE_RATE +// 16-bit sample quality - when changing this, also change CSampleGenerator::sampling_type and 16-bit flags in SampleGenerator.cpp! +#define SMPGEN_MIXBYTES 2 + +enum smpgen_clip_methods +{ + smpgen_clip, + smpgen_overflow, + smpgen_normalize, +}; + +class CSampleGenerator +{ +protected: + + // sample parameters + static int sample_frequency; + static int sample_length; + static mu::string_type expression; + static smpgen_clip_methods sample_clipping; + + // rendering helper variables (they're here for the callback functions) + static mu::value_type *sample_buffer; + static size_t samples_written; + + typedef int16 sampling_type; // has to match SMPGEN_MIXBYTES! + static constexpr sampling_type sample_maxvalue = (1 << ((SMPGEN_MIXBYTES << 3) - 1)) - 1; + + // muParser object for parsing the expression + mu::Parser muParser; + + // Rendering callback functions + // functions + static mu::value_type ClipCallback(mu::value_type val, mu::value_type min, mu::value_type max) { return Clamp(val, min, max); }; + static mu::value_type PWMCallback(mu::value_type pos, mu::value_type duty, mu::value_type width) { if(width == 0) return 0; else return (fmod(pos, width) < ((duty / 100) * width)) ? 1 : -1; }; + static mu::value_type RndCallback(mu::value_type v) { return v * std::rand() / (mu::value_type)(RAND_MAX + 1.0); }; + static mu::value_type SampleDataCallback(mu::value_type v); + static mu::value_type TriangleCallback(mu::value_type pos, mu::value_type width) { if((int)width == 0) return 0; else return std::abs(((int)pos % (int)(width)) - width / 2) / (width / 4) - 1; }; + + // binary operators + static mu::value_type ModuloCallback(mu::value_type x, mu::value_type y) { if(y == 0) return 0; else return fmod(x , y); }; + + void ShowError(mu::Parser::exception_type *e); + +public: + + bool ShowDialog(); + bool TestExpression(); + bool CanRenderSample() const; + bool RenderSample(CSoundFile *pSndFile, SAMPLEINDEX nSample); + + CSampleGenerator(); + +}; + + +////////////////////////////////////////////////////////////////////////// +// Sample Generator Formula Preset implementation + + +struct samplegen_expression +{ + std::string description; // e.g. "Pulse" + mu::string_type expression; // e.g. "pwm(x,y,z)" - empty if this is a sub menu +}; +#define MAX_SAMPLEGEN_PRESETS 100 + + +class CSmpGenPresets +{ +protected: + vector<samplegen_expression> presets; + +public: + bool AddPreset(samplegen_expression new_preset) { if(GetNumPresets() >= MAX_SAMPLEGEN_PRESETS) return false; presets.push_back(new_preset); return true;}; + bool RemovePreset(size_t which) { if(which < GetNumPresets()) { presets.erase(presets.begin() + which); return true; } else return false; }; + samplegen_expression *GetPreset(size_t which) { if(which < GetNumPresets()) return &presets[which]; else return nullptr; }; + size_t GetNumPresets() { return presets.size(); }; + void Clear() { presets.clear(); }; + + CSmpGenPresets() { Clear(); } + ~CSmpGenPresets() { Clear(); } +}; + + +////////////////////////////////////////////////////////////////////////// +// Sample Generator Dialog implementation + + +class CSmpGenDialog: public CDialog +{ +protected: + + // sample parameters + int sample_frequency; + int sample_length; + double sample_seconds; + mu::string_type expression; + smpgen_clip_methods sample_clipping; + // pressed "OK"? + bool apply; + // preset slots + CSmpGenPresets presets; + + HFONT hButtonFont; // "Marlett" font for "dropdown" button + + void RecalcParameters(bool secondsChanged, bool forceRefresh = false); + + // function presets + void CreateDefaultPresets(); + +public: + + const int GetFrequency() { return sample_frequency; }; + const int GetLength() { return sample_length; }; + const smpgen_clip_methods GetClipping() { return sample_clipping; } + const mu::string_type GetExpression() { return expression; }; + bool CanApply() { return apply; }; + + CSmpGenDialog(int freq, int len, smpgen_clip_methods clipping, mu::string_type expr):CDialog(IDD_SAMPLE_GENERATOR, CMainFrame::GetMainFrame()) + { + sample_frequency = freq; + sample_length = len; + sample_clipping = clipping; + expression = expr; + apply = false; + } + +protected: + virtual BOOL OnInitDialog(); + virtual void OnOK(); + virtual void OnCancel(); + + afx_msg void OnSampleLengthChanged(); + afx_msg void OnSampleSecondsChanged(); + afx_msg void OnSampleFreqChanged(); + afx_msg void OnExpressionChanged(); + afx_msg void OnShowExpressions(); + afx_msg void OnShowPresets(); + afx_msg void OnInsertExpression(UINT nId); + afx_msg void OnSelectPreset(UINT nId); + + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +////////////////////////////////////////////////////////////////////////// +// Sample Generator Preset Dialog implementation + + +class CSmpGenPresetDlg: public CDialog +{ +protected: + CSmpGenPresets *presets; + size_t currentItem; // first item is actually 1! + + void RefreshList(); + +public: + CSmpGenPresetDlg(CSmpGenPresets *pPresets):CDialog(IDD_SAMPLE_GENERATOR_PRESETS, CMainFrame::GetMainFrame()) + { + presets = pPresets; + currentItem = 0; + } + +protected: + virtual BOOL OnInitDialog(); + virtual void OnOK(); + + afx_msg void OnListSelChange(); + + afx_msg void OnTextChanged(); + afx_msg void OnExpressionChanged(); + + afx_msg void OnAddPreset(); + afx_msg void OnRemovePreset(); + + DECLARE_MESSAGE_MAP() +}; + +#endif // MPT_DISABLED_CODE diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SampleTrimmer.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/SampleTrimmer.cpp new file mode 100644 index 00000000..1f9b21d1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SampleTrimmer.cpp @@ -0,0 +1,178 @@ +/* + * SampleTrimmer.cpp + * ----------------- + * Purpose: Automatic trimming of unused sample parts for module size optimization. + * 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 <numeric> +#include "InputHandler.h" +#include "Mainfrm.h" +#include "Moddoc.h" +#include "ProgressDialog.h" +#include "../tracklib/SampleEdit.h" +#include "../soundlib/OPL.h" + +OPENMPT_NAMESPACE_BEGIN + +class CRenderProgressDlg : public CProgressDialog +{ + CSoundFile &m_SndFile; + + class DummyAudioTarget : public IAudioTarget + { + public: + void Process(mpt::audio_span_interleaved<MixSampleInt>) override { } + void Process(mpt::audio_span_interleaved<MixSampleFloat>) override { } + }; + +public: + std::vector<SmpLength> m_SamplePlayLengths; + + CRenderProgressDlg(CWnd *parent, CSoundFile &sndFile) + : CProgressDialog{parent} + , m_SndFile{sndFile} + { + m_SndFile.m_SamplePlayLengths = &m_SamplePlayLengths; + } + + ~CRenderProgressDlg() + { + m_SndFile.m_SamplePlayLengths = nullptr; + } + + void Run() override + { + // We're not interested in plugin rendering + std::bitset<MAX_MIXPLUGINS> plugMuteStatus; + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + plugMuteStatus[i] = m_SndFile.m_MixPlugins[i].IsBypassed(); + m_SndFile.m_MixPlugins[i].SetBypass(true); + } + + m_SamplePlayLengths.assign(m_SndFile.GetNumSamples() + 1, 0); + + const auto origSequence = m_SndFile.Order.GetCurrentSequenceIndex(); + const auto origRepeatCount = m_SndFile.GetRepeatCount(); + auto opl = std::move(m_SndFile.m_opl); + m_SndFile.SetRepeatCount(0); + m_SndFile.m_bIsRendering = true; + + auto prevTime = timeGetTime(); + const auto subSongs = m_SndFile.GetAllSubSongs(); + SetRange(0, mpt::saturate_round<uint64>(std::accumulate(subSongs.begin(), subSongs.end(), 0.0, [](double acc, const auto& song) { return acc + song.duration; }) * m_SndFile.GetSampleRate())); + size_t totalSamples = 0; + for(size_t i = 0; i < subSongs.size() && !m_abort; i++) + { + SetWindowText(MPT_CFORMAT("Automatic Sample Trimmer - Song {} / {}")(i + 1, subSongs.size())); + + const auto &song = subSongs[i]; + m_SndFile.ResetPlayPos(); + m_SndFile.GetLength(eAdjust, GetLengthTarget(song.startOrder, song.startRow).StartPos(song.sequence, 0, 0)); + m_SndFile.m_SongFlags.reset(SONG_PLAY_FLAGS); + + size_t subsongSamples = 0; + DummyAudioTarget target; + while(!m_abort) + { + auto count = m_SndFile.Read(MIXBUFFERSIZE, target); + if(count == 0) + break; + + totalSamples += count; + subsongSamples += count; + + auto currentTime = timeGetTime(); + if(currentTime - prevTime >= 16) + { + prevTime = currentTime; + auto timeSec = subsongSamples / m_SndFile.GetSampleRate(); + SetText(MPT_CFORMAT("Analyzing... {}:{}:{}")(timeSec / 3600, mpt::cfmt::dec0<2>((timeSec / 60) % 60), mpt::cfmt::dec0<2>(timeSec % 60))); + SetProgress(totalSamples); + ProcessMessages(); + } + } + } + + // Reset globals to previous values + m_SndFile.Order.SetSequence(origSequence); + m_SndFile.SetRepeatCount(origRepeatCount); + m_SndFile.ResetPlayPos(); + m_SndFile.StopAllVsti(); + m_SndFile.m_bIsRendering = false; + m_SndFile.m_opl = std::move(opl); + + for(PLUGINDEX i = 0; i < MAX_MIXPLUGINS; i++) + { + m_SndFile.m_MixPlugins[i].SetBypass(plugMuteStatus[i]); + } + + EndDialog(IDOK); + } +}; + + +void CModDoc::OnShowSampleTrimmer() +{ + BypassInputHandler bih; + CMainFrame::GetMainFrame()->StopMod(this); + CRenderProgressDlg dlg(CMainFrame::GetMainFrame(), m_SndFile); + dlg.DoModal(); + SAMPLEINDEX numTrimmed = 0; + SmpLength numBytes = 0; + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + ModSample &sample = m_SndFile.GetSample(smp); + auto &newLength = dlg.m_SamplePlayLengths[smp]; + if(newLength == 0) + continue; + // Take interpolation look-ahead into account + if((!sample.uFlags[CHN_LOOP] || newLength != sample.nLoopEnd) + && (!sample.uFlags[CHN_SUSTAINLOOP] || newLength != sample.nSustainEnd)) + { + newLength = std::min(newLength + InterpolationMaxLookahead, sample.nLength); + } + if(sample.nLength > newLength) + { + numTrimmed++; + numBytes += (sample.nLength - newLength) * sample.GetBytesPerSample(); + } + } + if(numTrimmed == 0) + { + Reporting::Information(_T("No samples can be trimmed, because all samples are played in their full length.")); + return; + } + + mpt::ustring s = MPT_UFORMAT("{} sample{} can be trimmed, saving {} byte{}.")(numTrimmed, (numTrimmed == 1) ? U_("") : U_("s"), mpt::ufmt::dec(3, ',', numBytes), numBytes != 1 ? U_("s") : U_("")); + if(dlg.m_abort) + { + s += U_("\n\nWARNING: Only partial results are available, possibly causing used sample parts to be trimmed.\nContinue anyway?"); + } else + { + s += U_(" Continue?"); + } + if(Reporting::Confirm(s, false, dlg.m_abort) == cnfYes) + { + for(SAMPLEINDEX smp = 1; smp <= GetNumSamples(); smp++) + { + ModSample &sample = m_SndFile.GetSample(smp); + if(dlg.m_SamplePlayLengths[smp] != 0 && sample.nLength > dlg.m_SamplePlayLengths[smp]) + { + GetSampleUndo().PrepareUndo(smp, sundo_delete, "Automatic Sample Trimming", dlg.m_SamplePlayLengths[smp], sample.nLength); + SampleEdit::ResizeSample(sample, dlg.m_SamplePlayLengths[smp], m_SndFile); + sample.uFlags.set(SMP_MODIFIED); + } + } + + SetModified(); + UpdateAllViews(SampleHint().Data().Info()); + } +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.cpp new file mode 100644 index 00000000..17d3df89 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.cpp @@ -0,0 +1,79 @@ +/* + * ScaleEnvPointsDlg.cpp + * --------------------- + * Purpose: Dialog for scaling instrument envelope points on x and y axis. + * 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 "resource.h" +#include "ModInstrument.h" +#include "ScaleEnvPointsDlg.h" + + +OPENMPT_NAMESPACE_BEGIN + +double CScaleEnvPointsDlg::m_factorX = 1.0; +double CScaleEnvPointsDlg::m_factorY = 1.0; +double CScaleEnvPointsDlg::m_offsetY = 0.0; + +BOOL CScaleEnvPointsDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + m_EditX.SubclassDlgItem(IDC_EDIT_FACTORX, this); + m_EditY.SubclassDlgItem(IDC_EDIT_FACTORY, this); + m_EditOffset.SubclassDlgItem(IDC_EDIT3, this); + m_EditX.AllowNegative(false); + m_EditX.SetDecimalValue(m_factorX); + m_EditY.SetDecimalValue(m_factorY); + m_EditOffset.SetDecimalValue(m_offsetY); + + return TRUE; // return TRUE unless you set the focus to a control +} + + +void CScaleEnvPointsDlg::OnOK() +{ + m_EditX.GetDecimalValue(m_factorX); + m_EditY.GetDecimalValue(m_factorY); + m_EditOffset.GetDecimalValue(m_offsetY); + CDialog::OnOK(); +} + + +void CScaleEnvPointsDlg::Apply() +{ + if(m_factorX > 0 && m_factorX != 1) + { + for(uint32 i = 0; i < m_Env.size(); i++) + { + m_Env[i].tick = static_cast<EnvelopeNode::tick_t>(m_factorX * m_Env[i].tick); + + // Checking that the order of points is preserved. + if(i > 0 && m_Env[i].tick <= m_Env[i - 1].tick) + m_Env[i].tick = m_Env[i - 1].tick + 1; + } + } + + if(m_factorY != 1 || m_offsetY != 0) + { + double factor = m_factorY; + bool invert = false; + if(m_factorY < 0) + { + invert = true; + factor = -factor; + } + for(auto &pt : m_Env) + { + if(invert) pt.value = ENVELOPE_MAX - pt.value; + pt.value = mpt::saturate_round<EnvelopeNode::value_t>(Clamp((factor * (pt.value - m_nCenter)) + m_nCenter + m_offsetY, double(ENVELOPE_MIN), double(ENVELOPE_MAX))); + } + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.h b/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.h new file mode 100644 index 00000000..e9738bf4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/ScaleEnvPointsDlg.h @@ -0,0 +1,43 @@ +/* + * ScaleEnvPointsDlg.h + * ------------------- + * Purpose: Dialog for scaling instrument envelope points on x and y axis. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +struct InstrumentEnvelope; + +class CScaleEnvPointsDlg : public CDialog +{ +protected: + CNumberEdit m_EditX, m_EditY, m_EditOffset; + InstrumentEnvelope &m_Env; + static double m_factorX, m_factorY, m_offsetY; + int m_nCenter; + +public: + CScaleEnvPointsDlg(CWnd* pParent, InstrumentEnvelope &env, int nCenter) + : CDialog(IDD_SCALE_ENV_POINTS, pParent) + , m_Env(env) + , m_nCenter(nCenter) + { } + + void Apply(); + +protected: + void OnOK() override; + BOOL OnInitDialog() override; +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.cpp new file mode 100644 index 00000000..18c411df --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.cpp @@ -0,0 +1,868 @@ +/* + * SelectPluginDialog.cpp + * ---------------------- + * Purpose: Dialog for adding plugins to a song. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS + +#include "Mptrack.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "../common/mptStringBuffer.h" +#include "FileDialog.h" +#include "../soundlib/plugins/PluginManager.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "SelectPluginDialog.h" +#include "../pluginBridge/BridgeWrapper.h" +#include "FolderScanner.h" + + +OPENMPT_NAMESPACE_BEGIN + +///////////////////////////////////////////////////////////////////////////////// +// Plugin selection dialog + + +BEGIN_MESSAGE_MAP(CSelectPluginDlg, ResizableDialog) + ON_NOTIFY(TVN_SELCHANGED, IDC_TREE1, &CSelectPluginDlg::OnSelChanged) + ON_NOTIFY(NM_DBLCLK, IDC_TREE1, &CSelectPluginDlg::OnSelDblClk) + ON_COMMAND(IDC_BUTTON1, &CSelectPluginDlg::OnAddPlugin) + ON_COMMAND(IDC_BUTTON3, &CSelectPluginDlg::OnScanFolder) + ON_COMMAND(IDC_BUTTON2, &CSelectPluginDlg::OnRemovePlugin) + ON_COMMAND(IDC_CHECK1, &CSelectPluginDlg::OnSetBridge) + ON_COMMAND(IDC_CHECK2, &CSelectPluginDlg::OnSetBridge) + ON_COMMAND(IDC_CHECK3, &CSelectPluginDlg::OnSetBridge) + ON_EN_CHANGE(IDC_NAMEFILTER, &CSelectPluginDlg::OnNameFilterChanged) + ON_EN_CHANGE(IDC_PLUGINTAGS, &CSelectPluginDlg::OnPluginTagsChanged) +END_MESSAGE_MAP() + + +void CSelectPluginDlg::DoDataExchange(CDataExchange* pDX) +{ + ResizableDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_TREE1, m_treePlugins); + DDX_Control(pDX, IDC_CHECK1, m_chkBridge); + DDX_Control(pDX, IDC_CHECK2, m_chkShare); + DDX_Control(pDX, IDC_CHECK3, m_chkLegacyBridge); +} + + +CSelectPluginDlg::CSelectPluginDlg(CModDoc *pModDoc, PLUGINDEX pluginSlot, CWnd *parent) + : ResizableDialog(IDD_SELECTMIXPLUGIN, parent) + , m_pModDoc(pModDoc) + , m_nPlugSlot(pluginSlot) +{ + if(m_pModDoc && 0 <= m_nPlugSlot && m_nPlugSlot < MAX_MIXPLUGINS) + { + m_pPlugin = &(pModDoc->GetSoundFile().m_MixPlugins[m_nPlugSlot]); + } + + CMainFrame::GetInputHandler()->Bypass(true); +} + + +CSelectPluginDlg::~CSelectPluginDlg() +{ + CMainFrame::GetInputHandler()->Bypass(false); +} + + +BOOL CSelectPluginDlg::OnInitDialog() +{ + DWORD dwRemove = TVS_EDITLABELS|TVS_SINGLEEXPAND; + DWORD dwAdd = TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS; + + ResizableDialog::OnInitDialog(); + m_treePlugins.ModifyStyle(dwRemove, dwAdd); + m_treePlugins.SetImageList(&CMainFrame::GetMainFrame()->m_MiscIcons, TVSIL_NORMAL); + + if (m_pPlugin) + { + CString targetSlot = MPT_CFORMAT("&Put in FX{}")(mpt::cfmt::dec0<2>(m_nPlugSlot + 1)); + SetDlgItemText(IDOK, targetSlot); + ::EnableWindow(::GetDlgItem(m_hWnd, IDOK), TRUE); + } else + { + ::EnableWindow(::GetDlgItem(m_hWnd, IDOK), FALSE); + } + + const int dpiX = Util::GetDPIx(m_hWnd); + const int dpiY = Util::GetDPIy(m_hWnd); + CRect rect + ( + CPoint(MulDiv(TrackerSettings::Instance().gnPlugWindowX, dpiX, 96), MulDiv(TrackerSettings::Instance().gnPlugWindowY, dpiY, 96)), + CSize(MulDiv(TrackerSettings::Instance().gnPlugWindowWidth, dpiX, 96), MulDiv(TrackerSettings::Instance().gnPlugWindowHeight, dpiY, 96)) + ); + ::MapWindowPoints(GetParent()->m_hWnd, HWND_DESKTOP, (CPoint *)&rect, 2); + WINDOWPLACEMENT wnd; + wnd.length = sizeof(wnd); + GetWindowPlacement(&wnd); + wnd.showCmd = SW_SHOW; + wnd.rcNormalPosition = rect; + SetWindowPlacement(&wnd); + + UpdatePluginsList(); + OnSelChanged(NULL, NULL); + return TRUE; +} + + +void CSelectPluginDlg::OnOK() +{ + if(m_pPlugin == nullptr) + { + ResizableDialog::OnOK(); + return; + } + + bool changed = false; + CVstPluginManager *pManager = theApp.GetPluginManager(); + VSTPluginLib *pNewPlug = GetSelectedPlugin(); + VSTPluginLib *pFactory = nullptr; + IMixPlugin *pCurrentPlugin = nullptr; + if(m_pPlugin) + pCurrentPlugin = m_pPlugin->pMixPlugin; + if((pManager) && (pManager->IsValidPlugin(pNewPlug))) + pFactory = pNewPlug; + + if (pFactory) + { + // Plugin selected + if ((!pCurrentPlugin) || &pCurrentPlugin->GetPluginFactory() != pFactory) + { + CriticalSection cs; + + // Destroy old plugin, if there was one. + const auto oldOutput = m_pPlugin->GetOutputPlugin(); + m_pPlugin->Destroy(); + + // Initialize plugin info + MemsetZero(m_pPlugin->Info); + if(oldOutput != PLUGINDEX_INVALID) + m_pPlugin->SetOutputPlugin(oldOutput); + m_pPlugin->Info.dwPluginId1 = pFactory->pluginId1; + m_pPlugin->Info.dwPluginId2 = pFactory->pluginId2; + m_pPlugin->editorX = m_pPlugin->editorY = int32_min; + m_pPlugin->SetAutoSuspend(TrackerSettings::Instance().enableAutoSuspend); + +#ifdef MPT_WITH_VST + if(m_pPlugin->Info.dwPluginId1 == Vst::kEffectMagic) + { + switch(m_pPlugin->Info.dwPluginId2) + { + // Enable drymix by default for these known plugins + case Vst::FourCC("Scop"): + m_pPlugin->SetWetMix(); + break; + } + } +#endif // MPT_WITH_VST + + m_pPlugin->Info.szName = pFactory->libraryName.ToLocale(); + m_pPlugin->Info.szLibraryName = pFactory->libraryName.ToUTF8(); + + cs.Leave(); + + // Now, create the new plugin + if(pManager && m_pModDoc) + { + pManager->CreateMixPlugin(*m_pPlugin, m_pModDoc->GetSoundFile()); + if (m_pPlugin->pMixPlugin) + { + IMixPlugin *p = m_pPlugin->pMixPlugin; + const CString name = p->GetDefaultEffectName(); + if(!name.IsEmpty()) + { + m_pPlugin->Info.szName = mpt::ToCharset(mpt::Charset::Locale, name); + } + // Check if plugin slot is already assigned to any instrument, and if not, create one. + if(p->IsInstrument() && m_pModDoc->HasInstrumentForPlugin(m_nPlugSlot) == INSTRUMENTINDEX_INVALID) + { + m_pModDoc->InsertInstrumentForPlugin(m_nPlugSlot); + } + } else + { + MemsetZero(m_pPlugin->Info); + } + } + changed = true; + } + } else if(m_pPlugin->IsValidPlugin()) + { + // No effect + if(m_pModDoc) + changed = m_pModDoc->RemovePlugin(m_nPlugSlot); + } + + //remember window size: + SaveWindowPos(); + + if(changed) + { + if(m_pPlugin->Info.dwPluginId2) + TrackerSettings::Instance().gnPlugWindowLast = m_pPlugin->Info.dwPluginId2; + if(m_pModDoc) + { + m_pModDoc->UpdateAllViews(nullptr, PluginHint(static_cast<PLUGINDEX>(m_nPlugSlot + 1)).Info().Names()); + } + ResizableDialog::OnOK(); + } else + { + ResizableDialog::OnCancel(); + } +} + + +void CSelectPluginDlg::OnCancel() +{ + //remember window size: + SaveWindowPos(); + ResizableDialog::OnCancel(); +} + + +VSTPluginLib* CSelectPluginDlg::GetSelectedPlugin() +{ + HTREEITEM item = m_treePlugins.GetSelectedItem(); + if(item) + return reinterpret_cast<VSTPluginLib *>(m_treePlugins.GetItemData(item)); + else + return nullptr; +} + + +void CSelectPluginDlg::SaveWindowPos() const +{ + WINDOWPLACEMENT wnd; + wnd.length = sizeof(WINDOWPLACEMENT); + GetWindowPlacement(&wnd); + CRect rect = wnd.rcNormalPosition; + ::MapWindowPoints(HWND_DESKTOP, GetParent()->m_hWnd, (CPoint *)&rect, 2); + const int dpiX = Util::GetDPIx(m_hWnd); + const int dpiY = Util::GetDPIy(m_hWnd); + TrackerSettings::Instance().gnPlugWindowX = MulDiv(rect.left, 96, dpiX); + TrackerSettings::Instance().gnPlugWindowY = MulDiv(rect.top, 96, dpiY); + TrackerSettings::Instance().gnPlugWindowWidth = MulDiv(rect.Width(), 96, dpiY); + TrackerSettings::Instance().gnPlugWindowHeight = MulDiv(rect.Height(), 96, dpiX); +} + + +BOOL CSelectPluginDlg::PreTranslateMessage(MSG *pMsg) +{ + // Use up/down keys to navigate in tree view, even if search field is focussed. + if(pMsg != nullptr && pMsg->message == WM_KEYDOWN && (pMsg->wParam == VK_UP || pMsg->wParam == VK_DOWN) && GetFocus() != &m_treePlugins) + { + HTREEITEM selItem = m_treePlugins.GetSelectedItem(); + if(selItem == nullptr) + { + selItem = m_treePlugins.GetRootItem(); + } + while((selItem = m_treePlugins.GetNextItem(selItem, pMsg->wParam == VK_UP ? TVGN_PREVIOUSVISIBLE : TVGN_NEXTVISIBLE)) != nullptr) + { + int nImage, nSelectedImage; + m_treePlugins.GetItemImage(selItem, nImage, nSelectedImage); + if(nImage != IMAGE_FOLDER) + { + m_treePlugins.SelectItem(selItem); + m_treePlugins.EnsureVisible(selItem); + return TRUE; + } + } + return TRUE; + } + + return ResizableDialog::PreTranslateMessage(pMsg); +} + + +void CSelectPluginDlg::OnNameFilterChanged() +{ + // Update name filter text + m_nameFilter = mpt::ToLowerCase(GetWindowTextUnicode(*GetDlgItem(IDC_NAMEFILTER))); + + UpdatePluginsList(); +} + + +void CSelectPluginDlg::UpdatePluginsList(const VSTPluginLib *forceSelect) +{ + CVstPluginManager *pManager = theApp.GetPluginManager(); + + m_treePlugins.SetRedraw(FALSE); + m_treePlugins.DeleteAllItems(); + + static constexpr struct + { + VSTPluginLib::PluginCategory category; + const TCHAR *description; + } categories[] = + { + { VSTPluginLib::catEffect, _T("Audio Effects") }, + { VSTPluginLib::catGenerator, _T("Tone Generators") }, + { VSTPluginLib::catRestoration, _T("Audio Restauration") }, + { VSTPluginLib::catSurroundFx, _T("Surround Effects") }, + { VSTPluginLib::catRoomFx, _T("Room Effects") }, + { VSTPluginLib::catSpacializer, _T("Spacializers") }, + { VSTPluginLib::catMastering, _T("Mastering Plugins") }, + { VSTPluginLib::catAnalysis, _T("Analysis Plugins") }, + { VSTPluginLib::catOfflineProcess, _T("Offline Processing") }, + { VSTPluginLib::catShell, _T("Shell Plugins") }, + { VSTPluginLib::catUnknown, _T("Unsorted") }, + { VSTPluginLib::catDMO, _T("DirectX Media Audio Effects") }, + { VSTPluginLib::catSynth, _T("Instrument Plugins") }, + { VSTPluginLib::catHidden, _T("Legacy Plugins") }, + }; + + const HTREEITEM noPlug = AddTreeItem(_T("No plugin (empty slot)"), IMAGE_NOPLUGIN, false); + HTREEITEM currentPlug = noPlug; + + std::bitset<VSTPluginLib::numCategories> categoryUsed; + HTREEITEM categoryFolders[VSTPluginLib::numCategories]; + for(const auto &cat : categories) + { + categoryFolders[cat.category] = AddTreeItem(cat.description, IMAGE_FOLDER, false); + } + + enum PlugMatchQuality + { + kNoMatch, + kSameIdAsLast, + kSameIdAsLastWithPlatformMatch, + kSameIdAsCurrent, + kFoundCurrentPlugin, + }; + PlugMatchQuality foundPlugin = kNoMatch; + + const int32 lastPluginID = TrackerSettings::Instance().gnPlugWindowLast; + const bool nameFilterActive = !m_nameFilter.empty(); + const auto currentTags = mpt::String::Split<mpt::ustring>(m_nameFilter, U_(" ")); + + if(pManager) + { + bool first = true; + + for(auto p : *pManager) + { + MPT_ASSERT(p); + const VSTPluginLib &plug = *p; + if(plug.category == VSTPluginLib::catHidden && (m_pPlugin == nullptr || m_pPlugin->pMixPlugin == nullptr || &m_pPlugin->pMixPlugin->GetPluginFactory() != p)) + continue; + + if(nameFilterActive) + { + // Apply name filter + bool matches = false; + // Search in plugin names + { + mpt::ustring displayName = mpt::ToLowerCase(plug.libraryName.ToUnicode()); + if(displayName.find(m_nameFilter, 0) != displayName.npos) + { + matches = true; + } + } + // Search in plugin tags + if(!matches) + { + mpt::ustring tags = mpt::ToLowerCase(plug.tags); + for(const auto &tag : currentTags) + { + if(!tag.empty() && tags.find(tag, 0) != tags.npos) + { + matches = true; + break; + } + } + } + // Search in plugin vendors + if(!matches) + { + mpt::ustring vendor = mpt::ToLowerCase(mpt::ToUnicode(plug.vendor)); + if(vendor.find(m_nameFilter, 0) != vendor.npos) + { + matches = true; + } + } + if(!matches) continue; + } + + CString title = plug.libraryName.ToCString(); +#ifdef MPT_WITH_VST + if(!plug.IsNativeFromCache()) + { + title += MPT_CFORMAT(" ({})")(plug.GetDllArchNameUser()); + } +#endif // MPT_WITH_VST + HTREEITEM h = AddTreeItem(title, plug.isInstrument ? IMAGE_PLUGININSTRUMENT : IMAGE_EFFECTPLUGIN, true, categoryFolders[plug.category], reinterpret_cast<LPARAM>(&plug)); + categoryUsed[plug.category] = true; + + if(nameFilterActive) + { + // If filter is active, expand nodes. + m_treePlugins.EnsureVisible(h); + if(first) + { + first = false; + m_treePlugins.SelectItem(h); + } + } + + if(forceSelect != nullptr && &plug == forceSelect) + { + // Forced selection (e.g. just after add plugin) + currentPlug = h; + foundPlugin = kFoundCurrentPlugin; + } + + if(m_pPlugin && foundPlugin < kFoundCurrentPlugin) + { + //Which plugin should be selected? + if(m_pPlugin->pMixPlugin) + { + // Current slot's plugin + IMixPlugin *pPlugin = m_pPlugin->pMixPlugin; + if (&pPlugin->GetPluginFactory() == &plug) + { + currentPlug = h; + foundPlugin = kFoundCurrentPlugin; + } + } else if(m_pPlugin->Info.dwPluginId1 != 0 || m_pPlugin->Info.dwPluginId2 != 0) + { + // Plugin with matching ID to current slot's plug + if(plug.pluginId1 == m_pPlugin->Info.dwPluginId1 + && plug.pluginId2 == m_pPlugin->Info.dwPluginId2) + { + currentPlug = h; + foundPlugin = kSameIdAsCurrent; + } + } else if(plug.pluginId2 == lastPluginID && foundPlugin < kSameIdAsLastWithPlatformMatch) + { + // Previously selected plugin +#ifdef MPT_WITH_VST + foundPlugin = plug.IsNativeFromCache() ? kSameIdAsLastWithPlatformMatch : kSameIdAsLast; +#else // !MPT_WITH_VST + foundPlugin = kSameIdAsLastWithPlatformMatch; +#endif // MPT_WITH_VST + currentPlug = h; + } + } + } + } + + // Remove empty categories + for(size_t i = 0; i < std::size(categoryFolders); i++) + { + if(!categoryUsed[i]) + { + m_treePlugins.DeleteItem(categoryFolders[i]); + } + } + + m_treePlugins.SetRedraw(TRUE); + + if(!nameFilterActive || currentPlug != noPlug) + { + m_treePlugins.SelectItem(currentPlug); + } + m_treePlugins.SetItemState(currentPlug, TVIS_BOLD, TVIS_BOLD); + m_treePlugins.EnsureVisible(currentPlug); +} + + +HTREEITEM CSelectPluginDlg::AddTreeItem(const TCHAR *title, int image, bool sort, HTREEITEM hParent, LPARAM lParam) +{ + return m_treePlugins.InsertItem( + TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM | TVIF_TEXT, + title, + image, image, + 0, 0, + lParam, + hParent, + (sort ? TVI_SORT : TVI_LAST)); +} + + +void CSelectPluginDlg::OnSelDblClk(NMHDR *, LRESULT *result) +{ + if(m_pPlugin == nullptr) return; + + HTREEITEM hSel = m_treePlugins.GetSelectedItem(); + int nImage, nSelectedImage; + m_treePlugins.GetItemImage(hSel, nImage, nSelectedImage); + + if ((hSel) && (nImage != IMAGE_FOLDER)) OnOK(); + if (result) *result = 0; +} + + +void CSelectPluginDlg::OnSelChanged(NMHDR *, LRESULT *result) +{ + CVstPluginManager *pManager = theApp.GetPluginManager(); + VSTPluginLib *pPlug = GetSelectedPlugin(); + bool showBoxes = false; + BOOL enableTagsTextBox = FALSE; + BOOL enableRemoveButton = FALSE; + if (pManager != nullptr && pManager->IsValidPlugin(pPlug)) + { + if(pPlug->vendor.IsEmpty()) + SetDlgItemText(IDC_VENDOR, _T("")); + else + SetDlgItemText(IDC_VENDOR, _T("Vendor: ") + pPlug->vendor); + if(pPlug->dllPath.empty()) + SetDlgItemText(IDC_TEXT_CURRENT_VSTPLUG, _T("Built-in plugin")); + else + SetDlgItemText(IDC_TEXT_CURRENT_VSTPLUG, pPlug->dllPath.ToCString()); + SetDlgItemText(IDC_PLUGINTAGS, mpt::ToCString(pPlug->tags)); + enableRemoveButton = pPlug->isBuiltIn ? FALSE : TRUE; +#ifdef MPT_WITH_VST + if(pPlug->pluginId1 == Vst::kEffectMagic && !pPlug->isBuiltIn) + { + bool isBridgeAvailable = + ((pPlug->GetDllArch() == PluginArch_x86) && IsComponentAvailable(pluginBridge_x86)) + || + ((pPlug->GetDllArch() == PluginArch_x86) && IsComponentAvailable(pluginBridgeLegacy_x86)) + || + ((pPlug->GetDllArch() == PluginArch_amd64) && IsComponentAvailable(pluginBridge_amd64)) + || + ((pPlug->GetDllArch() == PluginArch_amd64) && IsComponentAvailable(pluginBridgeLegacy_amd64)) +#if defined(MPT_WITH_WINDOWS10) + || + ((pPlug->GetDllArch() == PluginArch_arm) && IsComponentAvailable(pluginBridge_arm)) + || + ((pPlug->GetDllArch() == PluginArch_arm) && IsComponentAvailable(pluginBridgeLegacy_arm)) + || + ((pPlug->GetDllArch() == PluginArch_arm64) && IsComponentAvailable(pluginBridge_arm64)) + || + ((pPlug->GetDllArch() == PluginArch_arm64) && IsComponentAvailable(pluginBridgeLegacy_arm64)) +#endif // MPT_WITH_WINDOWS10 + ; + if(TrackerSettings::Instance().bridgeAllPlugins || !isBridgeAvailable) + { + m_chkBridge.EnableWindow(FALSE); + m_chkBridge.SetCheck(isBridgeAvailable ? BST_CHECKED : BST_UNCHECKED); + } else + { + bool native = pPlug->IsNative(); + + m_chkBridge.EnableWindow(native ? TRUE : FALSE); + m_chkBridge.SetCheck((pPlug->useBridge || !native) ? BST_CHECKED : BST_UNCHECKED); + } + + m_chkShare.SetCheck(pPlug->shareBridgeInstance ? BST_CHECKED : BST_UNCHECKED); + m_chkShare.EnableWindow(m_chkBridge.GetCheck() != BST_UNCHECKED); + + m_chkLegacyBridge.SetCheck((!pPlug->modernBridge) ? BST_CHECKED : BST_UNCHECKED); + m_chkLegacyBridge.EnableWindow(m_chkBridge.GetCheck() != BST_UNCHECKED); + + showBoxes = true; + } + enableTagsTextBox = TRUE; +#endif // MPT_WITH_VST + } else + { + SetDlgItemText(IDC_VENDOR, _T("")); + SetDlgItemText(IDC_TEXT_CURRENT_VSTPLUG, _T("")); + SetDlgItemText(IDC_PLUGINTAGS, _T("")); + } + GetDlgItem(IDC_PLUGINTAGS)->EnableWindow(enableTagsTextBox); + GetDlgItem(IDC_BUTTON2)->EnableWindow(enableRemoveButton); + if(!showBoxes) + { + m_chkBridge.EnableWindow(FALSE); + m_chkShare.EnableWindow(FALSE); + m_chkLegacyBridge.EnableWindow(FALSE); + m_chkBridge.SetCheck(BST_UNCHECKED); + m_chkShare.SetCheck(BST_UNCHECKED); + m_chkLegacyBridge.SetCheck(BST_UNCHECKED); + } + if (result) *result = 0; +} + + +#ifdef MPT_WITH_VST +namespace +{ +// TODO: Keep these lists up-to-date. +constexpr struct +{ + int32 id1; + int32 id2; + const char *name; + const char *problem; +} ProblematicPlugins[] = +{ + {Vst::kEffectMagic, Vst::FourCC("mdaC"), "MDA Degrade", "* Old versions of this plugin can crash OpenMPT.\nEnsure that you have the latest version of this plugin."}, + {Vst::kEffectMagic, Vst::FourCC("fV2s"), "Farbrausch V2", "* This plugin can cause OpenMPT to freeze if being used in a combination with various other plugins.\nIt is recommended to use V2 only through the Plugin Bridge."}, + {Vst::kEffectMagic, Vst::FourCC("frV2"), "Farbrausch V2", "* This plugin can cause OpenMPT to freeze if being used in a combination with various other plugins.\nIt is recommended to use V2 only through the Plugin Bridge."}, + {Vst::kEffectMagic, Vst::FourCC("MMID"), "MIDI Input Output", "* The MIDI Input / Output plugin is now built right into OpenMPT and should not be loaded from an external file."}, +}; + +// Plugins that should always be bridged or require a specific bridge mode. +constexpr struct +{ + int32 id1; + int32 id2; + bool useBridge; + bool shareInstance; + bool modernBridge; +} ForceBridgePlugins[] = +{ + {Vst::kEffectMagic, Vst::FourCC("fV2s"), true, false, false}, // V2 freezes on shutdown if there's more than one instance per process + {Vst::kEffectMagic, Vst::FourCC("frV2"), true, false, false}, // ditto + {Vst::kEffectMagic, Vst::FourCC("SKV3"), false, true, false}, // SideKick v3 always has to run in a shared instance + {Vst::kEffectMagic, Vst::FourCC("YWS!"), false, true, false}, // You Wa Shock ! always has to run in a shared instance + {Vst::kEffectMagic, Vst::FourCC("S1Vs"), mpt::arch_bits == 64, true, false}, // Synth1 64-bit has an issue with pointers using the high 32 bits, hence must use the legacy bridge without high-entropy heap +}; +} // namespace +#endif // MPT_WITH_VST + + +bool CSelectPluginDlg::VerifyPlugin(VSTPluginLib *plug, CWnd *parent) +{ +#ifdef MPT_WITH_VST + for(const auto &p : ProblematicPlugins) + { + if(p.id2 == plug->pluginId2 && p.id1 == plug->pluginId1) + { + std::string s = MPT_AFORMAT("WARNING: This plugin has been identified as {},\nwhich is known to have the following problem with OpenMPT:\n\n{}\n\nWould you still like to add this plugin to the library?")(p.name, p.problem); + if(Reporting::Confirm(s, false, false, parent) == cnfNo) + { + return false; + } + break; + } + } + + for(const auto &p : ForceBridgePlugins) + { + if(p.id2 == plug->pluginId2 && p.id1 == plug->pluginId1) + { + plug->useBridge = p.useBridge; + plug->shareBridgeInstance = p.shareInstance; + if(!p.modernBridge) + plug->modernBridge = false; + plug->WriteToCache(); + break; + } + } +#else // !MPT_WITH_VST + MPT_UNREFERENCED_PARAMETER(plug); + MPT_UNREFERENCED_PARAMETER(parent); +#endif // MPT_WITH_VST + return true; +} + + +void CSelectPluginDlg::OnAddPlugin() +{ + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .DefaultExtension("dll") + .ExtensionFilter("VST Plugins|*.dll;*.vst3||") + .WorkingDirectory(TrackerSettings::Instance().PathPlugins.GetWorkingDir()); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathPlugins.SetWorkingDir(dlg.GetWorkingDirectory()); + + CVstPluginManager *plugManager = theApp.GetPluginManager(); + if(!plugManager) + return; + + VSTPluginLib *plugLib = nullptr; + bool update = false; + + for(const auto &file : dlg.GetFilenames()) + { + VSTPluginLib *lib = plugManager->AddPlugin(file, TrackerSettings::Instance().BrokenPluginsWorkaroundVSTMaskAllCrashes, mpt::ustring(), false); + if(lib != nullptr) + { + update = true; + if(!VerifyPlugin(lib, this)) + { + plugManager->RemovePlugin(lib); + } else + { + plugLib = lib; + + // If this plugin was missing anywhere, try loading it + ReloadMissingPlugins(lib); + } + } + } + if(update) + { + // Force selection to last added plug. + UpdatePluginsList(plugLib); + } else + { + Reporting::Error("No valid VST Plugin was selected."); + } +} + + +void CSelectPluginDlg::OnScanFolder() +{ + BrowseForFolder dlg(TrackerSettings::Instance().PathPlugins.GetWorkingDir(), _T("Select a folder that should be scanned for VST plugins (including sub-folders)")); + if(!dlg.Show(this)) return; + + TrackerSettings::Instance().PathPlugins.SetWorkingDir(dlg.GetDirectory()); + VSTPluginLib *plugLib = ScanPlugins(dlg.GetDirectory(), this); + UpdatePluginsList(plugLib); + + // If any of the plugins was missing anywhere, try loading it + for(auto p : *theApp.GetPluginManager()) + { + ReloadMissingPlugins(p); + } +} + + +VSTPluginLib *CSelectPluginDlg::ScanPlugins(const mpt::PathString &path, CWnd *parent) +{ + CVstPluginManager *pManager = theApp.GetPluginManager(); + VSTPluginLib *plugLib = nullptr; + bool update = false; + + CDialog pluginScanDlg; + pluginScanDlg.Create(IDD_SCANPLUGINS, parent); + pluginScanDlg.CenterWindow(parent); + pluginScanDlg.ModifyStyle(0, WS_SYSMENU, WS_SYSMENU); + pluginScanDlg.ShowWindow(SW_SHOW); + + FolderScanner scan(path, FolderScanner::kOnlyFiles | FolderScanner::kFindInSubDirectories); + bool maskCrashes = TrackerSettings::Instance().BrokenPluginsWorkaroundVSTMaskAllCrashes; + mpt::PathString fileName; + int files = 0; + while(scan.Next(fileName) && pluginScanDlg.IsWindowVisible()) + { + if(!mpt::PathString::CompareNoCase(fileName.GetFileExt(), P_(".dll"))) + { + CWnd *text = pluginScanDlg.GetDlgItem(IDC_SCANTEXT); + CString scanStr = _T("Scanning Plugin...\n") + fileName.ToCString(); + text->SetWindowText(scanStr); + MSG msg; + while(::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) + { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + VSTPluginLib *lib = pManager->AddPlugin(fileName, maskCrashes, mpt::ustring(), false); + if(lib) + { + update = true; + if(!VerifyPlugin(lib, parent)) + { + pManager->RemovePlugin(lib); + } else + { + plugLib = lib; + files++; + } + } + } + } + + if(update) + { + // Force selection to last added plug. + Reporting::Information(MPT_AFORMAT("Found {} plugin{}.")(files, files == 1 ? "" : "s").c_str(), parent); + return plugLib; + } else + { + Reporting::Error("Could not find any valid VST plugins."); + return nullptr; + } +} + + +// After adding new plugins, check if they were missing in any open songs. +void CSelectPluginDlg::ReloadMissingPlugins(const VSTPluginLib *lib) const +{ + CVstPluginManager *plugManager = theApp.GetPluginManager(); + auto docs = theApp.GetOpenDocuments(); + for(auto &modDoc : docs) + { + CSoundFile &sndFile = modDoc->GetSoundFile(); + bool updateDoc = false; + for(auto &plugin : sndFile.m_MixPlugins) + { + if(plugin.pMixPlugin == nullptr + && plugin.Info.dwPluginId1 == lib->pluginId1 + && plugin.Info.dwPluginId2 == lib->pluginId2) + { + updateDoc = true; + plugManager->CreateMixPlugin(plugin, sndFile); + if(plugin.pMixPlugin) + { + plugin.pMixPlugin->RestoreAllParameters(plugin.defaultProgram); + } + } + } + if(updateDoc) + { + modDoc->UpdateAllViews(nullptr, PluginHint().Info().Names()); + CMainFrame::GetMainFrame()->UpdateTree(modDoc, PluginHint().Info().Names()); + } + } +} + + +void CSelectPluginDlg::OnRemovePlugin() +{ + const HTREEITEM pluginToDelete = m_treePlugins.GetSelectedItem(); + VSTPluginLib *plugin = GetSelectedPlugin(); + CVstPluginManager *plugManager = theApp.GetPluginManager(); + + if(plugManager && plugin) + { + if(plugManager->RemovePlugin(plugin)) + { + m_treePlugins.DeleteItem(pluginToDelete); + } + } +} + + +void CSelectPluginDlg::OnSetBridge() +{ + VSTPluginLib *plug = GetSelectedPlugin(); + if(plug) + { + if(m_chkBridge.IsWindowEnabled()) + { + // Only update this setting if the current setting isn't an enforced setting (e.g. because plugin isn't native). + // This has the advantage that plugins don't get force-checked when switching between 32-bit and 64-bit versions of OpenMPT. + plug->useBridge = m_chkBridge.GetCheck() != BST_UNCHECKED; + } + m_chkShare.EnableWindow(m_chkBridge.GetCheck() != BST_UNCHECKED); + m_chkLegacyBridge.EnableWindow(m_chkBridge.GetCheck() != BST_UNCHECKED); + plug->shareBridgeInstance = m_chkShare.GetCheck() != BST_UNCHECKED; + plug->modernBridge = m_chkLegacyBridge.GetCheck() == BST_UNCHECKED; + plug->WriteToCache(); + } +} + + +void CSelectPluginDlg::OnPluginTagsChanged() +{ + VSTPluginLib *plug = GetSelectedPlugin(); + if (plug) + { + plug->tags = GetWindowTextUnicode(*GetDlgItem(IDC_PLUGINTAGS)); + } +} + + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.h new file mode 100644 index 00000000..e1f49f7d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/SelectPluginDialog.h @@ -0,0 +1,92 @@ +/* + * SelectPluginDialog.h + * -------------------- + * Purpose: Dialog for adding plugins to a song. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "ResizableDialog.h" +#include "../common/ComponentManager.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +struct SNDMIXPLUGIN; +struct VSTPluginLib; +class ComponentPluginBridge_x86; +class ComponentPluginBridgeLegacy_x86; +class ComponentPluginBridge_amd64; +class ComponentPluginBridgeLegacy_amd64; +#if defined(MPT_WITH_WINDOWS10) +class ComponentPluginBridge_arm; +class ComponentPluginBridgeLegacy_arm; +class ComponentPluginBridge_arm64; +class ComponentPluginBridgeLegacy_arm64; +#endif // MPT_WITH_WINDOWS10 + +class CSelectPluginDlg : public ResizableDialog +{ +protected: + SNDMIXPLUGIN *m_pPlugin = nullptr; + CModDoc *m_pModDoc = nullptr; + CTreeCtrl m_treePlugins; + CButton m_chkBridge; + CButton m_chkShare; + CButton m_chkLegacyBridge; + mpt::ustring m_nameFilter; +#ifdef MPT_WITH_VST + ComponentHandle<ComponentPluginBridge_x86> pluginBridge_x86; + ComponentHandle<ComponentPluginBridgeLegacy_x86> pluginBridgeLegacy_x86; + ComponentHandle<ComponentPluginBridge_amd64> pluginBridge_amd64; + ComponentHandle<ComponentPluginBridgeLegacy_amd64> pluginBridgeLegacy_amd64; +#if defined(MPT_WITH_WINDOWS10) + ComponentHandle<ComponentPluginBridge_arm> pluginBridge_arm; + ComponentHandle<ComponentPluginBridgeLegacy_arm> pluginBridgeLegacy_arm; + ComponentHandle<ComponentPluginBridge_arm64> pluginBridge_arm64; + ComponentHandle<ComponentPluginBridgeLegacy_arm64> pluginBridgeLegacy_arm64; +#endif // MPT_WITH_WINDOWS10 +#endif // !MPT_WITH_VST + PLUGINDEX m_nPlugSlot = 0; + +public: + CSelectPluginDlg(CModDoc *pModDoc, PLUGINDEX pluginSlot, CWnd *parent); + ~CSelectPluginDlg(); + + static VSTPluginLib *ScanPlugins(const mpt::PathString &path, CWnd *parent); + static bool VerifyPlugin(VSTPluginLib *plug, CWnd *parent); + +protected: + HTREEITEM AddTreeItem(const TCHAR *title, int image, bool sort, HTREEITEM hParent = TVI_ROOT, LPARAM lParam = NULL); + + VSTPluginLib *GetSelectedPlugin(); + void SaveWindowPos() const; + + void ReloadMissingPlugins(const VSTPluginLib *lib) const; + + void UpdatePluginsList(const VSTPluginLib *forceSelect = nullptr); + + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + void OnCancel() override; + BOOL PreTranslateMessage(MSG *pMsg) override; + + DECLARE_MESSAGE_MAP() + afx_msg void OnAddPlugin(); + afx_msg void OnScanFolder(); + afx_msg void OnRemovePlugin(); + afx_msg void OnNameFilterChanged(); + afx_msg void OnSetBridge(); + afx_msg void OnSelChanged(NMHDR *pNotifyStruct, LRESULT *result); + afx_msg void OnSelDblClk(NMHDR *pNotifyStruct, LRESULT *result); + afx_msg void OnPluginTagsChanged(); +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Settings.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Settings.cpp new file mode 100644 index 00000000..a5715c88 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Settings.cpp @@ -0,0 +1,523 @@ +/* + * Settings.cpp + * ------------ + * Purpose: Application setting handling framework. + * Notes : (currently none) + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#include "Settings.h" + +#include "mpt/binary/hex.hpp" + +#include "../common/misc_util.h" +#include "../common/mptStringBuffer.h" +#include "Mptrack.h" +#include "Mainfrm.h" + +#include <algorithm> +#include "../common/mptFileIO.h" + + +OPENMPT_NAMESPACE_BEGIN + + +mpt::ustring SettingValue::FormatTypeAsString() const +{ + if(GetType() == SettingTypeNone) + { + return U_("nil"); + } + mpt::ustring result; + switch(GetType()) + { + case SettingTypeBool: + result += U_("bool"); + break; + case SettingTypeInt: + result += U_("int"); + break; + case SettingTypeFloat: + result += U_("float"); + break; + case SettingTypeString: + result += U_("string"); + break; + case SettingTypeBinary: + result += U_("binary"); + break; + case SettingTypeNone: + default: + result += U_("nil"); + break; + } + if(HasTypeTag() && !GetTypeTag().empty()) + { + result += U_(":") + mpt::ToUnicode(mpt::Charset::ASCII, GetTypeTag()); + } + return result; +} + + +mpt::ustring SettingValue::FormatValueAsString() const +{ + switch(GetType()) + { + case SettingTypeBool: + return mpt::ufmt::val(as<bool>()); + break; + case SettingTypeInt: + return mpt::ufmt::val(as<int32>()); + break; + case SettingTypeFloat: + return mpt::ufmt::val(as<double>()); + break; + case SettingTypeString: + return as<mpt::ustring>(); + break; + case SettingTypeBinary: + return mpt::encode_hex(mpt::as_span(as<std::vector<std::byte>>())); + break; + case SettingTypeNone: + default: + return mpt::ustring(); + break; + } +} + + +void SettingValue::SetFromString(const AnyStringLocale &newVal) +{ + switch(GetType()) + { + case SettingTypeBool: + value = ConvertStrTo<bool>(newVal); + break; + case SettingTypeInt: + value = ConvertStrTo<int32>(newVal); + break; + case SettingTypeFloat: + value = ConvertStrTo<double>(newVal); + break; + case SettingTypeString: + value = newVal; + break; + case SettingTypeBinary: + value = mpt::decode_hex(newVal); + break; + case SettingTypeNone: + default: + break; + } +} + + +SettingValue SettingsContainer::BackendsReadSetting(const SettingPath &path, const SettingValue &def) const +{ + return backend->ReadSetting(path, def); +} + +void SettingsContainer::BackendsWriteSetting(const SettingPath &path, const SettingValue &val) +{ + backend->WriteSetting(path, val); +} + +void SettingsContainer::BackendsRemoveSetting(const SettingPath &path) +{ + backend->RemoveSetting(path); +} + +void SettingsContainer::BackendsRemoveSection(const mpt::ustring §ion) +{ + backend->RemoveSection(section); +} + +SettingValue SettingsContainer::ReadSetting(const SettingPath &path, const SettingValue &def) const +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + auto entry = map.find(path); + if(entry == map.end()) + { + entry = map.insert(map.begin(), std::make_pair(path, SettingState(def).assign(BackendsReadSetting(path, def), false))); + } + return entry->second; +} + +bool SettingsContainer::IsDefaultSetting(const SettingPath &path) const +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + auto entry = map.find(path); + if(entry == map.end()) + { + return true; + } + return entry->second.IsDefault(); +} + +void SettingsContainer::WriteSetting(const SettingPath &path, const SettingValue &val, SettingFlushMode flushMode) +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + auto entry = map.find(path); + if(entry == map.end()) + { + map[path] = val; + entry = map.find(path); + } else + { + entry->second = val; + } + NotifyListeners(path); + if(immediateFlush || flushMode == SettingWriteThrough) + { + BackendsWriteSetting(path, val); + entry->second.Clean(); + } +} + +void SettingsContainer::ForgetSetting(const SettingPath &path) +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + map.erase(path); +} + +void SettingsContainer::ForgetAll() +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + map.clear(); +} + +void SettingsContainer::RemoveSetting(const SettingPath &path) +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + map.erase(path); + BackendsRemoveSetting(path); +} + +void SettingsContainer::RemoveSection(const mpt::ustring §ion) +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + std::vector<SettingPath> pathsToRemove; + for(const auto &entry : map) + { + if(entry.first.GetSection() == section) + { + pathsToRemove.push_back(entry.first); + } + } + for(const auto &path : pathsToRemove) + { + map.erase(path); + } + BackendsRemoveSection(section); +} + +void SettingsContainer::NotifyListeners(const SettingPath &path) +{ + const auto entry = mapListeners.find(path); + if(entry != mapListeners.end()) + { + for(auto &it : entry->second) + { + it->SettingChanged(path); + } + } +} + +void SettingsContainer::WriteSettings() +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + for(auto &[path, value] : map) + { + if(value.IsDirty()) + { + BackendsWriteSetting(path, value); + value.Clean(); + } + } +} + +void SettingsContainer::Flush() +{ + ASSERT(theApp.InGuiThread()); + ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. + WriteSettings(); +} + +void SettingsContainer::SetImmediateFlush(bool newImmediateFlush) +{ + if(newImmediateFlush) + { + Flush(); + } + immediateFlush = newImmediateFlush; +} + +void SettingsContainer::Register(ISettingChanged *listener, const SettingPath &path) +{ + mapListeners[path].insert(listener); +} + +void SettingsContainer::UnRegister(ISettingChanged *listener, const SettingPath &path) +{ + mapListeners[path].erase(listener); +} + +SettingsContainer::~SettingsContainer() +{ + WriteSettings(); +} + + +SettingsContainer::SettingsContainer(ISettingsBackend *backend) + : backend(backend) +{ + MPT_ASSERT(backend); +} + + + + +std::vector<std::byte> IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const +{ + std::vector<std::byte> result = def; + if(!mpt::in_range<UINT>(result.size())) + { + return result; + } + ::GetPrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), result.data(), static_cast<UINT>(result.size()), filename.AsNative().c_str()); + return result; +} + +mpt::ustring IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const +{ + std::vector<TCHAR> buf(128); + while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(def).c_str(), buf.data(), static_cast<DWORD>(buf.size()), filename.AsNative().c_str()) == buf.size() - 1) + { + if(buf.size() == std::numeric_limits<DWORD>::max()) + { + return def; + } + buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits<DWORD>::max())); + } + return mpt::ToUnicode(mpt::winstring(buf.data())); +} + +double IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, double def) const +{ + std::vector<TCHAR> buf(128); + while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(def).c_str(), buf.data(), static_cast<DWORD>(buf.size()), filename.AsNative().c_str()) == buf.size() - 1) + { + if(buf.size() == std::numeric_limits<DWORD>::max()) + { + return def; + } + buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits<DWORD>::max())); + } + return ConvertStrTo<double>(mpt::winstring(buf.data())); +} + +int32 IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, int32 def) const +{ + return (int32)::GetPrivateProfileInt(GetSection(path).c_str(), GetKey(path).c_str(), (UINT)def, filename.AsNative().c_str()); +} + +bool IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, bool def) const +{ + return ::GetPrivateProfileInt(GetSection(path).c_str(), GetKey(path).c_str(), def?1:0, filename.AsNative().c_str()) ? true : false; +} + + +void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, const std::vector<std::byte> &val) +{ + MPT_ASSERT(mpt::in_range<UINT>(val.size())); + ::WritePrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), (LPVOID)val.data(), static_cast<UINT>(val.size()), filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, const mpt::ustring &val) +{ + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(val).c_str(), filename.AsNative().c_str()); + + if(mpt::ToUnicode(mpt::Charset::Locale, mpt::ToCharset(mpt::Charset::Locale, val)) != val) // explicit round-trip + { + // Value is not representable in ANSI CP. + // Now check if the string got stored correctly. + if(ReadSettingRaw(path, mpt::ustring()) != val) + { + // The ini file is probably ANSI encoded. + ConvertToUnicode(); + // Re-write non-ansi-representable value. + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(val).c_str(), filename.AsNative().c_str()); + } + } +} + +void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, double val) +{ + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, int32 val) +{ + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, bool val) +{ + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::RemoveSettingRaw(const SettingPath &path) +{ + ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), NULL, filename.AsNative().c_str()); +} + +void IniFileSettingsBackend::RemoveSectionRaw(const mpt::ustring §ion) +{ + ::WritePrivateProfileSection(mpt::ToWin(section).c_str(), _T("\0"), filename.AsNative().c_str()); +} + + +mpt::winstring IniFileSettingsBackend::GetSection(const SettingPath &path) +{ + return mpt::ToWin(path.GetSection()); +} +mpt::winstring IniFileSettingsBackend::GetKey(const SettingPath &path) +{ + return mpt::ToWin(path.GetKey()); +} + + + +IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename) + : filename(filename) +{ + return; +} + +IniFileSettingsBackend::~IniFileSettingsBackend() +{ + return; +} + +static std::vector<char> ReadFile(const mpt::PathString &filename) +{ + mpt::ifstream s(filename, std::ios::binary); + std::vector<char> result; + while(s) + { + char buf[4096]; + s.read(buf, 4096); + std::streamsize count = s.gcount(); + result.insert(result.end(), buf, buf + count); + } + return result; +} + +static void WriteFileUTF16LE(const mpt::PathString &filename, const std::wstring &str) +{ + static_assert(sizeof(wchar_t) == 2); + mpt::SafeOutputFile sinifile(filename, std::ios::binary, mpt::FlushMode::Full); + mpt::ofstream& inifile = sinifile; + const uint8 UTF16LE_BOM[] = { 0xff, 0xfe }; + inifile.write(reinterpret_cast<const char*>(UTF16LE_BOM), 2); + inifile.write(reinterpret_cast<const char*>(str.c_str()), str.length() * sizeof(std::wstring::value_type)); +} + +void IniFileSettingsBackend::ConvertToUnicode(const mpt::ustring &backupTag) +{ + // Force ini file to be encoded in UTF16. + // This causes WINAPI ini file functions to keep it in UTF16 encoding + // and thus support storing unicode strings uncorrupted. + // This is backwards compatible because even ANSI WINAPI behaves the + // same way in this case. + const std::vector<char> data = ReadFile(filename); + if(!data.empty() && IsTextUnicode(data.data(), mpt::saturate_cast<int>(data.size()), NULL)) + { + return; + } + const mpt::PathString backupFilename = filename + mpt::PathString::FromUnicode(backupTag.empty() ? U_(".ansi.bak") : U_(".ansi.") + backupTag + U_(".bak")); + CopyFile(filename.AsNative().c_str(), backupFilename.AsNative().c_str(), FALSE); + WriteFileUTF16LE(filename, mpt::ToWide(mpt::Charset::Locale, mpt::buffer_cast<std::string>(data))); +} + +SettingValue IniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const +{ + switch(def.GetType()) + { + case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as<bool>()), def.GetTypeTag()); break; + case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as<int32>()), def.GetTypeTag()); break; + case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as<double>()), def.GetTypeTag()); break; + case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as<mpt::ustring>()), def.GetTypeTag()); break; + case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as<std::vector<std::byte> >()), def.GetTypeTag()); break; + default: return SettingValue(); break; + } +} + +void IniFileSettingsBackend::WriteSetting(const SettingPath &path, const SettingValue &val) +{ + ASSERT(val.GetType() != SettingTypeNone); + switch(val.GetType()) + { + case SettingTypeBool: WriteSettingRaw(path, val.as<bool>()); break; + case SettingTypeInt: WriteSettingRaw(path, val.as<int32>()); break; + case SettingTypeFloat: WriteSettingRaw(path, val.as<double>()); break; + case SettingTypeString: WriteSettingRaw(path, val.as<mpt::ustring>()); break; + case SettingTypeBinary: WriteSettingRaw(path, val.as<std::vector<std::byte> >()); break; + default: break; + } +} + +void IniFileSettingsBackend::RemoveSetting(const SettingPath &path) +{ + RemoveSettingRaw(path); +} + +void IniFileSettingsBackend::RemoveSection(const mpt::ustring §ion) +{ + RemoveSectionRaw(section); +} + + + + + +IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename) + : IniFileSettingsBackend(filename) + , SettingsContainer(this) +{ + return; +} + +IniFileSettingsContainer::~IniFileSettingsContainer() +{ + return; +} + + + +DefaultSettingsContainer::DefaultSettingsContainer() + : IniFileSettingsContainer(theApp.GetConfigFileName()) +{ + return; +} + +DefaultSettingsContainer::~DefaultSettingsContainer() +{ + return; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Settings.h b/Src/external_dependencies/openmpt-trunk/mptrack/Settings.h new file mode 100644 index 00000000..12549853 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Settings.h @@ -0,0 +1,768 @@ +/* + * Settings.h + * ---------- + * Purpose: Header file for application setting handling framework. + * Notes : (currently none) + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + + +#include "../common/misc_util.h" +#include "mpt/mutex/mutex.hpp" + +#include <map> +#include <set> +#include <variant> + + +OPENMPT_NAMESPACE_BEGIN + + +enum SettingType +{ + SettingTypeNone, + SettingTypeBool, + SettingTypeInt, + SettingTypeFloat, + SettingTypeString, + SettingTypeBinary, +}; + +// SettingValue is a variant type that stores any type that can natively be represented in a config backend. +// Any other type that should be stored must provide a matching ToSettingValue and FromSettingValue. +// Other types can optionally also set a type tag which would get checked in debug builds. +class SettingValue +{ +private: + std::variant<std::monostate, bool, int32, double, mpt::ustring, std::vector<std::byte>> value; + std::string typeTag; +public: + bool operator == (const SettingValue &other) const + { + return value == other.value && typeTag == other.typeTag; + } + bool operator != (const SettingValue &other) const + { + return !(*this == other); + } + SettingValue() + { + } + SettingValue(const SettingValue &other) + { + *this = other; + } + SettingValue & operator = (const SettingValue &other) + { + if(this == &other) + { + return *this; + } + MPT_ASSERT(value.index() == 0 || (value.index() == other.value.index() && typeTag == other.typeTag)); + value = other.value; + typeTag = other.typeTag; + return *this; + } + SettingValue(bool val) + : value(val) + { + } + SettingValue(int32 val) + : value(val) + { + } + SettingValue(double val) + : value(val) + { + } + SettingValue(const mpt::ustring &val) + : value(val) + { + } + SettingValue(const std::vector<std::byte> &val) + : value(val) + { + } + SettingValue(bool val, const std::string &typeTag_) + : value(val) + , typeTag(typeTag_) + { + } + SettingValue(int32 val, const std::string &typeTag_) + : value(val) + , typeTag(typeTag_) + { + } + SettingValue(double val, const std::string &typeTag_) + : value(val) + , typeTag(typeTag_) + { + } + SettingValue(const mpt::ustring &val, const std::string &typeTag_) + : value(val) + , typeTag(typeTag_) + { + } + SettingValue(const std::vector<std::byte> &val, const std::string &typeTag_) + : value(val) + , typeTag(typeTag_) + { + } + // these need to be explicitly deleted because otherwise the bool overload will catch the pointers + SettingValue(const char *val) = delete; + SettingValue(const wchar_t *val) = delete; + SettingValue(const char *val, const std::string &typeTag_) = delete; + SettingValue(const wchar_t *val, const std::string &typeTag_) = delete; + SettingType GetType() const + { + SettingType result = SettingTypeNone; + if(std::holds_alternative<bool>(value)) + { + result = SettingTypeBool; + } + if(std::holds_alternative<int32>(value)) + { + result = SettingTypeInt; + } + if(std::holds_alternative<double>(value)) + { + result = SettingTypeFloat; + } + if(std::holds_alternative<mpt::ustring>(value)) + { + result = SettingTypeString; + } + if(std::holds_alternative<std::vector<std::byte>>(value)) + { + result = SettingTypeBinary; + } + return result; + } + bool HasTypeTag() const + { + return !typeTag.empty(); + } + std::string GetTypeTag() const + { + return typeTag; + } + template <typename T> + T as() const + { + return *this; + } + operator bool () const + { + MPT_ASSERT(std::holds_alternative<bool>(value)); + return std::get<bool>(value); + } + operator int32 () const + { + MPT_ASSERT(std::holds_alternative<int32>(value)); + return std::get<int32>(value); + } + operator double () const + { + MPT_ASSERT(std::holds_alternative<double>(value)); + return std::get<double>(value); + } + operator mpt::ustring () const + { + MPT_ASSERT(std::holds_alternative<mpt::ustring>(value)); + return std::get<mpt::ustring>(value); + } + operator std::vector<std::byte> () const + { + MPT_ASSERT(std::holds_alternative<std::vector<std::byte>>(value)); + return std::get<std::vector<std::byte>>(value); + } + mpt::ustring FormatTypeAsString() const; + mpt::ustring FormatValueAsString() const; + void SetFromString(const AnyStringLocale &newVal); +}; + + +template<typename T> +std::vector<std::byte> EncodeBinarySetting(const T &val) +{ + std::vector<std::byte> result(sizeof(T)); + std::memcpy(result.data(), &val, sizeof(T)); + return result; +} +template<typename T> +T DecodeBinarySetting(const std::vector<std::byte> &val) +{ + T result = T(); + if(val.size() >= sizeof(T)) + { + std::memcpy(&result, val.data(), sizeof(T)); + } + return result; +} + + +template<typename T> +inline SettingValue ToSettingValue(const T &val) +{ + return SettingValue(val); +} + +template<typename T> +inline T FromSettingValue(const SettingValue &val) +{ + return val.as<T>(); +} + +// To support settings.Read<Tcustom> and settings.Write<Tcustom>, +// just provide specializations of ToSettingsValue<Tcustom> and FromSettingValue<Tcustom>. +// You may use the SettingValue(value, typeTag) constructor in ToSettingValue +// and check the typeTag FromSettingsValue to implement runtime type-checking for custom types. + +template<> inline SettingValue ToSettingValue(const std::string &val) { return SettingValue(mpt::ToUnicode(mpt::Charset::Locale, val)); } +template<> inline std::string FromSettingValue(const SettingValue &val) { return mpt::ToCharset(mpt::Charset::Locale, val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const mpt::lstring &val) { return SettingValue(mpt::ToUnicode(val)); } +template<> inline mpt::lstring FromSettingValue(const SettingValue &val) { return mpt::ToLocale(val.as<mpt::ustring>()); } + +#if !MPT_USTRING_MODE_WIDE +template<> inline SettingValue ToSettingValue(const std::wstring &val) { return SettingValue(mpt::ToUnicode(val)); } +template<> inline std::wstring FromSettingValue(const SettingValue &val) { return mpt::ToWide(val.as<mpt::ustring>()); } +#endif + +template<> inline SettingValue ToSettingValue(const CString &val) { return SettingValue(mpt::ToUnicode(val)); } +template<> inline CString FromSettingValue(const SettingValue &val) { return mpt::ToCString(val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const mpt::PathString &val) { return SettingValue(val.ToUnicode()); } +template<> inline mpt::PathString FromSettingValue(const SettingValue &val) { return mpt::PathString::FromUnicode(val); } + +template<> inline SettingValue ToSettingValue(const float &val) { return SettingValue(double(val)); } +template<> inline float FromSettingValue(const SettingValue &val) { return float(val.as<double>()); } + +template<> inline SettingValue ToSettingValue(const int64 &val) { return SettingValue(mpt::ufmt::dec(val), "int64"); } +template<> inline int64 FromSettingValue(const SettingValue &val) { return ConvertStrTo<int64>(val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const uint64 &val) { return SettingValue(mpt::ufmt::dec(val), "uint64"); } +template<> inline uint64 FromSettingValue(const SettingValue &val) { return ConvertStrTo<uint64>(val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const uint32 &val) { return SettingValue(int32(val)); } +template<> inline uint32 FromSettingValue(const SettingValue &val) { return uint32(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const uint16 &val) { return SettingValue(int32(val)); } +template<> inline uint16 FromSettingValue(const SettingValue &val) { return uint16(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const uint8 &val) { return SettingValue(int32(val)); } +template<> inline uint8 FromSettingValue(const SettingValue &val) { return uint8(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const LONG &val) { return SettingValue(int32(val)); } +template<> inline LONG FromSettingValue(const SettingValue &val) { return LONG(val.as<int32>()); } + + +// An instance of SetttingState represents the cached on-disk state of a certain SettingPath. +// The mapping is stored externally in SettingsContainer::map. +class SettingState +{ +private: + SettingValue value; + const SettingValue defaultValue; + bool dirty; +public: + SettingState() + : dirty(false) + { + return; + } + SettingState(const SettingValue &def) + : value(def) + , defaultValue(def) + , dirty(false) + { + return; + } + SettingState & assign(const SettingValue &other, bool setDirty = true) + { + MPT_ASSERT(defaultValue.GetType() == SettingTypeNone || (defaultValue.GetType() == other.GetType() && defaultValue.GetTypeTag() == other.GetTypeTag())); + if(setDirty) + { + if(value != other) + { + value = other; + dirty = true; + } + } else + { + value = other; + } + return *this; + } + SettingState & operator = (const SettingValue &val) + { + assign(val); + return *this; + } + + SettingValue GetDefault() const + { + return defaultValue; + } + const SettingValue &GetRefDefault() const + { + return defaultValue; + } + bool IsDefault() const + { + return value == defaultValue; + } + + bool IsDirty() const + { + return dirty; + } + void Clean() + { + dirty = false; + } + SettingValue GetValue() const + { + return value; + } + const SettingValue &GetRefValue() const + { + return value; + } + operator SettingValue () const + { + return value; + } +}; + + +// SettingPath represents the path in a config backend to a certain setting. +class SettingPath +{ +private: + mpt::ustring section; + mpt::ustring key; +public: + SettingPath() + { + return; + } + SettingPath(mpt::ustring section_, mpt::ustring key_) + : section(std::move(section_)) + , key(std::move(key_)) + { + return; + } + mpt::ustring GetSection() const + { + return section; + } + mpt::ustring GetKey() const + { + return key; + } + const mpt::ustring &GetRefSection() const + { + return section; + } + const mpt::ustring &GetRefKey() const + { + return key; + } + int compare(const SettingPath &other) const + { + int cmp_section = section.compare(other.section); + if(cmp_section) + { + return cmp_section; + } + int cmp_key = key.compare(other.key); + return cmp_key; + } + mpt::ustring FormatAsString() const + { + return section + U_(".") + key; + } +}; + +inline bool operator < (const SettingPath &left, const SettingPath &right) { return left.compare(right) < 0; } +inline bool operator <= (const SettingPath &left, const SettingPath &right) { return left.compare(right) <= 0; } +inline bool operator > (const SettingPath &left, const SettingPath &right) { return left.compare(right) > 0; } +inline bool operator >= (const SettingPath &left, const SettingPath &right) { return left.compare(right) >= 0; } +inline bool operator == (const SettingPath &left, const SettingPath &right) { return left.compare(right) == 0; } +inline bool operator != (const SettingPath &left, const SettingPath &right) { return left.compare(right) != 0; } + + +class ISettingsBackend +{ +public: + virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const = 0; + virtual void WriteSetting(const SettingPath &path, const SettingValue &val) = 0; + virtual void RemoveSetting(const SettingPath &path) = 0; + virtual void RemoveSection(const mpt::ustring §ion) = 0; +protected: + virtual ~ISettingsBackend() = default; +}; + + +class ISettingChanged +{ +public: + virtual void SettingChanged(const SettingPath &changedPath) = 0; +protected: + virtual ~ISettingChanged() = default; +}; + +enum SettingFlushMode +{ + SettingWriteBack = 0, + SettingWriteThrough = 1, +}; + +// SettingContainer basically represents a frontend to 1 or 2 backends (e.g. ini files or registry subtrees) for a collection of configuration settings. +// SettingContainer provides basic read/write access to individual setting. The values are cached and only flushed on destruction or explicit flushs. +class SettingsContainer +{ + +public: + using SettingsMap = std::map<SettingPath,SettingState>; + using SettingsListenerMap = std::map<SettingPath,std::set<ISettingChanged*>>; + void WriteSettings(); +private: + mutable SettingsMap map; + mutable SettingsListenerMap mapListeners; + +private: + ISettingsBackend *backend; +private: + bool immediateFlush = false; + SettingValue BackendsReadSetting(const SettingPath &path, const SettingValue &def) const; + void BackendsWriteSetting(const SettingPath &path, const SettingValue &val); + void BackendsRemoveSetting(const SettingPath &path); + void BackendsRemoveSection(const mpt::ustring §ion); + void NotifyListeners(const SettingPath &path); + SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const; + bool IsDefaultSetting(const SettingPath &path) const; + void WriteSetting(const SettingPath &path, const SettingValue &val, SettingFlushMode flushMode); + void ForgetSetting(const SettingPath &path); + void RemoveSetting(const SettingPath &path); + void RemoveSection(const mpt::ustring §ion); +private: + SettingsContainer(const SettingsContainer &other); // disable + SettingsContainer& operator = (const SettingsContainer &other); // disable +public: + SettingsContainer(ISettingsBackend *backend); + void SetImmediateFlush(bool newImmediateFlush); + template <typename T> + T Read(const SettingPath &path, const T &def = T()) const + { + return FromSettingValue<T>(ReadSetting(path, ToSettingValue<T>(def))); + } + template <typename T> + T Read(mpt::ustring section, mpt::ustring key, const T &def = T()) const + { + return FromSettingValue<T>(ReadSetting(SettingPath(std::move(section), std::move(key)), ToSettingValue<T>(def))); + } + bool IsDefault(const SettingPath &path) const + { + return IsDefaultSetting(path); + } + bool IsDefault(mpt::ustring section, mpt::ustring key) const + { + return IsDefaultSetting(SettingPath(std::move(section), std::move(key))); + } + template <typename T> + void Write(const SettingPath &path, const T &val, SettingFlushMode flushMode = SettingWriteBack) + { + WriteSetting(path, ToSettingValue<T>(val), flushMode); + } + template <typename T> + void Write(mpt::ustring section, mpt::ustring key, const T &val, SettingFlushMode flushMode = SettingWriteBack) + { + WriteSetting(SettingPath(std::move(section), std::move(key)), ToSettingValue<T>(val), flushMode); + } + void Forget(const SettingPath &path) + { + ForgetSetting(path); + } + void Forget(mpt::ustring section, mpt::ustring key) + { + ForgetSetting(SettingPath(std::move(section), std::move(key))); + } + void ForgetAll(); + void Remove(const SettingPath &path) + { + RemoveSetting(path); + } + void Remove(mpt::ustring section, mpt::ustring key) + { + RemoveSetting(SettingPath(std::move(section), std::move(key))); + } + void Remove(const mpt::ustring §ion) + { + RemoveSection(section); + } + void Flush(); + ~SettingsContainer(); + +public: + + void Register(ISettingChanged *listener, const SettingPath &path); + void UnRegister(ISettingChanged *listener, const SettingPath &path); + + SettingsMap::const_iterator begin() const { return map.begin(); } + SettingsMap::const_iterator end() const { return map.end(); } + SettingsMap::size_type size() const { return map.size(); } + bool empty() const { return map.empty(); } + const SettingsMap &GetMap() const { return map; } + +}; + + +// Setting<T> and CachedSetting<T> are references to a SettingPath below a SettingConainer (both provided to the constructor). +// They should mostly behave like normal non-reference variables of type T. I.e., they can be assigned to and read from. +// As they have actual reference semantics, all Setting<T> or CachedSetting<T> that access the same path consistently have the same value. +// The difference between the 2 lies in the way this consistency is achieved: +// Setting<T>: The actual value is not stored in an instance of Setting<T>. +// Instead, it is read/written and converted on every access from the SettingContainer. +// In the SettingContainer, each SettingPath is mapped to a single instance of SettingValue, so there cannot be any incoherence. +// CachedSetting<T>: The value, readily converted to T, is stored directly in each instance of CachedSetting<T>. +// A callback for its SettingPath is registered with SettingContainer, and on every change to this SettingPath, the value gets re-read and updated. +// Setting<T> implies some overhead on every access but is generally simpler to understand. +// CachedSetting<T> implies overhead in stored (the copy of T and the callback pointers). +// Except for the difference in runtime/space characteristics, Setting<T> and CachedSetting<T> behave exactly the same way. +// It is recommended to only use CachedSetting<T> for settings that get read frequently, i.e. during GUI updates (like in the pattern view). + +template <typename T> +class Setting +{ +private: + SettingsContainer &conf; + const SettingPath path; +public: + Setting(const Setting &other) = delete; + Setting & operator = (const Setting &other) = delete; +public: + Setting(SettingsContainer &conf_, mpt::ustring section, mpt::ustring key, const T&def) + : conf(conf_) + , path(std::move(section), std::move(key)) + { + conf.Read(path, def); // set default value + } + Setting(SettingsContainer &conf_, const SettingPath &path_, const T&def) + : conf(conf_) + , path(path_) + { + conf.Read(path, def); // set default value + } + SettingPath GetPath() const + { + return path; + } + Setting & operator = (const T &val) + { + conf.Write(path, val); + return *this; + } + operator T () const + { + return conf.Read<T>(path); + } + T Get() const + { + return conf.Read<T>(path); + } + bool IsDefault() const + { + return conf.IsDefault(path); + } + template<typename Trhs> Setting & operator += (const Trhs &rhs) { T tmp = *this; tmp += rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator -= (const Trhs &rhs) { T tmp = *this; tmp -= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator *= (const Trhs &rhs) { T tmp = *this; tmp *= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator /= (const Trhs &rhs) { T tmp = *this; tmp /= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator %= (const Trhs &rhs) { T tmp = *this; tmp %= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator |= (const Trhs &rhs) { T tmp = *this; tmp |= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator &= (const Trhs &rhs) { T tmp = *this; tmp &= rhs; *this = tmp; return *this; } + template<typename Trhs> Setting & operator ^= (const Trhs &rhs) { T tmp = *this; tmp ^= rhs; *this = tmp; return *this; } +}; + +template <typename T> +class CachedSetting + : public ISettingChanged +{ +private: + mutable mpt::mutex valueMutex; + T value; + SettingsContainer &conf; + const SettingPath path; +public: + CachedSetting(const CachedSetting &other) = delete; + CachedSetting & operator = (const CachedSetting &other) = delete; +public: + CachedSetting(SettingsContainer &conf_, mpt::ustring section, mpt::ustring key, const T&def) + : value(def) + , conf(conf_) + , path(std::move(section), std::move(key)) + { + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + value = conf.Read(path, def); + } + conf.Register(this, path); + } + CachedSetting(SettingsContainer &conf_, const SettingPath &path_, const T&def) + : value(def) + , conf(conf_) + , path(path_) + { + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + value = conf.Read(path, def); + } + conf.Register(this, path); + } + ~CachedSetting() + { + conf.UnRegister(this, path); + } + SettingPath GetPath() const + { + return path; + } + CachedSetting & operator = (const T &val) + { + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + value = val; + } + conf.Write(path, val); + return *this; + } + operator T () const + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + return value; + } + T Get() const + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + return value; + } + bool IsDefault() const + { + return conf.IsDefault(path); + } + CachedSetting & Update() + { + { + mpt::lock_guard<mpt::mutex> l(valueMutex); + value = conf.Read<T>(path); + } + return *this; + } + void SettingChanged(const SettingPath &changedPath) + { + MPT_UNREFERENCED_PARAMETER(changedPath); + Update(); + } + template<typename Trhs> CachedSetting & operator += (const Trhs &rhs) { T tmp = *this; tmp += rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator -= (const Trhs &rhs) { T tmp = *this; tmp -= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator *= (const Trhs &rhs) { T tmp = *this; tmp *= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator /= (const Trhs &rhs) { T tmp = *this; tmp /= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator %= (const Trhs &rhs) { T tmp = *this; tmp %= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator |= (const Trhs &rhs) { T tmp = *this; tmp |= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator &= (const Trhs &rhs) { T tmp = *this; tmp &= rhs; *this = tmp; return *this; } + template<typename Trhs> CachedSetting & operator ^= (const Trhs &rhs) { T tmp = *this; tmp ^= rhs; *this = tmp; return *this; } +}; + + +class IniFileSettingsBackend : public ISettingsBackend +{ +private: + const mpt::PathString filename; +private: + std::vector<std::byte> ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const; + mpt::ustring ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const; + double ReadSettingRaw(const SettingPath &path, double def) const; + int32 ReadSettingRaw(const SettingPath &path, int32 def) const; + bool ReadSettingRaw(const SettingPath &path, bool def) const; + void WriteSettingRaw(const SettingPath &path, const std::vector<std::byte> &val); + void WriteSettingRaw(const SettingPath &path, const mpt::ustring &val); + void WriteSettingRaw(const SettingPath &path, double val); + void WriteSettingRaw(const SettingPath &path, int32 val); + void WriteSettingRaw(const SettingPath &path, bool val); + void RemoveSettingRaw(const SettingPath &path); + void RemoveSectionRaw(const mpt::ustring §ion); + static mpt::winstring GetSection(const SettingPath &path); + static mpt::winstring GetKey(const SettingPath &path); +public: + IniFileSettingsBackend(const mpt::PathString &filename); + ~IniFileSettingsBackend() override; + void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring()); + virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override; + virtual void WriteSetting(const SettingPath &path, const SettingValue &val) override; + virtual void RemoveSetting(const SettingPath &path) override; + virtual void RemoveSection(const mpt::ustring §ion) override; + const mpt::PathString& GetFilename() const { return filename; } +}; + +class IniFileSettingsContainer : private IniFileSettingsBackend, public SettingsContainer +{ +public: + IniFileSettingsContainer(const mpt::PathString &filename); + ~IniFileSettingsContainer() override; +}; + +class DefaultSettingsContainer : public IniFileSettingsContainer +{ +public: + DefaultSettingsContainer(); + ~DefaultSettingsContainer() override; +}; + + +class SettingChangedNotifyGuard +{ +private: + SettingsContainer &conf; + SettingPath m_Path; + bool m_Registered; + ISettingChanged *m_Handler; +public: + SettingChangedNotifyGuard(SettingsContainer &conf, const SettingPath &path) + : conf(conf) + , m_Path(path) + , m_Registered(false) + , m_Handler(nullptr) + { + return; + } + void Register(ISettingChanged *handler) + { + if(m_Registered) + { + return; + } + m_Handler = handler; + conf.Register(m_Handler, m_Path); + m_Registered = true; + } + ~SettingChangedNotifyGuard() + { + if(m_Registered) + { + conf.UnRegister(m_Handler, m_Path); + m_Registered = false; + } + } +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.cpp new file mode 100644 index 00000000..e6cca7c3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.cpp @@ -0,0 +1,342 @@ +/* + * StreamEncoder.cpp + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" + +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "mpt/io_write/buffer.hpp" + +#include "openmpt/soundbase/SampleEncode.hpp" +#include "openmpt/soundbase/SampleFormat.hpp" + +#include <ostream> + + +OPENMPT_NAMESPACE_BEGIN + + +StreamWriterBase::StreamWriterBase(std::ostream &stream) + : f(stream) + , fStart(f.tellp()) +{ + return; +} + +StreamWriterBase::~StreamWriterBase() +{ + return; +} + + + +template <mpt::endian endian, typename Tsample> +static inline std::pair<bool, std::size_t> WriteInterleavedImpl(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const Tsample *interleaved) +{ + MPT_ASSERT(endian == format.endian); + MPT_ASSERT(format.GetSampleFormat() == SampleFormatTraits<Tsample>::sampleFormat()); + bool success = true; + std::size_t written = 0; + MPT_MAYBE_CONSTANT_IF(endian == mpt::get_endian() && format.encoding != Encoder::Format::Encoding::Alaw && format.encoding != Encoder::Format::Encoding::ulaw) + { + if(!mpt::IO::WriteRaw(f, reinterpret_cast<const std::byte*>(interleaved), frameCount * channels * format.GetSampleFormat().GetSampleSize())) + { + success = false; + } + written += frameCount * channels * format.GetSampleFormat().GetSampleSize(); + } else + { + std::array<std::byte, mpt::IO::BUFFERSIZE_TINY> fbuf; + mpt::IO::WriteBuffer<std::ostream> bf{f, fbuf}; + if(format.encoding == Encoder::Format::Encoding::Alaw) + { + if constexpr(std::is_same<Tsample, int16>::value) + { + SC::EncodeALaw conv; + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(uint16 channel = 0; channel < channels; ++channel) + { + std::byte sampledata = conv(interleaved[channel]); + mpt::IO::WriteRaw(bf, &sampledata, 1); + written += 1; + } + interleaved += channels; + } + } + } else if(format.encoding == Encoder::Format::Encoding::ulaw) + { + if constexpr(std::is_same<Tsample, int16>::value) + { + SC::EncodeuLaw conv; + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(uint16 channel = 0; channel < channels; ++channel) + { + std::byte sampledata = conv(interleaved[channel]); + mpt::IO::WriteRaw(bf, &sampledata, 1); + written += 1; + } + interleaved += channels; + } + } + } else + { + if constexpr(std::is_floating_point<Tsample>::value) + { + std::vector<typename mpt::make_float_endian<endian, Tsample>::type> frameData(channels); + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(uint16 channel = 0; channel < channels; ++channel) + { + frameData[channel] = typename mpt::make_float_endian<endian, Tsample>::type(interleaved[channel]); + } + mpt::IO::WriteRaw(bf, reinterpret_cast<const std::byte*>(frameData.data()), channels * format.GetSampleFormat().GetSampleSize()); + written += channels * format.GetSampleFormat().GetSampleSize(); + interleaved += channels; + } + } else if constexpr(std::is_same<Tsample, int24>::value) + { + MPT_ASSERT(!mpt::endian_is_weird()); + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(uint16 channel = 0; channel < channels; ++channel) + { + std::array<std::byte, 3> data; + std::memcpy(data.data(), interleaved, 3); + MPT_MAYBE_CONSTANT_IF(endian != mpt::get_endian()) + { + std::reverse(data.begin(), data.end()); + } + mpt::IO::Write(bf, data); + written += data.size(); + interleaved += 3; + } + } + } else + { + std::vector<typename mpt::make_endian<endian, Tsample>::type> frameData(channels); + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(uint16 channel = 0; channel < channels; ++channel) + { + frameData[channel] = interleaved[channel]; + } + mpt::IO::WriteRaw(bf, reinterpret_cast<const std::byte*>(frameData.data()), channels * format.GetSampleFormat().GetSampleSize()); + written += channels * format.GetSampleFormat().GetSampleSize(); + interleaved += channels; + } + } + } + if(bf.HasWriteError()) + { + success = false; + } + } + return std::make_pair(success, written); +} + +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const double *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const float *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int32 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int24 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int16 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int8 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const uint8 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::little); + return WriteInterleavedImpl<mpt::endian::little>(f, channels, format, frameCount, interleaved); +} + +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const double *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const float *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int32 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int24 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int16 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int8 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const uint8 *interleaved) +{ + MPT_ASSERT(format.endian == mpt::endian::big); + return WriteInterleavedImpl<mpt::endian::big>(f, channels, format, frameCount, interleaved); +} + + + +SampleFormat StreamWriterBase::GetSampleFormat() const +{ + return SampleFormat::Float32; +} + + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const double *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const float *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const int32 *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const int24 *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const int16 *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const int8 *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + +void StreamWriterBase::WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) +{ + MPT_UNREFERENCED_PARAMETER(frameCount); + MPT_UNREFERENCED_PARAMETER(interleaved); + MPT_ASSERT_NOTREACHED(); +} + + +void StreamWriterBase::WriteCues(const std::vector<uint64> &cues) +{ + MPT_UNREFERENCED_PARAMETER(cues); +} + + +void StreamWriterBase::WriteFinalize() +{ + return; +} + + +void StreamWriterBase::WriteBuffer() +{ + if(!f) + { + return; + } + if(buf.empty()) + { + return; + } + f.write(buf.data(), buf.size()); + buf.resize(0); +} + + +void EncoderFactoryBase::SetTraits(const Encoder::Traits &traits) +{ + m_Traits = traits; +} + + +bool EncoderFactoryBase::IsBitrateSupported(int samplerate, int channels, int bitrate) const +{ + MPT_UNREFERENCED_PARAMETER(samplerate); + MPT_UNREFERENCED_PARAMETER(channels); + MPT_UNREFERENCED_PARAMETER(bitrate); + return true; +} + + +mpt::ustring EncoderFactoryBase::DescribeQuality(float quality) const +{ + return MPT_UFORMAT("VBR {}%")(static_cast<int>(quality * 100.0f)); +} + +mpt::ustring EncoderFactoryBase::DescribeBitrateVBR(int bitrate) const +{ + return MPT_UFORMAT("VBR {} kbit")(bitrate); +} + +mpt::ustring EncoderFactoryBase::DescribeBitrateABR(int bitrate) const +{ + return MPT_UFORMAT("ABR {} kbit")(bitrate); +} + +mpt::ustring EncoderFactoryBase::DescribeBitrateCBR(int bitrate) const +{ + return MPT_UFORMAT("CBR {} kbit")(bitrate); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.h new file mode 100644 index 00000000..a2322c26 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoder.h @@ -0,0 +1,328 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/base/bit.hpp" +#include "openmpt/soundbase/SampleFormat.hpp" +#include "../soundlib/Tagging.h" + +#include <iosfwd> +#include <string> +#include <vector> + + +OPENMPT_NAMESPACE_BEGIN + + + +inline constexpr int opus_bitrates [] = { + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320, 384, 448, 510 +}; +inline constexpr int vorbis_bitrates [] = { + 32, 48, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500 +}; +inline constexpr int layer3_bitrates [] = { + 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 192, 224, 256, 320 +}; +inline constexpr int mpeg1layer3_bitrates [] = { + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320 +}; +inline constexpr uint32 opus_samplerates [] = { + 48000, + 24000, 16000, + 12000, 8000 +}; +inline constexpr uint32 opus_all_samplerates [] = { + 48000, 44100, 32000, + 24000, 22050, 16000, + 12000, 11025, 8000 +}; +inline constexpr uint32 vorbis_samplerates [] = { + 48000, 44100, 32000, + 24000, 22050, 16000, + 12000, 11025, 8000 +}; +inline constexpr uint32 layer3_samplerates [] = { + 48000, 44100, 32000, + 24000, 22050, 16000 +}; +inline constexpr uint32 mpeg1layer3_samplerates [] = { + 48000, 44100, 32000 +}; + + +namespace Encoder +{ + + enum Mode + { + ModeCBR = 1<<0, + ModeABR = 1<<1, + ModeVBR = 1<<2, + ModeQuality = 1<<3, + ModeLossless = 1<<4, + ModeInvalid = 0 + }; + + struct Format + { + enum class Encoding + { + Float = 1, + Integer = 2, + Alaw = 3, + ulaw = 4, + Unsigned = 5, + }; + Encoding encoding; + uint8 bits; + mpt::endian endian; + bool operator==(const Format &other) const + { + return encoding == other.encoding && bits == other.bits && endian == other.endian; + } + bool operator!=(const Format& other) const + { + return encoding != other.encoding || bits != other.bits || endian != other.endian; + } + int32 AsInt() const + { + return (static_cast<int32>(endian == mpt::endian::little) << 16) | (static_cast<int32>(encoding) << 8) | static_cast<int32>(bits); + } + static Format FromInt(int32 val) + { + Encoder::Format f; + f.bits = val & 0xff; + f.encoding = static_cast<Encoder::Format::Encoding>((val >> 8) & 0xff); + f.endian = ((val >> 16) & 0xff) ? mpt::endian::little : mpt::endian::big; + return f; + } + SampleFormat GetSampleFormat() const + { + SampleFormat result = SampleFormat::Invalid; + switch(encoding) + { + case Encoding::Float: + switch(bits) + { + case 32: + result = SampleFormat::Float32; + break; + case 64: + result = SampleFormat::Float64; + break; + } + break; + case Encoding::Integer: + switch(bits) + { + case 8: + result = SampleFormat::Int8; + break; + case 16: + result = SampleFormat::Int16; + break; + case 24: + result = SampleFormat::Int24; + break; + case 32: + result = SampleFormat::Int32; + break; + } + break; + case Encoding::Alaw: + switch (bits) + { + case 16: + result = SampleFormat::Int16; + break; + } + break; + case Encoding::ulaw: + switch (bits) + { + case 16: + result = SampleFormat::Int16; + break; + } + break; + case Encoding::Unsigned: + switch (bits) + { + case 8: + result = SampleFormat::Unsigned8; + break; + } + break; + } + return result; + } + }; + + struct Traits + { + + mpt::PathString fileExtension; + mpt::ustring fileShortDescription; + mpt::ustring encoderSettingsName; + + mpt::ustring fileDescription; + + bool canTags = false; + std::vector<mpt::ustring> genres; + int modesWithFixedGenres = 0; + + bool canCues = false; + + int maxChannels = 0; + std::vector<uint32> samplerates; + + int modes = Encoder::ModeInvalid; + std::vector<int> bitrates; + std::vector<Format> formats; + + uint32 defaultSamplerate = 48000; + uint16 defaultChannels = 2; + + Encoder::Mode defaultMode = Encoder::ModeInvalid; + int defaultBitrate = 0; + float defaultQuality = 0.0f; + Format defaultFormat = { Encoder::Format::Encoding::Float, 32, mpt::endian::little }; + int defaultDitherType = 1; + }; + + struct StreamSettings + { + int32 FLACCompressionLevel = 5; // 8 + uint32 AUPaddingAlignHint = 4096; + uint32 MP3ID3v2MinPadding = 1024; + uint32 MP3ID3v2PaddingAlignHint = 4096; + bool MP3ID3v2WriteReplayGainTXXX = true; + int32 MP3LameQuality = 3; // 0 + bool MP3LameID3v2UseLame = false; + bool MP3LameCalculateReplayGain = true; + bool MP3LameCalculatePeakSample = true; + int32 OpusComplexity = -1; // 10 + }; + + struct Settings + { + + bool Cues; + bool Tags; + + uint32 Samplerate; + uint16 Channels; + + Encoder::Mode Mode; + int Bitrate; + float Quality; + Encoder::Format Format; + int Dither; + + StreamSettings Details; + + }; + +} // namespace Encoder + + +class IAudioStreamEncoder +{ +protected: + IAudioStreamEncoder() { } +public: + virtual ~IAudioStreamEncoder() = default; +public: + virtual SampleFormat GetSampleFormat() const = 0; + virtual void WriteInterleaved(std::size_t frameCount, const double *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const float *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const int32 *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) = 0; + virtual void WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) = 0; + virtual void WriteCues(const std::vector<uint64> &cues) = 0; // optional + virtual void WriteFinalize() = 0; +}; + + + +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const double *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const float *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int32 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int24 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int16 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int8 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedLE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const uint8 *interleaved); + +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const double *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const float *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int32 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int24 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int16 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const int8 *interleaved); +std::pair<bool, std::size_t> WriteInterleavedBE(std::ostream &f, uint16 channels, Encoder::Format format, std::size_t frameCount, const uint8 *interleaved); + + + +class StreamWriterBase + : public IAudioStreamEncoder +{ +protected: + std::ostream &f; + std::streampos fStart; + std::vector<char> buf; +public: + StreamWriterBase(std::ostream &stream); + virtual ~StreamWriterBase(); +public: + SampleFormat GetSampleFormat() const override; + void WriteInterleaved(std::size_t frameCount, const double *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const float *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const int32 *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) override; + void WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) override; + void WriteCues(const std::vector<uint64> &cues) override; + void WriteFinalize() override; +protected: + void WriteBuffer(); +}; + + +class EncoderFactoryBase +{ +private: + Encoder::Traits m_Traits; +protected: + EncoderFactoryBase() { } + virtual ~EncoderFactoryBase() = default; + void SetTraits(const Encoder::Traits &traits); +public: + virtual std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const = 0; + const Encoder::Traits &GetTraits() const + { + return m_Traits; + } + virtual bool IsBitrateSupported(int samplerate, int channels, int bitrate) const; + virtual mpt::ustring DescribeQuality(float quality) const; + virtual mpt::ustring DescribeBitrateVBR(int bitrate) const; + virtual mpt::ustring DescribeBitrateABR(int bitrate) const; + virtual mpt::ustring DescribeBitrateCBR(int bitrate) const; + virtual bool IsAvailable() const = 0; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.cpp new file mode 100644 index 00000000..c1005b0a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.cpp @@ -0,0 +1,216 @@ +/* + * StreamEncoderAU.cpp + * ------------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderAU.h" + +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#include "Mptrack.h" +#include "TrackerSettings.h" + +#include "../common/mptFileIO.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class AUStreamWriter : public IAudioStreamEncoder +{ +private: + + const AUEncoder &enc; + std::ostream &f; + Encoder::Settings settings; + +private: + static std::string TagToAnnotation(const std::string & field, const mpt::ustring & tag) + { + if(tag.empty()) + { + return std::string(); + } + return MPT_AFORMAT("{}={}\n")(field, mpt::ToCharset(mpt::Charset::UTF8, mpt::String::Replace(tag, U_("="), MPT_UTF8("\xEF\xBF\xBD")))); // U+FFFD + } + +public: + AUStreamWriter(const AUEncoder &enc_, std::ostream &file, const Encoder::Settings &settings_, const FileTags &tags) + : enc(enc_) + , f(file) + , settings(settings_) + { + + MPT_ASSERT(settings.Format.GetSampleFormat().IsValid()); + MPT_ASSERT(settings.Samplerate > 0); + MPT_ASSERT(settings.Channels > 0); + + std::string annotation; + std::size_t annotationSize = 0; + std::size_t annotationTotalSize = 8; + if(settings.Tags) + { + // same format as invented by sox and implemented by ffmpeg + annotation += TagToAnnotation("title", tags.title); + annotation += TagToAnnotation("artist", tags.artist); + annotation += TagToAnnotation("album", tags.album); + annotation += TagToAnnotation("track", tags.trackno); + annotation += TagToAnnotation("genre", tags.genre); + annotation += TagToAnnotation("comment", tags.comments); + annotationSize = annotation.length() + 1; + annotationTotalSize = annotationSize; + if(settings.Details.AUPaddingAlignHint > 0) + { + annotationTotalSize = mpt::align_up<std::size_t>(24u + annotationTotalSize, settings.Details.AUPaddingAlignHint) - 24u; + } + annotationTotalSize = mpt::align_up<std::size_t>(annotationTotalSize, 8u); + } + MPT_ASSERT(annotationTotalSize >= annotationSize); + MPT_ASSERT(annotationTotalSize % 8 == 0); + + mpt::IO::WriteText(f, ".snd"); + mpt::IO::WriteIntBE<uint32>(f, mpt::saturate_cast<uint32>(24u + annotationTotalSize)); + mpt::IO::WriteIntBE<uint32>(f, ~uint32(0)); + uint32 encoding = 0; + if(settings.Format.encoding == Encoder::Format::Encoding::Float) + { + switch(settings.Format.bits) + { + case 32: encoding = 6; break; + case 64: encoding = 7; break; + } + } else if(settings.Format.encoding == Encoder::Format::Encoding::Integer) + { + switch(settings.Format.bits) + { + case 8: encoding = 2; break; + case 16: encoding = 3; break; + case 24: encoding = 4; break; + case 32: encoding = 5; break; + } + } else if(settings.Format.encoding == Encoder::Format::Encoding::Alaw) + { + encoding = 27; + } else if (settings.Format.encoding == Encoder::Format::Encoding::ulaw) + { + encoding = 1; + } + mpt::IO::WriteIntBE<uint32>(f, encoding); + mpt::IO::WriteIntBE<uint32>(f, settings.Samplerate); + mpt::IO::WriteIntBE<uint32>(f, settings.Channels); + if(annotationSize > 0) + { + mpt::IO::WriteText(f, annotation); + mpt::IO::WriteIntBE<uint8>(f, '\0'); + } + for(std::size_t i = 0; i < annotationTotalSize - annotationSize; ++i) + { + mpt::IO::WriteIntBE<uint8>(f, '\0'); + } + + } + + SampleFormat GetSampleFormat() const override + { + return settings.Format.GetSampleFormat(); + } + + void WriteInterleaved(std::size_t frameCount, const double *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const float *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int32 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + + void WriteCues(const std::vector<uint64> &cues) override + { + MPT_UNREFERENCED_PARAMETER(cues); + } + void WriteFinalize() override + { + return; + } + virtual ~AUStreamWriter() + { + return; + } +}; + + + +AUEncoder::AUEncoder() +{ + Encoder::Traits traits; + traits.fileExtension = P_("au"); + traits.fileShortDescription = U_("AU"); + traits.fileDescription = U_("NeXT/Sun Audio"); + traits.encoderSettingsName = U_("AU"); + traits.canTags = true; + traits.canCues = false; + traits.maxChannels = 4; + traits.samplerates = TrackerSettings::Instance().GetSampleRates(); + traits.modes = Encoder::ModeLossless; + traits.formats.push_back({ Encoder::Format::Encoding::Float, 64, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Float, 32, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 32, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 24, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 16, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 8, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Alaw, 16, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::ulaw, 16, mpt::endian::big }); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeLossless; + traits.defaultFormat = { Encoder::Format::Encoding::Float, 32, mpt::endian::big }; + SetTraits(traits); +} + + +bool AUEncoder::IsAvailable() const +{ + return true; +} + + +std::unique_ptr<IAudioStreamEncoder> AUEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + if(!IsAvailable()) + { + return nullptr; + } + return std::make_unique<AUStreamWriter>(*this, file, settings, tags); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.h new file mode 100644 index 00000000..580c1301 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderAU.h @@ -0,0 +1,41 @@ +/* + * StreamEncoderAU.h + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class AUtreamWriter; + + +class AUEncoder : public EncoderFactoryBase +{ + + friend class AUStreamWriter; + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsAvailable() const override; + +public: + + AUEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.cpp new file mode 100644 index 00000000..a5347311 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.cpp @@ -0,0 +1,219 @@ +/* + * StreamEncoder.cpp + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderFLAC.h" + +#include "Mptrack.h" +#include "TrackerSettings.h" + +#include <FLAC/metadata.h> +#include <FLAC/format.h> +#include <FLAC/stream_encoder.h> + + +OPENMPT_NAMESPACE_BEGIN + + +class FLACStreamWriter : public StreamWriterBase +{ +private: + const FLACEncoder &enc; + Encoder::Settings settings; + FLAC__StreamMetadata *flac_metadata[1]; + FLAC__StreamEncoder *encoder; + std::vector<FLAC__int32> sampleBuf; +private: + static FLAC__StreamEncoderWriteStatus FLACWriteCallback(const FLAC__StreamEncoder *flacenc, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame, void *client_data) + { + return reinterpret_cast<FLACStreamWriter*>(client_data)->WriteCallback(flacenc, buffer, bytes, samples, current_frame); + } + static FLAC__StreamEncoderSeekStatus FLACSeekCallback(const FLAC__StreamEncoder *flacenc, FLAC__uint64 absolute_byte_offset, void *client_data) + { + return reinterpret_cast<FLACStreamWriter*>(client_data)->SeekCallback(flacenc, absolute_byte_offset); + } + static FLAC__StreamEncoderTellStatus FLACTellCallback(const FLAC__StreamEncoder *flacenc, FLAC__uint64 *absolute_byte_offset, void *client_data) + { + return reinterpret_cast<FLACStreamWriter*>(client_data)->TellCallback(flacenc, absolute_byte_offset); + } + FLAC__StreamEncoderWriteStatus WriteCallback(const FLAC__StreamEncoder *flacenc, const FLAC__byte buffer[], size_t bytes, unsigned samples, unsigned current_frame) + { + MPT_UNREFERENCED_PARAMETER(flacenc); + MPT_UNREFERENCED_PARAMETER(samples); + MPT_UNREFERENCED_PARAMETER(current_frame); + f.write(reinterpret_cast<const char*>(buffer), bytes); + if(!f) return FLAC__STREAM_ENCODER_WRITE_STATUS_FATAL_ERROR; + return FLAC__STREAM_ENCODER_WRITE_STATUS_OK; + } + FLAC__StreamEncoderSeekStatus SeekCallback(const FLAC__StreamEncoder *flacenc, FLAC__uint64 absolute_byte_offset) + { + MPT_UNREFERENCED_PARAMETER(flacenc); + f.seekp(absolute_byte_offset); + if(!f) return FLAC__STREAM_ENCODER_SEEK_STATUS_ERROR; + return FLAC__STREAM_ENCODER_SEEK_STATUS_OK; + } + FLAC__StreamEncoderTellStatus TellCallback(const FLAC__StreamEncoder *flacenc, FLAC__uint64 *absolute_byte_offset) + { + MPT_UNREFERENCED_PARAMETER(flacenc); + if(absolute_byte_offset) + { + *absolute_byte_offset = f.tellp(); + } + if(!f) return FLAC__STREAM_ENCODER_TELL_STATUS_ERROR; + return FLAC__STREAM_ENCODER_TELL_STATUS_OK; + } +private: + void AddCommentField(const std::string &field, const mpt::ustring &data) + { + if(!field.empty() && !data.empty()) + { + FLAC__StreamMetadata_VorbisComment_Entry entry; + FLAC__metadata_object_vorbiscomment_entry_from_name_value_pair(&entry, field.c_str(), mpt::ToCharset(mpt::Charset::UTF8, data).c_str()); + FLAC__metadata_object_vorbiscomment_append_comment(flac_metadata[0], entry, false); + } + } +public: + FLACStreamWriter(const FLACEncoder &enc_, std::ostream &stream, const Encoder::Settings &settings_, const FileTags &tags) + : StreamWriterBase(stream) + , enc(enc_) + , settings(settings_) + { + flac_metadata[0] = nullptr; + encoder = nullptr; + + MPT_ASSERT(settings.Format.GetSampleFormat().IsValid()); + MPT_ASSERT(settings.Samplerate > 0); + MPT_ASSERT(settings.Channels > 0); + + encoder = FLAC__stream_encoder_new(); + + FLAC__stream_encoder_set_channels(encoder, settings.Channels); + FLAC__stream_encoder_set_bits_per_sample(encoder, settings.Format.GetSampleFormat().GetBitsPerSample()); + FLAC__stream_encoder_set_sample_rate(encoder, settings.Samplerate); + + int compressionLevel = settings.Details.FLACCompressionLevel; + FLAC__stream_encoder_set_compression_level(encoder, compressionLevel); + + if(settings.Tags) + { + flac_metadata[0] = FLAC__metadata_object_new(FLAC__METADATA_TYPE_VORBIS_COMMENT); + AddCommentField("ENCODER", tags.encoder); + AddCommentField("SOURCEMEDIA", U_("tracked music file")); + AddCommentField("TITLE", tags.title ); + AddCommentField("ARTIST", tags.artist ); + AddCommentField("ALBUM", tags.album ); + AddCommentField("DATE", tags.year ); + AddCommentField("COMMENT", tags.comments ); + AddCommentField("GENRE", tags.genre ); + AddCommentField("CONTACT", tags.url ); + AddCommentField("BPM", tags.bpm ); // non-standard + AddCommentField("TRACKNUMBER", tags.trackno ); + FLAC__stream_encoder_set_metadata(encoder, flac_metadata, 1); + } + + FLAC__stream_encoder_init_stream(encoder, FLACWriteCallback, FLACSeekCallback, FLACTellCallback, nullptr, this); + + } + SampleFormat GetSampleFormat() const + { + return settings.Format.GetSampleFormat(); + } + template <typename Tsample> + void WriteInterleavedInt(std::size_t frameCount, const Tsample *p) + { + MPT_ASSERT(settings.Format.GetSampleFormat() == SampleFormatTraits<Tsample>::sampleFormat()); + sampleBuf.resize(frameCount * settings.Channels); + for(std::size_t frame = 0; frame < frameCount; ++frame) + { + for(int channel = 0; channel < settings.Channels; ++channel) + { + sampleBuf[frame * settings.Channels + channel] = *p; + p++; + } + } + while(frameCount > 0) + { + unsigned int frameCountChunk = mpt::saturate_cast<unsigned int>(frameCount); + FLAC__stream_encoder_process_interleaved(encoder, sampleBuf.data(), frameCountChunk); + frameCount -= frameCountChunk; + } + } + void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) override + { + WriteInterleavedInt(frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) override + { + WriteInterleavedInt(frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) override + { + WriteInterleavedInt(frameCount, interleaved); + } + void WriteFinalize() override + { + FLAC__stream_encoder_finish(encoder); + } + virtual ~FLACStreamWriter() + { + FLAC__stream_encoder_delete(encoder); + encoder = nullptr; + + if(flac_metadata[0]) + { + FLAC__metadata_object_delete(flac_metadata[0]); + flac_metadata[0] = nullptr; + } + } +}; + + + +FLACEncoder::FLACEncoder() +{ + Encoder::Traits traits; + traits.fileExtension = P_("flac"); + traits.fileShortDescription = U_("FLAC"); + traits.fileDescription = U_("Free Lossless Audio Codec"); + traits.encoderSettingsName = U_("FLAC"); + traits.canTags = true; + traits.maxChannels = 4; + traits.samplerates = TrackerSettings::Instance().GetSampleRates(); + traits.modes = Encoder::ModeLossless; + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 24, mpt::get_endian() }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 16, mpt::get_endian() }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 8, mpt::get_endian() }); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeLossless; + traits.defaultFormat = { Encoder::Format::Encoding::Integer, 24, mpt::get_endian() }; + SetTraits(traits); +} + + +bool FLACEncoder::IsAvailable() const +{ + return true; +} + + +std::unique_ptr<IAudioStreamEncoder> FLACEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + if(!IsAvailable()) + { + return nullptr; + } + return std::make_unique<FLACStreamWriter>(*this, file, settings, tags); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.h new file mode 100644 index 00000000..7307ab56 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderFLAC.h @@ -0,0 +1,36 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class FLACEncoder : public EncoderFactoryBase +{ + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsAvailable() const override; + +public: + + FLACEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.cpp new file mode 100644 index 00000000..2b4788b5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.cpp @@ -0,0 +1,741 @@ +/* + * StreamEncoder.cpp + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderMP3.h" + +#include "Mptrack.h" + +#include "../soundlib/Sndfile.h" + +#include "../common/misc_util.h" +#include "../common/mptStringBuffer.h" + +#ifdef MPT_WITH_LAME +#if defined(MPT_BUILD_MSVC) +#include <lame.h> +#else +#include <lame/lame.h> +#endif +#endif // MPT_WITH_LAME + + + +OPENMPT_NAMESPACE_BEGIN + + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ID3v2.4 Tags + +struct ID3v2Header +{ + uint8 signature[3]; + uint8 version[2]; + uint8be flags; + uint32be size; +}; + +MPT_BINARY_STRUCT(ID3v2Header, 10) + +struct ID3v2Frame +{ + char frameid[4]; + uint32be size; + uint16be flags; +}; + +MPT_BINARY_STRUCT(ID3v2Frame, 10) + + +// charset... choose text ending accordingly. +// $00 = ISO-8859-1. Terminated with $00. +// $01 = UTF-16 with BOM. Terminated with $00 00. +// $02 = UTF-16BE without BOM. Terminated with $00 00. +// $03 = UTF-8. Terminated with $00. +#define ID3v2_CHARSET '\3' +#define ID3v2_TEXTENDING '\0' + +struct ReplayGain +{ + enum GainTag + { + TagSkip, + TagReserve, + TagWrite + }; + GainTag Tag; + float TrackPeak; + bool TrackPeakValid; + float TrackGaindB; + bool TrackGaindBValid; + ReplayGain() + : Tag(TagSkip) + , TrackPeak(0.0f) + , TrackPeakValid(false) + , TrackGaindB(0.0f) + , TrackGaindBValid(false) + { + return; + } +}; + +class ID3V2Tagger +{ +private: + Encoder::StreamSettings settings; +public: + // Write Tags + void WriteID3v2Tags(std::ostream &s, const FileTags &tags, ReplayGain replayGain = ReplayGain()); + + ID3V2Tagger(const Encoder::StreamSettings &settings_); + +private: + // Convert Integer to Synchsafe Integer (see ID3v2.4 specs) + uint32 intToSynchsafe(uint32 in); + // Return maximum value that fits into a syncsafe int + uint32 GetMaxSynchsafeInt() const; + // Write a frame + void WriteID3v2Frame(const char cFrameID[4], std::string sFramecontent, std::ostream &s); + // Return an upper bound for the size of all replay gain frames + uint32 GetMaxReplayGainFramesSizes(); + uint32 GetMaxReplayGainTxxxTrackGainFrameSize(); + uint32 GetMaxReplayGainTxxxTrackPeakFrameSize(); + // Write out all ReplayGain frames + void WriteID3v2ReplayGainFrames(ReplayGain replaygain, std::ostream &s); + // Size of our tag + uint32 totalID3v2Size; +}; + +/////////////////////////////////////////////////// +// CFileTagging - helper class for writing tags + +ID3V2Tagger::ID3V2Tagger(const Encoder::StreamSettings &settings_) + : settings(settings_) + , totalID3v2Size(0) +{ + return; +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// ID3v2.4 Tags + +// Convert Integer to Synchsafe Integer (see ID3v2.4 specs) +// Basically, it's a BigEndian integer, but the MSB of all bytes is 0. +// Thus, a 32-bit integer turns into a 28-bit integer. +uint32 ID3V2Tagger::intToSynchsafe(uint32 in) +{ + uint32 out = 0, steps = 0; + do + { + out |= (in & 0x7F) << steps; + steps += 8; + } while(in >>= 7); + return out; +} + +// Return maximum value that fits into a syncsafe int +uint32 ID3V2Tagger::GetMaxSynchsafeInt() const +{ + return 0x0fffffffu; +} + +// Write Tags +void ID3V2Tagger::WriteID3v2Tags(std::ostream &s, const FileTags &tags, ReplayGain replayGain) +{ + if(!s) return; + + ID3v2Header tHeader; + std::streampos fOffset = s.tellp(); + uint32 paddingSize = 0; + + totalID3v2Size = 0; + + // Correct header will be written later (tag size missing) + memcpy(tHeader.signature, "ID3", 3); + tHeader.version[0] = 0x04; // Version 2.4.0 + tHeader.version[1] = 0x00; // Ditto + tHeader.flags = 0; // No flags + tHeader.size = 0; // will be filled later + s.write(reinterpret_cast<const char*>(&tHeader), sizeof(tHeader)); + totalID3v2Size += sizeof(tHeader); + + WriteID3v2Frame("TIT2", mpt::ToCharset(mpt::Charset::UTF8, tags.title), s); + WriteID3v2Frame("TPE1", mpt::ToCharset(mpt::Charset::UTF8, tags.artist), s); + WriteID3v2Frame("TCOM", mpt::ToCharset(mpt::Charset::UTF8, tags.artist), s); + WriteID3v2Frame("TALB", mpt::ToCharset(mpt::Charset::UTF8, tags.album), s); + WriteID3v2Frame("TCON", mpt::ToCharset(mpt::Charset::UTF8, tags.genre), s); + //WriteID3v2Frame("TYER", mpt::ToCharset(mpt::Charset::UTF8, tags.year), s); // Deprecated + WriteID3v2Frame("TDRC", mpt::ToCharset(mpt::Charset::UTF8, tags.year), s); + WriteID3v2Frame("TBPM", mpt::ToCharset(mpt::Charset::UTF8, tags.bpm), s); + WriteID3v2Frame("WXXX", mpt::ToCharset(mpt::Charset::UTF8, tags.url), s); + WriteID3v2Frame("TENC", mpt::ToCharset(mpt::Charset::UTF8, tags.encoder), s); + WriteID3v2Frame("COMM", mpt::ToCharset(mpt::Charset::UTF8, tags.comments), s); + if(replayGain.Tag == ReplayGain::TagReserve) + { + paddingSize += GetMaxReplayGainFramesSizes(); + } else if(replayGain.Tag == ReplayGain::TagWrite) + { + std::streampos replayGainBeg = s.tellp(); + WriteID3v2ReplayGainFrames(replayGain, s); + std::streampos replayGainEnd = s.tellp(); + paddingSize += GetMaxReplayGainFramesSizes() - static_cast<uint32>(replayGainEnd - replayGainBeg); + } + + // Write Padding + uint32 totalID3v2SizeWithoutPadding = totalID3v2Size; + paddingSize += settings.MP3ID3v2MinPadding; + totalID3v2Size += paddingSize; + if(settings.MP3ID3v2PaddingAlignHint > 0) + { + totalID3v2Size = mpt::align_up<uint32>(totalID3v2Size, settings.MP3ID3v2PaddingAlignHint); + paddingSize = totalID3v2Size - totalID3v2SizeWithoutPadding; + } + for(size_t i = 0; i < paddingSize; i++) + { + char c = 0; + s.write(&c, 1); + } + + // Write correct header (update tag size) + tHeader.size = intToSynchsafe(totalID3v2Size - sizeof(tHeader)); + s.seekp(fOffset); + s.write(reinterpret_cast<const char*>(&tHeader), sizeof(tHeader)); + s.seekp(totalID3v2Size - sizeof(tHeader), std::ios::cur); + +} + +uint32 ID3V2Tagger::GetMaxReplayGainTxxxTrackGainFrameSize() +{ + return mpt::saturate_cast<uint32>(sizeof(ID3v2Frame) + 1 + std::strlen("REPLAYGAIN_TRACK_GAIN") + 1 + std::strlen("-123.45 dB") + 1); // should be enough +} + +uint32 ID3V2Tagger::GetMaxReplayGainTxxxTrackPeakFrameSize() +{ + return mpt::saturate_cast<uint32>(sizeof(ID3v2Frame) + 1 + std::strlen("REPLAYGAIN_TRACK_PEAK") + 1 + std::strlen("2147483648.123456") + 1); // unrealistic worst case +} + +uint32 ID3V2Tagger::GetMaxReplayGainFramesSizes() +{ + uint32 size = 0; + if(settings.MP3ID3v2WriteReplayGainTXXX) + { + size += GetMaxReplayGainTxxxTrackGainFrameSize(); + size += GetMaxReplayGainTxxxTrackPeakFrameSize(); + } + return size; +} + +void ID3V2Tagger::WriteID3v2ReplayGainFrames(ReplayGain replayGain, std::ostream &s) +{ + + if(settings.MP3ID3v2WriteReplayGainTXXX && replayGain.TrackGaindBValid) + { + + std::string content; + + content += std::string(1, 0x00); // ISO-8859-1 + content += std::string("REPLAYGAIN_TRACK_GAIN"); + content += std::string(1, '\0'); + + int32 gainTimes100 = mpt::saturate_round<int32>(replayGain.TrackGaindB * 100.0f); + if(gainTimes100 < 0) + { + content += "-"; + gainTimes100 = std::abs(gainTimes100); + } + content += mpt::afmt::dec(gainTimes100 / 100); + content += "."; + content += mpt::afmt::dec0<2>(gainTimes100 % 100); + content += " "; + content += "dB"; + + content += std::string(1, '\0'); + + if(sizeof(ID3v2Frame) + content.size() <= GetMaxReplayGainTxxxTrackGainFrameSize()) + { + ID3v2Frame frame; + std::memset(&frame, 0, sizeof(ID3v2Frame)); + std::memcpy(&frame.frameid, "TXXX", 4); + frame.size = intToSynchsafe(static_cast<uint32>(content.size())); + frame.flags = 0x4000; // discard if audio data changed + s.write(reinterpret_cast<const char*>(&frame), sizeof(ID3v2Frame)); + s.write(content.data(), content.size()); + } + + } + + + if(settings.MP3ID3v2WriteReplayGainTXXX && replayGain.TrackPeakValid) + { + + std::string content; + + content += std::string(1, 0x00); // ISO-8859-1 + content += std::string("REPLAYGAIN_TRACK_PEAK"); + content += std::string(1, '\0'); + + int32 peakTimes1000000 = mpt::saturate_round<int32>(std::fabs(replayGain.TrackPeak) * 1000000.0f); + std::string number; + number += mpt::afmt::dec(peakTimes1000000 / 1000000); + number += "."; + number += mpt::afmt::dec0<6>(peakTimes1000000 % 1000000); + content += number; + + content += std::string(1, '\0'); + + if(sizeof(ID3v2Frame) + content.size() <= GetMaxReplayGainTxxxTrackPeakFrameSize()) + { + ID3v2Frame frame; + std::memset(&frame, 0, sizeof(ID3v2Frame)); + std::memcpy(&frame.frameid, "TXXX", 4); + frame.size = intToSynchsafe(static_cast<uint32>(content.size())); + frame.flags = 0x4000; // discard if audio data changed + s.write(reinterpret_cast<const char*>(&frame), sizeof(ID3v2Frame)); + s.write(content.data(), content.size()); + } + + } + +} + +// Write a ID3v2 frame +void ID3V2Tagger::WriteID3v2Frame(const char cFrameID[4], std::string sFramecontent, std::ostream &s) +{ + if(!cFrameID[0] || sFramecontent.empty() || !s) return; + + if(!memcmp(cFrameID, "COMM", 4)) + { + // English language for comments - no description following (hence the text ending nullchar(s)) + // For language IDs, see https://en.wikipedia.org/wiki/ISO-639-2 + sFramecontent = "eng" + (ID3v2_TEXTENDING + sFramecontent); + } + if(!memcmp(cFrameID, "WXXX", 4)) + { + // User-defined URL field (we have no description for the URL, so we leave it out) + sFramecontent = ID3v2_TEXTENDING + sFramecontent; + } + sFramecontent = ID3v2_CHARSET + sFramecontent; + sFramecontent += ID3v2_TEXTENDING; + + if(sFramecontent.size() <= GetMaxSynchsafeInt()) + { + ID3v2Frame tFrame; + std::memset(&tFrame, 0, sizeof(ID3v2Frame)); + std::memcpy(&tFrame.frameid, cFrameID, 4); // ID + tFrame.size = intToSynchsafe(static_cast<uint32>(sFramecontent.size())); // Text size + tFrame.flags = 0x0000; // No flags + s.write(reinterpret_cast<const char*>(&tFrame), sizeof(tFrame)); + s.write(sFramecontent.c_str(), sFramecontent.size()); + + totalID3v2Size += static_cast<uint32>((sizeof(tFrame) + sFramecontent.size())); + } +} + + + + +#ifdef MPT_WITH_LAME + + +using lame_t = lame_global_flags *; + + +static void GenreEnumCallback(int num, const char *name, void *cookie) +{ + MPT_UNREFERENCED_PARAMETER(num); + Encoder::Traits &traits = *reinterpret_cast<Encoder::Traits*>(cookie); + if(name) + { + traits.genres.push_back(mpt::ToUnicode(mpt::Charset::ISO8859_1, name)); + } +} + + +static Encoder::Traits BuildTraits(bool compatible) +{ + Encoder::Traits traits; + traits.fileExtension = P_("mp3"); + traits.fileShortDescription = (compatible ? U_("Compatible MP3") : U_("MP3")); + traits.encoderSettingsName = (compatible ? U_("MP3LameCompatible") : U_("MP3Lame")); + traits.fileDescription = (compatible ? U_("MPEG-1 Layer 3") : U_("MPEG-1/2 Layer 3")); + traits.canTags = true; + traits.genres.clear(); + id3tag_genre_list(&GenreEnumCallback, &traits); + traits.modesWithFixedGenres = (compatible ? Encoder::ModeCBR : Encoder::ModeInvalid); + traits.maxChannels = 2; + traits.samplerates = (compatible + ? mpt::make_vector(mpeg1layer3_samplerates) + : mpt::make_vector(layer3_samplerates) + ); + traits.modes = (compatible ? Encoder::ModeCBR : (Encoder::ModeABR | Encoder::ModeQuality)); + traits.bitrates = (compatible + ? mpt::make_vector(mpeg1layer3_bitrates) + : mpt::make_vector(layer3_bitrates) + ); + traits.defaultSamplerate = 44100; + traits.defaultChannels = 2; + traits.defaultMode = (compatible ? Encoder::ModeCBR : Encoder::ModeQuality); + traits.defaultBitrate = 256; + traits.defaultQuality = 0.8f; + return traits; +} + + +class MP3LameStreamWriter : public StreamWriterBase +{ +private: + bool compatible; + Encoder::Settings settings; + Encoder::Mode Mode; + bool gfp_inited; + lame_t gfp; + enum ID3Type + { + ID3None, + ID3v1, + ID3v2Lame, + ID3v2OpenMPT, + }; + ID3Type id3type; + std::streamoff id3v2Size; + FileTags Tags; +public: + MP3LameStreamWriter(std::ostream &stream, bool compatible, const Encoder::Settings &settings_, const FileTags &tags) + : StreamWriterBase(stream) + , compatible(compatible) + , settings(settings_) + { + Mode = Encoder::ModeInvalid; + gfp_inited = false; + gfp = lame_t(); + id3type = ID3v2Lame; + id3v2Size = 0; + + if(!gfp) + { + gfp = lame_init(); + } + + uint32 samplerate = settings.Samplerate; + uint16 channels = settings.Channels; + if(settings.Tags) + { + if(compatible) + { + id3type = ID3v1; + } else if(settings.Details.MP3LameID3v2UseLame) + { + id3type = ID3v2Lame; + } else + { + id3type = ID3v2OpenMPT; + } + } else + { + id3type = ID3None; + } + id3v2Size = 0; + + lame_set_in_samplerate(gfp, samplerate); + lame_set_num_channels(gfp, channels); + + int lameQuality = settings.Details.MP3LameQuality; + lame_set_quality(gfp, lameQuality); + + if(settings.Mode == Encoder::ModeCBR) + { + + if(compatible) + { + if(settings.Bitrate >= 32) + { + // For maximum compatibility, + // force samplerate to a samplerate supported by MPEG1 streams. + if(samplerate <= 32000) + { + samplerate = 32000; + } else if(samplerate >= 48000) + { + samplerate = 48000; + } else + { + samplerate = 44100; + } + lame_set_out_samplerate(gfp, samplerate); + } else + { + // A very low bitrate was chosen, + // force samplerate to lowest possible for MPEG2. + // Disable unofficial MPEG2.5 however. + lame_set_out_samplerate(gfp, 16000); + } + } + + lame_set_brate(gfp, settings.Bitrate); + lame_set_VBR(gfp, vbr_off); + + if(compatible) + { + lame_set_bWriteVbrTag(gfp, 0); + lame_set_strict_ISO(gfp, 1); + lame_set_disable_reservoir(gfp, 1); + } else + { + lame_set_bWriteVbrTag(gfp, 1); + } + + } else if(settings.Mode == Encoder::ModeABR) + { + + lame_set_brate(gfp, settings.Bitrate); + lame_set_VBR(gfp, vbr_abr); + + lame_set_bWriteVbrTag(gfp, 1); + + } else + { + + float lame_quality = 10.0f - (settings.Quality * 10.0f); + Limit(lame_quality, 0.0f, 9.999f); + lame_set_VBR_quality(gfp, lame_quality); + lame_set_VBR(gfp, vbr_default); + + lame_set_bWriteVbrTag(gfp, 1); + + } + + lame_set_decode_on_the_fly(gfp, settings.Details.MP3LameCalculatePeakSample ? 1 : 0); // see LAME docs for why + lame_set_findReplayGain(gfp, settings.Details.MP3LameCalculateReplayGain ? 1 : 0); + + switch(id3type) + { + case ID3None: + lame_set_write_id3tag_automatic(gfp, 0); + break; + case ID3v1: + id3tag_init(gfp); + id3tag_v1_only(gfp); + break; + case ID3v2Lame: + id3tag_init(gfp); + id3tag_add_v2(gfp); + id3tag_v2_only(gfp); + id3tag_set_pad(gfp, settings.Details.MP3ID3v2MinPadding); + break; + case ID3v2OpenMPT: + lame_set_write_id3tag_automatic(gfp, 0); + break; + } + + Mode = settings.Mode; + + if(settings.Tags) + { + if(id3type == ID3v2Lame || id3type == ID3v1) + { + // Lame API expects Latin1, which is sad, but we cannot change that. + if(!tags.title.empty()) id3tag_set_title( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.title ).c_str()); + if(!tags.artist.empty()) id3tag_set_artist( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.artist ).c_str()); + if(!tags.album.empty()) id3tag_set_album( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.album ).c_str()); + if(!tags.year.empty()) id3tag_set_year( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.year ).c_str()); + if(!tags.comments.empty()) id3tag_set_comment(gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.comments).c_str()); + if(!tags.trackno.empty()) id3tag_set_track( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.trackno ).c_str()); + if(!tags.genre.empty()) id3tag_set_genre( gfp, mpt::ToCharset(mpt::Charset::ISO8859_1, tags.genre ).c_str()); + } else if(id3type == ID3v2OpenMPT) + { + Tags = tags; + std::streampos id3beg = f.tellp(); + ID3V2Tagger tagger(settings.Details); + ReplayGain replayGain; + if(settings.Details.MP3LameCalculatePeakSample || settings.Details.MP3LameCalculateReplayGain) + { + replayGain.Tag = ReplayGain::TagReserve; + } + tagger.WriteID3v2Tags(f, tags, replayGain); + std::streampos id3end = f.tellp(); + id3v2Size = id3end - id3beg; + } + } + + } + void WriteInterleaved(size_t count, const float *interleaved) override + { + if(!gfp_inited) + { + lame_init_params(gfp); + gfp_inited = true; + } + const int count_max = 0xffff; + while(count > 0) + { + int count_chunk = std::clamp(mpt::saturate_cast<int>(count), int(0), count_max); + buf.resize(count_chunk + (count_chunk+3)/4 + 7200); + int result = 0; + if(lame_get_num_channels(gfp) == 1) + { + // lame always assumes stereo input with interleaved interface, so use non-interleaved for mono + result = lame_encode_buffer_ieee_float(gfp, interleaved, nullptr, count_chunk, mpt::byte_cast<unsigned char*>(buf.data()), mpt::saturate_cast<int>(buf.size())); + } else + { + result = lame_encode_buffer_interleaved_ieee_float(gfp, interleaved, count_chunk, mpt::byte_cast<unsigned char*>(buf.data()), mpt::saturate_cast<int>(buf.size())); + } + buf.resize((result >= 0) ? result : 0); + if(result == -2) + { + throw std::bad_alloc(); + } + WriteBuffer(); + count -= static_cast<size_t>(count_chunk); + } + } + void WriteFinalize() override + { + if(!gfp_inited) + { + lame_init_params(gfp); + gfp_inited = true; + } + buf.resize(7200); + buf.resize(lame_encode_flush(gfp, mpt::byte_cast<unsigned char*>(buf.data()), mpt::saturate_cast<int>(buf.size()))); + WriteBuffer(); + ReplayGain replayGain; + if(settings.Details.MP3LameCalculatePeakSample) + { + replayGain.TrackPeak = std::fabs(lame_get_PeakSample(gfp)) / 32768.0f; + replayGain.TrackPeakValid = true; + } + if(settings.Details.MP3LameCalculateReplayGain) + { + replayGain.TrackGaindB = lame_get_RadioGain(gfp) / 10.0f; + replayGain.TrackGaindBValid = true; + } + if(id3type == ID3v2OpenMPT && (settings.Details.MP3LameCalculatePeakSample || settings.Details.MP3LameCalculateReplayGain)) + { // update ID3v2 tag with replay gain information + replayGain.Tag = ReplayGain::TagWrite; + std::streampos endPos = f.tellp(); + f.seekp(fStart); + std::string tagdata(static_cast<std::size_t>(id3v2Size), '\0'); + f.write(tagdata.data(), id3v2Size); // clear out the old tag + f.seekp(fStart); + ID3V2Tagger tagger(settings.Details); + tagger.WriteID3v2Tags(f, Tags, replayGain); + f.seekp(endPos); + } + if(id3type == ID3v2Lame) + { + id3v2Size = lame_get_id3v2_tag(gfp, nullptr, 0); + } else if(id3type == ID3v2OpenMPT) + { + // id3v2Size already set + } + if(!compatible) + { + std::streampos endPos = f.tellp(); + f.seekp(fStart + id3v2Size); + buf.resize(lame_get_lametag_frame(gfp, nullptr, 0)); + buf.resize(lame_get_lametag_frame(gfp, (unsigned char*)buf.data(), buf.size())); + WriteBuffer(); + f.seekp(endPos); + } + } + virtual ~MP3LameStreamWriter() + { + if(!gfp) + { + return; + } + lame_close(gfp); + gfp = lame_t(); + gfp_inited = false; + } +}; + +#endif // MPT_WITH_LAME + + + +MP3Encoder::MP3Encoder(MP3EncoderType type) + : m_Type(type) +{ +#ifdef MPT_WITH_LAME + if(type == MP3EncoderLame) + { + m_Type = MP3EncoderLame; + SetTraits(BuildTraits(false)); + return; + } + if(type == MP3EncoderLameCompatible) + { + m_Type = MP3EncoderLameCompatible; + SetTraits(BuildTraits(true)); + return; + } +#endif // MPT_WITH_LAME +} + + +bool MP3Encoder::IsAvailable() const +{ + return false +#ifdef MPT_WITH_LAME + || (m_Type == MP3EncoderLame) + || (m_Type == MP3EncoderLameCompatible) +#endif // MPT_WITH_LAME + ; +} + + +std::unique_ptr<IAudioStreamEncoder> MP3Encoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + std::unique_ptr<IAudioStreamEncoder> result = nullptr; + if(false) + { + // nothing +#ifdef MPT_WITH_LAME + } else if(m_Type == MP3EncoderLame || m_Type == MP3EncoderLameCompatible) + { + result = std::make_unique<MP3LameStreamWriter>(file, (m_Type == MP3EncoderLameCompatible), settings, tags); +#endif // MPT_WITH_LAME + } + return result; +} + + +mpt::ustring MP3Encoder::DescribeQuality(float quality) const +{ +#ifdef MPT_WITH_LAME + if(m_Type == MP3EncoderLame) + { + static constexpr int q_table[11] = { 240, 220, 190, 170, 160, 130, 120, 100, 80, 70, 50 }; // http://wiki.hydrogenaud.io/index.php?title=LAME + int q = mpt::saturate_round<int>((1.0f - quality) * 10.0f); + if(q < 0) q = 0; + if(q >= 10) + { + return MPT_UFORMAT("VBR -V{} (~{} kbit)")(U_("9.999"), q_table[q]); + } else + { + return MPT_UFORMAT("VBR -V{} (~{} kbit)")(q, q_table[q]); + } + } +#endif // MPT_WITH_LAME + return EncoderFactoryBase::DescribeQuality(quality); +} + +mpt::ustring MP3Encoder::DescribeBitrateABR(int bitrate) const +{ + return EncoderFactoryBase::DescribeBitrateABR(bitrate); +} + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.h new file mode 100644 index 00000000..ad61d68d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderMP3.h @@ -0,0 +1,51 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MPT_WITH_LAME +class ComponentLame; +#endif + +enum MP3EncoderType +{ + MP3EncoderLame, + MP3EncoderLameCompatible, +}; + +class MP3Encoder : public EncoderFactoryBase +{ + +private: + + MP3EncoderType m_Type; + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + mpt::ustring DescribeQuality(float quality) const override; + mpt::ustring DescribeBitrateABR(int bitrate) const override; + bool IsAvailable() const override; + +public: + + MP3Encoder(MP3EncoderType type); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.cpp new file mode 100644 index 00000000..e7926d6c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.cpp @@ -0,0 +1,221 @@ +/* + * StreamEncoderOpus.cpp + * --------------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderOpus.h" + +#include <sstream> + +#include "Mptrack.h" + +#include <deque> + +#if defined(MPT_WITH_OPUS) && defined(MPT_WITH_OPUSENC) +#include <opusenc.h> +#endif + + +OPENMPT_NAMESPACE_BEGIN + + + +static Encoder::Traits BuildTraits() + { + Encoder::Traits traits; +#if defined(MPT_WITH_OPUS) && defined(MPT_WITH_OPUSENC) + traits.fileExtension = P_("opus"); + traits.fileShortDescription = U_("Opus"); + traits.fileDescription = U_("Ogg Opus"); + traits.encoderSettingsName = U_("Opus"); + traits.canTags = true; + traits.maxChannels = 4; + traits.samplerates = mpt::make_vector(opus_all_samplerates); + traits.modes = Encoder::ModeCBR | Encoder::ModeVBR; + traits.bitrates = mpt::make_vector(opus_bitrates); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeVBR; + traits.defaultBitrate = 128; +#endif + return traits; + } + + + +#if defined(MPT_WITH_OPUS) && defined(MPT_WITH_OPUSENC) + +class OpusStreamWriter : public StreamWriterBase +{ +private: + OpusEncCallbacks ope_callbacks; + OggOpusComments *ope_comments; + OggOpusEnc *ope_encoder; + std::vector<std::pair<std::string, std::string> > opus_comments; +private: + static int CallbackWrite(void *user_data, const unsigned char *ptr, opus_int32 len) + { + return reinterpret_cast<OpusStreamWriter*>(user_data)->CallbackWriteImpl(ptr, len); + } + static int CallbackClose(void *user_data) + { + return reinterpret_cast<OpusStreamWriter*>(user_data)->CallbackCloseImpl(); + } + int CallbackWriteImpl(const unsigned char *ptr, opus_int32 len) + { + if(len < 0) + { + return 1; + } + if(!ptr && len > 0) + { + return 1; + } + buf.assign(ptr, ptr + len); + WriteBuffer(); + return 0; + } + int CallbackCloseImpl() + { + return 0; + } +private: + void AddCommentField(const std::string &field, const mpt::ustring &data) + { + if(!field.empty() && !data.empty()) + { + opus_comments.push_back(std::make_pair(field, mpt::ToCharset(mpt::Charset::UTF8, data))); + } + } +public: + OpusStreamWriter(std::ostream &stream, const Encoder::Settings &settings, const FileTags &tags) + : StreamWriterBase(stream) + { + ope_callbacks.write = &CallbackWrite; + ope_callbacks.close = &CallbackClose; + opus_comments.clear(); + + bool opus_cbr = (settings.Mode == Encoder::ModeCBR); + int opus_bitrate = settings.Bitrate * 1000; + + if(settings.Tags) + { + AddCommentField("ENCODER", tags.encoder); + AddCommentField("SOURCEMEDIA", U_("tracked music file")); + AddCommentField("TITLE", tags.title ); + AddCommentField("ARTIST", tags.artist ); + AddCommentField("ALBUM", tags.album ); + AddCommentField("DATE", tags.year ); + AddCommentField("COMMENT", tags.comments ); + AddCommentField("GENRE", tags.genre ); + AddCommentField("CONTACT", tags.url ); + AddCommentField("BPM", tags.bpm ); // non-standard + AddCommentField("TRACKNUMBER", tags.trackno ); + } + + int ope_error = 0; + + ope_comments = ope_comments_create(); + if(settings.Tags && ope_comments) + { + for(const auto & comment : opus_comments) + { + ope_comments_add(ope_comments, comment.first.c_str(), comment.second.c_str()); + } + } + + ope_encoder = ope_encoder_create_callbacks(&ope_callbacks, this, ope_comments, settings.Samplerate, settings.Channels, settings.Channels > 2 ? 1 : 0, &ope_error); + + opus_int32 ctl_serial = mpt::random<uint32>(theApp.PRNG()); + ope_encoder_ctl(ope_encoder, OPE_SET_SERIALNO(ctl_serial)); + + opus_int32 ctl_bitrate = opus_bitrate; + ope_encoder_ctl(ope_encoder, OPUS_SET_BITRATE(ctl_bitrate)); + + if(opus_cbr) + { + opus_int32 ctl_vbr = 0; + ope_encoder_ctl(ope_encoder, OPUS_SET_VBR(ctl_vbr)); + } else + { + opus_int32 ctl_vbr = 1; + ope_encoder_ctl(ope_encoder, OPUS_SET_VBR(ctl_vbr)); + opus_int32 ctl_vbrcontraint = 0; + ope_encoder_ctl(ope_encoder, OPUS_SET_VBR_CONSTRAINT(ctl_vbrcontraint)); + } + + opus_int32 complexity = settings.Details.OpusComplexity; + if(complexity >= 0) + { + ope_encoder_ctl(ope_encoder, OPUS_SET_COMPLEXITY(complexity)); + } + + ope_encoder_flush_header(ope_encoder); + + } + void WriteInterleaved(size_t count, const float *interleaved) override + { + while(count > 0) + { + ope_encoder_write_float(ope_encoder, interleaved, mpt::saturate_cast<int>(count)); + count -= static_cast<size_t>(mpt::saturate_cast<int>(count)); + } + } + void WriteFinalize() override + { + ope_encoder_drain(ope_encoder); + } + virtual ~OpusStreamWriter() + { + ope_encoder_destroy(ope_encoder); + ope_encoder = NULL; + + ope_comments_destroy(ope_comments); + ope_comments = NULL; + } +}; + +#endif // MPT_WITH_OGG + + + +OggOpusEncoder::OggOpusEncoder() +{ + SetTraits(BuildTraits()); +} + + +bool OggOpusEncoder::IsAvailable() const +{ +#if defined(MPT_WITH_OPUS) && defined(MPT_WITH_OPUSENC) + return true; +#else + return false; +#endif +} + + +std::unique_ptr<IAudioStreamEncoder> OggOpusEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + if(!IsAvailable()) + { + return nullptr; + } +#if defined(MPT_WITH_OPUS) && defined(MPT_WITH_OPUSENC) + return std::make_unique<OpusStreamWriter>(file, settings, tags); +#else + return nullptr; +#endif +} + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.h new file mode 100644 index 00000000..5cda9b5c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderOpus.h @@ -0,0 +1,36 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class OggOpusEncoder : public EncoderFactoryBase +{ + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsAvailable() const override; + +public: + + OggOpusEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.cpp new file mode 100644 index 00000000..bfda346e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.cpp @@ -0,0 +1,142 @@ +/* + * StreamEncoderRAW.cpp + * -------------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderRAW.h" + +#include "Mptrack.h" +#include "TrackerSettings.h" + +#include "../common/mptFileIO.h" +#include "../soundlib/Sndfile.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class RawStreamWriter : public IAudioStreamEncoder +{ +private: + const RAWEncoder &enc; + std::ostream &f; + Encoder::Settings settings; + +public: + RawStreamWriter(const RAWEncoder &enc_, std::ostream &file, const Encoder::Settings &settings_, const FileTags &tags) + : enc(enc_) + , f(file) + , settings(settings_) + { + MPT_ASSERT(settings.Format.GetSampleFormat().IsValid()); + MPT_ASSERT(settings.Samplerate > 0); + MPT_ASSERT(settings.Channels > 0); + MPT_UNREFERENCED_PARAMETER(tags); + } + SampleFormat GetSampleFormat() const override + { + return settings.Format.GetSampleFormat(); + } + void WriteInterleaved(std::size_t frameCount, const double *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const float *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int32 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) override + { + WriteInterleavedBE(f, settings.Channels, settings.Format, frameCount, interleaved); + } + void WriteCues(const std::vector<uint64> &cues) override + { + MPT_UNREFERENCED_PARAMETER(cues); + } + void WriteFinalize() override + { + // nothing + } + virtual ~RawStreamWriter() + { + // nothing + } +}; + + + +RAWEncoder::RAWEncoder() +{ + Encoder::Traits traits; + traits.fileExtension = P_("raw"); + traits.fileShortDescription = U_("Raw PCM"); + traits.fileDescription = U_("Headerless raw little-endian PCM"); + traits.encoderSettingsName = U_("RAW"); + traits.canTags = false; + traits.canCues = false; + traits.maxChannels = 4; + traits.samplerates = TrackerSettings::Instance().GetSampleRates(); + traits.modes = Encoder::ModeLossless; + traits.formats.push_back({ Encoder::Format::Encoding::Float, 64, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Float, 64, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Float, 32, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Float, 32, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 32, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 32, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 24, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 24, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 16, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 16, mpt::endian::big }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 8, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Unsigned, 8, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Alaw, 16, mpt::get_endian() }); + traits.formats.push_back({ Encoder::Format::Encoding::ulaw, 16, mpt::get_endian() }); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeLossless; + traits.defaultFormat = { Encoder::Format::Encoding::Float, 32, mpt::endian::little }; + SetTraits(traits); +} + + +bool RAWEncoder::IsAvailable() const +{ + return true; +} + + +std::unique_ptr<IAudioStreamEncoder> RAWEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + if(!IsAvailable()) + { + return nullptr; + } + return std::make_unique<RawStreamWriter>(*this, file, settings, tags); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.h new file mode 100644 index 00000000..be894252 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderRAW.h @@ -0,0 +1,41 @@ +/* + * StreamEncoderRAW.h + * ------------------ + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class RawStreamWriter; + + +class RAWEncoder : public EncoderFactoryBase +{ + + friend class RawStreamWriter; + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsAvailable() const override; + +public: + + RAWEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.cpp new file mode 100644 index 00000000..06e1849c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.cpp @@ -0,0 +1,103 @@ +/* + * StreamEncoderSettings.cpp + * ------------------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoderSettings.h" + +#include "TrackerSettings.h" + + +OPENMPT_NAMESPACE_BEGIN + + +static mpt::ustring GetDefaultYear() +{ + return mpt::ToUnicode(CTime::GetCurrentTime().Format(_T("%Y"))); +} + + +StoredTags::StoredTags(SettingsContainer &conf) + : artist(conf, U_("Export"), U_("TagArtist"), TrackerSettings::Instance().defaultArtist) + , album(conf, U_("Export"), U_("TagAlbum"), U_("")) + , trackno(conf, U_("Export"), U_("TagTrackNo"), U_("")) + , year(conf, U_("Export"), U_("TagYear"), GetDefaultYear()) + , url(conf, U_("Export"), U_("TagURL"), U_("")) + , genre(conf, U_("Export"), U_("TagGenre"), U_("")) +{ + return; +} + + +EncoderSettingsConf::EncoderSettingsConf(SettingsContainer &conf, const mpt::ustring &encoderName, bool cues, bool tags, uint32 samplerate, uint16 channels, Encoder::Mode mode, int bitrate, float quality, Encoder::Format format, int dither) + : Cues(conf, U_("Export"), encoderName + U_("_") + U_("Cues"), cues) + , Tags(conf, U_("Export"), encoderName + U_("_") + U_("Tags"), tags) + , Samplerate(conf, U_("Export"), encoderName + U_("_") + U_("Samplerate"), samplerate) + , Channels(conf, U_("Export"), encoderName + U_("_") + U_("Channels"), channels) + , Mode(conf, U_("Export"), encoderName + U_("_") + U_("Mode"), mode) + , Bitrate(conf, U_("Export"), encoderName + U_("_") + U_("Bitrate"), bitrate) + , Quality(conf, U_("Export"), encoderName + U_("_") + U_("Quality"), quality) + , Format2(conf, U_("Export"), encoderName + U_("_") + U_("Format2"), format) + , Dither(conf, U_("Export"), encoderName + U_("_") + U_("Dither"), dither) +{ + return; +} + + +EncoderSettingsConf::operator Encoder::Settings() const +{ + Encoder::Settings result; + result.Cues = Cues; + result.Tags = Tags; + result.Samplerate = Samplerate; + result.Channels = Channels; + result.Mode = Mode; + result.Bitrate = Bitrate; + result.Quality = Quality; + result.Format = Format2; + result.Dither = Dither; + return result; +} + + +StreamEncoderSettingsConf::StreamEncoderSettingsConf(SettingsContainer &conf, const mpt::ustring §ion) + : FLACCompressionLevel(conf, section, U_("FLACCompressionLevel"), Encoder::StreamSettings().FLACCompressionLevel) + , AUPaddingAlignHint(conf, section, U_("AUPaddingAlignHint"), Encoder::StreamSettings().AUPaddingAlignHint) + , MP3ID3v2MinPadding(conf, section, U_("MP3ID3v2MinPadding"), Encoder::StreamSettings().MP3ID3v2MinPadding) + , MP3ID3v2PaddingAlignHint(conf, section, U_("MP3ID3v2PaddingAlignHint"), Encoder::StreamSettings().MP3ID3v2PaddingAlignHint) + , MP3ID3v2WriteReplayGainTXXX(conf, section, U_("MP3ID3v2WriteReplayGainTXXX"), Encoder::StreamSettings().MP3ID3v2WriteReplayGainTXXX) + , MP3LameQuality(conf, section, U_("MP3LameQuality"), Encoder::StreamSettings().MP3LameQuality) + , MP3LameID3v2UseLame(conf, section, U_("MP3LameID3v2UseLame"), Encoder::StreamSettings().MP3LameID3v2UseLame) + , MP3LameCalculateReplayGain(conf, section, U_("MP3LameCalculateReplayGain"), Encoder::StreamSettings().MP3LameCalculateReplayGain) + , MP3LameCalculatePeakSample(conf, section, U_("MP3LameCalculatePeakSample"), Encoder::StreamSettings().MP3LameCalculatePeakSample) + , OpusComplexity(conf, section, U_("OpusComplexity"), Encoder::StreamSettings().OpusComplexity) +{ + return; +} + + +StreamEncoderSettingsConf::operator Encoder::StreamSettings() const +{ + Encoder::StreamSettings result; + result.FLACCompressionLevel = FLACCompressionLevel; + result.AUPaddingAlignHint = AUPaddingAlignHint; + result.MP3ID3v2MinPadding = MP3ID3v2MinPadding; + result.MP3ID3v2PaddingAlignHint = MP3ID3v2PaddingAlignHint; + result.MP3ID3v2WriteReplayGainTXXX = MP3ID3v2WriteReplayGainTXXX; + result.MP3LameQuality = MP3LameQuality; + result.MP3LameID3v2UseLame = MP3LameID3v2UseLame; + result.MP3LameCalculateReplayGain = MP3LameCalculateReplayGain; + result.MP3LameCalculatePeakSample = MP3LameCalculatePeakSample; + result.OpusComplexity = OpusComplexity; + return result; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.h new file mode 100644 index 00000000..c613b919 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderSettings.h @@ -0,0 +1,113 @@ +/* + * StreamEncoderSettings.h + * ----------------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" +#include "Settings.h" + + +OPENMPT_NAMESPACE_BEGIN + + + +template<> inline SettingValue ToSettingValue(const Encoder::Mode &val) +{ + switch(val) + { + case Encoder::ModeCBR: return SettingValue(U_("CBR"), "Encoder::Mode"); break; + case Encoder::ModeABR: return SettingValue(U_("ABR"), "Encoder::Mode"); break; + case Encoder::ModeVBR: return SettingValue(U_("VBR"), "Encoder::Mode"); break; + case Encoder::ModeQuality: return SettingValue(U_("Quality"), "Encoder::Mode"); break; + case Encoder::ModeLossless: return SettingValue(U_("Lossless"), "Encoder::Mode"); break; + default: return SettingValue(U_("Invalid"), "Encoder::Mode"); break; + } +} +template<> inline Encoder::Mode FromSettingValue(const SettingValue &val) +{ + ASSERT(val.GetTypeTag() == "Encoder::Mode"); + if(val.as<mpt::ustring>() == U_("")) { return Encoder::ModeInvalid; } + else if(val.as<mpt::ustring>() == U_("CBR")) { return Encoder::ModeCBR; } + else if(val.as<mpt::ustring>() == U_("ABR")) { return Encoder::ModeABR; } + else if(val.as<mpt::ustring>() == U_("VBR")) { return Encoder::ModeVBR; } + else if(val.as<mpt::ustring>() == U_("Quality")) { return Encoder::ModeQuality; } + else if(val.as<mpt::ustring>() == U_("Lossless")) { return Encoder::ModeLossless; } + else { return Encoder::ModeInvalid; } +} + + +template<> inline SettingValue ToSettingValue(const Encoder::Format &val) +{ + return SettingValue(val.AsInt(), "Encoder::Format"); +} +template<> inline Encoder::Format FromSettingValue(const SettingValue &val) +{ + MPT_ASSERT(val.GetTypeTag() == "Encoder::Format"); + return Encoder::Format::FromInt(val.as<int32>()); +} + + +struct StoredTags +{ + Setting<mpt::ustring> artist; + Setting<mpt::ustring> album; + Setting<mpt::ustring> trackno; + Setting<mpt::ustring> year; + Setting<mpt::ustring> url; + + Setting<mpt::ustring> genre; + + StoredTags(SettingsContainer &conf); + +}; + + +struct EncoderSettingsConf +{ + + Setting<bool> Cues; + Setting<bool> Tags; + + Setting<uint32> Samplerate; + Setting<uint16> Channels; + + Setting<Encoder::Mode> Mode; + Setting<int> Bitrate; + Setting<float> Quality; + Setting<Encoder::Format> Format2; + Setting<int> Dither; + + EncoderSettingsConf(SettingsContainer &conf, const mpt::ustring &encoderName, bool cues, bool tags, uint32 samplerate, uint16 channels, Encoder::Mode mode, int bitrate, float quality, Encoder::Format format, int dither); + + explicit operator Encoder::Settings() const; + +}; + + +struct StreamEncoderSettingsConf +{ + Setting<int32> FLACCompressionLevel; + Setting<uint32> AUPaddingAlignHint; + Setting<uint32> MP3ID3v2MinPadding; + Setting<uint32> MP3ID3v2PaddingAlignHint; + Setting<bool> MP3ID3v2WriteReplayGainTXXX; + Setting<int32> MP3LameQuality; + Setting<bool> MP3LameID3v2UseLame; + Setting<bool> MP3LameCalculateReplayGain; + Setting<bool> MP3LameCalculatePeakSample; + Setting<int32> OpusComplexity; + StreamEncoderSettingsConf(SettingsContainer &conf, const mpt::ustring §ion); + explicit operator Encoder::StreamSettings() const; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.cpp new file mode 100644 index 00000000..ea4f729e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.cpp @@ -0,0 +1,262 @@ +/* + * StreamEncoder.cpp + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderVorbis.h" + +#include "Mptrack.h" + +#ifdef MPT_WITH_OGG +#include <ogg/ogg.h> +#endif +#ifdef MPT_WITH_VORBIS +#include <vorbis/codec.h> +#endif +#ifdef MPT_WITH_VORBISENC +#include <vorbis/vorbisenc.h> +#endif + + + +OPENMPT_NAMESPACE_BEGIN + + + +static Encoder::Traits VorbisBuildTraits() + { + Encoder::Traits traits; +#if defined(MPT_WITH_OGG) && defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISENC) + traits.fileExtension = P_("ogg"); + traits.fileShortDescription = U_("Vorbis"); + traits.fileDescription = U_("Ogg Vorbis"); + traits.encoderSettingsName = U_("Vorbis"); + traits.canTags = true; + traits.maxChannels = 4; + traits.samplerates = mpt::make_vector(vorbis_samplerates); + traits.modes = Encoder::ModeABR | Encoder::ModeQuality; + traits.bitrates = mpt::make_vector(vorbis_bitrates); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeQuality; + traits.defaultBitrate = 160; + traits.defaultQuality = 0.5; +#endif + return traits; + } + +#if defined(MPT_WITH_OGG) && defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISENC) + +class VorbisStreamWriter : public StreamWriterBase +{ +private: + ogg_stream_state os; + ogg_page og; + ogg_packet op; + vorbis_info vi; + vorbis_comment vc; + vorbis_dsp_state vd; + vorbis_block vb; + int vorbis_channels; +private: + void WritePage() + { + buf.resize(og.header_len); + std::memcpy(buf.data(), og.header, og.header_len); + WriteBuffer(); + buf.resize(og.body_len); + std::memcpy(buf.data(), og.body, og.body_len); + WriteBuffer(); + } + void AddCommentField(const std::string &field, const mpt::ustring &data) + { + if(!field.empty() && !data.empty()) + { + vorbis_comment_add_tag(&vc, field.c_str(), mpt::ToCharset(mpt::Charset::UTF8, data).c_str()); + } + } +public: + VorbisStreamWriter(std::ostream &stream, const Encoder::Settings &settings, const FileTags &tags) + : StreamWriterBase(stream) + { + vorbis_channels = 0; + + vorbis_channels = settings.Channels; + + vorbis_info_init(&vi); + vorbis_comment_init(&vc); + + if(settings.Mode == Encoder::ModeQuality) + { + vorbis_encode_init_vbr(&vi, vorbis_channels, settings.Samplerate, settings.Quality); + } else + { + vorbis_encode_init(&vi, vorbis_channels, settings.Samplerate, -1, settings.Bitrate * 1000, -1); + } + + vorbis_analysis_init(&vd, &vi); + vorbis_block_init(&vd, &vb); + ogg_stream_init(&os, mpt::random<uint32>(theApp.PRNG())); + + if(settings.Tags) + { + AddCommentField("ENCODER", tags.encoder); + AddCommentField("SOURCEMEDIA", U_("tracked music file")); + AddCommentField("TITLE", tags.title ); + AddCommentField("ARTIST", tags.artist ); + AddCommentField("ALBUM", tags.album ); + AddCommentField("DATE", tags.year ); + AddCommentField("COMMENT", tags.comments ); + AddCommentField("GENRE", tags.genre ); + AddCommentField("CONTACT", tags.url ); + AddCommentField("BPM", tags.bpm ); // non-standard + AddCommentField("TRACKNUMBER", tags.trackno ); + } + + ogg_packet header; + ogg_packet header_comm; + ogg_packet header_code; + vorbis_analysis_headerout(&vd, &vc, &header, &header_comm, &header_code); + ogg_stream_packetin(&os, &header); + while(ogg_stream_flush(&os, &og)) + { + WritePage(); + } + ogg_stream_packetin(&os, &header_comm); + ogg_stream_packetin(&os, &header_code); + while(ogg_stream_flush(&os, &og)) + { + WritePage(); + } + + } + void WriteInterleaved(size_t count, const float *interleaved) override + { + size_t countTotal = count; + while(countTotal > 0) + { + int countChunk = mpt::saturate_cast<int>(countTotal); + countTotal -= countChunk; + float **buffer = vorbis_analysis_buffer(&vd, countChunk); + for(int frame = 0; frame < countChunk; ++frame) + { + for(int channel = 0; channel < vorbis_channels; ++channel) + { + buffer[channel][frame] = interleaved[frame*vorbis_channels+channel]; + } + } + vorbis_analysis_wrote(&vd, countChunk); + while(vorbis_analysis_blockout(&vd, &vb) == 1) + { + vorbis_analysis(&vb, NULL); + vorbis_bitrate_addblock(&vb); + while(vorbis_bitrate_flushpacket(&vd, &op)) + { + ogg_stream_packetin(&os, &op); + while(ogg_stream_pageout(&os, &og)) + { + WritePage(); + } + } + } + } + } + void WriteFinalize() override + { + vorbis_analysis_wrote(&vd, 0); + while(vorbis_analysis_blockout(&vd, &vb) == 1) + { + vorbis_analysis(&vb, NULL); + vorbis_bitrate_addblock(&vb); + while(vorbis_bitrate_flushpacket(&vd, &op)) + { + ogg_stream_packetin(&os, &op); + while(ogg_stream_flush(&os, &og)) + { + WritePage(); + if(ogg_page_eos(&og)) + { + break; + } + } + } + } + } + virtual ~VorbisStreamWriter() + { + ogg_stream_clear(&os); + vorbis_block_clear(&vb); + vorbis_dsp_clear(&vd); + vorbis_comment_clear(&vc); + vorbis_info_clear(&vi); + } +}; + +#endif // MPT_WITH_OGG && MPT_WITH_VORBIS && MPT_WITH_VORBISENC + + + +VorbisEncoder::VorbisEncoder() +{ + SetTraits(VorbisBuildTraits()); +} + + +bool VorbisEncoder::IsAvailable() const +{ +#if defined(MPT_WITH_OGG) && defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISENC) + return true; +#else + return false; +#endif +} + + +std::unique_ptr<IAudioStreamEncoder> VorbisEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ +#if defined(MPT_WITH_OGG) && defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISENC) + return std::make_unique<VorbisStreamWriter>(file, settings, tags); +#else + MPT_UNREFERENCED_PARAMETER(file); + MPT_UNREFERENCED_PARAMETER(settings); + MPT_UNREFERENCED_PARAMETER(tags); + return nullptr; +#endif +} + + +bool VorbisEncoder::IsBitrateSupported(int samplerate, int channels, int bitrate) const +{ +#if defined(MPT_WITH_OGG) && defined(MPT_WITH_VORBIS) && defined(MPT_WITH_VORBISENC) + vorbis_info vi; + vorbis_info_init(&vi); + bool result = (0 == vorbis_encode_init(&vi, channels, samplerate, -1, bitrate * 1000, -1)); + vorbis_info_clear(&vi); + return result; +#else + MPT_UNREFERENCED_PARAMETER(samplerate); + MPT_UNREFERENCED_PARAMETER(channels); + MPT_UNREFERENCED_PARAMETER(bitrate); + return false; +#endif +} + + +mpt::ustring VorbisEncoder::DescribeQuality(float quality) const +{ + static constexpr int q_table[11] = { 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 500 }; // http://wiki.hydrogenaud.io/index.php?title=Recommended_Ogg_Vorbis + int q = Clamp(mpt::saturate_round<int>(quality * 10.0f), 0, 10); + return MPT_UFORMAT("Q{} (~{} kbit)")(mpt::ufmt::fix(quality * 10.0f, 1), q_table[q]); +} + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.h new file mode 100644 index 00000000..b73dbeec --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderVorbis.h @@ -0,0 +1,38 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class VorbisEncoder : public EncoderFactoryBase +{ + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsBitrateSupported(int samplerate, int channels, int bitrate) const override; + mpt::ustring DescribeQuality(float quality) const override; + bool IsAvailable() const override; + +public: + + VorbisEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.cpp new file mode 100644 index 00000000..b3b888ae --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.cpp @@ -0,0 +1,188 @@ +/* + * StreamEncoder.cpp + * ----------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "StreamEncoder.h" +#include "StreamEncoderWAV.h" + +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#include "Mptrack.h" +#include "TrackerSettings.h" + +#include "../soundlib/Sndfile.h" +#include "../soundlib/WAVTools.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class WavStreamWriter : public IAudioStreamEncoder +{ +private: + + const WAVEncoder &enc; + std::ostream &f; + mpt::IO::OFile<std::ostream> ff; + std::unique_ptr<WAVWriter> fileWAV; + Encoder::Settings settings; + +public: + WavStreamWriter(const WAVEncoder &enc_, std::ostream &file, const Encoder::Settings &settings_, const FileTags &tags) + : enc(enc_) + , f(file) + , ff(f) + , fileWAV(nullptr) + , settings(settings_) + { + + MPT_ASSERT(settings.Format.GetSampleFormat().IsValid()); + MPT_ASSERT(settings.Samplerate > 0); + MPT_ASSERT(settings.Channels > 0); + + fileWAV = std::make_unique<WAVWriter>(ff); + fileWAV->WriteFormat(settings.Samplerate, settings.Format.GetSampleFormat().GetBitsPerSample(), settings.Channels, settings.Format.GetSampleFormat().IsFloat() ? WAVFormatChunk::fmtFloat : WAVFormatChunk::fmtPCM); + + if(settings.Tags) + { + fileWAV->WriteMetatags(tags); + } + + fileWAV->StartChunk(RIFFChunk::iddata); + + } + SampleFormat GetSampleFormat() const override + { + return settings.Format.GetSampleFormat(); + } + void WriteInterleaved(std::size_t frameCount, const double *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const float *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const int32 *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const int24 *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const int16 *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const int8 *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteInterleaved(std::size_t frameCount, const uint8 *interleaved) override + { + fileWAV->WriteBeforeDirect(); + auto result = WriteInterleavedLE(f, settings.Channels, settings.Format, frameCount, interleaved); + fileWAV->WriteAfterDirect(result.first, result.second); + } + void WriteCues(const std::vector<uint64> &cues) override + { + if(!cues.empty()) + { + // Cue point header + fileWAV->StartChunk(RIFFChunk::idcue_); + uint32le numPoints; + numPoints = mpt::saturate_cast<uint32>(cues.size()); + fileWAV->Write(numPoints); + + // Write all cue points + uint32 index = 0; + for(auto cue : cues) + { + WAVCuePoint cuePoint{}; + cuePoint.id = index++; + cuePoint.position = static_cast<uint32>(cue); + cuePoint.riffChunkID = static_cast<uint32>(RIFFChunk::iddata); + cuePoint.chunkStart = 0; // we use no Wave List Chunk (wavl) as we have only one data block, so this should be 0. + cuePoint.blockStart = 0; // ditto + cuePoint.offset = cuePoint.position; + fileWAV->Write(cuePoint); + } + } + } + void WriteFinalize() override + { + fileWAV->Finalize(); + } + virtual ~WavStreamWriter() + { + fileWAV = nullptr; + } +}; + + + +WAVEncoder::WAVEncoder() +{ + Encoder::Traits traits; + traits.fileExtension = P_("wav"); + traits.fileShortDescription = U_("Wave"); + traits.fileDescription = U_("Microsoft RIFF Wave"); + traits.encoderSettingsName = U_("Wave"); + traits.canTags = true; + traits.canCues = true; + traits.maxChannels = 4; + traits.samplerates = TrackerSettings::Instance().GetSampleRates(); + traits.modes = Encoder::ModeLossless; + traits.formats.push_back({ Encoder::Format::Encoding::Float, 64, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Float, 32, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 32, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 24, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Integer, 16, mpt::endian::little }); + traits.formats.push_back({ Encoder::Format::Encoding::Unsigned, 8, mpt::endian::little }); + traits.defaultSamplerate = 48000; + traits.defaultChannels = 2; + traits.defaultMode = Encoder::ModeLossless; + traits.defaultFormat = { Encoder::Format::Encoding::Float, 32, mpt::endian::little }; + SetTraits(traits); +} + + +bool WAVEncoder::IsAvailable() const +{ + return true; +} + + +std::unique_ptr<IAudioStreamEncoder> WAVEncoder::ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const +{ + if(!IsAvailable()) + { + return nullptr; + } + return std::make_unique<WavStreamWriter>(*this, file, settings, tags); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.h b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.h new file mode 100644 index 00000000..05412fe0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/StreamEncoderWAV.h @@ -0,0 +1,41 @@ +/* + * StreamEncoder.h + * --------------- + * Purpose: Exporting streamed music files. + * Notes : none + * Authors: Joern Heusipp + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" + + +OPENMPT_NAMESPACE_BEGIN + + +class WavStreamWriter; + + +class WAVEncoder : public EncoderFactoryBase +{ + + friend class WavStreamWriter; + +public: + + std::unique_ptr<IAudioStreamEncoder> ConstructStreamEncoder(std::ostream &file, const Encoder::Settings &settings, const FileTags &tags) const override; + bool IsAvailable() const override; + +public: + + WAVEncoder(); + +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.cpp new file mode 100644 index 00000000..b9373a59 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.cpp @@ -0,0 +1,429 @@ +/* + * TempoSwingDialog.cpp + * -------------------- + * Purpose: Implementation of the tempo swing configuration dialog. + * 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 "TempoSwingDialog.h" +#include "Mainfrm.h" + + +OPENMPT_NAMESPACE_BEGIN + +void CTempoSwingDlg::RowCtls::SetValue(TempoSwing::value_type v) +{ + int32 val = Util::muldivr(static_cast<int32>(v) - TempoSwing::Unity, CTempoSwingDlg::SliderUnity, TempoSwing::Unity); + valueSlider.SetPos(val); +} + + +TempoSwing::value_type CTempoSwingDlg::RowCtls::GetValue() const +{ + return Util::muldivr(valueSlider.GetPos(), TempoSwing::Unity, SliderUnity) + TempoSwing::Unity; +} + + +BEGIN_MESSAGE_MAP(CTempoSwingDlg, CDialog) + //{{AFX_MSG_MAP(CTempoSwingDlg) + ON_WM_VSCROLL() + ON_COMMAND(IDC_BUTTON1, &CTempoSwingDlg::OnReset) + ON_COMMAND(IDC_BUTTON2, &CTempoSwingDlg::OnUseGlobal) + ON_COMMAND(IDC_CHECK1, &CTempoSwingDlg::OnToggleGroup) + ON_EN_CHANGE(IDC_EDIT1, &CTempoSwingDlg::OnGroupChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + +int CTempoSwingDlg::m_groupSize = 1; + +CTempoSwingDlg::CTempoSwingDlg(CWnd *parent, const TempoSwing ¤tTempoSwing, CSoundFile &sndFile, PATTERNINDEX pattern) + : CDialog(IDD_TEMPO_SWING, parent) + , m_container(*this) + , m_scrollPos(0) + , m_tempoSwing(currentTempoSwing) + , m_origTempoSwing(pattern == PATTERNINDEX_INVALID ? sndFile.m_tempoSwing : sndFile.Patterns[pattern].GetTempoSwing()) + , m_sndFile(sndFile) + , m_pattern(pattern) +{ + m_groupSize = std::min(m_groupSize, static_cast<int>(m_tempoSwing.size())); +} + + +void CTempoSwingDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_CHECK1, m_checkGroup); + DDX_Control(pDX, IDC_SCROLLBAR1, m_scrollBar); + DDX_Control(pDX, IDC_CONTAINER, m_container); +} + + +BOOL CTempoSwingDlg::OnInitDialog() +{ + struct Measurements + { + enum + { + edRowLabelWidth = 64, // Label "Row 999:" + edSliderWidth = 220, // Setting slider + edSliderHeight = 20, // Setting slider + edValueLabelWidth = 64, // Label "100%" + edPaddingX = 8, // Spacing between elements + edPaddingY = 4, // Spacing between elements + edPaddingTop = 64, // Spacing from top of dialog + edRowHeight = edSliderHeight + edPaddingY, // Height of one set of controls + edFooterHeight = 32, // Buttons + edScrollbarWidth = 16, // Width of optional scrollbar + }; + + const int rowLabelWidth; + const int sliderWidth; + const int sliderHeight; + const int valueLabelWidth; + const int paddingX; + const int paddingY; + const int paddingTop; + const int rowHeight; + const int footerHeight; + const int scrollbarWidth; + + Measurements(HWND hWnd) + : rowLabelWidth(Util::ScalePixels(edRowLabelWidth, hWnd)) + , sliderWidth(Util::ScalePixels(edSliderWidth, hWnd)) + , sliderHeight(Util::ScalePixels(edSliderHeight, hWnd)) + , valueLabelWidth(Util::ScalePixels(edValueLabelWidth, hWnd)) + , paddingX(Util::ScalePixels(edPaddingX, hWnd)) + , paddingY(Util::ScalePixels(edPaddingY, hWnd)) + , paddingTop(Util::ScalePixels(edPaddingTop, hWnd)) + , rowHeight(Util::ScalePixels(edRowHeight, hWnd)) + , footerHeight(Util::ScalePixels(edFooterHeight, hWnd)) + , scrollbarWidth(Util::ScalePixels(edScrollbarWidth, hWnd)) + { } + }; + + CDialog::OnInitDialog(); + Measurements m(m_hWnd); + CRect windowRect, rect; + GetWindowRect(windowRect); + GetClientRect(rect); + windowRect.bottom = windowRect.top + windowRect.Height() - rect.Height(); + + CRect mainWindowRect; + CMainFrame::GetMainFrame()->GetClientRect(mainWindowRect); + + const int realHeight = static_cast<int>(m_tempoSwing.size()) * m.rowHeight; + const int displayHeight = std::min(realHeight, static_cast<int>(mainWindowRect.bottom - windowRect.Height() - m.paddingTop - m.footerHeight)); + + CRect containerRect; + m_container.GetClientRect(containerRect); + containerRect.bottom = displayHeight; + m_container.SetWindowPos(nullptr, 0, m.paddingTop, rect.right - m.scrollbarWidth, containerRect.bottom, SWP_NOZORDER); + m_container.ModifyStyleEx(0, WS_EX_CONTROLPARENT, 0); + + // Need scrollbar? + if(realHeight > displayHeight) + { + SCROLLINFO info; + info.cbSize = sizeof(info); + info.fMask = SIF_ALL; + info.nMin = 0; + info.nMax = realHeight; + info.nPage = displayHeight; + info.nTrackPos = info.nPos = 0; + m_scrollBar.SetScrollInfo(&info, FALSE); + + CRect scrollRect; + m_scrollBar.GetClientRect(scrollRect); + m_scrollBar.SetWindowPos(nullptr, containerRect.right, m.paddingTop, scrollRect.Width(), displayHeight, SWP_NOZORDER); + } else + { + m_scrollBar.ShowWindow(SW_HIDE); + } + + rect.DeflateRect(m.paddingX, 0/* m.paddingTop*/, m.paddingX + m.scrollbarWidth, 0); + + GetDlgItem(IDC_BUTTON2)->ShowWindow((m_pattern != PATTERNINDEX_INVALID) ? SW_SHOW : SW_HIDE); + + m_controls.resize(m_tempoSwing.size()); + for(size_t i = 0; i < m_controls.size(); i++) + { + m_controls[i] = std::make_unique<RowCtls>(); + auto &r = m_controls[i]; + // Row label + r->rowLabel.Create(MPT_CFORMAT("Row {}:")(i + 1), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(rect.left, rect.top, rect.right, rect.top + m.rowHeight), &m_container); + r->rowLabel.SetFont(GetFont()); + + // Value label + r->valueLabel.Create(_T("100%"), WS_CHILD | WS_VISIBLE | SS_CENTERIMAGE, CRect(rect.right - m.valueLabelWidth, rect.top, rect.right, rect.top + m.sliderHeight), &m_container); + r->valueLabel.SetFont(GetFont()); + + // Value slider + r->valueSlider.Create(WS_CHILD | WS_VISIBLE | WS_TABSTOP | TBS_TOOLTIPS | TBS_AUTOTICKS, CRect(rect.left + m.rowLabelWidth, rect.top, rect.right - m.valueLabelWidth, rect.top + m.sliderHeight), &m_container, 0xFFFF); + r->valueSlider.SetFont(GetFont()); + r->valueSlider.SetRange(-SliderResolution / 2, SliderResolution / 2); + r->valueSlider.SetTicFreq(SliderResolution / 8); + r->valueSlider.SetPageSize(SliderResolution / 8); + r->valueSlider.SetPos(1); // Work around https://bugs.winehq.org/show_bug.cgi?id=41909 + r->SetValue(m_tempoSwing[i]); + rect.MoveToY(rect.top + m.rowHeight); + } + + ((CSpinButtonCtrl *)GetDlgItem(IDC_SPIN1))->SetRange32(1, static_cast<int>(m_tempoSwing.size())); + SetDlgItemInt(IDC_EDIT1, m_groupSize); + OnToggleGroup(); + + m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider))); + rect.MoveToY(m.paddingTop + containerRect.bottom + m.paddingY); + { + // Buttons at dialog bottom + CRect buttonRect; + for(auto i : { IDOK, IDCANCEL, IDC_BUTTON2 }) + { + auto wnd = GetDlgItem(i); + wnd->GetWindowRect(buttonRect); + wnd->SetWindowPos(nullptr, buttonRect.left - windowRect.left - GetSystemMetrics(SM_CXEDGE), rect.top, 0, 0, SWP_NOSIZE | SWP_NOOWNERZORDER); + } + } + + windowRect.bottom += displayHeight + m.paddingTop + m.footerHeight; + SetWindowPos(nullptr, 0, 0, windowRect.Width(), windowRect.Height(), SWP_NOMOVE | SWP_NOOWNERZORDER); + EnableToolTips(); + + return TRUE; +} + + +void CTempoSwingDlg::OnOK() +{ + CDialog::OnOK(); + // If this is the default setup, just clear the vector. + if(m_pattern == PATTERNINDEX_INVALID) + { + if(static_cast<size_t>(std::count(m_tempoSwing.begin(), m_tempoSwing.end(), static_cast<TempoSwing::value_type>(TempoSwing::Unity))) == m_tempoSwing.size()) + { + m_tempoSwing.clear(); + } + } else + { + if(m_tempoSwing == m_sndFile.m_tempoSwing) + { + m_tempoSwing.clear(); + } + } + OnClose(); +} + + +void CTempoSwingDlg::OnCancel() +{ + CDialog::OnCancel(); + OnClose(); +} + + +void CTempoSwingDlg::OnClose() +{ + // Restore original swing properties after preview + if(m_pattern == PATTERNINDEX_INVALID) + { + m_sndFile.m_tempoSwing = m_origTempoSwing; + } else + { + m_sndFile.Patterns[m_pattern].SetTempoSwing(m_origTempoSwing); + } +} + + +void CTempoSwingDlg::OnReset() +{ + for(size_t i = 0; i < m_controls.size(); i++) + { + m_controls[i]->valueSlider.SetPos(0); + } + m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider))); +} + + +void CTempoSwingDlg::OnUseGlobal() +{ + if(m_sndFile.m_tempoSwing.empty()) + { + OnReset(); + return; + } + for(size_t i = 0; i < m_tempoSwing.size(); i++) + { + m_controls[i]->SetValue(m_sndFile.m_tempoSwing[i % m_sndFile.m_tempoSwing.size()]); + } + m_container.OnHScroll(0, 0, reinterpret_cast<CScrollBar *>(&(m_controls[0]->valueSlider))); +} + + +void CTempoSwingDlg::OnToggleGroup() +{ + const BOOL checked = m_checkGroup.GetCheck() != BST_UNCHECKED; + GetDlgItem(IDC_EDIT1)->EnableWindow(checked); + GetDlgItem(IDC_SPIN1)->EnableWindow(checked); +} + + +void CTempoSwingDlg::OnGroupChanged() +{ + int val = GetDlgItemInt(IDC_EDIT1); + if(val > 0) m_groupSize = std::min(val, static_cast<int>(m_tempoSwing.size())); +} + + +void CTempoSwingDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) +{ + if(pScrollBar == &m_scrollBar) + { + // Get the minimum and maximum scrollbar positions. + int minpos; + int maxpos; + pScrollBar->GetScrollRange(&minpos, &maxpos); + + SCROLLINFO sbInfo; + pScrollBar->GetScrollInfo(&sbInfo); + + // Get the current position of scroll box. + int curpos = pScrollBar->GetScrollPos(); + + // Determine the new position of scroll box. + switch(nSBCode) + { + case SB_LEFT: // Scroll to far left. + curpos = minpos; + break; + + case SB_RIGHT: // Scroll to far right. + curpos = maxpos; + break; + + case SB_ENDSCROLL: // End scroll. + m_container.Invalidate(); + break; + + case SB_LINELEFT: // Scroll left. + if(curpos > minpos) + curpos--; + break; + + case SB_LINERIGHT: // Scroll right. + if(curpos < maxpos) + curpos++; + break; + + case SB_PAGELEFT: // Scroll one page left. + if(curpos > minpos) + { + curpos = std::max(minpos, curpos - static_cast<int>(sbInfo.nPage)); + } + break; + + case SB_PAGERIGHT: // Scroll one page right. + if(curpos < maxpos) + { + curpos = std::min(maxpos, curpos + static_cast<int>(sbInfo.nPage)); + } + break; + + case SB_THUMBPOSITION: // Scroll to absolute position. nPos is the position + curpos = nPos; // of the scroll box at the end of the drag operation. + break; + + case SB_THUMBTRACK: // Drag scroll box to specified position. nPos is the + curpos = nPos; // position that the scroll box has been dragged to. + break; + } + + // Set the new position of the thumb (scroll box). + pScrollBar->SetScrollPos(curpos); + + m_container.ScrollWindowEx(0, m_scrollPos - curpos, nullptr, nullptr, nullptr, nullptr, SW_SCROLLCHILDREN | SW_INVALIDATE | SW_ERASE); + m_scrollPos = curpos; + } + + CDialog::OnVScroll(nSBCode, nPos, pScrollBar); +} + + +// Scrollable container for the sliders +BEGIN_MESSAGE_MAP(CTempoSwingDlg::SliderContainer, CDialog) + //{{AFX_MSG_MAP(CTempoSwingDlg::SliderContainer) + ON_WM_HSCROLL() + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CTempoSwingDlg::SliderContainer::OnToolTipNotify) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void CTempoSwingDlg::SliderContainer::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar) +{ + if(m_parent.m_checkGroup.GetCheck() != BST_UNCHECKED) + { + // Edit groups + size_t editedGroup = 0; + int editedValue = reinterpret_cast<CSliderCtrl *>(pScrollBar)->GetPos(); + for(size_t i = 0; i < m_parent.m_controls.size(); i++) + { + if(m_parent.m_controls[i]->valueSlider.m_hWnd == pScrollBar->m_hWnd) + { + editedGroup = (i / m_parent.m_groupSize) % 2u; + break; + } + } + for(size_t i = 0; i < m_parent.m_controls.size(); i++) + { + if((i / m_parent.m_groupSize) % 2u == editedGroup) + { + m_parent.m_controls[i]->valueSlider.SetPos(editedValue); + } + } + } + + for(size_t i = 0; i < m_parent.m_controls.size(); i++) + { + m_parent.m_tempoSwing[i] = m_parent.m_controls[i]->GetValue(); + } + m_parent.m_tempoSwing.Normalize(); + // Apply preview + if(m_parent.m_pattern == PATTERNINDEX_INVALID) + { + m_parent.m_sndFile.m_tempoSwing = m_parent.m_tempoSwing; + } else + { + m_parent.m_sndFile.Patterns[m_parent.m_pattern].SetTempoSwing(m_parent.m_tempoSwing); + } + + for(size_t i = 0; i < m_parent.m_tempoSwing.size(); i++) + { + TCHAR s[32]; + wsprintf(s, _T("%i%%"), Util::muldivr(m_parent.m_tempoSwing[i], 100, TempoSwing::Unity)); + m_parent.m_controls[i]->valueLabel.SetWindowText(s); + } + + CStatic::OnHScroll(nSBCode, nPos, pScrollBar); +} + + +BOOL CTempoSwingDlg::SliderContainer::OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT*)pNMHDR; + for(size_t i = 0; i < m_parent.m_controls.size(); i++) + { + if((HWND)pNMHDR->idFrom == m_parent.m_controls[i]->valueSlider.m_hWnd) + { + int32 val = Util::muldivr(m_parent.m_tempoSwing[i], 100, TempoSwing::Unity) - 100; + wsprintf(pTTT->szText, _T("%s%d"), val > 0 ? _T("+") : _T(""), val); + return TRUE; + } + } + return FALSE; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.h new file mode 100644 index 00000000..38fbb767 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TempoSwingDialog.h @@ -0,0 +1,77 @@ +/* + * TempoSwingDialog.h + * ------------------ + * Purpose: Implementation of the tempo swing configuration dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +class CSoundFile; +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +class CTempoSwingDlg: public CDialog +{ +protected: + // Scrollable container for the sliders + class SliderContainer : public CStatic + { + public: + CTempoSwingDlg &m_parent; + + SliderContainer(CTempoSwingDlg &parent) : m_parent(parent) { } + + afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar); + afx_msg BOOL OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *); + DECLARE_MESSAGE_MAP() + }; + + enum { SliderResolution = 1000, SliderUnity = SliderResolution / 2 }; + struct RowCtls + { + CStatic rowLabel, valueLabel; + CSliderCtrl valueSlider; + + void SetValue(TempoSwing::value_type v); + TempoSwing::value_type GetValue() const; + }; + std::vector<std::unique_ptr<RowCtls>> m_controls; + + CButton m_checkGroup; + CScrollBar m_scrollBar; + SliderContainer m_container; + + int m_scrollPos; + static int m_groupSize; + +public: + TempoSwing m_tempoSwing; + const TempoSwing m_origTempoSwing; + CSoundFile &m_sndFile; + PATTERNINDEX m_pattern; + +public: + CTempoSwingDlg(CWnd *parent, const TempoSwing ¤tTempoSwing, CSoundFile &sndFile, PATTERNINDEX pattern = PATTERNINDEX_INVALID); + +protected: + virtual void DoDataExchange(CDataExchange* pDX); + virtual BOOL OnInitDialog(); + virtual void OnOK(); + virtual void OnCancel(); + void OnClose(); + afx_msg void OnReset(); + afx_msg void OnUseGlobal(); + afx_msg void OnToggleGroup(); + afx_msg void OnGroupChanged(); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.cpp new file mode 100644 index 00000000..dbf081a1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.cpp @@ -0,0 +1,1605 @@ +/* + * TrackerSettings.cpp + * ------------------- + * Purpose: Code for managing, loading and saving all applcation settings. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Mptrack.h" +#include "Moddoc.h" +#include "Mainfrm.h" +#include "mpt/environment/environment.hpp" +#include "mpt/uuid/uuid.hpp" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "../common/version.h" +#include "UpdateCheck.h" +#include "Mpdlgs.h" +#include "../common/mptStringBuffer.h" +#include "TrackerSettings.h" +#include "../common/misc_util.h" +#include "PatternClipboard.h" +#include "../common/ComponentManager.h" +#include "ExceptionHandler.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/Tables.h" +#include "../common/mptFileIO.h" +#include "../soundlib/tuningcollection.h" +#include "TuningDialog.h" + + +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +#define OLD_SOUNDSETUP_REVERSESTEREO 0x20 +#define OLD_SOUNDSETUP_SECONDARY 0x40 +#define OLD_SOUNDSETUP_NOBOOSTTHREADPRIORITY 0x80 + +#ifndef NO_EQ + +constexpr EQPreset FlatEQPreset = {"Flat", {16, 16, 16, 16, 16, 16}, {125, 300, 600, 1250, 4000, 8000}}; + +#endif // !NO_EQ + + +TrackerSettings &TrackerSettings::Instance() +{ + return theApp.GetTrackerSettings(); +} + + +static Version GetPreviousSettingsVersion(const mpt::ustring &iniVersion) +{ + if(!iniVersion.empty()) + { + return Version::Parse(iniVersion); + } else + { + // No version stored. + // This is the first run, thus set the previous version to our current + // version which will avoid running all settings upgrade code. + return Version::Current(); + } +} + + +mpt::ustring SettingsModTypeToString(MODTYPE modtype) +{ + return mpt::ToUnicode(mpt::Charset::UTF8, CSoundFile::GetModSpecifications(modtype).fileExtension); +} + +MODTYPE SettingsStringToModType(const mpt::ustring &str) +{ + return CModSpecifications::ExtensionToType(mpt::ToCharset(mpt::Charset::UTF8, str)); +} + + +static uint32 GetDefaultPatternSetup() +{ + return PATTERN_PLAYNEWNOTE | PATTERN_EFFECTHILIGHT + | PATTERN_CENTERROW | PATTERN_DRAGNDROPEDIT + | PATTERN_FLATBUTTONS | PATTERN_NOEXTRALOUD | PATTERN_2NDHIGHLIGHT + | PATTERN_STDHIGHLIGHT | PATTERN_SHOWPREVIOUS | PATTERN_CONTSCROLL + | PATTERN_SYNCMUTE | PATTERN_AUTODELAY | PATTERN_NOTEFADE + | PATTERN_SHOWDEFAULTVOLUME | PATTERN_LIVEUPDATETREE | PATTERN_SYNCSAMPLEPOS; +} + + +void SampleUndoBufferSize::CalculateSize() +{ + if(sizePercent < 0) + sizePercent = 0; + MEMORYSTATUSEX memStatus; + memStatus.dwLength = sizeof(MEMORYSTATUSEX); + GlobalMemoryStatusEx(&memStatus); + // The setting is a percentage of the memory that's actually *available* to OpenMPT, which is a max of 4GB in 32-bit mode. + sizeByte = mpt::saturate_cast<size_t>(std::min(memStatus.ullTotalPhys, DWORDLONG(SIZE_T_MAX)) * sizePercent / 100); + + // Pretend there's at least one MiB of memory (haha) + if(sizePercent != 0 && sizeByte < 1 * 1024 * 1024) + { + sizeByte = 1 * 1024 * 1024; + } +} + + +DebugSettings::DebugSettings(SettingsContainer &conf) + : conf(conf) + // Debug +#if !defined(MPT_LOG_IS_DISABLED) + , DebugLogLevel(conf, U_("Debug"), U_("LogLevel"), static_cast<int>(mpt::log::GlobalLogLevel)) + , DebugLogFacilitySolo(conf, U_("Debug"), U_("LogFacilitySolo"), std::string()) + , DebugLogFacilityBlocked(conf, U_("Debug"), U_("LogFacilityBlocked"), std::string()) + , DebugLogFileEnable(conf, U_("Debug"), U_("LogFileEnable"), mpt::log::FileEnabled) + , DebugLogDebuggerEnable(conf, U_("Debug"), U_("LogDebuggerEnable"), mpt::log::DebuggerEnabled) + , DebugLogConsoleEnable(conf, U_("Debug"), U_("LogConsoleEnable"), mpt::log::ConsoleEnabled) +#endif + , DebugTraceEnable(conf, U_("Debug"), U_("TraceEnable"), false) + , DebugTraceSize(conf, U_("Debug"), U_("TraceSize"), 1000000) + , DebugTraceAlwaysDump(conf, U_("Debug"), U_("TraceAlwaysDump"), false) + , DebugStopSoundDeviceOnCrash(conf, U_("Debug"), U_("StopSoundDeviceOnCrash"), true) + , DebugStopSoundDeviceBeforeDump(conf, U_("Debug"), U_("StopSoundDeviceBeforeDump"), false) + , DebugDelegateToWindowsHandler(conf, U_("Debug"), U_("DelegateToWindowsHandler"), false) +{ + + // Duplicate state for debug stuff in order to avoid calling into settings framework from crash context. + ExceptionHandler::stopSoundDeviceOnCrash = DebugStopSoundDeviceOnCrash; + ExceptionHandler::stopSoundDeviceBeforeDump = DebugStopSoundDeviceBeforeDump; + ExceptionHandler::delegateToWindowsHandler = DebugDelegateToWindowsHandler; + + // enable debug features (as early as possible after reading the settings) + #if !defined(MPT_LOG_IS_DISABLED) + #if !defined(MPT_LOG_GLOBAL_LEVEL_STATIC) + mpt::log::GlobalLogLevel = DebugLogLevel; + #endif + mpt::log::SetFacilities(DebugLogFacilitySolo, DebugLogFacilityBlocked); + mpt::log::FileEnabled = DebugLogFileEnable; + mpt::log::DebuggerEnabled = DebugLogDebuggerEnable; + mpt::log::ConsoleEnabled = DebugLogConsoleEnable; + #endif + if(DebugTraceEnable) + { + mpt::log::Trace::Enable(DebugTraceSize); + } +} + + +DebugSettings::~DebugSettings() +{ + if(DebugTraceAlwaysDump) + { + DebugTraceDump(); + } +} + + +TrackerSettings::TrackerSettings(SettingsContainer &conf) + : conf(conf) + // Version + , IniVersion(conf, U_("Version"), U_("Version"), mpt::ustring()) + , FirstRun(IniVersion.Get() == mpt::ustring()) + , PreviousSettingsVersion(GetPreviousSettingsVersion(IniVersion)) + , VersionInstallGUID(conf, U_("Version"), U_("InstallGUID"), mpt::UUID()) + // Display + , m_ShowSplashScreen(conf, U_("Display"), U_("ShowSplashScreen"), true) + , gbMdiMaximize(conf, U_("Display"), U_("MDIMaximize"), true) + , highResUI(conf, U_("Display"), U_("HighResUI"), false) + , glTreeSplitRatio(conf, U_("Display"), U_("MDITreeRatio"), 128) + , glTreeWindowWidth(conf, U_("Display"), U_("MDITreeWidth"), 160) + , glGeneralWindowHeight(conf, U_("Display"), U_("MDIGeneralHeight"), 222) + , glPatternWindowHeight(conf, U_("Display"), U_("MDIPatternHeight"), 152) + , glSampleWindowHeight(conf, U_("Display"), U_("MDISampleHeight"), 190) + , glInstrumentWindowHeight(conf, U_("Display"), U_("MDIInstrumentHeight"), 300) + , glCommentsWindowHeight(conf, U_("Display"), U_("MDICommentsHeight"), 288) + , glGraphWindowHeight(conf, U_("Display"), U_("MDIGraphHeight"), 288) + , gnPlugWindowX(conf, U_("Display"), U_("PlugSelectWindowX"), 243) + , gnPlugWindowY(conf, U_("Display"), U_("PlugSelectWindowY"), 273) + , gnPlugWindowWidth(conf, U_("Display"), U_("PlugSelectWindowWidth"), 450) + , gnPlugWindowHeight(conf, U_("Display"), U_("PlugSelectWindowHeight"), 540) + , gnPlugWindowLast(conf, U_("Display"), U_("PlugSelectWindowLast"), 0) + , gnMsgBoxVisiblityFlags(conf, U_("Display"), U_("MsgBoxVisibilityFlags"), uint32_max) + , GUIUpdateInterval(conf, U_("Display"), U_("GUIUpdateInterval"), 0) + , FSUpdateInterval(conf, U_("Display"), U_("FSUpdateInterval"), 500) + , VuMeterUpdateInterval(conf, U_("Display"), U_("VuMeterUpdateInterval"), 15) + , VuMeterDecaySpeedDecibelPerSecond(conf, U_("Display"), U_("VuMeterDecaySpeedDecibelPerSecond"), 88.0f) + , accidentalFlats(conf, U_("Display"), U_("AccidentalFlats"), false) + , rememberSongWindows(conf, U_("Display"), U_("RememberSongWindows"), true) + , showDirsInSampleBrowser(conf, U_("Display"), U_("ShowDirsInSampleBrowser"), false) + , commentsFont(conf, U_("Display"), U_("Comments Font"), FontSetting(U_("Courier New"), 120)) + , defaultRainbowChannelColors(conf, U_("Display"), U_("DefaultChannelColors"), DefaultChannelColors::Random) + // Misc + , defaultModType(conf, U_("Misc"), U_("DefaultModType"), MOD_TYPE_IT) + , defaultNewFileAction(conf, U_("Misc"), U_("DefaultNewFileAction"), nfDefaultFormat) + , DefaultPlugVolumeHandling(conf, U_("Misc"), U_("DefaultPlugVolumeHandling"), PLUGIN_VOLUMEHANDLING_IGNORE) + , autoApplySmoothFT2Ramping(conf, U_("Misc"), U_("SmoothFT2Ramping"), false) + , MiscITCompressionStereo(conf, U_("Misc"), U_("ITCompressionStereo"), 4) + , MiscITCompressionMono(conf, U_("Misc"), U_("ITCompressionMono"), 4) + , MiscSaveChannelMuteStatus(conf, U_("Misc"), U_("SaveChannelMuteStatus"), true) + , MiscAllowMultipleCommandsPerKey(conf, U_("Misc"), U_("AllowMultipleCommandsPerKey"), false) + , MiscDistinguishModifiers(conf, U_("Misc"), U_("DistinguishModifiers"), false) + , MiscProcessPriorityClass(conf, U_("Misc"), U_("ProcessPriorityClass"), ProcessPriorityClassNORMAL) + , MiscFlushFileBuffersOnSave(conf, U_("Misc"), U_("FlushFileBuffersOnSave"), true) + , MiscCacheCompleteFileBeforeLoading(conf, U_("Misc"), U_("CacheCompleteFileBeforeLoading"), false) + , MiscUseSingleInstance(conf, U_("Misc"), U_("UseSingleInstance"), false) + // Sound Settings + , m_SoundShowRecordingSettings(false) + , m_SoundShowDeprecatedDevices(conf, U_("Sound Settings"), U_("ShowDeprecatedDevices"), false) + , m_SoundDeprecatedDeviceWarningShown(conf, U_("Sound Settings"), U_("DeprecatedDeviceWarningShown"), false) + , m_SoundSampleRates(conf, U_("Sound Settings"), U_("SampleRates"), GetDefaultSampleRates()) + , m_SoundSettingsOpenDeviceAtStartup(conf, U_("Sound Settings"), U_("OpenDeviceAtStartup"), false) + , m_SoundSettingsStopMode(conf, U_("Sound Settings"), U_("StopMode"), SoundDeviceStopModeClosed) + , m_SoundDeviceSettingsUseOldDefaults(false) + , m_SoundDeviceID_DEPRECATED(SoundDevice::Legacy::ID()) + , m_SoundDeviceIdentifier(conf, U_("Sound Settings"), U_("Device"), SoundDevice::Identifier()) + , MixerMaxChannels(conf, U_("Sound Settings"), U_("MixChannels"), MixerSettings().m_nMaxMixChannels) + , MixerDSPMask(conf, U_("Sound Settings"), U_("Quality"), MixerSettings().DSPMask) + , MixerFlags(conf, U_("Sound Settings"), U_("SoundSetup"), MixerSettings().MixerFlags) + , MixerSamplerate(conf, U_("Sound Settings"), U_("Mixing_Rate"), MixerSettings().gdwMixingFreq) + , MixerOutputChannels(conf, U_("Sound Settings"), U_("ChannelMode"), MixerSettings().gnChannels) + , MixerPreAmp(conf, U_("Sound Settings"), U_("PreAmp"), MixerSettings().m_nPreAmp) + , MixerStereoSeparation(conf, U_("Sound Settings"), U_("StereoSeparation"), MixerSettings().m_nStereoSeparation) + , MixerVolumeRampUpMicroseconds(conf, U_("Sound Settings"), U_("VolumeRampUpMicroseconds"), MixerSettings().GetVolumeRampUpMicroseconds()) + , MixerVolumeRampDownMicroseconds(conf, U_("Sound Settings"), U_("VolumeRampDownMicroseconds"), MixerSettings().GetVolumeRampDownMicroseconds()) + , MixerNumInputChannels(conf, U_("Sound Settings"), U_("NumInputChannels"), static_cast<uint32>(MixerSettings().NumInputChannels)) + , ResamplerMode(conf, U_("Sound Settings"), U_("SrcMode"), CResamplerSettings().SrcMode) + , ResamplerSubMode(conf, U_("Sound Settings"), U_("XMMSModplugResamplerWFIRType"), CResamplerSettings().gbWFIRType) + , ResamplerCutoffPercent(conf, U_("Sound Settings"), U_("ResamplerWFIRCutoff"), mpt::saturate_round<int32>(CResamplerSettings().gdWFIRCutoff * 100.0)) + , ResamplerEmulateAmiga(conf, U_("Sound Settings"), U_("ResamplerEmulateAmiga"), Resampling::AmigaFilter::A1200) + , SoundBoostedThreadPriority(conf, U_("Sound Settings"), U_("BoostedThreadPriority"), SoundDevice::AppInfo().BoostedThreadPriorityXP) + , SoundBoostedThreadMMCSSClass(conf, U_("Sound Settings"), U_("BoostedThreadMMCSSClass"), SoundDevice::AppInfo().BoostedThreadMMCSSClassVista) + , SoundBoostedThreadRealtimePosix(conf, U_("Sound Settings"), U_("BoostedThreadRealtimeLinux"), SoundDevice::AppInfo().BoostedThreadRealtimePosix) + , SoundBoostedThreadNicenessPosix(conf, U_("Sound Settings"), U_("BoostedThreadNicenessPosix"), SoundDevice::AppInfo().BoostedThreadNicenessPosix) + , SoundBoostedThreadRtprioPosix(conf, U_("Sound Settings"), U_("BoostedThreadRtprioLinux"), SoundDevice::AppInfo().BoostedThreadRtprioPosix) + , SoundMaskDriverCrashes(conf, U_("Sound Settings"), U_("MaskDriverCrashes"), SoundDevice::AppInfo().MaskDriverCrashes) + , SoundAllowDeferredProcessing(conf, U_("Sound Settings"), U_("AllowDeferredProcessing"), SoundDevice::AppInfo().AllowDeferredProcessing) + // MIDI Settings + , m_nMidiDevice(conf, U_("MIDI Settings"), U_("MidiDevice"), 0) + , midiDeviceName(conf, U_("MIDI Settings"), U_("MidiDeviceName"), _T("")) + , m_dwMidiSetup(conf, U_("MIDI Settings"), U_("MidiSetup"), MIDISETUP_RECORDVELOCITY | MIDISETUP_RECORDNOTEOFF | MIDISETUP_TRANSPOSEKEYBOARD | MIDISETUP_MIDITOPLUG) + , aftertouchBehaviour(conf, U_("MIDI Settings"), U_("AftertouchBehaviour"), atDoNotRecord) + , midiVelocityAmp(conf, U_("MIDI Settings"), U_("MidiVelocityAmp"), 100) + , midiIgnoreCCs(conf, U_("MIDI Settings"), U_("IgnoredCCs"), std::bitset<128>()) + , midiImportPatternLen(conf, U_("MIDI Settings"), U_("MidiImportPatLen"), 128) + , midiImportQuantize(conf, U_("MIDI Settings"), U_("MidiImportQuantize"), 32) + , midiImportTicks(conf, U_("MIDI Settings"), U_("MidiImportTicks"), 6) + // Pattern Editor + , gbLoopSong(conf, U_("Pattern Editor"), U_("LoopSong"), true) + , gnPatternSpacing(conf, U_("Pattern Editor"), U_("Spacing"), 0) + , gbPatternVUMeters(conf, U_("Pattern Editor"), U_("VU-Meters"), true) + , gbPatternPluginNames(conf, U_("Pattern Editor"), U_("Plugin-Names"), true) + , gbPatternRecord(conf, U_("Pattern Editor"), U_("Record"), true) + , patternNoEditPopup(conf, U_("Pattern Editor"), U_("NoEditPopup"), false) + , patternStepCommands(conf, U_("Pattern Editor"), U_("EditStepAppliesToCommands"), false) + , m_dwPatternSetup(conf, U_("Pattern Editor"), U_("PatternSetup"), GetDefaultPatternSetup()) + , m_nRowHighlightMeasures(conf, U_("Pattern Editor"), U_("RowSpacing"), 16) + , m_nRowHighlightBeats(conf, U_("Pattern Editor"), U_("RowSpacing2"), 4) + , recordQuantizeRows(conf, U_("Pattern Editor"), U_("RecordQuantize"), 0) + , gnAutoChordWaitTime(conf, U_("Pattern Editor"), U_("AutoChordWaitTime"), 60) + , orderlistMargins(conf, U_("Pattern Editor"), U_("DefaultSequenceMargins"), 0) + , rowDisplayOffset(conf, U_("Pattern Editor"), U_("RowDisplayOffset"), 0) + , patternFont(conf, U_("Pattern Editor"), U_("Font"), FontSetting(PATTERNFONT_SMALL, 0)) + , patternFontDot(conf, U_("Pattern Editor"), U_("FontDot"), U_(".")) + , effectVisWidth(conf, U_("Pattern Editor"), U_("EffectVisWidth"), -1) + , effectVisHeight(conf, U_("Pattern Editor"), U_("EffectVisHeight"), -1) + , effectVisX(conf, U_("Pattern Editor"), U_("EffectVisX"), int32_min) + , effectVisY(conf, U_("Pattern Editor"), U_("EffectVisY"), int32_min) + , patternAccessibilityFormat(conf, U_("Pattern Editor"), U_("AccessibilityFormat"), _T("Row %row%, Channel %channel%, %column_type%: %column_description%")) + , patternAlwaysDrawWholePatternOnScrollSlow(conf, U_("Pattern Editor"), U_("AlwaysDrawWholePatternOnScrollSlow"), false) + , orderListOldDropBehaviour(conf, U_("Pattern Editor"), U_("OrderListOldDropBehaviour"), false) + // Sample Editor + , m_SampleUndoBufferSize(conf, U_("Sample Editor"), U_("UndoBufferSize"), SampleUndoBufferSize()) + , sampleEditorKeyBehaviour(conf, U_("Sample Editor"), U_("KeyBehaviour"), seNoteOffOnNewKey) + , m_defaultSampleFormat(conf, U_("Sample Editor"), U_("DefaultFormat"), dfFLAC) + , sampleEditorTimelineFormat(conf, U_("Sample Editor"), U_("TimelineFormat"), TimelineFormat::Seconds) + , sampleEditorDefaultResampler(conf, U_("Sample Editor"), U_("DefaultResampler"), SRCMODE_DEFAULT) + , m_nFinetuneStep(conf, U_("Sample Editor"), U_("FinetuneStep"), 10) + , m_FLACCompressionLevel(conf, U_("Sample Editor"), U_("FLACCompressionLevel"), 5) + , compressITI(conf, U_("Sample Editor"), U_("CompressITI"), true) + , m_MayNormalizeSamplesOnLoad(conf, U_("Sample Editor"), U_("MayNormalizeSamplesOnLoad"), true) + , previewInFileDialogs(conf, U_("Sample Editor"), U_("PreviewInFileDialogs"), false) + , cursorPositionInHex(conf, U_("Sample Editor"), U_("CursorPositionInHex"), false) + // Export + , ExportDefaultToSoundcardSamplerate(conf, U_("Export"), U_("DefaultToSoundcardSamplerate"), true) + , ExportStreamEncoderSettings(conf, U_("Export")) + // Components + , ComponentsLoadOnStartup(conf, U_("Components"), U_("LoadOnStartup"), ComponentManagerSettingsDefault().LoadOnStartup()) + , ComponentsKeepLoaded(conf, U_("Components"), U_("KeepLoaded"), ComponentManagerSettingsDefault().KeepLoaded()) + // AutoSave + , CreateBackupFiles(conf, U_("AutoSave"), U_("CreateBackupFiles"), true) + , AutosaveEnabled(conf, U_("AutoSave"), U_("Enabled"), true) + , AutosaveIntervalMinutes(conf, U_("AutoSave"), U_("IntervalMinutes"), 10) + , AutosaveHistoryDepth(conf, U_("AutoSave"), U_("BackupHistory"), 3) + , AutosaveUseOriginalPath(conf, U_("AutoSave"), U_("UseOriginalPath"), true) + , AutosavePath(conf, U_("AutoSave"), U_("Path"), mpt::GetTempDirectory()) + // Paths + , PathSongs(conf, U_("Paths"), U_("Songs_Directory"), mpt::PathString()) + , PathSamples(conf, U_("Paths"), U_("Samples_Directory"), mpt::PathString()) + , PathInstruments(conf, U_("Paths"), U_("Instruments_Directory"), mpt::PathString()) + , PathPlugins(conf, U_("Paths"), U_("Plugins_Directory"), mpt::PathString()) + , PathPluginPresets(conf, U_("Paths"), U_("Plugin_Presets_Directory"), mpt::PathString()) + , PathExport(conf, U_("Paths"), U_("Export_Directory"), mpt::PathString()) + , PathTunings(theApp.GetConfigPath() + P_("tunings\\")) + , PathUserTemplates(theApp.GetConfigPath() + P_("TemplateModules\\")) + // Default template + , defaultTemplateFile(conf, U_("Paths"), U_("DefaultTemplate"), mpt::PathString()) + , defaultArtist(conf, U_("Misc"), U_("DefaultArtist"), mpt::getenv(U_("USERNAME")).value_or(U_(""))) + // MRU List + , mruListLength(conf, U_("Misc"), U_("MRUListLength"), 10) + // Plugins + , bridgeAllPlugins(conf, U_("VST Plugins"), U_("BridgeAllPlugins"), false) + , enableAutoSuspend(conf, U_("VST Plugins"), U_("EnableAutoSuspend"), false) + , midiMappingInPluginEditor(conf, U_("VST Plugins"), U_("EnableMidiMappingInEditor"), true) + , pluginProjectPath(conf, U_("VST Plugins"), U_("ProjectPath"), mpt::ustring()) + , vstHostProductString(conf, U_("VST Plugins"), U_("HostProductString"), "OpenMPT") + , vstHostVendorString(conf, U_("VST Plugins"), U_("HostVendorString"), "OpenMPT project") + , vstHostVendorVersion(conf, U_("VST Plugins"), U_("HostVendorVersion"), Version::Current().GetRawVersion()) + // Broken Plugins Workarounds + , BrokenPluginsWorkaroundVSTMaskAllCrashes(conf, U_("Broken Plugins Workarounds"), U_("VSTMaskAllCrashes"), true) // TODO: really should be false + , BrokenPluginsWorkaroundVSTNeverUnloadAnyPlugin(conf, U_("Broken Plugins Workarounds"), U_("VSTNeverUnloadAnyPlugin"), false) +#if defined(MPT_ENABLE_UPDATE) + // Update + , UpdateEnabled(conf, U_("Update"), U_("Enabled"), true) + , UpdateInstallAutomatically(conf, U_("Update"), U_("InstallAutomatically"), false) + , UpdateLastUpdateCheck(conf, U_("Update"), U_("LastUpdateCheck"), mpt::Date::Unix(time_t())) + , UpdateUpdateCheckPeriod_DEPRECATED(conf, U_("Update"), U_("UpdateCheckPeriod"), 7) + , UpdateIntervalDays(conf, U_("Update"), U_("UpdateCheckIntervalDays"), 7) + , UpdateChannel(conf, U_("Update"), U_("Channel"), UpdateChannelRelease) + , UpdateUpdateURL_DEPRECATED(conf, U_("Update"), U_("UpdateURL"), U_("https://update.openmpt.org/check/$VERSION/$GUID")) + , UpdateAPIURL(conf, U_("Update"), U_("APIURL"), CUpdateCheck::GetDefaultAPIURL()) + , UpdateStatisticsConsentAsked(conf, U_("Update"), U_("StatistisConsentAsked"), false) + , UpdateStatistics(conf, U_("Update"), U_("Statistis"), false) + , UpdateSendGUID_DEPRECATED(conf, U_("Update"), U_("SendGUID"), false) + , UpdateShowUpdateHint(conf, U_("Update"), U_("ShowUpdateHint"), true) + , UpdateIgnoreVersion(conf, U_("Update"), U_("IgnoreVersion"), _T("")) + , UpdateSkipSignatureVerificationUNSECURE(conf, U_("Update"), U_("SkipSignatureVerification"), false) + , UpdateSigningKeysRootAnchors(conf, U_("Update"), U_("SigningKeysRootAnchors"), CUpdateCheck::GetDefaultUpdateSigningKeysRootAnchors()) +#endif // MPT_ENABLE_UPDATE + // Wine suppport + , WineSupportEnabled(conf, U_("WineSupport"), U_("Enabled"), false) + , WineSupportAlwaysRecompile(conf, U_("WineSupport"), U_("AlwaysRecompile"), false) + , WineSupportAskCompile(conf, U_("WineSupport"), U_("AskCompile"), false) + , WineSupportCompileVerbosity(conf, U_("WineSupport"), U_("CompileVerbosity"), 2) // 0=silent 1=silentmake 2=progresswindow 3=standard 4=verbosemake 5=veryverbosemake 6=msgboxes + , WineSupportForeignOpenMPT(conf, U_("WineSupport"), U_("ForeignOpenMPT"), false) + , WineSupportAllowUnknownHost(conf, U_("WineSupport"), U_("AllowUnknownHost"), false) + , WineSupportEnablePulseAudio(conf, U_("WineSupport"), U_("EnablePulseAudio"), 1) + , WineSupportEnablePortAudio(conf, U_("WineSupport"), U_("EnablePortAudio"), 1) + , WineSupportEnableRtAudio(conf, U_("WineSupport"), U_("EnableRtAudio"), 1) +{ + + // Effects +#ifndef NO_DSP + m_MegaBassSettings.m_nXBassDepth = conf.Read<int32>(U_("Effects"), U_("XBassDepth"), m_MegaBassSettings.m_nXBassDepth); + m_MegaBassSettings.m_nXBassRange = conf.Read<int32>(U_("Effects"), U_("XBassRange"), m_MegaBassSettings.m_nXBassRange); +#endif +#ifndef NO_REVERB + m_ReverbSettings.m_nReverbDepth = conf.Read<int32>(U_("Effects"), U_("ReverbDepth"), m_ReverbSettings.m_nReverbDepth); + m_ReverbSettings.m_nReverbType = conf.Read<int32>(U_("Effects"), U_("ReverbType"), m_ReverbSettings.m_nReverbType); +#endif +#ifndef NO_DSP + m_SurroundSettings.m_nProLogicDepth = conf.Read<int32>(U_("Effects"), U_("ProLogicDepth"), m_SurroundSettings.m_nProLogicDepth); + m_SurroundSettings.m_nProLogicDelay = conf.Read<int32>(U_("Effects"), U_("ProLogicDelay"), m_SurroundSettings.m_nProLogicDelay); +#endif +#ifndef NO_EQ + m_EqSettings = conf.Read<EQPreset>(U_("Effects"), U_("EQ_Settings"), FlatEQPreset); + const EQPreset userPresets[] = + { + FlatEQPreset, + { "User 1", {16,16,16,16,16,16}, { 150, 350, 700, 1500, 4500, 8000 } }, + { "User 2", {16,16,16,16,16,16}, { 200, 400, 800, 1750, 5000, 9000 } }, + { "User 3", {16,16,16,16,16,16}, { 250, 450, 900, 2000, 5000, 10000 } } + }; + + m_EqUserPresets[0] = conf.Read<EQPreset>(U_("Effects"), U_("EQ_User1"), userPresets[0]); + m_EqUserPresets[1] = conf.Read<EQPreset>(U_("Effects"), U_("EQ_User2"), userPresets[1]); + m_EqUserPresets[2] = conf.Read<EQPreset>(U_("Effects"), U_("EQ_User3"), userPresets[2]); + m_EqUserPresets[3] = conf.Read<EQPreset>(U_("Effects"), U_("EQ_User4"), userPresets[3]); +#endif +#ifndef NO_DSP + m_BitCrushSettings.m_Bits = conf.Read<int32>(U_("Effects"), U_("BitCrushBits"), m_BitCrushSettings.m_Bits); +#endif + // Display (Colors) + GetDefaultColourScheme(rgbCustomColors); + for(int ncol = 0; ncol < MAX_MODCOLORS; ncol++) + { + const mpt::ustring colorName = MPT_UFORMAT("Color{}")(mpt::ufmt::dec0<2>(ncol)); + rgbCustomColors[ncol] = conf.Read<uint32>(U_("Display"), colorName, rgbCustomColors[ncol]); + } + // Paths + m_szKbdFile = conf.Read<mpt::PathString>(U_("Paths"), U_("Key_Config_File"), mpt::PathString()); + conf.Forget(U_("Paths"), U_("Key_Config_File")); + + // init old and messy stuff: + + // Default chords + MemsetZero(Chords); + for(UINT ichord = 0; ichord < 3 * 12; ichord++) + { + Chords[ichord].key = (uint8)ichord; + Chords[ichord].notes[0] = MPTChord::noNote; + Chords[ichord].notes[1] = MPTChord::noNote; + Chords[ichord].notes[2] = MPTChord::noNote; + + if(ichord < 12) + { + // Major Chords + Chords[ichord].notes[0] = (int8)(ichord + 4); + Chords[ichord].notes[1] = (int8)(ichord + 7); + Chords[ichord].notes[2] = (int8)(ichord + 10); + } else if(ichord < 24) + { + // Minor Chords + Chords[ichord].notes[0] = (int8)(ichord - 9); + Chords[ichord].notes[1] = (int8)(ichord - 5); + Chords[ichord].notes[2] = (int8)(ichord - 2); + } + } + + + // load old and messy stuff: + + PatternClipboard::SetClipboardSize(conf.Read<int32>(U_("Pattern Editor"), U_("NumClipboards"), mpt::saturate_cast<int32>(PatternClipboard::GetClipboardSize()))); + + // Chords + LoadChords(Chords); + + // Zxx Macros + MIDIMacroConfig macros; + theApp.GetDefaultMidiMacro(macros); + for(int i = 0; i < kSFxMacros; i++) + { + macros.SFx[i] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(i)), macros.SFx[i]); + } + for(int i = 0; i < kZxxMacros; i++) + { + macros.Zxx[i] = conf.Read<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(i | 0x80)), macros.Zxx[i]); + } + + + // MRU list + Limit(mruListLength, 0u, 32u); + mruFiles.reserve(mruListLength); + for(uint32 i = 0; i < mruListLength; i++) + { + mpt::ustring key = MPT_UFORMAT("File{}")(i); + + mpt::PathString path = theApp.PathInstallRelativeToAbsolute(conf.Read<mpt::PathString>(U_("Recent File List"), key, mpt::PathString())); + if(!path.empty()) + { + mruFiles.push_back(path); + } + } + + // Fixups: + // ------- + + const Version storedVersion = PreviousSettingsVersion; + + // Version + if(!VersionInstallGUID.Get().IsValid()) + { + // No UUID found - generate one. + VersionInstallGUID = mpt::UUID::Generate(mpt::global_prng()); + } + + // Plugins + if(storedVersion < MPT_V("1.19.03.01") && vstHostProductString.Get() == "OpenMPT") + { + vstHostVendorVersion = Version::Current().GetRawVersion(); + } + if(storedVersion < MPT_V("1.30.00.24")) + { + BrokenPluginsWorkaroundVSTNeverUnloadAnyPlugin = !conf.Read<bool>(U_("VST Plugins"), U_("FullyUnloadPlugins"), true); + conf.Remove(U_("VST Plugins"), U_("FullyUnloadPlugins")); + } + + // Sound Settings + if(storedVersion < MPT_V("1.22.07.30")) + { + if(conf.Read<bool>(U_("Sound Settings"), U_("KeepDeviceOpen"), false)) + { + m_SoundSettingsStopMode = SoundDeviceStopModePlaying; + } else + { + m_SoundSettingsStopMode = SoundDeviceStopModeStopped; + } + } + if(storedVersion < MPT_V("1.22.07.04")) + { + std::vector<uint32> sampleRates = m_SoundSampleRates; + if(std::count(sampleRates.begin(), sampleRates.end(), MixerSamplerate) == 0) + { + sampleRates.push_back(MixerSamplerate); + std::sort(sampleRates.begin(), sampleRates.end()); + std::reverse(sampleRates.begin(), sampleRates.end()); + m_SoundSampleRates = sampleRates; + } + } + if(storedVersion < MPT_V("1.22.07.04")) + { + m_SoundDeviceID_DEPRECATED = conf.Read<SoundDevice::Legacy::ID>(U_("Sound Settings"), U_("WaveDevice"), SoundDevice::Legacy::ID()); + Setting<uint32> m_BufferLength_DEPRECATED(conf, U_("Sound Settings"), U_("BufferLength"), 50); + Setting<uint32> m_LatencyMS(conf, U_("Sound Settings"), U_("Latency"), mpt::saturate_round<int32>(SoundDevice::Settings().Latency * 1000.0)); + Setting<uint32> m_UpdateIntervalMS(conf, U_("Sound Settings"), U_("UpdateInterval"), mpt::saturate_round<int32>(SoundDevice::Settings().UpdateInterval * 1000.0)); + Setting<SampleFormat> m_SampleFormat(conf, U_("Sound Settings"), U_("BitsPerSample"), SoundDevice::Settings().sampleFormat); + Setting<bool> m_SoundDeviceExclusiveMode(conf, U_("Sound Settings"), U_("ExclusiveMode"), SoundDevice::Settings().ExclusiveMode); + Setting<bool> m_SoundDeviceBoostThreadPriority(conf, U_("Sound Settings"), U_("BoostThreadPriority"), SoundDevice::Settings().BoostThreadPriority); + Setting<bool> m_SoundDeviceUseHardwareTiming(conf, U_("Sound Settings"), U_("UseHardwareTiming"), SoundDevice::Settings().UseHardwareTiming); + Setting<SoundDevice::ChannelMapping> m_SoundDeviceChannelMapping(conf, U_("Sound Settings"), U_("ChannelMapping"), SoundDevice::Settings().Channels); + if(storedVersion < MPT_V("1.21.01.26")) + { + if(m_BufferLength_DEPRECATED != 0) + { + if(m_BufferLength_DEPRECATED < 1) m_BufferLength_DEPRECATED = 1; // 1ms + if(m_BufferLength_DEPRECATED > 1000) m_BufferLength_DEPRECATED = 1000; // 1sec + if((m_SoundDeviceID_DEPRECATED & SoundDevice::Legacy::MaskType) == SoundDevice::Legacy::TypeASIO) + { + m_LatencyMS = m_BufferLength_DEPRECATED * 1; + m_UpdateIntervalMS = m_BufferLength_DEPRECATED / 8; + } else + { + m_LatencyMS = m_BufferLength_DEPRECATED * 3; + m_UpdateIntervalMS = m_BufferLength_DEPRECATED / 8; + } + if(!m_UpdateIntervalMS) m_UpdateIntervalMS = static_cast<uint32>(SoundDevice::Settings().UpdateInterval * 1000.0); + } + conf.Remove(m_BufferLength_DEPRECATED.GetPath()); + } + if(storedVersion < MPT_V("1.22.01.03")) + { + m_SoundDeviceExclusiveMode = ((MixerFlags & OLD_SOUNDSETUP_SECONDARY) == 0); + } + if(storedVersion < MPT_V("1.22.01.03")) + { + m_SoundDeviceBoostThreadPriority = ((MixerFlags & OLD_SOUNDSETUP_NOBOOSTTHREADPRIORITY) == 0); + } + if(storedVersion < MPT_V("1.22.07.03")) + { + m_SoundDeviceChannelMapping = SoundDevice::ChannelMapping::BaseChannel(MixerOutputChannels, conf.Read<int>(U_("Sound Settings"), U_("ASIOBaseChannel"), 0)); + } + m_SoundDeviceSettingsDefaults.Latency = m_LatencyMS / 1000.0; + m_SoundDeviceSettingsDefaults.UpdateInterval = m_UpdateIntervalMS / 1000.0; + m_SoundDeviceSettingsDefaults.Samplerate = MixerSamplerate; + if(m_SoundDeviceSettingsDefaults.Channels.GetNumHostChannels() != MixerOutputChannels) + { + // reset invalid channel mapping to default + m_SoundDeviceSettingsDefaults.Channels = SoundDevice::ChannelMapping(MixerOutputChannels); + } + m_SoundDeviceSettingsDefaults.InputChannels = 0; + m_SoundDeviceSettingsDefaults.sampleFormat = m_SampleFormat; + m_SoundDeviceSettingsDefaults.ExclusiveMode = m_SoundDeviceExclusiveMode; + m_SoundDeviceSettingsDefaults.BoostThreadPriority = m_SoundDeviceBoostThreadPriority; + m_SoundDeviceSettingsDefaults.UseHardwareTiming = m_SoundDeviceUseHardwareTiming; + m_SoundDeviceSettingsDefaults.InputSourceID = 0; + m_SoundDeviceSettingsUseOldDefaults = true; + } + if(storedVersion < MPT_V("1.28.00.41")) + { + // reset this setting to the default when updating, + // because we do not provide a GUI any more, + // and in general, it should not get changed anyway + ResamplerCutoffPercent = mpt::saturate_round<int32>(CResamplerSettings().gdWFIRCutoff * 100.0); + } + if(MixerSamplerate == 0) + { + MixerSamplerate = MixerSettings().gdwMixingFreq; + } + if(storedVersion < MPT_V("1.21.01.26")) + { + MixerFlags &= ~OLD_SOUNDSETUP_REVERSESTEREO; + } + if(storedVersion < MPT_V("1.22.01.03")) + { + MixerFlags &= ~OLD_SOUNDSETUP_SECONDARY; + } + if(storedVersion < MPT_V("1.22.01.03")) + { + MixerFlags &= ~OLD_SOUNDSETUP_NOBOOSTTHREADPRIORITY; + } + if(storedVersion < MPT_V("1.20.00.22")) + { + MixerSettings settings = GetMixerSettings(); + settings.SetVolumeRampUpSamples(conf.Read<int32>(U_("Sound Settings"), U_("VolumeRampSamples"), 42)); + settings.SetVolumeRampDownSamples(conf.Read<int32>(U_("Sound Settings"), U_("VolumeRampSamples"), 42)); + SetMixerSettings(settings); + conf.Remove(U_("Sound Settings"), U_("VolumeRampSamples")); + } else if(storedVersion < MPT_V("1.22.07.18")) + { + MixerSettings settings = GetMixerSettings(); + settings.SetVolumeRampUpSamples(conf.Read<int32>(U_("Sound Settings"), U_("VolumeRampUpSamples"), MixerSettings().GetVolumeRampUpSamples())); + settings.SetVolumeRampDownSamples(conf.Read<int32>(U_("Sound Settings"), U_("VolumeRampDownSamples"), MixerSettings().GetVolumeRampDownSamples())); + SetMixerSettings(settings); + } + Limit(ResamplerCutoffPercent, 0, 100); + if(storedVersion < MPT_V("1.29.00.11")) + { + MixerMaxChannels = MixerSettings().m_nMaxMixChannels; // reset to default on update because we removed the setting in the GUI + } + if(storedVersion < MPT_V("1.29.00.20")) + { + MixerDSPMask = MixerDSPMask & ~SNDDSP_BITCRUSH; + } + + // Misc + if(defaultModType == MOD_TYPE_NONE) + { + defaultModType = MOD_TYPE_IT; + } + + // MIDI Settings + if((m_dwMidiSetup & 0x40) != 0 && storedVersion < MPT_V("1.20.00.86")) + { + // This flag used to be "amplify MIDI Note Velocity" - with a fixed amplification factor of 2. + midiVelocityAmp = 200; + m_dwMidiSetup &= ~0x40; + } + + // Pattern Editor + if(storedVersion < MPT_V("1.17.02.50")) + { + m_dwPatternSetup |= PATTERN_NOTEFADE; + } + if(storedVersion < MPT_V("1.17.03.01")) + { + m_dwPatternSetup |= PATTERN_RESETCHANNELS; + } + if(storedVersion < MPT_V("1.19.00.07")) + { + m_dwPatternSetup &= ~0x800; // this was previously deprecated and is now used for something else + } + if(storedVersion < MPT_V("1.20.00.04")) + { + m_dwPatternSetup &= ~0x200000; // ditto + } + if(storedVersion < MPT_V("1.20.00.07")) + { + m_dwPatternSetup &= ~0x400000; // ditto + } + if(storedVersion < MPT_V("1.20.00.39")) + { + m_dwPatternSetup &= ~0x10000000; // ditto + } + if(storedVersion < MPT_V("1.24.01.04")) + { + commentsFont = FontSetting(U_("Courier New"), (m_dwPatternSetup & 0x02) ? 120 : 90); + patternFont = FontSetting((m_dwPatternSetup & 0x08) ? PATTERNFONT_SMALL : PATTERNFONT_LARGE, 0); + m_dwPatternSetup &= ~(0x08 | 0x02); + } + if(storedVersion < MPT_V("1.25.00.08") && glGeneralWindowHeight < 222) + { + glGeneralWindowHeight += 44; + } + if(storedVersion < MPT_V("1.25.00.16") && (m_dwPatternSetup & 0x100000)) + { + // Move MIDI recording to MIDI setup + m_dwPatternSetup &= ~0x100000; + m_dwMidiSetup |= MIDISETUP_ENABLE_RECORD_DEFAULT; + } + if(storedVersion < MPT_V("1.27.00.51")) + { + // Moving option out of pattern config + CreateBackupFiles = (m_dwPatternSetup & 0x200) != 0; + m_dwPatternSetup &= ~0x200; + } + + // Export + if(storedVersion < MPT_V("1.30.00.38")) + { + { + conf.Write<Encoder::Mode>(U_("Export"), U_("FLAC_Mode"), Encoder::ModeLossless); + const int oldformat = conf.Read<int>(U_("Export"), U_("FLAC_Format"), 1); + Encoder::Format newformat = { Encoder::Format::Encoding::Integer, 24, mpt::get_endian() }; + if (oldformat >= 0) + { + switch (oldformat % 3) + { + case 0: + newformat = { Encoder::Format::Encoding::Integer, 24, mpt::get_endian() }; + break; + case 1: + newformat = { Encoder::Format::Encoding::Integer, 16, mpt::get_endian() }; + break; + case 2: + newformat = { Encoder::Format::Encoding::Integer, 8, mpt::get_endian() }; + break; + } + } + conf.Write<Encoder::Format>(U_("Export"), U_("FLAC_Format2"), newformat); + conf.Forget(U_("Export"), U_("FLAC_Format")); + } + { + conf.Write<Encoder::Mode>(U_("Export"), U_("Wave_Mode"), Encoder::ModeLossless); + const int oldformat = conf.Read<int>(U_("Export"), U_("Wave_Format"), 1); + Encoder::Format newformat = { Encoder::Format::Encoding::Float, 32, mpt::endian::little }; + if (oldformat >= 0) + { + switch (oldformat % 6) + { + case 0: + newformat = { Encoder::Format::Encoding::Float, 64, mpt::endian::little }; + break; + case 1: + newformat = { Encoder::Format::Encoding::Float, 32, mpt::endian::little }; + break; + case 2: + newformat = { Encoder::Format::Encoding::Integer, 32, mpt::endian::little }; + break; + case 3: + newformat = { Encoder::Format::Encoding::Integer, 24, mpt::endian::little }; + break; + case 4: + newformat = { Encoder::Format::Encoding::Integer, 16, mpt::endian::little }; + break; + case 5: + newformat = { Encoder::Format::Encoding::Unsigned, 8, mpt::endian::little }; + break; + } + } + conf.Write<Encoder::Format>(U_("Export"), U_("Wave_Format2"), newformat); + conf.Forget(U_("Export"), U_("Wave_Format")); + } + { + conf.Write<Encoder::Mode>(U_("Export"), U_("AU_Mode"), Encoder::ModeLossless); + const int oldformat = conf.Read<int>(U_("Export"), U_("AU_Format"), 1); + Encoder::Format newformat = { Encoder::Format::Encoding::Float, 32, mpt::endian::big }; + if(oldformat >= 0) + { + switch(oldformat % 6) + { + case 0: + newformat = { Encoder::Format::Encoding::Float, 64, mpt::endian::big }; + break; + case 1: + newformat = { Encoder::Format::Encoding::Float, 32, mpt::endian::big }; + break; + case 2: + newformat = { Encoder::Format::Encoding::Integer, 32, mpt::endian::big }; + break; + case 3: + newformat = { Encoder::Format::Encoding::Integer, 24, mpt::endian::big }; + break; + case 4: + newformat = { Encoder::Format::Encoding::Integer, 16, mpt::endian::big }; + break; + case 5: + newformat = { Encoder::Format::Encoding::Integer, 8, mpt::endian::big }; + break; + } + } + conf.Write<Encoder::Format>(U_("Export"), U_("AU_Format2"), newformat); + conf.Forget(U_("Export"), U_("AU_Format")); + } + { + conf.Write<Encoder::Mode>(U_("Export"), U_("RAW_Mode"), Encoder::ModeLossless); + const int oldformat = conf.Read<int>(U_("Export"), U_("RAW_Format"), 1); + Encoder::Format newformat = { Encoder::Format::Encoding::Float, 32, mpt::get_endian() }; + if(oldformat >= 0) + { + switch(oldformat % 7) + { + case 0: + newformat = { Encoder::Format::Encoding::Float, 64, mpt::get_endian() }; + break; + case 1: + newformat = { Encoder::Format::Encoding::Float, 32, mpt::get_endian() }; + break; + case 2: + newformat = { Encoder::Format::Encoding::Integer, 32, mpt::get_endian() }; + break; + case 3: + newformat = { Encoder::Format::Encoding::Integer, 24, mpt::get_endian() }; + break; + case 4: + newformat = { Encoder::Format::Encoding::Integer, 16, mpt::get_endian() }; + break; + case 5: + newformat = { Encoder::Format::Encoding::Integer, 8, mpt::get_endian() }; + break; + case 6: + newformat = { Encoder::Format::Encoding::Unsigned, 8, mpt::get_endian() }; + break; + } + } + conf.Write<Encoder::Format>(U_("Export"), U_("RAW_Format2"), newformat); + conf.Forget(U_("Export"), U_("RAW_Format")); + } + } + +#if defined(MPT_ENABLE_UPDATE) + // Update + if(storedVersion < MPT_V("1.28.00.39")) + { + if(UpdateUpdateCheckPeriod_DEPRECATED <= 0) + { + UpdateEnabled = true; + UpdateIntervalDays = -1; + } else + { + UpdateEnabled = true; + UpdateIntervalDays = UpdateUpdateCheckPeriod_DEPRECATED.Get(); + } + const auto url = UpdateUpdateURL_DEPRECATED.Get(); + if(url.empty() || + url == UL_("http://update.openmpt.org/check/$VERSION/$GUID") || + url == UL_("https://update.openmpt.org/check/$VERSION/$GUID")) + { + UpdateChannel = UpdateChannelRelease; + } else if(url == UL_("http://update.openmpt.org/check/testing/$VERSION/$GUID") || + url == UL_("https://update.openmpt.org/check/testing/$VERSION/$GUID")) + { + UpdateChannel = UpdateChannelDevelopment; + } else + { + UpdateChannel = UpdateChannelDevelopment; + } + UpdateStatistics = UpdateSendGUID_DEPRECATED.Get(); + conf.Forget(UpdateUpdateCheckPeriod_DEPRECATED.GetPath()); + conf.Forget(UpdateUpdateURL_DEPRECATED.GetPath()); + conf.Forget(UpdateSendGUID_DEPRECATED.GetPath()); + } +#endif // MPT_ENABLE_UPDATE + + if(storedVersion < MPT_V("1.29.00.39")) + { + // ASIO device IDs are now normalized to upper-case in the device enumeration code. + // Previous device IDs could be mixed-case (as retrieved from the registry), which would make direct ID comparison fail now. + auto device = m_SoundDeviceIdentifier.Get(); + if(device.substr(0, 5) == UL_("ASIO_")) + { + device = mpt::ToUpperCase(device); + m_SoundDeviceIdentifier = device; + } + } + + // Effects +#ifndef NO_EQ + FixupEQ(m_EqSettings); + FixupEQ(m_EqUserPresets[0]); + FixupEQ(m_EqUserPresets[1]); + FixupEQ(m_EqUserPresets[2]); + FixupEQ(m_EqUserPresets[3]); +#endif // !NO_EQ + + // Zxx Macros + if((MPT_V("1.17.00.00") <= storedVersion) && (storedVersion < MPT_V("1.20.00.00"))) + { + // Fix old nasty broken (non-standard) MIDI configs in INI file. + macros.UpgradeMacros(); + } + theApp.SetDefaultMidiMacro(macros); + + // Paths + m_szKbdFile = theApp.PathInstallRelativeToAbsolute(m_szKbdFile); + + // Sample undo buffer size (used to be a hidden, absolute setting in MiB) + int64 oldUndoSize = m_SampleUndoBufferSize.Get().GetSizeInPercent(); + if(storedVersion < MPT_V("1.22.07.25") && oldUndoSize != SampleUndoBufferSize::defaultSize && oldUndoSize != 0) + { + m_SampleUndoBufferSize = SampleUndoBufferSize(static_cast<int32>(100 * (oldUndoSize << 20) / SampleUndoBufferSize(100).GetSizeInBytes())); + } + + // More controls in the plugin selection dialog + if(storedVersion < MPT_V("1.26.00.26")) + { + gnPlugWindowHeight += 40; + } + + // Sanitize resampling mode for sample editor + if(!Resampling::IsKnownMode(sampleEditorDefaultResampler) && sampleEditorDefaultResampler != SRCMODE_DEFAULT) + { + sampleEditorDefaultResampler = SRCMODE_DEFAULT; + } + + // Migrate Tuning data + MigrateTunings(storedVersion); + + // Sanitize MIDI import data + if(midiImportPatternLen < 1 || midiImportPatternLen > MAX_PATTERN_ROWS) + midiImportPatternLen = 128; + if(midiImportQuantize < 4 || midiImportQuantize > 256) + midiImportQuantize = 32; + if(midiImportTicks < 2 || midiImportTicks > 16) + midiImportTicks = 16; + + // Last fixup: update config version + IniVersion = mpt::ufmt::val(Version::Current()); + + // Write updated settings + conf.Flush(); +} + + +TrackerSettings::~TrackerSettings() +{ + return; +} + + +namespace SoundDevice +{ +namespace Legacy +{ +SoundDevice::Info FindDeviceInfo(SoundDevice::Manager &manager, SoundDevice::Legacy::ID id) +{ + if(manager.GetDeviceInfos().empty()) + { + return SoundDevice::Info(); + } + SoundDevice::Type type = SoundDevice::Type(); + switch((id & SoundDevice::Legacy::MaskType) >> SoundDevice::Legacy::ShiftType) + { + case SoundDevice::Legacy::TypeWAVEOUT: + type = SoundDevice::TypeWAVEOUT; + break; + case SoundDevice::Legacy::TypeDSOUND: + type = SoundDevice::TypeDSOUND; + break; + case SoundDevice::Legacy::TypeASIO: + type = SoundDevice::TypeASIO; + break; + case SoundDevice::Legacy::TypePORTAUDIO_WASAPI: + type = SoundDevice::TypePORTAUDIO_WASAPI; + break; + case SoundDevice::Legacy::TypePORTAUDIO_WDMKS: + type = SoundDevice::TypePORTAUDIO_WDMKS; + break; + case SoundDevice::Legacy::TypePORTAUDIO_WMME: + type = SoundDevice::TypePORTAUDIO_WMME; + break; + case SoundDevice::Legacy::TypePORTAUDIO_DS: + type = SoundDevice::TypePORTAUDIO_DS; + break; + } + if(type.empty()) + { // fallback to first device + return *manager.begin(); + } + std::size_t index = static_cast<uint8>((id & SoundDevice::Legacy::MaskIndex) >> SoundDevice::Legacy::ShiftIndex); + std::size_t seenDevicesOfDesiredType = 0; + for(const auto &info : manager) + { + if(info.type == type) + { + if(seenDevicesOfDesiredType == index) + { + if(!info.IsValid()) + { // fallback to first device + return *manager.begin(); + } + return info; + } + seenDevicesOfDesiredType++; + } + } + // default to first device + return *manager.begin(); +} +} // namespace Legacy +} // namespace SoundDevice + + +void TrackerSettings::MigrateOldSoundDeviceSettings(SoundDevice::Manager &manager) +{ + if(m_SoundDeviceSettingsUseOldDefaults) + { + // get the old default device + SetSoundDeviceIdentifier(SoundDevice::Legacy::FindDeviceInfo(manager, m_SoundDeviceID_DEPRECATED).GetIdentifier()); + // apply old global sound device settings to each found device + for(const auto &it : manager) + { + SetSoundDeviceSettings(it.GetIdentifier(), GetSoundDeviceSettingsDefaults()); + } + } +} + + +void TrackerSettings::MigrateTunings(const Version storedVersion) +{ + if(!PathTunings.GetDefaultDir().IsDirectory()) + { + CreateDirectory(PathTunings.GetDefaultDir().AsNative().c_str(), 0); + } + if(!(PathTunings.GetDefaultDir() + P_("Built-in\\")).IsDirectory()) + { + CreateDirectory((PathTunings.GetDefaultDir() + P_("Built-in\\")).AsNative().c_str(), 0); + } + if(!(PathTunings.GetDefaultDir() + P_("Locale\\")).IsDirectory()) + { + CreateDirectory((PathTunings.GetDefaultDir() + P_("Local\\")).AsNative().c_str(), 0); + } + { + mpt::PathString fn = PathTunings.GetDefaultDir() + P_("Built-in\\12TET.tun"); + if(!fn.FileOrDirectoryExists()) + { + std::unique_ptr<CTuning> pT = CSoundFile::CreateTuning12TET(U_("12TET")); + mpt::SafeOutputFile sf(fn, std::ios::binary, mpt::FlushMode::Full); + pT->Serialize(sf); + } + } + { + mpt::PathString fn = PathTunings.GetDefaultDir() + P_("Built-in\\12TET [[fs15 1.17.02.49]].tun"); + if(!fn.FileOrDirectoryExists()) + { + std::unique_ptr<CTuning> pT = CSoundFile::CreateTuning12TET(U_("12TET [[fs15 1.17.02.49]]")); + mpt::SafeOutputFile sf(fn, std::ios::binary, mpt::FlushMode::Full); + pT->Serialize(sf); + } + } + oldLocalTunings = LoadLocalTunings(); + if(storedVersion < MPT_V("1.27.00.56")) + { + UnpackTuningCollection(*oldLocalTunings, PathTunings.GetDefaultDir() + P_("Local\\")); + } +} + + +std::unique_ptr<CTuningCollection> TrackerSettings::LoadLocalTunings() +{ + std::unique_ptr<CTuningCollection> s_pTuningsSharedLocal = std::make_unique<CTuningCollection>(); + mpt::ifstream f( + PathTunings.GetDefaultDir() + + P_("local_tunings") + + mpt::PathString::FromUTF8(CTuningCollection::s_FileExtension) + , std::ios::binary); + if(f.good()) + { + mpt::ustring dummyName; + s_pTuningsSharedLocal->Deserialize(f, dummyName, TuningCharsetFallback); + } + return s_pTuningsSharedLocal; +} + + +struct StoredSoundDeviceSettings +{ + +private: + SettingsContainer &conf; + const SoundDevice::Info deviceInfo; + +private: + Setting<uint32> LatencyUS; + Setting<uint32> UpdateIntervalUS; + Setting<uint32> Samplerate; + Setting<uint8> ChannelsOld; // compatibility with older versions + Setting<SoundDevice::ChannelMapping> ChannelMapping; + Setting<uint8> InputChannels; + Setting<SampleFormat> sampleFormat; + Setting<bool> ExclusiveMode; + Setting<bool> BoostThreadPriority; + Setting<bool> KeepDeviceRunning; + Setting<bool> UseHardwareTiming; + Setting<int32> DitherType; + Setting<uint32> InputSourceID; + +public: + + StoredSoundDeviceSettings(SettingsContainer &conf, const SoundDevice::Info & deviceInfo, const SoundDevice::Settings & defaults) + : conf(conf) + , deviceInfo(deviceInfo) + , LatencyUS(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("Latency"), mpt::saturate_round<int32>(defaults.Latency * 1000000.0)) + , UpdateIntervalUS(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("UpdateInterval"), mpt::saturate_round<int32>(defaults.UpdateInterval * 1000000.0)) + , Samplerate(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("SampleRate"), defaults.Samplerate) + , ChannelsOld(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("Channels"), mpt::saturate_cast<uint8>((int)defaults.Channels)) + , ChannelMapping(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("ChannelMapping"), defaults.Channels) + , InputChannels(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("InputChannels"), defaults.InputChannels) + , sampleFormat(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("SampleFormat"), defaults.sampleFormat) + , ExclusiveMode(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("ExclusiveMode"), defaults.ExclusiveMode) + , BoostThreadPriority(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("BoostThreadPriority"), defaults.BoostThreadPriority) + , KeepDeviceRunning(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("KeepDeviceRunning"), defaults.KeepDeviceRunning) + , UseHardwareTiming(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("UseHardwareTiming"), defaults.UseHardwareTiming) + , DitherType(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("DitherType"), static_cast<int32>(defaults.DitherType)) + , InputSourceID(conf, U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("InputSourceID"), defaults.InputSourceID) + { + if(ChannelMapping.Get().GetNumHostChannels() != ChannelsOld) + { + // If the stored channel count and the count of channels used in the channel mapping do not match, + // construct a default mapping from the channel count. + ChannelMapping = SoundDevice::ChannelMapping(ChannelsOld); + } + // store informational data (not read back, just to allow the user to mock with the raw ini file) + conf.Write(U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("Type"), deviceInfo.type); + conf.Write(U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("InternalID"), deviceInfo.internalID); + conf.Write(U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("API"), deviceInfo.apiName); + conf.Write(U_("Sound Settings"), deviceInfo.GetIdentifier() + U_("_") + U_("Name"), deviceInfo.name); + } + + StoredSoundDeviceSettings & operator = (const SoundDevice::Settings &settings) + { + LatencyUS = mpt::saturate_round<int32>(settings.Latency * 1000000.0); + UpdateIntervalUS = mpt::saturate_round<int32>(settings.UpdateInterval * 1000000.0); + Samplerate = settings.Samplerate; + ChannelsOld = mpt::saturate_cast<uint8>((int)settings.Channels); + ChannelMapping = settings.Channels; + InputChannels = settings.InputChannels; + sampleFormat = settings.sampleFormat; + ExclusiveMode = settings.ExclusiveMode; + BoostThreadPriority = settings.BoostThreadPriority; + KeepDeviceRunning = settings.KeepDeviceRunning; + UseHardwareTiming = settings.UseHardwareTiming; + DitherType = static_cast<int32>(settings.DitherType); + InputSourceID = settings.InputSourceID; + return *this; + } + + operator SoundDevice::Settings () const + { + SoundDevice::Settings settings; + settings.Latency = LatencyUS / 1000000.0; + settings.UpdateInterval = UpdateIntervalUS / 1000000.0; + settings.Samplerate = Samplerate; + settings.Channels = ChannelMapping; + settings.InputChannels = InputChannels; + settings.sampleFormat = sampleFormat; + settings.ExclusiveMode = ExclusiveMode; + settings.BoostThreadPriority = BoostThreadPriority; + settings.KeepDeviceRunning = KeepDeviceRunning; + settings.UseHardwareTiming = UseHardwareTiming; + settings.DitherType = DitherType; + settings.InputSourceID = InputSourceID; + return settings; + } +}; + +SoundDevice::Settings TrackerSettings::GetSoundDeviceSettingsDefaults() const +{ + return m_SoundDeviceSettingsDefaults; +} + +SoundDevice::Identifier TrackerSettings::GetSoundDeviceIdentifier() const +{ + return m_SoundDeviceIdentifier; +} + +void TrackerSettings::SetSoundDeviceIdentifier(const SoundDevice::Identifier &identifier) +{ + m_SoundDeviceIdentifier = identifier; +} + +SoundDevice::Settings TrackerSettings::GetSoundDeviceSettings(const SoundDevice::Identifier &device) const +{ + const SoundDevice::Info deviceInfo = theApp.GetSoundDevicesManager()->FindDeviceInfo(device); + if(!deviceInfo.IsValid()) + { + return SoundDevice::Settings(); + } + const SoundDevice::Caps deviceCaps = theApp.GetSoundDevicesManager()->GetDeviceCaps(device, CMainFrame::GetMainFrame()->gpSoundDevice); + SoundDevice::Settings settings = StoredSoundDeviceSettings(conf, deviceInfo, deviceCaps.DefaultSettings); + return settings; +} + +void TrackerSettings::SetSoundDeviceSettings(const SoundDevice::Identifier &device, const SoundDevice::Settings &settings) +{ + const SoundDevice::Info deviceInfo = theApp.GetSoundDevicesManager()->FindDeviceInfo(device); + if(!deviceInfo.IsValid()) + { + return; + } + const SoundDevice::Caps deviceCaps = theApp.GetSoundDevicesManager()->GetDeviceCaps(device, CMainFrame::GetMainFrame()->gpSoundDevice); + StoredSoundDeviceSettings(conf, deviceInfo, deviceCaps.DefaultSettings) = settings; +} + + +MixerSettings TrackerSettings::GetMixerSettings() const +{ + MixerSettings settings; + settings.m_nMaxMixChannels = MixerMaxChannels; + settings.DSPMask = MixerDSPMask; + settings.MixerFlags = MixerFlags; + settings.gdwMixingFreq = MixerSamplerate; + settings.gnChannels = MixerOutputChannels; + settings.m_nPreAmp = MixerPreAmp; + settings.m_nStereoSeparation = MixerStereoSeparation; + settings.VolumeRampUpMicroseconds = MixerVolumeRampUpMicroseconds; + settings.VolumeRampDownMicroseconds = MixerVolumeRampDownMicroseconds; + settings.NumInputChannels = MixerNumInputChannels; + return settings; +} + +void TrackerSettings::SetMixerSettings(const MixerSettings &settings) +{ + MixerMaxChannels = settings.m_nMaxMixChannels; + MixerDSPMask = settings.DSPMask; + MixerFlags = settings.MixerFlags; + MixerSamplerate = settings.gdwMixingFreq; + MixerOutputChannels = settings.gnChannels; + MixerPreAmp = settings.m_nPreAmp; + MixerStereoSeparation = settings.m_nStereoSeparation; + MixerVolumeRampUpMicroseconds = settings.VolumeRampUpMicroseconds; + MixerVolumeRampDownMicroseconds = settings.VolumeRampDownMicroseconds; + MixerNumInputChannels = static_cast<uint32>(settings.NumInputChannels); +} + + +CResamplerSettings TrackerSettings::GetResamplerSettings() const +{ + CResamplerSettings settings; + settings.SrcMode = ResamplerMode; + settings.gbWFIRType = ResamplerSubMode; + settings.gdWFIRCutoff = ResamplerCutoffPercent * 0.01; + settings.emulateAmiga = ResamplerEmulateAmiga; + return settings; +} + +void TrackerSettings::SetResamplerSettings(const CResamplerSettings &settings) +{ + ResamplerMode = settings.SrcMode; + ResamplerSubMode = settings.gbWFIRType; + ResamplerCutoffPercent = mpt::saturate_round<int32>(settings.gdWFIRCutoff * 100.0); + ResamplerEmulateAmiga = settings.emulateAmiga; +} + + +void TrackerSettings::GetDefaultColourScheme(std::array<COLORREF, MAX_MODCOLORS> &colours) +{ + colours[MODCOLOR_BACKNORMAL] = RGB(0xFF, 0xFF, 0xFF); + colours[MODCOLOR_TEXTNORMAL] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_BACKCURROW] = RGB(0xC0, 0xC0, 0xC0); + colours[MODCOLOR_TEXTCURROW] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_BACKSELECTED] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_TEXTSELECTED] = RGB(0xFF, 0xFF, 0xFF); + colours[MODCOLOR_SAMPLE] = RGB(0xFF, 0x00, 0x00); + colours[MODCOLOR_BACKPLAYCURSOR] = RGB(0xFF, 0xFF, 0x80); + colours[MODCOLOR_TEXTPLAYCURSOR] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_BACKHILIGHT] = RGB(0xE0, 0xE8, 0xE0); + // Effect Colors + colours[MODCOLOR_NOTE] = RGB(0x00, 0x00, 0x80); + colours[MODCOLOR_INSTRUMENT] = RGB(0x00, 0x80, 0x80); + colours[MODCOLOR_VOLUME] = RGB(0x00, 0x80, 0x00); + colours[MODCOLOR_PANNING] = RGB(0x00, 0x80, 0x80); + colours[MODCOLOR_PITCH] = RGB(0x80, 0x80, 0x00); + colours[MODCOLOR_GLOBALS] = RGB(0x80, 0x00, 0x00); + // VU-Meters + colours[MODCOLOR_VUMETER_LO] = RGB(0x00, 0xC8, 0x00); + colours[MODCOLOR_VUMETER_MED] = RGB(0xFF, 0xC8, 0x00); + colours[MODCOLOR_VUMETER_HI] = RGB(0xE1, 0x00, 0x00); + colours[MODCOLOR_VUMETER_LO_VST] = RGB(0x18, 0x96, 0xE1); + colours[MODCOLOR_VUMETER_MED_VST] = RGB(0xFF, 0xC8, 0x00); + colours[MODCOLOR_VUMETER_HI_VST] = RGB(0xE1, 0x00, 0x00); + // Channel separators + colours[MODCOLOR_SEPSHADOW] = GetSysColor(COLOR_BTNSHADOW); + colours[MODCOLOR_SEPFACE] = GetSysColor(COLOR_BTNFACE); + colours[MODCOLOR_SEPHILITE] = GetSysColor(COLOR_BTNHIGHLIGHT); + // Pattern blend colour + colours[MODCOLOR_BLENDCOLOR] = GetSysColor(COLOR_BTNFACE); + // Dodgy commands + colours[MODCOLOR_DODGY_COMMANDS] = RGB(0xC0, 0x00, 0x00); + // Sample / instrument editor + colours[MODCOLOR_BACKSAMPLE] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_SAMPLESELECTED] = RGB(0xFF, 0xFF, 0xFF); + colours[MODCOLOR_BACKENV] = RGB(0x00, 0x00, 0x00); + colours[MODCOLOR_ENVELOPES] = RGB(0x00, 0x00, 0xFF); + colours[MODCOLOR_ENVELOPE_RELEASE] = RGB(0xFF, 0xFF, 0x00); + colours[MODCOLOR_SAMPLE_LOOPMARKER] = RGB(0x30, 0xCC, 0x30); + colours[MODCOLOR_SAMPLE_SUSTAINMARKER] = RGB(50, 0xCC, 0xCC); + colours[MODCOLOR_SAMPLE_CUEPOINT] = RGB(0xFF, 0xCC, 0x30); +} + + +#ifndef NO_EQ + +void TrackerSettings::FixupEQ(EQPreset &eqSettings) +{ + for(UINT i = 0; i < MAX_EQ_BANDS; i++) + { + if(eqSettings.Gains[i] > 32) + eqSettings.Gains[i] = 16; + if((eqSettings.Freqs[i] < 100) || (eqSettings.Freqs[i] > 10000)) + eqSettings.Freqs[i] = FlatEQPreset.Freqs[i]; + } + mpt::String::SetNullTerminator(eqSettings.szName); +} + +#endif // !NO_EQ + + +void TrackerSettings::SaveSettings() +{ + + WINDOWPLACEMENT wpl; + wpl.length = sizeof(WINDOWPLACEMENT); + CMainFrame::GetMainFrame()->GetWindowPlacement(&wpl); + conf.Write<WINDOWPLACEMENT>(U_("Display"), U_("WindowPlacement"), wpl); + + conf.Write<int32>(U_("Pattern Editor"), U_("NumClipboards"), mpt::saturate_cast<int32>(PatternClipboard::GetClipboardSize())); + + // Effects +#ifndef NO_DSP + conf.Write<int32>(U_("Effects"), U_("XBassDepth"), m_MegaBassSettings.m_nXBassDepth); + conf.Write<int32>(U_("Effects"), U_("XBassRange"), m_MegaBassSettings.m_nXBassRange); +#endif +#ifndef NO_REVERB + conf.Write<int32>(U_("Effects"), U_("ReverbDepth"), m_ReverbSettings.m_nReverbDepth); + conf.Write<int32>(U_("Effects"), U_("ReverbType"), m_ReverbSettings.m_nReverbType); +#endif +#ifndef NO_DSP + conf.Write<int32>(U_("Effects"), U_("ProLogicDepth"), m_SurroundSettings.m_nProLogicDepth); + conf.Write<int32>(U_("Effects"), U_("ProLogicDelay"), m_SurroundSettings.m_nProLogicDelay); +#endif +#ifndef NO_EQ + conf.Write<EQPreset>(U_("Effects"), U_("EQ_Settings"), m_EqSettings); + conf.Write<EQPreset>(U_("Effects"), U_("EQ_User1"), m_EqUserPresets[0]); + conf.Write<EQPreset>(U_("Effects"), U_("EQ_User2"), m_EqUserPresets[1]); + conf.Write<EQPreset>(U_("Effects"), U_("EQ_User3"), m_EqUserPresets[2]); + conf.Write<EQPreset>(U_("Effects"), U_("EQ_User4"), m_EqUserPresets[3]); +#endif +#ifndef NO_DSP + conf.Write<int32>(U_("Effects"), U_("BitCrushBits"), m_BitCrushSettings.m_Bits); +#endif + + // Display (Colors) + for(int ncol = 0; ncol < MAX_MODCOLORS; ncol++) + { + conf.Write<uint32>(U_("Display"), MPT_UFORMAT("Color{}")(mpt::ufmt::dec0<2>(ncol)), rgbCustomColors[ncol]); + } + + // Paths + // Obsolete, since we always write to Keybindings.mkb now. + // Older versions of OpenMPT 1.18+ will look for this file if this entry is missing, so removing this entry after having read it is kind of backwards compatible. + conf.Remove(U_("Paths"), U_("Key_Config_File")); + + // Chords + SaveChords(Chords); + + // Save default macro configuration + MIDIMacroConfig macros; + theApp.GetDefaultMidiMacro(macros); + for(int isfx = 0; isfx < kSFxMacros; isfx++) + { + conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("SF{}")(mpt::ufmt::HEX(isfx)), macros.SFx[isfx]); + } + for(int izxx = 0; izxx < kZxxMacros; izxx++) + { + conf.Write<std::string>(U_("Zxx Macros"), MPT_UFORMAT("Z{}")(mpt::ufmt::HEX0<2>(izxx | 0x80)), macros.Zxx[izxx]); + } + + // MRU list + for(uint32 i = 0; i < (ID_MRU_LIST_LAST - ID_MRU_LIST_FIRST + 1); i++) + { + mpt::ustring key = MPT_UFORMAT("File{}")(i); + + if(i < mruFiles.size()) + { + mpt::PathString path = mruFiles[i]; + if(theApp.IsPortableMode()) + { + path = theApp.PathAbsoluteToInstallRelative(path); + } + conf.Write<mpt::PathString>(U_("Recent File List"), key, path); + } else + { + conf.Remove(U_("Recent File List"), key); + } + } +} + + +bool TrackerSettings::IsComponentBlocked(const std::string &key) +{ + return Setting<bool>(conf, U_("Components"), U_("Block") + mpt::ToUnicode(mpt::Charset::ASCII, key), ComponentManagerSettingsDefault().IsBlocked(key)); +} + + +std::vector<uint32> TrackerSettings::GetSampleRates() const +{ + return m_SoundSampleRates; +} + + +std::vector<uint32> TrackerSettings::GetDefaultSampleRates() +{ + return std::vector<uint32>{ + 192000, + 176400, + 96000, + 88200, + 48000, + 44100, + 32000, + 24000, + 22050, + 16000, + 11025, + 8000 + }; +} + + +//////////////////////////////////////////////////////////////////////////////// +// Chords + +void TrackerSettings::LoadChords(MPTChords &chords) +{ + for(std::size_t i = 0; i < std::size(chords); i++) + { + uint32 chord; + mpt::ustring noteName = MPT_UFORMAT("{}{}")(mpt::ustring(NoteNamesSharp[i % 12]), i / 12); + if((chord = conf.Read<int32>(U_("Chords"), noteName, -1)) != uint32(-1)) + { + if((chord & 0xFFFFFFC0) || chords[i].notes[0] == MPTChord::noNote) + { + chords[i].key = (uint8)(chord & 0x3F); + int shift = 6; + for(auto ¬e : chords[i].notes) + { + // Extract 6 bits and sign-extend to 8 bits + const int signBit = ((chord >> (shift + 5)) & 1); + note = static_cast<MPTChord::NoteType>(((chord >> shift) & 0x3F) | (0xC0 * signBit)); + shift += 6; + if(note == 0) + note = MPTChord::noNote; + else if(note > 0) + note--; + } + } + } + } +} + + +void TrackerSettings::SaveChords(MPTChords &chords) +{ + for(std::size_t i = 0; i < std::size(chords); i++) + { + auto notes = chords[i].notes; + for(auto ¬e : notes) + { + if(note == MPTChord::noNote) + note = 0; + else if(note >= 0) + note++; + note &= 0x3F; + } + int32 s = (chords[i].key) | (notes[0] << 6) | (notes[1] << 12) | (notes[2] << 18); + mpt::ustring noteName = MPT_UFORMAT("{}{}")(mpt::ustring(NoteNamesSharp[i % 12]), i / 12); + conf.Write<int32>(U_("Chords"), noteName, s); + } +} + + +void TrackerSettings::SetMIDIDevice(UINT id) +{ + m_nMidiDevice = id; + MIDIINCAPS mic; + mic.szPname[0] = 0; + if(midiInGetDevCaps(id, &mic, sizeof(mic)) == MMSYSERR_NOERROR) + { + midiDeviceName = mic.szPname; + } +} + + +UINT TrackerSettings::GetCurrentMIDIDevice() +{ + if(midiDeviceName.Get().IsEmpty()) + return m_nMidiDevice; + + CString deviceName = midiDeviceName; + deviceName.TrimRight(); + + MIDIINCAPS mic; + UINT candidate = m_nMidiDevice, numDevs = midiInGetNumDevs(); + for(UINT i = 0; i < numDevs; i++) + { + mic.szPname[0] = 0; + if(midiInGetDevCaps(i, &mic, sizeof(mic)) != MMSYSERR_NOERROR) + continue; + + // Some device names have trailing spaces (e.g. "USB MIDI Interface "), but those may get lost in our settings framework. + mpt::String::SetNullTerminator(mic.szPname); + size_t strLen = _tcslen(mic.szPname); + while(strLen-- > 0) + { + if(mic.szPname[strLen] == _T(' ')) + mic.szPname[strLen] = 0; + else + break; + } + if(CString(mic.szPname) == deviceName) + { + candidate = i; + numDevs = m_nMidiDevice + 1; + // If the same device name exists twice, try to match both device number and name + if(candidate == m_nMidiDevice) + return candidate; + } + } + // If the device changed its ID, update it now. + m_nMidiDevice = candidate; + return candidate; +} + + +mpt::ustring IgnoredCCsToString(const std::bitset<128> &midiIgnoreCCs) +{ + mpt::ustring cc; + bool first = true; + for(int i = 0; i < 128; i++) + { + if(midiIgnoreCCs[i]) + { + if(!first) + { + cc += U_(","); + } + cc += mpt::ufmt::val(i); + first = false; + } + } + return cc; +} + + +std::bitset<128> StringToIgnoredCCs(const mpt::ustring &in) +{ + CString cc = mpt::ToCString(in); + std::bitset<128> midiIgnoreCCs; + midiIgnoreCCs.reset(); + int curPos = 0; + CString ccToken = cc.Tokenize(_T(", "), curPos); + while(ccToken != _T("")) + { + int ccNumber = ConvertStrTo<int>(ccToken); + if(ccNumber >= 0 && ccNumber <= 127) + midiIgnoreCCs.set(ccNumber); + ccToken = cc.Tokenize(_T(", "), curPos); + } + return midiIgnoreCCs; +} + + +DefaultAndWorkingDirectory::DefaultAndWorkingDirectory() +{ + return; +} + +DefaultAndWorkingDirectory::DefaultAndWorkingDirectory(const mpt::PathString &def) + : m_Default(def) + , m_Working(def) +{ + return; +} + +DefaultAndWorkingDirectory::~DefaultAndWorkingDirectory() +{ + return; +} + +void DefaultAndWorkingDirectory::SetDefaultDir(const mpt::PathString &filenameFrom, bool stripFilename) +{ + if(InternalSet(m_Default, filenameFrom, stripFilename) && !m_Default.empty()) + { + // When updating default directory, also update the working directory. + InternalSet(m_Working, filenameFrom, stripFilename); + } +} + +void DefaultAndWorkingDirectory::SetWorkingDir(const mpt::PathString &filenameFrom, bool stripFilename) +{ + InternalSet(m_Working, filenameFrom, stripFilename); +} + +mpt::PathString DefaultAndWorkingDirectory::GetDefaultDir() const +{ + return m_Default; +} + +mpt::PathString DefaultAndWorkingDirectory::GetWorkingDir() const +{ + return m_Working; +} + +// Retrieve / set default directory from given string and store it our setup variables +// If stripFilename is true, the filenameFrom parameter is assumed to be a full path including a filename. +// Return true if the value changed. +bool DefaultAndWorkingDirectory::InternalSet(mpt::PathString &dest, const mpt::PathString &filenameFrom, bool stripFilename) +{ + mpt::PathString newPath = (stripFilename ? filenameFrom.GetPath() : filenameFrom); + newPath.EnsureTrailingSlash(); + mpt::PathString oldPath = dest; + dest = newPath; + return newPath != oldPath; +} + +ConfigurableDirectory::ConfigurableDirectory(SettingsContainer &conf, const AnyStringLocale §ion, const AnyStringLocale &key, const mpt::PathString &def) + : conf(conf) + , m_Setting(conf, section, key, def) +{ + SetDefaultDir(theApp.PathInstallRelativeToAbsolute(m_Setting), false); +} + +ConfigurableDirectory::~ConfigurableDirectory() +{ + m_Setting = theApp.IsPortableMode() ? theApp.PathAbsoluteToInstallRelative(m_Default) : m_Default; +} + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.h b/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.h new file mode 100644 index 00000000..e31f8d16 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TrackerSettings.h @@ -0,0 +1,977 @@ +/* + * TrackerSettings.h + * ----------------- + * Purpose: Header file for application setting handling. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/uuid/uuid.hpp" + +#include "../common/Logging.h" +#include "../common/version.h" +#include "openmpt/soundbase/SampleFormat.hpp" +#include "../soundlib/MixerSettings.h" +#include "../soundlib/Resampler.h" +#include "../sounddsp/EQ.h" +#include "../sounddsp/DSP.h" +#include "../sounddsp/Reverb.h" +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "StreamEncoderSettings.h" +#include "Settings.h" + +#include <array> +#include <bitset> + + +OPENMPT_NAMESPACE_BEGIN + + +namespace SoundDevice { +class Manager; +} // namespace SoundDevice + + +namespace Tuning { +class CTuningCollection; +} // namespace Tuning +using CTuningCollection = Tuning::CTuningCollection; + + +// User-defined colors +enum ModColor : uint8 +{ + MODCOLOR_BACKNORMAL = 0, + MODCOLOR_TEXTNORMAL, + MODCOLOR_BACKCURROW, + MODCOLOR_TEXTCURROW, + MODCOLOR_BACKSELECTED, + MODCOLOR_TEXTSELECTED, + MODCOLOR_SAMPLE, + MODCOLOR_BACKPLAYCURSOR, + MODCOLOR_TEXTPLAYCURSOR, + MODCOLOR_BACKHILIGHT, + MODCOLOR_NOTE, + MODCOLOR_INSTRUMENT, + MODCOLOR_VOLUME, + MODCOLOR_PANNING, + MODCOLOR_PITCH, + MODCOLOR_GLOBALS, + MODCOLOR_ENVELOPES, + MODCOLOR_VUMETER_LO, + MODCOLOR_VUMETER_MED, + MODCOLOR_VUMETER_HI, + MODCOLOR_SEPSHADOW, + MODCOLOR_SEPFACE, + MODCOLOR_SEPHILITE, + MODCOLOR_BLENDCOLOR, + MODCOLOR_DODGY_COMMANDS, + MODCOLOR_BACKSAMPLE, + MODCOLOR_SAMPLESELECTED, + MODCOLOR_BACKENV, + MODCOLOR_VUMETER_LO_VST, + MODCOLOR_VUMETER_MED_VST, + MODCOLOR_VUMETER_HI_VST, + MODCOLOR_ENVELOPE_RELEASE, + MODCOLOR_SAMPLE_LOOPMARKER, + MODCOLOR_SAMPLE_SUSTAINMARKER, + MODCOLOR_SAMPLE_CUEPOINT, + MAX_MODCOLORS, + // Internal color codes (not saved to color preset files) + MODCOLOR_2NDHIGHLIGHT, + MODCOLOR_DEFAULTVOLUME, + MODCOLOR_DUMMYCOMMAND, + MAX_MODPALETTECOLORS +}; + + +// Pattern Setup (contains also non-pattern related settings) +// Feel free to replace the deprecated flags by new flags, but be sure to +// update TrackerSettings::TrackerSettings() as well. +#define PATTERN_PLAYNEWNOTE 0x01 // play new notes while recording +#define PATTERN_SMOOTHSCROLL 0x02 // scroll tick by tick, not row by row +#define PATTERN_STDHIGHLIGHT 0x04 // enable primary highlight (measures) +#define PATTERN_NOFOLLOWONCLICK 0x08 // disable song follow when clicking into pattern +#define PATTERN_CENTERROW 0x10 // always center active row +#define PATTERN_WRAP 0x20 // wrap around cursor in editor +#define PATTERN_EFFECTHILIGHT 0x40 // effect syntax highlighting +#define PATTERN_HEXDISPLAY 0x80 // display row number in hex +#define PATTERN_FLATBUTTONS 0x100 // flat toolbar buttons +#define PATTERN_PLAYNAVIGATEROW 0x200 // play whole row when navigating +#define PATTERN_SINGLEEXPAND 0x400 // single click to expand tree +#define PATTERN_PLAYEDITROW 0x800 // play all notes on the current row while entering notes +#define PATTERN_NOEXTRALOUD 0x1000 // no loud samples in sample editor +#define PATTERN_DRAGNDROPEDIT 0x2000 // enable drag and drop editing +#define PATTERN_2NDHIGHLIGHT 0x4000 // activate secondary highlight (beats) +#define PATTERN_MUTECHNMODE 0x8000 // ignore muted channels +#define PATTERN_SHOWPREVIOUS 0x10000 // show prev/next patterns +#define PATTERN_CONTSCROLL 0x20000 // continous pattern scrolling +#define PATTERN_KBDNOTEOFF 0x40000 // Record note-off events +#define PATTERN_FOLLOWSONGOFF 0x80000 // follow song off by default +#define PATTERN_PLAYTRANSPOSE 0x100000 // Preview note transposition +#define PATTERN_NOCLOSEDIALOG 0x200000 // Don't use OpenMPT's custom close dialog with a list of saved files when closing the main window +#define PATTERN_DBLCLICKSELECT 0x400000 // Double-clicking pattern selects whole channel +#define PATTERN_OLDCTXMENUSTYLE 0x800000 // Hide pattern context menu entries instead of greying them out. +#define PATTERN_SYNCMUTE 0x1000000 // maintain sample sync on mute +#define PATTERN_AUTODELAY 0x2000000 // automatically insert delay commands in pattern when entering notes +#define PATTERN_NOTEFADE 0x4000000 // alt. note fade behaviour when entering notes +#define PATTERN_OVERFLOWPASTE 0x8000000 // continue paste in the next pattern instead of cutting off +#define PATTERN_SHOWDEFAULTVOLUME 0x10000000 // if there is no volume command next to note+instr, display the sample's default volume. +#define PATTERN_RESETCHANNELS 0x20000000 // reset channels when looping +#define PATTERN_LIVEUPDATETREE 0x40000000 // update active sample / instr icons in treeview +#define PATTERN_SYNCSAMPLEPOS 0x80000000 // sync sample positions when seeking + +#define PATTERNFONT_SMALL UL_("@1") +#define PATTERNFONT_LARGE UL_("@2") + +// MIDI Setup +#define MIDISETUP_RECORDVELOCITY 0x01 // Record MIDI velocity +#define MIDISETUP_TRANSPOSEKEYBOARD 0x02 // Apply transpose value to MIDI Notes +#define MIDISETUP_MIDITOPLUG 0x04 // Pass MIDI messages to plugins +#define MIDISETUP_MIDIVOL_TO_NOTEVOL 0x08 // Combine MIDI volume to note velocity +#define MIDISETUP_RECORDNOTEOFF 0x10 // Record MIDI Note Off to pattern +#define MIDISETUP_RESPONDTOPLAYCONTROLMSGS 0x20 // Respond to Restart/Continue/Stop MIDI commands +#define MIDISETUP_MIDIMACROCONTROL 0x80 // Record MIDI controller changes a MIDI macro changes in pattern +#define MIDISETUP_PLAYPATTERNONMIDIIN 0x100 // Play pattern if MIDI Note is received and playback is paused +#define MIDISETUP_ENABLE_RECORD_DEFAULT 0x200 // Enable MIDI recording by default +#define MIDISETUP_MIDIMACROPITCHBEND 0x400 // Record MIDI pitch bend messages a MIDI macro changes in pattern + + +#ifndef NO_EQ + +// EQ + +struct EQPresetPacked +{ + char szName[12]; // locale encoding + uint32le Gains[MAX_EQ_BANDS]; + uint32le Freqs[MAX_EQ_BANDS]; +}; +MPT_BINARY_STRUCT(EQPresetPacked, 60) + +struct EQPreset +{ + char szName[12]; // locale encoding + uint32 Gains[MAX_EQ_BANDS]; + uint32 Freqs[MAX_EQ_BANDS]; +}; + + +template<> inline SettingValue ToSettingValue(const EQPreset &val) +{ + EQPresetPacked valpacked; + std::memcpy(valpacked.szName, val.szName, std::size(valpacked.szName)); + std::copy(val.Gains, val.Gains + MAX_EQ_BANDS, valpacked.Gains); + std::copy(val.Freqs, val.Freqs + MAX_EQ_BANDS, valpacked.Freqs); + return SettingValue(EncodeBinarySetting<EQPresetPacked>(valpacked), "EQPreset"); +} +template<> inline EQPreset FromSettingValue(const SettingValue &val) +{ + ASSERT(val.GetTypeTag() == "EQPreset"); + EQPresetPacked valpacked = DecodeBinarySetting<EQPresetPacked>(val.as<std::vector<std::byte> >()); + EQPreset valresult; + std::memcpy(valresult.szName, valpacked.szName, std::size(valresult.szName)); + std::copy(valpacked.Gains, valpacked.Gains + MAX_EQ_BANDS, valresult.Gains); + std::copy(valpacked.Freqs, valpacked.Freqs + MAX_EQ_BANDS, valresult.Freqs); + return valresult; +} + +#endif // !NO_EQ + + +template<> inline SettingValue ToSettingValue(const mpt::UUID &val) { return SettingValue(val.ToUString()); } +template<> inline mpt::UUID FromSettingValue(const SettingValue &val) { return mpt::UUID::FromString(val.as<mpt::ustring>()); } + + + +// Chords +struct MPTChord +{ + enum + { + notesPerChord = 4, + relativeMode = 0x3F, + noNote = int8_min, + }; + using NoteType = int8; + + uint8 key; // Base note + std::array<NoteType, notesPerChord - 1> notes; // Additional chord notes +}; + +using MPTChords = std::array<MPTChord, 3 * 12>; // 3 octaves + +// MIDI recording +enum RecordAftertouchOptions +{ + atDoNotRecord = 0, + atRecordAsVolume, + atRecordAsMacro, +}; + +// New file action +enum NewFileAction +{ + nfDefaultFormat = 0, + nfSameAsCurrent, + nfDefaultTemplate +}; + +// Sample editor preview behaviour +enum SampleEditorKeyBehaviour +{ + seNoteOffOnNewKey = 0, + seNoteOffOnKeyUp, + seNoteOffOnKeyRestrike, +}; + +enum SampleEditorDefaultFormat +{ + dfFLAC, + dfWAV, + dfRAW, + dfS3I, +}; + +enum class TimelineFormat +{ + Seconds = 0, + Samples, + SamplesPow2, +}; + +enum class DefaultChannelColors +{ + NoColors = 0, + Rainbow, + Random, +}; + + +class SampleUndoBufferSize +{ +protected: + size_t sizeByte; + int32 sizePercent; + + void CalculateSize(); + +public: + enum + { + defaultSize = 10, // In percent + }; + + SampleUndoBufferSize(int32 percent = defaultSize) : sizePercent(percent) { CalculateSize(); } + void Set(int32 percent) { sizePercent = percent; CalculateSize(); } + + int32 GetSizeInPercent() const { return sizePercent; } + size_t GetSizeInBytes() const { return sizeByte; } +}; + +template<> inline SettingValue ToSettingValue(const SampleUndoBufferSize &val) { return SettingValue(val.GetSizeInPercent()); } +template<> inline SampleUndoBufferSize FromSettingValue(const SettingValue &val) { return SampleUndoBufferSize(val.as<int32>()); } + + +mpt::ustring IgnoredCCsToString(const std::bitset<128> &midiIgnoreCCs); +std::bitset<128> StringToIgnoredCCs(const mpt::ustring &in); + +mpt::ustring SettingsModTypeToString(MODTYPE modtype); +MODTYPE SettingsStringToModType(const mpt::ustring &str); + + +template<> inline SettingValue ToSettingValue(const RecordAftertouchOptions &val) { return SettingValue(int32(val)); } +template<> inline RecordAftertouchOptions FromSettingValue(const SettingValue &val) { return RecordAftertouchOptions(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const SampleEditorKeyBehaviour &val) { return SettingValue(int32(val)); } +template<> inline SampleEditorKeyBehaviour FromSettingValue(const SettingValue &val) { return SampleEditorKeyBehaviour(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const TimelineFormat &val) { return SettingValue(int32(val)); } +template<> inline TimelineFormat FromSettingValue(const SettingValue &val) { return TimelineFormat(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const DefaultChannelColors & val) { return SettingValue(int32(val)); } +template<> inline DefaultChannelColors FromSettingValue(const SettingValue& val) { return DefaultChannelColors(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const MODTYPE &val) { return SettingValue(SettingsModTypeToString(val), "MODTYPE"); } +template<> inline MODTYPE FromSettingValue(const SettingValue &val) { ASSERT(val.GetTypeTag() == "MODTYPE"); return SettingsStringToModType(val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const PlugVolumeHandling &val) +{ + return SettingValue(int32(val), "PlugVolumeHandling"); +} +template<> inline PlugVolumeHandling FromSettingValue(const SettingValue &val) +{ + ASSERT(val.GetTypeTag() == "PlugVolumeHandling"); + if((uint32)val.as<int32>() > PLUGIN_VOLUMEHANDLING_MAX) + { + return PLUGIN_VOLUMEHANDLING_IGNORE; + } + return static_cast<PlugVolumeHandling>(val.as<int32>()); +} + +template<> inline SettingValue ToSettingValue(const std::vector<uint32> &val) { return mpt::String::Combine(val, U_(",")); } +template<> inline std::vector<uint32> FromSettingValue(const SettingValue &val) { return mpt::String::Split<uint32>(val, U_(",")); } + +template<> inline SettingValue ToSettingValue(const std::vector<mpt::ustring> &val) { return mpt::String::Combine(val, U_(";")); } +template<> inline std::vector<mpt::ustring> FromSettingValue(const SettingValue &val) { return mpt::String::Split<mpt::ustring>(val, U_(";")); } + +template<> inline SettingValue ToSettingValue(const SampleFormat &val) { return SettingValue(val.AsInt()); } +template<> inline SampleFormat FromSettingValue(const SettingValue &val) { return SampleFormat::FromInt(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const SoundDevice::ChannelMapping &val) { return SettingValue(val.ToUString(), "ChannelMapping"); } +template<> inline SoundDevice::ChannelMapping FromSettingValue(const SettingValue &val) { ASSERT(val.GetTypeTag() == "ChannelMapping"); return SoundDevice::ChannelMapping::FromString(val.as<mpt::ustring>()); } + +template<> inline SettingValue ToSettingValue(const ResamplingMode &val) { return SettingValue(int32(val)); } +template<> inline ResamplingMode FromSettingValue(const SettingValue &val) { return ResamplingMode(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const Resampling::AmigaFilter &val) { return SettingValue(int32(val)); } +template<> inline Resampling::AmigaFilter FromSettingValue(const SettingValue &val) { return static_cast<Resampling::AmigaFilter>(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const NewFileAction &val) { return SettingValue(int32(val)); } +template<> inline NewFileAction FromSettingValue(const SettingValue &val) { return NewFileAction(val.as<int32>()); } + +template<> inline SettingValue ToSettingValue(const std::bitset<128> &val) +{ + return SettingValue(IgnoredCCsToString(val), "IgnoredCCs"); +} +template<> inline std::bitset<128> FromSettingValue(const SettingValue &val) +{ + ASSERT(val.GetTypeTag() == "IgnoredCCs"); + return StringToIgnoredCCs(val.as<mpt::ustring>()); +} + +template<> inline SettingValue ToSettingValue(const SampleEditorDefaultFormat &val) +{ + mpt::ustring format; + switch(val) + { + case dfWAV: + format = U_("wav"); + break; + case dfFLAC: + default: + format = U_("flac"); + break; + case dfRAW: + format = U_("raw"); + break; + case dfS3I: + format = U_("s3i"); + break; + } + return SettingValue(format); +} +template<> inline SampleEditorDefaultFormat FromSettingValue(const SettingValue &val) +{ + mpt::ustring format = mpt::ToLowerCase(val.as<mpt::ustring>()); + if(format == U_("wav")) + return dfWAV; + if(format == U_("raw")) + return dfRAW; + if(format == U_("s3i")) + return dfS3I; + else // if(format == U_("flac")) + return dfFLAC; +} + +enum SoundDeviceStopMode +{ + SoundDeviceStopModeClosed = 0, + SoundDeviceStopModeStopped = 1, + SoundDeviceStopModePlaying = 2, +}; + +template<> inline SettingValue ToSettingValue(const SoundDeviceStopMode &val) +{ + return SettingValue(static_cast<int32>(val)); +} +template<> inline SoundDeviceStopMode FromSettingValue(const SettingValue &val) +{ + return static_cast<SoundDeviceStopMode>(static_cast<int32>(val)); +} + + +enum ProcessPriorityClass +{ + ProcessPriorityClassIDLE = IDLE_PRIORITY_CLASS, + ProcessPriorityClassBELOW = BELOW_NORMAL_PRIORITY_CLASS, + ProcessPriorityClassNORMAL = NORMAL_PRIORITY_CLASS, + ProcessPriorityClassABOVE = ABOVE_NORMAL_PRIORITY_CLASS, + ProcessPriorityClassHIGH = HIGH_PRIORITY_CLASS, + ProcessPriorityClassREALTIME = REALTIME_PRIORITY_CLASS +}; +template<> inline SettingValue ToSettingValue(const ProcessPriorityClass &val) +{ + mpt::ustring s; + switch(val) + { + case ProcessPriorityClassIDLE: + s = U_("idle"); + break; + case ProcessPriorityClassBELOW: + s = U_("below"); + break; + case ProcessPriorityClassNORMAL: + s = U_("normal"); + break; + case ProcessPriorityClassABOVE: + s = U_("above"); + break; + case ProcessPriorityClassHIGH: + s = U_("high"); + break; + case ProcessPriorityClassREALTIME: + s = U_("realtime"); + break; + default: + s = U_("normal"); + break; + } + return SettingValue(s); +} +template<> inline ProcessPriorityClass FromSettingValue(const SettingValue &val) +{ + ProcessPriorityClass result = ProcessPriorityClassNORMAL; + mpt::ustring s = val.as<mpt::ustring>(); + if(s.empty()) + { + result = ProcessPriorityClassNORMAL; + } else if(s == U_("idle")) + { + result = ProcessPriorityClassIDLE; + } else if(s == U_("below")) + { + result = ProcessPriorityClassBELOW; + } else if(s == U_("normal")) + { + result = ProcessPriorityClassNORMAL; + } else if(s == U_("above")) + { + result = ProcessPriorityClassABOVE; + } else if(s == U_("high")) + { + result = ProcessPriorityClassHIGH; + } else if(s == U_("realtime")) + { + result = ProcessPriorityClassREALTIME; + } else + { + result = ProcessPriorityClassNORMAL; + } + return result; +} + + +template<> inline SettingValue ToSettingValue(const mpt::Date::Unix &val) +{ + time_t t = val; + const tm* lastUpdate = gmtime(&t); + CString outDate; + if(lastUpdate) + { + outDate.Format(_T("%04d-%02d-%02d %02d:%02d"), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min); + } + return SettingValue(mpt::ToUnicode(outDate), "UTC"); +} +template<> inline mpt::Date::Unix FromSettingValue(const SettingValue &val) +{ + MPT_ASSERT(val.GetTypeTag() == "UTC"); + std::string s = mpt::ToCharset(mpt::Charset::Locale, val.as<mpt::ustring>()); + tm lastUpdate; + MemsetZero(lastUpdate); + if(sscanf(s.c_str(), "%04d-%02d-%02d %02d:%02d", &lastUpdate.tm_year, &lastUpdate.tm_mon, &lastUpdate.tm_mday, &lastUpdate.tm_hour, &lastUpdate.tm_min) == 5) + { + lastUpdate.tm_year -= 1900; + lastUpdate.tm_mon--; + } + time_t outTime = mpt::Date::Unix::FromUTC(lastUpdate); + if(outTime < 0) + { + outTime = 0; + } + return mpt::Date::Unix(outTime); +} + +struct FontSetting +{ + enum FontFlags + { + None = 0, + Bold = 1, + Italic = 2, + }; + + mpt::ustring name; + int32 size; + FlagSet<FontFlags> flags; + + FontSetting(const mpt::ustring &name = U_(""), int32 size = 120, FontFlags flags = None) : name(name), size(size), flags(flags) { } + + bool operator== (const FontSetting &other) const + { + return name == other.name && size == other.size && flags == other.flags; + } + + bool operator!= (const FontSetting &other) const + { + return !(*this == other); + } +}; + +MPT_DECLARE_ENUM(FontSetting::FontFlags) + +template<> inline SettingValue ToSettingValue(const FontSetting &val) +{ + return SettingValue(mpt::ToUnicode(val.name) + U_(",") + mpt::ufmt::val(val.size) + U_("|") + mpt::ufmt::val(val.flags.GetRaw())); +} +template<> inline FontSetting FromSettingValue(const SettingValue &val) +{ + FontSetting setting(val.as<mpt::ustring>()); + std::size_t sizeStart = setting.name.rfind(UC_(',')); + if(sizeStart != std::string::npos) + { + const std::vector<mpt::ustring> fields = mpt::String::Split<mpt::ustring>(setting.name.substr(sizeStart + 1), U_("|")); + if(fields.size() >= 1) + { + setting.size = ConvertStrTo<int32>(fields[0]); + } + if(fields.size() >= 2) + { + setting.flags = static_cast<FontSetting::FontFlags>(ConvertStrTo<int32>(fields[1])); + } + setting.name.resize(sizeStart); + } + return setting; +} + + +class DefaultAndWorkingDirectory +{ +protected: + mpt::PathString m_Default; + mpt::PathString m_Working; +public: + DefaultAndWorkingDirectory(); + DefaultAndWorkingDirectory(const mpt::PathString &def); + ~DefaultAndWorkingDirectory(); +public: + void SetDefaultDir(const mpt::PathString &filenameFrom, bool stripFilename = false); + void SetWorkingDir(const mpt::PathString &filenameFrom, bool stripFilename = false); + mpt::PathString GetDefaultDir() const; + mpt::PathString GetWorkingDir() const; +private: + bool InternalSet(mpt::PathString &dest, const mpt::PathString &filenameFrom, bool stripFilename); +}; + +class ConfigurableDirectory + : public DefaultAndWorkingDirectory +{ +protected: + SettingsContainer &conf; + Setting<mpt::PathString> m_Setting; +public: + ConfigurableDirectory(SettingsContainer &conf, const AnyStringLocale §ion, const AnyStringLocale &key, const mpt::PathString &def); + ~ConfigurableDirectory(); +}; + + +class DebugSettings +{ + +private: + + SettingsContainer &conf; + +private: + + // Debug + +#if !defined(MPT_LOG_IS_DISABLED) + Setting<int> DebugLogLevel; + Setting<std::string> DebugLogFacilitySolo; + Setting<std::string> DebugLogFacilityBlocked; + Setting<bool> DebugLogFileEnable; + Setting<bool> DebugLogDebuggerEnable; + Setting<bool> DebugLogConsoleEnable; +#endif + + Setting<bool> DebugTraceEnable; + Setting<uint32> DebugTraceSize; + Setting<bool> DebugTraceAlwaysDump; + + Setting<bool> DebugStopSoundDeviceOnCrash; + Setting<bool> DebugStopSoundDeviceBeforeDump; + + Setting<bool> DebugDelegateToWindowsHandler; + +public: + + DebugSettings(SettingsContainer &conf); + + ~DebugSettings(); + +}; + + +namespace SoundDevice +{ +namespace Legacy +{ +typedef uint16 ID; +inline constexpr SoundDevice::Legacy::ID MaskType = 0xff00; +inline constexpr SoundDevice::Legacy::ID MaskIndex = 0x00ff; +inline constexpr int ShiftType = 8; +inline constexpr int ShiftIndex = 0; +inline constexpr SoundDevice::Legacy::ID TypeWAVEOUT = 0; +inline constexpr SoundDevice::Legacy::ID TypeDSOUND = 1; +inline constexpr SoundDevice::Legacy::ID TypeASIO = 2; +inline constexpr SoundDevice::Legacy::ID TypePORTAUDIO_WASAPI = 3; +inline constexpr SoundDevice::Legacy::ID TypePORTAUDIO_WDMKS = 4; +inline constexpr SoundDevice::Legacy::ID TypePORTAUDIO_WMME = 5; +inline constexpr SoundDevice::Legacy::ID TypePORTAUDIO_DS = 6; +} // namespace Legacy +} // namespace SoundDevice + + +class TrackerSettings +{ + +private: + SettingsContainer &conf; + +public: + + // Version + + Setting<mpt::ustring> IniVersion; + const bool FirstRun; + const Version PreviousSettingsVersion; + Setting<mpt::UUID> VersionInstallGUID; + + // Display + + Setting<bool> m_ShowSplashScreen; + Setting<bool> gbMdiMaximize; + Setting<bool> highResUI; + Setting<LONG> glTreeSplitRatio; + Setting<LONG> glTreeWindowWidth; + Setting<LONG> glGeneralWindowHeight; + Setting<LONG> glPatternWindowHeight; + Setting<LONG> glSampleWindowHeight; + Setting<LONG> glInstrumentWindowHeight; + Setting<LONG> glCommentsWindowHeight; + Setting<LONG> glGraphWindowHeight; + + Setting<int32> gnPlugWindowX; + Setting<int32> gnPlugWindowY; + Setting<int32> gnPlugWindowWidth; + Setting<int32> gnPlugWindowHeight; + Setting<int32> gnPlugWindowLast; // Last selected plugin ID + + Setting<uint32> gnMsgBoxVisiblityFlags; + Setting<uint32> GUIUpdateInterval; + CachedSetting<uint32> FSUpdateInterval; + CachedSetting<uint32> VuMeterUpdateInterval; + CachedSetting<float> VuMeterDecaySpeedDecibelPerSecond; + + CachedSetting<bool> accidentalFlats; + Setting<bool> rememberSongWindows; + Setting<bool> showDirsInSampleBrowser; + Setting<DefaultChannelColors> defaultRainbowChannelColors; + + Setting<FontSetting> commentsFont; + + // Misc + + Setting<MODTYPE> defaultModType; + Setting<NewFileAction> defaultNewFileAction; + Setting<PlugVolumeHandling> DefaultPlugVolumeHandling; + Setting<bool> autoApplySmoothFT2Ramping; + CachedSetting<uint32> MiscITCompressionStereo; // Mask: bit0: IT, bit1: Compat IT, bit2: MPTM + CachedSetting<uint32> MiscITCompressionMono; // Mask: bit0: IT, bit1: Compat IT, bit2: MPTM + CachedSetting<bool> MiscSaveChannelMuteStatus; + CachedSetting<bool> MiscAllowMultipleCommandsPerKey; + CachedSetting<bool> MiscDistinguishModifiers; + Setting<ProcessPriorityClass> MiscProcessPriorityClass; + CachedSetting<bool> MiscFlushFileBuffersOnSave; + CachedSetting<bool> MiscCacheCompleteFileBeforeLoading; + Setting<bool> MiscUseSingleInstance; + + // Sound Settings + + bool m_SoundShowRecordingSettings; + Setting<bool> m_SoundShowDeprecatedDevices; + Setting<bool> m_SoundDeprecatedDeviceWarningShown; + Setting<std::vector<uint32> > m_SoundSampleRates; + Setting<bool> m_SoundSettingsOpenDeviceAtStartup; + Setting<SoundDeviceStopMode> m_SoundSettingsStopMode; + + bool m_SoundDeviceSettingsUseOldDefaults; + SoundDevice::Legacy::ID m_SoundDeviceID_DEPRECATED; + SoundDevice::Settings m_SoundDeviceSettingsDefaults; + SoundDevice::Settings GetSoundDeviceSettingsDefaults() const; +#if defined(MPT_WITH_DIRECTSOUND) + bool m_SoundDeviceDirectSoundOldDefaultIdentifier; +#endif // MPT_WITH_DIRECTSOUND + + Setting<SoundDevice::Identifier> m_SoundDeviceIdentifier; + SoundDevice::Identifier GetSoundDeviceIdentifier() const; + void SetSoundDeviceIdentifier(const SoundDevice::Identifier &identifier); + SoundDevice::Settings GetSoundDeviceSettings(const SoundDevice::Identifier &device) const; + void SetSoundDeviceSettings(const SoundDevice::Identifier &device, const SoundDevice::Settings &settings); + + Setting<uint32> MixerMaxChannels; + Setting<uint32> MixerDSPMask; + Setting<uint32> MixerFlags; + Setting<uint32> MixerSamplerate; + Setting<uint32> MixerOutputChannels; + Setting<uint32> MixerPreAmp; + Setting<uint32> MixerStereoSeparation; + Setting<uint32> MixerVolumeRampUpMicroseconds; + Setting<uint32> MixerVolumeRampDownMicroseconds; + Setting<uint32> MixerNumInputChannels; + MixerSettings GetMixerSettings() const; + void SetMixerSettings(const MixerSettings &settings); + + Setting<ResamplingMode> ResamplerMode; + Setting<uint8> ResamplerSubMode; + Setting<int32> ResamplerCutoffPercent; + Setting<Resampling::AmigaFilter> ResamplerEmulateAmiga; + CResamplerSettings GetResamplerSettings() const; + void SetResamplerSettings(const CResamplerSettings &settings); + + Setting<int> SoundBoostedThreadPriority; + Setting<mpt::ustring> SoundBoostedThreadMMCSSClass; + Setting<bool> SoundBoostedThreadRealtimePosix; + Setting<int> SoundBoostedThreadNicenessPosix; + Setting<int> SoundBoostedThreadRtprioPosix; + Setting<bool> SoundMaskDriverCrashes; + Setting<bool> SoundAllowDeferredProcessing; + + // MIDI Settings + + Setting<UINT> m_nMidiDevice; + Setting<CString> midiDeviceName; + // FIXME: MIDI recording is currently done in its own callback/thread and + // accesses settings framework from in there. Work-around the ASSERTs for + // now by using cached settings. + CachedSetting<uint32> m_dwMidiSetup; + CachedSetting<RecordAftertouchOptions> aftertouchBehaviour; + CachedSetting<uint16> midiVelocityAmp; + CachedSetting<std::bitset<128> > midiIgnoreCCs; + + Setting<uint32> midiImportPatternLen; + Setting<uint32> midiImportQuantize; + Setting<uint8> midiImportTicks; + + // Pattern Editor + + CachedSetting<bool> gbLoopSong; + CachedSetting<UINT> gnPatternSpacing; + CachedSetting<bool> gbPatternVUMeters; + CachedSetting<bool> gbPatternPluginNames; + CachedSetting<bool> gbPatternRecord; + CachedSetting<bool> patternNoEditPopup; + CachedSetting<bool> patternStepCommands; + CachedSetting<uint32> m_dwPatternSetup; + CachedSetting<uint32> m_nRowHighlightMeasures; // primary (measures) and secondary (beats) highlight + CachedSetting<uint32> m_nRowHighlightBeats; // primary (measures) and secondary (beats) highlight + CachedSetting<ROWINDEX> recordQuantizeRows; + CachedSetting<UINT> gnAutoChordWaitTime; + CachedSetting<int32> orderlistMargins; + CachedSetting<int32> rowDisplayOffset; + Setting<FontSetting> patternFont; + Setting<mpt::ustring> patternFontDot; + Setting<int32> effectVisWidth; + Setting<int32> effectVisHeight; + Setting<int32> effectVisX; + Setting<int32> effectVisY; + Setting<CString> patternAccessibilityFormat; + CachedSetting<bool> patternAlwaysDrawWholePatternOnScrollSlow; + CachedSetting<bool> orderListOldDropBehaviour; + + // Sample Editor + + Setting<SampleUndoBufferSize> m_SampleUndoBufferSize; + Setting<SampleEditorKeyBehaviour> sampleEditorKeyBehaviour; + Setting<SampleEditorDefaultFormat> m_defaultSampleFormat; + Setting<TimelineFormat> sampleEditorTimelineFormat; + Setting<ResamplingMode> sampleEditorDefaultResampler; + Setting<int32> m_nFinetuneStep; // Increment finetune by x cents when using spin control. + Setting<int32> m_FLACCompressionLevel; // FLAC compression level for saving (0...8) + Setting<bool> compressITI; + Setting<bool> m_MayNormalizeSamplesOnLoad; + Setting<bool> previewInFileDialogs; + CachedSetting<bool> cursorPositionInHex; + + // Export + + Setting<bool> ExportDefaultToSoundcardSamplerate; + StreamEncoderSettingsConf ExportStreamEncoderSettings; + + // Components + + Setting<bool> ComponentsLoadOnStartup; + Setting<bool> ComponentsKeepLoaded; + bool IsComponentBlocked(const std::string &name); + + // Effects + +#ifndef NO_REVERB + CReverbSettings m_ReverbSettings; +#endif +#ifndef NO_DSP + CSurroundSettings m_SurroundSettings; +#endif +#ifndef NO_DSP + CMegaBassSettings m_MegaBassSettings; +#endif +#ifndef NO_EQ + EQPreset m_EqSettings; + EQPreset m_EqUserPresets[4]; +#endif +#ifndef NO_DSP + BitCrushSettings m_BitCrushSettings; +#endif + + // Display (Colors) + + std::array<COLORREF, MAX_MODCOLORS> rgbCustomColors; + + // AutoSave + CachedSetting<bool> CreateBackupFiles; + CachedSetting<bool> AutosaveEnabled; + CachedSetting<uint32> AutosaveIntervalMinutes; + CachedSetting<uint32> AutosaveHistoryDepth; + CachedSetting<bool> AutosaveUseOriginalPath; + ConfigurableDirectory AutosavePath; + + // Paths + + ConfigurableDirectory PathSongs; + ConfigurableDirectory PathSamples; + ConfigurableDirectory PathInstruments; + ConfigurableDirectory PathPlugins; + ConfigurableDirectory PathPluginPresets; + ConfigurableDirectory PathExport; + DefaultAndWorkingDirectory PathTunings; + DefaultAndWorkingDirectory PathUserTemplates; + mpt::PathString m_szKbdFile; + + // Default template + + Setting<mpt::PathString> defaultTemplateFile; + Setting<mpt::ustring> defaultArtist; + + Setting<uint32> mruListLength; + std::vector<mpt::PathString> mruFiles; + + // Chords + + MPTChords Chords; + + // Tunings + + std::unique_ptr<CTuningCollection> oldLocalTunings; + + // Plugins + + Setting<bool> bridgeAllPlugins; + Setting<bool> enableAutoSuspend; + CachedSetting<bool> midiMappingInPluginEditor; + Setting<mpt::ustring> pluginProjectPath; + CachedSetting<mpt::lstring> vstHostProductString; + CachedSetting<mpt::lstring> vstHostVendorString; + CachedSetting<int32> vstHostVendorVersion; + + // Broken Plugins Workarounds + + Setting<bool> BrokenPluginsWorkaroundVSTMaskAllCrashes; + Setting<bool> BrokenPluginsWorkaroundVSTNeverUnloadAnyPlugin; + +#if defined(MPT_ENABLE_UPDATE) + + // Update + + Setting<bool> UpdateEnabled; + Setting<bool> UpdateInstallAutomatically; + Setting<mpt::Date::Unix> UpdateLastUpdateCheck; + Setting<int32> UpdateUpdateCheckPeriod_DEPRECATED; + Setting<int32> UpdateIntervalDays; + Setting<uint32> UpdateChannel; + Setting<mpt::ustring> UpdateUpdateURL_DEPRECATED; + Setting<mpt::ustring> UpdateAPIURL; + Setting<bool> UpdateStatisticsConsentAsked; + Setting<bool> UpdateStatistics; + Setting<bool> UpdateSendGUID_DEPRECATED; + Setting<bool> UpdateShowUpdateHint; + Setting<CString> UpdateIgnoreVersion; + Setting<bool> UpdateSkipSignatureVerificationUNSECURE; + Setting<std::vector<mpt::ustring>> UpdateSigningKeysRootAnchors; + +#endif // MPT_ENABLE_UPDATE + + // Wine support + + Setting<bool> WineSupportEnabled; + Setting<bool> WineSupportAlwaysRecompile; + Setting<bool> WineSupportAskCompile; + Setting<int32> WineSupportCompileVerbosity; + Setting<bool> WineSupportForeignOpenMPT; + Setting<bool> WineSupportAllowUnknownHost; + Setting<int32> WineSupportEnablePulseAudio; // 0==off 1==auto 2==on + Setting<int32> WineSupportEnablePortAudio; // 0==off 1==auto 2==on + Setting<int32> WineSupportEnableRtAudio; // 0==off 1==auto 2==on + +public: + + TrackerSettings(SettingsContainer &conf); + + ~TrackerSettings(); + + void MigrateOldSoundDeviceSettings(SoundDevice::Manager &manager); + +private: + void MigrateTunings(const Version storedVersion); + std::unique_ptr<CTuningCollection> LoadLocalTunings(); +public: + + void SaveSettings(); + + static void GetDefaultColourScheme(std::array<COLORREF, MAX_MODCOLORS> &colours); + + std::vector<uint32> GetSampleRates() const; + + static MPTChords &GetChords() { return Instance().Chords; } + + // Get settings object singleton + static TrackerSettings &Instance(); + + void SetMIDIDevice(UINT id); + UINT GetCurrentMIDIDevice(); + +protected: + + static std::vector<uint32> GetDefaultSampleRates(); + +#ifndef NO_EQ + + void FixupEQ(EQPreset &eqSettings); + +#endif // !NO_EQ + + void LoadChords(MPTChords &chords); + void SaveChords(MPTChords &chords); + +}; + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.cpp new file mode 100644 index 00000000..0afcd55d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.cpp @@ -0,0 +1,1745 @@ +/* + * TuningDialog.cpp + * ---------------- + * Purpose: Alternative sample tuning configuration dialog. + * 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 "TuningDialog.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "TrackerSettings.h" +#include <algorithm> +#include "../common/mptFileIO.h" +#include "../common/misc_util.h" +#include "TuningDialog.h" +#include "FileDialog.h" +#include "Mainfrm.h" + + +OPENMPT_NAMESPACE_BEGIN + + +const mpt::Charset TuningCharsetFallback = mpt::Charset::Locale; + + +const CTuningDialog::TUNINGTREEITEM CTuningDialog::s_notFoundItemTuning = TUNINGTREEITEM(); +const HTREEITEM CTuningDialog::s_notFoundItemTree = NULL; + +using UNOTEINDEXTYPE = Tuning::UNOTEINDEXTYPE; +using RATIOTYPE = Tuning::RATIOTYPE; +using NOTEINDEXTYPE = Tuning::NOTEINDEXTYPE; + + +// CTuningDialog dialog +CTuningDialog::CTuningDialog(CWnd* pParent, INSTRUMENTINDEX inst, CSoundFile &csf) + : CDialog(CTuningDialog::IDD, pParent), + m_sndFile(csf), + m_pActiveTuningCollection(NULL), + m_TreeCtrlTuning(this), + m_TreeItemTuningItemMap(s_notFoundItemTree, s_notFoundItemTuning), + m_NoteEditApply(true), + m_RatioEditApply(true), + m_DoErrorExit(false) +{ + m_TuningCollections.push_back(&(m_sndFile.GetTuneSpecificTunings())); + m_TuningCollectionsNames[&(m_sndFile.GetTuneSpecificTunings())] = _T("Tunings"); + m_pActiveTuning = m_sndFile.Instruments[inst]->pTuning; + m_RatioMapWnd.m_pTuning = m_pActiveTuning; //pTun is the tuning to show when dialog opens. +} + +CTuningDialog::~CTuningDialog() +{ + for(auto &tuningCol : m_TuningCollections) + { + if(IsDeletable(tuningCol)) + { + delete tuningCol; + tuningCol = nullptr; + } + } + m_TuningCollections.clear(); + m_DeletableTuningCollections.clear(); +} + +HTREEITEM CTuningDialog::AddTreeItem(CTuningCollection* pTC, HTREEITEM parent, HTREEITEM insertAfter) +{ + const HTREEITEM temp = m_TreeCtrlTuning.InsertItem((IsDeletable(pTC) ? CString(_T("loaded: ")) : CString()) + m_TuningCollectionsNames[pTC], parent, insertAfter); + HTREEITEM temp2 = NULL; + m_TreeItemTuningItemMap.AddPair(temp, TUNINGTREEITEM(pTC)); + for(const auto &tuning : *pTC) + { + temp2 = AddTreeItem(tuning.get(), temp, temp2); + } + m_TreeCtrlTuning.EnsureVisible(temp); + return temp; +} + +HTREEITEM CTuningDialog::AddTreeItem(CTuning* pT, HTREEITEM parent, HTREEITEM insertAfter) +{ + const HTREEITEM temp = m_TreeCtrlTuning.InsertItem(mpt::ToCString(pT->GetName()), parent, insertAfter); + m_TreeItemTuningItemMap.AddPair(temp, TUNINGTREEITEM(pT)); + m_TreeCtrlTuning.EnsureVisible(temp); + return temp; +} + +void CTuningDialog::DeleteTreeItem(CTuning* pT) +{ + if(!pT) + return; + + HTREEITEM temp = m_TreeItemTuningItemMap.GetMapping_21(TUNINGTREEITEM(pT)); + if(temp) + { + HTREEITEM nextitem = m_TreeCtrlTuning.GetNextItem(temp, TVGN_NEXT); + if(!nextitem) nextitem = m_TreeCtrlTuning.GetNextItem(temp, TVGN_PREVIOUS); + m_pActiveTuning = m_TreeItemTuningItemMap.GetMapping_12(nextitem).GetT(); + m_TreeCtrlTuning.DeleteItem(temp); + //Note: Item from map is deleted 'automatically' in + //OnTvnDeleteitemTreeTuning. + + } +} + +void CTuningDialog::DeleteTreeItem(CTuningCollection* pTC) +{ + if(!pTC) + return; + + m_pActiveTuning = nullptr; + const HTREEITEM temp = m_TreeItemTuningItemMap.GetMapping_21(TUNINGTREEITEM(pTC)); + if(temp) + { + TUNINGTREEITEM prevTTI = m_TreeItemTuningItemMap.GetMapping_12(m_TreeCtrlTuning.GetNextItem(temp, TVGN_PREVIOUS)); + TUNINGTREEITEM nextTTI = m_TreeItemTuningItemMap.GetMapping_12(m_TreeCtrlTuning.GetNextItem(temp, TVGN_NEXT)); + + CTuningCollection* pTCprev = prevTTI.GetTC(); + CTuningCollection* pTCnext = nextTTI.GetTC(); + if(pTCnext == nullptr) + pTCnext = GetpTuningCollection(nextTTI.GetT()); + if(pTCprev == nullptr) + pTCprev = GetpTuningCollection(prevTTI.GetT()); + + if(pTCnext != nullptr && pTCnext != m_pActiveTuningCollection) + m_pActiveTuningCollection = pTCnext; + else + { + if(pTCprev != m_pActiveTuningCollection) + m_pActiveTuningCollection = pTCprev; + else + m_pActiveTuningCollection = NULL; + } + + m_TreeCtrlTuning.DeleteItem(temp); + //Note: Item from map is deleted 'automatically' in + //OnTvnDeleteitemTreeTuning. + } + else + { + ASSERT(false); + m_DoErrorExit = true; + m_pActiveTuningCollection = NULL; + } +} + +BOOL CTuningDialog::OnInitDialog() +{ + CDialog::OnInitDialog(); + + m_EditRatioPeriod.SubclassDlgItem(IDC_EDIT_RATIOPERIOD, this); + m_EditRatio.SubclassDlgItem(IDC_EDIT_RATIOVALUE, this); + m_EditRatioPeriod.AllowNegative(false); + m_EditRatioPeriod.AllowFractions(true); + m_EditRatio.AllowNegative(false); + m_EditRatio.AllowFractions(true); + + m_RatioMapWnd.Init(this, 0); + + //-->Creating treeview + m_TreeItemTuningItemMap.ClearMapping(); + for(const auto &tuningCol : m_TuningCollections) + { + AddTreeItem(tuningCol, NULL, NULL); + } + //<-- Creating treeview + + m_pActiveTuningCollection = GetpTuningCollection(m_pActiveTuning); + + //Adding tuning type names to corresponding combobox. + m_CombobTuningType.SetItemData(m_CombobTuningType.AddString(_T("General")), static_cast<uint16>(Tuning::Type::GENERAL)); + m_CombobTuningType.SetItemData(m_CombobTuningType.AddString(_T("GroupGeometric")), static_cast<uint16>(Tuning::Type::GROUPGEOMETRIC)); + m_CombobTuningType.SetItemData(m_CombobTuningType.AddString(_T("Geometric")), static_cast<uint16>(Tuning::Type::GEOMETRIC)); + m_CombobTuningType.EnableWindow(FALSE); + + m_ButtonSet.EnableWindow(FALSE); + + m_EditSteps.SetLimitText(2); + m_EditFineTuneSteps.SetLimitText(3); + + if(m_pActiveTuning) m_RatioMapWnd.m_nNote = m_RatioMapWnd.m_nNoteCentre + m_pActiveTuning->GetNoteRange().first + (m_pActiveTuning->GetNoteRange().last - m_pActiveTuning->GetNoteRange().first)/2 + 1; + + UpdateView(); + + return TRUE; +} + + +bool CTuningDialog::CanEdit(CTuning * pT, CTuningCollection * pTC) const +{ + if(!pT) + { + return false; + } + if(!pTC) + { + return false; + } + if(pTC != m_TuningCollections[0]) + { + return false; + } + return true; +} + + +bool CTuningDialog::CanEdit(CTuningCollection * pTC) const +{ + if(!pTC) + { + return false; + } + if(pTC != m_TuningCollections[0]) + { + return false; + } + return true; +} + + +void CTuningDialog::UpdateView(const int updateMask) +{ + if(m_DoErrorExit) + { + DoErrorExit(); + return; + } + + //-->Updating treeview + if(updateMask != UM_TUNINGDATA) + { + TUNINGTREEITEM tuningitem; + if(m_pActiveTuning) + tuningitem.Set(m_pActiveTuning); + else + { + if(m_pActiveTuningCollection) + tuningitem.Set(m_pActiveTuningCollection); + } + HTREEITEM treeitem = m_TreeItemTuningItemMap.GetMapping_21(tuningitem); + if(treeitem) + { + m_TreeCtrlTuning.Select(treeitem, TVGN_CARET); + if(m_pActiveTuning) + m_TreeCtrlTuning.SetItemText(treeitem, mpt::ToCString(m_pActiveTuning->GetName())); + else + m_TreeCtrlTuning.SetItemText(treeitem, (IsDeletable(m_pActiveTuningCollection) ? CString(_T("loaded: ")) : CString()) + m_TuningCollectionsNames[m_pActiveTuningCollection]); + } + } + //<--Updating treeview + + + if(m_pActiveTuningCollection == NULL) + { + return; + } + + m_ButtonNew.EnableWindow(TRUE); + m_ButtonImport.EnableWindow(TRUE); + m_ButtonExport.EnableWindow((m_pActiveTuning || m_pActiveTuningCollection) ? TRUE : FALSE); + m_ButtonRemove.EnableWindow(((m_pActiveTuning && (m_pActiveTuningCollection == m_TuningCollections[0])) || (!m_pActiveTuning && m_pActiveTuningCollection && m_pActiveTuningCollection != m_TuningCollections[0])) ? TRUE : FALSE); + + //Updating tuning part--> + if(m_pActiveTuning != NULL && (updateMask & UM_TUNINGDATA || updateMask == 0)) + { + UpdateTuningType(); + + m_EditName.SetWindowText(mpt::ToCString(m_pActiveTuning->GetName())); + m_EditName.Invalidate(); + + //Finetunesteps-edit + m_EditFineTuneSteps.SetWindowText(mpt::cfmt::val(m_pActiveTuning->GetFineStepCount())); + m_EditFineTuneSteps.Invalidate(); + + //Making sure that ratiomap window is showing and + //updating its content. + m_RatioMapWnd.ShowWindow(SW_SHOW); + m_RatioMapWnd.m_pTuning = m_pActiveTuning; + m_RatioMapWnd.Invalidate(); + UpdateRatioMapEdits(m_RatioMapWnd.GetShownCentre()); + + + const UNOTEINDEXTYPE period = m_pActiveTuning->GetGroupSize(); + const RATIOTYPE GroupRatio = m_pActiveTuning->GetGroupRatio(); + if(m_pActiveTuning->GetType() == Tuning::Type::GROUPGEOMETRIC || m_pActiveTuning->GetType() == Tuning::Type::GEOMETRIC) + { + m_EditSteps.EnableWindow(TRUE); + m_EditRatioPeriod.EnableWindow(TRUE); + m_EditSteps.SetWindowText(mpt::cfmt::val(period)); + m_EditRatioPeriod.SetWindowText(mpt::cfmt::flt(GroupRatio, 6)); + } else + { + m_EditSteps.EnableWindow(FALSE); + m_EditRatioPeriod.EnableWindow(FALSE); + m_EditSteps.SetWindowText(_T("")); + m_EditRatioPeriod.SetWindowText(_T("")); + } + + m_EditRatioPeriod.Invalidate(); + m_EditSteps.Invalidate(); + + bool enableControls = CanEdit(m_pActiveTuning, m_pActiveTuningCollection); + + m_CombobTuningType.EnableWindow(FALSE); + m_EditSteps.SetReadOnly(!enableControls); + m_EditRatioPeriod.SetReadOnly(!enableControls); + m_EditRatio.SetReadOnly((m_pActiveTuning->GetType() == Tuning::Type::GEOMETRIC) ? TRUE : !enableControls); + m_EditNotename.SetReadOnly(!enableControls); + m_EditMiscActions.SetReadOnly((m_pActiveTuning->GetType() == Tuning::Type::GEOMETRIC) ? TRUE : !enableControls); + m_EditFineTuneSteps.SetReadOnly(!enableControls); + m_EditName.SetReadOnly(!enableControls); + + m_ButtonSet.EnableWindow((m_pActiveTuning->GetType() == Tuning::Type::GEOMETRIC) ? FALSE : enableControls); + + m_CombobTuningType.Invalidate(); + m_EditSteps.Invalidate(); + m_EditRatioPeriod.Invalidate(); + } + else + { + if(m_pActiveTuning == NULL) //No active tuning, clearing tuning part. + { + m_EditName.SetWindowText(_T("")); + m_EditSteps.SetWindowText(_T("")); + m_EditRatioPeriod.SetWindowText(_T("")); + m_EditRatio.SetWindowText(_T("")); + m_EditNotename.SetWindowText(_T("")); + m_EditMiscActions.SetWindowText(_T("")); + m_EditFineTuneSteps.SetWindowText(_T("")); + m_EditName.SetWindowText(_T("")); + + m_CombobTuningType.SetCurSel(-1); + + m_RatioMapWnd.ShowWindow(SW_HIDE); + m_RatioMapWnd.m_pTuning = NULL; + m_RatioMapWnd.Invalidate(); + } + } + //<--Updating tuning part +} + + +void CTuningDialog::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_STATICRATIOMAP, m_RatioMapWnd); + DDX_Control(pDX, IDC_COMBO_TTYPE, m_CombobTuningType); + DDX_Control(pDX, IDC_EDIT_STEPS, m_EditSteps); + DDX_Control(pDX, IDC_EDIT_NOTENAME, m_EditNotename); + DDX_Control(pDX, IDC_BUTTON_SETVALUES, m_ButtonSet); + DDX_Control(pDX, IDC_BUTTON_TUNING_NEW, m_ButtonNew); + DDX_Control(pDX, IDC_BUTTON_IMPORT, m_ButtonImport); + DDX_Control(pDX, IDC_BUTTON_EXPORT, m_ButtonExport); + DDX_Control(pDX, IDC_BUTTON_TUNING_REMOVE, m_ButtonRemove); + DDX_Control(pDX, IDC_EDIT_MISC_ACTIONS, m_EditMiscActions); + DDX_Control(pDX, IDC_EDIT_FINETUNESTEPS, m_EditFineTuneSteps); + DDX_Control(pDX, IDC_EDIT_NAME, m_EditName); + DDX_Control(pDX, IDC_TREE_TUNING, m_TreeCtrlTuning); +} + + + +BEGIN_MESSAGE_MAP(CTuningDialog, CDialog) + ON_EN_CHANGE(IDC_EDIT_STEPS, &CTuningDialog::OnEnChangeEditSteps) + ON_EN_CHANGE(IDC_EDIT_RATIOPERIOD, &CTuningDialog::OnEnChangeEditRatioperiod) + ON_EN_CHANGE(IDC_EDIT_NOTENAME, &CTuningDialog::OnEnChangeEditNotename) + ON_BN_CLICKED(IDC_BUTTON_SETVALUES, &CTuningDialog::OnBnClickedButtonSetvalues) + ON_EN_CHANGE(IDC_EDIT_RATIOVALUE, &CTuningDialog::OnEnChangeEditRatiovalue) + ON_BN_CLICKED(IDC_BUTTON_TUNING_NEW, &CTuningDialog::OnBnClickedButtonNew) + ON_BN_CLICKED(IDC_BUTTON_IMPORT, &CTuningDialog::OnBnClickedButtonImport) + ON_BN_CLICKED(IDC_BUTTON_EXPORT, &CTuningDialog::OnBnClickedButtonExport) + ON_BN_CLICKED(IDC_BUTTON_TUNING_REMOVE, &CTuningDialog::OnBnClickedButtonRemove) + ON_EN_CHANGE(IDC_EDIT_FINETUNESTEPS, &CTuningDialog::OnEnChangeEditFinetunesteps) + ON_EN_KILLFOCUS(IDC_EDIT_FINETUNESTEPS, &CTuningDialog::OnEnKillfocusEditFinetunesteps) + ON_EN_KILLFOCUS(IDC_EDIT_NAME, &CTuningDialog::OnEnKillfocusEditName) + ON_EN_KILLFOCUS(IDC_EDIT_STEPS, &CTuningDialog::OnEnKillfocusEditSteps) + ON_EN_KILLFOCUS(IDC_EDIT_RATIOPERIOD, &CTuningDialog::OnEnKillfocusEditRatioperiod) + ON_EN_KILLFOCUS(IDC_EDIT_RATIOVALUE, &CTuningDialog::OnEnKillfocusEditRatiovalue) + ON_EN_KILLFOCUS(IDC_EDIT_NOTENAME, &CTuningDialog::OnEnKillfocusEditNotename) + ON_NOTIFY(TVN_SELCHANGED, IDC_TREE_TUNING, &CTuningDialog::OnTvnSelchangedTreeTuning) + ON_NOTIFY(TVN_DELETEITEM, IDC_TREE_TUNING, &CTuningDialog::OnTvnDeleteitemTreeTuning) + ON_NOTIFY(NM_RCLICK, IDC_TREE_TUNING, &CTuningDialog::OnNMRclickTreeTuning) + ON_NOTIFY(TVN_BEGINDRAG, IDC_TREE_TUNING, &CTuningDialog::OnTvnBegindragTreeTuning) + ON_COMMAND(ID_REMOVETUNING, &CTuningDialog::OnRemoveTuning) + ON_COMMAND(ID_ADDTUNINGGENERAL, &CTuningDialog::OnAddTuningGeneral) + ON_COMMAND(ID_ADDTUNINGGROUPGEOMETRIC, &CTuningDialog::OnAddTuningGroupGeometric) + ON_COMMAND(ID_ADDTUNINGGEOMETRIC, &CTuningDialog::OnAddTuningGeometric) + ON_COMMAND(ID_COPYTUNING, &CTuningDialog::OnCopyTuning) + ON_COMMAND(ID_REMOVETUNINGCOLLECTION, &CTuningDialog::OnRemoveTuningCollection) +END_MESSAGE_MAP() + + +void CTuningDialog::DoErrorExit() +{ + m_DoErrorExit = false; + m_pActiveTuning = NULL; + m_pActiveTuningCollection = NULL; + Reporting::Message(LogInformation, _T("Dialog encountered an error and needs to close"), this); + OnOK(); +} + + +// CTuningDialog message handlers + +void CTuningDialog::UpdateTuningType() +{ + if(m_pActiveTuning) + { + if(m_CombobTuningType.GetCount() < 3) m_DoErrorExit = true; + + if(m_pActiveTuning->GetType() == Tuning::Type::GEOMETRIC) + m_CombobTuningType.SetCurSel(2); + else + if(m_pActiveTuning->GetType() == Tuning::Type::GROUPGEOMETRIC) + m_CombobTuningType.SetCurSel(1); + else + m_CombobTuningType.SetCurSel(0); + } +} + + + +bool CTuningDialog::AddTuning(CTuningCollection* pTC, Tuning::Type type) +{ + if(!pTC) + { + Reporting::Notification("No tuning collection chosen"); + return false; + } + + std::unique_ptr<CTuning> pNewTuning; + if(type == Tuning::Type::GROUPGEOMETRIC) + { + std::vector<Tuning::RATIOTYPE> ratios; + for(Tuning::NOTEINDEXTYPE n = 0; n < 12; ++n) + { + ratios.push_back(std::pow(static_cast<Tuning::RATIOTYPE>(2.0), static_cast<Tuning::RATIOTYPE>(n) / static_cast<Tuning::RATIOTYPE>(12))); + } + pNewTuning = CTuning::CreateGroupGeometric(U_("Unnamed"), ratios, 2, 15); + } else if(type == Tuning::Type::GEOMETRIC) + { + pNewTuning = CTuning::CreateGeometric(U_("Unnamed"), 12, 2, 15); + } else + { + pNewTuning = CTuning::CreateGeneral(U_("Unnamed")); + } + + CTuning *pT = pTC->AddTuning(std::move(pNewTuning)); + if(!pT) + { + Reporting::Notification("Add tuning failed"); + return false; + } + AddTreeItem(pT, m_TreeItemTuningItemMap.GetMapping_21(TUNINGTREEITEM(pTC)), NULL); + m_pActiveTuning = pT; + m_ModifiedTCs[pTC] = true; + UpdateView(); + + return true; +} + + +void CTuningDialog::OnEnChangeEditSteps() +{ +} + +void CTuningDialog::OnEnChangeEditRatioperiod() +{ +} + + +void CTuningDialog::OnEnChangeEditNotename() +{ + + if(!m_NoteEditApply) + { + m_NoteEditApply = true; + return; + } + + if(!m_pActiveTuning) + return; + + const NOTEINDEXTYPE currentNote = m_RatioMapWnd.GetShownCentre(); + CString buffer; + m_EditNotename.GetWindowText(buffer); + mpt::ustring str = mpt::ToUnicode(buffer); + { + if(str.size() > 3) + str.resize(3); + m_pActiveTuning->SetNoteName(currentNote, str); + } + + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + m_RatioMapWnd.Invalidate(); + +} + +void CTuningDialog::OnEnChangeEditRatiovalue() +{ + + if(!m_RatioEditApply) + { + m_RatioEditApply = true; + return; + } + + if(!m_pActiveTuning) + return; + + const NOTEINDEXTYPE currentNote = m_RatioMapWnd.GetShownCentre(); + + double ratio = 0.0; + if(m_EditRatio.GetDecimalValue(ratio)) + { + m_pActiveTuning->SetRatio(currentNote, static_cast<RATIOTYPE>(ratio)); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + UpdateTuningType(); + m_RatioMapWnd.Invalidate(); + } + +} + +void CTuningDialog::OnBnClickedButtonSetvalues() +{ + if(m_pActiveTuning) + { + if(m_EditMiscActions.GetWindowTextLength() < 1) + return; + + CString buffer; + m_EditMiscActions.GetWindowText(buffer); + m_pActiveTuning->Multiply(ConvertStrTo<RATIOTYPE>(buffer)); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + m_EditMiscActions.SetWindowText(_T("")); + m_RatioMapWnd.Invalidate(); + } +} + +void CTuningDialog::UpdateRatioMapEdits(const NOTEINDEXTYPE& note) +{ + if(m_pActiveTuning == NULL) + return; + + m_RatioEditApply = false; + m_EditRatio.SetWindowText(mpt::cfmt::val(m_pActiveTuning->GetRatio(note))); + m_NoteEditApply = false; + m_EditNotename.SetWindowText(mpt::ToCString(m_pActiveTuning->GetNoteName(note, false))); + + m_EditRatio.Invalidate(); + m_EditNotename.Invalidate(); +} + + +void CTuningDialog::OnBnClickedButtonNew() +{ + POINT point; + GetCursorPos(&point); + + HMENU popUpMenu = CreatePopupMenu(); + if(popUpMenu == NULL) return; + + AppendMenu(popUpMenu, MF_STRING, ID_ADDTUNINGGROUPGEOMETRIC, _T("Add &GroupGeometric tuning")); + AppendMenu(popUpMenu, MF_STRING, ID_ADDTUNINGGEOMETRIC, _T("Add G&eometric tuning")); + AppendMenu(popUpMenu, MF_STRING, ID_ADDTUNINGGENERAL, _T("Add Ge&neral tuning")); + + m_CommandItemDest.Set(m_TuningCollections[0]); + + TrackPopupMenu(popUpMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, 0, m_hWnd, NULL); + DestroyMenu(popUpMenu); +} + + +void CTuningDialog::OnBnClickedButtonExport() +{ + + if(m_pActiveTuning == NULL && m_pActiveTuningCollection == NULL) + { + Reporting::Message(LogInformation, _T("Operation failed - No tuning file selected."), this); + return; + } + + bool failure = true; + + if(m_pActiveTuning) + { + + const CTuning* pT = m_pActiveTuning; + + std::string filter; + int filters = 0; + int tuningFilter = -1; + int sclFilter = -1; + { + filters++; + filter += std::string("Tuning files (*") + CTuning::s_FileExtension + std::string(")|*") + CTuning::s_FileExtension + std::string("|"); + tuningFilter = filters; + } + { + filters++; + filter += std::string("Scala scale (*.scl)|*") + std::string(".scl")+ std::string("|"); + sclFilter = filters; + } + + int filterIndex = 0; + FileDialog dlg = SaveFileDialog() + .DefaultExtension(CTuning::s_FileExtension) + .ExtensionFilter(filter) + .WorkingDirectory(TrackerSettings::Instance().PathTunings.GetWorkingDir()) + .FilterIndex(&filterIndex); + + if (!dlg.Show(this)) return; + + BeginWaitCursor(); + try + { + mpt::SafeOutputFile sfout(dlg.GetFirstFile(), std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &fout = sfout; + fout.exceptions(fout.exceptions() | std::ios::badbit | std::ios::failbit); + + if(tuningFilter != -1 && filterIndex == tuningFilter) + { + failure = (pT->Serialize(fout) != Tuning::SerializationResult::Success); + } else if(sclFilter != -1 && filterIndex == sclFilter) + { + failure = !pT->WriteSCL(fout, dlg.GetFirstFile()); + if(!failure) + { + if(m_pActiveTuning->GetType() == Tuning::Type::GENERAL) + { + Reporting::Message(LogWarning, _T("The Scala SCL file format does not contain enough information to represent General Tunings without data loss.\n\nOpenMPT exported as much information as possible, but other software as well as OpenMPT itself will not be able to re-import the just exported Scala SCL in a way that resembles the original data completely.\n\nPlease consider additionally exporting the Tuning as an OpenMPT .tun file."), _T("Tuning - Incompatible export"), this); + } + } + } + } catch(const std::exception &) + { + failure = true; + } + EndWaitCursor(); + + } else + { + + const CTuningCollection* pTC = m_pActiveTuningCollection; + + std::string filter = std::string("Multiple Tuning files (") + CTuning::s_FileExtension + std::string(")|*") + CTuning::s_FileExtension + std::string("|"); + + mpt::PathString fileName; + if(!m_TuningCollectionsFilenames[pTC].empty()) + { + fileName = m_TuningCollectionsFilenames[pTC] + P_(" - "); + } + if(!m_TuningCollectionsNames[pTC].IsEmpty()) + { + mpt::PathString name = mpt::PathString::FromUnicode(mpt::ToUnicode(m_TuningCollectionsNames[pTC])); + SanitizeFilename(name); + fileName += name + P_(" - "); + } + fileName += P_("%tuning_number% - %tuning_name%"); + + int filterIndex = 0; + FileDialog dlg = SaveFileDialog() + .DefaultExtension(CTuning::s_FileExtension) + .ExtensionFilter(filter) + .WorkingDirectory(TrackerSettings::Instance().PathTunings.GetWorkingDir()) + .FilterIndex(&filterIndex); + dlg.DefaultFilename(fileName); + + if (!dlg.Show(this)) return; + + BeginWaitCursor(); + + failure = false; + + auto numberFmt = mpt::FormatSpec().Dec().FillNul().Width(1 + static_cast<int>(std::log10(pTC->GetNumTunings()))); + + for(std::size_t i = 0; i < pTC->GetNumTunings(); ++i) + { + const CTuning & tuning = *(pTC->GetTuning(i)); + fileName = dlg.GetFirstFile(); + mpt::ustring tuningName = mpt::ToUnicode(tuning.GetName()); + if(tuningName.empty()) + { + tuningName = U_("untitled"); + } + mpt::ustring fileNameW = fileName.ToUnicode(); + mpt::ustring numberW = mpt::ufmt::fmt(i + 1, numberFmt); + SanitizeFilename(numberW); + fileNameW = mpt::String::Replace(fileNameW, U_("%tuning_number%"), numberW); + mpt::ustring nameW = mpt::ToUnicode(tuningName); + SanitizeFilename(nameW); + fileNameW = mpt::String::Replace(fileNameW, U_("%tuning_name%"), nameW); + fileName = mpt::PathString::FromUnicode(fileNameW); + + try + { + mpt::SafeOutputFile sfout(fileName, std::ios::binary, mpt::FlushModeFromBool(TrackerSettings::Instance().MiscFlushFileBuffersOnSave)); + mpt::ofstream &fout = sfout; + fout.exceptions(fout.exceptions() | std::ios::badbit | std::ios::failbit); + if(tuning.Serialize(fout) != Tuning::SerializationResult::Success) + { + failure = true; + } + } catch(const std::exception &) + { + failure = true; + } + } + + EndWaitCursor(); + + } + + if(failure) + { + Reporting::Message(LogError, _T("Export failed"), _T("Error!"), this); + } + +} + + +void CTuningDialog::OnBnClickedButtonRemove() +{ + if(m_pActiveTuning) + { + if(CanEdit(m_pActiveTuning, m_pActiveTuningCollection)) + { + m_CommandItemDest.Set(m_pActiveTuning); + OnRemoveTuning(); + } + } else if(m_pActiveTuningCollection) + { + if(IsDeletable(m_pActiveTuningCollection)) + { + m_CommandItemDest.Set(m_pActiveTuningCollection); + OnRemoveTuningCollection(); + } + } +} + + +template <typename Tfile, std::size_t N> static bool CheckMagic(Tfile &f, mpt::IO::Offset offset, const uint8(&magic)[N]) +{ + if(!mpt::IO::SeekAbsolute(f, offset)) + { + return false; + } + uint8 buffer[N]; + MemsetZero(buffer); + if(mpt::IO::ReadRaw(f, buffer, N).size() != N) + { + return false; + } + bool result = (std::memcmp(magic, buffer, N) == 0); + mpt::IO::SeekBegin(f); + return result; +} + + +void CTuningDialog::OnBnClickedButtonImport() +{ + std::string sFilter = MPT_AFORMAT("Tuning files (*{}, *{}, *.scl)|*{};*{};*.scl|")( + CTuning::s_FileExtension, + CTuningCollection::s_FileExtension, + CTuning::s_FileExtension, + CTuningCollection::s_FileExtension); + + FileDialog dlg = OpenFileDialog() + .AllowMultiSelect() + .ExtensionFilter(sFilter) + .WorkingDirectory(TrackerSettings::Instance().PathTunings.GetWorkingDir()); + if(!dlg.Show(this)) + return; + + TrackerSettings::Instance().PathTunings.SetWorkingDir(dlg.GetWorkingDirectory()); + + mpt::ustring sLoadReport; + + const auto &files = dlg.GetFilenames(); + for(const auto &file : files) + { + mpt::PathString fileName; + mpt::PathString fileExt; + file.SplitPath(nullptr, nullptr, &fileName, &fileExt); + const mpt::ustring fileNameExt = (fileName + fileExt).ToUnicode(); + + const bool bIsTun = (mpt::PathString::CompareNoCase(fileExt, mpt::PathString::FromUTF8(CTuning::s_FileExtension)) == 0); + const bool bIsScl = (mpt::PathString::CompareNoCase(fileExt, P_(".scl")) == 0); + //const bool bIsTc = (mpt::PathString::CompareNoCase(fileExt, mpt::PathString::FromUTF8(CTuningCollection::s_FileExtension)) == 0); + + mpt::ifstream fin(file, std::ios::binary); + + // "HSCT", 0x01, 0x00, 0x00, 0x00 + const uint8 magicTColdV1 [] = { 'H', 'S', 'C', 'T',0x01,0x00,0x00,0x00 }; + // "HSCT", 0x02, 0x00, 0x00, 0x00 + const uint8 magicTColdV2 [] = { 'H', 'S', 'C', 'T',0x02,0x00,0x00,0x00 }; + // "CTRTI_B.", 0x03, 0x00 + const uint8 magicTUNoldV2[] = { 'C', 'T', 'R', 'T', 'I', '_', 'B', '.',0x02,0x00 }; + // "CTRTI_B.", 0x03, 0x00 + const uint8 magicTUNoldV3[] = { 'C', 'T', 'R', 'T', 'I', '_', 'B', '.',0x03,0x00 }; + // "228", 0x02, "TC" + const uint8 magicTC [] = { '2', '2', '8',0x02, 'T', 'C' }; + // "228", 0x09, "CTB244RTI" + const uint8 magicTUN [] = { '2', '2', '8',0x09, 'C', 'T', 'B', '2', '4', '4', 'R', 'T', 'I' }; + + CTuningCollection *pTC = nullptr; + CString tcName; + mpt::PathString tcFilename; + std::unique_ptr<CTuning> pT; + + if(bIsTun && CheckMagic(fin, 0, magicTC)) + { + // OpenMPT since r3115 wrongly wrote .tc files instead of .tun files when exporting. + // If such a file is detected and only contains a single Tuning, we can work-around that. + // For .tc files containing multiple Tunings, we sadly cannot decide which one the user wanted. + // In that case, we import as a Collection (an alternative might be to display a dialog in this case). + pTC = new CTuningCollection(); + mpt::ustring name; + if(pTC->Deserialize(fin, name, TuningCharsetFallback) == Tuning::SerializationResult::Success) + { // success + if(pTC->GetNumTunings() == 1) + { + Reporting::Message(LogInformation, U_("- Tuning Collection with a Tuning file extension (.tun) detected. It only contains a single Tuning, importing the file as a Tuning.\n"), this); + pT = std::unique_ptr<CTuning>(new CTuning(*(pTC->GetTuning(0)))); + delete pTC; + pTC = nullptr; + // ok + } else + { + Reporting::Message(LogNotification, U_("- Tuning Collection with a Tuning file extension (.tun) detected. It only contains multiple Tunings, importing the file as a Tuning Collection.\n"), this); + // ok + } + } else + { + delete pTC; + pTC = nullptr; + // fail + } + + } else if(CheckMagic(fin, 0, magicTC) || CheckMagic(fin, 0, magicTColdV2) || CheckMagic(fin, 0, magicTColdV1)) + { + + pTC = new CTuningCollection(); + mpt::ustring name; + if(pTC->Deserialize(fin, name, TuningCharsetFallback) != Tuning::SerializationResult::Success) + { // failure + delete pTC; + pTC = nullptr; + // fail + } else + { + tcName = mpt::ToCString(name); + tcFilename = file; + // ok + } + + } else if(CheckMagic(fin, 0, magicTUNoldV3) || CheckMagic(fin, 0, magicTUNoldV2)) + { + + pT = CTuning::CreateDeserializeOLD(fin, TuningCharsetFallback); + + } else if(CheckMagic(fin, 0, magicTUN)) + { + + pT = CTuning::CreateDeserialize(fin, TuningCharsetFallback); + + } else if(bIsScl) + { + + EnSclImport a = ImportScl(file, fileName.ToUnicode(), pT); + if(a != enSclImportOk) + { // failure + pT = nullptr; + } + + } + + bool success = false; + + if(pT) + { + CTuningCollection &tc = *m_TuningCollections.front(); + CTuning *activeTuning = tc.AddTuning(std::move(pT)); + if(!activeTuning) + { + if(tc.GetNumTunings() >= CTuningCollection::s_nMaxTuningCount) + { + sLoadReport += MPT_UFORMAT("- Failed to load file \"{}\": maximum number({}) of temporary tunings is already open.\n")(fileNameExt, static_cast<std::size_t>(CTuningCollection::s_nMaxTuningCount)); + } else + { + sLoadReport += MPT_UFORMAT("- Unable to import file \"{}\": unknown reason.\n")(fileNameExt); + } + } else + { + m_pActiveTuning = activeTuning; + AddTreeItem(m_pActiveTuning, m_TreeItemTuningItemMap.GetMapping_21(TUNINGTREEITEM(&tc)), NULL); + success = true; + } + } + + if(pTC) + { + m_TuningCollections.push_back(pTC); + m_TuningCollectionsNames[pTC] = tcName; + m_TuningCollectionsFilenames[pTC] = tcFilename; + m_DeletableTuningCollections.push_back(pTC); + AddTreeItem(pTC, NULL, NULL); + success = true; + } + + if(!success) + { + sLoadReport += MPT_UFORMAT("- Unable to load \"{}\": unrecognized file format.\n")(fileNameExt); + } + } + + if(sLoadReport.length() > 0) + Reporting::Information(sLoadReport); + UpdateView(); +} + + +void CTuningDialog::OnEnChangeEditFinetunesteps() +{ +} + + +void CTuningDialog::OnEnKillfocusEditFinetunesteps() +{ + if(m_pActiveTuning) + { + CString buffer; + m_EditFineTuneSteps.GetWindowText(buffer); + m_pActiveTuning->SetFineStepCount(ConvertStrTo<Tuning::USTEPINDEXTYPE>(buffer)); + m_EditFineTuneSteps.SetWindowText(mpt::cfmt::val(m_pActiveTuning->GetFineStepCount())); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + m_EditFineTuneSteps.Invalidate(); + } +} + + +void CTuningDialog::OnEnKillfocusEditName() +{ + if(m_pActiveTuning != NULL) + { + CString buffer; + m_EditName.GetWindowText(buffer); + m_pActiveTuning->SetName(mpt::ToUnicode(buffer)); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + UpdateView(UM_TUNINGDATA); + UpdateView(UM_TUNINGCOLLECTION); + } +} + + +void CTuningDialog::OnEnKillfocusEditSteps() +{ + if(m_pActiveTuning) + { + CString buffer; + m_EditSteps.GetWindowText(buffer); + m_pActiveTuning->ChangeGroupsize(ConvertStrTo<UNOTEINDEXTYPE>(buffer)); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + UpdateView(UM_TUNINGDATA); + } +} + + +void CTuningDialog::OnEnKillfocusEditRatioperiod() +{ + double ratio = 0.0; + if(m_pActiveTuning && m_EditRatioPeriod.GetDecimalValue(ratio)) + { + m_pActiveTuning->ChangeGroupRatio(static_cast<RATIOTYPE>(ratio)); + m_ModifiedTCs[GetpTuningCollection(m_pActiveTuning)] = true; + UpdateView(UM_TUNINGDATA); + } +} + +void CTuningDialog::OnEnKillfocusEditRatiovalue() +{ + UpdateView(UM_TUNINGDATA); +} + + +void CTuningDialog::OnEnKillfocusEditNotename() +{ + UpdateView(UM_TUNINGDATA); +} + +bool CTuningDialog::GetModifiedStatus(const CTuningCollection* const pTc) const +{ + auto iter = m_ModifiedTCs.find(pTc); + if(iter != m_ModifiedTCs.end()) + return (*iter).second; + else + return false; + +} + +CTuningCollection* CTuningDialog::GetpTuningCollection(HTREEITEM ti) const +{ + //If treeitem is that of a tuningcollection, return address of + //that tuning collection. If treeitem is that of a tuning, return + //the owning tuningcollection + TUNINGTREEITEM tunItem = m_TreeItemTuningItemMap.GetMapping_12(ti); + CTuningCollection* pTC = tunItem.GetTC(); + if(pTC) + return pTC; + else + { + CTuning* pT = tunItem.GetT(); + return GetpTuningCollection(pT); + } +} + +CTuningCollection* CTuningDialog::GetpTuningCollection(const CTuning* const pT) const +{ + for(auto &tuningCol : m_TuningCollections) + { + for(const auto &tuning : *tuningCol) + { + if(pT == tuning.get()) + { + return tuningCol; + } + } + } + return NULL; +} + + +void CTuningDialog::OnTvnSelchangedTreeTuning(NMHDR *pNMHDR, LRESULT *pResult) +{ + //This methods gets called when selected item in the treeview + //changes. + + //TODO: This gets called before killfocus messages of edits, which + // can be a problem. + + LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR); + + TUNINGTREEITEM ti = m_TreeItemTuningItemMap.GetMapping_12(pNMTreeView->itemNew.hItem); + + if(ti) + { + int updateMask = UM_TUNINGDATA; + CTuningCollection* pPrevTuningCollection = m_pActiveTuningCollection; + CTuning* pT = ti.GetT(); + CTuningCollection* pTC = ti.GetTC(); + if(pTC) + { + m_pActiveTuningCollection = pTC; + ASSERT(pT == NULL); + m_pActiveTuning = NULL; + } + else + { + m_pActiveTuning = pT; + m_pActiveTuningCollection = GetpTuningCollection(m_pActiveTuning); + + } + if(m_pActiveTuningCollection != pPrevTuningCollection) updateMask |= UM_TUNINGCOLLECTION; + UpdateView(updateMask); + } + else + { + m_DoErrorExit = true; + } + + *pResult = 0; +} + +void CTuningDialog::OnTvnDeleteitemTreeTuning(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR); + *pResult = 0; + if(pNMTreeView->itemOld.mask & TVIF_HANDLE && pNMTreeView->itemOld.hItem) + { + m_TreeItemTuningItemMap.RemoveValue_1(pNMTreeView->itemOld.hItem); + } + else + m_DoErrorExit = true; +} + +void CTuningDialog::OnNMRclickTreeTuning(NMHDR *, LRESULT *pResult) +{ + *pResult = 0; + + HTREEITEM hItem; + POINT point, ptClient; + + GetCursorPos(&point); + ptClient = point; + m_TreeCtrlTuning.ScreenToClient(&ptClient); + hItem = m_TreeCtrlTuning.HitTest(ptClient, NULL); + if(hItem == NULL) + return; + + m_TreeCtrlTuning.Select(hItem, TVGN_CARET); + + TUNINGTREEITEM tunitem = m_TreeItemTuningItemMap.GetMapping_12(hItem); + + if(!tunitem) + { + m_DoErrorExit = true; + return; + } + + HMENU popUpMenu = CreatePopupMenu(); + if(popUpMenu == NULL) return; + + CTuning* pT = tunitem.GetT(); + CTuningCollection* pTC = tunitem.GetTC(); + + if(pT) //Creating context menu for tuning-item + { + pTC = GetpTuningCollection(pT); + if(pTC != NULL) + { + UINT mask = MF_STRING; + if(!CanEdit(pT, pTC)) + { + mask |= MF_GRAYED; + } + AppendMenu(popUpMenu, mask, ID_REMOVETUNING, _T("&Remove")); + + m_CommandItemDest.Set(pT); + } + } + else //Creating context menu for tuning collection item. + { + if(pTC != NULL) + { + UINT mask = MF_STRING; + + mask = MF_STRING; + if (!CanEdit(pTC)) + mask |= MF_GRAYED; + AppendMenu(popUpMenu, mask, ID_ADDTUNINGGROUPGEOMETRIC, _T("Add &GroupGeometric tuning")); + AppendMenu(popUpMenu, mask, ID_ADDTUNINGGEOMETRIC, _T("Add G&eometric tuning")); + AppendMenu(popUpMenu, mask, ID_ADDTUNINGGENERAL, _T("Add Ge&neral tuning")); + + mask = MF_STRING; + if(!IsDeletable(pTC)) + mask |= MF_GRAYED; + AppendMenu(popUpMenu, mask, ID_REMOVETUNINGCOLLECTION, _T("&Unload tuning collection")); + + m_CommandItemDest.Set(pTC); + } + } + + GetCursorPos(&point); + TrackPopupMenu(popUpMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, 0, m_hWnd, NULL); + DestroyMenu(popUpMenu); +} + +bool CTuningDialog::IsDeletable(const CTuningCollection* const pTC) const +{ + auto iter = find(m_DeletableTuningCollections.begin(), m_DeletableTuningCollections.end(), pTC); + if(iter != m_DeletableTuningCollections.end()) + return true; + else + return false; +} + + +void CTuningDialog::OnTvnBegindragTreeTuning(NMHDR *pNMHDR, LRESULT *pResult) +{ + LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR); + *pResult = 0; + + m_CommandItemDest.Reset(); + m_CommandItemSrc.Reset(); + if(pNMTreeView == NULL || pNMTreeView->itemNew.hItem == NULL) return; + TUNINGTREEITEM tunitem = m_TreeItemTuningItemMap.GetMapping_12(pNMTreeView->itemNew.hItem); + + if(tunitem.GetT() == NULL) + { + Reporting::Message(LogNotification, _T("For the time being Drag and Drop is only supported for tuning instances."), this); + return; + } + + SetCursor(CMainFrame::curDragging); + + m_TreeCtrlTuning.SetDragging(); + m_DragItem = m_TreeItemTuningItemMap.GetMapping_12(pNMTreeView->itemNew.hItem); + + m_TreeCtrlTuning.Select(pNMTreeView->itemNew.hItem, TVGN_CARET); +} + + +CTuningCollection *CTuningDialog::CanDrop(HTREEITEM dragDestItem) +{ + if(!m_DragItem) + return nullptr; + + TUNINGTREEITEM destTunItem = m_TreeItemTuningItemMap.GetMapping_12(dragDestItem); + if(!destTunItem) + return nullptr; + + CTuningCollection* pTCdest = nullptr; + CTuningCollection* pTCsrc = m_DragItem.GetTC(); + + if(pTCsrc == nullptr) + pTCsrc = GetpTuningCollection(m_DragItem.GetT()); + + if(pTCsrc == NULL) + { + ASSERT(false); + return nullptr; + } + + if(destTunItem.GetT()) //Item dragged on tuning + pTCdest = GetpTuningCollection(destTunItem.GetT()); + else //Item dragged on tuningcollecition + pTCdest = destTunItem.GetTC(); + + //For now, ignoring drags within a tuning collection. + if(pTCdest == pTCsrc) + return nullptr; + + return pTCdest; +} + + +void CTuningDialog::OnEndDrag(HTREEITEM dragDestItem) +{ + SetCursor(CMainFrame::curArrow); + m_TreeCtrlTuning.SetDragging(false); + if(!m_DragItem) + return; + + CTuningCollection* pTCdest = CanDrop(dragDestItem); + m_CommandItemSrc = m_DragItem; + m_DragItem.Reset(); + + if(!pTCdest) + return; + + CTuningCollection* pTCsrc = m_CommandItemSrc.GetTC(); + if(pTCsrc == nullptr) + pTCsrc = GetpTuningCollection(m_CommandItemSrc.GetT()); + + if(pTCdest) + { + UINT mask = MF_STRING; + HMENU popUpMenu = CreatePopupMenu(); + if(popUpMenu == NULL) return; + + POINT point; + GetCursorPos(&point); + + if(!CanEdit(pTCdest)) + { + mask |= MF_GRAYED; + } + AppendMenu(popUpMenu, mask, ID_COPYTUNING, _T("&Copy here")); + + GetCursorPos(&point); + TrackPopupMenu(popUpMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, point.x, point.y, 0, m_hWnd, NULL); + DestroyMenu(popUpMenu); + + m_CommandItemDest.Set(pTCdest); + } +} + +bool CTuningDialog::AddTuning(CTuningCollection* pTC, CTuning* pT) +{ + //Default: pT == NULL + + if(!pTC) + { + Reporting::Notification("No tuning collection chosen"); + return false; + } + + std::unique_ptr<CTuning> pNewTuning; + if(pT) + { + pNewTuning = std::unique_ptr<CTuning>(new CTuning(*pT)); + } else + { + Reporting::Notification("Add tuning failed"); + return false; + } + CTuning *pNewTuningTmp = pTC->AddTuning(std::move(pNewTuning)); + if(!pNewTuningTmp) + { + Reporting::Notification("Add tuning failed"); + return false; + } + AddTreeItem(pNewTuningTmp, m_TreeItemTuningItemMap.GetMapping_21(TUNINGTREEITEM(pTC)), NULL); + m_pActiveTuning = pNewTuningTmp; + m_ModifiedTCs[pTC] = true; + UpdateView(); + + return true; +} + +void CTuningDialog::OnAddTuningGeneral() +{ + if(!m_CommandItemDest.GetTC()) + { + m_CommandItemDest = s_notFoundItemTuning; + return; + } + + CTuningCollection* pTC = m_CommandItemDest.GetTC(); + m_CommandItemDest = s_notFoundItemTuning; + m_ModifiedTCs[pTC]; + AddTuning(pTC, Tuning::Type::GENERAL); +} + +void CTuningDialog::OnAddTuningGroupGeometric() +{ + if(!m_CommandItemDest.GetTC()) + { + m_CommandItemDest = s_notFoundItemTuning; + return; + } + + CTuningCollection* pTC = m_CommandItemDest.GetTC(); + m_CommandItemDest = s_notFoundItemTuning; + m_ModifiedTCs[pTC]; + AddTuning(pTC, Tuning::Type::GROUPGEOMETRIC); +} + +void CTuningDialog::OnAddTuningGeometric() +{ + if(!m_CommandItemDest.GetTC()) + { + m_CommandItemDest = s_notFoundItemTuning; + return; + } + + CTuningCollection* pTC = m_CommandItemDest.GetTC(); + m_CommandItemDest = s_notFoundItemTuning; + m_ModifiedTCs[pTC]; + AddTuning(pTC, Tuning::Type::GEOMETRIC); +} + +void CTuningDialog::OnRemoveTuning() +{ + CTuning* pT = m_CommandItemDest.GetT(); + if(m_CommandItemDest.GetT()) + { + CTuningCollection* pTC = GetpTuningCollection(pT); + if(pTC) + { + bool used = false; + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + if(m_sndFile.Instruments[i]->pTuning == pT) + { + used = true; + } + } + if(used) + { + CString s = _T("Tuning '") + mpt::ToCString(pT->GetName()) + _T("' is used by instruments. Remove anyway?"); + if(Reporting::Confirm(s, false, true) == cnfYes) + { + CriticalSection cs; + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + if(m_sndFile.Instruments[i]->pTuning == pT) + { + m_sndFile.Instruments[i]->SetTuning(nullptr); + } + } + pTC->Remove(pT); + cs.Leave(); + m_ModifiedTCs[pTC] = true; + DeleteTreeItem(pT); + UpdateView(); + } + } else + { + CString s = _T("Remove tuning '") + mpt::ToCString(pT->GetName()) + _T("'?"); + if(Reporting::Confirm(s) == cnfYes) + { + pTC->Remove(pT); + m_ModifiedTCs[pTC] = true; + DeleteTreeItem(pT); + UpdateView(); + } + } + } + } + + m_CommandItemDest = s_notFoundItemTuning; +} + + +void CTuningDialog::OnCopyTuning() +{ + CTuningCollection* pTC = m_CommandItemDest.GetTC(); + + if(!pTC) + return; + + m_CommandItemDest = s_notFoundItemTuning; + + CTuning* pT = m_CommandItemSrc.GetT(); + if(pT == nullptr) + { + return; + } + m_ModifiedTCs[pTC] = true; + AddTuning(pTC, pT); +} + +void CTuningDialog::OnRemoveTuningCollection() +{ + if(!m_pActiveTuningCollection) + return; + + if(!IsDeletable(m_pActiveTuningCollection)) + { + ASSERT(false); + return; + } + + auto iter = find(m_TuningCollections.begin(), m_TuningCollections.end(), m_pActiveTuningCollection); + if(iter == m_TuningCollections.end()) + { + ASSERT(false); + return; + } + auto DTCiter = find(m_DeletableTuningCollections.begin(), m_DeletableTuningCollections.end(), *iter); + CTuningCollection* deletableTC = m_pActiveTuningCollection; + //Note: Order matters in the following lines. + m_DeletableTuningCollections.erase(DTCiter); + m_TuningCollections.erase(iter); + DeleteTreeItem(m_pActiveTuningCollection); + m_TuningCollectionsNames.erase(deletableTC); + m_TuningCollectionsFilenames.erase(deletableTC); + delete deletableTC; deletableTC = 0; + + UpdateView(); +} + + +void CTuningDialog::OnOK() +{ + // Prevent return-key from closing the window. + if(GetKeyState(VK_RETURN) <= -127 && GetFocus() != GetDlgItem(IDOK)) + return; + else + CDialog::OnOK(); +} + + +//////////////////////////////////////////////////////// +//*************** +//CTuningTreeCtrl +//*************** +//////////////////////////////////////////////////////// + +BEGIN_MESSAGE_MAP(CTuningTreeCtrl, CTreeCtrl) + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONUP() +END_MESSAGE_MAP() + + +void CTuningTreeCtrl::OnMouseMove(UINT nFlags, CPoint point) +{ + if(IsDragging()) + { + HTREEITEM hItem = HitTest(point, nullptr); + SetCursor((hItem == NULL || m_rParentDialog.CanDrop(hItem) == nullptr) ? CMainFrame::curNoDrop2 : CMainFrame::curDragging); + } + + CTreeCtrl::OnMouseMove(nFlags, point); +} + + +void CTuningTreeCtrl::OnLButtonUp(UINT nFlags, CPoint point) +{ + if(IsDragging()) + { + HTREEITEM hItem = HitTest(point, nullptr); + m_rParentDialog.OnEndDrag(hItem); + + CTreeCtrl::OnLButtonUp(nFlags, point); + } +} + + +//////////////////////////////////////////////////////// +// +// scl import +// +//////////////////////////////////////////////////////// + +using SclFloat = double; + +CString CTuningDialog::GetSclImportFailureMsg(EnSclImport id) +{ + switch(id) + { + case enSclImportFailTooManyNotes: + return MPT_CFORMAT("OpenMPT supports importing scl-files with at most {} notes")(mpt::cfmt::val(s_nSclImportMaxNoteCount)); + + case enSclImportFailTooLargeNumDenomIntegers: + return _T("Invalid numerator or denominator"); + + case enSclImportFailZeroDenominator: + return _T("Zero denominator"); + + case enSclImportFailNegativeRatio: + return _T("Negative ratio"); + + case enSclImportFailUnableToOpenFile: + return _T("Unable to open file"); + + case enSclImportLineCountMismatch: + return _T("Note count error"); + + case enSclImportTuningCreationFailure: + return _T("Unknown tuning creation error"); + + case enSclImportAddTuningFailure: + return _T("Can't add tuning to tuning collection"); + + default: + return _T(""); + } +} + + +static void SkipCommentLines(std::istream& iStrm, std::string& str) +{ + std::string whitespace(" \t"); + while(std::getline(iStrm, str)) + { + auto start = str.find_first_not_of(whitespace); + // Lines starting with a ! are comments + if(start != std::string::npos && str[start] != '!') + return; + } +} + + +static inline SclFloat CentToRatio(const SclFloat& val) +{ + return pow(2.0, val / 1200.0); +} + + +CTuningDialog::EnSclImport CTuningDialog::ImportScl(const mpt::PathString &filename, const mpt::ustring &name, std::unique_ptr<CTuning> & result) +{ + MPT_ASSERT(result == nullptr); + result = nullptr; + mpt::ifstream iStrm(filename, std::ios::in | std::ios::binary); + if(!iStrm) + { + return enSclImportFailUnableToOpenFile; + } + return ImportScl(iStrm, name, result); +} + + +CTuningDialog::EnSclImport CTuningDialog::ImportScl(std::istream& iStrm, const mpt::ustring &name, std::unique_ptr<CTuning> & result) +{ + MPT_ASSERT(result == nullptr); + result = nullptr; + std::string str; + + std::string filename; + bool first = true; + std::string whitespace(" \t"); + while(std::getline(iStrm, str)) + { + auto start = str.find_first_not_of(whitespace); + // Lines starting with a ! are comments + if(start != std::string::npos && str[start] != '!') + break; + if(first) + { + filename = mpt::trim(str.substr(start + 1), std::string(" \t\r\n")); + } + first = false; + } + std::string description = mpt::trim(str, std::string(" \t\r\n")); + + SkipCommentLines(iStrm, str); + // str should now contain number of notes. + const size_t nNotes = 1 + ConvertStrTo<size_t>(str.c_str()); + if (nNotes - 1 > s_nSclImportMaxNoteCount) + return enSclImportFailTooManyNotes; + + std::vector<mpt::ustring> names; + std::vector<Tuning::RATIOTYPE> fRatios; + fRatios.reserve(nNotes); + fRatios.push_back(1); + + char buffer[128]; + MemsetZero(buffer); + + while (iStrm.getline(buffer, sizeof(buffer))) + { + LPSTR psz = buffer; + LPSTR const pEnd = psz + strlen(buffer); + + // Skip tabs and spaces. + while(psz != pEnd && (*psz == ' ' || *psz == '\t')) + psz++; + + // Skip empty lines, comment lines and non-text. + if (*psz == 0 || *psz == '!' || *psz < 32) + continue; + + char* pNonDigit = pEnd; + + // Check type of first non digit. This tells whether to read cent, ratio or plain number. + for (pNonDigit = psz; pNonDigit != pEnd; pNonDigit++) + { + if (isdigit(*pNonDigit) == 0) + break; + } + + if (*pNonDigit == '.') // Reading cents + { + SclFloat fCent = ConvertStrTo<SclFloat>(psz); + fRatios.push_back(static_cast<Tuning::RATIOTYPE>(CentToRatio(fCent))); + } + else if (*pNonDigit == '/') // Reading ratios + { + *pNonDigit = 0; // Replace '/' with null. + int64 nNum = ConvertStrTo<int64>(psz); + psz = pNonDigit + 1; + int64 nDenom = ConvertStrTo<int64>(psz); + + if (nNum > int32_max || nDenom > int32_max) + return enSclImportFailTooLargeNumDenomIntegers; + if (nDenom == 0) + return enSclImportFailZeroDenominator; + + fRatios.push_back(static_cast<Tuning::RATIOTYPE>((SclFloat)nNum / (SclFloat)nDenom)); + } + else // Plain numbers. + fRatios.push_back(static_cast<Tuning::RATIOTYPE>(ConvertStrTo<int32>(psz))); + + std::string remainder = psz; + remainder = mpt::trim(remainder, std::string("\r\n")); + if(remainder.find_first_of(" \t") != std::string::npos) + { + remainder = remainder.substr(remainder.find_first_of(" \t")); + } else + { + remainder = std::string(); + } + remainder = mpt::trim(remainder, std::string(" \t")); + if(!remainder.empty()) + { + if(remainder[0] == '!') + { + remainder = remainder.substr(1); + remainder = mpt::trim(remainder, std::string(" \t")); + } + } + if(mpt::ToLowerCaseAscii(remainder) == "cents" || mpt::ToLowerCaseAscii(remainder) == "cent") + { + remainder = std::string(); + } + names.push_back(mpt::ToUnicode(mpt::Charset::ISO8859_1, remainder)); + + } + + if (nNotes != fRatios.size()) + return enSclImportLineCountMismatch; + + for(size_t i = 0; i < fRatios.size(); i++) + { + if (fRatios[i] < 0) + return enSclImportFailNegativeRatio; + } + + Tuning::RATIOTYPE groupRatio = fRatios.back(); + fRatios.pop_back(); + + mpt::ustring tuningName; + if(!description.empty()) + { + tuningName = mpt::ToUnicode(mpt::Charset::ISO8859_1, description); + } else if(!filename.empty()) + { + tuningName = mpt::ToUnicode(mpt::Charset::ISO8859_1, filename); + } else if(!name.empty()) + { + tuningName = name; + } else + { + tuningName = MPT_UFORMAT("{} notes: {}:{}")(nNotes - 1, mpt::ufmt::fix(groupRatio), 1); + } + + std::unique_ptr<CTuning> pT = CTuning::CreateGroupGeometric(tuningName, fRatios, groupRatio, 15); + if(!pT) + { + return enSclImportTuningCreationFailure; + } + + bool allNamesEmpty = true; + bool allNamesValid = true; + for(NOTEINDEXTYPE note = 0; note < mpt::saturate_cast<NOTEINDEXTYPE>(names.size()); ++note) + { + if(names[note].empty()) + { + allNamesValid = false; + } else + { + allNamesEmpty = false; + } + } + + if(nNotes - 1 == 12 && !allNamesValid) + { + for(NOTEINDEXTYPE note = 0; note < mpt::saturate_cast<NOTEINDEXTYPE>(names.size()); ++note) + { + pT->SetNoteName(note, mpt::ustring(CSoundFile::GetDefaultNoteNames()[note])); + } + } else + { + for(NOTEINDEXTYPE note = 0; note < mpt::saturate_cast<NOTEINDEXTYPE>(names.size()); ++note) + { + if(!names[note].empty()) + { + pT->SetNoteName(note, names[(note - 1 + names.size()) % names.size()]); + } + } + } + + result = std::move(pT); + + return enSclImportOk; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.h new file mode 100644 index 00000000..b8390853 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/TuningDialog.h @@ -0,0 +1,388 @@ +/* + * TuningDialog.h + * -------------- + * Purpose: Alternative sample tuning configuration dialog. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "tuningRatioMapWnd.h" +#include "tuningcollection.h" +#include <vector> +#include <string> +#include "resource.h" +#include "CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + + +// Tunings exist even outside of CSoundFile objects. We thus cannot use the +// GetCharsetInternal() encoding consistently. For now, just always treat +// tuning strings as Charset::Locale. As of OpenMPT 1.27, this distinction does +// not yet matter, because GetCharsetInteral() is always mpt::Charset::Locale if +// MODPLUG_TRACKER anyway. +extern const mpt::Charset TuningCharsetFallback; + +template<class T1, class T2> +class CBijectiveMap +{ +public: + CBijectiveMap(const T1& a, const T2& b) + : m_NotFoundT1(a), + m_NotFoundT2(b) + {} + + void AddPair(const T1& a, const T2& b) + { + m_T1.push_back(a); + m_T2.push_back(b); + } + + void ClearMapping() + { + m_T1.clear(); + m_T2.clear(); + } + + size_t Size() const + { + ASSERT(m_T1.size() == m_T2.size()); + return m_T1.size(); + } + + void RemoveValue_1(const T1& a) + { + auto iter = find(m_T1.begin(), m_T1.end(), a); + if(iter != m_T1.end()) + { + m_T2.erase(m_T2.begin() + (iter-m_T1.begin())); + m_T1.erase(iter); + } + } + + void RemoveValue_2(const T2& b) + { + auto iter = find(m_T2.begin(), m_T2.end(), b); + if(iter != m_T2.end()) + { + m_T1.erase(m_T1.begin() + (iter-m_T2.begin())); + m_T2.erase(iter); + } + } + + T2 GetMapping_12(const T1& a) const + { + auto iter = find(m_T1.begin(), m_T1.end(), a); + if(iter != m_T1.end()) + { + return m_T2[iter-m_T1.begin()]; + } + else + return m_NotFoundT2; + } + + T1 GetMapping_21(const T2& b) const + { + auto iter = find(m_T2.begin(), m_T2.end(), b); + if(iter != m_T2.end()) + { + return m_T1[iter-m_T2.begin()]; + } + else + return m_NotFoundT1; + } + +private: + //Elements are collected to two arrays so that elements with the + //same index are mapped to each other. + std::vector<T1> m_T1; + std::vector<T2> m_T2; + + T1 m_NotFoundT1; + T2 m_NotFoundT2; +}; + +class CTuningDialog; + +class CTuningTreeCtrl : public CTreeCtrl +{ +private: + CTuningDialog& m_rParentDialog; + bool m_Dragging; + +public: + CTuningTreeCtrl(CTuningDialog* parent) + : m_rParentDialog(*parent) + , m_Dragging(false) + {} + //Note: Parent address may be given in its initializer list. + + void SetDragging(bool state = true) {m_Dragging = state;} + bool IsDragging() {return m_Dragging;} + + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + DECLARE_MESSAGE_MAP() +}; + +class CTuningTreeItem +{ +private: + CTuning* m_pTuning; + CTuningCollection* m_pTuningCollection; + +public: + CTuningTreeItem() : m_pTuning(NULL), + m_pTuningCollection(NULL) + {} + + CTuningTreeItem(CTuning* pT) : + m_pTuning(pT), + m_pTuningCollection(NULL) + {} + + CTuningTreeItem(CTuningCollection* pTC) : + m_pTuning(NULL), + m_pTuningCollection(pTC) + {} + + bool operator==(const CTuningTreeItem& ti) const + { + if(m_pTuning == ti.m_pTuning && + m_pTuningCollection == ti.m_pTuningCollection) + return true; + else + return false; + } + + void Reset() {m_pTuning = NULL; m_pTuningCollection = NULL;} + + + void Set(CTuning* pT) + { + m_pTuning = pT; + m_pTuningCollection = NULL; + } + + void Set(CTuningCollection* pTC) + { + m_pTuning = NULL; + m_pTuningCollection = pTC; + } + + operator bool () const + { + return m_pTuning || m_pTuningCollection; + } + bool operator ! () const + { + return !operator bool(); + } + + CTuningCollection* GetTC() {return m_pTuningCollection;} + + CTuning* GetT() {return m_pTuning;} +}; + +// CTuningDialog dialog + +class CTuningDialog : public CDialog +{ + friend class CTuningTreeCtrl; + + enum EnSclImport + { + enSclImportOk, + enSclImportFailTooLargeNumDenomIntegers, + enSclImportFailZeroDenominator, + enSclImportFailNegativeRatio, + enSclImportFailUnableToOpenFile, + enSclImportLineCountMismatch, + enSclImportTuningCreationFailure, + enSclImportAddTuningFailure, + enSclImportFailTooManyNotes + }; + +public: + using TUNINGVECTOR = std::vector<CTuningCollection*>; + +public: + CTuningDialog(CWnd* pParent, INSTRUMENTINDEX inst, CSoundFile &csf); + virtual ~CTuningDialog(); + + BOOL OnInitDialog(); + + void UpdateRatioMapEdits(const Tuning::NOTEINDEXTYPE&); + + bool GetModifiedStatus(const CTuningCollection* const pTc) const; + +// Dialog Data + enum { IDD = IDD_TUNING }; + +protected: + virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV support + +private: + + bool CanEdit(CTuningCollection * pTC) const; + bool CanEdit(CTuning * pT, CTuningCollection * pTC) const; + + void UpdateView(const int UpdateMask = 0); + void UpdateTuningType(); + + HTREEITEM AddTreeItem(CTuningCollection* pTC, HTREEITEM parent, HTREEITEM insertAfter); + HTREEITEM AddTreeItem(CTuning* pT, HTREEITEM parent, HTREEITEM insertAfter); + + void DeleteTreeItem(CTuning* pT); + void DeleteTreeItem(CTuningCollection* pTC); + + // Check if item can be dropped here. If yes, the target collection is returned, otherwise nullptr. + CTuningCollection *CanDrop(HTREEITEM dragDestItem); + void OnEndDrag(HTREEITEM dragDestItem); + + //Returns pointer to the tuning collection where tuning given as argument + //belongs to. + CTuningCollection* GetpTuningCollection(const CTuning* const) const; + + //Returns the address of corresponding tuningcollection; if it points + //to tuning-entry, returning the owning tuningcollection + CTuningCollection* GetpTuningCollection(HTREEITEM ti) const; + + //Checks whether tuning collection can be deleted. + bool IsDeletable(const CTuningCollection* const pTC) const; + + // Scl-file import. + EnSclImport ImportScl(const mpt::PathString &filename, const mpt::ustring &name, std::unique_ptr<CTuning> & result); + EnSclImport ImportScl(std::istream& iStrm, const mpt::ustring &name, std::unique_ptr<CTuning> & result); + + +private: + + CSoundFile & m_sndFile; + + CTuningRatioMapWnd m_RatioMapWnd; + TUNINGVECTOR m_TuningCollections; + std::vector<CTuningCollection*> m_DeletableTuningCollections; + + std::map<const CTuningCollection*, CString> m_TuningCollectionsNames; + std::map<const CTuningCollection*, mpt::PathString> m_TuningCollectionsFilenames; + + CTuning* m_pActiveTuning; + CTuningCollection* m_pActiveTuningCollection; + + CComboBox m_CombobTuningType; + + //Tuning Edits--> + CEdit m_EditSteps; + CNumberEdit m_EditRatioPeriod; + CNumberEdit m_EditRatio; + CEdit m_EditNotename; + CEdit m_EditMiscActions; + CEdit m_EditFineTuneSteps; + CEdit m_EditName; + //<--Tuning Edits + + CButton m_ButtonSet; + CButton m_ButtonNew; + CButton m_ButtonExport; + CButton m_ButtonImport; + CButton m_ButtonRemove; + + CTuningTreeCtrl m_TreeCtrlTuning; + +private: + using TUNINGTREEITEM = CTuningTreeItem; + using TREETUNING_MAP = CBijectiveMap<HTREEITEM, TUNINGTREEITEM>; + TREETUNING_MAP m_TreeItemTuningItemMap; + + TUNINGTREEITEM m_DragItem; + TUNINGTREEITEM m_CommandItemSrc; + TUNINGTREEITEM m_CommandItemDest; + //Commanditem is used when receiving context menu-commands, + //m_CommandItemDest is used when the command really need only + //one argument. + + using MODIFIED_MAP = std::map<const CTuningCollection* const, bool>; + MODIFIED_MAP m_ModifiedTCs; + //If tuning collection seems to have been modified, its address + //is added to this map. + + enum + { + TT_TUNINGCOLLECTION = 1, + TT_TUNING + }; + + static CString GetSclImportFailureMsg(EnSclImport); + static constexpr size_t s_nSclImportMaxNoteCount = 256; + + //To indicate whether to apply changes made to + //those edit boxes(they are modified by certain activities + //in case which the modifications should not be applied to + //tuning data. + bool m_NoteEditApply; + bool m_RatioEditApply; + + enum + { + UM_TUNINGDATA = 1, //UM <-> Update Mask + UM_TUNINGCOLLECTION = 2, + }; + + static const TUNINGTREEITEM s_notFoundItemTuning; + static const HTREEITEM s_notFoundItemTree; + + bool AddTuning(CTuningCollection*, CTuning* pT); + bool AddTuning(CTuningCollection*, Tuning::Type type); + + //Flag to prevent multiple exit error-messages. + bool m_DoErrorExit; + + void DoErrorExit(); + + virtual void OnOK(); + +//Treectrl context menu functions. +public: + afx_msg void OnRemoveTuning(); + afx_msg void OnAddTuningGeneral(); + afx_msg void OnAddTuningGroupGeometric(); + afx_msg void OnAddTuningGeometric(); + afx_msg void OnCopyTuning(); + afx_msg void OnRemoveTuningCollection(); + +//Event-functions +public: + afx_msg void OnEnChangeEditSteps(); + afx_msg void OnEnChangeEditRatioperiod(); + afx_msg void OnEnChangeEditNotename(); + afx_msg void OnBnClickedButtonSetvalues(); + afx_msg void OnEnChangeEditRatiovalue(); + afx_msg void OnBnClickedButtonNew(); + afx_msg void OnBnClickedButtonExport(); + afx_msg void OnBnClickedButtonImport(); + afx_msg void OnBnClickedButtonRemove(); + afx_msg void OnEnChangeEditFinetunesteps(); + afx_msg void OnEnKillfocusEditFinetunesteps(); + afx_msg void OnEnKillfocusEditName(); + afx_msg void OnEnKillfocusEditSteps(); + afx_msg void OnEnKillfocusEditRatioperiod(); + afx_msg void OnEnKillfocusEditRatiovalue(); + afx_msg void OnEnKillfocusEditNotename(); + + //Treeview events + afx_msg void OnTvnSelchangedTreeTuning(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnTvnDeleteitemTreeTuning(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnNMRclickTreeTuning(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnTvnBegindragTreeTuning(NMHDR *pNMHDR, LRESULT *pResult); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END
\ No newline at end of file diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Undo.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Undo.cpp new file mode 100644 index 00000000..4f1ec01c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Undo.cpp @@ -0,0 +1,937 @@ +/* + * Undo.cpp + * -------- + * Purpose: Editor undo buffer functionality. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Moddoc.h" +#include "Mainfrm.h" +#include "Undo.h" +#include "../common/mptStringBuffer.h" +#include "../tracklib/SampleEdit.h" +#include "../soundlib/modsmp_ctrl.h" + + +OPENMPT_NAMESPACE_BEGIN + + +///////////////////////////////////////////////////////////////////////////////////////// +// Pattern Undo Functions + + +// Remove all undo steps. +void CPatternUndo::ClearUndo() +{ + UndoBuffer.clear(); + RedoBuffer.clear(); +} + + +// Create undo point. +// Parameter list: +// - pattern: Pattern of which an undo step should be created from. +// - firstChn: first channel, 0-based. +// - firstRow: first row, 0-based. +// - numChns: width +// - numRows: height +// - description: Short description text of action for undo menu. +// - linkToPrevious: Don't create a separate undo step, but link this to the previous undo event. Use this for commands that modify several patterns at once. +// - storeChannelInfo: Also store current channel header information (pan / volume / etc. settings) and number of channels in this undo point. +bool CPatternUndo::PrepareUndo(PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) +{ + if(PrepareBuffer(UndoBuffer, pattern, firstChn, firstRow, numChns, numRows, description, linkToPrevious, storeChannelInfo)) + { + RedoBuffer.clear(); + return true; + } + return false; +} + + +bool CPatternUndo::PrepareChannelUndo(CHANNELINDEX firstChn, CHANNELINDEX numChns, const char *description) +{ + return PrepareUndo(PATTERNINDEX_INVALID, firstChn, 0, numChns, 0, description, false, true); +} + + +bool CPatternUndo::PrepareBuffer(undobuf_t &buffer, PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) const +{ + const CSoundFile &sndFile = modDoc.GetSoundFile(); + const bool onlyChannelInfo = storeChannelInfo && numRows < 1; + + if(storeChannelInfo && pattern != PATTERNINDEX_INVALID && firstChn == 0 && numChns != sndFile.GetNumChannels()) + { + numChns = sndFile.GetNumChannels(); + } + + ROWINDEX patRows = 0; + if(sndFile.Patterns.IsValidPat(pattern)) + { + patRows = sndFile.Patterns[pattern].GetNumRows(); + if((firstRow >= patRows) || (firstChn >= sndFile.GetNumChannels())) + return false; + if(numChns < 1 || numRows < 1) + return false; + if(firstRow + numRows >= patRows) + numRows = patRows - firstRow; + if(firstChn + numChns >= sndFile.GetNumChannels()) + numChns = sndFile.GetNumChannels() - firstChn; + } else if(!onlyChannelInfo) + { + return false; + } + + // Remove an undo step if there are too many. + if(buffer.size() >= MAX_UNDO_LEVEL) + { + buffer.erase(buffer.begin(), buffer.begin() + (buffer.size() - MAX_UNDO_LEVEL + 1)); + } + + UndoInfo undo; + undo.pattern = pattern; + undo.numPatternRows = patRows; + undo.firstChannel = firstChn; + undo.firstRow = firstRow; + undo.numChannels = numChns; + undo.numRows = numRows; + undo.linkToPrevious = linkToPrevious; + undo.description = description; + + if(!onlyChannelInfo) + { + try + { + undo.content.resize(numRows * numChns); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } + const ModCommand *pPattern = sndFile.Patterns[pattern].GetpModCommand(firstRow, firstChn); + auto pUndoData = undo.content.begin(); + for(ROWINDEX iy = 0; iy < numRows; iy++) + { + std::copy(pPattern, pPattern + numChns, pUndoData); + pUndoData += numChns; + pPattern += sndFile.GetNumChannels(); + } + } + + if(storeChannelInfo) + { + undo.channelInfo.assign(std::begin(sndFile.ChnSettings) + firstChn, std::begin(sndFile.ChnSettings) + firstChn + numChns); + } + + buffer.push_back(std::move(undo)); + + if(!linkToPrevious) + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + return true; +} + + +// Restore an undo point. Returns which pattern has been modified. +PATTERNINDEX CPatternUndo::Undo() +{ + return Undo(UndoBuffer, RedoBuffer, false); +} + + +// Restore an undo point. Returns which pattern has been modified. +PATTERNINDEX CPatternUndo::Redo() +{ + return Undo(RedoBuffer, UndoBuffer, false); +} + + +// Restore an undo point. Returns which pattern has been modified. +// linkedFromPrevious is true if a connected undo event is going to be deleted (can only be called internally). +PATTERNINDEX CPatternUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, bool linkedFromPrevious) +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + + bool linkToPrevious = false; + + if(fromBuf.empty()) + return PATTERNINDEX_INVALID; + + // Select most recent undo slot + const UndoInfo &undo = fromBuf.back(); + const bool onlyChannelSettings = undo.OnlyChannelSettings(); + const bool deletePattern = (undo.numPatternRows == DELETE_PATTERN) && !onlyChannelSettings; + + // Add this action to redo buffer if the pattern exists; otherwise add a special deletion redo step later + const bool patternExists = sndFile.Patterns.IsValidPat(undo.pattern); + if(patternExists || onlyChannelSettings) + PrepareBuffer(toBuf, undo.pattern, undo.firstChannel, undo.firstRow, undo.numChannels, undo.numRows, undo.description, linkedFromPrevious, !undo.channelInfo.empty()); + + const bool modifyChannels = !undo.channelInfo.empty(); + const CHANNELINDEX updateChannel = (undo.numChannels == 1) ? undo.firstChannel : CHANNELINDEX_INVALID; + if(modifyChannels) + { + const bool modifyChannelCount = + (undo.pattern != PATTERNINDEX_INVALID && undo.channelInfo.size() != sndFile.GetNumChannels()) + || (undo.pattern == PATTERNINDEX_INVALID && (undo.firstChannel + undo.channelInfo.size()) > sndFile.GetNumChannels()); + if(modifyChannelCount) + { + // Add or remove channels + std::vector<CHANNELINDEX> channels(undo.channelInfo.size(), CHANNELINDEX_INVALID); + const CHANNELINDEX copyCount = std::min(sndFile.GetNumChannels(), static_cast<CHANNELINDEX>(undo.channelInfo.size())); + std::iota(channels.begin(), channels.begin() + copyCount, CHANNELINDEX(0)); + modDoc.ReArrangeChannels(channels, false); + } + if(undo.firstChannel + undo.channelInfo.size() <= sndFile.GetNumChannels()) + { + std::move(undo.channelInfo.cbegin(), undo.channelInfo.cend(), std::begin(sndFile.ChnSettings) + undo.firstChannel); + } + + // Channel mute status might have changed... + for(CHANNELINDEX i = undo.firstChannel; i < sndFile.GetNumChannels(); i++) + { + modDoc.UpdateChannelMuteStatus(i); + } + } + + PATTERNINDEX pat = undo.pattern; + if(deletePattern) + { + sndFile.Patterns.Remove(pat); + } else if(undo.firstChannel + undo.numChannels <= sndFile.GetNumChannels() && !onlyChannelSettings) + { + if(!patternExists) + { + if(!sndFile.Patterns.Insert(pat, undo.numPatternRows)) + { + fromBuf.pop_back(); + return PATTERNINDEX_INVALID; + } + } else if(sndFile.Patterns[pat].GetNumRows() != undo.numPatternRows) + { + sndFile.Patterns[pat].Resize(undo.numPatternRows); + } + + linkToPrevious = undo.linkToPrevious; + auto pUndoData = undo.content.cbegin(); + CPattern &pattern = sndFile.Patterns[pat]; + ModCommand *m = pattern.GetpModCommand(undo.firstRow, undo.firstChannel); + const ROWINDEX numRows = std::min(undo.numRows, pattern.GetNumRows()); + for(ROWINDEX iy = 0; iy < numRows; iy++) + { + std::move(pUndoData, pUndoData + undo.numChannels, m); + m += sndFile.GetNumChannels(); + pUndoData += undo.numChannels; + } + } + + if(!patternExists && !onlyChannelSettings) + { + // Redo a deletion + auto &redo = fromBuf.back(); + redo.content.clear(); + redo.numPatternRows = DELETE_PATTERN; + toBuf.push_back(std::move(redo)); + } + fromBuf.pop_back(); + + if(patternExists != sndFile.Patterns.IsValidPat(pat)) + { + modDoc.UpdateAllViews(nullptr, PatternHint(pat).Names().Undo()); + modDoc.UpdateAllViews(nullptr, SequenceHint().Data()); // Pattern color will change in sequence + } else + { + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + } + if(modifyChannels) + modDoc.UpdateAllViews(nullptr, GeneralHint(updateChannel).Channels()); + modDoc.SetModified(); + + if(linkToPrevious) + { + pat = Undo(fromBuf, toBuf, true); + } + + return pat; +} + + +// Public helper function to remove the most recent undo point. +void CPatternUndo::RemoveLastUndoStep() +{ + if(UndoBuffer.empty()) + return; + + UndoBuffer.pop_back(); + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); +} + + +CString CPatternUndo::GetName(const undobuf_t &buffer) +{ + if(buffer.empty()) + return CString(); + + const UndoInfo &info = buffer.back(); + CString desc = mpt::ToCString(mpt::Charset::Locale, info.description); + if(info.linkToPrevious) + desc += _T(" (Multiple Patterns)"); + else if(info.OnlyChannelSettings() && info.numChannels > 1) + desc += _T(" (Multiple Channels)"); + else if(info.OnlyChannelSettings()) + desc += MPT_CFORMAT(" (Channel {})")(info.firstChannel + 1); + else + desc += MPT_CFORMAT(" (Pat {} Row {} Chn {})")(info.pattern, info.firstRow, info.firstChannel + 1); + return desc; +} + + +void CPatternUndo::RearrangePatterns(undobuf_t &buffer, const std::vector<PATTERNINDEX> &newIndex) +{ + for(auto &step : buffer) + { + if(step.pattern < newIndex.size()) + step.pattern = newIndex[step.pattern]; + } +} + + +void CPatternUndo::RearrangePatterns(const std::vector<PATTERNINDEX> &newIndex) +{ + RearrangePatterns(UndoBuffer, newIndex); + RearrangePatterns(RedoBuffer, newIndex); +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// Sample Undo Functions + + +// Remove all undo steps for all samples. +void CSampleUndo::ClearUndo() +{ + for(SAMPLEINDEX smp = 1; smp <= MAX_SAMPLES; smp++) + { + ClearUndo(UndoBuffer, smp); + ClearUndo(RedoBuffer, smp); + } + UndoBuffer.clear(); + RedoBuffer.clear(); +} + + +// Remove all undo steps of a given sample. +void CSampleUndo::ClearUndo(undobuf_t &buffer, const SAMPLEINDEX smp) +{ + if(!SampleBufferExists(buffer, smp)) return; + + while(!buffer[smp - 1].empty()) + { + DeleteStep(buffer, smp, 0); + } +} + + +// Create undo point for given sample. +// The main program has to tell what kind of changes are going to be made to the sample. +// That way, a lot of RAM can be saved, because some actions don't even require an undo sample buffer. +bool CSampleUndo::PrepareUndo(const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd) +{ + if(PrepareBuffer(UndoBuffer, smp, changeType, description, changeStart, changeEnd)) + { + ClearUndo(RedoBuffer, smp); + return true; + } + return false; +} + + +bool CSampleUndo::PrepareBuffer(undobuf_t &buffer, const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd) +{ + if(smp == 0 || smp >= MAX_SAMPLES) return false; + if(!TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) + { + // Undo/Redo is disabled + return false; + } + if(smp > buffer.size()) + { + buffer.resize(smp); + } + + // Remove an undo step if there are too many. + while(buffer[smp - 1].size() >= MAX_UNDO_LEVEL) + { + DeleteStep(buffer, smp, 0); + } + + // Create new undo slot + UndoInfo undo; + + const CSoundFile &sndFile = modDoc.GetSoundFile(); + const ModSample &oldSample = sndFile.GetSample(smp); + + // Save old sample header + undo.OldSample = oldSample; + undo.oldName = sndFile.m_szNames[smp]; + undo.changeType = changeType; + undo.description = description; + + if(changeType == sundo_replace) + { + // ensure that size information is correct here. + changeStart = 0; + changeEnd = oldSample.nLength; + } else if(changeType == sundo_none) + { + // we do nothing... + changeStart = changeEnd = 0; + } + + if(changeStart > oldSample.nLength || changeStart > changeEnd) + { + // Something is surely screwed up. + MPT_ASSERT(false); + return false; + } + + // Restrict amount of memory that's being used + RestrictBufferSize(); + + undo.changeStart = changeStart; + undo.changeEnd = changeEnd; + undo.samplePtr = nullptr; + + switch(changeType) + { + case sundo_none: // we are done, no sample changes here. + case sundo_invert: // no action necessary, since those effects can be applied again to be undone. + case sundo_reverse: // ditto + case sundo_unsign: // ditto + case sundo_insert: // no action necessary, we already have stored the variables that are necessary. + break; + + case sundo_update: + case sundo_delete: + case sundo_replace: + if(oldSample.HasSampleData()) + { + const uint8 bytesPerSample = oldSample.GetBytesPerSample(); + const SmpLength changeLen = changeEnd - changeStart; + + undo.samplePtr = ModSample::AllocateSample(changeLen, bytesPerSample); + if(undo.samplePtr == nullptr) return false; + memcpy(undo.samplePtr, oldSample.sampleb() + changeStart * bytesPerSample, changeLen * bytesPerSample); + +#ifdef MPT_ALL_LOGGING + const size_t nSize = (GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer) + changeLen * bytesPerSample) >> 10; + MPT_LOG_GLOBAL(LogDebug, "Undo", MPT_UFORMAT("Sample undo/redo buffer size is now {}.{} MB")(nSize >> 10, (nSize & 1023) * 100 / 1024)); +#endif + + } + break; + + default: + MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here! + return false; + } + + buffer[smp - 1].push_back(std::move(undo)); + + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + + return true; +} + + +// Restore undo point for given sample +bool CSampleUndo::Undo(const SAMPLEINDEX smp) +{ + return Undo(UndoBuffer, RedoBuffer, smp); +} + + +// Restore redo point for given sample +bool CSampleUndo::Redo(const SAMPLEINDEX smp) +{ + return Undo(RedoBuffer, UndoBuffer, smp); +} + + +// Restore undo/redo point for given sample +bool CSampleUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const SAMPLEINDEX smp) +{ + if(!SampleBufferExists(fromBuf, smp) || fromBuf[smp - 1].empty()) return false; + + CSoundFile &sndFile = modDoc.GetSoundFile(); + + // Select most recent undo slot and temporarily remove it from the buffer so that it won't get deleted by possible buffer size restrictions in PrepareBuffer() + UndoInfo undo = fromBuf[smp - 1].back(); + fromBuf[smp - 1].pop_back(); + + // When turning an undo point into a redo point (and vice versa), some action types need to be adjusted. + sampleUndoTypes redoType = undo.changeType; + if(redoType == sundo_delete) + redoType = sundo_insert; + else if(redoType == sundo_insert) + redoType = sundo_delete; + PrepareBuffer(toBuf, smp, redoType, undo.description, undo.changeStart, undo.changeEnd); + + ModSample &sample = sndFile.GetSample(smp); + std::byte *pCurrentSample = mpt::void_cast<std::byte*>(sample.samplev()); + int8 *pNewSample = nullptr; // a new sample is possibly going to be allocated, depending on what's going to be undone. + bool keepOnDisk = sample.uFlags[SMP_KEEPONDISK]; + bool replace = false; + + uint8 bytesPerSample = undo.OldSample.GetBytesPerSample(); + SmpLength changeLen = undo.changeEnd - undo.changeStart; + + switch(undo.changeType) + { + case sundo_none: + break; + + case sundo_invert: + // invert again + SampleEdit::InvertSample(sample, undo.changeStart, undo.changeEnd, sndFile); + break; + + case sundo_reverse: + // reverse again + SampleEdit::ReverseSample(sample, undo.changeStart, undo.changeEnd, sndFile); + break; + + case sundo_unsign: + // unsign again + SampleEdit::UnsignSample(sample, undo.changeStart, undo.changeEnd, sndFile); + break; + + case sundo_insert: + // delete inserted data + MPT_ASSERT(changeLen == sample.nLength - undo.OldSample.nLength); + if(undo.OldSample.nLength > 0) + { + memcpy(pCurrentSample + undo.changeStart * bytesPerSample, pCurrentSample + undo.changeEnd * bytesPerSample, (sample.nLength - undo.changeEnd) * bytesPerSample); + // also clean the sample end + memset(pCurrentSample + undo.OldSample.nLength * bytesPerSample, 0, (sample.nLength - undo.OldSample.nLength) * bytesPerSample); + } else + { + replace = true; + } + break; + + case sundo_update: + // simply replace what has been updated. + if(sample.nLength < undo.changeEnd) return false; + memcpy(pCurrentSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample); + break; + + case sundo_delete: + // insert deleted data + pNewSample = static_cast<int8 *>(ModSample::AllocateSample(undo.OldSample.nLength, bytesPerSample)); + if(pNewSample == nullptr) return false; + replace = true; + memcpy(pNewSample, pCurrentSample, undo.changeStart * bytesPerSample); + memcpy(pNewSample + undo.changeStart * bytesPerSample, undo.samplePtr, changeLen * bytesPerSample); + memcpy(pNewSample + undo.changeEnd * bytesPerSample, pCurrentSample + undo.changeStart * bytesPerSample, (undo.OldSample.nLength - undo.changeEnd) * bytesPerSample); + break; + + case sundo_replace: + // simply exchange sample pointer + pNewSample = static_cast<int8 *>(undo.samplePtr); + undo.samplePtr = nullptr; // prevent sample from being deleted + replace = true; + break; + + default: + MPT_ASSERT(false); // whoops, what's this? someone forgot to implement it, some code is obviously missing here! + return false; + } + + // Restore old sample header + sample = undo.OldSample; + sample.pData.pSample = mpt::void_cast<void*>(pCurrentSample); // select the "correct" old sample + sndFile.m_szNames[smp] = undo.oldName; + + if(replace) + { + ctrlSmp::ReplaceSample(sample, pNewSample, undo.OldSample.nLength, sndFile); + } + sample.PrecomputeLoops(sndFile, true); + + if(undo.changeType != sundo_none) + { + sample.uFlags.set(SMP_MODIFIED); + } + if(!keepOnDisk) + { + // Never re-enable the keep on disk flag after it was disabled. + // This can lead to quite some dangerous situations when replacing samples. + sample.uFlags.reset(SMP_KEEPONDISK); + } + + fromBuf[smp - 1].push_back(std::move(undo)); + DeleteStep(fromBuf, smp, fromBuf[smp - 1].size() - 1); + + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + modDoc.SetModified(); + + return true; +} + + +// Delete a given undo / redo step of a sample. +void CSampleUndo::DeleteStep(undobuf_t &buffer, const SAMPLEINDEX smp, const size_t step) +{ + if(!SampleBufferExists(buffer, smp) || step >= buffer[smp - 1].size()) return; + ModSample::FreeSample(buffer[smp - 1][step].samplePtr); + buffer[smp - 1].erase(buffer[smp - 1].begin() + step); +} + + +// Public helper function to remove the most recent undo point. +void CSampleUndo::RemoveLastUndoStep(const SAMPLEINDEX smp) +{ + if(!CanUndo(smp)) + return; + + DeleteStep(UndoBuffer, smp, UndoBuffer[smp - 1].size() - 1); + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); +} + + +// Restrict undo buffer size so it won't grow too large. +// This is done in FIFO style, equally distributed over all sample slots (very simple). +void CSampleUndo::RestrictBufferSize() +{ + size_t capacity = GetBufferCapacity(UndoBuffer) + GetBufferCapacity(RedoBuffer); + while(capacity > TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) + { + RestrictBufferSize(UndoBuffer, capacity); + RestrictBufferSize(RedoBuffer, capacity); + } +} + + +void CSampleUndo::RestrictBufferSize(undobuf_t &buffer, size_t &capacity) +{ + for(SAMPLEINDEX smp = 1; smp <= buffer.size(); smp++) + { + if(capacity <= TrackerSettings::Instance().m_SampleUndoBufferSize.Get().GetSizeInBytes()) return; + for(size_t i = 0; i < buffer[smp - 1].size(); i++) + { + if(buffer[smp - 1][i].samplePtr != nullptr) + { + capacity -= (buffer[smp - 1][i].changeEnd - buffer[smp - 1][i].changeStart) * buffer[smp - 1][i].OldSample.GetBytesPerSample(); + for(size_t j = 0; j <= i; j++) + { + DeleteStep(buffer, smp, 0); + } + // Try to evenly spread out the restriction, i.e. move on to other samples before deleting another step for this sample. + break; + } + } + } +} + + +// Update undo buffer when using rearrange sample functionality. +// newIndex contains one new index for each old index. newIndex[1] represents the first sample. +void CSampleUndo::RearrangeSamples(undobuf_t &buffer, const std::vector<SAMPLEINDEX> &newIndex) +{ + undobuf_t newBuf(modDoc.GetNumSamples()); + + const SAMPLEINDEX newSize = static_cast<SAMPLEINDEX>(newIndex.size()); + const SAMPLEINDEX oldSize = static_cast<SAMPLEINDEX>(buffer.size()); + for(SAMPLEINDEX smp = 1; smp <= oldSize; smp++) + { + MPT_ASSERT(smp >= newSize || newIndex[smp] <= modDoc.GetNumSamples()); + if(smp < newSize && newIndex[smp] > 0 && newIndex[smp] <= modDoc.GetNumSamples()) + { + newBuf[newIndex[smp] - 1] = buffer[smp - 1]; + } else + { + ClearUndo(smp); + } + } +#ifdef _DEBUG + for(size_t i = 0; i < oldSize; i++) + { + if(i + 1 < newIndex.size() && newIndex[i + 1] != 0) + MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size()); + else + MPT_ASSERT(buffer[i].empty()); + } +#endif + buffer = newBuf; +} + + +// Return total amount of bytes used by the sample undo buffer. +size_t CSampleUndo::GetBufferCapacity(const undobuf_t &buffer) const +{ + size_t sum = 0; + for(auto &smp : buffer) + { + for(auto &step : smp) + { + if(step.samplePtr != nullptr) + { + sum += (step.changeEnd - step.changeStart) * step.OldSample.GetBytesPerSample(); + } + } + } + return sum; +} + + +// Ensure that the undo buffer is big enough for a given sample number +bool CSampleUndo::SampleBufferExists(const undobuf_t &buffer, const SAMPLEINDEX smp) const +{ + if(smp == 0 || smp >= MAX_SAMPLES) return false; + if(smp <= buffer.size()) return true; + return false; +} + +// Get name of next undo item +const char *CSampleUndo::GetUndoName(const SAMPLEINDEX smp) const +{ + if(!CanUndo(smp)) + { + return ""; + } + return UndoBuffer[smp - 1].back().description; +} + + +// Get name of next redo item +const char *CSampleUndo::GetRedoName(const SAMPLEINDEX smp) const +{ + if(!CanRedo(smp)) + { + return ""; + } + return RedoBuffer[smp - 1].back().description; +} + + +///////////////////////////////////////////////////////////////////////////////////////// +// Instrument Undo Functions + + +// Remove all undo steps for all instruments. +void CInstrumentUndo::ClearUndo() +{ + UndoBuffer.clear(); + RedoBuffer.clear(); +} + + +// Remove all undo steps of a given instrument. +void CInstrumentUndo::ClearUndo(undobuf_t &buffer, const INSTRUMENTINDEX ins) +{ + if(!InstrumentBufferExists(buffer, ins)) return; + buffer[ins - 1].clear(); +} + + +// Create undo point for given Instrument. +// The main program has to tell what kind of changes are going to be made to the Instrument. +// That way, a lot of RAM can be saved, because some actions don't even require an undo Instrument buffer. +bool CInstrumentUndo::PrepareUndo(const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType) +{ + if(PrepareBuffer(UndoBuffer, ins, description, envType)) + { + ClearUndo(RedoBuffer, ins); + return true; + } + return false; +} + + +bool CInstrumentUndo::PrepareBuffer(undobuf_t &buffer, const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType) +{ + if(ins == 0 || ins >= MAX_INSTRUMENTS || modDoc.GetSoundFile().Instruments[ins] == nullptr) return false; + if(ins > buffer.size()) + { + buffer.resize(ins); + } + auto &insBuffer = buffer[ins - 1]; + + // Remove undo steps if there are too many. + if(insBuffer.size() >= MAX_UNDO_LEVEL) + { + insBuffer.erase(insBuffer.begin(), insBuffer.begin() + (insBuffer.size() - MAX_UNDO_LEVEL + 1)); + } + + // Create new undo slot + UndoInfo undo; + + const CSoundFile &sndFile = modDoc.GetSoundFile(); + undo.description = description; + undo.editedEnvelope = envType; + if(envType < ENV_MAXTYPES) + { + undo.instr.GetEnvelope(envType) = sndFile.Instruments[ins]->GetEnvelope(envType); + } else + { + undo.instr = *sndFile.Instruments[ins]; + } + // cppcheck false-positive + // cppcheck-suppress uninitStructMember + insBuffer.push_back(std::move(undo)); + + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + + return true; +} + + +// Restore undo point for given Instrument +bool CInstrumentUndo::Undo(const INSTRUMENTINDEX ins) +{ + return Undo(UndoBuffer, RedoBuffer, ins); +} + + +// Restore redo point for given Instrument +bool CInstrumentUndo::Redo(const INSTRUMENTINDEX ins) +{ + return Undo(RedoBuffer, UndoBuffer, ins); +} + + +// Restore undo/redo point for given Instrument +bool CInstrumentUndo::Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const INSTRUMENTINDEX ins) +{ + CSoundFile &sndFile = modDoc.GetSoundFile(); + if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(fromBuf, ins) || fromBuf[ins - 1].empty()) return false; + + // Select most recent undo slot + const UndoInfo &undo = fromBuf[ins - 1].back(); + + PrepareBuffer(toBuf, ins, undo.description, undo.editedEnvelope); + + // When turning an undo point into a redo point (and vice versa), some action types need to be adjusted. + ModInstrument *instr = sndFile.Instruments[ins]; + if(undo.editedEnvelope < ENV_MAXTYPES) + { + instr->GetEnvelope(undo.editedEnvelope) = undo.instr.GetEnvelope(undo.editedEnvelope); + } else + { + *instr = undo.instr; + } + + DeleteStep(fromBuf, ins, fromBuf[ins - 1].size() - 1); + + modDoc.UpdateAllViews(nullptr, UpdateHint().Undo()); + modDoc.SetModified(); + + return true; +} + + +// Delete a given undo / redo step of a Instrument. +void CInstrumentUndo::DeleteStep(undobuf_t &buffer, const INSTRUMENTINDEX ins, const size_t step) +{ + if(!InstrumentBufferExists(buffer, ins) || step >= buffer[ins - 1].size()) return; + buffer[ins - 1].erase(buffer[ins - 1].begin() + step); +} + + +// Public helper function to remove the most recent undo point. +void CInstrumentUndo::RemoveLastUndoStep(const INSTRUMENTINDEX ins) +{ + if(!CanUndo(ins)) return; + DeleteStep(UndoBuffer, ins, UndoBuffer[ins - 1].size() - 1); +} + + +// Update undo buffer when using rearrange instruments functionality. +// newIndex contains one new index for each old index. newIndex[1] represents the first instrument. +void CInstrumentUndo::RearrangeInstruments(undobuf_t &buffer, const std::vector<INSTRUMENTINDEX> &newIndex) +{ + undobuf_t newBuf(modDoc.GetNumInstruments()); + + const INSTRUMENTINDEX newSize = static_cast<INSTRUMENTINDEX>(newIndex.size()); + const INSTRUMENTINDEX oldSize = static_cast<INSTRUMENTINDEX>(buffer.size()); + for(INSTRUMENTINDEX ins = 1; ins <= oldSize; ins++) + { + MPT_ASSERT(ins >= newSize || newIndex[ins] <= modDoc.GetNumInstruments()); + if(ins < newSize && newIndex[ins] > 0 && newIndex[ins] <= modDoc.GetNumInstruments()) + { + newBuf[newIndex[ins] - 1] = buffer[ins - 1]; + } else + { + ClearUndo(ins); + } + } +#ifdef _DEBUG + for(size_t i = 0; i < oldSize; i++) + { + if(i + 1 < newIndex.size() && newIndex[i + 1] != 0) + MPT_ASSERT(newBuf[newIndex[i + 1] - 1].size() == buffer[i].size()); + else + MPT_ASSERT(buffer[i].empty()); + } +#endif + buffer = newBuf; +} + + +// Update undo buffer when using rearrange samples functionality. +// newIndex contains one new index for each old index. newIndex[1] represents the first sample. +void CInstrumentUndo::RearrangeSamples(undobuf_t &buffer, const INSTRUMENTINDEX ins, std::vector<SAMPLEINDEX> &newIndex) +{ + const CSoundFile &sndFile = modDoc.GetSoundFile(); + if(sndFile.Instruments[ins] == nullptr || !InstrumentBufferExists(buffer, ins) || buffer[ins - 1].empty()) return; + + for(auto &i : buffer[ins - 1]) if(i.editedEnvelope >= ENV_MAXTYPES) + { + for(auto &sample : i.instr.Keyboard) + { + if(sample < newIndex.size()) + sample = newIndex[sample]; + else + sample = 0; + } + } +} + + +// Ensure that the undo buffer is big enough for a given Instrument number +bool CInstrumentUndo::InstrumentBufferExists(const undobuf_t &buffer, const INSTRUMENTINDEX ins) const +{ + if(ins == 0 || ins >= MAX_INSTRUMENTS) return false; + if(ins <= buffer.size()) return true; + return false; +} + + +// Get name of next undo item +const char *CInstrumentUndo::GetUndoName(const INSTRUMENTINDEX ins) const +{ + if(!CanUndo(ins)) + { + return ""; + } + return UndoBuffer[ins - 1].back().description; +} + + +// Get name of next redo item +const char *CInstrumentUndo::GetRedoName(const INSTRUMENTINDEX ins) const +{ + if(!CanRedo(ins)) + { + return ""; + } + return RedoBuffer[ins - 1].back().description; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Undo.h b/Src/external_dependencies/openmpt-trunk/mptrack/Undo.h new file mode 100644 index 00000000..0ff30162 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Undo.h @@ -0,0 +1,229 @@ +/* + * Undo.h + * ------ + * Purpose: Editor undo buffer functionality. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/ModChannel.h" +#include "../soundlib/modcommand.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +struct ModSample; + +#define MAX_UNDO_LEVEL 100000 // 100,000 undo steps for each undo type! + +///////////////////////////////////////////////////////////////////////////////////////// +// Pattern Undo + + +class CPatternUndo +{ +protected: + static constexpr auto DELETE_PATTERN = PATTERNINDEX_INVALID; + + struct UndoInfo + { + std::vector<ModChannelSettings> channelInfo; // Optional old channel information (pan / volume / etc.) + std::vector<ModCommand> content; // Rescued pattern content + const char *description; // Name of this undo action + ROWINDEX numPatternRows; // Original number of pattern rows (in case of resize, DELETE_PATTERN in case of deletion) + ROWINDEX firstRow, numRows; + PATTERNINDEX pattern; + CHANNELINDEX firstChannel, numChannels; + bool linkToPrevious; // This undo information is linked with the previous undo information + + bool OnlyChannelSettings() const noexcept + { + return !channelInfo.empty() && numRows < 1 && !linkToPrevious; + } + }; + + using undobuf_t = std::vector<UndoInfo>; + + undobuf_t UndoBuffer; + undobuf_t RedoBuffer; + CModDoc &modDoc; + + // Pattern undo helper functions + PATTERNINDEX Undo(undobuf_t &fromBuf, undobuf_t &toBuf, bool linkedFromPrevious); + + bool PrepareBuffer(undobuf_t &buffer, PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious, bool storeChannelInfo) const; + + static CString GetName(const undobuf_t &buffer); + static void RearrangePatterns(undobuf_t &buffer, const std::vector<PATTERNINDEX> &newIndex); + +public: + + // Removes all undo steps from the buffer. + void ClearUndo(); + // Adds a new action to the undo buffer. + bool PrepareUndo(PATTERNINDEX pattern, CHANNELINDEX firstChn, ROWINDEX firstRow, CHANNELINDEX numChns, ROWINDEX numRows, const char *description, bool linkToPrevious = false, bool storeChannelInfo = false); + // Adds a new action only affecting channel data to the undo buffer. + bool PrepareChannelUndo(CHANNELINDEX firstChn, CHANNELINDEX numChns, const char *description); + // Undoes the most recent action. + PATTERNINDEX Undo(); + // Redoes the most recent action. + PATTERNINDEX Redo(); + // Returns true if any actions can currently be undone. + bool CanUndo() const { return !UndoBuffer.empty(); } + // Returns true if any actions can currently be redone. + bool CanRedo() const { return !RedoBuffer.empty(); } + // Returns true if a channel-specific action (no pattern data) can currently be undone. + bool CanUndoChannelSettings() const { return !UndoBuffer.empty() && UndoBuffer.back().OnlyChannelSettings(); } + // Returns true if a channel-specific action (no pattern data) actions can currently be redone. + bool CanRedoChannelSettings() const { return !RedoBuffer.empty() && RedoBuffer.back().OnlyChannelSettings(); } + // Remove the latest added undo step from the undo buffer + void RemoveLastUndoStep(); + // Get name of next undo item + CString GetUndoName() const { return GetName(UndoBuffer); } + // Get name of next redo item + CString GetRedoName() const { return GetName(RedoBuffer); } + // Adjust undo buffers for rearranged patterns + void RearrangePatterns(const std::vector<PATTERNINDEX> &newIndex); + + CPatternUndo(CModDoc &parent) : modDoc(parent) { } +}; + + +///////////////////////////////////////////////////////////////////////////////////////// +// Sample Undo + +// We will differentiate between different types of undo actions so that we don't have to copy the whole sample everytime. +enum sampleUndoTypes +{ + sundo_none, // no changes to sample itself, e.g. loop point update + sundo_update, // silence, amplify, normalize, dc offset - update complete sample section + sundo_delete, // delete part of the sample + sundo_invert, // invert sample phase, apply again to undo + sundo_reverse, // reverse sample, ditto + sundo_unsign, // unsign sample, ditto + sundo_insert, // insert data, delete inserted data to undo + sundo_replace, // replace complete sample (16->8Bit, up/downsample, downmix to mono, pitch shifting / time stretching, trimming, pasting) +}; + + +class CSampleUndo +{ +protected: + + struct UndoInfo + { + ModSample OldSample; + mpt::charbuf<MAX_SAMPLENAME> oldName; + void *samplePtr = nullptr; + const char *description = nullptr; + SmpLength changeStart = 0, changeEnd = 0; + sampleUndoTypes changeType = sundo_none; + }; + + using undobuf_t = std::vector<std::vector<UndoInfo>>; + undobuf_t UndoBuffer; + undobuf_t RedoBuffer; + + CModDoc &modDoc; + + // Sample undo helper functions + void ClearUndo(undobuf_t &buffer, const SAMPLEINDEX smp); + void DeleteStep(undobuf_t &buffer, const SAMPLEINDEX smp, const size_t step); + bool SampleBufferExists(const undobuf_t &buffer, const SAMPLEINDEX smp) const; + void RestrictBufferSize(undobuf_t &buffer, size_t &capacity); + size_t GetBufferCapacity(const undobuf_t &buffer) const; + void RearrangeSamples(undobuf_t &buffer, const std::vector<SAMPLEINDEX> &newIndex); + + bool PrepareBuffer(undobuf_t &buffer, const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart, SmpLength changeEnd); + bool Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const SAMPLEINDEX smp); + +public: + + // Sample undo functions + void ClearUndo(); + void ClearUndo(const SAMPLEINDEX smp) { ClearUndo(UndoBuffer, smp); ClearUndo(RedoBuffer, smp); } + bool PrepareUndo(const SAMPLEINDEX smp, sampleUndoTypes changeType, const char *description, SmpLength changeStart = 0, SmpLength changeEnd = 0); + bool Undo(const SAMPLEINDEX smp); + bool Redo(const SAMPLEINDEX smp); + bool CanUndo(const SAMPLEINDEX smp) const { return SampleBufferExists(UndoBuffer, smp) && !UndoBuffer[smp - 1].empty(); } + bool CanRedo(const SAMPLEINDEX smp) const { return SampleBufferExists(RedoBuffer, smp) && !RedoBuffer[smp - 1].empty(); } + void RemoveLastUndoStep(const SAMPLEINDEX smp); + const char *GetUndoName(const SAMPLEINDEX smp) const; + const char *GetRedoName(const SAMPLEINDEX smp) const; + void RestrictBufferSize(); + void RearrangeSamples(const std::vector<SAMPLEINDEX> &newIndex) { RearrangeSamples(UndoBuffer, newIndex); RearrangeSamples(RedoBuffer, newIndex); } + + CSampleUndo(CModDoc &parent) : modDoc(parent) { } + + ~CSampleUndo() + { + ClearUndo(); + }; + +}; + + +///////////////////////////////////////////////////////////////////////////////////////// +// Instrument Undo + + +class CInstrumentUndo +{ +protected: + + struct UndoInfo + { + ModInstrument instr; + const char *description = nullptr; + EnvelopeType editedEnvelope = ENV_MAXTYPES; + }; + + using undobuf_t = std::vector<std::vector<UndoInfo>>; + undobuf_t UndoBuffer; + undobuf_t RedoBuffer; + + CModDoc &modDoc; + + // Instrument undo helper functions + void ClearUndo(undobuf_t &buffer, const INSTRUMENTINDEX ins); + void DeleteStep(undobuf_t &buffer, const INSTRUMENTINDEX ins, const size_t step); + bool InstrumentBufferExists(const undobuf_t &buffer, const INSTRUMENTINDEX ins) const; + void RearrangeInstruments(undobuf_t &buffer, const std::vector<INSTRUMENTINDEX> &newIndex); + void RearrangeSamples(undobuf_t &buffer, const INSTRUMENTINDEX ins, std::vector<SAMPLEINDEX> &newIndex); + + bool PrepareBuffer(undobuf_t &buffer, const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType); + bool Undo(undobuf_t &fromBuf, undobuf_t &toBuf, const INSTRUMENTINDEX ins); + +public: + + // Instrument undo functions + void ClearUndo(); + void ClearUndo(const INSTRUMENTINDEX ins) { ClearUndo(UndoBuffer, ins); ClearUndo(RedoBuffer, ins); } + bool PrepareUndo(const INSTRUMENTINDEX ins, const char *description, EnvelopeType envType = ENV_MAXTYPES); + bool Undo(const INSTRUMENTINDEX ins); + bool Redo(const INSTRUMENTINDEX ins); + bool CanUndo(const INSTRUMENTINDEX ins) const { return InstrumentBufferExists(UndoBuffer, ins) && !UndoBuffer[ins - 1].empty(); } + bool CanRedo(const INSTRUMENTINDEX ins) const { return InstrumentBufferExists(RedoBuffer, ins) && !RedoBuffer[ins - 1].empty(); } + void RemoveLastUndoStep(const INSTRUMENTINDEX ins); + const char *GetUndoName(const INSTRUMENTINDEX ins) const; + const char *GetRedoName(const INSTRUMENTINDEX ins) const; + void RearrangeInstruments(const std::vector<INSTRUMENTINDEX> &newIndex) { RearrangeInstruments(UndoBuffer, newIndex); RearrangeInstruments(RedoBuffer, newIndex); } + void RearrangeSamples(const INSTRUMENTINDEX ins, std::vector<SAMPLEINDEX> &newIndex) { RearrangeSamples(UndoBuffer, ins, newIndex); RearrangeSamples(RedoBuffer, ins, newIndex); } + + CInstrumentUndo(CModDoc &parent) : modDoc(parent) { } + + ~CInstrumentUndo() + { + ClearUndo(); + }; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.cpp new file mode 100644 index 00000000..f3febde2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.cpp @@ -0,0 +1,1699 @@ +/* + * UpdateCheck.cpp + * --------------- + * Purpose: Class for easy software update check. + * 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 "UpdateCheck.h" +#include "mpt/binary/hex.hpp" +#include "BuildVariants.h" +#include "../common/version.h" +#include "../common/misc_util.h" +#include "../common/mptStringBuffer.h" +#include "Mptrack.h" +#include "TrackerSettings.h" +// Setup dialog stuff +#include "Mainfrm.h" +#include "mpt/system_error/system_error.hpp" +#include "mpt/crypto/hash.hpp" +#include "mpt/crypto/jwk.hpp" +#include "HTTP.h" +#include "mpt/json/json.hpp" +#include "dlg_misc.h" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "ProgressDialog.h" +#include "Moddoc.h" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +#if defined(MPT_ENABLE_UPDATE) + + +namespace Update { + + struct windowsversion { + uint64 version_major = 0; + uint64 version_minor = 0; + uint64 servicepack_major = 0; + uint64 servicepack_minor = 0; + uint64 build = 0; + uint64 wine_major = 0; + uint64 wine_minor = 0; + uint64 wine_update = 0; + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(windowsversion + ,version_major + ,version_minor + ,servicepack_major + ,servicepack_minor + ,build + ,wine_major + ,wine_minor + ,wine_update + ) + + struct autoupdate_installer { + std::vector<mpt::ustring> arguments = {}; + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(autoupdate_installer + ,arguments + ) + + struct autoupdate_archive { + mpt::ustring subfolder = U_(""); + mpt::ustring restartbinary = U_(""); + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(autoupdate_archive + ,subfolder + ,restartbinary + ) + + struct downloadinfo { + mpt::ustring url = U_(""); + std::map<mpt::ustring, mpt::ustring> checksums = {}; + mpt::ustring filename = U_(""); + std::optional<autoupdate_installer> autoupdate_installer; + std::optional<autoupdate_archive> autoupdate_archive; + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(downloadinfo + ,url + ,checksums + ,filename + ,autoupdate_installer + ,autoupdate_archive + ) + + struct download { + mpt::ustring url = U_(""); + mpt::ustring download_url = U_(""); + mpt::ustring type = U_(""); + bool can_autoupdate = false; + mpt::ustring autoupdate_minversion = U_(""); + mpt::ustring os = U_(""); + std::optional<windowsversion> required_windows_version; + std::map<mpt::ustring, bool> required_architectures = {}; + std::map<mpt::ustring, bool> supported_architectures = {}; + std::map<mpt::ustring, std::map<mpt::ustring, bool>> required_processor_features = {}; + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(download + ,url + ,download_url + ,type + ,can_autoupdate + ,autoupdate_minversion + ,os + ,required_windows_version + ,required_architectures + ,supported_architectures + ,required_processor_features + ) + + struct versioninfo { + mpt::ustring version = U_(""); + mpt::ustring date = U_(""); + mpt::ustring announcement_url = U_(""); + mpt::ustring changelog_url = U_(""); + std::map<mpt::ustring, download> downloads = {}; + }; + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(versioninfo + ,version + ,date + ,announcement_url + ,changelog_url + ,downloads + ) + + using versions = std::map<mpt::ustring, versioninfo>; + +} // namespace Update + + +struct UpdateInfo { + mpt::ustring version; + mpt::ustring download; + bool IsAvailable() const + { + return !version.empty(); + } +}; + +static bool IsCurrentArchitecture(const mpt::ustring &architecture) +{ + return mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()) == architecture; +} + +static bool IsArchitectureSupported(const mpt::ustring &architecture) +{ + const auto & architectures = mpt::OS::Windows::GetSupportedProcessArchitectures(mpt::OS::Windows::GetHostArchitecture()); + for(const auto & arch : architectures) + { + if(mpt::OS::Windows::Name(arch) == architecture) + { + return true; + } + } + return false; +} + +static bool IsArchitectureFeatureSupported(const mpt::ustring &architecture, const mpt::ustring &feature) +{ + MPT_UNUSED_VARIABLE(architecture); + #ifdef MPT_ENABLE_ARCH_INTRINSICS + const CPU::Info CPUInfo = CPU::Info::Get(); + if(feature == U_("")) return true; + else if(feature == U_("lm")) return (CPUInfo.AvailableFeatures & CPU::feature::lm) != 0; + else if(feature == U_("mmx")) return (CPUInfo.AvailableFeatures & CPU::feature::mmx) != 0; + else if(feature == U_("sse")) return (CPUInfo.AvailableFeatures & CPU::feature::sse) != 0; + else if(feature == U_("sse2")) return (CPUInfo.AvailableFeatures & CPU::feature::sse2) != 0; + else if(feature == U_("sse3")) return (CPUInfo.AvailableFeatures & CPU::feature::sse3) != 0; + else if(feature == U_("ssse3")) return (CPUInfo.AvailableFeatures & CPU::feature::ssse3) != 0; + else if(feature == U_("sse4.1")) return (CPUInfo.AvailableFeatures & CPU::feature::sse4_1) != 0; + else if(feature == U_("sse4.2")) return (CPUInfo.AvailableFeatures & CPU::feature::sse4_2) != 0; + else if(feature == U_("avx")) return (CPUInfo.AvailableFeatures & CPU::feature::avx) != 0; + else if(feature == U_("avx2")) return (CPUInfo.AvailableFeatures & CPU::feature::avx2) != 0; + else return false; + #else // !MPT_ENABLE_ARCH_INTRINSICS + return true; + #endif // MPT_ENABLE_ARCH_INTRINSICS +} + + +static mpt::ustring GetChannelName(UpdateChannel channel) +{ + mpt::ustring channelName = U_("release"); + switch(channel) + { + case UpdateChannelDevelopment: + channelName = U_("development"); + break; + case UpdateChannelNext: + channelName = U_("next"); + break; + case UpdateChannelRelease: + channelName = U_("release"); + break; + default: + channelName = U_("release"); + break; + } + return channelName; +} + + +static UpdateInfo GetBestDownload(const Update::versions &versions) +{ + + UpdateInfo result; + VersionWithRevision bestVersion = VersionWithRevision::Current(); + + for(const auto & [versionname, versioninfo] : versions) + { + + if(!VersionWithRevision::Parse(versioninfo.version).IsNewerThan(bestVersion)) + { + continue; + } + + mpt::ustring bestDownloadName; + + // check if version supports the current system + bool is_supported = false; + for(auto & [downloadname, download] : versioninfo.downloads) + { + + // is it for windows? + if(download.os != U_("windows") || !download.required_windows_version) + { + continue; + } + + // can the installer run on the current system? + bool download_supported = true; + for(const auto & [architecture, required] : download.required_architectures) + { + if(!(required && IsArchitectureSupported(architecture))) + { + download_supported = false; + } + } + + // does the download run on current architecture? + bool architecture_supported = false; + for(const auto & [architecture, supported] : download.supported_architectures) + { + if(supported && IsCurrentArchitecture(architecture)) + { + architecture_supported = true; + } + } + if(!architecture_supported) + { + download_supported = false; + } + + // does the current system have all required features? + for(const auto & [architecture, features] : download.required_processor_features) + { + if(IsCurrentArchitecture(architecture)) + { + for(const auto & [feature, required] : features) + { + if(!(required && IsArchitectureFeatureSupported(architecture, feature))) + { + download_supported = false; + } + } + } + } + + if(mpt::OS::Windows::Version::Current().IsBefore( + mpt::osinfo::windows::Version::System(mpt::saturate_cast<uint32>(download.required_windows_version->version_major), mpt::saturate_cast<uint32>(download.required_windows_version->version_minor)), + mpt::osinfo::windows::Version::ServicePack(mpt::saturate_cast<uint16>(download.required_windows_version->servicepack_major), mpt::saturate_cast<uint16>(download.required_windows_version->servicepack_minor)), + mpt::osinfo::windows::Version::Build(mpt::saturate_cast<uint32>(download.required_windows_version->build)) + )) + { + download_supported = false; + } + + if(mpt::OS::Windows::IsWine() && theApp.GetWineVersion()->Version().IsValid()) + { + if(theApp.GetWineVersion()->Version().IsBefore(mpt::OS::Wine::Version(mpt::saturate_cast<uint8>(download.required_windows_version->wine_major), mpt::saturate_cast<uint8>(download.required_windows_version->wine_minor), mpt::saturate_cast<uint8>(download.required_windows_version->wine_update)))) + { + download_supported = false; + } + } + + if(download_supported) + { + is_supported = true; + if(theApp.IsInstallerMode() && download.type == U_("installer")) + { + bestDownloadName = downloadname; + } else if(theApp.IsPortableMode() && download.type == U_("archive")) + { + bestDownloadName = downloadname; + } + } + + } + + if(is_supported) + { + bestVersion = VersionWithRevision::Parse(versioninfo.version); + result.version = versionname; + result.download = bestDownloadName; + } + + } + + return result; + +} + + +// Update notification dialog +class UpdateDialog : public CDialog +{ +protected: + const CString m_releaseVersion; + const CString m_releaseDate; + const CString m_releaseURL; + const CString m_buttonText; + CFont m_boldFont; + +public: + UpdateDialog(const CString &releaseVersion, const CString &releaseDate, const CString &releaseURL, const CString &buttonText = _T("&Update")) + : CDialog(IDD_UPDATE) + , m_releaseVersion(releaseVersion) + , m_releaseDate(releaseDate) + , m_releaseURL(releaseURL) + , m_buttonText(buttonText) + { } + + BOOL OnInitDialog() override + { + CDialog::OnInitDialog(); + + SetDlgItemText(IDOK, m_buttonText); + + CFont *font = GetDlgItem(IDC_VERSION2)->GetFont(); + LOGFONT lf; + font->GetLogFont(&lf); + lf.lfWeight = FW_BOLD; + m_boldFont.CreateFontIndirect(&lf); + GetDlgItem(IDC_VERSION2)->SetFont(&m_boldFont); + + SetDlgItemText(IDC_VERSION1, mpt::cfmt::val(VersionWithRevision::Current())); + SetDlgItemText(IDC_VERSION2, m_releaseVersion); + SetDlgItemText(IDC_DATE, m_releaseDate); + SetDlgItemText(IDC_SYSLINK1, _T("More information about this build:\n<a href=\"") + m_releaseURL + _T("\">") + m_releaseURL + _T("</a>")); + CheckDlgButton(IDC_CHECK1, (TrackerSettings::Instance().UpdateIgnoreVersion == m_releaseVersion) ? BST_CHECKED : BST_UNCHECKED); + return FALSE; + } + + void OnDestroy() + { + TrackerSettings::Instance().UpdateIgnoreVersion = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED ? m_releaseVersion : CString(); + m_boldFont.DeleteObject(); + CDialog::OnDestroy(); + } + + void OnClickURL(NMHDR * /*pNMHDR*/, LRESULT * /*pResult*/) + { + CTrackApp::OpenURL(m_releaseURL); + } + + DECLARE_MESSAGE_MAP() +}; + +BEGIN_MESSAGE_MAP(UpdateDialog, CDialog) + ON_NOTIFY(NM_CLICK, IDC_SYSLINK1, &UpdateDialog::OnClickURL) + ON_WM_DESTROY() +END_MESSAGE_MAP() + + +mpt::ustring CUpdateCheck::GetStatisticsUserInformation(bool shortText) +{ + if(shortText) + { + return U_("A randomized user ID is sent together with basic system information." + " This ID cannot be linked to you personally in any way." + "\nOpenMPT will use this information to gather usage statistics and to plan system support for future OpenMPT versions."); + } else + { + return U_( + "When checking for updates, OpenMPT can additionally collect basic statistical information." + " A randomized user ID is sent alongside the update check. This ID and the transmitted statistics cannot be linked to you personally in any way." + " OpenMPT will use this information to gather usage statistics and to plan system support for future OpenMPT versions." + "\nOpenMPT would collect the following statistical data points: OpenMPT version, Windows version, type of CPU, amount of RAM, sound device settings, configured update check frequency of OpenMPT."); + } +} + + +std::vector<mpt::ustring> CUpdateCheck::GetDefaultUpdateSigningKeysRootAnchors() +{ + // IMPORTANT: + // Signing keys are *NOT* stored on the same server as openmpt.org or the updates themselves, + // because otherwise, a single compromised server could allow for rogue updates. + return { + U_("https://update.openmpt.de/update/"), + U_("https://demo-scene.de/openmpt/update/") + }; +} + + +mpt::ustring CUpdateCheck::GetDefaultAPIURL() +{ + return U_("https://update.openmpt.org/api/v3/"); +} + + +std::atomic<int32> CUpdateCheck::s_InstanceCount(0); + + +int32 CUpdateCheck::GetNumCurrentRunningInstances() +{ + return s_InstanceCount.load(); +} + + + +bool CUpdateCheck::IsSuitableUpdateMoment() +{ + const auto documents = theApp.GetOpenDocuments(); + return std::all_of(documents.begin(), documents.end(), [](auto doc) { return !doc->IsModified(); }); +} + + +// Start update check +void CUpdateCheck::StartUpdateCheckAsync(bool isAutoUpdate) +{ + bool loadPersisted = false; + if(isAutoUpdate) + { + if(!TrackerSettings::Instance().UpdateEnabled) + { + return; + } + if(!IsSuitableUpdateMoment()) + { + return; + } + int updateCheckPeriod = TrackerSettings::Instance().UpdateIntervalDays; + if(updateCheckPeriod < 0) + { + return; + } + // Do we actually need to run the update check right now? + const time_t now = time(nullptr); + const time_t lastCheck = TrackerSettings::Instance().UpdateLastUpdateCheck.Get(); + // Check update interval. Note that we always check for updates when the system time had gone backwards (i.e. when the last update check supposedly happened in the future). + const double secsSinceLastCheck = difftime(now, lastCheck); + if(secsSinceLastCheck > 0.0 && secsSinceLastCheck < updateCheckPeriod * 86400.0) + { + loadPersisted = true; + } + + // Never ran update checks before, so we notify the user of automatic update checks. + if(TrackerSettings::Instance().UpdateShowUpdateHint) + { + TrackerSettings::Instance().UpdateShowUpdateHint = false; + const auto checkIntervalDays = TrackerSettings::Instance().UpdateIntervalDays.Get(); + CString msg = MPT_CFORMAT("OpenMPT would like to check for updates now, proceed?\n\nNote: In the future, OpenMPT will check for updates {}. If you do not want this, you can disable update checks in the setup.") + ( + checkIntervalDays == 0 ? CString(_T("on every program start")) : + checkIntervalDays == 1 ? CString(_T("every day")) : + MPT_CFORMAT("every {} days")(checkIntervalDays) + ); + if(Reporting::Confirm(msg, _T("OpenMPT Update")) == cnfNo) + { + TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(now); + return; + } + } + } else + { + if(!IsSuitableUpdateMoment()) + { + Reporting::Notification(_T("Please save all modified modules before updating OpenMPT."), _T("OpenMPT Update")); + return; + } + if(!TrackerSettings::Instance().UpdateEnabled) + { + if(Reporting::Confirm(_T("Update Check is disabled. Do you want to check anyway?"), _T("OpenMPT Update")) != cnfYes) + { + return; + } + } + } + TrackerSettings::Instance().UpdateShowUpdateHint = false; + + // ask if user wants to contribute system statistics + if(!TrackerSettings::Instance().UpdateStatisticsConsentAsked) + { + const auto enableStatistics = Reporting::Confirm( + U_("Do you want to contribute to OpenMPT by providing system statistics?\r\n\r\n") + + mpt::String::Replace(CUpdateCheck::GetStatisticsUserInformation(false), U_("\n"), U_("\r\n")) + U_("\r\n\r\n") + + MPT_UFORMAT("This option was previously {} on your system.\r\n")(TrackerSettings::Instance().UpdateStatistics ? U_("enabled") : U_("disabled")), + false, !TrackerSettings::Instance().UpdateStatistics.Get()); + TrackerSettings::Instance().UpdateStatistics = (enableStatistics == ConfirmAnswer::cnfYes); + TrackerSettings::Instance().UpdateStatisticsConsentAsked = true; + } + + int32 expected = 0; + if(!s_InstanceCount.compare_exchange_strong(expected, 1)) + { + return; + } + + CUpdateCheck::Context context; + context.window = CMainFrame::GetMainFrame(); + context.msgStart = MPT_WM_APP_UPDATECHECK_START; + context.msgProgress = MPT_WM_APP_UPDATECHECK_PROGRESS; + context.msgCanceled = MPT_WM_APP_UPDATECHECK_CANCELED; + context.msgFailure = MPT_WM_APP_UPDATECHECK_FAILURE; + context.msgSuccess = MPT_WM_APP_UPDATECHECK_SUCCESS; + context.autoUpdate = isAutoUpdate; + context.loadPersisted = loadPersisted; + context.statistics = GetStatisticsDataV3(CUpdateCheck::Settings()); + std::thread(CUpdateCheck::ThreadFunc(CUpdateCheck::Settings(), context)).detach(); +} + + +CUpdateCheck::Settings::Settings() + : periodDays(TrackerSettings::Instance().UpdateIntervalDays) + , channel(static_cast<UpdateChannel>(TrackerSettings::Instance().UpdateChannel.Get())) + , persistencePath(theApp.GetConfigPath()) + , apiURL(TrackerSettings::Instance().UpdateAPIURL) + , sendStatistics(TrackerSettings::Instance().UpdateStatistics) + , statisticsUUID(TrackerSettings::Instance().VersionInstallGUID) +{ +} + + +CUpdateCheck::ThreadFunc::ThreadFunc(const CUpdateCheck::Settings &settings, const CUpdateCheck::Context &context) + : settings(settings) + , context(context) +{ + return; +} + + +void CUpdateCheck::ThreadFunc::operator () () +{ + SetThreadPriority(GetCurrentThread(), context.autoUpdate ? THREAD_PRIORITY_BELOW_NORMAL : THREAD_PRIORITY_NORMAL); + CheckForUpdate(settings, context); +} + + +std::string CUpdateCheck::GetStatisticsDataV3(const Settings &settings) +{ + nlohmann::json j; + j["OpenMPT"]["Version"] = mpt::ufmt::val(Version::Current()); + j["OpenMPT"]["BuildVariant"] = BuildVariants().GetBuildVariantName(BuildVariants().GetBuildVariant()); + j["OpenMPT"]["Architecture"] = mpt::OS::Windows::Name(mpt::OS::Windows::GetProcessArchitecture()); + j["Update"]["PeriodDays"] = settings.periodDays; + j["Update"]["Channel"] = ((settings.channel == UpdateChannelRelease) ? U_("Release") : (settings.channel == UpdateChannelNext) ? U_("Next") : (settings.channel == UpdateChannelDevelopment) ? U_("Development") : U_("")); + j["System"]["Windows"]["Version"]["Name"] = mpt::OS::Windows::Version::GetName(mpt::OS::Windows::Version::Current()); + j["System"]["Windows"]["Version"]["Major"] = mpt::OS::Windows::Version::Current().GetSystem().Major; + j["System"]["Windows"]["Version"]["Minor"] = mpt::OS::Windows::Version::Current().GetSystem().Minor; + j["System"]["Windows"]["ServicePack"]["Major"] = mpt::OS::Windows::Version::Current().GetServicePack().Major; + j["System"]["Windows"]["ServicePack"]["Minor"] = mpt::OS::Windows::Version::Current().GetServicePack().Minor; + j["System"]["Windows"]["Build"] = mpt::OS::Windows::Version::Current().GetBuild(); + j["System"]["Windows"]["Architecture"] = mpt::OS::Windows::Name(mpt::OS::Windows::GetHostArchitecture()); + j["System"]["Windows"]["IsWine"] = mpt::OS::Windows::IsWine(); + j["System"]["Windows"]["TypeRaw"] = MPT_AFORMAT("0x{}")(mpt::afmt::HEX0<8>(mpt::OS::Windows::Version::Current().GetTypeId())); + std::vector<mpt::OS::Windows::Architecture> architectures = mpt::OS::Windows::GetSupportedProcessArchitectures(mpt::OS::Windows::GetHostArchitecture()); + for(const auto & arch : architectures) + { + j["System"]["Windows"]["ProcessArchitectures"][mpt::ToCharset(mpt::Charset::UTF8, mpt::OS::Windows::Name(arch))] = true; + } + j["System"]["Memory"] = mpt::OS::Windows::GetSystemMemorySize() / 1024 / 1024; // MB + j["System"]["Threads"] = std::thread::hardware_concurrency(); + if(mpt::OS::Windows::IsWine()) + { + mpt::OS::Wine::VersionContext v; + j["System"]["Windows"]["Wine"]["Version"]["Raw"] = v.RawVersion(); + if(v.Version().IsValid()) + { + j["System"]["Windows"]["Wine"]["Version"]["Major"] = v.Version().GetMajor(); + j["System"]["Windows"]["Wine"]["Version"]["Minor"] = v.Version().GetMinor(); + j["System"]["Windows"]["Wine"]["Version"]["Update"] = v.Version().GetUpdate(); + } + j["System"]["Windows"]["Wine"]["HostSysName"] = v.RawHostSysName(); + } + const SoundDevice::Identifier deviceIdentifier = TrackerSettings::Instance().GetSoundDeviceIdentifier(); + const SoundDevice::Info deviceInfo = theApp.GetSoundDevicesManager()->FindDeviceInfo(deviceIdentifier); + const SoundDevice::Settings deviceSettings = TrackerSettings::Instance().GetSoundDeviceSettings(deviceIdentifier); + j["OpenMPT"]["SoundDevice"]["Type"] = deviceInfo.type; + j["OpenMPT"]["SoundDevice"]["Name"] = deviceInfo.name; + j["OpenMPT"]["SoundDevice"]["Settings"]["Samplerate"] = deviceSettings.Samplerate; + j["OpenMPT"]["SoundDevice"]["Settings"]["Latency"] = deviceSettings.Latency; + j["OpenMPT"]["SoundDevice"]["Settings"]["UpdateInterval"] = deviceSettings.UpdateInterval; + j["OpenMPT"]["SoundDevice"]["Settings"]["Channels"] = deviceSettings.Channels.GetNumHostChannels(); + j["OpenMPT"]["SoundDevice"]["Settings"]["BoostThreadPriority"] = deviceSettings.BoostThreadPriority; + j["OpenMPT"]["SoundDevice"]["Settings"]["ExclusiveMode"] = deviceSettings.ExclusiveMode; + j["OpenMPT"]["SoundDevice"]["Settings"]["UseHardwareTiming"] = deviceSettings.UseHardwareTiming; + j["OpenMPT"]["SoundDevice"]["Settings"]["KeepDeviceRunning"] = deviceSettings.KeepDeviceRunning; + #ifdef MPT_ENABLE_ARCH_INTRINSICS + const CPU::Info CPUInfo = CPU::Info::Get(); + j["System"]["Processor"]["Vendor"] = std::string(mpt::String::ReadAutoBuf(CPUInfo.VendorID)); + j["System"]["Processor"]["Brand"] = std::string(mpt::String::ReadAutoBuf(CPUInfo.BrandID)); + j["System"]["Processor"]["CpuidRaw"] = mpt::afmt::hex0<8>(CPUInfo.CPUID); + j["System"]["Processor"]["Id"]["Family"] = CPUInfo.Family; + j["System"]["Processor"]["Id"]["Model"] = CPUInfo.Model; + j["System"]["Processor"]["Id"]["Stepping"] = CPUInfo.Stepping; + j["System"]["Processor"]["Features"]["lm"] = ((CPUInfo.AvailableFeatures & CPU::feature::lm) != 0); + j["System"]["Processor"]["Features"]["mmx"] = ((CPUInfo.AvailableFeatures & CPU::feature::mmx) != 0); + j["System"]["Processor"]["Features"]["sse"] = ((CPUInfo.AvailableFeatures & CPU::feature::sse) != 0); + j["System"]["Processor"]["Features"]["sse2"] = ((CPUInfo.AvailableFeatures & CPU::feature::sse2) != 0); + j["System"]["Processor"]["Features"]["sse3"] = ((CPUInfo.AvailableFeatures & CPU::feature::sse3) != 0); + j["System"]["Processor"]["Features"]["ssse3"] = ((CPUInfo.AvailableFeatures & CPU::feature::ssse3) != 0); + j["System"]["Processor"]["Features"]["sse4.1"] = ((CPUInfo.AvailableFeatures & CPU::feature::sse4_1) != 0); + j["System"]["Processor"]["Features"]["sse4.2"] = ((CPUInfo.AvailableFeatures & CPU::feature::sse4_2) != 0); + j["System"]["Processor"]["Features"]["avx"] = ((CPUInfo.AvailableFeatures & CPU::feature::avx) != 0); + j["System"]["Processor"]["Features"]["avx2"] = ((CPUInfo.AvailableFeatures & CPU::feature::avx2) != 0); + #endif // MPT_ENABLE_ARCH_INTRINSICS + return j.dump(1, '\t'); +} + + +// Run update check (independent thread) +UpdateCheckResult CUpdateCheck::SearchUpdate(const CUpdateCheck::Context &context, const CUpdateCheck::Settings &settings, const std::string &statistics) +{ + UpdateCheckResult result; + { + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 0)) + { + throw CUpdateCheck::Cancel(); + } + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 10)) + { + throw CUpdateCheck::Cancel(); + } + bool loaded = false; + // try to load cached results before establishing any connection + if(context.loadPersisted) + { + try + { + InputFile f(settings.persistencePath + P_("update-") + mpt::PathString::FromUnicode(GetChannelName(settings.channel)) + P_(".json")); + if(f.IsValid()) + { + std::vector<std::byte> data = GetFileReader(f).ReadRawDataAsByteVector(); + nlohmann::json::parse(mpt::buffer_cast<std::string>(data)).get<Update::versions>(); + result.CheckTime = time_t{}; + result.json = data; + loaded = true; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } catch(const std::exception &) + { + // ignore + } + } + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 20)) + { + throw CUpdateCheck::Cancel(); + } + if(!loaded) + { + HTTP::InternetSession internet(Version::Current().GetOpenMPTVersionString()); + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 30)) + { + throw CUpdateCheck::Cancel(); + } + result = SearchUpdateModern(internet, settings); + try + { + mpt::SafeOutputFile f(settings.persistencePath + P_("update-") + mpt::PathString::FromUnicode(GetChannelName(settings.channel)) + P_(".json"), std::ios::binary); + f.stream().imbue(std::locale::classic()); + mpt::IO::WriteRaw(f.stream(), mpt::as_span(result.json)); + f.stream().flush(); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } catch(const std::exception &) + { + // ignore + } + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 50)) + { + throw CUpdateCheck::Cancel(); + } + SendStatistics(internet, settings, statistics); + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 70)) + { + throw CUpdateCheck::Cancel(); + } + } + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 90)) + { + throw CUpdateCheck::Cancel(); + } + CleanOldUpdates(settings, context); + if(!context.window->SendMessage(context.msgProgress, context.autoUpdate ? 1 : 0, 100)) + { + throw CUpdateCheck::Cancel(); + } + } + return result; +} + + +void CUpdateCheck::CleanOldUpdates(const CUpdateCheck::Settings & /* settings */ , const CUpdateCheck::Context & /* context */ ) +{ + mpt::PathString dirTemp = mpt::GetTempDirectory(); + if(dirTemp.empty()) + { + return; + } + if(PathIsRelative(dirTemp.AsNative().c_str())) + { + return; + } + if(!dirTemp.IsDirectory()) + { + return; + } + mpt::PathString dirTempOpenMPT = dirTemp + P_("OpenMPT") + mpt::PathString::FromNative(mpt::RawPathString(1, mpt::PathString::GetDefaultPathSeparator())); + mpt::PathString dirTempOpenMPTUpdates = dirTempOpenMPT + P_("Updates") + mpt::PathString::FromNative(mpt::RawPathString(1, mpt::PathString::GetDefaultPathSeparator())); + mpt::DeleteWholeDirectoryTree(dirTempOpenMPTUpdates); +} + + +void CUpdateCheck::SendStatistics(HTTP::InternetSession &internet, const CUpdateCheck::Settings &settings, const std::string &statistics) +{ + if(settings.sendStatistics) + { + HTTP::Request requestStatistics; + if(settings.statisticsUUID.IsValid()) + { + requestStatistics.SetURI(ParseURI(settings.apiURL + MPT_UFORMAT("statistics/{}")(settings.statisticsUUID))); + requestStatistics.method = HTTP::Method::Put; + } else + { + requestStatistics.SetURI(ParseURI(settings.apiURL + U_("statistics/"))); + requestStatistics.method = HTTP::Method::Post; + } + requestStatistics.dataMimeType = HTTP::MimeType::JSON(); + requestStatistics.acceptMimeTypes = HTTP::MimeTypes::JSON(); + std::string jsondata = statistics; + MPT_LOG_GLOBAL(LogInformation, "Update", mpt::ToUnicode(mpt::Charset::UTF8, jsondata)); + requestStatistics.data = mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(jsondata)); +#if defined(MPT_BUILD_RETRO) + requestSatistics.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + internet(requestStatistics); + } +} + + +UpdateCheckResult CUpdateCheck::SearchUpdateModern(HTTP::InternetSession &internet, const CUpdateCheck::Settings &settings) +{ + + HTTP::Request request; + request.SetURI(ParseURI(settings.apiURL + MPT_UFORMAT("update/{}")(GetChannelName(static_cast<UpdateChannel>(settings.channel))))); + request.method = HTTP::Method::Get; + request.acceptMimeTypes = HTTP::MimeTypes::JSON(); + request.flags = HTTP::NoCache; + +#if defined(MPT_BUILD_RETRO) + request.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + HTTP::Result resultHTTP = internet(request); + + // Retrieve HTTP status code. + if(resultHTTP.Status >= 400) + { + throw CUpdateCheck::Error(MPT_CFORMAT("Version information could not be found on the server (HTTP status code {}). Maybe your version of OpenMPT is too old!")(resultHTTP.Status)); + } + + // Now, evaluate the downloaded data. + UpdateCheckResult result; + result.CheckTime = time(nullptr); + try + { + nlohmann::json::parse(mpt::buffer_cast<std::string>(resultHTTP.Data)).get<Update::versions>(); + result.json = resultHTTP.Data; + } catch(mpt::out_of_memory e) + { + mpt::rethrow_out_of_memory(e); + } catch(const nlohmann::json::exception &e) + { + throw CUpdateCheck::Error(MPT_CFORMAT("Could not understand server response ({}). Maybe your version of OpenMPT is too old!")(mpt::get_exception_text<mpt::ustring>(e))); + } + + return result; + +} + + +void CUpdateCheck::CheckForUpdate(const CUpdateCheck::Settings &settings, const CUpdateCheck::Context &context) +{ + // incremented before starting the thread + MPT_ASSERT(s_InstanceCount.load() >= 1); + UpdateCheckResult result; + try + { + context.window->SendMessage(context.msgStart, context.autoUpdate ? 1 : 0, 0); + try + { + result = SearchUpdate(context, settings, context.statistics); + } catch(const bad_uri &e) + { + throw CUpdateCheck::Error(MPT_CFORMAT("Error parsing update URL: {}")(mpt::get_exception_text<mpt::ustring>(e))); + } catch(const HTTP::exception &e) + { + throw CUpdateCheck::Error(CString(_T("HTTP error: ")) + mpt::ToCString(e.GetMessage())); + } + } catch(const CUpdateCheck::Cancel &) + { + context.window->SendMessage(context.msgCanceled, context.autoUpdate ? 1 : 0, 0); + s_InstanceCount.fetch_sub(1); + MPT_ASSERT(s_InstanceCount.load() >= 0); + return; + } catch(const CUpdateCheck::Error &e) + { + context.window->SendMessage(context.msgFailure, context.autoUpdate ? 1 : 0, reinterpret_cast<LPARAM>(&e)); + s_InstanceCount.fetch_sub(1); + MPT_ASSERT(s_InstanceCount.load() >= 0); + return; + } + context.window->SendMessage(context.msgSuccess, context.autoUpdate ? 1 : 0, reinterpret_cast<LPARAM>(&result)); + s_InstanceCount.fetch_sub(1); + MPT_ASSERT(s_InstanceCount.load() >= 0); +} + + +bool CUpdateCheck::IsAutoUpdateFromMessage(WPARAM wparam, LPARAM /* lparam */ ) +{ + return wparam ? true : false; +} + + +const UpdateCheckResult &CUpdateCheck::MessageAsResult(WPARAM /* wparam */ , LPARAM lparam) +{ + return *reinterpret_cast<UpdateCheckResult *>(lparam); +} + + +const CUpdateCheck::Error &CUpdateCheck::MessageAsError(WPARAM /* wparam */ , LPARAM lparam) +{ + return *reinterpret_cast<CUpdateCheck::Error*>(lparam); +} + + + +static const char * const updateScript = R"vbs( + +Wscript.Echo +Wscript.Echo "OpenMPT portable Update" +Wscript.Echo "=======================" + +Wscript.Echo "[ 0%] Waiting for OpenMPT to close..." +WScript.Sleep 2000 + +Wscript.Echo "[ 10%] Loading update settings..." +zip = WScript.Arguments.Item(0) +subfolder = WScript.Arguments.Item(1) +dst = WScript.Arguments.Item(2) +restartbinary = WScript.Arguments.Item(3) + +Wscript.Echo "[ 20%] Preparing update..." +Set fso = CreateObject("Scripting.FileSystemObject") +Set shell = CreateObject("Wscript.Shell") +Set application = CreateObject("Shell.Application") + +Sub CreateFolder(pathname) + If Not fso.FolderExists(pathname) Then + fso.CreateFolder pathname + End If +End Sub + +Sub DeleteFolder(pathname) + If fso.FolderExists(pathname) Then + fso.DeleteFolder pathname + End If +End Sub + +Sub UnZIP(zipfilename, destinationfolder) + If Not fso.FolderExists(destinationfolder) Then + fso.CreateFolder(destinationfolder) + End If + application.NameSpace(destinationfolder).Copyhere application.NameSpace(zipfilename).Items, 16+256 +End Sub + +Wscript.Echo "[ 30%] Changing to temporary directory..." +shell.CurrentDirectory = fso.GetParentFolderName(WScript.ScriptFullName) + +Wscript.Echo "[ 40%] Decompressing update..." +UnZIP zip, fso.BuildPath(fso.GetAbsolutePathName("."), "tmp") + +Wscript.Echo "[ 60%] Installing update..." +If subfolder = "" Or subfolder = "." Then + fso.CopyFolder fso.BuildPath(fso.GetAbsolutePathName("."), "tmp"), dst, True +Else + fso.CopyFolder fso.BuildPath(fso.BuildPath(fso.GetAbsolutePathName("."), "tmp"), subfolder), dst, True +End If + +Wscript.Echo "[ 80%] Deleting temporary directory..." +DeleteFolder fso.BuildPath(fso.GetAbsolutePathName("."), "tmp") + +Wscript.Echo "[ 90%] Restarting OpenMPT..." +application.ShellExecute fso.BuildPath(dst, restartbinary), , dst, , 10 + +Wscript.Echo "[100%] Update successful!" +Wscript.Echo +WScript.Sleep 1000 + +Wscript.Echo "Closing update window in 5 seconds..." +WScript.Sleep 1000 +Wscript.Echo "Closing update window in 4 seconds..." +WScript.Sleep 1000 +Wscript.Echo "Closing update window in 3 seconds..." +WScript.Sleep 1000 +Wscript.Echo "Closing update window in 2 seconds..." +WScript.Sleep 1000 +Wscript.Echo "Closing update window in 1 seconds..." +WScript.Sleep 1000 +Wscript.Echo "Closing update window..." + +WScript.Quit + +)vbs"; + + + +class CDoUpdate: public CProgressDialog +{ +private: + Update::download download; + class Aborted : public std::exception {}; + class Warning : public std::exception + { + private: + mpt::ustring msg; + public: + Warning(const mpt::ustring &msg_) + : msg(msg_) + { + return; + } + mpt::ustring get_msg() const + { + return msg; + } + }; + class Error : public std::exception + { + private: + mpt::ustring msg; + public: + Error(const mpt::ustring &msg_) + : msg(msg_) + { + return; + } + mpt::ustring get_msg() const + { + return msg; + } + }; +public: + CDoUpdate(Update::download download, CWnd *parent = nullptr) + : CProgressDialog(parent) + , download(download) + { + return; + } + void UpdateProgress(const CString &text, double percent) + { + SetText(text); + SetProgress(static_cast<uint64>(percent * 100.0)); + ProcessMessages(); + if(m_abort) + { + throw Aborted(); + } + } + void Run() override + { + try + { + SetTitle(_T("OpenMPT Update")); + SetAbortText(_T("Cancel")); + SetText(_T("OpenMPT Update")); + SetRange(0, 10000); + ProcessMessages(); + + Update::downloadinfo downloadinfo; + mpt::PathString dirTempOpenMPTUpdates; + mpt::PathString updateFilename; + { + + UpdateProgress(_T("Connecting..."), 0.0); + HTTP::InternetSession internet(Version::Current().GetOpenMPTVersionString()); + + UpdateProgress(_T("Downloading update information..."), 1.0); + std::vector<std::byte> rawDownloadInfo; + { + HTTP::Request request; + request.SetURI(ParseURI(download.url)); + request.method = HTTP::Method::Get; + request.acceptMimeTypes = HTTP::MimeTypes::JSON(); +#if defined(MPT_BUILD_RETRO) + request.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + HTTP::Result resultHTTP = internet(request); + if(resultHTTP.Status != 200) + { + throw Error(MPT_UFORMAT("Error downloading update information: HTTP status {}.")(resultHTTP.Status)); + } + rawDownloadInfo = std::move(resultHTTP.Data); + } + + if(!TrackerSettings::Instance().UpdateSkipSignatureVerificationUNSECURE) + { + std::vector<std::byte> rawSignature; + UpdateProgress(_T("Retrieving update signature..."), 2.0); + { + HTTP::Request request; + request.SetURI(ParseURI(download.url + U_(".jws.json"))); + request.method = HTTP::Method::Get; + request.acceptMimeTypes = HTTP::MimeTypes::JSON(); +#if defined(MPT_BUILD_RETRO) + request.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + HTTP::Result resultHTTP = internet(request); + if(resultHTTP.Status != 200) + { + throw Error(MPT_UFORMAT("Error downloading update signature: HTTP status {}.")(resultHTTP.Status)); + } + rawSignature = std::move(resultHTTP.Data); + } + UpdateProgress(_T("Retrieving update signing public keys..."), 3.0); + std::vector<mpt::crypto::asymmetric::rsassa_pss<>::public_key> keys; + { + std::vector<mpt::ustring> keyAnchors = TrackerSettings::Instance().UpdateSigningKeysRootAnchors; + if(keyAnchors.empty()) + { + Reporting::Warning(U_("Warning: No update signing public key root anchors configured. Update cannot be verified."), U_("OpenMPT Update")); + } + for(const auto & keyAnchor : keyAnchors) + { + HTTP::Request request; + request.SetURI(ParseURI(keyAnchor + U_("signingkeys.jwkset.json"))); + request.method = HTTP::Method::Get; + request.flags = HTTP::NoCache; + request.acceptMimeTypes = HTTP::MimeTypes::JSON(); + try + { +#if defined(MPT_BUILD_RETRO) + request.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + HTTP::Result resultHTTP = internet(request); + resultHTTP.CheckStatus(200); + mpt::append(keys, mpt::crypto::asymmetric::rsassa_pss<>::parse_jwk_set(mpt::ToUnicode(mpt::Charset::UTF8, mpt::buffer_cast<std::string>(resultHTTP.Data)))); + } catch(mpt::out_of_memory e) + { + mpt::rethrow_out_of_memory(e); + } catch(const std::exception &e) + { + Reporting::Warning(MPT_UFORMAT("Warning: Retrieving update signing public keys from {} failed: {}")(keyAnchor, mpt::get_exception_text<mpt::ustring>(e)), U_("OpenMPT Update")); + } catch(...) + { + Reporting::Warning(MPT_UFORMAT("Warning: Retrieving update signing public keys from {} failed.")(keyAnchor), U_("OpenMPT Update")); + } + } + } + if(keys.empty()) + { + throw Error(U_("Error retrieving update signing public keys.")); + } + UpdateProgress(_T("Verifying signature..."), 4.0); + std::vector<std::byte> expectedPayload = mpt::buffer_cast<std::vector<std::byte>>(rawDownloadInfo); + mpt::ustring signature = mpt::ToUnicode(mpt::Charset::UTF8, mpt::buffer_cast<std::string>(rawSignature)); + + mpt::crypto::asymmetric::rsassa_pss<>::jws_verify_at_least_one(keys, expectedPayload, signature); + + } + + UpdateProgress(_T("Parsing update information..."), 5.0); + try + { + downloadinfo = nlohmann::json::parse(mpt::buffer_cast<std::string>(rawDownloadInfo)).get<Update::downloadinfo>(); + } catch(const nlohmann::json::exception &e) + { + throw Error(MPT_UFORMAT("Error parsing update information: {}.")(mpt::get_exception_text<mpt::ustring>(e))); + } + + UpdateProgress(_T("Preparing download..."), 6.0); + mpt::PathString dirTemp = mpt::GetTempDirectory(); + mpt::PathString dirTempOpenMPT = dirTemp + P_("OpenMPT") + mpt::PathString::FromNative(mpt::RawPathString(1, mpt::PathString::GetDefaultPathSeparator())); + dirTempOpenMPTUpdates = dirTempOpenMPT + P_("Updates") + mpt::PathString::FromNative(mpt::RawPathString(1, mpt::PathString::GetDefaultPathSeparator())); + updateFilename = dirTempOpenMPTUpdates + mpt::PathString::FromUnicode(downloadinfo.filename); + ::CreateDirectory(dirTempOpenMPT.AsNativePrefixed().c_str(), NULL); + ::CreateDirectory(dirTempOpenMPTUpdates.AsNativePrefixed().c_str(), NULL); + + { + + UpdateProgress(_T("Creating file..."), 7.0); + mpt::SafeOutputFile file(updateFilename, std::ios::binary); + file.stream().imbue(std::locale::classic()); + file.stream().exceptions(std::ios::failbit | std::ios::badbit); + + UpdateProgress(_T("Downloading update..."), 8.0); + HTTP::Request request; + request.SetURI(ParseURI(downloadinfo.url)); + request.method = HTTP::Method::Get; + request.acceptMimeTypes = HTTP::MimeTypes::Binary(); + request.outputStream = &file.stream(); + request.progressCallback = [&](HTTP::Progress progress, uint64 transferred, std::optional<uint64> expectedSize) { + switch(progress) + { + case HTTP::Progress::Start: + SetProgress(900); + break; + case HTTP::Progress::ConnectionEstablished: + SetProgress(1000); + break; + case HTTP::Progress::RequestOpened: + SetProgress(1100); + break; + case HTTP::Progress::RequestSent: + SetProgress(1200); + break; + case HTTP::Progress::ResponseReceived: + SetProgress(1300); + break; + case HTTP::Progress::TransferBegin: + SetProgress(1400); + break; + case HTTP::Progress::TransferRunning: + if(expectedSize && ((*expectedSize) != 0)) + { + SetProgress(static_cast<int64>((static_cast<double>(transferred) / static_cast<double>(*expectedSize)) * (10000.0-1500.0-400.0) + 1500.0)); + } else + { + SetProgress((1500 + 9600) / 2); + } + break; + case HTTP::Progress::TransferDone: + SetProgress(9600); + break; + } + ProcessMessages(); + if(m_abort) + { + throw HTTP::Abort(); + } + }; +#if defined(MPT_BUILD_RETRO) + request.InsecureTLSDowngradeWindowsXP(); +#endif // MPT_BUILD_RETRO + HTTP::Result resultHTTP = internet(request); + if(resultHTTP.Status != 200) + { + throw Error(MPT_UFORMAT("Error downloading update: HTTP status {}.")(resultHTTP.Status)); + } + } + + UpdateProgress(_T("Disconnecting..."), 97.0); + } + + UpdateProgress(_T("Verifying download..."), 98.0); + bool verified = false; + for(const auto & [algorithm, value] : downloadinfo.checksums) + { + if(algorithm == U_("SHA-512")) + { + std::vector<std::byte> binhash = mpt::decode_hex(value); + if(binhash.size() != 512/8) + { + throw Error(U_("Download verification failed.")); + } + std::array<std::byte, 512/8> expected; + std::copy(binhash.begin(), binhash.end(), expected.begin()); + mpt::crypto::hash::SHA512 hash; + mpt::ifstream f(updateFilename, std::ios::binary); + f.imbue(std::locale::classic()); + f.exceptions(std::ios::badbit); + while(!mpt::IO::IsEof(f)) + { + std::array<std::byte, mpt::IO::BUFFERSIZE_TINY> buf; + hash.process(mpt::IO::ReadRaw(f, mpt::as_span(buf))); + } + std::array<std::byte, 512/8> gotten = hash.result(); + if(gotten != expected) + { + throw Error(U_("Download verification failed.")); + } + verified = true; + } + } + if(!verified) + { + throw Error(U_("Error verifying update: No suitable checksum found.")); + } + + UpdateProgress(_T("Installing update..."), 99.0); + bool wantClose = false; + if(download.can_autoupdate && (Version::Current() >= Version::Parse(download.autoupdate_minversion))) + { + if(download.type == U_("installer") && downloadinfo.autoupdate_installer) + { + if(theApp.IsSourceTreeMode()) + { + throw Warning(MPT_UFORMAT("Refusing to launch update '{} {}' when running from source tree.")(updateFilename, mpt::String::Combine(downloadinfo.autoupdate_installer->arguments, U_(" ")))); + } + if(reinterpret_cast<INT_PTR>(ShellExecute(NULL, NULL, + updateFilename.AsNative().c_str(), + mpt::ToWin(mpt::String::Combine(downloadinfo.autoupdate_installer->arguments, U_(" "))).c_str(), + dirTempOpenMPTUpdates.AsNative().c_str(), + SW_SHOWDEFAULT)) < 32) + { + throw Error(U_("Error launching update.")); + } + } else if(download.type == U_("archive") && downloadinfo.autoupdate_archive) + { + try + { + mpt::SafeOutputFile file(dirTempOpenMPTUpdates + P_("update.vbs"), std::ios::binary); + file.stream().imbue(std::locale::classic()); + file.stream().exceptions(std::ios::failbit | std::ios::badbit); + mpt::IO::WriteRaw(file.stream(), mpt::as_span(std::string(updateScript))); + } catch(...) + { + throw Error(U_("Error creating update script.")); + } + std::vector<mpt::ustring> arguments; + arguments.push_back(U_("\"") + (dirTempOpenMPTUpdates + P_("update.vbs")).ToUnicode() + U_("\"")); + arguments.push_back(U_("\"") + updateFilename.ToUnicode() + U_("\"")); + arguments.push_back(U_("\"") + (downloadinfo.autoupdate_archive->subfolder.empty() ? U_(".") : downloadinfo.autoupdate_archive->subfolder) + U_("\"")); + arguments.push_back(U_("\"") + theApp.GetInstallPath().WithoutTrailingSlash().ToUnicode() + U_("\"")); + arguments.push_back(U_("\"") + downloadinfo.autoupdate_archive->restartbinary + U_("\"")); + if(theApp.IsSourceTreeMode()) + { + throw Warning(MPT_UFORMAT("Refusing to launch update '{} {}' when running from source tree.")(P_("cscript.exe"), mpt::String::Combine(arguments, U_(" ")))); + } + if(reinterpret_cast<INT_PTR>(ShellExecute(NULL, NULL, + P_("cscript.exe").AsNative().c_str(), + mpt::ToWin(mpt::String::Combine(arguments, U_(" "))).c_str(), + dirTempOpenMPTUpdates.AsNative().c_str(), + SW_SHOWDEFAULT)) < 32) + { + throw Error(U_("Error launching update.")); + } + wantClose = true; + } else + { + CTrackApp::OpenDirectory(dirTempOpenMPTUpdates); + wantClose = true; + } + } else + { + CTrackApp::OpenDirectory(dirTempOpenMPTUpdates); + wantClose = true; + } + UpdateProgress(_T("Waiting for installer..."), 100.0); + if(wantClose) + { + CMainFrame::GetMainFrame()->PostMessage(WM_QUIT, 0, 0); + } + EndDialog(IDOK); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + Reporting::Error(U_("Not enough memory to install update."), U_("OpenMPT Update Error")); + EndDialog(IDCANCEL); + return; + } catch(const HTTP::Abort &) + { + EndDialog(IDCANCEL); + return; + } catch(const Aborted &) + { + EndDialog(IDCANCEL); + return; + } catch(const Warning &e) + { + Reporting::Warning(e.get_msg(), U_("OpenMPT Update")); + EndDialog(IDCANCEL); + return; + } catch(const Error &e) + { + Reporting::Error(e.get_msg(), U_("OpenMPT Update Error")); + EndDialog(IDCANCEL); + return; + } catch(const std::exception &e) + { + Reporting::Error(MPT_UFORMAT("Error installing update: {}")(mpt::get_exception_text<mpt::ustring>(e)), U_("OpenMPT Update Error")); + EndDialog(IDCANCEL); + return; + } catch(...) + { + Reporting::Error(U_("Error installing update."), U_("OpenMPT Update Error")); + EndDialog(IDCANCEL); + return; + } + } +}; + + +void CUpdateCheck::AcknowledgeSuccess(const UpdateCheckResult &result) +{ + if(!result.IsFromCache()) + { + TrackerSettings::Instance().UpdateLastUpdateCheck = mpt::Date::Unix(result.CheckTime); + } +} + + +void CUpdateCheck::ShowSuccessGUI(const bool autoUpdate, const UpdateCheckResult &result) +{ + bool modal = !autoUpdate; + + Update::versions updateData = nlohmann::json::parse(mpt::buffer_cast<std::string>(result.json)).get<Update::versions>(); + UpdateInfo updateInfo = GetBestDownload(updateData); + + if(!updateInfo.IsAvailable()) + { + if(modal) + { + Reporting::Information(U_("You already have the latest version of OpenMPT installed."), U_("OpenMPT Update")); + } + return; + } + + auto &versionInfo = updateData[updateInfo.version]; + if(autoUpdate && (mpt::ToCString(versionInfo.version) == TrackerSettings::Instance().UpdateIgnoreVersion)) + { + return; + } + + if(autoUpdate && TrackerSettings::Instance().UpdateInstallAutomatically && !updateInfo.download.empty() && versionInfo.downloads[updateInfo.download].can_autoupdate && (Version::Current() >= Version::Parse(versionInfo.downloads[updateInfo.download].autoupdate_minversion))) + { + CDoUpdate updateDlg(versionInfo.downloads[updateInfo.download], theApp.GetMainWnd()); + if(updateDlg.DoModal() != IDOK) + { + return; + } + } else + { + const TCHAR *action = _T("&View Announcement"); + const bool canInstall = mpt::OS::Windows::IsOriginal() && !updateInfo.download.empty() && versionInfo.downloads[updateInfo.download].can_autoupdate && (Version::Current() >= Version::Parse(versionInfo.downloads[updateInfo.download].autoupdate_minversion)); + const bool canDownload = !canInstall && !updateInfo.download.empty() && !versionInfo.downloads[updateInfo.download].download_url.empty(); + if(canInstall) + { + action = _T("&Install Now"); + } else if(canDownload) + { + action = _T("&Download Now"); + } + + // always show indicator, do not highlight it with a tooltip if we show a modal window later or when it is a cached result + if(!CMainFrame::GetMainFrame()->ShowUpdateIndicator(result, mpt::ToCString(versionInfo.version), mpt::ToCString(versionInfo.announcement_url), !modal && !result.IsFromCache())) + { + // on failure to show indicator, continue and show modal dialog + modal = true; + } + + if(!modal) + { + return; + } + + UpdateDialog dlg( + mpt::ToCString(versionInfo.version), + mpt::ToCString(versionInfo.date), + mpt::ToCString(versionInfo.announcement_url), + action); + if(dlg.DoModal() != IDOK) + { + return; + } + + if(canInstall) + { + CDoUpdate updateDlg(versionInfo.downloads[updateInfo.download], theApp.GetMainWnd()); + if(updateDlg.DoModal() != IDOK) + { + return; + } + } else if(canDownload) + { + CTrackApp::OpenURL(versionInfo.downloads[updateInfo.download].download_url); + } else + { + CTrackApp::OpenURL(versionInfo.announcement_url); + } + } +} + + +void CUpdateCheck::ShowFailureGUI(const bool autoUpdate, const CUpdateCheck::Error &error) +{ + if(!autoUpdate) + { + Reporting::Error(error.GetMessage(), _T("OpenMPT Update Error")); + } +} + + +CUpdateCheck::Error::Error(CString errorMessage) + : std::runtime_error(mpt::ToCharset(mpt::CharsetException, errorMessage)) + , m_Message(errorMessage) +{ + return; +} + + +CUpdateCheck::Error::Error(CString errorMessage, DWORD errorCode) + : std::runtime_error(mpt::ToCharset(mpt::CharsetException, FormatErrorCode(errorMessage, errorCode))) + , m_Message(errorMessage) +{ + return; +} + + +CString CUpdateCheck::Error::GetMessage() const +{ + return m_Message; +} + + +CString CUpdateCheck::Error::FormatErrorCode(CString errorMessage, DWORD errorCode) +{ + errorMessage += mpt::ToCString(mpt::windows::GetErrorMessage(errorCode, GetModuleHandle(TEXT("wininet.dll")))); + return errorMessage; +} + + + +CUpdateCheck::Cancel::Cancel() +{ + return; +} + + +///////////////////////////////////////////////////////////// +// CUpdateSetupDlg + +BEGIN_MESSAGE_MAP(CUpdateSetupDlg, CPropertyPage) + ON_COMMAND(IDC_CHECK_UPDATEENABLED, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_RADIO1, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_RADIO2, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_RADIO3, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_BUTTON1, &CUpdateSetupDlg::OnCheckNow) + ON_CBN_SELCHANGE(IDC_COMBO_UPDATEFREQUENCY, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK_UPDATEINSTALLAUTOMATICALLY, &CUpdateSetupDlg::OnSettingsChanged) + ON_COMMAND(IDC_CHECK1, &CUpdateSetupDlg::OnSettingsChanged) + ON_NOTIFY(NM_CLICK, IDC_SYSLINK1, &CUpdateSetupDlg::OnShowStatisticsData) +END_MESSAGE_MAP() + + +CUpdateSetupDlg::CUpdateSetupDlg() + : CPropertyPage(IDD_OPTIONS_UPDATE) + , m_SettingChangedNotifyGuard(theApp.GetSettings(), TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath()) +{ + return; +} + + +void CUpdateSetupDlg::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO_UPDATEFREQUENCY, m_CbnUpdateFrequency); +} + + +BOOL CUpdateSetupDlg::OnInitDialog() +{ + CPropertyPage::OnInitDialog(); + + CheckDlgButton(IDC_CHECK_UPDATEENABLED, TrackerSettings::Instance().UpdateEnabled ? BST_CHECKED : BST_UNCHECKED); + + int radioID = 0; + uint32 updateChannel = TrackerSettings::Instance().UpdateChannel; + if(updateChannel == UpdateChannelRelease) + { + radioID = IDC_RADIO1; + } else if(updateChannel == UpdateChannelNext) + { + radioID = IDC_RADIO2; + } else if(updateChannel == UpdateChannelDevelopment) + { + radioID = IDC_RADIO3; + } else + { + radioID = IDC_RADIO1; + } + CheckRadioButton(IDC_RADIO1, IDC_RADIO3, radioID); + + int32 periodDays = TrackerSettings::Instance().UpdateIntervalDays; + int ndx; + + ndx = m_CbnUpdateFrequency.AddString(_T("always")); + m_CbnUpdateFrequency.SetItemData(ndx, 0); + if(periodDays >= 0) + { + m_CbnUpdateFrequency.SetCurSel(ndx); + } + + ndx = m_CbnUpdateFrequency.AddString(_T("daily")); + m_CbnUpdateFrequency.SetItemData(ndx, 1); + if(periodDays >= 1) + { + m_CbnUpdateFrequency.SetCurSel(ndx); + } + + ndx = m_CbnUpdateFrequency.AddString(_T("weekly")); + m_CbnUpdateFrequency.SetItemData(ndx, 7); + if(periodDays >= 7) + { + m_CbnUpdateFrequency.SetCurSel(ndx); + } + + ndx = m_CbnUpdateFrequency.AddString(_T("monthly")); + m_CbnUpdateFrequency.SetItemData(ndx, 30); + if(periodDays >= 30) + { + m_CbnUpdateFrequency.SetCurSel(ndx); + } + + ndx = m_CbnUpdateFrequency.AddString(_T("never")); + m_CbnUpdateFrequency.SetItemData(ndx, ~(DWORD_PTR)0); + if(periodDays < 0) + { + m_CbnUpdateFrequency.SetCurSel(ndx); + } + + CheckDlgButton(IDC_CHECK_UPDATEINSTALLAUTOMATICALLY, TrackerSettings::Instance().UpdateInstallAutomatically ? BST_CHECKED : BST_UNCHECKED); + + CheckDlgButton(IDC_CHECK1, TrackerSettings::Instance().UpdateStatistics ? BST_CHECKED : BST_UNCHECKED); + + GetDlgItem(IDC_STATIC_UPDATEPRIVACYTEXT)->SetWindowText(mpt::ToCString(CUpdateCheck::GetStatisticsUserInformation(true))); + + EnableDisableDialog(); + + m_SettingChangedNotifyGuard.Register(this); + SettingChanged(TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath()); + + return TRUE; +} + + +void CUpdateSetupDlg::OnShowStatisticsData(NMHDR * /*pNMHDR*/, LRESULT * /*pResult*/) +{ + CUpdateCheck::Settings settings; + + uint32 updateChannel = TrackerSettings::Instance().UpdateChannel; + const int channelRadio = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3); + if(channelRadio == IDC_RADIO1) updateChannel = UpdateChannelRelease; + if(channelRadio == IDC_RADIO2) updateChannel = UpdateChannelNext; + if(channelRadio == IDC_RADIO3) updateChannel = UpdateChannelDevelopment; + + int updateCheckPeriod = (m_CbnUpdateFrequency.GetItemData(m_CbnUpdateFrequency.GetCurSel()) == ~(DWORD_PTR)0) ? -1 : static_cast<int>(m_CbnUpdateFrequency.GetItemData(m_CbnUpdateFrequency.GetCurSel())); + + settings.periodDays = updateCheckPeriod; + settings.channel = static_cast<UpdateChannel>(updateChannel); + settings.sendStatistics = (IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED); + + mpt::ustring statistics; + + statistics += U_("Update:") + UL_("\n"); + statistics += UL_("\n"); + + { + statistics += U_("GET ") + settings.apiURL + MPT_UFORMAT("update/{}")(GetChannelName(static_cast<UpdateChannel>(settings.channel))) + UL_("\n"); + statistics += UL_("\n"); + std::vector<mpt::ustring> keyAnchors = TrackerSettings::Instance().UpdateSigningKeysRootAnchors; + for(const auto & keyAnchor : keyAnchors) + { + statistics += U_("GET ") + keyAnchor + U_("signingkeys.jwkset.json") + UL_("\n"); + statistics += UL_("\n"); + } + } + + if(settings.sendStatistics) + { + statistics += U_("Statistics:") + UL_("\n"); + statistics += UL_("\n"); + if(settings.statisticsUUID.IsValid()) + { + statistics += U_("PUT ") + settings.apiURL + MPT_UFORMAT("statistics/{}")(settings.statisticsUUID) + UL_("\n"); + } else + { + statistics += U_("POST ") + settings.apiURL + U_("statistics/") + UL_("\n"); + } + statistics += mpt::String::Replace(mpt::ToUnicode(mpt::Charset::UTF8, CUpdateCheck::GetStatisticsDataV3(settings)), U_("\t"), U_(" ")); + } + + InfoDialog dlg(this); + dlg.SetCaption(_T("Update Statistics Data")); + dlg.SetContent(mpt::ToWin(mpt::String::Replace(statistics, U_("\n"), U_("\r\n")))); + dlg.DoModal(); +} + + +void CUpdateSetupDlg::SettingChanged(const SettingPath &changedPath) +{ + if(changedPath == TrackerSettings::Instance().UpdateLastUpdateCheck.GetPath()) + { + CString updateText; + const time_t t = TrackerSettings::Instance().UpdateLastUpdateCheck.Get(); + if(t > 0) + { + const tm* const lastUpdate = localtime(&t); + if(lastUpdate != nullptr) + { + updateText.Format(_T("The last successful update check was run on %04d-%02d-%02d, %02d:%02d."), lastUpdate->tm_year + 1900, lastUpdate->tm_mon + 1, lastUpdate->tm_mday, lastUpdate->tm_hour, lastUpdate->tm_min); + } + } + updateText += _T("\r\n"); + SetDlgItemText(IDC_LASTUPDATE, updateText); + } +} + + +void CUpdateSetupDlg::EnableDisableDialog() +{ + + BOOL status = ((IsDlgButtonChecked(IDC_CHECK_UPDATEENABLED) != BST_UNCHECKED) ? TRUE : FALSE); + + GetDlgItem(IDC_STATIC_UDATECHANNEL)->EnableWindow(status); + GetDlgItem(IDC_RADIO1)->EnableWindow(status); + GetDlgItem(IDC_RADIO2)->EnableWindow(status); + GetDlgItem(IDC_RADIO3)->EnableWindow(status); + + GetDlgItem(IDC_STATIC_UPDATECHECK)->EnableWindow(status); + GetDlgItem(IDC_STATIC_UPDATEFREQUENCY)->EnableWindow(status); + GetDlgItem(IDC_COMBO_UPDATEFREQUENCY)->EnableWindow(status); + GetDlgItem(IDC_BUTTON1)->EnableWindow(status); + GetDlgItem(IDC_LASTUPDATE)->EnableWindow(status); + GetDlgItem(IDC_CHECK_UPDATEINSTALLAUTOMATICALLY)->EnableWindow(status); + + GetDlgItem(IDC_STATIC_UPDATEPRIVACY)->EnableWindow(status); + GetDlgItem(IDC_CHECK1)->EnableWindow(status); + GetDlgItem(IDC_STATIC_UPDATEPRIVACYTEXT)->EnableWindow(status); + GetDlgItem(IDC_SYSLINK1)->EnableWindow(status); +} + + +void CUpdateSetupDlg::OnSettingsChanged() +{ + EnableDisableDialog(); + SetModified(TRUE); +} + + +void CUpdateSetupDlg::OnOK() +{ + uint32 updateChannel = TrackerSettings::Instance().UpdateChannel; + const int channelRadio = GetCheckedRadioButton(IDC_RADIO1, IDC_RADIO3); + if(channelRadio == IDC_RADIO1) updateChannel = UpdateChannelRelease; + if(channelRadio == IDC_RADIO2) updateChannel = UpdateChannelNext; + if(channelRadio == IDC_RADIO3) updateChannel = UpdateChannelDevelopment; + + int updateCheckPeriod = (m_CbnUpdateFrequency.GetItemData(m_CbnUpdateFrequency.GetCurSel()) == ~(DWORD_PTR)0) ? -1 : static_cast<int>(m_CbnUpdateFrequency.GetItemData(m_CbnUpdateFrequency.GetCurSel())); + + TrackerSettings::Instance().UpdateEnabled = (IsDlgButtonChecked(IDC_CHECK_UPDATEENABLED) != BST_UNCHECKED); + + TrackerSettings::Instance().UpdateIntervalDays = updateCheckPeriod; + TrackerSettings::Instance().UpdateInstallAutomatically = (IsDlgButtonChecked(IDC_CHECK_UPDATEINSTALLAUTOMATICALLY) != BST_UNCHECKED); + TrackerSettings::Instance().UpdateChannel = updateChannel; + TrackerSettings::Instance().UpdateStatistics = (IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED); + + CPropertyPage::OnOK(); +} + + +BOOL CUpdateSetupDlg::OnSetActive() +{ + CMainFrame::m_nLastOptionsPage = OPTIONS_PAGE_UPDATE; + return CPropertyPage::OnSetActive(); +} + + +void CUpdateSetupDlg::OnCheckNow() +{ + CUpdateCheck::DoManualUpdateCheck(); +} + + +#endif // MPT_ENABLE_UPDATE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.h b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.h new file mode 100644 index 00000000..337be2c2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateCheck.h @@ -0,0 +1,185 @@ +/* + * UpdateCheck.h + * ------------- + * Purpose: Class for easy software update check. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/uuid/uuid.hpp" + +#include <time.h> + +#include <atomic> + +#include "resource.h" +#include "Settings.h" + +OPENMPT_NAMESPACE_BEGIN + + +#if defined(MPT_ENABLE_UPDATE) + + +namespace HTTP { +class InternetSession; +} + +enum UpdateChannel : uint32 +{ + UpdateChannelRelease = 1, + UpdateChannelNext = 2, + UpdateChannelDevelopment = 3, +}; + +struct UpdateCheckResult +{ + time_t CheckTime = time_t{}; + std::vector<std::byte> json; + bool IsFromCache() const noexcept { return CheckTime == time_t{}; } +}; + +class CUpdateCheck +{ + +private: + + static std::atomic<int32> s_InstanceCount; + +public: + + static mpt::ustring GetStatisticsUserInformation(bool shortText); + + static std::vector<mpt::ustring> GetDefaultUpdateSigningKeysRootAnchors(); + static mpt::ustring GetDefaultAPIURL(); + + int32 GetNumCurrentRunningInstances(); + + static bool IsSuitableUpdateMoment(); + + static void DoAutoUpdateCheck() { StartUpdateCheckAsync(true); } + static void DoManualUpdateCheck() { StartUpdateCheckAsync(false); } + +public: + + struct Context + { + CWnd *window; + UINT msgStart; + UINT msgProgress; + UINT msgSuccess; + UINT msgFailure; + UINT msgCanceled; + bool autoUpdate; + bool loadPersisted; + std::string statistics; + }; + + struct Settings + { + int32 periodDays; + UpdateChannel channel; + mpt::PathString persistencePath; + mpt::ustring apiURL; + bool sendStatistics; + mpt::UUID statisticsUUID; + Settings(); + }; + + class Error + : public std::runtime_error + { + private: + CString m_Message; + public: + Error(CString errorMessage); + Error(CString errorMessage, DWORD errorCode); + CString GetMessage() const; + protected: + static CString FormatErrorCode(CString errorMessage, DWORD errorCode); + }; + + class Cancel + : public std::exception + { + public: + Cancel(); + }; + + static bool IsAutoUpdateFromMessage(WPARAM wparam, LPARAM lparam); + + static const UpdateCheckResult &MessageAsResult(WPARAM wparam, LPARAM lparam); + static const CUpdateCheck::Error &MessageAsError(WPARAM wparam, LPARAM lparam); + + static void AcknowledgeSuccess(const UpdateCheckResult &result); + + static void ShowSuccessGUI(const bool autoUpdate, const UpdateCheckResult &result); + static void ShowFailureGUI(const bool autoUpdate, const CUpdateCheck::Error &error); + +public: + + // v3 + static std::string GetStatisticsDataV3(const Settings &settings); // UTF8 + +protected: + + static void StartUpdateCheckAsync(bool autoUpdate); + + struct ThreadFunc + { + CUpdateCheck::Settings settings; + CUpdateCheck::Context context; + ThreadFunc(const CUpdateCheck::Settings &settings, const CUpdateCheck::Context &context); + void operator () (); + }; + + static void CheckForUpdate(const CUpdateCheck::Settings &settings, const CUpdateCheck::Context &context); + + static UpdateCheckResult SearchUpdate(const CUpdateCheck::Context &context, const CUpdateCheck::Settings &settings, const std::string &statistics); // may throw + + static void CleanOldUpdates(const CUpdateCheck::Settings &settings, const CUpdateCheck::Context &context); + + static void SendStatistics(HTTP::InternetSession &internet, const CUpdateCheck::Settings &settings, const std::string &statistics); // may throw + + static UpdateCheckResult SearchUpdateModern(HTTP::InternetSession &internet, const CUpdateCheck::Settings &settings); // may throw + +}; + + +class CUpdateSetupDlg: public CPropertyPage + , public ISettingChanged +{ +public: + CUpdateSetupDlg(); + +public: + void SettingChanged(const SettingPath &changedPath) override; + +protected: + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + BOOL OnSetActive() override; + + afx_msg void OnSettingsChanged(); + afx_msg void OnCheckNow(); + afx_msg void OnShowStatisticsData(NMHDR * /*pNMHDR*/, LRESULT * /*pResult*/); + void EnableDisableDialog(); + DECLARE_MESSAGE_MAP() + +private: + SettingChangedNotifyGuard m_SettingChangedNotifyGuard; + CComboBox m_CbnUpdateFrequency; +}; + + +#endif // MPT_ENABLE_UPDATE + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/UpdateHints.h b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateHints.h new file mode 100644 index 00000000..118ac5b7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateHints.h @@ -0,0 +1,214 @@ +/* + * UpdateHints.h + * ------------- + * Purpose: Hint type and abstraction class for passing around hints between module views. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/base/FlagSet.hpp" +#include "../soundlib/Snd_defs.h" + +OPENMPT_NAMESPACE_BEGIN + +// Mutually exclusive hint categories +enum HintCategory +{ + HINTCAT_GLOBAL = 0, // Not a real category, since all other categories can be combined with this + HINTCAT_GENERAL = 0, + HINTCAT_PATTERNS = 1, + HINTCAT_SAMPLES = 2, + HINTCAT_INSTRUMENTS = 3, + HINTCAT_SEQUENCE = 4, + HINTCAT_PLUGINS = 5, + HINTCAT_COMMENTS = 6, + + NUM_HINTCATS +}; + +enum HintType +{ + // Hints that can be combined with any other hints (no parameter) + HINT_NONE = 0x00, // No specific hint + HINT_MODTYPE = 0x01, // Module type has changed. Generally this will force most things to update. + HINT_MPTOPTIONS = 0x02, // Some OpenMPT options (e.g. colours) have changed which might require stuff to be redrawn. + HINT_UNDO = 0x04, // Undo state information has changed + + HINT_ALLGLOBAL = HINT_MODTYPE | HINT_MPTOPTIONS | HINT_UNDO, + + // From here: Mutually exclusive hint categories + + // General module setting hints (GeneralHint) + HINT_MODGENERAL = 0x10, // General global module settings have changed + HINT_MODCHANNELS = 0x20, // Module channel settings have changed (e.g. channel volume). Parameter: Channel ID + HINT_TUNINGS = 0x40, // Tuning collection was updated + // Pattern-specific hints (PatternHint) + HINT_PATTERNDATA = 0x10, // Pattern data has changed. Parameter: Pattern ID (0 = all patterns) + HINT_PATTERNROW = 0x20, // A row of the currently edited pattern has changed. Parameter: Row number + HINT_PATNAMES = 0x40, // Pattern names have changed. Parameter: Pattern ID (0 = all patterns) + // Sample-specific hints (SampleHint) + HINT_SAMPLEINFO = 0x10, // Sample properties have changed. Parameter: Sample ID (0 = all samples) + HINT_SAMPLEDATA = 0x20, // Sample waveform has changed. Parameter: Sample ID (0 = all samples) + HINT_SMPNAMES = 0x40, // Sample name has changed. Parameter: Sample ID (0 = all samples) + // Instrument-specific hints (InstrumentHint) + HINT_INSTRUMENT = 0x10, // Instrument properties have changed. Parameter: Instrument ID (0 = all instruments) + HINT_ENVELOPE = 0x20, // An instrument envelope has changed. Parameter: Instrument ID (0 = all instruments) + HINT_INSNAMES = 0x40, // Instrument name has changed. Parameter: Instrument ID (0 = all instruments) + // Sequence-specific hints (SequenceHint) + HINT_MODSEQUENCE = 0x10, // The pattern sequence has changed. + HINT_SEQNAMES = 0x20, // Sequence names have changed. Parameter: Sequence ID (0 = all sequences) + HINT_RESTARTPOS = 0x40, // Restart position has changed. Parameter: Sequence ID (0 = all sequences) + // Plugin-specific hints (PluginHint) + HINT_MIXPLUGINS = 0x10, // Plugin properties have changed. Parameter: Plugin ID (0 = all plugins, 1 = first plugin) + HINT_PLUGINNAMES = 0x20, // Plugin names have changed. Parameter: Plugin ID (0 = all plugins, 1 = first plugin) + HINT_PLUGINPARAM = 0x40, // Plugin parameter has changed. Parameter: Plugin ID (0 = all plugins, 1 = first plugin) + // Comment text hints (CommentHint) + HINT_MODCOMMENTS = 0x10, // Module comment text has changed +}; +DECLARE_FLAGSET(HintType) + +struct UpdateHint +{ +protected: + using store_t = uint32; + union + { + struct + { + store_t type : 7; // All HintType flags must fit into this. + store_t category : 3; // All HintCategory types must fit into this. + store_t item : 22; + }; + store_t rawData; + }; + + UpdateHint(HintCategory category_, store_t item_ = 0) : type(HINT_NONE), category(category_), item(item_) + { + static_assert(sizeof(UpdateHint) == sizeof(store_t), "Internal UpdateHint size inconsistency"); + static_assert(sizeof(UpdateHint) <= sizeof(LPARAM), "Update hints are currently tunnelled through LPARAMs in MFC"); + MPT_ASSERT(static_cast<HintCategory>(category) == category_); + MPT_ASSERT(UpdateHint::item == item); + } + + template<typename T> + MPT_FORCEINLINE T GetData() const { return static_cast<T>(item); } + +public: + UpdateHint() : type(HINT_NONE), category(HINTCAT_GLOBAL), item(0) { } + + template<typename T> + MPT_FORCEINLINE UpdateHint &SetData(T i) { item = i; MPT_ASSERT(item == i); return *this; } + + MPT_FORCEINLINE HintCategory GetCategory() const { return static_cast<HintCategory>(category); } + MPT_FORCEINLINE FlagSet<HintType> GetType() const { return FlagSet<HintType>(static_cast<FlagSet<HintType>::store_type>(type)); } + + // CModDoc hint tunnelling + static MPT_FORCEINLINE UpdateHint FromLPARAM(LPARAM rawData) { UpdateHint hint; hint.rawData = static_cast<store_t>(rawData); return hint; } + MPT_FORCEINLINE LPARAM AsLPARAM() const { return rawData; } + + // Discard any hints that don't belong to class T. + template<typename T> + MPT_FORCEINLINE T ToType() const + { + T hint = static_cast<const T &>(*this); + if(T::classCategory != static_cast<HintCategory>(category)) + { + hint.type &= HINT_ALLGLOBAL; + hint.item = 0; + } + return hint; + } + + // Set global hint flags + MPT_FORCEINLINE UpdateHint &ModType() { type |= HINT_MODTYPE; return *this; } + MPT_FORCEINLINE UpdateHint &MPTOptions() { type |= HINT_MPTOPTIONS; return *this; } + MPT_FORCEINLINE UpdateHint &Undo() { type |= HINT_UNDO; return *this; } +}; + +struct GeneralHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_GENERAL; + GeneralHint() : UpdateHint(classCategory, 0) { } + GeneralHint(CHANNELINDEX channel) : UpdateHint(classCategory, 1 + channel) { } + MPT_FORCEINLINE GeneralHint &General() { type |= HINT_MODGENERAL; return *this; } + MPT_FORCEINLINE GeneralHint &Channels() { type |= HINT_MODCHANNELS; return *this; } + MPT_FORCEINLINE GeneralHint &Tunings() { type |= HINT_TUNINGS; return *this; } + + MPT_FORCEINLINE CHANNELINDEX GetChannel() const { return item ? static_cast<CHANNELINDEX>(item - 1) : CHANNELINDEX_INVALID; } +}; + +struct PatternHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_PATTERNS; + PatternHint(PATTERNINDEX item = 0) : UpdateHint(classCategory, item) { } + MPT_FORCEINLINE PatternHint &Data() { type |= HINT_PATTERNDATA; return *this; } + MPT_FORCEINLINE PatternHint &Names() { type |= HINT_PATNAMES; return *this; } + + PATTERNINDEX GetPattern() const { return GetData<PATTERNINDEX>(); } +}; + +struct RowHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_PATTERNS; + RowHint(ROWINDEX item = 0) : UpdateHint(classCategory, item) { type = HINT_PATTERNROW; } + + MPT_FORCEINLINE ROWINDEX GetRow() const { return GetData<ROWINDEX>(); } +}; + +struct SampleHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_SAMPLES; + SampleHint(SAMPLEINDEX item = 0) : UpdateHint(classCategory, item) { } + MPT_FORCEINLINE SampleHint &Info() { type |= HINT_SAMPLEINFO; return *this; } + MPT_FORCEINLINE SampleHint &Data() { type |= HINT_SAMPLEDATA; return *this; } + MPT_FORCEINLINE SampleHint &Names() { type |= HINT_SMPNAMES; return *this; } + + MPT_FORCEINLINE SAMPLEINDEX GetSample() const { return GetData<SAMPLEINDEX>(); } +}; + +struct InstrumentHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_INSTRUMENTS; + InstrumentHint(INSTRUMENTINDEX item = 0) : UpdateHint(classCategory, item) { } + MPT_FORCEINLINE InstrumentHint &Info() { type |= HINT_INSTRUMENT; return *this; } + MPT_FORCEINLINE InstrumentHint &Envelope() { type |= HINT_ENVELOPE; return *this; } + MPT_FORCEINLINE InstrumentHint &Names() { type |= HINT_INSNAMES; return *this; } + + MPT_FORCEINLINE INSTRUMENTINDEX GetInstrument() const { return GetData<INSTRUMENTINDEX>(); } +}; + +struct SequenceHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_SEQUENCE; + SequenceHint(SEQUENCEINDEX item = 0) : UpdateHint(classCategory, item) { } + MPT_FORCEINLINE SequenceHint &Data() { type |= HINT_MODSEQUENCE; return *this; } + MPT_FORCEINLINE SequenceHint &Names() { type |= HINT_SEQNAMES; return *this; } + MPT_FORCEINLINE SequenceHint &RestartPos() { type |= HINT_RESTARTPOS; return *this; } + + MPT_FORCEINLINE SEQUENCEINDEX GetSequence() const { return GetData<SEQUENCEINDEX>(); } +}; + +struct PluginHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_PLUGINS; + PluginHint(PLUGINDEX item = 0) : UpdateHint(classCategory, item) { } + MPT_FORCEINLINE PluginHint &Info() { type |= HINT_MIXPLUGINS; return *this; } + MPT_FORCEINLINE PluginHint &Names() { type |= HINT_PLUGINNAMES; return *this; } + MPT_FORCEINLINE PluginHint &Parameter() { type |= HINT_PLUGINPARAM; return *this; } + + MPT_FORCEINLINE PLUGINDEX GetPlugin() const { return GetData<PLUGINDEX>(); } +}; + +struct CommentHint : public UpdateHint +{ + static constexpr HintCategory classCategory = HINTCAT_COMMENTS; + CommentHint() : UpdateHint(classCategory) { type = HINT_MODCOMMENTS; } +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.cpp new file mode 100644 index 00000000..2045f40e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.cpp @@ -0,0 +1,90 @@ +/* + * UpdateToolTip.cpp + * ----------------- + * Purpose: Implementation of the update tooltip in the main toolbar. + * 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" + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(UpdateToolTip, CToolTipCtrl) + ON_WM_LBUTTONUP() + ON_NOTIFY_REFLECT(TTN_POP, &UpdateToolTip::OnPop) + ON_NOTIFY_REFLECT(TTN_SHOW, &UpdateToolTip::OnShow) + ON_NOTIFY_REFLECT(TTN_LINKCLICK, &UpdateToolTip::OnLinkClick) +END_MESSAGE_MAP() + + +bool UpdateToolTip::ShowUpdate(CWnd &parent, const CString &newVersion, const CString &infoURL, const CRect rectClient, const CPoint ptScreen, const int buttonID) +{ + if(m_hWnd) + DestroyWindow(); + Create(&parent, TTS_NOPREFIX | TTS_BALLOON | TTS_CLOSE | TTS_NOFADE); + + m_infoURL = infoURL; + + CString message = MPT_CFORMAT("OpenMPT {} has been released.\n<a>Click here to see what's new.</a>")(newVersion); + TOOLINFO ti{}; + ti.cbSize = TTTOOLINFO_V1_SIZE; + ti.uFlags = TTF_TRACK | TTF_PARSELINKS; + ti.hwnd = parent; + ti.lpszText = message.GetBuffer(); + ti.uId = buttonID; + ti.rect = rectClient; + if(!SendMessage(TTM_ADDTOOL, 0, reinterpret_cast<LPARAM>(&ti))) + return false; + + SetTitle(TTI_INFO, _T("Update Available")); + SendMessage(TTM_TRACKPOSITION, 0, static_cast<LPARAM>(MAKELONG(ptScreen.x, ptScreen.y))); + SendMessage(TTM_TRACKACTIVATE, TRUE, reinterpret_cast<LPARAM>(&ti)); + return true; +} + + +void UpdateToolTip::SetResult(PopAction action) +{ + m_popAction = action; + SendMessage(TTM_TRACKACTIVATE, FALSE, 0); + if(action != PopAction::CloseButton) + CMainFrame::GetMainFrame()->SendMessage(WM_MOD_UPDATENOTIFY, static_cast<WPARAM>(action)); +} + + +void UpdateToolTip::OnLButtonUp(UINT nFlags, CPoint point) +{ + CToolTipCtrl::OnLButtonUp(nFlags, point); + if(m_popAction == PopAction::Undetermined) + SetResult(PopAction::ClickBubble); +} + + +void UpdateToolTip::OnPop(NMHDR *pNMHDR, LRESULT *) +{ + if(pNMHDR->idFrom == UINT_PTR(-1)) + SetResult(PopAction::CloseButton); +} + + +void UpdateToolTip::OnShow(NMHDR *, LRESULT *) +{ + m_popAction = PopAction::Undetermined; +} + + +void UpdateToolTip::OnLinkClick(NMHDR *, LRESULT *) +{ + CTrackApp::OpenURL(m_infoURL); + SetResult(PopAction::ClickLink); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.h b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.h new file mode 100644 index 00000000..d464c9b0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/UpdateToolTip.h @@ -0,0 +1,44 @@ +/* + * UpdateToolTip.h + * --------------- + * Purpose: Implementation of the update tooltip in the main toolbar. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +class UpdateToolTip : public CToolTipCtrl +{ +public: + enum class PopAction + { + Undetermined, + CloseButton, + ClickLink, + ClickBubble, + }; +protected: + CString m_infoURL; + PopAction m_popAction = PopAction::Undetermined; + +public: + bool ShowUpdate(CWnd &parent, const CString &newVersion, const CString &infoURL, const CRect rectClient, const CPoint ptScreen, const int buttonID); + +protected: + void SetResult(PopAction action); + + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg void OnPop(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnShow(NMHDR *pNMHDR, LRESULT *pResult); + afx_msg void OnLinkClick(NMHDR *pNMHDR, LRESULT *pResult); + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.cpp new file mode 100644 index 00000000..13b2a46a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.cpp @@ -0,0 +1,146 @@ +/* + * VSTEditor.cpp + * ------------- + * Purpose: Implementation of the custom plugin editor window that is used if a plugin provides an own editor GUI. + * 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 "resource.h" +#include "Vstplug.h" +#include "VSTEditor.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#ifdef MPT_WITH_VST + +BEGIN_MESSAGE_MAP(COwnerVstEditor, CAbstractVstEditor) + ON_WM_ERASEBKGND() + ON_WM_PAINT() + // Messages from plugin bridge to check whether a key would be handled by OpenMPT + // We need an offset to WM_USER because the editor window receives some spurious WM_USER messages (from MFC?) when it gets activated + ON_MESSAGE(WM_USER + 4000 + WM_KEYDOWN - WM_KEYFIRST, &COwnerVstEditor::OnPreTranslateKeyDown) + ON_MESSAGE(WM_USER + 4000 + WM_KEYUP - WM_KEYFIRST, &COwnerVstEditor::OnPreTranslateKeyUp) + ON_MESSAGE(WM_USER + 4000 + WM_SYSKEYDOWN - WM_KEYFIRST, &COwnerVstEditor::OnPreTranslateSysKeyDown) + ON_MESSAGE(WM_USER + 4000 + WM_SYSKEYUP - WM_KEYFIRST, &COwnerVstEditor::OnPreTranslateSysKeyUp) +END_MESSAGE_MAP() + + +void COwnerVstEditor::OnPaint() +{ + CAbstractVstEditor::OnPaint(); + auto &plugin = static_cast<const CVstPlugin &>(m_VstPlugin); + if(plugin.isBridged) + { + // Force redrawing for the plugin window in the bridged process. + // Otherwise, bridged plugin GUIs will not always be refreshed properly (e.g. when restoring OpenMPT from minimized state). + // Synth1 is a good candidate for testing this behaviour. + CRect rect; + if(m_plugWindow.GetUpdateRect(&rect, FALSE)) + { + CWnd *child = m_plugWindow.GetWindow(GW_CHILD | GW_HWNDFIRST); + if(child) + child->RedrawWindow(&rect, nullptr, RDW_INVALIDATE | RDW_ALLCHILDREN | RDW_UPDATENOW); + } + } else + { + // For plugins that change their size without telling the host through audioMasterSizeWindow, e.g. Roland D-50 + CRect rect; + m_plugWindow.GetClientRect(rect); + if(rect.Width() != m_width || rect.Height() != m_height) + { + SetSize(rect.Width(), rect.Height()); + } + } +} + + +void COwnerVstEditor::UpdateParamDisplays() +{ + CAbstractVstEditor::UpdateParamDisplays(); + // We trust that the plugin GUI can update its display with a bit of idle time. + static_cast<CVstPlugin &>(m_VstPlugin).Dispatch(Vst::effEditIdle, 0, 0, nullptr, 0.0f); +} + +bool COwnerVstEditor::OpenEditor(CWnd *parent) +{ + Create(IDD_PLUGINEDITOR, parent); + + // Some plugins (e.g. ProteusVX) need to be planted into another control or else they will break our window proc, making the window unusable. + m_plugWindow.Create(nullptr, WS_CHILD | WS_VISIBLE, CRect(0, 0, 100, 100), this); + + // Set editor window size + Vst::ERect rect{}; + Vst::ERect *pRect = nullptr; + CVstPlugin &vstPlug = static_cast<CVstPlugin &>(m_VstPlugin); + vstPlug.Dispatch(Vst::effEditGetRect, 0, 0, &pRect, 0); + if(pRect) rect = *pRect; + vstPlug.Dispatch(Vst::effEditOpen, 0, 0, m_plugWindow.m_hWnd, 0); + vstPlug.Dispatch(Vst::effEditGetRect, 0, 0, &pRect, 0); + if(pRect) rect = *pRect; + if(rect.right > rect.left && rect.bottom > rect.top) + { + // Plugin provided valid window size. + SetSize(rect.Width(), rect.Height()); + } + + vstPlug.Dispatch(Vst::effEditTop, 0,0, nullptr, 0.0f); + vstPlug.Dispatch(Vst::effEditIdle, 0,0, nullptr, 0.0f); + + // Set knob mode to linear (2) instead of circular (0) for those plugins that support it (e.g. Steinberg VB-1) + vstPlug.Dispatch(Vst::effSetEditKnobMode, 0, 2, nullptr, 0.0f); + + return CAbstractVstEditor::OpenEditor(parent); +} + + +void COwnerVstEditor::DoClose() +{ + // Prevent some plugins from storing a bogus window size (e.g. Electri-Q) + ShowWindow(SW_HIDE); + if(m_isMinimized) OnNcLButtonDblClk(HTCAPTION, CPoint(0, 0)); + static_cast<CVstPlugin &>(m_VstPlugin).Dispatch(Vst::effEditClose, 0, 0, nullptr, 0.0f); + CAbstractVstEditor::DoClose(); +} + + +bool COwnerVstEditor::SetSize(int contentWidth, int contentHeight) +{ + if(contentWidth < 0 || contentHeight < 0 || !m_hWnd) + { + return false; + } + m_width = contentWidth; + m_height = contentHeight; + + CRect rcWnd, rcClient; + + // Get border / menu size. + GetWindowRect(&rcWnd); + GetClientRect(&rcClient); + + // Narrow plugin GUIs may force the number of menu bar lines (and thus the required window height) to change + WindowSizeAdjuster adjuster(*this); + + const int windowWidth = rcWnd.Width() - rcClient.Width() + contentWidth; + const int windowHeight = rcWnd.Height() - rcClient.Height() + contentHeight; + SetWindowPos(NULL, 0, 0, + windowWidth, windowHeight, + SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); + m_plugWindow.SetWindowPos(NULL, 0, 0, + contentWidth, contentHeight, + SWP_NOZORDER | SWP_NOMOVE | SWP_NOACTIVATE); + + return true; +} + + +#endif // MPT_WITH_VST + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.h b/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.h new file mode 100644 index 00000000..a708f99c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/VSTEditor.h @@ -0,0 +1,59 @@ +/* + * VSTEditor.h + * ----------- + * Purpose: Implementation of the custom plugin editor window that is used if a plugin provides an own editor GUI. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "AbstractVstEditor.h" + +OPENMPT_NAMESPACE_BEGIN + +#ifdef MPT_WITH_VST + +class COwnerVstEditor : public CAbstractVstEditor +{ +protected: + CStatic m_plugWindow; + int m_width = 0, m_height = 0; + +public: + COwnerVstEditor(CVstPlugin &plugin) : CAbstractVstEditor(plugin) { } + ~COwnerVstEditor() override { } + + // Plugins may request to change the GUI size. + bool IsResizable() const override { return true; } + bool SetSize(int contentWidth, int contentHeight) override; + + void UpdateParamDisplays() override; + + bool OpenEditor(CWnd *parent) override; + void DoClose() override; + +protected: + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + afx_msg void OnPaint(); + + LRESULT OnPreTranslateKeyDown(WPARAM wParam, LPARAM lParam) { return HandlePreTranslateMessage(WM_KEYDOWN, wParam, lParam); } + LRESULT OnPreTranslateKeyUp(WPARAM wParam, LPARAM lParam) { return HandlePreTranslateMessage(WM_KEYUP, wParam, lParam); } + LRESULT OnPreTranslateSysKeyDown(WPARAM wParam, LPARAM lParam) { return HandlePreTranslateMessage(WM_SYSKEYDOWN, wParam, lParam); } + LRESULT OnPreTranslateSysKeyUp(WPARAM wParam, LPARAM lParam) { return HandlePreTranslateMessage(WM_SYSKEYUP, wParam, lParam); } + LRESULT HandlePreTranslateMessage(UINT message, WPARAM wParam, LPARAM lParam) + { + MSG msg = {m_plugWindow, message, wParam, lParam, 0, {}}; + return HandleKeyMessage(msg); + } + + DECLARE_MESSAGE_MAP() +}; + +#endif // MPT_WITH_VST + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.cpp new file mode 100644 index 00000000..253bc4ee --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.cpp @@ -0,0 +1,1816 @@ +/* + * view_gen.cpp + * ------------ + * Purpose: General tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Childfrm.h" +#include "Moddoc.h" +#include "Globals.h" +#include "Ctrl_gen.h" +#include "View_gen.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "EffectVis.h" +#include "MoveFXSlotDialog.h" +#include "ChannelManagerDlg.h" +#include "SelectPluginDialog.h" +#include "../soundlib/mod_specifications.h" +#include "../common/mptStringBuffer.h" +#include "AbstractVstEditor.h" + +// This is used for retrieving the correct background colour for the +// frames on the general tab when using WinXP Luna or Vista/Win7 Aero. +#include <uxtheme.h> + + +OPENMPT_NAMESPACE_BEGIN + + +IMPLEMENT_SERIAL(CViewGlobals, CFormView, 0) + +BEGIN_MESSAGE_MAP(CViewGlobals, CFormView) + //{{AFX_MSG_MAP(CViewGlobals) + ON_WM_SIZE() + ON_WM_HSCROLL() + ON_WM_VSCROLL() + ON_WM_DESTROY() + + ON_MESSAGE(WM_MOD_MDIACTIVATE, &CViewGlobals::OnMDIDeactivate) + ON_MESSAGE(WM_MOD_MDIDEACTIVATE, &CViewGlobals::OnMDIDeactivate) + + ON_COMMAND(IDC_CHECK1, &CViewGlobals::OnMute1) + ON_COMMAND(IDC_CHECK3, &CViewGlobals::OnMute2) + ON_COMMAND(IDC_CHECK5, &CViewGlobals::OnMute3) + ON_COMMAND(IDC_CHECK7, &CViewGlobals::OnMute4) + ON_COMMAND(IDC_CHECK2, &CViewGlobals::OnSurround1) + ON_COMMAND(IDC_CHECK4, &CViewGlobals::OnSurround2) + ON_COMMAND(IDC_CHECK6, &CViewGlobals::OnSurround3) + ON_COMMAND(IDC_CHECK8, &CViewGlobals::OnSurround4) + + ON_COMMAND(IDC_BUTTON9, &CViewGlobals::OnEditColor1) + ON_COMMAND(IDC_BUTTON10, &CViewGlobals::OnEditColor2) + ON_COMMAND(IDC_BUTTON11, &CViewGlobals::OnEditColor3) + ON_COMMAND(IDC_BUTTON12, &CViewGlobals::OnEditColor4) + + ON_EN_UPDATE(IDC_EDIT1, &CViewGlobals::OnEditVol1) + ON_EN_UPDATE(IDC_EDIT3, &CViewGlobals::OnEditVol2) + ON_EN_UPDATE(IDC_EDIT5, &CViewGlobals::OnEditVol3) + ON_EN_UPDATE(IDC_EDIT7, &CViewGlobals::OnEditVol4) + ON_EN_UPDATE(IDC_EDIT2, &CViewGlobals::OnEditPan1) + ON_EN_UPDATE(IDC_EDIT4, &CViewGlobals::OnEditPan2) + ON_EN_UPDATE(IDC_EDIT6, &CViewGlobals::OnEditPan3) + ON_EN_UPDATE(IDC_EDIT8, &CViewGlobals::OnEditPan4) + ON_EN_UPDATE(IDC_EDIT9, &CViewGlobals::OnEditName1) + ON_EN_UPDATE(IDC_EDIT10, &CViewGlobals::OnEditName2) + ON_EN_UPDATE(IDC_EDIT11, &CViewGlobals::OnEditName3) + ON_EN_UPDATE(IDC_EDIT12, &CViewGlobals::OnEditName4) + + ON_CBN_SELCHANGE(IDC_COMBO1, &CViewGlobals::OnFx1Changed) + ON_CBN_SELCHANGE(IDC_COMBO2, &CViewGlobals::OnFx2Changed) + ON_CBN_SELCHANGE(IDC_COMBO3, &CViewGlobals::OnFx3Changed) + ON_CBN_SELCHANGE(IDC_COMBO4, &CViewGlobals::OnFx4Changed) + + // Plugins + ON_COMMAND(IDC_CHECK9, &CViewGlobals::OnMixModeChanged) + ON_COMMAND(IDC_CHECK10, &CViewGlobals::OnBypassChanged) + ON_COMMAND(IDC_CHECK11, &CViewGlobals::OnDryMixChanged) + ON_COMMAND(IDC_BUTTON1, &CViewGlobals::OnSelectPlugin) + ON_COMMAND(IDC_DELPLUGIN, &CViewGlobals::OnRemovePlugin) + ON_COMMAND(IDC_BUTTON2, &CViewGlobals::OnEditPlugin) + ON_COMMAND(IDC_BUTTON4, &CViewGlobals::OnNextPlugin) + ON_COMMAND(IDC_BUTTON5, &CViewGlobals::OnPrevPlugin) + ON_COMMAND(IDC_MOVEFXSLOT, &CViewGlobals::OnMovePlugToSlot) + ON_COMMAND(IDC_INSERTFXSLOT,&CViewGlobals::OnInsertSlot) + ON_COMMAND(IDC_CLONEPLUG, &CViewGlobals::OnClonePlug) + + ON_COMMAND(IDC_BUTTON6, &CViewGlobals::OnLoadParam) + ON_COMMAND(IDC_BUTTON8, &CViewGlobals::OnSaveParam) + + ON_EN_UPDATE(IDC_EDIT13, &CViewGlobals::OnPluginNameChanged) + ON_EN_UPDATE(IDC_EDIT14, &CViewGlobals::OnSetParameter) + ON_EN_SETFOCUS(IDC_EDIT14, &CViewGlobals::OnFocusParam) + ON_EN_KILLFOCUS(IDC_EDIT14, &CViewGlobals::OnParamChanged) + ON_CBN_SELCHANGE(IDC_COMBO5, &CViewGlobals::OnPluginChanged) + + ON_CBN_SELCHANGE(IDC_COMBO6, &CViewGlobals::OnParamChanged) + ON_CBN_SETFOCUS(IDC_COMBO6, &CViewGlobals::OnFillParamCombo) + + ON_CBN_SELCHANGE(IDC_COMBO7, &CViewGlobals::OnOutputRoutingChanged) + + ON_CBN_SELCHANGE(IDC_COMBO8, &CViewGlobals::OnProgramChanged) + ON_CBN_SETFOCUS(IDC_COMBO8, &CViewGlobals::OnFillProgramCombo) + + ON_COMMAND(IDC_CHECK12, &CViewGlobals::OnWetDryExpandChanged) + ON_CBN_SELCHANGE(IDC_COMBO9, &CViewGlobals::OnSpecialMixProcessingChanged) + + ON_NOTIFY(TCN_SELCHANGE, IDC_TABCTRL1, &CViewGlobals::OnTabSelchange) + ON_MESSAGE(WM_MOD_UNLOCKCONTROLS, &CViewGlobals::OnUnlockControls) + ON_MESSAGE(WM_MOD_VIEWMSG, &CViewGlobals::OnModViewMsg) + ON_MESSAGE(WM_MOD_MIDIMSG, &CViewGlobals::OnMidiMsg) + ON_MESSAGE(WM_MOD_PLUGPARAMAUTOMATE, &CViewGlobals::OnParamAutomated) + ON_MESSAGE(WM_MOD_PLUGINDRYWETRATIOCHANGED, &CViewGlobals::OnDryWetRatioChangedFromPlayer) + + ON_COMMAND(ID_EDIT_UNDO, &CViewGlobals::OnEditUndo) + ON_COMMAND(ID_EDIT_REDO, &CViewGlobals::OnEditRedo) + ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewGlobals::OnUpdateUndo) + ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewGlobals::OnUpdateRedo) + ON_NOTIFY_EX_RANGE(TTN_NEEDTEXT, 0, 0xFFFF, &CViewGlobals::OnToolTipText) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void CViewGlobals::DoDataExchange(CDataExchange* pDX) +{ + CFormView::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CViewGlobals) + DDX_Control(pDX, IDC_TABCTRL1, m_TabCtrl); + DDX_Control(pDX, IDC_COMBO1, m_CbnEffects[0]); + DDX_Control(pDX, IDC_COMBO2, m_CbnEffects[1]); + DDX_Control(pDX, IDC_COMBO3, m_CbnEffects[2]); + DDX_Control(pDX, IDC_COMBO4, m_CbnEffects[3]); + DDX_Control(pDX, IDC_COMBO5, m_CbnPlugin); + DDX_Control(pDX, IDC_COMBO6, m_CbnParam); + DDX_Control(pDX, IDC_COMBO7, m_CbnOutput); + + DDX_Control(pDX, IDC_COMBO8, m_CbnPreset); + DDX_Control(pDX, IDC_COMBO9, m_CbnSpecialMixProcessing); + DDX_Control(pDX, IDC_SPIN10, m_SpinMixGain); + + DDX_Control(pDX, IDC_SLIDER1, m_sbVolume[0]); + DDX_Control(pDX, IDC_SLIDER2, m_sbPan[0]); + DDX_Control(pDX, IDC_SLIDER3, m_sbVolume[1]); + DDX_Control(pDX, IDC_SLIDER4, m_sbPan[1]); + DDX_Control(pDX, IDC_SLIDER5, m_sbVolume[2]); + DDX_Control(pDX, IDC_SLIDER6, m_sbPan[2]); + DDX_Control(pDX, IDC_SLIDER7, m_sbVolume[3]); + DDX_Control(pDX, IDC_SLIDER8, m_sbPan[3]); + DDX_Control(pDX, IDC_SLIDER9, m_sbValue); + DDX_Control(pDX, IDC_SLIDER10, m_sbDryRatio); + DDX_Control(pDX, IDC_SPIN1, m_spinVolume[0]); + DDX_Control(pDX, IDC_SPIN2, m_spinPan[0]); + DDX_Control(pDX, IDC_SPIN3, m_spinVolume[1]); + DDX_Control(pDX, IDC_SPIN4, m_spinPan[1]); + DDX_Control(pDX, IDC_SPIN5, m_spinVolume[2]); + DDX_Control(pDX, IDC_SPIN6, m_spinPan[2]); + DDX_Control(pDX, IDC_SPIN7, m_spinVolume[3]); + DDX_Control(pDX, IDC_SPIN8, m_spinPan[3]); + DDX_Control(pDX, IDC_BUTTON1, m_BtnSelect); + DDX_Control(pDX, IDC_BUTTON2, m_BtnEdit); + //}}AFX_DATA_MAP +} + +void CViewGlobals::OnInitialUpdate() +{ + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + int nMapMode = MM_TEXT; + SIZE sizeTotal, sizePage, sizeLine; + + m_nActiveTab = CHANNELINDEX(-1); + m_nCurrentPlugin = 0; + m_nCurrentParam = 0; + CFormView::OnInitialUpdate(); + EnableToolTips(); + + if (pFrame) + { + GENERALVIEWSTATE &generalState = pFrame->GetGeneralViewState(); + if (generalState.initialized) + { + m_TabCtrl.SetCurSel(generalState.nTab); + m_nActiveTab = generalState.nTab; + m_nCurrentPlugin = generalState.nPlugin; + m_nCurrentParam = generalState.nParam; + } + } + GetDeviceScrollSizes(nMapMode, sizeTotal, sizePage, sizeLine); + m_rcClient.SetRect(0, 0, sizeTotal.cx, sizeTotal.cy); + RecalcLayout(); + + // Initializing scroll ranges + for(int ichn = 0; ichn < CHANNELS_IN_TAB; ichn++) + { + // Color select + m_channelColor[ichn].SubclassDlgItem(IDC_BUTTON9 + ichn, this); + // Volume Slider + m_sbVolume[ichn].SetRange(0, 64); + m_sbVolume[ichn].SetTicFreq(8); + // Pan Slider + m_sbPan[ichn].SetRange(0, 64); + m_sbPan[ichn].SetTicFreq(8); + // Volume Spin + m_spinVolume[ichn].SetRange(0, 64); + // Pan Spin + m_spinPan[ichn].SetRange(0, 256); + } + m_sbValue.SetPos(0); + m_sbValue.SetRange(0, 100); + + m_sbValue.SetPos(0); + m_sbValue.SetRange(0, 100); + + m_CbnSpecialMixProcessing.AddString(_T("Default")); + m_CbnSpecialMixProcessing.AddString(_T("Wet Subtract")); + m_CbnSpecialMixProcessing.AddString(_T("Dry Subtract")); + m_CbnSpecialMixProcessing.AddString(_T("Mix Subtract")); + m_CbnSpecialMixProcessing.AddString(_T("Middle Subtract")); + m_CbnSpecialMixProcessing.AddString(_T("LR Balance")); + m_SpinMixGain.SetRange(0, 80); + m_SpinMixGain.SetPos(10); + SetDlgItemText(IDC_EDIT16, _T("Gain: x1.0")); + + UpdateView(UpdateHint().ModType()); + OnParamChanged(); + m_nLockCount = 0; + + // Use tab background color rather than regular dialog background color (required for Aero/etc. where they are not the same color) + EnableThemeDialogTexture(m_hWnd, ETDT_ENABLETAB); +} + + +void CViewGlobals::OnDestroy() +{ + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + if (pFrame) + { + GENERALVIEWSTATE &generalState = pFrame->GetGeneralViewState(); + generalState.initialized = true; + generalState.nTab = m_nActiveTab; + generalState.nPlugin = m_nCurrentPlugin; + generalState.nParam = m_nCurrentParam; + } + CFormView::OnDestroy(); +} + + +LRESULT CViewGlobals::OnMDIDeactivate(WPARAM, LPARAM) +{ + // Create new undo point if we switch to / from other window + m_lastEdit = CHANNELINDEX_INVALID; + return 0; +} + + +LRESULT CViewGlobals::OnMidiMsg(WPARAM midiData_, LPARAM) +{ + uint32 midiData = static_cast<uint32>(midiData_); + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + ih->HandleMIDIMessage(kCtxViewGeneral, midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull; + return 1; +} + + +void CViewGlobals::RecalcLayout() +{ + if (m_TabCtrl.m_hWnd != NULL) + { + CRect rect; + GetClientRect(&rect); + if (rect.right < m_rcClient.right) rect.right = m_rcClient.right; + if (rect.bottom < m_rcClient.bottom) rect.bottom = m_rcClient.bottom; + m_TabCtrl.SetWindowPos(&CWnd::wndBottom, 0,0, rect.right, rect.bottom, SWP_NOMOVE); + } +} + + +int CViewGlobals::GetDlgItemIntEx(UINT nID) +{ + CString s; + GetDlgItemText(nID, s); + if(s.GetLength() < 1 || s[0] < _T('0') || s[0] > _T('9')) return -1; + return _ttoi(s); +} + + +void CViewGlobals::OnSize(UINT nType, int cx, int cy) +{ + CFormView::OnSize(nType, cx, cy); + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0) && (m_hWnd)) + { + RecalcLayout(); + } +} + + +void CViewGlobals::OnUpdate(CView *pView, LPARAM lHint, CObject *pHint) +{ + if (pView != this) UpdateView(UpdateHint::FromLPARAM(lHint), pHint); +} + + +void CViewGlobals::UpdateView(UpdateHint hint, CObject *pObject) +{ + const CModDoc *pModDoc = GetDocument(); + int nTabCount, nTabIndex; + + if (!pModDoc || pObject == this ) return; + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + const GeneralHint genHint = hint.ToType<GeneralHint>(); + const PluginHint plugHint = hint.ToType<PluginHint>(); + if (!genHint.GetType()[HINT_MODTYPE | HINT_MODCHANNELS] + && !plugHint.GetType()[HINT_MIXPLUGINS | HINT_PLUGINNAMES | HINT_PLUGINPARAM]) + { + return; + } + FlagSet<HintType> hintType = hint.GetType(); + const bool updateAll = hintType[HINT_MODTYPE]; + const auto updateChannel = genHint.GetChannel(); + const int updateTab = (updateChannel < sndFile.GetNumChannels()) ? (updateChannel / CHANNELS_IN_TAB) : m_nActiveTab; + if(genHint.GetType()[HINT_MODCHANNELS] && updateTab != m_nActiveTab) + return; + + CString s; + nTabCount = (sndFile.m_nChannels + (CHANNELS_IN_TAB - 1)) / CHANNELS_IN_TAB; + if (nTabCount != m_TabCtrl.GetItemCount()) + { + UINT nOldSel = m_TabCtrl.GetCurSel(); + if (!m_TabCtrl.GetItemCount()) nOldSel = m_nActiveTab; + m_TabCtrl.SetRedraw(FALSE); + m_TabCtrl.DeleteAllItems(); + for (int iItem=0; iItem<nTabCount; iItem++) + { + const int lastItem = std::min(iItem * CHANNELS_IN_TAB + CHANNELS_IN_TAB, static_cast<int>(MAX_BASECHANNELS)); + s = MPT_CFORMAT("{} - {}")(iItem * CHANNELS_IN_TAB + 1, lastItem); + TC_ITEM tci; + tci.mask = TCIF_TEXT | TCIF_PARAM; + tci.pszText = const_cast<TCHAR *>(s.GetString()); + tci.lParam = iItem * CHANNELS_IN_TAB; + m_TabCtrl.InsertItem(iItem, &tci); + } + if (nOldSel >= (UINT)nTabCount) nOldSel = 0; + + m_TabCtrl.SetRedraw(TRUE); + m_TabCtrl.SetCurSel(nOldSel); + + InvalidateRect(NULL, FALSE); + } + nTabIndex = m_TabCtrl.GetCurSel(); + if ((nTabIndex < 0) || (nTabIndex >= nTabCount)) return; // ??? + + if (m_nActiveTab != nTabIndex || genHint.GetType()[HINT_MODTYPE | HINT_MODCHANNELS]) + { + LockControls(); + m_nActiveTab = static_cast<CHANNELINDEX>(nTabIndex); + for (CHANNELINDEX ichn = 0; ichn < CHANNELS_IN_TAB; ichn++) + { + const CHANNELINDEX nChn = m_nActiveTab * CHANNELS_IN_TAB + ichn; + const BOOL bEnable = (nChn < sndFile.GetNumChannels()) ? TRUE : FALSE; + if(nChn < MAX_BASECHANNELS) + { + const auto &chnSettings = sndFile.ChnSettings[nChn]; + // Text + if(bEnable) + s = MPT_CFORMAT("Channel {}")(nChn + 1); + else + s = _T(""); + SetDlgItemText(IDC_TEXT1 + ichn, s); + // Channel color + m_channelColor[ichn].SetColor(chnSettings.color); + m_channelColor[ichn].EnableWindow(bEnable); + // Mute + CheckDlgButton(IDC_CHECK1 + ichn * 2, chnSettings.dwFlags[CHN_MUTE] ? TRUE : FALSE); + // Surround + CheckDlgButton(IDC_CHECK2 + ichn * 2, chnSettings.dwFlags[CHN_SURROUND] ? TRUE : FALSE); + // Volume + int vol = chnSettings.nVolume; + m_sbVolume[ichn].SetPos(vol); + m_sbVolume[ichn].Invalidate(FALSE); + SetDlgItemInt(IDC_EDIT1+ichn*2, vol); + // Pan + int pan = chnSettings.nPan; + m_sbPan[ichn].SetPos(pan/4); + m_sbPan[ichn].Invalidate(FALSE); + SetDlgItemInt(IDC_EDIT2+ichn*2, pan); + + // Channel name + s = mpt::ToCString(sndFile.GetCharsetInternal(), chnSettings.szName); + SetDlgItemText(IDC_EDIT9 + ichn, s); + ((CEdit*)(GetDlgItem(IDC_EDIT9 + ichn)))->LimitText(MAX_CHANNELNAME - 1); + } else + { + SetDlgItemText(IDC_TEXT1 + ichn, _T("")); + SetDlgItemText(IDC_EDIT9 + ichn, _T("")); + m_channelColor[ichn].EnableWindow(FALSE); + } + + // Enable/Disable controls for this channel + BOOL bIT = ((bEnable) && (sndFile.m_nType & (MOD_TYPE_IT|MOD_TYPE_MPT))); + GetDlgItem(IDC_CHECK1 + ichn * 2)->EnableWindow(bEnable); + GetDlgItem(IDC_CHECK2 + ichn * 2)->EnableWindow(bIT); + + m_sbVolume[ichn].EnableWindow(bIT); + m_spinVolume[ichn].EnableWindow(bIT); + + m_sbPan[ichn].EnableWindow(bEnable && !(sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_MOD))); + m_spinPan[ichn].EnableWindow(bEnable && !(sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_MOD))); + GetDlgItem(IDC_EDIT1 + ichn * 2)->EnableWindow(bIT); // channel vol + GetDlgItem(IDC_EDIT2 + ichn * 2)->EnableWindow(bEnable && !(sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_MOD))); // channel pan + GetDlgItem(IDC_EDIT9 + ichn)->EnableWindow(((bEnable) && (sndFile.m_nType & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)))); // channel name + m_CbnEffects[ichn].EnableWindow(bEnable & (sndFile.GetModSpecifications().supportsPlugins ? TRUE : FALSE)); + } + UnlockControls(); + } + + // Update plugin names + if(genHint.GetType()[HINT_MODTYPE | HINT_MODCHANNELS] || plugHint.GetType()[HINT_PLUGINNAMES]) + { + PopulateChannelPlugins(plugHint.GetPlugin() ? plugHint.GetPlugin() - 1 : PLUGINDEX_INVALID); + SetDlgItemText(IDC_EDIT13, mpt::ToCString(sndFile.m_MixPlugins[m_nCurrentPlugin].GetName())); + } + // Update plugin info + const SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + const bool updatePlug = (plugHint.GetPlugin() == 0 || plugHint.GetPlugin() == m_nCurrentPlugin + 1); + const bool updateWholePluginView = updateAll || (plugHint.GetType()[HINT_MIXPLUGINS] && updatePlug); + if(updateWholePluginView) + { + const PLUGINDEX plugIndex = (plugHint.GetPlugin() != 0) ? plugHint.GetPlugin() - 1 : PLUGINDEX_INVALID; + m_CbnPlugin.SetRedraw(FALSE); + if(plugIndex == PLUGINDEX_INVALID) + m_CbnPlugin.ResetContent(); + AddPluginNamesToCombobox(m_CbnPlugin, sndFile.m_MixPlugins, true, plugIndex); + m_CbnPlugin.SetRedraw(TRUE); + m_CbnPlugin.SetCurSel(m_nCurrentPlugin); + if (m_nCurrentPlugin >= MAX_MIXPLUGINS) m_nCurrentPlugin = 0; + SetDlgItemText(IDC_EDIT13, mpt::ToCString(plugin.GetName())); + CheckDlgButton(IDC_CHECK9, plugin.IsMasterEffect() ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK10, plugin.IsBypassed() ? BST_CHECKED : BST_UNCHECKED); + CheckDlgButton(IDC_CHECK11, plugin.IsWetMix() ? BST_CHECKED : BST_UNCHECKED); + IMixPlugin *pPlugin = plugin.pMixPlugin; + m_BtnEdit.EnableWindow((pPlugin != nullptr && (pPlugin->HasEditor() || pPlugin->GetNumParameters())) ? TRUE : FALSE); + GetDlgItem(IDC_MOVEFXSLOT)->EnableWindow((pPlugin) ? TRUE : FALSE); + GetDlgItem(IDC_INSERTFXSLOT)->EnableWindow((pPlugin) ? TRUE : FALSE); + GetDlgItem(IDC_CLONEPLUG)->EnableWindow((pPlugin) ? TRUE : FALSE); + UpdateDryWetDisplay(); + + if(pPlugin && pPlugin->IsInstrument()) + { + m_CbnSpecialMixProcessing.EnableWindow(FALSE); + GetDlgItem(IDC_CHECK12)->EnableWindow(FALSE); + } else + { + m_CbnSpecialMixProcessing.EnableWindow(TRUE); + GetDlgItem(IDC_CHECK12)->EnableWindow(TRUE); + m_CbnSpecialMixProcessing.SetCurSel(plugin.GetMixMode()); + CheckDlgButton(IDC_CHECK12, plugin.IsExpandedMix() ? BST_CHECKED : BST_UNCHECKED); + } + int gain = plugin.GetGain(); + if(gain == 0) gain = 10; + float value = 0.1f * (float)gain; + s.Format(_T("Gain: x%1.1f"), value); + SetDlgItemText(IDC_EDIT16, s); + m_SpinMixGain.SetPos(gain); + + if (pPlugin) + { + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + m_CbnParam.SetRedraw(FALSE); + m_CbnParam.ResetContent(); + if (m_nCurrentParam >= nParams) m_nCurrentParam = 0; + + if(nParams) + { + m_CbnParam.SetItemData(m_CbnParam.AddString(pPlugin->GetFormattedParamName(m_nCurrentParam)), m_nCurrentParam); + } + + m_CbnParam.SetCurSel(0); + m_CbnParam.SetRedraw(TRUE); + OnParamChanged(); + + // Input / Output type + int in = pPlugin->GetNumInputChannels(), out = pPlugin->GetNumOutputChannels(); + if (in < 1) s = _T("No input"); + else if (in == 1) s = _T("Mono-In"); + else s = _T("Stereo-In"); + s += _T(", "); + if (out < 1) s += _T("No output"); + else if (out == 1) s += _T("Mono-Out"); + else s += _T("Stereo-Out"); + + // For now, only display the "current" preset. + // This prevents the program from hanging when switching between plugin slots or + // switching to the general tab and the first plugin in the list has a lot of presets. + // Some plugins like Synth1 have so many presets that this *does* indeed make a difference, + // even on fairly modern CPUs. The rest of the presets are just added when the combo box + // gets the focus, i.e. just when they're needed. + int32 currentProg = pPlugin->GetCurrentProgram(); + FillPluginProgramBox(currentProg, currentProg); + m_CbnPreset.SetCurSel(0); + + m_sbValue.EnableWindow(TRUE); + m_sbDryRatio.EnableWindow(TRUE); + GetDlgItem(IDC_EDIT14)->EnableWindow(TRUE); + } else + { + s.Empty(); + if (m_CbnParam.GetCount() > 0) m_CbnParam.ResetContent(); + m_nCurrentParam = 0; + + m_CbnPreset.SetRedraw(FALSE); + m_CbnPreset.ResetContent(); + m_CbnPreset.SetItemData(m_CbnPreset.AddString(_T("none")), 0); + m_CbnPreset.SetRedraw(TRUE); + m_CbnPreset.SetCurSel(0); + m_sbValue.EnableWindow(FALSE); + m_sbDryRatio.EnableWindow(FALSE); + GetDlgItem(IDC_EDIT14)->EnableWindow(FALSE); + } + SetDlgItemText(IDC_TEXT6, s); + } + + if(updateWholePluginView || plugHint.GetPlugin() > m_nCurrentPlugin) + { + int insertAt = 1; + m_CbnOutput.SetRedraw(FALSE); + if(updateWholePluginView) + { + m_CbnOutput.ResetContent(); + m_CbnOutput.SetItemData(m_CbnOutput.AddString(_T("Default")), 0); + + for(PLUGINDEX i = m_nCurrentPlugin + 1; i < MAX_MIXPLUGINS; i++) + { + if(!sndFile.m_MixPlugins[i].IsValidPlugin()) + { + m_CbnOutput.SetItemData(m_CbnOutput.AddString(_T("New Plugin...")), 1); + insertAt = 2; + break; + } + } + } else + { + const DWORD_PTR changedPlugin = 0x80 + (plugHint.GetPlugin() - 1); + const int items = m_CbnOutput.GetCount(); + for(insertAt = 1; insertAt < items; insertAt++) + { + DWORD_PTR thisPlugin = m_CbnOutput.GetItemData(insertAt); + if(thisPlugin == changedPlugin) + m_CbnOutput.DeleteString(insertAt); + if(thisPlugin >= changedPlugin) + break; + } + } + + int outputSel = plugin.IsOutputToMaster() ? 0 : -1; + for(PLUGINDEX iOut = m_nCurrentPlugin + 1; iOut < MAX_MIXPLUGINS; iOut++) + { + if(!updateWholePluginView && (iOut + 1) != plugHint.GetPlugin()) + continue; + const SNDMIXPLUGIN &outPlug = sndFile.m_MixPlugins[iOut]; + if(outPlug.IsValidPlugin()) + { + const auto name = outPlug.GetName(), libName = outPlug.GetLibraryName(); + s.Format(_T("FX%d: "), iOut + 1); + s += mpt::ToCString(name.empty() ? libName : name); + if(!name.empty() && libName != name) + { + s += _T(" ("); + s += mpt::ToCString(libName); + s += _T(")"); + } + + insertAt = m_CbnOutput.InsertString(insertAt, s); + m_CbnOutput.SetItemData(insertAt, 0x80 + iOut); + if(!plugin.IsOutputToMaster() && (plugin.GetOutputPlugin() == iOut)) + { + outputSel = insertAt; + } + insertAt++; + } + } + m_CbnOutput.SetRedraw(TRUE); + if(outputSel >= 0) + m_CbnOutput.SetCurSel(outputSel); + } + if(plugHint.GetType()[HINT_PLUGINPARAM] && updatePlug) + { + OnParamChanged(); + } + + m_CbnPlugin.Invalidate(FALSE); + m_CbnParam.Invalidate(FALSE); + m_CbnPreset.Invalidate(FALSE); + m_CbnSpecialMixProcessing.Invalidate(FALSE); + m_CbnOutput.Invalidate(FALSE); +} + + +void CViewGlobals::PopulateChannelPlugins(PLUGINDEX plugin) +{ + // Channel effect lists + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + CString s; + for(CHANNELINDEX ichn = 0; ichn < CHANNELS_IN_TAB; ichn++) + { + const CHANNELINDEX nChn = m_nActiveTab * CHANNELS_IN_TAB + ichn; + auto &comboBox = m_CbnEffects[ichn]; + if(nChn < MAX_BASECHANNELS) + { + comboBox.SetRedraw(FALSE); + int insertAt = 1; + if(plugin == PLUGINDEX_INVALID) + { + comboBox.ResetContent(); + comboBox.SetItemData(comboBox.AddString(_T("No plugin")), 0); + } else + { + const int items = comboBox.GetCount(); + for(insertAt = 1; insertAt < items; insertAt++) + { + auto thisPlugin = static_cast<PLUGINDEX>(comboBox.GetItemData(insertAt)); + if(thisPlugin == (plugin + 1)) + comboBox.DeleteString(insertAt); + if(thisPlugin >= (plugin + 1)) + break; + } + } + int fxsel = 0; + for(PLUGINDEX ifx = 0; ifx < MAX_MIXPLUGINS; ifx++) + { + if(plugin != PLUGINDEX_INVALID && ifx != plugin) + continue; + if(sndFile.m_MixPlugins[ifx].IsValidPlugin() + || (sndFile.m_MixPlugins[ifx].GetName() != U_("")) + || (sndFile.ChnSettings[nChn].nMixPlugin == ifx + 1)) + { + s = MPT_CFORMAT("FX{}: ")(ifx + 1); + s += mpt::ToCString(sndFile.m_MixPlugins[ifx].GetName()); + insertAt = comboBox.InsertString(insertAt, s); + comboBox.SetItemData(insertAt, ifx + 1); + if(sndFile.ChnSettings[nChn].nMixPlugin == ifx + 1) + fxsel = insertAt; + insertAt++; + } + } + comboBox.SetRedraw(TRUE); + if(plugin == PLUGINDEX_INVALID || fxsel > 0) + comboBox.SetCurSel(fxsel); + comboBox.Invalidate(FALSE); + } + } +} + + +IMixPlugin *CViewGlobals::GetCurrentPlugin() const +{ + if(GetDocument() == nullptr || m_nCurrentPlugin >= MAX_MIXPLUGINS) + { + return nullptr; + } + + return GetDocument()->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].pMixPlugin; +} + + +void CViewGlobals::OnTabSelchange(NMHDR*, LRESULT* pResult) +{ + UpdateView(GeneralHint().Channels()); + if (pResult) *pResult = 0; +} + + +void CViewGlobals::OnUpdateUndo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetPatternUndo().CanUndoChannelSettings()); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + pModDoc->GetPatternUndo().GetUndoName())); + } +} + + +void CViewGlobals::OnUpdateRedo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetPatternUndo().CanRedoChannelSettings()); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + pModDoc->GetPatternUndo().GetRedoName())); + } +} + + +void CViewGlobals::OnEditUndo() +{ + UndoRedo(true); +} + + +void CViewGlobals::OnEditRedo() +{ + UndoRedo(false); +} + + +void CViewGlobals::UndoRedo(bool undo) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + if(undo && pModDoc->GetPatternUndo().CanUndoChannelSettings()) + pModDoc->GetPatternUndo().Undo(); + else if(!undo && pModDoc->GetPatternUndo().CanRedoChannelSettings()) + pModDoc->GetPatternUndo().Redo(); +} + + +void CViewGlobals::PrepareUndo(CHANNELINDEX chnMod4) +{ + if(m_lastEdit != chnMod4) + { + // Backup old channel settings through pattern undo. + m_lastEdit = chnMod4; + const CHANNELINDEX chn = static_cast<CHANNELINDEX>(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + GetDocument()->GetPatternUndo().PrepareChannelUndo(chn, 1, "Channel Settings"); + } +} + + +void CViewGlobals::OnEditColor(const CHANNELINDEX chnMod4) +{ + auto *modDoc = GetDocument(); + auto &sndFile = modDoc->GetSoundFile(); + const CHANNELINDEX chn = static_cast<CHANNELINDEX>(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + if(auto color = m_channelColor[chnMod4].PickColor(sndFile, chn); color.has_value()) + { + PrepareUndo(chnMod4); + sndFile.ChnSettings[chn].color = *color; + if(modDoc->SupportsChannelColors()) + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, GeneralHint(chn).Channels()); + } +} + + +void CViewGlobals::OnEditColor1() { OnEditColor(0); } +void CViewGlobals::OnEditColor2() { OnEditColor(1); } +void CViewGlobals::OnEditColor3() { OnEditColor(2); } +void CViewGlobals::OnEditColor4() { OnEditColor(3); } + + +void CViewGlobals::OnMute(const CHANNELINDEX chnMod4, const UINT itemID) +{ + CModDoc *pModDoc = GetDocument(); + + if (pModDoc) + { + const bool b = (IsDlgButtonChecked(itemID) != FALSE); + const CHANNELINDEX nChn = (CHANNELINDEX)(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + pModDoc->MuteChannel(nChn, b); + pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + } +} + +void CViewGlobals::OnMute1() {OnMute(0, IDC_CHECK1);} +void CViewGlobals::OnMute2() {OnMute(1, IDC_CHECK3);} +void CViewGlobals::OnMute3() {OnMute(2, IDC_CHECK5);} +void CViewGlobals::OnMute4() {OnMute(3, IDC_CHECK7);} + + +void CViewGlobals::OnSurround(const CHANNELINDEX chnMod4, const UINT itemID) +{ + CModDoc *pModDoc = GetDocument(); + + if (pModDoc) + { + PrepareUndo(chnMod4); + const bool b = (IsDlgButtonChecked(itemID) != FALSE); + const CHANNELINDEX nChn = (CHANNELINDEX)(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + pModDoc->SurroundChannel(nChn, b); + pModDoc->UpdateAllViews(nullptr, GeneralHint(nChn).Channels()); + } +} + +void CViewGlobals::OnSurround1() {OnSurround(0, IDC_CHECK2);} +void CViewGlobals::OnSurround2() {OnSurround(1, IDC_CHECK4);} +void CViewGlobals::OnSurround3() {OnSurround(2, IDC_CHECK6);} +void CViewGlobals::OnSurround4() {OnSurround(3, IDC_CHECK8);} + +void CViewGlobals::OnEditVol(const CHANNELINDEX chnMod4, const UINT itemID) +{ + CModDoc *pModDoc = GetDocument(); + const CHANNELINDEX nChn = (CHANNELINDEX)(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + const int vol = GetDlgItemIntEx(itemID); + if ((pModDoc) && (vol >= 0) && (vol <= 64) && (!m_nLockCount)) + { + PrepareUndo(chnMod4); + if (pModDoc->SetChannelGlobalVolume(nChn, static_cast<uint16>(vol))) + { + m_sbVolume[chnMod4].SetPos(vol); + pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + } + } +} + +void CViewGlobals::OnEditVol1() {OnEditVol(0, IDC_EDIT1);} +void CViewGlobals::OnEditVol2() {OnEditVol(1, IDC_EDIT3);} +void CViewGlobals::OnEditVol3() {OnEditVol(2, IDC_EDIT5);} +void CViewGlobals::OnEditVol4() {OnEditVol(3, IDC_EDIT7);} + + +void CViewGlobals::OnEditPan(const CHANNELINDEX chnMod4, const UINT itemID) +{ + CModDoc *pModDoc = GetDocument(); + const CHANNELINDEX nChn = (CHANNELINDEX)(m_nActiveTab * CHANNELS_IN_TAB) + chnMod4; + const int pan = GetDlgItemIntEx(itemID); + if ((pModDoc) && (pan >= 0) && (pan <= 256) && (!m_nLockCount)) + { + PrepareUndo(chnMod4); + if (pModDoc->SetChannelDefaultPan(nChn, static_cast<uint16>(pan))) + { + m_sbPan[chnMod4].SetPos(pan / 4); + pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + // Surround is forced off when changing pan, so uncheck the checkbox. + CheckDlgButton(IDC_CHECK2 + chnMod4 * 2, BST_UNCHECKED); + } + } +} + + +void CViewGlobals::OnEditPan1() {OnEditPan(0, IDC_EDIT2);} +void CViewGlobals::OnEditPan2() {OnEditPan(1, IDC_EDIT4);} +void CViewGlobals::OnEditPan3() {OnEditPan(2, IDC_EDIT6);} +void CViewGlobals::OnEditPan4() {OnEditPan(3, IDC_EDIT8);} + + +void CViewGlobals::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + CModDoc *pModDoc; + CHANNELINDEX nChn; + + CFormView::OnHScroll(nSBCode, nPos, pScrollBar); + + pModDoc = GetDocument(); + nChn = (CHANNELINDEX)(m_nActiveTab * CHANNELS_IN_TAB); + if ((pModDoc) && (!IsLocked()) && (nChn < MAX_BASECHANNELS)) + { + BOOL bUpdate = FALSE; + short int pos; + + LockControls(); + const CHANNELINDEX nLoopLimit = std::min(static_cast<CHANNELINDEX>(CHANNELS_IN_TAB), static_cast<CHANNELINDEX>(pModDoc->GetSoundFile().GetNumChannels() - nChn)); + for (CHANNELINDEX iCh = 0; iCh < nLoopLimit; iCh++) + { + if(pScrollBar == (CScrollBar *) &m_sbVolume[iCh]) + { + // Volume sliders + pos = (short int)m_sbVolume[iCh].GetPos(); + if ((pos >= 0) && (pos <= 64)) + { + PrepareUndo(iCh); + if (pModDoc->SetChannelGlobalVolume(nChn + iCh, pos)) + { + SetDlgItemInt(IDC_EDIT1 + iCh * 2, pos); + bUpdate = TRUE; + } + } + } else if(pScrollBar == (CScrollBar *) &m_sbPan[iCh]) + { + // Pan sliders + pos = (short int)m_sbPan[iCh].GetPos(); + if(pos >= 0 && pos <= 64 && (static_cast<uint16>(pos) != pModDoc->GetSoundFile().ChnSettings[nChn+iCh].nPan / 4u)) + { + PrepareUndo(iCh); + if (pModDoc->SetChannelDefaultPan(nChn + iCh, pos * 4)) + { + SetDlgItemInt(IDC_EDIT2 + iCh * 2, pos * 4); + CheckDlgButton(IDC_CHECK2 + iCh * 2, BST_UNCHECKED); + bUpdate = TRUE; + } + } + } + } + + + if ((pScrollBar) && (pScrollBar->m_hWnd == m_sbDryRatio.m_hWnd)) + { + int n = 100 - m_sbDryRatio.GetPos(); + if ((n >= 0) && (n <= 100) && (m_nCurrentPlugin < MAX_MIXPLUGINS)) + { + SNDMIXPLUGIN &plugin = pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin]; + + if(plugin.pMixPlugin) + { + plugin.fDryRatio = static_cast<float>(n) / 100.0f; + SetPluginModified(); + } + UpdateDryWetDisplay(); + } + } + + if (bUpdate) pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + UnlockControls(); + + if ((pScrollBar) && (pScrollBar->m_hWnd == m_sbValue.m_hWnd)) + { + int n = (short int)m_sbValue.GetPos(); + if ((n >= 0) && (n <= 100) && (m_nCurrentPlugin < MAX_MIXPLUGINS)) + { + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin != nullptr) + { + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + if(m_nCurrentParam < nParams) + { + if (nSBCode == SB_THUMBPOSITION || nSBCode == SB_THUMBTRACK || nSBCode == SB_ENDSCROLL) + { + pPlugin->SetScaledUIParam(m_nCurrentParam, 0.01f * n); + OnParamChanged(); + SetPluginModified(); + } + } + } + } + } + } +} + + +void CViewGlobals::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + CModDoc *pModDoc = GetDocument(); + + if((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + TCHAR s[32]; + + if(nSBCode != SB_ENDSCROLL && pScrollBar && pScrollBar == (CScrollBar*)&m_SpinMixGain) + { + + SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + + if(plugin.pMixPlugin) + { + uint8 gain = (uint8)nPos; + if(gain == 0) gain = 1; + + plugin.SetGain(gain); + + float fValue = 0.1f * (float)gain; + _stprintf(s, _T("Gain: x%1.1f"), fValue); + CEdit *gainEdit = (CEdit *)GetDlgItem(IDC_EDIT16); + gainEdit->SetWindowText(s); + + SetPluginModified(); + } + } + + CFormView::OnVScroll(nSBCode, nPos, pScrollBar); +} + + +void CViewGlobals::OnEditName(const CHANNELINDEX chnMod4, const UINT itemID) +{ + CModDoc *pModDoc = GetDocument(); + + if ((pModDoc) && (!m_nLockCount)) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + const CHANNELINDEX nChn = m_nActiveTab * CHANNELS_IN_TAB + chnMod4; + CString tmp; + GetDlgItemText(itemID, tmp); + const std::string s = mpt::ToCharset(sndFile.GetCharsetInternal(), tmp); + if ((sndFile.GetType() & (MOD_TYPE_XM|MOD_TYPE_IT|MOD_TYPE_MPT)) && (nChn < sndFile.GetNumChannels()) && (s != sndFile.ChnSettings[nChn].szName)) + { + PrepareUndo(chnMod4); + sndFile.ChnSettings[nChn].szName = s; + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + } + } +} +void CViewGlobals::OnEditName1() {OnEditName(0, IDC_EDIT9);} +void CViewGlobals::OnEditName2() {OnEditName(1, IDC_EDIT10);} +void CViewGlobals::OnEditName3() {OnEditName(2, IDC_EDIT11);} +void CViewGlobals::OnEditName4() {OnEditName(3, IDC_EDIT12);} + + +void CViewGlobals::OnFxChanged(const CHANNELINDEX chnMod4) +{ + CModDoc *pModDoc = GetDocument(); + + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + CHANNELINDEX nChn = m_nActiveTab * CHANNELS_IN_TAB + chnMod4; + int nfx = static_cast<int>(m_CbnEffects[chnMod4].GetItemData(m_CbnEffects[chnMod4].GetCurSel())); + if ((nfx >= 0) && (nfx <= MAX_MIXPLUGINS) && (nChn < sndFile.GetNumChannels()) + && (sndFile.ChnSettings[nChn].nMixPlugin != (UINT)nfx)) + { + PrepareUndo(chnMod4); + sndFile.ChnSettings[nChn].nMixPlugin = (PLUGINDEX)nfx; + if(sndFile.GetModSpecifications().supportsPlugins) + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, GeneralHint(nChn).Channels()); + } + } +} + + +void CViewGlobals::OnFx1Changed() {OnFxChanged(0);} +void CViewGlobals::OnFx2Changed() {OnFxChanged(1);} +void CViewGlobals::OnFx3Changed() {OnFxChanged(2);} +void CViewGlobals::OnFx4Changed() {OnFxChanged(3);} + + +void CViewGlobals::OnPluginNameChanged() +{ + CModDoc *pModDoc = GetDocument(); + + if ((pModDoc) && (m_nCurrentPlugin < MAX_MIXPLUGINS)) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + + CString s; + GetDlgItemText(IDC_EDIT13, s); + if (s != mpt::ToCString(plugin.GetName())) + { + plugin.Info.szName = mpt::ToCharset(mpt::Charset::Locale, s); + if(sndFile.GetModSpecifications().supportsPlugins) + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, PluginHint(m_nCurrentPlugin + 1).Info().Names(), this); + + IMixPlugin *pPlugin = plugin.pMixPlugin; + if(pPlugin != nullptr && pPlugin->GetEditor() != nullptr) + { + pPlugin->GetEditor()->SetTitle(); + } + // Update channel plugin assignments + PopulateChannelPlugins(m_nCurrentPlugin); + + m_CbnPlugin.SetRedraw(FALSE); + AddPluginNamesToCombobox(m_CbnPlugin, sndFile.m_MixPlugins, true, m_nCurrentPlugin); + m_CbnPlugin.SetCurSel(m_nCurrentPlugin); + m_CbnPlugin.Invalidate(FALSE); + m_CbnPlugin.SetRedraw(TRUE); + } + } +} + + +void CViewGlobals::OnPrevPlugin() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin > 0) && (pModDoc)) + { + m_nCurrentPlugin--; + UpdateView(PluginHint(m_nCurrentPlugin + 1).Info()); + } +} + + +void CViewGlobals::OnNextPlugin() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin < MAX_MIXPLUGINS-1) && (pModDoc)) + { + m_nCurrentPlugin++; + UpdateView(PluginHint(m_nCurrentPlugin + 1).Info()); + + } +} + + +void CViewGlobals::OnPluginChanged() +{ + CModDoc *pModDoc = GetDocument(); + int nPlugin = m_CbnPlugin.GetCurSel(); + if ((pModDoc) && (nPlugin >= 0) && (nPlugin < MAX_MIXPLUGINS)) + { + m_nCurrentPlugin = (PLUGINDEX)nPlugin; + UpdateView(PluginHint(m_nCurrentPlugin + 1).Info()); + } + m_CbnPreset.SetCurSel(0); +} + + +void CViewGlobals::OnSelectPlugin() +{ +#ifndef NO_PLUGINS + CModDoc *pModDoc = GetDocument(); + + if ((pModDoc) && (m_nCurrentPlugin < MAX_MIXPLUGINS)) + { + CSelectPluginDlg dlg(pModDoc, m_nCurrentPlugin, this); + if (dlg.DoModal() == IDOK) + { + if(pModDoc->GetSoundFile().GetModSpecifications().supportsPlugins) + pModDoc->SetModified(); + } + OnPluginChanged(); + OnParamChanged(); + } +#endif // NO_PLUGINS +} + + +void CViewGlobals::OnRemovePlugin() +{ +#ifndef NO_PLUGINS + CModDoc *pModDoc = GetDocument(); + + if(pModDoc && m_nCurrentPlugin < MAX_MIXPLUGINS && Reporting::Confirm(MPT_UFORMAT("Remove plugin FX{}: {}?")(m_nCurrentPlugin + 1, pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].GetName()), false, true) == cnfYes) + { + if(pModDoc->RemovePlugin(m_nCurrentPlugin)) + { + OnPluginChanged(); + OnParamChanged(); + } + } +#endif // NO_PLUGINS +} + + +LRESULT CViewGlobals::OnParamAutomated(WPARAM plugin, LPARAM param) +{ + if(plugin == m_nCurrentPlugin && static_cast<PlugParamIndex>(param) == m_nCurrentParam) + { + OnParamChanged(); + } + return 0; +} + + +LRESULT CViewGlobals::OnDryWetRatioChangedFromPlayer(WPARAM plugin, LPARAM) +{ + if(plugin == m_nCurrentPlugin) + { + UpdateDryWetDisplay(); + } + return 0; +} + + +void CViewGlobals::OnParamChanged() +{ + PlugParamIndex cursel = static_cast<PlugParamIndex>(m_CbnParam.GetItemData(m_CbnParam.GetCurSel())); + + IMixPlugin *pPlugin = GetCurrentPlugin(); + + if(pPlugin != nullptr && cursel != static_cast<PlugParamIndex>(CB_ERR)) + { + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + if(cursel < nParams) m_nCurrentParam = cursel; + if(m_nCurrentParam < nParams) + { + const auto value = pPlugin->GetScaledUIParam(m_nCurrentParam); + int intValue = mpt::saturate_round<int>(value * 100.0f); + LockControls(); + if(GetFocus() != GetDlgItem(IDC_EDIT14)) + { + CString s = pPlugin->GetFormattedParamValue(m_nCurrentParam).Trim(); + if(s.IsEmpty()) + { + s.Format(_T("%f"), value); + } + SetDlgItemText(IDC_EDIT14, s); + } + m_sbValue.SetPos(intValue); + UnlockControls(); + return; + } + } + SetDlgItemText(IDC_EDIT14, _T("")); + m_sbValue.SetPos(0); +} + + +// When focussing the parameter value, show its real value to edit +void CViewGlobals::OnFocusParam() +{ + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin != nullptr) + { + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + if(m_nCurrentParam < nParams) + { + TCHAR s[32]; + float fValue = pPlugin->GetScaledUIParam(m_nCurrentParam); + _stprintf(s, _T("%f"), fValue); + LockControls(); + SetDlgItemText(IDC_EDIT14, s); + UnlockControls(); + } + } +} + + +void CViewGlobals::SetPluginModified() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc->GetSoundFile().GetModSpecifications().supportsPlugins) + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, PluginHint(m_nCurrentPlugin + 1).Info()); +} + + +void CViewGlobals::OnProgramChanged() +{ + int32 curProg = static_cast<int32>(m_CbnPreset.GetItemData(m_CbnPreset.GetCurSel())); + + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin != nullptr) + { + const int32 numProgs = pPlugin->GetNumPrograms(); + if(curProg <= numProgs) + { + pPlugin->SetCurrentProgram(curProg); + // Update parameter display + OnParamChanged(); + + SetPluginModified(); + } + } +} + + +void CViewGlobals::OnLoadParam() +{ + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin != nullptr && pPlugin->LoadProgram()) + { + int32 currentProg = pPlugin->GetCurrentProgram(); + FillPluginProgramBox(currentProg, currentProg); + m_CbnPreset.SetCurSel(0); + SetPluginModified(); + } +} + + +void CViewGlobals::OnSaveParam() +{ + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin != nullptr) + { + pPlugin->SaveProgram(); + } +} + + +void CViewGlobals::OnSetParameter() +{ + if(m_nCurrentPlugin >= MAX_MIXPLUGINS || IsLocked()) return; + IMixPlugin *pPlugin = GetCurrentPlugin(); + + if(pPlugin != nullptr) + { + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + TCHAR s[32]; + GetDlgItemText(IDC_EDIT14, s, mpt::saturate_cast<int>(std::size(s))); + if ((m_nCurrentParam < nParams) && (s[0])) + { + float fValue = (float)_tstof(s); + pPlugin->SetScaledUIParam(m_nCurrentParam, fValue); + OnParamChanged(); + SetPluginModified(); + } + } +} + + +void CViewGlobals::UpdateDryWetDisplay() +{ + SNDMIXPLUGIN &plugin = GetDocument()->GetSoundFile().m_MixPlugins[m_nCurrentPlugin]; + float wetRatio = 1.0f - plugin.fDryRatio, dryRatio = plugin.fDryRatio; + m_sbDryRatio.SetPos(mpt::saturate_round<int>(wetRatio * 100)); + if(plugin.IsExpandedMix()) + { + wetRatio = 2.0f * wetRatio - 1.0f; + dryRatio = -wetRatio; + } + int wetInt = mpt::saturate_round<int>(wetRatio * 100), dryInt = mpt::saturate_round<int>(dryRatio * 100); + SetDlgItemText(IDC_STATIC8, MPT_TFORMAT("{}% wet, {}% dry")(wetInt, dryInt).c_str()); +} + + +void CViewGlobals::OnMixModeChanged() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + + pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].SetMasterEffect(IsDlgButtonChecked(IDC_CHECK9) != BST_UNCHECKED); + SetPluginModified(); +} + + +void CViewGlobals::OnBypassChanged() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + + pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].SetBypass(IsDlgButtonChecked(IDC_CHECK10) != BST_UNCHECKED); + SetPluginModified(); +} + + +void CViewGlobals::OnWetDryExpandChanged() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + + pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].SetExpandedMix(IsDlgButtonChecked(IDC_CHECK12) != BST_UNCHECKED); + UpdateDryWetDisplay(); + SetPluginModified(); +} + + +void CViewGlobals::OnSpecialMixProcessingChanged() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + + pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].SetMixMode((uint8)m_CbnSpecialMixProcessing.GetCurSel()); + SetPluginModified(); +} + + +void CViewGlobals::OnDryMixChanged() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + + pModDoc->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].SetWetMix(IsDlgButtonChecked(IDC_CHECK11) != BST_UNCHECKED); + SetPluginModified(); +} + + +void CViewGlobals::OnEditPlugin() +{ + CModDoc *pModDoc = GetDocument(); + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + pModDoc->TogglePluginEditor(m_nCurrentPlugin, CMainFrame::GetInputHandler()->ShiftPressed()); + return; +} + + +void CViewGlobals::OnOutputRoutingChanged() +{ + CModDoc *pModDoc = GetDocument(); + int nroute; + + if ((m_nCurrentPlugin >= MAX_MIXPLUGINS) || (!pModDoc)) return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + nroute = static_cast<int>(m_CbnOutput.GetItemData(m_CbnOutput.GetCurSel())); + + if(nroute == 1) + { + // Add new plugin + for(PLUGINDEX i = m_nCurrentPlugin + 1; i < MAX_MIXPLUGINS; i++) + { + if(!sndFile.m_MixPlugins[i].IsValidPlugin()) + { + CSelectPluginDlg dlg(pModDoc, i, this); + if(dlg.DoModal() != IDOK) + return; + + plugin.SetOutputPlugin(i); + SetPluginModified(); + nroute = 0x80 + i; + m_nCurrentPlugin = i; + m_CbnPlugin.SetCurSel(i); + OnPluginChanged(); + break; + } + } + if(nroute == 1) + { + Reporting::Error("All following plugin slots are used.", "Unable to add new plugin"); + return; + } + } + + if(!nroute) + plugin.SetOutputToMaster(); + else + plugin.SetOutputPlugin(static_cast<PLUGINDEX>(nroute - 0x80)); + + SetPluginModified(); +} + + + +LRESULT CViewGlobals::OnModViewMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + switch(wParam) + { + case VIEWMSG_SETFOCUS: + case VIEWMSG_SETACTIVE: + GetParentFrame()->SetActiveView(this); + SetFocus(); + return 0; + default: + return 0; + } +} + +void CViewGlobals::OnMovePlugToSlot() +{ + if(GetCurrentPlugin() == nullptr) + { + return; + } + + // If any plugin routes its output to the current plugin, we shouldn't try to move it before that plugin... + PLUGINDEX defaultIndex = 0; + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + for(PLUGINDEX i = 0; i < m_nCurrentPlugin; i++) + { + if(sndFile.m_MixPlugins[i].GetOutputPlugin() == m_nCurrentPlugin) + { + defaultIndex = i + 1; + } + } + + std::vector<PLUGINDEX> emptySlots; + BuildEmptySlotList(emptySlots); + + CMoveFXSlotDialog dlg(this, m_nCurrentPlugin, emptySlots, defaultIndex, false, !sndFile.m_MixPlugins[m_nCurrentPlugin].IsOutputToMaster()); + + if(dlg.DoModal() == IDOK) + { + size_t toIndex = dlg.GetSlotIndex(); + do + { + const SNDMIXPLUGIN &curPlugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + SNDMIXPLUGIN &newPlugin = sndFile.m_MixPlugins[emptySlots[toIndex]]; + const PLUGINDEX nextPlugin = curPlugin.GetOutputPlugin(); + + MovePlug(m_nCurrentPlugin, emptySlots[toIndex]); + + if(nextPlugin == PLUGINDEX_INVALID || toIndex == emptySlots.size() - 1) + { + break; + } + + m_nCurrentPlugin = nextPlugin; + + if(dlg.DoMoveChain()) + { + toIndex++; + newPlugin.SetOutputPlugin(emptySlots[toIndex]); + } + } while(dlg.DoMoveChain()); + + m_CbnPlugin.SetCurSel(dlg.GetSlot()); + OnPluginChanged(); + GetDocument()->UpdateAllViews(nullptr, PluginHint().Names().Info()); + } +} + + +// Functor for adjusting plug indexes in modcommands. Adjusts all instrument column values in +// range [m_nInstrMin, m_nInstrMax] by m_nDiff. +struct PlugIndexModifier +{ + PlugIndexModifier(PLUGINDEX nMin, PLUGINDEX nMax, int nDiff) : + m_nDiff(nDiff), m_nInstrMin(nMin), m_nInstrMax(nMax) {} + void operator()(ModCommand& m) + { + if (m.IsInstrPlug() && m.instr >= m_nInstrMin && m.instr <= m_nInstrMax) + m.instr = (ModCommand::INSTR)((int)m.instr + m_nDiff); + } + int m_nDiff; + ModCommand::INSTR m_nInstrMin; + ModCommand::INSTR m_nInstrMax; +}; + + +bool CViewGlobals::MovePlug(PLUGINDEX src, PLUGINDEX dest, bool bAdjustPat) +{ + if (src == dest) + return false; + CModDoc *pModDoc = GetDocument(); + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + BeginWaitCursor(); + + CriticalSection cs; + + // Move plug data + sndFile.m_MixPlugins[dest] = std::move(sndFile.m_MixPlugins[src]); + sndFile.m_MixPlugins[src] = SNDMIXPLUGIN(); + + // Prevent plug from pointing backwards. + if(!sndFile.m_MixPlugins[dest].IsOutputToMaster()) + { + PLUGINDEX nOutput = sndFile.m_MixPlugins[dest].GetOutputPlugin(); + if (nOutput <= dest && nOutput != PLUGINDEX_INVALID) + { + sndFile.m_MixPlugins[dest].SetOutputToMaster(); + } + } + + // Update current plug + IMixPlugin *pPlugin = sndFile.m_MixPlugins[dest].pMixPlugin; + if(pPlugin != nullptr) + { + pPlugin->SetSlot(dest); + if(pPlugin->GetEditor() != nullptr) + { + pPlugin->GetEditor()->SetTitle(); + } + } + + // Update all other plugs' outputs + for (PLUGINDEX nPlug = 0; nPlug < src; nPlug++) + { + if(!sndFile.m_MixPlugins[nPlug].IsOutputToMaster()) + { + if(sndFile.m_MixPlugins[nPlug].GetOutputPlugin() == src) + { + sndFile.m_MixPlugins[nPlug].SetOutputPlugin(dest); + } + } + } + // Update channels + for (CHANNELINDEX nChn = 0; nChn < sndFile.GetNumChannels(); nChn++) + { + if (sndFile.ChnSettings[nChn].nMixPlugin == src + 1u) + { + sndFile.ChnSettings[nChn].nMixPlugin = dest + 1u; + } + } + + // Update instruments + for (INSTRUMENTINDEX nIns = 1; nIns <= sndFile.GetNumInstruments(); nIns++) + { + if (sndFile.Instruments[nIns] && (sndFile.Instruments[nIns]->nMixPlug == src + 1)) + { + sndFile.Instruments[nIns]->nMixPlug = dest + 1u; + } + } + + // Update MODCOMMANDs so that they won't be referring to old indexes (e.g. with NOTE_PC). + if (bAdjustPat && sndFile.GetModSpecifications().HasNote(NOTE_PC)) + sndFile.Patterns.ForEachModCommand(PlugIndexModifier(src + 1, src + 1, int(dest) - int(src))); + + cs.Leave(); + + SetPluginModified(); + + EndWaitCursor(); + + return true; +} + + +void CViewGlobals::BuildEmptySlotList(std::vector<PLUGINDEX> &emptySlots) +{ + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + + emptySlots.clear(); + + for(PLUGINDEX nSlot = 0; nSlot < MAX_MIXPLUGINS; nSlot++) + { + if(sndFile.m_MixPlugins[nSlot].pMixPlugin == nullptr) + { + emptySlots.push_back(nSlot); + } + } + return; +} + +void CViewGlobals::OnInsertSlot() +{ + CString prompt; + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + prompt.Format(_T("Insert empty slot before slot FX%d?"), m_nCurrentPlugin + 1); + + // If last plugin slot is occupied, move it so that the plugin is not lost. + // This could certainly be improved... + bool moveLastPlug = false; + + if(sndFile.m_MixPlugins[MAX_MIXPLUGINS - 1].pMixPlugin) + { + if(sndFile.m_MixPlugins[MAX_MIXPLUGINS - 2].pMixPlugin == nullptr) + { + moveLastPlug = true; + } else + { + prompt += _T("\nWarning: plugin data in last slot will be lost."); + } + } + if(Reporting::Confirm(prompt) == cnfYes) + { + + // Delete last plug... + if(sndFile.m_MixPlugins[MAX_MIXPLUGINS - 1].pMixPlugin) + { + if(moveLastPlug) + { + MovePlug(MAX_MIXPLUGINS - 1, MAX_MIXPLUGINS - 2, true); + } else + { + sndFile.m_MixPlugins[MAX_MIXPLUGINS - 1].Destroy(); + MemsetZero(sndFile.m_MixPlugins[MAX_MIXPLUGINS - 1].Info); + } + } + + // Update MODCOMMANDs so that they won't be referring to old indexes (e.g. with NOTE_PC). + if(sndFile.GetModSpecifications().HasNote(NOTE_PC)) + sndFile.Patterns.ForEachModCommand(PlugIndexModifier(m_nCurrentPlugin + 1, MAX_MIXPLUGINS - 1, 1)); + + + for(PLUGINDEX nSlot = MAX_MIXPLUGINS - 1; nSlot > m_nCurrentPlugin; nSlot--) + { + if(sndFile.m_MixPlugins[nSlot-1].pMixPlugin) + { + MovePlug(nSlot - 1, nSlot, NoPatternAdjust); + } + } + + m_CbnPlugin.SetCurSel(m_nCurrentPlugin + 1); + OnPluginChanged(); + GetDocument()->UpdateAllViews(nullptr, PluginHint().Names().Info()); + + SetPluginModified(); + } + +} + + +void CViewGlobals::OnClonePlug() +{ + if(GetCurrentPlugin() == nullptr) + { + return; + } + + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + + std::vector<PLUGINDEX> emptySlots; + BuildEmptySlotList(emptySlots); + + CMoveFXSlotDialog dlg(this, m_nCurrentPlugin, emptySlots, 0, true, !sndFile.m_MixPlugins[m_nCurrentPlugin].IsOutputToMaster()); + + if(dlg.DoModal() == IDOK) + { + size_t toIndex = dlg.GetSlotIndex(); + do + { + const SNDMIXPLUGIN &curPlugin = sndFile.m_MixPlugins[m_nCurrentPlugin]; + SNDMIXPLUGIN &newPlugin = sndFile.m_MixPlugins[emptySlots[toIndex]]; + + GetDocument()->ClonePlugin(newPlugin, curPlugin); + IMixPlugin *mixPlug = newPlugin.pMixPlugin; + if(mixPlug != nullptr && mixPlug->IsInstrument() && GetDocument()->HasInstrumentForPlugin(emptySlots[toIndex]) == INSTRUMENTINDEX_INVALID) + { + GetDocument()->InsertInstrumentForPlugin(emptySlots[toIndex]); + } + + if(curPlugin.IsOutputToMaster() || toIndex == emptySlots.size() - 1) + { + break; + } + + m_nCurrentPlugin = curPlugin.GetOutputPlugin(); + + if(dlg.DoMoveChain()) + { + toIndex++; + newPlugin.SetOutputPlugin(emptySlots[toIndex]); + } + } while(dlg.DoMoveChain()); + + m_CbnPlugin.SetCurSel(dlg.GetSlot()); + OnPluginChanged(); + PopulateChannelPlugins(); + GetDocument()->UpdateAllViews(this, PluginHint().Names(), this); + + SetPluginModified(); + } +} + + +// The plugin param box is only filled when it gets the focus (done here). +void CViewGlobals::OnFillParamCombo() +{ + // no need to fill it again. + if(m_CbnParam.GetCount() > 1) + return; + + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin == nullptr) return; + + const PlugParamIndex nParams = pPlugin->GetNumParameters(); + m_CbnParam.SetRedraw(FALSE); + m_CbnParam.ResetContent(); + + AddPluginParameternamesToCombobox(m_CbnParam, *pPlugin); + + if (m_nCurrentParam >= nParams) m_nCurrentParam = 0; + m_CbnParam.SetCurSel(m_nCurrentParam); + m_CbnParam.SetRedraw(TRUE); + m_CbnParam.Invalidate(FALSE); +} + + +// The preset box is only filled when it gets the focus (done here). +void CViewGlobals::OnFillProgramCombo() +{ + // no need to fill it again. + if(m_CbnPreset.GetCount() > 1) + return; + + IMixPlugin *pPlugin = GetCurrentPlugin(); + if(pPlugin == nullptr) return; + + FillPluginProgramBox(0, pPlugin->GetNumPrograms() - 1); + m_CbnPreset.SetCurSel(pPlugin->GetCurrentProgram()); +} + + +void CViewGlobals::FillPluginProgramBox(int32 firstProg, int32 lastProg) +{ + IMixPlugin *pPlugin = GetCurrentPlugin(); + + m_CbnPreset.SetRedraw(FALSE); + m_CbnPreset.ResetContent(); + + pPlugin->CacheProgramNames(firstProg, lastProg + 1); + for (int32 i = firstProg; i <= lastProg; i++) + { + m_CbnPreset.SetItemData(m_CbnPreset.AddString(pPlugin->GetFormattedProgramName(i)), i); + } + + m_CbnPreset.SetRedraw(TRUE); + m_CbnPreset.Invalidate(FALSE); +} + + +BOOL CViewGlobals::OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult) +{ + auto pTTT = reinterpret_cast<TOOLTIPTEXT *>(pNMHDR); + UINT_PTR id = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + id = static_cast<UINT_PTR>(::GetDlgCtrlID(reinterpret_cast<HWND>(id))); + } + + mpt::tstring text; + const auto &chnSettings = GetDocument()->GetSoundFile().ChnSettings; + switch(id) + { + case IDC_EDIT1: + case IDC_EDIT3: + case IDC_EDIT5: + case IDC_EDIT7: + text = CModDoc::LinearToDecibels(chnSettings[m_nActiveTab * CHANNELS_IN_TAB + (id - IDC_EDIT1) / 2].nVolume, 64.0); + break; + case IDC_SLIDER1: + case IDC_SLIDER3: + case IDC_SLIDER5: + case IDC_SLIDER7: + text = CModDoc::LinearToDecibels(chnSettings[m_nActiveTab * CHANNELS_IN_TAB + (id - IDC_SLIDER1) / 2].nVolume, 64.0); + break; + case IDC_EDIT2: + case IDC_EDIT4: + case IDC_EDIT6: + case IDC_EDIT8: + text = CModDoc::PanningToString(chnSettings[m_nActiveTab * CHANNELS_IN_TAB + (id - IDC_EDIT2) / 2].nPan, 128); + break; + case IDC_SLIDER2: + case IDC_SLIDER4: + case IDC_SLIDER6: + case IDC_SLIDER8: + text = CModDoc::PanningToString(chnSettings[m_nActiveTab * CHANNELS_IN_TAB + (id - IDC_SLIDER2) / 2].nPan, 128); + break; + case IDC_EDIT16: + { + const auto gain = GetDocument()->GetSoundFile().m_MixPlugins[m_nCurrentPlugin].GetGain(); + text = CModDoc::LinearToDecibels(gain ? gain : 10, 10.0); + } + break; + case IDC_BUTTON5: + text = _T("Previous Plugin"); + break; + case IDC_BUTTON4: + text = _T("Next Plugin"); + break; + default: + return FALSE; + } + + mpt::String::WriteWinBuf(pTTT->szText) = text; + *pResult = 0; + + // bring the tooltip window above other popup windows + ::SetWindowPos(pNMHDR->hwndFrom, HWND_TOP, 0, 0, 0, 0, SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOOWNERZORDER); + + return TRUE; // message was handled +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.h b/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.h new file mode 100644 index 00000000..3833ae92 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_gen.h @@ -0,0 +1,169 @@ +/* + * view_gen.h + * ---------- + * Purpose: General tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "ColorPickerButton.h" + +OPENMPT_NAMESPACE_BEGIN + +//Note: Changing this won't increase the number of tabs in general view. Most +//of the code use plain number 4. +#define CHANNELS_IN_TAB 4 + +class CViewGlobals: public CFormView +{ +protected: + CRect m_rcClient; + CTabCtrl m_TabCtrl; + CComboBox m_CbnEffects[CHANNELS_IN_TAB]; + CComboBox m_CbnPlugin, m_CbnParam, m_CbnOutput; + + CSliderCtrl m_sbVolume[CHANNELS_IN_TAB], m_sbPan[CHANNELS_IN_TAB], m_sbValue, m_sbDryRatio; + ColorPickerButton m_channelColor[CHANNELS_IN_TAB]; + + CComboBox m_CbnPreset; + CSliderCtrl m_sbWetDry; + CSpinButtonCtrl m_spinVolume[CHANNELS_IN_TAB], m_spinPan[CHANNELS_IN_TAB]; + CButton m_BtnSelect, m_BtnEdit; + int m_nLockCount = 1; + PlugParamIndex m_nCurrentParam = 0; + CHANNELINDEX m_nActiveTab = 0; + CHANNELINDEX m_lastEdit = CHANNELINDEX_INVALID; + PLUGINDEX m_nCurrentPlugin = 0; + + CComboBox m_CbnSpecialMixProcessing; + CSpinButtonCtrl m_SpinMixGain; + + enum {AdjustPattern = true, NoPatternAdjust = false}; + +protected: + CViewGlobals() : CFormView(IDD_VIEW_GLOBALS) { } + DECLARE_SERIAL(CViewGlobals) + +public: + CModDoc* GetDocument() const { return static_cast<CModDoc *>(m_pDocument); } + void RecalcLayout(); + void LockControls() { m_nLockCount++; } + void UnlockControls() { PostMessage(WM_MOD_UNLOCKCONTROLS); } + bool IsLocked() const noexcept { return (m_nLockCount > 0); } + int GetDlgItemIntEx(UINT nID); + void PopulateChannelPlugins(PLUGINDEX plugin = PLUGINDEX_INVALID); + void BuildEmptySlotList(std::vector<PLUGINDEX> &emptySlots); + bool MovePlug(PLUGINDEX src, PLUGINDEX dest, bool bAdjustPat = AdjustPattern); + +public: + //{{AFX_VIRTUAL(CViewGlobals) + void OnInitialUpdate() override; + void DoDataExchange(CDataExchange *pDX) override; + void OnUpdate(CView *pSender, LPARAM lHint, CObject *pHint) override; + + void UpdateView(UpdateHint hint, CObject *pObj = nullptr); + LRESULT OnModViewMsg(WPARAM, LPARAM); + LRESULT OnMidiMsg(WPARAM midiData, LPARAM); + +private: + void PrepareUndo(CHANNELINDEX chnMod4); + void UndoRedo(bool undo); + + void OnEditColor(const CHANNELINDEX chnMod4); + void OnMute(const CHANNELINDEX chnMod4, const UINT itemID); + void OnSurround(const CHANNELINDEX chnMod4, const UINT itemID); + void OnEditVol(const CHANNELINDEX chnMod4, const UINT itemID); + void OnEditPan(const CHANNELINDEX chnMod4, const UINT itemID); + void OnEditName(const CHANNELINDEX chnMod4, const UINT itemID); + void OnFxChanged(const CHANNELINDEX chnMod4); + + IMixPlugin *GetCurrentPlugin() const; + + void FillPluginProgramBox(int32 firstProg, int32 lastProg); + void SetPluginModified(); + + void UpdateDryWetDisplay(); + +protected: + //{{AFX_MSG(CViewGlobals) + afx_msg void OnEditUndo(); + afx_msg void OnEditRedo(); + afx_msg void OnUpdateUndo(CCmdUI *pCmdUI); + afx_msg void OnUpdateRedo(CCmdUI *pCmdUI); + + afx_msg void OnEditColor1(); + afx_msg void OnEditColor2(); + afx_msg void OnEditColor3(); + afx_msg void OnEditColor4(); + afx_msg void OnMute1(); + afx_msg void OnMute2(); + afx_msg void OnMute3(); + afx_msg void OnMute4(); + afx_msg void OnSurround1(); + afx_msg void OnSurround2(); + afx_msg void OnSurround3(); + afx_msg void OnSurround4(); + afx_msg void OnEditVol1(); + afx_msg void OnEditVol2(); + afx_msg void OnEditVol3(); + afx_msg void OnEditVol4(); + afx_msg void OnEditPan1(); + afx_msg void OnEditPan2(); + afx_msg void OnEditPan3(); + afx_msg void OnEditPan4(); + afx_msg void OnEditName1(); + afx_msg void OnEditName2(); + afx_msg void OnEditName3(); + afx_msg void OnEditName4(); + afx_msg void OnFx1Changed(); + afx_msg void OnFx2Changed(); + afx_msg void OnFx3Changed(); + afx_msg void OnFx4Changed(); + afx_msg void OnPluginChanged(); + afx_msg void OnPluginNameChanged(); + afx_msg void OnFillParamCombo(); + afx_msg void OnParamChanged(); + afx_msg void OnFocusParam(); + afx_msg void OnFillProgramCombo(); + afx_msg void OnProgramChanged(); + afx_msg void OnLoadParam(); + afx_msg void OnSaveParam(); + afx_msg void OnSelectPlugin(); + afx_msg void OnRemovePlugin(); + afx_msg void OnSetParameter(); + afx_msg void OnEditPlugin(); + afx_msg void OnMixModeChanged(); + afx_msg void OnBypassChanged(); + afx_msg void OnDryMixChanged(); + afx_msg void OnMovePlugToSlot(); + afx_msg void OnInsertSlot(); + afx_msg void OnClonePlug(); + LRESULT OnParamAutomated(WPARAM plugin, LPARAM param); + LRESULT OnDryWetRatioChangedFromPlayer(WPARAM plugin, LPARAM); + + afx_msg void OnWetDryExpandChanged(); + afx_msg void OnSpecialMixProcessingChanged(); + + afx_msg void OnOutputRoutingChanged(); + afx_msg void OnPrevPlugin(); + afx_msg void OnNextPlugin(); + afx_msg void OnDestroy(); + afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnTabSelchange(NMHDR* pNMHDR, LRESULT* pResult); + afx_msg LRESULT OnMDIDeactivate(WPARAM, LPARAM); + afx_msg LRESULT OnUnlockControls(WPARAM, LPARAM) { if (m_nLockCount > 0) m_nLockCount--; return 0; } + afx_msg BOOL OnToolTipText(UINT, NMHDR *pNMHDR, LRESULT *pResult); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.cpp new file mode 100644 index 00000000..6426c0d7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.cpp @@ -0,0 +1,2954 @@ +/* + * view_ins.cpp + * ------------ + * Purpose: Instrument tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "ImageLists.h" +#include "Childfrm.h" +#include "Moddoc.h" +#include "Globals.h" +#include "Ctrl_ins.h" +#include "View_ins.h" +#include "Dlsbank.h" +#include "ChannelManagerDlg.h" +#include "ScaleEnvPointsDlg.h" +#include "../soundlib/MIDIEvents.h" +#include "../soundlib/mod_specifications.h" +#include "../common/mptStringBuffer.h" +#include "FileDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + +namespace +{ + const int ENV_POINT_SIZE = 4; + const float ENV_MIN_ZOOM = 2.0f; + const float ENV_MAX_ZOOM = 256.0f; +} + + +// Non-client toolbar +#define ENV_LEFTBAR_CY Util::ScalePixels(29, m_hWnd) +#define ENV_LEFTBAR_CXSEP Util::ScalePixels(14, m_hWnd) +#define ENV_LEFTBAR_CXSPC Util::ScalePixels(3, m_hWnd) +#define ENV_LEFTBAR_CXBTN Util::ScalePixels(24, m_hWnd) +#define ENV_LEFTBAR_CYBTN Util::ScalePixels(22, m_hWnd) + + +static constexpr UINT cLeftBarButtons[ENV_LEFTBAR_BUTTONS] = +{ + ID_ENVSEL_VOLUME, + ID_ENVSEL_PANNING, + ID_ENVSEL_PITCH, + ID_SEPARATOR, + ID_ENVELOPE_VOLUME, + ID_ENVELOPE_PANNING, + ID_ENVELOPE_PITCH, + ID_ENVELOPE_FILTER, + ID_SEPARATOR, + ID_ENVELOPE_SETLOOP, + ID_ENVELOPE_SUSTAIN, + ID_ENVELOPE_CARRY, + ID_SEPARATOR, + ID_INSTRUMENT_SAMPLEMAP, + ID_SEPARATOR, + ID_ENVELOPE_VIEWGRID, + ID_SEPARATOR, + ID_ENVELOPE_ZOOM_IN, + ID_ENVELOPE_ZOOM_OUT, + ID_SEPARATOR, + ID_ENVELOPE_LOAD, + ID_ENVELOPE_SAVE, +}; + + +IMPLEMENT_SERIAL(CViewInstrument, CModScrollView, 0) + +BEGIN_MESSAGE_MAP(CViewInstrument, CModScrollView) + //{{AFX_MSG_MAP(CViewInstrument) +#if !defined(MPT_BUILD_RETRO) + ON_MESSAGE(WM_DPICHANGED, &CViewInstrument::OnDPIChanged) +#endif + ON_WM_ERASEBKGND() + ON_WM_SETFOCUS() + ON_WM_SIZE() + ON_WM_NCCALCSIZE() + ON_WM_NCPAINT() + ON_WM_NCHITTEST() + ON_WM_MOUSEMOVE() + ON_WM_NCMOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONUP() + ON_WM_LBUTTONDBLCLK() + ON_WM_RBUTTONDOWN() + ON_WM_MBUTTONDOWN() + ON_WM_XBUTTONUP() + ON_WM_NCLBUTTONDOWN() + ON_WM_NCLBUTTONUP() + ON_WM_NCLBUTTONDBLCLK() + ON_WM_DROPFILES() + ON_COMMAND(ID_PREVINSTRUMENT, &CViewInstrument::OnPrevInstrument) + ON_COMMAND(ID_NEXTINSTRUMENT, &CViewInstrument::OnNextInstrument) + ON_COMMAND(ID_ENVELOPE_SETLOOP, &CViewInstrument::OnEnvLoopChanged) + ON_COMMAND(ID_ENVELOPE_SUSTAIN, &CViewInstrument::OnEnvSustainChanged) + ON_COMMAND(ID_ENVELOPE_CARRY, &CViewInstrument::OnEnvCarryChanged) + ON_COMMAND(ID_ENVELOPE_INSERTPOINT, &CViewInstrument::OnEnvInsertPoint) + ON_COMMAND(ID_ENVELOPE_REMOVEPOINT, &CViewInstrument::OnEnvRemovePoint) + ON_COMMAND(ID_ENVELOPE_VOLUME, &CViewInstrument::OnEnvVolChanged) + ON_COMMAND(ID_ENVELOPE_PANNING, &CViewInstrument::OnEnvPanChanged) + ON_COMMAND(ID_ENVELOPE_PITCH, &CViewInstrument::OnEnvPitchChanged) + ON_COMMAND(ID_ENVELOPE_FILTER, &CViewInstrument::OnEnvFilterChanged) + ON_COMMAND(ID_ENVELOPE_VIEWGRID, &CViewInstrument::OnEnvToggleGrid) + ON_COMMAND(ID_ENVELOPE_ZOOM_IN, &CViewInstrument::OnEnvZoomIn) + ON_COMMAND(ID_ENVELOPE_ZOOM_OUT, &CViewInstrument::OnEnvZoomOut) + ON_COMMAND(ID_ENVELOPE_LOAD, &CViewInstrument::OnEnvLoad) + ON_COMMAND(ID_ENVELOPE_SAVE, &CViewInstrument::OnEnvSave) + ON_COMMAND(ID_ENVSEL_VOLUME, &CViewInstrument::OnSelectVolumeEnv) + ON_COMMAND(ID_ENVSEL_PANNING, &CViewInstrument::OnSelectPanningEnv) + ON_COMMAND(ID_ENVSEL_PITCH, &CViewInstrument::OnSelectPitchEnv) + ON_COMMAND(ID_EDIT_COPY, &CViewInstrument::OnEditCopy) + ON_COMMAND(ID_EDIT_PASTE, &CViewInstrument::OnEditPaste) + ON_COMMAND(ID_EDIT_UNDO, &CViewInstrument::OnEditUndo) + ON_COMMAND(ID_EDIT_REDO, &CViewInstrument::OnEditRedo) + ON_COMMAND(ID_INSTRUMENT_SAMPLEMAP, &CViewInstrument::OnEditSampleMap) + ON_COMMAND(ID_ENVELOPE_TOGGLERELEASENODE, &CViewInstrument::OnEnvToggleReleasNode) + ON_COMMAND(ID_ENVELOPE_SCALEPOINTS, &CViewInstrument::OnEnvelopeScalePoints) + + ON_MESSAGE(WM_MOD_MIDIMSG, &CViewInstrument::OnMidiMsg) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewInstrument::OnCustomKeyMsg) + + ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewInstrument::OnUpdateUndo) + ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewInstrument::OnUpdateRedo) + + //}}AFX_MSG_MAP + ON_WM_MOUSEWHEEL() +END_MESSAGE_MAP() + + +/////////////////////////////////////////////////////////////// +// CViewInstrument operations + +CViewInstrument::CViewInstrument() +{ + EnableActiveAccessibility(); + m_rcClient.bottom = 2; + m_dwNotifyPos.fill(uint32(Notification::PosInvalid)); + MemsetZero(m_NcButtonState); + + m_bmpEnvBar.Create(&CMainFrame::GetMainFrame()->m_EnvelopeIcons); + + m_baPlayingNote.reset(); +} + + +void CViewInstrument::OnInitialUpdate() +{ + CModScrollView::OnInitialUpdate(); + ModifyStyleEx(0, WS_EX_ACCEPTFILES); + m_zoom = (ENV_POINT_SIZE * m_nDPIx) / 96.0f; + m_envPointSize = Util::ScalePixels(ENV_POINT_SIZE, m_hWnd); + UpdateScrollSize(); + UpdateNcButtonState(); + EnableToolTips(); +} + + +void CViewInstrument::UpdateScrollSize() +{ + CModDoc *pModDoc = GetDocument(); + GetClientRect(&m_rcClient); + if(m_rcClient.bottom < 2) + m_rcClient.bottom = 2; + if(pModDoc) + { + SIZE sizeTotal, sizePage, sizeLine; + uint32 maxTick = EnvGetTick(EnvGetLastPoint()); + + sizeTotal.cx = mpt::saturate_round<int>((maxTick + 2) * m_zoom); + sizeTotal.cy = 1; + sizeLine.cx = mpt::saturate_round<int>(m_zoom); + sizeLine.cy = 2; + sizePage.cx = sizeLine.cx * 4; + sizePage.cy = sizeLine.cy; + SetScrollSizes(MM_TEXT, sizeTotal, sizePage, sizeLine); + } +} + + +LRESULT CViewInstrument::OnDPIChanged(WPARAM wParam, LPARAM lParam) +{ + LRESULT res = CModScrollView::OnDPIChanged(wParam, lParam); + m_envPointSize = Util::ScalePixels(4, m_hWnd); + return res; +} + + +void CViewInstrument::PrepareUndo(const char *description) +{ + GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, description, m_nEnv); +} + + +// Set instrument (and moddoc) as modified. +// updateAll: Update all views including this one. Otherwise, only update update other views. +void CViewInstrument::SetModified(InstrumentHint hint, bool updateAll) +{ + CModDoc *pModDoc = GetDocument(); + pModDoc->SetModified(); + pModDoc->UpdateAllViews(nullptr, hint.SetData(m_nInstrument), updateAll ? nullptr : this); + CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this); +} + + +BOOL CViewInstrument::SetCurrentInstrument(INSTRUMENTINDEX nIns, EnvelopeType nEnv) +{ + CModDoc *pModDoc = GetDocument(); + Notification::Type type; + + if((!pModDoc) || (nIns < 1) || (nIns >= MAX_INSTRUMENTS)) + return FALSE; + m_nEnv = nEnv; + m_nInstrument = nIns; + switch(m_nEnv) + { + case ENV_PANNING: type = Notification::PanEnv; break; + case ENV_PITCH: type = Notification::PitchEnv; break; + default: m_nEnv = ENV_VOLUME; type = Notification::VolEnv; break; + } + pModDoc->SetNotifications(type, m_nInstrument); + pModDoc->SetFollowWnd(m_hWnd); + UpdateScrollSize(); + UpdateNcButtonState(); + InvalidateRect(NULL, FALSE); + return TRUE; +} + + +void CViewInstrument::OnSetFocus(CWnd *pOldWnd) +{ + CScrollView::OnSetFocus(pOldWnd); + SetCurrentInstrument(m_nInstrument, m_nEnv); +} + + +LRESULT CViewInstrument::OnModViewMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case VIEWMSG_SETCURRENTINSTRUMENT: + SetCurrentInstrument(lParam & 0xFFFF, m_nEnv); + break; + + case VIEWMSG_LOADSTATE: + if(lParam) + { + INSTRUMENTVIEWSTATE *pState = (INSTRUMENTVIEWSTATE *)lParam; + if(pState->initialized) + { + m_zoom = pState->zoom; + SetCurrentInstrument(m_nInstrument, pState->nEnv); + m_bGrid = pState->bGrid; + } + } + break; + + case VIEWMSG_SAVESTATE: + if(lParam) + { + INSTRUMENTVIEWSTATE *pState = (INSTRUMENTVIEWSTATE *)lParam; + pState->initialized = true; + pState->zoom = m_zoom; + pState->nEnv = m_nEnv; + pState->bGrid = m_bGrid; + } + break; + + default: + return CModScrollView::OnModViewMsg(wParam, lParam); + } + return 0; +} + + +uint32 CViewInstrument::EnvGetTick(int nPoint) const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + if((nPoint >= 0) && (nPoint < (int)envelope->size())) + return envelope->at(nPoint).tick; + else + return 0; +} + + +uint32 CViewInstrument::EnvGetValue(int nPoint) const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + if(nPoint >= 0 && nPoint < (int)envelope->size()) + return envelope->at(nPoint).value; + else + return 0; +} + + +bool CViewInstrument::EnvSetValue(int nPoint, int32 nTick, int32 nValue, bool moveTail) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr || nPoint < 0) + return false; + + if(nPoint == 0) + { + nTick = 0; + moveTail = false; + } + int tickDiff = 0; + + bool ok = false; + if(nPoint < (int)envelope->size()) + { + if(nTick != int32_min) + { + nTick = std::max(0, nTick); + tickDiff = envelope->at(nPoint).tick; + int mintick = (nPoint > 0) ? envelope->at(nPoint - 1).tick : 0; + int maxtick; + if(nPoint + 1 >= (int)envelope->size() || moveTail) + maxtick = std::numeric_limits<decltype(maxtick)>::max(); + else + maxtick = envelope->at(nPoint + 1).tick; + + // Can't have multiple points on same tick + if(nPoint > 0 && mintick < maxtick - 1) + { + mintick++; + if(nPoint + 1 < (int)envelope->size()) + maxtick--; + } + if(nTick < mintick) + nTick = mintick; + if(nTick > maxtick) + nTick = maxtick; + if(nTick != envelope->at(nPoint).tick) + { + envelope->at(nPoint).tick = static_cast<EnvelopeNode::tick_t>(nTick); + ok = true; + } + } + const int maxVal = (GetDocument()->GetModType() != MOD_TYPE_XM || m_nEnv != ENV_PANNING) ? 64 : 63; + if(nValue != int32_min) + { + Limit(nValue, 0, maxVal); + if(nValue != envelope->at(nPoint).value) + { + envelope->at(nPoint).value = static_cast<EnvelopeNode::value_t>(nValue); + ok = true; + } + } + } + + if(ok && moveTail) + { + // Move all points after modified point as well. + tickDiff = envelope->at(nPoint).tick - tickDiff; + for(auto it = envelope->begin() + nPoint + 1; it != envelope->end(); it++) + { + it->tick = static_cast<EnvelopeNode::tick_t>(std::max(0, (int)it->tick + tickDiff)); + } + } + + return ok; +} + + +uint32 CViewInstrument::EnvGetNumPoints() const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + return envelope->size(); +} + + +uint32 CViewInstrument::EnvGetLastPoint() const +{ + uint32 nPoints = EnvGetNumPoints(); + if(nPoints > 0) + return nPoints - 1; + return 0; +} + + +// Return if an envelope flag is set. +bool CViewInstrument::EnvGetFlag(const EnvelopeFlags dwFlag) const +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv != nullptr) + return pEnv->dwFlags[dwFlag]; + return false; +} + + +uint32 CViewInstrument::EnvGetLoopStart() const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + return envelope->nLoopStart; +} + + +uint32 CViewInstrument::EnvGetLoopEnd() const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + return envelope->nLoopEnd; +} + + +uint32 CViewInstrument::EnvGetSustainStart() const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + return envelope->nSustainStart; +} + + +uint32 CViewInstrument::EnvGetSustainEnd() const +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return 0; + return envelope->nSustainEnd; +} + + +bool CViewInstrument::EnvGetVolEnv() const +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns) + return pIns->VolEnv.dwFlags[ENV_ENABLED] != 0; + return false; +} + + +bool CViewInstrument::EnvGetPanEnv() const +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns) + return pIns->PanEnv.dwFlags[ENV_ENABLED] != 0; + return false; +} + + +bool CViewInstrument::EnvGetPitchEnv() const +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns) + return ((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == ENV_ENABLED); + return false; +} + + +bool CViewInstrument::EnvGetFilterEnv() const +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns) + return ((pIns->PitchEnv.dwFlags & (ENV_ENABLED | ENV_FILTER)) == (ENV_ENABLED | ENV_FILTER)); + return false; +} + + +bool CViewInstrument::EnvSetLoopStart(int nPoint) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return false; + if(nPoint < 0 || nPoint > (int)EnvGetLastPoint()) + return false; + + if(nPoint != envelope->nLoopStart) + { + envelope->nLoopStart = static_cast<decltype(envelope->nLoopStart)>(nPoint); + if(envelope->nLoopEnd < nPoint) + envelope->nLoopEnd = static_cast<decltype(envelope->nLoopEnd)>(nPoint); + return true; + } else + { + return false; + } +} + + +bool CViewInstrument::EnvSetLoopEnd(int nPoint) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return false; + if(nPoint < 0 || nPoint > (int)EnvGetLastPoint()) + return false; + + if(nPoint != envelope->nLoopEnd) + { + envelope->nLoopEnd = static_cast<decltype(envelope->nLoopEnd)>(nPoint); + if(envelope->nLoopStart > nPoint) + envelope->nLoopStart = static_cast<decltype(envelope->nLoopStart)>(nPoint); + return true; + } else + { + return false; + } +} + + +bool CViewInstrument::EnvSetSustainStart(int nPoint) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return false; + if(nPoint < 0 || nPoint > (int)EnvGetLastPoint()) + return false; + + // We won't do any security checks here as GetEnvelopePtr() does that for us. + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + + if(nPoint != envelope->nSustainStart) + { + envelope->nSustainStart = static_cast<decltype(envelope->nSustainStart)>(nPoint); + if((envelope->nSustainEnd < nPoint) || (sndFile.GetType() & MOD_TYPE_XM)) + envelope->nSustainEnd = static_cast<decltype(envelope->nSustainEnd)>(nPoint); + return true; + } else + { + return false; + } +} + + +bool CViewInstrument::EnvSetSustainEnd(int nPoint) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return false; + if(nPoint < 0 || nPoint > (int)EnvGetLastPoint()) + return false; + + // We won't do any security checks here as GetEnvelopePtr() does that for us. + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + + if(nPoint != envelope->nSustainEnd) + { + envelope->nSustainEnd = static_cast<decltype(envelope->nSustainEnd)>(nPoint); + if((envelope->nSustainStart > nPoint) || (sndFile.GetType() & MOD_TYPE_XM)) + envelope->nSustainStart = static_cast<decltype(envelope->nSustainStart)>(nPoint); + return true; + } else + { + return false; + } +} + + +bool CViewInstrument::EnvToggleReleaseNode(int nPoint) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return false; + if(nPoint < 0 || nPoint >= (int)EnvGetNumPoints()) + return false; + + // Don't allow release nodes in IT/XM. GetDocument()/... nullptr check is done in GetEnvelopePtr, so no need to check twice. + if(!GetDocument()->GetSoundFile().GetModSpecifications().hasReleaseNode) + { + if(envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET) + { + envelope->nReleaseNode = ENV_RELEASE_NODE_UNSET; + return true; + } + return false; + } + + if(envelope->nReleaseNode == nPoint) + { + envelope->nReleaseNode = ENV_RELEASE_NODE_UNSET; + } else + { + envelope->nReleaseNode = static_cast<decltype(envelope->nReleaseNode)>(nPoint); + } + return true; +} + +// Enable or disable a flag of the current envelope +bool CViewInstrument::EnvSetFlag(EnvelopeFlags flag, bool enable) +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr || envelope->empty()) + return false; + + bool modified = envelope->dwFlags[flag] != enable; + PrepareUndo("Toggle Envelope Flag"); + envelope->dwFlags.set(flag, enable); + return modified; +} + + +bool CViewInstrument::EnvToggleEnv(EnvelopeType envelope, CSoundFile &sndFile, ModInstrument &ins, bool enable, EnvelopeNode::value_t defaultValue, EnvelopeFlags extraFlags) +{ + InstrumentEnvelope &env = ins.GetEnvelope(envelope); + + const FlagSet<EnvelopeFlags> flags = (ENV_ENABLED | extraFlags); + + env.dwFlags.set(flags, enable); + if(enable && env.empty()) + { + env.reserve(2); + env.push_back(EnvelopeNode(0, defaultValue)); + env.push_back(EnvelopeNode(10, defaultValue)); + InvalidateRect(NULL, FALSE); + } + + CriticalSection cs; + + // Update mixing flags... + for(auto &chn : sndFile.m_PlayState.Chn) + { + if(chn.pModInstrument == &ins) + { + chn.GetEnvelope(envelope).flags.set(flags, enable); + } + } + + return true; +} + + +bool CViewInstrument::EnvSetVolEnv(bool enable) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return false; + return EnvToggleEnv(ENV_VOLUME, GetDocument()->GetSoundFile(), *pIns, enable, 64); +} + + +bool CViewInstrument::EnvSetPanEnv(bool enable) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return false; + return EnvToggleEnv(ENV_PANNING, GetDocument()->GetSoundFile(), *pIns, enable, 32); +} + + +bool CViewInstrument::EnvSetPitchEnv(bool enable) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return false; + + pIns->PitchEnv.dwFlags.reset(ENV_FILTER); + return EnvToggleEnv(ENV_PITCH, GetDocument()->GetSoundFile(), *pIns, enable, 32); +} + + +bool CViewInstrument::EnvSetFilterEnv(bool enable) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return false; + + return EnvToggleEnv(ENV_PITCH, GetDocument()->GetSoundFile(), *pIns, enable, 64, ENV_FILTER); +} + + +uint32 CViewInstrument::DragItemToEnvPoint() const +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !m_nDragItem) + return 0; + + switch(m_nDragItem) + { + case ENV_DRAGLOOPSTART: return pEnv->nLoopStart; + case ENV_DRAGLOOPEND: return pEnv->nLoopEnd; + case ENV_DRAGSUSTAINSTART: return pEnv->nSustainStart; + case ENV_DRAGSUSTAINEND: return pEnv->nSustainEnd; + default: return m_nDragItem - 1; + } +} + + +int CViewInstrument::TickToScreen(int tick) const +{ + return static_cast<int>((tick * m_zoom) - m_nScrollPosX + m_envPointSize); +} + +int CViewInstrument::PointToScreen(int nPoint) const +{ + return TickToScreen(EnvGetTick(nPoint)); +} + + +int CViewInstrument::ScreenToTick(int x) const +{ + int offset = m_nScrollPosX + x; + if(offset < m_envPointSize) + return 0; + return mpt::saturate_round<int>((offset - m_envPointSize) / m_zoom); +} + + +int CViewInstrument::ScreenToValue(int y) const +{ + if(m_rcClient.bottom < 2) + return ENVELOPE_MIN; + int n = ENVELOPE_MAX - Util::muldivr(y, ENVELOPE_MAX, m_rcClient.bottom - 1); + if(n < ENVELOPE_MIN) + return ENVELOPE_MIN; + if(n > ENVELOPE_MAX) + return ENVELOPE_MAX; + return n; +} + + +int CViewInstrument::ScreenToPoint(int x0, int y0) const +{ + int nPoint = -1; + int64 ydist = int64_max, xdist = int64_max; + int numPoints = EnvGetNumPoints(); + for(int i = 0; i < numPoints; i++) + { + int dx = x0 - PointToScreen(i); + int64 dx2 = Util::mul32to64(dx, dx); + if(dx2 <= xdist) + { + int dy = y0 - ValueToScreen(EnvGetValue(i)); + int64 dy2 = Util::mul32to64(dy, dy); + if(dx2 < xdist || (dx2 == xdist && dy2 < ydist)) + { + nPoint = i; + xdist = dx2; + ydist = dy2; + } + } + } + return nPoint; +} + + +bool CViewInstrument::GetNcButtonRect(UINT button, CRect &rect) const +{ + rect.left = 4; + rect.top = 3; + rect.bottom = rect.top + ENV_LEFTBAR_CYBTN; + if(button >= ENV_LEFTBAR_BUTTONS) + return false; + for(UINT i = 0; i < button; i++) + { + if(cLeftBarButtons[i] == ID_SEPARATOR) + rect.left += ENV_LEFTBAR_CXSEP; + else + rect.left += ENV_LEFTBAR_CXBTN + ENV_LEFTBAR_CXSPC; + } + if(cLeftBarButtons[button] == ID_SEPARATOR) + { + rect.left += ENV_LEFTBAR_CXSEP / 2 - 2; + rect.right = rect.left + 2; + return false; + } else + { + rect.right = rect.left + ENV_LEFTBAR_CXBTN; + } + return true; +} + + +UINT CViewInstrument::GetNcButtonAtPoint(CPoint point, CRect *outRect) const +{ + CRect rect, rcWnd; + UINT button = uint32_max; + GetWindowRect(&rcWnd); + for(UINT i = 0; i < ENV_LEFTBAR_BUTTONS; i++) + { + if(!(m_NcButtonState[i] & NCBTNS_DISABLED) && GetNcButtonRect(i, rect)) + { + rect.OffsetRect(rcWnd.left, rcWnd.top); + if(rect.PtInRect(point)) + { + button = i; + break; + } + } + } + if(outRect) + *outRect = rect; + return button; +} + + +void CViewInstrument::UpdateNcButtonState() +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + CDC *pDC = NULL; + for (UINT i=0; i<ENV_LEFTBAR_BUTTONS; i++) if (cLeftBarButtons[i] != ID_SEPARATOR) + { + DWORD dwStyle = 0; + + switch(cLeftBarButtons[i]) + { + case ID_ENVSEL_VOLUME: if (m_nEnv == ENV_VOLUME) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVSEL_PANNING: if (m_nEnv == ENV_PANNING) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVSEL_PITCH: if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; + else if (m_nEnv == ENV_PITCH) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_SETLOOP: if (EnvGetLoop()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_SUSTAIN: if (EnvGetSustain()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_CARRY: if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; + else if (EnvGetCarry()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_VOLUME: if (EnvGetVolEnv()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_PANNING: if (EnvGetPanEnv()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_PITCH: if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; else + if (EnvGetPitchEnv()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_FILTER: if (!(sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT))) dwStyle |= NCBTNS_DISABLED; else + if (EnvGetFilterEnv()) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_VIEWGRID: if (m_bGrid) dwStyle |= NCBTNS_CHECKED; break; + case ID_ENVELOPE_ZOOM_IN: if (m_zoom >= ENV_MAX_ZOOM) dwStyle |= NCBTNS_DISABLED; break; + case ID_ENVELOPE_ZOOM_OUT: if (m_zoom <= ENV_MIN_ZOOM) dwStyle |= NCBTNS_DISABLED; break; + case ID_ENVELOPE_LOAD: + case ID_ENVELOPE_SAVE: if (GetInstrumentPtr() == nullptr) dwStyle |= NCBTNS_DISABLED; break; + } + if (m_nBtnMouseOver == i) + { + dwStyle |= NCBTNS_MOUSEOVER; + if (m_dwStatus & INSSTATUS_NCLBTNDOWN) dwStyle |= NCBTNS_PUSHED; + } + if (dwStyle != m_NcButtonState[i]) + { + m_NcButtonState[i] = dwStyle; + if (!pDC) pDC = GetWindowDC(); + DrawNcButton(pDC, i); + } + } + if (pDC) ReleaseDC(pDC); +} + + +//////////////////////////////////////////////////////////////////// +// CViewInstrument drawing + +void CViewInstrument::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj == this) + { + return; + } + const InstrumentHint instrHint = hint.ToType<InstrumentHint>(); + FlagSet<HintType> hintType = instrHint.GetType(); + const INSTRUMENTINDEX updateIns = instrHint.GetInstrument(); + if(hintType[HINT_MPTOPTIONS | HINT_MODTYPE] + || (hintType[HINT_ENVELOPE] && (m_nInstrument == updateIns || updateIns == 0))) + { + UpdateScrollSize(); + UpdateNcButtonState(); + InvalidateRect(NULL, FALSE); + } +} + + +void CViewInstrument::DrawGrid(CDC *pDC, uint32 speed) +{ + bool windowResized = false; + + if(m_dcGrid.m_hDC) + { + m_dcGrid.SelectObject(m_pbmpOldGrid); + m_dcGrid.DeleteDC(); + m_bmpGrid.DeleteObject(); + windowResized = true; + } + + if(windowResized || m_bGridForceRedraw || (m_nScrollPosX != m_GridScrollPos) || (speed != (UINT)m_GridSpeed) && speed > 0) + { + m_GridSpeed = speed; + m_GridScrollPos = m_nScrollPosX; + m_bGridForceRedraw = false; + + // create a memory based dc for drawing the grid + m_dcGrid.CreateCompatibleDC(pDC); + m_bmpGrid.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); + m_pbmpOldGrid = *m_dcGrid.SelectObject(&m_bmpGrid); + + // Do draw + const int width = m_rcClient.Width(); + int rowsPerBeat = 1, rowsPerMeasure = 1; + const CModDoc *modDoc = GetDocument(); + if(modDoc != nullptr) + { + rowsPerBeat = modDoc->GetSoundFile().m_nDefaultRowsPerBeat; + rowsPerMeasure = modDoc->GetSoundFile().m_nDefaultRowsPerMeasure; + } + + // Paint it black! + m_dcGrid.FillSolidRect(&m_rcClient, TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKENV]); + + const uint32 startTick = (ScreenToTick(0) / speed) * speed; + const uint32 endTick = (ScreenToTick(width) / speed) * speed; + + auto oldPen = m_dcGrid.SelectStockObject(DC_PEN); + for(uint32 tick = startTick, row = startTick / speed; tick <= endTick; tick += speed, row++) + { + if(rowsPerMeasure > 0 && row % rowsPerMeasure == 0) + m_dcGrid.SetDCPenColor(RGB(0x80, 0x80, 0x80)); + else if(rowsPerBeat > 0 && row % rowsPerBeat == 0) + m_dcGrid.SetDCPenColor(RGB(0x55, 0x55, 0x55)); + else + m_dcGrid.SetDCPenColor(RGB(0x33, 0x33, 0x33)); + + int x = TickToScreen(tick); + m_dcGrid.MoveTo(x, 0); + m_dcGrid.LineTo(x, m_rcClient.bottom); + } + if(oldPen) + m_dcGrid.SelectObject(oldPen); + } + + pDC->BitBlt(m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), &m_dcGrid, 0, 0, SRCCOPY); +} + + +void CViewInstrument::OnDraw(CDC *pDC) +{ + CModDoc *pModDoc = GetDocument(); + if((!pModDoc) || (!pDC)) + return; + + // to avoid flicker, establish a memory dc, draw to it + // and then BitBlt it to the destination "pDC" + + //check for window resize + if(m_dcMemMain.GetSafeHdc() && m_rcOldClient != m_rcClient) + { + m_dcMemMain.SelectObject(oldBitmap); + m_dcMemMain.DeleteDC(); + m_bmpMemMain.DeleteObject(); + } + + if(!m_dcMemMain.m_hDC) + { + m_dcMemMain.CreateCompatibleDC(pDC); + if(!m_dcMemMain.m_hDC) + return; + } + if(!m_bmpMemMain.m_hObject) + { + m_bmpMemMain.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); + } + m_rcOldClient = m_rcClient; + oldBitmap = *m_dcMemMain.SelectObject(&m_bmpMemMain); + + auto stockBrush = CBrush::FromHandle(GetStockBrush(DC_BRUSH)); + if(m_bGrid) + { + DrawGrid(&m_dcMemMain, pModDoc->GetSoundFile().m_PlayState.m_nMusicSpeed); + } else + { + // Paint it black! + m_dcMemMain.FillSolidRect(&m_rcClient, TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKENV]); + } + + auto oldPen = m_dcMemMain.SelectObject(CMainFrame::penDarkGray); + + // Middle line (half volume or pitch / panning center) + const int ymed = (m_rcClient.bottom - 1) / 2; + m_dcMemMain.MoveTo(0, ymed); + m_dcMemMain.LineTo(m_rcClient.right, ymed); + + // Drawing Loop Start/End + if(EnvGetLoop()) + { + m_dcMemMain.SelectObject(m_nDragItem == ENV_DRAGLOOPSTART ? CMainFrame::penGray99 : CMainFrame::penDarkGray); + int x1 = PointToScreen(EnvGetLoopStart()) - m_envPointSize / 2; + m_dcMemMain.MoveTo(x1, 0); + m_dcMemMain.LineTo(x1, m_rcClient.bottom); + m_dcMemMain.SelectObject(m_nDragItem == ENV_DRAGLOOPEND ? CMainFrame::penGray99 : CMainFrame::penDarkGray); + int x2 = PointToScreen(EnvGetLoopEnd()) + m_envPointSize / 2; + m_dcMemMain.MoveTo(x2, 0); + m_dcMemMain.LineTo(x2, m_rcClient.bottom); + } + // Drawing Sustain Start/End + if(EnvGetSustain()) + { + m_dcMemMain.SelectObject(CMainFrame::penHalfDarkGray); + int nspace = m_rcClient.bottom / 4; + int n1 = EnvGetSustainStart(); + int x1 = PointToScreen(n1) - m_envPointSize / 2; + int y1 = ValueToScreen(EnvGetValue(n1)); + m_dcMemMain.MoveTo(x1, y1 - nspace); + m_dcMemMain.LineTo(x1, y1 + nspace); + int n2 = EnvGetSustainEnd(); + int x2 = PointToScreen(n2) + m_envPointSize / 2; + int y2 = ValueToScreen(EnvGetValue(n2)); + m_dcMemMain.MoveTo(x2, y2 - nspace); + m_dcMemMain.LineTo(x2, y2 + nspace); + } + uint32 maxpoint = EnvGetNumPoints(); + // Drawing Envelope + if(maxpoint) + { + maxpoint--; + m_dcMemMain.SelectObject(GetStockObject(DC_PEN)); + m_dcMemMain.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_ENVELOPES]); + uint32 releaseNode = EnvGetReleaseNode(); + RECT rect; + for(uint32 i = 0; i <= maxpoint; i++) + { + int x = PointToScreen(i); + int y = ValueToScreen(EnvGetValue(i)); + rect.left = x - m_envPointSize + 1; + rect.top = y - m_envPointSize + 1; + rect.right = x + m_envPointSize; + rect.bottom = y + m_envPointSize; + if(i) + m_dcMemMain.LineTo(x, y); + else + m_dcMemMain.MoveTo(x, y); + + if(i == releaseNode) + { + m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0x00, 0x00)); + m_dcMemMain.FrameRect(&rect, stockBrush); + m_dcMemMain.SetDCPenColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_ENVELOPE_RELEASE]); + } else if(i == m_nDragItem - 1) + { + // currently selected env point + m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0xFF, 0x00)); + m_dcMemMain.FrameRect(&rect, stockBrush); + } else + { + m_dcMemMain.SetDCBrushColor(RGB(0xFF, 0xFF, 0xFF)); + m_dcMemMain.FrameRect(&rect, stockBrush); + } + } + } + DrawPositionMarks(); + if(oldPen) + m_dcMemMain.SelectObject(oldPen); + + pDC->BitBlt(m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), &m_dcMemMain, 0, 0, SRCCOPY); +} + + +uint8 CViewInstrument::EnvGetReleaseNode() +{ + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr) + return ENV_RELEASE_NODE_UNSET; + return envelope->nReleaseNode; +} + + +bool CViewInstrument::EnvRemovePoint(uint32 nPoint) +{ + CModDoc *pModDoc = GetDocument(); + if((pModDoc) && (nPoint <= EnvGetLastPoint())) + { + ModInstrument *pIns = pModDoc->GetSoundFile().Instruments[m_nInstrument]; + if(pIns) + { + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope == nullptr || envelope->empty()) + return false; + + PrepareUndo("Remove Envelope Point"); + envelope->erase(envelope->begin() + nPoint); + if (nPoint >= envelope->size()) nPoint = envelope->size() - 1; + if (envelope->nLoopStart > nPoint) envelope->nLoopStart--; + if (envelope->nLoopEnd > nPoint) envelope->nLoopEnd--; + if (envelope->nSustainStart > nPoint) envelope->nSustainStart--; + if (envelope->nSustainEnd > nPoint) envelope->nSustainEnd--; + if (envelope->nReleaseNode>nPoint && envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET) envelope->nReleaseNode--; + envelope->at(0).tick = 0; + + if(envelope->size() <= 1) + { + // if only one node is left, just disable the envelope completely + *envelope = InstrumentEnvelope(); + } + + SetModified(InstrumentHint().Envelope(), true); + return true; + } + } + return false; +} + + +// Insert point. Returns 0 if error occurred, else point ID + 1. +uint32 CViewInstrument::EnvInsertPoint(int nTick, int nValue) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc && nTick >= 0) + { + InstrumentEnvelope *envelope = GetEnvelopePtr(); + if(envelope != nullptr && envelope->size() < pModDoc->GetSoundFile().GetModSpecifications().envelopePointsMax) + { + nValue = Clamp(nValue, ENVELOPE_MIN, ENVELOPE_MAX); + + if(std::binary_search(envelope->cbegin(), envelope->cend(), EnvelopeNode(static_cast<EnvelopeNode::tick_t>(nTick), 0), + [] (const EnvelopeNode &l, const EnvelopeNode &r) { return l.tick < r.tick; })) + { + // Don't want to insert a node at the same position as another node. + return 0; + } + + uint8 defaultValue; + switch(m_nEnv) + { + case ENV_VOLUME: + defaultValue = ENVELOPE_MAX; + break; + case ENV_PANNING: + defaultValue = ENVELOPE_MID; + break; + case ENV_PITCH: + defaultValue = envelope->dwFlags[ENV_FILTER] ? ENVELOPE_MAX : ENVELOPE_MID; + break; + default: + return 0; + } + + PrepareUndo("Insert Envelope Point"); + if(envelope->empty()) + { + envelope->reserve(2); + envelope->push_back(EnvelopeNode(0, defaultValue)); + envelope->dwFlags.set(ENV_ENABLED); + if(nTick == 0) + { + // Can't insert two points on the same tick! + nTick = 16; + } + } + uint32 i = 0; + for(i = 0; i < envelope->size(); i++) if(nTick <= envelope->at(i).tick) break; + envelope->insert(envelope->begin() + i, EnvelopeNode(mpt::saturate_cast<EnvelopeNode::tick_t>(nTick), static_cast<EnvelopeNode::value_t>(nValue))); + if(envelope->nLoopStart >= i) envelope->nLoopStart++; + if(envelope->nLoopEnd >= i) envelope->nLoopEnd++; + if(envelope->nSustainStart >= i) envelope->nSustainStart++; + if(envelope->nSustainEnd >= i) envelope->nSustainEnd++; + if(envelope->nReleaseNode >= i && envelope->nReleaseNode != ENV_RELEASE_NODE_UNSET) envelope->nReleaseNode++; + + SetModified(InstrumentHint().Envelope(), true); + return i + 1; + } + } + return 0; +} + + + +void CViewInstrument::DrawPositionMarks() +{ + CRect rect; + for(auto pos : m_dwNotifyPos) if (pos != Notification::PosInvalid) + { + rect.top = -2; + rect.left = TickToScreen(pos); + rect.right = rect.left + 1; + rect.bottom = m_rcClient.bottom + 1; + InvertRect(m_dcMemMain.m_hDC, &rect); + } +} + + +LRESULT CViewInstrument::OnPlayerNotify(Notification *pnotify) +{ + Notification::Type type; + CModDoc *pModDoc = GetDocument(); + if((!pnotify) || (!pModDoc)) + return 0; + switch(m_nEnv) + { + case ENV_PANNING: type = Notification::PanEnv; break; + case ENV_PITCH: type = Notification::PitchEnv; break; + default: type = Notification::VolEnv; break; + } + if(pnotify->type[Notification::Stop]) + { + bool invalidate = false; + for(auto &pos : m_dwNotifyPos) + { + if(pos != (uint32)Notification::PosInvalid) + { + pos = (uint32)Notification::PosInvalid; + invalidate = true; + } + } + if(invalidate) + { + InvalidateEnvelope(); + } + m_baPlayingNote.reset(); + } else if(pnotify->type[type] && pnotify->item == m_nInstrument) + { + bool update = false; + for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++) + { + uint32 newpos = (uint32)pnotify->pos[i]; + if(m_dwNotifyPos[i] != newpos) + { + update = true; + break; + } + } + if(update) + { + HDC hdc = ::GetDC(m_hWnd); + DrawPositionMarks(); + for(CHANNELINDEX j = 0; j < MAX_CHANNELS; j++) + { + uint32 newpos = (uint32)pnotify->pos[j]; + m_dwNotifyPos[j] = newpos; + } + DrawPositionMarks(); + BitBlt(hdc, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_dcMemMain.GetSafeHdc(), 0, 0, SRCCOPY); + ::ReleaseDC(m_hWnd, hdc); + } + } + return 0; +} + + +void CViewInstrument::DrawNcButton(CDC *pDC, UINT nBtn) +{ + CRect rect; + COLORREF crHi = GetSysColor(COLOR_3DHILIGHT); + COLORREF crDk = GetSysColor(COLOR_3DSHADOW); + COLORREF crFc = GetSysColor(COLOR_3DFACE); + COLORREF c1, c2; + + if(GetNcButtonRect(nBtn, rect)) + { + DWORD dwStyle = m_NcButtonState[nBtn]; + COLORREF c3, c4; + int xofs = 0, yofs = 0, nImage = 0; + + c1 = c2 = c3 = c4 = crFc; + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c1 = c3 = crHi; + c2 = crDk; + c4 = RGB(0, 0, 0); + } + if(dwStyle & (NCBTNS_PUSHED | NCBTNS_CHECKED)) + { + c1 = crDk; + c2 = crHi; + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c4 = crHi; + c3 = (dwStyle & NCBTNS_PUSHED) ? RGB(0, 0, 0) : crDk; + } + xofs = yofs = 1; + } else if((dwStyle & NCBTNS_MOUSEOVER) && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c1 = crHi; + c2 = crDk; + } + switch(cLeftBarButtons[nBtn]) + { + case ID_ENVSEL_VOLUME: nImage = IIMAGE_VOLENV; break; + case ID_ENVSEL_PANNING: nImage = IIMAGE_PANENV; break; + case ID_ENVSEL_PITCH: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOPITCHENV : IIMAGE_PITCHENV; break; + case ID_ENVELOPE_SETLOOP: nImage = IIMAGE_LOOP; break; + case ID_ENVELOPE_SUSTAIN: nImage = IIMAGE_SUSTAIN; break; + case ID_ENVELOPE_CARRY: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOCARRY : IIMAGE_CARRY; break; + case ID_ENVELOPE_VOLUME: nImage = IIMAGE_VOLSWITCH; break; + case ID_ENVELOPE_PANNING: nImage = IIMAGE_PANSWITCH; break; + case ID_ENVELOPE_PITCH: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOPITCHSWITCH : IIMAGE_PITCHSWITCH; break; + case ID_ENVELOPE_FILTER: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOFILTERSWITCH : IIMAGE_FILTERSWITCH; break; + case ID_INSTRUMENT_SAMPLEMAP: nImage = IIMAGE_SAMPLEMAP; break; + case ID_ENVELOPE_VIEWGRID: nImage = IIMAGE_GRID; break; + case ID_ENVELOPE_ZOOM_IN: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOZOOMIN : IIMAGE_ZOOMIN; break; + case ID_ENVELOPE_ZOOM_OUT: nImage = (dwStyle & NCBTNS_DISABLED) ? IIMAGE_NOZOOMOUT : IIMAGE_ZOOMOUT; break; + case ID_ENVELOPE_LOAD: nImage = IIMAGE_LOAD; break; + case ID_ENVELOPE_SAVE: nImage = IIMAGE_SAVE; break; + } + pDC->Draw3dRect(rect.left - 1, rect.top - 1, ENV_LEFTBAR_CXBTN + 2, ENV_LEFTBAR_CYBTN + 2, c3, c4); + pDC->Draw3dRect(rect.left, rect.top, ENV_LEFTBAR_CXBTN, ENV_LEFTBAR_CYBTN, c1, c2); + rect.DeflateRect(1, 1); + pDC->FillSolidRect(&rect, crFc); + rect.left += xofs; + rect.top += yofs; + if(dwStyle & NCBTNS_CHECKED) + m_bmpEnvBar.Draw(pDC, IIMAGE_CHECKED, rect.TopLeft(), ILD_NORMAL); + m_bmpEnvBar.Draw(pDC, nImage, rect.TopLeft(), ILD_NORMAL); + } else + { + c1 = c2 = crFc; + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS) + { + c1 = crDk; + c2 = crHi; + } + pDC->Draw3dRect(rect.left, rect.top, 2, ENV_LEFTBAR_CYBTN, c1, c2); + } +} + + +void CViewInstrument::OnNcPaint() +{ + RECT rect; + + CModScrollView::OnNcPaint(); + GetWindowRect(&rect); + // Assumes there is no other non-client items + rect.bottom = ENV_LEFTBAR_CY; + rect.right -= rect.left; + rect.left = 0; + rect.top = 0; + if((rect.left < rect.right) && (rect.top < rect.bottom)) + { + CDC *pDC = GetWindowDC(); + { + // Shadow + auto shadowRect = rect; + shadowRect.top = shadowRect.bottom - 1; + pDC->FillSolidRect(&shadowRect, GetSysColor(COLOR_BTNSHADOW)); + } + rect.bottom--; + if(rect.top < rect.bottom) + pDC->FillSolidRect(&rect, GetSysColor(COLOR_BTNFACE)); + if(rect.top + 2 < rect.bottom) + { + for(UINT i = 0; i < ENV_LEFTBAR_BUTTONS; i++) + { + DrawNcButton(pDC, i); + } + } + ReleaseDC(pDC); + } +} + + +//////////////////////////////////////////////////////////////////// +// CViewInstrument messages + + +void CViewInstrument::OnSize(UINT nType, int cx, int cy) +{ + CModScrollView::OnSize(nType, cx, cy); + if(((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) + { + UpdateScrollSize(); + } +} + + +void CViewInstrument::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS *lpncsp) +{ + CModScrollView::OnNcCalcSize(bCalcValidRects, lpncsp); + if(lpncsp) + { + lpncsp->rgrc[0].top += ENV_LEFTBAR_CY; + if(lpncsp->rgrc[0].bottom < lpncsp->rgrc[0].top) + lpncsp->rgrc[0].top = lpncsp->rgrc[0].bottom; + } +} + + +void CViewInstrument::OnNcMouseMove(UINT nHitTest, CPoint point) +{ + const auto button = GetNcButtonAtPoint(point); + if(button != m_nBtnMouseOver) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + { + CString strText; + if(button < ENV_LEFTBAR_BUTTONS && cLeftBarButtons[button] != ID_SEPARATOR) + { + strText = LoadResourceString(cLeftBarButtons[button]); + } + pMainFrm->SetHelpText(strText); + } + m_nBtnMouseOver = button; + UpdateNcButtonState(); + } + CModScrollView::OnNcMouseMove(nHitTest, point); +} + + +void CViewInstrument::OnNcLButtonDown(UINT uFlags, CPoint point) +{ + if(m_nBtnMouseOver < ENV_LEFTBAR_BUTTONS) + { + m_dwStatus |= INSSTATUS_NCLBTNDOWN; + if(cLeftBarButtons[m_nBtnMouseOver] != ID_SEPARATOR) + { + PostMessage(WM_COMMAND, cLeftBarButtons[m_nBtnMouseOver]); + UpdateNcButtonState(); + } + } + CModScrollView::OnNcLButtonDown(uFlags, point); +} + + +void CViewInstrument::OnNcLButtonUp(UINT uFlags, CPoint point) +{ + if(m_dwStatus & INSSTATUS_NCLBTNDOWN) + { + m_dwStatus &= ~INSSTATUS_NCLBTNDOWN; + UpdateNcButtonState(); + } + CModScrollView::OnNcLButtonUp(uFlags, point); +} + + +void CViewInstrument::OnNcLButtonDblClk(UINT uFlags, CPoint point) +{ + OnNcLButtonDown(uFlags, point); +} + + +LRESULT CViewInstrument::OnNcHitTest(CPoint point) +{ + CRect rect; + GetWindowRect(&rect); + rect.bottom = rect.top + ENV_LEFTBAR_CY; + if(rect.PtInRect(point)) + { + return HTBORDER; + } + return CModScrollView::OnNcHitTest(point); +} + + +void CViewInstrument::OnMouseMove(UINT, CPoint pt) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return; + + bool splitCursor = false; + + if((m_nBtnMouseOver < ENV_LEFTBAR_BUTTONS) || (m_dwStatus & INSSTATUS_NCLBTNDOWN)) + { + m_dwStatus &= ~INSSTATUS_NCLBTNDOWN; + m_nBtnMouseOver = 0xFFFF; + UpdateNcButtonState(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->SetHelpText(_T("")); + } + int nTick = ScreenToTick(pt.x); + int nVal = Clamp(ScreenToValue(pt.y), ENVELOPE_MIN, ENVELOPE_MAX); + if(nTick < 0) + nTick = 0; + UpdateIndicator(nTick, nVal); + + if((m_dwStatus & INSSTATUS_DRAGGING) && (m_nDragItem)) + { + if(!m_mouseMoveModified) + { + PrepareUndo("Move Envelope Point"); + m_mouseMoveModified = true; + } + bool changed = false; + if(pt.x >= m_rcClient.right - 2) + nTick++; + if(IsDragItemEnvPoint()) + { + // Ctrl pressed -> move tail of envelope + changed = EnvSetValue(m_nDragItem - 1, nTick, nVal, CMainFrame::GetInputHandler()->CtrlPressed()); + } else + { + int nPoint = ScreenToPoint(pt.x, pt.y); + if (nPoint >= 0) switch(m_nDragItem) + { + case ENV_DRAGLOOPSTART: + changed = EnvSetLoopStart(nPoint); + splitCursor = true; + break; + case ENV_DRAGLOOPEND: + changed = EnvSetLoopEnd(nPoint); + splitCursor = true; + break; + case ENV_DRAGSUSTAINSTART: + changed = EnvSetSustainStart(nPoint); + splitCursor = true; + break; + case ENV_DRAGSUSTAINEND: + changed = EnvSetSustainEnd(nPoint); + splitCursor = true; + break; + } + } + if(changed) + { + if(pt.x <= 0) + { + UpdateScrollSize(); + OnScrollBy(CSize(pt.x - (int)m_zoom, 0), TRUE); + } + if(pt.x >= m_rcClient.right - 1) + { + UpdateScrollSize(); + OnScrollBy(CSize((int)m_zoom + pt.x - m_rcClient.right, 0), TRUE); + } + SetModified(InstrumentHint().Envelope(), true); + UpdateWindow(); //rewbs: TODO - optimisation here so we don't redraw whole view. + } + } else + { + CRect rect; + if(EnvGetSustain()) + { + int nspace = m_rcClient.bottom / 4; + rect.top = ValueToScreen(EnvGetValue(EnvGetSustainStart())) - nspace; + rect.bottom = rect.top + nspace * 2; + rect.right = PointToScreen(EnvGetSustainStart()) + 1; + rect.left = rect.right - m_envPointSize * 2; + if(rect.PtInRect(pt)) + { + splitCursor = true; // ENV_DRAGSUSTAINSTART; + } else + { + rect.top = ValueToScreen(EnvGetValue(EnvGetSustainEnd())) - nspace; + rect.bottom = rect.top + nspace * 2; + rect.left = PointToScreen(EnvGetSustainEnd()) - 1; + rect.right = rect.left + m_envPointSize * 2; + if(rect.PtInRect(pt)) + splitCursor = true; // ENV_DRAGSUSTAINEND; + } + } + if(EnvGetLoop()) + { + rect.top = m_rcClient.top; + rect.bottom = m_rcClient.bottom; + rect.right = PointToScreen(EnvGetLoopStart()) + 1; + rect.left = rect.right - m_envPointSize * 2; + if(rect.PtInRect(pt)) + { + splitCursor = true; // ENV_DRAGLOOPSTART; + } else + { + rect.left = PointToScreen(EnvGetLoopEnd()) - 1; + rect.right = rect.left + m_envPointSize * 2; + if(rect.PtInRect(pt)) + splitCursor = true; // ENV_DRAGLOOPEND; + } + } + } + // Update the mouse cursor + if(splitCursor) + { + if(!(m_dwStatus & INSSTATUS_SPLITCURSOR)) + { + m_dwStatus |= INSSTATUS_SPLITCURSOR; + if(!(m_dwStatus & INSSTATUS_DRAGGING)) + SetCapture(); + SetCursor(CMainFrame::curVSplit); + } + } else + { + if(m_dwStatus & INSSTATUS_SPLITCURSOR) + { + m_dwStatus &= ~INSSTATUS_SPLITCURSOR; + SetCursor(CMainFrame::curArrow); + if(!(m_dwStatus & INSSTATUS_DRAGGING)) + ReleaseCapture(); + } + } +} + + + +void CViewInstrument::UpdateIndicator() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !m_nDragItem) + return; + + uint32 point = DragItemToEnvPoint(); + if(point < pEnv->size()) + { + UpdateIndicator(pEnv->at(point).tick, pEnv->at(point).value); + } +} + + +void CViewInstrument::UpdateIndicator(int tick, int val) +{ + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return; + + CString s; + s.Format(TrackerSettings::Instance().cursorPositionInHex ? _T("Tick %X, [%s]") : _T("Tick %d, [%s]"), tick, EnvValueToString(tick, val).GetString()); + CModScrollView::UpdateIndicator(s); + CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this); +} + + +CString CViewInstrument::EnvValueToString(int tick, int val) const +{ + const InstrumentEnvelope *env = GetEnvelopePtr(); + const bool hasReleaseNode = env->nReleaseNode != ENV_RELEASE_NODE_UNSET; + EnvelopeNode releaseNode; + if(hasReleaseNode) + { + releaseNode = env->at(env->nReleaseNode); + } + + CString s; + if(!hasReleaseNode || tick <= releaseNode.tick + 1) + { + // ticks before release node (or no release node) + const int displayVal = (m_nEnv != ENV_VOLUME && !(m_nEnv == ENV_PITCH && env->dwFlags[ENV_FILTER])) ? val - 32 : val; + if(m_nEnv != ENV_PANNING) + s.Format(_T("%d"), displayVal); + else // panning envelope: display right/center/left chars + s.Format(_T("%d %c"), std::abs(displayVal), displayVal > 0 ? _T('R') : (displayVal < 0 ? _T('L') : _T('C'))); + } else + { + // ticks after release node + int displayVal = (val - releaseNode.value) * 2; + displayVal = (m_nEnv != ENV_VOLUME) ? displayVal - 32 : displayVal; + s.Format(_T("Rel%c%d"), displayVal > 0 ? _T('+') : _T('-'), std::abs(displayVal)); + } + return s; +} + + +void CViewInstrument::OnLButtonDown(UINT, CPoint pt) +{ + m_mouseMoveModified = false; + if(!(m_dwStatus & INSSTATUS_DRAGGING)) + { + CRect rect; + // Look if dragging a point + uint32 maxpoint = EnvGetLastPoint(); + uint32 oldDragItem = m_nDragItem; + m_nDragItem = 0; + const int hitboxSize = static_cast<int>((6 * m_nDPIx) / 96.0f); + for(uint32 i = 0; i <= maxpoint; i++) + { + int x = PointToScreen(i); + int y = ValueToScreen(EnvGetValue(i)); + rect.SetRect(x - hitboxSize, y - hitboxSize, x + hitboxSize + 1, y + hitboxSize + 1); + if(rect.PtInRect(pt)) + { + m_nDragItem = i + 1; + break; + } + } + if((!m_nDragItem) && (EnvGetSustain())) + { + int nspace = m_rcClient.bottom / 4; + rect.top = ValueToScreen(EnvGetValue(EnvGetSustainStart())) - nspace; + rect.bottom = rect.top + nspace * 2; + rect.right = PointToScreen(EnvGetSustainStart()) + 1; + rect.left = rect.right - m_envPointSize * 2; + if(rect.PtInRect(pt)) + { + m_nDragItem = ENV_DRAGSUSTAINSTART; + } else + { + rect.top = ValueToScreen(EnvGetValue(EnvGetSustainEnd())) - nspace; + rect.bottom = rect.top + nspace * 2; + rect.left = PointToScreen(EnvGetSustainEnd()) - 1; + rect.right = rect.left + m_envPointSize * 2; + if(rect.PtInRect(pt)) + m_nDragItem = ENV_DRAGSUSTAINEND; + } + } + if((!m_nDragItem) && (EnvGetLoop())) + { + rect.top = m_rcClient.top; + rect.bottom = m_rcClient.bottom; + rect.right = PointToScreen(EnvGetLoopStart()) + 1; + rect.left = rect.right - m_envPointSize * 2; + if(rect.PtInRect(pt)) + { + m_nDragItem = ENV_DRAGLOOPSTART; + } else + { + rect.left = PointToScreen(EnvGetLoopEnd()) - 1; + rect.right = rect.left + m_envPointSize * 2; + if(rect.PtInRect(pt)) + m_nDragItem = ENV_DRAGLOOPEND; + } + } + + if(m_nDragItem) + { + SetCapture(); + m_dwStatus |= INSSTATUS_DRAGGING; + // refresh active node colour + InvalidateRect(NULL, FALSE); + } else + { + // Shift-Click: Insert envelope point here + if(CMainFrame::GetInputHandler()->ShiftPressed()) + { + if(InsertAtPoint(pt) == 0 && oldDragItem != 0) + { + InvalidateRect(NULL, FALSE); + } + } else if(oldDragItem) + { + InvalidateRect(NULL, FALSE); + } + } + } +} + + +void CViewInstrument::OnLButtonUp(UINT, CPoint) +{ + m_mouseMoveModified = false; + if(m_dwStatus & INSSTATUS_SPLITCURSOR) + { + m_dwStatus &= ~INSSTATUS_SPLITCURSOR; + SetCursor(CMainFrame::curArrow); + } + if(m_dwStatus & INSSTATUS_DRAGGING) + { + m_dwStatus &= ~INSSTATUS_DRAGGING; + ReleaseCapture(); + } +} + + +void CViewInstrument::OnRButtonDown(UINT flags, CPoint pt) +{ + const CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + + if(m_dwStatus & INSSTATUS_DRAGGING) + return; + + // Ctrl + Right-Click = Delete point + if(flags & MK_CONTROL) + { + OnMButtonDown(flags, pt); + return; + } + + CMenu Menu; + if((pModDoc) && (Menu.LoadMenu(IDR_ENVELOPES))) + { + CMenu *pSubMenu = Menu.GetSubMenu(0); + if(pSubMenu != nullptr) + { + m_nDragItem = ScreenToPoint(pt.x, pt.y) + 1; + const uint32 maxPoint = (sndFile.GetType() == MOD_TYPE_XM) ? 11 : 24; + const uint32 lastpoint = EnvGetLastPoint(); + const bool forceRelease = !sndFile.GetModSpecifications().hasReleaseNode && (EnvGetReleaseNode() != ENV_RELEASE_NODE_UNSET); + pSubMenu->EnableMenuItem(ID_ENVELOPE_INSERTPOINT, (lastpoint < maxPoint) ? MF_ENABLED : MF_GRAYED); + pSubMenu->EnableMenuItem(ID_ENVELOPE_REMOVEPOINT, ((m_nDragItem) && (lastpoint > 0)) ? MF_ENABLED : MF_GRAYED); + pSubMenu->EnableMenuItem(ID_ENVELOPE_CARRY, (sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT)) ? MF_ENABLED : MF_GRAYED); + pSubMenu->EnableMenuItem(ID_ENVELOPE_TOGGLERELEASENODE, ((sndFile.GetModSpecifications().hasReleaseNode && m_nEnv == ENV_VOLUME) || forceRelease) ? MF_ENABLED : MF_GRAYED); + pSubMenu->CheckMenuItem(ID_ENVELOPE_SETLOOP, (EnvGetLoop()) ? MF_CHECKED : MF_UNCHECKED); + pSubMenu->CheckMenuItem(ID_ENVELOPE_SUSTAIN, (EnvGetSustain()) ? MF_CHECKED : MF_UNCHECKED); + pSubMenu->CheckMenuItem(ID_ENVELOPE_CARRY, (EnvGetCarry()) ? MF_CHECKED : MF_UNCHECKED); + pSubMenu->CheckMenuItem(ID_ENVELOPE_TOGGLERELEASENODE, (EnvGetReleaseNode() == m_nDragItem - 1) ? MF_CHECKED : MF_UNCHECKED); + m_ptMenu = pt; + ClientToScreen(&pt); + pSubMenu->TrackPopupMenu(TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, this); + } + } +} + +void CViewInstrument::OnMButtonDown(UINT, CPoint pt) +{ + // Middle mouse button: Remove envelope point + int point = ScreenToPoint(pt.x, pt.y); + if(point >= 0) + { + EnvRemovePoint(point); + m_nDragItem = point + 1; + } +} + + +void CViewInstrument::OnPrevInstrument() +{ + SendCtrlMessage(CTRLMSG_INS_PREVINSTRUMENT); +} + + +void CViewInstrument::OnNextInstrument() +{ + SendCtrlMessage(CTRLMSG_INS_NEXTINSTRUMENT); +} + + +void CViewInstrument::OnEditSampleMap() +{ + SendCtrlMessage(CTRLMSG_INS_SAMPLEMAP); +} + + +void CViewInstrument::OnSelectVolumeEnv() +{ + if(m_nEnv != ENV_VOLUME) + SetCurrentInstrument(m_nInstrument, ENV_VOLUME); +} + + +void CViewInstrument::OnSelectPanningEnv() +{ + if(m_nEnv != ENV_PANNING) + SetCurrentInstrument(m_nInstrument, ENV_PANNING); +} + + +void CViewInstrument::OnSelectPitchEnv() +{ + if(m_nEnv != ENV_PITCH) + SetCurrentInstrument(m_nInstrument, ENV_PITCH); +} + + +void CViewInstrument::OnEnvLoopChanged() +{ + CModDoc *pModDoc = GetDocument(); + PrepareUndo("Toggle Envelope Loop"); + if((pModDoc) && (EnvSetLoop(!EnvGetLoop()))) + { + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(EnvGetLoop() && pEnv != nullptr && pEnv->nLoopEnd == 0) + { + // Enabled loop => set loop points if no loop has been specified yet. + pEnv->nLoopStart = 0; + pEnv->nLoopEnd = mpt::saturate_cast<decltype(pEnv->nLoopEnd)>(pEnv->size() - 1); + } + SetModified(InstrumentHint().Envelope(), true); + } +} + + +void CViewInstrument::OnEnvSustainChanged() +{ + CModDoc *pModDoc = GetDocument(); + PrepareUndo("Toggle Envelope Sustain"); + if((pModDoc) && (EnvSetSustain(!EnvGetSustain()))) + { + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(EnvGetSustain() && pEnv != nullptr && pEnv->nSustainStart == pEnv->nSustainEnd && IsDragItemEnvPoint()) + { + // Enabled sustain loop => set sustain loop points if no sustain loop has been specified yet. + pEnv->nSustainStart = pEnv->nSustainEnd = mpt::saturate_cast<decltype(pEnv->nSustainEnd)>(m_nDragItem - 1); + } + SetModified(InstrumentHint().Envelope(), true); + } +} + + +void CViewInstrument::OnEnvCarryChanged() +{ + CModDoc *pModDoc = GetDocument(); + PrepareUndo("Toggle Envelope Carry"); + if((pModDoc) && (EnvSetCarry(!EnvGetCarry()))) + { + SetModified(InstrumentHint().Envelope(), false); + UpdateNcButtonState(); + } +} + +void CViewInstrument::OnEnvToggleReleasNode() +{ + if(IsDragItemEnvPoint()) + { + PrepareUndo("Toggle Envelope Release Node"); + if(EnvToggleReleaseNode(m_nDragItem - 1)) + { + SetModified(InstrumentHint().Envelope(), true); + } + } +} + + +void CViewInstrument::OnEnvVolChanged() +{ + GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Volume Envelope", ENV_VOLUME); + if(EnvSetVolEnv(!EnvGetVolEnv())) + { + SetModified(InstrumentHint().Envelope(), false); + UpdateNcButtonState(); + } +} + + +void CViewInstrument::OnEnvPanChanged() +{ + GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Panning Envelope", ENV_PANNING); + if(EnvSetPanEnv(!EnvGetPanEnv())) + { + SetModified(InstrumentHint().Envelope(), false); + UpdateNcButtonState(); + } +} + + +void CViewInstrument::OnEnvPitchChanged() +{ + GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Pitch Envelope", ENV_PITCH); + if(EnvSetPitchEnv(!EnvGetPitchEnv())) + { + SetModified(InstrumentHint().Envelope(), false); + UpdateNcButtonState(); + } +} + + +void CViewInstrument::OnEnvFilterChanged() +{ + GetDocument()->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Toggle Filter Envelope", ENV_PITCH); + if(EnvSetFilterEnv(!EnvGetFilterEnv())) + { + SetModified(InstrumentHint().Envelope(), false); + UpdateNcButtonState(); + } +} + + +void CViewInstrument::OnEnvToggleGrid() +{ + m_bGrid = !m_bGrid; + if(m_bGrid) + m_bGridForceRedraw = true; + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + pModDoc->UpdateAllViews(nullptr, InstrumentHint(m_nInstrument).Envelope()); +} + + +void CViewInstrument::OnEnvRemovePoint() +{ + if(m_nDragItem > 0) + { + EnvRemovePoint(m_nDragItem - 1); + } +} + + +void CViewInstrument::OnEnvInsertPoint() +{ + const int tick = ScreenToTick(m_ptMenu.x), value = ScreenToValue(m_ptMenu.y); + if(!EnvInsertPoint(tick, value)) + { + // Couldn't insert point, maybe because there's already a point at this tick + // => Try next tick + EnvInsertPoint(tick + 1, value); + } +} + + +bool CViewInstrument::InsertAtPoint(CPoint pt) +{ + auto item = EnvInsertPoint(ScreenToTick(pt.x), ScreenToValue(pt.y)); // returns point ID + 1 if successful, else 0. + if(item > 0) + { + // Drag point if successful + SetCapture(); + m_dwStatus |= INSSTATUS_DRAGGING; + m_nDragItem = item; + } + return item > 0; +} + + +void CViewInstrument::OnEditCopy() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + pModDoc->CopyEnvelope(m_nInstrument, m_nEnv); +} + + +void CViewInstrument::OnEditPaste() +{ + CModDoc *pModDoc = GetDocument(); + PrepareUndo("Paste Envelope"); + if(pModDoc->PasteEnvelope(m_nInstrument, m_nEnv)) + { + SetModified(InstrumentHint().Envelope(), true); + } else + { + pModDoc->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } +} + + +void CViewInstrument::PlayNote(ModCommand::NOTE note) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr || pMainFrm == nullptr) + { + return; + } + if(note > 0 && note < 128) + { + if(m_nInstrument && !m_baPlayingNote[note]) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if((!pIns) || (!pIns->Keyboard[note - NOTE_MIN] && !pIns->nMixPlug)) + return; + { + if(pMainFrm->GetModPlaying() != pModDoc) + { + sndFile.m_SongFlags.set(SONG_PAUSED); + sndFile.ResetChannels(); + if(!pMainFrm->PlayMod(pModDoc)) + return; + } + pModDoc->PlayNote(PlayNoteParam(note).Instrument(m_nInstrument).CheckNNA(m_baPlayingNote), &m_noteChannel); + } + CString noteName; + if(ModCommand::IsNote(note)) + { + noteName = mpt::ToCString(sndFile.GetNoteName(note, m_nInstrument)); + } + pMainFrm->SetInfoText(noteName); + } + } else + { + pModDoc->PlayNote(PlayNoteParam(note).Instrument(m_nInstrument)); + } +} + + +// Drop files from Windows +void CViewInstrument::OnDropFiles(HDROP hDropInfo) +{ + const UINT nFiles = ::DragQueryFile(hDropInfo, (UINT)-1, NULL, 0); + CMainFrame::GetMainFrame()->SetForegroundWindow(); + for(UINT f = 0; f < nFiles; f++) + { + UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, _T('\0')); + if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) + { + const mpt::PathString file = mpt::PathString::FromNative(fileName.data()); + PrepareUndo("Replace Envelope"); + if(GetDocument()->LoadEnvelope(m_nInstrument, m_nEnv, file)) + { + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); + } else + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + if(SendCtrlMessage(CTRLMSG_INS_OPENFILE, (LPARAM)&file) && f < nFiles - 1) + { + // Insert more instrument slots + if(!SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT)) + break; + } + } + } + } + ::DragFinish(hDropInfo); +} + + +BOOL CViewInstrument::OnDragonDrop(BOOL doDrop, const DRAGONDROP *dropInfo) +{ + CModDoc *modDoc = GetDocument(); + bool canDrop = false; + + if((!dropInfo) || (!modDoc)) + return FALSE; + CSoundFile &sndFile = modDoc->GetSoundFile(); + switch(dropInfo->dropType) + { + case DRAGONDROP_INSTRUMENT: + if(dropInfo->sndFile == &sndFile) + { + canDrop = ((dropInfo->dropItem) + && (dropInfo->dropItem <= sndFile.m_nInstruments) + && (dropInfo->sndFile == &sndFile)); + } else + { + canDrop = ((dropInfo->dropItem) + && ((dropInfo->dropParam) || (dropInfo->sndFile))); + } + break; + + case DRAGONDROP_DLS: + canDrop = ((dropInfo->dropItem < CTrackApp::gpDLSBanks.size()) + && (CTrackApp::gpDLSBanks[dropInfo->dropItem])); + break; + + case DRAGONDROP_SOUNDFILE: + case DRAGONDROP_MIDIINSTR: + canDrop = !dropInfo->GetPath().empty(); + break; + } + + const bool insertNew = CMainFrame::GetInputHandler()->ShiftPressed() && sndFile.GetNumInstruments() > 0; + if(insertNew && !sndFile.CanAddMoreInstruments()) + canDrop = false; + + if(!canDrop || !doDrop) + return canDrop; + + if(!sndFile.GetNumInstruments() && sndFile.GetModSpecifications().instrumentsMax > 0) + SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT); + if(!m_nInstrument || m_nInstrument > sndFile.GetNumInstruments()) + return FALSE; + + // Do the drop + bool modified = false; + BeginWaitCursor(); + switch(dropInfo->dropType) + { + case DRAGONDROP_INSTRUMENT: + if(dropInfo->sndFile == &sndFile) + { + SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, dropInfo->dropItem); + } else + { + if(insertNew && !SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT)) + canDrop = false; + else + SendCtrlMessage(CTRLMSG_INS_SONGDROP, reinterpret_cast<LPARAM>(dropInfo)); + } + break; + + case DRAGONDROP_MIDIINSTR: + if(CDLSBank::IsDLSBank(dropInfo->GetPath())) + { + CDLSBank dlsbank; + if(dlsbank.Open(dropInfo->GetPath())) + { + const DLSINSTRUMENT *pDlsIns; + UINT nIns = 0, nRgn = 0xFF; + // Drums + if(dropInfo->dropItem & 0x80) + { + UINT key = dropInfo->dropItem & 0x7F; + pDlsIns = dlsbank.FindInstrument(true, 0xFFFF, 0xFF, key, &nIns); + if(pDlsIns) + nRgn = dlsbank.GetRegionFromKey(nIns, key); + } else + // Melodic + { + pDlsIns = dlsbank.FindInstrument(false, 0xFFFF, dropInfo->dropItem, 60, &nIns); + if(pDlsIns) + nRgn = dlsbank.GetRegionFromKey(nIns, 60); + } + canDrop = false; + if(pDlsIns) + { + if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT)) + { + CriticalSection cs; + modDoc->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Replace Instrument"); + canDrop = modified = dlsbank.ExtractInstrument(sndFile, m_nInstrument, nIns, nRgn); + } + } + break; + } + } + // Instrument file -> fall through + [[fallthrough]]; + case DRAGONDROP_SOUNDFILE: + if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT)) + SendCtrlMessage(CTRLMSG_INS_OPENFILE, dropInfo->dropParam); + break; + + case DRAGONDROP_DLS: + { + UINT nIns = dropInfo->dropParam & 0xFFFF; + uint32 drumRgn = uint32_max; + // Drums: (0x80000000) | (Region << 16) | (Instrument) + if(dropInfo->dropParam & 0x80000000) + drumRgn = (dropInfo->dropParam & 0x7FFF0000) >> 16; + + if(!insertNew || SendCtrlMessage(CTRLMSG_INS_NEWINSTRUMENT)) + { + CriticalSection cs; + modDoc->GetInstrumentUndo().PrepareUndo(m_nInstrument, "Replace Instrument"); + canDrop = modified = CTrackApp::gpDLSBanks[dropInfo->dropItem]->ExtractInstrument(sndFile, m_nInstrument, nIns, drumRgn); + } + } + break; + } + if(modified) + { + SetModified(InstrumentHint().Info().Envelope().Names(), true); + GetDocument()->UpdateAllViews(nullptr, SampleHint().Info().Names().Data(), this); + } + CMDIChildWnd *pMDIFrame = (CMDIChildWnd *)GetParentFrame(); + if(pMDIFrame) + { + pMDIFrame->MDIActivate(); + pMDIFrame->SetActiveView(this); + SetFocus(); + } + EndWaitCursor(); + return canDrop; +} + + +LRESULT CViewInstrument::OnMidiMsg(WPARAM midiDataParam, LPARAM) +{ + const uint32 midiData = static_cast<uint32>(midiDataParam); + CModDoc *modDoc = GetDocument(); + if(modDoc != nullptr) + { + modDoc->ProcessMIDI(midiData, m_nInstrument, modDoc->GetSoundFile().GetInstrumentPlugin(m_nInstrument), kCtxViewInstruments); + + MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData); + uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); + if(event == MIDIEvents::evNoteOn) + { + CMainFrame::GetMainFrame()->SetInfoText(mpt::ToCString(modDoc->GetSoundFile().GetNoteName(midiByte1 + NOTE_MIN, m_nInstrument))); + } + + return 1; + } + return 0; +} + + +BOOL CViewInstrument::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxViewInstruments); + + if(ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + + // Handle Application (menu) key + if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS) + { + CPoint pt(0, 0); + if(m_nDragItem > 0) + { + uint32 point = DragItemToEnvPoint(); + pt.SetPoint(PointToScreen(point), ValueToScreen(EnvGetValue(point))); + } + OnRButtonDown(0, pt); + } + } + } + + return CModScrollView::PreTranslateMessage(pMsg); +} + + +LRESULT CViewInstrument::OnCustomKeyMsg(WPARAM wParam, LPARAM) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return kcNull; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + switch(wParam) + { + case kcPrevInstrument: OnPrevInstrument(); return wParam; + case kcNextInstrument: OnNextInstrument(); return wParam; + case kcEditCopy: OnEditCopy(); return wParam; + case kcEditPaste: OnEditPaste(); return wParam; + case kcEditUndo: OnEditUndo(); return wParam; + case kcEditRedo: OnEditRedo(); return wParam; + case kcNoteOff: PlayNote(NOTE_KEYOFF); return wParam; + case kcNoteCut: PlayNote(NOTE_NOTECUT); return wParam; + case kcInstrumentLoad: SendCtrlMessage(IDC_INSTRUMENT_OPEN); return wParam; + case kcInstrumentSave: SendCtrlMessage(IDC_INSTRUMENT_SAVEAS); return wParam; + case kcInstrumentNew: SendCtrlMessage(IDC_INSTRUMENT_NEW); return wParam; + + // envelope editor + case kcInstrumentEnvelopeLoad: OnEnvLoad(); return wParam; + case kcInstrumentEnvelopeSave: OnEnvSave(); return wParam; + case kcInstrumentEnvelopeZoomIn: OnEnvZoomIn(); return wParam; + case kcInstrumentEnvelopeZoomOut: OnEnvZoomOut(); return wParam; + case kcInstrumentEnvelopeScale: OnEnvelopeScalePoints(); return wParam; + case kcInstrumentEnvelopeSwitchToVolume: OnSelectVolumeEnv(); return wParam; + case kcInstrumentEnvelopeSwitchToPanning: OnSelectPanningEnv(); return wParam; + case kcInstrumentEnvelopeSwitchToPitch: OnSelectPitchEnv(); return wParam; + case kcInstrumentEnvelopeToggleVolume: OnEnvVolChanged(); return wParam; + case kcInstrumentEnvelopeTogglePanning: OnEnvPanChanged(); return wParam; + case kcInstrumentEnvelopeTogglePitch: OnEnvPitchChanged(); return wParam; + case kcInstrumentEnvelopeToggleFilter: OnEnvFilterChanged(); return wParam; + case kcInstrumentEnvelopeToggleLoop: OnEnvLoopChanged(); return wParam; + case kcInstrumentEnvelopeSelectLoopStart: EnvKbdSelectPoint(ENV_DRAGLOOPSTART); return wParam; + case kcInstrumentEnvelopeSelectLoopEnd: EnvKbdSelectPoint(ENV_DRAGLOOPEND); return wParam; + case kcInstrumentEnvelopeToggleSustain: OnEnvSustainChanged(); return wParam; + case kcInstrumentEnvelopeSelectSustainStart: EnvKbdSelectPoint(ENV_DRAGSUSTAINSTART); return wParam; + case kcInstrumentEnvelopeSelectSustainEnd: EnvKbdSelectPoint(ENV_DRAGSUSTAINEND); return wParam; + case kcInstrumentEnvelopeToggleCarry: OnEnvCarryChanged(); return wParam; + case kcInstrumentEnvelopePointPrev: EnvKbdSelectPoint(ENV_DRAGPREVIOUS); return wParam; + case kcInstrumentEnvelopePointNext: EnvKbdSelectPoint(ENV_DRAGNEXT); return wParam; + case kcInstrumentEnvelopePointMoveLeft: EnvKbdMovePointLeft(1); return wParam; + case kcInstrumentEnvelopePointMoveRight: EnvKbdMovePointRight(1); return wParam; + case kcInstrumentEnvelopePointMoveLeftCoarse: EnvKbdMovePointLeft(sndFile.m_PlayState.m_nCurrentRowsPerBeat * sndFile.m_PlayState.m_nMusicSpeed); return wParam; + case kcInstrumentEnvelopePointMoveRightCoarse: EnvKbdMovePointRight(sndFile.m_PlayState.m_nCurrentRowsPerBeat * sndFile.m_PlayState.m_nMusicSpeed); return wParam; + case kcInstrumentEnvelopePointMoveUp: EnvKbdMovePointVertical(1); return wParam; + case kcInstrumentEnvelopePointMoveDown: EnvKbdMovePointVertical(-1); return wParam; + case kcInstrumentEnvelopePointMoveUp8: EnvKbdMovePointVertical(8); return wParam; + case kcInstrumentEnvelopePointMoveDown8: EnvKbdMovePointVertical(-8); return wParam; + case kcInstrumentEnvelopePointInsert: EnvKbdInsertPoint(); return wParam; + case kcInstrumentEnvelopePointRemove: EnvKbdRemovePoint(); return wParam; + case kcInstrumentEnvelopeSetLoopStart: EnvKbdSetLoopStart(); return wParam; + case kcInstrumentEnvelopeSetLoopEnd: EnvKbdSetLoopEnd(); return wParam; + case kcInstrumentEnvelopeSetSustainLoopStart: EnvKbdSetSustainStart(); return wParam; + case kcInstrumentEnvelopeSetSustainLoopEnd: EnvKbdSetSustainEnd(); return wParam; + case kcInstrumentEnvelopeToggleReleaseNode: EnvKbdToggleReleaseNode(); return wParam; + } + if(wParam >= kcInstrumentStartNotes && wParam <= kcInstrumentEndNotes) + { + PlayNote(pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcInstrumentStartNotes), m_nInstrument)); + return wParam; + } + if(wParam >= kcInstrumentStartNoteStops && wParam <= kcInstrumentEndNoteStops) + { + ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcInstrumentStartNoteStops), m_nInstrument); + if(ModCommand::IsNote(note)) + { + m_baPlayingNote[note] = false; + pModDoc->NoteOff(note, false, m_nInstrument, m_noteChannel[note - NOTE_MIN]); + } + return wParam; + } + + return kcNull; +} + + +void CViewInstrument::OnEnvelopeScalePoints() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + + if(m_nInstrument >= 1 + && m_nInstrument <= sndFile.GetNumInstruments() + && sndFile.Instruments[m_nInstrument]) + { + // "Center" y value of the envelope. For panning and pitch, this is 32, for volume and filter it is 0 (minimum). + int nOffset = ((m_nEnv != ENV_VOLUME) && !GetEnvelopePtr()->dwFlags[ENV_FILTER]) ? 32 : 0; + + CScaleEnvPointsDlg dlg(this, *GetEnvelopePtr(), nOffset); + if(dlg.DoModal() == IDOK) + { + PrepareUndo("Scale Envelope"); + dlg.Apply(); + SetModified(InstrumentHint().Envelope(), true); + } + } +} + + +void CViewInstrument::EnvSetZoom(float newZoom) +{ + m_zoom = Clamp(newZoom, ENV_MIN_ZOOM, ENV_MAX_ZOOM); + InvalidateRect(NULL, FALSE); + UpdateScrollSize(); + UpdateNcButtonState(); +} + + +//////////////////////////////////////// +// Envelope Editor - Keyboard actions + +void CViewInstrument::EnvKbdSelectPoint(DragPoints point) +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr) + return; + + switch(point) + { + case ENV_DRAGLOOPSTART: + case ENV_DRAGLOOPEND: + if(!pEnv->dwFlags[ENV_LOOP]) + return; + m_nDragItem = point; + break; + case ENV_DRAGSUSTAINSTART: + case ENV_DRAGSUSTAINEND: + if(!pEnv->dwFlags[ENV_SUSTAIN]) + return; + m_nDragItem = point; + break; + case ENV_DRAGPREVIOUS: + if(m_nDragItem <= 1 || m_nDragItem > pEnv->size()) + m_nDragItem = pEnv->size(); + else + m_nDragItem--; + break; + case ENV_DRAGNEXT: + if(m_nDragItem >= pEnv->size()) + m_nDragItem = 1; + else + m_nDragItem++; + break; + } + UpdateIndicator(); + InvalidateRect(NULL, FALSE); +} + + +void CViewInstrument::EnvKbdMovePointLeft(int stepsize) +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr) + return; + const MODTYPE modType = GetDocument()->GetModType(); + + // Move loop points? + PrepareUndo("Move Envelope Point"); + if(m_nDragItem == ENV_DRAGSUSTAINSTART) + { + if(pEnv->nSustainStart <= 0) + return; + pEnv->nSustainStart--; + if(modType == MOD_TYPE_XM) + pEnv->nSustainEnd = pEnv->nSustainStart; + } else if(m_nDragItem == ENV_DRAGSUSTAINEND) + { + if(pEnv->nSustainEnd <= 0) + return; + if(pEnv->nSustainEnd <= pEnv->nSustainStart) + pEnv->nSustainStart--; + pEnv->nSustainEnd--; + } else if(m_nDragItem == ENV_DRAGLOOPSTART) + { + if(pEnv->nLoopStart <= 0) + return; + pEnv->nLoopStart--; + } else if(m_nDragItem == ENV_DRAGLOOPEND) + { + if(pEnv->nLoopEnd <= 0) + return; + if(pEnv->nLoopEnd <= pEnv->nLoopStart) + pEnv->nLoopStart--; + pEnv->nLoopEnd--; + } else + { + // Move envelope node + if(!IsDragItemEnvPoint() || m_nDragItem <= 1) + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + return; + } + if(!EnvSetValue(m_nDragItem - 1, pEnv->at(m_nDragItem - 1).tick - stepsize)) + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + return; + } + } + UpdateIndicator(); + SetModified(InstrumentHint().Envelope(), true); +} + + +void CViewInstrument::EnvKbdMovePointRight(int stepsize) +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr) + return; + const MODTYPE modType = GetDocument()->GetModType(); + + // Move loop points? + PrepareUndo("Move Envelope Point"); + if(m_nDragItem == ENV_DRAGSUSTAINSTART) + { + if(pEnv->nSustainStart >= pEnv->size() - 1) + return; + if(pEnv->nSustainStart >= pEnv->nSustainEnd) + pEnv->nSustainEnd++; + pEnv->nSustainStart++; + } else if(m_nDragItem == ENV_DRAGSUSTAINEND) + { + if(pEnv->nSustainEnd >= pEnv->size() - 1) + return; + pEnv->nSustainEnd++; + if(modType == MOD_TYPE_XM) + pEnv->nSustainStart = pEnv->nSustainEnd; + } else if(m_nDragItem == ENV_DRAGLOOPSTART) + { + if(pEnv->nLoopStart >= pEnv->size() - 1) + return; + if(pEnv->nLoopStart >= pEnv->nLoopEnd) + pEnv->nLoopEnd++; + pEnv->nLoopStart++; + } else if(m_nDragItem == ENV_DRAGLOOPEND) + { + if(pEnv->nLoopEnd >= pEnv->size() - 1) + return; + pEnv->nLoopEnd++; + } else + { + // Move envelope node + if(!IsDragItemEnvPoint() || m_nDragItem <= 1) + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + return; + } + if(!EnvSetValue(m_nDragItem - 1, pEnv->at(m_nDragItem - 1).tick + stepsize)) + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + return; + } + } + UpdateIndicator(); + SetModified(InstrumentHint().Envelope(), true); +} + + +void CViewInstrument::EnvKbdMovePointVertical(int stepsize) +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + int val = pEnv->at(m_nDragItem - 1).value + stepsize; + PrepareUndo("Move Envelope Point"); + if(EnvSetValue(m_nDragItem - 1, int32_min, val, false)) + { + UpdateIndicator(); + SetModified(InstrumentHint().Envelope(), true); + } else + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } +} + + +void CViewInstrument::EnvKbdInsertPoint() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr) + return; + if(!IsDragItemEnvPoint()) + m_nDragItem = pEnv->size(); + EnvelopeNode::tick_t newTick = 10; + EnvelopeNode::value_t newVal = m_nEnv == ENV_VOLUME ? ENVELOPE_MAX : ENVELOPE_MID; + if(m_nDragItem < pEnv->size() && (pEnv->at(m_nDragItem).tick - pEnv->at(m_nDragItem - 1).tick > 1)) + { + // If some other point than the last is selected: interpolate between this and next point (if there's room between them) + newTick = (pEnv->at(m_nDragItem - 1).tick + pEnv->at(m_nDragItem).tick) / 2; + newVal = (pEnv->at(m_nDragItem - 1).value + pEnv->at(m_nDragItem).value) / 2; + } else if(!pEnv->empty()) + { + // Last point is selected: add point after last point + newTick = pEnv->back().tick + 4; + newVal = pEnv->back().value; + } + + auto newPoint = EnvInsertPoint(newTick, newVal); + if(newPoint > 0) + m_nDragItem = newPoint; + UpdateIndicator(); +} + + +void CViewInstrument::EnvKbdRemovePoint() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint() || pEnv->empty()) + return; + if(m_nDragItem > pEnv->size()) + m_nDragItem = pEnv->size(); + EnvRemovePoint(m_nDragItem - 1); + UpdateIndicator(); +} + + +void CViewInstrument::EnvKbdSetLoopStart() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + PrepareUndo("Set Envelope Loop Start"); + if(!EnvGetLoop()) + EnvSetLoopStart(0); + EnvSetLoopStart(m_nDragItem - 1); + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); +} + + +void CViewInstrument::EnvKbdSetLoopEnd() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + PrepareUndo("Set Envelope Loop End"); + if(!EnvGetLoop()) + { + EnvSetLoop(true); + EnvSetLoopStart(0); + } + EnvSetLoopEnd(m_nDragItem - 1); + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); +} + + +void CViewInstrument::EnvKbdSetSustainStart() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + PrepareUndo("Set Envelope Sustain Start"); + if(!EnvGetSustain()) + EnvSetSustain(true); + EnvSetSustainStart(m_nDragItem - 1); + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); +} + + +void CViewInstrument::EnvKbdSetSustainEnd() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + PrepareUndo("Set Envelope Sustain End"); + if(!EnvGetSustain()) + { + EnvSetSustain(true); + EnvSetSustainStart(0); + } + EnvSetSustainEnd(m_nDragItem - 1); + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); +} + + +void CViewInstrument::EnvKbdToggleReleaseNode() +{ + InstrumentEnvelope *pEnv = GetEnvelopePtr(); + if(pEnv == nullptr || !IsDragItemEnvPoint()) + return; + PrepareUndo("Toggle Release Node"); + if(EnvToggleReleaseNode(m_nDragItem - 1)) + { + UpdateIndicator(); + SetModified(InstrumentHint().Envelope(), true); + } else + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } +} + + +// Get a pointer to the currently active instrument. +ModInstrument *CViewInstrument::GetInstrumentPtr() const +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return nullptr; + return pModDoc->GetSoundFile().Instruments[m_nInstrument]; +} + + +// Get a pointer to the currently selected envelope. +// This function also implicitely validates the moddoc and soundfile pointers. +InstrumentEnvelope *CViewInstrument::GetEnvelopePtr() const +{ + // First do some standard checks... + ModInstrument *pIns = GetInstrumentPtr(); + if(pIns == nullptr) + return nullptr; + + return &pIns->GetEnvelope(m_nEnv); +} + + +bool CViewInstrument::CanMovePoint(uint32 envPoint, int step) +{ + const InstrumentEnvelope *env = GetEnvelopePtr(); + if(env == nullptr) + return false; + + // Can't move first point + if(envPoint == 0) + { + return false; + } + // Can't move left of previous point + if((step < 0) && (env->at(envPoint).tick - env->at(envPoint - 1).tick <= -step)) + { + return false; + } + // Can't move right of next point + if((step > 0) && (envPoint < env->size() - 1) && (env->at(envPoint + 1).tick - env->at(envPoint).tick <= step)) + { + return false; + } + return true; +} + + +BOOL CViewInstrument::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + // Ctrl + mouse wheel: envelope zoom. + if(nFlags == MK_CONTROL) + { + // Speed up zoom scrolling by some factor (might need some tuning). + const float speedUpFactor = std::max(1.0f, m_zoom * 7.0f / ENV_MAX_ZOOM); + EnvSetZoom(m_zoom + speedUpFactor * (zDelta / WHEEL_DELTA)); + } + + return CModScrollView::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CViewInstrument::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) + OnPrevInstrument(); + else if(nButton == XBUTTON2) + OnNextInstrument(); + CModScrollView::OnXButtonUp(nFlags, nButton, point); +} + + +void CViewInstrument::OnEnvLoad() +{ + if(GetInstrumentPtr() == nullptr) + return; + + FileDialog dlg = OpenFileDialog() + .DefaultExtension("envelope") + .ExtensionFilter("Instrument Envelopes (*.envelope)|*.envelope||") + .WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir()); + if(!dlg.Show(this)) return; + TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory()); + + PrepareUndo("Replace Envelope"); + if(GetDocument()->LoadEnvelope(m_nInstrument, m_nEnv, dlg.GetFirstFile())) + { + SetModified(InstrumentHint(m_nInstrument).Envelope(), true); + } else + { + GetDocument()->GetInstrumentUndo().RemoveLastUndoStep(m_nInstrument); + } +} + + +void CViewInstrument::OnEnvSave() +{ + const InstrumentEnvelope *env = GetEnvelopePtr(); + if(env == nullptr || env->empty()) + { + MessageBeep(MB_ICONWARNING); + return; + } + + FileDialog dlg = SaveFileDialog() + .DefaultExtension("envelope") + .ExtensionFilter("Instrument Envelopes (*.envelope)|*.envelope||") + .WorkingDirectory(TrackerSettings::Instance().PathInstruments.GetWorkingDir()); + if(!dlg.Show(this)) return; + TrackerSettings::Instance().PathInstruments.SetWorkingDir(dlg.GetWorkingDirectory()); + + if(!GetDocument()->SaveEnvelope(m_nInstrument, m_nEnv, dlg.GetFirstFile())) + { + Reporting::Error(MPT_CFORMAT("Unable to save file {}")(dlg.GetFirstFile()), _T("OpenMPT"), this); + } +} + + +void CViewInstrument::OnUpdateUndo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetInstrumentUndo().CanUndo(m_nInstrument)); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetInstrumentUndo().GetUndoName(m_nInstrument)))); + } +} + + +void CViewInstrument::OnUpdateRedo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetInstrumentUndo().CanRedo(m_nInstrument)); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetInstrumentUndo().GetRedoName(m_nInstrument)))); + } +} + + +void CViewInstrument::OnEditUndo() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + if(pModDoc->GetInstrumentUndo().Undo(m_nInstrument)) + { + SetModified(InstrumentHint().Info().Envelope().Names(), true); + } +} + + +void CViewInstrument::OnEditRedo() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + if(pModDoc->GetInstrumentUndo().Redo(m_nInstrument)) + { + SetModified(InstrumentHint().Info().Envelope().Names(), true); + } +} + + +INT_PTR CViewInstrument::OnToolHitTest(CPoint point, TOOLINFO *pTI) const +{ + CRect ncRect; + ClientToScreen(&point); + const auto ncButton = GetNcButtonAtPoint(point, &ncRect); + if(ncButton == uint32_max) + return CModScrollView::OnToolHitTest(point, pTI); + + auto buttonID = cLeftBarButtons[ncButton]; + ScreenToClient(&ncRect); + + pTI->hwnd = m_hWnd; + pTI->uId = buttonID; + pTI->rect = ncRect; + CString text = LoadResourceString(buttonID); + + CommandID cmd = kcNull; + switch(buttonID) + { + case ID_ENVSEL_VOLUME: cmd = kcInstrumentEnvelopeSwitchToVolume; break; + case ID_ENVSEL_PANNING: cmd = kcInstrumentEnvelopeSwitchToPanning; break; + case ID_ENVSEL_PITCH: cmd = kcInstrumentEnvelopeSwitchToPitch; break; + case ID_ENVELOPE_VOLUME: cmd = kcInstrumentEnvelopeToggleVolume; break; + case ID_ENVELOPE_PANNING: cmd = kcInstrumentEnvelopeTogglePanning; break; + case ID_ENVELOPE_PITCH: cmd = kcInstrumentEnvelopeTogglePitch; break; + case ID_ENVELOPE_FILTER: cmd = kcInstrumentEnvelopeToggleFilter; break; + case ID_ENVELOPE_SETLOOP: cmd = kcInstrumentEnvelopeToggleLoop; break; + case ID_ENVELOPE_SUSTAIN: cmd = kcInstrumentEnvelopeToggleSustain; break; + case ID_ENVELOPE_CARRY: cmd = kcInstrumentEnvelopeToggleCarry; break; + case ID_INSTRUMENT_SAMPLEMAP: cmd = kcInsNoteMapEditSampleMap; break; + case ID_ENVELOPE_ZOOM_IN: cmd = kcInstrumentEnvelopeZoomIn; break; + case ID_ENVELOPE_ZOOM_OUT: cmd = kcInstrumentEnvelopeZoomOut; break; + case ID_ENVELOPE_LOAD: cmd = kcInstrumentEnvelopeLoad; break; + case ID_ENVELOPE_SAVE: cmd = kcInstrumentEnvelopeSave; break; + } + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if(!keyText.IsEmpty()) + text += MPT_CFORMAT(" ({})")(keyText); + } + + // MFC will free() the text + auto size = text.GetLength() + 1; + TCHAR *textP = static_cast<TCHAR *>(calloc(size, sizeof(TCHAR))); + std::copy(text.GetString(), text.GetString() + size, textP); + pTI->lpszText = textP; + + return buttonID; +} + + +// Accessible description for screen readers +HRESULT CViewInstrument::get_accName(VARIANT varChild, BSTR *pszName) +{ + const InstrumentEnvelope *env = GetEnvelopePtr(); + if(env == nullptr) + return CModScrollView::get_accName(varChild, pszName); + + const TCHAR *typeStr = _T(""); + switch(m_nEnv) + { + case ENV_VOLUME: typeStr = _T("Volume"); break; + case ENV_PANNING: typeStr = _T("Panning"); break; + case ENV_PITCH: typeStr = env->dwFlags[ENV_FILTER] ? _T("Filter") : _T("Pitch"); break; + } + + CString str; + if(env->empty() || m_nDragItem == 0) + { + str = typeStr; + if(env->empty()) + str += _T(" envelope has no points"); + else + str += MPT_CFORMAT(" envelope, {} point{}")(env->size(), env->size() == 1 ? CString(_T("")) : CString(_T("s"))); + } else + { + bool isEnvPoint = false; + auto point = DragItemToEnvPoint(); + auto tick = EnvGetTick(point); + switch(m_nDragItem) + { + case ENV_DRAGLOOPSTART: str = _T("Loop start"); break; + case ENV_DRAGLOOPEND: str = _T("Loop end"); break; + case ENV_DRAGSUSTAINSTART: str = _T("Sustain loop start"); break; + case ENV_DRAGSUSTAINEND: str = _T("Sustain loop end"); break; + default: isEnvPoint = true; + } + if(!isEnvPoint) + { + str += MPT_CFORMAT(" at point {}, tick {}")(point + 1, tick); + } else + { + str = MPT_CFORMAT("Point {}, tick {}, {} {}")(point + 1, tick, CString(typeStr), EnvValueToString(EnvGetTick(point), EnvGetValue(point))); + if(env->dwFlags[ENV_LOOP]) + { + if(point == env->nLoopStart) + str += _T(", loop start"); + if(point == env->nLoopEnd) + str += _T(", loop end"); + } + if(env->dwFlags[ENV_SUSTAIN]) + { + if(point == env->nLoopStart) + str += _T(", sustain loop start"); + if(point == env->nLoopEnd) + str += _T(", sustain loop end"); + } + if(env->nReleaseNode == point) + str += _T(", release node"); + } + } + + *pszName = str.AllocSysString(); + return S_OK; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.h b/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.h new file mode 100644 index 00000000..b6f1943d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_ins.h @@ -0,0 +1,250 @@ +/* + * view_ins.h + * ---------- + * Purpose: Instrument tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +OPENMPT_NAMESPACE_BEGIN + +#define INSSTATUS_DRAGGING 0x01 +#define INSSTATUS_NCLBTNDOWN 0x02 +#define INSSTATUS_SPLITCURSOR 0x04 + +// Non-Client toolbar buttons +#define ENV_LEFTBAR_BUTTONS 22 + +enum DragPoints +{ + ENV_DRAGLOOPSTART = (MAX_ENVPOINTS + 1), + ENV_DRAGLOOPEND = (MAX_ENVPOINTS + 2), + ENV_DRAGSUSTAINSTART = (MAX_ENVPOINTS + 3), + ENV_DRAGSUSTAINEND = (MAX_ENVPOINTS + 4), + ENV_DRAGPREVIOUS = (MAX_ENVPOINTS + 5), + ENV_DRAGNEXT = (MAX_ENVPOINTS + 6), +}; + +class CViewInstrument: public CModScrollView +{ +protected: + CImageList m_bmpEnvBar; + CPoint m_ptMenu; + CRect m_rcClient, m_rcOldClient; + + CBitmap m_bmpGrid; + CBitmap m_bmpMemMain; + HBITMAP m_pbmpOldGrid = nullptr; + HBITMAP oldBitmap = nullptr; + + EnvelopeType m_nEnv = ENV_VOLUME; + uint32 m_nDragItem = 1, m_nBtnMouseOver = 0xFFFF; + DWORD m_dwStatus = 0; + DWORD m_NcButtonState[ENV_LEFTBAR_BUTTONS]; + + INSTRUMENTINDEX m_nInstrument = 1; + + CDC m_dcMemMain; + CDC m_dcGrid; + int m_GridScrollPos = -1; + int m_GridSpeed = -1; + + float m_zoom = 4; + int m_envPointSize = 4; + + bool m_bGrid = true; + bool m_bGridForceRedraw = false; + bool m_mouseMoveModified = false; + + std::bitset<128> m_baPlayingNote; + CModDoc::NoteToChannelMap m_noteChannel; // Note -> Preview channel assignment + std::array<uint32, MAX_CHANNELS> m_dwNotifyPos; + +public: + CViewInstrument(); + DECLARE_SERIAL(CViewInstrument) + +protected: + void PrepareUndo(const char *description); + + //////////////////////// + // Envelope get stuff + + // Flags + bool EnvGetFlag(const EnvelopeFlags dwFlag) const; + bool EnvGetLoop() const { return EnvGetFlag(ENV_LOOP); }; + bool EnvGetSustain() const { return EnvGetFlag(ENV_SUSTAIN); }; + bool EnvGetCarry() const { return EnvGetFlag(ENV_CARRY); }; + + // Misc. + uint32 EnvGetTick(int nPoint) const; + uint32 EnvGetValue(int nPoint) const; + uint32 EnvGetLastPoint() const; + uint32 EnvGetNumPoints() const; + + // Get loop points + uint32 EnvGetLoopStart() const; + uint32 EnvGetLoopEnd() const; + uint32 EnvGetSustainStart() const; + uint32 EnvGetSustainEnd() const; + + // Get envelope status + bool EnvGetVolEnv() const; + bool EnvGetPanEnv() const; + bool EnvGetPitchEnv() const; + bool EnvGetFilterEnv() const; + + //////////////////////// + // Envelope set stuff + + // Flags + bool EnvSetFlag(EnvelopeFlags flag, bool enable); + bool EnvSetLoop(bool enable) {return EnvSetFlag(ENV_LOOP, enable);}; + bool EnvSetSustain(bool enable) {return EnvSetFlag(ENV_SUSTAIN, enable);}; + bool EnvSetCarry(bool enable) {return EnvSetFlag(ENV_CARRY, enable);}; + + // Misc. + bool EnvSetValue(int nPoint, int32 nTick = int32_min, int32 nValue = int32_min, bool moveTail = false); + bool CanMovePoint(uint32 envPoint, int step); + + // Set loop points + bool EnvSetLoopStart(int nPoint); + bool EnvSetLoopEnd(int nPoint); + bool EnvSetSustainStart(int nPoint); + bool EnvSetSustainEnd(int nPoint); + bool EnvToggleReleaseNode(int nPoint); + + // Set envelope status + bool EnvToggleEnv(EnvelopeType envelope, CSoundFile &sndFile, ModInstrument &ins, bool enable, EnvelopeNode::value_t defaultValue, EnvelopeFlags extraFlags = EnvelopeFlags(0)); + bool EnvSetVolEnv(bool enable); + bool EnvSetPanEnv(bool enable); + bool EnvSetPitchEnv(bool enable); + bool EnvSetFilterEnv(bool enable); + + // Keyboard envelope control + void EnvKbdSelectPoint(DragPoints point); + void EnvKbdMovePointLeft(int stepsize); + void EnvKbdMovePointRight(int stepsize); + void EnvKbdMovePointVertical(int stepsize); + void EnvKbdInsertPoint(); + void EnvKbdRemovePoint(); + void EnvKbdSetLoopStart(); + void EnvKbdSetLoopEnd(); + void EnvKbdSetSustainStart(); + void EnvKbdSetSustainEnd(); + void EnvKbdToggleReleaseNode(); + + bool IsDragItemEnvPoint() const { return m_nDragItem >= 1 && m_nDragItem <= EnvGetNumPoints(); } + + //////////////////////// + // Misc stuff + void UpdateScrollSize(); + void SetModified(InstrumentHint hint, bool updateAll); + BOOL SetCurrentInstrument(INSTRUMENTINDEX nIns, EnvelopeType m_nEnv = ENV_VOLUME); + ModInstrument *GetInstrumentPtr() const; + InstrumentEnvelope *GetEnvelopePtr() const; + bool InsertAtPoint(CPoint pt); + uint32 EnvInsertPoint(int nTick, int nValue); + bool EnvRemovePoint(uint32 nPoint); + uint32 DragItemToEnvPoint() const; + int TickToScreen(int nTick) const; + int PointToScreen(int nPoint) const; + int ScreenToTick(int x) const; + int ScreenToPoint(int x, int y) const; + int ValueToScreen(int val) const { return m_rcClient.bottom - 1 - (val * (m_rcClient.bottom - 1)) / 64; } + int ScreenToValue(int y) const; + void InvalidateEnvelope() { InvalidateRect(NULL, FALSE); } + void DrawPositionMarks(); + void DrawNcButton(CDC *pDC, UINT nBtn); + bool GetNcButtonRect(UINT button, CRect &rect) const; + UINT GetNcButtonAtPoint(CPoint point, CRect *outRect = nullptr) const; + void UpdateNcButtonState(); + void PlayNote(ModCommand::NOTE note); + void DrawGrid(CDC *memDC, uint32 speed); + void UpdateIndicator(); + void UpdateIndicator(int tick, int val); + CString EnvValueToString(int tick, int val) const; + + void OnEnvZoomIn() { EnvSetZoom(m_zoom + 1); }; + void OnEnvZoomOut() { EnvSetZoom(m_zoom - 1); }; + void EnvSetZoom(float fNewZoom); + +public: + //{{AFX_VIRTUAL(CViewInstrument) + void OnDraw(CDC *) override; + void OnInitialUpdate() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + BOOL PreTranslateMessage(MSG *pMsg) override; + BOOL OnDragonDrop(BOOL, const DRAGONDROP *) override; + LRESULT OnModViewMsg(WPARAM, LPARAM) override; + LRESULT OnPlayerNotify(Notification *) override; + HRESULT get_accName(VARIANT varChild, BSTR *pszName) override; + INT_PTR OnToolHitTest(CPoint point, TOOLINFO *pTI) const override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CViewInstrument) + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + afx_msg void OnSetFocus(CWnd *pOldWnd); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg LRESULT OnDPIChanged(WPARAM = 0, LPARAM = 0); + afx_msg void OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp); + afx_msg LRESULT OnNcHitTest(CPoint point); + afx_msg void OnNcPaint(); + afx_msg void OnPrevInstrument(); + afx_msg void OnNextInstrument(); + afx_msg void OnMouseMove(UINT, CPoint); + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnLButtonUp(UINT, CPoint); + afx_msg void OnLButtonDblClk(UINT /*nFlags*/, CPoint point) { InsertAtPoint(point); } + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnMButtonDown(UINT, CPoint); + afx_msg void OnNcMouseMove(UINT nHitTest, CPoint point); + afx_msg void OnNcLButtonDown(UINT, CPoint); + afx_msg void OnNcLButtonUp(UINT, CPoint); + afx_msg void OnNcLButtonDblClk(UINT, CPoint); + afx_msg void OnEnvLoopChanged(); + afx_msg void OnEnvSustainChanged(); + afx_msg void OnEnvCarryChanged(); + afx_msg void OnEnvToggleReleasNode(); + afx_msg void OnEnvInsertPoint(); + afx_msg void OnEnvRemovePoint(); + afx_msg void OnSelectVolumeEnv(); + afx_msg void OnSelectPanningEnv(); + afx_msg void OnSelectPitchEnv(); + afx_msg void OnEnvVolChanged(); + afx_msg void OnEnvPanChanged(); + afx_msg void OnEnvPitchChanged(); + afx_msg void OnEnvFilterChanged(); + afx_msg void OnEnvToggleGrid(); + afx_msg void OnEnvLoad(); + afx_msg void OnEnvSave(); + afx_msg void OnEditCopy(); + afx_msg void OnEditPaste(); + afx_msg void OnEditSampleMap(); + afx_msg void OnEnvelopeScalePoints(); + afx_msg void OnDropFiles(HDROP hDropInfo); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + afx_msg void OnEditUndo(); + afx_msg void OnEditRedo(); + afx_msg void OnUpdateUndo(CCmdUI *pCmdUI); + afx_msg void OnUpdateRedo(CCmdUI *pCmdUI); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() + +private: + uint8 EnvGetReleaseNode(); +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.cpp new file mode 100644 index 00000000..944b7f38 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.cpp @@ -0,0 +1,7320 @@ +/* + * View_pat.cpp + * ------------ + * Purpose: Pattern tab, lower panel. + * Notes : Welcome to about 7000 lines of, err, very beautiful code. + * Authors: Olivier Lapicque + * 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 "Childfrm.h" +#include "Moddoc.h" +#include "SampleEditorDialogs.h" // For amplification dialog (which is re-used from sample editor) +#include "Globals.h" +#include "View_pat.h" +#include "Ctrl_pat.h" +#include "PatternFont.h" +#include "PatternFindReplace.h" +#include "PatternFindReplaceDlg.h" + +#include "EffectVis.h" +#include "PatternGotoDialog.h" +#include "MIDIMacros.h" +#include "../common/misc_util.h" +#include "../soundlib/MIDIEvents.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include <algorithm> + + +OPENMPT_NAMESPACE_BEGIN + + +// Static initializers +ModCommand CViewPattern::m_cmdOld = ModCommand::Empty(); +int32 CViewPattern::m_nTransposeAmount = 1; + +IMPLEMENT_SERIAL(CViewPattern, CModScrollView, 0) + +BEGIN_MESSAGE_MAP(CViewPattern, CModScrollView) + //{{AFX_MSG_MAP(CViewPattern) + ON_WM_ERASEBKGND() + ON_WM_VSCROLL() + ON_WM_SIZE() + ON_WM_MOUSEWHEEL() + ON_WM_XBUTTONUP() + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONDBLCLK() + ON_WM_LBUTTONUP() + ON_WM_RBUTTONDOWN() + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_WM_SYSKEYDOWN() + ON_WM_DESTROY() + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewPattern::OnCustomKeyMsg) + ON_MESSAGE(WM_MOD_MIDIMSG, &CViewPattern::OnMidiMsg) + ON_MESSAGE(WM_MOD_RECORDPARAM, &CViewPattern::OnRecordPlugParamChange) + ON_COMMAND(ID_EDIT_CUT, &CViewPattern::OnEditCut) + ON_COMMAND(ID_EDIT_COPY, &CViewPattern::OnEditCopy) + ON_COMMAND(ID_EDIT_PASTE, &CViewPattern::OnEditPaste) + ON_COMMAND(ID_EDIT_MIXPASTE, &CViewPattern::OnEditMixPaste) + ON_COMMAND(ID_EDIT_MIXPASTE_ITSTYLE,&CViewPattern::OnEditMixPasteITStyle) + ON_COMMAND(ID_EDIT_PASTEFLOOD, &CViewPattern::OnEditPasteFlood) + ON_COMMAND(ID_EDIT_PUSHFORWARDPASTE,&CViewPattern::OnEditPushForwardPaste) + ON_COMMAND(ID_EDIT_SELECT_ALL, &CViewPattern::OnEditSelectAll) + ON_COMMAND(ID_EDIT_SELECTCOLUMN,&CViewPattern::OnEditSelectChannel) + ON_COMMAND(ID_EDIT_SELECTCOLUMN2,&CViewPattern::OnSelectCurrentChannel) + ON_COMMAND(ID_EDIT_FIND, &CViewPattern::OnEditFind) + ON_COMMAND(ID_EDIT_GOTO_MENU, &CViewPattern::OnEditGoto) + ON_COMMAND(ID_EDIT_FINDNEXT, &CViewPattern::OnEditFindNext) + ON_COMMAND(ID_EDIT_RECSELECT, &CViewPattern::OnRecordSelect) + ON_COMMAND(ID_EDIT_SPLITRECSELECT, &CViewPattern::OnSplitRecordSelect) + ON_COMMAND(ID_EDIT_SPLITKEYBOARDSETTINGS, &CViewPattern::SetSplitKeyboardSettings) + ON_COMMAND(ID_EDIT_UNDO, &CViewPattern::OnEditUndo) + ON_COMMAND(ID_EDIT_REDO, &CViewPattern::OnEditRedo) + ON_COMMAND(ID_PATTERN_CHNRESET, &CViewPattern::OnChannelReset) + ON_COMMAND(ID_PATTERN_MUTE, &CViewPattern::OnMuteFromClick) + ON_COMMAND(ID_PATTERN_SOLO, &CViewPattern::OnSoloFromClick) + ON_COMMAND(ID_PATTERN_TRANSITIONMUTE, &CViewPattern::OnTogglePendingMuteFromClick) + ON_COMMAND(ID_PATTERN_TRANSITIONSOLO, &CViewPattern::OnPendingSoloChnFromClick) + ON_COMMAND(ID_PATTERN_TRANSITION_UNMUTEALL, &CViewPattern::OnPendingUnmuteAllChnFromClick) + ON_COMMAND(ID_PATTERN_UNMUTEALL,&CViewPattern::OnUnmuteAll) + ON_COMMAND(ID_PATTERN_SPLIT, &CViewPattern::OnSplitPattern) + ON_COMMAND(ID_NEXTINSTRUMENT, &CViewPattern::OnNextInstrument) + ON_COMMAND(ID_PREVINSTRUMENT, &CViewPattern::OnPrevInstrument) + ON_COMMAND(ID_PATTERN_PLAYROW, &CViewPattern::OnPatternStep) + ON_COMMAND(IDC_PATTERN_RECORD, &CViewPattern::OnPatternRecord) + ON_COMMAND(ID_PATTERN_DELETEROW, &CViewPattern::OnDeleteRow) + ON_COMMAND(ID_PATTERN_DELETEALLROW, &CViewPattern::OnDeleteWholeRow) + ON_COMMAND(ID_PATTERN_DELETEROWGLOBAL, &CViewPattern::OnDeleteRowGlobal) + ON_COMMAND(ID_PATTERN_DELETEALLROWGLOBAL, &CViewPattern::OnDeleteWholeRowGlobal) + ON_COMMAND(ID_PATTERN_INSERTROW, &CViewPattern::OnInsertRow) + ON_COMMAND(ID_PATTERN_INSERTALLROW, &CViewPattern::OnInsertWholeRow) + ON_COMMAND(ID_PATTERN_INSERTROWGLOBAL, &CViewPattern::OnInsertRowGlobal) + ON_COMMAND(ID_PATTERN_INSERTALLROWGLOBAL, &CViewPattern::OnInsertWholeRowGlobal) + ON_COMMAND(ID_RUN_SCRIPT, &CViewPattern::OnRunScript) + ON_COMMAND(ID_TRANSPOSE_UP, &CViewPattern::OnTransposeUp) + ON_COMMAND(ID_TRANSPOSE_DOWN, &CViewPattern::OnTransposeDown) + ON_COMMAND(ID_TRANSPOSE_OCTUP, &CViewPattern::OnTransposeOctUp) + ON_COMMAND(ID_TRANSPOSE_OCTDOWN, &CViewPattern::OnTransposeOctDown) + ON_COMMAND(ID_TRANSPOSE_CUSTOM, &CViewPattern::OnTransposeCustom) + ON_COMMAND(ID_PATTERN_PROPERTIES, &CViewPattern::OnPatternProperties) + ON_COMMAND(ID_PATTERN_INTERPOLATE_VOLUME, &CViewPattern::OnInterpolateVolume) + ON_COMMAND(ID_PATTERN_INTERPOLATE_EFFECT, &CViewPattern::OnInterpolateEffect) + ON_COMMAND(ID_PATTERN_INTERPOLATE_NOTE, &CViewPattern::OnInterpolateNote) + ON_COMMAND(ID_PATTERN_INTERPOLATE_INSTR, &CViewPattern::OnInterpolateInstr) + ON_COMMAND(ID_PATTERN_VISUALIZE_EFFECT, &CViewPattern::OnVisualizeEffect) + ON_COMMAND(ID_GROW_SELECTION, &CViewPattern::OnGrowSelection) + ON_COMMAND(ID_SHRINK_SELECTION, &CViewPattern::OnShrinkSelection) + ON_COMMAND(ID_PATTERN_SETINSTRUMENT, &CViewPattern::OnSetSelInstrument) + ON_COMMAND(ID_PATTERN_ADDCHANNEL_FRONT, &CViewPattern::OnAddChannelFront) + ON_COMMAND(ID_PATTERN_ADDCHANNEL_AFTER, &CViewPattern::OnAddChannelAfter) + ON_COMMAND(ID_PATTERN_RESETCHANNELCOLORS, &CViewPattern::OnResetChannelColors) + ON_COMMAND(ID_PATTERN_TRANSPOSECHANNEL, &CViewPattern::OnTransposeChannel) + ON_COMMAND(ID_PATTERN_DUPLICATECHANNEL, &CViewPattern::OnDuplicateChannel) + ON_COMMAND(ID_PATTERN_REMOVECHANNEL, &CViewPattern::OnRemoveChannel) + ON_COMMAND(ID_PATTERN_REMOVECHANNELDIALOG, &CViewPattern::OnRemoveChannelDialog) + ON_COMMAND(ID_PATTERN_AMPLIFY, &CViewPattern::OnPatternAmplify) + ON_COMMAND(ID_CLEAR_SELECTION, &CViewPattern::OnClearSelectionFromMenu) + ON_COMMAND(ID_SHOWTIMEATROW, &CViewPattern::OnShowTimeAtRow) + ON_COMMAND(ID_PATTERN_EDIT_PCNOTE_PLUGIN, &CViewPattern::OnTogglePCNotePluginEditor) + ON_COMMAND(ID_SETQUANTIZE, &CViewPattern::OnSetQuantize) + ON_COMMAND(ID_LOCK_PATTERN_ROWS, &CViewPattern::OnLockPatternRows) + ON_COMMAND_RANGE(ID_CHANGE_INSTRUMENT, ID_CHANGE_INSTRUMENT+MAX_INSTRUMENTS, &CViewPattern::OnSelectInstrument) + ON_COMMAND_RANGE(ID_CHANGE_PCNOTE_PARAM, ID_CHANGE_PCNOTE_PARAM + ModCommand::maxColumnValue, &CViewPattern::OnSelectPCNoteParam) + ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewPattern::OnUpdateUndo) + ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewPattern::OnUpdateRedo) + ON_COMMAND_RANGE(ID_PLUGSELECT, ID_PLUGSELECT+MAX_MIXPLUGINS, &CViewPattern::OnSelectPlugin) + + + //}}AFX_MSG_MAP + ON_WM_INITMENU() + ON_WM_RBUTTONDBLCLK() + ON_WM_RBUTTONUP() +END_MESSAGE_MAP() + +static_assert(ModCommand::maxColumnValue <= 999, "Command range for ID_CHANGE_PCNOTE_PARAM is designed for 999"); + +const CSoundFile *CViewPattern::GetSoundFile() const { return (GetDocument() != nullptr) ? &GetDocument()->GetSoundFile() : nullptr; }; +CSoundFile *CViewPattern::GetSoundFile() { return (GetDocument() != nullptr) ? &GetDocument()->GetSoundFile() : nullptr; }; + +const ModSequence &CViewPattern::Order() const { return GetSoundFile()->Order(); } +ModSequence &CViewPattern::Order() { return GetSoundFile()->Order(); } + + +CViewPattern::CViewPattern() +{ + EnableActiveAccessibility(); + + m_Dib.Init(CMainFrame::bmpNotes); + UpdateColors(); + m_PCNoteEditMemory = ModCommand::Empty(); + m_octaveKeyMemory.fill(NOTE_NONE); +} + + +CViewPattern::~CViewPattern() +{ + m_offScreenBitmap.DeleteObject(); + m_offScreenDC.DeleteDC(); +} + + +void CViewPattern::OnInitialUpdate() +{ + CModScrollView::OnInitialUpdate(); + EnableToolTips(); + ChnVUMeters.fill(0); + OldVUMeters.fill(0); + m_previousNote.fill(NOTE_NONE); + m_splitActiveNoteChannel.fill(NOTE_CHANNEL_MAP_INVALID); + m_activeNoteChannel.fill(NOTE_CHANNEL_MAP_INVALID); + m_nPlayPat = PATTERNINDEX_INVALID; + m_nPlayRow = m_nNextPlayRow = 0; + m_nPlayTick = 0; + m_nTicksOnRow = 1; + m_nMidRow = 0; + m_nDragItem = {}; + m_bInItemRect = false; + m_bContinueSearch = false; + m_Status = psShowPluginNames; + m_nXScroll = m_nYScroll = 0; + m_nPattern = 0; + m_nSpacing = 0; + m_nAccelChar = 0; + PatternFont::UpdateFont(m_hWnd); + UpdateSizes(); + UpdateScrollSize(); + SetCurrentPattern(0); + m_fallbackInstrument = 0; + m_nLastPlayedRow = 0; + m_nLastPlayedOrder = ORDERINDEX_INVALID; + m_prevChordNote = NOTE_NONE; +} + + +bool CViewPattern::SetCurrentPattern(PATTERNINDEX pat, ROWINDEX row) +{ + const CSoundFile *pSndFile = GetSoundFile(); + + if(pSndFile == nullptr) + return false; + if(pat == pSndFile->Order.GetIgnoreIndex() || pat == pSndFile->Order.GetInvalidPatIndex()) + return false; + if(m_pEditWnd && m_pEditWnd->IsWindowVisible()) + m_pEditWnd->ShowWindow(SW_HIDE); + + m_nPattern = pat; + bool updateScroll = false; + + if(pSndFile->Patterns.IsValidPat(pat)) + { + if(row != ROWINDEX_INVALID && row != GetCurrentRow() && row < pSndFile->Patterns[m_nPattern].GetNumRows()) + { + m_Cursor.SetRow(row); + updateScroll = true; + } + if(GetCurrentRow() >= pSndFile->Patterns[m_nPattern].GetNumRows()) + { + m_Cursor.SetRow(0); + updateScroll = true; + } + } + + SetSelToCursor(); + + UpdateSizes(); + UpdateScrollSize(); + UpdateIndicator(); + + if(m_bWholePatternFitsOnScreen) + SetScrollPos(SB_VERT, 0); + else if(updateScroll) + SetScrollPos(SB_VERT, (int)GetCurrentRow() * GetRowHeight()); + + UpdateScrollPos(); + InvalidatePattern(true, true); + SendCtrlMessage(CTRLMSG_PATTERNCHANGED, m_nPattern); + + return true; +} + + +// This should be used instead of consecutive calls to SetCurrentRow() then SetCurrentColumn(). +bool CViewPattern::SetCursorPosition(const PatternCursor &cursor, bool wrap) +{ + // Set row, but do not update scroll position yet + // as there is another position update on the way: + SetCurrentRow(cursor.GetRow(), wrap, false); + // Now set column and update scroll position: + SetCurrentColumn(cursor); + return true; +} + + +ROWINDEX CViewPattern::SetCurrentRow(ROWINDEX row, bool wrap, bool updateHorizontalScrollbar) +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidIndex(m_nPattern)) + return ROWINDEX_INVALID; + + if(wrap && pSndFile->Patterns[m_nPattern].GetNumRows()) + { + const auto &order = Order(); + if((int)row < 0) + { + if(m_Status[psKeyboardDragSelect | psMouseDragSelect]) + { + row = 0; + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) + { + ORDERINDEX curOrder = GetCurrentOrder(); + const ORDERINDEX prevOrd = order.GetPreviousOrderIgnoringSkips(curOrder); + if(prevOrd < curOrder && m_nPattern == order[curOrder]) + { + const PATTERNINDEX nPrevPat = order[prevOrd]; + if((nPrevPat < pSndFile->Patterns.Size()) && (pSndFile->Patterns[nPrevPat].GetNumRows())) + { + SetCurrentOrder(prevOrd); + if(SetCurrentPattern(nPrevPat)) + return SetCurrentRow(pSndFile->Patterns[nPrevPat].GetNumRows() + (int)row); + } + } + row = 0; + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP) + { + if((int)row < (int)0) + row += pSndFile->Patterns[m_nPattern].GetNumRows(); + row %= pSndFile->Patterns[m_nPattern].GetNumRows(); + } + } else //row >= 0 + if(row >= pSndFile->Patterns[m_nPattern].GetNumRows()) + { + if(m_Status[psKeyboardDragSelect | psMouseDragSelect]) + { + row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1; + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) + { + ORDERINDEX curOrder = GetCurrentOrder(); + ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder); + if(nextOrder > curOrder && m_nPattern == order[curOrder]) + { + PATTERNINDEX nextPat = order[nextOrder]; + if((nextPat < pSndFile->Patterns.Size()) && (pSndFile->Patterns[nextPat].GetNumRows())) + { + const ROWINDEX newRow = row - pSndFile->Patterns[m_nPattern].GetNumRows(); + SetCurrentOrder(nextOrder); + if(SetCurrentPattern(nextPat)) + return SetCurrentRow(newRow); + } + } + row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1; + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP) + { + row %= pSndFile->Patterns[m_nPattern].GetNumRows(); + } + } + } + + if(!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)) + { + if(static_cast<int>(row) < 0) + row = 0; + if(row >= pSndFile->Patterns[m_nPattern].GetNumRows()) + row = pSndFile->Patterns[m_nPattern].GetNumRows() - 1; + } + + if((row >= pSndFile->Patterns[m_nPattern].GetNumRows()) || (!m_szCell.cy)) + return false; + // Fix: If cursor isn't on screen move both scrollbars to make it visible + InvalidateRow(); + m_Cursor.SetRow(row); + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + UpdateScrollbarPositions(updateHorizontalScrollbar); + InvalidateRow(); + + PatternCursor selStart(m_Cursor); + if(m_Status[psKeyboardDragSelect | psMouseDragSelect] && !m_Status[psDragnDropEdit]) + { + selStart.Set(m_StartSel); + } + SetCurSel(selStart, m_Cursor); + + return row; +} + + +bool CViewPattern::SetCurrentColumn(CHANNELINDEX channel, PatternCursor::Columns column) +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr) + { + return false; + } + + LimitMax(column, m_nDetailLevel); + m_Cursor.SetColumn(channel, column); + + PatternCursor selStart(m_Cursor); + + if(m_Status[psKeyboardDragSelect | psMouseDragSelect] && !m_Status[psDragnDropEdit]) + { + selStart = m_StartSel; + } + SetCurSel(selStart, m_Cursor); + // Fix: If cursor isn't on screen move both scrollbars to make it visible + UpdateScrollbarPositions(); + return true; +} + + +// Set document as modified and optionally update all pattern views. +void CViewPattern::SetModified(bool updateAllViews) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc != nullptr) + { + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, PatternHint(m_nPattern).Data(), updateAllViews ? nullptr : this); + } + CMainFrame::GetMainFrame()->NotifyAccessibilityUpdate(*this); +} + + +// Fix: If cursor isn't on screen move scrollbars to make it visible +// Fix: save pattern scrollbar position when switching to other tab +// Assume that m_nRow and m_dwCursor are valid +// When we switching to other tab the CViewPattern object is deleted +// and when switching back new one is created +bool CViewPattern::UpdateScrollbarPositions(bool updateHorizontalScrollbar) +{ + // HACK - after new CViewPattern object created SetCurrentRow() and SetCurrentColumn() are called - + // just skip first two calls of UpdateScrollbarPositions() if pModDoc->GetOldPatternScrollbarsPos() is valid + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + CSize scroll = pModDoc->GetOldPatternScrollbarsPos(); + if(scroll.cx >= 0) + { + OnScrollBy(scroll); + scroll.cx = -1; + pModDoc->SetOldPatternScrollbarsPos(scroll); + return true; + } else if(scroll.cx >= -1) + { + scroll.cx = -2; + pModDoc->SetOldPatternScrollbarsPos(scroll); + return true; + } + } + CSize scroll(0, 0); + UINT row = GetCurrentRow(); + UINT yofs = GetYScrollPos(); + CRect rect; + GetClientRect(&rect); + rect.top += m_szHeader.cy; + int numrows = (rect.bottom - rect.top - 1) / m_szCell.cy; + if(numrows < 1) + numrows = 1; + if(m_nMidRow) + { + if(row != yofs) + { + scroll.cy = (int)(row - yofs) * m_szCell.cy; + } + } else + { + if(row < yofs) + { + scroll.cy = (int)(row - yofs) * m_szCell.cy; + } else if(row > yofs + (UINT)numrows - 1) + { + scroll.cy = (int)(row - yofs - numrows + 1) * m_szCell.cy; + } + } + + if(updateHorizontalScrollbar) + { + UINT xofs = GetXScrollPos(); + const CHANNELINDEX nchn = GetCurrentChannel(); + if(nchn < xofs) + { + scroll.cx = (int)(xofs - nchn) * m_szCell.cx; + scroll.cx *= -1; + } else if(nchn > xofs) + { + int maxcol = (rect.right - m_szHeader.cx) / m_szCell.cx; + if((nchn >= (xofs + maxcol)) && (maxcol >= 0)) + { + scroll.cx = (int)(nchn - xofs - maxcol + 1) * m_szCell.cx; + } + } + } + if(scroll.cx != 0 || scroll.cy != 0) + { + OnScrollBy(scroll, TRUE); + } + return true; +} + +DragItem CViewPattern::GetDragItem(CPoint point, RECT &outRect) const +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr) + return {}; + + CRect rcClient, rect, plugRect; + + GetClientRect(&rcClient); + rect.SetRect(m_szHeader.cx, 0, m_szHeader.cx + GetChannelWidth(), m_szHeader.cy); + plugRect.SetRect(m_szHeader.cx, m_szHeader.cy - m_szPluginHeader.cy, m_szHeader.cx + GetChannelWidth(), m_szHeader.cy); + + const auto xOffset = static_cast<CHANNELINDEX>(GetXScrollPos()); + const CHANNELINDEX numChannels = pSndFile->GetNumChannels(); + + // Checking channel headers + if(m_Status[psShowPluginNames]) + { + for(CHANNELINDEX n = xOffset; n < numChannels; n++) + { + if(plugRect.PtInRect(point)) + { + outRect = plugRect; + return {DragItem::PluginName, n}; + } + plugRect.OffsetRect(GetChannelWidth(), 0); + } + } + for(CHANNELINDEX n = xOffset; n < numChannels; n++) + { + if(rect.PtInRect(point)) + { + outRect = rect; + return {DragItem::ChannelHeader, n}; + } + rect.OffsetRect(GetChannelWidth(), 0); + } + if(pSndFile->Patterns.IsValidPat(m_nPattern) && (pSndFile->GetType() & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT))) + { + // Clicking on upper-left corner with pattern number (for pattern properties) + rect.SetRect(0, 0, m_szHeader.cx, m_szHeader.cy); + if(rect.PtInRect(point)) + { + outRect = rect; + return {DragItem::PatternHeader, 0}; + } + } + return {}; +} + + +// Drag a selection to position "cursor". +// If scrollHorizontal is true, the point's channel is ensured to be visible. +// Likewise, if scrollVertical is true, the point's row is ensured to be visible. +// If noMode if specified, the original selection points are not altered. +bool CViewPattern::DragToSel(const PatternCursor &cursor, bool scrollHorizontal, bool scrollVertical, bool noMove) +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + return false; + + CRect rect; + int yofs = GetYScrollPos(), xofs = GetXScrollPos(); + int row, col; + + if(!m_szCell.cy) + return false; + GetClientRect(&rect); + if(!noMove) + SetCurSel(m_StartSel, cursor); + if(!scrollHorizontal && !scrollVertical) + return true; + + // Scroll to row + row = cursor.GetRow(); + if(scrollVertical && row < (int)pSndFile->Patterns[m_nPattern].GetNumRows()) + { + row += m_nMidRow; + rect.top += m_szHeader.cy; + int numrows = (rect.bottom - rect.top - 1) / m_szCell.cy; + if(numrows < 1) + numrows = 1; + if(row < yofs) + { + CSize sz; + sz.cx = 0; + sz.cy = (int)(row - yofs) * m_szCell.cy; + OnScrollBy(sz, TRUE); + } else if(row > yofs + numrows - 1) + { + CSize sz; + sz.cx = 0; + sz.cy = (int)(row - yofs - numrows + 1) * m_szCell.cy; + OnScrollBy(sz, TRUE); + } + } + + // Scroll to column + col = cursor.GetChannel(); + if(scrollHorizontal && col < (int)pSndFile->GetNumChannels()) + { + int maxcol = (rect.right - m_szHeader.cx) - 4; + maxcol -= GetColumnOffset(cursor.GetColumnType()); + maxcol /= GetChannelWidth(); + if(col < xofs) + { + CSize size(-m_szCell.cx, 0); + if(!noMove) + size.cx = (col - xofs) * (int)m_szCell.cx; + OnScrollBy(size, TRUE); + } else if((col > xofs + maxcol) && (maxcol > 0)) + { + CSize size(m_szCell.cx, 0); + if(!noMove) + size.cx = (col - maxcol + 1) * (int)m_szCell.cx; + OnScrollBy(size, TRUE); + } + } + UpdateWindow(); + return true; +} + + +bool CViewPattern::SetPlayCursor(PATTERNINDEX pat, ROWINDEX row, uint32 tick) +{ + PATTERNINDEX oldPat = m_nPlayPat; + ROWINDEX oldRow = m_nPlayRow; + uint32 oldTick = m_nPlayTick; + + m_nPlayPat = pat; + m_nPlayRow = row; + m_nPlayTick = tick; + + if(m_nPlayTick != oldTick && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL)) + InvalidatePattern(true, true); + else if(oldPat == m_nPattern) + InvalidateRow(oldRow); + else if(m_nPlayPat == m_nPattern) + InvalidateRow(m_nPlayRow); + + return true; +} + + +UINT CViewPattern::GetCurrentInstrument() const +{ + return static_cast<UINT>(SendCtrlMessage(CTRLMSG_GETCURRENTINSTRUMENT)); +} + + +bool CViewPattern::ShowEditWindow() +{ + if(!m_pEditWnd) + { + m_pEditWnd = new CEditCommand(*GetSoundFile()); + } + if(m_pEditWnd) + { + m_pEditWnd->ShowEditWindow(m_nPattern, m_Cursor, this); + return true; + } + return false; +} + + +bool CViewPattern::PrepareUndo(const PatternCursor &beginSel, const PatternCursor &endSel, const char *description) +{ + CModDoc *pModDoc = GetDocument(); + const CHANNELINDEX chnBegin = beginSel.GetChannel(), chnEnd = endSel.GetChannel(); + const ROWINDEX rowBegin = beginSel.GetRow(), rowEnd = endSel.GetRow(); + + if((chnEnd < chnBegin) || (rowEnd < rowBegin) || pModDoc == nullptr) + return false; + return pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, chnBegin, rowBegin, chnEnd - chnBegin + 1, rowEnd - rowBegin + 1, description); +} + + +BOOL CViewPattern::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxViewPatterns + 1 + m_Cursor.GetColumnType()); + // If editing is disabled, preview notes no matter which column we are in + if(!IsEditingEnabled() && TrackerSettings::Instance().patternNoEditPopup) + ctx = kCtxViewPatternsNote; + + if(ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + { + return true; // Mapped to a command, no need to pass message on. + } + //HACK: fold kCtxViewPatternsFX and kCtxViewPatternsFXparam so that all commands of 1 are active in the other + else + { + if(ctx == kCtxViewPatternsFX) + { + if(ih->KeyEvent(kCtxViewPatternsFXparam, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } else if(ctx == kCtxViewPatternsFXparam) + { + if(ih->KeyEvent(kCtxViewPatternsFX, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } else if(ctx == kCtxViewPatternsIns) + { + // Do the same with instrument->note column + if(ih->KeyEvent(kCtxViewPatternsNote, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } + } + //end HACK. + + // Handle Application (menu) key + if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS) + { + OnRButtonDown(0, GetPointFromPosition(m_Cursor)); + } + } else if(pMsg->message == WM_MBUTTONDOWN) + { + // Open quick channel properties dialog if we're middle-clicking a channel header. + CPoint point(GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam)); + if(point.y < m_szHeader.cy - m_szPluginHeader.cy) + { + PatternCursor cursor = GetPositionFromPoint(point); + if(cursor.GetChannel() < GetDocument()->GetNumChannels()) + { + ClientToScreen(&point); + m_quickChannelProperties.Show(GetDocument(), cursor.GetChannel(), point); + return true; + } + } + } + } + + return CModScrollView::PreTranslateMessage(pMsg); +} + + +//////////////////////////////////////////////////////////////////////// +// CViewPattern message handlers + +void CViewPattern::OnDestroy() +{ + // Fix: save pattern scrollbar position when switching to other tab + // When we switching to other tab the CViewPattern object is deleted + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + pModDoc->SetOldPatternScrollbarsPos(CSize(m_nXScroll * m_szCell.cx, m_nYScroll * m_szCell.cy)); + } + if(m_pEffectVis) + { + m_pEffectVis->DoClose(); + m_pEffectVis = nullptr; + } + + if(m_pEditWnd) + { + m_pEditWnd->DestroyWindow(); + delete m_pEditWnd; + m_pEditWnd = NULL; + } + + CModScrollView::OnDestroy(); +} + + +void CViewPattern::OnSetFocus(CWnd *pOldWnd) +{ + CScrollView::OnSetFocus(pOldWnd); + m_Status.set(psFocussed); + InvalidateRow(); + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + pModDoc->SetNotifications(Notification::Position | Notification::VUMeters); + pModDoc->SetFollowWnd(m_hWnd); + UpdateIndicator(); + } +} + + +void CViewPattern::OnKillFocus(CWnd *pNewWnd) +{ + CScrollView::OnKillFocus(pNewWnd); + + m_Status.reset(psKeyboardDragSelect | psCtrlDragSelect | psFocussed); + InvalidateRow(); +} + + +void CViewPattern::OnGrowSelection() +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return; + } + + BeginWaitCursor(); + + m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + const PatternCursor startSel = m_Selection.GetUpperLeft(); + const PatternCursor endSel = m_Selection.GetLowerRight(); + PrepareUndo(startSel, PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows(), endSel), "Grow Selection"); + + const ROWINDEX finalDest = m_Selection.GetStartRow() + (m_Selection.GetNumRows() - 1) * 2; + for(int row = finalDest; row > (int)startSel.GetRow(); row -= 2) + { + if(ROWINDEX(row) >= pSndFile->Patterns[m_nPattern].GetNumRows()) + { + continue; + } + + int offset = row - startSel.GetRow(); + + for(CHANNELINDEX chn = startSel.GetChannel(); chn <= endSel.GetChannel(); chn++) + { + for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++) + { + PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i)); + if(!m_Selection.ContainsHorizontal(cell)) + { + // We might have to skip the first / last few entries. + continue; + } + + ModCommand *dest = pSndFile->Patterns[m_nPattern].GetpModCommand(row, chn); + ModCommand *src = pSndFile->Patterns[m_nPattern].GetpModCommand(row - offset / 2, chn); + ModCommand *blank = pSndFile->Patterns[m_nPattern].GetpModCommand(row - 1, chn); // Row "in between" + + switch(i) + { + case PatternCursor::noteColumn: + dest->note = src->note; + blank->note = NOTE_NONE; + break; + + case PatternCursor::instrColumn: + dest->instr = src->instr; + blank->instr = 0; + break; + + case PatternCursor::volumeColumn: + dest->volcmd = src->volcmd; + blank->volcmd = VOLCMD_NONE; + dest->vol = src->vol; + blank->vol = 0; + break; + + case PatternCursor::effectColumn: + dest->command = src->command; + blank->command = CMD_NONE; + break; + + case PatternCursor::paramColumn: + dest->param = src->param; + blank->param = 0; + break; + } + } + } + } + + // Adjust selection + m_Selection = PatternRect(startSel, PatternCursor(std::min(finalDest, static_cast<ROWINDEX>(pSndFile->Patterns[m_nPattern].GetNumRows() - 1)), endSel)); + + InvalidatePattern(); + SetModified(); + EndWaitCursor(); + SetFocus(); +} + + +void CViewPattern::OnShrinkSelection() +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return; + } + + BeginWaitCursor(); + + m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + const PatternCursor startSel = m_Selection.GetUpperLeft(); + const PatternCursor endSel = m_Selection.GetLowerRight(); + PrepareUndo(startSel, endSel, "Shrink Selection"); + + const ROWINDEX finalDest = m_Selection.GetStartRow() + (m_Selection.GetNumRows() - 1) / 2; + for(ROWINDEX row = startSel.GetRow(); row <= endSel.GetRow(); row++) + { + const ROWINDEX offset = row - startSel.GetRow(); + const ROWINDEX srcRow = startSel.GetRow() + (offset * 2); + + for(CHANNELINDEX chn = startSel.GetChannel(); chn <= endSel.GetChannel(); chn++) + { + ModCommand *dest = pSndFile->Patterns[m_nPattern].GetpModCommand(row, chn); + ModCommand src; + + if(row <= finalDest) + { + // Normal shrink operation + src = *pSndFile->Patterns[m_nPattern].GetpModCommand(srcRow, chn); + + // If source command is empty, try next source row (so we don't lose all the stuff that's on odd rows). + if(srcRow < pSndFile->Patterns[m_nPattern].GetNumRows() - 1) + { + const ModCommand &srcNext = *pSndFile->Patterns[m_nPattern].GetpModCommand(srcRow + 1, chn); + if(src.note == NOTE_NONE) + src.note = srcNext.note; + if(src.instr == 0) + src.instr = srcNext.instr; + if(src.volcmd == VOLCMD_NONE) + { + src.volcmd = srcNext.volcmd; + src.vol = srcNext.vol; + } + if(src.command == CMD_NONE) + { + src.command = srcNext.command; + src.param = srcNext.param; + } + } + } else + { + // Clean up rows that are now supposed to be empty. + src = ModCommand::Empty(); + } + + for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++) + { + PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i)); + if(!m_Selection.ContainsHorizontal(cell)) + { + // We might have to skip the first / last few entries. + continue; + } + + switch(i) + { + case PatternCursor::noteColumn: + dest->note = src.note; + break; + + case PatternCursor::instrColumn: + dest->instr = src.instr; + break; + + case PatternCursor::volumeColumn: + dest->vol = src.vol; + dest->volcmd = src.volcmd; + break; + + case PatternCursor::effectColumn: + dest->command = src.command; + break; + + case PatternCursor::paramColumn: + dest->param = src.param; + break; + } + } + } + } + + // Adjust selection + m_Selection = PatternRect(startSel, PatternCursor(std::min(finalDest, static_cast<ROWINDEX>(pSndFile->Patterns[m_nPattern].GetNumRows() - 1)), endSel)); + + InvalidatePattern(); + SetModified(); + EndWaitCursor(); + SetFocus(); +} + + +void CViewPattern::OnClearSelectionFromMenu() +{ + OnClearSelection(); +} + +void CViewPattern::OnClearSelection(bool ITStyle, RowMask rm) //Default RowMask: all elements enabled +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled_bmsg()) + { + return; + } + + BeginWaitCursor(); + + // If selection ends to a note column, in ITStyle extending it to instrument column since the instrument data is + // removed with note data. + if(ITStyle && m_Selection.GetEndColumn() == PatternCursor::noteColumn) + { + PatternCursor lower(m_Selection.GetLowerRight()); + lower.Move(0, 0, 1); + m_Selection = PatternRect(m_Selection.GetUpperLeft(), lower); + } + + m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + + PrepareUndo(m_Selection, "Clear Selection"); + + ApplyToSelection([&] (ModCommand &m, ROWINDEX row, CHANNELINDEX chn) + { + for(int i = PatternCursor::firstColumn; i <= PatternCursor::lastColumn; i++) + { + PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(i)); + if(!m_Selection.ContainsHorizontal(cell)) + { + // We might have to skip the first / last few entries. + continue; + } + + switch(i) + { + case PatternCursor::noteColumn: // Clear note + if(rm.note) + { + if(m.IsPcNote()) + { // Clear whole cell if clearing PC note + m.Clear(); + } else + { + m.note = NOTE_NONE; + if(ITStyle) + m.instr = 0; + } + } + break; + + case PatternCursor::instrColumn: // Clear instrument + if(rm.instrument) + { + m.instr = 0; + } + break; + + case PatternCursor::volumeColumn: // Clear volume + if(rm.volume) + { + m.volcmd = VOLCMD_NONE; + m.vol = 0; + } + break; + + case PatternCursor::effectColumn: // Clear Command + if(rm.command) + { + m.command = CMD_NONE; + if(m.IsPcNote()) + { + m.SetValueEffectCol(0); + } + } + break; + + case PatternCursor::paramColumn: // Clear Command Param + if(rm.parameter) + { + m.param = 0; + if(m.IsPcNote()) + { + m.SetValueEffectCol(0); + + if(cell.CompareColumn(m_Selection.GetUpperLeft()) == 0) + { + // If this is the first selected column, update effect column char as well + PatternCursor upper(m_Selection.GetUpperLeft()); + upper.Move(0, 0, -1); + m_Selection = PatternRect(upper, m_Selection.GetLowerRight()); + } + } + } + break; + } + } + }); + + // Expand invalidation to the whole column. Needed for: + // - Last column is the effect character (parameter needs to be invalidated, too + // - PC Notes + // - Default volume display is enabled. + PatternCursor endCursor(m_Selection.GetEndRow(), m_Selection.GetEndChannel() + 1); + + InvalidateArea(m_Selection.GetUpperLeft(), endCursor); + SetModified(); + EndWaitCursor(); + SetFocus(); +} + + +void CViewPattern::OnEditCut() +{ + OnEditCopy(); + OnClearSelection(false); +} + + +void CViewPattern::OnEditCopy() +{ + CModDoc *pModDoc = GetDocument(); + + if(pModDoc) + { + CopyPattern(m_nPattern, m_Selection); + SetFocus(); + } +} + + +void CViewPattern::StartRecordGroupDragging(const DragItem source) +{ + // Drag-select record channels + const auto *modDoc = GetDocument(); + if(modDoc == nullptr) + return; + + m_initialDragRecordStatus.resize(modDoc->GetNumChannels()); + for(CHANNELINDEX chn = 0; chn < modDoc->GetNumChannels(); chn++) + { + m_initialDragRecordStatus[chn] = modDoc->GetChannelRecordGroup(chn); + } + m_Status.reset(psDragging); + m_nDropItem = m_nDragItem = source; +} + + +void CViewPattern::OnLButtonDown(UINT nFlags, CPoint point) +{ + const auto *modDoc = GetDocument(); + if(modDoc == nullptr) + return; + const auto &sndFile = modDoc->GetSoundFile(); + + SetFocus(); + m_nDropItem = m_nDragItem = GetDragItem(point, m_rcDragItem); + m_Status.set(psDragging); + m_bInItemRect = true; + m_Status.reset(psShiftDragging); + + PatternCursor pointCursor(GetPositionFromPoint(point)); + + SetCapture(); + if(point.x >= m_szHeader.cx && point.y <= m_szHeader.cy - m_szPluginHeader.cy) + { + // Click on channel header + if(nFlags & MK_CONTROL) + TogglePendingMute(pointCursor.GetChannel()); + if(nFlags & MK_SHIFT) + { + // Drag-select record channels + StartRecordGroupDragging(m_nDragItem); + } + } else if(point.x >= m_szHeader.cx && point.y > m_szHeader.cy) + { + // Click on pattern data + if(IsLiveRecord() && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOFOLLOWONCLICK)) + { + SendCtrlMessage(CTRLMSG_PAT_FOLLOWSONG, 0); + } + + if(CMainFrame::GetInputHandler()->SelectionPressed() + && (m_Status[psShiftSelect] + || m_Selection.GetUpperLeft() == m_Selection.GetLowerRight() + || !m_Selection.Contains(pointCursor))) + { + // Shift pressed -> set 2nd selection point + // This behaviour is only used if: + // * Shift-click has previously been used since the shift key has been pressed down (psShiftSelect flag is set), + // * No selection has been made yet, or + // * Shift-clicking outside the current selection. + // This is necessary so that selections can still be moved properly while the shift button is pressed (for copy-move). + DragToSel(pointCursor, true, true); + m_Status.set(psShiftSelect); + } else + { + // Set first selection point + m_StartSel = pointCursor; + if(m_StartSel.GetChannel() < sndFile.GetNumChannels()) + { + m_Status.set(psMouseDragSelect); + + if(m_Status[psCtrlDragSelect]) + { + SetCurSel(m_StartSel); + } + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_DRAGNDROPEDIT) + && ((m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) || m_Status[psCtrlDragSelect]) + && m_Selection.Contains(m_StartSel)) + { + m_Status.set(psDragnDropEdit); + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CENTERROW) + { + SetCurSel(m_StartSel); + } else + { + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(m_StartSel); + } + } + } + } else if(point.x < m_szHeader.cx && point.y > m_szHeader.cy) + { + // Mark row number => mark whole row (start) + InvalidateSelection(); + if(pointCursor.GetRow() < sndFile.Patterns[m_nPattern].GetNumRows()) + { + m_StartSel.Set(pointCursor.GetRow(), 0); + SetCurSel(m_StartSel, PatternCursor(pointCursor.GetRow(), sndFile.GetNumChannels() - 1, PatternCursor::lastColumn)); + m_Status.set(psRowSelection); + } + } + + if(m_nDragItem.IsValid()) + { + InvalidateRect(&m_rcDragItem, FALSE); + UpdateWindow(); + } +} + + +void CViewPattern::OnLButtonDblClk(UINT uFlags, CPoint point) +{ + PatternCursor cursor = GetPositionFromPoint(point); + if(cursor == m_Cursor && point.y >= m_szHeader.cy) + { + // Double-click pattern cell: Select whole column or show cell properties. + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_DBLCLICKSELECT)) + { + OnSelectCurrentChannel(); + m_Status.set(psChannelSelection | psDragging); + return; + } else + { + if(ShowEditWindow()) + return; + } + } + + OnLButtonDown(uFlags, point); +} + + +void CViewPattern::OnLButtonUp(UINT nFlags, CPoint point) +{ + CModDoc *modDoc = GetDocument(); + if(modDoc == nullptr) + return; + + const auto dragType = m_nDragItem.Type(); + const bool wasDraggingRecordGroup = IsDraggingRecordGroup(); + const bool itemSelected = m_bInItemRect || (dragType == DragItem::ChannelHeader); + m_bInItemRect = false; + ResetRecordGroupDragging(); + ReleaseCapture(); + m_Status.reset(psMouseDragSelect | psRowSelection | psChannelSelection | psDragging); + // Drag & Drop Editing + if(m_Status[psDragnDropEdit]) + { + if(m_Status[psDragnDropping]) + { + OnDrawDragSel(); + m_Status.reset(psDragnDropping); + OnDropSelection(); + } + + if(GetPositionFromPoint(point) == m_StartSel) + { + SetCursorPosition(m_StartSel); + } + SetCursor(CMainFrame::curArrow); + m_Status.reset(psDragnDropEdit); + } + if(dragType != DragItem::ChannelHeader + && dragType != DragItem::PatternHeader + && dragType != DragItem::PluginName) + { + if((m_nMidRow) && (m_Selection.GetUpperLeft() == m_Selection.GetLowerRight())) + { + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(m_Selection.GetUpperLeft()); + //UpdateIndicator(); + } + } + if(!itemSelected || !m_nDragItem.IsValid()) + return; + InvalidateRect(&m_rcDragItem, FALSE); + const CHANNELINDEX sourceChn = static_cast<CHANNELINDEX>(m_nDragItem.Value()); + const CHANNELINDEX targetChn = m_nDropItem.IsValid() ? static_cast<CHANNELINDEX>(m_nDropItem.Value()) : CHANNELINDEX_INVALID; + + switch(m_nDragItem.Type()) + { + case DragItem::ChannelHeader: + if(sourceChn == targetChn && targetChn < modDoc->GetNumChannels()) + { + // Just clicked a channel header... + if(nFlags & MK_SHIFT) + { + // Toggle record state + modDoc->ToggleChannelRecordGroup(sourceChn, RecordGroup::Group1); + InvalidateChannelsHeaders(sourceChn); + } else if(CMainFrame::GetInputHandler()->AltPressed()) + { + // Solo / Unsolo + OnSoloChannel(sourceChn); + } else if(!(nFlags & MK_CONTROL)) + { + // Mute / Unmute + OnMuteChannel(sourceChn); + } + } else if(!wasDraggingRecordGroup && targetChn < modDoc->GetNumChannels() && m_nDropItem.Type() == DragItem::ChannelHeader) + { + // Dragged to other channel header => move or copy channel + + InvalidateRect(&m_rcDropItem, FALSE); + + const bool duplicate = (nFlags & MK_SHIFT) != 0; + DragChannel(sourceChn, targetChn, 1, duplicate); + } + break; + + case DragItem::PatternHeader: + OnPatternProperties(); + break; + + case DragItem::PluginName: + if(sourceChn < MAX_BASECHANNELS) + TogglePluginEditor(sourceChn); + break; + } + + m_nDropItem = {}; +} + + +void CViewPattern::DragChannel(CHANNELINDEX source, CHANNELINDEX target, CHANNELINDEX numChannels, bool duplicate) +{ + auto modDoc = GetDocument(); + const CHANNELINDEX newChannels = modDoc->GetNumChannels() + (duplicate ? numChannels : 0); + std::vector<CHANNELINDEX> channels(newChannels, 0); + bool modified = duplicate; + + for(CHANNELINDEX chn = 0, fromChn = 0; chn < newChannels; chn++) + { + if(chn >= target && chn < target + numChannels) + { + channels[chn] = source + chn - target; + } else + { + if(fromChn == source && !duplicate) // Don't want the source channels twice if we're just moving + { + fromChn += numChannels; + } + channels[chn] = fromChn++; + } + if(channels[chn] != chn) + { + modified = true; + } + } + if(modified && modDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID) + { + modDoc->UpdateAllViews(this, GeneralHint().Channels().ModType(), this); + if(duplicate) + { + // Number of channels changed: Update channel headers and other information. + SetCurrentPattern(m_nPattern); + } + + if(!duplicate) + { + const auto oldSel = m_Selection; + if(auto chn = m_Cursor.GetChannel(); (chn >= source && chn < source + numChannels)) + SetCurrentColumn(target + chn - source, m_Cursor.GetColumnType()); + if(oldSel.GetStartChannel() >= source && oldSel.GetEndChannel() < source + numChannels) + { + const auto diff = static_cast<int>(target) - source; + auto upperLeft = oldSel.GetUpperLeft(), lowerRight = oldSel.GetLowerRight(); + upperLeft.Move(0, diff, 0); + lowerRight.Move(0, diff, 0); + SetCurSel(upperLeft, lowerRight); + } + } + + InvalidatePattern(true, false); + SetModified(false); + } +} + + +void CViewPattern::ShowPatternProperties(PATTERNINDEX pat) +{ + CModDoc *pModDoc = GetDocument(); + if(pat == PATTERNINDEX_INVALID) + pat = m_nPattern; + if(pModDoc && pModDoc->GetSoundFile().Patterns.IsValidPat(pat)) + { + CPatternPropertiesDlg dlg(*pModDoc, pat, this); + if(dlg.DoModal() == IDOK) + { + UpdateScrollSize(); + InvalidatePattern(true, true); + SanitizeCursor(); + pModDoc->UpdateAllViews(this, PatternHint(pat).Data(), this); + } + } +} + + +void CViewPattern::OnRButtonDown(UINT flags, CPoint pt) +{ + CModDoc *modDoc = GetDocument(); + HMENU hMenu; + + // Too far left to get a ctx menu: + if(!modDoc || pt.x < m_szHeader.cx) + { + return; + } + + // Handle drag n drop + if(m_Status[psDragnDropEdit]) + { + if(m_Status[psDragnDropping]) + { + OnDrawDragSel(); + m_Status.reset(psDragnDropping); + } + m_Status.reset(psDragnDropEdit | psMouseDragSelect); + if(m_Status[psDragging]) + { + m_Status.reset(psDragging); + m_bInItemRect = false; + ReleaseCapture(); + } + SetCursor(CMainFrame::curArrow); + return; + } + + if((hMenu = ::CreatePopupMenu()) == NULL) + { + return; + } + + CSoundFile &sndFile = modDoc->GetSoundFile(); + m_MenuCursor = GetPositionFromPoint(pt); + + // Right-click outside single-point selection? Reposition cursor to the new location + if(!m_Selection.Contains(m_MenuCursor) && m_Selection.GetUpperLeft() == m_Selection.GetLowerRight()) + { + if(pt.y > m_szHeader.cy) + { + //ensure we're not clicking header + + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(m_MenuCursor); + } + } + const CHANNELINDEX nChn = m_MenuCursor.GetChannel(); + const bool inChannelHeader = (pt.y < m_szHeader.cy); + + if((flags & MK_CONTROL) && nChn < sndFile.GetNumChannels() && inChannelHeader) + { + // Ctrl+Right-Click: Open quick channel properties. + ClientToScreen(&pt); + m_quickChannelProperties.Show(GetDocument(), nChn, pt); + } else if((flags & MK_SHIFT) && inChannelHeader) + { + // Drag-select record channels + StartRecordGroupDragging(GetDragItem(pt, m_rcDragItem)); + } else if(nChn < sndFile.GetNumChannels() && sndFile.Patterns.IsValidPat(m_nPattern) && !(flags & (MK_CONTROL | MK_SHIFT))) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //------ Plugin Header Menu --------- : + if(m_Status[psShowPluginNames] && + inChannelHeader && (pt.y > m_szHeader.cy - m_szPluginHeader.cy)) + { + BuildPluginCtxMenu(hMenu, nChn, sndFile); + } + + //------ Channel Header Menu ---------- : + else if(inChannelHeader) + { + if(ih->ShiftPressed()) + { + //Don't bring up menu if shift is pressed, else we won't get button up msg. + } else + { + if(BuildSoloMuteCtxMenu(hMenu, ih, nChn, sndFile)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + BuildRecordCtxMenu(hMenu, ih, nChn); + BuildChannelControlCtxMenu(hMenu, ih); + } + } + + //------ Standard Menu ---------- : + else if((pt.x >= m_szHeader.cx) && (pt.y >= m_szHeader.cy)) + { + // When combining menus, use bitwise ORs to avoid shortcuts + if(BuildSelectionCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildEditCtxMenu(hMenu, ih, modDoc)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildInterpolationCtxMenu(hMenu, ih) + | BuildTransposeCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildVisFXCtxMenu(hMenu, ih) + | BuildAmplifyCtxMenu(hMenu, ih) + | BuildSetInstCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildPCNoteCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildGrowShrinkCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildMiscCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + if(BuildRowInsDelCtxMenu(hMenu, ih)) + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + + CString s = _T("&Quantize "); + if(TrackerSettings::Instance().recordQuantizeRows != 0) + { + uint32 rows = TrackerSettings::Instance().recordQuantizeRows.Get(); + s += MPT_CFORMAT("(Currently: {} Row{})")(rows, CString(rows == 1 ? _T("") : _T("s"))); + } else + { + s += _T("Settings..."); + } + AppendMenu(hMenu, MF_STRING | (TrackerSettings::Instance().recordQuantizeRows != 0 ? MF_CHECKED : 0), ID_SETQUANTIZE, ih->GetKeyTextFromCommand(kcQuantizeSettings, s)); + } + + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + } else if(nChn >= sndFile.GetNumChannels() && sndFile.GetNumChannels() < sndFile.GetModSpecifications().channelsMax && !(flags & (MK_CONTROL | MK_SHIFT))) + { + // Click outside of pattern: Offer easy way to add more channels + m_MenuCursor.Set(0, sndFile.GetNumChannels() - 1); + AppendMenu(hMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_AFTER, _T("&Add Channel")); + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + } + ::DestroyMenu(hMenu); +} + +void CViewPattern::OnRButtonUp(UINT nFlags, CPoint point) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + + ResetRecordGroupDragging(); + const CHANNELINDEX sourceChn = static_cast<CHANNELINDEX>(m_nDragItem.Value()); + const CHANNELINDEX targetChn = m_nDropItem.IsValid() ? static_cast<CHANNELINDEX>(m_nDropItem.Value()) : CHANNELINDEX_INVALID; + switch(m_nDragItem.Type()) + { + case DragItem::ChannelHeader: + if(nFlags & MK_SHIFT) + { + if(sourceChn < MAX_BASECHANNELS && sourceChn == targetChn) + { + pModDoc->ToggleChannelRecordGroup(sourceChn, RecordGroup::Group2); + InvalidateChannelsHeaders(sourceChn); + } + } + break; + } + + CModScrollView::OnRButtonUp(nFlags, point); +} + + +BOOL CViewPattern::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + if(nFlags & MK_CONTROL) + { + // Ctrl + mouse wheel: Increment / decrement values + DataEntry(zDelta > 0, (nFlags & MK_SHIFT) == MK_SHIFT); + return TRUE; + } + if(IsLiveRecord() && !m_Status[psDragActive]) + { + // During live playback with "follow song" enabled, the mouse wheel can be used to jump forwards and backwards. + CursorJump(-mpt::signum(zDelta), false); + return TRUE; + } + return CModScrollView::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CViewPattern::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) + OnPrevOrder(); + else if(nButton == XBUTTON2) + OnNextOrder(); + CModScrollView::OnXButtonUp(nFlags, nButton, point); +} + + +void CViewPattern::OnMouseMove(UINT nFlags, CPoint point) +{ + CModScrollView::OnMouseMove(nFlags, point); + + const bool isDraggingRecordGroup = IsDraggingRecordGroup(); + if(!m_Status[psDragging] && !isDraggingRecordGroup) + return; + + // Drag&Drop actions + if(m_nDragItem.IsValid()) + { + const CRect oldDropRect = m_rcDropItem; + const auto oldDropItem = m_nDropItem; + + if(isDraggingRecordGroup) + { + // When drag-selecting record channels, ignore y position + point.y = m_rcDragItem.top; + } + + m_Status.set(psShiftDragging, (nFlags & MK_SHIFT) != 0); + m_nDropItem = GetDragItem(point, m_rcDropItem); + + const bool b = (m_nDropItem == m_nDragItem); + const bool dragChannel = m_nDragItem.Type() == DragItem::ChannelHeader; + + if(b != m_bInItemRect || (m_nDropItem != oldDropItem && dragChannel)) + { + m_bInItemRect = b; + InvalidateRect(&m_rcDragItem, FALSE); + + // Drag-select record channels + if(isDraggingRecordGroup && m_nDropItem.Type() == DragItem::ChannelHeader) + { + auto modDoc = GetDocument(); + auto startChn = static_cast<CHANNELINDEX>(m_nDragItem.Value()); + auto endChn = static_cast<CHANNELINDEX>(m_nDropItem.Value()); + + RecordGroup setRecord = RecordGroup::NoGroup; + if(m_initialDragRecordStatus[startChn] != RecordGroup::Group1 && (nFlags & MK_LBUTTON)) + setRecord = RecordGroup::Group1; + else if (m_initialDragRecordStatus[startChn] != RecordGroup::Group2 && (nFlags & MK_RBUTTON)) + setRecord = RecordGroup::Group2; + + if(startChn > endChn) + std::swap(startChn, endChn); + + CHANNELINDEX numChannels = std::min(modDoc->GetNumChannels(), static_cast<CHANNELINDEX>(m_initialDragRecordStatus.size())); + for(CHANNELINDEX chn = 0; chn < numChannels; chn++) + { + auto oldState = modDoc->GetChannelRecordGroup(chn); + if(chn >= startChn && chn <= endChn) + GetDocument()->SetChannelRecordGroup(chn, setRecord); + else + GetDocument()->SetChannelRecordGroup(chn, m_initialDragRecordStatus[chn]); + if(oldState != modDoc->GetChannelRecordGroup(chn)) + InvalidateChannelsHeaders(chn); + } + } else + { + // Dragging around channel headers? Update move indicator... + if(m_nDropItem.Type() == DragItem::ChannelHeader) + InvalidateRect(&m_rcDropItem, FALSE); + if(oldDropItem.Type() == DragItem::ChannelHeader) + InvalidateRect(&oldDropRect, FALSE); + } + + UpdateWindow(); + } + } + + if(m_Status[psChannelSelection]) + { + // Double-clicked a pattern cell to select whole channel. + // Continue dragging to select more channels. + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile->Patterns.IsValidPat(m_nPattern)) + { + const ROWINDEX lastRow = pSndFile->Patterns[m_nPattern].GetNumRows() - 1; + + CHANNELINDEX startChannel = m_Cursor.GetChannel(); + CHANNELINDEX endChannel = GetPositionFromPoint(point).GetChannel(); + + m_StartSel = PatternCursor(0, startChannel, (startChannel <= endChannel ? PatternCursor::firstColumn : PatternCursor::lastColumn)); + PatternCursor endSel = PatternCursor(lastRow, endChannel, (startChannel <= endChannel ? PatternCursor::lastColumn : PatternCursor::firstColumn)); + + DragToSel(endSel, true, false, false); + } + } else if(m_Status[psRowSelection] && point.y > m_szHeader.cy) + { + // Mark row number => mark whole row (continue) + InvalidateSelection(); + + PatternCursor cursor(GetPositionFromPoint(point)); + cursor.SetColumn(GetDocument()->GetNumChannels() - 1, PatternCursor::lastColumn); + DragToSel(cursor, false, true, false); + + } else if(m_Status[psMouseDragSelect]) + { + PatternCursor cursor(GetPositionFromPoint(point)); + + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && m_nPattern < pSndFile->Patterns.Size()) + { + ROWINDEX row = cursor.GetRow(); + LimitMax(row, pSndFile->Patterns[m_nPattern].GetNumRows() - 1); + cursor.SetRow(row); + } + + // Drag & Drop editing + if(m_Status[psDragnDropEdit]) + { + const bool moved = m_DragPos.GetChannel() != cursor.GetChannel() || m_DragPos.GetRow() != cursor.GetRow(); + + if(!m_Status[psDragnDropping]) + { + SetCursor(CMainFrame::curDragging); + } + if(!m_Status[psDragnDropping] || moved) + { + if(m_Status[psDragnDropping]) + OnDrawDragSel(); + m_Status.reset(psDragnDropping); + DragToSel(cursor, true, true, true); + m_DragPos = cursor; + m_Status.set(psDragnDropping); + OnDrawDragSel(); + } + } else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CENTERROW) + { + // Default: selection + DragToSel(cursor, true, true); + } else + { + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(cursor); + } + } +} + + +void CViewPattern::OnEditSelectAll() +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + SetCurSel(PatternCursor(0), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn)); + } +} + + +void CViewPattern::OnEditSelectChannel() +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + SetCurSel(PatternCursor(0, m_MenuCursor.GetChannel()), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, m_MenuCursor.GetChannel(), PatternCursor::lastColumn)); + } +} + + +void CViewPattern::OnSelectCurrentChannel() +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + PatternCursor beginSel(0, GetCurrentChannel()); + PatternCursor endSel(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, GetCurrentChannel(), PatternCursor::lastColumn); + // If column is already selected, select the current pattern + if((beginSel == m_Selection.GetUpperLeft()) && (endSel == m_Selection.GetLowerRight())) + { + beginSel.Set(0, 0); + endSel.Set(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn); + } + SetCurSel(beginSel, endSel); + } +} + + +void CViewPattern::OnSelectCurrentColumn() +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + SetCurSel(PatternCursor(0, m_Cursor), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, m_Cursor)); + } +} + + +void CViewPattern::OnChannelReset() +{ + ResetChannel(m_MenuCursor.GetChannel()); +} + + +// Reset all channel variables +void CViewPattern::ResetChannel(CHANNELINDEX chn) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + CriticalSection cs; + if(!pModDoc->IsChannelMuted(chn)) + { + // Cut playing notes + sndFile.ChnSettings[chn].dwFlags.set(CHN_MUTE); + pModDoc->UpdateChannelMuteStatus(chn); + sndFile.ChnSettings[chn].dwFlags.reset(CHN_MUTE); + } + sndFile.m_PlayState.Chn[chn].Reset(ModChannel::resetTotal, sndFile, chn, CSoundFile::GetChannelMuteFlag()); +} + + +void CViewPattern::OnMuteFromClick() +{ + OnMuteChannel(m_MenuCursor.GetChannel()); +} + + +void CViewPattern::OnMuteChannel(CHANNELINDEX chn) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + pModDoc->SoloChannel(chn, false); + pModDoc->MuteChannel(chn, !pModDoc->IsChannelMuted(chn)); + + //If we just unmuted a channel, make sure none are still considered "solo". + if(!pModDoc->IsChannelMuted(chn)) + { + for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++) + { + pModDoc->SoloChannel(i, false); + } + } + + InvalidateChannelsHeaders(); + pModDoc->UpdateAllViews(this, GeneralHint(chn).Channels()); + } +} + + +void CViewPattern::OnSoloFromClick() +{ + OnSoloChannel(m_MenuCursor.GetChannel()); +} + + +// When trying to solo a channel that is already the only unmuted channel, +// this will result in unmuting all channels, in order to satisfy user habits. +// In all other cases, soloing a channel unsoloes all and mutes all except this channel +void CViewPattern::OnSoloChannel(CHANNELINDEX chn) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + + if(chn >= pModDoc->GetNumChannels()) + { + return; + } + + if(pModDoc->IsChannelSolo(chn)) + { + bool nChnIsOnlyUnMutedChan = true; + for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++) //check status of all other chans + { + if(i != chn && !pModDoc->IsChannelMuted(i)) + { + nChnIsOnlyUnMutedChan = false; //found a channel that isn't muted! + break; + } + } + if(nChnIsOnlyUnMutedChan) // this is the only playable channel and it is already soloed -> Unmute all + { + OnUnmuteAll(); + return; + } + } + for(CHANNELINDEX i = 0; i < pModDoc->GetNumChannels(); i++) + { + pModDoc->MuteChannel(i, !(i == chn)); //mute all chans except nChn, unmute nChn + pModDoc->SoloChannel(i, (i == chn)); //unsolo all chans except nChn, solo nChn + } + InvalidateChannelsHeaders(); + pModDoc->UpdateAllViews(this, GeneralHint(chn).Channels()); +} + + +void CViewPattern::OnRecordSelect() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + CHANNELINDEX chn = m_MenuCursor.GetChannel(); + if(chn < pModDoc->GetNumChannels()) + { + pModDoc->ToggleChannelRecordGroup(chn, RecordGroup::Group1); + InvalidateChannelsHeaders(chn); + } + } +} + + +void CViewPattern::OnSplitRecordSelect() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + CHANNELINDEX chn = m_MenuCursor.GetChannel(); + if(chn < pModDoc->GetNumChannels()) + { + pModDoc->ToggleChannelRecordGroup(chn, RecordGroup::Group2); + InvalidateChannelsHeaders(chn); + } + } +} + + +void CViewPattern::OnUnmuteAll() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + const CHANNELINDEX numChannels = pModDoc->GetNumChannels(); + for(CHANNELINDEX chn = 0; chn < numChannels; chn++) + { + pModDoc->MuteChannel(chn, false); + pModDoc->SoloChannel(chn, false); + } + InvalidateChannelsHeaders(); + } +} + + +bool CViewPattern::InsertOrDeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit, bool deleteRows) +{ + CModDoc &modDoc = *GetDocument(); + CSoundFile &sndFile = *GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled_bmsg()) + return false; + + LimitMax(lastChn, CHANNELINDEX(sndFile.GetNumChannels() - 1)); + if(firstChn > lastChn) + return false; + + const auto selection = (firstChn != lastChn || m_Selection.GetNumRows() > 1) ? PatternRect{{m_Selection.GetStartRow(), firstChn, PatternCursor::firstColumn}, {m_Selection.GetEndRow(), lastChn, PatternCursor::lastColumn}} : m_Selection; + const ROWINDEX numRows = selection.GetNumRows(); + + const char *undoDescription = ""; + if(deleteRows) + undoDescription = numRows != 1 ? "Delete Rows" : "Delete Row"; + else + undoDescription = numRows != 1 ? "Insert Rows" : "Insert Row"; + + const ROWINDEX startRow = selection.GetStartRow(); + const CHANNELINDEX numChannels = lastChn - firstChn + 1; + + std::vector<PATTERNINDEX> patterns; + if(globalEdit) + { + auto &order = Order(); + const auto start = order.begin() + GetCurrentOrder(); + const auto end = std::find(start, order.end(), order.GetInvalidPatIndex()); + + // As this is a global operation, ensure that all modified patterns are unique + bool orderListChanged = false; + const ORDERINDEX ordEnd = GetCurrentOrder() + static_cast<ORDERINDEX>(std::distance(start, end)); + for(ORDERINDEX ord = GetCurrentOrder(); ord < ordEnd; ord++) + { + const auto pat = order[ord]; + if(pat != order.EnsureUnique(ord)) + orderListChanged = true; + } + if(orderListChanged) + modDoc.UpdateAllViews(this, SequenceHint().Data(), nullptr); + + patterns.assign(start, end); + } else + { + patterns = {m_nPattern}; + } + + // Backup source data and create undo points + std::vector<ModCommand> patternData; + if(!deleteRows) + patternData.insert(patternData.begin(), numRows * numChannels, ModCommand{}); + + bool first = true; + for(auto pat : patterns) + { + if(!sndFile.Patterns.IsValidPat(pat)) + continue; + const auto &pattern = sndFile.Patterns[pat]; + const ROWINDEX firstRow = first ? startRow : 0; + for(ROWINDEX row = firstRow; row < pattern.GetNumRows(); row++) + { + const auto *m = pattern.GetpModCommand(row, firstChn); + patternData.insert(patternData.end(), m, m + numChannels); + } + modDoc.GetPatternUndo().PrepareUndo(pat, firstChn, firstRow, numChannels, pattern.GetNumRows(), undoDescription, !first); + first = false; + } + + if(deleteRows) + patternData.insert(patternData.end(), numRows * numChannels, ModCommand{}); + + // Now do the actual shifting + auto src = patternData.cbegin(); + if(deleteRows) + src += numRows * numChannels; + + PATTERNINDEX firstNewPattern = m_nPattern; + first = true; + for(auto pat : patterns) + { + if(!sndFile.Patterns.IsValidPat(pat)) + continue; + auto &pattern = sndFile.Patterns[pat]; + for(ROWINDEX row = first ? startRow : 0; row < pattern.GetNumRows(); row++, src += numChannels) + { + ModCommand *dest = pattern.GetpModCommand(row, firstChn); + std::copy(src, src + numChannels, dest); + } + if(first) + firstNewPattern = pat; + first = false; + modDoc.UpdateAllViews(this, PatternHint(pat).Data(), this); + } + + SetModified(); + SetCurrentPattern(firstNewPattern); + InvalidatePattern(); + + SetCursorPosition(selection.GetUpperLeft()); + SetCurSel(selection); + + return true; +} + + +void CViewPattern::DeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit) +{ + InsertOrDeleteRows(firstChn, lastChn, globalEdit, true); +} + + +void CViewPattern::OnDeleteRow() +{ + DeleteRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel()); +} + + +void CViewPattern::OnDeleteWholeRow() +{ + DeleteRows(0, GetSoundFile()->GetNumChannels() - 1); +} + + +void CViewPattern::OnDeleteRowGlobal() +{ + DeleteRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel(), true); +} + + +void CViewPattern::OnDeleteWholeRowGlobal() +{ + DeleteRows(0, GetSoundFile()->GetNumChannels() - 1, true); +} + + +void CViewPattern::InsertRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit) +{ + InsertOrDeleteRows(firstChn, lastChn, globalEdit, false); +} + + +void CViewPattern::OnInsertRow() +{ + InsertRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel()); +} + + +void CViewPattern::OnInsertWholeRow() +{ + InsertRows(0, GetSoundFile()->GetNumChannels() - 1); +} + + +void CViewPattern::OnInsertRowGlobal() +{ + InsertRows(m_Selection.GetStartChannel(), m_Selection.GetEndChannel(), true); +} + + +void CViewPattern::OnInsertWholeRowGlobal() +{ + InsertRows(0, GetSoundFile()->GetNumChannels() - 1, true); +} + +void CViewPattern::OnSplitPattern() +{ + COrderList &orderList = static_cast<CCtrlPatterns *>(GetControlDlg())->GetOrderList(); + CSoundFile &sndFile = *GetSoundFile(); + const auto &specs = sndFile.GetModSpecifications(); + const PATTERNINDEX sourcePat = m_nPattern; + const ROWINDEX splitRow = m_MenuCursor.GetRow(); + if(splitRow < 1 || !sndFile.Patterns.IsValidPat(sourcePat) || !sndFile.Patterns[sourcePat].IsValidRow(splitRow)) + { + MessageBeep(MB_ICONWARNING); + return; + } + + // Create a new pattern (ignore if it's too big for this format - if it is, then the source pattern already was too big, too) + CriticalSection cs; + const ROWINDEX numSplitRows = sndFile.Patterns[sourcePat].GetNumRows() - splitRow; + const PATTERNINDEX newPat = sndFile.Patterns.InsertAny(std::max(specs.patternRowsMin, numSplitRows), false); + if(newPat == PATTERNINDEX_INVALID) + { + cs.Leave(); + Reporting::Error(MPT_AFORMAT("Pattern limit of the {} format ({} patterns) has been reached.")(mpt::ToUpperCaseAscii(specs.fileExtension), specs.patternsMax), "Split Pattern"); + return; + } + auto &sourcePattern = sndFile.Patterns[sourcePat]; + auto &newPattern = sndFile.Patterns[newPat]; + + auto &undo = GetDocument()->GetPatternUndo(); + undo.PrepareUndo(sourcePat, 0, splitRow, sourcePattern.GetNumChannels(), numSplitRows, "Split Pattern"); + undo.PrepareUndo(newPat, 0, 0, newPattern.GetNumChannels(), newPattern.GetNumRows(), "Split Pattern", true); + + auto copyStart = sourcePattern.begin() + sourcePattern.GetNumChannels() * splitRow; + std::copy(copyStart, sourcePattern.end(), newPattern.begin()); + + // Reduce the row number or insert pattern breaks, if the patterns are too small for the format + sourcePattern.Resize(std::max(specs.patternRowsMin, splitRow)); + if(splitRow != sourcePattern.GetNumRows()) + { + std::fill(copyStart, sourcePattern.end(), ModCommand::Empty()); + sourcePattern.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(splitRow - 1).RetryNextRow()); + } + if(numSplitRows != newPattern.GetNumRows()) + { + newPattern.WriteEffect(EffectWriter(CMD_PATTERNBREAK, 0).Row(numSplitRows - 1).RetryNextRow()); + } + + // Update every occurrence of the split pattern in all order lists + auto editOrd = GetCurrentOrder(); + for(SEQUENCEINDEX seq = 0; seq < sndFile.Order.GetNumSequences(); seq++) + { + const bool isCurrentSeq = (seq == sndFile.Order.GetCurrentSequenceIndex()); + bool editedSeq = false; + auto &order = sndFile.Order(seq); + for(ORDERINDEX i = 0; i < order.GetLength(); i++) + { + if(order[i] == sourcePat) + { + if(!order.insert(i + 1, 1, newPat)) + continue; + editedSeq = true; + if(isCurrentSeq) + orderList.InsertUpdatePlaystate(i, i + 1); + i++; + + // Slide the current selection accordingly so it doesn't end up in the wrong id + if(i < editOrd && isCurrentSeq) + editOrd++; + } + } + if(editedSeq) + GetDocument()->UpdateAllViews(nullptr, SequenceHint(seq).Data(), this); + } + + orderList.SetSelection(editOrd + 1); + SetCurrentRow(0); + + SetModified(true); + GetDocument()->UpdateAllViews(nullptr, PatternHint(newPat).Names().Data(), this); +} + + +void CViewPattern::OnEditGoto() +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + + ORDERINDEX curOrder = GetCurrentOrder(); + CHANNELINDEX curChannel = GetCurrentChannel() + 1; + CPatternGotoDialog dlg(this, GetCurrentRow(), curChannel, m_nPattern, curOrder, pModDoc->GetSoundFile()); + + if(dlg.DoModal() == IDOK) + { + if(dlg.m_nPattern != m_nPattern) + SetCurrentPattern(dlg.m_nPattern); + if(dlg.m_nOrder != curOrder) + SetCurrentOrder(dlg.m_nOrder); + if(dlg.m_nChannel != curChannel) + SetCurrentColumn(dlg.m_nChannel - 1); + if(dlg.m_nRow != GetCurrentRow()) + SetCurrentRow(dlg.m_nRow); + CriticalSection cs; + pModDoc->SetElapsedTime(dlg.m_nOrder, dlg.m_nRow, false); + } + return; +} + + +void CViewPattern::OnPatternStep() +{ + PatternStep(); +} + + +void CViewPattern::PatternStep(ROWINDEX row) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + + if(pMainFrm != nullptr && pModDoc != nullptr) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + return; + + CriticalSection cs; + + // In case we were previously in smooth scrolling mode during live playback, the pattern might be misaligned. + if(GetSmoothScrollOffset() != 0) + InvalidatePattern(true, true); + + // Cut instruments/samples in virtual channels + for(CHANNELINDEX i = sndFile.GetNumChannels(); i < MAX_CHANNELS; i++) + { + sndFile.m_PlayState.Chn[i].dwFlags.set(CHN_NOTEFADE | CHN_KEYOFF); + } + sndFile.LoopPattern(m_nPattern); + sndFile.m_PlayState.m_nNextRow = row == ROWINDEX_INVALID ? GetCurrentRow() : row; + sndFile.m_SongFlags.reset(SONG_PAUSED); + sndFile.m_SongFlags.set(SONG_STEP); + + SetPlayCursor(m_nPattern, sndFile.m_PlayState.m_nNextRow, 0); + cs.Leave(); + + if(pMainFrm->GetModPlaying() != pModDoc) + { + pModDoc->SetFollowWnd(m_hWnd); + pMainFrm->PlayMod(pModDoc); + } + pModDoc->SetNotifications(Notification::Position | Notification::VUMeters); + if(row == ROWINDEX_INVALID) + { + SetCurrentRow(GetCurrentRow() + 1, + (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) || // Wrap around to next pattern if continous scroll is enabled... + (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP)); // ...or otherwise if cursor wrap is enabled. + } + SetFocus(); + } +} + + +// Copy cursor to internal clipboard +void CViewPattern::OnCursorCopy() +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return; + } + + const ModCommand &m = GetCursorCommand(); + switch(m_Cursor.GetColumnType()) + { + case PatternCursor::noteColumn: + case PatternCursor::instrColumn: + m_cmdOld.note = m.note; + m_cmdOld.instr = m.instr; + SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, m_cmdOld.instr); + break; + + case PatternCursor::volumeColumn: + m_cmdOld.volcmd = m.volcmd; + m_cmdOld.vol = m.vol; + break; + + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + m_cmdOld.command = m.command; + m_cmdOld.param = m.param; + break; + } +} + + +// Paste cursor from internal clipboard +void CViewPattern::OnCursorPaste() +{ + if(!IsEditingEnabled_bmsg()) + { + return; + } + + PrepareUndo(m_Cursor, m_Cursor, "Cursor Paste"); + PatternCursor::Columns column = m_Cursor.GetColumnType(); + + ModCommand &m = GetCursorCommand(); + + switch(column) + { + case PatternCursor::noteColumn: + m.note = m_cmdOld.note; + [[fallthrough]]; + case PatternCursor::instrColumn: + m.instr = m_cmdOld.instr; + break; + + case PatternCursor::volumeColumn: + m.vol = m_cmdOld.vol; + m.volcmd = m_cmdOld.volcmd; + break; + + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + m.command = m_cmdOld.command; + m.param = m_cmdOld.param; + break; + } + + SetModified(false); + // Preview Row + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !IsLiveRecord()) + { + PatternStep(GetCurrentRow()); + } + + if(GetSoundFile()->IsPaused() || !m_Status[psFollowSong] || (CMainFrame::GetMainFrame() && CMainFrame::GetMainFrame()->GetFollowSong(GetDocument()) != m_hWnd)) + { + InvalidateCell(m_Cursor); + SetCurrentRow(GetCurrentRow() + m_nSpacing); + SetSelToCursor(); + } +} + + +void CViewPattern::OnVisualizeEffect() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc != nullptr && pModDoc->GetSoundFile().Patterns.IsValidPat(m_nPattern)) + { + const ROWINDEX row0 = m_Selection.GetStartRow(), row1 = m_Selection.GetEndRow(); + const CHANNELINDEX nchn = m_Selection.GetStartChannel(); + if(m_pEffectVis) + { + // Window already there, update data + m_pEffectVis->UpdateSelection(row0, row1, nchn, m_nPattern); + } else + { + // Open window & send data + CriticalSection cs; + try + { + m_pEffectVis = std::make_unique<CEffectVis>(this, row0, row1, nchn, *pModDoc, m_nPattern); + m_pEffectVis->OpenEditor(CMainFrame::GetMainFrame()); + // HACK: to get status window set up; must create clear destinction between + // construction, 1st draw code and all draw code. + m_pEffectVis->OnSize(0, 0, 0); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + } +} + + +// Helper function for sweeping the pattern up and down to find suitable start and end points for interpolation. +// startCond must return true for the start row, endCond must return true for the end row. +PatternRect CViewPattern::SweepPattern(bool(*startCond)(const ModCommand &), bool(*endCond)(const ModCommand &, const ModCommand &)) const +{ + const auto &pattern = GetSoundFile()->Patterns[m_nPattern]; + const ROWINDEX numRows = pattern.GetNumRows(); + const ROWINDEX cursorRow = m_Selection.GetStartRow(); + if(cursorRow >= numRows) + return {}; + + const ModCommand *start = pattern.GetpModCommand(cursorRow, m_Selection.GetStartChannel()), *end = start; + + // Sweep up + ROWINDEX startRow = ROWINDEX_INVALID; + for(ROWINDEX row = 0; row <= cursorRow; row++, start -= pattern.GetNumChannels()) + { + if(startCond(*start)) + { + startRow = cursorRow - row; + break; + } + } + if(startRow == ROWINDEX_INVALID) + return {}; + + // Sweep down + ROWINDEX endRow = ROWINDEX_INVALID; + for(ROWINDEX row = cursorRow; row < numRows; row++, end += pattern.GetNumChannels()) + { + if(endCond(*start, *end)) + { + endRow = row; + break; + } + } + + if(endRow == ROWINDEX_INVALID) + return {}; + + return {PatternCursor(startRow, m_Selection.GetUpperLeft()), PatternCursor(endRow, m_Selection.GetUpperLeft())}; +} + + +void CViewPattern::Interpolate(PatternCursor::Columns type) +{ + CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern) || !IsEditingEnabled()) + return; + + bool changed = false; + std::vector<CHANNELINDEX> validChans; + + if(type == PatternCursor::effectColumn || type == PatternCursor::paramColumn) + { + std::vector<CHANNELINDEX> effectChans; + std::vector<CHANNELINDEX> paramChans; + ListChansWhereColSelected(PatternCursor::effectColumn, effectChans); + ListChansWhereColSelected(PatternCursor::paramColumn, paramChans); + + validChans.resize(effectChans.size() + paramChans.size()); + validChans.resize(std::set_union(effectChans.begin(), effectChans.end(), paramChans.begin(), paramChans.end(), validChans.begin()) - validChans.begin()); + } else + { + ListChansWhereColSelected(type, validChans); + } + + if(m_Selection.GetUpperLeft() == m_Selection.GetLowerRight() && !validChans.empty()) + { + // No selection has been made: Interpolate between closest non-zero values in this column. + PatternRect sweepSelection; + + switch(type) + { + case PatternCursor::noteColumn: + // Allow note-to-note interpolation only. + sweepSelection = SweepPattern( + [](const ModCommand &start) { return start.note != NOTE_NONE; }, + [](const ModCommand &start, const ModCommand &end) { return start.IsNote() && end.IsNote(); }); + break; + case PatternCursor::instrColumn: + // Allow interpolation between same instrument, as long as it's not a PC note. + sweepSelection = SweepPattern( + [](const ModCommand &start) { return start.instr != 0 && !start.IsPcNote(); }, + [](const ModCommand &start, const ModCommand &end) { return end.instr == start.instr; }); + break; + case PatternCursor::volumeColumn: + // Allow interpolation between same volume effect, as long as it's not a PC note. + sweepSelection = SweepPattern( + [](const ModCommand &start) { return start.volcmd != VOLCMD_NONE && !start.IsPcNote(); }, + [](const ModCommand &start, const ModCommand &end) { return end.volcmd == start.volcmd && !end.IsPcNote(); }); + break; + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + // Allow interpolation between same effect, or anything if it's a PC note. + sweepSelection = SweepPattern( + [](const ModCommand &start) { return start.command != CMD_NONE || start.IsPcNote(); }, + [](const ModCommand &start, const ModCommand &end) { return (end.command == start.command || start.IsPcNote()) && (!start.IsPcNote() || end.IsPcNote()); }); + break; + } + + if(sweepSelection.GetNumRows() > 1) + { + // Found usable end and start commands: Extend selection. + SetCurSel(sweepSelection); + } + } + + const ROWINDEX row0 = m_Selection.GetStartRow(), row1 = m_Selection.GetEndRow(); + + //for all channels where type is selected + for(auto nchn : validChans) + { + if(!IsInterpolationPossible(row0, row1, nchn, type)) + continue; //skip chans where interpolation isn't possible + + if(!changed) //ensure we save undo buffer only before any channels are interpolated + { + const char *description = ""; + switch(type) + { + case PatternCursor::noteColumn: + description = "Interpolate Note Column"; + break; + case PatternCursor::instrColumn: + description = "Interpolate Instrument Column"; + break; + case PatternCursor::volumeColumn: + description = "Interpolate Volume Column"; + break; + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + description = "Interpolate Effect Column"; + break; + } + PrepareUndo(m_Selection, description); + } + + bool doPCinterpolation = false; + + int vsrc, vdest, vcmd = 0, verr = 0, distance = row1 - row0; + + const ModCommand srcCmd = *sndFile->Patterns[m_nPattern].GetpModCommand(row0, nchn); + const ModCommand destCmd = *sndFile->Patterns[m_nPattern].GetpModCommand(row1, nchn); + + ModCommand::NOTE PCnote = NOTE_NONE; + uint16 PCinst = 0, PCparam = 0; + + switch(type) + { + case PatternCursor::noteColumn: + vsrc = srcCmd.note; + vdest = destCmd.note; + vcmd = srcCmd.instr; + verr = (distance * (NOTE_MAX - 1)) / NOTE_MAX; + if(srcCmd.note == NOTE_NONE) + { + vsrc = vdest; + vcmd = destCmd.note; + } else if(destCmd.note == NOTE_NONE) + { + vdest = vsrc; + } + break; + + case PatternCursor::instrColumn: + vsrc = srcCmd.instr; + vdest = destCmd.instr; + verr = (distance * 63) / 128; + if(srcCmd.instr == 0) + { + vsrc = vdest; + vcmd = destCmd.instr; + } else if(destCmd.instr == 0) + { + vdest = vsrc; + } + break; + + case PatternCursor::volumeColumn: + vsrc = srcCmd.vol; + vdest = destCmd.vol; + vcmd = srcCmd.volcmd; + verr = (distance * 63) / 128; + if(srcCmd.volcmd == VOLCMD_NONE) + { + vcmd = destCmd.volcmd; + if(vcmd == VOLCMD_VOLUME && srcCmd.IsNote() && srcCmd.instr) + vsrc = GetDefaultVolume(srcCmd); + else + vsrc = vdest; + } else if(destCmd.volcmd == VOLCMD_NONE) + { + if(vcmd == VOLCMD_VOLUME && destCmd.IsNote() && destCmd.instr) + vdest = GetDefaultVolume(srcCmd); + else + vdest = vsrc; + } + break; + + case PatternCursor::paramColumn: + case PatternCursor::effectColumn: + if(srcCmd.IsPcNote() || destCmd.IsPcNote()) + { + doPCinterpolation = true; + PCnote = (srcCmd.IsPcNote()) ? srcCmd.note : destCmd.note; + vsrc = srcCmd.GetValueEffectCol(); + vdest = destCmd.GetValueEffectCol(); + PCparam = srcCmd.GetValueVolCol(); + if((PCparam == 0 && destCmd.IsPcNote()) || !srcCmd.IsPcNote()) + PCparam = destCmd.GetValueVolCol(); + PCinst = srcCmd.instr; + if(PCinst == 0) + PCinst = destCmd.instr; + } else + { + vsrc = srcCmd.param; + vdest = destCmd.param; + vcmd = srcCmd.command; + if(srcCmd.command == CMD_NONE) + { + vsrc = vdest; + vcmd = destCmd.command; + } else if(destCmd.command == CMD_NONE) + { + vdest = vsrc; + } + } + verr = (distance * 63) / 128; + break; + + default: + MPT_ASSERT(false); + return; + } + + if(vdest < vsrc) + verr = -verr; + + ModCommand *pcmd = sndFile->Patterns[m_nPattern].GetpModCommand(row0, nchn); + + for(int i = 0; i <= distance; i++, pcmd += sndFile->GetNumChannels()) + { + switch(type) + { + case PatternCursor::noteColumn: + if((pcmd->note == NOTE_NONE || pcmd->instr == vcmd) && !pcmd->IsPcNote()) + { + int note = vsrc + ((vdest - vsrc) * i + verr) / distance; + pcmd->note = static_cast<ModCommand::NOTE>(note); + if(pcmd->instr == 0) + pcmd->instr = static_cast<ModCommand::VOLCMD>(vcmd); + } + break; + + case PatternCursor::instrColumn: + if(pcmd->instr == 0) + { + int instr = vsrc + ((vdest - vsrc) * i + verr) / distance; + pcmd->instr = static_cast<ModCommand::INSTR>(instr); + } + break; + + case PatternCursor::volumeColumn: + if((pcmd->volcmd == VOLCMD_NONE || pcmd->volcmd == vcmd) && !pcmd->IsPcNote()) + { + int vol = vsrc + ((vdest - vsrc) * i + verr) / distance; + pcmd->vol = static_cast<ModCommand::VOL>(vol); + pcmd->volcmd = static_cast<ModCommand::VOLCMD>(vcmd); + } + break; + + case PatternCursor::effectColumn: + if(doPCinterpolation) + { // With PC/PCs notes, copy PCs note and plug index to all rows where + // effect interpolation is done if no PC note with non-zero instrument is there. + const uint16 val = static_cast<uint16>(vsrc + ((vdest - vsrc) * i + verr) / distance); + if(!pcmd->IsPcNote() || pcmd->instr == 0) + { + pcmd->note = PCnote; + pcmd->instr = static_cast<ModCommand::INSTR>(PCinst); + } + pcmd->SetValueVolCol(PCparam); + pcmd->SetValueEffectCol(val); + } else if(!pcmd->IsPcNote()) + { + if((pcmd->command == CMD_NONE) || (pcmd->command == vcmd)) + { + int val = vsrc + ((vdest - vsrc) * i + verr) / distance; + pcmd->param = static_cast<ModCommand::PARAM>(val); + pcmd->command = static_cast<ModCommand::COMMAND>(vcmd); + } + } + break; + + default: + MPT_ASSERT(false); + } + } + + changed = true; + + } //end for all channels where type is selected + + if(changed) + { + SetModified(false); + InvalidatePattern(false); + } +} + + +void CViewPattern::OnResetChannelColors() +{ + CModDoc &modDoc = *GetDocument(); + const CSoundFile &sndFile = *GetSoundFile(); + modDoc.GetPatternUndo().PrepareChannelUndo(0, sndFile.GetNumChannels(), "Reset Channel Colours"); + if(modDoc.SetDefaultChannelColors()) + { + if(modDoc.SupportsChannelColors()) + modDoc.SetModified(); + modDoc.UpdateAllViews(nullptr, GeneralHint().Channels(), nullptr); + } else + { + modDoc.GetPatternUndo().RemoveLastUndoStep(); + } +} + + +void CViewPattern::OnTransposeChannel() +{ + CInputDlg dlg(this, _T("Enter transpose amount (affects all patterns):"), -(NOTE_MAX - NOTE_MIN), (NOTE_MAX - NOTE_MIN), m_nTransposeAmount); + if(dlg.DoModal() == IDOK) + { + m_nTransposeAmount = dlg.resultAsInt; + + CSoundFile &sndFile = *GetSoundFile(); + bool changed = false; + // Don't allow notes outside our supported note range. + const ModCommand::NOTE noteMin = sndFile.GetModSpecifications().noteMin; + const ModCommand::NOTE noteMax = sndFile.GetModSpecifications().noteMax; + + for(PATTERNINDEX pat = 0; pat < sndFile.Patterns.Size(); pat++) + { + bool changedThisPat = false; + if(sndFile.Patterns.IsValidPat(pat)) + { + ModCommand *m = sndFile.Patterns[pat].GetpModCommand(0, m_MenuCursor.GetChannel()); + const ROWINDEX numRows = sndFile.Patterns[pat].GetNumRows(); + for(ROWINDEX row = 0; row < numRows; row++) + { + if(m->IsNote()) + { + if(!changedThisPat) + { + GetDocument()->GetPatternUndo().PrepareUndo(pat, m_MenuCursor.GetChannel(), 0, 1, numRows, "Transpose Channel", changed); + changed = changedThisPat = true; + } + int note = m->note + m_nTransposeAmount; + Limit(note, noteMin, noteMax); + m->note = static_cast<ModCommand::NOTE>(note); + } + m += sndFile.Patterns[pat].GetNumChannels(); + } + } + } + if(changed) + { + SetModified(true); + InvalidatePattern(false); + } + } +} + + +void CViewPattern::OnTransposeCustom() +{ + CInputDlg dlg(this, _T("Enter transpose amount:"), -(NOTE_MAX - NOTE_MIN), (NOTE_MAX - NOTE_MIN), m_nTransposeAmount); + if(dlg.DoModal() == IDOK) + { + m_nTransposeAmount = dlg.resultAsInt; + TransposeSelection(dlg.resultAsInt); + } +} + + +void CViewPattern::OnTransposeCustomQuick() +{ + if(m_nTransposeAmount != 0) + TransposeSelection(m_nTransposeAmount); + else + OnTransposeCustom(); +} + + +bool CViewPattern::TransposeSelection(int transp) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return false; + } + + m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + + // Don't allow notes outside our supported note range. + const ModCommand::NOTE noteMin = pSndFile->GetModSpecifications().noteMin; + const ModCommand::NOTE noteMax = pSndFile->GetModSpecifications().noteMax; + + PrepareUndo(m_Selection, "Transpose"); + + std::vector<int> lastGroupSize(pSndFile->GetNumChannels(), 12); + ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn) + { + if(chn == m_Selection.GetStartChannel() && m_Selection.GetStartColumn() > PatternCursor::noteColumn) + return; + + if(m.IsNote()) + { + if(m.instr > 0) + { + lastGroupSize[chn] = GetDocument()->GetInstrumentGroupSize(m.instr); + } + int transpose = transp; + if(transpose == 12000 || transpose == -12000) + { + // Transpose one octave + transpose = lastGroupSize[chn] * mpt::signum(transpose); + } + int note = m.note + transpose; + Limit(note, noteMin, noteMax); + m.note = static_cast<ModCommand::NOTE>(note); + } + }); + SetModified(false); + InvalidateSelection(); + + if(m_Selection.GetNumChannels() == 1 && m_Selection.GetNumRows() == 1 && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYTRANSPOSE)) + { + // Preview a single transposed note + PreviewNote(m_Selection.GetStartRow(), m_Selection.GetStartChannel()); + } + + return true; +} + + +bool CViewPattern::DataEntry(bool up, bool coarse) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return false; + } + + m_Selection.Sanitize(pSndFile->Patterns[m_nPattern].GetNumRows(), pSndFile->GetNumChannels()); + + const PatternCursor::Columns column = m_Selection.GetStartColumn(); + + // Don't allow notes outside our supported note range. + const ModCommand::NOTE noteMin = pSndFile->GetModSpecifications().noteMin; + const ModCommand::NOTE noteMax = pSndFile->GetModSpecifications().noteMax; + const int instrMax = std::min(static_cast<int>(Util::MaxValueOfType(ModCommand::INSTR())), static_cast<int>(pSndFile->GetNumInstruments() ? pSndFile->GetNumInstruments() : pSndFile->GetNumSamples())); + const EffectInfo effectInfo(*pSndFile); + const int offset = up ? 1 : -1; + + PrepareUndo(m_Selection, "Data Entry"); + + // Notes per octave for non-TET12 tunings and coarse note steps + std::vector<int> lastGroupSize(pSndFile->GetNumChannels(), 12); + + bool applyToSpecialNotes = true; + if(column == PatternCursor::noteColumn) + { + const CPattern &pattern = pSndFile->Patterns[m_nPattern]; + const CHANNELINDEX startChn = m_Selection.GetStartChannel(), endChn = m_Selection.GetEndChannel(); + const ROWINDEX endRow = m_Selection.GetEndRow(); + for(ROWINDEX row = m_Selection.GetStartRow(); row <= endRow && applyToSpecialNotes; row++) + { + const ModCommand *m = pattern.GetpModCommand(row, startChn); + for(CHANNELINDEX chn = startChn; chn <= endChn; chn++, m++) + { + if(!m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::noteColumn))) + continue; + if(m->IsNote()) + { + applyToSpecialNotes = false; + break; + } + } + } + } + + ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn) + { + if(column == PatternCursor::noteColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::noteColumn))) + { + // Increase / decrease note + if(m.IsNote() && !applyToSpecialNotes) + { + if(m.instr > 0) + { + lastGroupSize[chn] = GetDocument()->GetInstrumentGroupSize(m.instr); + } + int note = m.note + offset * (coarse ? lastGroupSize[chn] : 1); + Limit(note, noteMin, noteMax); + m.note = (ModCommand::NOTE)note; + } else if(m.IsSpecialNote() && applyToSpecialNotes) + { + ModCommand::NOTE note = m.note; + do + { + note = static_cast<ModCommand::NOTE>(note + offset); + if(!ModCommand::IsSpecialNote(note)) + { + break; + } + } while(!pSndFile->GetModSpecifications().HasNote(note)); + if(ModCommand::IsSpecialNote(note)) + { + if(m.IsPcNote() != ModCommand::IsPcNote(note)) + { + m.Clear(); + } + m.note = (ModCommand::NOTE)note; + } + } + } + if(column == PatternCursor::instrColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::instrColumn)) && m.instr != 0) + { + // Increase / decrease instrument + int instr = m.instr + offset * (coarse ? 10 : 1); + Limit(instr, 1, m.IsInstrPlug() ? MAX_MIXPLUGINS : instrMax); + m.instr = (ModCommand::INSTR)instr; + } + if(column == PatternCursor::volumeColumn && m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::volumeColumn))) + { + // Increase / decrease volume parameter + if(m.IsPcNote()) + { + int val = m.GetValueVolCol() + offset * (coarse ? 10 : 1); + Limit(val, 0, ModCommand::maxColumnValue); + m.SetValueVolCol(static_cast<uint16>(val)); + } else + { + int vol = m.vol + offset * (coarse ? 10 : 1); + if(m.volcmd == VOLCMD_NONE && m.IsNote() && m.instr) + { + m.volcmd = VOLCMD_VOLUME; + vol = GetDefaultVolume(m); + } + ModCommand::VOL minValue = 0, maxValue = 64; + effectInfo.GetVolCmdInfo(effectInfo.GetIndexFromVolCmd(m.volcmd), nullptr, &minValue, &maxValue); + Limit(vol, (int)minValue, (int)maxValue); + m.vol = (ModCommand::VOL)vol; + } + } + if((column == PatternCursor::effectColumn || column == PatternCursor::paramColumn) && (m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::effectColumn)) || m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::paramColumn)))) + { + // Increase / decrease effect parameter + if(m.IsPcNote()) + { + int val = m.GetValueEffectCol() + offset * (coarse ? 10 : 1); + Limit(val, 0, ModCommand::maxColumnValue); + m.SetValueEffectCol(static_cast<uint16>(val)); + } else + { + int param = m.param + offset * (coarse ? 16 : 1); + ModCommand::PARAM minValue = 0x00, maxValue = 0xFF; + if(!m.IsSlideUpDownCommand()) + { + const auto effectIndex = effectInfo.GetIndexFromEffect(m.command, m.param); + effectInfo.GetEffectInfo(effectIndex, nullptr, false, &minValue, &maxValue); + minValue = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(effectIndex, minValue)); + maxValue = static_cast<ModCommand::PARAM>(effectInfo.MapPosToValue(effectIndex, maxValue)); + } + m.param = static_cast<ModCommand::PARAM>(Clamp(param, minValue, maxValue)); + } + } + }); + + SetModified(false); + InvalidatePattern(); + + if(column == PatternCursor::noteColumn && m_Selection.GetNumChannels() == 1 && m_Selection.GetNumRows() == 1 && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYTRANSPOSE)) + { + // Preview a single transposed note + PreviewNote(m_Selection.GetStartRow(), m_Selection.GetStartChannel()); + } + + return true; +} + + +// Get the velocity at which a given note would be played +int CViewPattern::GetDefaultVolume(const ModCommand &m, ModCommand::INSTR lastInstr) const +{ + const CSoundFile &sndFile = *GetSoundFile(); + SAMPLEINDEX sample = GetDocument()->GetSampleIndex(m, lastInstr); + if(sample) + return std::min(sndFile.GetSample(sample).nVolume, uint16(256)) / 4u; + else if(m.instr > 0 && m.instr <= sndFile.GetNumInstruments() && sndFile.Instruments[m.instr] != nullptr && sndFile.Instruments[m.instr]->HasValidMIDIChannel()) + return std::min(sndFile.Instruments[m.instr]->nGlobalVol, uint32(64)); // For instrument plugins + else + return 64; +} + + +int CViewPattern::GetBaseNote() const +{ + const CModDoc *modDoc = GetDocument(); + INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()); + if(!instr && !IsLiveRecord()) + instr = GetCursorCommand().instr; + return modDoc->GetBaseNote(instr); +} + + +ModCommand::NOTE CViewPattern::GetNoteWithBaseOctave(int note) const +{ + const CModDoc *modDoc = GetDocument(); + INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()); + if(!instr && !IsLiveRecord()) + instr = GetCursorCommand().instr; + return modDoc->GetNoteWithBaseOctave(note, instr); +} + + +void CViewPattern::OnDropSelection() +{ + CModDoc *pModDoc; + if((pModDoc = GetDocument()) == nullptr || !IsEditingEnabled_bmsg()) + { + return; + } + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + { + return; + } + + // Compute relative movement + int dx = (int)m_DragPos.GetChannel() - (int)m_StartSel.GetChannel(); + int dy = (int)m_DragPos.GetRow() - (int)m_StartSel.GetRow(); + if((!dx) && (!dy)) + { + return; + } + + // Allocate replacement pattern + CPattern &pattern = sndFile.Patterns[m_nPattern]; + auto origPattern = pattern.GetData(); + + // Compute destination rect + PatternCursor begin(m_Selection.GetUpperLeft()), end(m_Selection.GetLowerRight()); + begin.Move(dy, dx, 0); + if(begin.GetChannel() >= sndFile.GetNumChannels()) + { + // Moved outside pattern range. + return; + } + end.Move(dy, dx, 0); + if(end.GetColumnType() == PatternCursor::effectColumn) + { + // Extend to parameter column + end.Move(0, 0, 1); + } + begin.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels()); + end.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels()); + PatternRect destination(begin, end); + + const bool moveSelection = !m_Status[psKeyboardDragSelect | psCtrlDragSelect]; + + BeginWaitCursor(); + pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, 0, 0, sndFile.GetNumChannels(), pattern.GetNumRows(), moveSelection ? "Move Selection" : "Copy Selection"); + + const ModCommand empty = ModCommand::Empty(); + auto p = pattern.begin(); + for(ROWINDEX row = 0; row < pattern.GetNumRows(); row++) + { + for(CHANNELINDEX chn = 0; chn < sndFile.GetNumChannels(); chn++, p++) + { + for(int c = PatternCursor::firstColumn; c <= PatternCursor::lastColumn; c++) + { + PatternCursor cell(row, chn, static_cast<PatternCursor::Columns>(c)); + int xsrc = chn, ysrc = row; + + if(destination.Contains(cell)) + { + // Current cell is from destination selection + xsrc -= dx; + ysrc -= dy; + } else if(m_Selection.Contains(cell)) + { + // Current cell is from source rectangle (clear) + if(moveSelection) + { + xsrc = -1; + } + } else + { + continue; + } + + // Copy the data + const ModCommand &src = (xsrc >= 0 && xsrc < (int)sndFile.GetNumChannels() && ysrc >= 0 && ysrc < (int)sndFile.Patterns[m_nPattern].GetNumRows()) ? origPattern[ysrc * sndFile.GetNumChannels() + xsrc] : empty; + switch(c) + { + case PatternCursor::noteColumn: + p->note = src.note; + break; + case PatternCursor::instrColumn: + p->instr = src.instr; + break; + case PatternCursor::volumeColumn: + p->vol = src.vol; + p->volcmd = src.volcmd; + break; + case PatternCursor::effectColumn: + p->command = src.command; + p->param = src.param; + break; + } + } + } + } + + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(begin); + SetCurSel(destination); + InvalidatePattern(); + SetModified(false); + EndWaitCursor(); +} + + +void CViewPattern::OnSetSelInstrument() +{ + SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()), false); +} + + +void CViewPattern::OnRemoveChannelDialog() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + pModDoc->ChangeNumChannels(0); + SetCurrentPattern(m_nPattern); //Updating the screen. +} + + +void CViewPattern::OnRemoveChannel() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + + if(sndFile.GetNumChannels() <= sndFile.GetModSpecifications().channelsMin) + { + Reporting::Error("No channel removed - channel number already at minimum.", "Remove channel"); + return; + } + + CHANNELINDEX nChn = m_MenuCursor.GetChannel(); + const bool isEmpty = pModDoc->IsChannelUnused(nChn); + + CString str; + str.Format(_T("Remove channel %d? This channel still contains note data!"), nChn + 1); + if(isEmpty || Reporting::Confirm(str, "Remove channel") == cnfYes) + { + std::vector<bool> keepMask(pModDoc->GetNumChannels(), true); + keepMask[nChn] = false; + pModDoc->RemoveChannels(keepMask, true); + SetCurrentPattern(m_nPattern); //Updating the screen. + pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this); + } +} + + +void CViewPattern::AddChannel(CHANNELINDEX parent, bool afterCurrent) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + + BeginWaitCursor(); + // Create new channel order, with channel nBefore being an invalid (and thus empty) channel. + std::vector<CHANNELINDEX> channels(pModDoc->GetNumChannels() + 1, CHANNELINDEX_INVALID); + CHANNELINDEX i = 0; + for(CHANNELINDEX nChn = 0; nChn < pModDoc->GetNumChannels() + 1; nChn++) + { + if(nChn != (parent + (afterCurrent ? 1 : 0))) + { + channels[nChn] = i++; + } + } + + if(pModDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID) + { + auto &chnSettings = pModDoc->GetSoundFile().ChnSettings; + chnSettings[parent + (afterCurrent ? 1 : 0)].color = chnSettings[parent + (afterCurrent ? 0 : 1)].color; + pModDoc->SetModified(); + pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this); //refresh channel headers + SetCurrentPattern(m_nPattern); + } + EndWaitCursor(); +} + + +void CViewPattern::OnDuplicateChannel() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + + const CHANNELINDEX dupChn = m_MenuCursor.GetChannel(); + if(dupChn >= pModDoc->GetNumChannels()) + return; + + if(!pModDoc->IsChannelUnused(dupChn) && Reporting::Confirm(_T("This affects all patterns, proceed?"), _T("Duplicate Channel")) != cnfYes) + return; + + BeginWaitCursor(); + // Create new channel order, with channel nDupChn duplicated. + std::vector<CHANNELINDEX> channels(pModDoc->GetNumChannels() + 1, 0); + CHANNELINDEX i = 0; + for(CHANNELINDEX nChn = 0; nChn < pModDoc->GetNumChannels() + 1; nChn++) + { + channels[nChn] = i; + if(nChn != dupChn) + i++; + } + + // Check that duplication happened and in that case update. + if(pModDoc->ReArrangeChannels(channels) != CHANNELINDEX_INVALID) + { + pModDoc->SetModified(); + pModDoc->UpdateAllViews(nullptr, GeneralHint().General().Channels(), this); //refresh channel headers + SetCurrentPattern(m_nPattern); + } + EndWaitCursor(); +} + + +void CViewPattern::OnRunScript() +{ + ; +} + + + +void CViewPattern::OnSwitchToOrderList() +{ + PostCtrlMessage(CTRLMSG_SETFOCUS); +} + + +void CViewPattern::OnPrevOrder() +{ + PostCtrlMessage(CTRLMSG_PREVORDER); +} + + +void CViewPattern::OnNextOrder() +{ + PostCtrlMessage(CTRLMSG_NEXTORDER); +} + + +void CViewPattern::OnUpdateUndo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetPatternUndo().CanUndo()); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + pModDoc->GetPatternUndo().GetUndoName())); + } +} + + +void CViewPattern::OnUpdateRedo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetPatternUndo().CanRedo()); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + pModDoc->GetPatternUndo().GetRedoName())); + } +} + + +void CViewPattern::OnEditUndo() +{ + UndoRedo(true); +} + + +void CViewPattern::OnEditRedo() +{ + UndoRedo(false); +} + + +void CViewPattern::UndoRedo(bool undo) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc && IsEditingEnabled_bmsg()) + { + CHANNELINDEX oldNumChannels = pModDoc->GetNumChannels(); + PATTERNINDEX pat = undo ? pModDoc->GetPatternUndo().Undo() : pModDoc->GetPatternUndo().Redo(); + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(pat < sndFile.Patterns.Size()) + { + if(pat != m_nPattern) + { + // Find pattern in sequence. + ORDERINDEX matchingOrder = Order().FindOrder(pat, GetCurrentOrder()); + if(matchingOrder != ORDERINDEX_INVALID) + { + SetCurrentOrder(matchingOrder); + } + SetCurrentPattern(pat); + } else + { + InvalidatePattern(true, true); + } + SetModified(false); + SanitizeCursor(); + UpdateScrollSize(); + } + if(oldNumChannels != pModDoc->GetNumChannels()) + { + pModDoc->UpdateAllViews(this, GeneralHint().Channels().ModType(), this); + } + } +} + + +// Apply amplification and fade function to volume +static void AmplifyFade(int &vol, int amp, ROWINDEX row, ROWINDEX numRows, int fadeIn, int fadeOut, Fade::Func &fadeFunc) +{ + const bool doFadeIn = fadeIn != amp, doFadeOut = fadeOut != amp; + const double fadeStart = fadeIn / 100.0, fadeStartDiff = (amp - fadeIn) / 100.0; + const double fadeEnd = fadeOut / 100.0, fadeEndDiff = (amp - fadeOut) / 100.0; + + double l; + if(doFadeIn && doFadeOut) + { + ROWINDEX numRows2 = numRows / 2; + if(row < numRows2) + l = fadeStart + fadeFunc(static_cast<double>(row) / numRows2) * fadeStartDiff; + else + l = fadeEnd + fadeFunc(static_cast<double>(numRows - row) / (numRows - numRows2)) * fadeEndDiff; + } else if(doFadeIn) + { + l = fadeStart + fadeFunc(static_cast<double>(row + 1) / numRows) * fadeStartDiff; + } else if(doFadeOut) + { + l = fadeEnd + fadeFunc(static_cast<double>(numRows - row) / numRows) * fadeEndDiff; + } else + { + l = amp / 100.0; + } + vol = mpt::saturate_round<int>(vol * l); + Limit(vol, 0, 64); +} + + +void CViewPattern::OnPatternAmplify() +{ + static CAmpDlg::AmpSettings settings{Fade::kLinear, 0, 0, 100, false, false}; + + CAmpDlg dlg(this, settings, 0); + if(dlg.DoModal() != IDOK) + { + return; + } + + CSoundFile &sndFile = *GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + return; + + const bool useVolCol = sndFile.GetModSpecifications().HasVolCommand(VOLCMD_VOLUME); + + BeginWaitCursor(); + PrepareUndo(m_Selection, "Amplify"); + + m_Selection.Sanitize(sndFile.Patterns[m_nPattern].GetNumRows(), sndFile.GetNumChannels()); + const CHANNELINDEX firstChannel = m_Selection.GetStartChannel(), lastChannel = m_Selection.GetEndChannel(); + const ROWINDEX firstRow = m_Selection.GetStartRow(), lastRow = m_Selection.GetEndRow(); + + // For partically selected start and end channels, we check if the start and end columns contain the relevant columns. + bool firstChannelValid, lastChannelValid; + if(useVolCol) + { + // Volume column + firstChannelValid = m_Selection.ContainsHorizontal(PatternCursor(0, firstChannel, PatternCursor::volumeColumn)); + lastChannelValid = m_Selection.ContainsHorizontal(PatternCursor(0, lastChannel, PatternCursor::volumeColumn)); + } else + { + // Effect column + firstChannelValid = true; // We cannot start "too far right" in the channel, since this is the last column. + lastChannelValid = m_Selection.GetLowerRight().CompareColumn(PatternCursor(0, lastChannel, PatternCursor::effectColumn)) >= 0; + } + + // Adjust min/max channel if they're only partly selected (i.e. volume column or effect column (when using .MOD) is not covered) + // XXX if only the effect column is marked in the XM format, we cannot amplify volume commands there. Does anyone use that? + if((!firstChannelValid && firstChannel >= lastChannel) || (!lastChannelValid && lastChannel <= firstChannel)) + { + EndWaitCursor(); + return; + } + + // Volume memory for each channel. + std::vector<ModCommand::VOL> chvol(lastChannel + 1, 64); + + // First, fill the volume memory in case we start the selection before some note + ApplyToSelection([&] (ModCommand &m, ROWINDEX, CHANNELINDEX chn) + { + if((chn == firstChannel && !firstChannelValid) || (chn == lastChannel && !lastChannelValid)) + return; + + if(m.command == CMD_VOLUME) + chvol[chn] = std::min(m.param, ModCommand::PARAM(64)); + else if(m.volcmd == VOLCMD_VOLUME) + chvol[chn] = m.vol; + else if(m.instr != 0) + chvol[chn] = static_cast<ModCommand::VOL>(GetDefaultVolume(m)); + }); + + Fade::Func fadeFunc = GetFadeFunc(settings.fadeLaw); + + // Now do the actual amplification + const int cy = lastRow - firstRow + 1; // total rows (for fading) + ApplyToSelection([&] (ModCommand &m, ROWINDEX nRow, CHANNELINDEX chn) + { + if((chn == firstChannel && !firstChannelValid) || (chn == lastChannel && !lastChannelValid)) + return; + + if(m.command == CMD_VOLUME) + chvol[chn] = std::min(m.param, ModCommand::PARAM(64)); + else if(m.volcmd == VOLCMD_VOLUME) + chvol[chn] = m.vol; + else if(m.instr != 0) + chvol[chn] = static_cast<ModCommand::VOL>(GetDefaultVolume(m)); + + if(settings.fadeIn || settings.fadeOut || (m.IsNote() && m.instr != 0)) + { + // Insert new volume commands where necessary + if(useVolCol && m.volcmd == VOLCMD_NONE) + { + m.volcmd = VOLCMD_VOLUME; + m.vol = chvol[chn]; + } else if(!useVolCol && m.command == CMD_NONE) + { + m.command = CMD_VOLUME; + m.param = chvol[chn]; + } + } + + if(m.volcmd == VOLCMD_VOLUME) + { + int vol = m.vol; + AmplifyFade(vol, settings.factor, nRow - firstRow, cy, settings.fadeIn ? settings.fadeInStart : settings.factor, settings.fadeOut ? settings.fadeOutEnd : settings.factor, fadeFunc); + m.vol = static_cast<ModCommand::VOL>(vol); + } + + if(m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::effectColumn)) || m_Selection.ContainsHorizontal(PatternCursor(0, chn, PatternCursor::paramColumn))) + { + if(m.command == CMD_VOLUME && m.param <= 64) + { + int vol = m.param; + AmplifyFade(vol, settings.factor, nRow - firstRow, cy, settings.fadeIn ? settings.fadeInStart : settings.factor, settings.fadeOut ? settings.fadeOutEnd : settings.factor, fadeFunc); + m.param = static_cast<ModCommand::PARAM>(vol); + } + } + }); + SetModified(false); + InvalidateSelection(); + EndWaitCursor(); +} + + +LRESULT CViewPattern::OnPlayerNotify(Notification *pnotify) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || pnotify == nullptr) + { + return 0; + } + + if(pnotify->type[Notification::Position]) + { + ORDERINDEX ord = pnotify->order; + ROWINDEX row = pnotify->row; + PATTERNINDEX pat = pnotify->pattern; + bool updateOrderList = false; + + if(m_nLastPlayedOrder != ord) + { + updateOrderList = true; + m_nLastPlayedOrder = ord; + } + + if(row < m_nLastPlayedRow) + { + InvalidateChannelsHeaders(); + } + m_nLastPlayedRow = row; + + if(!pSndFile->m_SongFlags[SONG_PAUSED | SONG_STEP]) + { + const auto &order = Order(); + if(ord >= order.GetLength() || order[ord] != pat) + { + //order doesn't correlate with pattern, so mark it as invalid + ord = ORDERINDEX_INVALID; + } + + if(m_pEffectVis && m_pEffectVis->m_hWnd) + { + m_pEffectVis->SetPlayCursor(pat, row); + } + + // Simple detection of backwards-going patterns to avoid jerky animation + m_nNextPlayRow = ROWINDEX_INVALID; + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SMOOTHSCROLL) && pSndFile->Patterns.IsValidPat(pat) && pSndFile->Patterns[pat].IsValidRow(row)) + { + for(const ModCommand *m = pSndFile->Patterns[pat].GetRow(row), *mEnd = m + pSndFile->GetNumChannels(); m != mEnd; m++) + { + if(m->command == CMD_PATTERNBREAK) + m_nNextPlayRow = m->param; + else if(m->command == CMD_POSITIONJUMP && (m_nNextPlayRow == ROWINDEX_INVALID || pSndFile->GetType() == MOD_TYPE_XM)) + m_nNextPlayRow = 0; + } + } + if(m_nNextPlayRow == ROWINDEX_INVALID) + m_nNextPlayRow = row + 1; + + m_nTicksOnRow = pnotify->ticksOnRow; + SetPlayCursor(pat, row, pnotify->tick); + // Don't follow song if user drags selections or scrollbars. + if((m_Status & (psFollowSong | psDragActive)) == psFollowSong) + { + if(pat < pSndFile->Patterns.Size()) + { + if(pat != m_nPattern || ord != m_nOrder || updateOrderList) + { + if(pat != m_nPattern) + SetCurrentPattern(pat, row); + else if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SHOWPREVIOUS) + InvalidatePattern(true, true); // Redraw previous / next pattern + + if(ord < order.GetLength()) + { + m_nOrder = ord; + SendCtrlMessage(CTRLMSG_NOTIFYCURRENTORDER, ord); + } + updateOrderList = false; + } + if(row != GetCurrentRow()) + { + SetCurrentRow((row < pSndFile->Patterns[pat].GetNumRows()) ? row : 0, false, false); + } + } + } else + { + if(updateOrderList) + { + SendCtrlMessage(CTRLMSG_FORCEREFRESH); //force orderlist refresh + updateOrderList = false; + } + } + } + } + + if(pnotify->type[Notification::VUMeters | Notification::Stop] && m_Status[psShowVUMeters]) + { + UpdateAllVUMeters(pnotify); + } + + if(pnotify->type[Notification::Stop]) + { + m_baPlayingNote.reset(); + ChnVUMeters.fill(0); // Also zero all non-visible VU meters + SetPlayCursor(PATTERNINDEX_INVALID, ROWINDEX_INVALID, 0); + } + + UpdateIndicator(false); + + return 0; +} + +// record plugin parameter changes into current pattern +LRESULT CViewPattern::OnRecordPlugParamChange(WPARAM plugSlot, LPARAM paramIndex) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr || !IsEditingEnabled()) + return 0; + + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + //Work out where to put the new data + const PatternEditPos editPos = GetEditPos(sndFile, IsLiveRecord()); + const CHANNELINDEX chn = editPos.channel; + const ROWINDEX row = editPos.row; + const PATTERNINDEX pattern = editPos.pattern; + + ModCommand &mSrc = *sndFile.Patterns[pattern].GetpModCommand(row, chn); + ModCommand m = mSrc; + + // TODO: Is the right plugin active? Move to a chan with the right plug + // Probably won't do this - finish fluctuator implementation instead. + + IMixPlugin *pPlug = sndFile.m_MixPlugins[plugSlot].pMixPlugin; + if(pPlug == nullptr) + return 0; + + if(sndFile.GetModSpecifications().HasNote(NOTE_PCS)) + { + // MPTM: Use PC Notes + + // only overwrite existing PC Notes + if(m.IsEmpty() || m.IsPcNote()) + { + m.Set(NOTE_PCS, static_cast<ModCommand::INSTR>(plugSlot + 1), static_cast<uint16>(paramIndex), static_cast<uint16>(pPlug->GetParameter(static_cast<PlugParamIndex>(paramIndex)) * ModCommand::maxColumnValue)); + } + } else if(sndFile.GetModSpecifications().HasCommand(CMD_SMOOTHMIDI)) + { + // Other formats: Use MIDI macros + + // Figure out which plug param (if any) is controllable using the active macro on this channel. + int activePlugParam = -1; + auto activeMacro = sndFile.m_PlayState.Chn[chn].nActiveMacro; + + if(sndFile.m_MidiCfg.GetParameteredMacroType(activeMacro) == kSFxPlugParam) + activePlugParam = sndFile.m_MidiCfg.MacroToPlugParam(activeMacro); + + // If the wrong macro is active, see if we can find the right one. + // If we can, activate it for this chan by writing appropriate SFx command it. + if(activePlugParam != paramIndex) + { + int foundMacro = sndFile.m_MidiCfg.FindMacroForParam(static_cast<PlugParamIndex>(paramIndex)); + if(foundMacro >= 0) + { + sndFile.m_PlayState.Chn[chn].nActiveMacro = static_cast<uint8>(foundMacro); + if(m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI) //we overwrite existing Zxx and \xx only. + { + m.command = CMD_S3MCMDEX; + if(!sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX)) + m.command = CMD_MODCMDEX; + m.param = 0xF0 | (foundMacro & 0x0F); + } + } + } + + // Write the data, but we only overwrite if the command is a macro anyway. + if(m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI) + { + m.command = CMD_SMOOTHMIDI; + PlugParamValue param = pPlug->GetParameter(static_cast<PlugParamIndex>(paramIndex)); + Limit(param, 0.0f, 1.0f); + m.param = static_cast<ModCommand::PARAM>(param * 127.0f); + } + } + + if(m != mSrc) + { + pModDoc->GetPatternUndo().PrepareUndo(pattern, chn, row, 1, 1, "Automation Entry"); + mSrc = m; + InvalidateCell(PatternCursor(row, chn)); + SetModified(false); + } + + return 0; +} + + +PatternEditPos CViewPattern::GetEditPos(const CSoundFile &sndFile, const bool liveRecord) const +{ + PatternEditPos editPos; + if(liveRecord) + { + if(m_nPlayPat != PATTERNINDEX_INVALID) + { + editPos.row = m_nPlayRow; + editPos.order = GetCurrentOrder(); + editPos.pattern = m_nPlayPat; + } else + { + editPos.row = sndFile.m_PlayState.m_nRow; + editPos.order = sndFile.m_PlayState.m_nCurrentOrder; + editPos.pattern = sndFile.m_PlayState.m_nPattern; + } + + if(!sndFile.Patterns.IsValidPat(editPos.pattern) || !sndFile.Patterns[editPos.pattern].IsValidRow(editPos.row)) + { + editPos.row = GetCurrentRow(); + editPos.order = GetCurrentOrder(); + editPos.pattern = m_nPattern; + } + const auto &order = Order(); + if(!order.IsValidPat(editPos.order) || order[editPos.order] != editPos.pattern) + { + ORDERINDEX realOrder = order.FindOrder(editPos.pattern, editPos.order); + if(realOrder != ORDERINDEX_INVALID) + editPos.order = realOrder; + } + } else + { + editPos.row = GetCurrentRow(); + editPos.order = GetCurrentOrder(); + editPos.pattern = m_nPattern; + } + editPos.channel = GetCurrentChannel(); + return editPos; +} + + +// Return ModCommand at the given cursor position of the current pattern. +// If the position is not valid, a pointer to a dummy command is returned. +ModCommand &CViewPattern::GetModCommand(PatternCursor cursor) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(GetCurrentPattern()) && pSndFile->Patterns[GetCurrentPattern()].IsValidRow(cursor.GetRow())) + { + return *pSndFile->Patterns[GetCurrentPattern()].GetpModCommand(cursor.GetRow(), cursor.GetChannel()); + } + // Failed. + static ModCommand dummy; + return dummy; +} + + +// Sanitize cursor so that it can't point to an invalid position in the current pattern. +void CViewPattern::SanitizeCursor() +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(GetCurrentPattern())) + { + m_Cursor.Sanitize(GetSoundFile()->Patterns[m_nPattern].GetNumRows(), GetSoundFile()->Patterns[m_nPattern].GetNumChannels()); + } +}; + + +// Returns pointer to modcommand at given position. +// If the position is not valid, a pointer to a dummy command is returned. +ModCommand &CViewPattern::GetModCommand(CSoundFile &sndFile, const PatternEditPos &pos) +{ + static ModCommand dummy; + if(sndFile.Patterns.IsValidPat(pos.pattern) && pos.row < sndFile.Patterns[pos.pattern].GetNumRows() && pos.channel < sndFile.GetNumChannels()) + return *sndFile.Patterns[pos.pattern].GetpModCommand(pos.row, pos.channel); + else + return dummy; +} + + +LRESULT CViewPattern::OnMidiMsg(WPARAM dwMidiDataParam, LPARAM) +{ + const uint32 midiData = static_cast<uint32>(dwMidiDataParam); + static uint8 midiVolume = 127; + + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + + if(pModDoc == nullptr || pMainFrm == nullptr) + return 0; + + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + //Midi message from our perspective: + // +---------------------------+---------------------------+-------------+-------------+ + //bit: | 24.23.22.21 | 20.19.18.17 | 16.15.14.13 | 12.11.10.09 | 08.07.06.05 | 04.03.02.01 | + // +---------------------------+---------------------------+-------------+-------------+ + // | Velocity (0-127) | Note (middle C is 60) | Event | Channel | + // +---------------------------+---------------------------+-------------+-------------+ + //(http://home.roadrunner.com/~jgglatt/tech/midispec.htm) + + //Notes: + //. Initial midi data handling is done in MidiInCallBack(). + //. If no event is received, previous event is assumed. + //. A note-on (event=9) with velocity 0 is equivalent to a note off. + //. Basing the event solely on the velocity as follows is incorrect, + // since a note-off can have a velocity too: + // BYTE event = (dwMidiData>>16) & 0x64; + //. Sample- and instrumentview handle midi mesages in their own methods. + + const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); + const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData); + const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData); + + const uint8 nNote = midiByte1 + NOTE_MIN; + int vol = midiByte2; // At this stage nVol is a non linear value in [0;127] + // Need to convert to linear in [0;64] - see below + MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData); + + if((event == MIDIEvents::evNoteOn) && !vol) + event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd + + // Handle MIDI mapping. + PLUGINDEX mappedIndex = uint8_max; + PlugParamIndex paramIndex = 0; + uint16 paramValue = uint16_max; + bool captured = sndFile.GetMIDIMapper().OnMIDImsg(midiData, mappedIndex, paramIndex, paramValue); + + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + if(ih->HandleMIDIMessage(static_cast<InputTargetContext>(kCtxViewPatterns + 1 + m_Cursor.GetColumnType()), midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull) + { + // Mapped to a command, no need to pass message on. + captured = true; + } + + // Write parameter control commands if needed. + if(paramValue != uint16_max && IsEditingEnabled() && sndFile.GetType() == MOD_TYPE_MPT) + { + const bool liveRecord = IsLiveRecord(); + + PatternEditPos editPos = GetEditPos(sndFile, liveRecord); + ModCommand &m = GetModCommand(sndFile, editPos); + pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, editPos.channel, editPos.row, 1, 1, "MIDI Mapping Record"); + m.Set(NOTE_PCS, mappedIndex, static_cast<uint16>(paramIndex), static_cast<uint16>((paramValue * ModCommand::maxColumnValue) / 16383)); + if(!liveRecord) + InvalidateRow(editPos.row); + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, PatternHint(editPos.pattern).Data(), this); + } + + if(captured) + { + // Event captured by MIDI mapping or shortcut, no need to pass message on. + return 1; + } + + const auto &modSpecs = sndFile.GetModSpecifications(); + bool recordParamAsZxx = false; + + switch(event) + { + case MIDIEvents::evNoteOff: // Note Off + if(m_midiSustainActive[channel]) + { + m_midiSustainBuffer[channel].push_back(midiData); + return 1; + } + // The following method takes care of: + // . Silencing specific active notes (just setting nNote to 255 as was done before is not acceptible) + // . Entering a note off in pattern if required + TempStopNote(nNote, ((TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_RECORDNOTEOFF) != 0)); + break; + + case MIDIEvents::evNoteOn: // Note On + // Continue playing as soon as MIDI notes are being received + if((pMainFrm->GetSoundFilePlaying() != &sndFile || sndFile.IsPaused()) && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN)) + pModDoc->OnPatternPlayNoLoop(); + + vol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume); + if(vol < 0) + vol = -1; + else + vol = (vol + 3) / 4; //Value from [0,256] to [0,64] + TempEnterNote(nNote, vol, true); + break; + + case MIDIEvents::evPolyAftertouch: // Polyphonic aftertouch + EnterAftertouch(nNote, vol); + break; + + case MIDIEvents::evChannelAftertouch: // Channel aftertouch + EnterAftertouch(NOTE_NONE, midiByte1); + break; + + case MIDIEvents::evPitchBend: // Pitch wheel + recordParamAsZxx = (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIMACROPITCHBEND) != 0 || modSpecs.HasCommand(CMD_FINETUNE); + break; + + case MIDIEvents::evControllerChange: //Controller change + // Checking whether to record MIDI controller change as MIDI macro change. + // Don't write this if command was already written by MIDI mapping. + if((paramValue == uint16_max || sndFile.GetType() != MOD_TYPE_MPT) + && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDIMACROCONTROL) + && !TrackerSettings::Instance().midiIgnoreCCs.Get()[midiByte1 & 0x7F]) + { + recordParamAsZxx = true; + } + + 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]) + { + OnMidiMsg(offEvent, 0); + } + m_midiSustainBuffer[channel].clear(); + } + recordParamAsZxx = false; + break; + } + break; + + case MIDIEvents::evSystem: + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_RESPONDTOPLAYCONTROLMSGS) + { + // Respond to MIDI song messages + switch(channel) + { + case MIDIEvents::sysStart: //Start song + pModDoc->OnPlayerPlayFromStart(); + break; + + case MIDIEvents::sysContinue: //Continue song + pModDoc->OnPlayerPlay(); + break; + + case MIDIEvents::sysStop: //Stop song + pModDoc->OnPlayerStop(); + break; + } + } + break; + } + + // Write CC or pitch bend message as MIDI macro change. + if(recordParamAsZxx && IsEditingEnabled()) + { + const bool liveRecord = IsLiveRecord(); + + const auto editpos = GetEditPos(sndFile, liveRecord); + ModCommand &m = GetModCommand(sndFile, editpos); + bool update = false; + + if(event == MIDIEvents::evPitchBend && (m.command == CMD_NONE || m.command == CMD_FINETUNE || m.command == CMD_FINETUNE_SMOOTH) && modSpecs.HasCommand(CMD_FINETUNE)) + { + pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry"); + m.command = (m.command == CMD_NONE) ? CMD_FINETUNE : CMD_FINETUNE_SMOOTH; + m.param = (midiByte2 << 1) | (midiByte1 >> 7); + update = true; + } else if(m.IsPcNote()) + { + pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry"); + m.SetValueEffectCol(static_cast<decltype(m.GetValueEffectCol())>(Util::muldivr(midiByte2, ModCommand::maxColumnValue, 127))); + update = true; + } else if((m.command == CMD_NONE || m.command == CMD_SMOOTHMIDI || m.command == CMD_MIDI) + && (modSpecs.HasCommand(CMD_SMOOTHMIDI) || modSpecs.HasCommand(CMD_MIDI))) + { + // Write command only if there's no existing command or already a midi macro command. + pModDoc->GetPatternUndo().PrepareUndo(editpos.pattern, editpos.channel, editpos.row, 1, 1, "MIDI Record Entry"); + m.command = modSpecs.HasCommand(CMD_SMOOTHMIDI) ? CMD_SMOOTHMIDI : CMD_MIDI; + m.param = midiByte2; + update = true; + } + if(update) + { + pModDoc->SetModified(); + pModDoc->UpdateAllViews(this, PatternHint(editpos.pattern).Data(), this); + + // Update GUI only if not recording live. + if(!liveRecord) + InvalidateRow(editpos.row); + } + } + + // Pass MIDI to plugin + if(TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_MIDITOPLUG + && pMainFrm->GetModPlaying() == pModDoc + && event != MIDIEvents::evNoteOn + && event != MIDIEvents::evNoteOff) + { + const INSTRUMENTINDEX instr = static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()); + IMixPlugin *plug = sndFile.GetInstrumentPlugin(instr); + if(plug) + { + plug->MidiSend(midiData); + // Sending MIDI may modify the plugin. For now, if MIDI data + // is not active sensing, set modified. + if(midiData != MIDIEvents::System(MIDIEvents::sysActiveSense)) + pModDoc->SetModified(); + } + } + + return 1; +} + + +LRESULT CViewPattern::OnModViewMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + + case VIEWMSG_SETCTRLWND: + m_hWndCtrl = (HWND)lParam; + m_nOrder = static_cast<ORDERINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTORDER)); + SetCurrentPattern(static_cast<PATTERNINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTPATTERN))); + break; + + case VIEWMSG_GETCURRENTPATTERN: + return m_nPattern; + + case VIEWMSG_SETCURRENTPATTERN: + m_nOrder = static_cast<ORDERINDEX>(SendCtrlMessage(CTRLMSG_GETCURRENTORDER)); + SetCurrentPattern(static_cast<PATTERNINDEX>(lParam)); + break; + + case VIEWMSG_GETCURRENTPOS: + return (m_nPattern << 16) | GetCurrentRow(); + + case VIEWMSG_FOLLOWSONG: + m_Status.reset(psFollowSong); + if(lParam) + { + CModDoc *pModDoc = GetDocument(); + m_Status.set(psFollowSong); + if(pModDoc) + pModDoc->SetNotifications(Notification::Position | Notification::VUMeters); + if(pModDoc) + pModDoc->SetFollowWnd(m_hWnd); + SetFocus(); + } else + { + InvalidateRow(); + } + break; + + case VIEWMSG_PATTERNLOOP: + SendCtrlMessage(CTRLMSG_PAT_LOOP, lParam); + break; + + case VIEWMSG_SETRECORD: + m_Status.set(psRecordingEnabled, !!lParam); + break; + + case VIEWMSG_SETSPACING: + m_nSpacing = static_cast<UINT>(lParam); + break; + + case VIEWMSG_PATTERNPROPERTIES: + ShowPatternProperties(static_cast<PATTERNINDEX>(lParam)); + GetParentFrame()->SetActiveView(this); + break; + + case VIEWMSG_SETVUMETERS: + m_Status.set(psShowVUMeters, !!lParam); + UpdateSizes(); + UpdateScrollSize(); + InvalidatePattern(true, true); + break; + + case VIEWMSG_SETPLUGINNAMES: + m_Status.set(psShowPluginNames, !!lParam); + UpdateSizes(); + UpdateScrollSize(); + InvalidatePattern(true, true); + break; + + case VIEWMSG_DOMIDISPACING: + if(m_nSpacing) + { + int temp = timeGetTime(); + if(temp - lParam >= 60) + { + CModDoc *pModDoc = GetDocument(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(!m_Status[psFollowSong] + || (pMainFrm->GetFollowSong(pModDoc) != m_hWnd) + || (pModDoc->GetSoundFile().IsPaused())) + { + SetCurrentRow(GetCurrentRow() + m_nSpacing); + } + } else + { + Sleep(0); + PostMessage(WM_MOD_VIEWMSG, VIEWMSG_DOMIDISPACING, lParam); + } + } + break; + + case VIEWMSG_LOADSTATE: + if(lParam) + { + PATTERNVIEWSTATE *pState = (PATTERNVIEWSTATE *)lParam; + if(pState->nDetailLevel != PatternCursor::firstColumn) + m_nDetailLevel = pState->nDetailLevel; + if(pState->initialized) + { + SetCurrentPattern(pState->nPattern); + // Fix: Horizontal scrollbar pos screwed when selecting with mouse + SetCursorPosition(pState->cursor); + SetCurSel(pState->selection); + } + } + break; + + case VIEWMSG_SAVESTATE: + if(lParam) + { + PATTERNVIEWSTATE *pState = (PATTERNVIEWSTATE *)lParam; + pState->initialized = true; + pState->nPattern = m_nPattern; + pState->cursor = m_Cursor; + pState->selection = m_Selection; + pState->nDetailLevel = m_nDetailLevel; + pState->nOrder = GetCurrentOrder(); + } + break; + + case VIEWMSG_EXPANDPATTERN: + { + CModDoc *pModDoc = GetDocument(); + if(pModDoc->ExpandPattern(m_nPattern)) + { + m_Cursor.SetRow(m_Cursor.GetRow() * 2); + SetCurrentPattern(m_nPattern); + } + break; + } + + case VIEWMSG_SHRINKPATTERN: + { + CModDoc *pModDoc = GetDocument(); + if(pModDoc->ShrinkPattern(m_nPattern)) + { + m_Cursor.SetRow(m_Cursor.GetRow() / 2); + SetCurrentPattern(m_nPattern); + } + break; + } + + case VIEWMSG_COPYPATTERN: + { + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr && pSndFile->Patterns.IsValidPat(m_nPattern)) + { + CopyPattern(m_nPattern, PatternRect(PatternCursor(0, 0), PatternCursor(pSndFile->Patterns[m_nPattern].GetNumRows() - 1, pSndFile->GetNumChannels() - 1, PatternCursor::lastColumn))); + } + break; + } + + case VIEWMSG_PASTEPATTERN: + PastePattern(m_nPattern, PatternCursor(0), PatternClipboard::pmOverwrite); + InvalidatePattern(); + break; + + case VIEWMSG_AMPLIFYPATTERN: + OnPatternAmplify(); + break; + + case VIEWMSG_SETDETAIL: + if(lParam != m_nDetailLevel) + { + m_nDetailLevel = static_cast<PatternCursor::Columns>(lParam); + UpdateSizes(); + UpdateScrollSize(); + SetCurrentColumn(m_Cursor); + InvalidatePattern(true, true); + } + break; + case VIEWMSG_DOSCROLL: + OnMouseWheel(0, static_cast<short>(lParam), CPoint(0, 0)); + break; + + + default: + return CModScrollView::OnModViewMsg(wParam, lParam); + } + return 0; +} + + +void CViewPattern::CursorJump(int distance, bool snap) +{ + ROWINDEX row = GetCurrentRow(); + const bool upwards = distance < 0; + const int distanceAbs = std::abs(distance); + + if(snap && distanceAbs) + // cppcheck false-positive + // cppcheck-suppress signConversion + row = (((row + (upwards ? -1 : 0)) / distanceAbs) + (upwards ? 0 : 1)) * distanceAbs; + else + row += distance; + row = SetCurrentRow(row, true); + + if(IsLiveRecord() && !m_Status[psDragActive]) + { + CriticalSection cs; + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + if(m_nOrder != sndFile.m_PlayState.m_nCurrentOrder) + { + // We jumped to a different order + sndFile.ResetChannels(); + sndFile.StopAllVsti(); + } + + sndFile.m_PlayState.m_nCurrentOrder = sndFile.m_PlayState.m_nNextOrder = GetCurrentOrder(); + sndFile.m_PlayState.m_nPattern = m_nPattern; + sndFile.m_PlayState.m_nRow = m_nPlayRow = row; + sndFile.m_PlayState.m_nNextRow = m_nNextPlayRow = row + 1; + // Queue the correct follow-up pattern if we just jumped to the last row. + if(sndFile.Patterns.IsValidPat(m_nPattern) && m_nNextPlayRow >= sndFile.Patterns[m_nPattern].GetNumRows()) + { + sndFile.m_PlayState.m_nNextOrder++; + } + CMainFrame::GetMainFrame()->ResetNotificationBuffer(); + } else + { + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW) + { + PatternStep(row); + } + } +} + + +LRESULT CViewPattern::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return kcNull; + + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + switch(wParam) + { + case kcPrevInstrument: OnPrevInstrument(); return wParam; + case kcNextInstrument: OnNextInstrument(); return wParam; + case kcPrevOrder: OnPrevOrder(); return wParam; + case kcNextOrder: OnNextOrder(); return wParam; + case kcPatternPlayRow: OnPatternStep(); return wParam; + case kcPatternRecord: OnPatternRecord(); return wParam; + case kcCursorCopy: OnCursorCopy(); return wParam; + case kcCursorPaste: OnCursorPaste(); return wParam; + case kcChannelMute: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++) + OnMuteChannel(c); + return wParam; + case kcChannelSolo: OnSoloChannel(GetCurrentChannel()); return wParam; + case kcChannelUnmuteAll: OnUnmuteAll(); return wParam; + case kcToggleChanMuteOnPatTransition: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++) + TogglePendingMute(c); + return wParam; + case kcUnmuteAllChnOnPatTransition: OnPendingUnmuteAllChnFromClick(); return wParam; + case kcChannelRecordSelect: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++) + pModDoc->ToggleChannelRecordGroup(c, RecordGroup::Group1); + InvalidateChannelsHeaders(); return wParam; + case kcChannelSplitRecordSelect: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++) + pModDoc->ToggleChannelRecordGroup(c, RecordGroup::Group2); + InvalidateChannelsHeaders(); return wParam; + case kcChannelReset: for(CHANNELINDEX c = m_Selection.GetStartChannel(); c <= m_Selection.GetEndChannel(); c++) + ResetChannel(m_Cursor.GetChannel()); + return wParam; + case kcTimeAtRow: OnShowTimeAtRow(); return wParam; + case kcSoloChnOnPatTransition: PendingSoloChn(GetCurrentChannel()); return wParam; + case kcTransposeUp: OnTransposeUp(); return wParam; + case kcTransposeDown: OnTransposeDown(); return wParam; + case kcTransposeOctUp: OnTransposeOctUp(); return wParam; + case kcTransposeOctDown: OnTransposeOctDown(); return wParam; + case kcTransposeCustom: OnTransposeCustom(); return wParam; + case kcTransposeCustomQuick: OnTransposeCustomQuick(); return wParam; + case kcDataEntryUp: DataEntry(true, false); return wParam; + case kcDataEntryDown: DataEntry(false, false); return wParam; + case kcDataEntryUpCoarse: DataEntry(true, true); return wParam; + case kcDataEntryDownCoarse: DataEntry(false, true); return wParam; + case kcSelectChannel: OnSelectCurrentChannel(); return wParam; + case kcSelectColumn: OnSelectCurrentColumn(); return wParam; + case kcPatternAmplify: OnPatternAmplify(); return wParam; + case kcPatternSetInstrumentNotEmpty: + case kcPatternSetInstrument: SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(GetCurrentInstrument()), wParam == kcPatternSetInstrument); return wParam; + case kcPatternInterpolateNote: OnInterpolateNote(); return wParam; + case kcPatternInterpolateInstr: OnInterpolateInstr(); return wParam; + case kcPatternInterpolateVol: OnInterpolateVolume(); return wParam; + case kcPatternInterpolateEffect: OnInterpolateEffect(); return wParam; + case kcPatternVisualizeEffect: OnVisualizeEffect(); return wParam; + //case kcPatternOpenRandomizer: OnOpenRandomizer(); return wParam; + case kcPatternGrowSelection: OnGrowSelection(); return wParam; + case kcPatternShrinkSelection: OnShrinkSelection(); return wParam; + + // Pattern navigation: + case kcPatternJumpUph1Select: + case kcPatternJumpUph1: CursorJump(-(int)GetRowsPerMeasure(), false); return wParam; + case kcPatternJumpDownh1Select: + case kcPatternJumpDownh1: CursorJump(GetRowsPerMeasure(), false); return wParam; + case kcPatternJumpUph2Select: + case kcPatternJumpUph2: CursorJump(-(int)GetRowsPerBeat(), false); return wParam; + case kcPatternJumpDownh2Select: + case kcPatternJumpDownh2: CursorJump(GetRowsPerBeat(), false); return wParam; + + case kcPatternSnapUph1Select: + case kcPatternSnapUph1: CursorJump(-(int)GetRowsPerMeasure(), true); return wParam; + case kcPatternSnapDownh1Select: + case kcPatternSnapDownh1: CursorJump(GetRowsPerMeasure(), true); return wParam; + case kcPatternSnapUph2Select: + case kcPatternSnapUph2: CursorJump(-(int)GetRowsPerBeat(), true); return wParam; + case kcPatternSnapDownh2Select: + case kcPatternSnapDownh2: CursorJump(GetRowsPerBeat(), true); return wParam; + + case kcNavigateDownSelect: + case kcNavigateDown: CursorJump(1, false); return wParam; + case kcNavigateUpSelect: + case kcNavigateUp: CursorJump(-1, false); return wParam; + + case kcNavigateDownBySpacingSelect: + case kcNavigateDownBySpacing: CursorJump(m_nSpacing, false); return wParam; + case kcNavigateUpBySpacingSelect: + case kcNavigateUpBySpacing: CursorJump(-(int)m_nSpacing, false); return wParam; + + case kcNavigateLeftSelect: + case kcNavigateLeft: + MoveCursor(false); + return wParam; + case kcNavigateRightSelect: + case kcNavigateRight: + MoveCursor(true); + return wParam; + + case kcNavigateNextChanSelect: + case kcNavigateNextChan: SetCurrentColumn((GetCurrentChannel() + 1) % sndFile.GetNumChannels(), m_Cursor.GetColumnType()); return wParam; + case kcNavigatePrevChanSelect: + case kcNavigatePrevChan:{if(GetCurrentChannel() > 0) + SetCurrentColumn((GetCurrentChannel() - 1) % sndFile.GetNumChannels(), m_Cursor.GetColumnType()); + else + SetCurrentColumn(sndFile.GetNumChannels() - 1, m_Cursor.GetColumnType()); + SetSelToCursor(); + return wParam;} + + case kcHomeHorizontalSelect: + case kcHomeHorizontal: if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0); + else if (GetCurrentRow() > 0) SetCurrentRow(0); + return wParam; + case kcHomeVerticalSelect: + case kcHomeVertical: if (GetCurrentRow() > 0) SetCurrentRow(0); + else if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0); + return wParam; + case kcHomeAbsoluteSelect: + case kcHomeAbsolute: if (!m_Cursor.IsInFirstColumn()) SetCurrentColumn(0); + if (GetCurrentRow() > 0) SetCurrentRow(0); + return wParam; + + case kcEndHorizontalSelect: + case kcEndHorizontal: if (m_Cursor.CompareColumn(PatternCursor(0, sndFile.GetNumChannels() - 1, m_nDetailLevel)) < 0) SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel); + else if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1); + return wParam; + case kcEndVerticalSelect: + case kcEndVertical: if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1); + else if (m_Cursor.CompareColumn(PatternCursor(0, sndFile.GetNumChannels() - 1, m_nDetailLevel)) < 0) SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel); + return wParam; + case kcEndAbsoluteSelect: + case kcEndAbsolute: SetCurrentColumn(sndFile.GetNumChannels() - 1, m_nDetailLevel); + if (GetCurrentRow() < pModDoc->GetPatternSize(m_nPattern) - 1) SetCurrentRow(pModDoc->GetPatternSize(m_nPattern) - 1); + return wParam; + + case kcPrevEntryInColumn: + case kcNextEntryInColumn: + JumpToPrevOrNextEntry(wParam == kcNextEntryInColumn, false); + return wParam; + case kcPrevEntryInColumnSelect: + case kcNextEntryInColumnSelect: + JumpToPrevOrNextEntry(wParam == kcNextEntryInColumnSelect, true); + return wParam; + + case kcNextPattern: { PATTERNINDEX n = m_nPattern + 1; + while ((n < sndFile.Patterns.Size()) && !sndFile.Patterns.IsValidPat(n)) n++; + SetCurrentPattern((n < sndFile.Patterns.Size()) ? n : 0); + ORDERINDEX currentOrder = GetCurrentOrder(); + ORDERINDEX newOrder = Order().FindOrder(m_nPattern, currentOrder, true); + if(newOrder != ORDERINDEX_INVALID) + SetCurrentOrder(newOrder); + return wParam; + } + case kcPrevPattern: { PATTERNINDEX n = (m_nPattern) ? m_nPattern - 1 : sndFile.Patterns.Size() - 1; + while (n > 0 && !sndFile.Patterns.IsValidPat(n)) n--; + SetCurrentPattern(n); + ORDERINDEX currentOrder = GetCurrentOrder(); + ORDERINDEX newOrder = Order().FindOrder(m_nPattern, currentOrder, false); + if(newOrder != ORDERINDEX_INVALID) + SetCurrentOrder(newOrder); + return wParam; + } + case kcPrevSequence: + case kcNextSequence: + SendCtrlMessage(CTRLMSG_PAT_SETSEQUENCE, mpt::wrapping_modulo(sndFile.Order.GetCurrentSequenceIndex() + (wParam == kcPrevSequence ? -1 : 1), sndFile.Order.GetNumSequences())); + return wParam; + case kcSelectWithCopySelect: + case kcSelectWithNav: + case kcSelect: if(!m_Status[psDragnDropEdit | psRowSelection | psChannelSelection | psMouseDragSelect]) m_StartSel = m_Cursor; + m_Status.set(psKeyboardDragSelect); + return wParam; + case kcSelectOffWithCopySelect: + case kcSelectOffWithNav: + case kcSelectOff: m_Status.reset(psKeyboardDragSelect | psShiftSelect); + return wParam; + case kcCopySelectWithSelect: + case kcCopySelectWithNav: + case kcCopySelect: if(!m_Status[psDragnDropEdit | psRowSelection | psChannelSelection | psMouseDragSelect]) m_StartSel = m_Cursor; + m_Status.set(psCtrlDragSelect); return wParam; + case kcCopySelectOffWithSelect: + case kcCopySelectOffWithNav: + case kcCopySelectOff: m_Status.reset(psCtrlDragSelect); return wParam; + + case kcSelectBeat: + case kcSelectMeasure: + SelectBeatOrMeasure(wParam == kcSelectBeat); return wParam; + + case kcSelectEvent: SetCurSel(PatternCursor(m_Selection.GetStartRow(), m_Selection.GetStartChannel(), PatternCursor::firstColumn), + PatternCursor(m_Selection.GetEndRow(), m_Selection.GetEndChannel(), PatternCursor::lastColumn)); + return wParam; + case kcSelectRow: SetCurSel(PatternCursor(m_Selection.GetStartRow(), 0, PatternCursor::firstColumn), + PatternCursor(m_Selection.GetEndRow(), sndFile.GetNumChannels(), PatternCursor::lastColumn)); + return wParam; + + case kcClearRow: OnClearField(RowMask(), false); return wParam; + case kcClearField: OnClearField(RowMask(m_Cursor), false); return wParam; + case kcClearFieldITStyle: OnClearField(RowMask(m_Cursor), false, true); return wParam; + case kcClearRowStep: OnClearField(RowMask(), true); return wParam; + case kcClearFieldStep: OnClearField(RowMask(m_Cursor), true); return wParam; + case kcClearFieldStepITStyle: OnClearField(RowMask(m_Cursor), true, true); return wParam; + + case kcDeleteRow: OnDeleteRow(); return wParam; + case kcDeleteWholeRow: OnDeleteWholeRow(); return wParam; + case kcDeleteRowGlobal: OnDeleteRowGlobal(); return wParam; + case kcDeleteWholeRowGlobal: OnDeleteWholeRowGlobal(); return wParam; + + case kcInsertRow: OnInsertRow(); return wParam; + case kcInsertWholeRow: OnInsertWholeRow(); return wParam; + case kcInsertRowGlobal: OnInsertRowGlobal(); return wParam; + case kcInsertWholeRowGlobal: OnInsertWholeRowGlobal(); return wParam; + + case kcShowNoteProperties: ShowEditWindow(); return wParam; + case kcShowPatternProperties: OnPatternProperties(); return wParam; + case kcShowSplitKeyboardSettings: SetSplitKeyboardSettings(); return wParam; + case kcShowEditMenu: + { + CPoint pt = GetPointFromPosition(m_Cursor); + pt.x += GetChannelWidth() / 2; + pt.y += GetRowHeight() / 2; + OnRButtonDown(0, pt); + } + return wParam; + case kcShowChannelCtxMenu: + { + CPoint pt = GetPointFromPosition(m_Cursor); + pt.x += GetChannelWidth() / 2; + pt.y = (m_szHeader.cy - m_szPluginHeader.cy) / 2; + OnRButtonDown(0, pt); + } + return wParam; + case kcShowChannelPluginCtxMenu: + { + CPoint pt = GetPointFromPosition(m_Cursor); + pt.x += GetChannelWidth() / 2; + pt.y = m_szHeader.cy - m_szPluginHeader.cy / 2; + OnRButtonDown(0, pt); + } + return wParam; + case kcPatternGoto: OnEditGoto(); return wParam; + + case kcNoteCut: TempEnterNote(NOTE_NOTECUT); return wParam; + case kcNoteOff: TempEnterNote(NOTE_KEYOFF); return wParam; + case kcNoteFade: TempEnterNote(NOTE_FADE); return wParam; + case kcNotePC: TempEnterNote(NOTE_PC); return wParam; + case kcNotePCS: TempEnterNote(NOTE_PCS); return wParam; + + case kcEditUndo: OnEditUndo(); return wParam; + case kcEditRedo: OnEditRedo(); return wParam; + case kcEditFind: OnEditFind(); return wParam; + case kcEditFindNext: OnEditFindNext(); return wParam; + case kcEditCut: OnEditCut(); return wParam; + case kcEditCopy: OnEditCopy(); return wParam; + case kcCopyAndLoseSelection: + OnEditCopy(); + [[fallthrough]]; + case kcLoseSelection: + SetSelToCursor(); + return wParam; + case kcEditPaste: OnEditPaste(); return wParam; + case kcEditMixPaste: OnEditMixPaste(); return wParam; + case kcEditMixPasteITStyle: OnEditMixPasteITStyle(); return wParam; + case kcEditPasteFlood: OnEditPasteFlood(); return wParam; + case kcEditPushForwardPaste: OnEditPushForwardPaste(); return wParam; + case kcEditSelectAll: OnEditSelectAll(); return wParam; + case kcTogglePluginEditor: TogglePluginEditor(GetCurrentChannel()); return wParam; + case kcToggleFollowSong: SendCtrlMessage(CTRLMSG_PAT_FOLLOWSONG, 1); return wParam; + case kcChangeLoopStatus: SendCtrlMessage(CTRLMSG_PAT_LOOP, -1); return wParam; + case kcNewPattern: SendCtrlMessage(CTRLMSG_PAT_NEWPATTERN); return wParam; + case kcDuplicatePattern: SendCtrlMessage(CTRLMSG_PAT_DUPPATTERN); return wParam; + case kcSwitchToOrderList: OnSwitchToOrderList(); return wParam; + case kcToggleOverflowPaste: TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_OVERFLOWPASTE; return wParam; + case kcToggleNoteOffRecordPC: TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_KBDNOTEOFF; return wParam; + case kcToggleNoteOffRecordMIDI: TrackerSettings::Instance().m_dwMidiSetup ^= MIDISETUP_RECORDNOTEOFF; return wParam; + case kcPatternEditPCNotePlugin: OnTogglePCNotePluginEditor(); return wParam; + case kcQuantizeSettings: OnSetQuantize(); return wParam; + case kcLockPlaybackToRows: OnLockPatternRows(); return wParam; + case kcFindInstrument: FindInstrument(); return wParam; + case kcChannelSettings: + { + // Open centered Quick Channel Settings dialog. + CRect windowPos; + GetWindowRect(windowPos); + m_quickChannelProperties.Show(GetDocument(), m_Cursor.GetChannel(), CPoint(windowPos.left + windowPos.Width() / 2, windowPos.top + windowPos.Height() / 2)); + return wParam; + } + case kcChannelTranspose: m_MenuCursor = m_Cursor; OnTransposeChannel(); return wParam; + case kcChannelDuplicate: m_MenuCursor = m_Cursor; OnDuplicateChannel(); return wParam; + case kcChannelAddBefore: m_MenuCursor = m_Cursor; OnAddChannelFront(); return wParam; + case kcChannelAddAfter: m_MenuCursor = m_Cursor; OnAddChannelAfter(); return wParam; + case kcChannelRemove: m_MenuCursor = m_Cursor; OnRemoveChannel(); return wParam; + case kcChannelMoveLeft: + if(CHANNELINDEX chn = m_Selection.GetStartChannel(); chn > 0) + DragChannel(chn, chn - 1u, m_Selection.GetNumChannels(), false); + return wParam; + case kcChannelMoveRight: + if (CHANNELINDEX chn = m_Selection.GetStartChannel(); chn < sndFile.GetNumChannels() - m_Selection.GetNumChannels()) + DragChannel(chn, chn + 1u, m_Selection.GetNumChannels(), false); + return wParam; + + case kcSplitPattern: m_MenuCursor = m_Cursor; OnSplitPattern(); return wParam; + + case kcDecreaseSpacing: + if(m_nSpacing > 0) SetSpacing(m_nSpacing - 1); + return wParam; + case kcIncreaseSpacing: + if(m_nSpacing < MAX_SPACING) SetSpacing(m_nSpacing + 1); + return wParam; + + case kcChordEditor: + { + CChordEditor dlg(this); + dlg.DoModal(); + return wParam; + } + + // Clipboard Manager + case kcToggleClipboardManager: + PatternClipboardDialog::Toggle(); + return wParam; + case kcClipboardPrev: + PatternClipboard::CycleBackward(); + PatternClipboardDialog::UpdateList(); + return wParam; + case kcClipboardNext: + PatternClipboard::CycleForward(); + PatternClipboardDialog::UpdateList(); + return wParam; + + case kcCutPatternChannel: + PatternClipboard::Copy(sndFile, GetCurrentPattern(), GetCurrentChannel()); + OnEditSelectChannel(); + OnClearSelection(false); + return wParam; + case kcCutPattern: + PatternClipboard::Copy(sndFile, GetCurrentPattern()); + OnEditSelectAll(); + OnClearSelection(false); + return wParam; + case kcCopyPatternChannel: + PatternClipboard::Copy(sndFile, GetCurrentPattern(), GetCurrentChannel()); + return wParam; + case kcCopyPattern: + PatternClipboard::Copy(sndFile, GetCurrentPattern()); + return wParam; + case kcPastePatternChannel: + case kcPastePattern: + if(PatternClipboard::Paste(sndFile, GetCurrentPattern(), wParam == kcPastePatternChannel ? GetCurrentChannel() : CHANNELINDEX_INVALID)) + { + SetModified(); + InvalidatePattern(); + GetDocument()->UpdateAllViews(this, PatternHint(GetCurrentPattern()).Data(), this); + } + return wParam; + case kcTogglePatternPlayRow: + TrackerSettings::Instance().m_dwPatternSetup ^= PATTERN_PLAYNAVIGATEROW; + CMainFrame::GetMainFrame()->SetHelpText((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW) + ? _T("Play whole row when navigatin was turned is now enabled.") : _T("Play whole row when navigatin was turned is now disabled.")); + return wParam; + } + + // Ignore note entry if it is on key hold and user is in key-jazz mode or edit step is 0 (so repeated entry would be useless) + const auto keyCombination = KeyCombination::FromLPARAM(lParam); + const bool enterNote = keyCombination.EventType() != kKeyEventRepeat || (IsEditingEnabled() && m_nSpacing != 0); + + // Ranges: + if(wParam >= kcVPStartNotes && wParam <= kcVPEndNotes) + { + if(enterNote) + TempEnterNote(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartNotes))); + return wParam; + } else if(wParam >= kcVPStartChords && wParam <= kcVPEndChords) + { + if(enterNote) + TempEnterChord(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartChords))); + return wParam; + } + + if(wParam >= kcVPStartNoteStops && wParam <= kcVPEndNoteStops) + { + TempStopNote(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartNoteStops))); + return wParam; + } else if(wParam >= kcVPStartChordStops && wParam <= kcVPEndChordStops) + { + TempStopChord(GetNoteWithBaseOctave(static_cast<int>(wParam - kcVPStartChordStops))); + return wParam; + } + + if(wParam >= kcSetSpacing0 && wParam <= kcSetSpacing9) + { + SetSpacing(static_cast<int>(wParam) - kcSetSpacing0); + return wParam; + } + + if(wParam >= kcSetIns0 && wParam <= kcSetIns9) + { + if(IsEditingEnabled_bmsg()) + TempEnterIns(static_cast<int>(wParam) - kcSetIns0); + return wParam; + } + + if(wParam >= kcSetOctave0 && wParam <= kcSetOctave9) + { + if(IsEditingEnabled_bmsg()) + TempEnterOctave(static_cast<int>(wParam) - kcSetOctave0); + return wParam; + } + + if(wParam >= kcSetOctaveStop0 && wParam <= kcSetOctaveStop9) + { + TempStopOctave(static_cast<int>(wParam) - kcSetOctaveStop0); + return wParam; + } + + if(wParam >= kcSetVolumeStart && wParam <= kcSetVolumeEnd) + { + if(IsEditingEnabled_bmsg()) + TempEnterVol(static_cast<int>(wParam) - kcSetVolumeStart); + return wParam; + } + + if(wParam >= kcSetFXStart && wParam <= kcSetFXEnd) + { + if(IsEditingEnabled_bmsg()) + TempEnterFX(static_cast<ModCommand::COMMAND>(wParam - kcSetFXStart + 1)); + return wParam; + } + + if(wParam >= kcSetFXParam0 && wParam <= kcSetFXParamF) + { + if(IsEditingEnabled_bmsg()) + TempEnterFXparam(static_cast<int>(wParam) - kcSetFXParam0); + return wParam; + } + + return kcNull; +} + + +// Move pattern cursor to left or right, respecting invisible columns. +void CViewPattern::MoveCursor(bool moveRight) +{ + if(!moveRight) + { + // Move cursor one column to the left + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP) && m_Cursor.IsInFirstColumn()) + { + // Wrap around to last channel + SetCurrentColumn(GetDocument()->GetNumChannels() - 1, m_nDetailLevel); + } else if(!m_Cursor.IsInFirstColumn()) + { + m_Cursor.Move(0, 0, -1); + SetCurrentColumn(m_Cursor); + } + } else + { + // Move cursor one column to the right + const PatternCursor rightmost(0, GetDocument()->GetNumChannels() - 1, m_nDetailLevel); + if(m_Cursor.CompareColumn(rightmost) >= 0) + { + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_WRAP)) + { + // Wrap around to first channel. + SetCurrentColumn(0); + } else + { + SetCurrentColumn(rightmost); + } + } else + { + do + { + m_Cursor.Move(0, 0, 1); + } while(m_Cursor.GetColumnType() > m_nDetailLevel); + SetCurrentColumn(m_Cursor); + } + } +} + + +static bool EnterPCNoteValue(int v, ModCommand &m, uint16 (ModCommand::*getMethod)() const, void (ModCommand::*setMethod)(uint16)) +{ + if(v < 0 || v > 9) + return false; + + uint16 val = (m.*getMethod)(); + // Move existing digits to left, drop out leftmost digit and push new digit to the least significant digit. + val = static_cast<uint16>((val % 100) * 10 + v); + LimitMax(val, static_cast<uint16>(ModCommand::maxColumnValue)); + (m.*setMethod)(val); + return true; +} + + +// Enter volume effect / number in the pattern. +void CViewPattern::TempEnterVol(int v) +{ + CSoundFile *pSndFile = GetSoundFile(); + + if(pSndFile == nullptr || !IsEditingEnabled_bmsg()) + return; + + PrepareUndo(m_Cursor, m_Cursor, "Volume Entry"); + + ModCommand &target = GetCursorCommand(); + ModCommand oldcmd = target; // This is the command we are about to overwrite + const bool isDigit = (v >= 0) && (v <= 9); + + if(target.IsPcNote()) + { + if(EnterPCNoteValue(v, target, &ModCommand::GetValueVolCol, &ModCommand::SetValueVolCol)) + m_PCNoteEditMemory = target; + } else + { + ModCommand::VOLCMD volcmd = target.volcmd; + uint16 vol = target.vol; + if(isDigit) + { + vol = ((vol * 10) + v) % 100; + if(!volcmd) + volcmd = VOLCMD_VOLUME; + } else + { + switch(v + kcSetVolumeStart) + { + case kcSetVolumeVol: volcmd = VOLCMD_VOLUME; break; + case kcSetVolumePan: volcmd = VOLCMD_PANNING; break; + case kcSetVolumeVolSlideUp: volcmd = VOLCMD_VOLSLIDEUP; break; + case kcSetVolumeVolSlideDown: volcmd = VOLCMD_VOLSLIDEDOWN; break; + case kcSetVolumeFineVolUp: volcmd = VOLCMD_FINEVOLUP; break; + case kcSetVolumeFineVolDown: volcmd = VOLCMD_FINEVOLDOWN; break; + case kcSetVolumeVibratoSpd: volcmd = VOLCMD_VIBRATOSPEED; break; + case kcSetVolumeVibrato: volcmd = VOLCMD_VIBRATODEPTH; break; + case kcSetVolumeXMPanLeft: volcmd = VOLCMD_PANSLIDELEFT; break; + case kcSetVolumeXMPanRight: volcmd = VOLCMD_PANSLIDERIGHT; break; + case kcSetVolumePortamento: volcmd = VOLCMD_TONEPORTAMENTO; break; + case kcSetVolumeITPortaUp: volcmd = VOLCMD_PORTAUP; break; + case kcSetVolumeITPortaDown: volcmd = VOLCMD_PORTADOWN; break; + case kcSetVolumeITOffset: volcmd = VOLCMD_OFFSET; break; + } + if(target.volcmd == VOLCMD_NONE && volcmd == m_cmdOld.volcmd) + { + vol = m_cmdOld.vol; + } + } + + uint16 max; + switch(volcmd) + { + case VOLCMD_VOLUME: + case VOLCMD_PANNING: + max = 64; + break; + default: + max = (pSndFile->GetType() == MOD_TYPE_XM) ? 0x0F : 9; + break; + } + + if(vol > max) + vol %= 10; + if(pSndFile->GetModSpecifications().HasVolCommand(volcmd)) + { + m_cmdOld.volcmd = target.volcmd = volcmd; + m_cmdOld.vol = target.vol = static_cast<ModCommand::VOL>(vol); + } + } + + SetSelToCursor(); + + if(oldcmd != target) + { + SetModified(false); + InvalidateCell(m_Cursor); + UpdateIndicator(); + } + + // Cursor step for command letter + if(!target.IsPcNote() && !isDigit && m_nSpacing > 0 && !IsLiveRecord() && TrackerSettings::Instance().patternStepCommands) + { + if(m_Cursor.GetRow() + m_nSpacing < pSndFile->Patterns[m_nPattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)) + { + SetCurrentRow(m_Cursor.GetRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0); + } + } +} + + +void CViewPattern::SetSpacing(int n) +{ + if(static_cast<UINT>(n) != m_nSpacing) + { + m_nSpacing = static_cast<UINT>(n); + PostCtrlMessage(CTRLMSG_SETSPACING, m_nSpacing); + } +} + + +// Enter an effect letter in the pattern +void CViewPattern::TempEnterFX(ModCommand::COMMAND c, int v) +{ + CSoundFile *pSndFile = GetSoundFile(); + + if(pSndFile == nullptr || !IsEditingEnabled_bmsg()) + { + return; + } + + ModCommand &target = GetCursorCommand(); + ModCommand oldcmd = target; // This is the command we are about to overwrite + + PrepareUndo(m_Cursor, m_Cursor, "Effect Entry"); + + if(target.IsPcNote()) + { + if(EnterPCNoteValue(c, target, &ModCommand::GetValueEffectCol, &ModCommand::SetValueEffectCol)) + m_PCNoteEditMemory = target; + } else if(pSndFile->GetModSpecifications().HasCommand(c)) + { + if(c != CMD_NONE) + { + if((c == m_cmdOld.command) && (!target.param) && (target.command == CMD_NONE)) + { + target.param = m_cmdOld.param; + } else + { + m_cmdOld.param = 0; + } + m_cmdOld.command = c; + } + target.command = c; + if(v >= 0) + { + target.param = static_cast<ModCommand::PARAM>(v); + } + + // Check for MOD/XM Speed/Tempo command + if((pSndFile->GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) + && (target.command == CMD_SPEED || target.command == CMD_TEMPO)) + { + target.command = static_cast<ModCommand::COMMAND>((target.param <= pSndFile->GetModSpecifications().speedMax) ? CMD_SPEED : CMD_TEMPO); + } + } + + SetSelToCursor(); + + if(oldcmd != target) + { + SetModified(false); + InvalidateCell(m_Cursor); + UpdateIndicator(); + } + + // Cursor step for command letter + if(!target.IsPcNote() && m_nSpacing > 0 && !IsLiveRecord() && TrackerSettings::Instance().patternStepCommands) + { + if(m_Cursor.GetRow() + m_nSpacing < pSndFile->Patterns[m_nPattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)) + { + SetCurrentRow(m_Cursor.GetRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0); + } + } +} + + +// Enter an effect param in the pattenr +void CViewPattern::TempEnterFXparam(int v) +{ + CSoundFile *pSndFile = GetSoundFile(); + + if(pSndFile == nullptr || !IsEditingEnabled_bmsg()) + { + return; + } + + ModCommand &target = GetCursorCommand(); + ModCommand oldcmd = target; // This is the command we are about to overwrite + + PrepareUndo(m_Cursor, m_Cursor, "Parameter Entry"); + + if(target.IsPcNote()) + { + if(EnterPCNoteValue(v, target, &ModCommand::GetValueEffectCol, &ModCommand::SetValueEffectCol)) + m_PCNoteEditMemory = target; + } else + { + + target.param = static_cast<ModCommand::PARAM>((target.param << 4) | v); + if(target.command == m_cmdOld.command) + { + m_cmdOld.param = target.param; + } + + // Check for MOD/XM Speed/Tempo command + if((pSndFile->GetType() & (MOD_TYPE_MOD | MOD_TYPE_XM)) + && (target.command == CMD_SPEED || target.command == CMD_TEMPO)) + { + target.command = static_cast<ModCommand::COMMAND>((target.param <= pSndFile->GetModSpecifications().speedMax) ? CMD_SPEED : CMD_TEMPO); + } + } + + SetSelToCursor(); + + if(target != oldcmd) + { + SetModified(false); + InvalidateCell(m_Cursor); + UpdateIndicator(); + } +} + + +// Stop a note that has been entered +void CViewPattern::TempStopNote(ModCommand::NOTE note, const bool fromMidi, bool chordMode) +{ + CModDoc *pModDoc = GetDocument(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pModDoc == nullptr || pMainFrm == nullptr || !ModCommand::IsNote(note)) + { + return; + } + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + { + return; + } + const CModSpecifications &specs = sndFile.GetModSpecifications(); + Limit(note, specs.noteMin, specs.noteMax); + + const bool liveRecord = IsLiveRecord(); + const bool isSplit = IsNoteSplit(note); + UINT ins = 0; + chordMode = chordMode && (m_prevChordNote != NOTE_NONE); + + auto &activeNoteMap = isSplit ? m_splitActiveNoteChannel : m_activeNoteChannel; + const CHANNELINDEX nChnCursor = GetCurrentChannel(); + const CHANNELINDEX nChn = chordMode ? m_chordPatternChannels[0] : (activeNoteMap[note] < sndFile.GetNumChannels() ? activeNoteMap[note] : nChnCursor); + + CHANNELINDEX noteChannels[MPTChord::notesPerChord] = {nChn}; + ModCommand::NOTE notes[MPTChord::notesPerChord] = {note}; + int numNotes = 1; + + if(pModDoc) + { + if(isSplit) + { + ins = pModDoc->GetSplitKeyboardSettings().splitInstrument; + if(pModDoc->GetSplitKeyboardSettings().octaveLink) + { + int trNote = note + 12 * pModDoc->GetSplitKeyboardSettings().octaveModifier; + Limit(trNote, specs.noteMin, specs.noteMax); + note = static_cast<ModCommand::NOTE>(trNote); + } + } + if(!ins) + ins = GetCurrentInstrument(); + if(!ins) + ins = m_fallbackInstrument; + + if(chordMode) + { + m_Status.reset(psChordPlaying); + + numNotes = ConstructChord(note, notes, m_prevChordBaseNote); + if(!numNotes) + { + return; + } + for(int i = 0; i < numNotes; i++) + { + pModDoc->NoteOff(notes[i], true, static_cast<INSTRUMENTINDEX>(ins), m_noteChannel[notes[i] - NOTE_MIN]); + m_noteChannel[notes[i] - NOTE_MIN] = CHANNELINDEX_INVALID; + m_baPlayingNote.reset(notes[i]); + noteChannels[i] = m_chordPatternChannels[i]; + } + m_prevChordNote = NOTE_NONE; + } else + { + m_baPlayingNote.reset(note); + pModDoc->NoteOff(note, ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_NOTEFADE) || sndFile.GetNumInstruments() == 0), static_cast<INSTRUMENTINDEX>(ins), m_noteChannel[note - NOTE_MIN]); + m_noteChannel[note - NOTE_MIN] = CHANNELINDEX_INVALID; + } + } + + // Enter note off in pattern? + if(!ModCommand::IsNote(note)) + return; + if(m_Cursor.GetColumnType() > PatternCursor::instrColumn && (chordMode || !fromMidi)) + return; + if(!pModDoc || !pMainFrm || !(IsEditingEnabled())) + return; + + activeNoteMap[note] = NOTE_CHANNEL_MAP_INVALID; //unlock channel + + if(!((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_KBDNOTEOFF) || fromMidi)) + { + // We don't want to write the note-off into the pattern if this feature is disabled and we're not recording from MIDI. + return; + } + + // -- write sdx if playing live + const bool usePlaybackPosition = (!chordMode) && (liveRecord && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_AUTODELAY)); + + //Work out where to put the note off + PatternEditPos editPos = GetEditPos(sndFile, usePlaybackPosition); + + const bool doQuantize = (liveRecord || (fromMidi && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN))) && TrackerSettings::Instance().recordQuantizeRows != 0; + if(doQuantize) + { + QuantizeRow(editPos.pattern, editPos.row); + } + + ModCommand *pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn); + // Don't overwrite: + if(pTarget->note != NOTE_NONE || pTarget->instr || pTarget->volcmd != VOLCMD_NONE) + { + // If there's a note in the current location and the song is playing and following, + // the user probably just tapped the key - let's try the next row down. + editPos.row++; + if(pTarget->note == note && liveRecord && sndFile.Patterns[editPos.pattern].IsValidRow(editPos.row)) + { + pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn); + if(pTarget->note != NOTE_NONE || (!chordMode && (pTarget->instr || pTarget->volcmd))) + return; + } else + { + return; + } + } + + bool modified = false; + for(int i = 0; i < numNotes; i++) + { + if(m_previousNote[noteChannels[i]] != notes[i]) + { + // This might be a note-off from a past note, but since we already hit a new note on this channel, we ignore it. + continue; + } + + if(!modified) + { + pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, nChn, editPos.row, noteChannels[numNotes - 1] - nChn + 1, 1, "Note Stop Entry"); + modified = true; + } + + pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, noteChannels[i]); + + // -- write sdx if playing live + if(usePlaybackPosition && m_nPlayTick && pTarget->command == CMD_NONE && !doQuantize) + { + pTarget->command = CMD_S3MCMDEX; + if(!specs.HasCommand(CMD_S3MCMDEX)) + pTarget->command = CMD_MODCMDEX; + pTarget->param = static_cast<ModCommand::PARAM>(0xD0 | std::min(uint8(0xF), mpt::saturate_cast<uint8>(m_nPlayTick))); + } + + //Enter note off + if(sndFile.GetModSpecifications().hasNoteOff && (sndFile.GetNumInstruments() > 0 || !sndFile.GetModSpecifications().hasNoteCut)) + { + // === + // Not used in sample (if module format supports ^^^ instead) + pTarget->note = NOTE_KEYOFF; + } else if(sndFile.GetModSpecifications().hasNoteCut) + { + // ^^^ + pTarget->note = NOTE_NOTECUT; + } else + { + // we don't have anything to cut (MOD format) - use volume or ECx + if(usePlaybackPosition && m_nPlayTick && !doQuantize) // ECx + { + pTarget->command = CMD_S3MCMDEX; + if(!specs.HasCommand(CMD_S3MCMDEX)) + pTarget->command = CMD_MODCMDEX; + pTarget->param = static_cast<ModCommand::PARAM>(0xC0 | std::min(uint8(0xF), mpt::saturate_cast<uint8>(m_nPlayTick))); + } else // C00 + { + pTarget->note = NOTE_NONE; + pTarget->command = CMD_VOLUME; + pTarget->param = 0; + } + } + pTarget->instr = 0; // Instrument numbers next to note-offs can do all kinds of weird things in XM files, and they are pointless anyway. + pTarget->volcmd = VOLCMD_NONE; + pTarget->vol = 0; + } + if(!modified) + return; + + SetModified(false); + + if(editPos.pattern == m_nPattern) + { + InvalidateRow(editPos.row); + } else + { + InvalidatePattern(); + } + + // Update only if not recording live. + if(!liveRecord) + { + UpdateIndicator(); + } + + return; +} + + +// Enter an octave number in the pattern +void CViewPattern::TempEnterOctave(int val) +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr) + { + return; + } + + const ModCommand &target = GetCursorCommand(); + if(target.IsNote()) + { + int groupSize = GetDocument()->GetInstrumentGroupSize(target.instr); + // The following might look a bit convoluted... This is mostly because the "middle-C" in + // custom tunings always has octave 5, no matter how many octaves the tuning actually has. + int note = mpt::wrapping_modulo(target.note - NOTE_MIDDLEC, groupSize) + (val - 5) * groupSize + NOTE_MIDDLEC; + Limit(note, NOTE_MIN, NOTE_MAX); + TempEnterNote(static_cast<ModCommand::NOTE>(note)); + // Memorize note for key-up + ASSERT(size_t(val) < m_octaveKeyMemory.size()); + m_octaveKeyMemory[val] = target.note; + } +} + + +// Stop note that has been triggered by entering an octave in the pattern. +void CViewPattern::TempStopOctave(int val) +{ + ASSERT(size_t(val) < m_octaveKeyMemory.size()); + if(m_octaveKeyMemory[val] != NOTE_NONE) + { + TempStopNote(m_octaveKeyMemory[val]); + m_octaveKeyMemory[val] = NOTE_NONE; + } +} + + +// Enter an instrument number in the pattern +void CViewPattern::TempEnterIns(int val) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !IsEditingEnabled_bmsg()) + { + return; + } + + PrepareUndo(m_Cursor, m_Cursor, "Instrument Entry"); + + ModCommand &target = GetCursorCommand(); + ModCommand oldcmd = target; // This is the command we are about to overwrite + + UINT instr = target.instr, nTotalMax, nTempMax; + if(target.IsPcNote()) // this is a plugin index + { + nTotalMax = MAX_MIXPLUGINS + 1; + nTempMax = MAX_MIXPLUGINS + 1; + } else if(pSndFile->GetNumInstruments() > 0) // this is an instrument index + { + nTotalMax = MAX_INSTRUMENTS; + nTempMax = pSndFile->GetNumInstruments(); + } else + { + nTotalMax = MAX_SAMPLES; + nTempMax = pSndFile->GetNumSamples(); + } + + instr = ((instr * 10) + val) % 1000; + if(instr >= nTotalMax) + instr = instr % 100; + if(nTempMax < 100) // if we're using samples & have less than 100 samples + instr = instr % 100; // or if we're using instruments and have less than 100 instruments + // --> ensure the entered instrument value is less than 100. + target.instr = static_cast<ModCommand::INSTR>(instr); + + SetSelToCursor(); + + if(target != oldcmd) + { + SetModified(false); + InvalidateCell(m_Cursor); + UpdateIndicator(); + } + + if(target.IsPcNote()) + { + m_PCNoteEditMemory = target; + } +} + + +// Enter a note in the pattern +void CViewPattern::TempEnterNote(ModCommand::NOTE note, int vol, bool fromMidi) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + if(pMainFrm == nullptr || pModDoc == nullptr) + { + return; + } + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + { + return; + } + + if(note < NOTE_MIN_SPECIAL) + { + Limit(note, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax); + } + + // Special case: Convert note off commands to C00 for MOD files + if((sndFile.GetType() == MOD_TYPE_MOD) && (note == NOTE_NOTECUT || note == NOTE_FADE || note == NOTE_KEYOFF)) + { + TempEnterFX(CMD_VOLUME, 0); + return; + } + + // Check whether the module format supports the note. + if(sndFile.GetModSpecifications().HasNote(note) == false) + { + return; + } + + const bool liveRecord = IsLiveRecord(); + const bool usePlaybackPosition = (liveRecord && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_AUTODELAY) && !sndFile.m_SongFlags[SONG_STEP]); + const bool isSpecial = note >= NOTE_MIN_SPECIAL; + const bool isSplit = IsNoteSplit(note); + + PatternEditPos editPos = GetEditPos(sndFile, usePlaybackPosition); + + const bool recordEnabled = IsEditingEnabled(); + CHANNELINDEX nChn = GetCurrentChannel(); + + auto recordGroup = pModDoc->GetChannelRecordGroup(nChn); + + if(!isSpecial && pModDoc->GetSplitKeyboardSettings().IsSplitActive() + && ((recordGroup == RecordGroup::Group1 && isSplit) || (recordGroup == RecordGroup::Group2 && !isSplit))) + { + // Record group 1 should be used for normal notes, record group 2 for split notes. + // If there are any channels assigned to the "other" record group, we switch to another channel. + auto otherGroup = (recordGroup == RecordGroup::Group1) ? RecordGroup::Group2 : RecordGroup::Group1; + const CHANNELINDEX newChannel = FindGroupRecordChannel(otherGroup, true); + if(newChannel != CHANNELINDEX_INVALID) + { + // Found a free channel, switch to other record group. + nChn = newChannel; + recordGroup = otherGroup; + } + } + + // -- Chord autodetection: step back if we just entered a note + if(recordEnabled && recordGroup != RecordGroup::NoGroup && !liveRecord && !ModCommand::IsPcNote(note) && m_nSpacing > 0) + { + const auto &order = Order(); + if((timeGetTime() - m_autoChordStartTime) < TrackerSettings::Instance().gnAutoChordWaitTime + && order.IsValidPat(m_autoChordStartOrder) + && sndFile.Patterns[order[m_autoChordStartOrder]].IsValidRow(m_autoChordStartRow)) + { + const auto pattern = order[m_autoChordStartOrder]; + if(pattern != editPos.pattern) + { + SetCurrentOrder(m_autoChordStartOrder); + SetCurrentPattern(pattern, m_autoChordStartRow); + } + editPos.pattern = pattern; + editPos.row = m_autoChordStartRow; + } else + { + m_autoChordStartRow = ROWINDEX_INVALID; + m_autoChordStartOrder = ORDERINDEX_INVALID; + } + m_autoChordStartTime = timeGetTime(); + if(m_autoChordStartOrder == ORDERINDEX_INVALID || m_autoChordStartRow == ROWINDEX_INVALID) + { + m_autoChordStartOrder = editPos.order; + m_autoChordStartRow = editPos.row; + } + } + + // Quantize + const bool doQuantize = (liveRecord || (fromMidi && (TrackerSettings::Instance().m_dwMidiSetup & MIDISETUP_PLAYPATTERNONMIDIIN))) && TrackerSettings::Instance().recordQuantizeRows != 0; + if(doQuantize) + { + QuantizeRow(editPos.pattern, editPos.row); + // "Grace notes" are stuffed into the next row, if possible + if(sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn)->IsNote() && editPos.row < sndFile.Patterns[editPos.pattern].GetNumRows() - 1) + { + editPos.row++; + } + } + + // -- Work out where to put the new note + ModCommand *pTarget = sndFile.Patterns[editPos.pattern].GetpModCommand(editPos.row, nChn); + ModCommand newcmd = *pTarget; + + // Param control 'note' + if(ModCommand::IsPcNote(note)) + { + if(!pTarget->IsPcNote()) + { + // We're overwriting a normal cell with a PC note. + newcmd = m_PCNoteEditMemory; + if((pTarget->command == CMD_MIDI || pTarget->command == CMD_SMOOTHMIDI) && pTarget->param < 128) + { + newcmd.SetValueEffectCol(static_cast<decltype(newcmd.GetValueEffectCol())>(Util::muldivr(pTarget->param, ModCommand::maxColumnValue, 127))); + if(!newcmd.instr) + newcmd.instr = sndFile.ChnSettings[nChn].nMixPlugin; + auto activeMacro = sndFile.m_PlayState.Chn[nChn].nActiveMacro; + if(!newcmd.GetValueVolCol() && sndFile.m_MidiCfg.GetParameteredMacroType(activeMacro) == kSFxPlugParam) + { + PlugParamIndex plugParam = sndFile.m_MidiCfg.MacroToPlugParam(sndFile.m_PlayState.Chn[nChn].nActiveMacro); + if(plugParam < ModCommand::maxColumnValue) + newcmd.SetValueVolCol(static_cast<decltype(newcmd.GetValueVolCol())>(plugParam)); + } + } + } else if(recordEnabled) + { + // Pick up current entry to update PC note edit memory. + m_PCNoteEditMemory = newcmd; + } + + newcmd.note = note; + } else + { + + // Are we overwriting a PC note here? + if(pTarget->IsPcNote()) + { + newcmd.Clear(); + } + + // -- write note and instrument data. + HandleSplit(newcmd, note); + + // Nice idea actually: Use lower section of the keyboard to play chords (but it won't work 100% correctly this way...) + /*if(isSplit) + { + TempEnterChord(note); + return; + }*/ + + // -- write vol data + int volWrite = -1; + if(vol >= 0 && vol <= 64 && !(isSplit && pModDoc->GetSplitKeyboardSettings().splitVolume)) //write valid volume, as long as there's no split volume override. + { + volWrite = vol; + } else if(isSplit && pModDoc->GetSplitKeyboardSettings().splitVolume) //cater for split volume override. + { + if(pModDoc->GetSplitKeyboardSettings().splitVolume > 0 && pModDoc->GetSplitKeyboardSettings().splitVolume <= 64) + { + volWrite = pModDoc->GetSplitKeyboardSettings().splitVolume; + } + } + + if(volWrite != -1 && !isSpecial) + { + if(sndFile.GetModSpecifications().HasVolCommand(VOLCMD_VOLUME)) + { + newcmd.volcmd = VOLCMD_VOLUME; + newcmd.vol = (ModCommand::VOL)volWrite; + } else + { + newcmd.command = CMD_VOLUME; + newcmd.param = (ModCommand::PARAM)volWrite; + } + } + + // -- write sdx if playing live + if(usePlaybackPosition && m_nPlayTick && !doQuantize) // avoid SD0 which will be mis-interpreted + { + if(newcmd.command == CMD_NONE) //make sure we don't overwrite any existing commands. + { + newcmd.command = CMD_S3MCMDEX; + if(!sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX)) + newcmd.command = CMD_MODCMDEX; + uint8 maxSpeed = 0x0F; + if(m_nTicksOnRow > 0) + maxSpeed = std::min(uint8(0x0F), mpt::saturate_cast<uint8>(m_nTicksOnRow - 1)); + newcmd.param = static_cast<ModCommand::PARAM>(0xD0 | std::min(maxSpeed, mpt::saturate_cast<uint8>(m_nPlayTick))); + } + } + + // Note cut/off/fade: erase instrument number + if(newcmd.note >= NOTE_MIN_SPECIAL) + newcmd.instr = 0; + } + + // -- if recording, create undo point and write out modified command. + const bool modified = (recordEnabled && *pTarget != newcmd); + if(modified) + { + pModDoc->GetPatternUndo().PrepareUndo(editPos.pattern, nChn, editPos.row, 1, 1, "Note Entry"); + *pTarget = newcmd; + } + + // -- play note + if(((TrackerSettings::Instance().m_dwPatternSetup & (PATTERN_PLAYNEWNOTE | PATTERN_PLAYEDITROW)) || !recordEnabled) && !newcmd.IsPcNote()) + { + const bool playWholeRow = ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !liveRecord); + if(playWholeRow) + { + // play the whole row in "step mode" + PatternStep(editPos.row); + if(recordEnabled && newcmd.IsNote()) + m_noteChannel[newcmd.note - NOTE_MIN] = nChn; + } + if(!playWholeRow || !recordEnabled) + { + // NOTE: This code is *also* used for the PATTERN_PLAYEDITROW edit mode because of some unforseeable race conditions when modifying pattern data. + // We have to use this code when editing is disabled or else we will get some stupid hazards, because we would first have to write the new note + // data to the pattern and then remove it again - but often, it is actually removed before the row is parsed by the soundlib. + + // just play the newly inserted note using the already specified instrument... + ModCommand::INSTR playIns = newcmd.instr; + if(!playIns && ModCommand::IsNoteOrEmpty(note)) + { + // ...or one that can be found on a previous row of this pattern. + ModCommand *search = pTarget; + ROWINDEX srow = editPos.row; + while(srow-- > 0) + { + search -= sndFile.GetNumChannels(); + if(search->instr && !search->IsPcNote()) + { + playIns = search->instr; + m_fallbackInstrument = playIns; //used to figure out which instrument to stop on key release. + break; + } + } + } + PlayNote(newcmd.note, playIns, 4 * vol, nChn); + } + } + + if(newcmd.IsNote()) + { + m_previousNote[nChn] = note; + } + + // -- if recording, handle post note entry behaviour (move cursor etc..) + if(recordEnabled) + { + PatternCursor sel(editPos.row, nChn, m_Cursor.GetColumnType()); + if(!liveRecord) + { + // Update only when not recording live. + SetCurSel(sel); + } + + if(modified) // Has it really changed? + { + SetModified(false); + if(editPos.pattern == m_nPattern) + InvalidateCell(sel); + else + InvalidatePattern(); + if(!liveRecord) + { + // Update only when not recording live. + UpdateIndicator(); + } + } + + // Set new cursor position (edit step aka row spacing) + if(!liveRecord) + { + if(m_nSpacing > 0) + { + if(editPos.row + m_nSpacing < sndFile.Patterns[editPos.pattern].GetNumRows() || (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL)) + { + SetCurrentRow(editPos.row + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0); + } + } + + SetSelToCursor(); + } + + if(newcmd.IsPcNote()) + { + // Nothing to do here anymore. + return; + } + + auto &activeNoteMap = isSplit ? m_splitActiveNoteChannel : m_activeNoteChannel; + if(newcmd.note <= NOTE_MAX) + activeNoteMap[newcmd.note] = static_cast<decltype(m_activeNoteChannel)::value_type>(nChn); + + if(recordGroup != RecordGroup::NoGroup) + { + // Move to next channel in record group + nChn = FindGroupRecordChannel(recordGroup, false, nChn + 1); + if(nChn != CHANNELINDEX_INVALID) + { + SetCurrentColumn(nChn); + } + } + } +} + + +void CViewPattern::PlayNote(ModCommand::NOTE note, ModCommand::INSTR instr, int volume, CHANNELINDEX channel) +{ + CModDoc *modDoc = GetDocument(); + modDoc->PlayNote(PlayNoteParam(note).Instrument(instr).Volume(volume).Channel(channel).CheckNNA(m_baPlayingNote), &m_noteChannel); +} + + +void CViewPattern::PreviewNote(ROWINDEX row, CHANNELINDEX channel) +{ + const ModCommand &m = *GetSoundFile()->Patterns[m_nPattern].GetpModCommand(row, channel); + if(m.IsNote() && m.instr) + { + int vol = -1; + if(m.command == CMD_VOLUME) + vol = m.param * 4u; + else if(m.volcmd == VOLCMD_VOLUME) + vol = m.vol * 4u; + // Note-off any previews from this channel first + ModCommand::NOTE note = NOTE_MIN; + const auto &channels = GetSoundFile()->m_PlayState.Chn; + for(auto &chn : m_noteChannel) + { + if(chn != CHANNELINDEX_INVALID && channels[chn].isPreviewNote && channels[chn].nMasterChn == channel + 1) + { + GetDocument()->NoteOff(note, false, m.instr, chn); + } + note++; + } + PlayNote(m.note, m.instr, vol, channel); + } +} + + +// Construct a chord from the chord presets. Returns number of notes in chord. +int CViewPattern::ConstructChord(int note, ModCommand::NOTE (&outNotes)[MPTChord::notesPerChord], ModCommand::NOTE baseNote) +{ + const MPTChords &chords = TrackerSettings::GetChords(); + UINT chordNum = note - GetBaseNote(); + + if(chordNum >= chords.size()) + { + return 0; + } + const MPTChord &chord = chords[chordNum]; + + const bool relativeMode = (chord.key == MPTChord::relativeMode); // Notes are relative to a previously entered note in the pattern + ModCommand::NOTE key; + if(relativeMode) + { + // Relative mode: Use pattern note as base note. + // If there is no valid note in the pattern: Use shortcut note as relative base note + key = ModCommand::IsNote(baseNote) ? baseNote : static_cast<ModCommand::NOTE>(note); + } else + { + // Default mode: Use base key + key = GetNoteWithBaseOctave(chord.key); + } + if(!ModCommand::IsNote(key)) + { + return 0; + } + + int numNotes = 0; + const CModSpecifications &specs = GetSoundFile()->GetModSpecifications(); + if(specs.HasNote(key)) + { + outNotes[numNotes++] = key; + } + + int32 baseKey = key - NOTE_MIN; + if(!relativeMode) + { + // Only use octave information from the base key + baseKey = (baseKey / 12) * 12; + } + + for(auto cnote : chord.notes) + { + if(cnote != MPTChord::noNote) + { + int32 chordNote = baseKey + cnote + NOTE_MIN; + if(chordNote >= NOTE_MIN && chordNote <= NOTE_MAX && specs.HasNote(static_cast<ModCommand::NOTE>(chordNote))) + { + outNotes[numNotes++] = static_cast<ModCommand::NOTE>(chordNote); + } + } + } + return numNotes; +} + + +// Enter a chord in the pattern +void CViewPattern::TempEnterChord(ModCommand::NOTE note) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + if(pMainFrm == nullptr || pModDoc == nullptr) + { + return; + } + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + { + return; + } + + const CHANNELINDEX chn = GetCurrentChannel(); + const PatternRow rowBase = sndFile.Patterns[m_nPattern].GetRow(GetCurrentRow()); + + ModCommand::NOTE chordNotes[MPTChord::notesPerChord], baseNote = rowBase[chn].note; + if(!ModCommand::IsNote(baseNote)) + { + baseNote = m_prevChordBaseNote; + } + int numNotes = ConstructChord(note, chordNotes, baseNote); + if(!numNotes) + { + return; + } + + // Save old row contents + std::vector<ModCommand> newRow(rowBase, rowBase + sndFile.GetNumChannels()); + + const bool liveRecord = IsLiveRecord(); + const bool recordEnabled = IsEditingEnabled(); + bool modified = false; + + // -- establish note data + HandleSplit(newRow[chn], note); + const auto recordGroup = pModDoc->GetChannelRecordGroup(chn); + + CHANNELINDEX curChn = chn; + for(int i = 0; i < numNotes; i++) + { + // Find appropriate channel + while(curChn < sndFile.GetNumChannels() && pModDoc->GetChannelRecordGroup(curChn) != recordGroup) + { + curChn++; + } + if(curChn >= sndFile.GetNumChannels()) + { + numNotes = i; + break; + } + + m_chordPatternChannels[i] = curChn; + + ModCommand &m = newRow[curChn]; + m_previousNote[curChn] = m.note = chordNotes[i]; + if(newRow[chn].instr) + { + m.instr = newRow[chn].instr; + } + + if(rowBase[chn] != m) + { + modified = true; + } + curChn++; + } + + m_Status.set(psChordPlaying); + + // -- write notedata + if(recordEnabled) + { + SetSelToCursor(); + + if(modified) + { + // Simply backup the whole row. + pModDoc->GetPatternUndo().PrepareUndo(m_nPattern, chn, GetCurrentRow(), sndFile.GetNumChannels(), 1, "Chord Entry"); + + for(CHANNELINDEX n = 0; n < sndFile.GetNumChannels(); n++) + { + rowBase[n] = newRow[n]; + } + SetModified(false); + InvalidateRow(); + UpdateIndicator(); + } + } + + // -- play note + if((TrackerSettings::Instance().m_dwPatternSetup & (PATTERN_PLAYNEWNOTE | PATTERN_PLAYEDITROW)) || !recordEnabled) + { + if(m_prevChordNote != NOTE_NONE) + { + TempStopChord(m_prevChordNote); + } + + const bool playWholeRow = ((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !liveRecord); + if(playWholeRow) + { + // play the whole row in "step mode" + PatternStep(GetCurrentRow()); + if(recordEnabled) + { + for(int i = 0; i < numNotes; i++) + { + m_noteChannel[chordNotes[i] - NOTE_MIN] = m_chordPatternChannels[i]; + } + } + } + if(!playWholeRow || !recordEnabled) + { + // NOTE: This code is *also* used for the PATTERN_PLAYEDITROW edit mode because of some unforseeable race conditions when modifying pattern data. + // We have to use this code when editing is disabled or else we will get some stupid hazards, because we would first have to write the new note + // data to the pattern and then remove it again - but often, it is actually removed before the row is parsed by the soundlib. + // just play the newly inserted notes... + + const ModCommand &firstNote = rowBase[chn]; + ModCommand::INSTR playIns = 0; + if(firstNote.instr) + { + // ...using the already specified instrument + playIns = firstNote.instr; + } else if(!firstNote.instr) + { + // ...or one that can be found on a previous row of this pattern. + const ModCommand *search = &firstNote; + ROWINDEX srow = GetCurrentRow(); + while(srow-- > 0) + { + search -= sndFile.GetNumChannels(); + if(search->instr) + { + playIns = search->instr; + m_fallbackInstrument = playIns; //used to figure out which instrument to stop on key release. + break; + } + } + } + for(int i = 0; i < numNotes; i++) + { + pModDoc->PlayNote(PlayNoteParam(chordNotes[i]).Instrument(playIns).Channel(chn).CheckNNA(m_baPlayingNote), &m_noteChannel); + } + } + } // end play note + + m_prevChordNote = note; + m_prevChordBaseNote = baseNote; + + // Set new cursor position (edit step aka row spacing) - only when not recording live + if(recordEnabled && !liveRecord) + { + if(m_nSpacing > 0) + { + // Shift from entering chord may have triggered this flag, which will prevent us from wrapping to the next pattern. + m_Status.reset(psKeyboardDragSelect); + SetCurrentRow(GetCurrentRow() + m_nSpacing, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_CONTSCROLL) != 0); + } + SetSelToCursor(); + } +} + + +// Translate incoming MIDI aftertouch messages to pattern commands +void CViewPattern::EnterAftertouch(ModCommand::NOTE note, int atValue) +{ + if(TrackerSettings::Instance().aftertouchBehaviour == atDoNotRecord || !IsEditingEnabled()) + return; + + const CHANNELINDEX numChannels = GetSoundFile()->GetNumChannels(); + std::set<CHANNELINDEX> channels; + + if(ModCommand::IsNote(note)) + { + // For polyphonic aftertouch, map the aftertouch note to the correct pattern channel. + const auto &activeNoteMap = IsNoteSplit(note) ? m_splitActiveNoteChannel : m_activeNoteChannel; + if(activeNoteMap[note] < numChannels) + { + channels.insert(activeNoteMap[note]); + } else + { + // Couldn't find the channel that belongs to this note... Don't bother writing aftertouch messages. + // This is actually necessary, because it is possible that the last aftertouch message for a note + // is received after the key-off event, in which case OpenMPT won't know anymore on which channel + // that particular note was, so it will just put the message on some other channel. We don't want that! + return; + } + } else + { + for(const auto ¬eMap : { m_activeNoteChannel, m_splitActiveNoteChannel }) + { + for(const auto chn : noteMap) + { + if(chn < numChannels) + channels.insert(chn); + } + } + if(channels.empty()) + channels.insert(m_Cursor.GetChannel()); + } + + Limit(atValue, 0, 127); + + const PatternCursor endOfRow{ m_Cursor.GetRow(), static_cast<CHANNELINDEX>(numChannels - 1u), PatternCursor::lastColumn }; + const auto &specs = GetSoundFile()->GetModSpecifications(); + bool first = true, modified = false; + for(const auto chn : channels) + { + const PatternCursor cursor{ m_Cursor.GetRow(), chn }; + ModCommand &target = GetModCommand(cursor); + ModCommand newCommand = target; + + if(target.IsPcNote()) + continue; + + switch(TrackerSettings::Instance().aftertouchBehaviour) + { + case atRecordAsVolume: + // Record aftertouch messages as volume commands + if(specs.HasVolCommand(VOLCMD_VOLUME)) + { + if(newCommand.volcmd == VOLCMD_NONE || newCommand.volcmd == VOLCMD_VOLUME) + { + newCommand.volcmd = VOLCMD_VOLUME; + newCommand.vol = static_cast<ModCommand::VOL>((atValue * 64 + 64) / 127); + } + } else if(specs.HasCommand(CMD_VOLUME)) + { + if(newCommand.command == CMD_NONE || newCommand.command == CMD_VOLUME) + { + newCommand.command = CMD_VOLUME; + newCommand.param = static_cast<ModCommand::PARAM>((atValue * 64 + 64) / 127); + } + } + break; + + case atRecordAsMacro: + // Record aftertouch messages as MIDI Macros + if(newCommand.command == CMD_NONE || newCommand.command == CMD_SMOOTHMIDI || newCommand.command == CMD_MIDI) + { + auto cmd = + specs.HasCommand(CMD_SMOOTHMIDI) ? CMD_SMOOTHMIDI : + specs.HasCommand(CMD_MIDI) ? CMD_MIDI : + CMD_NONE; + + if(cmd != CMD_NONE) + { + newCommand.command = static_cast<ModCommand::COMMAND>(cmd); + newCommand.param = static_cast<ModCommand::PARAM>(atValue); + } + } + break; + } + + if(target != newCommand) + { + if(first) + PrepareUndo(cursor, endOfRow, "Aftertouch Entry"); + first = false; + modified = true; + target = newCommand; + + InvalidateCell(cursor); + } + } + if(modified) + { + SetModified(false); + UpdateIndicator(); + } +} + + +// Apply quantization factor to given row. +void CViewPattern::QuantizeRow(PATTERNINDEX &pat, ROWINDEX &row) const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || TrackerSettings::Instance().recordQuantizeRows == 0) + { + return; + } + + const ROWINDEX currentTick = m_nTicksOnRow * row + m_nPlayTick; + const ROWINDEX ticksPerNote = TrackerSettings::Instance().recordQuantizeRows * m_nTicksOnRow; + + // Previous quantization step + const ROWINDEX quantLow = (currentTick / ticksPerNote) * ticksPerNote; + // Next quantization step + const ROWINDEX quantHigh = (1 + (currentTick / ticksPerNote)) * ticksPerNote; + + if(currentTick - quantLow < quantHigh - currentTick) + { + row = quantLow / m_nTicksOnRow; + } else + { + row = quantHigh / m_nTicksOnRow; + } + + if(!sndFile->Patterns[pat].IsValidRow(row)) + { + // Quantization exceeds current pattern, try stuffing note into next pattern instead. + PATTERNINDEX nextPat = sndFile->m_SongFlags[SONG_PATTERNLOOP] ? m_nPattern : GetNextPattern(); + if(nextPat != PATTERNINDEX_INVALID) + { + pat = nextPat; + row = 0; + } else + { + row = sndFile->Patterns[pat].GetNumRows() - 1; + } + } +} + + +// Get previous pattern in order list +PATTERNINDEX CViewPattern::GetPrevPattern() const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile != nullptr) + { + const auto &order = Order(); + const ORDERINDEX curOrder = GetCurrentOrder(); + if(curOrder > 0 && m_nPattern == order[curOrder]) + { + const ORDERINDEX nextOrder = order.GetPreviousOrderIgnoringSkips(curOrder); + const PATTERNINDEX nextPat = order[nextOrder]; + if(sndFile->Patterns.IsValidPat(nextPat) && sndFile->Patterns[nextPat].GetNumRows()) + { + return nextPat; + } + } + } + return PATTERNINDEX_INVALID; +} + + +// Get follow-up pattern in order list +PATTERNINDEX CViewPattern::GetNextPattern() const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile != nullptr) + { + const auto &order = Order(); + const ORDERINDEX curOrder = GetCurrentOrder(); + if(curOrder + 1 < order.GetLength() && m_nPattern == order[curOrder]) + { + const ORDERINDEX nextOrder = order.GetNextOrderIgnoringSkips(curOrder); + const PATTERNINDEX nextPat = order[nextOrder]; + if(sndFile->Patterns.IsValidPat(nextPat) && sndFile->Patterns[nextPat].GetNumRows()) + { + return nextPat; + } + } + } + return PATTERNINDEX_INVALID; +} + + +void CViewPattern::OnSetQuantize() +{ + CInputDlg dlg(this, _T("Quantize amount in rows for live recording (0 to disable):"), 0, MAX_PATTERN_ROWS, TrackerSettings::Instance().recordQuantizeRows); + if(dlg.DoModal()) + { + TrackerSettings::Instance().recordQuantizeRows = static_cast<ROWINDEX>(dlg.resultAsInt); + } +} + + +void CViewPattern::OnLockPatternRows() +{ + CSoundFile &sndFile = *GetSoundFile(); + if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) + { + sndFile.m_lockRowStart = m_Selection.GetStartRow(); + sndFile.m_lockRowEnd = m_Selection.GetEndRow(); + } else + { + sndFile.m_lockRowStart = sndFile.m_lockRowEnd = ROWINDEX_INVALID; + } + InvalidatePattern(true, true); +} + + +// Find a free channel for a record group, starting search from a given channel. +// If forceFreeChannel is true and all channels in the specified record group are active, some channel is picked from the specified record group. +CHANNELINDEX CViewPattern::FindGroupRecordChannel(RecordGroup recordGroup, bool forceFreeChannel, CHANNELINDEX startChannel) const +{ + const CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return CHANNELINDEX_INVALID; + + CHANNELINDEX chn = startChannel; + CHANNELINDEX foundChannel = CHANNELINDEX_INVALID; + + for(CHANNELINDEX i = 1; i < pModDoc->GetNumChannels(); i++, chn++) + { + if(chn >= pModDoc->GetNumChannels()) + chn = 0; // loop around + + if(pModDoc->GetChannelRecordGroup(chn) == recordGroup) + { + // Check if any notes are playing on this channel + bool channelLocked = false; + for(size_t k = 0; k < m_activeNoteChannel.size(); k++) + { + if(m_activeNoteChannel[k] == chn || m_splitActiveNoteChannel[k] == chn) + { + channelLocked = true; + break; + } + } + + if(!channelLocked) + { + // Channel belongs to correct record group and no note is currently playing. + return chn; + } + + if(forceFreeChannel) + { + // If all channels are active, we might still pick a random channel from the specified group. + foundChannel = chn; + } + } + } + return foundChannel; +} + + +void CViewPattern::OnClearField(const RowMask &mask, bool step, bool ITStyle) +{ + CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !IsEditingEnabled_bmsg()) + return; + + // If we have a selection, we want to do something different + if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) + { + OnClearSelection(ITStyle); + return; + } + + PrepareUndo(m_Cursor, m_Cursor, "Clear Field"); + + ModCommand &target = GetCursorCommand(); + ModCommand oldcmd = target; + + if(mask.note) + { + // Clear note + if(target.IsPcNote()) + { + // Need to clear entire field if this is a PC Event. + target.Clear(); + } else + { + target.note = NOTE_NONE; + if(ITStyle) + { + target.instr = 0; + } + } + } + if(mask.instrument) + { + // Clear instrument + target.instr = 0; + } + if(mask.volume) + { + // Clear volume effect + target.volcmd = VOLCMD_NONE; + target.vol = 0; + } + if(mask.command) + { + // Clear effect command + target.command = CMD_NONE; + } + if(mask.parameter) + { + // Clear effect parameter + target.param = 0; + } + + if((mask.command || mask.parameter) && (target.IsPcNote())) + { + target.SetValueEffectCol(0); + } + + SetSelToCursor(); + + if(target != oldcmd) + { + SetModified(false); + InvalidateRow(); + UpdateIndicator(); + } + + if(step && (sndFile->IsPaused() || !m_Status[psFollowSong] || + (CMainFrame::GetMainFrame() != nullptr && CMainFrame::GetMainFrame()->GetFollowSong(GetDocument()) != m_hWnd))) + { + // Preview Row + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYEDITROW) && !IsLiveRecord()) + { + PatternStep(GetCurrentRow()); + } + + if(m_nSpacing > 0) + SetCurrentRow(GetCurrentRow() + m_nSpacing); + + SetSelToCursor(); + } +} + + + +void CViewPattern::OnInitMenu(CMenu *pMenu) +{ + CModScrollView::OnInitMenu(pMenu); +} + +void CViewPattern::TogglePluginEditor(int chan) +{ + CModDoc *modDoc = GetDocument(); + if(!modDoc) + return; + + int plug = modDoc->GetSoundFile().ChnSettings[chan].nMixPlugin; + if(plug > 0) + modDoc->TogglePluginEditor(plug - 1); + + return; +} + + +void CViewPattern::OnSelectInstrument(UINT nID) +{ + SetSelectionInstrument(static_cast<INSTRUMENTINDEX>(nID - ID_CHANGE_INSTRUMENT), true); +} + + +void CViewPattern::OnSelectPCNoteParam(UINT nID) +{ + CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + return; + + uint16 paramNdx = static_cast<uint16>(nID - ID_CHANGE_PCNOTE_PARAM); + bool modified = false; + ApplyToSelection([paramNdx, &modified] (ModCommand &m, ROWINDEX, CHANNELINDEX) + { + if(m.IsPcNote() && (m.GetValueVolCol() != paramNdx)) + { + m.SetValueVolCol(paramNdx); + modified = true; + } + }); + if(modified) + { + SetModified(); + InvalidatePattern(); + } +} + + +void CViewPattern::OnSelectPlugin(UINT nID) +{ + CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr) + return; + + const CHANNELINDEX plugChannel = m_MenuCursor.GetChannel(); + if(plugChannel < sndFile->GetNumChannels()) + { + PLUGINDEX newPlug = static_cast<PLUGINDEX>(nID - ID_PLUGSELECT); + if(newPlug <= MAX_MIXPLUGINS && newPlug != sndFile->ChnSettings[plugChannel].nMixPlugin) + { + sndFile->ChnSettings[plugChannel].nMixPlugin = newPlug; + if(sndFile->GetModSpecifications().supportsPlugins) + { + SetModified(false); + } + InvalidateChannelsHeaders(); + } + } +} + + +bool CViewPattern::HandleSplit(ModCommand &m, int note) +{ + ModCommand::INSTR ins = static_cast<ModCommand::INSTR>(GetCurrentInstrument()); + const bool isSplit = IsNoteSplit(note); + + if(isSplit) + { + CModDoc *modDoc = GetDocument(); + if(modDoc == nullptr) + return false; + const CSoundFile &sndFile = modDoc->GetSoundFile(); + + if(modDoc->GetSplitKeyboardSettings().octaveLink && note <= NOTE_MAX) + { + note += 12 * modDoc->GetSplitKeyboardSettings().octaveModifier; + Limit(note, sndFile.GetModSpecifications().noteMin, sndFile.GetModSpecifications().noteMax); + } + if(modDoc->GetSplitKeyboardSettings().splitInstrument) + { + ins = modDoc->GetSplitKeyboardSettings().splitInstrument; + } + } + + m.note = static_cast<ModCommand::NOTE>(note); + if(ins) + { + m.instr = ins; + } + + return isSplit; +} + + +bool CViewPattern::IsNoteSplit(int note) const +{ + CModDoc *pModDoc = GetDocument(); + return (pModDoc != nullptr + && pModDoc->GetSplitKeyboardSettings().IsSplitActive() + && note <= pModDoc->GetSplitKeyboardSettings().splitNote); +} + + +bool CViewPattern::BuildPluginCtxMenu(HMENU hMenu, UINT nChn, const CSoundFile &sndFile) const +{ + for(PLUGINDEX plug = 0; plug <= MAX_MIXPLUGINS; plug++) + { + bool itemFound = false; + + CString s; + if(!plug) + { + s = _T("No Plugin"); + itemFound = true; + } else + { + const SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[plug - 1]; + if(plugin.IsValidPlugin()) + { + s.Format(_T("FX%u: "), plug); + s += mpt::ToCString(plugin.GetName()); + itemFound = true; + } + } + + if(itemFound) + { + UINT flags = MF_STRING | ((plug == sndFile.ChnSettings[nChn].nMixPlugin) ? MF_CHECKED : 0); + AppendMenu(hMenu, flags, ID_PLUGSELECT + plug, s); + } + } + return true; +} + + +bool CViewPattern::BuildSoloMuteCtxMenu(HMENU hMenu, CInputHandler *ih, UINT nChn, const CSoundFile &sndFile) const +{ + AppendMenu(hMenu, sndFile.ChnSettings[nChn].dwFlags[CHN_MUTE] ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_PATTERN_MUTE, ih->GetKeyTextFromCommand(kcChannelMute, _T("&Mute Channel"))); + bool solo = false, unmuteAll = false; + bool soloPending = false, unmuteAllPending = false; // doesn't work perfectly yet + + for(CHANNELINDEX i = 0; i < sndFile.GetNumChannels(); i++) + { + if(i != nChn) + { + if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE]) + solo = soloPending = true; + if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i]) + soloPending = true; + } else + { + if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE]) + solo = soloPending = true; + if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i]) + soloPending = true; + } + if(sndFile.ChnSettings[i].dwFlags[CHN_MUTE]) + unmuteAll = unmuteAllPending = true; + if(!sndFile.ChnSettings[i].dwFlags[CHN_MUTE] && sndFile.m_bChannelMuteTogglePending[i]) + unmuteAllPending = true; + } + if(solo) + AppendMenu(hMenu, MF_STRING, ID_PATTERN_SOLO, ih->GetKeyTextFromCommand(kcChannelSolo, _T("&Solo Channel"))); + if(unmuteAll) + AppendMenu(hMenu, MF_STRING, ID_PATTERN_UNMUTEALL, ih->GetKeyTextFromCommand(kcChannelUnmuteAll, _T("&Unmute All"))); + + AppendMenu(hMenu, sndFile.m_bChannelMuteTogglePending[nChn] ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_PATTERN_TRANSITIONMUTE, ih->GetKeyTextFromCommand(kcToggleChanMuteOnPatTransition, sndFile.ChnSettings[nChn].dwFlags[CHN_MUTE] ? _T("On Transition: Unmute\t") : _T("On Transition: Mute\t"))); + + if(unmuteAllPending) + AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSITION_UNMUTEALL, ih->GetKeyTextFromCommand(kcUnmuteAllChnOnPatTransition, _T("On Transition: Unmute All"))); + if(soloPending) + AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSITIONSOLO, ih->GetKeyTextFromCommand(kcSoloChnOnPatTransition, _T("On Transition: Solo"))); + + AppendMenu(hMenu, MF_STRING, ID_PATTERN_CHNRESET, ih->GetKeyTextFromCommand(kcChannelReset, _T("&Reset Channel"))); + + return true; +} + +bool CViewPattern::BuildRecordCtxMenu(HMENU hMenu, CInputHandler *ih, CHANNELINDEX nChn) const +{ + const auto recordGroup = GetDocument()->GetChannelRecordGroup(nChn); + AppendMenu(hMenu, (recordGroup == RecordGroup::Group1) ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_EDIT_RECSELECT, ih->GetKeyTextFromCommand(kcChannelRecordSelect, _T("R&ecord Select"))); + AppendMenu(hMenu, (recordGroup == RecordGroup::Group2) ? (MF_STRING | MF_CHECKED) : MF_STRING, ID_EDIT_SPLITRECSELECT, ih->GetKeyTextFromCommand(kcChannelSplitRecordSelect, _T("S&plit Record Select"))); + return true; +} + + + +bool CViewPattern::BuildRowInsDelCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + HMENU subMenuInsert = CreatePopupMenu(); + HMENU subMenuDelete = CreatePopupMenu(); + + const auto numRows = m_Selection.GetNumRows(); + const CString label = (numRows != 1) ? MPT_CFORMAT("{} Rows")(numRows) : CString(_T("Row")); + + AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTROW, ih->GetKeyTextFromCommand(kcInsertRow, _T("Insert ") + label + _T(" (&Selection)"))); + AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTALLROW, ih->GetKeyTextFromCommand(kcInsertWholeRow, _T("Insert ") + label + _T(" (&All Channels)"))); + AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTROWGLOBAL, ih->GetKeyTextFromCommand(kcInsertRowGlobal, _T("Insert ") + label + _T(" (Selection, &Global)"))); + AppendMenu(subMenuInsert, MF_STRING, ID_PATTERN_INSERTALLROWGLOBAL, ih->GetKeyTextFromCommand(kcInsertWholeRowGlobal, _T("Insert ") + label + _T(" (All &Channels, Global)"))); + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(subMenuInsert), _T("&Insert ") + label); + + AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEROW, ih->GetKeyTextFromCommand(kcDeleteRow, _T("Delete ") + label + _T(" (&Selection)"))); + AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEALLROW, ih->GetKeyTextFromCommand(kcDeleteWholeRow, _T("Delete ") + label + _T(" (&All Channels)"))); + AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEROWGLOBAL, ih->GetKeyTextFromCommand(kcDeleteRowGlobal, _T("Delete ") + label + _T(" (Selection, &Global)"))); + AppendMenu(subMenuDelete, MF_STRING, ID_PATTERN_DELETEALLROWGLOBAL, ih->GetKeyTextFromCommand(kcDeleteWholeRowGlobal, _T("Delete ") + label + _T(" (All &Channels, Global)"))); + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(subMenuDelete), _T("&Delete ") + label); + return true; +} + +bool CViewPattern::BuildMiscCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + AppendMenu(hMenu, MF_STRING, ID_SHOWTIMEATROW, ih->GetKeyTextFromCommand(kcTimeAtRow, _T("Show Row Play Time"))); + + if(m_Selection.GetStartRow() == m_Selection.GetEndRow()) + { + CString s; + s.Format(_T("Split Pattern at Ro&w %u"), m_Selection.GetStartRow()); + AppendMenu(hMenu, MF_STRING | (m_Selection.GetStartRow() < 1 ? MF_GRAYED : 0), ID_PATTERN_SPLIT, ih->GetKeyTextFromCommand(kcSplitPattern, s)); + } + + const CSoundFile &sndFile = *GetSoundFile(); + CString lockStr; + bool lockActive = (sndFile.m_lockRowStart != ROWINDEX_INVALID); + if(m_Selection.GetUpperLeft() != m_Selection.GetLowerRight()) + { + lockStr = _T("&Lock Playback to Selection"); + if(lockActive) + { + lockStr.AppendFormat(_T(" (Current: %u-%u)"), sndFile.m_lockRowStart, sndFile.m_lockRowEnd); + } + } else if(lockActive) + { + lockStr = _T("Reset Playback &Lock"); + } else + { + return true; + } + AppendMenu(hMenu, MF_STRING | (lockActive ? MF_CHECKED : 0), ID_LOCK_PATTERN_ROWS, ih->GetKeyTextFromCommand(kcLockPlaybackToRows, lockStr)); + return true; +} + +bool CViewPattern::BuildSelectionCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + AppendMenu(hMenu, MF_STRING, ID_EDIT_SELECTCOLUMN, ih->GetKeyTextFromCommand(kcSelectChannel, _T("Select &Channel"))); + AppendMenu(hMenu, MF_STRING, ID_EDIT_SELECT_ALL, ih->GetKeyTextFromCommand(kcEditSelectAll, _T("Select &Pattern"))); + return true; +} + +bool CViewPattern::BuildGrowShrinkCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + AppendMenu(hMenu, MF_STRING, ID_GROW_SELECTION, ih->GetKeyTextFromCommand(kcPatternGrowSelection, _T("&Grow selection"))); + AppendMenu(hMenu, MF_STRING, ID_SHRINK_SELECTION, ih->GetKeyTextFromCommand(kcPatternShrinkSelection, _T("&Shrink selection"))); + return true; +} + + +bool CViewPattern::BuildInterpolationCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + const CSoundFile *sndFile = GetSoundFile(); + const bool isPCNote = sndFile->Patterns.IsValidPat(m_nPattern) && sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel())->IsPcNote(); + + HMENU subMenu = CreatePopupMenu(); + bool possible = BuildInterpolationCtxMenu(subMenu, PatternCursor::noteColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateNote, _T("&Note Column")), ID_PATTERN_INTERPOLATE_NOTE) + | BuildInterpolationCtxMenu(subMenu, PatternCursor::instrColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateInstr, isPCNote ? _T("&Plugin Column") : _T("&Instrument Column")), ID_PATTERN_INTERPOLATE_INSTR) + | BuildInterpolationCtxMenu(subMenu, PatternCursor::volumeColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateVol, isPCNote ? _T("&Parameter Column") : _T("&Volume Column")), ID_PATTERN_INTERPOLATE_VOLUME) + | BuildInterpolationCtxMenu(subMenu, PatternCursor::effectColumn, ih->GetKeyTextFromCommand(kcPatternInterpolateEffect, isPCNote ? _T("&Value Column") : _T("&Effect Column")), ID_PATTERN_INTERPOLATE_EFFECT); + if(possible || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_POPUP | (possible ? 0 : MF_GRAYED), reinterpret_cast<UINT_PTR>(subMenu), _T("I&nterpolate...")); + return true; + } + return false; +} + + +bool CViewPattern::BuildInterpolationCtxMenu(HMENU hMenu, PatternCursor::Columns colType, CString label, UINT command) const +{ + bool possible = IsInterpolationPossible(colType); + if(!possible && colType == PatternCursor::effectColumn) + { + // Extend search to param column + possible = IsInterpolationPossible(PatternCursor::paramColumn); + } + + if(possible || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_STRING | (possible ? 0 : MF_GRAYED), command, label); + } + + return possible; +} + + +bool CViewPattern::BuildEditCtxMenu(HMENU hMenu, CInputHandler *ih, CModDoc *pModDoc) const +{ + HMENU pasteSpecialMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_STRING, ID_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("Cu&t"))); + AppendMenu(hMenu, MF_STRING, ID_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy"))); + AppendMenu(hMenu, MF_STRING | (PatternClipboard::CanPaste() ? 0 : MF_GRAYED), ID_EDIT_PASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("&Paste"))); + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(pasteSpecialMenu), _T("Paste Special")); + AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_MIXPASTE, ih->GetKeyTextFromCommand(kcEditMixPaste, _T("&Mix Paste"))); + AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_MIXPASTE_ITSTYLE, ih->GetKeyTextFromCommand(kcEditMixPasteITStyle, _T("M&ix Paste (IT Style)"))); + AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_PASTEFLOOD, ih->GetKeyTextFromCommand(kcEditPasteFlood, _T("Paste Fl&ood"))); + AppendMenu(pasteSpecialMenu, MF_STRING, ID_EDIT_PUSHFORWARDPASTE, ih->GetKeyTextFromCommand(kcEditPushForwardPaste, _T("&Push Forward Paste (Insert)"))); + + DWORD greyed = pModDoc->GetPatternUndo().CanUndo() ? MF_ENABLED : MF_GRAYED; + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_STRING | greyed, ID_EDIT_UNDO, ih->GetKeyTextFromCommand(kcEditUndo, _T("&Undo"))); + } + greyed = pModDoc->GetPatternUndo().CanRedo() ? MF_ENABLED : MF_GRAYED; + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_STRING | greyed, ID_EDIT_REDO, ih->GetKeyTextFromCommand(kcEditRedo, _T("&Redo"))); + } + + AppendMenu(hMenu, MF_STRING, ID_CLEAR_SELECTION, ih->GetKeyTextFromCommand(kcSampleDelete, _T("Clear Selection"))); + + return true; +} + +bool CViewPattern::BuildVisFXCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + DWORD greyed = (IsColumnSelected(PatternCursor::effectColumn) || IsColumnSelected(PatternCursor::paramColumn)) ? FALSE : MF_GRAYED; + + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_VISUALIZE_EFFECT, ih->GetKeyTextFromCommand(kcPatternVisualizeEffect, _T("&Visualize Effect"))); + return true; + } + return false; +} + +bool CViewPattern::BuildTransposeCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + HMENU transMenu = CreatePopupMenu(); + + std::vector<CHANNELINDEX> validChans; + DWORD greyed = IsColumnSelected(PatternCursor::noteColumn) ? FALSE : MF_GRAYED; + + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_UP, ih->GetKeyTextFromCommand(kcTransposeUp, _T("Transpose +&1"))); + AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_DOWN, ih->GetKeyTextFromCommand(kcTransposeDown, _T("Transpose -&1"))); + AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_OCTUP, ih->GetKeyTextFromCommand(kcTransposeOctUp, _T("Transpose +1&2"))); + AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_OCTDOWN, ih->GetKeyTextFromCommand(kcTransposeOctDown, _T("Transpose -1&2"))); + AppendMenu(transMenu, MF_STRING | greyed, ID_TRANSPOSE_CUSTOM, ih->GetKeyTextFromCommand(kcTransposeCustom, _T("&Custom..."))); + AppendMenu(hMenu, MF_POPUP | greyed, reinterpret_cast<UINT_PTR>(transMenu), _T("&Transpose...")); + return true; + } + return false; +} + +bool CViewPattern::BuildAmplifyCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + std::vector<CHANNELINDEX> validChans; + DWORD greyed = IsColumnSelected(PatternCursor::volumeColumn) ? 0 : MF_GRAYED; + + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + AppendMenu(hMenu, MF_STRING | greyed, ID_PATTERN_AMPLIFY, ih->GetKeyTextFromCommand(kcPatternAmplify, _T("&Amplify"))); + return true; + } + return false; +} + + +bool CViewPattern::BuildChannelControlCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + const CModSpecifications &specs = GetDocument()->GetSoundFile().GetModSpecifications(); + CHANNELINDEX numChannels = GetDocument()->GetNumChannels(); + DWORD canAddChannels = (numChannels < specs.channelsMax) ? 0 : MF_GRAYED; + DWORD canRemoveChannels = (numChannels > specs.channelsMin) ? 0 : MF_GRAYED; + + AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + + AppendMenu(hMenu, MF_STRING, ID_PATTERN_TRANSPOSECHANNEL, ih->GetKeyTextFromCommand(kcChannelTranspose, _T("&Transpose Channel"))); + AppendMenu(hMenu, MF_STRING | canAddChannels, ID_PATTERN_DUPLICATECHANNEL, ih->GetKeyTextFromCommand(kcChannelDuplicate, _T("&Duplicate Channel"))); + + HMENU addChannelMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP | canAddChannels, reinterpret_cast<UINT_PTR>(addChannelMenu), _T("&Add Channel\t")); + AppendMenu(addChannelMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_FRONT, ih->GetKeyTextFromCommand(kcChannelAddBefore, _T("&Before this channel"))); + AppendMenu(addChannelMenu, MF_STRING, ID_PATTERN_ADDCHANNEL_AFTER, ih->GetKeyTextFromCommand(kcChannelAddAfter, _T("&After this channel"))); + + HMENU removeChannelMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP | canRemoveChannels, reinterpret_cast<UINT_PTR>(removeChannelMenu), _T("Remo&ve Channel\t")); + AppendMenu(removeChannelMenu, MF_STRING, ID_PATTERN_REMOVECHANNEL, ih->GetKeyTextFromCommand(kcChannelRemove, _T("&Remove this channel\t"))); + AppendMenu(removeChannelMenu, MF_STRING, ID_PATTERN_REMOVECHANNELDIALOG, _T("&Choose channels to remove...\t")); + + AppendMenu(hMenu, MF_STRING, ID_PATTERN_RESETCHANNELCOLORS, _T("Reset Channel &Colours")); + + return false; +} + + +bool CViewPattern::BuildSetInstCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + const CSoundFile *sndFile = GetSoundFile(); + const CModDoc *modDoc; + if(sndFile == nullptr || (modDoc = sndFile->GetpModDoc()) == nullptr) + { + return false; + } + + std::vector<CHANNELINDEX> validChans; + DWORD greyed = IsColumnSelected(PatternCursor::instrColumn) ? 0 : MF_GRAYED; + + if(!greyed || !(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_OLDCTXMENUSTYLE)) + { + if((sndFile->Patterns.IsValidPat(m_nPattern))) + { + if(sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel())->IsPcNote()) + { + // Don't build instrument menu for PC notes. + return false; + } + } + + // Create the new menu and add it to the existing menu. + HMENU instrumentChangeMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP | greyed, reinterpret_cast<UINT_PTR>(instrumentChangeMenu), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("Change Instrument"))); + + if(!greyed) + { + bool addSeparator = false; + if(sndFile->GetNumInstruments()) + { + for(INSTRUMENTINDEX i = 1; i <= sndFile->GetNumInstruments(); i++) + { + if(sndFile->Instruments[i] == nullptr) + continue; + + CString instString = modDoc->GetPatternViewInstrumentName(i, true); + if(!instString.IsEmpty()) + { + AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + i, modDoc->GetPatternViewInstrumentName(i)); + addSeparator = true; + } + } + + } else + { + CString s; + for(SAMPLEINDEX i = 1; i <= sndFile->GetNumSamples(); i++) if (sndFile->GetSample(i).HasSampleData()) + { + s.Format(_T("%02d: "), i); + s += mpt::ToCString(sndFile->GetCharsetInternal(), sndFile->GetSampleName(i)); + AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + i, s); + addSeparator = true; + } + } + + // Add options to remove instrument from selection. + if(addSeparator) + { + AppendMenu(instrumentChangeMenu, MF_SEPARATOR, 0, 0); + } + AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT, _T("&Remove Instrument")); + AppendMenu(instrumentChangeMenu, MF_STRING, ID_CHANGE_INSTRUMENT + GetCurrentInstrument(), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("&Current Instrument"))); + AppendMenu(instrumentChangeMenu, MF_STRING, ID_PATTERN_SETINSTRUMENT, ih->GetKeyTextFromCommand(kcPatternSetInstrumentNotEmpty, _T("Current Instrument (&only change existing)"))); + } + return BuildTogglePlugEditorCtxMenu(hMenu, ih); + } + return false; +} + + +// Context menu for Param Control notes +bool CViewPattern::BuildPCNoteCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + { + return false; + } + + const ModCommand &selStart = *sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel()); + if(!selStart.IsPcNote()) + { + return false; + } + + CString s; + + // Create sub menu for "change plugin" + HMENU pluginChangeMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(pluginChangeMenu), ih->GetKeyTextFromCommand(kcPatternSetInstrument, _T("Change Plugin"))); + for(PLUGINDEX nPlg = 0; nPlg < MAX_MIXPLUGINS; nPlg++) + { + if(sndFile->m_MixPlugins[nPlg].pMixPlugin != nullptr) + { + s = MPT_CFORMAT("{}: {}")(mpt::cfmt::dec0<2>(nPlg + 1), mpt::ToCString(sndFile->m_MixPlugins[nPlg].GetName())); + AppendMenu(pluginChangeMenu, MF_STRING | (((nPlg + 1) == selStart.instr) ? MF_CHECKED : 0), ID_CHANGE_INSTRUMENT + nPlg + 1, s); + } + } + + if(selStart.instr >= 1 && selStart.instr <= MAX_MIXPLUGINS) + { + const SNDMIXPLUGIN &plug = sndFile->m_MixPlugins[selStart.instr - 1]; + if(plug.pMixPlugin != nullptr) + { + + // Create sub menu for "change plugin param" + HMENU paramChangeMenu = ::CreatePopupMenu(); + AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(paramChangeMenu), _T("Change Plugin Parameter\t")); + + const PlugParamIndex curParam = selStart.GetValueVolCol(), nParams = plug.pMixPlugin->GetNumParameters(); + + for(PlugParamIndex i = 0; i < nParams; i++) + { + AppendMenu(paramChangeMenu, MF_STRING | ((i == curParam) ? MF_CHECKED : 0), ID_CHANGE_PCNOTE_PARAM + i, plug.pMixPlugin->GetFormattedParamName(i)); + } + } + } + + return BuildTogglePlugEditorCtxMenu(hMenu, ih); +} + + +bool CViewPattern::BuildTogglePlugEditorCtxMenu(HMENU hMenu, CInputHandler *ih) const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + { + return false; + } + + PLUGINDEX plug = 0; + const ModCommand &selStart = *sndFile->Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel()); + if(selStart.IsPcNote()) + { + // PC Event + plug = selStart.instr; + } else if(selStart.instr > 0 && selStart.instr <= sndFile->GetNumInstruments() + && sndFile->Instruments[selStart.instr] != nullptr + && sndFile->Instruments[selStart.instr]->nMixPlug) + { + // Regular instrument + plug = sndFile->Instruments[selStart.instr]->nMixPlug; + } + + if(plug && plug <= MAX_MIXPLUGINS && sndFile->m_MixPlugins[plug - 1].pMixPlugin != nullptr) + { + AppendMenu(hMenu, MF_STRING, ID_PATTERN_EDIT_PCNOTE_PLUGIN, ih->GetKeyTextFromCommand(kcPatternEditPCNotePlugin, _T("Toggle Plugin &Editor"))); + return true; + } + return false; +} + +// Returns an ordered list of all channels in which a given column type is selected. +CHANNELINDEX CViewPattern::ListChansWhereColSelected(PatternCursor::Columns colType, std::vector<CHANNELINDEX> &chans) const +{ + CHANNELINDEX startChan = m_Selection.GetStartChannel(); + CHANNELINDEX endChan = m_Selection.GetEndChannel(); + chans.clear(); + chans.reserve(endChan - startChan + 1); + + // Check in which channels this column is selected. + // Actually this check is only important for the first and last channel, but to keep things clean and simple, all channels are checked in the same manner. + for(CHANNELINDEX i = startChan; i <= endChan; i++) + { + if(m_Selection.ContainsHorizontal(PatternCursor(0, i, colType))) + { + chans.push_back(i); + } + } + + return static_cast<CHANNELINDEX>(chans.size()); +} + + +// Check if a column type is selected on any channel in the current selection. +bool CViewPattern::IsColumnSelected(PatternCursor::Columns colType) const +{ + return m_Selection.ContainsHorizontal(PatternCursor(0, m_Selection.GetStartChannel(), colType)) + || m_Selection.ContainsHorizontal(PatternCursor(0, m_Selection.GetEndChannel(), colType)); +} + + +// Check if the given interpolation type is actually possible in the current selection. +bool CViewPattern::IsInterpolationPossible(PatternCursor::Columns colType) const +{ + std::vector<CHANNELINDEX> validChans; + ListChansWhereColSelected(colType, validChans); + + ROWINDEX startRow = m_Selection.GetStartRow(); + ROWINDEX endRow = m_Selection.GetEndRow(); + for(auto chn : validChans) + { + if(IsInterpolationPossible(startRow, endRow, chn, colType)) + { + return true; + } + } + return false; +} + + +// Check if the given interpolation type is actually possible in a given channel. +bool CViewPattern::IsInterpolationPossible(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX chan, PatternCursor::Columns colType) const +{ + const CSoundFile *sndFile = GetSoundFile(); + if(startRow == endRow || sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + return false; + + bool result = false; + const ModCommand &startRowMC = *sndFile->Patterns[m_nPattern].GetpModCommand(startRow, chan); + const ModCommand &endRowMC = *sndFile->Patterns[m_nPattern].GetpModCommand(endRow, chan); + UINT startRowCmd, endRowCmd; + + if(colType == PatternCursor::effectColumn && (startRowMC.IsPcNote() || endRowMC.IsPcNote())) + return true; + + switch(colType) + { + case PatternCursor::noteColumn: + startRowCmd = startRowMC.note; + endRowCmd = endRowMC.note; + result = (startRowCmd == endRowCmd && startRowCmd != NOTE_NONE) // Interpolate between two identical notes or Cut / Fade / etc... + || (startRowCmd != NOTE_NONE && endRowCmd == NOTE_NONE) // Fill in values from the first row + || (startRowCmd == NOTE_NONE && endRowCmd != NOTE_NONE) // Fill in values from the last row + || (ModCommand::IsNoteOrEmpty(startRowMC.note) && ModCommand::IsNoteOrEmpty(endRowMC.note) && !(startRowCmd == NOTE_NONE && endRowCmd == NOTE_NONE)); // Interpolate between two notes of which one may be empty + break; + + case PatternCursor::instrColumn: + startRowCmd = startRowMC.instr; + endRowCmd = endRowMC.instr; + result = startRowCmd != 0 || endRowCmd != 0; + break; + + case PatternCursor::volumeColumn: + startRowCmd = startRowMC.volcmd; + endRowCmd = endRowMC.volcmd; + result = (startRowCmd == endRowCmd && startRowCmd != VOLCMD_NONE) // Interpolate between two identical commands + || (startRowCmd != VOLCMD_NONE && endRowCmd == VOLCMD_NONE) // Fill in values from the first row + || (startRowCmd == VOLCMD_NONE && endRowCmd != VOLCMD_NONE); // Fill in values from the last row + break; + + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + startRowCmd = startRowMC.command; + endRowCmd = endRowMC.command; + result = (startRowCmd == endRowCmd && startRowCmd != CMD_NONE) // Interpolate between two identical commands + || (startRowCmd != CMD_NONE && endRowCmd == CMD_NONE) // Fill in values from the first row + || (startRowCmd == CMD_NONE && endRowCmd != CMD_NONE); // Fill in values from the last row + break; + + default: + result = false; + } + return result; +} + + +void CViewPattern::OnRButtonDblClk(UINT nFlags, CPoint point) +{ + OnRButtonDown(nFlags, point); + CModScrollView::OnRButtonDblClk(nFlags, point); +} + + +// Toggle pending mute status for channel from context menu. +void CViewPattern::OnTogglePendingMuteFromClick() +{ + TogglePendingMute(m_MenuCursor.GetChannel()); +} + + +// Toggle pending solo status for channel from context menu. +void CViewPattern::OnPendingSoloChnFromClick() +{ + PendingSoloChn(m_MenuCursor.GetChannel()); +} + + +// Set pending unmute status for all channels. +void CViewPattern::OnPendingUnmuteAllChnFromClick() +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr) + { + GetSoundFile()->PatternTransitionChnUnmuteAll(); + InvalidateChannelsHeaders(); + } +} + + +// Toggle pending solo status for a channel. +void CViewPattern::PendingSoloChn(CHANNELINDEX nChn) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr) + { + GetSoundFile()->PatternTranstionChnSolo(nChn); + InvalidateChannelsHeaders(); + } +} + + +// Toggle pending mute status for a channel. +void CViewPattern::TogglePendingMute(CHANNELINDEX nChn) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile != nullptr) + { + pSndFile->m_bChannelMuteTogglePending[nChn] = !pSndFile->m_bChannelMuteTogglePending[nChn]; + InvalidateChannelsHeaders(); + } +} + + +// Check if editing is enabled, and if it's not, prompt the user to enable editing. +bool CViewPattern::IsEditingEnabled_bmsg() +{ + if(IsEditingEnabled()) + return true; + if(TrackerSettings::Instance().patternNoEditPopup) + return false; + + HMENU hMenu; + + if((hMenu = ::CreatePopupMenu()) == nullptr) + return false; + + CPoint pt = GetPointFromPosition(m_Cursor); + + // We add an mnemonic for an unbreakable space to avoid activating edit mode accidentally. + AppendMenuW(hMenu, MF_STRING, IDC_PATTERN_RECORD, L"Editing (recording) is disabled;&\u00A0 click here to enable it."); + + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN, pt.x, pt.y, 0, m_hWnd, NULL); + + ::DestroyMenu(hMenu); + + return false; +} + + +// Show playback time at a given pattern position. +void CViewPattern::OnShowTimeAtRow() +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr) + { + return; + } + + CString msg; + const auto &order = Order(); + ORDERINDEX currentOrder = GetCurrentOrder(); + if(currentOrder < order.size() && order[currentOrder] == m_nPattern) + { + const double t = pSndFile->GetPlaybackTimeAt(currentOrder, GetCurrentRow(), false, false); + if(t < 0) + msg.Format(_T("Unable to determine the time. Possible cause: No order %d, row %u found in play sequence."), currentOrder, GetCurrentRow()); + else + { + const uint32 minutes = static_cast<uint32>(t / 60.0); + const double seconds = t - (minutes * 60); + msg.Format(_T("Estimate for playback time at order %d (pattern %d), row %u: %u minute%s %.2f seconds."), currentOrder, m_nPattern, GetCurrentRow(), minutes, (minutes == 1) ? _T("") : _T("s"), seconds); + } + } else + { + msg.Format(_T("Unable to determine the time: pattern at current order (%d) does not correspond to pattern in pattern view (pattern %d)."), currentOrder, m_nPattern); + } + + Reporting::Notification(msg); +} + + +// Set up split keyboard +void CViewPattern::SetSplitKeyboardSettings() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + + CSplitKeyboardSettings dlg(CMainFrame::GetMainFrame(), pModDoc->GetSoundFile(), pModDoc->GetSplitKeyboardSettings()); + if(dlg.DoModal() == IDOK) + { + // Update split keyboard settings in other pattern views + pModDoc->UpdateAllViews(NULL, SampleHint().Names()); + } +} + + +// Paste pattern data using the given paste mode. +void CViewPattern::ExecutePaste(PatternClipboard::PasteModes mode) +{ + if(IsEditingEnabled_bmsg() && PastePattern(m_nPattern, m_Selection.GetUpperLeft(), mode)) + { + InvalidatePattern(false); + SetFocus(); + } +} + + +// Show plugin editor for plugin assigned to PC Event at the cursor position. +void CViewPattern::OnTogglePCNotePluginEditor() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) + return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(!sndFile.Patterns.IsValidPat(m_nPattern)) + return; + + const ModCommand &m = *sndFile.Patterns[m_nPattern].GetpModCommand(m_Selection.GetStartRow(), m_Selection.GetStartChannel()); + PLUGINDEX plug = 0; + if(!m.IsPcNote()) + { + // No PC note: Toggle instrument's plugin editor + if(m.instr && m.instr <= sndFile.GetNumInstruments() && sndFile.Instruments[m.instr]) + { + plug = sndFile.Instruments[m.instr]->nMixPlug; + } + } else + { + plug = m.instr; + } + + if(plug > 0 && plug <= MAX_MIXPLUGINS) + pModDoc->TogglePluginEditor(plug - 1); +} + + +// Get the active pattern's rows per beat, or, if they are not overriden, the song's default rows per beat. +ROWINDEX CViewPattern::GetRowsPerBeat() const +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + return 0; + if(!pSndFile->Patterns[m_nPattern].GetOverrideSignature()) + return pSndFile->m_nDefaultRowsPerBeat; + else + return pSndFile->Patterns[m_nPattern].GetRowsPerBeat(); +} + + +// Get the active pattern's rows per measure, or, if they are not overriden, the song's default rows per measure. +ROWINDEX CViewPattern::GetRowsPerMeasure() const +{ + const CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + return 0; + if(!pSndFile->Patterns[m_nPattern].GetOverrideSignature()) + return pSndFile->m_nDefaultRowsPerMeasure; + else + return pSndFile->Patterns[m_nPattern].GetRowsPerMeasure(); +} + + +// Set instrument +void CViewPattern::SetSelectionInstrument(const INSTRUMENTINDEX instr, bool setEmptyInstrument) +{ + CSoundFile *pSndFile = GetSoundFile(); + if(pSndFile == nullptr || !pSndFile->Patterns.IsValidPat(m_nPattern)) + { + return; + } + + BeginWaitCursor(); + PrepareUndo(m_Selection, "Set Instrument"); + + bool modified = false; + ApplyToSelection([instr, setEmptyInstrument, &modified] (ModCommand &m, ROWINDEX, CHANNELINDEX) + { + // If a note or an instr is present on the row, do the change, if required. + // Do not set instr if note and instr are both blank, + // but set instr if note is a PC note and instr is blank. + if(((setEmptyInstrument && (m.IsNote() || m.IsPcNote())) || m.instr != 0) + && (m.instr != instr)) + { + m.instr = static_cast<ModCommand::INSTR>(instr); + modified = true; + } + }); + + if(modified) + { + SetModified(); + InvalidatePattern(); + } + EndWaitCursor(); +} + + +// Select a whole beat (selectBeat = true) or measure. +void CViewPattern::SelectBeatOrMeasure(bool selectBeat) +{ + const ROWINDEX adjust = selectBeat ? GetRowsPerBeat() : GetRowsPerMeasure(); + + // Snap to start of beat / measure of upper-left corner of current selection + const ROWINDEX startRow = m_Selection.GetStartRow() - (m_Selection.GetStartRow() % adjust); + // Snap to end of beat / measure of lower-right corner of current selection + const ROWINDEX endRow = m_Selection.GetEndRow() + adjust - (m_Selection.GetEndRow() % adjust) - 1; + + CHANNELINDEX startChannel = m_Selection.GetStartChannel(), endChannel = m_Selection.GetEndChannel(); + PatternCursor::Columns startColumn = PatternCursor::firstColumn, endColumn = PatternCursor::firstColumn; + + if(m_Selection.GetUpperLeft() == m_Selection.GetLowerRight()) + { + // No selection has been made yet => expand selection to whole channel. + endColumn = PatternCursor::lastColumn; // Extend to param column + } else if(startRow == m_Selection.GetStartRow() && endRow == m_Selection.GetEndRow()) + { + // Whole beat or measure is already selected + if(m_Selection.GetStartColumn() == PatternCursor::firstColumn && m_Selection.GetEndColumn() == PatternCursor::lastColumn) + { + // Whole channel is already selected => expand selection to whole row. + startChannel = 0; + startColumn = PatternCursor::firstColumn; + endChannel = MAX_BASECHANNELS; + endColumn = PatternCursor::lastColumn; + } else + { + // Channel is only partly selected => expand to whole channel first. + endColumn = PatternCursor::lastColumn; // Extend to param column + } + } else + { + // Some arbitrary selection: Remember start / end column + startColumn = m_Selection.GetStartColumn(); + endColumn = m_Selection.GetEndColumn(); + } + + SetCurSel(PatternCursor(startRow, startChannel, startColumn), PatternCursor(endRow, endChannel, endColumn)); +} + + +// Sweep pattern channel to find instrument number to use +void CViewPattern::FindInstrument() +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr) + { + return; + } + const auto &order = Order(); + ORDERINDEX ord = GetCurrentOrder(); + PATTERNINDEX pat = m_nPattern; + ROWINDEX row = m_Cursor.GetRow(); + + while(sndFile->Patterns.IsValidPat(pat)) + { + // Seek upwards + do + { + auto &m = *sndFile->Patterns[pat].GetpModCommand(row, m_Cursor.GetChannel()); + if(!m.IsPcNote() && m.instr != 0) + { + SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, m.instr); + static_cast<CModControlView *>(CWnd::FromHandle(m_hWndCtrl))->InstrumentChanged(m.instr); + return; + } + } while(row-- != 0); + + // Try previous pattern + if(ord == 0) + { + return; + } + ord = order.GetPreviousOrderIgnoringSkips(ord); + pat = order[ord]; + if(!sndFile->Patterns.IsValidPat(pat)) + { + return; + } + row = sndFile->Patterns[pat].GetNumRows() - 1; + } +} + + +// Find previous or next column entry (note, instrument, ...) on this channel +void CViewPattern::JumpToPrevOrNextEntry(bool nextEntry, bool select) +{ + const CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || GetCurrentOrder() >= Order().size()) + { + return; + } + const auto &order = Order(); + ORDERINDEX ord = GetCurrentOrder(); + PATTERNINDEX pat = m_nPattern; + CHANNELINDEX chn = m_Cursor.GetChannel(); + PatternCursor::Columns column = m_Cursor.GetColumnType(); + int32 row = m_Cursor.GetRow(); + + int direction = nextEntry ? 1 : -1; + row += direction; // Don't want to find the cell we're already in + while(sndFile->Patterns.IsValidPat(pat)) + { + while(sndFile->Patterns[pat].IsValidRow(row)) + { + auto &m = *sndFile->Patterns[pat].GetpModCommand(row, chn); + bool found; + switch(column) + { + case PatternCursor::noteColumn: + found = m.note != NOTE_NONE; + break; + case PatternCursor::instrColumn: + found = m.instr != 0; + break; + case PatternCursor::volumeColumn: + found = m.volcmd != VOLCMD_NONE; + break; + case PatternCursor::effectColumn: + case PatternCursor::paramColumn: + found = m.command != CMD_NONE; + break; + default: + found = false; + } + + if(found) + { + if(select) + { + CursorJump(static_cast<int>(row) - m_Cursor.GetRow(), false); + } else + { + SetCurrentOrder(ord); + SetCurrentPattern(pat, row); + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_PLAYNAVIGATEROW) + { + PatternStep(row); + } + } + return; + } + row += direction; + } + + // Continue search in prev/next pattern (unless we also select - selections cannot span multiple patterns) + if(select) + return; + ORDERINDEX nextOrd = nextEntry ? order.GetNextOrderIgnoringSkips(ord) : order.GetPreviousOrderIgnoringSkips(ord); + pat = order[nextOrd]; + if(nextOrd == ord || !sndFile->Patterns.IsValidPat(pat)) + return; + ord = nextOrd; + row = nextEntry ? 0 : (sndFile->Patterns[pat].GetNumRows() - 1); + } +} + + +// Copy to clipboard +bool CViewPattern::CopyPattern(PATTERNINDEX nPattern, const PatternRect &selection) +{ + BeginWaitCursor(); + bool result = PatternClipboard::Copy(*GetSoundFile(), nPattern, selection); + EndWaitCursor(); + PatternClipboardDialog::UpdateList(); + return result; +} + + +// Paste from clipboard +bool CViewPattern::PastePattern(PATTERNINDEX nPattern, const PatternCursor &pastePos, PatternClipboard::PasteModes mode) +{ + BeginWaitCursor(); + PatternEditPos pos; + pos.pattern = nPattern; + pos.row = pastePos.GetRow(); + pos.channel = pastePos.GetChannel(); + pos.order = GetCurrentOrder(); + PatternRect rect; + const bool patternExisted = GetSoundFile()->Patterns.IsValidPat(nPattern); + bool orderChanged = false; + bool result = PatternClipboard::Paste(*GetSoundFile(), pos, mode, rect, orderChanged); + EndWaitCursor(); + + PatternHint updateHint = PatternHint(PATTERNINDEX_INVALID).Data(); + if(pos.pattern != nPattern) + { + // Multipaste: Switch to pasted pattern. + SetCurrentPattern(pos.pattern); + SetCurrentOrder(pos.order); + } + if(orderChanged || (patternExisted != GetSoundFile()->Patterns.IsValidPat(nPattern))) + { + updateHint.Names(); + GetDocument()->UpdateAllViews(nullptr, SequenceHint(GetSoundFile()->Order.GetCurrentSequenceIndex()).Data(), nullptr); + } + + if(result) + { + SetCurSel(rect); + GetDocument()->SetModified(); + GetDocument()->UpdateAllViews(nullptr, updateHint, nullptr); + } + + return result; +} + + +template<typename Func> +void CViewPattern::ApplyToSelection(Func func) +{ + CSoundFile *sndFile = GetSoundFile(); + if(sndFile == nullptr || !sndFile->Patterns.IsValidPat(m_nPattern)) + return; + auto &pattern = sndFile->Patterns[m_nPattern]; + m_Selection.Sanitize(pattern.GetNumRows(), pattern.GetNumChannels()); + const CHANNELINDEX startChn = m_Selection.GetStartChannel(), endChn = m_Selection.GetEndChannel(); + const ROWINDEX endRow = m_Selection.GetEndRow(); + for(ROWINDEX row = m_Selection.GetStartRow(); row <= endRow; row++) + { + ModCommand *m = pattern.GetpModCommand(row, startChn); + for(CHANNELINDEX chn = startChn; chn <= endChn; chn++, m++) + { + func(*m, row, chn); + } + } +} + + +INT_PTR CViewPattern::OnToolHitTest(CPoint point, TOOLINFO *pTI) const +{ + CRect rect; + const auto item = GetDragItem(point, rect); + const auto value = item.Value(); + const CSoundFile &sndFile = *GetSoundFile(); + + mpt::winstring text; + switch(item.Type()) + { + case DragItem::PatternHeader: + { + text = _T("Show Pattern Properties"); + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(kcShowPatternProperties, 0); + if(!keyText.IsEmpty()) + text += MPT_CFORMAT(" ({})")(keyText); + break; + } + case DragItem::ChannelHeader: + if(value < sndFile.GetNumChannels()) + { + if(!sndFile.ChnSettings[value].szName.empty()) + text = MPT_TFORMAT("{}: {}")(value + 1, mpt::ToWin(sndFile.GetCharsetInternal(), sndFile.ChnSettings[value].szName)); + else + text = MPT_TFORMAT("Channel {}")(value + 1); + } + break; + case DragItem::PluginName: + if(value < sndFile.GetNumChannels()) + { + PLUGINDEX mixPlug = sndFile.ChnSettings[value].nMixPlugin; + if(mixPlug && mixPlug <= MAX_MIXPLUGINS) + text = MPT_TFORMAT("{}: {}")(mixPlug, mpt::ToWin(sndFile.m_MixPlugins[mixPlug - 1].GetName())); + else + text = _T("No Plugin"); + } + break; + } + + if(text.empty()) + return CScrollView::OnToolHitTest(point, pTI); + + pTI->hwnd = m_hWnd; + pTI->uId = item.ToIntPtr(); + pTI->rect = rect; + // MFC will free() the text + TCHAR *textP = static_cast<TCHAR *>(calloc(text.size() + 1, sizeof(TCHAR))); + std::copy(text.begin(), text.end(), textP); + pTI->lpszText = textP; + + return item.ToIntPtr(); +} + + +// Accessible description for screen readers +HRESULT CViewPattern::get_accName(VARIANT varChild, BSTR *pszName) +{ + const ModCommand &m = GetCursorCommand(); + const size_t columnIndex = m_Cursor.GetColumnType(); + const TCHAR *column = _T(""); + static constexpr const TCHAR *regularColumns[] = {_T("Note"), _T("Instrument"), _T("Volume"), _T("Effect"), _T("Parameter")}; + static constexpr const TCHAR *pcColumns[] = {_T("Note"), _T("Plugin"), _T("Plugin Parameter"), _T("Parameter Value"), _T("Parameter Value")}; + static_assert(PatternCursor::lastColumn + 1 == std::size(regularColumns)); + static_assert(PatternCursor::lastColumn + 1 == std::size(pcColumns)); + + if(m.IsPcNote() && columnIndex < std::size(pcColumns)) + column = pcColumns[columnIndex]; + else if(!m.IsPcNote() && columnIndex < std::size(regularColumns)) + column = regularColumns[columnIndex]; + + const CSoundFile *sndFile = GetSoundFile(); + const CHANNELINDEX chn = m_Cursor.GetChannel(); + + const auto channelNumber = mpt::cfmt::val(chn + 1); + CString channelName = channelNumber; + if(chn < sndFile->GetNumChannels() && !sndFile->ChnSettings[chn].szName.empty()) + channelName += _T(": ") + mpt::ToCString(sndFile->GetCharsetInternal(), sndFile->ChnSettings[chn].szName); + + CString str = TrackerSettings::Instance().patternAccessibilityFormat; + str.Replace(_T("%sequence%"), mpt::cfmt::val(sndFile->Order.GetCurrentSequenceIndex())); + str.Replace(_T("%order%"), mpt::cfmt::val(GetCurrentOrder())); + str.Replace(_T("%pattern%"), mpt::cfmt::val(GetCurrentPattern())); + str.Replace(_T("%row%"), mpt::cfmt::val(m_Cursor.GetRow())); + str.Replace(_T("%channel%"), channelNumber); + str.Replace(_T("%column_type%"), column); + str.Replace(_T("%column_description%"), GetCursorDescription()); + str.Replace(_T("%channel_name%"), channelName); + + if(str.IsEmpty()) + return CModScrollView::get_accName(varChild, pszName); + + *pszName = str.AllocSysString(); + return S_OK; +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.h b/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.h new file mode 100644 index 00000000..1bec0687 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_pat.h @@ -0,0 +1,593 @@ +/* + * View_pat.h + * ---------- + * Purpose: Pattern tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "Globals.h" +#include "PatternCursor.h" +#include "Moddoc.h" +#include "PatternEditorDialogs.h" +#include "PatternClipboard.h" + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +class CEditCommand; +class CEffectVis; +class CInputHandler; + +// Drag & Drop info +class DragItem +{ + uint32 v = 0; + + enum : uint32 + { + ValueMask = 0x00FFFFFF, + TypeMask = 0xFF000000, + }; + +public: + enum DragType : uint32 + { + ChannelHeader = 0x01000000, + PatternHeader = 0x02000000, + PluginName = 0x04000000, + }; + + DragItem() = default; + DragItem(DragType type, uint32 value) + : v(type | value) {} + + DragType Type() const { return static_cast<DragType>(v & TypeMask); } + uint32 Value() const { return v & ValueMask; } + INT_PTR ToIntPtr() const { return v; } + bool IsValid() const { return v != 0; } + + bool operator==(const DragItem other) const { return v == other.v; } + bool operator!=(const DragItem other) const { return v != other.v; } +}; + + +// Edit Step aka Row Spacing +inline constexpr ROWINDEX MAX_SPACING = MAX_PATTERN_ROWS; + + +// Struct for controlling selection clearing. This is used to define which data fields should be cleared. +struct RowMask +{ + bool note : 1; + bool instrument : 1; + bool volume : 1; + bool command : 1; + bool parameter : 1; + + // Default row mask (all rows are selected) + RowMask() + { + note = instrument = volume = command = parameter = true; + }; + + // Construct mask from list + RowMask(bool n, bool i, bool v, bool c, bool p) + { + note = n; + instrument = i; + volume = v; + command = c; + parameter = p; + } + + // Construct mask from column index + RowMask(const PatternCursor &cursor) + { + const PatternCursor::Columns column = cursor.GetColumnType(); + + note = (column == PatternCursor::noteColumn); + instrument = (column == PatternCursor::instrColumn); + volume = (column == PatternCursor::volumeColumn); + command = (column == PatternCursor::effectColumn); + parameter = (column == PatternCursor::paramColumn); + } + + void Clear() + { + note = instrument = volume = command = parameter = false; + } +}; + + +struct PatternEditPos +{ + ROWINDEX row = ROWINDEX_INVALID; + ORDERINDEX order = ORDERINDEX_INVALID; + PATTERNINDEX pattern = PATTERNINDEX_INVALID; + CHANNELINDEX channel = CHANNELINDEX_INVALID; +}; + + +// Pattern editing class + + +class CViewPattern final : public CModScrollView +{ +public: + // Pattern status flags + enum PatternStatus + { + psMouseDragSelect = 0x01, // Creating a selection using the mouse + psKeyboardDragSelect = 0x02, // Creating a selection using shortcuts + psFocussed = 0x04, // Is the pattern editor focussed + psFollowSong = 0x08, // Does the cursor follow playback + psRecordingEnabled = 0x10, // Recording enabled + psDragVScroll = 0x40, // Indicates that the vertical scrollbar is being dragged + psShowVUMeters = 0x80, // Display channel VU meters + psChordPlaying = 0x100, // Is a chord playing? (pretty much unused) + psDragnDropEdit = 0x200, // Drag & Drop editing (?) + psDragnDropping = 0x400, // Dragging a selection around + psShiftSelect = 0x800, // User has made at least one selection using Shift-Click since the Shift key has been pressed. + psCtrlDragSelect = 0x1000, // Creating a selection using Ctrl + psShowPluginNames = 0x2000, // Show plugin names in channel headers + psRowSelection = 0x4000, // Selecting a whole pattern row by clicking the row numbers + psChannelSelection = 0x8000, // Double-clicked pattern to select a whole channel + psDragging = 0x10000, // Drag&Drop: Dragging an item around + psShiftDragging = 0x20000, // Drag&Drop: Dragging an item around and holding shift + + // All possible drag flags, to test if user is dragging a selection or a scrollbar + psDragActive = psDragVScroll | psMouseDragSelect | psRowSelection | psChannelSelection, + }; + +protected: + CFastBitmap m_Dib; + CDC m_offScreenDC; + CBitmap m_offScreenBitmap; + CEditCommand *m_pEditWnd = nullptr; + CSize m_szHeader, m_szPluginHeader, m_szCell; + CRect m_oldClient; + UINT m_nMidRow, m_nSpacing, m_nAccelChar, m_nLastPlayedRow, m_nLastPlayedOrder; + FlagSet<PatternStatus> m_Status; + ROWINDEX m_nPlayRow, m_nNextPlayRow; + uint32 m_nPlayTick, m_nTicksOnRow; + PATTERNINDEX m_nPattern = 0, m_nPlayPat = 0; + ORDERINDEX m_nOrder = 0; + static int32 m_nTransposeAmount; + + int m_nXScroll = 0, m_nYScroll = 0; + PatternCursor::Columns m_nDetailLevel = PatternCursor::lastColumn; // Visible Columns + + // Cursor and selection positions + PatternCursor m_Cursor; // Current cursor position in pattern. + PatternCursor m_StartSel, m_DragPos; // Point where selection was started. + PatternCursor m_MenuCursor; // Position at which context menu was opened. + PatternRect m_Selection; // Upper-left / Lower-right corners of selection. + + // Drag&Drop + DragItem m_nDragItem; // Currently dragged item + DragItem m_nDropItem; // Currently hovered item during dragondrop + RECT m_rcDragItem, m_rcDropItem; + bool m_bInItemRect = false; + + // Drag-select record group + std::vector<RecordGroup> m_initialDragRecordStatus; + + ModCommand::INSTR m_fallbackInstrument = 0; + + // Chord auto-detect interval + DWORD m_autoChordStartTime = 0; + ROWINDEX m_autoChordStartRow = ROWINDEX_INVALID; + ORDERINDEX m_autoChordStartOrder = ORDERINDEX_INVALID; + + bool m_bContinueSearch : 1, m_bWholePatternFitsOnScreen : 1; + + ModCommand m_PCNoteEditMemory; // PC Note edit memory + static ModCommand m_cmdOld; // Quick cursor copy/paste data + + QuickChannelProperties m_quickChannelProperties; + + // Chord preview + CHANNELINDEX m_chordPatternChannels[MPTChord::notesPerChord]; + ModCommand::NOTE m_prevChordNote, m_prevChordBaseNote; + + // Note-off event buffer for MIDI sustain pedal + std::array<std::vector<uint32>, 16> m_midiSustainBuffer; + std::bitset<16> m_midiSustainActive; + + std::array<uint16, MAX_BASECHANNELS> ChnVUMeters; + std::array<uint16, MAX_BASECHANNELS> OldVUMeters; + + std::bitset<128> m_baPlayingNote; + CModDoc::NoteToChannelMap m_noteChannel; // Note -> Preview channel assignment + std::array<ModCommand::NOTE, 10> m_octaveKeyMemory; + std::array<ModCommand::NOTE, MAX_BASECHANNELS> m_previousNote; + std::array<uint8, NOTE_MAX + NOTE_MIN> m_activeNoteChannel; + std::array<uint8, NOTE_MAX + NOTE_MIN> m_splitActiveNoteChannel; + static constexpr uint8 NOTE_CHANNEL_MAP_INVALID = 0xFF; + static_assert(MAX_BASECHANNELS <= std::numeric_limits<decltype(m_activeNoteChannel)::value_type>::max()); + static_assert(MAX_BASECHANNELS <= NOTE_CHANNEL_MAP_INVALID); + +public: + std::unique_ptr<CEffectVis> m_pEffectVis; + + CViewPattern(); + ~CViewPattern(); + DECLARE_SERIAL(CViewPattern) + +public: + const CSoundFile *GetSoundFile() const; + CSoundFile *GetSoundFile(); + + const ModSequence &Order() const; + ModSequence &Order(); + + void SetModified(bool updateAllViews = true); + + bool UpdateSizes(); + void UpdateScrollSize(); + void UpdateScrollPos(); + void UpdateIndicator(bool updateAccessibility = true); + void UpdateXInfoText(); + void UpdateColors(); + + CString GetCursorDescription() const; + + int GetXScrollPos() const { return m_nXScroll; } + int GetYScrollPos() const { return m_nYScroll; } + int GetChannelWidth() const { return m_szCell.cx; } + int GetRowHeight() const { return m_szCell.cy; } + int GetSmoothScrollOffset() const; + + PATTERNINDEX GetCurrentPattern() const { return m_nPattern; } + ROWINDEX GetCurrentRow() const { return m_Cursor.GetRow(); } + CHANNELINDEX GetCurrentChannel() const { return m_Cursor.GetChannel(); } + ORDERINDEX GetCurrentOrder() const { return m_nOrder; } + void SetCurrentOrder(ORDERINDEX ord) + { + m_nOrder = ord; + SendCtrlMessage(CTRLMSG_SETCURRENTORDER, ord); + } + // Get ModCommand at the pattern cursor position. + ModCommand &GetCursorCommand() { return GetModCommand(m_Cursor); }; + const ModCommand& GetCursorCommand() const { return const_cast<CViewPattern *>(this)->GetModCommand(m_Cursor); }; + void SanitizeCursor(); + + UINT GetColumnOffset(PatternCursor::Columns column) const; + POINT GetPointFromPosition(PatternCursor cursor) const; + PatternCursor GetPositionFromPoint(POINT pt) const; + + DragItem GetDragItem(CPoint point, RECT &rect) const; + + void StartRecordGroupDragging(const DragItem source); + void ResetRecordGroupDragging() { m_initialDragRecordStatus.clear(); } + bool IsDraggingRecordGroup() const { return !m_initialDragRecordStatus.empty(); } + + ROWINDEX GetRowsPerBeat() const; + ROWINDEX GetRowsPerMeasure() const; + + // Invalidate functions (for redrawing areas of the pattern) + void InvalidatePattern(bool invalidateChannelHeaders = false, bool invalidateRowHeaders = false); + void InvalidateRow(ROWINDEX n = ROWINDEX_INVALID); + void InvalidateArea(const PatternRect &rect) { InvalidateArea(rect.GetUpperLeft(), rect.GetLowerRight()); }; + void InvalidateArea(PatternCursor begin, PatternCursor end); + void InvalidateSelection() { InvalidateArea(m_Selection); } + void InvalidateCell(PatternCursor cursor); + void InvalidateChannelsHeaders(CHANNELINDEX chn = CHANNELINDEX_INVALID); + + // Selection functions + void SetCurSel(const PatternRect &rect) { SetCurSel(rect.GetUpperLeft(), rect.GetLowerRight()); }; + void SetCurSel(const PatternCursor &point) { SetCurSel(point, point); }; + void SetCurSel(PatternCursor beginSel, PatternCursor endSel); + void SetSelToCursor() { SetCurSel(m_Cursor); }; + + bool SetCurrentPattern(PATTERNINDEX pat, ROWINDEX row = ROWINDEX_INVALID); + ROWINDEX SetCurrentRow(ROWINDEX row, bool wrap = false, bool updateHorizontalScrollbar = true); + bool SetCurrentColumn(const PatternCursor &cursor) { return SetCurrentColumn(cursor.GetChannel(), cursor.GetColumnType()); }; + bool SetCurrentColumn(CHANNELINDEX channel, PatternCursor::Columns column = PatternCursor::firstColumn); + // This should be used instead of consecutive calls to SetCurrentRow() then SetCurrentColumn() + bool SetCursorPosition(const PatternCursor &cursor, bool wrap = false); + bool DragToSel(const PatternCursor &cursor, bool scrollHorizontal, bool scrollVertical, bool noMove = false); + bool SetPlayCursor(PATTERNINDEX pat, ROWINDEX row, uint32 tick); + bool UpdateScrollbarPositions(bool updateHorizontalScrollbar = true); + bool ShowEditWindow(); + UINT GetCurrentInstrument() const; + void SelectBeatOrMeasure(bool selectBeat); + // Move pattern cursor to left or right, respecting invisible columns. + void MoveCursor(bool moveRight); + + bool TransposeSelection(int transp); + bool DataEntry(bool up, bool coarse); + + bool PrepareUndo(const PatternRect &selection, const char *description) { return PrepareUndo(selection.GetUpperLeft(), selection.GetLowerRight(), description); }; + bool PrepareUndo(const PatternCursor &beginSel, const PatternCursor &endSel, const char *description); + void UndoRedo(bool undo); + + bool InsertOrDeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit, bool deleteRows); + void DeleteRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit = false); + void InsertRows(CHANNELINDEX firstChn, CHANNELINDEX lastChn, bool globalEdit = false); + + void OnDropSelection(); + +public: + void DrawPatternData(HDC hdc, PATTERNINDEX nPattern, bool selEnable, bool isPlaying, ROWINDEX startRow, ROWINDEX numRows, CHANNELINDEX startChan, CRect &rcClient, int *pypaint); + void DrawLetter(int x, int y, char letter, int sizex = 10, int ofsx = 0); + void DrawLetter(int x, int y, wchar_t letter, int sizex = 10, int ofsx = 0); +#if MPT_CXX_AT_LEAST(20) + void DrawLetter(int x, int y, char8_t letter, int sizex = 10, int ofsx = 0); +#endif + void DrawNote(int x, int y, UINT note, CTuning *pTuning = nullptr); + void DrawInstrument(int x, int y, UINT instr); + void DrawVolumeCommand(int x, int y, const ModCommand &mc, bool drawDefaultVolume); + void DrawChannelVUMeter(HDC hdc, int x, int y, UINT nChn); + void UpdateAllVUMeters(Notification *pnotify); + void DrawDragSel(HDC hdc); + void OnDrawDragSel(); + // True if default volume should be drawn for a given cell. + static bool DrawDefaultVolume(const ModCommand *m) { return (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SHOWDEFAULTVOLUME) && m->volcmd == VOLCMD_NONE && m->command != CMD_VOLUME && m->instr != 0 && m->IsNote(); } + + void CursorJump(int distance, bool snap); + + void TempEnterNote(ModCommand::NOTE n, int vol = -1, bool fromMidi = false); + void TempStopNote(ModCommand::NOTE note, const bool fromMidi = false, bool chordMode = false); + void TempEnterChord(ModCommand::NOTE n); + void TempStopChord(ModCommand::NOTE note) { TempStopNote(note, false, true); } + void TempEnterIns(int val); + void TempEnterOctave(int val); + void TempStopOctave(int val); + void TempEnterVol(int v); + void TempEnterFX(ModCommand::COMMAND c, int v = -1); + void TempEnterFXparam(int v); + void EnterAftertouch(ModCommand::NOTE note, int atValue); + + int GetDefaultVolume(const ModCommand &m, ModCommand::INSTR lastInstr = 0) const; + int GetBaseNote() const; + ModCommand::NOTE GetNoteWithBaseOctave(int note) const; + + // Construct a chord from the chord presets. Returns number of notes in chord. + int ConstructChord(int note, ModCommand::NOTE (&outNotes)[MPTChord::notesPerChord], ModCommand::NOTE baseNote); + + void QuantizeRow(PATTERNINDEX &pat, ROWINDEX &row) const; + PATTERNINDEX GetPrevPattern() const; + PATTERNINDEX GetNextPattern() const; + + void SetSpacing(int n); + void OnClearField(const RowMask &mask, bool step, bool ITStyle = false); + void SetSelectionInstrument(const INSTRUMENTINDEX instr, bool setEmptyInstrument); + + void FindInstrument(); + void JumpToPrevOrNextEntry(bool nextEntry, bool select); + + void TogglePluginEditor(int chan); + + void ExecutePaste(PatternClipboard::PasteModes mode); + + // Reset all channel variables + void ResetChannel(CHANNELINDEX chn); + +public: + //{{AFX_VIRTUAL(CViewPattern) + void OnDraw(CDC *) override; + void OnInitialUpdate() override; + BOOL OnScrollBy(CSize sizeScroll, BOOL bDoScroll = TRUE) override; + BOOL PreTranslateMessage(MSG *pMsg) override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + LRESULT OnModViewMsg(WPARAM, LPARAM) override; + LRESULT OnPlayerNotify(Notification *) override; + INT_PTR OnToolHitTest(CPoint point, TOOLINFO *pTI) const override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CViewPattern) + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnDestroy(); + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + afx_msg void OnMouseMove(UINT, CPoint); + afx_msg void OnLButtonUp(UINT, CPoint); + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnLButtonDblClk(UINT, CPoint); + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar *pScrollBar); + afx_msg void OnSetFocus(CWnd *pOldWnd); + afx_msg void OnKillFocus(CWnd *pNewWnd); + afx_msg void OnEditCut(); + afx_msg void OnEditCopy(); + + afx_msg void OnEditPaste() { ExecutePaste(PatternClipboard::pmOverwrite); }; + afx_msg void OnEditMixPaste() { ExecutePaste(PatternClipboard::pmMixPaste); }; + afx_msg void OnEditMixPasteITStyle() { ExecutePaste(PatternClipboard::pmMixPasteIT); }; + afx_msg void OnEditPasteFlood() { ExecutePaste(PatternClipboard::pmPasteFlood); }; + afx_msg void OnEditPushForwardPaste() { ExecutePaste(PatternClipboard::pmPushForward); }; + + afx_msg void OnClearSelection(bool ITStyle = false, RowMask sb = RowMask()); + afx_msg void OnGrowSelection(); + afx_msg void OnShrinkSelection(); + afx_msg void OnEditSelectAll(); + afx_msg void OnEditSelectChannel(); + afx_msg void OnSelectCurrentChannel(); + afx_msg void OnSelectCurrentColumn(); + afx_msg void OnEditFind(); + afx_msg void OnEditGoto(); + afx_msg void OnEditFindNext(); + afx_msg void OnEditUndo(); + afx_msg void OnEditRedo(); + afx_msg void OnChannelReset(); + afx_msg void OnMuteFromClick(); + afx_msg void OnSoloFromClick(); + afx_msg void OnTogglePendingMuteFromClick(); + afx_msg void OnPendingSoloChnFromClick(); + afx_msg void OnPendingUnmuteAllChnFromClick(); + afx_msg void OnSoloChannel(CHANNELINDEX chn); + afx_msg void OnMuteChannel(CHANNELINDEX chn); + afx_msg void OnUnmuteAll(); + afx_msg void OnRecordSelect(); + afx_msg void OnSplitRecordSelect(); + afx_msg void OnDeleteRow(); + afx_msg void OnDeleteWholeRow(); + afx_msg void OnDeleteRowGlobal(); + afx_msg void OnDeleteWholeRowGlobal(); + afx_msg void OnInsertRow(); + afx_msg void OnInsertWholeRow(); + afx_msg void OnInsertRowGlobal(); + afx_msg void OnInsertWholeRowGlobal(); + afx_msg void OnSplitPattern(); + afx_msg void OnPatternStep(); + afx_msg void OnSwitchToOrderList(); + afx_msg void OnPrevOrder(); + afx_msg void OnNextOrder(); + afx_msg void OnPrevInstrument() { PostCtrlMessage(CTRLMSG_PAT_PREVINSTRUMENT); } + afx_msg void OnNextInstrument() { PostCtrlMessage(CTRLMSG_PAT_NEXTINSTRUMENT); } + afx_msg void OnPatternRecord() { PostCtrlMessage(CTRLMSG_SETRECORD, -1); } + afx_msg void OnInterpolateVolume() { Interpolate(PatternCursor::volumeColumn); } + afx_msg void OnInterpolateEffect() { Interpolate(PatternCursor::effectColumn); } + afx_msg void OnInterpolateNote() { Interpolate(PatternCursor::noteColumn); } + afx_msg void OnInterpolateInstr() { Interpolate(PatternCursor::instrColumn); } + afx_msg void OnVisualizeEffect(); + afx_msg void OnTransposeUp() { TransposeSelection(1); } + afx_msg void OnTransposeDown() { TransposeSelection(-1); } + afx_msg void OnTransposeOctUp() { TransposeSelection(12000); } + afx_msg void OnTransposeOctDown() { TransposeSelection(-12000); } + afx_msg void OnTransposeCustom(); + afx_msg void OnTransposeCustomQuick(); + afx_msg void OnSetSelInstrument(); + afx_msg void OnAddChannelFront() { AddChannel(m_MenuCursor.GetChannel(), false); } + afx_msg void OnAddChannelAfter() { AddChannel(m_MenuCursor.GetChannel(), true); }; + afx_msg void OnDuplicateChannel(); + afx_msg void OnResetChannelColors(); + afx_msg void OnTransposeChannel(); + afx_msg void OnRemoveChannel(); + afx_msg void OnRemoveChannelDialog(); + afx_msg void OnPatternProperties() { ShowPatternProperties(PATTERNINDEX_INVALID); } + void ShowPatternProperties(PATTERNINDEX pat); + void OnCursorCopy(); + void OnCursorPaste(); + afx_msg void OnPatternAmplify(); + afx_msg void OnUpdateUndo(CCmdUI *pCmdUI); + afx_msg void OnUpdateRedo(CCmdUI *pCmdUI); + afx_msg void OnSelectPlugin(UINT nID); + afx_msg LRESULT OnUpdatePosition(WPARAM nOrd, LPARAM nRow); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + afx_msg LRESULT OnRecordPlugParamChange(WPARAM, LPARAM); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + afx_msg void OnClearSelectionFromMenu(); + afx_msg void OnSelectInstrument(UINT nid); + afx_msg void OnSelectPCNoteParam(UINT nid); + afx_msg void OnRunScript(); + afx_msg void OnShowTimeAtRow(); + afx_msg void OnTogglePCNotePluginEditor(); + afx_msg void OnSetQuantize(); + afx_msg void OnLockPatternRows(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() + + +public: + afx_msg void OnInitMenu(CMenu *pMenu); + +private: + // Copy&Paste + bool CopyPattern(PATTERNINDEX nPattern, const PatternRect &selection); + bool PastePattern(PATTERNINDEX nPattern, const PatternCursor &pastePos, PatternClipboard::PasteModes mode); + + void SetSplitKeyboardSettings(); + bool HandleSplit(ModCommand &m, int note); + bool IsNoteSplit(int note) const; + + CHANNELINDEX FindGroupRecordChannel(RecordGroup recordGroup, bool forceFreeChannel, CHANNELINDEX startChannel = 0) const; + + bool BuildChannelControlCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildPluginCtxMenu(HMENU hMenu, UINT nChn, const CSoundFile &sndFile) const; + bool BuildRecordCtxMenu(HMENU hMenu, CInputHandler *ih, CHANNELINDEX nChn) const; + bool BuildSoloMuteCtxMenu(HMENU hMenu, CInputHandler *ih, UINT nChn, const CSoundFile &sndFile) const; + bool BuildRowInsDelCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildMiscCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildSelectionCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildGrowShrinkCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildInterpolationCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildInterpolationCtxMenu(HMENU hMenu, PatternCursor::Columns colType, CString label, UINT command) const; + bool BuildEditCtxMenu(HMENU hMenu, CInputHandler *ih, CModDoc *pModDoc) const; + bool BuildVisFXCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildTransposeCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildSetInstCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildAmplifyCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildPCNoteCtxMenu(HMENU hMenu, CInputHandler *ih) const; + bool BuildTogglePlugEditorCtxMenu(HMENU hMenu, CInputHandler *ih) const; + + // Returns an ordered list of all channels in which a given column type is selected. + CHANNELINDEX ListChansWhereColSelected(PatternCursor::Columns colType, std::vector<CHANNELINDEX> &chans) const; + // Check if a column type is selected on any channel in the current selection. + bool IsColumnSelected(PatternCursor::Columns colType) const; + + bool IsInterpolationPossible(PatternCursor::Columns colType) const; + bool IsInterpolationPossible(ROWINDEX startRow, ROWINDEX endRow, CHANNELINDEX chan, PatternCursor::Columns colType) const; + void Interpolate(PatternCursor::Columns type); + PatternRect SweepPattern(bool (*startCond)(const ModCommand &), bool (*endCond)(const ModCommand &, const ModCommand &)) const; + + // Return true if recording live (i.e. editing while following playback). + bool IsLiveRecord() const + { + const CMainFrame *mainFrm = CMainFrame::GetMainFrame(); + const CSoundFile *sndFile = GetSoundFile(); + if(mainFrm == nullptr || sndFile == nullptr) + { + return false; + } + // (following song) && (following in correct document) && (playback is on) + return m_Status[psFollowSong] && mainFrm->GetFollowSong(GetDocument()) == m_hWnd && !sndFile->IsPaused(); + }; + + // Returns edit position. + PatternEditPos GetEditPos(const CSoundFile &sndFile, const bool liveRecord) const; + + // Returns pointer to modcommand at given position. + // If the position is not valid, a pointer to a dummy command is returned. + ModCommand &GetModCommand(PatternCursor cursor); + ModCommand &GetModCommand(CSoundFile &sndFile, const PatternEditPos &pos); + + // Returns true if pattern editing is enabled. + bool IsEditingEnabled() const { return m_Status[psRecordingEnabled]; } + + // Like IsEditingEnabled(), but shows some notification when editing is not enabled. + bool IsEditingEnabled_bmsg(); + + // Play one pattern row and stop ("step mode") + void PatternStep(ROWINDEX row = ROWINDEX_INVALID); + + // Add a channel. + void AddChannel(CHANNELINDEX parent, bool afterCurrent); + + void DragChannel(CHANNELINDEX source, CHANNELINDEX target, CHANNELINDEX numChannels, bool duplicate); + +public: + afx_msg void OnRButtonDblClk(UINT nFlags, CPoint point); + +private: + void TogglePendingMute(CHANNELINDEX nChn); + void PendingSoloChn(CHANNELINDEX nChn); + + template <typename Func> + void ApplyToSelection(Func func); + + void PlayNote(ModCommand::NOTE note, ModCommand::INSTR instr, int volume, CHANNELINDEX channel); + void PreviewNote(ROWINDEX row, CHANNELINDEX channel); + +public: + afx_msg void OnRButtonUp(UINT nFlags, CPoint point); + + HRESULT get_accName(VARIANT varChild, BSTR *pszName) override; +}; + +DECLARE_FLAGSET(CViewPattern::PatternStatus); + +void getXParam(ModCommand::COMMAND command, PATTERNINDEX nPat, ROWINDEX nRow, CHANNELINDEX nChannel, const CSoundFile &sndFile, UINT &xparam, UINT &multiplier); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.cpp new file mode 100644 index 00000000..f35a202b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.cpp @@ -0,0 +1,4110 @@ +/* + * View_smp.cpp + * ------------ + * Purpose: Sample tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "ImageLists.h" +#include "Childfrm.h" +#include "Clipboard.h" +#include "Moddoc.h" +#include "Globals.h" +#include "Ctrl_smp.h" +#include "Dlsbank.h" +#include "ChannelManagerDlg.h" +#include "View_smp.h" +#include "OPLInstrDlg.h" +#include "../soundlib/MIDIEvents.h" +#include "SampleEditorDialogs.h" +#include "../soundlib/WAVTools.h" +#include "../common/FileReader.h" +#include "../tracklib/SampleEdit.h" +#include "openmpt/soundbase/SampleConvert.hpp" +#include "openmpt/soundbase/SampleDecode.hpp" +#include "../soundlib/SampleCopy.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/S3MTools.h" +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_span.hpp" +#include "mpt/io/io_stdstream.hpp" +#include "mpt/io/io_virtual_wrapper.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +// Non-client toolbar +#define SMP_LEFTBAR_CY Util::ScalePixels(29, m_hWnd) +#define SMP_LEFTBAR_CXSEP Util::ScalePixels(14, m_hWnd) +#define SMP_LEFTBAR_CXSPC Util::ScalePixels(3, m_hWnd) +#define SMP_LEFTBAR_CXBTN Util::ScalePixels(24, m_hWnd) +#define SMP_LEFTBAR_CYBTN Util::ScalePixels(22, m_hWnd) +static constexpr int TIMELINE_HEIGHT = 26; + +static int TimelineHeight(HWND hwnd) +{ + auto height = Util::ScalePixels(TIMELINE_HEIGHT, hwnd); + if(height % 2) + height++; // Avoid weird-looking triangles if timeline is scaled to odd height + return height; +} + + +static constexpr int MIN_ZOOM = -6; +static constexpr int MAX_ZOOM = 10; + +// Defines the minimum length for selection for which +// trimming will be done. This is the minimum value for +// selection difference, so the minimum length of result +// of trimming is nTrimLengthMin + 1. +static constexpr SmpLength MIN_TRIM_LENGTH = 4; + +static constexpr UINT cLeftBarButtons[SMP_LEFTBAR_BUTTONS] = +{ + ID_SAMPLE_ZOOMUP, + ID_SAMPLE_ZOOMDOWN, + ID_SEPARATOR, + ID_SAMPLE_DRAW, + ID_SAMPLE_ADDSILENCE, + ID_SEPARATOR, + ID_SAMPLE_GRID, + ID_SEPARATOR, +}; + + +IMPLEMENT_SERIAL(CViewSample, CModScrollView, 0) + +BEGIN_MESSAGE_MAP(CViewSample, CModScrollView) + //{{AFX_MSG_MAP(CViewSample) + ON_WM_ERASEBKGND() + ON_WM_SETFOCUS() + ON_WM_SIZE() + ON_WM_NCCALCSIZE() + ON_WM_NCHITTEST() + ON_WM_NCPAINT() + ON_WM_MOUSEMOVE() + ON_WM_NCMOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONUP() + ON_WM_LBUTTONDBLCLK() + ON_WM_NCLBUTTONDOWN() + ON_WM_NCLBUTTONUP() + ON_WM_NCLBUTTONDBLCLK() + ON_WM_RBUTTONDOWN() + ON_WM_CHAR() + ON_WM_DROPFILES() + ON_WM_MOUSEWHEEL() + ON_WM_XBUTTONUP() + ON_WM_SETCURSOR() + + ON_COMMAND(ID_EDIT_UNDO, &CViewSample::OnEditUndo) + ON_COMMAND(ID_EDIT_REDO, &CViewSample::OnEditRedo) + ON_COMMAND(ID_EDIT_SELECT_ALL, &CViewSample::OnEditSelectAll) + ON_COMMAND(ID_EDIT_CUT, &CViewSample::OnEditCut) + ON_COMMAND(ID_EDIT_COPY, &CViewSample::OnEditCopy) + ON_COMMAND(ID_EDIT_PASTE, &CViewSample::OnEditPaste) + ON_COMMAND(ID_EDIT_MIXPASTE, &CViewSample::OnEditMixPaste) + ON_COMMAND(ID_EDIT_PUSHFORWARDPASTE, &CViewSample::OnEditInsertPaste) + ON_COMMAND(ID_SAMPLE_SETLOOP, &CViewSample::OnSetLoop) + ON_COMMAND(ID_SAMPLE_SETSUSTAINLOOP, &CViewSample::OnSetSustainLoop) + ON_COMMAND(ID_SAMPLE_8BITCONVERT, &CViewSample::On8BitConvert) + ON_COMMAND(ID_SAMPLE_16BITCONVERT, &CViewSample::On16BitConvert) + ON_COMMAND(ID_SAMPLE_MONOCONVERT, &CViewSample::OnMonoConvertMix) + ON_COMMAND(ID_SAMPLE_MONOCONVERT_LEFT, &CViewSample::OnMonoConvertLeft) + ON_COMMAND(ID_SAMPLE_MONOCONVERT_RIGHT, &CViewSample::OnMonoConvertRight) + ON_COMMAND(ID_SAMPLE_MONOCONVERT_SPLIT, &CViewSample::OnMonoConvertSplit) + ON_COMMAND(ID_SAMPLE_TRIM, &CViewSample::OnSampleTrim) + ON_COMMAND(ID_PREVINSTRUMENT, &CViewSample::OnPrevInstrument) + ON_COMMAND(ID_NEXTINSTRUMENT, &CViewSample::OnNextInstrument) + ON_COMMAND(ID_SAMPLE_ZOOMONSEL, &CViewSample::OnZoomOnSel) + ON_COMMAND(ID_SAMPLE_SETLOOPSTART, &CViewSample::OnSetLoopStart) + ON_COMMAND(ID_SAMPLE_SETLOOPEND, &CViewSample::OnSetLoopEnd) + ON_COMMAND(ID_CONVERT_PINGPONG_LOOP, &CViewSample::OnConvertPingPongLoop) + ON_COMMAND(ID_SAMPLE_SETSUSTAINSTART, &CViewSample::OnSetSustainStart) + ON_COMMAND(ID_SAMPLE_SETSUSTAINEND, &CViewSample::OnSetSustainEnd) + ON_COMMAND(ID_CONVERT_PINGPONG_SUSTAIN, &CViewSample::OnConvertPingPongSustain) + ON_COMMAND(ID_SAMPLE_ZOOMUP, &CViewSample::OnZoomUp) + ON_COMMAND(ID_SAMPLE_ZOOMDOWN, &CViewSample::OnZoomDown) + ON_COMMAND(ID_SAMPLE_DRAW, &CViewSample::OnDrawingToggle) + ON_COMMAND(ID_SAMPLE_ADDSILENCE, &CViewSample::OnAddSilence) + ON_COMMAND(ID_SAMPLE_GRID, &CViewSample::OnChangeGridSize) + ON_COMMAND(ID_SAMPLE_QUICKFADE, &CViewSample::OnQuickFade) + ON_COMMAND(ID_SAMPLE_SLICE, &CViewSample::OnSampleSlice) + ON_COMMAND(ID_SAMPLE_INSERT_CUEPOINT, &CViewSample::OnSampleInsertCuePoint) + ON_COMMAND(ID_SAMPLE_DELETE_CUEPOINT, &CViewSample::OnSampleDeleteCuePoint) + ON_COMMAND(ID_SAMPLE_TIMELINE_SECONDS, &CViewSample::OnTimelineFormatSeconds) + ON_COMMAND(ID_SAMPLE_TIMELINE_SAMPLES, &CViewSample::OnTimelineFormatSamples) + ON_COMMAND(ID_SAMPLE_TIMELINE_SAMPLES_POW2, &CViewSample::OnTimelineFormatSamplesPow2) + ON_COMMAND_RANGE(ID_SAMPLE_CUE_1, ID_SAMPLE_CUE_9, &CViewSample::OnSetCuePoint) + ON_UPDATE_COMMAND_UI(ID_EDIT_UNDO, &CViewSample::OnUpdateUndo) + ON_UPDATE_COMMAND_UI(ID_EDIT_REDO, &CViewSample::OnUpdateRedo) + ON_MESSAGE(WM_MOD_MIDIMSG, &CViewSample::OnMidiMsg) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewSample::OnCustomKeyMsg) //rewbs.customKeys + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +/////////////////////////////////////////////////////////////// +// CViewSample operations + +CViewSample::CViewSample() + : m_lastDrawPoint(-1, -1) + , m_timelineHeight(TIMELINE_HEIGHT) +{ + MemsetZero(m_NcButtonState); + m_dwNotifyPos.fill(Notification::PosInvalid); + m_bmpEnvBar.Create(&CMainFrame::GetMainFrame()->m_SampleIcons); +} + + +void CViewSample::OnInitialUpdate() +{ + CModScrollView::OnInitialUpdate(); + m_dwBeginSel = m_dwEndSel = 0; + m_dwStatus.reset(SMPSTATUS_DRAWING); + ModifyStyleEx(0, WS_EX_ACCEPTFILES); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) + { + pMainFrm->SetInfoText(_T("")); + pMainFrm->SetXInfoText(_T("")); + } + UpdateOPLEditor(); + UpdateScrollSize(); + UpdateNcButtonState(); + EnableToolTips(); +} + + +// updateAll: Update all views including this one. Otherwise, only update update other views. +void CViewSample::SetModified(SampleHint hint, bool updateAll, bool waveformModified) +{ + CModDoc *pModDoc = GetDocument(); + pModDoc->SetModified(); + + if(waveformModified) + { + // Update on-disk sample status in tree + ModSample &sample = pModDoc->GetSoundFile().GetSample(m_nSample); + if(sample.uFlags[SMP_KEEPONDISK] && !sample.uFlags[SMP_MODIFIED]) + hint.Names(); + sample.uFlags.set(SMP_MODIFIED); + } + pModDoc->UpdateAllViews(nullptr, hint.SetData(m_nSample), updateAll ? nullptr : this); +} + + +void CViewSample::UpdateScrollSize(int newZoom, bool forceRefresh, SmpLength centeredSample) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr || (newZoom == m_nZoom && !forceRefresh)) + { + return; + } + + const int oldZoom = m_nZoom; + m_nZoom = newZoom; + + GetClientRect(&m_rcClient); + + if(m_oplEditor && IsOPLInstrument()) + { + const auto size = m_oplEditor->GetMinimumSize(); + m_oplEditor->SetWindowPos(nullptr, -m_nScrollPosX, -m_nScrollPosY, std::max(size.cx, m_rcClient.right), std::max(size.cy, m_rcClient.bottom), SWP_NOZORDER | SWP_NOACTIVATE); + SetScrollSizes(MM_TEXT, size); + return; + } + + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + SIZE sizePage, sizeLine; + SmpLength dwLen = 0; + uint32 sampleRate = 8363; + + if((m_nSample > 0) && (m_nSample <= sndFile.GetNumSamples())) + { + const ModSample &sample = sndFile.GetSample(m_nSample); + if(sample.HasSampleData()) + dwLen = sample.nLength; + sampleRate = sample.GetSampleRate(sndFile.GetType()); + } + // Compute scroll size in pixels + if (newZoom == 0) // Fit to display + m_sizeTotal.cx = m_rcClient.Width(); + else if(newZoom == 1) // 1:1 + m_sizeTotal.cx = dwLen; + else if(newZoom > 1) // Zoom out + m_sizeTotal.cx = (dwLen + (1 << (newZoom - 1)) - 1) >> (newZoom - 1); + else // Zoom in - here, we don't compute the real number of visible pixels so that the scroll bar doesn't grow unnecessarily long. The scrolling code in OnScrollBy() compensates for this. + m_sizeTotal.cx = dwLen + m_rcClient.Width() - (m_rcClient.Width() >> (-newZoom - 1)); + + m_sizeTotal.cy = 1; + sizeLine.cx = (m_rcClient.right / 16) + 1; + if(newZoom < 0) + sizeLine.cx >>= (-newZoom - 1); + sizeLine.cy = 1; + sizePage.cx = sizeLine.cx * 4; + sizePage.cy = 1; + + SetScrollSizes(MM_TEXT, m_sizeTotal, sizePage, sizeLine); + + if(oldZoom != newZoom) // After zoom change, keep the view position. + { + if(centeredSample != SmpLength(-1)) + { + ScrollToSample(centeredSample, false); + } else + { + const SmpLength nOldPos = ScrollPosToSamplePos(oldZoom); + const float fPosFraction = (dwLen > 0) ? static_cast<float>(nOldPos) / dwLen : 0; + SetScrollPos(SB_HORZ, static_cast<int>(fPosFraction * GetScrollLimit(SB_HORZ))); + } + } + + // Choose optimal timeline interval for this zoom level + if(m_sizeTotal.cx == 0 || dwLen == 0) + return; + + const TimelineFormat format = TrackerSettings::Instance().sampleEditorTimelineFormat; + double timelineInterval = MulDiv(150, m_nDPIx, 96); // Timeline interval should be around 150 pixels + if(m_nZoom > 0) + timelineInterval *= 1 << (m_nZoom - 1); + else if(m_nZoom < 0) + timelineInterval /= 1 << (-m_nZoom - 1); + else if(m_sizeTotal.cx != 0) + timelineInterval = timelineInterval * dwLen / m_sizeTotal.cx; + if(format == TimelineFormat::Seconds) + timelineInterval *= 1000.0 / sampleRate; + if(timelineInterval < 1) + timelineInterval = 1; + + const double power = (format == TimelineFormat::SamplesPow2) ? 2.0 : 10.0; + m_timelineUnit = mpt::saturate_round<int>(std::log(static_cast<double>(timelineInterval)) / std::log(power)); + if(m_timelineUnit < 1) + m_timelineUnit = 0; + m_timelineUnit = mpt::saturate_cast<int>(std::pow(power, m_timelineUnit)); + timelineInterval = std::max(1.0, std::round(timelineInterval / m_timelineUnit)) * m_timelineUnit; + if(format == TimelineFormat::Seconds) + timelineInterval *= sampleRate / 1000.0; + + if(m_nZoom > 0) + timelineInterval /= 1 << (m_nZoom - 1); + else if(m_nZoom < 0) + timelineInterval *= 1 << (-m_nZoom - 1); + else + timelineInterval = timelineInterval * m_sizeTotal.cx / dwLen; + m_timelineInterval = mpt::saturate_round<int>(timelineInterval); + + m_cachedSampleRate = sampleRate; +} + + +// Center given sample in the view +void CViewSample::ScrollToSample(SmpLength centeredSample, bool refresh) +{ + int scrollToSample = centeredSample >> (std::max(1, m_nZoom) - 1); + scrollToSample -= (m_rcClient.Width() / 2) >> (-std::min(-1, m_nZoom) - 1); + + Limit(scrollToSample, 0, GetScrollLimit(SB_HORZ)); + SetScrollPos(SB_HORZ, scrollToSample); + + if(refresh) InvalidateSample(); +} + + +int CViewSample::CalcScroll(int ¤tPos, int amount, int style, int bar) +{ + // Don't scroll if there is no valid scroll range (ie. no scroll bar) + const CScrollBar *pBar = GetScrollBarCtrl(bar); + DWORD dwStyle = GetStyle(); + if((pBar != nullptr && !pBar->IsWindowEnabled()) + || (pBar == nullptr && !(dwStyle & style))) + { + // Scroll bar not enabled + amount = 0; + } + + // Adjust current position + int orig = GetScrollPos(bar); + currentPos = Clamp(orig + amount, 0, GetScrollLimit(bar)); + + return -(currentPos - orig); +} + + +BOOL CViewSample::OnScrollBy(CSize sizeScroll, BOOL bDoScroll) +{ + int x = 0, y = 0; + int scrollByX = CalcScroll(x, sizeScroll.cx, WS_HSCROLL, SB_HORZ); + int scrollByY = CalcScroll(y, sizeScroll.cy, WS_VSCROLL, SB_VERT); + + if(!scrollByX && !scrollByY) + { + // Nothing changed! + return FALSE; + } + + if (bDoScroll) + { + // Don't allow to scroll into the middle of a sampling point + if(m_nZoom < 0 && !IsOPLInstrument()) + { + scrollByX *= (1 << (-m_nZoom - 1)); + } + + ScrollWindow(scrollByX, scrollByY); + if(scrollByX) SetScrollPos(SB_HORZ, x); + if(scrollByY) SetScrollPos(SB_VERT, y); + m_forceRedrawWaveform = true; + } + return TRUE; +} + + +void CViewSample::SetCurrentSample(SAMPLEINDEX nSmp) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return; + if(nSmp < 1 || nSmp > pModDoc->GetNumSamples()) + return; + pModDoc->SetNotifications(Notification::Sample, nSmp); + pModDoc->SetFollowWnd(m_hWnd); + if(nSmp == m_nSample) + return; + m_dwBeginSel = m_dwEndSel = 0; + m_dwStatus.reset(SMPSTATUS_DRAWING); + if(CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); pMainFrm) + pMainFrm->SetInfoText(_T("")); + const bool wasOPL = IsOPLInstrument(); + m_nSample = nSmp; + m_dwNotifyPos.fill(Notification::PosInvalid); + if(!wasOPL && IsOPLInstrument()) + SetScrollPos(SB_HORZ, 0); + UpdateOPLEditor(); + UpdateScrollSize(); + UpdateNcButtonState(); + InvalidateSample(); +} + + +bool CViewSample::IsOPLInstrument() const +{ + return m_nSample >= 1 && m_nSample <= GetDocument()->GetNumSamples() && GetDocument()->GetSoundFile().GetSample(m_nSample).uFlags[CHN_ADLIB]; +} + + +void CViewSample::UpdateOPLEditor() +{ + if(!IsOPLInstrument()) + { + if(m_oplEditor) + m_oplEditor->ShowWindow(SW_HIDE); + return; + } + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(sample.uFlags[CHN_ADLIB]) + { + if(!m_oplEditor) + { + try + { + m_oplEditor = std::make_unique<OPLInstrDlg>(*this, sndFile); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + if(m_oplEditor) + { + m_oplEditor->SetPatch(sample.adlib); + auto size = m_oplEditor->GetMinimumSize(); + m_oplEditor->SetWindowPos(nullptr, -m_nScrollPosX, -m_nScrollPosY, std::max(size.cx, m_rcClient.right), std::max(size.cy, m_rcClient.bottom), SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW); + } + } +} + + +void CViewSample::OnSetFocus(CWnd *pOldWnd) +{ + CScrollView::OnSetFocus(pOldWnd); + SetCurrentSample(m_nSample); +} + + +void CViewSample::SetZoom(int nZoom, SmpLength centeredSample) +{ + + if(nZoom == m_nZoom && centeredSample == SmpLength(-1)) + return; + if(nZoom > MAX_ZOOM) + return; + + UpdateScrollSize(nZoom, true, centeredSample); + InvalidateSample(); +} + + +SmpLength CViewSample::SnapToGrid(const SmpLength pos) const +{ + if(m_nGridSegments <= 0 || GetDocument() == nullptr) + return pos; + const auto &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); + if(m_nGridSegments >= sample.nLength) + return pos; + + const auto samplesPerSegment = static_cast<double>(sample.nLength) / m_nGridSegments; + return static_cast<SmpLength>(mpt::round(pos / samplesPerSegment) * samplesPerSegment); +} + + +void CViewSample::SetCurSel(SmpLength nBegin, SmpLength nEnd) +{ + if(GetDocument() == nullptr) + return; + + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + const ModSample &sample = sndFile.GetSample(m_nSample); + + nBegin = SnapToGrid(nBegin); + nEnd = SnapToGrid(nEnd); + + if(nBegin > nEnd) + { + std::swap(nBegin, nEnd); + } + + if ((nBegin != m_dwBeginSel) || (nEnd != m_dwEndSel)) + { + RECT rect; + SmpLength dMin = m_dwBeginSel, dMax = m_dwEndSel; + if (m_dwBeginSel >= m_dwEndSel) + { + dMin = nBegin; + dMax = nEnd; + } + if ((nBegin == dMin) && (dMax != nEnd)) + { + dMin = dMax; + if (nEnd < dMin) dMin = nEnd; + if (nEnd > dMax) dMax = nEnd; + } else if ((nEnd == dMax) && (dMin != nBegin)) + { + dMax = dMin; + if (nBegin < dMin) dMin = nBegin; + if (nBegin > dMax) dMax = nBegin; + } else + { + if (nBegin < dMin) dMin = nBegin; + if (nEnd > dMax) dMax = nEnd; + } + m_dwBeginSel = nBegin; + m_dwEndSel = nEnd; + rect.top = m_rcClient.top; + rect.bottom = m_rcClient.bottom; + rect.left = SampleToScreen(dMin); + rect.right = SampleToScreen(dMax) + 1; + if (rect.left < 0) rect.left = 0; + if (rect.right > m_rcClient.right) rect.right = m_rcClient.right; + if (rect.right > rect.left) InvalidateRect(&rect, FALSE); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + { + mpt::ustring s; + if(m_dwEndSel > m_dwBeginSel) + { + const SmpLength selLength = m_dwEndSel - m_dwBeginSel; + + mpt::ustring (*fmt)(unsigned int, char, const SmpLength &) = &mpt::ufmt::dec<SmpLength>; + if(TrackerSettings::Instance().cursorPositionInHex) + fmt = &mpt::ufmt::HEX<SmpLength>; + s = MPT_UFORMAT("[{}-{}] ({} sample{}, ")(fmt(3, ',', m_dwBeginSel), fmt(3, ',', m_dwEndSel), fmt(3, ',', selLength), (selLength == 1) ? U_("") : U_("s")); + + // Length in seconds + auto sampleRate = sample.GetSampleRate(sndFile.GetType()); + if(sampleRate <= 0) sampleRate = 8363; + double sec = selLength / static_cast<double>(sampleRate); + if(sec < 1) + s += MPT_UFORMAT("{}ms")(mpt::ufmt::flt(sec * 1000.0, 3)); + else + s += MPT_UFORMAT("{}s")(mpt::ufmt::flt(sec, 3)); + + // Length in beats + double beats = selLength; + if(sndFile.m_nTempoMode == TempoMode::Modern) + { + beats *= sndFile.m_PlayState.m_nMusicTempo.ToDouble() * (1.0 / 60.0) / sampleRate; + } else + { + sndFile.RecalculateSamplesPerTick(); + beats *= sndFile.GetSampleRate() / static_cast<double>(Util::mul32to64_unsigned(sndFile.m_PlayState.m_nCurrentRowsPerBeat, sndFile.m_PlayState.m_nMusicSpeed) * Util::mul32to64_unsigned(sndFile.m_PlayState.m_nSamplesPerTick, sampleRate)); + } + s += MPT_UFORMAT(", {} beats)")(mpt::ufmt::flt(beats, 5)); + } + pMainFrm->SetInfoText(mpt::ToCString(s)); + } + } +} + + +int32 CViewSample::SampleToScreen(SmpLength pos, bool ignoreScrollPos) const +{ + CModDoc *pModDoc = GetDocument(); + if((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) + { + SmpLength nLen = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; + if(!nLen) + return 0; + + const SmpLength scrollPos = ignoreScrollPos ? 0 : m_nScrollPosX; + if(m_nZoom > 0) + return (pos >> (m_nZoom - 1)) - scrollPos; + else if(m_nZoom < 0) + return (pos - scrollPos) << (-m_nZoom - 1); + else + return Util::muldiv(pos, m_sizeTotal.cx, nLen); + } + return 0; +} + + +SmpLength CViewSample::ScreenToSample(int32 x, bool ignoreSampleLength) const +{ + const CModDoc *pModDoc = GetDocument(); + SmpLength n = 0; + + if((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) + { + SmpLength smpLen = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; + if(!smpLen) + return 0; + + if(m_nZoom > 0) + n = std::max(0, m_nScrollPosX + x) << (m_nZoom - 1); + else if(m_nZoom < 0) + n = std::max(0, m_nScrollPosX + mpt::rshift_signed(x, (-m_nZoom - 1))); + else + { + if(x < 0) + x = 0; + if(m_sizeTotal.cx) + n = Util::muldiv(x, smpLen, m_sizeTotal.cx); + } + if(!ignoreSampleLength) + LimitMax(n, smpLen); + } + return n; +} + + +int32 CViewSample::SecondsToScreen(double x) const +{ + const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); + const auto sampleRate = sample.GetSampleRate(GetDocument()->GetModType()); + if(sampleRate == 0 || sample.nLength == 0) + return 0; + + x *= sampleRate; + // This is essentially duplicated from SampleToScreen but carried out in double precision to avoid rounding errors at very high zoom levels + if(m_nZoom > 0) + x = x / (1 << (m_nZoom - 1)) - m_nScrollPosX; + else if(m_nZoom < 0) + x = (x - m_nScrollPosX) * (1 << (-m_nZoom - 1)); + else + x = x * m_sizeTotal.cx / sample.nLength; + + return mpt::saturate_round<int32>(x); +} + + +double CViewSample::ScreenToSeconds(int32 x, bool ignoreSampleLength) const +{ + const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); + const auto sampleRate = sample.GetSampleRate(GetDocument()->GetModType()); + if(sampleRate == 0) + return 0; + return ScreenToSample(x, ignoreSampleLength) / static_cast<double>(sampleRate); +} + + +static bool HitTest(int pointX, int objX, int marginL, int marginR, int top, int bottom, CRect *rect) +{ + if(!mpt::is_in_range(pointX, objX - marginL, objX + marginR)) + return false; + if(rect) + *rect = CRect{objX - marginL, top, objX + marginR + 1, bottom}; + return true; +} + +std::pair<CViewSample::HitTestItem, SmpLength> CViewSample::PointToItem(CPoint point, CRect *rect) const +{ + if(IsOPLInstrument()) + return {HitTestItem::Nothing, MAX_SAMPLE_LENGTH}; + + const bool inTimeline = point.y < m_timelineHeight; + if(m_dwEndSel > m_dwBeginSel && !inTimeline && !m_dwStatus[SMPSTATUS_DRAWING]) + { + const int margin = Util::ScalePixels(5, m_hWnd); + if(HitTest(point.x, SampleToScreen(m_dwBeginSel), margin, margin, m_timelineHeight, m_rcClient.bottom, rect)) + return {HitTestItem::SelectionStart, m_dwBeginSel}; + if(HitTest(point.x, SampleToScreen(m_dwEndSel), margin, margin, m_timelineHeight, m_rcClient.bottom, rect)) + return {HitTestItem::SelectionEnd, m_dwEndSel}; + } + + const auto &sndFile = GetDocument()->GetSoundFile(); + if(m_nSample <= sndFile.GetNumSamples() && inTimeline) + { + const auto &sample = sndFile.GetSample(m_nSample); + if(sample.nSustainStart < sample.nSustainEnd && sample.nSustainStart < sample.nLength) + { + if(HitTest(point.x, SampleToScreen(sample.nSustainEnd), m_timelineHeight / 2, 0, 0, m_timelineHeight, rect)) + return {HitTestItem::SustainEnd, sample.nSustainEnd}; + if(HitTest(point.x, SampleToScreen(sample.nSustainStart), 0, m_timelineHeight / 2, 0, m_timelineHeight, rect)) + return {HitTestItem::SustainStart, sample.nSustainStart}; + } + if (sample.nLoopStart < sample.nLoopEnd && sample.nLoopStart < sample.nLength) + { + if(HitTest(point.x, SampleToScreen(sample.nLoopEnd), m_timelineHeight / 2, 0, 0, m_timelineHeight, rect)) + return {HitTestItem::LoopEnd, sample.nLoopEnd}; + if(HitTest(point.x, SampleToScreen(sample.nLoopStart), 0, m_timelineHeight / 2, 0, m_timelineHeight, rect)) + return {HitTestItem::LoopStart, sample.nLoopStart }; + } + for(size_t i = 0; i < std::size(sample.cues); i++) + { + size_t cue = std::size(sample.cues) - 1 - i; // If two cues overlap visually, the cue with the higher ID is drawn on top, so pick it first + if(sample.cues[cue] < sample.nLength && HitTest(point.x, SampleToScreen(sample.cues[cue]), m_timelineHeight / 2, m_timelineHeight / 2, 0, m_timelineHeight, rect)) + return {static_cast<HitTestItem>(static_cast<size_t>(HitTestItem::CuePointFirst) + cue), sample.cues[cue]}; + } + } + if(inTimeline) + return {HitTestItem::Nothing, MAX_SAMPLE_LENGTH}; + else + return {HitTestItem::SampleData, ScreenToSample(point.x)}; +} + + +void CViewSample::InvalidateSample(bool invalidateWaveform) +{ + if(invalidateWaveform) + m_forceRedrawWaveform = true; + InvalidateRect(nullptr, FALSE); +} + + +void CViewSample::InvalidateTimeline() +{ + auto rect = m_rcClient; + rect.bottom = m_timelineHeight; + InvalidateRect(rect, FALSE); +} + + +LRESULT CViewSample::OnModViewMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case VIEWMSG_SETCURRENTSAMPLE: + SetZoom(static_cast<int>(lParam) >> 16); + SetCurrentSample(lParam & 0xFFFF); + break; + + case VIEWMSG_LOADSTATE: + if (lParam) + { + SAMPLEVIEWSTATE *pState = (SAMPLEVIEWSTATE *)lParam; + if (pState->nSample == m_nSample) + { + SetCurSel(pState->dwBeginSel, pState->dwEndSel); + SetScrollPos(SB_HORZ, pState->dwScrollPos); + UpdateScrollSize(); + InvalidateSample(); + } + } + break; + + case VIEWMSG_SAVESTATE: + if (lParam) + { + SAMPLEVIEWSTATE *pState = (SAMPLEVIEWSTATE *)lParam; + pState->dwScrollPos = m_nScrollPosX; + pState->dwBeginSel = m_dwBeginSel; + pState->dwEndSel = m_dwEndSel; + pState->nSample = m_nSample; + } + break; + + case VIEWMSG_SETMODIFIED: + // Update from OPL editor + SetModified(UpdateHint::FromLPARAM(lParam).ToType<SampleHint>(), false, true); + GetDocument()->UpdateOPLInstrument(m_nSample); + break; + + case VIEWMSG_PREPAREUNDO: + GetDocument()->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Edit OPL Patch"); + break; + + case VIEWMSG_SETFOCUS: + case VIEWMSG_SETACTIVE: + GetParentFrame()->SetActiveView(this); + if(IsOPLInstrument() && m_oplEditor) + m_oplEditor->SetFocus(); + else + SetFocus(); + break; + + default: + return CModScrollView::OnModViewMsg(wParam, lParam); + } + return 0; +} + + +/////////////////////////////////////////////////////////////// +// CViewSample drawing + +void CViewSample::UpdateView(UpdateHint hint, CObject *pObj) +{ + if(pObj == this) + { + return; + } + auto modDoc = GetDocument(); + auto &sndFile = modDoc->GetSoundFile(); + + const SampleHint sampleHint = hint.ToType<SampleHint>(); + FlagSet<HintType> hintType = sampleHint.GetType(); + const SAMPLEINDEX updateSmp = sampleHint.GetSample(); + if(hintType[HINT_MPTOPTIONS | HINT_MODTYPE] + || (hintType[HINT_SAMPLEDATA] && (m_nSample == updateSmp || updateSmp == 0))) + { + if(hintType[HINT_SAMPLEDATA] && m_oplEditor && m_nSample <= sndFile.GetNumSamples()) + { + ModSample &sample = sndFile.GetSample(m_nSample); + if(sample.uFlags[CHN_ADLIB]) + m_oplEditor->SetPatch(sample.adlib); + } + UpdateOPLEditor(); + UpdateScrollSize(); + UpdateNcButtonState(); + InvalidateSample(); + } + if(hintType[HINT_SAMPLEINFO]) + { + // Sample rate change may imply redrawing of timeline + if(m_nSample <= sndFile.GetNumSamples() && m_cachedSampleRate != sndFile.GetSample(m_nSample).GetSampleRate(sndFile.GetType())) + { + UpdateScrollSize(); + InvalidateTimeline(); + } + if(m_nSample > sndFile.GetNumSamples() || !sndFile.GetSample(m_nSample).HasSampleData()) + { + // Disable sample drawing if we cannot actually draw anymore. + m_dwStatus.reset(SMPSTATUS_DRAWING); + UpdateNcButtonState(); + } + if(m_nSample == updateSmp || updateSmp == 0) + InvalidateSample(false); + } +} + +#define YCVT(n, bits) (ymed - (((n) * yrange) >> (bits))) + + +// Draw one channel of sample data, 1:1 ratio or higher (zoomed in) +void CViewSample::DrawSampleData1(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData) +{ + int smplsize; + int yrange = cy/2; + const int8 *psample = static_cast<const int8 *>(pSampleData); + int y0 = 0; + + smplsize = (uFlags & CHN_16BIT) ? 2 : 1; + if (uFlags & CHN_STEREO) smplsize *= 2; + if (uFlags & CHN_16BIT) + { + y0 = YCVT(*((signed short *)(psample-smplsize)),15); + } else + { + y0 = YCVT(*(psample-smplsize),7); + } + ::MoveToEx(hdc, -1, y0, NULL); + + SmpLength numDrawSamples, loopDiv = 0; + int loopShift = 0; + if (m_nZoom == 1) + { + // Linear 1:1 scale + numDrawSamples = cx; + } else if(m_nZoom < 0) + { + // 2:1, 4:1, etc... zoom + loopShift = (-m_nZoom - 1); + // Round up + numDrawSamples = (cx + (1 << loopShift) - 1) >> loopShift; + } else + { + // Stretch to screen + ASSERT(!m_nZoom); + numDrawSamples = len; + loopDiv = numDrawSamples; + } + LimitMax(numDrawSamples, len); + + if (uFlags & CHN_16BIT) + { + // 16-Bit + for (SmpLength n = 0; n <= numDrawSamples; n++) + { + int x = loopDiv ? ((n * cx) / loopDiv) : (n << loopShift); + int y = *(const int16 *)psample; + ::LineTo(hdc, x, YCVT(y,15)); + psample += smplsize; + } + } else + { + // 8-bit + for (SmpLength n = 0; n <= numDrawSamples; n++) + { + int x = loopDiv ? ((n * cx) / loopDiv) : (n << loopShift); + int y = *psample; + ::LineTo(hdc, x, YCVT(y,7)); + psample += smplsize; + } + } +} + + +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) + +OPENMPT_NAMESPACE_END +#include <emmintrin.h> +OPENMPT_NAMESPACE_BEGIN + +// SSE2 implementation for min/max finder, packs 8*int16 in a 128-bit XMM register. +// scanlen = How many samples to process on this channel +static void sse2_findminmax16(const void *p, SmpLength scanlen, int channels, int &smin, int &smax) +{ + scanlen *= channels; + + // Put minimum / maximum in 8 packed int16 values + __m128i minVal = _mm_set1_epi16(static_cast<int16>(smin)); + __m128i maxVal = _mm_set1_epi16(static_cast<int16>(smax)); + + SmpLength scanlen8 = scanlen / 8; + if(scanlen8) + { + const __m128i *v = static_cast<const __m128i *>(p); + p = static_cast<const __m128i *>(p) + scanlen8; + + while(scanlen8--) + { + __m128i curVals = _mm_loadu_si128(v++); + minVal = _mm_min_epi16(minVal, curVals); + maxVal = _mm_max_epi16(maxVal, curVals); + } + + // Now we have 8 minima and maxima each, in case of stereo they are interleaved L/R values. + // Move the upper 4 values to the lower half and compute the minima/maxima of that. + __m128i minVal2 = _mm_unpackhi_epi64(minVal, minVal); + __m128i maxVal2 = _mm_unpackhi_epi64(maxVal, maxVal); + minVal = _mm_min_epi16(minVal, minVal2); + maxVal = _mm_max_epi16(maxVal, maxVal2); + + // Now we have 4 minima and maxima each, in case of stereo they are interleaved L/R values. + // Move the upper 2 values to the lower half and compute the minima/maxima of that. + minVal2 = _mm_shuffle_epi32(minVal, _MM_SHUFFLE(1, 1, 1, 1)); + maxVal2 = _mm_shuffle_epi32(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); + minVal = _mm_min_epi16(minVal, minVal2); + maxVal = _mm_max_epi16(maxVal, maxVal2); + + if(channels < 2) + { + // Mono: Compute the minima/maxima of the both remaining values + minVal2 = _mm_shufflelo_epi16(minVal, _MM_SHUFFLE(1, 1, 1, 1)); + maxVal2 = _mm_shufflelo_epi16(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); + minVal = _mm_min_epi16(minVal, minVal2); + maxVal = _mm_max_epi16(maxVal, maxVal2); + } + } + + const int16 *p16 = static_cast<const int16 *>(p); + while(scanlen & 7) + { + scanlen -= channels; + __m128i curVals = _mm_set1_epi16(*p16); + p16 += channels; + minVal = _mm_min_epi16(minVal, curVals); + maxVal = _mm_max_epi16(maxVal, curVals); + } + + smin = static_cast<int16>(_mm_cvtsi128_si32(minVal)); + smax = static_cast<int16>(_mm_cvtsi128_si32(maxVal)); +} + + +// SSE2 implementation for min/max finder, packs 16*int8 in a 128-bit XMM register. +// scanlen = How many samples to process on this channel +static void sse2_findminmax8(const void *p, SmpLength scanlen, int channels, int &smin, int &smax) +{ + scanlen *= channels; + + // Put minimum / maximum in 16 packed int8 values + __m128i minVal = _mm_set1_epi8(static_cast<int8>(smin ^ 0x80u)); + __m128i maxVal = _mm_set1_epi8(static_cast<int8>(smax ^ 0x80u)); + + // For signed <-> unsigned conversion (_mm_min_epi8/_mm_max_epi8 is SSE4) + __m128i xorVal = _mm_set1_epi8(0x80u); + + SmpLength scanlen16 = scanlen / 16; + if(scanlen16) + { + const __m128i *v = static_cast<const __m128i *>(p); + p = static_cast<const __m128i *>(p) + scanlen16; + + while(scanlen16--) + { + __m128i curVals = _mm_loadu_si128(v++); + curVals = _mm_xor_si128(curVals, xorVal); + minVal = _mm_min_epu8(minVal, curVals); + maxVal = _mm_max_epu8(maxVal, curVals); + } + + // Now we have 16 minima and maxima each, in case of stereo they are interleaved L/R values. + // Move the upper 8 values to the lower half and compute the minima/maxima of that. + __m128i minVal2 = _mm_unpackhi_epi64(minVal, minVal); + __m128i maxVal2 = _mm_unpackhi_epi64(maxVal, maxVal); + minVal = _mm_min_epu8(minVal, minVal2); + maxVal = _mm_max_epu8(maxVal, maxVal2); + + // Now we have 8 minima and maxima each, in case of stereo they are interleaved L/R values. + // Move the upper 4 values to the lower half and compute the minima/maxima of that. + minVal2 = _mm_shuffle_epi32(minVal, _MM_SHUFFLE(1, 1, 1, 1)); + maxVal2 = _mm_shuffle_epi32(maxVal, _MM_SHUFFLE(1, 1, 1, 1)); + minVal = _mm_min_epu8(minVal, minVal2); + maxVal = _mm_max_epu8(maxVal, maxVal2); + + // Now we have 4 minima and maxima each, in case of stereo they are interleaved L/R values. + // Move the upper 2 values to the lower half and compute the minima/maxima of that. + minVal2 = _mm_srai_epi32(minVal, 16); + maxVal2 = _mm_srai_epi32(maxVal, 16); + minVal = _mm_min_epu8(minVal, minVal2); + maxVal = _mm_max_epu8(maxVal, maxVal2); + + if(channels < 2) + { + // Mono: Compute the minima/maxima of the both remaining values + minVal2 = _mm_srai_epi16(minVal, 8); + maxVal2 = _mm_srai_epi16(maxVal, 8); + minVal = _mm_min_epu8(minVal, minVal2); + maxVal = _mm_max_epu8(maxVal, maxVal2); + } + } + + const int8 *p8 = static_cast<const int8 *>(p); + while(scanlen & 15) + { + scanlen -= channels; + __m128i curVals = _mm_set1_epi8((*p8) ^ 0x80u); + p8 += channels; + minVal = _mm_min_epu8(minVal, curVals); + maxVal = _mm_max_epu8(maxVal, curVals); + } + + smin = static_cast<int8>(_mm_cvtsi128_si32(minVal) ^ 0x80u); + smax = static_cast<int8>(_mm_cvtsi128_si32(maxVal) ^ 0x80u); +} + + +#endif + + +std::pair<int, int> CViewSample::FindMinMax(const int8 *p, SmpLength numSamples, int numChannels) +{ + int minVal = 127; + int maxVal = -128; +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) + if(CPU::HasFeatureSet(CPU::feature::sse2) && numSamples >= 16) + { + sse2_findminmax8(p, numSamples, numChannels, minVal, maxVal); + } else +#endif + { + while(numSamples--) + { + + int s = *p; + if(s < minVal) minVal = s; + if(s > maxVal) maxVal = s; + p += numChannels; + } + } + return { minVal, maxVal }; +} + + +std::pair<int, int> CViewSample::FindMinMax(const int16 *p, SmpLength numSamples, int numChannels) +{ + int minVal = 32767; + int maxVal = -32768; +#if defined(MPT_ENABLE_ARCH_INTRINSICS_SSE2) + if(CPU::HasFeatureSet(CPU::feature::sse2) && numSamples >= 8) + { + sse2_findminmax16(p, numSamples, numChannels, minVal, maxVal); + } else +#endif + { + while(numSamples--) + { + int s = *p; + if(s < minVal) minVal = s; + if(s > maxVal) maxVal = s; + p += numChannels; + } + } + return { minVal, maxVal }; +} + + +// Draw one channel of zoomed-out sample data +void CViewSample::DrawSampleData2(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData) +{ + int oldsmin, oldsmax; + int yrange = cy/2; + const int8 *psample = static_cast<const int8 *>(pSampleData); + int32 y0 = 0, xmax; + SmpLength poshi; + uint64 posincr, posfrac; // Increments have 16-bit fractional part + + if (len <= 0) return; + const int numChannels = (uFlags & CHN_STEREO) ? 2 : 1; + const int smplsize = ((uFlags & CHN_16BIT) ? 2 : 1) * numChannels; + + if (uFlags & CHN_16BIT) + { + y0 = YCVT(*((const int16 *)(psample-smplsize)), 15); + } else + { + y0 = YCVT(*(psample-smplsize), 7); + } + oldsmin = oldsmax = y0; + if (m_nZoom > 0) + { + xmax = len>>(m_nZoom-1); + if (xmax > cx) xmax = cx; + posincr = (uint64(1) << (m_nZoom-1+16)); + } else + { + xmax = cx; + //posincr = Util::muldiv(len, 0x10000, cx); + posincr = uint64(len) * uint64(0x10000) / uint64(cx); + } + ::MoveToEx(hdc, 0, ymed, NULL); + posfrac = 0; + poshi = 0; + for (int x=0; x<xmax; x++) + { + //int smin, smax, scanlen; + int smin, smax; + SmpLength scanlen; + + posfrac += posincr; + scanlen = static_cast<int32>((posfrac+0xffff) >> 16); + if (poshi >= len) poshi = len-1; + if (poshi + scanlen > len) scanlen = len-poshi; + if (scanlen < 1) scanlen = 1; + // 16-bit + if (uFlags & CHN_16BIT) + { + signed short *p = (signed short *)(psample + poshi*smplsize); + auto minMax = FindMinMax(p, scanlen, numChannels); + smin = YCVT(minMax.first, 15); + smax = YCVT(minMax.second, 15); + } else + // 8-bit + { + const int8 *p = psample + poshi * smplsize; + auto minMax = FindMinMax(p, scanlen, numChannels); + smin = YCVT(minMax.first, 7); + smax = YCVT(minMax.second, 7); + } + if (smin > oldsmax) + { + ::MoveToEx(hdc, x-1, oldsmax - 1, NULL); + ::LineTo(hdc, x, smin); + } + if (smax < oldsmin) + { + ::MoveToEx(hdc, x-1, oldsmin, NULL); + ::LineTo(hdc, x, smax); + } + ::MoveToEx(hdc, x, smax-1, NULL); + ::LineTo(hdc, x, smin); + oldsmin = smin; + oldsmax = smax; + poshi += static_cast<int32>(posfrac>>16); + posfrac &= 0xffff; + } +} + + +static void DrawTriangleHorz(CDC &dc, int x, int width, int height, COLORREF color) +{ + const POINT points[] = + { + {x, 0}, + {x, height}, + {x + width, height / 2}, + }; + dc.SetDCPenColor(RGB(GetRValue(color) / 2, GetGValue(color) / 2, GetBValue(color) / 2)); + dc.SetDCBrushColor(color); + dc.Polygon(points, static_cast<int>(std::size(points))); +} + +static void DrawTriangleVert(CDC &dc, int x, int width, int height, COLORREF color) +{ + const POINT points[] = + { + {x - width, height / 2}, + {x + width, height / 2}, + {x, height}, + }; + dc.SetDCPenColor(RGB(GetRValue(color) / 2, GetGValue(color) / 2, GetBValue(color) / 2)); + dc.SetDCBrushColor(color); + dc.Polygon(points, static_cast<int>(std::size(points))); +} + + +void CViewSample::OnDraw(CDC *pDC) +{ + const CModDoc *pModDoc = GetDocument(); + if ((!pModDoc) || (!pDC)) return; + + const CRect rcClient = m_rcClient; + CRect rect, rc; + const SmpLength smpScrollPos = ScrollPosToSamplePos(); + const auto &colors = TrackerSettings::Instance().rgbCustomColors; + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + const ModSample &sample = sndFile.GetSample((m_nSample <= sndFile.GetNumSamples()) ? m_nSample : 0); + if(sample.uFlags[CHN_ADLIB]) + { + CModScrollView::OnDraw(pDC); + return; + } + + // Create off-screen image and timeline font + if(!m_offScreenDC.m_hDC) + { + m_offScreenDC.CreateCompatibleDC(pDC); + m_offScreenBitmap.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); + m_offScreenDC.SelectObject(m_offScreenBitmap); + + NONCLIENTMETRICS metrics; + metrics.cbSize = sizeof(metrics); + SystemParametersInfo(SPI_GETNONCLIENTMETRICS, sizeof(metrics), &metrics, 0); + metrics.lfMessageFont.lfHeight = mpt::saturate_round<int>(metrics.lfMessageFont.lfHeight * 0.8); + metrics.lfMessageFont.lfWidth = mpt::saturate_round<int>(metrics.lfMessageFont.lfWidth * 0.8); + m_timelineFont.DeleteObject(); + m_timelineFont.CreateFontIndirect(&metrics.lfMessageFont); + } + if(!m_waveformDC.m_hDC) + { + m_waveformDC.CreateCompatibleDC(pDC); + m_waveformBitmap.CreateCompatibleBitmap(pDC, m_rcClient.Width(), m_rcClient.Height()); + m_waveformDC.SelectObject(m_waveformBitmap); + m_forceRedrawWaveform = true; + } + + const auto oldPen = m_offScreenDC.SelectObject(CMainFrame::penDarkGray); + const auto oldBrush = m_offScreenDC.SelectStockObject(DC_BRUSH); + const auto oldFont = m_offScreenDC.SelectObject(m_timelineFont); + + // Draw timeline + const int timelineHeight = TimelineHeight(m_hWnd); + if(timelineHeight != m_timelineHeight) + { + m_timelineHeight = timelineHeight; + m_forceRedrawWaveform = true; + } + + { + const TimelineFormat format = TrackerSettings::Instance().sampleEditorTimelineFormat; + CRect timeline = rcClient; + timeline.bottom = timeline.top + timelineHeight + 1; + m_offScreenDC.DrawEdge(timeline, EDGE_ETCHED, BF_MIDDLE | BF_BOTTOM); + m_offScreenDC.SetTextColor(GetSysColor(COLOR_BTNTEXT)); + m_offScreenDC.SetBkMode(TRANSPARENT); + if(!m_timelineUnit) + m_timelineUnit = 1; + + if(m_timelineInterval && sample.nLength) + { + rc = timeline; + const auto sampleRate = sample.GetSampleRate(sndFile.GetType()); + const int textOffset = Util::ScalePixels(4, m_hWnd); + mpt::tstring text; + for(int x = -(SampleToScreen(ScrollPosToSamplePos(), true) % m_timelineInterval); x < m_rcClient.right + m_timelineInterval; x += m_timelineInterval) + { + text.clear(); + if(format == TimelineFormat::Seconds && sampleRate) + { + const int64 time = mpt::saturate_round<int64>(std::round(ScreenToSeconds(x, true) * 1000.0 / m_timelineUnit) * m_timelineUnit); + + rc.left = SecondsToScreen(time / 1000.0); + if(rc.left >= m_rcClient.right) + break; + + const bool showSeconds = time >= 1000 || (time == 0 && m_timelineUnit >= 1000); + const auto secMs = std::div(time, int64(1000)); + if(showSeconds) + { + const auto minSec = std::div(secMs.quot, int64(60)); + const bool showMinutes = minSec.quot != 0 || (time == 0 && m_timelineUnit >= 60000); + if(showMinutes) + text += mpt::tfmt::dec(3, _T(','), minSec.quot) + _T("mn"); + if(minSec.rem || !showMinutes) + text += mpt::tfmt::dec(3, _T(','), minSec.rem) + _T("s"); + } + if(secMs.rem || !showSeconds) + { + text += mpt::tfmt::val(secMs.rem) + _T("ms"); + } + } else + { + const SmpLength smp = mpt::saturate_round<SmpLength>(std::round(ScreenToSample(x, true) / static_cast<double>(m_timelineUnit)) * m_timelineUnit); + + rc.left = SampleToScreen(smp); + if(rc.left >= m_rcClient.right) + break; + + text += mpt::tfmt::dec(3, _T(','), smp) + _T(" smp"); + } + + rc.bottom = timelineHeight; + for(int i = 0; i < 10; i++) + { + rect = rc; + rect.left += i * m_timelineInterval / 10; + if(i == 0) + rect.top = 0; + else if(i == 5) + rect.top = timelineHeight / 2; + else + rect.top = timelineHeight - timelineHeight / 4; + m_offScreenDC.DrawEdge(rect, EDGE_ETCHED, BF_LEFT); + } + rc.bottom = timelineHeight / 2; + rc.left += textOffset; + m_offScreenDC.DrawText(text.c_str(), rc, DT_VCENTER | DT_SINGLELINE | DT_NOPREFIX); + } + + // Cues + m_offScreenDC.SelectStockObject(DC_PEN); + m_offScreenDC.SetTextColor(RGB(0, 0, 0)); + const int arrowWidth = timelineHeight / 2; + for(size_t i = 0; i < std::size(sample.cues); i++) + { + if(sample.cues[i] >= sample.nLength) + continue; + int xl = SampleToScreen(sample.cues[i]); + if((xl >= -arrowWidth) && (xl < rcClient.right)) + { + DrawTriangleVert(m_offScreenDC, xl, arrowWidth, timelineHeight, colors[MODCOLOR_SAMPLE_CUEPOINT]); + rc.SetRect(xl - arrowWidth, timelineHeight / 2 - 1, xl + arrowWidth, timelineHeight); + m_offScreenDC.DrawText(mpt::tfmt::val(i + 1).c_str(), rc, DT_CENTER | DT_SINGLELINE | DT_NOPREFIX); + } + } + + // Loop Start/End + if(sample.nLoopEnd > sample.nLoopStart) + { + int xl = SampleToScreen(sample.nLoopStart); + if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) + DrawTriangleHorz(m_offScreenDC, xl, arrowWidth, timelineHeight, colors[MODCOLOR_SAMPLE_LOOPMARKER]); + + xl = SampleToScreen(sample.nLoopEnd); + if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) + DrawTriangleHorz(m_offScreenDC, xl, -timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_LOOPMARKER]); + } + + // Sustain Loop Start/End + if(sample.nSustainEnd > sample.nSustainStart) + { + int xl = SampleToScreen(sample.nSustainStart); + if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) + DrawTriangleHorz(m_offScreenDC, xl, timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_SUSTAINMARKER]); + + xl = SampleToScreen(sample.nSustainEnd); + if((xl >= -arrowWidth) && (xl <= rcClient.right + arrowWidth)) + DrawTriangleHorz(m_offScreenDC, xl, -timelineHeight / 2, timelineHeight, colors[MODCOLOR_SAMPLE_SUSTAINMARKER]); + } + } + } + + rect = rcClient; + rect.top = timelineHeight; + if((rcClient.bottom > rcClient.top) && (rcClient.right > rcClient.left)) + { + const int ymed = (rect.top + rect.bottom) / 2; + const int yrange = (rect.bottom - rect.top) / 2; + + // Erase background + if ((m_dwBeginSel < m_dwEndSel) && (m_dwEndSel > smpScrollPos)) + { + rc = rect; + if (m_dwBeginSel > smpScrollPos) + { + rc.right = SampleToScreen(m_dwBeginSel); + if (rc.right > rcClient.right) rc.right = rcClient.right; + if (rc.right > rc.left) m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_BACKSAMPLE]); + rc.left = rc.right; + } + if (rc.left < 0) rc.left = 0; + rc.right = SampleToScreen(m_dwEndSel) + 1; + if (rc.right > rcClient.right) rc.right = rcClient.right; + if(rc.right > rc.left) + m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_SAMPLESELECTED]); + rc.left = rc.right; + if (rc.left < 0) rc.left = 0; + rc.right = rcClient.right; + if(rc.right > rc.left) + m_offScreenDC.FillSolidRect(&rc, colors[MODCOLOR_BACKSAMPLE]); + } else + { + m_offScreenDC.FillSolidRect(&rect, colors[MODCOLOR_BACKSAMPLE]); + } + m_offScreenDC.SelectObject(CMainFrame::penDarkGray); + if (sample.uFlags[CHN_STEREO]) + { + m_offScreenDC.MoveTo(0, ymed - yrange / 2); + m_offScreenDC.LineTo(rcClient.right, ymed - yrange / 2); + m_offScreenDC.MoveTo(0, ymed + yrange / 2); + m_offScreenDC.LineTo(rcClient.right, ymed + yrange / 2); + } else + { + m_offScreenDC.MoveTo(0, ymed); + m_offScreenDC.LineTo(rcClient.right, ymed); + } + // Drawing sample + if(sample.HasSampleData() && yrange && (sample.nLength > 1) && (rect.right > 1)) + { + // Loop Start/End + if ((sample.nLoopEnd > smpScrollPos) && (sample.nLoopEnd > sample.nLoopStart)) + { + int xl = SampleToScreen(sample.nLoopStart); + if ((xl >= 0) && (xl < rcClient.right)) + { + m_offScreenDC.MoveTo(xl, rect.top); + m_offScreenDC.LineTo(xl, rect.bottom); + } + + xl = SampleToScreen(sample.nLoopEnd); + if((xl >= 0) && (xl < rcClient.right)) + { + m_offScreenDC.MoveTo(xl, rect.top); + m_offScreenDC.LineTo(xl, rect.bottom); + } + } + // Sustain Loop Start/End + if ((sample.nSustainEnd > smpScrollPos) && (sample.nSustainEnd > sample.nSustainStart)) + { + m_offScreenDC.SetBkMode(OPAQUE); + m_offScreenDC.SetBkColor(RGB(0xFF, 0xFF, 0xFF)); + m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); + int xl = SampleToScreen(sample.nSustainStart); + if ((xl >= 0) && (xl < rcClient.right)) + { + m_offScreenDC.MoveTo(xl, rect.top); + m_offScreenDC.LineTo(xl, rect.bottom); + } + + xl = SampleToScreen(sample.nSustainEnd); + if ((xl >= 0) && (xl < rcClient.right)) + { + m_offScreenDC.MoveTo(xl, rect.top); + m_offScreenDC.LineTo(xl, rect.bottom); + } + } + // Active cue point + if(IsCuePoint(m_dragItem)) + { + m_offScreenDC.SetBkMode(TRANSPARENT); + m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); + int xl = SampleToScreen(sample.cues[CuePointFromItem(m_dragItem)]); + if((xl >= 0) && (xl < rcClient.right)) + { + m_offScreenDC.MoveTo(xl, rect.top); + m_offScreenDC.LineTo(xl, rect.bottom); + } + } + + // Drawing Sample Data + const auto backgroundCol = ~colors[MODCOLOR_SAMPLE]; + if(m_forceRedrawWaveform) + { + m_forceRedrawWaveform = false; + m_waveformDC.SelectStockObject(DC_PEN); + m_waveformDC.FillSolidRect(rect, backgroundCol); + m_waveformDC.SetDCPenColor(colors[MODCOLOR_SAMPLE]); + const int smplsize = sample.GetBytesPerSample(); + if(m_nZoom == 1 || m_nZoom < 0 || ((!m_nZoom) && (sample.nLength <= (SmpLength)rect.Width()))) + { + // Draw sample data in 1:1 ratio or higher (zoom in) + SmpLength len = sample.nLength - smpScrollPos; + const std::byte *psample = sample.sampleb() + smpScrollPos * smplsize; + if(sample.uFlags[CHN_STEREO]) + { + DrawSampleData1(m_waveformDC, ymed - yrange / 2, rect.right, yrange, len, sample.uFlags, psample); + DrawSampleData1(m_waveformDC, ymed + yrange / 2, rect.right, yrange, len, sample.uFlags, psample + smplsize / 2); + } else + { + DrawSampleData1(m_waveformDC, ymed, rect.right, yrange * 2, len, sample.uFlags, psample); + } + } else + { + // Draw zoomed-out saple data + SmpLength len = sample.nLength; + int xscroll = 0; + if(m_nZoom > 0) + { + xscroll = smpScrollPos; + len -= smpScrollPos; + } + const std::byte *psample = sample.sampleb() + xscroll * smplsize; + if(sample.uFlags[CHN_STEREO]) + { + DrawSampleData2(m_waveformDC, ymed - yrange / 2, rect.right, yrange, len, sample.uFlags, psample); + DrawSampleData2(m_waveformDC, ymed + yrange / 2, rect.right, yrange, len, sample.uFlags, psample + smplsize / 2); + } else + { + DrawSampleData2(m_waveformDC, ymed, rect.right, yrange * 2, len, sample.uFlags, psample); + } + } + } + m_offScreenDC.TransparentBlt(rect.left, rect.top, rect.Width(), rect.Height(), &m_waveformDC, rect.left, rect.top, rect.Width(), rect.Height(), backgroundCol); + } + } + + if(m_nGridSegments > 0 && m_nGridSegments < sample.nLength && sample.nLength != 0) + { + // Draw sample grid + m_offScreenDC.SetBkColor(TrackerSettings::Instance().rgbCustomColors[MODCOLOR_BACKSAMPLE]); + m_offScreenDC.SelectObject(CMainFrame::penHalfDarkGray); + const auto segmentsByLength = static_cast<double>(m_nGridSegments) / sample.nLength; + const auto samplesPerSegment = static_cast<double>(sample.nLength) / m_nGridSegments; + const auto leftSegment = std::max(uint32(1), mpt::saturate_round<uint32>(ScreenToSample(rect.left) * segmentsByLength)); + const auto rightSegment = std::min(m_nGridSegments, mpt::saturate_round<uint32>(ScreenToSample(rect.right) * segmentsByLength)); + for(uint32 i = leftSegment; i <= rightSegment; i++) + { + int screenPos = SampleToScreen(mpt::saturate_round<SmpLength>(samplesPerSegment * i)); + m_offScreenDC.MoveTo(screenPos, rect.top); + m_offScreenDC.LineTo(screenPos, rect.bottom); + } + } + + DrawPositionMarks(); + + BitBlt(pDC->m_hDC, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_offScreenDC, 0, 0, SRCCOPY); + + if(oldFont) + m_offScreenDC.SelectObject(oldFont); + if(oldBrush) + m_offScreenDC.SelectObject(oldBrush); + if(oldPen) + m_offScreenDC.SelectObject(oldPen); +} + + +void CViewSample::DrawPositionMarks() +{ + const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + { + return; + } + CRect rect; + for(auto pos : m_dwNotifyPos) if (pos != Notification::PosInvalid) + { + rect.top = m_timelineHeight; + rect.left = SampleToScreen(pos); + rect.right = rect.left + 1; + rect.bottom = m_rcClient.bottom + 1; + if ((rect.right >= 0) && (rect.right < m_rcClient.right)) m_offScreenDC.InvertRect(&rect); + } +} + + +LRESULT CViewSample::OnPlayerNotify(Notification *pnotify) +{ + CModDoc *pModDoc = GetDocument(); + if ((!pnotify) || (!pModDoc)) return 0; + if (pnotify->type[Notification::Stop]) + { + bool invalidate = false; + for(auto &pos : m_dwNotifyPos) + { + if(pos != Notification::PosInvalid) + { + pos = Notification::PosInvalid; + invalidate = true; + } + } + if(invalidate) + InvalidateSample(false); + } else if (pnotify->type[Notification::Sample] && pnotify->item == m_nSample && !IsOPLInstrument()) + { + if(m_dwNotifyPos != pnotify->pos) + { + HDC hdc = ::GetDC(m_hWnd); + DrawPositionMarks(); // Erase old marks... + m_dwNotifyPos = pnotify->pos; + DrawPositionMarks(); // ...and draw new ones + BitBlt(hdc, m_rcClient.left, m_rcClient.top, m_rcClient.Width(), m_rcClient.Height(), m_offScreenDC, 0, 0, SRCCOPY); + ::ReleaseDC(m_hWnd, hdc); + } + } + return 0; +} + + +bool CViewSample::GetNcButtonRect(UINT button, CRect &rect) const +{ + rect.left = 4; + rect.top = 3; + rect.bottom = rect.top + SMP_LEFTBAR_CYBTN; + if(button >= SMP_LEFTBAR_BUTTONS) return false; + for(UINT i = 0; i < button; i++) + { + if(cLeftBarButtons[i] == ID_SEPARATOR) + rect.left += SMP_LEFTBAR_CXSEP; + else + rect.left += SMP_LEFTBAR_CXBTN + SMP_LEFTBAR_CXSPC; + } + if(cLeftBarButtons[button] == ID_SEPARATOR) + { + rect.left += SMP_LEFTBAR_CXSEP/2 - 2; + rect.right = rect.left + 2; + return false; + } else + { + rect.right = rect.left + SMP_LEFTBAR_CXBTN; + } + return true; +} + + +UINT CViewSample::GetNcButtonAtPoint(CPoint point, CRect *outRect) const +{ + CRect rect, rcWnd; + UINT button = uint32_max; + GetWindowRect(&rcWnd); + for(UINT i = 0; i < SMP_LEFTBAR_BUTTONS; i++) + { + if(!(m_NcButtonState[i] & NCBTNS_DISABLED) && GetNcButtonRect(i, rect)) + { + rect.OffsetRect(rcWnd.left, rcWnd.top); + if(rect.PtInRect(point)) + { + button = i; + break; + } + } + } + if(outRect) + *outRect = rect; + return button; +} + + +void CViewSample::DrawNcButton(CDC *pDC, UINT nBtn) +{ + CRect rect; + COLORREF crHi = GetSysColor(COLOR_3DHILIGHT); + COLORREF crDk = GetSysColor(COLOR_3DSHADOW); + COLORREF crFc = GetSysColor(COLOR_3DFACE); + COLORREF c1, c2; + + if(GetNcButtonRect(nBtn, rect)) + { + DWORD dwStyle = m_NcButtonState[nBtn]; + COLORREF c3, c4; + int xofs = 0, yofs = 0, nImage = 0; + + c1 = c2 = c3 = c4 = crFc; + if (!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c1 = c3 = crHi; + c2 = crDk; + c4 = RGB(0,0,0); + } + if (dwStyle & (NCBTNS_PUSHED|NCBTNS_CHECKED)) + { + c1 = crDk; + c2 = crHi; + if (!(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c4 = crHi; + c3 = (dwStyle & NCBTNS_PUSHED) ? RGB(0,0,0) : crDk; + } + xofs = yofs = 1; + } else + if ((dwStyle & NCBTNS_MOUSEOVER) && (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS)) + { + c1 = crHi; + c2 = crDk; + } + switch(cLeftBarButtons[nBtn]) + { + case ID_SAMPLE_ZOOMUP: nImage = SIMAGE_ZOOMUP; break; + case ID_SAMPLE_ZOOMDOWN: nImage = SIMAGE_ZOOMDOWN; break; + case ID_SAMPLE_DRAW: nImage = SIMAGE_DRAW; break; + case ID_SAMPLE_ADDSILENCE: nImage = SIMAGE_RESIZE; break; + case ID_SAMPLE_GRID: nImage = SIMAGE_GRID; break; + } + pDC->Draw3dRect(rect.left-1, rect.top-1, SMP_LEFTBAR_CXBTN+2, SMP_LEFTBAR_CYBTN+2, c3, c4); + pDC->Draw3dRect(rect.left, rect.top, SMP_LEFTBAR_CXBTN, SMP_LEFTBAR_CYBTN, c1, c2); + rect.DeflateRect(1, 1); + pDC->FillSolidRect(&rect, crFc); + rect.left += xofs; + rect.top += yofs; + if (dwStyle & NCBTNS_CHECKED) m_bmpEnvBar.Draw(pDC, SIMAGE_CHECKED, rect.TopLeft(), ILD_NORMAL); + m_bmpEnvBar.Draw(pDC, nImage, rect.TopLeft(), ILD_NORMAL); + } else + { + c1 = c2 = crFc; + if (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_FLATBUTTONS) + { + c1 = crDk; + c2 = crHi; + } + pDC->Draw3dRect(rect.left, rect.top, 2, SMP_LEFTBAR_CYBTN, c1, c2); + } +} + + +void CViewSample::OnNcPaint() +{ + RECT rect; + + CModScrollView::OnNcPaint(); + GetWindowRect(&rect); + // Assumes there is no other non-client items + rect.bottom = SMP_LEFTBAR_CY; + rect.right -= rect.left; + rect.left = 0; + rect.top = 0; + if ((rect.left < rect.right) && (rect.top < rect.bottom)) + { + CDC *pDC = GetWindowDC(); + { + // Shadow + auto shadowRect = rect; + shadowRect.top = shadowRect.bottom - 1; + pDC->FillSolidRect(&shadowRect, GetSysColor(COLOR_BTNSHADOW)); + } + rect.bottom--; + if(rect.top < rect.bottom) + pDC->FillSolidRect(&rect, GetSysColor(COLOR_BTNFACE)); + if(rect.top + 2 < rect.bottom) + { + for (UINT i=0; i<SMP_LEFTBAR_BUTTONS; i++) + { + DrawNcButton(pDC, i); + } + } + ReleaseDC(pDC); + } +} + + +void CViewSample::UpdateNcButtonState() +{ + CModDoc *pModDoc = GetDocument(); + CDC *pDC = NULL; + + if (!pModDoc) return; + for (UINT i=0; i<SMP_LEFTBAR_BUTTONS; i++) if (cLeftBarButtons[i] != ID_SEPARATOR) + { + DWORD dwStyle = 0; + + if (m_nBtnMouseOver == i) + { + dwStyle |= NCBTNS_MOUSEOVER; + if(m_dwStatus[SMPSTATUS_NCLBTNDOWN]) dwStyle |= NCBTNS_PUSHED; + } + + switch(cLeftBarButtons[i]) + { + case ID_SAMPLE_DRAW: + if(m_dwStatus[SMPSTATUS_DRAWING]) dwStyle |= NCBTNS_CHECKED; + if(m_nSample > pModDoc->GetNumSamples() || IsOPLInstrument()) + { + dwStyle |= NCBTNS_DISABLED; + } + break; + case ID_SAMPLE_ZOOMUP: + case ID_SAMPLE_ZOOMDOWN: + case ID_SAMPLE_GRID: + if(IsOPLInstrument()) dwStyle |= NCBTNS_DISABLED; + break; + } + + if (dwStyle != m_NcButtonState[i]) + { + m_NcButtonState[i] = dwStyle; + if (!pDC) pDC = GetWindowDC(); + DrawNcButton(pDC, i); + } + } + if (pDC) ReleaseDC(pDC); +} + + +/////////////////////////////////////////////////////////////// +// CViewSample messages + +void CViewSample::OnSize(UINT nType, int cx, int cy) +{ + CModScrollView::OnSize(nType, cx, cy); + + m_offScreenBitmap.DeleteObject(); + m_offScreenDC.DeleteDC(); + m_waveformBitmap.DeleteObject(); + m_waveformDC.DeleteDC(); + + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0)) + { + UpdateScrollSize(); + } +} + + +void CViewSample::OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp) +{ + CModScrollView::OnNcCalcSize(bCalcValidRects, lpncsp); + if (lpncsp) + { + lpncsp->rgrc[0].top += SMP_LEFTBAR_CY; + if (lpncsp->rgrc[0].bottom < lpncsp->rgrc[0].top) lpncsp->rgrc[0].top = lpncsp->rgrc[0].bottom; + } +} + + +void CViewSample::ScrollToPosition(int x) // logical coordinates +{ + CPoint pt; + // now in device coordinates - limit if out of range + int xMax = GetScrollLimit(SB_HORZ); + pt.x = x; + pt.y = 0; + if (pt.x < 0) + pt.x = 0; + else if (pt.x > xMax) + pt.x = xMax; + ScrollToDevicePosition(pt); +} + + +template<class T, class uT> +T CViewSample::GetSampleValueFromPoint(const ModSample &smp, const CPoint &point) const +{ + static_assert(sizeof(T) == sizeof(uT) && sizeof(T) <= 2); + const int channelHeight = (m_rcClient.Height() - m_timelineHeight) / smp.GetNumChannels(); + int yPos = point.y - m_drawChannel * channelHeight - m_timelineHeight; + + int value = std::numeric_limits<T>::max() - std::numeric_limits<uT>::max() * yPos / channelHeight; + Limit(value, std::numeric_limits<T>::min(), std::numeric_limits<T>::max()); + return static_cast<T>(value); +} + + +template<class T, class uT> +void CViewSample::SetInitialDrawPoint(ModSample &smp, const CPoint &point) +{ + if(m_rcClient.Height() >= m_timelineHeight) + m_drawChannel = (point.y - m_timelineHeight) * smp.GetNumChannels() / (m_rcClient.Height() - m_timelineHeight); + else + m_drawChannel = 0; + Limit(m_drawChannel, 0, (int)smp.GetNumChannels() - 1); + + T *data = static_cast<T *>(smp.samplev()) + m_drawChannel; + data[m_dwEndDrag * smp.GetNumChannels()] = GetSampleValueFromPoint<T, uT>(smp, point); +} + + +template<class T, class uT> +void CViewSample::SetSampleData(ModSample &smp, const CPoint &point, const SmpLength old) +{ + T *data = static_cast<T *>(smp.samplev()) + m_drawChannel + old * smp.GetNumChannels(); + const int oldvalue = *data; + const int value = GetSampleValueFromPoint<T, uT>(smp, point); + const int inc = (m_dwEndDrag > old ? 1 : -1); + const int ptrInc = inc * smp.GetNumChannels(); + + for(SmpLength i = old; i != m_dwEndDrag; i += inc, data += ptrInc) + { + *data = static_cast<T>(static_cast<double>(oldvalue) + (value - oldvalue) * (static_cast<double>(i - old) / static_cast<double>(m_dwEndDrag - old))); + } + *data = static_cast<T>(value); +} + + +void CViewSample::OnMouseMove(UINT flags, CPoint point) +{ + CModDoc *pModDoc = GetDocument(); + + if(m_nBtnMouseOver < SMP_LEFTBAR_BUTTONS || m_dwStatus[SMPSTATUS_NCLBTNDOWN]) + { + m_dwStatus.reset(SMPSTATUS_NCLBTNDOWN); + m_nBtnMouseOver = 0xFFFF; + UpdateNcButtonState(); + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if (pMainFrm) pMainFrm->SetHelpText(_T("")); + } + if(!pModDoc) + return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + if(m_nSample > sndFile.GetNumSamples()) + return; + auto &sample = sndFile.GetSample(m_nSample); + + if (m_rcClient.PtInRect(point)) + { + const SmpLength x = ScreenToSample(point.x); + CString(*fmt)(unsigned int, char, const SmpLength &) = &mpt::cfmt::dec<SmpLength>; + if(TrackerSettings::Instance().cursorPositionInHex) + fmt = &mpt::cfmt::HEX<SmpLength>; + UpdateIndicator(MPT_CFORMAT("Cursor: {}")(fmt(3, ',', x))); + + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm && m_dwEndSel <= m_dwBeginSel) + { + // Show cursor position as offset effect if no selection is made. + if(m_nSample > 0 && sample.HasSampleData() && x < sample.nLength) + { + const SmpLength xLow = (x / 0x100) % 0x100; + const SmpLength xHigh = x / 0x10000; + + const char offsetChar = sndFile.GetModSpecifications().GetEffectLetter(CMD_OFFSET); + const bool hasHighOffset = (sndFile.GetType() & (MOD_TYPE_S3M | MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)); + const char highOffsetChar = sndFile.GetModSpecifications().GetEffectLetter(static_cast<ModCommand::COMMAND>(sndFile.GetModSpecifications().HasCommand(CMD_S3MCMDEX) ? CMD_S3MCMDEX : CMD_XFINEPORTAUPDOWN)); + + CString s; + if(xHigh == 0) + s.Format(_T("Offset: %c%02X"), offsetChar, xLow); + else if(hasHighOffset && xHigh < 0x10) + s.Format(_T("Offset: %c%02X, %cA%X"), offsetChar, xLow, highOffsetChar, xHigh); + else + s = _T("Beyond offset range"); + pMainFrm->SetInfoText(s); + + double linear; + SmpLength offset = x * sample.GetNumChannels() + (point.y - m_timelineHeight) * sample.GetNumChannels() / (m_rcClient.Height() - m_timelineHeight); + if(sample.uFlags[CHN_16BIT]) + linear = sample.sample16()[offset] / 32768.0; + else + linear = sample.sample8()[offset] / 128.0; + pMainFrm->SetXInfoText(MPT_TFORMAT("Value At Cursor: {}% / {}")(mpt::tfmt::fix(linear * 100.0, 3), CModDoc::LinearToDecibels(std::abs(linear), 1.0)).c_str()); + } else + { + pMainFrm->SetInfoText(_T("")); + pMainFrm->SetXInfoText(_T("")); + } + } + } else + { + UpdateIndicator(nullptr); + } + + if(m_dwStatus[SMPSTATUS_MOUSEDRAG]) + { + const SmpLength len = sndFile.GetSample(m_nSample).nLength; + if(!len) + return; + SmpLength old = m_dwEndDrag; + if(m_nZoom) + { + if(point.x < 0) + { + CPoint pt; + pt.x = point.x; + pt.y = 0; + if(OnScrollBy(pt)) + { + UpdateWindow(); + } + point.x = 0; + } + if (point.x > m_rcClient.right) + { + CPoint pt; + pt.x = point.x - m_rcClient.right; + pt.y = 0; + if (OnScrollBy(pt)) + { + UpdateWindow(); + } + point.x = m_rcClient.right; + } + } + + // Note: point.x might have changed in if block above in case we're scrolling. + SmpLength x; + if(m_dwStatus[SMPSTATUS_DRAWING]) + { + // Do not snap to grid and adjust for mouse-down position when drawing + x = ScreenToSample(point.x); + } else if(m_fineDrag) + { + x = m_startDragValue + (point.x - m_startDragPoint.x) / Util::ScalePixels(2, m_hWnd); + } else if (m_nZoom < 0 || (m_nZoom == 0 && sample.nLength > static_cast<SmpLength>(m_rcClient.Width()))) + { + // Don't adjust selection to mouse down point when zooming into the sample + x = SnapToGrid(ScreenToSample(point.x)); + } else + { + x = SnapToGrid(ScreenToSample(SampleToScreen(m_startDragValue) + point.x - m_startDragPoint.x)); + } + + if((flags & MK_SHIFT) && !m_fineDrag) + { + m_fineDrag = true; + m_startDragPoint = point; + m_startDragValue = x; + } else if(!(flags & MK_SHIFT) && m_fineDrag) + { + m_fineDrag = false; + m_startDragPoint = point; + m_startDragValue = ScreenToSample(point.x); + } + + bool update = false; + SmpLength *updateLoopPoint = nullptr; + const char *updateLoopDesc = nullptr; + switch(m_dragItem) + { + case HitTestItem::SelectionStart: + case HitTestItem::SelectionEnd: + if(m_dwEndDrag != x) + { + m_dwEndDrag = x; + SetCurSel(m_dwBeginDrag, m_dwEndDrag); + update = true; + } + break; + case HitTestItem::LoopStart: + if(x < sample.nLoopEnd) + { + updateLoopPoint = &sample.nLoopStart; + updateLoopDesc = "Set Loop Start"; + } + break; + case HitTestItem::LoopEnd: + if(x > sample.nLoopStart) + { + updateLoopPoint = &sample.nLoopEnd; + updateLoopDesc = "Set Loop End"; + } + break; + case HitTestItem::SustainStart: + if(x < sample.nSustainEnd) + { + updateLoopPoint = &sample.nSustainStart; + updateLoopDesc = "Set Sustain Start"; + } + break; + case HitTestItem::SustainEnd: + if(x > sample.nSustainStart) + { + updateLoopPoint = &sample.nSustainEnd; + updateLoopDesc = "Set Sustain End"; + } + break; + default: + if(IsCuePoint(m_dragItem)) + { + int cue = CuePointFromItem(m_dragItem); + updateLoopPoint = &sample.cues[cue]; + updateLoopDesc ="Set Cue Point"; + } + break; + } + + if(updateLoopPoint && updateLoopDesc && *updateLoopPoint != x) + { + if(!m_dragPreparedUndo) + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, updateLoopDesc); + m_dragPreparedUndo = true; + update = true; + *updateLoopPoint = x; + sample.PrecomputeLoops(sndFile, true); + SetModified(SampleHint().Info(), true, false); + } + + if(m_dwStatus[SMPSTATUS_DRAWING] && m_dragItem == HitTestItem::SampleData) + { + m_dwEndDrag = x; + if(m_dwEndDrag < len) + { + // Shift = draw horizontal lines + if(flags & MK_SHIFT) + { + if(m_lastDrawPoint.y != -1) + point.y = m_lastDrawPoint.y; + m_lastDrawPoint = point; + } else + { + m_lastDrawPoint.SetPoint(-1, -1); + } + + LimitMax(old, sample.nLength); + if(sample.GetElementarySampleSize() == 2) + SetSampleData<int16, uint16>(sample, point, old); + else if(sample.GetElementarySampleSize() == 1) + SetSampleData<int8, uint8>(sample, point, old); + + sample.PrecomputeLoops(sndFile, false); + + InvalidateSample(); + SetModified(SampleHint().Data(), false, true); + } + } else if(update) + { + UpdateWindow(); + } + } +} + + +BOOL CViewSample::OnSetCursor(CWnd *pWnd, UINT nHitTest, UINT message) +{ + // Update mouse cursor if we are close to a selection point + if(nHitTest == HTCLIENT && (message == WM_MOUSEMOVE || message == WM_LBUTTONDOWN)) + { + CPoint point; + GetCursorPos(&point); + ScreenToClient(&point); + const auto item = PointToItem(point).first; + if(item != HitTestItem::Nothing && item != HitTestItem::SampleData) + { + SetCursor(CMainFrame::curVSplit); + return TRUE; + } + } + + return CModScrollView::OnSetCursor(pWnd, nHitTest, message); +} + + +void CViewSample::OnLButtonDown(UINT flags, CPoint point) +{ + CModDoc *pModDoc = GetDocument(); + + if(m_dwStatus[SMPSTATUS_MOUSEDRAG] || (!pModDoc)) return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + + if (!sample.nLength) + return; + + m_dwStatus.set(SMPSTATUS_MOUSEDRAG); + SetFocus(); + SetCapture(); + bool oldsel = (m_dwBeginSel != m_dwEndSel); + + // shift + click = update selection + const auto [item, itemPos] = PointToItem(point); + if(!m_dwStatus[SMPSTATUS_DRAWING] && (flags & MK_SHIFT) && item == HitTestItem::SampleData) + { + oldsel = true; + m_dwEndDrag = itemPos; + SetCurSel(m_dwBeginDrag, m_dwEndDrag); + } else + { + m_dragItem = item; + m_startDragPoint = point; + m_startDragValue = itemPos; + m_fineDrag = (flags & MK_SHIFT); + m_dragPreparedUndo = false; + + switch(m_dragItem) + { + case HitTestItem::SampleData: + m_dwBeginDrag = m_dwEndDrag = ScreenToSample(point.x); + if(!m_dwStatus[SMPSTATUS_DRAWING]) + m_dragItem = HitTestItem::SelectionEnd; + break; + case HitTestItem::SelectionStart: + m_dwBeginDrag = m_dwEndSel; + m_dwEndDrag = itemPos; + break; + case HitTestItem::SelectionEnd: + m_dwBeginDrag = m_dwBeginSel; + m_dwEndDrag = itemPos; + break; + default: + if(IsCuePoint(m_dragItem)) + InvalidateSample(false); + break; + } + } + if(oldsel) + SetCurSel(m_dwBeginDrag, m_dwEndDrag); + + // set initial point for sample drawing + if (m_dwStatus[SMPSTATUS_DRAWING] && m_dragItem == HitTestItem::SampleData) + { + m_lastDrawPoint = point; + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Draw Sample"); + if(sample.GetElementarySampleSize() == 2) + SetInitialDrawPoint<int16, uint16>(sample, point); + else if(sample.GetElementarySampleSize() == 1) + SetInitialDrawPoint<int8, uint8>(sample, point); + + sndFile.GetSample(m_nSample).PrecomputeLoops(sndFile, false); + + InvalidateSample(); + SetModified(SampleHint().Data(), false, true); + } else + { + // ctrl + click = play from cursor pos + if(flags & MK_CONTROL) + PlayNote(NOTE_MIDDLEC, ScreenToSample(point.x)); + } +} + + +void CViewSample::OnLButtonUp(UINT, CPoint) +{ + if(m_dwStatus[SMPSTATUS_MOUSEDRAG]) + { + m_dwStatus.reset(SMPSTATUS_MOUSEDRAG); + ReleaseCapture(); + } + if(IsCuePoint(m_dragItem)) + InvalidateSample(false); + m_dragItem = HitTestItem::Nothing; + m_startDragValue = MAX_SAMPLE_LENGTH; + m_lastDrawPoint.SetPoint(-1, -1); +} + + +void CViewSample::OnLButtonDblClk(UINT, CPoint) +{ + CModDoc *pModDoc = GetDocument(); + + if (pModDoc) + { + SmpLength len = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; + if (len && !m_dwStatus[SMPSTATUS_DRAWING]) SetCurSel(0, len); + } +} + + +void CViewSample::OnRButtonDown(UINT, CPoint pt) +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + const ModSample &sample = sndFile.GetSample(m_nSample); + HMENU hMenu = ::CreatePopupMenu(); + CInputHandler* ih = CMainFrame::GetInputHandler(); + if(!hMenu) + return; + + TCHAR s[256]; + + if(pt.y < m_timelineHeight) + { + const auto item = PointToItem(pt).first; + if(IsCuePoint(item)) + { + m_dwMenuParam = CuePointFromItem(item); + wsprintf(s, _T("&Delete Cue Point %d"), 1 + static_cast<int>(m_dwMenuParam)); + ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_DELETE_CUEPOINT, s); + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + } else + { + if(*std::max_element(sample.cues.begin(), sample.cues.end()) >= sample.nLength) + { + m_dwMenuParam = ScreenToSample(pt.x); + wsprintf(s, _T("&Insert Cue Point at %s"), mpt::cfmt::dec(3, ',', m_dwMenuParam).GetString()); + ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_INSERT_CUEPOINT, s); + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + } + } + + auto fmt = TrackerSettings::Instance().sampleEditorTimelineFormat.Get(); + ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::Seconds ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SECONDS, _T("&Seconds")); + ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::Samples ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SAMPLES, _T("S&les")); + ::AppendMenu(hMenu, MF_STRING | (fmt == TimelineFormat::SamplesPow2 ? MF_CHECKED : 0), ID_SAMPLE_TIMELINE_SAMPLES_POW2, _T("Samples (&Power of 2)")); + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + return; + } + + if (sample.HasSampleData() && !sample.uFlags[CHN_ADLIB]) + { + if (m_dwEndSel >= m_dwBeginSel + 4) + { + ::AppendMenu(hMenu, MF_STRING | (CanZoomSelection() ? 0 : MF_GRAYED), ID_SAMPLE_ZOOMONSEL, ih->GetKeyTextFromCommand(kcSampleZoomSelection, _T("Zoom"))); + ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_SETLOOP, _T("Set As Loop")); + if (sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) + ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_SETSUSTAINLOOP, _T("Set As Sustain Loop")); + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + } else + { + SmpLength dwPos = ScreenToSample(pt.x); + CString pos = mpt::cfmt::dec(3, ',', dwPos); + if (dwPos <= sample.nLength) + { + //Set loop points + SmpLength loopEnd = (sample.nLoopEnd > 0) ? sample.nLoopEnd : sample.nLength; + wsprintf(s, _T("Set &Loop Start to:\t%s"), pos.GetString()); + ::AppendMenu(hMenu, MF_STRING | (dwPos + 4 <= loopEnd ? 0 : MF_GRAYED), + ID_SAMPLE_SETLOOPSTART, s); + wsprintf(s, _T("Set &Loop End to:\t%s"), pos.GetString()); + ::AppendMenu(hMenu, MF_STRING | (dwPos >= sample.nLoopStart + 4 ? 0 : MF_GRAYED), + ID_SAMPLE_SETLOOPEND, s); + if(sample.HasPingPongLoop()) + ::AppendMenu(hMenu, MF_STRING, ID_CONVERT_PINGPONG_LOOP, ih->GetKeyTextFromCommand(kcSampleConvertPingPongLoop, _T("Convert to Unidirectional Loop"))); + + if (sndFile.GetType() & (MOD_TYPE_IT|MOD_TYPE_MPT)) + { + //Set sustain loop points + SmpLength sustainEnd = (sample.nSustainEnd > 0) ? sample.nSustainEnd : sample.nLength; + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + wsprintf(s, _T("Set &Sustain Start to:\t%s"), pos.GetString()); + ::AppendMenu(hMenu, MF_STRING | (dwPos + 4 <= sustainEnd ? 0 : MF_GRAYED), + ID_SAMPLE_SETSUSTAINSTART, s); + wsprintf(s, _T("Set &Sustain End to:\t%s"), pos.GetString()); + ::AppendMenu(hMenu, MF_STRING | (dwPos >= sample.nSustainStart + 4 ? 0 : MF_GRAYED), + ID_SAMPLE_SETSUSTAINEND, s); + if(sample.HasPingPongSustainLoop()) + ::AppendMenu(hMenu, MF_STRING, ID_CONVERT_PINGPONG_SUSTAIN, ih->GetKeyTextFromCommand(kcSampleConvertPingPongSustain, _T("Convert to Unidirectional Sustain Loop"))); + } + + //if(sndFile.GetModSpecifications().HasVolCommand(VOLCMD_OFFSET)) + { + // Sample cues + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + HMENU hCueMenu = ::CreatePopupMenu(); + bool hasValidCues = false; + for(std::size_t i = 0; i < std::size(sample.cues); i++) + { + const SmpLength cue = sample.cues[i]; + wsprintf(s, _T("Cue &%d: %s"), 1 + static_cast<int>(i), + cue < sample.nLength ? mpt::cfmt::dec(3, ',', cue).GetString() : _T("unused")); + ::AppendMenu(hCueMenu, MF_STRING, ID_SAMPLE_CUE_1 + i, s); + if(cue > 0 && cue < sample.nLength) hasValidCues = true; + } + wsprintf(s, _T("Set Sample Cu&e to:\t%s"), pos.GetString()); + ::AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(hCueMenu), s); + ::AppendMenu(hMenu, MF_STRING | (hasValidCues ? 0 : MF_GRAYED), ID_SAMPLE_SLICE, ih->GetKeyTextFromCommand(kcSampleSlice, _T("Slice at cue points"))); + } + + ::AppendMenu(hMenu, MF_SEPARATOR, 0, _T("")); + m_dwMenuParam = dwPos; + } + } + + if(sample.GetElementarySampleSize() > 1) ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_8BITCONVERT, ih->GetKeyTextFromCommand(kcSample8Bit, _T("Convert to &8-bit"))); + else ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_16BITCONVERT, ih->GetKeyTextFromCommand(kcSample8Bit, _T("Convert to &16-bit"))); + if(sample.GetNumChannels() > 1) + { + HMENU hMonoMenu = ::CreatePopupMenu(); + ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT, ih->GetKeyTextFromCommand(kcSampleMonoMix, _T("&Mix Channels"))); + ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_LEFT, ih->GetKeyTextFromCommand(kcSampleMonoLeft, _T("&Left Channel"))); + ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_RIGHT, ih->GetKeyTextFromCommand(kcSampleMonoRight, _T("&Right Channel"))); + ::AppendMenu(hMonoMenu, MF_STRING, ID_SAMPLE_MONOCONVERT_SPLIT, ih->GetKeyTextFromCommand(kcSampleMonoSplit, _T("&Split Sample"))); + ::AppendMenu(hMenu, MF_POPUP, reinterpret_cast<UINT_PTR>(hMonoMenu), _T("Convert to &Mono")); + } + + // "Trim" menu item is responding differently if there's no selection, + // but a loop present: "trim around loop point"! (jojo in topic 2258) + CString trimMenuText = _T("Tr&im"); + bool isGrayed = ((m_dwEndSel <= m_dwBeginSel) || (m_dwEndSel - m_dwBeginSel < MIN_TRIM_LENGTH) + || (m_dwEndSel - m_dwBeginSel == sample.nLength)); + + if ((m_dwBeginSel == m_dwEndSel) && (sample.nLoopStart < sample.nLoopEnd)) + { + // no selection => use loop points + trimMenuText += _T(" around loop points"); + // Check whether trim menu item can be enabled (loop not too short or long for trimming). + if( (sample.nLoopEnd <= sample.nLength) && + (sample.nLoopEnd - sample.nLoopStart >= MIN_TRIM_LENGTH) && + (sample.nLoopEnd - sample.nLoopStart < sample.nLength) ) + isGrayed = false; + } + + ::AppendMenu(hMenu, MF_STRING | (isGrayed ? MF_GRAYED : 0), ID_SAMPLE_TRIM, ih->GetKeyTextFromCommand(kcSampleTrim, trimMenuText)); + if((m_dwBeginSel == 0 && m_dwEndSel != 0) || (m_dwBeginSel < sample.nLength && m_dwEndSel == sample.nLength)) + { + ::AppendMenu(hMenu, MF_STRING, ID_SAMPLE_QUICKFADE, ih->GetKeyTextFromCommand(kcSampleQuickFade, _T("Quick &Fade"))); + } + ::AppendMenu(hMenu, MF_STRING, ID_EDIT_CUT, ih->GetKeyTextFromCommand(kcEditCut, _T("Cu&t"))); + ::AppendMenu(hMenu, MF_STRING, ID_EDIT_COPY, ih->GetKeyTextFromCommand(kcEditCopy, _T("&Copy"))); + } + const UINT clipboardFlag = (IsClipboardFormatAvailable(CF_WAVE) ? 0 : MF_GRAYED); + ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_PASTE, ih->GetKeyTextFromCommand(kcEditPaste, _T("&Paste (Replace)"))); + ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_PUSHFORWARDPASTE, ih->GetKeyTextFromCommand(kcEditPushForwardPaste, _T("Paste (&Insert)"))); + ::AppendMenu(hMenu, MF_STRING | clipboardFlag, ID_EDIT_MIXPASTE, ih->GetKeyTextFromCommand(kcEditMixPaste, _T("Mi&x Paste"))); + ::AppendMenu(hMenu, MF_STRING | (pModDoc->GetSampleUndo().CanUndo(m_nSample) ? 0 : MF_GRAYED), ID_EDIT_UNDO, ih->GetKeyTextFromCommand(kcEditUndo, _T("&Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetUndoName(m_nSample)))); + ::AppendMenu(hMenu, MF_STRING | (pModDoc->GetSampleUndo().CanRedo(m_nSample) ? 0 : MF_GRAYED), ID_EDIT_REDO, ih->GetKeyTextFromCommand(kcEditRedo, _T("&Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetRedoName(m_nSample)))); + ClientToScreen(&pt); + ::TrackPopupMenu(hMenu, TPM_LEFTALIGN|TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(hMenu); + } +} + + +void CViewSample::OnNcMouseMove(UINT nHitTest, CPoint point) +{ + const auto button = GetNcButtonAtPoint(point); + if(button != m_nBtnMouseOver) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + { + CString strText; + if(button < SMP_LEFTBAR_BUTTONS && cLeftBarButtons[button] != ID_SEPARATOR) + { + strText = LoadResourceString(cLeftBarButtons[button]); + } + pMainFrm->SetHelpText(strText); + } + m_nBtnMouseOver = button; + UpdateNcButtonState(); + } + CModScrollView::OnNcMouseMove(nHitTest, point); +} + + +void CViewSample::OnNcLButtonDown(UINT uFlags, CPoint point) +{ + if (m_nBtnMouseOver < SMP_LEFTBAR_BUTTONS) + { + m_dwStatus.set(SMPSTATUS_NCLBTNDOWN); + if (cLeftBarButtons[m_nBtnMouseOver] != ID_SEPARATOR) + { + PostMessage(WM_COMMAND, cLeftBarButtons[m_nBtnMouseOver]); + UpdateNcButtonState(); + } + } + CModScrollView::OnNcLButtonDown(uFlags, point); +} + + +void CViewSample::OnNcLButtonUp(UINT uFlags, CPoint point) +{ + if(m_dwStatus[SMPSTATUS_NCLBTNDOWN]) + { + m_dwStatus.reset(SMPSTATUS_NCLBTNDOWN); + UpdateNcButtonState(); + } + CModScrollView::OnNcLButtonUp(uFlags, point); +} + + +void CViewSample::OnNcLButtonDblClk(UINT uFlags, CPoint point) +{ + OnNcLButtonDown(uFlags, point); +} + + +LRESULT CViewSample::OnNcHitTest(CPoint point) +{ + CRect rect; + GetWindowRect(&rect); + rect.bottom = rect.top + SMP_LEFTBAR_CY; + if (rect.PtInRect(point)) + { + return HTBORDER; + } + return CModScrollView::OnNcHitTest(point); +} + + +void CViewSample::OnPrevInstrument() +{ + SendCtrlMessage(CTRLMSG_SMP_PREVINSTRUMENT); +} + + +void CViewSample::OnNextInstrument() +{ + SendCtrlMessage(CTRLMSG_SMP_NEXTINSTRUMENT); +} + + +void CViewSample::OnSetLoop() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if ((m_dwEndSel > m_dwBeginSel + 15) && (m_dwEndSel <= sample.nLength)) + { + if ((sample.nLoopStart != m_dwBeginSel) || (sample.nLoopEnd != m_dwEndSel)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop"); + sample.SetLoop(m_dwBeginSel, m_dwEndSel, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } + } +} + + +void CViewSample::OnSetSustainLoop() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if ((m_dwEndSel > m_dwBeginSel + 15) && (m_dwEndSel <= sample.nLength)) + { + if ((sample.nSustainStart != m_dwBeginSel) || (sample.nSustainEnd != m_dwEndSel)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain Loop"); + sample.SetSustainLoop(m_dwBeginSel, m_dwEndSel, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } + } +} + + +void CViewSample::OnEditSelectAll() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + SmpLength len = pModDoc->GetSoundFile().GetSample(m_nSample).nLength; + if (len) SetCurSel(0, len); + } +} + + +void CViewSample::OnEditDelete() +{ + CModDoc *pModDoc = GetDocument(); + SampleHint updateHint; + updateHint.Info().Data(); + + if (!pModDoc) return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if (!sample.HasSampleData()) return; + if (m_dwEndSel > sample.nLength) m_dwEndSel = sample.nLength; + if ((m_dwBeginSel >= m_dwEndSel) + || (m_dwEndSel - m_dwBeginSel + 4 >= sample.nLength)) + { + if (Reporting::Confirm("Remove this sample?", "Remove Sample", true) != cnfYes) return; + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Delete Sample"); + sndFile.DestroySampleThreadsafe(m_nSample); + updateHint.Names(); + } else + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Delete Selection", m_dwBeginSel, m_dwEndSel); + + CriticalSection cs; + SampleEdit::RemoveRange(sample, m_dwBeginSel, m_dwEndSel, sndFile); + } + SetCurSel(0, 0); + SetModified(updateHint, true, true); +} + + +void CViewSample::OnEditCut() +{ + OnEditCopy(); + OnEditDelete(); +} + + +void CViewSample::OnEditCopy() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm == nullptr || GetDocument() == nullptr) + { + return; + } + + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + const ModSample &sample = sndFile.GetSample(m_nSample); + + if(sample.uFlags[CHN_ADLIB]) + { + // We cannot store an OPL patch in a Wave file... + Clipboard clipboard(CF_WAVE, sizeof(S3MSampleHeader)); + if(clipboard.IsValid()) + { + S3MSampleHeader sampleHeader; + MemsetZero(sampleHeader); + sampleHeader.ConvertToS3M(sample); + mpt::String::WriteBuf(mpt::String::nullTerminated, sampleHeader.name) = sndFile.m_szNames[m_nSample]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, sampleHeader.reserved2) = mpt::ToCharset(mpt::Charset::UTF8, Version::Current().GetOpenMPTVersionString()); + clipboard = sampleHeader; + } + return; + } + + bool addLoopInfo = true; + size_t smpSize = sample.nLength; + size_t smpOffset = 0; + + // First things first: Calculate sample size, taking partial selections into account. + LimitMax(m_dwEndSel, sample.nLength); + if(m_dwEndSel > m_dwBeginSel) + { + smpSize = m_dwEndSel - m_dwBeginSel; + smpOffset = m_dwBeginSel; + addLoopInfo = false; + } + + smpSize *= sample.GetBytesPerSample(); + smpOffset *= sample.GetBytesPerSample(); + + // Ok, now calculate size of the resulting WAV file. + size_t memSize = sizeof(RIFFHeader) // RIFF Header + + sizeof(RIFFChunk) + sizeof(WAVFormatChunk) // Sample format + + sizeof(RIFFChunk) + ((smpSize + 1) & ~1) // Sample data + + sizeof(RIFFChunk) + sizeof(WAVExtraChunk) // Sample metadata + + MAX_SAMPLENAME + MAX_SAMPLEFILENAME; // Sample name + static_assert((sizeof(WAVExtraChunk) % 2u) == 0); + static_assert((MAX_SAMPLENAME % 2u) == 0); + static_assert((MAX_SAMPLEFILENAME % 2u) == 0); + + if(addLoopInfo) + { + // We want to store some loop metadata as well. + memSize += sizeof(RIFFChunk) + sizeof(WAVSampleInfoChunk) + 2 * sizeof(WAVSampleLoop); + // ...and cue points, too. + memSize += sizeof(RIFFChunk) + sizeof(uint32) + std::size(sample.cues) * sizeof(WAVCuePoint); + } + + ASSERT((memSize % 2u) == 0); + + BeginWaitCursor(); + Clipboard clipboard(CF_WAVE, memSize); + if(auto data = clipboard.Get(); data.data()) + { + std::pair<mpt::byte_span, mpt::IO::Offset> mf(data, 0); + mpt::IO::OFile<std::pair<mpt::byte_span, mpt::IO::Offset>> ff(mf); + WAVWriter file(ff); + + // Write sample format + file.WriteFormat(sample.GetSampleRate(sndFile.GetType()), sample.GetElementarySampleSize() * 8, sample.GetNumChannels(), WAVFormatChunk::fmtPCM); + + // Write sample data + file.StartChunk(RIFFChunk::iddata); + + uint8 *sampleData = mpt::byte_cast<uint8 *>(data.data()) + file.GetPosition(); + memcpy(sampleData, sample.sampleb() + smpOffset, smpSize); + if(sample.GetElementarySampleSize() == 1) + { + // 8-Bit samples have to be unsigned. + for(size_t i = smpSize; i != 0; i--) + { + *(sampleData++) ^= 0x80u; + } + } + + file.Skip(smpSize); + + if(addLoopInfo) + { + file.WriteLoopInformation(sample); + file.WriteCueInformation(sample); + } + file.WriteExtraInformation(sample, sndFile.GetType(), sndFile.GetSampleName(m_nSample)); + + file.Finalize(); + clipboard.Close(); + } + EndWaitCursor(); +} + + +void CViewSample::OnEditPaste() +{ + DoPaste(PasteMode::Replace); +} + + +void CViewSample::OnEditMixPaste() +{ + CMixSampleDlg::sampleOffset = m_dwMenuParam; + DoPaste(PasteMode::MixPaste); +} + + +void CViewSample::OnEditInsertPaste() +{ + if(m_dwBeginSel <= m_dwEndSel) + m_dwBeginSel = m_dwEndSel = m_dwBeginDrag = m_dwEndDrag = m_dwMenuParam; + DoPaste(PasteMode::Insert); +} + + +template<typename Tdst, typename Tsrc> +static void MixSampleLoop(SmpLength numSamples, const Tsrc *src, uint8 srcInc, int srcFact, Tdst *dst, uint8 dstInc) +{ + SC::Convert<Tdst, Tsrc> conv; + while(numSamples--) + { + *dst = mpt::saturate_cast<Tdst>(*dst + Util::muldivr(conv(*src), srcFact, 100)); + src += srcInc; + dst += dstInc; + } +} + + +static void MixSampleOnto(const ModSample &sample, SmpLength offset, int amplify, uint8 chn, uint8 newNumChannels, int16 *pNewSample) +{ + uint8 numChannels = sample.GetNumChannels(); + switch(sample.GetElementarySampleSize()) + { + case 1: + MixSampleLoop(sample.nLength, sample.sample8() + (chn % numChannels), numChannels, amplify, pNewSample + offset * newNumChannels + chn, newNumChannels); + break; + case 2: + MixSampleLoop(sample.nLength, sample.sample16() + (chn % numChannels), numChannels, amplify, pNewSample + offset * newNumChannels + chn, newNumChannels); + break; + default: + MPT_ASSERT_NOTREACHED(); + } +} + + +void CViewSample::DoPaste(PasteMode pasteMode) +{ + CModDoc *pModDoc = GetDocument(); + BeginWaitCursor(); + Clipboard clipboard(CF_WAVE); + if(auto data = clipboard.Get(); data.data()) + { + SmpLength selBegin = 0, selEnd = 0; + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Paste"); + + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + pasteMode = PasteMode::Replace; + // Show mix paste dialog + if(pasteMode == PasteMode::MixPaste) + { + CMixSampleDlg dlg(this); + if(dlg.DoModal() != IDOK) + { + EndWaitCursor(); + return; + } + } + + // Save old data for mixpaste + ModSample oldSample = sample; + std::string oldSampleName = sndFile.m_szNames[m_nSample]; + + if(pasteMode != PasteMode::Replace) + { + sample.pData.pSample = nullptr; // prevent old sample from being deleted. + } + + FileReader file(data); + CriticalSection cs; + bool ok = sndFile.ReadSampleFromFile(m_nSample, file, TrackerSettings::Instance().m_MayNormalizeSamplesOnLoad); + clipboard.Close(); + if(sample.uFlags[CHN_ADLIB] != oldSample.uFlags[CHN_ADLIB] && pasteMode != PasteMode::Replace) + { + // Cannot mix PCM with FM + pasteMode = PasteMode::Replace; + oldSample.FreeSample(); + } + if (!sndFile.m_szNames[m_nSample][0] || pasteMode != PasteMode::Replace) + { + sndFile.m_szNames[m_nSample] = oldSampleName; + } + if (!sample.filename[0]) + { + sample.filename = oldSample.filename; + } + + if(pasteMode == PasteMode::MixPaste && ok) + { + // Mix new sample (stored in the actual sample slot) and old sample (stored in oldSample) + SmpLength newLength = std::max(oldSample.nLength, CMixSampleDlg::sampleOffset + sample.nLength); + const uint8 newNumChannels = std::max(oldSample.GetNumChannels(), sample.GetNumChannels()); + + // Result is at least 9 bits (when mixing two 8-bit samples), so always enforce 16-bit resolution + int16 *pNewSample = static_cast<int16 *>(ModSample::AllocateSample(newLength, 2u * newNumChannels)); + if(pNewSample == nullptr) + { + ErrorBox(IDS_ERR_OUTOFMEMORY, this); + ok = false; + } else + { + selBegin = CMixSampleDlg::sampleOffset; + selEnd = selBegin + sample.nLength; + for(uint8 chn = 0; chn < newNumChannels; chn++) + { + MixSampleOnto(oldSample, 0, CMixSampleDlg::amplifyOriginal, chn, newNumChannels, pNewSample); + MixSampleOnto(sample, CMixSampleDlg::sampleOffset, CMixSampleDlg::amplifyMix, chn, newNumChannels, pNewSample); + } + sndFile.DestroySample(m_nSample); + sample = oldSample; + sample.uFlags.set(CHN_16BIT); + sample.uFlags.set(CHN_STEREO, newNumChannels == 2); + ctrlSmp::ReplaceSample(sample, pNewSample, newLength, sndFile); + } + } else if(pasteMode == PasteMode::Insert && ok) + { + // Insert / replace selection + SmpLength oldLength = oldSample.nLength; + SmpLength selLength = m_dwEndSel - m_dwBeginSel; + if(m_dwEndSel > m_dwBeginSel) + { + // Replace selection with pasted data + if(selLength >= sample.nLength) + ok = true; + else + ok = SampleEdit::InsertSilence(oldSample, sample.nLength - selLength, m_dwBeginSel, sndFile) > oldLength; + } else + { + m_dwBeginSel = m_dwBeginDrag; + ok = SampleEdit::InsertSilence(oldSample, sample.nLength, m_dwBeginSel, sndFile) > oldLength; + } + if(ok && sample.GetNumChannels() > oldSample.GetNumChannels()) + { + // Keep channel configuration with higher channel count + ok = ctrlSmp::ConvertToStereo(oldSample, sndFile); + } + if(ok && sample.GetElementarySampleSize() > oldSample.GetElementarySampleSize()) + { + // Keep higher bit depth of the two samples + ok = SampleEdit::ConvertTo16Bit(oldSample, sndFile); + } + if(ok) + { + selBegin = m_dwBeginSel; + selEnd = selBegin + sample.nLength; + uint8 numChannels = oldSample.GetNumChannels(); + SmpLength offset = m_dwBeginSel * numChannels; + for(uint8 chn = 0; chn < numChannels; chn++) + { + uint8 newChn = chn % sample.GetNumChannels(); + if(oldSample.GetElementarySampleSize() == 1 && sample.GetElementarySampleSize() == 1) + { + CopySample(oldSample.sample8() + offset + chn, sample.nLength, numChannels, sample.sample8() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain<SC::Convert<int8, int8>, SC::DecodeIdentity<int8> >()); + } else if(oldSample.GetElementarySampleSize() == 2 && sample.GetElementarySampleSize() == 1) + { + CopySample(oldSample.sample16() + offset + chn, sample.nLength, numChannels, sample.sample8() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain<SC::Convert<int16, int8>, SC::DecodeIdentity<int8> >()); + } else if(oldSample.GetElementarySampleSize() == 2 && sample.GetElementarySampleSize() == 2) + { + CopySample(oldSample.sample16() + offset + chn, sample.nLength, numChannels, sample.sample16() + newChn, sample.GetSampleSizeInBytes(), sample.GetNumChannels(), SC::ConversionChain<SC::Convert<int16, int16>, SC::DecodeIdentity<int16> >()); + } else + { + MPT_ASSERT_NOTREACHED(); + } + } + } else + { + ErrorBox(IDS_ERR_OUTOFMEMORY, this); + } + sndFile.DestroySample(m_nSample); + sample = oldSample; + } + + if(ok) + { + SetCurSel(selBegin, selEnd); + sample.PrecomputeLoops(sndFile, true); + SetModified(SampleHint().Info().Data().Names(), true, false); + if(pasteMode == PasteMode::Replace) + sndFile.ResetSamplePath(m_nSample); + } else + { + if(pasteMode == PasteMode::MixPaste) + ModSample::FreeSample(oldSample.samplev()); + pModDoc->GetSampleUndo().Undo(m_nSample); + sndFile.m_szNames[m_nSample] = oldSampleName; + } + } + EndWaitCursor(); +} + + +void CViewSample::OnEditUndo() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) return; + if(pModDoc->GetSampleUndo().Undo(m_nSample)) + { + SetModified(SampleHint().Info().Data().Names(), true, false); + } +} + + +void CViewSample::OnEditRedo() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc == nullptr) return; + if(pModDoc->GetSampleUndo().Redo(m_nSample)) + { + SetModified(SampleHint().Info().Data().Names(), true, false); + } +} + + +void CViewSample::On8BitConvert() +{ + CModDoc *pModDoc = GetDocument(); + BeginWaitCursor(); + if ((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(sample.uFlags[CHN_16BIT] && !sample.uFlags[CHN_ADLIB] && sample.HasSampleData()) + { + ASSERT(sample.GetElementarySampleSize() == 2); + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "8-Bit Conversion"); + + CriticalSection cs; + SampleEdit::ConvertTo8Bit(sample, sndFile); + cs.Leave(); + + SetModified(SampleHint().Info().Data(), true, true); + } + } + EndWaitCursor(); +} + + +void CViewSample::On16BitConvert() +{ + CModDoc *pModDoc = GetDocument(); + BeginWaitCursor(); + if ((pModDoc) && (m_nSample <= pModDoc->GetNumSamples())) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.uFlags[CHN_16BIT] && !sample.uFlags[CHN_ADLIB] && sample.HasSampleData()) + { + ASSERT(sample.GetElementarySampleSize() == 1); + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "16-Bit Conversion"); + if(!SampleEdit::ConvertTo16Bit(sample, sndFile)) + { + pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); + } else + { + SetModified(SampleHint().Info().Data(), true, true); + } + } + } + EndWaitCursor(); +} + + +void CViewSample::OnMonoConvert(ctrlSmp::StereoToMonoMode convert) +{ + CModDoc *pModDoc = GetDocument(); + BeginWaitCursor(); + if(pModDoc != nullptr && (m_nSample <= pModDoc->GetNumSamples())) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(sample.GetNumChannels() > 1 && sample.HasSampleData() && !sample.uFlags[CHN_ADLIB]) + { + SAMPLEINDEX rightSmp = SAMPLEINDEX_INVALID; + if(convert == ctrlSmp::splitSample) + { + // Split sample into two slots + rightSmp = pModDoc->InsertSample(); + if(rightSmp == SAMPLEINDEX_INVALID) + return; + } + + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Mono Conversion"); + + bool success = false; + if(convert == ctrlSmp::splitSample) + { + ModSample &right = sndFile.GetSample(rightSmp); + success = ctrlSmp::SplitStereo(sample, sample, right, sndFile); + + // Try to create a new instrument as well which maps to the right sample. + if(success) + { + INSTRUMENTINDEX ins = pModDoc->FindSampleParent(m_nSample); + if(ins != INSTRUMENTINDEX_INVALID) + { + INSTRUMENTINDEX rightIns = pModDoc->InsertInstrument(0, ins); + if(rightIns != INSTRUMENTINDEX_INVALID) + { + for(auto &smp : sndFile.Instruments[rightIns]->Keyboard) + { + if(smp == m_nSample) + smp = rightSmp; + } + } + pModDoc->UpdateAllViews(this, InstrumentHint(rightIns).Info().Envelope().Names(), this); + } + + // Finally, adjust sample panning + if(sndFile.GetType() & (MOD_TYPE_IT | MOD_TYPE_MPT | MOD_TYPE_XM)) + { + sample.uFlags.set(CHN_PANNING); + sample.nPan = 0; + right.uFlags.set(CHN_PANNING); + right.nPan = 256; + } + pModDoc->UpdateAllViews(this, SampleHint(rightSmp).Info().Data().Names(), this); + } + } else + { + success = ctrlSmp::ConvertToMono(sample, sndFile, convert); + } + + if(success) + SetModified(SampleHint().Info().Data().Names(), true, true); + else + pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); + } + } + EndWaitCursor(); +} + + +void CViewSample::TrimSample(bool trimToLoopEnd) +{ + CModDoc *pModDoc = GetDocument(); + //nothing loaded or invalid sample slot. + if(!pModDoc || m_nSample > pModDoc->GetNumSamples() || IsOPLInstrument()) return; + + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + + if(trimToLoopEnd) + { + m_dwBeginSel = 0; + m_dwEndSel = sample.nLoopEnd; + } else if(m_dwBeginSel == m_dwEndSel) + { + // Trim around loop points if there's no selection + m_dwBeginSel = sample.nLoopStart; + m_dwEndSel = sample.nLoopEnd; + } + + if (m_dwBeginSel >= m_dwEndSel) return; // invalid selection + + BeginWaitCursor(); + SmpLength nStart = m_dwBeginSel; + SmpLength nEnd = m_dwEndSel - m_dwBeginSel; + + if(sample.HasSampleData() && (nStart + nEnd <= sample.nLength) && (nEnd >= MIN_TRIM_LENGTH)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Trim"); + + CriticalSection cs; + + // Note: Sample is overwritten in-place! Unused data is not deallocated! + memmove(sample.sampleb(), sample.sampleb() + nStart * sample.GetBytesPerSample(), nEnd * sample.GetBytesPerSample()); + + for(SmpLength &point : SampleEdit::GetCuesAndLoops(sample)) + { + if(point >= nStart) + point -= nStart; + else + point = sample.nLength; + } + sample.nLength = nEnd; + sample.PrecomputeLoops(sndFile); + cs.Leave(); + + SetCurSel(0, 0); + SetModified(SampleHint().Info().Data(), true, true); + } + EndWaitCursor(); +} + + +void CViewSample::OnChar(UINT /*nChar*/, UINT, UINT /*nFlags*/) +{ +} + + +void CViewSample::PlayNote(ModCommand::NOTE note, const SmpLength nStartPos, int volume) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CModDoc *pModDoc = GetDocument(); + if ((pModDoc) && (pMainFrm)) + { + if (note >= NOTE_MIN_SPECIAL) + { + pModDoc->NoteOff(0, (note == NOTE_NOTECUT)); + } else + { + if(m_dwStatus[SMPSTATUS_KEYDOWN]) + pModDoc->NoteOff(note, true, m_noteChannel[note - NOTE_MIN]); + else + pModDoc->NoteOff(0, true); + + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + const ModSample &sample = sndFile.GetSample(m_nSample); + + SmpLength loopstart = m_dwBeginSel, loopend = m_dwEndSel; + // If selection is too small -> no loop + if((m_nZoom >= 0 && loopend - loopstart < (SmpLength)(4 << m_nZoom)) + || (m_nZoom < 0 && loopend - loopstart < 4) + || (loopstart >= sample.nLength)) + { + loopend = loopstart = 0; + } + + pModDoc->PlayNote(PlayNoteParam(note).Sample(m_nSample).Volume(volume).LoopStart(loopstart).LoopEnd(loopend).Offset(nStartPos), &m_noteChannel); + + m_dwStatus.set(SMPSTATUS_KEYDOWN); + + uint32 freq = sndFile.GetFreqFromPeriod(sndFile.GetPeriodFromNote(note + (sndFile.GetType() == MOD_TYPE_XM ? sample.RelativeTone : 0), sample.nFineTune, sample.nC5Speed), sample.nC5Speed, 0); + + pMainFrm->SetInfoText(MPT_CFORMAT("{} ({}.{} Hz)")( + mpt::ToCString(sndFile.GetNoteName((ModCommand::NOTE)note)), + freq >> FREQ_FRACBITS, + mpt::cfmt::dec0<2>(Util::muldiv(freq & ((1 << FREQ_FRACBITS) - 1), 100, 1 << FREQ_FRACBITS)))); + } + } +} + + +void CViewSample::NoteOff(ModCommand::NOTE note) +{ + CSoundFile &sndFile = GetDocument()->GetSoundFile(); + ModChannel &chn = sndFile.m_PlayState.Chn[m_noteChannel[note - NOTE_MIN]]; + sndFile.KeyOff(chn); + chn.dwFlags.set(CHN_NOTEFADE); + m_noteChannel[note - NOTE_MIN] = CHANNELINDEX_INVALID; +} + + +// Drop files from Windows +void CViewSample::OnDropFiles(HDROP hDropInfo) +{ + const UINT nFiles = ::DragQueryFile(hDropInfo, (UINT)-1, NULL, 0); + CMainFrame::GetMainFrame()->SetForegroundWindow(); + for(UINT f = 0; f < nFiles; f++) + { + UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, _T('\0')); + if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) + { + const mpt::PathString file = mpt::PathString::FromNative(fileName.data()); + if(SendCtrlMessage(CTRLMSG_SMP_OPENFILE, (LPARAM)&file) && f < nFiles - 1) + { + // Insert more sample slots + if(!SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) + break; + } + } + } + ::DragFinish(hDropInfo); +} + + +BOOL CViewSample::OnDragonDrop(BOOL doDrop, const DRAGONDROP *dropInfo) +{ + CModDoc *modDoc = GetDocument(); + bool canDrop = false; + + if ((!dropInfo) || (!modDoc)) return FALSE; + CSoundFile &sndFile = modDoc->GetSoundFile(); + switch(dropInfo->dropType) + { + case DRAGONDROP_SAMPLE: + if (dropInfo->sndFile == &sndFile) + { + canDrop = ((dropInfo->dropItem) + && (dropInfo->dropItem <= sndFile.GetNumSamples())); + } else + { + canDrop = ((dropInfo->dropItem) + && ((dropInfo->dropParam) || (dropInfo->sndFile))); + } + break; + + case DRAGONDROP_DLS: + canDrop = ((dropInfo->dropItem < CTrackApp::gpDLSBanks.size()) + && (CTrackApp::gpDLSBanks[dropInfo->dropItem])); + break; + + case DRAGONDROP_SOUNDFILE: + case DRAGONDROP_MIDIINSTR: + canDrop = !dropInfo->GetPath().empty(); + break; + } + + const bool insertNew = CMainFrame::GetInputHandler()->ShiftPressed(); + if(insertNew && !sndFile.CanAddMoreSamples()) + canDrop = false; + + if(!canDrop || !doDrop) + return canDrop; + + // Do the drop + BeginWaitCursor(); + bool modified = false; + switch(dropInfo->dropType) + { + case DRAGONDROP_SAMPLE: + if(dropInfo->sndFile == &sndFile) + { + SendCtrlMessage(CTRLMSG_SETCURRENTINSTRUMENT, dropInfo->dropItem); + } else + { + if(insertNew && !SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) + canDrop = false; + else + SendCtrlMessage(CTRLMSG_SMP_SONGDROP, (LPARAM)dropInfo); + } + break; + + case DRAGONDROP_MIDIINSTR: + if (CDLSBank::IsDLSBank(dropInfo->GetPath())) + { + CDLSBank dlsbank; + if (dlsbank.Open(dropInfo->GetPath())) + { + const DLSINSTRUMENT *pDlsIns; + UINT nIns = 0, nRgn = 0xFF; + int transpose = 0; + // Drums + if (dropInfo->dropItem & 0x80) + { + UINT key = dropInfo->dropItem & 0x7F; + pDlsIns = dlsbank.FindInstrument(TRUE, 0xFFFF, 0xFF, key, &nIns); + if(pDlsIns) + { + nRgn = dlsbank.GetRegionFromKey(nIns, key); + const auto ®ion = pDlsIns->Regions[nRgn]; + if(region.tuning != 0) + transpose = (region.uKeyMin + (region.uKeyMax - region.uKeyMin) / 2) - 60; + } + } else + // Melodic + { + pDlsIns = dlsbank.FindInstrument(FALSE, 0xFFFF, dropInfo->dropItem, 60, &nIns); + if (pDlsIns) nRgn = dlsbank.GetRegionFromKey(nIns, 60); + } + canDrop = FALSE; + if (pDlsIns) + { + if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) + { + CriticalSection cs; + modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Replace"); + canDrop = modified = dlsbank.ExtractSample(sndFile, m_nSample, nIns, nRgn, transpose); + } + } + break; + } + } + [[fallthrough]]; + case DRAGONDROP_SOUNDFILE: + if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) + SendCtrlMessage(CTRLMSG_SMP_OPENFILE, dropInfo->dropParam); + break; + + case DRAGONDROP_DLS: + { + const CDLSBank dlsBank = *CTrackApp::gpDLSBanks[dropInfo->dropItem]; + UINT nIns = dropInfo->dropParam & 0xFFFF; + UINT nRgn; + int transpose = 0; + // Drums: (0x80000000) | (Region << 16) | (Instrument) + if (dropInfo->dropParam & 0x80000000) + { + nRgn = (dropInfo->dropParam & 0x7FFF0000) >> 16; + const auto ®ion = dlsBank.GetInstrument(nIns)->Regions[nRgn]; + if(region.tuning != 0) + transpose = (region.uKeyMin + (region.uKeyMax - region.uKeyMin) / 2) - 60; + } else + // Melodic: (Instrument) + { + nRgn = dlsBank.GetRegionFromKey(nIns, 60); + } + if(!insertNew || SendCtrlMessage(CTRLMSG_SMP_NEWSAMPLE)) + { + CriticalSection cs; + modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Replace"); + canDrop = modified = dlsBank.ExtractSample(sndFile, m_nSample, nIns, nRgn, transpose); + } + } + break; + } + if(modified) + { + SetModified(SampleHint().Info().Data().Names(), true, false); + } + CMDIChildWnd *pMDIFrame = (CMDIChildWnd *)GetParentFrame(); + if (pMDIFrame) + { + pMDIFrame->MDIActivate(); + pMDIFrame->SetActiveView(this); + SetFocus(); + } + EndWaitCursor(); + return canDrop; +} + + +void CViewSample::OnZoomOnSel() +{ + int zoom = 0; + SmpLength selLength = (m_dwEndSel - m_dwBeginSel); + if (selLength > 0 && m_rcClient.right > 0) + { + zoom = GetZoomLevel(selLength); + if(zoom < 0) + { + zoom++; + if(zoom >= -1) + zoom = 1; + else if(zoom < MIN_ZOOM) + zoom = MIN_ZOOM; + } else if(zoom > MAX_ZOOM) + { + zoom = 0; + } + } + + if(zoom) + { + SetZoom(zoom, m_dwBeginSel + selLength / 2); + } + SendCtrlMessage(CTRLMSG_SMP_SETZOOM, zoom); +} + + +SmpLength CViewSample::ScrollPosToSamplePos(int nZoom) const +{ + if(nZoom < 0) + return m_nScrollPosX; + else if(nZoom > 0) + return m_nScrollPosX << (nZoom - 1); + else + return 0; +} + + +void CViewSample::OnSetLoopStart() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + SmpLength loopEnd = (sample.nLoopEnd > 0) ? sample.nLoopEnd : sample.nLength; + if ((m_dwMenuParam + 4 <= loopEnd) && (sample.nLoopStart != m_dwMenuParam)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop Start"); + sample.SetLoop(m_dwMenuParam, loopEnd, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } +} + + +void CViewSample::OnSetLoopEnd() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if ((m_dwMenuParam >= sample.nLoopStart + 4) && (sample.nLoopEnd != m_dwMenuParam)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Loop End"); + sample.SetLoop(sample.nLoopStart, m_dwMenuParam, true, sample.uFlags[CHN_PINGPONGLOOP], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } +} + + +void CViewSample::OnConvertPingPongLoop() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasPingPongLoop()) + return; + + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Convert Bidi Loop"); + if(SampleEdit::ConvertPingPongLoop(sample, sndFile, false)) + SetModified(SampleHint().Info().Data(), true, true); + else + pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); + } +} + + +void CViewSample::OnSetSustainStart() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + SmpLength sustainEnd = (sample.nSustainEnd > 0) ? sample.nSustainEnd : sample.nLength; + if ((m_dwMenuParam + 4 <= sustainEnd) && (sample.nSustainStart != m_dwMenuParam)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain Start"); + sample.SetSustainLoop(m_dwMenuParam, sustainEnd, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } +} + + +void CViewSample::OnSetSustainEnd() +{ + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if ((m_dwMenuParam >= sample.nSustainStart + 4) && (sample.nSustainEnd != m_dwMenuParam)) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Sustain End"); + sample.SetSustainLoop(sample.nSustainStart, m_dwMenuParam, true, sample.uFlags[CHN_PINGPONGSUSTAIN], sndFile); + SetModified(SampleHint().Info(), true, false); + } + } +} + + +void CViewSample::OnConvertPingPongSustain() +{ + CModDoc *pModDoc = GetDocument(); + if(pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasPingPongSustainLoop()) + return; + + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Convert Bidi Sustain Loop"); + if(SampleEdit::ConvertPingPongLoop(sample, sndFile, true)) + SetModified(SampleHint().Info().Data(), true, true); + else + pModDoc->GetSampleUndo().RemoveLastUndoStep(m_nSample); + } +} + + +void CViewSample::OnSetCuePoint(UINT nID) +{ + nID -= ID_SAMPLE_CUE_1; + CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Set Cue Point"); + sample.cues[nID] = m_dwMenuParam; + SetModified(SampleHint().Info(), true, false); + } +} + + +void CViewSample::OnZoomUp() +{ + DoZoom(1); +} + + +void CViewSample::OnZoomDown() +{ + DoZoom(-1); +} + + +void CViewSample::OnDrawingToggle() +{ + const CModDoc *pModDoc = GetDocument(); + if(!pModDoc) return; + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + + const ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasSampleData()) + { + OnAddSilence(); + if(!sample.HasSampleData()) + { + return; + } + } + + m_dwStatus.flip(SMPSTATUS_DRAWING); + UpdateNcButtonState(); +} + + +void CViewSample::OnAddSilence() +{ + CModDoc *pModDoc = GetDocument(); + if (!pModDoc) return; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + ModSample &sample = sndFile.GetSample(m_nSample); + + const SmpLength oldLength = IsOPLInstrument() ? 0 : sample.nLength; + + AddSilenceDlg dlg(this, oldLength, sample.GetSampleRate(sndFile.GetType()), sndFile.SupportsOPL()); + if (dlg.DoModal() != IDOK) return; + + if(dlg.m_editOption == AddSilenceDlg::kOPLInstrument) + { + SendCtrlMessage(CTRLMSG_SMP_INITOPL); + return; + } + + if(MAX_SAMPLE_LENGTH - oldLength < dlg.m_numSamples && dlg.m_editOption != AddSilenceDlg::kResize) + { + CString str; str.Format(_T("Cannot add silence because the new sample length would exceed maximum sample length %u."), MAX_SAMPLE_LENGTH); + Reporting::Information(str); + return; + } + + BeginWaitCursor(); + + if(sample.nLength == 0 && sample.nVolume == 0) + { + sample.nVolume = 256; + } + if(dlg.m_editOption == AddSilenceDlg::kResize) + { + // resize - dlg.m_nSamples = new size + if(dlg.m_numSamples == 0) + { + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_replace, "Delete Sample"); + sndFile.DestroySampleThreadsafe(m_nSample); + } else if(dlg.m_numSamples != sample.nLength) + { + CriticalSection cs; + + if(dlg.m_numSamples < sample.nLength) // make it shorter! + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Resize", dlg.m_numSamples, sample.nLength); + else // make it longer! + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_insert, "Add Silence", sample.nLength, dlg.m_numSamples); + sample.SetAdlib(false); + SampleEdit::ResizeSample(sample, dlg.m_numSamples, sndFile); + } + } else + { + // add silence - dlg.m_nSamples = amount of bytes to be added + if(dlg.m_numSamples > 0) + { + CriticalSection cs; + + SmpLength nStart = (dlg.m_editOption == AddSilenceDlg::kSilenceAtEnd) ? sample.nLength : 0; + pModDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_insert, "Add Silence", nStart, nStart + dlg.m_numSamples); + sample.SetAdlib(false); + SampleEdit::InsertSilence(sample, dlg.m_numSamples, nStart, sndFile); + } + } + + EndWaitCursor(); + + if(oldLength != sample.nLength) + { + SetCurSel(0, 0); + SetModified(SampleHint().Info().Data().Names(), true, true); + } +} + +LRESULT CViewSample::OnMidiMsg(WPARAM midiDataParam, LPARAM) +{ + const uint32 midiData = static_cast<uint32>(midiDataParam); + static uint8 midiVolume = 127; + + CModDoc *pModDoc = GetDocument(); + const uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); + const uint8 midiByte2 = MIDIEvents::GetDataByte2FromEvent(midiData); + const uint8 channel = MIDIEvents::GetChannelFromEvent(midiData); + + CSoundFile *pSndFile = (pModDoc) ? &pModDoc->GetSoundFile() : nullptr; + if (!pSndFile) return 0; + + uint8 nNote = midiByte1 + NOTE_MIN; + int nVol = midiByte2; + MIDIEvents::EventType event = MIDIEvents::GetTypeFromEvent(midiData); + if(event == MIDIEvents::evNoteOn && !nVol) + event = MIDIEvents::evNoteOff; //Convert event to note-off if req'd + + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + if(ih->HandleMIDIMessage(kCtxViewSamples, midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull) + { + // Mapped to a command, no need to pass message on. + return 1; + } + + switch(event) + { + case MIDIEvents::evNoteOff: // Note Off + if(m_midiSustainActive[channel]) + { + m_midiSustainBuffer[channel].push_back(midiData); + return 1; + } + [[fallthrough]]; + case MIDIEvents::evNoteOn: // Note On + LimitMax(nNote, NOTE_MAX); + pModDoc->NoteOff(nNote, true); + if(event != MIDIEvents::evNoteOff) + { + nVol = CMainFrame::ApplyVolumeRelatedSettings(midiData, midiVolume); + PlayNote(nNote, 0, nVol); + } + break; + + case MIDIEvents::evControllerChange: //Controller change + switch(midiByte1) + { + case MIDIEvents::MIDICC_Volume_Coarse: //Volume + 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]) + { + OnMidiMsg(offEvent, 0); + } + m_midiSustainBuffer[channel].clear(); + } + break; + } + } + + return 1; +} + +BOOL CViewSample::PreTranslateMessage(MSG *pMsg) +{ + if (pMsg) + { + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler* ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxViewSamples); + + if (ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + + // Handle Application (menu) key + if(pMsg->message == WM_KEYDOWN && nChar == VK_APPS) + { + int x = Util::ScalePixels(32, m_hWnd); + OnRButtonDown(0, CPoint(x, x)); + } + } + + } + + return CModScrollView::PreTranslateMessage(pMsg); +} + +LRESULT CViewSample::OnCustomKeyMsg(WPARAM wParam, LPARAM lParam) +{ + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) + return kcNull; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + switch(wParam) + { + case kcSampleTrim: TrimSample(false); return wParam; + case kcSampleTrimToLoopEnd: TrimSample(true); return wParam; + case kcSampleZoomUp: OnZoomUp(); return wParam; + case kcSampleZoomDown: OnZoomDown(); return wParam; + case kcSampleZoomSelection: OnZoomOnSel(); return wParam; + case kcSampleCenterSampleStart: + case kcSampleCenterSampleEnd: + case kcSampleCenterLoopStart: + case kcSampleCenterLoopEnd: + case kcSampleCenterSustainStart: + case kcSampleCenterSustainEnd: + { + SmpLength point = 0; + ModSample &sample = sndFile.GetSample(m_nSample); + switch(wParam) + { + case kcSampleCenterSampleStart: point = 0; break; + case kcSampleCenterSampleEnd: point = sample.nLength; break; + case kcSampleCenterLoopStart: point = sample.nLoopStart; break; + case kcSampleCenterLoopEnd: point = sample.nLoopEnd; break; + case kcSampleCenterSustainStart: point = sample.nSustainStart; break; + case kcSampleCenterSustainEnd: point = sample.nSustainEnd; break; + } + if(!m_nZoom) + SendCtrlMessage(CTRLMSG_SMP_SETZOOM, 1); + ScrollToSample(point); + } + return wParam; + case kcPrevInstrument: OnPrevInstrument(); return wParam; + case kcNextInstrument: OnNextInstrument(); return wParam; + case kcEditSelectAll: OnEditSelectAll(); return wParam; + case kcSampleDelete: OnEditDelete(); return wParam; + case kcEditCut: OnEditCut(); return wParam; + case kcEditCopy: OnEditCopy(); return wParam; + case kcEditPaste: OnEditPaste(); return wParam; + case kcEditMixPasteITStyle: + case kcEditMixPaste: DoPaste(PasteMode::MixPaste); return wParam; + case kcEditPushForwardPaste: DoPaste(PasteMode::Insert); return wParam; + case kcEditUndo: OnEditUndo(); return wParam; + case kcEditRedo: OnEditRedo(); return wParam; + case kcSampleConvertPingPongLoop: OnConvertPingPongLoop(); return wParam; + case kcSampleConvertPingPongSustain: OnConvertPingPongSustain(); return wParam; + case kcSample8Bit: if(sndFile.GetSample(m_nSample).uFlags[CHN_16BIT]) + On8BitConvert(); + else + On16BitConvert(); + return wParam; + case kcSampleMonoMix: OnMonoConvertMix(); return wParam; + case kcSampleMonoLeft: OnMonoConvertLeft(); return wParam; + case kcSampleMonoRight: OnMonoConvertRight(); return wParam; + case kcSampleMonoSplit: OnMonoConvertSplit(); return wParam; + + case kcSampleReverse: PostCtrlMessage(IDC_SAMPLE_REVERSE); return wParam; + case kcSampleSilence: PostCtrlMessage(IDC_SAMPLE_SILENCE); return wParam; + case kcSampleNormalize: PostCtrlMessage(IDC_SAMPLE_NORMALIZE); return wParam; + case kcSampleAmplify: PostCtrlMessage(IDC_SAMPLE_AMPLIFY); return wParam; + case kcSampleInvert: PostCtrlMessage(IDC_SAMPLE_INVERT); return wParam; + case kcSampleSignUnsign: PostCtrlMessage(IDC_SAMPLE_SIGN_UNSIGN); return wParam; + case kcSampleRemoveDCOffset: PostCtrlMessage(IDC_SAMPLE_DCOFFSET); return wParam; + case kcSampleXFade: PostCtrlMessage(IDC_SAMPLE_XFADE); return wParam; + case kcSampleAutotune: PostCtrlMessage(IDC_SAMPLE_AUTOTUNE); return wParam; + case kcSampleQuickFade: PostCtrlMessage(IDC_SAMPLE_QUICKFADE); return wParam; + case kcSampleStereoSep: PostCtrlMessage(IDC_SAMPLE_STEREOSEPARATION); return wParam; + case kcSampleSlice: OnSampleSlice(); return wParam; + case kcSampleToggleDrawing: OnDrawingToggle(); return wParam; + case kcSampleResize: OnAddSilence(); return wParam; + case kcSampleGrid: OnChangeGridSize(); return wParam; + + // Those don't seem to work. + case kcNoteOff: PlayNote(NOTE_KEYOFF); return wParam; + case kcNoteCut: PlayNote(NOTE_NOTECUT); return wParam; + } + + if(wParam >= kcSampStartNotes && wParam <= kcSampEndNotes) + { + const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcSampStartNotes), 0); + if(ModCommand::IsNote(note)) + { + switch(TrackerSettings::Instance().sampleEditorKeyBehaviour) + { + case seNoteOffOnKeyRestrike: + if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) + { + NoteOff(note); + break; + } + // Fall-through + default: + PlayNote(note); + } + return wParam; + } + } else if(wParam >= kcSampStartNoteStops && wParam <= kcSampEndNoteStops) + { + const ModCommand::NOTE note = pModDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcSampStartNoteStops), 0); + if(ModCommand::IsNote(note)) + { + switch(TrackerSettings::Instance().sampleEditorKeyBehaviour) + { + case seNoteOffOnNewKey: + m_dwStatus.reset(SMPSTATUS_KEYDOWN); + if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) + { + // Release sustain loop on key up + sndFile.KeyOff(sndFile.m_PlayState.Chn[m_noteChannel[note - NOTE_MIN]]); + } + break; + case seNoteOffOnKeyUp: + if(m_noteChannel[note - NOTE_MIN] != CHANNELINDEX_INVALID) + { + NoteOff(note); + } + break; + } + return wParam; + } + } else if(wParam >= kcStartSampleCues && wParam <= kcEndSampleCues) + { + const ModSample &sample = sndFile.GetSample(m_nSample); + SmpLength offset = sample.cues[wParam - kcStartSampleCues]; + if(offset < sample.nLength) + PlayNote(NOTE_MIDDLEC, offset); + return wParam; + } + + // Pass on to ctrl_smp + return GetControlDlg()->SendMessage(WM_MOD_KEYCOMMAND, wParam, lParam); +} + + +void CViewSample::OnSampleSlice() +{ + CModDoc *modDoc = GetDocument(); + if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) + return; + CSoundFile &sndFile = modDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + return; + + // Sort cue points and add two fake cue points to make things easier below... + constexpr size_t NUM_CUES = mpt::array_size<decltype(sample.cues)>::size; + std::array<SmpLength, NUM_CUES + 2> cues; + bool hasValidCues = false; // Any cues in ]0, length[ + for(std::size_t i = 0; i < std::size(sample.cues); i++) + { + cues[i] = sample.cues[i]; + if(cues[i] == 0 || cues[i] >= sample.nLength) + cues[i] = sample.nLength; + else + hasValidCues = true; + } + // Nothing to slice? + if(!hasValidCues) + return; + + cues[NUM_CUES] = 0; + cues[NUM_CUES + 1] = sample.nLength; + std::sort(cues.begin(), cues.end()); + + // Now slice the sample at each cue point + for(std::size_t i = 1; i < std::size(cues) - 1; i++) + { + const SmpLength cue = cues[i]; + if(cue > cues[i - 1] && cue < cues[i + 1]) + { + SAMPLEINDEX nextSmp = modDoc->InsertSample(); + if(nextSmp == SAMPLEINDEX_INVALID) + break; + + ModSample &newSample = sndFile.GetSample(nextSmp); + newSample = sample; + newSample.nLength = cues[i + 1] - cues[i]; + newSample.pData.pSample = nullptr; + sndFile.m_szNames[nextSmp] = sndFile.m_szNames[m_nSample]; + if(newSample.AllocateSample() > 0) + { + Util::DeleteRange(SmpLength(0), cues[i] - SmpLength(1), newSample.nLoopStart, newSample.nLoopEnd); + memcpy(newSample.sampleb(), sample.sampleb() + cues[i] * sample.GetBytesPerSample(), newSample.nLength * sample.GetBytesPerSample()); + newSample.PrecomputeLoops(sndFile, false); + + if(sndFile.GetNumInstruments() > 0) + { + if(auto instr = modDoc->InsertInstrument(nextSmp); instr != INSTRUMENTINDEX_INVALID) + sndFile.Instruments[instr]->name = sndFile.m_szNames[nextSmp]; + } + } + } + } + + modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_delete, "Slice Sample", cues[1], sample.nLength); + SampleEdit::ResizeSample(sample, cues[1], sndFile); + sample.PrecomputeLoops(sndFile, true); + SetModified(SampleHint().Info().Data().Names(), true, true); + modDoc->UpdateAllViews(this, SampleHint().Info().Data().Names(), this); + modDoc->UpdateAllViews(this, InstrumentHint().Info().Envelope().Names(), this); +} + + +void CViewSample::OnSampleInsertCuePoint() +{ + CModDoc *modDoc = GetDocument(); + if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) + return; + CSoundFile &sndFile = modDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB] || m_dwMenuParam >= sample.nLength) + return; + + for(auto &pt : sample.cues) + { + if(pt >= sample.nLength) + { + modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Insert Cue Point"); + pt = m_dwMenuParam; + SetModified(SampleHint().Info().Data(), true, true); + break; + } + } +} + + +void CViewSample::OnSampleDeleteCuePoint() +{ + CModDoc *modDoc = GetDocument(); + if(modDoc == nullptr || m_nSample > modDoc->GetNumSamples()) + return; + CSoundFile &sndFile = modDoc->GetSoundFile(); + ModSample &sample = sndFile.GetSample(m_nSample); + if(!sample.HasSampleData() || sample.uFlags[CHN_ADLIB] || m_dwMenuParam >= std::size(sample.cues)) + return; + + modDoc->GetSampleUndo().PrepareUndo(m_nSample, sundo_none, "Delete Cue Point"); + sample.cues[m_dwMenuParam] = MAX_SAMPLE_LENGTH; + SetModified(SampleHint().Info().Data(), true, true); +} + + +bool CViewSample::CanZoomSelection() const +{ + return GetZoomLevel(m_dwEndSel - m_dwBeginSel) <= MAX_ZOOM; +} + + +// Returns auto-zoom level compared to other zoom levels. +// Result is not limited to MIN_ZOOM...MAX_ZOOM range. +int CViewSample::GetZoomLevel(SmpLength length) const +{ + if(m_rcClient.Width() == 0 || length == 0) + return MAX_ZOOM + 1; + + // When m_nZoom > 0, 2^(m_nZoom - 1) = samplesPerPixel [1] + // With auto-zoom setting the whole sample is fitted to screen: + // ViewScreenWidthInPixels * samplesPerPixel = sampleLength (approximately) [2]. + // Solve samplesPerPixel from [2], then "m_nZoom" from [1]. + double zoom = static_cast<double>(length) / m_rcClient.Width(); + zoom = 1 + (std::log10(zoom) / std::log10(2.0)); + if(zoom <= 0) zoom -= 2; + + return static_cast<int>(zoom + mpt::signum(zoom)); +} + + +void CViewSample::DoZoom(int direction, const CPoint &zoomPoint) +{ + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + // zoomOrder: Biggest to smallest zoom order. + std::array<int, (-MIN_ZOOM - 1) + (MAX_ZOOM + 1)> zoomOrder; + for(int i = 2; i < -MIN_ZOOM + 1; ++i) + zoomOrder[i - 2] = MIN_ZOOM + i - 2; // -6, -5, -4, -3... + + for(int i = 1; i <= MAX_ZOOM; ++i) + zoomOrder[i - 1 + (-MIN_ZOOM - 1)] = i; // 1, 2, 3... + zoomOrder.back() = 0; + const int autoZoomLevel = std::max(MIN_ZOOM, GetZoomLevel(sndFile.GetSample(m_nSample).nLength)); + + // Move auto-zoom index (=zero) to the right position in the zoom order according to its real zoom strength. + if (autoZoomLevel < MAX_ZOOM + 1) + { + auto p = std::find(zoomOrder.begin(), zoomOrder.end(), autoZoomLevel); + if(p != zoomOrder.end()) + { + std::move(p, zoomOrder.end() - 1, p + 1); + *p = 0; + } + else + MPT_ASSERT_NOTREACHED(); + } + const std::ptrdiff_t nPos = std::distance(zoomOrder.begin(), std::find(zoomOrder.begin(), zoomOrder.end(), m_nZoom)); + + int newZoom; + if(direction > 0 && nPos > 0) // Zoom in + newZoom = zoomOrder[nPos - 1]; + else if(direction < 0 && nPos + 1 < static_cast<std::ptrdiff_t>(zoomOrder.size())) + newZoom = zoomOrder[nPos + 1]; + else + return; + + if(m_rcClient.PtInRect(zoomPoint)) + { + SetZoom(newZoom, ScreenToSample(zoomPoint.x)); + } else + { + SetZoom(newZoom); + } + SendCtrlMessage(CTRLMSG_SMP_SETZOOM, newZoom); +} + + +BOOL CViewSample::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + // Ctrl + mouse wheel: zoom control. + // One scroll direction zooms in and the other zooms out. + // This behaviour is different from what would happen if simply scrolling + // the zoom levels in the zoom combobox. + if (nFlags == MK_CONTROL && GetDocument()) + { + ScreenToClient(&pt); + DoZoom(zDelta, pt); + } + + return CModScrollView::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CViewSample::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + if(nButton == XBUTTON1) OnPrevInstrument(); + else if(nButton == XBUTTON2) OnNextInstrument(); + CModScrollView::OnXButtonUp(nFlags, nButton, point); +} + + +void CViewSample::OnChangeGridSize() +{ + CSampleGridDlg dlg(this, m_nGridSegments, GetDocument()->GetSoundFile().GetSample(m_nSample).nLength); + if(dlg.DoModal() == IDOK) + { + m_nGridSegments = dlg.m_nSegments; + InvalidateSample(false); + } +} + + +void CViewSample::OnQuickFade() +{ + PostCtrlMessage(IDC_SAMPLE_QUICKFADE); +} + + +void CViewSample::SetTimelineFormat(TimelineFormat fmt) +{ + TrackerSettings::Instance().sampleEditorTimelineFormat = fmt; + UpdateScrollSize(); + InvalidateTimeline(); +} + + +void CViewSample::OnUpdateUndo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if ((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetSampleUndo().CanUndo(m_nSample)); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditUndo, _T("Undo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetUndoName(m_nSample)))); + } +} + + +void CViewSample::OnUpdateRedo(CCmdUI *pCmdUI) +{ + CModDoc *pModDoc = GetDocument(); + if ((pCmdUI) && (pModDoc)) + { + pCmdUI->Enable(pModDoc->GetSampleUndo().CanRedo(m_nSample)); + pCmdUI->SetText(CMainFrame::GetInputHandler()->GetKeyTextFromCommand(kcEditRedo, _T("Redo ") + mpt::ToCString(pModDoc->GetSoundFile().GetCharsetInternal(), pModDoc->GetSampleUndo().GetRedoName(m_nSample)))); + } +} + + +INT_PTR CViewSample::OnToolHitTest(CPoint point, TOOLINFO *pTI) const +{ + CString text; + CRect ncRect; + CPoint screenPoint = point; + ClientToScreen(&screenPoint); + const auto ncButton = GetNcButtonAtPoint(screenPoint, &ncRect); + int buttonID = 0; + if(ncButton != uint32_max) + { + buttonID = cLeftBarButtons[ncButton]; + ScreenToClient(&ncRect); + + text = LoadResourceString(buttonID); + + CommandID cmd = kcNull; + switch(buttonID) + { + case ID_SAMPLE_ZOOMUP: cmd = kcSampleZoomUp; break; + case ID_SAMPLE_ZOOMDOWN: cmd = kcSampleZoomDown; break; + case ID_SAMPLE_DRAW: cmd = kcSampleToggleDrawing; break; + case ID_SAMPLE_ADDSILENCE: cmd = kcSampleResize; break; + case ID_SAMPLE_GRID: cmd = kcSampleGrid; break; + } + if(cmd != kcNull) + { + auto keyText = CMainFrame::GetInputHandler()->m_activeCommandSet->GetKeyTextFromCommand(cmd, 0); + if(!keyText.IsEmpty()) + text += MPT_CFORMAT(" ({})")(keyText); + } + } else + { + const ModSample &sample = GetDocument()->GetSoundFile().GetSample(m_nSample <= GetDocument()->GetNumSamples() ? m_nSample : 0); + auto [item, pos] = PointToItem(point, &ncRect); + switch(item) + { + case HitTestItem::LoopStart: + text = _T("Loop Start"); + break; + case HitTestItem::LoopEnd: + text = _T("Loop End"); + break; + case HitTestItem::SustainStart: + text = _T("Sustain Start"); + break; + case HitTestItem::SustainEnd: + text = _T("Sustain End"); + break; + default: + if(!IsCuePoint(item)) + return CModScrollView::OnToolHitTest(point, pTI); + auto cue = CuePointFromItem(item); + text = MPT_CFORMAT("Cue Point {}")(cue + 1); + } + if(pos <= sample.nLength) + text += _T(": ") + mpt::cfmt::dec(3, ',', pos); + buttonID = static_cast<int>(item) + 1; + } + + pTI->hwnd = m_hWnd; + pTI->uId = buttonID; + pTI->rect = ncRect; + + // MFC will free() the text + auto size = text.GetLength() + 1; + TCHAR *textP = static_cast<TCHAR *>(calloc(size, sizeof(TCHAR))); + std::copy(text.GetString(), text.GetString() + size, textP); + pTI->lpszText = textP; + + return buttonID; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.h b/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.h new file mode 100644 index 00000000..96847316 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_smp.h @@ -0,0 +1,250 @@ +/* + * View_smp.h + * ---------- + * Purpose: Sample tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" +#include "Globals.h" +#include "../soundlib/modsmp_ctrl.h" + + +OPENMPT_NAMESPACE_BEGIN + +#define SMP_LEFTBAR_BUTTONS 8 +class OPLInstrDlg; + +class CViewSample: public CModScrollView +{ +public: + enum Flags + { + SMPSTATUS_MOUSEDRAG = 0x01, + SMPSTATUS_KEYDOWN = 0x02, + SMPSTATUS_NCLBTNDOWN = 0x04, + SMPSTATUS_DRAWING = 0x08, + }; + +protected: + enum class PasteMode + { + Replace, + MixPaste, + Insert + }; + + enum class HitTestItem + { + Nothing, + SampleData, + SelectionStart, + SelectionEnd, + LoopStart, + LoopEnd, + SustainStart, + SustainEnd, + CuePointFirst, + CuePointLast = CuePointFirst + mpt::array_size<decltype(ModSample::cues)>::size - 1, + }; + + std::unique_ptr<OPLInstrDlg> m_oplEditor; + CImageList m_bmpEnvBar; + CRect m_rcClient; + CDC m_offScreenDC, m_waveformDC; + CBitmap m_offScreenBitmap, m_waveformBitmap; + CFont m_timelineFont; + SIZE m_sizeTotal; + UINT m_nBtnMouseOver = 0xFFFF; + int m_nZoom = 0; // < 0: Zoom into sample (2^x:1 ratio), 0: Auto zoom, > 0: Zoom out (1:2^x ratio) + int m_timelineHeight = 0; + int m_timelineUnit = 0; + int m_timelineInterval = 0; + decltype(ModSample::nC5Speed) m_cachedSampleRate = 8363; + FlagSet<Flags> m_dwStatus; + SmpLength m_dwBeginSel, m_dwEndSel, m_dwBeginDrag, m_dwEndDrag; + SmpLength m_dwMenuParam; + SmpLength m_nGridSegments = 0; + SAMPLEINDEX m_nSample = 1; + HitTestItem m_dragItem = HitTestItem::Nothing; + CPoint m_startDragPoint; + SmpLength m_startDragValue = MAX_SAMPLE_LENGTH; + bool m_dragPreparedUndo = false, m_fineDrag = false, m_forceRedrawWaveform = true; + + // Sample drawing + CPoint m_lastDrawPoint; // For drawing horizontal lines + int m_drawChannel; // Which sample channel are we drawing on? + + // Note-off event buffer for MIDI sustain pedal + std::array<std::vector<uint32>, 16> m_midiSustainBuffer; + std::bitset<16> m_midiSustainActive; + + DWORD m_NcButtonState[SMP_LEFTBAR_BUTTONS]; + std::array<SmpLength, MAX_CHANNELS> m_dwNotifyPos; + CModDoc::NoteToChannelMap m_noteChannel; // Note -> Preview channel assignment + +public: + CViewSample(); + DECLARE_SERIAL(CViewSample) + + static std::pair<int, int> FindMinMax(const int8 *p, SmpLength numSamples, int numChannels); + static std::pair<int, int> FindMinMax(const int16 *p, SmpLength numSamples, int numChannels); + +protected: + MPT_NOINLINE void SetModified(SampleHint hint, bool updateAll, bool waveformModified); + void UpdateScrollSize() { UpdateScrollSize(m_nZoom, true); } + void UpdateScrollSize(int newZoom, bool forceRefresh, SmpLength centeredSample = SmpLength(-1)); + void UpdateOPLEditor(); + void SetCurrentSample(SAMPLEINDEX nSmp); + bool IsOPLInstrument() const; + void SetZoom(int nZoom, SmpLength centeredSample = SmpLength(-1)); + int32 SampleToScreen(SmpLength pos, bool ignoreScrollPos = false) const; + SmpLength ScreenToSample(int32 x, bool ignoreSampleLength = false) const; + int32 SecondsToScreen(double x) const; + double ScreenToSeconds(int32 x, bool ignoreSampleLength = false) const; + std::pair<HitTestItem, SmpLength> PointToItem(CPoint point, CRect *rect = nullptr) const; + void PlayNote(ModCommand::NOTE note, const SmpLength nStartPos = 0, int volume = -1); + void NoteOff(ModCommand::NOTE note); + void InvalidateSample(bool invalidateWaveform = true); + void InvalidateTimeline(); + void SetCurSel(SmpLength nBegin, SmpLength nEnd); + void ScrollToPosition(int x); + void DrawPositionMarks(); + void DrawSampleData1(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData); + void DrawSampleData2(HDC hdc, int ymed, int cx, int cy, SmpLength len, SampleFlags uFlags, const void *pSampleData); + void DrawNcButton(CDC *pDC, UINT nBtn); + bool GetNcButtonRect(UINT button, CRect &rect) const; + UINT GetNcButtonAtPoint(CPoint point, CRect *outRect = nullptr) const; + void UpdateNcButtonState(); + void DoPaste(PasteMode pasteMode); + + // Sets sample data on sample draw. + template<class T, class uT> + void SetSampleData(ModSample &smp, const CPoint &point, const SmpLength old); + + // Sets initial draw point on sample draw. + template<class T, class uT> + void SetInitialDrawPoint(ModSample &smp, const CPoint &point); + + // Returns sample value corresponding given point in the sample view. + template<class T, class uT> + T GetSampleValueFromPoint(const ModSample &smp, const CPoint &point) const; + + int GetZoomLevel(SmpLength length) const; + void DoZoom(int direction, const CPoint &zoomPoint = CPoint(-1, -1)); + bool CanZoomSelection() const; + void ScrollToSample(SmpLength centeredSample, bool refresh = true); + + SmpLength ScrollPosToSamplePos() const {return ScrollPosToSamplePos(m_nZoom);} + SmpLength ScrollPosToSamplePos(int nZoom) const; + + void OnMonoConvert(ctrlSmp::StereoToMonoMode convert); + void TrimSample(bool trimToLoopEnd); + + int CalcScroll(int ¤tPos, int amount, int style, int bar); + + SmpLength SnapToGrid(const SmpLength pos) const; + +public: + //{{AFX_VIRTUAL(CViewSample) + void OnDraw(CDC *) override; + void OnInitialUpdate() override; + void UpdateView(UpdateHint hint, CObject *pObj = nullptr) override; + LRESULT OnModViewMsg(WPARAM, LPARAM) override; + BOOL OnDragonDrop(BOOL, const DRAGONDROP *) override; + LRESULT OnPlayerNotify(Notification *) override; + BOOL PreTranslateMessage(MSG *pMsg) override; + BOOL OnScrollBy(CSize sizeScroll, BOOL bDoScroll = TRUE) override; + INT_PTR OnToolHitTest(CPoint point, TOOLINFO *pTI) const override; + //}}AFX_VIRTUAL + +protected: + //{{AFX_MSG(CViewSample) + afx_msg BOOL OnEraseBkgnd(CDC *) { return TRUE; } + afx_msg void OnSetFocus(CWnd *pOldWnd); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnNcCalcSize(BOOL bCalcValidRects, NCCALCSIZE_PARAMS* lpncsp); + afx_msg LRESULT OnNcHitTest(CPoint point); + afx_msg void OnNcPaint(); + afx_msg void OnChar(UINT nChar, UINT nRepCnt, UINT nFlags); + afx_msg void OnNcMouseMove(UINT nHitTest, CPoint point); + afx_msg void OnNcLButtonDown(UINT, CPoint); + afx_msg void OnNcLButtonUp(UINT, CPoint); + afx_msg void OnNcLButtonDblClk(UINT, CPoint); + afx_msg void OnLButtonDblClk(UINT nFlags, CPoint point); + afx_msg void OnLButtonDown(UINT nFlags, CPoint point); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg void OnRButtonDown(UINT, CPoint); + afx_msg void OnMouseMove(UINT, CPoint); + afx_msg BOOL OnSetCursor(CWnd *pWnd, UINT nHitTest, UINT message); + afx_msg void OnEditSelectAll(); + afx_msg void OnEditDelete(); + afx_msg void OnEditCut(); + afx_msg void OnEditCopy(); + afx_msg void OnEditPaste(); + afx_msg void OnEditMixPaste(); + afx_msg void OnEditInsertPaste(); + afx_msg void OnEditUndo(); + afx_msg void OnEditRedo(); + afx_msg void OnSetLoop(); + afx_msg void OnSetSustainLoop(); + afx_msg void On8BitConvert(); + afx_msg void On16BitConvert(); + afx_msg void OnMonoConvertMix() { OnMonoConvert(ctrlSmp::mixChannels); } + afx_msg void OnMonoConvertLeft() { OnMonoConvert(ctrlSmp::onlyLeft); } + afx_msg void OnMonoConvertRight() { OnMonoConvert(ctrlSmp::onlyRight); } + afx_msg void OnMonoConvertSplit() { OnMonoConvert(ctrlSmp::splitSample); } + afx_msg void OnSampleTrim() { TrimSample(false); } + afx_msg void OnPrevInstrument(); + afx_msg void OnNextInstrument(); + afx_msg void OnZoomOnSel(); + afx_msg void OnDropFiles(HDROP hDropInfo); + afx_msg void OnSetLoopStart(); + afx_msg void OnSetLoopEnd(); + afx_msg void OnConvertPingPongLoop(); + afx_msg void OnSetSustainStart(); + afx_msg void OnSetSustainEnd(); + afx_msg void OnConvertPingPongSustain(); + afx_msg void OnSetCuePoint(UINT nID); + afx_msg void OnZoomUp(); + afx_msg void OnZoomDown(); + afx_msg void OnDrawingToggle(); + afx_msg void OnAddSilence(); + afx_msg void OnChangeGridSize(); + afx_msg void OnQuickFade(); + afx_msg LRESULT OnMidiMsg(WPARAM, LPARAM); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); //rewbs.customKeys + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + afx_msg void OnUpdateUndo(CCmdUI *pCmdUI); + afx_msg void OnUpdateRedo(CCmdUI *pCmdUI); + afx_msg void OnSampleSlice(); + afx_msg void OnSampleInsertCuePoint(); + afx_msg void OnSampleDeleteCuePoint(); + afx_msg void OnTimelineFormatSeconds() { SetTimelineFormat(TimelineFormat::Seconds); } + afx_msg void OnTimelineFormatSamples() { SetTimelineFormat(TimelineFormat::Samples); } + afx_msg void OnTimelineFormatSamplesPow2() { SetTimelineFormat(TimelineFormat::SamplesPow2); } + void SetTimelineFormat(TimelineFormat fmt); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() + + BOOL OnGestureZoom(CPoint ptCenter, long lDelta) override + { + DoZoom(lDelta / 10, ptCenter); + return TRUE; + } + + static bool IsCuePoint(HitTestItem item) { return item >= HitTestItem::CuePointFirst && item <= HitTestItem::CuePointLast; } + static int CuePointFromItem(HitTestItem item) { return static_cast<int>(item) - static_cast<int>(HitTestItem::CuePointFirst); } +}; + +DECLARE_FLAGSET(CViewSample::Flags) + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.cpp new file mode 100644 index 00000000..4b9a510f --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.cpp @@ -0,0 +1,4505 @@ +/* + * View_tre.cpp + * ------------ + * Purpose: Tree view for managing open songs, sound files, file browser, ... + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" +#include "Mainfrm.h" +#include "InputHandler.h" +#include "ImageLists.h" +#include "View_tre.h" +#include "Mptrack.h" +#include "Moddoc.h" +#include "Dlsbank.h" +#include "dlg_misc.h" +#include "../common/mptFileIO.h" +#include "../common/FileReader.h" +#include "FileDialog.h" +#include "Globals.h" +#include "ExternalSamples.h" +#include "FolderScanner.h" +#include "LinkResolver.h" +#include "../soundlib/mod_specifications.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../soundlib/MIDIEvents.h" + +#include <winioctl.h> + + +OPENMPT_NAMESPACE_BEGIN + + +CSoundFile *CModTree::m_SongFile = nullptr; + +///////////////////////////////////////////////////////////////////////////// +// CModTreeDropTarget + + +BOOL CModTreeDropTarget::Register(CModTree *pWnd) +{ + m_pModTree = pWnd; + return COleDropTarget::Register(pWnd); +} + + +DROPEFFECT CModTreeDropTarget::OnDragEnter(CWnd *pWnd, COleDataObject *pDataObject, DWORD dwKeyState, CPoint point) +{ + if((m_pModTree) && (m_pModTree == pWnd)) + return m_pModTree->OnDragEnter(pDataObject, dwKeyState, point); + return DROPEFFECT_NONE; +} + + +DROPEFFECT CModTreeDropTarget::OnDragOver(CWnd *pWnd, COleDataObject *pDataObject, DWORD dwKeyState, CPoint point) +{ + if((m_pModTree) && (m_pModTree == pWnd)) + return m_pModTree->OnDragOver(pDataObject, dwKeyState, point); + return DROPEFFECT_NONE; +} + + +BOOL CModTreeDropTarget::OnDrop(CWnd *pWnd, COleDataObject *pDataObject, DROPEFFECT dropEffect, CPoint point) +{ + if((m_pModTree) && (m_pModTree == pWnd)) + return m_pModTree->OnDrop(pDataObject, dropEffect, point); + return FALSE; +} + + +ModTreeDocInfo::ModTreeDocInfo(CModDoc &modDoc) + : modDoc(modDoc) +{ + const CSoundFile &sndFile = modDoc.GetSoundFile(); + tiPatterns.resize(sndFile.Patterns.Size(), nullptr); + tiOrders.resize(sndFile.Order.GetNumSequences()); + tiSequences.resize(sndFile.Order.GetNumSequences(), nullptr); +} + + +///////////////////////////////////////////////////////////////////////////// +// CModTree + +BEGIN_MESSAGE_MAP(CModTree, CTreeCtrl) + //{{AFX_MSG_MAP(CViewModTree) + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONUP() + ON_WM_RBUTTONUP() + ON_WM_XBUTTONUP() + ON_WM_KEYDOWN() + ON_WM_DROPFILES() + ON_NOTIFY_REFLECT(NM_DBLCLK, &CModTree::OnItemDblClk) + ON_NOTIFY_REFLECT(NM_RETURN, &CModTree::OnItemReturn) + ON_NOTIFY_REFLECT(NM_RCLICK, &CModTree::OnItemRightClick) + ON_NOTIFY_REFLECT(NM_CLICK, &CModTree::OnItemLeftClick) + ON_NOTIFY_REFLECT(TVN_ITEMEXPANDED, &CModTree::OnItemExpanded) + ON_NOTIFY_REFLECT(TVN_BEGINDRAG, &CModTree::OnBeginLDrag) + ON_NOTIFY_REFLECT(TVN_BEGINRDRAG, &CModTree::OnBeginRDrag) + ON_NOTIFY_REFLECT(TVN_BEGINLABELEDIT,&CModTree::OnBeginLabelEdit) + ON_NOTIFY_REFLECT(TVN_ENDLABELEDIT, &CModTree::OnEndLabelEdit) + ON_COMMAND(ID_MODTREE_REFRESH, &CModTree::OnRefreshTree) + ON_COMMAND(ID_MODTREE_EXECUTE, &CModTree::OnExecuteItem) + ON_COMMAND(ID_MODTREE_REMOVE, &CModTree::OnDeleteTreeItem) + ON_COMMAND(ID_MODTREE_PLAY, &CModTree::OnPlayTreeItem) + ON_COMMAND(ID_MODTREE_REFRESHINSTRLIB, &CModTree::OnRefreshInstrLib) + ON_COMMAND(ID_MODTREE_OPENITEM, &CModTree::OnOpenTreeItem) + ON_COMMAND(ID_MODTREE_MUTE, &CModTree::OnMuteTreeItem) + ON_COMMAND(ID_MODTREE_MUTE_ONLY_EFFECTS, &CModTree::OnMuteOnlyEffects) + ON_COMMAND(ID_MODTREE_SOLO, &CModTree::OnSoloTreeItem) + ON_COMMAND(ID_MODTREE_UNMUTEALL, &CModTree::OnUnmuteAllTreeItem) + ON_COMMAND(ID_MODTREE_DUPLICATE, &CModTree::OnDuplicateTreeItem) + ON_COMMAND(ID_MODTREE_INSERT, &CModTree::OnInsertTreeItem) + ON_COMMAND(ID_MODTREE_SWITCHTO, &CModTree::OnSwitchToTreeItem) + ON_COMMAND(ID_MODTREE_CLOSE, &CModTree::OnCloseItem) + ON_COMMAND(ID_MODTREE_SETPATH, &CModTree::OnSetItemPath) + ON_COMMAND(ID_MODTREE_SAVEITEM, &CModTree::OnSaveItem) + ON_COMMAND(ID_MODTREE_SAVEALL, &CModTree::OnSaveAll) + ON_COMMAND(ID_MODTREE_RELOADITEM, &CModTree::OnReloadItem) + ON_COMMAND(ID_MODTREE_RELOADALL, &CModTree::OnReloadAll) + ON_COMMAND(ID_MODTREE_FINDMISSING, &CModTree::OnFindMissing) + ON_COMMAND(ID_MODTREE_RENAME, &CModTree::OnRenameItem) + ON_COMMAND(ID_ADD_SOUNDBANK, &CModTree::OnAddDlsBank) + ON_COMMAND(ID_IMPORT_MIDILIB, &CModTree::OnImportMidiLib) + ON_COMMAND(ID_EXPORT_MIDILIB, &CModTree::OnExportMidiLib) + ON_COMMAND(ID_SOUNDBANK_PROPERTIES, &CModTree::OnSoundBankProperties) + ON_COMMAND(ID_MODTREE_SHOWDIRS, &CModTree::OnShowDirectories) + ON_COMMAND(ID_MODTREE_SHOWALLFILES, &CModTree::OnShowAllFiles) + ON_COMMAND(ID_MODTREE_SOUNDFILESONLY,&CModTree::OnShowSoundFiles) + ON_COMMAND(ID_MODTREE_GOTO_INSDIR, &CModTree::OnGotoInstrumentDir) + ON_COMMAND(ID_MODTREE_GOTO_SMPDIR, &CModTree::OnGotoSampleDir) + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CModTree::OnCustomKeyMsg) //rewbs.customKeys + ON_MESSAGE(WM_MOD_MIDIMSG, &CModTree::OnMidiMsg) + //}}AFX_MSG_MAP + ON_WM_KILLFOCUS() + ON_WM_SETFOCUS() +END_MESSAGE_MAP() + + +///////////////////////////////////////////////////////////////////////////// +// CViewModTree construction/destruction + +CModTree::CModTree(CModTree *pDataTree) + : m_pDataTree(pDataTree) +{ + if(m_pDataTree != nullptr) + { + // Set up instrument library monitoring thread + m_hWatchDirKillThread = CreateEvent(nullptr, FALSE, FALSE, nullptr); + m_hSwitchWatchDir = CreateEvent(nullptr, FALSE, FALSE, nullptr); + m_WatchDirThread = std::thread([this]() { MonitorInstrumentLibrary(); }); + } + MemsetZero(m_tiMidi); + MemsetZero(m_tiPerc); + +#if (_WIN32_WINNT >= _WIN32_WINNT_WIN7) + // Wine does not support natural sorting with SORT_DIGITSASNUMBERS, fall back to normal sorting + if(!::CompareString(LOCALE_USER_DEFAULT, m_stringCompareFlags, _T(""), -1, _T(""), -1)) + m_stringCompareFlags &= ~SORT_DIGITSASNUMBERS; +#endif +} + + +CModTree::~CModTree() +{ + delete m_SongFile; + m_SongFile = nullptr; + + if(m_pDataTree != nullptr) + { + SetEvent(m_hWatchDirKillThread); + m_WatchDirThread.join(); + CloseHandle(m_hSwitchWatchDir); + CloseHandle(m_hWatchDirKillThread); + } +} + + +void CModTree::Init() +{ + m_modExtensions = CSoundFile::GetSupportedExtensions(false); + m_MediaFoundationExtensions = FileType(CSoundFile::GetMediaFoundationFileTypes()).GetExtensions(); + + DWORD dwRemove = TVS_SINGLEEXPAND; + DWORD dwAdd = TVS_EDITLABELS | TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS; + + if(IsSampleBrowser()) + { + dwRemove |= (TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS); + dwAdd &= ~(TVS_HASLINES | TVS_LINESATROOT | TVS_HASBUTTONS | TVS_SHOWSELALWAYS); + } + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SINGLEEXPAND) + { + dwRemove &= ~TVS_SINGLEEXPAND; + dwAdd |= TVS_SINGLEEXPAND; + m_dwStatus |= TREESTATUS_SINGLEEXPAND; + } + ModifyStyle(dwRemove, dwAdd); + ModifyStyleEx(0, WS_EX_ACCEPTFILES); + + if(!IsSampleBrowser()) + { + std::vector<TCHAR> curDir(::GetCurrentDirectory(0, nullptr), '\0'); + ::GetCurrentDirectory(static_cast<DWORD>(curDir.size()), curDir.data()); + mpt::PathString dirs[] = + { + TrackerSettings::Instance().PathSamples.GetDefaultDir(), + TrackerSettings::Instance().PathInstruments.GetDefaultDir(), + TrackerSettings::Instance().PathSongs.GetDefaultDir(), + mpt::PathString::FromNative(curDir.data()) + }; + for(auto &path : dirs) + { + m_InstrLibPath = std::move(path); + if(!m_InstrLibPath.empty()) + break; + } + m_InstrLibPath.EnsureTrailingSlash(); + m_pDataTree->InsLibSetFullPath(m_InstrLibPath, mpt::PathString()); + } + + SetImageList(&CMainFrame::GetMainFrame()->m_MiscIcons, TVSIL_NORMAL); + if(!IsSampleBrowser()) + { + // Create Midi Library + m_hMidiLib = InsertItem(_T("MIDI Library"), IMAGE_FOLDER, IMAGE_FOLDER, TVI_ROOT, TVI_LAST); + for(UINT iMidGrp = 0; iMidGrp < 17; iMidGrp++) + { + InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, mpt::ToCString(mpt::Charset::ASCII, szMidiGroupNames[iMidGrp]), IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, (MODITEM_HDR_MIDIGROUP << MIDILIB_SHIFT) | iMidGrp, m_hMidiLib, TVI_LAST); + } + } + m_hInsLib = InsertItem(_T("Instrument Library"), IMAGE_FOLDER, IMAGE_FOLDER, TVI_ROOT, TVI_LAST); + SetItemData(m_hInsLib, reinterpret_cast<DWORD_PTR>(m_hInsLib)); + RefreshMidiLibrary(); + RefreshDlsBanks(); + RefreshInstrumentLibrary(); + m_DropTarget.Register(this); +} + + +BOOL CModTree::PreTranslateMessage(MSG *pMsg) +{ + if(!pMsg) + return TRUE; + + if(m_doLabelEdit) + { + if(pMsg->message == WM_KEYDOWN && pMsg->wParam == VK_RETURN) + { + // End editing by making edit box lose focus. + SetFocus(); + return TRUE; + } + return CTreeCtrl::PreTranslateMessage(pMsg); + } + + if(pMsg->message == WM_KEYDOWN) + { + switch(pMsg->wParam) + { + case VK_SPACE: + if(!(pMsg->lParam & 0x40000000)) + OnPlayTreeItem(); + return TRUE; + + case VK_RETURN: + if(!(pMsg->lParam & 0x40000000)) + { + HTREEITEM hItem = GetSelectedItem(); + if(hItem) + { + if(CMainFrame::GetInputHandler()->CtrlPressed()) + { + const ModItem modItem = GetModItem(hItem); + static constexpr ModItemType instrumentTypes[] = {MODITEM_INSLIB_SAMPLE, MODITEM_INSLIB_INSTRUMENT, MODITEM_MIDIINSTRUMENT, MODITEM_MIDIPERCUSSION, MODITEM_DLSBANK_INSTRUMENT}; + if(mpt::contains(instrumentTypes, modItem.type)) + { + // Ctrl+Enter: Load sample into currently selected sample or instrument slot + // Additionally pressing Shift creates a new sample or instrument slot. Shift key is handled in the Drag&drop handler + CModScrollView *view = static_cast<CModScrollView *>(CMainFrame::GetMainFrame()->GetActiveView()); + if(view) + { + const char *className = view->GetRuntimeClass()->m_lpszClassName; + const bool isSampleView = !strcmp("CViewSample", className); + const bool isInstrumentView = !strcmp("CViewInstrument", className); + mpt::PathString fullPath = InsLibGetFullPath(hItem); + DRAGONDROP dropInfo; + m_hItemDrag = hItem; + m_itemDrag = modItem; + if((isSampleView || isInstrumentView) && GetDropInfo(dropInfo, fullPath)) + { + view->SendMessage(WM_MOD_DRAGONDROPPING, TRUE, reinterpret_cast<LPARAM>(&dropInfo)); + // In case a message box like "create instrument for sample?" showed up + SetFocus(); + } + } + } else if(!IsSampleBrowser()) + { + // Ctrl+Enter: Edit item + EditLabel(hItem); + } + } else + { + if(!ExecuteItem(hItem)) + { + if(ItemHasChildren(hItem)) + { + Expand(hItem, TVE_TOGGLE); + } + } + } + } + } + return TRUE; + + case VK_TAB: + // Tab: Switch between folder and file view. + if(this == CMainFrame::GetMainFrame()->GetUpperTreeview()) + CMainFrame::GetMainFrame()->GetLowerTreeview()->SetFocus(); + else + CMainFrame::GetMainFrame()->GetUpperTreeview()->SetFocus(); + return TRUE; + + case VK_BACK: + // Backspace: Go up one directory + if(GetParentRootItem(GetSelectedItem()) == m_hInsLib || IsSampleBrowser()) + { + InstrumentLibraryChDir(P_(".."), !m_SongFileName.empty()); + return TRUE; + } + break; + + case VK_INSERT: + InsertOrDupItem(!CMainFrame::GetInputHandler()->ShiftPressed()); + return TRUE; + + case VK_APPS: + // Handle Application (menu) key + if(HTREEITEM item = GetSelectedItem()) + { + CRect rect; + GetItemRect(item, &rect, FALSE); + ClientToScreen(rect); + OnItemRightClick(item, rect.TopLeft()); + } + return TRUE; + } + } else if(pMsg->message == WM_CHAR) + { + ModItem item = GetModItem(GetSelectedItem()); + switch(item.type) + { + case MODITEM_SAMPLE: + case MODITEM_INSTRUMENT: + case MODITEM_MIDIINSTRUMENT: + case MODITEM_MIDIPERCUSSION: + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + case MODITEM_DLSBANK_INSTRUMENT: + // Avoid cycling through tree-view elements on key hold + return true; + } + } + + //We handle keypresses before Windows has a chance to handle them (for alt etc..) + if ((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) || + (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = (UINT)pMsg->wParam; + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + InputTargetContext ctx = (InputTargetContext)(kCtxViewTree); + + if(ih->KeyEvent(ctx, nChar, nRepCnt, nFlags, kT) != kcNull) + return true; // Mapped to a command, no need to pass message on. + } + return CTreeCtrl::PreTranslateMessage(pMsg); +} + + +mpt::PathString CModTree::InsLibGetFullPath(HTREEITEM hItem) const +{ + mpt::PathString fullPath = m_InstrLibPath; + fullPath.EnsureTrailingSlash(); + return fullPath + mpt::PathString::FromCString(GetItemText(hItem)); +} + + +bool CModTree::InsLibSetFullPath(const mpt::PathString &libPath, const mpt::PathString &songName) +{ + if(!songName.empty() && mpt::PathString::CompareNoCase(m_SongFileName, songName)) + { + // Load module for previewing its instruments + InputFile f(libPath + songName, TrackerSettings::Instance().MiscCacheCompleteFileBeforeLoading); + if(f.IsValid()) + { + FileReader file = GetFileReader(f); + if(file.IsValid()) + { + if(m_SongFile != nullptr) + { + m_SongFile->Destroy(); + } else + { + m_SongFile = new(std::nothrow) CSoundFile; + } + if(m_SongFile != nullptr) + { + try + { + if(!m_SongFile->Create(file, CSoundFile::loadNoPatternOrPluginData, nullptr)) + { + return false; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } catch(const std::exception &) + { + return false; + } + // Destroy some stuff that we're not going to use anyway. + m_SongFile->Patterns.DestroyPatterns(); + m_SongFile->m_songMessage.clear(); + } + } + } else + { + return false; + } + } + m_InstrLibPath = libPath; + m_SongFileName = songName; + return true; +} + + +bool CModTree::SetSoundFile(FileReader &file) +{ + std::unique_ptr<CSoundFile> sndFile; + try + { + sndFile = std::make_unique<CSoundFile>(); + if(!sndFile->Create(file, CSoundFile::loadNoPatternOrPluginData)) + { + return false; + } + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return false; + } catch(const std::exception &) + { + return false; + } + + if(m_SongFile != nullptr) + { + m_SongFile->Destroy(); + delete m_SongFile; + } + m_SongFile = sndFile.release(); + m_SongFile->Patterns.DestroyPatterns(); + m_SongFile->m_songMessage.clear(); + const mpt::PathString fileName = file.GetOptionalFileName().value_or(P_("")); + m_InstrLibPath = fileName.GetPath(); + m_SongFileName = fileName.GetFullFileName(); + RefreshInstrumentLibrary(); + return true; +} + + +void CModTree::OnOptionsChanged() +{ + DWORD dwRemove = TVS_SINGLEEXPAND, dwAdd = 0; + m_dwStatus &= ~TREESTATUS_SINGLEEXPAND; + if(TrackerSettings::Instance().m_dwPatternSetup & PATTERN_SINGLEEXPAND) + { + dwRemove = 0; + dwAdd = TVS_SINGLEEXPAND; + m_dwStatus |= TREESTATUS_SINGLEEXPAND; + } + ModifyStyle(dwRemove, dwAdd); +} + + +void CModTree::AddDocument(CModDoc &modDoc) +{ + const auto [it, inserted] = m_docInfo.try_emplace(&modDoc, modDoc); + if(!inserted) + return; + + auto &info = it->second; + UpdateView(info, UpdateHint().ModType()); + if(info.hSong) + { + Expand(info.hSong, TVE_EXPAND); + EnsureVisible(info.hSong); + SelectItem(info.hSong); + } +} + + +void CModTree::RemoveDocument(const CModDoc &modDoc) +{ + auto doc = m_docInfo.find(&modDoc); + if(doc == m_docInfo.end()) + return; + + DeleteItem(doc->second.hSong); + m_docInfo.erase(doc); +} + + +// Get CModDoc that is associated with a tree item +ModTreeDocInfo *CModTree::GetDocumentInfoFromItem(HTREEITEM hItem) +{ + hItem = GetParentRootItem(hItem); + if(hItem != nullptr) + { + // Root item has moddoc pointer + const auto doc = m_docInfo.find(reinterpret_cast<const CModDoc *>(GetItemData(hItem))); + if(doc != m_docInfo.end() && hItem == doc->second.hSong) + { + return &doc->second; + } + } + return nullptr; +} + + +// Get modtree doc information for a given CModDoc +ModTreeDocInfo *CModTree::GetDocumentInfoFromModDoc(CModDoc &modDoc) +{ + auto doc = m_docInfo.find(&modDoc); + if(doc != m_docInfo.end()) + return &doc->second; + else + return nullptr; +} + + +size_t CModTree::GetDLSBankIndexFromItem(HTREEITEM hItem) const +{ + return static_cast<size_t>(std::distance(m_tiDLS.begin(), std::find(m_tiDLS.begin(), m_tiDLS.end(), GetParentRootItem(hItem)))); +} + + +CDLSBank *CModTree::GetDLSBankFromItem(HTREEITEM hItem) const +{ + const auto bank = GetDLSBankIndexFromItem(hItem); + if(bank < CTrackApp::gpDLSBanks.size()) + return CTrackApp::gpDLSBanks[bank].get(); + else + return nullptr; +} + + +///////////////////////////////////////////////////////////////////////////// +// CViewModTree drawing + +void CModTree::RefreshMidiLibrary() +{ + CString s; + CString stmp; + TVITEM tvi; + const MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + + if(IsSampleBrowser()) + return; + // Midi Programs + HTREEITEM parent = GetChildItem(m_hMidiLib); + for(UINT iMidi = 0; iMidi < 128; iMidi++) + { + DWORD dwImage = IMAGE_INSTRMUTE; + s = mpt::cfmt::val(iMidi) + _T(": ") + mpt::ToCString(mpt::Charset::ASCII, szMidiProgramNames[iMidi]); + const LPARAM param = (MODITEM_MIDIINSTRUMENT << MIDILIB_SHIFT) | iMidi; + if(!midiLib[iMidi].empty()) + { + s += _T(": ") + midiLib[iMidi].GetFullFileName().ToCString(); + dwImage = IMAGE_INSTRUMENTS; + } + if(!m_tiMidi[iMidi]) + { + m_tiMidi[iMidi] = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + s, dwImage, dwImage, 0, 0, param, parent, TVI_LAST); + } else + { + tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; + tvi.hItem = m_tiMidi[iMidi]; + tvi.pszText = stmp.GetBuffer(s.GetLength() + 1); + tvi.cchTextMax = stmp.GetAllocLength(); + tvi.iImage = tvi.iSelectedImage = dwImage; + GetItem(&tvi); + s.ReleaseBuffer(); + if(s != stmp || tvi.iImage != (int)dwImage) + { + SetItem(m_tiMidi[iMidi], TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + s, dwImage, dwImage, 0, 0, param); + } + } + if((iMidi % 8u) == 7u) + { + parent = GetNextSiblingItem(parent); + } + } + // Midi Percussions + for(UINT iPerc = 24; iPerc <= 84; iPerc++) + { + DWORD dwImage = IMAGE_NOSAMPLE; + s = mpt::ToCString(CSoundFile::GetNoteName((ModCommand::NOTE)(iPerc + NOTE_MIN), CSoundFile::GetDefaultNoteNames())) + + _T(": ") + mpt::ToCString(mpt::Charset::ASCII, szMidiPercussionNames[iPerc - 24]); + const LPARAM param = (MODITEM_MIDIPERCUSSION << MIDILIB_SHIFT) | iPerc; + if(!midiLib[iPerc | 0x80].empty()) + { + s += _T(": ") + midiLib[iPerc | 0x80].GetFullFileName().ToCString(); + dwImage = IMAGE_SAMPLES; + } + if(!m_tiPerc[iPerc]) + { + m_tiPerc[iPerc] = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + s, dwImage, dwImage, 0, 0, param, parent, TVI_LAST); + } else + { + tvi.mask = TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; + tvi.hItem = m_tiPerc[iPerc]; + tvi.pszText = stmp.GetBuffer(s.GetLength() + 1); + tvi.cchTextMax = stmp.GetAllocLength(); + tvi.iImage = tvi.iSelectedImage = dwImage; + GetItem(&tvi); + s.ReleaseBuffer(); + if(s != stmp || tvi.iImage != (int)dwImage) + { + SetItem(m_tiPerc[iPerc], TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE, + s, dwImage, dwImage, 0, 0, param); + } + } + } +} + + +void CModTree::RefreshDlsBanks() +{ + const mpt::Charset charset = mpt::Charset::Locale; + TCHAR s[256]; + HTREEITEM hDlsRoot = m_hMidiLib; + + if(IsSampleBrowser()) + return; + + if(m_tiDLS.size() < CTrackApp::gpDLSBanks.size()) + { + m_tiDLS.resize(CTrackApp::gpDLSBanks.size(), nullptr); + } + + for(size_t iDls = 0; iDls < CTrackApp::gpDLSBanks.size(); iDls++) + { + if(CTrackApp::gpDLSBanks[iDls]) + { + if(!m_tiDLS[iDls]) + { + TVSORTCB tvs; + CDLSBank dlsBank = *CTrackApp::gpDLSBanks[iDls]; + // Add DLS file folder + m_tiDLS[iDls] = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + dlsBank.GetFileName().GetFullFileName().AsNative().c_str(), IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, iDls, TVI_ROOT, hDlsRoot); + // Memorize Banks + std::map<uint16, HTREEITEM> banks; + // Add Drum Kits folder + HTREEITEM hDrums = InsertItem(TVIF_TEXT|TVIF_IMAGE|TVIF_SELECTEDIMAGE | TVIF_PARAM, + _T("Drum Kits"), IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, DLS_DRUM_FOLDER_LPARAM, m_tiDLS[iDls], TVI_LAST); + // Add Instruments + UINT nInstr = dlsBank.GetNumInstruments(); + MPT_ASSERT(nInstr <= 0x10000); + for(UINT iIns = 0; iIns < nInstr; iIns++) + { + const DLSINSTRUMENT *pDlsIns = dlsBank.GetInstrument(iIns); + if(pDlsIns) + { + TCHAR szName[256]; + wsprintf(szName, _T("%u: %s"), pDlsIns->ulInstrument & 0x7F, mpt::ToCString(charset, pDlsIns->szName).GetString()); + const LPARAM lParamInstr = DlsItem::ToLPARAM(static_cast<uint16>(iIns), (pDlsIns->ulInstrument & 0x7F), false); + // Drum Kit + if(pDlsIns->ulBank & F_INSTRUMENT_DRUMS) + { + HTREEITEM hKit = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + szName, IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, lParamInstr, hDrums, TVI_LAST); + MPT_ASSERT(pDlsIns->Regions.size() <= 0x8000); + for(uint32 iRgn = 0; iRgn < static_cast<uint32>(pDlsIns->Regions.size()); iRgn++) + { + if(pDlsIns->Regions[iRgn].IsDummy()) + continue; + + UINT keymin = pDlsIns->Regions[iRgn].uKeyMin; + UINT keymax = pDlsIns->Regions[iRgn].uKeyMax; + + const char *regionName = dlsBank.GetRegionName(iIns, iRgn); + if(regionName == nullptr || !regionName[0]) + { + if(keymin >= 24 && keymin <= 84) + regionName = szMidiPercussionNames[keymin - 24]; + else + regionName = ""; + } + + if(keymin >= keymax) + { + wsprintf(szName, _T("%s%u: %s"), + mpt::ToCString(CSoundFile::GetDefaultNoteName(keymin % 12)).GetString(), + keymin / 12, + mpt::ToCString(charset, regionName).GetString()); + } else + { + wsprintf(szName, _T("%s%u-%s%u: %s"), + mpt::ToCString(CSoundFile::GetDefaultNoteName(keymin % 12)).GetString(), + keymin / 12, + mpt::ToCString(CSoundFile::GetDefaultNoteName(keymax % 12)).GetString(), + keymax / 12, + mpt::ToCString(charset, regionName).GetString()); + } + LPARAM lParam = DlsItem::ToLPARAM(static_cast<uint16>(iIns), static_cast<uint16>(iRgn), true); + InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + szName, IMAGE_INSTRUMENTS, IMAGE_INSTRUMENTS, 0, 0, lParam, hKit, TVI_LAST); + } + tvs.hParent = hKit; + tvs.lpfnCompare = ModTreeDrumCompareProc; + tvs.lParam = reinterpret_cast<LPARAM>(CTrackApp::gpDLSBanks[iDls].get()); + SortChildrenCB(&tvs); + } else + // Melodic + { + uint16 mbank = (pDlsIns->ulBank & 0x7F7F); + auto hbank = banks.find(mbank); + if(hbank == banks.end()) + { + wsprintf(s, (mbank) ? _T("Melodic Bank %02d.%02d") : _T("Melodic"), mbank >> 8, mbank & 0x7F); + // Find out where to insert this bank in the tree + hbank = banks.insert(std::make_pair(mbank, nullptr)).first; + HTREEITEM insertAfter = (hbank == banks.begin()) ? TVI_FIRST : std::prev(hbank)->second; + hbank->second = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE, + s, IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, 0, + m_tiDLS[iDls], insertAfter); + } + InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, + szName, IMAGE_INSTRUMENTS, IMAGE_INSTRUMENTS, 0, 0, lParamInstr, hbank->second, TVI_LAST); + } + } + } + // Sort items + for(auto &b : banks) + { + tvs.hParent = b.second; + tvs.lpfnCompare = ModTreeInsLibCompareProc; + tvs.lParam = reinterpret_cast<LPARAM>(this); + SortChildrenCB(&tvs); + } + if(hDrums) + { + tvs.hParent = hDrums; + tvs.lpfnCompare = ModTreeInsLibCompareProc; + tvs.lParam = reinterpret_cast<LPARAM>(this); + SortChildrenCB(&tvs); + } + } + hDlsRoot = m_tiDLS[iDls]; + } else + { + if(m_tiDLS[iDls]) + { + DeleteItem(m_tiDLS[iDls]); + m_tiDLS[iDls] = nullptr; + } + } + } +} + + +void CModTree::RefreshInstrumentLibrary() +{ + SetRedraw(FALSE); + // Check if the currently selected item should be selected after refreshing + CString selectedName; + if((IsSampleBrowser() || GetParentRootItem(GetSelectedItem()) == m_hInsLib) + && GetItemText(GetSampleBrowser()->m_hInsLib) == (m_SongFileName.empty() ? m_InstrLibPath : m_SongFileName).ToCString()) + { + selectedName = GetItemText(GetSelectedItem()); + } + if(!m_InstrLibHighlightPath.empty()) + { + selectedName = m_InstrLibHighlightPath.ToCString(); + } + m_InstrLibHighlightPath = {}; + EmptyInstrumentLibrary(); + FillInstrumentLibrary(selectedName); + auto selectedItem = GetSelectedItem(); + if(selectedItem) + EnsureVisible(selectedItem); + SetRedraw(TRUE); + if(!IsSampleBrowser()) + { + m_pDataTree->InsLibSetFullPath(m_InstrLibPath, m_SongFileName); + m_pDataTree->RefreshInstrumentLibrary(); + } +} + + +void CModTree::UpdateView(ModTreeDocInfo &info, UpdateHint hint) +{ + TCHAR stmp[256]; + TVITEM tvi; + MemsetZero(tvi); + const FlagSet<HintType> hintType = hint.GetType(); + if(IsSampleBrowser() || hintType == HINT_NONE) + return; + + const CModDoc &modDoc = info.modDoc; + const CSoundFile &sndFile = modDoc.GetSoundFile(); + + // Create headers + const GeneralHint generalHint = hint.ToType<GeneralHint>(); + if(generalHint.GetType()[HINT_MODTYPE | HINT_MODGENERAL] || (!info.hSong)) + { + // Module folder + sub folders + CString name = modDoc.GetPathNameMpt().GetFullFileName().ToCString(); + if(name.IsEmpty()) + name = mpt::PathString::FromCString(modDoc.GetTitle()).SanitizeComponent().ToCString(); + + if(!info.hSong) + { + info.hSong = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, name, IMAGE_FOLDER, IMAGE_FOLDER, 0, 0, reinterpret_cast<LPARAM>(&info.modDoc), TVI_ROOT, TVI_FIRST); + info.hOrders = InsertItem(_T("Sequence"), IMAGE_FOLDER, IMAGE_FOLDER, info.hSong, TVI_LAST); + info.hPatterns = InsertItem(_T("Patterns"), IMAGE_FOLDER, IMAGE_FOLDER, info.hSong, TVI_LAST); + info.hSamples = InsertItem(_T("Samples"), IMAGE_FOLDER, IMAGE_FOLDER, info.hSong, TVI_LAST); + } else if(generalHint.GetType()[HINT_MODGENERAL | HINT_MODTYPE]) + { + if(name != GetItemText(info.hSong)) + { + SetItemText(info.hSong, name); + } + } + } + + if(sndFile.GetModSpecifications().instrumentsMax > 0) + { + if(!info.hInstruments) + info.hInstruments = InsertItem(_T("Instruments"), IMAGE_FOLDER, IMAGE_FOLDER, info.hSong, info.hSamples); + } else + { + if(info.hInstruments) + { + DeleteItem(info.hInstruments); + info.hInstruments = NULL; + } + } + if(!info.hComments) + info.hComments = InsertItem(_T("Comments"), IMAGE_COMMENTS, IMAGE_COMMENTS, info.hSong, TVI_LAST); + // Add effects + const PluginHint pluginHint = hint.ToType<PluginHint>(); + if(pluginHint.GetType()[HINT_MODTYPE | HINT_PLUGINNAMES]) + { + HTREEITEM hItem = info.hEffects ? GetChildItem(info.hEffects) : nullptr; + PLUGINDEX firstPlug = 0, lastPlug = MAX_MIXPLUGINS - 1; + if(pluginHint.GetPlugin() && hItem) + { + // Only update one specific plugin name + firstPlug = lastPlug = pluginHint.GetPlugin() - 1; + while(hItem && GetItemData(hItem) != firstPlug) + { + hItem = GetNextSiblingItem(hItem); + } + } + bool hasPlugs = false; + for(PLUGINDEX i = firstPlug; i <= lastPlug; i++) + { + const SNDMIXPLUGIN &plugin = sndFile.m_MixPlugins[i]; + if(plugin.IsValidPlugin()) + { + // Now we can be sure that we want to create this folder. + if(!info.hEffects) + { + info.hEffects = InsertItem(_T("Plugins"), IMAGE_FOLDER, IMAGE_FOLDER, info.hSong, info.hInstruments ? info.hInstruments : info.hSamples); + } + + CString s = MPT_CFORMAT("FX{}: {}")(i + 1, mpt::ToCString(plugin.GetName())); + int nImage = IMAGE_NOPLUGIN; + if(plugin.pMixPlugin != nullptr) + nImage = (plugin.pMixPlugin->IsInstrument()) ? IMAGE_PLUGININSTRUMENT : IMAGE_EFFECTPLUGIN; + + if(hItem) + { + // Replace existing item + tvi.mask = TVIF_TEXT | TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM; + tvi.hItem = hItem; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + GetItem(&tvi); + if(tvi.iImage != nImage || tvi.lParam != i || s != CString(tvi.pszText)) + { + SetItem(hItem, TVIF_TEXT | TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, nImage, nImage, 0, 0, i); + } + hItem = GetNextSiblingItem(hItem); + } else + { + InsertItem(TVIF_TEXT | TVIF_HANDLE | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, nImage, nImage, 0, 0, i, info.hEffects, TVI_LAST); + } + hasPlugs = true; + } + } + if(!hasPlugs && firstPlug == lastPlug) + { + // If we only updated one plugin, we still need to check all the other slots if there is any plugin in them. + for(const auto &plug : sndFile.m_MixPlugins) + { + if(plug.IsValidPlugin()) + { + hasPlugs = true; + break; + } + } + } + if(!hasPlugs && info.hEffects) + { + DeleteItem(info.hEffects); + info.hEffects = nullptr; + } else if(!pluginHint.GetPlugin()) + { + // Delete superfluous tree items + while(hItem) + { + HTREEITEM nextItem = GetNextSiblingItem(hItem); + DeleteItem(hItem); + hItem = nextItem; + } + } + } + // Add Orders + const PatternHint patternHint = hint.ToType<PatternHint>(); + const SequenceHint seqHint = hint.ToType<SequenceHint>(); + if(info.hOrders && (seqHint.GetType()[HINT_MODTYPE | HINT_MODSEQUENCE | HINT_SEQNAMES] || patternHint.GetType()[HINT_PATNAMES])) + { + const PATTERNINDEX nPat = patternHint.GetPattern(); + bool adjustParentNode = false; // adjust sequence name of "Sequence" node? + + // (only one seq remaining || previously only one sequence): update parent item + if((info.tiSequences.size() > 1 && sndFile.Order.GetNumSequences() == 1) || (info.tiSequences.size() == 1 && sndFile.Order.GetNumSequences() > 1)) + { + for(auto &seq : info.tiOrders) + { + for(auto &ord : seq) + { + if(ord) + DeleteItem(ord); + ord = nullptr; + } + } + for(auto &seq : info.tiSequences) + { + if(seq) + DeleteItem(seq); + seq = nullptr; + } + info.tiOrders.resize(sndFile.Order.GetNumSequences()); + info.tiSequences.resize(sndFile.Order.GetNumSequences(), nullptr); + adjustParentNode = true; + } + + // If there are too many sequences, delete them. + for(size_t seq = sndFile.Order.GetNumSequences(); seq < info.tiSequences.size(); seq++) if (info.tiSequences[seq]) + { + for(auto &ord : info.tiOrders[seq]) if (ord) + { + DeleteItem(ord); ord = nullptr; + } + DeleteItem(info.tiSequences[seq]); info.tiSequences[seq] = nullptr; + } + if (info.tiSequences.size() < sndFile.Order.GetNumSequences()) // Resize tiSequences if needed. + { + info.tiSequences.resize(sndFile.Order.GetNumSequences(), nullptr); + info.tiOrders.resize(sndFile.Order.GetNumSequences()); + } + + HTREEITEM hAncestorNode = info.hOrders; + + SEQUENCEINDEX nSeqMin = 0, nSeqMax = sndFile.Order.GetNumSequences() - 1; + SEQUENCEINDEX nHintParam = seqHint.GetSequence(); + if(seqHint.GetType()[HINT_SEQNAMES] && (nHintParam <= nSeqMax)) + nSeqMin = nSeqMax = nHintParam; + + // Adjust caption of the "Sequence" node (if only one sequence exists, it should be labeled with the sequence name) + if((seqHint.GetType()[HINT_SEQNAMES] && sndFile.Order.GetNumSequences() == 1) || adjustParentNode) + { + CString seqName = mpt::ToCString(sndFile.Order(0).GetName()); + if(seqName.IsEmpty() || sndFile.Order.GetNumSequences() > 1) + seqName = _T("Sequence"); + else + seqName = _T("Sequence: ") + seqName; + SetItem(info.hOrders, TVIF_TEXT, seqName, 0, 0, 0, 0, 0); + } + + // go through all sequences + CString seqName; + for(SEQUENCEINDEX seq = nSeqMin; seq <= nSeqMax; seq++) + { + if(sndFile.Order.GetNumSequences() > 1) + { + // more than one sequence -> add folder + if(sndFile.Order(seq).GetName().empty()) + { + seqName = MPT_CFORMAT("Sequence {}")(seq + 1); + } else + { + seqName = MPT_CFORMAT("{}: ")(seq + 1); + seqName += mpt::ToCString(sndFile.Order(seq).GetName()); + } + + UINT state = (seq == sndFile.Order.GetCurrentSequenceIndex()) ? TVIS_BOLD : 0; + + if(info.tiSequences[seq] == NULL) + { + info.tiSequences[seq] = InsertItem(seqName, IMAGE_FOLDER, IMAGE_FOLDER, info.hOrders, TVI_LAST); + } + // Update bold item + _tcscpy(stmp, seqName); + tvi.mask = TVIF_TEXT | TVIF_HANDLE | TVIF_STATE | TVIF_PARAM; + tvi.state = 0; + tvi.stateMask = TVIS_BOLD; + tvi.hItem = info.tiSequences[seq]; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + LPARAM param = (seq << SEQU_SHIFT) | ORDERINDEX_INVALID; + GetItem(&tvi); + if(tvi.state != state || tvi.pszText != seqName || tvi.lParam != param) + SetItem(info.tiSequences[seq], TVIF_TEXT | TVIF_STATE | TVIF_PARAM, seqName, 0, 0, state, TVIS_BOLD, param); + + hAncestorNode = info.tiSequences[seq]; + } + + const ORDERINDEX ordLength = sndFile.Order(seq).GetLengthTailTrimmed(); + // If there are items past the new sequence length, delete them. + for(size_t nOrd = ordLength; nOrd < info.tiOrders[seq].size(); nOrd++) if (info.tiOrders[seq][nOrd]) + { + DeleteItem(info.tiOrders[seq][nOrd]); info.tiOrders[seq][nOrd] = NULL; + } + if (info.tiOrders[seq].size() < ordLength) // Resize tiOrders if needed. + info.tiOrders[seq].resize(ordLength, nullptr); + const bool patNamesOnly = patternHint.GetType()[HINT_PATNAMES]; + + //if (hintFlagPart == HINT_PATNAMES) && (dwHintParam < sndFile.Order().GetLength())) imin = imax = dwHintParam; + CString patName; + for(ORDERINDEX iOrd = 0; iOrd < ordLength; iOrd++) + { + TCHAR s[256]; + s[0] = 0; + if(patNamesOnly && sndFile.Order(seq)[iOrd] != nPat) + continue; + UINT state = (iOrd == info.ordSel && seq == info.seqSel) ? TVIS_BOLD : 0; + if(sndFile.Order(seq)[iOrd] < sndFile.Patterns.Size()) + { + patName = mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.Patterns[sndFile.Order(seq)[iOrd]].GetName()); + if(!patName.IsEmpty()) + { + wsprintf(s, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_HEXDISPLAY) ? _T("[%02Xh] %u: ") : _T("[%02u] %u: "), + iOrd, sndFile.Order(seq)[iOrd]); + _tcscat(s, patName.GetString()); + } else + { + wsprintf(s, (TrackerSettings::Instance().m_dwPatternSetup & PATTERN_HEXDISPLAY) ? _T("[%02Xh] Pattern %u") : _T("[%02u] Pattern %u"), + iOrd, sndFile.Order(seq)[iOrd]); + } + } else + { + if(sndFile.Order(seq)[iOrd] == sndFile.Order.GetIgnoreIndex()) + { + // +++ Item + wsprintf(s, _T("[%02u] Skip"), iOrd); + } else + { + // --- Item + wsprintf(s, _T("[%02u] Stop"), iOrd); + } + } + + LPARAM param = (seq << SEQU_SHIFT) | iOrd; + if(info.tiOrders[seq][iOrd]) + { + tvi.mask = TVIF_TEXT | TVIF_HANDLE | TVIF_STATE; + tvi.state = 0; + tvi.stateMask = TVIS_BOLD; + tvi.hItem = info.tiOrders[seq][iOrd]; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + GetItem(&tvi); + if(tvi.state != state || _tcscmp(s, stmp)) + SetItem(info.tiOrders[seq][iOrd], TVIF_TEXT | TVIF_STATE | TVIF_PARAM, s, 0, 0, state, TVIS_BOLD, param); + } else + { + info.tiOrders[seq][iOrd] = InsertItem(TVIF_HANDLE | TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, IMAGE_PARTITION, IMAGE_PARTITION, 0, 0, param, hAncestorNode, TVI_LAST); + } + } + } + } + // Add Patterns + if(info.hPatterns && patternHint.GetType()[HINT_MODTYPE | HINT_PATNAMES]) + { + const PATTERNINDEX nPat = patternHint.GetPattern(); + PATTERNINDEX minPat = 0, maxPat = sndFile.Patterns.Size(); + if(patternHint.GetType()[HINT_PATNAMES] && nPat < sndFile.Patterns.Size()) + { + minPat = nPat; + maxPat = nPat + 1; + } + + for(size_t pat = sndFile.Patterns.Size(); pat < info.tiPatterns.size(); pat++) + { + DeleteItem(info.tiPatterns[pat]); + } + info.tiPatterns.resize(sndFile.Patterns.Size(), nullptr); + + mpt::winstring patName; + for(PATTERNINDEX pat = minPat; pat < maxPat; pat++) + { + TCHAR s[256]; + s[0] = 0; + if(sndFile.Patterns.IsValidPat(pat)) + { + patName = mpt::ToWin(sndFile.GetCharsetInternal(), sndFile.Patterns[pat].GetName()); + wsprintf(s, _T("%u"), pat); + if(!patName.empty()) + { + _tcscat(s, _T(": ")); + _tcscat(s, patName.c_str()); + } + if(info.tiPatterns[pat]) + { + tvi.mask = TVIF_TEXT | TVIF_HANDLE; + tvi.hItem = info.tiPatterns[pat]; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + GetItem(&tvi); + if(_tcscmp(s, stmp)) + SetItem(info.tiPatterns[pat], TVIF_TEXT, s, 0, 0, 0, 0, 0); + } else + { + info.tiPatterns[pat] = InsertItem(s, IMAGE_PATTERNS, IMAGE_PATTERNS, info.hPatterns, TVI_LAST); + } + SetItemData(info.tiPatterns[pat], pat); + } else if(pat < info.tiPatterns.size() && info.tiPatterns[pat]) + { + DeleteItem(info.tiPatterns[pat]); + info.tiPatterns[pat] = nullptr; + } + } + } + // Add Samples + const SampleHint sampleHint = hint.ToType<SampleHint>(); + if(info.hSamples && sampleHint.GetType()[HINT_MODTYPE | HINT_SMPNAMES | HINT_SAMPLEINFO | HINT_SAMPLEDATA]) + { + const SAMPLEINDEX hintSmp = sampleHint.GetSample(); + SAMPLEINDEX smin = 1, smax = MAX_SAMPLES - 1; + if(sampleHint.GetType()[HINT_SMPNAMES | HINT_SAMPLEINFO | HINT_SAMPLEDATA] && hintSmp > 0 && hintSmp < MAX_SAMPLES) + { + smin = smax = hintSmp; + } + HTREEITEM hChild = GetNthChildItem(info.hSamples, smin - 1); + for(SAMPLEINDEX nSmp = smin; nSmp <= smax; nSmp++) + { + TCHAR s[256]; + s[0] = 0; + HTREEITEM hNextChild = GetNextSiblingItem(hChild); + if(nSmp <= sndFile.GetNumSamples()) + { + const ModSample &sample = sndFile.GetSample(nSmp); + const bool sampleExists = (sample.HasSampleData()); + + static constexpr int Images[] = + { + IMAGE_NOSAMPLE, IMAGE_NOSAMPLE, IMAGE_NOSAMPLE, + IMAGE_SAMPLES, IMAGE_SAMPLEACTIVE, IMAGE_SAMPLEMUTE, + IMAGE_EXTSAMPLE, IMAGE_EXTSAMPLEACTIVE, IMAGE_EXTSAMPLEMUTE, + IMAGE_OPLINSTR, IMAGE_OPLINSTRACTIVE, IMAGE_OPLINSTRMUTE, + }; + + int image = 0; + if(sampleExists) + image = 3; + if(sample.uFlags[SMP_KEEPONDISK]) + image = 6; + if(sample.uFlags[CHN_ADLIB]) + image = 9; + + if(info.modDoc.IsSampleMuted(nSmp)) + image += 2; + else if(info.samplesPlaying[nSmp]) + image++; + + if(sample.uFlags[SMP_KEEPONDISK] && !sampleExists) + image = IMAGE_EXTSAMPLEMISSING; + else + image = Images[image]; + + if(sndFile.GetType() == MOD_TYPE_MPT) + { + const TCHAR *status = _T(""); + if(sample.uFlags[SMP_KEEPONDISK]) + { + status = sampleExists ? _T(" [external]") : _T(" [MISSING]"); + } + wsprintf(s, _T("%3d: %s%s%s"), nSmp, sample.uFlags.test_all(SMP_MODIFIED | SMP_KEEPONDISK) ? _T("* ") : _T(""), mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[nSmp]).GetString(), status); + } else + { + wsprintf(s, _T("%3d: %s"), nSmp, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[nSmp]).GetString()); + } + + if(!hChild) + { + hChild = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, image, image, 0, 0, nSmp, info.hSamples, TVI_LAST); + } else + { + tvi.mask = TVIF_TEXT | TVIF_HANDLE | TVIF_IMAGE; + tvi.hItem = hChild; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + tvi.iImage = tvi.iSelectedImage = image; + GetItem(&tvi); + if(tvi.iImage != image || _tcsncmp(s, stmp, std::size(stmp)) || GetItemData(hChild) != nSmp) + { + SetItem(hChild, TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, image, image, 0, 0, nSmp); + } + } + } else if(hChild != nullptr) + { + DeleteItem(hChild); + } else + { + break; + } + hChild = hNextChild; + } + } + // Add Instruments + const InstrumentHint instrHint = hint.ToType<InstrumentHint>(); + if(info.hInstruments && instrHint.GetType()[HINT_MODTYPE | HINT_INSNAMES | HINT_INSTRUMENT]) + { + INSTRUMENTINDEX smin = 1, smax = MAX_INSTRUMENTS - 1; + const INSTRUMENTINDEX hintIns = instrHint.GetInstrument(); + if(instrHint.GetType()[HINT_INSNAMES | HINT_INSTRUMENT] && hintIns > 0 && hintIns < MAX_INSTRUMENTS) + { + smin = smax = hintIns; + } + HTREEITEM hChild = GetNthChildItem(info.hInstruments, smin - 1); + for(INSTRUMENTINDEX nIns = smin; nIns <= smax; nIns++) + { + TCHAR s[256]; + s[0] = 0; + HTREEITEM hNextChild = GetNextSiblingItem(hChild); + if(nIns <= sndFile.GetNumInstruments()) + { + wsprintf(s, _T("%3u: %s"), nIns, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.GetInstrumentName(nIns)).GetString()); + + int nImage = IMAGE_INSTRUMENTS; + if(info.instrumentsPlaying[nIns]) + nImage = IMAGE_INSTRACTIVE; + if(!sndFile.Instruments[nIns] || info.modDoc.IsInstrumentMuted(nIns)) + nImage = IMAGE_INSTRMUTE; + + if(!hChild) + { + hChild = InsertItem(TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, nImage, nImage, 0, 0, nIns, info.hInstruments, TVI_LAST); + } else + { + tvi.mask = TVIF_TEXT | TVIF_HANDLE | TVIF_IMAGE; + tvi.hItem = hChild; + tvi.pszText = stmp; + tvi.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + tvi.iImage = tvi.iSelectedImage = nImage; + GetItem(&tvi); + if(tvi.iImage != nImage || _tcscmp(s, stmp) || GetItemData(hChild) != nIns) + { + SetItem(hChild, TVIF_TEXT | TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_PARAM, s, nImage, nImage, 0, 0, nIns); + } + } + } else if(hChild != nullptr) + { + DeleteItem(hChild); + } else + { + break; + } + hChild = hNextChild; + } + } +} + + +CModTree::ModItem CModTree::GetModItem(HTREEITEM hItem) +{ + if(!hItem) + return ModItem(MODITEM_NULL); + // First, test root items + if(hItem == m_hInsLib) + return ModItem(MODITEM_HDR_INSTRUMENTLIB); + if(hItem == m_hMidiLib) + return ModItem(MODITEM_HDR_MIDILIB); + + // The immediate parent of the item (NULL if this item is on the root level of the tree) + HTREEITEM hItemParent = GetParentItem(hItem); + // Parent of the parent. + HTREEITEM hItemParentParent = GetParentItem(hItemParent); + // Get the root parent of the selected item, which can be the item itself. + HTREEITEM hRootParent = hItem; + if(!IsSampleBrowser()) + { + hRootParent = GetParentRootItem(hItem); + } + + uint32 itemData = static_cast<uint32>(GetItemData(hItem)); + uint32 rootItemData = static_cast<uint32>(GetItemData(hRootParent)); + + // Midi Library + if(hRootParent == m_hMidiLib && hRootParent != hItem && !IsSampleBrowser()) + { + return ModItem(static_cast<ModItemType>(itemData >> MIDILIB_SHIFT), itemData & MIDILIB_MASK); + } + // Instrument Library + if(hRootParent == m_hInsLib || (IsSampleBrowser() && hItem != m_hInsLib)) + { + TVITEM tvi; + tvi.mask = TVIF_IMAGE | TVIF_HANDLE; + tvi.hItem = hItem; + tvi.iImage = 0; + if(GetItem(&tvi)) + { + switch(tvi.iImage) + { + case IMAGE_SAMPLES: + case IMAGE_OPLINSTR: + // Sample + return ModItem(MODITEM_INSLIB_SAMPLE); + case IMAGE_INSTRUMENTS: + // Instrument + return ModItem(MODITEM_INSLIB_INSTRUMENT); + case IMAGE_FOLDERSONG: + // Song + return ModItem(MODITEM_INSLIB_SONG); + default: + return ModItem(MODITEM_INSLIB_FOLDER); + } + } + return ModItem(MODITEM_NULL); + } + if(IsSampleBrowser()) + return ModItem(MODITEM_NULL); + // Songs + if(auto info = GetDocumentInfoFromItem(hRootParent); info != nullptr) + { + m_selectedDoc = &info->modDoc; + + if(hItem == info->hSong) + return ModItem(MODITEM_HDR_SONG); + if(hRootParent == info->hSong) + { + if(hItem == info->hPatterns) + return ModItem(MODITEM_HDR_PATTERNS); + if(hItem == info->hOrders) + return ModItem(MODITEM_HDR_ORDERS); + if(hItem == info->hSamples) + return ModItem(MODITEM_HDR_SAMPLES); + if(hItem == info->hInstruments) + return ModItem(MODITEM_HDR_INSTRUMENTS); + if(hItem == info->hEffects) + return ModItem(MODITEM_HDR_EFFECTS); + if(hItem == info->hComments) + return ModItem(MODITEM_COMMENTS); + // Order List or Sequence item? + if((hItemParent == info->hOrders) || (hItemParentParent == info->hOrders)) + { + const auto ord = static_cast<ORDERINDEX>(itemData & SEQU_MASK); + const auto seq = static_cast<SEQUENCEINDEX>(itemData >> SEQU_SHIFT); + if(ord == ORDERINDEX_INVALID) + return ModItem(MODITEM_SEQUENCE, seq); + else + return ModItem(MODITEM_ORDER, ord, seq); + } + + ModItem modItem(MODITEM_NULL, itemData); + if(hItemParent == info->hPatterns) + { + // Pattern + modItem.type = MODITEM_PATTERN; + } else if(hItemParent == info->hSamples) + { + // Sample + modItem.type = MODITEM_SAMPLE; + } else if(hItemParent == info->hInstruments) + { + // Instrument + modItem.type = MODITEM_INSTRUMENT; + } else if(hItemParent == info->hEffects) + { + // Effect + modItem.type = MODITEM_EFFECT; + } + return modItem; + } + } + + // DLS banks + if(itemData < m_tiDLS.size() && hItem == m_tiDLS[itemData]) + return ModItem(MODITEM_DLSBANK_FOLDER, itemData); + + // DLS Instruments + if(hRootParent != nullptr) + { + if(rootItemData < m_tiDLS.size() && m_tiDLS[rootItemData] == hRootParent) + { + int image = 0, selImage = 0; + const bool isFolder = GetItemImage(hItem, image, selImage) && (image == IMAGE_FOLDER || image == IMAGE_OPENFOLDER); + if(!isFolder || GetItemData(hItemParent) == DLS_DRUM_FOLDER_LPARAM) + return DlsItem::FromLPARAM(itemData); + } + } + return ModItem(MODITEM_NULL); +} + + +bool CModTree::ExecuteItem(HTREEITEM hItem) +{ + if(hItem) + { + const ModItem modItem = GetModItem(hItem); + uint32 modItemID = modItem.val1; + CModDoc *modDoc = m_docInfo.count(m_selectedDoc) ? m_selectedDoc : nullptr; + + switch(modItem.type) + { + case MODITEM_COMMENTS: + if(modDoc) + modDoc->ActivateView(IDD_CONTROL_COMMENTS, 0); + return true; + + case MODITEM_SEQUENCE: + if(modDoc + && modItemID < modDoc->GetSoundFile().Order.GetNumSequences() + && !modDoc->GetSoundFile().Order(static_cast<SEQUENCEINDEX>(modItemID)).GetLengthTailTrimmed()) + modDoc->ActivateView(IDD_CONTROL_PATTERNS, (modItemID << SEQU_SHIFT) | SEQU_INDICATOR); + return true; + + case MODITEM_ORDER: + if(modDoc) + modDoc->ActivateView(IDD_CONTROL_PATTERNS, modItemID | (uint32(modItem.val2) << SEQU_SHIFT) | SEQU_INDICATOR); + return true; + + case MODITEM_PATTERN: + if(modDoc) + modDoc->ActivateView(IDD_CONTROL_PATTERNS, modItemID); + return true; + + case MODITEM_SAMPLE: + if(modDoc) + modDoc->ActivateView(IDD_CONTROL_SAMPLES, modItemID); + return true; + + case MODITEM_INSTRUMENT: + if(modDoc) + modDoc->ActivateView(IDD_CONTROL_INSTRUMENTS, modItemID); + return true; + + case MODITEM_MIDIPERCUSSION: + modItemID |= 0x80; + [[fallthrough]]; + case MODITEM_MIDIINSTRUMENT: + OpenMidiInstrument(modItemID); + return true; + + case MODITEM_EFFECT: + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + PlayItem(hItem, NOTE_MIDDLEC); + return true; + + case MODITEM_INSLIB_SONG: + case MODITEM_INSLIB_FOLDER: + // If it's a shell link, resolve the link target + if(auto file = LinkResolver().Resolve(m_InstrLibPath.ToCString() + GetItemText(hItem)); !file.empty()) + { + SetFullInstrumentLibraryPath(file); + } else + { + InstrumentLibraryChDir(mpt::PathString::FromCString(GetItemText(hItem)), modItem.type == MODITEM_INSLIB_SONG); + } + return true; + + case MODITEM_HDR_SONG: + if(modDoc) + modDoc->ActivateWindow(); + return true; + + case MODITEM_DLSBANK_INSTRUMENT: + if(GetItemData(GetParentItem(hItem)) != DLS_DRUM_FOLDER_LPARAM) + PlayItem(hItem, NOTE_MIDDLEC); + return true; + + case MODITEM_HDR_INSTRUMENTLIB: + if(IsSampleBrowser()) + { + BrowseForFolder dlg(m_InstrLibPath, _T("Select a new instrument library folder...")); + if(dlg.Show()) + { + SetFullInstrumentLibraryPath(dlg.GetDirectory()); + } + } + return true; + } + } + return false; +} + + +void CModTree::PlayDLSItem(const CDLSBank &dlsBank, const DlsItem &item, ModCommand::NOTE note) +{ + UINT rgn, instr = item.GetInstr(); + if(item.IsPercussion()) + rgn = item.GetRegion(); + else + rgn = dlsBank.GetRegionFromKey(instr, note - NOTE_MIN); + CMainFrame::GetMainFrame()->PlayDLSInstrument(dlsBank, instr, rgn, note); +} + + +BOOL CModTree::PlayItem(HTREEITEM hItem, ModCommand::NOTE note, int volume) +{ + if(hItem) + { + const ModItem modItem = GetModItem(hItem); + uint32 modItemID = modItem.val1; + CModDoc *modDoc = m_docInfo.count(m_selectedDoc) ? m_selectedDoc : nullptr; + + switch(modItem.type) + { + case MODITEM_SAMPLE: + if(modDoc) + { + if(note == NOTE_NOTECUT) + { + modDoc->NoteOff(0, true); // cut previous playing samples + } else if(note & 0x80) + { + modDoc->NoteOff(note & 0x7F, true); + } else + { + modDoc->NoteOff(0, true); // cut previous playing samples + modDoc->PlayNote(PlayNoteParam(note & 0x7F).Sample(static_cast<SAMPLEINDEX>(modItemID)).Volume(volume)); + } + } + return TRUE; + + case MODITEM_INSTRUMENT: + if(modDoc) + { + if(note == NOTE_NOTECUT) + { + modDoc->NoteOff(0, true); + } else if(note & 0x80) + { + modDoc->NoteOff(note & 0x7F, true); + } else + { + modDoc->NoteOff(0, true); + modDoc->PlayNote(PlayNoteParam(note & 0x7F).Instrument(static_cast<INSTRUMENTINDEX>(modItemID)).Volume(volume)); + } + } + return TRUE; + + case MODITEM_EFFECT: + if((modDoc) && (modItemID < MAX_MIXPLUGINS)) + { + modDoc->TogglePluginEditor(modItemID); + } + return TRUE; + + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + if(note != NOTE_NOTECUT) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(!m_SongFileName.empty()) + { + // Preview sample / instrument in module + const size_t n = ConvertStrTo<size_t>(GetItemText(hItem)); + if(pMainFrm && m_SongFile) + { + if(modItem.type == MODITEM_INSLIB_INSTRUMENT) + { + pMainFrm->PlaySoundFile(*m_SongFile, static_cast<INSTRUMENTINDEX>(n), SAMPLEINDEX_INVALID, note, volume); + } else + { + pMainFrm->PlaySoundFile(*m_SongFile, INSTRUMENTINDEX_INVALID, static_cast<SAMPLEINDEX>(n), note, volume); + } + } + } else + { + // Preview sample / instrument file + auto file = InsLibGetFullPath(hItem); + // If it's a shell link, resolve the link target + if(auto resolvedName = LinkResolver().Resolve(file.AsNative().c_str()); !resolvedName.empty()) + file = std::move(resolvedName); + + if(pMainFrm) + pMainFrm->PlaySoundFile(file, note, volume); + } + } else + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->StopPreview(); + } + + return TRUE; + + case MODITEM_MIDIPERCUSSION: + modItemID |= 0x80; + [[fallthrough]]; + case MODITEM_MIDIINSTRUMENT: + { + const MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + if(modItemID < midiLib.size() && !midiLib[modItemID].empty()) + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CDLSBank *dlsBank = nullptr; + if(!mpt::PathString::CompareNoCase(m_cachedBankName, midiLib[modItemID])) + { + dlsBank = m_cachedBank.get(); + } + if(dlsBank == nullptr && CDLSBank::IsDLSBank(midiLib[modItemID])) + { + m_cachedBank = std::make_unique<CDLSBank>(); + if(m_cachedBank->Open(midiLib[modItemID])) + { + m_cachedBankName = midiLib[modItemID]; + dlsBank = m_cachedBank.get(); + } + } + if(dlsBank != nullptr) + { + uint32 item = 0; + if(modItemID < 0x80) + { + dlsBank->FindInstrument(false, 0xFFFF, modItemID, note - NOTE_MIN, &item); + PlayDLSItem(*dlsBank, DlsItem(static_cast<uint16>(item)), note); + } else + { + dlsBank->FindInstrument(true, 0xFFFF, 0xFF, modItemID & 0x7F, &item); + PlayDLSItem(*dlsBank, DlsItem(static_cast<uint16>(item), static_cast<uint16>(dlsBank->GetRegionFromKey(item, modItemID & 0x7F))), note); + } + } else + { + pMainFrm->PlaySoundFile(midiLib[modItemID], note, volume); + } + } + } + return TRUE; + + case MODITEM_DLSBANK_INSTRUMENT: + { + const DlsItem &item = static_cast<const DlsItem &>(modItem); + CDLSBank *dlsBank = GetDLSBankFromItem(hItem); + if(dlsBank != nullptr) + { + PlayDLSItem(*dlsBank, item, note); + return TRUE; + } + } + } + } + return FALSE; +} + + +BOOL CModTree::SetMidiInstrument(UINT nIns, const mpt::PathString &fileName) +{ + MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + if(nIns < 128) + { + midiLib[nIns] = fileName; + RefreshMidiLibrary(); + return TRUE; + } + return FALSE; +} + + +BOOL CModTree::SetMidiPercussion(UINT nPerc, const mpt::PathString &fileName) +{ + MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + if(nPerc < 128) + { + UINT nIns = nPerc | 0x80; + midiLib[nIns] = fileName; + RefreshMidiLibrary(); + return TRUE; + } + return FALSE; +} + + +static mpt::ustring TreeDeletionString(const mpt::uchar *type, uint32 id, const mpt::ustring &name) +{ + mpt::ustring s = MPT_UFORMAT("Remove {} {}")(mpt::ustring(type), id); + if(!name.empty()) + s += U_(": ") + name; + s.append(1, UC_('?')); + return s; +} + + +void CModTree::DeleteTreeItem(HTREEITEM hItem) +{ + const ModItem modItem = GetModItem(hItem); + uint32 modItemID = modItem.val1; + + CModDoc *modDoc = m_docInfo.count(m_selectedDoc) ? m_selectedDoc : nullptr; + CSoundFile *sndFile = modDoc ? &modDoc->GetSoundFile() : nullptr; + if(modItem.IsSongItem() && modDoc == nullptr) + { + return; + } + + switch(modItem.type) + { + case MODITEM_SEQUENCE: + if(sndFile) + { + const SEQUENCEINDEX seq = static_cast<SEQUENCEINDEX>(modItemID); + if(Reporting::Confirm(TreeDeletionString(UL_("sequence"), seq + 1, sndFile->Order(seq).GetName()), false, true) == cnfNo) break; + sndFile->Order.RemoveSequence(seq); + modDoc->UpdateAllViews(nullptr, SequenceHint().Data()); + } + break; + + case MODITEM_ORDER: + // might be slightly annoying to ask for confirmation here, and it's rather easy to restore the orderlist anyway. + if(modDoc && modDoc->RemoveOrder(static_cast<SEQUENCEINDEX>(modItem.val2), static_cast<ORDERINDEX>(modItem.val1))) + { + modDoc->UpdateAllViews(nullptr, SequenceHint().Data()); + } + break; + + case MODITEM_PATTERN: + if(modDoc && sndFile) + { + const PATTERNINDEX pat = static_cast<PATTERNINDEX>(modItemID); + bool isUsed = false; + // First, find all used patterns in all sequences. + for(const auto &sequence : sndFile->Order) + { + if(sequence.FindOrder(pat) != ORDERINDEX_INVALID) + { + isUsed = true; + break; + } + } + mpt::ustring s = TreeDeletionString(UL_("pattern"), modItemID, mpt::ToUnicode(sndFile->GetCharsetInternal(), sndFile->Patterns[pat].GetName())); + s += MPT_UFORMAT("\nThis pattern is currently {}used.")(isUsed ? U_("") : U_("un")); + if(Reporting::Confirm(s, false, isUsed) == cnfYes && modDoc->RemovePattern(pat)) + { + modDoc->UpdateAllViews(nullptr, PatternHint(pat).Data().Names()); + if(isUsed) + modDoc->UpdateAllViews(nullptr, SequenceHint().Data()); // Pattern color will change in sequence + } + } + break; + + case MODITEM_SAMPLE: + if(modDoc && sndFile) + { + if(!sndFile->GetSample(static_cast<SAMPLEINDEX>(modItemID)).HasSampleData() + || Reporting::Confirm(TreeDeletionString(UL_("sample"), modItemID, mpt::ToUnicode(sndFile->GetCharsetInternal(), sndFile->m_szNames[modItemID])), false, true) == cnfYes) + { + const SAMPLEINDEX smp = static_cast<SAMPLEINDEX>(modItemID); + modDoc->GetSampleUndo().PrepareUndo(smp, sundo_replace, "Delete"); + const SAMPLEINDEX oldNumSamples = modDoc->GetNumSamples(); + if(modDoc->RemoveSample(smp)) + { + modDoc->UpdateAllViews(nullptr, SampleHint(modDoc->GetNumSamples() != oldNumSamples ? 0 : smp).Info().Data().Names()); + } + } + } + break; + + case MODITEM_INSTRUMENT: + if(modDoc && sndFile) + { + if(sndFile->Instruments[modItemID] == nullptr + || Reporting::Confirm(TreeDeletionString(UL_("instrument"), modItemID, mpt::ToUnicode(sndFile->GetCharsetInternal(), sndFile->Instruments[modItemID]->name)), false, true) == cnfYes) + { + const INSTRUMENTINDEX ins = static_cast<INSTRUMENTINDEX>(modItemID); + modDoc->GetInstrumentUndo().PrepareUndo(ins, "Delete"); + const INSTRUMENTINDEX oldNumInstrs = modDoc->GetNumInstruments(); + if(modDoc->RemoveInstrument(ins)) + { + modDoc->UpdateAllViews(nullptr, InstrumentHint(modDoc->GetNumInstruments() != oldNumInstrs ? 0 : ins).Info().Envelope().ModType()); + } + } + } + break; + + case MODITEM_EFFECT: + if(modDoc && Reporting::Confirm(TreeDeletionString(UL_("plugin FX"), modItemID + 1, sndFile->m_MixPlugins[modItemID].GetName()), false, true) == cnfYes) + { + modDoc->RemovePlugin(static_cast<PLUGINDEX>(modItemID)); + } + break; + + case MODITEM_MIDIINSTRUMENT: + SetMidiInstrument(modItemID, P_("")); + RefreshMidiLibrary(); + break; + case MODITEM_MIDIPERCUSSION: + SetMidiPercussion(modItemID, P_("")); + RefreshMidiLibrary(); + break; + + case MODITEM_DLSBANK_FOLDER: + CTrackApp::RemoveDLSBank(modItemID); + RefreshDlsBanks(); + break; + + case MODITEM_INSLIB_SONG: + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + { + // Create double-null-terminated path + const mpt::winstring fullPath = InsLibGetFullPath(hItem).AsNative() + _T('\0'); + SHFILEOPSTRUCT fos; + MemsetZero(fos); + fos.hwnd = m_hWnd; + fos.wFunc = FO_DELETE; + fos.pFrom = fullPath.c_str(); + fos.fFlags = CMainFrame::GetInputHandler()->ShiftPressed() ? 0 : FOF_ALLOWUNDO; + if(!SHFileOperation(&fos) && !fos.fAnyOperationsAborted) + { + HTREEITEM newSel = GetNextSiblingItem(hItem); + if(!newSel) newSel = GetPrevSiblingItem(hItem); + SelectItem(newSel); + RefreshInstrumentLibrary(); + SetFocus(); + } + } + break; + } +} + + +BOOL CModTree::OpenTreeItem(HTREEITEM hItem) +{ + const ModItem modItem = GetModItem(hItem); + + switch(modItem.type) + { + case MODITEM_INSLIB_SONG: + theApp.OpenDocumentFile(InsLibGetFullPath(hItem).ToCString()); + break; + case MODITEM_HDR_INSTRUMENTLIB: + CTrackApp::OpenDirectory(m_InstrLibPath); + break; + case MODITEM_INSLIB_FOLDER: + // Open path in Explorer + CTrackApp::OpenDirectory(InsLibGetFullPath(hItem)); + break; + } + return TRUE; +} + + +BOOL CModTree::OpenMidiInstrument(DWORD dwItem) +{ + std::vector<FileType> mediaFoundationTypes = CSoundFile::GetMediaFoundationFileTypes(); + FileDialog dlg = OpenFileDialog() + .EnableAudioPreview() + .ExtensionFilter( + "All Instruments and Banks (*.xi,*.pat,*.iti,*.sfz,*.dls,*.sf2,...)|*.xi;*.pat;*.iti;*.sfz;*.wav;*.w64;*.caf;*.aif;*.aiff;*.sbk;*.sf2;*.sf3;*.sf4;*.dls;*.mss;*.flac;*.opus;*.ogg;*.oga;*.mp1;*.mp2;*.mp3" + ToFilterOnlyString(mediaFoundationTypes, true).ToLocale() + "|" + "FastTracker II Instruments (*.xi)|*.xi|" + "GF1 Patches (*.pat)|*.pat|" + "Wave Files (*.wav)|*.wav|" + "Wave64 Files (*.w64)|*.w64|" + "CAF Files (*.caf)|*.caf|" + #ifdef MPT_WITH_FLAC + "FLAC Files (*.flac,*.oga)|*.flac;*.oga|" + #endif // MPT_WITH_FLAC + #if defined(MPT_WITH_OPUSFILE) + "Opus Files (*.opus,*.oga)|*.opus;*.oga|" + #endif // MPT_WITH_OPUSFILE + #if defined(MPT_WITH_VORBISFILE) || defined(MPT_WITH_STBVORBIS) + "Ogg Vorbis Files (*.ogg,*.oga)|*.ogg;*.oga|" + #endif // VORBIS + #if defined(MPT_ENABLE_MP3_SAMPLES) + "MPEG Files (*.mp1,*.mp2,*.mp3)|*.mp1;*.mp2;*.mp3|" + #endif // MPT_ENABLE_MP3_SAMPLES + #if defined(MPT_WITH_MEDIAFOUNDATION) + + ToFilterString(mediaFoundationTypes, FileTypeFormatShowExtensions).ToLocale() + + #endif + "Impulse Tracker Instruments (*.iti)|*.iti;*.its|" + "SFZ Instruments (*.sfz)|*.sfz|" + "SoundFont 2.0 Banks (*.sf2)|*.sbk;*.sf2;*.sf3;*.sf4|" + "DLS Sound Banks (*.dls;*.mss)|*.dls;*.mss|" + "All Files (*.*)|*.*||"); + if(!dlg.Show()) return FALSE; + + if(dwItem & 0x80) + return SetMidiPercussion(dwItem & 0x7F, dlg.GetFirstFile()); + else + return SetMidiInstrument(dwItem, dlg.GetFirstFile()); +} + + +// Empty Instrument Library +void CModTree::EmptyInstrumentLibrary() +{ + HTREEITEM h; + if(!m_hInsLib) + return; + if(!IsSampleBrowser()) + { + DeleteChildren(m_hInsLib); + } else + { + while((h = GetNextItem(m_hInsLib, TVGN_NEXT)) != NULL) + { + DeleteItem(h); + } + } +} + + +// Refresh Instrument Library +void CModTree::FillInstrumentLibrary(const TCHAR *selectedItem) +{ + if(!m_hInsLib) + return; + + SetRedraw(FALSE); + if(!m_SongFileName.empty() && IsSampleBrowser() && m_SongFile) + { + // Fill browser with samples / instruments of module file + SetItemText(m_hInsLib, m_SongFileName.AsNative().c_str()); + SetItemImage(m_hInsLib, IMAGE_FOLDERSONG, IMAGE_FOLDERSONG); + for(INSTRUMENTINDEX ins = 1; ins <= m_SongFile->GetNumInstruments(); ins++) + { + ModInstrument *pIns = m_SongFile->Instruments[ins]; + if(pIns) + { + TCHAR s[MAX_INSTRUMENTNAME + 10]; + _sntprintf(s, std::size(s), _T("%3d: %s"), ins, mpt::ToWin(m_SongFile->GetCharsetInternal(), pIns->name).c_str()); + InsertInsLibItem(s, IMAGE_INSTRUMENTS, selectedItem); + } + } + for(SAMPLEINDEX smp = 1; smp <= m_SongFile->GetNumSamples(); smp++) + { + const ModSample &sample = m_SongFile->GetSample(smp); + if(sample.HasSampleData() || sample.uFlags[CHN_ADLIB]) + { + TCHAR s[MAX_SAMPLENAME + 10]; + _sntprintf(s, std::size(s), _T("%3d: %s"), smp, mpt::ToWin(m_SongFile->GetCharsetInternal(), m_SongFile->m_szNames[smp]).c_str()); + InsertInsLibItem(s, sample.uFlags[CHN_ADLIB] ? IMAGE_OPLINSTR : IMAGE_SAMPLES, selectedItem); + } + } + } else if(!m_InstrLibPath.empty()) + { + if(!IsSampleBrowser()) + { + SetItemText(m_hInsLib, _T("Instrument Library (") + m_InstrLibPath.ToCString() + _T(")")); + } else + { + SetItemText(m_hInsLib, m_InstrLibPath.ToCString()); + SetItemImage(m_hInsLib, IMAGE_FOLDER, IMAGE_FOLDER); + } + + // Enumerating Drives... + if(!IsSampleBrowser()) + { + CImageList &images = CMainFrame::GetMainFrame()->m_MiscIcons; + // Avoid adding the same images again and again... + images.SetImageCount(IMGLIST_NUMIMAGES); + + TCHAR s[] = _T("?:\\"); + for(int drive = 'A'; drive <= 'Z'; drive++) + { + s[0] = static_cast<TCHAR>(drive); + auto driveType = GetDriveType(s); + if(driveType != DRIVE_UNKNOWN && driveType != DRIVE_NO_ROOT_DIR) + { + if(driveType == DRIVE_REMOVABLE) + { + TCHAR sDevice[] = _T("\\\\.\\?:"); + sDevice[4] = s[0]; + auto hDevice = CreateFile(sDevice, FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, 0, nullptr); + if(hDevice != INVALID_HANDLE_VALUE) + { + // Check if removable media is inserted + DWORD bytesReturned = 0; + auto success = DeviceIoControl(hDevice, IOCTL_STORAGE_CHECK_VERIFY2, nullptr, 0, nullptr, 0, &bytesReturned, nullptr); + CloseHandle(hDevice); + if(!success && GetLastError() == ERROR_NOT_READY) + continue; + } + } + SHFILEINFO fileInfo; + SHGetFileInfo(s, 0, &fileInfo, sizeof(fileInfo), SHGFI_ICON | SHGFI_SMALLICON | SHGFI_USEFILEATTRIBUTES); // SHGFI_USEFILEATTRIBUTES speeds up retrieval for offline network shares + const int imageIndex = fileInfo.hIcon ? images.Add(fileInfo.hIcon) : IMAGE_FOLDER; + InsertInsLibItem(s, imageIndex < 0 ? IMAGE_FOLDER : imageIndex, selectedItem); + DestroyIcon(fileInfo.hIcon); + } + } + } + + // Enumerating Directories and samples/instruments + const mpt::PathString path = m_InstrLibPath + P_("*.*"); + const bool showDirs = !IsSampleBrowser() || TrackerSettings::Instance().showDirsInSampleBrowser; + const bool showInstrs = IsSampleBrowser(); + + enum + { + FILTER_FIRST_VALID = 0, + FILTER_REJECT_FILE = -1, + FILTER_UNKNOWN_FILE = -2, + }; + + const auto FilterFile = [this, showInstrs, showDirs](const mpt::PathString &fileName) -> int + { + + static constexpr auto instrExts = {"xi", "iti", "sfz", "sf2", "sf3", "sf4", "sbk", "dls", "mss", "pat"}; + static constexpr auto sampleExts = {"wav", "flac", "ogg", "opus", "mp1", "mp2", "mp3", "smp", "raw", "s3i", "its", "aif", "aiff", "au", "snd", "svx", "voc", "8sv", "8svx", "16sv", "16svx", "w64", "caf", "sb0", "sb2", "sbi"}; + static constexpr auto allExtsBlacklist = {"txt", "diz", "nfo", "doc", "ini", "pdf", "zip", "rar", "lha", "exe", "dll", "lnk", "url"}; + + // Get lower-case file extension without dot. + mpt::PathString extPS = fileName.GetFileExt(); + std::string ext = extPS.ToUTF8(); + if(!ext.empty()) + { + ext.erase(0, 1); + ext = mpt::ToLowerCaseAscii(ext); + extPS = mpt::PathString::FromUTF8(ext); + } + + bool knownExtension = true; + if(mpt::contains(instrExts, ext)) + { + if(showInstrs) + return IMAGE_INSTRUMENTS; + } else if(mpt::contains(sampleExts, ext)) + { + if(showInstrs) + return IMAGE_SAMPLES; + } else if(mpt::contains(m_modExtensions, ext)) + { + if(showDirs || m_showAllFiles) + return IMAGE_FOLDERSONG; + } else if(!extPS.empty() && mpt::contains(m_MediaFoundationExtensions, extPS)) + { + if(showInstrs) + return IMAGE_SAMPLES; + } else + { + if(showDirs) + { + // Amiga-style prefix (i.e. mod.songname) + std::string prefixExt = fileName.ToUTF8(); + const auto dotPos = prefixExt.find('.'); + if(dotPos != std::string::npos && mpt::contains(m_modExtensions, prefixExt.erase(dotPos))) + return IMAGE_FOLDERSONG; + } + knownExtension = false; + } + + if(m_showAllFiles && !mpt::contains(allExtsBlacklist, ext)) + return IMAGE_SAMPLES; + + return knownExtension ? FILTER_REJECT_FILE : FILTER_UNKNOWN_FILE; + }; + + HKEY hkey = nullptr; + bool showHidden = false; + if(auto cr = RegOpenKeyEx(HKEY_CURRENT_USER, _T("Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Advanced"), 0, KEY_READ, &hkey); cr == ERROR_SUCCESS) + { + DWORD dataType = REG_NONE; + DWORD data = 0, datasize = sizeof(data); + if(RegQueryValueEx(hkey, _T("Hidden"), 0, &dataType, reinterpret_cast<BYTE *>(&data), &datasize) == ERROR_SUCCESS && dataType == REG_DWORD) + showHidden = (data == 1); + RegCloseKey(hkey); + } + + LinkResolver linkResolver; + HANDLE hFind; + WIN32_FIND_DATA wfd; + MemsetZero(wfd); + if((hFind = FindFirstFile(path.AsNative().c_str(), &wfd)) != INVALID_HANDLE_VALUE) + { + do + { + // Up Directory + int type = FILTER_REJECT_FILE; + if(!_tcscmp(wfd.cFileName, _T(".."))) + { + if(showDirs) + type = IMAGE_FOLDERPARENT; + } else if(wfd.dwFileAttributes & FILE_ATTRIBUTE_OFFLINE) + { + // Ignore unavailable files + continue; + } else if((wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) + && ((wfd.dwFileAttributes & FILE_ATTRIBUTE_SYSTEM) || !showHidden)) + { + // Only show hidden files if Explorer is configured to do so (and never show hidden system files) + continue; + } else if(wfd.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) + { + // Directory + if(_tcscmp(wfd.cFileName, _T(".")) && showDirs) + type = IMAGE_FOLDER; + } else if(wfd.nFileSizeHigh > 0 || wfd.nFileSizeLow >= 9) + { + type = FilterFile(mpt::PathString::FromNative(wfd.cFileName)); + if(type == FILTER_UNKNOWN_FILE) + { + // Try resolving file as link if it wasn't a module or instrument + const auto nativePath = (m_InstrLibPath.AsNative() + wfd.cFileName); + if(const auto resolvedName = linkResolver.Resolve(nativePath.c_str()); !resolvedName.empty()) + { + if(resolvedName.IsDirectory() && showDirs) + type = IMAGE_FOLDER; + else if(resolvedName.IsFile()) + type = FilterFile(resolvedName); + } + } + } + if(type >= FILTER_FIRST_VALID) + { + auto item = InsertInsLibItem(wfd.cFileName, type, selectedItem); + // Apparently TVIS_CUT cannot be set during insertion + if(wfd.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) + SetItemState(item, TVIS_CUT, TVIS_CUT); + } + } while(FindNextFile(hFind, &wfd)); + FindClose(hFind); + } + } + + // Sort items + TVSORTCB tvs; + tvs.hParent = (!IsSampleBrowser()) ? m_hInsLib : TVI_ROOT; + tvs.lpfnCompare = ModTreeInsLibCompareNamesProc; + tvs.lParam = (LPARAM)this; + SortChildrenCB(&tvs); + SetRedraw(TRUE); + + { + mpt::lock_guard<mpt::mutex> l(m_WatchDirMutex); + if(m_InstrLibPath != m_WatchDir) + { + m_WatchDir = m_InstrLibPath; + SetEvent(m_hSwitchWatchDir); + } + } +} + + +// Monitor changes in the instrument library folder. +void CModTree::MonitorInstrumentLibrary() +{ + SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_LOWEST); + mpt::log::Trace::SetThreadId(mpt::log::Trace::ThreadKindWatchdir, GetCurrentThreadId()); + DWORD result; + mpt::PathString lastWatchDir; + HANDLE hWatchDir = INVALID_HANDLE_VALUE; + DWORD64 lastRefresh = Util::GetTickCount64(); + DWORD timeout = INFINITE; + DWORD interval = TrackerSettings::Instance().FSUpdateInterval; + do + { + { + mpt::lock_guard<mpt::mutex> l(m_WatchDirMutex); + if(m_WatchDir != lastWatchDir) + { + if(hWatchDir != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(hWatchDir); + hWatchDir = INVALID_HANDLE_VALUE; + lastWatchDir = mpt::PathString(); + } + if(!m_WatchDir.empty()) + { + hWatchDir = FindFirstChangeNotification(m_WatchDir.AsNative().c_str(), FALSE, FILE_NOTIFY_CHANGE_FILE_NAME | FILE_NOTIFY_CHANGE_DIR_NAME); + lastWatchDir = m_WatchDir; + } + } + } + const HANDLE waitHandles[3] = {m_hWatchDirKillThread, m_hSwitchWatchDir, hWatchDir}; + result = WaitForMultipleObjects(hWatchDir != INVALID_HANDLE_VALUE ? 3 : 2, waitHandles, FALSE, timeout); + DWORD64 now = Util::GetTickCount64(); + if(result == WAIT_TIMEOUT) + { + PostMessage(WM_COMMAND, ID_MODTREE_REFRESHINSTRLIB); + lastRefresh = now; + timeout = INFINITE; + } else if(result == WAIT_OBJECT_0 + 1) + { + // nothing + // will switch to new dir in next loop iteration + } else if(result == WAIT_OBJECT_0 + 2) + { + FindNextChangeNotification(hWatchDir); + timeout = 0; // update timeout later + } + if(timeout != INFINITE) + { + // Update timeout. Can happen either because we just got a change event, + // or because we got a directory switch event and still have a change + // notification pending. + if(now - lastRefresh >= interval) + { + PostMessage(WM_COMMAND, ID_MODTREE_REFRESHINSTRLIB); + lastRefresh = now; + timeout = INFINITE; + } else + { + timeout = interval - static_cast<DWORD>(now - lastRefresh); + } + } + } while(result != WAIT_OBJECT_0); + if(hWatchDir != INVALID_HANDLE_VALUE) + { + FindCloseChangeNotification(hWatchDir); + hWatchDir = INVALID_HANDLE_VALUE; + lastWatchDir = mpt::PathString(); + } +} + + +// Insert sample browser item. +HTREEITEM CModTree::InsertInsLibItem(const TCHAR *name, int image, const TCHAR *selectIfMatch) +{ + HTREEITEM item = InsertItem(TVIF_IMAGE | TVIF_SELECTEDIMAGE | TVIF_TEXT, + name, + image, image, + 0, 0, + 0, + (!IsSampleBrowser()) ? m_hInsLib : TVI_ROOT, + TVI_LAST); + SetItemData(item, reinterpret_cast<DWORD_PTR>(item)); // Used by ModTreeInsLibCompareNamesProc + if(selectIfMatch != nullptr && !_tcscmp(name, selectIfMatch)) + { + SelectItem(item); + EnsureVisible(item); + } + return item; +} + + +int CModTree::ModTreeInsLibCompareNamesGetItem(HTREEITEM item, CString &resultStr) +{ + TVITEM tvi; + tvi.mask = TVIF_TEXT | TVIF_IMAGE; + tvi.hItem = item; + int len = std::max(64, resultStr.GetAllocLength()); + while(true) + { + tvi.pszText = resultStr.GetBuffer(len); + tvi.cchTextMax = len + 1; + if(!GetItem(&tvi)) + return int16_max; + if(int resultLen = static_cast<int>(_tcsnlen(tvi.pszText, len)); resultLen < len) + { + resultStr.ReleaseBuffer(resultLen); + + // Item image indicates sort order + switch(tvi.iImage) + { + case IMAGE_FOLDERPARENT: + return 1; + case IMAGE_FOLDER: + return 2; + case IMAGE_FOLDERSONG: + return 3; + case IMAGE_SAMPLES: + case IMAGE_OPLINSTR: + // Only group instruments and samples separately if we're browsing inside a module file + if(!m_SongFileName.empty()) + return 5; + [[fallthrough]]; + case IMAGE_INSTRUMENTS: + default: + return 4; + } + } + len *= 2; + } +} + + +int CALLBACK CModTree::ModTreeInsLibCompareNamesProc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort) +{ + auto that = reinterpret_cast<CModTree *>(lParamSort); + const auto itemL = reinterpret_cast<HTREEITEM>(lParam1), itemR = reinterpret_cast<HTREEITEM>(lParam2); + if(itemL == that->m_hInsLib) + return -1; + else if(itemR == that->m_hInsLib) + return 1; + + const int sortOrderL = that->ModTreeInsLibCompareNamesGetItem(itemL, that->m_compareStrL); + const int sortOrderR = that->ModTreeInsLibCompareNamesGetItem(itemR, that->m_compareStrR); + if(sortOrderL != sortOrderR) + return sortOrderL - sortOrderR; + + return ::CompareString(LOCALE_USER_DEFAULT, that->m_stringCompareFlags, + that->m_compareStrL.GetBuffer(), -1, + that->m_compareStrR.GetBuffer(), -1) - 2; +} + + +int CALLBACK CModTree::ModTreeInsLibCompareProc(LPARAM lParam1, LPARAM lParam2, LPARAM) +{ + lParam1 &= 0x7FFFFFFF; + lParam2 &= 0x7FFFFFFF; + return static_cast<int>(lParam1 - lParam2); +} + + +int CALLBACK CModTree::ModTreeDrumCompareProc(LPARAM lParam1, LPARAM lParam2, LPARAM pDLSBank) +{ + lParam1 &= 0x7FFFFFFF; + lParam2 &= 0x7FFFFFFF; + // Same drum instrument? + if((lParam1 & 0xFFFF) == (lParam2 & 0xFFFF)) + { + if(pDLSBank) + { + // Compare minimum key of these regions + const DLSINSTRUMENT *pDlsIns = reinterpret_cast<CDLSBank *>(pDLSBank)->GetInstrument(lParam1 & 0xFFFF); + uint16 region1 = static_cast<uint16>((lParam1 >> 16) & 0xFFFF); + uint16 region2 = static_cast<uint16>((lParam2 >> 16) & 0xFFFF); + if(pDlsIns && region1 < pDlsIns->Regions.size() && region2 < pDlsIns->Regions.size()) + { + lParam1 = pDlsIns->Regions[region1].uKeyMin; + lParam2 = pDlsIns->Regions[region2].uKeyMin; + } + } + } + return static_cast<int>(lParam1 - lParam2); +} + + +void CModTree::SetFullInstrumentLibraryPath(mpt::PathString path) +{ + if(path.IsDirectory()) + { + path.EnsureTrailingSlash(); + InstrumentLibraryChDir(path, false); + } else if(path.IsFile()) + { + // Browse module contents + CModTree *dirBrowser = CMainFrame::GetMainFrame()->GetUpperTreeview(); + dirBrowser->m_InstrLibPath = path.GetPath(); + dirBrowser->RefreshInstrumentLibrary(); + dirBrowser->InstrumentLibraryChDir(path.GetFullFileName(), true); + } +} + + +void CModTree::InstrumentLibraryChDir(mpt::PathString dir, bool isSong) +{ + if(dir.empty()) + return; + if(IsSampleBrowser()) + { + CMainFrame::GetMainFrame()->GetUpperTreeview()->InstrumentLibraryChDir(dir, isSong); + return; + } + + BeginWaitCursor(); + + bool ok = false; + const bool goUp = (dir == P_("..")); + m_previousPath = {}; + if(isSong && !goUp) + { + ok = m_pDataTree->InsLibSetFullPath(m_InstrLibPath, dir); + if(ok) + { + m_pDataTree->RefreshInstrumentLibrary(); + m_InstrLibHighlightPath = dir; + } + } else + { + if(goUp) + { + if(isSong) + { + // Leave song + m_InstrLibHighlightPath = std::move(m_pDataTree->m_SongFileName); + dir = m_InstrLibPath; + } else + { + // Go one dir up. + mpt::winstring prevDir = m_InstrLibPath.GetPath().AsNative(); + mpt::winstring::size_type pos = prevDir.find_last_of(_T("\\/"), prevDir.length() - 2); + if(pos != mpt::winstring::npos) + { + m_InstrLibHighlightPath = mpt::PathString::FromNative(prevDir.substr(pos + 1, prevDir.length() - pos - 2)); // Highlight previously accessed directory + prevDir = prevDir.substr(0, pos + 1); + } + m_previousPath = m_InstrLibHighlightPath; + dir = mpt::PathString::FromNative(prevDir); + } + } else + { + // Drives are formatted like "E:\", folders are just folder name without slash. + do + { + if(!dir.HasTrailingSlash()) + { + dir = m_InstrLibPath + dir; + dir.EnsureTrailingSlash(); + } + m_InstrLibHighlightPath = P_(".."); // Highlight first entry + + FolderScanner scan(dir, FolderScanner::kFilesAndDirectories); + mpt::PathString name; + if(scan.Next(name) && !scan.Next(name) && name.IsDirectory()) + { + // There is only one directory and nothing else in the path, + // so skip this directory and automatically descend further down into the tree. + dir = name; + dir.EnsureTrailingSlash(); + continue; + } + } while(false); + } + + if(dir.IsDirectory()) + { + m_SongFileName = P_(""); + delete m_SongFile; + m_SongFile = nullptr; + m_InstrLibPath = dir; + GetSampleBrowser()->m_InstrLibHighlightPath = m_InstrLibHighlightPath; + PostMessage(WM_COMMAND, ID_MODTREE_REFRESHINSTRLIB); + ok = true; + } + } + EndWaitCursor(); + + if(ok) + { + mpt::lock_guard<mpt::mutex> l(m_WatchDirMutex); + m_WatchDir = mpt::PathString(); + } else + { + Reporting::Error(MPT_CFORMAT("Unable to browse to \"{}\"")(dir), _T("Instrument Library")); + } +} + + +bool CModTree::GetDropInfo(DRAGONDROP &dropInfo, mpt::PathString &fullPath) +{ + const auto dragDoc = m_docInfo.find(m_dragDoc); + dropInfo.sndFile = dragDoc != m_docInfo.end() ? &dragDoc->second.modDoc.GetSoundFile() : nullptr; + dropInfo.dropType = DRAGONDROP_NOTHING; + dropInfo.dropItem = m_itemDrag.val1; + dropInfo.dropParam = 0; + switch(m_itemDrag.type) + { + case MODITEM_ORDER: + dropInfo.dropType = DRAGONDROP_ORDER; + break; + + case MODITEM_PATTERN: + dropInfo.dropType = DRAGONDROP_PATTERN; + break; + + case MODITEM_SAMPLE: + dropInfo.dropType = DRAGONDROP_SAMPLE; + break; + + case MODITEM_INSTRUMENT: + dropInfo.dropType = DRAGONDROP_INSTRUMENT; + break; + + case MODITEM_SEQUENCE: + case MODITEM_HDR_ORDERS: + dropInfo.dropType = DRAGONDROP_SEQUENCE; + break; + + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + if(!m_SongFileName.empty()) + { + const uint32 n = ConvertStrTo<uint32>(GetItemText(m_hItemDrag)); + dropInfo.dropType = (m_itemDrag.type == MODITEM_INSLIB_SAMPLE) ? DRAGONDROP_SAMPLE : DRAGONDROP_INSTRUMENT; + dropInfo.dropItem = n; + dropInfo.sndFile = m_SongFile; + dropInfo.dropParam = 0; + } else + { + fullPath = InsLibGetFullPath(m_hItemDrag); + dropInfo.dropType = DRAGONDROP_SOUNDFILE; + dropInfo.dropParam = reinterpret_cast<uintptr_t>(&fullPath); + } + break; + + case MODITEM_MIDIPERCUSSION: + dropInfo.dropItem |= 0x80; + [[fallthrough]]; + case MODITEM_MIDIINSTRUMENT: + { + MidiLibrary &midiLib = CTrackApp::GetMidiLibrary(); + if(!midiLib[dropInfo.dropItem & 0xFF].empty()) + { + fullPath = midiLib[dropInfo.dropItem & 0xFF]; + dropInfo.dropType = DRAGONDROP_MIDIINSTR; + dropInfo.dropParam = reinterpret_cast<uintptr_t>(&fullPath); + } + } + break; + + case MODITEM_INSLIB_SONG: + fullPath = InsLibGetFullPath(m_hItemDrag); + dropInfo.sndFile = nullptr; + dropInfo.dropType = DRAGONDROP_SONG; + dropInfo.dropItem = 0; + dropInfo.dropParam = reinterpret_cast<uintptr_t>(&fullPath); + break; + + case MODITEM_DLSBANK_INSTRUMENT: + { + dropInfo.dropType = DRAGONDROP_DLS; + dropInfo.dropItem = static_cast<uint32>(GetDLSBankIndexFromItem(m_hItemDrag)); // bank # + // Melodic: (Instrument) + // Drums: (0x80000000) | (Region << 16) | (Instrument) + dropInfo.dropParam = m_itemDrag.val1; + } + break; + } + return (dropInfo.dropType != DRAGONDROP_NOTHING); +} + + +bool CModTree::CanDrop(HTREEITEM hItem, bool doDrop) +{ + const ModItem modItemDrop = GetModItem(hItem); + const uint32 modItemDropID = modItemDrop.val1; + const uint32 modItemDragID = m_itemDrag.val1; + + const auto dragIter = m_docInfo.find(m_dragDoc); + const auto selIter = m_docInfo.find(m_selectedDoc); + const ModTreeDocInfo *infoDrag = dragIter != m_docInfo.end() ? &dragIter->second : nullptr; + const ModTreeDocInfo *infoDrop = selIter != m_docInfo.end() ? &selIter->second : nullptr; + CModDoc *modDoc = infoDrop ? &infoDrop->modDoc : nullptr; + CSoundFile *sndFile = modDoc ? &modDoc->GetSoundFile() : nullptr; + const bool sameModDoc = infoDrag && (modDoc == &infoDrag->modDoc); + const bool sameItem = modItemDrop == m_itemDrag && sameModDoc; + + switch(modItemDrop.type) + { + case MODITEM_ORDER: + case MODITEM_SEQUENCE: + if(m_itemDrag.type == MODITEM_ORDER && modDoc && sameModDoc) + { + // Drop an order somewhere + if(doDrop) + { + SEQUENCEINDEX seqFrom = static_cast<SEQUENCEINDEX>(m_itemDrag.val2), seqTo = static_cast<SEQUENCEINDEX>(modItemDrop.val2); + ORDERINDEX ordFrom = static_cast<ORDERINDEX>(m_itemDrag.val1), ordTo = static_cast<ORDERINDEX>(modItemDrop.val1); + if(modItemDrop.type == MODITEM_SEQUENCE) + { + // Drop on sequence -> attach + seqTo = static_cast<SEQUENCEINDEX>(modItemDrop.val1); + ordTo = sndFile->Order(seqTo).GetLengthTailTrimmed(); + } + + if(seqFrom != seqTo || ordFrom != ordTo) + { + if(modDoc->MoveOrder(ordFrom, ordTo, true, false, seqFrom, seqTo) == true) + { + modDoc->SetModified(); + } + } + } + return true; + } else if(modItemDrop.type == MODITEM_SEQUENCE && m_itemDrag.type == MODITEM_SEQUENCE && modDoc && sameModDoc) + { + if(doDrop && !sameItem) + { + // Rearrange sequences + const SEQUENCEINDEX from = static_cast<SEQUENCEINDEX>(modItemDragID), to = static_cast<SEQUENCEINDEX>(modItemDropID); + + std::vector<SEQUENCEINDEX> newOrder(sndFile->Order.GetNumSequences()); + std::iota(newOrder.begin(), newOrder.end(), SEQUENCEINDEX(0)); + newOrder.erase(newOrder.begin() + from); + newOrder.insert(newOrder.begin() + to, from); + + modDoc->ReArrangeSequences(newOrder); + + auto curSeq = sndFile->Order.GetCurrentSequenceIndex(); + if(curSeq == from) + curSeq = to; + else if(from > curSeq && to <= curSeq) + curSeq++; + else if(from < curSeq && to >= curSeq) + curSeq--; + sndFile->Order.SetSequence(curSeq); + + modDoc->UpdateAllViews(nullptr, SequenceHint(SEQUENCEINDEX_INVALID).Names().Data()); + modDoc->SetModified(); + + SelectItem(hItem); + } + return true; + } + break; + + case MODITEM_HDR_ORDERS: + // Drop your sequences here. + // At the moment, only dropping sequences into another module is possible and it doesn't copy the patterns themselves. + if((m_itemDrag.type == MODITEM_SEQUENCE || m_itemDrag.type == MODITEM_HDR_ORDERS) && modDoc && sndFile && infoDrag && !sameModDoc) + { + if(doDrop && infoDrag != nullptr) + { + // copy mod sequence over. + CSoundFile &dragSndFile = infoDrag->modDoc.GetSoundFile(); + const SEQUENCEINDEX origSeqId = static_cast<SEQUENCEINDEX>(modItemDragID); + const ModSequence &origSeq = dragSndFile.Order(origSeqId); + SEQUENCEINDEX sequenceHint = SEQUENCEINDEX_INVALID; + + if(sndFile->GetModSpecifications().sequencesMax > 1) + { + sequenceHint = sndFile->Order.AddSequence(); + } else + { + if(Reporting::Confirm(_T("Replace the current orderlist?"), _T("Sequence import")) == cnfNo) + return false; + sequenceHint = 0; + } + sndFile->Order().resize(std::min(sndFile->GetModSpecifications().ordersMax, origSeq.GetLength()), sndFile->Order.GetInvalidPatIndex()); + for(ORDERINDEX nOrd = 0; nOrd < std::min(sndFile->GetModSpecifications().ordersMax, origSeq.GetLengthTailTrimmed()); nOrd++) + { + PATTERNINDEX pat = dragSndFile.Order(origSeqId)[nOrd]; + // translate pattern index + if(pat == dragSndFile.Order.GetInvalidPatIndex()) + pat = sndFile->Order.GetInvalidPatIndex(); + else if(pat == dragSndFile.Order.GetIgnoreIndex() && sndFile->GetModSpecifications().hasIgnoreIndex) + pat = sndFile->Order.GetIgnoreIndex(); + else if(pat == dragSndFile.Order.GetIgnoreIndex() && !sndFile->GetModSpecifications().hasIgnoreIndex) + pat = sndFile->Order.GetInvalidPatIndex(); + else if(pat >= sndFile->GetModSpecifications().patternsMax) + pat = sndFile->Order.GetInvalidPatIndex(); + + sndFile->Order()[nOrd] = pat; + } + modDoc->UpdateAllViews(nullptr, SequenceHint(sequenceHint).Data()); + modDoc->SetModified(); + } + return true; + } + break; + + case MODITEM_SAMPLE: + if(m_itemDrag.type == MODITEM_SAMPLE && modDoc && infoDrag != nullptr) + { + if(doDrop) + { + if(sameModDoc) + { + // Reorder samples in a module + if(sameItem) + return true; + const SAMPLEINDEX from = static_cast<SAMPLEINDEX>(modItemDragID - 1), to = static_cast<SAMPLEINDEX>(modItemDropID - 1); + + std::vector<SAMPLEINDEX> newOrder(modDoc->GetNumSamples()); + std::iota(newOrder.begin(), newOrder.end(), SAMPLEINDEX(1)); + newOrder.erase(newOrder.begin() + from); + newOrder.insert(newOrder.begin() + to, from + 1); + + modDoc->ReArrangeSamples(newOrder); + } else + { + // Load sample into other module + sndFile->ReadSampleFromSong(static_cast<SAMPLEINDEX>(modItemDropID), infoDrag->modDoc.GetSoundFile(), static_cast<SAMPLEINDEX>(modItemDragID)); + } + modDoc->UpdateAllViews(nullptr, SampleHint().Info().Data().Names()); + modDoc->UpdateAllViews(nullptr, PatternHint().Data()); + modDoc->UpdateAllViews(nullptr, InstrumentHint().Info()); + modDoc->SetModified(); + SelectItem(hItem); + } + return true; + } + break; + + case MODITEM_INSTRUMENT: + if(m_itemDrag.type == MODITEM_INSTRUMENT && modDoc && infoDrag != nullptr) + { + if(doDrop) + { + if(sameModDoc) + { + // Reorder instruments in a module + if(sameItem) + return true; + const INSTRUMENTINDEX from = static_cast<INSTRUMENTINDEX>(modItemDragID - 1), to = static_cast<INSTRUMENTINDEX>(modItemDropID - 1); + + std::vector<INSTRUMENTINDEX> newOrder(modDoc->GetNumInstruments()); + std::iota(newOrder.begin(), newOrder.end(), INSTRUMENTINDEX(1)); + newOrder.erase(newOrder.begin() + from); + newOrder.insert(newOrder.begin() + to, from + 1); + + modDoc->ReArrangeInstruments(newOrder); + } else + { + // Load instrument into other module + sndFile->ReadInstrumentFromSong(static_cast<INSTRUMENTINDEX>(modItemDropID), infoDrag->modDoc.GetSoundFile(), static_cast<INSTRUMENTINDEX>(modItemDragID)); + } + modDoc->UpdateAllViews(nullptr, InstrumentHint().Info().Envelope().Names()); + modDoc->UpdateAllViews(nullptr, PatternHint().Data()); + modDoc->SetModified(); + SelectItem(hItem); + } + return true; + } + break; + + case MODITEM_MIDIINSTRUMENT: + case MODITEM_MIDIPERCUSSION: + if((m_itemDrag.type == MODITEM_INSLIB_SAMPLE) || (m_itemDrag.type == MODITEM_INSLIB_INSTRUMENT)) + { + if(doDrop) + { + mpt::PathString fullPath = InsLibGetFullPath(m_hItemDrag); + if(modItemDrop.type == MODITEM_MIDIINSTRUMENT) + SetMidiInstrument(modItemDropID, fullPath); + else + SetMidiPercussion(modItemDropID, fullPath); + } + return true; + } + break; + } + return false; +} + + +void CModTree::UpdatePlayPos(CModDoc &modDoc, Notification *pNotify) +{ + ModTreeDocInfo *info = GetDocumentInfoFromModDoc(modDoc); + if(info == nullptr) + return; + + const CSoundFile &sndFile = modDoc.GetSoundFile(); + ORDERINDEX nNewOrd = (pNotify) ? pNotify->order : ORDERINDEX_INVALID; + SEQUENCEINDEX nNewSeq = sndFile.Order.GetCurrentSequenceIndex(); + if(nNewOrd != info->ordSel || nNewSeq != info->seqSel) + { + // Remove bold state from old item + if(info->seqSel < info->tiOrders.size() && info->ordSel < info->tiOrders[info->seqSel].size()) + SetItemState(info->tiOrders[info->seqSel][info->ordSel], 0, TVIS_BOLD); + + info->ordSel = nNewOrd; + info->seqSel = nNewSeq; + if(info->seqSel < info->tiOrders.size() && info->ordSel < info->tiOrders[info->seqSel].size()) + SetItemState(info->tiOrders[info->seqSel][info->ordSel], TVIS_BOLD, TVIS_BOLD); + else + UpdateView(*info, SequenceHint().Data()); + } + + // Update sample / instrument playing status icons (will only detect instruments with samples, though) + + if((TrackerSettings::Instance().m_dwPatternSetup & PATTERN_LIVEUPDATETREE) == 0) + return; + // TODO: Is there a way to find out if the treeview is actually visible? + /*static int nUpdateCount = 0; + nUpdateCount++; + if(nUpdateCount < 5) return; // don't update too often + nUpdateCount = 0;*/ + + // check whether the lists are actually visible (don't waste resources) + const bool updateSamples = IsItemExpanded(info->hSamples), updateInstruments = IsItemExpanded(info->hInstruments); + + info->samplesPlaying.reset(); + info->instrumentsPlaying.reset(); + + if(!updateSamples && !updateInstruments) + return; + + for(const auto &chn : sndFile.m_PlayState.Chn) + { + if(chn.pCurrentSample != nullptr && chn.nLength != 0 && chn.IsSamplePlaying()) + { + if(updateSamples) + { + for(SAMPLEINDEX nSmp = sndFile.GetNumSamples(); nSmp >= 1; nSmp--) + { + if(chn.pModSample == &sndFile.GetSample(nSmp)) + { + info->samplesPlaying.set(nSmp); + break; + } + } + } + if(updateInstruments) + { + for(INSTRUMENTINDEX nIns = sndFile.GetNumInstruments(); nIns >= 1; nIns--) + { + if(chn.pModInstrument == sndFile.Instruments[nIns]) + { + info->instrumentsPlaying.set(nIns); + break; + } + } + } + } + } + // what should be updated? + if(updateSamples) + UpdateView(*info, SampleHint().Info()); + if(updateInstruments) + UpdateView(*info, InstrumentHint().Info()); +} + + + +///////////////////////////////////////////////////////////////////////////// +// CViewModTree message handlers + + +void CModTree::OnUpdate(CModDoc *pModDoc, UpdateHint hint, CObject *pHint) +{ + if(pHint == this) + return; + + for(auto &[doc, docInfo] : m_docInfo) + { + if(doc == pModDoc || !pModDoc) + { + UpdateView(docInfo, hint); + if(pModDoc) + break; + } + } +} + +void CModTree::OnItemExpanded(LPNMHDR pnmhdr, LRESULT *pResult) +{ + LPNMTREEVIEW pnm = (LPNMTREEVIEW)pnmhdr; + if((pnm->itemNew.iImage == IMAGE_FOLDER) || (pnm->itemNew.iImage == IMAGE_OPENFOLDER)) + { + int iNewImage = (pnm->itemNew.state & TVIS_EXPANDED) ? IMAGE_OPENFOLDER : IMAGE_FOLDER; + SetItemImage(pnm->itemNew.hItem, iNewImage, iNewImage); + } + if(pResult) + *pResult = TRUE; +} + + +void CModTree::OnBeginDrag(HTREEITEM hItem, bool bLeft, LRESULT *pResult) +{ + if(!(m_dwStatus & TREESTATUS_DRAGGING)) + { + bool bDrag = false; + + m_hDropWnd = NULL; + m_hItemDrag = hItem; + if(m_hItemDrag != NULL) + { + if(!ItemHasChildren(m_hItemDrag)) + SelectItem(m_hItemDrag); + } + m_itemDrag = GetModItem(m_hItemDrag); + m_dragDoc = m_selectedDoc; + switch(m_itemDrag.type) + { + case MODITEM_ORDER: + case MODITEM_PATTERN: + case MODITEM_SAMPLE: + case MODITEM_INSTRUMENT: + case MODITEM_SEQUENCE: + case MODITEM_MIDIINSTRUMENT: + case MODITEM_MIDIPERCUSSION: + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + case MODITEM_INSLIB_SONG: + bDrag = true; + break; + case MODITEM_HDR_ORDERS: + // can we drag an order header? (only in MPTM format and if there's only one sequence) + { + const CModDoc *pModDoc = m_docInfo.count(m_dragDoc) ? m_dragDoc : nullptr; + if(pModDoc && pModDoc->GetSoundFile().Order.GetNumSequences() == 1) + bDrag = true; + } + break; + default: + if(m_itemDrag.type == MODITEM_DLSBANK_INSTRUMENT) + bDrag = true; + } + if(bDrag) + { + m_dwStatus |= (bLeft) ? TREESTATUS_LDRAG : TREESTATUS_RDRAG; + m_hItemDrop = NULL; + SetCapture(); + } + } + if(pResult) + *pResult = TRUE; +} + + +void CModTree::OnBeginRDrag(LPNMHDR pnmhdr, LRESULT *pResult) +{ + if(pnmhdr) + { + LPNMTREEVIEW pnmtv = (LPNMTREEVIEW)pnmhdr; + OnBeginDrag(pnmtv->itemNew.hItem, false, pResult); + } +} + + +void CModTree::OnBeginLDrag(LPNMHDR pnmhdr, LRESULT *pResult) +{ + if(pnmhdr) + { + LPNMTREEVIEW pnmtv = (LPNMTREEVIEW)pnmhdr; + OnBeginDrag(pnmtv->itemNew.hItem, true, pResult); + } +} + + +void CModTree::OnItemDblClk(LPNMHDR, LRESULT *pResult) +{ + POINT pt; + GetCursorPos(&pt); + ScreenToClient(&pt); + HTREEITEM hItem = GetSelectedItem(); + if((hItem) && (hItem == HitTest(pt))) + { + ExecuteItem(hItem); + } + if(pResult) + *pResult = 0; +} + + +void CModTree::OnItemReturn(LPNMHDR, LRESULT *pResult) +{ + HTREEITEM hItem = GetSelectedItem(); + if(hItem) + ExecuteItem(hItem); + if(pResult) + *pResult = 0; +} + + +void CModTree::OnItemRightClick(LPNMHDR, LRESULT *pResult) +{ + CPoint pt, ptClient; + UINT flags = 0; + + GetCursorPos(&pt); + ptClient = pt; + ScreenToClient(&ptClient); + OnItemRightClick(HitTest(ptClient, &flags), pt); + if(pResult) + *pResult = 0; +} + + +void CModTree::OnItemRightClick(HTREEITEM hItem, CPoint pt) +{ + HMENU hMenu; + if(m_dwStatus & TREESTATUS_LDRAG) + { + if(ItemHasChildren(hItem)) + { + Expand(hItem, TVE_TOGGLE); + } else + { + m_hItemDrop = NULL; + m_hDropWnd = NULL; + OnEndDrag(TREESTATUS_DRAGGING); + } + } else + { + if(m_dwStatus & TREESTATUS_DRAGGING) + { + m_hItemDrop = NULL; + m_hDropWnd = NULL; + OnEndDrag(TREESTATUS_DRAGGING); + } + hMenu = ::CreatePopupMenu(); + if(hMenu) + { + const CModDoc *modDoc = GetDocumentFromItem(hItem); + const CSoundFile *sndFile = modDoc != nullptr ? &modDoc->GetSoundFile() : nullptr; + + UINT nDefault = 0; + BOOL bSep = FALSE; + + const ModItem modItem = GetModItem(hItem); + const uint32 modItemID = modItem.val1; + + SelectItem(hItem); + switch(modItem.type) + { + case MODITEM_HDR_SONG: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&View")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_CLOSE, _T("&Close")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name")); + break; + + case MODITEM_COMMENTS: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&View Comments")); + break; + + case MODITEM_ORDER: + case MODITEM_PATTERN: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Edit Pattern")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, + (modItem.type == MODITEM_ORDER) ? _T("&Delete from list") : _T("&Delete Pattern")); + if(modItem.type == MODITEM_PATTERN && sndFile && sndFile->GetModSpecifications().hasPatternNames) + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name Pattern")); + else if(modItem.type == MODITEM_ORDER) + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("&Set Pattern")); + break; + + case MODITEM_SEQUENCE: + if(sndFile) + { + bool isCurSeq = false; + if(sndFile->GetModSpecifications().sequencesMax > 1) + { + if(sndFile->Order((SEQUENCEINDEX)modItemID).GetLength() == 0) + { + nDefault = ID_MODTREE_SWITCHTO; + } + isCurSeq = (sndFile->Order.GetCurrentSequenceIndex() == (SEQUENCEINDEX)modItemID); + } + + if(!isCurSeq) + { + AppendMenu(hMenu, MF_STRING, ID_MODTREE_SWITCHTO, _T("&Switch to Seqeuence")); + } + AppendMenu(hMenu, MF_STRING | (sndFile->Order.GetNumSequences() < MAX_SEQUENCES ? 0 : MF_GRAYED), ID_MODTREE_INSERT, _T("&Insert Sequence")); + AppendMenu(hMenu, MF_STRING | (sndFile->Order.GetNumSequences() < MAX_SEQUENCES ? 0 : MF_GRAYED), ID_MODTREE_DUPLICATE , _T("D&uplicate Sequence")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete Sequence")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name Sequence")); + } + break; + + + case MODITEM_HDR_ORDERS: + if(sndFile && sndFile->GetModSpecifications().sequencesMax > 1) + { + AppendMenu(hMenu, MF_STRING, ID_MODTREE_INSERT, _T("&Insert Sequence")); + if(sndFile->Order.GetNumSequences() == 1) + { + // This is a sequence + AppendMenu(hMenu, MF_STRING, ID_MODTREE_DUPLICATE, _T("D&uplicate Sequence")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name Sequence")); + } + } + break; + + case MODITEM_SAMPLE: + { + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&View Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_INSERT, _T("&Insert Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_DUPLICATE, _T("D&uplicate Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name Sample")); + if ((modDoc) && (!modDoc->GetNumInstruments())) + { + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, (modDoc->IsSampleMuted((SAMPLEINDEX)modItemID) ? MF_CHECKED : 0) | MF_STRING, ID_MODTREE_MUTE, _T("&Mute Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_SOLO, _T("S&olo Sample")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_UNMUTEALL, _T("&Unmute all")); + } + if(sndFile != nullptr) + { + SAMPLEINDEX smpID = static_cast<SAMPLEINDEX>(modItem.val1); + const ModSample &sample = sndFile->GetSample(smpID); + const bool hasPath = sndFile->SampleHasPath(smpID); + const bool menuForThisSample = (sample.HasSampleData() && sndFile->GetType() == MOD_TYPE_MPT) || hasPath; + + bool anyPath = false, anyModified = false, anyMissing = false; + for(SAMPLEINDEX smp = 1; smp <= sndFile->GetNumSamples(); smp++) + { + if(sndFile->SampleHasPath(smp) && smp != smpID) + { + anyPath = true; + if(sndFile->GetSample(smp).HasSampleData() && sndFile->GetSample(smp).uFlags[SMP_MODIFIED]) + { + anyModified = true; + } + } + if(sndFile->IsExternalSampleMissing(smp)) + { + anyMissing = true; + } + if(anyPath && anyModified && anyMissing) break; + } + + if(menuForThisSample || anyPath || anyModified) + { + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + if(menuForThisSample) AppendMenu(hMenu, MF_STRING | ((sndFile->GetType() == MOD_TYPE_MPT || hasPath) ? 0 : MF_GRAYED), ID_MODTREE_SETPATH, _T("Set P&ath")); + if(menuForThisSample) AppendMenu(hMenu, MF_STRING | ((hasPath && sample.HasSampleData() && sample.uFlags[SMP_MODIFIED]) ? 0 : MF_GRAYED), ID_MODTREE_SAVEITEM, _T("&Save")); + if(anyModified) AppendMenu(hMenu, MF_STRING, ID_MODTREE_SAVEALL, _T("&Save All")); + if(menuForThisSample) AppendMenu(hMenu, MF_STRING | (hasPath ? 0 : MF_GRAYED), ID_MODTREE_RELOADITEM, _T("&Reload")); + if(anyPath) AppendMenu(hMenu, MF_STRING, ID_MODTREE_RELOADALL, _T("&Reload All")); + if(anyMissing) AppendMenu(hMenu, MF_STRING, ID_MODTREE_FINDMISSING, _T("&Find Missing Samples")); + } + } + } + break; + + case MODITEM_INSTRUMENT: + { + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&View Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_INSERT, _T("&Insert Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_DUPLICATE, _T("D&uplicate Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Re&name Instrument")); + if (modDoc) + { + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, (modDoc->IsInstrumentMuted((INSTRUMENTINDEX)modItemID) ? MF_CHECKED : 0) | MF_STRING, ID_MODTREE_MUTE, _T("&Mute Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_SOLO, _T("S&olo Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_UNMUTEALL, _T("&Unmute all")); + } + } + break; + + case MODITEM_HDR_EFFECTS: + if(sndFile && sndFile->m_loadedPlugins) + { + AppendMenu(hMenu, MF_STRING | (AllPluginsBypassed(*sndFile, false) ? MF_CHECKED : 0), ID_MODTREE_MUTE, _T("B&ypass All Plugins")); + if(HasEffectPlugins(*sndFile)) + AppendMenu(hMenu, MF_STRING | (AllPluginsBypassed(*sndFile, true) ? MF_CHECKED : 0), ID_MODTREE_MUTE_ONLY_EFFECTS, _T("Bypass All &Effects")); + } + break; + + case MODITEM_EFFECT: + { + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Edit")); + + if(modDoc != nullptr) + { + AppendMenu(hMenu, (modDoc->GetSoundFile().m_MixPlugins[modItemID].IsBypassed() ? MF_CHECKED : 0) | MF_STRING, ID_MODTREE_MUTE, _T("&Bypass")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete Plugin")); + } + } + break; + + case MODITEM_MIDIINSTRUMENT: + case MODITEM_MIDIPERCUSSION: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Map Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play Instrument")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Unmap Instrument")); + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + [[fallthrough]]; + case MODITEM_HDR_MIDILIB: + case MODITEM_HDR_MIDIGROUP: + AppendMenu(hMenu, MF_STRING, ID_IMPORT_MIDILIB, _T("&Import MIDI Library")); + AppendMenu(hMenu, MF_STRING, ID_EXPORT_MIDILIB, _T("E&xport MIDI Library")); + bSep = TRUE; + break; + + case MODITEM_HDR_INSTRUMENTLIB: + if(!IsSampleBrowser()) + break; + if(!m_SongFileName.empty()) + AppendMenu(hMenu, MF_STRING, ID_MODTREE_CLOSE, _T("&Close Song")); + [[fallthrough]]; + case MODITEM_INSLIB_FOLDER: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Browse...")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_RENAME, _T("Set &Path")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_OPENITEM, _T("&Open in Explorer")); + { + auto insDir = TrackerSettings::Instance().PathInstruments.GetDefaultDir(); + auto smpDir = TrackerSettings::Instance().PathSamples.GetDefaultDir(); + if(!insDir.empty() && insDir != m_InstrLibPath) + AppendMenu(hMenu, MF_STRING, ID_MODTREE_GOTO_INSDIR, _T("Go to &Instrument directory")); + if(!smpDir.empty() && smpDir != insDir && smpDir != m_InstrLibPath) + AppendMenu(hMenu, MF_STRING, ID_MODTREE_GOTO_SMPDIR, _T("Go to Sa&mple directory")); + } + break; + + case MODITEM_INSLIB_SONG: + nDefault = ID_MODTREE_EXECUTE; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Browse Song...")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_OPENITEM, _T("&Edit Song")); + AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete")); + break; + + case MODITEM_INSLIB_SAMPLE: + case MODITEM_INSLIB_INSTRUMENT: + nDefault = ID_MODTREE_PLAY; + if(!m_SongFileName.empty()) + { + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play")); + } else + { + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play File")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("&Delete")); + } + break; + + case MODITEM_DLSBANK_FOLDER: + nDefault = ID_SOUNDBANK_PROPERTIES; + AppendMenu(hMenu, MF_STRING, nDefault, _T("&Properties")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REMOVE, _T("Re&move this bank")); + [[fallthrough]]; + case MODITEM_NULL: + AppendMenu(hMenu, MF_STRING, ID_ADD_SOUNDBANK, _T("Add Sound &Bank...")); + bSep = TRUE; + break; + + case MODITEM_DLSBANK_INSTRUMENT: + nDefault = ID_MODTREE_PLAY; + AppendMenu(hMenu, MF_STRING, ID_MODTREE_PLAY, _T("&Play Instrument")); + break; + } + if (nDefault) SetMenuDefaultItem(hMenu, nDefault, FALSE); + if ((modItem.type == MODITEM_INSLIB_FOLDER) + || (modItem.type == MODITEM_INSLIB_SONG) + || (modItem.type == MODITEM_HDR_INSTRUMENTLIB)) + { + if ((bSep) || (nDefault)) AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, TrackerSettings::Instance().showDirsInSampleBrowser ? (MF_STRING|MF_CHECKED) : MF_STRING, ID_MODTREE_SHOWDIRS, _T("Show &Directories in Sample Browser")); + AppendMenu(hMenu, (m_showAllFiles) ? (MF_STRING|MF_CHECKED) : MF_STRING, ID_MODTREE_SHOWALLFILES, _T("Show &All Files")); + AppendMenu(hMenu, (m_showAllFiles) ? MF_STRING : (MF_STRING|MF_CHECKED), ID_MODTREE_SOUNDFILESONLY, _T("Show &Sound Files")); + bSep = TRUE; + } + if ((bSep) || (nDefault)) AppendMenu(hMenu, MF_SEPARATOR, NULL, _T("")); + AppendMenu(hMenu, MF_STRING, ID_MODTREE_REFRESH, _T("&Refresh")); + TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x + 4, pt.y, 0, m_hWnd, NULL); + DestroyMenu(hMenu); + } + } +} + + +void CModTree::OnItemLeftClick(LPNMHDR, LRESULT *pResult) +{ + if(!(m_dwStatus & TREESTATUS_RDRAG)) + { + POINT pt; + UINT flags = 0; + GetCursorPos(&pt); + ScreenToClient(&pt); + HTREEITEM hItem = HitTest(pt, &flags); + if(hItem != NULL) + { + const ModItem modItem = GetModItem(hItem); + const uint32 modItemID = modItem.val1; + + switch(modItem.type) + { + case MODITEM_INSLIB_FOLDER: + case MODITEM_INSLIB_SONG: + if(m_dwStatus & TREESTATUS_SINGLEEXPAND) + ExecuteItem(hItem); + break; + + case MODITEM_SAMPLE: + case MODITEM_INSTRUMENT: + { + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + CFrameWnd *pFrame = pMainFrm->GetActiveFrame(); + if (pFrame) + { + pFrame->SendMessage(WM_MOD_INSTRSELECTED, + (modItem.type == MODITEM_INSTRUMENT) ? TRUE : FALSE, + (LPARAM)modItemID); + } + } + break; + + case MODITEM_HDR_SONG: + ExecuteItem(hItem); + break; + } + } + } + if(pResult) + *pResult = 0; +} + + +void CModTree::OnEndDrag(DWORD dwMask) +{ + if(m_dwStatus & dwMask) + { + m_dwStatus &= ~dwMask; + if(!(m_dwStatus & TREESTATUS_DRAGGING)) + { + ReleaseCapture(); + SetCursor(CMainFrame::curArrow); + SelectDropTarget(nullptr); + if(m_hItemDrop != nullptr) + { + CanDrop(m_hItemDrop, true); + } else if(m_hDropWnd) + { + DRAGONDROP dropinfo; + mpt::PathString fullPath; + if(GetDropInfo(dropinfo, fullPath)) + { + if(dropinfo.dropType == DRAGONDROP_SONG) + { + theApp.OpenDocumentFile(fullPath.ToCString()); + } else + { + ::SendMessage(m_hDropWnd, WM_MOD_DRAGONDROPPING, TRUE, (LPARAM)&dropinfo); + } + } + } + } + } +} + + +void CModTree::OnLButtonUp(UINT nFlags, CPoint point) +{ + OnEndDrag(TREESTATUS_LDRAG); + CTreeCtrl::OnLButtonUp(nFlags, point); +} + + +void CModTree::OnRButtonUp(UINT nFlags, CPoint point) +{ + OnEndDrag(TREESTATUS_RDRAG); + CTreeCtrl::OnRButtonUp(nFlags, point); +} + + +void CModTree::OnXButtonUp(UINT nFlags, UINT nButton, CPoint point) +{ + bool isSampleBrowser = IsSampleBrowser(); + if(!isSampleBrowser) + { + // In the upper panel, only do folder navigation if the mouse cursor is somewhere below the "Instrument Library" item + CRect rect; + GetItemRect(m_hInsLib, rect, FALSE); + if(point.y > rect.top) + isSampleBrowser = true; + } + if(isSampleBrowser) + { + if(nButton == XBUTTON1) + { + InstrumentLibraryChDir(P_(".."), !m_SongFileName.empty()); + } else if(nButton == XBUTTON2) + { + const auto &previousPath = CMainFrame::GetMainFrame()->GetUpperTreeview()->m_previousPath; + InstrumentLibraryChDir(previousPath, (m_InstrLibPath + previousPath).IsFile()); + } + } + CTreeCtrl::OnXButtonUp(nFlags, nButton, point); +} + + +void CModTree::OnMouseMove(UINT nFlags, CPoint point) +{ + if(m_dwStatus & TREESTATUS_DRAGGING) + { + HTREEITEM hItem; + UINT flags = 0; + + // Bug? + if(!(nFlags & (MK_LBUTTON | MK_RBUTTON))) + { + m_itemDrag = ModItem(MODITEM_NULL); + m_hItemDrag = NULL; + OnEndDrag(TREESTATUS_DRAGGING); + return; + } + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + { + CRect rect; + GetClientRect(&rect); + if(rect.PtInRect(point)) + { + m_hDropWnd = m_hWnd; + bool bCanDrop = CanDrop(HitTest(point, &flags), false); + SetCursor((bCanDrop) ? CMainFrame::curDragging : CMainFrame::curNoDrop2); + } else + { + CPoint screenPt = point; + ClientToScreen(&screenPt); + HWND hwnd = ::WindowFromPoint(screenPt); + if(hwnd != m_hDropWnd) + { + bool canDrop = false; + m_hDropWnd = hwnd; + if(hwnd == m_hWnd) + { + canDrop = true; + } else if(hwnd != NULL) + { + DRAGONDROP dropinfo; + mpt::PathString fullPath; + if(GetDropInfo(dropinfo, fullPath)) + { + if(dropinfo.dropType == DRAGONDROP_SONG) + { + canDrop = true; + } else if(::SendMessage(hwnd, WM_MOD_DRAGONDROPPING, FALSE, (LPARAM)&dropinfo)) + { + canDrop = true; + } + } + } + SetCursor(canDrop ? CMainFrame::curDragging : CMainFrame::curNoDrop); + if(canDrop) + { + if(GetDropHilightItem() != m_hItemDrag) + { + SelectDropTarget(m_hItemDrag); + } + m_hItemDrop = NULL; + return; + } + } + } + if((point.x >= -1) && (point.x <= rect.right + GetSystemMetrics(SM_CXVSCROLL))) + { + if(point.y <= 0) + { + HTREEITEM hfirst = GetFirstVisibleItem(); + if(hfirst != NULL) + { + HTREEITEM hprev = GetPrevVisibleItem(hfirst); + if(hprev != NULL) + SelectSetFirstVisible(hprev); + } + } else if(point.y >= rect.bottom - 1) + { + hItem = HitTest(point, &flags); + HTREEITEM hNext = GetNextItem(hItem, TVGN_NEXTVISIBLE); + if(hNext != NULL) + { + EnsureVisible(hNext); + } + } + } + if((hItem = HitTest(point, &flags)) != NULL) + { + SelectDropTarget(hItem); + m_hItemDrop = hItem; + } + } + } + CTreeCtrl::OnMouseMove(nFlags, point); +} + + +void CModTree::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) +{ + switch(nChar) + { + case VK_DELETE: + DeleteTreeItem(GetSelectedItem()); + break; + } + CTreeCtrl::OnKeyDown(nChar, nRepCnt, nFlags); +} + + +void CModTree::OnRefreshTree() +{ + BeginWaitCursor(); + for(auto &doc : m_docInfo) + { + UpdateView(doc.second, UpdateHint().ModType()); + } + RefreshMidiLibrary(); + RefreshDlsBanks(); + RefreshInstrumentLibrary(); + EndWaitCursor(); +} + + +void CModTree::OnExecuteItem() +{ + ExecuteItem(GetSelectedItem()); +} + + +void CModTree::OnDeleteTreeItem() +{ + DeleteTreeItem(GetSelectedItem()); +} + + +void CModTree::OnPlayTreeItem() +{ + PlayItem(GetSelectedItem(), NOTE_MIDDLEC); +} + + +void CModTree::OnOpenTreeItem() +{ + OpenTreeItem(GetSelectedItem()); +} + + +void CModTree::OnMuteTreeItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + const uint32 modItemID = modItem.val1; + + ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); + if(info) + { + CModDoc &modDoc = info->modDoc; + if((modItem.type == MODITEM_SAMPLE) && !modDoc.GetNumInstruments()) + { + modDoc.MuteSample((SAMPLEINDEX)modItemID, !modDoc.IsSampleMuted((SAMPLEINDEX)modItemID)); + UpdateView(*info, SampleHint((SAMPLEINDEX)modItemID).Info().Names()); + } else if((modItem.type == MODITEM_INSTRUMENT) && modDoc.GetNumInstruments()) + { + modDoc.MuteInstrument((INSTRUMENTINDEX)modItemID, !modDoc.IsInstrumentMuted((INSTRUMENTINDEX)modItemID)); + UpdateView(*info, InstrumentHint((INSTRUMENTINDEX)modItemID).Info().Names()); + } else if(modItem.type == MODITEM_EFFECT) + { + IMixPlugin *pPlugin = modDoc.GetSoundFile().m_MixPlugins[modItemID].pMixPlugin; + if(pPlugin == nullptr) + return; + pPlugin->ToggleBypass(); + if(modDoc.GetSoundFile().GetModSpecifications().supportsPlugins) + modDoc.SetModified(); + //UpdateView(*info, PluginHint(static_cast<PLUGINDEX>(modItemID + 1))); + } else if(modItem.type == MODITEM_HDR_EFFECTS) + { + auto &sndFile = modDoc.GetSoundFile(); + BypassAllPlugins(sndFile, !AllPluginsBypassed(sndFile, false), false); + } + } +} + + +void CModTree::OnMuteOnlyEffects() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + + ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); + if(info) + { + CModDoc &modDoc = info->modDoc; + if(modItem.type == MODITEM_HDR_EFFECTS) + { + auto &sndFile = modDoc.GetSoundFile(); + BypassAllPlugins(sndFile, !AllPluginsBypassed(sndFile, true), true); + } + } +} + + +void CModTree::OnSoloTreeItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + const uint32 modItemID = modItem.val1; + + ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); + if(info) + { + CModDoc &modDoc = info->modDoc; + INSTRUMENTINDEX nInstruments = modDoc.GetNumInstruments(); + if((modItem.type == MODITEM_SAMPLE) && (!nInstruments)) + { + for(SAMPLEINDEX nSmp = 1; nSmp <= modDoc.GetNumSamples(); nSmp++) + { + modDoc.MuteSample(nSmp, nSmp != modItemID); + } + UpdateView(*info, SampleHint().Info().Names()); + } else if((modItem.type == MODITEM_INSTRUMENT) && (nInstruments)) + { + for(INSTRUMENTINDEX nIns = 1; nIns <= nInstruments; nIns++) + { + modDoc.MuteInstrument(nIns, nIns != modItemID); + } + UpdateView(*info, InstrumentHint().Info().Names()); + } + } +} + + +void CModTree::OnUnmuteAllTreeItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + + ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); + if(info) + { + CModDoc &modDoc = info->modDoc; + if((modItem.type == MODITEM_SAMPLE) || (modItem.type == MODITEM_INSTRUMENT)) + { + for(SAMPLEINDEX nSmp = 1; nSmp <= modDoc.GetNumSamples(); nSmp++) + { + modDoc.MuteSample(nSmp, false); + } + UpdateView(*info, SampleHint().Info().Names()); + for(INSTRUMENTINDEX nIns = 1; nIns <= modDoc.GetNumInstruments(); nIns++) + { + modDoc.MuteInstrument(nIns, false); + } + UpdateView(*info, InstrumentHint().Info().Names()); + } + } +} + + +bool CModTree::HasEffectPlugins(const CSoundFile &sndFile) +{ + for(const auto &plugin : sndFile.m_MixPlugins) + { + if(!plugin.pMixPlugin) + continue; + if(!plugin.pMixPlugin->IsInstrument()) + return true; + } + return false; + +} + + +bool CModTree::AllPluginsBypassed(const CSoundFile &sndFile, bool onlyEffects) +{ + for(const auto &plugin : sndFile.m_MixPlugins) + { + if(!plugin.pMixPlugin) + continue; + if(onlyEffects && plugin.pMixPlugin->IsInstrument()) + continue; + if(!plugin.IsBypassed()) + return false; + } + return true; +} + + +void CModTree::BypassAllPlugins(CSoundFile &sndFile, bool bypass, bool onlyEffects) +{ + bool modified = false; + for(auto &plugin : sndFile.m_MixPlugins) + { + if(!plugin.pMixPlugin) + continue; + if(onlyEffects && plugin.pMixPlugin->IsInstrument()) + continue; + if(plugin.IsBypassed() != bypass) + { + plugin.pMixPlugin->Bypass(bypass); + modified = true; + } + } + if(modified && sndFile.GetModSpecifications().supportsPlugins && sndFile.GetpModDoc()) + sndFile.GetpModDoc()->SetModified(); +} + + +// Helper function for generating an insert vector for samples/instruments/sequences +template <typename T> +static std::vector<T> GenerateInsertVector(size_t howMany, size_t insertPos, T insertId, T startId) +{ + std::vector<T> newOrder(howMany); + std::iota(newOrder.begin(), newOrder.end(), startId); + newOrder.insert(newOrder.begin() + insertPos, insertId); + return newOrder; +} + + +void CModTree::InsertOrDupItem(bool insert) +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + const uint32 modItemID = modItem.val1; + + ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); + if(info) + { + CModDoc &modDoc = info->modDoc; + CSoundFile &sndFile = modDoc.GetSoundFile(); + if(modItem.type == MODITEM_SEQUENCE || modItem.type == MODITEM_HDR_ORDERS) + { + // Duplicate / insert sequence + const SEQUENCEINDEX newIndex = (modItem.type == MODITEM_HDR_ORDERS) ? sndFile.Order.GetNumSequences() : static_cast<SEQUENCEINDEX>(modItemID + 1); + std::vector<SEQUENCEINDEX> newOrder = GenerateInsertVector<SEQUENCEINDEX>(sndFile.Order.GetNumSequences(), newIndex, static_cast<SEQUENCEINDEX>(insert ? SEQUENCEINDEX_INVALID : modItemID), 0); + if(modDoc.ReArrangeSequences(newOrder) != SEQUENCEINDEX_INVALID) + { + sndFile.Order.SetSequence(newIndex); + if(const auto name = sndFile.Order().GetName(); !insert && !name.empty()) + sndFile.Order().SetName(name + U_(" (Copy)")); + modDoc.UpdateAllViews(nullptr, SequenceHint(SEQUENCEINDEX_INVALID).Names().Data()); + modDoc.SetModified(); + } else + { + Reporting::Error("Maximum number of sequences reached."); + } + } else if(modItem.type == MODITEM_SAMPLE) + { + // Duplicate / insert sample + std::vector<SAMPLEINDEX> newOrder = GenerateInsertVector<SAMPLEINDEX>(sndFile.GetNumSamples(), modItemID, static_cast<SAMPLEINDEX>(insert ? 0 : modItemID), 1); + if(modDoc.ReArrangeSamples(newOrder) != SAMPLEINDEX_INVALID) + { + modDoc.SetModified(); + modDoc.UpdateAllViews(nullptr, SampleHint().Info().Data().Names()); + modDoc.UpdateAllViews(nullptr, PatternHint().Data()); + } else + { + Reporting::Error("Maximum number of samples reached."); + } + } else if(modItem.type == MODITEM_INSTRUMENT) + { + // Duplicate / insert instrument + std::vector<INSTRUMENTINDEX> newOrder = GenerateInsertVector<INSTRUMENTINDEX>(sndFile.GetNumInstruments(), modItemID, static_cast<INSTRUMENTINDEX>(insert ? 0 : modItemID), 1); + if(modDoc.ReArrangeInstruments(newOrder) != INSTRUMENTINDEX_INVALID) + { + modDoc.UpdateAllViews(NULL, InstrumentHint().Info().Envelope().Names()); + modDoc.UpdateAllViews(nullptr, PatternHint().Data()); + modDoc.SetModified(); + } else + { + Reporting::Error("Maximum number of instruments reached."); + } + } + } +} + + +void CModTree::OnSwitchToTreeItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + + CModDoc *pModDoc = GetDocumentFromItem(hItem); + if(pModDoc && (modItem.type == MODITEM_SEQUENCE)) + { + pModDoc->ActivateView(IDD_CONTROL_PATTERNS, uint32(modItem.val1 << SEQU_SHIFT) | SEQU_INDICATOR); + } +} + + +void CModTree::OnSetItemPath() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + CModDoc *pModDoc = GetDocumentFromItem(hItem); + + if(pModDoc && modItem.val1) + { + SAMPLEINDEX smpID = static_cast<SAMPLEINDEX>(modItem.val1); + const mpt::PathString path = pModDoc->GetSoundFile().GetSamplePath(smpID); + FileDialog dlg = OpenFileDialog() + .ExtensionFilter("All Samples|*.wav;*.flac|All files(*.*)|*.*||"); // Only show samples that we actually can save as well. + if(path.empty()) + dlg.WorkingDirectory(TrackerSettings::Instance().PathSamples.GetWorkingDir()); + else + dlg.DefaultFilename(path); + if(!dlg.Show()) + return; + TrackerSettings::Instance().PathSamples.SetWorkingDir(dlg.GetWorkingDirectory()); + + if(dlg.GetFirstFile() != pModDoc->GetSoundFile().GetSamplePath(smpID)) + { + pModDoc->GetSoundFile().SetSamplePath(smpID, dlg.GetFirstFile()); + pModDoc->SetModified(); + } + OnReloadItem(); + } +} + + +void CModTree::OnSaveItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + CModDoc *pModDoc = GetDocumentFromItem(hItem); + + if(pModDoc && modItem.val1) + { + SAMPLEINDEX smpID = static_cast<SAMPLEINDEX>(modItem.val1); + pModDoc->SaveSample(smpID); + if(pModDoc) + pModDoc->UpdateAllViews(NULL, SampleHint(smpID).Info()); + OnRefreshTree(); + } +} + + +void CModTree::OnSaveAll() +{ + CModDoc *pModDoc = GetDocumentFromItem(GetSelectedItem()); + if(pModDoc != nullptr) + { + pModDoc->SaveAllSamples(false); + if(pModDoc) + pModDoc->UpdateAllViews(nullptr, SampleHint().Info()); + OnRefreshTree(); + } +} + + +void CModTree::OnReloadItem() +{ + HTREEITEM hItem = GetSelectedItem(); + + const ModItem modItem = GetModItem(hItem); + CModDoc *pModDoc = GetDocumentFromItem(hItem); + + if(pModDoc && modItem.val1) + { + SAMPLEINDEX smpID = static_cast<SAMPLEINDEX>(modItem.val1); + CSoundFile &sndFile = pModDoc->GetSoundFile(); + pModDoc->GetSampleUndo().PrepareUndo(smpID, sundo_replace, "Replace"); + if(!sndFile.LoadExternalSample(smpID, sndFile.GetSamplePath(smpID))) + { + pModDoc->GetSampleUndo().RemoveLastUndoStep(smpID); + Reporting::Error(_T("Unable to load sample:\n") + sndFile.GetSamplePath(smpID).AsNative()); + } else + { + if(!sndFile.GetSample(smpID).uFlags[SMP_KEEPONDISK]) + { + pModDoc->SetModified(); + } + pModDoc->UpdateAllViews(NULL, SampleHint(smpID).Info().Data().Names()); + } + + OnRefreshTree(); + } +} + + +void CModTree::OnReloadAll() +{ + CModDoc *pModDoc = GetDocumentFromItem(GetSelectedItem()); + if(pModDoc != nullptr) + { + CSoundFile &sndFile = pModDoc->GetSoundFile(); + bool anyMissing = false; + for(SAMPLEINDEX smp = 1; smp <= sndFile.GetNumSamples(); smp++) + { + const mpt::PathString &path = sndFile.GetSamplePath(smp); + if(path.empty()) + continue; + + pModDoc->GetSampleUndo().PrepareUndo(smp, sundo_replace, "Replace"); + if(!sndFile.LoadExternalSample(smp, path)) + { + pModDoc->GetSampleUndo().RemoveLastUndoStep(smp); + anyMissing = true; + } else + { + if(!sndFile.GetSample(smp).uFlags[SMP_KEEPONDISK]) + { + pModDoc->SetModified(); + } + } + } + pModDoc->UpdateAllViews(nullptr, SampleHint().Info().Data().Names()); + OnRefreshTree(); + if(anyMissing) + { + OnFindMissing(); + } + } +} + + +// Find missing external samples +void CModTree::OnFindMissing() +{ + CModDoc *pModDoc = GetDocumentFromItem(GetSelectedItem()); + if(pModDoc == nullptr) + { + return; + } + MissingExternalSamplesDlg dlg(*pModDoc, CMainFrame::GetMainFrame()); + dlg.DoModal(); +} + + +void CModTree::OnAddDlsBank() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->OnAddDlsBank(); +} + + +void CModTree::OnImportMidiLib() +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + if(pMainFrm) + pMainFrm->OnImportMidiLib(); +} + + +void CModTree::OnExportMidiLib() +{ + FileDialog dlg = SaveFileDialog() + .DefaultExtension("ini") + .DefaultFilename("mptrack.ini") + .ExtensionFilter("Text and INI files (*.txt,*.ini)|*.txt;*.ini|" + "All Files (*.*)|*.*||"); + if(!dlg.Show()) return; + + CTrackApp::ExportMidiConfig(dlg.GetFirstFile()); +} + + +/////////////////////////////////////////////////////////////////////// +// Drop support + +DROPEFFECT CModTree::OnDragEnter(COleDataObject *, DWORD, CPoint) +{ + return DROPEFFECT_LINK; +} + + +DROPEFFECT CModTree::OnDragOver(COleDataObject *, DWORD, CPoint point) +{ + UINT flags = 0; + HTREEITEM hItem = HitTest(point, &flags); + + const ModItem modItem = GetModItem(hItem); + + switch(modItem.type) + { + case MODITEM_MIDIINSTRUMENT: + case MODITEM_MIDIPERCUSSION: + if(hItem != GetDropHilightItem()) + { + SelectDropTarget(hItem); + EnsureVisible(hItem); + } + m_hItemDrag = hItem; + m_itemDrag = modItem; + return DROPEFFECT_LINK; + // Folders: + case MODITEM_HDR_MIDILIB: + case MODITEM_HDR_MIDIGROUP: + EnsureVisible(GetChildItem(hItem)); + break; + } + return DROPEFFECT_NONE; +} + + +BOOL CModTree::OnDrop(COleDataObject *pDataObject, DROPEFFECT, CPoint) +{ + STGMEDIUM stgm; + HDROP hDropInfo; + UINT nFiles; + BOOL bOk = FALSE; + + if(!pDataObject) + return FALSE; + if(!pDataObject->GetData(CF_HDROP, &stgm)) + return FALSE; + if(stgm.tymed != TYMED_HGLOBAL) + return FALSE; + if(stgm.hGlobal == NULL) + return FALSE; + hDropInfo = (HDROP)stgm.hGlobal; + nFiles = DragQueryFile(hDropInfo, (UINT)-1, NULL, 0); + if(nFiles) + { + UINT size = ::DragQueryFile(hDropInfo, 0, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, _T('\0')); + if(DragQueryFile(hDropInfo, 0, fileName.data(), size)) + { + switch(m_itemDrag.type) + { + case MODITEM_MIDIINSTRUMENT: + bOk = SetMidiInstrument(m_itemDrag.val1, mpt::PathString::FromNative(fileName.data())); + break; + case MODITEM_MIDIPERCUSSION: + bOk = SetMidiPercussion(m_itemDrag.val1, mpt::PathString::FromNative(fileName.data())); + break; + } + } + } + // After dropping some file on the MIDI library, we will need to do this or you + // won't be able to select a new item until you started a new (internal) dragondrop. + if(m_hItemDrag) + { + OnBeginDrag(m_hItemDrag, true, nullptr); + OnEndDrag(TREESTATUS_DRAGGING); + } + m_itemDrag = ModItem(MODITEM_NULL); + m_hItemDrag = NULL; + ::DragFinish(hDropInfo); + return bOk; +} + + +void CModTree::OnRefreshInstrLib() +{ + BeginWaitCursor(); + RefreshInstrumentLibrary(); + EndWaitCursor(); +} + + +void CModTree::OnShowDirectories() +{ + TrackerSettings::Instance().showDirsInSampleBrowser = !TrackerSettings::Instance().showDirsInSampleBrowser; + OnRefreshInstrLib(); +} + + +void CModTree::OnShowAllFiles() +{ + if(!m_showAllFiles) + { + m_showAllFiles = true; + OnRefreshInstrLib(); + } +} + + +void CModTree::OnShowSoundFiles() +{ + if(m_showAllFiles) + { + m_showAllFiles = false; + OnRefreshInstrLib(); + } +} + + +void CModTree::OnGotoInstrumentDir() +{ + SetFullInstrumentLibraryPath(TrackerSettings::Instance().PathInstruments.GetDefaultDir()); +} + + +void CModTree::OnGotoSampleDir() +{ + SetFullInstrumentLibraryPath(TrackerSettings::Instance().PathSamples.GetDefaultDir()); +} + + +void CModTree::OnSoundBankProperties() +{ + const ModItem modItem = GetModItem(GetSelectedItem()); + if(modItem.type == MODITEM_DLSBANK_FOLDER + && modItem.val1 < CTrackApp::gpDLSBanks.size() && CTrackApp::gpDLSBanks[modItem.val1]) + { + CSoundBankProperties dlg(*CTrackApp::gpDLSBanks[modItem.val1], this); + dlg.DoModal(); + } +} + + +LRESULT CModTree::OnCustomKeyMsg(WPARAM wParam, LPARAM /*lParam*/) +{ + CMainFrame *pMainFrm = CMainFrame::GetMainFrame(); + + ModCommand::NOTE note = NOTE_NONE; + const bool start = wParam >= kcTreeViewStartNotes && wParam <= kcTreeViewEndNotes, + stop = (wParam >= kcTreeViewStartNoteStops && wParam <= kcTreeViewEndNoteStops) && !IsSampleBrowser(); + if(start || stop) + { + const ModItem modItem = GetModItem(GetSelectedItem()); + CModDoc *modDoc = m_docInfo.count(m_selectedDoc) ? m_selectedDoc : nullptr; + const int noteOffset = static_cast<int>(wParam - (start ? kcTreeViewStartNotes : kcTreeViewStartNoteStops)); + note = static_cast<ModCommand::NOTE>(Clamp(NOTE_MIN + pMainFrm->GetBaseOctave() * 12 + noteOffset, NOTE_MIN, NOTE_MAX)); + if(modDoc && modItem.type == MODITEM_INSTRUMENT) + note = modDoc->GetNoteWithBaseOctave(noteOffset, static_cast<INSTRUMENTINDEX>(modItem.val1)); + } else if(wParam == kcTreeViewStopPreview) + { + note = NOTE_NOTECUT; + } + if(note != NOTE_NONE) + { + if(stop) + note |= 0x80; + + if(PlayItem(GetSelectedItem(), note)) + return wParam; + else + return kcNull; + } + + return kcNull; +} + + +LRESULT CModTree::OnMidiMsg(WPARAM midiData_, LPARAM) +{ + uint32 midiData = static_cast<uint32>(midiData_); + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + ih->HandleMIDIMessage(kCtxViewTree, midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull; + + uint8 midiByte1 = MIDIEvents::GetDataByte1FromEvent(midiData); + int volume; + switch(MIDIEvents::GetTypeFromEvent(midiData)) + { + case MIDIEvents::evNoteOn: + volume = MIDIEvents::GetDataByte2FromEvent(midiData); + if(volume > 0) + { + PlayItem(GetSelectedItem(), midiByte1 + NOTE_MIN, Util::muldivr(volume, 256, 127)); + return 1; + } + [[fallthrough]]; + case MIDIEvents::evNoteOff: + PlayItem(GetSelectedItem(), NOTE_NOTECUT); + return 1; + } + return 0; +} + + +void CModTree::OnKillFocus(CWnd *pNewWnd) +{ + CTreeCtrl::OnKillFocus(pNewWnd); + CMainFrame::GetMainFrame()->m_bModTreeHasFocus = false; + if(pNewWnd != nullptr) + CMainFrame::GetMainFrame()->SetMidiRecordWnd(pNewWnd->m_hWnd); +} + + +void CModTree::OnSetFocus(CWnd *pOldWnd) +{ + CTreeCtrl::OnSetFocus(pOldWnd); + CMainFrame::GetMainFrame()->m_bModTreeHasFocus = true; + CMainFrame::GetMainFrame()->SetMidiRecordWnd(m_hWnd); +} + + +bool CModTree::IsItemExpanded(HTREEITEM hItem) +{ + // checks if a treeview item is expanded. + if(hItem == NULL) + return false; + TVITEM tvi; + tvi.mask = TVIF_HANDLE | TVIF_STATE; + tvi.state = 0; + tvi.stateMask = TVIS_EXPANDED; + tvi.hItem = hItem; + GetItem(&tvi); + return (tvi.state & TVIS_EXPANDED) != 0; +} + + +void CModTree::OnCloseItem() +{ + HTREEITEM hItem = GetSelectedItem(); + if(hItem == m_hInsLib && !m_SongFileName.empty()) + { + InstrumentLibraryChDir(P_(".."), true); + return; + } + CModDoc *pModDoc = GetDocumentFromItem(hItem); + if(pModDoc == nullptr) + return; + // Spam our message to the first available view + POSITION pos = pModDoc->GetFirstViewPosition(); + if(pos == nullptr) + return; + CView *pView = pModDoc->GetNextView(pos); + if(pView) + pView->PostMessage(WM_COMMAND, ID_FILE_CLOSE); +} + + +// Delete all children of a tree item +void CModTree::DeleteChildren(HTREEITEM hItem) +{ + if(hItem != nullptr) + { + HTREEITEM hChildItem; + while((hChildItem = GetChildItem(hItem)) != nullptr) + { + DeleteItem(hChildItem); + } + } +} + + +// Get the n-th child of a tree node +HTREEITEM CModTree::GetNthChildItem(HTREEITEM hItem, int index) const +{ + HTREEITEM hChildItem = nullptr; + if(hItem != nullptr && ItemHasChildren(hItem)) + { + hChildItem = GetChildItem(hItem); + while(index-- > 0) + { + hChildItem = GetNextSiblingItem(hChildItem); + } + } + return hChildItem; +} + + +// Gets the root parent of an item, i.e. if C is a child of B and B is a child of A, GetParentRootItem(C) returns A. +// A root item is considered to be its own parent, i.e. the returned value is only ever NULL if the input value was NULL. +HTREEITEM CModTree::GetParentRootItem(HTREEITEM hItem) const +{ + while(hItem != nullptr) + { + const HTREEITEM h = GetParentItem(hItem); + if(h == nullptr || h == hItem) + break; + hItem = h; + } + return hItem; +} + + +void CModTree::OnRenameItem() +{ + EditLabel(GetSelectedItem()); +} + + +// Editing sample, instrument, order, pattern, etc. labels +void CModTree::OnBeginLabelEdit(NMHDR *nmhdr, LRESULT *result) +{ + NMTVDISPINFO *info = reinterpret_cast<NMTVDISPINFO *>(nmhdr); + CEdit *editCtrl = GetEditControl(); + if(editCtrl == nullptr) + return; + + const ModItem modItem = GetModItem(info->item.hItem); + const CModDoc *modDoc = modItem.IsSongItem() ? GetDocumentFromItem(info->item.hItem) : nullptr; + + mpt::ustring text; + m_doLabelEdit = false; + + if(modDoc != nullptr) + { + const CSoundFile &sndFile = modDoc->GetSoundFile(); + const CModSpecifications &modSpecs = sndFile.GetModSpecifications(); + + switch(modItem.type) + { + case MODITEM_HDR_SONG: + text = mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.m_songName); + editCtrl->SetLimitText(modSpecs.modNameLengthMax); + m_doLabelEdit = true; + break; + + case MODITEM_ORDER: + { + PATTERNINDEX pat = sndFile.Order(static_cast<SEQUENCEINDEX>(modItem.val2)).at(static_cast<ORDERINDEX>(modItem.val1)); + if(pat == sndFile.Order.GetInvalidPatIndex()) + text = UL_("---"); + else if(pat == sndFile.Order.GetIgnoreIndex()) + text = UL_("+++"); + else + text = mpt::ufmt::val(pat); + m_doLabelEdit = true; + } + break; + + case MODITEM_HDR_ORDERS: + if(sndFile.Order.GetNumSequences() != 1 || sndFile.GetModSpecifications().sequencesMax <= 1) + { + break; + } + [[fallthrough]]; + case MODITEM_SEQUENCE: + if(modItem.val1 < sndFile.Order.GetNumSequences()) + { + text = sndFile.Order(static_cast<SEQUENCEINDEX>(modItem.val1)).GetName(); + m_doLabelEdit = true; + } + break; + + case MODITEM_PATTERN: + if(modItem.val1 < sndFile.Patterns.GetNumPatterns() && modSpecs.hasPatternNames) + { + text = mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.Patterns[modItem.val1].GetName()); + editCtrl->SetLimitText(MAX_PATTERNNAME - 1); + m_doLabelEdit = true; + } + break; + + case MODITEM_SAMPLE: + if(modItem.val1 <= sndFile.GetNumSamples()) + { + text = mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.m_szNames[modItem.val1]); + editCtrl->SetLimitText(modSpecs.sampleNameLengthMax); + m_doLabelEdit = true; + } + break; + + case MODITEM_INSTRUMENT: + if(modItem.val1 <= sndFile.GetNumInstruments() && sndFile.Instruments[modItem.val1] != nullptr) + { + text = mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.Instruments[modItem.val1]->name); + editCtrl->SetLimitText(modSpecs.instrNameLengthMax); + m_doLabelEdit = true; + } + break; + } + } else if(modItem.type == MODITEM_HDR_INSTRUMENTLIB) + { + text = m_InstrLibPath.ToUnicode(); + m_doLabelEdit = true; + } + + if(m_doLabelEdit) + { + CMainFrame::GetInputHandler()->Bypass(true); + editCtrl->SetWindowText(mpt::ToCString(text)); + } + *result = m_doLabelEdit ? FALSE : TRUE; +} + + +// End editing sample, instrument, order, pattern, etc. labels +void CModTree::OnEndLabelEdit(NMHDR *nmhdr, LRESULT *result) +{ + CMainFrame::GetInputHandler()->Bypass(false); + m_doLabelEdit = false; + + NMTVDISPINFO *info = reinterpret_cast<NMTVDISPINFO *>(nmhdr); + const ModItem modItem = GetModItem(info->item.hItem); + CModDoc *modDoc = modItem.IsSongItem() ? GetDocumentFromItem(info->item.hItem) : nullptr; + + *result = FALSE; + if(info->item.pszText == nullptr) + return; + + if(modDoc != nullptr) + { + CSoundFile &sndFile = modDoc->GetSoundFile(); + const CModSpecifications &modSpecs = sndFile.GetModSpecifications(); + + const mpt::ustring itemText = mpt::ToUnicode(CString(info->item.pszText)); + switch(modItem.type) + { + case MODITEM_HDR_SONG: + if(mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.m_songName) != itemText) + { + sndFile.m_songName = mpt::truncate(mpt::ToCharset(sndFile.GetCharsetInternal(), itemText), modSpecs.modNameLengthMax); + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, GeneralHint().General()); + } + break; + + case MODITEM_ORDER: + if(!itemText.empty()) + { + PATTERNINDEX pat = ConvertStrTo<PATTERNINDEX>(itemText); + bool valid = true; + if(itemText[0] == UC_('-')) + { + pat = sndFile.Order.GetInvalidPatIndex(); + } else if(itemText[0] == UC_('+')) + { + if(modSpecs.hasIgnoreIndex) + pat = sndFile.Order.GetIgnoreIndex(); + else + valid = false; + } else + { + valid = (pat < sndFile.Patterns.GetNumPatterns()); + } + PATTERNINDEX &target = sndFile.Order(static_cast<SEQUENCEINDEX>(modItem.val2))[static_cast<ORDERINDEX>(modItem.val1)]; + if(valid && pat != target) + { + target = pat; + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, SequenceHint().Data()); + } + } else + { + MessageBeep(MB_ICONWARNING); + } + break; + + case MODITEM_HDR_ORDERS: + case MODITEM_SEQUENCE: + if(modItem.val1 < sndFile.Order.GetNumSequences() && sndFile.Order(static_cast<SEQUENCEINDEX>(modItem.val1)).GetName() != itemText) + { + sndFile.Order(static_cast<SEQUENCEINDEX>(modItem.val1)).SetName(itemText); + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, SequenceHint(static_cast<SEQUENCEINDEX>(modItem.val1)).Names()); + } + break; + + case MODITEM_PATTERN: + if(modItem.val1 < sndFile.Patterns.GetNumPatterns() && modSpecs.hasPatternNames && mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.Patterns[modItem.val1].GetName()) != itemText) + { + sndFile.Patterns[modItem.val1].SetName(mpt::ToCharset(sndFile.GetCharsetInternal(), itemText)); + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, PatternHint(static_cast<PATTERNINDEX>(modItem.val1)).Data().Names()); + } + break; + + case MODITEM_SAMPLE: + if(modItem.val1 <= sndFile.GetNumSamples() && mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.m_szNames[modItem.val1]) != itemText) + { + sndFile.m_szNames[modItem.val1] = mpt::truncate(mpt::ToCharset(sndFile.GetCharsetInternal(), itemText), modSpecs.sampleNameLengthMax); + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, SampleHint(static_cast<SAMPLEINDEX>(modItem.val1)).Info().Names()); + } + break; + + case MODITEM_INSTRUMENT: + if(modItem.val1 <= sndFile.GetNumInstruments() && sndFile.Instruments[modItem.val1] != nullptr && mpt::ToUnicode(sndFile.GetCharsetInternal(), sndFile.Instruments[modItem.val1]->name) != itemText) + { + sndFile.Instruments[modItem.val1]->name = mpt::truncate(mpt::ToCharset(sndFile.GetCharsetInternal(), itemText), modSpecs.instrNameLengthMax); + modDoc->SetModified(); + modDoc->UpdateAllViews(nullptr, InstrumentHint(static_cast<INSTRUMENTINDEX>(modItem.val1)).Info().Names()); + } + break; + } + } else if(modItem.type == MODITEM_HDR_INSTRUMENTLIB) + { + SetFullInstrumentLibraryPath(mpt::PathString::FromNative(info->item.pszText)); + } +} + + +// Drop files from Windows +void CModTree::OnDropFiles(HDROP hDropInfo) +{ + bool refreshDLS = false; + const UINT nFiles = ::DragQueryFile(hDropInfo, (UINT)-1, NULL, 0); + CMainFrame::GetMainFrame()->SetForegroundWindow(); + for(UINT f = 0; f < nFiles; f++) + { + UINT size = ::DragQueryFile(hDropInfo, f, nullptr, 0) + 1; + std::vector<TCHAR> fileName(size, _T('\0')); + if(::DragQueryFile(hDropInfo, f, fileName.data(), size)) + { + mpt::PathString file(mpt::PathString::FromNative(fileName.data())); + if(IsSampleBrowser()) + { + // Set sample browser location to this directory or file + SetFullInstrumentLibraryPath(file); + break; + } else + { + if(CTrackApp::AddDLSBank(file)) + { + refreshDLS = true; + } else + { + // Pass message on + theApp.OpenDocumentFile(file.ToCString()); + } + } + } + } + if(refreshDLS) + { + RefreshDlsBanks(); + } + ::DragFinish(hDropInfo); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.h b/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.h new file mode 100644 index 00000000..2a730cab --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/View_tre.h @@ -0,0 +1,334 @@ +/* + * view_tre.h + * ---------- + * Purpose: Tree view for managing open songs, sound files, file browser, ... + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <vector> +#include <bitset> + +OPENMPT_NAMESPACE_BEGIN + +class CModDoc; +class CModTree; +class CSoundFile; +class CDLSBank; + +struct ModTreeDocInfo +{ + // Tree state variables + std::vector<std::vector<HTREEITEM>> tiOrders; + std::vector<HTREEITEM> tiSequences, tiPatterns; + CModDoc &modDoc; + HTREEITEM hSong = nullptr, hPatterns = nullptr, hSamples = nullptr, hInstruments = nullptr, hComments = nullptr, hOrders = nullptr, hEffects = nullptr; + + // Module information + ORDERINDEX ordSel = ORDERINDEX_INVALID; + SEQUENCEINDEX seqSel = SEQUENCEINDEX_INVALID; + + std::bitset<MAX_SAMPLES> samplesPlaying; + std::bitset<MAX_INSTRUMENTS> instrumentsPlaying; + + ModTreeDocInfo(CModDoc &modDoc); +}; + + +class CModTreeDropTarget: public COleDropTarget +{ +protected: + CModTree *m_pModTree; + +public: + CModTreeDropTarget() { m_pModTree = nullptr; } + BOOL Register(CModTree *pWnd); + +public: + DROPEFFECT OnDragEnter(CWnd *pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) override; + DROPEFFECT OnDragOver(CWnd *pWnd, COleDataObject* pDataObject, DWORD dwKeyState, CPoint point) override; + BOOL OnDrop(CWnd *pWnd, COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point) override; +}; + + +class CModTree: public CTreeCtrl +{ +protected: + enum TreeStatus + { + TREESTATUS_RDRAG = 0x01, + TREESTATUS_LDRAG = 0x02, + TREESTATUS_SINGLEEXPAND = 0x04, + TREESTATUS_DRAGGING = (TREESTATUS_RDRAG | TREESTATUS_LDRAG) + }; + + enum ModItemType : uint8 + { + MODITEM_NULL = 0, + + MODITEM_BEGIN_SONGITEMS, + MODITEM_ORDER = MODITEM_BEGIN_SONGITEMS, + MODITEM_PATTERN, + MODITEM_SAMPLE, + MODITEM_INSTRUMENT, + MODITEM_COMMENTS, + MODITEM_EFFECT, + MODITEM_SEQUENCE, + MODITEM_HDR_SONG, + MODITEM_HDR_ORDERS, + MODITEM_HDR_PATTERNS, + MODITEM_HDR_SAMPLES, + MODITEM_HDR_INSTRUMENTS, + MODITEM_HDR_EFFECTS, + MODITEM_END_SONGITEMS = MODITEM_HDR_EFFECTS, + + MODITEM_HDR_INSTRUMENTLIB, + MODITEM_HDR_MIDILIB, + MODITEM_HDR_MIDIGROUP, + MODITEM_MIDIINSTRUMENT, + MODITEM_MIDIPERCUSSION, + MODITEM_INSLIB_FOLDER, + MODITEM_INSLIB_SAMPLE, + MODITEM_INSLIB_INSTRUMENT, + MODITEM_INSLIB_SONG, + MODITEM_DLSBANK_FOLDER, + MODITEM_DLSBANK_INSTRUMENT, + }; + + // Bit mask magic + enum + { + MIDILIB_SHIFT = 16, + MIDILIB_MASK = (1 << MIDILIB_SHIFT) - 1, + + // Must be consistent with CCtrlPatterns::OnActivatePage + SEQU_SHIFT = 16, + SEQU_MASK = (1 << SEQU_SHIFT) - 1, + SEQU_INDICATOR = 0x80000000, + + // Soundbank instrument identification (must be consistent with CViewInstrument::OnDragonDrop / CViewSample::OnDragonDrop) + DLS_TYPEPERC = 0x80000000, + DLS_INSTRMASK = 0x0000FFFF, + DLS_REGIONMASK = 0x7FFF0000, // Drum region + DLS_REGIONSHIFT = 16, + + DLS_DRUM_FOLDER_LPARAM = 0x12345678, + }; + static_assert((ORDERINDEX_INVALID & SEQU_MASK) == ORDERINDEX_INVALID, "ORDERINDEX doesn't fit in GetItemData() parameter"); + static_assert((ORDERINDEX_MAX & SEQU_MASK) == ORDERINDEX_MAX, "ORDERINDEX doesn't fit in GetItemData() parameter"); + static_assert((((SEQUENCEINDEX_INVALID << SEQU_SHIFT) & ~SEQU_INDICATOR) >> SEQU_SHIFT) == SEQUENCEINDEX_INVALID, "SEQUENCEINDEX doesn't fit in GetItemData() parameter"); + + struct ModItem + { + uint32 val1; + uint16 val2; + ModItemType type; + + ModItem(ModItemType t = MODITEM_NULL, uint32 v1 = 0, uint16 v2 = 0) : val1(v1), val2(v2), type(t) { } + bool IsSongItem() const noexcept { return type >= MODITEM_BEGIN_SONGITEMS && type <= MODITEM_END_SONGITEMS; } + bool operator==(const ModItem &other) const noexcept { return val1 == other.val1 && val2 == other.val2 && type == other.type; } + bool operator!=(const ModItem &other) const noexcept { return !(*this == other); } + }; + + struct DlsItem : public ModItem + { + explicit DlsItem(uint16 instr) : ModItem(MODITEM_DLSBANK_INSTRUMENT, instr, 0) { } + DlsItem(uint16 instr, uint16 region) : ModItem(MODITEM_DLSBANK_INSTRUMENT, (instr & DLS_INSTRMASK) | ((region << DLS_REGIONSHIFT) & DLS_REGIONMASK) | DLS_TYPEPERC, 0) { } + + uint32 GetRegion() const noexcept { return (val1 & DLS_REGIONMASK) >> DLS_REGIONSHIFT; } + uint32 GetInstr() const noexcept { return (val1 & DLS_INSTRMASK); } + bool IsPercussion() const noexcept { return ((val1 & DLS_TYPEPERC) == DLS_TYPEPERC); } + bool IsMelodic() const noexcept { return !IsPercussion(); } + + static ModItem FromLPARAM(uint32 lparam) { return ModItem{MODITEM_DLSBANK_INSTRUMENT, lparam, 0}; } + static LPARAM ToLPARAM(uint16 instr, uint16 region, bool isPerc) { return (instr & DLS_INSTRMASK) | ((region << DLS_REGIONSHIFT) & DLS_REGIONMASK) | (isPerc ? DLS_TYPEPERC : 0); } + }; + + static CSoundFile *m_SongFile; // For browsing samples and instruments inside modules on disk + CModTreeDropTarget m_DropTarget; + CModTree *m_pDataTree = nullptr; // Pointer to instrument browser (lower part of tree view) - if it's a nullptr, this object is the instrument browser itself. + HWND m_hDropWnd = nullptr; + mpt::mutex m_WatchDirMutex; + HANDLE m_hSwitchWatchDir = nullptr; + mpt::PathString m_WatchDir; + HANDLE m_hWatchDirKillThread = nullptr; + std::thread m_WatchDirThread; + ModItem m_itemDrag; + DWORD m_dwStatus = 0; + CModDoc *m_selectedDoc = nullptr, *m_dragDoc = nullptr; + HTREEITEM m_hItemDrag = nullptr, m_hItemDrop = nullptr; + HTREEITEM m_hInsLib = nullptr, m_hMidiLib = nullptr; + HTREEITEM m_tiMidi[128]; + HTREEITEM m_tiPerc[128]; + std::vector<HTREEITEM> m_tiDLS; + std::map<const CModDoc *, ModTreeDocInfo> m_docInfo; + + std::unique_ptr<CDLSBank> m_cachedBank; + mpt::PathString m_cachedBankName; + + CString m_compareStrL, m_compareStrR; // Cache for ModTreeInsLibCompareNamesProc to avoid constant re-allocations +#if (_WIN32_WINNT >= _WIN32_WINNT_WIN7) + DWORD m_stringCompareFlags = NORM_IGNORECASE | NORM_IGNOREWIDTH | SORT_DIGITSASNUMBERS; +#else + DWORD m_stringCompareFlags = NORM_IGNORECASE | NORM_IGNOREWIDTH; +#endif + + // Instrument library + mpt::PathString m_InstrLibPath; // Current path to be explored + mpt::PathString m_InstrLibHighlightPath; // Folder to highlight in browser after a refresh + mpt::PathString m_SongFileName; // Name of open module, without path (== m_InstrLibPath). + mpt::PathString m_previousPath; // The folder from which we came from when navigating one folder up + std::vector<const char*> m_modExtensions; // cached in order to avoid querying too often when changing browsed folder + std::vector<mpt::PathString> m_MediaFoundationExtensions; // cached in order to avoid querying too often when changing browsed folder + bool m_showAllFiles = false; + + bool m_doLabelEdit = false; + +public: + CModTree(CModTree *pDataTree); + ~CModTree(); + +// Attributes +public: + void Init(); + bool InsLibSetFullPath(const mpt::PathString &libPath, const mpt::PathString &songFolder); + mpt::PathString InsLibGetFullPath(HTREEITEM hItem) const; + bool SetSoundFile(FileReader &file); + void RefreshMidiLibrary(); + void RefreshDlsBanks(); + void RefreshInstrumentLibrary(); + void EmptyInstrumentLibrary(); + void FillInstrumentLibrary(const TCHAR *selectedItem = nullptr); + void MonitorInstrumentLibrary(); + ModItem GetModItem(HTREEITEM hItem); + BOOL SetMidiInstrument(UINT nIns, const mpt::PathString &fileName); + BOOL SetMidiPercussion(UINT nPerc, const mpt::PathString &fileName); + bool ExecuteItem(HTREEITEM hItem); + void DeleteTreeItem(HTREEITEM hItem); + static void PlayDLSItem(const CDLSBank &dlsBank, const DlsItem &item, ModCommand::NOTE note); + BOOL PlayItem(HTREEITEM hItem, ModCommand::NOTE nParam, int volume = -1); + BOOL OpenTreeItem(HTREEITEM hItem); + BOOL OpenMidiInstrument(DWORD dwItem); + void SetFullInstrumentLibraryPath(mpt::PathString path); + void InstrumentLibraryChDir(mpt::PathString dir, bool isSong); + bool GetDropInfo(DRAGONDROP &dropInfo, mpt::PathString &fullPath); + void OnOptionsChanged(); + void AddDocument(CModDoc &modDoc); + void RemoveDocument(const CModDoc &modDoc); + void UpdateView(ModTreeDocInfo &info, UpdateHint hint); + void OnUpdate(CModDoc *pModDoc, UpdateHint hint, CObject *pHint); + bool CanDrop(HTREEITEM hItem, bool bDoDrop); + void UpdatePlayPos(CModDoc &modDoc, Notification *pNotify); + bool IsItemExpanded(HTREEITEM hItem); + void DeleteChildren(HTREEITEM hItem); + HTREEITEM GetNthChildItem(HTREEITEM hItem, int index) const; + HTREEITEM GetParentRootItem(HTREEITEM hItem) const; + + bool IsSampleBrowser() const { return m_pDataTree == nullptr; } + CModTree *GetSampleBrowser() { return IsSampleBrowser() ? this : m_pDataTree; } + +// Overrides + // ClassWizard generated virtual function overrides + //{{AFX_VIRTUAL(CModTree) +public: + BOOL PreTranslateMessage(MSG* pMsg) override; + //}}AFX_VIRTUAL + +// Drag & Drop operations +public: + DROPEFFECT OnDragEnter(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point); + DROPEFFECT OnDragOver(COleDataObject* pDataObject, DWORD dwKeyState, CPoint point); + BOOL OnDrop(COleDataObject* pDataObject, DROPEFFECT dropEffect, CPoint point); + +protected: + int ModTreeInsLibCompareNamesGetItem(HTREEITEM item, CString &resultStr); + static int CALLBACK ModTreeInsLibCompareNamesProc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); + static int CALLBACK ModTreeInsLibCompareProc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); + static int CALLBACK ModTreeDrumCompareProc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort); + HTREEITEM InsertInsLibItem(const TCHAR *name, int image, const TCHAR *selectIfMatch); + ModTreeDocInfo *GetDocumentInfoFromItem(HTREEITEM hItem); + CModDoc *GetDocumentFromItem(HTREEITEM hItem) { ModTreeDocInfo *info = GetDocumentInfoFromItem(hItem); return info ? &info->modDoc : nullptr; } + ModTreeDocInfo *GetDocumentInfoFromModDoc(CModDoc &modDoc); + + size_t GetDLSBankIndexFromItem(HTREEITEM hItem) const; + CDLSBank *GetDLSBankFromItem(HTREEITEM hItem) const; + + void InsertOrDupItem(bool insert); + void OnItemRightClick(HTREEITEM hItem, CPoint pt); + + static bool HasEffectPlugins(const CSoundFile &sndFile); + static bool AllPluginsBypassed(const CSoundFile &sndFile, bool onlyEffects); + static void BypassAllPlugins(CSoundFile &sndFile, bool bypass, bool onlyEffects); + +// Generated message map functions +protected: + //{{AFX_MSG(CModTree) + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + afx_msg void OnRButtonUp(UINT nFlags, CPoint point); + afx_msg void OnXButtonUp(UINT nFlags, UINT nButton, CPoint point); + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnBeginDrag(HTREEITEM, bool bLeft, LRESULT *pResult); + afx_msg void OnBeginLDrag(LPNMHDR, LRESULT *pResult); + afx_msg void OnBeginRDrag(LPNMHDR, LRESULT *pResult); + afx_msg void OnEndDrag(DWORD dwMask); + afx_msg void OnItemDblClk(LPNMHDR phdr, LRESULT *pResult); + afx_msg void OnItemReturn(LPNMHDR, LRESULT *pResult); + afx_msg void OnItemLeftClick(LPNMHDR pNMHDR, LRESULT *pResult); + afx_msg void OnItemRightClick(LPNMHDR, LRESULT *pResult); + afx_msg void OnItemExpanded(LPNMHDR pnmhdr, LRESULT *pResult); + afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags); + afx_msg void OnRefreshTree(); + afx_msg void OnExecuteItem(); + afx_msg void OnPlayTreeItem(); + afx_msg void OnDeleteTreeItem(); + afx_msg void OnOpenTreeItem(); + afx_msg void OnMuteTreeItem(); + afx_msg void OnMuteOnlyEffects(); + afx_msg void OnSoloTreeItem(); + afx_msg void OnUnmuteAllTreeItem(); + afx_msg void OnDuplicateTreeItem() { InsertOrDupItem(false); } + afx_msg void OnInsertTreeItem() { InsertOrDupItem(true); } + afx_msg void OnSwitchToTreeItem(); // hack for sequence items to avoid double-click action + afx_msg void OnCloseItem(); + afx_msg void OnRenameItem(); + afx_msg void OnBeginLabelEdit(NMHDR *nmhdr, LRESULT *result); + afx_msg void OnEndLabelEdit(NMHDR *nmhdr, LRESULT *result); + afx_msg void OnDropFiles(HDROP hDropInfo); + + afx_msg void OnSetItemPath(); + afx_msg void OnSaveItem(); + afx_msg void OnSaveAll(); + afx_msg void OnReloadItem(); + afx_msg void OnReloadAll(); + afx_msg void OnFindMissing(); + + afx_msg void OnAddDlsBank(); + afx_msg void OnImportMidiLib(); + afx_msg void OnExportMidiLib(); + afx_msg void OnSoundBankProperties(); + afx_msg void OnRefreshInstrLib(); + afx_msg void OnShowDirectories(); + afx_msg void OnShowAllFiles(); + afx_msg void OnShowSoundFiles(); + + afx_msg void OnGotoInstrumentDir(); + afx_msg void OnGotoSampleDir(); + + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + LRESULT OnMidiMsg(WPARAM midiData, LPARAM); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +public: + afx_msg void OnKillFocus(CWnd *pNewWnd); + afx_msg void OnSetFocus(CWnd *pOldWnd); +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.cpp new file mode 100644 index 00000000..44e94e6e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.cpp @@ -0,0 +1,307 @@ +/* + * VstPresets.cpp + * -------------- + * Purpose: Plugin preset / bank handling + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifndef NO_PLUGINS +#include "../soundlib/Sndfile.h" +#include "../soundlib/plugins/PlugInterface.h" +#ifdef MPT_WITH_VST +#include "Vstplug.h" +#endif // MPT_WITH_VST +#include "VstPresets.h" +#include "../common/FileReader.h" +#include <ostream> +#include "mpt/io/base.hpp" +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + +// This part of the header is identical for both presets and banks. +struct ChunkHeader +{ + char chunkMagic[4]; // 'CcnK' + int32be byteSize; // Size of this chunk, excluding magic + byteSize + + char fxMagic[4]; // 'FxBk' (regular) or 'FBCh' (opaque chunk) + int32be version; // Format version (1 or 2) + int32be fxID; // Plugin unique ID + int32be fxVersion; // Plugin version +}; + +MPT_BINARY_STRUCT(ChunkHeader, 24) + + +VSTPresets::ErrorCode VSTPresets::LoadFile(FileReader &file, IMixPlugin &plugin) +{ + const bool firstChunk = file.GetPosition() == 0; + ChunkHeader header; + if(!file.ReadStruct(header) || memcmp(header.chunkMagic, "CcnK", 4)) + { + return invalidFile; + } + if(header.fxID != plugin.GetUID()) + { + return wrongPlugin; + } +#ifdef MPT_WITH_VST + CVstPlugin *vstPlug = dynamic_cast<CVstPlugin *>(&plugin); +#endif // MPT_WITH_VST + + if(!memcmp(header.fxMagic, "FxCk", 4) || !memcmp(header.fxMagic, "FPCh", 4)) + { + // Program + PlugParamIndex numParams = file.ReadUint32BE(); + +#ifdef MPT_WITH_VST + if(vstPlug != nullptr) + { + Vst::VstPatchChunkInfo info; + info.version = 1; + info.pluginUniqueID = header.fxID; + info.pluginVersion = header.fxVersion; + info.numElements = numParams; + MemsetZero(info.reserved); + vstPlug->Dispatch(Vst::effBeginLoadProgram, 0, 0, &info, 0.0f); + } +#endif // MPT_WITH_VST + plugin.BeginSetProgram(); + + std::string prgName; + file.ReadString<mpt::String::maybeNullTerminated>(prgName, 28); + plugin.SetCurrentProgramName(mpt::ToCString(mpt::Charset::Locale, prgName)); + + if(!memcmp(header.fxMagic, "FxCk", 4)) + { + if(plugin.GetNumParameters() != numParams) + { + return wrongParameters; + } + for(PlugParamIndex p = 0; p < numParams; p++) + { + const auto value = file.ReadFloatBE(); + plugin.SetParameter(p, std::isfinite(value) ? value : 0.0f); + } + } else + { + uint32 chunkSize = file.ReadUint32BE(); + // Some nasty plugins (e.g. SmartElectronix Ambience) write to our memory block. + // Directly writing to a memory-mapped file block results in a crash... + std::byte *chunkData = new (std::nothrow) std::byte[chunkSize]; + if(chunkData) + { + file.ReadRaw(mpt::span(chunkData, chunkSize)); + plugin.SetChunk(mpt::as_span(chunkData, chunkSize), false); + delete[] chunkData; + } else + { + return outOfMemory; + } + } + plugin.EndSetProgram(); + } else if((!memcmp(header.fxMagic, "FxBk", 4) || !memcmp(header.fxMagic, "FBCh", 4)) && firstChunk) + { + // Bank - only read if it's the first chunk in the file, not if it's a sub chunk. + uint32 numProgs = file.ReadUint32BE(); + uint32 currentProgram = file.ReadUint32BE(); + file.Skip(124); + +#ifdef MPT_WITH_VST + if(vstPlug != nullptr) + { + Vst::VstPatchChunkInfo info; + info.version = 1; + info.pluginUniqueID = header.fxID; + info.pluginVersion = header.fxVersion; + info.numElements = numProgs; + MemsetZero(info.reserved); + vstPlug->Dispatch(Vst::effBeginLoadBank, 0, 0, &info, 0.0f); + } +#endif // MPT_WITH_VST + + if(!memcmp(header.fxMagic, "FxBk", 4)) + { + int32 oldCurrentProgram = plugin.GetCurrentProgram(); + for(uint32 p = 0; p < numProgs; p++) + { + plugin.BeginSetProgram(p); + ErrorCode retVal = LoadFile(file, plugin); + if(retVal != noError) + { + return retVal; + } + plugin.EndSetProgram(); + } + plugin.SetCurrentProgram(oldCurrentProgram); + } else + { + uint32 chunkSize = file.ReadUint32BE(); + // Some nasty plugins (e.g. SmartElectronix Ambience) write to our memory block. + // Directly writing to a memory-mapped file block results in a crash... + std::byte *chunkData = new (std::nothrow) std::byte[chunkSize]; + if(chunkData) + { + file.ReadRaw(mpt::span(chunkData, chunkSize)); + plugin.SetChunk(mpt::as_span(chunkData, chunkSize), true); + delete[] chunkData; + } else + { + return outOfMemory; + } + } + if(header.version >= 2) + { + plugin.SetCurrentProgram(currentProgram); + } + } + + return noError; +} + + +bool VSTPresets::SaveFile(std::ostream &f, IMixPlugin &plugin, bool bank) +{ + if(!bank) + { + SaveProgram(f, plugin); + } else + { + bool writeChunk = plugin.ProgramsAreChunks(); + ChunkHeader header; + memcpy(header.chunkMagic, "CcnK", 4); + header.byteSize = 0; // will be corrected later + header.version = 2; + header.fxID = plugin.GetUID(); + header.fxVersion = plugin.GetVersion(); + + // Write unfinished header... We need to update the size once we're done writing. + mpt::IO::Write(f, header); + + uint32 numProgs = std::max(plugin.GetNumPrograms(), int32(1)), curProg = plugin.GetCurrentProgram(); + mpt::IO::WriteIntBE(f, numProgs); + mpt::IO::WriteIntBE(f, curProg); + uint8 reserved[124]; + MemsetZero(reserved); + mpt::IO::WriteRaw(f, reserved, sizeof(reserved)); + + if(writeChunk) + { + auto chunk = plugin.GetChunk(true); + uint32 chunkSize = mpt::saturate_cast<uint32>(chunk.size()); + if(chunkSize) + { + mpt::IO::WriteIntBE(f, chunkSize); + mpt::IO::WriteRaw(f, chunk.data(), chunkSize); + } else + { + // The plugin returned no chunk! Gracefully go back and save parameters instead... + writeChunk = false; + } + } + if(!writeChunk) + { + for(uint32 p = 0; p < numProgs; p++) + { + plugin.SetCurrentProgram(p); + SaveProgram(f, plugin); + } + plugin.SetCurrentProgram(curProg); + } + + // Now we know the correct chunk size. + std::streamoff end = f.tellp(); + header.byteSize = static_cast<int32>(end - 8); + memcpy(header.fxMagic, writeChunk ? "FBCh" : "FxBk", 4); + mpt::IO::SeekBegin(f); + mpt::IO::Write(f, header); + } + + return true; +} + + +void VSTPresets::SaveProgram(std::ostream &f, IMixPlugin &plugin) +{ + bool writeChunk = plugin.ProgramsAreChunks(); + ChunkHeader header; + memcpy(header.chunkMagic, "CcnK", 4); + header.byteSize = 0; // will be corrected later + header.version = 1; + header.fxID = plugin.GetUID(); + header.fxVersion = plugin.GetVersion(); + + // Write unfinished header... We need to update the size once we're done writing. + mpt::IO::Offset start = mpt::IO::TellWrite(f); + mpt::IO::Write(f, header); + + const uint32 numParams = plugin.GetNumParameters(); + mpt::IO::WriteIntBE(f, numParams); + + char name[28]; + mpt::String::WriteBuf(mpt::String::maybeNullTerminated, name) = mpt::ToCharset(mpt::Charset::Locale, plugin.GetCurrentProgramName()); + mpt::IO::WriteRaw(f, name, 28); + + if(writeChunk) + { + auto chunk = plugin.GetChunk(false); + uint32 chunkSize = mpt::saturate_cast<uint32>(chunk.size()); + if(chunkSize) + { + mpt::IO::WriteIntBE(f, chunkSize); + mpt::IO::WriteRaw(f, chunk.data(), chunkSize); + } else + { + // The plugin returned no chunk! Gracefully go back and save parameters instead... + writeChunk = false; + } + } + if(!writeChunk) + { + plugin.BeginGetProgram(); + for(uint32 p = 0; p < numParams; p++) + { + mpt::IO::Write(f, IEEE754binary32BE(plugin.GetParameter(p))); + } + plugin.EndGetProgram(); + } + + // Now we know the correct chunk size. + mpt::IO::Offset end = mpt::IO::TellWrite(f); + header.byteSize = static_cast<int32>(end - start - 8); + memcpy(header.fxMagic, writeChunk ? "FPCh" : "FxCk", 4); + mpt::IO::SeekAbsolute(f, start); + mpt::IO::Write(f, header); + mpt::IO::SeekAbsolute(f, end); +} + + +// Translate error code to string. Returns nullptr if there was no error. +const char *VSTPresets::GetErrorMessage(ErrorCode code) +{ + switch(code) + { + case VSTPresets::invalidFile: + return "This does not appear to be a valid preset file."; + case VSTPresets::wrongPlugin: + return "This file appears to be for a different plugin."; + case VSTPresets::wrongParameters: + return "The number of parameters in this file is incompatible with the current plugin."; + case VSTPresets::outOfMemory: + return "Not enough memory to load preset data."; + } + return nullptr; +} + +#endif // NO_PLUGINS + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.h b/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.h new file mode 100644 index 00000000..9fbb83f1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/VstPresets.h @@ -0,0 +1,49 @@ +/* + * VstPresets.h + * ------------ + * Purpose: Plugin preset / bank handling + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <iosfwd> +#include "../common/FileReaderFwd.h" + +OPENMPT_NAMESPACE_BEGIN + +class IMixPlugin; + +class VSTPresets +{ +public: + enum ErrorCode + { + noError, + invalidFile, + wrongPlugin, + wrongParameters, + outOfMemory, + }; + +#ifndef NO_PLUGINS + static ErrorCode LoadFile(FileReader &file, IMixPlugin &plugin); + static bool SaveFile(std::ostream &, IMixPlugin &plugin, bool bank); + static const char *GetErrorMessage(ErrorCode code); + +protected: + static void SaveProgram(std::ostream &f, IMixPlugin &plugin); + +#else + static ErrorCode LoadFile(FileReader &, IMixPlugin &) { return invalidFile; } + static bool SaveFile(std::ostream &, IMixPlugin &, bool) { return false; } + static const char *GetErrorMessage(ErrorCode) { return "OpenMPT has been built without plugin support"; } +#endif // NO_PLUGINS +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.cpp new file mode 100644 index 00000000..54a8e058 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.cpp @@ -0,0 +1,1762 @@ +/* + * Vstplug.cpp + * ----------- + * Purpose: VST Plugin handling / processing + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#ifdef MPT_WITH_VST + +#include "Vstplug.h" +#ifdef MODPLUG_TRACKER +#include "Moddoc.h" +#include "Mainfrm.h" +#include "AbstractVstEditor.h" +#include "VSTEditor.h" +#include "DefaultVstEditor.h" +#include "ExceptionHandler.h" +#endif // MODPLUG_TRACKER +#include "../soundlib/Sndfile.h" +#include "../soundlib/MIDIEvents.h" +#include "MIDIMappingDialog.h" +#include "../common/mptStringBuffer.h" +#include "FileDialog.h" +#include "../pluginBridge/BridgeWrapper.h" +#include "../pluginBridge/BridgeOpCodes.h" +#include "../soundlib/plugins/OpCodes.h" +#include "../soundlib/plugins/PluginManager.h" +#include "../misc/mptOSException.h" + +using namespace Vst; +DECLARE_FLAGSET(Vst::VstTimeInfoFlags) + +OPENMPT_NAMESPACE_BEGIN + +static VstTimeInfo g_timeInfoFallback = { 0 }; + +#ifdef MPT_ALL_LOGGING +#define VST_LOG +#endif + + +using VstCrash = Windows::SEH::Code; + + +bool CVstPlugin::MaskCrashes() noexcept +{ + return m_maskCrashes; +} + + +template <typename Tfn> +DWORD CVstPlugin::SETryOrError(bool maskCrashes, Tfn fn) +{ + DWORD exception = 0; + if(maskCrashes) + { + exception = Windows::SEH::TryOrError(fn); + if(exception) + { + ExceptionHandler::TaintProcess(ExceptionHandler::TaintReason::Plugin); + } + } else + { + fn(); + } + return exception; +} + + +template <typename Tfn> +DWORD CVstPlugin::SETryOrError(Tfn fn) +{ + DWORD exception = 0; + if(MaskCrashes()) + { + exception = Windows::SEH::TryOrError(fn); + if(exception) + { + ExceptionHandler::TaintProcess(ExceptionHandler::TaintReason::Plugin); + } + } else + { +#ifdef MODPLUG_TRACKER + ExceptionHandler::ContextSetter ectxguard{&m_Ectx}; +#endif // MODPLUG_TRACKER + fn(); + } + return exception; +} + + +AEffect *CVstPlugin::LoadPlugin(bool maskCrashes, VSTPluginLib &plugin, HMODULE &library, BridgeMode bridgeMode) +{ + const mpt::PathString &pluginPath = plugin.dllPath; + + AEffect *effect = nullptr; + library = nullptr; + + const bool isNative = plugin.IsNative(false); + if(bridgeMode != BridgeMode::Automatic || plugin.useBridge || !isNative) + { + if(bridgeMode == BridgeMode::DetectRequiredBridgeMode) + { + // First try modern bridge, then legacy bridge + plugin.modernBridge = true; + try + { + effect = BridgeWrapper::Create(plugin, false); + if(effect != nullptr) + { + return effect; + } + } catch(BridgeWrapper::BridgeNotFoundException &) + { + } catch(BridgeWrapper::BridgeException &) + { + } + // Retry with legacy bridge + plugin.useBridge = true; + plugin.modernBridge = false; + } + + try + { + effect = BridgeWrapper::Create(plugin, bridgeMode == BridgeMode::DetectRequiredBridgeMode); + if(effect != nullptr) + { + return effect; + } + } catch(BridgeWrapper::BridgeNotFoundException &) + { + // Try normal loading + if(!isNative) + { + Reporting::Error("Could not locate the plugin bridge executable, which is required for running non-native plugins.", "OpenMPT Plugin Bridge"); + return nullptr; + } + } catch(BridgeWrapper::BridgeException &e) + { + // If there was some error, don't try normal loading as well... unless the user really wants it. + if(isNative) + { + const CString msg = + MPT_CFORMAT("The following error occurred while trying to load\n{}\n\n{}\n\nDo you want to try to load the plugin natively?") + (plugin.dllPath, mpt::get_exception_text<mpt::ustring>(e)); + if(Reporting::Confirm(msg, _T("OpenMPT Plugin Bridge")) == cnfNo) + { + return nullptr; + } + } else + { + Reporting::Error(mpt::get_exception_text<mpt::ustring>(e), "OpenMPT Plugin Bridge"); + return nullptr; + } + } + // If plugin was marked to use the plugin bridge but this somehow doesn't work (e.g. because the bridge is missing), + // disable the plugin bridge for this plugin. + plugin.useBridge = false; + plugin.modernBridge = true; + } + + { +#ifdef MODPLUG_TRACKER + ExceptionHandler::Context ectx{ MPT_UFORMAT("VST Plugin: {}")(plugin.dllPath.ToUnicode()) }; + ExceptionHandler::ContextSetter ectxguard{&ectx}; +#endif // MODPLUG_TRACKER + DWORD exception = SETryOrError(maskCrashes, [&](){ library = LoadLibrary(pluginPath.AsNative().c_str()); }); + if(exception) + { + CVstPluginManager::ReportPlugException(MPT_UFORMAT("Exception caught while loading {}")(pluginPath)); + return nullptr; + } + } + if(library == nullptr) + { + DWORD error = GetLastError(); + if(error == ERROR_MOD_NOT_FOUND) + { + return nullptr; + } else if(error == ERROR_DLL_INIT_FAILED) + { + // A likely reason for this error is that Fiber Local Storage slots are exhausted, e.g. because too many plugins ship with a statically linked runtime. + // Before Windows 10 1903, there was a limit of 128 FLS slots per process, and the VS2017 runtime uses two FLS slots, so this could cause a worst-case limit + // of 62 different plugins per process (assuming they all use a statically-linked runtime). + // In Windows 10 1903, the FLS limit was finally raised, so this message is mostly relevant for older systems. + CVstPluginManager::ReportPlugException(U_("Plugin initialization failed. This may be caused by loading too many plugins.\nTry activating the Plugin Bridge for this plugin.")); + } + +#ifdef _DEBUG + mpt::ustring buf = MPT_UFORMAT("Warning: encountered problem when loading plugin dll. Error {}: {}") + ( mpt::ufmt::hex(error) + , mpt::ToUnicode(mpt::windows::GetErrorMessage(error)) + ); + Reporting::Error(buf, "DEBUG: Error when loading plugin dll"); +#endif //_DEBUG + } + + if(library != nullptr && library != INVALID_HANDLE_VALUE) + { + auto pMainProc = (Vst::MainProc)GetProcAddress(library, "VSTPluginMain"); + if(pMainProc == nullptr) + { + pMainProc = (Vst::MainProc)GetProcAddress(library, "main"); + } + + if(pMainProc != nullptr) + { +#ifdef MODPLUG_TRACKER + ExceptionHandler::Context ectx{ MPT_UFORMAT("VST Plugin: {}")(plugin.dllPath.ToUnicode()) }; + ExceptionHandler::ContextSetter ectxguard{&ectx}; +#endif // MODPLUG_TRACKER + DWORD exception = SETryOrError(maskCrashes, [&](){ effect = pMainProc(CVstPlugin::MasterCallBack); }); + if(exception) + { + return nullptr; + } + } else + { +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("Entry point not found! (handle={})")(mpt::ufmt::PTR(library))); +#endif // VST_LOG + return nullptr; + } + } + + return effect; +} + +static void operator|= (Vst::VstTimeInfoFlags &lhs, Vst::VstTimeInfoFlags rhs) +{ + lhs = (lhs | rhs).as_enum(); +} + +intptr_t VSTCALLBACK CVstPlugin::MasterCallBack(AEffect *effect, VstOpcodeToHost opcode, int32 index, intptr_t value, void *ptr, float opt) +{ +#ifdef VST_LOG + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("VST plugin to host: Eff: {}, Opcode = {}, Index = {}, Value = {}, PTR = {}, OPT = {}\n")( + mpt::ufmt::PTR(effect), mpt::ufmt::val(opcode), + mpt::ufmt::val(index), mpt::ufmt::PTR(value), mpt::ufmt::PTR(ptr), mpt::ufmt::flt(opt, 3))); + MPT_TRACE(); +#else + MPT_UNREFERENCED_PARAMETER(opt); +#endif + + enum + { + HostDoNotKnow = 0, + HostCanDo = 1, + HostCanNotDo = -1 + }; + + CVstPlugin *pVstPlugin = nullptr; + if(effect != nullptr) + { + pVstPlugin = static_cast<CVstPlugin *>(effect->reservedForHost1); + } + + switch(opcode) + { + // Called when plugin param is changed via gui + case audioMasterAutomate: + // Strum Acoustic GS-1 and Strum Electric GS-1 send audioMasterAutomate during effOpen (WTF #1), + // but when sending back effCanBeAutomated, they just crash (WTF #2). + // As a consequence, just generally forbid this action while the plugin is not fully initialized yet. + if(pVstPlugin != nullptr && pVstPlugin->m_isInitialized && pVstPlugin->CanAutomateParameter(index)) + { + // This parameter can be automated. Ugo Motion constantly sends automation callback events for parameters that cannot be automated... + pVstPlugin->AutomateParameter((PlugParamIndex)index); + } + return 0; + + // Called when plugin asks for VST version supported by host + case audioMasterVersion: + return kVstVersion; + + // Returns the unique id of a plugin that's currently loading + // We don't support shell plugins currently, so we only support one effect ID as well. + case audioMasterCurrentId: + return (effect != nullptr) ? effect->uniqueID : 0; + + // Call application idle routine (this will call effEditIdle for all open editors too) + case audioMasterIdle: + theApp.GetPluginManager()->OnIdle(); + return 0; + + // Inquire if an input or output is beeing connected; index enumerates input or output counting from zero, + // value is 0 for input and != 0 otherwise. note: the return value is 0 for <true> such that older versions + // will always return true. + case audioMasterPinConnected: + if (value) //input: + return (index < 2) ? 0 : 1; //we only support up to 2 inputs. Remember: 0 means yes. + else //output: + return (index < 2) ? 0 : 1; //2 outputs max too + + //---from here VST 2.0 extension opcodes------------------------------------------------------ + + // <value> is a filter which is currently ignored - DEPRECATED in VST 2.4 + case audioMasterWantMidi: + return 1; + + // returns const VstTimeInfo* (or 0 if not supported) + // <value> should contain a mask indicating which fields are required + case audioMasterGetTime: + + if(pVstPlugin) + { + VstTimeInfo &timeInfo = pVstPlugin->timeInfo; + MemsetZero(timeInfo); + + timeInfo.sampleRate = pVstPlugin->m_nSampleRate; + CSoundFile &sndFile = pVstPlugin->GetSoundFile(); + if(pVstPlugin->IsSongPlaying()) + { + timeInfo.flags |= kVstTransportPlaying; + if(pVstPlugin->GetSoundFile().m_SongFlags[SONG_PATTERNLOOP]) timeInfo.flags |= kVstTransportCycleActive; + timeInfo.samplePos = sndFile.GetTotalSampleCount(); + if(pVstPlugin->m_positionChanged) + { + timeInfo.flags |= kVstTransportChanged; + pVstPlugin->lastBarStartPos = -1.0; + } + } else + { + timeInfo.flags |= kVstTransportChanged; //just stopped. + timeInfo.samplePos = 0; + pVstPlugin->lastBarStartPos = -1.0; + } + if((value & kVstNanosValid)) + { + timeInfo.flags |= kVstNanosValid; + timeInfo.nanoSeconds = static_cast<double>(Util::mul32to64_unsigned(timeGetTime(), 1000000)); + } + if((value & kVstPpqPosValid)) + { + timeInfo.flags |= kVstPpqPosValid; + if (timeInfo.flags & kVstTransportPlaying) + { + timeInfo.ppqPos = (timeInfo.samplePos / timeInfo.sampleRate) * (sndFile.GetCurrentBPM() / 60.0); + } else + { + timeInfo.ppqPos = 0; + } + + ROWINDEX rpm = pVstPlugin->GetSoundFile().m_PlayState.m_nCurrentRowsPerMeasure; + if(!rpm) + rpm = 4; + if((pVstPlugin->GetSoundFile().m_PlayState.m_nRow % rpm) == 0) + { + pVstPlugin->lastBarStartPos = std::floor(timeInfo.ppqPos); + } + if(pVstPlugin->lastBarStartPos >= 0) + { + timeInfo.barStartPos = pVstPlugin->lastBarStartPos; + timeInfo.flags |= kVstBarsValid; + } + } + if((value & kVstTempoValid)) + { + timeInfo.tempo = sndFile.GetCurrentBPM(); + if (timeInfo.tempo) + { + timeInfo.flags |= kVstTempoValid; + } + } + if((value & kVstTimeSigValid)) + { + timeInfo.flags |= kVstTimeSigValid; + + // Time signature. numerator = rows per beats / rows pear measure (should sound somewhat logical to you). + // the denominator is a bit more tricky, since it cannot be set explicitely. so we just assume quarters for now. + ROWINDEX rpb = std::max(sndFile.m_PlayState.m_nCurrentRowsPerBeat, ROWINDEX(1)); + timeInfo.timeSigNumerator = std::max(sndFile.m_PlayState.m_nCurrentRowsPerMeasure, rpb) / rpb; + timeInfo.timeSigDenominator = 4; //std::gcd(pSndFile->m_nCurrentRowsPerMeasure, pSndFile->m_nCurrentRowsPerBeat); + } + return ToIntPtr(&timeInfo); + } else + { + MemsetZero(g_timeInfoFallback); + return ToIntPtr(&g_timeInfoFallback); + } + + // Receive MIDI events from plugin + case audioMasterProcessEvents: + if(pVstPlugin != nullptr && ptr != nullptr) + { + pVstPlugin->ReceiveVSTEvents(static_cast<VstEvents *>(ptr)); + return 1; + } + break; + + // DEPRECATED in VST 2.4 + case audioMasterSetTime: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Set Time")); + break; + + // returns tempo (in bpm * 10000) at sample frame location passed in <value> - DEPRECATED in VST 2.4 + case audioMasterTempoAt: + // Screw it! Let's just return the tempo at this point in time (might be a bit wrong). + if (pVstPlugin != nullptr) + { + return mpt::saturate_round<int32>(pVstPlugin->GetSoundFile().GetCurrentBPM() * 10000); + } + return (125 * 10000); + + // parameters - DEPRECATED in VST 2.4 + case audioMasterGetNumAutomatableParameters: + //MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Num Automatable Parameters")); + if(pVstPlugin != nullptr) + { + return pVstPlugin->GetNumParameters(); + } + break; + + // Apparently, this one is broken in VST SDK anyway. - DEPRECATED in VST 2.4 + case audioMasterGetParameterQuantization: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Audio Master Get Parameter Quantization")); + break; + + // numInputs and/or numOutputs has changed + case audioMasterIOChanged: + if(pVstPlugin != nullptr) + { + CriticalSection cs; + return pVstPlugin->InitializeIOBuffers() ? 1 : 0; + } + break; + + // Plugin needs idle calls (outside its editor window) - DEPRECATED in VST 2.4 + case audioMasterNeedIdle: + if(pVstPlugin != nullptr) + { + pVstPlugin->m_needIdle = true; + } + + return 1; + + // index: width, value: height + case audioMasterSizeWindow: + if(pVstPlugin != nullptr) + { + CAbstractVstEditor *pVstEditor = pVstPlugin->GetEditor(); + if (pVstEditor && pVstEditor->IsResizable()) + { + pVstEditor->SetSize(index, static_cast<int>(value)); + } + } + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Size Window")); + return 1; + + case audioMasterGetSampleRate: + if(pVstPlugin) + { + return pVstPlugin->m_nSampleRate; + } else + { + // HERCs Abakos queries the sample rate while the plugin is being created and then never again... + return TrackerSettings::Instance().GetMixerSettings().gdwMixingFreq; + } + + case audioMasterGetBlockSize: + return MIXBUFFERSIZE; + + case audioMasterGetInputLatency: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Input Latency")); + break; + + case audioMasterGetOutputLatency: + if(pVstPlugin) + { + return mpt::saturate_round<intptr_t>(pVstPlugin->GetOutputLatency() * pVstPlugin->GetSoundFile().GetSampleRate()); + } + break; + + // input pin in <value> (-1: first to come), returns cEffect* - DEPRECATED in VST 2.4 + case audioMasterGetPreviousPlug: + if(pVstPlugin != nullptr) + { + std::vector<IMixPlugin *> list; + if(pVstPlugin->GetInputPlugList(list) != 0) + { + // We don't assign plugins to pins... + CVstPlugin *plugin = dynamic_cast<CVstPlugin *>(list[0]); + if(plugin != nullptr) + { + return ToIntPtr(&plugin->m_Effect); + } + } + } + break; + + // output pin in <value> (-1: first to come), returns cEffect* - DEPRECATED in VST 2.4 + case audioMasterGetNextPlug: + if(pVstPlugin != nullptr) + { + std::vector<IMixPlugin *> list; + if(pVstPlugin->GetOutputPlugList(list) != 0) + { + // We don't assign plugins to pins... + CVstPlugin *plugin = dynamic_cast<CVstPlugin *>(list[0]); + if(plugin != nullptr) + { + return ToIntPtr(&plugin->m_Effect); + } + } + } + break; + + // realtime info + // returns: 0: not supported, 1: replace, 2: accumulate - DEPRECATED in VST 2.4 (replace is default) + case audioMasterWillReplaceOrAccumulate: + return 1; //we replace. + + case audioMasterGetCurrentProcessLevel: + if(pVstPlugin != nullptr && pVstPlugin->GetSoundFile().IsRenderingToDisc()) + return kVstProcessLevelOffline; + else + return kVstProcessLevelRealtime; + break; + + // returns 0: not supported, 1: off, 2:read, 3:write, 4:read/write + case audioMasterGetAutomationState: + // Not entirely sure what this means. We can write automation TO the plug. + // Is that "read" in this context? + //MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Automation State")); + return kVstAutomationReadWrite; + + case audioMasterOfflineStart: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Offlinestart")); + break; + + case audioMasterOfflineRead: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Offlineread")); + break; + + case audioMasterOfflineWrite: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Offlinewrite")); + break; + + case audioMasterOfflineGetCurrentPass: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: OfflineGetcurrentpass")); + break; + + case audioMasterOfflineGetCurrentMetaPass: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: OfflineGetCurrentMetapass")); + break; + + // for variable i/o, sample rate in <opt> - DEPRECATED in VST 2.4 + case audioMasterSetOutputSampleRate: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Set Output Sample Rate")); + break; + + // result in ret - DEPRECATED in VST 2.4 + case audioMasterGetOutputSpeakerArrangement: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Output Speaker Arrangement")); + break; + + case audioMasterGetVendorString: + strcpy((char *) ptr, TrackerSettings::Instance().vstHostVendorString.Get().c_str()); + return 1; + + case audioMasterGetProductString: + strcpy((char *) ptr, TrackerSettings::Instance().vstHostProductString.Get().c_str()); + return 1; + + case audioMasterGetVendorVersion: + return TrackerSettings::Instance().vstHostVendorVersion; + + case audioMasterVendorSpecific: + return 0; + + // void* in <ptr>, format not defined yet - DEPRECATED in VST 2.4 + case audioMasterSetIcon: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Set Icon")); + break; + + // string in ptr, see below + case audioMasterCanDo: + //Other possible Can Do strings are: + if(!strcmp((char*)ptr, HostCanDo::sendVstEvents) + || !strcmp((char *)ptr, HostCanDo::sendVstMidiEvent) + || !strcmp((char *)ptr, HostCanDo::sendVstTimeInfo) + || !strcmp((char *)ptr, HostCanDo::receiveVstEvents) + || !strcmp((char *)ptr, HostCanDo::receiveVstMidiEvent) + || !strcmp((char *)ptr, HostCanDo::supplyIdle) + || !strcmp((char *)ptr, HostCanDo::sizeWindow) + || !strcmp((char *)ptr, HostCanDo::openFileSelector) + || !strcmp((char *)ptr, HostCanDo::closeFileSelector) + || !strcmp((char *)ptr, HostCanDo::acceptIOChanges) + || !strcmp((char *)ptr, HostCanDo::reportConnectionChanges)) + { + return HostCanDo; + } else + { + return HostCanNotDo; + } + + case audioMasterGetLanguage: + return kVstLangEnglish; + + // returns platform specific ptr - DEPRECATED in VST 2.4 + case audioMasterOpenWindow: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Open Window")); + break; + + // close window, platform specific handle in <ptr> - DEPRECATED in VST 2.4 + case audioMasterCloseWindow: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Close Window")); + break; + + // get plugin directory, FSSpec on MAC, else char* + case audioMasterGetDirectory: + //MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Directory")); + // Need to allocate space for path only, but I guess noone relies on this anyway. + //return ToVstPtr(pVstPlugin->GetPluginFactory().dllPath.GetPath().ToLocale()); + //return ToVstPtr(TrackerSettings::Instance().PathPlugins.GetDefaultDir()); + break; + + // something has changed, update 'multi-fx' display + case audioMasterUpdateDisplay: + if(pVstPlugin != nullptr) + { + // Note to self for testing: Electri-Q sends opcode. Korg M1 sends this when switching between Combi and Multi mode to update the preset names. + CAbstractVstEditor *pVstEditor = pVstPlugin->GetEditor(); + if(pVstEditor && ::IsWindow(pVstEditor->m_hWnd)) + { + pVstEditor->UpdateDisplay(); + } + } + return 0; + + //---from here VST 2.1 extension opcodes------------------------------------------------------ + + // begin of automation session (when mouse down), parameter index in <index> + case audioMasterBeginEdit: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Begin Edit")); + break; + + // end of automation session (when mouse up), parameter index in <index> + case audioMasterEndEdit: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: End Edit")); + break; + + // open a fileselector window with VstFileSelect* in <ptr> + case audioMasterOpenFileSelector: + + //---from here VST 2.2 extension opcodes------------------------------------------------------ + + // close a fileselector operation with VstFileSelect* in <ptr>: Must be always called after an open ! + case audioMasterCloseFileSelector: + if(pVstPlugin != nullptr && ptr != nullptr) + { + return pVstPlugin->VstFileSelector(opcode == audioMasterCloseFileSelector, *static_cast<VstFileSelect *>(ptr)); + } + + // open an editor for audio (defined by XML text in ptr) - DEPRECATED in VST 2.4 + case audioMasterEditFile: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Edit File")); + break; + + // get the native path of currently loading bank or project + // (called from writeChunk) void* in <ptr> (char[2048], or sizeof(FSSpec)) - DEPRECATED in VST 2.4 + // Note: The shortcircuit VSTi actually uses this feature. + case audioMasterGetChunkFile: +#ifdef MODPLUG_TRACKER + if(pVstPlugin && pVstPlugin->GetModDoc()) + { + mpt::ustring pathStr = TrackerSettings::Instance().pluginProjectPath; + if(pathStr.empty()) + { + pathStr = U_("%1"); + } + const mpt::PathString projectPath = pVstPlugin->GetModDoc()->GetPathNameMpt().GetPath(); + const mpt::PathString projectFile = pVstPlugin->GetModDoc()->GetPathNameMpt().GetFullFileName(); + pathStr = mpt::String::Replace(pathStr, U_("%1"), U_("?1?")); + pathStr = mpt::String::Replace(pathStr, U_("%2"), U_("?2?")); + pathStr = mpt::String::Replace(pathStr, U_("?1?"), projectPath.ToUnicode()); + pathStr = mpt::String::Replace(pathStr, U_("?2?"), projectFile.ToUnicode()); + mpt::PathString path = mpt::PathString::FromUnicode(pathStr); + if(path.empty()) + { + return 0; + } + path.EnsureTrailingSlash(); + ::SHCreateDirectoryEx(NULL, path.AsNative().c_str(), nullptr); + path += projectFile; + strcpy(static_cast<char*>(ptr), path.ToLocale().c_str()); + return 1; + } +#endif + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Chunk File")); + break; + + //---from here VST 2.3 extension opcodes------------------------------------------------------ + + // result a VstSpeakerArrangement in ret - DEPRECATED in VST 2.4 + case audioMasterGetInputSpeakerArrangement: + MPT_LOG_GLOBAL(LogDebug, "VST", U_("VST plugin to host: Get Input Speaker Arrangement")); + break; + + } + + // Unknown codes: + + return 0; +} + + +// Helper function for file selection dialog stuff. +intptr_t CVstPlugin::VstFileSelector(bool destructor, VstFileSelect &fileSel) +{ + if(!destructor) + { + fileSel.returnMultiplePaths = nullptr; + fileSel.numReturnPaths = 0; + fileSel.reserved = 0; + + std::string returnPath; + if(fileSel.command != kVstDirectorySelect) + { + // Plugin wants to load or save a file. + std::string extensions, workingDir; + for(int32 i = 0; i < fileSel.numFileTypes; i++) + { + const VstFileType &type = fileSel.fileTypes[i]; + extensions += type.name; + extensions += "|"; +#if MPT_OS_WINDOWS + extensions += "*."; + extensions += type.dosType; +#elif MPT_OS_MACOSX_OR_IOS + extensions += "*"; + extensions += type.macType; +#elif MPT_OS_GENERIC_UNIX + extensions += "*."; + extensions += type.unixType; +#else +#error Platform-specific code missing +#endif + extensions += "|"; + } + extensions += "|"; + + if(fileSel.initialPath != nullptr) + { + workingDir = fileSel.initialPath; + } else + { + // Plugins are probably looking for presets...? + //workingDir = TrackerSettings::Instance().PathPluginPresets.GetWorkingDir(); + } + + FileDialog dlg = OpenFileDialog(); + if(fileSel.command == kVstFileSave) + { + dlg = SaveFileDialog(); + } else if(fileSel.command == kVstMultipleFilesLoad) + { + dlg = OpenFileDialog().AllowMultiSelect(); + } + dlg.ExtensionFilter(extensions) + .WorkingDirectory(mpt::PathString::FromLocale(workingDir)) + .AddPlace(GetPluginFactory().dllPath.GetPath()); + if(!dlg.Show(GetEditor())) + return 0; + + if(fileSel.command == kVstMultipleFilesLoad) + { + // Multiple paths + const auto &files = dlg.GetFilenames(); + fileSel.numReturnPaths = mpt::saturate_cast<int32>(files.size()); + fileSel.returnMultiplePaths = new (std::nothrow) char *[fileSel.numReturnPaths]; + if(!fileSel.returnMultiplePaths) + return 0; + for(int32 i = 0; i < fileSel.numReturnPaths; i++) + { + const std::string fname_ = files[i].ToLocale(); + char *fname = new (std::nothrow) char[fname_.length() + 1]; + if(fname) + strcpy(fname, fname_.c_str()); + fileSel.returnMultiplePaths[i] = fname; + } + return 1; + } else + { + // Single path + + // VOPM doesn't initialize required information properly (it doesn't memset the struct to 0)... + if(FourCC("VOPM") == GetUID()) + { + fileSel.sizeReturnPath = _MAX_PATH; + } + + returnPath = dlg.GetFirstFile().ToLocale(); + } + } else + { + // Plugin wants a directory + BrowseForFolder dlg(mpt::PathString::FromLocale(fileSel.initialPath != nullptr ? fileSel.initialPath : ""), mpt::ToCString(mpt::Charset::Locale, fileSel.title != nullptr ? fileSel.title : "")); + if(!dlg.Show(GetEditor())) + return 0; + + returnPath = dlg.GetDirectory().ToLocale(); + if(FourCC("VSTr") == GetUID() && fileSel.returnPath != nullptr && fileSel.sizeReturnPath == 0) + { + // Old versions of reViSiT (which still relied on the host's file selector) seem to be dodgy. + // They report a path size of 0, but when using an own buffer, they will crash. + // So we'll just assume that reViSiT can handle long enough (_MAX_PATH) paths here. + fileSel.sizeReturnPath = mpt::saturate_cast<int32>(returnPath.length() + 1); + } + } + + // Return single path (file or directory) + if(fileSel.returnPath == nullptr || fileSel.sizeReturnPath == 0) + { + // Provide some memory for the return path. + fileSel.sizeReturnPath = mpt::saturate_cast<int32>(returnPath.length() + 1); + fileSel.returnPath = new(std::nothrow) char[fileSel.sizeReturnPath]; + if(fileSel.returnPath == nullptr) + { + return 0; + } + fileSel.reserved = 1; + } else + { + fileSel.reserved = 0; + } + const auto len = std::min(returnPath.size(), static_cast<size_t>(fileSel.sizeReturnPath - 1)); + strncpy(fileSel.returnPath, returnPath.data(), len); + fileSel.returnPath[len] = '\0'; + fileSel.numReturnPaths = 1; + fileSel.returnMultiplePaths = nullptr; + return 1; + } else + { + // Close file selector - delete allocated strings. + if(fileSel.command == kVstMultipleFilesLoad && fileSel.returnMultiplePaths != nullptr) + { + for(int32 i = 0; i < fileSel.numReturnPaths; i++) + { + if(fileSel.returnMultiplePaths[i] != nullptr) + { + delete[] fileSel.returnMultiplePaths[i]; + } + } + delete[] fileSel.returnMultiplePaths; + fileSel.returnMultiplePaths = nullptr; + } else + { + if(fileSel.reserved == 1 && fileSel.returnPath != nullptr) + { + delete[] fileSel.returnPath; + fileSel.returnPath = nullptr; + } + } + return 1; + } +} + + +////////////////////////////////////////////////////////////////////////////// +// +// CVstPlugin +// + +CVstPlugin::CVstPlugin(bool maskCrashes, HMODULE hLibrary, VSTPluginLib &factory, SNDMIXPLUGIN &mixStruct, AEffect &effect, CSoundFile &sndFile) + : IMidiPlugin(factory, sndFile, &mixStruct) + , m_maskCrashes(maskCrashes) + , m_Effect(effect) + , timeInfo{} + , isBridged(!memcmp(&effect.reservedForHost2, "OMPT", 4)) + , m_hLibrary(hLibrary) + , m_nSampleRate(sndFile.GetSampleRate()) + , m_isInitialized(false) + , m_needIdle(false) +{ + // Open plugin and initialize data structures + Initialize(); + InsertIntoFactoryList(); + + m_isInitialized = true; +} + + +void CVstPlugin::Initialize() +{ + + m_Ectx = { MPT_UFORMAT("VST Plugin: {}")(m_Factory.dllPath.ToUnicode()) }; + + // If filename matched during load but plugin ID didn't, make sure it's updated. + m_pMixStruct->Info.dwPluginId1 = m_Factory.pluginId1 = m_Effect.magic; + m_pMixStruct->Info.dwPluginId2 = m_Factory.pluginId2 = m_Effect.uniqueID; + + // Store a pointer so we can get the CVstPlugin object from the basic VST effect object. + m_Effect.reservedForHost1 = this; + m_nSampleRate = m_SndFile.GetSampleRate(); + + // First try to let the plugin know the render parameters. + Dispatch(effSetSampleRate, 0, 0, nullptr, static_cast<float>(m_nSampleRate)); + Dispatch(effSetBlockSize, 0, MIXBUFFERSIZE, nullptr, 0.0f); + + Dispatch(effOpen, 0, 0, nullptr, 0.0f); + + // VST 2.0 plugins return 2 here, VST 2.4 plugins return 2400... Great! + m_isVst2 = Dispatch(effGetVstVersion, 0,0, nullptr, 0.0f) >= 2; + if(m_isVst2) + { + // Set VST speaker in/out setup to Stereo. Required for some plugins (e.g. Voxengo SPAN 2) + // All this might get more interesting when adding sidechaining support... + VstSpeakerArrangement sa{}; + sa.numChannels = 2; + sa.type = kSpeakerArrStereo; + for(std::size_t i = 0; i < std::size(sa.speakers); i++) + { + // For now, only left and right speaker are used. + switch(i) + { + case 0: + sa.speakers[i].type = kSpeakerL; + mpt::String::WriteAutoBuf(sa.speakers[i].name) = "Left"; + break; + case 1: + sa.speakers[i].type = kSpeakerR; + mpt::String::WriteAutoBuf(sa.speakers[i].name) = "Right"; + break; + default: + sa.speakers[i].type = kSpeakerUndefined; + break; + } + } + + // For some reason, this call crashes in a call to free() in AdmiralQuality NaiveLPF / SCAMP 1.2 (newer versions are fine). + // This does not happen when running the plugin in pretty much any host, or when running in OpenMPT 1.22 and older + // (EXCEPT when recompiling those old versions with VS2010), so it sounds like an ASLR issue to me. + // AdmiralQuality also doesn't know what to do. + if(GetUID() != FourCC("CSI4")) + { + // For now, input setup = output setup. + Dispatch(effSetSpeakerArrangement, 0, ToIntPtr(&sa), &sa, 0.0f); + } + + // Dummy pin properties collection. + // We don't use them but some plugs might do inits in here. + VstPinProperties tempPinProperties; + Dispatch(effGetInputProperties, 0, 0, &tempPinProperties, 0); + Dispatch(effGetOutputProperties, 0, 0, &tempPinProperties, 0); + + Dispatch(effConnectInput, 0, 1, nullptr, 0.0f); + if (m_Effect.numInputs > 1) Dispatch(effConnectInput, 1, 1, nullptr, 0.0f); + Dispatch(effConnectOutput, 0, 1, nullptr, 0.0f); + if (m_Effect.numOutputs > 1) Dispatch(effConnectOutput, 1, 1, nullptr, 0.0f); + // Disable all inputs and outputs beyond stereo left and right: + for(int32 i = 2; i < m_Effect.numInputs; i++) + Dispatch(effConnectInput, i, 0, nullptr, 0.0f); + for(int32 i = 2; i < m_Effect.numOutputs; i++) + Dispatch(effConnectOutput, i, 0, nullptr, 0.0f); + } + + // Second try to let the plugin know the render parameters. + Dispatch(effSetSampleRate, 0, 0, nullptr, static_cast<float>(m_nSampleRate)); + Dispatch(effSetBlockSize, 0, MIXBUFFERSIZE, nullptr, 0.0f); + if(m_Effect.numPrograms > 0) + { + BeginSetProgram(0); + EndSetProgram(); + } + + InitializeIOBuffers(); + + Dispatch(effSetProcessPrecision, 0, kVstProcessPrecision32, nullptr, 0.0f); + + m_isInstrument = IsInstrument(); + RecalculateGain(); + m_pProcessFP = (m_Effect.flags & effFlagsCanReplacing) ? m_Effect.processReplacing : m_Effect.process; + + // Issue samplerate again here, cos some plugs like it before the block size, other like it right at the end. + Dispatch(effSetSampleRate, 0, 0, nullptr, static_cast<float>(m_nSampleRate)); + + // Korg Wavestation GUI won't work until plugin was resumed at least once. + // On the other hand, some other plugins (notably Synthedit plugins like Superwave P8 2.3 or Rez 3.0) don't like this + // and won't load their stored plugin data instantly, so only do this for the troublesome plugins... + // Also apply this fix for Korg's M1 plugin, as this will fixes older versions of said plugin, newer versions don't require the fix. + // EZDrummer / Superior Drummer won't load their samples until playback has started. + if(GetUID() == FourCC("KLWV") // Wavestation + || GetUID() == FourCC("KLM1") // M1 + || GetUID() == FourCC("dfhe") // EZDrummer + || GetUID() == FourCC("dfh2")) // Superior Drummer + { + Resume(); + Suspend(); + } +} + + +bool CVstPlugin::InitializeIOBuffers() +{ + // Input pointer array size must be >= 2 for now - the input buffer assignment might write to non allocated mem. otherwise + // In case of a bridged plugin, the AEffect struct has been updated before calling this opcode, so we don't have to worry about it being up-to-date. + return m_mixBuffer.Initialize(std::max(m_Effect.numInputs, int32(2)), m_Effect.numOutputs); +} + + +CVstPlugin::~CVstPlugin() +{ + CriticalSection cs; + + CloseEditor(); + if (m_isVst2) + { + Dispatch(effConnectInput, 0, 0, nullptr, 0); + if (m_Effect.numInputs > 1) Dispatch(effConnectInput, 1, 0, nullptr, 0); + Dispatch(effConnectOutput, 0, 0, nullptr, 0); + if (m_Effect.numOutputs > 1) Dispatch(effConnectOutput, 1, 0, nullptr, 0); + } + CVstPlugin::Suspend(); + m_isInitialized = false; + + Dispatch(effClose, 0, 0, nullptr, 0); + if(TrackerSettings::Instance().BrokenPluginsWorkaroundVSTNeverUnloadAnyPlugin) + { + // Buggy SynthEdit 1.4 plugins: Showing a SynthEdit 1.4 plugin's editor, fully unloading the plugin, + // then loading another (unrelated) SynthEdit 1.4 plugin and showing its editor causes a crash. + } else + { + if(m_hLibrary) + { + FreeLibrary(m_hLibrary); + } + } + +} + + +void CVstPlugin::Release() +{ + delete this; +} + + +void CVstPlugin::Idle() +{ + if(m_needIdle) + { + if(!(Dispatch(effIdle, 0, 0, nullptr, 0.0f))) + m_needIdle = false; + } + if (m_pEditor && m_pEditor->m_hWnd) + { + Dispatch(effEditIdle, 0, 0, nullptr, 0.0f); + } +} + + +int32 CVstPlugin::GetNumPrograms() const +{ + return std::max(m_Effect.numPrograms, int32(0)); +} + + +PlugParamIndex CVstPlugin::GetNumParameters() const +{ + return m_Effect.numParams; +} + + +// Check whether a VST parameter can be automated +bool CVstPlugin::CanAutomateParameter(PlugParamIndex index) +{ + return (Dispatch(effCanBeAutomated, index, 0, nullptr, 0.0f) != 0); +} + + +int32 CVstPlugin::GetUID() const +{ + return m_Effect.uniqueID; +} + + +int32 CVstPlugin::GetVersion() const +{ + return m_Effect.version; +} + + +// Wrapper for VST dispatch call with structured exception handling. +intptr_t CVstPlugin::DispatchSEH(bool maskCrashes, AEffect *effect, VstOpcodeToPlugin opCode, int32 index, intptr_t value, void *ptr, float opt, unsigned long &exception) +{ + if(effect->dispatcher != nullptr) + { + intptr_t result = 0; + DWORD e = SETryOrError(maskCrashes, [&](){ result = effect->dispatcher(effect, opCode, index, value, ptr, opt); }); + if(e) + { + exception = e; + } + return result; + } + return 0; +} + + +intptr_t CVstPlugin::Dispatch(VstOpcodeToPlugin opCode, int32 index, intptr_t value, void *ptr, float opt) +{ +#ifdef VST_LOG + { + mpt::ustring codeStr; + if(opCode >= 0 && static_cast<std::size_t>(opCode) < std::size(VstOpCodes)) + { + codeStr = mpt::ToUnicode(mpt::Charset::ASCII, VstOpCodes[opCode]); + } else + { + codeStr = mpt::ufmt::val(opCode); + } + MPT_LOG_GLOBAL(LogDebug, "VST", MPT_UFORMAT("About to Dispatch({}) (Plugin=\"{}\"), index: {}, value: {}, ptr: {}, opt: {}!\n")(codeStr, m_Factory.libraryName, index, mpt::ufmt::PTR(value), mpt::ufmt::PTR(ptr), mpt::ufmt::flt(opt, 3))); + } +#endif + if(!m_Effect.dispatcher) + { + return 0; + } + intptr_t result = 0; + { + DWORD exception = SETryOrError([&](){ result = m_Effect.dispatcher(&m_Effect, opCode, index, value, ptr, opt); }); + if(exception) + { + mpt::ustring codeStr; + if(opCode < mpt::saturate_cast<int32>(std::size(VstOpCodes))) + { + codeStr = mpt::ToUnicode(mpt::Charset::ASCII, VstOpCodes[opCode]); + } else + { + codeStr = mpt::ufmt::val(opCode); + } + ReportPlugException(MPT_UFORMAT("Exception {} in Dispatch({})")(mpt::ufmt::HEX0<8>(exception), codeStr)); + } + } + return result; +} + + +int32 CVstPlugin::GetCurrentProgram() +{ + if(m_Effect.numPrograms > 0) + { + return static_cast<int32>(Dispatch(effGetProgram, 0, 0, nullptr, 0)); + } + return 0; +} + + +CString CVstPlugin::GetCurrentProgramName() +{ + std::vector<char> s(256, 0); + // kVstMaxProgNameLen is 24... too short for some plugins, so use at least 256 bytes. + Dispatch(effGetProgramName, 0, 0, s.data(), 0); + return mpt::ToCString(mpt::Charset::Locale, s.data()); +} + + +void CVstPlugin::SetCurrentProgramName(const CString &name) +{ + Dispatch(effSetProgramName, 0, 0, const_cast<char *>(mpt::ToCharset(mpt::Charset::Locale, name.Left(kVstMaxProgNameLen)).c_str()), 0.0f); +} + + +CString CVstPlugin::GetProgramName(int32 program) +{ + // kVstMaxProgNameLen is 24... too short for some plugins, so use at least 256 bytes. + std::vector<char> rawname(256, 0); + if(program < m_Effect.numPrograms) + { + if(Dispatch(effGetProgramNameIndexed, program, -1 /*category*/, rawname.data(), 0) != 1) + { + // Fallback: Try to get current program name. + rawname.assign(256, 0); + int32 curProg = GetCurrentProgram(); + if(program != curProg) + { + SetCurrentProgram(program); + } + Dispatch(effGetProgramName, 0, 0, rawname.data(), 0); + if(program != curProg) + { + SetCurrentProgram(curProg); + } + } + } + return mpt::ToCString(mpt::Charset::Locale, rawname.data()); +} + + +void CVstPlugin::SetCurrentProgram(int32 nIndex) +{ + if(m_Effect.numPrograms > 0) + { + if(nIndex < m_Effect.numPrograms) + { + BeginSetProgram(nIndex); + EndSetProgram(); + } + } +} + + +void CVstPlugin::BeginSetProgram(int32 program) +{ + Dispatch(effBeginSetProgram, 0, 0, nullptr, 0); + if(program != -1) + Dispatch(effSetProgram, 0, program, nullptr, 0); +} + + +void CVstPlugin::EndSetProgram() +{ + Dispatch(effEndSetProgram, 0, 0, nullptr, 0); +} + + +void CVstPlugin::BeginGetProgram(int32 program) +{ + if(program != -1) + Dispatch(effSetProgram, 0, program, nullptr, 0); + if(isBridged) + Dispatch(effVendorSpecific, kVendorOpenMPT, kBeginGetProgram, nullptr, 0); +} + + +void CVstPlugin::EndGetProgram() +{ + if(isBridged) + Dispatch(effVendorSpecific, kVendorOpenMPT, kEndGetProgram, nullptr, 0); +} + + +PlugParamValue CVstPlugin::GetParameter(PlugParamIndex nIndex) +{ + float fResult = 0; + if(nIndex < m_Effect.numParams && m_Effect.getParameter != nullptr) + { + DWORD exception = SETryOrError([&](){ fResult = m_Effect.getParameter(&m_Effect, nIndex); }); + if(exception) + { + //ReportPlugException(U_("Exception in getParameter (Plugin=\"{}\")!\n"), m_Factory.szLibraryName); + } + } + return fResult; +} + + +void CVstPlugin::SetParameter(PlugParamIndex nIndex, PlugParamValue fValue) +{ + DWORD exception = 0; + if(nIndex < m_Effect.numParams && m_Effect.setParameter) + { + exception = SETryOrError([&](){ m_Effect.setParameter(&m_Effect, nIndex, fValue); }); + } + ResetSilence(); + if(exception) + { + //ReportPlugException(mpt::format(U_("Exception in SetParameter({}, {})!"))(nIndex, fValue)); + } +} + + +// Helper function for retreiving parameter name / label / display +CString CVstPlugin::GetParamPropertyString(PlugParamIndex param, Vst::VstOpcodeToPlugin opcode) +{ + if(m_Effect.numParams > 0 && param < m_Effect.numParams) + { + // Increased to 256 bytes since SynthMaster 2.8 writes more than 64 bytes of 0-padding. Kind of ridiculous if you consider that kVstMaxParamStrLen = 8... + std::vector<char> s(256, 0); + Dispatch(opcode, param, 0, s.data(), 0); + return mpt::ToCString(mpt::Charset::Locale, s.data()); + } + return CString(); +} + + +CString CVstPlugin::GetParamName(PlugParamIndex param) +{ + VstParameterProperties properties{}; + if(param < m_Effect.numParams && Dispatch(effGetParameterProperties, param, 0, &properties, 0.0f) == 1) + { + mpt::String::SetNullTerminator(properties.label); + return mpt::ToCString(mpt::Charset::Locale, properties.label); + } else + { + return GetParamPropertyString(param, effGetParamName); + } +} + + +CString CVstPlugin::GetDefaultEffectName() +{ + if(m_isVst2) + { + std::vector<char> s(256, 0); + Dispatch(effGetEffectName, 0, 0, s.data(), 0); + return mpt::ToCString(mpt::Charset::Locale, s.data()); + } + return CString(); +} + + +void CVstPlugin::Resume() +{ + const uint32 sampleRate = m_SndFile.GetSampleRate(); + + //reset some stuff + m_MixState.nVolDecayL = 0; + m_MixState.nVolDecayR = 0; + if(m_isResumed) + { + Dispatch(effStopProcess, 0, 0, nullptr, 0.0f); + Dispatch(effMainsChanged, 0, 0, nullptr, 0.0f); // calls plugin's suspend + } + if (sampleRate != m_nSampleRate) + { + m_nSampleRate = sampleRate; + Dispatch(effSetSampleRate, 0, 0, nullptr, static_cast<float>(m_nSampleRate)); + } + Dispatch(effSetBlockSize, 0, MIXBUFFERSIZE, nullptr, 0.0f); + //start off some stuff + Dispatch(effMainsChanged, 0, 1, nullptr, 0.0f); // calls plugin's resume + Dispatch(effStartProcess, 0, 0, nullptr, 0.0f); + m_isResumed = true; +} + + +void CVstPlugin::Suspend() +{ + if(m_isResumed) + { + Dispatch(effStopProcess, 0, 0, nullptr, 0.0f); + Dispatch(effMainsChanged, 0, 0, nullptr, 0.0f); // calls plugin's suspend (theoretically, plugins should clean their buffers here, but oh well, the number of plugins which don't do this is surprisingly high.) + m_isResumed = false; + } +} + + +// Send events to plugin. Returns true if there are events left to be processed. +void CVstPlugin::ProcessVSTEvents() +{ + // Process VST events + if(m_Effect.dispatcher != nullptr && vstEvents.Finalise() > 0) + { + DWORD exception = SETryOrError([&](){ m_Effect.dispatcher(&m_Effect, effProcessEvents, 0, 0, &vstEvents, 0); }); + ResetSilence(); + if(exception) + { + ReportPlugException(MPT_UFORMAT("Exception {} in ProcessVSTEvents(numEvents:{})!")( + mpt::ufmt::HEX0<8>(exception), + vstEvents.size())); + } + } +} + + +// Receive events from plugin and send them to the next plugin in the chain. +void CVstPlugin::ReceiveVSTEvents(const VstEvents *events) +{ + if(m_pMixStruct == nullptr) + { + return; + } + + ResetSilence(); + + // I think we should only route events to plugins that are explicitely specified as output plugins of the current plugin. + // This should probably use GetOutputPlugList here if we ever get to support multiple output plugins. + PLUGINDEX receiver = m_pMixStruct->GetOutputPlugin(); + + if(receiver != PLUGINDEX_INVALID) + { + IMixPlugin *plugin = m_SndFile.m_MixPlugins[receiver].pMixPlugin; + CVstPlugin *vstPlugin = dynamic_cast<CVstPlugin *>(plugin); + // Add all events to the plugin's queue. + for(const auto &ev : *events) + { + if(vstPlugin != nullptr) + { + // Directly enqueue the message and preserve as much of the event data as possible (e.g. delta frames, which are currently not used by OpenMPT but might be by plugins) + vstPlugin->vstEvents.Enqueue(ev); + } else if(plugin != nullptr) + { + if(ev->type == kVstMidiType) + { + plugin->MidiSend(static_cast<const VstMidiEvent *>(ev)->midiData); + } else if(ev->type == kVstSysExType) + { + auto event = static_cast<const VstMidiSysexEvent *>(ev); + plugin->MidiSysexSend(mpt::as_span(mpt::byte_cast<const std::byte *>(event->sysexDump), event->dumpBytes)); + } + } + } + } + +#ifdef MODPLUG_TRACKER + if(m_recordMIDIOut) + { + // Spam MIDI data to all views + for(const auto &ev : *events) + { + if(ev->type == kVstMidiType) + { + VstMidiEvent *event = static_cast<VstMidiEvent *>(ev); + ::SendNotifyMessage(CMainFrame::GetMainFrame()->GetMidiRecordWnd(), WM_MOD_MIDIMSG, event->midiData, reinterpret_cast<LPARAM>(this)); + } + } + } +#endif // MODPLUG_TRACKER +} + + +void CVstPlugin::Process(float *pOutL, float *pOutR, uint32 numFrames) +{ + ProcessVSTEvents(); + + // If the plugin is found & ok, continue + if(m_pProcessFP != nullptr && m_mixBuffer.Ok()) + { + int32 numInputs = m_Effect.numInputs, numOutputs = m_Effect.numOutputs; + //RecalculateGain(); + + // Merge stereo input before sending to the plugin if it can only handle one input. + if (numInputs == 1) + { + float *plugInputL = m_mixBuffer.GetInputBuffer(0); + float *plugInputR = m_mixBuffer.GetInputBuffer(1); + for (uint32 i = 0; i < numFrames; i++) + { + plugInputL[i] = 0.5f * (plugInputL[i] + plugInputR[i]); + } + } + + float **outputBuffers = m_mixBuffer.GetOutputBufferArray(); + if(!isBridged) + { + m_mixBuffer.ClearOutputBuffers(numFrames); + } + + // Do the VST processing magic + MPT_ASSERT(numFrames <= MIXBUFFERSIZE); + { + DWORD exception = SETryOrError([&](){ m_pProcessFP(&m_Effect, m_mixBuffer.GetInputBufferArray(), outputBuffers, numFrames); }); + if(exception) + { + Bypass(); + mpt::ustring processMethod = (m_Effect.flags & effFlagsCanReplacing) ? U_("processReplacing") : U_("process"); + ReportPlugException(MPT_UFORMAT("The plugin threw an exception ({}) in {}. It has automatically been set to \"Bypass\".")(mpt::ufmt::HEX0<8>(exception), processMethod)); + } + } + + // Mix outputs of multi-output VSTs: + if(numOutputs > 2) + { + MPT_ASSERT(outputBuffers != nullptr); + // first, mix extra outputs on a stereo basis + int32 outs = numOutputs; + // so if nOuts is not even, let process the last output later + if((outs % 2u) == 1) outs--; + + // mix extra stereo outputs + for(int32 iOut = 2; iOut < outs; iOut++) + { + for(uint32 i = 0; i < numFrames; i++) + { + outputBuffers[iOut % 2u][i] += outputBuffers[iOut][i]; // assumed stereo. + } + } + + // if m_Effect.numOutputs is odd, mix half the signal of last output to each channel + if(outs != numOutputs) + { + // trick : if we are here, numOutputs = m_Effect.numOutputs - 1 !!! + for(uint32 i = 0; i < numFrames; i++) + { + float v = 0.5f * outputBuffers[outs][i]; + outputBuffers[0][i] += v; + outputBuffers[1][i] += v; + } + } + } + + if(numOutputs != 0) + { + MPT_ASSERT(outputBuffers != nullptr); + ProcessMixOps(pOutL, pOutR, outputBuffers[0], outputBuffers[numOutputs > 1 ? 1 : 0], numFrames); + } + + // If the I/O format of the bridge changed in the meanwhile, update it now. + if(isBridged && Dispatch(effVendorSpecific, kVendorOpenMPT, kCloseOldProcessingMemory, nullptr, 0.0f) != 0) + { + InitializeIOBuffers(); + } + } + + vstEvents.Clear(); + m_positionChanged = false; +} + + +bool CVstPlugin::MidiSend(uint32 dwMidiCode) +{ + // Note-Offs go at the start of the queue (since OpenMPT 1.17). Needed for situations like this: + // ... ..|C-5 01 + // C-5 01|=== .. + // TODO: Should not be used with real-time notes! Letting the key go too quickly + // (e.g. while output device is being initalized) will cause the note to be stuck! + bool insertAtFront = (MIDIEvents::GetTypeFromEvent(dwMidiCode) == MIDIEvents::evNoteOff); + + VstMidiEvent event{}; + event.type = kVstMidiType; + event.byteSize = sizeof(event); + event.midiData = dwMidiCode; + + ResetSilence(); + return vstEvents.Enqueue(&event, insertAtFront); +} + + +bool CVstPlugin::MidiSysexSend(mpt::const_byte_span sysex) +{ + VstMidiSysexEvent event{}; + event.type = kVstSysExType; + event.byteSize = sizeof(event); + event.dumpBytes = mpt::saturate_cast<int32>(sysex.size()); + event.sysexDump = sysex.data(); // We will make our own copy in VstEventQueue::Enqueue + + ResetSilence(); + return vstEvents.Enqueue(&event); +} + + +void CVstPlugin::HardAllNotesOff() +{ + constexpr uint32 SCRATCH_BUFFER_SIZE = 64; + float out[2][SCRATCH_BUFFER_SIZE]; // scratch buffers + + // The JUCE framework doesn't like processing while being suspended. + const bool wasSuspended = !IsResumed(); + if(wasSuspended) + { + Resume(); + } + + const bool isWavestation = GetUID() == FourCC("KLWV"); + const bool isSawer = GetUID() == FourCC("SaWR"); + for(uint8 mc = 0; mc < m_MidiCh.size(); mc++) + { + PlugInstrChannel &channel = m_MidiCh[mc]; + channel.ResetProgram(); + + SendMidiPitchBend(mc, EncodePitchBendParam(MIDIEvents::pitchBendCentre)); // centre pitch bend + + if(!isWavestation && !isSawer) + { + // Korg Wavestation doesn't seem to like this CC, it can introduce ghost notes or + // prevent new notes from being played. + // Image-Line Sawer does not like it either and resets some parameters so that the plugin is all + // distorted afterwards. + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllControllersOff, mc, 0)); + } + if(!isSawer) + { + // Image-Line Sawer takes ages to execute this CC. + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllNotesOff, mc, 0)); + } + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllSoundOff, mc, 0)); + + for(std::size_t i = 0; i < std::size(channel.noteOnMap); i++) //all notes + { + for(auto &c : channel.noteOnMap[i]) + { + while(c != 0) + { + MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0)); + c--; + } + } + } + } + // Let plugin process events + while(vstEvents.GetNumQueuedEvents() > 0) + { + Process(out[0], out[1], SCRATCH_BUFFER_SIZE); + } + + if(wasSuspended) + { + Suspend(); + } +} + + +void CVstPlugin::SaveAllParameters() +{ + if(m_pMixStruct == nullptr) + { + return; + } + m_pMixStruct->defaultProgram = -1; + + if(ProgramsAreChunks()) + { + void *p = nullptr; + + // Try to get whole bank + intptr_t byteSize = Dispatch(effGetChunk, 0, 0, &p, 0); + + if (!p) + { + // Getting bank failed, try to get just preset + byteSize = Dispatch(effGetChunk, 1, 0, &p, 0); + } else + { + // We managed to get the bank, now we need to remember which program we're on. + m_pMixStruct->defaultProgram = GetCurrentProgram(); + } + if (p != nullptr) + { + LimitMax(byteSize, Util::MaxValueOfType(byteSize) - 4); + try + { + m_pMixStruct->pluginData.resize(byteSize + 4); + auto data = m_pMixStruct->pluginData.data(); + memcpy(data, "fEvN", 4); // 'NvEf', return value of deprecated effIdentify call + memcpy(data + 4, p, byteSize); + return; + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + } + } + } + // This plugin doesn't support chunks: save parameters + IMixPlugin::SaveAllParameters(); +} + + +void CVstPlugin::RestoreAllParameters(int32 program) +{ + if(m_pMixStruct != nullptr && m_pMixStruct->pluginData.size() >= 4) + { + auto data = m_pMixStruct->pluginData.data(); + if (!memcmp(data, "fEvN", 4)) // 'NvEf', return value of deprecated effIdentify call + { + if ((program>=0) && (program < m_Effect.numPrograms)) + { + // Bank + Dispatch(effSetChunk, 0, m_pMixStruct->pluginData.size() - 4, data + 4, 0); + SetCurrentProgram(program); + } else + { + // Program + BeginSetProgram(-1); + Dispatch(effSetChunk, 1, m_pMixStruct->pluginData.size() - 4, data + 4, 0); + EndSetProgram(); + } + } else + { + IMixPlugin::RestoreAllParameters(program); + } + } +} + + +CAbstractVstEditor *CVstPlugin::OpenEditor() +{ + try + { + if(HasEditor()) + return new COwnerVstEditor(*this); + else + return new CDefaultVstEditor(*this); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + ReportPlugException(U_("Exception in OpenEditor()")); + return nullptr; + } +} + + +void CVstPlugin::Bypass(bool bypass) +{ + Dispatch(effSetBypass, bypass ? 1 : 0, 0, nullptr, 0.0f); + IMixPlugin::Bypass(bypass); +} + + +void CVstPlugin::NotifySongPlaying(bool playing) +{ + m_isSongPlaying = playing; +} + + +bool CVstPlugin::IsInstrument() const +{ + return ((m_Effect.flags & effFlagsIsSynth) || (!m_Effect.numInputs)); +} + + +bool CVstPlugin::CanRecieveMidiEvents() +{ + return Dispatch(effCanDo, 0, 0, const_cast<char *>(PluginCanDo::receiveVstMidiEvent), 0.0f) != 0; +} + + +void CVstPlugin::ReportPlugException(const mpt::ustring &text) const +{ + CVstPluginManager::ReportPlugException(MPT_UFORMAT("{} (Plugin: {})")(text, m_Factory.libraryName)); +} + + +// Cache program names for plugin bridge +void CVstPlugin::CacheProgramNames(int32 firstProg, int32 lastProg) +{ + if(isBridged) + { + int32 offsets[2] = { firstProg, lastProg }; + Dispatch(effVendorSpecific, kVendorOpenMPT, kCacheProgramNames, offsets, 0.0f); + } +} + + +// Cache parameter names for plugin bridge +void CVstPlugin::CacheParameterNames(int32 firstParam, int32 lastParam) +{ + if(isBridged) + { + int32 offsets[2] = { firstParam, lastParam }; + Dispatch(effVendorSpecific, kVendorOpenMPT, kCacheParameterInfo, offsets, 0.0f); + } +} + + +IMixPlugin::ChunkData CVstPlugin::GetChunk(bool isBank) +{ + std::byte *chunk = nullptr; + auto size = Dispatch(effGetChunk, isBank ? 0 : 1, 0, &chunk, 0); + if(chunk == nullptr) + { + size = 0; + } + return ChunkData(chunk, size); +} + + +void CVstPlugin::SetChunk(const ChunkData &chunk, bool isBank) +{ + Dispatch(effSetChunk, isBank ? 0 : 1, chunk.size(), const_cast<std::byte *>(chunk.data()), 0); +} + + +OPENMPT_NAMESPACE_END + +#endif // MPT_WITH_VST diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.h b/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.h new file mode 100644 index 00000000..1f49a94c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/Vstplug.h @@ -0,0 +1,185 @@ +/* + * Vstplug.h + * --------- + * Purpose: Plugin handling (loading and processing plugins) + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifdef MPT_WITH_VST + +#include "../soundlib/Snd_defs.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../soundlib/Mixer.h" +#include "plugins/VstDefinitions.h" +#include "plugins/VstEventQueue.h" +#if defined(MODPLUG_TRACKER) +#include "ExceptionHandler.h" +#endif // MODPLUG_TRACKER + + +OPENMPT_NAMESPACE_BEGIN + + +class CSoundFile; +struct SNDMIXPLUGIN; +struct VSTPluginLib; + + +class CVstPlugin final : public IMidiPlugin +{ +protected: + + bool m_maskCrashes; + HMODULE m_hLibrary; + Vst::AEffect &m_Effect; + Vst::ProcessProc m_pProcessFP = nullptr; // Function pointer to AEffect processReplacing if supported, else process. + + double lastBarStartPos = -1.0; + uint32 m_nSampleRate; + + bool m_isVst2 : 1; + bool m_isInstrument : 1; + bool m_isInitialized : 1; + bool m_needIdle : 1; + bool m_positionChanged : 1; + + VstEventQueue vstEvents; // MIDI events that should be sent to the plugin + + Vst::VstTimeInfo timeInfo; + +public: + + const bool isBridged : 1; // True if our built-in plugin bridge is being used. + +private: + +#if defined(MODPLUG_TRACKER) + ExceptionHandler::Context m_Ectx; +#endif // MODPLUG_TRACKER + +public: + bool MaskCrashes() noexcept; + +public: + template <typename Tfn> static DWORD SETryOrError(bool maskCrashes, Tfn fn); + +private: + template <typename Tfn> DWORD SETryOrError(Tfn fn); + +public: + CVstPlugin(bool maskCrashes, HMODULE hLibrary, VSTPluginLib &factory, SNDMIXPLUGIN &mixPlugin, Vst::AEffect &effect, CSoundFile &sndFile); + ~CVstPlugin(); + + enum class BridgeMode + { + Automatic, + ForceBridgeWithFallback, + DetectRequiredBridgeMode, + }; + + static Vst::AEffect *LoadPlugin(bool maskCrashes, VSTPluginLib &plugin, HMODULE &library, BridgeMode bridgeMode); + +protected: + void Initialize(); + +public: + int32 GetUID() const override; + int32 GetVersion() const override; + void Idle() override; + uint32 GetLatency() const override { return m_Effect.initialDelay; } + + // Check if programs should be stored as chunks or parameters + bool ProgramsAreChunks() const override { return (m_Effect.flags & Vst::effFlagsProgramChunks) != 0; } + ChunkData GetChunk(bool isBank) override; + void SetChunk(const ChunkData &chunk, bool isBank) override; + // If true, the plugin produces an output even if silence is being fed into it. + //bool ShouldProcessSilence() { return IsInstrument() || ((m_Effect.flags & effFlagsNoSoundInStop) == 0 && Dispatch(effGetTailSize, 0, 0, nullptr, 0.0f) != 1) override; } + // Old JUCE versions set effFlagsNoSoundInStop even when the shouldn't (see various ValhallaDSP reverb plugins). While the user cannot change the plugin bypass setting manually yet, play safe with VST plugins and do not optimize. + bool ShouldProcessSilence() override { return true; } + + int32 GetNumPrograms() const override; + int32 GetCurrentProgram() override; + void SetCurrentProgram(int32 nIndex) override; + + PlugParamIndex GetNumParameters() const override; + PlugParamValue GetParameter(PlugParamIndex nIndex) override; + void SetParameter(PlugParamIndex nIndex, PlugParamValue fValue) override; + + CString GetCurrentProgramName() override; + void SetCurrentProgramName(const CString &name) override; + CString GetProgramName(int32 program) override; + + CString GetParamName(PlugParamIndex param) override; + CString GetParamLabel(PlugParamIndex param) override { return GetParamPropertyString(param, Vst::effGetParamLabel); }; + CString GetParamDisplay(PlugParamIndex param) override { return GetParamPropertyString(param, Vst::effGetParamDisplay); }; + + static intptr_t DispatchSEH(bool maskCrashes, Vst::AEffect *effect, Vst::VstOpcodeToPlugin opCode, int32 index, intptr_t value, void *ptr, float opt, unsigned long &exception); + intptr_t Dispatch(Vst::VstOpcodeToPlugin opCode, int32 index, intptr_t value, void *ptr, float opt); + + bool HasEditor() const override { return (m_Effect.flags & Vst::effFlagsHasEditor) != 0; } + CAbstractVstEditor *OpenEditor() override; + CString GetDefaultEffectName() override; + + void Bypass(bool bypass = true) override; + + bool IsInstrument() const override; + bool CanRecieveMidiEvents() override; + + void CacheProgramNames(int32 firstProg, int32 lastProg) override; + void CacheParameterNames(int32 firstParam, int32 lastParam) override; + +public: + void Release() override; + void SaveAllParameters() override; + void RestoreAllParameters(int32 program) override; + void Process(float *pOutL, float *pOutR, uint32 numFrames) override; + bool MidiSend(uint32 dwMidiCode) override; + bool MidiSysexSend(mpt::const_byte_span sysex) override; + void HardAllNotesOff() override; + void NotifySongPlaying(bool playing) override; + + void Resume() override; + void Suspend() override; + void PositionChanged() override { m_positionChanged = true; } + + // Check whether a VST parameter can be automated + bool CanAutomateParameter(PlugParamIndex index); + + int GetNumInputChannels() const override { return m_Effect.numInputs; } + int GetNumOutputChannels() const override { return m_Effect.numOutputs; } + + void BeginSetProgram(int32 program) override; + void EndSetProgram() override; + void BeginGetProgram(int32 program) override; + void EndGetProgram() override; + +protected: + // Helper function for retreiving parameter name / label / display + CString GetParamPropertyString(PlugParamIndex param, Vst::VstOpcodeToPlugin opcode); + + // Set up input / output buffers. + bool InitializeIOBuffers(); + + // Process incoming and outgoing VST events. + void ProcessVSTEvents(); + void ReceiveVSTEvents(const Vst::VstEvents *events); + + void ReportPlugException(const mpt::ustring &text) const; + +public: + static intptr_t VSTCALLBACK MasterCallBack(Vst::AEffect *effect, Vst::VstOpcodeToHost opcode, int32 index, intptr_t value, void *ptr, float opt); +protected: + intptr_t VstFileSelector(bool destructor, Vst::VstFileSelect &fileSel); +}; + + +OPENMPT_NAMESPACE_END + +#endif // MPT_WITH_VST
\ No newline at end of file diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.cpp new file mode 100644 index 00000000..fa589f57 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.cpp @@ -0,0 +1,202 @@ +/* + * WelcomeDialog.cpp + * ----------------- + * Purpose: "First run" OpenMPT welcome dialog + * 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 "WelcomeDialog.h" +#include "resource.h" +#include "Mainfrm.h" +#include "../common/mptStringBuffer.h" +#include "InputHandler.h" +#include "CommandSet.h" +#include "SelectPluginDialog.h" +#include "UpdateCheck.h" + + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(WelcomeDlg, CDialog) + ON_COMMAND(IDC_BUTTON1, &WelcomeDlg::OnOptions) + ON_COMMAND(IDC_BUTTON2, &WelcomeDlg::OnScanPlugins) +END_MESSAGE_MAP() + + +WelcomeDlg::WelcomeDlg(CWnd *parent) +{ + Create(IDD_WECLOME, parent); + CenterWindow(parent); +} + + +static mpt::PathString GetFullKeyPath(const char *keyFile) +{ + return theApp.GetInstallPkgPath() + P_("extraKeymaps\\") + mpt::PathString::FromUTF8(keyFile) + P_(".mkb"); +} + + +BOOL WelcomeDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + +#ifdef MPT_WITH_VST + HKEY hkEnum = NULL; + TCHAR str[MAX_PATH]; + DWORD datasize = sizeof(str); + DWORD datatype = REG_SZ; + if(RegOpenKey(HKEY_LOCAL_MACHINE, _T("Software\\VST"), &hkEnum) == ERROR_SUCCESS + && RegQueryValueEx(hkEnum, _T("VSTPluginsPath"), 0, &datatype, (LPBYTE)str, &datasize) == ERROR_SUCCESS) + { + m_vstPath = mpt::PathString::FromNative(ParseMaybeNullTerminatedStringFromBufferWithSizeInBytes<mpt::winstring>(str, datasize)); + } else if(SHGetSpecialFolderPath(0, str, CSIDL_PROGRAM_FILES, FALSE)) + { + m_vstPath = mpt::PathString::FromNative(ParseMaybeNullTerminatedStringFromBufferWithSizeInBytes<mpt::winstring>(str, datasize)) + P_("\\Steinberg\\VstPlugins\\"); + if(!m_vstPath.IsDirectory()) + { + m_vstPath = mpt::PathString(); + } + } + SetDlgItemText(IDC_EDIT2, mpt::ToCString(TrackerSettings::Instance().defaultArtist)); + if(!m_vstPath.empty()) + { + SetDlgItemText(IDC_EDIT1, m_vstPath.AsNative().c_str()); + if(TrackerSettings::Instance().PathPlugins.GetDefaultDir().empty()) + { + TrackerSettings::Instance().PathPlugins.SetDefaultDir(m_vstPath); + } + } else +#endif // MPT_WITH_VST + { + SetDlgItemText(IDC_EDIT1, _T("No plugin path found!")); + GetDlgItem(IDC_BUTTON2)->EnableWindow(FALSE); + } + + const char *keyFile = nullptr; + const TCHAR *keyFileName = nullptr; + const uint16 language = LOWORD(GetKeyboardLayout(0)), primaryLang = language & 0x3FF; + CComboBox *combo = (CComboBox *)GetDlgItem(IDC_COMBO1); + combo->AddString(_T("OpenMPT / Chromatic (Default)")); + combo->SetCurSel(0); + switch(primaryLang) + { + case LANG_GERMAN: + keyFile = "DE_jojo"; + keyFileName = _T("German"); + break; + case LANG_SPANISH: + // Spanish latin-american keymap, so we ignore Spain. + if(language != SUBLANG_SPANISH_MODERN && language != SUBLANG_SPANISH) + { + keyFile = "es-LA_mpt_(jmkz)"; + keyFileName = _T("Spanish"); + } + break; + case LANG_FRENCH: + keyFile = "FR_mpt_(legovitch)"; + keyFileName = _T("French"); + break; + case LANG_NORWEGIAN: + keyFile = "NO_mpt_classic_(rakib)"; + keyFileName = _T("Norwegian"); + break; + } + if(keyFile != nullptr) + { + if(GetFullKeyPath(keyFile).IsFile()) + { + int i = combo->AddString(_T("OpenMPT / Chromatic (") + CString(keyFileName) + _T(")")); + combo->SetItemDataPtr(i, (void *)keyFile); + combo->SetCurSel(i); + + // As this is presented as the default, load it right now, even if the user closes the dialog through the close button + auto cmdSet = std::make_unique<CCommandSet>(); + cmdSet->LoadFile(GetFullKeyPath(keyFile)); + CMainFrame::GetInputHandler()->SetNewCommandSet(cmdSet.get()); + } + } + combo->SetItemDataPtr(combo->AddString(_T("Impulse Tracker")), (void*)("US_mpt-it2_classic")); + combo->SetItemDataPtr(combo->AddString(_T("FastTracker 2")), (void*)("US_mpt-ft2_classic")); + + CheckDlgButton(IDC_CHECK1, BST_CHECKED); + CheckDlgButton(IDC_CHECK3, BST_CHECKED); +#if defined(MPT_ENABLE_UPDATE) + GetDlgItem(IDC_STATIC_WELCOME_STATISTICS)->SetWindowText(mpt::ToCString(mpt::String::Replace(CUpdateCheck::GetStatisticsUserInformation(false), U_("\n"), U_(" ")))); +#endif // MPT_ENABLE_UPDATE + CheckDlgButton(IDC_CHECK2, (TrackerSettings::Instance().patternFont.Get().name == PATTERNFONT_LARGE) ? BST_CHECKED : BST_UNCHECKED); + + ShowWindow(SW_SHOW); + + return TRUE; +} + + +void WelcomeDlg::OnOptions() +{ + OnOK(); + CMainFrame::GetMainFrame()->PostMessage(WM_COMMAND, ID_VIEW_OPTIONS); +} + + +void WelcomeDlg::OnScanPlugins() +{ +#ifdef MPT_WITH_VST + CSelectPluginDlg::ScanPlugins(m_vstPath, this); +#endif // MPT_WITH_VST +} + + +void WelcomeDlg::OnOK() +{ + CDialog::OnOK(); + +#if defined(MPT_ENABLE_UPDATE) + bool runUpdates = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + TrackerSettings::Instance().UpdateIntervalDays = (runUpdates ? 7 : -1); + TrackerSettings::Instance().UpdateStatistics = (IsDlgButtonChecked(IDC_CHECK3) != BST_UNCHECKED); + TrackerSettings::Instance().UpdateShowUpdateHint = false; + TrackerSettings::Instance().UpdateStatisticsConsentAsked = true; + + CString artistName; + GetDlgItemText(IDC_EDIT2, artistName); + TrackerSettings::Instance().defaultArtist = mpt::ToUnicode(artistName); + +#endif // MPT_ENABLE_UPDATE + if(IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED) + { + FontSetting font = TrackerSettings::Instance().patternFont; + font.name = PATTERNFONT_LARGE; + TrackerSettings::Instance().patternFont = font; + } + + CComboBox *combo = (CComboBox *)GetDlgItem(IDC_COMBO1); + const char *keyFile = static_cast<char *>(combo->GetItemDataPtr(combo->GetCurSel())); + auto cmdSet = std::make_unique<CCommandSet>(); + if(keyFile != nullptr) + cmdSet->LoadFile(GetFullKeyPath(keyFile)); + else + cmdSet->LoadDefaultKeymap(); + CMainFrame::GetInputHandler()->SetNewCommandSet(cmdSet.get()); + +#if defined(MPT_ENABLE_UPDATE) + if(runUpdates) + { + CUpdateCheck::DoAutoUpdateCheck(); + } +#endif // MPT_ENABLE_UPDATE + CMainFrame::GetMainFrame()->PostMessage(WM_MOD_INVALIDATEPATTERNS, HINT_MPTOPTIONS); + + DestroyWindow(); +} + +void WelcomeDlg::OnCancel() +{ + CDialog::OnCancel(); + DestroyWindow(); +} + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.h b/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.h new file mode 100644 index 00000000..ad25e10e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/WelcomeDialog.h @@ -0,0 +1,39 @@ +/* + * WelcomeDialog.cpp + * ----------------- + * Purpose: "First run" OpenMPT welcome dialog + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../common/mptPathString.h" + +OPENMPT_NAMESPACE_BEGIN + +class WelcomeDlg : public CDialog +{ +protected: + mpt::PathString m_vstPath; + +public: + WelcomeDlg(CWnd *parent); + +protected: + BOOL OnInitDialog() override; + void OnOK() override; + void OnCancel() override; + void PostNcDestroy() override { CDialog::PostNcDestroy(); delete this; } + + afx_msg void OnOptions(); + afx_msg void OnScanPlugins(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.cpp new file mode 100644 index 00000000..1b434c4e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.cpp @@ -0,0 +1,388 @@ +/* + * WineSoundDeviceStub.cpp + * ----------------------- + * Purpose: Stub sound device driver class connection to WineSupport Wrapper. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#include "stdafx.h" + +#if MPT_COMPILER_MSVC +#pragma warning(disable:4800) // 'T' : forcing value to bool 'true' or 'false' (performance warning) +#endif // MPT_COMPILER_MSVC + +#include "WineSoundDeviceStub.h" + +#include "openmpt/sounddevice/SoundDevice.hpp" + +#include "../common/misc_util.h" + +#include "MPTrackWine.h" +#include "wine/NativeSoundDeviceMarshalling.h" + + + +OPENMPT_NAMESPACE_BEGIN + + +namespace SoundDevice { + + +static mpt::ustring GetTypePrefix() +{ + return U_("Wine"); +} + +static SoundDevice::Info AddTypePrefix(SoundDevice::Info info) +{ + info.type = GetTypePrefix() + U_("-") + info.type; + info.apiPath.insert(info.apiPath.begin(), U_("Wine")); + return info; +} + +static SoundDevice::Info RemoveTypePrefix(SoundDevice::Info info) +{ + info.type = info.type.substr(GetTypePrefix().length() + 1); + info.apiPath.erase(info.apiPath.begin()); + return info; +} + + +std::vector<SoundDevice::Info> SoundDeviceStub::EnumerateDevices(ILogger &logger, SoundDevice::SysInfo sysInfo) +{ + ComponentHandle<ComponentWineWrapper> WineWrapper; + if(!IsComponentAvailable(WineWrapper)) + { + return std::vector<SoundDevice::Info>(); + } + MPT_UNREFERENCED_PARAMETER(logger); + MPT_UNREFERENCED_PARAMETER(sysInfo); // we do not want to pass this to the native layer because it would actually be totally wrong + std::vector<SoundDevice::Info> result = json_cast<std::vector<SoundDevice::Info> >(WineWrapper->SoundDevice_EnumerateDevices()); + for(auto &info : result) + { + info = AddTypePrefix(info); + } + return result; +} + +SoundDeviceStub::SoundDeviceStub(ILogger &logger, SoundDevice::Info info, SoundDevice::SysInfo sysInfo) + : impl(nullptr) +{ + MPT_UNREFERENCED_PARAMETER(logger); + MPT_UNREFERENCED_PARAMETER(sysInfo); // we do not want to pass this to the native layer because it would actually be totally wrong + info = RemoveTypePrefix(info); + impl = w->OpenMPT_Wine_Wrapper_SoundDevice_Construct(json_cast<std::string>(info).c_str()); +} + +SoundDeviceStub::~SoundDeviceStub() { + if(impl) + { + w->OpenMPT_Wine_Wrapper_SoundDevice_Destruct(impl); + impl = nullptr; + } +} + +static void __cdecl SoundDevice_MessageReceiver_SoundDeviceMessage(void * inst, uintptr_t level, const char * message) +{ + SoundDevice::IMessageReceiver * mr = (SoundDevice::IMessageReceiver*)inst; + if(!mr) + { + return; + } + mr->SoundDeviceMessage((LogLevel)level, mpt::ToUnicode(mpt::Charset::UTF8, message ? message : "")); +} + +void SoundDeviceStub::SetMessageReceiver(SoundDevice::IMessageReceiver *receiver) { + OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver messageReceiver = {}; + messageReceiver.inst = receiver; + messageReceiver.SoundDeviceMessageFunc = &SoundDevice_MessageReceiver_SoundDeviceMessage; + return w->OpenMPT_Wine_Wrapper_SoundDevice_SetMessageReceiver(impl, &messageReceiver); +} + +static void __cdecl SoundCallbackGetReferenceClockNowNanosecondsFunc( void * inst, uint64_t * result ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + *result = 0; + return; + } + *result = callback->SoundCallbackGetReferenceClockNowNanoseconds(); +} +static void __cdecl SoundCallbackPreStartFunc( void * inst ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + return; + } + callback->SoundCallbackPreStart(); +} +static void __cdecl SoundCallbackPostStopFunc( void * inst ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + return; + } + callback->SoundCallbackPostStop(); +} +static void __cdecl SoundCallbackIsLockedByCurrentThreadFunc( void * inst, uintptr_t * result ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + *result = 0; + return; + } + *result = callback->SoundCallbackIsLockedByCurrentThread(); +} +static void __cdecl SoundCallbackLockFunc( void * inst ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + return; + } + callback->SoundCallbackLock(); +} +static void __cdecl SoundCallbackLockedGetReferenceClockNowNanosecondsFunc( void * inst, uint64_t * result ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + *result = 0; + return; + } + *result = callback->SoundCallbackLockedGetReferenceClockNowNanoseconds(); +} +static void __cdecl SoundCallbackLockedProcessPrepareFunc( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::TimeInfo ti = C::decode(*timeInfo); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcessPrepare(ti); +} +static void __cdecl SoundCallbackLockedProcessUint8Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, uint8_t * buffer, const uint8_t * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessInt8Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int8_t * buffer, const int8_t * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessInt16Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int16_t * buffer, const int16_t * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessInt24Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, OpenMPT_int24 * buffer, const OpenMPT_int24 * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, static_cast<int24*>(buffer), static_cast<const int24*>(inputBuffer)); +} +static void __cdecl SoundCallbackLockedProcessInt32Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int32_t * buffer, const int32_t * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessFloatFunc( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, float * buffer, const float * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessDoubleFunc( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, double * buffer, const double * inputBuffer ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::BufferFormat bf = C::decode(*bufferFormat); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcess(bf, numFrames, buffer, inputBuffer); +} +static void __cdecl SoundCallbackLockedProcessDoneFunc( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + SoundDevice::TimeInfo ti = C::decode(*timeInfo); + if(!callback) + { + return; + } + callback->SoundCallbackLockedProcessDone(ti); +} +static void __cdecl SoundCallbackUnlockFunc( void * inst ) { + SoundDevice::ICallback * callback = ((SoundDevice::ICallback*)inst); + if(!callback) + { + return; + } + callback->SoundCallbackUnlock(); +} + +void SoundDeviceStub::SetCallback(SoundDevice::ICallback *icallback) { + OpenMPT_Wine_Wrapper_SoundDevice_ICallback callback = {}; + callback.inst = icallback; + callback.SoundCallbackGetReferenceClockNowNanosecondsFunc = &SoundCallbackGetReferenceClockNowNanosecondsFunc; + callback.SoundCallbackPreStartFunc = &SoundCallbackPreStartFunc; + callback.SoundCallbackPostStopFunc = &SoundCallbackPostStopFunc; + callback.SoundCallbackIsLockedByCurrentThreadFunc = &SoundCallbackIsLockedByCurrentThreadFunc; + callback.SoundCallbackLockFunc = &SoundCallbackLockFunc; + callback.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc = &SoundCallbackLockedGetReferenceClockNowNanosecondsFunc; + callback.SoundCallbackLockedProcessPrepareFunc = &SoundCallbackLockedProcessPrepareFunc; + callback.SoundCallbackLockedProcessUint8Func = &SoundCallbackLockedProcessUint8Func; + callback.SoundCallbackLockedProcessInt8Func = &SoundCallbackLockedProcessInt8Func; + callback.SoundCallbackLockedProcessInt16Func = &SoundCallbackLockedProcessInt16Func; + callback.SoundCallbackLockedProcessInt24Func = &SoundCallbackLockedProcessInt24Func; + callback.SoundCallbackLockedProcessInt32Func = &SoundCallbackLockedProcessInt32Func; + callback.SoundCallbackLockedProcessFloatFunc = &SoundCallbackLockedProcessFloatFunc; + callback.SoundCallbackLockedProcessDoubleFunc = &SoundCallbackLockedProcessDoubleFunc; + callback.SoundCallbackLockedProcessDoneFunc = &SoundCallbackLockedProcessDoneFunc; + callback.SoundCallbackUnlockFunc = &SoundCallbackUnlockFunc; + return w->OpenMPT_Wine_Wrapper_SoundDevice_SetCallback(impl, &callback); +} + +SoundDevice::Info SoundDeviceStub::GetDeviceInfo() const { + SoundDevice::Info info = json_cast<SoundDevice::Info>(w->result_as_string(w->OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceInfo(impl))); + info = AddTypePrefix(info); + return info; +} + +SoundDevice::Caps SoundDeviceStub::GetDeviceCaps() const { + return json_cast<SoundDevice::Caps>(w->result_as_string(w->OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceCaps(impl))); +} + +SoundDevice::DynamicCaps SoundDeviceStub::GetDeviceDynamicCaps(const std::vector<uint32> &baseSampleRates) { + return json_cast<SoundDevice::DynamicCaps>(w->result_as_string(w->OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceDynamicCaps(impl, json_cast<std::string>(baseSampleRates).c_str()))); +} + +bool SoundDeviceStub::Init(const SoundDevice::AppInfo &appInfo) { + return w->OpenMPT_Wine_Wrapper_SoundDevice_Init(impl, json_cast<std::string>(appInfo).c_str()); +} + +bool SoundDeviceStub::Open(const SoundDevice::Settings &settings) { + return w->OpenMPT_Wine_Wrapper_SoundDevice_Open(impl, json_cast<std::string>(settings).c_str()); +} + +bool SoundDeviceStub::Close() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_Close(impl); +} + +bool SoundDeviceStub::Start() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_Start(impl); +} + +void SoundDeviceStub::Stop() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_Stop(impl); +} + +FlagSet<RequestFlags> SoundDeviceStub::GetRequestFlags() const { + uint32_t result = 0; + w->OpenMPT_Wine_Wrapper_SoundDevice_GetRequestFlags(impl, &result); + return FlagSet<RequestFlags>(result); +} + +bool SoundDeviceStub::IsInited() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_IsInited(impl); +} + +bool SoundDeviceStub::IsOpen() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_IsOpen(impl); +} + +bool SoundDeviceStub::IsAvailable() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_IsAvailable(impl); +} + +bool SoundDeviceStub::IsPlaying() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_IsPlaying(impl); +} + +bool SoundDeviceStub::IsPlayingSilence() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_IsPlayingSilence(impl); +} + +void SoundDeviceStub::StopAndAvoidPlayingSilence() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_StopAndAvoidPlayingSilence(impl); +} + +void SoundDeviceStub::EndPlayingSilence() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_EndPlayingSilence(impl); +} + +bool SoundDeviceStub::OnIdle() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_OnIdle(impl); +} + +SoundDevice::Settings SoundDeviceStub::GetSettings() const { + return json_cast<SoundDevice::Settings>(w->result_as_string(w->OpenMPT_Wine_Wrapper_SoundDevice_GetSettings(impl))); +} + +SampleFormat SoundDeviceStub::GetActualSampleFormat() const { + int32_t result = 0; + w->OpenMPT_Wine_Wrapper_SoundDevice_GetActualSampleFormat(impl, &result); + return SampleFormat::FromInt(result); +} + +SoundDevice::BufferAttributes SoundDeviceStub::GetEffectiveBufferAttributes() const { + OpenMPT_SoundDevice_BufferAttributes result; + w->OpenMPT_Wine_Wrapper_SoundDevice_GetEffectiveBufferAttributes(impl, &result); + return C::decode(result); +} + +SoundDevice::TimeInfo SoundDeviceStub::GetTimeInfo() const { + OpenMPT_SoundDevice_TimeInfo result; + w->OpenMPT_Wine_Wrapper_SoundDevice_GetTimeInfo(impl, &result); + return C::decode(result); +} + +SoundDevice::StreamPosition SoundDeviceStub::GetStreamPosition() const { + OpenMPT_SoundDevice_StreamPosition result; + w->OpenMPT_Wine_Wrapper_SoundDevice_GetStreamPosition(impl, &result); + return C::decode(result); +} + +bool SoundDeviceStub::DebugIsFragileDevice() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_DebugIsFragileDevice(impl); +} + +bool SoundDeviceStub::DebugInRealtimeCallback() const { + return w->OpenMPT_Wine_Wrapper_SoundDevice_DebugInRealtimeCallback(impl); +} + +SoundDevice::Statistics SoundDeviceStub::GetStatistics() const { + return json_cast<SoundDevice::Statistics>(w->result_as_string(w->OpenMPT_Wine_Wrapper_SoundDevice_GetStatistics(impl))); +} + +bool SoundDeviceStub::OpenDriverSettings() { + return w->OpenMPT_Wine_Wrapper_SoundDevice_OpenDriverSettings(impl); +} + + +} // namespace SoundDevice + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.h b/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.h new file mode 100644 index 00000000..6114fbf8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/WineSoundDeviceStub.h @@ -0,0 +1,102 @@ +/* + * WineSoundDeviceStub.h + * --------------------- + * Purpose: Stub sound device driver class connection to WineSupport Wrapper. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "openmpt/sounddevice/SoundDeviceBase.hpp" + +#include "../common/ComponentManager.h" + + +extern "C" { + typedef struct OpenMPT_Wine_Wrapper_SoundDevice OpenMPT_Wine_Wrapper_SoundDevice; +}; + + +OPENMPT_NAMESPACE_BEGIN + + +class ComponentWineWrapper; + + +namespace SoundDevice { + + +class SoundDeviceStub + : public SoundDevice::IBase +{ + +public: + + static std::unique_ptr<SoundDevice::BackendInitializer> BackendInitializer() { return std::make_unique< SoundDevice::BackendInitializer>(); } + static std::vector<SoundDevice::Info> EnumerateDevices(ILogger &logger, SoundDevice::SysInfo sysInfo); + +public: + + SoundDeviceStub(ILogger &logger, SoundDevice::Info info, SoundDevice::SysInfo sysInfo); + + virtual ~SoundDeviceStub(); + +public: + + virtual void SetCallback(SoundDevice::ICallback *callback); + virtual void SetMessageReceiver(SoundDevice::IMessageReceiver *receiver); + + virtual SoundDevice::Info GetDeviceInfo() const; + + virtual SoundDevice::Caps GetDeviceCaps() const; + virtual SoundDevice::DynamicCaps GetDeviceDynamicCaps(const std::vector<uint32> &baseSampleRates); + + virtual bool Init(const SoundDevice::AppInfo &appInfo); + virtual bool Open(const SoundDevice::Settings &settings); + virtual bool Close(); + virtual bool Start(); + virtual void Stop(); + + virtual FlagSet<RequestFlags> GetRequestFlags() const; + + virtual bool IsInited() const; + virtual bool IsOpen() const; + virtual bool IsAvailable() const; + virtual bool IsPlaying() const; + + virtual bool IsPlayingSilence() const; + virtual void StopAndAvoidPlayingSilence(); + virtual void EndPlayingSilence(); + + virtual bool OnIdle(); + + virtual SoundDevice::Settings GetSettings() const; + virtual SampleFormat GetActualSampleFormat() const; + virtual SoundDevice::BufferAttributes GetEffectiveBufferAttributes() const; + + virtual SoundDevice::TimeInfo GetTimeInfo() const; + virtual SoundDevice::StreamPosition GetStreamPosition() const; + + virtual bool DebugIsFragileDevice() const; + virtual bool DebugInRealtimeCallback() const; + + virtual SoundDevice::Statistics GetStatistics() const; + + virtual bool OpenDriverSettings(); + +private: + + ComponentHandle<ComponentWineWrapper> w; + OpenMPT_Wine_Wrapper_SoundDevice * impl; + +}; + + +} // namespace SoundDevice + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.cpp new file mode 100644 index 00000000..23ae0718 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.cpp @@ -0,0 +1,1567 @@ +/* + * dlg_misc.cpp + * ------------ + * Purpose: Implementation of various OpenMPT dialogs. + * 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 "Moddoc.h" +#include "Mainfrm.h" +#include "dlg_misc.h" +#include "Dlsbank.h" +#include "Childfrm.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "ChannelManagerDlg.h" +#include "TempoSwingDialog.h" +#include "../soundlib/mod_specifications.h" +#include "../common/version.h" +#include "../common/mptStringBuffer.h" + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////////////////// +// CModTypeDlg + + +BEGIN_MESSAGE_MAP(CModTypeDlg, CDialog) + //{{AFX_MSG_MAP(CModTypeDlg) + ON_CBN_SELCHANGE(IDC_COMBO1, &CModTypeDlg::UpdateDialog) + ON_CBN_SELCHANGE(IDC_COMBO_TEMPOMODE, &CModTypeDlg::OnTempoModeChanged) + ON_COMMAND(IDC_CHECK_PT1X, &CModTypeDlg::OnPTModeChanged) + ON_COMMAND(IDC_BUTTON1, &CModTypeDlg::OnTempoSwing) + ON_COMMAND(IDC_BUTTON2, &CModTypeDlg::OnLegacyPlaybackSettings) + ON_COMMAND(IDC_BUTTON3, &CModTypeDlg::OnDefaultBehaviour) + + ON_NOTIFY_EX(TTN_NEEDTEXT, 0, &CModTypeDlg::OnToolTipNotify) + + //}}AFX_MSG_MAP + +END_MESSAGE_MAP() + + +void CModTypeDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CModTypeDlg) + DDX_Control(pDX, IDC_COMBO1, m_TypeBox); + DDX_Control(pDX, IDC_COMBO2, m_ChannelsBox); + DDX_Control(pDX, IDC_COMBO_TEMPOMODE, m_TempoModeBox); + DDX_Control(pDX, IDC_COMBO_MIXLEVELS, m_PlugMixBox); + + DDX_Control(pDX, IDC_CHECK1, m_CheckBox1); + DDX_Control(pDX, IDC_CHECK2, m_CheckBox2); + DDX_Control(pDX, IDC_CHECK3, m_CheckBox3); + DDX_Control(pDX, IDC_CHECK4, m_CheckBox4); + DDX_Control(pDX, IDC_CHECK5, m_CheckBox5); + DDX_Control(pDX, IDC_CHECK_PT1X, m_CheckBoxPT1x); + DDX_Control(pDX, IDC_CHECK_AMIGALIMITS, m_CheckBoxAmigaLimits); + + //}}AFX_DATA_MAP +} + + +BOOL CModTypeDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + m_nType = sndFile.GetType(); + m_nChannels = sndFile.GetNumChannels(); + m_tempoSwing = sndFile.m_tempoSwing; + m_playBehaviour = sndFile.m_playBehaviour; + initialized = false; + + // Mod types + + m_TypeBox.SetItemData(m_TypeBox.AddString(_T("ProTracker MOD")), MOD_TYPE_MOD); + m_TypeBox.SetItemData(m_TypeBox.AddString(_T("Scream Tracker S3M")), MOD_TYPE_S3M); + m_TypeBox.SetItemData(m_TypeBox.AddString(_T("FastTracker XM")), MOD_TYPE_XM); + m_TypeBox.SetItemData(m_TypeBox.AddString(_T("Impulse Tracker IT")), MOD_TYPE_IT); + m_TypeBox.SetItemData(m_TypeBox.AddString(_T("OpenMPT MPTM")), MOD_TYPE_MPT); + switch(m_nType) + { + case MOD_TYPE_S3M: m_TypeBox.SetCurSel(1); break; + case MOD_TYPE_XM: m_TypeBox.SetCurSel(2); break; + case MOD_TYPE_IT: m_TypeBox.SetCurSel(3); break; + case MOD_TYPE_MPT: m_TypeBox.SetCurSel(4); break; + default: m_TypeBox.SetCurSel(0); break; + } + + // Time signature information + + SetDlgItemInt(IDC_ROWSPERBEAT, sndFile.m_nDefaultRowsPerBeat); + SetDlgItemInt(IDC_ROWSPERMEASURE, sndFile.m_nDefaultRowsPerMeasure); + + // Version information + + if(sndFile.m_dwCreatedWithVersion) SetDlgItemText(IDC_EDIT_CREATEDWITH, _T("OpenMPT ") + FormatVersionNumber(sndFile.m_dwCreatedWithVersion)); + SetDlgItemText(IDC_EDIT_SAVEDWITH, mpt::ToCString(sndFile.m_modFormat.madeWithTracker.empty() ? sndFile.m_modFormat.formatName : sndFile.m_modFormat.madeWithTracker)); + + const int iconSize = Util::ScalePixels(32, m_hWnd); + m_warnIcon = (HICON)::LoadImage(NULL, IDI_EXCLAMATION, IMAGE_ICON, iconSize, iconSize, LR_SHARED); + + UpdateDialog(); + + initialized = true; + EnableToolTips(TRUE); + return TRUE; +} + + +CString CModTypeDlg::FormatVersionNumber(Version version) +{ + return mpt::ToCString(version.ToUString() + (version.IsTestVersion() ? U_(" (test build)") : U_(""))); +} + + +void CModTypeDlg::UpdateChannelCBox() +{ + const MODTYPE type = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel())); + CHANNELINDEX currChanSel = static_cast<CHANNELINDEX>(m_ChannelsBox.GetItemData(m_ChannelsBox.GetCurSel())); + const CHANNELINDEX minChans = CSoundFile::GetModSpecifications(type).channelsMin; + const CHANNELINDEX maxChans = CSoundFile::GetModSpecifications(type).channelsMax; + + if(m_ChannelsBox.GetCount() < 1 + || m_ChannelsBox.GetItemData(0) != minChans + || m_ChannelsBox.GetItemData(m_ChannelsBox.GetCount() - 1) != maxChans) + { + // Update channel list if number of supported channels has changed. + if(m_ChannelsBox.GetCount() < 1) currChanSel = m_nChannels; + m_ChannelsBox.ResetContent(); + + CString s; + for(CHANNELINDEX i = minChans; i <= maxChans; i++) + { + s.Format(_T("%u Channel%s"), i, (i != 1) ? _T("s") : _T("")); + m_ChannelsBox.SetItemData(m_ChannelsBox.AddString(s), i); + } + + Limit(currChanSel, minChans, maxChans); + m_ChannelsBox.SetCurSel(currChanSel - minChans); + } +} + + +void CModTypeDlg::UpdateDialog() +{ + m_nType = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel())); + + UpdateChannelCBox(); + + m_CheckBox1.SetCheck(sndFile.m_SongFlags[SONG_LINEARSLIDES] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBox2.SetCheck(sndFile.m_SongFlags[SONG_FASTVOLSLIDES] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBox3.SetCheck(sndFile.m_SongFlags[SONG_ITOLDEFFECTS] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBox4.SetCheck(sndFile.m_SongFlags[SONG_ITCOMPATGXX] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBox5.SetCheck(sndFile.m_SongFlags[SONG_EXFILTERRANGE] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBoxPT1x.SetCheck(sndFile.m_SongFlags[SONG_PT_MODE] ? BST_CHECKED : BST_UNCHECKED); + m_CheckBoxAmigaLimits.SetCheck(sndFile.m_SongFlags[SONG_AMIGALIMITS] ? BST_CHECKED : BST_UNCHECKED); + + const FlagSet<SongFlags> allowedFlags(sndFile.GetModSpecifications(m_nType).songFlags); + m_CheckBox1.EnableWindow(allowedFlags[SONG_LINEARSLIDES]); + m_CheckBox2.EnableWindow(allowedFlags[SONG_FASTVOLSLIDES]); + m_CheckBox3.EnableWindow(allowedFlags[SONG_ITOLDEFFECTS]); + m_CheckBox4.EnableWindow(allowedFlags[SONG_ITCOMPATGXX]); + m_CheckBox5.EnableWindow(allowedFlags[SONG_EXFILTERRANGE]); + m_CheckBoxPT1x.EnableWindow(allowedFlags[SONG_PT_MODE]); + m_CheckBoxAmigaLimits.EnableWindow(allowedFlags[SONG_AMIGALIMITS]); + + // These two checkboxes are mutually exclusive and share the same screen space + m_CheckBoxPT1x.ShowWindow(m_nType == MOD_TYPE_MOD ? SW_SHOW : SW_HIDE); + m_CheckBox5.ShowWindow(m_nType != MOD_TYPE_MOD ? SW_SHOW : SW_HIDE); + if(allowedFlags[SONG_PT_MODE]) OnPTModeChanged(); + + // Tempo modes + const TempoMode oldTempoMode = initialized ? static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) : sndFile.m_nTempoMode; + m_TempoModeBox.ResetContent(); + + m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Classic")), static_cast<DWORD_PTR>(TempoMode::Classic)); + if(m_nType == MOD_TYPE_MPT || (sndFile.GetType() != MOD_TYPE_MPT && sndFile.m_nTempoMode == TempoMode::Alternative)) + m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Alternative")), static_cast<DWORD_PTR>(TempoMode::Alternative)); + if(m_nType == MOD_TYPE_MPT || (sndFile.GetType() != MOD_TYPE_MPT && sndFile.m_nTempoMode == TempoMode::Modern)) + m_TempoModeBox.SetItemData(m_TempoModeBox.AddString(_T("Modern (accurate)")), static_cast<DWORD_PTR>(TempoMode::Modern)); + m_TempoModeBox.SetCurSel(0); + for(int i = m_TempoModeBox.GetCount(); i > 0; i--) + { + if(static_cast<TempoMode>(m_TempoModeBox.GetItemData(i)) == oldTempoMode) + { + m_TempoModeBox.SetCurSel(i); + break; + } + } + OnTempoModeChanged(); + + // Mix levels + const MixLevels oldMixLevels = initialized ? static_cast<MixLevels>(m_PlugMixBox.GetItemData(m_PlugMixBox.GetCurSel())) : sndFile.GetMixLevels(); + m_PlugMixBox.ResetContent(); + if(m_nType == MOD_TYPE_MPT || sndFile.GetMixLevels() == MixLevels::v1_17RC3) // In XM/IT, this is only shown for backwards compatibility with existing tunes + m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC3")), static_cast<DWORD_PTR>(MixLevels::v1_17RC3)); + if(sndFile.GetMixLevels() == MixLevels::v1_17RC2) // Only shown for backwards compatibility with existing tunes + m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC2")), static_cast<DWORD_PTR>(MixLevels::v1_17RC2)); + if(sndFile.GetMixLevels() == MixLevels::v1_17RC1) // Ditto + m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("OpenMPT 1.17RC1")), static_cast<DWORD_PTR>(MixLevels::v1_17RC1)); + if(sndFile.GetMixLevels() == MixLevels::Original) // Ditto + m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("Original (MPT 1.16)")), static_cast<DWORD_PTR>(MixLevels::Original)); + int compatMixMode = m_PlugMixBox.AddString(_T("Compatible")); + m_PlugMixBox.SetItemData(compatMixMode, static_cast<DWORD_PTR>(MixLevels::Compatible)); + if(m_nType == MOD_TYPE_XM) + m_PlugMixBox.SetItemData(m_PlugMixBox.AddString(_T("Compatible (FT2 Pan Law)")), static_cast<DWORD_PTR>(MixLevels::CompatibleFT2)); + + // Default to compatible mix mode + m_PlugMixBox.SetCurSel(compatMixMode); + int mixCount = m_PlugMixBox.GetCount(); + for(int i = 0; i < mixCount; i++) + { + if(static_cast<MixLevels>(m_PlugMixBox.GetItemData(i)) == oldMixLevels) + { + m_PlugMixBox.SetCurSel(i); + break; + } + } + + const bool XMorITorMPT = (m_nType & (MOD_TYPE_XM | MOD_TYPE_IT | MOD_TYPE_MPT)); + const bool isMPTM = (m_nType == MOD_TYPE_MPT); + + // Mixmode Box + GetDlgItem(IDC_TEXT_MIXMODE)->EnableWindow(XMorITorMPT); + m_PlugMixBox.EnableWindow(XMorITorMPT); + + // Tempo mode box + m_TempoModeBox.EnableWindow(XMorITorMPT); + GetDlgItem(IDC_ROWSPERBEAT)->EnableWindow(XMorITorMPT); + GetDlgItem(IDC_ROWSPERMEASURE)->EnableWindow(XMorITorMPT); + GetDlgItem(IDC_TEXT_ROWSPERBEAT)->EnableWindow(XMorITorMPT); + GetDlgItem(IDC_TEXT_ROWSPERMEASURE)->EnableWindow(XMorITorMPT); + GetDlgItem(IDC_TEXT_TEMPOMODE)->EnableWindow(XMorITorMPT); + GetDlgItem(IDC_FRAME_TEMPOMODE)->EnableWindow(XMorITorMPT); + + // Compatibility settings + const PlayBehaviourSet defaultBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_nType); + const PlayBehaviourSet supportedBehaviour = CSoundFile::GetSupportedPlaybackBehaviour(m_nType); + bool enableSetDefaults = false, showWarning = false; + if(m_nType & (MOD_TYPE_MPT | MOD_TYPE_IT | MOD_TYPE_XM)) + { + for(size_t i = 0; i < m_playBehaviour.size(); i++) + { + // Some flags are not really important for "default" behaviour. + if(defaultBehaviour[i] != m_playBehaviour[i] + && i != MSF_COMPATIBLE_PLAY + && i != kFT2VolumeRamping) + { + enableSetDefaults = true; + if(!isMPTM) + { + showWarning = true; + break; + } + } + if(isMPTM && m_playBehaviour[i] && !supportedBehaviour[i]) + { + + enableSetDefaults = true; + showWarning = true; + break; + } + } + } + static_cast<CStatic *>(GetDlgItem(IDC_STATIC1))->SetIcon(showWarning ? m_warnIcon : nullptr); + GetDlgItem(IDC_STATIC2)->SetWindowText(showWarning + ? _T("Playback settings have been set to legacy compatibility mode. Click \"Set Defaults\" to use the recommended settings instead.") + : _T("Compatibility settings are currently optimal. It is advised to not edit them.")); + GetDlgItem(IDC_BUTTON3)->EnableWindow(enableSetDefaults ? TRUE : FALSE); +} + + +void CModTypeDlg::OnPTModeChanged() +{ + // PT1/2 mode enforces Amiga limits + const bool ptMode = IsDlgButtonChecked(IDC_CHECK_PT1X) != BST_UNCHECKED; + m_CheckBoxAmigaLimits.EnableWindow(!ptMode); + if(ptMode) m_CheckBoxAmigaLimits.SetCheck(BST_CHECKED); +} + + +void CModTypeDlg::OnTempoModeChanged() +{ + GetDlgItem(IDC_BUTTON1)->EnableWindow(static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) == TempoMode::Modern); +} + + +void CModTypeDlg::OnTempoSwing() +{ + const ROWINDEX oldRPB = sndFile.m_nDefaultRowsPerBeat; + const ROWINDEX oldRPM = sndFile.m_nDefaultRowsPerMeasure; + const TempoMode oldMode = sndFile.m_nTempoMode; + + // Temporarily apply new tempo signature for preview + const ROWINDEX newRPB = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT)), ROWINDEX(1), MAX_ROWS_PER_BEAT); + const ROWINDEX newRPM = std::clamp(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE)), newRPB, MAX_ROWS_PER_BEAT); + sndFile.m_nDefaultRowsPerBeat = newRPB; + sndFile.m_nDefaultRowsPerMeasure = newRPM; + sndFile.m_nTempoMode = TempoMode::Modern; + + m_tempoSwing.resize(newRPB, TempoSwing::Unity); + CTempoSwingDlg dlg(this, m_tempoSwing, sndFile); + if(dlg.DoModal() == IDOK) + { + m_tempoSwing = dlg.m_tempoSwing; + } + sndFile.m_nDefaultRowsPerBeat = oldRPB; + sndFile.m_nDefaultRowsPerMeasure = oldRPM; + sndFile.m_nTempoMode = oldMode; +} + + +void CModTypeDlg::OnLegacyPlaybackSettings() +{ + CLegacyPlaybackSettingsDlg dlg(this, m_playBehaviour, m_nType); + if(dlg.DoModal() == IDOK) + { + m_playBehaviour = dlg.GetPlayBehaviour(); + } + UpdateDialog(); +} + + +void CModTypeDlg::OnDefaultBehaviour() +{ + m_playBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_nType); + UpdateDialog(); +} + + +bool CModTypeDlg::VerifyData() +{ + const int newRPB = GetDlgItemInt(IDC_ROWSPERBEAT); + const int newRPM = GetDlgItemInt(IDC_ROWSPERMEASURE); + if(newRPB > newRPM) + { + Reporting::Warning("Error: Rows per measure must be greater than or equal to rows per beat."); + GetDlgItem(IDC_ROWSPERMEASURE)->SetFocus(); + return false; + } + if(newRPB == 0 && static_cast<TempoMode>(m_TempoModeBox.GetItemData(m_TempoModeBox.GetCurSel())) == TempoMode::Modern) + { + Reporting::Warning("Error: Rows per beat must be greater than 0 in modern tempo mode."); + GetDlgItem(IDC_ROWSPERBEAT)->SetFocus(); + return false; + } + + int sel = static_cast<int>(m_ChannelsBox.GetItemData(m_ChannelsBox.GetCurSel())); + MODTYPE type = static_cast<MODTYPE>(m_TypeBox.GetItemData(m_TypeBox.GetCurSel())); + + CHANNELINDEX maxChans = CSoundFile::GetModSpecifications(type).channelsMax; + + if(sel > maxChans) + { + CString error; + error.Format(_T("Error: Maximum number of channels for this module type is %u."), maxChans); + Reporting::Warning(error); + return false; + } + + if(maxChans < sndFile.GetNumChannels()) + { + if(Reporting::Confirm("New module type supports less channels than currently used, and reducing channel number is required. Continue?") != cnfYes) + return false; + } + + return true; +} + + +void CModTypeDlg::OnOK() +{ + if (!VerifyData()) + return; + + int sel = m_TypeBox.GetCurSel(); + if (sel >= 0) + { + m_nType = static_cast<MODTYPE>(m_TypeBox.GetItemData(sel)); + } + const auto &newModSpecs = sndFile.GetModSpecifications(m_nType); + + sndFile.m_SongFlags.set(SONG_LINEARSLIDES, m_CheckBox1.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_FASTVOLSLIDES, m_CheckBox2.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_ITOLDEFFECTS, m_CheckBox3.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_ITCOMPATGXX, m_CheckBox4.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_EXFILTERRANGE, m_CheckBox5.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_PT_MODE, m_CheckBoxPT1x.GetCheck() != BST_UNCHECKED); + sndFile.m_SongFlags.set(SONG_AMIGALIMITS, m_CheckBoxAmigaLimits.GetCheck() != BST_UNCHECKED); + + sel = m_ChannelsBox.GetCurSel(); + if (sel >= 0) + { + m_nChannels = static_cast<CHANNELINDEX>(m_ChannelsBox.GetItemData(sel)); + } + + sndFile.m_nDefaultRowsPerBeat = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERBEAT)), MAX_ROWS_PER_BEAT); + sndFile.m_nDefaultRowsPerMeasure = std::min(static_cast<ROWINDEX>(GetDlgItemInt(IDC_ROWSPERMEASURE)), MAX_ROWS_PER_BEAT); + + sel = m_TempoModeBox.GetCurSel(); + if(sel >= 0) + { + const auto oldMode = sndFile.m_nTempoMode; + sndFile.m_nTempoMode = static_cast<TempoMode>(m_TempoModeBox.GetItemData(sel)); + if(oldMode == TempoMode::Modern && sndFile.m_nTempoMode != TempoMode::Modern) + { + double newTempo = sndFile.m_nDefaultTempo.ToDouble() * (sndFile.m_nDefaultSpeed * sndFile.m_nDefaultRowsPerBeat) / ((sndFile.m_nTempoMode == TempoMode::Classic) ? 24 : 60); + if(!newModSpecs.hasFractionalTempo) + newTempo = std::round(newTempo); + sndFile.m_nDefaultTempo = Clamp(TEMPO(newTempo), newModSpecs.GetTempoMin(), newModSpecs.GetTempoMax()); + } + } + if(sndFile.m_nTempoMode == TempoMode::Modern) + { + sndFile.m_tempoSwing = m_tempoSwing; + if(!sndFile.m_tempoSwing.empty()) + sndFile.m_tempoSwing.resize(sndFile.m_nDefaultRowsPerBeat); + } else + { + sndFile.m_tempoSwing.clear(); + } + + sel = m_PlugMixBox.GetCurSel(); + if(sel >= 0) + { + sndFile.SetMixLevels(static_cast<MixLevels>(m_PlugMixBox.GetItemData(sel))); + } + + PlayBehaviourSet allowedFlags = CSoundFile::GetSupportedPlaybackBehaviour(m_nType); + for(size_t i = 0; i < kMaxPlayBehaviours; i++) + { + // Only set those flags which are supported by the new format or were already enabled previously + sndFile.m_playBehaviour.set(i, m_playBehaviour[i] && (allowedFlags[i] || (sndFile.m_playBehaviour[i] && sndFile.GetType() == m_nType))); + } + + DestroyIcon(m_warnIcon); + CDialog::OnOK(); +} + + +BOOL CModTypeDlg::OnToolTipNotify(UINT, NMHDR *pNMHDR, LRESULT *) +{ + TOOLTIPTEXT *pTTT = (TOOLTIPTEXT*)pNMHDR; + UINT_PTR nID = pNMHDR->idFrom; + if(pTTT->uFlags & TTF_IDISHWND) + { + // idFrom is actually the HWND of the tool + nID = ::GetDlgCtrlID((HWND)nID); + } + + mpt::tstring text; + switch(nID) + { + case IDC_CHECK1: + text = _T("Note slides always slide the same amount, not depending on the sample frequency."); + break; + case IDC_CHECK2: + text = _T("Old Scream Tracker 3 volume slide behaviour (not recommended)."); + break; + case IDC_CHECK3: + text = _T("Play some effects like in early versions of Impulse Tracker (not recommended)."); + break; + case IDC_CHECK4: + text = _T("Gxx and Exx/Fxx won't share effect memory. Gxx resets instrument envelopes."); + break; + case IDC_CHECK5: + text = _T("The resonant filter's frequency range is increased from about 5kHz to 10kHz."); + break; + case IDC_CHECK_PT1X: + text = _T("Enforce Amiga frequency limits, ProTracker offset bug emulation."); + break; + case IDC_COMBO_MIXLEVELS: + text = _T("Mixing method of sample and instrument plugin levels."); + break; + case IDC_BUTTON1: + if(!GetDlgItem(IDC_BUTTON1)->IsWindowEnabled()) + { + text = _T("Tempo swing is only available in modern tempo mode."); + } else + { + text = _T("Swing setting: "); + if(m_tempoSwing.empty()) + { + text += _T("Default"); + } else + { + for(size_t i = 0; i < m_tempoSwing.size(); i++) + { + if(i > 0) + text += _T(" / "); + text += MPT_TFORMAT("{}%")(Util::muldivr(m_tempoSwing[i], 100, TempoSwing::Unity)); + } + } + } + } + + mpt::String::WriteWinBuf(pTTT->szText) = text; + return TRUE; +} + + +////////////////////////////////////////////////////////////////////////////// +// Legacy Playback Settings dialog + +BEGIN_MESSAGE_MAP(CLegacyPlaybackSettingsDlg, ResizableDialog) + ON_COMMAND(IDC_BUTTON1, &CLegacyPlaybackSettingsDlg::OnSelectDefaults) + ON_EN_UPDATE(IDC_EDIT1, &CLegacyPlaybackSettingsDlg::OnFilterStringChanged) + ON_CLBN_CHKCHANGE(IDC_LIST1, &CLegacyPlaybackSettingsDlg::UpdateSelectDefaults) +END_MESSAGE_MAP() + + +void CLegacyPlaybackSettingsDlg::DoDataExchange(CDataExchange* pDX) +{ + ResizableDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_LIST1, m_CheckList); +} + + +BOOL CLegacyPlaybackSettingsDlg::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + OnFilterStringChanged(); + UpdateSelectDefaults(); + return TRUE; +} + + +void CLegacyPlaybackSettingsDlg::OnSelectDefaults() +{ + const int count = m_CheckList.GetCount(); + m_playBehaviour = CSoundFile::GetDefaultPlaybackBehaviour(m_modType); + for(int i = 0; i < count; i++) + { + m_CheckList.SetCheck(i, m_playBehaviour[m_CheckList.GetItemData(i)] ? BST_CHECKED : BST_UNCHECKED); + } +} + + +void CLegacyPlaybackSettingsDlg::UpdateSelectDefaults() +{ + const int count = m_CheckList.GetCount(); + for(int i = 0; i < count; i++) + { + m_playBehaviour.set(m_CheckList.GetItemData(i), m_CheckList.GetCheck(i) != BST_UNCHECKED); + } + const auto defaults = CSoundFile::GetDefaultPlaybackBehaviour(m_modType); + GetDlgItem(IDC_BUTTON1)->EnableWindow(m_playBehaviour != defaults ? TRUE : FALSE); +} + + +void CLegacyPlaybackSettingsDlg::OnFilterStringChanged() +{ + CString s; + GetDlgItemText(IDC_EDIT1, s); + const bool filterActive = !s.IsEmpty(); + s.MakeLower(); + + m_CheckList.SetRedraw(FALSE); + m_CheckList.ResetContent(); + + const auto allowedFlags = CSoundFile::GetSupportedPlaybackBehaviour(m_modType); + for(size_t i = 0; i < kMaxPlayBehaviours; i++) + { + const TCHAR *desc = _T(""); + switch(i) + { + case MSF_COMPATIBLE_PLAY: continue; + + case kMPTOldSwingBehaviour: desc = _T("OpenMPT 1.17 compatible random variation behaviour for instruments"); break; + case kMIDICCBugEmulation: desc = _T("Plugin volume MIDI CC bug emulation"); break; + case kOldMIDIPitchBends: desc = _T("Old Pitch Wheel behaviour for instrument plugins"); break; + case kFT2VolumeRamping: desc = _T("Use smooth Fasttracker 2 volume ramping"); break; + case kMODVBlankTiming: desc = _T("VBlank timing: F20 and above sets speed instead of tempo"); break; + + case kSlidesAtSpeed1: desc = _T("Execute regular portamento slides at speed 1"); break; + case kPeriodsAreHertz: desc = _T("Compute note frequency in Hertz rather than periods"); break; + case kTempoClamp: desc = _T("Clamp tempo to 32-255 range"); break; + case kPerChannelGlobalVolSlide: desc = _T("Global volume slide memory is per-channel"); break; + case kPanOverride: desc = _T("Panning commands override surround and random pan variation"); break; + + case kITInstrWithoutNote: desc = _T("Retrigger instrument envelopes on instrument change"); break; + case kITVolColFinePortamento: desc = _T("Volume column portamento never does fine portamento"); break; + case kITArpeggio: desc = _T("IT arpeggio algorithm"); break; + case kITOutOfRangeDelay: desc = _T("Out-of-range delay commands queue new instrument"); break; + case kITPortaMemoryShare: desc = _T("Gxx shares memory with Exx and Fxx"); break; + case kITPatternLoopTargetReset: desc = _T("After finishing a pattern loop, set the pattern loop target to the next row"); break; + case kITFT2PatternLoop: desc = _T("Nested pattern loop behaviour"); break; + case kITPingPongNoReset: desc = _T("Do not reset ping pong direction with instrument numbers"); break; + case kITEnvelopeReset: desc = _T("IT envelope reset behaviour"); break; + case kITClearOldNoteAfterCut: desc = _T("Forget the previous note after cutting it"); break; + case kITVibratoTremoloPanbrello: desc = _T("More IT-like Vibrato, Tremolo and Panbrello handling"); break; + case kITTremor: desc = _T("Ixx behaves like in IT"); break; + case kITRetrigger: desc = _T("Qxx behaves like in IT"); break; + case kITMultiSampleBehaviour: desc = _T("Properly update C-5 frequency when changing note in multisampled instrument"); break; + case kITPortaTargetReached: desc = _T("Clear portamento target after it has been reached"); break; + case kITPatternLoopBreak: desc = _T("Do not reset loop count on pattern break"); break; + case kITOffset: desc = _T("Offset after sample end is treated like in IT"); break; + case kITSwingBehaviour: desc = _T("Volume and panning random variation work more like in IT"); break; + case kITNNAReset: desc = _T("NNA is reset on every note change, not every instrument change"); break; + case kITSCxStopsSample: desc = _T("SCx really stops the sample and does not just mute it"); break; + case kITEnvelopePositionHandling: desc = _T("IT-style envelope position advance + enable/disable behaviour"); break; + case kITPortamentoInstrument: desc = _T("More compatible instrument change + portamento"); break; + case kITPingPongMode: desc = _T("Do not repeat last sample point in ping pong loop, like IT's software mixer"); break; + case kITRealNoteMapping: desc = _T("Use triggered note rather than translated note for PPS and DNA note check"); break; + case kITHighOffsetNoRetrig: desc = _T("SAx does not apply an offset effect to a note next to it"); break; + case kITFilterBehaviour: desc = _T("User IT's filter coefficients (unless extended filter range is used) and behaviour"); break; + case kITNoSurroundPan: desc = _T("Panning modulation is disabled on surround channels"); break; + case kITShortSampleRetrig: desc = _T("Do not retrigger already stopped channels"); break; + case kITPortaNoNote: desc = _T("Do not apply any portamento if no previous note is playing"); break; + case kITFT2DontResetNoteOffOnPorta: + if(m_modType == MOD_TYPE_XM) + desc = _T("Reset note-off on portamento if there is an instrument number"); + else + desc = _T("Reset note-off on portamento if there is an instrument number in Compatible Gxx mode"); + break; + case kITVolColMemory: desc = _T("Volume column effects share their memory with the effect column"); break; + case kITPortamentoSwapResetsPos: desc = _T("Portamento with sample swap plays the new sample from the beginning"); break; + case kITEmptyNoteMapSlot: desc = _T("Ignore instrument note map entries with no note completely"); break; + case kITFirstTickHandling: desc = _T("IT-style first tick handling"); break; + case kITSampleAndHoldPanbrello: desc = _T("IT-style sample&hold panbrello waveform"); break; + case kITClearPortaTarget: desc = _T("New notes reset portamento target in IT"); break; + case kITPanbrelloHold: desc = _T("Do not reset panbrello effect until next note or panning effect"); break; + case kITPanningReset: desc = _T("Sample and instrument panning is only applied on note change, not instrument change"); break; + case kITPatternLoopWithJumpsOld: desc = _T("Bxx on the same row as SBx terminates the loop in IT"); break; + case kITInstrWithNoteOff: desc = _T("Instrument number with note-off recalls default volume"); break; + case kFT2Arpeggio: desc = _T("FT2 arpeggio algorithm"); break; + case kFT2Retrigger: desc = _T("Rxx behaves like in FT2"); break; + case kFT2VolColVibrato: desc = _T("Vibrato speed in volume column does not actually execute the vibrato effect"); break; + case kFT2PortaNoNote: desc = _T("Do not play portamento-ed note if no previous note is playing"); break; + case kFT2KeyOff: desc = _T("FT2-style Kxx handling"); break; + case kFT2PanSlide: desc = _T("Volume-column pan slides are finer"); break; + case kFT2ST3OffsetOutOfRange: desc = _T("Offset past sample end stops the note"); break; + case kFT2RestrictXCommand: desc = _T("Do not allow ModPlug extensions to X command"); break; + case kFT2RetrigWithNoteDelay: desc = _T("Retrigger envelopes if there is a note delay with no note"); break; + case kFT2SetPanEnvPos: desc = _T("Lxx only sets the pan envelope position if the volume envelope's sustain flag is set"); break; + case kFT2PortaIgnoreInstr: desc = _T("Portamento with instrument number applies volume settings of new sample, but not the new sample itself"); break; + case kFT2VolColMemory: desc = _T("No volume column memory"); break; + case kFT2LoopE60Restart: desc = _T("Next pattern starts on the same row as the last E60 command"); break; + case kFT2ProcessSilentChannels: desc = _T("Keep processing faded channels for later portamento pickup"); break; + case kFT2ReloadSampleSettings: desc = _T("Reload sample settings even if a note-off is placed next to an instrument number"); break; + case kFT2PortaDelay: desc = _T("Portamento with note delay next to it is ignored"); break; + case kFT2Transpose: desc = _T("Ignore out-of-range transposed notes"); break; + case kFT2PatternLoopWithJumps: desc = _T("Bxx or Dxx on the same row as E6x terminates the loop"); break; + case kFT2PortaTargetNoReset: desc = _T("Portamento target is not reset with new notes"); break; + case kFT2EnvelopeEscape: desc = _T("Sustain point at end of envelope loop stops the loop after release"); break; + case kFT2Tremor: desc = _T("Txx behaves like in FT2"); break; + case kFT2OutOfRangeDelay: desc = _T("Do not trigger notes with out-of-range note delay"); break; + case kFT2Periods: desc = _T("Use FT2's broken period handling"); break; + case kFT2PanWithDelayedNoteOff: desc = _T("Panning command with delayed note-off is ignored"); break; + case kFT2VolColDelay: desc = _T("FT2-style volume column handling if there is a note delay"); break; + case kFT2FinetunePrecision: desc = _T("Round sample finetune to multiples of 8"); break; + case kFT2NoteOffFlags: desc = _T("Fade instrument on note-off when there is no volume envelope; instrument numbers reset note-off status"); break; + case kITMultiSampleInstrumentNumber: desc = _T("Lone instrument number after portamento within multi-sampled instrument sets the target sample's settings"); break; + case kRowDelayWithNoteDelay: desc = _T("Note delays next to a row delay are repeated on every row repetition"); break; + case kFT2MODTremoloRampWaveform: desc = _T("Emulate FT2/ProTracker tremolo ramp down / triangle waveform"); break; + case kFT2PortaUpDownMemory: desc = _T("Portamento Up and Down have separate effect memory"); break; + case kST3NoMutedChannels: desc = _T("Do not process any effects on muted S3M channels"); break; + case kST3EffectMemory: desc = _T("Most effects share the same memory"); break; + case kST3PortaSampleChange: desc = _T("Portamento with instrument number applies volume settings of new sample, but not the new sample itself (GUS)"); break; + case kST3VibratoMemory: desc = _T("Do not remember vibrato type in effect memory"); break; + case kST3LimitPeriod: desc = _T("ModPlug Tracker frequency limits"); break; + case KST3PortaAfterArpeggio: desc = _T("Portamento immediately following an arpeggio effect continues at the last arpeggiated note"); break; + case kMODOneShotLoops: desc = _T("ProTracker one-shot loops"); break; + case kMODIgnorePanning: desc = _T("Ignore panning commands"); break; + case kMODSampleSwap: desc = _T("Enable on-the-fly sample swapping"); break; + case kMODOutOfRangeNoteDelay: desc = _T("Out-of-range note delay is played on next row"); break; + case kMODTempoOnSecondTick: desc = _T("Tempo changes are handled on second tick instead of first"); break; + case kFT2PanSustainRelease: desc = _T("If the sustain point of the panning envelope is reached before key-off, it is never released"); break; + case kLegacyReleaseNode: desc = _T("Old volume envelope release node scaling behaviour"); break; + case kOPLBeatingOscillators: desc = _T("Beating OPL oscillators"); break; + case kST3OffsetWithoutInstrument: desc = _T("Notes without instrument use the previous note's sample offset"); break; + case kReleaseNodePastSustainBug: desc = _T("Broken release node after sustain end behaviour"); break; + case kFT2NoteDelayWithoutInstr: desc = _T("Delayed instrument-less notes should not recall volume and panning"); break; + case kOPLFlexibleNoteOff: desc = _T("Full control over OPL notes after note-off"); break; + case kITInstrWithNoteOffOldEffects: desc = _T("Instrument number with note-off retriggers envelopes with Old Effects enabled"); break; + case kMIDIVolumeOnNoteOffBug: desc = _T("Reset VST volume on note-off"); break; + case kITDoNotOverrideChannelPan: desc = _T("Instruments / samples with forced panning do not override channel panning for following instruments / samples"); break; + case kITPatternLoopWithJumps: desc = _T("Bxx right of SBx terminates the loop in IT"); break; + case kITDCTBehaviour: desc = _T("Duplicate Sample Check requires same instrument, Duplicate Note Check uses pattern notes for comparison"); break; + case kOPLwithNNA: desc = _T("New Note Action / Duplicate Note Action set to Note Off and Note Fade affect OPL notes like samples"); break; + case kST3RetrigAfterNoteCut: desc = _T("Notes cannot be retriggered after they have been cut"); break; + case kST3SampleSwap: desc = _T("Enable on-the-fly sample swapping (SoundBlaster driver)"); break; + case kOPLRealRetrig: desc = _T("Retrigger (Qxy) affects OPL notes"); break; + case kOPLNoResetAtEnvelopeEnd: desc = _T("Do not reset OPL channel status at end of envelopes"); break; + case kOPLNoteStopWith0Hz: desc = _T("OPL key-off sets note frequency to 0 Hz"); break; + case kOPLNoteOffOnNoteChange: desc = _T("Send OPL key-off when triggering notes"); break; + case kFT2PortaResetDirection: desc = _T("Tone Portamento direction resets after reaching portamento target from below"); break; + case kApplyUpperPeriodLimit: desc = _T("Apply lower frequency limit"); break; + case kApplyOffsetWithoutNote: desc = _T("Offset commands work without a note next to them"); break; + case kITPitchPanSeparation: desc = _T("Pitch / Pan Separation can be overridden by panning commands"); break; + case kImprecisePingPongLoops: desc = _T("Use old imprecise ping-pong loop end calculation"); break; + + default: MPT_ASSERT_NOTREACHED(); + } + + if(filterActive && CString{desc}.MakeLower().Find(s) < 0) + continue; + + if(m_playBehaviour[i] || allowedFlags[i]) + { + int item = m_CheckList.AddString(desc); + m_CheckList.SetItemData(item, i); + int check = m_playBehaviour[i] ? BST_CHECKED : BST_UNCHECKED; + if(!allowedFlags[i]) + check = BST_INDETERMINATE; // Is checked but not supported by format -> grey out + m_CheckList.SetCheck(item, check); + } + } + m_CheckList.SetRedraw(TRUE); +} + + +/////////////////////////////////////////////////////////// +// CRemoveChannelsDlg + +void CRemoveChannelsDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_REMCHANSLIST, m_RemChansList); +} + + +BEGIN_MESSAGE_MAP(CRemoveChannelsDlg, CDialog) + ON_LBN_SELCHANGE(IDC_REMCHANSLIST, &CRemoveChannelsDlg::OnChannelChanged) +END_MESSAGE_MAP() + + + +BOOL CRemoveChannelsDlg::OnInitDialog() +{ + CString s; + CDialog::OnInitDialog(); + const CHANNELINDEX numChannels = sndFile.GetNumChannels(); + for(CHANNELINDEX n = 0; n < numChannels; n++) + { + s = MPT_CFORMAT("Channel {}")(n + 1); + if(sndFile.ChnSettings[n].szName[0] >= 0x20) + { + s += _T(": "); + s += mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.ChnSettings[n].szName); + } + m_RemChansList.SetItemData(m_RemChansList.AddString(s), n); + if (!m_bKeepMask[n]) m_RemChansList.SetSel(n); + } + + if (m_nRemove > 0) + s = MPT_CFORMAT("Select {} channel{} to remove:")(m_nRemove, (m_nRemove != 1) ? CString(_T("s")) : CString(_T(""))); + else + s = MPT_CFORMAT("Select channels to remove (the minimum number of remaining channels is {})")(sndFile.GetModSpecifications().channelsMin); + + SetDlgItemText(IDC_QUESTION1, s); + if(GetDlgItem(IDCANCEL)) GetDlgItem(IDCANCEL)->ShowWindow(m_ShowCancel); + + OnChannelChanged(); + return TRUE; +} + + +void CRemoveChannelsDlg::OnOK() +{ + int selCount = m_RemChansList.GetSelCount(); + std::vector<int> selected(selCount); + m_RemChansList.GetSelItems(selCount, selected.data()); + + m_bKeepMask.assign(sndFile.GetNumChannels(), true); + for (const auto sel : selected) + { + m_bKeepMask[sel] = false; + } + if ((static_cast<CHANNELINDEX>(selCount) == m_nRemove && selCount > 0) + || (m_nRemove == 0 && (sndFile.GetNumChannels() >= selCount + sndFile.GetModSpecifications().channelsMin))) + CDialog::OnOK(); + else + CDialog::OnCancel(); +} + + +void CRemoveChannelsDlg::OnChannelChanged() +{ + const UINT selCount = m_RemChansList.GetSelCount(); + GetDlgItem(IDOK)->EnableWindow(((selCount == m_nRemove && selCount > 0) || (m_nRemove == 0 && (sndFile.GetNumChannels() >= selCount + sndFile.GetModSpecifications().channelsMin) && selCount > 0)) ? TRUE : FALSE); +} + + +InfoDialog::InfoDialog(CWnd *parent) + : ResizableDialog(IDD_INFO_BOX, parent) +{ } + +BOOL InfoDialog::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + SetWindowText(m_caption.c_str()); + SetDlgItemText(IDC_EDIT1, m_content.c_str()); + return TRUE; +} + +void InfoDialog::SetContent(mpt::winstring content) +{ + m_content = std::move(content); +} + +void InfoDialog::SetCaption(mpt::winstring caption) +{ + m_caption = std::move(caption); +} + +//////////////////////////////////////////////////////////////////////////////// +// Sound Bank Information + +CSoundBankProperties::CSoundBankProperties(const CDLSBank &bank, CWnd *parent) + : InfoDialog(parent) +{ + const SOUNDBANKINFO &bi = bank.GetBankInfo(); + std::string info; + info.reserve(128 + bi.szBankName.size() + bi.szDescription.size() + bi.szCopyRight.size() + bi.szEngineer.size() + bi.szSoftware.size() + bi.szComments.size()); + info = "Type:\t" + std::string((bank.GetBankType() & SOUNDBANK_TYPE_SF2) ? "Sound Font (SF2)" : "Downloadable Sound (DLS)"); + if (bi.szBankName.size()) + info += "\r\nName:\t" + bi.szBankName; + if (bi.szDescription.size()) + info += "\r\n\t" + bi.szDescription; + if (bi.szCopyRight.size()) + info += "\r\nCopyright:\t" + bi.szCopyRight; + if (bi.szEngineer.size()) + info += "\r\nAuthor:\t" + bi.szEngineer; + if (bi.szSoftware.size()) + info += "\r\nSoftware:\t" + bi.szSoftware; + if (bi.szComments.size()) + info += "\r\n\r\nComments:\r\n" + bi.szComments; + SetCaption((bank.GetFileName().AsNative() + _T(" - Sound Bank Information"))); + SetContent(mpt::ToWin(mpt::Charset::Locale, info)); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Keyboard Control + +static constexpr uint8 whitetab[7] = {0,2,4,5,7,9,11}; +static constexpr uint8 blacktab[7] = {0xff,1,3,0xff,6,8,10}; + +BEGIN_MESSAGE_MAP(CKeyboardControl, CWnd) + ON_WM_DESTROY() + ON_WM_PAINT() + ON_WM_MOUSEMOVE() + ON_WM_LBUTTONDOWN() + ON_WM_LBUTTONUP() +END_MESSAGE_MAP() + + +void CKeyboardControl::Init(CWnd *parent, int octaves, bool cursorNotify) +{ + m_parent = parent; + m_nOctaves = std::max(1, octaves); + m_cursorNotify = cursorNotify; + MemsetZero(KeyFlags); + MemsetZero(m_sampleNum); + + // Point size to pixels + int fontSize = -MulDiv(60, Util::GetDPIy(m_hWnd), 720); + m_font.CreateFont(fontSize, 0, 0, 0, FW_NORMAL, FALSE, FALSE, FALSE, DEFAULT_CHARSET, OUT_RASTER_PRECIS, CLIP_DEFAULT_PRECIS, CLEARTYPE_QUALITY, FIXED_PITCH | FF_DONTCARE, _T("MS Shell Dlg")); +} + + +void CKeyboardControl::OnDestroy() +{ + m_font.DeleteObject(); +} + + +void CKeyboardControl::DrawKey(CPaintDC &dc, const CRect rect, int key, bool black) const +{ + const bool selected = (key == m_nSelection); + COLORREF color = black ? RGB(20, 20, 20) : RGB(255, 255, 255); + if(m_mouseDown && selected) + color = black ? RGB(104, 104, 104) : RGB(212, 212, 212); + else if(selected) + color = black ? RGB(130, 130, 130) : RGB(228, 228, 228); + dc.SetDCBrushColor(color); + dc.Rectangle(&rect); + + if(static_cast<size_t>(key) < std::size(KeyFlags) && KeyFlags[key] != KEYFLAG_NORMAL) + { + const int margin = black ? 0 : 2; + CRect ellipseRect(rect.left + margin, rect.bottom - rect.Width() + margin, rect.right - margin, rect.bottom - margin); + dc.SetDCBrushColor((KeyFlags[key] & KEYFLAG_BRIGHTDOT) ? RGB(255, 192, 192) : RGB(255, 0, 0)); + dc.Ellipse(ellipseRect); + if(m_sampleNum[key] != 0) + { + dc.SetTextColor((KeyFlags[key] & KEYFLAG_BRIGHTDOT) ? RGB(0, 0, 0) : RGB(255, 255, 255)); + TCHAR s[16]; + wsprintf(s, _T("%u"), m_sampleNum[key]); + dc.DrawText(s, -1, ellipseRect, DT_CENTER | DT_SINGLELINE | DT_VCENTER); + } + + if(KeyFlags[key] == (KEYFLAG_REDDOT | KEYFLAG_BRIGHTDOT)) + { + // Both flags set: Draw second dot + ellipseRect.MoveToY(ellipseRect.top - ellipseRect.Height() - 2); + dc.SetDCBrushColor(RGB(255, 0, 0)); + dc.Ellipse(ellipseRect); + } + } +} + + +void CKeyboardControl::OnPaint() +{ + CRect rcClient, rect; + CPaintDC dc(this); + + dc.SetBkMode(TRANSPARENT); + GetClientRect(&rcClient); + rect = rcClient; + auto oldBrush = dc.SelectObject(GetStockObject(DC_BRUSH)); + auto oldPen = dc.SelectObject(GetStockObject(DC_PEN)); + auto oldFont = dc.SelectObject(&m_font); + + // Rectangle outline + dc.SetDCPenColor(RGB(50, 50, 50)); + + // White notes + for(int note = 0; note < m_nOctaves * 7; note++) + { + rect.right = ((note + 1) * rcClient.Width()) / (m_nOctaves * 7); + int val = (note / 7) * 12 + whitetab[note % 7]; + + DrawKey(dc, rect, val, false); + + rect.left = rect.right - 1; + } + + // Black notes + rect = rcClient; + rect.bottom -= rcClient.Height() / 3; + for(int note = 0; note < m_nOctaves * 7; note++) + { + switch(note % 7) + { + case 1: + case 2: + case 4: + case 5: + case 6: + { + rect.left = (note * rcClient.Width()) / (m_nOctaves * 7); + rect.right = rect.left; + int delta = rcClient.Width() / (m_nOctaves * 7 * 3); + rect.left -= delta; + rect.right += delta; + int val = (note / 7) * 12 + blacktab[note % 7]; + + DrawKey(dc, rect, val, true); + break; + } + } + } + + dc.SelectObject(oldBrush); + dc.SelectObject(oldPen); + dc.SelectObject(oldFont); +} + + +void CKeyboardControl::OnMouseMove(UINT flags, CPoint point) +{ + CRect rcClient, rect; + GetClientRect(&rcClient); + rect = rcClient; + int xmin = rcClient.right; + int xmax = rcClient.left; + int sel = -1; + // White notes + for(int note = 0; note < m_nOctaves * 7; note++) + { + int val = (note / 7) * 12 + whitetab[note % 7]; + rect.right = ((note + 1) * rcClient.Width()) / (m_nOctaves * 7); + if (val == m_nSelection) + { + if (rect.left < xmin) xmin = rect.left; + if (rect.right > xmax) xmax = rect.right; + } + if (rect.PtInRect(point)) + { + sel = val; + if (rect.left < xmin) xmin = rect.left; + if (rect.right > xmax) xmax = rect.right; + } + rect.left = rect.right - 1; + } + // Black notes + rect = rcClient; + rect.bottom -= rcClient.Height() / 3; + for(int note = 0; note < m_nOctaves * 7; note++) + { + switch(note % 7) + { + case 1: + case 2: + case 4: + case 5: + case 6: + { + int val = (note / 7) * 12 + blacktab[note % 7]; + rect.left = (note * rcClient.Width()) / (m_nOctaves * 7); + rect.right = rect.left; + int delta = rcClient.Width() / (m_nOctaves * 7 * 3); + rect.left -= delta; + rect.right += delta; + if(val == m_nSelection) + { + if(rect.left < xmin) + xmin = rect.left; + if(rect.right > xmax) + xmax = rect.right; + } + if(rect.PtInRect(point)) + { + sel = val; + if(rect.left < xmin) + xmin = rect.left; + if(rect.right > xmax) + xmax = rect.right; + } + break; + } + } + } + // Check for selection change + if(sel != m_nSelection) + { + m_nSelection = sel; + rcClient.left = xmin; + rcClient.right = xmax; + InvalidateRect(&rcClient, FALSE); + if(m_cursorNotify && m_parent) + { + m_parent->PostMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_MOUSEMOVE, m_nSelection); + if(flags & MK_LBUTTON) + m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONDOWN, m_nSelection); + } + } + if(sel >= 0) + { + if(!m_mouseCapture) + { + m_mouseCapture = true; + SetCapture(); + } + } else + { + if(m_mouseCapture) + { + m_mouseCapture = false; + ReleaseCapture(); + } + } +} + + +void CKeyboardControl::OnLButtonDown(UINT, CPoint) +{ + m_mouseDown = true; + InvalidateRect(nullptr, FALSE); + if(m_parent) + m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONDOWN, m_nSelection); +} + + +void CKeyboardControl::OnLButtonUp(UINT, CPoint) +{ + m_mouseDown = false; + InvalidateRect(nullptr, FALSE); + if(m_parent) + m_parent->SendMessage(WM_MOD_KBDNOTIFY, KBDNOTIFY_LBUTTONUP, m_nSelection); +} + + +//////////////////////////////////////////////////////////////////////////////// +// +// Sample Map +// + +BEGIN_MESSAGE_MAP(CSampleMapDlg, CDialog) + ON_MESSAGE(WM_MOD_KBDNOTIFY, &CSampleMapDlg::OnKeyboardNotify) + ON_WM_HSCROLL() + ON_COMMAND(IDC_CHECK1, &CSampleMapDlg::OnUpdateSamples) + ON_CBN_SELCHANGE(IDC_COMBO1, &CSampleMapDlg::OnUpdateKeyboard) +END_MESSAGE_MAP() + +void CSampleMapDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(CSampleMapDlg) + DDX_Control(pDX, IDC_KEYBOARD1, m_Keyboard); + DDX_Control(pDX, IDC_COMBO1, m_CbnSample); + DDX_Control(pDX, IDC_SLIDER1, m_SbOctave); + //}}AFX_DATA_MAP +} + + +BOOL CSampleMapDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if(pIns) + { + for(UINT i = 0; i < NOTE_MAX; i++) + { + KeyboardMap[i] = pIns->Keyboard[i]; + } + } + m_Keyboard.Init(this, 3, TRUE); + m_SbOctave.SetRange(0, 7); + m_SbOctave.SetPos(4); + OnUpdateSamples(); + OnUpdateOctave(); + return TRUE; +} + + +void CSampleMapDlg::OnHScroll(UINT nCode, UINT nPos, CScrollBar *pBar) +{ + CDialog::OnHScroll(nCode, nPos, pBar); + OnUpdateKeyboard(); + OnUpdateOctave(); +} + + +void CSampleMapDlg::OnUpdateSamples() +{ + UINT oldPos = 0; + UINT newPos = 0; + + if(m_nInstrument >= MAX_INSTRUMENTS) + return; + if(m_CbnSample.GetCount() > 0) + oldPos = static_cast<UINT>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel())); + m_CbnSample.SetRedraw(FALSE); + m_CbnSample.ResetContent(); + const bool showAll = (IsDlgButtonChecked(IDC_CHECK1) != FALSE) || (*std::max_element(std::begin(KeyboardMap), std::end(KeyboardMap)) == 0); + + UINT insertPos = m_CbnSample.AddString(_T("0: No sample")); + m_CbnSample.SetItemData(insertPos, 0); + + for(SAMPLEINDEX i = 1; i <= sndFile.GetNumSamples(); i++) + { + bool isUsed = showAll || mpt::contains(KeyboardMap, i); + if(isUsed) + { + CString sampleName; + sampleName.Format(_T("%d: %s"), i, mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.GetSampleName(i)).GetString()); + insertPos = m_CbnSample.AddString(sampleName); + + m_CbnSample.SetItemData(insertPos, i); + if(i == oldPos) + newPos = insertPos; + } + } + m_CbnSample.SetRedraw(TRUE); + m_CbnSample.SetCurSel(newPos); + OnUpdateKeyboard(); +} + + +void CSampleMapDlg::OnUpdateOctave() +{ + TCHAR s[64]; + const UINT baseOctave = m_SbOctave.GetPos() & 7; + wsprintf(s, _T("Octaves %u-%u"), baseOctave, baseOctave + 2); + SetDlgItemText(IDC_TEXT1, s); +} + + + +void CSampleMapDlg::OnUpdateKeyboard() +{ + SAMPLEINDEX nSample = static_cast<SAMPLEINDEX>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel())); + const UINT baseOctave = m_SbOctave.GetPos() & 7; + bool redraw = false; + for(UINT iNote = 0; iNote < 3 * 12; iNote++) + { + uint8 oldFlags = m_Keyboard.GetFlags(iNote); + SAMPLEINDEX oldSmp = m_Keyboard.GetSample(iNote); + UINT ndx = baseOctave * 12 + iNote; + uint8 newFlags = CKeyboardControl::KEYFLAG_NORMAL; + if(KeyboardMap[ndx] == nSample) + newFlags = CKeyboardControl::KEYFLAG_REDDOT; + else if(KeyboardMap[ndx] != 0) + newFlags = CKeyboardControl::KEYFLAG_BRIGHTDOT; + if(newFlags != oldFlags || oldSmp != KeyboardMap[ndx]) + { + m_Keyboard.SetFlags(iNote, newFlags); + m_Keyboard.SetSample(iNote, KeyboardMap[ndx]); + redraw = true; + } + } + if(redraw) + m_Keyboard.InvalidateRect(NULL, FALSE); +} + + +LRESULT CSampleMapDlg::OnKeyboardNotify(WPARAM wParam, LPARAM lParam) +{ + TCHAR s[32] = _T("--"); + + if((lParam >= 0) && (lParam < 3 * 12)) + { + const SAMPLEINDEX sample = static_cast<SAMPLEINDEX>(m_CbnSample.GetItemData(m_CbnSample.GetCurSel())); + const uint32 baseOctave = m_SbOctave.GetPos() & 7; + + const CString temp = mpt::ToCString(sndFile.GetNoteName(static_cast<ModCommand::NOTE>(lParam + 1 + 12 * baseOctave), m_nInstrument)); + if(temp.GetLength() >= mpt::saturate_cast<int>(std::size(s))) + wsprintf(s, _T("%s"), _T("...")); + else + wsprintf(s, _T("%s"), temp.GetString()); + + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if((wParam == KBDNOTIFY_LBUTTONDOWN) && (sample < MAX_SAMPLES) && (pIns)) + { + const uint32 note = static_cast<uint32>(baseOctave * 12 + lParam); + + if(mouseAction == mouseUnknown) + { + // Mouse down -> decide if we are going to set or remove notes + mouseAction = mouseSet; + if(KeyboardMap[note] == sample) + { + mouseAction = (KeyboardMap[note] == pIns->Keyboard[note]) ? mouseZero : mouseUnset; + } + } + + switch(mouseAction) + { + case mouseSet: + KeyboardMap[note] = sample; + break; + case mouseUnset: + KeyboardMap[note] = pIns->Keyboard[note]; + break; + case mouseZero: + if(KeyboardMap[note] == sample) + { + KeyboardMap[note] = 0; + } + break; + } + OnUpdateKeyboard(); + } + } + if(wParam == KBDNOTIFY_LBUTTONUP) + { + mouseAction = mouseUnknown; + } + SetDlgItemText(IDC_TEXT2, s); + return 0; +} + + +void CSampleMapDlg::OnOK() +{ + ModInstrument *pIns = sndFile.Instruments[m_nInstrument]; + if(pIns) + { + bool modified = false; + for(UINT i = 0; i < NOTE_MAX; i++) + { + if(KeyboardMap[i] != pIns->Keyboard[i]) + { + pIns->Keyboard[i] = KeyboardMap[i]; + modified = true; + } + } + if(modified) + { + CDialog::OnOK(); + return; + } + } + CDialog::OnCancel(); +} + + +//////////////////////////////////////////////////////////////////////////////////////////// +// Edit history dialog + +BEGIN_MESSAGE_MAP(CEditHistoryDlg, ResizableDialog) + ON_COMMAND(IDC_BTN_CLEAR, &CEditHistoryDlg::OnClearHistory) +END_MESSAGE_MAP() + + +BOOL CEditHistoryDlg::OnInitDialog() +{ + ResizableDialog::OnInitDialog(); + + CString s; + uint64 totalTime = 0; + const auto &editHistory = m_modDoc.GetSoundFile().GetFileHistory(); + const bool isEmpty = editHistory.empty(); + + for(const auto &entry : editHistory) + { + totalTime += entry.openTime; + + // Date + CString sDate; + if(entry.HasValidDate()) + { + TCHAR szDate[32]; + _tcsftime(szDate, std::size(szDate), _T("%d %b %Y, %H:%M:%S"), &entry.loadDate); + sDate = szDate; + } else + { + sDate = _T("<unknown date>"); + } + // Time + stuff + uint32 duration = mpt::saturate_round<uint32>(entry.openTime / HISTORY_TIMER_PRECISION); + s += MPT_CFORMAT("Loaded {}, open for {}h {}m {}s\r\n")( + sDate, mpt::cfmt::dec(duration / 3600), mpt::cfmt::dec0<2>((duration / 60) % 60), mpt::cfmt::dec0<2>(duration % 60)); + } + if(isEmpty) + { + s = _T("No information available about the previous edit history of this module."); + } + SetDlgItemText(IDC_EDIT_HISTORY, s); + + // Total edit time + s.Empty(); + if(totalTime) + { + totalTime = mpt::saturate_round<uint64>(totalTime / HISTORY_TIMER_PRECISION); + + s.Format(_T("Total edit time: %lluh %02llum %02llus (%zu session%s)"), totalTime / 3600, (totalTime / 60) % 60, totalTime % 60, editHistory.size(), (editHistory.size() != 1) ? _T("s") : _T("")); + SetDlgItemText(IDC_TOTAL_EDIT_TIME, s); + // Window title + s.Format(_T("Edit History for %s"), m_modDoc.GetTitle().GetString()); + SetWindowText(s); + } + // Enable or disable Clear button + GetDlgItem(IDC_BTN_CLEAR)->EnableWindow(isEmpty ? FALSE : TRUE); + + return TRUE; +} + + +void CEditHistoryDlg::OnClearHistory() +{ + if(!m_modDoc.GetSoundFile().GetFileHistory().empty()) + { + m_modDoc.GetSoundFile().GetFileHistory().clear(); + m_modDoc.SetModified(); + OnInitDialog(); + } +} + + +///////////////////////////////////////////////////////////////////////// +// Generic input dialog + +void CInputDlg::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + if(m_minValueInt == m_maxValueInt && m_minValueDbl == m_maxValueDbl) + { + // Only need this for freeform text + DDX_Control(pDX, IDC_EDIT1, m_edit); + } + DDX_Control(pDX, IDC_SPIN1, m_spin); +} + + +BOOL CInputDlg::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetDlgItemText(IDC_PROMPT, m_description); + + // Get all current control sizes and positions + CRect windowRect, labelRect, inputRect, okRect, cancelRect; + GetWindowRect(windowRect); + GetDlgItem(IDC_PROMPT)->GetWindowRect(labelRect); + GetDlgItem(IDC_EDIT1)->GetWindowRect(inputRect); + GetDlgItem(IDOK)->GetWindowRect(okRect); + GetDlgItem(IDCANCEL)->GetWindowRect(cancelRect); + ScreenToClient(labelRect); + ScreenToClient(inputRect); + ScreenToClient(okRect); + ScreenToClient(cancelRect); + + // Find out how big our label shall be + HDC dc = ::GetDC(m_hWnd); + CRect textRect(0,0,0,0); + DrawText(dc, m_description, m_description.GetLength(), textRect, DT_CALCRECT); + LPtoDP(dc, &textRect.BottomRight(), 1); + ::ReleaseDC(m_hWnd, dc); + if(textRect.right < 320) textRect.right = 320; + const int windowWidth = windowRect.Width() - labelRect.Width() + textRect.right; + const int windowHeight = windowRect.Height() - labelRect.Height() + textRect.bottom; + + // Resize and move all controls + GetDlgItem(IDC_PROMPT)->SetWindowPos(nullptr, 0, 0, textRect.right, textRect.bottom, SWP_NOMOVE | SWP_NOZORDER); + GetDlgItem(IDC_EDIT1)->SetWindowPos(nullptr, inputRect.left, labelRect.top + textRect.bottom + (inputRect.top - labelRect.bottom), textRect.right, inputRect.Height(), SWP_NOZORDER); + GetDlgItem(IDOK)->SetWindowPos(nullptr, windowWidth - (windowRect.Width() - okRect.left), windowHeight - (windowRect.Height() - okRect.top), 0, 0, SWP_NOSIZE | SWP_NOZORDER); + GetDlgItem(IDCANCEL)->SetWindowPos(nullptr, windowWidth - (windowRect.Width() - cancelRect.left), windowHeight - (windowRect.Height() - cancelRect.top), 0, 0, SWP_NOSIZE | SWP_NOZORDER); + SetWindowPos(nullptr, 0, 0, windowWidth, windowHeight, SWP_NOMOVE | SWP_NOZORDER); + + if(m_minValueInt != m_maxValueInt) + { + // Numeric (int) + m_spin.SetRange32(m_minValueInt, m_maxValueInt); + m_edit.SubclassDlgItem(IDC_EDIT1, this); + m_edit.ModifyStyle(0, ES_NUMBER); + m_edit.AllowNegative(m_minValueInt < 0); + m_edit.AllowFractions(false); + SetDlgItemInt(IDC_EDIT1, resultAsInt); + m_spin.SetBuddy(&m_edit); + } else if(m_minValueDbl != m_maxValueDbl) + { + // Numeric (double) + m_spin.SetRange32(static_cast<int32>(m_minValueDbl), static_cast<int32>(m_maxValueDbl)); + m_edit.SubclassDlgItem(IDC_EDIT1, this); + m_edit.ModifyStyle(0, ES_NUMBER); + m_edit.AllowNegative(m_minValueDbl < 0); + m_edit.AllowFractions(true); + m_edit.SetDecimalValue(resultAsDouble); + m_spin.SetBuddy(&m_edit); + } else + { + // Text + m_spin.ShowWindow(SW_HIDE); + if(m_maxLength > 0) + Edit_LimitText(m_edit, m_maxLength); + SetDlgItemText(IDC_EDIT1, resultAsString); + } + + return TRUE; +} + + +void CInputDlg::OnOK() +{ + CDialog::OnOK(); + GetDlgItemText(IDC_EDIT1, resultAsString); + resultAsInt = static_cast<int32>(GetDlgItemInt(IDC_EDIT1)); + Limit(resultAsInt, m_minValueInt, m_maxValueInt); + m_edit.GetDecimalValue(resultAsDouble); + Limit(resultAsDouble, m_minValueDbl, m_maxValueDbl); +} + + +/////////////////////////////////////////////////////////////////////////////////////// +// Messagebox with 'don't show again'-option. + +class CMsgBoxHidable : public CDialog +{ +public: + CMsgBoxHidable(const TCHAR *strMsg, bool checkStatus = true, CWnd* pParent = NULL); + enum { IDD = IDD_MSGBOX_HIDABLE }; + + const TCHAR *m_StrMsg; + int m_nCheckStatus; +protected: + void DoDataExchange(CDataExchange* pDX) override; // DDX/DDV support + BOOL OnInitDialog() override; +}; + + +struct MsgBoxHidableMessage +{ + const TCHAR *message; + uint32 mask; + bool defaultDontShowAgainStatus; // true for don't show again, false for show again. +}; + +static constexpr MsgBoxHidableMessage HidableMessages[] = +{ + { _T("Note: First two bytes of oneshot samples are silenced for ProTracker compatibility."), 1, true }, + { _T("Hint: To create IT-files without MPT-specific extensions included, try compatibility export from File-menu."), 1 << 1, true }, + { _T("Press OK to apply signed/unsigned conversion\n (note: this often significantly increases volume level)"), 1 << 2, false }, + { _T("Hint: To create XM-files without MPT-specific extensions included, try compatibility export from File-menu."), 1 << 3, true }, + { _T("Warning: The exported file will not contain any of MPT's file format hacks."), 1 << 4, true }, +}; + +static_assert(mpt::array_size<decltype(HidableMessages)>::size == enMsgBoxHidableMessage_count); + +// Messagebox with 'don't show this again'-checkbox. Uses parameter 'enMsg' +// to get the needed information from message array, and updates the variable that +// controls the show/don't show-flags. +void MsgBoxHidable(enMsgBoxHidableMessage enMsg) +{ + // Check whether the message should be shown. + if((TrackerSettings::Instance().gnMsgBoxVisiblityFlags & HidableMessages[enMsg].mask) == 0) + return; + + // Show dialog. + CMsgBoxHidable dlg(HidableMessages[enMsg].message, HidableMessages[enMsg].defaultDontShowAgainStatus); + dlg.DoModal(); + + // Update visibility flags. + const uint32 mask = HidableMessages[enMsg].mask; + if(dlg.m_nCheckStatus == BST_CHECKED) + TrackerSettings::Instance().gnMsgBoxVisiblityFlags &= ~mask; + else + TrackerSettings::Instance().gnMsgBoxVisiblityFlags |= mask; +} + + +CMsgBoxHidable::CMsgBoxHidable(const TCHAR *strMsg, bool checkStatus, CWnd* pParent) + : CDialog(CMsgBoxHidable::IDD, pParent) + , m_StrMsg(strMsg) + , m_nCheckStatus((checkStatus) ? BST_CHECKED : BST_UNCHECKED) +{} + +BOOL CMsgBoxHidable::OnInitDialog() +{ + CDialog::OnInitDialog(); + SetDlgItemText(IDC_MESSAGETEXT, m_StrMsg); + SetWindowText(AfxGetAppName()); + return TRUE; +} + +void CMsgBoxHidable::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Check(pDX, IDC_DONTSHOWAGAIN, m_nCheckStatus); +} + + +///////////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////////// + + +void AppendNotesToControl(CComboBox& combobox, ModCommand::NOTE noteStart, ModCommand::NOTE noteEnd) +{ + const ModCommand::NOTE upperLimit = std::min(ModCommand::NOTE(NOTE_MAX), noteEnd); + for(ModCommand::NOTE note = noteStart; note <= upperLimit; note++) + combobox.SetItemData(combobox.AddString(mpt::ToCString(CSoundFile::GetNoteName(note, CSoundFile::GetDefaultNoteNames()))), note); +} + + +void AppendNotesToControlEx(CComboBox& combobox, const CSoundFile &sndFile, INSTRUMENTINDEX nInstr, ModCommand::NOTE noteStart, ModCommand::NOTE noteEnd) +{ + bool addSpecial = noteStart == noteEnd; + if(noteStart == noteEnd) + { + noteStart = sndFile.GetModSpecifications().noteMin; + noteEnd = sndFile.GetModSpecifications().noteMax; + } + for(ModCommand::NOTE note = noteStart; note <= noteEnd; note++) + { + combobox.SetItemData(combobox.AddString(mpt::ToCString(sndFile.GetNoteName(note, nInstr))), note); + } + if(addSpecial) + { + for(ModCommand::NOTE note = NOTE_MIN_SPECIAL - 1; note++ < NOTE_MAX_SPECIAL;) + { + if(sndFile.GetModSpecifications().HasNote(note)) + combobox.SetItemData(combobox.AddString(szSpecialNoteNamesMPT[note - NOTE_MIN_SPECIAL]), note); + } + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.h b/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.h new file mode 100644 index 00000000..9618bad5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/dlg_misc.h @@ -0,0 +1,329 @@ +/* + * dlg_misc.h + * ---------- + * Purpose: Implementation for various OpenMPT dialogs. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CDecimalSupport.h" +#include "ResizableDialog.h" + +OPENMPT_NAMESPACE_BEGIN + +class Version; +class CSoundFile; +class CModDoc; +class CDLSBank; + +class CModTypeDlg: public CDialog +{ +protected: + CComboBox m_TypeBox, m_ChannelsBox, m_TempoModeBox, m_PlugMixBox; + CButton m_CheckBox1, m_CheckBox2, m_CheckBox3, m_CheckBox4, m_CheckBox5, m_CheckBoxPT1x, m_CheckBoxFt2VolRamp, m_CheckBoxAmigaLimits; + HICON m_warnIcon = nullptr; + + CSoundFile &sndFile; +public: + TempoSwing m_tempoSwing; + PlayBehaviourSet m_playBehaviour; + CHANNELINDEX m_nChannels = 0; + MODTYPE m_nType = MOD_TYPE_NONE; + bool initialized = false; + +public: + CModTypeDlg(CSoundFile &sf, CWnd *parent) : CDialog(IDD_MODDOC_MODTYPE, parent), sndFile(sf) { } + bool VerifyData(); + void UpdateDialog(); + void OnPTModeChanged(); + void OnTempoModeChanged(); + void OnTempoSwing(); + void OnLegacyPlaybackSettings(); + void OnDefaultBehaviour(); + +protected: + void UpdateChannelCBox(); + CString FormatVersionNumber(Version version); + +protected: + //{{AFX_VIRTUAL(CModTypeDlg) + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + //}}AFX_VIRTUAL + + BOOL OnToolTipNotify(UINT id, NMHDR* pNMHDR, LRESULT* pResult); + + DECLARE_MESSAGE_MAP() +}; + + +class CLegacyPlaybackSettingsDlg : public ResizableDialog +{ +protected: + CCheckListBox m_CheckList; + PlayBehaviourSet m_playBehaviour; + MODTYPE m_modType; + +public: + CLegacyPlaybackSettingsDlg(CWnd *parent, PlayBehaviourSet &playBehaviour, MODTYPE modType) + : ResizableDialog{IDD_LEGACY_PLAYBACK, parent} + , m_playBehaviour{playBehaviour} + , m_modType{modType} + { + } + + PlayBehaviourSet GetPlayBehaviour() const { return m_playBehaviour; } + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + + afx_msg void OnSelectDefaults(); + afx_msg void UpdateSelectDefaults(); + afx_msg void OnFilterStringChanged(); + + DECLARE_MESSAGE_MAP() +}; + + +class CRemoveChannelsDlg: public CDialog +{ +public: + CSoundFile &sndFile; + std::vector<bool> m_bKeepMask; + CHANNELINDEX m_nRemove; + CListBox m_RemChansList; + bool m_ShowCancel; + +public: + CRemoveChannelsDlg(CSoundFile &sf, CHANNELINDEX toRemove, bool showCancel = true, CWnd *parent = nullptr) + : CDialog{IDD_REMOVECHANNELS, parent} + , sndFile{sf} + , m_bKeepMask(sf.GetNumChannels(), true) + , m_nRemove{toRemove} + , m_ShowCancel{showCancel} + { + } + +protected: + //{{AFX_VIRTUAL(CRemoveChannelsDlg) + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + //}}AFX_VIRTUAL + //{{AFX_MSG(CRemoveChannelsDlg) + afx_msg void OnChannelChanged(); + //}}AFX_MSG + DECLARE_MESSAGE_MAP(); +}; + + +class InfoDialog : protected ResizableDialog +{ +private: + mpt::winstring m_caption, m_content; + +public: + InfoDialog(CWnd *parent = nullptr); + void SetCaption(mpt::winstring caption); + void SetContent(mpt::winstring content); + using ResizableDialog::DoModal; + +protected: + BOOL OnInitDialog() override; +}; + +//////////////////////////////////////////////////////////////////////// +// Sound Banks + +class CSoundBankProperties : public InfoDialog +{ +public: + CSoundBankProperties(const CDLSBank &bank, CWnd *parent = nullptr); +}; + + +///////////////////////////////////////////////////////////////////////// +// Keyboard control + +enum +{ + KBDNOTIFY_MOUSEMOVE=0, + KBDNOTIFY_LBUTTONDOWN, + KBDNOTIFY_LBUTTONUP, +}; + +class CKeyboardControl: public CWnd +{ +public: + enum + { + KEYFLAG_NORMAL = 0x00, + KEYFLAG_REDDOT = 0x01, + KEYFLAG_BRIGHTDOT = 0x02, + }; +protected: + CWnd *m_parent = nullptr; + CFont m_font; + int m_nOctaves = 1; + int m_nSelection = -1; + bool m_mouseCapture = false, m_cursorNotify = false; + bool m_mouseDown = false; + + uint8 KeyFlags[NOTE_MAX]; // 10 octaves max + SAMPLEINDEX m_sampleNum[NOTE_MAX]; + +public: + CKeyboardControl() = default; + +public: + void Init(CWnd *parent, int octaves = 1, bool cursorNotify = false); + void SetFlags(UINT key, uint8 flags) { if (key < NOTE_MAX) KeyFlags[key] = flags; } + uint8 GetFlags(UINT key) const { return (key < NOTE_MAX) ? KeyFlags[key] : 0; } + void SetSample(UINT key, SAMPLEINDEX sample) { if (key < NOTE_MAX) m_sampleNum[key] = sample; } + SAMPLEINDEX GetSample(UINT key) const { return (key < NOTE_MAX) ? m_sampleNum[key] : 0; } + +protected: + void DrawKey(CPaintDC &dc, const CRect rect, int key, bool black) const; + + afx_msg void OnDestroy(); + afx_msg void OnPaint(); + afx_msg void OnMouseMove(UINT nFlags, CPoint point); + afx_msg void OnLButtonDown(UINT nFlags, CPoint point); + afx_msg void OnLButtonUp(UINT nFlags, CPoint point); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Sample Map + +class CSampleMapDlg: public CDialog +{ +protected: + enum MouseAction + { + mouseUnknown, // Didn't mouse-down yet + mouseSet, // Set selected sample + mouseUnset, // Unset (revert to original keymap) + mouseZero, // Set to zero + }; + + CKeyboardControl m_Keyboard; + CComboBox m_CbnSample; + CSliderCtrl m_SbOctave; + CSoundFile &sndFile; + INSTRUMENTINDEX m_nInstrument; + SAMPLEINDEX KeyboardMap[NOTE_MAX]; + MouseAction mouseAction; + +public: + CSampleMapDlg(CSoundFile &sf, INSTRUMENTINDEX nInstr, CWnd *parent=NULL) : CDialog(IDD_EDITSAMPLEMAP, parent), sndFile(sf), mouseAction(mouseUnknown) + { m_nInstrument = nInstr; } + +protected: + void DoDataExchange(CDataExchange* pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; + afx_msg void OnUpdateSamples(); + afx_msg void OnUpdateKeyboard(); + afx_msg void OnUpdateOctave(); + afx_msg void OnHScroll(UINT, UINT, CScrollBar *); + afx_msg LRESULT OnKeyboardNotify(WPARAM, LPARAM); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Edit history dialog + +class CEditHistoryDlg: public ResizableDialog +{ +protected: + CModDoc &m_modDoc; + +public: + CEditHistoryDlg(CWnd *parent, CModDoc &modDoc) + : ResizableDialog(IDD_EDITHISTORY, parent), m_modDoc(modDoc) {} + +protected: + BOOL OnInitDialog() override; + afx_msg void OnClearHistory(); + DECLARE_MESSAGE_MAP() +}; + + +///////////////////////////////////////////////////////////////////////// +// Generic input dialog + +class CInputDlg: public CDialog +{ +protected: + CNumberEdit m_edit; + CSpinButtonCtrl m_spin; + CString m_description; + double m_minValueDbl = 0.0, m_maxValueDbl = 0.0; + int32 m_minValueInt = 0, m_maxValueInt = 0; + int32 m_maxLength = 0; + +public: + int32 resultAsInt = 0; + double resultAsDouble = 0.0; + CString resultAsString; + +public: + // Initialize text input box + CInputDlg(CWnd *parent, const TCHAR *desc, const TCHAR *defaultString, int32 maxLength = -1) : CDialog(IDD_INPUT, parent) + , m_description(desc) + , m_maxLength(maxLength) + , resultAsString(defaultString) + { } + // Initialize numeric input box (float) + CInputDlg(CWnd *parent, const TCHAR *desc, double minVal, double maxVal, double defaultNumber) : CDialog(IDD_INPUT, parent) + , m_description(desc) + , m_minValueDbl(minVal), m_maxValueDbl(maxVal) + , resultAsDouble(defaultNumber) + { } + CInputDlg(CWnd *parent, const TCHAR *desc, float minVal, float maxVal, float defaultNumber) : CDialog(IDD_INPUT, parent) + , m_description(desc) + , m_minValueDbl(minVal), m_maxValueDbl(maxVal) + , resultAsDouble(defaultNumber) + { } + // Initialize numeric input box (int) + CInputDlg(CWnd *parent, const TCHAR *desc, int32 minVal, int32 maxVal, int32 defaultNumber) : CDialog(IDD_INPUT, parent) + , m_description(desc) + , m_minValueInt(minVal), m_maxValueInt(maxVal) + , resultAsInt(defaultNumber) + { } + +protected: + void DoDataExchange(CDataExchange *pDX) override; + BOOL OnInitDialog() override; + void OnOK() override; +}; + + +///////////////////////////////////////////////////////////////////////// +// Messagebox with 'don't show again'-option. + +// Enums for message entries. See dlg_misc.cpp for the array of entries. +enum enMsgBoxHidableMessage +{ + ModSaveHint = 0, + ItCompatibilityExportTip = 1, + ConfirmSignUnsignWhenPlaying = 2, + XMCompatibilityExportTip = 3, + CompatExportDefaultWarning = 4, + enMsgBoxHidableMessage_count +}; + +void MsgBoxHidable(enMsgBoxHidableMessage enMsg); + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/readme.txt b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/readme.txt new file mode 100644 index 00000000..8d6f5d22 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/readme.txt @@ -0,0 +1,8 @@ +Prerequisites:
+- HTML Help Workshop must be placed in the directory "include\htmlhelp"
+ (build\download_externals.cmd will take care of that)
+- Python 3
+
+Once these prerequisites are fulfilled, simply run wiki.py.
+This will download all required files into the "html" sub directory and prepare
+all files needed for HTML Help Workshop and spit out a CHM file.
diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/bullet.png b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/bullet.png Binary files differnew file mode 100644 index 00000000..7bae98f6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/bullet.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/external.png b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/external.png Binary files differnew file mode 100644 index 00000000..63083831 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/external.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/help.css b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/help.css new file mode 100644 index 00000000..9d47c14e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/source/help.css @@ -0,0 +1,47 @@ +body +{ + margin: 0; + padding: 0; + border: 0; + background: #fff; + color: #000; +} + +img +{ + border: 0; +} + +h1 +{ + background-color: #DFEFFF; + padding: 0.25em; + margin: 0; +} + +h1, h2 +{ + border-bottom: 1px solid #aaa; +} + +#content +{ + margin: 0 !important; + border: 0 !important; +} + +#toc +{ + display: none; +} + +ul +{ + list-style-image: url(bullet.png) !important; +} + +div#content a.external +{ + background: center right no-repeat url(external.png); + padding-right: 13px; +}
\ No newline at end of file diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/wiki.py b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/wiki.py new file mode 100644 index 00000000..3d26880e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/manual_generator/wiki.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +# OpenMPT help file scraper +# by coda (https://coda.s3m.us/) and Saga Musix (https://sagamusix.de/) +# This script downloads the OpenMPT manual TOC and then downloads all pages +# from that TOC. The pages are parsed and all required image files are fetched. +# The script also generates the appropriate files that can be fed into the +# HTML Help Workshop to generate a CHM file. + +from urllib.request import urlopen, urlretrieve +import re, os, shutil, subprocess + +base_url = 'https://wiki.openmpt.org' +base_url_regex = 'https?://wiki.openmpt.org' + +os.chdir(os.path.dirname(os.path.abspath(__file__))) + +shutil.rmtree('html', ignore_errors=True) +shutil.copytree('source', 'html') + +style = urlopen(base_url + '/load.php?debug=false&lang=en&modules=mediawiki.legacy.commonPrint%2Cshared%7Cmediawiki.page.gallery.styles%7Cmediawiki.skinning.interface%7Cskins.vector.styles%7Csite.styles&only=styles&skin=vector').read().decode('UTF-8') + +# Remove a few unused CSS classes +style = re.sub(r'\}(\w+)?[\.#]vector([\w >]+)\{.+?\}', '}', style) +style_file = open('html/style.css', 'w') +style_file.write(style) +style_file.close() + +toc_page = urlopen(base_url + '/index.php?title=Manual:_CHM_TOC&action=render').read().decode('UTF-8') + +pages = re.findall('href="' + base_url_regex + '/(.+?)"', toc_page) + +def destname(p): + p = p.split(':_')[1] + p = p.replace('/', '_') + p = p.replace('.', '_') + while p.find('__') >= 0: + p = p.replace('__', '_') + if p.find('#') >= 0: + parts = p.split('#') + return parts[0] + '.html#' + parts[1] + return p + '.html' + +def title(p): + p = p.split(':_')[1] + p = p.replace('_', ' ') + return p + +def localurl(p): + p = destname(p) + return p + +def replace_images(m): + global base_url + filepath = m.group(1) + '/' + m.group(2) + '/' + filename = m.group(3) + project.write(filename + "\n") + + urlretrieve(base_url + '/images/' + filepath + filename, 'html/' + filename) + return '"' + filename + '"' + +def fix_internal_links(m): + return '<a href="' + localurl(m.group(1)) + '"' + +project = open('html/OpenMPT Manual.hhp', 'w') + +project.write("""[OPTIONS] +Compatibility=1.1 or later +Compiled file=OpenMPT Manual.chm +Contents file=OpenMPT Manual.hhc +Display compile progress=No +Full-text search=Yes +Language=0x409 English (United States) +Title=OpenMPT Manual +Default Window=OpenMPT +Default topic=""" + localurl(pages[0]) + """ + +[WINDOWS] +OpenMPT=,"OpenMPT Manual.hhc",,""" + localurl(pages[0]) + """,,,,,,0x42520,215,0x300e,[20,20,780,580],0xb0000,,,,,,0 + +[FILES] +style.css +help.css +bullet.png +external.png +""") + +for p in pages: + content = urlopen(base_url + '/index.php?title=' + p + '&action=render').read().decode('UTF-8') + # Download and replace image URLs + content = re.sub(r' srcset=".+?"', '', content); + content = re.sub(r'"/images/thumb/(\w+)/(\w+)/([^\/]+?)/([^\/]+?)"', replace_images, content) + content = re.sub(r'"/images/(\w+)/(\w+)/([^\/]+?)"', replace_images, content) + # Remove comments + content = re.sub(r'<!--(.+?)-->', '', content, flags = re.DOTALL) + # Fix local URLs + content = re.sub(r'<a href="' + base_url_regex + '/File:', '<a href="', content) + content = re.sub(r'<a href="' + base_url_regex + '/(Manual:.+?)"', fix_internal_links, content) + content = re.sub(r'<a href="/(Manual:.+?)"', fix_internal_links, content) + # Remove templates that shouldn't turn up in the manual + content = re.sub(r'<div class="todo".+?</div>', '', content, flags = re.DOTALL); + content = re.sub(r'<p class="newversion".+?</p>', '', content, flags = re.DOTALL); + # Don't need this attribute in our CHM + content = re.sub(r' rel="nofollow"', '', content); + + section = re.match(r'(.+)/', title(p)) + section_str = '' + if section: + section_str = section.group(1) + + content = """<!DOCTYPE html> + <html lang="en"> + <head> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <link href="style.css" rel="stylesheet"> + <link href="help.css" rel="stylesheet"> + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> + <title>OpenMPT Manual - """ + title(p) + """</title> + </head> + <body> + <h1>""" + title(p) + '</h1><div id="content" class="mw-body">' + content + '</div></body></html>' + + saved = open('html/' + destname(p), 'wb') + + saved.write(bytes(content, 'UTF-8')) + saved.close() + + project.write(destname(p)+"\n") + print(p) + +project.close() + +# Create TOC +toc = open('html/OpenMPT Manual.hhc', 'w') + +toc.write(""" +<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN"> +<HTML> +<HEAD> +<meta name="GENERATOR" content="OpenMPT Help Generator"> +<!-- Sitemap 1.0 --> +</HEAD><BODY> +<OBJECT type="text/site properties"> + <param name="ImageType" value="Folder"> +</OBJECT> +""") + +def toc_parse(m): + return """<OBJECT type="text/sitemap"> + <param name="Name" value=\"""" + m.group(2) + """"> + <param name="Local" value=\"""" + localurl(m.group(1)) + """"> + </OBJECT>""" + +def toc_parse_chapter(m): + return """<li><OBJECT type="text/sitemap"> + <param name="Name" value=\"""" + m.group(1) + """"> + </OBJECT>""" + +toc_text = re.sub(r'<!--(.+?)-->', '', toc_page, flags = re.DOTALL) +toc_text = re.sub(r'<div(.+?)>', '', toc_text, flags = re.DOTALL) +toc_text = re.sub(r'</div>', '', toc_text, flags = re.DOTALL) +toc_text = re.sub(r'<a href="' + base_url_regex + '/(.+?)".*?>(.+?)</a>', toc_parse, toc_text) +toc_text = re.sub(r'<li>([^<]+)$', toc_parse_chapter, toc_text, flags = re.MULTILINE) +toc.write(toc_text) + +toc.write(""" +</BODY></HTML> +""") +toc.close() + +if(subprocess.call(['../../build/tools/htmlhelp/hhc.exe', '"html/OpenMPT Manual.hhp"']) != 1): + raise Exception("Something went wrong during manual creation!") +try: + os.remove('../../packageTemplate/html/OpenMPT Manual.chm') +except OSError: + pass +shutil.copy2('html/OpenMPT Manual.chm', '../../packageTemplate/') diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp new file mode 100644 index 00000000..aad37810 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.cpp @@ -0,0 +1,863 @@ +/* + * mod2midi.cpp + * ------------ + * Purpose: Module to MIDI conversion (dialog + conversion code). + * Notes : This code makes use of the existing MIDI plugin output functionality. + * 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 "Moddoc.h" +#include "../common/mptStringBuffer.h" +#include "../common/mptFileIO.h" +#include "mod2midi.h" +#include "../soundlib/plugins/PlugInterface.h" +#include "../soundlib/plugins/PluginManager.h" +#include <sstream> +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + +#ifndef NO_PLUGINS + +OPENMPT_NAMESPACE_BEGIN + +namespace MidiExport +{ + // MIDI file resolution + constexpr int32 ppq = 480; + + enum StringType : uint8 + { + kText = 1, + kCopyright = 2, + kTrackName = 3, + kInstrument = 4, + kLyric = 5, + kMarker = 6, + kCue = 7, + }; + + class MidiTrack final : public IMidiPlugin + { + ModInstrument m_instr; + const ModInstrument *const m_oldInstr; + const CSoundFile &m_sndFile; + const GetLengthType &m_songLength; + MidiTrack *const m_tempoTrack; // Pointer to tempo track, nullptr if this is the tempo track + decltype(m_MidiCh) *m_lastMidiCh = nullptr; + std::array<decltype(m_instr.midiPWD), 16> m_pitchWheelDepth = { 0 }; + + std::vector<std::array<char, 4>> m_queuedEvents; + + std::ostringstream f; + double m_tempo = 0.0; + double m_ticks = 0.0; // MIDI ticks since previous event + CSoundFile::samplecount_t m_samplePos = 0; // Current sample position + CSoundFile::samplecount_t m_prevEventTime = 0; // Sample position of previous event + uint32 m_sampleRate; + uint32 m_oldSigNumerator = 0; + int32 m_oldGlobalVol = -1; + const bool m_overlappingInstruments; + bool m_wroteLoopStart = false; + + // Calculate how many MIDI ticks have passed since the last written event + void UpdateTicksSinceLastEvent() + { + m_ticks += (m_samplePos - m_prevEventTime) * m_tempo * static_cast<double>(MidiExport::ppq) / (m_sampleRate * 60); + m_prevEventTime = m_samplePos; + } + + // Write delta tick count since last event + void WriteTicks() + { + uint32 ticks = (m_ticks <= 0) ? 0 : mpt::saturate_round<uint32>(m_ticks); + mpt::IO::WriteVarInt(f, ticks); + m_ticks -= ticks; + } + + // Update MIDI channel states in non-overlapping export mode so that all plugins have the same view + void SynchronizeMidiChannelState() + { + if(m_tempoTrack != nullptr && !m_overlappingInstruments) + { + if(m_tempoTrack->m_lastMidiCh != nullptr && m_tempoTrack->m_lastMidiCh != &m_MidiCh) + m_MidiCh = *m_tempoTrack->m_lastMidiCh; + m_tempoTrack->m_lastMidiCh = &m_MidiCh; + } + } + + void SynchronizeMidiPitchWheelDepth(CHANNELINDEX trackerChn) + { + if(trackerChn >= std::size(m_sndFile.m_PlayState.Chn)) + return; + const auto midiCh = GetMidiChannel(m_sndFile.m_PlayState.Chn[trackerChn], trackerChn); + if(!m_overlappingInstruments && m_tempoTrack && m_tempoTrack->m_pitchWheelDepth[midiCh] != m_instr.midiPWD) + WritePitchWheelDepth(static_cast<MidiChannel>(midiCh + MidiFirstChannel)); + } + + public: + + operator ModInstrument& () { return m_instr; } + + MidiTrack(VSTPluginLib &factory, CSoundFile &sndFile, const GetLengthType &songLength, SNDMIXPLUGIN *mixStruct, MidiTrack *tempoTrack, const mpt::ustring &name, const ModInstrument *oldInstr, bool overlappingInstruments) + : IMidiPlugin(factory, sndFile, mixStruct) + , m_oldInstr(oldInstr) + , m_sndFile(sndFile) + , m_songLength(songLength) + , m_tempoTrack(tempoTrack) + , m_sampleRate(sndFile.GetSampleRate()) + , m_overlappingInstruments(overlappingInstruments) + { + // Write instrument / song name + WriteString(kTrackName, name); + m_pMixStruct->pMixPlugin = this; + } + + void WritePitchWheelDepth(MidiChannel midiChOverride = MidiNoChannel) + { + // Set up MIDI pitch wheel depth + uint8 firstCh = 0, lastCh = 15; + if(midiChOverride != MidiNoChannel) + firstCh = lastCh = midiChOverride - MidiFirstChannel; + else if(m_instr.nMidiChannel != MidiMappedChannel && m_instr.nMidiChannel != MidiNoChannel) + firstCh = lastCh = m_instr.nMidiChannel - MidiFirstChannel; + + for(uint8 i = firstCh; i <= lastCh; i++) + { + const uint8 ch = 0xB0 | i; + const uint8 msg[] = { ch, 0x64, 0x00, 0x00, ch, 0x65, 0x00, 0x00, ch, 0x06, static_cast<uint8>(std::abs(m_instr.midiPWD)) }; + WriteTicks(); + mpt::IO::WriteRaw(f, msg, sizeof(msg)); + if(m_tempoTrack) + m_tempoTrack->m_pitchWheelDepth[i] = m_instr.midiPWD; + } + } + + void UpdateGlobals() + { + m_samplePos = m_sndFile.GetTotalSampleCount(); + m_sampleRate = m_sndFile.GetSampleRate(); + + const double curTempo = m_sndFile.GetCurrentBPM(); + const ROWINDEX rpb = std::max(m_sndFile.m_PlayState.m_nCurrentRowsPerBeat, ROWINDEX(1)); + const uint32 timeSigNumerator = std::max(m_sndFile.m_PlayState.m_nCurrentRowsPerMeasure, rpb) / rpb; + + const bool tempoChanged = curTempo != m_tempo; + const bool sigChanged = timeSigNumerator != m_oldSigNumerator; + const bool volChanged = m_sndFile.m_PlayState.m_nGlobalVolume != m_oldGlobalVol; + + if(curTempo > 0.0) + m_tempo = curTempo; + m_oldSigNumerator = timeSigNumerator; + m_oldGlobalVol = m_sndFile.m_PlayState.m_nGlobalVolume; + + if(m_tempoTrack != nullptr) + return; + + // This is the tempo track + if(tempoChanged && curTempo > 0.0) + { + // Write MIDI tempo + WriteTicks(); + + uint32 mspq = mpt::saturate_round<uint32>(60000000.0 / curTempo); + uint8 msg[6] = { 0xFF, 0x51, 0x03, static_cast<uint8>(mspq >> 16), static_cast<uint8>(mspq >> 8), static_cast<uint8>(mspq) }; + mpt::IO::WriteRaw(f, msg, 6); + } + + if(sigChanged) + { + // Write MIDI time signature + WriteTicks(); + + uint8 msg[7] = { 0xFF, 0x58, 0x04, static_cast<uint8>(timeSigNumerator), 2, 24, 8 }; + mpt::IO::WriteRaw(f, msg, 7); + } + + if(volChanged) + { + // Write MIDI master volume + WriteTicks(); + + int32 midiVol = Util::muldiv(m_oldGlobalVol, 0x3FFF, MAX_GLOBAL_VOLUME); + uint8 msg[9] = { 0xF0, 0x07, 0x7F, 0x7F, 0x04, 0x01, static_cast<uint8>(midiVol & 0x7F), static_cast<uint8>((midiVol >> 7) & 0x7F), 0xF7 }; + mpt::IO::WriteRaw(f, msg, 9); + } + + if(!m_tempoTrack && !m_wroteLoopStart && m_sndFile.m_PlayState.m_nRow == m_songLength.lastRow && m_sndFile.m_PlayState.m_nCurrentOrder == m_songLength.lastOrder) + { + WriteString(kCue, U_("loopStart")); + m_wroteLoopStart = true; + } + } + + void Process(float *, float *, uint32 numFrames) override + { + UpdateGlobals(); + if(m_tempoTrack != nullptr) + m_tempoTrack->UpdateGlobals(); + + for(const auto &midiData : m_queuedEvents) + { + WriteTicks(); + mpt::IO::WriteRaw(f, midiData.data(), MIDIEvents::GetEventLength(midiData[0])); + } + m_queuedEvents.clear(); + + m_samplePos += numFrames; + if (m_tempoTrack != nullptr) + { + m_tempoTrack->m_samplePos = std::max(m_tempoTrack->m_samplePos, m_samplePos); + m_tempoTrack->UpdateTicksSinceLastEvent(); + } + UpdateTicksSinceLastEvent(); + } + + // Write end marker and return the stream + const std::ostringstream& Finalise() + { + HardAllNotesOff(); + UpdateTicksSinceLastEvent(); + + if(!m_tempoTrack) + WriteString(kCue, U_("loopEnd")); + + WriteTicks(); + uint8 msg[3] = { 0xFF, 0x2F, 0x00 }; + mpt::IO::WriteRaw(f, msg, 3); + + return f; + } + + void WriteString(StringType strType, const mpt::ustring &ustr) + { + std::string str = mpt::ToCharset(mpt::Charset::Locale, ustr); + if(!str.empty()) + { + WriteTicks(); + uint8 msg[2] = { 0xFF, strType }; + mpt::IO::WriteRaw(f, msg, 2); + mpt::IO::WriteVarInt(f, str.length()); + mpt::IO::WriteRaw(f, str.data(), str.length()); + } + } + + void Release() override { } + int32 GetUID() const override { return 0; } + int32 GetVersion() const override { return 0; } + void Idle() override { } + uint32 GetLatency() const override { return 0; } + + int32 GetNumPrograms() const override { return 0; } + int32 GetCurrentProgram() override { return 0; } + void SetCurrentProgram(int32) override { } + + PlugParamIndex GetNumParameters() const override { return 0; } + PlugParamValue GetParameter(PlugParamIndex) override { return 0; } + void SetParameter(PlugParamIndex, PlugParamValue) override { } + + float RenderSilence(uint32) override { return 0.0f; } + + bool MidiSend(uint32 midiCode) override + { + std::array<char, 4> midiData; + memcpy(midiData.data(), &midiCode, 4); + + // Note-On events go last to prevent early note-off in a situation like this: + // ... ..|C-5 01 + // C-5 01|=== .. + if(MIDIEvents::GetTypeFromEvent(midiCode) == MIDIEvents::evNoteOn) + { + m_queuedEvents.push_back(midiData); + return true; + } + WriteTicks(); + mpt::IO::WriteRaw(f, midiData.data(), MIDIEvents::GetEventLength(midiData[0])); + return true; + } + + bool MidiSysexSend(mpt::const_byte_span sysex) override + { + if(sysex.size() > 1) + { + WriteTicks(); + mpt::IO::WriteIntBE<uint8>(f, 0xF0); + mpt::IO::WriteVarInt(f, mpt::saturate_cast<uint32>(sysex.size() - 1)); + mpt::IO::WriteRaw(f, sysex.data() + 1, sysex.size() - 1); + } + return true; + } + + uint8 GetMidiChannel(const ModChannel &chn, CHANNELINDEX trackChannel) const override + { + if(m_instr.nMidiChannel == MidiMappedChannel && trackChannel < std::size(m_sndFile.m_PlayState.Chn)) + { + // For mapped channels, distribute tracker channels evenly over MIDI channels, but avoid channel 10 (drums) + uint8 midiCh = trackChannel % 15u; + if(midiCh >= 9) + midiCh++; + return midiCh; + } + return IMidiPlugin::GetMidiChannel(chn, trackChannel); + } + + void MidiCommand(const ModInstrument &instr, uint16 note, uint16 vol, CHANNELINDEX trackChannel) override + { + if(note == NOTE_NOTECUT && (m_oldInstr == nullptr || !(m_oldInstr->nMixPlug != 0 && m_oldInstr->HasValidMIDIChannel()))) + { + // The default implementation does things with Note Cut that we don't want here: it cuts all notes. + note = NOTE_KEYOFF; + } + SynchronizeMidiChannelState(); + IMidiPlugin::MidiCommand(instr, note, vol, trackChannel); + } + + void MidiPitchBendRaw(int32 pitchbend, CHANNELINDEX trackerChn) override + { + SynchronizeMidiChannelState(); + SynchronizeMidiPitchWheelDepth(trackerChn); + IMidiPlugin::MidiPitchBendRaw(pitchbend, trackerChn); + } + + void MidiPitchBend(int32 increment, int8 pwd, CHANNELINDEX trackerChn) override + { + SynchronizeMidiChannelState(); + SynchronizeMidiPitchWheelDepth(trackerChn); + IMidiPlugin::MidiPitchBend(increment, pwd, trackerChn); + } + + void MidiVibrato(int32 depth, int8 pwd, CHANNELINDEX trackerChn) override + { + SynchronizeMidiChannelState(); + SynchronizeMidiPitchWheelDepth(trackerChn); + IMidiPlugin::MidiVibrato(depth, pwd, trackerChn); + } + + bool IsNotePlaying(uint8 note, CHANNELINDEX trackerChn) override + { + SynchronizeMidiChannelState(); + return IMidiPlugin::IsNotePlaying(note, trackerChn); + } + + void HardAllNotesOff() override + { + for(uint8 mc = 0; mc < m_MidiCh.size(); mc++) + { + PlugInstrChannel &channel = m_MidiCh[mc]; + for(size_t i = 0; i < std::size(channel.noteOnMap); i++) + { + for(auto &c : channel.noteOnMap[i]) + { + while(c != 0) + { + MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0)); + c--; + } + } + } + } + } + + void Resume() override { } + void Suspend() override { } + void PositionChanged() override { } + + bool IsInstrument() const override { return true; } + bool CanRecieveMidiEvents() override { return true; } + bool ShouldProcessSilence() override { return true; } +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() override { return {}; } + CString GetParamName(PlugParamIndex) override { return {}; } + CString GetParamLabel(PlugParamIndex) override { return {}; } + CString GetParamDisplay(PlugParamIndex) override { return {}; } + CString GetCurrentProgramName() override { return {}; } + void SetCurrentProgramName(const CString &) override { } + CString GetProgramName(int32) override { return {}; } + bool HasEditor() const override { return false; } +#endif // MODPLUG_TRACKER + int GetNumInputChannels() const override { return 0; } + int GetNumOutputChannels() const override { return 0; } + }; + + + class Conversion + { + std::vector<ModInstrument *> m_oldInstruments; + std::vector<MidiTrack *> m_tracks; + std::vector<SNDMIXPLUGIN> m_oldPlugins; + SNDMIXPLUGIN tempoTrackPlugin; + VSTPluginLib m_plugFactory; + CSoundFile &m_sndFile; + mpt::ofstream &m_file; + const GetLengthType m_songLength; + const bool m_wasInstrumentMode; + + public: + Conversion(CSoundFile &sndFile, const InstrMap &instrMap, mpt::ofstream &file, bool overlappingInstruments, const GetLengthType &songLength) + : m_oldInstruments(sndFile.GetNumInstruments()) + , m_plugFactory(nullptr, true, {}, {}, {}) + , m_sndFile(sndFile) + , m_file(file) + , m_songLength(songLength) + , m_wasInstrumentMode(sndFile.GetNumInstruments() > 0) + { + m_oldPlugins.assign(std::begin(m_sndFile.m_MixPlugins), std::end(m_sndFile.m_MixPlugins)); + std::fill(std::begin(m_sndFile.m_MixPlugins), std::end(m_sndFile.m_MixPlugins), SNDMIXPLUGIN()); + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + m_oldInstruments[i - 1] = m_sndFile.Instruments[i]; + } + if(!m_wasInstrumentMode) + { + m_sndFile.m_nInstruments = std::min<INSTRUMENTINDEX>(m_sndFile.m_nSamples, MAX_INSTRUMENTS - 1u); + } + + m_tracks.reserve(m_sndFile.GetNumInstruments() + 1); + MidiTrack &tempoTrack = *(new MidiTrack(m_plugFactory, m_sndFile, m_songLength, &tempoTrackPlugin, nullptr, mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.m_songName), nullptr, overlappingInstruments)); + tempoTrack.WriteString(kText, mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.m_songMessage)); + tempoTrack.WriteString(kCopyright, m_sndFile.m_songArtist); + m_tracks.push_back(&tempoTrack); + + PLUGINDEX nextPlug = 0; + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + m_sndFile.Instruments[i] = nullptr; + if(!m_sndFile.GetpModDoc()->IsInstrumentUsed(i) || (m_wasInstrumentMode && m_oldInstruments[i - 1] == nullptr) || nextPlug >= MAX_MIXPLUGINS) + continue; + + // FIXME: Having > MAX_MIXPLUGINS used instruments won't work! So in MPTM, you can only use 250 out of 255 instruments... + SNDMIXPLUGIN &mixPlugin = m_sndFile.m_MixPlugins[nextPlug++]; + + ModInstrument *oldInstr = m_wasInstrumentMode ? m_oldInstruments[i - 1] : nullptr; + MidiTrack &midiInstr = *(new MidiTrack(m_plugFactory, m_sndFile, m_songLength, &mixPlugin, &tempoTrack, m_wasInstrumentMode ? mpt::ToUnicode(m_sndFile.GetCharsetInternal(), oldInstr->name) : mpt::ToUnicode(m_sndFile.GetCharsetInternal(), m_sndFile.GetSampleName(i)), oldInstr, overlappingInstruments)); + ModInstrument &instr = midiInstr; + mixPlugin.pMixPlugin = &midiInstr; + + m_sndFile.Instruments[i] = &instr; + m_tracks.push_back(&midiInstr); + + if(m_wasInstrumentMode) instr = *oldInstr; + instr.nMixPlug = nextPlug; + if((oldInstr != nullptr && oldInstr->nMixPlug == 0) || instr.nMidiChannel == MidiNoChannel) + { + instr.midiPWD = 12; + } + + instr.nMidiChannel = instrMap[i].channel; + if(instrMap[i].channel != MidiFirstChannel + 9) + { + // Melodic instrument + instr.nMidiProgram = instrMap[i].program; + } else + { + // Drums + if(oldInstr != nullptr && oldInstr->nMidiChannel != MidiFirstChannel + 9) + instr.nMidiProgram = 0; + if(instrMap[i].program > 0) + { + for(auto &key : instr.NoteMap) + { + key = instrMap[i].program + NOTE_MIN - 1; + } + } + } + midiInstr.WritePitchWheelDepth(); + } + + mpt::IO::WriteRaw(m_file, "MThd", 4); + mpt::IO::WriteIntBE<uint32>(m_file, 6); + mpt::IO::WriteIntBE<uint16>(m_file, 1); // Type 1 MIDI - multiple simultaneous tracks + mpt::IO::WriteIntBE<uint16>(m_file, static_cast<uint16>(m_tracks.size())); // Number of tracks + mpt::IO::WriteIntBE<uint16>(m_file, MidiExport::ppq); + } + + void Finalise() + { + for(auto track : m_tracks) + { + std::string data = track->Finalise().str(); + if(!data.empty()) + { + const uint32 len = mpt::saturate_cast<uint32>(data.size()); + mpt::IO::WriteRaw(m_file, "MTrk", 4); + mpt::IO::WriteIntBE<uint32>(m_file, len); + mpt::IO::WriteRaw(m_file, data.data(), len); + } + } + } + + ~Conversion() + { + for(INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + m_sndFile.Instruments[i] = m_wasInstrumentMode ? m_oldInstruments[i - 1] : nullptr; + } + + if(!m_wasInstrumentMode) + { + m_sndFile.m_nInstruments = 0; + } + + for(auto &plug : m_sndFile.m_MixPlugins) + { + plug.Destroy(); + } + for(auto &track : m_tracks) + { + delete track; // Resets m_MixPlugins[i].pMixPlugin, so do it before copying back the old structs + } + std::move(m_oldPlugins.cbegin(), m_oldPlugins.cend(), std::begin(m_sndFile.m_MixPlugins)); + + // Be sure that instrument pointers to our faked instruments are gone. + const auto muteFlag = CSoundFile::GetChannelMuteFlag(); + for(CHANNELINDEX i = 0; i < MAX_CHANNELS; i++) + { + m_sndFile.m_PlayState.Chn[i].Reset(ModChannel::resetTotal, m_sndFile, i, muteFlag); + } + } + }; + + + class DummyAudioTarget : public IAudioTarget + { + public: + void Process(mpt::audio_span_interleaved<MixSampleInt>) override { } + void Process(mpt::audio_span_interleaved<MixSampleFloat>) override { } + }; +} + + +//////////////////////////////////////////////////////////////////////////////////// +// +// CModToMidi dialog implementation +// + +bool CModToMidi::s_overlappingInstruments = false; + +BEGIN_MESSAGE_MAP(CModToMidi, CDialog) + ON_CBN_SELCHANGE(IDC_COMBO1, &CModToMidi::UpdateDialog) + ON_CBN_SELCHANGE(IDC_COMBO2, &CModToMidi::OnChannelChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &CModToMidi::OnProgramChanged) + ON_COMMAND(IDC_CHECK1, &CModToMidi::OnOverlapChanged) + ON_WM_VSCROLL() +END_MESSAGE_MAP() + + +void CModToMidi::DoDataExchange(CDataExchange *pDX) +{ + CDialog::DoDataExchange(pDX); + DDX_Control(pDX, IDC_COMBO1, m_CbnInstrument); + DDX_Control(pDX, IDC_COMBO2, m_CbnChannel); + DDX_Control(pDX, IDC_COMBO3, m_CbnProgram); + DDX_Control(pDX, IDC_SPIN1, m_SpinInstrument); +} + + +CModToMidi::CModToMidi(CSoundFile &sndFile, CWnd *pWndParent) + : CDialog(IDD_MOD2MIDI, pWndParent) + , m_sndFile(sndFile) + , m_instrMap((sndFile.GetNumInstruments() ? sndFile.GetNumInstruments() : sndFile.GetNumSamples()) + 1) +{ + for (INSTRUMENTINDEX i = 1; i <= m_sndFile.GetNumInstruments(); i++) + { + ModInstrument *pIns = m_sndFile.Instruments[i]; + if(pIns != nullptr) + { + m_instrMap[i].channel = pIns->nMidiChannel; + if(m_instrMap[i].channel != MidiFirstChannel + 9) + { + if(!pIns->HasValidMIDIChannel()) + m_instrMap[i].channel = MidiMappedChannel; + m_instrMap[i].program = pIns->nMidiProgram; + } + } + } +} + + +BOOL CModToMidi::OnInitDialog() +{ + CString s; + + CDialog::OnInitDialog(); + + // Fill instruments box + m_SpinInstrument.SetRange(-1, 1); + m_SpinInstrument.SetPos(0); + m_currentInstr = 1; + m_CbnInstrument.SetRedraw(FALSE); + if(m_sndFile.GetNumInstruments()) + { + for(INSTRUMENTINDEX nIns = 1; nIns <= m_sndFile.GetNumInstruments(); nIns++) + { + ModInstrument *pIns = m_sndFile.Instruments[nIns]; + if(pIns && m_sndFile.GetpModDoc()->IsInstrumentUsed(nIns, false)) + { + const CString name = m_sndFile.GetpModDoc()->GetPatternViewInstrumentName(nIns); + m_CbnInstrument.SetItemData(m_CbnInstrument.AddString(name), nIns); + } + } + } else + { + for(SAMPLEINDEX nSmp = 1; nSmp <= m_sndFile.GetNumSamples(); nSmp++) + { + if(m_sndFile.GetpModDoc()->IsSampleUsed(nSmp, false)) + { + s.Format(_T("%02d: "), nSmp); + s += mpt::ToCString(m_sndFile.GetCharsetInternal(), m_sndFile.m_szNames[nSmp]); + m_CbnInstrument.SetItemData(m_CbnInstrument.AddString(s), nSmp); + } + } + } + m_CbnInstrument.SetRedraw(TRUE); + m_CbnInstrument.SetCurSel(0); + + // Fill channels box + m_CbnChannel.SetRedraw(FALSE); + m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Don't Export")), MidiNoChannel); + m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Melodic (any)")), MidiMappedChannel); + m_CbnChannel.SetItemData(m_CbnChannel.AddString(_T("Percussions")), MidiFirstChannel + 9); + for(uint32 chn = 1; chn <= 16; chn++) + { + if(chn == 10) + continue; + s.Format(_T("Melodic %u"), chn); + m_CbnChannel.SetItemData(m_CbnChannel.AddString(s), MidiFirstChannel - 1 + chn); + } + m_CbnChannel.SetRedraw(TRUE); + m_CbnChannel.SetCurSel(1); + + m_currentInstr = 1; + m_percussion = true; + FillProgramBox(false); + m_CbnProgram.SetCurSel(0); + UpdateDialog(); + CheckDlgButton(IDC_CHECK1, s_overlappingInstruments ? BST_CHECKED : BST_UNCHECKED); + return TRUE; +} + + +void CModToMidi::FillProgramBox(bool percussion) +{ + if(m_percussion == percussion) + return; + m_CbnProgram.SetRedraw(FALSE); + m_CbnProgram.ResetContent(); + if(percussion) + { + m_CbnProgram.SetItemData(m_CbnProgram.AddString(_T("Mapped")), 0); + for(ModCommand::NOTE i = 0; i < 61; i++) + { + ModCommand::NOTE note = i + 24; + auto s = MPT_CFORMAT("{} ({}): {}")( + note, + mpt::ToCString(m_sndFile.GetNoteName(note + NOTE_MIN)), + mpt::ToCString(mpt::Charset::ASCII, szMidiPercussionNames[i])); + m_CbnProgram.SetItemData(m_CbnProgram.AddString(s), note); + } + } else + { + m_CbnProgram.SetItemData(m_CbnProgram.AddString(_T("No Program Change")), 0); + for(int i = 1; i <= 128; i++) + { + auto s = MPT_CFORMAT("{}: {}")( + mpt::cfmt::dec0<3>(i), + mpt::ToCString(mpt::Charset::ASCII, szMidiProgramNames[i - 1])); + m_CbnProgram.SetItemData(m_CbnProgram.AddString(s), i); + } + } + m_CbnProgram.SetRedraw(TRUE); + m_CbnProgram.Invalidate(FALSE); + m_percussion = percussion; +} + + +void CModToMidi::UpdateDialog() +{ + m_currentInstr = static_cast<UINT>(m_CbnInstrument.GetItemData(m_CbnInstrument.GetCurSel())); + const bool validInstr = (m_currentInstr > 0 && m_currentInstr < m_instrMap.size()); + + m_CbnProgram.EnableWindow(validInstr && m_instrMap[m_currentInstr].channel != MidiNoChannel); + + if(!validInstr) + return; + + uint8 nMidiCh = m_instrMap[m_currentInstr].channel; + int sel; + switch(nMidiCh) + { + case MidiNoChannel: + sel = 0; + break; + case MidiMappedChannel: + sel = 1; + break; + case MidiFirstChannel + 9: + sel = 2; + break; + default: + sel = nMidiCh - MidiFirstChannel + 2; + if(nMidiCh < MidiFirstChannel + 9) + sel++; + } + if(!m_percussion && (nMidiCh == MidiFirstChannel + 9)) + { + FillProgramBox(true); + } else if(m_percussion && (nMidiCh != MidiFirstChannel + 9)) + { + FillProgramBox(false); + } + m_CbnChannel.SetCurSel(sel); + UINT nMidiProgram = m_instrMap[m_currentInstr].program; + if(m_percussion) + { + if(nMidiProgram >= 24 && nMidiProgram <= 84) + nMidiProgram -= 23; + else + nMidiProgram = 0; + } else + { + if(nMidiProgram > 127) + nMidiProgram = 0; + } + m_CbnProgram.SetCurSel(nMidiProgram); +} + + +void CModToMidi::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) +{ + CDialog::OnVScroll(nSBCode, nPos, pScrollBar); + int pos = m_SpinInstrument.GetPos32(); + if(pos) + { + m_SpinInstrument.SetPos(0); + int numIns = m_CbnInstrument.GetCount(); + int ins = m_CbnInstrument.GetCurSel() + pos; + if(ins < 0) + ins = numIns - 1; + if(ins >= numIns) + ins = 0; + m_CbnInstrument.SetCurSel(ins); + UpdateDialog(); + } +} + + +void CModToMidi::OnChannelChanged() +{ + uint8 midiCh = static_cast<uint8>(m_CbnChannel.GetItemData(m_CbnChannel.GetCurSel())); + if(m_currentInstr >= m_instrMap.size()) + return; + const auto oldCh = m_instrMap[m_currentInstr].channel; + m_instrMap[m_currentInstr].channel = midiCh; + if(midiCh == MidiNoChannel + || oldCh == MidiNoChannel + || (!m_percussion && midiCh == MidiFirstChannel + 9) + || (m_percussion && midiCh != MidiFirstChannel + 9)) + { + UpdateDialog(); + } +} + + +void CModToMidi::OnProgramChanged() +{ + DWORD_PTR nProgram = m_CbnProgram.GetItemData(m_CbnProgram.GetCurSel()); + if (nProgram == CB_ERR) return; + if ((m_currentInstr > 0) && (m_currentInstr < MAX_SAMPLES)) + { + m_instrMap[m_currentInstr].program = static_cast<uint8>(nProgram); + } +} + + +void CModToMidi::OnOverlapChanged() +{ + s_overlappingInstruments = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; +} + + +void CModToMidi::OnOK() +{ + for(size_t i = 1; i < m_instrMap.size(); i++) + { + if(m_instrMap[i].channel != MidiNoChannel) + { + CDialog::OnOK(); + return; + } + } + + auto choice = Reporting::Confirm(_T("No instruments have been selected for export. Would you still like to export the file?"), true, true); + if(choice == cnfYes) + CDialog::OnOK(); + else if(choice == cnfNo) + CDialog::OnCancel(); +} + + +void CDoMidiConvert::Run() +{ + CMainFrame::GetMainFrame()->PauseMod(m_sndFile.GetpModDoc()); + + const auto songLength = m_sndFile.GetLength(eNoAdjust).front(); + const double duration = songLength.duration; + const uint64 totalSamples = mpt::saturate_round<uint64>(duration * m_sndFile.m_MixerSettings.gdwMixingFreq); + SetRange(0, totalSamples); + + auto conv = std::make_unique<MidiExport::Conversion>(m_sndFile, m_instrMap, m_file, CModToMidi::s_overlappingInstruments, songLength); + + auto startTime = timeGetTime(), prevTime = startTime; + + m_sndFile.SetCurrentOrder(0); + m_sndFile.GetLength(eAdjust, GetLengthTarget(0, 0)); + m_sndFile.m_SongFlags.reset(SONG_PATTERNLOOP); + int oldRepCount = m_sndFile.GetRepeatCount(); + m_sndFile.SetRepeatCount(0); + m_sndFile.m_bIsRendering = true; + + EnableTaskbarProgress(); + + MidiExport::DummyAudioTarget target; + UINT ok = IDOK; + const auto fmt = MPT_TFORMAT("Rendering file... ({}mn{}s, {}mn{}s remaining)"); + while(m_sndFile.Read(MIXBUFFERSIZE, target) > 0) + { + auto currentTime = timeGetTime(); + if(currentTime - prevTime >= 16) + { + prevTime = currentTime; + uint64 curSamples = m_sndFile.GetTotalSampleCount(); + uint32 curTime = static_cast<uint32>(curSamples / m_sndFile.m_MixerSettings.gdwMixingFreq); + uint32 timeRemaining = 0; + if(curSamples > 0 && curSamples < totalSamples) + { + timeRemaining = static_cast<uint32>(((currentTime - startTime) * (totalSamples - curSamples) / curSamples) / 1000u); + } + SetText(fmt(curTime / 60u, mpt::tfmt::dec0<2>(curTime % 60u), timeRemaining / 60u, mpt::tfmt::dec0<2>(timeRemaining % 60u)).c_str()); + SetProgress(curSamples); + ProcessMessages(); + + if(m_abort) + { + ok = IDCANCEL; + break; + } + } + } + + conv->Finalise(); + + m_sndFile.m_bIsRendering = false; + m_sndFile.SetRepeatCount(oldRepCount); + + EndDialog(ok); +} + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.h b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.h new file mode 100644 index 00000000..e6478f24 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/mod2midi.h @@ -0,0 +1,81 @@ +/* + * mod2midi.h + * ---------- + * Purpose: Module to MIDI conversion (dialog + conversion code). + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifndef NO_PLUGINS +#include "ProgressDialog.h" + +OPENMPT_NAMESPACE_BEGIN + + +namespace MidiExport +{ + struct Mod2MidiInstr + { + uint8 channel = MidiMappedChannel; // See enum MidiChannel + uint8 program = 0; + }; + using InstrMap = std::vector<Mod2MidiInstr>; +} + + +class CModToMidi: public CDialog +{ +protected: + CComboBox m_CbnInstrument, m_CbnChannel, m_CbnProgram; + CSpinButtonCtrl m_SpinInstrument; + CSoundFile &m_sndFile; + UINT m_currentInstr; + bool m_percussion; +public: + MidiExport::InstrMap m_instrMap; + static bool s_overlappingInstruments; + +public: + CModToMidi(CSoundFile &sndFile, CWnd *pWndParent = nullptr); + +protected: + void OnOK() override; + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange *pDX) override; + void FillProgramBox(bool percussion); + afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); + afx_msg void UpdateDialog(); + afx_msg void OnChannelChanged(); + afx_msg void OnProgramChanged(); + afx_msg void OnOverlapChanged(); + DECLARE_MESSAGE_MAP(); +}; + + +class CDoMidiConvert: public CProgressDialog +{ +public: + CSoundFile &m_sndFile; + mpt::ofstream &m_file; + const MidiExport::InstrMap &m_instrMap; + +public: + CDoMidiConvert(CSoundFile &sndFile, mpt::ofstream &f, const MidiExport::InstrMap &instrMap, CWnd *parent = nullptr) + : CProgressDialog(parent) + , m_sndFile(sndFile) + , m_file(f) + , m_instrMap(instrMap) + { } + void Run() override; +}; + + +OPENMPT_NAMESPACE_END + +#endif // NO_PLUGINS diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/mod2wave.h b/Src/external_dependencies/openmpt-trunk/mptrack/mod2wave.h new file mode 100644 index 00000000..e53ffce2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/mod2wave.h @@ -0,0 +1,138 @@ +/* + * mod2wave.h + * ---------- + * Purpose: Module to WAV conversion (dialog + conversion code). + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "StreamEncoder.h" +#include "StreamEncoderSettings.h" +#include "Settings.h" +#include "ProgressDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Direct To Disk Recording + + + +struct CWaveConvertSettings +{ + std::vector<EncoderFactoryBase*> EncoderFactories; + std::vector<std::unique_ptr<EncoderSettingsConf>> EncoderSettings; + + Setting<mpt::ustring> EncoderName; + std::size_t EncoderIndex; + + StoredTags storedTags; + FileTags Tags; + + int repeatCount; + ORDERINDEX minOrder, maxOrder; + SAMPLEINDEX sampleSlot; + SEQUENCEINDEX minSequence, maxSequence; + + bool normalize : 1; + bool silencePlugBuffers : 1; + bool outputToSample : 1; + + std::size_t FindEncoder(const mpt::ustring &name) const; + void SelectEncoder(std::size_t index); + EncoderFactoryBase *GetEncoderFactory() const; + const Encoder::Traits *GetTraits() const; + EncoderSettingsConf &GetEncoderSettings() const; + Encoder::Settings GetEncoderSettingsWithDetails() const; + CWaveConvertSettings(SettingsContainer &conf, const std::vector<EncoderFactoryBase*> &encFactories); +}; + + +class CWaveConvert: public CDialog +{ +public: + CWaveConvertSettings m_Settings; + const Encoder::Traits *encTraits; + CSoundFile &m_SndFile; + uint64 m_dwSongLimit; + ORDERINDEX m_nNumOrders; + + CComboBox m_CbnFileType, m_CbnSampleRate, m_CbnChannels, m_CbnDither, m_CbnSampleFormat, m_CbnSampleSlot; + CSpinButtonCtrl m_SpinLoopCount, m_SpinMinOrder, m_SpinMaxOrder, m_SpinMinSequence, m_SpinMaxSequence; + + bool m_bGivePlugsIdleTime; + bool m_bChannelMode; // Render by channel + bool m_bInstrumentMode; // Render by instrument + + CEdit m_EditTitle, m_EditAuthor, m_EditURL, m_EditAlbum, m_EditYear; + CComboBox m_CbnGenre; + CEdit m_EditGenre; + +private: + void FillFileTypes(); + void FillSamplerates(); + void FillChannels(); + void FillFormats(); + void FillDither(); + void FillTags(); + + void LoadTags(); + + void SaveEncoderSettings(); + void SaveTags(); + +public: + CWaveConvert(CWnd *parent, ORDERINDEX minOrder, ORDERINDEX maxOrder, ORDERINDEX numOrders, CSoundFile &sndFile, const std::vector<EncoderFactoryBase*> &encFactories); + +public: + void UpdateDialog(); + BOOL OnInitDialog() override; + void DoDataExchange(CDataExchange *pDX) override; + void OnOK() override; + afx_msg void OnCheckTimeLimit(); + afx_msg void OnCheckChannelMode(); + afx_msg void OnCheckInstrMode(); + afx_msg void OnFileTypeChanged(); + afx_msg void OnSamplerateChanged(); + afx_msg void OnChannelsChanged(); + afx_msg void OnDitherChanged(); + afx_msg void OnFormatChanged(); + afx_msg void OnPlayerOptions(); + afx_msg void OnExportModeChanged(); + afx_msg void OnSampleSlotChanged(); + DECLARE_MESSAGE_MAP() +}; + + +class CDoWaveConvert: public CProgressDialog +{ +public: + const CWaveConvertSettings &m_Settings; + CSoundFile &m_SndFile; + mpt::ofstream &fileStream; + const CString &caption; + uint64 m_dwSongLimit; + bool m_bGivePlugsIdleTime; + +public: + CDoWaveConvert(CSoundFile &sndFile, mpt::ofstream &f, const CString &caption, const CWaveConvertSettings &settings, CWnd *parent = NULL) + : CProgressDialog(parent) + , m_Settings(settings) + , m_SndFile(sndFile) + , fileStream(f) + , caption(caption) + , m_dwSongLimit(0) + { } + void Run() override; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/mptrack.rc b/Src/external_dependencies/openmpt-trunk/mptrack/mptrack.rc new file mode 100644 index 00000000..d6419d59 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/mptrack.rc @@ -0,0 +1,3649 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "afxres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// German (Germany) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_DEU) +LANGUAGE LANG_GERMAN, SUBLANG_GERMAN +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_MISSINGSAMPLES DIALOGEX 0, 0, 316, 185 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_THICKFRAME +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,262,164,50,14 + LTEXT "The following external samples could not be found.\nDouble-click an entry to set its new file location.\nClick ""Scan Folder"" to search for matching filenames of all missing samples in a folder.",IDC_STATIC,6,6,306,24 + CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,6,36,306,120 + PUSHBUTTON "&Scan Folder...",IDC_BUTTON1,7,164,60,14 + LTEXT "",IDC_STATIC1,72,167,186,8,SS_PATHELLIPSIS +END + +IDD_CLEANUP_SONG DIALOGEX 0, 0, 394, 154 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Cleanup" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,330,6,60,14 + PUSHBUTTON "Cancel",IDCANCEL,330,24,60,14 + LTEXT "Patterns:",IDC_STATIC,6,6,84,12 + CONTROL "Remove unused",IDC_CHK_CLEANUP_PATTERNS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,6,90,10 + CONTROL "Remove all",IDC_CHK_REMOVE_PATTERNS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,6,114,10 + CONTROL "Rearrange",IDC_CHK_REARRANGE_PATTERNS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,18,90,10 + CONTROL "Remove duplicates",IDC_CHK_REMOVE_DUPLICATES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,18,114,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,30,312,1 + LTEXT "Orders / Sequences:",IDC_STATIC,6,36,84,12 + CONTROL "Merge sequences",IDC_CHK_MERGE_SEQUENCES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,36,90,10 + CONTROL "Remove all orders",IDC_CHK_REMOVE_ORDERS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,36,114,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,48,312,1 + LTEXT "Samples:",IDC_STATIC,6,53,84,12 + CONTROL "Remove unused",IDC_CHK_CLEANUP_SAMPLES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,54,90,10 + CONTROL "Remove all",IDC_CHK_REMOVE_SAMPLES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,54,114,10 + CONTROL "Rearrange",IDC_CHK_REARRANGE_SAMPLES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,66,90,10 + CONTROL "Remove unused sample data",IDC_CHK_OPTIMIZE_SAMPLES, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,66,108,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,78,312,1 + LTEXT "Instruments:",IDC_STATIC,6,82,84,12 + CONTROL "Remove unused",IDC_CHK_CLEANUP_INSTRUMENTS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,84,90,10 + CONTROL "Remove all (convert to samples)",IDC_CHK_REMOVE_INSTRUMENTS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,84,120,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,96,312,1 + LTEXT "Plugins:",IDC_STATIC,6,101,84,12 + CONTROL "Remove unused",IDC_CHK_CLEANUP_PLUGINS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,102,90,10 + CONTROL "Remove all",IDC_CHK_REMOVE_PLUGINS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,102,90,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,114,312,1 + LTEXT "Miscellaneous:",IDC_STATIC,6,119,84,12 + CONTROL "Reset all variables",IDC_CHK_RESET_VARIABLES,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,120,90,10 + CONTROL "Remove unused channels",IDC_CHK_UNUSED_CHANNELS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,120,114,10 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,132,312,1 + LTEXT "Presets:",IDC_STATIC,6,137,84,12 + PUSHBUTTON "Remove all unused stuff",IDC_BTN_CLEANUP_SONG,108,138,90,12 + PUSHBUTTON "Create samplepack",IDC_BTN_COMPO_CLEANUP,204,138,90,12 +END + +IDD_KEYBOARD_SPLIT DIALOGEX 0, 0, 184, 137 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Keyboard Split Settings" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,72,114,50,14 + PUSHBUTTON "Cancel",IDCANCEL,126,114,50,14 + LTEXT "Split Note",IDC_STATIC,6,8,32,8 + COMBOBOX IDC_COMBO_SPLITNOTE,60,6,54,137,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Instrument",IDC_STATIC,6,26,42,8 + COMBOBOX IDC_COMBO_SPLITINSTRUMENT,60,24,114,125,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Volume",IDC_STATIC,6,44,24,8 + COMBOBOX IDC_COMBO_SPLITVOLUME,60,42,54,146,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Transpose",IDC_PATTERN_OCTAVELINK,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,61,49,10 + COMBOBOX IDC_COMBO_OCTAVEMODIFIER,60,60,54,137,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Enabling instrument, volume or octave change will enable the keyboard split feature. These options are applied to the lower section of the keyboard.",IDC_STATIC,6,78,174,24 +END + +IDD_SAMPLE_GENERATOR DIALOGEX 0, 0, 304, 191 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Generator" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Mathematical Formula",IDC_STATIC,6,6,294,30 + EDITTEXT IDC_EDIT_FORMULA,12,18,270,12,ES_AUTOHSCROLL + PUSHBUTTON "6",IDC_BUTTON_SHOW_EXPRESSIONS,282,18,12,12 + GROUPBOX "Sample Parameters",IDC_STATIC,6,42,294,48 + LTEXT "Sample Length:",IDC_STATIC,12,56,60,8 + EDITTEXT IDC_EDIT_SAMPLE_LENGTH,108,54,42,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Sample Frequency:",IDC_STATIC,12,74,66,8 + EDITTEXT IDC_EDIT_SAMPLE_FREQ,108,72,42,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Sample Length in Seconds:",IDC_STATIC,156,56,90,8 + EDITTEXT IDC_EDIT_SAMPLE_LENGTH_SEC,252,54,42,12,ES_AUTOHSCROLL + LTEXT "Size: 9999 KB",IDC_STATIC_SMPSIZE_KB,156,72,138,12 + GROUPBOX "Clipping Options",IDC_STATIC,6,96,294,66 + CONTROL "Normalize - All values are accepted and normalized.",IDC_RADIO_SMPCLIP3, + "Button",BS_AUTORADIOBUTTON,12,114,270,12 + CONTROL "Clip - Values in [-1,1] are accepted, other values are clipped.",IDC_RADIO_SMPCLIP1, + "Button",BS_AUTORADIOBUTTON,12,126,270,12 + CONTROL "Overflow - Values in [-1,1] are accepted, other values are wrapped around.",IDC_RADIO_SMPCLIP2, + "Button",BS_AUTORADIOBUTTON,12,138,270,12 + DEFPUSHBUTTON "OK",IDOK,192,168,50,14 + PUSHBUTTON "Cancel",IDCANCEL,246,168,50,14 + PUSHBUTTON "Presets...",IDC_BUTTON_SAMPLEGEN_PRESETS,6,168,50,14 +END + +IDD_SAMPLE_GENERATOR_PRESETS DIALOGEX 0, 0, 316, 101 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Generator Presets" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,258,78,50,14 + LISTBOX IDC_LIST_SAMPLEGEN_PRESETS,6,6,108,90,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + EDITTEXT IDC_EDIT_PRESET_NAME,120,18,186,12,ES_AUTOHSCROLL + LTEXT "Name:",IDC_STATIC,120,6,186,8 + EDITTEXT IDC_EDIT_PRESET_EXPR,120,54,186,12,ES_AUTOHSCROLL + LTEXT "Expression:",IDC_STATIC,120,42,186,8 + PUSHBUTTON "Remove",IDC_BUTTON_REMOVE,120,78,50,14 + PUSHBUTTON "Add",IDC_BUTTON_ADD,174,78,50,14 +END + +IDD_MIXSAMPLES DIALOGEX 0, 0, 196, 89 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Mix Samples" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Amplify source by:",IDC_STATIC,6,8,84,8 + EDITTEXT IDC_EDIT_SAMPVOL1,90,6,54,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_SAMPVOL1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,120,6,11,14 + LTEXT "%",IDC_STATIC,150,9,18,8 + LTEXT "Amplify clipboard by:",IDC_STATIC,6,27,84,8 + EDITTEXT IDC_EDIT_SAMPVOL2,90,24,54,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_SAMPVOL2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,120,24,11,14 + LTEXT "%",IDC_STATIC,150,27,18,8 + LTEXT "Offset clipboard by:",IDC_STATIC,6,44,84,8 + EDITTEXT IDC_EDIT_OFFSET,90,42,54,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_OFFSET,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,132,42,11,14 + DEFPUSHBUTTON "OK",IDOK,84,66,50,14 + PUSHBUTTON "Cancel",IDCANCEL,138,66,50,14 + LTEXT "samples",IDC_STATIC,150,45,42,8 +END + +IDD_OPTIONS_SAMPLEEDITOR DIALOGEX 0, 0, 287, 273 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Samples" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Sample Editor",IDC_STATIC,6,6,276,138 + LTEXT "Sample &Undo Buffer:",IDC_STATIC,12,20,102,8 + EDITTEXT IDC_EDIT_UNDOSIZE,114,18,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,144,24,12,12 + LTEXT "%",IDC_UNDOSIZE,162,20,114,16 + LTEXT "&Frequency Finetune Steps:",IDC_STATIC,12,38,102,8 + EDITTEXT IDC_EDIT_FINETUNE,114,36,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,144,41,11,12 + LTEXT "Cents",IDC_STATIC,162,38,20,8 + LTEXT "&Default Sample Format:",IDC_STATIC,12,56,102,8 + COMBOBOX IDC_DEFAULT_FORMAT,114,54,42,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "FLAC Compression &Level:",IDC_STATIC,12,74,90,8 + LTEXT "Faster Encoding",IDC_STATIC,114,74,54,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,168,72,48,12 + LTEXT "Smaller Files",IDC_STATIC,222,74,54,8 + LTEXT "Note Cuts:",IDC_STATIC,12,90,264,12 + CONTROL "On New &Note",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,18,102,258,12 + CONTROL "On Key &Release",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,18,114,258,12 + CONTROL "Press &Again",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,18,126,258,12 + GROUPBOX "Instrument Editor",IDC_STATIC,6,150,276,48 + CONTROL "&Compress ITI Files",IDC_COMPRESS_ITI,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,162,264,12 + LTEXT "Default &Plugin Volume Command Handling:",IDC_STATIC,12,182,150,8 + COMBOBOX IDC_VOLUME_HANDLING,162,180,78,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Common",IDC_STATIC,6,204,276,66 + CONTROL "Pre&view Samples / Instruments in File Browser",IDC_PREVIEW_SAMPLES, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,217,264,12 + CONTROL "N&ormalize 24-/32-bit Samples on Load",IDC_NORMALIZE, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,235,264,12 + CONTROL "Show Cursor Position in He&x",IDC_CURSORINHEX,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,252,264,12 +END + +IDD_EDITHISTORY DIALOGEX 0, 0, 316, 185 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Module edit history" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,258,162,50,14 + PUSHBUTTON "Clear",IDC_BTN_CLEAR,6,162,50,14 + EDITTEXT IDC_EDIT_HISTORY,6,6,300,150,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL + LTEXT "",IDC_TOTAL_EDIT_TIME,60,165,192,15 +END + +IDD_SAMPLE_GRID_SIZE DIALOGEX 0, 0, 154, 71 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Grid Size" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + EDITTEXT IDC_EDIT1,66,6,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,102,6,11,14 + DEFPUSHBUTTON "&OK",IDOK,42,48,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,96,48,50,14 + LTEXT "Input the number of grid segments, or input ""0"" to disable the grid.",IDC_STATIC,6,24,138,24 + LTEXT "Grid Segments:",IDC_STATIC,6,8,60,8 +END + +IDD_SAMPLE_XFADE DIALOGEX 0, 0, 292, 101 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Crossfader" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,180,79,50,14 + PUSHBUTTON "Cancel",IDCANCEL,234,79,50,14 + RTEXT "Apply to:",IDC_STATIC,6,6,84,8 + CONTROL "S&le Loop",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,102,4,84,12 + CONTROL "S&ustain Loop",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,186,4,84,12 + RTEXT "&Samples used for fading:",IDC_STATIC,6,27,84,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,96,24,120,15 + EDITTEXT IDC_EDIT1,222,25,54,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,258,25,11,14 + RTEXT "&Constant Volume",IDC_STATIC,30,46,60,8 + CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,96,43,120,15 + LTEXT "Constant Power",IDC_STATIC,222,46,60,8 + CONTROL "&Post-loop fade",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,102,61,180,12 +END + +IDD_OPTIONS_UPDATE DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Update" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "Enable Online &Update Check",IDC_CHECK_UPDATEENABLED, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,6,106,10 + GROUPBOX "Check for Updates",IDC_STATIC_UPDATECHECK,6,24,276,66 + CONTROL "&Install Updates automatically",IDC_CHECK_UPDATEINSTALLAUTOMATICALLY, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,36,264,12 + LTEXT "Automatically check on program &start:",IDC_STATIC_UPDATEFREQUENCY,12,54,126,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_UPDATEFREQUENCY,138,54,48,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Check now...",IDC_BUTTON1,216,54,60,12 + LTEXT "",IDC_LASTUPDATE,12,73,264,12 + GROUPBOX "Privacy Settings",IDC_STATIC_UPDATEPRIVACY,6,96,276,84 + CONTROL "&Allow OpenMPT to collect basic statistics about your system configuration",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,108,264,12 + LTEXT "",IDC_STATIC_UPDATEPRIVACYTEXT,12,120,264,36 + CONTROL "<a>Show which information would be transmitted to OpenMPT...</a>",IDC_SYSLINK1, + "SysLink",WS_TABSTOP,12,162,264,12 + GROUPBOX "Update Channel",IDC_STATIC_UDATECHANNEL,6,192,276,52 + CONTROL "&Release: Official stable released versions only (recommended)",IDC_RADIO1, + "Button",BS_AUTORADIOBUTTON,12,204,264,10 + CONTROL "&Next: Previews of the next official stable release",IDC_RADIO2, + "Button",BS_AUTORADIOBUTTON,12,217,264,10 + CONTROL "&Development: Bleeding-edge development versions",IDC_RADIO3, + "Button",BS_AUTORADIOBUTTON,12,230,264,10 +END + +IDD_CLOSEDOCUMENTS DIALOGEX 0, 0, 370, 197 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Save modified files" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Save &Selected",IDOK,300,138,66,14 + PUSHBUTTON "&Cancel",IDCANCEL,300,157,66,14 + LISTBOX IDC_LIST1,6,6,288,168,LBS_SORT | LBS_MULTIPLESEL | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + CONTROL "Show &full paths",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,180,252,12 + PUSHBUTTON "Save &All",IDC_BUTTON1,300,90,66,14 + PUSHBUTTON "Save &None",IDC_BUTTON2,300,108,66,14 +END + +IDD_AUTOTUNE DIALOGEX 0, 0, 166, 82 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Tuner" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,54,60,50,14 + PUSHBUTTON "Cancel",IDCANCEL,108,60,50,14 + LTEXT "Tune sample to closest",IDC_STATIC,6,8,96,8 + COMBOBOX IDC_COMBO1,102,6,36,12,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LTEXT "Pitch reference (A)",IDC_STATIC,6,26,96,8 + EDITTEXT IDC_EDIT1,102,24,36,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Hz",IDC_STATIC,144,26,18,8 + CONTROL "",IDC_STATIC,"Static",SS_BLACKFRAME | SS_SUNKEN,6,48,150,1 +END + +IDD_CHANNELSETTINGS DIALOGEX 0, 0, 244, 94 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "Channel 1:",IDC_STATIC_CHANNEL_NAME,"Static",SS_LEFTNOWORDWRAP | WS_GROUP,6,8,45,8 + EDITTEXT IDC_EDIT3,60,6,126,12,ES_AUTOHSCROLL + PUSHBUTTON "&<<",IDC_BUTTON1,192,6,18,12 + PUSHBUTTON "&>>",IDC_BUTTON2,216,6,18,12 + LTEXT "Initial Volume:",IDC_STATIC,6,30,45,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,54,24,92,22 + EDITTEXT IDC_EDIT1,150,30,36,12,ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,174,30,11,14 + CONTROL "&Mute",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,192,30,42,12 + LTEXT "Initial Pan:",IDC_STATIC,6,54,34,8 + CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,54,48,92,22 + EDITTEXT IDC_EDIT2,150,54,36,12,ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,174,54,11,14 + CONTROL "&Surround",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,192,54,42,12 + LTEXT "Colour:",IDC_STATIC,6,78,34,8 + CONTROL "",IDC_BUTTON5,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,60,78,22,12 + CONTROL "",IDC_BUTTON4,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,82,78,82,12 + CONTROL "",IDC_BUTTON6,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,164,78,22,12 + PUSHBUTTON "Change...",IDC_BUTTON3,192,78,42,12 +END + +IDD_CLIPBOARD DIALOGEX 0, 0, 166, 201 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CENTERMOUSE | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +EXSTYLE WS_EX_TOOLWINDOW +CAPTION "Clipboard Manager" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LISTBOX IDC_LIST1,6,6,156,156,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + LTEXT "Number of clipboards:",IDC_STATIC,6,168,114,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT1,126,168,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS | WS_TABSTOP,150,171,11,11 + LTEXT "Click an entry to make it the current clipboard.",IDC_STATIC,6,186,156,14 +END + +IDD_INPUT DIALOGEX 0, 0, 166, 66 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTERMOUSE | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "OpenMPT" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "",IDC_PROMPT,6,6,156,12 + EDITTEXT IDC_EDIT1,6,24,150,12,ES_AUTOHSCROLL + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_ARROWKEYS | UDS_NOTHOUSANDS,150,24,11,11 + DEFPUSHBUTTON "OK",IDOK,54,42,50,14 + PUSHBUTTON "Cancel",IDCANCEL,108,42,50,14 +END + +IDD_OPTIONS_ADVANCED DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_CAPTION +CAPTION "Advanced" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Warning:\nChanging advanced settings might cause stability problems.\nYou should only continue if you know what you are doing.",IDC_STATIC,6,6,204,24 + LTEXT "&Find:",IDC_STATIC,6,38,17,8 + EDITTEXT IDC_EDIT1,30,36,252,12,ES_AUTOHSCROLL + CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SORTASCENDING | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,6,54,276,222 + PUSHBUTTON "&Save now",IDC_BUTTON1,228,12,50,14 +END + +IDD_SCANPLUGINS DIALOGEX 0, 0, 316, 34 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION +EXSTYLE WS_EX_APPWINDOW +CAPTION "Updating Plugin Cache..." +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CTEXT "",IDC_SCANTEXT,6,7,306,20,SS_NOPREFIX +END + +IDD_RESAMPLE DIALOGEX 0, 0, 197, 110 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTERMOUSE | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Resample" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "&Upsample",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,6,6,114,10 + CONTROL "&Downsample",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,6,24,114,10 + CONTROL "",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,6,42,12,10 + EDITTEXT IDC_EDIT1,18,41,48,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,49,44,10,14 + LTEXT "Hz",IDC_STATIC,72,43,9,8 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDFRAME,6,60,180,1 + LTEXT "&Filter:",IDC_STATIC,6,70,24,8,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_FILTER,36,68,96,56,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + DEFPUSHBUTTON "&OK",IDOK,138,6,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,138,24,50,14 + CONTROL "&Adjust Offset Commands in Patterns",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,90,133,10 +END + +IDD_WECLOME DIALOGEX 0, 0, 257, 266 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Welcome to OpenMPT!" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,198,248,50,14 + LTEXT "Please review the following settings before using this software:",IDC_STATIC,6,6,246,8 + CONTROL "&Automatically check for new versions of OpenMPT",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,24,246,10 + CONTROL "Help OpenMPT development by providing basic s&tatistics",IDC_CHECK3, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,42,234,10 + LTEXT "Static",IDC_STATIC_WELCOME_STATISTICS,30,54,216,78 + CONTROL "&Use a big font in the pattern editor",IDC_CHECK2, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,138,246,10 + LTEXT "Use the following artist &name in modules by default (can be changed individually per module, not supported by all formats):",IDC_STATIC,6,156,240,18 + EDITTEXT IDC_EDIT2,6,174,240,12,ES_AUTOHSCROLL + LTEXT "&Default keyboard scheme:",IDC_STATIC,6,194,108,8 + COMBOBOX IDC_COMBO1,114,192,132,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Scan for existing VST plugins in the following location:",IDC_STATIC,6,214,172,8 + PUSHBUTTON "&Scan",IDC_BUTTON2,198,210,50,14 + EDITTEXT IDC_EDIT1,6,226,240,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + PUSHBUTTON "&More Settings",IDC_BUTTON1,6,248,60,14 +END + +IDD_UPDATE DIALOGEX 0, 0, 196, 138 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "OpenMPT Update" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&Update",IDOK,18,120,116,14 + PUSHBUTTON "&Cancel",IDCANCEL,138,120,50,14 + LTEXT "A new version of OpenMPT has been released.",IDC_STATIC,6,6,180,12 + LTEXT "Your version:",IDC_STATIC,6,24,44,8 + LTEXT "",IDC_VERSION1,78,24,114,8 + LTEXT "Current version:",IDC_STATIC,6,36,54,8 + LTEXT "",IDC_VERSION2,78,36,114,8 + LTEXT "Release date:",IDC_STATIC,6,52,46,8 + LTEXT "",IDC_DATE,78,52,114,8 + CONTROL "Link",IDC_SYSLINK1,"SysLink",WS_TABSTOP,6,66,186,24 + CONTROL "&Do not remind me about this version again.",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,96,186,12 +END + +IDD_TEMPO_SWING DIALOGEX 0, 0, 246, 143 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Tempo Swing Settings" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,132,120,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,186,120,50,14 + PUSHBUTTON "Use &Global Settings",IDC_BUTTON2,6,120,72,14 + LTEXT "Configure how much each row contributes to a beat:",IDC_STATIC,6,9,174,8 + PUSHBUTTON "&Reset",IDC_BUTTON1,186,6,50,14 + CONTROL "&Edit group:",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,24,54,12 + EDITTEXT IDC_EDIT1,60,24,30,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,78,30,11,12 + LTEXT "consecutive rows are grouped",IDC_STATIC,96,26,138,8 + SCROLLBAR IDC_SCROLLBAR1,234,42,11,69,SBS_VERT + LTEXT "",IDC_CONTAINER,0,47,234,62 +END + +IDD_LEGACY_PLAYBACK DIALOGEX 0, 0, 389, 244 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Playback Compatibility Settings" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,270,223,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,324,223,50,14 + LTEXT "Enable or disable playback quirks to remain compatible with previous versions of OpenMPT.",IDC_STATIC,6,6,372,12 + LTEXT "&Find a setting:",IDC_STATIC,6,21,60,8 + EDITTEXT IDC_EDIT1,66,18,312,14,ES_AUTOHSCROLL + LISTBOX IDC_LIST1,6,36,372,180,LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | LBS_EXTENDEDSEL | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Select Defaults",IDC_BUTTON1,6,223,60,14 + LTEXT "(Best compatibility with other trackers)",IDC_STATIC,72,225,186,8 +END + +IDD_FIND_RANGE DIALOGEX 0, 0, 256, 53 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Find Range..." +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Find parameters in range:",IDC_STATIC,6,8,96,8 + COMBOBOX IDC_COMBO1,108,6,60,77,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CTEXT "to",IDC_STATIC,168,8,24,8 + COMBOBOX IDC_COMBO2,192,6,60,77,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + DEFPUSHBUTTON "OK",IDOK,144,30,50,14 + PUSHBUTTON "Cancel",IDCANCEL,198,30,50,14 +END + +IDD_MIDI_IO_PLUGIN DIALOGEX 0, 0, 251, 82 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CLIENTEDGE +CAPTION "MIDI Input / Output" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "MIDI Input Device (Sends MIDI data to OpenMPT):",IDC_STATIC,6,6,240,12 + COMBOBOX IDC_COMBO1,6,18,240,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "MIDI Output Device (Receives MIDI data from OpenMPT):",IDC_STATIC,6,36,240,12 + COMBOBOX IDC_COMBO2,6,48,240,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Output Latency:",IDC_STATIC,6,68,60,8 + EDITTEXT IDC_EDIT1,66,66,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,90,60,11,12 + LTEXT "ms",IDC_STATIC,108,68,24,8 + CONTROL "Send timing messages",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,126,66,120,12 +END + +IDD_OPTIONS_WINE DIALOGEX 0, 0, 286, 278 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Wine" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "Enable Wine native host support (requires restarting OpenMPT)",IDC_CHECK_WINE_ENABLE, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,6,273,10 + LTEXT "P&ortAudio:",IDC_STATIC_WINE_PORTAUDIO,18,42,42,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_WINE_PORTAUDIO,66,42,48,83,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&PulseAudio:",IDC_STATIC_WINE_PULSEAUDIO,18,24,42,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_WINE_PULSEAUDIO,66,24,48,83,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&RtAudio:",IDC_STATIC_WINE_RTAUDIO,18,60,42,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_WINE_RTAUDIO,66,60,48,83,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Support for native host system audio output is EXPERIMENTAL. Consult the OpenMPT manual for more information.",IDC_STATIC,6,84,238,23 +END + +IDD_LFOPLUGIN DIALOGEX 0, 0, 303, 201 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Waveform",IDC_STATIC,6,6,84,120 + CONTROL "&Sine",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,18,66,8 + CONTROL "&Triangle",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,30,66,8 + CONTROL "Sa&w",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,42,66,8 + CONTROL "S&quare",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,54,66,8 + CONTROL "&Noise",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,66,66,8 + CONTROL "S&moothed Noise",IDC_RADIO6,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,18,78,66,8 + CONTROL "Invert &Polarity",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,96,66,10 + CONTROL "One-Shot",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,108,66,10 + LTEXT "&Amplitude",IDC_STATIC1,96,6,126,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,96,18,204,12 + LTEXT "&Offset",IDC_STATIC2,96,42,198,8 + CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,96,54,204,12 + LTEXT "&Frequency",IDC_STATIC3,96,78,126,8 + CONTROL "",IDC_SLIDER3,"msctls_trackbar32",TBS_AUTOTICKS | WS_TABSTOP,96,91,204,12 + CONTROL "&Bypass LFO",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,222,114,78,8 + CONTROL "T&empo Sync",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,96,114,90,8 + GROUPBOX "Output",IDC_STATIC,6,132,294,66 + LTEXT "Plu&gin:",IDC_STATIC,12,146,60,8 + COMBOBOX IDC_COMBO2,78,145,162,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "E&ditor",IDC_BUTTON1,246,145,42,14 + CONTROL "To Pa&rameter",IDC_RADIO7,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,162,60,10 + COMBOBOX IDC_COMBO1,78,162,210,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "To MIDI &CC",IDC_RADIO8,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,180,53,10 + COMBOBOX IDC_COMBO3,78,180,108,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "On C&hannel:",IDC_STATIC,192,183,48,8 + EDITTEXT IDC_EDIT1,246,180,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,270,180,11,14 +END + +IDD_OPL_PARAMS DIALOGEX 0, 0, 410, 218 +STYLE DS_SETFONT | DS_FIXEDSYS | DS_CONTROL | WS_CHILD +EXSTYLE WS_EX_ACCEPTFILES +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "Additive Synthesis",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,7,174,10 + LTEXT "Modulation Feedback:",IDC_STATIC,216,8,72,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,6,108,12 + GROUPBOX "Carrier",IDC_STATIC,6,24,198,192 + LTEXT "Attack Rate:",IDC_STATIC,12,38,72,8 + CONTROL "",IDC_SLIDER9,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,36,108,12 + LTEXT "Decay Rate:",IDC_STATIC,12,50,72,8 + CONTROL "",IDC_SLIDER10,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,48,108,12 + LTEXT "Sustain Level:",IDC_STATIC,12,62,72,8 + CONTROL "",IDC_SLIDER11,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,60,108,12 + LTEXT "Release Rate:",IDC_STATIC,12,74,72,8 + CONTROL "",IDC_SLIDER12,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,72,108,12 + CONTROL "Sustain Sound",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,87,174,10 + LTEXT "Volume:",IDC_STATIC,12,104,72,8 + CONTROL "",IDC_SLIDER13,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,102,108,12 + CONTROL "Scale Envelope with Keys",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,120,174,10 + LTEXT "Key Scale Level:",IDC_STATIC,12,134,72,8 + CONTROL "",IDC_SLIDER14,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,132,108,12 + LTEXT "Frequency Multiplier:",IDC_STATIC,12,152,72,8 + CONTROL "",IDC_SLIDER15,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,84,150,108,12 + LTEXT "Waveform:",IDC_STATIC,12,168,68,8 + COMBOBOX IDC_COMBO2,84,168,108,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Vibrato",IDC_CHECK8,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,186,180,10 + CONTROL "Tremolo",IDC_CHECK9,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,198,180,10 + GROUPBOX "Modulator",IDC_STATIC,210,24,198,192 + LTEXT "Attack Rate:",IDC_STATIC,216,38,72,8 + CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,36,108,12 + LTEXT "Decay Rate:",IDC_STATIC,216,50,72,8 + CONTROL "",IDC_SLIDER3,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,48,108,12 + LTEXT "Sustain Level:",IDC_STATIC,216,62,72,8 + CONTROL "",IDC_SLIDER4,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,60,108,12 + LTEXT "Release Rate:",IDC_STATIC,216,74,72,8 + CONTROL "",IDC_SLIDER5,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,72,108,12 + CONTROL "Sustain Sound",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,216,86,174,10 + LTEXT "Volume:",IDC_STATIC,216,104,72,8 + CONTROL "",IDC_SLIDER6,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,102,108,12 + CONTROL "Scale Envelope with Keys",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,216,120,174,10 + LTEXT "Key Scale Level:",IDC_STATIC,216,134,72,8 + CONTROL "",IDC_SLIDER7,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,132,108,12 + LTEXT "Frequency Multiplier:",IDC_STATIC,216,152,72,8 + CONTROL "",IDC_SLIDER8,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOOLTIPS | WS_TABSTOP,288,150,108,12 + LTEXT "Waveform:",IDC_STATIC,216,168,68,8 + COMBOBOX IDC_COMBO1,288,168,108,30,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Vibrato",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,216,186,180,10 + CONTROL "Tremolo",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,216,198,180,10 +END + +IDD_MISSINGPLUGS DIALOGEX 0, 0, 307, 170 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +EXSTYLE WS_EX_APPWINDOW +CAPTION "Plugins Not Found - OpenMPT" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,252,150,50,14 + LTEXT "The following plugins could not be found.\nYou can remove the checked plugins by pressing ""Remove"".\nPressing ""OK"" keeps the plugins in your library.",IDC_STATIC,6,6,300,24 + CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,6,36,294,108 + PUSHBUTTON "&Remove",IDC_BUTTON_REMOVE,198,150,50,14 +END + +IDD_MODIFIEDSAMPLES DIALOGEX 0, 0, 316, 185 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_THICKFRAME +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&Embed Selected",IDOK,112,164,63,14 + LTEXT "The following external samples have been modified or are missing on disk.\nYou can either update the external files or choose to embed them in the module instead.\nCheck the samples to which you would like to apply this action.",IDC_STATIC,6,6,306,24 + CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,6,36,306,120 + PUSHBUTTON "&Cancel",IDCANCEL,248,164,63,14 + PUSHBUTTON "&Save Selected",IDC_SAVE,180,164,63,14 + CONTROL "Select / Deselect &All",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,166,102,10 +END + +IDD_OPLEXPORT DIALOGEX 0, 0, 309, 158 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "OPL Export" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,192,138,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,246,138,50,14 + GROUPBOX "Export Format",IDC_STATIC,6,6,144,54 + CONTROL "VG&Z (Compressed VGM 1.60)",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,12,20,132,10 + CONTROL "&VGM (1.60)",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,32,132,10 + CONTROL "&DRO (DOSBox Raw OPL v1)",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,44,132,10 + GROUPBOX "Sub Songs",IDC_STATIC,6,66,144,66 + CONTROL "&All Sub Songs (Separate Files)",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,12,80,132,10 + CONTROL "&Sub Song:",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,94,54,10 + EDITTEXT IDC_EDIT1,66,93,54,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,126,90,10,14 + CONTROL "",IDC_SUBSONG,"Static",SS_LEFTNOWORDWRAP | SS_NOPREFIX | WS_GROUP,23,108,122,18 + GROUPBOX "Metadata",IDC_STATIC,156,6,144,126 + LTEXT "&Title:",IDC_STATIC,162,20,30,8 + EDITTEXT IDC_EDIT2,192,18,102,12,ES_AUTOHSCROLL + LTEXT "A&uthor:",IDC_STATIC,162,34,30,8 + EDITTEXT IDC_EDIT3,192,32,102,12,ES_AUTOHSCROLL + LTEXT "Dat&e:",IDC_STATIC,162,48,30,8 + EDITTEXT IDC_EDIT4,192,46,102,12,ES_AUTOHSCROLL + LTEXT "&Notes:",IDC_STATIC,162,66,36,8 + EDITTEXT IDC_EDIT5,162,78,132,48,ES_MULTILINE | ES_AUTOHSCROLL | WS_VSCROLL + CONTROL "",IDC_PROGRESS1,"msctls_progress32",NOT WS_VISIBLE | WS_BORDER,6,138,144,14 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_MISSINGSAMPLES, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 312 + TOPMARGIN, 7 + BOTTOMMARGIN, 178 + END + + IDD_CLEANUP_SONG, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 387 + TOPMARGIN, 7 + BOTTOMMARGIN, 147 + END + + IDD_KEYBOARD_SPLIT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 177 + TOPMARGIN, 7 + BOTTOMMARGIN, 130 + END + + IDD_SAMPLE_GENERATOR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 297 + TOPMARGIN, 7 + BOTTOMMARGIN, 184 + END + + IDD_SAMPLE_GENERATOR_PRESETS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 309 + TOPMARGIN, 7 + BOTTOMMARGIN, 94 + END + + IDD_MIXSAMPLES, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 189 + TOPMARGIN, 7 + BOTTOMMARGIN, 82 + END + + IDD_OPTIONS_SAMPLEEDITOR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 280 + TOPMARGIN, 7 + BOTTOMMARGIN, 266 + END + + IDD_EDITHISTORY, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 309 + TOPMARGIN, 7 + BOTTOMMARGIN, 178 + END + + IDD_SAMPLE_GRID_SIZE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 147 + TOPMARGIN, 7 + BOTTOMMARGIN, 64 + END + + IDD_SAMPLE_XFADE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 285 + TOPMARGIN, 7 + BOTTOMMARGIN, 94 + END + + IDD_OPTIONS_UPDATE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 279 + TOPMARGIN, 7 + BOTTOMMARGIN, 274 + END + + IDD_CLOSEDOCUMENTS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 363 + TOPMARGIN, 7 + BOTTOMMARGIN, 190 + END + + IDD_AUTOTUNE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 159 + TOPMARGIN, 7 + BOTTOMMARGIN, 75 + END + + IDD_CHANNELSETTINGS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 237 + TOPMARGIN, 7 + BOTTOMMARGIN, 88 + END + + IDD_CLIPBOARD, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 159 + TOPMARGIN, 7 + BOTTOMMARGIN, 194 + END + + IDD_INPUT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 159 + TOPMARGIN, 7 + BOTTOMMARGIN, 59 + END + + IDD_OPTIONS_ADVANCED, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 279 + TOPMARGIN, 7 + BOTTOMMARGIN, 274 + END + + IDD_SCANPLUGINS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 309 + TOPMARGIN, 7 + BOTTOMMARGIN, 27 + END + + IDD_RESAMPLE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 190 + TOPMARGIN, 7 + BOTTOMMARGIN, 103 + END + + IDD_WECLOME, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 250 + TOPMARGIN, 7 + BOTTOMMARGIN, 259 + END + + IDD_UPDATE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 189 + TOPMARGIN, 7 + BOTTOMMARGIN, 131 + END + + IDD_TEMPO_SWING, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 239 + TOPMARGIN, 7 + BOTTOMMARGIN, 136 + END + + IDD_LEGACY_PLAYBACK, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 382 + TOPMARGIN, 7 + BOTTOMMARGIN, 237 + END + + IDD_FIND_RANGE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 249 + TOPMARGIN, 7 + BOTTOMMARGIN, 46 + END + + IDD_MIDI_IO_PLUGIN, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 244 + TOPMARGIN, 7 + BOTTOMMARGIN, 75 + END + + IDD_OPTIONS_WINE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 279 + TOPMARGIN, 7 + BOTTOMMARGIN, 271 + END + + IDD_LFOPLUGIN, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 296 + TOPMARGIN, 7 + BOTTOMMARGIN, 194 + END + + IDD_OPL_PARAMS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 403 + TOPMARGIN, 7 + BOTTOMMARGIN, 211 + END + + IDD_MISSINGPLUGS, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 300 + TOPMARGIN, 7 + BOTTOMMARGIN, 163 + END + + IDD_MODIFIEDSAMPLES, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 312 + TOPMARGIN, 7 + BOTTOMMARGIN, 178 + END + + IDD_OPLEXPORT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 302 + TOPMARGIN, 7 + BOTTOMMARGIN, 151 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDR_MODULETYPE ICON "res\\moddoc.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// PNG +// + +IDB_ENVTOOLBAR PNG "res\\envelope_toolbar.png" + +IDB_IMAGELIST PNG "res\\icons.png" + +IDB_MAINBAR PNG "res\\main_toolbar.png" + +IDB_PATTERNS PNG "res\\pattern_toolbar.png" + +IDB_SMPTOOLBAR PNG "res\\sample_toolbar.png" + +IDB_SPLASHNOFOLDFIN PNG "res\\Splash.png" + +IDB_MPTRACK PNG "res\\About.png" + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_MIDI_IO_PLUGIN AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_LFOPLUGIN AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPTIONS_SAMPLEEDITOR AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPTIONS_WINE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CLIPBOARD AFX_DIALOG_LAYOUT +BEGIN + 0, + 0, 0, 100, 100, + 0, 100, 0, 0, + 0, 100, 0, 0, + 0, 100, 0, 0, + 0, 100, 0, 0 +END + +IDD_OPL_PARAMS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CLOSEDOCUMENTS AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 100, 100, 0, 0, + 0, 0, 100, 100, + 0, 100, 0, 0, + 100, 100, 0, 0, + 100, 100, 0, 0 +END + +IDD_MISSINGPLUGS AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 0, 0, 0, + 0, 0, 100, 100, + 100, 100, 0, 0 +END + +IDD_MISSINGSAMPLES AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 0, 0, 0, + 0, 0, 100, 100, + 0, 100, 0, 0, + 0, 100, 100, 0 +END + +IDD_OPTIONS_UPDATE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_WECLOME AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_LEGACY_PLAYBACK AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 100, 100, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 100, 0, + 0, 0, 100, 100, + 0, 100, 0, 0, + 0, 100, 0, 0 +END + +IDD_EDITHISTORY AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 100, 0, 0, + 0, 0, 100, 100, + 0, 100, 100, 0 +END + +IDD_MODIFIEDSAMPLES AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 0, 0, 0, + 0, 0, 100, 100, + 100, 100, 0, 0, + 100, 100, 0, 0, + 0, 100, 100, 0 +END + +IDD_UPDATE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CHANNELSETTINGS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_RESAMPLE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPLEXPORT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +#endif // German (Germany) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_ABOUTBOX DIALOGEX 0, 0, 340, 266 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "About OpenMPT" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,264,78,66,14,WS_GROUP + CONTROL "",IDC_BITMAP1,"Static",SS_BLACKRECT | SS_NOTIFY | SS_CENTERIMAGE,6,6,205,81,WS_EX_CLIENTEDGE + EDITTEXT IDC_EDIT3,213,7,121,70,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | NOT WS_BORDER + CONTROL "",IDC_TABABOUT,"SysTabControl32",0x0,6,96,330,162 + EDITTEXT IDC_EDITABOUT,12,114,318,138,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY | WS_VSCROLL +END + +IDD_OPTIONS_PLAYER DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "DSP" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "",IDC_STATIC,6,6,276,150 + CONTROL "&Enable Graphic Equalizer",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,6,94,10 + LTEXT "+12dB",IDC_STATIC,12,21,22,8 + LTEXT "+0dB",IDC_STATIC,16,49,18,8 + LTEXT "-12dB",IDC_STATIC,14,74,20,8 + CTEXT "100Hz",IDC_TEXT1,37,87,28,8 + CONTROL "",IDC_SLIDER7,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,38,18,24,68 + CTEXT "500Hz",IDC_TEXT2,71,87,29,8 + CONTROL "",IDC_SLIDER8,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,72,18,24,68 + CTEXT "1kHz",IDC_TEXT3,108,87,25,8 + CONTROL "",IDC_SLIDER9,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,106,18,24,68 + CTEXT "3kHz",IDC_TEXT4,143,87,23,8 + CONTROL "",IDC_SLIDER10,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,140,18,24,68 + CTEXT "6kHz",IDC_TEXT5,177,87,21,8 + CONTROL "",IDC_SLIDER11,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,174,18,24,68 + CTEXT "10kHz",IDC_TEXT6,210,87,22,8 + CONTROL "",IDC_SLIDER12,"msctls_trackbar32",TBS_AUTOTICKS | TBS_VERT | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,206,18,24,68 + CTEXT "Right-click on a band to change its center frequency",IDC_STATIC,36,102,200,8 + PUSHBUTTON "User1",IDC_BUTTON1,240,18,36,12 + PUSHBUTTON "User2",IDC_BUTTON2,240,36,36,12 + PUSHBUTTON "User3",IDC_BUTTON3,240,54,36,12 + PUSHBUTTON "User4",IDC_BUTTON4,240,72,36,12 + PUSHBUTTON "&Save...",IDC_BUTTON5,240,90,36,12 + LTEXT "EQ Warning Message",IDC_EQ_WARNING,12,114,264,36,0,WS_EX_TRANSPARENT + GROUPBOX "",IDC_STATIC,6,156,276,120 + CONTROL "&Automatic Gain Control",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,164,97,10 + CONTROL "&Bit Crush",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,114,164,48,10 + CONTROL "",IDC_SLIDER4,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,162,162,62,15 + CONTROL "&Bass Expansion",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,178,81,10 + CONTROL "Slider1",IDC_SLIDER1,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,33,191,62,15 + LTEXT "Low",IDC_STATIC,17,193,14,8 + LTEXT "High",IDC_STATIC,95,193,16,8 + LTEXT "Range:",IDC_STATIC,169,178,24,8 + CONTROL "Slider2",IDC_SLIDER2,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,149,190,62,15 + LTEXT "10Hz",IDC_STATIC,131,193,18,8 + LTEXT "100Hz",IDC_STATIC,211,191,23,8 + CONTROL "&Reverb",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,210,62,10 + CONTROL "Slider1",IDC_SLIDER3,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,33,222,62,15 + LTEXT "Low",IDC_STATIC,17,226,14,8 + LTEXT "High",IDC_STATIC,95,226,16,8 + CTEXT "Reverb Preset:",IDC_STATIC,150,210,62,8 + COMBOBOX IDC_COMBO2,135,223,100,100,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Pro-Logic Surround",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,15,242,99,10 + CONTROL "Slider1",IDC_SLIDER5,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,33,255,62,15 + LTEXT "Low",IDC_STATIC,17,258,14,8 + LTEXT "High",IDC_STATIC,95,258,16,8 + CTEXT "Front/Rear Delay:",IDC_STATIC,147,244,65,8 + CONTROL "Slider2",IDC_SLIDER6,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,149,254,62,15 + LTEXT "5ms",IDC_STATIC,131,257,18,8 + LTEXT "50ms",IDC_STATIC,211,257,23,8 +END + +IDD_WAVECONVERT DIALOGEX 0, 0, 388, 239 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "Export" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,270,216,50,14 + PUSHBUTTON "Cancel",IDCANCEL,330,216,50,14 + GROUPBOX "For&mat",IDC_STATIC,6,6,186,66 + COMBOBOX IDC_COMBO5,12,18,174,82,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO1,12,36,54,82,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO4,72,36,42,82,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO6,120,36,66,82,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO2,12,54,174,75,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Render",IDC_STATIC,6,78,186,84 + PUSHBUTTON "Player &Options",IDC_PLAYEROPTIONS,12,90,60,14,BS_CENTER + CONTROL "&Channel mode (one file per channel)",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,108,174,12 + CONTROL "&Instrument mode (one file per instrument)",IDC_CHECK6, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,120,174,12 + CONTROL "&Normalize Output",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,132,174,12 + CONTROL "&Write cue points on pattern transitions",IDC_CHECK3, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,144,174,12 + GROUPBOX "Plugin Quirks",IDC_STATIC,6,168,186,42 + CONTROL "Slow &render (for Kontakt+DFD)",IDC_GIVEPLUGSIDLETIME, + "Button",BS_AUTOCHECKBOX | BS_LEFT | BS_MULTILINE | WS_TABSTOP,12,180,174,12 + CONTROL "Clear plugin &buffers before exporting",IDC_RENDERSILENCE, + "Button",BS_AUTOCHECKBOX | BS_LEFT | BS_MULTILINE | WS_TABSTOP,12,192,174,12 + GROUPBOX "Limit",IDC_STATIC,198,6,186,84 + CONTROL "Limit song &length to: (seconds)",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,18,114,10 + EDITTEXT IDC_EDIT2,324,18,50,12,ES_AUTOHSCROLL + CONTROL "&Sequences",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,204,36,72,12 + CONTROL "Play &entire song",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,204,54,72,12 + CONTROL "From &position",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,204,73,72,12 + EDITTEXT IDC_EDIT12,276,36,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN6,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,300,41,11,11 + CTEXT "to",IDC_STATIC,318,36,8,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT13,330,36,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN7,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,354,41,11,11 + EDITTEXT IDC_EDIT5,276,54,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN5,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,300,60,11,11 + LTEXT "times",IDC_STATIC,319,55,59,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT3,276,73,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,300,78,11,11 + CTEXT "to",IDC_STATIC,318,73,8,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT4,330,73,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN4,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,354,78,11,11 + GROUPBOX "Tags",IDC_STATIC,198,96,186,114 + CONTROL "Incl&ude Song Information",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,108,174,10 + LTEXT "Title:",IDC_STATIC,204,122,17,8 + EDITTEXT IDC_EDIT11,240,120,138,12,ES_AUTOHSCROLL + LTEXT "Author:",IDC_STATIC,204,140,30,8 + EDITTEXT IDC_EDIT6,240,138,138,12,ES_AUTOHSCROLL + LTEXT "Album:",IDC_STATIC,204,158,30,8 + EDITTEXT IDC_EDIT7,240,156,138,12,ES_AUTOHSCROLL + LTEXT "URL:",IDC_STATIC,204,176,30,8 + EDITTEXT IDC_EDIT8,240,174,138,12,ES_AUTOHSCROLL + LTEXT "Genre:",IDC_STATIC,204,194,29,8 + EDITTEXT IDC_EDIT10,240,192,78,12,ES_AUTOHSCROLL + RTEXT "Year:",IDC_STATIC,324,194,18,8 + EDITTEXT IDC_EDIT9,348,192,30,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Export To:",IDC_STATIC,6,219,48,8 + CONTROL "&File",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,54,219,38,8 + CONTROL "S&le Slot",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,96,219,56,8 + COMBOBOX IDC_COMBO9,156,217,96,119,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO3,240,192,78,64,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP +END + +IDD_PROGRESS DIALOGEX 0, 0, 220, 55 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_VISIBLE | WS_CAPTION | WS_SYSMENU +CAPTION "Saving File" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + PUSHBUTTON "Stop",IDCANCEL,84,34,50,14 + CONTROL "Progress1",IDC_PROGRESS1,"msctls_progress32",WS_BORDER,4,19,212,12 + CTEXT "Writing file...",IDC_TEXT1,4,4,212,9 +END + +IDD_OPTIONS_KEYBOARD DIALOGEX 0, 0, 286, 282 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Keyboard" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Select category:",IDC_STATIC,7,5,131,11 + COMBOBOX IDC_KEYCATEGORY,5,16,139,204,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LISTBOX IDC_COMMAND_LIST,5,32,139,226,LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Key setup for selected command ",IDC_STATIC,150,6,132,84 + COMBOBOX IDC_CHOICECOMBO,156,18,78,51,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Key:",IDC_STATIC,156,38,16,8 + EDITTEXT IDC_CUSTHOTKEY,174,36,60,13,ES_AUTOHSCROLL + CONTROL "On Key Down",IDC_CHECKKEYDOWN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,156,54,66,8 + CONTROL "On Key Hold",IDC_CHECKKEYHOLD,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,156,66,66,8 + CONTROL "On Key Up",IDC_CHECKKEYUP,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,156,78,66,8 + PUSHBUTTON "Restore",IDC_RESTORE,240,36,37,13 + PUSHBUTTON "Delete",IDC_DELETE,240,18,37,13 + GROUPBOX "Misc",IDC_STATIC,150,96,132,40 + LTEXT "Repeat notes on hold?",IDC_STATIC,156,108,74,8 + PUSHBUTTON "Yes",IDC_NOTESREPEAT,240,108,18,9 + PUSHBUTTON "No",IDC_NONOTESREPEAT,258,108,18,9 + LTEXT "Chord detect interval (ms):",IDC_STATIC,156,122,88,8 + EDITTEXT IDC_CHORDDETECTWAITTIME,246,120,30,12,ES_AUTOHSCROLL | ES_NUMBER,WS_EX_RIGHT + GROUPBOX "Keyboard Mapping",IDC_STATIC,150,144,132,48 + PUSHBUTTON "Import Keys...",IDC_LOAD,156,156,54,13 + PUSHBUTTON "Export Keys...",IDC_SAVE,222,156,54,13 + PUSHBUTTON "Restore default configuration",IDC_RESTORE_KEYMAP,156,174,120,12 + LTEXT "Error Log:",IDC_STATIC,150,198,78,8 + PUSHBUTTON "Clear Log",IDC_CLEARLOG,239,196,42,12 + EDITTEXT IDC_KEYREPORT,150,210,132,66,ES_MULTILINE | ES_AUTOVSCROLL | ES_READONLY | WS_VSCROLL + EDITTEXT IDC_FIND,30,264,54,12,ES_AUTOHSCROLL + LTEXT "Find:",IDC_STATIC,6,266,24,8 + EDITTEXT IDC_FINDHOTKEY,108,264,36,12,ES_AUTOHSCROLL + LTEXT "Key:",IDC_STATIC,90,266,17,8 +END + +IDD_OPTIONS_COLORS DIALOGEX 0, 0, 286, 282 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Display" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "",IDC_STATIC,6,6,276,162 + LTEXT "Pattern &Font:",IDC_STATIC,18,20,54,8 + COMBOBOX IDC_COMBO2,72,18,144,66,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Custom...",IDC_BUTTON9,222,18,50,12 + LTEXT "Comments f&ont:",IDC_STATIC,18,38,52,8 + PUSHBUTTON "Font",IDC_BUTTON10,72,36,144,12 + LTEXT "Display accidentals as:",IDC_STATIC,18,53,78,12,SS_CENTERIMAGE + CONTROL "&Sharps (#)",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,102,54,66,12 + CONTROL "Flats (&b)",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,168,54,108,12 + LTEXT "Default channel colours:",IDC_STATIC,18,67,78,12,SS_CENTERIMAGE + CONTROL "&No Colours",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,102,67,66,12 + CONTROL "&Rainbow",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON,168,67,60,12 + CONTROL "Ran&dom",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON,228,66,48,12 + CONTROL "&Enable effect highlighting",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,84,114,10 + CONTROL "Remember each song's &window positions",IDC_CHECK5, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,102,168,10 + PUSHBUTTON "Clear Song Cac&he",IDC_BUTTON11,186,100,78,12 + CONTROL "&Primary highlight",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,118,84,10 + EDITTEXT IDC_PRIMARYHILITE,102,118,24,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Rows per measure (default)",IDC_STATIC,132,121,144,8 + CONTROL "Secondar&y highlight",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,137,80,10 + EDITTEXT IDC_SECONDARYHILITE,102,137,24,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Rows per beat (default)",IDC_STATIC,132,140,144,8 + LTEXT "Note: Songs' time signatures will override the default highlight values",IDC_STATIC,18,153,258,8 + GROUPBOX "Colours",IDC_STATIC,6,174,276,105 + LTEXT "Select colo&ur for:",IDC_STATIC,18,188,63,8 + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_ARROWKEYS,84,186,11,12 + COMBOBOX IDC_COMBO1,96,186,114,109,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "",IDC_BUTTON4,"Button",BS_OWNERDRAW | BS_FLAT,18,204,120,48 + LTEXT "C1",IDC_TEXT1,144,207,72,8 + CONTROL "Change &1",IDC_BUTTON1,"Button",BS_OWNERDRAW,216,204,51,12 + LTEXT "C2",IDC_TEXT2,144,224,72,8 + CONTROL "Change &2",IDC_BUTTON2,"Button",BS_OWNERDRAW,216,223,51,12 + LTEXT "C3",IDC_TEXT3,144,242,72,8 + CONTROL "Change &3",IDC_BUTTON3,"Button",BS_OWNERDRAW,216,240,51,12 + LTEXT "Presets:",IDC_STATIC,18,261,36,8 + PUSHBUTTON "&Load...",IDC_LOAD_COLORSCHEME,54,258,42,15 + PUSHBUTTON "S&ave...",IDC_SAVE_COLORSCHEME,102,258,42,15 + COMBOBOX IDC_COMBO3,150,259,126,203,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP +END + +IDD_OPTIONS_MIDI DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "MIDI" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "MIDI Recording",IDC_STATIC,6,6,276,108 + LTEXT "MIDI &Input Device:",IDC_STATIC,18,18,67,8 + COMBOBOX IDC_COMBO1,18,30,198,74,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Re&name",IDC_BUTTON1,222,30,50,14 + CONTROL "&Apply Octave Transpose to incoming MIDI Notes",IDC_CHECK4, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,48,237,12 + CONTROL "&Respond to Play / Continue / Stop Song messages",IDC_MIDIPLAYCONTROL, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,60,237,12 + CONTROL "&Continue song as soon as MIDI notes are being received",IDC_MIDIPLAYPATTERNONMIDIIN, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,73,237,12 + CONTROL "&Pass MIDI to active instrument plugin",IDC_MIDI_TO_PLUGIN, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,84,237,12 + CONTROL "&Enable MIDI recording when launching OpenMPT",IDC_CHECK3, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,97,240,12 + GROUPBOX "MIDI Recording - Volume and Controllers",IDC_STATIC,6,120,276,114 + CONTROL "Record Note &Off",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,138,237,8 + CONTROL "Record MIDI Note &Velocity, amplify by",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,150,144,8 + EDITTEXT IDC_EDIT3,162,148,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,186,145,11,14 + LTEXT "%",IDC_STATIC,207,151,48,8 + CONTROL "Combine MIDI Vol&ume (CC#07) to Note Velocity",IDC_MIDIVOL_TO_NOTEVOL, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,162,258,8 + CONTROL "Recor&d Pitch Bend messages as MIDI Macro changes in pattern",IDC_CHECK5, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,174,258,8 + CONTROL "Record MIDI Contro&ller changes as MIDI Macro changes in pattern",IDC_MIDI_MACRO_CONTROL, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,186,258,8 + LTEXT "I&gnore CCs (e.g. 1,123,127)",IDC_STATIC,30,200,108,8 + EDITTEXT IDC_EDIT4,138,198,132,12,ES_AUTOHSCROLL + LTEXT "Record A&ftertouch Messages",IDC_STATIC,30,216,108,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO2,138,216,132,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "MIDI File Import",IDC_STATIC,6,240,276,36 + LTEXT "&Quantize:",IDC_STATIC,12,258,32,8 + COMBOBOX IDC_COMBO3,48,256,60,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Tic&ks / Row:",IDC_STATIC,114,258,40,8 + EDITTEXT IDC_EDIT1,156,256,30,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,174,258,11,14 + LTEXT "Pattern Si&ze:",IDC_STATIC,192,258,48,8 + EDITTEXT IDC_EDIT2,240,256,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,259,254,11,14 +END + +IDD_LOADRAWSAMPLE DIALOGEX 0, 0, 249, 128 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Unknown file type" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,138,108,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,192,108,50,14 + LTEXT "Load it as a raw sample of the following format:",IDC_STATIC,6,6,156,12 + PUSHBUTTON "&Auto-Detect Format",IDC_BUTTON1,6,84,72,14 + GROUPBOX "",IDC_STATIC,6,18,48,60,WS_GROUP + CONTROL "&8-bit",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,12,26,36,10 + CONTROL "&16-bit",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,12,38,36,10 + CONTROL "&24-bit",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,12,50,36,10 + CONTROL "&32-bit",IDC_RADIO4,"Button",BS_AUTORADIOBUTTON,12,62,36,10 + GROUPBOX "",IDC_STATIC,60,18,54,60,WS_GROUP + CONTROL "S&igned",IDC_RADIO7,"Button",BS_AUTORADIOBUTTON,66,26,42,10 + CONTROL "&Unsigned",IDC_RADIO8,"Button",BS_AUTORADIOBUTTON,66,38,42,10 + CONTROL "&Delta",IDC_RADIO9,"Button",BS_AUTORADIOBUTTON,66,50,42,10 + CONTROL "&Float",IDC_RADIO10,"Button",BS_AUTORADIOBUTTON,66,62,42,10 + GROUPBOX "",IDC_STATIC,120,18,54,34,WS_GROUP + CONTROL "&Mono",IDC_RADIO5,"Button",BS_AUTORADIOBUTTON,126,26,42,10 + CONTROL "&Stereo",IDC_RADIO6,"Button",BS_AUTORADIOBUTTON,126,38,42,10 + GROUPBOX "",IDC_STATIC,180,18,66,34,WS_GROUP + CONTROL "&Little Endian",IDC_RADIO11,"Button",BS_AUTORADIOBUTTON,186,26,54,10 + CONTROL "&Big Endian",IDC_RADIO12,"Button",BS_AUTORADIOBUTTON,186,39,54,10 + LTEXT "Offset (Bytes):",IDC_STATIC,120,63,50,8 + EDITTEXT IDC_EDIT1,174,60,60,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS,234,60,11,14 + CONTROL "Remember these settings",IDC_CHK_REMEMBERSETTINGS, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,84,85,156,12 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,102,234,1 +END + +IDD_CONTROL_GLOBALS DIALOGEX 0, 0, 440, 122 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + LTEXT "Name:",IDC_STATIC,2,5,23,8 + EDITTEXT IDC_EDIT_SONGTITLE,30,3,156,12,ES_AUTOHSCROLL + LTEXT "Artist:",IDC_STATIC,2,20,23,8 + EDITTEXT IDC_EDIT_ARTIST,30,18,156,12,ES_AUTOHSCROLL + PUSHBUTTON "Type EXT, ## channels",IDC_BUTTON_MODTYPE,192,3,150,12 + LTEXT "Resampling:",IDC_STATIC,192,20,48,8 + COMBOBOX IDC_COMBO1,240,18,102,90,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Initial Tempo",IDC_STATIC,5,42,49,12 + CONTROL "",IDC_SLIDER_SONGTEMPO,"msctls_trackbar32",TBS_VERT | TBS_BOTH | TBS_NOTICKS,11,50,15,50 + EDITTEXT IDC_EDIT_TEMPO,5,101,43,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_TEMPO,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,33,98,11,14 + PUSHBUTTON "Tap",IDC_BUTTON1,54,42,36,12 + LTEXT "Ticks/Row:",IDC_STATIC,54,90,36,8 + EDITTEXT IDC_EDIT_SPEED,54,101,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN_SPEED,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,82,82,11,14 + LTEXT "Restart Position",IDC_STATIC,102,42,60,8 + EDITTEXT IDC_EDIT_RESTARTPOS,102,54,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN_RESTARTPOS,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,132,54,11,14 + CONTROL "Loop Song",IDC_CHECK_LOOPSONG,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,102,90,54,10 + CTEXT "Not saved in song!",IDC_STATIC,96,102,66,12 + CTEXT "Initial Global Vol",IDC_STATIC,165,42,51,8 + CONTROL "",IDC_SLIDER_GLOBALVOL,"msctls_trackbar32",TBS_VERT | TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS,181,50,24,50 + EDITTEXT IDC_EDIT_GLOBALVOL,174,100,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_GLOBALVOL,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,203,98,11,14 + CTEXT "Synth Volume",IDC_STATIC,228,42,48,8 + CONTROL "",IDC_SLIDER_VSTIVOL,"msctls_trackbar32",TBS_VERT | TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS,243,50,22,50 + EDITTEXT IDC_EDIT_VSTIVOL,235,100,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_VSTIVOL,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,263,98,11,14 + CTEXT "Sample Volume",IDC_STATIC,280,42,55,8 + CONTROL "Slider1",IDC_SLIDER_SAMPLEPREAMP,"msctls_trackbar32",TBS_VERT | TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS,300,50,19,50 + EDITTEXT IDC_EDIT_SAMPLEPA,291,100,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_SAMPLEPA,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,317,97,11,14 + CONTROL "",IDC_VUMETER_LEFT,"Static",SS_BLACKRECT | SS_SUNKEN,348,3,15,115 + CONTROL "",IDC_VUMETER_RIGHT,"Static",SS_BLACKRECT | SS_SUNKEN,366,3,15,115 + GROUPBOX "",IDC_STATIC,1,30,96,88 + GROUPBOX "",IDC_STATIC,96,30,67,88 + GROUPBOX "",IDC_STATIC,162,30,180,88 +END + +IDD_CONTROL_COMMENTS DIALOGEX 0, 0, 435, 119 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + EDITTEXT IDC_EDIT_COMMENTS,4,11,426,104,ES_MULTILINE | ES_WANTRETURN | NOT WS_BORDER | WS_VSCROLL | WS_HSCROLL | NOT WS_TABSTOP,WS_EX_STATICEDGE + LTEXT "Song Message:",IDC_STATIC,4,2,76,8 +END + +IDD_CONTROL_PATTERNS DIALOGEX 0, 0, 560, 86 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + CONTROL "Toolbar1",IDC_TOOLBAR1,"ToolbarWindow32",WS_GROUP | 0x4d,2,4,370,18 + CONTROL "Spin1",IDC_SPIN_INSTRUMENT,"msctls_updown32",0x0,6,31,11,12 + COMBOBOX IDC_COMBO_INSTRUMENT,18,31,120,137,CBS_DROPDOWNLIST | WS_VSCROLL + PUSHBUTTON "Plugin",IDC_PATINSTROPLUGGUI,144,30,33,13,0,WS_EX_STATICEDGE + RTEXT "Edit Step",IDC_STATIC,180,33,30,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_SPACING,216,31,28,12,ES_AUTOHSCROLL | ES_NUMBER | NOT WS_TABSTOP + CONTROL "Spin1",IDC_SPIN_SPACING,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,236,31,11,11 + CONTROL "Loop Pattern",IDC_PATTERN_LOOP,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,252,33,57,8 + CONTROL "Follow Song",IDC_PATTERN_FOLLOWSONG,"Button",BS_AUTOCHECKBOX | BS_FLAT,312,33,55,8 + LTEXT "Pattern Name",IDC_STATIC_PATTERNNAME,384,33,48,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_PATTERNNAME,438,31,109,12,ES_AUTOHSCROLL + LTEXT "Sequence",IDC_STATIC_SEQUENCE_NAME,384,10,36,8 + EDITTEXT IDC_EDIT_SEQNUM,420,8,28,12,ES_AUTOHSCROLL | ES_NUMBER | NOT WS_TABSTOP + CONTROL "",IDC_SPIN_SEQNUM,"msctls_updown32",UDS_WRAP | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,438,6,11,11 + EDITTEXT IDC_EDIT_SEQUENCE_NAME,450,8,97,12,ES_AUTOHSCROLL + PUSHBUTTON "<<",IDC_BUTTON2,3,51,14,15,0,WS_EX_STATICEDGE + PUSHBUTTON ">>",IDC_BUTTON1,15,51,14,15,0,WS_EX_STATICEDGE + GROUPBOX "",IDC_STATIC,3,23,369,24 + GROUPBOX "",IDC_STATIC_SEQUENCE_NAME_FRAME,378,0,174,24 + GROUPBOX "",IDC_STATIC,378,23,174,24 +END + +IDD_CONTROL_SAMPLES DIALOGEX 0, 0, 544, 106 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + CONTROL "Toolbar1",IDC_TOOLBAR1,"ToolbarWindow32",WS_GROUP | 0x4d,4,5,86,19 + RTEXT "Sample",IDC_STATIC,90,8,24,8 + EDITTEXT IDC_EDIT_SAMPLE,120,6,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin6",IDC_SPIN_SAMPLE,"msctls_updown32",UDS_WRAP | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,138,4,12,14 + COMBOBOX IDC_COMBO_ZOOM,162,6,36,89,CBS_DROPDOWNLIST | WS_VSCROLL + CTEXT "Length: 000000 (16-bit)",IDC_TEXT5,204,6,102,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "Toolbar2",IDC_TOOLBAR2,"ToolbarWindow32",WS_GROUP | 0x4d,308,5,232,17 + CTEXT "Name",IDC_STATIC,102,26,28,12,SS_CENTERIMAGE | NOT WS_GROUP,WS_EX_STATICEDGE + EDITTEXT IDC_SAMPLE_NAME,129,26,135,12,ES_AUTOHSCROLL + CTEXT "File",IDC_STATIC,269,26,23,13,SS_CENTERIMAGE | NOT WS_GROUP,WS_EX_STATICEDGE + EDITTEXT IDC_SAMPLE_FILENAME,292,26,72,13,ES_AUTOHSCROLL + CONTROL "&Keep Sample Data on Disk",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,372,27,150,10 + LTEXT "Default Volume",IDC_STATIC,8,32,49,8 + EDITTEXT IDC_EDIT7,57,30,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN7,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,87,29,11,11 + LTEXT "Global Volume",IDC_STATIC,8,45,46,8 + EDITTEXT IDC_EDIT8,57,43,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN8,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,87,42,11,11 + CONTROL "Set Pan",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,9,58,42,10 + EDITTEXT IDC_EDIT9,57,56,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN9,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,87,56,11,11 + LTEXT "FineTune",IDC_TEXT7,8,73,37,8 + EDITTEXT IDC_EDIT5,48,70,45,12,ES_AUTOHSCROLL + CONTROL "Spin1",IDC_SPIN5,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,87,69,11,11 + LTEXT "Transpose",IDC_TEXT6,8,86,38,8 + COMBOBOX IDC_COMBO_BASENOTE,48,83,45,89,CBS_DROPDOWNLIST | WS_VSCROLL + LTEXT "Type",IDC_STATIC,107,57,17,8 + COMBOBOX IDC_COMBO1,130,54,45,46,CBS_DROPDOWNLIST | WS_TABSTOP + LTEXT "Start",IDC_STATIC,107,73,16,8 + EDITTEXT IDC_EDIT1,130,70,45,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN1,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,169,69,11,11 + LTEXT "End",IDC_STATIC,107,86,14,8 + EDITTEXT IDC_EDIT2,130,83,45,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN2,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,169,84,11,11 + LTEXT "Type",IDC_STATIC,189,57,17,8 + COMBOBOX IDC_COMBO2,212,54,45,46,CBS_DROPDOWNLIST | WS_TABSTOP + LTEXT "Start",IDC_STATIC,189,73,16,8 + EDITTEXT IDC_EDIT3,212,70,45,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN3,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,253,70,11,11 + LTEXT "End",IDC_STATIC,189,86,14,8 + EDITTEXT IDC_EDIT4,212,83,45,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN4,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,253,83,11,11 + LTEXT "Auto-Vibrato",IDC_STATIC,275,50,41,8 + COMBOBOX IDC_COMBO3,275,59,47,70,CBS_DROPDOWNLIST | WS_TABSTOP + LTEXT "Depth",IDC_STATIC,327,50,20,8 + EDITTEXT IDC_EDIT15,327,59,32,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Depth",IDC_SPIN12,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,353,59,11,11 + LTEXT "Sweep",IDC_STATIC,275,74,23,8 + EDITTEXT IDC_EDIT14,275,83,38,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Sweep",IDC_SPIN11,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,305,84,11,11 + LTEXT "Rate",IDC_STATIC,321,74,16,8 + EDITTEXT IDC_EDIT16,321,83,38,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Rate",IDC_SPIN13,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,353,83,11,11 + GROUPBOX "Pitch Shifting / Time Stretching",IDC_GROUPBOX_PITCH_TIME,372,41,168,59 + LTEXT "Pitch",IDC_TEXT_PITCH,375,53,45,8 + COMBOBOX IDC_COMBO4,420,51,39,61,CBS_DROPDOWNLIST | WS_VSCROLL | WS_GROUP | WS_TABSTOP + EDITTEXT IDC_EDIT10,420,51,39,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN10,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,450,54,11,11 + LTEXT "Quality",IDC_TEXT_QUALITY,376,69,44,8 + COMBOBOX IDC_COMBO5,420,67,39,86,CBS_DROPDOWNLIST | WS_VSCROLL | WS_GROUP | WS_TABSTOP + EDITTEXT IDC_EDIT11,420,67,39,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN14,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,450,67,11,11 + LTEXT "FFT",IDC_TEXT_FFT,376,85,44,8 + COMBOBOX IDC_COMBO6,420,83,39,61,CBS_DROPDOWNLIST | WS_VSCROLL | WS_GROUP | WS_TABSTOP + EDITTEXT IDC_EDIT12,420,83,39,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN15,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,450,81,11,11 + CONTROL "Time Stretching",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,465,52,68,11 + EDITTEXT IDC_EDIT6,465,67,39,12,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER,WS_EX_RIGHT + CONTROL "",IDC_SPIN16,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,495,64,11,11 + LTEXT "%",IDC_TEXT_PERCENT,510,68,8,10 + PUSHBUTTON "...",IDC_BUTTON2,521,67,13,13 + PUSHBUTTON "Process",IDC_BUTTON1,465,82,70,14 + GROUPBOX "",IDC_STATIC,3,22,94,78 + GROUPBOX "Loop",IDC_STATIC,102,43,79,57 + GROUPBOX "Sustain Loop",IDC_STATIC,185,43,79,57 + GROUPBOX "",IDC_STATIC,269,43,96,57 +END + +IDD_CONTROL_INSTRUMENTS DIALOGEX 0, 0, 536, 170 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + CONTROL "Toolbar1",IDC_TOOLBAR1,"ToolbarWindow32",WS_GROUP | 0x4d,4,5,92,19 + EDITTEXT IDC_EDIT_INSTRUMENT,97,7,33,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN_INSTRUMENT,"msctls_updown32",UDS_WRAP | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS | WS_TABSTOP,121,8,11,11 + EDITTEXT IDC_SAMPLE_NAME,141,7,151,12,ES_AUTOHSCROLL + CTEXT "File",IDC_STATIC,299,7,35,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_SAMPLE_FILENAME,334,7,105,12,ES_AUTOHSCROLL + CTEXT "Global &Volume",IDC_STATIC,8,36,72,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT8,84,36,37,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN8,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,112,40,8,10 + CTEXT "&Fade Out",IDC_STATIC,8,53,72,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT7,84,53,37,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN7,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,112,57,8,10 + CONTROL "Set &Pan",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,12,72,66,10 + EDITTEXT IDC_EDIT9,84,70,37,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN9,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,112,74,8,11 + CTEXT "S&ep",IDC_STATIC,8,99,20,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT15,30,99,30,13,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN12,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,54,102,11,11 + CTEXT "Cen&tre",IDC_STATIC,62,99,26,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO4,90,99,31,91,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CTEXT "Rampi&ng",IDC_STATIC,7,133,39,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "",IDC_SLIDER5,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS,47,135,36,10 + EDITTEXT IDC_EDIT2,85,133,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,116,133,8,12 + CTEXT "Resamplin&g",IDC_STATIC,7,151,39,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO9,50,151,71,81,CBS_DROPDOWNLIST | WS_TABSTOP + CONTROL "&Reso",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,136,37,33,10 + CONTROL "Slider2",IDC_SLIDER4,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_DISABLED | WS_TABSTOP,168,37,64,10 + CONTROL "&Cutoff",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,136,52,35,10 + CONTROL "Slider2",IDC_SLIDER3,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_DISABLED | WS_TABSTOP,168,53,64,10 + RTEXT "--",IDC_FILTERTEXT,166,62,65,8 + CTEXT "&Mode",IDC_STATIC,135,70,23,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_FILTERMODE,163,70,69,42,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CTEXT "V&olume",IDC_STATIC,136,99,33,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "Slider1",IDC_SLIDER1,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,172,101,60,10 + CTEXT "P&anning",IDC_STATIC,136,116,33,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "Slider1",IDC_SLIDER2,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,172,118,60,10 + CTEXT "C&utoff",IDC_STATIC,136,133,33,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "",IDC_SLIDER6,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,172,135,60,10 + CTEXT "R&eso",IDC_STATIC,136,151,33,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "",IDC_SLIDER7,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | TBS_TOOLTIPS | WS_TABSTOP,172,152,60,10 + CTEXT "Acti&on",IDC_STATIC,246,36,59,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO1,308,36,50,54,CBS_DROPDOWNLIST | WS_TABSTOP + CTEXT "&Duplicate Check",IDC_STATIC,246,53,59,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO2,308,53,50,69,CBS_DROPDOWNLIST | WS_TABSTOP + CTEXT "Dupl&icate Action",IDC_STATIC,246,70,59,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO3,308,70,50,54,CBS_DROPDOWNLIST | WS_TABSTOP + COMBOBOX IDC_COMBO6,246,99,72,127,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Editor",IDC_INSVIEWPLG,321,99,37,13,0,WS_EX_STATICEDGE + CTEXT "MIDI &Channel",IDC_STATIC,246,116,72,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO5,321,116,37,169,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CTEXT "MIDI &Program",IDC_STATIC,246,134,72,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT10,321,134,37,12,ES_AUTOHSCROLL + CONTROL "Spin1",IDC_SPIN10,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,352,135,8,11 + CTEXT "MIDI &Bank",IDC_STATIC,245,152,72,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT11,321,152,37,12,ES_AUTOHSCROLL + CONTROL "",IDC_SPIN11,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,351,153,8,11 + CTEXT "P. &Bend Range",IDC_STATIC,366,99,52,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_PITCHWHEELDEPTH,420,99,36,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS | WS_TABSTOP,444,102,11,11 + CTEXT "Volume Command &Handling",IDC_STATIC,366,116,90,10,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_PLUGIN_VOLUMESTYLE,366,128,90,53,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Volume commands with note are Velocities",IDC_PLUGIN_VELOCITYSTYLE, + "Button",BS_AUTOCHECKBOX | BS_VCENTER | BS_MULTILINE | BS_FLAT | WS_TABSTOP,366,141,90,24 + COMBOBOX IDC_COMBOTUNING,368,38,88,48,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Loop Tempo:",IDC_CHECK_PITCHTEMPOLOCK,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,369,70,51,8 + EDITTEXT IDC_EDIT_PITCHTEMPOLOCK,426,68,30,12,ES_AUTOHSCROLL | ES_NUMBER + GROUPBOX "Sample &Map",IDC_STATIC,466,27,62,141 + CONTROL "",IDC_NOTEMAP,"Static",SS_GRAYRECT | SS_NOTIFY | WS_TABSTOP,466,37,62,131,WS_EX_CLIENTEDGE + GROUPBOX "General",IDC_STATIC,3,27,123,141 + GROUPBOX "Pitch/Pan Separation",IDC_STATIC,3,88,123,80 + GROUPBOX "Sample Quality",IDC_STATIC,3,118,123,50 + GROUPBOX "Filter",IDC_STATIC,131,27,104,141 + GROUPBOX "Random Variation",IDC_STATIC,131,88,104,80 + GROUPBOX "New Note Action",IDC_STATIC,240,27,122,60 + GROUPBOX "Plugin / MIDI",IDC_STATIC,240,88,222,80 + GROUPBOX "Alternative Tuning",IDC_STATIC,364,27,98,28 + GROUPBOX "Pitch/Tempo Lock",IDC_STATIC,364,57,98,30 +END + +IDD_MODDOC_MODTYPE DIALOGEX 0, 0, 262, 287 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Song Properties" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,204,6,50,14 + PUSHBUTTON "Cancel",IDCANCEL,204,24,50,14 + COMBOBOX IDC_COMBO1,12,18,108,51,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO2,126,18,66,77,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Linear Frequency Slides",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,54,108,10 + CONTROL "&Old Effects (IT)",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,120,54,108,10 + CONTROL "Fast &Volume Slides (S3M)",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,66,108,10 + CONTROL "Compatible &Gxx (IT)",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,120,66,108,10 + CONTROL "&ProTracker 1/2 Mode (MOD)",IDC_CHECK_PT1X,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,78,108,10 + CONTROL "Extended &filter range",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,78,108,10 + CONTROL "&Amiga Frequency Limits",IDC_CHECK_AMIGALIMITS,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,120,78,108,10 + LTEXT "&Mix Levels:",IDC_TEXT_MIXMODE,12,116,48,8 + COMBOBOX IDC_COMBO_MIXLEVELS,60,114,108,51,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Tempo Mode:",IDC_TEXT_TEMPOMODE,12,196,44,8 + COMBOBOX IDC_COMBO_TEMPOMODE,62,194,82,77,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Configure S&wing",IDC_BUTTON1,62,212,82,12 + LTEXT "Rows per beat",IDC_TEXT_ROWSPERBEAT,180,196,66,8 + EDITTEXT IDC_ROWSPERBEAT,150,194,24,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Rows per measure",IDC_TEXT_ROWSPERMEASURE,180,212,66,8 + EDITTEXT IDC_ROWSPERMEASURE,150,212,24,12,ES_AUTOHSCROLL | ES_NUMBER + RTEXT "Created with:",IDC_TEXT_CREATEDWITH,12,250,60,8 + EDITTEXT IDC_EDIT_CREATEDWITH,78,247,166,13,ES_AUTOHSCROLL | ES_READONLY,WS_EX_STATICEDGE + RTEXT "Last saved with:",IDC_TEXT_SAVEDWITH,12,266,60,8 + EDITTEXT IDC_EDIT_SAVEDWITH,78,266,166,13,ES_AUTOHSCROLL | ES_READONLY,WS_EX_STATICEDGE + GROUPBOX "Type",IDC_FRAME_MODTYPE,6,6,192,30 + GROUPBOX "Playback",IDC_FRAME_MODFLAGS,6,42,246,54 + GROUPBOX "Extended Playback Options (OpenMPT only)",IDC_FRAME_MPTEXT,6,102,246,72 + GROUPBOX "Tempo",IDC_FRAME_TEMPOMODE,6,180,246,48 + GROUPBOX "Version Info",IDC_FRAME_MPTVERSION,6,234,246,48 + PUSHBUTTON "&Edit Compatibility Settings",IDC_BUTTON2,96,156,102,12 + LTEXT "",IDC_STATIC2,12,132,210,18 + ICON "",IDC_STATIC1,222,132,20,20,SS_CENTERIMAGE | SS_REALSIZEIMAGE + PUSHBUTTON "&Set Defaults",IDC_BUTTON3,12,156,72,12 +END + +IDD_REMOVECHANNELS DIALOGEX 0, 0, 171, 221 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Remove Channels" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,54,198,50,14 + PUSHBUTTON "Cancel",IDCANCEL,114,198,50,14 + LISTBOX IDC_REMCHANSLIST,6,36,156,156,LBS_NOINTEGRALHEIGHT | LBS_EXTENDEDSEL | WS_VSCROLL | WS_TABSTOP + LTEXT "Channels:",IDC_QUESTION1,6,6,157,18 + LTEXT "Detected unused channels are already selected",IDC_STATIC,4,24,159,10 +END + +IDD_VIEW_GLOBALS DIALOGEX 0, 0, 471, 337 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + CONTROL "",IDC_TABCTRL1,"SysTabControl32",0x0,0,0,412,14 + GROUPBOX "Channel 1",IDC_TEXT1,5,18,99,157 + EDITTEXT IDC_EDIT9,10,30,89,12,ES_AUTOHSCROLL + GROUPBOX "Channel 2",IDC_TEXT2,108,18,99,157 + EDITTEXT IDC_EDIT10,113,30,89,12,ES_AUTOHSCROLL + GROUPBOX "Channel 3",IDC_TEXT3,211,18,99,157 + EDITTEXT IDC_EDIT11,216,30,89,12,ES_AUTOHSCROLL + GROUPBOX "Channel 4",IDC_TEXT4,314,18,99,157 + EDITTEXT IDC_EDIT12,319,30,89,12,ES_AUTOHSCROLL + CONTROL "",IDC_BUTTON9,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,66,16,33,11 + CONTROL "",IDC_BUTTON10,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,169,16,33,11 + CONTROL "",IDC_BUTTON11,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,272,16,33,11 + CONTROL "",IDC_BUTTON12,"Button",BS_OWNERDRAW | BS_FLAT | WS_TABSTOP,375,16,33,11 + LTEXT "Initial Volume:",IDC_STATIC,10,48,45,8 + EDITTEXT IDC_EDIT1,63,46,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,95,45,11,14 + LTEXT "Initial Volume:",IDC_STATIC,113,48,45,8 + EDITTEXT IDC_EDIT3,166,46,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,198,45,11,14 + LTEXT "Initial Volume:",IDC_STATIC,216,48,45,8 + EDITTEXT IDC_EDIT5,269,46,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN5,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,301,45,11,14 + LTEXT "Initial Volume:",IDC_STATIC,319,48,45,8 + EDITTEXT IDC_EDIT7,372,46,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN7,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,399,45,11,14 + CONTROL "Slider2",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,10,59,92,22 + CONTROL "Slider2",IDC_SLIDER3,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,113,59,92,22 + CONTROL "Slider2",IDC_SLIDER5,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,216,59,92,22 + CONTROL "Slider2",IDC_SLIDER7,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,319,59,92,22 + LTEXT "Initial Pan:",IDC_STATIC,10,89,34,8 + EDITTEXT IDC_EDIT2,63,88,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,95,86,11,14 + LTEXT "Initial Pan:",IDC_STATIC,113,89,34,8 + EDITTEXT IDC_EDIT4,166,88,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN4,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,198,86,11,14 + LTEXT "Initial Pan:",IDC_STATIC,216,89,34,8 + EDITTEXT IDC_EDIT6,269,88,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN6,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,301,86,11,14 + LTEXT "Initial Pan:",IDC_STATIC,319,89,34,8 + EDITTEXT IDC_EDIT8,372,88,36,12,ES_NUMBER + CONTROL "Spin1",IDC_SPIN8,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,401,86,11,14 + CONTROL "Slider2",IDC_SLIDER2,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,10,100,92,22 + CONTROL "Slider2",IDC_SLIDER4,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,113,100,92,22 + CONTROL "Slider2",IDC_SLIDER6,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,216,100,92,22 + CONTROL "Slider2",IDC_SLIDER8,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,319,100,92,22 + CONTROL "Mute",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,13,129,42,12 + CONTROL "Surround",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,57,129,42,12 + CONTROL "Mute",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,116,129,42,12 + CONTROL "Surround",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,159,129,42,12 + CONTROL "Mute",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,219,129,42,12 + CONTROL "Surround",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,262,129,42,12 + CONTROL "Mute",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,322,129,42,12 + CONTROL "Surround",IDC_CHECK8,"Button",BS_AUTOCHECKBOX | BS_PUSHLIKE | WS_TABSTOP,365,129,42,12 + LTEXT "Effect:",IDC_STATIC,10,146,29,8 + COMBOBOX IDC_COMBO1,10,155,89,99,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Effect:",IDC_STATIC,113,146,29,8 + COMBOBOX IDC_COMBO2,113,155,89,99,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Effect:",IDC_STATIC,216,146,29,8 + COMBOBOX IDC_COMBO3,216,155,89,99,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Effect:",IDC_STATIC,319,146,29,8 + COMBOBOX IDC_COMBO4,319,155,89,99,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Plugins",IDC_STATIC,5,177,408,131 + PUSHBUTTON "<<",IDC_BUTTON5,11,191,18,13 + PUSHBUTTON ">>",IDC_BUTTON4,32,191,18,13 + COMBOBOX IDC_COMBO5,53,191,135,148,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&Select...",IDC_BUTTON1,193,191,34,13 + PUSHBUTTON "&Editor",IDC_BUTTON2,231,191,27,13 + PUSHBUTTON "M&ove...",IDC_MOVEFXSLOT,53,207,34,13 + PUSHBUTTON "&Clone...",IDC_CLONEPLUG,100,207,34,13 + PUSHBUTTON "&Insert",IDC_INSERTFXSLOT,146,207,34,13 + PUSHBUTTON "&Remove",IDC_DELPLUGIN,193,207,34,13 + CTEXT "Display Name",IDC_STATIC,263,191,48,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + EDITTEXT IDC_EDIT13,312,191,94,13,ES_CENTER | ES_AUTOHSCROLL + CTEXT "I/O Type:",IDC_STATIC,263,208,47,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "",IDC_TEXT6,"Static",SS_LEFTNOWORDWRAP | SS_CENTERIMAGE | WS_GROUP,312,208,95,13,WS_EX_STATICEDGE + GROUPBOX "Factory Preset",IDC_STATIC,11,226,177,34 + COMBOBOX IDC_COMBO8,16,239,102,68,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Load",IDC_BUTTON6,122,239,27,13 + PUSHBUTTON "Save",IDC_BUTTON8,154,239,27,13 + GROUPBOX "",IDC_STATIC,11,255,177,49 + CTEXT "Parameter",IDC_STATIC,16,266,43,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO6,61,266,122,162,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "ValSld",IDC_SLIDER9,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,16,283,102,14 + EDITTEXT IDC_EDIT14,121,283,62,14,ES_AUTOHSCROLL + GROUPBOX "Mix Settings",IDC_STATIC,193,226,214,78 + CONTROL "&Master",IDC_CHECK9,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,199,242,46,10 + CTEXT "Mix Mode",IDC_STATIC,245,241,37,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO9,285,241,62,99,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + EDITTEXT IDC_EDIT16,349,241,52,12,ES_AUTOHSCROLL | ES_READONLY + CONTROL "",IDC_SPIN10,"msctls_updown32",UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | WS_GROUP | WS_TABSTOP,390,234,11,11 + CONTROL "&Bypass",IDC_CHECK10,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,199,256,39,10 + CTEXT "Output to",IDC_STATIC,245,261,37,13,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO7,285,261,117,68,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "E&xpand",IDC_CHECK12,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,199,271,37,10 + CONTROL "&Dry Mix",IDC_CHECK11,"Button",BS_AUTOCHECKBOX | BS_FLAT | WS_TABSTOP,199,286,37,10 + CTEXT "Dry/Wet Ratio",IDC_STATIC8,245,282,65,14,SS_CENTERIMAGE,WS_EX_STATICEDGE + CONTROL "",IDC_SLIDER10,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,314,282,88,14 +END + +IDD_EDIT_FIND DIALOGEX 0, 0, 214, 162 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_CAPTION +CAPTION "Find" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "Search for:",IDC_STATIC,4,4,206,110 + CONTROL "&Note",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,18,60,10 + COMBOBOX IDC_COMBO1,12,30,59,94,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Instrument",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,84,18,114,10 + COMBOBOX IDC_COMBO2,84,30,114,103,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Volume Effect",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,48,114,10 + COMBOBOX IDC_COMBO7,12,60,114,77,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO3,12,60,114,77,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CONTROL "V&olume Data",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,138,48,60,10 + COMBOBOX IDC_COMBO4,138,60,60,77,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + CONTROL "&Effect",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,78,114,10 + COMBOBOX IDC_COMBO5,12,90,113,71,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CONTROL "E&ffect Data",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,138,78,60,10 + COMBOBOX IDC_COMBO6,138,90,60,68,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + CONTROL "Find in current &pattern",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,6,120,108,8 + CONTROL "Find in current &selection",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,6,132,108,8 + CONTROL "Find in the &whole song",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,6,144,108,8 + CONTROL "Only in &channels",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,114,120,76,10 + EDITTEXT IDC_EDIT1,126,132,30,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,150,138,10,14 + CTEXT "&to",IDC_STATIC,164,134,8,8 + EDITTEXT IDC_EDIT2,176,132,30,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,198,138,11,14 +END + +IDD_EDIT_REPLACE DIALOGEX 0, 0, 214, 149 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD | WS_CAPTION +CAPTION "Replace" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "",IDC_STATIC,4,4,206,140 + CONTROL "&Replace By:",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,15,63,10 + CONTROL "&Note",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,30,60,10 + COMBOBOX IDC_COMBO1,12,42,59,93,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Instrument",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,84,30,114,10 + COMBOBOX IDC_COMBO2,84,42,114,92,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Volume Effect",IDC_CHECK3,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,13,60,113,10 + COMBOBOX IDC_COMBO7,12,72,114,64,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO3,12,72,114,64,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CONTROL "V&olume Data",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,138,60,60,10 + COMBOBOX IDC_COMBO4,138,72,60,64,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + CONTROL "&Effect",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,90,114,10 + COMBOBOX IDC_COMBO5,12,102,114,70,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + CONTROL "E&ffect Data",IDC_CHECK6,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,138,90,60,10 + COMBOBOX IDC_COMBO6,138,102,60,80,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + CONTROL "Replace &All (No confirmation)",IDC_CHECK8,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,126,186,10 +END + +IDD_PATTERN_PROPERTIES DIALOGEX 0, 0, 191, 149 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Pattern Properties" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + LTEXT "Rows:",IDC_STATIC,6,6,108,8 + COMBOBOX IDC_COMBO1,6,18,48,93,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "&/2",IDC_BUTTON_HALF,60,18,18,12 + PUSHBUTTON "&x2",IDC_BUTTON_DOUBLE,84,18,18,12 + LTEXT "Remove rows from / Add rows at:",IDC_STATIC,6,36,126,8 + CONTROL "&Top of Pattern",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,6,48,84,12 + CONTROL "&Bottom of Pattern",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,90,48,84,12 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDFRAME,6,66,174,1 + CONTROL "Override &song signature:",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,72,108,12 + PUSHBUTTON "Configure S&wing",IDC_BUTTON1,114,72,66,12 + LTEXT "Rows per beat",IDC_STATIC,54,92,126,8 + EDITTEXT IDC_ROWSPERBEAT,18,90,30,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Rows per measure",IDC_STATIC,54,110,126,8 + EDITTEXT IDC_ROWSPERMEASURE,18,108,30,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDFRAME,6,126,174,1 + LTEXT "Pattern Info",IDC_TEXT1,6,132,174,14 + DEFPUSHBUTTON "&OK",IDOK,132,6,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,132,24,50,14 +END + +IDD_PATTERN_EDITCOMMAND DIALOGEX 0, 0, 304, 95 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_TOOLWINDOW +CAPTION "Note Properties" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + LTEXT "Note:",IDC_STATIC,6,6,72,8 + COMBOBOX IDC_COMBO1,6,18,132,66,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Instrument:",IDC_STATIC,144,6,150,8 + COMBOBOX IDC_COMBO2,144,18,149,70,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Volume Command:",IDC_STATIC,6,36,71,8 + COMBOBOX IDC_COMBO3,6,48,132,65,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Value:",IDC_TEXT2,144,36,150,8 + CONTROL "",IDC_SLIDER1,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,144,48,150,12 + LTEXT "Effect Type:",IDC_STATIC,6,66,55,8 + COMBOBOX IDC_COMBO4,6,78,132,83,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO5,6,78,132,83,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Value:",IDC_TEXT1,144,66,150,8 + CONTROL "",IDC_SLIDER2,"msctls_trackbar32",TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,144,78,150,12 +END + +IDD_SAMPLE_AMPLIFY DIALOGEX 0, 0, 208, 95 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Amplify" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "&Amplify by",IDC_STATIC,13,21,37,8 + EDITTEXT IDC_EDIT1,84,18,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "Spin1",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,119,19,11,14 + LTEXT "%",IDC_STATIC,127,21,11,8 + CONTROL "Fade &In from",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,38,72,10 + EDITTEXT IDC_EDIT2,84,36,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,119,38,11,14 + LTEXT "%",IDC_STATIC,127,39,11,8 + CONTROL "Fade O&ut to",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,56,72,10 + EDITTEXT IDC_EDIT3,84,54,40,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,119,55,11,14 + LTEXT "%",IDC_STATIC,127,57,11,8 + LTEXT "&Fade Curve:",IDC_STATIC,12,74,48,8 + CONTROL "",IDC_COMBO1,"ComboBoxEx32",CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP,60,72,78,78 + DEFPUSHBUTTON "&OK",IDOK,150,6,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,150,24,50,14 + GROUPBOX "",IDC_STATIC,6,6,138,84 +END + +IDD_INFO_BOX DIALOGEX 0, 0, 273, 170 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Close",IDOK,216,152,50,14 + EDITTEXT IDC_EDIT1,6,6,260,143,ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER | WS_VSCROLL | WS_HSCROLL | NOT WS_TABSTOP,WS_EX_STATICEDGE +END + +IDD_OPTIONS_GENERAL DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "General" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Default &Artist Name:",IDC_STATIC,12,20,78,8 + EDITTEXT IDC_EDIT1,90,18,186,12,ES_AUTOHSCROLL + LTEXT "Newly created files default to:",IDC_STATIC,12,36,264,12 + CONTROL "&Fixed format:",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,12,49,59,10 + COMBOBOX IDC_COMBO1,90,48,84,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Same format as currently open file",IDC_RADIO2,"Button",BS_AUTORADIOBUTTON,12,66,264,10 + CONTROL "Fixed &template:",IDC_RADIO3,"Button",BS_AUTORADIOBUTTON,12,85,66,10 + COMBOBOX IDC_COMBO2,90,84,168,12,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "...",IDC_BUTTON1,264,84,12,11 + LISTBOX IDC_LIST1,9,126,147,144,LBS_OWNERDRAWFIXED | LBS_HASSTRINGS | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + LTEXT "Description",IDC_TEXT1,162,126,111,138 + GROUPBOX "Miscellaneous Options",IDC_STATIC,4,114,278,162 + GROUPBOX "Defaults",IDC_STATIC,6,6,276,96 +END + +IDD_OPTIONS_SOUNDCARD DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Sound Card" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + GROUPBOX "&Device",IDC_STATIC,6,6,276,156 + CONTROL "",IDC_COMBO1,"ComboBoxEx32",CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP,12,18,264,96 + PUSHBUTTON "&Setup device ...",IDC_BUTTON2,24,36,72,12 + CONTROL "Show deprecated devices",IDC_CHECK_SOUNDCARD_SHOWALL, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,102,36,96,12 + PUSHBUTTON "&Rescan device list",IDC_BUTTON1,204,36,72,12 + LTEXT "&Latency:",IDC_STATIC_LATENCY,12,54,30,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO2,48,54,54,83,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + LTEXT "&Period:",IDC_STATIC_UPDATEINTERVAL,12,72,30,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_UPDATEINTERVAL,48,72,54,83,CBS_DROPDOWN | WS_VSCROLL | WS_TABSTOP + LTEXT "&Format:",IDC_STATIC_FORMAT,12,90,30,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO3,48,90,54,90,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO5,108,90,42,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO6,156,90,54,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO10,216,90,60,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Use device &exclusively",IDC_CHECK4,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,54,90,12 + CONTROL "&Boost thread priority",IDC_CHECK5,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,108,72,90,12 + CONTROL "&Hardware timing",IDC_CHECK9,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,204,72,72,12 + LTEXT "&Mapping:",IDC_STATIC_CHANNELMAPPING,12,108,30,12,SS_CENTERIMAGE + CTEXT "Front",IDC_STATIC_CHANNEL_FRONT,48,108,24,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO_CHANNEL_FRONTLEFT,78,108,96,72,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_CHANNEL_FRONTRIGHT,180,108,96,72,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CTEXT "Rear",IDC_STATIC_CHANNEL_REAR,48,126,24,12,SS_CENTERIMAGE,WS_EX_STATICEDGE + COMBOBOX IDC_COMBO_CHANNEL_REARLEFT,78,126,96,72,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_CHANNEL_REARRIGHT,180,126,96,72,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Record:",IDC_STATIC_RECORDING,12,144,36,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_RECORDING_CHANNELS,48,144,42,12,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO_RECORDING_SOURCE,96,144,180,72,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "General",IDC_STATIC,6,186,276,30 + LTEXT "&When playback is stopped:",IDC_STATIC,12,198,90,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO11,108,198,60,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "&Open device at startup",IDC_CHECK7,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,186,198,90,12 + GROUPBOX "Stat&istics",IDC_STATIC,6,222,276,55 + EDITTEXT IDC_EDIT_STATISTICS,12,233,264,38,ES_MULTILINE | ES_AUTOHSCROLL | ES_READONLY | NOT WS_TABSTOP +END + +IDD_MIDIMACRO DIALOGEX 0, 0, 359, 335 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Zxx Macro Configuration" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,300,6,50,14 + PUSHBUTTON "Cancel",IDCANCEL,300,24,50,14 + PUSHBUTTON "Set as default",IDC_BUTTON1,300,42,50,14 + PUSHBUTTON "Reset",IDC_BUTTON2,300,60,50,14 + GROUPBOX "Parametered Macro (Z00-Z7F)",IDC_STATIC,6,6,288,272 + LTEXT "Modify macro:",IDC_STATIC,13,221,47,8 + COMBOBOX IDC_COMBO1,13,231,48,103,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Type:",IDC_STATIC,64,221,47,8 + COMBOBOX IDC_COMBO2,64,231,111,79,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Value:",IDC_STATIC,177,221,47,8 + EDITTEXT IDC_EDIT1,177,231,110,12,ES_AUTOHSCROLL + GROUPBOX "Generate Macro:",IDC_STATIC,13,246,274,28 + LTEXT "Plugin/Param:",IDC_GENMACROLABEL,19,259,41,8 + COMBOBOX IDC_MACROPLUG,66,257,114,111,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_MACROPARAM,186,257,97,126,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Fixed Macros (Z80-ZFF)",IDC_STATIC,6,282,288,50 + RTEXT "Preset:",IDC_STATIC,12,298,37,8 + COMBOBOX IDC_COMBO3,54,296,173,65,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + RTEXT "Customize:",IDC_STATIC,12,316,36,8 + COMBOBOX IDC_COMBO4,54,314,48,101,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + RTEXT "Value:",IDC_STATIC,114,316,24,8 + EDITTEXT IDC_EDIT2,144,314,116,12,ES_AUTOHSCROLL + COMBOBOX IDC_MACROCC,66,257,114,111,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "Macro Help",IDC_BUTTON3,300,84,50,14 +END + +IDD_CHORDEDIT DIALOGEX 0, 0, 321, 122 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Chord Editor" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "Close",IDOK,264,102,50,14 + CONTROL "",IDC_KEYBOARD1,"Static",SS_WHITERECT | SS_NOTIFY,6,36,306,56,WS_EX_CLIENTEDGE + COMBOBOX IDC_COMBO1,6,18,52,114,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Shortcut Key:",IDC_STATIC,6,6,57,8 + LTEXT "Base Key:",IDC_STATIC,78,6,40,8 + COMBOBOX IDC_COMBO2,78,18,52,89,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO3,138,19,52,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Note #1",IDC_STATIC,138,6,39,8 + COMBOBOX IDC_COMBO4,198,19,52,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Note #2",IDC_STATIC,198,6,39,8 + COMBOBOX IDC_COMBO5,258,19,52,80,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Note #3",IDC_STATIC,258,6,39,8 + LTEXT "In ""Relative"" Base Key mode, all notes are relative to a previously entered note in the pattern cell where the chord is entered.",IDC_STATIC,6,102,252,18 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDVERT,66,6,1,25 +END + +IDD_SPLASHSCREEN DIALOGEX 0, 0, 266, 188 +STYLE DS_SETFONT | DS_SETFOREGROUND | DS_FIXEDSYS | DS_CENTER | WS_POPUP +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN +END + +IDD_TREEVIEW DIALOG 0, 0, 85, 266 +STYLE DS_SETFONT | WS_CHILD +FONT 8, "MS Shell Dlg" +BEGIN + CONTROL "Tree1",IDC_TREEVIEW,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | WS_BORDER,0,0,85,131 + CONTROL "Tree1",IDC_TREEDATA,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | WS_BORDER,0,135,85,131 +END + +IDD_MOD2MIDI DIALOGEX 0, 0, 257, 98 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "MIDI Conversion Setup" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + DEFPUSHBUTTON "OK",IDOK,198,6,50,14 + PUSHBUTTON "Cancel",IDCANCEL,198,24,50,14 + GROUPBOX "",IDC_STATIC,6,6,184,72 + LTEXT "Instrument:",IDC_STATIC,12,18,67,8 + CONTROL "Spin1",IDC_SPIN1,"msctls_updown32",0x0,12,28,11,12 + COMBOBOX IDC_COMBO1,25,28,158,114,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "MIDI Channel:",IDC_STATIC,12,48,46,8 + COMBOBOX IDC_COMBO2,12,58,66,114,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "MIDI Program / Percussion:",IDC_STATIC,83,48,97,8 + COMBOBOX IDC_COMBO3,83,58,100,114,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Allow instruments to overlap on same MIDI channel",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,6,84,246,10 +END + +IDD_SAVEPRESET DIALOGEX 0, 0, 154, 58 +STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Save EQ Preset" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Save As:",IDC_STATIC,6,6,51,8 + EDITTEXT IDC_EDIT1,6,18,69,12,ES_AUTOHSCROLL + COMBOBOX IDC_COMBO1,6,36,70,68,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + DEFPUSHBUTTON "OK",IDOK,96,6,50,14 + PUSHBUTTON "Cancel",IDCANCEL,96,24,50,14 +END + +IDD_EDITSAMPLEMAP DIALOGEX 0, 0, 287, 111 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Sample Map" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,233,4,50,14 + PUSHBUTTON "Cancel",IDCANCEL,233,22,50,14 + GROUPBOX "Samples:",IDC_STATIC,4,4,222,31 + COMBOBOX IDC_COMBO1,10,17,135,84,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Show all samples",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,150,19,71,10 + CONTROL "",IDC_KEYBOARD1,"Static",SS_WHITERECT | SS_NOTIFY,4,41,279,45,WS_EX_CLIENTEDGE + CONTROL "Slider1",IDC_SLIDER1,"msctls_trackbar32",TBS_AUTOTICKS | TBS_TOP | WS_TABSTOP,4,90,100,21 + LTEXT "Octaves",IDC_TEXT1,110,94,58,8 + RTEXT "--",IDC_TEXT2,261,89,22,8 +END + +IDD_SELECTMIXPLUGIN DIALOGEX 0, 0, 253, 285 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +CAPTION "Plugin Manager" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + LTEXT "&Filter:",IDC_STATIC_VSTNAMEFILTER,6,9,18,8 + EDITTEXT IDC_NAMEFILTER,30,6,150,14,ES_AUTOHSCROLL + DEFPUSHBUTTON "&Add to Song",IDOK,186,6,60,14 + PUSHBUTTON "&Cancel",IDCANCEL,186,24,60,14 + CONTROL "Tree1",IDC_TREE1,"SysTreeView32",TVS_DISABLEDRAGDROP | TVS_SHOWSELALWAYS | WS_HSCROLL | WS_TABSTOP,6,24,174,157,WS_EX_CLIENTEDGE + GROUPBOX "Plugin Settings",IDC_PLUGFRAME,6,185,240,91 + LTEXT "&Tags:",IDC_STATIC_PLUGINTAGS,13,201,19,8 + EDITTEXT IDC_PLUGINTAGS,36,198,204,14,ES_AUTOHSCROLL + CONTROL "Use Plugin &Bridge",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,216,72,12 + CONTROL "Share Bridge between all &Instances",IDC_CHECK2,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,84,216,156,12 + CONTROL "Increase &compatibility for broken Plugins",IDC_CHECK3, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,228,228,12 + EDITTEXT IDC_TEXT_CURRENT_VSTPLUG,12,246,227,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + EDITTEXT IDC_VENDOR,12,258,227,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER + PUSHBUTTON "&New Plugin...",IDC_BUTTON1,186,131,60,14,BS_MULTILINE + PUSHBUTTON "&Scan Folder...",IDC_BUTTON3,186,149,60,14,BS_MULTILINE + PUSHBUTTON "&Remove",IDC_BUTTON2,186,167,60,14 +END + +IDD_PLUGINEDITOR DIALOGEX 0, 0, 485, 153 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CLIENTEDGE +CAPTION "Editor" +MENU IDR_VSTMENU +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN +END + +IDD_EFFECTVISUALIZER DIALOGEX 0, 0, 422, 109 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME +EXSTYLE WS_EX_TOOLWINDOW | WS_EX_CLIENTEDGE +CAPTION "Parameter Editor" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + EDITTEXT IDC_VISSTATUS,4,92,162,12,ES_AUTOHSCROLL | ES_READONLY | NOT WS_BORDER | NOT WS_TABSTOP,WS_EX_STATICEDGE + COMBOBOX IDC_VISEFFECTLIST,299,89,85,158,CBS_DROPDOWNLIST | CBS_SORT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_VISACTION,221,90,76,158,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "",IDC_RENDERZONE,4,3,414,84,NOT WS_VISIBLE +END + +IDD_PITCHSHIFT DIALOGEX 0, 0, 181, 124 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Time-Stretch Ratio Calculator" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + LTEXT "Original:",IDC_STATIC,12,19,26,8 + EDITTEXT IDC_SAMPLE_LENGTH_ORIGINAL,42,16,40,14,ES_AUTOHSCROLL | ES_READONLY | ES_NUMBER + LTEXT "New:",IDC_STATIC,12,35,18,8 + EDITTEXT IDC_SAMPLE_LENGTH_NEW,42,32,40,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Original:",IDC_STATIC,101,19,26,8 + EDITTEXT IDC_MS_LENGTH_ORIGINAL2,131,16,40,14,ES_AUTOHSCROLL | ES_READONLY | ES_NUMBER + LTEXT "New:",IDC_STATIC,101,35,18,8 + EDITTEXT IDC_MS_LENGTH_NEW,131,32,40,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "S&peed:",IDC_STATIC,12,68,24,8 + EDITTEXT IDC_SPEED,55,65,27,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "&Tempo:",IDC_STATIC,12,84,25,8 + EDITTEXT IDC_TEMPO,55,81,27,14,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Original:",IDC_STATIC,101,68,26,8 + EDITTEXT IDC_ROW_LENGTH_ORIGINAL,131,65,40,14,ES_AUTOHSCROLL | ES_READONLY + LTEXT "&New:",IDC_STATIC,101,84,18,8 + EDITTEXT IDC_ROW_LENGTH_NEW2,131,81,40,14,ES_AUTOHSCROLL + LTEXT "&Ratio:",IDC_STATIC,12,109,20,8 + EDITTEXT IDC_PSRATIO,42,106,33,14,ES_AUTOHSCROLL + LTEXT "%",IDC_STATIC,78,109,8,8 + DEFPUSHBUTTON "&OK",IDOK,127,106,50,14 + GROUPBOX "Length in &Samples",IDC_STATIC,6,5,82,45 + GROUPBOX "Length in &ms at Mid-C",IDC_STATIC,95,5,82,45 + GROUPBOX "Length in Rows at Middle-C",IDC_STATIC,6,55,171,45 +END + +IDD_OPTIONS_AUTOSAVE DIALOGEX 0, 0, 286, 279 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Paths / Auto Save" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + RTEXT "Songs:",IDC_STATIC_MODDIR,12,18,48,12,SS_CENTERIMAGE + EDITTEXT IDC_OPTIONS_DIR_MODS,66,18,192,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_BUTTON_CHANGE_MODDIR,264,18,12,11 + RTEXT "Samples:",IDC_STATIC_SAMPDIR,12,36,48,12,SS_CENTERIMAGE + EDITTEXT IDC_OPTIONS_DIR_SAMPS,66,36,192,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_BUTTON_CHANGE_SAMPDIR,264,36,12,11 + RTEXT "Instruments:",IDC_STATIC_INSTRDIR,12,54,48,12,SS_CENTERIMAGE + EDITTEXT IDC_OPTIONS_DIR_INSTS,66,54,192,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_BUTTON_CHANGE_INSTRDIR,264,54,12,11 + RTEXT "VST Plugins:",IDC_STATIC_VSTDIR,12,72,48,12,SS_CENTERIMAGE + EDITTEXT IDC_OPTIONS_DIR_VSTS,66,72,192,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_BUTTON_CHANGE_VSTDIR,264,72,12,11 + RTEXT "VST Presets:",IDC_STATIC_VSTPRESETDIR,12,90,48,12,SS_CENTERIMAGE + EDITTEXT IDC_OPTIONS_DIR_VSTPRESETS,66,90,192,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_BUTTON_CHANGE_VSTPRESETSDIR,264,90,12,11 + CONTROL "Create &backup copy (*.bak) when saving module files",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,135,258,12 + CONTROL "&Enable Auto Save",IDC_AUTOSAVE_ENABLE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,18,154,237,10 + LTEXT "&Save every:",IDC_STATIC,18,168,40,12,SS_CENTERIMAGE + EDITTEXT IDC_AUTOSAVE_INTERVAL,62,168,31,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,72,168,10,14 + LTEXT "minutes",IDC_STATIC,98,168,157,12,SS_CENTERIMAGE + LTEXT "&Keep up to:",IDC_STATIC,18,185,36,12,SS_CENTERIMAGE + EDITTEXT IDC_AUTOSAVE_HISTORY,62,185,31,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,72,186,10,14 + LTEXT "backups of each file name",IDC_STATIC,98,185,157,12,SS_CENTERIMAGE + CONTROL "Store auto save files in song's &original directory.",IDC_AUTOSAVE_USEORIGDIR, + "Button",BS_AUTORADIOBUTTON,18,202,218,10 + CONTROL "S&tore in:",IDC_AUTOSAVE_USECUSTOMDIR,"Button",BS_AUTORADIOBUTTON,18,218,43,10 + EDITTEXT IDC_AUTOSAVE_PATH,72,216,186,12,ES_AUTOHSCROLL + PUSHBUTTON "...",IDC_AUTOSAVE_BROWSE,264,216,12,11 + LTEXT "Filename:",IDC_STATIC,30,240,36,12,SS_CENTERIMAGE + LTEXT "[filename].AutoSave.[timestamp].[extension]",IDC_STATIC,72,240,186,12,SS_CENTERIMAGE,WS_EX_CLIENTEDGE + LTEXT "(example: mySong.AutoSave.20050327.2343.it)",IDC_STATIC,72,258,186,12,SS_CENTERIMAGE + GROUPBOX "Backup and Auto Save",IDC_STATIC,6,120,278,157 + GROUPBOX "Default Directories",IDC_STATIC,6,6,276,102 +END + +IDD_EDIT_GOTO DIALOGEX 0, 0, 123, 122 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Go to..." +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + RTEXT "&Row:",IDC_STATIC,6,8,33,8 + EDITTEXT IDC_GOTO_ROW,42,6,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN1,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,84,6,11,14 + RTEXT "C&hannel:",IDC_STATIC,6,27,33,8 + EDITTEXT IDC_GOTO_CHAN,42,25,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,84,24,11,14 + RTEXT "&Pattern:",IDC_STATIC,6,44,33,8 + EDITTEXT IDC_GOTO_PAT,42,42,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,84,42,11,14 + RTEXT "&Order:",IDC_STATIC,6,62,33,8 + EDITTEXT IDC_GOTO_ORD,42,60,42,12,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN4,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,84,60,11,14 + RTEXT "&Time:",IDC_STATIC,6,80,33,8 + EDITTEXT IDC_EDIT5,42,78,22,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "mn",IDC_STATIC,66,80,10,8 + EDITTEXT IDC_EDIT6,78,78,22,12,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "sec",IDC_STATIC,102,80,12,8 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDHORZ,6,96,108,1 + DEFPUSHBUTTON "&OK",IDOK,72,102,44,14 + PUSHBUTTON "&Cancel",IDCANCEL,24,102,44,14 +END + +IDD_ADDSILENCE DIALOGEX 0, 0, 195, 93 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Add Silence / Resize" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "&OK",IDOK,138,12,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,138,30,50,14 + GROUPBOX "",IDC_STATIC,6,6,126,78 + EDITTEXT IDC_EDIT_ADDSILENCE,12,18,60,14,ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN_ADDSILENCE,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_ARROWKEYS | UDS_NOTHOUSANDS,60,18,11,14 + COMBOBOX IDC_COMBO1,78,18,48,36,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Add at &beginning of sample",IDC_RADIO_ADDSILENCE_BEGIN, + "Button",BS_AUTORADIOBUTTON | WS_GROUP | WS_TABSTOP,12,36,114,8 + CONTROL "Add at &end of sample",IDC_RADIO_ADDSILENCE_END,"Button",BS_AUTORADIOBUTTON | WS_TABSTOP,12,48,114,8 + CONTROL "&Resize to",IDC_RADIO_RESIZETO,"Button",BS_AUTORADIOBUTTON,12,60,114,8 + CONTROL "OPL &Instrument",IDC_RADIO1,"Button",BS_AUTORADIOBUTTON,12,72,108,8 +END + +IDD_OPTIONS_MIXER DIALOGEX 0, 0, 286, 281 +STYLE DS_SETFONT | DS_3DLOOK | DS_FIXEDSYS | WS_CHILD | WS_DISABLED | WS_CAPTION +CAPTION "Mixer" +FONT 8, "MS Shell Dlg", 400, 0, 0x0 +BEGIN + GROUPBOX "Resampling",IDC_STATIC,6,6,276,48 + LTEXT "&Filter:",IDC_STATIC,12,18,24,12,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_FILTER,36,18,102,56,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Use &Amiga resampler for Amiga modules",IDC_CHECK1, + "Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,36,144,12 + COMBOBOX IDC_COMBO_AMIGA_TYPE,162,36,102,56,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + GROUPBOX "Volume Ramping",IDC_STATIC,6,60,276,72 + EDITTEXT IDC_RAMPING_IN,12,72,36,12,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN2,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,42,66,11,14 + LTEXT "µs &up",IDC_STATIC,54,72,22,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_VOLRAMP_SAMPLES_UP,90,72,90,12,ES_AUTOHSCROLL | ES_READONLY + EDITTEXT IDC_RAMPING_OUT,12,90,36,12,ES_RIGHT | ES_AUTOHSCROLL | ES_NUMBER + CONTROL "",IDC_SPIN3,"msctls_updown32",UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_AUTOBUDDY | UDS_NOTHOUSANDS,42,84,11,14 + LTEXT "µs &down",IDC_STATIC,54,92,28,11,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_VOLRAMP_SAMPLES_DOWN,90,90,90,12,ES_AUTOHSCROLL | ES_READONLY + PUSHBUTTON "&Reset to Defaults",IDC_BUTTON1,12,110,72,14 + GROUPBOX "Behaviour",IDC_STATIC,6,150,276,30 + LTEXT "&Stereo Separation:",IDC_STATIC,12,162,62,8 + CONTROL "",IDC_SLIDER_STEREOSEP,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_NOTICKS | WS_TABSTOP,78,162,54,12 + LTEXT "100%",IDC_TEXT_STEREOSEP,132,162,36,8 + GROUPBOX "Pre-1.17RC3 mixing levels",IDC_STATIC,6,186,276,72 + CONTROL "Soft &Panning",IDC_CHECK_SOFTPAN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,204,66,12 + LTEXT "Sample Pre-A&mp:",IDC_STATIC,102,204,60,12,SS_CENTERIMAGE + CONTROL "",IDC_SLIDER_PREAMP,"msctls_trackbar32",TBS_AUTOTICKS | TBS_BOTH | TBS_TOOLTIPS | WS_TABSTOP,168,198,96,24 + LTEXT "Warning: Only samples are affected by these settings! Modifying them will change the balance between samples and plugins in songs with mix levels set to 1.17RC2 or earlier in the Song Properties! ",IDC_STATIC,12,222,252,30 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_ABOUTBOX, DIALOG + BEGIN + RIGHTMARGIN, 334 + BOTTOMMARGIN, 261 + END + + IDD_OPTIONS_PLAYER, DIALOG + BEGIN + RIGHTMARGIN, 280 + VERTGUIDE, 50 + VERTGUIDE, 169 + VERTGUIDE, 256 + BOTTOMMARGIN, 280 + END + + IDD_WAVECONVERT, DIALOG + BEGIN + RIGHTMARGIN, 337 + BOTTOMMARGIN, 236 + END + + IDD_PROGRESS, DIALOG + BEGIN + RIGHTMARGIN, 186 + END + + IDD_OPTIONS_KEYBOARD, DIALOG + BEGIN + RIGHTMARGIN, 280 + TOPMARGIN, 5 + BOTTOMMARGIN, 278 + END + + IDD_OPTIONS_COLORS, DIALOG + BEGIN + RIGHTMARGIN, 261 + BOTTOMMARGIN, 202 + END + + IDD_OPTIONS_MIDI, DIALOG + BEGIN + RIGHTMARGIN, 254 + BOTTOMMARGIN, 278 + END + + IDD_LOADRAWSAMPLE, DIALOG + BEGIN + RIGHTMARGIN, 227 + BOTTOMMARGIN, 127 + END + + IDD_CONTROL_GLOBALS, DIALOG + BEGIN + RIGHTMARGIN, 374 + BOTTOMMARGIN, 118 + END + + IDD_CONTROL_COMMENTS, DIALOG + BEGIN + RIGHTMARGIN, 348 + END + + IDD_CONTROL_PATTERNS, DIALOG + BEGIN + RIGHTMARGIN, 548 + VERTGUIDE, 39 + VERTGUIDE, 134 + BOTTOMMARGIN, 76 + HORZGUIDE, 37 + HORZGUIDE, 61 + HORZGUIDE, 73 + END + + IDD_CONTROL_SAMPLES, DIALOG + BEGIN + LEFTMARGIN, 2 + RIGHTMARGIN, 536 + BOTTOMMARGIN, 104 + END + + IDD_CONTROL_INSTRUMENTS, DIALOG + BEGIN + RIGHTMARGIN, 500 + BOTTOMMARGIN, 165 + END + + IDD_MODDOC_MODTYPE, DIALOG + BEGIN + RIGHTMARGIN, 248 + BOTTOMMARGIN, 280 + END + + IDD_REMOVECHANNELS, DIALOG + BEGIN + RIGHTMARGIN, 163 + BOTTOMMARGIN, 219 + END + + IDD_VIEW_GLOBALS, DIALOG + BEGIN + BOTTOMMARGIN, 335 + END + + IDD_EDIT_FIND, DIALOG + BEGIN + RIGHTMARGIN, 176 + BOTTOMMARGIN, 146 + END + + IDD_EDIT_REPLACE, DIALOG + BEGIN + RIGHTMARGIN, 182 + BOTTOMMARGIN, 148 + END + + IDD_PATTERN_PROPERTIES, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 187 + TOPMARGIN, 4 + BOTTOMMARGIN, 145 + END + + IDD_PATTERN_EDITCOMMAND, DIALOG + BEGIN + RIGHTMARGIN, 244 + BOTTOMMARGIN, 47 + END + + IDD_SAMPLE_AMPLIFY, DIALOG + BEGIN + RIGHTMARGIN, 184 + BOTTOMMARGIN, 89 + END + + IDD_INFO_BOX, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 266 + TOPMARGIN, 7 + BOTTOMMARGIN, 166 + END + + IDD_OPTIONS_GENERAL, DIALOG + BEGIN + RIGHTMARGIN, 280 + BOTTOMMARGIN, 278 + END + + IDD_OPTIONS_SOUNDCARD, DIALOG + BEGIN + RIGHTMARGIN, 264 + BOTTOMMARGIN, 178 + END + + IDD_MIDIMACRO, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 355 + TOPMARGIN, 4 + BOTTOMMARGIN, 330 + END + + IDD_CHORDEDIT, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 317 + TOPMARGIN, 4 + BOTTOMMARGIN, 120 + END + + IDD_SPLASHSCREEN, DIALOG + BEGIN + END + + IDD_MOD2MIDI, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 253 + TOPMARGIN, 4 + BOTTOMMARGIN, 94 + END + + IDD_SAVEPRESET, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 147 + BOTTOMMARGIN, 52 + END + + IDD_EDITSAMPLEMAP, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 283 + TOPMARGIN, 4 + END + + IDD_SELECTMIXPLUGIN, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 246 + TOPMARGIN, 7 + BOTTOMMARGIN, 278 + END + + IDD_PLUGINEDITOR, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 481 + TOPMARGIN, 4 + BOTTOMMARGIN, 149 + END + + IDD_EFFECTVISUALIZER, DIALOG + BEGIN + LEFTMARGIN, 4 + RIGHTMARGIN, 418 + TOPMARGIN, 3 + BOTTOMMARGIN, 104 + END + + IDD_PITCHSHIFT, DIALOG + BEGIN + END + + IDD_OPTIONS_AUTOSAVE, DIALOG + BEGIN + RIGHTMARGIN, 280 + BOTTOMMARGIN, 276 + END + + IDD_EDIT_GOTO, DIALOG + BEGIN + LEFTMARGIN, 6 + RIGHTMARGIN, 116 + TOPMARGIN, 4 + BOTTOMMARGIN, 117 + END + + IDD_ADDSILENCE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 188 + TOPMARGIN, 7 + BOTTOMMARGIN, 86 + END + + IDD_OPTIONS_MIXER, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 279 + TOPMARGIN, 7 + BOTTOMMARGIN, 274 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDR_MAINFRAME ICON "res\\mptrack.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_MIDIMACRO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPTIONS_MIXER AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_WAVECONVERT AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_ABOUTBOX AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CONTROL_INSTRUMENTS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPTIONS_KEYBOARD AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_ADDSILENCE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_INFO_BOX AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 0, 100, 100 +END + +IDD_OPTIONS_MIDI AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CONTROL_GLOBALS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_CHORDEDIT AFX_DIALOG_LAYOUT +BEGIN + 0, + 100, 100, 0, 0, + 0, 0, 100, 100, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0, + 0, 100, 0, 0, + 0, 0, 0, 0 +END + +IDD_SELECTMIXPLUGIN AFX_DIALOG_LAYOUT +BEGIN + 0, + 0, 0, 0, 0, + 0, 0, 100, 0, + 100, 0, 0, 0, + 100, 0, 0, 0, + 0, 0, 100, 100, + 0, 100, 100, 0, + 0, 100, 0, 0, + 0, 100, 100, 0, + 0, 100, 0, 0, + 0, 100, 100, 0, + 0, 100, 100, 0, + 0, 100, 100, 0, + 0, 100, 100, 0, + 100, 100, 0, 0, + 100, 100, 0, 0, + 100, 100, 0, 0 +END + +IDD_CONTROL_SAMPLES AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_VIEW_GLOBALS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_OPTIONS_COLORS AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_EDIT_GOTO AFX_DIALOG_LAYOUT +BEGIN + 0 +END + +IDD_LOADRAWSAMPLE AFX_DIALOG_LAYOUT +BEGIN + 0 +END + + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""afxres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "#define _AFX_NO_OLE_RESOURCES\r\n" + "#define _AFX_NO_TRACKER_RESOURCES\r\n" + "#define _AFX_NO_PROPERTY_RESOURCES\r\n" + "\r\n" + "#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU)\r\n" + "#ifdef _WIN32\r\n" + "LANGUAGE 9, 1\r\n" + "#pragma code_page(1252)\r\n" + "#endif\r\n" + "#include ""res\\mptrack.rc2"" // non-Microsoft Visual C++ edited resources\r\n" + "#include ""afxres.rc"" // Standard components\r\n" + "#endif\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Bitmap +// + +IDB_PATTERNVIEW BITMAP "res\\view_pat.bmp" + +IDB_COLORSETUP BITMAP "res\\colors.bmp" + +IDB_VUMETERS BITMAP "res\\vumeters.bmp" + + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_MAINFRAME MENU +BEGIN + POPUP "&File" + BEGIN + POPUP "&New\tCtrl+N" + BEGIN + MENUITEM "&IT", ID_FILE_NEWIT + MENUITEM "&XM", ID_FILE_NEWXM + MENUITEM "&S3M", ID_FILE_NEWS3M + MENUITEM "M&OD", ID_FILE_NEWMOD + MENUITEM "Open&MPT Module", ID_NEW_MPT + END + MENUITEM "&Open...\tCtrl+O", ID_FILE_OPEN + MENUITEM "Open template", 65535 + MENUITEM "Appen&d Module...", ID_FILE_APPENDMODULE + MENUITEM "&Close", ID_FILE_CLOSE + MENUITEM "C&lose All", ID_FILE_CLOSEALL + MENUITEM "&Save\tCtrl+S", ID_FILE_SAVE + MENUITEM "Save &As...", ID_FILE_SAVE_AS + MENUITEM "Save Cop&y...", ID_FILE_SAVE_COPY + MENUITEM "Save as Template...", ID_FILE_SAVEASTEMPLATE + MENUITEM "Compatibility &Export...", ID_FILE_SAVECOMPAT + MENUITEM "Stream Export...", ID_FILE_SAVEASWAVE + MENUITEM "Export as M&IDI...", ID_FILE_SAVEMIDI + MENUITEM "Export O&PL Register Dump...", ID_FILE_SAVEOPL + MENUITEM SEPARATOR + MENUITEM "Import &MIDI Library...", ID_IMPORT_MIDILIB + MENUITEM "Add Sound &Bank...", ID_ADD_SOUNDBANK + MENUITEM SEPARATOR + MENUITEM "Recent File", ID_MRU_LIST_FIRST, GRAYED + MENUITEM SEPARATOR + MENUITEM "E&xit", ID_APP_EXIT + END + POPUP "P&layer" + BEGIN + MENUITEM "&Play\tF5", ID_PLAYER_PLAY + MENUITEM "P&lay from start\tF6", ID_PLAYER_PLAYFROMSTART + MENUITEM "&Stop\tESC", ID_PLAYER_STOP + MENUITEM "P&ause\tF8", ID_PLAYER_PAUSE + MENUITEM "&MIDI Record\tF9", ID_MIDI_RECORD + MENUITEM SEPARATOR + MENUITEM "&Estimate song length", ID_ESTIMATESONGLENGTH + MENUITEM "Approx. real &BPM", ID_APPROX_BPM + END + POPUP "&Edit" + BEGIN + MENUITEM "&Undo\tCtrl+Z", ID_EDIT_UNDO + MENUITEM "&Redo", ID_EDIT_REDO + MENUITEM SEPARATOR + MENUITEM "Cu&t\tCtrl+X", ID_EDIT_CUT + MENUITEM "&Copy\tCtrl+C", ID_EDIT_COPY + MENUITEM "&Paste\tCtrl+V", ID_EDIT_PASTE + POPUP "Paste &Special" + BEGIN + MENUITEM "&Mix Paste\tCtrl+M", ID_EDIT_PASTE_SPECIAL + MENUITEM "Mix Paste (IT Style)", ID_EDIT_MIXPASTE_ITSTYLE + MENUITEM "Paste &Flood", ID_EDIT_PASTEFLOOD + MENUITEM "&Push Forward Paste (Insert)", ID_EDIT_PUSHFORWARDPASTE + END + MENUITEM "Select &All\tCtrl+5", ID_EDIT_SELECT_ALL + MENUITEM SEPARATOR + MENUITEM "C&leanup...", ID_EDIT_CLEANUP + MENUITEM "Auto&matic Sample Trimmer", ID_EDIT_SAMPLETRIMMER + MENUITEM SEPARATOR + MENUITEM "&Find\tCtrl+F", ID_EDIT_FIND + MENUITEM "Find &Next\tF3", ID_EDIT_FINDNEXT + MENUITEM "&Goto", ID_EDIT_GOTO_MENU + MENUITEM "Split &Keyboard Settings", ID_EDIT_SPLITKEYBOARDSETTINGS + END + POPUP "&View" + BEGIN + MENUITEM "&Globals\tAlt+G", ID_VIEW_GLOBALS + MENUITEM "&Patterns\tAlt+P", ID_VIEW_PATTERNS + MENUITEM "&Samples\tAlt+S", ID_VIEW_SAMPLES + MENUITEM "&Instruments\tAlt+N", ID_VIEW_INSTRUMENTS + MENUITEM "&Comments\tShift+F9", ID_VIEW_COMMENTS + MENUITEM SEPARATOR + POPUP "&Toolbars" + BEGIN + MENUITEM "&Main", ID_VIEW_TOOLBAR + MENUITEM "&Tree", IDD_TREEVIEW + END + MENUITEM SEPARATOR + MENUITEM "S&etup...", ID_VIEW_OPTIONS + MENUITEM "Pl&ugin Manager", ID_PLUGIN_SETUP + MENUITEM "Ch&annel Manager", ID_CHANNEL_MANAGER + MENUITEM "Clipboard Manager", ID_CLIPBOARD_MANAGER + MENUITEM "Song P&roperties", ID_VIEW_SONGPROPERTIES + MENUITEM "&Zxx Macro Configuration", ID_PATTERN_MIDIMACRO + MENUITEM "&MIDI Mapping", ID_VIEW_MIDIMAPPING + MENUITEM "Edit &History", ID_VIEW_EDITHISTORY + MENUITEM "&Find MPT Hacks in Song", ID_VIEW_MPTHACKS + END + POPUP "&Window" + BEGIN + MENUITEM "&New Window", ID_WINDOW_NEW + MENUITEM SEPARATOR + MENUITEM "&Cascade", ID_WINDOW_CASCADE + MENUITEM "&Tile Horizontal", ID_WINDOW_TILE_HORZ + MENUITEM "Tile &Vertical", ID_WINDOW_TILE_VERT + MENUITEM "&Arrange Icons", ID_WINDOW_ARRANGE + MENUITEM "S&plit", ID_WINDOW_SPLIT + END + POPUP "&Help" + BEGIN + MENUITEM "&Help...", ID_HELPSHOW + MENUITEM "&Report a Bug", ID_REPORT_BUG + MENUITEM SEPARATOR + MENUITEM "&Example Modules", ID_EXAMPLE_MODULES + MENUITEM "Show Settings Folder", ID_HELP_SHOWSETTINGSFOLDER + MENUITEM SEPARATOR + MENUITEM "&OpenMPT Website", ID_NETLINK_MODPLUG + MENUITEM "&Web Resources", ID_NETLINK_TOP_PICKS + MENUITEM SEPARATOR + MENUITEM "Check for &Updates...", ID_INTERNETUPDATE + MENUITEM "&About OpenMPT", ID_APP_ABOUT + END +END + +IDR_TOOLBARS MENU +BEGIN + POPUP "&ToolBars" + BEGIN + MENUITEM "&Main", ID_VIEW_TOOLBAR + MENUITEM "&Tree", IDD_TREEVIEW + END +END + +IDR_ENVELOPES MENU +BEGIN + POPUP "&Envelope" + BEGIN + MENUITEM "&Loop", ID_ENVELOPE_SETLOOP + MENUITEM "&Sustain", ID_ENVELOPE_SUSTAIN + MENUITEM "C&arry envelope", ID_ENVELOPE_CARRY + MENUITEM "&Insert Point", ID_ENVELOPE_INSERTPOINT + MENUITEM "&Remove Point", ID_ENVELOPE_REMOVEPOINT + MENUITEM "&Toggle Release Node", ID_ENVELOPE_TOGGLERELEASENODE + MENUITEM SEPARATOR + MENUITEM "&Copy envelope", ID_EDIT_COPY + MENUITEM "&Paste envelope", ID_EDIT_PASTE + MENUITEM SEPARATOR + MENUITEM "Scale &Envelope Points...", ID_ENVELOPE_SCALEPOINTS + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Cursor +// + +IDC_DRAGGING CURSOR "res\\dragging.cur" + +IDC_NODROP CURSOR "res\\nodrop.cur" + +IDC_NODRAG CURSOR "res\\nodrag.cur" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDR_MAINFRAME "OpenMPT" + IDR_MODULETYPE "\nModule\nuntitled\nMusic Modules (*.mod;*.s3m;*.it;*.xm)\n.mod\nModplugTracker.Document\nMusic Module" + ID_PATTERN_CHANNELMANAGER + "Display Channel Manager window\nChannel Manager" + ID_INDICATOR_CPU "-" + ID_FILE_EXPORTCOMPAT "Export file to standard IT/XM." + ID_ENVELOPE_SETRELEASENODE "Set release node" +END + +STRINGTABLE +BEGIN + ID_ENVELOPE_VIEWGRID "Show/hide row guidelines" + ID_REPORT_BUG "Report a bug!\nReport a bug!" +END + +STRINGTABLE +BEGIN + AFX_IDS_APP_TITLE "OpenMPT" + AFX_IDS_IDLEMESSAGE "Ready" +END + +STRINGTABLE +BEGIN + IDS_ERR_FILEOPEN "Unable to open file." + IDS_ERR_FILETYPE "Unsupported file type" + IDS_ERR_SAVEINS "Unable to save instrument" + IDS_ERR_OUTOFMEMORY "Not enough memory" + IDS_ERR_TOOMANYINS "Too many instruments!" + IDS_ERR_SAVESONG "Unable to save song!" +END + +STRINGTABLE +BEGIN + IDS_ERR_TOOMANYPAT "Too many patterns!" + IDS_ERR_TOOMANYSMP "Too many samples!" + IDS_ERR_SAVESMP "Unable to save sample" +END + +STRINGTABLE +BEGIN + ID_INDICATOR_EXT "EXT" + ID_INDICATOR_CAPS "CAP" + ID_INDICATOR_NUM "NUM" + ID_INDICATOR_SCRL "SCRL" + ID_INDICATOR_OVR "OVR" + ID_INDICATOR_REC "REC" +END + +STRINGTABLE +BEGIN + ID_INDICATOR_TIME "00:00:00 [999] 999ch" + ID_INDICATOR_USER "Row 000, Col 000" + ID_INDICATOR_INFO "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" + ID_FILE_SAVECOMPAT "Creates a copy of the current module not using any of OpenMPT's IT / XM add-ons" + ID_INDICATOR_XINFO "Chn:999; Vol:FF; Mac:F; Cut:FF; Res:FF; Pan:FF-S" +END + +STRINGTABLE +BEGIN + ID_VIEW_MIDIMAPPING "Configure the MIDI Mapping" +END + +STRINGTABLE +BEGIN + ID_FILE_NEW "Create a new document\nNew" + ID_FILE_OPEN "Open an existing document\nOpen" + ID_FILE_CLOSE "Close the active document\nClose" + ID_FILE_SAVE "Save the active document\nSave" + ID_FILE_SAVE_AS "Save the active document with a new name\nSave As" +END + +STRINGTABLE +BEGIN + ID_APP_ABOUT "About OpenMPT\nAbout" + ID_APP_EXIT "Exit OpenMPT\nExit" + ID_HELP_INDEX "Opens Help\nHelp Topics" + ID_HELP_FINDER "List Help topics\nHelp Topics" + ID_HELP_USING "Display instructions about how to use help\nHelp" + ID_CONTEXT_HELP "Display help for buttons, menus and windows\nHelp" + ID_HELP "Display help for current task or command\nHelp" +END + +STRINGTABLE +BEGIN + ID_NEXT_PANE "Switch to the next window pane\nNext Pane" + ID_PREV_PANE "Switch back to the previous window pane\nPrevious Pane" +END + +STRINGTABLE +BEGIN + ID_WINDOW_NEW "Open another window for the active song\nNew Window" + ID_WINDOW_ARRANGE "Arrange icons at the bottom of the window\nArrange Icons" + ID_WINDOW_CASCADE "Arrange windows so they overlap\nCascade Windows" + ID_WINDOW_TILE_HORZ "Arrange windows as non-overlapping tiles\nTile Windows" + ID_WINDOW_TILE_VERT "Arrange windows as non-overlapping tiles\nTile Windows" + ID_WINDOW_SPLIT "Split the active window into panes\nSplit" +END + +STRINGTABLE +BEGIN + ID_EDIT_CLEAR "Erase the selection\nErase" + ID_EDIT_CLEAR_ALL "Erase everything\nErase All" + ID_EDIT_COPY "Copy the selection and put it on the Clipboard\nCopy" + ID_EDIT_CUT "Cut the selection and put it on the Clipboard\nCut" + ID_EDIT_FIND "Find the specified text\nFind" + ID_EDIT_PASTE "Insert Clipboard contents\nPaste" + ID_EDIT_PASTE_SPECIAL "Mix Clipboard contents\nPaste" + ID_EDIT_REPEAT "Repeat the last action\nRepeat" + ID_EDIT_REPLACE "Replace specific text with different text\nReplace" + ID_EDIT_SELECT_ALL "Select the entire document\nSelect All" + ID_EDIT_UNDO "Undo the last action\nUndo" + ID_EDIT_REDO "Redo the previously undone action\nRedo" +END + +STRINGTABLE +BEGIN + ID_VIEW_TOOLBAR "Show or hide the main toolbar\nToggle ToolBar" + ID_VIEW_STATUS_BAR "Show or hide the status bar\nToggle StatusBar" +END + +STRINGTABLE +BEGIN + AFX_IDS_SCSIZE "Change the window size" + AFX_IDS_SCMOVE "Change the window position" + AFX_IDS_SCMINIMIZE "Reduce the window to an icon" + AFX_IDS_SCMAXIMIZE "Enlarge the window to full size" + AFX_IDS_SCNEXTWINDOW "Switch to the next document window" + AFX_IDS_SCPREVWINDOW "Switch to the previous document window" + AFX_IDS_SCCLOSE "Close the active window and prompts to save the documents" +END + +STRINGTABLE +BEGIN + AFX_IDS_SCRESTORE "Restore the window to normal size" + AFX_IDS_SCTASKLIST "Activate Task List" + AFX_IDS_MDICHILD "Activate this window" +END + +STRINGTABLE +BEGIN + ID_FILE_NEWMOD "Create a new ProTracker (MOD) music module\nNew MOD" + ID_FILE_NEWXM "Create a new FastTracker II (XM) music module\nNew XM" + ID_FILE_NEWS3M "Create a new Scream Tracker (S3M) music module\nNew S3M" + ID_FILE_NEWIT "Create a new Impulse Tracker (IT) music module\nNew IT" + ID_FILE_SAVEASWAVE "Records the current file as a high-quality lossless or lossy file" + ID_PLAYER_PLAY "Resume playing, or play the song from the selected position\nPlay" + ID_PLAYER_PLAYFROMSTART "Play the song from the beginning\nPlay from start" + ID_PLAYER_STOP "Stop Playing\nStop" + ID_PLAYER_PAUSE "Pause\nPause" + ID_PLAYER_SETUP "Configure the sound device and player options" +END + +STRINGTABLE +BEGIN + ID_VIEW_OPTIONS "Configure the audio device, keyboard layout, colors etc...\nSetup" + ID_INSTRUMENTS_REMOVEALL "Remove all the instruments in the song." + ID_CLEANUP_INSTRUMENTS "Remove all unused instruments" + ID_CLEANUP_SAMPLES "Remove all unused samples" + ID_CLEANUP_PATTERNS "Remove unused empty patterns from the file" + ID_CLEANUP_SONG "Cleanup everything in the song (patterns, instruments and samples)\nCleanup Song" + ID_EDIT_FINDNEXT "Continue search\nFind Next" +END + +STRINGTABLE +BEGIN + IDC_COMBO_INSTRUMENT "Change the current instrument for entering notes\nActive Instrument" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_NEW "Create a new sample\nNew Sample" + IDC_SAMPLE_OPEN "Load sample from disk\nImport sample" + IDC_SAMPLE_SAVEAS "Save the current sample to disk\nSave sample" + IDC_SAMPLE_NORMALIZE "Normalize the current sample (full scale)\nNormalize (hold shift to normalize all samples)" + IDC_SAMPLE_AMPLIFY "Amplify selection\nAmplify" + IDC_SAMPLE_RESAMPLE "Resample the entire sample or current selection\nResample" + IDC_SAMPLE_REVERSE "Reverse selection\nReverse" + IDC_COMBO_ZOOM "Changes the current zoom value\nZoom" +END + +STRINGTABLE +BEGIN + IDC_PATTERN_PLAY "Play the curent pattern from the current position\nPlay Pattern" + IDC_PATTERN_PLAYFROMSTART + "Play the current pattern from the beginning\nReplay Pattern" + IDC_PATTERN_STOP "Stop playing\nStop" + IDC_PATTERN_RECORD "Enables the recording of notes\nRecord" + IDC_SAMPLE_PLAY "Play a middle C using the current sample\nPlay sample" + IDC_PATTERN_NEW "Create a new pattern\nInsert Pattern" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_DOWNSAMPLE "Shrink selection\nDownsample" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_SILENCE "Silence Sample\nSilence" + IDC_SAMPLE_INVERT "Invert phase\nInvert phase" + IDC_SAMPLE_SIGN_UNSIGN "Signed/Unsigned conversion\nSigned/Unsigned conversion" + IDC_INSTRUMENT_NEW "Create a new instrument\nNew instrument (hold shift to duplicate)" + IDC_INSTRUMENT_OPEN "Import instrument\nImport instrument" + IDC_INSTRUMENT_SAVEAS "Save the current instrument to disk\nSave instrument" + IDC_INSTRUMENT_PLAY "Play the current instrument\nPlay instrument" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_DCOFFSET "Remove DC Offset\nRemove DC Offset and normalize (hold shift to process all samples)" +END + +STRINGTABLE +BEGIN + ID_ENVELOPE_SETLOOP "Enable or disable the envelope loop" + ID_ENVELOPE_SUSTAIN "Enable or disable the envelope sustain" + ID_ENVELOPE_INSERTPOINT "Insert a new point in the envelope at the current location" + ID_ENVELOPE_REMOVEPOINT "Remove the selected point from the envelope" + ID_MODTREE_REFRESH "Refresh the display\nRefresh" +END + +STRINGTABLE +BEGIN + ID_PATTERN_PLAYROW "Play current row\nPlay Row" + ID_IMPORT_MIDILIB "Defines the default MIDI library used when importing MIDI files" + ID_CLEANUP_REARRANGE "Rearrange all patterns so that they are sorted in the order list\nRearrange Patterns" +END + +STRINGTABLE +BEGIN + ID_EDIT_GOTO_MENU "Go to row / channel / pattern / order" + ID_CLEANUP_COMPO "Reset attributes to defaults (useful for creating sample packs)\nCompo Cleanup" + ID_SAMPLE_DRAW "Toggle Sample Drawing" + ID_SAMPLE_ADDSILENCE "Add Silence / Create Sample" + ID_OVERFLOWPASTE "Toggle overflow paste\nToggle overflow paste" +END + +STRINGTABLE +BEGIN + ID_VIEW_COMMENTS "Activates the song text editor" + ID_ADD_SOUNDBANK "Add a new DLS sound bank" + ID_ORDERLIST_INSERT "Insert a pattern in the order list\nInsert Pattern" + ID_ORDERLIST_DELETE "Remove a pattern from the order list\nRemove Pattern" + ID_MIDI_RECORD "Record from an external MIDI keyboard\nMIDI Record" +END + +STRINGTABLE +BEGIN + ID_PATTERN_PROPERTIES "Pattern Properties\nPattern Properties" + ID_PATTERN_VUMETERS "Show or hide channel VU-Meters\nVU-Meters" + ID_ENVELOPE_CARRY "Enable or disable envelope carry" + ID_PATTERN_CHORDEDIT "Chord Editor\nChord Editor" + ID_PATTERN_MIDIMACRO "Zxx Macros\nZxx Macros" + ID_ENVSEL_VOLUME "Show Volume Envelope" + ID_ENVSEL_PANNING "Show Panning Envelope" +END + +STRINGTABLE +BEGIN + ID_ENVSEL_PITCH "Show Pitch/Filter Envelope" + ID_ENVELOPE_VOLUME "Enable or disable the volume envelope" + ID_ENVELOPE_PANNING "Enable or disable the panning envelope" + ID_ENVELOPE_PITCH "Enable or disable the pitch envelope" + ID_ENVELOPE_FILTER "Enable or disable the filter envelope" + ID_PATTERN_EXPAND "Expand pattern\nExpand Pattern" + ID_PATTERN_SHRINK "Shrink Pattern\nShrink Pattern" + ID_HELP_SEARCH "Displays the help index\nHelp Index" + ID_NETLINK_MODPLUG "Visit the OpenMPT website" + ID_NETLINK_TOP_PICKS "Visit our list of free web resources!" +END + +STRINGTABLE +BEGIN + IDD_TREEVIEW "Show or hide the tree view" +END + +STRINGTABLE +BEGIN + ID_SAMPLE_TRIM "Delete everything except the current selection\nTrim Sample" + ID_FILE_SAVEMIDI "Export the current song to a standard MIDI file" + ID_INSTRUMENT_SAMPLEMAP "Edit the sample map" + ID_SAMPLE_ZOOMUP "Zoom In" + ID_SAMPLE_ZOOMDOWN "Zoom Out" + ID_PATTERNDETAIL_LO "Low pattern detail level\nLow pattern detail level" + ID_PATTERNDETAIL_MED "Medium pattern detail level\nMedium pattern detail level" + ID_PATTERNDETAIL_HI "High pattern detail level\nHigh pattern detail level" + ID_PATTERN_AMPLIFY "Amplify selection\nAmplify" +END + +STRINGTABLE +BEGIN + ID_ESTIMATESONGLENGTH "Displays the approximate song length" +END + +STRINGTABLE +BEGIN + ID_VIEWPLUGNAMES "Displays the channel plugins in channel headers\nShow Plugins" +END + +STRINGTABLE +BEGIN + ID_EDIT_GOTO "Go to row, channel, pattern...\nGo to row" +END + +STRINGTABLE +BEGIN + IDS_ERR_TUNING_SERIALISATION "Static tuning serialisation failed" +END + +STRINGTABLE +BEGIN + IDS_UNABLE_TO_LOAD_KEYBINDINGS + "Loading keybindings failed. The keyboard won't work properly." + IDS_CANT_OPEN_FILE_FOR_WRITING "Can't open keybindings file for writing." + IDS_PATTERN_CLEANUP_UNAVAILABLE + "Removing unused patterns is not available when using multiple sequences." +END + +STRINGTABLE +BEGIN + ID_ENVELOPE_ZOOM_IN "Zoom In" + ID_ENVELOPE_ZOOM_OUT "Zoom Out" + ID_PANIC "Stop all plugin and sample voices\nStop all hanging plugin and sample voices" + ID_VIEW_EDITHISTORY "View the edit history of this module" + ID_SAMPLE_GRID "Configure Sample Grid" + ID_FILE_SAVEASTEMPLATE "Save the active document as template module\nSave as Template" +END + +STRINGTABLE +BEGIN + ID_UPDATE_AVAILABLE "A new OpenMPT version is available. Click the icon for details and installation.\nA new update is available." +END + +STRINGTABLE +BEGIN + ID_ENVELOPE_LOAD "Load instrument envelope from file" + ID_ENVELOPE_SAVE "Save instrument envelope to file" +END + +STRINGTABLE +BEGIN + ID_FILE_OPENTEMPLATE "Open a template document\nOpen template document" +END + +STRINGTABLE +BEGIN + ID_CHANNEL_MANAGER "Add, remove, mute and manage channels" + ID_PLUGIN_SETUP "Register plugins and add them to the current module" +END + +STRINGTABLE +BEGIN + ID_VIEW_SONGPROPERTIES "Edit global song properties or convert the module to another format" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_XFADE "Crossfade Loop Points\nCrossfade between loop start and loop end to create seamless sample loops." + IDC_SAMPLE_AUTOTUNE "Sample Tuner\nTune the sample to a given note." +END + +STRINGTABLE +BEGIN + IDC_VUMETER "Global VU Meter (Click to reset clip indicator)" +END + +STRINGTABLE +BEGIN + IDC_SAMPLE_STEREOSEPARATION + "Change Stereo Separation\nChange Stereo Separation / Stereo Width of the sample" +END + +STRINGTABLE +BEGIN + ID_FILE_SAVEOPL "Exports all the playback data generated by OPL instruments" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// English (United Kingdom) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENG) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_UK +#pragma code_page(1252) + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog +// + +IDD_MSGBOX_HIDABLE DIALOGEX 0, 0, 187, 71 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + DEFPUSHBUTTON "OK",IDOK,150,50,30,14 + PUSHBUTTON "Cancel",IDCANCEL,111,49,30,14,NOT WS_VISIBLE + CONTROL "Don't show this again.",IDC_DONTSHOWAGAIN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,7,54,90,10 + LTEXT "Static",IDC_MESSAGETEXT,7,7,173,40 +END + +IDD_MIDIPARAMCONTROL DIALOGEX 0, 0, 413, 247 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "MIDI Mapping" +FONT 8, "MS Shell Dlg", 0, 0, 0x1 +BEGIN + CONTROL "&Active",IDC_CHECKACTIVE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,12,18,48,10 + CONTROL "&Capture",IDC_CHECKCAPTURE,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,66,18,48,10 + CONTROL "Pattern &Record",IDC_CHECK_PATRECORD,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,114,18,65,10 + CONTROL "&MIDI Learn",IDC_CHECK_MIDILEARN,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,276,18,59,11 + LTEXT "C&hannel",IDC_STATIC,12,36,27,8 + COMBOBOX IDC_COMBO_CHANNEL,12,48,36,65,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Event",IDC_STATIC,60,36,102,8 + COMBOBOX IDC_COMBO_EVENT,60,48,102,73,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "C&ontroller",IDC_STATIC,174,36,150,8 + COMBOBOX IDC_COMBO_CONTROLLER,174,48,150,90,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Pl&ugin",IDC_STATIC,12,66,150,8 + COMBOBOX IDC_COMBO_PLUGIN,12,78,150,76,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "&Parameter",IDC_STATIC,174,66,150,8 + COMBOBOX IDC_COMBO_PARAM,174,78,150,83,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON "A&dd",IDC_BUTTON_ADD,342,18,45,12 + PUSHBUTTON "Rep&lace",IDC_BUTTON_REPLACE,342,36,45,12 + PUSHBUTTON "Remo&ve",IDC_BUTTON_REMOVE,342,54,45,12 + CONTROL "",IDC_LIST1,"SysListView32",LVS_REPORT | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_ALIGNLEFT | LVS_NOSORTHEADER | WS_BORDER | WS_TABSTOP,5,102,385,120 + CONTROL "",IDC_SPINMOVEMAPPING,"msctls_updown32",0x0,390,102,11,120 + DEFPUSHBUTTON "Close",IDOK,342,228,60,14 + GROUPBOX "Current Mapping",IDC_STATIC,5,5,391,91 +END + +IDD_TUNING DIALOGEX 0, 0, 537, 231 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_CONTROLPARENT +CAPTION "Tuning Properties" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + CONTROL "",IDC_TREE_TUNING,"SysTreeView32",TVS_HASBUTTONS | TVS_HASLINES | TVS_SHOWSELALWAYS | WS_BORDER | WS_HSCROLL | WS_TABSTOP,6,5,252,199 + PUSHBUTTON "New...",IDC_BUTTON_TUNING_NEW,6,210,48,14 + PUSHBUTTON "Load...",IDC_BUTTON_IMPORT,60,210,48,14 + PUSHBUTTON "Save As...",IDC_BUTTON_EXPORT,114,210,48,14 + PUSHBUTTON "Remove",IDC_BUTTON_TUNING_REMOVE,168,210,48,14 + GROUPBOX "Tuning",IDC_STATIC,264,6,264,198 + LTEXT "Name:",IDC_STATIC,270,20,24,10 + EDITTEXT IDC_EDIT_NAME,300,18,222,12,ES_AUTOHSCROLL | ES_READONLY + LTEXT "Tuning Type:",IDC_STATIC,270,36,46,14,SS_CENTERIMAGE + COMBOBOX IDC_COMBO_TTYPE,270,54,90,45,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + LTEXT "Group Size:",IDC_STATIC,270,80,60,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_STEPS,330,78,30,12,ES_AUTOHSCROLL | ES_READONLY + LTEXT "Group Ratio:",IDC_STATIC,270,97,60,9,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_RATIOPERIOD,330,96,30,12,ES_AUTOHSCROLL | ES_READONLY + LTEXT "Finetune Steps:",IDC_STATIC,270,116,60,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_FINETUNESTEPS,330,115,30,12,ES_AUTOHSCROLL + LTEXT "Multiply all ratios by",IDC_STATIC,276,150,66,12,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_MISC_ACTIONS,276,168,42,13,ES_AUTOHSCROLL + PUSHBUTTON "Apply",IDC_BUTTON_SETVALUES,324,168,36,12 + CONTROL "",IDC_STATICRATIOMAP,"Static",SS_GRAYRECT | SS_NOTIFY | WS_TABSTOP,366,48,156,132,WS_EX_CLIENTEDGE + CTEXT "Note",IDC_STATIC,372,36,30,8,SS_CENTERIMAGE + CTEXT "Name",IDC_STATIC,408,36,36,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_NOTENAME,408,186,30,12,ES_AUTOHSCROLL | ES_WANTRETURN + CTEXT "Ratio",IDC_STATIC,444,36,36,8,SS_CENTERIMAGE + EDITTEXT IDC_EDIT_RATIOVALUE,444,186,42,12,ES_AUTOHSCROLL | ES_WANTRETURN + CTEXT "Cents",IDC_STATIC,486,36,30,8,SS_CENTERIMAGE + DEFPUSHBUTTON "OK",IDOK,480,210,48,14 +END + +IDD_CHANNELMANAGER DIALOGEX 0, 0, 524, 116 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | DS_CENTER | WS_MINIMIZEBOX | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Channel Manager" +FONT 8, "MS Shell Dlg", 0, 0, 0x0 +BEGIN + CONTROL "",IDC_TAB1,"SysTabControl32",WS_TABSTOP,0,0,523,18,WS_EX_CLIENTEDGE + PUSHBUTTON "Apply",IDC_BUTTON1,395,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Close",IDC_BUTTON2,461,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Select all",IDC_BUTTON3,1,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Invert selection",IDC_BUTTON4,67,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Solo",IDC_BUTTON5,198,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Mute",IDC_BUTTON6,264,103,60,12,0,WS_EX_STATICEDGE + PUSHBUTTON "Store",IDC_BUTTON7,438,2,41,11,0,WS_EX_STATICEDGE + PUSHBUTTON "Restore",IDC_BUTTON8,480,2,42,11,0,WS_EX_STATICEDGE +END + +IDD_DEFAULTPLUGINEDITOR DIALOGEX 0, 0, 336, 247 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Editor" +FONT 8, "MS Shell Dlg", 400, 0, 0x0 +BEGIN + SCROLLBAR IDC_SCROLLBAR1,317,7,12,233,SBS_VERT | WS_DISABLED | WS_TABSTOP +END + +IDD_MOVEFXSLOT DIALOGEX 0, 0, 208, 58 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Move",IDC_STATIC1,6,6,192,8 + COMBOBOX IDC_COMBO1,6,18,48,172,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + CONTROL "Chain",IDC_CHECK1,"Button",BS_AUTOCHECKBOX | WS_TABSTOP,60,20,138,8 + DEFPUSHBUTTON "&OK",IDOK,150,36,50,14 + PUSHBUTTON "&Cancel",IDCANCEL,96,36,50,14 +END + +IDD_CONTROL_GRAPH DIALOGEX 0, 0, 424, 145 +STYLE DS_SETFONT | DS_FIXEDSYS | WS_CHILD +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + GROUPBOX "Node",IDC_STATIC,9,9,408,132 + COMBOBOX IDC_COMBO5,57,22,140,148,CBS_DROPDOWNLIST | WS_VSCROLL | WS_TABSTOP + PUSHBUTTON ">>",IDC_BUTTON4,36,22,18,13,NOT WS_TABSTOP,WS_EX_STATICEDGE + PUSHBUTTON "<<",IDC_BUTTON5,15,22,18,13,NOT WS_TABSTOP,WS_EX_STATICEDGE + GROUPBOX "Audio",IDC_STATIC,16,38,182,100 + COMBOBOX IDC_COMBO1,21,48,86,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + GROUPBOX "MIDI",IDC_STATIC,219,38,182,100 + LISTBOX IDC_LIST3,21,63,86,68,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO2,109,48,86,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LISTBOX IDC_LIST4,109,62,86,68,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO3,224,48,86,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LISTBOX IDC_LIST5,224,62,86,68,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP + COMBOBOX IDC_COMBO4,312,48,86,30,CBS_DROPDOWN | CBS_SORT | WS_VSCROLL | WS_TABSTOP + LISTBOX IDC_LIST6,312,62,86,68,LBS_SORT | LBS_NOINTEGRALHEIGHT | WS_VSCROLL | WS_TABSTOP +END + +IDD_SCALE_ENV_POINTS DIALOGEX 0, 0, 173, 93 +STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION | WS_SYSMENU +CAPTION "Scale Envelope Points" +FONT 8, "MS Shell Dlg", 400, 0, 0x1 +BEGIN + LTEXT "Scale envelope points (x axis) by:",IDC_STATIC,6,8,120,8 + EDITTEXT IDC_EDIT_FACTORX,126,6,36,13,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Scale envelope values (y axis) by:",IDC_STATIC,6,26,120,8 + EDITTEXT IDC_EDIT_FACTORY,126,24,36,13,ES_AUTOHSCROLL | ES_NUMBER + LTEXT "Offset envelope values (y axis) by:",IDC_STATIC,6,44,120,8 + EDITTEXT IDC_EDIT3,126,42,36,13,ES_AUTOHSCROLL | ES_NUMBER + DEFPUSHBUTTON "OK",IDOK,60,75,50,14 + PUSHBUTTON "Cancel",IDCANCEL,114,75,50,14 + CONTROL "",IDC_STATIC,"Static",SS_ETCHEDFRAME,6,66,156,1 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// DESIGNINFO +// + +#ifdef APSTUDIO_INVOKED +GUIDELINES DESIGNINFO +BEGIN + IDD_MSGBOX_HIDABLE, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 180 + TOPMARGIN, 7 + BOTTOMMARGIN, 64 + END + + IDD_MIDIPARAMCONTROL, DIALOG + BEGIN + RIGHTMARGIN, 409 + TOPMARGIN, 2 + BOTTOMMARGIN, 246 + END + + IDD_TUNING, DIALOG + BEGIN + RIGHTMARGIN, 524 + VERTGUIDE, 101 + VERTGUIDE, 104 + VERTGUIDE, 109 + VERTGUIDE, 167 + VERTGUIDE, 268 + VERTGUIDE, 299 + VERTGUIDE, 442 + BOTTOMMARGIN, 227 + END + + IDD_CHANNELMANAGER, DIALOG + BEGIN + RIGHTMARGIN, 523 + BOTTOMMARGIN, 115 + END + + IDD_DEFAULTPLUGINEDITOR, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 329 + TOPMARGIN, 7 + BOTTOMMARGIN, 240 + END + + IDD_MOVEFXSLOT, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 201 + TOPMARGIN, 7 + BOTTOMMARGIN, 53 + END + + IDD_CONTROL_GRAPH, DIALOG + BEGIN + LEFTMARGIN, 7 + RIGHTMARGIN, 417 + TOPMARGIN, 7 + BOTTOMMARGIN, 138 + END + + IDD_SCALE_ENV_POINTS, DIALOG + BEGIN + LEFTMARGIN, 2 + RIGHTMARGIN, 166 + TOPMARGIN, 6 + BOTTOMMARGIN, 89 + END +END +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// AFX_DIALOG_LAYOUT +// + +IDD_TUNING AFX_DIALOG_LAYOUT +BEGIN + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Menu +// + +IDR_VSTMENU MENU +BEGIN + POPUP "&File" + BEGIN + MENUITEM "&Copy Preset", ID_EDIT_COPY + MENUITEM "&Paste Preset", ID_EDIT_PASTE + MENUITEM SEPARATOR + MENUITEM "&Load Preset / Bank...", ID_PRESET_LOAD + MENUITEM "&Save Preset / Bank...", ID_PRESET_SAVE + MENUITEM SEPARATOR + MENUITEM "Create &instrument from plugin", ID_PLUGINTOINSTRUMENT + MENUITEM "&Randomize Parameters", ID_PRESET_RANDOM + MENUITEM SEPARATOR + MENUITEM "Re&name Plugin", ID_RENAME_PLUGIN + END + POPUP "&Info" + BEGIN + MENUITEM "I&nputs", ID_INFO_INPUTS + MENUITEM "Ou&tputs", ID_INFO_OUPUTS + MENUITEM "&Macros", ID_INFO_MACROS + END + POPUP "&Options" + BEGIN + MENUITEM "&Bypass Plugin", ID_PLUG_BYPASS + MENUITEM "Record &Parameter Changes", ID_PLUG_RECORDAUTOMATION + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// Dialog Info +// + +IDD_MIDIPARAMCONTROL DLGINIT +BEGIN + IDC_COMBO_CHANNEL, 0x403, 4, 0 +0x6e41, 0x0079, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0031, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0032, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0033, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0034, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0035, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0036, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0037, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0038, + IDC_COMBO_CHANNEL, 0x403, 2, 0 +0x0039, + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3031, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3131, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3231, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3331, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3431, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3531, "\000" + IDC_COMBO_CHANNEL, 0x403, 3, 0 +0x3631, "\000" + 0 +END + + +///////////////////////////////////////////////////////////////////////////// +// +// KEYBINDINGS +// + +IDR_DEFAULT_KEYBINDINGS KEYBINDINGS "res\\defaultKeybindings.mkb" + +#endif // English (United Kingdom) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// +#define _AFX_NO_OLE_RESOURCES +#define _AFX_NO_TRACKER_RESOURCES +#define _AFX_NO_PROPERTY_RESOURCES + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +#ifdef _WIN32 +LANGUAGE 9, 1 +#pragma code_page(1252) +#endif +#include "res\mptrack.rc2" // non-Microsoft Visual C++ edited resources +#include "afxres.rc" // Standard components +#endif +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.cpp new file mode 100644 index 00000000..f840ecfa --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.cpp @@ -0,0 +1,401 @@ +/* + * LFOPluginEditor.cpp + * ------------------- + * Purpose: Editor interface for the LFO plugin. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#include "LFOPluginEditor.h" +#include "../Mptrack.h" +#include "../UpdateHints.h" +#include "../../soundlib/Sndfile.h" +#include "../../soundlib/MIDIEvents.h" +#include "../../mptrack/resource.h" + +OPENMPT_NAMESPACE_BEGIN + +BEGIN_MESSAGE_MAP(LFOPluginEditor, CAbstractVstEditor) + //{{AFX_MSG_MAP(LFOPluginEditor) + ON_WM_HSCROLL() + ON_COMMAND(IDC_BUTTON1, &LFOPluginEditor::OnPluginEditor) + ON_COMMAND(IDC_CHECK1, &LFOPluginEditor::OnPolarityChanged) + ON_COMMAND(IDC_CHECK2, &LFOPluginEditor::OnTempoSyncChanged) + ON_COMMAND(IDC_CHECK3, &LFOPluginEditor::OnBypassChanged) + ON_COMMAND(IDC_CHECK4, &LFOPluginEditor::OnLoopModeChanged) + ON_COMMAND_RANGE(IDC_RADIO1, IDC_RADIO1 + LFOPlugin::kNumWaveforms - 1, &LFOPluginEditor::OnWaveformChanged) + ON_CBN_SELCHANGE(IDC_COMBO1, &LFOPluginEditor::OnPlugParameterChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &LFOPluginEditor::OnOutputPlugChanged) + ON_CBN_SELCHANGE(IDC_COMBO3, &LFOPluginEditor::OnMidiCCChanged) + ON_COMMAND(IDC_RADIO7, &LFOPluginEditor::OnParameterChanged) + ON_COMMAND(IDC_RADIO8, &LFOPluginEditor::OnParameterChanged) + ON_EN_UPDATE(IDC_EDIT1, &LFOPluginEditor::OnParameterChanged) + ON_MESSAGE(LFOPlugin::WM_PARAM_UDPATE, &LFOPluginEditor::OnUpdateParam) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void LFOPluginEditor::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(LFOPluginEditor) + DDX_Control(pDX, IDC_COMBO1, m_plugParam); + DDX_Control(pDX, IDC_COMBO2, m_outPlug); + DDX_Control(pDX, IDC_COMBO3, m_midiCC); + DDX_Control(pDX, IDC_SLIDER1, m_amplitudeSlider); + DDX_Control(pDX, IDC_SLIDER2, m_offsetSlider); + DDX_Control(pDX, IDC_SLIDER3, m_frequencySlider); + DDX_Control(pDX, IDC_EDIT1, m_midiChnEdit); + DDX_Control(pDX, IDC_SPIN1, m_midiChnSpin); + //}}AFX_DATA_MAP +} + + +LFOPluginEditor::LFOPluginEditor(LFOPlugin &plugin) + : CAbstractVstEditor(plugin) + , m_lfoPlugin(plugin) + , m_locked(true) +{ +} + + +bool LFOPluginEditor::OpenEditor(CWnd *parent) +{ + m_locked = true; + Create(IDD_LFOPLUGIN, parent); + + m_midiChnSpin.SetRange32(1, 16); + m_midiCC.SetRedraw(FALSE); + CString s; + for(unsigned int i = 0; i < 128; i++) + { + s.Format(_T("%3u: "), i); + s += mpt::ToCString(mpt::Charset::UTF8, MIDIEvents::MidiCCNames[i]); + m_midiCC.AddString(s); + } + if(m_lfoPlugin.m_outputToCC && m_lfoPlugin.m_outputParam != LFOPlugin::INVALID_OUTPUT_PARAM) + { + m_midiCC.SetCurSel(m_lfoPlugin.m_outputParam & 0x7F); + SetDlgItemInt(IDC_EDIT1, 1 + ((m_lfoPlugin.m_outputParam & 0xF00) >> 8)); + } else + { + SetDlgItemInt(IDC_EDIT1, 1); + } + m_midiCC.SetRedraw(TRUE); + + UpdateView(PluginHint().Info().Names()); + + for(int32 i = 0; i < LFOPlugin::kLFONumParameters; i++) + { + UpdateParam(i); + } + UpdateParamDisplays(); + + m_locked = false; + // Avoid weird WM_COMMAND message (following a WM_ACTIVATE message) activating a wrong waveform when closing the plugin editor while the pattern editor is open + GetDlgItem(IDC_RADIO1 + m_lfoPlugin.m_waveForm)->SetFocus(); + return CAbstractVstEditor::OpenEditor(parent); +} + + +void LFOPluginEditor::UpdateParamDisplays() +{ + CAbstractVstEditor::UpdateParamDisplays(); + m_locked = true; + CheckRadioButton(IDC_RADIO7, IDC_RADIO8, m_lfoPlugin.m_outputToCC ? IDC_RADIO8 : IDC_RADIO7); + m_plugParam.SetRedraw(FALSE); + IMixPlugin *outPlug = m_lfoPlugin.GetOutputPlugin(); + if(outPlug != nullptr) + { + if(LONG_PTR(outPlug) != GetWindowLongPtr(m_plugParam, GWLP_USERDATA)) + { + m_plugParam.ResetContent(); + AddPluginParameternamesToCombobox(m_plugParam, *outPlug); + if(!m_lfoPlugin.m_outputToCC) + m_plugParam.SetCurSel(m_lfoPlugin.m_outputParam); + SetWindowLongPtr(m_plugParam, GWLP_USERDATA, LONG_PTR(outPlug)); + } + GetDlgItem(IDC_BUTTON1)->EnableWindow(outPlug ? TRUE :FALSE); + } else + { + m_plugParam.ResetContent(); + SetWindowLongPtr(m_plugParam, GWLP_USERDATA, 0); + } + m_plugParam.SetRedraw(TRUE); + m_locked = false; +} + + +void LFOPluginEditor::UpdateParam(int32 p) +{ + LFOPlugin::Parameters param = static_cast<LFOPlugin::Parameters>(p); + CAbstractVstEditor::UpdateParam(p); + m_locked = true; + switch(param) + { + case LFOPlugin::kAmplitude: + InitSlider(m_amplitudeSlider, LFOPlugin::kAmplitude); + break; + case LFOPlugin::kOffset: + InitSlider(m_offsetSlider, LFOPlugin::kOffset); + break; + case LFOPlugin::kFrequency: + InitSlider(m_frequencySlider, LFOPlugin::kFrequency); + break; + case LFOPlugin::kTempoSync: + CheckDlgButton(IDC_CHECK2, m_lfoPlugin.m_tempoSync ? BST_CHECKED : BST_UNCHECKED); + InitSlider(m_frequencySlider, LFOPlugin::kFrequency); + break; + case LFOPlugin::kWaveform: + CheckRadioButton(IDC_RADIO1, IDC_RADIO1 + LFOPlugin::kNumWaveforms - 1, IDC_RADIO1 + m_lfoPlugin.m_waveForm); + break; + case LFOPlugin::kPolarity: + CheckDlgButton(IDC_CHECK1, m_lfoPlugin.m_polarity ? BST_CHECKED : BST_UNCHECKED); + break; + case LFOPlugin::kBypassed: + CheckDlgButton(IDC_CHECK3, m_lfoPlugin.m_bypassed ? BST_CHECKED : BST_UNCHECKED); + break; + case LFOPlugin::kLoopMode: + CheckDlgButton(IDC_CHECK4, m_lfoPlugin.m_oneshot ? BST_CHECKED : BST_UNCHECKED); + break; + default: + break; + } + m_locked = false; +} + + +void LFOPluginEditor::UpdateView(UpdateHint hint) +{ + CAbstractVstEditor::UpdateView(hint); + if(hint.GetType()[HINT_PLUGINNAMES | HINT_MIXPLUGINS]) + { + PLUGINDEX hintPlug = hint.ToType<PluginHint>().GetPlugin(); + if(hintPlug > 0 && hintPlug <= m_lfoPlugin.GetSlot()) + { + return; + } + + CString s; + IMixPlugin *outPlugin = m_lfoPlugin.GetOutputPlugin(); + m_outPlug.SetRedraw(FALSE); + m_outPlug.ResetContent(); + for(PLUGINDEX out = m_lfoPlugin.GetSlot() + 1; out < MAX_MIXPLUGINS; out++) + { + const SNDMIXPLUGIN &outPlug = m_lfoPlugin.GetSoundFile().m_MixPlugins[out]; + if(outPlug.IsValidPlugin()) + { + mpt::ustring libName = outPlug.GetLibraryName(); + s.Format(_T("FX%d: "), out + 1); + s += mpt::ToCString(libName); + if(outPlug.GetName() != U_("") && libName != outPlug.GetName()) + { + s += _T(" ("); + s += mpt::ToCString(outPlug.GetName()); + s += _T(")"); + } + + int n = m_outPlug.AddString(s); + m_outPlug.SetItemData(n, out); + if(outPlugin == outPlug.pMixPlugin) + { + m_outPlug.SetCurSel(n); + } + } + } + m_outPlug.SetRedraw(TRUE); + m_outPlug.Invalidate(FALSE); + m_plugParam.Invalidate(FALSE); + } +} + + +void LFOPluginEditor::InitSlider(CSliderCtrl &slider, LFOPlugin::Parameters param) +{ + slider.SetRange(0, SLIDER_GRANULARITY); + slider.SetTicFreq(SLIDER_GRANULARITY / 10); + SetSliderValue(slider, m_lfoPlugin.GetParameter(param)); + SetSliderText(param); +} + + +void LFOPluginEditor::SetSliderText(LFOPlugin::Parameters param) +{ + CString s = m_lfoPlugin.GetParamName(param) + _T(": ") + m_lfoPlugin.GetFormattedParamValue(param); + SetDlgItemText(IDC_STATIC1 + param, s); +} + + +void LFOPluginEditor::SetSliderValue(CSliderCtrl &slider, float value) +{ + slider.SetPos(mpt::saturate_round<int>(value * SLIDER_GRANULARITY)); +} + + +float LFOPluginEditor::GetSliderValue(CSliderCtrl &slider) +{ + float value = slider.GetPos() / static_cast<float>(SLIDER_GRANULARITY); + return value; +} + + +void LFOPluginEditor::OnHScroll(UINT nCode, UINT nPos, CScrollBar *pSB) +{ + CAbstractVstEditor::OnHScroll(nCode, nPos, pSB); + if(!m_locked && nCode != SB_ENDSCROLL && pSB != nullptr) + { + auto slider = reinterpret_cast<CSliderCtrl *>(pSB); + LFOPlugin::Parameters param; + if(slider == &m_amplitudeSlider) + param = LFOPlugin::kAmplitude; + else if(slider == &m_offsetSlider) + param = LFOPlugin::kOffset; + else if(slider == &m_frequencySlider) + param = LFOPlugin::kFrequency; + else + return; + + float value = GetSliderValue(*slider); + m_lfoPlugin.SetParameter(param, value); + m_lfoPlugin.AutomateParameter(param); + SetSliderText(param); + } +} + + +void LFOPluginEditor::OnPolarityChanged() +{ + if(!m_locked) + { + m_lfoPlugin.m_polarity = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + m_lfoPlugin.AutomateParameter(LFOPlugin::kPolarity); + } +} + + +void LFOPluginEditor::OnTempoSyncChanged() +{ + if(!m_locked) + { + m_lfoPlugin.m_tempoSync = IsDlgButtonChecked(IDC_CHECK2) != BST_UNCHECKED; + m_lfoPlugin.RecalculateFrequency(); + m_lfoPlugin.AutomateParameter(LFOPlugin::kTempoSync); + InitSlider(m_frequencySlider, LFOPlugin::kFrequency); + } +} + + +void LFOPluginEditor::OnBypassChanged() +{ + if(!m_locked) + { + m_lfoPlugin.m_bypassed = IsDlgButtonChecked(IDC_CHECK3) != BST_UNCHECKED; + m_lfoPlugin.AutomateParameter(LFOPlugin::kBypassed); + } +} + + +void LFOPluginEditor::OnLoopModeChanged() +{ + if(!m_locked) + { + m_lfoPlugin.m_oneshot = IsDlgButtonChecked(IDC_CHECK4) != BST_UNCHECKED; + m_lfoPlugin.AutomateParameter(LFOPlugin::kLoopMode); + } +} + + +void LFOPluginEditor::OnWaveformChanged(UINT nID) +{ + if(!m_locked) + { + m_lfoPlugin.m_waveForm = static_cast<LFOPlugin::LFOWaveform>(nID - IDC_RADIO1); + m_lfoPlugin.AutomateParameter(LFOPlugin::kWaveform); + } +} + + +void LFOPluginEditor::OnPlugParameterChanged() +{ + if(!m_locked) + { + CheckRadioButton(IDC_RADIO7, IDC_RADIO8, IDC_RADIO7); + OnParameterChanged(); + } +} + + +void LFOPluginEditor::OnMidiCCChanged() +{ + if(!m_locked) + { + CheckRadioButton(IDC_RADIO7, IDC_RADIO8, IDC_RADIO8); + OnParameterChanged(); + } +} + + +void LFOPluginEditor::OnParameterChanged() +{ + if(!m_locked) + { + m_locked = true; + PlugParamIndex param = 0; + bool outputToCC = IsDlgButtonChecked(IDC_RADIO8) != BST_UNCHECKED; + if(outputToCC) + { + param = ((Clamp(GetDlgItemInt(IDC_EDIT1), 1u, 16u) - 1) << 8); + if(m_lfoPlugin.m_outputToCC || m_midiCC.GetCurSel() >= 0) + param |= (m_midiCC.GetCurSel() & 0x7F); + } else + { + if(!m_lfoPlugin.m_outputToCC || m_plugParam.GetCurSel() >= 0) + param = static_cast<PlugParamIndex>(m_plugParam.GetItemData(m_plugParam.GetCurSel())); + } + m_lfoPlugin.m_outputToCC = outputToCC; + m_lfoPlugin.m_outputParam = param; + m_lfoPlugin.SetModified(); + m_locked = false; + } +} + + +void LFOPluginEditor::OnOutputPlugChanged() +{ + if(!m_locked) + { + PLUGINDEX plug = static_cast<PLUGINDEX>(m_outPlug.GetItemData(m_outPlug.GetCurSel())); + if(plug > m_lfoPlugin.GetSlot()) + { + m_lfoPlugin.GetSoundFile().m_MixPlugins[m_lfoPlugin.GetSlot()].SetOutputPlugin(plug); + m_lfoPlugin.SetModified(); + UpdateParamDisplays(); + } + } +} + + +void LFOPluginEditor::OnPluginEditor() +{ + std::vector<IMixPlugin *> plug; + if(m_lfoPlugin.GetOutputPlugList(plug) && plug.front() != nullptr) + { + plug.front()->ToggleEditor(); + } +} + + +LRESULT LFOPluginEditor::OnUpdateParam(WPARAM wParam, LPARAM lParam) +{ + if(wParam == m_lfoPlugin.GetSlot()) + { + UpdateParam(static_cast<int32>(lParam)); + } + return 0; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.h b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.h new file mode 100644 index 00000000..bb6fb2b5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/LFOPluginEditor.h @@ -0,0 +1,69 @@ +/* + * LFOPluginEditor.h + * ----------------- + * Purpose: Editor interface for the LFO plugin. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../AbstractVstEditor.h" +#include "../../soundlib/plugins/LFOPlugin.h" + +OPENMPT_NAMESPACE_BEGIN + +struct UpdateHint; + +class LFOPluginEditor : public CAbstractVstEditor +{ +protected: + CComboBox m_plugParam, m_outPlug, m_midiCC; + CSliderCtrl m_amplitudeSlider, m_offsetSlider, m_frequencySlider; + CEdit m_midiChnEdit; + CSpinButtonCtrl m_midiChnSpin; + LFOPlugin &m_lfoPlugin; + bool m_locked : 1; + static constexpr int SLIDER_GRANULARITY = 1000; + +public: + + LFOPluginEditor(LFOPlugin &plugin); + + bool OpenEditor(CWnd *parent) override; + bool IsResizable() const override { return false; } + bool SetSize(int, int) override { return false; } + + void UpdateParamDisplays() override; + void UpdateParam(int32 param) override; + void UpdateView(UpdateHint hint) override; + +protected: + void DoDataExchange(CDataExchange* pDX) override; + void OnHScroll(UINT nCode, UINT nPos, CScrollBar *pSB); + + void InitSlider(CSliderCtrl &slider, LFOPlugin::Parameters param); + void SetSliderText(LFOPlugin::Parameters param); + void SetSliderValue(CSliderCtrl &slider, float value); + float GetSliderValue(CSliderCtrl &slider); + + void OnPolarityChanged(); + void OnTempoSyncChanged(); + void OnBypassChanged(); + void OnLoopModeChanged(); + void OnWaveformChanged(UINT nID); + void OnPlugParameterChanged(); + void OnMidiCCChanged(); + void OnParameterChanged(); + void OnOutputPlugChanged(); + void OnPluginEditor(); + LRESULT OnUpdateParam(WPARAM wParam, LPARAM lParam); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.cpp new file mode 100644 index 00000000..fd5900de --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.cpp @@ -0,0 +1,547 @@ +/* + * MidiInOut.cpp + * ------------- + * Purpose: A plugin for sending and receiving MIDI data. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" +#include "MidiInOut.h" +#include "MidiInOutEditor.h" +#include "../../common/FileReader.h" +#include "../../soundlib/Sndfile.h" +#include "../Reporting.h" +#include <algorithm> +#include <sstream> +#ifdef MODPLUG_TRACKER +#include "../Mptrack.h" +#endif +#include "mpt/io/io.hpp" +#include "mpt/io/io_stdstream.hpp" + + +OPENMPT_NAMESPACE_BEGIN + + +IMixPlugin* MidiInOut::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) +{ + try + { + return new (std::nothrow) MidiInOut(factory, sndFile, mixStruct); + } catch(RtMidiError &) + { + return nullptr; + } +} + + +MidiInOut::MidiInOut(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct) + : IMidiPlugin(factory, sndFile, mixStruct) + , m_inputDevice(m_midiIn) + , m_outputDevice(m_midiOut) +#ifdef MODPLUG_TRACKER + , m_programName(_T("Default")) +#endif // MODPLUG_TRACKER +{ + m_mixBuffer.Initialize(2, 2); + InsertIntoFactoryList(); +} + + +MidiInOut::~MidiInOut() +{ + MidiInOut::Suspend(); +} + + +uint32 MidiInOut::GetLatency() const +{ + // There is only a latency if the user-provided latency value is greater than the negative output latency. + return mpt::saturate_round<uint32>(std::min(0.0, m_latency + GetOutputLatency()) * m_SndFile.GetSampleRate()); +} + + +void MidiInOut::SaveAllParameters() +{ + auto chunk = GetChunk(false); + if(chunk.empty()) + return; + + m_pMixStruct->defaultProgram = -1; + m_pMixStruct->pluginData.assign(chunk.begin(), chunk.end()); +} + + +void MidiInOut::RestoreAllParameters(int32 program) +{ + IMixPlugin::RestoreAllParameters(program); // First plugin version didn't use chunks. + SetChunk(mpt::as_span(m_pMixStruct->pluginData), false); +} + + +enum ChunkFlags +{ + kLatencyCompensation = 0x01, // Implicit in current plugin version + kLatencyPresent = 0x02, // Latency value is present as double-precision float + kIgnoreTiming = 0x04, // Do not send timing and sequencing information + kFriendlyInputName = 0x08, // Preset also stores friendly name of input device + kFriendlyOutputName = 0x10, // Preset also stores friendly name of output device +}; + +IMixPlugin::ChunkData MidiInOut::GetChunk(bool /*isBank*/) +{ + const std::string programName8 = mpt::ToCharset(mpt::Charset::UTF8, m_programName); + uint32 flags = kLatencyCompensation | kLatencyPresent | (m_sendTimingInfo ? 0 : kIgnoreTiming); +#ifdef MODPLUG_TRACKER + const std::string inFriendlyName = (m_inputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_inputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_inputDevice.name), true, false)); + const std::string outFriendlyName = (m_outputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_outputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_outputDevice.name), false, false)); + if(inFriendlyName != m_inputDevice.name) + { + flags |= kFriendlyInputName; + } + if(outFriendlyName != m_outputDevice.name) + { + flags |= kFriendlyOutputName; + } +#endif + + std::ostringstream s; + mpt::IO::WriteRaw(s, "fEvN", 4); // VST program chunk magic + mpt::IO::WriteIntLE< int32>(s, GetVersion()); + mpt::IO::WriteIntLE<uint32>(s, 1); // Number of programs + mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(programName8.size())); + mpt::IO::WriteIntLE<uint32>(s, m_inputDevice.index); + mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_inputDevice.name.size())); + mpt::IO::WriteIntLE<uint32>(s, m_outputDevice.index); + mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_outputDevice.name.size())); + mpt::IO::WriteIntLE<uint32>(s, flags); + mpt::IO::WriteRaw(s, programName8.c_str(), programName8.size()); + mpt::IO::WriteRaw(s, m_inputDevice.name.c_str(), m_inputDevice.name.size()); + mpt::IO::WriteRaw(s, m_outputDevice.name.c_str(), m_outputDevice.name.size()); + mpt::IO::WriteIntLE<uint64>(s, IEEE754binary64LE(m_latency).GetInt64()); +#ifdef MODPLUG_TRACKER + if(flags & kFriendlyInputName) + { + mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(inFriendlyName.size())); + mpt::IO::WriteRaw(s, inFriendlyName.c_str(), inFriendlyName.size()); + } + if(flags & kFriendlyOutputName) + { + mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(outFriendlyName.size())); + mpt::IO::WriteRaw(s, outFriendlyName.c_str(), outFriendlyName.size()); + } +#endif + m_chunkData = s.str(); + return mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_chunkData)); +} + + +// Try to match a port name against stored name or friendly name (preferred) +static void FindPort(MidiDevice::ID &id, unsigned int numPorts, const std::string &name, const std::string &friendlyName, MidiDevice &midiDevice, bool isInput) +{ + bool foundFriendly = false; + for(unsigned int i = 0; i < numPorts; i++) + { + try + { + auto portName = midiDevice.GetPortName(i); + bool deviceNameMatches = (portName == name); +#ifdef MODPLUG_TRACKER + if(!friendlyName.empty() && friendlyName == mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, portName), isInput, false))) + { + // Preferred match + id = i; + foundFriendly = true; + if(deviceNameMatches) + { + return; + } + } +#else + MPT_UNREFERENCED_PARAMETER(friendlyName) +#endif + if(deviceNameMatches && !foundFriendly) + { + id = i; + } + } catch(const RtMidiError &) + { + } + } +} + + +void MidiInOut::SetChunk(const ChunkData &chunk, bool /*isBank*/) +{ + FileReader file(chunk); + if(!file.CanRead(9 * sizeof(uint32)) + || !file.ReadMagic("fEvN") // VST program chunk magic + || file.ReadInt32LE() > GetVersion() // Plugin version + || file.ReadUint32LE() < 1) // Number of programs + return; + + uint32 nameStrSize = file.ReadUint32LE(); + MidiDevice::ID inID = file.ReadUint32LE(); + uint32 inStrSize = file.ReadUint32LE(); + MidiDevice::ID outID = file.ReadUint32LE(); + uint32 outStrSize = file.ReadUint32LE(); + uint32 flags = file.ReadUint32LE(); + + std::string progName, inName, outName, inFriendlyName, outFriendlyName; + file.ReadString<mpt::String::maybeNullTerminated>(progName, nameStrSize); + m_programName = mpt::ToCString(mpt::Charset::UTF8, progName); + + file.ReadString<mpt::String::maybeNullTerminated>(inName, inStrSize); + file.ReadString<mpt::String::maybeNullTerminated>(outName, outStrSize); + + if(flags & kLatencyPresent) + m_latency = file.ReadDoubleLE(); + else + m_latency = 0.0f; + m_sendTimingInfo = !(flags & kIgnoreTiming); + + if(flags & kFriendlyInputName) + file.ReadString<mpt::String::maybeNullTerminated>(inFriendlyName, file.ReadUint32LE()); + if(flags & kFriendlyOutputName) + file.ReadString<mpt::String::maybeNullTerminated>(outFriendlyName, file.ReadUint32LE()); + + // Try to match an input port name against stored name or friendly name (preferred) + FindPort(inID, m_midiIn.getPortCount(), inName, inFriendlyName, m_inputDevice, true); + FindPort(outID, m_midiOut.getPortCount(), outName, outFriendlyName, m_outputDevice, false); + + SetParameter(MidiInOut::kInputParameter, DeviceIDToParameter(inID)); + SetParameter(MidiInOut::kOutputParameter, DeviceIDToParameter(outID)); +} + + +void MidiInOut::SetParameter(PlugParamIndex index, PlugParamValue value) +{ + value = mpt::safe_clamp(value, 0.0f, 1.0f); + MidiDevice::ID newDevice = ParameterToDeviceID(value); + OpenDevice(newDevice, (index == kInputParameter)); + + // Update selection in editor + MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor()); + if(editor != nullptr) + editor->SetCurrentDevice((index == kInputParameter), newDevice); +} + + +float MidiInOut::GetParameter(PlugParamIndex index) +{ + const MidiDevice &device = (index == kInputParameter) ? m_inputDevice : m_outputDevice; + return DeviceIDToParameter(device.index); +} + + +#ifdef MODPLUG_TRACKER + +CString MidiInOut::GetParamName(PlugParamIndex param) +{ + if(param == kInputParameter) + return _T("MIDI In"); + else + return _T("MIDI Out"); +} + + +// Parameter value as text +CString MidiInOut::GetParamDisplay(PlugParamIndex param) +{ + const MidiDevice &device = (param == kInputParameter) ? m_inputDevice : m_outputDevice; + return mpt::ToCString(mpt::Charset::UTF8, device.name); +} + + +CAbstractVstEditor *MidiInOut::OpenEditor() +{ + try + { + return new MidiInOutEditor(*this); + } catch(mpt::out_of_memory e) + { + mpt::delete_out_of_memory(e); + return nullptr; + } +} + +#endif // MODPLUG_TRACKER + + +// Processing (we don't process any audio, only MIDI messages) +void MidiInOut::Process(float *, float *, uint32 numFrames) +{ + if(m_midiOut.isPortOpen()) + { + mpt::lock_guard<mpt::mutex> lock(m_mutex); + + // Send MIDI clock + if(m_nextClock < 1) + { + if(m_sendTimingInfo) + { + m_outQueue.push_back(Message(GetOutputTimestamp(), 0xF8)); + } + + double bpm = m_SndFile.GetCurrentBPM(); + if(bpm > 0.0) + { + m_nextClock += 2.5 * m_SndFile.GetSampleRate() / bpm; + } + } + m_nextClock -= numFrames; + + double now = m_clock.Now() * (1.0 / 1000.0); + auto message = m_outQueue.begin(); + while(message != m_outQueue.end() && message->m_time <= now) + { + try + { + m_midiOut.sendMessage(message->m_message, message->m_size); + } catch(const RtMidiError &) + { + } + message++; + } + m_outQueue.erase(m_outQueue.begin(), message); + } +} + + +void MidiInOut::InputCallback(double /*deltatime*/, std::vector<unsigned char> &message) +{ + // We will check the bypass status before passing on the message, and not before entering the function, + // because otherwise we might read garbage if we toggle bypass status in the middle of a SysEx message. + bool isBypassed = IsBypassed(); + if(message.empty()) + { + return; + } else if(!m_bufferedInput.empty()) + { + // SysEx message (continued) + m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end()); + if(message.back() == 0xF7) + { + // End of message found! + if(!isBypassed) + ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_bufferedInput))); + m_bufferedInput.clear(); + } + } else if(message.front() == 0xF0) + { + // Start of SysEx message... + if(message.back() != 0xF7) + m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end()); // ...but not the end! + else if(!isBypassed) + ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(message))); + } else if(!isBypassed) + { + // Regular message + uint32 msg = 0; + memcpy(&msg, message.data(), std::min(message.size(), sizeof(msg))); + ReceiveMidi(msg); + } +} + + +// Resume playback +void MidiInOut::Resume() +{ + // Resume MIDI I/O + m_isResumed = true; + m_nextClock = 0; + m_outQueue.clear(); + m_clock.SetResolution(1); + OpenDevice(m_inputDevice.index, true); + OpenDevice(m_outputDevice.index, false); + if(m_midiOut.isPortOpen() && m_sendTimingInfo) + { + MidiSend(0xFA); // Start + } +} + + +// Stop playback +void MidiInOut::Suspend() +{ + // Suspend MIDI I/O + if(m_midiOut.isPortOpen() && m_sendTimingInfo) + { + try + { + unsigned char message[1] = { 0xFC }; // Stop + m_midiOut.sendMessage(message, 1); + } catch(const RtMidiError &) + { + } + } + //CloseDevice(inputDevice); + CloseDevice(m_outputDevice); + m_clock.SetResolution(0); + m_isResumed = false; +} + + +// Playback discontinuity +void MidiInOut::PositionChanged() +{ + if(m_sendTimingInfo) + { + MidiSend(0xFC); // Stop + MidiSend(0xFA); // Start + } +} + + +void MidiInOut::Bypass(bool bypass) +{ + if(bypass) + { + mpt::lock_guard<mpt::mutex> lock(m_mutex); + m_outQueue.clear(); + } + IMidiPlugin::Bypass(bypass); +} + + +bool MidiInOut::MidiSend(uint32 midiCode) +{ + if(!m_midiOut.isPortOpen() || IsBypassed()) + { + // We need an output device to send MIDI messages to. + return true; + } + + mpt::lock_guard<mpt::mutex> lock(m_mutex); + m_outQueue.push_back(Message(GetOutputTimestamp(), &midiCode, 3)); + return true; +} + + +bool MidiInOut::MidiSysexSend(mpt::const_byte_span sysex) +{ + if(!m_midiOut.isPortOpen() || IsBypassed()) + { + // We need an output device to send MIDI messages to. + return true; + } + + mpt::lock_guard<mpt::mutex> lock(m_mutex); + m_outQueue.push_back(Message(GetOutputTimestamp(), sysex.data(), sysex.size())); + return true; +} + + +void MidiInOut::HardAllNotesOff() +{ + const bool wasSuspended = !IsResumed(); + if(wasSuspended) + { + Resume(); + } + + for(uint8 mc = 0; mc < std::size(m_MidiCh); mc++) //all midi chans + { + PlugInstrChannel &channel = m_MidiCh[mc]; + channel.ResetProgram(); + + SendMidiPitchBend(mc, EncodePitchBendParam(MIDIEvents::pitchBendCentre)); // centre pitch bend + MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllSoundOff, mc, 0)); // all sounds off + + for(size_t i = 0; i < std::size(channel.noteOnMap); i++) + { + for(auto &c : channel.noteOnMap[i]) + { + while(c != 0) + { + MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0)); + c--; + } + } + } + } + + if(wasSuspended) + { + Suspend(); + } +} + + +// Open a device for input or output. +void MidiInOut::OpenDevice(MidiDevice::ID newDevice, bool asInputDevice) +{ + MidiDevice &device = asInputDevice ? m_inputDevice : m_outputDevice; + + if(device.index == newDevice && device.stream.isPortOpen()) + { + // No need to re-open this device. + return; + } + + CloseDevice(device); + + device.index = newDevice; + device.stream.closePort(); + + if(device.index == kNoDevice) + { + // Dummy device + device.name = "<none>"; + return; + } + + device.name = device.GetPortName(newDevice); + //if(m_isResumed) + { + mpt::lock_guard<mpt::mutex> lock(m_mutex); + + try + { + device.stream.openPort(newDevice); + if(asInputDevice) + { + m_midiIn.setCallback(InputCallback, this); + m_midiIn.ignoreTypes(false, true, true); + } + } catch(RtMidiError &error) + { + device.name = "Unavailable"; + MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor()); + if(editor != nullptr) + { + Reporting::Error("MIDI device cannot be opened. Is it open in another application?\n\n" + error.getMessage(), "MIDI Input / Output", editor); + } + } + } +} + + +// Close an active device. +void MidiInOut::CloseDevice(MidiDevice &device) +{ + if(device.stream.isPortOpen()) + { + mpt::lock_guard<mpt::mutex> lock(m_mutex); + device.stream.closePort(); + } +} + + +// Calculate the current output timestamp +double MidiInOut::GetOutputTimestamp() const +{ + return m_clock.Now() * (1.0 / 1000.0) + GetOutputLatency() + m_latency; +} + + +// Get a device name +std::string MidiDevice::GetPortName(MidiDevice::ID port) +{ + return stream.getPortName(port); +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.h b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.h new file mode 100644 index 00000000..cf8aa86c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOut.h @@ -0,0 +1,232 @@ +/* + * MidiInOut.h + * ----------- + * Purpose: A plugin for sending and receiving MIDI data. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "mpt/mutex/mutex.hpp" +#include "../../common/mptTime.h" +#include "../../soundlib/plugins/PlugInterface.h" +#include <rtmidi/RtMidi.h> +#include <array> +#include <deque> + + +OPENMPT_NAMESPACE_BEGIN + + +class MidiDevice +{ +public: + using ID = decltype(RtMidiIn().getPortCount()); + static constexpr ID NO_MIDI_DEVICE = ID(-1); + + RtMidi &stream; + std::string name; // Charset::UTF8 + ID index = NO_MIDI_DEVICE; + +public: + MidiDevice(RtMidi &stream) + : stream(stream) + , name("<none>") + { } + + std::string GetPortName(ID port); // Charset::UTF8 +}; + + +class MidiInOut final : public IMidiPlugin +{ + friend class MidiInOutEditor; + +protected: + enum + { + kInputParameter = 0, + kOutputParameter = 1, + + kNumPrograms = 1, + kNumParams = 2, + + kNoDevice = MidiDevice::NO_MIDI_DEVICE, + kMaxDevices = 65536, // Should be a power of 2 to avoid rounding errors. + }; + + // MIDI queue entry with small storage optimiziation. + // This optimiziation is going to be used for all messages that OpenMPT can send internally, + // but SysEx messages received from other plugins may be longer. + class Message + { + public: + double m_time; + size_t m_size; + unsigned char *m_message = nullptr; + protected: + std::array<unsigned char, 32> m_msgSmall; + + public: + Message(double time, const void *data, size_t size) + : m_time(time) + , m_size(size) + { + if(size > m_msgSmall.size()) + m_message = new unsigned char[size]; + else + m_message = m_msgSmall.data(); + std::memcpy(m_message, data, size); + } + + Message(const Message &) = delete; + Message & operator=(const Message &) = delete; + + Message(double time, unsigned char msg) noexcept : Message(time, &msg, 1) { } + + Message(Message &&other) noexcept + : m_time(other.m_time) + , m_size(other.m_size) + , m_message(other.m_message) + , m_msgSmall(other.m_msgSmall) + { + other.m_message = nullptr; + if(m_size <= m_msgSmall.size()) + m_message = m_msgSmall.data(); + + } + + ~Message() + { + if(m_size > m_msgSmall.size()) + delete[] m_message; + } + + Message& operator= (Message &&other) noexcept + { + m_time = other.m_time; + m_size = other.m_size; + m_message = (m_size <= m_msgSmall.size()) ? m_msgSmall.data() : other.m_message; + m_msgSmall = other.m_msgSmall; + other.m_message = nullptr; + return *this; + } + }; + + std::string m_chunkData; // Storage for GetChunk + std::deque<Message> m_outQueue; // Latency-compensated output + std::vector<unsigned char> m_bufferedInput; // For receiving long SysEx messages + mpt::mutex m_mutex; + double m_nextClock = 0.0; // Remaining samples until next MIDI clock tick should be sent + double m_latency = 0.0; // User-adjusted latency in seconds + + // I/O device settings + Util::MultimediaClock m_clock; + RtMidiIn m_midiIn; + RtMidiOut m_midiOut; + MidiDevice m_inputDevice; + MidiDevice m_outputDevice; + bool m_sendTimingInfo = true; + +#ifdef MODPLUG_TRACKER + CString m_programName; +#endif + +public: + static IMixPlugin* Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + MidiInOut(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct); + ~MidiInOut(); + + // Translate a VST parameter to an RtMidi device ID + static MidiDevice::ID ParameterToDeviceID(float value) + { + return static_cast<MidiDevice::ID>(value * static_cast<float>(kMaxDevices)) - 1; + } + + // Translate a RtMidi device ID to a VST parameter + static float DeviceIDToParameter(MidiDevice::ID index) + { + return static_cast<float>(index + 1) / static_cast<float>(kMaxDevices); + } + + ///////////////////////////////////////////////// + // Destroy the plugin + void Release() final { delete this; } + int32 GetUID() const final { return 'MMID'; } + int32 GetVersion() const final { return 2; } + void Idle() final { } + uint32 GetLatency() const final; + + int32 GetNumPrograms() const final { return kNumPrograms; } + int32 GetCurrentProgram() final { return 0; } + void SetCurrentProgram(int32) final { } + + PlugParamIndex GetNumParameters() const final { return kNumParams; } + void SetParameter(PlugParamIndex paramindex, PlugParamValue paramvalue) final; + PlugParamValue GetParameter(PlugParamIndex nIndex) final; + + // Save parameters for storing them in a module file + void SaveAllParameters() final; + // Restore parameters from module file + void RestoreAllParameters(int32 program) final; + void Process(float *pOutL, float *pOutR, uint32 numFrames) final; + // Render silence and return the highest resulting output level + float RenderSilence(uint32) final{ return 0; } + bool MidiSend(uint32 midiCode) final; + bool MidiSysexSend(mpt::const_byte_span sysex) final; + void HardAllNotesOff() final; + // Modify parameter by given amount. Only needs to be re-implemented if plugin architecture allows this to be performed atomically. + void Resume() final; + void Suspend() final; + // Tell the plugin that there is a discontinuity between the previous and next render call (e.g. aftert jumping around in the module) + void PositionChanged() final; + void Bypass(bool bypass = true) final; + bool IsInstrument() const final { return true; } + bool CanRecieveMidiEvents() final { return true; } + // If false is returned, mixing this plugin can be skipped if its input are currently completely silent. + bool ShouldProcessSilence() final { return true; } + +#ifdef MODPLUG_TRACKER + CString GetDefaultEffectName() final { return _T("MIDI Input / Output"); } + + CString GetParamName(PlugParamIndex param) final; + CString GetParamLabel(PlugParamIndex) final{ return CString(); } + CString GetParamDisplay(PlugParamIndex param) final; + CString GetCurrentProgramName() final { return m_programName; } + void SetCurrentProgramName(const CString &name) final { m_programName = name; } + CString GetProgramName(int32) final { return m_programName; } + virtual CString GetPluginVendor() { return _T("OpenMPT Project"); } + + bool HasEditor() const final { return true; } +protected: + CAbstractVstEditor *OpenEditor() final; +#endif + +public: + int GetNumInputChannels() const final { return 0; } + int GetNumOutputChannels() const final { return 0; } + + bool ProgramsAreChunks() const final { return true; } + ChunkData GetChunk(bool isBank) final; + void SetChunk(const ChunkData &chunk, bool isBank) final; + +protected: + // Open a device for input or output. + void OpenDevice(MidiDevice::ID newDevice, bool asInputDevice); + // Close an active device. + void CloseDevice(MidiDevice &device); + + static void InputCallback(double deltatime, std::vector<unsigned char> *message, void *userData) { static_cast<MidiInOut *>(userData)->InputCallback(deltatime, *message); } + void InputCallback(double deltatime, std::vector<unsigned char> &message); + + // Calculate the current output timestamp + double GetOutputTimestamp() const; +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.cpp new file mode 100644 index 00000000..858050f8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.cpp @@ -0,0 +1,161 @@ +/* + * MidiInOutEditor.cpp + * ------------------- + * Purpose: Editor interface for the MidiInOut plugin. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#include "stdafx.h" + +#ifdef MODPLUG_TRACKER +#include "MidiInOut.h" +#include "MidiInOutEditor.h" +#include "../Mptrack.h" +#include "../resource.h" +#include <rtmidi/RtMidi.h> + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(MidiInOutEditor, CAbstractVstEditor) + //{{AFX_MSG_MAP(MidiInOutEditor) + ON_CBN_SELCHANGE(IDC_COMBO1, &MidiInOutEditor::OnInputChanged) + ON_CBN_SELCHANGE(IDC_COMBO2, &MidiInOutEditor::OnOutputChanged) + ON_EN_CHANGE(IDC_EDIT1, &MidiInOutEditor::OnLatencyChanged) + ON_COMMAND(IDC_CHECK1, &MidiInOutEditor::OnTimingMessagesChanged) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void MidiInOutEditor::DoDataExchange(CDataExchange* pDX) +{ + CDialog::DoDataExchange(pDX); + //{{AFX_DATA_MAP(MidiInOutEditor) + DDX_Control(pDX, IDC_COMBO1, m_inputCombo); + DDX_Control(pDX, IDC_COMBO2, m_outputCombo); + DDX_Control(pDX, IDC_EDIT1, m_latencyEdit); + DDX_Control(pDX, IDC_SPIN1, m_latencySpin); + //}}AFX_DATA_MAP +} + + +MidiInOutEditor::MidiInOutEditor(MidiInOut &plugin) + : CAbstractVstEditor(plugin) +{ +} + + +bool MidiInOutEditor::OpenEditor(CWnd *parent) +{ + Create(IDD_MIDI_IO_PLUGIN, parent); + MidiInOut &plugin = static_cast<MidiInOut &>(m_VstPlugin); + m_latencyEdit.AllowFractions(true); + m_latencyEdit.AllowNegative(true); + m_latencyEdit.SetDecimalValue(plugin.m_latency * 1000.0, 4); + m_latencySpin.SetRange32(mpt::saturate_round<int>(plugin.GetOutputLatency() * -1000.0), int32_max); + PopulateList(m_inputCombo, plugin.m_midiIn, plugin.m_inputDevice, true); + PopulateList(m_outputCombo, plugin.m_midiOut, plugin.m_outputDevice, false); + CheckDlgButton(IDC_CHECK1, plugin.m_sendTimingInfo ? BST_CHECKED : BST_UNCHECKED); + m_locked = false; + return CAbstractVstEditor::OpenEditor(parent); +} + + +// Update lists of available input / output devices +void MidiInOutEditor::PopulateList(CComboBox &combo, RtMidi &rtDevice, MidiDevice &midiDevice, bool isInput) +{ + combo.SetRedraw(FALSE); + combo.ResetContent(); + + // Add dummy device + combo.SetItemData(combo.AddString(_T("<none>")), static_cast<DWORD_PTR>(MidiInOut::kNoDevice)); + + // Go through all RtMidi devices + auto ports = rtDevice.getPortCount(); + int selectedItem = 0; + CString portName; + for(unsigned int i = 0; i < ports; i++) + { + try + { + portName = theApp.GetFriendlyMIDIPortName(mpt::ToCString(mpt::Charset::UTF8, midiDevice.GetPortName(i)), isInput); + int result = combo.AddString(portName); + combo.SetItemData(result, i); + + if(result != CB_ERR && i == midiDevice.index) + selectedItem = result; + } catch(RtMidiError &) + { + } + } + + combo.SetCurSel(selectedItem); + combo.SetRedraw(TRUE); +} + + +// Refresh current input / output device in GUI +void MidiInOutEditor::SetCurrentDevice(CComboBox &combo, MidiDevice::ID device) +{ + int items = combo.GetCount(); + for(int i = 0; i < items; i++) + { + if(static_cast<MidiDevice::ID>(combo.GetItemData(i)) == device) + { + combo.SetCurSel(i); + break; + } + } +} + + +static void IOChanged(MidiInOut &plugin, CComboBox &combo, PlugParamIndex param) +{ + // Update device ID and notify plugin. + MidiDevice::ID newDevice = static_cast<MidiDevice::ID>(combo.GetItemData(combo.GetCurSel())); + plugin.SetParameter(param, MidiInOut::DeviceIDToParameter(newDevice)); + plugin.AutomateParameter(param); +} + + +void MidiInOutEditor::OnInputChanged() +{ + IOChanged(static_cast<MidiInOut &>(m_VstPlugin), m_inputCombo, MidiInOut::kInputParameter); +} + + +void MidiInOutEditor::OnOutputChanged() +{ + IOChanged(static_cast<MidiInOut &>(m_VstPlugin), m_outputCombo, MidiInOut::kOutputParameter); +} + + +void MidiInOutEditor::OnLatencyChanged() +{ + MidiInOut &plugin = static_cast<MidiInOut &>(m_VstPlugin); + double latency = 0.0; + if(!m_locked && m_latencyEdit.GetDecimalValue(latency)) + { + plugin.m_latency = latency * (1.0 / 1000.0); + plugin.SetModified(); + } +} + + +void MidiInOutEditor::OnTimingMessagesChanged() +{ + if(!m_locked) + { + MidiInOut &plugin = static_cast<MidiInOut &>(m_VstPlugin); + plugin.m_sendTimingInfo = IsDlgButtonChecked(IDC_CHECK1) != BST_UNCHECKED; + plugin.SetModified(); + } +} + + +OPENMPT_NAMESPACE_END + +#endif // MODPLUG_TRACKER diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.h b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.h new file mode 100644 index 00000000..1ac7cdf9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/MidiInOutEditor.h @@ -0,0 +1,66 @@ +/* + * MidiInOutEditor.h + * ----------------- + * Purpose: Editor interface for the MidiInOut plugin. + * Notes : (currently none) + * Authors: Johannes Schultz (OpenMPT Devs) + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#ifdef MODPLUG_TRACKER + +#include "../AbstractVstEditor.h" +#include "../CDecimalSupport.h" + +OPENMPT_NAMESPACE_BEGIN + +class MidiInOut; + +class MidiInOutEditor : public CAbstractVstEditor +{ +protected: + CComboBox m_inputCombo, m_outputCombo; + CNumberEdit m_latencyEdit; + CSpinButtonCtrl m_latencySpin; + bool m_locked = true; + +public: + + MidiInOutEditor(MidiInOut &plugin); + + // Refresh current input / output device in GUI + void SetCurrentDevice(bool asInputDevice, MidiDevice::ID device) + { + CComboBox &combo = asInputDevice ? m_inputCombo : m_outputCombo; + SetCurrentDevice(combo, device); + } + + bool OpenEditor(CWnd *parent) override; + bool IsResizable() const override { return false; } + bool SetSize(int, int) override { return false; } + +protected: + + // Update lists of available input / output devices + static void PopulateList(CComboBox &combo, RtMidi &rtDevice, MidiDevice &midiDevice, bool isInput); + // Refresh current input / output device in GUI + void SetCurrentDevice(CComboBox &combo, MidiDevice::ID device); + + void DoDataExchange(CDataExchange* pDX) override; + + afx_msg void OnInputChanged(); + afx_msg void OnOutputChanged(); + afx_msg void OnLatencyChanged(); + afx_msg void OnTimingMessagesChanged(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END + +#endif // MODPLUG_TRACKER diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstDefinitions.h b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstDefinitions.h new file mode 100644 index 00000000..974672d1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstDefinitions.h @@ -0,0 +1,925 @@ +/* + * VstDefinitions.h + * ---------------- + * Purpose: Definition of all VST-related constants, function prototypes and structures. + * Notes : Based on BeRo's independent VST header, a clean-room implementation based + * on several third-party information sources. + * The original header, licensed under the Zlib license, can be found at + * https://github.com/BeRo1985/br808/blob/master/VSTi/VST/VST.pas + * Authors: OpenMPT Devs + * Benjamin "BeRo" Rosseaux + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#if MPT_OS_WINDOWS +#define VSTCALLBACK __cdecl +#else +#define VSTCALLBACK +#endif + +namespace Vst +{ + +inline constexpr int32 kVstVersion = 2400; + +enum VstStringLengths +{ + kVstMaxProgNameLen = 24, + kVstMaxParamStrLen = 8, + kVstMaxVendorStrLen = 64, + kVstMaxProductStrLen = 64, + kVstMaxEffectNameLen = 32, + + kVstMaxNameLen = 64, + kVstMaxLabelLen = 64, + kVstMaxShortLabelLen = 8, + kVstMaxCategLabelLen = 24, + kVstMaxFileNameLen = 100, +}; + +enum VstFlags : int32 +{ + effFlagsHasEditor = 1 << 0, + effFlagsHasClip = 1 << 1, + effFlagsHasVu = 1 << 2, + effFlagsCanMono = 1 << 3, + effFlagsCanReplacing = 1 << 4, + effFlagsProgramChunks = 1 << 5, + effFlagsIsSynth = 1 << 8, + effFlagsNoSoundInStop = 1 << 9, + effFlagsExtIsAsync = 1 << 10, + effFlagsExtHasBuffer = 1 << 11, + effFlagsCanDoubleReplacing = 1 << 12, +}; + +enum VstOpcodeToPlugin : int32 +{ + effOpen = 0, + effClose = 1, + effSetProgram = 2, + effGetProgram = 3, + effSetProgramName = 4, + effGetProgramName = 5, + effGetParamLabel = 6, + effGetParamDisplay = 7, + effGetParamName = 8, + effGetVu = 9, + effSetSampleRate = 10, + effSetBlockSize = 11, + effMainsChanged = 12, + effEditGetRect = 13, + effEditOpen = 14, + effEditClose = 15, + effEditDraw = 16, + effEditMouse = 17, + effEditKey = 18, + effEditIdle = 19, + effEditTop = 20, + effEditSleep = 21, + effIdentify = 22, + effGetChunk = 23, + effSetChunk = 24, + effProcessEvents = 25, + effCanBeAutomated = 26, + effString2Parameter = 27, + effGetNumProgramCategories = 28, + effGetProgramNameIndexed = 29, + effCopyProgram = 30, + effConnectInput = 31, + effConnectOutput = 32, + effGetInputProperties = 33, + effGetOutputProperties = 34, + effGetPlugCategory = 35, + effGetCurrentPosition = 36, + effGetDestinationBuffer = 37, + effOfflineNotify = 38, + effOfflinePrepare = 39, + effOfflineRun = 40, + effProcessVarIo = 41, + effSetSpeakerArrangement = 42, + effSetBlockSizeAndSampleRate = 43, + effSetBypass = 44, + effGetEffectName = 45, + effGetErrorText = 46, + effGetVendorString = 47, + effGetProductString = 48, + effGetVendorVersion = 49, + effVendorSpecific = 50, + effCanDo = 51, + effGetTailSize = 52, + effIdle = 53, + effGetIcon = 54, + effSetViewPosition = 55, + effGetParameterProperties = 56, + effKeysRequired = 57, + effGetVstVersion = 58, + effEditKeyDown = 59, + effEditKeyUp = 60, + effSetEditKnobMode = 61, + effGetMidiProgramName = 62, + effGetCurrentMidiProgram = 63, + effGetMidiProgramCategory = 64, + effHasMidiProgramsChanged = 65, + effGetMidiKeyName = 66, + effBeginSetProgram = 67, + effEndSetProgram = 68, + effGetSpeakerArrangement = 69, + effShellGetNextPlugin = 70, + effStartProcess = 71, + effStopProcess = 72, + effSetTotalSampleToProcess = 73, + effSetPanLaw = 74, + effBeginLoadBank = 75, + effBeginLoadProgram = 76, + effSetProcessPrecision = 77, + effGetNumMidiInputChannels = 78, + effGetNumMidiOutputChannels = 79, +}; + +enum VstOpcodeToHost : int32 +{ + audioMasterAutomate = 0, + audioMasterVersion = 1, + audioMasterCurrentId = 2, + audioMasterIdle = 3, + audioMasterPinConnected = 4, + audioMasterWantMidi = 6, + audioMasterGetTime = 7, + audioMasterProcessEvents = 8, + audioMasterSetTime = 9, + audioMasterTempoAt = 10, + audioMasterGetNumAutomatableParameters = 11, + audioMasterGetParameterQuantization = 12, + audioMasterIOChanged = 13, + audioMasterNeedIdle = 14, + audioMasterSizeWindow = 15, + audioMasterGetSampleRate = 16, + audioMasterGetBlockSize = 17, + audioMasterGetInputLatency = 18, + audioMasterGetOutputLatency = 19, + audioMasterGetPreviousPlug = 20, + audioMasterGetNextPlug = 21, + audioMasterWillReplaceOrAccumulate = 22, + audioMasterGetCurrentProcessLevel = 23, + audioMasterGetAutomationState = 24, + audioMasterOfflineStart = 25, + audioMasterOfflineRead = 26, + audioMasterOfflineWrite = 27, + audioMasterOfflineGetCurrentPass = 28, + audioMasterOfflineGetCurrentMetaPass = 29, + audioMasterSetOutputSampleRate = 30, + audioMasterGetOutputSpeakerArrangement = 31, + audioMasterGetVendorString = 32, + audioMasterGetProductString = 33, + audioMasterGetVendorVersion = 34, + audioMasterVendorSpecific = 35, + audioMasterSetIcon = 36, + audioMasterCanDo = 37, + audioMasterGetLanguage = 38, + audioMasterOpenWindow = 39, + audioMasterCloseWindow = 40, + audioMasterGetDirectory = 41, + audioMasterUpdateDisplay = 42, + audioMasterBeginEdit = 43, + audioMasterEndEdit = 44, + audioMasterOpenFileSelector = 45, + audioMasterCloseFileSelector = 46, + audioMasterEditFile = 47, + audioMasterGetChunkFile = 48, + audioMasterGetInputSpeakerArrangement = 49, +}; + +enum VstEventTypes : int32 +{ + kVstMidiType = 1, + kVstAudioType = 2, + kVstVideoType = 3, + kVstParameterType = 4, + kVstTriggerType = 5, + kVstSysExType = 6, +}; + +enum VstEventFlags : int32 +{ + kVstMidiEventIsRealtime = 1 << 0, +}; + +enum VstTimeInfoFlags : int32 +{ + kVstTransportChanged = 1, + kVstTransportPlaying = 1 << 1, + kVstTransportCycleActive = 1 << 2, + kVstTransportRecording = 1 << 3, + kVstAutomationWriting = 1 << 6, + kVstAutomationReading = 1 << 7, + kVstNanosValid = 1 << 8, + kVstPpqPosValid = 1 << 9, + kVstTempoValid = 1 << 10, + kVstBarsValid = 1 << 11, + kVstCyclePosValid = 1 << 12, + kVstTimeSigValid = 1 << 13, + kVstSmpteValid = 1 << 14, + kVstClockValid = 1 << 15, +}; + +enum VstSmpteFrameRate : int32 +{ + kVstSmpte24fps = 0, + kVstSmpte25fps = 1, + kVstSmpte2997fps = 2, + kVstSmpte30fps = 3, + kVstSmpte2997dfps = 4, + kVstSmpte30dfps = 5, + + kVstSmpteFilm16mm = 6, + kVstSmpteFilm35mm = 7, + kVstSmpte239fps = 10, + kVstSmpte249fps = 11, + kVstSmpte599fps = 12, + kVstSmpte60fps = 13, +}; + +enum VstLanguage : int32 +{ + kVstLangEnglish = 1, + kVstLangGerman = 2, + kVstLangFrench = 3, + kVstLangItalian = 4, + kVstLangSpanish = 5, + kVstLangJapanese = 6, +}; + +enum VstProcessPrecision : int32 +{ + kVstProcessPrecision32 = 0, + kVstProcessPrecision64 = 1, +}; + +enum VstParameterFlags : int32 +{ + kVstParameterIsSwitch = 1 << 0, + kVstParameterUsesIntegerMinMax = 1 << 1, + kVstParameterUsesFloatStep = 1 << 2, + kVstParameterUsesIntStep = 1 << 3, + kVstParameterSupportsDisplayIndex = 1 << 4, + kVstParameterSupportsDisplayCategory = 1 << 5, + kVstParameterCanRamp = 1 << 6, +}; + +enum VstPinPropertiesFlags : int32 +{ + kVstPinIsActive = 1 << 0, + kVstPinIsStereo = 1 << 1, + kVstPinUseSpeaker = 1 << 2, +}; + +enum VstPlugCategory : int32 +{ + kPlugCategUnknown = 0, + kPlugCategEffect = 1, + kPlugCategSynth = 2, + kPlugCategAnalysis = 3, + kPlugCategMastering = 4, + kPlugCategSpacializer = 5, + kPlugCategRoomFx = 6, + kPlugSurroundFx = 7, + kPlugCategRestoration = 8, + kPlugCategOfflineProcess = 9, + kPlugCategShell = 10, + kPlugCategGenerator = 11, + kPlugCategMaxCount = 12, +}; + +enum VstMidiProgramNameFlags : int32 +{ + kMidiIsOmni = 1, +}; + +enum VstSpeakerType : int32 +{ + kSpeakerUndefined = 0x7FFFFFFF, + kSpeakerM = 0, + kSpeakerL = 1, + kSpeakerR = 2, + kSpeakerC = 3, + kSpeakerLfe = 4, + kSpeakerLs = 5, + kSpeakerRs = 6, + kSpeakerLc = 7, + kSpeakerRc = 8, + kSpeakerS = 9, + kSpeakerCs = kSpeakerS, + kSpeakerSl = 10, + kSpeakerSr = 11, + kSpeakerTm = 12, + kSpeakerTfl = 13, + kSpeakerTfc = 14, + kSpeakerTfr = 15, + kSpeakerTrl = 16, + kSpeakerTrc = 17, + kSpeakerTrr = 18, + kSpeakerLfe2 = 19, + + kSpeakerU32 = -32, + kSpeakerU31 = -31, + kSpeakerU30 = -30, + kSpeakerU29 = -29, + kSpeakerU28 = -28, + kSpeakerU27 = -27, + kSpeakerU26 = -26, + kSpeakerU25 = -25, + kSpeakerU24 = -24, + kSpeakerU23 = -23, + kSpeakerU22 = -22, + kSpeakerU21 = -21, + kSpeakerU20 = -20, + kSpeakerU19 = -19, + kSpeakerU18 = -18, + kSpeakerU17 = -17, + kSpeakerU16 = -16, + kSpeakerU15 = -15, + kSpeakerU14 = -14, + kSpeakerU13 = -13, + kSpeakerU12 = -12, + kSpeakerU11 = -11, + kSpeakerU10 = -10, + kSpeakerU9 = -9, + kSpeakerU8 = -8, + kSpeakerU7 = -7, + kSpeakerU6 = -6, + kSpeakerU5 = -5, + kSpeakerU4 = -4, + kSpeakerU3 = -3, + kSpeakerU2 = -2, + kSpeakerU1 = -1, +}; + +enum VstSpeakerArrangementType : int32 +{ + kSpeakerArrUserDefined = -2, + kSpeakerArrEmpty = -1, + kSpeakerArrMono = 0, + kSpeakerArrStereo = 1, + kSpeakerArrStereoSurround = 2, + kSpeakerArrStereoCenter = 3, + kSpeakerArrStereoSide = 4, + kSpeakerArrStereoCLfe = 5, + kSpeakerArr30Cine = 6, + kSpeakerArr30Music = 7, + kSpeakerArr31Cine = 8, + kSpeakerArr31Music = 9, + kSpeakerArr40Cine = 10, + kSpeakerArr40Music = 11, + kSpeakerArr41Cine = 12, + kSpeakerArr41Music = 13, + kSpeakerArr50 = 14, + kSpeakerArr51 = 15, + kSpeakerArr60Cine = 16, + kSpeakerArr60Music = 17, + kSpeakerArr61Cine = 18, + kSpeakerArr61Music = 19, + kSpeakerArr70Cine = 20, + kSpeakerArr70Music = 21, + kSpeakerArr71Cine = 22, + kSpeakerArr71Music = 23, + kSpeakerArr80Cine = 24, + kSpeakerArr80Music = 25, + kSpeakerArr81Cine = 26, + kSpeakerArr81Music = 27, + kSpeakerArr102 = 28, + kNumSpeakerArr = 29, +}; + +enum VstOfflineTaskFlags : int32 +{ + kVstOfflineUnvalidParameter = 1 << 0, + kVstOfflineNewFile = 1 << 1, + + kVstOfflinePlugError = 1 << 10, + kVstOfflineInterleavedAudio = 1 << 11, + kVstOfflineTempOutputFile = 1 << 12, + kVstOfflineFloatOutputFile = 1 << 13, + kVstOfflineRandomWrite = 1 << 14, + kVstOfflineStretch = 1 << 15, + kVstOfflineNoThread = 1 << 16, +}; + +enum VstOfflineOption : int32 +{ + kVstOfflineAudio = 0, + kVstOfflinePeaks = 1, + kVstOfflineParameter = 2, + kVstOfflineMarker = 3, + kVstOfflineCursor = 4, + kVstOfflineSelection = 5, + kVstOfflineQueryFiles = 6, +}; + +enum VstAudioFileFlags : int32 +{ + kVstOfflineReadOnly = 1 << 0, + kVstOfflineNoRateConversion = 1 << 1, + kVstOfflineNoChannelChange = 1 << 2, + + kVstOfflineCanProcessSelection = 1 << 10, + kVstOfflineNoCrossfade = 1 << 11, + kVstOfflineWantRead = 1 << 12, + kVstOfflineWantWrite = 1 << 13, + kVstOfflineWantWriteMarker = 1 << 14, + kVstOfflineWantMoveCursor = 1 << 15, + kVstOfflineWantSelect = 1 << 16, +}; + +enum VstVirtualKey : uint8 +{ + VKEY_BACK = 1, + VKEY_TAB = 2, + VKEY_CLEAR = 3, + VKEY_RETURN = 4, + VKEY_PAUSE = 5, + VKEY_ESCAPE = 6, + VKEY_SPACE = 7, + VKEY_NEXT = 8, + VKEY_END = 9, + VKEY_HOME = 10, + + VKEY_LEFT = 11, + VKEY_UP = 12, + VKEY_RIGHT = 13, + VKEY_DOWN = 14, + VKEY_PAGEUP = 15, + VKEY_PAGEDOWN = 16, + + VKEY_SELECT = 17, + VKEY_PRINT = 18, + VKEY_ENTER = 19, + VKEY_SNAPSHOT = 20, + VKEY_INSERT = 21, + VKEY_DELETE = 22, + VKEY_HELP = 23, + VKEY_NUMPAD0 = 24, + VKEY_NUMPAD1 = 25, + VKEY_NUMPAD2 = 26, + VKEY_NUMPAD3 = 27, + VKEY_NUMPAD4 = 28, + VKEY_NUMPAD5 = 29, + VKEY_NUMPAD6 = 30, + VKEY_NUMPAD7 = 31, + VKEY_NUMPAD8 = 32, + VKEY_NUMPAD9 = 33, + VKEY_MULTIPLY = 34, + VKEY_ADD = 35, + VKEY_SEPARATOR = 36, + VKEY_SUBTRACT = 37, + VKEY_DECIMAL = 38, + VKEY_DIVIDE = 39, + VKEY_F1 = 40, + VKEY_F2 = 41, + VKEY_F3 = 42, + VKEY_F4 = 43, + VKEY_F5 = 44, + VKEY_F6 = 45, + VKEY_F7 = 46, + VKEY_F8 = 47, + VKEY_F9 = 48, + VKEY_F10 = 49, + VKEY_F11 = 50, + VKEY_F12 = 51, + VKEY_NUMLOCK = 52, + VKEY_SCROLL = 53, + + VKEY_SHIFT = 54, + VKEY_CONTROL = 55, + VKEY_ALT = 56, + + VKEY_EQUALS = 57, +}; + +enum VstModifierKey : uint8 +{ + MODIFIER_SHIFT = 1 << 0, + MODIFIER_ALTERNATE = 1 << 1, + MODIFIER_COMMAND = 1 << 2, + MODIFIER_CONTROL = 1 << 3, +}; + +enum VstFileSelectCommand : int32 +{ + kVstFileLoad = 0, + kVstFileSave = 1, + kVstMultipleFilesLoad = 2, + kVstDirectorySelect = 3, +}; + +enum VstFileSelectType : int32 +{ + kVstFileType = 0, +}; + +enum VstPanLaw : int32 +{ + kLinearPanLaw = 0, + kEqualPowerPanLaw = 1, +}; + +enum VstProcessLevel : int32 +{ + kVstProcessLevelUnknown = 0, + kVstProcessLevelUser = 1, + kVstProcessLevelRealtime = 2, + kVstProcessLevelPrefetch = 3, + kVstProcessLevelOffline = 4, +}; + +enum VstAutomationState : int32 +{ + kVstAutomationUnsupported = 0, + kVstAutomationOff = 1, + kVstAutomationRead = 2, + kVstAutomationWrite = 3, + kVstAutomationReadWrite = 4, +}; + + +namespace HostCanDo +{ +inline constexpr char sendVstEvents[] = "sendVstEvents"; +inline constexpr char sendVstMidiEvent[] = "sendVstMidiEvent"; +inline constexpr char sendVstTimeInfo[] = "sendVstTimeInfo"; +inline constexpr char receiveVstEvents[] = "receiveVstEvents"; +inline constexpr char receiveVstMidiEvent[] = "receiveVstMidiEvent"; +inline constexpr char reportConnectionChanges[] = "reportConnectionChanges"; +inline constexpr char acceptIOChanges[] = "acceptIOChanges"; +inline constexpr char sizeWindow[] = "sizeWindow"; +inline constexpr char asyncProcessing[] = "asyncProcessing"; +inline constexpr char ofline[] = "offline"; +inline constexpr char supplyIdle[] = "supplyIdle"; +inline constexpr char supportShell[] = "supportShell"; +inline constexpr char openFileSelector[] = "openFileSelector"; +inline constexpr char closeFileSelector[] = "closeFileSelector"; +inline constexpr char startStopProcess[] = "startStopProcess"; +inline constexpr char shellCategory[] = "shellCategory"; +inline constexpr char editFile[] = "editFile"; +inline constexpr char sendVstMidiEventFlagIsRealtime[] = "sendVstMidiEventFlagIsRealtime"; +} // namespace HostCanDo + +namespace PluginCanDo +{ +inline constexpr char sendVstEvents[] = "sendVstEvents"; +inline constexpr char sendVstMidiEvent[] = "sendVstMidiEvent"; +inline constexpr char receiveVstEvents[] = "receiveVstEvents"; +inline constexpr char receiveVstMidiEvent[] = "receiveVstMidiEvent"; +inline constexpr char receiveVstTimeInfo[] = "receiveVstTimeInfo"; +inline constexpr char offline[] = "offline"; +inline constexpr char midiProgramNames[] = "midiProgramNames"; +inline constexpr char bypass[] = "bypass"; +inline constexpr char MPE[] = "MPE"; +} // namespace PluginCanDo + + +struct AEffect; +typedef intptr_t(VSTCALLBACK *AudioMasterCallbackFunc)(AEffect *effect, VstOpcodeToHost opcode, int32 index, intptr_t value, void *ptr, float opt); +typedef intptr_t(VSTCALLBACK *DispatcherFunc)(AEffect *effect, VstOpcodeToPlugin opcode, int32 index, intptr_t value, void *ptr, float opt); +typedef void(VSTCALLBACK *ProcessProc)(AEffect *effect, float **inputs, float **outputs, int32 sampleFrames); +typedef void(VSTCALLBACK *ProcessDoubleProc)(AEffect *effect, double **inputs, double **outputs, int32 sampleFrames); +typedef void(VSTCALLBACK *SetParameterProc)(AEffect *effect, int32 index, float parameter); +typedef float(VSTCALLBACK *GetParameterFunc)(AEffect *effect, int32 index); +typedef AEffect *(VSTCALLBACK *MainProc)(AudioMasterCallbackFunc audioMaster); + +#pragma pack(push, 8) + +struct AEffect +{ + int32 magic; + DispatcherFunc dispatcher; + ProcessProc process; + SetParameterProc setParameter; + GetParameterFunc getParameter; + int32 numPrograms; + uint32 numParams; + int32 numInputs; + int32 numOutputs; + VstFlags flags; + void *reservedForHost1; + void *reservedForHost2; + int32 initialDelay; + int32 realQualities; + int32 offQualities; + float ioRatio; + void *object; + void *user; + int32 uniqueID; + int32 version; + ProcessProc processReplacing; + ProcessDoubleProc processDoubleReplacing; +}; + +struct ERect +{ + int16 top, left, bottom, right; + + int16 Width() const { return right - left; } + int16 Height() const { return bottom - top; } +}; + +struct VstEvent +{ + VstEventTypes type; + int32 byteSize; + int32 deltaFrames; + VstEventFlags flags; +}; + +struct VstEvents +{ + static constexpr size_t MAX_EVENTS = 256; + + int32 numEvents; + intptr_t reserved; + VstEvent *events[MAX_EVENTS]; + + size_t size() { return numEvents; } + auto begin() { return std::begin(events); } + auto end() { return std::begin(events) + numEvents; } + auto begin() const { return std::begin(events); } + auto end() const { return std::begin(events) + numEvents; } + auto cbegin() const { return std::cbegin(events); } + auto cend() const { return std::cbegin(events) + numEvents; } +}; + +struct VstMidiEvent : public VstEvent +{ + int32 noteLength; + int32 noteOffset; + uint32 midiData; + int8 detune; + int8 noteOffVelocity; + int8 reserved1; + int8 reserved2; +}; + +struct VstMidiSysexEvent : public VstEvent +{ + int32 dumpBytes; + intptr_t reserved1; + const std::byte *sysexDump; + intptr_t reserved2; +}; + +struct VstTimeInfo +{ + double samplePos; + double sampleRate; + double nanoSeconds; + double ppqPos; + double tempo; + double barStartPos; + double cycleStartPos; + double cycleEndPos; + int32 timeSigNumerator; + int32 timeSigDenominator; + int32 smpteOffset; + VstSmpteFrameRate smpteFrameRate; + int32 samplesToNextClock; + VstTimeInfoFlags flags; +}; + +struct VstVariableIo +{ + float **inputs; + float **outputs; + int32 numSamplesInput; + int32 numSamplesOutput; + int32 *numSamplesInputProcessed; + int32 *numSamplesOutputProcessed; +}; + +struct VstParameterProperties +{ + float stepFloat; + float smallStepFloat; + float largeStepFloat; + char label[kVstMaxLabelLen]; + VstParameterFlags flags; + int32 minInteger; + int32 maxInteger; + int32 stepInteger; + int32 largeStepInteger; + char shortLabel[kVstMaxShortLabelLen]; + int16 displayIndex; + int16 category; + int16 numParametersInCategory; + int16 reserved; + char categoryLabel[kVstMaxCategLabelLen]; + int8 reserved2[16]; +}; + +struct VstPinProperties +{ + char label[kVstMaxLabelLen]; + VstPinPropertiesFlags flags; + VstSpeakerArrangementType arragementType; + char shortLabel[kVstMaxShortLabelLen]; + int8 reserved[48]; +}; + +struct MidiProgramName +{ + int32 thisProgramIndex; + char name[kVstMaxNameLen]; + int8 midiProgram; + int8 midiBankMSB; + int8 midiBankLSB; + int8 reserved; + int32 parentCategoryIndex; + VstMidiProgramNameFlags flags; +}; + +struct MidiProgramCategory +{ + int32 thisCategoryIndex; + char name[kVstMaxNameLen]; + int32 parentCategoryIndex; + int32 flags; +}; + +struct MidiKeyName +{ + int32 thisProgramIndex; + int32 thisKeyNumber; + char keyName[kVstMaxNameLen]; + int32 reserved; + int32 flags; +}; + +struct VstSpeakerProperties +{ + float azimuth; + float elevation; + float radius; + float reserved1; + char name[kVstMaxNameLen]; + VstSpeakerType type; + int8 reserved2[28]; +}; + +struct VstSpeakerArrangement +{ + VstSpeakerArrangementType type; + int32 numChannels; + VstSpeakerProperties speakers[8]; +}; + +struct VstOfflineTask +{ + char processName[96]; + double readPosition; + double writePosition; + int32 readCount; + int32 writeCount; + int32 sizeInputBuffer; + int32 sizeOutputBuffer; + void *inputBuffer; + void *outputBuffer; + double positionToProcessFrom; + double numFramesToProcess; + double maxFramesToWrite; + void *extraBuffer; + int32 value; + int32 index; + double numFramesInSourceFile; + double sourceSampleRate; + double destinationSampleRate; + int32 numSourceChannels; + int32 numDestinationChannels; + int32 sourceFormat; + int32 destinationFormat; + char outputText[512]; + double progress; + int32 progressMode; + char progressText[100]; + VstOfflineTaskFlags flags; + int32 returnValue; + void *hostOwned; + void *plugOwned; + int8 reserved[1024]; +}; + +struct VstAudioFile +{ + VstAudioFileFlags flags; + void *hostOwned; + void *plugOwned; + char name[kVstMaxFileNameLen]; + int32 uniqueID; + double sampleRate; + int32 numChannels; + double numFrames; + int32 format; + double editCursorPosition; + double selectionStart; + double selectionSize; + int32 selectedChannelsMask; + int32 numMarkers; + int32 timeRulerUnit; + double timeRulerOffset; + double tempo; + int32 timeSigNumerator; + int32 timeSigDenominator; + int32 ticksPerBlackNote; + int32 smtpeFrameRate; + int8 reserved[64]; +}; + +struct VstAudioFileMarker +{ + double position; + char name[32]; + int32 type; + int32 id; + int32 reserved; +}; + +struct VstWindow +{ + char title[128]; + int16 xPos; + int16 yPos; + int16 width; + int16 height; + int32 style; + void *parent; + void *userHandle; + void *windowHandle; + int8 reserved[104]; +}; + +struct VstKeyCode +{ + int32 characterCode; + VstVirtualKey virtualCode; + VstModifierKey modifierCode; +}; + +struct VstFileType +{ + char name[128]; + char macType[8]; + char dosType[8]; + char unixType[8]; + char mimeType1[128]; + char mimeType2[128]; +}; + +struct VstFileSelect +{ + VstFileSelectCommand command; + VstFileSelectType type; + int32 macCreator; + int32 numFileTypes; + VstFileType *fileTypes; + char title[1024]; + char *initialPath; + char *returnPath; + int32 sizeReturnPath; + char **returnMultiplePaths; + int32 numReturnPaths; + intptr_t reserved; + int8 reserved2[116]; +}; + +struct VstPatchChunkInfo +{ + int32 version; + int32 pluginUniqueID; + int32 pluginVersion; + int32 numElements; + int8 reserved[48]; +}; + +#pragma pack(pop) + +int32 constexpr FourCC(const char (&cc)[5]) +{ + return static_cast<int32>(static_cast<uint32>(cc[3]) | (static_cast<uint32>(cc[2]) << 8) | (static_cast<uint32>(cc[1]) << 16) | (static_cast<uint32>(cc[0]) << 24)); +} + +inline constexpr int32 kEffectMagic = FourCC("VstP"); + +template <typename T> +constexpr T *FromIntPtr(intptr_t v) +{ + return reinterpret_cast<T *>(v); +} + +template <typename T> +constexpr intptr_t ToIntPtr(T *v) +{ + return reinterpret_cast<intptr_t>(v); +} + +} // namespace Vst diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstEventQueue.h b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstEventQueue.h new file mode 100644 index 00000000..4989da27 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/plugins/VstEventQueue.h @@ -0,0 +1,126 @@ +/* + * VstEventQueue.h + * --------------- + * Purpose: Event queue for VST events. + * Notes : Modelled after an idea from https://www.kvraudio.com/forum/viewtopic.php?p=3043807#p3043807 + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include <deque> +#include "mpt/mutex/mutex.hpp" +#include "VstDefinitions.h" + +OPENMPT_NAMESPACE_BEGIN + +class VstEventQueue : public Vst::VstEvents +{ +protected: + + // Although originally all event types were apparently supposed to be of the same size, this is not the case on 64-Bit systems. + // VstMidiSysexEvent contains 3 pointers, so the struct size differs between 32-Bit and 64-Bit systems. + union Event + { + Vst::VstEvent event; + Vst::VstMidiEvent midi; + Vst::VstMidiSysexEvent sysex; + }; + + // Here we store our events which are then inserted into the fixed-size event buffer sent to the plugin. + std::deque<Event> eventQueue; + // Since plugins can also add events to the queue (even from a different thread than the processing thread), + // we need to ensure that reading and writing is never done in parallel. + mpt::mutex criticalSection; + +public: + + VstEventQueue() + { + numEvents = 0; + reserved = 0; + std::fill(std::begin(events), std::end(events), nullptr); + } + + // Get the number of events that are currently queued, but not in the output buffer. + size_t GetNumQueuedEvents() + { + return eventQueue.size() - numEvents; + } + + // Add a VST event to the queue. Returns true on success. + // Set insertFront to true to prioritise this event (i.e. add it at the front of the queue instead of the back) + bool Enqueue(const Vst::VstEvent *event, bool insertFront = false) + { + MPT_ASSERT(event->type != Vst::kVstSysExType || event->byteSize == sizeof(Vst::VstMidiSysexEvent)); + MPT_ASSERT(event->type != Vst::kVstMidiType || event->byteSize == sizeof(Vst::VstMidiEvent)); + + Event copyEvent; + size_t copySize; + // randomid by Insert Piz Here sends events of type kVstMidiType, but with a claimed size of 24 bytes instead of 32. + // Hence, we enforce the size of known events. + if(event->type == Vst::kVstSysExType) + copySize = sizeof(Vst::VstMidiSysexEvent); + else if(event->type == Vst::kVstMidiType) + copySize = sizeof(Vst::VstMidiEvent); + else + copySize = std::min(size_t(event->byteSize), sizeof(copyEvent)); + memcpy(©Event, event, copySize); + + if(event->type == Vst::kVstSysExType) + { + // SysEx messages need to be copied, as the space used for the dump might be freed in the meantime. + auto &e = copyEvent.sysex; + auto sysexDump = new (std::nothrow) std::byte[e.dumpBytes]; + if(sysexDump == nullptr) + return false; + memcpy(sysexDump, e.sysexDump, e.dumpBytes); + e.sysexDump = sysexDump; + } + + mpt::lock_guard<mpt::mutex> lock(criticalSection); + if(insertFront) + eventQueue.push_front(copyEvent); + else + eventQueue.push_back(copyEvent); + return true; + } + + // Set up the queue for transmitting to the plugin. Returns number of elements that are going to be transmitted. + int32 Finalise() + { + mpt::lock_guard<mpt::mutex> lock(criticalSection); + numEvents = static_cast<int32>(std::min(eventQueue.size(), MAX_EVENTS)); + for(int32 i = 0; i < numEvents; i++) + { + events[i] = &eventQueue[i].event; + } + return numEvents; + } + + // Remove transmitted events from the queue + void Clear() + { + mpt::lock_guard<mpt::mutex> lock(criticalSection); + if(numEvents) + { + // Release temporarily allocated buffer for SysEx messages + for(auto e = eventQueue.begin(); e != eventQueue.begin() + numEvents; ++e) + { + if(e->event.type == Vst::kVstSysExType) + { + delete[] e->sysex.sysexDump; + } + } + eventQueue.erase(eventQueue.begin(), eventQueue.begin() + numEvents); + numEvents = 0; + } + } + +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/About.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/About.png Binary files differnew file mode 100644 index 00000000..c319cc45 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/About.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/COLORS.BMP b/Src/external_dependencies/openmpt-trunk/mptrack/res/COLORS.BMP Binary files differnew file mode 100644 index 00000000..dd8fb44b --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/COLORS.BMP diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/DRAGGING.CUR b/Src/external_dependencies/openmpt-trunk/mptrack/res/DRAGGING.CUR Binary files differnew file mode 100644 index 00000000..9752a686 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/DRAGGING.CUR diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.ICO b/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.ICO Binary files differnew file mode 100644 index 00000000..4c45c0fe --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.ICO diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.RC2 b/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.RC2 new file mode 100644 index 00000000..48b0fa6c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/MPTRACK.RC2 @@ -0,0 +1,76 @@ +// +// MPTRACK.RC2 - resources Microsoft Visual C++ does not edit directly +// + +#ifdef APSTUDIO_INVOKED + #error this file is not editable by Microsoft Visual C++ +#endif //APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// Add manually edited resources here... + +#include "../common/versionNumber.h" +#include <winver.h> + +#define VER_HELPER_STRINGIZE(x) #x +#define VER_STRINGIZE(x) VER_HELPER_STRINGIZE(x) + +#define VER_FILEVERSION VER_MAJORMAJOR,VER_MAJOR,VER_MINOR,VER_MINORMINOR +#define VER_FILEVERSION_DOT VER_MAJORMAJOR.VER_MAJOR.VER_MINOR.VER_MINORMINOR +#define VER_FILEVERSION_STR VER_STRINGIZE(VER_FILEVERSION_DOT) + +#ifdef _DEBUG + #define VER_DEBUG VS_FF_DEBUG +#else + #define VER_DEBUG 0 +#endif + +#define MAKEHEX(c) 0x##c // Avoid issues with version numbers 08 and 09 being interpreted as octal +#if (MAKEHEX(VER_MINOR) == 0) || (MAKEHEX(VER_MINORMINOR) != 0) + #define VER_PRERELEASE VS_FF_PRERELEASE +#else + #define VER_PRERELEASE 0 +#endif +#undef MAKEHEX + +// Note: Changing this might need changes to BLOCK "StringFileInfo" defined below. +#define VER_FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#define VER_FILEFLAGS (VER_DEBUG|VER_PRERELEASE) + +#ifdef VER_ARCHNAME +#define VER_FILEDESCRIPTION "OpenMPT (" VER_ARCHNAME ")" +#else +#define VER_FILEDESCRIPTION "OpenMPT" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VER_FILEVERSION + PRODUCTVERSION VER_FILEVERSION + FILEFLAGSMASK VER_FILEFLAGSMASK + FILEFLAGS VER_FILEFLAGS + FILEOS VOS_NT_WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904b0" + BEGIN + VALUE "CompanyName", "OpenMPT (https://openmpt.org)" + VALUE "FileDescription", VER_FILEDESCRIPTION + VALUE "FileVersion", VER_FILEVERSION_STR + VALUE "InternalName", "OpenMPT" + VALUE "LegalCopyright", "Copyright © 2004-2022 OpenMPT Project Developers and Contributors, Copyright © 1997-2003 Olivier Lapicque" + VALUE "OriginalFilename", "OpenMPT.exe" + VALUE "ProductName", "OpenMPT" + VALUE "ProductVersion", VER_FILEVERSION_STR + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1200 + END +END + +///////////////////////////////////////////////////////////////////////////// diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/NODROP.CUR b/Src/external_dependencies/openmpt-trunk/mptrack/res/NODROP.CUR Binary files differnew file mode 100644 index 00000000..a299255d --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/NODROP.CUR diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win10.manifest b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win10.manifest new file mode 100644 index 00000000..dbe875f0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win10.manifest @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <assemblyIdentity + type="win32" + name="OpenMPT.OpenMPT" + version="1.0.0.0" + /> + <description>OpenMPT / ModPlug Tracker</description> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + </application> + </compatibility> +</assembly> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win7.manifest b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win7.manifest new file mode 100644 index 00000000..16199501 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win7.manifest @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <assemblyIdentity + type="win32" + name="OpenMPT.OpenMPT" + version="1.0.0.0" + /> + <description>OpenMPT / ModPlug Tracker</description> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + <!-- Windows 8 --> + <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> + <!-- Windows 7 --> + <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> + </application> + </compatibility> +</assembly> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win81.manifest b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win81.manifest new file mode 100644 index 00000000..cacaa2b5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/OpenMPT-win81.manifest @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="UTF-8" standalone="yes"?> +<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> + <assemblyIdentity + type="win32" + name="OpenMPT.OpenMPT" + version="1.0.0.0" + /> + <description>OpenMPT / ModPlug Tracker</description> + <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> + <application> + <!-- Windows 10 --> + <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> + <!-- Windows 8.1 --> + <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> + </application> + </compatibility> +</assembly> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/Splash.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/Splash.png Binary files differnew file mode 100644 index 00000000..9afd259e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/Splash.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/defaultKeybindings.mkb b/Src/external_dependencies/openmpt-trunk/mptrack/res/defaultKeybindings.mkb new file mode 100644 index 00000000..267ade7c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/defaultKeybindings.mkb @@ -0,0 +1,486 @@ +//----------------- OpenMPT key binding definition file --------------- +//- Format is: - +//- Context:Command ID:Modifiers:Key:KeypressEventType //Comments - +//---------------------------------------------------------------------- +version:1 + +//----( Global Context )------------ +0:1347:2:78:1 //File/New: Ctrl+N (KeyDown) +0:1346:2:79:1 //File/Open: Ctrl+O (KeyDown) +0:1348:2:87:5 //File/Close: Ctrl+W (KeyDown|KeyHold) +0:1864:3:87:1 //File/Close All: Shift+Ctrl+W (KeyDown) +0:1349:2:83:1 //File/Save: Ctrl+S (KeyDown) +0:1350:3:83:1 //File/Save As: Shift+Ctrl+S (KeyDown) +0:1693:0:166:1 //Previous Document: (KeyDown) +0:1694:0:167:1 //Next Document: (KeyDown) +0:1030:0:116:1 //Play Song / Pause song: F5 (KeyDown) +0:1031:0:119:1 //Pause Song: F8 (KeyDown) +0:1375:0:27:1 //Stop Song: ESC (KeyDown) +0:1029:0:117:1 //Play Song from Start: F6 (KeyDown) +0:1028:2:117:1 //Play Song from Cursor: Ctrl+F6 (KeyDown) +0:1027:0:118:1 //Play Pattern from Start: F7 (KeyDown) +0:1026:2:118:1 //Play Pattern from Cursor: Ctrl+F7 (KeyDown) +0:1376:0:120:1 //Toggle MIDI Record: F9 (KeyDown) +0:1359:2:90:1 //Undo: Ctrl+Z (KeyDown) +0:1905:2:89:1 //Redo: Ctrl+Y (KeyDown) +0:1360:2:88:1 //Cut: Ctrl+X (KeyDown) +0:1361:2:67:1 //Copy: Ctrl+C (KeyDown) +0:1362:2:86:1 //Paste: Ctrl+V (KeyDown) +0:1362:1:45:1 //Paste: Shift+INSERT (KeyDown) +0:1363:3:86:1 //Mix Paste: Shift+Ctrl+V (KeyDown) +0:1793:1:86:5 //Paste Flood: Shift+V (KeyDown|KeyHold) +0:1820:6:86:1 //Push Forward Paste (Insert): Ctrl+Alt+V (KeyDown) +0:1364:2:53:1 //Select All: Ctrl+5 (KeyDown) +0:1365:2:70:1 //Find / Replace: Ctrl+F (KeyDown) +0:1366:0:114:1 //Find Next: F3 (KeyDown) +0:1021:4:71:1 //View General: Alt+G (KeyDown) +0:1022:4:80:1 //View Pattern: Alt+P (KeyDown) +0:1023:4:83:1 //View Samples: Alt+S (KeyDown) +0:1024:4:78:1 //View Instruments: Alt+N (KeyDown) +0:1025:1:120:1 //View Comments: Shift+F9 (KeyDown) +0:1025:4:67:1 //View Comments: Alt+C (KeyDown) +0:1368:2:113:1 //Toggle Tree View: Ctrl+F2 (KeyDown) +0:1369:2:112:1 //View Options: Ctrl+F1 (KeyDown) +0:1781:2:114:1 //View MIDI Mapping: Ctrl+F3 (KeyDown) +0:1966:4:73:5 //Switch To Instrument Library: Alt+I (KeyDown|KeyHold) +0:1370:0:112:1 //Help: F1 (KeyDown) +0:1032:2:111:5 //Previous Instrument: Ctrl+NUM DIVIDE (KeyDown|KeyHold) +0:1032:2:38:5 //Previous Instrument: Ctrl+UP (KeyDown|KeyHold) +0:1033:2:106:5 //Next Instrument: Ctrl+NUMMULT (KeyDown|KeyHold) +0:1033:2:40:5 //Next Instrument: Ctrl+DOWN (KeyDown|KeyHold) +0:1036:0:111:1 //Previous Octave: NUM DIVIDE (KeyDown) +0:1037:0:106:1 //Next Octave: NUMMULT (KeyDown) +0:1034:2:37:5 //Previous Order: Ctrl+LEFT (KeyDown|KeyHold) +0:1035:2:39:5 //Next Order: Ctrl+RIGHT (KeyDown|KeyHold) + +//----( General Context [bottom] )------------ + +//----( Pattern Context [bottom] )------------ +2:1017:0:34:5 //Jump down by measure: PGDOWN (KeyDown|KeyHold) +2:1018:0:33:5 //Jump up by measure: PGUP (KeyDown|KeyHold) +2:1338:4:34:5 //Jump down by beat: Alt+PGDOWN (KeyDown|KeyHold) +2:1339:4:33:5 //Jump up by beat: Alt+PGUP (KeyDown|KeyHold) +2:1340:6:34:5 //Snap down to beat: Ctrl+Alt+PGDOWN (KeyDown|KeyHold) +2:1341:6:33:5 //Snap up to beat: Ctrl+Alt+PGUP (KeyDown|KeyHold) +2:1971:6:38:5 //Jump to previous entry in column: Ctrl+Alt+UP (KeyDown|KeyHold) +2:1972:6:40:5 //Jump to next entry in column: Ctrl+Alt+DOWN (KeyDown|KeyHold) +2:1038:0:40:5 //Navigate down by 1 row: DOWN (KeyDown|KeyHold) +2:1039:0:38:5 //Navigate up by 1 row: UP (KeyDown|KeyHold) +2:1691:4:40:5 //Navigate down by spacing: Alt+DOWN (KeyDown|KeyHold) +2:1692:4:38:5 //Navigate up by spacing: Alt+UP (KeyDown|KeyHold) +2:1040:0:37:5 //Navigate left: LEFT (KeyDown|KeyHold) +2:1041:0:39:5 //Navigate right: RIGHT (KeyDown|KeyHold) +2:1042:0:9:5 //Navigate to next channel: TAB (KeyDown|KeyHold) +2:1043:1:9:5 //Navigate to previous channel: Shift+TAB (KeyDown|KeyHold) +2:1044:0:36:1 //Go to first channel: HOME (KeyDown) +2:1045:2:36:1 //Go to first row: Ctrl+HOME (KeyDown) +2:1046:6:36:1 //Go to first row of first channel: Ctrl+Alt+HOME (KeyDown) +2:1047:0:35:1 //Go to last channel: END (KeyDown) +2:1048:2:35:1 //Go to last row: Ctrl+END (KeyDown) +2:1049:6:35:1 //Go to last row of last channel: Ctrl+Alt+END (KeyDown) +2:1050:1:16:1 //Selection key: Shift (KeyDown) +2:1051:2:17:1 //Copy select key: Ctrl (KeyDown) +2:1011:2:76:1 //Select Channel / Select All: Ctrl+L (KeyDown) +2:1968:3:76:1 //Select Column: Shift+Ctrl+L (KeyDown) +2:1858:2:66:1 //Select Beat: Ctrl+B (KeyDown) +2:1859:3:66:1 //Select Measure: Shift+Ctrl+B (KeyDown) +2:1663:2:3:1 //Toggle follow song: Ctrl+SCROLL LOCK (KeyDown) +2:1663:0:145:1 //Toggle follow song: SCROLL LOCK (KeyDown) +2:1663:0:122:1 //Toggle follow song: F11 (KeyDown) +2:1003:0:13:1 //Quick Copy: ENTER (KeyDown) +2:1004:0:32:5 //Quick Paste: SPACE (KeyDown|KeyHold) +2:1001:2:32:1 //Enable Recording: Ctrl+SPACE (KeyDown) +2:1002:2:13:5 //Play Row: Ctrl+ENTER (KeyDown|KeyHold) +2:1317:4:18:1 //Set edit step on note entry: Alt (KeyDown) +2:1685:2:9:1 //Switch to Order List: Ctrl+TAB (KeyDown) +2:1806:2:68:5 //Duplicate Pattern: Ctrl+D (KeyDown|KeyHold) +2:1836:2:191:1 //Toggle PC Event/instrument plugin editor: Ctrl+/ (KeyDown) +2:1662:6:80:1 //Toggle channel's plugin editor: Ctrl+Alt+P (KeyDown) +2:1062:0:93:1 //Show Note Properties: ANWENDUNG (KeyDown) +2:1772:5:80:1 //Show Pattern Properties: Shift+Alt+P (KeyDown) +2:1819:2:69:1 //Split Keyboard Settings dialog: Ctrl+E (KeyDown) +2:1776:1:122:1 //Toggle Loop Pattern: Shift+F11 (KeyDown) +2:1780:2:80:1 //Show playback time at current row: Ctrl+P (KeyDown) +2:1892:4:81:1 //Quantize Settings: Alt+Q (KeyDown) +2:1900:3:77:1 //Toggle Clipboard Manager: Shift+Ctrl+M (KeyDown) +2:1901:3:37:1 //Cycle to Previous Clipboard: Shift+Ctrl+LEFT (KeyDown) +2:1902:3:39:1 //Cycle to Next Clipboard: Shift+Ctrl+RIGHT (KeyDown) +2:1005:0:121:1 //Mute Current Channel: F10 (KeyDown) +2:1006:2:121:1 //Solo Current Channel: Ctrl+F10 (KeyDown) +2:1771:6:121:1 //Unmute all channels: Ctrl+Alt+F10 (KeyDown) +2:1883:2:49:1 //Channel Record Select: Ctrl+1 (KeyDown) +2:1884:2:50:1 //Channel Split Record Select: Ctrl+2 (KeyDown) +2:1786:2:82:1 //Reset Channel: Ctrl+R (KeyDown) +2:1007:2:81:5 //Transpose +1: Ctrl+Q (KeyDown|KeyHold) +2:1008:2:65:5 //Transpose -1: Ctrl+A (KeyDown|KeyHold) +2:1009:3:81:5 //Transpose +1 Octave: Shift+Ctrl+Q (KeyDown|KeyHold) +2:1010:3:65:5 //Transpose -1 Octave: Shift+Ctrl+A (KeyDown|KeyHold) +2:1881:2:84:1 //Transpose Custom: Ctrl+T (KeyDown) +2:1885:2:107:5 //Data Entry +1: Ctrl+NUM PLUS (KeyDown|KeyHold) +2:1885:2:187:5 //Data Entry +1: Ctrl++ (KeyDown|KeyHold) +2:1886:2:109:5 //Data Entry -1: Ctrl+NUM SUB (KeyDown|KeyHold) +2:1886:2:189:5 //Data Entry -1: Ctrl+- (KeyDown|KeyHold) +2:1893:3:107:1 //Data Entry Up (Coarse): Shift+Ctrl+NUM PLUS (KeyDown) +2:1893:3:187:1 //Data Entry Up (Coarse): Shift+Ctrl++ (KeyDown) +2:1894:3:109:1 //Data Entry Down (Coarse): Shift+NUM SUB (KeyDown) +2:1894:3:189:1 //Data Entry Down (Coarse): Shift+Ctrl+- (KeyDown) +2:1012:2:77:1 //Amplify selection: Ctrl+M (KeyDown) +2:1014:2:74:1 //Interpolate Volume: Ctrl+J (KeyDown) +2:1015:2:75:1 //Interpolate Effect: Ctrl+K (KeyDown) +2:1016:4:66:1 //Open Effect Visualizer: Alt+B (KeyDown) +2:1766:2:71:1 //Go to row/channel/...: Ctrl+G (KeyDown) +2:1013:2:73:1 //Apply current instrument: Ctrl+I (KeyDown) +2:1660:4:69:5 //Grow selection: Alt+E (KeyDown|KeyHold) +2:1661:4:68:5 //Shrink selection: Alt+D (KeyDown|KeyHold) +2:1058:0:46:1 //Clear field: DELETE (KeyDown) +2:1664:1:190:1 //Clear field (IT Style): Shift+. (KeyDown) +2:1059:2:46:5 //Clear row and step: Ctrl+DELETE (KeyDown|KeyHold) +2:1665:1:46:5 //Clear field and step (IT Style): Shift+DELETE (KeyDown|KeyHold) +2:1061:0:8:5 //Delete Row(s): BACKSPACE (KeyDown|KeyHold) +2:1377:2:8:5 //Delete Row(s) (All Channels): Ctrl+BACKSPACE (KeyDown|KeyHold) +2:2002:4:8:5 //Delete Row(s) (Global): Alt+BACKSPACE (KeyDown|KeyHold) +2:2003:6:8:5 //Delete Row(s) (All Channels, Global): Ctrl+Alt+BACKSPACE (KeyDown|KeyHold) +2:1378:0:45:5 //Insert Row(s): INSERT (KeyDown|KeyHold) +2:1379:2:45:5 //Insert Row(s) (All Channels): Ctrl+INSERT (KeyDown|KeyHold) +2:2004:4:45:5 //Insert Row(s) (Global): Alt+INSERT (KeyDown|KeyHold) +2:2005:6:45:5 //Insert Row(s) (All Channels, Global): Ctrl+Alt+INSERT (KeyDown|KeyHold) +2:1055:0:109:5 //Previous pattern: NUM SUB (KeyDown|KeyHold) +2:1054:0:107:5 //Next pattern: NUM PLUS (KeyDown|KeyHold) +2:2006:6:189:5 //Previous Sequence: Ctrl+Alt+- (KeyDown|KeyHold) +2:2006:6:109:5 //Previous Sequence: Ctrl+Alt+NUM SUB (KeyDown|KeyHold) +2:2007:6:187:5 //Next Sequence: Ctrl+Alt++ (KeyDown|KeyHold) +2:2007:6:107:5 //Next Sequence: Ctrl+Alt+NUM PLUS (KeyDown|KeyHold) + +//----( Pattern Context [bottom] - Note Col )------------ +3:1064:0:81/16:1 //Base octave C: Q (KeyDown) +3:1065:0:87/17:1 //Base octave C#: W (KeyDown) +3:1066:0:69/18:1 //Base octave D: E (KeyDown) +3:1067:0:82/19:1 //Base octave D#: R (KeyDown) +3:1068:0:84/20:1 //Base octave E: T (KeyDown) +3:1069:0:89/21:1 //Base octave F: Y (KeyDown) +3:1070:0:85/22:1 //Base octave F#: U (KeyDown) +3:1071:0:73/23:1 //Base octave G: I (KeyDown) +3:1072:0:79/24:1 //Base octave G#: O (KeyDown) +3:1073:0:80/25:1 //Base octave A: P (KeyDown) +3:1074:0:219/26:1 //Base octave A#: [ (KeyDown) +3:1075:0:221/27:1 //Base octave B: ] (KeyDown) +3:1076:0:65/30:1 //Base octave +1 C: A (KeyDown) +3:1077:0:83/31:1 //Base octave +1 C#: S (KeyDown) +3:1078:0:68/32:1 //Base octave +1 D: D (KeyDown) +3:1079:0:70/33:1 //Base octave +1 D#: F (KeyDown) +3:1080:0:71/34:1 //Base octave +1 E: G (KeyDown) +3:1081:0:72/35:1 //Base octave +1 F: H (KeyDown) +3:1082:0:74/36:1 //Base octave +1 F#: J (KeyDown) +3:1083:0:75/37:1 //Base octave +1 G: K (KeyDown) +3:1084:0:76/38:1 //Base octave +1 G#: L (KeyDown) +3:1085:0:186/39:1 //Base octave +1 A: ; (KeyDown) +3:1086:0:222/40:1 //Base octave +1 A#: # (KeyDown) +3:1087:0:220/43:1 //Base octave +1 B: \ (KeyDown) +3:1088:0:90/44:1 //Base octave +2 C: Z (KeyDown) +3:1089:0:88/45:1 //Base octave +2 C#: X (KeyDown) +3:1090:0:67/46:1 //Base octave +2 D: C (KeyDown) +3:1091:0:86/47:1 //Base octave +2 D#: V (KeyDown) +3:1092:0:66/48:1 //Base octave +2 E: B (KeyDown) +3:1093:0:78/49:1 //Base octave +2 F: N (KeyDown) +3:1094:0:77/50:1 //Base octave +2 F#: M (KeyDown) +3:1095:0:188/51:1 //Base octave +2 G: , (KeyDown) +3:1096:0:190/52:1 //Base octave +2 G#: . (KeyDown) +3:1097:0:191/53:1 //Base octave +2 A: / (KeyDown) +3:1212:0:48:1 //Set octave 0: 0 (KeyDown) +3:1212:0:96:1 //Set octave 0: NUM 0 (KeyDown) +3:1213:0:49:1 //Set octave 1: 1 (KeyDown) +3:1213:0:97:1 //Set octave 1: NUM 1 (KeyDown) +3:1214:0:50:1 //Set octave 2: 2 (KeyDown) +3:1214:0:98:1 //Set octave 2: NUM 2 (KeyDown) +3:1215:0:51:1 //Set octave 3: 3 (KeyDown) +3:1215:0:99:1 //Set octave 3: NUM 3 (KeyDown) +3:1216:0:52:1 //Set octave 4: 4 (KeyDown) +3:1216:0:100:1 //Set octave 4: NUM 4 (KeyDown) +3:1217:0:53:1 //Set octave 5: 5 (KeyDown) +3:1217:0:101:1 //Set octave 5: NUM 5 (KeyDown) +3:1218:0:54:1 //Set octave 6: 6 (KeyDown) +3:1218:0:102:1 //Set octave 6: NUM 6 (KeyDown) +3:1219:0:55:1 //Set octave 7: 7 (KeyDown) +3:1219:0:103:1 //Set octave 7: NUM 7 (KeyDown) +3:1220:0:56:1 //Set octave 8: 8 (KeyDown) +3:1220:0:104:1 //Set octave 8: NUM 8 (KeyDown) +3:1221:0:57:1 //Set octave 9: 9 (KeyDown) +3:1221:0:105:1 //Set octave 9: NUM 9 (KeyDown) +3:1316:1:16:1 //Chord Modifier: Shift (KeyDown) +3:1200:0:192:1 //Note Cut: ' (KeyDown) +3:1201:0:187:1 //Note Off: = (KeyDown) +3:1791:1:187:1 //Note Fade: Shift+= (KeyDown) +3:1788:1:189:1 //Parameter Control: Shift+- (KeyDown) +3:1789:0:189:1 //Parameter Control (smooth): - (KeyDown) + +//----( Pattern Context [bottom] - Ins Col )------------ +4:1202:0:96:1 //Set instrument digit 0: NUM 0 (KeyDown) +4:1202:0:48:1 //Set instrument digit 0: 0 (KeyDown) +4:1203:0:97:1 //Set instrument digit 1: NUM 1 (KeyDown) +4:1203:0:49:1 //Set instrument digit 1: 1 (KeyDown) +4:1204:0:98:1 //Set instrument digit 2: NUM 2 (KeyDown) +4:1204:0:50:1 //Set instrument digit 2: 2 (KeyDown) +4:1205:0:99:1 //Set instrument digit 3: NUM 3 (KeyDown) +4:1205:0:51:1 //Set instrument digit 3: 3 (KeyDown) +4:1206:0:100:1 //Set instrument digit 4: NUM 4 (KeyDown) +4:1206:0:52:1 //Set instrument digit 4: 4 (KeyDown) +4:1207:0:101:1 //Set instrument digit 5: NUM 5 (KeyDown) +4:1207:0:53:1 //Set instrument digit 5: 5 (KeyDown) +4:1208:0:102:1 //Set instrument digit 6: NUM 6 (KeyDown) +4:1208:0:54:1 //Set instrument digit 6: 6 (KeyDown) +4:1209:0:103:1 //Set instrument digit 7: NUM 7 (KeyDown) +4:1209:0:55:1 //Set instrument digit 7: 7 (KeyDown) +4:1210:0:104:1 //Set instrument digit 8: NUM 8 (KeyDown) +4:1210:0:56:1 //Set instrument digit 8: 8 (KeyDown) +4:1211:0:105:1 //Set instrument digit 9: NUM 9 (KeyDown) +4:1211:0:57:1 //Set instrument digit 9: 9 (KeyDown) + +//----( Pattern Context [bottom] - Vol Col )------------ +5:1222:0:48:1 //Set volume digit 0: 0 (KeyDown) +5:1222:0:96:1 //Set volume digit 0: NUM 0 (KeyDown) +5:1223:0:49:1 //Set volume digit 1: 1 (KeyDown) +5:1223:0:97:1 //Set volume digit 1: NUM 1 (KeyDown) +5:1224:0:50:1 //Set volume digit 2: 2 (KeyDown) +5:1224:0:98:1 //Set volume digit 2: NUM 2 (KeyDown) +5:1225:0:51:1 //Set volume digit 3: 3 (KeyDown) +5:1225:0:99:1 //Set volume digit 3: NUM 3 (KeyDown) +5:1226:0:52:1 //Set volume digit 4: 4 (KeyDown) +5:1226:0:100:1 //Set volume digit 4: NUM 4 (KeyDown) +5:1227:0:53:1 //Set volume digit 5: 5 (KeyDown) +5:1227:0:101:1 //Set volume digit 5: NUM 5 (KeyDown) +5:1228:0:54:1 //Set volume digit 6: 6 (KeyDown) +5:1228:0:102:1 //Set volume digit 6: NUM 6 (KeyDown) +5:1229:0:55:1 //Set volume digit 7: 7 (KeyDown) +5:1229:0:103:1 //Set volume digit 7: NUM 7 (KeyDown) +5:1230:0:56:1 //Set volume digit 8: 8 (KeyDown) +5:1230:0:104:1 //Set volume digit 8: NUM 8 (KeyDown) +5:1231:0:57:1 //Set volume digit 9: 9 (KeyDown) +5:1231:0:105:1 //Set volume digit 9: NUM 9 (KeyDown) +5:1232:0:86:1 //Volume Command - Volume: V (KeyDown) +5:1233:0:80:1 //Volume Command - Panning: P (KeyDown) +5:1234:0:67:1 //Volume Command - Volume Slide Up: C (KeyDown) +5:1235:0:68:1 //Volume Command - Volume Slide Down: D (KeyDown) +5:1236:0:65:1 //Volume Command - Fine Volume Slide Up: A (KeyDown) +5:1237:0:66:1 //Volume Command - Fine Volume Slide Down: B (KeyDown) +5:1238:0:85:1 //Volume Command - Vibrato Speed: U (KeyDown) +5:1239:0:72:1 //Volume Command - Vibrato Depth: H (KeyDown) +5:1240:0:76:1 //Volume Command - XM Pan Slide Left: L (KeyDown) +5:1241:0:82:1 //Volume Command - XM Pan Slide Right: R (KeyDown) +5:1242:0:71:1 //Volume Command - Portamento: G (KeyDown) +5:1243:0:70:1 //Volume Command - Portamento Up: F (KeyDown) +5:1244:0:69:1 //Volume Command - Portamento Down: E (KeyDown) +5:1246:0:79:1 //Volume Command - Offset: O (KeyDown) + +//----( Pattern Context [bottom] - FX Col )------------ +6:1294:0:220:1 //Smooth MIDI Macro Slide: \ (KeyDown) +6:1295:1:186:1 //Combined Note Delay and Note Cut: Shift+; (KeyDown) +6:1295:0:186:1 //Combined Note Delay and Note Cut: ; (KeyDown) +6:1666:0:191:1 //Parameter Extension Command: / (KeyDown) +6:2017:0:187:1 //Finetune: = (KeyDown) +6:2018:1:187:1 //Finetune (Smooth): Shift+= (KeyDown) + +//----( Pattern Context [bottom] - Param Col )------------ +7:1247:0:48:1 //Effect Parameter Digit 0: 0 (KeyDown) +7:1247:0:96:1 //Effect Parameter Digit 0: NUM 0 (KeyDown) +7:1248:0:49:1 //Effect Parameter Digit 1: 1 (KeyDown) +7:1248:0:97:1 //Effect Parameter Digit 1: NUM 1 (KeyDown) +7:1249:0:50:1 //Effect Parameter Digit 2: 2 (KeyDown) +7:1249:0:98:1 //Effect Parameter Digit 2: NUM 2 (KeyDown) +7:1250:0:51:1 //Effect Parameter Digit 3: 3 (KeyDown) +7:1250:0:99:1 //Effect Parameter Digit 3: NUM 3 (KeyDown) +7:1251:0:52:1 //Effect Parameter Digit 4: 4 (KeyDown) +7:1251:0:100:1 //Effect Parameter Digit 4: NUM 4 (KeyDown) +7:1252:0:53:1 //Effect Parameter Digit 5: 5 (KeyDown) +7:1252:0:101:1 //Effect Parameter Digit 5: NUM 5 (KeyDown) +7:1253:0:54:1 //Effect Parameter Digit 6: 6 (KeyDown) +7:1253:0:102:1 //Effect Parameter Digit 6: NUM 6 (KeyDown) +7:1254:0:55:1 //Effect Parameter Digit 7: 7 (KeyDown) +7:1254:0:103:1 //Effect Parameter Digit 7: NUM 7 (KeyDown) +7:1255:0:56:1 //Effect Parameter Digit 8: 8 (KeyDown) +7:1255:0:104:1 //Effect Parameter Digit 8: NUM 8 (KeyDown) +7:1256:0:57:1 //Effect Parameter Digit 9: 9 (KeyDown) +7:1256:0:105:1 //Effect Parameter Digit 9: NUM 9 (KeyDown) +7:1257:0:65:1 //Effect Parameter Digit A: A (KeyDown) +7:1258:0:66:1 //Effect Parameter Digit B: B (KeyDown) +7:1259:0:67:1 //Effect Parameter Digit C: C (KeyDown) +7:1260:0:68:1 //Effect Parameter Digit D: D (KeyDown) +7:1261:0:69:1 //Effect Parameter Digit E: E (KeyDown) +7:1262:0:70:1 //Effect Parameter Digit F: F (KeyDown) + +//----( Sample Context [bottom] )------------ +8:1673:0:13:1 //Load Sample: ENTER (KeyDown) +8:2031:1:13:1 //Load Raw Sample: Shift+ENTER (KeyDown) +8:1907:2:81:1 //Transpose +1: Ctrl+Q (KeyDown) +8:1908:2:65:1 //Transpose -1: Ctrl+A (KeyDown) +8:1909:3:81:1 //Transpose +1 Octave: Shift+Ctrl+Q (KeyDown) +8:1910:3:65:1 //Transpose -1 Octave: Shift+Ctrl+A (KeyDown) +8:1380:2:84:1 //Trim sample around loop points: Ctrl+T (KeyDown) +8:1964:3:84:1 //Trim to loop end: Shift+Ctrl+T (KeyDown) +8:1383:0:8:1 //Silence Sample Selection: BACKSPACE (KeyDown) +8:1384:1:78:1 //Normalise Sample: Shift+N (KeyDown) +8:1385:2:77:1 //Amplify Sample: Ctrl+M (KeyDown) +8:1381:3:82:1 //Reverse Sample: Shift+Ctrl+R (KeyDown) +8:1382:0:46:1 //Delete Sample Selection: DELETE (KeyDown) +8:1386:0:107:5 //Zoom In: NUM PLUS (KeyDown|KeyHold) +8:1386:2:187:5 //Zoom In: Ctrl++ (KeyDown|KeyHold) +8:1387:0:109:5 //Zoom Out: NUM SUB (KeyDown|KeyHold) +8:1387:2:189:5 //Zoom Out: Ctrl+- (KeyDown|KeyHold) +8:1882:0:32:1 //Zoom into Selection: Space (KeyDown) +8:1962:0:49:1 //Zoom into sample start: 1 (KeyDown) +8:1963:0:50:1 //Zoom into sample end: 2 (KeyDown) +8:1916:2:49:1 //Center loop start in view: Ctrl+1 (KeyDown) +8:1917:2:50:1 //Center loop end in view: Ctrl+2 (KeyDown) +8:1918:2:51:1 //Center sustain loop start in view: Ctrl+3 (KeyDown) +8:1919:2:52:1 //Center sustain loop end in view: Ctrl+4 (KeyDown) +8:1887:2:56:1 //Convert to 8-bit / 16-bit: Ctrl+8 (KeyDown) +8:1888:1:77:1 //Convert to Mono (Mix): Shift+M (KeyDown) +8:1889:1:76:1 //Convert to Mono (Left Channel): Shift+L (KeyDown) +8:1890:1:82:1 //Convert to Mono (Right Channel): Shift+R (KeyDown) +8:1891:1:83:1 //Convert to Mono (Split Sample): Shift+S (KeyDown) +8:1969:2:80:1 //Change Stereo Separation: Ctrl+P (KeyDown) +8:1913:2:70:1 //Upsample: Ctrl+F (KeyDown) +8:1914:2:71:1 //Downsample: Ctrl+G (KeyDown) +8:1915:2:82:1 //Resample: Ctrl+R (KeyDown) +8:1784:2:73:1 //Invert Sample Phase: Ctrl+I (KeyDown) +8:1785:2:85:1 //Signed / Unsigned Conversion: Ctrl+U (KeyDown) +8:1790:2:69:1 //Remove DC Offset: Ctrl+E (KeyDown) +8:1856:2:68:1 //Quick Fade: Ctrl+D (KeyDown) +8:1857:2:76:1 //Crossfade Sample Loop: Ctrl+L (KeyDown) +8:1924:1:49:1 //Preview Sample Cue 1: Shift+1 (KeyDown) +8:1925:1:50:1 //Preview Sample Cue 2: Shift+2 (KeyDown) +8:1926:1:51:1 //Preview Sample Cue 3: Shift+3 (KeyDown) +8:1927:1:52:1 //Preview Sample Cue 4: Shift+4 (KeyDown) +8:1928:1:53:1 //Preview Sample Cue 5: Shift+5 (KeyDown) +8:1929:1:54:1 //Preview Sample Cue 6: Shift+6 (KeyDown) +8:1930:1:55:1 //Preview Sample Cue 7: Shift+7 (KeyDown) +8:1931:1:56:1 //Preview Sample Cue 8: Shift+8 (KeyDown) +8:1932:1:57:1 //Preview Sample Cue 9: Shift+9 (KeyDown) + +//----( Instrument Context [bottom] )------------ +9:1837:0:107:5 //Zoom In: NUM PLUS (KeyDown|KeyHold) +9:1837:2:187:5 //Zoom In: Ctrl++ (KeyDown|KeyHold) +9:1838:0:109:5 //Zoom Out: NUM SUB (KeyDown|KeyHold) +9:1838:2:189:5 //Zoom Out: Ctrl+- (KeyDown|KeyHold) +9:1954:2:69:1 //Scale Envelope Points: Ctrl+E (KeyDown) +9:1976:1:77:1 //Switch to Volume Envelope: Shift+M (KeyDown) +9:1977:1:80:1 //Switch to Panning Envelope: Shift+P (KeyDown) +9:1978:1:73:1 //Switch to Pitch / Filter Envelope: Shift+I (KeyDown) +9:1979:3:77:1 //Toggle Volume Envelope: Shift+Ctrl+M (KeyDown) +9:1980:3:80:1 //Toggle Panning Envelope: Shift+Ctrl+P (KeyDown) +9:1981:3:73:1 //Toggle Pitch Envelope: Shift+Ctrl+I (KeyDown) +9:1982:3:70:1 //Toggle Filter Envelope: Shift+Ctrl+F (KeyDown) +9:1983:1:76:1 //Toggle Envelope Loop: Shift+L (KeyDown) +9:1956:1:36:1 //Select Envelope Loop Start: Shift+POS1 (KeyDown) +9:1957:1:35:1 //Select Envelope Loop End: Shift+ENDE (KeyDown) +9:1984:3:76:1 //Toggle Envelope Sustain Loop: Shift+Ctrl+L (KeyDown) +9:1958:3:36:1 //Select Envelope Sustain Start: Shift+Ctrl+POS1 (KeyDown) +9:1959:3:35:1 //Select Envelope Sustain End: Shift+Ctrl+ENDE (KeyDown) +9:1985:3:67:1 //Toggle Envelope Carry: Shift+Ctrl+C (KeyDown) +9:1825:1:9:5 //Select previous envelope point: Shift+TAB (KeyDown|KeyHold) +9:1826:0:9:5 //Select next envelope point: TAB (KeyDown|KeyHold) +9:1821:0:37:5 //Move envelope point left: LEFT (KeyDown|KeyHold) +9:1822:0:39:5 //Move envelope point right: RIGHT (KeyDown|KeyHold) +9:1960:1:37:5 //Move envelope point left (Coarse): Shift+LEFT (KeyDown|KeyHold) +9:1961:1:39:5 //Move envelope point right (Coarse): Shift+RIGHT (KeyDown|KeyHold) +9:1823:0:38:5 //Move envelope point up: UP (KeyDown|KeyHold) +9:1834:0:33:5 //Move envelope point up (Coarse): PGUP (KeyDown|KeyHold) +9:1824:0:40:5 //Move envelope point down: DOWN (KeyDown|KeyHold) +9:1835:0:34:5 //Move envelope point down (Coarse): PGDOWN (KeyDown|KeyHold) +9:1827:0:45:1 //Insert Envelope Point: INSERT (KeyDown) +9:1828:0:46:1 //Remove Envelope Point: DELETE (KeyDown) +9:1828:0:8:1 //Remove Envelope Point: BACKSPACE (KeyDown) +9:1829:0:36:1 //Set Loop Start: HOME (KeyDown) +9:1830:0:35:1 //Set Loop End: END (KeyDown) +9:1831:2:36:1 //Set sustain loop start: Ctrl+HOME (KeyDown) +9:1832:2:35:1 //Set sustain loop end: Ctrl+END (KeyDown) +9:1833:2:82:1 //Toggle release node: Ctrl+R (KeyDown) + +//----( Comments Context [bottom] )------------ +10:2000:0:9:1 //Toggle between lists: TAB (KeyDown) +10:2001:0:13:1 //Open item in editor: ENTER (KeyDown) +10:2026:2:13:1 //Rename Item: Ctrl+ENTER (KeyDown) + +//----( Unknown Context )------------ + +//----( Unknown Context )------------ + +//----( Plugin GUI Context )------------ +13:1763:0:109:5 //Previous Plugin Preset: NUM SUB (KeyDown|KeyHold) +13:1764:0:107:5 //Next Plugin Preset: NUM PLUS (KeyDown|KeyHold) +13:1782:2:109:5 //Plugin preset backward jump: Ctrl+NUM SUB (KeyDown|KeyHold) +13:1783:2:107:5 //Plugin preset forward jump: Ctrl+NUM PLUS (KeyDown|KeyHold) +13:1765:3:68:1 //Randomize Plugin Parameters: Shift+Ctrl+D (KeyDown) +13:1839:2:82:1 //Toggle Parameter Recording: Ctrl+R (KeyDown) +13:1840:2:75:1 //Pass Key Presses to Plugin: Ctrl+K (KeyDown) +13:1841:2:66:1 //Bypass Plugin: Ctrl+B (KeyDown) + +//----( General Context [top] )------------ + +//----( Pattern Context [top] )------------ + +//----( Sample Context [top] )------------ + +//----( Instrument Context [top] )------------ +17:1851:2:68:1 //Duplicate Instrument: Ctrl+D (KeyDown) +17:1850:3:69:1 //Edit Sample Map: Shift+Ctrl+E (KeyDown) +17:1849:2:69:1 //Edit Current Sample: Ctrl+E (KeyDown) +17:1846:3:77:1 //Map all notes to selected note: Shift+Ctrl+M (KeyDown) +17:1847:2:77:1 //Map all notes to selected sample: Ctrl+M (KeyDown) +17:1848:2:82:1 //Reset Note Mapping: Ctrl+R (KeyDown) +17:1843:2:81:1 //Transpose +1 (Note Map): Ctrl+Q (KeyDown) +17:1842:2:65:1 //Transpose -1 (Note Map): Ctrl+A (KeyDown) +17:1845:3:81:1 //Transpose +1 Octave (Note Map): Shift+Ctrl+Q (KeyDown) +17:1844:3:65:1 //Transpose -1 Octave (Note Map): Shift+Ctrl+A (KeyDown) + +//----( Comments Context [top] )------------ + +//----( Orderlist )------------ +19:1802:0:46:5 //Delete Order: DELETE (KeyDown|KeyHold) +19:1803:0:45:5 //Insert Order: INSERT (KeyDown|KeyHold) +19:2019:2:45:5 //Insert Separator: Ctrl+INSERT (KeyDown|KeyHold) +19:1950:3:67:1 //Copy Orders: Shift+Ctrl+C (KeyDown) +19:1804:0:13:5 //Edit Pattern: ENTER (KeyDown|KeyHold) +19:1805:0:9:5 //Switch to pattern editor: TAB (KeyDown|KeyHold) +19:1794:0:37:5 //Previous Order: LEFT (KeyDown|KeyHold) +19:1794:0:38:5 //Previous Order: UP (KeyDown|KeyHold) +19:1795:0:39:5 //Next Order: RIGHT (KeyDown|KeyHold) +19:1795:0:40:5 //Next Order: DOWN (KeyDown|KeyHold) +19:1796:0:36:5 //First Order: HOME (KeyDown|KeyHold) +19:1797:0:35:5 //Last Order: END (KeyDown|KeyHold) +19:1807:0:48:5 //Pattern index digit 0: 0 (KeyDown|KeyHold) +19:1807:0:96:5 //Pattern index digit 0: NUM 0 (KeyDown|KeyHold) +19:1808:0:49:5 //Pattern index digit 1: 1 (KeyDown|KeyHold) +19:1808:0:97:5 //Pattern index digit 1: NUM 1 (KeyDown|KeyHold) +19:1809:0:50:5 //Pattern index digit 2: 2 (KeyDown|KeyHold) +19:1809:0:98:5 //Pattern index digit 2: NUM 2 (KeyDown|KeyHold) +19:1810:0:51:5 //Pattern index digit 3: 3 (KeyDown|KeyHold) +19:1810:0:99:5 //Pattern index digit 3: NUM 3 (KeyDown|KeyHold) +19:1811:0:52:5 //Pattern index digit 4: 4 (KeyDown|KeyHold) +19:1811:0:100:5 //Pattern index digit 4: NUM 4 (KeyDown|KeyHold) +19:1812:0:53:5 //Pattern index digit 5: 5 (KeyDown|KeyHold) +19:1812:0:101:5 //Pattern index digit 5: NUM 5 (KeyDown|KeyHold) +19:1813:0:54:5 //Pattern index digit 6: 6 (KeyDown|KeyHold) +19:1813:0:102:5 //Pattern index digit 6: NUM 6 (KeyDown|KeyHold) +19:1814:0:55:5 //Pattern index digit 7: 7 (KeyDown|KeyHold) +19:1814:0:103:5 //Pattern index digit 7: NUM 7 (KeyDown|KeyHold) +19:1815:0:56:5 //Pattern index digit 8: 8 (KeyDown|KeyHold) +19:1815:0:104:5 //Pattern index digit 8: NUM 8 (KeyDown|KeyHold) +19:1816:0:57:5 //Pattern index digit 9: 9 (KeyDown|KeyHold) +19:1816:0:105:5 //Pattern index digit 9: NUM 9 (KeyDown|KeyHold) +19:1817:0:107:5 //Increase pattern index : NUM PLUS (KeyDown|KeyHold) +19:1817:0:187:5 //Increase pattern index : = (KeyDown|KeyHold) +19:1818:0:109:5 //Decrease pattern index: NUM SUB (KeyDown|KeyHold) +19:1818:0:189:5 //Decrease pattern index: - (KeyDown|KeyHold) +19:1853:0:73:1 //Separator (+++) Index: I (KeyDown) +19:1854:0:32:1 //Stop (---) Index: SPACE (KeyDown) +19:1875:2:76:1 //Lock Playback to Selection: Ctrl+L (KeyDown) +19:1876:2:85:1 //Unlock Playback: Ctrl+U (KeyDown) + +//----( Quick Channel Settings Context )------------ +20:1878:4:37:5 //Previous Channel: Alt+Left (KeyDown|KeyHold) +20:1878:1:8:5 //Previous Channel: Shift+Backspace (KeyDown|KeyHold) +20:1879:4:39:5 //Next Channel: Alt+Right (KeyDown|KeyHold) +20:1879:1:13:5 //Next Channel: Shift+ENTER (KeyDown|KeyHold) +20:2008:5:37:1 //Pick Color from Previous Channel: Shift+Alt+Left (KeyDown) +20:2009:5:39:1 //Pick Color from Next Channel: Shift+Alt+Right (KeyDown) +20:1880:0:13:1 //Switch to Pattern Editor: ENTER (KeyDown) diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/envelope_toolbar.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/envelope_toolbar.png Binary files differnew file mode 100644 index 00000000..cee6ff10 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/envelope_toolbar.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/icons.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/icons.png Binary files differnew file mode 100644 index 00000000..f475a471 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/icons.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/main_toolbar.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/main_toolbar.png Binary files differnew file mode 100644 index 00000000..4e2bef50 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/main_toolbar.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/moddoc.ico b/Src/external_dependencies/openmpt-trunk/mptrack/res/moddoc.ico Binary files differnew file mode 100644 index 00000000..4eab2b05 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/moddoc.ico diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/nodrag.cur b/Src/external_dependencies/openmpt-trunk/mptrack/res/nodrag.cur Binary files differnew file mode 100644 index 00000000..85d3250c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/nodrag.cur diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/128x128.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/128x128.svg new file mode 100644 index 00000000..b424d2d4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/128x128.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 128 128" style="enable-background:new 0 0 128 128;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="128" height="128"/> +<g> + <path class="st1" d="M43.4,67.2V38c3-1.6,7.7-3.4,10.2-3.4c2.4,0,3.4,1.3,3.4,3.4v27.3l14-2V38c3-1.6,7.7-3.4,10.2-3.4 + c2.4,0,3.4,1.3,3.4,3.4v23.4l14-2V36.1C98.5,27,95.7,21,87.6,21c-6.5,0-13.6,1.9-18.9,4.6C67,22.7,64.2,21,60.1,21 + c-5.4,0-13,1.7-18.8,4.6L39.8,22H29.4v47.2l0,0L43.4,67.2z"/> +</g> +<g> + <polygon class="st2" points="71,91 71,110.5 57,112.4 57,69.2 71,67.2 71,79 "/> +</g> +<g> + <path class="st2" d="M70.5,91.1c3.2,0.8,8.1,1.4,12,1.4c11.1,0,15.9-6.3,15.9-17.5V63.3l-14,2V75c0,3.1-1.4,5.3-5,5.3 + c-2.9,0-6.5-0.6-8.9-1.2V91.1z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/16x16.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/16x16.svg new file mode 100644 index 00000000..f8494f23 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/16x16.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#D8001A;} + .st2{fill:#CCCCCC;} +</style> +<rect class="st0" width="16" height="16"/> +<g> + <path class="st1" d="M4.8,8.4V3.8C5.3,3.6,6,3.3,6.4,3.3s0.5,0.2,0.5,0.5V8L9,7.8v-4c0.5-0.2,1.2-0.5,1.6-0.5 + c0.4,0,0.5,0.2,0.5,0.5v3.6l2.1-0.2V3.6c0-1.4-0.5-2.4-1.7-2.4c-1,0-2.1,0.2-3,0.7c0-0.5-0.5-0.7-1.1-0.7c-0.9,0-2,0.2-3,0.7 + L4.3,1.3H2.7v7.3l0,0L4.8,8.4z"/> +</g> +<g> + <polygon class="st2" points="9.1,12.1 9.1,15.1 6.9,15.4 6.9,8.7 9.1,8.4 9.1,10.3 "/> +</g> +<g> + <path class="st2" d="M9,12.1c0.5,0.1,1.2,0.2,1.9,0.2c1.7,0,2.5-1,2.5-2.7V7.8L11.3,8v1.5c0,0.5-0.2,0.9-0.7,0.9s-1.1,0-1.5-0.1 + L9,12.1L9,12.1z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/192x192.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/192x192.svg new file mode 100644 index 00000000..178577f0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/192x192.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 192 192" style="enable-background:new 0 0 192 192;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="192" height="192"/> +<g> + <path class="st1" d="M65.1,100.8V57c4.5-2.5,11.5-5.1,15.3-5.1c3.6,0,5.1,1.9,5.1,5.1v40.9l21-3V57c4.5-2.5,11.5-5.1,15.3-5.1 + c3.6,0,5.1,1.9,5.1,5.1v35l21-3V54.2c0-13.7-4.2-22.7-16.3-22.7c-9.7,0-20.4,2.9-28.4,6.8c-2.5-4.4-6.7-6.8-13-6.8 + c-8.1,0-19.5,2.6-28.2,6.8L59.7,33H44.1v70.9l0,0L65.1,100.8z"/> +</g> +<g> + <polygon class="st2" points="106.5,136.4 106.5,165.7 85.5,168.6 85.5,103.8 106.5,100.8 106.5,118.5 "/> +</g> +<g> + <path class="st2" d="M105.8,136.6c4.8,1.2,12.2,2,18,2c16.6,0,23.9-9.5,23.9-26.2V94.9l-21,3v14.5c0,4.7-2,8-7.6,8 + c-4.4,0-9.7-0.9-13.4-1.7v17.9H105.8z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/22x22.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/22x22.svg new file mode 100644 index 00000000..26c60ea5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/22x22.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 22 22" style="enable-background:new 0 0 22 22;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#D8001A;} + .st2{fill:#CCCCCC;} +</style> +<rect class="st0" width="22" height="22"/> +<g> + <path class="st1" d="M6.6,11.6V5.3C7.3,5,8.3,4.6,8.8,4.6s0.7,0.3,0.7,0.7v5.8l2.9-0.3V5.3c0.7-0.3,1.7-0.7,2.2-0.7 + s0.7,0.3,0.7,0.7v4.9l2.9-0.3v-5c0-1.9-0.7-3.2-2.4-3.2c-1.4,0-2.9,0.3-4.1,1c0-0.7-0.7-1-1.5-1C9,1.7,7.4,2,6.1,2.7L5.9,1.9H3.7 + V12l0,0L6.6,11.6z"/> +</g> +<g> + <polygon class="st2" points="12.5,16.7 12.5,20.8 9.5,21.1 9.5,11.9 12.5,11.6 12.5,14.1 "/> +</g> +<g> + <path class="st2" d="M12.4,16.7c0.7,0.2,1.7,0.3,2.6,0.3c2.4,0,3.4-1.4,3.4-3.8v-2.6l-2.9,0.3v2c0,0.7-0.3,1.2-1,1.2s-1.5,0-2-0.2 + v2.8H12.4z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/24x24.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/24x24.svg new file mode 100644 index 00000000..5e995adc --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/24x24.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#D8001A;} + .st2{fill:#CCCCCC;} +</style> +<rect class="st0" width="23.9" height="23.9"/> +<g> + <path class="st1" d="M7.2,12.6V5.7C7.9,5.4,9,5,9.6,5s0.7,0.4,0.7,0.7V12l3.2-0.4V5.7C14.2,5.4,15.3,5,15.9,5 + c0.6,0,0.7,0.4,0.7,0.7v5.4l3.2-0.4V5.4c0-2-0.7-3.5-2.6-3.5c-1.5,0-3.2,0.4-4.5,1.1c0-0.7-0.7-1.1-1.7-1.1C9.7,1.9,8,2.3,6.5,3 + L6.4,2H4v11l0,0L7.2,12.6z"/> +</g> +<g> + <polygon class="st2" points="13.7,18.2 13.7,22.6 10.3,23 10.3,13 13.7,12.6 13.7,15.4 "/> +</g> +<g> + <path class="st2" d="M13.5,18.2c0.7,0.2,1.9,0.4,2.8,0.4c2.6,0,3.7-1.5,3.7-4.1v-2.8l-3.2,0.4v2.2c0,0.7-0.4,1.3-1.1,1.3 + c-0.7,0-1.7,0-2.2-0.2C13.5,15.4,13.5,18.2,13.5,18.2z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/256x256.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/256x256.svg new file mode 100644 index 00000000..315e7169 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/256x256.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 256 256" style="enable-background:new 0 0 256 256;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="256" height="256"/> +<g> + <path class="st1" d="M86.8,134.4V76c6-3.3,15.3-6.8,20.4-6.8c4.9,0,6.8,2.5,6.8,6.8v54.6l27.9-4V76c6-3.3,15.3-6.8,20.4-6.8 + c4.9,0,6.8,2.5,6.8,6.8v46.7l27.9-4V72.3C197,54,191.3,42,175.2,42c-13,0-27.2,3.9-37.8,9.1c-3.3-5.8-8.9-9.1-17.3-9.1 + c-10.9,0-26,3.5-37.6,9.1L79.6,44H58.8v94.5l0,0L86.8,134.4z"/> +</g> +<g> + <polygon class="st2" points="141.9,181.9 141.9,220.9 114,224.8 114,138.4 141.9,134.4 141.9,158.1 "/> +</g> +<g> + <path class="st2" d="M141.1,182.1c6.4,1.6,16.3,2.7,24.1,2.7c22.1,0,31.8-12.6,31.8-34.9v-23.3l-27.9,4v19.3 + c0,6.2-2.7,10.7-10.1,10.7c-5.8,0-13-1.2-17.8-2.3v23.8H141.1z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/32x32.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/32x32.svg new file mode 100644 index 00000000..af7c0b13 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/32x32.svg @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#D8001A;} + .st2{fill:#CCCCCC;} +</style> +<rect x="0.1" class="st0" width="31.9" height="32"/> +<g> + <path class="st1" d="M9.6,16.9V7.7c1-0.5,2.5-1,3.2-1s1,0.5,1,1v8.4l4.2-0.5V7.7c1-0.5,2.5-1,3.2-1s1,0.5,1,1v7.2l4.2-0.5V7.2 + c0-2.7-1-4.7-3.5-4.7C21,2.5,18.7,3,17,4c0-1-1-1.5-2.2-1.5c-1.8,0-4,0.5-6,1.5L8.6,2.8H5.4v14.6l0,0L9.6,16.9z"/> +</g> +<g> + <polygon class="st2" points="18.2,24.3 18.2,30.3 13.8,30.7 13.8,17.4 18.2,16.9 18.2,20.6 "/> +</g> +<g> + <path class="st2" d="M18,24.3c1,0.2,2.5,0.5,3.7,0.5c3.5,0,5-2,5-5.4v-3.7l-4.2,0.5v3c0,1-0.5,1.7-1.5,1.7s-2.2,0-3-0.2V24.3z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/36x36.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/36x36.svg new file mode 100644 index 00000000..3db8f573 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/36x36.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#D8001A;} + .st2{fill:#CCCCCC;} +</style> +<rect class="st0" width="36" height="36"/> +<g> + <path class="st1" d="M10.7,19V8.6c1.1-0.6,2.8-1.1,3.6-1.1c0.8,0,1.1,0.6,1.1,1.1v9.5l4.7-0.6V8.6c1.1-0.6,2.8-1.1,3.6-1.1 + S25,8.1,25,8.6v8.1l4.7-0.6v-8c0-3.1-1.1-5.3-3.9-5.3c-2.2,0-4.7,0.6-6.7,1.7c0-1.1-1.1-1.7-2.5-1.7c-2,0-4.5,0.6-6.7,1.7L9.6,3H6 + v16.5l0,0L10.7,19z"/> +</g> +<g> + <polygon class="st2" points="20.5,27.3 20.5,34 15.5,34.6 15.5,19.5 20.5,19 20.5,23.1 "/> +</g> +<g> + <path class="st2" d="M20.2,27.3c1.1,0.3,2.8,0.6,4.2,0.6c3.9,0,5.6-2.2,5.6-6.1v-4.2l-4.7,0.6v3.4c0,1.1-0.6,2-1.7,2 + s-2.5,0-3.4-0.3C20.2,23.3,20.2,27.3,20.2,27.3z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/40x40.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/40x40.svg new file mode 100644 index 00000000..f00728c0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/40x40.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 40 40" style="enable-background:new 0 0 40 40;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="40" height="40"/> +<g> + <path class="st1" d="M13.6,21v-9.1c0.9-0.5,2.4-1.1,3.2-1.1c0.8,0,1.1,0.4,1.1,1.1v8.5l4.4-0.6v-7.9c0.9-0.5,2.4-1.1,3.2-1.1 + c0.8,0,1.1,0.4,1.1,1.1v7.3l4.4-0.6v-7.2c0-2.8-0.9-4.7-3.4-4.7c-2,0-4.2,0.6-5.9,1.4c-0.5-0.9-1.4-1.4-2.7-1.4 + c-1.7,0-4.1,0.5-5.9,1.4L12.6,7H9.2v14.8l0,0L13.6,21z"/> +</g> +<g> + <polygon class="st2" points="22.2,28.4 22.2,34.5 17.8,35.1 17.8,21.6 22.2,21 22.2,24.7 "/> +</g> +<g> + <path class="st2" d="M22,28.5c1,0.2,2.5,0.4,3.8,0.4c3.5,0,5-2,5-5.5v-3.6l-4.4,0.6v3c0,1-0.4,1.7-1.6,1.7c-0.9,0-2-0.2-2.8-0.4 + V28.5z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/48x48.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/48x48.svg new file mode 100644 index 00000000..f2c44a91 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/48x48.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="48" height="48"/> +<g> + <path class="st1" d="M16.3,25.2v-11c1.1-0.6,2.9-1.3,3.8-1.3s1.3,0.5,1.3,1.3v10.2l5.2-0.7v-9.5c1.1-0.6,2.9-1.3,3.8-1.3 + s1.3,0.5,1.3,1.3V23l5.2-0.7v-8.7c0-3.4-1.1-5.7-4.1-5.7c-2.4,0-5.1,0.7-7.1,1.7c-0.6-1.1-1.7-1.7-3.2-1.7c-2,0-4.9,0.7-7.1,1.7 + l-0.5-1.3H11V26l0,0L16.3,25.2z"/> +</g> +<g> + <polygon class="st2" points="26.6,34.1 26.6,41.4 21.4,42.1 21.4,25.9 26.6,25.2 26.6,29.6 "/> +</g> +<g> + <path class="st2" d="M26.5,34.2c1.2,0.3,3.1,0.5,4.5,0.5c4.1,0,6-2.4,6-6.5v-4.4l-5.2,0.7v3.6c0,1.2-0.5,2-1.9,2 + c-1.1,0-2.4-0.2-3.3-0.4v4.5H26.5z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/512x512.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/512x512.svg new file mode 100644 index 00000000..e44ac9a0 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/512x512.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="512" height="512"/> +<g> + <path class="st1" d="M173.5,268.9v-117c12-6.6,30.7-13.6,40.7-13.6c9.7,0,13.6,5,13.6,13.6v109.2l55.9-8V151.9 + c12-6.6,30.7-13.6,40.7-13.6c9.7,0,13.6,5,13.6,13.6v93.4l55.9-8v-92.8c0-36.5-11.3-60.5-43.5-60.5c-26,0-54.3,7.8-75.7,18.2 + C268.2,90.6,256.9,84,240.3,84c-21.7,0-52,7-75.3,18.2l-5.8-14.4h-41.5v189l0,0L173.5,268.9z"/> +</g> +<g> + <polygon class="st2" points="283.9,363.8 283.9,441.8 228,449.6 228,276.7 283.9,268.8 283.9,316.1 "/> +</g> +<g> + <path class="st2" d="M282.1,364.3c12.8,3.1,32.6,5.4,48.1,5.4c44.2,0,63.6-25.2,63.6-69.9v-46.7l-55.9,8v38.7 + c0,12.4-5.4,21.3-20.2,21.3c-11.6,0-26-2.3-35.7-4.7v47.9H282.1z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/64x64.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/64x64.svg new file mode 100644 index 00000000..7117f3e1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/64x64.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="64" height="64"/> +<g> + <path class="st1" d="M21.7,33.6V19c1.5-0.8,3.8-1.7,5.1-1.7c1.2,0,1.7,0.6,1.7,1.7v13.6l7-1V19c1.5-0.8,3.8-1.7,5.1-1.7 + c1.2,0,1.7,0.6,1.7,1.7v11.7l7-1V18.1c0-4.6-1.4-7.6-5.4-7.6c-3.2,0-6.8,1-9.5,2.3c-0.8-1.5-2.2-2.3-4.3-2.3 + c-2.7,0-6.5,0.9-9.4,2.3L19.9,11h-5.2v23.6l0,0L21.7,33.6z"/> +</g> +<g> + <polygon class="st2" points="35.5,45.5 35.5,55.2 28.5,56.2 28.5,34.6 35.5,33.6 35.5,39.5 "/> +</g> +<g> + <path class="st2" d="M35.3,45.5c1.6,0.4,4.1,0.7,6,0.7c5.5,0,8-3.2,8-8.7v-5.8l-7,1v4.8c0,1.6-0.7,2.7-2.5,2.7 + c-1.5,0-3.2-0.3-4.5-0.6V45.5z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/72x72.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/72x72.svg new file mode 100644 index 00000000..91fb3fb1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/72x72.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 72 72" style="enable-background:new 0 0 72 72;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="72" height="72"/> +<g> + <path class="st1" d="M24.4,37.8V21.4c1.7-0.9,4.3-1.9,5.7-1.9c1.4,0,1.9,0.7,1.9,1.9v15.4l7.9-1.1V21.4c1.7-0.9,4.3-1.9,5.7-1.9 + s1.9,0.7,1.9,1.9v13.1l7.9-1.1v-13c0-5.1-1.6-8.5-6.1-8.5c-3.7,0-7.6,1.1-10.6,2.6c-0.9-1.6-2.5-2.6-4.9-2.6c-3.1,0-7.3,1-10.6,2.6 + l-0.8-2h-5.8v26.6l0,0L24.4,37.8z"/> +</g> +<g> + <polygon class="st2" points="39.9,51.2 39.9,62.1 32.1,63.2 32.1,38.9 39.9,37.8 39.9,44.5 "/> +</g> +<g> + <path class="st2" d="M39.7,51.2c1.8,0.4,4.6,0.8,6.8,0.8c6.2,0,8.9-3.5,8.9-9.8v-6.6l-7.9,1.1v5.4c0,1.7-0.8,3-2.8,3 + c-1.6,0-3.7-0.3-5-0.7V51.2z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/96x96.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/96x96.svg new file mode 100644 index 00000000..0796d1ae --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/96x96.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 96 96" style="enable-background:new 0 0 96 96;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#020203;} + .st1{fill:#B11C22;} + .st2{fill:#AFAFAF;} +</style> +<rect y="0" class="st0" width="95.9" height="95.9"/> +<g> + <path class="st1" d="M32.5,50.4V28.5c2.3-1.2,5.7-2.5,7.6-2.5c1.8,0,2.5,0.9,2.5,2.5V49l10.5-1.5v-19c2.3-1.2,5.7-2.5,7.6-2.5 + c1.8,0,2.5,0.9,2.5,2.5V46l10.5-1.5V27.1c0-6.8-2.1-11.3-8.1-11.3c-4.9,0-10.2,1.5-14.2,3.4c-1.2-2.2-3.3-3.4-6.5-3.4 + c-4.1,0-9.7,1.3-14.1,3.4l-1.1-2.7h-7.8v35.4l0,0L32.5,50.4z"/> +</g> +<g> + <polygon class="st2" points="53.2,68.2 53.2,82.8 42.8,84.3 42.8,51.9 53.2,50.4 53.2,59.3 "/> +</g> +<g> + <path class="st2" d="M52.9,68.3c2.4,0.6,6.1,1,9,1c8.3,0,11.9-4.7,11.9-13.1v-8.7L63.4,49v7.2c0,2.3-1,4-3.8,4 + c-2.2,0-4.9-0.4-6.7-0.9V68.3z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/NewDoc.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/NewDoc.pfi Binary files differnew file mode 100644 index 00000000..eaa949d3 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/NewDoc.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen Waveform.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen Waveform.png Binary files differnew file mode 100644 index 00000000..0734d231 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen Waveform.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen.svg new file mode 100644 index 00000000..e3af85db --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT About Screen.svg @@ -0,0 +1,136 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 2560 1080" style="enable-background:new 0 0 2560 1080;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#232323;} + .st1{opacity:0.7;fill:#232323;enable-background:new ;} + .st2{fill:#B21A20;} + .st3{fill:#AFAFAF;} + .st4{fill:#B11C22;} +</style> +<rect x="-55.3" y="-22.1" class="st0" width="2669.4" height="1124.7"/> +<image style="overflow:visible;enable-background:new ;" width="2575" height="792" xlink:href="OpenMPT About Screen Waveform.png" transform="matrix(0.9994 0 0 0.5704 -2.4364 354.1657)"> +</image> +<rect x="-43.3" y="-24.2" class="st1" width="2649.3" height="1128.1"/> +<g> + <g> + <rect x="857.7" y="346" class="st2" width="44.6" height="347.4"/> + <path class="st0" d="M896.8,351.5v336.4h-33.6v-0.1V351.5H896.8 M907.8,340.5h-11h-33.6h-11v11v336.3v0.1v11h11h33.6h11v-11V351.5 + V340.5L907.8,340.5z"/> + </g> +</g> +<g> + <g> + <g> + <path class="st3" d="M1096.8,626.1c-30.7,0-54.5-7.7-70.9-23c-13.8-12.9-21.4-31.1-21.4-51.1v-93.5c0-20,7.6-38.2,21.4-51.1 + c16.3-15.3,40.2-23,70.9-23c30.7,0,54.5,7.7,70.9,23c13.8,12.9,21.4,31.1,21.4,51.1V552c0,20-7.6,38.2-21.4,51.1 + C1151.4,618.3,1127.5,626.1,1096.8,626.1z M1096.8,440c-30.8,0-30.8,17.2-30.8,22.8v84.8c0,5.6,0,22.8,30.8,22.8 + s30.8-17.2,30.8-22.8v-84.8C1127.6,457.2,1127.6,440,1096.8,440z"/> + <path class="st0" d="M1096.8,389.9c63.2,0,86.8,34.6,86.8,68.6V552c0,34-23.5,68.6-86.8,68.6c-63.2,0-86.8-34.6-86.8-68.6v-93.5 + C1010,424.5,1033.5,389.9,1096.8,389.9 M1096.8,575.9c25.2,0,36.3-10.8,36.3-28.3v-84.8c0-17.5-11.1-28.3-36.3-28.3 + c-25.2,0-36.3,10.8-36.3,28.3v84.8C1060.5,565.1,1071.6,575.9,1096.8,575.9 M1096.8,378.9c-32.1,0-57.3,8.2-74.7,24.5 + c-14.9,14-23.1,33.5-23.1,55.1V552c0,21.5,8.2,41.1,23.2,55.1c17.4,16.3,42.5,24.5,74.6,24.5s57.3-8.2,74.7-24.5 + c14.9-14,23.1-33.5,23.1-55.1v-93.5c0-21.5-8.2-41.1-23.2-55.1C1154,387.1,1128.9,378.9,1096.8,378.9L1096.8,378.9z + M1096.8,564.9c-25.3,0-25.3-11.7-25.3-17.3v-84.8c0-5.6,0-17.3,25.3-17.3s25.3,11.7,25.3,17.3v84.8 + C1122.1,553.2,1122.1,564.9,1096.8,564.9L1096.8,564.9z"/> + </g> + </g> + <g> + <g> + <path class="st3" d="M1207,447.9h47.3l4,10.5c13.1-7.2,31.9-13.9,47.7-13.9c37,0,57.3,23.4,57.3,66V560 + c0,21.1-4.9,37.3-14.6,48.4c-10.2,11.7-25.8,17.6-46.1,17.6c-11.1,0-25.1-1.3-36.2-3.4v65.6l-59.4,8.2L1207,447.9L1207,447.9z + M1266.4,570.1c7.4,1.5,17.2,2.9,25.4,2.9c8.5,0,12-3.8,12-13v-49.4c0-9.5-3.3-13-12-13c-8.4,0-18.7,4.8-25.4,9.9V570.1z"/> + <path class="st0" d="M1306,450c33.3,0,51.8,20.5,51.8,60.5V560l0,0c0,38.7-16.8,60.5-55.2,60.5c-13.5,0-30.6-2-41.7-4.7v67.6 + l-48.4,6.7V453.4h38l5,13.1C1266.9,459.1,1288.1,450,1306,450 M1291.8,578.5c12.8,0,17.5-7.7,17.5-18.5v-49.4 + c0-11.4-4.7-18.5-17.5-18.5c-11.1,0-23.9,6.7-30.9,12.8v69.6C1269.3,576.5,1281.8,578.5,1291.8,578.5 M1306,439 + c-16,0-32.7,5.9-44.7,11.7l-0.5-1.3l-2.7-7.1h-7.6h-38h-11v11V690v12.6l12.5-1.7l48.4-6.7l9.5-1.3v-9.6V629 + c10,1.5,21.2,2.4,30.7,2.4c22,0,39-6.6,50.3-19.5c10.6-12.1,15.9-29.6,15.9-52v-49.5c0-22.5-5.4-40.1-16.1-52.5 + C1341.7,445.4,1326,439,1306,439L1306,439z M1271.9,510.3c5.8-3.9,13.8-7.2,19.9-7.2c2.6,0,4.6,0.5,5.2,1.2s1.3,2.7,1.3,6.3V560 + c0,4.3-1,5.8-1.3,6.2c-1,1.2-3.9,1.3-5.2,1.3c-6.3,0-13.6-0.9-19.9-2V510.3L1271.9,510.3z"/> + </g> + </g> + <g> + <g> + <path class="st3" d="M1458.1,626c-26.9,0-47.2-6.6-60.4-19.7c-12.2-12.1-18.4-29.7-18.4-52.4v-38.3c0-46.5,26.2-71.1,75.8-71.1 + c48.3,0,71.8,24,71.8,73.4v39.8h-88.2c0.1,9.4,2.4,12.4,4.4,14c3.3,2.6,9.4,3.7,19.7,3.7c15.3,0,34.7-2.8,48.2-6.9l5.8-1.8 + l9.2,46.9l-4.4,1.6C1508,620.2,1483.2,626,1458.1,626z M1470,510.5v-1.6c0-10-2.3-15.4-14.7-15.4c-11.5,0-16.7,4.7-16.7,15.4v1.6 + H1470z"/> + <path class="st0" d="M1455.1,450c54.5,0,66.3,32.3,66.3,67.9v34.3h-88.2v0.1v4.7c0,18.8,7.4,23.9,29.6,23.9 + c15.1,0,35.3-2.7,49.8-7.1l7.1,36.3c-14.8,5.4-38.7,10.4-61.6,10.4c-53.1,0-73.3-26.2-73.3-66.6v-38.3 + C1384.8,480.3,1400.6,450,1455.1,450 M1433.1,516h42.4v-7.1l0,0c0-11.4-3.4-20.9-20.2-20.9c-16.8,0-22.2,9.4-22.2,20.9 + L1433.1,516 M1455.1,439c-28.5,0-49.9,7.8-63.8,23.2c-11.6,13-17.5,30.9-17.5,53.4v38.3c0,24.2,6.7,43.1,20,56.3 + c14.3,14.2,35.9,21.3,64.3,21.3c25.9,0,51.4-6,65.4-11.1l8.8-3.2l-1.8-9.2l-7.1-36.3l-1.7-8.5h10.7v-11v-34.3 + c0-23.4-4.9-41.1-15-54.1C1504.5,447.4,1483.6,439,1455.1,439L1455.1,439z M1444.5,505c0.3-1.3,0.8-2.6,1.6-3.5 + c2-2.2,6.7-2.5,9.3-2.5c4,0,6.8,0.7,7.7,1.9c0.4,0.5,1,1.6,1.3,4.1H1444.5L1444.5,505z M1444.6,563.2h65l-0.2,0.1 + c-12.9,3.9-32,6.6-46.6,6.6c-10.8,0-14.9-1.4-16.3-2.6C1445.5,566.6,1445,565.1,1444.6,563.2L1444.6,563.2z"/> + </g> + </g> + <g> + <g> + <path class="st3" d="M1640.9,622.7V508.9c0-4.9-1.4-6.3-6.3-6.3c-6.7,0-20.1,4.7-29.8,9.6v110.5h-59.4V447.9h45.2l4.2,10.5 + c19.4-8.4,43.8-13.9,62.4-13.9c28.7,0,43.2,19.5,43.2,58v120.2H1640.9z"/> + <path class="st0" d="M1657.2,450c27.9,0,37.7,20.9,37.7,52.5v114.7h-48.5V508.9c0-7.4-3.4-11.8-11.8-11.8 + c-8.7,0-24.9,6.1-35.3,11.8v108.3h-48.4V453.4h36l5,12.4C1612.1,456,1638.3,450,1657.2,450 M1657.2,439 + c-17.9,0-40.2,4.7-59.3,12.2l-0.8-1.9l-2.8-6.9h-7.4h-36h-11v11v163.8v11h11h48.4h11v-11V515.7c9.2-4.3,19.5-7.6,24.3-7.6 + c0.3,0,0.6,0,0.8,0c0,0.2,0,0.5,0,0.8v108.3v11h11h48.5h11v-11V502.5c0-19.1-3.3-33.5-10.2-44.1 + C1690,449.6,1678.7,439,1657.2,439L1657.2,439z"/> + </g> + </g> + <g> + <g> + <polygon class="st3" points="1897.6,622.8 1894.8,519.1 1864.3,593.8 1811.8,593.8 1781.4,519.1 1778.6,622.8 1718.7,622.8 + 1728.5,387.8 1788.1,387.8 1838.1,520.3 1888.1,387.8 1947.7,387.8 1957.5,622.8 "/> + <path class="st0" d="M1942.4,393.3l9.4,224H1903l-3.4-124.5l-39,95.5h-45.1l-38.9-95.5l-3.4,124.5h-48.8l9.4-224h50.5l53.8,142.6 + l53.8-142.6L1942.4,393.3 M1952.9,382.3h-10.5h-50.5h-7.6l-2.7,7.1l-43.5,115.3l-43.5-115.3l-2.7-7.1h-7.6h-50.5h-10.5l-0.4,10.5 + l-9.4,224l-0.5,11.5h11.5h48.8h10.7l0.3-10.7l2-72.2l19.1,47l2.8,6.9h7.4h45.1h7.4l2.8-6.8l19.2-47.1l2,72.3l0.3,10.7h10.7h48.8 + h11.5l-0.5-11.5l-9.4-224L1952.9,382.3L1952.9,382.3z"/> + </g> + </g> + <g> + <g> + <path class="st3" d="M1975.6,622.7v-235h97c28.3,0,48.4,5.7,61.5,17.4c12.6,11.3,18.7,28.2,18.7,51.7V486 + c0,23.3-6.6,40.2-20.2,51.6c-13.7,11.5-34.2,17.2-62.7,17.2h-32.8v67.9H1975.6z M2069.9,500.1c19.3,0,21.4-6.1,21.4-19.1v-19.5 + c0-12.7-3-18-21.4-18h-32.8v56.6H2069.9z"/> + <path class="st0" d="M2072.6,393.2c54.5,0,74.7,21.5,74.7,63.6V486c0,42.1-22.9,63.3-77.4,63.3h-38.3v67.9h-50.5v-224H2072.6 + M2031.6,505.6h38.3c22.2,0,26.9-8.4,26.9-24.6v-19.5c0-16.1-5.7-23.5-26.9-23.5h-38.3L2031.6,505.6 M2072.6,382.2h-91.5h-11v11 + v224v11h11h50.5h11v-11v-56.9h27.3c29.8,0,51.5-6,66.2-18.4s22.2-31.2,22.2-55.9v-29.2c0-25.1-6.7-43.4-20.6-55.8 + C2123.6,388.4,2102.3,382.2,2072.6,382.2L2072.6,382.2z M2042.6,449h27.3c9.9,0,13,1.9,13.8,2.7c1.7,1.6,2.1,6.2,2.1,9.8V481 + c0,5.7-0.7,9.3-2.1,10.8c-0.8,0.9-3.8,2.8-13.8,2.8h-27.3L2042.6,449L2042.6,449z"/> + </g> + </g> + <g> + <g> + <polygon class="st3" points="2222.8,622.7 2222.8,443.4 2162.6,443.4 2162.6,387.7 2344.5,387.7 2344.5,443.4 2284.3,443.4 + 2284.3,622.7 "/> + <path class="st0" d="M2339,393.2v44.7h-60.2v179.3h-50.5V437.9h-60.2v-44.7L2339,393.2 M2350,382.2h-11h-170.9h-11v11v44.7v11h11 + h49.2v168.3v11h11h50.5h11v-11V448.9h49.2h11v-11v-44.7V382.2L2350,382.2z"/> + </g> + </g> +</g> +<g> + <g> + <path class="st4" d="M220.8,617.2V247.7h88l10.2,25.2c42.8-19.6,98.2-32.6,139.9-32.6c30.6,0,53.3,11.1,67.6,33 + c43.8-20.7,96.3-33,141.4-33c31,0,54.2,11.4,68.7,34c12.9,20,19.2,48.2,19.2,86.3v180.8l-117,16.8V374.6 + c0-14.2-6.1-20.3-20.3-20.3c-17.1,0-49.7,12-71.8,23.6v193.5l-117,16.8V374.7c0-14.2-6.1-20.3-20.3-20.3 + c-17.1,0-49.7,12-71.8,23.6v223.4l-110.8,15.8H220.8z"/> + <path class="st0" d="M458.9,245.8c31.6,0,53,12.5,65.5,34.6c40.4-19.9,94.2-34.6,143.5-34.6c61.1,0,82.4,45.6,82.4,114.8v176 + l-106,15.2V374.6c0-16.2-7.4-25.8-25.8-25.8c-19.1,0-54.5,13.3-77.3,25.8v192l-106,15.2V374.7c0-16.2-7.4-25.8-25.8-25.8 + c-19.1,0-54.5,13.3-77.3,25.8v221.9l-105.7,15.1h-0.1V253.2h78.8l11,27.2C360.3,259,417.7,245.8,458.9,245.8 M458.9,234.8 + c-40.7,0-94.3,12.2-136.9,30.8l-6.7-16.5l-2.8-6.9h-7.4h-78.8h-11v11v358.5v11h11h0.1h0.8l0.8-0.1l105.7-15.1l9.4-1.3v-9.5V381.3 + c21.3-10.8,50.8-21.4,66.3-21.4c11.2,0,14.8,3.6,14.8,14.8v207.1v12.7l12.6-1.8l106-15.2l9.4-1.4v-9.5V381.2 + c21.3-10.8,50.8-21.4,66.3-21.4c11.2,0,14.8,3.6,14.8,14.8v177.2v12.7l12.6-1.8l106-15.2l9.4-1.4v-9.5v-176 + c0-39.2-6.6-68.5-20-89.3c-15.6-24.2-40.3-36.5-73.4-36.5c-44.4,0-95.9,11.7-139.5,31.5c-6.6-8.9-14.8-16-24.3-21.2 + C491.4,238.3,476.2,234.8,458.9,234.8L458.9,234.8z"/> + </g> +</g> +<g> + <path class="st3" d="M430,606.8L547,590v93.3c16.5,3.5,39.5,7,58.9,7c22.1,0,32.8-11.4,32.8-35v-78.2l117-16.8v94.8 + c0,91.6-42.5,138-126.2,138c-25.5,0-57.7-3.2-82.5-8.2v144.5l-117,16.2V606.8z"/> + <path class="st0" d="M750.2,566.8v88.5c0,84.7-36.8,132.5-120.7,132.5c-28.2,0-63.7-4-88-9.5v146.5l-106,14.7V611.6l106-15.2v89.8 + v1.6c18.2,4.2,43.6,8.1,64.4,8.1c28,0,38.3-16.9,38.3-40.5V582L750.2,566.8 M761.2,554.1l-12.6,1.8l-106,15.2l-9.4,1.4v9.5v73.4 + c0,9.9-2,17.3-6,22c-4.3,5.1-11.2,7.5-21.3,7.5c-17.3,0-37.6-2.8-53.4-6v-82.5v-12.7l-12.6,1.8l-106,15.2l-9.4,1.4v9.5v327.9v12.6 + l12.5-1.7l106-14.7l9.5-1.3v-9.6V791.7c24.1,4.3,53.4,7.1,77,7.1c44.2,0,77.9-12.9,100.1-38.3c10.8-12.3,18.9-27.7,24.1-45.7 + c5-17.1,7.5-37.1,7.5-59.5v-88.5L761.2,554.1L761.2,554.1z"/> +</g> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT Splash Screen.svg b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT Splash Screen.svg new file mode 100644 index 00000000..dd30aef7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/OpenMPT Splash Screen.svg @@ -0,0 +1,620 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 21.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" [ + <!ENTITY ns_extend "http://ns.adobe.com/Extensibility/1.0/"> + <!ENTITY ns_ai "http://ns.adobe.com/AdobeIllustrator/10.0/"> + <!ENTITY ns_graphs "http://ns.adobe.com/Graphs/1.0/"> + <!ENTITY ns_vars "http://ns.adobe.com/Variables/1.0/"> + <!ENTITY ns_imrep "http://ns.adobe.com/ImageReplacement/1.0/"> + <!ENTITY ns_sfw "http://ns.adobe.com/SaveForWeb/1.0/"> + <!ENTITY ns_custom "http://ns.adobe.com/GenericCustomNamespace/1.0/"> + <!ENTITY ns_adobe_xpath "http://ns.adobe.com/XPath/1.0/"> +]> +<svg version="1.1" id="Layer_1" xmlns:x="&ns_extend;" xmlns:i="&ns_ai;" xmlns:graph="&ns_graphs;" + xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 2560 1080" + style="enable-background:new 0 0 2560 1080;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#B21A20;} + .st1{fill:#010202;} + .st2{fill:#AFAFAF;} + .st3{fill:#B11C22;} +</style> +<switch> + <foreignObject requiredExtensions="&ns_ai;" x="0" y="0" width="1" height="1"> + <i:pgfRef xlink:href="#adobe_illustrator_pgf"> + </i:pgfRef> + </foreignObject> + <g i:extraneous="self"> + <rect x="-55.3" y="-22.1" width="2669.4" height="1124.7"/> + <g> + <g> + <rect x="857.7" y="346" class="st0" width="44.6" height="347.4"/> + <path class="st1" d="M896.8,351.5v336.4h-33.6v-0.1V351.5H896.8 M907.8,340.5h-11h-33.6h-11v11v336.3v0.1v11h11h33.6h11v-11 + V351.5V340.5L907.8,340.5z"/> + </g> + </g> + <g> + <g> + <g> + <path class="st2" d="M1096.8,626.1c-30.7,0-54.5-7.7-70.9-23c-13.8-12.9-21.4-31.1-21.4-51.1v-93.5c0-20,7.6-38.2,21.4-51.1 + c16.3-15.3,40.2-23,70.9-23c30.7,0,54.5,7.7,70.9,23c13.8,12.9,21.4,31.1,21.4,51.1V552c0,20-7.6,38.2-21.4,51.1 + C1151.4,618.3,1127.5,626.1,1096.8,626.1z M1096.8,440c-30.8,0-30.8,17.2-30.8,22.8v84.8c0,5.6,0,22.8,30.8,22.8 + s30.8-17.2,30.8-22.8v-84.8C1127.6,457.2,1127.6,440,1096.8,440z"/> + <path class="st1" d="M1096.8,389.9c63.2,0,86.8,34.6,86.8,68.6V552c0,34-23.5,68.6-86.8,68.6c-63.2,0-86.8-34.6-86.8-68.6 + v-93.5C1010,424.5,1033.5,389.9,1096.8,389.9 M1096.8,575.9c25.2,0,36.3-10.8,36.3-28.3v-84.8c0-17.5-11.1-28.3-36.3-28.3 + c-25.2,0-36.3,10.8-36.3,28.3v84.8C1060.5,565.1,1071.6,575.9,1096.8,575.9 M1096.8,378.9c-32.1,0-57.3,8.2-74.7,24.5 + c-14.9,14-23.1,33.5-23.1,55.1V552c0,21.5,8.2,41.1,23.2,55.1c17.4,16.3,42.5,24.5,74.6,24.5s57.3-8.2,74.7-24.5 + c14.9-14,23.1-33.5,23.1-55.1v-93.5c0-21.5-8.2-41.1-23.2-55.1C1154,387.1,1128.9,378.9,1096.8,378.9L1096.8,378.9z + M1096.8,564.9c-25.3,0-25.3-11.7-25.3-17.3v-84.8c0-5.6,0-17.3,25.3-17.3s25.3,11.7,25.3,17.3v84.8 + C1122.1,553.2,1122.1,564.9,1096.8,564.9L1096.8,564.9z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M1207,447.9h47.3l4,10.5c13.1-7.2,31.9-13.9,47.7-13.9c37,0,57.3,23.4,57.3,66V560 + c0,21.1-4.9,37.3-14.6,48.4c-10.2,11.7-25.8,17.6-46.1,17.6c-11.1,0-25.1-1.3-36.2-3.4v65.6l-59.4,8.2L1207,447.9L1207,447.9z + M1266.4,570.1c7.4,1.5,17.2,2.9,25.4,2.9c8.5,0,12-3.8,12-13v-49.4c0-9.5-3.3-13-12-13c-8.4,0-18.7,4.8-25.4,9.9V570.1z"/> + <path class="st1" d="M1306,450c33.3,0,51.8,20.5,51.8,60.5V560l0,0c0,38.7-16.8,60.5-55.2,60.5c-13.5,0-30.6-2-41.7-4.7v67.6 + l-48.4,6.7V453.4h38l5,13.1C1266.9,459.1,1288.1,450,1306,450 M1291.8,578.5c12.8,0,17.5-7.7,17.5-18.5v-49.4 + c0-11.4-4.7-18.5-17.5-18.5c-11.1,0-23.9,6.7-30.9,12.8v69.6C1269.3,576.5,1281.8,578.5,1291.8,578.5 M1306,439 + c-16,0-32.7,5.9-44.7,11.7l-0.5-1.3l-2.7-7.1h-7.6h-38h-11v11V690v12.6l12.5-1.7l48.4-6.7l9.5-1.3v-9.6V629 + c10,1.5,21.2,2.4,30.7,2.4c22,0,39-6.6,50.3-19.5c10.6-12.1,15.9-29.6,15.9-52v-49.5c0-22.5-5.4-40.1-16.1-52.5 + C1341.7,445.4,1326,439,1306,439L1306,439z M1271.9,510.3c5.8-3.9,13.8-7.2,19.9-7.2c2.6,0,4.6,0.5,5.2,1.2s1.3,2.7,1.3,6.3 + V560c0,4.3-1,5.8-1.3,6.2c-1,1.2-3.9,1.3-5.2,1.3c-6.3,0-13.6-0.9-19.9-2V510.3L1271.9,510.3z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M1458.1,626c-26.9,0-47.2-6.6-60.4-19.7c-12.2-12.1-18.4-29.7-18.4-52.4v-38.3c0-46.5,26.2-71.1,75.8-71.1 + c48.3,0,71.8,24,71.8,73.4v39.8h-88.2c0.1,9.4,2.4,12.4,4.4,14c3.3,2.6,9.4,3.7,19.7,3.7c15.3,0,34.7-2.8,48.2-6.9l5.8-1.8 + l9.2,46.9l-4.4,1.6C1508,620.2,1483.2,626,1458.1,626z M1470,510.5v-1.6c0-10-2.3-15.4-14.7-15.4c-11.5,0-16.7,4.7-16.7,15.4 + v1.6H1470z"/> + <path class="st1" d="M1455.1,450c54.5,0,66.3,32.3,66.3,67.9v34.3h-88.2v0.1v4.7c0,18.8,7.4,23.9,29.6,23.9 + c15.1,0,35.3-2.7,49.8-7.1l7.1,36.3c-14.8,5.4-38.7,10.4-61.6,10.4c-53.1,0-73.3-26.2-73.3-66.6v-38.3 + C1384.8,480.3,1400.6,450,1455.1,450 M1433.1,516h42.4v-7.1l0,0c0-11.4-3.4-20.9-20.2-20.9c-16.8,0-22.2,9.4-22.2,20.9 + L1433.1,516 M1455.1,439c-28.5,0-49.9,7.8-63.8,23.2c-11.6,13-17.5,30.9-17.5,53.4v38.3c0,24.2,6.7,43.1,20,56.3 + c14.3,14.2,35.9,21.3,64.3,21.3c25.9,0,51.4-6,65.4-11.1l8.8-3.2l-1.8-9.2l-7.1-36.3l-1.7-8.5h10.7v-11v-34.3 + c0-23.4-4.9-41.1-15-54.1C1504.5,447.4,1483.6,439,1455.1,439L1455.1,439z M1444.5,505c0.3-1.3,0.8-2.6,1.6-3.5 + c2-2.2,6.7-2.5,9.3-2.5c4,0,6.8,0.7,7.7,1.9c0.4,0.5,1,1.6,1.3,4.1H1444.5L1444.5,505z M1444.6,563.2h65l-0.2,0.1 + c-12.9,3.9-32,6.6-46.6,6.6c-10.8,0-14.9-1.4-16.3-2.6C1445.5,566.6,1445,565.1,1444.6,563.2L1444.6,563.2z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M1640.9,622.7V508.9c0-4.9-1.4-6.3-6.3-6.3c-6.7,0-20.1,4.7-29.8,9.6v110.5h-59.4V447.9h45.2l4.2,10.5 + c19.4-8.4,43.8-13.9,62.4-13.9c28.7,0,43.2,19.5,43.2,58v120.2H1640.9z"/> + <path class="st1" d="M1657.2,450c27.9,0,37.7,20.9,37.7,52.5v114.7h-48.5V508.9c0-7.4-3.4-11.8-11.8-11.8 + c-8.7,0-24.9,6.1-35.3,11.8v108.3h-48.4V453.4h36l5,12.4C1612.1,456,1638.3,450,1657.2,450 M1657.2,439 + c-17.9,0-40.2,4.7-59.3,12.2l-0.8-1.9l-2.8-6.9h-7.4h-36h-11v11v163.8v11h11h48.4h11v-11V515.7c9.2-4.3,19.5-7.6,24.3-7.6 + c0.3,0,0.6,0,0.8,0c0,0.2,0,0.5,0,0.8v108.3v11h11h48.5h11v-11V502.5c0-19.1-3.3-33.5-10.2-44.1 + C1690,449.6,1678.7,439,1657.2,439L1657.2,439z"/> + </g> + </g> + <g> + <g> + <polygon class="st2" points="1897.6,622.8 1894.8,519.1 1864.3,593.8 1811.8,593.8 1781.4,519.1 1778.6,622.8 1718.7,622.8 + 1728.5,387.8 1788.1,387.8 1838.1,520.3 1888.1,387.8 1947.7,387.8 1957.5,622.8 "/> + <path class="st1" d="M1942.4,393.3l9.4,224H1903l-3.4-124.5l-39,95.5h-45.1l-38.9-95.5l-3.4,124.5h-48.8l9.4-224h50.5 + l53.8,142.6l53.8-142.6L1942.4,393.3 M1952.9,382.3h-10.5h-50.5h-7.6l-2.7,7.1l-43.5,115.3l-43.5-115.3l-2.7-7.1h-7.6h-50.5 + h-10.5l-0.4,10.5l-9.4,224l-0.5,11.5h11.5h48.8h10.7l0.3-10.7l2-72.2l19.1,47l2.8,6.9h7.4h45.1h7.4l2.8-6.8l19.2-47.1l2,72.3 + l0.3,10.7h10.7h48.8h11.5l-0.5-11.5l-9.4-224L1952.9,382.3L1952.9,382.3z"/> + </g> + </g> + <g> + <g> + <path class="st2" d="M1975.6,622.7v-235h97c28.3,0,48.4,5.7,61.5,17.4c12.6,11.3,18.7,28.2,18.7,51.7V486 + c0,23.3-6.6,40.2-20.2,51.6c-13.7,11.5-34.2,17.2-62.7,17.2h-32.8v67.9H1975.6z M2069.9,500.1c19.3,0,21.4-6.1,21.4-19.1v-19.5 + c0-12.7-3-18-21.4-18h-32.8v56.6H2069.9z"/> + <path class="st1" d="M2072.6,393.2c54.5,0,74.7,21.5,74.7,63.6V486c0,42.1-22.9,63.3-77.4,63.3h-38.3v67.9h-50.5v-224H2072.6 + M2031.6,505.6h38.3c22.2,0,26.9-8.4,26.9-24.6v-19.5c0-16.1-5.7-23.5-26.9-23.5h-38.3L2031.6,505.6 M2072.6,382.2h-91.5h-11 + v11v224v11h11h50.5h11v-11v-56.9h27.3c29.8,0,51.5-6,66.2-18.4s22.2-31.2,22.2-55.9v-29.2c0-25.1-6.7-43.4-20.6-55.8 + C2123.6,388.4,2102.3,382.2,2072.6,382.2L2072.6,382.2z M2042.6,449h27.3c9.9,0,13,1.9,13.8,2.7c1.7,1.6,2.1,6.2,2.1,9.8V481 + c0,5.7-0.7,9.3-2.1,10.8c-0.8,0.9-3.8,2.8-13.8,2.8h-27.3L2042.6,449L2042.6,449z"/> + </g> + </g> + <g> + <g> + <polygon class="st2" points="2222.8,622.7 2222.8,443.4 2162.6,443.4 2162.6,387.7 2344.5,387.7 2344.5,443.4 2284.3,443.4 + 2284.3,622.7 "/> + <path class="st1" d="M2339,393.2v44.7h-60.2v179.3h-50.5V437.9h-60.2v-44.7L2339,393.2 M2350,382.2h-11h-170.9h-11v11v44.7v11 + h11h49.2v168.3v11h11h50.5h11v-11V448.9h49.2h11v-11v-44.7V382.2L2350,382.2z"/> + </g> + </g> + </g> + <g> + <g> + <path class="st3" d="M220.8,617.2V247.7h88l10.2,25.2c42.8-19.6,98.2-32.6,139.9-32.6c30.6,0,53.3,11.1,67.6,33 + c43.8-20.7,96.3-33,141.4-33c31,0,54.2,11.4,68.7,34c12.9,20,19.2,48.2,19.2,86.3v180.8l-117,16.8V374.6 + c0-14.2-6.1-20.3-20.3-20.3c-17.1,0-49.7,12-71.8,23.6v193.5l-117,16.8V374.7c0-14.2-6.1-20.3-20.3-20.3 + c-17.1,0-49.7,12-71.8,23.6v223.4l-110.8,15.8H220.8z"/> + <path class="st1" d="M458.9,245.8c31.6,0,53,12.5,65.5,34.6c40.4-19.9,94.2-34.6,143.5-34.6c61.1,0,82.4,45.6,82.4,114.8v176 + l-106,15.2V374.6c0-16.2-7.4-25.8-25.8-25.8c-19.1,0-54.5,13.3-77.3,25.8v192l-106,15.2V374.7c0-16.2-7.4-25.8-25.8-25.8 + c-19.1,0-54.5,13.3-77.3,25.8v221.9l-105.7,15.1h-0.1V253.2h78.8l11,27.2C360.3,259,417.7,245.8,458.9,245.8 M458.9,234.8 + c-40.7,0-94.3,12.2-136.9,30.8l-6.7-16.5l-2.8-6.9h-7.4h-78.8h-11v11v358.5v11h11h0.1h0.8l0.8-0.1l105.7-15.1l9.4-1.3v-9.5 + V381.3c21.3-10.8,50.8-21.4,66.3-21.4c11.2,0,14.8,3.6,14.8,14.8v207.1v12.7l12.6-1.8l106-15.2l9.4-1.4v-9.5V381.2 + c21.3-10.8,50.8-21.4,66.3-21.4c11.2,0,14.8,3.6,14.8,14.8v177.2v12.7l12.6-1.8l106-15.2l9.4-1.4v-9.5v-176 + c0-39.2-6.6-68.5-20-89.3c-15.6-24.2-40.3-36.5-73.4-36.5c-44.4,0-95.9,11.7-139.5,31.5c-6.6-8.9-14.8-16-24.3-21.2 + C491.4,238.3,476.2,234.8,458.9,234.8L458.9,234.8z"/> + </g> + </g> + <g> + <path class="st2" d="M430,606.8L547,590v93.3c16.5,3.5,39.5,7,58.9,7c22.1,0,32.8-11.4,32.8-35v-78.2l117-16.8v94.8 + c0,91.6-42.5,138-126.2,138c-25.5,0-57.7-3.2-82.5-8.2v144.5l-117,16.2V606.8z"/> + <path class="st1" d="M750.2,566.8v88.5c0,84.7-36.8,132.5-120.7,132.5c-28.2,0-63.7-4-88-9.5v146.5l-106,14.7V611.6l106-15.2 + v89.8v1.6c18.2,4.2,43.6,8.1,64.4,8.1c28,0,38.3-16.9,38.3-40.5V582L750.2,566.8 M761.2,554.1l-12.6,1.8l-106,15.2l-9.4,1.4v9.5 + v73.4c0,9.9-2,17.3-6,22c-4.3,5.1-11.2,7.5-21.3,7.5c-17.3,0-37.6-2.8-53.4-6v-82.5v-12.7l-12.6,1.8l-106,15.2l-9.4,1.4v9.5 + v327.9v12.6l12.5-1.7l106-14.7l9.5-1.3v-9.6V791.7c24.1,4.3,53.4,7.1,77,7.1c44.2,0,77.9-12.9,100.1-38.3 + c10.8-12.3,18.9-27.7,24.1-45.7c5-17.1,7.5-37.1,7.5-59.5v-88.5L761.2,554.1L761.2,554.1z"/> + </g> + </g> +</switch> +<i:pgf id="adobe_illustrator_pgf"> + <![CDATA[ + eJztvfdeasuyKPy9AO8AKgoiMHIwK8GEYZqzoqIyRUDCCueP++xfdY/YYQTCOmvv+7t7neOEMYoO +1ZW7qjudOjnLb711Xhp5uSAkE+l0qdeoDzq95SR+mtxrtYb9QQ89ypxmk6JeEABoa894sgEvG71+ +s9NeTkpiQcQvq+jXmUorWeu8dLLJTBaenTcHrQY8Pe422snDk/Pk1ktnOEievfYajXZeEAv9Pz6y +Tu/QXLk+AHC9KJpF0UiK4rIqJk8OEUC9/Ue932/+D7wWNdmQ4dl2Z9h+a7Y/tjt/LSfzqpbMS3JS +0kQVfigggN3maaNPQakFWTJkTRU1QTMME34jFTTRVARVkFTdQL9XCoJkiJqpaJouobakgiiasiYA +vI7aLXdeh9+N9uCk13lt9PulTqvT6y8nS3/XYZL1D3hTT940Wq3On8ntVv31KwGIU5+qzVYDcPRd +HyRFGWFsa0+UnraHzdbb0fD7pQHYkyUNPZafcJMXfWgLmkWf0WP9ae8bnpw1BgOYD3SIsH66s+0f +BjzE/2XuThsfTbyCgNaHrN1sr9P9rve++haYpGpCUhQMwXp73vjutmABMKZESTcLalJVBECP4PxP +DHputwBTxL82BQlal9BSaUlDMqzXHt4afzQbfy4njzrthoWcrd7gzFpcRREE66/15nTYavQu2s0B +DBp3Y1rYOey8NVr2I/z7aqv+0XfmL3p/LYDzeu+jMQB66LSGA0y4htMDYL9W/7vRczoQ9adtQF4b ++mkPYLhPzfenPyxyf/oYLIu6Mw5E1eedSzyVvKpDf7opAK0khYIimUpSEkyLFoGq8HiUpCK7AxS9 +v3avW61Bo9eGFXB6/me6qbTfnmz2bbz5u1JxV6gjBx86UNgJ0NxxrwkIWTYNWFIJ9U2uvP6002u+ +eYQJbGNYf/AKFAwbVjRhrJIZ94mJ/6cDRxuiIsV5YiEMKGEAc3KWU3oqHfqYRigcnsG0AAulzjci +xz6SJWjBgZFanQ/rnfsZv4GfD7sWiizSAMo96TXbqM3EEX5jPJ20hvBqp9cZdvfa751ExhKll41X +kJdA3G/J45ff8AVkI2bk5Hmv/goNwHcXplBvdrOhzcHkeo2k9RJ+ib86/0b/utx4BxHk/dx6Wmn/ +0Wh1ur5mLbjo9k5a9Xa9l8TP3eZqzT/gTR3Q4zWIABuD6xgtAuF1AR24DQxCtR4C4HsV3VGt2WYa +wM/qvcGfnd6X26FPFxbq3YhWz74ag9dPul376UQtn9QHn6BPGu23vtuA9dXDMsK89Sx6/qV6q9X8 +6NW7n83X5HZv2P9Mnnc6Lbdtznu3H/87/Ar9MrrLs1fEmD1eb+QrtyP78X9YHyf4B+3jtrUmbE82 +AN0TaCnrN/+hfbm/4fUDL/9b+jj7+/ul02r2v72l9z05ATZsvrYaZ3/3B404Ms5GcOWtCYwaIJRC +Yc7+rAP715ovoWyJJvnebL8BhZ4Nm4OGx4md7y6yYpNnn/VuA8/AgTxzG1SxXvfppXw+SmFZSg8P +w9JMzoytJTgE0f4BgznuQUfZ8HfJUJlShL9DkH/s62I2WbAGg3SufyhTnlKQtEtkgoYGr5C5i/8P +maSCkPT9N9Vxm9YbZAQP/m41+oniQbvzZxt/SS4nMnegjuvD1uAhmywe1b8byaVE8awJ1nrDARGS +xwnBZ/OJyes6PDhNCAVTFHURzHZV1XVVUcFo0g3NAJ9H1cDYUmU9iUx5+CCqoiRpgoKfmLqiAJih +K6piGrrd9PVWwrGt4cvf8GUfPvyGR38CgpKHybsHIfkGj69PE3gQb4miZQisJJJFmAX8i+cLCPJm +G4myk3oL7LcGnuHJy5TnRLtKGIPX/5NgXKiT18TJtjtYZ0yjLGwNmvOvJoMLDDAOBYmC9cqSc6iZ +/89+DA1RDwMat8fqOGngitcT6aei8x2IDH1rviJs1Ht/W9+3er3639xXmgmMo2sqcp6TxdNGvXXS +aSI/4ymZwT6d5VNkgZbTT0KyuAddZMrNPrigfx/WkZGEXtg/BZG3dWo9wU6r4GvxtNE675xareHW +Tzr9JhoJfis5HSA/N/avROtXSPm8dOq9tyQ8KIIT+gpO59kA0PaRzCBGdAe5DbIEzPq+8wN7iT2Y +Ffuv2yTGHDl75J5v9Rr1LWzBWu+K14e1I+iVi2No7q/vVhte50H/9Jovw0Gjb//OtzL/cBNTaN8H +9frZbL31GjZhSDZqnLfoz+Dvro2czHy7/9R//3MFVsdZFj/kH/XW0AFFz/sBcG3fMjmAS/+b+A2c +vxg6/+hZ91vN10acaUfS2r+3ymzQzpYJgUsda5mnxl/jTkuSCyYKeZggJTVJUaKm9Xecaf39b08L +1LJRQPEhSRV1PWpOn43mx+cgzsQcyH93dpKmmQVZ1VRZ0SRdEaPm92fzbfAZZ3o24L+8do4iC5rO +S2cw6HzXGu8DR3tHz4z9TXzR+s/KxbPOsPfawBsEcUXkPzeg9z//u6U01sV/1Hv9/2Bl/C9ip91p +N2IgptV5/Wq8xcGMA/nvSozMSxPvrokx5gbEcdawTeLo+fmh/++wxWBGzfpL67/dHBud0Zf/iM3q +CPRfpmg0vddhH3TWvyvJ/jk6XO7XUewIefjAYv+66nP4AsbyHzSUfx0t341B/Q2WaNJxmBOOY/bN +jgDFoXIfMHqiqapsd+/bm0ObxU/IIDx1FJikSaISAriD8kUsUEV1Q0IcwGr9tbHV/mg1HGBDD2q3 +2up0er6GhaB2MeDx+3u/Yc8qg2JJBWu/2oeUs8sdnGGx3flr1+esZFDEKhT6ymf7s6LODcjBm2Ll +r26nN0AkutWH8fQPGrbDl3G6SAb0hbb1oa/rGzLmxEMi3qu9bPabL81Wc/D32aA+aIT/CqMo+GeG +pkv2qARJVNWkIZq8uCDd7CnC4mW93ex/wjwwXPg4fIHFvfZb469qs9e3f4K6TBqKErtfPJ1t7Lz4 +etZNVSxYSQemYUrQpCzqMZp08VpqtFqVvwYui4wyLrcRZlhxEXLWeO2033w96+aIGDnvdP3o0EUX +HbqhxF5ZNBPewo4yJhcb5JBcJxaFv5PvVi5Io5fs9hr9Ru+PRrLzR6PXRaFvW+caiuxSpyjoRtwl +9XBCrynaK7LWgxpDr94fNHr5P3C6R/KlDj9+pX1v6ievrWY3+dpBCvuvZK/xAcKh76x5XhWEyFE2 +oCn4gll0JF5yp8fl6czdYeOtOfxOejlVD2yQHCYjJt0tBJxGk6y6cztBSzJI+iLpqiuFt/bM5GGj +/5k8xThr/g/ehvH1Zv1CFohfHA8H3eEg4jeuquGNrlZvfwxRds5Jp4s2b9EPVMM0xBCBue1qS1nS +NT1MSHoqTw1UOLsoFajT9isRMVDrIdordYbOaga3aqmxbv0V1tFdeS7JdYaDFkqE6Q96nS/HMs1Y +iZ4+rfK1tVcdtloO/uyUMnjLtq75Wq+3B81kvdWs950VDJsaygqMN9pB469IHOAolB8FwdYEJn/K +nAhbXQzvMyeCB4EhiVFEUo1HYKpreViaNGSifokUyuRe68heU8NAPfINgDr3BRMjJejZa73Fn5cY +ijz/zBQjENHI5AFKLTf7A0/OjqJvkbhztgTD8YgSVFkyFZOOOZxsgggC2Zl8QdlZOOmPxjcSX3Vn +v/EVpbgCAl/cpaEtQ0eGASTNcMBcNm/pzjK9dZsFCuq73vc2Wa1H/W7HnoWsOUjt1t/eXPN+ay+5 +NRx0XPnqLJ6kSEbwNCTfNCJBew6FRUJ+eMwmBsLFxLPba8DyfbU7r18odf3DSumJ1ahvgJIPbtCr +t/vdOjhor38DTPMt2ecJOTPZrXdBvvWb38NW3ae9iBGeOFbNOYg/nH5Ut1S1DYxy5d2+HVFtZdN7 +onUFLfZrp/fWeGNpLVk86gyI1w62a0AFznb2XplcCXA5qp324Gz40rdSb6n5wXucmIN0PyCDYwWd +1f9oHA5bgyZYPk4v/S3QCI0WMJ6zXlR/nviRvRc4tcJ7I3lvKm0wVtzBUW3VOq8k2t1ffb803iwW +YN7Berw2++6vBO9N+bxMzREeolWz0tvYd4jTDho9etlRJx/vLDqd9H32zXW9yzGvRCkJ7LzTq781 +EYXX22+2vRVmYVk/wmmyKFcP/QgTHmNiET2JycrJ2chdWb+K7svHM5LgmXMnzb8aLZDT7zjlmkRh +u969+my2GkgB+Zffes2ogbaDQCEQ5LPz527zjeViLhXTQsvC6jHll5CMjnBRQr5AyfYFTv2+gK5S +jVnkWbQSy5PbQZ6G1e6xbUKd+Q0+0n5DYFvIcttClpuLswAvgDD/0U/DbH5fP3jowbOM7g03EK8z +M9nueL5gstnGXhbKAmqwOLVmEYJUEltIT7LIEuiZOng/d01XBhncpRHpgdGkEyHAHalU9MLPxd+d +l8JLc/BdR34yzV8WT/rBux/fXwUwZhpvnff3grXHa6vFQHBcekSDi5yxkI0P+w1g6m301Zs/20Xv +rdcvOG5B3yNipvW/uoVOt8/EP5m2rE2A4FG+fhe+O28jucPUMLzQKW+c1oSG7deIqbwNI9potFHA +kDLcqWb6wy5a+P6fn0D/nW5wizBrJ5LSGXw6GpDXKkACRTb8gZmw1l6///4KnSlyHOsEQ8lCAGiP +pmFe3wCHgisR2H3thLfQaQPvDSxXOTa/tMA9C+4Xw/Z8yacRoK8934JFdT0YAbbnBSACO+/WP2CJ +3zvBYO/tAZDXSz+EDBDIW8vjphgyJGJsVovd3nvHFYoybwp9WIs/Gq0QWdQHsYgEaQgeoBERKY96 +GLJAoAAI4rHwFe0X2o2PupcBGgMX7prGgPVILxMun7DoqLfbjkuYKXXAunxLVo93TrdkM3mf2Ts7 +ToqSpuh5aVkSBOU+G94iZvbOe7MVIvQ8lvLiOdiY5EF+0GzuqmwKkI60cpBk6Y5wzeFxhw1TkNRA +jL813pHm9iV2RSDc4mWsdMJxZAEiJ/Ol3uvHbxtU/AAVZEe03EfuUiiBgk1FIstnuNOgn/W3Rq8R +hlTEGO0Bzo/By8Nv67X3VuijeFHhj4jxE+ousC1k+bXq3cJniKoDsEGz5bffuDD9127r9e9gEWLB +vLb7g5hj+iMKrvcRC+YzJkY/g8feH7QQJaNkdmxgOgo/gt7Qz2x4z8gh6ljC5ESnhVx84GrPcI/R +n8U63e5b/BF235DOabX9/kEoCpzGNbDOeQTf7RV8DM/VJwBit2bLLDOwKX/Oq2gEQTVRMbGnA1RT +CYfsxW80wk4BOL+RzKcyjJOOf/scqAAE41vy5e9kuQd6rhchu6ABwlIO6MSPLV0NRKo1M78LEtKk +f9wh0/OWnCsjEAyO+gXDIPcLM0yEfkRwb41+86Ptswx51rfb4Iud3xrZc71v+X8hE2n1XIO32xnE +Uz2o/XBHBzXrOuIv6MSMcFjPTgySyKB5u5+d3v/YHBEA1bVLfmySkYJa6/RQ2MqH7w== + ]]> + <![CDATA[ + FTf4iiIHWw50EnndrKtPxlg8Xz9mlJW/Vq1WnYwmBS1t/6vZBZy2v4JFkr/V8MZ6DXQQRqODqz5D +IZEM9/aKoynEmQ1wyUeoa44Hi2pPPUByOfY7L6ie1rcYKyxyvTJEst4NtWPVwaF1ct4liui5/wla +sK2z0t6eoZYbaF6o1dy6+qznNi5fikIxd5jPbXwOZPRJUlZ/Lcvui1/uJ/xiRd44H2yX382dr93Z +07V6+V24WXffSrm1U+0zlZV311L54sJpIp3KrX+tprJHt2Zq6bMJr57fC6nccPkstXR4XU7lhUNJ +KK7dZHD3aqqU/aX0pf4hDK78pWwcP6/L24ZsaLfa9+1q/rna0a9k4c17K+w+NUqJdK+3vvaytdQ9 +2t88MPvrxu7qVaHauVUuK737W6F8W705r65trb2Ki1t62+5FnhttaoAxa3K9npJaunzcS+WNvJHK +vRbO0KwM9KySWnp6U1PZ5lcrlR1sN9HUsv6pKUunQlE5O3V7tia+3s2udmFW+9+JtD2v+um2/el5 +fwNPuFDsqz/w6azLgAAS+ka1fS5lzJs54W1exJM89pakd9//MApyzRjm1ndS89CLi2Vo1tiRr1Zm +3+HrTgt+fVN2hlfr93or/dvew8rJMRq3NVp/pzvivXL9WVvidQrr8vCxvet1S3WqSb+LqaBOX3qP +xdwVv9Oj7ezsRbdVw50m0lS3/dlHbTuo0931X3r7kt+pkr3OrbweH/I6TaT7M9VHxeuW6lSobh9s +BnSqzc61e9m1gE5vHoXq+9EZ7hRojJrrzE5qdV7cfzrndrqzK50Hole6XT9/xp0C2b9UvE4Rv6z0 +r3r3w/Ua6jbLrmrmRr5riVnoVOnQpPRQExz0niwsUJ2q6vdTF3eaSFPdok6feg+f7fOATjfrml6d +E7mdPm48n/I6TaTxqs7JmZV73lx7vdXUQ3/2s3HK7/RXKbv+M/dd43WaW2ktr+NOkbSk56rNzv4M +b3V+p8rNnVA9WD3hdjpT/TbTylH5F9WpLWGEaqe5H4BgbXb+8LixE9RpQ9j5NXvLn+nRlZD+Kl6e +JdLQrd6lmWZh50m2O73JZ6hO1Z8vkFm408r9V5Xo9HZdqO2ZMup00esUerHmuvv1oyvbGwqvU6H2 +/d4I7NQ4rDy+BHX6KBy3jtFcyG7tTg8Wbg8KYAlxOz07lPXATms7yoFCdYp6sbq9U4TzvbNsQKfD +wVlt507ndnopDJqBnZ43joqfWCPz5nq3J1w+LqzzO62ZC5eZ1NEGt9Or/OU81Snuxe72aiuzMhvU +aVO4XVp75Hd6tNf4nXpdzHA7fXiZgV4C5/q13FrYD+j0fkN4fNxS+J0e52a6i8CTXqegxbxun+Xr +TGCnMzePi0V+p6upVK9XPflCneYYpjmWNgFjM8bqK3S7/ENLpaHYvrQ7/TIXqU5/nvTvHu5UWljP +7JKa5iS33BlUUKdLuFNX71u8el5MHzcPT6HTzT6N4MpRV7A7HWzkKPWWFuaeLKaRHgbL+6R4+AWU +XKlcVFG3BWquveFzOrXQyOg30Gl1SIvCtWL+yup0QzzIU53OnO+fW+JB3rio1ZAW87qVBu2UVBq8 +ok4FBsGX8v3v6/LqInS6n6LR2+tVco56O/lmrIuUZO69B75Pya97+cC3ufW1Tivk7cl8z5X87Huh +svq25Pz6jFHLtc3dO+ftJa1VhNrp+RP1lqDk2tvza/CvD1Otj5C3ylwh8C2s/vHw63fwr88uTTPk +7WdlO/jt+Wnn2MMY8743WJIcpbZzw4rHC/3HeXtPs5lw+VEaUm8JjF3NncwF//pKu8+EvN39LAe+ +BYzdqne54F8/fGWOg98+ZpTrkLcfjw0PY8x7UO/r64G/FpXLfD347Vrh/SIEY+K2qSjBvz5alzvB +b49/lldCMLY83+08Bb5PzedKqvP2qUe/nTtc/3LevtCSLyUJ5W8PY8z73KpUKwf+Ore6clol33ZD +XbN52xp3nLPDdqhrtrt2Xv4SS9vF/avybGP/rLyROzs38qn5IXzaOdksDhZK1ev76htog5ky/hk0 +MLvgSUufrz1bbK69ZIEEZiqgDTZOCbnZm5EW1k7ytrNzeXPim+n6rLyAHUnL2Vku3d349X7x0Ggv +gFd6PcTuDqDjfZXXKWiDZTGwU6EqLB7yOsXWOLg7+mn2wdMRRKc394GdgmHclYM73Zm9vSAo2T9X +bXZh76Xw4XS60/J3uprK+jtVzub86D1RJV+nb/Pzs7hT2xrvL+jzHpnKRKfqZzazcdfid6pkH4I7 +nanWC4Ted7u1EQy+Q0Cn2izyHZ4DOr154nWKfTHU7c6cxpurjWDwHQI6BcMC7JF6UKenuFOXkikE +Hx1cBneK7BGSlObQ2yX3U942tRbXivTqB0DKJSFGi8Lz4vJWIFwi7UFiU82TFyioo1i/YRl3AWNn +3dyd+41pf93cuS5JCxvCHkKLTMXWEFcuVZZ8f0r5xZLr6ttRKnh26nETIFXN5rY7/SNrDPCpjNz/ +Cu6ZEkzQ/cUJ8sWe55FDNlx0OrBMaCoM9gvZPV0H5GzTH2cDoSfsq+m092e9m3k7t61taIWMXQA4 +DHTzxwIi4mx4yNvFz0Y5jf7MuthZ5AUBYQZ3ZbE+t7CLEeisvm/I65W0/WfpsEMNih1StR86pFlx +6UJcAn6Bf+78ERt7ZMc+pG9kKgeBSLdRjv/Ur8ueT+7Nz5bJeIby3MpBzSWRgPmhP1HrtzBDrZ8/ +ougtHuLPK79HxJkfMnzjrp/fE+etoND43bgcBVncphJpu7H+11VUY7GIvSNWHvo7FN6xvz86Za0U +YnCOh3egsRDMP/Wmwzkwl/dS4W5cZFGip3K/vkiKngoWPXbswhHw9vxHXI2bpR6BQHfADgITtvBp +nBTmLaOMxV0FeeJ7IaN5Gi6AZD9ccnFHbz14XPlYkR6G2wc8wR3AlVY0jzO1/Aw1NT5XRkxtY/nX +YRSin7PzmIfwQFCcn8Fy1+WxsFntHMzbq88h9sp9Oe9NiFj9kSaEXEE/nbtsSND5PPpzausXlsgf +K8K7OHMzEmJ4aPHJMRYxS9sZm2zWDovO1K2AEL+x7VtzP7ApbF2ENUbxXX1mJ0fyXZVW+Zw5J+Lx +3Ys0DOe7YnN2eQn/sWUg3mtwyMJPyTDQp5noBcV/Tv1BbIY2qpQM9K0Lfr8pNAYXRWZkL3KWS7D1 +mUMpcJLFz05uDZtfjj0WvSSWpUgsyU64FeaJHrT6AcLHAX/bERe3f1f5BkG0xUhosS9jEK6YwtfX +A4bBPw4pteTYY6T9FG097dBymkXWoqd2sb8fOKjGTJCu3HRHE2dISMIEm3THpy7PBisPb/0CDbqE +a1NEraCzfmZ69PVzJT+NrMv81IjhspAYY2RBjRUDm0qkR25MmCbGxOlNUpomxuTJMGarZZvQlljH +9WNXeFuoVwK9kkRs61javn6jJUegQRskxwYbqdgGNp8rP3ZJT3wSrhxszI3oKJ8sLATQ2Pb1ID0m +dtzRzFP22DjYERr1p4uYrsJp4Fyegjk5/kAcB8+1LsKGEjSQCMb1BhJoW8JQKNtyrIFYnIp33mP5 +hmCvDHY4KmoXh67DSSVB+YY5Ksjta3EjU+4Bi9+VfKN96H8MYtCiz4ahwxoOdn7v8ZzrENcaJzsQ +CEQWrDWon0Adz/OHg4fEEwCkvx9TAIClECHKKQHg5pBwSORhkJmfzvwSaXnj8vxwEqQ7KP+M1Mg0 +1QbY8nt4Q3D0+VmZXfQMhfeFn+sR4hkBPjnQ/gCHFQnPYnxkKeHEkBgFWVEsHkgMSCZTLC7u9ykW +l+fM7jDaSQuOLnkRku99ksXHiCXIc8uZOd5ooJdRnYp92n7nBwL8IdlAyf+9T1rw401NyTi8Hx3g +CBwIrZaDwhuJdHCAA+hgSLqUY03I/JEpC3aMuM8+3hgOp8BELMS8L2duR4g8MvEaR/Ij7IREbHii +gIg0kKZ0kTKlcdS6e0Aa05EdBJjS8sZFbjaCX6Jx1z0I3JMKV4NsRHFj+ddCHIogLeGAqS1Pzi/d +A1IDBtO5Rcl8St+4SIuT0vkB1nuJdLSsjZgQrfICYn2hEU5YppS3jTamAEDWxUVai4GYaFv2gFF0 +QZQcy5ZFm1Vk/sYv61kQ17keXzy+Q0I4NzpFENnpMKsSLJMSLvviajvU1HI/Dr/EiLeixtYGE8vk +67PRNykoSrUz7uIqvZCYPm6F3Cn07YqO1k40B/JaIXJ7rHYm3qnArRB6z+9XxtV8/sbI/cGgpjy9 +HxIWR9u8GcreRM8K4ZzjBaocaRkU90KNCROrG9uvRI0tRilCeo8vEJVnbQqVQeviN0BDJdplh5Vo +8IyUaCSNjSTR+l/5uBINrQtfy5WYpIrxJRrOU6rFcE7iSDR4cZKa2FK6vhhTolG8D+s2BYmGWiEk +WiCNRbYzikRzPXFeO5NLNNQKSDRyH9m/9fLL28fh2wLkgoV50JuAMd/WId9K6fpZ5TmbY2Pj15fh +G7Cj5Fzt3HTHdKOJLAJY0AjJGHdzATXl2y4N29+PtupRY3IMMZMIN6ZLYuX+nuv4jMLMl7Z1MXE7 +TEiLaCWRHqGd+CkQgXEY3E6USx1vNEziEb3DG2aS043FD2RZUbhQbXj/w2pDeDaKV40wFqQNQY4V +J/X4SiBGrocRHl9sbRieTeGjsVjasD7zMgb7+CuF0apeTWzfW6tG6sIxtRhqJ8C+H0mLQTvj2fdM +K6ALo3PhYrRDxrU4utDKUI2hDa/CtWG4LrQwRmnDPEcbXsdIRwrUhZSl9NQL0Yb+PKwYs79Ggbpd +YifRPzK/YeExZFDEogSdvnAtSo+37XWJwd3Q2GcMhuTyNhNVgMa+uFH0kcX2tcfgPIyN5Fw+9WLY +vL4sNW7MCTFDISR0HS8zz93jQ4MKMVmjTU1aLb30PbXk9fLSn064F7mUdCDSb/V5uWvBmWv+JXnp +hwQBCeby7fAGEsvNFLNtobGRd2+Cx7UhHtSmEyF56ceKJ8NKurGLgCxEtJbFWMm3mCwCTAxYl+3r +p2BLn6AI/nYFNSQeRbiUHM1eFuPabIEWoqbzPQuiWk5/v3vXE+nybfXybLM42K5Veo9rT2PW0Pnj +3PYnX11ZIj2dGrpcaAWdhbHJa+jCK+jQ2R3TqKHjdepV0Nk2zMQ1dOEVdLhacAo1dLxOvQo6y3+Z +vIYuvILOObtj0ho6PnqdCrqAasGRa+jC4YDGplJDF15Bx1ZyOZ9Gq6ELz4d29isnraGjEpIpfU3k +wC9urXWj3V6/XRdcB3ST3yG02CiDIoZUCq8nmvXEu+98GF7FVH86+aucSG80ngJ82zKl7T0subs8 +sfF0Fp5nNkuoQRtLnCqbk/kOqejZ1CIYTbwivLM2ucc3CT1F5WHNWpUp9AzjVs7FnZ8vplRmIlfx +kM4bEpXcx9kZiY30iMhVKL/ELJoLNZtpcuVn20Lbe70Jo36PFQ9tjq6MmQzCqXNLxQ== + ]]> + <![CDATA[ + i/RGh41hajcxSomCp4Z5/7ESNxkktNgteOPfnwkZWewWEjGOG+lFxW4Th6pulrqhNn98xIzhhgRS +cjk44SaGT0OmYC3aFixhRNSrUT55LPFQ9ZzeoAhJpNvrDSlGiamTqRJRZPoi/YQZSVGBMyrSC8gK +SQyIEzgjw4aKFb0nZfKX0ZuOOQGzuvZJbIuSx60Do8yuCev4CEbjqMn4dXxmRP0LIt1MjCGRmY5U +Dny84kJ3SLOBQ/KWzlu/8Do+MoIZcDRDzDo+cpMiaAVjICuwJsZnW47QWPgpCGxTgfnJqLHwUxBG +GBc6JSA8q36USQZVzIyFsZCtkNExFnEUQvi4iMAvouTB+g9dPbN9/RjbcAyxZQfrw6iKIY/ZA5qI +qpbzGgjaFxts0GeJME2cXIv4T4R03r7+mUvEKHGL4eztBjp7bt6FZz3zTbHdaPEQ0QAniyBkpQPL +wiIK5RLRi7zeD6kTJ00aPlN87Mbh/Sh8RrtwPr8yokoupnrj7klgCYNq9qL4fDGmHcnZ60Q1VuF1 +sbQdGVzpE2ZnkJQcUX4UUR5HSh2/AUmePpepBOvz0eyxh0E6HWyPjcTxe+Ec72EpEeFSwqByC2Pi +ichPRrVjo4V3Rq6M81c/xRiUVc42UkQGDcmnK8lBjRSRCRkSfabKBHiKishQkStiUGRERp4zv4pk +RGY/bkQmQR97RheOzC3PTBS2wIVNPs9itPIctuzrYD5e7CJGec7csrgw+tQov/J7f+KIDCpDowMP +Y5yntD96RIZXkYrK0CaNyODqPOqMuzERo8YtznHOGw9IdtkPLM8ZqTjH3eVZPhnQdagbF+mIbJo4 +xjLYcrOT1yQexMhkcE9tivBaugeTnBtGauTlX5nJp8ZJ2fMiijEpdPlXKlbqAkGfzJmQaM3jJANH +1tX5k2bps6FGqKsbIXc9wEtCiIlVQhKZi4Fwo8ei5Jhp6xnOPupZ9Hl98fbKYtTDxTmvb/J6ON9e +kq8ibvScOnZc/nq4MWlsxHq4wAzVqdbDTZyhGqseLrIidSr1cE614KSV4OH1cBxpGdTYBPVwTlXa +SKUaI9fDhZxDMsV6OLwuTEXctOvh7LsGprKtU2LOKx63Xmy8nEiebYmKz6aVE3mB3eiJef+yE8N+ +T0QYqkgM5UY3J5gcEtzOxMdaWK0E5ZyTdeLR7YxXY0/4L6idwJPKRjyLFBfXOUzIO01r9Iznmy5b +iAPPXCYMzCKIx4YxcxoS6ahapglTk72IIjQ2NTa8pKNZ41jjCN+ju9GMNY60+NLEbIhb8ZhwbO/V +aidmIWTI2R1WO5MedYFbiY7DxDPtcWO8bVeOhIkVl17gHAmMSrxqwaa0ey5cjP2nnfufqVSkijMR +h6jEr0gVZ3x+46QVqeKMEsNmiqxIrc80YhTdhJsLV1OqSL2aUkXq1ZQqUq+mUpF6xVSk+u6AiK5f +YxeMOgaaSFggk4z41RwUG3KOgUa1WBfhCixutu3USuEsLeYrhhsvoyeiFG7cs6BHK4Vz4pZBjvt0 +SuEcjMXx3scvhQv2K6dZChcYVZhqKZx9JxdrGkaXwsUzDPEhwsG11aOdCI9s4uAT4ZG0HLWubvQd +m8B1gcamdPEENJVIxzrIMFapmB5rLymGJEbb/RFnAvGOCaar0W9Cld5oygENSaKybsbKQfW4F90Z +xyVnd08cX5DZ/0jl27+erAvE81vyYyq/b1bQzekl9OkmtfT5+xz92US3je+nlsqnJfQHXchpzrnL +OU8N2I0pESVgvb6Y9l+7R1YoKTPSsskvdludmQ+sOkMVQ8+FvJ9iiE5zq9LHr4BOtVl0qfZd0HVx +tyEVdv2Z6u+wCrv3o7PATtGl2q/BFXZvYdVY22e+TqliN3TVtNspXQCGrpq+C6qwU7I3gZ0CeteC +K+yEqikc8Tp17uM7/ZYeg+rOQivs+kpwpztLz5e+CDxdYYfukv0Muo+vENZpbYHXqV1h1+tfrKcC +EKzNpjYf8ufEqjZMp3v8yV6IbOn27ZsHB5KfgnwcfrcjW9Rmfx4r18eRcOonpjvfOb2o6Oh+izJF +nShNhj3SsDoMviWJSrnln6VGWrCcPaK78kjpTaH3WJ0sVP1jHOvSr3XrHqtJ6o38QyJN4Micqwlv +kuNZv/TpDZPfJOfhydP1QTflxcRTzBTJqHwYdC/aYqDLxcnrC7/3bRqXyK0HXSFHWuOx6WAl8kYV +Yn6B+ckwqIi7BuIPqRh9z0gk0u0hhaVSjsQvK5H3qHCSXUv2TRPTraYLqEidcjUdz9pOcD0BvvEe +r5qOF/Bi7+KctJqOV0sXdPLk+NV0vAQQbo7iRNV03oS8bezQs27GqqYbMWo9ZjVdICVPtZqONRLq +VToLevJqOt4KWBJmmtV0PL+ZU8E9YTUdr5YucO917Go6/5CcWrrQmpGxqul4tXSxz7mKXU3HW13K +359CNR2vlo5/X9Ik1XS8WjpPI0+rmo63fs6NOdOrpuNZgrSlNHk1Ha8pK+dqmtV0gbblVKvpePU0 +42Ms0iYcBWMjV9OFYWx61XS8Wjp8ztVUq+l4fOW7l2dK1XRB2VDTraaLqBmZUjUdr/aL9l4nr6bj +1dIxe0kTV9PxVoi0YKdRTReH9yevpuMhw7/zPp1qOl4tXfQZRAFGKRqSGndIxM4ImVEpPfRfCqQD +uDfi0U2BXhK03U6NIHACC58qM6NZF6PdV8czeCKti5Hvqwu9jy/qvrq4eFrgDYnMg42Hp2jDgiaB +gPsrH/qfseMUEUNCoiDwTPsQuuQMKYKZEyMMKsoCCB4SIWFgULHZOWpINeU+poTxi0zSI1ob0B4R +KqqK2q+MFwYb+5o7P8YCL7ob2SSnr7mLc89IEPrjX3MXdluWe9Hd+MEY+5q7CfOTY15zF5WfjC+6 +m7RMaR/3MnEOVNQ1d7ybJgLSFCa45s7WleEX3a3HueYu5rlwB3TcYAxiWP7FraYap85C3rjQYxS/ ++kxufMIh1+g+4KUAjzy1bEiuwgiFdLEyD8NzrWHNGyMcwx80oRS9xzdOBezGRTqiAjZOVdoBL+1w +pCQyz+ZHZYbxbtiKVU9Uf1rwHGX3Hqv6UzYq0TaOokPlgSzXjZ6dfvI9xWwoaGxa2VAn3zGzocLL +POpPEWnNcSsfsxMV91r+Pm5ncVJlhVvhbsz4pWXMdpZGHw3vnsRsnNvJYmR2oaYKsaRlzMLaLKv3 +rs+neCogNBZdb5KIaeGhkcUSZv5wEXUDiB+Zb/NSrHtyfI5kaCEDOorZomR/KcPi1nL49l7MQoZL +3zm141emgHqLmHNU1g3ZWKyrZePdYNj/Ck5jjX1rEX3u8diVj2MVMtBZnRfT2NzFrUzhlADcTnwj +PjC7A7fDXK41Zq4qti1XhED/e+Qd3gVOXkUJ7yOXwmtBYrJhyA13I9RXTnDDHbf2LRbnjHLD3UTW +eOwb7qIrH6dxw51dWz1xO+H1RCPdlDe2L03dlBfMhhPdcBd8ylnMG6xj3XA37qnmqPrpNMbhGFFy +7Go6hbVO7dvUCmuvAr3qke56vv8Z83wquvIxP4XCWmiFimaNk3NltTOKa82PjVvtTF5YC62Engs3 +Wn07ui4veCP6+JcvUyVmfftTj2VDeBYa10rEL2NaWLsLvGg9ooiJo8WWMzETvWIUMT316HUZ/yoA +1Fio254YxXF/6o1ZxMTFmDYdvxJW8jI/gl8ZILiu4xYxJeIOaqLUP/emb8c0lLavb/J0jSudmjGq +Yei/KW961z3eUOn91M7IqDWu29df4Umzs75cuKgaVzprY9SjqfznXEFjg/BLZuPaMDdU9sZkN+XF +OqQKmgo9vQHdSDfpdY+uZ4EGNY0aV89gR5m6tT43h8Q2/VCZUmrxYEXERXqorO8stfT0dp5aPN/S +0KcTu7bv4EkVitdfuq141jpf/sE50SVcOYX4xa6dygRf/macFAU/Pslr7hbmOl2/ovNVTqmfmZe5 +0y+/J06WpoXeOPcYXPknVLe1Y6pTbI07F9215ZWnoNq/x5BOd1JqcKc7O70bX+SKLsOb/dGG90Gl +aSH1cMavTf8tgqg0DZ8J6VQ5rrQuuGV4GMGLm1eL3aAivODKP0Dvh0BoMbr272E3qOBQm01/FS9f +gjqtU51a+sVFsLAc2Gmv/7E/G9hp6n5WuwhCL0jLsLnuzVOrilgzj7vHn5wSzOFbOByOkCDI49lG +nBZnjtdSMeB6w6cv306oTcmM2emwLvx6KUOpzjD36fiUo+2oGw0oa/Vk/ptKnGRipxPcYtYdMWUo +uPrHPlot+BazuLd8Rd4Ys/aS8Z3bE1zaFDe1KtDSdW2Yk/ngBNORIkmApxM6AS04EzIUTwuR2aax +q9JGS60KKQDLRVWljUBPK0uBTY2U24NK3OJef0AMia2xgkEVRkJ6yJCcjA/eiW2jIT04Sjwiv1B5 +WqSbfcHPjWVOCLlZatPiakox5psluv5ljBhsJW5wK8bdT4+VifetXQlzk5+J4bNGTC3oLJtR4mM3 +S90x41rEHamV+3Kczd3IG/WYyDItLeNVAY56CC1HVwJiYtaLRYa0UFMoSysoDhP3Sm63ZpY9K67q +pUwH71bHkjD1mevhlHzk6sTBYC/Oj0rugrPmRgsEVDlx/lFPHiJjKjk2geBthzp1gRfnj1vdZgTf +0hvrjAhfni8drRyrkMzaR94JP3xhpLIonvFG3l9JRGNDqhMHG/tBSi2GNU4OipD3waWA0YWAc9H1 ++7FLOdvBF2/ZZ0LGL+WMvPSK1xS3gnsn595sPAllOU1ddiLuFxulsZvg+7tHx1hULc9IGIvYuRxl +XOhksKk11giUEZv+/RersWBndvwb9Tx+GacKMG4NYPxTNYKaiFMDmEhPUgU4Rjx5jCpAkgSCawA5 +N7CPUAUY5AfQkWOnLmm8KkCSSoJrABNpWrXwFQG/CjAuPi1dOW4VIEEgITWAXB85dhVg3BpAtsIu +tArQHg07q/BL+TCN/d77hy/l41ByRNXWOJfysZ7FP3EpX3AUbpqX8iXSsfA04aV8jtX3z17KFx6F +m9alfME1I9O8lI8+1fyfuZSPd9542E2BIbazNZrIs6Emudcv6myo6dzrN7WzoULv9RvxbKgx7/UL +v9VvnDwl3r1+4VEhrp08xr1+vAlN42wo8l6/8HBZaH3lCPf6hYYy9u098bFzB0iDPehWv+BTmke7 +128qlVwTZiv6K7mmVIjEudVvjPsruff6jRq3HO9eP5o+yVv9wm5nGOVevzEzVEe81y9ohVLRuXAj +3OsXi5InvtcvXNGR9tj49/rFrUqb7F4/b8ecd6sfN84/xr1+42ZBj3avX0DClH2r3zTu44suwI15 +H9+EV9m69/FNyoGht/qRO1bj3+vHDIloKuQUmpHu9Qv31AJyrUe+1y/8Vr+YNxlF3uuXC73Vj9pJ +HPtev1zorX5sHCa87iHoXr+Rq9KmVffgu9Uv3LaMf6/fxLwf616/cDPAvo9v8rqHRg== + ]]> + <![CDATA[ + 2K1+o97HN4V7rBrj3+tHtUI53qSXNP69fry6By/q7uP9ie7140/cYULnXp5J7/WLrkqbFhsG3+oX +LMdGu9dvVGt8vHv9mAxq4la/Ce/ji3m5ZuR9fJOe7GHfxzede/3C419+/RJu2off6zfGiTpj3OsX +7lCTEmb8e/38c2Zv9WO817ghaepev3AzJzGle/3Cq3ARjU3jXr/wKtyR7uMbO5pF3cc39r1+RCtM +Fe7oOVece/0ii+HxukzhXr/w7BxrN2Hye/1CNqJh4qEnHY1Y8xR8qx/WYlO41y/cbcf5llO41y/c +bcd1FlO412899Fa/0f1K/r1+QX5l4D28Y93rFzwk5H3HOm0+nmEYcqtfeK1o/Hv9wssfE6xCGete +v/CCWJ49Ns69fgHrYt/qFx1RjHevX3hB7Cg2TNi9fuGbC8zpQGPe6xd+q19ovuUI9/qFU0QiGFl0 +QSx7BHEJnn0EV3VbjOvclxR8iGlW4IR278JCu/zEe9/9YmThYpXKEi75Y1jP6RbB9shkc3OILcFs +oyBrr741l3X1pONhlPJFUe3brpFPzQ8rV9tXKXh21rVBnhqlXm9N2l6/eLjJpNJtXUktLAvV1GLn +7FdKWj47zK20lrfRuqz1NnMXB58LQuWoKwtVs7IpVLcPKsLO0vBUqOnbd0Lt7vZVOFzsF4SzlQVV +OHva2BYufr+8CZcL7U/hsib+CJfd/TnhauOlLDwcfx0ID4PCtfC0l28Lz4unKELyvPqY7fWO8gu9 +/taD1ut35vd6w+XBXX+2u/1ekGvG0K7s/OicrGsLqcOL7bSkZ+oLjZPZy9vz9bl2r1idl5Sno4Xn +s2Vj5nz/NZ87r54srP9abWg59z4+9XNht3Ffyeu1+d+wJLkqKnvLp3rNh2L6uHl4is19Dtv7q0uz +zZaZyrWUX8QVkOg2SSWLJP/qan6ZiyyMDpjwvPB8mPkVPtPcyqsCvxU21oXq+VVV2JntnPT6xtUb +riR1K1KV7HlurZjHt1HOWDWJlcrvYq9/31lBz2Ypg93iEo991te6vlCbZUzY0daaSfgvPEwsPlSH +6G7Ma+v6zKXDq+tUJl+fQSW2++jPCrpS8ySVL84/IrSto7s279FFmiYaXNpn9c1b2xCNwaaBV2vr +u1Prbx1cXT3mykvpYTWzu7cH/uf3ffUpe3cAPH2jIotjDkkYcLifZ/OW57Su3vfR16ItgtXdBVcm +q7sZdIdfC0WD0LU6u4tW3S9o7CX0dcn+WioU0NeC20QxV7m83kWjuZc3LrvbpWa9KArFdSXjDvNB +eMtklhNp59XyovdKrDwurzovNpe8F6DeauvOi2rB9+Jh+LjpvKgJ3gvcvdvLznIWzTWL+3aebeY8 +3Pl73qnmvRfYnoZntaKlF4o7xwI6DS6HtrsVmPMRrhgCdIgvj+n65kot1YOnv3K46FF86YooEexX +3iWLOWlhPYMs4V8FC+RVK0noq4AyTHri6+4x/mqVWouvt3cidgXR6t9Uc8XDsy8Z3p9bayBlteU3 +p9PzgtWLkJ0zBKl1lq4smdnHzRXhcs7vbbgXNlp0QKh0IkICLRYnbpFsT3AoGbcoFmf7q7mLlZ62 +fqEcbekPbxmsA6Ts7Vmq8vr1Y8Kc7yR3NZ7ludXVZ4uws8O3L2fiV0se7Uvl3TVElVcFe12k8u2e +aLFA+fXYZoZy60KyPw3vHrGSkSrZ+pP9SfpQUROq1cDT7SZq8baALlzqSE8tsGjwb5+Gp6LlI89J +z9nrT2c8t5JvPM8rb7/dF4rLIC++uTxfpUpIWqzIG+eD0+1W4WNx6+T1vVY+2Eud+W9ncEt1S673 +Ou9zTlwlKqeHew2n04cinoY8L10oeKzy/Mqdan8q15sunGxjTJ6//N6ix3NcOn+ollszr1un5/cL +lZf85hWSfavojHWHNwZdrVqRDg3KTCft8nkiprSuXv6Eq+V1LV0alm+rjevy7NuwUnrYOb1efjju +ZEBVH+5vZ1u9k83iQNvdrgiF85XbWnt9p3V5fZ9Ib+1qqRe/KgfTAQ1lAU8dmNhYwELBkV4nHXsa +1185R2Q+glPxNVMoNmdO5oHjvweCkCn0bdJUCwv4/pebPDaC4YGSxSdFgsLcRF+XcyLYSOuWyHQl +I/ziUrDkJjK7kCAp4KCruPNYuLXEKOl6Jlznc1fgrD56cZDBieniktB+dhb0wBLb4lIp3bWHDFIV +zRl5aqc/8HWvKKgvBwjbB4hfPA2y1C0J5f3VjgBAx6InD63FQesHonBt0ZnfUQ6vr2XBwTSQ0DvK +WxJtp7uMzuA+gj/65S7I5OevrQaQzRvgdufU17a8cZEaCsWTzUXSw+5S9iaM4aTkat81nu51oj1Y ++y6VTyVQrQtHqdxtZhNZJFV0wMU++pq1Lq8GTXuUWpKqpVR2cXsplTfylB4WkJbesAwZ1yqw+QVW +X1qwzh2wzxg4bCM6yPuEp9C/bGxeHqztVZ8G56nKs3b8WX1a2p3dOv5YrW2dl5W58vuv/im2N/up +h4Wy8JZul8HyvJ1l88fid1pubx+fbALjqurz1vnK70x5707b3zobZmY2VzeqBZfNfmxPHLNc8TPd +7gERnynj9iy9Pi1urtx/PpULp8+90vta+wVPF7gyesK4RSSsN0jb0jF9TtZyYNXd2AdBIH/BPaAE +S6xNpPer873Ny4Xrn83Obu8mLqpxz/JWL1WO0TM+eZLu+1f5fb47uyyXyi+V+vPrUzjSWZTfm2ym +yj9NYxvLv9zzLnx+niUtM87JF8U51L1ltIANM1OGMTyDDarN9iu55u3n1vnlVW+lYfa+YKapT/1n +97RWqdcXCpXbt5lPzIm4F89D6acnXWkuthPpGPhev3harAjF6lPR88lHmjhalxGmzpt4DEJLjDb1 +USYuSds34p7rI2febvwbIBwnJkyMCqmln+6VJUZzv2ZWkAQ9Ti10L+bQi2MkPOcSaSRhz5Cns47+ +vIEfdL2bAnd2KbWUel5A87scndkpVk+kR2L2MYkvkcZ9A+YrX5vtn+IzDOD0ZgRm7yyNK2HoCU8s +22zJHwPVo0wXCO1yDm3ung56PWkAllJB2d1eGFOix51zIo3E2cmY3BaX10CLTSrWYwh1x0saRYuP +rsPR6v8DRsskWmzs6WJ+GQfVI00X3/aLooIcQpueoRasxWjOAr7a+p6QxoLstTHkCvagt6/rA2aH +1x9R3fjQlx9ODk1w0rRN+LO6vf18NHuEv5oVYyjtqEPxDFy8sxp4dxsHW7tqqld62C+fbVekXQTy +ulTpPS4/kh6feidsFvvdT+z2ER7D7AKKTuRwzMw+7Kv6lPPcQ3nbkA1tdrl2urlcaTS3vzKZX5Xd +k6a4+bNV3CrvfTf74P6K2zgKh2OY5aazBv0jK6y4sbI+T20VxO+5Wyptfy2078sFfaGfSMc2J9Kd +h9KYnUIvB3Pl+/LOztoXLLL8M1KnlBERaD1Zp89NZDjGkOdkdHSqqswnZsBSQvbTmPiOi23glwls +xilYSlNUopYnPrn9EG494Dj/iH7C6PL1f8UTRzS2XLrbd5yztdOJF56H9zD9QnU/QQwgkY6rVaAr +/cTnpvzMfddIpWYFC70dei9YaOsX/MPSdkGQQG/cHG0tdYXNrddfR9XyRk7d31rqXAjlYf3zCpTM +89Hm+3l3pdJ7eDle2RAvDRxbLG8sVuTq+YZ+8w/plxgCIJEOFbjjuquUuB1Rv4zZM14X3PfcHGjV +n3vEgcB8pcd7nmoNUKxRYYnp6pcgzYaz0+OiepTpgoTJt/NGCzOc5b/8+oeDMbZ+AbHenQuPDIza +/ZVULhRWr8CAfhAR7+9/nY6w0uOYUHZW50j6dXQS906dnUIYLLBnMufqH6E2QDnOVInPXmNON0KO +TUBj/ukiT3wE9hqTuTw5Fn+RR2euGDTmZ68xmcutF8Mbc/UfgVbvvjQNaaClske3JorrbaI/2yiC +d2R9zS5uS6n8feUMxQRrKMJ3gF4UUJ7DWSKNHlRT2dWjZZTzIPwTob9fYMNMOdrOM24S6WnHKSaM +84/rrDtx/kk91bF85MniFLyJJ9KRFuUUnJhEesxQ5Ejdx47Bjh32R/b0xPGxWBRv5Y/RfU9x4fGy +h3uvEa7EPxmDZdwnfnzsa2bs+JgmZ/OF0sO+cQxf3094vgrPU3FySOLy+cbp4v3W+cXvx1hKBn+q +PqWQBatkXywlix02Iikzrmzfqj59tRarW63675AYrCfd8CcrUTG1dtsZl9DEhcag9Kk/NGLvJP7g +dtCn79/9p8ryuHtJERMOm+4DUyIUX4uREx5lup+5gXI1HFeOTTDd2874cZhRpuvJscnWN3y6yIaZ +xvqGTzf2jlXE+saO9U20vuPvWI1PzvTqjhZPHne6iTS5vlaMy3MR4qQhO4uTIZyKQoe0LXGtgPND +KwfVn5nJlGfTqZ1+t6C/6bNr5tVOFXdF5FzNlLZmXPfjkrgix2oi629iqVnxLTdO6y7NbVQ8HS5l +zBvZryvx05mZ9Y0dZyeqs+ifP8L3TLXqS2oSGjNq0UMBfnZ9fbLlNtDMeHmJjhxDuXJWEl3lfiWP +EidF8NTLSyjHzap0ws9QSl/BevbQf0L5ufd9KxNyXX0a0HFCPL9V6eOXLzv9wUtMz628Hh8KxePG +DE6UJ68XWnAnhO/qqdsXvjyvSR7jZjMbdw8wTGUHXogFKqqQazvrMkyhTlPWGij9u3mswFGn11ZK +PHtfkGRfjoAxtp09yDk3B+0m0g+Fmd8XmKqhW5wBeNJxx9OiT3q3kFDunxG3N7koGN4duyi49lBg +cWXOh4S7nvziIGFZ9CGhdPv26CChQNz99Kz3wlGAbZjFOR8SNko/Hb+PnCn3LCTYnfquT6rHQAE+ +BNCJwsn68ZeNBGkoxqSDokfY16lOsWKhoLd9c+ehAN2WdY8rUm1K4J3QZI07vZp65pESbiKogazX +QCI9XhOLI40B1oVtIjc6R5ANLFFjsG9lGmUa+dFRSXFltjDpNIq8MWCMxZ6GEJeoqAa8+1+y4phN +OA0sZQN527ZgI7l7aXES8YDj/EuTSpilpYAG7LWwa6xCMZEPb4IdA91AwZ/TG7ga8uavTMZuor5x +TDbBJSpqDIl06DSE0XmDbED0WeMhmAjhjSXJGYM26zFXf2bnoETAFRc9jFm3/p04V759SyRkHAIJ +nBWmsSItd0ZFTDGIQOwx2BgLI9NiYUJ2LRZ9NDbmNIQYqEykQ6cxqdQpSoE0Fpfji3L0GHaVtcNS +ztHIvXVVJpqQJpU6kkdSYI+FjyJoDBFERY0B6xd6FIUJKUKijRtHI8cXXNIYUsetSwKXCzyrzb5Q +VNeW/EG59dNm9Xxj+bbSeyzcbu1qR7ul+sf+b1SFokrbtxcbdgWhWipYNSr+exfdXR4x96WjqyZv +Hp1CncO2WwTz5NYJPefWT1qondklp2LzpodLAYXiIFPAnoVdsKQWFpxPYgbXiGGM6Q== + ]]> + <![CDATA[ + YOmr+iJ2OmCae108IfwVHJa7njVQ+4fVouOw7Ar+OqCFzye3nijrf2EO6miH136V87+q5V7dF3n/ +i0fdrUo7KJLVRh/uC9H3Ymf7x5LJR4v+pxeZFwf8aMn/4kNxqyGPCriGVdyd27TK9cTdfBUx6ZFo +Xche3LmQEDIAQfWB6daKwrKhqMKSjcCTvbxDAsNFu/DppFbAIOAAXgOVnJwKqNpetGuaTq6tZuWN +c6AzXEiWnStkvSrOPbwG0EtJXLE7vTku+LaxG4/Z99KnVj3ebhmzhi8PHxflomJ4iw7oAxadmn3c +y2lxii1CexeCLzZ8sbZcSqS3LuZXnkuf+kF363yrjesBTWn7bvHRLtF8vXaLC89lj7D9pcEvNa/M +tw6kW/iGZ6cW2aML2LJYV8Ln+sxlHn8CpJ8tWp+2b+VdXLkgPQxX9/Anq0iv+PKh4lpnm+++LgrI +6xTdor88tL2VsT7V50pr7up/1b1y0xc/T399KA6D3Kq+Ks6Niyqq5sXbs7f5crG+Or/80V64qK5W +lN+U245Pbrn+ZZ+wZv2xj21zNgNqq5ga7U4Hd0U0RkRtW4vWp8r9fs76hMbtwH3I+Jm0fZOreLvV +9ni02+/dVvk93T/fXN24VrYOrgWUtfugAh/8ytiFoM30PK4RdA5haGz7IhZ2heT+ty0tYH3tuldA +6z6urc45QJc/SAIVkMzK5Lwqc7f80fa/QVJZtZKLW2tdQk6dWVWq6LyZn0TaqbXczOPYG7rf8Ta3 +vnRpi8LGib5qnRHxfrF8xz2g5P1x896WoO4f9ALJtiIWa2TloznIWRLNGfKDLTzXS3mreBKY/Wu7 +NdOukhJNwAWOqLzxzU/iO8tZxDmH+KzOmg7YMax6ZCTHDGDDj2NLjuE4E4gw1ZZe6CtIr6Jdh7lz +LOB6VXlu5WPLadsVYc85+0iXzv9ZT2imKCUNXZYKopgsng5bjd5xr/nRbCeXEiuJ4taeKF603zrV +XqNx3vhrUO68Dr8b7UFyOVncOivt7RlqufHaeWsklyxi0l1CztviwBYg/hNdiJohHBXdLr+bO1+7 +s6dr9fK7cLPOHBAh766h7IFTK11g1Uo2WPpswqvn90IqN1w+Q5kC5VReOJSQyuQek+iTS7fa9+1q +/rna0a9k4S1H7qqhsM762svWUvdof/PA7K8bu6tXhWrnVrms9O5vhfJt9ea8ura19oplP3uubs2M +cc4DKnOa8KSHGOc8JNITn/SQiz7nwUlKmOCkhxjnPIAimfSkhxjnPEAvY5/0gFQKYS0E2gpuKGQa +1kKgrUAkvU1Bt/NthUR6itZCoK2ANvKnZi0E2gqoUH9q1kKgreCZEVOwFgJtBTeFaxrWQqCtAMZ9 +PGsh/KAmhyLqP4Z71NSRl2nmpgpJg3mULaYgfXCJBGU5NVc2Vl25uYFe7FvVpPBiw5dGtvgz23O3 +49zdwuK8vXvjHGpz2LYZ+7ixaDGaSx2YsNFJFnnEqYJ1CsbGVaWOSRMdnlewpSW6yR6k5RIpLYtS +OX+HixDAwXL9qDw+9Qp7WegQrGvX/sjaByuC08U/wlfdE5jVxy9AvlpHThQo4YrnCrLt7Q4PGflg +ecv9so6A+FhzzAhCqqKNiJZlyuTn9twDTI6y9pFG9oEOtZkCniGWptiYwaaMfc4FiFEk0QT7UAkk +Qe1DJY4E24rRal7bkntcTp68uoA9KsR/sBmmIub8WlowEad2FX/7wjrcDcC0clT+5WwA9nLUDmKv +t7px5sUihL37pQLZwKed16mcDpB1IfUvOFuds/6tzltpn9rqFCqrxxV3r/QX28DQ1wBYF/O787t+ +zlLOFizb3DrMZOlIgnVZR8S3m0OfCu6zovsM1mXp7AcFEnqWgS1kCrdMGsf4IRy8a4zTgqydzZS9 +37fyfepuMt5YyYgB24yI0TjbjLE3GWGSiw7GxtphG20XgRcIU35cJDz6kSB9/HKRcOtHAYzGjwJ8 +0SiBApuS/Uiorx55SJDvWqLh2+bHZwD6Oo232ew7zmzS+PDmMHCnNZGOtd2MLsVwsHgfRErBDeCr +Ps7akzTh3P0Q0oCV9BbaBD63Poojwhq4/3ET98edxlMvRgOhXGmdsjrJNBoDV1qOO43PYKKiGrC3 +2NkmWqnJkiBOLj2SerQwNip3n9x0RhIPMBemifvuZBLm5IkWUQ7GYq/GyUuPPwZ/A7dUA8SRrOsn +jX7kaoRlY6yffA54hI2OmIw9ja/hmMzlpD2tn3RS0ZgI442TnpMypdx8ew0Iu4c3ryTGbu5d2rnx +jVaoPuwShyLfPMUgkGuSxuh0n5efyaTOTYMmEJfG4pLpzWc0gTBjoDD2xSWREabRHvJQiWgs9jR6 +40odl8ZuhjOTcfzt7Gy0Rpb6914TqYXLzzN/Ey8vE0kdnIzY+InCRMQYPgOkjjMGS7+EYeLlqz8Z +Rby0B14p3XiC6+UnjtThauSu7ZAiI/F9OWP6opCZu0UFncx0jbzlGvKRj510/F3fdhwKpRdtH2Lt +RnFpzE62zdnHJHqBp+qTuxV40nG83LMuOJcf61bg3wpaoQMTcZjAOTCxaAX0wY3O2NsG+LovNJ6s +7UMv/KCg+27O/rqcydseNP4KA7X3BC6WB86OwJ4vCOgPg6zrWX9E0T+D9bWc74UvJLJeyvtCjb5t +n/W9ot9b9iLv68eir5dHO3S0s+aFOR/9Pe+UlnwvfD3v7BXQfskiOPX3OFSTBav99xAfjYi/ovIA +51xEb5vYPnHypIrjg4vehg3aJUEuQM0Ak/wYuOTkuGiFH7evf+bg64VoxRut0NfJnYyPlMUe3zmg +f3Z1Fkcc7ejaTc1DzLPTy0jhKzeyYV/x5d/sMmGM7dIkLVLtUeWa2X3jl2nsVDcXGuflvbe1lBOe +vBYd+j33n9WabT3NOufO1hVfqNFHXi/HeTuiaEd9Xy6KNuG/XAviS20vC5/uRDvY9PIorZt7EqzV +S112Pr0puAl7Yb9O81b4ER3yD1+vnQj0153gXB9b/HoUfcFGX5T86032vXhMyU/2obKHP87m2de3 +Fh4vdnmfjO96f+j47uDaCyK92qQyeBTEHXEzA5/qovPpzQuRvuJCfRQhGvxWmPGog8vqYnZ9Rb/M +zxxWnw/T1k6kPP/U14X9nRzaFHvQ7FCqFZI8bPMOdsXrMoWjXcMPdoVepnK0a/jBrpiSp3C0a/jB +rvbOiDDp0a7hB7siXek72rX/f9YTK9Dznig+Vdpv/m3IRDoNT84ag2EXAahP242PZrtW/7vRS4hJ +6z8B/kN/dTMpSkZSUlX4oqKntZdEBsMmxWyy1k4Iya1KIv1U3OoNys3XQbPTrvf+Ti6jR9eHtYu9 +cnI5af3gCX6wkszAkIQngIZXWbQB+gTDfELNwP9f/wl/jhNCwRRFXRQlXVV1XVWg84JuaIYmmqom +wh9ZhyfomyyqoiRpgoKfmLqiAJihK6piGjqeBTRaT1iTkdCXv+HLPnz4DY/+TIpC8jB59yAk31Dv +p4m8qhZkyZA1VdQEaAmmLwpom9eUNQG+6nLyOyFpolIQJEPUTEXTdImFqbEweUlCI1YEVZBU3QAQ +tq84MJy+3vn4F5LFvfYgmTm73Hk6+6x3G+d/dxskyhuJoftPQRIUQZRFwZQEXVRlhE9TURRVM0RV +F01NRE8EQ1QETTVlWYLVgSdAHoaua4ai6LJpIhjNNATZlET4oWbDCPBzQTcUWZR1yfoVrJD3FK2R +oeoFXRAEWE9BF+CZLgMG0QNFUwDIAMybMHfJNGFtdVMyNZOFqbEwsqEVVHgAL2SgEw1gmL7iwHD6 +mgTziwjnBowTEK6IkgmUi7AH9C6rmqYKsiopQAOYzgVd1hVDNQxVk9AqaDBKWCRAoi4L6Ingw7ii +iviJi2/AtMR5gnBuagWdwKdkFFQS5wwM9FQQKFxpMpCPH58xYUQA0VRDgu8yD4YZTy3GmGuJKgiy +MlCCTkHKZuTsGBjeqDgwqjQGDGd2NAzCEnogaYaqKjDAABgRHoAs1EAmSlwYQ2RgmBXhwDCrz8Iw +eI4Fw46ZXS8WP9FrGg/mPbF4wWdePYJ5L2xtIjjaBP55S2jJTDZ5fYXfEg8oKev7BwkAoFJL4cmy +qau6rMqCBqNFbC+BFDCAV2VdRLIePdFUEMemqCNhq2OWVkHPgMgGsaCYooKEhSUBBEsGS7Ge0AoS +VIBWMABfimGABpOTiiqD5EcyRtAkVRCBb0RB0/D6mboGvassjCgoUkH2t6KJBc2/4qDJVPwbSVGQ +Vkkquka2UYJ+RItOZE2TYGBJxTDx2DRZQT3L0IqgFFQwTgxSOjvPJeo5ahO/gfZoqnDeKNQbPAok +z8AgMVRTSYIGxMrB5UpmLqDz8WyBtVQwVBTcr2IxpS5LiiSaSVhf/BvBgIU1JTQXwCrRj6mSrTBr +w0C8JpDhhGbB/BaMJWJF2P5FzVJ67gg5sxANjB1XarC4EGGN/LTB4pOBoNcC90PCMCtZ48AwVECP +1qEfe6F0ds48KkSYU/yUCrRMzRnjnMMFUbz0CrIIaSt2ZRWBtn8QfbDPradgO5Lzwk9FndQeJasN +YD88H3cUNfu5pNHchZ4KJs251FgZCJsKNWJt6FYYCKZ3BoIed4nth5lzzeYIP18x2GIgWCxHrc/r +BFqF0Aj/tlnIETICZjNXDAApgkPCPgd2k8nFAAeGZn0Kgsv6MkUYPNYnYRRToVlfEygVZZLEw2FO +CgLIWJCpfthWRIsQNIAFP40zEhqCq5QoGK56An+VxSgaocpZi8hVLNmGMoedFdJ8R+sNlgLzXBSh +b0IAg9trIDYyBAkcJoUDASws+yHQelMwiIUp14CFAXdM88OgERb85qymqAUFIFQZjCTJ4IgtBgKt +ty6S9Mm2AnTF699+To8di1z0hpl5zX0DWKPUBozCL45YzEeuGV5dGkoXRIaTBU0hFCADAxRmYgqT +aHNFIo0zDVQnY/QIut/LSAIoCQND9hG+ihScTPbPQrAyg4HhyAwWRtBoXrJH6xoDhkavjEQacYou +sUarbFIyQ6MVKIVzRTGiZBMNgVWsQRrQTCuiqlAykB6JiCiNOwsR5k4YPQwuRJB0JJVqdP80BEfy +0TA8ycfAAI3Qhh45WobO8EwJCA61Ar44dE7jmcMhUXxWisGLyC0VuPJYVBl5jCUE+8Z5bptSPnPQ +es6RvtYbnsy13wDFI39aFwEZhmQ6bSEJGCFfaQhMsxIjx8lWGAi7f4x7SRU5EByZS8PwpC/WGAR1 +0lhjIFh8R60VZ+UZmH83IIHtzv+4KASwBRCHxKgrsFNJL5aGqSEYg4SRRJLVEQwtDoDJ0SLJkqka +pqEmwfoWSH9dpk0vGf2OfgpEDqvKg9ZkclQiNXIGQtVMxjhmYCSBMY5pGMU0KNEtqwYl3CXKo5VB +mZB+sUjGyNFYZIP0rsE4Js0/WZJIFaXSxjG4KpSyVGi/ClaC8uKZViRdpyBUjAGPlQ== + ]]> + <![CDATA[ + JQ0rCPcniqozviQNI0O/hEdaw3RJmJmy7Yj4IpkMDJdGefRdc6IB9khcvCHvmY7BAYopE5BW/xKM +jVTQOo03e4ZeGzqDfcAjSXFMK7JAjQScGVKQMhA01SJqomE0y9VyFXGNA6MblLJGowVxb0jkb9mZ +0hCIzgBfBNbZVnTKndF1ysxgVg5MK5qHI1d3EoXwnxVLsEQkG8oCXUOJcTrgIgODEtaEYNIClYLg +iksahicux4BRDJESMLJiCUBaRdDizWTUhUSFMdlWJJ30bVEAkoLQ0PasT7yBh0GH/mgY2dRY0SVK +hCCVDYY0GRhN0lj1qgoxYFQSRpQZyx+xChFYlwSK4SXDIIPmLCXxaNCNfrAikMItolbwrslgJo1/ +WTApIWmS+GchuCLQZAwAVgRSMAYrbKnRMoF+VhhSEJgqBcIA4LSi6cTaqGjjD5kImizrhim6VOkT +hirjFzEwgsqhAhq7Mk3/RsQqRq8zpghMK4rIyCswBtnnWB2QUoyaH4yc2nSRTFJu2Bgg/SaBTkfA +Ji3pN+k4/uTimsdzPBiad3kwgjg6DOLvCBiQIYxcZWB0ndmakESFNLl0k948hk8SaboZKmu6gQlL +yD+gaKYdGgbtgPv5mAMD+pCdl0HBKIxMN0lTAkVviFVndQcNgYwzSQmHQXqJ0kAq6S2DbpMo3WEw +PixyKciNNIFuRcMOlKgL4D6B7a+YOhUhpSGwBGT6oWCQTqd4gYGh003waP3UgGwHgjrxnGFdcRKZ +HWRghoIQh0ITKHRkGFbIl2BuWeIIC74IKQWIFjfoYxOVJwY1al8VW/zkXiU467T1CWRPQtBBO0YM +0hDY31IoG5ZthbZy6dCfZOpY3BNzwG2TO20IhhKqJum9o1lGQGgm68vRMByjjoERqfHi0ZIQlFll +zVOUSRWM+wesIGFLECZiejKgJEWpKRoCq2YVB/+Y34I35I9YoHFRq0JTGcyH8U6iKLEWC+b/hbTo +kJaikpYqE8YAFgf/CctVR+iwoQ5FJO0lRRNoiW9LXrcNYCBicZAwwiSk2FrSZzuCbkX5k2DbgW8t +YkuLJCEGQtJILYpap2E0hdHqNIxmz8vrSRHw3pubyOWE1EDfapogwkyRLiNtfwoCmMWZgetBMK2g +GRA+hihGQABhMHNmYCQrAcpvgcACkuI5AIYWABReFJFKezEZCCpMIRhkPzBeRSGDXcjuRmMzQEFC +Oxi7pK2o2n6Ft3ukaBKzC0kngii6QY2FaQWpBBxWIikN6Bdmb62FrmqGjJYYYVqk4+kyg0VYDeAM +jaE6VSJ3VpGfTNuJgC0rlElwhmLIUZwZxd9uso+iC2y04tt+rjP7QtZzg951UjSSz1WD3s90wqCe +n6cxygTRihEGgymS9CdFGjc0BD2HUoKFoedfC8DL/z3BMFZcsYExxMxqGAwmYcL2MnXKamEgNNa6 +ZWAknVxULIpk0p6ICUNbCyyMzNhiDIxAW1qKIlApPyZl9tPiiIHA4ogMWnBaMWmBRZM7EslkOEhj +NiCB7sn9E43aG8GCRqFUOb0ZisKMArXDQkFQgQEGAs1ZUCNakQ2FCm+qtPNGQSB1h/AG9ir8EW2j +goKB+aBNV1FDIIYVSKVhTDp/VBGECA6I5CM3uIeoirC3NY7JpWNrmn5Tc99wBXLEG4O2zVGKBTES +U+LMihDWNMSrJURV3m/pNCuDTn9gILgimoJhcVLlSDFOuEyRNCphg/aSEU361alm878vYAqeEGkg +6oyXhYxIwkcXFMaglXH6gGuK2mksvh0jGsKmWn+SBANjUz8RlqBhkNwgd0MNgTBokUlBYkU2yTaw +1KDmrAi0maFSDoAikVjBQaFwU4WGQLRmcJ5b+gmbNtRzSS7YWxe0ppHpSgCJ0SIqCnniNCSaB1G7 +5F6ZQQZQbaOOMKtFjdODRLYTB4aj7xgYWrfyYAwridBQRFkwsD+BU/QFmYqXq6JO+iqiRkKULKWh +4q1jisGpzV4Oy0UxbikGc7sBK0XBBRZ0+ObbekOHmMi9MvC3qRAPvY2KvBNCXOoGE6hAxEnA2DsW +PlWiShTz83BCLpXICl075zcQBhsehPFiCPSMNUoR2TFsQvyiGCueKYULTaF+q9PsrNEBO5VdWf56 +1ULevNvviIAVxTYck5XLWoLJsB9jJpqaHdaivAtdp34rkN4364fSEHgVJd4vFWoPi/Y3MSVao6Jj +KIiKVU6QDZ5zcnojMVmKge1/Maz2HxdOQxUYZG6RTjLdNwvDs9hYGI1ibw0nQKs2GXqWA9oRUCnG +pHNKYBJU60wrosV+ErVFgWwZ0i5h7R8ahsEBYj0kbF0FpcaC4WUsqSa2yw0VZ0/zM+8AC2g8hqAA +VajcxDs0XdBpBjgOAs7mZsx7TTaojuhUOk3VqTbYJDwNxTZ91hfbio4Sm/wQIr3wNAQjeWEJaBge +eiNJNbCYPHaA418Pbthr4jPo2eAGWpPwXA3NtCpIXEIUKD3JQPDWhIbhmZwayHV5RBi++KBhaJNZ +U6SC6TP7kXNGuiC2IHEpnoHAgkQl5822Yu1yulyBBBkVuDBxzN1hT54PSIFwjXVVIDgrDgwv/we5 +YpEwpkjC0BtwWKJQ5bkSHZlFEiU82yySet2QAgPJcX+RwiBcV8YWB2cW+qDtYhTKI6jfTkr2i3wE +IxDSUWAyL5AY9i0iSuCgN9EZ5PNg6EXkwcjm6DASc94GA8P3EikYVYwmTh4MReMcELQbNSIIT36Q +IDiywIDgWJxFu2gbRqMTOFDKkE4ybYT0oCGQ9JBpqcm2ohI6lQ9BS0MCAkleDgxl1nAgaLOKhaDF +FA3DxT4Nw1tEWgvwYGhtwoHRBXV0GM6YaRhGu/FgTEpuAoREWlAiI39MlQyc0ZYa1qICli7U6iD/ +NEz+RUvIUgwp+i96Ov+Rp3IYVN4MMiEJRfiNYOj4O7WlDsRjaGT9DCrroxWqIZI5KDwYUGtUHIzt +S9eN8DEjGJEkJy4MOgkGF+axo6CSfzkwhkyFiegTKxBMjHZMhdxu5cKo1jkQ7Bwi1y/QBdD+a1wA +UyHjntAAk8NmqhRtaRKzGqbAfw7zI/1tg4Nnjdw9Ah5k2xFx8gvnja5rkT3oOpUIwBmpLimMemJg +ZDLWz2AL07dCGpIcGIa+FYWJSIPmJVNcDDZdl7d6FIxtApsqWdWl0TvQ35zWaBjeqHgwBlXvx4Wh +ivZNgXEkEJbI/S+VscV0U2Haoe1U3SQTezWTTY7WrTG7CapcGFkmkli5MJIc3Y5EJu+iU31EGkak +knc1mSn2RTACsUcskjvANlWrUTA6OWYujKFE90XhEHEQXcCrGxoBo9rxOILyBZWs+zNY/8QQcN6k +kwyMcpvp4SCRAaQhiaYOKl/jw2iCP6eYD4KpMLwZHWtSl8DRiIndesw5AtGOM3N3Nx7DSGRfmnU0 +oltIbcMo/r5UkWnHFGSyHR6MKpJj5sFYSdkRMBLZjr1tQvaFqdkWGtY5VixMhISKB/Ov57D+R5qi +pq4ypgwdeKdhkLFEiVtgfokStywMEB4lbikYSZQstUgwtSTK5MkXmq7QvqoEjRI1ABqVTQ8mELmt +hJxFQv6yEJJMb9gyMCp9QFCNA6NTW5l4sH6rQ1WpnHOYMpWVpDCnZUmiaBDoRPvuhNUuwbKTaaGS +SlpAkqCZZBs0BPQjyDp1eptK21EMDC9wHklqTsU0HhXpOJvkgn8DDCq09OOQhoHZi0TAxqB2BBgA ++0wZn2tLg2gic4wlAyKLNI7BHyEAqJxMZroMhL0IZKRQY0dCwTAoqcVA7f81GZqsSDKY7W4JnRUn +hsEAr5JVIzB8WmjREDy5QcFw5QYNY+lhHz+LkkLys0xLFoafaQhMSqSFzcDUWBheeMs0yNT1ODAM +fnmqg4Wp2nJBpo7S0hUyoPXNI3AKBrOsRDEKBSMJ1mG5nuygpsYAcGUHAcKXHSQIXZWGhQvB9opE +CzFaeNAQJRdvwa3UWBgO3qo8Na/TPMWuOQ2DdYEwOgxLFwwMlwZpGE44l6FlFoblCR6MQh8REAOG +HTMD42QgEzxK8zoPxsrIdcwiBgRZXIyJQEHI5PHAqu18+/JxQDqTVc+qVb3sZd4ABFvmR9R5sBC2 +iHQrd7D5RR8GZCWagjkNykKxzS8KRjGYnqgQg0adnQBzpsKDusmORaJOvzRF2uQUJCbcQUmRKHYq +xWA5J4sPEw2ZbM3sZnJsDSZWDz2apFQSaZmDxCGxtUlt7AOETG1JUJmeaGIGladCl51jCNIip/Iz +WQhayNr90FnqzJQZGJnS8giCzMqmWYmes2oYrC1NYQ7FJFE/7klVGPkkhEpa/qyGoyFKLin4khAo +mBoLwyWFKJL6f0lsri8tSRJVssL60gwMsuOp8KYkahTHx4Hh+NsyzgUNecNpV6JDtDFgOI5cNC7+ ++7dIJCA+rlOBnytMKrmESrrJEEQ0DM84kCRSw8WBQX2JDA1R55DEgbHn6wqsGokH33PbaJdVgWcm +Wr9idRnbIw9G1UeH4bgeDAxnRRgYWWRh6DFzYCSRchNjwHBXlobhmJgMhfBgaErjwdhH7I8Ewxsz +BYPmTlMapgfec0Q/Qc85FIuf86iUR4fBz//X1FrsFAZQ/LIsKWh7BHCIb8mCz2ANgMEDDhpYpSAS +DUOX0U0JGtCtigQX6DkDwSHhJYjWrU4KWCiaogsgEyUF3+pEXPSEf0X8j6Pr7IOqZLCaddVgV/2b +hTHs4iFRxy4JonhZMMgLXngwIjwWdHqhZXRxA/FTidpPVUTivGoRGqdsNlQ6hUwpUzZMZNCwEK8J +dM2Eyr5ARQRk4xJV64HqyuCHgkZHYFBAWCJHbhdFyLAMoJncc3J9aKG618jzumTeyDXTiIDRyVO/ +FTwJwh7Vbf3vQQgqPR/dPg/JPYxbNygYMB99/A8CUlepVCgWQmTOgWFgVLsM3Xfok0acuICj1cy5 +nwyMbu0r+mMdLIxoksuLUua9QjWw98E0IP0Xxxnx8EJDwBoJVuqN1TM6sp2GUTVmJDIFYe19+EYi +MDXiDAyqvaP2TlGpIQGjmIy4Z2AQ7uQoGJE6BkCxfWcN5mKgO/XQrInRKHaNEYEX6sAs2bZsdVMG +81KQWRhZZUYiUxAyeeUIOskbvXfK+0oJGkLRGcRJ9mHTpqIJaLh8HRglMf/76wY44tTK8BZF1QS3 +DJSCgtIwmOeoUov8oUTl86gSURUHa21S/Ag0rikkpxg03SGOJKWZTKWgc+QqBfGa0CWTfayr/pN1 +ESUZlD5gIXgijoJRFZl23DVFoWAkg96QY2B4Io6BofNGkfNHIEOXqVMhWRFHQ7wmULyFZGUaxqlN +91oRqfMpVUX0FWXaYoeYDxJxFAxTzQeiSVZJGHRABIU7BoYn4hgY+qYDxa439oQTbe+zIo6GQCJO +oEQcDSOr1ma41wpdWi/L5PH5zHSQjCNBFIPBHJJxKFAMYxGgeetkQeqkHgQjk3IwGg== + ]]> + <![CDATA[ + xrCFJ2EVkrE2HohIRqUZcYAMRMbMooJXyEAkLgal5VKkQHMqSVhARaUlH7AKonqwzQ1TUWUGRNb8 +FYgyspepIwVkCdAlKjThy6JKTVQW6E1dGR0wBy8YP8fBtXtbEQfGWTOfJc/zt8eAoW9p4MA4NBQK +w4tAabFgMC5N5CzpAZEsyongwthXoaiSio7csmDIBDdZlklaQwckUKlgsn3Ul3f/lH0MhS/Ni4UB +XpUiYJAnQG0NyfbJGC7F6vSZ3Oj8CoKmdYm6dUohC8FlFgLcJ0kgVS7biqRQl6mKGi1LaQhbhLlu +SinBwPAkOwNjMOcTK7ItawwUIFZ0nLJHrTfKxiGdDEGi21FVidJGgkwn1bMwrMZiYNBaUjyDTtUk +tbBAHd2BjqcgNawkRGlyGgKcFcawZ1qRZdrGobK5WAhWkzMwPAuHgRGZtUQXfhH2iyoya6mrIgUj +M2upEwc/Y8eTWUsWhrXcGBjGAuTBqJSTq6sKtQr2ad5uK7pCtYGSYmmnXVLJVlDkBSPCCUIgV4ig +GUdhhRjLNASiGXQQBvNCBacP6TeEaZBegg5yFi8zIU8MkTnt0vHnvblJ1OkpiIlII1+iYzeCQoU9 +ZIWcO8gT8H1lUqNbLolrG+gaFQCi9HmkTVCKNhv+je21/8Q8VUUWsDKkqqSwfGZPIkePOYd9q/hM +FqTWSFJECaf4cgQywiZQwoOBQMRtcH5oHwzqaRmBukSbhZAUms4ZGKRjaJmjqhQMmwLCwKC+otqR +ZeoUThQ3IYhdom+QQ7pBZR+jCJfCPkZloESAUzKp01zQGoJ2p49mRY9RtgkV9kP0gdKEGXePQzb/ +/XEWFCWQyHVnWIOBYWisxsLI9EW4uiiTElmiC+XsdacfW5FNiVxiCkbVVJoIqNpE5NIDawoiowXw +C7Daec67ik6kY+06XCemGYyBhZtS2Kuf7M5NmY4fOy+s6bhn7CJbzaT4yLAcAzcPSiUOzUBGtoFV +kShKMkq3MXhih4J4xbZPOAyK7ZCrbylzkNu6aICi5EBYtw94bZQSLIzJ3MQZTYvOTjCybEhI+6J1 +30VYwC6k7kZxP+peB83OjfNgBIOWaM4JhD7pydReMzCc2bEwElOWxMKI1H2yTk64bzUoO1RjvEyB +Om4eVoO8fw1W3dTp8KYGD6kLCVmCoiFeE6pBoZ2BUTWDNCll+3gL7zwddMK0RWfU1oP1wpBpxNkv +TI3xr2STHDK6h5nhZ4oHFN2k21GsGjc7BKAjm40aNA8GzTQChidfGBhJp6lSka3b1nwwJit1ZHS+ +GB02s3AlKioDb71gJaH1At2oRGOXqItF+XwGlYzOkAy6AYUn9enHIJp0KszBwOiCTqsUisV1ySQH +KNNxfLTniE9qsh1PAW3kUHtVSJQAmSpU0S/oaV+Wng4QGlPygl0uAsagcqiQLBP9EIoQCcHaeAwM +R3ayMLTUrMWQrP+cH1HbTqS39tSnSvutVv+70cvnE+n0Sf2jcd6rN1uNXuKjX/+jkay3251BfdDo +wpvkR6/RH3R6jWT/s/MnegI/ccDT6cpxNfH/A9tvoQk= + ]]> +</i:pgf> +</svg> diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/envelope_toolbar.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/envelope_toolbar.pfi Binary files differnew file mode 100644 index 00000000..55c69246 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/envelope_toolbar.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/icons.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/icons.pfi Binary files differnew file mode 100644 index 00000000..f6c6e769 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/icons.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/main_toolbar.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/main_toolbar.pfi Binary files differnew file mode 100644 index 00000000..e627e865 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/main_toolbar.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/note.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/note.pfi Binary files differnew file mode 100644 index 00000000..2a9c94b8 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/note.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/pattern_toolbar.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/pattern_toolbar.pfi Binary files differnew file mode 100644 index 00000000..939a1f22 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/pattern_toolbar.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/readme.txt b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/readme.txt new file mode 100644 index 00000000..72262daa --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/readme.txt @@ -0,0 +1,5 @@ +These PFI files were made with PhotoFiltre (http://www.photofiltre.com/).
+They contain all the icons as separate layers, some icons are also divided into several layers.
+Export them to PNG to use them in OpenMPT.
+
+The SVG files (icons, about and splash screen) were created by Úlfur Kolka.
diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/sample_toolbar.pfi b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/sample_toolbar.pfi Binary files differnew file mode 100644 index 00000000..4a7af21a --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/originals/sample_toolbar.pfi diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/pattern_toolbar.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/pattern_toolbar.png Binary files differnew file mode 100644 index 00000000..d938e4b5 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/pattern_toolbar.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/sample_toolbar.png b/Src/external_dependencies/openmpt-trunk/mptrack/res/sample_toolbar.png Binary files differnew file mode 100644 index 00000000..47203eb6 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/sample_toolbar.png diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/view_pat.bmp b/Src/external_dependencies/openmpt-trunk/mptrack/res/view_pat.bmp Binary files differnew file mode 100644 index 00000000..ce2d8be4 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/view_pat.bmp diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/res/vumeters.bmp b/Src/external_dependencies/openmpt-trunk/mptrack/res/vumeters.bmp Binary files differnew file mode 100644 index 00000000..3db04e75 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/res/vumeters.bmp diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/resource.h b/Src/external_dependencies/openmpt-trunk/mptrack/resource.h new file mode 100644 index 00000000..d7e4cb6e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/resource.h @@ -0,0 +1,1313 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by mptrack.rc +// +#define IDD_ABOUTBOX 100 +#define IDD_FILE_NEW 101 +#define IDD_OPTIONS_PLAYER 103 +#define IDD_DIRECTORIES 104 +#define IDD_WAVECONVERT 105 +#define IDD_PROGRESS 106 +#define IDD_OPTIONS_MIXER 107 +#define IDD_OPTIONS_KEYBOARD 108 +#define IDD_OPTIONS_COLORS 109 +#define IDD_OPTIONS_MIDI 111 +#define IDD_OPTIONS_PATTERN 112 +#define IDD_LOADRAWSAMPLE 113 +#define IDD_CONTROL_GLOBALS 114 +#define IDD_CONTROL_COMMENTS 115 +#define IDD_CONTROL_PATTERNS 116 +#define IDD_CONTROL_SAMPLES 117 +#define IDD_CONTROL_INSTRUMENTS 118 +#define IDD_MODDOC_MODTYPE 119 +#define IDD_REMOVECHANNELS 120 +#define IDD_EDIT_FIND 121 +#define IDD_EDIT_REPLACE 122 +#define IDD_PATTERN_PROPERTIES 123 +#define IDD_PATTERN_EDITCOMMAND 124 +#define IDD_VIEW_GLOBALS 126 +#define IDD_SAMPLE_AMPLIFY 130 +#define IDD_SAMPLE_RESAMPLE 131 +#define IDD_OPTIONS_EFFECTS 133 +#define IDR_MAINFRAME 200 +#define IDR_MODULETYPE 201 +#define IDR_TOOLBARS 202 +#define ID_PATTERN_CHANNELMANAGER 202 +#define IDR_ENVELOPES 203 +#define ID_INDICATOR_CPU 203 +#define ID_FILE_EXPORTCOMPAT 204 +#define ID_ENVELOPE_SETRELEASENODE 205 +#define IDS_UNABLE_TO_LOAD_KEYBINDINGS 212 +#define IDS_CANT_OPEN_FILE_FOR_WRITING 213 +#define IDS_PATTERN_CLEANUP_UNAVAILABLE 215 +#define IDS_ERR_FILEOPEN 234 +#define IDS_ERR_FILETYPE 235 +#define IDS_ERR_SAVEINS 236 +#define IDS_ERR_OUTOFMEMORY 237 +#define IDS_ERR_TOOMANYINS 238 +#define IDS_ERR_SAVESONG 239 +#define IDS_ERR_TOOMANYPAT 240 +#define IDS_ERR_TOOMANYSMP 241 +#define IDS_ERR_SAVESMP 242 +#define IDB_MAINBAR 300 +#define IDB_IMAGELIST 301 +#define IDB_PATTERNS 302 +#define IDB_PATTERNVIEW 303 +#define IDB_MPTRACK 304 +#define IDB_COLORSETUP 305 +#define IDB_VUMETERS 306 +#define IDB_SPLASHSCREEN 307 +#define IDB_ENVTOOLBAR 308 +#define IDB_SMPTOOLBAR 309 +#define IDC_DRAGGING 350 +#define IDC_NODROP 351 +#define IDC_NODRAG 352 +#define ID_ENVELOPE_VIEWGRID 353 +#define ID_REPORT_BUG 354 +#define IDD_INFO_BOX 401 +#define IDD_OPTIONS_GENERAL 402 +#define IDD_OPTIONS_SOUNDCARD 403 +#define IDD_MIDIMACRO 404 +#define IDD_WAVECOMPRESSION 405 +#define IDD_CHORDEDIT 406 +#define IDD_SPLASHSCREEN 408 +#define IDD_TREEVIEW 409 +#define IDD_MOD2MIDI 420 +#define IDD_SAVEPRESET 421 +#define IDD_EDITSAMPLEMAP 422 +#define IDD_SELECTMIXPLUGIN 423 +#define IDD_PLUGINEDITOR 424 +#define IDD_EFFECTVISUALIZER 426 +#define IDD_LFOPLUGIN 427 +#define IDB_SPLASHNOFOLDFIN 435 +#define IDR_VSTMENU 436 +#define IDD_DEFAULTPLUGINEDITOR 438 +#define IDD_CHANNELMANAGER 440 +#define IDD_MISSINGPLUGS 441 +#define IDD_PITCHSHIFT 442 +#define IDD_OPTIONS_AUTOSAVE 443 +#define IDD_EDIT_GOTO 444 +#define IDD_MOVEFXSLOT 501 +#define IDD_LEGACY_PLAYBACK 502 +#define IDD_FIND_RANGE 503 +#define IDD_MIDI_IO_PLUGIN 504 +#define IDD_CONTROL_GRAPH 507 +#define IDD_SCALE_ENV_POINTS 510 +#define IDD_TUNING 511 +#define IDD_UPDATE 512 +#define IDD_MIXSAMPLES 513 +#define IDS_ERR_TUNING_SERIALISATION 514 +#define IDD_MIDIPARAMCONTROL 515 +#define IDD_MSGBOX_HIDABLE 516 +#define IDD_ADDSILENCE 517 +#define IDD_OPLEXPORT 518 +#define IDR_DEFAULT_KEYBINDINGS 519 +#define IDD_OPL_PARAMS 520 +#define IDD_CLEANUP_SONG 521 +#define IDD_CHANNELSETTINGS 522 +#define IDD_KEYBOARD_SPLIT 523 +#define IDD_SAMPLE_GENERATOR 524 +#define IDD_SAMPLE_GENERATOR_PRESETS 525 +#define IDD_EDITHISTORY 526 +#define IDD_SAMPLE_GRID_SIZE 527 +#define IDD_SAMPLE_XFADE 528 +#define IDD_OPTIONS_UPDATE 529 +#define IDD_CLOSEDOCUMENTS 530 +#define IDD_AUTOTUNE 531 +#define IDD_INPUT 532 +#define IDD_CLIPBOARD 533 +#define IDD_OPTIONS_ADVANCED 534 +#define IDD_OPTIONS_SAMPLEEDITOR 535 +#define IDD_SCANPLUGINS 536 +#define IDD_RESAMPLE 537 +#define IDD_MISSINGSAMPLES 538 +#define IDD_WECLOME 539 +#define IDD_TEMPO_SWING 540 +#define IDD_OPTIONS_WINE 541 +#define IDD_MODIFIEDSAMPLES 542 +#define IDC_BUTTON1 1001 +#define IDC_BUTTON2 1002 +#define IDC_BUTTON3 1003 +#define IDC_BUTTON4 1004 +#define IDC_BUTTON5 1005 +#define IDC_BUTTON6 1006 +#define IDC_BUTTON7 1007 +#define IDC_BUTTON8 1008 +#define IDC_BUTTON9 1009 +#define IDC_BUTTON10 1010 +#define IDC_BUTTON11 1011 +#define IDC_BUTTON12 1012 +#define IDC_BUTTON13 1013 +#define IDC_BUTTON14 1014 +#define IDC_BUTTON15 1015 +#define IDC_BUTTON16 1016 +#define IDC_BUTTON17 1017 +#define IDC_BUTTON18 1018 +#define IDC_BUTTON19 1019 +#define IDC_BUTTON20 1020 +#define IDC_COMBO_ENVELOPE 1037 +#define IDC_LIST_DETAILS 1040 +#define IDC_LIST_SAMPLES 1041 +#define IDC_LIST_INSTRUMENTS 1042 +#define IDC_LIST_PATTERNS 1043 +#define IDC_SPIN21 1068 +#define IDC_SPIN22 1069 +#define IDC_EDIT1 1101 +#define IDC_EDIT2 1102 +#define IDC_EDIT3 1103 +#define IDC_EDIT4 1104 +#define IDC_EDIT5 1105 +#define IDC_EDIT6 1106 +#define IDC_EDIT7 1107 +#define IDC_EDIT8 1108 +#define IDC_EDIT9 1109 +#define IDC_EDIT10 1110 +#define IDC_EDIT11 1111 +#define IDC_EDIT12 1112 +#define IDC_EDIT13 1113 +#define IDC_EDIT14 1114 +#define IDC_EDIT15 1115 +#define IDC_EDIT16 1116 +#define IDC_EDIT17 1117 +#define IDC_EDIT18 1118 +#define IDC_EDIT19 1119 +#define IDC_EDIT20 1120 +#define IDC_EDIT21 1121 +#define IDC_EDIT22 1122 +#define IDC_EDIT23 1123 +#define IDC_EDIT24 1124 +#define IDC_EDIT25 1125 +#define IDC_EDIT26 1126 +#define IDC_EDIT27 1127 +#define IDC_EDIT28 1128 +#define IDC_EDIT29 1129 +#define IDC_EDIT_RATIOPERIOD 1130 +#define IDC_COMBO1 1201 +#define IDC_COMBO2 1202 +#define IDC_COMBO3 1203 +#define IDC_COMBO4 1204 +#define IDC_COMBO5 1205 +#define IDC_COMBO6 1206 +#define IDC_COMBO9 1207 +#define IDC_COMBO10 1208 +#define IDC_COMBO11 1209 +#define IDC_NOTEMAP 1213 +#define IDC_TEXT1 1301 +#define IDC_TEXT2 1302 +#define IDC_TEXT3 1303 +#define IDC_TEXT4 1304 +#define IDC_TEXT5 1305 +#define IDC_TEXT6 1306 +#define IDC_TEXT7 1307 +#define IDC_TEXT8 1308 +#define IDC_TEXT9 1309 +#define IDC_TEXT10 1310 +#define IDC_FILTERTEXT 1310 +#define IDC_TEXT11 1311 +#define IDC_TEXT12 1312 +#define IDC_TEXT13 1313 +#define IDC_TEXT14 1314 +#define IDC_TEXT15 1315 +#define IDC_TEXT16 1316 +#define IDC_TEXT17 1317 +#define IDC_TEXT18 1318 +#define IDC_TEXT19 1319 +#define IDC_TEXT20 1320 +#define IDC_TEXT21 1321 +#define IDC_TEXT22 1322 +#define IDC_TEXT23 1323 +#define IDC_TEXT24 1324 +#define IDC_TEXT25 1325 +#define IDC_TEXT26 1326 +#define IDC_TEXT27 1327 +#define IDC_TEXT28 1328 +#define IDC_TEXT29 1329 +#define IDC_TEXT30 1330 +#define IDC_TEXT31 1331 +#define IDC_TEXT32 1332 +#define IDC_TEXT33 1333 +#define IDC_TEXT34 1334 +#define IDC_TEXT35 1335 +#define IDC_TEXT36 1336 +#define IDC_TEXT37 1337 +#define IDC_TEXT38 1338 +#define IDC_TEXT39 1339 +#define IDC_TEXT40 1340 +#define IDC_TEXT41 1341 +#define IDC_TEXT42 1342 +#define IDC_TEXT43 1343 +#define IDC_TEXT44 1344 +#define IDC_TEXT45 1345 +#define IDC_TEXT46 1346 +#define IDC_TEXT47 1347 +#define IDC_TEXT48 1348 +#define IDC_TEXT49 1349 +#define IDC_TEXT50 1350 +#define IDC_TEXT51 1351 +#define IDC_TEXT52 1352 +#define IDC_TEXT53 1353 +#define IDC_TEXT54 1354 +#define IDC_TEXT55 1355 +#define IDC_TEXT56 1356 +#define IDC_TEXT57 1357 +#define IDC_TEXT58 1358 +#define IDC_TEXT59 1359 +#define IDC_TEXT60 1360 +#define IDC_TEXT61 1361 +#define IDC_TEXT62 1362 +#define IDC_TEXT63 1363 +#define IDC_TEXT64 1364 +#define IDC_TEXT65 1365 +#define IDC_TEXT66 1366 +#define IDC_TEXT67 1367 +#define IDC_TEXT68 1368 +#define IDC_TEXT69 1369 +#define IDC_RADIO1 1401 +#define IDC_RADIO2 1402 +#define IDC_RADIO3 1403 +#define IDC_RADIO4 1404 +#define IDC_RADIO5 1405 +#define IDC_RADIO6 1406 +#define IDC_RADIO7 1407 +#define IDC_RADIO8 1408 +#define IDC_RADIO9 1409 +#define IDC_RADIO10 1410 +#define IDC_RADIO11 1411 +#define IDC_RADIO12 1412 +#define IDC_SLIDER1 1501 +#define IDC_SLIDER2 1502 +#define IDC_SLIDER3 1503 +#define IDC_SLIDER4 1504 +#define IDC_SLIDER5 1505 +#define IDC_SLIDER6 1506 +#define IDC_SLIDER7 1507 +#define IDC_SLIDER8 1508 +#define IDC_SLIDER9 1509 +#define IDC_SLIDER10 1510 +#define IDC_SLIDER11 1511 +#define IDC_SLIDER12 1512 +#define IDC_SLIDER13 1513 +#define IDC_SLIDER14 1514 +#define IDC_SLIDER15 1515 +#define IDC_SLIDER16 1516 +#define IDC_SLIDER17 1517 +#define IDC_SLIDER18 1518 +#define IDC_SLIDER19 1519 +#define IDC_SLIDER20 1520 +#define IDC_SLIDER21 1521 +#define IDC_SLIDER22 1522 +#define IDC_SLIDER23 1523 +#define IDC_SLIDER24 1524 +#define IDC_SLIDER25 1525 +#define IDC_SLIDER26 1526 +#define IDC_SLIDER27 1527 +#define IDC_SLIDER28 1528 +#define IDC_SLIDER29 1529 +#define IDC_SLIDER30 1530 +#define IDC_SLIDER31 1531 +#define IDC_SLIDER32 1532 +#define IDC_SLIDER33 1533 +#define IDC_SLIDER34 1534 +#define IDC_SLIDER35 1535 +#define IDC_SLIDER36 1536 +#define IDC_SLIDER37 1537 +#define IDC_SLIDER38 1538 +#define IDC_SLIDER39 1539 +#define IDC_SLIDER40 1540 +#define IDC_SLIDER41 1541 +#define IDC_SLIDER42 1542 +#define IDC_SLIDER43 1543 +#define IDC_SLIDER44 1544 +#define IDC_SLIDER45 1545 +#define IDC_SLIDER46 1546 +#define IDC_SLIDER47 1547 +#define IDC_SLIDER48 1548 +#define IDC_SLIDER49 1549 +#define IDC_SLIDER50 1550 +#define IDC_SLIDER51 1551 +#define IDC_SLIDER52 1552 +#define IDC_SLIDER53 1553 +#define IDC_SLIDER54 1554 +#define IDC_SLIDER55 1555 +#define IDC_SLIDER56 1556 +#define IDC_SLIDER57 1557 +#define IDC_SLIDER58 1558 +#define IDC_SLIDER59 1559 +#define IDC_SLIDER60 1560 +#define IDC_SLIDER61 1561 +#define IDC_SLIDER62 1562 +#define IDC_SLIDER63 1563 +#define IDC_SLIDER64 1564 +#define IDC_SLIDER65 1565 +#define IDC_SLIDER66 1566 +#define IDC_SLIDER67 1567 +#define IDC_SLIDER68 1568 +#define IDC_SLIDER69 1569 +#define IDC_SLIDER70 1570 +#define IDC_SLIDER71 1571 +#define IDC_SLIDER72 1572 +#define IDC_SLIDER73 1573 +#define IDC_SLIDER74 1574 +#define IDC_SLIDER75 1575 +#define IDC_SLIDER76 1576 +#define IDC_SLIDER77 1577 +#define IDC_SLIDER78 1578 +#define IDC_SLIDER79 1579 +#define IDC_SLIDER80 1580 +#define IDC_SLIDER81 1581 +#define IDC_SLIDER82 1582 +#define IDC_SLIDER83 1583 +#define IDC_SLIDER84 1584 +#define IDC_SLIDER85 1585 +#define IDC_SLIDER86 1586 +#define IDC_SLIDER87 1587 +#define IDC_SLIDER88 1588 +#define IDC_SLIDER89 1589 +#define IDC_SLIDER90 1590 +#define IDC_SLIDER91 1591 +#define IDC_SLIDER92 1592 +#define IDC_SLIDER93 1593 +#define IDC_SLIDER94 1594 +#define IDC_SLIDER95 1595 +#define IDC_SLIDER96 1596 +#define IDC_SLIDER97 1597 +#define IDC_SLIDER98 1598 +#define IDC_SLIDER99 1599 +#define IDC_SLIDER100 1600 +#define IDC_SLIDER101 1601 +#define IDC_SLIDER102 1602 +#define IDC_SLIDER103 1603 +#define IDC_SLIDER104 1604 +#define IDC_SLIDER105 1605 +#define IDC_SLIDER106 1606 +#define IDC_SLIDER107 1607 +#define IDC_SLIDER108 1608 +#define IDC_SLIDER109 1609 +#define IDC_SLIDER110 1610 +#define IDC_SLIDER111 1611 +#define IDC_SLIDER112 1612 +#define IDC_SLIDER113 1613 +#define IDC_SLIDER114 1614 +#define IDC_SLIDER115 1615 +#define IDC_SLIDER116 1616 +#define IDC_SLIDER117 1617 +#define IDC_SLIDER118 1618 +#define IDC_SLIDER119 1619 +#define IDC_SLIDER120 1620 +#define IDC_SLIDER121 1621 +#define IDC_SLIDER122 1622 +#define IDC_SLIDER123 1623 +#define IDC_SLIDER124 1624 +#define IDC_SLIDER125 1625 +#define IDC_SLIDER126 1626 +#define IDC_SLIDER127 1627 +#define IDC_SLIDER128 1628 +#define IDC_SLIDER129 1629 +#define IDC_BITMAP1 1700 +#define IDC_CHECK1 1701 +#define IDC_CHECK2 1702 +#define IDC_CHECK3 1703 +#define IDC_CHECK4 1704 +#define IDC_CHECK5 1705 +#define IDC_CHECK6 1706 +#define IDC_CHECK7 1707 +#define IDC_CHECK8 1708 +#define IDC_CHECK9 1709 +#define IDC_CHECK10 1710 +#define IDC_CHECK11 1711 +#define IDC_CHECK12 1712 +#define IDC_CHECK13 1713 +#define IDC_CHECK14 1714 +#define IDC_CHECK15 1715 +#define IDC_CHECK16 1716 +#define IDC_CHECK17 1717 +#define IDC_CHECK18 1718 +#define IDC_CHECK19 1719 +#define IDC_CHECK20 1720 +#define IDC_CHECK21 1721 +#define IDC_CHECK22 1722 +#define IDC_CHECK23 1723 +#define IDC_CHECK24 1724 +#define IDC_CHECK25 1725 +#define IDC_CHECK26 1726 +#define IDC_CHECK27 1727 +#define IDC_CHECK28 1728 +#define IDC_CHECK29 1729 +#define IDC_CHECK30 1730 +#define IDC_CHECK31 1731 +#define IDC_CHECK32 1732 +#define IDC_CHECK33 1733 +#define IDC_CHECK34 1734 +#define IDC_CHECK35 1735 +#define IDC_CHECK36 1736 +#define IDC_CHECK37 1737 +#define IDC_CHECK38 1738 +#define IDC_CHECK39 1739 +#define IDC_CHECK40 1740 +#define IDC_CHECK41 1741 +#define IDC_CHECK42 1742 +#define IDC_CHECK43 1743 +#define IDC_CHECK44 1744 +#define IDC_CHECK45 1745 +#define IDC_CHECK46 1746 +#define IDC_CHECK47 1747 +#define IDC_CHECK48 1748 +#define IDC_CHECK49 1749 +#define IDC_MIDI_TO_PLUGIN 1750 +#define IDC_CHECK51 1751 +#define IDC_CHECK52 1752 +#define IDC_CHECK53 1753 +#define IDC_CHECK54 1754 +#define IDC_CHECK55 1755 +#define IDC_CHECK56 1756 +#define IDC_CHECK57 1757 +#define IDC_CHECK58 1758 +#define IDC_CHECK59 1759 +#define IDC_CHECK60 1760 +#define IDC_CHECK61 1761 +#define IDC_CHECK62 1762 +#define IDC_CHECK63 1763 +#define IDC_CHECK64 1764 +#define IDC_CHECK65 1765 +#define IDC_CHECK66 1766 +#define IDC_CHECK67 1767 +#define IDC_CHECK68 1768 +#define IDC_CHECK69 1769 +#define IDC_IT_COMPATIBLEPLAY 1770 +#define IDC_CHECK71 1771 +#define IDC_CHECK72 1772 +#define IDC_CHECK73 1773 +#define IDC_CHECK74 1774 +#define IDC_CHECK75 1775 +#define IDC_CHECK76 1776 +#define IDC_CHECK77 1777 +#define IDC_CHECK78 1778 +#define IDC_CHECK79 1779 +#define IDC_CHECK80 1780 +#define IDC_CHECK81 1781 +#define IDC_CHECK82 1782 +#define IDC_CHECK83 1783 +#define IDC_CHECK84 1784 +#define IDC_CHECK85 1785 +#define IDC_CHECK86 1786 +#define IDC_CHECK87 1787 +#define IDC_CHECK88 1788 +#define IDC_CHECK89 1789 +#define IDC_CHECK90 1790 +#define IDC_CHECK91 1791 +#define IDC_CHECK92 1792 +#define IDC_CHECK93 1793 +#define IDC_CHECK94 1794 +#define IDC_CHECK95 1795 +#define IDC_CHECK96 1796 +#define IDC_CHECK97 1797 +#define IDC_CHECK98 1798 +#define IDC_CHECK99 1799 +#define IDC_CHECK100 1800 +#define IDC_CHECK101 1801 +#define IDC_CHECK102 1802 +#define IDC_CHECK103 1803 +#define IDC_CHECK104 1804 +#define IDC_CHECK105 1805 +#define IDC_CHECK106 1806 +#define IDC_CHECK107 1807 +#define IDC_CHECK108 1808 +#define IDC_CHECK109 1809 +#define IDC_CHECK110 1810 +#define IDC_CHECK111 1811 +#define IDC_CHECK112 1812 +#define IDC_CHECK113 1813 +#define IDC_CHECK114 1814 +#define IDC_CHECK115 1815 +#define IDC_CHECK116 1816 +#define IDC_CHECK117 1817 +#define IDC_CHECK118 1818 +#define IDC_CHECK119 1819 +#define IDC_CHECK120 1820 +#define IDC_CHECK121 1821 +#define IDC_CHECK122 1822 +#define IDC_CHECK123 1823 +#define IDC_CHECK124 1824 +#define IDC_CHECK125 1825 +#define IDC_CHECK126 1826 +#define IDC_CHECK127 1827 +#define IDC_CHECK128 1828 +#define IDC_CHECK129 1829 +#define IDC_SPIN1 1851 +#define IDC_SPIN2 1852 +#define IDC_SPIN3 1853 +#define IDC_SPIN4 1854 +#define IDC_SPIN5 1855 +#define IDC_SPIN6 1856 +#define IDC_SPIN7 1857 +#define IDC_SPIN8 1858 +#define IDC_SPIN9 1859 +#define IDC_SPIN10 1860 +#define IDC_SPIN11 1861 +#define IDC_SPIN12 1862 +#define IDC_SPIN13 1863 +#define IDC_SPIN14 1864 +#define IDC_SPIN15 1865 +#define IDC_SPIN16 1866 +#define IDC_SPIN17 1867 +#define IDC_SPIN18 1868 +#define IDC_SPIN19 1869 +#define IDC_SPIN_TEMPO 1896 +#define IDC_SPIN_SPEED 1897 +#define IDC_SPIN_GLOBALVOL 1898 +#define IDC_SPIN_RESTARTPOS 1899 +#define IDC_SPIN_SAMPLEPA 1900 +#define IDC_PROGRESS1 1901 +#define IDC_PROGRESS2 1902 +#define IDC_SPIN_VSTIVOL 1921 +#define IDC_TABCTRL1 1951 +#define IDC_SCROLLBAR1 1998 +#define IDC_QUESTION1 1999 +#define IDC_COMBO_INSTRUMENT 2002 +#define IDC_EDIT_COMMENTS 2004 +#define IDC_EDIT_SONGTITLE 2005 +#define IDC_SLIDER_PREAMP 2006 +#define IDC_EDIT_ARTIST 2006 +#define IDC_EDIT_TEMPO 2007 +#define IDC_EDIT_SPEED 2008 +#define IDC_EDIT_GLOBALVOL 2009 +#define IDC_EDIT_RESTARTPOS 2010 +#define IDC_SLIDER_SAMPLEPREAMP 2011 +#define IDC_BUTTON_MODTYPE 2012 +#define IDC_EDIT_SAMPLEPA 2013 +#define IDC_EDIT_MODTYPE 2014 +#define IDC_EDIT_CURRENTTEMPO 2015 +#define IDC_EDIT_CURRENTSPEED 2016 +#define IDC_SPIN_CURRENTTEMPO 2017 +#define IDC_SPIN_CURRENTSPEED 2018 +#define IDC_TEXT_CURRENTTEMPO 2019 +#define IDC_TEXT_CURRENTSPEED 2020 +#define IDC_ORDERLIST 2024 +#define IDC_PATTERN_PLAY 2025 +#define IDC_PATTERN_PLAYFROMSTART 2026 +#define IDC_PATTERN_STOP 2027 +#define IDC_PATTERN_RECORD 2028 +#define IDC_SAMPLE_PLAY 2029 +#define IDC_PATTERN_NEW 2030 +#define IDC_PATTERN_OCTAVELINK 2031 +#define IDC_SPIN_SPACING 2032 +#define IDC_EDIT_SPACING 2033 +#define IDC_SAMPLE_NAME 2034 +#define IDC_SAMPLE_FILENAME 2035 +#define IDC_SAMPLE_NEW 2036 +#define IDC_SAMPLE_OPEN 2037 +#define IDC_SAMPLE_SAVEAS 2038 +#define IDC_SPIN_SAMPLE 2039 +#define IDC_EDIT_SAMPLE 2040 +#define IDC_SAMPLE_NORMALIZE 2041 +#define IDC_SAMPLE_AMPLIFY 2042 +#define IDC_SAMPLE_RESAMPLE 2043 +#define IDC_SAMPLE_REVERSE 2044 +#define IDC_COMBO_ZOOM 2045 +#define IDC_COMBO_BASENOTE 2046 +#define IDC_COMBO_RESAMPLING 2047 +#define IDC_CHECK_BASS 2048 +#define IDC_CHECK_REVERB 2049 +#define IDC_CHECK_SURROUND 2050 +#define IDC_CHECK_LOOPSONG 2051 +#define IDC_CHECK_EQ 2052 +#define IDC_CHECK_AGC 2053 +#define IDC_SAMPLE_SILENCE 2054 +#define IDC_SAMPLE_INVERT 2055 +#define IDC_SAMPLE_SIGN_UNSIGN 2056 +#define IDC_EDIT_SEQNUM 2057 +#define IDC_SPIN_SEQNUM 2058 +#define IDC_SAVE_ALL 2059 +#define IDC_INSTRUMENT_NEW 2060 +#define IDC_INSTRUMENT_OPEN 2061 +#define IDC_INSTRUMENT_SAVEAS 2062 +#define IDC_INSTRUMENT_PLAY 2063 +#define IDC_SPIN_INSTRUMENT 2064 +#define IDC_EDIT_INSTRUMENT 2065 +#define IDC_EDIT_PATTERNNAME 2066 +#define IDC_INSTRUMENT_MORE 2067 +#define IDC_EDIT_TRANSPOSE 2068 +#define IDC_SPIN_TRANSPOSE 2069 +#define IDC_EDIT_BASEOCTAVE 2070 +#define IDC_SPIN_BASEOCTAVE 2071 +#define IDC_LIST1 2072 +#define IDC_LIST3 2073 +#define IDC_TOOLBAR1 2074 +#define IDC_LIST4 2074 +#define IDC_TOOLBAR2 2075 +#define IDC_LIST5 2075 +#define IDC_TOOLBAR_DETAILS 2076 +#define IDC_LIST6 2076 +#define IDC_KEYBOARD1 2078 +#define IDC_SPLASH 2079 +#define IDC_SAVE_ONE 2080 +#define IDC_TREEVIEW 2081 +#define IDC_COMMANDKEY 2081 +#define IDC_VUMETER_LEFT 2082 +#define IDC_TREEDATA 2082 +#define IDC_VUMETER_RIGHT 2083 +#define IDC_SAMPLE_DOWNSAMPLE 2084 +#define IDC_COMBO7 2085 +#define IDC_TREE1 2086 +#define IDC_VISSTATUS 2087 +#define IDC_CHOICECOMBO 2090 +#define IDC_CHOICECOMBO2 2092 +#define IDC_KEYCATEGORY 2092 +#define IDC_CHECKKEYDOWN 2094 +#define IDC_CHECKKEYHOLD 2095 +#define IDC_CHECKKEYUP 2096 +#define IDC_CUSTHOTKEY 2098 +#define IDC_SET 2099 +#define IDC_RESTORE 2100 +#define IDC_DELETE 2101 +#define IDC_SAVE 2102 +#define IDC_LOAD 2103 +#define IDC_KEYREPORT 2105 +#define IDC_BROWSEKEYCONF 2107 +#define IDC_CLEARLOG 2107 +#define IDC_NOTESREPEAT 2108 +#define IDC_REMCHANSLIST 2108 +#define IDC_NOTESREPEAT2 2109 +#define IDC_NONOTESREPEAT 2109 +#define IDC_EFFECTLETTERSIT 2110 +#define IDC_INSVIEWPLG 2110 +#define IDC_EFFECTLETTERSXM 2111 +#define IDC_VISFILLBLANKS 2115 +#define IDC_CHECK158 2116 +#define IDC_TEXT70 2116 +#define IDC_VISEFFECTLIST 2116 +#define IDC_VISACTION 2117 +#define IDC_COMBO8 2121 +#define IDC_COMBO_SPLITNOTE 2122 +#define IDC_COMBO_OCTAVEMODIFIER 2123 +#define IDC_COMBO_SPLITINSTRUMENT 2124 +#define IDC_COMBO_SPLITVOLUME 2125 +#define IDC_TAB1 2126 +#define IDC_CUSTOM2 2127 +#define IDC_SLIDER_SONGTEMPO 2128 +#define IDC_COMMAND_LIST 2129 +#define IDC_STATIC8 2200 +#define IDC_PATINSTROPLUGGUI 2201 +#define IDC_RAMPING_IN 2204 +#define IDC_PLAYEROPTIONS 2205 +#define IDC_RAMPING_OUT 2205 +#define IDC_CHORDDETECTWAITTIME 2206 +#define IDC_STATIC1 2208 +#define IDC_TEXT_PREVIEW 2208 +#define IDC_STATIC2 2209 +#define IDC_MACROPLUG 2209 +#define IDC_STATIC3 2210 +#define IDC_MACROPARAM 2210 +#define IDC_SAMPLE_LENGTH_NEW 2211 +#define IDC_SAMPLE_LENGTH_ORIGINAL 2212 +#define IDC_ROW_LENGTH_NEW2 2213 +#define IDC_ROW_LENGTH_ORIGINAL 2214 +#define IDC_PSRATIO 2215 +#define IDC_MS_LENGTH_NEW 2216 +#define IDC_TEMPO 2217 +#define IDC_SPEED 2218 +#define IDC_MS_LENGTH_ORIGINAL2 2219 +#define IDC_RICHEDIT21 2219 +#define IDC_AUTOSAVE_PATH 2220 +#define IDC_AUTOSAVE_BROWSE 2221 +#define IDC_AUTOSAVE_INTERVAL 2222 +#define IDC_AUTOSAVE_ENABLE 2223 +#define IDC_AUTOSAVE_HISTORY 2224 +#define IDC_AUTOSAVE_USEORIGDIR 2225 +#define IDC_PRIMARYHILITE 2226 +#define IDC_SECONDARYHILITE 2227 +#define IDC_GIVEPLUGSIDLETIME 2228 +#define IDC_ROWSPERBEAT 2229 +#define IDC_RENDERSILENCE 2229 +#define IDC_ROWSPERMEASURE 2230 +#define IDC_GOTO_ROW 2231 +#define IDC_GOTO_CHAN 2232 +#define IDC_GOTO_PAT 2233 +#define IDC_GOTO_ORD 2234 +#define IDC_MOVEFXSLOT 2235 +#define IDC_CLONEPLUG 2236 +#define IDC_INSERTFXSLOT 2237 +#define IDC_FILTERMODE 2238 +#define IDC_DELPLUGIN 2239 +#define IDC_AUTOSAVE_USECUSTOMDIR 2245 +#define IDC_BUTTON_MODTYPE2 2246 +#define IDC_SLIDER_SAMPLEPREAMP3 2248 +#define IDC_SLIDER_GLOBALVOL 2249 +#define IDC_EDIT_VSTIVOL 2250 +#define IDC_SLIDER_VSTIVOL 2251 +#define IDC_MACROCC 2252 +#define IDC_GENMACROLABEL 2253 +#define IDC_RENDERZONE 2254 +#define IDC_PATTERN_LOOP 2255 +#define IDC_EDIT_FACTORX 2256 +#define IDC_EDIT_FACTORY 2257 +#define IDC_STATICRATIOMAP 2259 +#define IDC_COMBO_TCOL 2261 +#define IDC_TUNINGBOX 2262 +#define IDC_TUNINGNAME 2263 +#define IDC_COMBO_TTYPE 2264 +#define IDC_EDIT_STEPS 2265 +#define IDC_EDIT_BEGINNOTE 2266 +#define IDC_EDIT_TABLESIZE 2267 +#define IDC_EDIT_FINETUNESTEPS 2268 +#define IDC_ADD_TUNING 2269 +#define IDC_REMOVE_TUNING 2270 +#define IDC_EDIT_RATIOVALUE 2272 +#define IDC_EDIT_NOTENAME 2273 +#define IDC_BUTTON_SETVALUES 2274 +#define IDC_BUTTON_IMPORT 2275 +#define IDC_BUTTON_EXPORT 2276 +#define IDC_COMBOTUNING 2277 +#define IDC_EDIT_PITCHTEMPOLOCK 2278 +#define IDC_CHECK_PITCHTEMPOLOCK 2279 +#define IDC_CHECK_NEWTUNING 2280 +#define IDC_COMBO_T 2281 +#define IDC_COMBOTUNINGNAME 2282 +#define IDC_EDIT_NAME 2283 +#define IDC_EDIT_MISC_ACTIONS 2284 +#define IDC_TREE_TUNING 2292 +#define IDC_PATTERN_FOLLOWSONG 2293 +#define IDC_TEXT_BPM 2300 +#define IDC_TEXT_RPB 2301 +#define IDC_SPIN_RPB 2302 +#define IDC_EDIT_RPB 2303 +#define IDC_NAMEFILTER 2304 +#define IDC_MIDIVOL_TO_NOTEVOL 2306 +#define IDC_PLUGIN_VELOCITYSTYLE 2307 +#define IDC_PLUGIN_VOLUMESTYLE 2308 +#define IDC_CHECKACTIVE 2310 +#define IDC_COMBO_CONTROLLER 2311 +#define IDC_COMBO_CHANNEL 2312 +#define IDC_COMBO_PLUGIN 2313 +#define IDC_COMBO_PARAM 2314 +#define IDC_COMBO_EVENT 2315 +#define IDC_BUTTON_ADD 2316 +#define IDC_BUTTON_REPLACE 2317 +#define IDC_BUTTON_REMOVE 2318 +#define IDC_CHECK_MIDILEARN 2319 +#define IDC_MIDIPLAYCONTROL 2320 +#define IDC_CHK_COMPATPLAY 2321 +#define IDC_CHK_MIDICCBUG 2322 +#define IDC_CHK_OLDRANDOM 2323 +#define IDC_CHECKCAPTURE 2324 +#define IDC_SPINMOVEMAPPING 2325 +#define IDC_BUTTON_HALF 2326 +#define IDC_BUTTON_DOUBLE 2327 +#define IDC_GROUPBOX_PITCH_TIME 2328 +#define IDC_TEXT_PITCH 2329 +#define IDC_TEXT_QUALITY 2330 +#define IDC_TEXT_FFT 2331 +#define IDC_TEXT_PERCENT 2332 +#define IDC_EQ_WARNING 2333 +#define IDC_PLUGFRAME 2334 +#define IDC_SAMPLE_STEREOSEPARATION 2335 +#define IDC_TEXT_STRETCHPARAMS 2337 +#define IDC_EDIT_STRETCHPARAMS 2338 +#define IDC_MIDI_MACRO_CONTROL 2339 +#define IDC_MIDIPLAYPATTERNONMIDIIN 2340 +#define IDC_DONTSHOWAGAIN 2341 +#define IDC_MESSAGETEXT 2342 +#define IDC_SAMPLE_DCOFFSET 2343 +#define IDC_OPTIONS_DIR_MODS 2344 +#define IDC_OPTIONS_DIR_SAMPS 2345 +#define IDC_OPTIONS_DIR_INSTS 2346 +#define IDC_OPTIONS_DIR_VSTS 2347 +#define IDC_STATIC_MODDIR 2348 +#define IDC_STATIC_SAMPDIR 2349 +#define IDC_STATIC_INSTRDIR 2350 +#define IDC_STATIC_VSTDIR 2351 +#define IDC_BUTTON_CHANGE_MODDIR 2352 +#define IDC_BUTTON_CHANGE_SAMPDIR 2353 +#define IDC_BUTTON_CHANGE_INSTRDIR 2354 +#define IDC_BUTTON_CHANGE_VSTDIR 2355 +#define IDC_STATIC_AUTOSAVE_OPTIONS 2356 +#define IDC_STATIC_AUTOSAVE_LOCATION 2357 +#define IDC_OPTIONS_DIR_VSTPRESETS 2357 +#define IDC_BUTTON_CHANGE_VSTDIR2 2358 +#define IDC_BUTTON_CHANGE_VSTPRESETSDIR 2358 +#define IDC_STATIC_VSTPRESETDIR 2359 +#define IDC_BUTTON_DEFAULT_RESAMPLING 2360 +#define IDC_STATIC_CREATEDWITH 2361 +#define IDC_TEXT_CREATEDWITH 2361 +#define IDC_STATIC_SAVEDWITH 2362 +#define IDC_STATIC_VSTNAMEFILTER 2362 +#define IDC_TEXT_SAVEDWITH 2362 +#define IDC_TEXT_CURRENT_VSTPLUG 2363 +#define IDC_VENDOR 2364 +#define IDC_FRAME_MPTEXT 2365 +#define IDC_COMBO_MIXLEVELS 2366 +#define IDC_COMBO_TEMPOMODE 2367 +#define IDC_TEXT_TEMPOMODE 2368 +#define IDC_FRAME_TEMPOMODE 2369 +#define IDC_TEXT_MIXMODE 2370 +#define IDC_TEXT_ROWSPERBEAT 2371 +#define IDC_TEXT_ROWSPERMEASURE 2372 +#define IDC_FRAME_MPTVERSION 2373 +#define IDC_EDIT_CREATEDWITH 2374 +#define IDC_EDIT_SAVEDWITH 2375 +#define IDC_FRAME_MODFLAGS 2376 +#define IDC_FRAME_MODTYPE 2377 +#define IDC_EDIT_ADDSILENCE 2378 +#define IDC_EDIT_RESIZESAMPLE 2379 +#define IDC_RADIO_ADDSILENCE_BEGIN 2380 +#define IDC_RADIO_ADDSILENCE_END 2381 +#define IDC_SPIN_ADDSILENCE 2382 +#define IDC_SAMPLE_INITOPL 2383 +#define IDC_RADIO_RESIZETO 2384 +#define IDC_CHECK_PATRECORD 2386 +#define IDC_LOAD_COLORSCHEME 2387 +#define IDC_SAVE_COLORSCHEME 2388 +#define IDC_EDIT_SEQUENCE_NAME 2389 +#define IDC_STATIC_SEQUENCE_NAME 2390 +#define IDC_STATIC_SEQUENCE_NAME_FRAME 2391 +#define IDC_CHK_CLEANUP_PATTERNS 2392 +#define IDC_CHK_CLEANUP_SAMPLES 2393 +#define IDC_CHK_CLEANUP_INSTRUMENTS 2394 +#define IDC_CHK_CLEANUP_PLUGINS 2395 +#define IDC_CHK_REARRANGE_PATTERNS 2396 +#define IDC_CHK_REARRANGE_SAMPLES 2397 +#define IDC_CHK_REMOVE_INSTRUMENTS 2398 +#define IDC_CHK_SAMPLEPACK 2399 +#define IDC_CHK_RESET_VARIABLES 2399 +#define IDC_BTN_CLEANUP_SONG 2400 +#define IDC_BTN_COMPO_CLEANUP 2401 +#define IDC_CHK_REMOVE_ORDERS 2402 +#define IDC_CHK_REMOVE_PATTERNS 2403 +#define IDC_CHK_REMOVE_SAMPLES 2404 +#define IDC_CHK_REMOVE_PLUGINS 2405 +#define IDC_CHK_OPTIMIZE_SAMPLES 2406 +#define IDC_CHK_REMOVE_DUPLICATES 2407 +#define IDC_CHK_MERGE_SEQUENCES 2408 +#define IDC_CHECK_PT1X 2409 +#define IDC_CHK_UNUSED_CHANNELS 2409 +#define IDC_STATIC_CHANNEL_NAME 2410 +#define IDC_CHECK_AMIGALIMITS 2410 +#define IDC_FINDHOTKEY 2411 +#define IDC_STATIC_PATTERNNAME 2412 +#define IDC_EDIT_SAMPLE_LENGTH 2413 +#define IDC_EDIT_SAMPLE_FREQ 2414 +#define IDC_EDIT_FORMULA 2415 +#define IDC_EDIT_SAMPLE_LENGTH_SEC 2416 +#define IDC_RADIO_SMPCLIP1 2417 +#define IDC_RADIO_SMPCLIP2 2418 +#define IDC_RADIO_SMPCLIP3 2419 +#define IDC_STATIC_SMPSIZE_KB 2420 +#define IDC_BUTTON_SHOW_EXPRESSIONS 2421 +#define IDC_SAMPLEGEN_PRESETS 2422 +#define IDC_BUTTON_SAMPLEGEN_PRESETS 2422 +#define IDC_EDIT_PRESET_NAME 2423 +#define IDC_EDIT_PRESET_EXPR 2424 +#define IDC_LIST_SAMPLEGEN_PRESETS 2425 +#define IDC_CHECK_UNDO 2426 +#define IDC_CHK_REMEMBERSETTINGS 2427 +#define IDC_PLUGINTAGS 2428 +#define IDC_STATIC_PLUGINTAGS 2429 +#define IDC_BTN_CLEAR 2430 +#define IDC_TOTAL_EDIT_TIME 2431 +#define IDC_EDIT_HISTORY 2432 +#define IDC_SAMPLE_QUICKFADE 2433 +#define IDC_SAMPLE_XFADE 2434 +#define IDC_LASTUPDATE 2435 +#define IDC_RESTORE_KEYMAP 2436 +#define IDC_SAMPLE_AUTOTUNE 2437 +#define IDC_FIND 2438 +#define IDC_PITCHWHEELDEPTH 2439 +#define IDC_PROMPT 2440 +#define IDC_EDIT_FINETUNE 2441 +#define IDC_EDIT_UNDOSIZE 2442 +#define IDC_FLAC_COMPRESSION 2443 +#define IDC_DEFAULT_FORMAT 2444 +#define IDC_COMPRESS_ITI 2445 +#define IDC_PREVIEW_SAMPLES 2446 +#define IDC_VOLUME_HANDLING 2447 +#define IDC_NORMALIZE 2448 +#define IDC_UNDOSIZE 2449 +#define IDC_SYSLINK1 2450 +#define IDC_VERSION1 2451 +#define IDC_VERSION2 2452 +#define IDC_DATE 2453 +#define IDC_SLIDER_NUMBUFFERS 2454 +#define IDC_EDIT_STATISTICS 2455 +#define IDC_VUMETER 2456 +#define IDC_STATIC_BUFFERLENGTH 2457 +#define IDC_STATIC_UPDATEINTERVAL 2458 +#define IDC_COMBO_UPDATEINTERVAL 2459 +#define IDC_CURSORINHEX 2460 +#define IDC_STATIC_BASECHANNEL 2461 +#define IDC_COMBO_FILTER 2462 +#define IDC_COMBO_AMIGA_TYPE 2463 +#define IDC_SLIDER_STEREOSEP 2464 +#define IDC_CHECK_SOFTPAN 2465 +#define IDC_COMBO_FILTERWINDOW 2466 +#define IDC_TEXT_STEREOSEP 2467 +#define IDC_EDIT_VOLRAMP_SAMPLES_UP 2468 +#define IDC_EDIT_VOLRAMP_SAMPLES_DOWN 2469 +#define IDC_CHECK_KEEPDEVICEOPEN 2470 +#define IDC_STATIC_CHANNEL_FRONT 2471 +#define IDC_STATIC_CHANNEL_REAR 2473 +#define IDC_COMBO_CHANNEL_FRONTLEFT 2475 +#define IDC_COMBO_CHANNEL_FRONTRIGHT 2476 +#define IDC_COMBO_CHANNEL_REARLEFT 2477 +#define IDC_COMBO_CHANNEL_REARRIGHT 2478 +#define IDC_SCANTEXT 2479 +#define IDC_STATIC_CHANNELMAPPING 2480 +#define IDC_STATIC_LATENCY 2481 +#define IDC_STATIC_FORMAT 2482 +#define IDC_TABABOUT 2483 +#define IDC_EDITABOUT 2484 +#define IDC_CONTAINER 2485 +#define IDC_SAMPLE_OPENKNOWN 2486 +#define IDC_SAMPLE_OPENRAW 2487 +#define IDC_SAMPLE_DUPLICATE 2488 +#define IDC_EDIT_SAMPVOL1 2489 +#define IDC_SPIN_SAMPVOL1 2490 +#define IDC_EDIT_SAMPVOL2 2491 +#define IDC_SPIN_SAMPVOL2 2492 +#define IDC_EDIT_OFFSET 2493 +#define IDC_SPIN_OFFSET 2494 +#define IDC_CHECK_WINE_ENABLE 2495 +#define IDC_STATIC_WINE_PORTAUDIO 2496 +#define IDC_STATIC_WINE_PULSEAUDIO 2497 +#define IDC_COMBO_WINE_PORTAUDIO 2498 +#define IDC_COMBO_WINE_PULSEAUDIO 2499 +#define IDC_BUTTON_TUNING_NEW 2500 +#define IDC_BUTTON_TUNING_REMOVE 2501 +#define IDC_STATIC_WINE_RTAUDIO 2502 +#define IDC_COMBO_WINE_RTAUDIO 2503 +#define IDC_STATIC_UPDATECHECK 2504 +#define IDC_STATIC_UPDATEPRIVACY 2505 +#define IDC_STATIC_UDATECHANNEL 2506 +#define IDC_COMBO_UPDATEFREQUENCY 2507 +#define IDC_STATIC_UPDATEFREQUENCY 2508 +#define IDC_CHECK_UPDATEENABLED 2509 +#define IDC_STATIC_UPDATEPRIVACYTEXT 2510 +#define IDC_STATIC_WELCOME_STATISTICS 2511 +#define IDC_CHECK_SOUNDCARD_SHOWALL 2512 +#define IDC_STATIC_RECORDING 2513 +#define IDC_COMBO_RECORDING_CHANNELS 2514 +#define IDC_COMBO_RECORDING_SOURCE 2515 +#define IDC_CHECK_UPDATEINSTALLAUTOMATICALLY 2516 +#define IDC_SUBSONG 2517 +#define ID_FILE_NEWMOD 32771 +#define ID_FILE_NEWXM 32772 +#define ID_FILE_NEWS3M 32773 +#define ID_FILE_NEWIT 32774 +#define ID_FILE_SAVEASWAVE 32775 +#define ID_PLAYER_PLAY 32776 +#define ID_PLAYER_PLAYFROMSTART 32777 +#define ID_PLAYER_STOP 32778 +#define ID_PLAYER_PAUSE 32779 +#define ID_PLAYER_SETUP 32780 +#define ID_VIEW_GLOBALS 32781 +#define ID_VIEW_PATTERNS 32782 +#define ID_VIEW_SAMPLES 32783 +#define ID_VIEW_INSTRUMENTS 32784 +#define ID_VIEW_OPTIONS 32785 +#define ID_INSTRUMENTS_REMOVEALL 32789 +#define ID_CLEANUP_INSTRUMENTS 32790 +#define ID_CLEANUP_SAMPLES 32791 +#define ID_CLEANUP_PATTERNS 32792 +#define ID_CLEANUP_SONG 32793 +#define ID_NEXTOCTAVE 32794 +#define ID_PREVOCTAVE 32795 +#define ID_NEXTINSTRUMENT 32796 +#define ID_PREVINSTRUMENT 32797 +#define ID_EDIT_FINDNEXT 32798 +#define ID_EDIT_SELECTCOLUMN 32799 +#define ID_PATTERN_UNMUTEALL 32800 +#define ID_PATTERN_MUTE 32801 +#define ID_PATTERN_SOLO 32802 +#define ID_PATTERN_INSERTROW 32803 +#define ID_PATTERN_DELETEROW 32804 +#define ID_PATTERN_TOP 32805 +#define ID_PATTERN_BOTTOM 32806 +#define ID_PATTERN_RESTART 32807 +#define ID_SAMPLE_SETLOOP 32808 +#define ID_SAMPLE_SETSUSTAINLOOP 32809 +#define ID_SAMPLE_8BITCONVERT 32810 +#define ID_ENVELOPE_SETLOOP 32811 +#define ID_ENVELOPE_SUSTAIN 32812 +#define ID_ENVELOPE_INSERTPOINT 32813 +#define ID_ENVELOPE_REMOVEPOINT 32814 +#define ID_MODTREE_REFRESH 32815 +#define ID_PATTERN_PLAYROW 32816 +#define ID_PATTERN_DELETEALLROW 32818 +#define ID_PATTERN_INSERTALLROW 32819 +#define ID_NOTEMAP_REMOVE 32820 +#define ID_NOTEMAP_TRANSPOSE_SAMPLES 32821 +#define ID_MODTREE_EXECUTE 32822 +#define ID_MODTREE_REMOVE 32823 +#define ID_IMPORT_MIDILIB 32824 +#define ID_EXPORT_MIDILIB 32825 +#define ID_PATTERN_PLAY 32826 +#define ID_SAMPLE_ZOOMONSEL 32827 +#define ID_MODTREE_PLAY 32828 +#define ID_CLEANUP_REARRANGE 32829 +#define ID_SAMPLE_SETLOOPSTART 32830 +#define ID_SAMPLE_SETLOOPEND 32831 +#define ID_SAMPLE_SETSUSTAINSTART 32832 +#define ID_SAMPLE_SETSUSTAINEND 32833 +#define ID_NOTEMAP_COPY_SMP 32834 +#define ID_NOTEMAP_RESET 32835 +#define ID_PATTERN_INTERPOLATE_VOLUME 32836 +#define ID_PATTERN_INTERPOLATE_EFFECT 32837 +#define ID_MODTREE_REFRESHINSTRLIB 32838 +#define ID_VIEW_COMMENTS 32839 +#define ID_TRANSPOSE_UP 32840 +#define ID_TRANSPOSE_DOWN 32841 +#define ID_TRANSPOSE_OCTUP 32842 +#define ID_TRANSPOSE_OCTDOWN 32843 +#define ID_ADD_SOUNDBANK 32844 +#define ID_ORDERLIST_INSERT 32845 +#define ID_ORDERLIST_DELETE 32846 +#define ID_MIDI_RECORD 32847 +#define ID_PATTERN_PROPERTIES 32848 +#define ID_SOUNDBANK_PROPERTIES 32849 +#define ID_PATTERN_VUMETERS 32850 +#define ID_ENVELOPE_CARRY 32851 +#define ID_EDIT_SELECTCOLUMN2 32852 +#define ID_PATTERN_CHORDEDIT 32853 +#define ID_PATTERN_MIDIMACRO 32854 +#define ID_EDIT_RECSELECT 32855 +#define ID_ORDERLIST_NEW 32856 +#define ID_ORDERLIST_COPY 32857 +#define ID_PATTERN_SETINSTRUMENT 32858 +#define ID_MODTREE_SHOWALLFILES 32860 +#define ID_MODTREE_SOUNDFILESONLY 32861 +#define ID_ENVSEL_VOLUME 32862 +#define ID_ENVSEL_PANNING 32863 +#define ID_ENVSEL_PITCH 32864 +#define ID_ENVELOPE_VOLUME 32865 +#define ID_ENVELOPE_PANNING 32866 +#define ID_ENVELOPE_PITCH 32867 +#define ID_ENVELOPE_FILTER 32868 +#define ID_PATTERN_EXPAND 32869 +#define ID_PATTERN_SHRINK 32870 +#define ID_HELP_SEARCH 32871 +#define ID_MODTREE_SHOWDIRS 32872 +#define ID_PATTERNCOPY 32874 +#define ID_PATTERNPASTE 32875 +#define ID_NETLINK_MODPLUG 32876 +#define ID_NETLINK_TOP_PICKS 32877 +#define ID_SETRESTARTPOS 32878 +#define ID_SAMPLE_TRIM 32880 +#define ID_FILE_SAVEMIDI 32881 +#define ID_MODTREE_OPENITEM 32882 +#define ID_SAMPLE_SLICE 32885 +#define ID_INSTRUMENT_SAMPLEMAP 32886 +#define ID_SAMPLE_MONOCONVERT 32887 +#define ID_SAMPLE_ZOOMUP 32889 +#define ID_SAMPLE_ZOOMDOWN 32890 +#define ID_PATTERNDETAIL_LO 32891 +#define ID_PATTERNDETAIL_MED 32892 +#define ID_PATTERNDETAIL_HI 32893 +#define ID_INSTRUMENT_DUPLICATE 32894 +#define ID_PATTERN_AMPLIFY 32895 +#define ID_MODTREE_MUTE 32896 +#define ID_MODTREE_UNMUTEALL 32897 +#define ID_MODTREE_SOLO 32898 +#define ID_ESTIMATESONGLENGTH 32899 +#define ID_PATTERN_VISUALIZE_EFFECT 32900 +#define ID_PATTERN_PLAYNOLOOP 32901 +#define ID_MODTREE_MUTE_ONLY_EFFECTS 32902 +#define ID_PATTERN_OPEN_RANDOMIZER 32905 +#define ID_PATTERN_INTERPOLATE_NOTE 32906 +#define ID_PATTERN_CHNRESET 32907 +#define ID_PATTERN_INTERPOLATE_INSTR 32908 +#define ID_PATTERN_SPLIT 32909 +#define ID_ORDERLIST_MERGE 32910 +#define ID_PRESET_LOAD 32915 +#define ID_PRESET_SAVE 32916 +#define ID_PRESET_RANDOM 32917 +#define ID_PRESET_LIST 32919 +#define ID_INFO 32920 +#define ID_VIEWPLUGNAMES 32921 +#define ID_EQSLIDER_BASE 32922 +// From here: Command range [ID_EQSLIDER_BASE, ID_EQSLIDER_BASE + MAX_EQ_BANDS] +#define ID_EQMENU_BASE 32950 +// From here: Command range [ID_EQMENU_BASE, ID_EQMENU_BASE + EQ_MAX_FREQS] +#define ID_SELECT_MIDI_DEVICE 33000 +// From here: Command range [ID_SELECT_MIDI_DEVICE, ID_SELECT_MIDI_DEVICE + MAX_MIDI_DEVICES] +#define ID_EDIT_SPLITRECSELECT 33900 +#define ID_REARRANGE_SAMPLES 33901 +#define ID_CHANNEL_MANAGER 33905 +#define ID_MODTREE_RELOADITEM 33906 +#define ID_MODTREE_RELOADALL 33907 +#define ID_MODTREE_SAVEALL 33908 +#define ID_MODTREE_FINDMISSING 33909 +#define ID_MODTREE_GOTO_INSDIR 33910 +#define ID_MODTREE_GOTO_SMPDIR 33911 +#define ID_MODTREE_SETPATH 33916 +#define ID_MODTREE_SAVEITEM 33917 +#define ID_PLUGIN_SETUP 33918 +#define ID_PRESET_SET 33920 +// From here: Command range [ID_PRESET_SET, ID_PRESET_SET + PRESETS_PER_GROUP] +#define ID_PLUGSELECT 35000 +// From here: Command range [ID_PLUGSELECT, ID_PLUGSELECT + MAX_MIXPLUGINS] +#define ID_VSTMACRO_INFO 36002 +#define ID_VSTINPUT_INFO 36003 +#define ID_APPROX_BPM 36007 +#define ID_FACTORY_MENU 36008 +#define ID_PLUG_BYPASS 36009 +#define ID_FACTORY_PRESETS 36010 +#define ID_INFO_INPUTS 36011 +#define ID_INFO_OUTPUTS 36012 +#define ID_INFO_MACROS 36013 +#define ID_INFO_OUPUTS 36016 +#define ID_EDIT_SAMPLETRIMMER 36017 +#define ID_EDIT_GOTO 36018 +#define ID_VIEW_GRAPH 36019 +#define ID_PATTERN_TRANSITIONMUTE 36020 +#define ID_PATTERN_TRANSITIONSOLO 36021 +#define ID_PATTERN_TRANSITION_UNMUTEALL 36022 +#define ID_REMOVETUNING 36023 +#define ID_COPYTUNING 36026 +#define ID_REMOVETUNINGCOLLECTION 36027 +#define ID_SHOWTIMEATROW 36028 +#define ID_PREVIOUSVSTPRESET 36029 +#define ID_NEXTVSTPRESET 36030 +#define ID_VSTPRESETBACKWARDJUMP 36031 +#define ID_VSTPRESETFORWARDJUMP 36032 +#define ID_VSTPRESETNAME 36033 +#define ID_ADDTUNINGGENERAL 36034 +#define ID_ADDTUNINGGROUPGEOMETRIC 36035 +#define ID_ADDTUNINGGEOMETRIC 36036 +#define ID_SELECTINST 36100 +// From here: Command range [ID_SELECTINST, ID_SELECTINST + MAX_INSTRUMENTS] +#define ID_PLUG_RECORDAUTOMATION 37003 +#define ID_LEARN_MACRO_FROM_PLUGGUI 37004 +// From here: Command range [ID_LEARN_MACRO_FROM_PLUGGUI, ID_LEARN_MACRO_FROM_PLUGGUI + NUM_MACROS] +#define ID_CHANGE_INSTRUMENT 37020 +// From here: Command range [ID_CHANGE_INSTRUMENT, ID_CHANGE_INSTRUMENT + MAX_INSTRUMENTS] +#define ID_CLEAR_SELECTION 38000 +#define ID_PLUG_PASSKEYS 38001 +#define ID_VIEW_SONGPROPERTIES 38002 +#define ID_SEQUENCE_ITEM 38003 +// From here: Command range [ID_SEQUENCE_ITEM, ID_SEQUENCE_ITEM + MAX_SEQUENCES + 2] +#define ID_NOTEMAP_EDITSAMPLE 39000 +// From here: Command range [ID_NOTEMAP_EDITSAMPLE, ID_NOTEMAP_EDITSAMPLE + MAX_SAMPLES] +#define ID_GROW_SELECTION 43001 +#define ID_SHRINK_SELECTION 43002 +#define ID_RUN_SCRIPT 43003 +#define ID_EXAMPLE_MODULES 43004 +// From here: Command range [ID_EXAMPLE_MODULES, ID_EXAMPLE_MODULES_LASTINRANGE] +#define ID_EXAMPLE_MODULES_LASTINRANGE 43054 +#define ID_FILE_OPENTEMPLATE 43055 +// From here: Command range [ID_FILE_OPENTEMPLATE, ID_FILE_OPENTEMPLATE_LASTINRANGE] +#define ID_FILE_OPENTEMPLATE_LASTINRANGE 43105 +#define ID_INDICATOR_TIME 43143 +#define ID_INDICATOR_USER 43144 +#define ID_INDICATOR_INFO 43145 +#define ID_FILE_SAVECOMPAT 43146 +#define ID_INDICATOR_XINFO 43147 +#define ID_PATTERN_ADDCHANNEL_FRONT 43148 +#define ID_PATTERN_ADDCHANNEL_AFTER 43149 +#define ID_PATTERN_REMOVECHANNEL 43150 +#define ID_PATTERN_REMOVECHANNELDIALOG 43151 +#define ID_NEW_MPT 43152 +#define ID_CLEANUP_PLUGS 43153 +#define ID_ENVELOPE_TOGGLERELEASENODE 43154 +#define ID_ENVELOPE_SCALEPOINTS 43155 +#define ID_VIEW_MIDIMAPPING 43156 +#define ID_COPY_ALL_NAMES 43157 +#define ID_PATTERN_DELETEROWGLOBAL 43158 +#define ID_PATTERN_INSERTROWGLOBAL 43159 +#define ID_PATTERN_DELETEALLROWGLOBAL 43160 +#define ID_PATTERN_INSERTALLROWGLOBAL 43161 +#define ID_PATTERN_RESETCHANNELCOLORS 43214 +#define ID_PATTERN_TRANSPOSECHANNEL 43215 +#define ID_PATTERN_DUPLICATECHANNEL 43216 +#define ID_EDIT_GOTO_MENU 43217 +#define ID_CLEANUP_COMPO 43218 +#define ID_SAMPLE_DRAW 43219 +#define ID_SAMPLE_ADDSILENCE 43220 +#define ID_OVERFLOWPASTE 43221 +#define ID_NOTEMAP_COPY_NOTE 43222 +#define ID_CLEANUP_REARRANGESAMPLES 43223 +#define ID_ORDERLIST_RENDER 43224 +#define ID_EDIT_CLEANUP 43225 +#define ID_ORDERLIST_EDIT_COPY 43226 +#define ID_ORDERLIST_EDIT_CUT 43227 +#define ID_CLIPBOARD_MANAGER 43228 +#define ID_CHANNEL_RENAME 43229 +#define ID_EDIT_PASTEFLOOD 43230 +#define ID_MODTREE_DUPLICATE 43231 +#define ID_MODTREE_INSERT 43232 +#define ID_MODTREE_SWITCHTO 43233 +#define ID_EDIT_PUSHFORWARDPASTE 43234 +#define ID_EDIT_SPLITKEYBOARDSETTINGS 43235 +#define ID_EDIT_PASTESPECIAL 43236 +#define ID_ORDERLIST_EDIT_COPY_ORDERS 43237 +#define ID_FILE_SAVE_COPY 43238 +#define ID_MODTREE_RENAME 43239 +#define ID_UPDATE_AVAILABLE 43240 +#define ID_CHANGE_PCNOTE_PARAM 43242 +// From here: Command range [ID_CHANGE_PCNOTE_PARAM, ID_CHANGE_PCNOTE_PARAM + ModCommand::maxColumnValue] +#define ID_MODTREE_CLOSE 44243 +#define ID_SAMPLE_GENERATOR_MENU 44244 +#define ID_SAMPLE_GENERATOR_PRESET_MENU 44344 +// From here: Command range [ID_SAMPLE_GENERATOR_PRESET_MENU, ID_SAMPLE_GENERATOR_PRESET_MENU + MAX_SAMPLEGEN_EXPRESSIONS - 1] +#define ID_SAMPLE_GENERATE 44445 +#define ID_NOTEMAP_TRANS_UP 44446 +#define ID_NOTEMAP_TRANS_DOWN 44447 +#define ID_PATTERN_EDIT_PCNOTE_PLUGIN 44448 +#define ID_ENVELOPE_ZOOM_IN 44449 +#define ID_ENVELOPE_ZOOM_OUT 44450 +#define ID_PANIC 44451 +#define ID_VIEW_EDITHISTORY 44452 +#define ID_SAMPLE_GRID 44453 +#define ID_SAMPLE_QUICKFADE 44454 +#define ID_EDIT_MIXPASTE_ITSTYLE 44455 +#define ID_VIEW_MPTHACKS 44456 +#define ID_PLUGINTOINSTRUMENT 44457 +#define ID_INTERNETUPDATE 44458 +#define ID_HELP_EXAMPLEMODULES 44459 +#define ID_FILE_SAVEASTEMPLATE 44460 +#define ID_SAMPLE_CUE_1 44461 +// From here: Command range [ID_SAMPLE_CUE_1, ID_SAMPLE_CUE_9] +#define ID_SAMPLE_CUE_9 44469 +#define ID_ORDERLIST_INSERT_SEPARATOR 44470 +#define ID_ENVELOPE_LOAD 44471 +#define ID_ENVELOPE_SAVE 44472 +#define ID_PLUGINEDITOR_SLIDERS_BASE 44500 +// From here: Command range [ID_PLUGINEDITOR_SLIDERS_BASE, ID_PLUGINEDITOR_SLIDERS_BASE + NUM_PLUGINEDITOR_PARAMETERS] +#define ID_PLUGINEDITOR_EDIT_BASE 44550 +// From here: Command range [ID_PLUGINEDITOR_EDIT_BASE, ID_PLUGINEDITOR_EDIT_BASE + NUM_PLUGINEDITOR_PARAMETERS] +#define ID_HELP_SHOWSETTINGSFOLDER 44600 +#define ID_FILE_CLOSEALL 44601 +#define ID_HELPSHOW 44602 +#define ID_ORDERLIST_LOCKPLAYBACK 44603 +#define ID_ORDERLIST_UNLOCKPLAYBACK 44604 +#define ID_TRANSPOSE_CUSTOM 44605 +#define ID_SAMPLE_MONOCONVERT_LEFT 44606 +#define ID_SAMPLE_MONOCONVERT_RIGHT 44607 +#define ID_SAMPLE_MONOCONVERT_SPLIT 44608 +#define ID_SETQUANTIZE 44609 +#define ID_PLUG_RECORD_MIDIOUT 44610 +#define ID_MRU_LIST_FIRST 44611 +// From here: Command range [ID_MRU_LIST_FIRST, ID_MRU_LIST_LAST] +#define ID_MRU_LIST_LAST 44642 +#define ID_FILE_APPENDMODULE 44643 +#define ID_SAMPLE_16BITCONVERT 44644 +#define ID_LOCK_PATTERN_ROWS 44645 +#define ID_SAMPLE_TIMELINE_SECONDS 44646 +#define ID_SAMPLE_TIMELINE_SAMPLES 44647 +#define ID_SAMPLE_TIMELINE_SAMPLES_POW2 44648 +#define ID_SAMPLE_INSERT_CUEPOINT 44649 +#define ID_SAMPLE_DELETE_CUEPOINT 44650 +#define ID_CONVERT_PINGPONG_LOOP 44651 +#define ID_CONVERT_PINGPONG_SUSTAIN 44652 +#define ID_RENAME_PLUGIN 44653 +#define ID_FILE_SAVEOPL 44654 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_3D_CONTROLS 1 +#define _APS_NEXT_RESOURCE_VALUE 543 +#define _APS_NEXT_COMMAND_VALUE 44655 +#define _APS_NEXT_CONTROL_VALUE 2518 +#define _APS_NEXT_SYMED_VALUE 901 +#endif +#endif diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.cpp new file mode 100644 index 00000000..1a55a2c1 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.cpp @@ -0,0 +1,180 @@ +/* + * tuningRatioMapWnd.cpp + * --------------------- + * Purpose: Alternative sample tuning configuration dialog - ratio map edit control. + * 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 "../soundlib/tuning.h" +#include "tuningRatioMapWnd.h" +#include "TuningDialog.h" + + +OPENMPT_NAMESPACE_BEGIN + + +BEGIN_MESSAGE_MAP(CTuningRatioMapWnd, CStatic) + ON_WM_PAINT() + ON_WM_SETFOCUS() + ON_WM_KILLFOCUS() + ON_WM_LBUTTONDOWN() + ON_WM_MOUSEWHEEL() +END_MESSAGE_MAP() + + +void CTuningRatioMapWnd::Init(CTuningDialog* const pParent, CTuning* const tuning) +{ + m_pParent = pParent; + m_pTuning = tuning; +} + +void CTuningRatioMapWnd::OnPaint() +{ + CPaintDC dc(this); + + if(!m_pTuning) return; + + CRect rcClient; + GetClientRect(&rcClient); + + const auto colorText = GetSysColor(COLOR_WINDOWTEXT); + const auto colorTextSel = GetSysColor(COLOR_HIGHLIGHTTEXT); + const auto highlightBrush = GetSysColorBrush(COLOR_HIGHLIGHT), windowBrush = GetSysColorBrush(COLOR_WINDOW); + + auto oldFont = dc.SelectObject(CMainFrame::GetGUIFont()); + dc.SetBkMode(TRANSPARENT); + if ((m_cxFont <= 0) || (m_cyFont <= 0)) + { + CSize sz; + sz = dc.GetTextExtent(_T("123456789")); + m_cyFont = sz.cy + 2; + m_cxFont = rcClient.right / 4; + } + dc.IntersectClipRect(&rcClient); + if ((m_cxFont > 0) && (m_cyFont > 0)) + { + const bool focus = (::GetFocus() == m_hWnd); + CRect rect; + + NOTEINDEXTYPE nNotes = static_cast<NOTEINDEXTYPE>((rcClient.bottom + m_cyFont - 1) / m_cyFont); + //if(!m_nNote) m_nNote = m_nNoteCentre; + NOTEINDEXTYPE nPos = m_nNote - (nNotes/2); + int ypaint = 0; + + for (int ynote=0; ynote<nNotes; ynote++, ypaint+=m_cyFont, nPos++) + { + // Note + NOTEINDEXTYPE noteToDraw = nPos - m_nNoteCentre; + const bool isValidNote = m_pTuning->IsValidNote(noteToDraw); + + rect.SetRect(0, ypaint, m_cxFont, ypaint + m_cyFont); + const auto noteStr = isValidNote ? mpt::tfmt::val(noteToDraw) : mpt::tstring(_T("...")); + DrawButtonRect(dc, &rect, noteStr.c_str(), FALSE, FALSE); + + // Mapped Note + const bool highLight = focus && (nPos == (int)m_nNote); + rect.left = rect.right; + rect.right = m_cxFont*4-1; + FillRect(dc, &rect, highLight ? highlightBrush : windowBrush); + if(nPos == (int)m_nNote) + { + rect.InflateRect(-1, -1); + dc.DrawFocusRect(&rect); + rect.InflateRect(1, 1); + } + dc.SetTextColor(highLight ? colorTextSel : colorText); + + rect.SetRect(m_cxFont * 1, ypaint, m_cxFont * 2 - 1, ypaint + m_cyFont); + dc.DrawText(mpt::ToCString(m_pTuning->GetNoteName(noteToDraw)), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_NOPREFIX); + + rect.SetRect(m_cxFont * 2, ypaint, m_cxFont * 3 - 1, ypaint + m_cyFont); + dc.DrawText(mpt::cfmt::flt(m_pTuning->GetRatio(noteToDraw), 6), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_NOPREFIX); + + rect.SetRect(m_cxFont * 3, ypaint, m_cxFont * 4 - 1, ypaint + m_cyFont); + dc.DrawText(mpt::cfmt::fix(std::log2(static_cast<double>(m_pTuning->GetRatio(noteToDraw))) * 1200.0, 1), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER | DT_NOPREFIX); + + } + rect.SetRect(rcClient.left + m_cxFont * 4 - 1, rcClient.top, rcClient.left + m_cxFont * 4 + 3, ypaint); + DrawButtonRect(dc, &rect, _T("")); + if (ypaint < rcClient.bottom) + { + rect.SetRect(rcClient.left, ypaint, rcClient.right, rcClient.bottom); + FillRect(dc, &rect, GetSysColorBrush(COLOR_BTNFACE)); + } + } + dc.SelectObject(oldFont); +} + +void CTuningRatioMapWnd::OnSetFocus(CWnd *pOldWnd) +{ + CWnd::OnSetFocus(pOldWnd); + InvalidateRect(NULL, FALSE); +} + + +void CTuningRatioMapWnd::OnKillFocus(CWnd *pNewWnd) +{ + CWnd::OnKillFocus(pNewWnd); + InvalidateRect(NULL, FALSE); +} + + +BOOL CTuningRatioMapWnd::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) +{ + NOTEINDEXTYPE note = static_cast<NOTEINDEXTYPE>(m_nNote - mpt::signum(zDelta)); + if(m_pTuning->IsValidNote(note - m_nNoteCentre)) + { + m_nNote = note; + InvalidateRect(NULL, FALSE); + if(m_pParent) + m_pParent->UpdateRatioMapEdits(GetShownCentre()); + } + + return CWnd::OnMouseWheel(nFlags, zDelta, pt); +} + + +void CTuningRatioMapWnd::OnLButtonDown(UINT, CPoint pt) +{ + if ((pt.x >= m_cxFont) && (pt.x < m_cxFont*2)) + { + InvalidateRect(NULL, FALSE); + } + if ((pt.x > m_cxFont*2) && (pt.x <= m_cxFont*3)) + { + InvalidateRect(NULL, FALSE); + } + if ((pt.x >= 0) && (m_cyFont)) + { + CRect rcClient; + GetClientRect(&rcClient); + int nNotes = (rcClient.bottom + m_cyFont - 1) / m_cyFont; + const int n = (pt.y / m_cyFont) + m_nNote - (nNotes/2); + const NOTEINDEXTYPE note = static_cast<NOTEINDEXTYPE>(n - m_nNoteCentre); + if(m_pTuning->IsValidNote(note)) + { + m_nNote = static_cast<NOTEINDEXTYPE>(n); + InvalidateRect(NULL, FALSE); + if(m_pParent) + m_pParent->UpdateRatioMapEdits(GetShownCentre()); + } + + } + SetFocus(); +} + + + +NOTEINDEXTYPE CTuningRatioMapWnd::GetShownCentre() const +{ + return m_nNote - m_nNoteCentre; +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.h b/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.h new file mode 100644 index 00000000..c849a857 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/tuningRatioMapWnd.h @@ -0,0 +1,54 @@ +/* + * tuningRatioMapWnd.h + * ------------------- + * Purpose: Alternative sample tuning configuration dialog - ratio map edit control. + * Notes : (currently none) + * Authors: OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "../soundlib/tuning.h" + +OPENMPT_NAMESPACE_BEGIN + +class CTuningDialog; + +using NOTEINDEXTYPE = Tuning::NOTEINDEXTYPE; + +//Copied from CNoteMapWnd. +class CTuningRatioMapWnd: public CStatic +{ + friend class CTuningDialog; +protected: + const CTuning* m_pTuning = nullptr; + CTuningDialog* m_pParent = nullptr; + + int m_cxFont = 0, m_cyFont = 0; + NOTEINDEXTYPE m_nNote = NOTE_MIDDLEC; + + NOTEINDEXTYPE m_nNoteCentre = NOTE_MIDDLEC; + + +public: + void Init(CTuningDialog* const pParent, CTuning* const tuning); + + NOTEINDEXTYPE GetShownCentre() const; + +protected: + + afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); + afx_msg void OnLButtonDown(UINT, CPoint); + afx_msg void OnLButtonUp(UINT, CPoint); + afx_msg void OnSetFocus(CWnd *pOldWnd); + afx_msg void OnKillFocus(CWnd *pNewWnd); + afx_msg void OnPaint(); + + DECLARE_MESSAGE_MAP() +}; + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/view_com.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/view_com.cpp new file mode 100644 index 00000000..12bb7983 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/view_com.cpp @@ -0,0 +1,697 @@ +/* + * view_com.cpp + * ------------ + * Purpose: Song comments tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * 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 "Childfrm.h" +#include "Clipboard.h" +#include "ImageLists.h" +#include "Moddoc.h" +#include "Globals.h" +#include "Ctrl_com.h" +#include "ChannelManagerDlg.h" +#include "../common/mptStringBuffer.h" +#include "view_com.h" +#include "../soundlib/mod_specifications.h" + + +OPENMPT_NAMESPACE_BEGIN + + +#define DETAILS_TOOLBAR_CY Util::ScalePixels(28, m_hWnd) + +enum +{ + SMPLIST_SAMPLENAME = 0, + SMPLIST_SAMPLENO, + SMPLIST_SIZE, + SMPLIST_TYPE, + SMPLIST_MIDDLEC, + SMPLIST_INSTR, + SMPLIST_FILENAME, + SMPLIST_PATH, + SMPLIST_COLUMNS +}; + + +enum +{ + INSLIST_INSTRUMENTNAME = 0, + INSLIST_INSTRUMENTNO, + INSLIST_SAMPLES, + INSLIST_ENVELOPES, + INSLIST_FILENAME, + INSLIST_PLUGIN, + INSLIST_COLUMNS +}; + + +const CListCtrlEx::Header gSampleHeaders[SMPLIST_COLUMNS] = +{ + { _T("Sample Name"), 192, LVCFMT_LEFT }, + { _T("Num"), 45, LVCFMT_RIGHT }, + { _T("Size"), 72, LVCFMT_RIGHT }, + { _T("Type"), 45, LVCFMT_RIGHT }, + { _T("C-5 Freq"), 80, LVCFMT_RIGHT }, + { _T("Instr"), 64, LVCFMT_RIGHT }, + { _T("File Name"), 128, LVCFMT_RIGHT }, + { _T("Path"), 256, LVCFMT_LEFT }, +}; + +const CListCtrlEx::Header gInstrumentHeaders[INSLIST_COLUMNS] = +{ + { _T("Instrument Name"), 192, LVCFMT_LEFT }, + { _T("Num"), 45, LVCFMT_RIGHT }, + { _T("Samples"), 64, LVCFMT_RIGHT }, + { _T("Envelopes"), 128, LVCFMT_RIGHT }, + { _T("File Name"), 128, LVCFMT_RIGHT }, + { _T("Plugin"), 128, LVCFMT_RIGHT }, +}; + + +IMPLEMENT_SERIAL(CViewComments, CModScrollView, 0) + +BEGIN_MESSAGE_MAP(CViewComments, CModScrollView) + //{{AFX_MSG_MAP(CViewComments) + ON_WM_SIZE() + ON_WM_DESTROY() + ON_MESSAGE(WM_MOD_KEYCOMMAND, &CViewComments::OnCustomKeyMsg) + ON_MESSAGE(WM_MOD_MIDIMSG, &CViewComments::OnMidiMsg) + ON_COMMAND(IDC_LIST_SAMPLES, &CViewComments::OnShowSamples) + ON_COMMAND(IDC_LIST_INSTRUMENTS, &CViewComments::OnShowInstruments) + ON_COMMAND(IDC_LIST_PATTERNS, &CViewComments::OnShowPatterns) + ON_COMMAND(ID_COPY_ALL_NAMES, &CViewComments::OnCopyNames) + ON_NOTIFY(LVN_ENDLABELEDIT, IDC_LIST_DETAILS, &CViewComments::OnEndLabelEdit) + ON_NOTIFY(LVN_BEGINLABELEDIT, IDC_LIST_DETAILS, &CViewComments::OnBeginLabelEdit) + ON_NOTIFY(NM_DBLCLK, IDC_LIST_DETAILS, &CViewComments::OnDblClickListItem) + ON_NOTIFY(NM_RCLICK, IDC_LIST_DETAILS, &CViewComments::OnRClickListItem) + //}}AFX_MSG_MAP +END_MESSAGE_MAP() + + +void CViewComments::OnInitialUpdate() +{ + CModScrollView::OnInitialUpdate(); + if(m_nListId == 0) + { + m_nListId = IDC_LIST_SAMPLES; + + // For XM, set the instrument list as the default list + const CModDoc *pModDoc = GetDocument(); + if(pModDoc && pModDoc->GetSoundFile().GetMessageHeuristic() == ModMessageHeuristicOrder::InstrumentsSamples && pModDoc->GetNumInstruments() > 0) + { + m_nListId = IDC_LIST_INSTRUMENTS; + } + } + + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + CRect rect; + + if (pFrame) + { + COMMENTVIEWSTATE &commentState = pFrame->GetCommentViewState(); + if (commentState.initialized) + { + m_nListId = commentState.nId; + } + } + GetClientRect(&rect); + m_ToolBar.Create(WS_CHILD|WS_VISIBLE|CCS_NOPARENTALIGN, rect, this, IDC_TOOLBAR_DETAILS); + m_ToolBar.Init(CMainFrame::GetMainFrame()->m_MiscIcons, CMainFrame::GetMainFrame()->m_MiscIconsDisabled); + m_ItemList.Create(WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_SINGLESEL | LVS_EDITLABELS | LVS_NOSORTHEADER, rect, this, IDC_LIST_DETAILS); + m_ItemList.ModifyStyleEx(0, WS_EX_STATICEDGE); + // Add ToolBar Buttons + m_ToolBar.AddButton(IDC_LIST_SAMPLES, IMAGE_SAMPLES); + m_ToolBar.AddButton(IDC_LIST_INSTRUMENTS, IMAGE_INSTRUMENTS); + //m_ToolBar.AddButton(IDC_LIST_PATTERNS, TIMAGE_TAB_PATTERNS); + m_ToolBar.SetIndent(4); + UpdateButtonState(); + UpdateView(UpdateHint().ModType()); +} + + +void CViewComments::OnDestroy() +{ + if(m_lastNote != NOTE_NONE) + GetDocument()->NoteOff(m_lastNote, true, m_noteInstr, m_noteChannel); + + CChildFrame *pFrame = (CChildFrame *)GetParentFrame(); + if (pFrame) + { + COMMENTVIEWSTATE &commentState = pFrame->GetCommentViewState(); + commentState.initialized = true; + commentState.nId = m_nListId; + } + CModScrollView::OnDestroy(); +} + + +LRESULT CViewComments::OnModViewMsg(WPARAM wParam, LPARAM lParam) +{ + switch(wParam) + { + case VIEWMSG_SETFOCUS: + case VIEWMSG_SETACTIVE: + GetParentFrame()->SetActiveView(this); + m_ItemList.SetFocus(); + return 0; + default: + return CModScrollView::OnModViewMsg(wParam, lParam); + } +} + + +LRESULT CViewComments::OnMidiMsg(WPARAM midiData_, LPARAM) +{ + uint32 midiData = static_cast<uint32>(midiData_); + // Handle MIDI messages assigned to shortcuts + CInputHandler *ih = CMainFrame::GetInputHandler(); + ih->HandleMIDIMessage(kCtxViewComments, midiData) != kcNull + || ih->HandleMIDIMessage(kCtxAllContexts, midiData) != kcNull; + return 1; +} + + +LRESULT CViewComments::OnCustomKeyMsg(WPARAM wParam, LPARAM) +{ + const int item = m_ItemList.GetSelectionMark() + 1; + if(item == 0) + return kcNull; + + auto modDoc = GetDocument(); + if(wParam >= kcCommentsStartNotes && wParam <= kcCommentsEndNotes) + { + const auto lastInstr = m_noteInstr; + m_noteInstr = (m_nListId == IDC_LIST_SAMPLES) ? INSTRUMENTINDEX_INVALID : static_cast<INSTRUMENTINDEX>(item); + const auto note = modDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcCommentsStartNotes), m_noteInstr); + PlayNoteParam params(note); + if(m_nListId == IDC_LIST_SAMPLES) + params.Sample(static_cast<SAMPLEINDEX>(item)); + else if(m_nListId == IDC_LIST_INSTRUMENTS) + params.Instrument(m_noteInstr); + else + return kcNull; + if(m_lastNote != NOTE_NONE) + modDoc->NoteOff(m_lastNote, true, lastInstr, m_noteChannel); + m_noteChannel = modDoc->PlayNote(params); + m_lastNote = note; + return wParam; + } else if(wParam >= kcCommentsStartNoteStops && wParam <= kcCommentsEndNoteStops) + { + const auto note = modDoc->GetNoteWithBaseOctave(static_cast<int>(wParam - kcCommentsStartNoteStops), m_noteInstr); + modDoc->NoteOff(note, false, m_noteInstr, m_noteChannel); + return wParam; + } else if(wParam == kcToggleSmpInsList) + { + bool ok = SwitchToList(m_nListId == IDC_LIST_SAMPLES ? IDC_LIST_INSTRUMENTS : IDC_LIST_SAMPLES); + if(ok) + { + int newItem = 0; + switch(m_nListId) + { + case IDC_LIST_SAMPLES: + // Switch to a sample belonging to previously selected instrument + if(SAMPLEINDEX smp = modDoc->FindInstrumentChild(static_cast<INSTRUMENTINDEX>(item)); smp != 0 && smp != SAMPLEINDEX_INVALID) + newItem = smp - 1; + break; + + case IDC_LIST_INSTRUMENTS: + // Switch to parent instrument of previously selected sample + if(INSTRUMENTINDEX ins = modDoc->FindSampleParent(static_cast<SAMPLEINDEX>(item)); ins != 0 && ins != INSTRUMENTINDEX_INVALID) + newItem = ins - 1; + break; + } + m_ItemList.SetItemState(newItem, LVIS_SELECTED | LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED); + m_ItemList.SetSelectionMark(newItem); + m_ItemList.EnsureVisible(newItem, FALSE); + m_ItemList.SetFocus(); + } + return wParam; + } else if(wParam == kcExecuteSmpInsListItem) + { + OnDblClickListItem(nullptr, nullptr); + return wParam; + } else if(wParam == kcRenameSmpInsListItem) + { + m_ItemList.EditLabel(item - 1); + return wParam; + } + return kcNull; +} + + +BOOL CViewComments::PreTranslateMessage(MSG *pMsg) +{ + if(pMsg) + { + if((pMsg->message == WM_SYSKEYUP) || (pMsg->message == WM_KEYUP) + || (pMsg->message == WM_SYSKEYDOWN) || (pMsg->message == WM_KEYDOWN)) + { + CInputHandler *ih = CMainFrame::GetInputHandler(); + + //Translate message manually + UINT nChar = static_cast<UINT>(pMsg->wParam); + UINT nRepCnt = LOWORD(pMsg->lParam); + UINT nFlags = HIWORD(pMsg->lParam); + KeyEventType kT = ih->GetKeyEventType(nFlags); + if(!ih->IsBypassed() && ih->KeyEvent(kCtxViewComments, nChar, nRepCnt, nFlags, kT) != kcNull) + { + return TRUE; // Mapped to a command, no need to pass message on. + } + } + } + + return CModScrollView::PreTranslateMessage(pMsg); +} + + +/////////////////////////////////////////////////////////////// +// CViewComments drawing + +void CViewComments::UpdateView(UpdateHint hint, CObject *) +{ + const CModDoc *pModDoc = GetDocument(); + + if ((!pModDoc) || (!(m_ItemList.m_hWnd))) return; + const FlagSet<HintType> hintType = hint.GetType(); + if (hintType[HINT_MPTOPTIONS]) + { + m_ToolBar.UpdateStyle(); + } + const SampleHint sampleHint = hint.ToType<SampleHint>(); + const InstrumentHint instrHint = hint.ToType<InstrumentHint>(); + const bool updateSamples = sampleHint.GetType()[HINT_SMPNAMES | HINT_SAMPLEINFO]; + const bool updateInstr = instrHint.GetType()[HINT_INSNAMES|HINT_INSTRUMENT]; + bool updateAll = hintType[HINT_MODTYPE]; + + if(!updateSamples && !updateInstr && !updateAll) return; + + const CSoundFile &sndFile = pModDoc->GetSoundFile(); + + m_ToolBar.ChangeBitmap(IDC_LIST_INSTRUMENTS, sndFile.GetNumInstruments() ? IMAGE_INSTRUMENTS : IMAGE_INSTRMUTE); + + CString s; + LV_ITEM lvi, lvi2; + + m_ItemList.SetRedraw(FALSE); + // Add sample headers + if (m_nListId != m_nCurrentListId || updateAll) + { + UINT ichk = 0; + m_ItemList.DeleteAllItems(); + while ((m_ItemList.DeleteColumn(0)) && (ichk < 25)) ichk++; + m_nCurrentListId = m_nListId; + if (m_nCurrentListId == IDC_LIST_SAMPLES) + { + // Add Sample Headers + m_ItemList.SetHeaders(gSampleHeaders); + } else if (m_nCurrentListId == IDC_LIST_INSTRUMENTS) + { + // Add Instrument Headers + m_ItemList.SetHeaders(gInstrumentHeaders); + } else + updateAll = true; + } + // Add Items + UINT nCount = m_ItemList.GetItemCount(); + // Add Samples + if (m_nCurrentListId == IDC_LIST_SAMPLES && (updateAll || updateSamples)) + { + SAMPLEINDEX nMax = static_cast<SAMPLEINDEX>(nCount); + if (nMax < sndFile.GetNumSamples()) nMax = sndFile.GetNumSamples(); + for (SAMPLEINDEX iSmp = 0; iSmp < nMax; iSmp++) + { + if (iSmp < sndFile.GetNumSamples()) + { + UINT nCol = 0; + for (UINT iCol=0; iCol<SMPLIST_COLUMNS; iCol++) + { + const ModSample &sample = sndFile.GetSample(iSmp + 1); + s.Empty(); + switch(iCol) + { + case SMPLIST_SAMPLENAME: + s = mpt::ToCString(sndFile.GetCharsetInternal(), sndFile.m_szNames[iSmp + 1]); + break; + case SMPLIST_SAMPLENO: + s = mpt::cfmt::dec0<2>(iSmp + 1); + break; + case SMPLIST_SIZE: + if(sample.nLength && !sample.uFlags[CHN_ADLIB]) + { + auto size = sample.GetSampleSizeInBytes(); + if(size >= 1024) + s.Format(_T("%u KB"), size >> 10); + else + s.Format(_T("%u B"), size); + } + break; + case SMPLIST_TYPE: + if(sample.uFlags[CHN_ADLIB]) + s = _T("OPL"); + else if(sample.HasSampleData()) + s = MPT_CFORMAT("{} Bit")(sample.GetElementarySampleSize() * 8); + break; + case SMPLIST_INSTR: + if (sndFile.GetNumInstruments()) + { + bool first = true; + for (INSTRUMENTINDEX i = 1; i <= sndFile.GetNumInstruments(); i++) + { + if (sndFile.IsSampleReferencedByInstrument(iSmp + 1, i)) + { + if (!first) s.AppendChar(_T(',')); + first = false; + + s.AppendFormat(_T("%u"), i); + } + } + } + break; + case SMPLIST_MIDDLEC: + if (sample.nLength) + { + s.Format(_T("%u Hz"), sample.GetSampleRate(sndFile.GetType())); + } + break; + case SMPLIST_FILENAME: + s = mpt::ToCString(sndFile.GetCharsetInternal(), sample.filename); + break; + case SMPLIST_PATH: + s = sndFile.GetSamplePath(iSmp + 1).ToCString(); + break; + } + lvi.mask = LVIF_TEXT; + lvi.iItem = iSmp; + lvi.iSubItem = nCol; + lvi.pszText = const_cast<TCHAR *>(s.GetString()); + if ((iCol) || (iSmp < nCount)) + { + bool update = true; + if (iSmp < nCount) + { + TCHAR stmp[512]; + lvi2 = lvi; + lvi2.pszText = stmp; + lvi2.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + stmp[0] = 0; + m_ItemList.GetItem(&lvi2); + if (s == stmp) update = false; + } + if (update) m_ItemList.SetItem(&lvi); + } else + { + m_ItemList.InsertItem(&lvi); + } + nCol++; + } + } else + { + m_ItemList.DeleteItem(iSmp); + } + } + } else + // Add Instruments + if ((m_nCurrentListId == IDC_LIST_INSTRUMENTS) && (updateAll || updateInstr)) + { + INSTRUMENTINDEX nMax = static_cast<INSTRUMENTINDEX>(nCount); + if (nMax < sndFile.GetNumInstruments()) nMax = sndFile.GetNumInstruments(); + for (INSTRUMENTINDEX iIns = 0; iIns < nMax; iIns++) + { + if (iIns < sndFile.GetNumInstruments()) + { + UINT nCol = 0; + for (UINT iCol=0; iCol<INSLIST_COLUMNS; iCol++) + { + ModInstrument *pIns = sndFile.Instruments[iIns+1]; + s.Empty(); + switch(iCol) + { + case INSLIST_INSTRUMENTNAME: + if (pIns) s = mpt::ToCString(sndFile.GetCharsetInternal(), pIns->name); + break; + case INSLIST_INSTRUMENTNO: + s = mpt::cfmt::dec0<2>(iIns + 1); + break; + case INSLIST_SAMPLES: + if (pIns) + { + bool first = true; + for(auto sample : pIns->GetSamples()) + { + if(!first) s.AppendChar(_T(',')); + first = false; + s.AppendFormat(_T("%u"), sample); + } + } + break; + case INSLIST_ENVELOPES: + if (pIns) + { + if (pIns->VolEnv.dwFlags[ENV_ENABLED]) s += _T("Vol"); + if (pIns->PanEnv.dwFlags[ENV_ENABLED]) { if (!s.IsEmpty()) s += _T(", "); s += _T("Pan"); } + if (pIns->PitchEnv.dwFlags[ENV_ENABLED]) { if (!s.IsEmpty()) s += _T(", "); s += (pIns->PitchEnv.dwFlags[ENV_FILTER] ? _T("Filter") : _T("Pitch")); } + } + break; + case INSLIST_FILENAME: + if (pIns) + { + s = mpt::ToCString(sndFile.GetCharsetInternal(), pIns->filename); + } + break; + case INSLIST_PLUGIN: + if (pIns != nullptr && pIns->nMixPlug > 0 && sndFile.m_MixPlugins[pIns->nMixPlug - 1].IsValidPlugin()) + { + s.Format(_T("FX%02u: "), pIns->nMixPlug); + s += mpt::ToCString(sndFile.m_MixPlugins[pIns->nMixPlug - 1].GetLibraryName()); + } + break; + } + lvi.mask = LVIF_TEXT; + lvi.iItem = iIns; + lvi.iSubItem = nCol; + lvi.pszText = const_cast<TCHAR *>(s.GetString()); + if ((iCol) || (iIns < nCount)) + { + bool update = true; + if (iIns < nCount) + { + TCHAR stmp[512]; + lvi2 = lvi; + lvi2.pszText = stmp; + lvi2.cchTextMax = mpt::saturate_cast<int>(std::size(stmp)); + stmp[0] = 0; + m_ItemList.GetItem(&lvi2); + if (s == stmp) update = false; + } + if (update) m_ItemList.SetItem(&lvi); + } else + { + m_ItemList.InsertItem(&lvi); + } + nCol++; + } + } else + { + m_ItemList.DeleteItem(iIns); + } + } + } else + // Add Patterns + //if ((m_nCurrentListId == IDC_LIST_PATTERNS) && (hintType & (HINT_MODTYPE|HINT_PATNAMES|HINT_PATTERNROW))) + { + } + m_ItemList.SetRedraw(TRUE); +} + + +void CViewComments::RecalcLayout() +{ + CRect rect; + + if (!m_hWnd) return; + GetClientRect(&rect); + m_ToolBar.SetWindowPos(NULL, 0, 0, rect.Width(), DETAILS_TOOLBAR_CY, SWP_NOZORDER|SWP_NOACTIVATE); + m_ItemList.SetWindowPos(NULL, -1, DETAILS_TOOLBAR_CY, rect.Width()+2, rect.Height() - DETAILS_TOOLBAR_CY + 1, SWP_NOZORDER|SWP_NOACTIVATE); +} + + +void CViewComments::UpdateButtonState() +{ + const CModDoc *pModDoc = GetDocument(); + if (pModDoc) + { + m_ToolBar.SetState(IDC_LIST_SAMPLES, ((m_nListId == IDC_LIST_SAMPLES) ? TBSTATE_CHECKED : 0)|TBSTATE_ENABLED); + m_ToolBar.SetState(IDC_LIST_INSTRUMENTS, ((m_nListId == IDC_LIST_INSTRUMENTS) ? TBSTATE_CHECKED : 0)|TBSTATE_ENABLED); + m_ToolBar.SetState(IDC_LIST_PATTERNS, ((m_nListId == IDC_LIST_PATTERNS) ? TBSTATE_CHECKED : 0)|TBSTATE_ENABLED); + m_ToolBar.EnableButton(IDC_LIST_INSTRUMENTS, (pModDoc->GetNumInstruments()) ? TRUE : FALSE); + } +} + + +void CViewComments::OnBeginLabelEdit(LPNMHDR, LRESULT *) +{ + CEdit *editCtrl = m_ItemList.GetEditControl(); + if(editCtrl) + { + const CModSpecifications &specs = GetDocument()->GetSoundFile().GetModSpecifications(); + const auto maxStrLen = (m_nListId == IDC_LIST_SAMPLES) ? specs.sampleNameLengthMax : specs.instrNameLengthMax; + editCtrl->LimitText(maxStrLen); + CMainFrame::GetInputHandler()->Bypass(true); + } +} + + +void CViewComments::OnEndLabelEdit(LPNMHDR pnmhdr, LRESULT *) +{ + CMainFrame::GetInputHandler()->Bypass(false); + LV_DISPINFO *plvDispInfo = (LV_DISPINFO *)pnmhdr; + LV_ITEM &lvItem = plvDispInfo->item; + CModDoc *pModDoc = GetDocument(); + + if(lvItem.pszText != nullptr && !lvItem.iSubItem && pModDoc) + { + UINT iItem = lvItem.iItem; + CSoundFile &sndFile = pModDoc->GetSoundFile(); + + if(m_nListId == IDC_LIST_SAMPLES) + { + if(iItem < sndFile.GetNumSamples()) + { + sndFile.m_szNames[iItem + 1] = mpt::ToCharset(sndFile.GetCharsetInternal(), CString(lvItem.pszText)); + pModDoc->UpdateAllViews(this, SampleHint(static_cast<SAMPLEINDEX>(iItem + 1)).Info().Names(), this); + pModDoc->SetModified(); + } + } else if(m_nListId == IDC_LIST_INSTRUMENTS) + { + if((iItem < sndFile.GetNumInstruments()) && (sndFile.Instruments[iItem + 1])) + { + ModInstrument *pIns = sndFile.Instruments[iItem + 1]; + pIns->name = mpt::ToCharset(sndFile.GetCharsetInternal(), CString(lvItem.pszText)); + pModDoc->UpdateAllViews(this, InstrumentHint(static_cast<INSTRUMENTINDEX>(iItem + 1)).Info().Names(), this); + pModDoc->SetModified(); + } + } else + { + return; + } + m_ItemList.SetItemText(iItem, lvItem.iSubItem, lvItem.pszText); + } +} + + +/////////////////////////////////////////////////////////////// +// CViewComments messages + + +void CViewComments::OnSize(UINT nType, int cx, int cy) +{ + CModScrollView::OnSize(nType, cx, cy); + if (((nType == SIZE_RESTORED) || (nType == SIZE_MAXIMIZED)) && (cx > 0) && (cy > 0) && (m_hWnd)) + { + RecalcLayout(); + } +} + + +bool CViewComments::SwitchToList(int list) +{ + if(list == m_nListId) + return false; + + if(list == IDC_LIST_SAMPLES) + { + m_nListId = IDC_LIST_SAMPLES; + UpdateButtonState(); + UpdateView(UpdateHint().ModType()); + } else if(list == IDC_LIST_INSTRUMENTS) + { + const CModDoc *modDoc = GetDocument(); + if(!modDoc || !modDoc->GetNumInstruments()) + return false; + + m_nListId = IDC_LIST_INSTRUMENTS; + UpdateButtonState(); + UpdateView(UpdateHint().ModType()); + /*} else if(list == IDC_LIST_PATTERNS) + { + m_nListId = IDC_LIST_PATTERNS; + UpdateButtonState(); + UpdateView(UpdateHint().ModType());*/ + } else + { + return false; + } + return true; +} + + +void CViewComments::OnDblClickListItem(NMHDR *, LRESULT *) +{ + // Double click -> switch to instrument or sample tab + int nItem = m_ItemList.GetSelectionMark(); + if(nItem == -1) return; + CModDoc *pModDoc = GetDocument(); + if(!pModDoc) return; + nItem++; + + switch(m_nListId) + { + case IDC_LIST_SAMPLES: + pModDoc->ViewSample(nItem); + break; + case IDC_LIST_INSTRUMENTS: + pModDoc->ViewInstrument(nItem); + break; + case IDC_LIST_PATTERNS: + pModDoc->ViewPattern(nItem, 0); + break; + } +} + + +void CViewComments::OnRClickListItem(NMHDR *, LRESULT *) +{ + HMENU menu = ::CreatePopupMenu(); + ::AppendMenu(menu, MF_STRING, ID_COPY_ALL_NAMES, _T("&Copy Names")); + CPoint pt; + ::GetCursorPos(&pt); + ::TrackPopupMenu(menu, TPM_LEFTALIGN | TPM_RIGHTBUTTON, pt.x, pt.y, 0, m_hWnd, NULL); + ::DestroyMenu(menu); + +} + + +void CViewComments::OnCopyNames() +{ + std::wstring names; + const CSoundFile &sndFile = GetDocument()->GetSoundFile(); + if(m_nListId == IDC_LIST_SAMPLES) + { + for(SAMPLEINDEX i = 1; i <= sndFile.GetNumSamples(); i++) + names += mpt::ToWide(sndFile.GetCharsetInternal(), sndFile.GetSampleName(i)) + L"\r\n"; + } else if(m_nListId == IDC_LIST_INSTRUMENTS) + { + for(INSTRUMENTINDEX i = 1; i <= sndFile.GetNumInstruments(); i++) + names += mpt::ToWide(sndFile.GetCharsetInternal(), sndFile.GetInstrumentName(i)) + L"\r\n"; + } + const size_t sizeBytes = (names.length() + 1) * sizeof(wchar_t); + Clipboard clipboard(CF_UNICODETEXT, sizeBytes); + if(auto dst = clipboard.Get(); dst.data()) + { + std::memcpy(dst.data(), names.c_str(), sizeBytes); + } +} + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/view_com.h b/Src/external_dependencies/openmpt-trunk/mptrack/view_com.h new file mode 100644 index 00000000..ebd1e604 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/view_com.h @@ -0,0 +1,67 @@ +/* + * view_com.h + * ---------- + * Purpose: Song comments tab, lower panel. + * Notes : (currently none) + * Authors: Olivier Lapicque + * OpenMPT Devs + * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. + */ + + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "CListCtrl.h" + +OPENMPT_NAMESPACE_BEGIN + +class CViewComments: public CModScrollView +{ +public: + CViewComments() = default; + DECLARE_SERIAL(CViewComments) + +protected: + CModControlBar m_ToolBar; + CListCtrlEx m_ItemList; + int m_nCurrentListId = 0, m_nListId = 0; + ModCommand::NOTE m_lastNote = NOTE_NONE; + CHANNELINDEX m_noteChannel = CHANNELINDEX_INVALID; + INSTRUMENTINDEX m_noteInstr = INSTRUMENTINDEX_INVALID; + +public: + void RecalcLayout(); + void UpdateButtonState(); + +public: + //{{AFX_VIRTUAL(CViewComments) + void OnInitialUpdate() override; + BOOL PreTranslateMessage(MSG *pMsg) override; + LRESULT OnModViewMsg(WPARAM wParam, LPARAM lParam) override; + void UpdateView(UpdateHint hint, CObject *pObject = nullptr) override; + //}}AFX_VIRTUAL + +protected: + bool SwitchToList(int list); + + //{{AFX_MSG(CViewGlobals) + afx_msg void OnDestroy(); + afx_msg void OnSize(UINT nType, int cx, int cy); + afx_msg void OnShowSamples() { SwitchToList(IDC_LIST_SAMPLES); } + afx_msg void OnShowInstruments() { SwitchToList(IDC_LIST_INSTRUMENTS); } + afx_msg void OnShowPatterns() { SwitchToList(IDC_LIST_PATTERNS); } + afx_msg void OnEndLabelEdit(LPNMHDR pnmhdr, LRESULT *pLResult); + afx_msg void OnBeginLabelEdit(LPNMHDR pnmhdr, LRESULT *pLResult); + afx_msg void OnDblClickListItem(NMHDR *, LRESULT *); + afx_msg void OnRClickListItem(NMHDR *, LRESULT *); + afx_msg void OnCopyNames(); + afx_msg LRESULT OnMidiMsg(WPARAM midiData, LPARAM); + afx_msg LRESULT OnCustomKeyMsg(WPARAM, LPARAM); + //}}AFX_MSG + DECLARE_MESSAGE_MAP() +}; + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.cpp new file mode 100644 index 00000000..ef3e7ed7 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.cpp @@ -0,0 +1,103 @@ + +#include "stdafx.h" + +#include "Native.h" +#include "NativeUtils.h" + +#include "../../common/ComponentManager.h" + +#if defined(_MSC_VER) + +#pragma comment(lib, "advapi32.lib") +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "ncrypt.lib") +#pragma comment(lib, "ole32.lib") +#pragma comment(lib, "rpcrt4.lib") +#pragma comment(lib, "shell32.lib") +#pragma comment(lib, "shlwapi.lib") + +#pragma comment(lib, "strmiids.lib") + +#if (_WIN32_WINNT >= 0x600) +#pragma comment(lib, "avrt.lib") +#endif +#if defined(MPT_WITH_DIRECTSOUND) +#pragma comment(lib, "dsound.lib") +#endif // MPT_WITH_DIRECTSOUND +#pragma comment(lib, "winmm.lib") + +#pragma comment(lib, "ksuser.lib") + +#if defined(_MSC_VER) && !defined(__clang__) +#pragma comment( linker, "\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df'\"" ) +#endif + +#endif + +OPENMPT_NAMESPACE_BEGIN + +#if defined(MPT_ASSERT_HANDLER_NEEDED) && defined(MPT_BUILD_WINESUPPORT) +MPT_NOINLINE void AssertHandler(const mpt::source_location &loc, const char *expr, const char *msg) +{ + if(msg) + { + mpt::log::GlobalLogger().SendLogMessage(loc, LogError, "ASSERT", + U_("ASSERTION FAILED: ") + mpt::ToUnicode(mpt::Charset::ASCII, msg) + U_(" (") + mpt::ToUnicode(mpt::Charset::ASCII, expr) + U_(")") + ); + } else + { + mpt::log::GlobalLogger().SendLogMessage(loc, LogError, "ASSERT", + U_("ASSERTION FAILED: ") + mpt::ToUnicode(mpt::Charset::ASCII, expr) + ); + } +} +#endif + +namespace Wine +{ + +class ComponentManagerSettings + : public IComponentManagerSettings +{ + virtual bool LoadOnStartup() const { return true; } // required to simplify object lifetimes + virtual bool KeepLoaded() const { return true; } // required to simplify object lifetimes + virtual bool IsBlocked(const std::string &key) const { MPT_UNREFERENCED_PARAMETER(key); return false; } + virtual mpt::PathString Path() const { return mpt::PathString(); } +}; + +static ComponentManagerSettings & ComponentManagerSettingsSingleton() +{ + static ComponentManagerSettings gs_Settings; + return gs_Settings; +} + +void Init() +{ + ComponentManager::Init(ComponentManagerSettingsSingleton()); + ComponentManager::Instance()->Startup(); +} + +void Fini() +{ + ComponentManager::Release(); +} + +} // namespace Wine + +OPENMPT_NAMESPACE_END + +extern "C" { + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_Init(void) +{ + OPENMPT_NAMESPACE::Wine::Init(); + return 0; +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_Fini(void) +{ + OPENMPT_NAMESPACE::Wine::Fini(); + return 0; +} + +} // extern "C" diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.h b/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.h new file mode 100644 index 00000000..d08419d2 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/Native.h @@ -0,0 +1,20 @@ + +#ifndef OPENMPT_WINE_H +#define OPENMPT_WINE_H + +#include "NativeConfig.h" +#include "NativeUtils.h" +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_Init(void); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_Fini(void); + +#ifdef __cplusplus +} +#endif + +#endif // OPENMPT_WINE_H diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeConfig.h b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeConfig.h new file mode 100644 index 00000000..3652a0da --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeConfig.h @@ -0,0 +1,162 @@ + +#ifndef OPENMPT_WNESUPPORT_CONFIG_H +#define OPENMPT_WNESUPPORT_CONFIG_H + +#include <stdint.h> + +#if defined(__DOXYGEN__) + +#define OPENMPT_API_HELPER_EXPORT +#define OPENMPT_API_HELPER_IMPORT +#define OPENMPT_API_HELPER_PUBLIC +#define OPENMPT_API_HELPER_LOCAL + +#elif defined(MPT_WINEGCC) + +#define OPENMPT_API_HELPER_EXPORT __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_IMPORT __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_PUBLIC __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_LOCAL __attribute__((visibility("hidden"))) + +#elif defined(_MSC_VER) + +#define OPENMPT_API_HELPER_EXPORT __declspec(dllexport) +#define OPENMPT_API_HELPER_IMPORT __declspec(dllimport) +#define OPENMPT_API_HELPER_PUBLIC +#define OPENMPT_API_HELPER_LOCAL + +#elif defined(__GNUC__) || defined(__clang__) + +#define OPENMPT_API_HELPER_EXPORT __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_IMPORT __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_PUBLIC __attribute__((visibility("default"))) +#define OPENMPT_API_HELPER_LOCAL __attribute__((visibility("hidden"))) + +#else + +#define OPENMPT_API_HELPER_EXPORT +#define OPENMPT_API_HELPER_IMPORT +#define OPENMPT_API_HELPER_PUBLIC +#define OPENMPT_API_HELPER_LOCAL + +#endif + +#if defined(__DOXYGEN__) + +#define OPENMPT_API_WINE_MS_CDECL +#define OPENMPT_API_WINE_MS_STDCALL +#define OPENMPT_API_WINE_MS_FASTCALL +#define OPENMPT_API_WINE_MS_THISCALL +#undef OPENMPT_API_WINE_SYSV + +#elif defined(MPT_WINEGCC) + +#ifdef _WIN64 +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) +#else +// winegcc on Ubuntu 16.04, wine-development 1.9.6 completely explodes in +// incomprehensible ways while parsing __attribute__((cdecl)). +#if defined(__cdecl) +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) __cdecl +#else +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) __attribute__((cdecl)) +#endif +#if defined(__stdcall) +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) __stdcall +#else +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) __attribute__((stdcall)) +#endif +#if defined(__fastcall) +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) __fastcall +#else +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) __attribute__((fastcall)) +#endif +#if defined(__thiscall) +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) __thiscall +#else +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) __attribute__((thiscall)) +#endif +#endif +#define OPENMPT_API_WINE_SYSV __attribute__((sysv_abi)) + +#elif defined(_MSC_VER) + +#define OPENMPT_API_WINE_MS_CDECL __cdecl +#define OPENMPT_API_WINE_MS_STDCALL __stdcall +#define OPENMPT_API_WINE_MS_FASTCALL __fastcall +#define OPENMPT_API_WINE_MS_THISCALL __thiscall +#undef OPENMPT_API_WINE_SYSV + +#elif defined(__GNUC__) || defined(__clang__) + +#ifdef _WIN64 +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) +#else +// winegcc on Ubuntu 16.04, wine-development 1.9.6 completely explodes in +// incomprehensible ways while parsing __attribute__((cdecl)). +#if defined(__cdecl) +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) __cdecl +#else +#define OPENMPT_API_WINE_MS_CDECL __attribute__((ms_abi)) __attribute__((cdecl)) +#endif +#if defined(__stdcall) +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) __stdcall +#else +#define OPENMPT_API_WINE_MS_STDCALL __attribute__((ms_abi)) __attribute__((stdcall)) +#endif +#if defined(__fastcall) +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) __fastcall +#else +#define OPENMPT_API_WINE_MS_FASTCALL __attribute__((ms_abi)) __attribute__((fastcall)) +#endif +#if defined(__thiscall) +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) __thiscall +#else +#define OPENMPT_API_WINE_MS_THISCALL __attribute__((ms_abi)) __attribute__((thiscall)) +#endif +#endif +#define OPENMPT_API_WINE_SYSV __attribute__((sysv_abi)) + +#endif + +#if defined(MODPLUG_TRACKER) && (!(defined(MPT_BUILD_WINESUPPORT) || defined(MPT_BUILD_WINESUPPORT_WRAPPER))) + +#define OPENMPT_WINESUPPORT_API +#define OPENMPT_WINESUPPORT_CALL +#define OPENMPT_WINESUPPORT_WRAPPER_API +#define OPENMPT_WINESUPPORT_WRAPPER_CALL + +#else + +#if defined(__DOXYGEN__) +#define OPENMPT_WINESUPPORT_CALL OPENMPT_API_WINE_SYSV +#elif defined(MPT_WINEGCC) +#define OPENMPT_WINESUPPORT_CALL OPENMPT_API_WINE_SYSV +#elif defined(_MSC_VER) +#define OPENMPT_WINESUPPORT_CALL OPENMPT_API_WINE_MS_CDECL +#elif defined(__GNUC__) || defined(__clang__) +#define OPENMPT_WINESUPPORT_CALL OPENMPT_API_WINE_SYSV +#endif +#define OPENMPT_WINESUPPORT_WRAPPER_CALL OPENMPT_API_WINE_MS_CDECL + +#if defined(MPT_BUILD_WINESUPPORT) +#define OPENMPT_WINESUPPORT_API OPENMPT_API_HELPER_EXPORT +#else +#define OPENMPT_WINESUPPORT_API OPENMPT_API_HELPER_IMPORT +#endif + +#if defined(MPT_BUILD_WINESUPPORT_WRAPPER) +#define OPENMPT_WINESUPPORT_WRAPPER_API OPENMPT_API_HELPER_EXPORT +#else +#define OPENMPT_WINESUPPORT_WRAPPER_API OPENMPT_API_HELPER_IMPORT +#endif + +#endif + +#endif // OPENMPT_WNESUPPORT_CONFIG_H diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.cpp new file mode 100644 index 00000000..ed6710a9 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.cpp @@ -0,0 +1,525 @@ + +#include "stdafx.h" + +#if MPT_COMPILER_MSVC +#pragma warning(disable:4800) // 'T' : forcing value to bool 'true' or 'false' (performance warning) +#endif // MPT_COMPILER_MSVC + +#include "NativeSoundDevice.h" +#include "NativeUtils.h" + +#include "openmpt/sounddevice/SoundDevice.hpp" +#include "openmpt/sounddevice/SoundDeviceManager.hpp" +#include "openmpt/sounddevice/SoundDeviceUtilities.hpp" + +#include "../../common/ComponentManager.h" + +#include "../../misc/mptOS.h" + +#include "NativeSoundDeviceMarshalling.h" + +#include <string> +#include <type_traits> + +#include <cstdint> +#include <cstdlib> +#include <cstring> + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +OPENMPT_NAMESPACE_BEGIN + +namespace C { + +class ComponentSoundDeviceManager + : public ComponentBuiltin +{ + MPT_DECLARE_COMPONENT_MEMBERS(ComponentSoundDeviceManager, "SoundDeviceManager") +private: + mpt::log::GlobalLogger logger; + SoundDevice::Manager manager; +private: + static SoundDevice::SysInfo GetSysInfo() + { + mpt::OS::Wine::VersionContext wineVersionContext; + return SoundDevice::SysInfo(mpt::osinfo::get_class(), mpt::OS::Windows::Version::Current(), mpt::OS::Windows::IsWine(), wineVersionContext.HostClass(), wineVersionContext.Version()); + } + +public: + ComponentSoundDeviceManager() + : manager(logger, GetSysInfo(), SoundDevice::AppInfo()) + { + return; + } + virtual ~ComponentSoundDeviceManager() { } + bool DoInitialize() override + { + return true; + } + SoundDevice::Manager & get() const + { + return const_cast<SoundDevice::Manager &>(manager); + } +}; + +static mpt::ustring GetTypePrefix() +{ + return U_("Native"); +} + +static SoundDevice::Info AddTypePrefix(SoundDevice::Info info) +{ + info.type = GetTypePrefix() + U_("-") + info.type; + info.apiPath.insert(info.apiPath.begin(), U_("Native")); + return info; +} + +static SoundDevice::Info RemoveTypePrefix(SoundDevice::Info info) +{ + info.type = info.type.substr(GetTypePrefix().length() + 1); + info.apiPath.erase(info.apiPath.begin()); + return info; +} + +std::string SoundDevice_EnumerateDevices() +{ + ComponentHandle<ComponentSoundDeviceManager> manager; + if(!IsComponentAvailable(manager)) + { + return std::string(); + } + std::vector<SoundDevice::Info> infos = std::vector<SoundDevice::Info>(manager->get().begin(), manager->get().end()); + for(auto &info : infos) + { + info = AddTypePrefix(info); + } + return json_cast<std::string>(infos); +} + +SoundDevice::IBase * SoundDevice_Construct(std::string info_) +{ + MPT_LOG_GLOBAL(LogDebug, "NativeSupport", MPT_UFORMAT("Contruct: {}")(mpt::ToUnicode(mpt::Charset::UTF8, info_))); + ComponentHandle<ComponentSoundDeviceManager> manager; + if(!IsComponentAvailable(manager)) + { + return nullptr; + } + SoundDevice::Info info = json_cast<SoundDevice::Info>(info_); + info = RemoveTypePrefix(info); + return manager->get().CreateSoundDevice(info.GetIdentifier()); +} + +class NativeMessageReceiverProxy + : public SoundDevice::IMessageReceiver +{ +private: + OpenMPT_SoundDevice_IMessageReceiver impl; +public: + NativeMessageReceiverProxy(const OpenMPT_SoundDevice_IMessageReceiver * impl_) + { + MemsetZero(impl); + if(impl_) + { + impl = *impl_; + } + } + virtual ~NativeMessageReceiverProxy() + { + return; + } +public: + virtual void SoundDeviceMessage(LogLevel level, const mpt::ustring &str) + { + if(!impl.SoundDeviceMessageFunc) + { + return; + } + return impl.SoundDeviceMessageFunc(impl.inst, level, mpt::ToCharset(mpt::Charset::UTF8, str).c_str()); + } +}; + +class NativeCallbackProxy + : public SoundDevice::ICallback +{ +private: + OpenMPT_SoundDevice_ICallback impl; +public: + NativeCallbackProxy(const OpenMPT_SoundDevice_ICallback * impl_) + { + MemsetZero(impl); + if(impl_) + { + impl = *impl_; + } + } + virtual ~NativeCallbackProxy() + { + return; + } +public: + // main thread + virtual uint64 SoundCallbackGetReferenceClockNowNanoseconds() const + { + if(!impl.SoundCallbackGetReferenceClockNowNanosecondsFunc) + { + return 0; + } + uint64_t result = 0; + impl.SoundCallbackGetReferenceClockNowNanosecondsFunc(impl.inst, &result); + return result; + } + virtual void SoundCallbackPreStart() + { + if(!impl.SoundCallbackPreStartFunc) + { + return; + } + return impl.SoundCallbackPreStartFunc(impl.inst); + } + virtual void SoundCallbackPostStop() + { + if(!impl.SoundCallbackPostStopFunc) + { + return; + } + return impl.SoundCallbackPostStopFunc(impl.inst); + } + virtual bool SoundCallbackIsLockedByCurrentThread() const + { + if(!impl.SoundCallbackIsLockedByCurrentThreadFunc) + { + return 0; + } + uintptr_t result = 0; + impl.SoundCallbackIsLockedByCurrentThreadFunc(impl.inst, &result); + return result; + } + // audio thread + virtual void SoundCallbackLock() + { + if(!impl.SoundCallbackLockFunc) + { + return; + } + return impl.SoundCallbackLockFunc(impl.inst); + } + virtual uint64 SoundCallbackLockedGetReferenceClockNowNanoseconds() const + { + if(!impl.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc) + { + return 0; + } + uint64_t result = 0; + impl.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc(impl.inst, &result); + return result; + } + virtual void SoundCallbackLockedProcessPrepare(SoundDevice::TimeInfo timeInfo) + { + if(!impl.SoundCallbackLockedProcessPrepareFunc) + { + return; + } + OpenMPT_SoundDevice_TimeInfo c_timeInfo = C::encode(timeInfo); + return impl.SoundCallbackLockedProcessPrepareFunc(impl.inst, &c_timeInfo); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, uint8 *buffer, const uint8 *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessUint8Func) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessUint8Func(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, int8 *buffer, const int8 *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessInt8Func) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessInt8Func(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, int16 *buffer, const int16 *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessInt16Func) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessInt16Func(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, int24 *buffer, const int24 *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessInt24Func) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessInt24Func(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, int32 *buffer, const int32 *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessInt32Func) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessInt32Func(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, float *buffer, const float *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessFloatFunc) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessFloatFunc(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcess(SoundDevice::BufferFormat bufferFormat, std::size_t numFrames, double *buffer, const double *inputBuffer) + { + if(!impl.SoundCallbackLockedProcessDoubleFunc) + { + return; + } + OpenMPT_SoundDevice_BufferFormat c_bufferFormat = C::encode(bufferFormat); + return impl.SoundCallbackLockedProcessDoubleFunc(impl.inst, &c_bufferFormat, numFrames, buffer, inputBuffer); + } + virtual void SoundCallbackLockedProcessDone(SoundDevice::TimeInfo timeInfo) + { + if(!impl.SoundCallbackLockedProcessDoneFunc) + { + return; + } + OpenMPT_SoundDevice_TimeInfo c_timeInfo = C::encode(timeInfo); + return impl.SoundCallbackLockedProcessDoneFunc(impl.inst, &c_timeInfo); + } + virtual void SoundCallbackUnlock() + { + if(!impl.SoundCallbackUnlockFunc) + { + return; + } + return impl.SoundCallbackUnlockFunc(impl.inst); + } +}; + +} // namespace C + +OPENMPT_NAMESPACE_END + +extern "C" { + +struct OpenMPT_SoundDevice { + OPENMPT_NAMESPACE::SoundDevice::IBase * impl; + OPENMPT_NAMESPACE::C::NativeMessageReceiverProxy * messageReceiver; + OPENMPT_NAMESPACE::C::NativeCallbackProxy * callback; +}; + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_EnumerateDevices() { + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::C::SoundDevice_EnumerateDevices().c_str() ); +} + +OPENMPT_WINESUPPORT_API OpenMPT_SoundDevice * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Construct( const char * info ) { + if ( !info ) { + return nullptr; + } + OpenMPT_SoundDevice * result = reinterpret_cast< OpenMPT_SoundDevice * >( OpenMPT_Alloc( sizeof( OpenMPT_SoundDevice ) ) ); + if ( !result ) { + return nullptr; + } + result->impl = OPENMPT_NAMESPACE::C::SoundDevice_Construct( info ); + if ( !result->impl ) { + OpenMPT_Free( result ); + return nullptr; + } + return result; +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Destruct( OpenMPT_SoundDevice * sd ) { + if ( sd ) { + if ( sd->impl ) { + sd->impl->SetMessageReceiver( nullptr ); + delete sd->messageReceiver; + sd->messageReceiver = nullptr; + delete sd->impl; + sd->impl = nullptr; + } + OpenMPT_Free( sd ); + } +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_SetMessageReceiver( OpenMPT_SoundDevice * sd, const OpenMPT_SoundDevice_IMessageReceiver * receiver ) { + if ( !sd ) { + return; + } + if ( !sd->impl ) { + return; + } + sd->impl->SetMessageReceiver( nullptr ); + delete sd->messageReceiver; + sd->messageReceiver = nullptr; + sd->messageReceiver = new OPENMPT_NAMESPACE::C::NativeMessageReceiverProxy( receiver ); + sd->impl->SetMessageReceiver( sd->messageReceiver ); + return; +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_SetCallback( OpenMPT_SoundDevice * sd, const OpenMPT_SoundDevice_ICallback * callback ) { + if ( !sd ) { + return; + } + if ( !sd->impl ) { + return; + } + sd->impl->SetCallback( nullptr ); + delete sd->callback; + sd->callback = nullptr; + sd->callback = new OPENMPT_NAMESPACE::C::NativeCallbackProxy( callback ); + sd->impl->SetCallback( sd->callback ); + return; +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceInfo( const OpenMPT_SoundDevice * sd ) { + OPENMPT_NAMESPACE::SoundDevice::Info info = sd->impl->GetDeviceInfo(); + info = OPENMPT_NAMESPACE::C::AddTypePrefix(info); + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::json_cast<std::string>( sd->impl->GetDeviceInfo() ).c_str() ); +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceCaps( const OpenMPT_SoundDevice * sd ) { + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::json_cast<std::string>( sd->impl->GetDeviceCaps() ).c_str() ); +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceDynamicCaps( OpenMPT_SoundDevice * sd, const char * baseSampleRates ) { + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::json_cast<std::string>( sd->impl->GetDeviceDynamicCaps( OPENMPT_NAMESPACE::json_cast<std::vector<OPENMPT_NAMESPACE::uint32> >( std::string(baseSampleRates) ) ) ).c_str() ); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Init( OpenMPT_SoundDevice * sd, const char * appInfo ) { + return sd->impl->Init( OPENMPT_NAMESPACE::json_cast<OPENMPT_NAMESPACE::SoundDevice::AppInfo>( std::string(appInfo) ) ); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Open( OpenMPT_SoundDevice * sd, const char * settings ) { + return sd->impl->Open( OPENMPT_NAMESPACE::json_cast<OPENMPT_NAMESPACE::SoundDevice::Settings>( std::string(settings) ) ); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Close( OpenMPT_SoundDevice * sd ) { + return sd->impl->Close(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Start( OpenMPT_SoundDevice * sd ) { + return sd->impl->Start(); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Stop( OpenMPT_SoundDevice * sd ) { + return sd->impl->Stop(); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetRequestFlags( const OpenMPT_SoundDevice * sd, uint32_t * result) { + *result = sd->impl->GetRequestFlags().GetRaw(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsInited( const OpenMPT_SoundDevice * sd ) { + return sd->impl->IsInited(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsOpen( const OpenMPT_SoundDevice * sd ) { + return sd->impl->IsOpen(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsAvailable( const OpenMPT_SoundDevice * sd ) { + return sd->impl->IsAvailable(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsPlaying( const OpenMPT_SoundDevice * sd ) { + return sd->impl->IsPlaying(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsPlayingSilence( const OpenMPT_SoundDevice * sd ) { + return sd->impl->IsPlayingSilence(); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_StopAndAvoidPlayingSilence( OpenMPT_SoundDevice * sd ) { + return sd->impl->StopAndAvoidPlayingSilence(); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_EndPlayingSilence( OpenMPT_SoundDevice * sd ) { + return sd->impl->EndPlayingSilence(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_OnIdle( OpenMPT_SoundDevice * sd ) { + return sd->impl->OnIdle(); +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetSettings( const OpenMPT_SoundDevice * sd ) { + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::json_cast<std::string>( sd->impl->GetSettings() ).c_str() ); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetActualSampleFormat( const OpenMPT_SoundDevice * sd, int32_t * result ) { + *result = OPENMPT_NAMESPACE::mpt::to_underlying<OPENMPT_NAMESPACE::SampleFormat::Enum>(sd->impl->GetActualSampleFormat()); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetEffectiveBufferAttributes( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_BufferAttributes * result ) { + *result = OPENMPT_NAMESPACE::C::encode( sd->impl->GetEffectiveBufferAttributes() ); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetTimeInfo( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_TimeInfo * result ) { + *result = OPENMPT_NAMESPACE::C::encode( sd->impl->GetTimeInfo() ); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetStreamPosition( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_StreamPosition * result ) { + *result = OPENMPT_NAMESPACE::C::encode( sd->impl->GetStreamPosition() ); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_DebugIsFragileDevice( const OpenMPT_SoundDevice * sd ) { + return sd->impl->DebugIsFragileDevice(); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_DebugInRealtimeCallback( const OpenMPT_SoundDevice * sd ) { + return sd->impl->DebugInRealtimeCallback(); +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetStatistics( const OpenMPT_SoundDevice * sd ) { + return OpenMPT_String_Duplicate( OPENMPT_NAMESPACE::json_cast<std::string>( sd->impl->GetStatistics() ).c_str() ); +} + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_OpenDriverSettings( OpenMPT_SoundDevice * sd ) { + return sd->impl->OpenDriverSettings(); +} + +typedef struct OpenMPT_PriorityBooster { +#ifndef _MSC_VER + OPENMPT_NAMESPACE::SoundDevice::ThreadPriorityGuard * impl; +#else + void * dummy; +#endif +} OpenMPT_PriorityBooster; + +OPENMPT_WINESUPPORT_API OpenMPT_PriorityBooster * OPENMPT_WINESUPPORT_CALL OpenMPT_PriorityBooster_Construct_From_SoundDevice( const OpenMPT_SoundDevice * sd ) { +#if !MPT_OS_WINDOWS + OpenMPT_PriorityBooster * pb = (OpenMPT_PriorityBooster*)OpenMPT_Alloc( sizeof( OpenMPT_PriorityBooster ) ); + pb->impl = new OPENMPT_NAMESPACE::SoundDevice::ThreadPriorityGuard + ( dynamic_cast<OPENMPT_NAMESPACE::SoundDevice::Base*>(sd->impl)->GetLogger() + , dynamic_cast<OPENMPT_NAMESPACE::SoundDevice::Base*>(sd->impl)->GetSettings().BoostThreadPriority + , dynamic_cast<OPENMPT_NAMESPACE::SoundDevice::Base*>(sd->impl)->GetAppInfo().BoostedThreadRealtimePosix + , dynamic_cast<OPENMPT_NAMESPACE::SoundDevice::Base*>(sd->impl)->GetAppInfo().BoostedThreadNicenessPosix + , dynamic_cast<OPENMPT_NAMESPACE::SoundDevice::Base*>(sd->impl)->GetAppInfo().BoostedThreadRtprioPosix + ); + return pb; +#else + MPT_UNREFERENCED_PARAMETER(sd); + return nullptr; +#endif +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_PriorityBooster_Destruct( OpenMPT_PriorityBooster * pb ) { +#if !MPT_OS_WINDOWS + delete pb->impl; + pb->impl = nullptr; + OpenMPT_Free( pb ); +#else + MPT_UNREFERENCED_PARAMETER(pb); +#endif +} + +} // extern "C" diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.h b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.h new file mode 100644 index 00000000..932fcd69 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDevice.h @@ -0,0 +1,145 @@ + +#ifndef OPENMPT_WINE_SOUNDDEVICE_H +#define OPENMPT_WINE_SOUNDDEVICE_H + +#include "NativeConfig.h" +#include "NativeUtils.h" +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_EnumerateDevices(); + +typedef void OpenMPT_int24; + +typedef struct OpenMPT_SoundDevice_StreamPosition { + int64_t Frames; + double Seconds; +} OpenMPT_SoundDevice_StreamPosition; + +typedef struct OpenMPT_SoundDevice_TimeInfo { + int64_t SyncPointStreamFrames; + uint64_t SyncPointSystemTimestamp; + double Speed; + OpenMPT_SoundDevice_StreamPosition RenderStreamPositionBefore; + OpenMPT_SoundDevice_StreamPosition RenderStreamPositionAfter; + double Latency; +} OpenMPT_SoundDevice_TimeInfo; + +typedef struct OpenMPT_SoundDevice_Flags { + uint8_t WantsClippedOutput; + uint8_t pad1; + uint16_t pad2; + uint32_t pad3; +} OpenMPT_SoundDevice_Flags; + +typedef struct OpenMPT_SoundDevice_BufferFormat { + uint32_t Samplerate; + uint32_t Channels; + uint8_t InputChannels; + uint8_t pad1; + uint16_t pad2; + int32_t sampleFormat; + uint8_t WantsClippedOutput; + uint8_t pad3; + uint16_t pad4; + int32_t DitherType; +} OpenMPT_SoundDevice_BufferFormat; + +typedef struct OpenMPT_SoundDevice_BufferAttributes { + double Latency; + double UpdateInterval; + int32_t NumBuffers; + uint32_t pad1; +} OpenMPT_SoundDevice_BufferAttributes; + +typedef struct OpenMPT_SoundDevice_RequestFlags { + uint32_t RequestFlags; + uint32_t pad1; +} OpenMPT_SoundDevice_RequestFlags; + +typedef struct OpenMPT_SoundDevice OpenMPT_SoundDevice; + +typedef struct OpenMPT_SoundDevice_IMessageReceiver { + void * inst; + void (OPENMPT_WINESUPPORT_CALL * SoundDeviceMessageFunc)( void * inst, uintptr_t level, const char * message ); +} OpenMPT_SoundDevice_IMessageReceiver; + +typedef struct OpenMPT_SoundDevice_ICallback { + void * inst; + // main thread + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackPreStartFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackPostStopFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackIsLockedByCurrentThreadFunc)( void * inst, uintptr_t * result ); + // audio thread + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessPrepareFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessUint8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, uint8_t * buffer, const uint8_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessInt8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int8_t * buffer, const int8_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessInt16Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int16_t * buffer, const int16_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessInt24Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, OpenMPT_int24 * buffer, const OpenMPT_int24 * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessInt32Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int32_t * buffer, const int32_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessFloatFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, float * buffer, const float * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessDoubleFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, double * buffer, const double * inputBuffer ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackLockedProcessDoneFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (OPENMPT_WINESUPPORT_CALL * SoundCallbackUnlockFunc)( void * inst ); +} OpenMPT_SoundDevice_ICallback; + +OPENMPT_WINESUPPORT_API OpenMPT_SoundDevice * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Construct( const char * info ); + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Destruct( OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_SetMessageReceiver( OpenMPT_SoundDevice * sd, const OpenMPT_SoundDevice_IMessageReceiver * receiver ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_SetCallback( OpenMPT_SoundDevice * sd, const OpenMPT_SoundDevice_ICallback * callback ); + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceInfo( const OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceCaps( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetDeviceDynamicCaps( OpenMPT_SoundDevice * sd, const char * baseSampleRates ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Init( OpenMPT_SoundDevice * sd, const char * appInfo ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Open( OpenMPT_SoundDevice * sd, const char * settings ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Close( OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Start( OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_Stop( OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetRequestFlags( const OpenMPT_SoundDevice * sd, uint32_t * result ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsInited( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsOpen( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsAvailable( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsPlaying( const OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_IsPlayingSilence( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_StopAndAvoidPlayingSilence( OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_EndPlayingSilence( OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_OnIdle( OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetSettings( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetActualSampleFormat( const OpenMPT_SoundDevice * sd, int32_t * result ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetEffectiveBufferAttributes( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_BufferAttributes * result ); + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetTimeInfo( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_TimeInfo * result ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetStreamPosition( const OpenMPT_SoundDevice * sd, OpenMPT_SoundDevice_StreamPosition * result ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_DebugIsFragileDevice( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_DebugInRealtimeCallback( const OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_GetStatistics( const OpenMPT_SoundDevice * sd ); + +OPENMPT_WINESUPPORT_API uintptr_t OPENMPT_WINESUPPORT_CALL OpenMPT_SoundDevice_OpenDriverSettings( OpenMPT_SoundDevice * sd ); + +typedef struct OpenMPT_PriorityBooster OpenMPT_PriorityBooster; +OPENMPT_WINESUPPORT_API OpenMPT_PriorityBooster * OPENMPT_WINESUPPORT_CALL OpenMPT_PriorityBooster_Construct_From_SoundDevice( const OpenMPT_SoundDevice * sd ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_PriorityBooster_Destruct( OpenMPT_PriorityBooster * pb ); + +#ifdef __cplusplus +} +#endif + +#endif // OPENMPT_WINE_SOUNDDEVICE_H diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDeviceMarshalling.h b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDeviceMarshalling.h new file mode 100644 index 00000000..95b2ae99 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeSoundDeviceMarshalling.h @@ -0,0 +1,326 @@ + +#pragma once + +#include "openmpt/all/BuildSettings.hpp" + +#include "NativeSoundDevice.h" + +#include "openmpt/sounddevice/SoundDevice.hpp" + +#ifdef MPT_WITH_NLOHMANNJSON + +// https://github.com/nlohmann/json/issues/1204 +#if MPT_COMPILER_MSVC +#pragma warning(push) +#pragma warning(disable:4127) // conditional expression is constant +#endif // MPT_COMPILER_MSVC +#include "mpt/json/json.hpp" +#if MPT_COMPILER_MSVC +#pragma warning(pop) +#endif // MPT_COMPILER_MSVC + +#endif // MPT_WITH_NLOHMANNJSON + + + +OPENMPT_NAMESPACE_BEGIN + + + +#ifdef MPT_WITH_NLOHMANNJSON + +inline void to_json(nlohmann::json &j, const SampleFormat &val) +{ + j = SampleFormat::ToInt(val); +} +inline void from_json(const nlohmann::json &j, SampleFormat &val) +{ + val = SampleFormat::FromInt(j); +} + +namespace SoundDevice +{ + + inline void to_json(nlohmann::json &j, const SoundDevice::ChannelMapping &val) + { + j = val.ToUString(); + } + inline void from_json(const nlohmann::json &j, SoundDevice::ChannelMapping &val) + { + val = SoundDevice::ChannelMapping::FromString(j); + } + + inline void to_json(nlohmann::json &j, const SoundDevice::Info::Default &val) + { + j = static_cast<int>(val); + } + inline void from_json(const nlohmann::json &j, SoundDevice::Info::Default &val) + { + val = static_cast<SoundDevice::Info::Default>(static_cast<int>(j)); + } +} // namespace SoundDevice + + + +namespace SoundDevice +{ + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Info::ManagerFlags + ,defaultFor + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Info::Flags + ,usability + ,level + ,compatible + ,api + ,io + ,mixing + ,implementor + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Info + ,type + ,internalID + ,name + ,apiName + ,apiPath + ,default_ + ,useNameAsIdentifier + ,managerFlags + ,flags + ,extraData + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::AppInfo + ,Name + ,BoostedThreadPriorityXP + ,BoostedThreadMMCSSClassVista + ,BoostedThreadRealtimePosix + ,BoostedThreadNicenessPosix + ,BoostedThreadRtprioPosix + ,MaskDriverCrashes + ,AllowDeferredProcessing + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Settings + ,Latency + ,UpdateInterval + ,Samplerate + ,Channels + ,InputChannels + ,sampleFormat + ,ExclusiveMode + ,BoostThreadPriority + ,KeepDeviceRunning + ,UseHardwareTiming + ,DitherType + ,InputSourceID + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Caps + ,Available + ,CanUpdateInterval + ,CanSampleFormat + ,CanExclusiveMode + ,CanBoostThreadPriority + ,CanKeepDeviceRunning + ,CanUseHardwareTiming + ,CanChannelMapping + ,CanInput + ,HasNamedInputSources + ,CanDriverPanel + ,HasInternalDither + ,ExclusiveModeDescription + ,LatencyMin + ,LatencyMax + ,UpdateIntervalMin + ,UpdateIntervalMax + ,DefaultSettings + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::DynamicCaps + ,currentSampleRate + ,supportedSampleRates + ,supportedExclusiveSampleRates + ,supportedSampleFormats + ,supportedExclusiveModeSampleFormats + ,channelNames + ,inputSourceNames + ) + + NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(SoundDevice::Statistics + ,InstantaneousLatency + ,LastUpdateInterval + ,text + ) + +} // namespace SoundDevice + + + +template <typename Tdst, typename Tsrc> +struct json_cast_impl +{ + Tdst operator () (const Tsrc &src); +}; + + +template <typename Tdst, typename Tsrc> +Tdst json_cast(const Tsrc &src) +{ + return json_cast_impl<Tdst, Tsrc>()(src); +} + + +template <typename Tsrc> +struct json_cast_impl<nlohmann::json, Tsrc> +{ + nlohmann::json operator () (const Tsrc &src) + { + return static_cast<nlohmann::json>(src); + } +}; + +template <typename Tdst> +struct json_cast_impl<Tdst, nlohmann::json> +{ + Tdst operator () (const nlohmann::json &src) + { + return src.get<Tdst>(); + } +}; + +template <typename Tsrc> +struct json_cast_impl<std::string, Tsrc> +{ + std::string operator () (const Tsrc &src) + { + return json_cast<nlohmann::json>(src).dump(4); + } +}; + +template <typename Tdst> +struct json_cast_impl<Tdst, std::string> +{ + Tdst operator () (const std::string &str) + { + return json_cast<Tdst>(nlohmann::json::parse(str)); + } +}; + +#endif // MPT_WITH_NLOHMANNJSON + + + +namespace C { + +static_assert(sizeof(OpenMPT_SoundDevice_StreamPosition) % 8 == 0); +inline OpenMPT_SoundDevice_StreamPosition encode(SoundDevice::StreamPosition src) { + OpenMPT_SoundDevice_StreamPosition dst; + MemsetZero(dst); + dst.Frames = src.Frames; + dst.Seconds = src.Seconds; + return dst; +} +inline SoundDevice::StreamPosition decode(OpenMPT_SoundDevice_StreamPosition src) { + SoundDevice::StreamPosition dst; + dst.Frames = src.Frames; + dst.Seconds = src.Seconds; + return dst; +} + +static_assert(sizeof(OpenMPT_SoundDevice_TimeInfo) % 8 == 0); +inline OpenMPT_SoundDevice_TimeInfo encode(SoundDevice::TimeInfo src) { + OpenMPT_SoundDevice_TimeInfo dst; + MemsetZero(dst); + dst.SyncPointStreamFrames = src.SyncPointStreamFrames; + dst.SyncPointSystemTimestamp = src.SyncPointSystemTimestamp; + dst.Speed = src.Speed; + dst.RenderStreamPositionBefore = encode(src.RenderStreamPositionBefore); + dst.RenderStreamPositionAfter = encode(src.RenderStreamPositionAfter); + dst.Latency = src.Latency; + return dst; +} +inline SoundDevice::TimeInfo decode(OpenMPT_SoundDevice_TimeInfo src) { + SoundDevice::TimeInfo dst; + dst.SyncPointStreamFrames = src.SyncPointStreamFrames; + dst.SyncPointSystemTimestamp = src.SyncPointSystemTimestamp; + dst.Speed = src.Speed; + dst.RenderStreamPositionBefore = decode(src.RenderStreamPositionBefore); + dst.RenderStreamPositionAfter = decode(src.RenderStreamPositionAfter); + dst.Latency = src.Latency; + return dst; +} + +static_assert(sizeof(OpenMPT_SoundDevice_Flags) % 8 == 0); +inline OpenMPT_SoundDevice_Flags encode(SoundDevice::Flags src) { + OpenMPT_SoundDevice_Flags dst; + MemsetZero(dst); + dst.WantsClippedOutput = src.WantsClippedOutput; + return dst; +} +inline SoundDevice::Flags decode(OpenMPT_SoundDevice_Flags src) { + SoundDevice::Flags dst; + dst.WantsClippedOutput = src.WantsClippedOutput; + return dst; +} + +static_assert(sizeof(OpenMPT_SoundDevice_BufferFormat) % 8 == 0); +inline OpenMPT_SoundDevice_BufferFormat encode(SoundDevice::BufferFormat src) { + OpenMPT_SoundDevice_BufferFormat dst; + MemsetZero(dst); + dst.Samplerate = src.Samplerate; + dst.Channels = src.Channels; + dst.InputChannels = src.InputChannels; + dst.sampleFormat = SampleFormat::ToInt(src.sampleFormat); + dst.WantsClippedOutput = src.WantsClippedOutput; + dst.DitherType = src.DitherType; + return dst; +} +inline SoundDevice::BufferFormat decode(OpenMPT_SoundDevice_BufferFormat src) { + SoundDevice::BufferFormat dst; + dst.Samplerate = src.Samplerate; + dst.Channels = src.Channels; + dst.InputChannels = src.InputChannels; + dst.sampleFormat = SampleFormat::FromInt(src.sampleFormat); + dst.WantsClippedOutput = src.WantsClippedOutput; + dst.DitherType = src.DitherType; + return dst; +} + +static_assert(sizeof(OpenMPT_SoundDevice_BufferAttributes) % 8 == 0); +inline OpenMPT_SoundDevice_BufferAttributes encode(SoundDevice::BufferAttributes src) { + OpenMPT_SoundDevice_BufferAttributes dst; + MemsetZero(dst); + dst.Latency = src.Latency; + dst.UpdateInterval = src.UpdateInterval; + dst.NumBuffers = src.NumBuffers; + return dst; +} +inline SoundDevice::BufferAttributes decode(OpenMPT_SoundDevice_BufferAttributes src) { + SoundDevice::BufferAttributes dst; + dst.Latency = src.Latency; + dst.UpdateInterval = src.UpdateInterval; + dst.NumBuffers = src.NumBuffers; + return dst; +} + +static_assert(sizeof(OpenMPT_SoundDevice_RequestFlags) % 8 == 0); +inline OpenMPT_SoundDevice_RequestFlags encode(FlagSet<SoundDevice::RequestFlags> src) { + OpenMPT_SoundDevice_RequestFlags dst; + MemsetZero(dst); + dst.RequestFlags = src.GetRaw(); + return dst; +} +inline FlagSet<SoundDevice::RequestFlags> decode(OpenMPT_SoundDevice_RequestFlags src) { + FlagSet<SoundDevice::RequestFlags> dst; + dst.SetRaw(src.RequestFlags); + return dst; +} + +} // namespace C + + + +OPENMPT_NAMESPACE_END diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.cpp new file mode 100644 index 00000000..d4a5805e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.cpp @@ -0,0 +1,151 @@ + +#include "stdafx.h" + +#include "NativeUtils.h" + +#include <string> +#ifndef _MSC_VER +#include <condition_variable> +#include <mutex> +#include <thread> +#endif + +#include <cstdint> +#include <cstdlib> +#include <cstring> + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +OPENMPT_NAMESPACE_BEGIN + +#ifndef _MSC_VER + +namespace mpt { + +class semaphore { +private: + unsigned int count; + unsigned int waiters_count; + std::mutex mutex; + std::condition_variable count_nonzero; +public: + semaphore( unsigned int initial_count = 0 ) + : count(initial_count) + , waiters_count(0) + { + return; + } + ~semaphore() { + return; + } + void wait() { + std::unique_lock<std::mutex> l(mutex); + waiters_count++; + while ( count == 0 ) { + count_nonzero.wait( l ); + } + waiters_count--; + count--; + } + void post() { + std::unique_lock<std::mutex> l(mutex); + if ( waiters_count > 0 ) { + count_nonzero.notify_one(); + } + count++; + } + void lock() { + wait(); + } + void unlock() { + post(); + } +}; + +} // namespace mpt + +#endif + +OPENMPT_NAMESPACE_END + +extern "C" { + +OPENMPT_WINESUPPORT_API void * OPENMPT_WINESUPPORT_CALL OpenMPT_Alloc( size_t size ) { + return calloc( 1, size ); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Free( void * mem ) { + if ( mem == nullptr ) { + return; + } + free( mem ); + return; +} + +OPENMPT_WINESUPPORT_API size_t OPENMPT_WINESUPPORT_CALL OpenMPT_String_Length( const char * str ) { + size_t len = 0; + if ( !str ) { + return len; + } + len = strlen( str ); + return len; +} + +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_String_Duplicate( const char * src ) { + if ( !src ) { + return strdup( "" ); + } + return strdup( src ); +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_String_Free( char * str ) { + OpenMPT_Free( str ); +} + +typedef struct OpenMPT_Semaphore { +#ifndef _MSC_VER + OPENMPT_NAMESPACE::mpt::semaphore * impl; +#else + void * dummy; +#endif +} OpenMPT_Semaphore; + +OPENMPT_WINESUPPORT_API OpenMPT_Semaphore * OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Construct(void) { +#ifndef _MSC_VER + OpenMPT_Semaphore * sem = (OpenMPT_Semaphore*)OpenMPT_Alloc( sizeof( OpenMPT_Semaphore ) ); + sem->impl = new OPENMPT_NAMESPACE::mpt::semaphore(0); + return sem; +#else + return nullptr; +#endif +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Destruct( OpenMPT_Semaphore * sem ) { +#ifndef _MSC_VER + delete sem->impl; + sem->impl = nullptr; + OpenMPT_Free( sem ); +#else + MPT_UNREFERENCED_PARAMETER(sem); +#endif +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Wait( OpenMPT_Semaphore * sem ) { +#ifndef _MSC_VER + sem->impl->wait(); +#else + MPT_UNREFERENCED_PARAMETER(sem); +#endif +} + +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Post( OpenMPT_Semaphore * sem ) { +#ifndef _MSC_VER + sem->impl->post(); +#else + MPT_UNREFERENCED_PARAMETER(sem); +#endif +} + +} // extern "C" diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.h b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.h new file mode 100644 index 00000000..0af4f272 --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/NativeUtils.h @@ -0,0 +1,30 @@ + +#ifndef OPENMPT_WINE_UTILS_H +#define OPENMPT_WINE_UTILS_H + +#include "NativeConfig.h" + +#include <stdint.h> + +#ifdef __cplusplus +extern "C" { +#endif + +OPENMPT_WINESUPPORT_API void * OPENMPT_WINESUPPORT_CALL OpenMPT_Alloc( size_t size ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Free( void * mem ); + +OPENMPT_WINESUPPORT_API size_t OPENMPT_WINESUPPORT_CALL OpenMPT_String_Length( const char * str ); +OPENMPT_WINESUPPORT_API char * OPENMPT_WINESUPPORT_CALL OpenMPT_String_Duplicate( const char * src ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_String_Free( char * str ); + +typedef struct OpenMPT_Semaphore OpenMPT_Semaphore; +OPENMPT_WINESUPPORT_API OpenMPT_Semaphore * OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Construct(void); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Destruct( OpenMPT_Semaphore * sem ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Wait( OpenMPT_Semaphore * sem ); +OPENMPT_WINESUPPORT_API void OPENMPT_WINESUPPORT_CALL OpenMPT_Semaphore_Post( OpenMPT_Semaphore * sem ); + +#ifdef __cplusplus +} +#endif + +#endif // OPENMPT_WINE_UTILS_H diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.c b/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.c new file mode 100644 index 00000000..019f408e --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.c @@ -0,0 +1,678 @@ + +#if defined(MPT_BUILD_WINESUPPORT_WRAPPER) + +#include "NativeConfig.h" + +#include <windows.h> + +#include <stdint.h> + +#include "Native.h" +#include "NativeUtils.h" +#include "NativeSoundDevice.h" + +#ifdef _MSC_VER +#pragma warning(disable:4098) /* 'void' function returning a value */ +#endif + +#define WINE_THREAD + +#ifdef __cplusplus +extern "C" { +#endif + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_Init(void) { + return OpenMPT_Init(); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_Fini(void) { + return OpenMPT_Fini(); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_Alloc( size_t size ) { + return OpenMPT_Alloc( size ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_Free( void * mem ) { + return OpenMPT_Free( mem ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API size_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_String_Length( const char * str ) { + return OpenMPT_String_Length( str ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_String_Duplicate( const char * src ) { + return OpenMPT_String_Duplicate( src ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_String_Free( char * str ) { + return OpenMPT_String_Free( str ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_EnumerateDevices() { + return OpenMPT_SoundDevice_EnumerateDevices(); +} + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver { + void * inst; + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundDeviceMessageFunc)( void * inst, uintptr_t level, const char * message ); +} OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver; + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice_ICallback { + void * inst; + // main thread + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackPreStartFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackPostStopFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackIsLockedByCurrentThreadFunc)( void * inst, uintptr_t * result ); + // audio thread + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockFunc)( void * inst ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedGetReferenceClockNowNanosecondsFunc)( void * inst, uint64_t * result ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessPrepareFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessUint8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, uint8_t * buffer, const uint8_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessInt8Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int8_t * buffer, const int8_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessInt16Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int16_t * buffer, const int16_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessInt24Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, OpenMPT_int24 * buffer, const OpenMPT_int24 * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessInt32Func)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int32_t * buffer, const int32_t * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessFloatFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, float * buffer, const float * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessDoubleFunc)( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, double * buffer, const double * inputBuffer ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackLockedProcessDoneFunc)( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ); + void (OPENMPT_WINESUPPORT_WRAPPER_CALL * SoundCallbackUnlockFunc)( void * inst ); +} OpenMPT_Wine_Wrapper_SoundDevice_ICallback; + +#ifdef WINE_THREAD +typedef enum OpenMPT_Wine_Wrapper_AudioThreadCommand { + AudioThreadCommandInvalid = -1 + , AudioThreadCommandExit = 0 + , AudioThreadCommandLock = 1 + , AudioThreadCommandClock = 2 + , AudioThreadCommandReadPrepare = 3 + , AudioThreadCommandReadUint8 = 4 + , AudioThreadCommandReadInt8 = 5 + , AudioThreadCommandReadInt16 = 6 + , AudioThreadCommandReadInt24 = 7 + , AudioThreadCommandReadInt32 = 8 + , AudioThreadCommandReadFloat = 9 + , AudioThreadCommandReadDouble = 10 + , AudioThreadCommandReadDone = 11 + , AudioThreadCommandUnlock = 12 +} OpenMPT_Wine_Wrapper_AudioThreadCommand; +#endif + +typedef struct OpenMPT_Wine_Wrapper_SoundDevice { + OpenMPT_SoundDevice * impl; + OpenMPT_SoundDevice_IMessageReceiver native_receiver; + OpenMPT_SoundDevice_ICallback native_callback; + OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver wine_receiver; + OpenMPT_Wine_Wrapper_SoundDevice_ICallback wine_callback; +#ifdef WINE_THREAD + HANDLE audiothread_startup_done; + OpenMPT_Semaphore * audiothread_sem_request; + OpenMPT_Semaphore * audiothread_sem_done; + OpenMPT_Wine_Wrapper_AudioThreadCommand audiothread_command; + const OpenMPT_SoundDevice_TimeInfo * audiothread_command_timeInfo; + const OpenMPT_SoundDevice_BufferFormat * audiothread_command_bufferFormat; + const OpenMPT_SoundDevice_BufferAttributes * audiothread_command_bufferAttributes; + uintptr_t audiothread_command_numFrames; + union { + uint8_t * buf_uint8; + int8_t * buf_int8; + int16_t * buf_int16; + OpenMPT_int24 * buf_int24; + int32_t * buf_int32; + float * buf_float; + double * buf_double; + } audiothread_command_buffer; + union { + const uint8_t * buf_uint8; + const int8_t * buf_int8; + const int16_t * buf_int16; + const OpenMPT_int24 * buf_int24; + const int32_t * buf_int32; + const float * buf_float; + const double * buf_double; + } audiothread_command_inputBuffer; + uint64_t * audiothread_command_result; + HANDLE audiothread; + OpenMPT_PriorityBooster * priority_booster; +#endif +} OpenMPT_Wine_Wrapper_SoundDevice; + +static void OPENMPT_WINESUPPORT_CALL SoundDeviceMessageFunc( void * inst, uintptr_t level, const char * message ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } + return sd->wine_receiver.SoundDeviceMessageFunc( sd->wine_receiver.inst, level, message ); +} + +static void OPENMPT_WINESUPPORT_CALL SoundCallbackGetReferenceClockNowNanosecondsFunc( void * inst, uint64_t * result ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } + return sd->wine_callback.SoundCallbackGetReferenceClockNowNanosecondsFunc( sd->wine_callback.inst, result ); +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackPreStartFunc( void * inst ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } + return sd->wine_callback.SoundCallbackPreStartFunc( sd->wine_callback.inst ); +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackPostStopFunc( void * inst ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } + return sd->wine_callback.SoundCallbackPostStopFunc( sd->wine_callback.inst ); +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackIsLockedByCurrentThreadFunc( void * inst, uintptr_t * result ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } + return sd->wine_callback.SoundCallbackIsLockedByCurrentThreadFunc( sd->wine_callback.inst, result ); +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockFunc( void * inst ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandLock; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockFunc( sd->wine_callback.inst ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedGetReferenceClockNowNanosecondsFunc( void * inst, uint64_t * result ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandClock; + sd->audiothread_command_result = result; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc( sd->wine_callback.inst, result ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessPrepareFunc( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadPrepare; + sd->audiothread_command_timeInfo = timeInfo; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessPrepareFunc( sd->wine_callback.inst, timeInfo ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessUint8Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, uint8_t * buffer, const uint8_t * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadUint8; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_uint8 = buffer; + sd->audiothread_command_inputBuffer.buf_uint8 = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessUint8Func( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessInt8Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int8_t * buffer, const int8_t * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadInt8; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_int8 = buffer; + sd->audiothread_command_inputBuffer.buf_int8 = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessInt8Func( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessInt16Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int16_t * buffer, const int16_t * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadInt16; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_int16 = buffer; + sd->audiothread_command_inputBuffer.buf_int16 = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessInt16Func( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessInt24Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, OpenMPT_int24 * buffer, const OpenMPT_int24 * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadInt24; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_int24 = buffer; + sd->audiothread_command_inputBuffer.buf_int24 = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessInt24Func( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessInt32Func( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, int32_t * buffer, const int32_t * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadInt32; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_int32 = buffer; + sd->audiothread_command_inputBuffer.buf_int32 = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessInt32Func( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessFloatFunc( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, float * buffer, const float * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadFloat; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_float = buffer; + sd->audiothread_command_inputBuffer.buf_float = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessFloatFunc( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessDoubleFunc( void * inst, const OpenMPT_SoundDevice_BufferFormat * bufferFormat, uintptr_t numFrames, double * buffer, const double * inputBuffer ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadDouble; + sd->audiothread_command_bufferFormat = bufferFormat; + sd->audiothread_command_numFrames = numFrames; + sd->audiothread_command_buffer.buf_double = buffer; + sd->audiothread_command_inputBuffer.buf_double = inputBuffer; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessDoubleFunc( sd->wine_callback.inst, bufferFormat, bufferAttributes, numFrames, buffer, inputBuffer ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackLockedProcessDoneFunc( void * inst, const OpenMPT_SoundDevice_TimeInfo * timeInfo ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandReadDone; + sd->audiothread_command_timeInfo = timeInfo; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackLockedProcessDoneFunc( sd->wine_callback.inst, timeInfo ); +#endif +} +static void OPENMPT_WINESUPPORT_CALL SoundCallbackUnlockFunc( void * inst ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)inst; + if ( !sd ) { + return; + } +#ifdef WINE_THREAD + sd->audiothread_command = AudioThreadCommandUnlock; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); +#else + return sd->wine_callback.SoundCallbackUnlockFunc( sd->wine_callback.inst ); +#endif +} + +#ifdef WINE_THREAD +static DWORD WINAPI AudioThread( LPVOID userdata ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)userdata; + sd->priority_booster = OpenMPT_PriorityBooster_Construct_From_SoundDevice( sd->impl ); + SetEvent( sd->audiothread_startup_done ); + { + int exit = 0; + while(!exit) + { + OpenMPT_Semaphore_Wait( sd->audiothread_sem_request ); + switch ( sd->audiothread_command ) { + case AudioThreadCommandExit: + exit = 1; + break; + case AudioThreadCommandLock: + if(sd->wine_callback.SoundCallbackLockFunc) + sd->wine_callback.SoundCallbackLockFunc( sd->wine_callback.inst ); + break; + case AudioThreadCommandClock: + if(sd->wine_callback.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc) + sd->wine_callback.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc( sd->wine_callback.inst, sd->audiothread_command_result ); + break; + case AudioThreadCommandReadPrepare: + if(sd->wine_callback.SoundCallbackLockedProcessPrepareFunc) + sd->wine_callback.SoundCallbackLockedProcessPrepareFunc + ( sd->wine_callback.inst + , sd->audiothread_command_timeInfo + ); + break; + case AudioThreadCommandReadUint8: + if(sd->wine_callback.SoundCallbackLockedProcessUint8Func) + sd->wine_callback.SoundCallbackLockedProcessUint8Func + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_uint8 + , sd->audiothread_command_inputBuffer.buf_uint8 + ); + break; + case AudioThreadCommandReadInt8: + if(sd->wine_callback.SoundCallbackLockedProcessInt8Func) + sd->wine_callback.SoundCallbackLockedProcessInt8Func + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_int8 + , sd->audiothread_command_inputBuffer.buf_int8 + ); + break; + case AudioThreadCommandReadInt16: + if(sd->wine_callback.SoundCallbackLockedProcessInt16Func) + sd->wine_callback.SoundCallbackLockedProcessInt16Func + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_int16 + , sd->audiothread_command_inputBuffer.buf_int16 + ); + break; + case AudioThreadCommandReadInt24: + if(sd->wine_callback.SoundCallbackLockedProcessInt24Func) + sd->wine_callback.SoundCallbackLockedProcessInt24Func + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_int24 + , sd->audiothread_command_inputBuffer.buf_int24 + ); + break; + case AudioThreadCommandReadInt32: + if(sd->wine_callback.SoundCallbackLockedProcessInt32Func) + sd->wine_callback.SoundCallbackLockedProcessInt32Func + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_int32 + , sd->audiothread_command_inputBuffer.buf_int32 + ); + break; + case AudioThreadCommandReadFloat: + if(sd->wine_callback.SoundCallbackLockedProcessFloatFunc) + sd->wine_callback.SoundCallbackLockedProcessFloatFunc + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_float + , sd->audiothread_command_inputBuffer.buf_float + ); + break; + case AudioThreadCommandReadDouble: + if(sd->wine_callback.SoundCallbackLockedProcessDoubleFunc) + sd->wine_callback.SoundCallbackLockedProcessDoubleFunc + ( sd->wine_callback.inst + , sd->audiothread_command_bufferFormat + , sd->audiothread_command_numFrames + , sd->audiothread_command_buffer.buf_double + , sd->audiothread_command_inputBuffer.buf_double + ); + break; + case AudioThreadCommandReadDone: + if(sd->wine_callback.SoundCallbackLockedProcessDoneFunc) + sd->wine_callback.SoundCallbackLockedProcessDoneFunc + ( sd->wine_callback.inst + , sd->audiothread_command_timeInfo + ); + break; + case AudioThreadCommandUnlock: + if(sd->wine_callback.SoundCallbackUnlockFunc) + sd->wine_callback.SoundCallbackUnlockFunc( sd->wine_callback.inst ); + break; + default: + break; + } + OpenMPT_Semaphore_Post( sd->audiothread_sem_done ); + } + } + OpenMPT_PriorityBooster_Destruct(sd->priority_booster); + sd->priority_booster = NULL; + return 0; +} +#endif + +OPENMPT_WINESUPPORT_WRAPPER_API OpenMPT_Wine_Wrapper_SoundDevice * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Construct( const char * info ) { + OpenMPT_Wine_Wrapper_SoundDevice * sd = (OpenMPT_Wine_Wrapper_SoundDevice*)OpenMPT_Wine_Wrapper_Alloc( sizeof( OpenMPT_Wine_Wrapper_SoundDevice ) ); + sd->impl = OpenMPT_SoundDevice_Construct( info ); + return sd; +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Destruct( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + OpenMPT_SoundDevice_Destruct( sd->impl ); + sd->impl = NULL; + OpenMPT_Wine_Wrapper_Free( sd ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_SetMessageReceiver( OpenMPT_Wine_Wrapper_SoundDevice * sd, const OpenMPT_Wine_Wrapper_SoundDevice_IMessageReceiver * receiver ) { + OpenMPT_SoundDevice_SetMessageReceiver( sd->impl, NULL ); + ZeroMemory( &( sd->wine_receiver ), sizeof( sd->wine_receiver ) ); + if(receiver) + { + sd->wine_receiver = *receiver; + } + sd->native_receiver.inst = sd; + sd->native_receiver.SoundDeviceMessageFunc = &SoundDeviceMessageFunc; + OpenMPT_SoundDevice_SetMessageReceiver( sd->impl, &sd->native_receiver ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_SetCallback( OpenMPT_Wine_Wrapper_SoundDevice * sd, const OpenMPT_Wine_Wrapper_SoundDevice_ICallback * callback ) { + OpenMPT_SoundDevice_SetCallback( sd->impl, NULL ); + ZeroMemory( &( sd->wine_callback ), sizeof( sd->wine_callback) ); + if(callback) + { + sd->wine_callback = *callback; + } + sd->native_callback.inst = sd; + sd->native_callback.SoundCallbackGetReferenceClockNowNanosecondsFunc = &SoundCallbackGetReferenceClockNowNanosecondsFunc; + sd->native_callback.SoundCallbackPreStartFunc = &SoundCallbackPreStartFunc; + sd->native_callback.SoundCallbackPostStopFunc = &SoundCallbackPostStopFunc; + sd->native_callback.SoundCallbackIsLockedByCurrentThreadFunc = &SoundCallbackIsLockedByCurrentThreadFunc; + sd->native_callback.SoundCallbackLockFunc = &SoundCallbackLockFunc; + sd->native_callback.SoundCallbackLockedGetReferenceClockNowNanosecondsFunc = &SoundCallbackLockedGetReferenceClockNowNanosecondsFunc; + sd->native_callback.SoundCallbackLockedProcessPrepareFunc = &SoundCallbackLockedProcessPrepareFunc; + sd->native_callback.SoundCallbackLockedProcessUint8Func = &SoundCallbackLockedProcessUint8Func; + sd->native_callback.SoundCallbackLockedProcessInt8Func = &SoundCallbackLockedProcessInt8Func; + sd->native_callback.SoundCallbackLockedProcessInt16Func = &SoundCallbackLockedProcessInt16Func; + sd->native_callback.SoundCallbackLockedProcessInt24Func = &SoundCallbackLockedProcessInt24Func; + sd->native_callback.SoundCallbackLockedProcessInt32Func = &SoundCallbackLockedProcessInt32Func; + sd->native_callback.SoundCallbackLockedProcessFloatFunc = &SoundCallbackLockedProcessFloatFunc; + sd->native_callback.SoundCallbackLockedProcessDoubleFunc = &SoundCallbackLockedProcessDoubleFunc; + sd->native_callback.SoundCallbackLockedProcessDoneFunc = &SoundCallbackLockedProcessDoneFunc; + sd->native_callback.SoundCallbackUnlockFunc = &SoundCallbackUnlockFunc; + OpenMPT_SoundDevice_SetCallback( sd->impl, &sd->native_callback ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceInfo( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_GetDeviceInfo( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceCaps( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_GetDeviceCaps( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetDeviceDynamicCaps( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * baseSampleRates ) { + return OpenMPT_SoundDevice_GetDeviceDynamicCaps( sd->impl, baseSampleRates ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Init( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * appInfo ) { + return OpenMPT_SoundDevice_Init( sd->impl, appInfo ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Open( OpenMPT_Wine_Wrapper_SoundDevice * sd, const char * settings ) { + uintptr_t result = 0; + result = OpenMPT_SoundDevice_Open( sd->impl, settings ); +#ifdef WINE_THREAD + if ( result ) { + DWORD threadId = 0; + sd->audiothread_startup_done = CreateEvent( NULL, FALSE, FALSE, NULL ); + sd->audiothread_sem_request = OpenMPT_Semaphore_Construct(); + sd->audiothread_sem_done = OpenMPT_Semaphore_Construct(); + sd->audiothread_command = AudioThreadCommandInvalid; + sd->audiothread = CreateThread( NULL, 0, &AudioThread, sd, 0, &threadId ); + WaitForSingleObject( sd->audiothread_startup_done, INFINITE ); + } +#endif + return result; +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Close( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + uintptr_t result = 0; +#ifdef WINE_THREAD + if ( OpenMPT_SoundDevice_IsOpen( sd->impl ) ) { + sd->audiothread_command = AudioThreadCommandExit; + OpenMPT_Semaphore_Post( sd->audiothread_sem_request ); + OpenMPT_Semaphore_Wait( sd->audiothread_sem_done ); + sd->audiothread_command = AudioThreadCommandInvalid; + WaitForSingleObject( sd->audiothread, INFINITE ); + CloseHandle( sd->audiothread ); + sd->audiothread = NULL; + OpenMPT_Semaphore_Destruct( sd->audiothread_sem_done ); + sd->audiothread_sem_done = NULL; + OpenMPT_Semaphore_Destruct( sd->audiothread_sem_request ); + sd->audiothread_sem_request = NULL; + CloseHandle( sd->audiothread_startup_done ); + sd->audiothread_startup_done = NULL; + } +#endif + result = OpenMPT_SoundDevice_Close( sd->impl ); + return result; +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Start( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_Start( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_Stop( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_Stop( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetRequestFlags( const OpenMPT_Wine_Wrapper_SoundDevice * sd, uint32_t * result) { + return OpenMPT_SoundDevice_GetRequestFlags( sd->impl, result ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_IsInited( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_IsInited( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_IsOpen( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_IsOpen( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_IsAvailable( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_IsAvailable( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_IsPlaying( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_IsPlaying( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_IsPlayingSilence( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_IsPlayingSilence( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_StopAndAvoidPlayingSilence( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_StopAndAvoidPlayingSilence( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_EndPlayingSilence( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_EndPlayingSilence( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_OnIdle( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_OnIdle( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetSettings( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_GetSettings( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetActualSampleFormat( const OpenMPT_Wine_Wrapper_SoundDevice * sd, int32_t * result ) { + return OpenMPT_SoundDevice_GetActualSampleFormat( sd->impl, result ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetEffectiveBufferAttributes( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_BufferAttributes * result ) { + return OpenMPT_SoundDevice_GetEffectiveBufferAttributes( sd->impl, result ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetTimeInfo( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_TimeInfo * result ) { + return OpenMPT_SoundDevice_GetTimeInfo( sd->impl, result ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API void OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetStreamPosition( const OpenMPT_Wine_Wrapper_SoundDevice * sd, OpenMPT_SoundDevice_StreamPosition * result ) { + return OpenMPT_SoundDevice_GetStreamPosition( sd->impl, result ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_DebugIsFragileDevice( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_DebugIsFragileDevice( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_DebugInRealtimeCallback( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_DebugInRealtimeCallback( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API char * OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_GetStatistics( const OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_GetStatistics( sd->impl ); +} + +OPENMPT_WINESUPPORT_WRAPPER_API uintptr_t OPENMPT_WINESUPPORT_WRAPPER_CALL OpenMPT_Wine_Wrapper_SoundDevice_OpenDriverSettings( OpenMPT_Wine_Wrapper_SoundDevice * sd ) { + return OpenMPT_SoundDevice_OpenDriverSettings( sd->impl ); +} + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // MPT_BUILD_WINESUPPORT_WRAPPER diff --git a/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.cpp b/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.cpp new file mode 100644 index 00000000..e4709c8c --- /dev/null +++ b/Src/external_dependencies/openmpt-trunk/mptrack/wine/WineWrapper.cpp @@ -0,0 +1,6 @@ + +#if defined(MPT_BUILD_WINESUPPORT_WRAPPER) + +#include "WineWrapper.c" + +#endif // MPT_BUILD_WINESUPPORT_WRAPPER |