mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
audio: create a wrapper for asio drivers (#731)
## Link to GitHub Issue or related Pull Request, if one exists
n/a
## Description of change
Create `WrappedAsio`, similar to how we wrap `IAudioEndpoint`
So far, the wrapper does these things:
1. logging (for diagnosis, since iidx and gfdm don't produce any logs
when asio succeeds)
2. iidx32+ hack to work around refcount mismatch issue
3. sdvx valk cab hack to force 2-channel audio ("downmixing" by taking
only the front channels)
4. honor volume boost
As a result of `#2` the mempatch was removed from `iidx.cpp` since we
can tackle it cleanly in the hook. `#3` also removes the need for manual
patches.
Real downmixing is hard & expensive on the CPU so it was not
implemented.
## Testing
Tested iidx/sdvx/gfdm with xonar and flexasio
This commit is contained in:
@@ -494,6 +494,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
# hooks
|
# hooks
|
||||||
hooks/audio/acm.cpp
|
hooks/audio/acm.cpp
|
||||||
hooks/audio/audio.cpp
|
hooks/audio/audio.cpp
|
||||||
|
hooks/audio/asio_proxy.cpp
|
||||||
hooks/audio/buffer.cpp
|
hooks/audio/buffer.cpp
|
||||||
hooks/audio/mme.cpp
|
hooks/audio/mme.cpp
|
||||||
hooks/audio/util.cpp
|
hooks/audio/util.cpp
|
||||||
|
|||||||
@@ -893,38 +893,10 @@ namespace games::iidx {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// patch iidx32+ for asio compatibility
|
// note: the iidx32+ ASIO refcount bug (a duplicate AddRef on the ASIO instance with
|
||||||
// only do this if NOT wasapi (as opposed to checking if it's asio)
|
// no matching Release, which leaks the driver and can hang non-XONAR devices) is now
|
||||||
// the patch is only really needed for (some) non-XONAR devices but since people sometimes disguise
|
// handled transparently by the WrappedAsio proxy (see hooks/audio/asio_proxy.cpp),
|
||||||
// other devices as a XONAR, don't check for the exact string (common ASIO workaround for INF)
|
// so no game-DLL signature patch is needed here anymore
|
||||||
if (avs::game::is_ext(2024090100, INT_MAX) &&
|
|
||||||
!(SOUND_OUTPUT_DEVICE_IN_EFFECT.has_value() &&
|
|
||||||
SOUND_OUTPUT_DEVICE_IN_EFFECT.value() == "wasapi")) {
|
|
||||||
|
|
||||||
// in iidx32 final:
|
|
||||||
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
|
|
||||||
// 48 8b 4b 08 mov rcx,QWORD PTR [rbx+0x8]
|
|
||||||
// 48 8b 01 mov rax,QWORD PTR [rcx]
|
|
||||||
// ff 50 08 call QWORD PTR [rax+0x8] ; ASIO instance AddRef
|
|
||||||
|
|
||||||
intptr_t result = replace_pattern(
|
|
||||||
avs::game::DLL_INSTANCE,
|
|
||||||
"FF50????????????????FF50??4533C94533C0418D51",
|
|
||||||
"????????????????????909090??????????????????",
|
|
||||||
0, 0);
|
|
||||||
|
|
||||||
if (result == 0) {
|
|
||||||
log_warning(
|
|
||||||
"iidx",
|
|
||||||
"Failed to apply ASIO compatibility fix for iidx32+. "
|
|
||||||
"Unless patches are applied, your ASIO device may hang or fail to work");
|
|
||||||
} else {
|
|
||||||
log_info(
|
|
||||||
"iidx",
|
|
||||||
"Successfully applied ASIO compatibility fix for iidx32+ using signature matching @ 0x{:x}.",
|
|
||||||
result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,813 @@
|
|||||||
|
#include "asio_proxy.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <mutex>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "external/asio/asiolist.h"
|
||||||
|
#include "hooks/audio/audio.h"
|
||||||
|
#include "util/logging.h"
|
||||||
|
#include "util/utils.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// readable name for an ASIO sample type (e.g. "ASIOSTInt32LSB"), falling back to the
|
||||||
|
// numeric value for unknown types
|
||||||
|
const char *asio_sample_type_name(AsioSampleType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ASIOSTInt16MSB: return "ASIOSTInt16MSB";
|
||||||
|
case ASIOSTInt24MSB: return "ASIOSTInt24MSB";
|
||||||
|
case ASIOSTInt32MSB: return "ASIOSTInt32MSB";
|
||||||
|
case ASIOSTFloat32MSB: return "ASIOSTFloat32MSB";
|
||||||
|
case ASIOSTFloat64MSB: return "ASIOSTFloat64MSB";
|
||||||
|
case ASIOSTInt32MSB16: return "ASIOSTInt32MSB16";
|
||||||
|
case ASIOSTInt32MSB18: return "ASIOSTInt32MSB18";
|
||||||
|
case ASIOSTInt32MSB20: return "ASIOSTInt32MSB20";
|
||||||
|
case ASIOSTInt32MSB24: return "ASIOSTInt32MSB24";
|
||||||
|
case ASIOSTInt16LSB: return "ASIOSTInt16LSB";
|
||||||
|
case ASIOSTInt24LSB: return "ASIOSTInt24LSB";
|
||||||
|
case ASIOSTInt32LSB: return "ASIOSTInt32LSB";
|
||||||
|
case ASIOSTFloat32LSB: return "ASIOSTFloat32LSB";
|
||||||
|
case ASIOSTFloat64LSB: return "ASIOSTFloat64LSB";
|
||||||
|
case ASIOSTInt32LSB16: return "ASIOSTInt32LSB16";
|
||||||
|
case ASIOSTInt32LSB18: return "ASIOSTInt32LSB18";
|
||||||
|
case ASIOSTInt32LSB20: return "ASIOSTInt32LSB20";
|
||||||
|
case ASIOSTInt32LSB24: return "ASIOSTInt32LSB24";
|
||||||
|
default: return "unknown";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// duration in milliseconds of a buffer of the given frame count at a sample rate, or a
|
||||||
|
// negative sentinel when the frame count or sample rate is unusable
|
||||||
|
double frames_to_ms(long frames, AsioSampleRate sample_rate) {
|
||||||
|
if (frames < 0 || sample_rate <= 0.0) {
|
||||||
|
return -1.0;
|
||||||
|
}
|
||||||
|
return (frames * 1000.0) / sample_rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// scales one planar ASIO output buffer (frames samples of the given type) by gain in
|
||||||
|
// place, clamping integer formats so a boost saturates instead of wrapping. unsupported
|
||||||
|
// formats are left untouched. runs on the driver's realtime thread, so no allocation,
|
||||||
|
// locking or logging here
|
||||||
|
void apply_gain_planar(void *buffer, long frames, AsioSampleType type, float gain) {
|
||||||
|
if (buffer == nullptr || frames <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (type) {
|
||||||
|
case ASIOSTFloat32LSB: {
|
||||||
|
auto p = static_cast<float *>(buffer);
|
||||||
|
for (long i = 0; i < frames; i++) {
|
||||||
|
p[i] = std::clamp(p[i] * gain, -1.0f, 1.0f);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ASIOSTFloat64LSB: {
|
||||||
|
auto p = static_cast<double *>(buffer);
|
||||||
|
for (long i = 0; i < frames; i++) {
|
||||||
|
p[i] = std::clamp(p[i] * static_cast<double>(gain), -1.0, 1.0);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ASIOSTInt16LSB: {
|
||||||
|
auto p = static_cast<int16_t *>(buffer);
|
||||||
|
for (long i = 0; i < frames; i++) {
|
||||||
|
p[i] = static_cast<int16_t>(
|
||||||
|
std::clamp(std::lround(p[i] * gain), -32768L, 32767L));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ASIOSTInt24LSB: {
|
||||||
|
// packed 24-bit little-endian, 3 bytes per sample
|
||||||
|
auto bytes = static_cast<uint8_t *>(buffer);
|
||||||
|
for (long i = 0; i < frames; i++) {
|
||||||
|
uint8_t *s = bytes + i * 3;
|
||||||
|
int32_t v = s[0] | (s[1] << 8) | (s[2] << 16);
|
||||||
|
if (v & 0x800000) {
|
||||||
|
v |= ~0xFFFFFF; // sign extend
|
||||||
|
}
|
||||||
|
int64_t scaled = std::clamp<int64_t>(
|
||||||
|
std::llround(static_cast<double>(v) * gain), -8388608, 8388607);
|
||||||
|
s[0] = scaled & 0xFF;
|
||||||
|
s[1] = (scaled >> 8) & 0xFF;
|
||||||
|
s[2] = (scaled >> 16) & 0xFF;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ASIOSTInt32LSB: {
|
||||||
|
auto p = static_cast<int32_t *>(buffer);
|
||||||
|
for (long i = 0; i < frames; i++) {
|
||||||
|
p[i] = static_cast<int32_t>(std::clamp<int64_t>(
|
||||||
|
std::llround(static_cast<double>(p[i]) * gain), INT32_MIN, INT32_MAX));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// unsupported format (MSB, aligned 32-bit, DSD): leave untouched
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// live wrappers by CLSID. ASIO drivers are single-instance, so a host that creates a
|
||||||
|
// new instance without releasing the old one has leaked it; we track this to tear the
|
||||||
|
// stale one down
|
||||||
|
std::mutex g_wrappers_mutex;
|
||||||
|
std::vector<std::pair<CLSID, WrappedAsio *>> g_wrappers;
|
||||||
|
|
||||||
|
// register a wrapper as the live instance for its CLSID, returning any stale wrapper it
|
||||||
|
// replaces so the caller can tear it down outside the lock
|
||||||
|
WrappedAsio *register_wrapper(REFCLSID clsid, WrappedAsio *wrapper) {
|
||||||
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
|
for (auto &entry : g_wrappers) {
|
||||||
|
if (IsEqualCLSID(entry.first, clsid)) {
|
||||||
|
WrappedAsio *stale = entry.second;
|
||||||
|
entry.second = wrapper;
|
||||||
|
return stale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
g_wrappers.emplace_back(clsid, wrapper);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void unregister_wrapper(WrappedAsio *wrapper) {
|
||||||
|
std::lock_guard lock(g_wrappers_mutex);
|
||||||
|
for (auto it = g_wrappers.begin(); it != g_wrappers.end(); ++it) {
|
||||||
|
if (it->second == wrapper) {
|
||||||
|
g_wrappers.erase(it);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASIO drivers registered on this system (CLSID + registry name), scanned once
|
||||||
|
const std::vector<std::pair<CLSID, std::string>> ®istered_asio_drivers() {
|
||||||
|
static const std::vector<std::pair<CLSID, std::string>> drivers = [] {
|
||||||
|
std::vector<std::pair<CLSID, std::string>> result;
|
||||||
|
AsioDriverList driver_list;
|
||||||
|
for (const auto &driver : driver_list.driver_list) {
|
||||||
|
result.emplace_back(driver.clsid, driver.name);
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"registered ASIO driver: name='{}', clsid={}",
|
||||||
|
driver.name,
|
||||||
|
guid2s(driver.clsid));
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info("audio::wrappedasio", "discovered {} registered ASIO driver(s)", result.size());
|
||||||
|
return result;
|
||||||
|
}();
|
||||||
|
|
||||||
|
return drivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string registered_asio_name(REFCLSID clsid) {
|
||||||
|
for (const auto &driver : registered_asio_drivers()) {
|
||||||
|
if (IsEqualCLSID(driver.first, clsid)) {
|
||||||
|
return driver.second;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return guid2s(clsid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace hooks::audio::asio {
|
||||||
|
|
||||||
|
bool is_asio_creation(REFCLSID rclsid, REFIID riid) {
|
||||||
|
|
||||||
|
// ASIO hosts request the driver using its own CLSID as the interface id
|
||||||
|
if (!IsEqualGUID(rclsid, riid)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &driver : registered_asio_drivers()) {
|
||||||
|
if (IsEqualCLSID(driver.first, rclsid)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma region IUnknown
|
||||||
|
bool WrappedAsio::FORCE_TWO_CHANNELS = false;
|
||||||
|
std::atomic<WrappedAsio *> WrappedAsio::volume_active_instance {nullptr};
|
||||||
|
|
||||||
|
WrappedAsio::~WrappedAsio() {
|
||||||
|
unregister_wrapper(this);
|
||||||
|
|
||||||
|
this->detach_volume();
|
||||||
|
|
||||||
|
// our refcount is decoupled from the host's (see AddRef/Release), so pReal's count is
|
||||||
|
// exactly one here and this releases/unloads it deterministically
|
||||||
|
this->pReal->Release();
|
||||||
|
|
||||||
|
log_info("audio::wrappedasio", "destroying wrapped ASIO driver, clsid={}", guid2s(this->clsid));
|
||||||
|
}
|
||||||
|
|
||||||
|
HRESULT STDMETHODCALLTYPE WrappedAsio::QueryInterface(REFIID riid, void **ppv) {
|
||||||
|
if (ppv == nullptr) {
|
||||||
|
return E_POINTER;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASIO hosts query for the driver using its own CLSID as the IID
|
||||||
|
if (IsEqualIID(riid, IID_IUnknown) || IsEqualIID(riid, this->clsid)) {
|
||||||
|
this->AddRef();
|
||||||
|
*ppv = static_cast<IAsio *>(this);
|
||||||
|
|
||||||
|
return S_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the host is asking for some other interface; forward to the real driver. a failure
|
||||||
|
// here is a common reason a host discards a driver and retries
|
||||||
|
const HRESULT ret = this->pReal->QueryInterface(riid, ppv);
|
||||||
|
if (SUCCEEDED(ret)) {
|
||||||
|
log_info("audio::wrappedasio", "QueryInterface({}) -> forwarded to real driver", guid2s(riid));
|
||||||
|
} else {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"QueryInterface({}) -> not supported by driver, hr={:#x}",
|
||||||
|
guid2s(riid),
|
||||||
|
static_cast<unsigned long>(ret));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE WrappedAsio::AddRef() {
|
||||||
|
// decoupled from the real driver: we count host references on the wrapper and hold a
|
||||||
|
// single reference on pReal for our lifetime. this neutralizes a host bug (iidx32+)
|
||||||
|
// that takes a duplicate AddRef with no matching Release, which would leak the driver
|
||||||
|
return ++this->ref_count;
|
||||||
|
}
|
||||||
|
|
||||||
|
ULONG STDMETHODCALLTYPE WrappedAsio::Release() {
|
||||||
|
const ULONG refs = --this->ref_count;
|
||||||
|
if (refs == 0) {
|
||||||
|
delete this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return refs;
|
||||||
|
}
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
#pragma region IAsio
|
||||||
|
AsioBool __thiscall WrappedAsio::init(void *sys_handle) {
|
||||||
|
const AsioBool result = this->pReal->init(sys_handle);
|
||||||
|
if (result == AsioTrue) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"init succeeded for '{}' (driver version {})",
|
||||||
|
this->driver_name,
|
||||||
|
this->pReal->get_driver_version());
|
||||||
|
} else {
|
||||||
|
char message[128] = {};
|
||||||
|
this->pReal->get_error_message(message);
|
||||||
|
log_warning("audio::wrappedasio", "init failed: {}", message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void __thiscall WrappedAsio::get_driver_name(char *name) {
|
||||||
|
this->pReal->get_driver_name(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
long __thiscall WrappedAsio::get_driver_version() {
|
||||||
|
return this->pReal->get_driver_version();
|
||||||
|
}
|
||||||
|
|
||||||
|
void __thiscall WrappedAsio::get_error_message(char *string) {
|
||||||
|
this->pReal->get_error_message(string);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::start() {
|
||||||
|
const AsioError result = this->pReal->start();
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"start succeeded, ASIO stream is now running on '{}'",
|
||||||
|
this->driver_name);
|
||||||
|
} else {
|
||||||
|
log_warning("audio::wrappedasio", "start failed, err={}", static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::stop() {
|
||||||
|
const AsioError result = this->pReal->stop();
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_info("audio::wrappedasio", "stop succeeded, ASIO stream on '{}' halted", this->driver_name);
|
||||||
|
} else {
|
||||||
|
log_warning("audio::wrappedasio", "stop failed, err={}", static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_channels(long *num_input_channels, long *num_output_channels) {
|
||||||
|
const AsioError result = this->pReal->get_channels(num_input_channels, num_output_channels);
|
||||||
|
if (result != ASE_OK) {
|
||||||
|
log_warning("audio::wrappedasio", "get_channels failed, err={}", static_cast<long>(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FORCE_TWO_CHANNELS
|
||||||
|
&& num_output_channels != nullptr
|
||||||
|
&& *num_output_channels < FORCED_OUTPUT_CHANNELS)
|
||||||
|
{
|
||||||
|
// the device has fewer outputs than the game hardcodes; report the count it
|
||||||
|
// expects so it proceeds to create_buffers, where we forward only the real
|
||||||
|
// front pair and discard the rest
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"reporting output channel count as {} (device has {}) for forced two-channel",
|
||||||
|
FORCED_OUTPUT_CHANNELS,
|
||||||
|
*num_output_channels);
|
||||||
|
*num_output_channels = FORCED_OUTPUT_CHANNELS;
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"get_channels -> in={}, out={}",
|
||||||
|
num_input_channels ? *num_input_channels : -1,
|
||||||
|
num_output_channels ? *num_output_channels : -1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_latencies(long *input_latency, long *output_latency) {
|
||||||
|
const AsioError result = this->pReal->get_latencies(input_latency, output_latency);
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
// include millisecond equivalents alongside the frame counts for readability
|
||||||
|
AsioSampleRate sample_rate = 0.0;
|
||||||
|
this->pReal->get_sample_rate(&sample_rate);
|
||||||
|
const long in_frames = input_latency ? *input_latency : -1;
|
||||||
|
const long out_frames = output_latency ? *output_latency : -1;
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"get_latencies -> in={} frames ({:.2f} ms), out={} frames ({:.2f} ms)",
|
||||||
|
in_frames,
|
||||||
|
frames_to_ms(in_frames, sample_rate),
|
||||||
|
out_frames,
|
||||||
|
frames_to_ms(out_frames, sample_rate));
|
||||||
|
} else {
|
||||||
|
log_warning("audio::wrappedasio", "get_latencies failed, err={}", static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_buffer_size(
|
||||||
|
long *min_size,
|
||||||
|
long *max_size,
|
||||||
|
long *preferred_size,
|
||||||
|
long *granularity)
|
||||||
|
{
|
||||||
|
const AsioError result = this->pReal->get_buffer_size(min_size, max_size, preferred_size, granularity);
|
||||||
|
if (result != ASE_OK) {
|
||||||
|
log_warning("audio::wrappedasio", "get_buffer_size failed, err={}", static_cast<long>(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// include millisecond equivalents alongside the frame counts for readability
|
||||||
|
AsioSampleRate sample_rate = 0.0;
|
||||||
|
this->pReal->get_sample_rate(&sample_rate);
|
||||||
|
const long min_frames = min_size ? *min_size : -1;
|
||||||
|
const long max_frames = max_size ? *max_size : -1;
|
||||||
|
const long preferred_frames = preferred_size ? *preferred_size : -1;
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"get_buffer_size -> min={} frames ({:.2f} ms), max={} frames ({:.2f} ms), "
|
||||||
|
"preferred={} frames ({:.2f} ms), granularity={}",
|
||||||
|
min_frames,
|
||||||
|
frames_to_ms(min_frames, sample_rate),
|
||||||
|
max_frames,
|
||||||
|
frames_to_ms(max_frames, sample_rate),
|
||||||
|
preferred_frames,
|
||||||
|
frames_to_ms(preferred_frames, sample_rate),
|
||||||
|
granularity ? *granularity : -1);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::can_sample_rate(AsioSampleRate sample_rate) {
|
||||||
|
const AsioError result = this->pReal->can_sample_rate(sample_rate);
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_misc("audio::wrappedasio", "can_sample_rate({} Hz) -> supported", sample_rate);
|
||||||
|
} else {
|
||||||
|
log_misc(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"can_sample_rate({} Hz) -> not supported, err={}",
|
||||||
|
sample_rate,
|
||||||
|
static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_sample_rate(AsioSampleRate *sample_rate) {
|
||||||
|
const AsioError result = this->pReal->get_sample_rate(sample_rate);
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_misc("audio::wrappedasio", "get_sample_rate -> {} Hz", sample_rate ? *sample_rate : 0.0);
|
||||||
|
} else {
|
||||||
|
log_warning("audio::wrappedasio", "get_sample_rate failed, err={}", static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::set_sample_rate(AsioSampleRate sample_rate) {
|
||||||
|
const AsioError result = this->pReal->set_sample_rate(sample_rate);
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_info("audio::wrappedasio", "set_sample_rate({} Hz) succeeded", sample_rate);
|
||||||
|
} else {
|
||||||
|
log_warning(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"set_sample_rate({} Hz) failed, err={}",
|
||||||
|
sample_rate,
|
||||||
|
static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_clock_sources(ASIOClockSource *clocks, long *num_sources) {
|
||||||
|
return this->pReal->get_clock_sources(clocks, num_sources);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::set_clock_source(long reference) {
|
||||||
|
return this->pReal->set_clock_source(reference);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) {
|
||||||
|
return this->pReal->get_sample_position(s_pos, t_stamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::get_channel_info(AsioChannelInfo *info) {
|
||||||
|
// forced two-channel: the game probes all output channels it thinks exist, but the
|
||||||
|
// device only has the real front pair. fabricate a plausible entry for the channels
|
||||||
|
// beyond the device without touching the real driver - they are discarded in
|
||||||
|
// create_buffers anyway
|
||||||
|
long real_in = 0, real_out = 0;
|
||||||
|
if (FORCE_TWO_CHANNELS
|
||||||
|
&& info != nullptr
|
||||||
|
&& info->is_input == AsioFalse
|
||||||
|
&& this->pReal->get_channels(&real_in, &real_out) == ASE_OK
|
||||||
|
&& info->channel >= real_out)
|
||||||
|
{
|
||||||
|
const long channel = info->channel;
|
||||||
|
info->is_active = AsioTrue;
|
||||||
|
info->channel_group = 0;
|
||||||
|
info->type = ASIOSTInt32LSB;
|
||||||
|
snprintf(info->name, sizeof(info->name), "Fake ASIO OUT %ld", channel);
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"get_channel_info(channel={}, dir=output) -> fake channel, type={} ({})",
|
||||||
|
channel,
|
||||||
|
asio_sample_type_name(info->type),
|
||||||
|
static_cast<long>(info->type));
|
||||||
|
|
||||||
|
return ASE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AsioError result = this->pReal->get_channel_info(info);
|
||||||
|
if (result == ASE_OK && info != nullptr) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"get_channel_info(channel={}, dir={}) -> active={}, group={}, type={} ({}), name='{}'",
|
||||||
|
info->channel,
|
||||||
|
info->is_input == AsioTrue ? "input" : "output",
|
||||||
|
info->is_active == AsioTrue,
|
||||||
|
info->channel_group,
|
||||||
|
asio_sample_type_name(info->type),
|
||||||
|
static_cast<long>(info->type),
|
||||||
|
info->name);
|
||||||
|
} else if (result != ASE_OK) {
|
||||||
|
log_warning("audio::wrappedasio", "get_channel_info failed, err={}", static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioCallbacks *WrappedAsio::install_volume_callbacks(AsioCallbacks *game_callbacks) {
|
||||||
|
const float gain = hooks::audio::VOLUME_BOOST;
|
||||||
|
|
||||||
|
// start from a clean slate; a previous buffer set may have left state behind
|
||||||
|
this->volume_channels.clear();
|
||||||
|
this->volume_active = false;
|
||||||
|
|
||||||
|
// no boost configured (or no callbacks to wrap): pass the game's callbacks straight
|
||||||
|
// through and do zero realtime work, exactly as before
|
||||||
|
if (gain == 1.0f || game_callbacks == nullptr) {
|
||||||
|
return game_callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
this->volume_active = true;
|
||||||
|
this->volume_gain = gain;
|
||||||
|
this->volume_game_callbacks = *game_callbacks;
|
||||||
|
|
||||||
|
// wrap only the buffer-switch callbacks, where the audio data lives and we apply the
|
||||||
|
// gain. the other two carry no data we touch, so forward the game's own pointers
|
||||||
|
// unchanged - the driver expects them non-null and the game already owns their context
|
||||||
|
this->volume_proxy_callbacks = {};
|
||||||
|
this->volume_proxy_callbacks.buffer_switch = &WrappedAsio::volume_buffer_switch;
|
||||||
|
this->volume_proxy_callbacks.sample_rate_did_change = game_callbacks->sample_rate_did_change;
|
||||||
|
this->volume_proxy_callbacks.asio_message = game_callbacks->asio_message;
|
||||||
|
this->volume_proxy_callbacks.buffer_switch_time_info =
|
||||||
|
game_callbacks->buffer_switch_time_info ? &WrappedAsio::volume_buffer_switch_time_info : nullptr;
|
||||||
|
|
||||||
|
return &this->volume_proxy_callbacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::record_volume_output_channel(const AsioBufferInfo &info) {
|
||||||
|
if (info.is_input != AsioFalse) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask the real driver for this channel's sample format so the realtime path knows how
|
||||||
|
// to scale it; fall back to a sentinel that apply_gain_planar leaves untouched
|
||||||
|
AsioChannelInfo ci {};
|
||||||
|
ci.channel = info.channel_num;
|
||||||
|
ci.is_input = AsioFalse;
|
||||||
|
AsioSampleType type = ASIOSTLastEntry;
|
||||||
|
if (this->pReal->get_channel_info(&ci) == ASE_OK) {
|
||||||
|
type = ci.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
VolumeOutputChannel ch;
|
||||||
|
ch.buffers[0] = info.buffers[0];
|
||||||
|
ch.buffers[1] = info.buffers[1];
|
||||||
|
ch.type = type;
|
||||||
|
this->volume_channels.push_back(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::publish_volume(long buffer_size) {
|
||||||
|
if (!this->volume_active) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// everything the realtime thread reads is now in place; make ourselves reachable
|
||||||
|
this->volume_buffer_size = buffer_size;
|
||||||
|
WrappedAsio::volume_active_instance.store(this, std::memory_order_release);
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"volume boost active: gain={}, scaling {} output channel(s)",
|
||||||
|
this->volume_gain,
|
||||||
|
this->volume_channels.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::detach_volume() {
|
||||||
|
// stop our realtime trampolines from reaching this wrapper, but only if we are the
|
||||||
|
// currently published instance
|
||||||
|
WrappedAsio *expected = this;
|
||||||
|
WrappedAsio::volume_active_instance.compare_exchange_strong(expected, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WrappedAsio::apply_output_volume(long double_buffer_index) {
|
||||||
|
if (double_buffer_index != 0 && double_buffer_index != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const float gain = this->volume_gain;
|
||||||
|
const long frames = this->volume_buffer_size;
|
||||||
|
for (const VolumeOutputChannel &ch : this->volume_channels) {
|
||||||
|
apply_gain_planar(ch.buffers[double_buffer_index], frames, ch.type, gain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void __cdecl WrappedAsio::volume_buffer_switch(long double_buffer_index, AsioBool direct_process) {
|
||||||
|
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
|
||||||
|
if (self == nullptr) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// let the game write its samples into the driver buffers first, then scale them before
|
||||||
|
// the driver plays this half on the next switch
|
||||||
|
if (self->volume_game_callbacks.buffer_switch != nullptr) {
|
||||||
|
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
|
||||||
|
}
|
||||||
|
self->apply_output_volume(double_buffer_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioTime * __cdecl WrappedAsio::volume_buffer_switch_time_info(
|
||||||
|
AsioTime *params, long double_buffer_index, AsioBool direct_process)
|
||||||
|
{
|
||||||
|
WrappedAsio *self = WrappedAsio::volume_active_instance.load(std::memory_order_acquire);
|
||||||
|
if (self == nullptr) {
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioTime *ret = params;
|
||||||
|
if (self->volume_game_callbacks.buffer_switch_time_info != nullptr) {
|
||||||
|
ret = self->volume_game_callbacks.buffer_switch_time_info(
|
||||||
|
params, double_buffer_index, direct_process);
|
||||||
|
} else if (self->volume_game_callbacks.buffer_switch != nullptr) {
|
||||||
|
self->volume_game_callbacks.buffer_switch(double_buffer_index, direct_process);
|
||||||
|
}
|
||||||
|
self->apply_output_volume(double_buffer_index);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::create_buffers(
|
||||||
|
AsioBufferInfo *buffer_infos,
|
||||||
|
long num_channels,
|
||||||
|
long buffer_size,
|
||||||
|
AsioCallbacks *callbacks)
|
||||||
|
{
|
||||||
|
// swap in our buffer-switch trampolines if a volume boost is configured, so the real
|
||||||
|
// driver calls us and we scale its output after the game fills it (no-op otherwise)
|
||||||
|
AsioCallbacks *effective = this->install_volume_callbacks(callbacks);
|
||||||
|
|
||||||
|
if (FORCE_TWO_CHANNELS) {
|
||||||
|
return this->create_buffers_front_pair(buffer_infos, num_channels, buffer_size, effective);
|
||||||
|
}
|
||||||
|
|
||||||
|
const AsioError result = this->pReal->create_buffers(buffer_infos, num_channels, buffer_size, effective);
|
||||||
|
if (result == ASE_OK) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"create_buffers(channels={}, size={} frames) succeeded",
|
||||||
|
num_channels,
|
||||||
|
buffer_size);
|
||||||
|
|
||||||
|
// capture the device output channels we will scale, then publish ourselves to the
|
||||||
|
// realtime thread once everything is in place
|
||||||
|
if (this->volume_active) {
|
||||||
|
for (long i = 0; i < num_channels; i++) {
|
||||||
|
this->record_volume_output_channel(buffer_infos[i]);
|
||||||
|
}
|
||||||
|
this->publish_volume(buffer_size);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_warning(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"create_buffers(channels={}, size={} frames) failed, err={}",
|
||||||
|
num_channels,
|
||||||
|
buffer_size,
|
||||||
|
static_cast<long>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError WrappedAsio::create_buffers_front_pair(
|
||||||
|
AsioBufferInfo *buffer_infos,
|
||||||
|
long num_channels,
|
||||||
|
long buffer_size,
|
||||||
|
AsioCallbacks *callbacks)
|
||||||
|
{
|
||||||
|
// front-pair extraction (forced two-channel ASIO): the game asks for more output
|
||||||
|
// channels than the real device has (e.g. 8 vs 2). forward only the channels the
|
||||||
|
// device actually provides (channel 0/1 = front L/R) and hand the game throwaway
|
||||||
|
// buffers for the rest, so its front mix lands on the device and the surround
|
||||||
|
// channels are discarded. the game writes directly into the driver/dummy buffers
|
||||||
|
// from its own bufferSwitch; the only realtime work we do is the volume boost (if
|
||||||
|
// configured), which scales the forwarded device channels via our trampolines
|
||||||
|
long real_in = 0, real_out = 0;
|
||||||
|
const AsioError ch_result = this->pReal->get_channels(&real_in, &real_out);
|
||||||
|
if (ch_result != ASE_OK) {
|
||||||
|
log_warning(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"create_buffers: get_channels failed, err={}",
|
||||||
|
static_cast<long>(ch_result));
|
||||||
|
return ch_result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// partition the requested channels: those the device can serve are forwarded, the rest
|
||||||
|
// are discarded. record source indices for both so we can patch the game's array after
|
||||||
|
std::vector<AsioBufferInfo> forwarded;
|
||||||
|
std::vector<long> forwarded_src;
|
||||||
|
std::vector<long> discarded_src;
|
||||||
|
forwarded.reserve(num_channels);
|
||||||
|
forwarded_src.reserve(num_channels);
|
||||||
|
discarded_src.reserve(num_channels);
|
||||||
|
for (long i = 0; i < num_channels; i++) {
|
||||||
|
const AsioBufferInfo &bi = buffer_infos[i];
|
||||||
|
const long limit = (bi.is_input == AsioTrue) ? real_in : real_out;
|
||||||
|
if (bi.channel_num < limit) {
|
||||||
|
forwarded.push_back(bi);
|
||||||
|
forwarded_src.push_back(i);
|
||||||
|
} else {
|
||||||
|
discarded_src.push_back(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const AsioError result = this->pReal->create_buffers(
|
||||||
|
forwarded.data(), static_cast<long>(forwarded.size()), buffer_size, callbacks);
|
||||||
|
if (result != ASE_OK) {
|
||||||
|
log_warning(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"create_buffers(forwarded={} of {}, size={} frames) failed, err={}",
|
||||||
|
forwarded.size(),
|
||||||
|
num_channels,
|
||||||
|
buffer_size,
|
||||||
|
static_cast<long>(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the real driver buffer pointers back into the game's array
|
||||||
|
for (size_t k = 0; k < forwarded.size(); k++) {
|
||||||
|
AsioBufferInfo &dst = buffer_infos[forwarded_src[k]];
|
||||||
|
dst.buffers[0] = forwarded[k].buffers[0];
|
||||||
|
dst.buffers[1] = forwarded[k].buffers[1];
|
||||||
|
|
||||||
|
// only the forwarded channels reach the device, so those are the ones the volume
|
||||||
|
// boost scales (the discarded channels go to throwaway buffers below)
|
||||||
|
if (this->volume_active) {
|
||||||
|
this->record_volume_output_channel(dst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this->publish_volume(buffer_size);
|
||||||
|
|
||||||
|
// hand throwaway double buffers to the discarded channels. sized generously at
|
||||||
|
// 8 bytes/sample (covers every ASIO sample type) so the game can never overrun them
|
||||||
|
// regardless of the negotiated format
|
||||||
|
this->dummy_buffers.clear();
|
||||||
|
this->dummy_buffers.reserve(discarded_src.size() * 2);
|
||||||
|
const size_t dummy_bytes = static_cast<size_t>(buffer_size) * 8;
|
||||||
|
for (const long i : discarded_src) {
|
||||||
|
for (void *&buffer : buffer_infos[i].buffers) {
|
||||||
|
auto buf = std::make_unique<uint8_t[]>(dummy_bytes);
|
||||||
|
std::memset(buf.get(), 0, dummy_bytes);
|
||||||
|
buffer = buf.get();
|
||||||
|
this->dummy_buffers.push_back(std::move(buf));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"create_buffers: front-pair extraction - forwarded {} channel(s) to device, "
|
||||||
|
"discarded {} (requested {}, size={} frames)",
|
||||||
|
forwarded.size(),
|
||||||
|
discarded_src.size(),
|
||||||
|
num_channels,
|
||||||
|
buffer_size);
|
||||||
|
|
||||||
|
return ASE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::dispose_buffers() {
|
||||||
|
// stop our realtime trampolines from touching buffers the driver is about to free
|
||||||
|
this->detach_volume();
|
||||||
|
|
||||||
|
const AsioError result = this->pReal->dispose_buffers();
|
||||||
|
this->dummy_buffers.clear();
|
||||||
|
this->volume_channels.clear();
|
||||||
|
this->volume_active = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::control_panel() {
|
||||||
|
return this->pReal->control_panel();
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::future(long selector, void *opt) {
|
||||||
|
return this->pReal->future(selector, opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
AsioError __thiscall WrappedAsio::output_ready() {
|
||||||
|
return this->pReal->output_ready();
|
||||||
|
}
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
namespace hooks::audio::asio {
|
||||||
|
|
||||||
|
IUnknown *wrap(REFCLSID clsid, void *real) {
|
||||||
|
log_info("audio::wrappedasio", "wrapping ASIO driver interface, clsid={}", guid2s(clsid));
|
||||||
|
|
||||||
|
auto *wrapper = new WrappedAsio(
|
||||||
|
reinterpret_cast<IAsio *>(real), clsid, registered_asio_name(clsid));
|
||||||
|
|
||||||
|
// if the host already had a live wrapper for this CLSID it leaked the previous
|
||||||
|
// instance (ASIO is single-instance); tear it down now so the real driver is
|
||||||
|
// released before the reinit. works around games (iidx32+) that ref twice but
|
||||||
|
// deref once before re-initializing.
|
||||||
|
//
|
||||||
|
// FlexASIO 1.9 and many DAC ASIO drivers can't handle this; FlexASIO 1.10 and
|
||||||
|
// Xonar AE can
|
||||||
|
if (WrappedAsio *stale = register_wrapper(clsid, wrapper)) {
|
||||||
|
log_info(
|
||||||
|
"audio::wrappedasio",
|
||||||
|
"host did not release previous instance for clsid={}, forcing teardown",
|
||||||
|
guid2s(clsid));
|
||||||
|
|
||||||
|
// the stale instance may still have a running stream on the driver's realtime
|
||||||
|
// thread. stop and dispose it before deleting so no in-flight buffer switch
|
||||||
|
// (e.g. our volume trampoline) touches buffers we are about to free. ASIO
|
||||||
|
// guarantees no further buffer_switch once stop() returns, and dispose_buffers
|
||||||
|
// detaches our trampolines, fully quiescing the realtime path before delete
|
||||||
|
stale->stop();
|
||||||
|
stale->dispose_buffers();
|
||||||
|
delete stale;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<IAsio *>(wrapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "external/asio/asio.h"
|
||||||
|
#include "external/asio/iasiodrv.h"
|
||||||
|
|
||||||
|
namespace hooks::audio::asio {
|
||||||
|
|
||||||
|
// returns true if a CoCreateInstance call is instantiating a registered ASIO driver.
|
||||||
|
// ASIO hosts pass the driver CLSID as both class id and interface id; we also validate
|
||||||
|
// it against the system's registered ASIO drivers to avoid false positives
|
||||||
|
bool is_asio_creation(REFCLSID rclsid, REFIID riid);
|
||||||
|
|
||||||
|
// wrap a real ASIO driver instance, taking ownership of the supplied reference, and
|
||||||
|
// return a proxy that forwards every call to it
|
||||||
|
IUnknown *wrap(REFCLSID clsid, void *real);
|
||||||
|
}
|
||||||
|
|
||||||
|
// transparent proxy around a real ASIO driver; a single place to intercept ASIO traffic
|
||||||
|
struct WrappedAsio final : IAsio {
|
||||||
|
WrappedAsio(IAsio *real, REFCLSID clsid, std::string name)
|
||||||
|
: pReal(real), clsid(clsid), driver_name(std::move(name)) {
|
||||||
|
}
|
||||||
|
|
||||||
|
WrappedAsio(const WrappedAsio &) = delete;
|
||||||
|
WrappedAsio &operator=(const WrappedAsio &) = delete;
|
||||||
|
|
||||||
|
virtual ~WrappedAsio();
|
||||||
|
|
||||||
|
// when set, the proxy presents the game's expected multichannel layout to the host so
|
||||||
|
// it proceeds to create_buffers, then forwards only the device's real front pair and
|
||||||
|
// discards the rest (see create_buffers). set once at boot, before any wrapper exists,
|
||||||
|
// so it needs no synchronization
|
||||||
|
static bool FORCE_TWO_CHANNELS;
|
||||||
|
|
||||||
|
// some games hardcode a multichannel ASIO output and bail before create_buffers if
|
||||||
|
// get_channels reports fewer, so we report at least this many output channels when
|
||||||
|
// FORCE_TWO_CHANNELS is active
|
||||||
|
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
|
||||||
|
|
||||||
|
#pragma region IUnknown
|
||||||
|
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppv) override;
|
||||||
|
ULONG STDMETHODCALLTYPE AddRef() override;
|
||||||
|
ULONG STDMETHODCALLTYPE Release() override;
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
#pragma region IAsio
|
||||||
|
AsioBool __thiscall init(void *sys_handle) override;
|
||||||
|
void __thiscall get_driver_name(char *name) override;
|
||||||
|
long __thiscall get_driver_version() override;
|
||||||
|
void __thiscall get_error_message(char *string) override;
|
||||||
|
AsioError __thiscall start() override;
|
||||||
|
AsioError __thiscall stop() override;
|
||||||
|
AsioError __thiscall get_channels(long *num_input_channels, long *num_output_channels) override;
|
||||||
|
AsioError __thiscall get_latencies(long *input_latency, long *output_latency) override;
|
||||||
|
AsioError __thiscall get_buffer_size(
|
||||||
|
long *min_size,
|
||||||
|
long *max_size,
|
||||||
|
long *preferred_size,
|
||||||
|
long *granularity) override;
|
||||||
|
AsioError __thiscall can_sample_rate(AsioSampleRate sample_rate) override;
|
||||||
|
AsioError __thiscall get_sample_rate(AsioSampleRate *sample_rate) override;
|
||||||
|
AsioError __thiscall set_sample_rate(AsioSampleRate sample_rate) override;
|
||||||
|
AsioError __thiscall get_clock_sources(ASIOClockSource *clocks, long *num_sources) override;
|
||||||
|
AsioError __thiscall set_clock_source(long reference) override;
|
||||||
|
AsioError __thiscall get_sample_position(ASIOSamples *s_pos, ASIOTimeStamp *t_stamp) override;
|
||||||
|
AsioError __thiscall get_channel_info(AsioChannelInfo *info) override;
|
||||||
|
AsioError __thiscall create_buffers(
|
||||||
|
AsioBufferInfo *buffer_infos,
|
||||||
|
long num_channels,
|
||||||
|
long buffer_size,
|
||||||
|
AsioCallbacks *callbacks) override;
|
||||||
|
AsioError __thiscall dispose_buffers() override;
|
||||||
|
AsioError __thiscall control_panel() override;
|
||||||
|
AsioError __thiscall future(long selector, void *opt) override;
|
||||||
|
AsioError __thiscall output_ready() override;
|
||||||
|
#pragma endregion
|
||||||
|
|
||||||
|
private:
|
||||||
|
// create_buffers implementation used when FORCE_TWO_CHANNELS is active: forwards only
|
||||||
|
// the channels the real device has and hands the game throwaway buffers for the rest
|
||||||
|
AsioError create_buffers_front_pair(
|
||||||
|
AsioBufferInfo *buffer_infos,
|
||||||
|
long num_channels,
|
||||||
|
long buffer_size,
|
||||||
|
AsioCallbacks *callbacks);
|
||||||
|
|
||||||
|
// if hooks::audio::VOLUME_BOOST is set, saves the game's callbacks and returns a proxy
|
||||||
|
// callback set (our buffer-switch trampolines) to hand the real driver instead, so we
|
||||||
|
// can scale its output buffers after the game fills them. otherwise returns the game's
|
||||||
|
// callbacks unchanged. called at create_buffers time, before the stream starts
|
||||||
|
AsioCallbacks *install_volume_callbacks(AsioCallbacks *game_callbacks);
|
||||||
|
|
||||||
|
// records a device output channel whose buffers we scale by the volume boost. queries
|
||||||
|
// the real driver for the channel's sample format. called at create_buffers time
|
||||||
|
void record_volume_output_channel(const AsioBufferInfo &info);
|
||||||
|
|
||||||
|
// publishes the captured volume state to the realtime thread once the buffers exist,
|
||||||
|
// making our trampolines start scaling. called at the end of either create_buffers path
|
||||||
|
void publish_volume(long buffer_size);
|
||||||
|
|
||||||
|
// detaches this instance from the realtime trampolines so they stop touching its
|
||||||
|
// buffers. called from dispose_buffers and the destructor
|
||||||
|
void detach_volume();
|
||||||
|
|
||||||
|
// multiplies every recorded output channel's buffer for the given double-buffer index
|
||||||
|
// by the volume boost. runs on the driver's realtime thread from our buffer switch
|
||||||
|
void apply_output_volume(long double_buffer_index);
|
||||||
|
|
||||||
|
// realtime-thread trampolines for the buffer-switch callbacks, handed to the real
|
||||||
|
// driver in place of the game's; ASIO callbacks carry no user data, so they reach the
|
||||||
|
// active wrapper through volume_active_instance, call the game's original, then scale.
|
||||||
|
// the other two callbacks (sample_rate_did_change, asio_message) are forwarded as the
|
||||||
|
// game's own pointers, so they need no trampoline
|
||||||
|
static void __cdecl volume_buffer_switch(long double_buffer_index, AsioBool direct_process);
|
||||||
|
static AsioTime * __cdecl volume_buffer_switch_time_info(
|
||||||
|
AsioTime *params, long double_buffer_index, AsioBool direct_process);
|
||||||
|
|
||||||
|
// the single wrapper whose proxy callbacks are installed (ASIO is single-instance with
|
||||||
|
// one running stream); read by the static trampolines to reach the right wrapper
|
||||||
|
static std::atomic<WrappedAsio *> volume_active_instance;
|
||||||
|
|
||||||
|
IAsio *const pReal;
|
||||||
|
const CLSID clsid;
|
||||||
|
|
||||||
|
// registry name of the driver (not get_driver_name), used in our logs as a single
|
||||||
|
// unambiguous name; constant for our lifetime
|
||||||
|
std::string driver_name;
|
||||||
|
|
||||||
|
// our own reference count; we hold one reference on pReal and release it when this
|
||||||
|
// drops to zero
|
||||||
|
std::atomic<ULONG> ref_count {1};
|
||||||
|
|
||||||
|
// throwaway double buffers handed to the channels we discard when FORCE_TWO_CHANNELS
|
||||||
|
// is active (see create_buffers). owned for the lifetime of the buffer set and freed
|
||||||
|
// in dispose_buffers; only read by the game from its own bufferSwitch, never by us
|
||||||
|
std::vector<std::unique_ptr<uint8_t[]>> dummy_buffers;
|
||||||
|
|
||||||
|
// one device output channel scaled by the volume boost in our buffer switch
|
||||||
|
struct VolumeOutputChannel {
|
||||||
|
void *buffers[2];
|
||||||
|
AsioSampleType type;
|
||||||
|
};
|
||||||
|
|
||||||
|
// volume boost state, captured at create_buffers time and published to the realtime
|
||||||
|
// thread via volume_active_instance once fully built; untouched while the stream runs.
|
||||||
|
// volume_active gates whether we install our proxy callbacks at all
|
||||||
|
bool volume_active = false;
|
||||||
|
float volume_gain = 1.0f;
|
||||||
|
long volume_buffer_size = 0;
|
||||||
|
AsioCallbacks volume_game_callbacks {};
|
||||||
|
AsioCallbacks volume_proxy_callbacks {};
|
||||||
|
std::vector<VolumeOutputChannel> volume_channels;
|
||||||
|
};
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include "audio_private.h"
|
#include "audio_private.h"
|
||||||
#include "acm.h"
|
#include "acm.h"
|
||||||
|
#include "asio_proxy.h"
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
DEFINE_GUID(CLSID_MMDeviceEnumerator,
|
||||||
@@ -95,6 +96,10 @@ static HRESULT STDAPICALLTYPE CoCreateInstance_hook(
|
|||||||
// wrap object
|
// wrap object
|
||||||
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
|
auto mmde = reinterpret_cast<IMMDeviceEnumerator **>(ppv);
|
||||||
*mmde = new WrappedIMMDeviceEnumerator(*mmde);
|
*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 original result
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
#include "games/museca/museca.h"
|
#include "games/museca/museca.h"
|
||||||
#include "hooks/avshook.h"
|
#include "hooks/avshook.h"
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
|
#include "hooks/audio/asio_proxy.h"
|
||||||
#include "hooks/audio/backends/wasapi/downmix.h"
|
#include "hooks/audio/backends/wasapi/downmix.h"
|
||||||
#include "hooks/debughook.h"
|
#include "hooks/debughook.h"
|
||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
@@ -496,6 +497,9 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) {
|
if (options[launcher::Options::spice2x_SDVXAsioDriver].is_active()) {
|
||||||
games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text();
|
games::sdvx::ASIO_DRIVER = options[launcher::Options::spice2x_SDVXAsioDriver].value_text();
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::SDVXAsioTwoChannel].value_bool()) {
|
||||||
|
WrappedAsio::FORCE_TWO_CHANNELS = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) {
|
if (options[launcher::Options::spice2x_SDVXSubPos].is_active()) {
|
||||||
auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text();
|
auto txt = options[launcher::Options::spice2x_SDVXSubPos].value_text();
|
||||||
if (txt == "top") {
|
if (txt == "top") {
|
||||||
|
|||||||
@@ -948,6 +948,15 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
.category = "Game Options",
|
.category = "Game Options",
|
||||||
.picker = OptionPickerType::AsioDriver,
|
.picker = OptionPickerType::AsioDriver,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// SDVXAsioTwoChannel
|
||||||
|
.title = "SDVX ASIO Two Channel Audio",
|
||||||
|
.name = "sdvxasio2ch",
|
||||||
|
.desc = "Force the game to use two channels for ASIO output.",
|
||||||
|
.type = OptionType::Bool,
|
||||||
|
.game_name = "Sound Voltex",
|
||||||
|
.category = "Game Options",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// spice2x_SDVXSubPos
|
// spice2x_SDVXSubPos
|
||||||
.title = "SDVX Subscreen Overlay Position",
|
.title = "SDVX Subscreen Overlay Position",
|
||||||
@@ -1965,7 +1974,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// VolumeBoost
|
// VolumeBoost
|
||||||
.title = "WASAPI Boost Audio Volume",
|
.title = "WASAPI/ASIO Boost Audio Volume",
|
||||||
.name = "volumeboost",
|
.name = "volumeboost",
|
||||||
.desc = "Artificially amplifies the hooked audio output by the selected amount, applied "
|
.desc = "Artificially amplifies the hooked audio output by the selected amount, applied "
|
||||||
"right before the audio reaches the device. Works regardless of channel layout or "
|
"right before the audio reaches the device. Works regardless of channel layout or "
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ namespace launcher {
|
|||||||
spice2x_SDVXDigitalKnobSensitivity,
|
spice2x_SDVXDigitalKnobSensitivity,
|
||||||
SDVXDigitalKnobSocd,
|
SDVXDigitalKnobSocd,
|
||||||
spice2x_SDVXAsioDriver,
|
spice2x_SDVXAsioDriver,
|
||||||
|
SDVXAsioTwoChannel,
|
||||||
spice2x_SDVXSubPos,
|
spice2x_SDVXSubPos,
|
||||||
SDVXSubMonitorOverride,
|
SDVXSubMonitorOverride,
|
||||||
LoadDDRModule,
|
LoadDDRModule,
|
||||||
|
|||||||
Reference in New Issue
Block a user