ReaWwise

REAPER extension
Log | Files | Refs | Submodules

ReaperContext.cpp (13899B)


      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 #include "Helpers/WwiseHelper.h"
     20 #include "Model/Wwise.h"
     21 
     22 #include <regex>
     23 
     24 namespace AK::ReaWwise
     25 {
     26 	namespace ReaperContextConstants
     27 	{
     28 		constexpr int defaultBufferSize = 4 * 1024;
     29 		constexpr int largeBufferSize = 4 * 1024 * 1024;
     30 		const juce::String stateSizeKey = "stateSize";
     31 		const juce::String stateKey = "state";
     32 		const juce::String applicationKey = "ReaWwise";
     33 		const juce::String defaultRenderPattern = "untitled";
     34 	} // namespace ReaperContextConstants
     35 
     36 	enum ReaperCommands
     37 	{
     38 		Render = 42230
     39 	};
     40 
     41 	ReaperContext::ReaperContext(IReaperPlugin& reaperPlugin)
     42 		: reaperPlugin(reaperPlugin)
     43 	{
     44 	}
     45 
     46 	ReaperContext::~ReaperContext()
     47 	{
     48 	}
     49 
     50 	juce::String ReaperContext::getSessionName()
     51 	{
     52 		juce::ScopedLock lock{apiAccess};
     53 
     54 		auto projectInfo = getProjectInfo();
     55 		return projectInfo.projectPath;
     56 	}
     57 
     58 	bool ReaperContext::saveState(juce::ValueTree applicationState)
     59 	{
     60 		using namespace ReaperContextConstants;
     61 
     62 		juce::ScopedLock lock{apiAccess};
     63 
     64 		auto projectInfo = getProjectInfo();
     65 
     66 		const auto applicationStateString = applicationState.toXmlString();
     67 		const auto applicationStateStringSize = juce::String(applicationStateString.getNumBytesAsUTF8());
     68 		if(reaperPlugin.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), applicationStateStringSize.toUTF8()) &&
     69 			reaperPlugin.setProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), applicationStateString.toUTF8()))
     70 		{
     71 			reaperPlugin.markProjectDirty(projectInfo.projectReference);
     72 			return true;
     73 		}
     74 
     75 		return false;
     76 	}
     77 
     78 	juce::ValueTree ReaperContext::retrieveState()
     79 	{
     80 		using namespace ReaperContextConstants;
     81 
     82 		juce::ScopedLock lock{apiAccess};
     83 
     84 		auto projectInfo = getProjectInfo();
     85 
     86 		std::string buffer(defaultBufferSize, '\0');
     87 		reaperPlugin.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateSizeKey.toUTF8(), &buffer[0], buffer.size());
     88 
     89 		const auto stateSize = std::strtoll(&buffer[0], nullptr, 10);
     90 		if(stateSize == 0)
     91 			return {};
     92 
     93 		buffer.resize(stateSize);
     94 		if(reaperPlugin.getProjExtState(projectInfo.projectReference, applicationKey.toUTF8(), stateKey.toUTF8(), &buffer[0], buffer.size()))
     95 			return juce::ValueTree::fromXml(buffer);
     96 
     97 		return {};
     98 	}
     99 
    100 	void ReaperContext::renderItems()
    101 	{
    102 		reaperPlugin.main_OnCommand(ReaperCommands::Render, 0);
    103 	}
    104 
    105 	std::vector<juce::String> ReaperContext::getRenderTargets()
    106 	{
    107 		auto projectInfo = getProjectInfo();
    108 
    109 		std::vector<juce::String> renderTargets;
    110 
    111 		auto result = getProjectStringBuffer(projectInfo.projectReference, "RENDER_TARGETS_EX");
    112 
    113 		if(result.status)
    114 			renderTargets = WwiseTransfer::StringHelper::splitDoubleNullTerminatedString(result.buffer);
    115 		else
    116 		{
    117 			// For REAPER < 6.69
    118 			auto renderTargetsString = getProjectString(projectInfo.projectReference, "RENDER_TARGETS");
    119 
    120 			juce::StringArray renderTargetsStringArray;
    121 			renderTargetsStringArray.addTokens(renderTargetsString, ";", "");
    122 			renderTargetsStringArray.removeEmptyStrings();
    123 
    124 			renderTargets = std::vector<juce::String>(renderTargetsStringArray.strings.begin(), renderTargetsStringArray.strings.end());
    125 		}
    126 
    127 		return renderTargets;
    128 	}
    129 
    130 	juce::String ReaperContext::getProjectString(ReaProject* proj, const char* key) const
    131 	{
    132 		auto result = getProjectStringBuffer(proj, key);
    133 
    134 		if(result.buffer.size() > 0)
    135 			return WwiseTransfer::StringHelper::utf8EncodedCharArrayToString(result.buffer);
    136 
    137 		return {};
    138 	}
    139 
    140 	ReaperContext::ProjectStringBufferResult ReaperContext::getProjectStringBuffer(ReaProject* proj, const char* key) const
    141 	{
    142 		ProjectStringBufferResult result;
    143 
    144 		if(reaperPlugin.supportsReallocCommands())
    145 		{
    146 			// For REAPER 6.68+
    147 			char buffer[ReaperContextConstants::defaultBufferSize];
    148 			char* bufferPtr = buffer;
    149 
    150 			int bufferSize = (int)sizeof(buffer);
    151 
    152 			int token = reaperPlugin.reallocCmdRegisterBuf(&bufferPtr, &bufferSize);
    153 
    154 			result.status = reaperPlugin.getSetProjectInfo_String(proj, key, bufferPtr, false);
    155 
    156 			if(result.status)
    157 				result.buffer.assign(bufferPtr, bufferPtr + bufferSize);
    158 
    159 			reaperPlugin.reallocCmdClear(token);
    160 		}
    161 		else
    162 		{
    163 			static std::vector<char> buffer(ReaperContextConstants::largeBufferSize);
    164 			std::fill(buffer.begin(), buffer.end(), '\0');
    165 
    166 			result.status = reaperPlugin.getSetProjectInfo_String(proj, key, &buffer[0], false);
    167 
    168 			if(result.status)
    169 				result.buffer = buffer;
    170 		}
    171 
    172 		return result;
    173 	}
    174 
    175 	std::vector<WwiseTransfer::Import::Item> ReaperContext::getItemsForImport(const WwiseTransfer::Import::Options& options)
    176 	{
    177 		juce::ScopedLock lock{apiAccess};
    178 
    179 		std::vector<WwiseTransfer::Import::Item> importItems;
    180 
    181 		auto importItemsForPreview = getItemsForPreview(options);
    182 		if(importItemsForPreview.size() == 0)
    183 			return importItems;
    184 
    185 		auto projectInfo = getProjectInfo();
    186 
    187 		juce::String renderStats = getProjectString(projectInfo.projectReference, "RENDER_STATS");
    188 
    189 		static juce::String fileToken("FILE:");
    190 		static juce::String delimiter(';' + fileToken);
    191 
    192 		if(renderStats.isNotEmpty())
    193 		{
    194 			// To ease parsing, append ";FILE:" to the end of renderStats
    195 			if(renderStats.endsWithChar(';'))
    196 				renderStats << fileToken;
    197 			else
    198 				renderStats << delimiter;
    199 
    200 			int endPosition, startPosition = renderStats.indexOf(fileToken) + fileToken.length();
    201 
    202 			if(startPosition != -1) // If we don't find the first "FILE:", exit since we are receiving something unexpected
    203 			{
    204 				static std::regex regex("(.+?);[A-Z]+");
    205 
    206 				while((endPosition = renderStats.indexOf(startPosition, delimiter)) != -1)
    207 				{
    208 					auto finalRenderPath = renderStats.substring(startPosition, endPosition).toStdString();
    209 
    210 					std::smatch results;
    211 					if(std::regex_search(finalRenderPath, results, regex))
    212 						finalRenderPath = results[1];
    213 
    214 					const auto& importItemForPreview = importItemsForPreview[importItems.size()];
    215 
    216 					importItems.push_back({
    217 						importItemForPreview.path,
    218 						importItemForPreview.originalsSubFolder,
    219 						importItemForPreview.audioFilePath,
    220 						finalRenderPath,
    221 					});
    222 
    223 					startPosition = endPosition + delimiter.length();
    224 				}
    225 			}
    226 		}
    227 
    228 		return importItems;
    229 	}
    230 
    231 	std::vector<juce::String> ReaperContext::getOriginalSubfolders(const ProjectInfo& projectInfo, const juce::String& originalsSubfolder)
    232 	{
    233 		// The originals subfolder is a combination of what the user inputs in the originals subfolder input field
    234 		// Combined with anything in the render file path after the render folder
    235 
    236 		// To get the resolved file paths relative to the render directory, we can simply subtract the parent paths in resolvedDummyRenderPattern from
    237 		// the file paths in resolvedRenderPattern. Other approaches require us to know the render directory which is difficult to figure out and
    238 		// requires alot of logic on our end.
    239 
    240 		const auto dummyRenderPattern = juce::File::getSeparatorString();
    241 		const auto resolvedDummyRenderPattern = getItemListFromRenderPattern(projectInfo.projectReference, dummyRenderPattern, true);
    242 
    243 		const auto renderPattern = getRenderPattern(projectInfo);
    244 		const auto resolvedRenderPattern = getItemListFromRenderPattern(projectInfo.projectReference, renderPattern, true);
    245 
    246 		const auto originalsSubfolderRenderPattern = originalsSubfolder + juce::File::getSeparatorString();
    247 		const auto resolvedOriginalsSubfolder = getItemListFromRenderPattern(projectInfo.projectReference, originalsSubfolderRenderPattern, true);
    248 
    249 		if(resolvedDummyRenderPattern.size() != resolvedRenderPattern.size() && resolvedDummyRenderPattern.size() != resolvedOriginalsSubfolder.size())
    250 		{
    251 			juce::Logger::writeToLog("Reaper: Mismatch between resolvedDummyRenderPattern, resolvedRenderPattern and resolvedOriginalsSubfolder");
    252 			return {};
    253 		}
    254 
    255 		std::vector<juce::String> finalOriginalsSubfolders;
    256 		for(int i = 0; i < resolvedDummyRenderPattern.size(); ++i)
    257 		{
    258 			auto renderDirectory = juce::File(resolvedDummyRenderPattern[i]).getParentDirectory();
    259 			auto relativeResolvedRenderPattern = juce::File(resolvedRenderPattern[i]).getRelativePathFrom(renderDirectory);
    260 			auto originalsSubfolderFile = juce::File(resolvedOriginalsSubfolder[i]).getParentDirectory().getChildFile(relativeResolvedRenderPattern);
    261 
    262 			juce::String originalsSubfolder = "";
    263 			if(originalsSubfolderFile.getParentDirectory() != renderDirectory)
    264 				originalsSubfolder = originalsSubfolderFile.getRelativePathFrom(renderDirectory).upToLastOccurrenceOf(juce::File::getSeparatorString(), false, true);
    265 
    266 			finalOriginalsSubfolders.push_back(originalsSubfolder);
    267 		}
    268 
    269 		return finalOriginalsSubfolders;
    270 	}
    271 
    272 	std::vector<WwiseTransfer::Import::PreviewItem> ReaperContext::getItemsForPreview(const WwiseTransfer::Import::Options& options)
    273 	{
    274 		juce::ScopedLock lock{apiAccess};
    275 
    276 		auto projectInfo = getProjectInfo();
    277 
    278 		auto renderTargets = getRenderTargets();
    279 		auto resolvedOriginalsSubfolder = getOriginalSubfolders(projectInfo, options.originalsSubfolder);
    280 
    281 		const auto objectPathsPattern = options.importDestination + options.hierarchyMappingPath;
    282 		std::vector<juce::String> resolvedObjectPaths = getItemListFromRenderPattern(projectInfo.projectReference, objectPathsPattern, false);
    283 
    284 		if(renderTargets.size() != resolvedOriginalsSubfolder.size() || renderTargets.size() != resolvedObjectPaths.size())
    285 		{
    286 			juce::Logger::writeToLog("Reaper: Mismatch between renderTargets, resolvedObjectPaths and resolvedOriginalsSubfolder");
    287 			return {};
    288 		}
    289 
    290 		std::vector<WwiseTransfer::Import::PreviewItem> importItems;
    291 		for(int i = 0; i < resolvedObjectPaths.size(); ++i)
    292 		{
    293 			const auto& objectPath = resolvedObjectPaths[i].upToLastOccurrenceOf(".", false, false);
    294 
    295 			importItems.push_back({objectPath, resolvedOriginalsSubfolder[i], renderTargets[i]});
    296 		}
    297 
    298 		return importItems;
    299 	}
    300 
    301 	ReaperContext::ProjectInfo ReaperContext::getProjectInfo() const
    302 	{
    303 		std::string buffer(ReaperContextConstants::defaultBufferSize, '\0');
    304 
    305 		// The buffer sent to enumProjects will contain the project path.
    306 		auto projectReference = reaperPlugin.enumProjects(-1, &buffer[0], buffer.size());
    307 		if(!projectReference || buffer.empty())
    308 			return {};
    309 
    310 		const juce::File projectFile(buffer);
    311 
    312 		// REAPER requires that a project file ends with the .rpp (case insensitive) file extension.
    313 		if(projectFile.getFileExtension().compareIgnoreCase(".rpp") != 0)
    314 			return {};
    315 
    316 		return {
    317 			projectReference,
    318 			projectFile.getFileNameWithoutExtension(),
    319 			projectFile.getFullPathName()};
    320 	}
    321 
    322 	juce::String ReaperContext::getRenderPattern(const ReaperContext::ProjectInfo& projectInfo) const
    323 	{
    324 		// There are several scenarios where the render pattern could be empty
    325 		// 1. When the project hasn't been saved (reaper uses "untitled")
    326 		// 2. When the project has been saved (reaper uses the project name)
    327 		auto renderPattern = getProjectString(projectInfo.projectReference, "RENDER_PATTERN");
    328 		if(renderPattern.isNotEmpty())
    329 			return renderPattern;
    330 
    331 		if(projectInfo.projectPath.isEmpty())
    332 			return ReaperContextConstants::defaultRenderPattern;
    333 
    334 		return projectInfo.projectName;
    335 	}
    336 
    337 	bool ReaperContext::sessionChanged()
    338 	{
    339 		auto sessionChanged = false;
    340 
    341 		auto projectInfo = getProjectInfo();
    342 
    343 		auto projectStateCount = reaperPlugin.getProjectStateChangeCount(projectInfo.projectReference);
    344 		auto renderSource = reaperPlugin.getSetProjectInfo(projectInfo.projectReference, "RENDER_SETTINGS", 0, false);
    345 		auto renderBounds = reaperPlugin.getSetProjectInfo(projectInfo.projectReference, "RENDER_BOUNDSFLAG", 0, false);
    346 		auto renderFile = getProjectString(projectInfo.projectReference, "RENDER_FILE");
    347 		auto renderPattern = getProjectString(projectInfo.projectReference, "RENDER_PATTERN");
    348 
    349 		if(projectStateCount != stateInfo.projectStateCount ||
    350 			renderSource != stateInfo.renderSource ||
    351 			renderBounds != stateInfo.renderBounds ||
    352 			renderFile != stateInfo.renderFile ||
    353 			renderPattern != stateInfo.renderPattern)
    354 		{
    355 			sessionChanged = true;
    356 		}
    357 
    358 		stateInfo = {
    359 			projectStateCount,
    360 			renderSource,
    361 			renderBounds,
    362 			renderFile,
    363 			renderPattern};
    364 
    365 		return sessionChanged;
    366 	}
    367 
    368 	std::vector<juce::String> ReaperContext::getItemListFromRenderPattern(ReaProject* project, const juce::String& pattern, bool suppressIllegalPaths)
    369 	{
    370 		const int bufferLength = reaperPlugin.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), nullptr, 0);
    371 
    372 		if(bufferLength == 0)
    373 			return {};
    374 
    375 		std::vector<char> buffer(bufferLength, '\0');
    376 		const int newBufferLength = reaperPlugin.resolveRenderPattern(project, suppressIllegalPaths ? "" : nullptr, pattern.toUTF8(), &buffer[0], bufferLength);
    377 		if(newBufferLength > bufferLength)
    378 		{
    379 			// It is possible the resolved render pattern changes between the two calls to resolveRenderPattern.
    380 			// For example, a track that is set to render can be unmuted between the two calls which will result in a bigger buffer needed.
    381 			// In that case we just return nothing and the next call will be good.
    382 			juce::Logger::writeToLog("Reaper: Mismatch between calls to resolveRenderPattern");
    383 			return {};
    384 		}
    385 
    386 		return WwiseTransfer::StringHelper::splitDoubleNullTerminatedString(buffer);
    387 	}
    388 } // namespace AK::ReaWwise