Browse Source

Very crude and dirty first host midi implementation

Signed-off-by: falkTX <falktx@falktx.com>
tags/22.02
falkTX 3 years ago
parent
commit
b863d0e54c
Signed by: falkTX <falktx@falktx.com> GPG Key ID: CDBAA37ABC74FBA0
6 changed files with 758 additions and 450 deletions
  1. +661
    -21
      plugins/Cardinal/src/HostMIDI.cpp
  2. +11
    -0
      plugins/Cardinal/src/plugincontext.hpp
  3. +70
    -132
      src/CardinalPlugin.cpp
  4. +6
    -4
      src/PluginContext.hpp
  5. +0
    -257
      src/PluginDriver.hpp
  6. +10
    -36
      src/template.vcv

+ 661
- 21
plugins/Cardinal/src/HostMIDI.cpp View File

@@ -15,26 +15,613 @@
* For a full copy of the GNU General Public License see the LICENSE file.
*/

/**
* This file contains a substantial amount of code from VCVRack's core/CV_MIDI.cpp and core/MIDI_CV.cpp
* Copyright (C) 2016-2021 VCV.
*
* This program is free software: you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or (at your option) any later version.
*/

#include "plugincontext.hpp"

#include <algorithm>

// -----------------------------------------------------------------------------------------------------------

USE_NAMESPACE_DISTRHO;

struct HostMIDI : Module {
enum ParamIds {
NUM_PARAMS
};
enum InputIds {
PITCH_INPUT,
GATE_INPUT,
VELOCITY_INPUT,
AFTERTOUCH_INPUT,
PITCHBEND_INPUT,
MODWHEEL_INPUT,
CLK_INPUT, // RETRIGGER_OUTPUT
VOL_INPUT, // CLOCK_OUTPUT
PAN_INPUT, // CLOCK_DIV_OUTPUT
START_INPUT,
STOP_INPUT,
CONTINUE_INPUT,
NUM_INPUTS
};
enum OutputIds {
PITCH_OUTPUT,
GATE_OUTPUT,
VELOCITY_OUTPUT,
AFTERTOUCH_OUTPUT,
PITCHBEND_OUTPUT,
MODWHEEL_OUTPUT,
RETRIGGER_OUTPUT, // CLK_INPUT
CLOCK_OUTPUT, // VOL_INPUT
CLOCK_DIV_OUTPUT, // PAN_INPUT
START_OUTPUT,
STOP_OUTPUT,
CONTINUE_OUTPUT,
NUM_OUTPUTS
};
enum LightIds {
NUM_LIGHTS
};

CardinalPluginContext* const pcontext;

struct MidiInput {
CardinalPluginContext* const pcontext;
midi::Message converterMsg;

bool smooth;
int channels;
enum PolyMode {
ROTATE_MODE,
REUSE_MODE,
RESET_MODE,
MPE_MODE,
NUM_POLY_MODES
};
PolyMode polyMode;

bool pedal;
// Indexed by channel
uint8_t notes[16];
bool gates[16];
uint8_t velocities[16];
uint8_t aftertouches[16];
std::vector<uint8_t> heldNotes;

int rotateIndex;

/** Pitch wheel.
When MPE is disabled, only the first channel is used.
[channel]
*/
uint16_t pws[16];
/** [channel] */
uint8_t mods[16];
dsp::ExponentialFilter pwFilters[16];
dsp::ExponentialFilter modFilters[16];

dsp::PulseGenerator startPulse;
dsp::PulseGenerator stopPulse;
dsp::PulseGenerator continuePulse;

MidiInput(CardinalPluginContext* const pc)
: pcontext(pc)
{
converterMsg.bytes.resize(0xff);
heldNotes.reserve(128);
for (int c = 0; c < 16; c++) {
pwFilters[c].setTau(1 / 30.f);
modFilters[c].setTau(1 / 30.f);
}
reset();
}

void reset()
{
smooth = true;
channels = 1;
polyMode = ROTATE_MODE;
panic();
}

/** Resets performance state */
void panic()
{
for (int c = 0; c < 16; c++) {
notes[c] = 60;
gates[c] = false;
velocities[c] = 0;
aftertouches[c] = 0;
pws[c] = 8192;
mods[c] = 0;
pwFilters[c].reset();
modFilters[c].reset();
}
pedal = false;
rotateIndex = -1;
heldNotes.clear();
}

void process(const ProcessArgs& args, std::vector<rack::engine::Output>& outputs)
{
DISTRHO_SAFE_ASSERT_RETURN(args.frame >= 0,);

const int64_t blockFrame = pcontext->engine->getBlockFrame();
DISTRHO_SAFE_ASSERT_RETURN(blockFrame >= 0,);
DISTRHO_SAFE_ASSERT_RETURN(args.frame >= blockFrame,);

/*
if (lastBlockFrame != blockFrame)
{
lastBlockFrame = blockFrame;
}
*/

const uint32_t frame = static_cast<uint32_t>(args.frame - blockFrame);

for (uint32_t i=0; i<pcontext->midiEventCount; ++i)
{
const MidiEvent& midiEvent(pcontext->midiEvents[i]);

if (midiEvent.frame < frame)
continue;
if (midiEvent.frame > frame)
break;

const uint8_t* data;

if (midiEvent.size > MidiEvent::kDataSize)
{
data = midiEvent.dataExt;
converterMsg.bytes.resize(midiEvent.size);
}
else
{
data = midiEvent.data;
}

converterMsg.frame = midiEvent.frame;
std::memcpy(converterMsg.bytes.data(), data, midiEvent.size);

processMessage(converterMsg);
}

outputs[PITCH_OUTPUT].setChannels(channels);
outputs[GATE_OUTPUT].setChannels(channels);
outputs[VELOCITY_OUTPUT].setChannels(channels);
outputs[AFTERTOUCH_OUTPUT].setChannels(channels);
outputs[RETRIGGER_OUTPUT].setChannels(channels);
for (int c = 0; c < channels; c++) {
outputs[PITCH_OUTPUT].setVoltage((notes[c] - 60.f) / 12.f, c);
outputs[GATE_OUTPUT].setVoltage(gates[c] ? 10.f : 0.f, c);
outputs[VELOCITY_OUTPUT].setVoltage(rescale(velocities[c], 0, 127, 0.f, 10.f), c);
outputs[AFTERTOUCH_OUTPUT].setVoltage(rescale(aftertouches[c], 0, 127, 0.f, 10.f), c);
}

// Set pitch and mod wheel
const int wheelChannels = (polyMode == MPE_MODE) ? 16 : 1;
outputs[PITCHBEND_OUTPUT].setChannels(wheelChannels);
outputs[MODWHEEL_OUTPUT].setChannels(wheelChannels);
for (int c = 0; c < wheelChannels; c++) {
float pw = ((int) pws[c] - 8192) / 8191.f;
pw = clamp(pw, -1.f, 1.f);
if (smooth)
pw = pwFilters[c].process(args.sampleTime, pw);
else
pwFilters[c].out = pw;
outputs[PITCHBEND_OUTPUT].setVoltage(pw * 5.f);

float mod = mods[c] / 127.f;
mod = clamp(mod, 0.f, 1.f);
if (smooth)
mod = modFilters[c].process(args.sampleTime, mod);
else
modFilters[c].out = mod;
outputs[MODWHEEL_OUTPUT].setVoltage(mod * 10.f);
}

outputs[START_OUTPUT].setVoltage(startPulse.process(args.sampleTime) ? 10.f : 0.f);
outputs[STOP_OUTPUT].setVoltage(stopPulse.process(args.sampleTime) ? 10.f : 0.f);
outputs[CONTINUE_OUTPUT].setVoltage(continuePulse.process(args.sampleTime) ? 10.f : 0.f);
}

void processMessage(const midi::Message& msg) {
// DEBUG("MIDI: %ld %s", msg.getFrame(), msg.toString().c_str());

switch (msg.getStatus()) {
// note off
case 0x8: {
releaseNote(msg.getNote());
} break;
// note on
case 0x9: {
if (msg.getValue() > 0) {
int c = msg.getChannel();
pressNote(msg.getNote(), &c);
velocities[c] = msg.getValue();
}
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(msg.getNote());
}
} break;
// key pressure
case 0xa: {
// Set the aftertouches with the same note
// TODO Should we handle the MPE case differently?
for (int c = 0; c < 16; c++) {
if (notes[c] == msg.getNote())
aftertouches[c] = msg.getValue();
}
} break;
// cc
case 0xb: {
processCC(msg);
} break;
// channel pressure
case 0xd: {
if (polyMode == MPE_MODE) {
// Set the channel aftertouch
aftertouches[msg.getChannel()] = msg.getNote();
}
else {
// Set all aftertouches
for (int c = 0; c < 16; c++) {
aftertouches[c] = msg.getNote();
}
}
} break;
// pitch wheel
case 0xe: {
int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
pws[c] = ((uint16_t) msg.getValue() << 7) | msg.getNote();
} break;
case 0xf: {
processSystem(msg);
} break;
default: break;
}
}

void processCC(const midi::Message& msg) {
switch (msg.getNote()) {
// mod
case 0x01: {
int c = (polyMode == MPE_MODE) ? msg.getChannel() : 0;
mods[c] = msg.getValue();
} break;
// sustain
case 0x40: {
if (msg.getValue() >= 64)
pressPedal();
else
releasePedal();
} break;
// all notes off (panic)
case 0x7b: {
if (msg.getValue() == 0) {
panic();
}
} break;
default: break;
}
}

void processSystem(const midi::Message& msg)
{
switch (msg.getChannel())
{
// Start
case 0xa:
startPulse.trigger(1e-3);
break;
// Continue
case 0xb:
continuePulse.trigger(1e-3);
break;
// Stop
case 0xc:
stopPulse.trigger(1e-3);
break;
}
}

int assignChannel(uint8_t note) {
if (channels == 1)
return 0;

switch (polyMode) {
case REUSE_MODE: {
// Find channel with the same note
for (int c = 0; c < channels; c++) {
if (notes[c] == note)
return c;
}
} // fallthrough

case ROTATE_MODE: {
// Find next available channel
for (int i = 0; i < channels; i++) {
rotateIndex++;
if (rotateIndex >= channels)
rotateIndex = 0;
if (!gates[rotateIndex])
return rotateIndex;
}
// No notes are available. Advance rotateIndex once more.
rotateIndex++;
if (rotateIndex >= channels)
rotateIndex = 0;
return rotateIndex;
} break;

case RESET_MODE: {
for (int c = 0; c < channels; c++) {
if (!gates[c])
return c;
}
return channels - 1;
} break;

case MPE_MODE: {
// This case is handled by querying the MIDI message channel.
return 0;
} break;

default: return 0;
}
}

void pressNote(uint8_t note, int* channel) {
// Remove existing similar note
auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
if (it != heldNotes.end())
heldNotes.erase(it);
// Push note
heldNotes.push_back(note);
// Determine actual channel
if (polyMode == MPE_MODE) {
// Channel is already decided for us
}
else {
*channel = assignChannel(note);
}
// Set note
notes[*channel] = note;
gates[*channel] = true;
}

void releaseNote(uint8_t note) {
// Remove the note
auto it = std::find(heldNotes.begin(), heldNotes.end(), note);
if (it != heldNotes.end())
heldNotes.erase(it);
// Hold note if pedal is pressed
if (pedal)
return;
// Turn off gate of all channels with note
for (int c = 0; c < channels; c++) {
if (notes[c] == note) {
gates[c] = false;
}
}
// Set last note if monophonic
if (channels == 1) {
if (note == notes[0] && !heldNotes.empty()) {
uint8_t lastNote = heldNotes.back();
notes[0] = lastNote;
gates[0] = true;
return;
}
}
}

void pressPedal() {
if (pedal)
return;
pedal = true;
}

void releasePedal() {
if (!pedal)
return;
pedal = false;
// Set last note if monophonic
if (channels == 1) {
if (!heldNotes.empty()) {
uint8_t lastNote = heldNotes.back();
notes[0] = lastNote;
}
}
// Clear notes that are not held if polyphonic
else {
for (int c = 0; c < channels; c++) {
if (!gates[c])
continue;
gates[c] = false;
for (uint8_t note : heldNotes) {
if (notes[c] == note) {
gates[c] = true;
break;
}
}
}
}
}

void setChannels(const int channels)
{
if (channels == this->channels)
return;
this->channels = channels;
panic();
}

void setPolyMode(const PolyMode polyMode)
{
if (polyMode == this->polyMode)
return;
this->polyMode = polyMode;
panic();
}
} midiInput;

struct MidiOutput : dsp::MidiGenerator<PORT_MAX_CHANNELS> {
CardinalPluginContext* const pcontext;
dsp::Timer rateLimiterTimer;

MidiOutput(CardinalPluginContext* const pc)
: pcontext(pc) {}

void onMessage(const midi::Message& message) override
{
pcontext->writeMidiMessage(message);
}
} midiOutput;

HostMIDI()
: pcontext(static_cast<CardinalPluginContext*>(APP))
: pcontext(static_cast<CardinalPluginContext*>(APP)),
midiInput(pcontext),
midiOutput(pcontext)
{
if (pcontext == nullptr)
throw rack::Exception("Plugin context is null");

config(0, 9, 9, 0);
config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
configInput(PITCH_INPUT, "1V/octave pitch");
configInput(GATE_INPUT, "Gate");
configInput(VELOCITY_INPUT, "Velocity");
configInput(AFTERTOUCH_INPUT, "Aftertouch");
configInput(PITCHBEND_INPUT, "Pitchbend");
configInput(MODWHEEL_INPUT, "Mod wheel");
configInput(CLK_INPUT, "Clock");
configInput(VOL_INPUT, "Volume");
configInput(PAN_INPUT, "Pan");
configInput(START_INPUT, "Start trigger");
configInput(STOP_INPUT, "Stop trigger");
configInput(CONTINUE_INPUT, "Continue trigger");
configOutput(PITCH_OUTPUT, "1V/octave pitch");
configOutput(GATE_OUTPUT, "Gate");
configOutput(VELOCITY_OUTPUT, "Velocity");
configOutput(AFTERTOUCH_OUTPUT, "Aftertouch");
configOutput(PITCHBEND_OUTPUT, "Pitchbend");
configOutput(MODWHEEL_OUTPUT, "Mod wheel");
configOutput(RETRIGGER_OUTPUT, "Retrigger");
configOutput(CLOCK_OUTPUT, "Clock");
configOutput(CLOCK_DIV_OUTPUT, "Clock divider");
configOutput(START_OUTPUT, "Start trigger");
configOutput(STOP_OUTPUT, "Stop trigger");
configOutput(CONTINUE_OUTPUT, "Continue trigger");
}

void process(const ProcessArgs& args) override
{
midiInput.process(args, outputs);

// 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.
static constexpr const float rateLimiterPeriod = 1 / 200.f;
bool rateLimiterTriggered = (midiOutput.rateLimiterTimer.process(args.sampleTime) >= rateLimiterPeriod);
if (rateLimiterTriggered)
midiOutput.rateLimiterTimer.time -= rateLimiterPeriod;

midiOutput.setFrame(args.frame);

for (int c = 0; c < inputs[PITCH_INPUT].getChannels(); c++) {
int vel = (int) std::round(inputs[VELOCITY_INPUT].getNormalPolyVoltage(10.f * 100 / 127, c) / 10.f * 127);
vel = clamp(vel, 0, 127);
midiOutput.setVelocity(vel, c);

int note = (int) std::round(inputs[PITCH_INPUT].getVoltage(c) * 12.f + 60.f);
note = clamp(note, 0, 127);
bool gate = inputs[GATE_INPUT].getPolyVoltage(c) >= 1.f;
midiOutput.setNoteGate(note, gate, c);

int aft = (int) std::round(inputs[AFTERTOUCH_INPUT].getPolyVoltage(c) / 10.f * 127);
aft = clamp(aft, 0, 127);
midiOutput.setKeyPressure(aft, c);
}

if (rateLimiterTriggered) {
int pw = (int) std::round((inputs[PITCHBEND_INPUT].getVoltage() + 5.f) / 10.f * 0x4000);
pw = clamp(pw, 0, 0x3fff);
midiOutput.setPitchWheel(pw);

int mw = (int) std::round(inputs[MODWHEEL_INPUT].getVoltage() / 10.f * 127);
mw = clamp(mw, 0, 127);
midiOutput.setModWheel(mw);

/* unused
int vol = (int) std::round(inputs[VOL_INPUT].getNormalVoltage(10.f) / 10.f * 127);
vol = clamp(vol, 0, 127);
midiOutput.setVolume(vol);

int pan = (int) std::round((inputs[PAN_INPUT].getVoltage() + 5.f) / 10.f * 127);
pan = clamp(pan, 0, 127);
midiOutput.setPan(pan);
*/
}

/* unused
bool clk = inputs[CLK_INPUT].getVoltage() >= 1.f;
midiOutput.setClock(clk);
*/

bool start = inputs[START_INPUT].getVoltage() >= 1.f;
midiOutput.setStart(start);

bool stop = inputs[STOP_INPUT].getVoltage() >= 1.f;
midiOutput.setStop(stop);

bool cont = inputs[CONTINUE_INPUT].getVoltage() >= 1.f;
midiOutput.setContinue(cont);
}

json_t* dataToJson() override
{
json_t* rootJ = json_object();
json_object_set_new(rootJ, "smooth", json_boolean(midiInput.smooth));
json_object_set_new(rootJ, "channels", json_integer(midiInput.channels));
json_object_set_new(rootJ, "polyMode", json_integer(midiInput.polyMode));
// Saving/restoring pitch and mod doesn't make much sense for MPE.
if (midiInput.polyMode != MidiInput::MPE_MODE) {
json_object_set_new(rootJ, "lastPitch", json_integer(midiInput.pws[0]));
json_object_set_new(rootJ, "lastMod", json_integer(midiInput.mods[0]));
}
return rootJ;
}

void process(const ProcessArgs&) override
{}
void dataFromJson(json_t* rootJ) override
{
json_t* smoothJ = json_object_get(rootJ, "smooth");
if (smoothJ)
midiInput.smooth = json_boolean_value(smoothJ);

json_t* channelsJ = json_object_get(rootJ, "channels");
if (channelsJ)
midiInput.setChannels(json_integer_value(channelsJ));

json_t* polyModeJ = json_object_get(rootJ, "polyMode");
if (polyModeJ)
midiInput.polyMode = (MidiInput::PolyMode) json_integer_value(polyModeJ);

json_t* lastPitchJ = json_object_get(rootJ, "lastPitch");
if (lastPitchJ)
midiInput.pws[0] = json_integer_value(lastPitchJ);

json_t* lastModJ = json_object_get(rootJ, "lastMod");
if (lastModJ)
midiInput.mods[0] = json_integer_value(lastModJ);
}
};

// --------------------------------------------------------------------------------------------------------------------
@@ -44,7 +631,7 @@ struct HostMIDIWidget : ModuleWidget {
static constexpr const float startX_Out = 96.0f;
static constexpr const float startY = 74.0f;
static constexpr const float padding = 29.0f;
static constexpr const float middleX = startX_In + (startX_Out - startX_In) * 0.5f + padding * 0.25f;
static constexpr const float middleX = startX_In + (startX_Out - startX_In) * 0.5f + padding * 0.35f;

HostMIDI* const module;

@@ -59,19 +646,33 @@ struct HostMIDIWidget : ModuleWidget {
addChild(createWidget<ScrewBlack>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
addChild(createWidget<ScrewBlack>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));

for (uint i=0; i<9; ++i)
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * i), m, i));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 0), m, HostMIDI::PITCH_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 1), m, HostMIDI::GATE_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 2), m, HostMIDI::VELOCITY_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 3), m, HostMIDI::AFTERTOUCH_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 4), m, HostMIDI::PITCHBEND_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 5), m, HostMIDI::MODWHEEL_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 6), m, HostMIDI::START_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 7), m, HostMIDI::STOP_INPUT));
addInput(createInput<PJ301MPort>(Vec(startX_In, startY + padding * 8), m, HostMIDI::CONTINUE_INPUT));

for (uint i=0; i<9; ++i)
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * i), m, i));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 0), m, HostMIDI::PITCH_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 1), m, HostMIDI::GATE_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 2), m, HostMIDI::VELOCITY_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 3), m, HostMIDI::AFTERTOUCH_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 4), m, HostMIDI::PITCHBEND_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 5), m, HostMIDI::MODWHEEL_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 6), m, HostMIDI::START_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 7), m, HostMIDI::STOP_OUTPUT));
addOutput(createOutput<PJ301MPort>(Vec(startX_Out, startY + padding * 8), m, HostMIDI::CONTINUE_OUTPUT));
}

void drawTextLine(NVGcontext* const vg, const float offsetX, const uint posY, const char* const text)
void drawTextLine(NVGcontext* const vg, const uint posY, const char* const text)
{
const float y = startY + posY * padding;
nvgBeginPath(vg);
nvgFillColor(vg, color::WHITE);
nvgText(vg, middleX + offsetX, y + 16, text, nullptr);
nvgText(vg, middleX, y + 16, text, nullptr);
}

void draw(const DrawArgs& args) override
@@ -87,22 +688,61 @@ struct HostMIDIWidget : ModuleWidget {
nvgTextAlign(args.vg, NVG_ALIGN_CENTER);

nvgBeginPath(args.vg);
nvgRoundedRect(args.vg, startX_Out - 4.0f, startY - 2.0f, padding, padding * 9, 4);
nvgRoundedRect(args.vg, startX_Out - 2.5f, startY - 2.0f, padding, padding * 9, 4);
nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0));
nvgFill(args.vg);

drawTextLine(args.vg, 0.0f, 0, "V/Oct");
drawTextLine(args.vg, 0.0f, 1, "Gate");
drawTextLine(args.vg, 0.0f, 2, "Vel");
drawTextLine(args.vg, 0.0f, 3, "Aft");
drawTextLine(args.vg, 0.0f, 4, "PW");
drawTextLine(args.vg, 0.0f, 5, "MW");
drawTextLine(args.vg, 0.0f, 6, "Start");
drawTextLine(args.vg, 0.0f, 7, "Stop");
drawTextLine(args.vg, 0.0f, 8, "Cont");
drawTextLine(args.vg, 0, "V/Oct");
drawTextLine(args.vg, 1, "Gate");
drawTextLine(args.vg, 2, "Velocity");
drawTextLine(args.vg, 3, "Aftertouch");
drawTextLine(args.vg, 4, "Pitchbend");
drawTextLine(args.vg, 5, "Mod Wheel");
drawTextLine(args.vg, 6, "Start");
drawTextLine(args.vg, 7, "Stop");
drawTextLine(args.vg, 8, "Cont");

ModuleWidget::draw(args);
}

void appendContextMenu(Menu* const menu) override
{
menu->addChild(new MenuSeparator);

/*
menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->smooth));

struct ChannelItem : MenuItem {
MIDI_CV* module;
Menu* createChildMenu() override {
Menu* menu = new Menu;
for (int c = 1; c <= 16; c++) {
menu->addChild(createCheckMenuItem((c == 1) ? "Monophonic" : string::f("%d", c), "",
[=]() {return module->channels == c;},
[=]() {module->setChannels(c);}
));
}
return menu;
}
};
ChannelItem* channelItem = new ChannelItem;
channelItem->text = "Polyphony channels";
channelItem->rightText = string::f("%d", module->channels) + " " + RIGHT_ARROW;
channelItem->module = module;
menu->addChild(channelItem);

menu->addChild(createIndexPtrSubmenuItem("Polyphony mode", {
"Rotate",
"Reuse",
"Reset",
"MPE",
}, &module->polyMode));

menu->addChild(createMenuItem("Panic", "",
[=]() {module->panic();}
));
*/
}
};

// --------------------------------------------------------------------------------------------------------------------


+ 11
- 0
plugins/Cardinal/src/plugincontext.hpp View File

@@ -42,6 +42,14 @@ enum CardinalVariant {
class Plugin;
class UI;

struct MidiEvent {
static const uint32_t kDataSize = 4;
uint32_t frame;
uint32_t size;
uint8_t data[kDataSize];
const uint8_t* dataExt;
};

struct CardinalPluginContext : rack::Context {
uint32_t bufferSize;
double sampleRate;
@@ -55,11 +63,14 @@ struct CardinalPluginContext : rack::Context {
uintptr_t nativeWindowId;
const float* const* dataIns;
float** dataOuts;
const MidiEvent* midiEvents;
uint32_t midiEventCount;
Plugin* const plugin;
#ifndef HEADLESS
UI* ui;
#endif
CardinalPluginContext(Plugin* const p);
void writeMidiMessage(const rack::midi::Message& message);
#ifndef HEADLESS
bool addIdleCallback(IdleCallback* cb) const;
void removeIdleCallback(IdleCallback* cb) const;


+ 70
- 132
src/CardinalPlugin.cpp View File

@@ -42,7 +42,7 @@
#include <list>
#include "DistrhoPluginUtils.hpp"
#include "PluginDriver.hpp"
#include "PluginContext.hpp"
#include "extra/Base64.hpp"
#include "extra/SharedResourcePointer.hpp"
@@ -182,9 +182,6 @@ struct Initializer
"Make sure Cardinal was downloaded and installed correctly.", asset::systemDir.c_str());
}
INFO("Initializing midi driver");
midi::addDriver(0, new CardinalMidiDriver);
INFO("Initializing plugins");
plugin::initStaticPlugins();
@@ -315,6 +312,66 @@ struct Initializer
// -----------------------------------------------------------------------------------------------------------
void CardinalPluginContext::writeMidiMessage(const rack::midi::Message& message)
{
const size_t size = message.bytes.size();
DISTRHO_SAFE_ASSERT_RETURN(size > 0,);
DISTRHO_SAFE_ASSERT_RETURN(message.frame >= 0,);
MidiEvent event;
event.frame = message.frame;
switch (message.bytes[0] & 0xF0)
{
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xE0:
event.size = 3;
break;
case 0xC0:
case 0xD0:
event.size = 2;
break;
case 0xF0:
switch (message.bytes[0] & 0x0F)
{
case 0x0:
case 0x4:
case 0x5:
case 0x7:
case 0x9:
case 0xD:
// unsupported
return;
case 0x1:
case 0x2:
case 0x3:
case 0xE:
event.size = 3;
break;
case 0x6:
case 0x8:
case 0xA:
case 0xB:
case 0xC:
case 0xF:
event.size = 1;
break;
}
break;
}
DISTRHO_SAFE_ASSERT_RETURN(size >= event.size,);
std::memcpy(event.data, message.bytes.data(), event.size);
plugin->writeMidiEvent(event);
}
// -----------------------------------------------------------------------------------------------------------
struct ScopedContext {
ScopedContext(const CardinalBasePlugin* const plugin)
{
@@ -327,6 +384,7 @@ struct ScopedContext {
}
};
// -----------------------------------------------------------------------------------------------------------
class CardinalPlugin : public CardinalBasePlugin
@@ -335,13 +393,8 @@ class CardinalPlugin : public CardinalBasePlugin
float** fAudioBufferCopy;
std::string fAutosavePath;
String fWindowSize;
// for base/context handling
CardinalMidiInputDevice** fCurrentMidiInputs;
CardinalMidiOutputDevice** fCurrentMidiOutputs;
uint64_t fPreviousFrame;
Mutex fDeviceMutex;
String fWindowSize;
#ifndef HEADLESS
// real values, not VCV interpreted ones
@@ -353,8 +406,6 @@ public:
: CardinalBasePlugin(kModuleParameters + kWindowParameterCount, 0, kCardinalStateCount),
fInitializer(this),
fAudioBufferCopy(nullptr),
fCurrentMidiInputs(nullptr),
fCurrentMidiOutputs(nullptr),
fPreviousFrame(0)
{
#ifndef HEADLESS
@@ -432,14 +483,6 @@ public:
delete context;
}
{
const MutexLocker cml(fDeviceMutex);
delete[] fCurrentMidiInputs;
fCurrentMidiInputs = nullptr;
delete[] fCurrentMidiOutputs;
fCurrentMidiOutputs = nullptr;
}
if (! fAutosavePath.empty())
rack::system::removeRecursively(fAutosavePath);
}
@@ -450,101 +493,6 @@ public:
}
protected:
/* --------------------------------------------------------------------------------------------------------
* Cardinal Base things */
void assignMidiInputDevice(CardinalMidiInputDevice* const dev) noexcept override
{
CardinalMidiInputDevice** const oldDevs = fCurrentMidiInputs;
uint numDevs = 0;
if (oldDevs != nullptr)
{
while (oldDevs[numDevs] != nullptr)
++numDevs;
}
CardinalMidiInputDevice** const newDevs = new CardinalMidiInputDevice*[numDevs + 2];
for (uint i=0; i<numDevs; ++i)
newDevs[i] = oldDevs[i];
newDevs[numDevs+0] = dev;
newDevs[numDevs+1] = nullptr;
{
const MutexLocker cml(fDeviceMutex);
fCurrentMidiInputs = newDevs;
}
delete[] oldDevs;
}
void assignMidiOutputDevice(CardinalMidiOutputDevice* const dev) noexcept override
{
CardinalMidiOutputDevice** const oldDevs = fCurrentMidiOutputs;
uint numDevs = 0;
if (oldDevs != nullptr)
{
while (oldDevs[numDevs] != nullptr)
++numDevs;
}
CardinalMidiOutputDevice** const newDevs = new CardinalMidiOutputDevice*[numDevs + 2];
for (uint i=0; i<numDevs; ++i)
newDevs[i] = oldDevs[i];
newDevs[numDevs+0] = dev;
newDevs[numDevs+1] = nullptr;
{
const MutexLocker cml(fDeviceMutex);
fCurrentMidiOutputs = newDevs;
}
delete[] oldDevs;
}
void clearMidiInputDevice(CardinalMidiInputDevice* const dev) noexcept override
{
const MutexLocker cml(fDeviceMutex);
CardinalMidiInputDevice** const inputs = fCurrentMidiInputs;
DISTRHO_SAFE_ASSERT_RETURN(inputs != nullptr,);
for (uint i=0; inputs[i] != nullptr; ++i)
{
CardinalMidiInputDevice* const input = inputs[i];
if (input != dev)
continue;
for (; inputs[i+1] != nullptr; ++i)
inputs[i] = inputs[i+1];
inputs[i] = nullptr;
break;
}
}
void clearMidiOutputDevice(CardinalMidiOutputDevice* const dev) noexcept override
{
const MutexLocker cml(fDeviceMutex);
CardinalMidiOutputDevice** const outputs = fCurrentMidiOutputs;
DISTRHO_SAFE_ASSERT_RETURN(outputs != nullptr,);
for (uint i=0; outputs[i] != nullptr; ++i)
{
CardinalMidiOutputDevice* const output = outputs[i];
if (output != dev)
continue;
for (; outputs[i+1] != nullptr; ++i)
outputs[i] = outputs[i+1];
outputs[i] = nullptr;
break;
}
}
/* --------------------------------------------------------------------------------------------------------
* Information */
@@ -875,32 +823,22 @@ protected:
}
}
inline void sendSingleSimpleMidiMessage(const MidiEvent& midiEvent)
{
if (CardinalMidiInputDevice** inputs = fCurrentMidiInputs)
{
for (;*inputs != nullptr; ++inputs)
(*inputs)->handleSingleSimpleMessageFromHost(midiEvent);
}
}
void run(const float** const inputs, float** const outputs, const uint32_t frames,
const MidiEvent* const midiEvents, const uint32_t midiEventCount) override
{
const MutexLocker cml(fDeviceMutex);
rack::contextSet(context);
{
const TimePosition& timePos(getTimePosition());
bool reset = false;
MidiEvent singleTimeMidiEvent = { 0, 1, { 0, 0, 0, 0 }, nullptr };
if (timePos.playing)
{
if (timePos.frame == 0 || fPreviousFrame + frames != timePos.frame)
reset = true;
/*
if (! context->playing)
{
if (timePos.frame == 0)
@@ -912,11 +850,14 @@ protected:
singleTimeMidiEvent.data[0] = 0xFB; // continue
sendSingleSimpleMidiMessage(singleTimeMidiEvent);
}
*/
}
else if (context->playing)
{
/*
singleTimeMidiEvent.data[0] = 0xFC; // stop
sendSingleSimpleMidiMessage(singleTimeMidiEvent);
*/
}
context->playing = timePos.playing;
@@ -945,12 +886,6 @@ protected:
fPreviousFrame = timePos.frame;
}
if (CardinalMidiInputDevice** inputs = fCurrentMidiInputs)
{
for (;*inputs != nullptr; ++inputs)
(*inputs)->handleMessagesFromHost(midiEvents, midiEventCount);
}
// separate buffers, use them
if (inputs != outputs && (inputs == nullptr || inputs[0] != outputs[0]))
{
@@ -970,6 +905,9 @@ protected:
for (int i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
std::memset(outputs[i], 0, sizeof(float)*frames);
context->midiEvents = midiEvents;
context->midiEventCount = midiEventCount;
context->engine->stepBlock(frames);
}


+ 6
- 4
src/PluginContext.hpp View File

@@ -60,6 +60,8 @@ struct CardinalPluginContext : rack::Context {
uintptr_t nativeWindowId;
const float* const* dataIns;
float** dataOuts;
const MidiEvent* midiEvents;
uint32_t midiEventCount;
Plugin* const plugin;
#ifndef HEADLESS
UI* ui;
@@ -95,6 +97,8 @@ struct CardinalPluginContext : rack::Context {
nativeWindowId(0),
dataIns(nullptr),
dataOuts(nullptr),
midiEvents(nullptr),
midiEventCount(0),
plugin(p)
#ifndef HEADLESS
, ui(nullptr)
@@ -103,6 +107,8 @@ struct CardinalPluginContext : rack::Context {
std::memset(parameters, 0, sizeof(parameters));
}

void writeMidiMessage(const rack::midi::Message& message);

#ifndef HEADLESS
bool addIdleCallback(IdleCallback* cb) const;
void removeIdleCallback(IdleCallback* cb) const;
@@ -129,10 +135,6 @@ public:
: Plugin(parameterCount, programCount, stateCount),
context(new CardinalPluginContext(this)) {}
~CardinalBasePlugin() override {}
virtual void assignMidiInputDevice(CardinalMidiInputDevice* dev) noexcept = 0;
virtual void assignMidiOutputDevice(CardinalMidiOutputDevice* dev) noexcept = 0;
virtual void clearMidiInputDevice(CardinalMidiInputDevice* dev) noexcept = 0;
virtual void clearMidiOutputDevice(CardinalMidiOutputDevice* dev) noexcept = 0;
};

#ifndef HEADLESS


+ 0
- 257
src/PluginDriver.hpp View File

@@ -1,257 +0,0 @@
/*
* DISTRHO Cardinal Plugin
* Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation; either version 3 of
* the License, or any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* For a full copy of the GNU General Public License see the LICENSE file.
*/

#pragma once

#include "PluginContext.hpp"

START_NAMESPACE_DISTRHO

// -----------------------------------------------------------------------------------------------------------

struct CardinalMidiInputDevice : rack::midi::InputDevice
{
CardinalBasePlugin* const fPlugin;
rack::midi::Message msg;

CardinalMidiInputDevice(CardinalBasePlugin* const plugin)
: fPlugin(plugin)
{
msg.bytes.resize(0xff);
}

std::string getName() override
{
return "Cardinal";
}

inline void handleSingleSimpleMessageFromHost(const MidiEvent& midiEvent)
{
if (subscribed.size() == 0)
return;

msg.frame = midiEvent.frame;
std::memcpy(msg.bytes.data(), midiEvent.data, midiEvent.size);

onMessage(msg);
}

inline void handleMessagesFromHost(const MidiEvent* const midiEvents, const uint32_t midiEventCount)
{
if (subscribed.size() == 0)
return;

for (uint32_t i=0; i<midiEventCount; ++i)
{
const MidiEvent& midiEvent(midiEvents[i]);
const uint8_t* data;

if (midiEvent.size > MidiEvent::kDataSize)
{
data = midiEvent.dataExt;
msg.bytes.resize(midiEvent.size);
}
else
{
data = midiEvent.data;
}

msg.frame = midiEvent.frame;
std::memcpy(msg.bytes.data(), data, midiEvent.size);

onMessage(msg);
}
}
};

// -----------------------------------------------------------------------------------------------------------

struct CardinalMidiOutputDevice : rack::midi::OutputDevice
{
CardinalBasePlugin* const fPlugin;

CardinalMidiOutputDevice(CardinalBasePlugin* const plugin)
: fPlugin(plugin) {}

std::string getName() override
{
return "Cardinal";
}

void sendMessage(const rack::midi::Message& message) override
{
const size_t size = message.bytes.size();
DISTRHO_SAFE_ASSERT_RETURN(size > 0,);

MidiEvent event;
event.frame = message.frame < 0 ? 0 : (message.frame - fPlugin->context->engine->getBlockFrame());

switch (message.bytes[0] & 0xF0)
{
case 0x80:
case 0x90:
case 0xA0:
case 0xB0:
case 0xE0:
event.size = 3;
break;
case 0xC0:
case 0xD0:
event.size = 2;
break;
case 0xF0:
switch (message.bytes[0] & 0x0F)
{
case 0x0:
case 0x4:
case 0x5:
case 0x7:
case 0x9:
case 0xD:
// unsupported
return;
case 0x1:
case 0x2:
case 0x3:
case 0xE:
event.size = 3;
break;
case 0x6:
case 0x8:
case 0xA:
case 0xB:
case 0xC:
case 0xF:
event.size = 1;
break;
}
break;
}

DISTRHO_SAFE_ASSERT_RETURN(size >= event.size,);

std::memcpy(event.data, message.bytes.data(), event.size);

fPlugin->writeMidiEvent(event);
}
};

// -----------------------------------------------------------------------------------------------------------

struct CardinalMidiDriver : rack::midi::Driver
{
CardinalMidiDriver() {}

std::string getName() override
{
return "Plugin Driver";
}

std::vector<int> getInputDeviceIds() override
{
return std::vector<int>({ 0 });
}

std::vector<int> getOutputDeviceIds() override
{
return std::vector<int>({ 0 });
}

int getDefaultInputDeviceId() override
{
return 0;
}

int getDefaultOutputDeviceId() override
{
return 0;
}

std::string getInputDeviceName(int) override
{
return "Plugin Device";
}

std::string getOutputDeviceName(int) override
{
return "Plugin Device";
}

rack::midi::InputDevice* subscribeInput(int, rack::midi::Input* const input) override
{
CardinalPluginContext* const pluginContext = reinterpret_cast<CardinalPluginContext*>(input->context);
DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr, nullptr);

CardinalBasePlugin* const plugin = reinterpret_cast<CardinalBasePlugin*>(pluginContext->plugin);
DISTRHO_SAFE_ASSERT_RETURN(plugin != nullptr, nullptr);

CardinalMidiInputDevice* const device = new CardinalMidiInputDevice(plugin);
device->subscribe(input);
plugin->assignMidiInputDevice(device);
return device;
}

rack::midi::OutputDevice* subscribeOutput(int, rack::midi::Output* const output) override
{
CardinalPluginContext* const pluginContext = reinterpret_cast<CardinalPluginContext*>(output->context);
DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr, nullptr);

CardinalBasePlugin* const plugin = reinterpret_cast<CardinalBasePlugin*>(pluginContext->plugin);
DISTRHO_SAFE_ASSERT_RETURN(plugin != nullptr, nullptr);

CardinalMidiOutputDevice* const device = new CardinalMidiOutputDevice(plugin);
device->subscribe(output);
plugin->assignMidiOutputDevice(device);
return device;
}

void unsubscribeInput(int, rack::midi::Input* const input) override
{
CardinalMidiInputDevice* const device = reinterpret_cast<CardinalMidiInputDevice*>(input->device);
DISTRHO_SAFE_ASSERT_RETURN(device != nullptr,);

CardinalPluginContext* const pluginContext = reinterpret_cast<CardinalPluginContext*>(input->context);
DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr,);

CardinalBasePlugin* const plugin = reinterpret_cast<CardinalBasePlugin*>(pluginContext->plugin);
DISTRHO_SAFE_ASSERT_RETURN(plugin != nullptr,);

plugin->clearMidiInputDevice(device);
device->unsubscribe(input);
delete device;
}

void unsubscribeOutput(int, rack::midi::Output* const output) override
{
CardinalMidiOutputDevice* const device = reinterpret_cast<CardinalMidiOutputDevice*>(output->device);
DISTRHO_SAFE_ASSERT_RETURN(device != nullptr,);

CardinalPluginContext* const pluginContext = reinterpret_cast<CardinalPluginContext*>(output->context);
DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr,);

CardinalBasePlugin* const plugin = reinterpret_cast<CardinalBasePlugin*>(pluginContext->plugin);
DISTRHO_SAFE_ASSERT_RETURN(plugin != nullptr,);

plugin->clearMidiOutputDevice(device);
device->unsubscribe(output);
delete device;
}
};

// -----------------------------------------------------------------------------------------------------------

END_NAMESPACE_DISTRHO

+ 10
- 36
src/template.vcv View File

@@ -24,9 +24,9 @@
},
{
"id": 2,
"plugin": "Core",
"version": "2.0.0",
"model": "MIDIToCVInterface",
"plugin": "Cardinal",
"model": "HostMIDI",
"version": "2.0",
"params": [],
"leftModuleId": 1,
"rightModuleId": 3,
@@ -35,12 +35,7 @@
"polyMode": 0,
"clockDivision": 24,
"lastPitch": 8192,
"lastMod": 0,
"midi": {
"driver": 0,
"deviceName": "Cardinal",
"channel": -1
}
"lastMod": 0
},
"pos": [
5,
@@ -49,47 +44,26 @@
},
{
"id": 3,
"plugin": "Core",
"version": "2.0.0",
"model": "CV-MIDI",
"params": [],
"leftModuleId": 2,
"rightModuleId": 4,
"data": {
"midi": {
"driver": 0,
"deviceName": "Cardinal",
"channel": 0
}
},
"pos": [
13,
0
]
},
{
"id": 4,
"plugin": "Cardinal",
"model": "HostParameters",
"version": "2.0",
"params": [],
"leftModuleId": 3,
"rightModuleId": 5,
"leftModuleId": 2,
"rightModuleId": 4,
"pos": [
21,
18,
0
]
},
{
"id": 5,
"id": 4,
"plugin": "Cardinal",
"model": "HostTime",
"version": "2.0",
"params": [],
"leftModuleId": 4,
"rightModuleId": 6,
"leftModuleId": 3,
"pos": [
30,
27,
0
]
}


Loading…
Cancel
Save