414 lines
14 KiB
C++
414 lines
14 KiB
C++
/**
|
|
******************************************************************************
|
|
* Xenia : Xbox 360 Emulator Research Project *
|
|
******************************************************************************
|
|
* Copyright 2020 Ben Vanik. All rights reserved. *
|
|
* Released under the BSD license - see LICENSE in the root for more details. *
|
|
******************************************************************************
|
|
*/
|
|
|
|
#include "xenia/apu/xaudio2/xaudio2_audio_driver.h"
|
|
|
|
#include "xenia/apu/apu_flags.h"
|
|
#include "xenia/apu/conversion.h"
|
|
#include "xenia/apu/xaudio2/xaudio2_api.h"
|
|
#include "xenia/base/assert.h"
|
|
#include "xenia/base/clock.h"
|
|
#include "xenia/base/logging.h"
|
|
#include "xenia/base/platform_win.h"
|
|
#include "xenia/base/threading.h"
|
|
|
|
namespace xe {
|
|
namespace apu {
|
|
namespace xaudio2 {
|
|
|
|
class XAudio2AudioDriver::VoiceCallback : public api::IXAudio2VoiceCallback {
|
|
public:
|
|
explicit VoiceCallback(xe::threading::Semaphore* semaphore)
|
|
: semaphore_(semaphore) {}
|
|
~VoiceCallback() {}
|
|
|
|
void OnStreamEnd() noexcept {}
|
|
void OnVoiceProcessingPassEnd() noexcept {}
|
|
void OnVoiceProcessingPassStart(uint32_t samples_required) noexcept {}
|
|
void OnBufferEnd(void* context) noexcept {
|
|
auto ret = semaphore_->Release(1, nullptr);
|
|
assert_true(ret);
|
|
}
|
|
void OnBufferStart(void* context) noexcept {}
|
|
void OnLoopEnd(void* context) noexcept {}
|
|
void OnVoiceError(void* context, HRESULT result) noexcept {}
|
|
|
|
private:
|
|
xe::threading::Semaphore* semaphore_ = nullptr;
|
|
};
|
|
|
|
XAudio2AudioDriver::XAudio2AudioDriver(xe::threading::Semaphore* semaphore,
|
|
uint32_t frequency, uint32_t channels,
|
|
bool need_format_conversion)
|
|
: semaphore_(semaphore),
|
|
frame_frequency_(frequency),
|
|
frame_channels_(channels),
|
|
need_format_conversion_(need_format_conversion) {
|
|
switch (frame_channels_) {
|
|
case 6:
|
|
channel_samples_ = 256;
|
|
break;
|
|
case 2:
|
|
channel_samples_ = 768;
|
|
break;
|
|
default:
|
|
assert_unhandled_case(frame_channels_);
|
|
}
|
|
frame_size_ = sizeof(float) * frame_channels_ * channel_samples_;
|
|
assert_true(frame_size_ <= kFrameSizeMax);
|
|
assert_true(!need_format_conversion_ || frame_channels_ == 6);
|
|
}
|
|
|
|
XAudio2AudioDriver::~XAudio2AudioDriver() = default;
|
|
|
|
bool XAudio2AudioDriver::Initialize() {
|
|
voice_callback_ = new VoiceCallback(semaphore_);
|
|
|
|
// Load the XAudio2 DLL dynamically. Needed both for 2.7 and for
|
|
// differentiating between 2.8 and later versions. Windows 8.1 SDK references
|
|
// XAudio2_8.dll in xaudio2.lib, which is available on Windows 8.1, however,
|
|
// Windows 10 SDK references XAudio2_9.dll in it, which is only available in
|
|
// Windows 10, and XAudio2_8.dll is linked through a different .lib -
|
|
// xaudio2_8.lib, so easier not to link the .lib at all.
|
|
xaudio2_module_ = static_cast<void*>(LoadLibraryW(L"XAudio2_8.dll"));
|
|
if (xaudio2_module_) {
|
|
xaudio2_create_ = reinterpret_cast<decltype(xaudio2_create_)>(
|
|
GetProcAddress(static_cast<HMODULE>(xaudio2_module_), "XAudio2Create"));
|
|
if (xaudio2_create_) {
|
|
api_minor_version_ = 8;
|
|
} else {
|
|
XELOGE("XAudio2Create not found in XAudio2_8.dll");
|
|
FreeLibrary(static_cast<HMODULE>(xaudio2_module_));
|
|
xaudio2_module_ = nullptr;
|
|
}
|
|
}
|
|
if (!xaudio2_module_) {
|
|
XELOGE("Failed to load XAudio 2.8 library DLL");
|
|
return false;
|
|
}
|
|
|
|
// We need to be able to accept frames from any non-STA thread - primarily
|
|
// from any guest thread, so MTA needs to be used. The AudioDriver, however,
|
|
// may be initialized from the UI thread, which has the STA concurrency model,
|
|
// or from another thread regardless of its concurrency model. So, all
|
|
// management of the objects needs to be performed in MTA. Launch the
|
|
// lifecycle management thread, which will handle initialization and shutdown,
|
|
// and also provide a scope for implicit MTA in threads that have never
|
|
// initialized COM explicitly, which lasts until all threads that have
|
|
// initialized MTA explicitly have uninitialized it - the thread that holds
|
|
// the MTA scope needs to be running while other threads are able to submit
|
|
// frames as they might have not initialized MTA explicitly.
|
|
// https://devblogs.microsoft.com/oldnewthing/?p=4613
|
|
// This is needed for both XAudio 2.7 (created via explicit CoCreateInstance)
|
|
// and 2.8 (using XAudio2Create, but still requiring CoInitializeEx).
|
|
// https://docs.microsoft.com/en-us/windows/win32/xaudio2/how-to--initialize-xaudio2
|
|
assert_false(mta_thread_.joinable());
|
|
mta_thread_initialization_attempt_completed_ = false;
|
|
mta_thread_shutdown_requested_ = false;
|
|
mta_thread_ = std::thread(&XAudio2AudioDriver::MTAThread, this);
|
|
{
|
|
std::unique_lock<std::mutex> mta_thread_initialization_completion_lock(
|
|
mta_thread_initialization_completion_mutex_);
|
|
while (true) {
|
|
if (mta_thread_initialization_attempt_completed_) {
|
|
break;
|
|
}
|
|
mta_thread_initialization_completion_cond_.wait(
|
|
mta_thread_initialization_completion_lock);
|
|
}
|
|
}
|
|
if (!mta_thread_initialization_completion_result_) {
|
|
mta_thread_.join();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
template <typename Objects>
|
|
bool XAudio2AudioDriver::InitializeObjects(Objects& objects) {
|
|
HRESULT hr;
|
|
|
|
api::XAUDIO2_DEBUG_CONFIGURATION config;
|
|
config.TraceMask = api::XE_XAUDIO2_LOG_ERRORS | api::XE_XAUDIO2_LOG_WARNINGS;
|
|
config.BreakMask = 0;
|
|
config.LogThreadID = false;
|
|
config.LogTiming = true;
|
|
config.LogFunctionName = true;
|
|
config.LogFileline = true;
|
|
objects.audio->SetDebugConfiguration(&config);
|
|
|
|
hr = objects.audio->CreateMasteringVoice(&objects.mastering_voice);
|
|
if (FAILED(hr)) {
|
|
XELOGE("IXAudio2::CreateMasteringVoice failed with 0x{:08X}", hr);
|
|
ShutdownObjects(objects);
|
|
return false;
|
|
}
|
|
|
|
WAVEFORMATIEEEFLOATEX waveformat;
|
|
|
|
waveformat.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE;
|
|
waveformat.Format.nChannels = frame_channels_;
|
|
waveformat.Format.nSamplesPerSec = frame_frequency_;
|
|
waveformat.Format.wBitsPerSample = 32;
|
|
waveformat.Format.nBlockAlign =
|
|
(waveformat.Format.nChannels * waveformat.Format.wBitsPerSample) / 8;
|
|
waveformat.Format.nAvgBytesPerSec =
|
|
waveformat.Format.nSamplesPerSec * waveformat.Format.nBlockAlign;
|
|
waveformat.Format.cbSize =
|
|
sizeof(WAVEFORMATIEEEFLOATEX) - sizeof(WAVEFORMATEX);
|
|
|
|
waveformat.SubFormat = KSDATAFORMAT_SUBTYPE_IEEE_FLOAT;
|
|
waveformat.Samples.wValidBitsPerSample = waveformat.Format.wBitsPerSample;
|
|
static constexpr DWORD kChannelMasks[] = {
|
|
0,
|
|
0,
|
|
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT,
|
|
0,
|
|
0,
|
|
0,
|
|
SPEAKER_FRONT_LEFT | SPEAKER_FRONT_CENTER | SPEAKER_FRONT_RIGHT |
|
|
SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT,
|
|
0,
|
|
};
|
|
waveformat.dwChannelMask = kChannelMasks[waveformat.Format.nChannels];
|
|
|
|
hr = objects.audio->CreateSourceVoice(
|
|
&objects.pcm_voice, &waveformat.Format,
|
|
0, // api::XE_XAUDIO2_VOICE_NOSRC | api::XE_XAUDIO2_VOICE_NOPITCH,
|
|
api::XE_XAUDIO2_MAX_FREQ_RATIO, voice_callback_);
|
|
if (FAILED(hr)) {
|
|
XELOGE("IXAudio2::CreateSourceVoice failed with 0x{:08X}", hr);
|
|
ShutdownObjects(objects);
|
|
return false;
|
|
}
|
|
|
|
hr = objects.pcm_voice->Start();
|
|
if (FAILED(hr)) {
|
|
XELOGE("IXAudio2SourceVoice::Start failed with 0x{:08X}", hr);
|
|
ShutdownObjects(objects);
|
|
return false;
|
|
}
|
|
|
|
if (cvars::mute) {
|
|
objects.pcm_voice->SetVolume(0.0f);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void XAudio2AudioDriver::SubmitFrame(float* frame) {
|
|
HRESULT hr;
|
|
|
|
api::XAUDIO2_VOICE_STATE state;
|
|
if (api_minor_version_ >= 8) {
|
|
objects_.api_2_8.pcm_voice->GetState(&state,
|
|
api::XE_XAUDIO2_VOICE_NOSAMPLESPLAYED);
|
|
} else {
|
|
objects_.api_2_7.pcm_voice->GetState(&state);
|
|
}
|
|
assert_true(state.BuffersQueued < frame_count_);
|
|
|
|
auto output_frame = reinterpret_cast<float*>(frames_[current_frame_]);
|
|
|
|
if (need_format_conversion_) {
|
|
// Convert planar big endian samples into interleaved little endian.
|
|
conversion::sequential_6_BE_to_interleaved_6_LE(output_frame, frame,
|
|
channel_samples_);
|
|
} else {
|
|
memcpy(output_frame, frame, frame_size_);
|
|
}
|
|
|
|
api::XAUDIO2_BUFFER buffer;
|
|
buffer.Flags = 0;
|
|
buffer.pAudioData = reinterpret_cast<BYTE*>(output_frame);
|
|
buffer.AudioBytes = frame_size_;
|
|
buffer.PlayBegin = 0;
|
|
buffer.PlayLength = channel_samples_;
|
|
buffer.LoopBegin = api::XE_XAUDIO2_NO_LOOP_REGION;
|
|
buffer.LoopLength = 0;
|
|
buffer.LoopCount = 0;
|
|
buffer.pContext = 0;
|
|
if (api_minor_version_ >= 8) {
|
|
hr = objects_.api_2_8.pcm_voice->SubmitSourceBuffer(&buffer);
|
|
} else {
|
|
hr = objects_.api_2_7.pcm_voice->SubmitSourceBuffer(&buffer);
|
|
}
|
|
if (FAILED(hr)) {
|
|
XELOGE("SubmitSourceBuffer failed with 0x{:08X}", hr);
|
|
return;
|
|
}
|
|
|
|
current_frame_ = (current_frame_ + 1) % frame_count_;
|
|
|
|
// Update playback ratio to our time scalar.
|
|
// This will keep audio in sync with the game clock.
|
|
float frequency_ratio = static_cast<float>(xe::Clock::guest_time_scalar());
|
|
if (api_minor_version_ >= 8) {
|
|
objects_.api_2_8.pcm_voice->SetFrequencyRatio(frequency_ratio);
|
|
} else {
|
|
objects_.api_2_7.pcm_voice->SetFrequencyRatio(frequency_ratio);
|
|
}
|
|
}
|
|
|
|
void XAudio2AudioDriver::Pause() {
|
|
if (api_minor_version_ >= 8) {
|
|
objects_.api_2_8.pcm_voice->Stop();
|
|
} else {
|
|
objects_.api_2_7.pcm_voice->Stop();
|
|
}
|
|
}
|
|
|
|
void XAudio2AudioDriver::Resume() {
|
|
if (api_minor_version_ >= 8) {
|
|
objects_.api_2_8.pcm_voice->Start();
|
|
} else {
|
|
objects_.api_2_7.pcm_voice->Start();
|
|
}
|
|
}
|
|
|
|
void XAudio2AudioDriver::SetVolume(float volume) {
|
|
if (cvars::mute) return;
|
|
|
|
if (api_minor_version_ >= 8) {
|
|
objects_.api_2_8.pcm_voice->SetVolume(volume);
|
|
} else {
|
|
objects_.api_2_7.pcm_voice->SetVolume(volume);
|
|
}
|
|
}
|
|
|
|
void XAudio2AudioDriver::Shutdown() {
|
|
// XAudio2 lifecycle is managed by the MTA thread.
|
|
if (mta_thread_.joinable()) {
|
|
{
|
|
std::unique_lock<std::mutex> mta_thread_shutdown_request_lock(
|
|
mta_thread_shutdown_request_mutex_);
|
|
mta_thread_shutdown_requested_ = true;
|
|
}
|
|
mta_thread_shutdown_request_cond_.notify_all();
|
|
mta_thread_.join();
|
|
}
|
|
|
|
xaudio2_create_ = nullptr;
|
|
if (xaudio2_module_) {
|
|
FreeLibrary(static_cast<HMODULE>(xaudio2_module_));
|
|
xaudio2_module_ = nullptr;
|
|
}
|
|
|
|
if (voice_callback_) {
|
|
delete voice_callback_;
|
|
voice_callback_ = nullptr;
|
|
}
|
|
}
|
|
|
|
template <typename Objects>
|
|
void XAudio2AudioDriver::ShutdownObjects(Objects& objects) {
|
|
// Stop the engine first to ensure no callbacks are in-flight before
|
|
// destroying voices and the callback object.
|
|
if (objects.audio) {
|
|
objects.audio->StopEngine();
|
|
}
|
|
|
|
if (objects.pcm_voice) {
|
|
objects.pcm_voice->DestroyVoice();
|
|
objects.pcm_voice = nullptr;
|
|
}
|
|
|
|
if (objects.mastering_voice) {
|
|
objects.mastering_voice->DestroyVoice();
|
|
objects.mastering_voice = nullptr;
|
|
}
|
|
|
|
if (objects.audio) {
|
|
objects.audio->Release();
|
|
objects.audio = nullptr;
|
|
}
|
|
}
|
|
|
|
void XAudio2AudioDriver::MTAThread() {
|
|
xe::threading::set_name("XAudio2 MTA");
|
|
|
|
assert_false(mta_thread_initialization_attempt_completed_);
|
|
|
|
bool initialized = false;
|
|
|
|
// Initializing MTA COM in this thread, as well making other (guest) threads
|
|
// that don't explicitly call CoInitializeEx implicitly MTA for the period of
|
|
// time when they can interact with XAudio through the XAudio2AudioDriver,
|
|
// until the CoUninitialize (to be more precise, the CoUninitialize for the
|
|
// last remaining MTA thread, but we need implicit MTA for the audio here).
|
|
// https://devblogs.microsoft.com/oldnewthing/?p=4613
|
|
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
|
|
bool com_initialized = SUCCEEDED(hr);
|
|
if (!com_initialized) {
|
|
XELOGE("XAudio2 MTA thread CoInitializeEx failed with 0x{:08X}", hr);
|
|
} else {
|
|
if (api_minor_version_ >= 8) {
|
|
hr = xaudio2_create_(&objects_.api_2_8.audio, 0, kProcessor);
|
|
if (FAILED(hr)) {
|
|
XELOGE("XAudio2Create failed with 0x{:08X}", hr);
|
|
} else {
|
|
initialized = InitializeObjects(objects_.api_2_8);
|
|
}
|
|
} else {
|
|
hr = CoCreateInstance(__uuidof(api::XAudio2_7), nullptr,
|
|
CLSCTX_INPROC_SERVER,
|
|
IID_PPV_ARGS(&objects_.api_2_7.audio));
|
|
if (FAILED(hr)) {
|
|
XELOGE("CoCreateInstance for XAudio2 failed with 0x{:08X}", hr);
|
|
} else {
|
|
hr = objects_.api_2_7.audio->Initialize(0, kProcessor);
|
|
if (FAILED(hr)) {
|
|
XELOGE("IXAudio2::Initialize failed with 0x{:08X}", hr);
|
|
} else {
|
|
initialized = InitializeObjects(objects_.api_2_7);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Notify the threads waiting for the initialization of the result.
|
|
mta_thread_initialization_completion_result_ = initialized;
|
|
{
|
|
std::unique_lock<std::mutex> mta_thread_initialization_completion_lock(
|
|
mta_thread_initialization_completion_mutex_);
|
|
mta_thread_initialization_attempt_completed_ = true;
|
|
}
|
|
mta_thread_initialization_completion_cond_.notify_all();
|
|
|
|
if (initialized) {
|
|
// Initialized successfully, await a shutdown request while keeping an
|
|
// implicit COM MTA scope.
|
|
{
|
|
std::unique_lock<std::mutex> mta_thread_shutdown_request_lock(
|
|
mta_thread_shutdown_request_mutex_);
|
|
while (true) {
|
|
if (mta_thread_shutdown_requested_) {
|
|
break;
|
|
}
|
|
mta_thread_shutdown_request_cond_.wait(
|
|
mta_thread_shutdown_request_lock);
|
|
}
|
|
}
|
|
|
|
if (api_minor_version_ >= 8) {
|
|
ShutdownObjects(objects_.api_2_8);
|
|
} else {
|
|
ShutdownObjects(objects_.api_2_7);
|
|
}
|
|
}
|
|
|
|
if (com_initialized) {
|
|
CoUninitialize();
|
|
}
|
|
}
|
|
|
|
} // namespace xaudio2
|
|
} // namespace apu
|
|
} // namespace xe
|