Skip to content

Commit

Permalink
Round Robin distribution of CC sends
Browse files Browse the repository at this point in the history
  • Loading branch information
hemmer committed Feb 24, 2024
1 parent 7a9fcee commit 931d248
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 42 deletions.
2 changes: 1 addition & 1 deletion docs/MIDIThingV2.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ The VCV counterpart is designed to allow users to quickly get up and running wit

## Setup

To use, first ensure the MIDI Thing v2 is plugged into your computer, and visible as a MIDI device. Then select it, either from the top of the module, or the right click context menu. Then select "Request Bridge Mode" from the context menu - this puts the MIDI Thing into a preset designed to work with VCV Rack. See below for details.
To use, first ensure the MIDI Thing v2 is plugged into your computer, and visible as a MIDI device. Then select it, either from the top of the module, or the right click context menu. Then click "SYNC" - this puts the MIDI Thing into a preset designed to work with VCV Rack, and syncronises settings/voltage ranges etc.

![MIDI Thing Config](img/MidiThingV2.png "MIDI Thing v2 Setup")

Expand Down
Binary file modified docs/img/MidiThingV2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
174 changes: 133 additions & 41 deletions src/MidiThing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,40 @@ unsigned decodeSysEx(const uint8_t* inSysEx,
return count;
}

struct RoundRobinProcessor {
// if a channel (0 - 11) should be updated, return it's index, otherwise return -1
int process(float sampleTime, float period, int numActiveChannels) {

if (numActiveChannels == 0 || period <= 0) {
return -1;
}

time += sampleTime;

if (time > period) {
time -= period;

// special case: when there's only one channel, the below logic (which looks for when active channel changes)
// wont fire. as we've completed a period, return an "update channel 0" value
if (numActiveChannels == 1) {
return 0;
}
}

int currentActiveChannel = numActiveChannels * time / period;

if (currentActiveChannel != previousActiveChannel) {
previousActiveChannel = currentActiveChannel;
return currentActiveChannel;
}

// if we've got this far, no updates needed (-1)
return -1;
}
private:
float time = 0.f;
int previousActiveChannel = -1;
};


struct MidiThing : Module {
Expand Down Expand Up @@ -88,10 +121,15 @@ struct MidiThing : Module {
""
};

const std::vector<float> updateRates = {200., 1000., 4000., 16000.};
const std::vector<std::string> updateRateNames = {"200hz", "1khz", "4khz", "16khz"};
int updateRateIdx = 1;

// use Pre-def 4 for bridge mode
const static int VCV_BRIDGE_PREDEF = 4;

midi::Output midiOut;
RoundRobinProcessor roundRobinProcessor;

MidiThing() {
config(PARAMS_LEN, INPUTS_LEN, OUTPUTS_LEN, LIGHTS_LEN);
Expand Down Expand Up @@ -199,20 +237,30 @@ struct MidiThing : Module {
}
}

// debug only
bool setFrame = true;

dsp::BooleanTrigger buttonTrigger;
dsp::Timer rateLimiterTimer;
PORTMODE_t portModes[NUM_INPUTS] = {};
void process(const ProcessArgs& args) override {

if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) {
if (buttonTrigger.process(params[REFRESH_PARAM].getValue())) {

// currently this sets the predef to 4, which will reset ranges etc
// TODO: figure this out!
setPredef(4);
refreshConfig();
}

//DEBUG("inputDriver id: %d, outMidi id: %d", inputQueue.getDriverId(), midiOut.getDriverId());
//DEBUG("inputDevice id: %d, outMidi id: %d", inputQueue.getDeviceId(), midiOut.getDeviceId());
//DEBUG("inputChannel: %d, outChannel: %d", inputQueue.getChannel(), midiOut.getChannel());

midi::Message msg;
uint8_t outData[32] = {};
while (inputQueue.tryPop(&msg, args.frame)) {
// DEBUG("msg (size: %d): %s", msg.getSize(), msg.toString().c_str());
DEBUG("msg (size: %d): %s", msg.getSize(), msg.toString().c_str());

uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false);
if (outLen > 3) {
Expand All @@ -222,49 +270,68 @@ struct MidiThing : Module {
if (channel >= 0 && channel < NUM_INPUTS) {
if (outData[outLen - 1] < LASTPORTMODE) {
portModes[channel] = (PORTMODE_t) outData[outLen - 1];
// DEBUG("Channel %d, %d: mode %d (%s)", outData[2], channel, portModes[channel], cfgPortModeNames[portModes[channel]]);
DEBUG("Channel %d, %d: mode %d (%s)", outData[2], channel, portModes[channel], cfgPortModeNames[portModes[channel]]);
}

}
}
}

std::vector<int> activeChannels;
for (int c = 0; c < NUM_INPUTS; ++c) {
if (inputs[A1_INPUT + c].isConnected()) {
activeChannels.push_back(c);
}
}
const int numActiveChannels = activeChannels.size();
// we're done if no channels are active
if (numActiveChannels == 0) {
return;
}

//DEBUG("updateRateIdx: %d", updateRateIdx);
const float updateRateHz = updateRates[updateRateIdx];
//DEBUG("updateRateHz: %f", updateRateHz);
const int maxCCMessagesPerSecondPerChannel = updateRateHz / numActiveChannels;

// MIDI baud rate is 31250 b/s, or 3125 B/s.
// CC messages are 3 bytes, so we can send a maximum of 1041 CC messages per second.
// Since multiple CCs can be generated, play it safe and limit the CC rate to 200 Hz.
const float rateLimiterPeriod = 1 / 200.f;
bool rateLimiterTriggered = (rateLimiterTimer.process(args.sampleTime) >= rateLimiterPeriod);
if (rateLimiterTriggered)
rateLimiterTimer.time -= rateLimiterPeriod;
// The refresh rate period (i.e. how often we can send X channels of data is:
const float rateLimiterPeriod = 1.f / maxCCMessagesPerSecondPerChannel;

if (rateLimiterTriggered) {
// this returns -1 if no channel should be updated, or the index of the channel that should be updated
// it distributes update times in a round robin fashion
int channelIdxToUpdate = roundRobinProcessor.process(args.sampleTime, rateLimiterPeriod, numActiveChannels);

for (int c = 0; c < NUM_INPUTS; ++c) {
if (channelIdxToUpdate >= 0 && channelIdxToUpdate < numActiveChannels) {
int c = activeChannels[channelIdxToUpdate];

if (!inputs[A1_INPUT + c].isConnected()) {
continue;
}
const float channelVoltage = inputs[A1_INPUT + c].getVoltage();
uint16_t pw = rescaleVoltageForChannel(c, channelVoltage);
isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage);
midi::Message m;
m.setStatus(0xe);
m.setNote(pw & 0x7f);
m.setValue((pw >> 7) & 0x7f);
m.setFrame(args.frame);
const float channelVoltage = inputs[A1_INPUT + c].getVoltage();
uint16_t pw = rescaleVoltageForChannel(c, channelVoltage);
isClipping[c] = !checkIsVoltageWithinRange(c, channelVoltage);
midi::Message m;
m.setStatus(0xe);
m.setNote(pw & 0x7f);
m.setValue((pw >> 7) & 0x7f);

midiOut.setChannel(c);
midiOut.sendMessage(m);
if (setFrame) {
m.setFrame(args.frame);
}

midiOut.setChannel(c);
midiOut.sendMessage(m);
}
}


json_t* dataToJson() override {
json_t* rootJ = json_object();
json_object_set_new(rootJ, "midiOutput", midiOut.toJson());
json_object_set_new(rootJ, "inputQueue", inputQueue.toJson());

json_object_set_new(rootJ, "setFrame", json_boolean(setFrame));
json_object_set_new(rootJ, "updateRateIdx", json_integer(updateRateIdx));

return rootJ;
}

Expand All @@ -279,6 +346,16 @@ struct MidiThing : Module {
inputQueue.fromJson(midiInputQueueJ);
}

json_t* setFrameJ = json_object_get(rootJ, "setFrame");
if (setFrameJ) {
setFrame = json_boolean_value(setFrameJ);
}

json_t* updateRateIdxJ = json_object_get(rootJ, "updateRateIdx");
if (updateRateIdxJ) {
updateRateIdx = json_integer_value(updateRateIdxJ);
}

refreshConfig();
}
};
Expand Down Expand Up @@ -504,17 +581,18 @@ struct MidiThingWidget : ModuleWidget {
};

struct MidiDeviceItem : ui::MenuItem {
midi::Port* port;
midi::Port* outPort, *inPort;
int deviceId;
void onAction(const event::Action& e) override {
port->setDeviceId(deviceId);
outPort->setDeviceId(deviceId);
inPort->setDeviceId(deviceId);
}
};

struct MidiDeviceChoice : LedDisplayCenterChoiceEx {
midi::Port* port;
midi::Port* outPort, *inPort;
void onAction(const event::Action& e) override {
if (!port)
if (!outPort || !inPort)
return;
createContextMenu();
}
Expand All @@ -524,25 +602,27 @@ struct MidiThingWidget : ModuleWidget {
menu->addChild(createMenuLabel("MIDI device"));
{
MidiDeviceItem* item = new MidiDeviceItem;
item->port = port;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = -1;
item->text = "(No device)";
item->rightText = CHECKMARK(item->deviceId == port->deviceId);
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
for (int deviceId : port->getDeviceIds()) {
for (int deviceId : outPort->getDeviceIds()) {
MidiDeviceItem* item = new MidiDeviceItem;
item->port = port;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = deviceId;
item->text = port->getDeviceName(deviceId);
item->rightText = CHECKMARK(item->deviceId == port->deviceId);
item->text = outPort->getDeviceName(deviceId);
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = port ? port->getDeviceName(port->deviceId) : "";
text = outPort ? outPort->getDeviceName(outPort->deviceId) : "";
if (text.empty()) {
text = "(No device)";
color.a = 0.5f;
Expand All @@ -559,7 +639,7 @@ struct MidiThingWidget : ModuleWidget {
MidiDeviceChoice* deviceChoice;
LedDisplaySeparator* deviceSeparator;

void setMidiPort(midi::Port* port) {
void setMidiPorts(midi::Port* outPort, midi::Port* inPort) {

clearChildren();
math::Vec pos;
Expand All @@ -568,7 +648,8 @@ struct MidiThingWidget : ModuleWidget {
driverChoice->box.size = Vec(box.size.x, 20.f);
//driverChoice->textOffset = Vec(6.f, 14.7f);
driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
driverChoice->port = port;
driverChoice->port = outPort;

addChild(driverChoice);
pos = driverChoice->box.getBottomLeft();
this->driverChoice = driverChoice;
Expand All @@ -581,7 +662,8 @@ struct MidiThingWidget : ModuleWidget {
deviceChoice->box.size = Vec(box.size.x, 21.f);
//deviceChoice->textOffset = Vec(6.f, 14.7f);
deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
deviceChoice->port = port;
deviceChoice->outPort = outPort;
deviceChoice->inPort = inPort;
addChild(deviceChoice);
pos = deviceChoice->box.getBottomLeft();
this->deviceChoice = deviceChoice;
Expand All @@ -598,7 +680,12 @@ struct MidiThingWidget : ModuleWidget {

MidiWidget* midiInputWidget = createWidget<MidiWidget>(Vec(1.5f, 36.4f)); //mm2px(Vec(0.5f, 10.f)));
midiInputWidget->box.size = mm2px(Vec(5.08 * 6 - 1, 13.5f));
midiInputWidget->setMidiPort(module ? &module->midiOut : NULL);
if (module) {
midiInputWidget->setMidiPorts(&module->midiOut, &module->inputQueue);
}
else {
midiInputWidget->setMidiPorts(nullptr, nullptr);
}
addChild(midiInputWidget);

addParam(createParamCentered<BefacoButton>(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM));
Expand Down Expand Up @@ -656,7 +743,7 @@ struct MidiThingWidget : ModuleWidget {
module->inputQueue.setChannel(0); // TODO update

module->refreshConfig();

// DEBUG("Updating Output MIDI settings - driver: %s, device: %s",
// driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str());
}));
Expand All @@ -674,9 +761,14 @@ struct MidiThingWidget : ModuleWidget {
[ = ](int mode) {
module->setPredef(mode + 1);
module->refreshConfig();
}
));
}));


menu->addChild(createIndexPtrSubmenuItem("MIDI Rate Limiting",
module->updateRateNames,
&module->updateRateIdx));

menu->addChild(createBoolPtrMenuItem("Set frame", "", &module->setFrame));
}
};

Expand Down

0 comments on commit 931d248

Please sign in to comment.