#include "plugin.hpp" namespace rack { namespace core { struct MIDI_Gate : Module { enum ParamIds { NUM_PARAMS }; enum InputIds { NUM_INPUTS }; enum OutputIds { ENUMS(GATE_OUTPUTS, 16), NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; midi::InputQueue midiInput; /** True when a cell's gate is held. [cell][channel] */ bool gates[16][16]; /** Last velocity value of cell. [cell][channel] */ uint8_t velocities[16][16]; /** Triggered when a cell's note is played. [cell][channel] */ dsp::PulseGenerator trigPulses[16][16]; /** Cell ID in learn mode, or -1 if none. */ int learningId; /** [cell] */ int8_t learnedNotes[16]; // Settings enum VelocityMode { FIXED_MODE, VELOCITY_MODE, AFTERTOUCH_MODE, }; VelocityMode velocityMode; bool mpeMode; bool trigMode; MIDI_Gate() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); for (int i = 0; i < 16; i++) configOutput(GATE_OUTPUTS + i, string::f("Gate %d", i + 1)); onReset(); } void onReset() override { for (int y = 0; y < 4; y++) { for (int x = 0; x < 4; x++) { learnedNotes[4 * y + x] = 36 + 4 * (3 - y) + x; } } learningId = -1; midiInput.reset(); velocityMode = FIXED_MODE; mpeMode = false; trigMode = false; panic(); } void panic() { for (int i = 0; i < 16; i++) { for (int c = 0; c < 16; c++) { gates[i][c] = false; velocities[i][c] = 127; trigPulses[i][c].reset(); } } } void process(const ProcessArgs& args) override { midi::Message msg; while (midiInput.tryPop(&msg, args.frame)) { processMessage(msg); } int channels = mpeMode ? 16 : 1; for (int i = 0; i < 16; i++) { for (int c = 0; c < channels; c++) { bool pulse = trigPulses[i][c].process(args.sampleTime); bool gate = pulse || (!trigMode && gates[i][c]); float velocity = gate ? 1.f : 0.f; if (velocityMode != FIXED_MODE) velocity *= velocities[i][c] / 127.f; outputs[GATE_OUTPUTS + i].setVoltage(velocity * 10.f, c); } outputs[GATE_OUTPUTS + i].setChannels(channels); } } void processMessage(const midi::Message& msg) { switch (msg.getStatus()) { // note off case 0x8: { releaseNote(msg.getChannel(), msg.getNote()); } break; // note on case 0x9: { uint8_t velocity = msg.getValue(); if (velocity > 0) { pressNote(msg.getChannel(), msg.getNote(), velocity); } else { // Note-on event with velocity 0 is an alternative for note-off event. releaseNote(msg.getChannel(), msg.getNote()); } } break; // polyphonic pressure/aftertouch case 0xa: { if (velocityMode == AFTERTOUCH_MODE) { uint8_t c = mpeMode ? msg.getChannel() : 0; uint8_t note = msg.getNote(); uint8_t velocity = msg.getValue(); for (int i = 0; i < 16; i++) { if (learnedNotes[i] == note) { velocities[i][c] = velocity; } } } } break; // channel pressure/aftertouch case 0xd: { if (velocityMode == AFTERTOUCH_MODE) { uint8_t c = mpeMode ? msg.getChannel() : 0; uint8_t velocity = msg.getNote(); for (int i = 0; i < 16; i++) { velocities[i][c] = velocity; } } } break; default: break; } } void pressNote(uint8_t channel, uint8_t note, uint8_t vel) { int c = mpeMode ? channel : 0; // Learn if (learningId >= 0) { setLearnedNote(learningId, note); learningId = -1; } // Find id for (int i = 0; i < 16; i++) { if (learnedNotes[i] == note) { gates[i][c] = true; if (velocityMode == VELOCITY_MODE) velocities[i][c] = vel; trigPulses[i][c].trigger(1e-3f); } } } void releaseNote(uint8_t channel, uint8_t note) { int c = mpeMode ? channel : 0; // Find id for (int i = 0; i < 16; i++) { if (learnedNotes[i] == note) { gates[i][c] = false; } } } void setLearnedNote(int id, int8_t note) { // Unset IDs of similar note if (note >= 0) { for (int id = 0; id < 16; id++) { if (learnedNotes[id] == note) learnedNotes[id] = -1; } } learnedNotes[id] = note; } json_t* dataToJson() override { json_t* rootJ = json_object(); json_t* notesJ = json_array(); for (int i = 0; i < 16; i++) { json_array_append_new(notesJ, json_integer(learnedNotes[i])); } json_object_set_new(rootJ, "notes", notesJ); json_object_set_new(rootJ, "velocity", json_integer(velocityMode)); json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); json_object_set_new(rootJ, "trigMode", json_boolean(trigMode)); json_object_set_new(rootJ, "midi", midiInput.toJson()); return rootJ; } void dataFromJson(json_t* rootJ) override { json_t* notesJ = json_object_get(rootJ, "notes"); if (notesJ) { for (int i = 0; i < 16; i++) { json_t* noteJ = json_array_get(notesJ, i); if (noteJ) setLearnedNote(i, json_integer_value(noteJ)); } } json_t* velocityJ = json_object_get(rootJ, "velocity"); // Boolean in Rack <=v2.6.4 if (json_is_true(velocityJ)) velocityMode = VELOCITY_MODE; else if (json_is_integer(velocityJ)) velocityMode = VelocityMode(json_integer_value(velocityJ)); json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); if (mpeModeJ) mpeMode = json_boolean_value(mpeModeJ); json_t* trigModeJ = json_object_get(rootJ, "trigMode"); if (trigModeJ) trigMode = json_boolean_value(trigModeJ); json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); } }; struct MIDI_GateWidget : ModuleWidget { MIDI_GateWidget(MIDI_Gate* module) { setModule(module); setPanel(createPanel(asset::system("res/Core/MIDI_Gate.svg"), asset::system("res/Core/MIDI_Gate-dark.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); 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))); addOutput(createOutputCentered(mm2px(Vec(8.189, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 0)); addOutput(createOutputCentered(mm2px(Vec(19.739, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 1)); addOutput(createOutputCentered(mm2px(Vec(31.289, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 2)); addOutput(createOutputCentered(mm2px(Vec(42.838, 78.431)), module, MIDI_Gate::GATE_OUTPUTS + 3)); addOutput(createOutputCentered(mm2px(Vec(8.189, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 4)); addOutput(createOutputCentered(mm2px(Vec(19.739, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 5)); addOutput(createOutputCentered(mm2px(Vec(31.289, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 6)); addOutput(createOutputCentered(mm2px(Vec(42.838, 89.946)), module, MIDI_Gate::GATE_OUTPUTS + 7)); addOutput(createOutputCentered(mm2px(Vec(8.189, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 8)); addOutput(createOutputCentered(mm2px(Vec(19.739, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 9)); addOutput(createOutputCentered(mm2px(Vec(31.289, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 10)); addOutput(createOutputCentered(mm2px(Vec(42.838, 101.466)), module, MIDI_Gate::GATE_OUTPUTS + 11)); addOutput(createOutputCentered(mm2px(Vec(8.189, 112.998)), module, MIDI_Gate::GATE_OUTPUTS + 12)); addOutput(createOutputCentered(mm2px(Vec(19.739, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 13)); addOutput(createOutputCentered(mm2px(Vec(31.289, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 14)); addOutput(createOutputCentered(mm2px(Vec(42.838, 112.984)), module, MIDI_Gate::GATE_OUTPUTS + 15)); typedef Grid16MidiDisplay> TMidiDisplay; TMidiDisplay* display = createWidget(mm2px(Vec(0.0, 13.039))); display->box.size = mm2px(Vec(50.8, 55.88)); display->setMidiPort(module ? &module->midiInput : NULL); display->setModule(module); addChild(display); } void appendContextMenu(Menu* menu) override { MIDI_Gate* module = dynamic_cast(this->module); menu->addChild(new MenuSeparator); menu->addChild(createIndexPtrSubmenuItem("Gate amplitude", { "10V", "Velocity", "Aftertouch", }, &module->velocityMode)); menu->addChild(createBoolPtrMenuItem("MPE mode", "", &module->mpeMode)); menu->addChild(createBoolPtrMenuItem("Trigger mode", "", &module->trigMode)); menu->addChild(new MenuSeparator); menu->addChild(createMenuItem("Panic", "", [=]() { module->panic(); })); } }; // Use legacy slug for compatibility Model* modelMIDI_Gate = createModel("MIDITriggerToCVInterface"); } // namespace core } // namespace rack