mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 14:20: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_collection.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_render_client.cpp
|
||||
hooks/audio/backends/wasapi/downmix.cpp
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace games::gitadora {
|
||||
|
||||
static decltype(RegCloseKey) *RegCloseKey_orig = nullptr;
|
||||
static decltype(RegEnumKeyA) *RegEnumKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyA) *RegOpenKeyA_orig = nullptr;
|
||||
static decltype(RegOpenKeyExA) *RegOpenKeyExA_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,
|
||||
PHKEY phkResult)
|
||||
{
|
||||
// ASIO\XONAR redirect to ASIO\<configured>
|
||||
if (ASIO_DRIVER.has_value() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
@@ -60,7 +60,10 @@ namespace games::gitadora {
|
||||
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() &&
|
||||
lpSubKey != nullptr &&
|
||||
phkResult != nullptr &&
|
||||
@@ -68,10 +71,10 @@ namespace games::gitadora {
|
||||
_stricmp(lpSubKey, "software\\asio") == 0)
|
||||
{
|
||||
*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) {
|
||||
@@ -158,6 +161,8 @@ namespace games::gitadora {
|
||||
"RegCloseKey", RegCloseKey_hook, avs::game::DLL_INSTANCE);
|
||||
RegEnumKeyA_orig = detour::iat_try(
|
||||
"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", RegOpenKeyExA_hook, avs::game::DLL_INSTANCE);
|
||||
RegQueryValueExA_orig = detour::iat_try(
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <ksmedia.h>
|
||||
|
||||
#include "cfg/configurator.h"
|
||||
#include "hooks/audio/audio.h"
|
||||
#include "hooks/audio/mme.h"
|
||||
#include "hooks/graphics/graphics.h"
|
||||
#include "misc/wintouchemu.h"
|
||||
|
||||
@@ -47,6 +47,7 @@ namespace hooks::audio {
|
||||
bool USE_DUMMY = false;
|
||||
WAVEFORMATEXTENSIBLE FORMAT {};
|
||||
std::optional<Backend> BACKEND = std::nullopt;
|
||||
bool FAKE_REALTEK_RENDER_DEVICE = false;
|
||||
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
|
||||
std::string ASIO_DRIVER_NAME = "";
|
||||
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
|
||||
|
||||
@@ -40,6 +40,11 @@ namespace hooks::audio {
|
||||
extern bool USE_DUMMY;
|
||||
extern WAVEFORMATEXTENSIBLE FORMAT;
|
||||
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::string ASIO_DRIVER_NAME;
|
||||
extern bool ASIO_FORCE_UNLOAD_ON_STOP;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "device_collection.h"
|
||||
#include "device.h"
|
||||
#include "null_device.h"
|
||||
#include "util/utils.h"
|
||||
#include "util/logging.h"
|
||||
|
||||
@@ -28,17 +29,48 @@ ULONG STDMETHODCALLTYPE WrappedIMMDeviceCollection::Release() {
|
||||
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) {
|
||||
// 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);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// call original
|
||||
const auto hr = pReal->Item(nDevice, ppDevice);
|
||||
|
||||
// wrap interface
|
||||
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
||||
if (SUCCEEDED(hr) && *ppDevice != nullptr) {
|
||||
*ppDevice = new WrappedIMMDevice(*ppDevice);
|
||||
}
|
||||
return hr;
|
||||
}
|
||||
@@ -4,7 +4,8 @@
|
||||
#include <mmdeviceapi.h>
|
||||
|
||||
struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
||||
explicit WrappedIMMDeviceCollection(IMMDeviceCollection *orig) : pReal(orig) {
|
||||
WrappedIMMDeviceCollection(IMMDeviceCollection *orig, EDataFlow dataFlow)
|
||||
: pReal(orig), data_flow(dataFlow) {
|
||||
}
|
||||
|
||||
WrappedIMMDeviceCollection(const WrappedIMMDeviceCollection &) = delete;
|
||||
@@ -24,5 +25,9 @@ struct WrappedIMMDeviceCollection : IMMDeviceCollection {
|
||||
#pragma endregion
|
||||
|
||||
private:
|
||||
// whether the synthetic fake Realtek render device should be appended to this collection
|
||||
bool should_inject_fake_realtek() const;
|
||||
|
||||
IMMDeviceCollection *const pReal;
|
||||
const EDataFlow data_flow;
|
||||
};
|
||||
|
||||
@@ -45,7 +45,7 @@ HRESULT STDMETHODCALLTYPE WrappedIMMDeviceEnumerator::EnumAudioEndpoints(
|
||||
{
|
||||
const auto hr = pReal->EnumAudioEndpoints(dataFlow, dwStateMask, ppDevices);
|
||||
if (SUCCEEDED(hr) && (ppDevices != nullptr) && (*ppDevices != nullptr)) {
|
||||
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices);
|
||||
*ppDevices = new WrappedIMMDeviceCollection(*ppDevices, dataFlow);
|
||||
}
|
||||
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()) {
|
||||
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()) {
|
||||
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. "
|
||||
"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 "
|
||||
"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,
|
||||
.game_name = "GitaDora",
|
||||
.category = "Game Options",
|
||||
.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",
|
||||
.name = "jb",
|
||||
|
||||
@@ -117,6 +117,7 @@ namespace launcher {
|
||||
GitaDoraArenaSingleWindow,
|
||||
GitaDoraArenaWindowLayout,
|
||||
GitaDoraArenaAsioDriver,
|
||||
GitaDoraArenaFakeRealtekDevice,
|
||||
LoadJubeatModule,
|
||||
LoadReflecBeatModule,
|
||||
LoadShogikaiModule,
|
||||
|
||||
Reference in New Issue
Block a user