GeneratedSoundBanksDirectoryWatcher.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. /*******************************************************************************
  2. The content of this file includes portions of the proprietary AUDIOKINETIC Wwise
  3. Technology released in source code form as part of the game integration package.
  4. The content of this file may not be used without valid licenses to the
  5. AUDIOKINETIC Wwise Technology.
  6. Note that the use of the game engine is subject to the Unreal(R) Engine End User
  7. License Agreement at https://www.unrealengine.com/en-US/eula/unreal
  8. License Usage
  9. Licensees holding valid licenses to the AUDIOKINETIC Wwise Technology may use
  10. this file in accordance with the end user license agreement provided with the
  11. software or, alternatively, in accordance with the terms contained
  12. in a written agreement between you and Audiokinetic Inc.
  13. Copyright (c) 2023 Audiokinetic Inc.
  14. *******************************************************************************/
  15. #include "AssetManagement/GeneratedSoundBanksDirectoryWatcher.h"
  16. #include "AkAudioModule.h"
  17. #include "AkAudioStyle.h"
  18. #include "AkSettings.h"
  19. #include "AkSettingsPerUser.h"
  20. #include "WwiseUnrealHelper.h"
  21. #include "IAudiokineticTools.h"
  22. #include "DirectoryWatcherModule.h"
  23. #include "Async/Async.h"
  24. #include "Framework/Docking/TabManager.h"
  25. #include "Framework/Notifications/NotificationManager.h"
  26. #include "Wwise/WwiseProjectDatabase.h"
  27. #include "Wwise/WwiseProjectDatabaseDelegates.h"
  28. #include "Wwise/WwiseSoundEngineModule.h"
  29. #include "Wwise/Metadata/WwiseMetadataProjectInfo.h"
  30. #define LOCTEXT_NAMESPACE "AkAudio"
  31. bool GeneratedSoundBanksDirectoryWatcher::DoesWwiseProjectExist()
  32. {
  33. return FPaths::FileExists(WwiseUnrealHelper::GetWwiseProjectPath());
  34. }
  35. void GeneratedSoundBanksDirectoryWatcher::CheckIfCachePathChanged()
  36. {
  37. auto* ProjectDatabase = FWwiseProjectDatabase::Get();
  38. if (UNLIKELY(!ProjectDatabase) || !IWwiseProjectDatabaseModule::ShouldInitializeProjectDatabase())
  39. {
  40. return;
  41. }
  42. const FWwiseDataStructureScopeLock DataStructure(*ProjectDatabase);
  43. const FWwiseRefPlatform Platform = DataStructure.GetPlatform(ProjectDatabase->GetCurrentPlatform());
  44. if (auto* ProjectInfo = Platform.ProjectInfo.GetProjectInfo())
  45. {
  46. const FString SourceCachePath = WwiseUnrealHelper::GetSoundBankDirectory() / ProjectInfo->CacheRoot.ToString();
  47. if (SourceCachePath != CachePath || !CacheChangedHandle.IsValid())
  48. {
  49. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher::CheckIfCachePathChanged: Cache path changed, restarting cache watcher."));
  50. StopCacheWatcher();
  51. StartCacheWatcher(SourceCachePath);
  52. }
  53. }
  54. }
  55. void GeneratedSoundBanksDirectoryWatcher::Initialize()
  56. {
  57. ProjectParsedHandle = FWwiseProjectDatabaseDelegates::Get()->GetOnDatabaseUpdateCompletedDelegate().AddRaw(this, &GeneratedSoundBanksDirectoryWatcher::CheckIfCachePathChanged);
  58. if (UAkSettings* AkSettings = GetMutableDefault<UAkSettings>())
  59. {
  60. // When GeneratedSoundBanks folder changes we need to reset the watcher
  61. if (SettingsChangedHandle.IsValid())
  62. {
  63. AkSettings->OnGeneratedSoundBanksPathChanged.Remove(SettingsChangedHandle);
  64. SettingsChangedHandle.Reset();
  65. }
  66. SettingsChangedHandle = AkSettings->OnGeneratedSoundBanksPathChanged.AddRaw(this, &GeneratedSoundBanksDirectoryWatcher::RestartWatchers);
  67. }
  68. if (UAkSettingsPerUser* UserSettings = GetMutableDefault<UAkSettingsPerUser>())
  69. {
  70. // When GeneratedSoundBanks Override folder changes we need to reset the watcher
  71. if (UserSettingsChangedHandle.IsValid())
  72. {
  73. UserSettings->OnGeneratedSoundBanksPathChanged.Remove(UserSettingsChangedHandle);
  74. UserSettingsChangedHandle.Reset();
  75. }
  76. UserSettingsChangedHandle = UserSettings->OnGeneratedSoundBanksPathChanged.AddRaw(this, &GeneratedSoundBanksDirectoryWatcher::RestartWatchers);
  77. }
  78. StartWatchers();
  79. }
  80. void GeneratedSoundBanksDirectoryWatcher::StartWatchers()
  81. {
  82. if (!IWwiseProjectDatabaseModule::ShouldInitializeProjectDatabase())
  83. {
  84. return;
  85. }
  86. //Start GeneratedSoundBanksWatcher to watch files which can be updated by source control (or direct manipulation)
  87. StartSoundBanksWatcher(WwiseUnrealHelper::GetSoundBankDirectory());
  88. // If there is a wwise project, we also watch the cache root file which notifies us when bank generation is done
  89. if (DoesWwiseProjectExist())
  90. {
  91. auto* ProjectDatabase = FWwiseProjectDatabase::Get();
  92. if (UNLIKELY(!ProjectDatabase))
  93. {
  94. UE_LOG(LogAudiokineticTools, Warning, TEXT("GeneratedSoundBanksDirectoryWatcher::StartWatchers: Could not get WwiseProjectDatabase. Wwise Cache watcher will not be initialized"));
  95. return;
  96. }
  97. const FWwiseDataStructureScopeLock DataStructure(*ProjectDatabase);
  98. const FWwiseRefPlatform Platform = DataStructure.GetPlatform(ProjectDatabase->GetCurrentPlatform());
  99. if (Platform.IsValid())
  100. {
  101. if (auto* ProjectInfo = Platform.ProjectInfo.GetProjectInfo())
  102. {
  103. const FString SourceCachePath = WwiseUnrealHelper::GetSoundBankDirectory() / ProjectInfo->CacheRoot.ToString();
  104. StartCacheWatcher(SourceCachePath);
  105. }
  106. }
  107. else
  108. {
  109. UE_LOG(LogAudiokineticTools, Warning, TEXT("GeneratedSoundBanksDirectoryWatcher::StartWatchers: Could not get Project Info for current platform from WwiseProjectDatabase. Wwise Cache watcher will not be initialized"));
  110. }
  111. }
  112. }
  113. bool GeneratedSoundBanksDirectoryWatcher::StartCacheWatcher(const FString& InCachePath)
  114. {
  115. if (CacheChangedHandle.IsValid())
  116. {
  117. StopCacheWatcher();
  118. }
  119. if (!FPaths::DirectoryExists(InCachePath))
  120. {
  121. UE_LOG(LogAudiokineticTools, Log, TEXT("GeneratedSoundBanksDirectoryWatcher::StartCacheWatcher: Cache directory to watch does not exist %s"), *InCachePath);
  122. bCacheFolderExists = false;
  123. return false;
  124. }
  125. bCacheFolderExists = true;
  126. CachePath = InCachePath;
  127. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher::StartCacheWatcher: Starting cache watcher - %s."), *CachePath);
  128. auto& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
  129. return DirectoryWatcherModule.Get()->RegisterDirectoryChangedCallback_Handle(
  130. CachePath
  131. , IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &GeneratedSoundBanksDirectoryWatcher::OnCacheChanged)
  132. , CacheChangedHandle
  133. , IDirectoryWatcher::WatchOptions::IgnoreChangesInSubtree
  134. );
  135. }
  136. void GeneratedSoundBanksDirectoryWatcher::StartSoundBanksWatcher(const FString& GeneratedSoundBanksFolder)
  137. {
  138. if (GeneratedSoundBanksHandle.IsValid())
  139. {
  140. StopSoundBanksWatcher();
  141. }
  142. if (!FPaths::DirectoryExists(GeneratedSoundBanksFolder))
  143. {
  144. UE_LOG(LogAudiokineticTools, Warning, TEXT("GeneratedSoundBanksDirectoryWatcher::StartSoundBanksWatcher: Generated Soundbanks Folder '%s' to watch not found.\nMake sure the Generated SoundBanks Folder setting is correct and ensure that SoundBanks are generated. Press the 'Refresh' button in the Wwise Browser to restart the watcher."), *GeneratedSoundBanksFolder);
  145. bGeneratedSoundBanksFolderExists = false;
  146. return;
  147. }
  148. bGeneratedSoundBanksFolderExists = true;
  149. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher::StartSoundBanksWatcher: Starting Generated Soundbanks watcher - %s."), *GeneratedSoundBanksFolder);
  150. SoundBankDirectory = GeneratedSoundBanksFolder;
  151. auto& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
  152. DirectoryWatcherModule.Get()->RegisterDirectoryChangedCallback_Handle(
  153. GeneratedSoundBanksFolder
  154. , IDirectoryWatcher::FDirectoryChanged::CreateRaw(this, &GeneratedSoundBanksDirectoryWatcher::OnGeneratedSoundBanksChanged)
  155. , GeneratedSoundBanksHandle
  156. , IDirectoryWatcher::WatchOptions::IncludeDirectoryChanges
  157. );
  158. }
  159. void GeneratedSoundBanksDirectoryWatcher::OnCacheChanged(const TArray<FFileChangeData>& ChangedFiles)
  160. {
  161. for (FFileChangeData FileData : ChangedFiles)
  162. {
  163. const FString FileName = FPaths::GetBaseFilename(FileData.Filename);
  164. if (FileName == TEXT("SoundBankInfoCache"))
  165. {
  166. if (bParseTimerRunning)
  167. {
  168. UpdateNotificationOnGenerationComplete();
  169. EndParseTimer();
  170. }
  171. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher: SoundBankInfoCache updated."));
  172. OnSoundBankGenerationDone();
  173. break;
  174. }
  175. }
  176. UE_LOG(LogAudiokineticTools, VeryVerbose, TEXT("GeneratedSoundBanksDirectoryWatcher: Modifications to files in Wwise project cache detected."));
  177. }
  178. void GeneratedSoundBanksDirectoryWatcher::OnGeneratedSoundBanksChanged(const TArray<FFileChangeData>& ChangedFiles)
  179. {
  180. ParseTimer = ParseDelaySeconds;
  181. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher: %d files changed in the Generated Soundbanks folder."), ChangedFiles.Num());
  182. if (!PostEditorTickHandle.IsValid() && !bParseTimerRunning)
  183. {
  184. bParseTimerRunning = true;
  185. PostEditorTickHandle = GEngine->OnPostEditorTick().AddRaw(this, &GeneratedSoundBanksDirectoryWatcher::TimerTick);
  186. NotifyFilesChanged();
  187. }
  188. }
  189. bool GeneratedSoundBanksDirectoryWatcher::ShouldRestartWatchers()
  190. {
  191. const bool bCacheWatcherNeedsRestart = DoesWwiseProjectExist() && (!bCacheFolderExists || !CacheChangedHandle.IsValid());
  192. const bool bGeneratedSoundBanksWatcherNeedsRestart = !bGeneratedSoundBanksFolderExists || !GeneratedSoundBanksHandle.IsValid();
  193. return bCacheWatcherNeedsRestart || bGeneratedSoundBanksWatcherNeedsRestart;
  194. }
  195. void GeneratedSoundBanksDirectoryWatcher::TimerTick(float DeltaSeconds)
  196. {
  197. if (ParseTimer < 0 && bParseTimerRunning)
  198. {
  199. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher: No files have changed in the last %d seconds."), ParseDelaySeconds);
  200. OnSoundBankGenerationDone();
  201. EndParseTimer();
  202. }
  203. else if (!bParseTimerRunning)
  204. {
  205. EndParseTimer();
  206. }
  207. else if (ParseTimer > 0)
  208. {
  209. ParseTimer -= DeltaSeconds;
  210. }
  211. UpdateNotification();
  212. }
  213. void GeneratedSoundBanksDirectoryWatcher::EndParseTimer()
  214. {
  215. bParseTimerRunning = false;
  216. ParseTimer = 0;
  217. GEngine->OnPostEditorTick().Remove(PostEditorTickHandle);
  218. PostEditorTickHandle.Reset();
  219. HideNotification();
  220. }
  221. void GeneratedSoundBanksDirectoryWatcher::OnSoundBankGenerationDone() const
  222. {
  223. UE_LOG(LogAudiokineticTools, Verbose, TEXT("GeneratedSoundBanksDirectoryWatcher: Soundbank generation done."));
  224. OnSoundBanksGenerated.Broadcast();
  225. }
  226. void GeneratedSoundBanksDirectoryWatcher::NotifyFilesChanged()
  227. {
  228. if (!FApp::CanEverRender())
  229. {
  230. return;
  231. }
  232. AsyncTask(ENamedThreads::Type::GameThread, [this]
  233. {
  234. FText InfoString = LOCTEXT("GeneratedSoundbanksWatcherInfoString", "Changes in generated soundbanks detected. \nProject data will be re-parsed in {parseSeconds} seconds.");
  235. FFormatNamedArguments NamedArguments;
  236. NamedArguments.Add(TEXT("parseSeconds"), static_cast<int>(this->ParseTimer));
  237. InfoString = FText::Format(InfoString, NamedArguments);
  238. FNotificationInfo Info(InfoString);
  239. Info.Image = FAkAudioStyle::GetBrush(TEXT("AudiokineticTools.AkBrowserTabIcon"));
  240. Info.bFireAndForget = false;
  241. Info.FadeOutDuration = 0.5f;
  242. Info.ExpireDuration = 0.0f;
  243. #if UE_4_26_OR_LATER
  244. Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FGlobalTabmanager::Get()->TryInvokeTab(FName("OutputLog")); });
  245. #else
  246. Info.Hyperlink = FSimpleDelegate::CreateLambda([]() { FGlobalTabmanager::Get()->InvokeTab(FName("OutputLog")); });
  247. #endif
  248. Info.HyperlinkText = LOCTEXT("ShowOutputLogHyperlink", "Show Output Log");
  249. this->NotificationItem = FSlateNotificationManager::Get().AddNotification(Info);
  250. });
  251. }
  252. void GeneratedSoundBanksDirectoryWatcher::HideNotification()
  253. {
  254. if (!FApp::CanEverRender())
  255. {
  256. return;
  257. }
  258. AsyncTask(ENamedThreads::Type::GameThread, [this]
  259. {
  260. if (this->NotificationItem)
  261. {
  262. this->NotificationItem->Fadeout();
  263. }
  264. });
  265. }
  266. void GeneratedSoundBanksDirectoryWatcher::UpdateNotificationOnGenerationComplete() const
  267. {
  268. if (!FApp::CanEverRender())
  269. {
  270. return;
  271. }
  272. AsyncTask(ENamedThreads::Type::GameThread, [this]
  273. {
  274. if (this->NotificationItem)
  275. {
  276. FText InfoString = LOCTEXT("GeneratedSoundbanksWatcherInfoString", "Detected that sound data generation is finished.");
  277. this->NotificationItem->SetText(InfoString);
  278. this->NotificationItem->SetFadeOutDuration(2.0f);
  279. }
  280. });
  281. }
  282. void GeneratedSoundBanksDirectoryWatcher::UpdateNotification() const
  283. {
  284. if (!FApp::CanEverRender())
  285. {
  286. return;
  287. }
  288. AsyncTask(ENamedThreads::Type::GameThread, [this]
  289. {
  290. if (this->NotificationItem)
  291. {
  292. FText InfoString = LOCTEXT("GeneratedSoundbanksWatcherInfoString", "Changes in generated soundbanks detected. \nProject data will be re-parsed in {parseSeconds} seconds.");
  293. FFormatNamedArguments NamedArguments;
  294. NamedArguments.Add(TEXT("parseSeconds"), static_cast<int>(this->ParseTimer));
  295. InfoString = FText::Format(InfoString, NamedArguments);
  296. this->NotificationItem->SetText(InfoString);
  297. }
  298. });
  299. }
  300. void GeneratedSoundBanksDirectoryWatcher::StopWatchers()
  301. {
  302. StopCacheWatcher();
  303. StopSoundBanksWatcher();
  304. }
  305. void GeneratedSoundBanksDirectoryWatcher::StopSoundBanksWatcher()
  306. {
  307. if (GeneratedSoundBanksHandle.IsValid())
  308. {
  309. auto& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
  310. DirectoryWatcherModule.Get()->UnregisterDirectoryChangedCallback_Handle(SoundBankDirectory, GeneratedSoundBanksHandle);
  311. GeneratedSoundBanksHandle.Reset();
  312. }
  313. }
  314. void GeneratedSoundBanksDirectoryWatcher::StopCacheWatcher()
  315. {
  316. if (CacheChangedHandle.IsValid())
  317. {
  318. auto& DirectoryWatcherModule = FModuleManager::LoadModuleChecked<FDirectoryWatcherModule>(TEXT("DirectoryWatcher"));
  319. DirectoryWatcherModule.Get()->UnregisterDirectoryChangedCallback_Handle(CachePath, CacheChangedHandle);
  320. CacheChangedHandle.Reset();
  321. }
  322. }
  323. void GeneratedSoundBanksDirectoryWatcher::RestartWatchers()
  324. {
  325. AsyncTask(ENamedThreads::Type::GameThread, [this]
  326. {
  327. StopWatchers();
  328. StartWatchers();
  329. });
  330. }
  331. void GeneratedSoundBanksDirectoryWatcher::ConditionalRestartWatchers()
  332. {
  333. if(ShouldRestartWatchers())
  334. {
  335. RestartWatchers();
  336. }
  337. }
  338. void GeneratedSoundBanksDirectoryWatcher::Uninitialize(const bool bIsModuleShutdown)
  339. {
  340. StopWatchers();
  341. if (ProjectParsedHandle.IsValid())
  342. {
  343. FWwiseProjectDatabaseDelegates::Get()->GetOnDatabaseUpdateCompletedDelegate().Remove(ProjectParsedHandle);
  344. ProjectParsedHandle.Reset();
  345. }
  346. //Can't access settings while module is being shutdown
  347. if (!bIsModuleShutdown)
  348. {
  349. if (SettingsChangedHandle.IsValid())
  350. {
  351. if (UAkSettings* AkSettings = GetMutableDefault<UAkSettings>())
  352. {
  353. AkSettings->OnGeneratedSoundBanksPathChanged.Remove(SettingsChangedHandle);
  354. }
  355. SettingsChangedHandle.Reset();
  356. }
  357. if (UserSettingsChangedHandle.IsValid())
  358. {
  359. if (UAkSettingsPerUser* UserSettings = GetMutableDefault<UAkSettingsPerUser>())
  360. {
  361. UserSettings->OnGeneratedSoundBanksPathChanged.Remove(UserSettingsChangedHandle);
  362. }
  363. UserSettingsChangedHandle.Reset();
  364. }
  365. }
  366. }
  367. #undef LOCTEXT_NAMESPACE