diff --git a/docs/MIDIThingV2.md b/docs/MIDIThingV2.md index 13a028f..b3051eb 100644 --- a/docs/MIDIThingV2.md +++ b/docs/MIDIThingV2.md @@ -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") diff --git a/docs/img/MidiThingV2.png b/docs/img/MidiThingV2.png index b99c5f9..6d3dc95 100644 Binary files a/docs/img/MidiThingV2.png and b/docs/img/MidiThingV2.png differ diff --git a/src/MidiThing.cpp b/src/MidiThing.cpp index 4d09564..00bdf06 100644 --- a/src/MidiThing.cpp +++ b/src/MidiThing.cpp @@ -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 { @@ -88,10 +121,15 @@ struct MidiThing : Module { "" }; + const std::vector updateRates = {200., 1000., 4000., 16000.}; + const std::vector 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); @@ -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) { @@ -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 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; } @@ -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(); } }; @@ -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(); } @@ -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; @@ -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; @@ -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; @@ -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; @@ -598,7 +680,12 @@ struct MidiThingWidget : ModuleWidget { MidiWidget* midiInputWidget = createWidget(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(mm2px(Vec(21.12, 57.32)), module, MidiThing::REFRESH_PARAM)); @@ -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()); })); @@ -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)); } };