diff --git a/Common/Audio/AudioBackend.h b/Common/Audio/AudioBackend.h index 2de24176c0..bca0dfe53a 100644 --- a/Common/Audio/AudioBackend.h +++ b/Common/Audio/AudioBackend.h @@ -4,16 +4,31 @@ #include #include -// Always 2 channels, 16-bit audio. -typedef int (*StreamCallback)(short *buffer, int numSamples, int rate, void *userdata); +// For absolute minimal latency, we do not use std::function. Might be overthinking, but... +typedef void (*RenderCallback)(float *dest, int framesToWrite, int channels, int sampleRateHz, void *userdata); + +enum class LatencyMode { + Safe, + Aggressive +}; + +struct AudioDeviceDesc { + std::string name; // User-friendly name + std::string uniqueId; // store-able ID for settings. +}; + +inline float FramesToMs(int frames, int sampleRate) { + return 1000.0f * (float)frames / (float)sampleRate; +} -// Note that the backend may override the passed in sample rate. The actual sample rate -// should be returned by SampleRate though. class AudioBackend { public: virtual ~AudioBackend() {} - virtual bool Init(StreamCallback _callback, void *userdata = nullptr) = 0; + virtual void EnumerateDevices(std::vector *outputDevices, bool captureDevices = false) = 0; + virtual void SetRenderCallback(RenderCallback callback, void *userdata) = 0; + virtual bool InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault) = 0; virtual int SampleRate() const = 0; + virtual int BufferSize() const = 0; virtual int PeriodFrames() const = 0; - virtual void FrameUpdate() {} + virtual void FrameUpdate(bool allowAutoChange) {} }; diff --git a/Common/Render/Text/draw_text_uwp.cpp b/Common/Render/Text/draw_text_uwp.cpp index 2169db003e..15a14110a0 100644 --- a/Common/Render/Text/draw_text_uwp.cpp +++ b/Common/Render/Text/draw_text_uwp.cpp @@ -184,7 +184,7 @@ uint32_t TextDrawerUWP::SetFont(const char *fontName, int size, int flags) { fname = L"Tahoma"; TextDrawerFontContext *font = new TextDrawerFontContext(); - font->weight = DWRITE_FONT_WEIGHT_LIGHT; + font->weight = DWRITE_FONT_WEIGHT_NORMAL; font->height = size; font->fname = fname; font->dpiScale = dpiScale_; diff --git a/Common/UI/PopupScreens.h b/Common/UI/PopupScreens.h index be57d09ef6..6dce5b7ae6 100644 --- a/Common/UI/PopupScreens.h +++ b/Common/UI/PopupScreens.h @@ -261,19 +261,30 @@ private: // Allows passing in a dynamic vector of strings. Saves the string. class PopupMultiChoiceDynamic : public PopupMultiChoice { public: - PopupMultiChoiceDynamic(std::string *value, std::string_view text, std::vector choices, - I18NCat category, ScreenManager *screenManager, UI::LayoutParams *layoutParams = nullptr) + // TODO: This all is absolutely terrible, just done this way to be conformant with the internals of PopupMultiChoice. + PopupMultiChoiceDynamic(std::string *value, std::string_view text, const std::vector &choices, + I18NCat category, ScreenManager *screenManager, std::vector *values = nullptr, UI::LayoutParams *layoutParams = nullptr) : UI::PopupMultiChoice(&valueInt_, text, nullptr, 0, (int)choices.size(), category, screenManager, layoutParams), valueStr_(value) { + if (values) { + _dbg_assert_(choices.size() == values->size()); + } choices_ = new const char *[numChoices_]; valueInt_ = 0; for (int i = 0; i < numChoices_; i++) { choices_[i] = new char[choices[i].size() + 1]; memcpy((char *)choices_[i], choices[i].c_str(), choices[i].size() + 1); + if (values) { + if (*value == (*values)[i]) + valueInt_ = i; + } if (*value == choices_[i]) valueInt_ = i; } value_ = &valueInt_; + if (values) { + choiceValues_ = *values; + } UpdateText(); } ~PopupMultiChoiceDynamic() { @@ -288,8 +299,13 @@ protected: if (!valueStr_) { return true; } - if (*valueStr_ != choices_[num]) { - *valueStr_ = choices_[num]; + const char *value = choices_[num]; + if (choiceValues_.size() == numChoices_) { + value = choiceValues_[num].c_str(); + } + + if (*valueStr_ != value) { + *valueStr_ = value; return true; } else { return false; @@ -299,6 +315,7 @@ protected: private: int valueInt_; std::string *valueStr_; + std::vector choiceValues_; }; class PopupSliderChoice : public AbstractChoiceWithValueDisplay { diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index a9d22743c1..03a623b4dc 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -21,6 +21,7 @@ #include #include "Common/Net/Resolve.h" +#include "Common/Audio/AudioBackend.h" #include "Common/GPU/OpenGL/GLFeatures.h" #include "Common/Render/DrawBuffer.h" #include "Common/UI/Root.h" @@ -674,10 +675,11 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { PlayUISound(UI::UISound::CONFIRM); return UI::EVENT_DONE; }); - audioSettings->Add(new ItemHeader(a->T("Audio backend"))); bool sdlAudio = false; + #if defined(SDL) + audioSettings->Add(new ItemHeader(a->T("Audio backend"))); std::vector audioDeviceList; SplitString(System_GetProperty(SYSPROP_AUDIO_DEVICE_LIST), '\0', audioDeviceList); audioDeviceList.insert(audioDeviceList.begin(), a->T_cstr("Auto")); @@ -694,13 +696,48 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { }); #endif -#if PPSSPP_PLATFORM(WINDOWS) && !PPSSPP_PLATFORM(UWP) +#if PPSSPP_PLATFORM(WINDOWS) + extern AudioBackend *g_audioBackend; + + std::vector audioDeviceNames; + std::vector audioDeviceIds; + + std::vector deviceDescs; + g_audioBackend->EnumerateDevices(&deviceDescs); + if (!deviceDescs.empty()) { + audioSettings->Add(new ItemHeader(a->T("Audio backend"))); + for (auto &desc : deviceDescs) { + audioDeviceNames.push_back(desc.name); + audioDeviceIds.push_back(desc.uniqueId); + } + + audioDeviceNames.insert(audioDeviceNames.begin(), std::string(a->T("Auto"))); + audioDeviceIds.insert(audioDeviceIds.begin(), ""); + + PopupMultiChoiceDynamic *audioDevice = audioSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sAudioDevice, a->T("Device"), audioDeviceNames, I18NCat::NONE, screenManager(), &audioDeviceIds)); + audioDevice->OnChoice.Add([this](UI::EventParams &) { + bool reverted; + if (g_audioBackend->InitOutputDevice(g_Config.sAudioDevice, LatencyMode::Aggressive, &reverted)) { + if (reverted) { + WARN_LOG(Log::Audio, "Unexpected: After a direct choice, audio device reverted to default. '%s'", g_Config.sAudioDevice.c_str()); + } + } else { + WARN_LOG(Log::Audio, "InitOutputDevice failed"); + } + return UI::EVENT_DONE; + }); + CheckBox *autoAudio = audioSettings->Add(new CheckBox(&g_Config.bAutoAudioDevice, a->T("Use new audio devices automatically"))); + autoAudio->SetEnabledFunc([]()->bool { + return g_Config.sAudioDevice.empty(); + }); + } + const bool isWindows = true; #else const bool isWindows = false; -#endif + audioSettings->Add(new ItemHeader(a->T("Audio backend"))); - if (sdlAudio || isWindows) { + if (sdlAudio) { audioSettings->Add(new CheckBox(&g_Config.bAutoAudioDevice, a->T("Use new audio devices automatically"))); } @@ -712,6 +749,7 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { if (!audioErrorStr.empty()) { audioSettings->Add(new InfoItem(a->T("Audio Error"), audioErrorStr)); } +#endif #endif std::vector micList = Microphone::getDeviceList(); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 2854e7f593..e51e3e1dd5 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -803,6 +803,22 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch void CallbackPostRender(UIContext *dc, void *userdata); bool CreateGlobalPipelines(); +void NativeMixWrapper(float *dest, int framesToWrite, int channels, int sampleRateHz, void *userdata) { + static int16_t *buffer; + static int bufSize; + if (bufSize < framesToWrite * channels) { + buffer = new int16_t[framesToWrite * channels]; + bufSize = framesToWrite * channels; + } + + NativeMix(buffer, framesToWrite, sampleRateHz, userdata); + + for (int i = 0; i < framesToWrite * channels; i++) { + int16_t value = buffer[i]; + dest[i] = (float)value * (float)(1.0f / 32767.0f); + } +} + bool NativeInitGraphics(GraphicsContext *graphicsContext) { INFO_LOG(Log::System, "NativeInitGraphics"); @@ -840,7 +856,12 @@ bool NativeInitGraphics(GraphicsContext *graphicsContext) { g_audioBackend = System_CreateAudioBackend(); if (g_audioBackend) { - g_audioBackend->Init(&NativeMix); + g_audioBackend->SetRenderCallback(&NativeMixWrapper, nullptr); + bool reverted = false; + g_audioBackend->InitOutputDevice(g_Config.sAudioDevice, LatencyMode::Aggressive, &reverted); + if (reverted) { + g_Config.sAudioDevice.clear(); + } } #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) @@ -1093,7 +1114,7 @@ void NativeFrame(GraphicsContext *graphicsContext) { g_screenManager->update(); if (g_audioBackend) { - g_audioBackend->FrameUpdate(); + g_audioBackend->FrameUpdate(g_Config.bAutoAudioDevice); } // Do this after g_screenManager.update() so we can receive setting changes before rendering. diff --git a/UWP/PPSSPP_UWPMain.cpp b/UWP/PPSSPP_UWPMain.cpp index 258d525c9d..3e7e85e9dc 100644 --- a/UWP/PPSSPP_UWPMain.cpp +++ b/UWP/PPSSPP_UWPMain.cpp @@ -17,7 +17,6 @@ #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Encoding/Utf8.h" #include "Common/DirectXHelper.h" -#include "Common/File/FileUtil.h" #include "Common/Log.h" #include "Common/Log/LogManager.h" #include "Common/TimeUtil.h" @@ -173,7 +172,7 @@ void PPSSPP_UWPMain::UpdateScreenState() { bool PPSSPP_UWPMain::Render() { static bool hasSetThreadName = false; if (!hasSetThreadName) { - SetCurrentThreadName("UWPRenderThread"); + SetCurrentThreadName("EmuThread"); hasSetThreadName = true; } diff --git a/UWP/XAudioSoundStream.cpp b/UWP/XAudioSoundStream.cpp index 46bb9fc127..0691919d38 100644 --- a/UWP/XAudioSoundStream.cpp +++ b/UWP/XAudioSoundStream.cpp @@ -4,12 +4,12 @@ #include #include +#include #include "Common/Log.h" #include "Common/Thread/ThreadUtil.h" #include "Common/Audio/AudioBackend.h" #include "XAudioSoundStream.h" -#include const size_t BUFSIZE = 32 * 1024; @@ -18,69 +18,85 @@ public: XAudioBackend(); ~XAudioBackend() override; - bool Init(StreamCallback callback, void *userdata = nullptr) override; // If fails, can safely delete the object + void EnumerateDevices(std::vector *outputDevices, bool captureDevices = false) { + // Do nothing! Auto is the only option. + } + void SetRenderCallback(RenderCallback callback, void *userdata) override { + callback_ = callback; + userdata_ = userdata; + } + + bool InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *reverted) override; int SampleRate() const override { return sampleRate_; } - int PeriodFrames() const override { return 0; } + int PeriodFrames() const override { return samplesPerBuffer; } + int BufferSize() const override { return samplesPerBuffer * bufferCount; } private: - bool RunSound(); - bool CreateBuffer(); - void PollLoop(); + void Start(); + void Stop(); - StreamCallback callback_ = nullptr; + RenderCallback callback_ = nullptr; IXAudio2 *xaudioDevice = nullptr; - IXAudio2MasteringVoice *xaudioMaster = nullptr; - IXAudio2SourceVoice *xaudioVoice = nullptr; + IXAudio2MasteringVoice *masterVoice_ = nullptr; + IXAudio2SourceVoice *sourceVoice_ = nullptr; void *userdata_ = nullptr; + WAVEFORMATEX format_; + int sampleRate_ = 44100; int periodFrames_ = 0; - char realtimeBuffer_[BUFSIZE]{}; + enum { + samplesPerBuffer = 480, // 10 ms @ 48kHz. maybe we can tweak this using latency mode. + bufferCount = 3, + channels = 2, + }; + float audioBuffer_[samplesPerBuffer * channels]; + uint32_t cursor_ = 0; - HANDLE thread_ = 0; - HANDLE exitEvent_ = 0; - - bool exit = false; + std::thread thread_; + std::atomic running_{}; }; // TODO: Get rid of this static XAudioBackend *g_dsound; XAudioBackend::XAudioBackend() { - exitEvent_ = CreateEvent(nullptr, true, true, L""); + format_.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; + format_.nChannels = channels; + format_.nSamplesPerSec = 48000; + format_.wBitsPerSample = 32; + format_.nBlockAlign = format_.nChannels * format_.wBitsPerSample / 8; + format_.nAvgBytesPerSec = format_.nSamplesPerSec * format_.nBlockAlign; + format_.cbSize = 0; } -inline int RoundDown128(int x) { - return x & (~127); +XAudioBackend::~XAudioBackend() { + Stop(); } -bool XAudioBackend::CreateBuffer() { - if FAILED(xaudioDevice->CreateMasteringVoice(&xaudioMaster, 2, sampleRate_, 0, 0, NULL)) - return false; +void XAudioBackend::Stop() { + running_ = false; + if (thread_.joinable()) { + thread_.join(); + } - WAVEFORMATEX waveFormat; - waveFormat.cbSize = sizeof(waveFormat); - waveFormat.nAvgBytesPerSec = sampleRate_ * 4; - waveFormat.nBlockAlign = 4; - waveFormat.nChannels = 2; - waveFormat.nSamplesPerSec = sampleRate_; - waveFormat.wBitsPerSample = 16; - waveFormat.wFormatTag = WAVE_FORMAT_PCM; - - if FAILED(xaudioDevice->CreateSourceVoice(&xaudioVoice, &waveFormat, 0, 1.0, nullptr, nullptr, nullptr)) - return false; - - xaudioVoice->SetFrequencyRatio(1.0); - return true; + if (xaudioDevice) { + xaudioDevice->Release(); + xaudioDevice = nullptr; + sourceVoice_ = nullptr; + } } -bool XAudioBackend::RunSound() { +bool XAudioBackend::InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *reverted) { + Stop(); + + *reverted = false; if FAILED(XAudio2Create(&xaudioDevice, 0, XAUDIO2_DEFAULT_PROCESSOR)) { - xaudioDevice = NULL; + xaudioDevice = nullptr; return false; } @@ -90,96 +106,50 @@ bool XAudioBackend::RunSound() { //dbgCfg.BreakMask = XAUDIO2_LOG_ERRORS; xaudioDevice->SetDebugConfiguration(&dbgCfg); - if (!CreateBuffer()) { + if FAILED(xaudioDevice->CreateMasteringVoice(&masterVoice_, 2, sampleRate_, 0, 0, nullptr)) { xaudioDevice->Release(); - xaudioDevice = NULL; + xaudioDevice = nullptr; return false; } + if FAILED(xaudioDevice->CreateSourceVoice(&sourceVoice_, &format_, 0, 1.0, nullptr, nullptr, nullptr)) { + xaudioDevice->Release(); + xaudioDevice = nullptr; + return false; + } + + sourceVoice_->SetFrequencyRatio(1.0); + cursor_ = 0; - if FAILED(xaudioVoice->Start(0, XAUDIO2_COMMIT_NOW)) { + if FAILED(sourceVoice_->Start(0, XAUDIO2_COMMIT_NOW)) { xaudioDevice->Release(); - xaudioDevice = NULL; + xaudioDevice = nullptr; return false; } - thread_ = (HANDLE)_beginthreadex(0, 0, [](void* param) - { - SetCurrentThreadName("XAudio2"); - XAudioBackend *backend = (XAudioBackend *)param; - backend->PollLoop(); - return 0U; - }, (void *)this, 0, 0); - SetThreadPriority(thread_, THREAD_PRIORITY_ABOVE_NORMAL); + running_ = true; + thread_ = std::thread([this]() { + while (running_) { + XAUDIO2_VOICE_STATE state = {}; + sourceVoice_->GetState(&state); + if (state.BuffersQueued < bufferCount) { + // Fill buffer with audio + callback_(audioBuffer_, samplesPerBuffer, channels, format_.nSamplesPerSec, userdata_); + XAUDIO2_BUFFER buf = {}; + buf.AudioBytes = samplesPerBuffer * channels * sizeof(float); + buf.pAudioData = reinterpret_cast(audioBuffer_); + buf.Flags = 0; + sourceVoice_->SubmitSourceBuffer(&buf); + } else { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + }); return true; } -XAudioBackend::~XAudioBackend() { - if (!xaudioDevice) - return; - - if (!xaudioVoice) - return; - - exit = true; - WaitForSingleObject(exitEvent_, INFINITE); - CloseHandle(exitEvent_); - - xaudioDevice->Release(); -} - -bool XAudioBackend::Init(StreamCallback _callback, void *userdata) { - callback_ = _callback; - userdata_ = userdata; - return RunSound(); -} - -void XAudioBackend::PollLoop() { - ResetEvent(exitEvent_); - - while (!exit) { - XAUDIO2_VOICE_STATE state; - xaudioVoice->GetState(&state); - - // TODO: Still plenty of tuning to do here. - // 4 seems to work fine. - if (state.BuffersQueued > 4) { - Sleep(1); - continue; - } - - uint32_t bytesRequired = (sampleRate_ * 4) / 100; - - uint32_t bytesLeftInBuffer = BUFSIZE - cursor_; - uint32_t readCount = std::min(bytesRequired, bytesLeftInBuffer); - - // realtimeBuffer_ is just used as a ring of scratch space to be submitted, since SubmitSourceBuffer doesn't - // take ownership of the data. It needs to be big enough to fit the max number of buffers we check for - // above, which it is, easily. - - int stereoSamplesRendered = (*callback_)((short*)&realtimeBuffer_[cursor_], readCount / 4, sampleRate_, userdata_); - int numBytesRendered = 2 * sizeof(short) * stereoSamplesRendered; - - XAUDIO2_BUFFER xaudioBuffer{}; - xaudioBuffer.pAudioData = (const BYTE*)&realtimeBuffer_[cursor_]; - xaudioBuffer.AudioBytes = numBytesRendered; - - if FAILED(xaudioVoice->SubmitSourceBuffer(&xaudioBuffer, NULL)) { - WARN_LOG(Log::Audio, "XAudioBackend: Failed writing bytes"); - } - cursor_ += numBytesRendered; - if (cursor_ >= BUFSIZE) { - cursor_ = 0; - bytesLeftInBuffer = BUFSIZE; - } - periodFrames_ = stereoSamplesRendered; - } - - SetEvent(exitEvent_); -} - AudioBackend *System_CreateAudioBackend() { // Only one type available on UWP. return new XAudioBackend(); diff --git a/Windows/PPSSPP.vcxproj b/Windows/PPSSPP.vcxproj index 0bf9a82814..c52a62c76e 100644 --- a/Windows/PPSSPP.vcxproj +++ b/Windows/PPSSPP.vcxproj @@ -195,7 +195,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/x86/lib true Windows @@ -235,7 +235,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/x86_64/lib true $(OutDir)$(ProjectName).pdb @@ -271,7 +271,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;oleaut32.lib;comdlg32.lib;shell32.lib;user32.lib;gdi32.lib;advapi32.lib;ole32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;oleaut32.lib;comdlg32.lib;shell32.lib;user32.lib;gdi32.lib;advapi32.lib;ole32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/aarch64/lib true $(OutDir)$(ProjectName).pdb @@ -309,7 +309,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/x86/lib;%(AdditionalLibraryDirectories) $(OutDir)$(TargetName)$(TargetExt) true @@ -358,7 +358,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;opengl32.lib;glu32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/x86_64/lib;%(AdditionalLibraryDirectories) true Windows @@ -401,7 +401,7 @@ stdcpp17 - wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;oleaut32.lib;comdlg32.lib;shell32.lib;user32.lib;gdi32.lib;advapi32.lib;ole32.lib;%(AdditionalDependencies) + avrt.lib;mmdevapi.lib;wbemuuid.lib;dwmapi.lib;winhttp.lib;uxtheme.lib;mf.lib;mfplat.lib;mfreadwrite.lib;mfuuid.lib;shlwapi.lib;Winmm.lib;Ws2_32.lib;comctl32.lib;dxguid.lib;avcodec.lib;avformat.lib;avutil.lib;swresample.lib;swscale.lib;oleaut32.lib;comdlg32.lib;shell32.lib;user32.lib;gdi32.lib;advapi32.lib;ole32.lib;%(AdditionalDependencies) ../ffmpeg/Windows/aarch64/lib;%(AdditionalLibraryDirectories) true Windows diff --git a/Windows/WASAPIContext.cpp b/Windows/WASAPIContext.cpp index a7adfd73df..8e3becd287 100644 --- a/Windows/WASAPIContext.cpp +++ b/Windows/WASAPIContext.cpp @@ -1,583 +1,269 @@ -#include "stdafx.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include -#include -#include "WindowsAudio.h" #include "WASAPIContext.h" -#include "Common/Log.h" -#include "Common/LogReporting.h" -#include "Core/Config.h" -#include "Core/Util/AudioFormat.h" -#include "Common/Data/Encoding/Utf8.h" -#include "Common/Thread/ThreadUtil.h" - -#include -#include -#include -#include -#include -#include -#include -#include -#include "Functiondiscoverykeys_devpkey.h" - -// Includes some code from https://learn.microsoft.com/en-us/windows/win32/coreaudio/device-events - -#ifdef _MSC_VER -#pragma comment(lib, "ole32.lib") -#endif - -// Adapted from a MSDN sample. - -using Microsoft::WRL::ComPtr; - -class CMMNotificationClient final : public IMMNotificationClient { -public: - CMMNotificationClient() { - } - - virtual ~CMMNotificationClient() { - CoTaskMemFree(currentDevice_); - currentDevice_ = nullptr; - } - - void SetCurrentDevice(IMMDevice *device) { - std::lock_guard guard(lock_); - - CoTaskMemFree(currentDevice_); - currentDevice_ = nullptr; - if (!device || FAILED(device->GetId(¤tDevice_))) { - currentDevice_ = nullptr; - } - - if (currentDevice_) { - INFO_LOG(Log::sceAudio, "Switching to WASAPI audio device: '%s'", GetDeviceName(currentDevice_).c_str()); - } - - deviceChanged_ = false; - } - - bool HasDefaultDeviceChanged() const { - return deviceChanged_; - } - - // IUnknown methods -- AddRef, Release, and QueryInterface - ULONG STDMETHODCALLTYPE AddRef() override { - return InterlockedIncrement(&_cRef); - } - - ULONG STDMETHODCALLTYPE Release() override { - ULONG ulRef = InterlockedDecrement(&_cRef); - if (0 == ulRef) { - delete this; - } - return ulRef; - } - - HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, VOID **ppvInterface) override { - if (IID_IUnknown == riid) { - AddRef(); - *ppvInterface = (IUnknown*)this; - } else if (__uuidof(IMMNotificationClient) == riid) { - AddRef(); - *ppvInterface = (IMMNotificationClient*)this; - } else { - *ppvInterface = nullptr; - return E_NOINTERFACE; - } - return S_OK; - } - - // Callback methods for device-event notifications. - - HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override { - std::lock_guard guard(lock_); - - if (flow != eRender || role != eConsole) { - // Not relevant to us. - return S_OK; - } - - // pwstrDeviceId can be null. We consider that a new device, I think? - bool same = currentDevice_ == pwstrDeviceId; - if (!same && currentDevice_ && pwstrDeviceId) { - same = !wcscmp(currentDevice_, pwstrDeviceId); - } - if (same) { - // Already the current device, nothing to do. - return S_OK; - } - - deviceChanged_ = true; - INFO_LOG(Log::sceAudio, "New default eRender/eConsole WASAPI audio device detected: '%s'", GetDeviceName(pwstrDeviceId).c_str()); - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR pwstrDeviceId) override { - // Ignore. - return S_OK; - }; - - HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR pwstrDeviceId) override { - // Ignore. - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR pwstrDeviceId, DWORD dwNewState) override { - // Ignore. - return S_OK; - } - - HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR pwstrDeviceId, const PROPERTYKEY key) override { - INFO_LOG(Log::sceAudio, "Changed audio device property " - "{%8.8x-%4.4x-%4.4x-%2.2x%2.2x-%2.2x%2.2x%2.2x%2.2x%2.2x%2.2x}#%d", - (uint32_t)key.fmtid.Data1, key.fmtid.Data2, key.fmtid.Data3, - key.fmtid.Data4[0], key.fmtid.Data4[1], - key.fmtid.Data4[2], key.fmtid.Data4[3], - key.fmtid.Data4[4], key.fmtid.Data4[5], - key.fmtid.Data4[6], key.fmtid.Data4[7], - (int)key.pid); - return S_OK; - } - - std::string GetDeviceName(LPCWSTR pwstrId) - { - HRESULT hr = S_OK; - ComPtr pDevice; - ComPtr pProps; - PROPVARIANT varString; - PropVariantInit(&varString); - - if (_pEnumerator == NULL) - { - // Get enumerator for audio endpoint devices. - hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), - NULL, CLSCTX_INPROC_SERVER, - IID_PPV_ARGS(&_pEnumerator)); - } - if (hr == S_OK && _pEnumerator) { - hr = _pEnumerator->GetDevice(pwstrId, &pDevice); - } - if (hr == S_OK && pDevice) { - hr = pDevice->OpenPropertyStore(STGM_READ, &pProps); - } - if (hr == S_OK && pProps) { - // Get the endpoint device's friendly-name property. - hr = pProps->GetValue(PKEY_Device_FriendlyName, &varString); - } - - std::string name = ConvertWStringToUTF8((hr == S_OK) ? varString.pwszVal : L"null device"); - - PropVariantClear(&varString); - - return name; - } - -private: - std::mutex lock_; - LONG _cRef = 1; - ComPtr _pEnumerator; - wchar_t *currentDevice_ = nullptr; - bool deviceChanged_ = false; -}; - -// TODO: Make these adjustable. This is from the example in MSDN. -// 200 times/sec = 5ms, pretty good :) Wonder if all computers can handle it though. -#define REFTIMES_PER_SEC (10000000/200) -#define REFTIMES_PER_MILLISEC (REFTIMES_PER_SEC / 1000) - -WASAPIAudioBackend::WASAPIAudioBackend() : threadData_(0) { } - -WASAPIAudioBackend::~WASAPIAudioBackend() { - if (threadData_ == 0) { - threadData_ = 1; - } - - if (hThread_) { - WaitForSingleObject(hThread_, 1000); - CloseHandle(hThread_); - hThread_ = nullptr; - } - - if (threadData_ == 2) { - // blah. +static std::string ConvertWStringToUTF8(const std::wstring &wstr) { + int len = (int)wstr.size(); + int size = (int)WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), len, 0, 0, NULL, NULL); + std::string s; + s.resize(size); + if (size > 0) { + WideCharToMultiByte(CP_UTF8, 0, wstr.c_str(), len, &s[0], size, NULL, NULL); } + return s; } -unsigned int WINAPI WASAPIAudioBackend::soundThread(void *param) { - WASAPIAudioBackend *backend = (WASAPIAudioBackend *)param; - return backend->RunThread(); -} - -bool WASAPIAudioBackend::Init(StreamCallback callback, void *userdata) { - threadData_ = 0; - userdata_ = userdata; - callback_ = callback; - hThread_ = (HANDLE)_beginthreadex(0, 0, soundThread, (void *)this, 0, 0); - if (!hThread_) - return false; - SetThreadPriority(hThread_, THREAD_PRIORITY_ABOVE_NORMAL); - return true; -} - -// This to be run only on the thread. -class WASAPIAudioThread { -public: - WASAPIAudioThread(std::atomic &threadData, int &sampleRate, StreamCallback &callback) - : threadData_(threadData), sampleRate_(sampleRate), callback_(callback) { +static std::wstring ConvertUTF8ToWString(const std::string_view source) { + int len = (int)source.size(); + int size = (int)MultiByteToWideChar(CP_UTF8, 0, source.data(), len, NULL, 0); + std::wstring str; + str.resize(size); + if (size > 0) { + MultiByteToWideChar(CP_UTF8, 0, source.data(), (int)source.size(), &str[0], size); } - ~WASAPIAudioThread(); - - void Run(); - -private: - bool ActivateDefaultDevice(); - bool InitAudioDevice(); - void ShutdownAudioDevice(); - bool DetectFormat(); - bool ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt); - bool PrepareFormat(); - - std::atomic &threadData_; - int &sampleRate_; - StreamCallback &callback_; - - ComPtr deviceEnumerator_; - ComPtr device_; - ComPtr audioInterface_; - ComPtr notificationClient_; - WAVEFORMATEXTENSIBLE *deviceFormat_ = nullptr; - ComPtr renderClient_; - int16_t *shortBuf_ = nullptr; - - enum class Format { - UNKNOWN = 0, - IEEE_FLOAT = 1, - PCM16 = 2, - }; - - uint32_t numBufferFrames = 0; - Format format_ = Format::UNKNOWN; - REFERENCE_TIME actualDuration_{}; -}; - -WASAPIAudioThread::~WASAPIAudioThread() { - delete [] shortBuf_; - shortBuf_ = nullptr; - ShutdownAudioDevice(); - if (notificationClient_ && deviceEnumerator_) - deviceEnumerator_->UnregisterEndpointNotificationCallback(notificationClient_.Get()); - notificationClient_ = nullptr; - deviceEnumerator_ = nullptr; + return str; } -bool WASAPIAudioThread::ActivateDefaultDevice() { - _assert_(device_ == nullptr); - HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_); - if (FAILED(hresult) || device_ == nullptr) - return false; - - _assert_(audioInterface_ == nullptr); - hresult = device_->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, &audioInterface_); - if (FAILED(hresult) || audioInterface_ == nullptr) - return false; - - return true; +WASAPIContext::WASAPIContext() : notificationClient_(this) { + HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, __uuidof(IMMDeviceEnumerator), (void**)&enumerator_); + if (FAILED(hr)) { + // Bad! + enumerator_ = nullptr; + return; + } + enumerator_->RegisterEndpointNotificationCallback(¬ificationClient_); } -bool WASAPIAudioThread::InitAudioDevice() { - REFERENCE_TIME hnsBufferDuration = REFTIMES_PER_SEC; - _assert_(deviceFormat_ == nullptr); - HRESULT hresult = audioInterface_->GetMixFormat((WAVEFORMATEX **)&deviceFormat_); - if (FAILED(hresult) || !deviceFormat_) - return false; +WASAPIContext::~WASAPIContext() { + if (!enumerator_) { + // Nothing can have been happening. + return; + } + Stop(); + enumerator_->UnregisterEndpointNotificationCallback(¬ificationClient_); + enumerator_->Release(); +} - if (!DetectFormat()) { - // Format unsupported - let's not even try to initialize. - return false; +void WASAPIContext::EnumerateDevices(std::vector *output, bool captureDevices) { + IMMDeviceCollection *collection = nullptr; + enumerator_->EnumAudioEndpoints(captureDevices ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection); + + UINT count = 0; + collection->GetCount(&count); + + for (UINT i = 0; i < count; ++i) { + IMMDevice *device = nullptr; + collection->Item(i, &device); + + IPropertyStore *props = nullptr; + device->OpenPropertyStore(STGM_READ, &props); + + PROPVARIANT nameProp; + PropVariantInit(&nameProp); + props->GetValue(PKEY_Device_FriendlyName, &nameProp); + + LPWSTR id_str = 0; + if (SUCCEEDED(device->GetId(&id_str))) { + AudioDeviceDesc desc; + desc.name = ConvertWStringToUTF8(nameProp.pwszVal); + desc.uniqueId = ConvertWStringToUTF8(id_str); + output->push_back(desc); + CoTaskMemFree(id_str); + } + + PropVariantClear(&nameProp); + props->Release(); + device->Release(); } - hresult = audioInterface_->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, hnsBufferDuration, 0, &deviceFormat_->Format, nullptr); - if (FAILED(hresult)) - return false; - _assert_(renderClient_ == nullptr); - hresult = audioInterface_->GetService(IID_PPV_ARGS(&renderClient_)); - if (FAILED(hresult) || !renderClient_) - return false; - - numBufferFrames = 0; - hresult = audioInterface_->GetBufferSize(&numBufferFrames); - if (FAILED(hresult) || numBufferFrames == 0) - return false; - - sampleRate_ = deviceFormat_->Format.nSamplesPerSec; - - return true; + collection->Release(); } -void WASAPIAudioThread::ShutdownAudioDevice() { - renderClient_ = nullptr; - CoTaskMemFree(deviceFormat_); - deviceFormat_ = nullptr; - audioInterface_ = nullptr; - device_ = nullptr; -} +bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault) { + Stop(); -bool WASAPIAudioThread::DetectFormat() { - if (deviceFormat_ && !ValidateFormat(deviceFormat_)) { - // Last chance, let's try to ask for one we support instead. - WAVEFORMATEXTENSIBLE fmt{}; - fmt.Format.cbSize = sizeof(fmt); - fmt.Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT; - fmt.Format.nChannels = 2; - fmt.Format.nSamplesPerSec = 44100; - if (deviceFormat_->Format.nSamplesPerSec >= 22050 && deviceFormat_->Format.nSamplesPerSec <= 192000) - fmt.Format.nSamplesPerSec = deviceFormat_->Format.nSamplesPerSec; - fmt.Format.nBlockAlign = 2 * sizeof(float); - fmt.Format.nAvgBytesPerSec = fmt.Format.nSamplesPerSec * fmt.Format.nBlockAlign; - fmt.Format.wBitsPerSample = sizeof(float) * 8; - fmt.Samples.wReserved = 0; - fmt.dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; - fmt.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT; + *revertedToDefault = false; - WAVEFORMATEXTENSIBLE *closest = nullptr; - HRESULT hr = audioInterface_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, &fmt.Format, (WAVEFORMATEX **)&closest); - if (hr == S_OK) { - // Okay, great. Let's just use ours. - CoTaskMemFree(closest); - CoTaskMemFree(deviceFormat_); - deviceFormat_ = (WAVEFORMATEXTENSIBLE *)CoTaskMemAlloc(sizeof(fmt)); - if (deviceFormat_) - memcpy(deviceFormat_, &fmt, sizeof(fmt)); - - // In case something above gets out of date. - return ValidateFormat(deviceFormat_); - } else if (hr == S_FALSE && closest != nullptr) { - // This means check closest. We'll allow it only if it's less specific on channels. - if (ValidateFormat(closest)) { - CoTaskMemFree(deviceFormat_); - deviceFormat_ = closest; - } else { - wchar_t guid[256]{}; - StringFromGUID2(closest->SubFormat, guid, 256); - ERROR_LOG_REPORT_ONCE(badfallbackclosest, Log::sceAudio, "WASAPI fallback and closest unsupported (fmt=%04x/%s)", closest->Format.wFormatTag, ConvertWStringToUTF8(guid).c_str()); - CoTaskMemFree(closest); - return false; - } - } else { - CoTaskMemFree(closest); - if (hr != AUDCLNT_E_DEVICE_INVALIDATED && hr != AUDCLNT_E_SERVICE_NOT_RUNNING) - ERROR_LOG_REPORT_ONCE(badfallback, Log::sceAudio, "WASAPI fallback format was unsupported (%08lx)", hr); + IMMDevice *device = nullptr; + if (uniqueId.empty()) { + // Use the default device. + if (FAILED(enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device))) { return false; } - } - - return true; -} - -bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) { - // Don't know if PCM16 ever shows up here, the documentation only talks about float... but let's blindly - // try to support it :P - format_ = Format::UNKNOWN; - if (!fmt) - return false; - - if (fmt->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) { - if (!memcmp(&fmt->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(fmt->SubFormat))) { - if (fmt->Format.nChannels >= 1) - format_ = Format::IEEE_FLOAT; - } else { - wchar_t guid[256]{}; - StringFromGUID2(fmt->SubFormat, guid, 256); - ERROR_LOG_REPORT_ONCE(unexpectedformat, Log::sceAudio, "Got unexpected WASAPI 0xFFFE stream format (%S), expected float!", guid); - if (fmt->Format.wBitsPerSample == 16 && fmt->Format.nChannels == 2) { - format_ = Format::PCM16; - } - } - } else if (fmt->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { - if (fmt->Format.nChannels >= 1) - format_ = Format::IEEE_FLOAT; } else { - ERROR_LOG_REPORT_ONCE(unexpectedformat2, Log::sceAudio, "Got unexpected non-extensible WASAPI stream format, expected extensible float!"); - if (fmt->Format.wBitsPerSample == 16 && fmt->Format.nChannels == 2) { - format_ = Format::PCM16; + // Use whatever device. + std::wstring wId = ConvertUTF8ToWString(uniqueId); + if (FAILED(enumerator_->GetDevice(wId.c_str(), &device))) { + // Fallback to default device + printf("Falling back to default device...\n"); + *revertedToDefault = true; + if (FAILED(enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device))) { + return false; + } } } - return format_ != Format::UNKNOWN; -} + deviceId_ = uniqueId; -bool WASAPIAudioThread::PrepareFormat() { - delete [] shortBuf_; - shortBuf_ = nullptr; + HRESULT hr = E_FAIL; + // 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. + if (latencyMode != LatencyMode::Safe) { + hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, (void**)&audioClient3_); + } + if (SUCCEEDED(hr)) { + audioClient3_->GetMixFormat(&format_); + audioClient3_->GetSharedModeEnginePeriod(format_, &defaultPeriodFrames, &fundamentalPeriodFrames, &minPeriodFrames, &maxPeriodFrames); - BYTE *pData = nullptr; - HRESULT hresult = renderClient_->GetBuffer(numBufferFrames, &pData); - if (FAILED(hresult) || !pData) - return false; + printf("default: %d fundamental: %d min: %d max: %d\n", defaultPeriodFrames, fundamentalPeriodFrames, minPeriodFrames, maxPeriodFrames); + printf("initializing with %d frame period at %d Hz, meaning %0.1fms\n", (int)minPeriodFrames, (int)format_->nSamplesPerSec, FramesToMs(minPeriodFrames, format_->nSamplesPerSec)); - const int numSamples = numBufferFrames * deviceFormat_->Format.nChannels; - if (format_ == Format::IEEE_FLOAT) { - memset(pData, 0, sizeof(float) * numSamples); - // This buffer is always stereo - PPSSPP writes to it. - shortBuf_ = new short[numBufferFrames * 2]; - } else if (format_ == Format::PCM16) { - memset(pData, 0, sizeof(short) * numSamples); + audioEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); + HRESULT result = audioClient3_->InitializeSharedAudioStream( + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + minPeriodFrames, + format_, + nullptr + ); + if (FAILED(result)) { + printf("Error initializing shared audio stream: %08x", result); + audioClient3_->Release(); + device->Release(); + audioClient3_ = nullptr; + device = nullptr; + return false; + } + actualPeriodFrames_ = minPeriodFrames; + + audioClient3_->GetBufferSize(&reportedBufferSize_); + audioClient3_->SetEventHandle(audioEvent_); + audioClient3_->GetService(__uuidof(IAudioRenderClient), (void**)&renderClient_); + } else { + // Fallback to IAudioClient (older OS) + device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&audioClient_); + + audioClient_->GetMixFormat(&format_); + + // Get engine period info + REFERENCE_TIME defaultPeriod = 0, minPeriod = 0; + audioClient_->GetDevicePeriod(&defaultPeriod, &minPeriod); + + audioEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr); + + const REFERENCE_TIME duration = minPeriod; + HRESULT hr = audioClient_->Initialize( + AUDCLNT_SHAREMODE_SHARED, + AUDCLNT_STREAMFLAGS_EVENTCALLBACK, + duration, // This is a minimum, the result might be larger. We use GetBufferSize to check. + 0, // ref duration, always 0 in shared mode. + format_, + nullptr + ); + + if (FAILED(hr)) { + printf("ERROR: Failed to initialize audio with all attempted buffer sizes\n"); + audioClient_->Release(); + device->Release(); + audioClient_ = nullptr; + device = nullptr; + return false; + } + audioClient_->GetBufferSize(&reportedBufferSize_); + actualPeriodFrames_ = reportedBufferSize_; // we don't have a better estimate. + audioClient_->SetEventHandle(audioEvent_); + audioClient_->GetService(__uuidof(IAudioRenderClient), (void**)&renderClient_); } - hresult = renderClient_->ReleaseBuffer(numBufferFrames, 0); - if (FAILED(hresult)) - return false; + latencyMode_ = latencyMode; - actualDuration_ = (REFERENCE_TIME)((double)REFTIMES_PER_SEC * numBufferFrames / deviceFormat_->Format.nSamplesPerSec); + Start(); + + device->Release(); return true; } -void WASAPIAudioThread::Run() { - // Adapted from http://msdn.microsoft.com/en-us/library/windows/desktop/dd316756(v=vs.85).aspx +void WASAPIContext::Start() { + running_ = true; + audioThread_ = std::thread([this]() { AudioLoop(); }); +} - _assert_(deviceEnumerator_ == nullptr); - HRESULT hresult = CoCreateInstance(__uuidof(MMDeviceEnumerator), - nullptr, /* Object is not created as the part of the aggregate */ - CLSCTX_ALL, IID_PPV_ARGS(&deviceEnumerator_)); +void WASAPIContext::Stop() { + running_ = false; + if (audioClient_) audioClient_->Stop(); + if (audioEvent_) SetEvent(audioEvent_); + if (audioThread_.joinable()) audioThread_.join(); - if (FAILED(hresult) || deviceEnumerator_ == nullptr) - return; + if (renderClient_) { renderClient_->Release(); renderClient_ = nullptr; } + if (audioClient_) { audioClient_->Release(); audioClient_ = nullptr; } + if (audioEvent_) { CloseHandle(audioEvent_); audioEvent_ = nullptr; } + if (format_) { CoTaskMemFree(format_); format_ = nullptr; } +} - if (!ActivateDefaultDevice()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not activate default device"); - return; - } - - notificationClient_ = new CMMNotificationClient(); - notificationClient_->SetCurrentDevice(device_.Get()); - hresult = deviceEnumerator_->RegisterEndpointNotificationCallback(notificationClient_.Get()); - if (FAILED(hresult)) { - // Let's just keep going, but release the client since it doesn't work. - notificationClient_ = nullptr; - } - - if (!InitAudioDevice()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not init audio device"); - return; - } - if (!PrepareFormat()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not find a suitable audio output format"); - return; - } - - hresult = audioInterface_->Start(); - if (FAILED(hresult)) { - ERROR_LOG(Log::sceAudio, "WASAPI: Failed to start audio stream"); - return; - } - - DWORD flags = 0; - while (flags != AUDCLNT_BUFFERFLAGS_SILENT) { - Sleep((DWORD)(actualDuration_ / REFTIMES_PER_MILLISEC / 2)); - - uint32_t pNumPaddingFrames = 0; - hresult = audioInterface_->GetCurrentPadding(&pNumPaddingFrames); - if (FAILED(hresult)) { - // What to do? - pNumPaddingFrames = 0; - } - uint32_t pNumAvFrames = numBufferFrames - pNumPaddingFrames; - - BYTE *pData = nullptr; - hresult = renderClient_->GetBuffer(pNumAvFrames, &pData); - if (FAILED(hresult) || pData == nullptr) { - // What to do? - } else if (pNumAvFrames) { - int chans = deviceFormat_->Format.nChannels; - switch (format_) { - case Format::IEEE_FLOAT: - callback_(shortBuf_, pNumAvFrames, sampleRate_, nullptr); - if (chans == 1) { - float *ptr = (float *)pData; - memset(ptr, 0, pNumAvFrames * chans * sizeof(float)); - for (uint32_t i = 0; i < pNumAvFrames; i++) { - ptr[i * chans + 0] = 0.5f * ((float)shortBuf_[i * 2] + (float)shortBuf_[i * 2 + 1]) * (1.0f / 32768.0f); - } - } else if (chans == 2) { - ConvertS16ToF32((float *)pData, shortBuf_, pNumAvFrames * chans); - } else if (chans > 2) { - float *ptr = (float *)pData; - memset(ptr, 0, pNumAvFrames * chans * sizeof(float)); - for (uint32_t i = 0; i < pNumAvFrames; i++) { - ptr[i * chans + 0] = (float)shortBuf_[i * 2] * (1.0f / 32768.0f); - ptr[i * chans + 1] = (float)shortBuf_[i * 2 + 1] * (1.0f / 32768.0f); - } - } - break; - case Format::PCM16: - callback_((short *)pData, pNumAvFrames, sampleRate_, nullptr); - break; - case Format::UNKNOWN: - break; - } - } - - if (threadData_ != 0) { - flags = AUDCLNT_BUFFERFLAGS_SILENT; - } - - if (!FAILED(hresult) && pData) { - hresult = renderClient_->ReleaseBuffer(pNumAvFrames, flags); - if (FAILED(hresult)) { - // Not much to do here either... - } - } - - // Check if we should use a new device. - if (notificationClient_ && notificationClient_->HasDefaultDeviceChanged() && g_Config.bAutoAudioDevice) { - hresult = audioInterface_->Stop(); - ShutdownAudioDevice(); - - if (!ActivateDefaultDevice()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not activate default device"); - // TODO: Return to the old device here? - return; - } - notificationClient_->SetCurrentDevice(device_.Get()); - if (!InitAudioDevice()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not init audio device"); - return; - } - if (!PrepareFormat()) { - ERROR_LOG(Log::sceAudio, "WASAPI: Could not find a suitable audio output format"); - return; - } - - hresult = audioInterface_->Start(); - if (FAILED(hresult)) { - ERROR_LOG(Log::sceAudio, "WASAPI: Failed to start audio stream"); - return; - } - } - } - - // Wait for last data in buffer to play before stopping. - Sleep((DWORD)(actualDuration_ / REFTIMES_PER_MILLISEC / 2)); - - hresult = audioInterface_->Stop(); - if (FAILED(hresult)) { - ERROR_LOG(Log::sceAudio, "WASAPI: Failed to stop audio stream"); +void WASAPIContext::FrameUpdate(bool allowAutoChange) { + if (deviceId_.empty() && defaultDeviceChanged_ && allowAutoChange) { + defaultDeviceChanged_ = false; + Stop(); + Start(); } } -int WASAPIAudioBackend::RunThread() { - HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); - _dbg_assert_(SUCCEEDED(hr)); - SetCurrentThreadName("WASAPI_audio"); - - if (threadData_ == 0) { - // This will free everything once it's done. - WASAPIAudioThread renderer(threadData_, sampleRate_, callback_); - renderer.Run(); +void WASAPIContext::AudioLoop() { + DWORD taskID = 0; + HANDLE mmcssHandle = NULL; + if (latencyMode_ == LatencyMode::Aggressive) { + mmcssHandle = AvSetMmThreadCharacteristics(L"Pro Audio", &taskID); } - threadData_ = 2; - CoUninitialize(); - return 0; + if (audioClient3_) { + audioClient3_->Start(); + } else { + audioClient_->Start(); + } + + double phase = 0.0; + + while (running_) { + DWORD result = WaitForSingleObject(audioEvent_, INFINITE); + if (result != WAIT_OBJECT_0) { + // Something bad happened. + break; + } + + UINT32 padding = 0, available = 0; + if (audioClient3_) + audioClient3_->GetCurrentPadding(&padding), audioClient3_->GetBufferSize(&available); + else + audioClient_->GetCurrentPadding(&padding), audioClient_->GetBufferSize(&available); + + UINT32 framesToWrite = available - padding; + BYTE* buffer = nullptr; + if (framesToWrite > 0 && SUCCEEDED(renderClient_->GetBuffer(framesToWrite, &buffer))) { + callback_((float *)buffer, framesToWrite, 2, format_->nSamplesPerSec, userdata_); + renderClient_->ReleaseBuffer(framesToWrite, 0); + } + + // In the old mode, we just estimate the "actualPeriodFrames_" from the framesToWrite. + if (audioClient_ && framesToWrite < actualPeriodFrames_) { + actualPeriodFrames_ = framesToWrite; + } + } + + if (audioClient3_) { + audioClient3_->Stop(); + } else { + audioClient_->Stop(); + } + + if (mmcssHandle) { + AvRevertMmThreadCharacteristics(mmcssHandle); + } } diff --git a/Windows/WASAPIContext.h b/Windows/WASAPIContext.h index 50fc12f907..f83fb0f4c8 100644 --- a/Windows/WASAPIContext.h +++ b/Windows/WASAPIContext.h @@ -3,26 +3,89 @@ #include #include "WindowsAudio.h" -// This should only be included from WindowsAudio.cpp and WASAPIStream.cpp. +#include +#include +#include +#include +#include +#include +#include +#include -class WASAPIAudioBackend : public AudioBackend { +class WASAPIContext : public AudioBackend { public: - WASAPIAudioBackend(); - ~WASAPIAudioBackend(); + WASAPIContext(); + ~WASAPIContext(); - bool Init(StreamCallback callback, void *userdata) override; // If fails, can safely delete the object - int SampleRate() const override { return sampleRate_; } - int PeriodFrames() const override { return periodFrames_; } // amount of frames normally requested - void FrameUpdate() override {} + void SetRenderCallback(RenderCallback callback, void *userdata) override { + callback_ = callback; + userdata_ = userdata; + } + + void EnumerateDevices(std::vector *outputDevices, bool captureDevices = false); + + bool InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault); + + void FrameUpdate(bool allowAutoChange) override; + + int PeriodFrames() const override { return actualPeriodFrames_; } // NOTE: This may have the wrong value (too large) until audio has started playing. + int BufferSize() const override { return reportedBufferSize_; } + int SampleRate() const override { return format_->nSamplesPerSec; } + + // Implements device change notifications + class DeviceNotificationClient : public IMMNotificationClient { + public: + DeviceNotificationClient(WASAPIContext *engine) : engine_(engine) {} + ULONG STDMETHODCALLTYPE AddRef() override { return 1; } + ULONG STDMETHODCALLTYPE Release() override { return 1; } + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid, void** ppv) override { + if (iid == __uuidof(IUnknown) || iid == __uuidof(IMMNotificationClient)) { + *ppv = static_cast(this); + return S_OK; + } + *ppv = nullptr; + return E_NOINTERFACE; + } + + HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR) override { + if (flow == eRender && role == eConsole) { + // PostMessage(hwnd, WM_APP + 1, 0, 0); + engine_->defaultDeviceChanged_ = true; + } + return S_OK; + } + + HRESULT STDMETHODCALLTYPE OnDeviceAdded(LPCWSTR) override { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceRemoved(LPCWSTR) override { return S_OK; } + HRESULT STDMETHODCALLTYPE OnDeviceStateChanged(LPCWSTR, DWORD) override { return S_OK; } + HRESULT STDMETHODCALLTYPE OnPropertyValueChanged(LPCWSTR, const PROPERTYKEY) override { return S_OK; } + private: + WASAPIContext *engine_; + }; private: - int RunThread(); - static unsigned int WINAPI soundThread(void *param); + void Start(); + void Stop(); - HANDLE hThread_ = nullptr; - StreamCallback callback_ = nullptr; - int sampleRate_ = 0; - int periodFrames_ = 0; - void *userdata_ = 0; - std::atomic threadData_{}; + void AudioLoop(); + + // Only one of these can be non-null at a time. Check audioClient3 to determine if it's being used. + IAudioClient3 *audioClient3_ = nullptr; + IAudioClient *audioClient_ = nullptr; + + IAudioRenderClient* renderClient_ = nullptr; + WAVEFORMATEX *format_ = nullptr; + HANDLE audioEvent_ = nullptr; + std::thread audioThread_; + UINT32 defaultPeriodFrames = 0, fundamentalPeriodFrames = 0, minPeriodFrames = 0, maxPeriodFrames = 0; + std::atomic running_ = true; + UINT32 actualPeriodFrames_ = 0; // may not be the requested. + UINT32 reportedBufferSize_ = 0; + IMMDeviceEnumerator *enumerator_ = nullptr; + DeviceNotificationClient notificationClient_; + RenderCallback callback_{}; + void *userdata_ = nullptr; + LatencyMode latencyMode_ = LatencyMode::Aggressive; + std::string deviceId_; + std::atomic defaultDeviceChanged_{}; }; diff --git a/Windows/WindowsAudio.cpp b/Windows/WindowsAudio.cpp index 330712deba..b39a00f337 100644 --- a/Windows/WindowsAudio.cpp +++ b/Windows/WindowsAudio.cpp @@ -3,5 +3,5 @@ #include "WASAPIContext.h" AudioBackend *System_CreateAudioBackend() { - return new WASAPIAudioBackend(); + return new WASAPIContext(); }