mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-08-01 22:30:42 -07:00
48033816a8
## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Resampler: Implement resampler for exclusive mode streams, as we are seeing more and more devices - not just laptops but onboard audio devices - that only support 48khz and not 44.1khz. Should work with volume boost (gain calculated inside resample) and also downmixer (hands off intermediate scratch buffers). Buffer size increase: By default many of these games request a tiny buffer when in shared mode (TDJ uses 3ms). On some audio setup this results in crackling due to underflow. Add an option to forcibly increase the buffer size. ## Testing With resampler set to 48kHz and buffer set to 20ms I can reliably boot and play IIDX on my display port monitor's speakers; previously this wasn't possible. IIDX is event-driven. Tested SDVX7 as well at 48kHz, which opens timer-driven streams.
151 lines
6.8 KiB
C++
151 lines
6.8 KiB
C++
#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);
|
|
|
|
// 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<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;
|
|
};
|
|
}
|