CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!
Path: blob/master/Windows/WASAPIStream.cpp
Views: 1401
#include "stdafx.h"12#include "WindowsAudio.h"3#include "WASAPIStream.h"4#include "Common/Log.h"5#include "Common/LogReporting.h"6#include "Core/Config.h"7#include "Core/Util/AudioFormat.h"8#include "Common/Data/Encoding/Utf8.h"910#include "Common/Thread/ThreadUtil.h"1112#include <mutex>13#include <Objbase.h>14#include <Mmreg.h>15#include <MMDeviceAPI.h>16#include <AudioClient.h>17#include <AudioPolicy.h>18#include "Functiondiscoverykeys_devpkey.h"1920// Includes some code from https://msdn.microsoft.com/en-us/library/dd370810%28VS.85%29.aspx?f=255&MSPPError=-21472173962122#pragma comment(lib, "ole32.lib")2324const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);25const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);26const IID IID_IAudioClient = __uuidof(IAudioClient);27const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient);2829// Adapted from a MSDN sample.3031#define SAFE_RELEASE(punk) \32if ((punk) != NULL) \33{ (punk)->Release(); (punk) = NULL; }3435class CMMNotificationClient final : public IMMNotificationClient {36public:37CMMNotificationClient() {38}3940virtual ~CMMNotificationClient() {41CoTaskMemFree(currentDevice_);42currentDevice_ = nullptr;43SAFE_RELEASE(_pEnumerator)44}4546void SetCurrentDevice(IMMDevice *device) {47std::lock_guard<std::mutex> guard(lock_);4849CoTaskMemFree(currentDevice_);50currentDevice_ = nullptr;51if (!device || FAILED(device->GetId(¤tDevice_))) {52currentDevice_ = nullptr;53}5455if (currentDevice_) {56INFO_LOG(Log::sceAudio, "Switching to WASAPI audio device: '%s'", GetDeviceName(currentDevice_).c_str());57}5859deviceChanged_ = false;60}6162bool HasDefaultDeviceChanged() const {63return deviceChanged_;64}6566// IUnknown methods -- AddRef, Release, and QueryInterface67ULONG STDMETHODCALLTYPE AddRef() override {68return InterlockedIncrement(&_cRef);69}7071ULONG STDMETHODCALLTYPE Release() override {72ULONG ulRef = InterlockedDecrement(&_cRef);73if (0 == ulRef) {74delete this;75}76return ulRef;77}7879HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override {80if (IID_IUnknown == riid) {81AddRef();82*ppvInterface = (IUnknown*)this;83} else if (__uuidof(IMMNotificationClient) == riid) {84AddRef();85*ppvInterface = (IMMNotificationClient*)this;86} else {87*ppvInterface = nullptr;88return E_NOINTERFACE;89}90return S_OK;91}9293// Callback methods for device-event notifications.9495HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override {96std::lock_guard<std::mutex> guard(lock_);9798if (flow != eRender || role != eConsole) {99// Not relevant to us.100return S_OK;101}102103// pwstrDeviceId can be null. We consider that a new device, I think?104bool same = currentDevice_ == pwstrDeviceId;105if (!same && currentDevice_ && pwstrDeviceId) {106same = !wcscmp(currentDevice_, pwstrDeviceId);107}108if (same) {109// Already the current device, nothing to do.110return S_OK;111}112113deviceChanged_ = true;114INFO_LOG(Log::sceAudio, "New default eRender/eConsole WASAPI audio device detected: '%s'", GetDeviceName(pwstrDeviceId).c_str());115return S_OK;116}117118HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override {119// Ignore.120return S_OK;121};122123HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override {124// Ignore.125return S_OK;126}127128HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override {129// Ignore.130return S_OK;131}132133HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) override {134INFO_LOG(Log::sceAudio, "Changed audio device property "135"{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}#%d",136(uint32_t)key.fmtid.Data1, key.fmtid.Data2, key.fmtid.Data3,137key.fmtid.Data4[0], key.fmtid.Data4[1],138key.fmtid.Data4[2], key.fmtid.Data4[3],139key.fmtid.Data4[4], key.fmtid.Data4[5],140key.fmtid.Data4[6], key.fmtid.Data4[7],141(int)key.pid);142return S_OK;143}144145std::string GetDeviceName(LPCWSTR pwstrId)146{147HRESULT hr = S_OK;148IMMDevice *pDevice = NULL;149IPropertyStore *pProps = NULL;150PROPVARIANT varString;151PropVariantInit(&varString);152153if (_pEnumerator == NULL)154{155// Get enumerator for audio endpoint devices.156hr = CoCreateInstance(__uuidof(MMDeviceEnumerator),157NULL, CLSCTX_INPROC_SERVER,158__uuidof(IMMDeviceEnumerator),159(void**)&_pEnumerator);160}161if (hr == S_OK && _pEnumerator) {162hr = _pEnumerator->GetDevice(pwstrId, &pDevice);163}164if (hr == S_OK && pDevice) {165hr = pDevice->OpenPropertyStore(STGM_READ, &pProps);166}167if (hr == S_OK && pProps) {168// Get the endpoint device's friendly-name property.169hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString);170}171172std::string name = ConvertWStringToUTF8((hr == S_OK) ? varString.pwszVal : L"null device");173174PropVariantClear(&varString);175176SAFE_RELEASE(pProps)177SAFE_RELEASE(pDevice)178return name;179}180181private:182std::mutex lock_;183LONG _cRef = 1;184IMMDeviceEnumerator *_pEnumerator = nullptr;185wchar_t *currentDevice_ = nullptr;186bool deviceChanged_ = false;187};188189// TODO: Make these adjustable. This is from the example in MSDN.190// 200 times/sec = 5ms, pretty good :) Wonder if all computers can handle it though.191#define REFTIMES_PER_SEC (10000000/200)192#define REFTIMES_PER_MILLISEC (REFTIMES_PER_SEC / 1000)193194WASAPIAudioBackend::WASAPIAudioBackend() : threadData_(0) {195}196197WASAPIAudioBackend::~WASAPIAudioBackend() {198if (threadData_ == 0) {199threadData_ = 1;200}201202if (hThread_) {203WaitForSingleObject(hThread_, 1000);204CloseHandle(hThread_);205hThread_ = nullptr;206}207208if (threadData_ == 2) {209// blah.210}211}212213unsigned int WINAPI WASAPIAudioBackend::soundThread(void *param) {214WASAPIAudioBackend *backend = (WASAPIAudioBackend *)param;215return backend->RunThread();216}217218bool WASAPIAudioBackend::Init(HWND window, StreamCallback callback, int sampleRate) {219threadData_ = 0;220callback_ = callback;221sampleRate_ = sampleRate;222hThread_ = (HANDLE)_beginthreadex(0, 0, soundThread, (void *)this, 0, 0);223if (!hThread_)224return false;225SetThreadPriority(hThread_, THREAD_PRIORITY_ABOVE_NORMAL);226return true;227}228229// This to be run only on the thread.230class WASAPIAudioThread {231public:232WASAPIAudioThread(std::atomic<int> &threadData, int &sampleRate, StreamCallback &callback)233: threadData_(threadData), sampleRate_(sampleRate), callback_(callback) {234}235~WASAPIAudioThread();236237void Run();238239private:240bool ActivateDefaultDevice();241bool InitAudioDevice();242void ShutdownAudioDevice();243bool DetectFormat();244bool ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt);245bool PrepareFormat();246247std::atomic<int> &threadData_;248int &sampleRate_;249StreamCallback &callback_;250251IMMDeviceEnumerator *deviceEnumerator_ = nullptr;252IMMDevice *device_ = nullptr;253IAudioClient *audioInterface_ = nullptr;254CMMNotificationClient *notificationClient_ = nullptr;255WAVEFORMATEXTENSIBLE *deviceFormat_ = nullptr;256IAudioRenderClient *renderClient_ = nullptr;257int16_t *shortBuf_ = nullptr;258259enum class Format {260UNKNOWN = 0,261IEEE_FLOAT = 1,262PCM16 = 2,263};264265uint32_t numBufferFrames = 0;266Format format_ = Format::UNKNOWN;267REFERENCE_TIME actualDuration_{};268};269270WASAPIAudioThread::~WASAPIAudioThread() {271delete [] shortBuf_;272shortBuf_ = nullptr;273ShutdownAudioDevice();274if (notificationClient_ && deviceEnumerator_)275deviceEnumerator_->UnregisterEndpointNotificationCallback(notificationClient_);276delete notificationClient_;277notificationClient_ = nullptr;278SAFE_RELEASE(deviceEnumerator_);279}280281bool WASAPIAudioThread::ActivateDefaultDevice() {282_assert_(device_ == nullptr);283HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_);284if (FAILED(hresult) || device_ == nullptr)285return false;286287_assert_(audioInterface_ == nullptr);288hresult = device_->Activate(IID_IAudioClient, CLSCTX_ALL, nullptr, (void **)&audioInterface_);289if (FAILED(hresult) || audioInterface_ == nullptr)290return false;291292return true;293}294295bool WASAPIAudioThread::InitAudioDevice() {296REFERENCE_TIME hnsBufferDuration = REFTIMES_PER_SEC;297_assert_(deviceFormat_ == nullptr);298HRESULT hresult = audioInterface_->GetMixFormat((WAVEFORMATEX **)&deviceFormat_);299if (FAILED(hresult) || !deviceFormat_)300return false;301302if (!DetectFormat()) {303// Format unsupported - let's not even try to initialize.304return false;305}306307hresult = audioInterface_->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, hnsBufferDuration, 0, &deviceFormat_->Format, nullptr);308if (FAILED(hresult))309return false;310_assert_(renderClient_ == nullptr);311hresult = audioInterface_->GetService(IID_IAudioRenderClient, (void **)&renderClient_);312if (FAILED(hresult) || !renderClient_)313return false;314315numBufferFrames = 0;316hresult = audioInterface_->GetBufferSize(&numBufferFrames);317if (FAILED(hresult) || numBufferFrames == 0)318return false;319320sampleRate_ = deviceFormat_->Format.nSamplesPerSec;321322return true;323}324325void WASAPIAudioThread::ShutdownAudioDevice() {326SAFE_RELEASE(renderClient_);327CoTaskMemFree(deviceFormat_);328deviceFormat_ = nullptr;329SAFE_RELEASE(audioInterface_);330SAFE_RELEASE(device_);331}332333bool WASAPIAudioThread::DetectFormat() {334if (deviceFormat_ && !ValidateFormat(deviceFormat_)) {335// Last chance, let's try to ask for one we support instead.336WAVEFORMATEXTENSIBLE fmt{};337fmt.Format.cbSize = sizeof(fmt);338fmt.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;339fmt.Format.nChannels = 2;340fmt.Format.nSamplesPerSec = 44100;341if (deviceFormat_->Format.nSamplesPerSec >= 22050 && deviceFormat_->Format.nSamplesPerSec <= 192000)342fmt.Format.nSamplesPerSec = deviceFormat_->Format.nSamplesPerSec;343fmt.Format.nBlockAlign = 2 * sizeof(float);344fmt.Format.nAvgBytesPerSec = fmt.Format.nSamplesPerSec * fmt.Format.nBlockAlign;345fmt.Format.wBitsPerSample = sizeof(float) * 8;346fmt.Samples.wReserved = 0;347fmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;348fmt.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;349350WAVEFORMATEXTENSIBLE *closest = nullptr;351HRESULT hr = audioInterface_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &fmt.Format, (WAVEFORMATEX **)&closest);352if (hr == S_OK) {353// Okay, great. Let's just use ours.354CoTaskMemFree(closest);355CoTaskMemFree(deviceFormat_);356deviceFormat_ = (WAVEFORMATEXTENSIBLE *)CoTaskMemAlloc(sizeof(fmt));357if (deviceFormat_)358memcpy(deviceFormat_, &fmt, sizeof(fmt));359360// In case something above gets out of date.361return ValidateFormat(deviceFormat_);362} else if (hr == S_FALSE && closest != nullptr) {363// This means check closest. We'll allow it only if it's less specific on channels.364if (ValidateFormat(closest)) {365CoTaskMemFree(deviceFormat_);366deviceFormat_ = closest;367} else {368wchar_t guid[256]{};369StringFromGUID2(closest->SubFormat, guid, 256);370ERROR_LOG_REPORT_ONCE(badfallbackclosest, Log::sceAudio, "WASAPI fallback and closest unsupported (fmt=%04x/%s)", closest->Format.wFormatTag, guid);371CoTaskMemFree(closest);372return false;373}374} else {375CoTaskMemFree(closest);376if (hr != AUDCLNT_E_DEVICE_INVALIDATED && hr != AUDCLNT_E_SERVICE_NOT_RUNNING)377ERROR_LOG_REPORT_ONCE(badfallback, Log::sceAudio, "WASAPI fallback format was unsupported (%08x)", hr);378return false;379}380}381382return true;383}384385bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) {386// Don't know if PCM16 ever shows up here, the documentation only talks about float... but let's blindly387// try to support it :P388format_ = Format::UNKNOWN;389if (!fmt)390return false;391392if (fmt->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) {393if (!memcmp(&fmt->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(fmt->SubFormat))) {394if (fmt->Format.nChannels >= 1)395format_ = Format::IEEE_FLOAT;396} else {397wchar_t guid[256]{};398StringFromGUID2(fmt->SubFormat, guid, 256);399ERROR_LOG_REPORT_ONCE(unexpectedformat, Log::sceAudio, "Got unexpected WASAPI 0xFFFE stream format (%S), expected float!", guid);400if (fmt->Format.wBitsPerSample == 16 && fmt->Format.nChannels == 2) {401format_ = Format::PCM16;402}403}404} else if (fmt->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {405if (fmt->Format.nChannels >= 1)406format_ = Format::IEEE_FLOAT;407} else {408ERROR_LOG_REPORT_ONCE(unexpectedformat2, Log::sceAudio, "Got unexpected non-extensible WASAPI stream format, expected extensible float!");409if (fmt->Format.wBitsPerSample == 16 && fmt->Format.nChannels == 2) {410format_ = Format::PCM16;411}412}413414return format_ != Format::UNKNOWN;415}416417bool WASAPIAudioThread::PrepareFormat() {418delete [] shortBuf_;419shortBuf_ = nullptr;420421BYTE *pData = nullptr;422HRESULT hresult = renderClient_->GetBuffer(numBufferFrames, &pData);423if (FAILED(hresult) || !pData)424return false;425426const int numSamples = numBufferFrames * deviceFormat_->Format.nChannels;427if (format_ == Format::IEEE_FLOAT) {428memset(pData, 0, sizeof(float) * numSamples);429// This buffer is always stereo - PPSSPP writes to it.430shortBuf_ = new short[numBufferFrames * 2];431} else if (format_ == Format::PCM16) {432memset(pData, 0, sizeof(short) * numSamples);433}434435hresult = renderClient_->ReleaseBuffer(numBufferFrames, 0);436if (FAILED(hresult))437return false;438439actualDuration_ = (REFERENCE_TIME)((double)REFTIMES_PER_SEC * numBufferFrames / deviceFormat_->Format.nSamplesPerSec);440return true;441}442443void WASAPIAudioThread::Run() {444// Adapted from http://msdn.microsoft.com/en-us/library/windows/desktop/dd316756(v=vs.85).aspx445446_assert_(deviceEnumerator_ == nullptr);447HRESULT hresult = CoCreateInstance(CLSID_MMDeviceEnumerator,448nullptr, /* Object is not created as the part of the aggregate */449CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&deviceEnumerator_);450451if (FAILED(hresult) || deviceEnumerator_ == nullptr)452return;453454if (!ActivateDefaultDevice()) {455ERROR_LOG(Log::sceAudio, "WASAPI: Could not activate default device");456return;457}458459notificationClient_ = new CMMNotificationClient();460notificationClient_->SetCurrentDevice(device_);461hresult = deviceEnumerator_->RegisterEndpointNotificationCallback(notificationClient_);462if (FAILED(hresult)) {463// Let's just keep going, but release the client since it doesn't work.464delete notificationClient_;465notificationClient_ = nullptr;466}467468if (!InitAudioDevice()) {469ERROR_LOG(Log::sceAudio, "WASAPI: Could not init audio device");470return;471}472if (!PrepareFormat()) {473ERROR_LOG(Log::sceAudio, "WASAPI: Could not find a suitable audio output format");474return;475}476477hresult = audioInterface_->Start();478if (FAILED(hresult)) {479ERROR_LOG(Log::sceAudio, "WASAPI: Failed to start audio stream");480return;481}482483DWORD flags = 0;484while (flags != AUDCLNT_BUFFERFLAGS_SILENT) {485Sleep((DWORD)(actualDuration_ / REFTIMES_PER_MILLISEC / 2));486487uint32_t pNumPaddingFrames = 0;488hresult = audioInterface_->GetCurrentPadding(&pNumPaddingFrames);489if (FAILED(hresult)) {490// What to do?491pNumPaddingFrames = 0;492}493uint32_t pNumAvFrames = numBufferFrames - pNumPaddingFrames;494495BYTE *pData = nullptr;496hresult = renderClient_->GetBuffer(pNumAvFrames, &pData);497if (FAILED(hresult) || pData == nullptr) {498// What to do?499} else if (pNumAvFrames) {500int chans = deviceFormat_->Format.nChannels;501switch (format_) {502case Format::IEEE_FLOAT:503callback_(shortBuf_, pNumAvFrames, sampleRate_);504if (chans == 1) {505float *ptr = (float *)pData;506memset(ptr, 0, pNumAvFrames * chans * sizeof(float));507for (uint32_t i = 0; i < pNumAvFrames; i++) {508ptr[i * chans + 0] = 0.5f * ((float)shortBuf_[i * 2] + (float)shortBuf_[i * 2 + 1]) * (1.0f / 32768.0f);509}510} else if (chans == 2) {511ConvertS16ToF32((float *)pData, shortBuf_, pNumAvFrames * chans);512} else if (chans > 2) {513float *ptr = (float *)pData;514memset(ptr, 0, pNumAvFrames * chans * sizeof(float));515for (uint32_t i = 0; i < pNumAvFrames; i++) {516ptr[i * chans + 0] = (float)shortBuf_[i * 2] * (1.0f / 32768.0f);517ptr[i * chans + 1] = (float)shortBuf_[i * 2 + 1] * (1.0f / 32768.0f);518}519}520break;521case Format::PCM16:522callback_((short *)pData, pNumAvFrames, sampleRate_);523break;524}525}526527if (threadData_ != 0) {528flags = AUDCLNT_BUFFERFLAGS_SILENT;529}530531if (!FAILED(hresult) && pData) {532hresult = renderClient_->ReleaseBuffer(pNumAvFrames, flags);533if (FAILED(hresult)) {534// Not much to do here either...535}536}537538// Check if we should use a new device.539if (notificationClient_ && notificationClient_->HasDefaultDeviceChanged() && g_Config.bAutoAudioDevice) {540hresult = audioInterface_->Stop();541ShutdownAudioDevice();542543if (!ActivateDefaultDevice()) {544ERROR_LOG(Log::sceAudio, "WASAPI: Could not activate default device");545// TODO: Return to the old device here?546return;547}548notificationClient_->SetCurrentDevice(device_);549if (!InitAudioDevice()) {550ERROR_LOG(Log::sceAudio, "WASAPI: Could not init audio device");551return;552}553if (!PrepareFormat()) {554ERROR_LOG(Log::sceAudio, "WASAPI: Could not find a suitable audio output format");555return;556}557558hresult = audioInterface_->Start();559if (FAILED(hresult)) {560ERROR_LOG(Log::sceAudio, "WASAPI: Failed to start audio stream");561return;562}563}564}565566// Wait for last data in buffer to play before stopping.567Sleep((DWORD)(actualDuration_ / REFTIMES_PER_MILLISEC / 2));568569hresult = audioInterface_->Stop();570if (FAILED(hresult)) {571ERROR_LOG(Log::sceAudio, "WASAPI: Failed to stop audio stream");572}573}574575int WASAPIAudioBackend::RunThread() {576HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);577_dbg_assert_(SUCCEEDED(hr));578SetCurrentThreadName("WASAPI_audio");579580if (threadData_ == 0) {581// This will free everything once it's done.582WASAPIAudioThread renderer(threadData_, sampleRate_, callback_);583renderer.Run();584}585586threadData_ = 2;587CoUninitialize();588return 0;589}590591592