Files
ppsspp/Windows/WASAPIContext.cpp
Henrik Rydgård 971b5eca13 WASAPI: Use device's suggested format when available
When IsFormatSupported returns S_FALSE with a closestMatch format,
evaluate it and use it if it's acceptable (stereo float). This can
improve compatibility with devices that don't support our exact
requested format but have a similar one.

Previously we just discarded closestMatch and forced manual conversion,
which was wasteful when the device provided a perfectly usable
alternative format.
2026-08-04 00:37:55 +02:00

929 lines
30 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include <windows.h>
#include <mmdeviceapi.h>
#include <functiondiscoverykeys_devpkey.h>
#include <audioclient.h>
#include <avrt.h>
#include <comdef.h>
#include <atomic>
#include <thread>
#include <vector>
#include <string_view>
#include <wrl/client.h>
#include "Common/Data/Encoding/Utf8.h"
#include "Common/Log.h"
#include "Common/StringUtils.h"
#include "Common/Thread/ThreadUtil.h"
#include "WASAPIContext.h"
using Microsoft::WRL::ComPtr;
// Channel upmixing attenuation factors
static constexpr float SURROUND_ATTENUATION = 0.7f; // For rear/side channels
static constexpr float CENTER_MIX_ATTENUATION = 0.7f; // For center channel mix
static constexpr float LFE_MIX_ATTENUATION = 0.5f; // For LFE channel mix
// Helper to determine channel positions from WAVEFORMATEXTENSIBLE
struct ChannelMapping {
int frontLeft = -1;
int frontRight = -1;
int center = -1;
int lfe = -1;
int rearLeft = -1;
int rearRight = -1;
};
static ChannelMapping GetChannelMapping(const WAVEFORMATEX *format) {
ChannelMapping mapping;
if (!format) {
return mapping;
}
// For non-extensible formats, assume standard stereo/mono
if (format->wFormatTag != WAVE_FORMAT_EXTENSIBLE) {
if (format->nChannels >= 1) mapping.frontLeft = 0;
if (format->nChannels >= 2) mapping.frontRight = 1;
return mapping;
}
const WAVEFORMATEXTENSIBLE *formatEx = reinterpret_cast<const WAVEFORMATEXTENSIBLE*>(format);
const DWORD mask = formatEx->dwChannelMask;
// Build mapping based on channel mask
int channelIndex = 0;
// Front speakers
if (mask & SPEAKER_FRONT_LEFT) mapping.frontLeft = channelIndex++;
if (mask & SPEAKER_FRONT_RIGHT) mapping.frontRight = channelIndex++;
if (mask & SPEAKER_FRONT_CENTER) mapping.center = channelIndex++;
if (mask & SPEAKER_LOW_FREQUENCY) mapping.lfe = channelIndex++;
// Rear/back speakers
if (mask & SPEAKER_BACK_LEFT) mapping.rearLeft = channelIndex++;
if (mask & SPEAKER_BACK_RIGHT) mapping.rearRight = channelIndex++;
// Side speakers (if no back, use these as rear)
if (mapping.rearLeft == -1 && (mask & SPEAKER_SIDE_LEFT)) mapping.rearLeft = channelIndex++;
if (mapping.rearRight == -1 && (mask & SPEAKER_SIDE_RIGHT)) mapping.rearRight = channelIndex++;
// Note: We skip other speaker positions (top, front center of height, etc.)
// as they're rarely used and we're just doing stereo upmix anyway
return mapping;
}
// We must have one of these already...
static inline s16 ClampFloatToS16(float f) {
f *= 32768.0f;
if (f >= 32767) {
return 32767;
} else if (f < -32768) {
return -32768;
} else {
return (s16)(s32)f;
}
}
static const char *GetAudioClientErrorName(HRESULT hr) {
switch (hr) {
case AUDCLNT_E_UNSUPPORTED_FORMAT:
return "AUDCLNT_E_UNSUPPORTED_FORMAT";
case AUDCLNT_E_DEVICE_INVALIDATED:
return "AUDCLNT_E_DEVICE_INVALIDATED";
case AUDCLNT_E_DEVICE_IN_USE:
return "AUDCLNT_E_DEVICE_IN_USE";
case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED:
return "AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED";
case AUDCLNT_E_BUFFER_SIZE_ERROR:
return "AUDCLNT_E_BUFFER_SIZE_ERROR";
case E_INVALIDARG:
return "E_INVALIDARG";
case E_POINTER:
return "E_POINTER";
case E_OUTOFMEMORY:
return "E_OUTOFMEMORY";
default:
return nullptr;
}
}
void BuildStereoFloatFormat(const WAVEFORMATEXTENSIBLE *original, WAVEFORMATEXTENSIBLE *output) {
// Zeroinit all fields first.
ZeroMemory(output, sizeof(WAVEFORMATEXTENSIBLE));
// Fill the WAVEFORMATEX base part.
output->Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
output->Format.nChannels = 2;
output->Format.nSamplesPerSec = original->Format.nSamplesPerSec;
output->Format.wBitsPerSample = 32; // 32bit float
output->Format.nBlockAlign = output->Format.nChannels *
output->Format.wBitsPerSample / 8;
output->Format.nAvgBytesPerSec = output->Format.nSamplesPerSec *
output->Format.nBlockAlign;
output->Format.cbSize = sizeof(WAVEFORMATEXTENSIBLE) - sizeof(WAVEFORMATEX);
// Fill the extensible fields.
output->Samples.wValidBitsPerSample = 32;
output->dwChannelMask = SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT;
output->SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
}
WASAPIContext::WASAPIContext() : notificationClient_(this) {
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_ALL, IID_PPV_ARGS(&enumerator_));
if (FAILED(hr)) {
// Bad!
enumerator_ = nullptr;
return;
}
hr = enumerator_->RegisterEndpointNotificationCallback(&notificationClient_);
if (FAILED(hr)) {
WARN_LOG(Log::Audio, "Failed to register endpoint notification callback: %08lx", hr);
}
}
WASAPIContext::~WASAPIContext() {
if (!enumerator_) {
// Nothing can have been happening.
return;
}
Stop();
enumerator_->UnregisterEndpointNotificationCallback(&notificationClient_);
}
WASAPIContext::AudioFormat WASAPIContext::Classify(const WAVEFORMATEX *format) {
if (format->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
const WAVEFORMATEXTENSIBLE *ex = (const WAVEFORMATEXTENSIBLE *)format;
if (ex->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
if (format->nChannels >= 1)
return AudioFormat::Float;
} else {
wchar_t guid[256]{};
StringFromGUID2(ex->SubFormat, guid, 256);
ERROR_LOG(Log::Audio, "Got unexpected WASAPI 0xFFFE stream format (%S), expected float!", guid);
if (ex->Format.wBitsPerSample == 16 && format->nChannels >= 1) {
INFO_LOG(Log::Audio, "Got a PCM16 audio output (%d channels)", format->nChannels);
return AudioFormat::PCM16;
}
}
} else if (format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT && format->nChannels >= 1) {
return AudioFormat::Float;
} else if (format->wFormatTag == WAVE_FORMAT_PCM && format->wBitsPerSample == 16 && format->nChannels >= 1) {
INFO_LOG(Log::Audio, "Got a PCM16 audio output", format->nChannels);
return AudioFormat::PCM16;
} else {
WARN_LOG(Log::Audio, "Unhandled output format!");
}
return AudioFormat::Unhandled;
}
bool GetDeviceDesc(IMMDevice *device, AudioDeviceDesc *desc) {
ComPtr<IPropertyStore> props;
HRESULT hr = device->OpenPropertyStore(STGM_READ, &props);
if (FAILED(hr) || !props) {
return false;
}
PROPVARIANT nameProp;
PropVariantInit(&nameProp);
hr = props->GetValue(PKEY_Device_FriendlyName, &nameProp);
LPWSTR id_str = 0;
bool success = false;
if (SUCCEEDED(device->GetId(&id_str))) {
// Only use the name if GetValue succeeded, otherwise use empty string
if (SUCCEEDED(hr) && nameProp.pwszVal) {
desc->name = ConvertWStringToUTF8(nameProp.pwszVal);
} else {
desc->name = "(Unknown device)";
}
desc->uniqueId = ConvertWStringToUTF8(id_str);
CoTaskMemFree(id_str);
success = true;
}
PropVariantClear(&nameProp);
return success;
}
void WASAPIContext::EnumerateDevices(std::vector<AudioDeviceDesc> *output, bool captureDevices) {
ComPtr<IMMDeviceCollection> collection;
enumerator_->EnumAudioEndpoints(captureDevices ? eCapture : eRender, DEVICE_STATE_ACTIVE, &collection);
if (!collection) {
ERROR_LOG(Log::Audio, "Failed to enumerate devices");
return;
}
UINT count = 0;
collection->GetCount(&count);
for (UINT i = 0; i < count; ++i) {
ComPtr<IMMDevice> device;
if (FAILED(collection->Item(i, &device)) || !device) {
continue;
}
AudioDeviceDesc desc{};
if (GetDeviceDesc(device.Get(), &desc)) {
output->push_back(desc);
}
}
}
// Also logs.
void WASAPIContext::SetErrorString(std::string_view str, HRESULT hr) {
std::string temp = StringFromFormat("%s (HRESULT: %08lx)", str.data(), hr);
ERROR_LOG(Log::Audio, "%s", temp.c_str());
std::lock_guard<std::mutex> guard(errorLock_);
errorString_ = temp;
}
void WASAPIContext::ClearErrorString() {
std::lock_guard<std::mutex> guard(errorLock_);
errorString_.clear();
}
bool WASAPIContext::TryInitAudioClient3(IMMDevice *device, LatencyMode latencyMode) {
HRESULT hr = E_FAIL;
// Try IAudioClient3 first if not in "safe" mode. It's probably safe anyway, but still, let's use the legacy client as a safe fallback option.
if (latencyMode != LatencyMode::Safe) {
hr = device->Activate(__uuidof(IAudioClient3), CLSCTX_ALL, nullptr, (void**)&audioClient3_);
} else {
// Proceed to AudioClient.
INFO_LOG(Log::Audio, "LatencyMode::Safe is set, skipping AudioClient3 and going directly to AudioClient");
return false;
}
if (!SUCCEEDED(hr)) {
audioClient3_.Reset();
return false;
}
hr = audioClient3_->GetMixFormat(&format_);
if (FAILED(hr)) {
audioClient3_.Reset();
SetErrorString("AudioClient3 GetMixFormat failed", hr);
return false;
}
curSamplesPerSec_ = format_->nSamplesPerSec;
curChannels_.store(format_->nChannels);
// AudioClient3 requires an exact format match because it doesn't support AUTOCONVERTPCM.
// Our callback always produces stereo float (see RenderCallback in AudioBackend.h),
// so we can only use AudioClient3 when the device's native format is stereo float.
// For other formats, we fall back to AudioClient which supports conversion via AUTOCONVERTPCM
// or manual conversion through tempBuf_.
if (curChannels_.load() != 2 || Classify(format_) != AudioFormat::Float) {
INFO_LOG(Log::Audio, "AudioClient3: Got %d channels or non-float format, falling back to AudioClient", curChannels_.load());
audioClient3_.Reset();
// Free the format before falling through - AudioClient will allocate a new one
CoTaskMemFree(format_);
format_ = nullptr;
return false;
} else {
hr = audioClient3_->GetSharedModeEnginePeriod(format_, &defaultPeriodFrames_, &fundamentalPeriodFrames_, &minPeriodFrames_, &maxPeriodFrames_);
if (FAILED(hr)) {
audioClient3_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient3 GetSharedModeEnginePeriod failed", hr);
return false;
}
INFO_LOG(Log::Audio, "AudioClient3: default: %d fundamental: %d min: %d max: %d\n", (int)defaultPeriodFrames_, (int)fundamentalPeriodFrames_, (int)minPeriodFrames_, (int)maxPeriodFrames_);
INFO_LOG(Log::Audio, "initializing with %d frame period at %d Hz, meaning %0.1fms\n", (int)minPeriodFrames_, (int)format_->nSamplesPerSec, FramesToMs(minPeriodFrames_, format_->nSamplesPerSec));
hr = audioClient3_->InitializeSharedAudioStream(
AUDCLNT_STREAMFLAGS_EVENTCALLBACK,
minPeriodFrames_,
format_,
nullptr
);
if (FAILED(hr)) {
WARN_LOG(Log::Audio, "Error initializing AudioClient3 shared audio stream: %08lx", hr);
audioClient3_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient3 init failed", hr);
return false;
}
actualPeriodFrames_ = minPeriodFrames_;
UINT32 bufSize = 0;
hr = audioClient3_->GetBufferSize(&bufSize);
reportedBufferSize_.store(bufSize);
if (FAILED(hr)) {
audioClient3_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient3 GetBufferSize failed", hr);
return false;
}
hr = audioClient3_->SetEventHandle(audioEvent_);
if (FAILED(hr)) {
audioClient3_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient3 SetEventHandle failed", hr);
return false;
}
hr = audioClient3_->GetService(IID_PPV_ARGS(&renderClient_));
if (FAILED(hr)) {
audioClient3_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient3 GetService failed", hr);
return false;
}
}
return true;
}
bool WASAPIContext::TryInitAudioClient(IMMDevice *device, LatencyMode latencyMode) {
// Fallback to IAudioClient (older OS)
HRESULT hr = device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&audioClient_);
if (FAILED(hr)) {
SetErrorString("Failed to activate audio device", hr);
return false;
}
hr = audioClient_->GetMixFormat(&format_);
if (FAILED(hr)) {
audioClient_.Reset();
SetErrorString("AudioClient GetMixFormat failed", hr);
return false;
}
// If there are too many channels, try asking for a 2-channel output format.
DWORD extraStreamFlags = 0;
const AudioFormat fmt = Classify(format_);
curSamplesPerSec_ = format_->nSamplesPerSec;
curChannels_.store(format_->nChannels);
bool createBuffer = false;
if (fmt == AudioFormat::Float) {
if (curChannels_.load() != 2) {
INFO_LOG(Log::Audio, "Got %d channels, asking for stereo instead", curChannels_.load());
WAVEFORMATEXTENSIBLE stereo;
BuildStereoFloatFormat((const WAVEFORMATEXTENSIBLE *)format_, &stereo);
WAVEFORMATEX *closestMatch = nullptr;
const HRESULT result = audioClient_->IsFormatSupported(AUDCLNT_SHAREMODE_SHARED, (const WAVEFORMATEX *)&stereo, &closestMatch);
if (result == S_OK) {
// We got the format! Use it and set as current.
_dbg_assert_(!closestMatch);
WAVEFORMATEX *newFormat = (WAVEFORMATEX *)CoTaskMemAlloc(sizeof(WAVEFORMATEXTENSIBLE));
_dbg_assert_(newFormat);
memcpy(newFormat, &stereo, sizeof(WAVEFORMATEX) + stereo.Format.cbSize);
CoTaskMemFree(format_);
format_ = newFormat;
curChannels_.store(newFormat->nChannels);
extraStreamFlags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
INFO_LOG(Log::Audio, "Successfully asked for two channels");
} else if (result == S_FALSE) {
// The device suggests a closest match format
if (closestMatch) {
// Check if the closest match is acceptable (stereo float)
if (closestMatch->nChannels == 2 && Classify(closestMatch) == AudioFormat::Float) {
INFO_LOG(Log::Audio, "Using device's suggested format: %lu Hz, %d channels",
closestMatch->nSamplesPerSec, closestMatch->nChannels);
// Use the suggested format
CoTaskMemFree(format_);
format_ = closestMatch;
curChannels_.store(format_->nChannels);
curSamplesPerSec_ = format_->nSamplesPerSec;
extraStreamFlags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY;
} else {
// Closest match is not stereo float, fall back to manual conversion
WARN_LOG(Log::Audio, "Device suggested format (%d channels) isn't stereo float, using manual conversion",
closestMatch->nChannels);
CoTaskMemFree(closestMatch);
createBuffer = true;
}
} else {
WARN_LOG(Log::Audio, "IsFormatSupported returned S_FALSE but no closest match. Using workarounds.");
createBuffer = true;
}
} else {
// IsFormatSupported failed - log detailed error information
const char *errorName = GetAudioClientErrorName(result);
if (errorName) {
WARN_LOG(Log::Audio, "IsFormatSupported failed with %s (0x%08lx)", errorName, result);
} else {
WARN_LOG(Log::Audio, "IsFormatSupported failed with unknown error 0x%08lx", result);
}
// Log the format we tried to request for debugging
WARN_LOG(Log::Audio, " Requested format: %d Hz, %d channels, %d-bit %s",
stereo.Format.nSamplesPerSec,
stereo.Format.nChannels,
stereo.Format.wBitsPerSample,
"float");
// Log the device's native format
WARN_LOG(Log::Audio, " Device native format: %d Hz, %d channels",
format_->nSamplesPerSec,
format_->nChannels);
// Common causes based on error code
if (result == AUDCLNT_E_UNSUPPORTED_FORMAT) {
INFO_LOG(Log::Audio, " Device doesn't support our requested stereo float format.");
INFO_LOG(Log::Audio, " Will use manual conversion from stereo to %d-channel output.", format_->nChannels);
} else if (result == AUDCLNT_E_DEVICE_INVALIDATED) {
WARN_LOG(Log::Audio, " Audio device was removed or disabled. Audio may not work.");
} else if (result == AUDCLNT_E_DEVICE_IN_USE) {
WARN_LOG(Log::Audio, " Audio device is in exclusive use by another application.");
}
_dbg_assert_(!closestMatch);
createBuffer = true;
}
} else {
// All good, nothing to convert.
_dbg_assert_(format_);
}
} else {
// Some other format.
WARN_LOG(Log::Audio, "Format not float, applying conversion.");
createBuffer = true;
}
// Get engine period info
REFERENCE_TIME defaultPeriod = 0, minPeriod = 0;
hr = audioClient_->GetDevicePeriod(&defaultPeriod, &minPeriod);
if (FAILED(hr)) {
// Non-fatal, but log it. We'll use a default duration.
WARN_LOG(Log::Audio, "GetDevicePeriod failed: %08lx, using default period", hr);
minPeriod = 100000; // 10ms default
}
const REFERENCE_TIME duration = minPeriod;
hr = audioClient_->Initialize(
AUDCLNT_SHAREMODE_SHARED,
AUDCLNT_STREAMFLAGS_EVENTCALLBACK | extraStreamFlags,
duration, // This is a minimum, the result might be larger. We use GetBufferSize to check.
0, // ref duration, always 0 in shared mode.
format_,
nullptr
);
if (FAILED(hr)) {
audioClient_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient init failed", hr);
return false;
}
UINT32 bufSize = 0;
hr = audioClient_->GetBufferSize(&bufSize);
reportedBufferSize_.store(bufSize);
if (FAILED(hr)) {
audioClient_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient GetBufferSize failed", hr);
return false;
}
actualPeriodFrames_.store(reportedBufferSize_.load()); // we don't have a better estimate.
hr = audioClient_->SetEventHandle(audioEvent_);
if (FAILED(hr)) {
audioClient_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient SetEventHandle failed", hr);
return false;
}
hr = audioClient_->GetService(IID_PPV_ARGS(&renderClient_));
if (FAILED(hr)) {
audioClient_.Reset();
CoTaskMemFree(format_);
format_ = nullptr;
SetErrorString("AudioClient GetService failed", hr);
return false;
}
if (createBuffer) {
tempBuf_ = std::make_unique<float[]>(reportedBufferSize_.load() * 2);
}
return true;
}
bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault) {
Stop();
*revertedToDefault = false;
ComPtr<IMMDevice> device;
if (uniqueId.empty()) {
// Use the default device.
HRESULT hr = enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device);
if (FAILED(hr)) {
SetErrorString("Failed to get the default endpoint", hr);
return false;
}
} else {
// Use whatever device.
std::wstring wId = ConvertUTF8ToWString(uniqueId);
HRESULT hr = enumerator_->GetDevice(wId.c_str(), &device);
if (FAILED(hr)) {
// Fallback to default device
INFO_LOG(Log::Audio, "Falling back to default device...\n");
*revertedToDefault = true;
hr = enumerator_->GetDefaultAudioEndpoint(eRender, eConsole, &device);
if (FAILED(hr)) {
SetErrorString("Failed to fallback", hr);
return false;
}
}
}
AudioDeviceDesc desc{};
GetDeviceDesc(device.Get(), &desc);
INFO_LOG(Log::Audio, "Activating audio device: %s : %s", desc.name.c_str(), desc.uniqueId.c_str());
{
std::lock_guard<std::mutex> guard(deviceLock_);
curDeviceId_ = desc.uniqueId;
curDeviceName_ = desc.name;
}
// Get rid of any old tempBuf_.
tempBuf_.reset();
// This is used by both paths.
audioEvent_ = CreateEvent(nullptr, FALSE, FALSE, nullptr);
if (!TryInitAudioClient3(device.Get(), latencyMode)) {
if (!TryInitAudioClient(device.Get(), latencyMode)) {
// Failed both client types.
CloseHandle(audioEvent_);
audioEvent_ = nullptr;
return false;
}
}
latencyMode_ = latencyMode;
_dbg_assert_(audioClient_ || audioClient3_);
Start();
return true;
}
void WASAPIContext::Start() {
if (audioThread_.joinable()) {
_dbg_assert_(false);
ERROR_LOG(Log::Audio, "Audio thread already running!");
return;
}
running_ = true;
audioThread_ = std::thread([this]() { AudioLoop(); });
}
void WASAPIContext::Stop() {
running_ = false;
if (audioEvent_) SetEvent(audioEvent_);
// Stop is actually called on the audioclient in the thread, while exiting.
if (audioThread_.joinable()) audioThread_.join();
renderClient_.Reset();
audioClient_.Reset();
audioClient3_.Reset();
if (audioEvent_) {
CloseHandle(audioEvent_);
audioEvent_ = nullptr;
}
if (format_) {
CoTaskMemFree(format_);
format_ = nullptr;
}
curChannels_.store(0);
{
std::lock_guard<std::mutex> guard(deviceLock_);
curDeviceId_.clear();
curDeviceName_.clear();
}
}
void WASAPIContext::FrameUpdate(bool allowAutoChange) {
std::string deviceIdToInit;
{
std::lock_guard<std::mutex> guard(deviceLock_);
if (!defaultDeviceChanged_) {
return;
}
if (allowAutoChange) {
// Check if there actually was a change, we ignore false positives.
{
if (newDeviceId_ == curDeviceId_) {
// False positive, ignore.
defaultDeviceChanged_ = false;
return;
}
deviceIdToInit = newDeviceId_;
newDeviceId_.clear();
}
defaultDeviceChanged_ = false;
}
}
bool reverted;
InitOutputDevice(deviceIdToInit, latencyMode_, &reverted);
}
void WASAPIContext::AudioLoop() {
SetCurrentThreadName("WASAPIAudioLoop");
DWORD taskID = 0;
HANDLE mmcssHandle = nullptr;
if (latencyMode_ == LatencyMode::Aggressive) {
mmcssHandle = AvSetMmThreadCharacteristics(L"Pro Audio", &taskID);
}
UINT32 available;
HRESULT hr;
if (audioClient3_) {
hr = audioClient3_->Start();
if (FAILED(hr)) {
SetErrorString("AudioClient3::Start failed", hr);
return;
}
hr = audioClient3_->GetBufferSize(&available);
if (FAILED(hr)) {
SetErrorString("AudioClient3::GetBufferSize failed", hr);
audioClient3_->Stop();
return;
}
// Check if buffer grew beyond what we allocated tempBuf_ for
if (tempBuf_ && available > reportedBufferSize_.load()) {
INFO_LOG(Log::Audio, "Buffer size grew from %d to %d, reallocating tempBuf_", reportedBufferSize_.load(), available);
tempBuf_ = std::make_unique<float[]>(available * 2);
reportedBufferSize_.store(available);
}
} else if (audioClient_) {
hr = audioClient_->Start();
if (FAILED(hr)) {
SetErrorString("AudioClient::Start failed", hr);
return;
}
hr = audioClient_->GetBufferSize(&available);
if (FAILED(hr)) {
SetErrorString("AudioClient::GetBufferSize failed", hr);
audioClient_->Stop();
return;
}
// Check if buffer grew beyond what we allocated tempBuf_ for
if (tempBuf_ && available > reportedBufferSize_.load()) {
INFO_LOG(Log::Audio, "Buffer size grew from %d to %d, reallocating tempBuf_", reportedBufferSize_.load(), available);
tempBuf_ = std::make_unique<float[]>(available * 2);
reportedBufferSize_.store(available);
}
} else {
// No audio client, nothing to do.
SetErrorString("No audio client in AudioLoop", 0);
return;
}
if (!format_) {
ERROR_LOG(Log::Audio, "Can't start audio - no format");
return;
}
const AudioFormat format = Classify(format_);
const int nChannels = curChannels_.load();
const ChannelMapping channelMap = GetChannelMapping(format_);
ClearErrorString();
while (running_) {
const DWORD waitResult = WaitForSingleObject(audioEvent_, INFINITE);
if (waitResult != WAIT_OBJECT_0) {
// Something bad happened.
break;
}
UINT32 padding = 0;
if (audioClient3_) {
hr = audioClient3_->GetCurrentPadding(&padding);
if (FAILED(hr)) {
WARN_LOG(Log::Audio, "AudioClient3::GetCurrentPadding failed: %08lx", hr);
continue;
}
} else {
hr = audioClient_->GetCurrentPadding(&padding);
if (FAILED(hr)) {
WARN_LOG(Log::Audio, "AudioClient::GetCurrentPadding failed: %08lx", hr);
continue;
}
}
// Calculate frames to write, checking for underflow
UINT32 framesToWrite = 0;
if (padding < available) {
framesToWrite = available - padding;
} else if (padding > available) {
// This shouldn't happen, but log it if it does
WARN_LOG(Log::Audio, "Padding (%d) exceeds available (%d), skipping frame", padding, available);
}
// Safety: clamp framesToWrite to tempBuf_ capacity if using conversion path
const UINT32 bufCapacity = reportedBufferSize_.load();
if (tempBuf_ && framesToWrite > bufCapacity) {
WARN_LOG(Log::Audio, "framesToWrite (%d) exceeds buffer capacity (%d), clamping", framesToWrite, bufCapacity);
framesToWrite = bufCapacity;
}
BYTE* buffer = nullptr;
if (framesToWrite > 0 && SUCCEEDED(renderClient_->GetBuffer(framesToWrite, &buffer))) {
if (!callback_) {
// No callback set, fill with silence
if (buffer) {
memset(buffer, 0, framesToWrite * format_->nChannels * (format_->wBitsPerSample / 8));
}
} else if (!tempBuf_) {
// Mix directly to the output buffer, avoiding a copy.
if (buffer) {
callback_(reinterpret_cast<float *>(buffer), framesToWrite, format_->nSamplesPerSec, userdata_);
}
} else {
// We decided previously that we need conversion, so mix to our temp buffer...
callback_(tempBuf_.get(), framesToWrite, format_->nSamplesPerSec, userdata_);
// .. and convert according to format (we support multi-channel float and s16)
// Use the channel mapping to place audio in the correct channels
if (format == AudioFormat::PCM16 && buffer) {
// Need to convert.
s16 *dest = reinterpret_cast<s16 *>(buffer);
for (UINT32 i = 0; i < framesToWrite; i++) {
// Zero the entire frame first
for (int j = 0; j < nChannels; j++) {
dest[i * nChannels + j] = 0;
}
// Map stereo input to appropriate output channels
const float left = tempBuf_[i * 2];
const float right = tempBuf_[i * 2 + 1];
if (nChannels == 1) {
// Mono: mixdown stereo to mono
dest[i] = ClampFloatToS16((left + right) * 0.5f);
} else if (nChannels == 2) {
// Stereo: direct copy
dest[i * 2] = ClampFloatToS16(left);
dest[i * 2 + 1] = ClampFloatToS16(right);
} else {
// Multi-channel: use channel mapping
if (channelMap.frontLeft >= 0) {
dest[i * nChannels + channelMap.frontLeft] = ClampFloatToS16(left);
}
if (channelMap.frontRight >= 0) {
dest[i * nChannels + channelMap.frontRight] = ClampFloatToS16(right);
}
if (channelMap.center >= 0) {
dest[i * nChannels + channelMap.center] = ClampFloatToS16((left + right) * 0.5f * CENTER_MIX_ATTENUATION);
}
if (channelMap.lfe >= 0) {
dest[i * nChannels + channelMap.lfe] = ClampFloatToS16((left + right) * 0.5f * LFE_MIX_ATTENUATION);
}
if (channelMap.rearLeft >= 0) {
dest[i * nChannels + channelMap.rearLeft] = ClampFloatToS16(left * SURROUND_ATTENUATION);
}
if (channelMap.rearRight >= 0) {
dest[i * nChannels + channelMap.rearRight] = ClampFloatToS16(right * SURROUND_ATTENUATION);
}
}
}
} else if (format == AudioFormat::Float && buffer) {
// We have a non-2 number of channels (since we're in the tempBuf_ 'if'), so we contract/expand.
float *dest = reinterpret_cast<float *>(buffer);
for (UINT32 i = 0; i < framesToWrite; i++) {
// Zero the entire frame first
for (int j = 0; j < nChannels; j++) {
dest[i * nChannels + j] = 0.0f;
}
// Map stereo input to appropriate output channels
const float left = tempBuf_[i * 2];
const float right = tempBuf_[i * 2 + 1];
if (nChannels == 1) {
// Mono: mixdown stereo to mono
dest[i] = (left + right) * 0.5f;
} else if (nChannels == 2) {
// Stereo: direct copy
dest[i * 2] = left;
dest[i * 2 + 1] = right;
} else {
// Multi-channel: use channel mapping
if (channelMap.frontLeft >= 0) {
dest[i * nChannels + channelMap.frontLeft] = left;
}
if (channelMap.frontRight >= 0) {
dest[i * nChannels + channelMap.frontRight] = right;
}
if (channelMap.center >= 0) {
dest[i * nChannels + channelMap.center] = (left + right) * 0.5f * CENTER_MIX_ATTENUATION;
}
if (channelMap.lfe >= 0) {
dest[i * nChannels + channelMap.lfe] = (left + right) * 0.5f * LFE_MIX_ATTENUATION;
}
if (channelMap.rearLeft >= 0) {
dest[i * nChannels + channelMap.rearLeft] = left * SURROUND_ATTENUATION;
}
if (channelMap.rearRight >= 0) {
dest[i * nChannels + channelMap.rearRight] = right * SURROUND_ATTENUATION;
}
}
}
}
}
hr = renderClient_->ReleaseBuffer(framesToWrite, 0);
if (FAILED(hr)) {
WARN_LOG(Log::Audio, "ReleaseBuffer failed: %08lx", hr);
}
// In the old mode, we just estimate the "actualPeriodFrames_" from the framesToWrite.
if (audioClient_ && framesToWrite < actualPeriodFrames_.load()) {
actualPeriodFrames_.store(framesToWrite);
}
}
}
if (audioClient3_) {
audioClient3_->Stop();
}
if (audioClient_) {
audioClient_->Stop();
}
if (mmcssHandle) {
AvRevertMmThreadCharacteristics(mmcssHandle);
}
}
void WASAPIContext::DescribeOutputFormat(char *buffer, size_t bufferSize) const {
if (!format_) {
snprintf(buffer, bufferSize, "No format");
return;
}
const int numChannels = format_->nChannels;
const int sampleBits = format_->wBitsPerSample;
const int sampleRateHz = format_->nSamplesPerSec;
const char *fmt = "N/A";
if (format_->wFormatTag == WAVE_FORMAT_EXTENSIBLE) {
const WAVEFORMATEXTENSIBLE *ex = (const WAVEFORMATEXTENSIBLE *)format_;
if (ex->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT) {
fmt = "float";
} else {
fmt = "PCM";
}
} else {
fmt = "PCM"; // probably
}
snprintf(buffer, bufferSize, "%d Hz %s %d-bit, %d ch%s", sampleRateHz, fmt, sampleBits, numChannels, audioClient3_ ? " (ac3)" : " (ac)");
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDefaultDeviceChanged(EDataFlow flow, ERole role, LPCWSTR device) {
if (flow != eRender) {
INFO_LOG(Log::Audio, "Default WASAPI audio recording device changed! Currently ignoring.");
return S_OK;
}
INFO_LOG(Log::Audio, "Default device changed to %s! role=%d", ConvertWStringToUTF8(device).c_str(), role);
if (role == eConsole) {
// PostMessage(hwnd, WM_APP + 1, 0, 0);
std::lock_guard<std::mutex> guard(engine_->deviceLock_);
engine_->defaultDeviceChanged_ = true;
engine_->newDeviceId_ = ConvertWStringToUTF8(device);
}
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceAdded(LPCWSTR device) {
INFO_LOG(Log::Audio, "Audio device added! device=%s", ConvertWStringToUTF8(device).c_str());
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceRemoved(LPCWSTR device) {
INFO_LOG(Log::Audio, "Audio device removed! device=%s", ConvertWStringToUTF8(device).c_str());
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnDeviceStateChanged(LPCWSTR device, DWORD state) {
INFO_LOG(Log::Audio, "Audio device state changed! device=%s state=%08x", ConvertWStringToUTF8(device).c_str(), state);
return S_OK;
}
HRESULT STDMETHODCALLTYPE WASAPIContext::DeviceNotificationClient::OnPropertyValueChanged(LPCWSTR device, const PROPERTYKEY key) {
return S_OK;
}