Files
UnrealEngineUWP/Engine/Source/Runtime/Windows/UnrealAudioWasapi/Private/UnrealAudioDeviceWasapi.cpp
ryan durand 0f0464a30e Updating copyright for Engine Runtime.
#rnx
#rb none


#ROBOMERGE-OWNER: ryan.durand
#ROBOMERGE-AUTHOR: ryan.durand
#ROBOMERGE-SOURCE: CL 10869210 via CL 10869511 via CL 10869900
#ROBOMERGE-BOT: (v613-10869866)

[CL 10870549 by ryan durand in Main branch]
2019-12-26 14:45:42 -05:00

859 lines
27 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/**
Concrete implementation of IAudioDevice for WASAPI (Windows Audio Session API)
See https://msdn.microsoft.com/en-us/library/windows/desktop/dd371455%28v=vs.85%29.aspx
*/
#include "UnrealAudioDeviceModule.h"
#include "UnrealAudioBuffer.h"
#include "Windows/AllowWindowsPlatformTypes.h"
#include <Audioclient.h> // WASAPI api
#include <avrt.h> // This module contains the multimedia class scheduler APIs and any public data structures needed to call these APIs.
#include <mmdeviceapi.h> // For getting endpoint notifications about audio devices
#include <functiondiscoverykeys_devpkey.h> // Defines property keys for the Plug and Play Device Property API.
#include "Windows/HideWindowsPlatformTypes.h"
#if ENABLE_UNREAL_AUDIO
// See MSDN documentation for what these error codes mean in the context of the API call
static const TCHAR* GetWasapiError(HRESULT Result)
{
switch (Result)
{
case AUDCLNT_E_NOT_INITIALIZED: return TEXT("AUDCLNT_E_NOT_INITIALIZED");
case AUDCLNT_E_ALREADY_INITIALIZED: return TEXT("AUDCLNT_E_ALREADY_INITIALIZED");
case AUDCLNT_E_WRONG_ENDPOINT_TYPE: return TEXT("AUDCLNT_E_WRONG_ENDPOINT_TYPE");
case AUDCLNT_E_DEVICE_INVALIDATED: return TEXT("AUDCLNT_E_DEVICE_INVALIDATED");
case AUDCLNT_E_NOT_STOPPED: return TEXT("AUDCLNT_E_NOT_STOPPED");
case AUDCLNT_E_BUFFER_TOO_LARGE: return TEXT("AUDCLNT_E_BUFFER_TOO_LARGE");
case AUDCLNT_E_OUT_OF_ORDER: return TEXT("AUDCLNT_E_OUT_OF_ORDER");
case AUDCLNT_E_UNSUPPORTED_FORMAT: return TEXT("AUDCLNT_E_UNSUPPORTED_FORMAT");
case AUDCLNT_E_INVALID_SIZE: return TEXT("AUDCLNT_E_INVALID_SIZE");
case AUDCLNT_E_DEVICE_IN_USE: return TEXT("AUDCLNT_E_DEVICE_IN_USE");
case AUDCLNT_E_BUFFER_OPERATION_PENDING: return TEXT("AUDCLNT_E_BUFFER_OPERATION_PENDING");
case AUDCLNT_E_THREAD_NOT_REGISTERED: return TEXT("AUDCLNT_E_THREAD_NOT_REGISTERED");
case AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED: return TEXT("AUDCLNT_E_EXCLUSIVE_MODE_NOT_ALLOWED");
case AUDCLNT_E_ENDPOINT_CREATE_FAILED: return TEXT("AUDCLNT_E_ENDPOINT_CREATE_FAILED");
case AUDCLNT_E_SERVICE_NOT_RUNNING: return TEXT("AUDCLNT_E_SERVICE_NOT_RUNNING");
case AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED: return TEXT("AUDCLNT_E_EVENTHANDLE_NOT_EXPECTED");
case AUDCLNT_E_EXCLUSIVE_MODE_ONLY: return TEXT("AUDCLNT_E_EXCLUSIVE_MODE_ONLY");
case AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL: return TEXT("AUDCLNT_E_BUFDURATION_PERIOD_NOT_EQUAL");
case AUDCLNT_E_EVENTHANDLE_NOT_SET: return TEXT("AUDCLNT_E_EVENTHANDLE_NOT_SET");
case AUDCLNT_E_INCORRECT_BUFFER_SIZE: return TEXT("AUDCLNT_E_INCORRECT_BUFFER_SIZE");
case AUDCLNT_E_BUFFER_SIZE_ERROR: return TEXT("AUDCLNT_E_BUFFER_SIZE_ERROR");
case AUDCLNT_E_CPUUSAGE_EXCEEDED: return TEXT("AUDCLNT_E_CPUUSAGE_EXCEEDED");
case AUDCLNT_E_BUFFER_ERROR: return TEXT("AUDCLNT_E_BUFFER_ERROR");
case AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED: return TEXT("AUDCLNT_E_BUFFER_SIZE_NOT_ALIGNED");
case AUDCLNT_E_INVALID_DEVICE_PERIOD: return TEXT("AUDCLNT_E_INVALID_DEVICE_PERIOD");
case AUDCLNT_E_INVALID_STREAM_FLAG: return TEXT("AUDCLNT_E_INVALID_STREAM_FLAG");
case AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE: return TEXT("AUDCLNT_E_ENDPOINT_OFFLOAD_NOT_CAPABLE");
case AUDCLNT_E_OUT_OF_OFFLOAD_RESOURCES: return TEXT("AUDCLNT_E_OUT_OF_OFFLOAD_RESOURCES");
case AUDCLNT_E_OFFLOAD_MODE_ONLY: return TEXT("AUDCLNT_E_OFFLOAD_MODE_ONLY");
case AUDCLNT_E_NONOFFLOAD_MODE_ONLY: return TEXT("AUDCLNT_E_NONOFFLOAD_MODE_ONLY");
case AUDCLNT_E_RESOURCES_INVALIDATED: return TEXT("AUDCLNT_E_RESOURCES_INVALIDATED");
case AUDCLNT_E_RAW_MODE_UNSUPPORTED: return TEXT("AUDCLNT_E_RAW_MODE_UNSUPPORTED");
case REGDB_E_CLASSNOTREG: return TEXT("REGDB_E_CLASSNOTREG");
case CLASS_E_NOAGGREGATION: return TEXT("CLASS_E_NOAGGREGATION");
case E_NOINTERFACE: return TEXT("E_NOINTERFACE");
case E_POINTER: return TEXT("E_POINTER");
case E_INVALIDARG: return TEXT("E_INVALIDARG");
case E_OUTOFMEMORY: return TEXT("E_OUTOFMEMORY");
default: return TEXT("UKNOWN");
}
}
/** Some helper defines to reduce boilerplate code for error handling windows API calls. */
#define CLEANUP_ON_FAIL(Result) \
if (FAILED(Result)) \
{ \
const TCHAR* Err = GetWasapiError(Result); \
UA_DEVICE_PLATFORM_ERROR(Err); \
goto Cleanup; \
}
#define RETURN_FALSE_ON_FAIL(Result) \
if (FAILED(Result)) \
{ \
const TCHAR* Err = GetWasapiError(Result); \
UA_DEVICE_PLATFORM_ERROR(Err); \
return false; \
}
namespace UAudio
{
/** Struct used to map Unreal Audio enumerations for speaker types to device speaker types */
struct FSpeakerMaskMapping
{
ESpeaker::Type UnrealSpeaker;
long XAudio2SpeakerTypeFlag;
};
/** Mapping of XAudio2/Windows speaker type enumerations and unreal audio enumerations */
FSpeakerMaskMapping SpeakerMaskMapping[18] =
{
{ ESpeaker::FRONT_LEFT, SPEAKER_FRONT_LEFT },
{ ESpeaker::FRONT_RIGHT, SPEAKER_FRONT_RIGHT },
{ ESpeaker::FRONT_CENTER, SPEAKER_FRONT_CENTER },
{ ESpeaker::LOW_FREQUENCY, SPEAKER_LOW_FREQUENCY },
{ ESpeaker::BACK_LEFT, SPEAKER_BACK_LEFT },
{ ESpeaker::BACK_RIGHT, SPEAKER_BACK_RIGHT },
{ ESpeaker::FRONT_LEFT_OF_CENTER, SPEAKER_FRONT_LEFT_OF_CENTER },
{ ESpeaker::FRONT_RIGHT_OF_CENTER, SPEAKER_FRONT_RIGHT_OF_CENTER },
{ ESpeaker::BACK_CENTER, SPEAKER_BACK_CENTER },
{ ESpeaker::SIDE_LEFT, SPEAKER_SIDE_LEFT },
{ ESpeaker::SIDE_RIGHT, SPEAKER_SIDE_RIGHT },
{ ESpeaker::TOP_CENTER, SPEAKER_TOP_CENTER },
{ ESpeaker::TOP_FRONT_LEFT, SPEAKER_TOP_FRONT_LEFT },
{ ESpeaker::TOP_FRONT_CENTER, SPEAKER_TOP_FRONT_CENTER },
{ ESpeaker::TOP_FRONT_RIGHT, SPEAKER_TOP_FRONT_RIGHT },
{ ESpeaker::TOP_BACK_LEFT, SPEAKER_TOP_BACK_LEFT },
{ ESpeaker::TOP_BACK_CENTER, SPEAKER_TOP_BACK_CENTER },
{ ESpeaker::TOP_BACK_RIGHT, SPEAKER_TOP_BACK_RIGHT },
};
/** Helper function which turns a windows speaker flag mask into an array of speaker types. */
static void GetArrayOfSpeakers(const uint32 NumChannels, TArray<ESpeaker::Type>& Speakers, const ::DWORD ChannelMask)
{
Speakers.Reset();
uint32 ChanCount = 0;
// Build a flag field of the speaker outputs of this device
for (uint32 SpeakerTypeIndex = 0; SpeakerTypeIndex < ESpeaker::SPEAKER_TYPE_COUNT && ChanCount < NumChannels; ++SpeakerTypeIndex)
{
if (ChannelMask & SpeakerMaskMapping[SpeakerTypeIndex].XAudio2SpeakerTypeFlag)
{
++ChanCount;
Speakers.Add(SpeakerMaskMapping[SpeakerTypeIndex].UnrealSpeaker);
}
}
check(ChanCount == NumChannels);
}
/**
* FUnrealAudioWasapi
* Wasapi implementation of IUnrealAudioDeviceModule
* Inherits from FRunnable so that audio device I/O can be run on separate thread.
*/
class FUnrealAudioWasapi : public IUnrealAudioDeviceModule,
public FRunnable
{
public:
FUnrealAudioWasapi();
~FUnrealAudioWasapi();
// IUnrealAudioDeviceModule
bool Initialize() override;
bool Shutdown() override;
bool GetDevicePlatformApi(EDeviceApi::Type & OutType) const override;
bool GetNumOutputDevices(uint32& OutNumDevices) const override;
bool GetOutputDeviceInfo(const uint32 DeviceIndex, FDeviceInfo& OutInfo) const override;
bool GetDefaultOutputDeviceIndex(uint32& OutDefaultIndex) const override;
bool StartStream() override;
bool StopStream() override;
bool ShutdownStream() override;
bool GetLatency(uint32& OutputDeviceLatency) const override;
bool GetFrameRate(uint32& OutFrameRate) const override;
// FRunnable
uint32 Run() override;
void Stop() override;
void Exit() override;
private:
/**
* FWasapiInfo
*
* Structure for holding WASAPI specific resources
*
*/
struct FWasapiInfo
{
/** The windows device enumerator. Used to query connected audio devices. */
IMMDeviceEnumerator* DeviceEnumerator;
/** Allows creation and initialization of audio output stream from audio engine and hardware buffer of audio endpoint device*/
IAudioClient* RenderClient;
/** Allows writing data output to capture endpoint buffer */
IAudioRenderClient* RenderService;
/** Handle used to notify when hardware is ready for new audio data to be written to it*/
HANDLE RenderEvent;
/** Intermediate buffer used to store audio data from user callback, converted to hardware native format*/
IIntermediateBuffer* RenderIntermediateBuffer;
/** Whether or not devices are open */
bool bDevicesOpen;
FWasapiInfo()
: DeviceEnumerator(nullptr)
, RenderClient(nullptr)
, RenderService(nullptr)
, RenderEvent(0)
, RenderIntermediateBuffer(nullptr)
, bDevicesOpen(false)
{
}
};
/** The number of active output devices detected by WASAPI */
uint32 OutputDeviceCount;
/** WASAPI-specific data */
FWasapiInfo WasapiInfo;
/** Whether or not the device api has been initialized */
bool bInitialized;
/** Whether or not com was initialized */
bool bComInitialized;
private:
// IUnrealAudioDeviceModule
bool OpenDevice(const FCreateStreamParams& CreateStreamParams) override;
// Helper functions
bool OpenDevice(uint32 DeviceIndex);
bool GetDeviceCount(uint32& NumDevices) const;
bool GetDeviceInfo(const uint32 DeviceINdex, FDeviceInfo& DeviceInfo) const;
bool GetDefaultDeviceIndex(uint32& OutDeviceIndex) const;
void GetDeviceBufferSize(uint32& DeviceBufferSize);
};
/**
* FUnrealAudioWasapi Implementation
*/
FUnrealAudioWasapi::FUnrealAudioWasapi()
: OutputDeviceCount(0)
, bInitialized(false)
, bComInitialized(false)
{
}
FUnrealAudioWasapi::~FUnrealAudioWasapi()
{
}
bool FUnrealAudioWasapi::Initialize()
{
bComInitialized = FWindowsPlatformMisc::CoInitialize();
HRESULT Result = CoCreateInstance(__uuidof(MMDeviceEnumerator), NULL, CLSCTX_ALL, IID_PPV_ARGS(&WasapiInfo.DeviceEnumerator));
RETURN_FALSE_ON_FAIL(Result);
bInitialized = true;
return true;
}
bool FUnrealAudioWasapi::Shutdown()
{
if (!bInitialized)
{
return false;
}
SAFE_RELEASE(WasapiInfo.DeviceEnumerator);
if (bComInitialized)
{
FWindowsPlatformMisc::CoUninitialize();
}
return true;
}
bool FUnrealAudioWasapi::GetDevicePlatformApi(EDeviceApi::Type& OutType) const
{
OutType = EDeviceApi::WASAPI;
return true;
}
bool FUnrealAudioWasapi::GetNumOutputDevices(uint32& OutNumDevices) const
{
return GetDeviceCount(OutNumDevices);
}
bool FUnrealAudioWasapi::GetDeviceCount(uint32& OutNumDevices) const
{
if (!bInitialized || !WasapiInfo.DeviceEnumerator)
{
return false;
}
IMMDeviceCollection* Devices = nullptr;
HRESULT Result = S_OK;
Result = WasapiInfo.DeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &Devices);
CLEANUP_ON_FAIL(Result);
Result = Devices->GetCount(&OutNumDevices);
CLEANUP_ON_FAIL(Result);
Cleanup:
SAFE_RELEASE(Devices);
return SUCCEEDED(Result);
}
bool FUnrealAudioWasapi::GetOutputDeviceInfo(const uint32 DeviceIndex, FDeviceInfo& OutInfo) const
{
return GetDeviceInfo(DeviceIndex, OutInfo);
}
bool FUnrealAudioWasapi::GetDeviceInfo(const uint32 DeviceIndex, FDeviceInfo& DeviceInfo) const
{
if (!bInitialized || !WasapiInfo.DeviceEnumerator)
{
return false;
}
IMMDeviceCollection* Devices = nullptr;
IMMDevice* DefaultDevice = nullptr;
IMMDevice* Device = nullptr;
IPropertyStore* DevicePropertyStore = nullptr;
IPropertyStore* DefaultDevicePropertyStore = nullptr;
IAudioClient* WasapiAudioClient = nullptr;
PROPVARIANT DeviceNameProperty;
PROPVARIANT DefaultDeviceNameProperty;
WAVEFORMATEX* WaveFormatEx = nullptr;
HRESULT Result = S_OK;
Result = WasapiInfo.DeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &Devices);
CLEANUP_ON_FAIL(Result);
uint32 NumDevices = 0;
Result = Devices->GetCount(&NumDevices);
CLEANUP_ON_FAIL(Result);
Result = WasapiInfo.DeviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &DefaultDevice);
CLEANUP_ON_FAIL(Result);
Result = Devices->Item(DeviceIndex, &Device);
CLEANUP_ON_FAIL(Result);
Result = Device->OpenPropertyStore(STGM_READ, &DevicePropertyStore);
CLEANUP_ON_FAIL(Result);
Result = DefaultDevice->OpenPropertyStore(STGM_READ, &DefaultDevicePropertyStore);
CLEANUP_ON_FAIL(Result);
Result = DevicePropertyStore->GetValue(PKEY_Device_FriendlyName, &DeviceNameProperty);
CLEANUP_ON_FAIL(Result);
Result = DefaultDevicePropertyStore->GetValue(PKEY_Device_FriendlyName, &DefaultDeviceNameProperty);
CLEANUP_ON_FAIL(Result);
Result = Device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, NULL, (void **)&WasapiAudioClient);
CLEANUP_ON_FAIL(Result);
Result = WasapiAudioClient->GetMixFormat(&WaveFormatEx);
CLEANUP_ON_FAIL(Result);
// At this point we've succeeded in getting all the device objects from WindowsMM that we need
DeviceInfo.FriendlyName = FString(DeviceNameProperty.pwszVal);
DeviceInfo.bIsSystemDefault = (DeviceInfo.FriendlyName == FString(DefaultDeviceNameProperty.pwszVal));
DeviceInfo.NumChannels = WaveFormatEx->nChannels;
DeviceInfo.FrameRate = WaveFormatEx->nSamplesPerSec;
// Figure out native supported data formats of the device
DeviceInfo.StreamFormat = EStreamFormat::UNSUPPORTED;
bool bIsFloat = (WaveFormatEx->wFormatTag == WAVE_FORMAT_IEEE_FLOAT);
bool bIsExtensible = (WaveFormatEx->wFormatTag == WAVE_FORMAT_EXTENSIBLE);
bool bIsSubtypeFloat = (bIsExtensible && (((WAVEFORMATEXTENSIBLE*)WaveFormatEx)->SubFormat == KSDATAFORMAT_SUBTYPE_IEEE_FLOAT));
if (bIsFloat || bIsSubtypeFloat)
{
if (WaveFormatEx->wBitsPerSample == 32)
{
DeviceInfo.StreamFormat = EStreamFormat::FLT;
}
else if (WaveFormatEx->wBitsPerSample == 64)
{
DeviceInfo.StreamFormat = EStreamFormat::DBL;
}
}
else
{
bool bIsPcm = (WaveFormatEx->wFormatTag == WAVE_FORMAT_PCM);
bool bIsSubtypePcm = (bIsExtensible && (((WAVEFORMATEXTENSIBLE*)WaveFormatEx)->SubFormat == KSDATAFORMAT_SUBTYPE_PCM));
if (bIsPcm || bIsSubtypePcm)
{
if (WaveFormatEx->wBitsPerSample == 16)
{
DeviceInfo.StreamFormat = EStreamFormat::INT_16;
}
else if (WaveFormatEx->wBitsPerSample == 24)
{
DeviceInfo.StreamFormat = EStreamFormat::INT_24;
}
else if (WaveFormatEx->wBitsPerSample == 32)
{
DeviceInfo.StreamFormat = EStreamFormat::INT_32;
}
else
{
goto Cleanup;
}
}
}
DeviceInfo.Speakers.Reset();
uint32 ChanCount = 0;
if (bIsExtensible)
{
// Build a flag field of the speaker outputs of this device
WAVEFORMATEXTENSIBLE* WaveFormatExtensible = (WAVEFORMATEXTENSIBLE*)WaveFormatEx;
GetArrayOfSpeakers(DeviceInfo.NumChannels, DeviceInfo.Speakers, WaveFormatExtensible->dwChannelMask);
}
else
{
// Non-extensible formats only support 1 or 2 channels
DeviceInfo.Speakers.Add(ESpeaker::FRONT_LEFT);
if (DeviceInfo.NumChannels == 2)
{
DeviceInfo.Speakers.Add(ESpeaker::FRONT_RIGHT);
}
}
Cleanup:
// Release all our resources that we used above
SAFE_RELEASE(Devices);
SAFE_RELEASE(DefaultDevice);
SAFE_RELEASE(Device);
SAFE_RELEASE(DevicePropertyStore);
SAFE_RELEASE(DefaultDevicePropertyStore);
PropVariantClear(&DeviceNameProperty);
PropVariantClear(&DefaultDeviceNameProperty);
CoTaskMemFree(WaveFormatEx);
return SUCCEEDED(Result);
}
bool FUnrealAudioWasapi::GetDefaultOutputDeviceIndex(uint32& OutDefaultIndex) const
{
return GetDefaultDeviceIndex(OutDefaultIndex);
}
bool FUnrealAudioWasapi::GetDefaultDeviceIndex(uint32& OutDeviceIndex) const
{
uint32 NumDevices = 0;
if (!GetDeviceCount(NumDevices))
{
return false;
}
bool bFoundDeviceIndex = false;
for (uint32 DeviceIndex = 0; DeviceIndex < NumDevices; ++DeviceIndex)
{
FDeviceInfo DeviceInfo;
if (!GetDeviceInfo(DeviceIndex, DeviceInfo))
{
return false;
}
if (DeviceInfo.bIsSystemDefault)
{
bFoundDeviceIndex = true;
OutDeviceIndex = DeviceIndex;
break;
}
}
return bFoundDeviceIndex;
}
bool FUnrealAudioWasapi::OpenDevice(const FCreateStreamParams& CreateStreamParams)
{
if (!bInitialized || WasapiInfo.bDevicesOpen)
{
return false;
}
StreamInfo.BlockSize = CreateStreamParams.CallbackBlockSize;
StreamInfo.FrameRate = INDEX_NONE;
check(CreateStreamParams.OutputDeviceIndex != INDEX_NONE);
if (!OpenDevice(CreateStreamParams.OutputDeviceIndex))
{
return false;
}
StreamInfo.FrameRate = FMath::Min(StreamInfo.FrameRate, StreamInfo.DeviceInfo.FrameRate);
return true;
}
bool FUnrealAudioWasapi::OpenDevice(uint32 DeviceIndex)
{
check(WasapiInfo.DeviceEnumerator);
IMMDevice* Device = nullptr;
IMMDeviceCollection* DeviceList = nullptr;
WAVEFORMATEX* DeviceFormat = nullptr;
FDeviceInfo DeviceInfo;
HRESULT Result = S_OK;
Result = WasapiInfo.DeviceEnumerator->EnumAudioEndpoints(eRender, DEVICE_STATE_ACTIVE, &DeviceList);
CLEANUP_ON_FAIL(Result);
if (!GetDeviceInfo(DeviceIndex, DeviceInfo))
{
Result = S_FALSE;
goto Cleanup;
}
Result = DeviceList->Item(DeviceIndex, &Device);
CLEANUP_ON_FAIL(Result);
FStreamDeviceInfo& StreamDeviceInfo = StreamInfo.DeviceInfo;
StreamDeviceInfo.DeviceIndex = DeviceIndex;
IAudioClient*& AudioClient = WasapiInfo.RenderClient;
check(AudioClient == nullptr);
Result = Device->Activate(__uuidof(IAudioClient), CLSCTX_ALL, nullptr, (void**)&AudioClient);
CLEANUP_ON_FAIL(Result);
Result = AudioClient->GetMixFormat(&DeviceFormat);
CLEANUP_ON_FAIL(Result);
StreamDeviceInfo.bPerformByteSwap = false;
StreamDeviceInfo.NumChannels = DeviceFormat->nChannels;
StreamDeviceInfo.DeviceDataFormat = DeviceInfo.StreamFormat;
StreamDeviceInfo.FrameRate = DeviceInfo.FrameRate;
StreamDeviceInfo.Speakers = DeviceInfo.Speakers;
if (StreamDeviceInfo.DeviceDataFormat != EStreamFormat::FLT)
{
StreamDeviceInfo.bPerformFormatConversion = true;
}
SetupBufferFormatConvertInfo();
// Initialize the raw buffer used to write into
uint32 BufferSize = StreamDeviceInfo.NumChannels * StreamInfo.BlockSize * sizeof(float);
StreamDeviceInfo.UserBuffer.Init(0, BufferSize);
Cleanup:
SAFE_RELEASE(DeviceList);
SAFE_RELEASE(Device);
CoTaskMemFree(DeviceFormat);
if (FAILED(Result))
{
ShutdownStream();
}
return SUCCEEDED(Result);
}
bool FUnrealAudioWasapi::StartStream()
{
if (!bInitialized || StreamInfo.State == EStreamState::CLOSED || StreamInfo.State == EStreamState::RUNNING)
{
return false;
}
check(StreamInfo.Thread == nullptr);
StreamInfo.Thread = FRunnableThread::Create(this, TEXT("WasapiDeviceThread"), 0, TPri_AboveNormal);
return true;
}
bool FUnrealAudioWasapi::StopStream()
{
if (!bInitialized || StreamInfo.State == EStreamState::CLOSED || StreamInfo.State == EStreamState::STOPPED || StreamInfo.State == EStreamState::STOPPING)
{
return false;
}
check(StreamInfo.Thread != nullptr);
check(WasapiInfo.RenderClient != nullptr);
// Tell the stream update that we are stopping
Stop();
// Wait for the thread to finish
StreamInfo.Thread->WaitForCompletion();
check(StreamInfo.State == EStreamState::STOPPED);
// delete the thread
delete StreamInfo.Thread;
StreamInfo.Thread = nullptr;
// clean up any audio resources
HRESULT Result = S_OK;
bool bSuccess = true;
if (WasapiInfo.RenderClient)
{
Result = WasapiInfo.RenderClient->Stop();
if (FAILED(Result))
{
bSuccess = false;
UA_DEVICE_PLATFORM_ERROR("Failed to stop render client.");
}
}
return true;
}
bool FUnrealAudioWasapi::ShutdownStream()
{
if (!bInitialized || StreamInfo.State == EStreamState::CLOSED)
{
return false;
}
if (StreamInfo.State != EStreamState::STOPPED)
{
StopStream();
}
SAFE_RELEASE(WasapiInfo.RenderClient);
SAFE_RELEASE(WasapiInfo.RenderService);
if (WasapiInfo.RenderEvent)
{
CloseHandle(WasapiInfo.RenderEvent);
WasapiInfo.RenderEvent = nullptr;
}
StreamInfo.State = EStreamState::CLOSED;
return true;
}
bool FUnrealAudioWasapi::GetLatency(uint32& OutputDeviceLatency) const
{
OutputDeviceLatency = StreamInfo.DeviceInfo.Latency;
return true;
}
bool FUnrealAudioWasapi::GetFrameRate(uint32& OutFrameRate) const
{
OutFrameRate = StreamInfo.FrameRate;
return true;
}
// FRunnable
uint32 FUnrealAudioWasapi::Run()
{
// This is on a new thread, so lets initialize com again in case it wasn't already
bool bThreadComInitialized = FWindowsPlatformMisc::CoInitialize();
// Get local stack versions of various wasapi info we need for the device thread
WAVEFORMATEX* OutputFormat = nullptr;
BYTE* DeviceByteBuffer = nullptr;
FStreamDeviceInfo& OutputDeviceInfo = StreamInfo.DeviceInfo;
uint32 NumBytesRenderFormat = GetNumBytesForFormat(OutputDeviceInfo.DeviceDataFormat);
uint32 OutputDeviceSamples = StreamInfo.BlockSize * OutputDeviceInfo.NumChannels;
bool bWasUserCallbackMade = false;
bool bWasUserBufferWritten = false;
HRESULT Result = S_OK;
FCallbackInfo CallbackInfo;
StreamInfo.State = EStreamState::RUNNING;
// Number used to compute the REFERENCE_TIME for callback periods (REF_TIME is 100 nanoseconds)
static const uint64 REF_TIMES_PER_SECOND = 10000000;
// We haven't set up an actual render service yet
check(WasapiInfo.RenderService == nullptr);
Result = WasapiInfo.RenderClient->GetMixFormat(&OutputFormat);
CLEANUP_ON_FAIL(Result);
// Compute the callback buffer period using the buffer size
REFERENCE_TIME CallbackBufferPeriod = (REFERENCE_TIME)((StreamInfo.BlockSize * REF_TIMES_PER_SECOND) / OutputFormat->nSamplesPerSec);
Result = WasapiInfo.RenderClient->Initialize(
AUDCLNT_SHAREMODE_SHARED, // Other clients can use this input device
AUDCLNT_STREAMFLAGS_EVENTCALLBACK, // Processing of the buffer by client is event driven
CallbackBufferPeriod, // the size of the buffer in 100-nanosecond units (REF_TIME)
0, // always 0 in shared mode
OutputFormat, // the output format to use
nullptr // audio session guid, we're ignoring
);
CLEANUP_ON_FAIL(Result);
// Get the output latency
REFERENCE_TIME OutputLatency = 0;
WasapiInfo.RenderClient->GetStreamLatency(&OutputLatency);
OutputDeviceInfo.Latency = (1000 * OutputLatency) / REF_TIMES_PER_SECOND;
Result = WasapiInfo.RenderClient->GetService(__uuidof(IAudioRenderClient), (void**)&WasapiInfo.RenderService);
CLEANUP_ON_FAIL(Result);
WasapiInfo.RenderEvent = CreateEvent(nullptr, false, false, nullptr);
if (!WasapiInfo.RenderEvent)
{
Result = HRESULT_FROM_WIN32(GetLastError());
CLEANUP_ON_FAIL(Result);
}
Result = WasapiInfo.RenderClient->SetEventHandle(WasapiInfo.RenderEvent);
CLEANUP_ON_FAIL(Result);
uint32 ReadBufferSize = 0;
Result = WasapiInfo.RenderClient->GetBufferSize(&ReadBufferSize);
CLEANUP_ON_FAIL(Result);
uint32 WriteBufferSize = StreamInfo.BlockSize* OutputDeviceInfo.NumChannels;
ReadBufferSize *= OutputDeviceInfo.NumChannels;
WasapiInfo.RenderIntermediateBuffer = IIntermediateBuffer::CreateIntermediateBuffer(OutputDeviceInfo.DeviceDataFormat);
check(WasapiInfo.RenderIntermediateBuffer);
WasapiInfo.RenderIntermediateBuffer->Initialize(ReadBufferSize + WriteBufferSize);
Result = WasapiInfo.RenderClient->Reset();
CLEANUP_ON_FAIL(Result);
Result = WasapiInfo.RenderClient->Start();
CLEANUP_ON_FAIL(Result);
// Set up the device buffer (buffer read from and written directly to the device in the native device format)
// If we have an input stream, set up our device buffer size to accommodate
uint32 DeviceBufferSize = 0;
GetDeviceBufferSize(DeviceBufferSize);
StreamInfo.DeviceBuffer.Init(0, DeviceBufferSize);
uint32 Count = 0;
// Prepare the struct which will be used to make audio callbacks
CallbackInfo.OutBuffer = (float *)OutputDeviceInfo.UserBuffer.GetData();;
CallbackInfo.NumFrames = StreamInfo.BlockSize;
CallbackInfo.NumChannels = OutputDeviceInfo.NumChannels;
CallbackInfo.StreamTime = 0.0;
CallbackInfo.UserData = StreamInfo.UserData;
CallbackInfo.StatusFlags = 0;
CallbackInfo.OutputSpeakers = OutputDeviceInfo.Speakers;
CallbackInfo.FrameRate = OutputDeviceInfo.FrameRate;
while (StreamInfo.State != EStreamState::STOPPING)
{
if (!bWasUserCallbackMade)
{
CallbackInfo.StatusFlags = 0;
CallbackInfo.StreamTime = StreamInfo.StreamTime;
memset((void*)CallbackInfo.OutBuffer, 0, StreamInfo.BlockSize*OutputDeviceInfo.NumChannels*sizeof(float));
if (!StreamInfo.CallbackFunction(CallbackInfo))
{
StreamInfo.State = EStreamState::STOPPING;
}
UpdateStreamTimeTick();
bWasUserCallbackMade = true;
}
if (bWasUserCallbackMade)
{
if (OutputDeviceInfo.bPerformFormatConversion)
{
ConvertBufferFormat(StreamInfo.DeviceBuffer, OutputDeviceInfo.UserBuffer);
bWasUserBufferWritten = WasapiInfo.RenderIntermediateBuffer->Write(StreamInfo.DeviceBuffer.GetData(), OutputDeviceSamples);
}
else
{
bWasUserBufferWritten = WasapiInfo.RenderIntermediateBuffer->Write(OutputDeviceInfo.UserBuffer.GetData(), OutputDeviceSamples);
}
}
else
{
bWasUserBufferWritten = true;
}
// If the user output buffer was not pushed to the intermediate buffer, wait for the
// next render event from the device before going on
if (bWasUserCallbackMade && !bWasUserBufferWritten)
{
WaitForSingleObject(WasapiInfo.RenderEvent, INFINITE);
}
uint32 OutputBufferFrameCount = 0;
uint32 OutputBufferFramePadding = 0;
// Get render buffer from stream
Result = WasapiInfo.RenderClient->GetBufferSize(&OutputBufferFrameCount);
CLEANUP_ON_FAIL(Result);
Result = WasapiInfo.RenderClient->GetCurrentPadding(&OutputBufferFramePadding);
CLEANUP_ON_FAIL(Result);
OutputBufferFrameCount -= OutputBufferFramePadding;
if (OutputBufferFrameCount != 0)
{
Result = WasapiInfo.RenderService->GetBuffer(OutputBufferFrameCount, &DeviceByteBuffer);
CLEANUP_ON_FAIL(Result);
// Read the next buffer from the intermediate output buffer
uint32 ReadSamples = OutputBufferFrameCount * OutputDeviceInfo.NumChannels;
bool bSuccess = WasapiInfo.RenderIntermediateBuffer->Read((uint8*)DeviceByteBuffer, ReadSamples);
if (bSuccess)
{
Result = WasapiInfo.RenderService->ReleaseBuffer(OutputBufferFrameCount, 0);
CLEANUP_ON_FAIL(Result);
}
else
{
Result = WasapiInfo.RenderService->ReleaseBuffer(0, 0);
CLEANUP_ON_FAIL(Result);
}
}
if (bWasUserBufferWritten)
{
bWasUserCallbackMade = false;
}
}
Cleanup:
CoTaskMemFree(OutputFormat);
if (bThreadComInitialized)
{
FWindowsPlatformMisc::CoUninitialize();
}
StreamInfo.State = EStreamState::STOPPED;
return 0;
}
void FUnrealAudioWasapi::Stop()
{
StreamInfo.State = EStreamState::STOPPING;
}
void FUnrealAudioWasapi::Exit()
{
}
void FUnrealAudioWasapi::GetDeviceBufferSize(uint32& DeviceBufferSize)
{
uint32 NumBytes = GetNumBytesForFormat(StreamInfo.DeviceInfo.DeviceDataFormat);
uint32 NumChannels = StreamInfo.DeviceInfo.NumChannels;
DeviceBufferSize = StreamInfo.BlockSize * NumChannels * NumBytes;
}
}
IMPLEMENT_MODULE(UAudio::FUnrealAudioWasapi, UnrealAudioWasapi);
#endif // #if ENABLE_UNREAL_AUDIO