mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-05 17:48:13 -07:00
4af006e869
## Link to GitHub Issue or related Pull Request, if one exists Building on #745 ## Description of change With `-wasapishared`, some users see `AUDCLNT_E_BUFFER_TOO_LARGE` (`0x88890006`). The existing one-device-period buffer clamp wasn't enough on endpoints with a small shared buffer; the shared buffer is double-buffered, so a full-buffer write often exceeds the free space. ### Fix Added a FIFO bridge to `SharedRedirect` that decouples the game's per-event writes from the shared engine's clock: - **`GetBuffer`** hands the game a pointer into the FIFO tail to write in place. - **`ReleaseBuffer`** commits the write and drains `min(pending, device_free)` frames to the device - so a write can never exceed what the device accepts, structurally preventing the overflow on any endpoint. - **`GetCurrentPadding`** reports the FIFO fill level (capped to the reported buffer size) so poll/timer-driven games pace correctly against the virtual buffer. ## Testing
194 lines
7.0 KiB
C++
194 lines
7.0 KiB
C++
#include "audio_render_client.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstring>
|
|
|
|
#include "audio_client.h"
|
|
#include "hooks/audio/audio.h"
|
|
#include "util.h"
|
|
#include "wasapi_private.h"
|
|
|
|
const char CLASS_NAME[] = "WrappedIAudioRenderClient";
|
|
|
|
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::QueryInterface(REFIID riid, void **ppvObj) {
|
|
if (ppvObj == nullptr) {
|
|
return E_POINTER;
|
|
}
|
|
|
|
if (riid == IID_WrappedIAudioRenderClient ||
|
|
riid == IID_IAudioRenderClient)
|
|
{
|
|
this->AddRef();
|
|
*ppvObj = this;
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
return pReal->QueryInterface(riid, ppvObj);
|
|
}
|
|
ULONG STDMETHODCALLTYPE WrappedIAudioRenderClient::AddRef() {
|
|
return pReal->AddRef();
|
|
}
|
|
ULONG STDMETHODCALLTYPE WrappedIAudioRenderClient::Release() {
|
|
|
|
// get reference count of underlying interface
|
|
ULONG refs = pReal != nullptr ? pReal->Release() : 0;
|
|
|
|
if (refs == 0) {
|
|
delete this;
|
|
}
|
|
|
|
return refs;
|
|
}
|
|
|
|
// IAudioRenderClient
|
|
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::GetBuffer(UINT32 NumFramesRequested, BYTE **ppData) {
|
|
static std::once_flag printed;
|
|
std::call_once(printed, []() {
|
|
log_misc("audio::wasapi", "WrappedIAudioRenderClient::GetBuffer");
|
|
});
|
|
|
|
if (this->client->backend) {
|
|
SAFE_CALL("AudioBackend", "on_get_buffer", this->client->backend->on_get_buffer(
|
|
NumFramesRequested,
|
|
ppData));
|
|
|
|
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
|
|
} 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));
|
|
|
|
// shared-mode redirect bridge: point the game at the FIFO tail it can always fill, decoupling
|
|
// its per-event writes from the shared engine's clock. the real device buffer is acquired in
|
|
// ReleaseBuffer and filled only as fast as the device frees space (see SharedRedirect::drain).
|
|
} else if (this->client->shared.bridge_enabled()) {
|
|
*ppData = this->client->shared.begin_write(NumFramesRequested);
|
|
|
|
return S_OK;
|
|
}
|
|
|
|
// call original
|
|
HRESULT ret = pReal->GetBuffer(NumFramesRequested, ppData);
|
|
|
|
// store buffer reference
|
|
if (SUCCEEDED(ret)) {
|
|
this->audio_buffer = *ppData;
|
|
}
|
|
|
|
CHECK_RESULT(ret);
|
|
}
|
|
HRESULT STDMETHODCALLTYPE WrappedIAudioRenderClient::ReleaseBuffer(UINT32 NumFramesWritten, DWORD dwFlags) {
|
|
static std::once_flag printed;
|
|
std::call_once(printed, []() {
|
|
log_misc("audio::wasapi", "WrappedIAudioRenderClient::ReleaseBuffer");
|
|
});
|
|
|
|
if (this->client->backend) {
|
|
SAFE_CALL("AudioBackend", "on_release_buffer", this->client->backend->on_release_buffer(
|
|
NumFramesWritten,
|
|
dwFlags));
|
|
|
|
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);
|
|
}
|
|
|
|
// shared-mode redirect bridge: queue the game's write and drain it to the device at the
|
|
// device's own pace, so a full-buffer write never overflows the shared buffer.
|
|
if (this->client->shared.bridge_enabled()) {
|
|
this->client->shared.commit_write(
|
|
NumFramesWritten, (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) != 0);
|
|
|
|
return this->client->shared.drain(
|
|
pReal,
|
|
this->client->pReal,
|
|
this->client->device_format,
|
|
hooks::audio::VOLUME_BOOST);
|
|
}
|
|
|
|
// resolve the real device buffer for whichever path produced the audio
|
|
BYTE *device_buffer;
|
|
if (this->client->downmix.enabled) {
|
|
|
|
// downmix the game's multi-channel scratch into the real stereo buffer held since GetBuffer
|
|
this->client->downmix.write_device_buffer(NumFramesWritten, dwFlags);
|
|
device_buffer = this->client->downmix.current_buffer();
|
|
} else {
|
|
device_buffer = this->audio_buffer;
|
|
|
|
// mute the first few buffers to avoid a startup pop
|
|
if (this->buffers_to_mute > 0 && this->client->frame_size > 0) {
|
|
memset(this->audio_buffer, 0, NumFramesWritten * this->client->frame_size);
|
|
this->buffers_to_mute--;
|
|
}
|
|
}
|
|
|
|
// boost the final output volume just before it reaches the device, layout-agnostic
|
|
if (hooks::audio::VOLUME_BOOST != 1.0f
|
|
&& device_buffer != nullptr
|
|
&& (dwFlags & AUDCLNT_BUFFERFLAGS_SILENT) == 0) {
|
|
static std::once_flag boost_printed;
|
|
std::call_once(boost_printed, []() {
|
|
log_info("audio::wasapi", "volume boost active: gain={}", hooks::audio::VOLUME_BOOST);
|
|
});
|
|
apply_gain(device_buffer, NumFramesWritten, this->client->device_format,
|
|
hooks::audio::VOLUME_BOOST);
|
|
}
|
|
|
|
HRESULT ret = pReal->ReleaseBuffer(NumFramesWritten, dwFlags);
|
|
|
|
if (this->client->downmix.enabled) {
|
|
this->client->downmix.buffer_released();
|
|
}
|
|
|
|
if (FAILED(ret)) {
|
|
PRINT_FAILED_RESULT(CLASS_NAME, __func__, ret);
|
|
}
|
|
|
|
return ret;
|
|
}
|