Browse Source

Merge branch 'bontric-2017-10-bontric-midi-interface-revamp'

tags/v0.5.0
Andrew Belt 7 years ago
parent
commit
f8d980eb76
9 changed files with 1568 additions and 722 deletions
  1. +269
    -0
      src/core/MidiCCToCV.cpp
  2. +336
    -0
      src/core/MidiClockToCV.cpp
  3. +241
    -0
      src/core/MidiIO.cpp
  4. +154
    -0
      src/core/MidiIO.hpp
  5. +0
    -712
      src/core/MidiInterface.cpp
  6. +280
    -0
      src/core/MidiToCV.cpp
  7. +285
    -0
      src/core/MidiTriggerToCV.cpp
  8. +3
    -0
      src/core/core.cpp
  9. +0
    -10
      src/core/core.hpp

+ 269
- 0
src/core/MidiCCToCV.cpp View File

@@ -0,0 +1,269 @@
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "MidiIO.hpp"

/*
* MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to
* CV
*/
struct MIDICCToCVInterface : MidiIO, Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
NUM_OUTPUTS = 16
};

int cc[NUM_OUTPUTS];
int ccNum[NUM_OUTPUTS];
bool ccNumInited[NUM_OUTPUTS];
bool onFocus[NUM_OUTPUTS];

MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {
for (int i = 0; i < NUM_OUTPUTS; i++) {
cc[i] = 0;
ccNum[i] = i;
onFocus[i] = false;
}
}

~MIDICCToCVInterface() {

}

void step();

void processMidi(std::vector<unsigned char> msg);

void resetMidi();

json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(ccNum[i]));
}
return rootJ;
}

void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str());
if (ccNumJ) {
ccNum[i] = json_integer_value(ccNumJ);
ccNumInited[i] = true;
}

}
}

void reset() {
resetMidi();
}

};


void MIDICCToCVInterface::step() {
if (isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue
getMessage(&message);
while (message.size() > 0) {
processMidi(message);
getMessage(&message);
}
}

for (int i = 0; i < NUM_OUTPUTS; i++) {
outputs[i].value = cc[i] / 127.0 * 10.0;
}
}

void MIDICCToCVInterface::resetMidi() {
for (int i = 0; i < NUM_OUTPUTS; i++) {
cc[i] = 0;
}
};

void MIDICCToCVInterface::processMidi(std::vector<unsigned char> msg) {
int channel = msg[0] & 0xf;
int status = (msg[0] >> 4) & 0xf;
int data1 = msg[1];
int data2 = msg[2];

//fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2);

// Filter channels
if (this->channel >= 0 && this->channel != channel)
return;

if (status == 0xb) {
for (int i = 0; i < NUM_OUTPUTS; i++) {
if (onFocus[i]) {
this->ccNum[i] = data1;
}
}
for (int i = 0; i < NUM_OUTPUTS; i++) {
if (data1 == ccNum[i]) {
this->cc[i] = data2;
}
}
}
}

struct CCTextField : TextField {
void onTextChange();

void draw(NVGcontext *vg);

void onMouseDownOpaque(int button);

void onMouseUpOpaque(int button);

void onMouseLeave();

int *ccNum;
bool *inited;
bool *onFocus;
};

void CCTextField::draw(NVGcontext *vg) {
/* This is necessary, since the save
* file is loaded after constructing the widget*/
if (*inited) {
*inited = false;
text = std::to_string(*ccNum);
}

if (*onFocus) {
text = std::to_string(*ccNum);
}

TextField::draw(vg);
}

void CCTextField::onMouseUpOpaque(int button) {
if (button == 1) {
*onFocus = false;
}

}

void CCTextField::onMouseDownOpaque(int button) {
if (button == 1) {
*onFocus = true;
}
}

void CCTextField::onMouseLeave() {
*onFocus = false;
}


void CCTextField::onTextChange() {
if (text.size() > 0) {
try {
*ccNum = std::stoi(text);
// Only allow valid cc numbers
if (*ccNum < 0 || *ccNum > 127 || text.size() > 3) {
text = "";
begin = end = 0;
*ccNum = -1;
}
} catch (...) {
text = "";
begin = end = 0;
*ccNum = -1;
}
};
}

MIDICCToCVWidget::MIDICCToCVWidget() {
MIDICCToCVInterface *module = new MIDICCToCVInterface();
setModule(module);
box.size = Vec(16 * 15, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));
{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 11 * 15, margin);
label->text = "MIDI CC to CV";
addChild(label);
yPos = labelHeight * 2;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
midiChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Channel";
addChild(label);

ChannelChoice *channelChoice = new ChannelChoice();
channelChoice->midiModule = dynamic_cast<MidiIO *>(module);
channelChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
channelChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(channelChoice);
yPos += channelChoice->box.size.y + margin * 3;
}

for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) {
CCTextField *ccNumChoice = new CCTextField();
ccNumChoice->ccNum = &module->ccNum[i];
ccNumChoice->inited = &module->ccNumInited[i];
ccNumChoice->onFocus = &module->onFocus[i];
ccNumChoice->text = std::to_string(module->ccNum[i]);
ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos);
ccNumChoice->box.size.x = 29;

addChild(ccNumChoice);

yPos += labelHeight + margin;
addOutput(createOutput<PJ3410Port>(Vec((i % 4) * (63) + 10, yPos + 5), module, i));

if ((i + 1) % 4 == 0) {
yPos += 47 + margin;
} else {
yPos -= labelHeight + margin;
}
}
}

void MIDICCToCVWidget::step() {

ModuleWidget::step();
}

+ 336
- 0
src/core/MidiClockToCV.cpp View File

@@ -0,0 +1,336 @@
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "MidiIO.hpp"
#include "dsp/digital.hpp"

using namespace rack;

struct MIDIClockToCVInterface : MidiIO, Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
CLOCK1_RATIO,
CLOCK2_RATIO,
NUM_INPUTS
};
enum OutputIds {
CLOCK1_PULSE,
CLOCK2_PULSE,
RESET_PULSE,
NUM_OUTPUTS
};

int clock1ratio = 0;
int clock2ratio = 0;

PulseGenerator clock1Pulse;
PulseGenerator clock2Pulse;
PulseGenerator resetPulse;
bool tick = false;
bool running = false;
bool reset = false;


MIDIClockToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {

}

~MIDIClockToCVInterface() {
}

void step();

void processMidi(std::vector<unsigned char> msg);

void onDeviceChange();

void resetMidi();

json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
json_object_set_new(rootJ, "clock1ratio", json_integer(clock1ratio));
json_object_set_new(rootJ, "clock2ratio", json_integer(clock2ratio));
return rootJ;
}

void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
json_t *c1rJ = json_object_get(rootJ, "clock1ratio");
if (c1rJ) {
clock1ratio = json_integer_value(c1rJ);
}

json_t *c2rJ = json_object_get(rootJ, "clock2ratio");
if (c2rJ) {
clock2ratio = json_integer_value(c2rJ);
}
}
};

void MIDIClockToCVInterface::step() {
static int c_bar = 0;
static float trigger_length = 0.05;
static float sampleRate = engineGetSampleRate();

/* Note this is in relation to the Midi clock's Tick (6x per 16th note).
* Therefore, e.g. the 2:3 is calculated:
*
* 24 (Ticks per quarter note) * 2 / 3 = 16
*
* Implying that every 16 midi clock ticks we need to send a pulse
* */
static int ratios[] = {6, 8, 12, 16, 24, 32, 48, 96, 192};
static int numratios = sizeof(ratios) / sizeof(*ratios);

if (isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue
getMessage(&message);
while (message.size() > 0) {
processMidi(message);
getMessage(&message);
}
}

if (inputs[CLOCK1_RATIO].active) {
clock1ratio = int(clampf(inputs[CLOCK1_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10);
}

if (inputs[CLOCK2_RATIO].active) {
clock2ratio = int(clampf(inputs[CLOCK2_RATIO].value, 0.0, 10.0) * (numratios - 1) / 10);
}

if (reset) {
resetPulse.trigger(trigger_length);
reset = false;
c_bar = 0;
clock1Pulse.time = 0.0;
clock1Pulse.pulseTime = 0.0;
clock2Pulse.time = 0.0;
clock2Pulse.pulseTime = 0.0;
}

if (tick) {
tick = false;

/* Note: At least for my midi clock, the clock ticks are sent
* even if the midi clock is stopped.
* Therefore, we need to keep track of ticks even when the clock
* is stopped. Otherwise we can run into weird timing issues.
*/
if (running) {
if (c_bar % ratios[clock1ratio] == 0) {
clock1Pulse.trigger(trigger_length);
}

if (c_bar % ratios[clock2ratio] == 0) {
clock2Pulse.trigger(trigger_length);
}
}

c_bar++;

// One "midi bar" = 4 whole notes = (6 ticks per 16th) 6 * 16 *4 = 384
if (c_bar >= 384) {
c_bar = 0;
}
}


bool pulse = clock1Pulse.process(1.0 / sampleRate);
outputs[CLOCK1_PULSE].value = pulse ? 10.0 : 0.0;

pulse = clock2Pulse.process(1.0 / sampleRate);
outputs[CLOCK2_PULSE].value = pulse ? 10.0 : 0.0;

pulse = resetPulse.process(1.0 / sampleRate);
outputs[RESET_PULSE].value = pulse ? 10.0 : 0.0;

}

void MIDIClockToCVInterface::resetMidi() {
outputs[CLOCK1_PULSE].value = 0.0;
}

void MIDIClockToCVInterface::processMidi(std::vector<unsigned char> msg) {

switch (msg[0]) {
case 0xfa:
reset = true;
running = true;
break;
case 0xfc:
running = false;
break;
case 0xf8:
tick = true;
break;
}


}

void MIDIClockToCVInterface::onDeviceChange() {
setIgnores(true, false);
}

struct ClockRatioItem : MenuItem {
int ratio;
int *clockRatio;

void onAction() {
*clockRatio = ratio;
}
};

struct ClockRatioChoice : ChoiceButton {
int *clockRatio;
const std::vector<std::string> ratioNames = {"Sixteenth note (1:4 ratio)", "Eighth note triplet (1:3 ratio)",
"Eighth note (1:2 ratio)", "Quarter note triplet (2:3 ratio)",
"Quarter note (tap speed)", "Half note triplet (4:3 ratio)",
"Half note (2:1 ratio)", "Whole note (4:1 ratio)",
"Two whole notes (8:1 ratio)"};

const std::vector<std::string> ratioNames_short = {"1:4 ratio", "1:3 ratio", "1:2 ratio", "2:3 ratio", "1:1 ratio",
"4:3", "2:1 ratio", "4:1 ratio", "8:1 ratio"};

void onAction() {
Menu *menu = gScene->createMenu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

for (unsigned long ratio = 0; ratio < ratioNames.size(); ratio++) {
ClockRatioItem *clockRatioItem = new ClockRatioItem();
clockRatioItem->ratio = ratio;
clockRatioItem->clockRatio = clockRatio;
clockRatioItem->text = ratioNames[ratio];
menu->pushChild(clockRatioItem);
}
}

void step() {
text = ratioNames_short[*clockRatio];
}
};

MIDIClockToCVWidget::MIDIClockToCVWidget() {
MIDIClockToCVInterface *module = new MIDIClockToCVInterface();
setModule(module);
box.size = Vec(15 * 9, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));

{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 7 * 15, margin);
label->text = "MIDI Clock to CV";
addChild(label);
yPos = labelHeight * 2;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);
yPos += labelHeight + margin;

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec(margin, yPos);
midiChoice->box.size.x = box.size.x - 10;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin * 6;
}


{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "C1 Ratio";
addChild(label);

addInput(createInput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_RATIO));

yPos += margin * 6;

ClockRatioChoice *ratioChoice = new ClockRatioChoice();
ratioChoice->clockRatio = &module->clock1ratio;
ratioChoice->box.pos = Vec(margin, yPos);
ratioChoice->box.size.x = box.size.x - 10;
addChild(ratioChoice);
yPos += ratioChoice->box.size.y + margin * 2;

}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "C1 Pulse";
addChild(label);

addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK1_PULSE));
yPos += margin * 10;
}


{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "C2 Ratio";
addChild(label);

addInput(createInput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_RATIO));

yPos += margin * 6;

ClockRatioChoice *ratioChoice = new ClockRatioChoice();
ratioChoice->clockRatio = &module->clock2ratio;
ratioChoice->box.pos = Vec(margin, yPos);
ratioChoice->box.size.x = box.size.x - 10;
addChild(ratioChoice);
yPos += ratioChoice->box.size.y + margin * 2;

}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "C2 Pulse";
addChild(label);

addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::CLOCK2_PULSE));
yPos += labelHeight + margin * 7;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Reset";
addChild(label);
addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, MIDIClockToCVInterface::RESET_PULSE));
}
}

void MIDIClockToCVWidget::step() {

ModuleWidget::step();
}

+ 241
- 0
src/core/MidiIO.cpp View File

@@ -0,0 +1,241 @@
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "MidiIO.hpp"

using namespace rack;


/**
* MidiIO implements the shared functionality of all midi modules, namely:
* + Channel Selection (including helper for storing json)
* + Interface Selection (including helper for storing json)
* + rtMidi initialisation (input or output)
*/
MidiIO::MidiIO(bool isOut) {
channel = -1;
this->isOut = isOut;

if (isOut) {
fprintf(stderr, "Midi Out is currently not supported (will be added soon)");
}
};

void MidiIO::setChannel(int channel) {
this->channel = channel;
}

std::unordered_map<std::string, MidiInWrapper *> MidiIO::midiInMap = {};

json_t *MidiIO::addBaseJson(json_t *rootJ) {
if (deviceName != "") {
json_object_set_new(rootJ, "interfaceName", json_string(deviceName.c_str()));
json_object_set_new(rootJ, "channel", json_integer(channel));
}
return rootJ;
}

void MidiIO::baseFromJson(json_t *rootJ) {
json_t *portNameJ = json_object_get(rootJ, "interfaceName");
if (portNameJ) {
openDevice(json_string_value(portNameJ));
}

json_t *channelJ = json_object_get(rootJ, "channel");
if (channelJ) {
setChannel(json_integer_value(channelJ));
}
}

std::vector<std::string> MidiIO::getDevices() {
/* Note: we could also use an existing interface if one exists */
static RtMidiIn *m = new RtMidiIn();

std::vector<std::string> names = {};

for (unsigned int i = 0; i < m->getPortCount(); i++) {
names.push_back(m->getPortName(i));
}

return names;
}

void MidiIO::openDevice(std::string deviceName) {

MidiInWrapper *mw = midiInMap[deviceName];

if (this->id > 0 || this->deviceName != "") {
close();
}

if (!mw) {
try {
mw = new MidiInWrapper();
midiInMap[deviceName] = mw;


for (unsigned int i = 0; i < mw->getPortCount(); i++) {
if (deviceName == mw->getPortName(i)) {
mw->openPort(i);
break;
}
}
}
catch (RtMidiError &error) {
fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str());
this->deviceName = "";
this->id = -1;
return;
}
}

this->deviceName = deviceName;

id = midiInMap[deviceName]->add();
onDeviceChange();
}

void MidiIO::setIgnores(bool ignoreSysex, bool ignoreTime, bool ignoreSense) {
bool sy = true, ti = true, se = true;

midiInMap[deviceName]->ignoresMap[id][0] = ignoreSysex;
midiInMap[deviceName]->ignoresMap[id][1] = ignoreTime;
midiInMap[deviceName]->ignoresMap[id][2] = ignoreSense;

for (auto kv : midiInMap[deviceName]->ignoresMap) {
sy = sy && kv.second[0];
ti = ti && kv.second[1];
se = se && kv.second[2];
}

midiInMap[deviceName]->ignoreTypes(se,ti,se);


}

std::string MidiIO::getDeviceName() {
return deviceName;
}

double MidiIO::getMessage(std::vector<unsigned char> *msg) {
std::vector<unsigned char> next_msg;

MidiInWrapper *mw = midiInMap[deviceName];

if (!mw) {
fprintf(stderr, "Device not opened!: %s\n", deviceName.c_str());
return 0;
}

double stamp = midiInMap[deviceName]->getMessage(&next_msg);

if (next_msg.size() > 0) {
for (auto kv : mw->idMessagesMap) {
mw->idMessagesMap[kv.first].push_back(next_msg);
mw->idStampsMap[kv.first].push_back(stamp);
}
}

if (mw->idMessagesMap[id].size() <= 0) {
*msg = next_msg;
return stamp;
}

*msg = mw->idMessagesMap[id].front();
stamp = mw->idStampsMap[id].front();
mw->idMessagesMap[id].pop_front();
return stamp;
}

bool MidiIO::isPortOpen() {
return id > 0;
}

void MidiIO::close() {

MidiInWrapper *mw = midiInMap[deviceName];

if (!mw || id < 0) {
//fprintf(stderr, "Trying to close already closed device!\n");
return;
}

setIgnores(); // reset ignore types for this instance

mw->erase(id);

if (mw->subscribers == 0) {
mw->closePort();
midiInMap.erase(deviceName);
}

id = -1;
deviceName = "";
}


void MidiItem::onAction() {
midiModule->resetMidi(); // reset Midi values
midiModule->openDevice(text);
}

void MidiChoice::onAction() {
Menu *menu = gScene->createMenu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

{
MidiItem *midiItem = new MidiItem();
midiItem->midiModule = midiModule;
midiItem->text = "";
menu->pushChild(midiItem);
}

std::vector<std::string> deviceNames = midiModule->getDevices();
for (unsigned int i = 0; i < deviceNames.size(); i++) {
MidiItem *midiItem = new MidiItem();
midiItem->midiModule = midiModule;
midiItem->text = deviceNames[i];
menu->pushChild(midiItem);
}
}

void MidiChoice::step() {
if (midiModule->getDeviceName() == "") {
text = "No Device";
return;
}
std::string name = midiModule->getDeviceName();
text = ellipsize(name, 15);
}

void ChannelItem::onAction() {
midiModule->resetMidi(); // reset Midi values
midiModule->setChannel(channel);
}

void ChannelChoice::onAction() {
Menu *menu = gScene->createMenu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

{
ChannelItem *channelItem = new ChannelItem();
channelItem->midiModule = midiModule;
channelItem->channel = -1;
channelItem->text = "All";
menu->pushChild(channelItem);
}
for (int channel = 0; channel < 16; channel++) {
ChannelItem *channelItem = new ChannelItem();
channelItem->midiModule = midiModule;
channelItem->channel = channel;
channelItem->text = stringf("%d", channel + 1);
menu->pushChild(channelItem);
}
}

void ChannelChoice::step() {
text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All";
}

+ 154
- 0
src/core/MidiIO.hpp View File

@@ -0,0 +1,154 @@
#include <unordered_map>
#include "rack.hpp"
#include "rtmidi/RtMidi.h"


using namespace rack;


/**
* This class allows to use one instance of rtMidiIn with
* multiple modules. A MidiIn port will be opened only once while multiple
* instances can use it simultaniously, each receiving all its incoming messages.
*/

struct MidiInWrapper : RtMidiIn {
std::unordered_map<int, std::list<std::vector<unsigned char>>> idMessagesMap;
std::unordered_map<int, std::list<double>> idStampsMap;

/* Stores Ignore settings for each instance in the following order:
* {ignore_midiSysex, ignore_midiTime, ignore_midiSense}
*/
std::unordered_map<int, bool[3]> ignoresMap;

int uid_c = 0;
int subscribers = 0;

MidiInWrapper() : RtMidiIn() {
idMessagesMap = {};
idStampsMap = {};
};

int add() {
int id = ++uid_c;
subscribers++;
idMessagesMap[id] = {};
idStampsMap[id] = {};

ignoresMap[id][0] = true;
ignoresMap[id][1] = true;
ignoresMap[id][2] = true;
return id;
}

void erase(int id) {
subscribers--;
idMessagesMap.erase(id);
idStampsMap.erase(id);
ignoresMap.erase(id);
}
};

struct MidiIO {
private:
static std::unordered_map<std::string, MidiInWrapper *> midiInMap;
/* TODO: add for midi out*/
int id = -1;
std::string deviceName = "";
bool isOut = false;

public:
int channel;


MidiIO(bool isOut = false);

~MidiIO() {
close();
}

std::vector<std::string> getDevices();

void openDevice(std::string deviceName);

void setIgnores(bool ignoreSysex = true, bool ignoreTime = true, bool ignoreSense = true);

std::string getDeviceName();

void setChannel(int channel);

double getMessage(std::vector<unsigned char> *msg);

json_t *addBaseJson(json_t *rootJ);

void baseFromJson(json_t *rootJ);

bool isPortOpen();

void close();

/* called when midi port is set */
virtual void resetMidi()=0;

/* called if a user switches or sets the deivce (and after this device is initialised)*/
virtual void onDeviceChange(){};
};

//////////////////////
// MIDI module widgets
//////////////////////

struct MidiItem : MenuItem {
MidiIO *midiModule;

void onAction();
};

struct MidiChoice : ChoiceButton {
MidiIO *midiModule;

void onAction();

void step();
};

struct ChannelItem : MenuItem {
MidiIO *midiModule;
int channel;

void onAction();
};

struct ChannelChoice : ChoiceButton {
MidiIO *midiModule;

void onAction();

void step();
};


struct MidiToCVWidget : ModuleWidget {
MidiToCVWidget();

void step();
};

struct MIDICCToCVWidget : ModuleWidget {
MIDICCToCVWidget();

void step();
};

struct MIDIClockToCVWidget : ModuleWidget {
MIDIClockToCVWidget();

void step();
};

struct MIDITriggerToCVWidget : ModuleWidget {
MIDITriggerToCVWidget();

void step();
};


+ 0
- 712
src/core/MidiInterface.cpp View File

@@ -1,712 +0,0 @@
#include <assert.h>
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "gui.hpp"
#include "engine.hpp"
#include "dsp/digital.hpp"


using namespace rack;

/**
* MidiIO implements the shared functionality of all midi modules, namely:
* + Channel Selection (including helper for storing json)
* + Interface Selection (including helper for storing json)
* + rtMidi initialisation (input or output)
*/
struct MidiIO {
int portId = -1;
RtMidi *rtMidi = NULL;

/** Filter MIDI channel
-1 means all MIDI channels
*/
int channel = -1;

/*
* If isOut is set to true, creates a RtMidiOut, RtMidiIn otherwise
*/
MidiIO(bool isOut = false) {
try {
if (isOut) {
rtMidi = new RtMidiOut(RtMidi::UNSPECIFIED, "Rack");
} else {
rtMidi = new RtMidiIn(RtMidi::UNSPECIFIED, "Rack");
}
}
catch (RtMidiError &error) {
fprintf(stderr, "Failed to create RtMidiIn: %s\n", error.getMessage().c_str());
}
}

~MidiIO() {}

int getPortCount();

std::string getPortName(int portId);

// -1 will close the port
void setPortId(int portId);

void setChannel(int channel) {
this->channel = channel;
}

json_t *addBaseJson(json_t *rootJ) {
if (portId >= 0) {
std::string portName = getPortName(portId);
json_object_set_new(rootJ, "portName", json_string(portName.c_str()));
json_object_set_new(rootJ, "channel", json_integer(channel));
}
return rootJ;
}

void baseFromJson(json_t *rootJ) {
json_t *portNameJ = json_object_get(rootJ, "portName");
if (portNameJ) {
std::string portName = json_string_value(portNameJ);
for (int i = 0; i < getPortCount(); i++) {
if (portName == getPortName(i)) {
setPortId(i);
break;
}
}
}

json_t *channelJ = json_object_get(rootJ, "channel");
if (channelJ) {
setChannel(json_integer_value(channelJ));
}
}

virtual void resetMidi()=0; // called when midi port is set
};

int MidiIO::getPortCount() {
return rtMidi->getPortCount();
}

std::string MidiIO::getPortName(int portId) {
std::string portName;
try {
portName = rtMidi->getPortName(portId);
}
catch (RtMidiError &error) {
fprintf(stderr, "Failed to get Port Name: %d, %s\n", portId, error.getMessage().c_str());
}
return portName;
}

void MidiIO::setPortId(int portId) {

// Close port if it was previously opened
if (rtMidi->isPortOpen()) {
rtMidi->closePort();
}
this->portId = -1;

// Open new port
if (portId >= 0) {
rtMidi->openPort(portId, "Midi Interface");
}
this->portId = portId;
}

struct MidiItem : MenuItem {
MidiIO *midiModule;
int portId;

void onAction() {
midiModule->resetMidi(); // reset Midi values
midiModule->setPortId(portId);
}
};

struct MidiChoice : ChoiceButton {
MidiIO *midiModule;

void onAction() {
Menu *menu = gScene->createMenu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

int portCount = midiModule->getPortCount();
{
MidiItem *midiItem = new MidiItem();
midiItem->midiModule = midiModule;
midiItem->portId = -1;
midiItem->text = "No device";
menu->pushChild(midiItem);
}
for (int portId = 0; portId < portCount; portId++) {
MidiItem *midiItem = new MidiItem();
midiItem->midiModule = midiModule;
midiItem->portId = portId;
midiItem->text = midiModule->getPortName(portId);
menu->pushChild(midiItem);
}
}

void step() {
if (midiModule->portId < 0) {
text = "No Device";
return;
}
std::string name = midiModule->getPortName(midiModule->portId);
text = ellipsize(name, 15);
}
};

struct ChannelItem : MenuItem {
MidiIO *midiModule;
int channel;

void onAction() {
midiModule->resetMidi(); // reset Midi values
midiModule->setChannel(channel);
}
};

struct ChannelChoice : ChoiceButton {
MidiIO *midiModule;

void onAction() {
Menu *menu = gScene->createMenu();
menu->box.pos = getAbsolutePos().plus(Vec(0, box.size.y));
menu->box.size.x = box.size.x;

{
ChannelItem *channelItem = new ChannelItem();
channelItem->midiModule = midiModule;
channelItem->channel = -1;
channelItem->text = "All";
menu->pushChild(channelItem);
}
for (int channel = 0; channel < 16; channel++) {
ChannelItem *channelItem = new ChannelItem();
channelItem->midiModule = midiModule;
channelItem->channel = channel;
channelItem->text = stringf("%d", channel + 1);
menu->pushChild(channelItem);
}
}

void step() {
text = (midiModule->channel >= 0) ? stringf("%d", midiModule->channel + 1) : "All";
}
};

/*
* MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to
* CV
*/
struct MIDIToCVInterface : MidiIO, Module {
enum ParamIds {
RESET_PARAM,
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
PITCH_OUTPUT = 0,
GATE_OUTPUT,
VELOCITY_OUTPUT,
MOD_OUTPUT,
PITCHWHEEL_OUTPUT,
CHANNEL_AFTERTOUCH_OUTPUT,
NUM_OUTPUTS
};

std::list<int> notes;
bool pedal = false;
int note = 60; // C4, most modules should use 261.626 Hz
int mod = 0;
int vel = 0;
int afterTouch = 0;
int pitchWheel = 64;
bool retrigger = false;
bool retriggered = false;

SchmittTrigger resetTrigger;
float resetLight = 0.0;

MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {

}

~MIDIToCVInterface() {
setPortId(-1);
};

void step();

void pressNote(int note);

void releaseNote(int note);

void processMidi(std::vector<unsigned char> msg);

virtual json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
return rootJ;
}

virtual void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
}

virtual void reset() {
setPortId(-1);
}

virtual void resetMidi();

};

void MIDIToCVInterface::resetMidi() {
mod = 0;
pitchWheel = 64;
afterTouch = 0;
vel = 0;
resetLight = 1.0;
outputs[GATE_OUTPUT].value = 0.0;
notes.clear();
}

void MIDIToCVInterface::step() {
if (rtMidi->isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue

dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message);
while (message.size() > 0) {
processMidi(message);
dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message);
}
}

outputs[PITCH_OUTPUT].value = ((note - 60)) / 12.0;

bool gate = pedal || !notes.empty();
if (retrigger && retriggered) {
gate = false;
retriggered = false;
}
if (resetTrigger.process(params[RESET_PARAM].value)) {
resetMidi();
return;
}

if (resetLight > 0) {
resetLight -= resetLight / 0.55 / engineGetSampleRate(); // fade out light
}


outputs[GATE_OUTPUT].value = gate ? 10.0 : 0.0;
outputs[MOD_OUTPUT].value = mod / 127.0 * 10.0;
outputs[PITCHWHEEL_OUTPUT].value = (pitchWheel - 64) / 64.0 * 10.0;
outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch / 127.0 * 10.0;
outputs[VELOCITY_OUTPUT].value = vel / 127.0 * 10.0;

}

void MIDIToCVInterface::pressNote(int note) {
// Remove existing similar note
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);
// Push note
notes.push_back(note);
this->note = note;
retriggered = true;
}

void MIDIToCVInterface::releaseNote(int note) {
// Remove the note
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);

if (pedal) {
// Don't release if pedal is held
} else if (!notes.empty()) {
// Play previous note
auto it2 = notes.end();
it2--;
this->note = *it2;
retriggered = true;
}
}

void MIDIToCVInterface::processMidi(std::vector<unsigned char> msg) {
int channel = msg[0] & 0xf;
int status = (msg[0] >> 4) & 0xf;
int data1 = msg[1];
int data2 = msg[2];

//fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2);

// Filter channels
if (this->channel >= 0 && this->channel != channel)
return;

switch (status) {
// note off
case 0x8: {
releaseNote(data1);
}
break;
case 0x9: // note on
if (data2 > 0) {
pressNote(data1);
this->vel = data2;
} else {
// For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released.
releaseNote(data1);
}
break;
case 0xb: // cc
switch (data1) {
case 0x01: // mod
this->mod = data2;
break;
case 0x40: // sustain
pedal = (data2 >= 64);
releaseNote(-1);
break;
}
break;
case 0xe: // pitch wheel
this->pitchWheel = data2;
break;
case 0xd: // channel aftertouch
this->afterTouch = data1;
break;
}
}


MidiToCVWidget::MidiToCVWidget() {
MIDIToCVInterface *module = new MIDIToCVInterface();
setModule(module);
box.size = Vec(15 * 9, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;
float yGap = 35;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));

{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 7 * 15, margin);
label->text = "MIDI to CV";
addChild(label);
yPos = labelHeight * 2;
}

addParam(createParam<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0));
addChild(createValueLight<SmallLight<RedValueLight>>(Vec(7 * 15 + 5, labelHeight + 5), &module->resetLight));
{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);
yPos += labelHeight + margin;

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec(margin, yPos);
midiChoice->box.size.x = box.size.x - 10;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Channel";
addChild(label);
yPos += labelHeight + margin;

ChannelChoice *channelChoice = new ChannelChoice();
channelChoice->midiModule = dynamic_cast<MidiIO *>(module);
channelChoice->box.pos = Vec(margin, yPos);
channelChoice->box.size.x = box.size.x - 10;
addChild(channelChoice);
yPos += channelChoice->box.size.y + margin + 15;
}

std::string labels[MIDIToCVInterface::NUM_OUTPUTS] = {"1V/oct", "Gate", "Velocity", "Mod Wheel", "Pitch Wheel",
"Aftertouch"};

for (int i = 0; i < MIDIToCVInterface::NUM_OUTPUTS; i++) {
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = labels[i];
addChild(label);

addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, i));

yPos += yGap + margin;
}
}

void MidiToCVWidget::step() {
// Assume QWERTY
#define MIDI_KEY(key, midi) if (glfwGetKey(gWindow, key)) printf("%d\n", midi);

// MIDI_KEY(GLFW_KEY_Z, 48);

ModuleWidget::step();
}


/*
* MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to
* CV
*/
struct MIDICCToCVInterface : MidiIO, Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
NUM_OUTPUTS = 16
};

int cc[NUM_OUTPUTS];
int ccNum[NUM_OUTPUTS];
bool ccNumInited[NUM_OUTPUTS];

MIDICCToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {
for (int i = 0; i < NUM_OUTPUTS; i++) {
cc[i] = 0;
ccNum[i] = i;
}
}

~MIDICCToCVInterface() {
setPortId(-1);
}

void step();

void processMidi(std::vector<unsigned char> msg);

virtual void resetMidi();

virtual json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(ccNum[i]));
}
return rootJ;
}

virtual void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str());
if (ccNumJ) {
ccNum[i] = json_integer_value(ccNumJ);
ccNumInited[i] = true;
}

}
}

virtual void reset() {
setPortId(-1);
}

};


void MIDICCToCVInterface::step() {
if (rtMidi->isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue

dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message);
while (message.size() > 0) {
processMidi(message);
dynamic_cast<RtMidiIn *>(rtMidi)->getMessage(&message);
}
}

for (int i = 0; i < NUM_OUTPUTS; i++) {
outputs[i].value = cc[i] / 127.0 * 10.0;
}
}

void MIDICCToCVInterface::resetMidi() {
for (int i = 0; i < NUM_OUTPUTS; i++) {
cc[i] = 0;
}
};

void MIDICCToCVInterface::processMidi(std::vector<unsigned char> msg) {
int channel = msg[0] & 0xf;
int status = (msg[0] >> 4) & 0xf;
int data1 = msg[1];
int data2 = msg[2];

//fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2);

// Filter channels
if (this->channel >= 0 && this->channel != channel)
return;

if (status == 0xb) {
for (int i = 0; i < NUM_OUTPUTS; i++) {
if (data1 == ccNum[i]) {
this->cc[i] = data2;
}

}
}
}


struct CCTextField : TextField {
void onTextChange();

void draw(NVGcontext *vg);

int *ccNum;
bool *inited;
};

void CCTextField::draw(NVGcontext *vg) {
/* This is necessary, since the save
* file is loaded after constructing the widget*/
if (*inited) {
*inited = false;
text = std::to_string(*ccNum);
}

TextField::draw(vg);
}

void CCTextField::onTextChange() {
if (text.size() > 0) {
try {
*ccNum = std::stoi(text);
// Only allow valid cc numbers
if (*ccNum < 0 || *ccNum > 127 || text.size() > 3) {
text = "";
begin = end = 0;
*ccNum = -1;
}
} catch (...) {
text = "";
begin = end = 0;
*ccNum = -1;
}
};
}

MIDICCToCVWidget::MIDICCToCVWidget() {
MIDICCToCVInterface *module = new MIDICCToCVInterface();
setModule(module);
box.size = Vec(16 * 15, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));
{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 11 * 15, margin);
label->text = "MIDI CC to CV";
addChild(label);
yPos = labelHeight * 2;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
midiChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Channel";
addChild(label);

ChannelChoice *channelChoice = new ChannelChoice();
channelChoice->midiModule = dynamic_cast<MidiIO *>(module);
channelChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
channelChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(channelChoice);
yPos += channelChoice->box.size.y + margin * 3;
}

for (int i = 0; i < MIDICCToCVInterface::NUM_OUTPUTS; i++) {
CCTextField *ccNumChoice = new CCTextField();
ccNumChoice->ccNum = &module->ccNum[i];
ccNumChoice->inited = &module->ccNumInited[i];
ccNumChoice->text = std::to_string(module->ccNum[i]);
ccNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos);
ccNumChoice->box.size.x = 29;

addChild(ccNumChoice);

yPos += labelHeight + margin;
addOutput(createOutput<PJ3410Port>(Vec((i % 4) * (63) + 10, yPos + 5), module, i));

if ((i + 1) % 4 == 0) {
yPos += 47 + margin;
} else {
yPos -= labelHeight + margin;
}
}
}

void MIDICCToCVWidget::step() {
// Assume QWERTY
#define MIDI_KEY(key, midi) if (glfwGetKey(gWindow, key)) printf("%d\n", midi);

// MIDI_KEY(GLFW_KEY_Z, 48);

ModuleWidget::step();
}

+ 280
- 0
src/core/MidiToCV.cpp View File

@@ -0,0 +1,280 @@
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "MidiIO.hpp"
#include "dsp/digital.hpp"

/*
* MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod wheel to
* CV
*/
struct MIDIToCVInterface : MidiIO, Module {
enum ParamIds {
RESET_PARAM,
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
PITCH_OUTPUT = 0,
GATE_OUTPUT,
VELOCITY_OUTPUT,
MOD_OUTPUT,
PITCHWHEEL_OUTPUT,
CHANNEL_AFTERTOUCH_OUTPUT,
NUM_OUTPUTS
};

std::list<int> notes;
bool pedal = false;
int note = 60; // C4, most modules should use 261.626 Hz
int mod = 0;
int vel = 0;
int afterTouch = 0;
int pitchWheel = 64;
bool retrigger = false;
bool retriggered = false;

SchmittTrigger resetTrigger;
float resetLight = 0.0;

MIDIToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {

}

~MIDIToCVInterface() {
};

void step();

void pressNote(int note);

void releaseNote(int note);

void processMidi(std::vector<unsigned char> msg);

json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
return rootJ;
}

void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
}

void reset() {
resetMidi();
}

void resetMidi();

};

void MIDIToCVInterface::resetMidi() {
mod = 0;
pitchWheel = 64;
afterTouch = 0;
vel = 0;
resetLight = 1.0;
outputs[GATE_OUTPUT].value = 0.0;
notes.clear();
}

void MIDIToCVInterface::step() {
static float sampleRate = engineGetSampleRate();
if (isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue
getMessage(&message);
while (message.size() > 0) {
processMidi(message);
getMessage(&message);
}
}

outputs[PITCH_OUTPUT].value = ((note - 60)) / 12.0;

bool gate = pedal || !notes.empty();
if (retrigger && retriggered) {
gate = false;
retriggered = false;
}
if (resetTrigger.process(params[RESET_PARAM].value)) {
resetMidi();
return;
}

if (resetLight > 0) {
resetLight -= resetLight / 0.55 / sampleRate; // fade out light
}


outputs[GATE_OUTPUT].value = gate ? 10.0 : 0.0;
outputs[MOD_OUTPUT].value = mod / 127.0 * 10.0;
outputs[PITCHWHEEL_OUTPUT].value = (pitchWheel - 64) / 64.0 * 10.0;
outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch / 127.0 * 10.0;
outputs[VELOCITY_OUTPUT].value = vel / 127.0 * 10.0;

}

void MIDIToCVInterface::pressNote(int note) {
// Remove existing similar note
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);
// Push note
notes.push_back(note);
this->note = note;
retriggered = true;
}

void MIDIToCVInterface::releaseNote(int note) {
// Remove the note
auto it = std::find(notes.begin(), notes.end(), note);
if (it != notes.end())
notes.erase(it);

if (pedal) {
// Don't release if pedal is held
} else if (!notes.empty()) {
// Play previous note
auto it2 = notes.end();
it2--;
this->note = *it2;
retriggered = true;
}
}

void MIDIToCVInterface::processMidi(std::vector<unsigned char> msg) {
int channel = msg[0] & 0xf;
int status = (msg[0] >> 4) & 0xf;
int data1 = msg[1];
int data2 = msg[2];

//fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2);

// Filter channels
if (this->channel >= 0 && this->channel != channel)
return;

switch (status) {
// note off
case 0x8: {
releaseNote(data1);
}
break;
case 0x9: // note on
if (data2 > 0) {
pressNote(data1);
this->vel = data2;
} else {
// For some reason, some keyboards send a "note on" event with a velocity of 0 to signal that the key has been released.
releaseNote(data1);
}
break;
case 0xb: // cc
switch (data1) {
case 0x01: // mod
this->mod = data2;
break;
case 0x40: // sustain
pedal = (data2 >= 64);
releaseNote(-1);
break;
}
break;
case 0xe: // pitch wheel
this->pitchWheel = data2;
break;
case 0xd: // channel aftertouch
this->afterTouch = data1;
break;
}
}


MidiToCVWidget::MidiToCVWidget() {
MIDIToCVInterface *module = new MIDIToCVInterface();
setModule(module);
box.size = Vec(15 * 9, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;
float yGap = 35;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));

{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 7 * 15, margin);
label->text = "MIDI to CV";
addChild(label);
yPos = labelHeight * 2;
}

addParam(createParam<LEDButton>(Vec(7 * 15, labelHeight), module, MIDIToCVInterface::RESET_PARAM, 0.0, 1.0, 0.0));
addChild(createValueLight<SmallLight<RedValueLight>>(Vec(7 * 15 + 5, labelHeight + 5), &module->resetLight));
{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);
yPos += labelHeight + margin;

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec(margin, yPos);
midiChoice->box.size.x = box.size.x - 10;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Channel";
addChild(label);
yPos += labelHeight + margin;

ChannelChoice *channelChoice = new ChannelChoice();
channelChoice->midiModule = dynamic_cast<MidiIO *>(module);
channelChoice->box.pos = Vec(margin, yPos);
channelChoice->box.size.x = box.size.x - 10;
addChild(channelChoice);
yPos += channelChoice->box.size.y + margin + 15;
}

std::string labels[MIDIToCVInterface::NUM_OUTPUTS] = {"1V/oct", "Gate", "Velocity", "Mod Wheel", "Pitch Wheel",
"Aftertouch"};

for (int i = 0; i < MIDIToCVInterface::NUM_OUTPUTS; i++) {
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = labels[i];
addChild(label);

addOutput(createOutput<PJ3410Port>(Vec(15 * 6, yPos - 5), module, i));

yPos += yGap + margin;
}
}

void MidiToCVWidget::step() {

ModuleWidget::step();
}

+ 285
- 0
src/core/MidiTriggerToCV.cpp View File

@@ -0,0 +1,285 @@
#include <list>
#include <algorithm>
#include "rtmidi/RtMidi.h"
#include "core.hpp"
#include "MidiIO.hpp"
#include "dsp/digital.hpp"

using namespace rack;

/*
* MIDIToCVInterface converts midi note on/off events, velocity , channel aftertouch, pitch wheel and mod weel to
* CV
*/
struct MIDITriggerToCVInterface : MidiIO, Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
NUM_INPUTS
};
enum OutputIds {
NUM_OUTPUTS = 16
};

int trigger[NUM_OUTPUTS];
int triggerNum[NUM_OUTPUTS];
bool triggerNumInited[NUM_OUTPUTS];
bool onFocus[NUM_OUTPUTS];

MIDITriggerToCVInterface() : MidiIO(), Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) {
for (int i = 0; i < NUM_OUTPUTS; i++) {
trigger[i] = 0;
triggerNum[i] = i;
onFocus[i] = false;
}
}

~MIDITriggerToCVInterface() {
}

void step();

void processMidi(std::vector<unsigned char> msg);

void resetMidi();

virtual json_t *toJson() {
json_t *rootJ = json_object();
addBaseJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_object_set_new(rootJ, std::to_string(i).c_str(), json_integer(triggerNum[i]));
}
return rootJ;
}

void fromJson(json_t *rootJ) {
baseFromJson(rootJ);
for (int i = 0; i < NUM_OUTPUTS; i++) {
json_t *ccNumJ = json_object_get(rootJ, std::to_string(i).c_str());
if (ccNumJ) {
triggerNum[i] = json_integer_value(ccNumJ);
triggerNumInited[i] = true;
}

}
}

void reset() final {
resetMidi();
}

};


void MIDITriggerToCVInterface::step() {
if (isPortOpen()) {
std::vector<unsigned char> message;

// midiIn->getMessage returns empty vector if there are no messages in the queue
getMessage(&message);
while (message.size() > 0) {
processMidi(message);
getMessage(&message);
}
}

for (int i = 0; i < NUM_OUTPUTS; i++) {
// Note: Could have an option to select between gate and velocity
// but trigger seams more useful
// outputs[i].value = trigger[i] / 127.0 * 10;
outputs[i].value = trigger[i] > 0 ? 10.0 : 0.0;
}
}

void MIDITriggerToCVInterface::resetMidi() {
for (int i = 0; i < NUM_OUTPUTS; i++) {
trigger[i] = 0;
}
};

void MIDITriggerToCVInterface::processMidi(std::vector<unsigned char> msg) {
int channel = msg[0] & 0xf;
int status = (msg[0] >> 4) & 0xf;
int data1 = msg[1];
int data2 = msg[2];

//fprintf(stderr, "channel %d status %d data1 %d data2 %d\n", channel, status, data1,data2);

// Filter channels
if (this->channel >= 0 && this->channel != channel)
return;

if (status == 0x8) { // note off
for (int i = 0; i < NUM_OUTPUTS; i++) {
if (data1 == triggerNum[i]) {
trigger[i] = data2;
}
}
return;
}

if (status == 0x9) { // note on
for (int i = 0; i < NUM_OUTPUTS; i++) {
if (onFocus[i]) {
this->triggerNum[i] = data1;
}
}

for (int i = 0; i < NUM_OUTPUTS; i++) {
if (data1 == triggerNum[i]) {
trigger[i] = data2;
}
}
}

}

struct TriggerTextField : TextField {
void onTextChange();

void draw(NVGcontext *vg);

void onMouseDownOpaque(int button);

void onMouseUpOpaque(int button);

void onMouseLeave();


int *triggerNum;
bool *inited;
bool *onFocus;
};

void TriggerTextField::draw(NVGcontext *vg) {
/* This is necessary, since the save
* file is loaded after constructing the widget*/
if (*inited) {
*inited = false;
text = std::to_string(*triggerNum);
}

if (*onFocus) {
text = std::to_string(*triggerNum);
}

TextField::draw(vg);
}

void TriggerTextField::onTextChange() {
if (text.size() > 0) {
try {
*triggerNum = std::stoi(text);
// Only allow valid cc numbers
if (*triggerNum < 0 || *triggerNum > 127 || text.size() > 3) {
text = "";
begin = end = 0;
*triggerNum = -1;
}
} catch (...) {
text = "";
begin = end = 0;
*triggerNum = -1;
}
};
}

void TriggerTextField::onMouseUpOpaque(int button) {
if (button == 1) {
*onFocus = false;
}

}

void TriggerTextField::onMouseDownOpaque(int button) {
if (button == 1) {
*onFocus = true;
}
}

void TriggerTextField::onMouseLeave() {
*onFocus = false;
}

MIDITriggerToCVWidget::MIDITriggerToCVWidget() {
MIDITriggerToCVInterface *module = new MIDITriggerToCVInterface();
setModule(module);
box.size = Vec(16 * 15, 380);

{
Panel *panel = new LightPanel();
panel->box.size = box.size;
addChild(panel);
}

float margin = 5;
float labelHeight = 15;
float yPos = margin;

addChild(createScrew<ScrewSilver>(Vec(15, 0)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 0)));
addChild(createScrew<ScrewSilver>(Vec(15, 365)));
addChild(createScrew<ScrewSilver>(Vec(box.size.x - 30, 365)));
{
Label *label = new Label();
label->box.pos = Vec(box.size.x - margin - 11 * 15, margin);
label->text = "MIDI Trigger to CV";
addChild(label);
yPos = labelHeight * 2;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "MIDI Interface";
addChild(label);

MidiChoice *midiChoice = new MidiChoice();
midiChoice->midiModule = dynamic_cast<MidiIO *>(module);
midiChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
midiChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(midiChoice);
yPos += midiChoice->box.size.y + margin;
}

{
Label *label = new Label();
label->box.pos = Vec(margin, yPos);
label->text = "Channel";
addChild(label);

ChannelChoice *channelChoice = new ChannelChoice();
channelChoice->midiModule = dynamic_cast<MidiIO *>(module);
channelChoice->box.pos = Vec((box.size.x - 10) / 2 + margin, yPos);
channelChoice->box.size.x = (box.size.x / 2.0) - margin;
addChild(channelChoice);
yPos += channelChoice->box.size.y + margin * 3;
}

for (int i = 0; i < MIDITriggerToCVInterface::NUM_OUTPUTS; i++) {
TriggerTextField *triggerNumChoice = new TriggerTextField();
triggerNumChoice->triggerNum = &module->triggerNum[i];
triggerNumChoice->inited = &module->triggerNumInited[i];
triggerNumChoice->onFocus = &module->onFocus[i];
triggerNumChoice->text = std::to_string(module->triggerNum[i]);
triggerNumChoice->box.pos = Vec(11 + (i % 4) * (63), yPos);
triggerNumChoice->box.size.x = 29;

addChild(triggerNumChoice);

yPos += labelHeight + margin;
addOutput(createOutput<PJ3410Port>(Vec((i % 4) * (63) + 10, yPos + 5), module, i));

if ((i + 1) % 4 == 0) {
yPos += 47 + margin;
} else {
yPos -= labelHeight + margin;
}
}
}

void MIDITriggerToCVWidget::step() {

ModuleWidget::step();
}

+ 3
- 0
src/core/core.cpp View File

@@ -1,4 +1,5 @@
#include "core.hpp"
#include "MidiIO.hpp"


void init(rack::Plugin *plugin) {
@@ -8,6 +9,8 @@ void init(rack::Plugin *plugin) {
createModel<AudioInterfaceWidget>(plugin, "AudioInterface", "Audio Interface");
createModel<MidiToCVWidget>(plugin, "MIDIToCVInterface", "MIDI-to-CV Interface");
createModel<MIDICCToCVWidget>(plugin, "MIDICCToCVInterface", "MIDI CC-to-CV Interface");
createModel<MIDIClockToCVWidget>(plugin, "MIDIClockToCVInterface", "MIDI Clock-to-CV Interface");
createModel<MIDITriggerToCVWidget>(plugin, "MIDITriggerToCVInterface", "MIDI Trigger-to-CV Interface");
// createModel<BridgeWidget>(plugin, "Bridge", "Bridge");
createModel<BlankWidget>(plugin, "Blank", "Blank");
}

+ 0
- 10
src/core/core.hpp View File

@@ -12,16 +12,6 @@ struct AudioInterfaceWidget : ModuleWidget {
AudioInterfaceWidget();
};

struct MidiToCVWidget : ModuleWidget {
MidiToCVWidget();
void step() override;
};

struct MIDICCToCVWidget : ModuleWidget {
MIDICCToCVWidget();
void step() override;
};

struct BridgeWidget : ModuleWidget {
BridgeWidget();
};


Loading…
Cancel
Save