Merge pull request #38210 from benjarmstrong/wasapi-audio-output-latency

Add support for variable output latency in WASAPI audio driver
This commit is contained in:
K. S. Ernest (iFire) Lee 2021-08-22 15:35:36 -07:00 committed by GitHub
commit 7560ba8aa1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 131 additions and 18 deletions

View file

@ -35,8 +35,60 @@
#include "core/config/project_settings.h"
#include "core/os/os.h"
#include <stdint.h> // INT32_MAX
#include <functiondiscoverykeys.h>
// Define IAudioClient3 if not already defined by MinGW headers
#if defined __MINGW32__ || defined __MINGW64__
#ifndef __IAudioClient3_FWD_DEFINED__
#define __IAudioClient3_FWD_DEFINED__
typedef interface IAudioClient3 IAudioClient3;
#endif // __IAudioClient3_FWD_DEFINED__
#ifndef __IAudioClient3_INTERFACE_DEFINED__
#define __IAudioClient3_INTERFACE_DEFINED__
MIDL_INTERFACE("7ED4EE07-8E67-4CD4-8C1A-2B7A5987AD42")
IAudioClient3 : public IAudioClient2 {
public:
virtual HRESULT STDMETHODCALLTYPE GetSharedModeEnginePeriod(
/* [annotation][in] */
_In_ const WAVEFORMATEX *pFormat,
/* [annotation][out] */
_Out_ UINT32 *pDefaultPeriodInFrames,
/* [annotation][out] */
_Out_ UINT32 *pFundamentalPeriodInFrames,
/* [annotation][out] */
_Out_ UINT32 *pMinPeriodInFrames,
/* [annotation][out] */
_Out_ UINT32 *pMaxPeriodInFrames) = 0;
virtual HRESULT STDMETHODCALLTYPE GetCurrentSharedModeEnginePeriod(
/* [unique][annotation][out] */
_Out_ WAVEFORMATEX * *ppFormat,
/* [annotation][out] */
_Out_ UINT32 * pCurrentPeriodInFrames) = 0;
virtual HRESULT STDMETHODCALLTYPE InitializeSharedAudioStream(
/* [annotation][in] */
_In_ DWORD StreamFlags,
/* [annotation][in] */
_In_ UINT32 PeriodInFrames,
/* [annotation][in] */
_In_ const WAVEFORMATEX *pFormat,
/* [annotation][in] */
_In_opt_ LPCGUID AudioSessionGuid) = 0;
};
__CRT_UUID_DECL(IAudioClient3, 0x7ED4EE07, 0x8E67, 0x4CD4, 0x8C, 0x1A, 0x2B, 0x7A, 0x59, 0x87, 0xAD, 0x42)
#endif // __IAudioClient3_INTERFACE_DEFINED__
#endif // __MINGW32__ || __MINGW64__
#ifndef PKEY_Device_FriendlyName
#undef DEFINE_PROPERTYKEY
@ -51,6 +103,7 @@ DEFINE_PROPERTYKEY(PKEY_Device_FriendlyName, 0xa45c254e, 0xdf1c, 0x4efd, 0x80, 0
const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
const IID IID_IAudioClient = __uuidof(IAudioClient);
const IID IID_IAudioClient3 = __uuidof(IAudioClient3);
const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);
const IID IID_IAudioCaptureClient = __uuidof(IAudioCaptureClient);
@ -221,7 +274,22 @@ Error AudioDriverWASAPI::audio_device_init(AudioDeviceWASAPI *p_device, bool p_c
ERR_PRINT("WASAPI: RegisterEndpointNotificationCallback error");
}
hr = device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void **)&p_device->audio_client);
bool using_audio_client_3 = !p_capture; // IID_IAudioClient3 is only used for adjustable output latency (not input)
if (using_audio_client_3) {
hr = device->Activate(IID_IAudioClient3, CLSCTX_ALL, nullptr, (void **)&p_device->audio_client);
if (hr != S_OK) {
// IID_IAudioClient3 will never activate on OS versions before Windows 10.
// Older Windows versions should fall back gracefully.
using_audio_client_3 = false;
print_verbose("WASAPI: Couldn't activate device with IAudioClient3 interface, falling back to IAudioClient interface");
} else {
print_verbose("WASAPI: Activated device using IAudioClient3 interface");
}
}
if (!using_audio_client_3) {
hr = device->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void **)&p_device->audio_client);
}
SAFE_RELEASE(device)
if (reinit) {
@ -232,6 +300,16 @@ Error AudioDriverWASAPI::audio_device_init(AudioDeviceWASAPI *p_device, bool p_c
ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN);
}
if (using_audio_client_3) {
AudioClientProperties audioProps;
audioProps.cbSize = sizeof(AudioClientProperties);
audioProps.bIsOffload = FALSE;
audioProps.eCategory = AudioCategory_GameEffects;
hr = ((IAudioClient3 *)p_device->audio_client)->SetClientProperties(&audioProps);
ERR_FAIL_COND_V_MSG(hr != S_OK, ERR_CANT_OPEN, "WASAPI: SetClientProperties failed with error 0x" + String::num_uint64(hr, 16) + ".");
}
hr = p_device->audio_client->GetMixFormat(&pwfex);
ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN);
@ -285,15 +363,55 @@ Error AudioDriverWASAPI::audio_device_init(AudioDeviceWASAPI *p_device, bool p_c
}
}
DWORD streamflags = 0;
if ((DWORD)mix_rate != pwfex->nSamplesPerSec) {
streamflags |= AUDCLNT_STREAMFLAGS_RATEADJUST;
pwfex->nSamplesPerSec = mix_rate;
pwfex->nAvgBytesPerSec = pwfex->nSamplesPerSec * pwfex->nChannels * (pwfex->wBitsPerSample / 8);
}
if (!using_audio_client_3) {
DWORD streamflags = 0;
if ((DWORD)mix_rate != pwfex->nSamplesPerSec) {
streamflags |= AUDCLNT_STREAMFLAGS_RATEADJUST;
pwfex->nSamplesPerSec = mix_rate;
pwfex->nAvgBytesPerSec = pwfex->nSamplesPerSec * pwfex->nChannels * (pwfex->wBitsPerSample / 8);
}
hr = p_device->audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, streamflags, p_capture ? REFTIMES_PER_SEC : 0, 0, pwfex, nullptr);
ERR_FAIL_COND_V_MSG(hr != S_OK, ERR_CANT_OPEN, "WASAPI: Initialize failed with error 0x" + String::num_uint64(hr, 16) + ".");
UINT32 max_frames;
HRESULT hr = p_device->audio_client->GetBufferSize(&max_frames);
ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN);
hr = p_device->audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, streamflags, p_capture ? REFTIMES_PER_SEC : 0, 0, pwfex, nullptr);
ERR_FAIL_COND_V_MSG(hr != S_OK, ERR_CANT_OPEN, "WASAPI: Initialize failed with error 0x" + String::num_uint64(hr, 16) + ".");
// Due to WASAPI Shared Mode we have no control of the buffer size
buffer_frames = max_frames;
} else {
IAudioClient3 *device_audio_client_3 = (IAudioClient3 *)p_device->audio_client;
// AUDCLNT_STREAMFLAGS_RATEADJUST is an invalid flag with IAudioClient3, therefore we have to use
// the closest supported mix rate supported by the audio driver.
mix_rate = pwfex->nSamplesPerSec;
print_verbose("WASAPI: mix_rate = " + itos(mix_rate));
UINT32 default_period_frames, fundamental_period_frames, min_period_frames, max_period_frames;
hr = device_audio_client_3->GetSharedModeEnginePeriod(
pwfex,
&default_period_frames,
&fundamental_period_frames,
&min_period_frames,
&max_period_frames);
ERR_FAIL_COND_V_MSG(hr != S_OK, ERR_CANT_OPEN, "WASAPI: GetSharedModeEnginePeriod failed with error 0x" + String::num_uint64(hr, 16) + ".");
// Period frames must be an integral multiple of fundamental_period_frames or IAudioClient3 initialization will fail,
// so we need to select the closest multiple to the user-specified latency.
UINT32 desired_period_frames = target_latency_ms * mix_rate / 1000;
UINT32 period_frames = (desired_period_frames / fundamental_period_frames) * fundamental_period_frames;
if (ABS((int64_t)period_frames - (int64_t)desired_period_frames) > ABS((int64_t)(period_frames + fundamental_period_frames) - (int64_t)desired_period_frames)) {
period_frames = period_frames + fundamental_period_frames;
}
period_frames = CLAMP(period_frames, min_period_frames, max_period_frames);
print_verbose("WASAPI: fundamental_period_frames = " + itos(fundamental_period_frames));
print_verbose("WASAPI: min_period_frames = " + itos(min_period_frames));
print_verbose("WASAPI: max_period_frames = " + itos(max_period_frames));
print_verbose("WASAPI: selected a period frame size of " + itos(period_frames));
buffer_frames = period_frames;
hr = device_audio_client_3->InitializeSharedAudioStream(0, period_frames, pwfex, nullptr);
ERR_FAIL_COND_V_MSG(hr != S_OK, ERR_CANT_OPEN, "WASAPI: InitializeSharedAudioStream failed with error 0x" + String::num_uint64(hr, 16) + ".");
}
if (p_capture) {
hr = p_device->audio_client->GetService(IID_IAudioCaptureClient, (void **)&p_device->capture_client);
@ -328,13 +446,6 @@ Error AudioDriverWASAPI::init_render_device(bool reinit) {
break;
}
UINT32 max_frames;
HRESULT hr = audio_output.audio_client->GetBufferSize(&max_frames);
ERR_FAIL_COND_V(hr != S_OK, ERR_CANT_OPEN);
// Due to WASAPI Shared Mode we have no control of the buffer size
buffer_frames = max_frames;
// Sample rate is independent of channels (ref: https://stackoverflow.com/questions/11048825/audio-sample-frequency-rely-on-channels)
samples_in.resize(buffer_frames * channels);
@ -367,7 +478,6 @@ Error AudioDriverWASAPI::audio_device_finish(AudioDeviceWASAPI *p_device) {
if (p_device->audio_client) {
p_device->audio_client->Stop();
}
p_device->active = false;
}
@ -389,6 +499,8 @@ Error AudioDriverWASAPI::finish_capture_device() {
Error AudioDriverWASAPI::init() {
mix_rate = GLOBAL_GET("audio/driver/mix_rate");
target_latency_ms = GLOBAL_GET("audio/output_latency");
Error err = init_render_device();
if (err != OK) {
ERR_PRINT("WASAPI: init_render_device error");

View file

@ -71,6 +71,7 @@ class AudioDriverWASAPI : public AudioDriver {
unsigned int channels = 0;
int mix_rate = 0;
int buffer_frames = 0;
int target_latency_ms = 0;
bool thread_exited = false;
mutable bool exit_thread = false;
@ -114,5 +115,5 @@ public:
AudioDriverWASAPI();
};
#endif // WASAPI_ENABLED
#endif // AUDIO_DRIVER_WASAPI_H
#endif