ReaWwise

REAPER extension
Log | Files | Refs | Submodules

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