Improved: Device strings like "Output A5" no longer accidentally match GUIDs. Now, only strings containing { will match GUIDs.
508 lines
15 KiB
C++
508 lines
15 KiB
C++
/*
|
|
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 <ShlObj.h>
|
|
#include <fstream>
|
|
#include <algorithm>
|
|
#include <KsMedia.h>
|
|
#include <shellapi.h>
|
|
#include <TlHelp32.h>
|
|
#include <Winternl.h>
|
|
#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<shared_ptr<AbstractAPOInfo> >& 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<AbstractAPOInfo>& 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<AbstractAPOInfo> info = *list.insert(list.begin() + i, make_shared<VoicemeeterAPOInfo>(sstream.str(), true));
|
|
if (info->isInstalled())
|
|
anyInstalled = true;
|
|
}
|
|
|
|
if (defaultDevice)
|
|
{
|
|
for (unsigned i = 0; i < outputCount; i++)
|
|
{
|
|
shared_ptr<VoicemeeterAPOInfo>& info = (shared_ptr<VoicemeeterAPOInfo>&)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<wstring> args = splitArgs(argString);
|
|
|
|
int i = 0;
|
|
for (wstring arg : args)
|
|
{
|
|
shared_ptr<AbstractAPOInfo> info = *list.insert(list.begin() + i, make_shared<VoicemeeterAPOInfo>(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<wstring> 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<wstring> args = splitArgs(argString);
|
|
vector<wstring>::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<wstring> args = splitArgs(argString);
|
|
vector<wstring>::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<wstring> VoicemeeterAPOInfo::splitArgs(const wstring& argString)
|
|
{
|
|
vector<wstring> 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<wstring>& 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<wstring> 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<wstring> 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;
|
|
}
|
|
}
|