Skip to content

Commit 6c79b87

Browse files
committed
cubeb - re-enable and polish IAudioClient3 to achieve lower latencies
1 parent 27d2a10 commit 6c79b87

File tree

1 file changed

+191
-39
lines changed

1 file changed

+191
-39
lines changed

src/cubeb_wasapi.cpp

Lines changed: 191 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@
77
#define _WIN32_WINNT 0x0603
88
#define NOMINMAX
99

10+
// Makes sure cubeb doesn't try to initialize an IAudioClient3 with unsupported formats, as it forces its own (e.g. unsupported bit depths or sample rates).
11+
// I'm not 100% sure this is right as IAudioClient::IsFormatSupported() might not be relevant with IAudioClient3, but it probably is.
12+
#define CHECK_MIXER_FORMAT_SUPPORT 1
13+
// Force raw output (skips some processing) with IAudioClient3
14+
#define USE_RAW_OUTPUT 0
15+
// Forces the output format to match our mixer one
16+
#define FORCE_MATCHING_OUTPUT_FORMAT 0
17+
// "Forces" (?) a IAudioClient3 to resample (in high quality) our stream to the device output sample rate
18+
#define CONVERT_STREAM 0
19+
// If we are targeting IAudioClient3, we don't want to limit the latency to the default latency (likely 10ms), but to the minimum one.
20+
// Though it seems that IAudioClient::GetDevicePeriod() returns lower latencies than GetSharedModeEnginePeriod() allows, so that's weird.
21+
#define ALLOW_MIN_LATENCY 0
22+
// Can we go lower than the user/client requested latency? This isn't particularly necessary as dolphin should produce enough audio samples to go with latencies < 10ms (which is what it asks for),
23+
// and the user requested latency is just a suggestion.
24+
#define FORCE_RESPECT_USER_LATENCY 0
25+
// IAudioClient3::GetSharedModeEnginePeriod() doesn't seem to work as well as IAudioClient::GetDevicePeriod() and often return a min and max latency of 10ms, even if lower ones would be supported
26+
#define USE_AUDIO_CLIENT_3_LATENCY 0
27+
1028
#include <algorithm>
1129
#include <atomic>
1230
#include <audioclient.h>
@@ -1867,6 +1885,45 @@ wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params,
18671885
return CUBEB_ERROR;
18681886
}
18691887

1888+
#if USE_AUDIO_CLIENT_3_LATENCY
1889+
//TODO: this is likely unreliable as we can't know the actual mixer format cubeb will ask for later on (we'd need to calculate it) (and the min latency could change based on that)
1890+
com_ptr<IAudioClient3> client3;
1891+
hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_INPROC_SERVER, NULL,
1892+
client3.receive_vpp());
1893+
if (SUCCEEDED(hr)) {
1894+
WAVEFORMATEX * mix_format = nullptr;
1895+
hr = client3->GetMixFormat(&mix_format);
1896+
1897+
if (SUCCEEDED(hr)) {
1898+
uint32_t default_period = 0, fundamental_period = 0, min_period = 0,
1899+
max_period = 0;
1900+
hr = client3->GetSharedModeEnginePeriod(
1901+
mix_format, &default_period, &fundamental_period, &min_period,
1902+
&max_period);
1903+
1904+
auto sample_rate = mix_format->nSamplesPerSec;
1905+
CoTaskMemFree(mix_format);
1906+
if (SUCCEEDED(hr)) {
1907+
// Keep values in the same format as IAudioDevice::GetDevicePeriod()
1908+
REFERENCE_TIME min_period_rt(frames_to_hns(sample_rate, min_period));
1909+
REFERENCE_TIME default_period_rt(frames_to_hns(sample_rate, default_period));
1910+
LOG("default device period: %I64d, minimum device period: %I64d",
1911+
default_period_rt, min_period_rt);
1912+
1913+
#if ALLOW_MIN_LATENCY
1914+
*latency_frames = min_period;
1915+
#else
1916+
*latency_frames = default_period; // This can be 0
1917+
#endif
1918+
1919+
LOG("Minimum latency in frames: %u", *latency_frames);
1920+
1921+
return CUBEB_OK;
1922+
}
1923+
}
1924+
}
1925+
#endif
1926+
18701927
com_ptr<IAudioClient> client;
18711928
hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL,
18721929
client.receive_vpp());
@@ -1891,10 +1948,8 @@ wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params,
18911948
synchronizing the stream and the engine.
18921949
http://msdn.microsoft.com/en-us/library/windows/desktop/dd370871%28v=vs.85%29.aspx
18931950
*/
1894-
1895-
// #ifdef _WIN32_WINNT_WIN10
1896-
#if 0
1897-
*latency_frames = hns_to_frames(params.rate, minimum_period);
1951+
#if defined(_WIN32_WINNT_WIN10) && ALLOW_MIN_LATENCY
1952+
*latency_frames = hns_to_frames(params.rate, minimum_period);
18981953
#else
18991954
*latency_frames = hns_to_frames(params.rate, default_period);
19001955
#endif
@@ -1987,7 +2042,10 @@ handle_channel_layout(cubeb_stream * stm, EDataFlow direction,
19872042
if (hr == S_FALSE) {
19882043
/* Channel layout not supported, but WASAPI gives us a suggestion. Use it,
19892044
and handle the eventual upmix/downmix ourselves. Ignore the subformat of
1990-
the suggestion, since it seems to always be IEEE_FLOAT. */
2045+
the suggestion, since it seems to always be IEEE_FLOAT.
2046+
This fallback doesn't update the bit depth, so if a device
2047+
only supported bit depths cubeb doesn't support, we will need to
2048+
make sure the stream is compatible with it (e.g. IAudioClient3 is not) */
19912049
LOG("Using WASAPI suggested format: channels: %d", closest->nChannels);
19922050
XASSERT(closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
19932051
WAVEFORMATEXTENSIBLE * closest_pcm =
@@ -2031,12 +2089,12 @@ initialize_iaudioclient2(com_ptr<IAudioClient> & audio_client)
20312089
return CUBEB_OK;
20322090
}
20332091

2034-
#if 0
20352092
bool
20362093
initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
20372094
cubeb_stream * stm,
20382095
const com_heap_ptr<WAVEFORMATEX> & mix_format,
2039-
DWORD flags, EDataFlow direction)
2096+
DWORD flags, EDataFlow direction,
2097+
REFERENCE_TIME latency_hns)
20402098
{
20412099
com_ptr<IAudioClient3> audio_client3;
20422100
audio_client->QueryInterface<IAudioClient3>(audio_client3.receive());
@@ -2072,37 +2130,98 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
20722130
// would do this, then stop and use IAudioClient instead.
20732131

20742132
HRESULT hr;
2133+
2134+
#if CHECK_MIXER_FORMAT_SUPPORT
2135+
WAVEFORMATEX * tmp = nullptr;
2136+
// The mixer format here might have bit depths "forced" by cubeb even if the output device doesn't directly support it.
2137+
// This doesn't work with IAudioClient3 as there it applies no resampling.
2138+
hr = audio_client3->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, mix_format.get(), &tmp);
2139+
if (FAILED(hr)) {
2140+
LOG("IAudioClient3 attempted format is not supported: error: %lx", hr);
2141+
return false;
2142+
}
2143+
CoTaskMemFree(tmp);
2144+
#endif
2145+
20752146
uint32_t default_period = 0, fundamental_period = 0, min_period = 0,
20762147
max_period = 0;
2148+
//TODO: review... this returns the same default/min/max latency every time on my PC (10ms, which is high),
2149+
//independently of the device and its settings (with no other apps running). Not sure if it means that lag would be bigger than with IAudioDevice(1).
20772150
hr = audio_client3->GetSharedModeEnginePeriod(
20782151
mix_format.get(), &default_period, &fundamental_period, &min_period,
20792152
&max_period);
20802153
if (FAILED(hr)) {
20812154
LOG("Could not get shared mode engine period: error: %lx", hr);
20822155
return false;
20832156
}
2084-
uint32_t requested_latency = stm->latency;
2085-
if (requested_latency >= default_period) {
2086-
LOG("Requested latency %i greater than default latency %i, not using "
2157+
uint32_t requested_latency = hns_to_frames(mix_format->nSamplesPerSec, latency_hns);
2158+
#if FORCE_RESPECT_USER_LATENCY
2159+
if (requested_latency > max_period) {
2160+
// Fallback to IAudioClient(1) as it's less restrictive towards bigger latencies
2161+
LOG("Requested latency %i greater than max latency %i, not using "
20872162
"IAudioClient3",
2088-
requested_latency, default_period);
2163+
requested_latency, max_period);
20892164
return false;
20902165
}
2166+
#endif
20912167
LOG("Got shared mode engine period: default=%i fundamental=%i min=%i max=%i",
20922168
default_period, fundamental_period, min_period, max_period);
20932169
// Snap requested latency to a valid value
20942170
uint32_t old_requested_latency = requested_latency;
2171+
// The period is required to be a multiple of the fundamental period (and >= min and <= max, which should still be true)
2172+
requested_latency -= requested_latency % fundamental_period;
20952173
if (requested_latency < min_period) {
20962174
requested_latency = min_period;
20972175
}
2098-
requested_latency -= (requested_latency - min_period) % fundamental_period;
2176+
// This is likely unnecessary, but it won't hurt
2177+
if (requested_latency > max_period) {
2178+
requested_latency = max_period;
2179+
}
20992180
if (requested_latency != old_requested_latency) {
21002181
LOG("Requested latency %i was adjusted to %i", old_requested_latency,
21012182
requested_latency);
21022183
}
21032184

2104-
hr = audio_client3->InitializeSharedAudioStream(flags, requested_latency,
2185+
#if USE_RAW_OUTPUT || FORCE_MATCHING_OUTPUT_FORMAT
2186+
AudioClientProperties properties = {0};
2187+
properties.cbSize = sizeof(AudioClientProperties);
2188+
properties.bIsOffload = false; //TODO: review this (take from chromium)
2189+
#ifndef __MINGW32__
2190+
#if USE_RAW_OUTPUT
2191+
// Raw audio streams skip some kinds of processing, like AEC and AGC.
2192+
// Do this independent from CUBEB_STREAM_PREF_RAW, we force this with IAudioClient3, to hopefully get the lowest latency/quality and support.
2193+
properties.Options |= AUDCLNT_STREAMOPTIONS_RAW;
2194+
#endif
2195+
#if FORCE_MATCHING_OUTPUT_FORMAT
2196+
properties.Options |= AUDCLNT_STREAMOPTIONS_MATCH_FORMAT;
2197+
#endif
2198+
#endif
2199+
com_ptr<IAudioClient2> audio_client2;
2200+
hr = audio_client->QueryInterface<IAudioClient2>(audio_client2.receive());
2201+
if (audio_client2) {
2202+
hr = audio_client2->SetClientProperties(&properties);
2203+
}
2204+
if (FAILED(hr)) {
2205+
LOG("Could not set IAudioClient2 properties: error: %lx", hr);
2206+
// This is not fatal, it should work anyway (AUDCLNT_STREAMOPTIONS_RAW might not be supported)
2207+
}
2208+
#endif
2209+
2210+
DWORD new_flags = flags;
2211+
#if CONVERT_STREAM
2212+
// Always add these flags to IAudioClient3 (they can help if the stream doesn't have the same format as the output)
2213+
new_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM;
2214+
new_flags |= AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
2215+
#endif
2216+
2217+
hr = audio_client3->InitializeSharedAudioStream(new_flags, requested_latency,
21052218
mix_format.get(), NULL);
2219+
if (hr == AUDCLNT_E_INVALID_STREAM_FLAG && CONVERT_STREAM) {
2220+
LOG("Got AUDCLNT_E_INVALID_STREAM_FLAG, removing some flag");
2221+
hr = audio_client3->InitializeSharedAudioStream(flags, requested_latency,
2222+
mix_format.get(), NULL);
2223+
}
2224+
21062225
if (SUCCEEDED(hr)) {
21072226
return true;
21082227
} else if (hr == AUDCLNT_E_ENGINE_PERIODICITY_LOCKED) {
@@ -2114,22 +2233,48 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
21142233
}
21152234

21162235
uint32_t current_period = 0;
2117-
WAVEFORMATEX * current_format = nullptr;
2236+
WAVEFORMATEX * current_format_ptr = nullptr;
21182237
// We have to pass a valid WAVEFORMATEX** and not nullptr, otherwise
21192238
// GetCurrentSharedModeEnginePeriod will return E_POINTER
2120-
hr = audio_client3->GetCurrentSharedModeEnginePeriod(&current_format,
2239+
hr = audio_client3->GetCurrentSharedModeEnginePeriod(&current_format_ptr,
21212240
&current_period);
2122-
CoTaskMemFree(current_format);
21232241
if (FAILED(hr)) {
21242242
LOG("Could not get current shared mode engine period: error: %lx", hr);
21252243
return false;
21262244
}
2245+
com_heap_ptr<WAVEFORMATEX> current_format(current_format_ptr);
2246+
#if 1
2247+
// Unless some other external app locked the shared mode engine period within our audio initialization,
2248+
// this likely shouldn't happen
2249+
if (current_format->nSamplesPerSec != mix_format->nSamplesPerSec)
2250+
{
2251+
hr = audio_client3->GetSharedModeEnginePeriod(
2252+
current_format.get(), &default_period, &fundamental_period, &min_period,
2253+
&max_period);
2254+
if (FAILED(hr)) {
2255+
LOG("IAudioClient3::GetCurrentSharedModeEnginePeriod() returned a different mixer format (nSamplesPerSec) from IAudioClient::GetMixFormat(); not using IAudioClient3");
2256+
return false;
2257+
}
2258+
LOG("IAudioClient3::GetCurrentSharedModeEnginePeriod() returned a different mixer format (nSamplesPerSec) from IAudioClient::GetMixFormat(); attempting a matching latency");
2259+
REFERENCE_TIME current_period_hns = frames_to_hns(current_format->nSamplesPerSec, current_period);
2260+
current_period = hns_to_frames(mix_format->nSamplesPerSec, current_period_hns);
2261+
current_period -= current_period % fundamental_period;
2262+
// Note: the follow up IAudioClient3::InitializeSharedAudioStream() is likely to fail anyway,
2263+
// given that the period was already locked and we changed it.
2264+
// The only right way to proceed would be to change the "mix_format->nSamplesPerSec" to "current_format->nSamplesPerSec",
2265+
// but we can't do that for now.
2266+
}
2267+
#endif
21272268

2128-
if (current_period >= default_period) {
2129-
LOG("Current shared mode engine period %i too high, not using IAudioClient",
2130-
current_period);
2269+
#if FORCE_RESPECT_USER_LATENCY
2270+
// This used to return false if current_period was >= default_latency, but that didn't seem to make sense
2271+
if (old_requested_latency > current_period) {
2272+
LOG("Requested latency %i greater than currently locked shared mode latency %i, not using "
2273+
"IAudioClient3",
2274+
old_requested_latency, current_period);
21312275
return false;
21322276
}
2277+
#endif
21332278

21342279
hr = audio_client3->InitializeSharedAudioStream(flags, current_period,
21352280
mix_format.get(), NULL);
@@ -2142,7 +2287,6 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
21422287
LOG("Could not initialize shared stream with IAudioClient3: error: %lx", hr);
21432288
return false;
21442289
}
2145-
#endif
21462290

21472291
#define DIRECTION_NAME (direction == eCapture ? "capture" : "render")
21482292

@@ -2166,6 +2310,8 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
21662310
return CUBEB_ERROR;
21672311
}
21682312

2313+
const bool has_capture = direction == eCapture || direction == eAll;
2314+
21692315
stm->stream_reset_lock.assert_current_thread_owns();
21702316
// If user doesn't specify a particular device, we can choose another one when
21712317
// the given devid is unavailable.
@@ -2202,17 +2348,17 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
22022348

22032349
/* Get a client. We will get all other interfaces we need from
22042350
* this pointer. */
2205-
#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
2206-
hr = device->Activate(__uuidof(IAudioClient3),
2207-
CLSCTX_INPROC_SERVER,
2208-
NULL, audio_client.receive_vpp());
2209-
if (hr == E_NOINTERFACE) {
2210-
#endif
2211-
hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL,
2212-
audio_client.receive_vpp());
2213-
#if 0
2351+
if (!has_capture) {
2352+
hr = device->Activate(__uuidof(IAudioClient3),
2353+
CLSCTX_INPROC_SERVER,
2354+
NULL, audio_client.receive_vpp());
2355+
}
2356+
// IAudioClient3 has problems with capture sessions:
2357+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
2358+
if (has_capture || hr == E_NOINTERFACE) {
2359+
hr = device->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, NULL,
2360+
audio_client.receive_vpp());
22142361
}
2215-
#endif
22162362

22172363
if (FAILED(hr)) {
22182364
LOG("Could not activate the device to get an audio"
@@ -2341,16 +2487,12 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
23412487
}
23422488
}
23432489

2344-
#if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
2345-
if (initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction)) {
2490+
if (!has_capture && initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction, latency_hns)) {
23462491
LOG("Initialized with IAudioClient3");
23472492
} else {
2348-
#endif
2349-
hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, latency_hns, 0,
2350-
mix_format.get(), NULL);
2351-
#if 0
2493+
hr = audio_client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, latency_hns, 0, mix_format.get(), NULL);
23522494
}
2353-
#endif
2495+
23542496
if (FAILED(hr)) {
23552497
LOG("Unable to initialize audio client for %s: %lx.", DIRECTION_NAME, hr);
23562498
return CUBEB_ERROR;
@@ -3310,6 +3452,7 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
33103452
CUBEB_DEVICE_FMT_S16NE);
33113453
ret.default_format = CUBEB_DEVICE_FMT_F32NE;
33123454
prop_variant fmtvar;
3455+
WAVEFORMATEX* wfx = NULL;
33133456
hr = propstore->GetValue(PKEY_AudioEngine_DeviceFormat, &fmtvar);
33143457
if (SUCCEEDED(hr) && fmtvar.vt == VT_BLOB) {
33153458
if (fmtvar.blob.cbSize == sizeof(PCMWAVEFORMAT)) {
@@ -3319,8 +3462,7 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
33193462
ret.max_rate = ret.min_rate = ret.default_rate = pcm->wf.nSamplesPerSec;
33203463
ret.max_channels = pcm->wf.nChannels;
33213464
} else if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX)) {
3322-
WAVEFORMATEX * wfx =
3323-
reinterpret_cast<WAVEFORMATEX *>(fmtvar.blob.pBlobData);
3465+
wfx = reinterpret_cast<WAVEFORMATEX *>(fmtvar.blob.pBlobData);
33243466

33253467
if (fmtvar.blob.cbSize >= sizeof(WAVEFORMATEX) + wfx->cbSize ||
33263468
wfx->wFormatTag == WAVE_FORMAT_PCM) {
@@ -3330,6 +3472,16 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
33303472
}
33313473
}
33323474

3475+
#if USE_AUDIO_CLIENT_3_LATENCY
3476+
// Here we guess that an IAudioClient3 stream will successfully be initialized later (it might fail).
3477+
com_ptr<IAudioClient3> client3;
3478+
uint32_t def, fun, min, max;
3479+
if (wfx && SUCCEEDED(dev->Activate(__uuidof(IAudioClient3), CLSCTX_INPROC_SERVER, NULL, client3.receive_vpp()))
3480+
&& SUCCEEDED(client3->GetSharedModeEnginePeriod(wfx, &def, &fun, &min, &max))) {
3481+
ret.latency_lo = min;
3482+
ret.latency_hi = def;
3483+
} else
3484+
#endif
33333485
if (SUCCEEDED(dev->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER,
33343486
NULL, client.receive_vpp())) &&
33353487
SUCCEEDED(client->GetDevicePeriod(&def_period, &min_period))) {

0 commit comments

Comments
 (0)