From 828036f87b252157c8e241680209b6551854b892 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 10 Jul 2021 01:22:34 +0200 Subject: [PATCH] WASAPI: Survive switching to a mono device. Fixes #14591 It's a little flaky whether it tries to switch or not, but you can at least use Windows' device selector to switch back and forth. Should maybe also add an option to not follow the default device... Not sure. --- UI/NativeApp.cpp | 2 +- Windows/DSoundStream.cpp | 2 +- Windows/WASAPIStream.cpp | 72 +++++++++++++++++++++++++++++++++++----- Windows/WindowsAudio.h | 3 +- 4 files changed, 67 insertions(+), 12 deletions(-) diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 8e675dee95..5fb7f3a8de 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -205,7 +205,7 @@ public: }; #ifdef _WIN32 -int Win32Mix(short *buffer, int numSamples, int bits, int rate, int channels) { +int Win32Mix(short *buffer, int numSamples, int bits, int rate) { return NativeMix(buffer, numSamples); } #endif diff --git a/Windows/DSoundStream.cpp b/Windows/DSoundStream.cpp index ba03f042fb..96265d5eac 100644 --- a/Windows/DSoundStream.cpp +++ b/Windows/DSoundStream.cpp @@ -123,7 +123,7 @@ int DSoundAudioBackend::RunThread() { int numBytesToRender = RoundDown128(ModBufferSize(currentPos_ - lastPos_)); if (numBytesToRender >= 256) { - int numBytesRendered = 4 * (*callback_)(realtimeBuffer_, numBytesToRender >> 2, 16, 44100, 2); + int numBytesRendered = 4 * (*callback_)(realtimeBuffer_, numBytesToRender >> 2, 16, 44100); //We need to copy the full buffer, regardless of what the mixer claims to have filled //If we don't do this then the sound will loop if the sound stops and the mixer writes only zeroes numBytesRendered = numBytesToRender; diff --git a/Windows/WASAPIStream.cpp b/Windows/WASAPIStream.cpp index e8f7fe79f7..3a7b6565b2 100644 --- a/Windows/WASAPIStream.cpp +++ b/Windows/WASAPIStream.cpp @@ -5,6 +5,7 @@ #include "Common/Log.h" #include "Core/Reporting.h" #include "Core/Util/AudioFormat.h" +#include "Common/Data/Encoding/Utf8.h" #include "Common/Thread/ThreadUtil.h" @@ -50,6 +51,11 @@ public: if (!device || FAILED(device->GetId(¤tDevice_))) { currentDevice_ = nullptr; } + + if (currentDevice_) { + INFO_LOG(SCEAUDIO, "Switching to WASAPI audio device: '%s'", GetDeviceName(currentDevice_).c_str()); + } + deviceChanged_ = false; } @@ -89,7 +95,7 @@ public: HRESULT STDMETHODCALLTYPE OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR pwstrDeviceId) override { std::lock_guard guard(lock_); - if (flow != eRender || role != eMultimedia) { + if (flow != eRender || role != eConsole) { // Not relevant to us. return S_OK; } @@ -105,7 +111,7 @@ public: } deviceChanged_ = true; - INFO_LOG(SCEAUDIO, "New default WASAPI audio device detected"); + INFO_LOG(SCEAUDIO, "New default eRender/eConsole WASAPI audio device detected: '%s'", GetDeviceName(pwstrDeviceId).c_str()); return S_OK; } @@ -136,6 +142,45 @@ public: return S_OK; } + std::string GetDeviceName(LPCWSTR pwstrId) + { + HRESULT hr = S_OK; + IMMDevice *pDevice = NULL; + IPropertyStore *pProps = NULL; + PROPVARIANT varString; + PropVariantInit(&varString); + + if (_pEnumerator == NULL) + { + // Get enumerator for audio endpoint devices. + hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), + NULL, CLSCTX_INPROC_SERVER, + __uuidof(IMMDeviceEnumerator), + (void**)&_pEnumerator); + } + if (hr == S_OK) + { + hr = _pEnumerator->GetDevice(pwstrId, &pDevice); + } + if (hr == S_OK) + { + hr = pDevice->OpenPropertyStore(STGM_READ, &pProps); + } + if (hr == S_OK) + { + // 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); + + SAFE_RELEASE(pProps) + SAFE_RELEASE(pDevice) + return name; + } + private: std::mutex lock_; LONG _cRef = 1; @@ -236,7 +281,7 @@ WASAPIAudioThread::~WASAPIAudioThread() { bool WASAPIAudioThread::ActivateDefaultDevice() { _assert_(device_ == nullptr); - HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eMultimedia, &device_); + HRESULT hresult = deviceEnumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device_); if (FAILED(hresult) || device_ == nullptr) return false; @@ -341,7 +386,7 @@ bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) { if (fmt->Format.wFormatTag == WAVE_FORMAT_EXTENSIBLE) { if (!memcmp(&fmt->SubFormat, &KSDATAFORMAT_SUBTYPE_IEEE_FLOAT, sizeof(fmt->SubFormat))) { - if (fmt->Format.nChannels >= 2) + if (fmt->Format.nChannels >= 1) format_ = Format::IEEE_FLOAT; } else { ERROR_LOG_REPORT_ONCE(unexpectedformat, SCEAUDIO, "Got unexpected WASAPI 0xFFFE stream format, expected float!"); @@ -350,7 +395,7 @@ bool WASAPIAudioThread::ValidateFormat(const WAVEFORMATEXTENSIBLE *fmt) { } } } else if (fmt->Format.wFormatTag == WAVE_FORMAT_IEEE_FLOAT) { - if (fmt->Format.nChannels >= 2) + if (fmt->Format.nChannels >= 1) format_ = Format::IEEE_FLOAT; } else { ERROR_LOG_REPORT_ONCE(unexpectedformat2, SCEAUDIO, "Got unexpected non-extensible WASAPI stream format, expected extensible float!"); @@ -374,7 +419,8 @@ bool WASAPIAudioThread::PrepareFormat() { const int numSamples = numBufferFrames * deviceFormat_->Format.nChannels; if (format_ == Format::IEEE_FLOAT) { memset(pData, 0, sizeof(float) * numSamples); - shortBuf_ = new short[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); } @@ -394,6 +440,7 @@ void WASAPIAudioThread::Run() { HRESULT hresult = CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, /* Object is not created as the part of the aggregate */ CLSCTX_ALL, IID_IMMDeviceEnumerator, (void **)&deviceEnumerator_); + if (FAILED(hresult) || deviceEnumerator_ == nullptr) return; @@ -446,8 +493,14 @@ void WASAPIAudioThread::Run() { int chans = deviceFormat_->Format.nChannels; switch (format_) { case Format::IEEE_FLOAT: - callback_(shortBuf_, pNumAvFrames, 16, sampleRate_, chans); - if (chans == 2) { + callback_(shortBuf_, pNumAvFrames, 16, sampleRate_); + if (chans == 1) { + float *ptr = (float *)pData; + memset(ptr, 0, pNumAvFrames * chans * sizeof(float)); + for (UINT32 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; @@ -459,7 +512,7 @@ void WASAPIAudioThread::Run() { } break; case Format::PCM16: - callback_((short *)pData, pNumAvFrames, 16, sampleRate_, chans); + callback_((short *)pData, pNumAvFrames, 16, sampleRate_); break; } } @@ -482,6 +535,7 @@ void WASAPIAudioThread::Run() { if (!ActivateDefaultDevice()) { ERROR_LOG(SCEAUDIO, "WASAPI: Could not activate default device"); + // TODO: Return to the old device here? return; } notificationClient_->SetCurrentDevice(device_); diff --git a/Windows/WindowsAudio.h b/Windows/WindowsAudio.h index 2e9d0f730e..9adc196c01 100644 --- a/Windows/WindowsAudio.h +++ b/Windows/WindowsAudio.h @@ -4,7 +4,8 @@ #include "Core/Config.h" #include "Core/ConfigValues.h" -typedef int(*StreamCallback)(short *buffer, int numSamples, int bits, int rate, int channels); +// Always 2 channels. +typedef int(*StreamCallback)(short *buffer, int numSamples, int bits, int rate); // Note that the backend may override the passed in sample rate. The actual sample rate // should be returned by GetSampleRate though.