Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,31 @@ On A and M series keyboards, pulling the 4-d encoder right and left moves throug
Use 4-d encoder down and up to switch plugins.
To return to normal operation, press the 4-d encoder again.

## Configurable Mapping of Non-NKS FX Parameters
By default, all FX parameters are mapped to pages and knobs on the Kontrol keyboard.
For plugins with many parameters, this might not be efficient.
ReaKontrol enables you to specify how parameters are mapped to Kontrol knobs for specific plugins using map files.
ReaKontrol will load these maps from `reaKontrol/fxMaps/plugin name.rkfm` inside the REAPER resource folder.
Slash, backslash and colon are removed from the plugin name.
For example, on Windows, you might have `%appdata%\REAPER\reaKontrol\fxMaps\VST ReaComp (Cockos).rkfm`.
The file format is as follows:

```
# This is a comment.
ReaComp: # This is optional and overrides the FX name reported to users.
0 # Parameter 0 will be mapped to the first knob on the first page.
2 # Parameter 2 will be mapped to the second knob.
4 Pre comp # Overrides the name reported to the user for this third knob.
--- # Page break. The remaining knobs on this page will be unassigned.
[mix] # This is a section name for the following knobs.
6 # Parameter 6 will be mapped to the first knob on the second page.
```

ReaKontrol can help you create these maps.
First, select the FX you want to work with using your Kontrol keyboard.
Note that the first FX (if any) will be automatically selected when you navigate to a track.
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.

## Reconnecting
ReaKontrol will connect to a Kontrol keyboard when REAPER starts.
If the keyboard isn't turned on or connected when REAPER starts, ReaKontrol is not currently able to detect when the keyboard is connected.
Expand Down
256 changes: 256 additions & 0 deletions src/fxMap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
/*
* ReaKontrol
* FX Map code
* Author: James Teh <jamie@jantrid.net>
* Copyright 2026 James Teh
* License: GNU General Public License version 2.0
*/

#ifdef _WIN32
#include <WDL/win32_utf8.h>
#endif

#include <filesystem>
#include <fstream>
#include <regex>
#include <string>
#include "fxMap.h"
#include "reaKontrol.h"

static const std::regex RE_LINE(
// Ignore any space at the start of a line.
"^\\s*"
"(?:"
// Group 1: the line may be the map name, ending with a colon.
"([^#]+):"
// Groups 2 and 3: or a parameter number, optionally followed by space and
// a name.
"|(\\d+)(?:\\s+([^#]+))?"
// Group 4: or a page break indicator.
"|(---)"
// Group 5: or a section name in square brackets.
"|\\[([^#]+)\\]"
")?"
// This may be followed by optional space and an optional comment starting
// with "#".
"\\s*(?:#.*)?$"
);

// The generateMapFileForSelectedFx action can't access the FxMap instance,
// so we cache the last selected FX here.
static MediaTrack* lastTrack = nullptr;
static int lastFx = -1;

static std::filesystem::path getFxMapDir() {
std::filesystem::path path(std::u8string_view((char8_t*)GetResourcePath()));
path /= "reaKontrol";
path /= "fxMaps";
return path;
}

static std::filesystem::path getFxMapFileName(MediaTrack* track, int fx) {
char name[100] = "";
TrackFX_GetFXName(track, fx, name, sizeof(name));
if (!name[0]) {
// This will happen when there are no FX on this track.
return "";
}
std::filesystem::path path = getFxMapDir();
path /= "";
for (char* c = name; *c; ++c) {
if (*c == '/' || *c == '\\' || *c == ':') {
continue;
}
path += (char8_t)*c;
}
path += ".rkfm";
return path;
}

FxMap::FxMap(MediaTrack* track, int fx) : _track(track), _fx(fx) {
lastTrack = track;
lastFx = fx;
const std::filesystem::path path = getFxMapFileName(track, fx);
if (path.empty()) {
return;
}
std::ifstream input(path);
if (!input) {
log("no FX map " << path);
return;
}
log("loading FX map " << path);
std::string line;
while (std::getline(input, line)) {
std::smatch m;
std::regex_search(line, m, RE_LINE);
if (m.empty()) {
log("invalid FX map line: " << line);
continue;
}
if (!m.str(1).empty()) {
if (!this->_mapName.empty()) {
log("map name specified more than once, ignoring: " << line);
continue;
}
this->_mapName = m.str(1);
log("map name: " << this->_mapName);
continue;
}
const std::string numStr = m.str(2);
if (!numStr.empty()) {
const int rp = std::atoi(numStr.c_str());
const int mp = this->_reaperParams.size();
this->_reaperParams.push_back(rp);
this->_mapParams.insert({rp, mp});
const std::string paramName = m.str(3);
if (!paramName.empty()) {
this->_paramNames.insert({mp, paramName});
}
continue;
}
if (!m.str(4).empty()) {
// A page break has been requested. Any remaining slots on this page
// should be empty.
while (this->_reaperParams.size() % BANK_NUM_SLOTS != 0) {
this->_reaperParams.push_back(-1);
}
continue;
}
const std::string section = m.str(5);
if (!section.empty()) {
this->_sections.insert({this->_reaperParams.size(), section});
}
}
log("loaded " << this->_mapParams.size() << " params from FX map");
}

std::string FxMap::getMapName() const {
if (this->_mapName.empty()) {
char name[100];
TrackFX_GetFXName(this->_track, this->_fx, name, sizeof(name));
return name;
}
return this->_mapName;
}

int FxMap::getParamCount() const {
if (this->_reaperParams.empty()) {
return TrackFX_GetNumParams(this->_track, this->_fx);
}
return this->_reaperParams.size();
}

int FxMap::getReaperParam(int mapParam) const {
if (this->_reaperParams.empty()) {
return mapParam;
}
return this->_reaperParams[mapParam];
}

int FxMap::getMapParam(int reaperParam) const {
if (this->_reaperParams.empty()) {
return reaperParam;
}
auto it = this->_mapParams.find(reaperParam);
if (it == this->_mapParams.end()) {
return -1;
}
return it->second;
}

std::string FxMap::getParamName(int mapParam) const {
auto it = this->_paramNames.find(mapParam);
if (it == this->_paramNames.end()) {
char name[100];
const int rp = this->getReaperParam(mapParam);
TrackFX_GetParamName(this->_track, this->_fx, rp, name, sizeof(name));
return name;
}
return it->second;
}

std::string FxMap::getSection(int mapParam) const {
auto it = this->_sections.find(mapParam);
if (it == this->_sections.end()) {
return "";
}
return it->second;
}

std::string FxMap::getSectionsForPage(int mapParam) const {
std::ostringstream s;
int bankEnd = mapParam + BANK_NUM_SLOTS;
if (bankEnd > this->_reaperParams.size()) {
bankEnd = this->_reaperParams.size();
}
for (; mapParam < bankEnd; ++mapParam) {
std::string section = this->getSection(mapParam);
if (!section.empty()) {
if (s.tellp() > 0) {
s << ", ";
}
s << section;
}
}
return s.str();
}

std::string FxMap::getMapNameFor(MediaTrack* track, int fx) {
auto getOrigName = [&]() -> std::string {
char name[100] = "";
TrackFX_GetFXName(track, fx, name, sizeof(name));
return name;
};
const std::filesystem::path path = getFxMapFileName(track, fx);
if (path.empty()) {
return getOrigName();
}
std::ifstream input(path);
if (!input) {
return getOrigName();
}
std::string line;
while (std::getline(input, line)) {
std::smatch m;
std::regex_search(line, m, RE_LINE);
if (m.empty()) {
continue;
}
if (!m.str(1).empty()) {
return m.str(1);
}
if (!m.str(2).empty() || !m.str(4).empty() || !m.str(5).empty()) {
// The map name must be the first non-comment, non-blank line. If we hit
// anything else, there's no map name, so don't process any further.
break;
}
}
return getOrigName();
}

void FxMap::generateMapFileForSelectedFx() {
const std::filesystem::path fn = getFxMapFileName(lastTrack, lastFx);
if (fn.empty()) {
// No selected FX.
return;
}
const std::filesystem::path dir = getFxMapDir();
std::filesystem::create_directories(dir);
std::ofstream output(fn);
if (!output) {
return;
}
const int count = TrackFX_GetNumParams(lastTrack, lastFx);
for (int p = 0; p < count; ++p) {
char name[100];
TrackFX_GetParamName(lastTrack, lastFx, p, name, sizeof(name));
output << p << " # " << name << std::endl;
}
output.close();
// Locate the file in Explorer/Finder.
std::u8string params(u8"/select,");
params += fn.u8string();
ShellExecute(nullptr, "open", "explorer.exe", (const char*)params.c_str(),
nullptr, SW_SHOW);
}
40 changes: 40 additions & 0 deletions src/fxMap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* ReaKontrol
* FX Map header
* Author: James Teh <jamie@jantrid.net>
* Copyright 2026 James Teh
* License: GNU General Public License version 2.0
*/

#pragma once

#include <map>
#include <string>
#include <vector>

class MediaTrack;

class FxMap {
public:
FxMap() = default;
FxMap(MediaTrack* track, int fx);
std::string getMapName() const;
int getParamCount() const;
int getReaperParam(int mapParam) const;
int getMapParam(int reaperParam) const;
std::string getParamName(int mapParam) const;
std::string getSection(int mapParam) const;
std::string getSectionsForPage(int mapParam) const;

static std::string getMapNameFor(MediaTrack* track, int fx);
static void generateMapFileForSelectedFx();

private:
std::string _mapName;
MediaTrack* _track = nullptr;
int _fx = -1;
std::vector<int> _reaperParams;
std::map<int, std::string> _paramNames;
std::map<int, int> _mapParams;
std::map<int, std::string> _sections;
};
9 changes: 9 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "swell.h"
#endif
#define REAPERAPI_IMPLEMENT
#include "fxMap.h"
#include "reaKontrol.h"

using namespace std;
Expand Down Expand Up @@ -204,6 +205,7 @@ void disconnect() {
}

int CMD_RECONNECT = 0;
int CMD_GENERATE_FX_MAP = 0;

bool handleCommand(KbdSectionInfo* section, int command, int val, int valHw,
int relMode, HWND hwnd
Expand All @@ -213,6 +215,10 @@ bool handleCommand(KbdSectionInfo* section, int command, int val, int valHw,
connect();
return true;
}
if (command == CMD_GENERATE_FX_MAP) {
FxMap::generateMapFileForSelectedFx();
return true;
}
return false;
}

Expand All @@ -235,6 +241,9 @@ REAPER_PLUGIN_DLL_EXPORT int REAPER_PLUGIN_ENTRYPOINT(REAPER_PLUGIN_HINSTANCE hI
custom_action_register_t action = {MAIN_SECTION, "REAKONTROL_RECONNECT",
"ReaKontrol: Reconnect"};
CMD_RECONNECT = rec->Register("custom_action", &action);
action = {MAIN_SECTION, "REAKONTROL_GENFXMAP",
"ReaKontrol: Generate map file for selected FX"};
CMD_GENERATE_FX_MAP = rec->Register("custom_action", &action);
rec->Register("hookcommand2", (void*)handleCommand);
rec->Register("timer", (void*)delayedInit);
return 1;
Expand Down
Loading