Files
spice2x.github.io/src/spice2x/hooks/audio/audio.cpp
T
bicarus 9d41ca2515 nost: xact audio hook (#823)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #822

## Description of change
Add hooks for `libxact.dll` which only Nostalgia uses for audio. Detect
cases where the game would fail to launch (wrong number of channels) and
log a warning / deferred log.

## Testing
tested 2024102200
2026-07-21 15:33:30 -07:00

189 lines
5.7 KiB
C++

#include "audio.h"
#include <vector>
#include <windows.h>
#include <initguid.h>
#include <audioclient.h>
#include <mmdeviceapi.h>
#include "avs/game.h"
#include "hooks/audio/backends/mmdevice/device_enumerator.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/memutils.h"
#include "audio_private.h"
#include "acm.h"
#include "asio_proxy.h"
#include "xact.h"
#ifdef _MSC_VER
DEFINE_GUID(CLSID_MMDeviceEnumerator,
0xBCDE0395, 0xE52F, 0x467C,
0x8E, 0x3D, 0xC4, 0x57, 0x92, 0x91, 0x69, 0x2E);
#endif
// {ef985e71-d5c7-42d4-ba4d-2d073e2e96f4}
DEFINE_GUID(CLSID_DirectSoundI3DL2ReverbDMO,
0xef985e71, 0xd5c7, 0x42d4,
0xba, 0x4d, 0x2d, 0x07, 0x3e, 0x2e, 0x96, 0xf4);
// {87fc0268-9a55-4360-95aa-004a1d9de26c}
DEFINE_GUID(CLSID_DirectSoundWavesReverbDMO,
0x87fc0268, 0x9a55, 0x4360,
0x95, 0xaa, 0x00, 0x4a, 0x1d, 0x9d, 0xe2, 0x6c);
// function pointers
static decltype(CoCreateInstance) *CoCreateInstance_orig = nullptr;
namespace hooks::audio {
// public globals
bool ENABLED = true;
bool VOLUME_HOOK_ENABLED = true;
std::optional<DownmixAlgorithm> DOWNMIX_ALGORITHM = std::nullopt;
float VOLUME_BOOST = 1.0f;
std::optional<uint32_t> RESAMPLE_RATE = std::nullopt;
std::optional<uint32_t> EXCLUSIVE_BUFFER_MS = std::nullopt;
bool WASAPI_COMPATIBILITY_MODE = false;
bool USE_DUMMY = false;
WAVEFORMATEXTENSIBLE FORMAT {};
std::optional<Backend> BACKEND = std::nullopt;
bool INJECT_FAKE_REALTEK_AUDIO = false;
std::optional<size_t> ASIO_DRIVER_ID = std::nullopt;
std::string ASIO_DRIVER_NAME = "";
bool ASIO_FORCE_UNLOAD_ON_STOP = false;
std::mutex INITIALIZE_LOCK; // for asio
IAudioClient *CLIENT = nullptr;
static std::mutex CLIENT_LOCK;
static bool CLIENT_STOPPING = false;
void set_active_client(IAudioClient *client, const char *source) {
IAudioClient *previous_client = nullptr;
{
std::lock_guard lock(CLIENT_LOCK);
if (CLIENT_STOPPING || CLIENT == client) {
return;
}
if (client) {
client->AddRef();
}
previous_client = CLIENT;
CLIENT = client;
}
if (previous_client) {
previous_client->Release();
}
if (client) {
log_info(
"audio",
"active client selected by {} after successful initialization",
source);
}
}
}
static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
REFCLSID rclsid,
LPUNKNOWN pUnkOuter,
DWORD dwClsContext,
REFIID riid,
LPVOID *ppv)
{
HRESULT ret;
// ASIO: if we already hold a cached instance for this CLSID, hand back a fresh
// wrapper over it instead of re-creating the real driver (see asio_proxy.cpp)
if (ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
if (IUnknown *reused = hooks::audio::asio::wrap_existing(rclsid)) {
*ppv = reused;
return S_OK;
}
}
// call original
ret = CoCreateInstance_orig(rclsid, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) {
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
log_warning("audio", "CoCreateInstance failed for CLSID_MMDeviceEnumerator, hr={}", FMT_HRESULT(ret));
} else if (IsEqualCLSID(rclsid, CLSID_DirectSoundI3DL2ReverbDMO)) {
// deal with reverb DMO missing from dsdmo.dll
// it usually fails with 0x80040154 (REGDB_E_CLASSNOTREG), but we have also seen 0x8007007e (ERROR_MOD_NOT_FOUND)
static std::once_flag printed;
std::call_once(printed, [ret]() {
log_warning(
"audio",
"CoCreateInstance for CLSID_DirectSoundI3DL2ReverbDMO failed with {}, swap with CLSID_DirectSoundWavesReverbDMO...",
FMT_HRESULT(ret));
});
ret = CoCreateInstance_orig(CLSID_DirectSoundWavesReverbDMO, pUnkOuter, dwClsContext, riid, ppv);
if (FAILED(ret)) {
log_warning("audio", "CoCreateInstance(CLSID_DirectSoundWavesReverbDMO...) failed, hr={}", FMT_HRESULT(ret));
}
}
return ret;
}
// check if this is the audio device enumerator
if (IsEqualCLSID(rclsid, CLSID_MMDeviceEnumerator)) {
// wrap object
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
*mmde = new WrappedIMMDeviceEnumerator(*mmde);
} else if (ppv != nullptr && *ppv != nullptr && hooks::audio::asio::is_asio_creation(rclsid, riid)) {
// wrap every ASIO driver so calls pass through to the real driver
*ppv = hooks::audio::asio::wrap(rclsid, *ppv);
}
// return original result
return ret;
}
namespace hooks::audio {
void init() {
if (!ENABLED) {
return;
}
log_info("audio", "initializing");
init_low_latency();
hooks::audio::acm::init();
// general hooks
CoCreateInstance_orig = detour::iat_try("CoCreateInstance", CoCreateInstance_hook);
if (avs::game::is_model("PAN")) {
hooks::audio::xact::init();
}
}
void stop() {
log_info("audio", "stopping");
IAudioClient *client = nullptr;
{
std::lock_guard lock(CLIENT_LOCK);
CLIENT_STOPPING = true;
client = CLIENT;
CLIENT = nullptr;
}
if (client) {
client->Stop();
client->Release();
}
stop_low_latency();
// release the ASIO drivers we kept pinned for the process lifetime now that we are
// at a controlled shutdown point (see asio_proxy.cpp)
hooks::audio::asio::release_all_wrappers();
}
}