ReaWwise

REAPER extension
Log | Files | Refs | Submodules

commit 6397e72977b9315e06b91fd2cb0d87958b2dc92c
parent 208a34d1812a2e20541c5173c27a20b1d718f021
Author: Andrew Costa <acosta@audiokinetic.com>
Date:   Tue, 20 Sep 2022 10:21:37 -0400

New Features:

* Added "ReaWwise: Open", "ReaWwise: Close" and "ReaWwise: Transfer To Wwise" actions to REAPER's action list. And added AK_Json_Clear and AK_Json_ClearAll functions to ReaScript.

Miscellaneous Changes:

* Made read-only text fields look different from editable ones.

* Added focus feedback for drop-down lists.

* Added more tooltips throughout the user interface.

* Added Actor-Mixer as a possible Object Type for Wwise structures.

Bug Fixes:

* Fixed: A corrupted preset file is created when clicking Cancel in the Wwise structures Save Preset dialog.

* Fixed: Transfer to Wwise button is still available when there are no items in preview pane.

* Fixed: Import details are wrong when Originals folder is blank.

* Fixed: When attempting to create a Sound SFX directly under a Physical Folder (an illegal parent/child relationship), no error is generated and the audio files are added to the Originals folder.

Change-Id: I0de0150ab48a7c84f6edb3782cd7cbfebbab7d86

Diffstat:
M3rd/reaper-sdk/sdk/reaper_plugin_functions.h | 2+-
Msrc/extension/CMakeLists.txt | 76++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/extension/Extension.cpp | 325++++++++++++++++++++++++++++++++++---------------------------------------------
Dsrc/extension/ExtensionWindow.cpp | 62--------------------------------------------------------------
Dsrc/extension/ExtensionWindow.h | 23-----------------------
Asrc/extension/IReaperPlugin.h | 27+++++++++++++++++++++++++++
Dsrc/extension/MacHelpers.h | 8--------
Dsrc/extension/MacHelpers.mm | 16----------------
Msrc/extension/ReaperContext.cpp | 261+++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/extension/ReaperContext.h | 44+++++++++++++-------------------------------
Asrc/extension/ReaperPlugin.cpp | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/extension/ReaperPlugin.h | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/shared/Core/DawWatcher.cpp | 5+++--
Msrc/shared/Core/DawWatcher.h | 1+
Msrc/shared/Core/WaapiClient.cpp | 67+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/shared/Core/WaapiClient.h | 12++++++++++++
Asrc/shared/Helpers/StringHelper.h | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/shared/Helpers/WwiseHelper.h | 4++--
Msrc/shared/Persistance/ApplicationStateValidator.cpp | 96++++++++++++++++++++++++++++++++++++++++++++++---------------------------------
Msrc/shared/Persistance/ApplicationStateValidator.h | 8+++++---
Msrc/shared/Theme/CustomLookAndFeel.cpp | 96++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Msrc/shared/Theme/CustomLookAndFeel.h | 8+++++++-
Msrc/shared/UI/AboutComponent.h | 2++
Msrc/shared/UI/CustomDrawableButton.h | 2++
Msrc/shared/UI/ImportConflictsComponent.cpp | 46+++++++++++++++++++++-------------------------
Msrc/shared/UI/ImportConflictsComponent.h | 6++----
Msrc/shared/UI/ImportControlsComponent.cpp | 30++++++++++++++++++------------
Msrc/shared/UI/ImportControlsComponent.h | 4+++-
Msrc/shared/UI/ImportDestinationComponent.cpp | 29++++++++++++++++++++++-------
Msrc/shared/UI/ImportDestinationComponent.h | 3+++
Msrc/shared/UI/ImportPreviewComponent.h | 4++--
Msrc/shared/UI/LoadingComponent.h | 1+
Msrc/shared/UI/MainComponent.cpp | 7++++++-
Msrc/shared/UI/MainComponent.h | 4+++-
Asrc/shared/UI/MainWindow.cpp | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/shared/UI/MainWindow.h | 25+++++++++++++++++++++++++
Msrc/shared/UI/OriginalsSubfolderComponent.cpp | 5+++--
Msrc/shared/UI/PresetMenuComponent.cpp | 26+++++++++++++++++++-------
Msrc/shared/UI/SelectedRowPropertiesComponent.cpp | 6+++---
Msrc/shared/UI/TruncatableTextEditor.h | 2++
Msrc/standalone/Standalone.cpp | 13+++++++------
Msrc/standalone/Standalone.h | 4+++-
Msrc/standalone/StandaloneWindow.cpp | 32+++-----------------------------
Msrc/standalone/StandaloneWindow.h | 7++-----
Msrc/test/CMakeLists.txt | 2++
Asrc/test/ReaperContextTest.cpp | 455+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/test/WwiseHelperTests.h | 14++++++--------
47 files changed, 1527 insertions(+), 643 deletions(-)

diff --git a/3rd/reaper-sdk/sdk/reaper_plugin_functions.h b/3rd/reaper-sdk/sdk/reaper_plugin_functions.h @@ -2,7 +2,7 @@ #define _REAPER_PLUGIN_FUNCTIONS_H_ // REAPER API functions -// Generated by REAPER v6.67+dev0914/win32 +// Generated by REAPER v6.68+dev0927/win64 /* * Copyright 2006 and later, Cockos Incorporated diff --git a/src/extension/CMakeLists.txt b/src/extension/CMakeLists.txt @@ -15,11 +15,13 @@ if(WIN32) else() file(GLOB_RECURSE EXTENSION_SOURCES "${PROJECT_SOURCE_DIR}/*.h" - "${PROJECT_SOURCE_DIR}/*.cpp" - "${PROJECT_SOURCE_DIR}/*.mm") + "${PROJECT_SOURCE_DIR}/*.cpp") endif() add_library(${PROJECT_NAME} SHARED) +add_library(${PROJECT_NAME}_Static STATIC) + +set(PROJECT_LIST ${PROJECT_NAME} ${PROJECT_NAME}_Static) if(NOT DEFINED ENV{BUILD_NUMBER}) if(APPLE) @@ -42,19 +44,46 @@ if(NOT DEFINED ENV{BUILD_NUMBER}) endif() endif() - set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "") set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME reaper_reawwise) -target_sources(${PROJECT_NAME} PRIVATE ${EXTENSION_SOURCES}) +foreach(PROJ ${PROJECT_LIST}) + target_sources(${PROJ} PRIVATE ${EXTENSION_SOURCES}) -find_package(Reaper REQUIRED) + target_include_directories(${PROJ} + PUBLIC + ${PROJECT_SOURCE_DIR}/ + ) -target_link_libraries(${PROJECT_NAME} - PRIVATE - WwiseTransfer_Shared - Reaper -) + target_link_libraries(${PROJ} + PRIVATE + WwiseTransfer_Shared + Reaper + ) + + target_compile_definitions(${PROJ} + PRIVATE + REAPERAPI_MINIMAL + REAPERAPI_WANT_GetMainHwnd + REAPERAPI_WANT_AddExtensionsMainMenu + REAPERAPI_WANT_EnumProjects + REAPERAPI_WANT_GetSetProjectInfo_String + REAPERAPI_WANT_ResolveRenderPattern + REAPERAPI_WANT_Main_OnCommand + REAPERAPI_WANT_GetProjExtState + REAPERAPI_WANT_SetProjExtState + REAPERAPI_WANT_MarkProjectDirty + REAPERAPI_WANT_realloc_cmd_register_buf + REAPERAPI_WANT_realloc_cmd_clear + REAPERAPI_WANT_GetProjectStateChangeCount + REAPERAPI_WANT_GetSetProjectInfo + JUCE_APPLICATION_NAME_STRING="${PROJECT_NAME}" + JUCE_STANDALONE_APPLICATION=0 + JUCE_REMOVE_COMPONENT_FROM_DESKTOP_ON_WM_DESTROY=1 + ) +endforeach() + +find_package(Reaper REQUIRED) if(APPLE) if(DEFINED ENV{CODE_SIGN_IDENTITY_ID} AND DEFINED ENV{DEVELOPMENT_TEAM_ID}) @@ -64,32 +93,11 @@ if(APPLE) find_package(Swell REQUIRED) - target_link_libraries(${PROJECT_NAME} - PRIVATE - Swell - ) + foreach(PROJ ${PROJECT_LIST}) + target_link_libraries(${PROJ} PRIVATE Swell) + endforeach() endif() -target_compile_definitions(${PROJECT_NAME} - PRIVATE - REAPERAPI_MINIMAL - REAPERAPI_WANT_GetMainHwnd - REAPERAPI_WANT_AddExtensionsMainMenu - REAPERAPI_WANT_EnumProjects - REAPERAPI_WANT_GetSetProjectInfo_String - REAPERAPI_WANT_ResolveRenderPattern - REAPERAPI_WANT_Main_OnCommand - REAPERAPI_WANT_GetProjExtState - REAPERAPI_WANT_SetProjExtState - REAPERAPI_WANT_MarkProjectDirty - REAPERAPI_WANT_realloc_cmd_register_buf - REAPERAPI_WANT_realloc_cmd_clear - REAPERAPI_WANT_GetProjectStateChangeCount - REAPERAPI_WANT_GetSetProjectInfo - JUCE_APPLICATION_NAME_STRING="${PROJECT_NAME}" - JUCE_STANDALONE_APPLICATION=0 -) - source_group(TREE ${PROJECT_SOURCE_DIR} PREFIX "Source Files" FILES ${EXTENSION_SOURCES}) build_juce_source_groups() diff --git a/src/extension/Extension.cpp b/src/extension/Extension.cpp @@ -1,9 +1,10 @@ #define REAPERAPI_IMPLEMENT #include "Core/WaapiClient.h" -#include "ExtensionWindow.h" #include "ReaperContext.h" +#include "ReaperPlugin.h" #include "Theme/CustomLookAndFeel.h" +#include "UI/MainWindow.h" #include <JSONHelpers.h> #include <juce_events/juce_events.h> @@ -12,18 +13,12 @@ #include <tuple> #include <variant> -#ifdef __APPLE__ -#include "MacHelpers.h" -#endif - namespace AK::ReaWwise { - static bool juceInitialised = false; - static constexpr int defaultBufferSize = 4096; - static constexpr int largeBufferSize = 4 * 1024 * 1024; - static std::unique_ptr<ExtensionWindow> mainWindow; + static bool guiInitialised = false; + static std::unique_ptr<WwiseTransfer::MainWindow> mainWindow; static std::unique_ptr<ReaperContext> reaperContext; - static std::unique_ptr<ReaperPluginInterface> reaperPluginInterface; + static std::unique_ptr<IReaperPlugin> reaperPlugin; static std::string returnString; static std::string emptyReturnString; static double returnDouble; @@ -42,28 +37,32 @@ namespace AK::ReaWwise } } - static bool onHookCommand(int command, int flag) + static void initialiseGui() { - if(command == openReaperWwiseTransferCommandId) - { - if(!juceInitialised) - { - juce::initialiseJuce_GUI(); - juceInitialised = true; - } + juce::initialiseJuce_GUI(); + guiInitialised = true; + } - if(!mainWindow) - { - mainWindow = std::make_unique<ExtensionWindow>(*reaperContext); + static void initialiseMainWindow() + { + mainWindow = std::make_unique<WwiseTransfer::MainWindow>(*reaperContext, JUCE_APPLICATION_NAME_STRING, false); #ifdef WIN32 - mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), reaperPluginInterface->getMainHwnd()); + mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), reaperPlugin->getMainHwnd()); #else - mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), 0); - // MacHelpers::makeWindowFloatingPanel(dynamic_cast<juce::Component*>(mainWindow.get())); + mainWindow->addToDesktop(mainWindow->getDesktopWindowStyleFlags(), 0); #endif - } + } + + static bool onHookCommand(int command, int flag) + { + if(command == openReaperWwiseTransferCommandId) + { + if(!guiInitialised) + initialiseGui(); + + if(mainWindow == nullptr) + initialiseMainWindow(); - // TODO: There might be more stuff that needs to be done on visibility toggle, i.e. re-read app properties, keeping the extension alive mainWindow->setVisible(!mainWindow->isVisible()); return true; @@ -654,6 +653,74 @@ namespace AK::ReaWwise return (void*)AkJson_GetStatus(arguments.get<0>()); } + static const bool AkJson_Clear(std::shared_ptr<AkJsonRef>* akJsonRef) + { + if(isObjectValid(akJsonRef, objects)) + return objects.erase(*akJsonRef) == 1; + + return false; + } + + static void* AkJson_ClearVarArg(void** argv, int argc) + { + juce::ignoreUnused(argc); + + Arguments<std::shared_ptr<AkJsonRef>*> arguments(argv); + + return (void*)AkJson_Clear(arguments.get<0>()); + } + + static const bool AkJson_ClearAll() + { + objects.clear(); + + return objects.size() == 0; + } + + static void* AkJson_ClearAllVarArg(void** argv, int argc) + { + juce::ignoreUnused(argv, argc); + + return (void*)AkJson_ClearAll(); + } + + static void ReaWwise_Open() + { + if(!guiInitialised) + initialiseGui(); + + if(mainWindow == nullptr) + initialiseMainWindow(); + + if(!mainWindow->isVisible()) + mainWindow->setVisible(true); + } + + static void ReaWwise_Close() + { + if(mainWindow && mainWindow->isVisible()) + mainWindow->setVisible(false); + } + + static void ReaWwise_TransferToWwise() + { + if(mainWindow) + mainWindow->transferToWwise(); + } + + struct ApiAction + { + int id; + custom_action_register_t definition; + void (*function)(); + }; + + static ApiAction apiActions[] = { + ApiAction{0, {0, "AK_ReaWwise_Open", "ReaWwise: Open", nullptr}, &ReaWwise_Open}, + ApiAction{0, {0, "AK_ReaWwise_Close", "ReaWwise: Close", nullptr}, &ReaWwise_Close}, + ApiAction{0, {0, "AK_ReaWwise_TransferToWwise", "ReaWwise: Transfer To Wwise (Using current settings)", nullptr}, &ReaWwise_TransferToWwise}, + }; + struct ApiFunctionDefinition { const char* Api; @@ -691,6 +758,9 @@ namespace AK::ReaWwise AK_RWT_GENERATE_API_FUNC_DEF(AkJson_GetStatus, "bool", "*", "*", "Ak: Get the status of a result from a call to waapi"), + AK_RWT_GENERATE_API_FUNC_DEF(AkJson_Clear, "bool", "*", "*", "Ak: Clear object referenced by pointer"), + AK_RWT_GENERATE_API_FUNC_DEF(AkJson_ClearAll, "bool", "", "", "Ak: Clear all objects rederenced by pointers"), + AK_RWT_GENERATE_API_FUNC_DEF(AkVariant_Bool, "*", "bool", "bool", "Ak: Create a bool object"), AK_RWT_GENERATE_API_FUNC_DEF(AkVariant_GetBool, "bool", "*", "*", "Ak: Extract raw boolean value from bool object"), @@ -707,195 +777,70 @@ namespace AK::ReaWwise #undef AK_RWT_GENERATE_API_FUNC_DEF } // namespace Scripting - static int initialize(reaper_plugin_info_t* pluginInfo) + static bool onHookCommand2(KbdSectionInfo* sec, int command, int val, int val2, int relmode, HWND hwnd) { - class ReaperPluginImplementation : public ReaperPluginInterface + for(const auto& apiAction : Scripting::apiActions) { - public: - ReaperPluginImplementation(reaper_plugin_info_t* pluginInfo) - : pluginInfo(pluginInfo) - { - _getMainHwnd = decltype(GetMainHwnd)(pluginInfo->GetFunc("GetMainHwnd")); - _addExtensionsMainMenu = decltype(AddExtensionsMainMenu)(pluginInfo->GetFunc("AddExtensionsMainMenu")); - _enumProjects = decltype(EnumProjects)(pluginInfo->GetFunc("EnumProjects")); - _getSetProjectInfo_String = decltype(GetSetProjectInfo_String)(pluginInfo->GetFunc("GetSetProjectInfo_String")); - _resolveRenderPattern = decltype(ResolveRenderPattern)(pluginInfo->GetFunc("ResolveRenderPattern")); - _main_OnCommand = decltype(Main_OnCommand)(pluginInfo->GetFunc("Main_OnCommand")); - _getProjExtState = decltype(GetProjExtState)(pluginInfo->GetFunc("GetProjExtState")); - _setProjExtState = decltype(SetProjExtState)(pluginInfo->GetFunc("SetProjExtState")); - _markProjectDirty = decltype(MarkProjectDirty)(pluginInfo->GetFunc("MarkProjectDirty")); - _getProjectStateChangeCount = decltype(GetProjectStateChangeCount)(pluginInfo->GetFunc("GetProjectStateChangeCount")); - _getSetProjectInfo = decltype(GetSetProjectInfo)(pluginInfo->GetFunc("GetSetProjectInfo")); - _realloc_cmd_register_buf = decltype(realloc_cmd_register_buf)(pluginInfo->GetFunc("realloc_cmd_register_buf")); - _realloc_cmd_clear = decltype(realloc_cmd_clear)(pluginInfo->GetFunc("realloc_cmd_clear")); - } - - ~ReaperPluginImplementation() override = default; - - int getCallerVersion() const override - { - return pluginInfo->caller_version; - } - - int registerFunction(const char* name, void* infoStruct) const override - { - return pluginInfo->Register(name, infoStruct); - } - - bool isValid() const override - { - if(_getMainHwnd && - _addExtensionsMainMenu && - _enumProjects && - _getSetProjectInfo_String && - _resolveRenderPattern && - _main_OnCommand && - _getProjExtState && - _setProjExtState && - _markProjectDirty && - _getProjectStateChangeCount && - _getSetProjectInfo) - return true; - - return false; - } - - void* getMainHwnd() override - { - return _getMainHwnd(); - } - - bool addExtensionsMainMenu() override - { - return _addExtensionsMainMenu(); - } - - ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) override - { - return _enumProjects(idx, projfnOutOptional, projfnOutOptional_sz); - } - - juce::String getProjectString(ReaProject* proj, const char* key) override - { - juce::String projectString; - - if (realloc_cmd_register_buf && realloc_cmd_clear) - { - // For REAPER 6.68+ - char buffer[defaultBufferSize]; - char* bufferPtr = buffer; - - int bufferSize = (int)sizeof(buffer); - - int token = realloc_cmd_register_buf(&bufferPtr, &bufferSize); - - if (_getSetProjectInfo_String(proj, key, bufferPtr, false)) - projectString = juce::String(bufferPtr, bufferSize); - - realloc_cmd_clear(token); - } - else - { - static std::string buffer(largeBufferSize, '\0'); - - if (_getSetProjectInfo_String(proj, key, &buffer[0], false)) - projectString = buffer; - } - - return projectString; - } - - int resolveRenderPattern(ReaProject* project, const char* path, const char* pattern, char* targets, int targets_sz) override - { - return _resolveRenderPattern(project, path, pattern, targets, targets_sz); - } - - void main_OnCommand(int command, int flag) override - { - _main_OnCommand(command, flag); - } - - int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) override + if(apiAction.id == command) { - return _getProjExtState(proj, extname, key, valOutNeedBig, valOutNeedBig_sz); - } - - int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) override - { - return _setProjExtState(proj, extname, key, value); - } - - void markProjectDirty(ReaProject* proj) override - { - return _markProjectDirty(proj); - } - - int getProjectStateChangeCount(ReaProject* proj) override - { - return _getProjectStateChangeCount(proj); - } - - double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) override - { - return _getSetProjectInfo(proj, desc, value, is_set); + apiAction.function(); + return true; } + } - private: - reaper_plugin_info_t* pluginInfo; - - decltype(GetMainHwnd) _getMainHwnd; - decltype(AddExtensionsMainMenu) _addExtensionsMainMenu; - decltype(EnumProjects) _enumProjects; - decltype(GetSetProjectInfo_String) _getSetProjectInfo_String; - decltype(ResolveRenderPattern) _resolveRenderPattern; - decltype(Main_OnCommand) _main_OnCommand; - decltype(GetProjExtState) _getProjExtState; - decltype(SetProjExtState) _setProjExtState; - decltype(MarkProjectDirty) _markProjectDirty; - decltype(GetProjectStateChangeCount) _getProjectStateChangeCount; - decltype(GetSetProjectInfo) _getSetProjectInfo; - decltype(realloc_cmd_register_buf) _realloc_cmd_register_buf; - decltype(realloc_cmd_clear) _realloc_cmd_clear; - }; + return false; + } - reaperPluginInterface = std::make_unique<ReaperPluginImplementation>(pluginInfo); - reaperContext = std::make_unique<ReaperContext>(*reaperPluginInterface); + static int initialize(reaper_plugin_info_t* pluginInfo) + { + reaperPlugin = std::make_unique<ReaperPlugin>(pluginInfo); + reaperContext = std::make_unique<ReaperContext>(*reaperPlugin); // Should actually report errors to the user somehow - if(reaperPluginInterface->getCallerVersion() != REAPER_PLUGIN_VERSION) + if(reaperPlugin->getCallerVersion() != REAPER_PLUGIN_VERSION) { return 0; } // Checks that all function pointers needed from reaper are valid - if(!reaperPluginInterface->isValid()) + if(!reaperPlugin->isValid()) { return 0; } - openReaperWwiseTransferCommandId = reaperPluginInterface->registerFunction("command_id", (void*)"openReaperWwiseTransferCommand"); + openReaperWwiseTransferCommandId = reaperPlugin->registerFunction("command_id", (void*)"openReaperWwiseTransferCommand"); if(!openReaperWwiseTransferCommandId) { return 0; } - if(!reaperPluginInterface->registerFunction("hookcommand", (void*)onHookCommand)) + if(!reaperPlugin->registerFunction("hookcommand2", (void*)onHookCommand2)) + { + return 0; + } + + if(!reaperPlugin->registerFunction("hookcommand", (void*)onHookCommand)) { return 0; } - if(!reaperPluginInterface->registerFunction("hookcustommenu", (void*)onHookCustomMenu)) + if(!reaperPlugin->registerFunction("hookcustommenu", (void*)onHookCustomMenu)) { return 0; } - reaperPluginInterface->addExtensionsMainMenu(); + reaperPlugin->addExtensionsMainMenu(); for(const auto& apiFunctionDefinition : Scripting::apiFunctionDefinitions) { - reaperPluginInterface->registerFunction(apiFunctionDefinition.Api, (void*)apiFunctionDefinition.FunctionPointer); - reaperPluginInterface->registerFunction(apiFunctionDefinition.ApiVarArg, (void*)apiFunctionDefinition.FunctionPointerVarArg); - reaperPluginInterface->registerFunction(apiFunctionDefinition.ApiDef, (void*)apiFunctionDefinition.FunctionSignature); + reaperPlugin->registerFunction(apiFunctionDefinition.Api, (void*)apiFunctionDefinition.FunctionPointer); + reaperPlugin->registerFunction(apiFunctionDefinition.ApiVarArg, (void*)apiFunctionDefinition.FunctionPointerVarArg); + reaperPlugin->registerFunction(apiFunctionDefinition.ApiDef, (void*)apiFunctionDefinition.FunctionSignature); + } + + for(auto& apiAction : Scripting::apiActions) + { + apiAction.id = reaperPlugin->registerFunction("custom_action", (void*)&apiAction.definition); } return 1; @@ -903,13 +848,25 @@ namespace AK::ReaWwise static int cleanup() { - if(juceInitialised) + if(mainWindow != nullptr) { + mainWindow->removeFromDesktop(); mainWindow.reset(nullptr); + } + + if(reaperContext != nullptr) reaperContext.reset(nullptr); + if(Scripting::waapiClient != nullptr) + Scripting::waapiClient.reset(nullptr); + + if(Scripting::objects.size() > 0) + Scripting::objects.clear(); + + if(guiInitialised) + { juce::shutdownJuce_GUI(); - juceInitialised = false; + guiInitialised = false; } return 0; diff --git a/src/extension/ExtensionWindow.cpp b/src/extension/ExtensionWindow.cpp @@ -1,62 +0,0 @@ -#include "ExtensionWindow.h" -#include "UI/MainComponent.h" - -#include <limits> - -namespace AK::ReaWwise -{ - namespace ExtensionWindowConstants - { - constexpr int width = 600; - constexpr int height = 800; - constexpr int minWidth = 420; - constexpr int minHeight = 650; - constexpr int standardDPI = 96; - } // namespace ExtensionWindowConstants - - ExtensionWindow::ExtensionWindow(WwiseTransfer::DawContext& dawContext) - : juce::ResizableWindow(JUCE_APPLICATION_NAME_STRING, false) - { - using namespace ExtensionWindowConstants; - - juce::LookAndFeel::setDefaultLookAndFeel(&lookAndFeel); - - auto mainContentComponent = new WwiseTransfer::MainComponent(dawContext, JUCE_APPLICATION_NAME_STRING); - -#ifdef WIN32 - if(!mainContentComponent->hasScaleFactorOverride()) - { - auto scaleFactor = juce::Desktop::getInstance().getDisplays().getMainDisplay().dpi / standardDPI; - juce::Desktop::getInstance().setGlobalScaleFactor(scaleFactor); - } -#endif - - setContentOwned(mainContentComponent, true); - centreWithSize(width, height); - setResizable(true, true); - setResizeLimits(minWidth, minHeight, (std::numeric_limits<int>::max)(), (std::numeric_limits<int>::max)()); - } - - ExtensionWindow::~ExtensionWindow() - { - juce::LookAndFeel::setDefaultLookAndFeel(nullptr); - } - - int ExtensionWindow::getDesktopWindowStyleFlags() const - { - return juce::ComponentPeer::windowHasCloseButton | juce::ComponentPeer::windowHasTitleBar | - juce::ComponentPeer::windowIsResizable | juce::ComponentPeer::windowHasMinimiseButton | - juce::ComponentPeer::windowAppearsOnTaskbar | juce::ComponentPeer::windowHasMaximiseButton; - } - - void ExtensionWindow::userTriedToCloseWindow() - { - setVisible(false); - } - - void ExtensionWindow::resized() - { - juce::ResizableWindow::resized(); - } - -} // namespace AK::ReaWwise diff --git a/src/extension/ExtensionWindow.h b/src/extension/ExtensionWindow.h @@ -1,23 +0,0 @@ -#pragma once - -#include "Core/DawContext.h" -#include "Theme/CustomLookAndFeel.h" - -namespace AK::ReaWwise -{ - class ExtensionWindow : public juce::ResizableWindow - { - public: - ExtensionWindow(WwiseTransfer::DawContext& dawContext); - ~ExtensionWindow() override; - - int getDesktopWindowStyleFlags() const override; - void userTriedToCloseWindow() override; - - protected: - void resized() override; - - private: - WwiseTransfer::CustomLookAndFeel lookAndFeel; - }; -} // namespace AK::ReaWwise diff --git a/src/extension/IReaperPlugin.h b/src/extension/IReaperPlugin.h @@ -0,0 +1,27 @@ +#pragma once + +class ReaProject; + +class IReaperPlugin +{ +public: + virtual ~IReaperPlugin() = default; + + virtual int getCallerVersion() const = 0; + virtual int registerFunction(const char* name, void* infoStruct) const = 0; + virtual bool isValid() const = 0; + virtual void* getMainHwnd() = 0; + virtual bool addExtensionsMainMenu() = 0; + virtual ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) = 0; + virtual int resolveRenderPattern(ReaProject* proj, const char* path, const char* pattern, char* targets, int targets_sz) = 0; + virtual void main_OnCommand(int command, int flag) = 0; + virtual int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) = 0; + virtual int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) = 0; + virtual void markProjectDirty(ReaProject* proj) = 0; + virtual int getProjectStateChangeCount(ReaProject* proj) = 0; + virtual double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) = 0; + virtual bool getSetProjectInfo_String(ReaProject* project, const char* desc, char* valuestrNeedBig, bool is_set) = 0; + virtual int reallocCmdRegisterBuf(char** ptr, int* ptr_size) = 0; + virtual void reallocCmdClear(int tok) = 0; + virtual bool supportsReallocCommands() = 0; +}; diff --git a/src/extension/MacHelpers.h b/src/extension/MacHelpers.h @@ -1,8 +0,0 @@ -#pragma once - -#include <juce_gui_basics/juce_gui_basics.h> - -namespace AK::ReaWwise::MacHelpers -{ - void makeWindowFloatingPanel(juce::Component* component); -} diff --git a/src/extension/MacHelpers.mm b/src/extension/MacHelpers.mm @@ -1,15 +0,0 @@ -#include "MacHelpers.h" - -#include <Cocoa/Cocoa.h> - -namespace AK::ReaWwise::MacHelpers -{ - void makeWindowFloatingPanel(juce::Component* component) - { - juce::ComponentPeer* componentPeer = component->getPeer(); - componentPeer->setAlwaysOnTop(true); - NSView* const nativeHandle = (NSView*)(componentPeer->getNativeHandle()); - NSWindow *window = [nativeHandle window]; - [window setHidesOnDeactivate:YES]; - } -} -\ No newline at end of file diff --git a/src/extension/ReaperContext.cpp b/src/extension/ReaperContext.cpp @@ -1,22 +1,30 @@ #include "ReaperContext.h" +#include "Helpers/StringHelper.h" #include "Helpers/WwiseHelper.h" #include "Model/Wwise.h" +#include <regex> + namespace AK::ReaWwise { namespace ReaperContextConstants { - constexpr int defaultBufferSize = 4096; + constexpr int defaultBufferSize = 4 * 1024; + constexpr int largeBufferSize = 4 * 1024 * 1024; const juce::String stateSizeKey = "stateSize"; const juce::String stateKey = "state"; const juce::String applicationKey = "ReaWwise"; const juce::String defaultRenderPattern = "untitled"; } // namespace ReaperContextConstants - ReaperContext::ReaperContext(ReaperPluginInterface& pluginInfo) - : reaperPluginInterface(pluginInfo) - , defaultRenderDirectory(juce::File::getSpecialLocation(juce::File::userDocumentsDirectory).getChildFile("REAPER Media")) + enum ReaperCommands + { + Render = 42230 + }; + + ReaperContext::ReaperContext(IReaperPlugin& reaperPlugin) + : reaperPlugin(reaperPlugin) { } @@ -42,10 +50,10 @@ namespace AK::ReaWwise const auto applicationStateString = applicationState.toXmlString(); const auto applicationStateStringSize = juce::String(applicationStateString.getNumBytesAsUTF8()); - if(reaperPluginInterface.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), applicationStateStringSize.toUTF8()) && - reaperPluginInterface.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), applicationStateString.toUTF8())) + if(reaperPlugin.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), applicationStateStringSize.toUTF8()) && + reaperPlugin.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), applicationStateString.toUTF8())) { - reaperPluginInterface.markProjectDirty(projectInfo.projectReference); + reaperPlugin.markProjectDirty(projectInfo.projectReference); return true; } @@ -61,34 +69,92 @@ namespace AK::ReaWwise auto projectInfo = getProjectInfo(); std::string buffer(defaultBufferSize, '\0'); - reaperPluginInterface.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), &buffer[0], buffer.size()); + reaperPlugin.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), &buffer[0], buffer.size()); const auto stateSize = std::strtoll(&buffer[0], nullptr, 10); if(stateSize == 0) return {}; buffer.resize(stateSize); - if(reaperPluginInterface.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), &buffer[0], buffer.size())) + if(reaperPlugin.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), &buffer[0], buffer.size())) return juce::ValueTree::fromXml(buffer); return {}; } - std::vector<juce::String> ReaperContext::splitDoubleNullTerminatedString(const char* buffer) + void ReaperContext::renderItems() + { + reaperPlugin.main_OnCommand(ReaperCommands::Render, 0); + } + + std::vector<juce::String> ReaperContext::getRenderTargets() { - std::vector<juce::String> result; + auto projectInfo = getProjectInfo(); + + std::vector<juce::String> renderTargets; + + auto result = getProjectStringBuffer(projectInfo.projectReference, "RENDER_TARGETS_EX"); - for(const char* current = buffer; current && *current; current += result.back().length() + 1) + if(result.status) + renderTargets = WwiseTransfer::StringHelper::splitDoubleNullTerminatedString(result.buffer); + else { - result.emplace_back(current); + // For REAPER < 6.69 + auto renderTargetsString = getProjectString(projectInfo.projectReference, "RENDER_TARGETS"); + + juce::StringArray renderTargetsStringArray; + renderTargetsStringArray.addTokens(renderTargetsString, ";", ""); + renderTargetsStringArray.removeEmptyStrings(); + + renderTargets = std::vector<juce::String>(renderTargetsStringArray.strings.begin(), renderTargetsStringArray.strings.end()); } - return result; + return renderTargets; } - void ReaperContext::renderItems() + juce::String ReaperContext::getProjectString(ReaProject* proj, const char* key) const { - reaperPluginInterface.main_OnCommand(42230, 0); + auto result = getProjectStringBuffer(proj, key); + + if(result.buffer.size() > 0) + return juce::String(&result.buffer[0], result.buffer.size()); + + return {}; + } + + ReaperContext::ProjectStringBufferResult ReaperContext::getProjectStringBuffer(ReaProject* proj, const char* key) const + { + ProjectStringBufferResult result; + + if(reaperPlugin.supportsReallocCommands()) + { + // For REAPER 6.68+ + char buffer[ReaperContextConstants::defaultBufferSize]; + char* bufferPtr = buffer; + + int bufferSize = (int)sizeof(buffer); + + int token = reaperPlugin.reallocCmdRegisterBuf(&bufferPtr, &bufferSize); + + result.status = reaperPlugin.getSetProjectInfo_String(proj, key, bufferPtr, false); + + if(result.status) + result.buffer.assign(bufferPtr, bufferPtr + bufferSize); + + reaperPlugin.reallocCmdClear(token); + } + else + { + static std::vector<char> buffer(ReaperContextConstants::largeBufferSize); + std::fill(buffer.begin(), buffer.end(), '\0'); + + result.status = reaperPlugin.getSetProjectInfo_String(proj, key, &buffer[0], false); + + if(result.status) + result.buffer = buffer; + } + + return result; } std::vector<WwiseTransfer::Import::Item> ReaperContext::getItemsForImport(const WwiseTransfer::Import::Options& options) @@ -102,57 +168,107 @@ namespace AK::ReaWwise return importItems; auto projectInfo = getProjectInfo(); - const auto renderDirectory = getRenderDirectory(projectInfo); - juce::StringArray temp; - juce::String renderStats = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_STATS"); - temp.addTokens(renderStats, ";", ""); + juce::String renderStats = getProjectString(projectInfo.projectReference, "RENDER_STATS"); - juce::StringArray finalRenderPaths; + static juce::String fileToken("FILE:"); + static juce::String delimiter(';' + fileToken); - // Not all items in here are paths. Paths are prefixed with "FILE:" - for(auto item : temp) + if(renderStats.isNotEmpty()) { - if(item.startsWith("FILE:")) - finalRenderPaths.add(item.trimCharactersAtStart("FILE:")); - } + // To ease parsing, append ";FILE:" to the end of renderStats + if(renderStats.endsWithChar(';')) + renderStats << fileToken; + else + renderStats << delimiter; - for(int i = 0; i < importItemsForPreview.size(); ++i) - { - importItems.push_back({importItemsForPreview[i].path, importItemsForPreview[i].originalsSubFolder, importItemsForPreview[i].audioFilePath, finalRenderPaths[i]}); + int endPosition, startPosition = renderStats.indexOf(fileToken) + fileToken.length(); + + if(startPosition != -1) // If we don't find the first "FILE:", exit since we are receiving something unexpected + { + static std::regex regex("(.+?);[A-Z]+"); + + while((endPosition = renderStats.indexOf(startPosition, delimiter)) != -1) + { + auto finalRenderPath = renderStats.substring(startPosition, endPosition).toStdString(); + + std::smatch results; + if(std::regex_search(finalRenderPath, results, regex)) + finalRenderPath = results[1]; + + const auto& importItemForPreview = importItemsForPreview[importItems.size()]; + + importItems.push_back({ + importItemForPreview.path, + importItemForPreview.originalsSubFolder, + importItemForPreview.audioFilePath, + finalRenderPath, + }); + + startPosition = endPosition + delimiter.length(); + } + } } return importItems; } - std::vector<WwiseTransfer::Import::PreviewItem> ReaperContext::getItemsForPreview(const WwiseTransfer::Import::Options& options) + std::vector<juce::String> ReaperContext::getOriginalSubfolders(const ProjectInfo& projectInfo, const juce::String& originalsSubfolder) { - juce::ScopedLock lock{apiAccess}; + // The originals subfolder is a combination of what the user inputs in the originals subfolder input field + // Combined with anything in the render file path after the render folder - auto projectInfo = getProjectInfo(); + // To get the resolved file paths relative to the render directory, we can simply subtract the parent paths in resolvedDummyRenderPattern from + // the file paths in resolvedRenderPattern. Other approaches require us to know the render directory which is difficult to figure out and + // requires alot of logic on our end. - const auto renderDirectory = getRenderDirectory(projectInfo); - const auto originalsSubfolderPathPart = options.originalsSubfolder + juce::File::getSeparatorString(); + const auto dummyRenderPattern = juce::File::getSeparatorString(); + const auto resolvedDummyRenderPattern = getItemListFromRenderPattern(projectInfo.projectReference, dummyRenderPattern, true); - const auto resolvedOriginalsSubfolder = getItemListFromRenderPattern(projectInfo.projectReference, originalsSubfolderPathPart, false); + const auto renderPattern = getRenderPattern(projectInfo); + const auto resolvedRenderPattern = getItemListFromRenderPattern(projectInfo.projectReference, renderPattern, true); - juce::StringArray renderTargets; - juce::String renderTargetsString = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_TARGETS"); - renderTargets.addTokens(renderTargetsString, ";", ""); - renderTargets.removeEmptyStrings(); + const auto originalsSubfolderRenderPattern = originalsSubfolder + juce::File::getSeparatorString(); + const auto resolvedOriginalsSubfolder = getItemListFromRenderPattern(projectInfo.projectReference, originalsSubfolderRenderPattern, true); - if(renderTargets.size() != resolvedOriginalsSubfolder.size()) + if(resolvedDummyRenderPattern.size() != resolvedRenderPattern.size() && resolvedDummyRenderPattern.size() != resolvedOriginalsSubfolder.size()) { - juce::Logger::writeToLog("Reaper: Mismatch between renderTargets and resolvedOriginalsSubfolder"); + juce::Logger::writeToLog("Reaper: Mismatch between resolvedDummyRenderPattern, resolvedRenderPattern and resolvedOriginalsSubfolder"); return {}; } + std::vector<juce::String> finalOriginalsSubfolders; + for(int i = 0; i < resolvedDummyRenderPattern.size(); ++i) + { + auto renderDirectory = juce::File(resolvedDummyRenderPattern[i]).getParentDirectory(); + auto relativeResolvedRenderPattern = juce::File(resolvedRenderPattern[i]).getRelativePathFrom(renderDirectory); + auto originalsSubfolderFile = juce::File(resolvedOriginalsSubfolder[i]).getParentDirectory().getChildFile(relativeResolvedRenderPattern); + + juce::String originalsSubfolder = ""; + if(originalsSubfolderFile.getParentDirectory() != renderDirectory) + originalsSubfolder = originalsSubfolderFile.getRelativePathFrom(renderDirectory).upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true); + + finalOriginalsSubfolders.push_back(originalsSubfolder); + } + + return finalOriginalsSubfolders; + } + + std::vector<WwiseTransfer::Import::PreviewItem> ReaperContext::getItemsForPreview(const WwiseTransfer::Import::Options& options) + { + juce::ScopedLock lock{apiAccess}; + + auto projectInfo = getProjectInfo(); + + auto renderTargets = getRenderTargets(); + auto resolvedOriginalsSubfolder = getOriginalSubfolders(projectInfo, options.originalsSubfolder); + const auto objectPathsPattern = options.importDestination + options.hierarchyMappingPath; std::vector<juce::String> resolvedObjectPaths = getItemListFromRenderPattern(projectInfo.projectReference, objectPathsPattern, false); - if(resolvedObjectPaths.size() != renderTargets.size() || resolvedObjectPaths.size() != resolvedOriginalsSubfolder.size()) + if(renderTargets.size() != resolvedOriginalsSubfolder.size() || renderTargets.size() != resolvedObjectPaths.size()) { - juce::Logger::writeToLog("Reaper: Mismatch between resolvedObjectPaths, renderTargets and resolvedOriginalsSubfolder"); + juce::Logger::writeToLog("Reaper: Mismatch between renderTargets, resolvedObjectPaths and resolvedOriginalsSubfolder"); return {}; } @@ -160,27 +276,8 @@ namespace AK::ReaWwise for(int i = 0; i < resolvedObjectPaths.size(); ++i) { const auto& objectPath = resolvedObjectPaths[i].upToLastOccurrenceOf(".", false, false); - const auto& renderTarget = renderTargets[i]; - - // Get the parent of the render target, as a relative path against the render directory - // We want to preserve this hierarchy in the wwise originals folder - juce::String relativeParentDir; - if(juce::File(renderTarget).getParentDirectory() != renderDirectory) - relativeParentDir = juce::File(renderTarget).getRelativePathFrom(renderDirectory).upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true); - - // Reaper may append a differentiator at the end of a path during pattern resolving. We don't care about it. - auto originalsSubfolder = resolvedOriginalsSubfolder[i].upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true); - - // The originalsSubfolder is a combination of what the user inputs in the gui (resolvedOriginalsSubfolder) and the directory structure under the render directory. - if(relativeParentDir.isNotEmpty()) - { - if(originalsSubfolder.isNotEmpty()) - originalsSubfolder << juce::File::getSeparatorString(); - originalsSubfolder << relativeParentDir; - } - - importItems.push_back({objectPath, originalsSubfolder, renderTarget}); + importItems.push_back({objectPath, resolvedOriginalsSubfolder[i], renderTargets[i]}); } return importItems; @@ -191,7 +288,7 @@ namespace AK::ReaWwise std::string buffer(ReaperContextConstants::defaultBufferSize, '\0'); // The buffer sent to enumProjects will contain the project path. - auto projectReference = reaperPluginInterface.enumProjects(-1, &buffer[0], buffer.size()); + auto projectReference = reaperPlugin.enumProjects(-1, &buffer[0], buffer.size()); if(!projectReference || buffer.empty()) return {}; @@ -212,7 +309,7 @@ namespace AK::ReaWwise // There are several scenarios where the render pattern could be empty // 1. When the project hasn't been saved (reaper uses "untitled") // 2. When the project has been saved (reaper uses the project name) - auto renderPattern = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); + auto renderPattern = getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); if(renderPattern.isNotEmpty()) return renderPattern; @@ -222,30 +319,17 @@ namespace AK::ReaWwise return projectInfo.projectName; } - juce::File ReaperContext::getRenderDirectory(const ReaperContext::ProjectInfo& projectInfo) const - { - auto renderDirectoryPath = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_FILE"); - if(juce::File::isAbsolutePath(renderDirectoryPath)) - return juce::File(renderDirectoryPath); - - if(projectInfo.projectPath.isNotEmpty()) - return juce::File(projectInfo.projectPath).getParentDirectory().getChildFile(renderDirectoryPath); - - // If the project wasnt saved, reaper uses a general render directory - return defaultRenderDirectory.getChildFile(renderDirectoryPath); - } - bool ReaperContext::sessionChanged() { auto sessionChanged = false; auto projectInfo = getProjectInfo(); - auto projectStateCount = reaperPluginInterface.getProjectStateChangeCount(projectInfo.projectReference); - auto renderSource = reaperPluginInterface.getSetProjectInfo(projectInfo.projectReference, "RENDER_SETTINGS", 0, false); - auto renderBounds = reaperPluginInterface.getSetProjectInfo(projectInfo.projectReference, "RENDER_BOUNDSFLAG", 0, false); - auto renderFile = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_FILE"); - auto renderPattern = reaperPluginInterface.getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); + auto projectStateCount = reaperPlugin.getProjectStateChangeCount(projectInfo.projectReference); + auto renderSource = reaperPlugin.getSetProjectInfo(projectInfo.projectReference, "RENDER_SETTINGS", 0, false); + auto renderBounds = reaperPlugin.getSetProjectInfo(projectInfo.projectReference, "RENDER_BOUNDSFLAG", 0, false); + auto renderFile = getProjectString(projectInfo.projectReference, "RENDER_FILE"); + auto renderPattern = getProjectString(projectInfo.projectReference, "RENDER_PATTERN"); if(projectStateCount != stateInfo.projectStateCount || renderSource != stateInfo.renderSource || @@ -268,10 +352,13 @@ namespace AK::ReaWwise std::vector<juce::String> ReaperContext::getItemListFromRenderPattern(ReaProject* project, const juce::String& pattern, bool suppressIllegalPaths) { - const int bufferLength = reaperPluginInterface.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), nullptr, 0); - std::string buffer(bufferLength, '\0'); + const int bufferLength = reaperPlugin.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), nullptr, 0); + + if(bufferLength == 0) + return {}; - const int newBufferLength = reaperPluginInterface.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), &buffer[0], bufferLength); + std::vector<char> buffer(bufferLength, '\0'); + const int newBufferLength = reaperPlugin.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), &buffer[0], bufferLength); if(newBufferLength > bufferLength) { // It is possible the resolved render pattern changes between the two calls to resolveRenderPattern. @@ -281,6 +368,6 @@ namespace AK::ReaWwise return {}; } - return splitDoubleNullTerminatedString(&buffer[0]); + return WwiseTransfer::StringHelper::splitDoubleNullTerminatedString(buffer); } } // namespace AK::ReaWwise diff --git a/src/extension/ReaperContext.h b/src/extension/ReaperContext.h @@ -1,38 +1,16 @@ #pragma once #include "Core/DawContext.h" +#include "IReaperPlugin.h" #include "Model/Import.h" -class ReaProject; namespace AK::ReaWwise { - class ReaperPluginInterface - { - public: - virtual ~ReaperPluginInterface() = default; - - virtual int getCallerVersion() const = 0; - virtual int registerFunction(const char* name, void* infoStruct) const = 0; - virtual bool isValid() const = 0; - - virtual void* getMainHwnd() = 0; - virtual bool addExtensionsMainMenu() = 0; - virtual ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) = 0; - virtual juce::String getProjectString(ReaProject* project, const char* key) = 0; - virtual int resolveRenderPattern(ReaProject* proj, const char* path, const char* pattern, char* targets, int targets_sz) = 0; - virtual void main_OnCommand(int command, int flag) = 0; - virtual int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) = 0; - virtual int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) = 0; - virtual void markProjectDirty(ReaProject* proj) = 0; - virtual int getProjectStateChangeCount(ReaProject* proj) = 0; - virtual double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) = 0; - }; - class ReaperContext : public WwiseTransfer::DawContext { public: - ReaperContext(ReaperPluginInterface& pluginInfo); + ReaperContext(IReaperPlugin& pluginInfo); ~ReaperContext() override; bool sessionChanged() override; @@ -60,18 +38,22 @@ namespace AK::ReaWwise juce::String renderPattern; }; - std::vector<juce::String> splitDoubleNullTerminatedString(const char*); + struct ProjectStringBufferResult + { + bool status{false}; + std::vector<char> buffer; + }; + std::vector<juce::String> getItemListFromRenderPattern(ReaProject* project, const juce::String& pattern, bool suppressIllegalPaths = true); ProjectInfo getProjectInfo() const; juce::String getRenderPattern(const ProjectInfo& projectInfo) const; - juce::File getRenderDirectory(const ProjectInfo& projectInfo) const; - - juce::File defaultRenderDirectory; + std::vector<juce::String> getOriginalSubfolders(const ProjectInfo& projectInfo, const juce::String& originalsSubfolder); + std::vector<juce::String> getRenderTargets(); + juce::String getProjectString(ReaProject* proj, const char* key) const; + ProjectStringBufferResult getProjectStringBuffer(ReaProject* proj, const char* key) const; juce::CriticalSection apiAccess; - - ReaperPluginInterface& reaperPluginInterface; - + IReaperPlugin& reaperPlugin; StateInfo stateInfo; }; } // namespace AK::ReaWwise diff --git a/src/extension/ReaperPlugin.cpp b/src/extension/ReaperPlugin.cpp @@ -0,0 +1,121 @@ +#include "ReaperPlugin.h" + +namespace AK::ReaWwise +{ + + ReaperPlugin::ReaperPlugin(reaper_plugin_info_t* pluginInfo) + : pluginInfo(pluginInfo) + { + _getMainHwnd = decltype(GetMainHwnd)(pluginInfo->GetFunc("GetMainHwnd")); + _addExtensionsMainMenu = decltype(AddExtensionsMainMenu)(pluginInfo->GetFunc("AddExtensionsMainMenu")); + _enumProjects = decltype(EnumProjects)(pluginInfo->GetFunc("EnumProjects")); + _getSetProjectInfo_String = decltype(GetSetProjectInfo_String)(pluginInfo->GetFunc("GetSetProjectInfo_String")); + _resolveRenderPattern = decltype(ResolveRenderPattern)(pluginInfo->GetFunc("ResolveRenderPattern")); + _main_OnCommand = decltype(Main_OnCommand)(pluginInfo->GetFunc("Main_OnCommand")); + _getProjExtState = decltype(GetProjExtState)(pluginInfo->GetFunc("GetProjExtState")); + _setProjExtState = decltype(SetProjExtState)(pluginInfo->GetFunc("SetProjExtState")); + _markProjectDirty = decltype(MarkProjectDirty)(pluginInfo->GetFunc("MarkProjectDirty")); + _getProjectStateChangeCount = decltype(GetProjectStateChangeCount)(pluginInfo->GetFunc("GetProjectStateChangeCount")); + _getSetProjectInfo = decltype(GetSetProjectInfo)(pluginInfo->GetFunc("GetSetProjectInfo")); + _realloc_cmd_register_buf = decltype(realloc_cmd_register_buf)(pluginInfo->GetFunc("realloc_cmd_register_buf")); + _realloc_cmd_clear = decltype(realloc_cmd_clear)(pluginInfo->GetFunc("realloc_cmd_clear")); + } + + int ReaperPlugin::getCallerVersion() const + { + return pluginInfo->caller_version; + } + + int ReaperPlugin::registerFunction(const char* name, void* infoStruct) const + { + return pluginInfo->Register(name, infoStruct); + } + + bool ReaperPlugin::isValid() const + { + if(_getMainHwnd && + _addExtensionsMainMenu && + _enumProjects && + _getSetProjectInfo_String && + _resolveRenderPattern && + _main_OnCommand && + _getProjExtState && + _setProjExtState && + _markProjectDirty && + _getProjectStateChangeCount && + _getSetProjectInfo) + return true; + + return false; + } + + void* ReaperPlugin::getMainHwnd() + { + return _getMainHwnd(); + } + + bool ReaperPlugin::addExtensionsMainMenu() + { + return _addExtensionsMainMenu(); + } + + ReaProject* ReaperPlugin::enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) + { + return _enumProjects(idx, projfnOutOptional, projfnOutOptional_sz); + } + + int ReaperPlugin::resolveRenderPattern(ReaProject* project, const char* path, const char* pattern, char* targets, int targets_sz) + { + return _resolveRenderPattern(project, path, pattern, targets, targets_sz); + } + + void ReaperPlugin::main_OnCommand(int command, int flag) + { + _main_OnCommand(command, flag); + } + + int ReaperPlugin::getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) + { + return _getProjExtState(proj, extname, key, valOutNeedBig, valOutNeedBig_sz); + } + + int ReaperPlugin::setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) + { + return _setProjExtState(proj, extname, key, value); + } + + void ReaperPlugin::markProjectDirty(ReaProject* proj) + { + return _markProjectDirty(proj); + } + + int ReaperPlugin::getProjectStateChangeCount(ReaProject* proj) + { + return _getProjectStateChangeCount(proj); + } + + double ReaperPlugin::getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) + { + return _getSetProjectInfo(proj, desc, value, is_set); + } + + bool ReaperPlugin::getSetProjectInfo_String(ReaProject* project, const char* desc, char* valuestrNeedBig, bool is_set) + { + return _getSetProjectInfo_String(project, desc, valuestrNeedBig, is_set); + } + + int ReaperPlugin::reallocCmdRegisterBuf(char** ptr, int* ptr_size) + { + return _realloc_cmd_register_buf(ptr, ptr_size); + } + + void ReaperPlugin::reallocCmdClear(int tok) + { + _realloc_cmd_clear(tok); + } + + bool ReaperPlugin::supportsReallocCommands() + { + return _realloc_cmd_register_buf && _realloc_cmd_clear; + } +} // namespace AK::ReaWwise diff --git a/src/extension/ReaperPlugin.h b/src/extension/ReaperPlugin.h @@ -0,0 +1,49 @@ +#pragma once + +#include "IReaperPlugin.h" + +#include <reaper_plugin_functions.h> + +namespace AK::ReaWwise +{ + class ReaperPlugin : public IReaperPlugin + { + public: + ReaperPlugin(reaper_plugin_info_t* pluginInfo); + ~ReaperPlugin() override = default; + + int getCallerVersion() const override; + int registerFunction(const char* name, void* infoStruct) const override; + bool isValid() const override; + void* getMainHwnd() override; + bool addExtensionsMainMenu() override; + ReaProject* enumProjects(int idx, char* projfnOutOptional, int projfnOutOptional_sz) override; + int resolveRenderPattern(ReaProject* project, const char* path, const char* pattern, char* targets, int targets_sz) override; + void main_OnCommand(int command, int flag) override; + int getProjExtState(ReaProject* proj, const char* extname, const char* key, char* valOutNeedBig, int valOutNeedBig_sz) override; + int setProjExtState(ReaProject* proj, const char* extname, const char* key, const char* value) override; + void markProjectDirty(ReaProject* proj) override; + int getProjectStateChangeCount(ReaProject* proj) override; + double getSetProjectInfo(ReaProject* proj, const char* desc, double value, bool is_set) override; + bool getSetProjectInfo_String(ReaProject* project, const char* desc, char* valuestrNeedBig, bool is_set) override; + int reallocCmdRegisterBuf(char** ptr, int* ptr_size) override; + void reallocCmdClear(int tok) override; + bool supportsReallocCommands() override; + + private: + reaper_plugin_info_t* pluginInfo; + decltype(GetMainHwnd) _getMainHwnd; + decltype(AddExtensionsMainMenu) _addExtensionsMainMenu; + decltype(EnumProjects) _enumProjects; + decltype(GetSetProjectInfo_String) _getSetProjectInfo_String; + decltype(ResolveRenderPattern) _resolveRenderPattern; + decltype(Main_OnCommand) _main_OnCommand; + decltype(GetProjExtState) _getProjExtState; + decltype(SetProjExtState) _setProjExtState; + decltype(MarkProjectDirty) _markProjectDirty; + decltype(GetProjectStateChangeCount) _getProjectStateChangeCount; + decltype(GetSetProjectInfo) _getSetProjectInfo; + decltype(realloc_cmd_register_buf) _realloc_cmd_register_buf; + decltype(realloc_cmd_clear) _realloc_cmd_clear; + }; +} // namespace AK::ReaWwise diff --git a/src/shared/Core/DawWatcher.cpp b/src/shared/Core/DawWatcher.cpp @@ -32,6 +32,7 @@ namespace AK::WwiseTransfer , projectPath(appState, IDs::projectPath, nullptr) , originalsFolder(appState, IDs::originalsFolder, nullptr) , languageSubfolder(appState, IDs::languageSubfolder, nullptr) + , waapiConnected(appState, IDs::waapiConnected, nullptr) , dawContext(dawContext) , waapiClient(waapiClient) , lastImportItemsHash(0) @@ -176,7 +177,7 @@ namespace AK::WwiseTransfer previewLoading = false; }; - if(importDestination.get().isNotEmpty()) + if(waapiConnected.get() && importDestination.get().isNotEmpty()) { previewLoading = true; @@ -196,7 +197,7 @@ namespace AK::WwiseTransfer void DawWatcher::valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) { static std::initializer_list<juce::Identifier> properties{IDs::containerNameExists, IDs::projectPath, IDs::originalsFolder, - IDs::wwiseObjectsChanged, IDs::waqlEnabled, IDs::languageSubfolder, IDs::originalsFolder, IDs::importDestination, IDs::originalsSubfolder}; + IDs::wwiseObjectsChanged, IDs::waqlEnabled, IDs::languageSubfolder, IDs::originalsFolder, IDs::importDestination, IDs::originalsSubfolder, IDs::waapiConnected}; if(treeWhosePropertyHasChanged == applicationState && std::find(properties.begin(), properties.end(), property) != properties.end() || treeWhosePropertyHasChanged.getType() == IDs::hierarchyMappingNode) diff --git a/src/shared/Core/DawWatcher.h b/src/shared/Core/DawWatcher.h @@ -33,6 +33,7 @@ namespace AK::WwiseTransfer juce::CachedValue<juce::String> projectPath; juce::CachedValue<juce::String> originalsFolder; juce::CachedValue<juce::String> languageSubfolder; + juce::CachedValue<bool> waapiConnected; DawContext& dawContext; WaapiClient& waapiClient; diff --git a/src/shared/Core/WaapiClient.cpp b/src/shared/Core/WaapiClient.cpp @@ -28,6 +28,11 @@ namespace AK::WwiseTransfer static constexpr const char* const commandsExecute = "ak.wwise.ui.commands.execute"; } // namespace WaapiCommands + namespace WaapiURIs + { + static constexpr const char* const unknownObject = "ak.wwise.query.unknown_object"; + } + WaapiClientWatcher::WaapiClientWatcher(juce::ValueTree appState, WaapiClient& waapiClient, WaapiClientWatcherConfig waapiClientWatcherConfig) : juce::Thread("WaapiService") , applicationState(appState) @@ -695,6 +700,68 @@ namespace AK::WwiseTransfer return response; } + Waapi::Response<Waapi::ObjectResponse> WaapiClient::getObject(const juce::String& objectPath) + { + using namespace WwiseAuthoringAPI; + + Waapi::Response<Waapi::ObjectResponse> response; + + const auto args = AkJson::Map{ + { + "from", + AkJson::Map{ + { + "path", + AkJson::Array{ + AkVariant{objectPath.toStdString()}, + }, + }, + }, + }, + }; + + static const auto options = AkJson::Map{ + { + "return", + AkJson::Array{ + AkVariant{"id"}, + AkVariant{"name"}, + AkVariant{"type"}, + AkVariant{"path"}, + AkVariant{"sound:originalWavFilePath"}, + AkVariant{"workunitType"}, + }, + }, + }; + + AkJson result; + response.status = call(WaapiCommands::objectGet, args, options, result); + + if(response.status) + { + if(result.HasKey("return")) + { + auto objects = result["return"].GetArray(); + + for(auto& object : objects) + { + response.result = object; + } + } + } + // Special Case: The call actually succeeds but does not find the object + else if(result.HasKey("uri") && result["uri"].GetVariant().GetString() == WaapiURIs::unknownObject) + { + response.status = true; + } + else + { + response.errorMessage << WaapiHelper::getErrorMessage(result); + } + + return response; + } + void WaapiClient::beginUndoGroup() { using namespace WwiseAuthoringAPI; diff --git a/src/shared/Core/WaapiClient.h b/src/shared/Core/WaapiClient.h @@ -84,6 +84,7 @@ namespace AK::WwiseTransfer Waapi::Response<Waapi::ObjectResponseSet> getObjectAncestorsAndDescendantsLegacy(const juce::String& objectPath); Waapi::Response<std::vector<juce::String>> getProjectLanguages(); Waapi::Response<juce::String> getOriginalsFolder(); + Waapi::Response<Waapi::ObjectResponse> getObject(const juce::String& objectPath); bool selectObjects(const juce::String& selectObjectsCommand, const std::vector<juce::String>& objectPaths); @@ -189,6 +190,17 @@ namespace AK::WwiseTransfer threadPool.addJob(new AsyncJob(onJobExecute, callback), true); } + template <typename Callback> + void getObjectAsync(const juce::String& objectPath, Callback& callback) + { + auto onJobExecute = [objectPath, this]() + { + return getObject(objectPath); + }; + + threadPool.addJob(new AsyncJob(onJobExecute, callback), true); + } + private: juce::ThreadPool threadPool; }; diff --git a/src/shared/Helpers/StringHelper.h b/src/shared/Helpers/StringHelper.h @@ -0,0 +1,64 @@ +#pragma once + +#include <algorithm> +#include <iostream> +#include <iterator> +#include <string> +#include <vector> + +namespace AK::WwiseTransfer::StringHelper +{ + inline std::vector<juce::String> splitDoubleNullTerminatedString(const std::vector<char>& buffer) + { + std::vector<juce::String> stringArray; + + std::vector<char> tempBuffer; + for(const char character : buffer) + { + if(character != '\0') + tempBuffer.push_back(character); + else if(!tempBuffer.empty()) + { + stringArray.push_back(juce::String(&tempBuffer[0], tempBuffer.size())); + tempBuffer.clear(); + } + else + break; + } + + return stringArray; + } + + inline std::vector<char> createDoubleNullTerminatedStringBuffer(const std::vector<juce::String>& strings) + { + std::vector<char> rv; + + if(constexpr bool minimizeAllocations = true) + { + std::size_t size = strings.empty() ? 2 : 1; + std::for_each(strings.cbegin(), strings.cend(), [&size](const juce::String& string) + { + auto s = string.length(); + size += s + (s ? 1 : 0); + }); + rv.reserve(size); + } + + std::for_each(strings.cbegin(), strings.cend(), [&rv](const juce::String& s) + { + if(s.isEmpty()) + return; + + for(const auto& c : s) + rv.push_back(c); + + rv.push_back('\0'); + }); + + if(strings.empty()) + rv.push_back('\0'); + + rv.push_back('\0'); + return rv; + } +} // namespace AK::WwiseTransfer::StringHelper diff --git a/src/shared/Helpers/WwiseHelper.h b/src/shared/Helpers/WwiseHelper.h @@ -70,7 +70,7 @@ namespace AK::WwiseTransfer::WwiseHelper switch(objectType) { case ObjectType::ActorMixer: - return "Actor Mixer"; + return "Actor-Mixer"; case ObjectType::AudioFileSource: return "Audio File Source"; case ObjectType::BlendContainer: @@ -104,7 +104,7 @@ namespace AK::WwiseTransfer::WwiseHelper if(objectTypeAsString == "AudioFileSource" || objectTypeAsString == "Audio File Source") return ObjectType::AudioFileSource; - else if(objectTypeAsString == "ActorMixer" || objectTypeAsString == "Actor Mixer") + else if(objectTypeAsString == "ActorMixer" || objectTypeAsString == "Actor Mixer" || objectTypeAsString == "Actor-Mixer") return ObjectType::ActorMixer; else if(objectTypeAsString == "BlendContainer" || objectTypeAsString == "Blend Container") return ObjectType::BlendContainer; diff --git a/src/shared/Persistance/ApplicationStateValidator.cpp b/src/shared/Persistance/ApplicationStateValidator.cpp @@ -6,8 +6,9 @@ namespace AK::WwiseTransfer::ApplicationState { - Validator::Validator(juce::ValueTree appState) + Validator::Validator(juce::ValueTree appState, WaapiClient& waapiClient) : applicationState(appState) + , waapiClient(waapiClient) { applicationState.addListener(this); } @@ -33,16 +34,33 @@ namespace AK::WwiseTransfer::ApplicationState valueTree.setPropertyExcludingListener(this, IDs::originalsSubfolderValid, isValid, nullptr); valueTree.setPropertyExcludingListener(this, IDs::originalsSubfolderErrorMessage, errorMessage, nullptr); } - else if(property == IDs::importDestination || property == IDs::importDestinationType) + else if(property == IDs::importDestination) { const juce::String importDestination = valueTree[IDs::importDestination]; - const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(valueTree[IDs::importDestinationType]); - auto isValid = validateImportDestination(importDestination, importDestinationType); + auto isValid = validateImportDestination(importDestination); juce::String errorMessage = isValid ? "" : "Invalid import destination"; valueTree.setPropertyExcludingListener(this, IDs::importDestinationValid, isValid, nullptr); valueTree.setPropertyExcludingListener(this, IDs::importDestinationErrorMessage, errorMessage, nullptr); + + auto onGetObjectAsync = [this](const Waapi::Response<Waapi::ObjectResponse> response) + { + auto objectType = Wwise::ObjectType::VirtualFolder; + + if(response.result.path.isNotEmpty()) + objectType = response.result.type; + + applicationState.setProperty(IDs::importDestinationType, juce::VariantConverter<Wwise::ObjectType>::toVar(objectType), nullptr); + }; + + waapiClient.getObjectAsync(importDestination, onGetObjectAsync); + } + else if(property == IDs::importDestinationType) + { + const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(valueTree[IDs::importDestinationType]); + + validateHierarchyMapping(importDestinationType, applicationState.getChildWithName(IDs::hierarchyMapping)); } } else if(valueTree.getType() == IDs::hierarchyMappingNode) @@ -50,7 +68,9 @@ namespace AK::WwiseTransfer::ApplicationState if(property == IDs::objectType) { auto hierarchyMapping = valueTree.getParent(); - validateHierarchyMapping(hierarchyMapping); + + const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(applicationState[IDs::importDestinationType]); + validateHierarchyMapping(importDestinationType, hierarchyMapping); } else if(property == IDs::propertyTemplatePath || property == IDs::propertyTemplatePathType) { @@ -67,7 +87,9 @@ namespace AK::WwiseTransfer::ApplicationState { if(parent.getType() == IDs::hierarchyMapping) { - validateHierarchyMapping(parent); + const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(applicationState[IDs::importDestinationType]); + + validateHierarchyMapping(importDestinationType, parent); } if(child.getType() == IDs::hierarchyMappingNode) @@ -81,7 +103,9 @@ namespace AK::WwiseTransfer::ApplicationState { if(parent.getType() == IDs::hierarchyMapping) { - validateHierarchyMapping(parent); + const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(applicationState[IDs::importDestinationType]); + + validateHierarchyMapping(importDestinationType, parent); } } @@ -89,7 +113,9 @@ namespace AK::WwiseTransfer::ApplicationState { if(parent.getType() == IDs::hierarchyMapping) { - validateHierarchyMapping(parent); + const auto importDestinationType = juce::VariantConverter<Wwise::ObjectType>::fromVar(applicationState[IDs::importDestinationType]); + + validateHierarchyMapping(importDestinationType, parent); } } @@ -105,21 +131,15 @@ namespace AK::WwiseTransfer::ApplicationState return originalsSubfolderAbsolutePath.isAChildOf(originalsFolderWithLanguageSubfolder); } - bool Validator::validateImportDestination(const juce::String& importDestination, Wwise::ObjectType objectType) + bool Validator::validateImportDestination(const juce::String& importDestination) { using namespace Wwise; - static const std::initializer_list<ObjectType> allowedObjectTypes = {ObjectType::VirtualFolder, ObjectType::WorkUnit, - ObjectType::RandomContainer, ObjectType::BlendContainer, ObjectType::ActorMixer, ObjectType::SwitchContainer}; - static const juce::String pathPrefix = "\\Actor-Mixer Hierarchy"; - auto allowedType = std::find(allowedObjectTypes.begin(), allowedObjectTypes.end(), - objectType) != allowedObjectTypes.end(); - auto allowedPathPrefix = importDestination.startsWith(pathPrefix); - return importDestination.isNotEmpty() && !importDestination.endsWith("\\") && allowedType && allowedPathPrefix; + return importDestination.isNotEmpty() && !importDestination.endsWith("\\") && allowedPathPrefix; } void Validator::validatePropertyTemplatePath(juce::ValueTree hierarchyMappingNode) @@ -159,43 +179,39 @@ namespace AK::WwiseTransfer::ApplicationState hierarchyMappingNode.setPropertyExcludingListener(this, IDs::objectNameErrorMessage, errorMessage, nullptr); } - void Validator::validateHierarchyMapping(juce::ValueTree hierarchyMapping) + void Validator::validateHierarchyMapping(Wwise::ObjectType importDestinationType, juce::ValueTree hierarchyMapping) { - // TODO: Should error be reported on parent or child? - auto hierarchyMappingNodeList = ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping); + // To properly validate the hierarchy mapping, we must include the import destination + std::vector<Wwise::ObjectType> hierarchyTypes{importDestinationType}; + for(int i = 0; i < hierarchyMapping.getNumChildren(); ++i) + { + const auto hierarchyMappingNode = hierarchyMapping.getChild(i); + hierarchyTypes.emplace_back(juce::VariantConverter<Wwise::ObjectType>::fromVar(hierarchyMappingNode[IDs::objectType])); + } - for(std::size_t i = 0; i < hierarchyMappingNodeList.size(); ++i) + for(std::size_t i = 1; i < hierarchyTypes.size(); ++i) { - auto& child = hierarchyMappingNodeList.at(i); + const auto child = hierarchyTypes[i]; + const auto parent = hierarchyTypes[i - 1]; bool isValid = true; juce::String errorMessage; // Last item must be SoundSFX, report this error above any others - if(i == hierarchyMappingNodeList.size() - 1) + if(i == hierarchyTypes.size() - 1 && child != Wwise::ObjectType::SoundSFX && child != Wwise::ObjectType::SoundVoice) { - if(child.type != Wwise::ObjectType::SoundSFX && child.type != Wwise::ObjectType::SoundVoice) - { - isValid = false; - errorMessage << "Last item must be of type 'SoundSFX' or 'Sound Voice'"; - } + isValid = false; + errorMessage << "Last item must be of type 'SoundSFX' or 'Sound Voice'"; } - - // Since we have limited space in the tooltip, do not do any other validation if an error was already detected - if(isValid && i != 0) + else if(parent != Wwise::ObjectType::Unknown && + child != Wwise::ObjectType::Unknown && + !WwiseHelper::validateObjectTypeParentChildRelationShip(parent, child)) { - auto& parent = hierarchyMappingNodeList.at(i - 1); - - if(parent.type != Wwise::ObjectType::Unknown && - child.type != Wwise::ObjectType::Unknown && - !WwiseHelper::validateObjectTypeParentChildRelationShip(parent.type, child.type)) - { - isValid = false; - errorMessage << "'" << WwiseHelper::objectTypeToReadableString(child.type) << "' cannot be a child of '" << WwiseHelper::objectTypeToReadableString(parent.type) << "'"; - } + isValid = false; + errorMessage << "'" << WwiseHelper::objectTypeToReadableString(child) << "' cannot be a child of '" << WwiseHelper::objectTypeToReadableString(parent) << "'"; } - auto hierarchyMappingNode = hierarchyMapping.getChild(i); + auto hierarchyMappingNode = hierarchyMapping.getChild(i - 1); // Index is off by 1 due to the importDestination hierarchyMappingNode.setPropertyExcludingListener(this, IDs::objectTypeValid, isValid, nullptr); hierarchyMappingNode.setPropertyExcludingListener(this, IDs::objectTypeErrorMessage, errorMessage, nullptr); } diff --git a/src/shared/Persistance/ApplicationStateValidator.h b/src/shared/Persistance/ApplicationStateValidator.h @@ -1,6 +1,7 @@ #pragma once #include "Core/DawContext.h" +#include "Core/WaapiClient.h" #include "Model/Wwise.h" #include <juce_data_structures/juce_data_structures.h> @@ -11,21 +12,22 @@ namespace AK::WwiseTransfer::ApplicationState : juce::ValueTree::Listener { public: - Validator(juce::ValueTree appState); + Validator(juce::ValueTree appState, WaapiClient& waapiClient); ~Validator(); private: juce::ValueTree applicationState; + WaapiClient& waapiClient; void valueTreePropertyChanged(juce::ValueTree& valueTree, const juce::Identifier& property) override; void valueTreeChildAdded(juce::ValueTree& parent, juce::ValueTree& child) override; void valueTreeChildRemoved(juce::ValueTree& parent, juce::ValueTree& child, int indexOfChild) override; void valueTreeChildOrderChanged(juce::ValueTree& parent, int oldIndex, int newIndex) override; - bool validateImportDestination(const juce::String& importDestination, Wwise::ObjectType objectType); + bool validateImportDestination(const juce::String& importDestination); bool validateOriginalsSubfolder(const juce::String& originalsFolder, const juce::String& languageSubfolder, const juce::String& originalsSubfolder); void validatePropertyTemplatePath(juce::ValueTree hierarchyMappingNode); void validateObjectName(juce::ValueTree hierarchyMappingNode); - void validateHierarchyMapping(juce::ValueTree hierarchyMapping); + void validateHierarchyMapping(Wwise::ObjectType importDestinationType, juce::ValueTree hierarchyMapping); }; } // namespace AK::WwiseTransfer::ApplicationState diff --git a/src/shared/Theme/CustomLookAndFeel.cpp b/src/shared/Theme/CustomLookAndFeel.cpp @@ -14,6 +14,7 @@ namespace AK::WwiseTransfer , highlightedFillColor{0xff646464} , buttonBackgroundColor{0xff5a5a5a} , thinOutlineColor{0xff292929} + , focusedOutlineColor{0xffbc9770} , tableHeaderBackgroundColor{0xff545454} , previewItemNoChangeColor{0xff7d7d7d} , previewItemNewColor{0xff29afff} @@ -35,6 +36,8 @@ namespace AK::WwiseTransfer setColour(juce::TextButton::buttonColourId, buttonBackgroundColor); setColour(juce::TextEditor::outlineColourId, thinOutlineColor); + setColour(juce::TextEditor::focusedOutlineColourId, focusedOutlineColor); + setColour(juce::ComboBox::focusedOutlineColourId, focusedOutlineColor); setColour(juce::ComboBox::outlineColourId, thinOutlineColor); setColour(juce::TreeView::backgroundColourId, widgetBackgroundColor); setColour(juce::TableHeaderComponent::backgroundColourId, tableHeaderBackgroundColor); @@ -45,7 +48,7 @@ namespace AK::WwiseTransfer setColour(juce::TooltipWindow::backgroundColourId, widgetBackgroundColor); } - const std::shared_ptr<juce::Drawable>& CustomLookAndFeel::getIconForObjectType(Wwise::ObjectType objectType) + std::unique_ptr<juce::Drawable> CustomLookAndFeel::getIconForObjectType(Wwise::ObjectType objectType) { using namespace Wwise; @@ -53,63 +56,51 @@ namespace AK::WwiseTransfer { case ObjectType::ActorMixer: { - static std::shared_ptr<juce::Drawable> actorMixerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_PhysicalFolder_nor_svg, BinaryData::ObjectIcons_PhysicalFolder_nor_svgSize)); - return actorMixerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_PhysicalFolder_nor_svg, BinaryData::ObjectIcons_PhysicalFolder_nor_svgSize); } case ObjectType::AudioFileSource: { - static std::shared_ptr<juce::Drawable> actorMixerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_AudioObjectSound_nor_svg, BinaryData::ObjectIcons_AudioObjectSound_nor_svgSize)); - return actorMixerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_AudioObjectSound_nor_svg, BinaryData::ObjectIcons_AudioObjectSound_nor_svgSize); } case ObjectType::BlendContainer: { - static std::shared_ptr<juce::Drawable> blendContainerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_BlendContainer_nor_svg, BinaryData::ObjectIcons_BlendContainer_nor_svgSize)); - return blendContainerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_BlendContainer_nor_svg, BinaryData::ObjectIcons_BlendContainer_nor_svgSize); } case ObjectType::PhysicalFolder: { - static std::shared_ptr<juce::Drawable> physicalFolderIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_PhysicalFolder_nor_svg, BinaryData::ObjectIcons_PhysicalFolder_nor_svgSize)); - return physicalFolderIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_PhysicalFolder_nor_svg, BinaryData::ObjectIcons_PhysicalFolder_nor_svgSize); } case ObjectType::RandomContainer: { - static std::shared_ptr<juce::Drawable> randomContainerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_RandomContainer_nor_svg, BinaryData::ObjectIcons_RandomContainer_nor_svgSize)); - return randomContainerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_RandomContainer_nor_svg, BinaryData::ObjectIcons_RandomContainer_nor_svgSize); } case ObjectType::SequenceContainer: { - static std::shared_ptr<juce::Drawable> sequenceContainerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SequenceContainer_nor_svg, BinaryData::ObjectIcons_SequenceContainer_nor_svgSize)); - return sequenceContainerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SequenceContainer_nor_svg, BinaryData::ObjectIcons_SequenceContainer_nor_svgSize); } case ObjectType::SoundSFX: { - static std::shared_ptr<juce::Drawable> soundSFXIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SoundFX_nor_svg, BinaryData::ObjectIcons_SoundFX_nor_svgSize)); - return soundSFXIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SoundFX_nor_svg, BinaryData::ObjectIcons_SoundFX_nor_svgSize); } case ObjectType::SoundVoice: { - static std::shared_ptr<juce::Drawable> soundVoiceIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SoundVoice_nor_svg, BinaryData::ObjectIcons_SoundVoice_nor_svgSize)); - return soundVoiceIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SoundVoice_nor_svg, BinaryData::ObjectIcons_SoundVoice_nor_svgSize); } case ObjectType::SwitchContainer: { - static std::shared_ptr<juce::Drawable> switchContainerIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SwitchContainer_nor_svg, BinaryData::ObjectIcons_SwitchContainer_nor_svgSize)); - return switchContainerIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_SwitchContainer_nor_svg, BinaryData::ObjectIcons_SwitchContainer_nor_svgSize); } case ObjectType::VirtualFolder: { - static std::shared_ptr<juce::Drawable> virtualFolderIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Folder_nor_svg, BinaryData::ObjectIcons_Folder_nor_svgSize)); - return virtualFolderIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Folder_nor_svg, BinaryData::ObjectIcons_Folder_nor_svgSize); } case ObjectType::WorkUnit: { - static std::shared_ptr<juce::Drawable> workUnitIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Workunit_nor_svg, BinaryData::ObjectIcons_Workunit_nor_svgSize)); - return workUnitIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Workunit_nor_svg, BinaryData::ObjectIcons_Workunit_nor_svgSize); } default: { - static std::shared_ptr<juce::Drawable> defaultIcon(juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Folder_nor_svg, BinaryData::ObjectIcons_Folder_nor_svgSize)); - return defaultIcon; + return juce::Drawable::createFromImageData(BinaryData::ObjectIcons_Folder_nor_svg, BinaryData::ObjectIcons_Folder_nor_svgSize); } } } @@ -218,28 +209,27 @@ namespace AK::WwiseTransfer juce::Justification::centred, true); } - juce::Typeface::Ptr CustomLookAndFeel::getTypefaceForFont(const juce::Font& font) - { - if(font.isBold()) - return boldTypeFace; - - return regularTypeFace; - } - void CustomLookAndFeel::drawTextEditorOutline(juce::Graphics& g, int width, int height, juce::TextEditor& textEditor) { - if(dynamic_cast<juce::AlertWindow*>(textEditor.getParentComponent()) != nullptr || !textEditor.isEnabled() || textEditor.isReadOnly()) + if(dynamic_cast<juce::AlertWindow*>(textEditor.getParentComponent()) != nullptr) return; const auto hasKeyboardFocus = textEditor.hasKeyboardFocus(true); auto colour = textEditor.findColour(hasKeyboardFocus ? juce::TextEditor::focusedOutlineColourId : juce::TextEditor::outlineColourId); - auto lineThickness = hasKeyboardFocus ? 2 : 1; + auto lineThickness = 1; g.setColour(colour); g.drawRect(0, 0, width, height, lineThickness); } + void CustomLookAndFeel::fillTextEditorBackground(juce::Graphics& g, int width, int height, juce::TextEditor& textEditor) + { + auto backgroundColor = textEditor.isReadOnly() ? windowBackgroundColor : textEditor.findColour(juce::TextEditor::backgroundColourId); + + g.fillAll(backgroundColor); + } + void CustomLookAndFeel::drawTooltip(juce::Graphics& g, const juce::String& text, int width, int height) { juce::Rectangle<int> bounds(width, height); @@ -263,6 +253,42 @@ namespace AK::WwiseTransfer tl.draw(g, {static_cast<float>(width), static_cast<float>(height)}); } + void CustomLookAndFeel::drawComboBox(juce::Graphics& g, int width, int height, bool isButtonDown, + int buttonX, int buttonY, int buttonW, int buttonH, juce::ComboBox& box) + { + // Copied from juce_LookAndFeel_V4.cpp and added support for outlining the component when in focus + + auto cornerSize = box.findParentComponentOfClass<juce::ChoicePropertyComponent>() != nullptr ? 0.0f : 3.0f; + juce::Rectangle<int> boxBounds(0, 0, width, height); + + g.setColour(box.findColour(juce::ComboBox::backgroundColourId)); + g.fillRoundedRectangle(boxBounds.toFloat(), cornerSize); + + if(box.isEnabled() && box.hasKeyboardFocus(false)) + g.setColour(box.findColour(juce::ComboBox::focusedOutlineColourId)); + else + g.setColour(box.findColour(juce::ComboBox::outlineColourId)); + + g.drawRoundedRectangle(boxBounds.toFloat().reduced(0.5f, 0.5f), cornerSize, 1.0f); + + juce::Rectangle<int> arrowZone(width - 30, 0, 20, height); + juce::Path path; + path.startNewSubPath((float)arrowZone.getX() + 3.0f, (float)arrowZone.getCentreY() - 2.0f); + path.lineTo((float)arrowZone.getCentreX(), (float)arrowZone.getCentreY() + 3.0f); + path.lineTo((float)arrowZone.getRight() - 3.0f, (float)arrowZone.getCentreY() - 2.0f); + + g.setColour(box.findColour(juce::ComboBox::arrowColourId).withAlpha((box.isEnabled() ? 0.9f : 0.2f))); + g.strokePath(path, juce::PathStrokeType(2.0f)); + } + + juce::Typeface::Ptr CustomLookAndFeel::getTypefaceForFont(const juce::Font& font) + { + if(font.isBold()) + return boldTypeFace; + + return regularTypeFace; + } + juce::Font CustomLookAndFeel::getLabelFont(juce::Label& label) { static auto defaultLabelFont = juce::Label().getFont(); diff --git a/src/shared/Theme/CustomLookAndFeel.h b/src/shared/Theme/CustomLookAndFeel.h @@ -20,11 +20,13 @@ namespace AK::WwiseTransfer public: CustomLookAndFeel(); - const std::shared_ptr<juce::Drawable>& getIconForObjectType(Wwise::ObjectType objectType); + std::unique_ptr<juce::Drawable> getIconForObjectType(Wwise::ObjectType objectType); juce::Colour getTextColourForObjectStatus(Import::ObjectStatus objectStatus); void drawTextEditorOutline(juce::Graphics& g, int width, int height, juce::TextEditor& textEditor) override; + void fillTextEditorBackground(juce::Graphics& g, int width, int height, juce::TextEditor& textEditor) override; + void drawTableHeaderColumn(juce::Graphics& g, juce::TableHeaderComponent& header, const juce::String& columnName, int /*columnId*/, int width, int height, bool isMouseOver, bool isMouseDown, @@ -35,6 +37,9 @@ namespace AK::WwiseTransfer void drawTooltip(juce::Graphics&, const juce::String& text, int w, int h) override; + void drawComboBox(juce::Graphics& g, int width, int height, bool isButtonDown, int buttonX, + int buttonY, int buttonW, int buttonH, juce::ComboBox& box) override; + juce::Typeface::Ptr getTypefaceForFont(const juce::Font&) override; juce::Font getLabelFont(juce::Label& label) override; @@ -58,6 +63,7 @@ namespace AK::WwiseTransfer juce::Colour highlightedFillColor; juce::Colour buttonBackgroundColor; juce::Colour thinOutlineColor; + juce::Colour focusedOutlineColor; juce::Colour tableHeaderBackgroundColor; juce::Colour previewItemNoChangeColor; juce::Colour previewItemNewColor; diff --git a/src/shared/UI/AboutComponent.h b/src/shared/UI/AboutComponent.h @@ -31,5 +31,7 @@ namespace AK::WwiseTransfer juce::TooltipWindow tooltipWindow; std::unique_ptr<juce::Drawable> wwiseIcon; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(AboutComponent) }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/CustomDrawableButton.h b/src/shared/UI/CustomDrawableButton.h @@ -14,5 +14,7 @@ namespace AK::WwiseTransfer private: std::unique_ptr<juce::Drawable> drawable; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CustomDrawableButton) }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/ImportConflictsComponent.cpp b/src/shared/UI/ImportConflictsComponent.cpp @@ -20,7 +20,6 @@ namespace AK::WwiseTransfer ImportConflictsComponent::ImportConflictsComponent(juce::ValueTree appState) : applicationState(appState) , containerNameExists(applicationState, IDs::containerNameExists, nullptr) - , audioFilenameExists(applicationState, IDs::audioFileNameExists, nullptr) , applyTemplate(applicationState, IDs::applyTemplate, nullptr) { using namespace ImportConflictsComponentContants; @@ -45,7 +44,6 @@ namespace AK::WwiseTransfer applyTemplateComboBox.addItem(ImportHelper::applyTemplateOptionToReadableString(applyTemplateOption), static_cast<int>(applyTemplateOption)); containerNameExistsComboBox.getSelectedIdAsValue().referTo(containerNameExists.getPropertyAsValue()); - audioFileNameExistsComboBox.getSelectedIdAsValue().referTo(audioFilenameExists.getPropertyAsValue()); applyTemplateComboBox.getSelectedIdAsValue().referTo(applyTemplate.getPropertyAsValue()); auto containerNameExistsComboBoxOnChange = [this] @@ -53,27 +51,27 @@ namespace AK::WwiseTransfer const char* tooltip = ""; switch(static_cast<Import::ContainerNameExistsOption>(containerNameExistsComboBox.getSelectedId())) { - case Import::ContainerNameExistsOption::UseExisting: - tooltip = "Retains the existing sounds but adds the transferred files as source audio " - "files. The new files do not become the main audio sources for the existing " - "objects unless they have identical names to the existing sources, and " - "therefore overwrite them."; - break; - - case Import::ContainerNameExistsOption::CreateNew: - tooltip = "Creates new sounds with increments appended to the names, for example " - "SoundName_01. The existing sounds are not affected, unless the existing audio " - "sources have the same names as the newly transferred files. In that case, the " - "new sources overwrite the existing ones."; - break; - - case Import::ContainerNameExistsOption::Replace: - tooltip = "Deletes and recreates the sounds, with the transferred files as sources."; - break; - - default: - jassertfalse; - break; + case Import::ContainerNameExistsOption::UseExisting: + tooltip = "Retains the existing sounds but adds the transferred files as source audio " + "files. The new files do not become the main audio sources for the existing " + "objects unless they have identical names to the existing sources, and " + "therefore overwrite them."; + break; + + case Import::ContainerNameExistsOption::CreateNew: + tooltip = "Creates new sounds with increments appended to the names, for example " + "SoundName_01. The existing sounds are not affected, unless the existing audio " + "sources have the same names as the newly transferred files. In that case, the " + "new sources overwrite the existing ones."; + break; + + case Import::ContainerNameExistsOption::Replace: + tooltip = "Deletes and recreates the sounds, with the transferred files as sources."; + break; + + default: + jassertfalse; + break; } containerNameExistsComboBox.setTooltip(tooltip); @@ -83,10 +81,8 @@ namespace AK::WwiseTransfer containerNameExistsComboBox.onChange = containerNameExistsComboBoxOnChange; addAndMakeVisible(containerNameExistsLabel); - addAndMakeVisible(audioFileNameExistsLabel); addAndMakeVisible(applyTemplateLabel); addAndMakeVisible(containerNameExistsComboBox); - addAndMakeVisible(audioFileNameExistsComboBox); addAndMakeVisible(applyTemplateComboBox); refreshComponent(); diff --git a/src/shared/UI/ImportConflictsComponent.h b/src/shared/UI/ImportConflictsComponent.h @@ -19,11 +19,9 @@ namespace AK::WwiseTransfer private: juce::Label containerNameExistsLabel; - juce::Label audioFileNameExistsLabel; juce::Label applyTemplateLabel; juce::ComboBox containerNameExistsComboBox; - juce::ComboBox audioFileNameExistsComboBox; juce::ComboBox applyTemplateComboBox; juce::ValueTree applicationState; @@ -35,10 +33,10 @@ namespace AK::WwiseTransfer juce::CachedValue<juce::String> selectObjectsOnImportCommand; - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportConflictsComponent); - void valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) override; void handleAsyncUpdate() override; void refreshComponent(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportConflictsComponent); }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/ImportControlsComponent.cpp b/src/shared/UI/ImportControlsComponent.cpp @@ -32,6 +32,7 @@ namespace AK::WwiseTransfer , containerNameExistsOption(applicationState, IDs::containerNameExists, nullptr) , applyTemplateOption(applicationState, IDs::applyTemplate, nullptr) , hierarchyMapping(applicationState.getChildWithName(IDs::hierarchyMapping)) + , previewItems(applicationState.getChildWithName(IDs::previewItems)) , waapiClient(waapiClient) , dawContext(dawContext) , applicationProperties(applicationProperties) @@ -49,7 +50,7 @@ namespace AK::WwiseTransfer importButton.onClick = [this] { - onImportButtonClick(); + transferToWwise(); }; addAndMakeVisible(importButton); @@ -90,8 +91,11 @@ namespace AK::WwiseTransfer } } // namespace - void ImportControlsComponent::onImportButtonClick() + void ImportControlsComponent::transferToWwise() { + if(!importButton.isEnabled()) + return; + using namespace ImportControlsComponentConstants; // Disable the import button while rendering @@ -115,7 +119,7 @@ namespace AK::WwiseTransfer { const juce::String message("One or more files failed to render."); juce::Logger::writeToLog(message); - juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Import Aborted", message); + juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Transfer to Wwise Aborted", message); importButton.setEnabled(true); return; } @@ -134,7 +138,7 @@ namespace AK::WwiseTransfer break; } - if(importItem.audioFilePath != importItem.renderFilePath) + if(juce::File(importItem.audioFilePath) != juce::File(importItem.renderFilePath)) { showRenameWarning = true; break; @@ -152,7 +156,7 @@ namespace AK::WwiseTransfer { const juce::String message("One or more files failed to render."); juce::Logger::writeToLog(message); - juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Import Aborted", message); + juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Transfer to Wwise Aborted", message); importButton.setEnabled(true); return; } @@ -236,7 +240,7 @@ namespace AK::WwiseTransfer message << juce::NewLine() << summary.errorMessage; auto messageBoxOptions = juce::MessageBoxOptions() - .withTitle("Import Summary") + .withTitle("Wwise Import Summary") .withMessage(message) .withButton("View Details") .withButton("Close"); @@ -257,7 +261,7 @@ namespace AK::WwiseTransfer auto currentTime = juce::Time::getCurrentTime(); auto importSummaryFile = juce::File::getSpecialLocation(juce::File::tempDirectory) - .getChildFile(applicationName + "_ImportSummary_" + currentTime.formatted("%Y-%m-%d_%H-%M-%S")) + .getChildFile(applicationName + "_WwiseImportSummary_" + currentTime.formatted("%Y-%m-%d_%H-%M-%S")) .withFileExtension(".html"); importSummaryFile.create(); @@ -265,7 +269,7 @@ namespace AK::WwiseTransfer importSummaryFile.appendText("<style>table td, th { border:1px solid black; padding:10px; }"); importSummaryFile.appendText("table { border-collapse:collapse; }"); importSummaryFile.appendText("table th { text-align: left; }</style>"); - importSummaryFile.appendText("<pre>" + applicationName + ": Import Summary " + currentTime.formatted("%Y-%m-%d %H:%M:%S") + "\n\n"); + importSummaryFile.appendText("<pre>" + applicationName + ": Wwise Import Summary " + currentTime.formatted("%Y-%m-%d %H:%M:%S") + "\n\n"); importSummaryFile.appendText("Import Destination: " + importTaskOptions.importDestination + "\n"); importSummaryFile.appendText("Container Name Exists: " + ImportHelper::containerNameExistsOptionToReadableString(importTaskOptions.containerNameExistsOption) + "\n"); importSummaryFile.appendText("Apply Template: " + ImportHelper::applyTemplateOptionToReadableString(importTaskOptions.applyTemplateOption) + "\n\n"); @@ -291,7 +295,7 @@ namespace AK::WwiseTransfer void ImportControlsComponent::refreshComponent() { - auto importButtonEnabled = originalsSubfolderValid.get() && importDestinationValid.get() && projectPath.get().isNotEmpty(); + auto importButtonEnabled = originalsSubfolderValid.get() && importDestinationValid.get() && projectPath.get().isNotEmpty() && previewItems.getNumChildren() > 0; auto hieararchyMappingNodes = ImportHelper::valueTreeToHierarchyMappingNodeList(hierarchyMapping); @@ -307,10 +311,12 @@ namespace AK::WwiseTransfer importButton.setEnabled(importButtonEnabled); - juce::String tooltip = ""; + juce::String tooltip; if(projectPath.get().isEmpty()) tooltip = "Connect to Wwise to continue"; + else if(previewItems.getNumChildren() == 0) + tooltip = "Nothing to transfer"; else if(!importButtonEnabled) tooltip = "Fix pending errors to continue"; @@ -328,7 +334,7 @@ namespace AK::WwiseTransfer void ImportControlsComponent::valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded) { - if(parentTree.getType() == IDs::hierarchyMapping) + if(parentTree.getType() == IDs::hierarchyMapping || parentTree.getType() == IDs::previewItems) { triggerAsyncUpdate(); } @@ -336,7 +342,7 @@ namespace AK::WwiseTransfer void ImportControlsComponent::valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved) { - if(parentTree.getType() == IDs::hierarchyMapping) + if(parentTree.getType() == IDs::hierarchyMapping || parentTree.getType() == IDs::previewItems) { triggerAsyncUpdate(); } diff --git a/src/shared/UI/ImportControlsComponent.h b/src/shared/UI/ImportControlsComponent.h @@ -20,6 +20,8 @@ namespace AK::WwiseTransfer void resized() override; + void transferToWwise(); + private: juce::TextButton importButton; juce::ValueTree applicationState; @@ -33,6 +35,7 @@ namespace AK::WwiseTransfer juce::CachedValue<Import::ContainerNameExistsOption> containerNameExistsOption; juce::CachedValue<Import::ApplyTemplateOption> applyTemplateOption; juce::ValueTree hierarchyMapping; + juce::ValueTree previewItems; juce::CachedValue<juce::String> selectObjectsOnImportCommand; juce::CachedValue<bool> applyTemplateFeatureEnabled; @@ -49,7 +52,6 @@ namespace AK::WwiseTransfer const juce::String applicationName; - void onImportButtonClick(); void showImportSummary(const Import::Summary& summary, const Import::Task::Options& importTaskOptions); void viewImportSummaryDetails(const Import::Summary& summary, const Import::Task::Options& importTaskOptions); diff --git a/src/shared/UI/ImportDestinationComponent.cpp b/src/shared/UI/ImportDestinationComponent.cpp @@ -13,6 +13,7 @@ namespace AK::WwiseTransfer constexpr int editorBoxHeight = 26; constexpr int labelWidth = 120; constexpr int syncButtonWidth = 36; + constexpr int iconSize = 16; } // namespace ImportDestinationComponentConstants ImportDestinationComponent::ImportDestinationComponent(juce::ValueTree appState, WaapiClient& waapiClient) @@ -45,6 +46,7 @@ namespace AK::WwiseTransfer addAndMakeVisible(importDestinationLabel); addAndMakeVisible(importDestinationEditor); addAndMakeVisible(updateImportDestinationButton); + addAndMakeVisible(objectTypeIconComposite); refreshComponent(); } @@ -60,15 +62,19 @@ namespace AK::WwiseTransfer auto area = getLocalBounds(); - auto ImportDestinationSection = area.removeFromTop(editorBoxHeight); + auto importDestinationSection = area.removeFromTop(editorBoxHeight); { - importDestinationLabel.setBounds(ImportDestinationSection.removeFromLeft(labelWidth)); - ImportDestinationSection.removeFromLeft(margin); + importDestinationLabel.setBounds(importDestinationSection.removeFromLeft(labelWidth)); + importDestinationSection.removeFromLeft(margin); - updateImportDestinationButton.setBounds(ImportDestinationSection.removeFromRight(syncButtonWidth)); - ImportDestinationSection.removeFromRight(spacing); + updateImportDestinationButton.setBounds(importDestinationSection.removeFromRight(syncButtonWidth)); + importDestinationSection.removeFromRight(spacing); - importDestinationEditor.setBounds(ImportDestinationSection); + objectTypeIconComposite.setBounds(importDestinationSection.removeFromRight(iconSize).withSizeKeepingCentre(iconSize, iconSize)); + + importDestinationSection.removeFromRight(spacing); + + importDestinationEditor.setBounds(importDestinationSection); } } @@ -77,6 +83,16 @@ namespace AK::WwiseTransfer auto projectPathEmpty = projectPath.get().isEmpty(); updateImportDestinationButton.setEnabled(!projectPathEmpty); + + auto* customLookAndFeel = dynamic_cast<CustomLookAndFeel*>(&getLookAndFeel()); + + if(customLookAndFeel) + { + // Reset the icon and add it as a child of the composite. The composite will refresh the icon automatically + objectTypeIconComposite.removeAllChildren(); + objectTypeIcon = customLookAndFeel->getIconForObjectType(importDestinationType); + objectTypeIconComposite.addAndMakeVisible(objectTypeIcon.get()); + } } void ImportDestinationComponent::updateImportDestination() @@ -87,7 +103,6 @@ namespace AK::WwiseTransfer return juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Import Destination", "No object is selected in Wwise. Please select one and try again."); importDestination = response.result.path; - importDestinationType = response.result.type; }; waapiClient.getSelectedObjectAsync(onGetSelectedObject); diff --git a/src/shared/UI/ImportDestinationComponent.h b/src/shared/UI/ImportDestinationComponent.h @@ -36,6 +36,9 @@ namespace AK::WwiseTransfer void valueTreePropertyChanged(juce::ValueTree& treeWhosePropertyHasChanged, const juce::Identifier& property) override; void handleAsyncUpdate() override; + juce::DrawableComposite objectTypeIconComposite; + std::unique_ptr<juce::Drawable> objectTypeIcon; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportDestinationComponent); }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/ImportPreviewComponent.h b/src/shared/UI/ImportPreviewComponent.h @@ -83,10 +83,10 @@ namespace AK::WwiseTransfer void valueTreeChildAdded(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenAdded); void valueTreeChildRemoved(juce::ValueTree& parentTree, juce::ValueTree& childWhichHasBeenRemoved, int indexFromWhichChildWasRemoved); - JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportPreviewComponent) - // Inherited via AsyncUpdater void handleAsyncUpdate() override; void refreshHeader(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ImportPreviewComponent) }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/LoadingComponent.h b/src/shared/UI/LoadingComponent.h @@ -17,6 +17,7 @@ namespace AK::WwiseTransfer juce::Label text; double progress = -1; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(LoadingComponent) }; } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/MainComponent.cpp b/src/shared/UI/MainComponent.cpp @@ -23,7 +23,7 @@ namespace AK::WwiseTransfer MainComponent::MainComponent(DawContext& dawContext, const juce::String& applicationName) : applicationState(ApplicationState::create()) - , validator(applicationState) + , validator(applicationState, waapiClient) , applicationProperties(applicationName) , waapiClientWatcher(applicationState, waapiClient, WaapiClientWatcherConfig{applicationProperties.getWaapiIp(), applicationProperties.getWaapiPort(), MainComponentConstants::connectionMonitorDelayDefault, MainComponentConstants::minConnectionRetryDelayDefault, MainComponentConstants::maxConnectionRetryDelayDefault}) , originalsSubfolderComponent(applicationState, applicationName) @@ -143,4 +143,9 @@ namespace AK::WwiseTransfer { return applicationProperties.getScaleFactorOverride() > 0.0; } + + void MainComponent::transferToWwise() + { + importControlsComponent.transferToWwise(); + } } // namespace AK::WwiseTransfer diff --git a/src/shared/UI/MainComponent.h b/src/shared/UI/MainComponent.h @@ -35,12 +35,14 @@ namespace AK::WwiseTransfer void resized() override; bool hasScaleFactorOverride(); + void transferToWwise(); + private: juce::ValueTree applicationState; + WaapiClient waapiClient; ApplicationState::Validator validator; ApplicationProperties applicationProperties; - WaapiClient waapiClient; WaapiClientWatcher waapiClientWatcher; Logger logger; DawWatcher dawWatcher; diff --git a/src/shared/UI/MainWindow.cpp b/src/shared/UI/MainWindow.cpp @@ -0,0 +1,66 @@ +#include "MainWindow.h" + +#include <limits> + +namespace AK::WwiseTransfer +{ + namespace ExtensionWindowConstants + { + constexpr int width = 600; + constexpr int height = 800; + constexpr int minWidth = 420; + constexpr int minHeight = 650; + constexpr int standardDPI = 96; + } // namespace ExtensionWindowConstants + + MainWindow::MainWindow(WwiseTransfer::DawContext& dawContext, const juce::String& applicationName, bool addToDesktop) + : juce::ResizableWindow(applicationName, addToDesktop) + { + using namespace ExtensionWindowConstants; + + juce::LookAndFeel::setDefaultLookAndFeel(&lookAndFeel); + + auto mainComponent = new WwiseTransfer::MainComponent(dawContext, applicationName); + +#ifdef WIN32 + if(!mainComponent->hasScaleFactorOverride()) + { + auto scaleFactor = juce::Desktop::getInstance().getDisplays().getMainDisplay().dpi / standardDPI; + juce::Desktop::getInstance().setGlobalScaleFactor(scaleFactor); + } +#endif + + setContentOwned(mainComponent, true); + centreWithSize(width, height); + setResizable(true, true); + setResizeLimits(minWidth, minHeight, (std::numeric_limits<int>::max)(), (std::numeric_limits<int>::max)()); + } + + MainWindow::~MainWindow() + { + juce::LookAndFeel::setDefaultLookAndFeel(nullptr); + } + + int MainWindow::getDesktopWindowStyleFlags() const + { + return juce::ComponentPeer::windowHasCloseButton | juce::ComponentPeer::windowHasTitleBar | + juce::ComponentPeer::windowIsResizable | juce::ComponentPeer::windowHasMinimiseButton | + juce::ComponentPeer::windowAppearsOnTaskbar | juce::ComponentPeer::windowHasMaximiseButton; + } + + void MainWindow::userTriedToCloseWindow() + { + setVisible(false); + } + + void MainWindow::transferToWwise() + { + auto contentComponent = getContentComponent(); + if(contentComponent != nullptr) + { + auto mainComponent = dynamic_cast<MainComponent*>(contentComponent); + if(mainComponent != nullptr) + mainComponent->transferToWwise(); + } + } +} // namespace AK::WwiseTransfer diff --git a/src/shared/UI/MainWindow.h b/src/shared/UI/MainWindow.h @@ -0,0 +1,25 @@ +#pragma once + +#include "Core/DawContext.h" +#include "MainComponent.h" +#include "Theme/CustomLookAndFeel.h" + +namespace AK::WwiseTransfer +{ + class MainWindow : public juce::ResizableWindow + { + public: + MainWindow(WwiseTransfer::DawContext& dawContext, const juce::String& applicationName, bool addToDesktop); + ~MainWindow() override; + + int getDesktopWindowStyleFlags() const override; + void userTriedToCloseWindow() override; + + void transferToWwise(); + + private: + WwiseTransfer::CustomLookAndFeel lookAndFeel; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow) + }; +} // namespace AK::WwiseTransfer diff --git a/src/shared/UI/OriginalsSubfolderComponent.cpp b/src/shared/UI/OriginalsSubfolderComponent.cpp @@ -47,12 +47,12 @@ namespace AK::WwiseTransfer originalsSubfolderEditor.getValidationValue().referTo(applicationState.getPropertyAsValue(IDs::originalsSubfolderValid, nullptr)); originalsSubfolderEditor.getErrorMessageValue().referTo(applicationState.getPropertyAsValue(IDs::originalsSubfolderErrorMessage, nullptr)); + aboutButton.setTooltip("About ReaWwise"); aboutButton.onClick = [this] { showAboutWindow(); }; - fileBrowserButton.setButtonText("^"); fileBrowserButton.onClick = [this] { selectOriginalsSubfoler(); @@ -121,9 +121,10 @@ namespace AK::WwiseTransfer auto originalsFolderEmpty = originalsFolder.get().isEmpty(); projectPathLabel.setEnabled(!projectPathEmpty); + projectPathEditor.setAlpha(!projectPathEmpty ? 1 : 0.5f); fileBrowserButton.setEnabled(!projectPathEmpty && !originalsFolderEmpty); - fileBrowserButton.setTooltip(originalsFolderEmpty ? "File browser is only available when connected to Wwise 2022+" : ""); + fileBrowserButton.setTooltip(originalsFolderEmpty ? "File browser is only available when connected to Wwise 2022+" : "Browse"); } void OriginalsSubfolderComponent::selectOriginalsSubfoler() diff --git a/src/shared/UI/PresetMenuComponent.cpp b/src/shared/UI/PresetMenuComponent.cpp @@ -9,9 +9,9 @@ namespace AK::WwiseTransfer namespace PresetMenuComponentConstants { constexpr auto loadFlags = juce::FileBrowserComponent::openMode | - juce::FileBrowserComponent::canSelectFiles; + juce::FileBrowserComponent::canSelectFiles; constexpr auto saveFlags = juce::FileBrowserComponent::saveMode | - juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::warnAboutOverwriting; + juce::FileBrowserComponent::canSelectFiles | juce::FileBrowserComponent::warnAboutOverwriting; constexpr auto fileFilter = "*.xml"; } // namespace PresetMenuComponentConstants @@ -24,6 +24,8 @@ namespace AK::WwiseTransfer if(!presetFolder.exists()) presetFolder.createDirectory(); + setTooltip("Manage Presets"); + onClick = [this] { showMenu(); @@ -44,11 +46,21 @@ namespace AK::WwiseTransfer auto onFileChosen = [this](const juce::FileChooser& chooser) { auto file = chooser.getResult(); - file.create(); - const auto presetData = PersistanceHelper::hierarchyMappingToPresetData(hierarchyMapping); - file.replaceWithText(presetData); - applicationProperties.addRecentHierarchyMappingPreset(file.getFullPathName()); + auto createResult = file.create(); + + if(createResult) + { + const auto presetData = PersistanceHelper::hierarchyMappingToPresetData(hierarchyMapping); + file.replaceWithText(presetData); + applicationProperties.addRecentHierarchyMappingPreset(file.getFullPathName()); + } + // An empty path indicates that the file is invalid. This is the behavior for when the user clicks cancel in the file chooser + // https://docs.juce.com/master/classFile.html#a38506545c1402119b5fef7bcf6289fa9 + else if(!file.getFullPathName().isEmpty()) + { + juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Save Preset", createResult.getErrorMessage()); + } }; fileChooser->launchAsync(saveFlags, onFileChosen); @@ -63,7 +75,7 @@ namespace AK::WwiseTransfer auto onFileChosen = [this](const juce::FileChooser& chooser) { auto presetFile = chooser.getResult(); - if (presetFile.exists()) + if(presetFile.exists()) loadPreset(presetFile); }; diff --git a/src/shared/UI/SelectedRowPropertiesComponent.cpp b/src/shared/UI/SelectedRowPropertiesComponent.cpp @@ -20,7 +20,7 @@ namespace AK::WwiseTransfer constexpr int syncButtonWidth = 36; constexpr int propertyTemplateToggleButtonWidth = 22; constexpr int lineThickness = 2; - constexpr std::initializer_list<Wwise::ObjectType> objectTypes{Wwise::ObjectType::BlendContainer, + constexpr std::initializer_list<Wwise::ObjectType> objectTypes{Wwise::ObjectType::ActorMixer, Wwise::ObjectType::BlendContainer, Wwise::ObjectType::PhysicalFolder, Wwise::ObjectType::RandomContainer, Wwise::ObjectType::SequenceContainer, Wwise::ObjectType::SoundSFX, Wwise::ObjectType::SoundVoice, Wwise::ObjectType::SwitchContainer, Wwise::ObjectType::VirtualFolder, Wwise::ObjectType::WorkUnit}; const juce::String pastePropertiesToolTip = "Paste properties are only available when connected to Wwise 2022+"; @@ -158,7 +158,7 @@ namespace AK::WwiseTransfer auto onGetSelectedObject = [this](const Waapi::Response<Waapi::ObjectResponse>& response) { if(response.result.path.isEmpty()) - return juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Import Destination", "No object is selected in Wwise. Please select one and try again."); + return juce::AlertWindow::showMessageBoxAsync(juce::MessageBoxIconType::InfoIcon, "Property Template Path", "No object is selected in Wwise. Please select one and try again."); propertyTemplatePath = response.result.path; propertyTemplatePathType = response.result.type; @@ -185,7 +185,7 @@ namespace AK::WwiseTransfer propertyTemplatePathLabel.setEnabled(applyTemplateFeatureEnabled); propertyTemplatePathEditor.setTooltip(applyTemplateFeatureEnabled ? "" : pastePropertiesToolTip); - propertyTemplatePathSyncButton.setTooltip(applyTemplateFeatureEnabled ? "" : pastePropertiesToolTip); + propertyTemplatePathSyncButton.setTooltip(applyTemplateFeatureEnabled ? "Sync with selected object in Wwise" : pastePropertiesToolTip); propertyTemplateToggleButton.setTooltip(applyTemplateFeatureEnabled ? "" : pastePropertiesToolTip); propertyTemplatePathLabel.setTooltip(applyTemplateFeatureEnabled ? "" : pastePropertiesToolTip); } diff --git a/src/shared/UI/TruncatableTextEditor.h b/src/shared/UI/TruncatableTextEditor.h @@ -22,5 +22,7 @@ namespace AK::WwiseTransfer juce::String lastValue; void resetText(); + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(TruncatableTextEditor); }; } // namespace AK::WwiseTransfer diff --git a/src/standalone/Standalone.cpp b/src/standalone/Standalone.cpp @@ -4,31 +4,32 @@ namespace AK::WwiseTransfer { - const juce::String ReaperWwiseTransferStandalone::getApplicationName() + const juce::String Standalone::getApplicationName() { return JUCE_APPLICATION_NAME_STRING; } - const juce::String ReaperWwiseTransferStandalone::getApplicationVersion() + const juce::String Standalone::getApplicationVersion() { return JUCE_APPLICATION_VERSION_STRING; } - bool ReaperWwiseTransferStandalone::moreThanOneInstanceAllowed() + bool Standalone::moreThanOneInstanceAllowed() { return false; } - void ReaperWwiseTransferStandalone::initialise(const juce::String& commandLine) + void Standalone::initialise(const juce::String& commandLine) { juce::ignoreUnused(commandLine); mainWindow.reset(new StandaloneWindow()); + mainWindow->setVisible(true); } - void ReaperWwiseTransferStandalone::shutdown() + void Standalone::shutdown() { mainWindow.reset(); } - START_JUCE_APPLICATION(ReaperWwiseTransferStandalone) + START_JUCE_APPLICATION(Standalone) } // namespace AK::WwiseTransfer diff --git a/src/standalone/Standalone.h b/src/standalone/Standalone.h @@ -1,5 +1,7 @@ #pragma once +#include "StubContext.h" + #include <juce_gui_basics/juce_gui_basics.h> #include <memory> @@ -7,7 +9,7 @@ namespace AK::WwiseTransfer { class StandaloneWindow; - class ReaperWwiseTransferStandalone : public juce::JUCEApplication + class Standalone : public juce::JUCEApplication { public: const juce::String getApplicationName() override; diff --git a/src/standalone/StandaloneWindow.cpp b/src/standalone/StandaloneWindow.cpp @@ -4,45 +4,19 @@ namespace AK::WwiseTransfer { - namespace StandaloneWindowConstants - { - constexpr int width = 600; - constexpr int height = 800; - constexpr int minWidth = 420; - constexpr int minHeight = 650; - } // namespace StandaloneWindowConstants - StandaloneWindow::StandaloneWindow() - : juce::ResizableWindow(JUCE_APPLICATION_NAME_STRING, true) + : MainWindow(stubContext, JUCE_APPLICATION_NAME_STRING, true) { - using namespace StandaloneWindowConstants; - - juce::LookAndFeel::setDefaultLookAndFeel(&lookAndFeel); - - mainContentComponent.reset(new MainComponent(stubContext, JUCE_APPLICATION_NAME_STRING)); - - setContentNonOwned(mainContentComponent.get(), true); - centreWithSize(width, height); - setResizable(true, true); - setResizeLimits(minWidth, minHeight, (std::numeric_limits<int>::max)(), (std::numeric_limits<int>::max)()); - setVisible(true); } StandaloneWindow::~StandaloneWindow() { - juce::LookAndFeel::setDefaultLookAndFeel(nullptr); - } - - int StandaloneWindow::getDesktopWindowStyleFlags() const - { - return juce::ComponentPeer::windowHasCloseButton | juce::ComponentPeer::windowHasTitleBar | - juce::ComponentPeer::windowIsResizable | juce::ComponentPeer::windowHasMinimiseButton | - juce::ComponentPeer::windowAppearsOnTaskbar | juce::ComponentPeer::windowHasMaximiseButton; } void StandaloneWindow::userTriedToCloseWindow() { - setVisible(false); + MainWindow::userTriedToCloseWindow(); + juce::JUCEApplication::getInstance()->systemRequestedQuit(); } } // namespace AK::WwiseTransfer diff --git a/src/standalone/StandaloneWindow.h b/src/standalone/StandaloneWindow.h @@ -1,22 +1,19 @@ #pragma once #include "StubContext.h" -#include "UI/MainComponent.h" +#include "UI/MainWindow.h" namespace AK::WwiseTransfer { - class StandaloneWindow : public juce::ResizableWindow + class StandaloneWindow : public MainWindow { public: StandaloneWindow(); ~StandaloneWindow() override; - int getDesktopWindowStyleFlags() const override; void userTriedToCloseWindow() override; private: - std::unique_ptr<MainComponent> mainContentComponent; - CustomLookAndFeel lookAndFeel; StubContext stubContext; }; } // namespace AK::WwiseTransfer diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt @@ -13,6 +13,8 @@ target_sources(${PROJECT_NAME} PRIVATE ${TEST_SOURCES}) target_link_libraries(${PROJECT_NAME} PRIVATE WwiseTransfer_Shared + Reaper + ReaWwise_Static PUBLIC Catch2WithMain trompeloeil::trompeloeil diff --git a/src/test/ReaperContextTest.cpp b/src/test/ReaperContextTest.cpp @@ -0,0 +1,455 @@ +#include "ReaperContext.h" + +#include "Helpers/StringHelper.h" + +#include <catch2/catch_all.hpp> +#include <catch2/trompeloeil.hpp> + +namespace AK::ReaWwise::Test +{ +#ifdef WIN32 + juce::File projectDirectory = juce::File("C:\\MyProjectDirectory"); + juce::File otherProjectDirectory = juce::File("C:\\OtherProjectDirectory"); +#else + juce::File projectDirectory = juce::File("/MyProjectDirectory"); + juce::File otherProjectDirectory = juce::File("/OtherProjectDirectory"); +#endif + + struct TestParams + { + TestParams(const juce::File& projectDirectory) + : projectDirectory(projectDirectory) + { + resolvedDummyRenderPattern = { + projectDirectory.getChildFile("-001.wav").getFullPathName(), + projectDirectory.getChildFile("-002.wav").getFullPathName(), + }; + + renderTargets = { + projectDirectory.getChildFile("audio-file-001.wav").getFullPathName(), + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName(), + }; + + resolvedOriginalsSubfolder = { + projectDirectory.getChildFile("-001.wav").getFullPathName(), + projectDirectory.getChildFile("-002.wav").getFullPathName(), + }; + + resolvedObjectPaths = { + "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio-file-001.wav", + "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio-file-002.wav", + }; + + renderStats << "FILE:" + projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" + << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;"; + } + + juce::File projectDirectory; + juce::String outputFilename; + juce::String outputDirectory; + + std::vector<juce::String> resolvedDummyRenderPattern; + std::vector<juce::String> renderTargets; + std::vector<juce::String> resolvedOriginalsSubfolder; + std::vector<juce::String> resolvedObjectPaths; + + juce::String renderStats; + }; + + class MockReaperPlugin : public trompeloeil::mock_interface<IReaperPlugin> + { + public: + IMPLEMENT_CONST_MOCK0(getCallerVersion); + IMPLEMENT_CONST_MOCK2(registerFunction); + IMPLEMENT_CONST_MOCK0(isValid); + + IMPLEMENT_MOCK0(getMainHwnd); + IMPLEMENT_MOCK0(addExtensionsMainMenu); + IMPLEMENT_MOCK3(enumProjects); + IMPLEMENT_MOCK4(getSetProjectInfo_String); + IMPLEMENT_MOCK5(resolveRenderPattern); + IMPLEMENT_MOCK2(main_OnCommand); + IMPLEMENT_MOCK5(getProjExtState); + IMPLEMENT_MOCK4(setProjExtState); + IMPLEMENT_MOCK1(markProjectDirty); + IMPLEMENT_MOCK1(getProjectStateChangeCount); + IMPLEMENT_MOCK4(getSetProjectInfo); + IMPLEMENT_MOCK2(reallocCmdRegisterBuf); + IMPLEMENT_MOCK1(reallocCmdClear); + IMPLEMENT_MOCK0(supportsReallocCommands); + }; + + struct GetItemsForPreviewExpectations + { + GetItemsForPreviewExpectations(MockReaperPlugin& plugin, const TestParams& params) + : plugin(plugin) + , reaproject(42) + , reaperProjectPath(params.projectDirectory.getChildFile("test.rpp").getFullPathName()) + , outputDirectory(params.outputDirectory) + , dummyResolvedRenderPatternDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedDummyRenderPattern)) + , resolvedOutputFilenameDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.renderTargets)) + , resolvedOriginalsSubfolderDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedOriginalsSubfolder)) + , resolvedObjectPathsDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedObjectPaths)) + , renderFile("RENDER_FILE") + , renderPattern("RENDER_PATTERN") + , renderTargetsEx("RENDER_TARGETS_EX") + { + using trompeloeil::_; // wild card for matching any value + + expectations[0] = NAMED_ALLOW_CALL(plugin, enumProjects(-1, _, _)) + .SIDE_EFFECT(memset(_2, '\0', size_t(reaperProjectPath.length()))) + .SIDE_EFFECT(memcpy(_2, reaperProjectPath.getCharPointer(), size_t(reaperProjectPath.length()))) + .RETURN((ReaProject*)&reaproject); + + expectations[1] = NAMED_ALLOW_CALL(plugin, supportsReallocCommands()) + .RETURN(false); + + expectations[2] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) + .TIMES(1) + .WITH(juce::String(_2) == renderTargetsEx) + .SIDE_EFFECT(memset(_3, '\0', size_t(resolvedOutputFilenameDblNullTerminated.size()))) + .SIDE_EFFECT(memcpy(_3, &resolvedOutputFilenameDblNullTerminated[0], size_t(resolvedOutputFilenameDblNullTerminated.size()))) + .RETURN(true) + .IN_SEQUENCE(sequence); + + expectations[3] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) + .TIMES(1) + .RETURN(dummyResolvedRenderPatternDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[4] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) + .TIMES(1) + .WITH(_4 != nullptr) + .SIDE_EFFECT(memset(_4, '\0', size_t(dummyResolvedRenderPatternDblNullTerminated.size()))) + .SIDE_EFFECT(memcpy(_4, &dummyResolvedRenderPatternDblNullTerminated[0], size_t(dummyResolvedRenderPatternDblNullTerminated.size()))) + .RETURN(dummyResolvedRenderPatternDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[5] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) + .TIMES(1) + .WITH(juce::String(_2) == renderPattern) + .SIDE_EFFECT(memset(_3, '\0', size_t(renderPattern.length()))) + .SIDE_EFFECT(memcpy(_3, renderPattern.getCharPointer(), size_t(renderPattern.length()))) + .RETURN(true) + .IN_SEQUENCE(sequence); + + expectations[6] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) + .TIMES(1) + .RETURN(resolvedOutputFilenameDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[7] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) + .TIMES(1) + .WITH(_4 != nullptr) + .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedOutputFilenameDblNullTerminated.size()))) + .SIDE_EFFECT(memcpy(_4, &resolvedOutputFilenameDblNullTerminated[0], size_t(resolvedOutputFilenameDblNullTerminated.size()))) + .RETURN(resolvedOutputFilenameDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[8] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) + .TIMES(1) + .RETURN(resolvedOriginalsSubfolderDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[9] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) + .TIMES(1) + .WITH(_4 != nullptr) + .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedOriginalsSubfolderDblNullTerminated.size()))) + .SIDE_EFFECT(memcpy(_4, &resolvedOriginalsSubfolderDblNullTerminated[0], size_t(resolvedOriginalsSubfolderDblNullTerminated.size()))) + .RETURN(resolvedOriginalsSubfolderDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[10] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, nullptr, _, nullptr, 0)) + .TIMES(1) + .RETURN(resolvedObjectPathsDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + + expectations[11] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, nullptr, _, _, _)) + .TIMES(1) + .WITH(_4 != nullptr) + .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedObjectPathsDblNullTerminated.size()))) + .SIDE_EFFECT(memcpy(_4, &resolvedObjectPathsDblNullTerminated[0], size_t(resolvedObjectPathsDblNullTerminated.size()))) + .RETURN(resolvedObjectPathsDblNullTerminated.size()) + .IN_SEQUENCE(sequence); + } + + protected: + MockReaperPlugin& plugin; + trompeloeil::sequence sequence; + int reaproject; + juce::String reaperProjectPath; + + private: + juce::String outputDirectory; + std::vector<char> dummyResolvedRenderPatternDblNullTerminated; + std::vector<char> resolvedOutputFilenameDblNullTerminated; + std::vector<char> resolvedOriginalsSubfolderDblNullTerminated; + std::vector<char> resolvedObjectPathsDblNullTerminated; + juce::String renderFile; + juce::String renderPattern; + juce::String renderTargetsEx; + + std::array<std::unique_ptr<trompeloeil::expectation>, 12> expectations; + }; + + struct GetItemsForImportExpectations : private GetItemsForPreviewExpectations + { + GetItemsForImportExpectations(MockReaperPlugin& plugin, const TestParams& params) + : GetItemsForPreviewExpectations(plugin, params) + , renderStatsKey("RENDER_STATS") + , renderStats(params.renderStats) + { + using trompeloeil::_; // wild card for matching any value + + expectations[0] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) + .TIMES(1) + .WITH(juce::String(_2) == renderStatsKey) + .SIDE_EFFECT(memset(_3, '\0', size_t(renderStats.length()))) + .SIDE_EFFECT(memcpy(_3, renderStats.getCharPointer(), size_t(renderStats.length()))) + .RETURN(true) + .IN_SEQUENCE(sequence); + } + + private: + juce::String renderStatsKey; + juce::String renderStats; + std::array<std::unique_ptr<trompeloeil::expectation>, 1> expectations; + }; + + std::vector<WwiseTransfer::Import::PreviewItem> getItemsForPreview(const TestParams& params) + { + WwiseTransfer::Import::Options importOptions{"", "", ""}; + + MockReaperPlugin plugin; + ReaperContext reaperContext(plugin); + + GetItemsForPreviewExpectations expectations(plugin, params); + + return reaperContext.getItemsForPreview(importOptions); + } + + std::vector<WwiseTransfer::Import::Item> getItemsForImport(const TestParams& params) + { + WwiseTransfer::Import::Options importOptions{"", "", ""}; + + MockReaperPlugin plugin; + ReaperContext reaperContext(plugin); + + GetItemsForImportExpectations expectations(plugin, params); + + return reaperContext.getItemsForImport(importOptions); + } + + SCENARIO("ReaperContext getItemsForImport") + { + TestParams params(projectDirectory); + + GIVEN("Default test params") + { + WHEN("Render stats contains a semi-colon at the end") + { + auto importItems = getItemsForImport(params); + + THEN("The resulting render file paths must be parsed correctly") + { + REQUIRE(importItems.size() == 2); + REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); + REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); + } + } + + AND_WHEN("Render stats does not contain semi-colon at the end") + { + params.renderStats.clear(); + params.renderStats << "FILE:" + projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" + << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; + + auto importItems = getItemsForImport(params); + + THEN("The resulting render file paths must be parsed correctly") + { + REQUIRE(importItems.size() == 2); + REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); + REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); + } + } + + AND_WHEN("Render stats contains some unexpected text before the first \"FILE:\"") + { + params.renderStats.clear(); + params.renderStats << "Unex:pect;edTextFILE:" + projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" + << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; + + auto importItems = getItemsForImport(params); + + THEN("The resulting render file paths must be parsed correctly") + { + REQUIRE(importItems.size() == 2); + REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); + REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); + } + } + + AND_WHEN("Render stats does not contain \"FILE:\"") + { + params.renderStats.clear(); + params.renderStats << projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" + << projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; + + auto importItems = getItemsForImport(params); + + THEN("No import items are returned") + { + REQUIRE(importItems.size() == 0); + } + } + + AND_WHEN("Render stats is empty") + { + params.renderStats.clear(); + + auto importItems = getItemsForImport(params); + + THEN("No import items are returned") + { + REQUIRE(importItems.size() == 0); + } + } + } + } + + SCENARIO("ReaperContext getItemsForPreview") + { + TestParams params(projectDirectory); + + const juce::String upOneDirectorySymbol = ".." + juce::File::getSeparatorString(); + + GIVEN("Default test params") + { + THEN("The resulting audio file will match the render target returned by REAPER") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].audioFilePath == params.renderTargets[0]); + REQUIRE(previewItems[1].audioFilePath == params.renderTargets[1]); + } + + AND_WHEN("The configured originals subfolder is empty") + { + THEN("The originals subfolder parameter for the import item should be empty") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].originalsSubFolder.isEmpty()); + REQUIRE(previewItems[1].originalsSubFolder.isEmpty()); + } + } + + AND_WHEN("The configured originals subfolder is not empty") + { + juce::String expectedOriginalsSubfolder = "OriginalsSubfolder"; + + params.resolvedOriginalsSubfolder = { + projectDirectory.getChildFile(expectedOriginalsSubfolder).getChildFile("-001.wav").getFullPathName(), + projectDirectory.getChildFile(expectedOriginalsSubfolder).getChildFile("-002.wav").getFullPathName(), + }; + + THEN("The originals subfolder parameter for the import item should match the configured originals subfolder") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].originalsSubFolder == expectedOriginalsSubfolder); + REQUIRE(previewItems[1].originalsSubFolder == expectedOriginalsSubfolder); + } + + AND_WHEN("The audio file path contains subdirectories") + { + params.renderTargets = { + projectDirectory.getChildFile("AudioFolder").getChildFile("audio-file-001.wav").getFullPathName(), + projectDirectory.getChildFile("AudioFolder").getChildFile("audio-file-002.wav").getFullPathName(), + }; + + THEN("The originals subfolder for the preview item should be a combination of the configured originals subfolder and the audio file path's folder relative to the render directory") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].originalsSubFolder == juce::String("OriginalsSubfolder") + juce::File::getSeparatorChar() + "AudioFolder"); + REQUIRE(previewItems[1].originalsSubFolder == juce::String("OriginalsSubfolder") + juce::File::getSeparatorChar() + "AudioFolder"); + } + } + + AND_WHEN("The audio file path contains " + upOneDirectorySymbol) + { + params.renderTargets = { + projectDirectory.getChildFile(upOneDirectorySymbol + "audio-file-001.wav").getFullPathName(), + projectDirectory.getChildFile(upOneDirectorySymbol + "audio-file-002.wav").getFullPathName(), + }; + + THEN("The originals subfolder for the preview item should be empty since it would be cancelled out due to the " + upOneDirectorySymbol) + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].originalsSubFolder.isEmpty()); + REQUIRE(previewItems[1].originalsSubFolder.isEmpty()); + } + } + } + + AND_WHEN("The audio file path contains a semi-colon") + { + params.renderTargets = { + projectDirectory.getChildFile("audio;-file-001.wav").getFullPathName(), + projectDirectory.getChildFile("audio;-file-002.wav").getFullPathName(), + }; + + THEN("The resulting audio file path should match the expected value") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].audioFilePath == projectDirectory.getChildFile("audio;-file-001.wav").getFullPathName()); + REQUIRE(previewItems[1].audioFilePath == projectDirectory.getChildFile("audio;-file-002.wav").getFullPathName()); + } + } + + AND_WHEN("The audio file path contains an extra period") + { + params.renderTargets = { + projectDirectory.getChildFile("audio.file-001.wav").getFullPathName(), + projectDirectory.getChildFile("audio.file-002.wav").getFullPathName(), + }; + + THEN("The resulting audio file path should match the expected value") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].audioFilePath == projectDirectory.getChildFile("audio.file-001.wav").getFullPathName()); + REQUIRE(previewItems[1].audioFilePath == projectDirectory.getChildFile("audio.file-002.wav").getFullPathName()); + } + } + + AND_WHEN("The object path contains an extra period") + { + params.resolvedObjectPaths = { + "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-001.wav", + "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-002.wav", + }; + + THEN("The resulting object path should be the resolved object path without the file extension") + { + auto previewItems = getItemsForPreview(params); + + REQUIRE(previewItems.size() == 2); + REQUIRE(previewItems[0].path == "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-001"); + REQUIRE(previewItems[1].path == "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-002"); + } + } + } + } +} // namespace AK::ReaWwise::Test diff --git a/src/test/WwiseHelperTests.h b/src/test/WwiseHelperTests.h @@ -8,8 +8,8 @@ namespace AK::WwiseTransfer::Test { - std::unordered_map <Wwise::ObjectType, juce::String> objectTypeStringMap{ - {Wwise::ObjectType::ActorMixer, "Actor Mixer"}, + std::unordered_map<Wwise::ObjectType, juce::String> objectTypeStringMap{ + {Wwise::ObjectType::ActorMixer, "Actor-Mixer"}, {Wwise::ObjectType::AudioFileSource, "Audio File Source"}, {Wwise::ObjectType::BlendContainer, "Blend Container"}, {Wwise::ObjectType::PhysicalFolder, "Physical Folder"}, @@ -21,10 +21,9 @@ namespace AK::WwiseTransfer::Test {Wwise::ObjectType::VirtualFolder, "Virtual Folder"}, {Wwise::ObjectType::WorkUnit, "Work Unit"}, {Wwise::ObjectType::Sound, "Sound"}, - {Wwise::ObjectType::Unknown, "Unknown"} - }; + {Wwise::ObjectType::Unknown, "Unknown"}}; - std::vector <Wwise::ObjectType> objectTypes{ + std::vector<Wwise::ObjectType> objectTypes{ Wwise::ObjectType::ActorMixer, Wwise::ObjectType::AudioFileSource, Wwise::ObjectType::BlendContainer, @@ -37,6 +36,5 @@ namespace AK::WwiseTransfer::Test Wwise::ObjectType::VirtualFolder, Wwise::ObjectType::WorkUnit, Wwise::ObjectType::Sound, - Wwise::ObjectType::Unknown - }; -} // namespace AK::WwiseTransfer:Test + Wwise::ObjectType::Unknown}; +} // namespace AK::WwiseTransfer::Test