#include #include "plugin.hpp" namespace rack { namespace core { struct MIDI_CV : Module { enum ParamIds { NUM_PARAMS }; enum InputIds { NUM_INPUTS }; enum OutputIds { CV_OUTPUT, GATE_OUTPUT, VELOCITY_OUTPUT, AFTERTOUCH_OUTPUT, PITCH_OUTPUT, MOD_OUTPUT, RETRIGGER_OUTPUT, CLOCK_OUTPUT, CLOCK_DIV_OUTPUT, START_OUTPUT, STOP_OUTPUT, CONTINUE_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; midi::InputQueue midiInput; bool smooth; int channels; enum PolyMode { ROTATE_MODE, REUSE_MODE, RESET_MODE, MPE_MODE, NUM_POLY_MODES }; PolyMode polyMode; uint32_t clock = 0; int clockDivision; 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; // 16 channels for MPE. When MPE is disabled, only the first channel is used. uint16_t pitches[16]; uint8_t mods[16]; dsp::ExponentialFilter pitchFilters[16]; dsp::ExponentialFilter modFilters[16]; dsp::PulseGenerator clockPulse; dsp::PulseGenerator clockDividerPulse; dsp::PulseGenerator retriggerPulses[16]; dsp::PulseGenerator startPulse; dsp::PulseGenerator stopPulse; dsp::PulseGenerator continuePulse; MIDI_CV() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configOutput(CV_OUTPUT, "V/oct"); configOutput(GATE_OUTPUT, "Gate"); configOutput(VELOCITY_OUTPUT, "Velocity"); configOutput(AFTERTOUCH_OUTPUT, "Aftertouch"); configOutput(PITCH_OUTPUT, "Pitch wheel"); configOutput(MOD_OUTPUT, "Mod wheel"); configOutput(RETRIGGER_OUTPUT, "Retrigger"); configOutput(CLOCK_OUTPUT, "Clock"); configOutput(CLOCK_DIV_OUTPUT, "Clock divider"); configOutput(START_OUTPUT, "Start"); configOutput(STOP_OUTPUT, "Stop"); configOutput(CONTINUE_OUTPUT, "Continue"); heldNotes.reserve(128); for (int c = 0; c < 16; c++) { pitchFilters[c].setTau(1 / 30.f); modFilters[c].setTau(1 / 30.f); } onReset(); } void onReset() override { smooth = true; channels = 1; polyMode = ROTATE_MODE; clockDivision = 24; panic(); midiInput.reset(); } /** Resets performance state */ void panic() { for (int c = 0; c < 16; c++) { notes[c] = 60; gates[c] = false; velocities[c] = 0; aftertouches[c] = 0; pitches[c] = 8192; mods[c] = 0; pitchFilters[c].reset(); modFilters[c].reset(); } pedal = false; rotateIndex = -1; heldNotes.clear(); } void process(const ProcessArgs& args) override { while (!midiInput.queue.empty()) { const midi::Message& msg = midiInput.queue.front(); // Don't process MIDI message until we've reached its frame. if (msg.frame > args.frame) break; processMessage(msg); midiInput.queue.pop(); } outputs[CV_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[CV_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); outputs[RETRIGGER_OUTPUT].setVoltage(retriggerPulses[c].process(args.sampleTime) ? 10.f : 0.f, c); } // Set pitch and mod wheel auto updatePitch = [&](int c) { float pitch = ((int) pitches[c] - 8192) / 8191.f; pitch = clamp(pitch, -1.f, 1.f); if (smooth) pitch = pitchFilters[c].process(args.sampleTime, pitch); else pitchFilters[c].out = pitch; outputs[PITCH_OUTPUT].setVoltage(pitchFilters[c].out * 5.f); }; auto updateMod = [&](int c) { float mod = mods[c] / 127.f; mod = clamp(mod, 0.f, 1.f); if (smooth) modFilters[c].process(args.sampleTime, mod); else modFilters[c].out = mod; outputs[MOD_OUTPUT].setVoltage(modFilters[c].out * 10.f); }; if (polyMode == MPE_MODE) { for (int c = 0; c < channels; c++) { updatePitch(c); outputs[PITCH_OUTPUT].setChannels(1); updateMod(c); outputs[MOD_OUTPUT].setChannels(1); } } else { updatePitch(0); outputs[PITCH_OUTPUT].setChannels(1); updateMod(0); outputs[MOD_OUTPUT].setChannels(1); } outputs[CLOCK_OUTPUT].setVoltage(clockPulse.process(args.sampleTime) ? 10.f : 0.f); outputs[CLOCK_DIV_OUTPUT].setVoltage(clockDividerPulse.process(args.sampleTime) ? 10.f : 0.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.frame, 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; pitches[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()) { // Timing case 0x8: { clockPulse.trigger(1e-3); if (clock % clockDivision == 0) { clockDividerPulse.trigger(1e-3); } clock++; } break; // Start case 0xa: { startPulse.trigger(1e-3); clock = 0; } break; // Continue case 0xb: { continuePulse.trigger(1e-3); } break; // Stop case 0xc: { stopPulse.trigger(1e-3); clock = 0; } 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; retriggerPulses[*channel].trigger(1e-3); } 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(int channels) { if (channels == this->channels) return; this->channels = channels; panic(); } void setPolyMode(PolyMode polyMode) { if (polyMode == this->polyMode) return; this->polyMode = polyMode; panic(); } json_t* dataToJson() override { json_t* rootJ = json_object(); 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)); json_object_set_new(rootJ, "clockDivision", json_integer(clockDivision)); // Saving/restoring pitch and mod doesn't make much sense for MPE. if (polyMode != MPE_MODE) { json_object_set_new(rootJ, "lastPitch", json_integer(pitches[0])); json_object_set_new(rootJ, "lastMod", json_integer(mods[0])); } json_object_set_new(rootJ, "midi", midiInput.toJson()); return rootJ; } void dataFromJson(json_t* rootJ) override { json_t* smoothJ = json_object_get(rootJ, "smooth"); if (smoothJ) smooth = json_boolean_value(smoothJ); json_t* channelsJ = json_object_get(rootJ, "channels"); if (channelsJ) setChannels(json_integer_value(channelsJ)); json_t* polyModeJ = json_object_get(rootJ, "polyMode"); if (polyModeJ) polyMode = (PolyMode) json_integer_value(polyModeJ); json_t* clockDivisionJ = json_object_get(rootJ, "clockDivision"); if (clockDivisionJ) clockDivision = json_integer_value(clockDivisionJ); json_t* lastPitchJ = json_object_get(rootJ, "lastPitch"); if (lastPitchJ) pitches[0] = json_integer_value(lastPitchJ); json_t* lastModJ = json_object_get(rootJ, "lastMod"); if (lastModJ) mods[0] = json_integer_value(lastModJ); json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); } }; struct MIDI_CVWidget : ModuleWidget { MIDI_CVWidget(MIDI_CV* module) { setModule(module); setPanel(APP->window->loadSvg(asset::system("res/Core/MIDI-CV.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(4.61505, 60.1445)), module, MIDI_CV::CV_OUTPUT)); addOutput(createOutput(mm2px(Vec(16.214, 60.1445)), module, MIDI_CV::GATE_OUTPUT)); addOutput(createOutput(mm2px(Vec(27.8143, 60.1445)), module, MIDI_CV::VELOCITY_OUTPUT)); addOutput(createOutput(mm2px(Vec(4.61505, 76.1449)), module, MIDI_CV::AFTERTOUCH_OUTPUT)); addOutput(createOutput(mm2px(Vec(16.214, 76.1449)), module, MIDI_CV::PITCH_OUTPUT)); addOutput(createOutput(mm2px(Vec(27.8143, 76.1449)), module, MIDI_CV::MOD_OUTPUT)); addOutput(createOutput(mm2px(Vec(4.61505, 92.1439)), module, MIDI_CV::CLOCK_OUTPUT)); addOutput(createOutput(mm2px(Vec(16.214, 92.1439)), module, MIDI_CV::CLOCK_DIV_OUTPUT)); addOutput(createOutput(mm2px(Vec(27.8143, 92.1439)), module, MIDI_CV::RETRIGGER_OUTPUT)); addOutput(createOutput(mm2px(Vec(4.61505, 108.144)), module, MIDI_CV::START_OUTPUT)); addOutput(createOutput(mm2px(Vec(16.214, 108.144)), module, MIDI_CV::STOP_OUTPUT)); addOutput(createOutput(mm2px(Vec(27.8143, 108.144)), module, MIDI_CV::CONTINUE_OUTPUT)); MidiWidget* midiWidget = createWidget(mm2px(Vec(3.41891, 14.8373))); midiWidget->box.size = mm2px(Vec(33.840, 28)); midiWidget->setMidiPort(module ? &module->midiInput : NULL); addChild(midiWidget); } void appendContextMenu(Menu* menu) override { MIDI_CV* module = dynamic_cast(this->module); menu->addChild(new MenuSeparator); struct SmoothItem : MenuItem { MIDI_CV* module; void onAction(const ActionEvent& e) override { module->smooth ^= true; } }; SmoothItem* smoothItem = new SmoothItem; smoothItem->text = "Smooth pitch/mod wheel"; smoothItem->rightText = CHECKMARK(module->smooth); smoothItem->module = module; menu->addChild(smoothItem); struct ClockDivisionValueItem : MenuItem { MIDI_CV* module; int clockDivision; void onAction(const ActionEvent& e) override { module->clockDivision = clockDivision; } }; struct ClockDivisionItem : MenuItem { MIDI_CV* module; Menu* createChildMenu() override { Menu* menu = new Menu; std::vector divisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1}; std::vector divisionNames = {"Whole", "Half", "Quarter", "8th", "16th", "32nd", "12 PPQN", "24 PPQN"}; for (size_t i = 0; i < divisions.size(); i++) { ClockDivisionValueItem* item = new ClockDivisionValueItem; item->text = divisionNames[i]; item->rightText = CHECKMARK(module->clockDivision == divisions[i]); item->module = module; item->clockDivision = divisions[i]; menu->addChild(item); } return menu; } }; ClockDivisionItem* clockDivisionItem = new ClockDivisionItem; clockDivisionItem->text = "CLK/N divider"; clockDivisionItem->rightText = RIGHT_ARROW; clockDivisionItem->module = module; menu->addChild(clockDivisionItem); struct ChannelValueItem : MenuItem { MIDI_CV* module; int channels; void onAction(const ActionEvent& e) override { module->setChannels(channels); } }; struct ChannelItem : MenuItem { MIDI_CV* module; Menu* createChildMenu() override { Menu* menu = new Menu; for (int channels = 1; channels <= 16; channels++) { ChannelValueItem* item = new ChannelValueItem; if (channels == 1) item->text = "Monophonic"; else item->text = string::f("%d", channels); item->rightText = CHECKMARK(module->channels == channels); item->module = module; item->channels = channels; menu->addChild(item); } 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); struct PolyModeValueItem : MenuItem { MIDI_CV* module; MIDI_CV::PolyMode polyMode; void onAction(const ActionEvent& e) override { module->setPolyMode(polyMode); } }; struct PolyModeItem : MenuItem { MIDI_CV* module; Menu* createChildMenu() override { Menu* menu = new Menu; std::vector polyModeNames = { "Rotate", "Reuse", "Reset", "MPE", }; for (int i = 0; i < MIDI_CV::NUM_POLY_MODES; i++) { MIDI_CV::PolyMode polyMode = (MIDI_CV::PolyMode) i; PolyModeValueItem* item = new PolyModeValueItem; item->text = polyModeNames[i]; item->rightText = CHECKMARK(module->polyMode == polyMode); item->module = module; item->polyMode = polyMode; menu->addChild(item); } return menu; } }; PolyModeItem* polyModeItem = new PolyModeItem; polyModeItem->text = "Polyphony mode"; polyModeItem->rightText = RIGHT_ARROW; polyModeItem->module = module; menu->addChild(polyModeItem); struct PanicItem : MenuItem { MIDI_CV* module; void onAction(const ActionEvent& e) override { module->panic(); } }; PanicItem* panicItem = new PanicItem; panicItem->text = "Panic"; panicItem->module = module; menu->addChild(panicItem); // Example of using appendMidiMenu() // menu->addChild(new MenuSeparator); // appendMidiMenu(menu, &module->midiInput); } }; // Use legacy slug for compatibility Model* modelMIDI_CV = createModel("MIDIToCVInterface"); } // namespace core } // namespace rack