1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318 |
- /*
- * Bridge.cpp
- * ----------
- * Purpose: VST plugin bridge (plugin side)
- * Notes : (currently none)
- * Authors: OpenMPT Devs
- * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
- */
- // TODO: Translate pointer-sized members in remaining structs: VstVariableIo, VstOfflineTask, VstAudioFile, VstWindow (all these are currently not supported by OpenMPT, so not urgent at all)
- #include "openmpt/all/BuildSettings.hpp"
- #include "../common/mptBaseMacros.h"
- #include "../common/mptBaseTypes.h"
- #include "../common/mptBaseUtils.h"
- #include <Windows.h>
- #include <ShellAPI.h>
- #include <ShlObj.h>
- #include <CommDlg.h>
- #include <tchar.h>
- #include <algorithm>
- #include <string>
- #if defined(MPT_BUILD_MSVC)
- #pragma comment(lib, "comdlg32.lib")
- #pragma comment(lib, "ole32.lib")
- #pragma comment(lib, "shell32.lib")
- #endif
- #if MPT_BUILD_DEBUG
- #include <intrin.h>
- #define MPT_ASSERT(x) \
- MPT_MAYBE_CONSTANT_IF(!(x)) \
- { \
- if(IsDebuggerPresent()) \
- __debugbreak(); \
- ::MessageBoxA(nullptr, "Debug Assertion Failed:\n\n" #x, "OpenMPT Plugin Bridge", MB_ICONERROR); \
- }
- #else
- #define MPT_ASSERT(x)
- #endif
- #include "../misc/WriteMemoryDump.h"
- #include "Bridge.h"
- // Crash handler for writing memory dumps
- static LONG WINAPI CrashHandler(_EXCEPTION_POINTERS *pExceptionInfo)
- {
- WCHAR tempPath[MAX_PATH + 2];
- DWORD result = GetTempPathW(MAX_PATH + 1, tempPath);
- if(result > 0 && result <= MAX_PATH + 1)
- {
- std::wstring filename = tempPath;
- filename += L"OpenMPT Crash Files\\";
- CreateDirectoryW(filename.c_str(), nullptr);
- tempPath[0] = 0;
- const int ch = GetDateFormatW(LOCALE_SYSTEM_DEFAULT, 0, nullptr, L"'PluginBridge 'yyyy'-'MM'-'dd ", tempPath, mpt::saturate_cast<int>(std::size(tempPath)));
- if(ch)
- GetTimeFormatW(LOCALE_SYSTEM_DEFAULT, 0, nullptr, L"HH'.'mm'.'ss'.dmp'", tempPath + ch - 1, mpt::saturate_cast<int>(std::size(tempPath)) - ch + 1);
- filename += tempPath;
- OPENMPT_NAMESPACE::WriteMemoryDump(pExceptionInfo, filename.c_str(), OPENMPT_NAMESPACE::PluginBridge::m_fullMemDump);
- }
- // Let Windows handle the exception...
- return EXCEPTION_CONTINUE_SEARCH;
- }
- int _tmain(int argc, TCHAR *argv[])
- {
- if(argc != 2)
- {
- MessageBox(nullptr, _T("This executable is part of OpenMPT. You do not need to run it by yourself."), _T("OpenMPT Plugin Bridge"), 0);
- return -1;
- }
- ::SetUnhandledExceptionFilter(CrashHandler);
- // We don't need COM, but some plugins do and don't initialize it themselves.
- // Note 1: Which plugins? This was added in r6459 on 2016-05-31 but with no remark whether it fixed a specific plugin,
- // but the fix doesn't seem to make a lot of sense since back then no plugin code was ever running on the main thread.
- // Could it have been for file dialogs, which were added a while before?
- // Note 2: M1 editor crashes if it runs on this thread and it was initialized with COINIT_MULTITHREADED
- const bool comInitialized = SUCCEEDED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED));
- OPENMPT_NAMESPACE::PluginBridge::MainLoop(argv);
- if(comInitialized)
- CoUninitialize();
- return 0;
- }
- int WINAPI WinMain(_In_ HINSTANCE /*hInstance*/, _In_opt_ HINSTANCE /*hPrevInstance*/, _In_ LPSTR /*lpCmdLine*/, _In_ int /*nCmdShow*/)
- {
- int argc = 0;
- auto argv = CommandLineToArgvW(GetCommandLineW(), &argc);
- return _tmain(argc, argv);
- }
- OPENMPT_NAMESPACE_BEGIN
- using namespace Vst;
- std::vector<BridgeCommon *> BridgeCommon::m_plugins;
- HWND BridgeCommon::m_communicationWindow = nullptr;
- int BridgeCommon::m_instanceCount = 0;
- thread_local bool BridgeCommon::m_isAudioThread = false;
- // This is kind of a back-up pointer in case we couldn't sneak our pointer into the AEffect struct yet.
- // It always points to the last initialized PluginBridge object.
- PluginBridge *PluginBridge::m_latestInstance = nullptr;
- ATOM PluginBridge::m_editorClassAtom = 0;
- bool PluginBridge::m_fullMemDump = false;
- void PluginBridge::MainLoop(TCHAR *argv[])
- {
- WNDCLASSEX editorWndClass;
- editorWndClass.cbSize = sizeof(WNDCLASSEX);
- editorWndClass.style = CS_HREDRAW | CS_VREDRAW;
- editorWndClass.lpfnWndProc = WindowProc;
- editorWndClass.cbClsExtra = 0;
- editorWndClass.cbWndExtra = 0;
- editorWndClass.hInstance = GetModuleHandle(nullptr);
- editorWndClass.hIcon = nullptr;
- editorWndClass.hCursor = LoadCursor(nullptr, IDC_ARROW);
- editorWndClass.hbrBackground = nullptr;
- editorWndClass.lpszMenuName = nullptr;
- editorWndClass.lpszClassName = _T("OpenMPTPluginBridgeEditor");
- editorWndClass.hIconSm = nullptr;
- m_editorClassAtom = RegisterClassEx(&editorWndClass);
- CreateCommunicationWindow(WindowProc);
- SetTimer(m_communicationWindow, TIMER_IDLE, 20, IdleTimerProc);
- uint32 parentProcessId = _ttoi(argv[1]);
- new PluginBridge(argv[0], OpenProcess(SYNCHRONIZE, FALSE, parentProcessId));
- MSG msg;
- while(::GetMessage(&msg, nullptr, 0, 0))
- {
- // Let host pre-process key messages like it does for non-bridged plugins
- if(msg.message >= WM_KEYFIRST && msg.message <= WM_KEYLAST)
- {
- HWND owner = nullptr;
- for(HWND hwnd = msg.hwnd; hwnd != nullptr; hwnd = GetParent(hwnd))
- {
- // Does it come from a child window? (e.g. Kirnu editor)
- if(GetClassWord(hwnd, GCW_ATOM) == m_editorClassAtom)
- {
- owner = GetParent(GetParent(hwnd));
- break;
- }
- // Does the message come from a top-level window? This is required e.g. for the slider pop-up windows and patch browser in Synth1.
- if(!(GetWindowLong(hwnd, GWL_STYLE) & WS_CHILD))
- {
- owner = GetWindow(hwnd, GW_OWNER);
- break;
- }
- }
- // Send to top-level VST editor window in host
- if(owner && SendMessage(owner, msg.message + WM_BRIDGE_KEYFIRST - WM_KEYFIRST, msg.wParam, msg.lParam))
- continue;
- }
- TranslateMessage(&msg);
- DispatchMessage(&msg);
- }
- DestroyWindow(m_communicationWindow);
- }
- PluginBridge::PluginBridge(const wchar_t *memName, HANDLE otherProcess)
- {
- PluginBridge::m_latestInstance = this;
- m_thisPluginID = static_cast<int32>(m_plugins.size());
- m_plugins.push_back(this);
- if(!m_queueMem.Open(memName)
- || !CreateSignals(memName))
- {
- MessageBox(nullptr, _T("Could not connect to OpenMPT."), _T("OpenMPT Plugin Bridge"), 0);
- delete this;
- return;
- }
- m_sharedMem = m_queueMem.Data<SharedMemLayout>();
- // Store parent process handle so that we can terminate the bridge process when OpenMPT closes (e.g. through a crash).
- m_otherProcess.DuplicateFrom(otherProcess);
- m_sigThreadExit.Create(true);
- DWORD dummy = 0; // For Win9x
- m_audioThread = CreateThread(NULL, 0, &PluginBridge::AudioThread, this, 0, &dummy);
- m_sharedMem->bridgeCommWindow = m_communicationWindow;
- m_sharedMem->bridgePluginID = m_thisPluginID;
- m_sigBridgeReady.Trigger();
- }
- PluginBridge::~PluginBridge()
- {
- SignalObjectAndWait(m_sigThreadExit, m_audioThread, INFINITE, FALSE);
- CloseHandle(m_audioThread);
- BridgeMessage dispatchMsg;
- dispatchMsg.Dispatch(effClose, 0, 0, 0, 0.0f, 0);
- DispatchToPlugin(dispatchMsg.dispatch);
- m_plugins[m_thisPluginID] = nullptr;
- if(m_instanceCount == 1)
- PostQuitMessage(0);
- }
- void PluginBridge::RequestDelete()
- {
- PostMessage(m_communicationWindow, WM_BRIDGE_DELETE_PLUGIN, m_thisPluginID, 0);
- }
- // Send an arbitrary message to the host.
- // Returns true if the message was processed by the host.
- bool PluginBridge::SendToHost(BridgeMessage &sendMsg)
- {
- auto &messages = m_sharedMem->ipcMessages;
- const auto msgID = CopyToSharedMemory(sendMsg, messages);
- if(msgID < 0)
- return false;
- BridgeMessage &sharedMsg = messages[msgID];
- if(!m_isAudioThread)
- {
- if(SendMessage(m_sharedMem->hostCommWindow, WM_BRIDGE_MESSAGE_TO_HOST, m_otherPluginID, msgID) == WM_BRIDGE_SUCCESS)
- {
- sharedMsg.CopyTo(sendMsg);
- return true;
- }
- return false;
- }
- // Audio thread: Use signals instead of window messages
- m_sharedMem->audioThreadToHostMsgID = msgID;
- m_sigToHostAudio.Send();
- // Wait until we get the result from the host.
- DWORD result;
- const HANDLE objects[] = {m_sigToHostAudio.confirm, m_sigToBridgeAudio.send, m_otherProcess};
- do
- {
- result = WaitForMultipleObjects(mpt::saturate_cast<DWORD>(std::size(objects)), objects, FALSE, INFINITE);
- if(result == WAIT_OBJECT_0)
- {
- // Message got answered
- sharedMsg.CopyTo(sendMsg);
- break;
- } else if(result == WAIT_OBJECT_0 + 1)
- {
- ParseNextMessage(m_sharedMem->audioThreadToBridgeMsgID);
- m_sigToBridgeAudio.Confirm();
- }
- } while(result != WAIT_OBJECT_0 + 2 && result != WAIT_FAILED);
- return (result == WAIT_OBJECT_0);
- }
- // Copy AEffect to shared memory.
- void PluginBridge::UpdateEffectStruct()
- {
- if(m_nativeEffect == nullptr)
- return;
- else if(m_otherPtrSize == 4)
- m_sharedMem->effect32.FromNative(*m_nativeEffect);
- else if(m_otherPtrSize == 8)
- m_sharedMem->effect64.FromNative(*m_nativeEffect);
- else
- MPT_ASSERT(false);
- }
- // Create the memory-mapped file containing the processing message and audio buffers
- void PluginBridge::CreateProcessingFile(std::vector<char> &dispatchData)
- {
- static uint32 plugId = 0;
- wchar_t mapName[64];
- swprintf(mapName, std::size(mapName), L"Local\\openmpt-%u-%u", GetCurrentProcessId(), plugId++);
- PushToVector(dispatchData, mapName[0], sizeof(mapName));
- if(!m_processMem.Create(mapName, sizeof(ProcessMsg) + m_mixBufSize * (m_nativeEffect->numInputs + m_nativeEffect->numOutputs) * sizeof(double)))
- {
- SendErrorMessage(L"Could not initialize plugin bridge audio memory.");
- return;
- }
- }
- // Receive a message from the host and translate it.
- void PluginBridge::ParseNextMessage(int msgID)
- {
- auto &msg = m_sharedMem->ipcMessages[msgID];
- switch(msg.header.type)
- {
- case MsgHeader::newInstance:
- NewInstance(msg.newInstance);
- break;
- case MsgHeader::init:
- InitBridge(msg.init);
- break;
- case MsgHeader::dispatch:
- DispatchToPlugin(msg.dispatch);
- break;
- case MsgHeader::setParameter:
- SetParameter(msg.parameter);
- break;
- case MsgHeader::getParameter:
- GetParameter(msg.parameter);
- break;
- case MsgHeader::automate:
- AutomateParameters();
- break;
- }
- }
- // Create a new bridge instance within this one (creates a new thread).
- void PluginBridge::NewInstance(NewInstanceMsg &msg)
- {
- msg.memName[mpt::array_size<decltype(msg.memName)>::size - 1] = 0;
- new PluginBridge(msg.memName, m_otherProcess);
- }
- // Load the plugin.
- void PluginBridge::InitBridge(InitMsg &msg)
- {
- m_otherPtrSize = msg.hostPtrSize;
- m_mixBufSize = msg.mixBufSize;
- m_otherPluginID = msg.pluginID;
- m_fullMemDump = msg.fullMemDump != 0;
- msg.result = 0;
- msg.str[mpt::array_size<decltype(msg.str)>::size - 1] = 0;
- #ifdef _CONSOLE
- SetConsoleTitleW(msg->str);
- #endif
- m_nativeEffect = nullptr;
- __try
- {
- m_library = LoadLibraryW(msg.str);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- m_library = nullptr;
- }
- if(m_library == nullptr)
- {
- FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, GetLastError(), MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), msg.str, mpt::saturate_cast<DWORD>(std::size(msg.str)), nullptr);
- RequestDelete();
- return;
- }
- auto mainProc = (Vst::MainProc)GetProcAddress(m_library, "VSTPluginMain");
- if(mainProc == nullptr)
- {
- mainProc = (Vst::MainProc)GetProcAddress(m_library, "main");
- }
- if(mainProc != nullptr)
- {
- __try
- {
- m_nativeEffect = mainProc(MasterCallback);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- m_nativeEffect = nullptr;
- }
- }
- if(m_nativeEffect == nullptr || m_nativeEffect->dispatcher == nullptr || m_nativeEffect->magic != kEffectMagic)
- {
- FreeLibrary(m_library);
- m_library = nullptr;
- wcscpy(msg.str, L"File is not a valid plugin");
- RequestDelete();
- return;
- }
- m_nativeEffect->reservedForHost1 = this;
- msg.result = 1;
- UpdateEffectStruct();
- // Init process buffer
- DispatchToHost(audioMasterVendorSpecific, kVendorOpenMPT, kUpdateProcessingBuffer, nullptr, 0.0f);
- }
- void PluginBridge::SendErrorMessage(const wchar_t *str)
- {
- BridgeMessage msg;
- msg.Error(str);
- SendToHost(msg);
- }
- // Wrapper for VST dispatch call with structured exception handling.
- static intptr_t DispatchSEH(AEffect *effect, VstOpcodeToPlugin opCode, int32 index, intptr_t value, void *ptr, float opt, bool &exception)
- {
- __try
- {
- if(effect->dispatcher != nullptr)
- {
- return effect->dispatcher(effect, opCode, index, value, ptr, opt);
- }
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- exception = true;
- }
- return 0;
- }
- // Host-to-plugin opcode dispatcher
- void PluginBridge::DispatchToPlugin(DispatchMsg &msg)
- {
- if(m_nativeEffect == nullptr)
- {
- return;
- }
- // Various dispatch data - depending on the opcode, one of those might be used.
- std::vector<char> extraData;
- size_t extraDataSize = 0;
- MappedMemory auxMem;
- // Content of ptr is usually stored right after the message header, ptr field indicates size.
- void *ptr = (msg.ptr != 0) ? (&msg + 1) : nullptr;
- if(msg.size > sizeof(BridgeMessage))
- {
- if(!auxMem.Open(static_cast<const wchar_t *>(ptr)))
- {
- return;
- }
- ptr = auxMem.Data();
- }
- void *origPtr = ptr;
- switch(msg.opcode)
- {
- case effGetProgramName:
- case effGetParamLabel:
- case effGetParamDisplay:
- case effGetParamName:
- case effString2Parameter:
- case effGetProgramNameIndexed:
- case effGetEffectName:
- case effGetErrorText:
- case effGetVendorString:
- case effGetProductString:
- case effShellGetNextPlugin:
- // Name in [ptr]
- extraDataSize = 256;
- break;
- case effMainsChanged:
- // [value]: 0 means "turn off", 1 means "turn on"
- ::SetThreadPriority(m_audioThread, msg.value ? THREAD_PRIORITY_ABOVE_NORMAL : THREAD_PRIORITY_NORMAL);
- m_sharedMem->tailSize = static_cast<int32>(Dispatch(effGetTailSize, 0, 0, nullptr, 0.0f));
- break;
- case effEditGetRect:
- // ERect** in [ptr]
- extraDataSize = sizeof(void *);
- break;
- case effEditOpen:
- // HWND in [ptr] - Note: Window handles are interoperable between 32-bit and 64-bit applications in Windows (http://msdn.microsoft.com/en-us/library/windows/desktop/aa384203%28v=vs.85%29.aspx)
- {
- TCHAR str[_MAX_PATH];
- GetModuleFileName(m_library, str, mpt::saturate_cast<DWORD>(std::size(str)));
- const auto parentWindow = reinterpret_cast<HWND>(msg.ptr);
- ptr = m_window = CreateWindow(
- MAKEINTATOM(m_editorClassAtom),
- str,
- WS_CHILD | WS_VISIBLE,
- CW_USEDEFAULT, CW_USEDEFAULT,
- 1, 1,
- parentWindow,
- nullptr,
- GetModuleHandle(nullptr),
- nullptr);
- SetWindowLongPtr(m_window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(this));
- }
- break;
- case effGetChunk:
- // void** in [ptr] for chunk data address
- extraDataSize = sizeof(void *);
- break;
- case effProcessEvents:
- // VstEvents* in [ptr]
- TranslateBridgeToVstEvents(m_eventCache, ptr);
- ptr = m_eventCache.data();
- break;
- case effOfflineNotify:
- // VstAudioFile* in [ptr]
- extraData.resize(sizeof(VstAudioFile *) * static_cast<size_t>(msg.value));
- ptr = extraData.data();
- for(int64 i = 0; i < msg.value; i++)
- {
- // TODO create pointers
- }
- break;
- case effOfflinePrepare:
- case effOfflineRun:
- // VstOfflineTask* in [ptr]
- extraData.resize(sizeof(VstOfflineTask *) * static_cast<size_t>(msg.value));
- ptr = extraData.data();
- for(int64 i = 0; i < msg.value; i++)
- {
- // TODO create pointers
- }
- break;
- case effSetSpeakerArrangement:
- case effGetSpeakerArrangement:
- // VstSpeakerArrangement* in [value] and [ptr]
- msg.value = reinterpret_cast<int64>(ptr) + sizeof(VstSpeakerArrangement);
- break;
- case effVendorSpecific:
- // Let's implement some custom opcodes!
- if(msg.index == kVendorOpenMPT)
- {
- msg.result = 1;
- switch(msg.value)
- {
- case kUpdateEffectStruct:
- UpdateEffectStruct();
- break;
- case kUpdateEventMemName:
- if(ptr)
- m_eventMem.Open(static_cast<const wchar_t *>(ptr));
- break;
- case kCacheProgramNames:
- if(ptr)
- {
- int32 progMin = static_cast<const int32 *>(ptr)[0];
- int32 progMax = static_cast<const int32 *>(ptr)[1];
- char *name = static_cast<char *>(ptr);
- for(int32 i = progMin; i < progMax; i++)
- {
- strcpy(name, "");
- if(m_nativeEffect->numPrograms <= 0 || Dispatch(effGetProgramNameIndexed, i, -1, name, 0) != 1)
- {
- // Fallback: Try to get current program name.
- strcpy(name, "");
- int32 curProg = static_cast<int32>(Dispatch(effGetProgram, 0, 0, nullptr, 0.0f));
- if(i != curProg)
- {
- Dispatch(effSetProgram, 0, i, nullptr, 0.0f);
- }
- Dispatch(effGetProgramName, 0, 0, name, 0);
- if(i != curProg)
- {
- Dispatch(effSetProgram, 0, curProg, nullptr, 0.0f);
- }
- }
- name[kCachedProgramNameLength - 1] = '\0';
- name += kCachedProgramNameLength;
- }
- }
- break;
- case kCacheParameterInfo:
- if(ptr)
- {
- int32 paramMin = static_cast<const int32 *>(ptr)[0];
- int32 paramMax = static_cast<const int32 *>(ptr)[1];
- ParameterInfo *param = static_cast<ParameterInfo *>(ptr);
- for(int32 i = paramMin; i < paramMax; i++, param++)
- {
- strcpy(param->name, "");
- strcpy(param->label, "");
- strcpy(param->display, "");
- Dispatch(effGetParamName, i, 0, param->name, 0.0f);
- Dispatch(effGetParamLabel, i, 0, param->label, 0.0f);
- Dispatch(effGetParamDisplay, i, 0, param->display, 0.0f);
- param->name[mpt::array_size<decltype(param->label)>::size - 1] = '\0';
- param->label[mpt::array_size<decltype(param->label)>::size - 1] = '\0';
- param->display[mpt::array_size<decltype(param->display)>::size - 1] = '\0';
- if(Dispatch(effGetParameterProperties, i, 0, ¶m->props, 0.0f) != 1)
- {
- memset(¶m->props, 0, sizeof(param->props));
- strncpy(param->props.label, param->name, std::size(param->props.label));
- }
- }
- }
- break;
- case kBeginGetProgram:
- if(ptr)
- {
- int32 numParams = static_cast<int32>((msg.size - sizeof(DispatchMsg)) / sizeof(float));
- float *params = static_cast<float *>(ptr);
- for(int32 i = 0; i < numParams; i++)
- {
- params[i] = m_nativeEffect->getParameter(m_nativeEffect, i);
- }
- }
- break;
- default:
- msg.result = 0;
- }
- return;
- }
- break;
- }
- if(extraDataSize != 0)
- {
- extraData.resize(extraDataSize, 0);
- ptr = extraData.data();
- }
- //std::cout << "about to dispatch " << msg.opcode << " to effect...";
- //std::flush(std::cout);
- bool exception = false;
- msg.result = static_cast<int32>(DispatchSEH(m_nativeEffect, static_cast<VstOpcodeToPlugin>(msg.opcode), msg.index, static_cast<intptr_t>(msg.value), ptr, msg.opt, exception));
- if(exception && msg.opcode != effClose)
- {
- msg.type = MsgHeader::exceptionMsg;
- return;
- }
- //std::cout << "done" << std::endl;
- // Post-fix some opcodes
- switch(msg.opcode)
- {
- case effClose:
- m_nativeEffect = nullptr;
- FreeLibrary(m_library);
- m_library = nullptr;
- RequestDelete();
- return;
- case effGetProgramName:
- case effGetParamLabel:
- case effGetParamDisplay:
- case effGetParamName:
- case effString2Parameter:
- case effGetProgramNameIndexed:
- case effGetEffectName:
- case effGetErrorText:
- case effGetVendorString:
- case effGetProductString:
- case effShellGetNextPlugin:
- // Name in [ptr]
- {
- extraData.back() = 0;
- char *dst = static_cast<char *>(origPtr);
- size_t length = static_cast<size_t>(msg.ptr - 1);
- strncpy(dst, extraData.data(), length);
- dst[length] = 0;
- break;
- }
- case effEditGetRect:
- // ERect** in [ptr]
- {
- ERect *rectPtr = *reinterpret_cast<ERect **>(extraData.data());
- if(rectPtr != nullptr && origPtr != nullptr)
- {
- MPT_ASSERT(static_cast<size_t>(msg.ptr) >= sizeof(ERect));
- std::memcpy(origPtr, rectPtr, std::min(sizeof(ERect), static_cast<size_t>(msg.ptr)));
- m_windowWidth = rectPtr->right - rectPtr->left;
- m_windowHeight = rectPtr->bottom - rectPtr->top;
- // For plugins that don't know their size until after effEditOpen is done.
- if(m_window)
- {
- SetWindowPos(m_window, nullptr, 0, 0, m_windowWidth, m_windowHeight, SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
- }
- }
- break;
- }
- case effEditClose:
- DestroyWindow(m_window);
- m_window = nullptr;
- break;
- case effGetChunk:
- // void** in [ptr] for chunk data address
- if(m_getChunkMem.Create(static_cast<const wchar_t *>(origPtr), msg.result))
- {
- std::memcpy(m_getChunkMem.Data(), *reinterpret_cast<void **>(extraData.data()), msg.result);
- }
- break;
- }
- UpdateEffectStruct(); // Regularly update the struct
- }
- intptr_t PluginBridge::Dispatch(VstOpcodeToPlugin opcode, int32 index, intptr_t value, void *ptr, float opt)
- {
- __try
- {
- return m_nativeEffect->dispatcher(m_nativeEffect, opcode, index, value, ptr, opt);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- SendErrorMessage(L"Exception in dispatch()!");
- }
- return 0;
- }
- // Set a plugin parameter.
- void PluginBridge::SetParameter(ParameterMsg &msg)
- {
- __try
- {
- m_nativeEffect->setParameter(m_nativeEffect, msg.index, msg.value);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- msg.type = MsgHeader::exceptionMsg;
- }
- }
- // Get a plugin parameter.
- void PluginBridge::GetParameter(ParameterMsg &msg)
- {
- __try
- {
- msg.value = m_nativeEffect->getParameter(m_nativeEffect, msg.index);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- msg.type = MsgHeader::exceptionMsg;
- }
- }
- // Execute received parameter automation messages
- void PluginBridge::AutomateParameters()
- {
- __try
- {
- const AutomationQueue::Parameter *param = m_sharedMem->automationQueue.params;
- const AutomationQueue::Parameter *paramEnd = param + std::min(m_sharedMem->automationQueue.pendingEvents.exchange(0), static_cast<int32>(std::size(m_sharedMem->automationQueue.params)));
- while(param != paramEnd)
- {
- m_nativeEffect->setParameter(m_nativeEffect, param->index, param->value);
- param++;
- }
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- SendErrorMessage(L"Exception in setParameter()!");
- }
- }
- // Audio rendering thread
- DWORD WINAPI PluginBridge::AudioThread(LPVOID param)
- {
- static_cast<PluginBridge*>(param)->AudioThread();
- return 0;
- }
- void PluginBridge::AudioThread()
- {
- m_isAudioThread = true;
- const HANDLE objects[] = {m_sigProcessAudio.send, m_sigToBridgeAudio.send, m_sigThreadExit, m_otherProcess};
- DWORD result = 0;
- do
- {
- result = WaitForMultipleObjects(mpt::saturate_cast<DWORD>(std::size(objects)), objects, FALSE, INFINITE);
- if(result == WAIT_OBJECT_0)
- {
- ProcessMsg *msg = m_processMem.Data<ProcessMsg>();
- AutomateParameters();
- m_sharedMem->tailSize = static_cast<int32>(Dispatch(effGetTailSize, 0, 0, nullptr, 0.0f));
- m_isProcessing = true;
- // Prepare VstEvents.
- if(auto *events = m_eventMem.Data<int32>(); events != nullptr && *events != 0)
- {
- TranslateBridgeToVstEvents(m_eventCache, events);
- *events = 0;
- Dispatch(effProcessEvents, 0, 0, m_eventCache.data(), 0.0f);
- }
- switch(msg->processType)
- {
- case ProcessMsg::process:
- Process();
- break;
- case ProcessMsg::processReplacing:
- ProcessReplacing();
- break;
- case ProcessMsg::processDoubleReplacing:
- ProcessDoubleReplacing();
- break;
- }
- m_isProcessing = false;
- m_sigProcessAudio.Confirm();
- } else if(result == WAIT_OBJECT_0 + 1)
- {
- ParseNextMessage(m_sharedMem->audioThreadToBridgeMsgID);
- m_sigToBridgeAudio.Confirm();
- } else if(result == WAIT_OBJECT_0 + 2)
- {
- // Main thread asked for termination
- break;
- } else if(result == WAIT_OBJECT_0 + 3)
- {
- // Host process died
- RequestDelete();
- break;
- }
- } while(result != WAIT_FAILED);
- }
- // Process audio.
- void PluginBridge::Process()
- {
- if(m_nativeEffect->process)
- {
- float **inPointers, **outPointers;
- int32 sampleFrames = BuildProcessPointers(inPointers, outPointers);
- __try
- {
- m_nativeEffect->process(m_nativeEffect, inPointers, outPointers, sampleFrames);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- SendErrorMessage(L"Exception in process()!");
- }
- }
- }
- // Process audio.
- void PluginBridge::ProcessReplacing()
- {
- if(m_nativeEffect->processReplacing)
- {
- float **inPointers, **outPointers;
- int32 sampleFrames = BuildProcessPointers(inPointers, outPointers);
- __try
- {
- m_nativeEffect->processReplacing(m_nativeEffect, inPointers, outPointers, sampleFrames);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- SendErrorMessage(L"Exception in processReplacing()!");
- }
- }
- }
- // Process audio.
- void PluginBridge::ProcessDoubleReplacing()
- {
- if(m_nativeEffect->processDoubleReplacing)
- {
- double **inPointers, **outPointers;
- int32 sampleFrames = BuildProcessPointers(inPointers, outPointers);
- __try
- {
- m_nativeEffect->processDoubleReplacing(m_nativeEffect, inPointers, outPointers, sampleFrames);
- } __except(EXCEPTION_EXECUTE_HANDLER)
- {
- SendErrorMessage(L"Exception in processDoubleReplacing()!");
- }
- }
- }
- // Helper function to build the pointer arrays required by the VST process functions.
- template <typename buf_t>
- int32 PluginBridge::BuildProcessPointers(buf_t **(&inPointers), buf_t **(&outPointers))
- {
- MPT_ASSERT(m_processMem.Good());
- ProcessMsg &msg = *m_processMem.Data<ProcessMsg>();
- const size_t numPtrs = msg.numInputs + msg.numOutputs;
- m_sampleBuffers.resize(numPtrs, 0);
- if(numPtrs)
- {
- buf_t *offset = reinterpret_cast<buf_t *>(&msg + 1);
- for(size_t i = 0; i < numPtrs; i++)
- {
- m_sampleBuffers[i] = offset;
- offset += msg.sampleFrames;
- }
- inPointers = reinterpret_cast<buf_t **>(m_sampleBuffers.data());
- outPointers = inPointers + msg.numInputs;
- }
- return msg.sampleFrames;
- }
- // Send a message to the host.
- intptr_t PluginBridge::DispatchToHost(VstOpcodeToHost opcode, int32 index, intptr_t value, void *ptr, float opt)
- {
- std::vector<char> dispatchData(sizeof(DispatchMsg), 0);
- int64 ptrOut = 0;
- char *ptrC = static_cast<char *>(ptr);
- switch(opcode)
- {
- case audioMasterAutomate:
- case audioMasterVersion:
- case audioMasterCurrentId:
- case audioMasterIdle:
- case audioMasterPinConnected:
- break;
- case audioMasterWantMidi:
- return 1;
- case audioMasterGetTime:
- // VstTimeInfo* in [return value]
- if(m_isProcessing)
- {
- // During processing, read the cached time info. It won't change during the call
- // and we can save some valuable inter-process calls that way.
- return ToIntPtr<VstTimeInfo>(&m_sharedMem->timeInfo);
- }
- break;
- case audioMasterProcessEvents:
- // VstEvents* in [ptr]
- if(ptr == nullptr)
- return 0;
- TranslateVstEventsToBridge(dispatchData, *static_cast<VstEvents *>(ptr), m_otherPtrSize);
- ptrOut = dispatchData.size() - sizeof(DispatchMsg);
- // If we are currently processing, try to return the events as part of the process call
- if(m_isAudioThread && m_isProcessing && m_eventMem.Good())
- {
- auto *memBytes = m_eventMem.Data<char>();
- auto *eventBytes = m_eventMem.Data<char>() + sizeof(int32);
- int32 &memNumEvents = *m_eventMem.Data<int32>();
- const auto memEventsSize = BridgeVstEventsSize(memBytes);
- if(m_eventMem.Size() >= static_cast<size_t>(ptrOut) + memEventsSize)
- {
- // Enough shared memory for possibly pre-existing and new events; add new events at the end
- memNumEvents += static_cast<VstEvents *>(ptr)->numEvents;
- std::memcpy(eventBytes + memEventsSize, dispatchData.data() + sizeof(DispatchMsg) + sizeof(int32), static_cast<size_t>(ptrOut) - sizeof(int32));
- return 1;
- } else if(memNumEvents)
- {
- // Not enough memory; merge what we have and what we want to add so that it arrives in the correct order
- dispatchData.insert(dispatchData.begin() + sizeof(DispatchMsg) + sizeof(int32), eventBytes, eventBytes + memEventsSize);
- *reinterpret_cast<int32 *>(dispatchData.data() + sizeof(DispatchMsg)) += memNumEvents;
- memNumEvents = 0;
- ptrOut += memEventsSize;
- }
- }
- break;
- case audioMasterSetTime:
- case audioMasterTempoAt:
- case audioMasterGetNumAutomatableParameters:
- case audioMasterGetParameterQuantization:
- break;
- case audioMasterVendorSpecific:
- if(index != kVendorOpenMPT || value != kUpdateProcessingBuffer)
- {
- if(ptr != 0)
- {
- // Cannot translate this.
- return 0;
- }
- break;
- }
- [[fallthrough]];
- case audioMasterIOChanged:
- // We need to be sure that the new values are known to the master.
- if(m_nativeEffect != nullptr)
- {
- UpdateEffectStruct();
- CreateProcessingFile(dispatchData);
- ptrOut = dispatchData.size() - sizeof(DispatchMsg);
- }
- break;
- case audioMasterNeedIdle:
- m_needIdle = true;
- return 1;
- case audioMasterSizeWindow:
- if(m_window)
- {
- SetWindowPos(m_window, nullptr, 0, 0, index, static_cast<int>(value), SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
- }
- break;
- case audioMasterGetSampleRate:
- case audioMasterGetBlockSize:
- case audioMasterGetInputLatency:
- case audioMasterGetOutputLatency:
- break;
- case audioMasterGetPreviousPlug:
- case audioMasterGetNextPlug:
- // Don't even bother, this would explode :)
- return 0;
- case audioMasterWillReplaceOrAccumulate:
- case audioMasterGetCurrentProcessLevel:
- case audioMasterGetAutomationState:
- break;
- case audioMasterOfflineStart:
- case audioMasterOfflineRead:
- case audioMasterOfflineWrite:
- case audioMasterOfflineGetCurrentPass:
- case audioMasterOfflineGetCurrentMetaPass:
- // Currently not supported in OpenMPT
- return 0;
- case audioMasterSetOutputSampleRate:
- break;
- case audioMasterGetOutputSpeakerArrangement:
- case audioMasterGetInputSpeakerArrangement:
- // VstSpeakerArrangement* in [return value]
- ptrOut = sizeof(VstSpeakerArrangement);
- break;
- case audioMasterGetVendorString:
- case audioMasterGetProductString:
- // Name in [ptr]
- ptrOut = 256;
- break;
- case audioMasterGetVendorVersion:
- case audioMasterSetIcon:
- break;
- case audioMasterCanDo:
- // Name in [ptr]
- ptrOut = strlen(ptrC) + 1;
- dispatchData.insert(dispatchData.end(), ptrC, ptrC + ptrOut);
- break;
- case audioMasterGetLanguage:
- case audioMasterOpenWindow:
- case audioMasterCloseWindow:
- break;
- case audioMasterGetDirectory:
- // Name in [return value]
- ptrOut = 256;
- break;
- case audioMasterUpdateDisplay:
- case audioMasterBeginEdit:
- case audioMasterEndEdit:
- break;
- case audioMasterOpenFileSelector:
- // VstFileSelect* in [ptr]
- if(ptr != nullptr)
- {
- auto &fileSel = *static_cast<VstFileSelect *>(ptr);
- fileSel.returnMultiplePaths = nullptr;
- fileSel.numReturnPaths = 0;
- TranslateVstFileSelectToBridge(dispatchData, fileSel, m_otherPtrSize);
- ptrOut = dispatchData.size() - sizeof(DispatchMsg) + 65536; // enough space for return paths
- }
- break;
- case audioMasterCloseFileSelector:
- // VstFileSelect* in [ptr]
- if(auto *fileSel = static_cast<VstFileSelect *>(ptr); fileSel != nullptr && fileSel->reserved == 1)
- {
- fileSel->returnPath = nullptr;
- fileSel->returnMultiplePaths = nullptr;
- }
- m_fileSelectCache.clear();
- m_fileSelectCache.shrink_to_fit();
- return 1;
- case audioMasterEditFile:
- break;
- case audioMasterGetChunkFile:
- // Name in [ptr]
- ptrOut = 256;
- dispatchData.insert(dispatchData.end(), ptrC, ptrC + ptrOut);
- break;
- default:
- #ifdef MPT_BUILD_DEBUG
- if(ptr != nullptr)
- __debugbreak();
- #endif
- break;
- }
- if(ptrOut != 0)
- {
- // In case we only reserve space and don't copy stuff over...
- dispatchData.resize(sizeof(DispatchMsg) + static_cast<size_t>(ptrOut), 0);
- }
- uint32 extraSize = static_cast<uint32>(dispatchData.size() - sizeof(DispatchMsg));
- // Create message header
- BridgeMessage *msg = reinterpret_cast<BridgeMessage *>(dispatchData.data());
- msg->Dispatch(opcode, index, value, ptrOut, opt, extraSize);
- const bool useAuxMem = dispatchData.size() > sizeof(BridgeMessage);
- MappedMemory auxMem;
- if(useAuxMem)
- {
- // Extra data doesn't fit in message - use secondary memory
- wchar_t auxMemName[64];
- static_assert(sizeof(DispatchMsg) + sizeof(auxMemName) <= sizeof(BridgeMessage), "Check message sizes, this will crash!");
- swprintf(auxMemName, std::size(auxMemName), L"Local\\openmpt-%u-auxmem-%u", GetCurrentProcessId(), GetCurrentThreadId());
- if(auxMem.Create(auxMemName, extraSize))
- {
- // Move message data to shared memory and then move shared memory name to message data
- std::memcpy(auxMem.Data(), &dispatchData[sizeof(DispatchMsg)], extraSize);
- std::memcpy(&dispatchData[sizeof(DispatchMsg)], auxMemName, sizeof(auxMemName));
- } else
- {
- return 0;
- }
- }
- //std::cout << "about to dispatch " << opcode << " to host...";
- //std::flush(std::cout);
- if(!SendToHost(*msg))
- {
- return 0;
- }
- //std::cout << "done." << std::endl;
- const DispatchMsg *resultMsg = &msg->dispatch;
- const char *extraData = useAuxMem ? auxMem.Data<const char>() : reinterpret_cast<const char *>(resultMsg + 1);
- // Post-fix some opcodes
- switch(opcode)
- {
- case audioMasterGetTime:
- // VstTimeInfo* in [return value]
- return ToIntPtr<VstTimeInfo>(&m_sharedMem->timeInfo);
- case audioMasterGetOutputSpeakerArrangement:
- case audioMasterGetInputSpeakerArrangement:
- // VstSpeakerArrangement* in [return value]
- std::memcpy(&m_host2PlugMem.speakerArrangement, extraData, sizeof(VstSpeakerArrangement));
- return ToIntPtr<VstSpeakerArrangement>(&m_host2PlugMem.speakerArrangement);
- case audioMasterGetVendorString:
- case audioMasterGetProductString:
- // Name in [ptr]
- strcpy(ptrC, extraData);
- break;
- case audioMasterGetDirectory:
- // Name in [return value]
- strncpy(m_host2PlugMem.name, extraData, std::size(m_host2PlugMem.name) - 1);
- m_host2PlugMem.name[std::size(m_host2PlugMem.name) - 1] = 0;
- return ToIntPtr<char>(m_host2PlugMem.name);
- case audioMasterOpenFileSelector:
- if(resultMsg->result != 0 && ptr != nullptr)
- {
- TranslateBridgeToVstFileSelect(m_fileSelectCache, extraData, static_cast<size_t>(ptrOut));
- auto &fileSel = *static_cast<VstFileSelect *>(ptr);
- const auto &fileSelResult = *reinterpret_cast<const VstFileSelect *>(m_fileSelectCache.data());
- if((fileSel.command == kVstFileLoad || fileSel.command == kVstFileSave))
- {
- if(FourCC("VOPM") == m_nativeEffect->uniqueID)
- {
- fileSel.sizeReturnPath = _MAX_PATH;
- }
- } else if(fileSel.command == kVstDirectorySelect)
- {
- if(FourCC("VSTr") == m_nativeEffect->uniqueID && 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 = static_cast<int32>(strlen(fileSelResult.returnPath) + 1);
- }
- }
- fileSel.numReturnPaths = fileSelResult.numReturnPaths;
- fileSel.reserved = 1;
- if(fileSel.command == kVstMultipleFilesLoad)
- {
- fileSel.returnMultiplePaths = fileSelResult.returnMultiplePaths;
- } else if(fileSel.returnPath != nullptr && fileSel.sizeReturnPath != 0)
- {
- // Plugin provides memory
- const auto len = strnlen(fileSelResult.returnPath, fileSel.sizeReturnPath - 1);
- strncpy(fileSel.returnPath, fileSelResult.returnPath, len);
- fileSel.returnPath[len] = '\0';
- fileSel.reserved = 0;
- } else
- {
- fileSel.returnPath = fileSelResult.returnPath;
- fileSel.sizeReturnPath = fileSelResult.sizeReturnPath;
- }
- }
- break;
- case audioMasterGetChunkFile:
- // Name in [ptr]
- strcpy(ptrC, extraData);
- break;
- }
- return static_cast<intptr_t>(resultMsg->result);
- }
- // Helper function for sending messages to the host.
- intptr_t VSTCALLBACK PluginBridge::MasterCallback(AEffect *effect, VstOpcodeToHost opcode, int32 index, intptr_t value, void *ptr, float opt)
- {
- PluginBridge *instance = (effect != nullptr && effect->reservedForHost1 != nullptr) ? static_cast<PluginBridge *>(effect->reservedForHost1) : PluginBridge::m_latestInstance;
- return instance->DispatchToHost(opcode, index, value, ptr, opt);
- }
- LRESULT CALLBACK PluginBridge::WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
- {
- PluginBridge *that = nullptr;
- if(hwnd == m_communicationWindow && wParam < m_plugins.size())
- that = static_cast<PluginBridge *>(m_plugins[wParam]);
- else // Editor windows
- that = reinterpret_cast<PluginBridge *>(GetWindowLongPtr(hwnd, GWLP_USERDATA));
- if(that == nullptr)
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- switch(uMsg)
- {
- case WM_ERASEBKGND:
- // Pretend that we erased the background
- return 1;
- case WM_SIZE:
- {
- // For plugins that change their size but do not notify the host, e.g. Roland D-50
- RECT rect{0, 0, 0, 0};
- GetClientRect(hwnd, &rect);
- const int width = rect.right - rect.left, height = rect.bottom - rect.top;
- if(width > 0 && height > 0 && (width != that->m_windowWidth || height != that->m_windowHeight))
- {
- that->m_windowWidth = width;
- that->m_windowHeight = height;
- that->DispatchToHost(audioMasterSizeWindow, width, height, nullptr, 0.0f);
- }
- break;
- }
- case WM_BRIDGE_MESSAGE_TO_BRIDGE:
- that->ParseNextMessage(static_cast<int>(lParam));
- return WM_BRIDGE_SUCCESS;
- case WM_BRIDGE_DELETE_PLUGIN:
- delete that;
- return 0;
- }
- return DefWindowProc(hwnd, uMsg, wParam, lParam);
- }
- void PluginBridge::IdleTimerProc(HWND, UINT, UINT_PTR idTimer, DWORD)
- {
- if(idTimer != TIMER_IDLE)
- return;
- for(auto *plugin : m_plugins)
- {
- auto *that = static_cast<PluginBridge *>(plugin);
- if(that == nullptr)
- continue;
- if(that->m_needIdle)
- {
- that->Dispatch(effIdle, 0, 0, nullptr, 0.0f);
- that->m_needIdle = false;
- }
- if(that->m_window)
- {
- that->Dispatch(effEditIdle, 0, 0, nullptr, 0.0f);
- }
- }
- }
- OPENMPT_NAMESPACE_END
|