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