mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
gitadora: (arena model) simulate Realtek device, fix asio redirect hooks (#732)
## Link to GitHub Issue or related Pull Request, if one exists #730, #718 ## Description of change Part 1) When ASIO is in use, the game also looks for a Realtek device so that it can open a WASAPI Exclusive mode stream for headphones. Add an option to fake that, in case the user doesn't have a Realtek device. Part 2) `-gdaasio` wasn't workign properly - fix the logic. ## Testing Seems to work with FlexASIO, and Xonar with no real Realtek device.
This commit is contained in:
@@ -503,6 +503,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/audio/backends/mmdevice/device.cpp
|
hooks/audio/backends/mmdevice/device.cpp
|
||||||
hooks/audio/backends/mmdevice/device_collection.cpp
|
hooks/audio/backends/mmdevice/device_collection.cpp
|
||||||
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
hooks/audio/backends/mmdevice/device_enumerator.cpp
|
||||||
|
hooks/audio/backends/mmdevice/null_device.cpp
|
||||||
|
hooks/audio/backends/mmdevice/null_discard_backend.cpp
|
||||||
hooks/audio/backends/wasapi/audio_client.cpp
|
hooks/audio/backends/wasapi/audio_client.cpp
|
||||||
hooks/audio/backends/wasapi/audio_render_client.cpp
|
hooks/audio/backends/wasapi/audio_render_client.cpp
|
||||||
hooks/audio/backends/wasapi/downmix.cpp
|
hooks/audio/backends/wasapi/downmix.cpp
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ namespace games::gitadora {
|
|||||||
|
|
||||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||||
|
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
static decltype(RegOpenKeyExA) *RegOpenKeyExA_orig = nullptr;
|
||||||
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
static decltype(RegQueryValueExA) *RegQueryValueExA_orig = nullptr;
|
||||||
|
|
||||||
@@ -30,7 +31,6 @@ namespace games::gitadora {
|
|||||||
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
static LONG WINAPI RegOpenKeyExA_hook(HKEY hKey, LPCSTR lpSubKey, DWORD ulOptions, REGSAM samDesired,
|
||||||
PHKEY phkResult)
|
PHKEY phkResult)
|
||||||
{
|
{
|
||||||
// ASIO\XONAR redirect to ASIO\<configured>
|
|
||||||
if (ASIO_DRIVER.has_value() &&
|
if (ASIO_DRIVER.has_value() &&
|
||||||
lpSubKey != nullptr &&
|
lpSubKey != nullptr &&
|
||||||
phkResult != nullptr &&
|
phkResult != nullptr &&
|
||||||
@@ -60,7 +60,10 @@ namespace games::gitadora {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// open of the ASIO root: hand back a sentinel
|
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
|
||||||
if (ASIO_DRIVER.has_value() &&
|
if (ASIO_DRIVER.has_value() &&
|
||||||
lpSubKey != nullptr &&
|
lpSubKey != nullptr &&
|
||||||
phkResult != nullptr &&
|
phkResult != nullptr &&
|
||||||
@@ -68,10 +71,10 @@ namespace games::gitadora {
|
|||||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||||
{
|
{
|
||||||
*phkResult = PARENT_ASIO_REG_HANDLE;
|
*phkResult = PARENT_ASIO_REG_HANDLE;
|
||||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, &real_asio_reg_handle);
|
return RegOpenKeyA_orig(hKey, lpSubKey, &real_asio_reg_handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
return RegOpenKeyExA_orig(hKey, lpSubKey, ulOptions, samDesired, phkResult);
|
return RegOpenKeyA_orig(hKey, lpSubKey, phkResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
static LONG WINAPI RegEnumKeyA_hook(HKEY hKey, DWORD dwIndex, LPSTR lpName, DWORD cchName) {
|
||||||
@@ -158,6 +161,8 @@ namespace games::gitadora {
|
|||||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||||
RegEnumKeyA_orig = detour::iat_try(
|
RegEnumKeyA_orig = detour::iat_try(
|
||||||
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
"RegEnumKeyA", RegEnumKeyA_hook, avs::game::DLL_INSTANCE);
|
||||||
|
RegOpenKeyA_orig = detour::iat_try(
|
||||||
|
"RegOpenKeyA", RegOpenKeyA_hook, avs::game::DLL_INSTANCE);
|
||||||
RegOpenKeyExA_orig = detour::iat_try(
|
RegOpenKeyExA_orig = detour::iat_try(
|
||||||
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
"RegOpenKeyExA", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||||
RegQueryValueExA_orig = detour::iat_try(
|
RegQueryValueExA_orig = detour::iat_try(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <ksmedia.h>
|
#include <ksmedia.h>
|
||||||
|
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
|
#include "hooks/audio/audio.h"
|
||||||
#include "hooks/audio/mme.h"
|
#include "hooks/audio/mme.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ namespace hooks::audio {
|
|||||||
bool USE_DUMMY = false;
|
bool USE_DUMMY = false;
|
||||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||||
std::optional<Backend> BACKEND = std::nullopt;
|
std::optional<Backend> BACKEND = std::nullopt;
|
||||||
|
bool FAKE_REALTEK_RENDER_DEVICE = false;
|
||||||
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
|
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
|
||||||
std::string ASIO_DRIVER_NAME = "";
|
std::string ASIO_DRIVER_NAME = "";
|
||||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||||
|
|||||||
@@ -40,6 +40,11 @@ namespace hooks::audio {
|
|||||||
extern bool USE_DUMMY;
|
extern bool USE_DUMMY;
|
||||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||||
extern std::optional<Backend> BACKEND;
|
extern std::optional<Backend> BACKEND;
|
||||||
|
|
||||||
|
// when true, a synthetic "Realtek" render endpoint is injected into device enumeration that
|
||||||
|
// discards all audio. used by gitadora arena, whose device search crashes when no render
|
||||||
|
// endpoint reports a "Realtek" friendly name.
|
||||||
|
extern bool FAKE_REALTEK_RENDER_DEVICE;
|
||||||
extern std::optional<size_t> ASIO_DRIVER_ID;
|
extern std::optional<size_t> ASIO_DRIVER_ID;
|
||||||
extern std::string ASIO_DRIVER_NAME;
|
extern std::string ASIO_DRIVER_NAME;
|
||||||
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
|
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "device_collection.h"
|
#include "device_collection.h"
|
||||||
#include "device.h"
|
#include "device.h"
|
||||||
|
#include "null_device.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
@@ -28,17 +29,48 @@ ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
|
|||||||
return refs;
|
return refs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool WrappedIMMDeviceCollection::should_inject_fake_realtek() const {
|
||||||
|
return null_render_device_enabled()
|
||||||
|
&& (data_flow == eRender || data_flow == eAll);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
|
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::GetCount(UINT *pcDevices) {
|
||||||
|
// when active, hide all real devices and present only the synthetic one
|
||||||
|
if (should_inject_fake_realtek()) {
|
||||||
|
if (pcDevices == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*pcDevices = 1;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
return pReal->GetCount(pcDevices);
|
return pReal->GetCount(pcDevices);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
|
HRESULT STDMETHODCALLTYPE WrappedIMMDeviceCollection::Item(UINT nDevice, IMMDevice **ppDevice) {
|
||||||
|
if (ppDevice == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when active, the only device in the collection is the synthetic fake Realtek
|
||||||
|
// render device; all real devices are hidden
|
||||||
|
if (should_inject_fake_realtek()) {
|
||||||
|
if (nDevice != 0) {
|
||||||
|
return E_INVALIDARG;
|
||||||
|
}
|
||||||
|
log_info("audio", "WrappedIMMDeviceCollection::Item[{}] -> synthetic fake Realtek render device", nDevice);
|
||||||
|
*ppDevice = new NullMMDevice();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
|
log_info("audio", "WrappedIMMDeviceCollection::Item[{}]", nDevice);
|
||||||
|
|
||||||
// call original
|
// call original
|
||||||
const auto hr = pReal->Item(nDevice, ppDevice);
|
const auto hr = pReal->Item(nDevice, ppDevice);
|
||||||
|
|
||||||
// wrap interface
|
// wrap interface
|
||||||
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
if (SUCCEEDED(hr) && *ppDevice != nullptr) {
|
||||||
|
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
||||||
|
}
|
||||||
return hr;
|
return hr;
|
||||||
}
|
}
|
||||||
@@ -4,7 +4,8 @@
|
|||||||
#include <mmdeviceapi.h>
|
#include <mmdeviceapi.h>
|
||||||
|
|
||||||
struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
||||||
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) {
|
WrappedIMMDeviceCollection(IMMDeviceCollection *orig, EDataFlow dataFlow)
|
||||||
|
: pReal(orig), data_flow(dataFlow) {
|
||||||
}
|
}
|
||||||
|
|
||||||
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
|
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
|
||||||
@@ -24,5 +25,9 @@ struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
|||||||
#pragma endregion
|
#pragma endregion
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// whether the synthetic fake Realtek render device should be appended to this collection
|
||||||
|
bool should_inject_fake_realtek() const;
|
||||||
|
|
||||||
IMMDeviceCollection *const pReal;
|
IMMDeviceCollection *const pReal;
|
||||||
|
const EDataFlow data_flow;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
|
|||||||
{
|
{
|
||||||
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
|
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
|
||||||
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
|
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
|
||||||
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices);
|
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices, dataFlow);
|
||||||
}
|
}
|
||||||
return hr;
|
return hr;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,195 @@
|
|||||||
|
#include "null_device.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include <audioclient.h>
|
||||||
|
|
||||||
|
#include "hooks/audio/audio.h"
|
||||||
|
#include "hooks/audio/audio_private.h"
|
||||||
|
#include "hooks/audio/backends/wasapi/dummy_audio_client.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
#include "null_discard_backend.h"
|
||||||
|
|
||||||
|
// friendly name reported by the synthetic device. must contain "Realtek" so the
|
||||||
|
// gitadora arena device search matches it.
|
||||||
|
static const wchar_t NULL_DEVICE_FRIENDLY_NAME[] = L"Realtek High Definition Audio";
|
||||||
|
|
||||||
|
// arbitrary identifier reported by the synthetic device.
|
||||||
|
static const wchar_t NULL_DEVICE_ID[] = L"{spice2x-null-render-device}";
|
||||||
|
|
||||||
|
// PKEY_Device_FriendlyName, hardcoded to avoid pulling in functiondiscoverykeys_devpkey.h
|
||||||
|
static const PROPERTYKEY PKEY_DEVICE_FRIENDLY_NAME_LOCAL = {
|
||||||
|
{ 0xa45c254e, 0xdf1c, 0x4efd, { 0x80, 0x20, 0x67, 0xd1, 0x46, 0xa8, 0x50, 0xe0 } },
|
||||||
|
14
|
||||||
|
};
|
||||||
|
|
||||||
|
bool null_render_device_enabled() {
|
||||||
|
return hooks::audio::FAKE_REALTEK_RENDER_DEVICE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// duplicate a wide string into CoTaskMem so the caller can free it with
|
||||||
|
// CoTaskMemFree / PropVariantClear as the COM API contract requires.
|
||||||
|
static LPWSTR co_task_wcsdup(const wchar_t *src) {
|
||||||
|
const size_t bytes = (wcslen(src) + 1) * sizeof(wchar_t);
|
||||||
|
auto *dst = static_cast<LPWSTR>(CoTaskMemAlloc(bytes));
|
||||||
|
if (dst != nullptr) {
|
||||||
|
memcpy(dst, src, bytes);
|
||||||
|
}
|
||||||
|
return dst;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// minimal IPropertyStore that only answers PKEY_Device_FriendlyName.
|
||||||
|
struct NullPropertyStore : IPropertyStore {
|
||||||
|
std::atomic<ULONG> ref_cnt = 1;
|
||||||
|
|
||||||
|
virtual ~NullPropertyStore() = default;
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override {
|
||||||
|
if (ppvObj == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(IPropertyStore)) {
|
||||||
|
this->AddRef();
|
||||||
|
*ppvObj = this;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppvObj = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override {
|
||||||
|
return ++this->ref_cnt;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override {
|
||||||
|
const ULONG refs = --this->ref_cnt;
|
||||||
|
if (refs == 0) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE GetCount(DWORD *cProps) override {
|
||||||
|
if (cProps == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*cProps = 1;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE GetAt(DWORD iProp, PROPERTYKEY *pkey) override {
|
||||||
|
if (pkey == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
if (iProp != 0) {
|
||||||
|
return E_INVALIDARG;
|
||||||
|
}
|
||||||
|
*pkey = PKEY_DEVICE_FRIENDLY_NAME_LOCAL;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE GetValue(REFPROPERTYKEY key, PROPVARIANT *pv) override {
|
||||||
|
if (pv == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
PropVariantInit(pv);
|
||||||
|
if (key.fmtid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.fmtid
|
||||||
|
&& key.pid == PKEY_DEVICE_FRIENDLY_NAME_LOCAL.pid) {
|
||||||
|
pv->pwszVal = co_task_wcsdup(NULL_DEVICE_FRIENDLY_NAME);
|
||||||
|
if (pv->pwszVal == nullptr) {
|
||||||
|
return E_OUTOFMEMORY;
|
||||||
|
}
|
||||||
|
pv->vt = VT_LPWSTR;
|
||||||
|
}
|
||||||
|
// unknown keys are returned as VT_EMPTY / S_OK
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE SetValue(REFPROPERTYKEY, REFPROPVARIANT) override {
|
||||||
|
return STG_E_ACCESSDENIED;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE Commit() override {
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma region IUnknown
|
||||||
|
HRESULT STDMETHODCALLTYPE NullMMDevice::QueryInterface(REFIID riid, void **ppvObj) {
|
||||||
|
if (ppvObj == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
if (riid == __uuidof(IUnknown) || riid == __uuidof(IMMDevice)) {
|
||||||
|
this->AddRef();
|
||||||
|
*ppvObj = this;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
*ppvObj = nullptr;
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE NullMMDevice::AddRef() {
|
||||||
|
return ++this->ref_cnt;
|
||||||
|
}
|
||||||
|
ULONG STDMETHODCALLTYPE NullMMDevice::Release() {
|
||||||
|
const ULONG refs = --this->ref_cnt;
|
||||||
|
if (refs == 0) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
#pragma region IMMDevice
|
||||||
|
HRESULT STDMETHODCALLTYPE NullMMDevice::Activate(
|
||||||
|
REFIID iid,
|
||||||
|
DWORD,
|
||||||
|
PROPVARIANT *,
|
||||||
|
void **ppInterface)
|
||||||
|
{
|
||||||
|
if (ppInterface == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*ppInterface = nullptr;
|
||||||
|
|
||||||
|
log_info("audio::null", "NullMMDevice::Activate {}", guid2s(iid));
|
||||||
|
|
||||||
|
if (iid == IID_IAudioClient) {
|
||||||
|
|
||||||
|
// release any previously persisted client
|
||||||
|
if (hooks::audio::CLIENT != nullptr) {
|
||||||
|
hooks::audio::CLIENT->Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto *client = static_cast<IAudioClient *>(new DummyIAudioClient(new NullDiscardBackend()));
|
||||||
|
*ppInterface = client;
|
||||||
|
|
||||||
|
// persist the audio client
|
||||||
|
hooks::audio::CLIENT = client;
|
||||||
|
hooks::audio::CLIENT->AddRef();
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
return E_NOINTERFACE;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE NullMMDevice::OpenPropertyStore(DWORD, IPropertyStore **ppProperties) {
|
||||||
|
if (ppProperties == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*ppProperties = new NullPropertyStore();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE NullMMDevice::GetId(LPWSTR *ppstrId) {
|
||||||
|
if (ppstrId == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*ppstrId = co_task_wcsdup(NULL_DEVICE_ID);
|
||||||
|
return *ppstrId != nullptr ? S_OK : E_OUTOFMEMORY;
|
||||||
|
}
|
||||||
|
HRESULT STDMETHODCALLTYPE NullMMDevice::GetState(DWORD *pdwState) {
|
||||||
|
if (pdwState == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
*pdwState = DEVICE_STATE_ACTIVE;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
#pragma endregion
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include <mmdeviceapi.h>
|
||||||
|
|
||||||
|
// returns true when a synthetic render endpoint should be injected into device
|
||||||
|
// enumeration. games like gitadora arena search the render endpoint list for a
|
||||||
|
// device whose friendly name contains "Realtek" and crash with a null pointer
|
||||||
|
// dereference when no match exists. presenting a fake match that routes to the
|
||||||
|
// null audio backend lets the search succeed while discarding the audio.
|
||||||
|
bool null_render_device_enabled();
|
||||||
|
|
||||||
|
// fake IMMDevice that reports a "Realtek" friendly name and activates straight
|
||||||
|
// into the null audio backend, never touching real hardware.
|
||||||
|
struct NullMMDevice : IMMDevice {
|
||||||
|
NullMMDevice() = default;
|
||||||
|
|
||||||
|
NullMMDevice(const NullMMDevice &) = delete;
|
||||||
|
NullMMDevice &operator=(const NullMMDevice &) = delete;
|
||||||
|
|
||||||
|
virtual ~NullMMDevice() = default;
|
||||||
|
|
||||||
|
#pragma region IUnknown
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObj) override;
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override;
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
#pragma region IMMDevice
|
||||||
|
HRESULT STDMETHODCALLTYPE Activate(REFIID iid, DWORD dwClsCtx, PROPVARIANT *pActivationParams, void **ppInterface) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE OpenPropertyStore(DWORD stgmAccess, IPropertyStore **ppProperties) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE GetId(LPWSTR *ppstrId) override;
|
||||||
|
HRESULT STDMETHODCALLTYPE GetState(DWORD *pdwState) override;
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::atomic<ULONG> ref_cnt = 1;
|
||||||
|
};
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#include "null_discard_backend.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
#include "hooks/audio/util.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/precise_timer.h"
|
||||||
|
|
||||||
|
NullDiscardBackend::~NullDiscardBackend() {
|
||||||
|
this->running = false;
|
||||||
|
if (this->pacing_thread.joinable()) {
|
||||||
|
this->pacing_thread.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const WAVEFORMATEXTENSIBLE &NullDiscardBackend::format() const noexcept {
|
||||||
|
return this->format_;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_initialize(
|
||||||
|
AUDCLNT_SHAREMODE *,
|
||||||
|
DWORD *,
|
||||||
|
REFERENCE_TIME *hnsBufferDuration,
|
||||||
|
REFERENCE_TIME *,
|
||||||
|
const WAVEFORMATEX *pFormat,
|
||||||
|
LPCGUID)
|
||||||
|
{
|
||||||
|
copy_wave_format(&this->format_, pFormat);
|
||||||
|
|
||||||
|
// honor the game's requested buffer duration, falling back to 10 ms
|
||||||
|
constexpr REFERENCE_TIME DEFAULT_REFTIME = 100000; // 10 ms in 100-ns units
|
||||||
|
this->period_reftime = (hnsBufferDuration && *hnsBufferDuration > 0)
|
||||||
|
? *hnsBufferDuration
|
||||||
|
: DEFAULT_REFTIME;
|
||||||
|
|
||||||
|
this->buffer_frames = std::max<uint32_t>(1, static_cast<uint32_t>(
|
||||||
|
static_cast<double>(this->format_.Format.nSamplesPerSec)
|
||||||
|
* this->period_reftime / 10000000.0 + 0.5));
|
||||||
|
|
||||||
|
log_info("audio::null", "initializing null render device with {} channels, {} Hz, {}-bit",
|
||||||
|
this->format_.Format.nChannels,
|
||||||
|
this->format_.Format.nSamplesPerSec,
|
||||||
|
this->format_.Format.wBitsPerSample);
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_buffer_size(uint32_t *buffer_frames) {
|
||||||
|
*buffer_frames = this->buffer_frames;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_stream_latency(REFERENCE_TIME *latency) {
|
||||||
|
*latency = this->period_reftime;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) {
|
||||||
|
// discarded immediately, so the buffer always reads as fully drained
|
||||||
|
padding_frames = 0;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_is_format_supported(
|
||||||
|
AUDCLNT_SHAREMODE *,
|
||||||
|
const WAVEFORMATEX *,
|
||||||
|
WAVEFORMATEX **ppClosestMatch)
|
||||||
|
{
|
||||||
|
if (ppClosestMatch) {
|
||||||
|
*ppClosestMatch = nullptr;
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_mix_format(WAVEFORMATEX **) {
|
||||||
|
return E_NOTIMPL;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_device_period(
|
||||||
|
REFERENCE_TIME *default_device_period,
|
||||||
|
REFERENCE_TIME *minimum_device_period)
|
||||||
|
{
|
||||||
|
if (default_device_period) {
|
||||||
|
*default_device_period = this->period_reftime;
|
||||||
|
}
|
||||||
|
if (minimum_device_period) {
|
||||||
|
*minimum_device_period = this->period_reftime;
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_start() {
|
||||||
|
if (!this->running.exchange(true)) {
|
||||||
|
this->pacing_thread = std::thread(&NullDiscardBackend::pace_loop, this);
|
||||||
|
}
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_stop() {
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_set_event_handle(HANDLE *event_handle) {
|
||||||
|
// keep the game's event so pace_loop() can wake it; there is no real device behind it
|
||||||
|
this->relay_handle = *event_handle;
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
|
||||||
|
const size_t buffer_size =
|
||||||
|
static_cast<size_t>(this->format_.Format.nBlockAlign) * num_frames_requested;
|
||||||
|
if (this->scratch.size() < buffer_size) {
|
||||||
|
this->scratch.resize(buffer_size);
|
||||||
|
}
|
||||||
|
*ppData = this->scratch.data();
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT NullDiscardBackend::on_release_buffer(uint32_t, DWORD) {
|
||||||
|
// discard the audio entirely
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NullDiscardBackend::pace_loop() {
|
||||||
|
using namespace std::chrono;
|
||||||
|
|
||||||
|
timeutils::PreciseSleepTimer timer;
|
||||||
|
|
||||||
|
// audio is discarded, so timing precision and drift do not matter; just wake the
|
||||||
|
// game once per buffer period to keep its render thread from blocking on the event.
|
||||||
|
const auto period = duration_cast<steady_clock::duration>(
|
||||||
|
duration<double>(this->period_reftime / 10000000.0));
|
||||||
|
|
||||||
|
while (this->running.load()) {
|
||||||
|
if (this->relay_handle) {
|
||||||
|
SetEvent(this->relay_handle);
|
||||||
|
}
|
||||||
|
timer.sleep(period);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <optional>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <audioclient.h>
|
||||||
|
|
||||||
|
#include "hooks/audio/implementations/backend.h"
|
||||||
|
|
||||||
|
// discards all audio while pacing the game's event handle once per buffer period, so the game
|
||||||
|
// keeps running normally with nothing output to any real device. routed through the shared
|
||||||
|
// DummyIAudioClient, the same plumbing the asio backend uses.
|
||||||
|
struct NullDiscardBackend final : AudioBackend {
|
||||||
|
~NullDiscardBackend() final;
|
||||||
|
|
||||||
|
const WAVEFORMATEXTENSIBLE &format() const noexcept override;
|
||||||
|
|
||||||
|
HRESULT on_initialize(
|
||||||
|
AUDCLNT_SHAREMODE *,
|
||||||
|
DWORD *,
|
||||||
|
REFERENCE_TIME *hnsBufferDuration,
|
||||||
|
REFERENCE_TIME *,
|
||||||
|
const WAVEFORMATEX *pFormat,
|
||||||
|
LPCGUID) override;
|
||||||
|
HRESULT on_get_buffer_size(uint32_t *buffer_frames) override;
|
||||||
|
HRESULT on_get_stream_latency(REFERENCE_TIME *latency) override;
|
||||||
|
HRESULT on_get_current_padding(std::optional<uint32_t> &padding_frames) override;
|
||||||
|
HRESULT on_is_format_supported(
|
||||||
|
AUDCLNT_SHAREMODE *,
|
||||||
|
const WAVEFORMATEX *,
|
||||||
|
WAVEFORMATEX **ppClosestMatch) override;
|
||||||
|
HRESULT on_get_mix_format(WAVEFORMATEX **) override;
|
||||||
|
HRESULT on_get_device_period(
|
||||||
|
REFERENCE_TIME *default_device_period,
|
||||||
|
REFERENCE_TIME *minimum_device_period) override;
|
||||||
|
HRESULT on_start() override;
|
||||||
|
HRESULT on_stop() override;
|
||||||
|
HRESULT on_set_event_handle(HANDLE *event_handle) override;
|
||||||
|
HRESULT on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) override;
|
||||||
|
HRESULT on_release_buffer(uint32_t, DWORD) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
void pace_loop();
|
||||||
|
|
||||||
|
WAVEFORMATEXTENSIBLE format_ {};
|
||||||
|
uint32_t buffer_frames = 0;
|
||||||
|
REFERENCE_TIME period_reftime = 0;
|
||||||
|
HANDLE relay_handle = nullptr;
|
||||||
|
std::vector<BYTE> scratch;
|
||||||
|
std::thread pacing_thread;
|
||||||
|
std::atomic<bool> running = false;
|
||||||
|
};
|
||||||
@@ -687,6 +687,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::GitaDoraArenaAsioDriver].is_active()) {
|
if (options[launcher::Options::GitaDoraArenaAsioDriver].is_active()) {
|
||||||
games::gitadora::ASIO_DRIVER = options[launcher::Options::GitaDoraArenaAsioDriver].value_text();
|
games::gitadora::ASIO_DRIVER = options[launcher::Options::GitaDoraArenaAsioDriver].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::GitaDoraArenaFakeRealtekDevice].value_bool()) {
|
||||||
|
hooks::audio::FAKE_REALTEK_RENDER_DEVICE = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
if (options[launcher::Options::LoadNostalgiaModule].value_bool()) {
|
||||||
attach_nostalgia = true;
|
attach_nostalgia = true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1207,12 +1207,24 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.desc = "For Arena Model: ASIO driver name to use in place of XONAR. "
|
.desc = "For Arena Model: ASIO driver name to use in place of XONAR. "
|
||||||
"String should match a subkey under HKLM\\SOFTWARE\\ASIO\\\n\n"
|
"String should match a subkey under HKLM\\SOFTWARE\\ASIO\\\n\n"
|
||||||
"Requires 7.1 @ 48kHz; if the game rejects your ASIO device, it will automatically "
|
"Requires 7.1 @ 48kHz; if the game rejects your ASIO device, it will automatically "
|
||||||
"fall back to using WASAPI.",
|
"fall back to using WASAPI.\n\n"
|
||||||
|
"If the game crashes after successful ASIO init, try enabling -gdafakerealtek.",
|
||||||
.type = OptionType::Text,
|
.type = OptionType::Text,
|
||||||
.game_name = "GitaDora",
|
.game_name = "GitaDora",
|
||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.picker = OptionPickerType::AsioDriver,
|
.picker = OptionPickerType::AsioDriver,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// GitaDoraArenaFakeRealtekDevice
|
||||||
|
.title = "GitaDora Arena Fake Realtek Device",
|
||||||
|
.name = "gdafakerealtek",
|
||||||
|
.desc = "For Arena Model: inject a fake \"Realtek\" audio render device that discards all "
|
||||||
|
"audio sent to it. This is needed as the game crashes when ASIO is in use but Realtek "
|
||||||
|
"audio is not available. On an arcade cabinet, Realtek is used for headphone output.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "GitaDora",
|
||||||
|
.category = "Game Options",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.title = "Force Load Jubeat Module",
|
.title = "Force Load Jubeat Module",
|
||||||
.name = "jb",
|
.name = "jb",
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ namespace launcher {
|
|||||||
GitaDoraArenaSingleWindow,
|
GitaDoraArenaSingleWindow,
|
||||||
GitaDoraArenaWindowLayout,
|
GitaDoraArenaWindowLayout,
|
||||||
GitaDoraArenaAsioDriver,
|
GitaDoraArenaAsioDriver,
|
||||||
|
GitaDoraArenaFakeRealtekDevice,
|
||||||
LoadJubeatModule,
|
LoadJubeatModule,
|
||||||
LoadReflecBeatModule,
|
LoadReflecBeatModule,
|
||||||
LoadShogikaiModule,
|
LoadShogikaiModule,
|
||||||
|
|||||||
Reference in New Issue
Block a user