33
33
#include " cubeb_tracing.h"
34
34
#include " cubeb_utils.h"
35
35
36
+ // Some people have reported glitches with IAudioClient3 capture streams:
37
+ // http://blog.nirbheek.in/2018/03/low-latency-audio-on-windows-with.html
38
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
39
+ #define ALLOW_AUDIO_CLIENT_3_FOR_INPUT 0
40
+ // IAudioClient3::GetSharedModeEnginePeriod() seem to return min latencies
41
+ // bigger than IAudioClient::GetDevicePeriod(), which is confusing (10ms vs
42
+ // 3ms), though the default latency is usually the same and we should use the
43
+ // IAudioClient3 function anyway, as it's more correct
44
+ #define USE_AUDIO_CLIENT_3_MIN_PERIOD 1
45
+ // If this is true, we allow IAudioClient3 the creation of sessions with a
46
+ // latency above the default one (usually 10ms).
47
+ // Whether we should default this to true or false depend on many things:
48
+ // -Does creating a shared IAudioClient3 session (not locked to a format)
49
+ // actually forces all the IAudioClient(1) sessions to have the same latency?
50
+ // I could find no proof of that.
51
+ // -Does creating a shared IAudioClient3 session with a latency >= the default
52
+ // one actually improve the latency (as in how late the audio is) at all?
53
+ // -Maybe we could expose this as cubeb stream pref
54
+ // (e.g. take priority over other apps)?
55
+ #define ALLOW_AUDIO_CLIENT_3_LATENCY_OVER_DEFAULT 1
56
+ // If this is true and the user specified a target latency >= the IAudioClient3
57
+ // max one, then we reject it and fall back to IAudioClient(1). There wouldn't
58
+ // be much point in having a low latency if that's not what the user wants.
59
+ #define REJECT_AUDIO_CLIENT_3_LATENCY_OVER_MAX 0
60
+
36
61
// Windows 10 exposes the IAudioClient3 interface to create low-latency streams.
37
62
// Copy the interface definition from audioclient.h here to make the code
38
63
// simpler and so that we can still access IAudioClient3 via COM if cubeb was
@@ -1867,6 +1892,44 @@ wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params,
1867
1892
return CUBEB_ERROR;
1868
1893
}
1869
1894
1895
+ #if USE_AUDIO_CLIENT_3_MIN_PERIOD
1896
+ // This is unreliable as we can't know the actual mixer format cubeb will
1897
+ // ask for later on (nor we can branch on ALLOW_AUDIO_CLIENT_3_FOR_INPUT),
1898
+ // and the min latency can change based on that.
1899
+ com_ptr<IAudioClient3> client3;
1900
+ hr = device->Activate (__uuidof (IAudioClient3), CLSCTX_INPROC_SERVER, NULL ,
1901
+ client3.receive_vpp ());
1902
+ if (SUCCEEDED (hr)) {
1903
+ WAVEFORMATEX * mix_format = nullptr ;
1904
+ hr = client3->GetMixFormat (&mix_format);
1905
+
1906
+ if (SUCCEEDED (hr)) {
1907
+ uint32_t default_period = 0 , fundamental_period = 0 , min_period = 0 ,
1908
+ max_period = 0 ;
1909
+ hr = client3->GetSharedModeEnginePeriod (mix_format, &default_period,
1910
+ &fundamental_period, &min_period,
1911
+ &max_period);
1912
+
1913
+ auto sample_rate = mix_format->nSamplesPerSec ;
1914
+ CoTaskMemFree (mix_format);
1915
+ if (SUCCEEDED (hr)) {
1916
+ // Print values in the same format as IAudioDevice::GetDevicePeriod()
1917
+ REFERENCE_TIME min_period_rt (frames_to_hns (sample_rate, min_period));
1918
+ REFERENCE_TIME default_period_rt (
1919
+ frames_to_hns (sample_rate, default_period));
1920
+ LOG (" default device period: %I64d, minimum device period: %I64d" ,
1921
+ default_period_rt, min_period_rt);
1922
+
1923
+ *latency_frames = min_period;
1924
+
1925
+ LOG (" Minimum latency in frames: %u" , *latency_frames);
1926
+
1927
+ return CUBEB_OK;
1928
+ }
1929
+ }
1930
+ }
1931
+ #endif
1932
+
1870
1933
com_ptr<IAudioClient> client;
1871
1934
hr = device->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER, NULL ,
1872
1935
client.receive_vpp ());
@@ -1886,18 +1949,8 @@ wasapi_get_min_latency(cubeb * ctx, cubeb_stream_params params,
1886
1949
LOG (" default device period: %I64d, minimum device period: %I64d" ,
1887
1950
default_period, minimum_period);
1888
1951
1889
- /* If we're on Windows 10, we can use IAudioClient3 to get minimal latency.
1890
- Otherwise, according to the docs, the best latency we can achieve is by
1891
- synchronizing the stream and the engine.
1892
- http://msdn.microsoft.com/en-us/library/windows/desktop/dd370871%28v=vs.85%29.aspx
1893
- */
1894
-
1895
- // #ifdef _WIN32_WINNT_WIN10
1896
- #if 0
1897
- *latency_frames = hns_to_frames(params.rate, minimum_period);
1898
- #else
1952
+ // The minimum_period is only relevant in exclusive streams.
1899
1953
*latency_frames = hns_to_frames (params.rate , default_period);
1900
- #endif
1901
1954
1902
1955
LOG (" Minimum latency in frames: %u" , *latency_frames);
1903
1956
@@ -1987,7 +2040,10 @@ handle_channel_layout(cubeb_stream * stm, EDataFlow direction,
1987
2040
if (hr == S_FALSE) {
1988
2041
/* Channel layout not supported, but WASAPI gives us a suggestion. Use it,
1989
2042
and handle the eventual upmix/downmix ourselves. Ignore the subformat of
1990
- the suggestion, since it seems to always be IEEE_FLOAT. */
2043
+ the suggestion, since it seems to always be IEEE_FLOAT.
2044
+ This fallback doesn't update the bit depth, so if a device
2045
+ only supported bit depths cubeb doesn't support, so IAudioClient3
2046
+ streams might fail */
1991
2047
LOG (" Using WASAPI suggested format: channels: %d" , closest->nChannels );
1992
2048
XASSERT (closest->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
1993
2049
WAVEFORMATEXTENSIBLE * closest_pcm =
@@ -2031,12 +2087,12 @@ initialize_iaudioclient2(com_ptr<IAudioClient> & audio_client)
2031
2087
return CUBEB_OK;
2032
2088
}
2033
2089
2034
- #if 0
2035
2090
bool
2036
2091
initialize_iaudioclient3 (com_ptr<IAudioClient> & audio_client,
2037
2092
cubeb_stream * stm,
2038
2093
const com_heap_ptr<WAVEFORMATEX> & mix_format,
2039
- DWORD flags, EDataFlow direction)
2094
+ DWORD flags, EDataFlow direction,
2095
+ REFERENCE_TIME latency_hns)
2040
2096
{
2041
2097
com_ptr<IAudioClient3> audio_client3;
2042
2098
audio_client->QueryInterface <IAudioClient3>(audio_client3.receive ());
@@ -2052,24 +2108,22 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
2052
2108
return false ;
2053
2109
}
2054
2110
2055
- // Some people have reported glitches with capture streams:
2056
- // http://blog.nirbheek.in/2018/03/low-latency-audio-on-windows-with.html
2057
- if (direction == eCapture) {
2058
- LOG("Audio stream is capture, not using IAudioClient3");
2059
- return false;
2060
- }
2061
-
2062
2111
// Possibly initialize a shared-mode stream using IAudioClient3. Initializing
2063
2112
// a stream this way lets you request lower latencies, but also locks the
2064
2113
// global WASAPI engine at that latency.
2065
2114
// - If we request a shared-mode stream, streams created with IAudioClient
2066
- // will
2067
- // have their latency adjusted to match. When the shared-mode stream is
2068
- // closed, they'll go back to normal.
2069
- // - If there's already a shared-mode stream running, then we cannot request
2070
- // the engine change to a different latency - we have to match it.
2071
- // - It's antisocial to lock the WASAPI engine at its default latency. If we
2072
- // would do this, then stop and use IAudioClient instead.
2115
+ // might have their latency adjusted to match. When the shared-mode stream
2116
+ // is closed, they'll go back to normal.
2117
+ // - If there's already a shared-mode stream running, if it created with the
2118
+ // AUDCLNT_STREAMOPTIONS_MATCH_FORMAT option, the audio engine would be
2119
+ // locked to that format, so we have to match it (a custom one would fail).
2120
+ // - We don't lock the WASAPI engine to a format, as it's antisocial towards
2121
+ // other apps, especially if we locked to a latency >= than its default.
2122
+ // - If the user requested latency is >= the default one, we might still
2123
+ // accept it (without locking the format) depending on
2124
+ // ALLOW_AUDIO_CLIENT_3_LATENCY_OVER_DEFAULT, as we might want to prioritize
2125
+ // to lower our latency over other apps
2126
+ // (there might still be latency advantages compared to IAudioDevice(1)).
2073
2127
2074
2128
HRESULT hr;
2075
2129
uint32_t default_period = 0 , fundamental_period = 0 , min_period = 0 ,
@@ -2081,28 +2135,59 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
2081
2135
LOG (" Could not get shared mode engine period: error: %lx" , hr);
2082
2136
return false ;
2083
2137
}
2084
- uint32_t requested_latency = stm->latency;
2138
+ uint32_t requested_latency =
2139
+ hns_to_frames (mix_format->nSamplesPerSec , latency_hns);
2140
+ #if !ALLOW_AUDIO_CLIENT_3_LATENCY_OVER_DEFAULT
2085
2141
if (requested_latency >= default_period) {
2086
- LOG("Requested latency %i greater than default latency %i, not using "
2087
- "IAudioClient3",
2142
+ LOG (" Requested latency %i equal or greater than default latency %i,"
2143
+ " not using IAudioClient3" ,
2088
2144
requested_latency, default_period);
2089
2145
return false ;
2090
2146
}
2147
+ #elif REJECT_AUDIO_CLIENT_3_LATENCY_OVER_MAX
2148
+ if (requested_latency > max_period) {
2149
+ // Fallback to IAudioClient(1) as it's more accepting of large latencies
2150
+ LOG (" Requested latency %i greater than max latency %i,"
2151
+ " not using IAudioClient3" ,
2152
+ requested_latency, max_period);
2153
+ return false ;
2154
+ }
2155
+ #endif
2091
2156
LOG (" Got shared mode engine period: default=%i fundamental=%i min=%i max=%i" ,
2092
2157
default_period, fundamental_period, min_period, max_period);
2093
2158
// Snap requested latency to a valid value
2094
2159
uint32_t old_requested_latency = requested_latency;
2160
+ // The period is required to be a multiple of the fundamental period
2161
+ // (and >= min and <= max, which should still be true)
2162
+ requested_latency -= requested_latency % fundamental_period;
2095
2163
if (requested_latency < min_period) {
2096
2164
requested_latency = min_period;
2097
2165
}
2098
- requested_latency -= (requested_latency - min_period) % fundamental_period;
2166
+ // Likely unnecessary, but won't hurt
2167
+ if (requested_latency > max_period) {
2168
+ requested_latency = max_period;
2169
+ }
2099
2170
if (requested_latency != old_requested_latency) {
2100
2171
LOG (" Requested latency %i was adjusted to %i" , old_requested_latency,
2101
2172
requested_latency);
2102
2173
}
2103
2174
2104
- hr = audio_client3->InitializeSharedAudioStream(flags, requested_latency,
2175
+ DWORD new_flags = flags;
2176
+ // Always add these flags to IAudioClient3, they might help
2177
+ // if the stream doesn't have the same format as the audio engine.
2178
+ new_flags |= AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM;
2179
+ new_flags |= AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
2180
+
2181
+ hr = audio_client3->InitializeSharedAudioStream (new_flags, requested_latency,
2105
2182
mix_format.get (), NULL );
2183
+ // This error should be returned first even if
2184
+ // the period was locked (AUDCLNT_E_ENGINE_PERIODICITY_LOCKED)
2185
+ if (hr == AUDCLNT_E_INVALID_STREAM_FLAG) {
2186
+ LOG (" Got AUDCLNT_E_INVALID_STREAM_FLAG, removing some flags" );
2187
+ hr = audio_client3->InitializeSharedAudioStream (flags, requested_latency,
2188
+ mix_format.get (), NULL );
2189
+ }
2190
+
2106
2191
if (SUCCEEDED (hr)) {
2107
2192
return true ;
2108
2193
} else if (hr == AUDCLNT_E_ENGINE_PERIODICITY_LOCKED) {
@@ -2114,22 +2199,37 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
2114
2199
}
2115
2200
2116
2201
uint32_t current_period = 0 ;
2117
- WAVEFORMATEX * current_format = nullptr;
2202
+ WAVEFORMATEX * current_format_ptr = nullptr ;
2118
2203
// We have to pass a valid WAVEFORMATEX** and not nullptr, otherwise
2119
2204
// GetCurrentSharedModeEnginePeriod will return E_POINTER
2120
- hr = audio_client3->GetCurrentSharedModeEnginePeriod(¤t_format ,
2205
+ hr = audio_client3->GetCurrentSharedModeEnginePeriod (¤t_format_ptr ,
2121
2206
¤t_period);
2122
- CoTaskMemFree(current_format);
2123
2207
if (FAILED (hr)) {
2124
2208
LOG (" Could not get current shared mode engine period: error: %lx" , hr);
2125
2209
return false ;
2126
2210
}
2211
+ com_heap_ptr<WAVEFORMATEX> current_format (current_format_ptr);
2212
+ if (current_format->nSamplesPerSec != mix_format->nSamplesPerSec ) {
2213
+ // Unless some other external app locked the shared mode engine period
2214
+ // within our audio initialization, this is unlikely to happen, though we
2215
+ // can't respect the user selected latency, so we fallback on IAudioClient
2216
+ LOG (" IAudioClient3::GetCurrentSharedModeEnginePeriod() returned a "
2217
+ " different mixer format (nSamplesPerSec) from "
2218
+ " IAudioClient::GetMixFormat(); not using IAudioClient3" );
2219
+ return false ;
2220
+ }
2127
2221
2128
- if (current_period >= default_period) {
2129
- LOG("Current shared mode engine period %i too high, not using IAudioClient",
2130
- current_period);
2222
+ #if REJECT_AUDIO_CLIENT_3_LATENCY_OVER_MAX
2223
+ // Reject IAudioClient3 if we can't respect the user target latency.
2224
+ // We don't need to check against default_latency anymore,
2225
+ // as the current_period is already the best one we could get.
2226
+ if (old_requested_latency > current_period) {
2227
+ LOG (" Requested latency %i greater than currently locked shared mode "
2228
+ " latency %i, not using IAudioClient3" ,
2229
+ old_requested_latency, current_period);
2131
2230
return false ;
2132
2231
}
2232
+ #endif
2133
2233
2134
2234
hr = audio_client3->InitializeSharedAudioStream (flags, current_period,
2135
2235
mix_format.get (), NULL );
@@ -2142,7 +2242,6 @@ initialize_iaudioclient3(com_ptr<IAudioClient> & audio_client,
2142
2242
LOG (" Could not initialize shared stream with IAudioClient3: error: %lx" , hr);
2143
2243
return false ;
2144
2244
}
2145
- #endif
2146
2245
2147
2246
#define DIRECTION_NAME (direction == eCapture ? " capture" : " render" )
2148
2247
@@ -2166,6 +2265,12 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
2166
2265
return CUBEB_ERROR;
2167
2266
}
2168
2267
2268
+ #if ALLOW_AUDIO_CLIENT_3_FOR_INPUT
2269
+ constexpr bool allow_audio_client_3 = true ;
2270
+ #else
2271
+ const bool allow_audio_client_3 = direction == eRender;
2272
+ #endif
2273
+
2169
2274
stm->stream_reset_lock .assert_current_thread_owns ();
2170
2275
// If user doesn't specify a particular device, we can choose another one when
2171
2276
// the given devid is unavailable.
@@ -2202,17 +2307,14 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
2202
2307
2203
2308
/* Get a client. We will get all other interfaces we need from
2204
2309
* 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
2310
+ if (allow_audio_client_3) {
2311
+ hr = device->Activate (__uuidof (IAudioClient3), CLSCTX_INPROC_SERVER, NULL ,
2312
+ audio_client.receive_vpp ());
2313
+ }
2314
+ if (!allow_audio_client_3 || hr == E_NOINTERFACE) {
2315
+ hr = device->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER, NULL ,
2316
+ audio_client.receive_vpp ());
2214
2317
}
2215
- #endif
2216
2318
2217
2319
if (FAILED (hr)) {
2218
2320
LOG (" Could not activate the device to get an audio"
@@ -2341,16 +2443,15 @@ setup_wasapi_stream_one_side(cubeb_stream * stm,
2341
2443
}
2342
2444
}
2343
2445
2344
- #if 0 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1590902
2345
- if (initialize_iaudioclient3(audio_client, stm, mix_format, flags, direction)) {
2446
+ if (allow_audio_client_3 &&
2447
+ initialize_iaudioclient3 (audio_client, stm, mix_format, flags, direction,
2448
+ latency_hns)) {
2346
2449
LOG (" Initialized with IAudioClient3" );
2347
2450
} else {
2348
- #endif
2349
- hr = audio_client->Initialize (AUDCLNT_SHAREMODE_SHARED, flags, latency_hns, 0 ,
2350
- mix_format.get (), NULL );
2351
- #if 0
2451
+ hr = audio_client->Initialize (AUDCLNT_SHAREMODE_SHARED, flags, latency_hns,
2452
+ 0 , mix_format.get (), NULL );
2352
2453
}
2353
- # endif
2454
+
2354
2455
if (FAILED (hr)) {
2355
2456
LOG (" Unable to initialize audio client for %s: %lx." , DIRECTION_NAME, hr);
2356
2457
return CUBEB_ERROR;
@@ -3310,6 +3411,7 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
3310
3411
CUBEB_DEVICE_FMT_S16NE);
3311
3412
ret.default_format = CUBEB_DEVICE_FMT_F32NE;
3312
3413
prop_variant fmtvar;
3414
+ WAVEFORMATEX * wfx = NULL ;
3313
3415
hr = propstore->GetValue (PKEY_AudioEngine_DeviceFormat, &fmtvar);
3314
3416
if (SUCCEEDED (hr) && fmtvar.vt == VT_BLOB) {
3315
3417
if (fmtvar.blob .cbSize == sizeof (PCMWAVEFORMAT)) {
@@ -3319,8 +3421,7 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
3319
3421
ret.max_rate = ret.min_rate = ret.default_rate = pcm->wf .nSamplesPerSec ;
3320
3422
ret.max_channels = pcm->wf .nChannels ;
3321
3423
} else if (fmtvar.blob .cbSize >= sizeof (WAVEFORMATEX)) {
3322
- WAVEFORMATEX * wfx =
3323
- reinterpret_cast <WAVEFORMATEX *>(fmtvar.blob .pBlobData );
3424
+ wfx = reinterpret_cast <WAVEFORMATEX *>(fmtvar.blob .pBlobData );
3324
3425
3325
3426
if (fmtvar.blob .cbSize >= sizeof (WAVEFORMATEX) + wfx->cbSize ||
3326
3427
wfx->wFormatTag == WAVE_FORMAT_PCM) {
@@ -3330,9 +3431,30 @@ wasapi_create_device(cubeb * ctx, cubeb_device_info & ret,
3330
3431
}
3331
3432
}
3332
3433
3333
- if (SUCCEEDED (dev->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER,
3334
- NULL , client.receive_vpp ())) &&
3335
- SUCCEEDED (client->GetDevicePeriod (&def_period, &min_period))) {
3434
+ #if USE_AUDIO_CLIENT_3_MIN_PERIOD
3435
+ // Here we assume an IAudioClient3 stream will successfully
3436
+ // be initialized later (it might fail)
3437
+ #if ALLOW_AUDIO_CLIENT_3_FOR_INPUT
3438
+ constexpr bool allow_audio_client_3 = true ;
3439
+ #else
3440
+ const bool allow_audio_client_3 = flow == eRender;
3441
+ #endif
3442
+ com_ptr<IAudioClient3> client3;
3443
+ uint32_t def, fun, min, max;
3444
+ if (allow_audio_client_3 && wfx &&
3445
+ SUCCEEDED (dev->Activate (__uuidof (IAudioClient3), CLSCTX_INPROC_SERVER,
3446
+ NULL , client3.receive_vpp ())) &&
3447
+ SUCCEEDED (
3448
+ client3->GetSharedModeEnginePeriod (wfx, &def, &fun, &min, &max))) {
3449
+ ret.latency_lo = min;
3450
+ // This latency might actually be used as "default" and not "max" later on,
3451
+ // so we return the default (we never really want to use the max anyway)
3452
+ ret.latency_hi = def;
3453
+ } else
3454
+ #endif
3455
+ if (SUCCEEDED (dev->Activate (__uuidof (IAudioClient), CLSCTX_INPROC_SERVER,
3456
+ NULL , client.receive_vpp ())) &&
3457
+ SUCCEEDED (client->GetDevicePeriod (&def_period, &min_period))) {
3336
3458
ret.latency_lo = hns_to_frames (ret.default_rate , min_period);
3337
3459
ret.latency_hi = hns_to_frames (ret.default_rate , def_period);
3338
3460
} else {
0 commit comments