#pragma once #include #include #include #include #include #include "hooks/audio/audio.h" struct IAudioClient; struct IAudioRenderClient; namespace hooks::audio { // Generic WASAPI surround-to-stereo downmix. The real device is opened in stereo while the // game keeps writing its native multi-channel audio into a scratch buffer; on release that // buffer is mixed down into the two front channels. // // The mix is derived from the source format's speaker mask according to the selected // DownmixAlgorithm: // FrontOnly / RearOnly / SideOnly - keep only that group of channels, routed to their side // AC4 - AC-4 stereo downmix coefficients (ETSI TS 103 190-1 ยง6.2.17): front left/right // pass at 0 dB, center and surrounds fold in at -3 dB, LFE dropped // Normalize - every channel folded in (center to both sides) with each output side averaged // so its channels are equally loud, LFE dropped struct Downmix { // a source channel routed into one output speaker at the given gain struct Contribution { int channel; float gain; }; // map an option value (front/rear/side/ac4/normalize) to its algorithm. static std::optional name_to_algorithm(const char *value) { if (_stricmp(value, "front") == 0) { return DownmixAlgorithm::FrontOnly; } else if (_stricmp(value, "rear") == 0) { return DownmixAlgorithm::RearOnly; } else if (_stricmp(value, "side") == 0) { return DownmixAlgorithm::SideOnly; } else if (_stricmp(value, "ac4") == 0) { return DownmixAlgorithm::AC4; } else if (_stricmp(value, "normalize") == 0) { return DownmixAlgorithm::Normalize; } return std::nullopt; } // human-readable name of an algorithm, for logging. static const char *algorithm_name(DownmixAlgorithm algorithm) { switch (algorithm) { case DownmixAlgorithm::FrontOnly: return "front"; case DownmixAlgorithm::RearOnly: return "rear"; case DownmixAlgorithm::SideOnly: return "side"; case DownmixAlgorithm::AC4: return "ac4"; case DownmixAlgorithm::Normalize: return "normalize"; default: return "unknown"; } } // whether the downmix is active for the current stream bool enabled = false; // algorithm used to fold the multi-channel audio into stereo DownmixAlgorithm algorithm = DownmixAlgorithm::AC4; // size in bytes of one frame of the game's multi-channel format int game_frame_size = 0; // size in bytes of a single sample (per channel) int bytes_per_sample = 0; // whether samples are IEEE floating point rather than integer PCM bool is_float = false; // enable the downmix for the given game format and fill stereo_out with the equivalent // stereo format to open the real device with. void setup(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out, DownmixAlgorithm algorithm); // build the stereo format equivalent to game_format (same sample rate and bit depth). static void make_stereo_format(const WAVEFORMATEX *game_format, WAVEFORMATEXTENSIBLE *stereo_out); // initialize the real device with the stereo format. downmixing reduces the channel count, // shrinking the buffer's byte size, so the duration the game sized for its multi-channel // format can leave the smaller stereo buffer unaligned. on AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED // this performs the standard WASAPI realignment and retries. HRESULT initialize(IAudioClient *real, AUDCLNT_SHAREMODE share_mode, DWORD stream_flags, REFERENCE_TIME buffer_duration, REFERENCE_TIME periodicity, const WAVEFORMATEX *device_format, LPCGUID session_guid); // mix `frames` frames of multi-channel `src` down into stereo `dst`. void process(BYTE *dst, const BYTE *src, UINT32 frames) const; // grab the real stereo device buffer and hand the game the scratch buffer to write into. HRESULT get_buffer(IAudioRenderClient *real, UINT32 frames, BYTE **ppData); // size the scratch and hand it to the game without acquiring a device buffer. used when a // later stage (the resampler) owns the device interaction. HRESULT get_scratch(UINT32 frames, BYTE **ppData); // downmix the scratch the game wrote into the caller's stereo buffer, without touching the // device. used to feed the resampler when the two stages are chained. void downmix_into(BYTE *dst, UINT32 frames) const; // mix the scratch buffer into the stereo device buffer held since get_buffer. the caller // owns releasing the device buffer afterwards (see current_buffer / buffer_released). void write_device_buffer(UINT32 frames, DWORD flags); // the real device buffer currently held, or null. BYTE *current_buffer() const { return this->device_buffer; } // forget the held device buffer once the caller has released it. void buffer_released() { this->device_buffer = nullptr; } private: // build the mix from the source speaker layout for the selected algorithm void build_layout_mix(const WAVEFORMATEX *game_format); // per-algorithm builders, each filling left_mix / right_mix from the speaker mask void build_ac4_mix(DWORD mask, int channels); void build_extract_mix(DWORD mask, int channels, DWORD keep); void build_normalize_mix(DWORD mask, int channels); // fallback for streams without a speaker mask: fold interleaved L/R pairs at `gain` void build_pairs_mix(int channels, float gain); // append one source channel to the output side(s) matching its speaker, at `gain` void add_channel(int channel, DWORD speaker, float gain); // source channels summed into each output speaker std::vector left_mix; std::vector right_mix; // buffer the game writes its multi-channel audio into between get/release std::vector scratch; // the real stereo device buffer currently held, or null BYTE *device_buffer = nullptr; // leading buffers to silence to avoid a pop on stream start int buffers_to_mute = 16; }; }