Files
spice2x_spice2x.github.io/src/spice2x/hooks/audio/implementations/wave_out.cpp
T
bicarus 71ba9b6b47 os: implement win10 high-resolution timer as replacement for Sleep() / sleep_for() (#682)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #681 

## Description of change
`Sleep` and `sleep_for()` can be very inaccurate and varies depending on
what the OS gives us...

### `timeBeginPeriod(1)`
On boot, we are now calling `timeBeginPeriod(1)`, which affects the
whole process but makes `Sleep` more accurate. There is some risk here
if any game was relying on doing things like `Sleep(1)` and expecting it
to run for 15.6ms. Most games already call `timeBeginPeriod(1)` in the
game engine, though not the whole time, so I'm hoping that this is not
too impactful.

### Opt out of Win11 power throttling
Ensure that timer resolution change above is respected even when the
window is occluded / minimized by opting out of throttling via
`PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION`.

### Use Win10 high resolution timer instead of Sleep

On Win10 1803 and above, there is a new OS-level API for high resolution
timers; if this is available, use it
(`CREATE_WAITABLE_TIMER_HIGH_RESOLUTION`). Worth noting that WINE
doesn't support this currently.

If not, fall back to `Sleep`, which is significantly better than
`sleep_for()` in my experiments.

Callers of Sleep / sleep_for were replaced with this new timer. Most of
them anyway; calls to Sleep() with more than 100ms+ was left alone.

### Add an option as a chicken bit

To opt out I'm adding a new option called `Use Legacy Timers` which will
revert to behavior before this PR. The code paths that switched from
`sleep_for` to `Sleep` will remain in place though, not affected by the
option.

## Expected changes
In some I/O emulation modules, poll threads may run more frequently,
resulting in lower latency.

It also means that spice overall may use more CPU resources and power.
If you don't like this, you can always enable the option to opt out;
e.g., if you're on old arcade cab PC.

## Testing

DDR p4io - ok
drs touch hook - ok
IIDX camera hook - ok
CCJ trackball - ok
2026-05-08 02:25:20 -07:00

227 lines
7.0 KiB
C++

#include "wave_out.h"
#include "hooks/audio/audio.h"
#include "hooks/audio/backends/wasapi/audio_client.h"
#include "hooks/audio/backends/wasapi/defs.h"
#include "util/precise_timer.h"
static REFERENCE_TIME WASAPI_TARGET_REFTIME = TARGET_REFTIME;
HRESULT WaveOutBackend::init(uint32_t buffer_size) {
auto &format = hooks::audio::FORMAT.Format;
format.wFormatTag = WAVE_FORMAT_PCM;
log_info("audio::wave_out", "initializing waveOut backend with {} channels, {} Hz, {}-bit",
format.nChannels,
format.nSamplesPerSec,
format.wBitsPerSample);
log_info("audio::wave_out", "... nBlockAlign : {} bytes", format.nBlockAlign);
log_info("audio::wave_out", "... nAvgBytesPerSec : {} bytes", format.nAvgBytesPerSec);
log_info("audio::wave_out", "... buffer reftime : {} ms", WASAPI_TARGET_REFTIME / 10000.f);
log_info("audio::wave_out", "... buffer count : {} buffers", _countof(this->hdrs));
MMRESULT ret = waveOutOpen(
&this->handle,
WAVE_MAPPER,
reinterpret_cast<const WAVEFORMATEX *>(&hooks::audio::FORMAT.Format),
reinterpret_cast<DWORD_PTR>(this->dispatcher_event),
reinterpret_cast<DWORD_PTR>(nullptr),
CALLBACK_EVENT);
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to initialize waveOut backend, hr={:#08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
// initialize buffers
for (auto &hdr : this->hdrs) {
memset(&hdr, 0, sizeof(hdr));
hdr.lpData = new char[buffer_size] {};
hdr.dwBufferLength = buffer_size;
hdr.dwBytesRecorded = 0;
hdr.dwUser = 0;
hdr.dwFlags = 0;
hdr.dwLoops = 0;
hdr.lpNext = nullptr;
ret = waveOutPrepareHeader(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to prepare waveOut header, hr=0x{:08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to write waveOut header, hr=0x{:08x}",
static_cast<unsigned>(ret));
return static_cast<HRESULT>(ret);
}
}
// mark as initialized
this->initialized = true;
return S_OK;
}
const WAVEFORMATEXTENSIBLE &WaveOutBackend::format() const noexcept {
return hooks::audio::FORMAT;
}
HRESULT WaveOutBackend::on_initialize(
AUDCLNT_SHAREMODE *ShareMode,
DWORD *StreamFlags,
REFERENCE_TIME *hnsBufferDuration,
REFERENCE_TIME *hnsPeriodicity,
const WAVEFORMATEX *pFormat,
LPCGUID AudioSessionGuid) noexcept
{
*ShareMode = AUDCLNT_SHAREMODE_SHARED;
*StreamFlags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK |
AUDCLNT_STREAMFLAGS_RATEADJUST |
AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM |
AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
*hnsBufferDuration = WASAPI_TARGET_REFTIME;
*hnsPeriodicity = WASAPI_TARGET_REFTIME;
// this backend only supports stereo audio
if (pFormat->nChannels > 2) {
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
return S_OK;
}
HRESULT WaveOutBackend::on_get_buffer_size(uint32_t *buffer_frames) noexcept {
*buffer_frames = _countof(this->hdrs);
return S_OK;
}
HRESULT WaveOutBackend::on_get_stream_latency(REFERENCE_TIME *latency) noexcept {
*latency = WASAPI_TARGET_REFTIME;
return S_OK;
}
HRESULT WaveOutBackend::on_get_current_padding(std::optional<uint32_t> &padding_frames) noexcept {
size_t queued_bytes = 0;
for (auto &hdr : this->hdrs) {
if (hdr.dwFlags & WHDR_DONE) {
queued_bytes += static_cast<unsigned>(hdr.dwBufferLength);
}
}
auto frames = static_cast<uint32_t>(queued_bytes / hooks::audio::FORMAT.Format.nBlockAlign);
//log_info("audio::wave_out", "queued_bytes = {}, frames = {}", queued_bytes, frames);
padding_frames = frames;
return S_OK;
}
HRESULT WaveOutBackend::on_is_format_supported(
AUDCLNT_SHAREMODE *ShareMode,
const WAVEFORMATEX *pFormat,
WAVEFORMATEX **ppClosestMatch) noexcept
{
// always support 44.1 kHz, stereo, 16-bits per channel with custom backends
if (*ShareMode == AUDCLNT_SHAREMODE_EXCLUSIVE &&
pFormat->nChannels == 2 &&
pFormat->nSamplesPerSec == 44100 &&
pFormat->wBitsPerSample == 16)
{
return S_OK;
}
return AUDCLNT_E_UNSUPPORTED_FORMAT;
}
HRESULT WaveOutBackend::on_get_mix_format(WAVEFORMATEX **pp_device_format) noexcept {
return E_NOTIMPL;
}
HRESULT WaveOutBackend::on_get_device_period(
REFERENCE_TIME *default_device_period,
REFERENCE_TIME *minimum_device_period)
{
*default_device_period = WASAPI_TARGET_REFTIME;
*minimum_device_period = WASAPI_TARGET_REFTIME;
return S_OK;
}
HRESULT WaveOutBackend::on_start() noexcept {
return S_OK;
}
HRESULT WaveOutBackend::on_stop() noexcept {
return S_OK;
}
HRESULT WaveOutBackend::on_set_event_handle(HANDLE *event_handle) {
this->relay_event = *event_handle;
this->dispatcher_event = CreateEvent(nullptr, true, false, nullptr);
*event_handle = this->dispatcher_event;
return S_OK;
}
HRESULT WaveOutBackend::on_get_buffer(uint32_t num_frames_requested, BYTE **ppData) {
auto buffer_size = hooks::audio::FORMAT.Format.nBlockAlign * num_frames_requested;
if (!this->initialized) {
this->init(buffer_size);
}
// wait for a free slot
WaitForSingleObject(this->dispatcher_event, INFINITE);
// allocate temporary sound buffer
this->active_sound_buffer = reinterpret_cast<BYTE *>(CoTaskMemAlloc(buffer_size));
// hand the buffer to the callee
*ppData = this->active_sound_buffer;
return S_OK;
}
HRESULT WaveOutBackend::on_release_buffer(uint32_t num_frames_written, DWORD dwFlags) {
bool written = false;
timeutils::PreciseSleepTimer timer;
// reset the dispatcher event
ResetEvent(this->dispatcher_event);
while (!written) {
for (WAVEHDR &hdr : this->hdrs) {
if (hdr.dwFlags & WHDR_DONE) {
memcpy(hdr.lpData, this->active_sound_buffer, hdr.dwBufferLength);
// write the data to the device now
MMRESULT ret = waveOutWrite(this->handle, &hdr, sizeof(hdr));
if (ret != MMSYSERR_NOERROR) {
log_warning("audio::wave_out", "failed to write waveOut data, hr={:#08x}",
static_cast<unsigned>(ret));
}
written = true;
break;
}
}
// avoid pegging the CPU
if (!written) {
timer.sleep(1);
}
}
// free temporary sound buffer
CoTaskMemFree(this->active_sound_buffer);
this->active_sound_buffer = nullptr;
// trigger game audio callback
SetEvent(this->relay_event);
return S_OK;
}