Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
hrydgard
GitHub Repository: hrydgard/ppsspp
Path: blob/master/Windows/WASAPIContext.cpp
5654 views
1
#include <windows.h>
2
#include <mmdeviceapi.h>
3
#include <functiondiscoverykeys_devpkey.h>
4
#include <audioclient.h>
5
#include <avrt.h>
6
#include <comdef.h>
7
#include <atomic>
8
#include <thread>
9
#include <vector>
10
#include <string_view>
11
#include <wrl/client.h>
12
13
#include "Common/Data/Encoding/Utf8.h"
14
#include "Common/Log.h"
15
#include "Common/Thread/ThreadUtil.h"
16
#include "WASAPIContext.h"
17
18
using Microsoft::WRL::ComPtr;
19
20
// We must have one of these already...
21
static inline s16 ClampFloatToS16(float f) {
22
f *= 32768.0f;
23
if (f >= 32767) {
24
return 32767;
25
} else if (f < -32767) {
26
return -32767;
27
} else {
28
return (s16)(s32)f;
29
}
30
}
31
32
void BuildStereoFloatFormat(const WAVEFORMATEXTENSIBLE *original, WAVEFORMATEXTENSIBLE *output) {
33
// Zero‑init all fields first.
34
ZeroMemory(output, sizeof(WAVEFORMATEXTENSIBLE));
35
36
// Fill the WAVEFORMATEX base part.
37
output->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
38
output->Format.nChannels = 2;
39
output->Format.nSamplesPerSec = original->Format.nSamplesPerSec;
40
output->Format.wBitsPerSample = 32; // 32‑bit float
41
output->Format.nBlockAlign = output->Format.nChannels *
42
output->Format.wBitsPerSample / 8;
43
output->Format.nAvgBytesPerSec = output->Format.nSamplesPerSec *
44
output->Format.nBlockAlign;
45
output->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
46
47
// Fill the extensible fields.
48
output->Samples.wValidBitsPerSample = 32;
49
output->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
50
output->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
51
}
52
53
WASAPIContext::WASAPIContext() : notificationClient_(this) {
54
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator_));
55
if (FAILED(hr)) {
56
// Bad!
57
enumerator_ = nullptr;
58
return;
59
}
60
enumerator_->RegisterEndpointNotificationCallback(&notificationClient_);
61
}
62
63
WASAPIContext::~WASAPIContext() {
64
if (!enumerator_) {
65
// Nothing can have been happening.
66
return;
67
}
68
Stop();
69
enumerator_->UnregisterEndpointNotificationCallback(&notificationClient_);
70
delete[] tempBuf_;
71
}
72
73
WASAPIContext::AudioFormat WASAPIContext::Classify(const WAVEFORMATEX *format) {
74
if (format->wFormatTag == WAVE_FORMAT_PCM && format->wBitsPerSample == 2) {
75
return AudioFormat::S16;
76
} else if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
77
const WAVEFORMATEXTENSIBLE *ex = (const WAVEFORMATEXTENSIBLE *)format;
78
if (ex->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
79
return AudioFormat::Float;
80
}
81
} else {
82
WARN_LOG(Log::Audio, "Unhandled output format!");
83
}
84
return AudioFormat::Unhandled;
85
}
86
87
bool GetDeviceDesc(IMMDevice *device, AudioDeviceDesc *desc) {
88
ComPtr<IPropertyStore> props;
89
device->OpenPropertyStore(STGM_READ, &props);
90
PROPVARIANT nameProp;
91
PropVariantInit(&nameProp);
92
props->GetValue(PKEY_Device_FriendlyName, &nameProp);
93
LPWSTR id_str = 0;
94
bool success = false;
95
if (SUCCEEDED(device->GetId(&id_str))) {
96
desc->name = ConvertWStringToUTF8(nameProp.pwszVal);
97
desc->uniqueId = ConvertWStringToUTF8(id_str);
98
CoTaskMemFree(id_str);
99
success = true;
100
}
101
PropVariantClear(&nameProp);
102
return success;
103
}
104
105
void WASAPIContext::EnumerateDevices(std::vector<AudioDeviceDesc> *output, bool captureDevices) {
106
ComPtr<IMMDeviceCollection> collection;
107
enumerator_->EnumAudioEndpoints(captureDevices ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection);
108
109
if (!collection) {
110
ERROR_LOG(Log::Audio, "Failed to enumerate devices");
111
return;
112
}
113
114
UINT count = 0;
115
collection->GetCount(&count);
116
117
for (UINT i = 0; i < count; ++i) {
118
ComPtr<IMMDevice> device;
119
collection->Item(i, &device);
120
121
AudioDeviceDesc desc{};
122
if (GetDeviceDesc(device.Get(), &desc)) {
123
output->push_back(desc);
124
}
125
}
126
}
127
128
bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault) {
129
Stop();
130
131
*revertedToDefault = false;
132
133
ComPtr<IMMDevice> device;
134
if (uniqueId.empty()) {
135
// Use the default device.
136
if (FAILED(enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device))) {
137
return false;
138
}
139
} else {
140
// Use whatever device.
141
std::wstring wId = ConvertUTF8ToWString(uniqueId);
142
if (FAILED(enumerator_->GetDevice(wId.c_str(), &device))) {
143
// Fallback to default device
144
INFO_LOG(Log::Audio, "Falling back to default device...\n");
145
*revertedToDefault = true;
146
if (FAILED(enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device))) {
147
return false;
148
}
149
}
150
}
151
152
AudioDeviceDesc desc{};
153
GetDeviceDesc(device.Get(), &desc);
154
INFO_LOG(Log::Audio, "Activating audio device: %s", desc.name.c_str());
155
156
deviceId_ = uniqueId;
157
158
HRESULT hr = E_FAIL;
159
// Try IAudioClient3 first if not in "safe" mode. It's probably safe anyway, but still, let's use the legacy client as a safe fallback option.
160
if (latencyMode != LatencyMode::Safe) {
161
hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, (void**)&audioClient3_);
162
}
163
164
// Get rid of any old tempBuf_.
165
delete[] tempBuf_;
166
tempBuf_ = nullptr;
167
168
if (SUCCEEDED(hr)) {
169
audioClient3_->GetMixFormat(&format_);
170
// We only use AudioClient3 if we got the format we wanted (stereo float).
171
if (format_->nChannels != 2 || Classify(format_) != AudioFormat::Float) {
172
// Let's fall back to the old path. The docs seem to be wrong, if you try to create an
173
// AudioClient3 with low latency audio with AUTOCONVERTPCM, you get the error 0x88890021.
174
audioClient3_.Reset();
175
// Fall through to AudioClient creation below.
176
} else {
177
audioClient3_->GetSharedModeEnginePeriod(format_, &defaultPeriodFrames, &fundamentalPeriodFrames, &minPeriodFrames, &maxPeriodFrames);
178
179
INFO_LOG(Log::Audio, "AudioClient3: default: %d fundamental: %d min: %d max: %d\n", (int)defaultPeriodFrames, (int)fundamentalPeriodFrames, (int)minPeriodFrames, (int)maxPeriodFrames);
180
INFO_LOG(Log::Audio, "initializing with %d frame period at %d Hz, meaning %0.1fms\n", (int)minPeriodFrames, (int)format_->nSamplesPerSec, FramesToMs(minPeriodFrames, format_->nSamplesPerSec));
181
182
audioEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
183
HRESULT result = audioClient3_->InitializeSharedAudioStream(
184
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
185
minPeriodFrames,
186
format_,
187
nullptr
188
);
189
if (FAILED(result)) {
190
WARN_LOG(Log::Audio, "Error initializing AudioClient3 shared audio stream: %08lx", result);
191
audioClient3_.Reset();
192
return false;
193
}
194
actualPeriodFrames_ = minPeriodFrames;
195
196
audioClient3_->GetBufferSize(&reportedBufferSize_);
197
audioClient3_->SetEventHandle(audioEvent_);
198
audioClient3_->GetService(IID_PPV_ARGS(&renderClient_));
199
}
200
}
201
202
if (!audioClient3_) {
203
// Fallback to IAudioClient (older OS)
204
HRESULT hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&audioClient_);
205
if (FAILED(hr)) {
206
ERROR_LOG(Log::Audio, "Failed to activate audio device: %08lx", hr);
207
return false;
208
}
209
210
audioClient_->GetMixFormat(&format_);
211
212
// If there are too many channels, try asking for a 2-channel output format.
213
DWORD extraStreamFlags = 0;
214
const AudioFormat fmt = Classify(format_);
215
216
bool createBuffer = false;
217
if (fmt == AudioFormat::Float) {
218
if (format_->nChannels != 2) {
219
INFO_LOG(Log::Audio, "Got %d channels, asking for stereo instead", format_->nChannels);
220
WAVEFORMATEXTENSIBLE stereo;
221
BuildStereoFloatFormat((const WAVEFORMATEXTENSIBLE *)format_, &stereo);
222
223
WAVEFORMATEX *closestMatch = nullptr;
224
const HRESULT result = audioClient_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, (const WAVEFORMATEX *)&stereo, &closestMatch);
225
if (result == S_OK) {
226
// We got the format! Use it and set as current.
227
_dbg_assert_(!closestMatch);
228
format_ = (WAVEFORMATEX *)CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE));
229
memcpy(format_, &stereo, sizeof(WAVEFORMATEX) + stereo.Format.cbSize);
230
extraStreamFlags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
231
INFO_LOG(Log::Audio, "Successfully asked for two channels");
232
} else if (result == S_FALSE) {
233
// We got another format. Meh, let's just use what we got.
234
if (closestMatch) {
235
WARN_LOG(Log::Audio, "Didn't get the format we wanted, but got: %lu ch=%d", closestMatch->nSamplesPerSec, closestMatch->nChannels);
236
CoTaskMemFree(closestMatch);
237
} else {
238
WARN_LOG(Log::Audio, "Failed to fall back to two channels. Using workarounds.");
239
}
240
createBuffer = true;
241
} else {
242
WARN_LOG(Log::Audio, "Got other error %08lx", result);
243
_dbg_assert_(!closestMatch);
244
}
245
} else {
246
// All good, nothing to convert.
247
}
248
} else {
249
// Some other format.
250
WARN_LOG(Log::Audio, "Format not float, applying conversion.");
251
createBuffer = true;
252
}
253
254
// Get engine period info
255
REFERENCE_TIME defaultPeriod = 0, minPeriod = 0;
256
audioClient_->GetDevicePeriod(&defaultPeriod, &minPeriod);
257
258
audioEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
259
260
const REFERENCE_TIME duration = minPeriod;
261
hr = audioClient_->Initialize(
262
AUDCLNT_SHAREMODE_SHARED,
263
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | extraStreamFlags,
264
duration, // This is a minimum, the result might be larger. We use GetBufferSize to check.
265
0, // ref duration, always 0 in shared mode.
266
format_,
267
nullptr
268
);
269
270
if (FAILED(hr)) {
271
WARN_LOG(Log::Audio, "ERROR: Failed to initialize audio with all attempted buffer sizes\n");
272
audioClient_.Reset();
273
return false;
274
}
275
276
audioClient_->GetBufferSize(&reportedBufferSize_);
277
actualPeriodFrames_ = reportedBufferSize_; // we don't have a better estimate.
278
audioClient_->SetEventHandle(audioEvent_);
279
audioClient_->GetService(IID_PPV_ARGS(&renderClient_));
280
281
if (createBuffer) {
282
tempBuf_ = new float[reportedBufferSize_ * 2];
283
}
284
}
285
286
latencyMode_ = latencyMode;
287
288
_dbg_assert_(audioClient_ || audioClient3_);
289
290
Start();
291
292
return true;
293
}
294
295
void WASAPIContext::Start() {
296
running_ = true;
297
audioThread_ = std::thread([this]() { AudioLoop(); });
298
}
299
300
void WASAPIContext::Stop() {
301
running_ = false;
302
if (audioClient_) audioClient_->Stop();
303
if (audioEvent_) SetEvent(audioEvent_);
304
if (audioThread_.joinable()) audioThread_.join();
305
306
renderClient_.Reset();
307
audioClient_.Reset();
308
if (audioEvent_) {
309
CloseHandle(audioEvent_);
310
audioEvent_ = nullptr;
311
}
312
if (format_) {
313
CoTaskMemFree(format_);
314
format_ = nullptr;
315
}
316
}
317
318
void WASAPIContext::FrameUpdate(bool allowAutoChange) {
319
if (deviceId_.empty() && defaultDeviceChanged_ && allowAutoChange) {
320
defaultDeviceChanged_ = false;
321
Stop();
322
Start();
323
}
324
}
325
326
void WASAPIContext::AudioLoop() {
327
SetCurrentThreadName("WASAPIAudioLoop");
328
329
DWORD taskID = 0;
330
HANDLE mmcssHandle = nullptr;
331
if (latencyMode_ == LatencyMode::Aggressive) {
332
mmcssHandle = AvSetMmThreadCharacteristics(L"Pro Audio", &taskID);
333
}
334
335
UINT32 available;
336
if (audioClient3_) {
337
audioClient3_->Start();
338
audioClient3_->GetBufferSize(&available);
339
} else if (audioClient_) {
340
audioClient_->Start();
341
audioClient_->GetBufferSize(&available);
342
} else {
343
// No audio client, nothing to do.
344
WARN_LOG(Log::Audio, "No audio client");
345
return;
346
}
347
348
const AudioFormat format = Classify(format_);
349
const int nChannels = format_->nChannels;
350
351
while (running_) {
352
const DWORD waitResult = WaitForSingleObject(audioEvent_, INFINITE);
353
if (waitResult != WAIT_OBJECT_0) {
354
// Something bad happened.
355
break;
356
}
357
358
UINT32 padding = 0;
359
if (audioClient3_) {
360
audioClient3_->GetCurrentPadding(&padding);
361
} else {
362
audioClient_->GetCurrentPadding(&padding);
363
}
364
365
const UINT32 framesToWrite = available - padding;
366
BYTE* buffer = nullptr;
367
if (framesToWrite > 0 && SUCCEEDED(renderClient_->GetBuffer(framesToWrite, &buffer))) {
368
if (!tempBuf_) {
369
// Mix directly to the output buffer, avoiding a copy.
370
if (buffer) {
371
callback_(reinterpret_cast<float *>(buffer), framesToWrite, format_->nSamplesPerSec, userdata_);
372
}
373
} else {
374
// We decided previously that we need conversion, so mix to our temp buffer...
375
callback_(tempBuf_, framesToWrite, format_->nSamplesPerSec, userdata_);
376
// .. and convert according to format (we support multi-channel float and s16)
377
if (format == AudioFormat::S16 && buffer) {
378
// Need to convert.
379
s16 *dest = reinterpret_cast<s16 *>(buffer);
380
for (UINT32 i = 0; i < framesToWrite; i++) {
381
if (nChannels == 1) {
382
// Maybe some bluetooth speakers? Mixdown.
383
float sum = 0.5f * (tempBuf_[i * 2] + tempBuf_[i * 2 + 1]);
384
dest[i] = ClampFloatToS16(sum);
385
} else {
386
dest[i * nChannels] = ClampFloatToS16(tempBuf_[i * 2]);
387
dest[i * nChannels + 1] = ClampFloatToS16(tempBuf_[i * 2 + 1]);
388
// Zero other channels.
389
for (int j = 2; j < nChannels; j++) {
390
dest[i * nChannels + j] = 0;
391
}
392
}
393
}
394
} else if (format == AudioFormat::Float && buffer) {
395
// We have a non-2 number of channels (since we're in the tempBuf_ 'if'), so we contract/expand.
396
float *dest = reinterpret_cast<float *>(buffer);
397
for (UINT32 i = 0; i < framesToWrite; i++) {
398
if (nChannels == 1) {
399
// Maybe some bluetooth speakers? Mixdown.
400
dest[i] = 0.5f * (tempBuf_[i * 2] + tempBuf_[i * 2 + 1]);
401
} else {
402
dest[i * nChannels] = tempBuf_[i * 2];
403
dest[i * nChannels + 1] = tempBuf_[i * 2 + 1];
404
// Zero other channels.
405
for (int j = 2; j < nChannels; j++) {
406
dest[i * nChannels + j] = 0;
407
}
408
}
409
}
410
}
411
}
412
413
renderClient_->ReleaseBuffer(framesToWrite, 0);
414
}
415
416
// In the old mode, we just estimate the "actualPeriodFrames_" from the framesToWrite.
417
if (audioClient_ && framesToWrite < actualPeriodFrames_) {
418
actualPeriodFrames_ = framesToWrite;
419
}
420
}
421
422
if (audioClient3_) {
423
audioClient3_->Stop();
424
} else {
425
audioClient_->Stop();
426
}
427
428
if (mmcssHandle) {
429
AvRevertMmThreadCharacteristics(mmcssHandle);
430
}
431
}
432
433
void WASAPIContext::DescribeOutputFormat(char *buffer, size_t bufferSize) const {
434
const int numChannels = format_->nChannels;
435
const int sampleBits = format_->wBitsPerSample;
436
const int sampleRateHz = format_->nSamplesPerSec;
437
const char *fmt = "N/A";
438
if (format_->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
439
const WAVEFORMATEXTENSIBLE *ex = (const WAVEFORMATEXTENSIBLE *)format_;
440
if (ex->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
441
fmt = "float";
442
} else {
443
fmt = "PCM";
444
}
445
} else {
446
fmt = "PCM"; // probably
447
}
448
snprintf(buffer, bufferSize, "%d Hz %s %d-bit, %d ch%s", sampleRateHz, fmt, sampleBits, numChannels, audioClient3_ ? " (ac3)" : " (ac)");
449
}
450
451