|
| 1 | +/* |
| 2 | + * ReaKontrol |
| 3 | + * FX Map code |
| 4 | + * Author: James Teh <jamie@jantrid.net> |
| 5 | + * Copyright 2026 James Teh |
| 6 | + * License: GNU General Public License version 2.0 |
| 7 | + */ |
| 8 | + |
| 9 | +#ifdef _WIN32 |
| 10 | +#include <WDL/win32_utf8.h> |
| 11 | +#endif |
| 12 | + |
| 13 | +#include <filesystem> |
| 14 | +#include <fstream> |
| 15 | +#include <regex> |
| 16 | +#include <string> |
| 17 | +#include "fxMap.h" |
| 18 | +#include "reaKontrol.h" |
| 19 | + |
| 20 | +static const std::regex RE_LINE( |
| 21 | + // Ignore any space at the start of a line. |
| 22 | + "^\\s*" |
| 23 | + "(?:" |
| 24 | + // Group 1: the line may be the map name, ending with a colon. |
| 25 | + "([^#]+):" |
| 26 | + // Groups 2 and 3: or a parameter number, optionally followed by space and |
| 27 | + // a name. |
| 28 | + "|(\\d+)(?:\\s+([^#]+))?" |
| 29 | + // Group 4: or a page break indicator. |
| 30 | + "|(---)" |
| 31 | + // Group 5: or a section name in square brackets. |
| 32 | + "|\\[([^#]+)\\]" |
| 33 | + ")?" |
| 34 | + // This may be followed by optional space and an optional comment starting |
| 35 | + // with "#". |
| 36 | + "\\s*(?:#.*)?$" |
| 37 | +); |
| 38 | + |
| 39 | +// The generateMapFileForSelectedFx action can't access the FxMap instance, |
| 40 | +// so we cache the last selected FX here. |
| 41 | +static MediaTrack* lastTrack = nullptr; |
| 42 | +static int lastFx = -1; |
| 43 | + |
| 44 | +static std::filesystem::path getFxMapDir() { |
| 45 | + std::filesystem::path path(std::u8string_view((char8_t*)GetResourcePath())); |
| 46 | + path /= "reaKontrol"; |
| 47 | + path /= "fxMaps"; |
| 48 | + return path; |
| 49 | +} |
| 50 | + |
| 51 | +static std::filesystem::path getFxMapFileName(MediaTrack* track, int fx) { |
| 52 | + char name[100] = ""; |
| 53 | + TrackFX_GetFXName(track, fx, name, sizeof(name)); |
| 54 | + if (!name[0]) { |
| 55 | + // This will happen when there are no FX on this track. |
| 56 | + return ""; |
| 57 | + } |
| 58 | + std::filesystem::path path = getFxMapDir(); |
| 59 | + path /= ""; |
| 60 | + for (char* c = name; *c; ++c) { |
| 61 | + if (*c == '/' || *c == '\\' || *c == ':') { |
| 62 | + continue; |
| 63 | + } |
| 64 | + path += (char8_t)*c; |
| 65 | + } |
| 66 | + path += ".rkfm"; |
| 67 | + return path; |
| 68 | +} |
| 69 | + |
| 70 | +FxMap::FxMap(MediaTrack* track, int fx) : _track(track), _fx(fx) { |
| 71 | + lastTrack = track; |
| 72 | + lastFx = fx; |
| 73 | + const std::filesystem::path path = getFxMapFileName(track, fx); |
| 74 | + if (path.empty()) { |
| 75 | + return; |
| 76 | + } |
| 77 | + std::ifstream input(path); |
| 78 | + if (!input) { |
| 79 | + log("no FX map " << path); |
| 80 | + return; |
| 81 | + } |
| 82 | + log("loading FX map " << path); |
| 83 | + std::string line; |
| 84 | + while (std::getline(input, line)) { |
| 85 | + std::smatch m; |
| 86 | + std::regex_search(line, m, RE_LINE); |
| 87 | + if (m.empty()) { |
| 88 | + log("invalid FX map line: " << line); |
| 89 | + continue; |
| 90 | + } |
| 91 | + if (!m.str(1).empty()) { |
| 92 | + if (!this->_mapName.empty()) { |
| 93 | + log("map name specified more than once, ignoring: " << line); |
| 94 | + continue; |
| 95 | + } |
| 96 | + this->_mapName = m.str(1); |
| 97 | + log("map name: " << this->_mapName); |
| 98 | + continue; |
| 99 | + } |
| 100 | + const std::string numStr = m.str(2); |
| 101 | + if (!numStr.empty()) { |
| 102 | + const int rp = std::atoi(numStr.c_str()); |
| 103 | + const int mp = this->_reaperParams.size(); |
| 104 | + this->_reaperParams.push_back(rp); |
| 105 | + this->_mapParams.insert({rp, mp}); |
| 106 | + const std::string paramName = m.str(3); |
| 107 | + if (!paramName.empty()) { |
| 108 | + this->_paramNames.insert({mp, paramName}); |
| 109 | + } |
| 110 | + continue; |
| 111 | + } |
| 112 | + if (!m.str(4).empty()) { |
| 113 | + // A page break has been requested. Any remaining slots on this page |
| 114 | + // should be empty. |
| 115 | + while (this->_reaperParams.size() % BANK_NUM_SLOTS != 0) { |
| 116 | + this->_reaperParams.push_back(-1); |
| 117 | + } |
| 118 | + continue; |
| 119 | + } |
| 120 | + const std::string section = m.str(5); |
| 121 | + if (!section.empty()) { |
| 122 | + this->_sections.insert({this->_reaperParams.size(), section}); |
| 123 | + } |
| 124 | + } |
| 125 | + log("loaded " << this->_mapParams.size() << " params from FX map"); |
| 126 | +} |
| 127 | + |
| 128 | +std::string FxMap::getMapName() const { |
| 129 | + if (this->_mapName.empty()) { |
| 130 | + char name[100]; |
| 131 | + TrackFX_GetFXName(this->_track, this->_fx, name, sizeof(name)); |
| 132 | + return name; |
| 133 | + } |
| 134 | + return this->_mapName; |
| 135 | +} |
| 136 | + |
| 137 | +int FxMap::getParamCount() const { |
| 138 | + if (this->_reaperParams.empty()) { |
| 139 | + return TrackFX_GetNumParams(this->_track, this->_fx); |
| 140 | + } |
| 141 | + return this->_reaperParams.size(); |
| 142 | +} |
| 143 | + |
| 144 | +int FxMap::getReaperParam(int mapParam) const { |
| 145 | + if (this->_reaperParams.empty()) { |
| 146 | + return mapParam; |
| 147 | + } |
| 148 | + return this->_reaperParams[mapParam]; |
| 149 | +} |
| 150 | + |
| 151 | +int FxMap::getMapParam(int reaperParam) const { |
| 152 | + if (this->_reaperParams.empty()) { |
| 153 | + return reaperParam; |
| 154 | + } |
| 155 | + auto it = this->_mapParams.find(reaperParam); |
| 156 | + if (it == this->_mapParams.end()) { |
| 157 | + return -1; |
| 158 | + } |
| 159 | + return it->second; |
| 160 | +} |
| 161 | + |
| 162 | +std::string FxMap::getParamName(int mapParam) const { |
| 163 | + auto it = this->_paramNames.find(mapParam); |
| 164 | + if (it == this->_paramNames.end()) { |
| 165 | + char name[100]; |
| 166 | + const int rp = this->getReaperParam(mapParam); |
| 167 | + TrackFX_GetParamName(this->_track, this->_fx, rp, name, sizeof(name)); |
| 168 | + return name; |
| 169 | + } |
| 170 | + return it->second; |
| 171 | +} |
| 172 | + |
| 173 | +std::string FxMap::getSection(int mapParam) const { |
| 174 | + auto it = this->_sections.find(mapParam); |
| 175 | + if (it == this->_sections.end()) { |
| 176 | + return ""; |
| 177 | + } |
| 178 | + return it->second; |
| 179 | +} |
| 180 | + |
| 181 | +std::string FxMap::getSectionsForPage(int mapParam) const { |
| 182 | + std::ostringstream s; |
| 183 | + int bankEnd = mapParam + BANK_NUM_SLOTS; |
| 184 | + if (bankEnd > this->_reaperParams.size()) { |
| 185 | + bankEnd = this->_reaperParams.size(); |
| 186 | + } |
| 187 | + for (; mapParam < bankEnd; ++mapParam) { |
| 188 | + std::string section = this->getSection(mapParam); |
| 189 | + if (!section.empty()) { |
| 190 | + if (s.tellp() > 0) { |
| 191 | + s << ", "; |
| 192 | + } |
| 193 | + s << section; |
| 194 | + } |
| 195 | + } |
| 196 | + return s.str(); |
| 197 | +} |
| 198 | + |
| 199 | +std::string FxMap::getMapNameFor(MediaTrack* track, int fx) { |
| 200 | + auto getOrigName = [&]() -> std::string { |
| 201 | + char name[100] = ""; |
| 202 | + TrackFX_GetFXName(track, fx, name, sizeof(name)); |
| 203 | + return name; |
| 204 | + }; |
| 205 | + const std::filesystem::path path = getFxMapFileName(track, fx); |
| 206 | + if (path.empty()) { |
| 207 | + return getOrigName(); |
| 208 | + } |
| 209 | + std::ifstream input(path); |
| 210 | + if (!input) { |
| 211 | + return getOrigName(); |
| 212 | + } |
| 213 | + std::string line; |
| 214 | + while (std::getline(input, line)) { |
| 215 | + std::smatch m; |
| 216 | + std::regex_search(line, m, RE_LINE); |
| 217 | + if (m.empty()) { |
| 218 | + continue; |
| 219 | + } |
| 220 | + if (!m.str(1).empty()) { |
| 221 | + return m.str(1); |
| 222 | + } |
| 223 | + if (!m.str(2).empty() || !m.str(4).empty() || !m.str(5).empty()) { |
| 224 | + // The map name must be the first non-comment, non-blank line. If we hit |
| 225 | + // anything else, there's no map name, so don't process any further. |
| 226 | + break; |
| 227 | + } |
| 228 | + } |
| 229 | + return getOrigName(); |
| 230 | +} |
| 231 | + |
| 232 | +void FxMap::generateMapFileForSelectedFx() { |
| 233 | + const std::filesystem::path fn = getFxMapFileName(lastTrack, lastFx); |
| 234 | + if (fn.empty()) { |
| 235 | + // No selected FX. |
| 236 | + return; |
| 237 | + } |
| 238 | + const std::filesystem::path dir = getFxMapDir(); |
| 239 | + std::filesystem::create_directories(dir); |
| 240 | + std::ofstream output(fn); |
| 241 | + if (!output) { |
| 242 | + return; |
| 243 | + } |
| 244 | + const int count = TrackFX_GetNumParams(lastTrack, lastFx); |
| 245 | + for (int p = 0; p < count; ++p) { |
| 246 | + char name[100]; |
| 247 | + TrackFX_GetParamName(lastTrack, lastFx, p, name, sizeof(name)); |
| 248 | + output << p << " # " << name << std::endl; |
| 249 | + } |
| 250 | + output.close(); |
| 251 | + // Locate the file in Explorer/Finder. |
| 252 | + std::u8string params(u8"/select,"); |
| 253 | + params += fn.u8string(); |
| 254 | + ShellExecute(nullptr, "open", "explorer.exe", (const char*)params.c_str(), |
| 255 | + nullptr, SW_SHOW); |
| 256 | +} |
0 commit comments