diff --git a/plugins/Cardinal/plugin.json b/plugins/Cardinal/plugin.json index 80d7799..396ebf1 100644 --- a/plugins/Cardinal/plugin.json +++ b/plugins/Cardinal/plugin.json @@ -109,9 +109,19 @@ { "slug": "ExpanderInputMIDI", "name": "ExpanderInputMIDI", - "description": "MIDI input expander for Carla Plugin Host or Ildaeil", + "description": "MIDI input (from CV) expander for Carla Plugin Host and Ildaeil", "tags": [ - "Expander" + "Expander", + "MIDI" + ] + }, + { + "slug": "ExpanderOutputMIDI", + "name": "ExpanderOutputMIDI", + "description": "MIDI output (to CV) expander for Carla Plugin Host and Ildaeil", + "tags": [ + "Expander", + "MIDI" ] }, { diff --git a/plugins/Cardinal/src/Carla.cpp b/plugins/Cardinal/src/Carla.cpp index fd39362..c786c43 100644 --- a/plugins/Cardinal/src/Carla.cpp +++ b/plugins/Cardinal/src/Carla.cpp @@ -96,6 +96,7 @@ struct CarlaModule : Module { float* dataOutPtr[NUM_OUTPUTS]; unsigned audioDataFill = 0; int64_t lastBlockFrame = -1; + CardinalExpanderFromCarlaMIDIToCV* midiOutExpander = nullptr; std::string patchStorage; CarlaModule() @@ -387,16 +388,28 @@ struct CarlaModule : Module { midiEventCount = 0; } + if ((midiOutExpander = rightExpander.module != nullptr && rightExpander.module->model == modelExpanderOutputMIDI + ? static_cast(rightExpander.module) + : nullptr)) + midiOutExpander->midiEventCount = 0; + audioDataFill = 0; fCarlaPluginDescriptor->process(fCarlaPluginHandle, dataInPtr, dataOutPtr, BUFFER_SIZE, midiEvents, midiEventCount); } } + void onReset() override + { + midiOutExpander = nullptr; + } + void onSampleRateChange(const SampleRateChangeEvent& e) override { if (fCarlaPluginHandle == nullptr) return; + midiOutExpander = nullptr; + fCarlaPluginDescriptor->deactivate(fCarlaPluginHandle); fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED, 0, 0, nullptr, e.sampleRate); @@ -434,6 +447,16 @@ static const NativeTimeInfo* host_get_time_info(const NativeHostHandle handle) static bool host_write_midi_event(const NativeHostHandle handle, const NativeMidiEvent* const event) { + if (CardinalExpanderFromCarlaMIDIToCV* const expander = static_cast(handle)->midiOutExpander) + { + if (expander->midiEventCount == CardinalExpanderFromCarlaMIDIToCV::MAX_MIDI_EVENTS) + return false; + + NativeMidiEvent& expanderEvent(expander->midiEvents[expander->midiEventCount++]); + carla_copyStruct(expanderEvent, *event); + return true; + } + return false; } @@ -464,6 +487,7 @@ static intptr_t host_dispatcher(const NativeHostHandle handle, const NativeHostD struct CarlaModuleWidget : ModuleWidgetWith9HP, IdleCallback { CarlaModule* const module; bool hasLeftSideExpander = false; + bool hasRightSideExpander = false; bool idleCallbackActive = false; bool visible = false; @@ -578,7 +602,6 @@ struct CarlaModuleWidget : ModuleWidgetWith9HP, IdleCallback { void draw(const DrawArgs& args) override { drawBackground(args.vg); - drawOutputJacksArea(args.vg, CarlaModule::NUM_INPUTS); if (hasLeftSideExpander) { @@ -599,6 +622,21 @@ struct CarlaModuleWidget : ModuleWidgetWith9HP, IdleCallback { } } + if (hasRightSideExpander) + { + nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0)); + + for (int i=0; i<6; ++i) + { + const float y = 90 + 49 * i - 19; + nvgBeginPath(args.vg); + nvgRect(args.vg, box.size.x - 19, y, 18, 49 - 4); + nvgFill(args.vg); + } + } + + drawOutputJacksArea(args.vg, CarlaModule::NUM_INPUTS); + setupTextLines(args.vg); drawTextLine(args.vg, 0, "Audio 1"); @@ -621,6 +659,10 @@ struct CarlaModuleWidget : ModuleWidgetWith9HP, IdleCallback { && module->leftExpander.module != nullptr && module->leftExpander.module->model == modelExpanderInputMIDI; + hasRightSideExpander = module != nullptr + && module->rightExpander.module != nullptr + && module->rightExpander.module->model == modelExpanderOutputMIDI; + ModuleWidgetWith9HP::step(); } diff --git a/plugins/Cardinal/src/Expander.hpp b/plugins/Cardinal/src/Expander.hpp index aa50504..4edbd20 100644 --- a/plugins/Cardinal/src/Expander.hpp +++ b/plugins/Cardinal/src/Expander.hpp @@ -29,7 +29,16 @@ struct CardinalExpander : Module { struct CardinalExpanderFromCVToCarlaMIDI : CardinalExpander<6, 0> { static const constexpr uint MAX_MIDI_EVENTS = 128; - // continuously filled up, flushed on each new block frame + // continuously filled up by expander, flushed on each new block frame + // frames are related to host block size uint frame, midiEventCount; NativeMidiEvent midiEvents[MAX_MIDI_EVENTS]; }; + +struct CardinalExpanderFromCarlaMIDIToCV : CardinalExpander<0, 6> { + static const constexpr uint MAX_MIDI_EVENTS = 128; + // filled up by connector-side in bursts, must be reset on next cycle by expander + // frames are not related to any particular block size + uint midiEventCount; + NativeMidiEvent midiEvents[MAX_MIDI_EVENTS]; +}; diff --git a/plugins/Cardinal/src/ExpanderInputMIDI.cpp b/plugins/Cardinal/src/ExpanderInputMIDI.cpp index a1753a4..921130f 100644 --- a/plugins/Cardinal/src/ExpanderInputMIDI.cpp +++ b/plugins/Cardinal/src/ExpanderInputMIDI.cpp @@ -64,6 +64,7 @@ struct CardinalExpanderForInputMIDI : CardinalExpanderFromCVToCarlaMIDI { CardinalExpanderForInputMIDI() { static_assert(NUM_INPUTS == kNumInputs, "Invalid input configuration"); + static_assert(NUM_OUTPUTS == kNumOutputs, "Invalid output configuration"); config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configInput(PITCH_INPUT, "1V/octave pitch"); configInput(GATE_INPUT, "Gate"); @@ -333,8 +334,6 @@ struct CardinalExpanderForInputMIDIWidget : ModuleWidgetWith3HP { nvgRoundedRect(args.vg, 6.5f, startY - 19.0f, 3.0f, padding * 6.0f - 4.0f, 1); nvgFill(args.vg); - nvgBeginPath(args.vg); - nvgRect(args.vg, box.size.x * 0.5f, 0, box.size.x, box.size.y); nvgFillColor(args.vg, color::BLACK); nvgFontFaceId(args.vg, 0); nvgFontSize(args.vg, 11); diff --git a/plugins/Cardinal/src/ExpanderOutputMIDI.cpp b/plugins/Cardinal/src/ExpanderOutputMIDI.cpp new file mode 100644 index 0000000..776ff83 --- /dev/null +++ b/plugins/Cardinal/src/ExpanderOutputMIDI.cpp @@ -0,0 +1,617 @@ +/* + * 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. + */ + +#include "Expander.hpp" +#include "ModuleWidgets.hpp" + +// -------------------------------------------------------------------------------------------------------------------- + +/** + * This class contains a substantial amount of code from VCVRack's 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. + */ + +struct CardinalExpanderForOutputMIDI : CardinalExpanderFromCarlaMIDIToCV { + enum ParamIds { + NUM_PARAMS + }; + enum InputIds { + NUM_INPUTS + }; + enum OutputIds { + PITCH_OUTPUT, + GATE_OUTPUT, + VELOCITY_OUTPUT, + AFTERTOUCH_OUTPUT, + PITCHBEND_OUTPUT, + MODWHEEL_OUTPUT, + NUM_OUTPUTS + }; + enum LightIds { + NUM_LIGHTS + }; + + NativeMidiEvent midiEventsCopy[MAX_MIDI_EVENTS]; + const NativeMidiEvent* midiEventsPtr; + uint32_t midiEventsLeft; + uint32_t midiEventFrame; + uint8_t channel; + Module* lastConnectedModule; + midi::Message converterMsg; + + // stuff from Rack + 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]; + + CardinalExpanderForOutputMIDI() { + static_assert(NUM_INPUTS == kNumInputs, "Invalid input configuration"); + static_assert(NUM_OUTPUTS == kNumOutputs, "Invalid output configuration"); + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + 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"); + onReset(); + } + + void reset() + { + midiEventsPtr = nullptr; + midiEventCount = 0; + midiEventsLeft = 0; + midiEventFrame = 0; + channel = 0; + smooth = true; + channels = 1; + polyMode = ROTATE_MODE; + panic(); + } + + 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 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(); + } + + void onReset() override + { + reset(); + channel = 0; + lastConnectedModule = nullptr; + } + + void process(const ProcessArgs& args) override + { + // only handle messages if there is something close to us + if (leftExpander.module == nullptr) + { + // something was connected before, but not anymore, reset + if (midiEventCount != 0) + onReset(); + return; + } + else if (lastConnectedModule != nullptr && lastConnectedModule != leftExpander.module) + { + // whatever we were connected to has changed, reset + lastConnectedModule = leftExpander.module; + if (midiEventCount != 0) + onReset(); + return; + } + + // check if expanding side has messages for us + if (midiEventCount != 0) + { + midiEventFrame = 0; + midiEventsLeft = midiEventCount; + midiEventsPtr = midiEventsCopy; + std::memcpy(midiEventsCopy, midiEvents, sizeof(NativeMidiEvent)*midiEventCount); + // reset as required + midiEventCount = 0; + } + + while (midiEventsLeft != 0) + { + const NativeMidiEvent& midiEvent(*midiEventsPtr); + + if (midiEvent.time > midiEventFrame) + break; + + ++midiEventsPtr; + --midiEventsLeft; + + const uint8_t* const data = midiEvent.data; + + if (channel != 0 && data[0] < 0xF0) + { + if ((data[0] & 0x0F) != (channel - 1)) + continue; + } + + converterMsg.frame = midiEventFrame; + std::memcpy(converterMsg.bytes.data(), data, midiEvent.size); + + processMessage(converterMsg); + } + + ++midiEventFrame; + + // Rack stuff + outputs[PITCH_OUTPUT].setChannels(channels); + outputs[GATE_OUTPUT].setChannels(channels); + outputs[VELOCITY_OUTPUT].setChannels(channels); + outputs[AFTERTOUCH_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); + } + } + + 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; + 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; + } + } + + 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; + } + } + } + } + } + + json_t* dataToJson() override + { + json_t* const rootJ = json_object(); + DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr); + + json_object_set_new(rootJ, "smooth", json_boolean(smooth)); + json_object_set_new(rootJ, "channels", json_integer(channels)); + json_object_set_new(rootJ, "polyMode", json_integer(polyMode)); + + // Saving/restoring pitch and mod doesn't make much sense for MPE. + if (polyMode != MPE_MODE) + { + json_object_set_new(rootJ, "lastPitch", json_integer(pws[0])); + json_object_set_new(rootJ, "lastMod", json_integer(mods[0])); + } + + json_object_set_new(rootJ, "channel", json_integer(channel)); + return rootJ; + } + + void dataFromJson(json_t* const rootJ) override + { + if (json_t* const smoothJ = json_object_get(rootJ, "smooth")) + smooth = json_boolean_value(smoothJ); + + if (json_t* const channelsJ = json_object_get(rootJ, "channels")) + setChannels(json_integer_value(channelsJ)); + + if (json_t* const polyModeJ = json_object_get(rootJ, "polyMode")) + polyMode = (PolyMode) json_integer_value(polyModeJ); + + if (json_t* const lastPitchJ = json_object_get(rootJ, "lastPitch")) + pws[0] = json_integer_value(lastPitchJ); + + if (json_t* const lastModJ = json_object_get(rootJ, "lastMod")) + mods[0] = json_integer_value(lastModJ); + + if (json_t* const channelJ = json_object_get(rootJ, "channel")) + channel = json_integer_value(channelJ); + } +}; + +// -------------------------------------------------------------------------------------------------------------------- + +struct CardinalExpanderForOutputMIDIWidget : ModuleWidgetWith3HP { + static constexpr const float startX = 1.0f; + static constexpr const float startY = 90.0f; + static constexpr const float padding = 49.0f; + + CardinalExpanderForOutputMIDI* const module; + + CardinalExpanderForOutputMIDIWidget(CardinalExpanderForOutputMIDI* const m) + : module(m) + { + setModule(m); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/ExpanderMIDI.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + for (int i=0; i(Vec(startX + 4.0f, startY + padding * i), m, i)); + } + + void draw(const DrawArgs& args) override + { + drawBackground(args.vg); + + nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0)); + + nvgSave(args.vg); + nvgIntersectScissor(args.vg, startX, 0.0f, box.size.x - startX - 1.0f, box.size.y); + + for (int i=0; iaddChild(new MenuSeparator); + + menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->smooth)); + + struct ChannelItem : MenuItem { + CardinalExpanderForOutputMIDI* module; + Menu* createChildMenu() override { + Menu* menu = new Menu; + for (int c = 0; c <= 16; c++) { + menu->addChild(createCheckMenuItem((c == 0) ? "All" : string::f("%d", c), "", + [=]() {return module->channel == c;}, + [=]() {module->channel = c;} + )); + } + return menu; + } + }; + ChannelItem* const channelItem = new ChannelItem; + channelItem->text = "MIDI channel"; + channelItem->rightText = (module->channel ? string::f("%d", module->channel) : "All") + " " + RIGHT_ARROW; + channelItem->module = module; + menu->addChild(channelItem); + + struct PolyphonyChannelItem : MenuItem { + CardinalExpanderForOutputMIDI* 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; + } + }; + PolyphonyChannelItem* const polyphonyChannelItem = new PolyphonyChannelItem; + polyphonyChannelItem->text = "Polyphony channels"; + polyphonyChannelItem->rightText = string::f("%d", module->channels) + " " + RIGHT_ARROW; + polyphonyChannelItem->module = module; + menu->addChild(polyphonyChannelItem); + + menu->addChild(createIndexPtrSubmenuItem("Polyphony mode", { + "Rotate", + "Reuse", + "Reset", + "MPE", + }, &module->polyMode)); + + menu->addChild(createMenuItem("Panic", "", + [=]() { module->panic(); } + )); + } +}; + +// -------------------------------------------------------------------------------------------------------------------- + +Model* modelExpanderOutputMIDI = createModel("ExpanderOutputMIDI"); + +// -------------------------------------------------------------------------------------------------------------------- diff --git a/plugins/Cardinal/src/HostMIDI.cpp b/plugins/Cardinal/src/HostMIDI.cpp index 62ad2bf..f3852f5 100644 --- a/plugins/Cardinal/src/HostMIDI.cpp +++ b/plugins/Cardinal/src/HostMIDI.cpp @@ -238,7 +238,7 @@ struct HostMIDI : Module { 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); diff --git a/plugins/Cardinal/src/Ildaeil.cpp b/plugins/Cardinal/src/Ildaeil.cpp index bd99c66..4fe0d70 100644 --- a/plugins/Cardinal/src/Ildaeil.cpp +++ b/plugins/Cardinal/src/Ildaeil.cpp @@ -147,6 +147,7 @@ struct IldaeilModule : Module { float audioDataOut2[BUFFER_SIZE]; unsigned audioDataFill = 0; int64_t lastBlockFrame = -1; + CardinalExpanderFromCarlaMIDIToCV* midiOutExpander = nullptr; volatile bool resetMeterIn = true; volatile bool resetMeterOut = true; @@ -422,6 +423,11 @@ struct IldaeilModule : Module { midiEventCount = 0; } + if ((midiOutExpander = rightExpander.module != nullptr && rightExpander.module->model == modelExpanderOutputMIDI + ? static_cast(rightExpander.module) + : nullptr)) + midiOutExpander->midiEventCount = 0; + audioDataFill = 0; float* ins[2] = { audioDataIn1, audioDataIn2 }; float* outs[2] = { audioDataOut1, audioDataOut2 }; @@ -447,6 +453,7 @@ struct IldaeilModule : Module { void onReset() override { resetMeterIn = resetMeterOut = true; + midiOutExpander = nullptr; } void onSampleRateChange(const SampleRateChangeEvent& e) override @@ -455,6 +462,7 @@ struct IldaeilModule : Module { return; resetMeterIn = resetMeterOut = true; + midiOutExpander = nullptr; fCarlaPluginDescriptor->deactivate(fCarlaPluginHandle); fCarlaPluginDescriptor->dispatcher(fCarlaPluginHandle, NATIVE_PLUGIN_OPCODE_SAMPLE_RATE_CHANGED, @@ -491,6 +499,16 @@ static const NativeTimeInfo* host_get_time_info(const NativeHostHandle handle) static bool host_write_midi_event(const NativeHostHandle handle, const NativeMidiEvent* const event) { + if (CardinalExpanderFromCarlaMIDIToCV* const expander = static_cast(handle)->midiOutExpander) + { + if (expander->midiEventCount == CardinalExpanderFromCarlaMIDIToCV::MAX_MIDI_EVENTS) + return false; + + NativeMidiEvent& expanderEvent(expander->midiEvents[expander->midiEventCount++]); + carla_copyStruct(expanderEvent, *event); + return true; + } + return false; } @@ -1595,6 +1613,7 @@ struct IldaeilNanoMeterOut : NanoMeter { struct IldaeilModuleWidget : ModuleWidgetWithSideScrews<26> { bool hasLeftSideExpander = false; + bool hasRightSideExpander = false; IldaeilWidget* ildaeilWidget = nullptr; IldaeilModuleWidget(IldaeilModule* const module) @@ -1631,7 +1650,6 @@ struct IldaeilModuleWidget : ModuleWidgetWithSideScrews<26> { void draw(const DrawArgs& args) override { drawBackground(args.vg); - drawOutputJacksArea(args.vg, 2); if (hasLeftSideExpander) { @@ -1652,6 +1670,36 @@ struct IldaeilModuleWidget : ModuleWidgetWithSideScrews<26> { } } + if (hasRightSideExpander) + { + // i == 0 + nvgBeginPath(args.vg); + nvgRect(args.vg, box.size.x - 19, 90 - 19, 18, 49 - 4); + nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0)); + nvgFill(args.vg); + + // gradient for i > 0 + nvgBeginPath(args.vg); + nvgRect(args.vg, box.size.x - 19, 90 + 49 - 23, 18, 49 * 5); + nvgFillPaint(args.vg, nvgLinearGradient(args.vg, + box.size.x - 19, 0, box.size.x - 1, 0, + nvgRGBA(0xd0, 0xd0, 0xd0, 0), nvgRGB(0xd0, 0xd0, 0xd0))); + nvgFill(args.vg); + + for (int i=1; i<6; ++i) + { + const float y = 90 + 49 * i - 23; + const int col1 = 0x18 + static_cast((y / box.size.y) * (0x21 - 0x18) + 0.5f); + const int col2 = 0x19 + static_cast((y / box.size.y) * (0x22 - 0x19) + 0.5f); + nvgBeginPath(args.vg); + nvgRect(args.vg, box.size.x - 19, y, 18, 4); + nvgFillColor(args.vg, nvgRGB(col1, col2, col2)); + nvgFill(args.vg); + } + } + + drawOutputJacksArea(args.vg, 2); + ModuleWidgetWithSideScrews<26>::draw(args); } @@ -1661,6 +1709,10 @@ struct IldaeilModuleWidget : ModuleWidgetWithSideScrews<26> { && module->leftExpander.module != nullptr && module->leftExpander.module->model == modelExpanderInputMIDI; + hasRightSideExpander = module != nullptr + && module->rightExpander.module != nullptr + && module->rightExpander.module->model == modelExpanderOutputMIDI; + ModuleWidgetWithSideScrews<26>::step(); } diff --git a/plugins/Cardinal/src/plugin.hpp b/plugins/Cardinal/src/plugin.hpp index cf50652..15ddd85 100644 --- a/plugins/Cardinal/src/plugin.hpp +++ b/plugins/Cardinal/src/plugin.hpp @@ -31,6 +31,7 @@ extern Model* modelAudioFile; extern Model* modelCarla; extern Model* modelCardinalBlank; extern Model* modelExpanderInputMIDI; +extern Model* modelExpanderOutputMIDI; extern Model* modelGlBars; extern Model* modelHostAudio2; extern Model* modelHostAudio8; diff --git a/plugins/Makefile b/plugins/Makefile index 78f15f6..0985106 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -188,6 +188,7 @@ PLUGIN_FILES = plugins.cpp PLUGIN_FILES += Cardinal/src/Blank.cpp PLUGIN_FILES += Cardinal/src/ExpanderInputMIDI.cpp +PLUGIN_FILES += Cardinal/src/ExpanderOutputMIDI.cpp PLUGIN_FILES += Cardinal/src/glBars.cpp PLUGIN_FILES += Cardinal/src/HostAudio.cpp PLUGIN_FILES += Cardinal/src/HostCV.cpp diff --git a/plugins/plugins.cpp b/plugins/plugins.cpp index 1af3179..1ab6c27 100644 --- a/plugins/plugins.cpp +++ b/plugins/plugins.cpp @@ -675,6 +675,7 @@ static void initStatic__Cardinal() { p->addModel(modelCardinalBlank); p->addModel(modelExpanderInputMIDI); + p->addModel(modelExpanderOutputMIDI); p->addModel(modelGlBars); p->addModel(modelHostAudio2); p->addModel(modelHostAudio8);