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:
bicarus
2026-06-05 00:08:52 -07:00
committed by GitHub
parent 85058c2156
commit 2dae86a6f2
15 changed files with 504 additions and 8 deletions
+2
View File
@@ -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
+9 -4
View File
@@ -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(
+1
View File
@@ -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"
+1
View File
@@ -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;
+5
View File
@@ -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;
};
+3
View File
@@ -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;
} }
+13 -1
View File
@@ -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",
+1
View File
@@ -117,6 +117,7 @@ namespace launcher {
GitaDoraArenaSingleWindow, GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout, GitaDoraArenaWindowLayout,
GitaDoraArenaAsioDriver, GitaDoraArenaAsioDriver,
GitaDoraArenaFakeRealtekDevice,
LoadJubeatModule, LoadJubeatModule,
LoadReflecBeatModule, LoadReflecBeatModule,
LoadShogikaiModule, LoadShogikaiModule,