Skip to content

Commit acd98be

Browse files
authored
Configurable mapping of non-NKS FX parameters. (#65)
This enables you to specify how parameters are mapped to Kontrol knobs for specific plugins using map files. See the "Configurable Mapping of Non-NKS FX Parameters" section of the website or readme for details.
1 parent 44db5f4 commit acd98be

File tree

7 files changed

+382
-31
lines changed

7 files changed

+382
-31
lines changed

readme.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,31 @@ On A and M series keyboards, pulling the 4-d encoder right and left moves throug
5757
Use 4-d encoder down and up to switch plugins.
5858
To return to normal operation, press the 4-d encoder again.
5959

60+
## Configurable Mapping of Non-NKS FX Parameters
61+
By default, all FX parameters are mapped to pages and knobs on the Kontrol keyboard.
62+
For plugins with many parameters, this might not be efficient.
63+
ReaKontrol enables you to specify how parameters are mapped to Kontrol knobs for specific plugins using map files.
64+
ReaKontrol will load these maps from `reaKontrol/fxMaps/plugin name.rkfm` inside the REAPER resource folder.
65+
Slash, backslash and colon are removed from the plugin name.
66+
For example, on Windows, you might have `%appdata%\REAPER\reaKontrol\fxMaps\VST ReaComp (Cockos).rkfm`.
67+
The file format is as follows:
68+
69+
```
70+
# This is a comment.
71+
ReaComp: # This is optional and overrides the FX name reported to users.
72+
0 # Parameter 0 will be mapped to the first knob on the first page.
73+
2 # Parameter 2 will be mapped to the second knob.
74+
4 Pre comp # Overrides the name reported to the user for this third knob.
75+
--- # Page break. The remaining knobs on this page will be unassigned.
76+
[mix] # This is a section name for the following knobs.
77+
6 # Parameter 6 will be mapped to the first knob on the second page.
78+
```
79+
80+
ReaKontrol can help you create these maps.
81+
First, select the FX you want to work with using your Kontrol keyboard.
82+
Note that the first FX (if any) will be automatically selected when you navigate to a track.
83+
Then, run the "ReaKontrol: Generate map file for selected FX" action and it will generate the appropriate map file with all the parameter numbers and their names as comments.
84+
6085
## Reconnecting
6186
ReaKontrol will connect to a Kontrol keyboard when REAPER starts.
6287
If the keyboard isn't turned on or connected when REAPER starts, ReaKontrol is not currently able to detect when the keyboard is connected.

src/fxMap.cpp

Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
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+
}

src/fxMap.h

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* ReaKontrol
3+
* FX Map header
4+
* Author: James Teh <jamie@jantrid.net>
5+
* Copyright 2026 James Teh
6+
* License: GNU General Public License version 2.0
7+
*/
8+
9+
#pragma once
10+
11+
#include <map>
12+
#include <string>
13+
#include <vector>
14+
15+
class MediaTrack;
16+
17+
class FxMap {
18+
public:
19+
FxMap() = default;
20+
FxMap(MediaTrack* track, int fx);
21+
std::string getMapName() const;
22+
int getParamCount() const;
23+
int getReaperParam(int mapParam) const;
24+
int getMapParam(int reaperParam) const;
25+
std::string getParamName(int mapParam) const;
26+
std::string getSection(int mapParam) const;
27+
std::string getSectionsForPage(int mapParam) const;
28+
29+
static std::string getMapNameFor(MediaTrack* track, int fx);
30+
static void generateMapFileForSelectedFx();
31+
32+
private:
33+
std::string _mapName;
34+
MediaTrack* _track = nullptr;
35+
int _fx = -1;
36+
std::vector<int> _reaperParams;
37+
std::map<int, std::string> _paramNames;
38+
std::map<int, int> _mapParams;
39+
std::map<int, std::string> _sections;
40+
};

src/main.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "swell.h"
1919
#endif
2020
#define REAPERAPI_IMPLEMENT
21+
#include "fxMap.h"
2122
#include "reaKontrol.h"
2223

2324
using namespace std;
@@ -204,6 +205,7 @@ void disconnect() {
204205
}
205206

206207
int CMD_RECONNECT = 0;
208+
int CMD_GENERATE_FX_MAP = 0;
207209

208210
bool handleCommand(KbdSectionInfo* section, int command, int val, int valHw,
209211
int relMode, HWND hwnd
@@ -213,6 +215,10 @@ bool handleCommand(KbdSectionInfo* section, int command, int val, int valHw,
213215
connect();
214216
return true;
215217
}
218+
if (command == CMD_GENERATE_FX_MAP) {
219+
FxMap::generateMapFileForSelectedFx();
220+
return true;
221+
}
216222
return false;
217223
}
218224

@@ -235,6 +241,9 @@ REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(REAPER_PLUGIN_HINSTANCE hI
235241
custom_action_register_t action = {MAIN_SECTION, "REAKONTROL_RECONNECT",
236242
"ReaKontrol: Reconnect"};
237243
CMD_RECONNECT = rec->Register("custom_action", &action);
244+
action = {MAIN_SECTION, "REAKONTROL_GENFXMAP",
245+
"ReaKontrol: Generate map file for selected FX"};
246+
CMD_GENERATE_FX_MAP = rec->Register("custom_action", &action);
238247
rec->Register("hookcommand2", (void*)handleCommand);
239248
rec->Register("timer", (void*)delayedInit);
240249
return 1;

0 commit comments

Comments
 (0)