#include #include #include #include #include #include #include #include #include #include #include #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; // We must have one of these already... static inline s16 ClampFloatToS16(float f) { f *= 32768.0f; if (f >= 32767) { return 32767; } else if (f < -32767) { return -32767; } else { return (s16)(s32)f; } } void BuildStereoFloatFormat(const WAVEFORMATEXTENSIBLE *original, WAVEFORMATEXTENSIBLE *output) { // Zero‑init 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; // 32‑bit 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; } enumerator_->RegisterEndpointNotificationCallback(¬ificationClient_); } WASAPIContext::~WASAPIContext() { if (!enumerator_) { // Nothing can have been happening. return; } Stop(); enumerator_->UnregisterEndpointNotificationCallback(¬ificationClient_); } 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 props; device->OpenPropertyStore(STGM_READ, &props); PROPVARIANT nameProp; PropVariantInit(&nameProp); props->GetValue(PKEY_Device_FriendlyName, &nameProp); LPWSTR id_str = 0; bool success = false; if (SUCCEEDED(device->GetId(&id_str))) { desc->name = ConvertWStringToUTF8(nameProp.pwszVal); desc->uniqueId = ConvertWStringToUTF8(id_str); CoTaskMemFree(id_str); success = true; } PropVariantClear(&nameProp); return success; } void WASAPIContext::EnumerateDevices(std::vector *output, bool captureDevices) { ComPtr 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 device; collection->Item(i, &device); 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 guard(errorLock_); errorString_ = temp; } void WASAPIContext::ClearErrorString() { std::lock_guard 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; // We only use AudioClient3 if we got the format we wanted (stereo float). if (format_->nChannels != 2 || Classify(format_) != AudioFormat::Float) { // Let's fall back to the old path. The docs seem to be wrong, if you try to create an // AudioClient3 with low latency audio with AUTOCONVERTPCM, you get the error 0x88890021. INFO_LOG(Log::Audio, "AudioClient3: Got %d channels or non-float format, falling back to AudioClient", format_->nChannels); 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_; hr = audioClient3_->GetBufferSize(&reportedBufferSize_); 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; bool createBuffer = false; if (fmt == AudioFormat::Float) { if (format_->nChannels != 2) { INFO_LOG(Log::Audio, "Got %d channels, asking for stereo instead", format_->nChannels); 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; extraStreamFlags = AUDCLNT_STREAMFLAGS_AUTOCONVERTPCM | AUDCLNT_STREAMFLAGS_SRC_DEFAULT_QUALITY; INFO_LOG(Log::Audio, "Successfully asked for two channels"); } else if (result == S_FALSE) { // We got another format. Meh, let's just use what we got. if (closestMatch) { WARN_LOG(Log::Audio, "Didn't get the format we wanted, but got: %lu ch=%d", closestMatch->nSamplesPerSec, closestMatch->nChannels); CoTaskMemFree(closestMatch); } else { WARN_LOG(Log::Audio, "Failed to fall back to two channels. Using workarounds."); } createBuffer = true; } else { WARN_LOG(Log::Audio, "Got other error %08lx", result); _dbg_assert_(!closestMatch); } } 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; audioClient_->GetDevicePeriod(&defaultPeriod, &minPeriod); 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; } hr = audioClient_->GetBufferSize(&reportedBufferSize_); if (FAILED(hr)) { audioClient_.Reset(); CoTaskMemFree(format_); format_ = nullptr; SetErrorString("AudioClient GetBufferSize failed", hr); return false; } actualPeriodFrames_ = reportedBufferSize_; // 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(reportedBufferSize_ * 2); } return true; } bool WASAPIContext::InitOutputDevice(std::string_view uniqueId, LatencyMode latencyMode, bool *revertedToDefault) { Stop(); *revertedToDefault = false; ComPtr 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 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; } { std::lock_guard guard(deviceLock_); curDeviceId_.clear(); curDeviceName_.clear(); } } void WASAPIContext::FrameUpdate(bool allowAutoChange) { std::string deviceIdToInit; { std::lock_guard 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; } } 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; } } 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 = format_->nChannels; ClearErrorString(); while (running_) { const DWORD waitResult = WaitForSingleObject(audioEvent_, INFINITE); if (waitResult != WAIT_OBJECT_0) { // Something bad happened. break; } UINT32 padding = 0; if (audioClient3_) { audioClient3_->GetCurrentPadding(&padding); } else { audioClient_->GetCurrentPadding(&padding); } const UINT32 framesToWrite = available - padding; BYTE* buffer = nullptr; if (framesToWrite > 0 && SUCCEEDED(renderClient_->GetBuffer(framesToWrite, &buffer))) { if (!tempBuf_) { // Mix directly to the output buffer, avoiding a copy. if (buffer) { callback_(reinterpret_cast(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) if (format == AudioFormat::PCM16 && buffer) { // Need to convert. s16 *dest = reinterpret_cast(buffer); for (UINT32 i = 0; i < framesToWrite; i++) { if (nChannels == 1) { // Maybe some bluetooth speakers? Mixdown. float sum = 0.5f * (tempBuf_[i * 2] + tempBuf_[i * 2 + 1]); dest[i] = ClampFloatToS16(sum); } else { dest[i * nChannels] = ClampFloatToS16(tempBuf_[i * 2]); dest[i * nChannels + 1] = ClampFloatToS16(tempBuf_[i * 2 + 1]); // Zero other channels. for (int j = 2; j < nChannels; j++) { dest[i * nChannels + j] = 0; } } } } 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(buffer); for (UINT32 i = 0; i < framesToWrite; i++) { if (nChannels == 1) { // Maybe some bluetooth speakers? Mixdown. dest[i] = 0.5f * (tempBuf_[i * 2] + tempBuf_[i * 2 + 1]); } else { dest[i * nChannels] = tempBuf_[i * 2]; dest[i * nChannels + 1] = tempBuf_[i * 2 + 1]; // Zero other channels. for (int j = 2; j < nChannels; j++) { dest[i * nChannels + j] = 0; } } } } } renderClient_->ReleaseBuffer(framesToWrite, 0); } // In the old mode, we just estimate the "actualPeriodFrames_" from the framesToWrite. if (audioClient_ && framesToWrite < actualPeriodFrames_) { actualPeriodFrames_ = 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 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; }