1299 lines
43 KiB
C++
1299 lines
43 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
/**
|
|
Concrete implementation of FAudioDevice for XAudio2
|
|
|
|
See https://msdn.microsoft.com/en-us/library/windows/desktop/hh405049%28v=vs.85%29.aspx
|
|
*/
|
|
|
|
#include "AudioMixerPlatformXAudio2.h"
|
|
#include "AudioMixer.h"
|
|
#include "AudioDevice.h"
|
|
#include "HAL/PlatformAffinity.h"
|
|
#include "HAL/PlatformTime.h"
|
|
#include "HAL/PlatformProcess.h"
|
|
#include "Engine/EngineTypes.h"
|
|
|
|
#include "CoreGlobals.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Misc/ScopeLock.h"
|
|
#include "HAL/Event.h"
|
|
#include "CoreMinimal.h"
|
|
#include "Logging/LogMacros.h"
|
|
#include "ToStringHelpers.h"
|
|
#include "ScopedCom.h"
|
|
|
|
THIRD_PARTY_INCLUDES_START
|
|
// Including initguid.h will define the PKEY symbols below which area used cross-platform
|
|
#include <initguid.h>
|
|
#include <mmdeviceapi.h>
|
|
#include <AudioClient.h>
|
|
#if PLATFORM_WINDOWS
|
|
#include <FunctionDiscoveryKeys_devpkey.h>
|
|
#endif //PLATFORM_WINDOWS
|
|
THIRD_PARTY_INCLUDES_END
|
|
|
|
#include "Misc/CoreDelegates.h"
|
|
#include "ProfilingDebugging/ScopedTimers.h"
|
|
#include "Async/Async.h"
|
|
|
|
#define XAUDIO2_LOG_AND_HANDLE_ON_FAIL(FunctionName, Result, OnError)\
|
|
if (FAILED(Result))\
|
|
{\
|
|
UE_LOG(LogAudioMixer, Error, TEXT("XAudio2 Error: %s -> 0x%X: '%s', called in '%hs' (%s:%d)"),\
|
|
TEXT(FunctionName), (uint32)Result, *Audio::ToErrorFString(Result), __func__, __FILEW__, __LINE__);\
|
|
OnError;\
|
|
}
|
|
|
|
#define XAUDIO2_CALL_AND_HANDLE_ERROR(CALL, OnError)\
|
|
{\
|
|
const HRESULT Result = CALL;\
|
|
XAUDIO2_LOG_AND_HANDLE_ON_FAIL(#CALL, Result, OnError);\
|
|
}
|
|
|
|
static XAUDIO2_PROCESSOR GetXAudio2ProcessorsToUse()
|
|
{
|
|
XAUDIO2_PROCESSOR ProcessorsToUse = (XAUDIO2_PROCESSOR)FPlatformAffinity::GetAudioRenderThreadMask();
|
|
// https://docs.microsoft.com/en-us/windows/win32/api/xaudio2/nf-xaudio2-xaudio2create
|
|
// Warning If you specify XAUDIO2_ANY_PROCESSOR, the system will use all of the device's processors and, as noted above, create a worker thread for each processor.
|
|
// We certainly don't want to use all available CPU. XAudio threads are time critical priority and wake up every 10 ms, they may cause lots of unwarranted context switches.
|
|
// In case no specific affinity is specified, let XAudio choose the default processor. It should allocate a single thread and should be enough.
|
|
if (ProcessorsToUse == XAUDIO2_ANY_PROCESSOR)
|
|
{
|
|
#ifdef XAUDIO2_USE_DEFAULT_PROCESSOR
|
|
ProcessorsToUse = XAUDIO2_USE_DEFAULT_PROCESSOR;
|
|
#else
|
|
ProcessorsToUse = XAUDIO2_DEFAULT_PROCESSOR;
|
|
#endif
|
|
}
|
|
|
|
return ProcessorsToUse;
|
|
}
|
|
|
|
#if USE_REDIST_LIB
|
|
const FString& GetDllName(FName Current = NAME_None)
|
|
{
|
|
#if PLATFORM_64BITS
|
|
static const FString XAudio2_9Redist = FPaths::EngineDir() / TEXT("Binaries/ThirdParty/Windows/XAudio2_9/x64/xaudio2_9redist.dll");
|
|
#else
|
|
static const FString XAudio2_9Redist = FPaths::EngineDir() / TEXT("Binaries/ThirdParty/Windows/XAudio2_9/x86/xaudio2_9redist.dll");
|
|
#endif
|
|
return XAudio2_9Redist;
|
|
}
|
|
#endif //#if USE_REDIST_LIB
|
|
|
|
/*
|
|
Whether or not to enable xaudio2 debugging mode
|
|
To see the debug output, you need to view ETW logs for this application:
|
|
Go to Control Panel, Administrative Tools, Event Viewer.
|
|
View->Show Analytic and Debug Logs.
|
|
Applications and Services Logs / Microsoft / Windows / XAudio2.
|
|
Right click on Microsoft Windows XAudio2 debug logging, Properties, then Enable Logging, and hit OK
|
|
*/
|
|
#define XAUDIO2_DEBUG_ENABLED 0
|
|
|
|
namespace Audio
|
|
{
|
|
static float GThreadedSwapDebugExtraTimeMs = 0;
|
|
static FAutoConsoleVariableRef GThreadedSwapDebugExtraTimeMsCVar(
|
|
TEXT("au.ThreadedSwapDebugExtraTime"),
|
|
GThreadedSwapDebugExtraTimeMs,
|
|
TEXT("Simulate a slow device swap by adding additional time to the swap task"),
|
|
ECVF_Default);
|
|
|
|
void FXAudio2VoiceCallback::OnBufferEnd(void* BufferContext)
|
|
{
|
|
SCOPED_NAMED_EVENT(FXAudio2VoiceCallback_OnBufferEnd, FColor::Blue);
|
|
|
|
check(BufferContext);
|
|
IAudioMixerPlatformInterface* MixerPlatform = (IAudioMixerPlatformInterface*)BufferContext;
|
|
MixerPlatform->ReadNextBuffer();
|
|
}
|
|
|
|
static uint32 ChannelTypeMap[EAudioMixerChannel::ChannelTypeCount] =
|
|
{
|
|
SPEAKER_FRONT_LEFT,
|
|
SPEAKER_FRONT_RIGHT,
|
|
SPEAKER_FRONT_CENTER,
|
|
SPEAKER_LOW_FREQUENCY,
|
|
SPEAKER_BACK_LEFT,
|
|
SPEAKER_BACK_RIGHT,
|
|
SPEAKER_FRONT_LEFT_OF_CENTER,
|
|
SPEAKER_FRONT_RIGHT_OF_CENTER,
|
|
SPEAKER_BACK_CENTER,
|
|
SPEAKER_SIDE_LEFT,
|
|
SPEAKER_SIDE_RIGHT,
|
|
SPEAKER_TOP_CENTER,
|
|
SPEAKER_TOP_FRONT_LEFT,
|
|
SPEAKER_TOP_FRONT_CENTER,
|
|
SPEAKER_TOP_FRONT_RIGHT,
|
|
SPEAKER_TOP_BACK_LEFT,
|
|
SPEAKER_TOP_BACK_CENTER,
|
|
SPEAKER_TOP_BACK_RIGHT,
|
|
SPEAKER_RESERVED,
|
|
};
|
|
|
|
FMixerPlatformXAudio2::FMixerPlatformXAudio2()
|
|
: XAudio2Dll(nullptr)
|
|
, XAudio2System(nullptr)
|
|
, OutputAudioStreamMasteringVoice(nullptr)
|
|
, OutputAudioStreamSourceVoice(nullptr)
|
|
, TimeSinceNullDeviceWasLastChecked(0.0f)
|
|
, bIsInitialized(false)
|
|
, bIsDeviceOpen(false)
|
|
{
|
|
#if PLATFORM_WINDOWS
|
|
FPlatformMisc::CoInitialize();
|
|
#endif // #if PLATFORM_WINDOWS
|
|
}
|
|
|
|
FMixerPlatformXAudio2::~FMixerPlatformXAudio2()
|
|
{
|
|
#if PLATFORM_WINDOWS
|
|
FPlatformMisc::CoUninitialize();
|
|
#endif // #if PLATFORM_WINDOWS
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS
|
|
// Dirty extern for now.
|
|
extern void RegisterForSessionEvents(const FString& InDeviceId);
|
|
#endif //PLATFORM_WINDOWS
|
|
|
|
bool FMixerPlatformXAudio2::CheckThreadedDeviceSwap()
|
|
{
|
|
bool bDidStopGeneratingAudio = false;
|
|
#if PLATFORM_WINDOWS
|
|
bDidStopGeneratingAudio = FAudioMixerPlatformSwappable::CheckThreadedDeviceSwap();
|
|
#endif //PLATFORM_WINDOWS
|
|
return bDidStopGeneratingAudio;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::PreDeviceSwap()
|
|
{
|
|
// Access to device swap context must be protected by DeviceSwapCriticalSection
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
if (DeviceSwapContext.IsValid())
|
|
{
|
|
// Finish initializing the device swap context
|
|
DeviceSwapContext->PreviousSystem = XAudio2System;
|
|
DeviceSwapContext->PreviousMasteringVoice = OutputAudioStreamMasteringVoice;
|
|
DeviceSwapContext->PreviousSourceVoice = OutputAudioStreamSourceVoice;
|
|
DeviceSwapContext->Callbacks = &OutputVoiceCallback;
|
|
DeviceSwapContext->RenderingSampleRate = OpenStreamParams.SampleRate;
|
|
|
|
// NULL our system / master / source voices. (as they are about to be torn down async).
|
|
XAudio2System = nullptr;
|
|
OutputAudioStreamMasteringVoice = nullptr;
|
|
OutputAudioStreamSourceVoice = nullptr;
|
|
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::PreDeviceSwap - Starting swap to [%s]"), DeviceSwapContext->RequestedDeviceId.IsEmpty() ? TEXT("[System Default]") : *DeviceSwapContext->RequestedDeviceId);
|
|
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("FMixerPlatformXAudio2::PreDeviceSwap - null device swap context"));
|
|
return false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::EnqueueAsyncDeviceSwap()
|
|
{
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::EnqueueAsyncDeviceSwap - enqueuing async device swap"));
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
TFunction<TUniquePtr<FDeviceSwapResult>()> AsyncDeviceSwap = [this]() mutable -> TUniquePtr<FDeviceSwapResult>
|
|
{
|
|
// Transfer ownership of DeviceSwapContext to the async task.
|
|
TUniquePtr<FXAudio2DeviceSwapContext> TempContext;
|
|
{
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
if (AudioStreamInfo.StreamState == EAudioOutputStreamState::SwappingDevice)
|
|
{
|
|
TempContext = MoveTemp(DeviceSwapContext);
|
|
}
|
|
}
|
|
|
|
return PerformDeviceSwap(MoveTemp(TempContext));
|
|
};
|
|
SetActiveDeviceSwapFuture(Async(EAsyncExecution::TaskGraph, MoveTemp(AsyncDeviceSwap)));
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::PostDeviceSwap()
|
|
{
|
|
// Once the context is handed off to the device swap routine (either async or synchronous),
|
|
// it should no longer be valid.
|
|
check(!DeviceSwapContext.IsValid());
|
|
bool bDidSucceed = false;
|
|
const FXAudio2DeviceSwapResult* DeviceSwapResult = static_cast<const FXAudio2DeviceSwapResult*>(GetDeviceSwapResult());
|
|
|
|
if (DeviceSwapResult && DeviceSwapResult->IsNewDeviceReady())
|
|
{
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
XAudio2System = DeviceSwapResult->NewSystem;
|
|
OutputAudioStreamMasteringVoice = DeviceSwapResult->NewMasteringVoice;
|
|
OutputAudioStreamSourceVoice = DeviceSwapResult->NewSourceVoice;
|
|
|
|
// Success?
|
|
if (XAudio2System && OutputAudioStreamSourceVoice && OutputAudioStreamMasteringVoice)
|
|
{
|
|
HRESULT Result;
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_PostDeviceSwap_StartEngine, FColor::Blue);
|
|
Result = XAudio2System->StartEngine();
|
|
}
|
|
if (SUCCEEDED(Result))
|
|
{
|
|
// Copy our new Device Info into our active one.
|
|
AudioStreamInfo.DeviceInfo = DeviceSwapResult->DeviceInfo;
|
|
|
|
// Display our new XAudio2 Mastering voice details.
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::PostDeviceSwap - successful Swap new Device is (NumChannels=%u, SampleRate=%u, DeviceID=%s, Name=%s), Reason=%s, InstanceID=%d, DurationMS=%.2f"),
|
|
(uint32)AudioStreamInfo.DeviceInfo.NumChannels, (uint32)AudioStreamInfo.DeviceInfo.SampleRate, *AudioStreamInfo.DeviceInfo.DeviceId, *AudioStreamInfo.DeviceInfo.Name,
|
|
*DeviceSwapResult->SwapReason, InstanceID, DeviceSwapResult->SuccessfulDurationMs);
|
|
|
|
// Reinitialize the output circular buffer to match the buffer math of the new audio device.
|
|
const int32 NumOutputSamples = AudioStreamInfo.NumOutputFrames * AudioStreamInfo.DeviceInfo.NumChannels;
|
|
if (ensure(NumOutputSamples > 0))
|
|
{
|
|
OutputBuffer.Init(AudioStreamInfo.AudioMixer, NumOutputSamples, NumOutputBuffers, AudioStreamInfo.DeviceInfo.Format);
|
|
}
|
|
|
|
bDidSucceed = true;
|
|
}
|
|
else
|
|
{
|
|
XAUDIO2_LOG_AND_HANDLE_ON_FAIL("XAudio2System->StartEngine()", Result, {/* nop */});
|
|
}
|
|
}
|
|
else // We either failed to init or deliberately switched to null device.
|
|
{
|
|
// Null renderer doesn't/shouldn't care about the format, so leave the format as it was before.
|
|
}
|
|
}
|
|
|
|
ResetActiveDeviceSwapFuture();
|
|
|
|
return bDidSucceed;
|
|
}
|
|
|
|
|
|
void FMixerPlatformXAudio2::SynchronousDeviceSwap()
|
|
{
|
|
// Transfer ownership of DeviceSwapContext memory to the device swap routine
|
|
TUniquePtr<FDeviceSwapResult> DeviceSwapResult = PerformDeviceSwap(TUniquePtr<FXAudio2DeviceSwapContext>(MoveTemp(DeviceSwapContext)));
|
|
|
|
// Set the promise and future result to replicate what the async task does
|
|
TPromise<TUniquePtr<FDeviceSwapResult>> Promise;
|
|
|
|
// It's ok if DeviceSwapResult is null here. It indicates an invalid device which will be handled.
|
|
Promise.SetValue(MoveTemp(DeviceSwapResult));
|
|
SetActiveDeviceSwapFuture(Promise.GetFuture());
|
|
}
|
|
|
|
TUniquePtr<FDeviceSwapResult> FMixerPlatformXAudio2::PerformDeviceSwap(TUniquePtr<FXAudio2DeviceSwapContext>&& InDeviceContext)
|
|
{
|
|
// Static method enforces no other state sharing occurs with the object that called it. InDeviceContext
|
|
// should have no other references outside of this method so that the device swap operation is isolated.
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_PerformDeviceSwap, FColor::Blue);
|
|
|
|
const uint64 StartTimeCycles = FPlatformTime::Cycles64();
|
|
|
|
// New thread might not have COM setup.
|
|
FScopedCoInitialize ScopedCoInitialize;
|
|
|
|
// Critical section not required here as this function is now sole owner of context
|
|
if (!InDeviceContext.IsValid())
|
|
{
|
|
UE_LOG(LogAudioMixer, Error, TEXT("FMixerPlatformXAudio2::PerformDeviceSwap - failed due to invalid DeviceSwapContext"));
|
|
return {};
|
|
}
|
|
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::PerformDeviceSwap - AsyncTask Start. Because=%s"), *InDeviceContext->DeviceSwapReason);
|
|
|
|
// Stop old engine running.
|
|
if (InDeviceContext->PreviousSystem)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_StopEngine, FColor::Blue);
|
|
InDeviceContext->PreviousSystem->StopEngine();
|
|
}
|
|
|
|
// Kill source voice.
|
|
if (InDeviceContext->PreviousSourceVoice)
|
|
{
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_FlushSourceBuffers, FColor::Blue);
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(InDeviceContext->PreviousSourceVoice->FlushSourceBuffers(), /* nop */ );
|
|
}
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_DestroySourceVoice, FColor::Blue);
|
|
InDeviceContext->PreviousSourceVoice->DestroyVoice();
|
|
InDeviceContext->PreviousSourceVoice = nullptr;
|
|
}
|
|
|
|
// Now destroy the mastering voice
|
|
if (InDeviceContext->PreviousMasteringVoice)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_DestroyMasterVoice, FColor::Blue);
|
|
InDeviceContext->PreviousMasteringVoice->DestroyVoice();
|
|
InDeviceContext->PreviousMasteringVoice = nullptr;
|
|
}
|
|
|
|
// Destroy System
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_DestroySystem, FColor::Blue);
|
|
SAFE_RELEASE(InDeviceContext->PreviousSystem);
|
|
InDeviceContext->PreviousSystem = nullptr;
|
|
}
|
|
|
|
// Don't attempt to create a new setup if there's no devices available.
|
|
if (!InDeviceContext->NewDevice.IsSet())
|
|
{
|
|
return {};
|
|
}
|
|
|
|
TUniquePtr<FXAudio2DeviceSwapResult> DeviceSwapResult = MakeUnique<FXAudio2DeviceSwapResult>();
|
|
|
|
// Create System.
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_CreateSystem, FColor::Blue);
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(XAudio2Create(&DeviceSwapResult->NewSystem, 0, GetXAudio2ProcessorsToUse()), return {});
|
|
}
|
|
|
|
// Create Master
|
|
{
|
|
check(InDeviceContext->NewDevice->NumChannels <= XAUDIO2_MAX_AUDIO_CHANNELS);
|
|
check(InDeviceContext->NewDevice->SampleRate >= XAUDIO2_MIN_SAMPLE_RATE);
|
|
check(InDeviceContext->NewDevice->SampleRate <= XAUDIO2_MAX_SAMPLE_RATE);
|
|
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_CreateMasterVoice, FColor::Blue);
|
|
DeviceSwapResult->NewMasteringVoice = CreateMasteringVoice(*DeviceSwapResult->NewSystem, *InDeviceContext->NewDevice, InDeviceContext->bUseDefaultDevice);
|
|
if (!DeviceSwapResult->NewMasteringVoice)
|
|
{
|
|
SAFE_RELEASE(DeviceSwapResult->NewSystem);
|
|
return {}; // FAIL.
|
|
}
|
|
}
|
|
|
|
// Create Source Voice.
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_AsyncDeleteCreate_CreateSourceVoice, FColor::Blue);
|
|
|
|
// Setup the format of the output source voice
|
|
WAVEFORMATEX Format = { 0 };
|
|
Format.nChannels = InDeviceContext->NewDevice->NumChannels;
|
|
Format.nSamplesPerSec = InDeviceContext->RenderingSampleRate; // NOTE: We use the Rendering sample rate here.
|
|
Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
|
|
Format.nAvgBytesPerSec = Format.nSamplesPerSec * sizeof(float) * Format.nChannels;
|
|
Format.nBlockAlign = sizeof(float) * Format.nChannels;
|
|
Format.wBitsPerSample = sizeof(float) * 8;
|
|
|
|
// Create the output source voice
|
|
HRESULT Result = DeviceSwapResult->NewSystem->CreateSourceVoice(
|
|
&DeviceSwapResult->NewSourceVoice,
|
|
&Format,
|
|
XAUDIO2_VOICE_NOPITCH,
|
|
XAUDIO2_DEFAULT_FREQ_RATIO,
|
|
InDeviceContext->Callbacks,
|
|
nullptr,
|
|
nullptr
|
|
);
|
|
|
|
if (FAILED(Result))
|
|
{
|
|
if (DeviceSwapResult->NewSourceVoice)
|
|
{
|
|
DeviceSwapResult->NewSourceVoice->DestroyVoice();
|
|
DeviceSwapResult->NewSourceVoice = nullptr;
|
|
}
|
|
|
|
if (DeviceSwapResult->NewMasteringVoice)
|
|
{
|
|
DeviceSwapResult->NewMasteringVoice->DestroyVoice();
|
|
DeviceSwapResult->NewMasteringVoice = nullptr;
|
|
}
|
|
|
|
SAFE_RELEASE(DeviceSwapResult->NewSystem);
|
|
XAUDIO2_LOG_AND_HANDLE_ON_FAIL("XAudio2System->CreateSourceVoice", Result, return {});
|
|
}
|
|
}
|
|
|
|
// Optionally for testing, sleep for some duration in order to help repro race conditions.
|
|
if (GThreadedSwapDebugExtraTimeMs > 0.f)
|
|
{
|
|
FPlatformProcess::Sleep(GThreadedSwapDebugExtraTimeMs / 1000.f);
|
|
}
|
|
|
|
// Listen session for changes to this device.
|
|
#if PLATFORM_WINDOWS
|
|
RegisterForSessionEvents(InDeviceContext->RequestedDeviceId);
|
|
#endif
|
|
|
|
DeviceSwapResult->SuccessfulDurationMs = FPlatformTime::ToMilliseconds64(FPlatformTime::Cycles64() - StartTimeCycles);
|
|
DeviceSwapResult->DeviceInfo = InDeviceContext->NewDevice.Get(FAudioPlatformDeviceInfo());
|
|
DeviceSwapResult->SwapReason = InDeviceContext->DeviceSwapReason;
|
|
|
|
return DeviceSwapResult;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::ResetXAudio2System()
|
|
{
|
|
SAFE_RELEASE(XAudio2System);
|
|
|
|
XAudio2System = nullptr;
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(
|
|
XAudio2Create(&XAudio2System, GetCreateFlags(), GetXAudio2ProcessorsToUse()),
|
|
return false);
|
|
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(XAudio2System->RegisterForCallbacks(this), return false);
|
|
|
|
// success.
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::InitializeHardware()
|
|
{
|
|
if (bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 already initialized."), Warning);
|
|
return false;
|
|
}
|
|
|
|
#if USE_REDIST_LIB
|
|
// Work around the fact the x64 version of XAudio2_7.dll does not properly ref count
|
|
// by forcing it to be always loaded
|
|
|
|
// Load the xaudio2 library and keep a handle so we can free it on teardown
|
|
// Note: windows internally ref-counts the library per call to load library so
|
|
// when we call FreeLibrary, it will only free it once the refcount is zero
|
|
// Also, FPlatformProcess::GetDllHandle should not be used, as it will not increase ref count further if the library is already loaded.
|
|
// FPaths::ConvertRelativePathToFull is used for parity with how GetDllHandle calls LoadLibrary.
|
|
XAudio2Dll = LoadLibrary(*FPaths::ConvertRelativePathToFull(GetDllName()));
|
|
|
|
// returning null means we failed to load XAudio2, which means everything will fail
|
|
if (XAudio2Dll == nullptr)
|
|
{
|
|
UE_LOG(LogInit, Warning, TEXT("Failed to load XAudio2 dll"));
|
|
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("Audio", "XAudio2Missing", "XAudio2.7 is not installed. Make sure you have XAudio 2.7 installed. XAudio 2.7 is available in the DirectX End-User Runtime (June 2010)."));
|
|
return false;
|
|
}
|
|
#endif // #if PLATFORM_WINDOWS
|
|
|
|
const uint32 Flags = GetCreateFlags();
|
|
if (!XAudio2System && FAILED(XAudio2Create(&XAudio2System, Flags, GetXAudio2ProcessorsToUse())))
|
|
{
|
|
FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("Audio", "XAudio2Error", "Failed to initialize audio. This may be an issue with your installation of XAudio 2.7. XAudio2 is available in the DirectX End-User Runtime (June 2010)."));
|
|
return false;
|
|
}
|
|
|
|
#if XAUDIO2_DEBUG_ENABLED
|
|
XAUDIO2_DEBUG_CONFIGURATION DebugConfiguration = { 0 };
|
|
DebugConfiguration.TraceMask = XAUDIO2_LOG_ERRORS | XAUDIO2_LOG_WARNINGS;
|
|
XAudio2System->SetDebugConfiguration(&DebugConfiguration, 0);
|
|
#endif // #if XAUDIO2_DEBUG_ENABLED
|
|
|
|
if (FAILED(XAudio2System->RegisterForCallbacks(this)))
|
|
{
|
|
UE_LOG(LogAudioMixer, Error, TEXT("Failed to register for callbacks."));
|
|
}
|
|
|
|
if(IAudioMixer::ShouldRecycleThreads())
|
|
{
|
|
// Pre-create the null render device thread on XAudio2, so we can simple wake it up when we need it.
|
|
// Give it nothing to do, with a slow tick as the default, but ask it to wait for a signal to wake up.
|
|
CreateNullDeviceThread([] {}, 1.0f, true);
|
|
}
|
|
|
|
bIsInitialized = true;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::TeardownHardware()
|
|
{
|
|
if (!bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 was already tore down."), Warning);
|
|
return false;
|
|
}
|
|
|
|
// Lock prior to changing state to avoid race condition if there happens to be an in-flight device swap
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
if (XAudio2System)
|
|
{
|
|
XAudio2System->UnregisterForCallbacks(this);
|
|
}
|
|
SAFE_RELEASE(XAudio2System);
|
|
|
|
#if PLATFORM_WINDOWS
|
|
if (XAudio2Dll != nullptr && IsEngineExitRequested())
|
|
{
|
|
if (!FreeLibrary(XAudio2Dll))
|
|
{
|
|
UE_LOG(LogAudio, Warning, TEXT("Failed to free XAudio2 Dll"));
|
|
}
|
|
|
|
XAudio2Dll = nullptr;
|
|
}
|
|
#endif
|
|
bIsInitialized = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::IsInitialized() const
|
|
{
|
|
return bIsInitialized;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::GetNumOutputDevices(uint32& OutNumOutputDevices)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_GetNumOutputDevices, FColor::Blue);
|
|
|
|
// Use Cache if we have it.
|
|
if (IAudioPlatformDeviceInfoCache* Cache = GetDeviceInfoCache())
|
|
{
|
|
OutNumOutputDevices = Cache->GetAllActiveOutputDevices().Num();
|
|
return true;
|
|
}
|
|
|
|
OutNumOutputDevices = 0;
|
|
|
|
if (!bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 was not initialized."), Error);
|
|
return false;
|
|
}
|
|
|
|
#if XAUDIO_SUPPORTS_DEVICE_DETAILS
|
|
|
|
IMMDeviceEnumerator* DeviceEnumerator = nullptr;
|
|
IMMDeviceCollection* DeviceCollection = nullptr;
|
|
bool bSuccess = false;
|
|
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&DeviceEnumerator)), goto Cleanup);
|
|
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &DeviceCollection), goto Cleanup);
|
|
|
|
uint32 DeviceCount;
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceCollection->GetCount(&DeviceCount), goto Cleanup);
|
|
|
|
OutNumOutputDevices = DeviceCount;
|
|
bSuccess = true;
|
|
|
|
Cleanup:
|
|
SAFE_RELEASE(DeviceCollection);
|
|
SAFE_RELEASE(DeviceEnumerator);
|
|
|
|
return bSuccess;
|
|
#else
|
|
OutNumOutputDevices = 1;
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
|
|
static bool GetMMDeviceInfo(IMMDevice* MMDevice, FAudioPlatformDeviceInfo& OutInfo)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_GetMMDeviceInfo, FColor::Blue);
|
|
|
|
check(MMDevice);
|
|
|
|
OutInfo.Reset();
|
|
|
|
bool bSuccess = false;
|
|
IPropertyStore *PropertyStore = nullptr;
|
|
WAVEFORMATEX* WaveFormatEx = nullptr;
|
|
PROPVARIANT FriendlyName;
|
|
PROPVARIANT DeviceFormat;
|
|
LPWSTR DeviceId;
|
|
|
|
check(MMDevice);
|
|
PropVariantInit(&FriendlyName);
|
|
PropVariantInit(&DeviceFormat);
|
|
|
|
// Get the device id
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(MMDevice->GetId(&DeviceId), goto Cleanup);
|
|
|
|
// Open up the property store so we can read properties from the device
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(MMDevice->OpenPropertyStore(STGM_READ, &PropertyStore), goto Cleanup);
|
|
|
|
#if PLATFORM_WINDOWS
|
|
// Grab the friendly name
|
|
PropVariantInit(&FriendlyName);
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(PropertyStore->GetValue(PKEY_Device_FriendlyName, &FriendlyName), goto Cleanup);
|
|
OutInfo.Name = FString(FriendlyName.pwszVal);
|
|
#endif
|
|
|
|
// Retrieve the DeviceFormat prop variant
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(PropertyStore->GetValue(PKEY_AudioEngine_DeviceFormat, &DeviceFormat), goto Cleanup);
|
|
|
|
// Get the format of the property
|
|
WaveFormatEx = (WAVEFORMATEX *)DeviceFormat.blob.pBlobData;
|
|
if (!WaveFormatEx)
|
|
{
|
|
// Some devices don't provide the Device format, so try the OEMFormat as well.
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(PropertyStore->GetValue(PKEY_AudioEngine_OEMFormat, &DeviceFormat), goto Cleanup);
|
|
|
|
WaveFormatEx = (WAVEFORMATEX*)DeviceFormat.blob.pBlobData;
|
|
if (!ensure(DeviceFormat.blob.pBlobData))
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
}
|
|
|
|
// We've succeeded at this point.
|
|
bSuccess = true;
|
|
|
|
OutInfo.DeviceId = FString(DeviceId);
|
|
OutInfo.NumChannels = FMath::Clamp((int32)WaveFormatEx->nChannels, 2, 8);
|
|
OutInfo.SampleRate = WaveFormatEx->nSamplesPerSec;
|
|
|
|
// XAudio2 automatically converts the audio format to output device us so we don't need to do any format conversions
|
|
OutInfo.Format = EAudioMixerStreamDataFormat::Float;
|
|
|
|
OutInfo.OutputChannelArray.Reset();
|
|
|
|
// Extensible format supports surround sound so we need to parse the channel configuration to build our channel output array
|
|
if (WaveFormatEx->wFormatTag == WAVE_FORMAT_EXTENSIBLE)
|
|
{
|
|
// Cast to the extensible format to get access to extensible data
|
|
const WAVEFORMATEXTENSIBLE* WaveFormatExtensible = (WAVEFORMATEXTENSIBLE*)WaveFormatEx;
|
|
|
|
// Loop through the extensible format channel flags in the standard order and build our output channel array
|
|
// From https://msdn.microsoft.com/en-us/library/windows/hardware/dn653308(v=vs.85).aspx
|
|
// The channels in the interleaved stream corresponding to these spatial positions must appear in the order specified above. This holds true even in the
|
|
// case of a non-contiguous subset of channels. For example, if a stream contains left, bass enhance and right, then channel 1 is left, channel 2 is right,
|
|
// and channel 3 is bass enhance. This enables the linkage of multi-channel streams to well-defined multi-speaker configurations.
|
|
|
|
uint32 ChanCount = 0;
|
|
for (uint32 ChannelTypeIndex = 0; ChannelTypeIndex < EAudioMixerChannel::ChannelTypeCount && ChanCount < (uint32)OutInfo.NumChannels; ++ChannelTypeIndex)
|
|
{
|
|
if (WaveFormatExtensible->dwChannelMask & ChannelTypeMap[ChannelTypeIndex])
|
|
{
|
|
OutInfo.OutputChannelArray.Add((EAudioMixerChannel::Type)ChannelTypeIndex);
|
|
++ChanCount;
|
|
}
|
|
}
|
|
|
|
// We didn't match channel masks for all channels, revert to a default ordering
|
|
if (ChanCount < (uint32)OutInfo.NumChannels)
|
|
{
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("Did not find the channel type flags for audio device '%s'. Reverting to a default channel ordering."), *OutInfo.Name);
|
|
|
|
OutInfo.OutputChannelArray.Reset();
|
|
|
|
static EAudioMixerChannel::Type DefaultChannelOrdering[] = {
|
|
EAudioMixerChannel::FrontLeft,
|
|
EAudioMixerChannel::FrontRight,
|
|
EAudioMixerChannel::FrontCenter,
|
|
EAudioMixerChannel::LowFrequency,
|
|
EAudioMixerChannel::SideLeft,
|
|
EAudioMixerChannel::SideRight,
|
|
EAudioMixerChannel::BackLeft,
|
|
EAudioMixerChannel::BackRight,
|
|
};
|
|
|
|
EAudioMixerChannel::Type* ChannelOrdering = DefaultChannelOrdering;
|
|
|
|
// Override channel ordering for some special cases
|
|
if (OutInfo.NumChannels == 4)
|
|
{
|
|
static EAudioMixerChannel::Type DefaultChannelOrderingQuad[] = {
|
|
EAudioMixerChannel::FrontLeft,
|
|
EAudioMixerChannel::FrontRight,
|
|
EAudioMixerChannel::BackLeft,
|
|
EAudioMixerChannel::BackRight,
|
|
};
|
|
|
|
ChannelOrdering = DefaultChannelOrderingQuad;
|
|
}
|
|
else if (OutInfo.NumChannels == 6)
|
|
{
|
|
static EAudioMixerChannel::Type DefaultChannelOrdering51[] = {
|
|
EAudioMixerChannel::FrontLeft,
|
|
EAudioMixerChannel::FrontRight,
|
|
EAudioMixerChannel::FrontCenter,
|
|
EAudioMixerChannel::LowFrequency,
|
|
EAudioMixerChannel::BackLeft,
|
|
EAudioMixerChannel::BackRight,
|
|
};
|
|
|
|
ChannelOrdering = DefaultChannelOrdering51;
|
|
}
|
|
|
|
check(OutInfo.NumChannels <= 8);
|
|
for (int32 Index = 0; Index < OutInfo.NumChannels; ++Index)
|
|
{
|
|
OutInfo.OutputChannelArray.Add(ChannelOrdering[Index]);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Non-extensible formats only support mono or stereo channel output
|
|
OutInfo.OutputChannelArray.Add(EAudioMixerChannel::FrontLeft);
|
|
if (OutInfo.NumChannels == 2)
|
|
{
|
|
OutInfo.OutputChannelArray.Add(EAudioMixerChannel::FrontRight);
|
|
}
|
|
}
|
|
|
|
Cleanup:
|
|
PropVariantClear(&FriendlyName);
|
|
PropVariantClear(&DeviceFormat);
|
|
SAFE_RELEASE(PropertyStore);
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::GetOutputDeviceInfo(const uint32 InDeviceIndex, FAudioPlatformDeviceInfo& OutInfo)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_GetOutputDeviceInfo, FColor::Blue);
|
|
|
|
// Use Cache if we have it. (index is a bad way to find the device, but we do it here).
|
|
if (IAudioPlatformDeviceInfoCache* Cache = GetDeviceInfoCache())
|
|
{
|
|
if (InDeviceIndex == AUDIO_MIXER_DEFAULT_DEVICE_INDEX)
|
|
{
|
|
if (TOptional<FAudioPlatformDeviceInfo> Defaults = Cache->FindDefaultOutputDevice())
|
|
{
|
|
OutInfo = *Defaults;
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TArray<FAudioPlatformDeviceInfo> ActiveDevices = Cache->GetAllActiveOutputDevices();
|
|
if (ActiveDevices.IsValidIndex(InDeviceIndex))
|
|
{
|
|
OutInfo = ActiveDevices[InDeviceIndex];
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (!bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 was not initialized."), Error);
|
|
return false;
|
|
}
|
|
|
|
IMMDeviceEnumerator* DeviceEnumerator = nullptr;
|
|
IMMDeviceCollection* DeviceCollection = nullptr;
|
|
IMMDevice* DefaultDevice = nullptr;
|
|
IMMDevice* Device = nullptr;
|
|
bool bIsDefault = false;
|
|
bool bSucceeded = false;
|
|
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&DeviceEnumerator)), goto Cleanup);
|
|
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &DeviceCollection), goto Cleanup);
|
|
|
|
uint32 DeviceCount;
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceCollection->GetCount(&DeviceCount), goto Cleanup);
|
|
|
|
if (DeviceCount == 0)
|
|
{
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("No available audio device"));
|
|
goto Cleanup;
|
|
}
|
|
|
|
// Get the default device
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceEnumerator->GetDefaultAudioEndpoint(eRender, eMultimedia, &DefaultDevice), goto Cleanup);
|
|
|
|
// If we are asking to get info on default device
|
|
if (InDeviceIndex == AUDIO_MIXER_DEFAULT_DEVICE_INDEX)
|
|
{
|
|
Device = DefaultDevice;
|
|
bIsDefault = true;
|
|
}
|
|
// Make sure we're not asking for a bad device index
|
|
else if (InDeviceIndex >= DeviceCount)
|
|
{
|
|
UE_LOG(LogAudioMixer, Error, TEXT("Requested device index (%d) is larger than the number of devices available (%d)"), InDeviceIndex, DeviceCount);
|
|
goto Cleanup;
|
|
}
|
|
else
|
|
{
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(DeviceCollection->Item(InDeviceIndex, &Device), goto Cleanup);
|
|
}
|
|
|
|
if (ensure(Device))
|
|
{
|
|
bSucceeded = GetMMDeviceInfo(Device, OutInfo);
|
|
|
|
// Fix up if this was a default device
|
|
if (bIsDefault)
|
|
{
|
|
OutInfo.bIsSystemDefault = true;
|
|
}
|
|
else if(DefaultDevice)
|
|
{
|
|
FAudioPlatformDeviceInfo DefaultInfo;
|
|
GetMMDeviceInfo(DefaultDevice, DefaultInfo);
|
|
OutInfo.bIsSystemDefault = OutInfo.DeviceId == DefaultInfo.DeviceId;
|
|
}
|
|
}
|
|
|
|
Cleanup:
|
|
SAFE_RELEASE(Device);
|
|
SAFE_RELEASE(DeviceCollection);
|
|
SAFE_RELEASE(DeviceEnumerator);
|
|
|
|
return bSucceeded;
|
|
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::GetDefaultOutputDeviceIndex(uint32& OutDefaultDeviceIndex) const
|
|
{
|
|
OutDefaultDeviceIndex = AUDIO_MIXER_DEFAULT_DEVICE_INDEX;
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::OpenAudioStream(const FAudioMixerOpenStreamParams& Params)
|
|
{
|
|
if (!bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 was not initialized."), Error);
|
|
return false;
|
|
}
|
|
|
|
if (bIsDeviceOpen)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 audio stream already opened."), Warning);
|
|
return false;
|
|
}
|
|
|
|
check(XAudio2System);
|
|
check(OutputAudioStreamMasteringVoice == nullptr);
|
|
|
|
WAVEFORMATEX Format = { 0 };
|
|
|
|
OpenStreamParams = Params;
|
|
|
|
AudioStreamInfo.Reset();
|
|
|
|
AudioStreamInfo.OutputDeviceIndex = OpenStreamParams.OutputDeviceIndex;
|
|
AudioStreamInfo.NumOutputFrames = OpenStreamParams.NumFrames;
|
|
AudioStreamInfo.NumBuffers = OpenStreamParams.NumBuffers;
|
|
AudioStreamInfo.AudioMixer = OpenStreamParams.AudioMixer;
|
|
|
|
uint32 NumOutputDevices = 0;
|
|
|
|
if (GetNumOutputDevices(NumOutputDevices) && NumOutputDevices > 0)
|
|
{
|
|
if (!GetOutputDeviceInfo(AudioStreamInfo.OutputDeviceIndex, AudioStreamInfo.DeviceInfo))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Store the device ID here in case it is removed. We can switch back if the device comes back.
|
|
if (Params.bRestoreIfRemoved)
|
|
{
|
|
SetOriginalAudioDeviceId(AudioStreamInfo.DeviceInfo.DeviceId);
|
|
}
|
|
|
|
// Passing the device-id to CreateMasteringVoice on a non-windows platform, will prevent
|
|
// the creation of virtualized device which handles disconnection state for us, but
|
|
// if we need to handle these errors i.e. OnCriticalError callback, we need to pass the device here.
|
|
OutputAudioStreamMasteringVoice = CreateMasteringVoice(*XAudio2System, AudioStreamInfo.DeviceInfo, ShouldUseDefaultDevice());
|
|
if (!OutputAudioStreamMasteringVoice)
|
|
{
|
|
goto Cleanup;
|
|
}
|
|
|
|
// Start the xaudio2 engine running, which will now allow us to start feeding audio to it
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(XAudio2System->StartEngine(), goto Cleanup);
|
|
|
|
// Setup the format of the output source voice
|
|
Format.nChannels = AudioStreamInfo.DeviceInfo.NumChannels;
|
|
Format.nSamplesPerSec = Params.SampleRate;
|
|
Format.wFormatTag = WAVE_FORMAT_IEEE_FLOAT;
|
|
Format.nAvgBytesPerSec = Format.nSamplesPerSec * sizeof(float) * Format.nChannels;
|
|
Format.nBlockAlign = sizeof(float) * Format.nChannels;
|
|
Format.wBitsPerSample = sizeof(float) * 8;
|
|
|
|
// Create the output source voice
|
|
XAUDIO2_CALL_AND_HANDLE_ERROR(XAudio2System->CreateSourceVoice(&OutputAudioStreamSourceVoice, &Format, XAUDIO2_VOICE_NOPITCH, 2.0f, &OutputVoiceCallback), goto Cleanup);
|
|
}
|
|
Cleanup:
|
|
|
|
bool bXAudioOpenSuccessfully = OutputAudioStreamSourceVoice && OutputAudioStreamMasteringVoice;
|
|
if (!bXAudioOpenSuccessfully)
|
|
{
|
|
// Undo anything we created.
|
|
if (OutputAudioStreamSourceVoice)
|
|
{
|
|
OutputAudioStreamSourceVoice->DestroyVoice();
|
|
OutputAudioStreamSourceVoice = nullptr;
|
|
}
|
|
if (OutputAudioStreamMasteringVoice)
|
|
{
|
|
OutputAudioStreamMasteringVoice->DestroyVoice();
|
|
OutputAudioStreamMasteringVoice = nullptr;
|
|
}
|
|
|
|
// Setup for running null device.
|
|
AudioStreamInfo.NumOutputFrames = OpenStreamParams.NumFrames;
|
|
AudioStreamInfo.DeviceInfo.OutputChannelArray = { EAudioMixerChannel::FrontLeft, EAudioMixerChannel::FrontRight };
|
|
AudioStreamInfo.DeviceInfo.NumChannels = 2;
|
|
AudioStreamInfo.DeviceInfo.SampleRate = OpenStreamParams.SampleRate;
|
|
AudioStreamInfo.DeviceInfo.Format = EAudioMixerStreamDataFormat::Float;
|
|
}
|
|
|
|
#if PLATFORM_WINDOWS || (1) // Currently all targets do this.
|
|
if(!bXAudioOpenSuccessfully)
|
|
{
|
|
// On Windows where we can have audio devices unplugged/removed/hot-swapped:
|
|
// We must mark ourselves open, even if we failed to open. This will allow the device-swap logic to run.
|
|
// StartAudioStream will happily use the null renderer path if there's no real stream open.
|
|
bXAudioOpenSuccessfully = true;
|
|
}
|
|
#endif //PLATFORM_WINDOWS
|
|
|
|
// If we opened, mark the stream as open.
|
|
if(bXAudioOpenSuccessfully)
|
|
{
|
|
AudioStreamInfo.StreamState = EAudioOutputStreamState::Open;
|
|
bIsDeviceOpen = true;
|
|
}
|
|
|
|
return bXAudioOpenSuccessfully;
|
|
}
|
|
|
|
FAudioPlatformDeviceInfo FMixerPlatformXAudio2::GetPlatformDeviceInfo() const
|
|
{
|
|
return AudioStreamInfo.DeviceInfo;
|
|
}
|
|
|
|
FString FMixerPlatformXAudio2::GetCurrentDeviceName() const
|
|
{
|
|
return AudioStreamInfo.DeviceInfo.Name;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::CloseAudioStream()
|
|
{
|
|
if (!bIsInitialized || AudioStreamInfo.StreamState == EAudioOutputStreamState::Closed)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Lock prior to changing state to avoid race condition if there happens to be an in-flight device swap
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
// If we're closing the stream, we're not interested in the results of the device swap.
|
|
// Reset the handle to the future.
|
|
ResetActiveDeviceSwapFuture();
|
|
|
|
if (bIsDeviceOpen && !StopAudioStream())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (XAudio2System)
|
|
{
|
|
XAudio2System->StopEngine();
|
|
}
|
|
|
|
if (OutputAudioStreamSourceVoice)
|
|
{
|
|
OutputAudioStreamSourceVoice->DestroyVoice();
|
|
OutputAudioStreamSourceVoice = nullptr;
|
|
}
|
|
|
|
if (OutputAudioStreamMasteringVoice)
|
|
{
|
|
OutputAudioStreamMasteringVoice->DestroyVoice();
|
|
OutputAudioStreamMasteringVoice = nullptr;
|
|
}
|
|
|
|
if (bIsUsingNullDevice)
|
|
{
|
|
StopRunningNullDevice();
|
|
}
|
|
|
|
bIsDeviceOpen = false;
|
|
|
|
AudioStreamInfo.StreamState = EAudioOutputStreamState::Closed;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::StartAudioStream()
|
|
{
|
|
UE_LOG(LogAudioMixer, Log, TEXT("FMixerPlatformXAudio2::StartAudioStream() called. InstanceID=%d"), InstanceID);
|
|
|
|
// If we already have a source voice, we can just restart it
|
|
if (OutputAudioStreamSourceVoice)
|
|
{
|
|
OutputAudioStreamSourceVoice->Start();
|
|
}
|
|
else
|
|
{
|
|
check(!bIsUsingNullDevice);
|
|
StartRunningNullDevice();
|
|
}
|
|
|
|
// Start generating audio with our output source voice
|
|
// Can be called during device swap when AudioRenderEvent can be null
|
|
if (nullptr == AudioRenderEvent)
|
|
{
|
|
// This will set AudioStreamInfo.StreamState to EAudioOutputStreamState::Running
|
|
BeginGeneratingAudio();
|
|
}
|
|
else
|
|
{
|
|
AudioStreamInfo.StreamState = EAudioOutputStreamState::Running;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::StopAudioStream()
|
|
{
|
|
if (!bIsInitialized)
|
|
{
|
|
AUDIO_PLATFORM_LOG_ONCE(TEXT("XAudio2 was not initialized."), Warning);
|
|
return false;
|
|
}
|
|
|
|
// Lock prior to changing state to avoid race condition if there happens to be an in-flight device swap
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::StopAudioStream() called. InstanceID=%d, StreamState=%d"),
|
|
InstanceID, static_cast<int32>(AudioStreamInfo.StreamState));
|
|
|
|
if (AudioStreamInfo.StreamState != EAudioOutputStreamState::Stopped && AudioStreamInfo.StreamState != EAudioOutputStreamState::Closed)
|
|
{
|
|
// Shutdown the AudioRenderThread if we're running or mid-device swap
|
|
if (AudioStreamInfo.StreamState == EAudioOutputStreamState::Running ||
|
|
AudioStreamInfo.StreamState == EAudioOutputStreamState::SwappingDevice)
|
|
{
|
|
StopGeneratingAudio();
|
|
}
|
|
|
|
// Signal that the thread that is running the update that we're stopping
|
|
if (OutputAudioStreamSourceVoice)
|
|
{
|
|
OutputAudioStreamSourceVoice->Stop(0, 0); // Don't wait for tails, stop as quick as you can.
|
|
}
|
|
|
|
check(AudioStreamInfo.StreamState == EAudioOutputStreamState::Stopped);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::CheckAudioDeviceChange()
|
|
{
|
|
#if XAUDIO_SUPPORTS_DEVICE_DETAILS
|
|
return FAudioMixerPlatformSwappable::CheckAudioDeviceChange();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::MoveAudioStreamToNewAudioDevice()
|
|
{
|
|
bool bDidStopGeneratingAudio = false;
|
|
#if XAUDIO_SUPPORTS_DEVICE_DETAILS
|
|
bDidStopGeneratingAudio = FAudioMixerPlatformSwappable::MoveAudioStreamToNewAudioDevice();
|
|
#endif // XAUDIO_SUPPORTS_DEVICE_DETAILS
|
|
return bDidStopGeneratingAudio;
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::SubmitBuffer(const uint8* Buffer)
|
|
{
|
|
SCOPED_NAMED_EVENT(FMixerPlatformXAudio2_SubmitBuffer, FColor::Blue);
|
|
|
|
if (OutputAudioStreamSourceVoice)
|
|
{
|
|
// Create a new xaudio2 buffer submission
|
|
XAUDIO2_BUFFER XAudio2Buffer = { 0 };
|
|
XAudio2Buffer.AudioBytes = OpenStreamParams.NumFrames * AudioStreamInfo.DeviceInfo.NumChannels * sizeof(float);
|
|
XAudio2Buffer.pAudioData = (const BYTE*)Buffer;
|
|
XAudio2Buffer.pContext = this;
|
|
|
|
// Submit buffer to the output streaming voice
|
|
OutputAudioStreamSourceVoice->SubmitSourceBuffer(&XAudio2Buffer);
|
|
|
|
if(!FirstBufferSubmitted)
|
|
{
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::SubmitBuffer() called for the first time. InstanceID=%d"), InstanceID);
|
|
FirstBufferSubmitted = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::InitializeDeviceSwapContext(const FString& InRequestedDeviceID, const TCHAR* InReason)
|
|
{
|
|
check(GetDeviceInfoCache());
|
|
|
|
// Look up device. Blank name looks up current default.
|
|
const FName NewDeviceName = *InRequestedDeviceID;
|
|
TOptional<FAudioPlatformDeviceInfo> DeviceInfo;
|
|
|
|
if (TOptional<FAudioPlatformDeviceInfo> TempDeviceInfo = GetDeviceInfoCache()->FindActiveOutputDevice(NewDeviceName))
|
|
{
|
|
if (TempDeviceInfo.IsSet())
|
|
{
|
|
if (IsDeviceInfoValid(*TempDeviceInfo))
|
|
{
|
|
DeviceInfo = MoveTemp(TempDeviceInfo);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("Ignoring attempt to switch to device with unsupported params: Channels=%u, SampleRate=%u, Id=%s, Name=%s"),
|
|
(uint32)TempDeviceInfo->NumChannels, (uint32)TempDeviceInfo->SampleRate, *TempDeviceInfo->DeviceId, *TempDeviceInfo->Name);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return InitDeviceSwapContextInternal(InRequestedDeviceID, InReason, DeviceInfo);
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::InitDeviceSwapContextInternal(const FString& InRequestedDeviceID, const TCHAR* InReason, const TOptional<FAudioPlatformDeviceInfo>& InDeviceInfo)
|
|
{
|
|
// Access to device swap context must be protected by DeviceSwapCriticalSection
|
|
FScopeLock Lock(&DeviceSwapCriticalSection);
|
|
|
|
if (DeviceSwapContext.IsValid())
|
|
{
|
|
UE_LOG(LogAudioMixer, Display, TEXT("FMixerPlatformXAudio2::InitDeviceSwapContextInternal DeviceSwapContext in-flight, ignoring"));
|
|
return false;
|
|
}
|
|
|
|
// Create the device swap context which will be valid over the course of the swap
|
|
DeviceSwapContext = MakeUnique<FXAudio2DeviceSwapContext>(InRequestedDeviceID, InReason);
|
|
if (!DeviceSwapContext.IsValid())
|
|
{
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("FMixerPlatformXAudio2::InitDeviceSwapContextInternal failed to create DeviceSwapContext"));
|
|
return false;
|
|
}
|
|
|
|
DeviceSwapContext->NewDevice = InDeviceInfo;
|
|
DeviceSwapContext->bUseDefaultDevice = ShouldUseDefaultDevice();
|
|
|
|
return true;
|
|
}
|
|
|
|
FString FMixerPlatformXAudio2::GetDefaultDeviceName()
|
|
{
|
|
return FString();
|
|
}
|
|
|
|
FAudioPlatformSettings FMixerPlatformXAudio2::GetPlatformSettings() const
|
|
{
|
|
#if WITH_ENGINE
|
|
return FAudioPlatformSettings::GetPlatformSettings(FPlatformProperties::GetRuntimeSettingsClassName());
|
|
#else
|
|
return FAudioPlatformSettings();
|
|
#endif // WITH_ENGINE
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::OnHardwareUpdate()
|
|
{
|
|
}
|
|
|
|
Audio::IAudioPlatformDeviceInfoCache* FMixerPlatformXAudio2::GetDeviceInfoCache() const
|
|
{
|
|
if (ShouldUseDeviceInfoCache())
|
|
{
|
|
return DeviceInfoCache.Get();
|
|
}
|
|
// Disabled.
|
|
return nullptr;
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::IsDeviceInfoValid(const FAudioPlatformDeviceInfo& InDeviceInfo) const
|
|
{
|
|
if (InDeviceInfo.NumChannels <= XAUDIO2_MAX_AUDIO_CHANNELS &&
|
|
InDeviceInfo.SampleRate >= XAUDIO2_MIN_SAMPLE_RATE &&
|
|
InDeviceInfo.SampleRate <= XAUDIO2_MAX_SAMPLE_RATE)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::OnSessionDisconnect(IAudioMixerDeviceChangedListener::EDisconnectReason InReason)
|
|
{
|
|
// Device has disconnected from current session.
|
|
if (InReason == IAudioMixerDeviceChangedListener::EDisconnectReason::FormatChanged)
|
|
{
|
|
// OnFormatChanged, retry again same device.
|
|
RequestDeviceSwap(GetDeviceId(), /*force*/ true, TEXT("FMixerPlatformXAudio2::OnSessionDisconnect() - FormatChanged"));
|
|
}
|
|
else if (InReason == IAudioMixerDeviceChangedListener::EDisconnectReason::DeviceRemoval)
|
|
{
|
|
// Ignore Device Removal, as this is handle by the Device Removal logic in the Notification Client.
|
|
}
|
|
else
|
|
{
|
|
// ServerShutdown, SessionLogoff, SessionDisconnected, ExclusiveModeOverride
|
|
// Attempt a default swap, will likely fail, but then we'll switch to a null device.
|
|
RequestDeviceSwap(TEXT(""), /*force*/ true, TEXT("FMixerPlatformXAudio2::OnSessionDisconnect() - Other"));
|
|
}
|
|
}
|
|
|
|
bool FMixerPlatformXAudio2::DisablePCMAudioCaching() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
IXAudio2MasteringVoice* FMixerPlatformXAudio2::CreateMasteringVoice(IXAudio2& InXAudio2System, const FAudioPlatformDeviceInfo& NewDevice, bool bUseDefaultDevice)
|
|
{
|
|
IXAudio2MasteringVoice* MasteringVoice = nullptr;
|
|
const TCHAR* DeviceId = bUseDefaultDevice ? nullptr : *NewDevice.DeviceId;
|
|
const HRESULT Result = InXAudio2System.CreateMasteringVoice(
|
|
&MasteringVoice,
|
|
NewDevice.NumChannels,
|
|
NewDevice.SampleRate,
|
|
0,
|
|
DeviceId,
|
|
nullptr,
|
|
AudioCategory_GameEffects);
|
|
if (FAILED(Result))
|
|
{
|
|
if (MasteringVoice)
|
|
{
|
|
// Probably unreachable, but just to be safe...
|
|
MasteringVoice->DestroyVoice();
|
|
MasteringVoice = nullptr;
|
|
}
|
|
|
|
const TCHAR* DeviceIdDebug = DeviceId ? DeviceId : TEXT("(default)");
|
|
UE_LOG(LogAudioMixer, Error, TEXT("CreateMasteringVoice failed with result 0x%X: %s (line: %d) with Args (NumChannels=%u, SampleRate=%u, DeviceID=%s, Name=%s)"),
|
|
Result, *Audio::ToErrorFString(Result), __LINE__, NewDevice.NumChannels, NewDevice.SampleRate, DeviceIdDebug, *NewDevice.Name);
|
|
}
|
|
|
|
return MasteringVoice;
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::OnProcessingPassStart()
|
|
{
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::OnProcessingPassEnd()
|
|
{
|
|
}
|
|
|
|
void FMixerPlatformXAudio2::OnCriticalError(HRESULT Error)
|
|
{
|
|
// Windows should handle this via session events, log if we receive one.
|
|
UE_LOG(LogAudioMixer, Warning, TEXT("FMixerPlatformXAudio2::OnCriticalError: 0x%X: %s"), Error, *Audio::ToErrorFString(Error));
|
|
}
|
|
|
|
}
|
|
|