mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-02 06:40:42 -07:00
b9f3be4df2
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change IIDX likes to call `CoCreateInstance` and `init` multiple times on ASIO drivers, but some ASIO drivers like `Neva Uno` really don't like that and ends up crashing. In our wrapper, cache the instances and try to reuse them for better compatibility. ## Testing Worked fine for FlexASIO / Xonar AE / Realtek ASIO though none of these drivers repro the crashing behavior.
241 lines
12 KiB
C++
241 lines
12 KiB
C++
#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. also records it as the cached
|
|
// instance for its CLSID so later CoCreate calls can reuse it (see wrap_existing)
|
|
IUnknown *wrap(REFCLSID clsid, void *real);
|
|
|
|
// if a cached wrapper already exists for this CLSID, return it (with an added
|
|
// reference); otherwise nullptr to signal the caller to create the real driver and
|
|
// wrap it. lets the host reuse one driver instance instead of re-instantiating it
|
|
IUnknown *wrap_existing(REFCLSID clsid);
|
|
|
|
// drop the process-lifetime references taken by wrap() so cached drivers can be released
|
|
// at shutdown. only relinquishes our pin, so a real driver is torn down once the host
|
|
// has released its own references too. call from a controlled shutdown point, never from
|
|
// a static destructor (the driver DLL may already be unloaded)
|
|
void release_all_wrappers();
|
|
}
|
|
|
|
// 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();
|
|
|
|
// selects which source channel pair of a multichannel ASIO output reaches the device's
|
|
// 2.0 front pair. when not None, the proxy presents the game's expected multichannel
|
|
// layout to the host so it proceeds to create_buffers, then opens only a two-channel
|
|
// stream on the real device and routes the selected pair onto it (see create_buffers).
|
|
// Front is the plain "force two channel" case (forward the device's own front pair);
|
|
// the others copy a different pair onto 0/1. assumes a standard 7.1 layout (0-indexed).
|
|
// set once at boot, before any wrapper exists, so it needs no synchronization
|
|
enum class StereoDownmix {
|
|
None, // feature disabled - full multichannel passthrough
|
|
Front, // channels 0/1 - the device front pair is forwarded as-is (no copy)
|
|
Center, // channel 2 duplicated to both 0 and 1
|
|
Rear, // channels 4/5 -> 0/1
|
|
Side, // channels 6/7 -> 0/1
|
|
};
|
|
static StereoDownmix STEREO_DOWNMIX;
|
|
|
|
// true when a stereo extraction is configured, i.e. the real device should open a 2.0
|
|
// stream and only the selected pair should reach it. the former standalone
|
|
// FORCE_TWO_CHANNELS flag is now just the Front case of this
|
|
static bool force_two_channels() {
|
|
return STEREO_DOWNMIX != StereoDownmix::None;
|
|
}
|
|
|
|
// 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 a
|
|
// stereo extraction is active
|
|
static constexpr long FORCED_OUTPUT_CHANNELS = 8;
|
|
|
|
// maps an option string ("front", "center", "rear", "side") to a StereoDownmix value,
|
|
// returning None for anything unrecognized
|
|
static StereoDownmix name_to_stereo_downmix(const char *name);
|
|
|
|
#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
|
|
|
|
// quiesces any leftover stream/buffer state before the cached wrapper is handed back
|
|
// for reuse, without destroying the real driver (see wrap_existing)
|
|
void quiesce_for_reuse();
|
|
|
|
private:
|
|
// create_buffers implementation used when a stereo extraction 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 any post-processing effect (volume boost or stereo downmix) is active, saves the
|
|
// game's callbacks and returns a proxy callback set (our buffer-switch trampolines) to
|
|
// hand the real driver instead, so we can rework 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_proxy_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);
|
|
|
|
// the real device's output sample format, queried from its first output channel. all
|
|
// output channels of a device share one format, so this characterizes them all. returns
|
|
// ASIOSTLastEntry if the device has no output channels or the query fails
|
|
AsioSampleType device_output_sample_type();
|
|
|
|
// locates the destination pair (device channels 0/1) and the configured source channels
|
|
// in the game's buffer set so the realtime path can copy the selected pair onto 0/1.
|
|
// a no-op unless STEREO_DOWNMIX selects a non-front pair. called at create_buffers time
|
|
void record_downmix_channels(AsioBufferInfo *buffer_infos, long num_channels, long buffer_size);
|
|
|
|
// publishes the captured post-process state to the realtime thread once the buffers
|
|
// exist, making our trampolines start reworking output. called at the end of either
|
|
// create_buffers path
|
|
void publish_post_process(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_post_process();
|
|
|
|
// 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);
|
|
|
|
// copies the configured source channel pair onto device channels 0/1 for the given
|
|
// double-buffer index. runs on the driver's realtime thread from our buffer switch
|
|
void apply_downmix(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 active_instance, call the game's original, then rework output.
|
|
// 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 proxy_buffer_switch(long double_buffer_index, AsioBool direct_process);
|
|
static AsioTime * __cdecl proxy_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 *> 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;
|
|
|
|
// the real driver is initialized exactly once; repeat init() calls are a no-op success
|
|
bool initialized = false;
|
|
|
|
// whether the real driver currently has a buffer set / running stream. used to quiesce
|
|
// leftover state when the cached wrapper is reused (see quiesce_for_reuse)
|
|
bool buffers_created = false;
|
|
bool started = false;
|
|
|
|
// 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 a stereo extraction
|
|
// 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;
|
|
};
|
|
|
|
// the game's original callbacks (captured when we install our proxy set) and the proxy
|
|
// set we hand the real driver; the realtime trampolines reach the game's buffer_switch
|
|
// through game_callbacks regardless of which effect is active
|
|
AsioCallbacks game_callbacks {};
|
|
AsioCallbacks proxy_callbacks {};
|
|
|
|
// volume boost state, captured at create_buffers time and published to the realtime
|
|
// thread via active_instance once fully built; untouched while the stream runs.
|
|
// volume_active gates whether the realtime path scales any buffers
|
|
bool volume_active = false;
|
|
float volume_gain = 1.0f;
|
|
long volume_buffer_size = 0;
|
|
std::vector<VolumeOutputChannel> volume_channels;
|
|
|
|
// one device channel (0 or 1) fed by a source channel during stereo downmix; both
|
|
// buffer pointers are indexed by the ASIO double-buffer index, the same as the channels
|
|
struct DownmixCopy {
|
|
void *dst[2];
|
|
void *src[2];
|
|
};
|
|
|
|
// stereo downmix state, captured at create_buffers time and published alongside the
|
|
// volume state; untouched while the stream runs. downmix_active gates whether the
|
|
// realtime path copies the selected source pair onto device channels 0/1. copies[0]
|
|
// feeds device channel 0, copies[1] feeds device channel 1
|
|
bool downmix_active = false;
|
|
DownmixCopy downmix_copies[2] {};
|
|
size_t downmix_bytes = 0;
|
|
};
|