Import base implementation of MIDI-Map
Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
parent
cc098aae4c
commit
570d8bc2da
2 changed files with 337 additions and 2 deletions
|
|
@ -219,6 +219,7 @@ struct HostMIDICC : Module {
|
||||||
CardinalPluginContext* const pcontext;
|
CardinalPluginContext* const pcontext;
|
||||||
uint8_t channel = 0;
|
uint8_t channel = 0;
|
||||||
|
|
||||||
|
// from Rack
|
||||||
dsp::Timer rateLimiterTimer;
|
dsp::Timer rateLimiterTimer;
|
||||||
int lastValues[128];
|
int lastValues[128];
|
||||||
int64_t frame = 0;
|
int64_t frame = 0;
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This file contains a substantial amount of code from VCVRack's core/....cpp
|
* This file contains a substantial amount of code from VCVRack's core/MIDIMap.cpp
|
||||||
* Copyright (C) 2016-2021 VCV.
|
* Copyright (C) 2016-2021 VCV.
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or
|
* This program is free software: you can redistribute it and/or
|
||||||
|
|
@ -33,6 +33,8 @@
|
||||||
|
|
||||||
USE_NAMESPACE_DISTRHO;
|
USE_NAMESPACE_DISTRHO;
|
||||||
|
|
||||||
|
static const int MAX_CHANNELS = 128;
|
||||||
|
|
||||||
struct HostMIDIMap : Module {
|
struct HostMIDIMap : Module {
|
||||||
enum ParamIds {
|
enum ParamIds {
|
||||||
NUM_PARAMS
|
NUM_PARAMS
|
||||||
|
|
@ -47,7 +49,36 @@ struct HostMIDIMap : Module {
|
||||||
NUM_LIGHTS
|
NUM_LIGHTS
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cardinal specific
|
||||||
CardinalPluginContext* const pcontext;
|
CardinalPluginContext* const pcontext;
|
||||||
|
const MidiEvent* midiEvents;
|
||||||
|
uint32_t midiEventsLeft;
|
||||||
|
uint32_t midiEventFrame;
|
||||||
|
int64_t lastBlockFrame;
|
||||||
|
uint8_t channel;
|
||||||
|
|
||||||
|
// from Rack
|
||||||
|
bool smooth;
|
||||||
|
/** Number of maps */
|
||||||
|
int mapLen = 0;
|
||||||
|
/** The mapped CC number of each channel */
|
||||||
|
int ccs[MAX_CHANNELS];
|
||||||
|
/** The mapped param handle of each channel */
|
||||||
|
ParamHandle paramHandles[MAX_CHANNELS];
|
||||||
|
|
||||||
|
/** Channel ID of the learning session */
|
||||||
|
int learningId;
|
||||||
|
/** Whether the CC has been set during the learning session */
|
||||||
|
bool learnedCc;
|
||||||
|
/** Whether the param has been set during the learning session */
|
||||||
|
bool learnedParam;
|
||||||
|
|
||||||
|
/** The value of each CC number */
|
||||||
|
int8_t values[128];
|
||||||
|
/** The smoothing processor (normalized between 0 and 1) of each channel */
|
||||||
|
dsp::ExponentialFilter valueFilters[MAX_CHANNELS];
|
||||||
|
bool filterInitialized[MAX_CHANNELS] = {};
|
||||||
|
dsp::ClockDivider divider;
|
||||||
|
|
||||||
HostMIDIMap()
|
HostMIDIMap()
|
||||||
: pcontext(static_cast<CardinalPluginContext*>(APP))
|
: pcontext(static_cast<CardinalPluginContext*>(APP))
|
||||||
|
|
@ -56,14 +87,244 @@ struct HostMIDIMap : Module {
|
||||||
throw rack::Exception("Plugin context is null");
|
throw rack::Exception("Plugin context is null");
|
||||||
|
|
||||||
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
|
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
|
||||||
|
|
||||||
|
for (int id = 0; id < MAX_CHANNELS; ++id)
|
||||||
|
{
|
||||||
|
paramHandles[id].color = nvgRGB(0xff, 0xff, 0x40);
|
||||||
|
pcontext->engine->addParamHandle(¶mHandles[id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < MAX_CHANNELS; i++)
|
||||||
|
valueFilters[i].setTau(1 / 30.f);
|
||||||
|
|
||||||
|
divider.setDivision(32);
|
||||||
|
onReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
~HostMIDIMap()
|
||||||
|
{
|
||||||
|
if (pcontext == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (int id = 0; id < MAX_CHANNELS; ++id)
|
||||||
|
pcontext->engine->removeParamHandle(¶mHandles[id]);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onReset() override
|
void onReset() override
|
||||||
{
|
{
|
||||||
|
midiEvents = nullptr;
|
||||||
|
midiEventsLeft = 0;
|
||||||
|
midiEventFrame = 0;
|
||||||
|
lastBlockFrame = -1;
|
||||||
|
channel = 0;
|
||||||
|
|
||||||
|
smooth = true;
|
||||||
|
learningId = -1;
|
||||||
|
learnedCc = false;
|
||||||
|
learnedParam = false;
|
||||||
|
// Use NoLock because we're already in an Engine write-lock if Engine::resetModule().
|
||||||
|
// We also might be in the MIDIMap() constructor, which could cause problems, but when constructing, all ParamHandles will point to no Modules anyway.
|
||||||
|
clearMaps_NoLock();
|
||||||
|
mapLen = 1;
|
||||||
|
for (int i = 0; i < 128; i++) {
|
||||||
|
values[i] = -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void process(const ProcessArgs& args) override
|
void process(const ProcessArgs& args) override
|
||||||
{
|
{
|
||||||
|
// Cardinal specific
|
||||||
|
const int64_t blockFrame = pcontext->engine->getBlockFrame();
|
||||||
|
const bool blockFrameChanged = lastBlockFrame != blockFrame;
|
||||||
|
|
||||||
|
if (blockFrameChanged)
|
||||||
|
{
|
||||||
|
lastBlockFrame = blockFrame;
|
||||||
|
|
||||||
|
midiEvents = pcontext->midiEvents;
|
||||||
|
midiEventsLeft = pcontext->midiEventCount;
|
||||||
|
midiEventFrame = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!divider.process())
|
||||||
|
{
|
||||||
|
++midiEventFrame;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (midiEventsLeft != 0)
|
||||||
|
{
|
||||||
|
const MidiEvent& midiEvent(*midiEvents);
|
||||||
|
|
||||||
|
if (midiEvent.frame > midiEventFrame)
|
||||||
|
break;
|
||||||
|
|
||||||
|
++midiEvents;
|
||||||
|
--midiEventsLeft;
|
||||||
|
|
||||||
|
const uint8_t* const data = midiEvent.size > MidiEvent::kDataSize
|
||||||
|
? midiEvent.dataExt
|
||||||
|
: midiEvent.data;
|
||||||
|
|
||||||
|
if (channel != 0 && data[0] < 0xF0)
|
||||||
|
{
|
||||||
|
if ((data[0] & 0x0F) != (channel - 1))
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adapted from Rack
|
||||||
|
if ((data[0] & 0xF0) != 0xB0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
uint8_t cc = data[1];
|
||||||
|
int8_t value = data[2];
|
||||||
|
|
||||||
|
// Learn
|
||||||
|
if (0 <= learningId && values[cc] != value)
|
||||||
|
{
|
||||||
|
ccs[learningId] = cc;
|
||||||
|
valueFilters[learningId].reset();
|
||||||
|
learnedCc = true;
|
||||||
|
commitLearn();
|
||||||
|
updateMapLen();
|
||||||
|
refreshParamHandleText(learningId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore negative values generated using the nonstandard 8-bit MIDI extension from the gamepad driver
|
||||||
|
if (value < 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
values[cc] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
++midiEventFrame;
|
||||||
|
|
||||||
|
// Step channels
|
||||||
|
for (int id = 0; id < mapLen; ++id)
|
||||||
|
{
|
||||||
|
int cc = ccs[id];
|
||||||
|
if (cc < 0)
|
||||||
|
continue;
|
||||||
|
// Get Module
|
||||||
|
Module* module = paramHandles[id].module;
|
||||||
|
if (!module)
|
||||||
|
continue;
|
||||||
|
// Get ParamQuantity from ParamHandle
|
||||||
|
int paramId = paramHandles[id].paramId;
|
||||||
|
ParamQuantity* paramQuantity = module->paramQuantities[paramId];
|
||||||
|
if (!paramQuantity)
|
||||||
|
continue;
|
||||||
|
if (!paramQuantity->isBounded())
|
||||||
|
continue;
|
||||||
|
// Set filter from param value if filter is uninitialized
|
||||||
|
if (!filterInitialized[id]) {
|
||||||
|
valueFilters[id].out = paramQuantity->getScaledValue();
|
||||||
|
filterInitialized[id] = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Check if CC has been set by the MIDI device
|
||||||
|
if (values[cc] < 0)
|
||||||
|
continue;
|
||||||
|
float value = values[cc] / 127.f;
|
||||||
|
// Detect behavior from MIDI buttons.
|
||||||
|
if (smooth && std::fabs(valueFilters[id].out - value) < 1.f) {
|
||||||
|
// Smooth value with filter
|
||||||
|
valueFilters[id].process(args.sampleTime * divider.getDivision(), value);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Jump value
|
||||||
|
valueFilters[id].out = value;
|
||||||
|
}
|
||||||
|
paramQuantity->setScaledValue(valueFilters[id].out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearMap(int id)
|
||||||
|
{
|
||||||
|
learningId = -1;
|
||||||
|
ccs[id] = -1;
|
||||||
|
pcontext->engine->updateParamHandle(¶mHandles[id], -1, 0, true);
|
||||||
|
valueFilters[id].reset();
|
||||||
|
updateMapLen();
|
||||||
|
refreshParamHandleText(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearMaps_NoLock()
|
||||||
|
{
|
||||||
|
learningId = -1;
|
||||||
|
for (int id = 0; id < MAX_CHANNELS; id++) {
|
||||||
|
ccs[id] = -1;
|
||||||
|
pcontext->engine->updateParamHandle_NoLock(¶mHandles[id], -1, 0, true);
|
||||||
|
valueFilters[id].reset();
|
||||||
|
refreshParamHandleText(id);
|
||||||
|
}
|
||||||
|
mapLen = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateMapLen()
|
||||||
|
{
|
||||||
|
// Find last nonempty map
|
||||||
|
int id;
|
||||||
|
for (id = MAX_CHANNELS - 1; id >= 0; id--) {
|
||||||
|
if (ccs[id] >= 0 || paramHandles[id].moduleId >= 0)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mapLen = id + 1;
|
||||||
|
// Add an empty "Mapping..." slot
|
||||||
|
if (mapLen < MAX_CHANNELS)
|
||||||
|
mapLen++;
|
||||||
|
}
|
||||||
|
|
||||||
|
void commitLearn()
|
||||||
|
{
|
||||||
|
if (learningId < 0)
|
||||||
|
return;
|
||||||
|
if (!learnedCc)
|
||||||
|
return;
|
||||||
|
if (!learnedParam)
|
||||||
|
return;
|
||||||
|
// Reset learned state
|
||||||
|
learnedCc = false;
|
||||||
|
learnedParam = false;
|
||||||
|
// Find next incomplete map
|
||||||
|
while (++learningId < MAX_CHANNELS) {
|
||||||
|
if (ccs[learningId] < 0 || paramHandles[learningId].moduleId < 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
learningId = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void enableLearn(int id)
|
||||||
|
{
|
||||||
|
if (learningId != id) {
|
||||||
|
learningId = id;
|
||||||
|
learnedCc = false;
|
||||||
|
learnedParam = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disableLearn(int id)
|
||||||
|
{
|
||||||
|
if (learningId == id) {
|
||||||
|
learningId = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void learnParam(int id, int64_t moduleId, int paramId)
|
||||||
|
{
|
||||||
|
pcontext->engine->updateParamHandle(¶mHandles[id], moduleId, paramId, true);
|
||||||
|
learnedParam = true;
|
||||||
|
commitLearn();
|
||||||
|
updateMapLen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void refreshParamHandleText(int id) {
|
||||||
|
std::string text;
|
||||||
|
if (ccs[id] >= 0)
|
||||||
|
text = string::f("CC%02d", ccs[id]);
|
||||||
|
else
|
||||||
|
text = "MIDI-Map";
|
||||||
|
paramHandles[id].text = text;
|
||||||
}
|
}
|
||||||
|
|
||||||
json_t* dataToJson() override
|
json_t* dataToJson() override
|
||||||
|
|
@ -71,11 +332,62 @@ struct HostMIDIMap : Module {
|
||||||
json_t* const rootJ = json_object();
|
json_t* const rootJ = json_object();
|
||||||
DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
|
DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
|
||||||
|
|
||||||
|
if (json_t* const mapsJ = json_array())
|
||||||
|
{
|
||||||
|
for (int id = 0; id < mapLen; id++)
|
||||||
|
{
|
||||||
|
json_t* const mapJ = json_object();
|
||||||
|
DISTRHO_SAFE_ASSERT_CONTINUE(mapJ != nullptr);
|
||||||
|
json_object_set_new(mapJ, "cc", json_integer(ccs[id]));
|
||||||
|
json_object_set_new(mapJ, "moduleId", json_integer(paramHandles[id].moduleId));
|
||||||
|
json_object_set_new(mapJ, "paramId", json_integer(paramHandles[id].paramId));
|
||||||
|
json_array_append_new(mapsJ, mapJ);
|
||||||
|
}
|
||||||
|
json_object_set_new(rootJ, "maps", mapsJ);
|
||||||
|
}
|
||||||
|
|
||||||
|
json_object_set_new(rootJ, "smooth", json_boolean(smooth));
|
||||||
|
|
||||||
|
// FIXME use "midi" object?
|
||||||
|
json_object_set_new(rootJ, "channel", json_integer(channel));
|
||||||
return rootJ;
|
return rootJ;
|
||||||
}
|
}
|
||||||
|
|
||||||
void dataFromJson(json_t* rootJ) override
|
void dataFromJson(json_t* const rootJ) override
|
||||||
{
|
{
|
||||||
|
// Use NoLock because we're already in an Engine write-lock.
|
||||||
|
clearMaps_NoLock();
|
||||||
|
|
||||||
|
if (json_t* const mapsJ = json_object_get(rootJ, "maps"))
|
||||||
|
{
|
||||||
|
json_t* mapJ;
|
||||||
|
size_t mapIndex;
|
||||||
|
json_array_foreach(mapsJ, mapIndex, mapJ)
|
||||||
|
{
|
||||||
|
json_t* ccJ = json_object_get(mapJ, "cc");
|
||||||
|
json_t* moduleIdJ = json_object_get(mapJ, "moduleId");
|
||||||
|
json_t* paramIdJ = json_object_get(mapJ, "paramId");
|
||||||
|
if (!(ccJ && moduleIdJ && paramIdJ))
|
||||||
|
continue;
|
||||||
|
if (mapIndex >= MAX_CHANNELS)
|
||||||
|
continue;
|
||||||
|
ccs[mapIndex] = json_integer_value(ccJ);
|
||||||
|
pcontext->engine->updateParamHandle_NoLock(¶mHandles[mapIndex],
|
||||||
|
json_integer_value(moduleIdJ),
|
||||||
|
json_integer_value(paramIdJ),
|
||||||
|
false);
|
||||||
|
refreshParamHandleText(mapIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMapLen();
|
||||||
|
|
||||||
|
if (json_t* const smoothJ = json_object_get(rootJ, "smooth"))
|
||||||
|
smooth = json_boolean_value(smoothJ);
|
||||||
|
|
||||||
|
// FIXME use "midi" object?
|
||||||
|
if (json_t* const channelJ = json_object_get(rootJ, "channel"))
|
||||||
|
channel = json_integer_value(channelJ);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -115,6 +427,28 @@ struct HostMIDIMapWidget : ModuleWidget {
|
||||||
|
|
||||||
void appendContextMenu(Menu* const menu) override
|
void appendContextMenu(Menu* const menu) override
|
||||||
{
|
{
|
||||||
|
menu->addChild(new MenuSeparator);
|
||||||
|
menu->addChild(createBoolPtrMenuItem("Smooth CC", "", &module->smooth));
|
||||||
|
|
||||||
|
struct InputChannelItem : MenuItem {
|
||||||
|
HostMIDIMap* module;
|
||||||
|
Menu* createChildMenu() override {
|
||||||
|
Menu* menu = new Menu;
|
||||||
|
for (int c = 0; c <= 16; c++) {
|
||||||
|
menu->addChild(createCheckMenuItem((c == 0) ? "All" : string::f("%d", c), "",
|
||||||
|
[=]() {return module->channel == c;},
|
||||||
|
[=]() {module->channel = c;}
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return menu;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
InputChannelItem* const inputChannelItem = new InputChannelItem;
|
||||||
|
inputChannelItem->text = "MIDI channel";
|
||||||
|
inputChannelItem->rightText = (module->channel ? string::f("%d", module->channel) : "All")
|
||||||
|
+ " " + RIGHT_ARROW;
|
||||||
|
inputChannelItem->module = module;
|
||||||
|
menu->addChild(inputChannelItem);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue