AudioStream: Reintroduce XAudio2 backend

With device selection/lower latency this time.
pull/3732/head
Stenzek 2 months ago
parent 5d92da522b
commit dea0480166
No known key found for this signature in database

@ -242,6 +242,7 @@ if(WIN32)
http_downloader_winhttp.cpp
win32_raw_input_source.cpp
win32_raw_input_source.h
xaudio2_audio_stream.cpp
xinput_source.cpp
xinput_source.h
)

@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2019-2025 Connor McLaughlin <stenzek@gmail.com>
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "audio_stream.h"
@ -18,24 +18,35 @@ AudioStream::DeviceInfo::DeviceInfo(std::string name_, std::string display_name_
AudioStream::DeviceInfo::~DeviceInfo() = default;
static constexpr const std::array s_backend_names = {
// clang-format off
"Null",
#ifndef __ANDROID__
"Cubeb",
"SDL",
#else
"AAudio",
"OpenSLES",
#endif
#ifdef _WIN32
"XAudio2",
#endif
#ifdef __ANDROID__
"AAudio", "OpenSLES",
#endif
// clang-format on
};
static constexpr const std::array s_backend_display_names = {
// clang-format off
TRANSLATE_DISAMBIG_NOOP("Settings", "Null (No Output)", "AudioBackend"),
#ifndef __ANDROID__
TRANSLATE_DISAMBIG_NOOP("Settings", "Cubeb", "AudioBackend"),
TRANSLATE_DISAMBIG_NOOP("Settings", "SDL", "AudioBackend"),
#else
#endif
#ifdef _WIN32
TRANSLATE_DISAMBIG_NOOP("Settings", "XAudio2", "AudioBackend"),
#endif
#ifdef __ANDROID__
"AAudio",
"OpenSL ES",
#endif
// clang-format on
};
std::optional<AudioBackend> AudioStream::ParseBackendName(std::string_view str)
@ -96,6 +107,11 @@ std::vector<AudioStream::DeviceInfo> AudioStream::GetOutputDevices(AudioBackend
ret = GetCubebOutputDevices(driver, sample_rate);
break;
#endif
#ifdef _WIN32
case AudioBackend::XAudio2:
ret = GetXAudio2OutputDevices(sample_rate);
break;
#endif
default:
break;
@ -119,7 +135,13 @@ std::unique_ptr<AudioStream> AudioStream::CreateStream(AudioBackend backend, u32
case AudioBackend::SDL:
return CreateSDLAudioStream(sample_rate, channels, output_latency_frames, output_latency_minimal, source,
auto_start, error);
#else
#endif
#ifdef _WIN32
case AudioBackend::XAudio2:
return CreateXAudio2AudioStream(sample_rate, channels, output_latency_frames, output_latency_minimal, device_name,
source, auto_start, error);
#endif
#ifdef __ANDROID__
case AudioBackend::AAudio:
return CreateAAudioAudioStream(sample_rate, channels, output_latency_frames, output_latency_minimal, source,
auto_start, error);

@ -18,6 +18,9 @@ enum class AudioBackend : u8
#ifndef __ANDROID__
Cubeb,
SDL,
#ifdef _WIN32
XAudio2,
#endif
#else
AAudio,
OpenSLES,
@ -94,6 +97,13 @@ private:
static std::unique_ptr<AudioStream> CreateSDLAudioStream(u32 sample_rate, u32 channels, u32 output_latency_frames,
bool output_latency_minimal, AudioStreamSource* source,
bool auto_start, Error* error);
#ifdef _WIN32
static std::vector<DeviceInfo> GetXAudio2OutputDevices(u32 sample_rate);
static std::unique_ptr<AudioStream> CreateXAudio2AudioStream(u32 sample_rate, u32 channels, u32 output_latency_frames,
bool output_latency_minimal,
std::string_view device_name, AudioStreamSource* source,
bool auto_start, Error* error);
#endif
#else
static std::unique_ptr<AudioStream> CreateAAudioAudioStream(u32 sample_rate, u32 channels, u32 output_latency_frames,
bool output_latency_minimal, AudioStreamSource* source,

@ -225,6 +225,7 @@
<ClCompile Include="x11_tools.cpp">
<ExcludedFromBuild>true</ExcludedFromBuild>
</ClCompile>
<ClCompile Include="xaudio2_audio_stream.cpp" />
<ClCompile Include="xinput_source.cpp" />
</ItemGroup>
<ItemGroup>

@ -171,6 +171,7 @@
<ClCompile Include="cd_image_ccd.cpp" />
<ClCompile Include="object_archive.cpp" />
<ClCompile Include="http_cache.cpp" />
<ClCompile Include="xaudio2_audio_stream.cpp" />
</ItemGroup>
<ItemGroup>
<None Include="metal_shaders.metal" />

@ -0,0 +1,314 @@
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0
#include "audio_stream.h"
#include "translation.h"
#include "common/assert.h"
#include "common/dynamic_library.h"
#include "common/error.h"
#include "common/heap_array.h"
#include "common/log.h"
#include "common/string_util.h"
#include <array>
#include <atomic>
#include <vector>
#include "common/windows_headers.h"
#include <mmdeviceapi.h> // must be included before functiondiscoverykeys_devpkey.h
#include <devpkey.h>
#include <functiondiscoverykeys_devpkey.h>
#include <propsys.h>
#include <wrl/client.h>
#include <xaudio2.h>
LOG_CHANNEL(AudioStream);
namespace {
static constexpr u32 NUM_BUFFERS = 2;
class XAudio2AudioStream final : public AudioStream, private IXAudio2VoiceCallback
{
public:
XAudio2AudioStream(AudioStreamSource* source, u32 channels);
~XAudio2AudioStream() override;
bool Initialize(u32 sample_rate, u32 channels, u32 output_latency_frames, bool output_latency_minimal,
std::string_view device_name, bool auto_start, Error* error);
bool Start(Error* error) override;
bool Stop(Error* error) override;
private:
s16* GetBufferPointer(u32 buffer_index);
// IXAudio2VoiceCallback — only OnBufferEnd needs a body
void STDMETHODCALLTYPE OnVoiceProcessingPassStart(UINT32 BytesRequired) override {}
void STDMETHODCALLTYPE OnVoiceProcessingPassEnd() override {}
void STDMETHODCALLTYPE OnStreamEnd() override {}
void STDMETHODCALLTYPE OnBufferStart(void* pBufferContext) override {}
void STDMETHODCALLTYPE OnBufferEnd(void* pBufferContext) override;
void STDMETHODCALLTYPE OnLoopEnd(void* pBufferContext) override {}
void STDMETHODCALLTYPE OnVoiceError(void* pBufferContext, HRESULT Error) override {}
AudioStreamSource* m_source;
u32 m_buffer_frames = 0;
u32 m_channels;
DynamicLibrary m_library;
Microsoft::WRL::ComPtr<IXAudio2> m_xaudio2;
IXAudio2MasteringVoice* m_mastering_voice = nullptr;
IXAudio2SourceVoice* m_source_voice = nullptr;
DynamicHeapArray<s16> m_buffer;
std::atomic_bool m_shutting_down{false};
};
} // namespace
XAudio2AudioStream::XAudio2AudioStream(AudioStreamSource* source, u32 channels) : m_source(source), m_channels(channels)
{
}
XAudio2AudioStream::~XAudio2AudioStream()
{
if (m_source_voice)
{
m_shutting_down.store(true, std::memory_order_release);
m_source_voice->Stop(0);
m_source_voice->FlushSourceBuffers();
m_source_voice->DestroyVoice();
m_source_voice = nullptr;
}
if (m_mastering_voice)
{
m_mastering_voice->DestroyVoice();
m_mastering_voice = nullptr;
}
m_xaudio2.Reset();
}
bool XAudio2AudioStream::Initialize(u32 sample_rate, u32 channels, u32 output_latency_frames,
bool output_latency_minimal, std::string_view device_name, bool auto_start,
Error* error)
{
if (!m_library.IsOpen() && !m_library.Open(XAUDIO2_DLL_A, error))
return false;
HRESULT(WINAPI * pXAudio2Create)(IXAudio2**, UINT32, XAUDIO2_PROCESSOR);
if (!m_library.GetSymbol("XAudio2Create", &pXAudio2Create))
{
Error::SetWin32(error, "Failed to get XAudio2Create function: ", GetLastError());
return false;
}
HRESULT hr = pXAudio2Create(m_xaudio2.GetAddressOf(), 0, XAUDIO2_DEFAULT_PROCESSOR);
if (FAILED(hr))
{
Error::SetHResult(error, "XAudio2Create() failed: ", hr);
return false;
}
// Convert optional device name from UTF-8 to wide for CreateMasteringVoice.
std::wstring device_id;
if (!device_name.empty())
device_id = StringUtil::UTF8StringToWideString(device_name);
hr = m_xaudio2->CreateMasteringVoice(&m_mastering_voice, channels, sample_rate, 0,
device_id.empty() ? nullptr : device_id.c_str());
if (FAILED(hr))
{
if (!device_name.empty())
{
// Try with the default device if a specific one was requested, in case the requested device
// is invalid or unavailable.
ERROR_LOG("IXAudio2::CreateMasteringVoice() for specific device failed: {:08X}", static_cast<unsigned>(hr));
hr = m_xaudio2->CreateMasteringVoice(&m_mastering_voice, channels, sample_rate, 0, nullptr);
if (SUCCEEDED(hr))
{
WARNING_LOG("IXAudio2::CreateMasteringVoice() succeeded with default device after specific device failed, "
"ignoring requested device '{}'",
device_name);
}
}
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2::CreateMasteringVoice() failed: ", hr);
return false;
}
}
WAVEFORMATEX wfx = {};
wfx.wFormatTag = WAVE_FORMAT_PCM;
wfx.nChannels = static_cast<WORD>(channels);
wfx.nSamplesPerSec = sample_rate;
wfx.wBitsPerSample = 16;
wfx.nBlockAlign = static_cast<WORD>((channels * 16) / 8);
wfx.nAvgBytesPerSec = sample_rate * wfx.nBlockAlign;
wfx.cbSize = 0;
hr = m_xaudio2->CreateSourceVoice(&m_source_voice, &wfx, 0, 1.0f, this);
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2::CreateSourceVoice() failed: ", hr);
return false;
}
m_buffer_frames = output_latency_frames;
m_buffer.resize(NUM_BUFFERS * m_buffer_frames * channels);
// Pre-fill with a single frame buffers and enqueue them so playback can begin immediately.
for (u32 i = 0; i < NUM_BUFFERS; i++)
{
XAUDIO2_BUFFER buffer = {};
buffer.AudioBytes = sizeof(s16) * channels;
buffer.pAudioData = reinterpret_cast<const BYTE*>(GetBufferPointer(i));
buffer.pContext = reinterpret_cast<void*>(static_cast<uintptr_t>(i));
hr = m_source_voice->SubmitSourceBuffer(&buffer);
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2SourceVoice::SubmitSourceBuffer() failed: ", hr);
return false;
}
}
if (auto_start)
{
hr = m_source_voice->Start(0);
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2SourceVoice::Start() failed: ", hr);
return false;
}
}
INFO_LOG("XAudio2 stream initialized: {}hz, {} channels, {} frames/buffer ({} ms)", sample_rate, channels,
output_latency_frames, FramesToMS(sample_rate, output_latency_frames));
return true;
}
s16* XAudio2AudioStream::GetBufferPointer(u32 buffer_index)
{
DebugAssert(((buffer_index * (m_channels * m_buffer_frames)) + m_buffer_frames) <= m_buffer.size());
return &m_buffer[buffer_index * (m_channels * m_buffer_frames)];
}
void XAudio2AudioStream::OnBufferEnd(void* pBufferContext)
{
if (m_shutting_down.load(std::memory_order_acquire))
return;
const u32 buffer_idx = static_cast<u32>(reinterpret_cast<uintptr_t>(pBufferContext));
s16* const buffer_ptr = GetBufferPointer(buffer_idx);
m_source->ReadFrames(buffer_ptr, m_buffer_frames);
XAUDIO2_BUFFER buffer = {};
buffer.AudioBytes = static_cast<UINT32>(m_buffer_frames * m_channels * sizeof(s16));
buffer.pAudioData = reinterpret_cast<const BYTE*>(buffer_ptr);
buffer.pContext = pBufferContext;
m_source_voice->SubmitSourceBuffer(&buffer);
}
bool XAudio2AudioStream::Start(Error* error)
{
const HRESULT hr = m_source_voice->Start(0);
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2SourceVoice::Start() failed: ", hr);
return false;
}
return true;
}
bool XAudio2AudioStream::Stop(Error* error)
{
const HRESULT hr = m_source_voice->Stop(0);
if (FAILED(hr))
{
Error::SetHResult(error, "IXAudio2SourceVoice::Stop() failed: ", hr);
return false;
}
return true;
}
std::vector<AudioStream::DeviceInfo> AudioStream::GetXAudio2OutputDevices(u32 sample_rate)
{
std::vector<AudioStream::DeviceInfo> ret;
ret.emplace_back(std::string(), TRANSLATE_STR("AudioStream", "Default"), 0);
Microsoft::WRL::ComPtr<IMMDeviceEnumerator> enumerator;
HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER,
__uuidof(IMMDeviceEnumerator), reinterpret_cast<void**>(enumerator.GetAddressOf()));
if (FAILED(hr))
{
WARNING_LOG("CoCreateInstance(IMMDeviceEnumerator) failed: {:08X}", static_cast<unsigned>(hr));
return ret;
}
Microsoft::WRL::ComPtr<IMMDeviceCollection> devices;
hr = enumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, devices.GetAddressOf());
if (FAILED(hr))
{
WARNING_LOG("IMMDeviceEnumerator::EnumAudioEndpoints() failed: {:08X}", static_cast<unsigned>(hr));
return ret;
}
UINT count = 0;
devices->GetCount(&count);
for (UINT i = 0; i < count; i++)
{
Microsoft::WRL::ComPtr<IMMDevice> device;
if (FAILED(devices->Item(i, device.GetAddressOf())))
continue;
// The WASAPI device ID is what XAudio2 expects for CreateMasteringVoice.
LPWSTR device_id_wide = nullptr;
if (FAILED(device->GetId(&device_id_wide)))
continue;
std::string device_id = StringUtil::WideStringToUTF8String(device_id_wide);
CoTaskMemFree(device_id_wide);
std::string friendly_name;
Microsoft::WRL::ComPtr<IPropertyStore> props;
if (SUCCEEDED(device->OpenPropertyStore(STGM_READ, props.GetAddressOf())))
{
PROPVARIANT pv;
PropVariantInit(&pv);
if (SUCCEEDED(props->GetValue(PKEY_Device_FriendlyName, &pv)) && pv.vt == VT_LPWSTR)
friendly_name = StringUtil::WideStringToUTF8String(pv.pwszVal);
PropVariantClear(&pv);
}
ret.emplace_back(std::move(device_id), friendly_name.empty() ? ret.back().name : std::move(friendly_name), 0);
}
return ret;
}
std::unique_ptr<AudioStream>
AudioStream::CreateXAudio2AudioStream(u32 sample_rate, u32 channels, u32 output_latency_frames,
bool output_latency_minimal, std::string_view device_name,
AudioStreamSource* source, bool auto_start, Error* error)
{
std::unique_ptr<XAudio2AudioStream> stream = std::make_unique<XAudio2AudioStream>(source, channels);
if (!stream->Initialize(sample_rate, channels, output_latency_frames, output_latency_minimal, device_name, auto_start,
error))
{
stream.reset();
}
return stream;
}
Loading…
Cancel
Save