From b863d0e54c54b4e67eb9d38fbc0fcbc5668d8060 Mon Sep 17 00:00:00 2001 From: falkTX Date: Sun, 23 Jan 2022 19:52:46 +0000 Subject: [PATCH] Very crude and dirty first host midi implementation Signed-off-by: falkTX --- plugins/Cardinal/src/HostMIDI.cpp | 682 ++++++++++++++++++++++++- plugins/Cardinal/src/plugincontext.hpp | 11 + src/CardinalPlugin.cpp | 202 +++----- src/PluginContext.hpp | 10 +- src/PluginDriver.hpp | 257 ---------- src/template.vcv | 46 +- 6 files changed, 758 insertions(+), 450 deletions(-) delete mode 100644 src/PluginDriver.hpp diff --git a/plugins/Cardinal/src/HostMIDI.cpp b/plugins/Cardinal/src/HostMIDI.cpp index b8fdc2d..09bdbb8 100644 --- a/plugins/Cardinal/src/HostMIDI.cpp +++ b/plugins/Cardinal/src/HostMIDI.cpp @@ -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 + // ----------------------------------------------------------------------------------------------------------- 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 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& 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(args.frame - blockFrame); + + for (uint32_t i=0; imidiEventCount; ++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 { + 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(APP)) + : pcontext(static_cast(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(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); - for (uint i=0; i<9; ++i) - addInput(createInput(Vec(startX_In, startY + padding * i), m, i)); + addInput(createInput(Vec(startX_In, startY + padding * 0), m, HostMIDI::PITCH_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 1), m, HostMIDI::GATE_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 2), m, HostMIDI::VELOCITY_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 3), m, HostMIDI::AFTERTOUCH_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 4), m, HostMIDI::PITCHBEND_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 5), m, HostMIDI::MODWHEEL_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 6), m, HostMIDI::START_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 7), m, HostMIDI::STOP_INPUT)); + addInput(createInput(Vec(startX_In, startY + padding * 8), m, HostMIDI::CONTINUE_INPUT)); - for (uint i=0; i<9; ++i) - addOutput(createOutput(Vec(startX_Out, startY + padding * i), m, i)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 0), m, HostMIDI::PITCH_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 1), m, HostMIDI::GATE_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 2), m, HostMIDI::VELOCITY_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 3), m, HostMIDI::AFTERTOUCH_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 4), m, HostMIDI::PITCHBEND_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 5), m, HostMIDI::MODWHEEL_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 6), m, HostMIDI::START_OUTPUT)); + addOutput(createOutput(Vec(startX_Out, startY + padding * 7), m, HostMIDI::STOP_OUTPUT)); + addOutput(createOutput(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();} + )); + */ + } }; // -------------------------------------------------------------------------------------------------------------------- diff --git a/plugins/Cardinal/src/plugincontext.hpp b/plugins/Cardinal/src/plugincontext.hpp index 618e981..712d563 100644 --- a/plugins/Cardinal/src/plugincontext.hpp +++ b/plugins/Cardinal/src/plugincontext.hpp @@ -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; diff --git a/src/CardinalPlugin.cpp b/src/CardinalPlugin.cpp index 125c347..821ab58 100644 --- a/src/CardinalPlugin.cpp +++ b/src/CardinalPlugin.cpp @@ -42,7 +42,7 @@ #include #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; ihandleSingleSimpleMessageFromHost(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; imidiEvents = midiEvents; + context->midiEventCount = midiEventCount; + context->engine->stepBlock(frames); } diff --git a/src/PluginContext.hpp b/src/PluginContext.hpp index 4d261a6..d2a738e 100644 --- a/src/PluginContext.hpp +++ b/src/PluginContext.hpp @@ -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 diff --git a/src/PluginDriver.hpp b/src/PluginDriver.hpp deleted file mode 100644 index d9d21dc..0000000 --- a/src/PluginDriver.hpp +++ /dev/null @@ -1,257 +0,0 @@ -/* - * DISTRHO Cardinal Plugin - * Copyright (C) 2021-2022 Filipe Coelho - * - * 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 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 getInputDeviceIds() override - { - return std::vector({ 0 }); - } - - std::vector getOutputDeviceIds() override - { - return std::vector({ 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(input->context); - DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr, nullptr); - - CardinalBasePlugin* const plugin = reinterpret_cast(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(output->context); - DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr, nullptr); - - CardinalBasePlugin* const plugin = reinterpret_cast(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(input->device); - DISTRHO_SAFE_ASSERT_RETURN(device != nullptr,); - - CardinalPluginContext* const pluginContext = reinterpret_cast(input->context); - DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr,); - - CardinalBasePlugin* const plugin = reinterpret_cast(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(output->device); - DISTRHO_SAFE_ASSERT_RETURN(device != nullptr,); - - CardinalPluginContext* const pluginContext = reinterpret_cast(output->context); - DISTRHO_SAFE_ASSERT_RETURN(pluginContext != nullptr,); - - CardinalBasePlugin* const plugin = reinterpret_cast(pluginContext->plugin); - DISTRHO_SAFE_ASSERT_RETURN(plugin != nullptr,); - - plugin->clearMidiOutputDevice(device); - device->unsubscribe(output); - delete device; - } -}; - -// ----------------------------------------------------------------------------------------------------------- - -END_NAMESPACE_DISTRHO diff --git a/src/template.vcv b/src/template.vcv index e19bcdc..1005f01 100644 --- a/src/template.vcv +++ b/src/template.vcv @@ -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 ] }