mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 15:00:30 +02:00
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) <noreply@anthropic.com>
This commit is contained in:
1 parent
5071ffa22d
commit
3656fb46e2
12 files changed
+376
-279
No files matched your search
@@ -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
|
||||
|
||||
+3
-35
@@ -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<std::vector<u32> *>(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<std::string> 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<std::string>();
|
||||
}
|
||||
|
||||
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.
|
||||
|
||||
@@ -770,10 +770,8 @@ void GameSettingsScreen::CreateAudioSettings(UI::ViewGroup *audioSettings) {
|
||||
if (!micList.empty()) {
|
||||
audioSettings->Add(new ItemHeader(a->T("Microphone")));
|
||||
std::vector<std::string> 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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
+2
-199
@@ -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<u32> *micParam = static_cast<std::vector<u32>*>(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
|
||||
//
|
||||
|
||||
+1
-35
@@ -27,13 +27,12 @@
|
||||
#include <thread>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#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 <class T> 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();
|
||||
@@ -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:
|
||||
|
||||
@@ -666,6 +666,7 @@
|
||||
</ClCompile>
|
||||
<ClCompile Include="W32Util\UAHMenuBar.cpp" />
|
||||
<ClCompile Include="WASAPIContext.cpp" />
|
||||
<ClCompile Include="WASAPIMicrophone.cpp" />
|
||||
<ClCompile Include="WindowsAudio.cpp" />
|
||||
<ClCompile Include="main.cpp" />
|
||||
<ClCompile Include="stdafx.cpp">
|
||||
@@ -1035,6 +1036,7 @@
|
||||
<ClInclude Include="GPU\WindowsGLContext.h" />
|
||||
<ClInclude Include="W32Util\UAHMenuBar.h" />
|
||||
<ClInclude Include="WASAPIContext.h" />
|
||||
<ClInclude Include="WASAPIMicrophone.h" />
|
||||
<ClInclude Include="WindowsAudio.h" />
|
||||
<ClInclude Include="main.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -178,6 +178,9 @@
|
||||
<ClCompile Include="WASAPIContext.cpp">
|
||||
<Filter>Windows\System</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="WASAPIMicrophone.cpp">
|
||||
<Filter>Windows\System</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="CaptureDevice.cpp">
|
||||
<Filter>Windows\Input</Filter>
|
||||
</ClCompile>
|
||||
@@ -372,6 +375,9 @@
|
||||
<ClInclude Include="WASAPIContext.h">
|
||||
<Filter>Windows\System</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="WASAPIMicrophone.h">
|
||||
<Filter>Windows\System</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="..\ppsspp_config.h" />
|
||||
<ClInclude Include="CaptureDevice.h">
|
||||
<Filter>Windows\Input</Filter>
|
||||
|
||||
@@ -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 <windows.h>
|
||||
// EndpointFormFactor has an unscoped Microphone, which clashes with our namespace.
|
||||
#define Microphone EndpointFormFactor_Microphone
|
||||
#include <mmdeviceapi.h>
|
||||
#undef Microphone
|
||||
#include <functiondiscoverykeys_devpkey.h>
|
||||
#include <audioclient.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
#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<uint32_t> g_micWritePos{0};
|
||||
static std::atomic<uint32_t> 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<IPropertyStore> 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<IMMDevice> FindDevice(IMMDeviceEnumerator *enumerator) {
|
||||
ComPtr<IMMDevice> device;
|
||||
const std::string wanted = g_Config.sMicDevice;
|
||||
if (!wanted.empty()) {
|
||||
ComPtr<IMMDeviceCollection> 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<IMMDevice> 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<IMMDevice> device = FindDevice(enumerator);
|
||||
if (!device) {
|
||||
if (logErrors) {
|
||||
WARN_LOG(Log::Audio, "No microphone, the game will get silence");
|
||||
}
|
||||
return CaptureResult::FAILED;
|
||||
}
|
||||
|
||||
ComPtr<IAudioClient> 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<IAudioCaptureClient> 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<IMMDeviceEnumerator> 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<std::string> WASAPIMicrophoneGetDeviceList() {
|
||||
std::vector<std::string> list;
|
||||
// Can be called from any thread.
|
||||
const HRESULT coHr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
||||
ComPtr<IMMDeviceEnumerator> enumerator;
|
||||
ComPtr<IMMDeviceCollection> 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<IMMDevice> 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;
|
||||
}
|
||||
@@ -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 <string>
|
||||
#include <vector>
|
||||
|
||||
// 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<std::string> WASAPIMicrophoneGetDeviceList();
|
||||
@@ -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<std::string> 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();
|
||||
|
||||
|
||||
Reference in new issue
Block a user