#include "Core.hpp" #include "midi.hpp" #include "event.hpp" struct MIDITriggerToCVInterface : Module { enum ParamIds { NUM_PARAMS }; enum InputIds { NUM_INPUTS }; enum OutputIds { ENUMS(TRIG_OUTPUT, 16), NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; midi::InputQueue midiInput; bool gates[16]; float gateTimes[16]; uint8_t velocities[16]; int learningId = -1; uint8_t learnedNotes[16] = {}; bool velocity = false; MIDITriggerToCVInterface() { setup(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); onReset(); } void onReset() override { for (int i = 0; i < 16; i++) { gates[i] = false; gateTimes[i] = 0.f; learnedNotes[i] = i + 36; } learningId = -1; } void pressNote(uint8_t note, uint8_t vel) { // Learn if (learningId >= 0) { learnedNotes[learningId] = note; learningId = -1; } // Find id for (int i = 0; i < 16; i++) { if (learnedNotes[i] == note) { gates[i] = true; gateTimes[i] = 1e-3f; velocities[i] = vel; } } } void releaseNote(uint8_t note) { // Find id for (int i = 0; i < 16; i++) { if (learnedNotes[i] == note) { gates[i] = false; } } } void step() override { midi::Message msg; while (midiInput.shift(&msg)) { processMessage(msg); } float deltaTime = context()->engine->getSampleTime(); for (int i = 0; i < 16; i++) { if (gateTimes[i] > 0.f) { outputs[TRIG_OUTPUT + i].value = velocity ? rescale(velocities[i], 0, 127, 0.f, 10.f) : 10.f; // If the gate is off, wait 1 ms before turning the pulse off. // This avoids drum controllers sending a pulse with 0 ms duration. if (!gates[i]) { gateTimes[i] -= deltaTime; } } else { outputs[TRIG_OUTPUT + i].value = 0.f; } } } void processMessage(midi::Message msg) { switch (msg.status()) { // note off case 0x8: { releaseNote(msg.note()); } break; // note on case 0x9: { if (msg.value() > 0) { pressNote(msg.note(), msg.value()); } else { // Many stupid keyboards send a "note on" command with 0 velocity to mean "note release" releaseNote(msg.note()); } } break; default: break; } } json_t *dataToJson() override { json_t *rootJ = json_object(); json_t *notesJ = json_array(); for (int i = 0; i < 16; i++) { json_t *noteJ = json_integer(learnedNotes[i]); json_array_append_new(notesJ, noteJ); } json_object_set_new(rootJ, "notes", notesJ); json_object_set_new(rootJ, "midi", midiInput.toJson()); json_object_set_new(rootJ, "velocity", json_boolean(velocity)); 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) learnedNotes[i] = json_integer_value(noteJ); } } json_t *midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); json_t *velocityJ = json_object_get(rootJ, "velocity"); if (velocityJ) velocity = json_boolean_value(velocityJ); } }; struct MidiTrigChoice : GridChoice { MIDITriggerToCVInterface *module; int id; MidiTrigChoice() { box.size.y = mm2px(6.666); textOffset.y -= 4; textOffset.x -= 4; } void setId(int id) override { this->id = id; } void step() override { if (module->learningId == id) { text = "LRN"; color.a = 0.5; } else { uint8_t note = module->learnedNotes[id]; static const char *noteNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; int oct = note / 12 - 1; int semi = note % 12; text = string::f("%s%d", noteNames[semi], oct); color.a = 1.0; if (context()->event->selectedWidget == this) context()->event->selectedWidget = NULL; } } void onSelect(const event::Select &e) override { e.consume(this); module->learningId = id; } void onDeselect(const event::Deselect &e) override { module->learningId = -1; } }; struct MidiTrigWidget : Grid16MidiWidget { MIDITriggerToCVInterface *module; GridChoice *createGridChoice() override { MidiTrigChoice *gridChoice = new MidiTrigChoice; gridChoice->module = module; return gridChoice; } }; struct MIDITriggerToCVInterfaceWidget : ModuleWidget { MIDITriggerToCVInterfaceWidget(MIDITriggerToCVInterface *module) : ModuleWidget(module) { setPanel(SVG::load(asset::system("res/Core/MIDITriggerToCVInterface.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(createOutput(mm2px(Vec(3.894335, 73.344704)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 0)); addOutput(createOutput(mm2px(Vec(15.494659, 73.344704)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 1)); addOutput(createOutput(mm2px(Vec(27.094982, 73.344704)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 2)); addOutput(createOutput(mm2px(Vec(38.693932, 73.344704)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 3)); addOutput(createOutput(mm2px(Vec(3.8943355, 84.945023)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 4)); addOutput(createOutput(mm2px(Vec(15.49466, 84.945023)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 5)); addOutput(createOutput(mm2px(Vec(27.094982, 84.945023)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 6)); addOutput(createOutput(mm2px(Vec(38.693932, 84.945023)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 7)); addOutput(createOutput(mm2px(Vec(3.8943343, 96.543976)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 8)); addOutput(createOutput(mm2px(Vec(15.494659, 96.543976)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 9)); addOutput(createOutput(mm2px(Vec(27.09498, 96.543976)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 10)); addOutput(createOutput(mm2px(Vec(38.693932, 96.543976)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 11)); addOutput(createOutput(mm2px(Vec(3.894335, 108.14429)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 12)); addOutput(createOutput(mm2px(Vec(15.49466, 108.14429)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 13)); addOutput(createOutput(mm2px(Vec(27.09498, 108.14429)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 14)); addOutput(createOutput(mm2px(Vec(38.693932, 108.14429)), module, MIDITriggerToCVInterface::TRIG_OUTPUT + 15)); MidiTrigWidget *midiWidget = createWidget(mm2px(Vec(3.399621, 14.837339))); midiWidget->module = module; midiWidget->box.size = mm2px(Vec(44, 54.667)); midiWidget->midiIO = &module->midiInput; midiWidget->createGridChoices(); addChild(midiWidget); } void appendContextMenu(Menu *menu) override { MIDITriggerToCVInterface *module = dynamic_cast(this->module); struct VelocityItem : MenuItem { MIDITriggerToCVInterface *module; void onAction(const event::Action &e) override { module->velocity ^= true; } }; menu->addChild(new MenuEntry); VelocityItem *velocityItem = createMenuItem("Velocity", CHECKMARK(module->velocity)); velocityItem->module = module; menu->addChild(velocityItem); } }; Model *modelMIDITriggerToCVInterface = createModel("MIDITriggerToCVInterface");