ReaWwise

REAPER extension
Log | Files | Refs | Submodules

WaapiClient.cpp (27130B)


      1 /*----------------------------------------------------------------------------------------
      2 
      3 Copyright (c) 2023 AUDIOKINETIC Inc.
      4 
      5 This file is licensed to use under the license available at:
      6 https://github.com/audiokinetic/ReaWwise/blob/main/License.txt (the "License").
      7 You may not use this file except in compliance with the License.
      8 
      9 Unless required by applicable law or agreed to in writing, software distributed
     10 under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
     11 CONDITIONS OF ANY KIND, either express or implied.  See the License for the
     12 specific language governing permissions and limitations under the License.
     13 
     14 ----------------------------------------------------------------------------------------*/
     15 
     16 #include "WaapiClient.h"
     17 
     18 #include "Helpers/ImportHelper.h"
     19 #include "Model/IDs.h"
     20 
     21 #include <JSONHelpers.h>
     22 #include <juce_events/juce_events.h>
     23 #include <set>
     24 
     25 namespace AK::WwiseTransfer
     26 {
     27 	namespace WaapiCommands
     28 	{
     29 		static constexpr const char* const projectLoaded = "ak.wwise.core.project.loaded";
     30 		static constexpr const char* const projectPostClosed = "ak.wwise.core.project.postClosed";
     31 		static constexpr const char* const objectPostDeleted = "ak.wwise.core.object.postDeleted";
     32 		static constexpr const char* const objectNameChanged = "ak.wwise.core.object.nameChanged";
     33 		static constexpr const char* const objectCreated = "ak.wwise.core.object.created";
     34 		static constexpr const char* const objectGet = "ak.wwise.core.object.get";
     35 		static constexpr const char* const audioImport = "ak.wwise.core.audio.import";
     36 		static constexpr const char* const getProjectInfo = "ak.wwise.core.getProjectInfo";
     37 		static constexpr const char* const undoBeginGroup = "ak.wwise.core.undo.beginGroup";
     38 		static constexpr const char* const undoCancelGroup = "ak.wwise.core.undo.cancelGroup";
     39 		static constexpr const char* const undoEndGroup = "ak.wwise.core.undo.endGroup";
     40 		static constexpr const char* const objectPasteProperties = "ak.wwise.core.object.pasteProperties";
     41 		static constexpr const char* const getInfo = "ak.wwise.core.getInfo";
     42 		static constexpr const char* const getSelectedObjects = "ak.wwise.ui.getSelectedObjects";
     43 		static constexpr const char* const commandsExecute = "ak.wwise.ui.commands.execute";
     44 	} // namespace WaapiCommands
     45 
     46 	namespace WaapiURIs
     47 	{
     48 		static constexpr const char* const unknownObject = "ak.wwise.query.unknown_object";
     49 	}
     50 
     51 	namespace WaapiMessages
     52 	{
     53 		static constexpr const char* const objectNotFound = "Object not found";
     54 	}
     55 
     56 	WaapiClientWatcher::WaapiClientWatcher(juce::ValueTree appState, WaapiClient& waapiClient, WaapiClientWatcherConfig&& waapiClientWatcherConfig)
     57 		: juce::Thread("WaapiService")
     58 		, applicationState(appState)
     59 		, projectId(applicationState, IDs::projectId, nullptr)
     60 		, waapiConnected(applicationState, IDs::waapiConnected, nullptr)
     61 		, wwiseObjectsChanged(applicationState, IDs::wwiseObjectsChanged, nullptr)
     62 		, waapiClient(waapiClient)
     63 		, waapiClientWatcherConfig(waapiClientWatcherConfig)
     64 		, connectionRetryDelay(waapiClientWatcherConfig.MinConnectionRetryDelay)
     65 		, languages(applicationState.getChildWithName(IDs::languages))
     66 		, ip(waapiClientWatcherConfig.Ip)
     67 		, port(waapiClientWatcherConfig.Port)
     68 	{
     69 	}
     70 
     71 	void WaapiClientWatcher::start()
     72 	{
     73 		startThread();
     74 	}
     75 
     76 	void WaapiClientWatcher::stop()
     77 	{
     78 		stopThread(-1);
     79 	}
     80 
     81 	void WaapiClientWatcher::changeParameters(const juce::String& ip, int port)
     82 	{
     83 		{
     84 			std::lock_guard lock(guiMutex);
     85 			this->ip = ip;
     86 			this->port = port;
     87 			shouldReconnect = true;
     88 		}
     89 		notify();
     90 	}
     91 
     92 	void WaapiClientWatcher::run()
     93 	{
     94 		using namespace WwiseAuthoringAPI;
     95 		auto onDisconnect = [this]
     96 		{
     97 			juce::Logger::writeToLog("Disconnected from waapi");
     98 
     99 			setWaapiConnected(false);
    100 		};
    101 
    102 		auto onProjectLoaded = [this](auto, auto)
    103 		{
    104 			juce::Logger::writeToLog("Received project loaded event");
    105 
    106 			setProjectId("");
    107 		};
    108 
    109 		auto onProjectPostClosed = [this](auto, auto)
    110 		{
    111 			juce::Logger::writeToLog("Received project post close event");
    112 		};
    113 
    114 		auto onObjectEvent = [this](auto, auto)
    115 		{
    116 			juce::Logger::writeToLog("Received object related event");
    117 
    118 			setWwiseObjectsChanged(true);
    119 		};
    120 
    121 		while(!threadShouldExit())
    122 		{
    123 			{
    124 				std::unique_lock lock(guiMutex);
    125 				if(shouldReconnect)
    126 				{
    127 					waapiClientWatcherConfig.Ip = ip;
    128 					waapiClientWatcherConfig.Port = port;
    129 					shouldReconnect = false;
    130 					lock.unlock();
    131 					disconnectFromWaapi();
    132 				}
    133 			}
    134 
    135 			int waitTime = waapiClientWatcherConfig.ConnectionMonitorDelay;
    136 
    137 			if(!waapiClient.isConnected())
    138 			{
    139 				if(waapiClient.connect(static_cast<const char*>(waapiClientWatcherConfig.Ip.toUTF8()), waapiClientWatcherConfig.Port, onDisconnect))
    140 				{
    141 					juce::Logger::writeToLog("Connected to waapi");
    142 
    143 					connectionRetryDelay = waapiClientWatcherConfig.MinConnectionRetryDelay;
    144 
    145 					AkJson subscribeResult;
    146 					if(waapiClient.subscribe(WaapiCommands::projectLoaded, AkJson::Map{}, onProjectLoaded, projectLoadedSubscriptionId, subscribeResult))
    147 					{
    148 						juce::Logger::writeToLog("Subscribed to project loaded");
    149 					}
    150 					else
    151 					{
    152 						juce::Logger::writeToLog("Failed to subscribed to project loaded");
    153 					}
    154 
    155 					if(waapiClient.subscribe(WaapiCommands::projectPostClosed, AkJson::Map{}, onProjectPostClosed, projectPostClosedSubscriptionId, subscribeResult))
    156 					{
    157 						juce::Logger::writeToLog("Subscribed to project unloaded");
    158 					}
    159 					else
    160 					{
    161 						juce::Logger::writeToLog("Failed to subscribed to project post closed");
    162 					}
    163 
    164 					if(waapiClient.subscribe(WaapiCommands::objectCreated, AkJson::Map{}, onObjectEvent, objectCreatedEventSubscriptionId, subscribeResult))
    165 					{
    166 						juce::Logger::writeToLog("Subscribed to object created");
    167 					}
    168 					else
    169 					{
    170 						juce::Logger::writeToLog("Failed to subscribed to object created");
    171 					}
    172 
    173 					if(waapiClient.subscribe(WaapiCommands::objectPostDeleted, AkJson::Map{}, onObjectEvent, objectPostDeletedEventSubscriptionId, subscribeResult))
    174 					{
    175 						juce::Logger::writeToLog("Subscribed to object postDeleted");
    176 					}
    177 					else
    178 					{
    179 						juce::Logger::writeToLog("Failed to subscribed to object postDeleted");
    180 					}
    181 
    182 					if(waapiClient.subscribe(WaapiCommands::objectNameChanged, AkJson::Map{}, onObjectEvent, objectNameChangedEventSubscriptionId, subscribeResult))
    183 					{
    184 						juce::Logger::writeToLog("Subscribed to object nameChanged");
    185 					}
    186 					else
    187 					{
    188 						juce::Logger::writeToLog("Failed to subscribed to object nameChanged");
    189 					}
    190 
    191 					setWaapiConnected(true);
    192 				}
    193 				else
    194 				{
    195 					waitTime = connectionRetryDelay;
    196 					connectionRetryDelay = juce::jmin(connectionRetryDelay * 2, waapiClientWatcherConfig.MaxConnectionRetryDelay);
    197 
    198 					juce::String errorMessage("Error trying to connect to waapi ... Retrying in ");
    199 
    200 					errorMessage << connectionRetryDelay << " ms";
    201 
    202 					juce::Logger::writeToLog(errorMessage);
    203 				}
    204 			}
    205 
    206 			wait(waitTime);
    207 		}
    208 		if(waapiClient.isConnected())
    209 		{
    210 			disconnectFromWaapi();
    211 		}
    212 	}
    213 	void WaapiClientWatcher::setProjectId(const juce::String& id)
    214 	{
    215 		juce::Logger::writeToLog("Setting projectId");
    216 
    217 		auto onCallAsync = [this, id = id]
    218 		{
    219 			projectId = id;
    220 			// On the first run of the extension, the project ID will be empty. On project load we
    221 			// also explicetly set the id to empty so that Project Support knows it has to refresh the project data.
    222 			// The juce value tree will not send updates if the property doesnt change. Lets force it to send an update.
    223 			applicationState.sendPropertyChangeMessage(IDs::projectId);
    224 		};
    225 
    226 		juce::MessageManager::callAsync(onCallAsync);
    227 	}
    228 
    229 	void WaapiClientWatcher::setWwiseObjectsChanged(bool changed)
    230 	{
    231 		juce::Logger::writeToLog("Setting wwise objects changed");
    232 
    233 		auto onCallAsync = [this, changed = changed]
    234 		{
    235 			wwiseObjectsChanged = changed;
    236 		};
    237 
    238 		juce::MessageManager::callAsync(onCallAsync);
    239 	}
    240 
    241 	void WaapiClientWatcher::disconnectFromWaapi()
    242 	{
    243 		using namespace WwiseAuthoringAPI;
    244 		AkJson result;
    245 		if(waapiClient.unsubscribe(projectLoadedSubscriptionId, result))
    246 		{
    247 			juce::Logger::writeToLog("Unsubscribed from project loaded");
    248 		}
    249 		else
    250 		{
    251 			juce::Logger::writeToLog("Failed to unsubscribed from project unloaded");
    252 		}
    253 
    254 		if(waapiClient.unsubscribe(projectPostClosedSubscriptionId, result))
    255 		{
    256 			juce::Logger::writeToLog("Unsubscribed from project post closed");
    257 		}
    258 		else
    259 		{
    260 			juce::Logger::writeToLog("Failed to unsubscribed from project post closed");
    261 		}
    262 
    263 		if(waapiClient.unsubscribe(objectCreatedEventSubscriptionId, result))
    264 		{
    265 			juce::Logger::writeToLog("Unsubscribed from object created");
    266 		}
    267 		else
    268 		{
    269 			juce::Logger::writeToLog("Failed to unsubscribed from object created");
    270 		}
    271 
    272 		if(waapiClient.unsubscribe(objectPostDeletedEventSubscriptionId, result))
    273 		{
    274 			juce::Logger::writeToLog("Unsubscribed from object postDeleted");
    275 		}
    276 		else
    277 		{
    278 			juce::Logger::writeToLog("Failed to unsubscribed from object postDeleted");
    279 		}
    280 
    281 		if(waapiClient.unsubscribe(objectNameChangedEventSubscriptionId, result))
    282 		{
    283 			juce::Logger::writeToLog("Unsubscribed from object postDeleted");
    284 		}
    285 		else
    286 		{
    287 			juce::Logger::writeToLog("Failed to unsubscribed from object postDeleted");
    288 		}
    289 
    290 		waapiClient.disconnect();
    291 
    292 		setWaapiConnected(false);
    293 		setProjectId("");
    294 	}
    295 
    296 	void WaapiClientWatcher::setWaapiConnected(bool connected)
    297 	{
    298 		juce::Logger::writeToLog("Setting waapi connected");
    299 
    300 		auto resetProjectInfo = [this, connected = connected]
    301 		{
    302 			waapiConnected = connected;
    303 		};
    304 
    305 		juce::MessageManager::callAsync(resetProjectInfo);
    306 	}
    307 
    308 	WaapiClient::WaapiClient()
    309 	{
    310 	}
    311 
    312 	bool WaapiClient::connect(const char* in_uri, unsigned int in_port, WwiseAuthoringAPI::disconnectHandler_t disconnectHandler, int in_timeoutMs)
    313 	{
    314 		return Connect(in_uri, in_port, disconnectHandler, in_timeoutMs);
    315 	}
    316 
    317 	bool WaapiClient::subscribe(const char* in_uri, const WwiseAuthoringAPI::AkJson& in_options, WampEventCallback in_callback, uint64_t& out_subscriptionId, WwiseAuthoringAPI::AkJson& out_result, int in_timeoutMs)
    318 	{
    319 		return Subscribe(in_uri, in_options, in_callback, out_subscriptionId, out_result, in_timeoutMs);
    320 	}
    321 
    322 	bool WaapiClient::unsubscribe(const uint64_t& in_subscriptionId, WwiseAuthoringAPI::AkJson& out_result, int in_timeoutMs)
    323 	{
    324 		return Unsubscribe(in_subscriptionId, out_result, in_timeoutMs);
    325 	}
    326 
    327 	bool WaapiClient::isConnected() const
    328 	{
    329 		return IsConnected();
    330 	}
    331 
    332 	void WaapiClient::disconnect()
    333 	{
    334 		Disconnect();
    335 	}
    336 
    337 	bool WaapiClient::call(const char* in_uri, const WwiseAuthoringAPI::AkJson& in_args, const WwiseAuthoringAPI::AkJson& in_options, WwiseAuthoringAPI::AkJson& out_result, int in_timeoutMs)
    338 	{
    339 		using namespace WwiseAuthoringAPI;
    340 
    341 		auto status = Call(in_uri, in_args, in_options, out_result, in_timeoutMs);
    342 
    343 		juce::Logger::writeToLog(juce::String(in_uri) +
    344 								 juce::NewLine() + juce::String("args: ") + JSONHelpers::GetAkJsonString(in_args) +
    345 								 juce::NewLine() + juce::String("options: ") + JSONHelpers::GetAkJsonString(in_options) +
    346 								 juce::NewLine() + juce::String("result: ") + JSONHelpers::GetAkJsonString(out_result));
    347 
    348 		return status;
    349 	}
    350 
    351 	bool WaapiClient::call(const char* in_uri, const char* in_args, const char* in_options, std::string& out_result, int in_timeoutMs)
    352 	{
    353 		auto status = Call(in_uri, in_args, in_options, out_result, in_timeoutMs);
    354 
    355 		juce::Logger::writeToLog(juce::String(in_uri) +
    356 								 juce::NewLine() + juce::String("args: ") + in_args +
    357 								 juce::NewLine() + juce::String("options: ") + in_options +
    358 								 juce::NewLine() + juce::String("result: ") + out_result);
    359 
    360 		return status;
    361 	}
    362 
    363 	Waapi::Response<Waapi::ObjectResponseSet> WaapiClient::import(const std::vector<Waapi::ImportItemRequest>& importItemsRequest, Import::ContainerNameExistsOption containerNameExistsOption, const juce::String& objectLanguage)
    364 	{
    365 		using namespace WwiseAuthoringAPI;
    366 
    367 		Waapi::Response<Waapi::ObjectResponseSet> response;
    368 
    369 		AkJson::Array importItemsAsJson;
    370 		for(const auto& importItemRequest : importItemsRequest)
    371 		{
    372 			std::string key;
    373 			std::string value;
    374 			if(importItemRequest.renderFileWavBase64.isEmpty())
    375 			{
    376 				key = "audioFile";
    377 				value = importItemRequest.renderFilePath.toStdString();
    378 			}
    379 			else
    380 			{
    381 				key = "audioFileBase64";
    382 				value = importItemRequest.renderFileName.toStdString() + "|" + importItemRequest.renderFileWavBase64.toStdString();
    383 			}
    384 			using namespace juce;
    385 			auto importItemAsJson = AkJson(AkJson::Map{
    386 				{key,
    387 					AkVariant(value)},
    388 				{
    389 					"objectPath",
    390 					AkVariant(importItemRequest.path.toStdString()),
    391 				},
    392 				{
    393 					"originalsSubFolder",
    394 					AkVariant(importItemRequest.originalsSubFolder.toStdString()),
    395 				},
    396 			});
    397 
    398 			importItemsAsJson.push_back(importItemAsJson);
    399 		};
    400 
    401 		const auto args = AkJson::Map{
    402 			{
    403 				"importOperation",
    404 				AkVariant(ImportHelper::containerNameExistsOptionToString(containerNameExistsOption).toStdString()),
    405 			},
    406 			{
    407 				"default",
    408 				AkJson::Map{{"importLanguage", AkVariant(objectLanguage.toStdString())}},
    409 			},
    410 			{
    411 				"imports",
    412 				importItemsAsJson,
    413 			},
    414 			{
    415 				"autoAddToSourceControl",
    416 				AkVariant(true),
    417 			},
    418 		};
    419 
    420 		static const auto options = AkJson::Map{
    421 			{
    422 				"return",
    423 				AkJson::Array{
    424 					AkVariant{"id"},
    425 					AkVariant{"name"},
    426 					AkVariant{"type"},
    427 					AkVariant{"path"},
    428 					AkVariant{"sound:originalWavFilePath"},
    429 				},
    430 			},
    431 		};
    432 
    433 		AkJson result;
    434 		response.status = call(WaapiCommands::audioImport, args, options, result);
    435 
    436 		if(response.status)
    437 		{
    438 			if(result.HasKey("objects"))
    439 			{
    440 				auto objects = result["objects"].GetArray();
    441 
    442 				for(auto& object : objects)
    443 				{
    444 					response.result.emplace(object);
    445 				}
    446 			}
    447 		}
    448 		else
    449 		{
    450 			response.error = WaapiHelper::parseError(WaapiCommands::audioImport, result);
    451 		}
    452 
    453 		return response;
    454 	}
    455 
    456 	bool WaapiClient::selectObjects(const juce::String& selectObjectsOnImportCommand, const std::vector<juce::String>& objectPaths)
    457 	{
    458 		using namespace WwiseAuthoringAPI;
    459 
    460 		AkJson::Array objects;
    461 		for(const auto& objectPath : objectPaths)
    462 		{
    463 			objects.emplace_back(AkVariant(objectPath.toStdString()));
    464 		}
    465 
    466 		const auto args = AkJson::Map{
    467 			{
    468 				"command",
    469 				AkVariant(selectObjectsOnImportCommand.toStdString()),
    470 			},
    471 			{
    472 				"objects",
    473 				objects,
    474 			},
    475 		};
    476 
    477 		AkJson result;
    478 		return call(WaapiCommands::commandsExecute, args, AkJson::Map{}, result);
    479 	}
    480 
    481 	Waapi::Response<Waapi::ObjectResponseSet> WaapiClient::getObjectAncestorsAndDescendants(const juce::String& objectPath)
    482 	{
    483 		using namespace WwiseAuthoringAPI;
    484 
    485 		Waapi::Response<Waapi::ObjectResponseSet> response;
    486 
    487 		auto waql = juce::String("\"" + objectPath + "\" select this, ancestors, descendants");
    488 
    489 		const auto args = AkJson::Map{
    490 			{
    491 				"waql",
    492 				AkVariant{waql.toStdString()},
    493 			},
    494 		};
    495 
    496 		static const auto options = AkJson::Map{
    497 			{
    498 				"return",
    499 				AkJson::Array{
    500 					AkVariant{"id"},
    501 					AkVariant{"name"},
    502 					AkVariant{"type"},
    503 					AkVariant{"path"},
    504 					AkVariant{"sound:originalWavFilePath"},
    505 					AkVariant{"workunitType"},
    506 				},
    507 			},
    508 		};
    509 
    510 		AkJson result;
    511 		response.status = call(WaapiCommands::objectGet, args, options, result);
    512 
    513 		if(!response.status)
    514 		{
    515 			// The waql query above will fail if the object was not found.
    516 			// We still want to know if ancestors exist.
    517 			auto objectAncestors = WwiseHelper::pathToAncestorPaths(objectPath);
    518 
    519 			for(int i = objectAncestors.size() - 1; i >= 0; --i)
    520 			{
    521 				auto waql = juce::String("\"" + objectAncestors[i] + "\" select this, ancestors");
    522 
    523 				const auto args = AkJson::Map{
    524 					{
    525 						"waql",
    526 						AkVariant{waql.toStdString()},
    527 					},
    528 				};
    529 
    530 				response.status = call(WaapiCommands::objectGet, args, options, result);
    531 
    532 				if(response.status)
    533 					break;
    534 			}
    535 		}
    536 
    537 		if(response.status)
    538 		{
    539 			if(result.HasKey("return"))
    540 			{
    541 				auto objects = result["return"].GetArray();
    542 
    543 				for(auto& object : objects)
    544 				{
    545 					response.result.emplace(object);
    546 				}
    547 			}
    548 		}
    549 		else
    550 		{
    551 			if(result.HasKey("message"))
    552 			{
    553 				juce::String message(result["message"].GetVariant().GetString());
    554 				if(message.contains(WaapiMessages::objectNotFound))
    555 					response.status = true;
    556 			}
    557 
    558 			response.error = WaapiHelper::parseError(WaapiCommands::objectGet, result);
    559 		}
    560 
    561 		return response;
    562 	}
    563 
    564 	Waapi::Response<std::vector<juce::String>> WaapiClient::getProjectLanguages()
    565 	{
    566 		using namespace WwiseAuthoringAPI;
    567 
    568 		Waapi::Response<std::vector<juce::String>> response;
    569 
    570 		static const auto args(AkJson::Map{
    571 			{
    572 				"from",
    573 				AkJson::Map{
    574 					{
    575 						"ofType",
    576 						AkJson::Array{
    577 							{AkVariant("Language")},
    578 						},
    579 					},
    580 				},
    581 			},
    582 		});
    583 
    584 		static const auto options(AkJson::Map{
    585 			{
    586 				"return",
    587 				AkJson::Array{
    588 					{AkVariant("name")},
    589 				},
    590 			},
    591 		});
    592 
    593 		AkJson result;
    594 		response.status = call(WaapiCommands::objectGet, args, options, result);
    595 
    596 		static const std::set<juce::String> exceptions = {
    597 			"Mixed",
    598 			"SFX",
    599 			"External",
    600 			"SoundSeed Grain",
    601 		};
    602 
    603 		if(response.status)
    604 		{
    605 			if(result.HasKey("return"))
    606 			{
    607 				auto objects = result["return"].GetArray();
    608 
    609 				for(auto& object : objects)
    610 				{
    611 					auto language = juce::String(object["name"].GetVariant().GetString());
    612 
    613 					if(exceptions.find(language) == exceptions.end())
    614 						response.result.emplace_back(language);
    615 				}
    616 			}
    617 		}
    618 		else
    619 		{
    620 			response.error = WaapiHelper::parseError(WaapiCommands::objectGet, result);
    621 		}
    622 
    623 		return response;
    624 	}
    625 
    626 	Waapi::Response<Waapi::ObjectResponseSet> WaapiClient::getObjectAncestorsAndDescendantsLegacy(const juce::String& objectPath)
    627 	{
    628 		using namespace WwiseAuthoringAPI;
    629 
    630 		Waapi::Response<Waapi::ObjectResponseSet> response;
    631 
    632 		static auto buildArgs = [](const juce::String& path)
    633 		{
    634 			auto from = AkJson::Map{
    635 				{
    636 					"from",
    637 					AkJson::Map{
    638 						{
    639 							"path",
    640 							AkJson::Array{
    641 								AkVariant{path.toStdString()},
    642 							},
    643 						},
    644 					},
    645 				},
    646 			};
    647 
    648 			return from;
    649 		};
    650 
    651 		static auto addTransform = [](AkJson::Map& args, const juce::String& transform)
    652 		{
    653 			args["transform"] = AkJson::Array{
    654 				AkJson::Map{
    655 					{
    656 						"select",
    657 						AkJson::Array{
    658 							AkVariant{transform.toStdString()},
    659 						},
    660 					},
    661 				},
    662 			};
    663 		};
    664 
    665 		static auto fillResponse = [](auto& response, const auto& result)
    666 		{
    667 			if(result.HasKey("return"))
    668 			{
    669 				auto objects = result["return"].GetArray();
    670 
    671 				for(auto& object : objects)
    672 				{
    673 					response.result.emplace(object);
    674 				}
    675 			}
    676 		};
    677 
    678 		static const auto options = AkJson::Map{
    679 			{
    680 				"return",
    681 				AkJson::Array{
    682 					AkVariant{"id"},
    683 					AkVariant{"name"},
    684 					AkVariant{"type"},
    685 					AkVariant{"path"},
    686 					AkVariant{"sound:originalWavFilePath"},
    687 					AkVariant{"workunitType"},
    688 				},
    689 			},
    690 		};
    691 
    692 		auto args = buildArgs(objectPath);
    693 
    694 		AkJson result;
    695 		response.status = call(WaapiCommands::objectGet, args, options, result);
    696 
    697 		if(response.status)
    698 			fillResponse(response, result);
    699 
    700 		args = buildArgs(objectPath);
    701 		addTransform(args, "descendants");
    702 
    703 		response.status = call(WaapiCommands::objectGet, args, options, result);
    704 
    705 		if(response.status)
    706 			fillResponse(response, result);
    707 
    708 		args = buildArgs(objectPath);
    709 		addTransform(args, "ancestors");
    710 
    711 		response.status = call(WaapiCommands::objectGet, args, options, result);
    712 
    713 		if(response.status)
    714 			fillResponse(response, result);
    715 		else
    716 		{
    717 			auto objectAncestors = WwiseHelper::pathToAncestorPaths(objectPath);
    718 
    719 			for(int i = objectAncestors.size() - 1; i >= 0; --i)
    720 			{
    721 				args = buildArgs(objectAncestors[i]);
    722 
    723 				response.status = call(WaapiCommands::objectGet, args, options, result);
    724 
    725 				if(response.status)
    726 				{
    727 					fillResponse(response, result);
    728 
    729 					args = buildArgs(objectAncestors[i]);
    730 					addTransform(args, "ancestors");
    731 
    732 					response.status = call(WaapiCommands::objectGet, args, options, result);
    733 
    734 					if(response.status)
    735 					{
    736 						fillResponse(response, result);
    737 						break;
    738 					}
    739 				}
    740 			}
    741 		}
    742 
    743 		if(!response.status)
    744 			response.error = WaapiHelper::parseError(WaapiCommands::objectGet, result);
    745 
    746 		return response;
    747 	}
    748 
    749 	Waapi::Response<Waapi::ObjectResponse> WaapiClient::getObject(const juce::String& objectPath)
    750 	{
    751 		using namespace WwiseAuthoringAPI;
    752 
    753 		Waapi::Response<Waapi::ObjectResponse> response;
    754 
    755 		const auto args = AkJson::Map{
    756 			{
    757 				"from",
    758 				AkJson::Map{
    759 					{
    760 						"path",
    761 						AkJson::Array{
    762 							AkVariant{objectPath.toStdString()},
    763 						},
    764 					},
    765 				},
    766 			},
    767 		};
    768 
    769 		static const auto options = AkJson::Map{
    770 			{
    771 				"return",
    772 				AkJson::Array{
    773 					AkVariant{"id"},
    774 					AkVariant{"name"},
    775 					AkVariant{"type"},
    776 					AkVariant{"path"},
    777 					AkVariant{"sound:originalWavFilePath"},
    778 					AkVariant{"workunitType"},
    779 				},
    780 			},
    781 		};
    782 
    783 		AkJson result;
    784 		response.status = call(WaapiCommands::objectGet, args, options, result);
    785 
    786 		if(response.status)
    787 		{
    788 			if(result.HasKey("return"))
    789 			{
    790 				auto objects = result["return"].GetArray();
    791 
    792 				for(auto& object : objects)
    793 				{
    794 					response.result = object;
    795 				}
    796 			}
    797 		}
    798 		// Special Case: The call actually succeeds but does not find the object
    799 		else if(result.HasKey("uri") && result["uri"].GetVariant().GetString() == WaapiURIs::unknownObject)
    800 		{
    801 			response.status = true;
    802 		}
    803 		else
    804 		{
    805 			response.error = WaapiHelper::parseError(WaapiCommands::objectGet, result);
    806 		}
    807 
    808 		return response;
    809 	}
    810 
    811 	void WaapiClient::beginUndoGroup()
    812 	{
    813 		using namespace WwiseAuthoringAPI;
    814 
    815 		AkJson result;
    816 		call(WaapiCommands::undoBeginGroup, AkJson::Map{}, AkJson::Map{}, result);
    817 	}
    818 
    819 	void WaapiClient::cancelUndoGroup()
    820 	{
    821 		using namespace WwiseAuthoringAPI;
    822 
    823 		AkJson result;
    824 		call(WaapiCommands::undoCancelGroup, AkJson::Map{}, AkJson::Map{}, result);
    825 	}
    826 
    827 	void WaapiClient::endUndoGroup(const juce::String& displayName)
    828 	{
    829 		using namespace WwiseAuthoringAPI;
    830 
    831 		auto arguments = AkJson::Map{
    832 			{
    833 				"displayName",
    834 				AkVariant{displayName.toUTF8()},
    835 			},
    836 		};
    837 
    838 		AkJson result;
    839 		call(WaapiCommands::undoEndGroup, arguments, AkJson::Map{}, result);
    840 	}
    841 
    842 	Waapi::Response<Waapi::ObjectResponseSet> WaapiClient::pasteProperties(const Waapi::PastePropertiesRequest& pastePropertiesRequest)
    843 	{
    844 		using namespace WwiseAuthoringAPI;
    845 
    846 		Waapi::Response<Waapi::ObjectResponseSet> response;
    847 
    848 		AkJson::Array targets;
    849 		for(auto& target : pastePropertiesRequest.targets)
    850 		{
    851 			targets.push_back(AkVariant(target.toStdString()));
    852 		}
    853 
    854 		const auto args(AkJson::Map{
    855 			{
    856 				"source",
    857 				AkVariant(pastePropertiesRequest.source.toStdString()),
    858 			},
    859 			{
    860 				"targets",
    861 				targets,
    862 			},
    863 		});
    864 
    865 		AkJson result;
    866 		response.status = call(WaapiCommands::objectPasteProperties, args, AkJson::Map{}, result);
    867 
    868 		if(!response.status)
    869 		{
    870 			response.error = WaapiHelper::parseError(WaapiCommands::objectPasteProperties, result);
    871 		}
    872 
    873 		return response;
    874 	}
    875 
    876 	Waapi::Response<Wwise::Version> WaapiClient::getVersion()
    877 	{
    878 		using namespace WwiseAuthoringAPI;
    879 
    880 		Waapi::Response<Wwise::Version> response;
    881 
    882 		AkJson result;
    883 		response.status = call(WaapiCommands::getInfo, AkJson::Map{}, AkJson::Map{}, result);
    884 
    885 		if(response.status)
    886 		{
    887 			if(result.HasKey("version") && result["version"].HasKey("displayName"))
    888 			{
    889 				response.result = {
    890 					result["version"]["year"].GetVariant().GetInt32(),
    891 					result["version"]["major"].GetVariant().GetInt32(),
    892 					result["version"]["minor"].GetVariant().GetInt32(),
    893 					result["version"]["build"].GetVariant().GetInt32(),
    894 				};
    895 			}
    896 		}
    897 		else
    898 		{
    899 			response.error = WaapiHelper::parseError(WaapiCommands::getInfo, result);
    900 		}
    901 
    902 		return response;
    903 	}
    904 
    905 	Waapi::Response<Waapi::ProjectInfo> WaapiClient::getProjectInfo()
    906 	{
    907 		using namespace WwiseAuthoringAPI;
    908 
    909 		Waapi::Response<Waapi::ProjectInfo> response;
    910 
    911 		static const auto args(AkJson::Map{
    912 			{
    913 				"from",
    914 				AkJson::Map{
    915 					{
    916 						"ofType",
    917 						AkJson::Array{
    918 							{AkVariant("Project")},
    919 						},
    920 					},
    921 				},
    922 			},
    923 		});
    924 
    925 		static const auto options(AkJson::Map{
    926 			{
    927 				"return",
    928 				AkJson::Array{
    929 					{AkVariant("filePath")},
    930 					{AkVariant("id")},
    931 				},
    932 			},
    933 		});
    934 
    935 		AkJson result;
    936 		response.status = call(WaapiCommands::objectGet, args, options, result);
    937 
    938 		if(response.status)
    939 		{
    940 			if(result.HasKey("return"))
    941 			{
    942 				const auto& returnResult = result["return"];
    943 
    944 				if(!returnResult.GetArray().empty())
    945 				{
    946 					auto projectPath = juce::String(returnResult[0]["filePath"].GetVariant().GetString());
    947 					auto projectId = juce::String(returnResult[0]["id"].GetVariant().GetString()).removeCharacters("{}");
    948 
    949 #ifndef WIN32
    950 					// Waapi returns the path as a windows path
    951 					projectPath = projectPath.replace("\\", juce::File::getSeparatorString()).replace("Y:", "~").replace("Z:", "/");
    952 #endif
    953 
    954 					response.result.projectPath = projectPath;
    955 					response.result.projectId = projectId;
    956 				}
    957 			}
    958 		}
    959 		else
    960 		{
    961 			response.error = WaapiHelper::parseError(WaapiCommands::objectGet, result);
    962 		}
    963 
    964 		return response;
    965 	}
    966 	Waapi::Response<Waapi::AdditionalProjectInfo> WaapiClient::getAdditionalProjectInfo()
    967 	{
    968 		using namespace WwiseAuthoringAPI;
    969 
    970 		Waapi::Response<Waapi::AdditionalProjectInfo> response;
    971 
    972 		AkJson result;
    973 		response.status = call(WaapiCommands::getProjectInfo, AkJson::Map{}, AkJson::Map{}, result);
    974 
    975 		if(result.HasKey("directories") && result["directories"].HasKey("originals"))
    976 		{
    977 			auto originalsFolder = juce::String(result["directories"]["originals"].GetVariant().GetString());
    978 
    979 #ifndef WIN32
    980 			// Waapi returns the path as a windows path
    981 			originalsFolder = originalsFolder.replace("\\", juce::File::getSeparatorString()).replace("Y:", "~").replace("Z:", "/");
    982 #endif
    983 
    984 			response.result.originalsFolder = originalsFolder;
    985 		}
    986 
    987 		if(result.HasKey("referenceLanguageId") && result.HasKey("languages"))
    988 		{
    989 			const auto& referenceLanguageId = result["referenceLanguageId"].GetVariant().GetString();
    990 			const auto& languages = result["languages"].GetArray();
    991 
    992 			for(const auto& languageAsAkJson : languages)
    993 			{
    994 				const auto& language = languageAsAkJson.GetMap();
    995 				const auto& langIdIt = language.find("id");
    996 				const auto& langNameIt = language.find("name");
    997 
    998 				if(langIdIt != language.cend() && langNameIt != language.cend())
    999 				{
   1000 					if(langIdIt->second.GetVariant().GetString() == referenceLanguageId)
   1001 					{
   1002 						response.result.referenceLanguage = langNameIt->second.GetVariant().GetString();
   1003 						break;
   1004 					}
   1005 				}
   1006 			}
   1007 		}
   1008 
   1009 		return response;
   1010 	}
   1011 	Waapi::Response<Waapi::ObjectResponse> WaapiClient::getSelectedObject()
   1012 	{
   1013 		using namespace WwiseAuthoringAPI;
   1014 
   1015 		Waapi::Response<Waapi::ObjectResponse> response;
   1016 
   1017 		static const auto options = AkJson::Map{
   1018 			{
   1019 				"return",
   1020 				AkJson::Array{
   1021 					AkVariant{"id"},
   1022 					AkVariant{"name"},
   1023 					AkVariant{"type"},
   1024 					AkVariant{"path"},
   1025 				},
   1026 			},
   1027 		};
   1028 
   1029 		AkJson result;
   1030 		response.status = call(WaapiCommands::getSelectedObjects, AkJson::Map{}, options, result);
   1031 
   1032 		if(response.status)
   1033 		{
   1034 			if(result.HasKey("objects"))
   1035 			{
   1036 				const auto& returnObjects = result["objects"];
   1037 
   1038 				if(!returnObjects.GetArray().empty())
   1039 					response.result = Waapi::ObjectResponse(returnObjects.GetArray()[0]);
   1040 			}
   1041 		}
   1042 		else
   1043 		{
   1044 			response.error = WaapiHelper::parseError(WaapiCommands::getSelectedObjects, result);
   1045 		}
   1046 
   1047 		return response;
   1048 	}
   1049 } // namespace AK::WwiseTransfer