mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-08 14:48:03 +03:00
## 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
227 lines
7.0 KiB
C++
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;
|
|
}
|