WwiseBuilderWorker.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <WwiseBuilderWorker.h>
  9. #include <AzCore/Debug/Trace.h>
  10. #include <AzCore/IO/FileIO.h>
  11. #include <AzCore/IO/Path/Path.h>
  12. #include <AzCore/IO/SystemFile.h>
  13. #include <AzCore/JSON/document.h>
  14. #include <AzCore/JSON/rapidjson.h>
  15. #include <AzCore/StringFunc/StringFunc.h>
  16. namespace WwiseBuilder
  17. {
  18. const char WwiseBuilderWindowName[] = "WwiseBuilder";
  19. namespace Internal
  20. {
  21. const char SoundbankDependencyFileExtension[] = ".bankdeps";
  22. const char JsonDependencyKey[] = "dependencies";
  23. AZ::Outcome<AZStd::string, AZStd::string> GetDependenciesFromMetadata(const rapidjson::Value& rootObject, AZStd::vector<AZStd::string>& fileNames)
  24. {
  25. if (!rootObject.IsObject())
  26. {
  27. return AZ::Failure(AZStd::string("The root of the metadata file is not an object. "
  28. "Please regenerate the dependencies metadata for this soundbank."));
  29. }
  30. // If the file doesn't define a dependency field, then there are no dependencies.
  31. if (!rootObject.HasMember(JsonDependencyKey))
  32. {
  33. AZStd::string addingDefaultDependencyWarning = AZStd::string::format(
  34. "Dependencies array does not exist - the .bankdeps file may have been manually edited. "
  35. "Registering a default dependency on %s. Dependencies may need to be regenerated via the authoring tool scripts.",
  36. Audio::Wwise::InitBank);
  37. fileNames.push_back(Audio::Wwise::InitBank);
  38. return AZ::Success(addingDefaultDependencyWarning);
  39. }
  40. const rapidjson::Value& dependenciesArray = rootObject[JsonDependencyKey];
  41. if (!dependenciesArray.IsArray())
  42. {
  43. return AZ::Failure(AZStd::string("Dependency field is not an array. Please regenerate the dependencies metadata for this soundbank."));
  44. }
  45. for (rapidjson::SizeType dependencyIndex = 0; dependencyIndex < dependenciesArray.Size(); ++dependencyIndex)
  46. {
  47. fileNames.push_back(dependenciesArray[dependencyIndex].GetString());
  48. }
  49. // Make sure init.bnk is a dependency. Force-add it if it's not.
  50. // Look for init.bnk in the dependencies file list...
  51. auto iter = AZStd::find_if(
  52. fileNames.begin(), fileNames.end(),
  53. [](AZStd::string fileName) -> bool
  54. {
  55. // use a string copy argument in order to to_lower it...
  56. AZStd::to_lower(fileName.begin(), fileName.end());
  57. return fileName == Audio::Wwise::InitBank;
  58. });
  59. if (iter == fileNames.end())
  60. {
  61. // Init bank wasn't found, which likely means it was modified by hand. However, every bank is dependent
  62. // on init.bnk (other than itself), so force-add it as a dependency here and return a warning message.
  63. AZStd::string dependencyWarning;
  64. if (fileNames.empty())
  65. {
  66. dependencyWarning = AZStd::string::format(
  67. "Dependencies array is empty - the .bankdeps file may have been manually edited. "
  68. "Registering a default dependency on %s. Dependencies may need to be regenerated via the authoring tool scripts.",
  69. Audio::Wwise::InitBank);
  70. }
  71. else
  72. {
  73. dependencyWarning = AZStd::string::format(
  74. "Dependencies did not contain the initialization bank - it may have been manually removed from the .bankdeps file. "
  75. "It is necessary for all banks to declare %s as a dependency, so it has been automatically added. "
  76. "Dependencies may need to be regenerated via the authoring tool scripts.",
  77. Audio::Wwise::InitBank);
  78. }
  79. fileNames.push_back(Audio::Wwise::InitBank);
  80. return AZ::Success(dependencyWarning);
  81. }
  82. return AZ::Success(AZStd::string());
  83. }
  84. }
  85. WwiseBuilderWorker::WwiseBuilderWorker()
  86. : m_isShuttingDown(false)
  87. {
  88. }
  89. void WwiseBuilderWorker::ShutDown()
  90. {
  91. // This will be called on a different thread than the process job thread
  92. m_isShuttingDown = true;
  93. }
  94. void WwiseBuilderWorker::Initialize()
  95. {
  96. AZ::IO::Path configFile("@projectroot@");
  97. configFile /= Audio::Wwise::DefaultBanksPath;
  98. configFile /= Audio::Wwise::ConfigFile;
  99. if (AZ::IO::FileIOBase::GetInstance()->Exists(configFile.c_str()))
  100. {
  101. m_wwiseConfig.Load(configFile.Native());
  102. }
  103. m_initialized = true;
  104. }
  105. // This happens early on in the file scanning pass.
  106. // This function should always create the same jobs and not do any checking whether the job is up to date.
  107. void WwiseBuilderWorker::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
  108. {
  109. if (m_isShuttingDown)
  110. {
  111. response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
  112. return;
  113. }
  114. if (!m_initialized)
  115. {
  116. Initialize();
  117. }
  118. AZStd::string jobKey = "Wwise";
  119. if (AZ::StringFunc::EndsWith(request.m_sourceFile, Audio::Wwise::MediaExtension))
  120. {
  121. jobKey.append(" Media");
  122. }
  123. else if (AZ::StringFunc::EndsWith(request.m_sourceFile, Audio::Wwise::BankExtension))
  124. {
  125. jobKey.append(" Bank");
  126. }
  127. for (const AssetBuilderSDK::PlatformInfo& info : request.m_enabledPlatforms)
  128. {
  129. // If there are no platform mappings (i.e. there was no config file), we want to
  130. // process the job anyways.
  131. bool createJob = m_wwiseConfig.m_platformMappings.empty();
  132. // If the config file was parsed, need to filter out jobs that don't apply.
  133. for (const auto& platformConfig : m_wwiseConfig.m_platformMappings)
  134. {
  135. // Check if the job request should go through.
  136. if (info.m_identifier == platformConfig.m_assetPlatform
  137. || info.m_identifier == platformConfig.m_altAssetPlatform)
  138. {
  139. AZStd::string sourceFile(request.m_sourceFile);
  140. AZStd::string_view banksPath(Audio::Wwise::DefaultBanksPath);
  141. if (AZ::StringFunc::StartsWith(sourceFile, banksPath))
  142. {
  143. // Remove the leading banks path from the source file...
  144. AZ::StringFunc::RKeep(sourceFile, banksPath.length(), true);
  145. }
  146. // If the source file now begins with the right Wwise platform folder, create the job...
  147. if (AZ::StringFunc::StartsWith(sourceFile, platformConfig.m_wwisePlatform, true))
  148. {
  149. createJob = true;
  150. break;
  151. }
  152. }
  153. }
  154. if (createJob)
  155. {
  156. AssetBuilderSDK::JobDescriptor descriptor;
  157. descriptor.m_jobKey = jobKey;
  158. descriptor.m_critical = true;
  159. descriptor.SetPlatformIdentifier(info.m_identifier.c_str());
  160. descriptor.m_priority = 0;
  161. response.m_createJobOutputs.push_back(descriptor);
  162. }
  163. }
  164. response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
  165. }
  166. // The request will contain the CreateJobResponse you constructed earlier, including any keys and
  167. // values you placed into the hash table
  168. void WwiseBuilderWorker::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
  169. {
  170. AZ_TracePrintf(AssetBuilderSDK::InfoWindow, "Starting Job.\n");
  171. AZ::IO::PathView fullPath(request.m_fullPath);
  172. if (m_isShuttingDown)
  173. {
  174. AZ_TracePrintf(AssetBuilderSDK::ErrorWindow, "Cancelled job %s because shutdown was requested.\n", request.m_fullPath.c_str());
  175. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
  176. return;
  177. }
  178. else
  179. {
  180. response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
  181. AssetBuilderSDK::JobProduct jobProduct(request.m_fullPath);
  182. // if the file is a bnk
  183. AZ::IO::PathView requestExtension = fullPath.Extension();
  184. if (requestExtension.Native() == Audio::Wwise::BankExtension)
  185. {
  186. AssetBuilderSDK::ProductPathDependencySet dependencyPaths;
  187. // Push assets back into the response's product list
  188. // Assets you created in your temp path can be specified using paths relative to the temp path
  189. // since that is assumed where you're writing stuff.
  190. AZ::Outcome<AZStd::string, AZStd::string> gatherProductDependenciesResponse = GatherProductDependencies(request.m_fullPath, request.m_sourceFile, dependencyPaths);
  191. if (!gatherProductDependenciesResponse.IsSuccess())
  192. {
  193. AZ_Error(WwiseBuilderWindowName, false, "Dependency gathering for %s failed. %s",
  194. request.m_fullPath.c_str(), gatherProductDependenciesResponse.GetError().c_str());
  195. }
  196. else
  197. {
  198. if (!gatherProductDependenciesResponse.GetValue().empty())
  199. {
  200. AZ_Warning(WwiseBuilderWindowName, false, "%s", gatherProductDependenciesResponse.GetValue().c_str());
  201. }
  202. jobProduct.m_pathDependencies = AZStd::move(dependencyPaths);
  203. }
  204. }
  205. response.m_outputProducts.push_back(jobProduct);
  206. }
  207. }
  208. AZ::Outcome<AZStd::string, AZStd::string> WwiseBuilderWorker::GatherProductDependencies(const AZStd::string& fullPath, const AZStd::string& relativePath, AssetBuilderSDK::ProductPathDependencySet& dependencies)
  209. {
  210. AZ::IO::Path bankMetadataPath(fullPath);
  211. bankMetadataPath.ReplaceExtension(Internal::SoundbankDependencyFileExtension);
  212. AZ::IO::Path relativeSoundsPath(relativePath, AZ::IO::PosixPathSeparator);
  213. relativeSoundsPath.RemoveFilename();
  214. AZStd::string success_message;
  215. // Look for the corresponding .bankdeps file next to the bank itself.
  216. if (!AZ::IO::SystemFile::Exists(bankMetadataPath.c_str()))
  217. {
  218. // If this is the init bank, skip it. Otherwise, register the init bank as a dependency, and warn that a full
  219. // dependency graph can't be created without a .bankdeps file for the bank.
  220. AZ::IO::PathView requestFileName = AZ::IO::PathView(fullPath).Filename();
  221. if (requestFileName != Audio::Wwise::InitBank)
  222. {
  223. success_message = AZStd::string::format(
  224. "Failed to find the metadata file %s for soundbank %s. Full dependency information cannot be determined without the "
  225. "metadata file. Please regenerate the metadata for this soundbank.",
  226. bankMetadataPath.c_str(), fullPath.c_str());
  227. }
  228. return AZ::Success(success_message);
  229. }
  230. AZ::u64 fileSize = AZ::IO::SystemFile::Length(bankMetadataPath.c_str());
  231. if (fileSize == 0)
  232. {
  233. return AZ::Failure(AZStd::string::format(
  234. "Soundbank metadata file at path %s is an empty file. Please regenerate the metadata for this soundbank.",
  235. bankMetadataPath.c_str()));
  236. }
  237. AZStd::vector<char> buffer(fileSize + 1);
  238. buffer[fileSize] = 0;
  239. if (!AZ::IO::SystemFile::Read(bankMetadataPath.c_str(), buffer.data()))
  240. {
  241. return AZ::Failure(AZStd::string::format(
  242. "Failed to read the soundbank metadata file at path %s. Please make sure the file is not open or being edited by another "
  243. "program.",
  244. bankMetadataPath.c_str()));
  245. }
  246. // load the file
  247. rapidjson::Document bankMetadataDoc;
  248. bankMetadataDoc.Parse(buffer.data());
  249. if (bankMetadataDoc.GetParseError() != rapidjson::ParseErrorCode::kParseErrorNone)
  250. {
  251. return AZ::Failure(AZStd::string::format(
  252. "Failed to parse soundbank metadata at path %s into JSON. Please regenerate the metadata for this soundbank.",
  253. bankMetadataPath.c_str()));
  254. }
  255. AZStd::vector<AZStd::string> wwiseFiles;
  256. AZ::Outcome<AZStd::string, AZStd::string> gatherDependenciesResult = Internal::GetDependenciesFromMetadata(bankMetadataDoc, wwiseFiles);
  257. if (!gatherDependenciesResult.IsSuccess())
  258. {
  259. return AZ::Failure(AZStd::string::format(
  260. "Dependency metadata file %s was processed, with errors:\n%s", bankMetadataPath.c_str(),
  261. gatherDependenciesResult.GetError().c_str()));
  262. }
  263. else if (!gatherDependenciesResult.GetValue().empty())
  264. {
  265. success_message = AZStd::string::format(
  266. "Dependency metadata file %s was processed, with warnings:\n%s", bankMetadataPath.c_str(),
  267. gatherDependenciesResult.GetValue().c_str());
  268. }
  269. // Register dependencies stored in the file to the job response. (they'll be relative to the bank itself.)
  270. for (const AZStd::string& wwiseFile : wwiseFiles)
  271. {
  272. dependencies.emplace((relativeSoundsPath / wwiseFile).Native(), AssetBuilderSDK::ProductPathDependencyType::ProductFile);
  273. }
  274. return AZ::Success(success_message);
  275. }
  276. } // namespace WwiseBuilder