ReaperContextTest.cpp (20565B)
1 /*---------------------------------------------------------------------------------------- 2 3 Copyright (c) 2023 AUDIOKINETIC Inc. 4 5 This file is licensed to use under the license available at: 6 https://github.com/audiokinetic/ReaWwise/blob/main/License.txt (the "License"). 7 You may not use this file except in compliance with the License. 8 9 Unless required by applicable law or agreed to in writing, software distributed 10 under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 specific language governing permissions and limitations under the License. 13 14 ----------------------------------------------------------------------------------------*/ 15 16 #include "ReaperContext.h" 17 18 #include "Helpers/StringHelper.h" 19 20 #include <catch2/catch_all.hpp> 21 #include <catch2/trompeloeil.hpp> 22 23 namespace AK::ReaWwise::Test 24 { 25 #ifdef WIN32 26 juce::File projectDirectory = juce::File("C:\\MyProjectDirectory"); 27 juce::File otherProjectDirectory = juce::File("C:\\OtherProjectDirectory"); 28 #else 29 juce::File projectDirectory = juce::File("/MyProjectDirectory"); 30 juce::File otherProjectDirectory = juce::File("/OtherProjectDirectory"); 31 #endif 32 33 struct TestParams 34 { 35 TestParams(const juce::File& projectDirectory) 36 : projectDirectory(projectDirectory) 37 { 38 resolvedDummyRenderPattern = { 39 projectDirectory.getChildFile("-001.wav").getFullPathName(), 40 projectDirectory.getChildFile("-002.wav").getFullPathName(), 41 }; 42 43 renderTargets = { 44 projectDirectory.getChildFile("audio-file-001.wav").getFullPathName(), 45 projectDirectory.getChildFile("audio-file-002.wav").getFullPathName(), 46 }; 47 48 resolvedOriginalsSubfolder = { 49 projectDirectory.getChildFile("-001.wav").getFullPathName(), 50 projectDirectory.getChildFile("-002.wav").getFullPathName(), 51 }; 52 53 resolvedObjectPaths = { 54 "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio-file-001.wav", 55 "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio-file-002.wav", 56 }; 57 58 renderStats << "FILE:" + projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" 59 << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;"; 60 } 61 62 juce::File projectDirectory; 63 juce::String outputFilename; 64 juce::String outputDirectory; 65 66 std::vector<juce::String> resolvedDummyRenderPattern; 67 std::vector<juce::String> renderTargets; 68 std::vector<juce::String> resolvedOriginalsSubfolder; 69 std::vector<juce::String> resolvedObjectPaths; 70 71 juce::String renderStats; 72 }; 73 74 class MockReaperPlugin : public trompeloeil::mock_interface<IReaperPlugin> 75 { 76 public: 77 IMPLEMENT_CONST_MOCK0(getCallerVersion); 78 IMPLEMENT_CONST_MOCK2(registerFunction); 79 IMPLEMENT_CONST_MOCK0(isValid); 80 81 IMPLEMENT_MOCK0(getMainHwnd); 82 IMPLEMENT_MOCK0(addExtensionsMainMenu); 83 IMPLEMENT_MOCK3(enumProjects); 84 IMPLEMENT_MOCK4(getSetProjectInfo_String); 85 IMPLEMENT_MOCK5(resolveRenderPattern); 86 IMPLEMENT_MOCK2(main_OnCommand); 87 IMPLEMENT_MOCK5(getProjExtState); 88 IMPLEMENT_MOCK4(setProjExtState); 89 IMPLEMENT_MOCK1(markProjectDirty); 90 IMPLEMENT_MOCK1(getProjectStateChangeCount); 91 IMPLEMENT_MOCK4(getSetProjectInfo); 92 IMPLEMENT_MOCK2(reallocCmdRegisterBuf); 93 IMPLEMENT_MOCK1(reallocCmdClear); 94 IMPLEMENT_MOCK0(supportsReallocCommands); 95 }; 96 97 struct GetItemsForPreviewExpectations 98 { 99 GetItemsForPreviewExpectations(MockReaperPlugin& plugin, const TestParams& params) 100 : plugin(plugin) 101 , reaproject(42) 102 , reaperProjectPath(params.projectDirectory.getChildFile("test.rpp").getFullPathName()) 103 , outputDirectory(params.outputDirectory) 104 , dummyResolvedRenderPatternDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedDummyRenderPattern)) 105 , resolvedOutputFilenameDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.renderTargets)) 106 , resolvedOriginalsSubfolderDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedOriginalsSubfolder)) 107 , resolvedObjectPathsDblNullTerminated(WwiseTransfer::StringHelper::createDoubleNullTerminatedStringBuffer(params.resolvedObjectPaths)) 108 , renderFile("RENDER_FILE") 109 , renderPattern("RENDER_PATTERN") 110 , renderTargetsEx("RENDER_TARGETS_EX") 111 { 112 using trompeloeil::_; // wild card for matching any value 113 114 expectations[0] = NAMED_ALLOW_CALL(plugin, enumProjects(-1, _, _)) 115 .SIDE_EFFECT(memset(_2, '\0', size_t(reaperProjectPath.length()))) 116 .SIDE_EFFECT(memcpy(_2, reaperProjectPath.getCharPointer(), size_t(reaperProjectPath.length()))) 117 .RETURN((ReaProject*)&reaproject); 118 119 expectations[1] = NAMED_ALLOW_CALL(plugin, supportsReallocCommands()) 120 .RETURN(false); 121 122 expectations[2] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) 123 .TIMES(1) 124 .WITH(juce::String(_2) == renderTargetsEx) 125 .SIDE_EFFECT(memset(_3, '\0', size_t(resolvedOutputFilenameDblNullTerminated.size()))) 126 .SIDE_EFFECT(memcpy(_3, &resolvedOutputFilenameDblNullTerminated[0], size_t(resolvedOutputFilenameDblNullTerminated.size()))) 127 .RETURN(true) 128 .IN_SEQUENCE(sequence); 129 130 expectations[3] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) 131 .TIMES(1) 132 .RETURN(dummyResolvedRenderPatternDblNullTerminated.size()) 133 .IN_SEQUENCE(sequence); 134 135 expectations[4] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) 136 .TIMES(1) 137 .WITH(_4 != nullptr) 138 .SIDE_EFFECT(memset(_4, '\0', size_t(dummyResolvedRenderPatternDblNullTerminated.size()))) 139 .SIDE_EFFECT(memcpy(_4, &dummyResolvedRenderPatternDblNullTerminated[0], size_t(dummyResolvedRenderPatternDblNullTerminated.size()))) 140 .RETURN(dummyResolvedRenderPatternDblNullTerminated.size()) 141 .IN_SEQUENCE(sequence); 142 143 expectations[5] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) 144 .TIMES(1) 145 .WITH(juce::String(_2) == renderPattern) 146 .SIDE_EFFECT(memset(_3, '\0', size_t(renderPattern.length()))) 147 .SIDE_EFFECT(memcpy(_3, renderPattern.getCharPointer(), size_t(renderPattern.length()))) 148 .RETURN(true) 149 .IN_SEQUENCE(sequence); 150 151 expectations[6] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) 152 .TIMES(1) 153 .RETURN(resolvedOutputFilenameDblNullTerminated.size()) 154 .IN_SEQUENCE(sequence); 155 156 expectations[7] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) 157 .TIMES(1) 158 .WITH(_4 != nullptr) 159 .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedOutputFilenameDblNullTerminated.size()))) 160 .SIDE_EFFECT(memcpy(_4, &resolvedOutputFilenameDblNullTerminated[0], size_t(resolvedOutputFilenameDblNullTerminated.size()))) 161 .RETURN(resolvedOutputFilenameDblNullTerminated.size()) 162 .IN_SEQUENCE(sequence); 163 164 expectations[8] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, nullptr, 0)) 165 .TIMES(1) 166 .RETURN(resolvedOriginalsSubfolderDblNullTerminated.size()) 167 .IN_SEQUENCE(sequence); 168 169 expectations[9] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, _, _, _, _)) 170 .TIMES(1) 171 .WITH(_4 != nullptr) 172 .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedOriginalsSubfolderDblNullTerminated.size()))) 173 .SIDE_EFFECT(memcpy(_4, &resolvedOriginalsSubfolderDblNullTerminated[0], size_t(resolvedOriginalsSubfolderDblNullTerminated.size()))) 174 .RETURN(resolvedOriginalsSubfolderDblNullTerminated.size()) 175 .IN_SEQUENCE(sequence); 176 177 expectations[10] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, nullptr, _, nullptr, 0)) 178 .TIMES(1) 179 .RETURN(resolvedObjectPathsDblNullTerminated.size()) 180 .IN_SEQUENCE(sequence); 181 182 expectations[11] = NAMED_REQUIRE_CALL(plugin, resolveRenderPattern(_, nullptr, _, _, _)) 183 .TIMES(1) 184 .WITH(_4 != nullptr) 185 .SIDE_EFFECT(memset(_4, '\0', size_t(resolvedObjectPathsDblNullTerminated.size()))) 186 .SIDE_EFFECT(memcpy(_4, &resolvedObjectPathsDblNullTerminated[0], size_t(resolvedObjectPathsDblNullTerminated.size()))) 187 .RETURN(resolvedObjectPathsDblNullTerminated.size()) 188 .IN_SEQUENCE(sequence); 189 } 190 191 protected: 192 MockReaperPlugin& plugin; 193 trompeloeil::sequence sequence; 194 int reaproject; 195 juce::String reaperProjectPath; 196 197 private: 198 juce::String outputDirectory; 199 std::vector<char> dummyResolvedRenderPatternDblNullTerminated; 200 std::vector<char> resolvedOutputFilenameDblNullTerminated; 201 std::vector<char> resolvedOriginalsSubfolderDblNullTerminated; 202 std::vector<char> resolvedObjectPathsDblNullTerminated; 203 juce::String renderFile; 204 juce::String renderPattern; 205 juce::String renderTargetsEx; 206 207 std::array<std::unique_ptr<trompeloeil::expectation>, 12> expectations; 208 }; 209 210 struct GetItemsForImportExpectations : private GetItemsForPreviewExpectations 211 { 212 GetItemsForImportExpectations(MockReaperPlugin& plugin, const TestParams& params) 213 : GetItemsForPreviewExpectations(plugin, params) 214 , renderStatsKey("RENDER_STATS") 215 , renderStats(params.renderStats) 216 { 217 using trompeloeil::_; // wild card for matching any value 218 219 expectations[0] = NAMED_REQUIRE_CALL(plugin, getSetProjectInfo_String(_, _, _, false)) 220 .TIMES(1) 221 .WITH(juce::String(_2) == renderStatsKey) 222 .SIDE_EFFECT(memset(_3, '\0', size_t(renderStats.length()))) 223 .SIDE_EFFECT(memcpy(_3, renderStats.getCharPointer(), size_t(renderStats.length()))) 224 .RETURN(true) 225 .IN_SEQUENCE(sequence); 226 } 227 228 private: 229 juce::String renderStatsKey; 230 juce::String renderStats; 231 std::array<std::unique_ptr<trompeloeil::expectation>, 1> expectations; 232 }; 233 234 std::vector<WwiseTransfer::Import::PreviewItem> getItemsForPreview(const TestParams& params) 235 { 236 WwiseTransfer::Import::Options importOptions{"", "", ""}; 237 238 MockReaperPlugin plugin; 239 ReaperContext reaperContext(plugin); 240 241 GetItemsForPreviewExpectations expectations(plugin, params); 242 243 return reaperContext.getItemsForPreview(importOptions); 244 } 245 246 std::vector<WwiseTransfer::Import::Item> getItemsForImport(const TestParams& params) 247 { 248 WwiseTransfer::Import::Options importOptions{"", "", ""}; 249 250 MockReaperPlugin plugin; 251 ReaperContext reaperContext(plugin); 252 253 GetItemsForImportExpectations expectations(plugin, params); 254 255 return reaperContext.getItemsForImport(importOptions); 256 } 257 258 SCENARIO("ReaperContext getItemsForImport") 259 { 260 TestParams params(projectDirectory); 261 262 GIVEN("Default test params") 263 { 264 WHEN("Render stats contains a semi-colon at the end") 265 { 266 auto importItems = getItemsForImport(params); 267 268 THEN("The resulting render file paths must be parsed correctly") 269 { 270 REQUIRE(importItems.size() == 2); 271 REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); 272 REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); 273 } 274 } 275 276 AND_WHEN("Render stats does not contain semi-colon at the end") 277 { 278 params.renderStats.clear(); 279 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;" 280 << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; 281 282 auto importItems = getItemsForImport(params); 283 284 THEN("The resulting render file paths must be parsed correctly") 285 { 286 REQUIRE(importItems.size() == 2); 287 REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); 288 REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); 289 } 290 } 291 292 AND_WHEN("Render stats contains some unexpected text before the first \"FILE:\"") 293 { 294 params.renderStats.clear(); 295 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;" 296 << "FILE:" + projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; 297 298 auto importItems = getItemsForImport(params); 299 300 THEN("The resulting render file paths must be parsed correctly") 301 { 302 REQUIRE(importItems.size() == 2); 303 REQUIRE(importItems[0].renderFilePath == projectDirectory.getChildFile("audio-file-001.wav").getFullPathName()); 304 REQUIRE(importItems[1].renderFilePath == projectDirectory.getChildFile("audio-file-002.wav").getFullPathName()); 305 } 306 } 307 308 AND_WHEN("Render stats does not contain \"FILE:\"") 309 { 310 params.renderStats.clear(); 311 params.renderStats << projectDirectory.getChildFile("audio-file-001.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000;" 312 << projectDirectory.getChildFile("audio-file-002.wav").getFullPathName() + ";PEAK:-0000.000000;LRA:-0000.000000;LUFSMMAX:-0000.000000;LUFSSMAX:-0000.000000;LUFSI:-0000.000000"; 313 314 auto importItems = getItemsForImport(params); 315 316 THEN("No import items are returned") 317 { 318 REQUIRE(importItems.size() == 0); 319 } 320 } 321 322 AND_WHEN("Render stats is empty") 323 { 324 params.renderStats.clear(); 325 326 auto importItems = getItemsForImport(params); 327 328 THEN("No import items are returned") 329 { 330 REQUIRE(importItems.size() == 0); 331 } 332 } 333 } 334 } 335 336 SCENARIO("ReaperContext getItemsForPreview") 337 { 338 TestParams params(projectDirectory); 339 340 const juce::String upOneDirectorySymbol = ".." + juce::File::getSeparatorString(); 341 342 GIVEN("Default test params") 343 { 344 THEN("The resulting audio file will match the render target returned by REAPER") 345 { 346 auto previewItems = getItemsForPreview(params); 347 348 REQUIRE(previewItems.size() == 2); 349 REQUIRE(previewItems[0].audioFilePath == params.renderTargets[0]); 350 REQUIRE(previewItems[1].audioFilePath == params.renderTargets[1]); 351 } 352 353 AND_WHEN("The configured originals subfolder is empty") 354 { 355 THEN("The originals subfolder parameter for the import item should be empty") 356 { 357 auto previewItems = getItemsForPreview(params); 358 359 REQUIRE(previewItems.size() == 2); 360 REQUIRE(previewItems[0].originalsSubFolder.isEmpty()); 361 REQUIRE(previewItems[1].originalsSubFolder.isEmpty()); 362 } 363 } 364 365 AND_WHEN("The configured originals subfolder is not empty") 366 { 367 juce::String expectedOriginalsSubfolder = "OriginalsSubfolder"; 368 369 params.resolvedOriginalsSubfolder = { 370 projectDirectory.getChildFile(expectedOriginalsSubfolder).getChildFile("-001.wav").getFullPathName(), 371 projectDirectory.getChildFile(expectedOriginalsSubfolder).getChildFile("-002.wav").getFullPathName(), 372 }; 373 374 THEN("The originals subfolder parameter for the import item should match the configured originals subfolder") 375 { 376 auto previewItems = getItemsForPreview(params); 377 378 REQUIRE(previewItems.size() == 2); 379 REQUIRE(previewItems[0].originalsSubFolder == expectedOriginalsSubfolder); 380 REQUIRE(previewItems[1].originalsSubFolder == expectedOriginalsSubfolder); 381 } 382 383 AND_WHEN("The audio file path contains subdirectories") 384 { 385 params.renderTargets = { 386 projectDirectory.getChildFile("AudioFolder").getChildFile("audio-file-001.wav").getFullPathName(), 387 projectDirectory.getChildFile("AudioFolder").getChildFile("audio-file-002.wav").getFullPathName(), 388 }; 389 390 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") 391 { 392 auto previewItems = getItemsForPreview(params); 393 394 REQUIRE(previewItems.size() == 2); 395 REQUIRE(previewItems[0].originalsSubFolder == juce::String("OriginalsSubfolder") + juce::File::getSeparatorChar() + "AudioFolder"); 396 REQUIRE(previewItems[1].originalsSubFolder == juce::String("OriginalsSubfolder") + juce::File::getSeparatorChar() + "AudioFolder"); 397 } 398 } 399 400 AND_WHEN("The audio file path contains " + upOneDirectorySymbol) 401 { 402 params.renderTargets = { 403 projectDirectory.getChildFile(upOneDirectorySymbol + "audio-file-001.wav").getFullPathName(), 404 projectDirectory.getChildFile(upOneDirectorySymbol + "audio-file-002.wav").getFullPathName(), 405 }; 406 407 THEN("The originals subfolder for the preview item should be empty since it would be cancelled out due to the " + upOneDirectorySymbol) 408 { 409 auto previewItems = getItemsForPreview(params); 410 411 REQUIRE(previewItems.size() == 2); 412 REQUIRE(previewItems[0].originalsSubFolder.isEmpty()); 413 REQUIRE(previewItems[1].originalsSubFolder.isEmpty()); 414 } 415 } 416 } 417 418 AND_WHEN("The audio file path contains a semi-colon") 419 { 420 params.renderTargets = { 421 projectDirectory.getChildFile("audio;-file-001.wav").getFullPathName(), 422 projectDirectory.getChildFile("audio;-file-002.wav").getFullPathName(), 423 }; 424 425 THEN("The resulting audio file path should match the expected value") 426 { 427 auto previewItems = getItemsForPreview(params); 428 429 REQUIRE(previewItems.size() == 2); 430 REQUIRE(previewItems[0].audioFilePath == projectDirectory.getChildFile("audio;-file-001.wav").getFullPathName()); 431 REQUIRE(previewItems[1].audioFilePath == projectDirectory.getChildFile("audio;-file-002.wav").getFullPathName()); 432 } 433 } 434 435 AND_WHEN("The audio file path contains an extra period") 436 { 437 params.renderTargets = { 438 projectDirectory.getChildFile("audio.file-001.wav").getFullPathName(), 439 projectDirectory.getChildFile("audio.file-002.wav").getFullPathName(), 440 }; 441 442 THEN("The resulting audio file path should match the expected value") 443 { 444 auto previewItems = getItemsForPreview(params); 445 446 REQUIRE(previewItems.size() == 2); 447 REQUIRE(previewItems[0].audioFilePath == projectDirectory.getChildFile("audio.file-001.wav").getFullPathName()); 448 REQUIRE(previewItems[1].audioFilePath == projectDirectory.getChildFile("audio.file-002.wav").getFullPathName()); 449 } 450 } 451 452 AND_WHEN("The object path contains an extra period") 453 { 454 params.resolvedObjectPaths = { 455 "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-001.wav", 456 "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-002.wav", 457 }; 458 459 THEN("The resulting object path should be the resolved object path without the file extension") 460 { 461 auto previewItems = getItemsForPreview(params); 462 463 REQUIRE(previewItems.size() == 2); 464 REQUIRE(previewItems[0].path == "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-001"); 465 REQUIRE(previewItems[1].path == "\\Actor-Mixer Hierarchy\\Default Work Unit\\<Random Container>Footsteps\\<SoundSFX>audio.file-002"); 466 } 467 } 468 } 469 } 470 } // namespace AK::ReaWwise::Test