/******************************************************************************* The content of this file includes portions of the proprietary AUDIOKINETIC Wwise Technology released in source code form as part of the game integration package. The content of this file may not be used without valid licenses to the AUDIOKINETIC Wwise Technology. Note that the use of the game engine is subject to the Unreal(R) Engine End User License Agreement at https://www.unrealengine.com/en-US/eula/unreal License Usage Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use this file in accordance with the end user license agreement provided with the software or, alternatively, in accordance with the terms contained in a written agreement between you and Audiokinetic Inc. Copyright (c) 2023 Audiokinetic Inc. *******************************************************************************/ /*============================================================================= AkWaapiClient.cpp: Audiokinetic WAAPI interface object. Unreal is RHS with Y and Z swapped (or technically LHS with flipped axis) =============================================================================*/ /*------------------------------------------------------------------------------------ Audio includes. ------------------------------------------------------------------------------------*/ #include "AkWaapiClient.h" #include "AkAudioDevice.h" #include "AkSettings.h" #include "AkSettingsPerUser.h" #include "AkUnrealHelper.h" #include "Serialization/JsonSerializer.h" #include "Async/Async.h" #include "Misc/ScopeLock.h" #include "Misc/CoreDelegates.h" #include "Wwise/API/WAAPI.h" #if AK_SUPPORT_WAAPI #if PLATFORM_WINDOWS // Problem with the Windows API as a whole: it uses noexcept straight up, whether exceptions are used or not, generating a warning that Unreal then converts to an error. #if _MSC_VER >= 1910 #pragma warning (disable: 4577) #endif // _MSC_VER >= 1910 #endif // #if PLATFORM_WINDOWS typedef AK::WwiseAuthoringAPI::JsonProvider JsonProvider; #endif /*------------------------------------------------------------------------------------ Statics and Globals ------------------------------------------------------------------------------------*/ FAkWaapiClient* g_AkWaapiClient = nullptr; /*------------------------------------------------------------------------------------ FAkWaapiClientConnectionHandler ------------------------------------------------------------------------------------*/ FAkWaapiClientConnectionHandler::FAkWaapiClientConnectionHandler(FAkWaapiClient& in_Client) : m_Client(in_Client) { WaitEvent = FPlatformProcess::GetSynchEventFromPool(true); } FAkWaapiClientConnectionHandler::~FAkWaapiClientConnectionHandler() { FPlatformProcess::ReturnSynchEventToPool(WaitEvent); WaitEvent = nullptr; } void FAkWaapiClientConnectionHandler::RegisterAutoConnectChangedCallback() { #if WITH_EDITOR FScopeLock Lock(&AkSettingsSection); if (auto AkSettingsPerUser = GetDefault()) { AutoConnectChangedHandle = AkSettingsPerUser->OnAutoConnectToWaapiChanged.AddLambda([this, AkSettingsPerUser]() { ResetReconnectionDelay(); if (AkSettingsPerUser->bAutoConnectToWAAPI) WaitEvent->Trigger(); else { m_Client.BroadcastConnectionLost(); } }); } #endif } void FAkWaapiClientConnectionHandler::Wake() { WaitEvent->Trigger(); } /* FRunnable interface */ bool FAkWaapiClientConnectionHandler::Init() { return true; } uint32 FAkWaapiClientConnectionHandler::Run() { #if AK_SUPPORT_WAAPI AKASSERT(!IsInGameThread()); while (!ThreadShouldExit) { if (!m_Client.IsProjectLoaded()) { /** Check if we should attempt to reconnect according to the Wwise Plugin Settings. */ bool bReconnect = !m_Client.IsDisconnecting() && !m_Client.AppIsExiting(); { FScopeLock Lock(&AkSettingsSection); if (auto AkSettingsPerUser = GetDefault()) { bReconnect = AkSettingsPerUser->bAutoConnectToWAAPI; } } /** If we previously had a connection (and we're not exiting), broadcast connection lost.*/ if (hadConnection && !m_Client.AppIsExiting()) { if (bReconnect) UE_LOG(LogAkAudio, Warning, TEXT("Lost connection to WAAPI client. Attempting reconnection ...")); hadConnection = false; AsyncTask(ENamedThreads::GameThread, [this]() { m_Client.BroadcastConnectionLost(); }); } /** If we should reconnect, attempt a reconnection and, if successful, call the client's connection established function on the game thread. * Otherwise, print a failed connection log. */ if (bReconnect) { if (AttemptReconnect()) { hadConnection = true; m_Client.SetConnectionClosing(false); ResetReconnectionDelay(); AsyncTask(ENamedThreads::GameThread, [this]() { m_Client.ConnectionEstablished(); }); } else { if (LogOutputCount.GetValue() < 7) { UE_LOG(LogAkAudio, Warning, TEXT("Failed to connect to WAAPI client on local host. Trying again in %i seconds."), ReconnectDelay.GetValue()); LogOutputCount.Increment(); } } /** Delay the next reconnection attempt according to the ReconnectDelay value. */ const int iCurrentDelay = ReconnectDelay.GetValue(); if (iCurrentDelay < m_iMaxReconnectDelay) ReconnectDelay.Set(iCurrentDelay * 2); WaitEvent->Wait(iCurrentDelay * 1000); WaitEvent->Reset(); } else { /** We shouldn't attempt reconnection, so wait until the auto-reconnect option is changed. */ WaitEvent->Wait(); WaitEvent->Reset(); } } else /** We're already connected. Check connection status. */ { TSharedRef args = MakeShareable(new FJsonObject()); TSharedRef options = MakeShareable(new FJsonObject()); TSharedPtr result = MakeShareable(new FJsonObject()); m_Client.Call(ak::wwise::core::getInfo, args, options, result, 500, true); WaitEvent->Wait(ConnectionMonitorDelay.GetValue() * 1000); WaitEvent->Reset(); } } #endif return 0; } void FAkWaapiClientConnectionHandler::ResetReconnectionDelay() { bool bReconnect = true; { FScopeLock Lock(&AkSettingsSection); if (auto AkSettingsPerUser = GetDefault()) { bReconnect = AkSettingsPerUser->bAutoConnectToWAAPI; } } if (bReconnect && !m_Client.AppIsExiting() && !m_Client.IsDisconnecting()) { ReconnectDelay.Set(2); LogOutputCount.Set(0); } } void FAkWaapiClientConnectionHandler::Stop() { ThreadShouldExit = true; } void FAkWaapiClientConnectionHandler::Exit() { ThreadShouldExit = true; } bool FAkWaapiClientConnectionHandler::AttemptReconnect() { #if AK_SUPPORT_WAAPI if (m_Client.AttemptConnection()) { UE_LOG(LogAkAudio, Log, TEXT("Successfully connected to Wwise Authoring on localhost.")); return true; } #endif return false; } /*------------------------------------------------------------------------------------ Helpers ------------------------------------------------------------------------------------*/ struct FAkWaapiClientImpl { void Init(FAkWaapiClient& in_Client) { #if AK_SUPPORT_WAAPI auto* WAAPI = IWAAPI::Get(); if (UNLIKELY(!WAAPI)) { return; } m_Client = WAAPI->NewClient(); if (UNLIKELY(!m_Client)) { return; } m_pConnectionHandler = MakeShareable(new FAkWaapiClientConnectionHandler(in_Client)); FString ThreadName(FString::Printf(TEXT("WAAPIClientConnectionThread%i"), ThreadCounter.Increment())); m_pReconnectionThread = MakeShareable(FRunnableThread::Create(m_pConnectionHandler.Get(), *ThreadName, 0, EThreadPriority::TPri_BelowNormal)); m_pConnectionHandler->RegisterAutoConnectChangedCallback(); #endif } ~FAkWaapiClientImpl() { #if AK_SUPPORT_WAAPI if (m_pConnectionHandler.IsValid()) { m_pConnectionHandler->Exit(); m_pConnectionHandler->Wake(); } if (m_pReconnectionThread.IsValid()) { if (!m_pReconnectionThread->Kill(true)) { UE_LOG(LogAkAudio, Error, TEXT("WAAPI Connection Thread Failed to Exit!")); } } delete m_Client; m_Client = nullptr; #endif } #if AK_SUPPORT_WAAPI /** Map containing id keys and WampEventCallback values. */ TMap m_wampEventCallbackMap; IWAAPI::Client* m_Client = nullptr; /** A non-0 value indicates that UE is exiting. */ FThreadSafeCounter AppExitingCounter = 0; FThreadSafeCounter ThreadCounter; /** Thread on which the WAAPI connection is monitored. */ TSharedPtr m_pReconnectionThread; /** The connection to WAAPI is monitored by this connection handler. * It tries to reconnect when connection is lost, and continuously polls WAAPI for the connection status when WAAPI is connected. * This behaviour can be disabled in AkSettings using the AutoConnectToWaapi boolean option. */ TSharedPtr m_pConnectionHandler; FCriticalSection ClientSection; /** Flag indicating whether the correct project has been loaded (it's "correct" if it matches the Project Path in AkSettings.) */ FThreadSafeBool bProjectLoaded = false; /** Flag indicating if the connection is being killed and shouldn't be restarted. */ FThreadSafeBool bIsConnectionClosing = false; #endif }; bool FAkWaapiClient::JsonObjectToString(const TSharedRef& in_jsonObject, FString& ou_jsonObjectString) { TSharedRef> JsonWriterArgs = TJsonWriterFactory<>::Create(&ou_jsonObjectString); auto result = FJsonSerializer::Serialize(in_jsonObject, JsonWriterArgs); if (!result) { UE_LOG(LogAkAudio, Log, TEXT("Unable to get a string representation of the Json Object.")); } JsonWriterArgs->Close(); return result; } #if AK_SUPPORT_WAAPI void WampEventCallbacks(const uint64_t& in_subscriptionId, const JsonProvider& in_rJson) { if (g_AkWaapiClient == nullptr) return; auto wampEventCallbacks = g_AkWaapiClient->GetWampEventCallback(in_subscriptionId); if (!wampEventCallbacks) return; auto* WAAPI = IWAAPI::Get(); if (UNLIKELY(!WAAPI)) { return; } TSharedPtr ueJsonObject; TSharedRef> Reader = TJsonReaderFactory<>::Create(UTF8_TO_TCHAR(WAAPI->GetJsonString(in_rJson).c_str())); if (!FJsonSerializer::Deserialize(Reader, ueJsonObject) || !ueJsonObject.IsValid()) { UE_LOG(LogAkAudio, Log, TEXT("Unable to deserialize a JSON object from the string : %s"), UTF8_TO_TCHAR(WAAPI->GetJsonString(in_rJson).c_str())); return; } wampEventCallbacks->Execute(in_subscriptionId, ueJsonObject); } #endif /*------------------------------------------------------------------------------------ Implementation. ------------------------------------------------------------------------------------*/ FAkWaapiClient::~FAkWaapiClient() { delete m_Impl; } void FAkWaapiClient::Initialize() { #if AK_SUPPORT_WAAPI if (!g_AkWaapiClient) { g_AkWaapiClient = new FAkWaapiClient(); if(g_AkWaapiClient) { g_AkWaapiClient->m_Impl->Init(*g_AkWaapiClient); } FCoreDelegates::OnPreExit.AddLambda([] { if (g_AkWaapiClient != nullptr) { g_AkWaapiClient->m_Impl->AppExitingCounter.Increment(); TArray aSubscriptionIDs; g_AkWaapiClient->m_Impl->m_wampEventCallbackMap.GetKeys(aSubscriptionIDs); TSharedPtr jsonResult = MakeShareable(new FJsonObject()); for (auto iSubscriptionID : aSubscriptionIDs) { g_AkWaapiClient->Unsubscribe(iSubscriptionID, jsonResult); } } DeleteInstance(); }); } #endif } /** Returns the singleton instance of FAkWaapiClient. Be sure to call FAkWaapiClient::Initialize() first (i.e. during Module startup). */ FAkWaapiClient* FAkWaapiClient::Get() { return g_AkWaapiClient; } bool FAkWaapiClient::AppIsExiting() { #if AK_SUPPORT_WAAPI return m_Impl->AppExitingCounter.GetValue() != 0; #else return false; #endif } void FAkWaapiClient::SetConnectionClosing(bool isClosing) { #if AK_SUPPORT_WAAPI m_Impl->bIsConnectionClosing = isClosing; #endif } void FAkWaapiClient::DeleteInstance() { #if AK_SUPPORT_WAAPI if (g_AkWaapiClient == nullptr) return; g_AkWaapiClient->OnClientBeginDestroy.Broadcast(); g_AkWaapiClient->m_Impl->bIsConnectionClosing = true; if (g_AkWaapiClient->m_Impl->m_Client) { g_AkWaapiClient->m_Impl->m_Client->Disconnect(); } delete g_AkWaapiClient; g_AkWaapiClient = nullptr; #endif } bool FAkWaapiClient::IsDisconnecting() { #if AK_SUPPORT_WAAPI return m_Impl->bIsConnectionClosing; #else return false; #endif } WampEventCallback* FAkWaapiClient::GetWampEventCallback(const uint64_t& in_subscriptionId) { #if AK_SUPPORT_WAAPI return m_Impl->m_wampEventCallbackMap.Find(in_subscriptionId); #else return nullptr; #endif } bool FAkWaapiClient::IsProjectLoaded() { #if AK_SUPPORT_WAAPI if (g_AkWaapiClient == nullptr) return false; if (!g_AkWaapiClient->IsConnected()) { return false; } return g_AkWaapiClient->m_Impl->bProjectLoaded; #else return false; #endif } /** This is called when the reconnection handler successfully connects to WAAPI. * We check if the correct project is loaded on a background thread. If it is, we broadcast OnProjectLoaded. * We also subscribe to ak::wwise::core::project::loaded in order to check the project whenever one is loaded. * If an incorrect project is loaded we broadcast OnConnectionLost. */ void FAkWaapiClient::ConnectionEstablished() { #if AK_SUPPORT_WAAPI ensure(IsInGameThread()); if (g_AkWaapiClient == nullptr) return; // Broadcast OnProjectLoaded. If we are here, it means connection was successful, and that the correct project is // loaded and available OnProjectLoaded.Broadcast(); //We also subscribe to ak::wwise::core::project::loaded in order to check the project whenever one is loaded. auto projectLoadedCallback = WampEventCallback::CreateLambda([this](uint64_t id, TSharedPtr in_UEJsonObject) { AsyncTask(ENamedThreads::GameThread, [this, id, in_UEJsonObject]() { m_Impl->bProjectLoaded = CheckProjectLoaded(); if (m_Impl->bProjectLoaded) { OnProjectLoaded.Broadcast(); } else if (!AppIsExiting())//If an incorrect project is loaded we broadcast OnConnectionLost { BroadcastConnectionLost(); } }); }); TSharedPtr projectLoadedSubscriptionResult = MakeShareable(new FJsonObject()); TSharedRef projectLoadedOptions = MakeShareable(new FJsonObject()); uint64_t projectLoadedSubscriptionID; g_AkWaapiClient->Subscribe(ak::wwise::core::project::loaded, projectLoadedOptions, projectLoadedCallback, projectLoadedSubscriptionID, projectLoadedSubscriptionResult); //And we need to subscribe to ak::wwise::core::project::postClosed such that we are able to re-connect to WAAPI (for example if Wwise is closed then opened again). auto projectClosedCallback = WampEventCallback::CreateLambda([this](uint64_t id, TSharedPtr in_UEJsonObject) { AsyncTask(ENamedThreads::GameThread, [this]() { BroadcastConnectionLost(); }); }); TSharedPtr projectClosedSubscriptionResult = MakeShareable(new FJsonObject()); TSharedRef projectClosedOptions = MakeShareable(new FJsonObject()); uint64_t projectClosedSubscriptionID; g_AkWaapiClient->Subscribe(ak::wwise::core::project::postClosed, projectClosedOptions, projectClosedCallback, projectClosedSubscriptionID, projectClosedSubscriptionResult); #endif } /** Returns the path of the Wwise project as defined in AkSettings (WWise Plugin Settings). */ bool FAkWaapiClient::GetProjectPath(TSharedPtr& inOutJsonResult, FString& ProjectPath) { #if AK_SUPPORT_WAAPI TArray> inFromItems; inFromItems.Add(MakeShareable(new FJsonValueString("Project"))); const bool bSuccess = WAAPIGet(WAAPIGetFromOption::OF_TYPE, inFromItems, (AkInt64)WAAPIGetReturnOptionFlag::FILEPATH, inOutJsonResult, WAAPIGetTransformOption::NONE, TArray>(), true); if (bSuccess) { if (inOutJsonResult->HasField(FAkWaapiClient::WAAPIStrings::RETURN)) { TArray> returnJson = inOutJsonResult->GetArrayField(FAkWaapiClient::WAAPIStrings::RETURN); if (returnJson.Num() > 0) ProjectPath = returnJson[0]->AsObject()->GetStringField(FAkWaapiClient::WAAPIStrings::FILEPATH); } } return bSuccess; #else return true; #endif } // WAAPI can become temporarily unavailable from time to time, this should be taken into account in the design. // Please note that you shouldn't use this kind of design unless you know your call is accurate. If you do this // and the error is caused by an argument problem, you might end up with an infinite retry loop. FString GetAndWaitForCurrentProject(float RetrySleepTimeSeconds) { FString ProjectPath; static int RetryCount = 5; while (g_AkWaapiClient->IsConnected()) { TSharedPtr JsonResult = MakeShareable(new FJsonObject()); if (g_AkWaapiClient->GetProjectPath(JsonResult, ProjectPath)) break; if (RetrySleepTimeSeconds > 0.0f) { if (--RetryCount == 0) { RetryCount = 5; return {}; } // Avoid flooding with requests while WAAPI is unavailable. FPlatformProcess::Sleep(RetrySleepTimeSeconds); } } #if PLATFORM_MAC if(ProjectPath.StartsWith(TEXT("Y:"))) { ProjectPath.ReplaceInline(TEXT("Y:"), *FPlatformMisc::GetEnvironmentVariable(TEXT("HOME"))); } if(ProjectPath.StartsWith(TEXT("Z:"))) { ProjectPath.ReplaceInline(TEXT("Z:"), *FPlatformMisc::GetEnvironmentVariable(TEXT("ROOT"))); } ProjectPath.ReplaceInline(TEXT("\\"), TEXT("/")); #endif return ProjectPath; } /** Checks if the currently loaded Wwise project matches the project path set in AkSettings (Wwise plugin settings). * NOTE: This function will block while Wwise has a modal window open. It should not be called on the Game thread. */ bool FAkWaapiClient::CheckProjectLoaded() { #if AK_SUPPORT_WAAPI if (g_AkWaapiClient == nullptr) return false; if (!g_AkWaapiClient->IsConnected()) { g_AkWaapiClient->m_Impl->m_pConnectionHandler->ResetReconnectionDelay(); return false; } if (const UAkSettings* AkSettings = GetDefault()) { auto ProjectPathString = AkUnrealHelper::GetWwiseProjectPath(); FString sCurrentlyLoadedProjectPath = GetAndWaitForCurrentProject(1.0f); if (FPaths::IsSamePath(sCurrentlyLoadedProjectPath, ProjectPathString)) { return true; } } #endif return false; } void FAkWaapiClient::BroadcastConnectionLost() { #if AK_SUPPORT_WAAPI m_Impl->bProjectLoaded = false; OnConnectionLost.Broadcast(); #endif } bool FAkWaapiClient::IsConnected() { #if AK_SUPPORT_WAAPI if (UNLIKELY(!g_AkWaapiClient->m_Impl->m_Client)) { return false; } return m_Impl->m_Client->IsConnected(); #else return false; #endif } bool FAkWaapiClient::AttemptConnection() { bIsWrongProjectLoaded = false; bool bConnected = false; #if AK_SUPPORT_WAAPI if (UNLIKELY(!g_AkWaapiClient->m_Impl->m_Client)) { bConnected = false; } else if (const UAkSettingsPerUser* AkSettingsPerUser = GetDefault()) { bConnected = m_Impl->m_Client->Connect(TCHAR_TO_UTF8(*AkSettingsPerUser->WaapiIPAddress), AkSettingsPerUser->WaapiPort); } else { bConnected = m_Impl->m_Client->Connect(WAAPI_LOCAL_HOST_IP_STRING, WAAPI_PORT); } if (bConnected) { bool bProjectLoaded = CheckProjectLoaded(); if (!bProjectLoaded) { // We successfully connected, but the wrong project is open (or getting the project timed out). Disconnect. // We will attemps reconnection later. bIsWrongProjectLoaded = true; m_Impl->m_Client->Disconnect(); bConnected = false; } m_Impl->bProjectLoaded = bProjectLoaded; } #endif return bConnected; } bool FAkWaapiClient::Subscribe(const char* in_uri, const FString& in_options, WampEventCallback in_callback, uint64& out_subscriptionId, FString& out_result, int in_iTimeoutMs /*= 500*/) { bool eResult = false; #if AK_SUPPORT_WAAPI std::string out_resultString(""); if (IsConnected()) { if (!m_Impl->bIsConnectionClosing) { // Call for the AK WAAPI method using string params. if (LIKELY(g_AkWaapiClient->m_Impl->m_Client)) { FScopeLock Lock(&m_Impl->ClientSection); eResult = m_Impl->m_Client->Subscribe(in_uri, TCHAR_TO_UTF8(*in_options), &WampEventCallbacks, out_subscriptionId, out_resultString, in_iTimeoutMs); } if (eResult) { m_Impl->m_wampEventCallbackMap.Add(out_subscriptionId, in_callback); } else { UE_LOG(LogAkAudio, Log, TEXT("Subscription failed: %s"), *FString(UTF8_TO_TCHAR(out_resultString.c_str()))); } out_result = FString(UTF8_TO_TCHAR(out_resultString.c_str())); } } else { if (m_Impl && m_Impl->m_pConnectionHandler) { m_Impl->m_pConnectionHandler->ResetReconnectionDelay(); } } #endif return eResult; } bool FAkWaapiClient::Subscribe(const char* in_uri, const TSharedRef& in_options, WampEventCallback in_callback, uint64& out_subscriptionId, TSharedPtr& out_result, int in_iTimeoutMs /*= 500*/) { bool eResult = false; #if AK_SUPPORT_WAAPI FString in_optionsString = TEXT(""); // Retrieve the options data string from the Json object. JsonObjectToString(in_options, in_optionsString); FString out_resultString(TEXT("")); // Call for the AK WAAPI method using string params. eResult = Subscribe(in_uri, in_optionsString, in_callback, out_subscriptionId, out_resultString, in_iTimeoutMs); if (!eResult) { // Deserialize a JSON object from the string. TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(out_resultString); if ((!FJsonSerializer::Deserialize(Reader, out_result) || !out_result.IsValid()) && IsConnected()) { UE_LOG(LogAkAudio, Log, TEXT("Subscribe: Output result -> unable to deserialize the Json object from the string : %s"), *out_resultString); } } #endif return eResult; } bool FAkWaapiClient::Unsubscribe(const uint64_t& in_subscriptionId, FString& out_result, int in_iTimeoutMs /*= 500*/, bool in_bSilenceLog /*= false*/) { bool eResult = false; #if AK_SUPPORT_WAAPI if (IsConnected()) { if (!m_Impl->bIsConnectionClosing) { std::string out_resultString(""); // Call the AK WAAPI method. if (LIKELY(g_AkWaapiClient->m_Impl->m_Client)) { FScopeLock Lock(&m_Impl->ClientSection); eResult = m_Impl->m_Client->Unsubscribe(in_subscriptionId, out_resultString, in_iTimeoutMs); } if (eResult) { if (m_Impl->m_wampEventCallbackMap.Contains(in_subscriptionId)) m_Impl->m_wampEventCallbackMap.Remove(in_subscriptionId); } else if (!in_bSilenceLog) { UE_LOG(LogAkAudio, Log, TEXT("Unsubscription failed: %s"), *FString(UTF8_TO_TCHAR(out_resultString.c_str()))); } out_result = FString(UTF8_TO_TCHAR(out_resultString.c_str())); } } else { if (m_Impl && m_Impl->m_pConnectionHandler) { m_Impl->m_pConnectionHandler->ResetReconnectionDelay(); } } #endif return eResult; } bool FAkWaapiClient::Unsubscribe(const uint64_t& in_subscriptionId, TSharedPtr& out_result, int in_iTimeoutMs /*= 500*/, bool in_bSilenceLog /*= false*/) { bool eResult = false; #if AK_SUPPORT_WAAPI if (IsConnected()) { FString out_resultString(TEXT("")); // Call the AK WAAPI method. eResult = Unsubscribe(in_subscriptionId, out_resultString, in_iTimeoutMs, in_bSilenceLog); if (!eResult) { // Deserialize a JSON object from the string TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(out_resultString); if ((!FJsonSerializer::Deserialize(Reader, out_result) || !out_result.IsValid()) && IsConnected()) { UE_LOG(LogAkAudio, Log, TEXT("Unsubscribe: Output result -> unable to deserialize the Json object from the string : %s"), *out_resultString); } } } else { if (m_Impl && m_Impl->m_pConnectionHandler) { m_Impl->m_pConnectionHandler->ResetReconnectionDelay(); } } #endif return eResult; } bool FAkWaapiClient::RemoveWampEventCallback(const uint64_t in_subscriptionId) { #if AK_SUPPORT_WAAPI if (m_Impl->m_wampEventCallbackMap.Contains(in_subscriptionId)) { m_Impl->m_wampEventCallbackMap.Remove(in_subscriptionId); return true; } #endif return false; } bool FAkWaapiClient::Call(const char* in_uri, const FString& in_args, const FString& in_options, FString& out_result, int in_iTimeoutMs /*= 500*/, bool silenceLog /* = false*/) { bool eResult = false; #if AK_SUPPORT_WAAPI if (IsConnected()) { if (!m_Impl->bIsConnectionClosing) { std::string out_resultString(""); // Call the AK WAAPI method. if (LIKELY(g_AkWaapiClient->m_Impl->m_Client)) { FScopeLock Lock(&m_Impl->ClientSection); eResult = m_Impl->m_Client->Call(in_uri, TCHAR_TO_UTF8(*in_args), TCHAR_TO_UTF8(*in_options), out_resultString, in_iTimeoutMs); } if (!eResult && !silenceLog) { UE_LOG(LogAkAudio, Log, TEXT("Call failed: %s"), *FString(UTF8_TO_TCHAR(out_resultString.c_str()))); } out_result = FString(UTF8_TO_TCHAR(out_resultString.c_str())); } } else { if (m_Impl && m_Impl->m_pConnectionHandler) { m_Impl->m_pConnectionHandler->ResetReconnectionDelay(); } } #endif return eResult; } bool FAkWaapiClient::Call(const char* in_uri, const TSharedRef& in_args, const TSharedRef& in_options, TSharedPtr& out_result, int in_iTimeoutMs /*= 500*/, bool silenceLog /*= false*/) { bool eResult = false; #if AK_SUPPORT_WAAPI FString in_argsString = TEXT(""); FString in_optionsString = TEXT(""); // Make sure the arguments are valid Json data. JsonObjectToString(in_args, in_argsString); // Make sure the options are valid Json data. JsonObjectToString(in_options, in_optionsString); FString out_resultString(TEXT("")); // Call the AK WAAPI method. eResult = Call(in_uri, in_argsString, in_optionsString, out_resultString, in_iTimeoutMs, silenceLog); // Deserialize a JSON object from the string. TSharedRef< TJsonReader<> > Reader = TJsonReaderFactory<>::Create(out_resultString); if (!FJsonSerializer::Deserialize(Reader, out_result) || !out_result.IsValid()) { if (!silenceLog && IsConnected()) { UE_LOG(LogAkAudio, Log, TEXT("Output result -> unable to deserialize a JSON object from the string : %s"), *out_resultString); } } #endif return eResult; } bool FAkWaapiClient::Call(const char* inUri, const TArray& Values, TSharedPtr& outJsonResult) { TSharedRef Args = MakeShareable(new FJsonObject()); for (const auto& Value : Values) { Args->SetStringField(Value.KeyArg, Value.ValueArg); } // Construct the options Json object; TSharedRef Options = MakeShareable(new FJsonObject()); // Request infos/changes in Waapi Picker using WAAPI if (Call(inUri, Args, Options, outJsonResult)) { return true; } UE_LOG(LogAkAudio, Log, TEXT("Call %hs failed."), inUri); return false; } FAkWaapiClient::FAkWaapiClient() : m_Impl(new FAkWaapiClientImpl) { } /** Sets in_outParentGUID to the object ID of a parent of object in_objectGUID of type in_strType. */ void FAkWaapiClient::GetParentOfType(FGuid in_objectGUID, FGuid& in_outParentGUID, FString in_strType) { #if AK_SUPPORT_WAAPI if (g_AkWaapiClient == nullptr) return; /* Construct the relevant WAAPI json fields. */ AkInt64 returnFlags = (AkInt64)WAAPIGetReturnOptionFlag::ID | (AkInt64)WAAPIGetReturnOptionFlag::TYPE; TArray> fromID; fromID.Add(MakeShareable(new FJsonValueString(in_objectGUID.ToString(EGuidFormats::DigitsWithHyphensInBraces)))); TSharedPtr outJsonResult; if (!WAAPIGet(WAAPIGetFromOption::ID, fromID, returnFlags, outJsonResult)) return; if (!outJsonResult->HasField(WAAPIStrings::RETURN)) return; TArray> returnJson = outJsonResult->GetArrayField(WAAPIStrings::RETURN); if (returnJson.Num() <= 0) return; FString objectType; { TSharedPtr typeObject = returnJson[0]->AsObject(); if (typeObject->HasField(WAAPIStrings::TYPE)) objectType = typeObject->GetStringField(WAAPIStrings::TYPE); } in_outParentGUID = in_objectGUID; if (objectType.Equals(in_strType, ESearchCase::IgnoreCase)) return; TSharedPtr select = MakeShareable(new FJsonObject()); TArray> selectJsonArray; selectJsonArray.Add(MakeShareable(new FJsonValueString(WAAPIStrings::PARENT))); select->SetArrayField(WAAPIStrings::SELECT, selectJsonArray); TArray> transform; transform.Add(MakeShareable(new FJsonValueObject(select))); while (!objectType.Equals(in_strType, ESearchCase::IgnoreCase)) { fromID.Empty(); fromID.Add(MakeShareable(new FJsonValueString(in_outParentGUID.ToString(EGuidFormats::DigitsWithHyphensInBraces)))); outJsonResult = MakeShareable(new FJsonObject()); if (!WAAPIGet(WAAPIGetFromOption::ID, fromID, returnFlags, outJsonResult, WAAPIGetTransformOption::SELECT, selectJsonArray)) break; if (!outJsonResult->HasField(WAAPIStrings::RETURN)) continue; TArray> aReturnJson = outJsonResult->GetArrayField(WAAPIStrings::RETURN); TSharedPtr returnObject = aReturnJson[0]->AsObject(); if (returnObject->HasField(WAAPIStrings::TYPE)) objectType = returnObject->GetStringField(WAAPIStrings::TYPE); if (returnObject->HasField(WAAPIStrings::ID)) FGuid::Parse(returnObject->GetStringField(WAAPIStrings::ID), in_outParentGUID); } #endif } bool FAkWaapiClient::IsProjectDirty() { #if AK_SUPPORT_WAAPI /* Ensure that Initialize() has been called! */ AKASSERT(g_AkWaapiClient != nullptr); TArray> inFromItems; AkInt64 iReturnOptions = (uint64_t)WAAPIGetReturnOptionFlag::WORKUNIT_IS_DIRTY; TSharedPtr pJsonResult = MakeShareable(new FJsonObject()); inFromItems.Add(MakeShareable(new FJsonValueString("WorkUnit"))); WAAPIGet(WAAPIGetFromOption::OF_TYPE, inFromItems, iReturnOptions, pJsonResult); if (pJsonResult->HasField(WAAPIStrings::RETURN)) { TArray> returnJson = pJsonResult->GetArrayField(WAAPIStrings::RETURN); FString workunitDirty = GetReturnOptionString(WAAPIGetReturnOptionFlag::WORKUNIT_IS_DIRTY); for (auto json : returnJson) { if (json->AsObject()->HasField(workunitDirty) && json->AsObject()->GetBoolField(workunitDirty)) return true; } } #endif return false; } /** * WAAPI Structures */ FString FAkWaapiClient::GetFromOptionString(WAAPIGetFromOption from) { switch (from) { case WAAPIGetFromOption::ID: return "id"; case WAAPIGetFromOption::SEARCH: return "search"; case WAAPIGetFromOption::PATH: return "path"; case WAAPIGetFromOption::OF_TYPE: return "ofType"; case WAAPIGetFromOption::QUERY: return "query"; default: AKASSERT(false && "From option unhandled"); return ""; } } FString FAkWaapiClient::GetTransformOptionString(WAAPIGetTransformOption transform) { switch (transform) { case WAAPIGetTransformOption::SELECT: return "select"; case WAAPIGetTransformOption::RANGE: return "range"; case WAAPIGetTransformOption::WHERE: return "where"; case WAAPIGetTransformOption::NONE: return ""; default: AKASSERT(false && "Transform option unhandled"); return ""; } } FAkWaapiClient::WAAPIGetReturnOptionFlag FAkWaapiClient::GetReturnOptionFlagValue(int in_iFlagIndex) { return (WAAPIGetReturnOptionFlag)(AkInt64)pow(2, in_iFlagIndex); } FString FAkWaapiClient::GetReturnOptionString(WAAPIGetReturnOptionFlag returnOption) { switch (returnOption) { case WAAPIGetReturnOptionFlag::ID: return "id"; case WAAPIGetReturnOptionFlag::NAME: return "name"; case WAAPIGetReturnOptionFlag::NOTES: return "notes"; case WAAPIGetReturnOptionFlag::TYPE: return "type"; case WAAPIGetReturnOptionFlag::PATH: return "path"; case WAAPIGetReturnOptionFlag::PARENT: return "parent"; case WAAPIGetReturnOptionFlag::OWNER: return "owner"; case WAAPIGetReturnOptionFlag::IS_PLAYABLE: return "isPlayable"; case WAAPIGetReturnOptionFlag::SHORT_ID: return "shortId"; case WAAPIGetReturnOptionFlag::CATEGORY: return "category"; case WAAPIGetReturnOptionFlag::FILEPATH: return "filePath"; case WAAPIGetReturnOptionFlag::WORKUNIT: return "workunit"; case WAAPIGetReturnOptionFlag::CHILDREN_COUNT: return "childrenCount"; case WAAPIGetReturnOptionFlag::MUSIC_TRANSITION_ROOT: return "music:transitionRoot"; case WAAPIGetReturnOptionFlag::MUSIC_PLAYLIST_ROOT: return "music:playlistRoot"; case WAAPIGetReturnOptionFlag::SOUND_ORIGINAL_WAV_FILE_PATH: return "sound:originalWavFilePath"; case WAAPIGetReturnOptionFlag::SOUND_CONVERTED_WEM_FILE_PATH: return "sound:convertedWemFilePath"; case WAAPIGetReturnOptionFlag::SOUNDBANK_BANK_FILE_PATH: return "soundbank:bnkFilePath"; case WAAPIGetReturnOptionFlag::AUDIO_SOURCE_PLAYBACK_DURATION: return "audioSource:playbackDuration"; case WAAPIGetReturnOptionFlag::AUDIO_SOURCE_MAX_DURATION_SOURCE: return "audioSource:maxDurationSource"; case WAAPIGetReturnOptionFlag::AUDIO_SOURCE_TRIM_VALUES: return "audioSource:trimValues"; case WAAPIGetReturnOptionFlag::WORKUNIT_IS_DEFAULT: return "workunit:isDefault"; case WAAPIGetReturnOptionFlag::WORKUNIT_TYPE: return "workunit:type"; case WAAPIGetReturnOptionFlag::WORKUNIT_IS_DIRTY: return "workunit:isDirty"; default: AKASSERT(false && "Return option unhandled"); return ""; } } /** * JSon Helpers */ TSharedRef FAkWaapiClient::CreateWAAPIGetArgumentJson(WAAPIGetFromOption in_FromOption, TArray> in_FromItems, WAAPIGetTransformOption in_TransformOption /*= WAAPIGetTransformOption::NONE*/, TArray> in_TransformItems /*= TArray>()*/) { TSharedRef args = MakeShareable(new FJsonObject()); TSharedPtr from = MakeShareable(new FJsonObject()); from->SetArrayField(GetFromOptionString(in_FromOption), in_FromItems); args->SetObjectField(FAkWaapiClient::WAAPIStrings::FROM, from); if (in_TransformOption != WAAPIGetTransformOption::NONE && in_TransformItems.Num() > 0) { TArray> transformArgArray; TSharedPtr transformObjectArg = MakeShareable(new FJsonObject()); transformObjectArg->SetArrayField(GetTransformOptionString(in_TransformOption), in_TransformItems); transformArgArray.Add(MakeShareable(new FJsonValueObject(transformObjectArg))); args->SetArrayField(FAkWaapiClient::WAAPIStrings::TRANSFORM, transformArgArray); } return args; } TSharedRef FAkWaapiClient::CreateWAAPIGetReturnOptionsJson(AkInt64 ReturnOptions) { TSharedRef options = MakeShareable(new FJsonObject()); TArray> StructJsonArray; for (int bitIndex = 0; bitIndex < (int)WAAPIGetReturnOptionFlag::NUM_FLAGS; ++bitIndex) { WAAPIGetReturnOptionFlag returnOption = GetReturnOptionFlagValue(bitIndex); if ((ReturnOptions & (AkInt64)returnOption) != 0) { StructJsonArray.Add(MakeShareable(new FJsonValueString(GetReturnOptionString(returnOption)))); } } options->SetArrayField(FAkWaapiClient::WAAPIStrings::RETURN, StructJsonArray); return options; } /** * WAAPI Helpers */ bool FAkWaapiClient::WAAPIGet(WAAPIGetFromOption inFromField, TArray> inFromItems, AkInt64 inReturnOptionsFlags, TSharedPtr& outJsonResult, WAAPIGetTransformOption inTransformField /*= WAAPIGetTransformOption::NONE*/, TArray> inTransformItems /*= TArray>()*/, bool in_bSilenceLog /*= false*/) { #if AK_SUPPORT_WAAPI TSharedRef getArgsJson = CreateWAAPIGetArgumentJson(inFromField, inFromItems, inTransformField, inTransformItems); TSharedRef returnOptionsJson = CreateWAAPIGetReturnOptionsJson(inReturnOptionsFlags); if (g_AkWaapiClient != nullptr && g_AkWaapiClient->IsConnected()) { if (g_AkWaapiClient->Call(ak::wwise::core::object::get, getArgsJson, returnOptionsJson, outJsonResult, 500, in_bSilenceLog)) return true; else if (!in_bSilenceLog) UE_LOG(LogAkAudio, Log, TEXT("Call to ak.wwise.core.object.get Failed")); } #endif return false; } bool FAkWaapiClient::GetGUIDForObjectOfTypeWithName(FGuid& io_GUID, const FString& in_sTypeName, const FString& in_sName) { #if AK_SUPPORT_WAAPI TArray> nameArray; nameArray.Add(MakeShareable(new FJsonValueString(in_sName))); TSharedPtr outJsonResult = MakeShareable(new FJsonObject()); AkInt64 returnOptionFlags = (AkInt64)WAAPIGetReturnOptionFlag::ID | (AkInt64)WAAPIGetReturnOptionFlag::NAME | (AkInt64)WAAPIGetReturnOptionFlag::TYPE; if (WAAPIGet(WAAPIGetFromOption::SEARCH, nameArray, returnOptionFlags, outJsonResult)) { if (outJsonResult->HasField(WAAPIStrings::RETURN)) { TArray> returnJson = outJsonResult->GetArrayField(WAAPIStrings::RETURN); for (auto json : returnJson) { auto jsonObj = json->AsObject(); auto name = jsonObj->GetStringField(WAAPIStrings::NAME); auto typeName = jsonObj->GetStringField(WAAPIStrings::TYPE); if (name == in_sName && typeName.Equals(in_sTypeName, ESearchCase::IgnoreCase)) { auto iD = jsonObj->GetStringField(WAAPIStrings::ID); FGuid::Parse(iD, io_GUID); return true; } } } } #endif return false; } void FAkWaapiClient::SaveProject() { #if AK_SUPPORT_WAAPI TSharedRef argsJson = MakeShareable(new FJsonObject()); TSharedRef optionsJson = MakeShareable(new FJsonObject()); TSharedPtr outputJson = MakeShareable(new FJsonObject()); if (g_AkWaapiClient->IsConnected()) { g_AkWaapiClient->Call(ak::wwise::core::project::save, argsJson, optionsJson, outputJson); } #endif } const FString FAkWaapiClient::WAAPIStrings::BACK_SLASH = TEXT("\\"); const FString FAkWaapiClient::WAAPIStrings::ID = TEXT("id"); const FString FAkWaapiClient::WAAPIStrings::RETURN = TEXT("return"); const FString FAkWaapiClient::WAAPIStrings::PATH = TEXT("path"); const FString FAkWaapiClient::WAAPIStrings::FILEPATH = TEXT("filePath"); const FString FAkWaapiClient::WAAPIStrings::FROM = TEXT("from"); const FString FAkWaapiClient::WAAPIStrings::NAME = TEXT("name"); const FString FAkWaapiClient::WAAPIStrings::TYPE = TEXT("type"); const FString FAkWaapiClient::WAAPIStrings::CHILDREN = TEXT("children"); const FString FAkWaapiClient::WAAPIStrings::CHILDREN_COUNT = TEXT("childrenCount"); const FString FAkWaapiClient::WAAPIStrings::ANCESTORS = TEXT("ancestors"); const FString FAkWaapiClient::WAAPIStrings::DESCENDANTS = TEXT("descendants"); const FString FAkWaapiClient::WAAPIStrings::WOKUNIT_TYPE = TEXT("workunit:type"); const FString FAkWaapiClient::WAAPIStrings::FOLDER = TEXT("Folder"); const FString FAkWaapiClient::WAAPIStrings::PHYSICAL_FOLDER = TEXT("PhysicalFolder"); const FString FAkWaapiClient::WAAPIStrings::SEARCH = TEXT("search"); const FString FAkWaapiClient::WAAPIStrings::PARENT = TEXT("parent"); const FString FAkWaapiClient::WAAPIStrings::SELECT = TEXT("select"); const FString FAkWaapiClient::WAAPIStrings::TRANSFORM = TEXT("transform"); const FString FAkWaapiClient::WAAPIStrings::OBJECT = TEXT("object"); const FString FAkWaapiClient::WAAPIStrings::OBJECTS = TEXT("objects"); const FString FAkWaapiClient::WAAPIStrings::VALUE = TEXT("value"); const FString FAkWaapiClient::WAAPIStrings::COMMAND = TEXT("command"); const FString FAkWaapiClient::WAAPIStrings::TRANSPORT = TEXT("transport"); const FString FAkWaapiClient::WAAPIStrings::ACTION = TEXT("action"); const FString FAkWaapiClient::WAAPIStrings::PLAY = TEXT("play"); const FString FAkWaapiClient::WAAPIStrings::STOP = TEXT("stop"); const FString FAkWaapiClient::WAAPIStrings::STOPPED = TEXT("stopped"); const FString FAkWaapiClient::WAAPIStrings::DISPLAY_NAME = TEXT("displayName"); const FString FAkWaapiClient::WAAPIStrings::DELETE_ITEMS = TEXT("Delete Items"); const FString FAkWaapiClient::WAAPIStrings::DRAG_DROP_ITEMS = TEXT("Drag Drop Items"); const FString FAkWaapiClient::WAAPIStrings::UNDO = TEXT("Undo"); const FString FAkWaapiClient::WAAPIStrings::REDO = TEXT("Redo"); const FString FAkWaapiClient::WAAPIStrings::STATE = TEXT("state"); const FString FAkWaapiClient::WAAPIStrings::OF_TYPE = TEXT("ofType"); const FString FAkWaapiClient::WAAPIStrings::PROJECT = TEXT("Project"); const FString FAkWaapiClient::WAAPIStrings::PROPERTY = TEXT("property"); const FString FAkWaapiClient::WAAPIStrings::VOLUME = TEXT("Volume"); const FString FAkWaapiClient::WAAPIStrings::FIND_IN_PROJECT_EXPLORER = TEXT("FindInProjectExplorerSelectionChannel1"); const FString FAkWaapiClient::WAAPIStrings::TRIMMED_DURATION = TEXT("trimmedDuration"); const FString FAkWaapiClient::WwiseTypeStrings::SOUND = TEXT("Sound"); const FString FAkWaapiClient::WwiseTypeStrings::WORKUNIT = TEXT("WorkUnit"); const FString FAkWaapiClient::AudioPeaksStrings::Args::OBJECT = TEXT("object"); const FString FAkWaapiClient::AudioPeaksStrings::Args::NUM_PEAKS = TEXT("numPeaks"); const FString FAkWaapiClient::AudioPeaksStrings::Args::TIME_FROM = TEXT("timeFrom"); const FString FAkWaapiClient::AudioPeaksStrings::Args::TIME_TO = TEXT("timeTo"); const FString FAkWaapiClient::AudioPeaksStrings::Args::CROSS_CHANNEL_PEAKS = TEXT("getCrossChannelPeaks"); const FString FAkWaapiClient::AudioPeaksStrings::Results::PEAKS_BINARY = TEXT("peaksBinaryStrings"); const FString FAkWaapiClient::AudioPeaksStrings::Results::MAX_ABS_VALUE = TEXT("maxAbsValue"); const FString FAkWaapiClient::AudioPeaksStrings::Results::PEAKS_ARRAY_LENGTH = TEXT("peaksArrayLength"); const FString FAkWaapiClient::AudioPeaksStrings::Results::PEAKS_DATA_SIZE = TEXT("peaksDataSize"); const FString FAkWaapiClient::PropertyChangedStrings::RequiredOptions::OBJECT = TEXT("object"); const FString FAkWaapiClient::PropertyChangedStrings::RequiredOptions::PROPERTY = TEXT("property"); const FString FAkWaapiClient::PropertyChangedStrings::OptionalOptions::RETURN = TEXT("return"); const FString FAkWaapiClient::PropertyChangedStrings::OptionalOptions::PLATFORM = TEXT("platform"); const FString FAkWaapiClient::AudioSourceProperties::TRIM_END = TEXT("TrimEnd"); const FString FAkWaapiClient::AudioSourceProperties::TRIM_BEGIN = TEXT("TrimBegin"); const FString FAkWaapiClient::PlaybackDurationStrings::MIN = TEXT("playbackDurationMin"); const FString FAkWaapiClient::PlaybackDurationStrings::MAX = TEXT("playbackDurationMax"); const FString FAkWaapiClient::PlaybackDurationStrings::TYPE = TEXT("playbackDurationType"); const FString FAkWaapiClient::TrimValuesStrings::TRIM_BEGIN = TEXT("trimBegin"); const FString FAkWaapiClient::TrimValuesStrings::TRIM_END = TEXT("trimEnd"); // end