#pragma once #include #include #include #include #include #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 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 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> 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 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; };