From 3656fb46e2dc4b72e8a0d9a96553907e77344bbb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sat, 10 Oct 2026 18:23:04 +0200 Subject: [PATCH] Windows: Capture the microphone through WASAPI instead of Media Foundation Windows now uses the same host interface as SDL and iOS: sceUsbMic sends startRecording/stopRecording/deviceChanged, and drains buffered input on the CPU thread with pollRecording. A capture thread asks WASAPI for mono 16-bit at the game's rate (it converts from the mix format, so no more ffmpeg resampling), and copies packets into a lock-free ring buffer. It reopens the device when the selection changes, and retries once a second if the device is missing or gets unplugged. The device list comes from the active capture endpoints. Their friendly names are the ones Media Foundation reported, so existing MicDevice settings still match. An empty setting, now selectable as Auto like on SDL, means the default capture device. The Media Foundation audio capture code is removed; the camera still uses it. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 2 + Core/HLE/sceUsbMic.cpp | 38 +---- UI/GameSettingsScreen.cpp | 2 - UI/NativeApp.cpp | 6 - Windows/CaptureDevice.cpp | 201 +--------------------- Windows/CaptureDevice.h | 36 +--- Windows/MainWindow.cpp | 2 - Windows/PPSSPP.vcxproj | 2 + Windows/PPSSPP.vcxproj.filters | 6 + Windows/WASAPIMicrophone.cpp | 302 +++++++++++++++++++++++++++++++++ Windows/WASAPIMicrophone.h | 33 ++++ Windows/main.cpp | 25 +++ 12 files changed, 376 insertions(+), 279 deletions(-) create mode 100644 Windows/WASAPIMicrophone.cpp create mode 100644 Windows/WASAPIMicrophone.h diff --git a/CMakeLists.txt b/CMakeLists.txt index cfe6c38565..61f5ffa9ce 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1162,6 +1162,8 @@ set(WindowsFiles Windows/WindowsAudio.h Windows/WASAPIContext.cpp Windows/WASAPIContext.h + Windows/WASAPIMicrophone.cpp + Windows/WASAPIMicrophone.h Windows/Debugger/BreakpointWindow.cpp Windows/Debugger/BreakpointWindow.h Windows/Debugger/DumpMemoryWindow.cpp diff --git a/Core/HLE/sceUsbMic.cpp b/Core/HLE/sceUsbMic.cpp index 91189c5678..323e364b36 100644 --- a/Core/HLE/sceUsbMic.cpp +++ b/Core/HLE/sceUsbMic.cpp @@ -31,15 +31,6 @@ #include "Core/CoreTiming.h" #include "Core/MemMapHelpers.h" -#if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) && !defined(__LIBRETRO__) -#define HAVE_WIN32_MICROPHONE -#endif - -#ifdef HAVE_WIN32_MICROPHONE -#include "Common/CommonWindows.h" -#include "Windows/CaptureDevice.h" -#endif - int eventMicBlockingResume = -1; static QueueBuf *audioBuf = nullptr; @@ -355,10 +346,6 @@ static int sceUsbMicWaitInputEnd() { } int Microphone::startMic(void *param) { -#ifdef HAVE_WIN32_MICROPHONE - if (winMic) - winMic->sendMessage({ CAPTUREDEVICE_COMMAND::START, param }); -#else // The rate is curSampleRate, which every caller has set. param can be null (from onMicDeviceChange). delete static_cast *>(param); INFO_LOG(Log::HLE, "microphone_command : sr = %d", curSampleRate); @@ -368,7 +355,6 @@ int Microphone::startMic(void *param) { CoreTiming::UnscheduleEvent(eventMicBlockingResume, 0); CoreTiming::ScheduleEvent(usToCycles(MIC_POLL_INTERVAL_US), eventMicBlockingResume, 0); } -#endif micState = 1; return 0; } @@ -378,26 +364,17 @@ int Microphone::stopMic() { micPolling = false; CoreTiming::UnscheduleEvent(eventMicBlockingResume, 0); } -#ifdef HAVE_WIN32_MICROPHONE - if (winMic) - winMic->sendMessage({ CAPTUREDEVICE_COMMAND::STOP, nullptr }); -#else System_MicrophoneCommand("stopRecording"); -#endif micState = 0; return 0; } bool Microphone::isHaveDevice() { -#ifdef HAVE_WIN32_MICROPHONE - // Only the app creates winMic, headless doesn't. - return winMic && winMic->getDeviceCounts() >= 1; -#elif PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) +#if PPSSPP_PLATFORM(ANDROID) || PPSSPP_PLATFORM(IOS) return System_AudioRecordingIsAvailable(); -#elif defined(SDL) +#else return micPolling || !Microphone::getDeviceList().empty(); #endif - return false; } bool Microphone::isMicStarted() { @@ -449,24 +426,15 @@ void Microphone::flushAudioData() { } std::vector Microphone::getDeviceList() { -#ifdef HAVE_WIN32_MICROPHONE - if (winMic) { - return winMic->getDeviceList(); - } -#elif defined(SDL) return System_GetPropertyStringVec(SYSPROP_MICROPHONE_DEVICE_LIST); -#endif - return std::vector(); } void Microphone::onMicDeviceChange() { -#if !defined(HAVE_WIN32_MICROPHONE) if (micPolling) { - // Reopen on the CPU thread when it next polls the capture stream. + // The host reopens its capture device. System_MicrophoneCommand("deviceChanged"); return; } -#endif if (Microphone::isMicStarted()) { Microphone::stopMic(); // Just use the last rate. diff --git a/UI/GameSettingsScreen.cpp b/UI/GameSettingsScreen.cpp index 19761c2291..e20b5ff7ff 100644 --- a/UI/GameSettingsScreen.cpp +++ b/UI/GameSettingsScreen.cpp @@ -770,10 +770,8 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) { if (!micList.empty()) { audioSettings->Add(new ItemHeader(a->T("Microphone"))); std::vector micValues = micList; -#if defined(SDL) micList.emplace(micList.begin(), a->T("Auto")); micValues.insert(micValues.begin(), ""); -#endif PopupMultiChoiceDynamic *MicChoice = audioSettings->Add(new PopupMultiChoiceDynamic(&g_Config.sMicDevice, a->T("Microphone Device"), micList, I18NCat::NONE, screenManager(), &micValues)); MicChoice->OnChoice.Add([](UI::EventParams &e) { Microphone::onMicDeviceChange(); diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 0bc223ee09..d7e397db1e 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -947,7 +947,6 @@ bool NativeInitGraphics(GraphicsContext *graphicsContext) { #if defined(_WIN32) && !PPSSPP_PLATFORM(UWP) if (IsWin7OrHigher()) { winCamera = new WindowsCaptureDevice(CAPTUREDEVICE_TYPE::VIDEO); - winMic = new WindowsCaptureDevice(CAPTUREDEVICE_TYPE::AUDIO); } #endif @@ -1078,11 +1077,6 @@ void NativeShutdownGraphics(GraphicsContext *graphicsContext) { delete winCamera; winCamera = nullptr; } - if (winMic) { - winMic->waitShutDown(); - delete winMic; - winMic = nullptr; - } #endif #if PPSSPP_PLATFORM(IOS) diff --git a/Windows/CaptureDevice.cpp b/Windows/CaptureDevice.cpp index 64f73bdbd1..be21044166 100644 --- a/Windows/CaptureDevice.cpp +++ b/Windows/CaptureDevice.cpp @@ -115,7 +115,6 @@ bool UnRegisterCMPTMFApis() { } WindowsCaptureDevice *winCamera; -WindowsCaptureDevice *winMic; // TODO: Add more formats, but need some tests. VideoFormatTransform g_VideoFormats[] = { @@ -125,18 +124,9 @@ VideoFormatTransform g_VideoFormats[] = { MFVideoFormat_NV12, AV_PIX_FMT_NV12 } }; -AudioFormatTransform g_AudioFormats[] = { - { MFAudioFormat_PCM, 8, AV_SAMPLE_FMT_U8 }, - { MFAudioFormat_PCM, 16, AV_SAMPLE_FMT_S16 }, - { MFAudioFormat_PCM, 32, AV_SAMPLE_FMT_S32 }, - { MFAudioFormat_Float, 32, AV_SAMPLE_FMT_FLT } -}; - const int g_cVideoFormats = ARRAYSIZE(g_VideoFormats); -const int g_cAudioFormats = ARRAYSIZE(g_AudioFormats); MediaParam defaultVideoParam = { { 640, 480, 0, MFVideoFormat_RGB24 } }; -MediaParam defaultAudioParam = { { 44100, 2, 16, MFAudioFormat_PCM } }; HRESULT GetDefaultStride(IMFMediaType *pType, LONG *plStride); @@ -147,9 +137,6 @@ ReaderCallback::~ReaderCallback() { if (img_convert_ctx) { sws_freeContext(img_convert_ctx); } - if (resample_ctx) { - swr_free(&resample_ctx); - } #endif } @@ -277,37 +264,6 @@ HRESULT ReaderCallback::OnReadSample( delete videoBuffer; break; } - case CAPTUREDEVICE_TYPE::AUDIO: { - BYTE *sampleBuf = nullptr; - DWORD length = 0; - u32 sizeAfterResample = 0; - // pSample can be null, in this case ReadSample still should be called to request next frame. - if (pSample) { - pBuffer->Lock(&sampleBuf, nullptr, &length); - if (device->needResample()) { - sizeAfterResample = doResample( - &device->resampleBuf, device->targetMediaParam.sampleRate, device->targetMediaParam.channels, &device->resampleBufSize, - sampleBuf, device->deviceParam.sampleRate, device->deviceParam.channels, device->deviceParam.audioFormat, length, device->deviceParam.bitsPerSample); - if (device->resampleBuf) - Microphone::addAudioData(device->resampleBuf, sizeAfterResample); - } else { - Microphone::addAudioData(sampleBuf, length); - } - pBuffer->Unlock(); - } - // Request the next frame. - if (SUCCEEDED(hr)) { - hr = device->m_pReader->ReadSample( - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - 0, - nullptr, - nullptr, - nullptr, - nullptr - ); - } - break; - } } } @@ -322,14 +278,6 @@ AVPixelFormat ReaderCallback::getAVVideoFormatbyMFVideoFormat(const GUID &MFVide return AV_PIX_FMT_RGB24; } -AVSampleFormat ReaderCallback::getAVAudioFormatbyMFAudioFormat(const GUID &MFAudioFormat, const u32 &bitsPerSample) { - for (int i = 0; i < g_cAudioFormats; i++) { - if (MFAudioFormat == g_AudioFormats[i].MFAudioFormat && bitsPerSample == g_AudioFormats[i].bitsPerSample) - return g_AudioFormats[i].AVAudioFormat; - } - return AV_SAMPLE_FMT_S16; -} - void ReaderCallback::imgConvert( unsigned char *dst, unsigned int &dstW, unsigned int &dstH, int dstLineSizes[4], unsigned char *src, const unsigned int &srcW, const unsigned int &srcH, const GUID &srcFormat, @@ -452,48 +400,6 @@ void ReaderCallback::imgInvertNV12(unsigned char *dst, int &dstStride, unsigned } } -u32 ReaderCallback::doResample(u8 **dst, u32 &dstSampleRate, u32 &dstChannels, u32 *dstSize, u8 *src, const u32 &srcSampleRate, const u32 &srcChannels, const GUID &srcFormat, const u32& srcSize, const u32& srcBitsPerSample) { -#ifdef USE_FFMPEG - AVSampleFormat srcAVFormat = getAVAudioFormatbyMFAudioFormat(srcFormat, srcBitsPerSample); - int outSamplesCount = 0; - if (resample_ctx == nullptr) { - resample_ctx = swr_alloc_set_opts(nullptr, - av_get_default_channel_layout(dstChannels), - AV_SAMPLE_FMT_S16, - dstSampleRate, - av_get_default_channel_layout(srcChannels), - srcAVFormat, - srcSampleRate, - 0, - nullptr); - if (resample_ctx == nullptr || swr_init(resample_ctx) < 0) { - swr_free(&resample_ctx); - return 0; - } - } - int srcSamplesCount = srcSize / srcChannels / av_get_bytes_per_sample(srcAVFormat); // per channel. - int outCount = srcSamplesCount * dstSampleRate / srcSampleRate + 256; - unsigned int outSize = av_samples_get_buffer_size(nullptr, dstChannels, outCount, AV_SAMPLE_FMT_S16, 0); - - if (!*dst) { - *dst = (u8 *)av_malloc(outSize); - *dstSize = outSize; - } - if (!*dst) - return 0; - - if(*dstSize < outSize) - av_fast_malloc(dst, dstSize, outSize); - - outSamplesCount = swr_convert(resample_ctx, dst, outCount, (const uint8_t **)&src, srcSamplesCount); - if (outSamplesCount < 0) - return 0; - return av_samples_get_buffer_size(nullptr, dstChannels, outSamplesCount, AV_SAMPLE_FMT_S16, 0); -#else - return 0; -#endif -} - WindowsCaptureDevice::WindowsCaptureDevice(CAPTUREDEVICE_TYPE _type) : type(_type), error(CAPTUREDEVIDE_ERROR_NO_ERROR), @@ -505,9 +411,6 @@ WindowsCaptureDevice::WindowsCaptureDevice(CAPTUREDEVICE_TYPE _type) : case CAPTUREDEVICE_TYPE::VIDEO: targetMediaParam = defaultVideoParam; break; - case CAPTUREDEVICE_TYPE::AUDIO: - targetMediaParam = defaultAudioParam; - break; } thread_ = std::thread(&WindowsCaptureDevice::messageHandler, this); @@ -520,9 +423,6 @@ WindowsCaptureDevice::~WindowsCaptureDevice() { av_freep(&imageRGB); av_freep(&imageJpeg); break; - case CAPTUREDEVICE_TYPE::AUDIO: - av_freep(&resampleBuf); - break; } #endif } @@ -552,7 +452,7 @@ bool WindowsCaptureDevice::start(void *startParam) { m_pCallback = new ReaderCallback(this); - std::string selectedDeviceName = type == CAPTUREDEVICE_TYPE::VIDEO ? g_Config.sCameraDevice : g_Config.sMicDevice; + std::string selectedDeviceName = g_Config.sCameraDevice; switch (state) { case CAPTUREDEVICE_STATE::STOPPED: @@ -644,41 +544,6 @@ bool WindowsCaptureDevice::start(void *startParam) { break; } - case CAPTUREDEVICE_TYPE::AUDIO: { - if (startParam) { - std::vector *micParam = static_cast*>(startParam); - targetMediaParam.sampleRate = micParam->at(0); - targetMediaParam.channels = micParam->at(1); - delete micParam; - } - - for (DWORD i = 0; ; i++) { - hr = m_pReader->GetNativeMediaType( - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - i, - &pType - ); - - if (FAILED(hr)) { break; } - - hr = setDeviceParam(pType.Get()); - - if (SUCCEEDED(hr)) - break; - } - - if (SUCCEEDED(hr)) { - hr = m_pReader->ReadSample( - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - 0, - nullptr, - nullptr, - nullptr, - nullptr - ); - } - break; - } } } @@ -825,48 +690,6 @@ HRESULT WindowsCaptureDevice::setDeviceParam(IMFMediaType *pType) { if (SUCCEEDED(hr)) hr = GetDefaultStride(pType, &deviceParam.default_stride); - break; - case CAPTUREDEVICE_TYPE::AUDIO: - hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype); - if (FAILED(hr)) - break; - - for (int i = 0; i < g_cAudioFormats; i++) { - if (subtype == g_AudioFormats[i].MFAudioFormat) { - deviceParam.audioFormat = subtype; - getFormat = true; - break; - } - } - - if (!getFormat) { - for (int i = 0; i < g_cAudioFormats; i++) { - hr = pType->SetGUID(MF_MT_SUBTYPE, g_AudioFormats[i].MFAudioFormat); - if (FAILED(hr)) - continue; - - hr = m_pReader->SetCurrentMediaType( - (DWORD)MF_SOURCE_READER_FIRST_AUDIO_STREAM, - NULL, - pType - ); - - if (SUCCEEDED(hr)) { - deviceParam.audioFormat = g_AudioFormats[i].MFAudioFormat; - getFormat = true; - break; - } - } - } - if (SUCCEEDED(hr)) - hr = pType->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &deviceParam.sampleRate); - - if (SUCCEEDED(hr)) - hr = pType->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &deviceParam.channels); - - if (SUCCEEDED(hr)) - hr = pType->GetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, (UINT32 *)&deviceParam.bitsPerSample); - break; } @@ -912,7 +735,7 @@ void WindowsCaptureDevice::waitShutDown() { // This is the main thread for the capture device. void WindowsCaptureDevice::messageHandler() { - SetCurrentThreadName(type == CAPTUREDEVICE_TYPE::AUDIO ? "AudioCapture" : "VideoCapture"); + SetCurrentThreadName("Camera"); if (!MFAPI::Startup) { setError(CAPTUREDEVIDE_ERROR_INIT_FAILED, "Cannot register devices"); @@ -932,12 +755,6 @@ void WindowsCaptureDevice::messageHandler() { updateState(CAPTUREDEVICE_STATE::STOPPED); CAPTUREDEVICE_MESSAGE message; - if (type == CAPTUREDEVICE_TYPE::VIDEO) { - SetCurrentThreadName("Camera"); - } else if (type == CAPTUREDEVICE_TYPE::AUDIO) { - SetCurrentThreadName("Microphone"); - } - while ((message = getMessage()).command != CAPTUREDEVICE_COMMAND::SHUTDOWN) { switch (message.command) { case CAPTUREDEVICE_COMMAND::START: @@ -985,13 +802,6 @@ HRESULT WindowsCaptureDevice::enumDevices() { MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID ); - break; - case CAPTUREDEVICE_TYPE::AUDIO: - hr = pAttributes->SetGUID( - MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE, - MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_AUDCAP_GUID - ); - break; default: setError(CAPTUREDEVIDE_ERROR_UNKNOWN_TYPE, "Unknown device type"); @@ -1005,13 +815,6 @@ HRESULT WindowsCaptureDevice::enumDevices() { return hr; } -bool WindowsCaptureDevice::needResample() { - return deviceParam.sampleRate != targetMediaParam.sampleRate || - deviceParam.channels != targetMediaParam.channels || - deviceParam.audioFormat != targetMediaParam.audioFormat || - deviceParam.bitsPerSample != targetMediaParam.bitsPerSample; -} - //----------------------------------------------------------------------------- // GetDefaultStride // diff --git a/Windows/CaptureDevice.h b/Windows/CaptureDevice.h index 81acd712ce..53af1e034b 100644 --- a/Windows/CaptureDevice.h +++ b/Windows/CaptureDevice.h @@ -27,13 +27,12 @@ #include #include -#include "Core/HLE/sceUsbMic.h" +#include "Common/CommonTypes.h" #ifdef __cplusplus extern "C" { #include "libavformat/avformat.h" #include "libswscale/swscale.h" -#include "libswresample/swresample.h" #include "libavutil/imgutils.h" } #endif // __cplusplus @@ -43,15 +42,8 @@ struct VideoFormatTransform { AVPixelFormat AVVideoFormat; }; -struct AudioFormatTransform { - GUID MFAudioFormat; - u32 bitsPerSample; - AVSampleFormat AVAudioFormat; -}; - enum class CAPTUREDEVICE_TYPE { VIDEO, - AUDIO, }; enum class CAPTUREDEVICE_STATE { @@ -96,12 +88,6 @@ union MediaParam { LONG default_stride; GUID videoFormat; }; - struct { - UINT32 sampleRate; - UINT32 channels; - LONG bitsPerSample; - GUID audioFormat; - }; }; template void SafeRelease(T **ppT) { @@ -136,7 +122,6 @@ public: STDMETHODIMP OnFlush(DWORD) override { return S_OK; } AVPixelFormat getAVVideoFormatbyMFVideoFormat(const GUID &MFVideoFormat); - AVSampleFormat getAVAudioFormatbyMFAudioFormat(const GUID &MFAudioFormat, const u32 &bitsPerSample); /* * Always convert the image to RGB24 @@ -159,20 +144,9 @@ public: void imgInvertYUY2(unsigned char *dst, int &dstStride, unsigned char *src, const int &srcStride, const int &h); void imgInvertNV12(unsigned char *dst, int &dstStride, unsigned char *src, const int &srcStride, const int &h); - /* - * Always resample to uncompressed signed 16bits - * @param dst pointer to pointer of dst buffer could be a nullptr, would be overwritten by a new pointer if space too small or a nullptr, should be freed by av_free/av_freep by caller - * @param dstSize pointer to size of the dst buffer, could be modified after this func - * @param srcFormat MF_MT_SUBTYPE attribute of the source audio data - * @param srcSize size of valid data in the source buffer, in bytes - * @return size of output in bytes - */ - u32 doResample(u8 **dst, u32 &dstSampleRate, u32 &dstChannels, u32 *dstSize, u8 *src, const u32 &srcSampleRate, const u32 &srcChannels, const GUID &srcFormat, const u32 &srcSize, const u32& srcBitsPerSamples); - protected: WindowsCaptureDevice *device; SwsContext *img_convert_ctx = nullptr; - SwrContext *resample_ctx = nullptr; }; class WindowsCaptureDevice { @@ -204,8 +178,6 @@ public: HRESULT enumDevices(); - bool needResample(); - friend class ReaderCallback; protected: @@ -242,21 +214,15 @@ protected: std::mutex stateMutex_; std::condition_variable stateCond_; - // Camera only unsigned char *imageRGB = nullptr; int imgRGBLineSizes[4]{}; unsigned char *imageJpeg = nullptr; int imgJpegSize = 0; - //Microphone only - u8 *resampleBuf = nullptr; - u32 resampleBufSize = 0; - std::thread thread_; }; extern WindowsCaptureDevice *winCamera; -extern WindowsCaptureDevice *winMic; bool RegisterCMPTMFApis(); bool UnRegisterCMPTMFApis(); diff --git a/Windows/MainWindow.cpp b/Windows/MainWindow.cpp index 9ca51ade99..3cf7f60693 100644 --- a/Windows/MainWindow.cpp +++ b/Windows/MainWindow.cpp @@ -1003,8 +1003,6 @@ namespace MainWindow { DinputDevice::CheckDevices(); if (winCamera) winCamera->CheckDevices(); - if (winMic) - winMic->CheckDevices(); return DefWindowProc(hWnd, message, wParam, lParam); case WM_VERYSLEEPY_MSG: diff --git a/Windows/PPSSPP.vcxproj b/Windows/PPSSPP.vcxproj index c63eb2e768..f62c374f30 100644 --- a/Windows/PPSSPP.vcxproj +++ b/Windows/PPSSPP.vcxproj @@ -666,6 +666,7 @@ + @@ -1035,6 +1036,7 @@ + diff --git a/Windows/PPSSPP.vcxproj.filters b/Windows/PPSSPP.vcxproj.filters index 7d4ff63545..f3fbd990a7 100644 --- a/Windows/PPSSPP.vcxproj.filters +++ b/Windows/PPSSPP.vcxproj.filters @@ -178,6 +178,9 @@ Windows\System + + Windows\System + Windows\Input @@ -372,6 +375,9 @@ Windows\System + + Windows\System + Windows\Input diff --git a/Windows/WASAPIMicrophone.cpp b/Windows/WASAPIMicrophone.cpp new file mode 100644 index 0000000000..8a9873d42f --- /dev/null +++ b/Windows/WASAPIMicrophone.cpp @@ -0,0 +1,302 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#include +// EndpointFormFactor has an unscoped Microphone, which clashes with our namespace. +#define Microphone EndpointFormFactor_Microphone +#include +#undef Microphone +#include +#include +#include + +#include +#include +#include + +#include "Common/Data/Encoding/Utf8.h" +#include "Common/Log.h" +#include "Common/Thread/ThreadUtil.h" +#include "Core/Config.h" +#include "Core/HLE/sceUsbMic.h" +#include "Windows/WASAPIMicrophone.h" + +using Microsoft::WRL::ComPtr; + +// Single producer (the capture thread), single consumer (the CPU thread, in WASAPIMicrophonePoll). +// Input that doesn't fit is dropped. +static constexpr uint32_t MIC_RING_SIZE = 32768; // Samples, a power of two. 0.7 s at 44.1 kHz. +static int16_t g_micRing[MIC_RING_SIZE]; +static std::atomic g_micWritePos{0}; +static std::atomic g_micReadPos{0}; + +// Only touched on the CPU thread. +static std::thread g_micThread; + +static HANDLE g_stopEvent; // Manual reset, the thread exits. +static HANDLE g_reopenEvent; // Auto reset, the thread reopens the device. + +// How long the shared-mode buffer is, in 100 ns units. +static constexpr REFERENCE_TIME MIC_BUFFER_DURATION = 1000000; // 100 ms. + +static std::string GetFriendlyName(IMMDevice *device) { + std::string name; + ComPtr props; + if (FAILED(device->OpenPropertyStore(STGM_READ, &props)) || !props) { + return name; + } + PROPVARIANT nameProp; + PropVariantInit(&nameProp); + if (SUCCEEDED(props->GetValue(PKEY_Device_FriendlyName, &nameProp)) && nameProp.vt == VT_LPWSTR && nameProp.pwszVal) { + name = ConvertWStringToUTF8(nameProp.pwszVal); + } + PropVariantClear(&nameProp); + return name; +} + +// The configured device if it's there, otherwise the default one. +static ComPtr FindDevice(IMMDeviceEnumerator *enumerator) { + ComPtr device; + const std::string wanted = g_Config.sMicDevice; + if (!wanted.empty()) { + ComPtr collection; + UINT count = 0; + if (SUCCEEDED(enumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection)) && collection) { + collection->GetCount(&count); + } + for (UINT i = 0; i < count; i++) { + ComPtr candidate; + if (SUCCEEDED(collection->Item(i, &candidate)) && candidate && GetFriendlyName(candidate.Get()) == wanted) { + return candidate; + } + } + WARN_LOG(Log::Audio, "Microphone '%s' not found, using the default one", wanted.c_str()); + } + enumerator->GetDefaultAudioEndpoint(eCapture, eConsole, &device); + return device; +} + +static void PushSamples(const int16_t *samples, uint32_t frames) { + const uint32_t write = g_micWritePos.load(std::memory_order_relaxed); + const uint32_t read = g_micReadPos.load(std::memory_order_acquire); + const uint32_t count = std::min(frames, MIC_RING_SIZE - (write - read)); + for (uint32_t i = 0; i < count; i++) { + g_micRing[(write + i) & (MIC_RING_SIZE - 1)] = samples ? samples[i] : 0; + } + g_micWritePos.store(write + count, std::memory_order_release); +} + +enum class CaptureResult { + STOPPED, + REOPEN, + FAILED, +}; + +// Captures until we're stopped, or asked to reopen, or the device goes away. +static CaptureResult RunCapture(IMMDeviceEnumerator *enumerator, int sampleRate, HANDLE audioEvent, bool logErrors) { + ComPtr device = FindDevice(enumerator); + if (!device) { + if (logErrors) { + WARN_LOG(Log::Audio, "No microphone, the game will get silence"); + } + return CaptureResult::FAILED; + } + + ComPtr client; + HRESULT hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void **)&client); + if (SUCCEEDED(hr)) { + // Mono 16-bit at the rate the game asked for. WASAPI converts from the mix format. + WAVEFORMATEX format{}; + format.wFormatTag = WAVE_FORMAT_PCM; + format.nChannels = 1; + format.nSamplesPerSec = sampleRate; + format.wBitsPerSample = 16; + format.nBlockAlign = 2; + format.nAvgBytesPerSec = sampleRate * 2; + const DWORD flags = AUDCLNT_STREAMFLAGS_EVENTCALLBACK | AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; + hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED, flags, MIC_BUFFER_DURATION, 0, &format, nullptr); + } + if (SUCCEEDED(hr)) { + hr = client->SetEventHandle(audioEvent); + } + ComPtr capture; + if (SUCCEEDED(hr)) { + hr = client->GetService(IID_PPV_ARGS(&capture)); + } + if (SUCCEEDED(hr)) { + hr = client->Start(); + } + const std::string name = GetFriendlyName(device.Get()); + if (FAILED(hr)) { + if (logErrors) { + ERROR_LOG(Log::Audio, "Could not start microphone '%s' (HRESULT: %08lx)", name.c_str(), hr); + } + return CaptureResult::FAILED; + } + INFO_LOG(Log::Audio, "Microphone '%s' started at %d Hz", name.c_str(), sampleRate); + + const HANDLE events[3] = { g_stopEvent, g_reopenEvent, audioEvent }; + CaptureResult captureResult = CaptureResult::FAILED; + while (true) { + const DWORD result = WaitForMultipleObjects(3, events, FALSE, INFINITE); + if (result == WAIT_OBJECT_0) { + captureResult = CaptureResult::STOPPED; + break; + } else if (result == WAIT_OBJECT_0 + 1) { + captureResult = CaptureResult::REOPEN; + break; + } else if (result != WAIT_OBJECT_0 + 2) { + ERROR_LOG(Log::Audio, "Microphone wait failed: %08lx", GetLastError()); + break; + } + + UINT32 packetFrames = 0; + while (SUCCEEDED(hr = capture->GetNextPacketSize(&packetFrames)) && packetFrames != 0) { + BYTE *data = nullptr; + UINT32 frames = 0; + DWORD bufferFlags = 0; + hr = capture->GetBuffer(&data, &frames, &bufferFlags, nullptr, nullptr); + if (FAILED(hr)) { + break; + } + PushSamples((bufferFlags & AUDCLNT_BUFFERFLAGS_SILENT) ? nullptr : (const int16_t *)data, frames); + capture->ReleaseBuffer(frames); + } + if (FAILED(hr)) { + // Most likely AUDCLNT_E_DEVICE_INVALIDATED, the device was unplugged or disabled. + WARN_LOG(Log::Audio, "Lost microphone '%s' (HRESULT: %08lx)", name.c_str(), hr); + break; + } + } + + client->Stop(); + return captureResult; +} + +static void CaptureThread(int sampleRate) { + SetCurrentThreadName("Microphone"); + const HRESULT coHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + + HANDLE audioEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + ComPtr enumerator; + CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator)); + if (audioEvent && enumerator) { + bool logErrors = true; + while (true) { + const CaptureResult result = RunCapture(enumerator.Get(), sampleRate, audioEvent, logErrors); + if (result == CaptureResult::STOPPED) { + break; + } + logErrors = true; + if (result == CaptureResult::FAILED) { + // Check again for a device every so often, until we're stopped. No need to log each time. + const HANDLE events[2] = { g_stopEvent, g_reopenEvent }; + const DWORD waitResult = WaitForMultipleObjects(2, events, FALSE, 1000); + if (waitResult == WAIT_OBJECT_0) { + break; + } + logErrors = waitResult == WAIT_OBJECT_0 + 1; + } + } + } else { + ERROR_LOG(Log::Audio, "Could not set up the microphone"); + } + + enumerator.Reset(); + if (audioEvent) { + CloseHandle(audioEvent); + } + if (SUCCEEDED(coHr)) { + CoUninitialize(); + } +} + +void WASAPIMicrophoneStart(int sampleRate) { + WASAPIMicrophoneStop(); + + if (!g_stopEvent) { + g_stopEvent = CreateEvent(nullptr, TRUE, FALSE, nullptr); + g_reopenEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr); + } + ResetEvent(g_stopEvent); + ResetEvent(g_reopenEvent); + + // Whatever is left over is from before, maybe at another rate. We're the consumer, so we can drop it. + g_micReadPos.store(g_micWritePos.load(std::memory_order_acquire), std::memory_order_release); + + g_micThread = std::thread(&CaptureThread, sampleRate); +} + +void WASAPIMicrophoneStop() { + if (!g_micThread.joinable()) { + return; + } + SetEvent(g_stopEvent); + g_micThread.join(); + INFO_LOG(Log::Audio, "Microphone stopped"); +} + +void WASAPIMicrophonePoll() { + const uint32_t read = g_micReadPos.load(std::memory_order_relaxed); + const uint32_t write = g_micWritePos.load(std::memory_order_acquire); + const uint32_t count = write - read; + if (count == 0) { + return; + } + // In up to two pieces, if it wraps around the end of the ring. + const uint32_t start = read & (MIC_RING_SIZE - 1); + const uint32_t first = std::min(count, MIC_RING_SIZE - start); + Microphone::addAudioData((u8 *)&g_micRing[start], first * sizeof(int16_t)); + if (count > first) { + Microphone::addAudioData((u8 *)g_micRing, (count - first) * sizeof(int16_t)); + } + g_micReadPos.store(read + count, std::memory_order_release); +} + +void WASAPIMicrophoneDeviceChanged() { + if (g_micThread.joinable()) { + SetEvent(g_reopenEvent); + } +} + +std::vector WASAPIMicrophoneGetDeviceList() { + std::vector list; + // Can be called from any thread. + const HRESULT coHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED); + ComPtr enumerator; + ComPtr collection; + UINT count = 0; + if (SUCCEEDED(CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator))) && + SUCCEEDED(enumerator->EnumAudioEndpoints(eCapture, DEVICE_STATE_ACTIVE, &collection)) && collection) { + collection->GetCount(&count); + } + for (UINT i = 0; i < count; i++) { + ComPtr device; + if (SUCCEEDED(collection->Item(i, &device)) && device) { + std::string name = GetFriendlyName(device.Get()); + if (!name.empty()) { + list.push_back(name); + } + } + } + collection.Reset(); + enumerator.Reset(); + if (SUCCEEDED(coHr)) { + CoUninitialize(); + } + return list; +} diff --git a/Windows/WASAPIMicrophone.h b/Windows/WASAPIMicrophone.h new file mode 100644 index 0000000000..7644cc4147 --- /dev/null +++ b/Windows/WASAPIMicrophone.h @@ -0,0 +1,33 @@ +// Copyright (c) 2012- PPSSPP Project. + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, version 2.0 or later versions. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License 2.0 for more details. + +// A copy of the GPL 2.0 should have been included with the program. +// If not, see http://www.gnu.org/licenses/ + +// Official git repository and contact information can be found at +// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/. + +#pragma once + +#include +#include + +// Microphone input for sceUsbMic, through WASAPI. A capture thread buffers the input, and +// WASAPIMicrophonePoll hands it over to Microphone::addAudioData. Start, stop and poll are called +// on the CPU thread. +void WASAPIMicrophoneStart(int sampleRate); +void WASAPIMicrophoneStop(); +void WASAPIMicrophonePoll(); +// Reopens the device, picking up a changed g_Config.sMicDevice. +void WASAPIMicrophoneDeviceChanged(); + +// Friendly names of the active capture devices, which is what g_Config.sMicDevice holds. +std::vector WASAPIMicrophoneGetDeviceList(); diff --git a/Windows/main.cpp b/Windows/main.cpp index cca22423a8..8ff1fc464b 100644 --- a/Windows/main.cpp +++ b/Windows/main.cpp @@ -71,6 +71,7 @@ #include "Windows/InputDevice.h" #include "Windows/MainWindow.h" #include "Windows/CaptureDevice.h" +#include "Windows/WASAPIMicrophone.h" #include "Windows/Debugger/Debugger_Disasm.h" #include "Windows/Debugger/Debugger_MemoryDlg.h" #include "Windows/Debugger/Debugger_VFPUDlg.h" @@ -316,6 +317,8 @@ std::vector System_GetPropertyStringVec(SystemProperty prop) { result.push_back(getenv("TEMP")); return result; } + case SYSPROP_MICROPHONE_DEVICE_LIST: + return WASAPIMicrophoneGetDeviceList(); default: return result; @@ -457,6 +460,8 @@ bool System_GetPropertyBool(SystemProperty prop) { return true; case SYSPROP_HAS_BACK_BUTTON: return true; + case SYSPROP_MICROPHONE_NEEDS_POLLING: + return true; case SYSPROP_HAS_LOGIN_DIALOG: return true; case SYSPROP_APP_GOLD: @@ -832,6 +837,24 @@ bool System_MakeRequest(SystemRequestType type, int requestId, const std::string W32Util::OpenDisplaySettings(); return true; } + case SystemRequestType::MICROPHONE_COMMAND: + // From the CPU thread. + if (startsWith(param1, "startRecording:")) { + int sampleRate = 0; + if (sscanf(param1.c_str(), "startRecording:%d", &sampleRate) != 1 || sampleRate <= 0) { + return false; + } + WASAPIMicrophoneStart(sampleRate); + } else if (param1 == "stopRecording") { + WASAPIMicrophoneStop(); + } else if (param1 == "pollRecording") { + WASAPIMicrophonePoll(); + } else if (param1 == "deviceChanged") { + WASAPIMicrophoneDeviceChanged(); + } else { + return false; + } + return true; default: return false; } @@ -1320,6 +1343,8 @@ int WINAPI WinMain(HINSTANCE _hInstance, HINSTANCE hPrevInstance, LPSTR szCmdLin // It's safe to call NativeShutdown, we've joined the main thread. NativeShutdown(); + // The game shut down the microphone with the core, but its thread mustn't outlive us either way. + WASAPIMicrophoneStop(); g_VFS.Clear();