audio: stereo downmix, volume boost options (#722)

## Link to GitHub Issue or related Pull Request, if one exists
Fixes #717, fixes #647

## Description of change
Adds an option to downmix surround sound (5.1, 7.1, etc) down to stereo
(2 speakers). This can be used in most WASAPI games, including Gitadora
and FTT.

How it works: when the game tries to open surround format (say, 7.1) we
create a fake buffer and tell the game that it is supported. In reality
we open a 2-channel stream with the real sound card. When the stream
begins, we downmix the channels down to two (using one of many
algorithms) and output to the sound card.

While we're here, implement an option to boost the game volume by some
decibel value, which is a feature often requested by SDVX players. This
was needed since downmixing can sometimes result in quieter audio.

## Testing
Tested GW Delta and FTT.

Downmix doesn't work on IIDX (32 bits) but volume boost works.
This commit is contained in:
bicarus
2026-05-31 20:49:22 -07:00
committed by GitHub
parent 6bb6b0a301
commit 84ea3f9e8e
13 changed files with 768 additions and 34 deletions
@@ -0,0 +1,142 @@
#pragma once
#include <optional>
#include <vector>
#include <windows.h>
#include <mmreg.h>
#include <audioclient.h>
#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<DownmixAlgorithm> 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);
// 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<Contribution> left_mix;
std::vector<Contribution> right_mix;
// buffer the game writes its multi-channel audio into between get/release
std::vector<BYTE> 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;
};
}