/* This file is part of EqualizerAPO, a system-wide equalizer. Copyright (C) 2017 Jonas Thedering This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. */ #include "stdafx.h" #include #include #include #include #include #include #include #include "helpers/RegistryHelper.h" #include "VoicemeeterAPOInfo.h" using namespace std; #define voicemeeterKeyPath L"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\VB:Voicemeeter {17359A74-1236-5467}" #define voicemeeterWowKeyPath L"HKEY_LOCAL_MACHINE\\SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\VB:Voicemeeter {17359A74-1236-5467}" #define uninstallStringValueName L"UninstallString" static const wchar_t* startupFilename = L"Equalizer APO Voicemeeter Client.lnk"; static const wchar_t* clientFilename = L"VoicemeeterClient.exe"; static const wchar_t* voicemeeterClientKeyPath = USER_REGPATH L"\\Voicemeeter Client"; static const wchar_t* sampleRateValueName = L"sampleRate"; typedef NTSTATUS (NTAPI * pfnNtQueryInformationProcess)( IN HANDLE ProcessHandle, IN PROCESSINFOCLASS ProcessInformationClass, OUT PVOID ProcessInformation, IN ULONG ProcessInformationLength, OUT PULONG ReturnLength OPTIONAL ); void VoicemeeterAPOInfo::prependInfos(vector>& list) { wstring voicemeeterDirectory; if (RegistryHelper::keyExists(voicemeeterKeyPath)) voicemeeterDirectory = RegistryHelper::readValue(voicemeeterKeyPath, uninstallStringValueName); else if (RegistryHelper::keyExists(voicemeeterWowKeyPath)) voicemeeterDirectory = RegistryHelper::readValue(voicemeeterWowKeyPath, uninstallStringValueName); if (voicemeeterDirectory.length() > 0) { size_t index = voicemeeterDirectory.find_last_of(L'\\'); wstring setupFilename = voicemeeterDirectory.substr(index + 1); long voicemeeterType = 1; if (setupFilename == L"VoicemeeterProSetup.exe") voicemeeterType = 2;// banana else if (setupFilename == L"Voicemeeter8Setup.exe") voicemeeterType = 3;// potato unsigned outputCount; if (voicemeeterType == 3) outputCount = 5; else if (voicemeeterType == 2) outputCount = 3; else outputCount = 1; bool defaultDevice = false; list.erase(remove_if(list.begin(), list.end(), [&defaultDevice](shared_ptr& info) { if (info->getDeviceName().find(L"VB-Audio VoiceMeeter") != wstring::npos) { if (info->isDefaultDevice()) defaultDevice = true; return true; } return false; }), list.end()); bool anyInstalled = false; for (unsigned i = 0; i < outputCount; i++) { wstringstream sstream; sstream << "Output A" << (i + 1); shared_ptr info = *list.insert(list.begin() + i, make_shared(sstream.str(), true)); if (info->isInstalled()) anyInstalled = true; } if (defaultDevice) { for (unsigned i = 0; i < outputCount; i++) { shared_ptr& info = (shared_ptr&)list[i]; if (!anyInstalled || info->isInstalled()) { info->defaultDevice = true; break; } } } } else { // Voicemeeter was uninstalled but Voicemeeter Client might still be installed wstring startupFilePath = getStartupPath(); wstring argString = getLinkArgs(startupFilePath); vector args = splitArgs(argString); int i = 0; for (wstring arg : args) { shared_ptr info = *list.insert(list.begin() + i, make_shared(arg, false)); i++; } } } VoicemeeterAPOInfo::VoicemeeterAPOInfo(const wstring& connectionName, bool voicemeeterInstalled) : connectionName(connectionName), voicemeeterInstalled(voicemeeterInstalled) { wstring startupFilePath = getStartupPath(); wstring path; wstring argString = getLinkArgs(startupFilePath, &path); vector args = splitArgs(argString); installed = find(args.begin(), args.end(), connectionName) != args.end(); sampleRate = 48000; try { if (RegistryHelper::keyExists(voicemeeterClientKeyPath) && RegistryHelper::valueExists(voicemeeterClientKeyPath, sampleRateValueName)) sampleRate = (unsigned)RegistryHelper::readDWORDValue(voicemeeterClientKeyPath, sampleRateValueName); } catch (RegistryException e) { // ignore } wstring clientPath = getClientPath(); changes = (path != clientPath); } wstring VoicemeeterAPOInfo::getConnectionName() const { return connectionName; } wstring VoicemeeterAPOInfo::getDeviceName() const { return L"Voicemeeter"; } wstring VoicemeeterAPOInfo::getDeviceGuid() const { return L""; } wstring VoicemeeterAPOInfo::getDeviceString() const { return getConnectionName() + L" " + getDeviceName(); } unsigned VoicemeeterAPOInfo::getChannelCount() const { return 8; } unsigned VoicemeeterAPOInfo::getSampleRate() const { return sampleRate; } unsigned long VoicemeeterAPOInfo::getChannelMask() const { return KSAUDIO_SPEAKER_7POINT1_SURROUND; } bool VoicemeeterAPOInfo::isInput() const { return false; } bool VoicemeeterAPOInfo::isInstalled() const { return installed; } bool VoicemeeterAPOInfo::canBeUpgraded() const { return false; } bool VoicemeeterAPOInfo::hasChanges() const { return changes; } bool VoicemeeterAPOInfo::isExperimental() const { return false; } bool VoicemeeterAPOInfo::isEnhancementsDisabled() const { return false; } bool VoicemeeterAPOInfo::isDefaultDevice() const { return defaultDevice; } bool VoicemeeterAPOInfo::isVoicemeeterInstalled() const { return voicemeeterInstalled; } void VoicemeeterAPOInfo::install() { wstring startupFilePath = getStartupPath(); wstring argString = getLinkArgs(startupFilePath); vector args = splitArgs(argString); vector::iterator it = find(args.begin(), args.end(), connectionName); if (it == args.end()) args.push_back(connectionName); sort(args.begin(), args.end()); argString = joinArgs(args); wstring clientPath = getClientPath(); createLink(startupFilePath, clientPath, argString); } void VoicemeeterAPOInfo::uninstall() { wstring startupFilePath = getStartupPath(); wstring argString = getLinkArgs(startupFilePath); vector args = splitArgs(argString); vector::iterator it = find(args.begin(), args.end(), connectionName); if (it != args.end()) args.erase(it); if (args.size() > 0) { sort(args.begin(), args.end()); argString = joinArgs(args); wchar_t filename[MAX_PATH]; GetModuleFileNameW(NULL, filename, ARRAYSIZE(filename)); PathRemoveFileSpecW(filename); wstring clientPath = filename; clientPath = clientPath + L"\\" + clientFilename; createLink(startupFilePath, clientPath, argString); } else { DeleteFileW(startupFilePath.c_str()); } } void VoicemeeterAPOInfo::reinstall() { uninstall(); install(); } wstring VoicemeeterAPOInfo::getStartupPath() { PWSTR startupPath; SHGetKnownFolderPath(FOLDERID_Startup, KF_FLAG_DONT_UNEXPAND, NULL, &startupPath); wstring result(startupPath); CoTaskMemFree(startupPath); return result + L"\\" + startupFilename; } wstring VoicemeeterAPOInfo::getClientPath() { wchar_t filename[MAX_PATH]; GetModuleFileNameW(NULL, filename, ARRAYSIZE(filename)); PathRemoveFileSpecW(filename); wstring clientPath = filename; clientPath = clientPath + L"\\" + clientFilename; return clientPath; } void VoicemeeterAPOInfo::createLink(const wstring& lnkPath, const wstring& path, const wstring& args) { IShellLink* shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&shellLink); if (SUCCEEDED(hr)) { shellLink->SetPath(path.c_str()); shellLink->SetArguments(args.c_str()); IPersistFile* persistFile; hr = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); if (SUCCEEDED(hr)) { hr = persistFile->Save(lnkPath.c_str(), TRUE); persistFile->Release(); } shellLink->Release(); } } wstring VoicemeeterAPOInfo::getLinkArgs(const wstring& lnkPath, wstring* path) { wstring result; IShellLink* shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, (LPVOID*)&shellLink); if (SUCCEEDED(hr)) { IPersistFile* persistFile; hr = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); if (SUCCEEDED(hr)) { hr = persistFile->Load(lnkPath.c_str(), STGM_READ); if (SUCCEEDED(hr)) { wchar_t buf[MAX_PATH]; hr = shellLink->GetArguments(buf, ARRAYSIZE(buf)); if (SUCCEEDED(hr)) result = buf; if (path != NULL) { hr = shellLink->GetPath(buf, ARRAYSIZE(buf), NULL, 0); if (SUCCEEDED(hr)) *path = buf; } } persistFile->Release(); } shellLink->Release(); } return result; } vector VoicemeeterAPOInfo::splitArgs(const wstring& argString) { vector result; if (argString.length() > 0) { int argc; wchar_t** argv = CommandLineToArgvW(argString.c_str(), &argc); for (int i = 0; i < argc; i++) result.push_back(argv[i]); LocalFree(argv); } return result; } wstring VoicemeeterAPOInfo::joinArgs(const vector& args) { wstring result; for (const wstring& arg : args) { if (result.length() > 0) result += L" "; if (arg.find(' ') != wstring::npos) result += L"\"" + arg + L"\""; else result += arg; } return result; } void VoicemeeterAPOInfo::ensureVoicemeeterClientRunning() { wstring startupFilePath = getStartupPath(); wstring argString = getLinkArgs(startupFilePath); vector args = splitArgs(argString); wstring clientPath = getClientPath(); HANDLE tokenHandle; if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &tokenHandle)) throw RegistryException(L"Error in OpenProcessToken while taking ownership"); LUID luid; if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) throw RegistryException(L"Error in LookupPrivilegeValue while taking ownership"); TOKEN_PRIVILEGES tp; tp.PrivilegeCount = 1; tp.Privileges[0].Luid = luid; tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; if (!AdjustTokenPrivileges(tokenHandle, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL)) throw RegistryException(L"Error in AdjustTokenPrivileges while taking ownership"); HMODULE module = LoadLibraryW(L"ntdll.dll"); if (module == NULL) throw exception("Could not load ntdll.dll"); SCOPE_EXIT {FreeLibrary(module);}; pfnNtQueryInformationProcess NtQueryInformationProcess = (pfnNtQueryInformationProcess)GetProcAddress(module, "NtQueryInformationProcess"); if (NtQueryInformationProcess == NULL) throw exception("Function NtQueryInformationProcess not found in ntdll.dll"); HANDLE snapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); if (snapshotHandle == INVALID_HANDLE_VALUE) throw exception("Could not take a snapshot of all processes"); SCOPE_EXIT {CloseHandle(snapshotHandle);}; bool matchingProcessExists = false; PROCESSENTRY32W entry; entry.dwSize = sizeof(PROCESSENTRY32W); bool loop = Process32FirstW(snapshotHandle, &entry) != 0; while (loop) { if (wcscmp(entry.szExeFile, clientFilename) == 0) { HANDLE processHandle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | PROCESS_TERMINATE, FALSE, entry.th32ProcessID); if (processHandle == INVALID_HANDLE_VALUE) throw exception("Could not open process"); SCOPE_EXIT {CloseHandle(processHandle);}; PROCESS_BASIC_INFORMATION basicInformation; NTSTATUS status = NtQueryInformationProcess(processHandle, ProcessBasicInformation, &basicInformation, sizeof(basicInformation), NULL); if (status < 0) throw exception("Could not query process information"); _PEB peb; if (!ReadProcessMemory(processHandle, basicInformation.PebBaseAddress, &peb, sizeof(peb), NULL)) throw exception("Could not read peb from process memory"); RTL_USER_PROCESS_PARAMETERS processParams; if (!ReadProcessMemory(processHandle, peb.ProcessParameters, &processParams, sizeof(processParams), NULL)) throw exception("Could not read process parameters from process memory"); wchar_t* cmdLineBuf = new wchar_t[processParams.CommandLine.Length / sizeof(wchar_t)]; SCOPE_EXIT {delete cmdLineBuf;}; if (!ReadProcessMemory(processHandle, processParams.CommandLine.Buffer, cmdLineBuf, processParams.CommandLine.Length, NULL)) throw exception("Could not read command line from process memory"); wstring cmdLine(cmdLineBuf, processParams.CommandLine.Length / sizeof(wchar_t)); vector processArgs = splitArgs(cmdLine); wstring path = processArgs.front(); processArgs.erase(processArgs.begin()); if (path != clientPath || processArgs != args) closeProcess(entry.th32ProcessID); else matchingProcessExists = true; } loop = Process32NextW(snapshotHandle, &entry) != 0; } if (!matchingProcessExists && !args.empty()) { ShellExecuteW(NULL, NULL, clientPath.c_str(), argString.c_str(), NULL, SW_SHOWDEFAULT); } } void VoicemeeterAPOInfo::saveVoicemeeterSampleRate(unsigned sampleRate) { try { RegistryHelper::createKey(voicemeeterClientKeyPath); RegistryHelper::writeDWORDValue(voicemeeterClientKeyPath, sampleRateValueName, sampleRate); } catch (RegistryException e) { // ignore } } void VoicemeeterAPOInfo::closeProcess(unsigned long processId) { HANDLE snapshotHandle = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); if (snapshotHandle == INVALID_HANDLE_VALUE) throw exception("Could not take a snapshot of all processes"); SCOPE_EXIT {CloseHandle(snapshotHandle);}; THREADENTRY32 entry; entry.dwSize = sizeof(THREADENTRY32); bool loop = Thread32First(snapshotHandle, &entry) != 0; while (loop) { if (entry.th32OwnerProcessID == processId) // will just fail for threads not having a message queue PostThreadMessageW(entry.th32ThreadID, WM_QUIT, 0, 0); loop = Thread32Next(snapshotHandle, &entry) != 0; } }