From 570d8bc2da0ef574fb685c64c81b94921868ec71 Mon Sep 17 00:00:00 2001 From: falkTX Date: Fri, 28 Jan 2022 19:53:00 +0000 Subject: [PATCH] Import base implementation of MIDI-Map Signed-off-by: falkTX --- plugins/Cardinal/src/HostMIDI-CC.cpp | 1 + plugins/Cardinal/src/HostMIDI-Map.cpp | 338 +++++++++++++++++++++++++- 2 files changed, 337 insertions(+), 2 deletions(-) diff --git a/plugins/Cardinal/src/HostMIDI-CC.cpp b/plugins/Cardinal/src/HostMIDI-CC.cpp index c175f80..62b8e8e 100644 --- a/plugins/Cardinal/src/HostMIDI-CC.cpp +++ b/plugins/Cardinal/src/HostMIDI-CC.cpp @@ -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; diff --git a/plugins/Cardinal/src/HostMIDI-Map.cpp b/plugins/Cardinal/src/HostMIDI-Map.cpp index cc1d3fd..126c23d 100644 --- a/plugins/Cardinal/src/HostMIDI-Map.cpp +++ b/plugins/Cardinal/src/HostMIDI-Map.cpp @@ -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(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(¶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 { + 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(¶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 @@ -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(¶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 { + 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); } };