audio: WASAPI exclusive resampling, buffer size increase options (#727)

## 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.
This commit is contained in:
bicarus
2026-06-02 01:55:50 -07:00
committed by GitHub
parent b517ef3182
commit 48033816a8
17 changed files with 967 additions and 128 deletions
@@ -7,6 +7,7 @@
#include "audio_client.h"
#include "hooks/audio/audio.h"
#include "util.h"
#include "wasapi_private.h"
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
@@ -17,9 +18,7 @@ static void apply_gain(BYTE *buffer, UINT32 frames, const WAVEFORMATEXTENSIBLE &
const WAVEFORMATEX &f = fmt.Format;
const size_t samples = (size_t) frames * f.nChannels;
// KSDATAFORMAT_SUBTYPE_IEEE_FLOAT has Data1 == 3, _PCM has Data1 == 1
bool is_float = f.wFormatTag == WAVE_FORMAT_IEEE_FLOAT
|| (f.wFormatTag == WAVE_FORMAT_EXTENSIBLE && fmt.SubFormat.Data1 == 0x00000003);
bool is_float = is_ieee_float(&f);
if (is_float && f.wBitsPerSample == 32) {
auto p = reinterpret_cast<float *>(buffer);
@@ -113,10 +112,23 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesR
return S_OK;
}
// downmix + resample chained: the game writes its multi-channel native-rate audio into the
// downmix scratch, which is downmixed to stereo and then resampled on release. size the
// resampler's (stereo) input scratch now and hand the game the multi-channel downmix scratch.
if (this->client->downmix.enabled && this->client->resample.enabled) {
BYTE *resample_scratch = nullptr;
this->client->resample.get_buffer(NumFramesRequested, &resample_scratch);
CHECK_RESULT(this->client->downmix.get_scratch(NumFramesRequested, ppData));
// surround downmix: reserve the real (stereo) device buffer, but hand the game a
// multi-channel scratch buffer that we downmix on release
if (this->client->downmix.enabled) {
} else if (this->client->downmix.enabled) {
CHECK_RESULT(this->client->downmix.get_buffer(pReal, NumFramesRequested, ppData));
// resample: hand the game a native-rate scratch buffer that we convert on release. the real
// device buffer is acquired in ReleaseBuffer once the converted frame count is known.
} else if (this->client->resample.enabled) {
CHECK_RESULT(this->client->resample.get_buffer(NumFramesRequested, ppData));
}
// call original
@@ -143,6 +155,34 @@ HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFra
return S_OK;
}
// downmix + resample chained: downmix the game's multi-channel scratch into the resampler's
// stereo input scratch, then let the resampler convert and push it to the device. a silent
// buffer skips the downmix and feeds silence straight through.
if (this->client->downmix.enabled && this->client->resample.enabled) {
if ((dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
this->client->downmix.downmix_into(
this->client->resample.input_data(), NumFramesWritten);
}
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
}
// resample: convert the game's native-rate scratch and push as many output frames as the
// device has room for, applying the volume boost to the converted output. handles acquiring
// and releasing the real device buffer itself.
if (this->client->resample.enabled) {
return this->client->resample.flush(
pReal,
this->client->pReal,
NumFramesWritten,
dwFlags,
hooks::audio::VOLUME_BOOST);
}
// resolve the real device buffer for whichever path produced the audio
BYTE *device_buffer;
if (this->client->downmix.enabled) {