mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-14 15:00:30 +02:00
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>
303 lines
10 KiB
C++
303 lines
10 KiB
C++
// 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;
|
|
}
|