Browse Source

MidiThing v2 bridge (#48)

Better default resampling for Octaves
tags/v2.7.0^0
Ewan GitHub 1 year ago
parent
commit
b6cb9e8587
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
13 changed files with 6467 additions and 74 deletions
  1. +7
    -0
      CHANGELOG.md
  2. +28
    -0
      docs/MIDIThingV2.md
  3. BIN
      docs/img/MidiThingV2.png
  4. BIN
      docs/img/UpdateRate.png
  5. BIN
      docs/img/UpdateRatesScope.png
  6. BIN
      docs/img/VoltageRange.png
  7. +15
    -2
      plugin.json
  8. +5595
    -0
      res/panels/MidiThing.svg
  9. +804
    -0
      src/MidiThing.cpp
  10. +12
    -59
      src/Octaves.cpp
  11. +4
    -13
      src/Voltio.cpp
  12. +1
    -0
      src/plugin.cpp
  13. +1
    -0
      src/plugin.hpp

+ 7
- 0
CHANGELOG.md View File

@@ -1,6 +1,13 @@
# Change Log


## v2.7.0
* Midi Thing 2
* Initial release
* Octaves
* Better default oversampling setting (x4)


## v2.6.0
* Octaves
* Initial release


+ 28
- 0
docs/MIDIThingV2.md View File

@@ -0,0 +1,28 @@
# MIDI Thing v2

The original MIDI Thing v2 hardware unit is described as follows:

> Midi Thing v2 is a flexible MIDI to CV converter. Allowing polyphonic notes handling, envelope and LFO generation as well as all available MIDI messages to be converted into CV. This is a huge upgrade from our previous beloved MIDI Thing, which adds a screen for easy configuration,12 assignable ports, TRS, USB Host and Device, MIDI merge OUT, a web configuration tool, and a VCV rack Bridge counterpart.

The VCV counterpart is designed to allow users to quickly get up and running with their hardware, i.e. sending CV from VCV to the hardware unit.

## 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 click "SYNC" - this puts the MIDI Thing into a preset designed to work with VCV Rack, and syncronises settings/voltage ranges etc. Note that for now, sync is one-way (VCV to hardware).

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

## Usage

To use, simply wire CV which you wish to send to the hardware to the matching input on the VCV module. Note that you will need to select the range, which can be done by right-clicking on the matching box (see below). Options are 0/10v, -5/5v, -10/0v, 0/8v, 0/5v. Note that the module is **not** designed to work with audio rate signals, just CV.

![MIDI Thing Voltage Range](img/VoltageRange.png "MIDI Thing v2 Voltage Range")

## Update Rate

Midi Thing v2 VCV allows the user to configure the update rate at which data is sent over MIDI. This must be shared between the channels, so if we set the hardware to update at 1 kHz, 1 active channel will update at 1 kHz, 2 active channels will update at 500 Hz, 4 active channels at 250 Hz and so on. The total update rate (to be shared between channels) is set from the context menu, noting that higher update rates will use more CPU. The effect of the update rate on a 90 Hz saw (blue trace) can be seen in the bottom image, specifically that the temporal resolution of the reconstructed signal (red traces) improves as the update rate is increased from 500 Hz to 1000 Hz to 2000 Hz.

![MIDI Thing Update Rates](img/UpdateRate.png "MIDI Thing v2 Update Ranges Menu")
![MIDI Thing Update Rates](img/UpdateRatesScope.png "MIDI Thing v2 Update Ranges Menu")



BIN
docs/img/MidiThingV2.png View File

Before After
Width: 281  |  Height: 1015  |  Size: 102KB

BIN
docs/img/UpdateRate.png View File

Before After
Width: 762  |  Height: 799  |  Size: 102KB

BIN
docs/img/UpdateRatesScope.png View File

Before After
Width: 2968  |  Height: 963  |  Size: 754KB

BIN
docs/img/VoltageRange.png View File

Before After
Width: 376  |  Height: 394  |  Size: 30KB

+ 15
- 2
plugin.json View File

@@ -1,6 +1,6 @@
{
"slug": "Befaco",
"version": "2.6.0",
"version": "2.7.0",
"license": "GPL-3.0-or-later",
"name": "Befaco",
"brand": "Befaco",
@@ -296,6 +296,18 @@
"Hardware clone"
]
},
{
"slug": "MidiThingV2",
"name": "MIDI Thing V2",
"description": "Hardware MIDI Thing v2 is a flexible MIDI to CV converter, this module acts as a bridge from VCV",
"manualUrl": "https://github.com/VCVRack/Befaco/blob/v2/docs/MIDIThingV2.md",
"modularGridUrl": "https://www.modulargrid.net/e/befaco-midi-thing-v2",
"tags": [
"External",
"MIDI",
"Hardware clone"
]
},
{
"slug": "Voltio",
"name": "Voltio",
@@ -316,7 +328,8 @@
"modularGridUrl": "https://www.modulargrid.net/e/befaco-octaves-vco",
"tags": [
"Hardware clone",
"Oscillator"
"Oscillator",
"Polyphonic"
]
}
]


+ 5595
- 0
res/panels/MidiThing.svg
File diff suppressed because it is too large
View File


+ 804
- 0
src/MidiThing.cpp View File

@@ -0,0 +1,804 @@
#include "plugin.hpp"


/*! \brief Decode System Exclusive messages.
SysEx messages are encoded to guarantee transmission of data bytes higher than
127 without breaking the MIDI protocol. Use this static method to reassemble
your received message.
\param inSysEx The SysEx data received from MIDI in.
\param outData The output buffer where to store the decrypted message.
\param inLength The length of the input buffer.
\param inFlipHeaderBits True for Korg and other who store MSB in reverse order
\return The length of the output buffer.
@see encodeSysEx @see getSysExArrayLength
Code inspired from Ruin & Wesen's SysEx encoder/decoder - http://ruinwesen.com
*/
unsigned decodeSysEx(const uint8_t* inSysEx,
uint8_t* outData,
unsigned inLength,
bool inFlipHeaderBits) {
unsigned count = 0;
uint8_t msbStorage = 0;
uint8_t byteIndex = 0;

for (unsigned i = 0; i < inLength; ++i) {
if ((i % 8) == 0) {
msbStorage = inSysEx[i];
byteIndex = 6;
}
else {
const uint8_t body = inSysEx[i];
const uint8_t shift = inFlipHeaderBits ? 6 - byteIndex : byteIndex;
const uint8_t msb = uint8_t(((msbStorage >> shift) & 1) << 7);
byteIndex--;
outData[count++] = msb | body;
}
}
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 {
enum ParamId {
REFRESH_PARAM,
PARAMS_LEN
};
enum InputId {
A1_INPUT,
B1_INPUT,
C1_INPUT,
A2_INPUT,
B2_INPUT,
C2_INPUT,
A3_INPUT,
B3_INPUT,
C3_INPUT,
A4_INPUT,
B4_INPUT,
C4_INPUT,
INPUTS_LEN
};
enum OutputId {
OUTPUTS_LEN
};
enum LightId {
LIGHTS_LEN
};
/// Port mode
enum PORTMODE_t {
NOPORTMODE = 0,
MODE10V,
MODEPN5V,
MODENEG10V,
MODE8V,
MODE5V,

LASTPORTMODE
};

const char* cfgPortModeNames[7] = {
"No Mode",
"0/10v",
"-5/5v",
"-10/0v",
"0/8v",
"0/5v",
""
};

const std::vector<float> updateRates = {250., 500., 1000., 2000., 4000., 8000.};
const std::vector<std::string> updateRateNames = {"250 Hz (fewest active channels, slowest, lowest-cpu)", "500 Hz", "1 kHz", "2 kHz", "4 kHz",
"8 kHz (most active channels, fast, highest-cpu)"
};
int updateRateIdx = 2;

// 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);
configButton(REFRESH_PARAM, "");

for (int i = 0; i < NUM_INPUTS; ++i) {
portModes[i] = MODE10V;
configInput(A1_INPUT + i, string::f("Port %d", i + 1));
}
}

void onReset() override {
midiOut.reset();

}

void requestAllChannelsParamsOverSysex() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
const int PORT_CONFIG = 2;
requestParamOverSysex(row, col, PORT_CONFIG);
}
}
}

// request that MidiThing loads a pre-defined template, 1-4
void setPredef(uint8_t predef) {
predef = clamp(predef, 1, 4);
midi::Message msg;
msg.bytes.resize(8);
// Midi spec is zeroo indexed
uint8_t predefToSend = predef - 1;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x00, 0x02, 0x00, predefToSend, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", predef, msg.toString().c_str());
}

void setMidiMergeViaSysEx(bool mergeOn) {
midi::Message msg;
msg.bytes.resize(8);

msg.bytes = {0xF0, 0x7D, 0x19, 0x00, 0x05, 0x02, 0x00, (uint8_t) mergeOn, 0xF7};
midiOut.setChannel(0);
midiOut.sendMessage(msg);
// DEBUG("Predef %d msg request sent: %s", mergeOn, msg.toString().c_str());
}


void setVoltageModeOnHardware(uint8_t row, uint8_t col, PORTMODE_t outputMode_) {
uint8_t port = 3 * row + col;
portModes[port] = outputMode_;

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 2n 02 02 00 0m F7
// Where n = 0 based port number
// and m is the volt output mode to select from:
msg.bytes = {0xF0, 0x7D, 0x17, static_cast<unsigned char>(32 + port), 0x02, 0x02, 0x00, (uint8_t) portModes[port], 0xF7};
midiOut.sendMessage(msg);
// DEBUG("Voltage mode msg sent: port %d (%d), mode %d", port, static_cast<unsigned char>(32 + port), portModes[port]);
}

void setVoltageModeOnHardware(uint8_t row, uint8_t col) {
setVoltageModeOnHardware(row, col, portModes[3 * row + col]);
}

void syncVcvStateToHardware() {
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 3; ++col) {
setVoltageModeOnHardware(row, col);
}
}
}


midi::InputQueue inputQueue;
void requestParamOverSysex(uint8_t row, uint8_t col, uint8_t mode) {

midi::Message msg;
msg.bytes.resize(8);
// F0 7D 17 00 01 03 00 nm pp F7
uint8_t port = 3 * row + col;
//Where n is:
// 0 = Full configuration request. The module will send only pre def, port functions and modified parameters
// 2 = Send Port configuration
// 4 = Send MIDI Channel configuration
// 6 = Send Voice Configuration

uint8_t n = mode * 16;
uint8_t m = port; // element number: 0-11 port number, 1-16 channel or voice number
uint8_t pp = 2;
msg.bytes = {0xF0, 0x7D, 0x17, 0x00, 0x01, 0x03, 0x00, static_cast<uint8_t>(n + m), pp, 0xF7};
midiOut.sendMessage(msg);
// DEBUG("API request mode msg sent: port %d, pp %s", port, msg.toString().c_str());
}

int getVoltageMode(uint8_t row, uint8_t col) {
// -1 because menu is zero indexed but enum is not
int channel = clamp(3 * row + col, 0, NUM_INPUTS - 1);
return portModes[channel] - 1;
}

const static int NUM_INPUTS = 12;
bool isClipping[NUM_INPUTS] = {};

bool checkIsVoltageWithinRange(uint8_t channel, float voltage) {
const float tol = 0.001;
switch (portModes[channel]) {
case MODE10V: return 0 - tol < voltage && voltage < 10 + tol;
case MODEPN5V: return -5 - tol < voltage && voltage < 5 + tol;
case MODENEG10V: return -10 - tol < voltage && voltage < 0 + tol;
case MODE8V: return 0 - tol < voltage && voltage < 8 + tol;
case MODE5V: return 0 - tol < voltage && voltage < 5 + tol;
default: return false;
}
}

uint16_t rescaleVoltageForChannel(uint8_t channel, float voltage) {
switch (portModes[channel]) {
case MODE10V: return rescale(clamp(voltage, 0.f, 10.f), 0.f, +10.f, 0, 16383);
case MODEPN5V: return rescale(clamp(voltage, -5.f, 5.f), -5.f, +5.f, 0, 16383);
case MODENEG10V: return rescale(clamp(voltage, -10.f, 0.f), -10.f, +0.f, 0, 16383);
case MODE8V: return rescale(clamp(voltage, 0.f, 8.f), 0.f, +8.f, 0, 16383);
case MODE5V: return rescale(clamp(voltage, 0.f, 5.f), 0.f, +5.f, 0, 16383);
default: return 0;
}
}

// one way sync (VCV -> hardware) for now
void doSync() {
// switch to VCV template (predef 4)
setPredef(4);

// disable MIDI merge (otherwise large sample rates will not work)
setMidiMergeViaSysEx(false);

// send full VCV config
syncVcvStateToHardware();

// disabled for now, but this would request what state the hardware is in
if (parseSysExMessagesFromHardware) {
requestAllChannelsParamsOverSysex();
}
}

// debug only
bool parseSysExMessagesFromHardware = false;
int numActiveChannels = 0;
dsp::BooleanTrigger buttonTrigger;
dsp::Timer rateLimiterTimer;
PORTMODE_t portModes[NUM_INPUTS] = {};
void process(const ProcessArgs& args) override {

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

// disabled for now, but this is how VCV would read SysEx coming from the hardware (if requested above)
if (parseSysExMessagesFromHardware) {
midi::Message msg;
uint8_t outData[32] = {};
while (inputQueue.tryPop(&msg, args.frame)) {

uint8_t outLen = decodeSysEx(&msg.bytes[0], outData, msg.bytes.size(), false);
if (outLen > 3) {

int channel = (outData[2] & 0x0f) >> 0;

if (channel >= 0 && channel < NUM_INPUTS) {
if (outData[outLen - 1] < LASTPORTMODE) {
portModes[channel] = (PORTMODE_t) outData[outLen - 1];
}
}
}
}
}

std::vector<int> activeChannels;
for (int c = 0; c < NUM_INPUTS; ++c) {
if (inputs[A1_INPUT + c].isConnected()) {
activeChannels.push_back(c);
}
}
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.
// The refresh rate period (i.e. how often we can send X channels of data is:
const float rateLimiterPeriod = 1.f / maxCCMessagesPerSecondPerChannel;

// 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);

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

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);

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, "updateRateIdx", json_integer(updateRateIdx));

for (int c = 0; c < NUM_INPUTS; ++c) {
json_object_set_new(rootJ, string::f("portMode%d", c).c_str(), json_integer(portModes[c]));
}

return rootJ;
}

void dataFromJson(json_t* rootJ) override {
json_t* midiOutputJ = json_object_get(rootJ, "midiOutput");
if (midiOutputJ) {
midiOut.fromJson(midiOutputJ);
}

json_t* midiInputQueueJ = json_object_get(rootJ, "inputQueue");
if (midiInputQueueJ) {
inputQueue.fromJson(midiInputQueueJ);
}

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

for (int c = 0; c < NUM_INPUTS; ++c) {
json_t* portModeJ = json_object_get(rootJ, string::f("portMode%d", c).c_str());
if (portModeJ) {
portModes[c] = (PORTMODE_t)json_integer_value(portModeJ);
}
}

// requestAllChannelsParamsOverSysex();
syncVcvStateToHardware();
}
};

struct MidiThingPort : BefacoInputPort {
int row = 0, col = 0;
MidiThing* module;

void appendContextMenu(Menu* menu) override {

menu->addChild(new MenuSeparator());
std::string label = string::f("Voltage Mode Port %d", 3 * row + col + 1);

menu->addChild(createIndexSubmenuItem(label,
{"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"},
[ = ]() {
return module->getVoltageMode(row, col);
},
[ = ](int modeIdx) {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(modeIdx + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));

/*
menu->addChild(createIndexSubmenuItem("Get Port Info",
{"Full", "Port", "MIDI", "Voice"},
[ = ]() {
return -1;
},
[ = ](int mode) {
module->requestParamOverSysex(row, col, 2 * mode);
}
));
*/
}
};

// dervied from https://github.com/countmodula/VCVRackPlugins/blob/v2.0.0/src/components/CountModulaLEDDisplay.hpp
struct LEDDisplay : LightWidget {
float fontSize = 9;
Vec textPos = Vec(1, 13);
int numChars = 7;
int row = 0, col = 0;
MidiThing* module;

LEDDisplay() {
box.size = mm2px(Vec(9.298, 5.116));
}

void setCentredPos(Vec pos) {
box.pos.x = pos.x - box.size.x / 2;
box.pos.y = pos.y - box.size.y / 2;
}

void drawBackground(const DrawArgs& args) override {
// Background
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
nvgFillColor(args.vg, backgroundColor);
nvgFill(args.vg);
nvgStrokeWidth(args.vg, 1.0);
nvgStrokeColor(args.vg, borderColor);
nvgStroke(args.vg);
}

void drawLight(const DrawArgs& args) override {
// Background
NVGcolor backgroundColor = nvgRGB(0x20, 0x20, 0x20);
NVGcolor borderColor = nvgRGB(0x10, 0x10, 0x10);
NVGcolor textColor = nvgRGB(0xff, 0x10, 0x10);

nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, 0.0, 0.0, box.size.x, box.size.y, 2.0);
nvgFillColor(args.vg, backgroundColor);
nvgFill(args.vg);
nvgStrokeWidth(args.vg, 1.0);

if (module) {
const bool isClipping = module->isClipping[col + row * 3];
if (isClipping) {
borderColor = nvgRGB(0xff, 0x20, 0x20);
}
}

nvgStrokeColor(args.vg, borderColor);
nvgStroke(args.vg);

std::shared_ptr<Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0) {

std::string text = "?-?v"; // fallback if module not yet defined
if (module) {
text = module->cfgPortModeNames[module->getVoltageMode(row, col) + 1];
}
char buffer[numChars + 1];
int l = text.size();
if (l > numChars)
l = numChars;

nvgGlobalTint(args.vg, color::WHITE);

text.copy(buffer, l);
buffer[l] = '\0';

nvgFontSize(args.vg, fontSize);
nvgFontFaceId(args.vg, font->handle);
nvgFillColor(args.vg, textColor);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textPos.x, textPos.y, box.size.x, textRow.start, textRow.end);
}
}

void onButton(const ButtonEvent& e) override {
if (e.button == GLFW_MOUSE_BUTTON_RIGHT && e.action == GLFW_PRESS) {
ui::Menu* menu = createMenu();

menu->addChild(createMenuLabel(string::f("Voltage mode port %d:", col + 3 * row + 1)));

const std::string labels[5] = {"0 to 10v", "-5 to 5v", "-10 to 0v", "0 to 8v", "0 to 5v"};

for (int i = 0; i < 5; ++i) {
menu->addChild(createCheckMenuItem(labels[i], "",
[ = ]() {
return module->getVoltageMode(row, col) == i;
},
[ = ]() {
MidiThing::PORTMODE_t mode = (MidiThing::PORTMODE_t)(i + 1);
module->setVoltageModeOnHardware(row, col, mode);
}
));
}

e.consume(this);
return;
}

LightWidget::onButton(e);
}

};


struct MidiThingWidget : ModuleWidget {

struct LedDisplayCenterChoiceEx : LedDisplayChoice {
LedDisplayCenterChoiceEx() {
box.size = mm2px(math::Vec(0, 8.0));
color = nvgRGB(0xf0, 0xf0, 0xf0);
bgColor = nvgRGBAf(0, 0, 0, 0);
textOffset = math::Vec(0, 16);
}

void drawLayer(const DrawArgs& args, int layer) override {
nvgScissor(args.vg, RECT_ARGS(args.clipBox));
if (layer == 1) {
if (bgColor.a > 0.0) {
nvgBeginPath(args.vg);
nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
nvgFillColor(args.vg, bgColor);
nvgFill(args.vg);
}

std::shared_ptr<window::Font> font = APP->window->loadFont(asset::plugin(pluginInstance, "res/fonts/miso.otf"));

if (font && font->handle >= 0 && !text.empty()) {
nvgFillColor(args.vg, color);
nvgFontFaceId(args.vg, font->handle);
nvgTextLetterSpacing(args.vg, -0.6f);
nvgFontSize(args.vg, 10);
nvgTextAlign(args.vg, NVG_ALIGN_CENTER | NVG_ALIGN_BOTTOM);
NVGtextRow textRow;
nvgTextBreakLines(args.vg, text.c_str(), NULL, box.size.x, &textRow, 1);
nvgTextBox(args.vg, textOffset.x, textOffset.y, box.size.x, textRow.start, textRow.end);
}
}
nvgResetScissor(args.vg);
}
};


struct MidiDriverItem : ui::MenuItem {
midi::Port* port;
int driverId;
void onAction(const event::Action& e) override {
port->setDriverId(driverId);
}
};

struct MidiDriverChoice : LedDisplayCenterChoiceEx {
midi::Port* port;
void onAction(const event::Action& e) override {
if (!port)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI driver"));
for (int driverId : midi::getDriverIds()) {
MidiDriverItem* item = new MidiDriverItem;
item->port = port;
item->driverId = driverId;
item->text = midi::getDriver(driverId)->getName();
item->rightText = CHECKMARK(item->driverId == port->driverId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = port ? port->getDriver()->getName() : "";
if (text.empty()) {
text = "(No driver)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

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

struct MidiDeviceChoice : LedDisplayCenterChoiceEx {
midi::Port* outPort, *inPort;
void onAction(const event::Action& e) override {
if (!outPort || !inPort)
return;
createContextMenu();
}

virtual ui::Menu* createContextMenu() {
ui::Menu* menu = createMenu();
menu->addChild(createMenuLabel("MIDI device"));
{
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = -1;
item->text = "(No device)";
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
for (int deviceId : outPort->getDeviceIds()) {
MidiDeviceItem* item = new MidiDeviceItem;
item->outPort = outPort;
item->inPort = inPort;
item->deviceId = deviceId;
item->text = outPort->getDeviceName(deviceId);
item->rightText = CHECKMARK(item->deviceId == outPort->deviceId);
menu->addChild(item);
}
return menu;
}

void step() override {
text = outPort ? outPort->getDeviceName(outPort->deviceId) : "";
if (text.empty()) {
text = "(No device)";
color.a = 0.5f;
}
else {
color.a = 1.f;
}
}
};

struct MidiWidget : LedDisplay {
MidiDriverChoice* driverChoice;
LedDisplaySeparator* driverSeparator;
MidiDeviceChoice* deviceChoice;
LedDisplaySeparator* deviceSeparator;

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

clearChildren();
math::Vec pos;

MidiDriverChoice* driverChoice = createWidget<MidiDriverChoice>(pos);
driverChoice->box.size = Vec(box.size.x, 20.f);
//driverChoice->textOffset = Vec(6.f, 14.7f);
driverChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
driverChoice->port = outPort;

addChild(driverChoice);
pos = driverChoice->box.getBottomLeft();
this->driverChoice = driverChoice;

this->driverSeparator = createWidget<LedDisplaySeparator>(pos);
this->driverSeparator->box.size.x = box.size.x;
addChild(this->driverSeparator);

MidiDeviceChoice* deviceChoice = createWidget<MidiDeviceChoice>(pos);
deviceChoice->box.size = Vec(box.size.x, 21.f);
//deviceChoice->textOffset = Vec(6.f, 14.7f);
deviceChoice->color = nvgRGB(0xf0, 0xf0, 0xf0);
deviceChoice->outPort = outPort;
deviceChoice->inPort = inPort;
addChild(deviceChoice);
pos = deviceChoice->box.getBottomLeft();
this->deviceChoice = deviceChoice;
}
};


MidiThingWidget(MidiThing* module) {
setModule(module);
setPanel(createPanel(asset::plugin(pluginInstance, "res/panels/MidiThing.svg")));

addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, 0)));
addChild(createWidget<Knurlie>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

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));
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));

const float xStartLed = 0.2 + 0.628;
const float yStartLed = 28.019;

for (int row = 0; row < 4; row++) {
for (int col = 0; col < 3; col++) {

LEDDisplay* display = createWidget<LEDDisplay>(mm2px(Vec(xStartLed + 9.751 * col, yStartLed + 5.796 * row)));
display->module = module;
display->row = row;
display->col = col;
addChild(display);

auto input = createInputCentered<MidiThingPort>(mm2px(Vec(5.08 + 10 * col, 69.77 + 14.225 * row)), module, MidiThing::A1_INPUT + 3 * row + col);
input->row = row;
input->col = col;
input->module = module;
addInput(input);


}
}
}

void appendContextMenu(Menu* menu) override {
MidiThing* module = dynamic_cast<MidiThing*>(this->module);
assert(module);

menu->addChild(new MenuSeparator());

menu->addChild(createSubmenuItem("Select MIDI Device", "",
[ = ](Menu * menu) {

for (auto driverId : rack::midi::getDriverIds()) {
midi::Driver* driver = midi::getDriver(driverId);
const bool activeDriver = module->midiOut.getDriverId() == driverId;

menu->addChild(createSubmenuItem(driver->getName(), CHECKMARK(activeDriver),
[ = ](Menu * menu) {

for (auto deviceId : driver->getOutputDeviceIds()) {
const bool activeDevice = activeDriver && module->midiOut.getDeviceId() == deviceId;

menu->addChild(createMenuItem(driver->getOutputDeviceName(deviceId),
CHECKMARK(activeDevice),
[ = ]() {
module->midiOut.setDriverId(driverId);
module->midiOut.setDeviceId(deviceId);

module->inputQueue.setDriverId(driverId);
module->inputQueue.setDeviceId(deviceId);
module->inputQueue.setChannel(0); // TODO update

module->doSync();

// DEBUG("Updating Output MIDI settings - driver: %s, device: %s",
// driver->getName().c_str(), driver->getOutputDeviceName(deviceId).c_str());
}));
}
}));
}
}));

menu->addChild(createIndexPtrSubmenuItem("All channels MIDI update rate",
module->updateRateNames,
&module->updateRateIdx));

float updateRate = module->updateRates[module->updateRateIdx] / module->numActiveChannels;
menu->addChild(createMenuLabel(string::f("Per-channel MIDI update rate: %.3g Hz", updateRate)));
}
};


Model* modelMidiThing = createModel<MidiThing, MidiThingWidget>("MidiThingV2");

+ 12
- 59
src/Octaves.cpp View File

@@ -3,60 +3,6 @@

using namespace simd;

float aliasSuppressedSaw(const float* phases, float pw) {
float sawBuffer[3];
for (int i = 0; i < 3; ++i) {
float p = 2 * phases[i] - 1.0; // range -1 to +1
float pwp = p + 2 * pw; // phase after pw (pw in [0, 1])
pwp += simd::ifelse(pwp > 1, -2, simd::ifelse(pwp < -1, +2, 0)); // modulo on [-1, +1]
sawBuffer[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}

return (sawBuffer[0] - 2.0 * sawBuffer[1] + sawBuffer[2]);
}

float aliasSuppressedOffsetSaw(const float* phases, float pw) {
float sawOffsetBuff[3];

for (int i = 0; i < 3; ++i) {
float pwp = 2 * phases[i] - 2 * pw; // range -1 to +1

pwp += simd::ifelse(pwp > 1, -2, 0); // modulo on [-1, +1]
sawOffsetBuff[i] = (pwp * pwp * pwp - pwp) / 6.0; // eq 11
}
return (sawOffsetBuff[0] - 2.0 * sawOffsetBuff[1] + sawOffsetBuff[2]);
}

template<typename T>
class HardClipperADAA {
public:

T process(T x) {
T y = simd::ifelse(simd::abs(x - xPrev) < 1e-5,
f(0.5 * (xPrev + x)),
(F(x) - F(xPrev)) / (x - xPrev));

xPrev = x;
return y;
}


static T f(T x) {
return simd::ifelse(simd::abs(x) < 1, x, simd::sgn(x));
}

static T F(T x) {
return simd::ifelse(simd::abs(x) < 1, 0.5 * x * x, x * simd::sgn(x) - 0.5);
}

void reset() {
xPrev = 0.f;
}

private:
T xPrev = 0.f;
};

struct Octaves : Module {
enum ParamId {
PWM_CV_PARAM,
@@ -106,7 +52,7 @@ struct Octaves : Module {

float_4 phase[4] = {}; // phase for core waveform, in [0, 1]
chowdsp::VariableOversampling<6, float_4> oversampler[NUM_OUTPUTS][4]; // uses a 2*6=12th order Butterworth filter
int oversamplingIndex = 1; // default is 2^oversamplingIndex == x2 oversampling
int oversamplingIndex = 2; // default is 2^oversamplingIndex == x4 oversampling

DCBlockerT<2, float_4> blockDCFilter[NUM_OUTPUTS][4]; // optionally block DC with RC filter @ ~22 Hz
dsp::TSchmittTrigger<float_4> syncTrigger[4]; // for hard sync
@@ -205,13 +151,20 @@ struct Octaves : Module {

float_4 sum = {};
for (int oct = 0; oct <= highestOutput; oct++) {

const float_4 gainCV = simd::clamp(inputs[GAIN_01F_INPUT + oct].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.0f);
const float_4 gain = params[GAIN_01F_PARAM + oct].getValue() * gainCV;

// don't bother processing if gain is zero and no output is connected
const bool isGainZero = simd::movemask(gain != 0.f) == 0;
if (isGainZero && !outputs[OUT_01F_OUTPUT + oct].isConnected()) {
continue;
}

// derive phases for higher octaves from base phase (this keeps things in sync!)
const float_4 n = (float)(1 << oct);
// this is on [0, 1]
const float_4 effectivePhase = n * simd::fmod(phase[c / 4], 1 / n);
const float_4 gainCV = simd::clamp(inputs[GAIN_01F_INPUT + oct].getNormalPolyVoltageSimd<float_4>(10.f, c) / 10.f, 0.f, 1.0f);
const float_4 gain = params[GAIN_01F_PARAM + oct].getValue() * gainCV;

const float_4 waveTri = 1.0 - 2.0 * simd::abs(2.f * effectivePhase - 1.0);
// build square from triangle + comparator
const float_4 waveSquare = simd::ifelse(waveTri > pwm, +1.f, -1.f);
@@ -324,7 +277,7 @@ struct OctavesWidget : ModuleWidget {
addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(52.138, 15.037)), module, Octaves::PWM_CV_PARAM));
addParam(createParam<CKSSVert7>(mm2px(Vec(22.171, 30.214)), module, Octaves::OCTAVE_PARAM));
addParam(createParamCentered<BefacoTinyKnobLightGrey>(mm2px(Vec(10.264, 33.007)), module, Octaves::TUNE_PARAM));
addParam(createParamCentered<Davies1900hLargeRedKnob>(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
addParam(createParamCentered<Davies1900hLargeGreyKnob>(mm2px(Vec(45.384, 40.528)), module, Octaves::PWM_PARAM));
addParam(createParam<CKSSThreeHorizontal>(mm2px(Vec(6.023, 48.937)), module, Octaves::RANGE_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(2.9830, 60.342)), module, Octaves::GAIN_01F_PARAM));
addParam(createParam<BefacoSlidePotSmall>(mm2px(Vec(12.967, 60.342)), module, Octaves::GAIN_02F_PARAM));


+ 4
- 13
src/Voltio.cpp View File

@@ -2,18 +2,6 @@

using simd::float_4;

struct Davies1900hLargeLightGreyKnobCustom : Davies1900hLargeLightGreyKnob {
widget::SvgWidget* bg;

Davies1900hLargeLightGreyKnobCustom() {
minAngle = -0.83 * M_PI;
maxAngle = M_PI;

bg = new widget::SvgWidget;
fb->addChildBelow(bg, tw);
}
};

struct Voltio : Module {
enum ParamId {
OCT_PARAM,
@@ -79,7 +67,10 @@ struct VoltioWidget : ModuleWidget {

addParam(createParamCentered<Davies1900hLargeLightGreyKnob>(mm2px(Vec(15.0, 20.828)), module, Voltio::OCT_PARAM));
addParam(createParamCentered<BefacoSwitch>(mm2px(Vec(22.083, 44.061)), module, Voltio::RANGE_PARAM));
addParam(createParamCentered<Davies1900hLargeLightGreyKnobCustom>(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM));
auto p = createParamCentered<Davies1900hLargeLightGreyKnob>(mm2px(Vec(15.0, 67.275)), module, Voltio::SEMITONES_PARAM);
p->minAngle = -0.83 * M_PI;
p->maxAngle = M_PI;
addParam(p);

addInput(createInputCentered<BefacoInputPort>(mm2px(Vec(7.117, 111.003)), module, Voltio::SUM_INPUT));



+ 1
- 0
src/plugin.cpp View File

@@ -28,6 +28,7 @@ void init(rack::Plugin *p) {
p->addModel(modelPonyVCO);
p->addModel(modelMotionMTR);
p->addModel(modelBurst);
p->addModel(modelMidiThing);
p->addModel(modelVoltio);
p->addModel(modelOctaves);
}

+ 1
- 0
src/plugin.hpp View File

@@ -29,6 +29,7 @@ extern Model* modelChannelStrip;
extern Model* modelPonyVCO;
extern Model* modelMotionMTR;
extern Model* modelBurst;
extern Model* modelMidiThing;
extern Model* modelVoltio;
extern Model* modelOctaves;



Loading…
Cancel
Save