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();