Import base implementation of MIDI-Map

Signed-off-by: falkTX <falktx@falktx.com>
This commit is contained in:
falkTX 2022-01-28 19:53:00 +00:00
parent cc098aae4c
commit 570d8bc2da
No known key found for this signature in database
GPG key ID: CDBAA37ABC74FBA0
2 changed files with 337 additions and 2 deletions

View file

@ -219,6 +219,7 @@ struct HostMIDICC : Module {
CardinalPluginContext* const pcontext;
uint8_t channel = 0;
// from Rack
dsp::Timer rateLimiterTimer;
int lastValues[128];
int64_t frame = 0;

View file

@ -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.
*
* This program is free software: you can redistribute it and/or
@ -33,6 +33,8 @@
USE_NAMESPACE_DISTRHO;
static const int MAX_CHANNELS = 128;
struct HostMIDIMap : Module {
enum ParamIds {
NUM_PARAMS
@ -47,7 +49,36 @@ struct HostMIDIMap : Module {
NUM_LIGHTS
};
// Cardinal specific
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()
: pcontext(static_cast<CardinalPluginContext*>(APP))
@ -56,14 +87,244 @@ struct HostMIDIMap : Module {
throw rack::Exception("Plugin context is null");
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(&paramHandles[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(&paramHandles[id]);
}
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
{
// 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(&paramHandles[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(&paramHandles[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(&paramHandles[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
@ -71,11 +332,62 @@ struct HostMIDIMap : Module {
json_t* const rootJ = json_object();
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;
}
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(&paramHandles[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
{
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);
}
};