#ifdef _MSC_VER
#include "isac_spatial_sound.h"
#include <winrt/Windows.Foundation.h>
#include <mmdeviceapi.h>
#include <SpatialAudioClient.h>
#include <winrt/Windows.Media.Devices.h>
#include <wrl.h>
using namespace Microsoft::WRL;
using namespace winrt::Windows::Media::Devices;
using namespace sk;
constexpr uint32_t c_HrtfSampleRate = 48000;
#define RETURN_IF_NULL_ALLOC(x) \
if (x == nullptr) return E_OUTOFMEMORY
#define RETURN_IF_FAILED(x) \
{HRESULT __hr = x; \
if (FAILED(__hr)) return __hr;}
#define RETURN_HR_IF(x, y) \
if (y) return x
#define RETURN_HR_IF_NULL(x, y) \
if (y == nullptr) return x
class IsacAdapter final
{
public:
IsacAdapter(uint32_t maxSources);
virtual ~IsacAdapter();
HRESULT Activate(isac_callback callback);
static DWORD WINAPI SpatialAudioClientWorker(LPVOID lpParam);
private:
HRESULT ActivateIsacInterface(winrt::hstring* activatedDeviceId);
HRESULT ActivateSpatialAudioStream(const WAVEFORMATEX& objectFormat, UINT32 maxObjects);
HRESULT HandleDeviceChange(winrt::hstring newDeviceId);
void Stop();
private:
Microsoft::WRL::ComPtr<ISpatialAudioObject>* m_Sources;
Microsoft::WRL::ComPtr<ISpatialAudioClient> m_Isac;
Microsoft::WRL::ComPtr<ISpatialAudioObjectRenderStream> m_SpatialAudioStream;
winrt::hstring m_DeviceIdInUse;
bool m_IsActivated;
winrt::event_token m_DeviceChangeToken;
uint32_t m_MaxSources;
HANDLE m_PumpEvent;
HANDLE m_PumpThread;
HANDLE m_PumpThreadCanceller;
isac_callback m_AppCallback;
};
class IsacActivator final : public Microsoft::WRL::RuntimeClass<
Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::RuntimeClassType::ClassicCom>,
Microsoft::WRL::FtmBase, IActivateAudioInterfaceCompletionHandler>
{
public:
IsacActivator();
virtual ~IsacActivator();
STDMETHOD(ActivateCompleted)(IActivateAudioInterfaceAsyncOperation* operation);
STDMETHOD(Wait)(DWORD Timeout);
STDMETHOD(GetActivateResult)(ISpatialAudioClient** deviceAccess);
private:
HANDLE m_CompletedEvent;
HRESULT m_ActivateResult;
Microsoft::WRL::ComPtr<ISpatialAudioClient> m_Isac;
};
IsacAdapter* isac_adapter = nullptr;
long isac_activate(uint32_t maxSources, isac_callback callback) {
isac_adapter = new IsacAdapter(maxSources);
HRESULT hr = isac_adapter->Activate(callback);
if (FAILED(hr)) {
delete isac_adapter;
isac_adapter = nullptr;
}
return hr;
}
void isac_destroy() {
if (isac_adapter)
delete isac_adapter;
}
IsacAdapter::IsacAdapter(uint32_t maxSources) : m_IsActivated(false),
m_PumpEvent(nullptr), m_AppCallback(nullptr), m_PumpThread(nullptr)
{
winrt::Windows::Foundation::TypedEventHandler<struct winrt::Windows::Foundation::IInspectable,
struct winrt::Windows::Media::Devices::DefaultAudioRenderDeviceChangedEventArgs>
deviceChangeHandler = [&](winrt::Windows::Foundation::IInspectable const&,
DefaultAudioRenderDeviceChangedEventArgs const& args) {
return HandleDeviceChange(args.Id());
};
m_DeviceChangeToken = MediaDevice::DefaultAudioRenderDeviceChanged(deviceChangeHandler);
m_Sources = new ComPtr<ISpatialAudioObject>[maxSources];
m_MaxSources = maxSources;
m_PumpThreadCanceller = CreateEvent(nullptr, FALSE, FALSE, nullptr);
}
IsacAdapter::~IsacAdapter() {
MediaDevice::DefaultAudioRenderDeviceChanged(m_DeviceChangeToken);
Stop();
delete[] m_Sources;
CloseHandle(m_PumpEvent);
CloseHandle(m_PumpThreadCanceller);
}
void IsacAdapter::Stop()
{
if (m_SpatialAudioStream)
{
m_SpatialAudioStream->Stop();
SetEvent(m_PumpThreadCanceller);
}
}
HRESULT IsacAdapter::ActivateIsacInterface(winrt::hstring* activatedDeviceId) {
ComPtr<IsacActivator> spCompletionObject = Make<IsacActivator>();
RETURN_IF_NULL_ALLOC(spCompletionObject);
ComPtr<IActivateAudioInterfaceCompletionHandler> spCompletionHandler;
RETURN_IF_FAILED(spCompletionObject.As(&spCompletionHandler));
winrt::hstring deviceId = MediaDevice::GetDefaultAudioRenderId(AudioDeviceRole::Default);
ComPtr<IActivateAudioInterfaceAsyncOperation> spOperation;
RETURN_IF_FAILED(ActivateAudioInterfaceAsync(
deviceId.c_str(), __uuidof(ISpatialAudioClient), nullptr, spCompletionHandler.Get(), &spOperation));
RETURN_IF_FAILED(spCompletionObject->Wait(INFINITE));
RETURN_IF_FAILED(spCompletionObject->GetActivateResult(&m_Isac));
*activatedDeviceId = deviceId;
return S_OK;
}
HRESULT FindAcceptableWaveformat(ISpatialAudioClient* isac, WAVEFORMATEX* objectFormat) {
ComPtr<IAudioFormatEnumerator> audioObjectFormatEnumerator;
RETURN_IF_FAILED(isac->GetSupportedAudioObjectFormatEnumerator(&audioObjectFormatEnumerator));
UINT32 audioObjectFormatCount = 0;
RETURN_IF_FAILED(audioObjectFormatEnumerator->GetCount(&audioObjectFormatCount));
RETURN_HR_IF(E_FAIL, audioObjectFormatCount == 0);
for (uint32_t i = 0u; i < audioObjectFormatCount; i++) {
WAVEFORMATEX* format = nullptr;
RETURN_IF_FAILED(audioObjectFormatEnumerator->GetFormat(0, &format));
if (format->nSamplesPerSec == c_HrtfSampleRate && format->wFormatTag == WAVE_FORMAT_IEEE_FLOAT) {
*objectFormat = *format;
break;
}
}
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, objectFormat);
return S_OK;
}
HRESULT IsacAdapter::ActivateSpatialAudioStream(const WAVEFORMATEX& objectFormat, UINT32 maxObjects) {
RETURN_HR_IF_NULL(E_NOT_VALID_STATE, m_Isac);
if (m_PumpEvent == nullptr) {
m_PumpEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
}
AudioObjectType objectMask = AudioObjectType_None;
SpatialAudioObjectRenderStreamActivationParams activationParams = {
&objectFormat, objectMask, 0, maxObjects, AudioCategory_GameEffects, m_PumpEvent, nullptr };
PROPVARIANT activateParams;
PropVariantInit(&activateParams);
activateParams.vt = VT_BLOB;
activateParams.blob.cbSize = sizeof(activationParams);
activateParams.blob.pBlobData = reinterpret_cast<BYTE*>(&activationParams);
RETURN_IF_FAILED(m_Isac->ActivateSpatialAudioStream(&activateParams, IID_PPV_ARGS(&m_SpatialAudioStream)));
return S_OK;
}
HRESULT IsacAdapter::Activate(isac_callback callback) {
RETURN_HR_IF_NULL(E_INVALIDARG, callback);
m_AppCallback = callback;
RETURN_HR_IF(S_OK, m_IsActivated);
winrt::hstring deviceId;
RETURN_IF_FAILED(ActivateIsacInterface(&deviceId));
WAVEFORMATEX objectFormat;
RETURN_IF_FAILED(FindAcceptableWaveformat(m_Isac.Get(), &objectFormat));
UINT32 maxObjects = 0;
RETURN_IF_FAILED(m_Isac->GetMaxDynamicObjectCount(&maxObjects));
RETURN_HR_IF(E_NOT_VALID_STATE, maxObjects == 0 || maxObjects < m_MaxSources);
RETURN_IF_FAILED(ActivateSpatialAudioStream(objectFormat, maxObjects));
m_DeviceIdInUse = deviceId;
m_IsActivated = true;
for (uint32_t i = 0; i < m_MaxSources; i++) {
RETURN_IF_FAILED(m_SpatialAudioStream->ActivateSpatialAudioObject(AudioObjectType_Dynamic, &m_Sources[i]));
}
RETURN_IF_FAILED(m_SpatialAudioStream->Start());
if (m_PumpThread != nullptr) CloseHandle(m_PumpThread);
m_PumpThread = CreateThread(nullptr, 0, &IsacAdapter::SpatialAudioClientWorker, (void*)this, 0, nullptr);
SetThreadPriority(m_PumpThread, THREAD_PRIORITY_ABOVE_NORMAL);
return S_OK;
}
HRESULT IsacAdapter::HandleDeviceChange(winrt::hstring newDeviceId) {
RETURN_HR_IF(S_OK, newDeviceId == m_DeviceIdInUse);
m_IsActivated = false;
Stop();
RETURN_IF_FAILED(Activate(m_AppCallback));
return S_OK;
}
DWORD WINAPI IsacAdapter::SpatialAudioClientWorker(LPVOID lpParam) {
IsacAdapter* pThis = (IsacAdapter*)lpParam;
BYTE** objectBuffers = new BYTE*[pThis->m_MaxSources];
vec3* positions = new vec3[pThis->m_MaxSources];
float* volumes = new float[pThis->m_MaxSources];
const HANDLE eventsToWaitOn[2] = { pThis->m_PumpEvent, pThis->m_PumpThreadCanceller };
while (WAIT_OBJECT_0 == WaitForMultipleObjects(2, eventsToWaitOn, FALSE, INFINITE)) {
UINT32 objects = 0;
UINT32 frameCount = 0;
if (FAILED(pThis->m_SpatialAudioStream->BeginUpdatingAudioObjects(&objects, &frameCount))) break;
UINT32 byteCount = 0;
for (uint32_t i = 0; i < pThis->m_MaxSources; i++) {
pThis->m_Sources[i]->GetBuffer(&objectBuffers[i], &byteCount);
memset(objectBuffers[i], 0, byteCount);
positions[i] = vec3_zero;
volumes[i] = 0.0f;
}
pThis->m_AppCallback((float**)objectBuffers, pThis->m_MaxSources, byteCount / sizeof(float), positions, volumes);
for (uint32_t i = 0; i < pThis->m_MaxSources; i++) {
pThis->m_Sources[i]->SetPosition(positions[i].x, positions[i].y, positions[i].z);
float dist = vec3_magnitude(positions[i]);
if (dist <= 0.00001f) dist = 1;
pThis->m_Sources[i]->SetVolume(min(1.0f, volumes[i] / dist));
}
pThis->m_SpatialAudioStream->EndUpdatingAudioObjects();
}
delete[] objectBuffers;
delete[] positions;
delete[] volumes;
return 0;
}
IsacActivator::IsacActivator() : m_ActivateResult(E_ILLEGAL_METHOD_CALL) {
m_CompletedEvent = CreateEvent(nullptr, FALSE, FALSE, nullptr);
}
IsacActivator::~IsacActivator() {
CloseHandle(m_CompletedEvent);
}
STDMETHODIMP IsacActivator::ActivateCompleted(IActivateAudioInterfaceAsyncOperation* operation) {
SetEvent(m_CompletedEvent);
ComPtr<IUnknown> spAudioAsUnknown;
RETURN_IF_FAILED(operation->GetActivateResult(&m_ActivateResult, &spAudioAsUnknown));
RETURN_IF_FAILED(m_ActivateResult);
m_ActivateResult = spAudioAsUnknown.As(&m_Isac);
return S_OK;
}
STDMETHODIMP IsacActivator::Wait(DWORD Timeout) {
DWORD waitResult = WaitForSingleObject(m_CompletedEvent, Timeout);
if (waitResult == WAIT_OBJECT_0) return S_OK;
RETURN_HR_IF(HRESULT_FROM_WIN32(ERROR_TIMEOUT), waitResult == WAIT_TIMEOUT);
RETURN_HR_IF(HRESULT_FROM_WIN32(GetLastError()), waitResult == WAIT_FAILED);
return E_FAIL;
}
STDMETHODIMP IsacActivator::GetActivateResult(ISpatialAudioClient** deviceAccess) {
*deviceAccess = nullptr;
RETURN_IF_FAILED(m_ActivateResult);
RETURN_IF_FAILED(m_Isac.CopyTo(deviceAccess));
return S_OK;
}
#endif