| @@ -34,49 +34,7 @@ struct MIDI_CV : Module { | |||||
| }; | }; | ||||
| midi::InputQueue midiInput; | midi::InputQueue midiInput; | ||||
| /** Number of semitones to bend up/down by pitch wheel */ | |||||
| float pwRange; | |||||
| bool smooth; | |||||
| int clockDivision; | |||||
| int channels; | |||||
| enum PolyMode { | |||||
| ROTATE_MODE, | |||||
| REUSE_MODE, | |||||
| RESET_MODE, | |||||
| MPE_MODE, | |||||
| NUM_POLY_MODES | |||||
| }; | |||||
| PolyMode polyMode; | |||||
| uint32_t clock = 0; | |||||
| bool pedal; | |||||
| // Indexed by channel | |||||
| uint8_t notes[16]; | |||||
| bool gates[16]; | |||||
| uint8_t velocities[16]; | |||||
| uint8_t aftertouches[16]; | |||||
| std::vector<uint8_t> 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 clockPulse; | |||||
| dsp::PulseGenerator clockDividerPulse; | |||||
| dsp::PulseGenerator retriggerPulses[16]; | |||||
| dsp::PulseGenerator startPulse; | |||||
| dsp::PulseGenerator stopPulse; | |||||
| dsp::PulseGenerator continuePulse; | |||||
| dsp::MidiParser<16> midiParser; | |||||
| MIDI_CV() { | MIDI_CV() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | ||||
| @@ -92,401 +50,65 @@ struct MIDI_CV : Module { | |||||
| configOutput(START_OUTPUT, "Start trigger"); | configOutput(START_OUTPUT, "Start trigger"); | ||||
| configOutput(STOP_OUTPUT, "Stop trigger"); | configOutput(STOP_OUTPUT, "Stop trigger"); | ||||
| configOutput(CONTINUE_OUTPUT, "Continue trigger"); | configOutput(CONTINUE_OUTPUT, "Continue trigger"); | ||||
| heldNotes.reserve(128); | |||||
| for (int c = 0; c < 16; c++) { | |||||
| pwFilters[c].setTau(1 / 30.f); | |||||
| modFilters[c].setTau(1 / 30.f); | |||||
| } | |||||
| onReset(); | |||||
| } | } | ||||
| void onReset() override { | void onReset() override { | ||||
| smooth = true; | |||||
| channels = 1; | |||||
| polyMode = ROTATE_MODE; | |||||
| pwRange = 2; | |||||
| clockDivision = 24; | |||||
| panic(); | |||||
| midiParser.reset(); | |||||
| midiInput.reset(); | 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; | |||||
| pws[c] = 8192; | |||||
| mods[c] = 0; | |||||
| pwFilters[c].reset(); | |||||
| modFilters[c].reset(); | |||||
| } | |||||
| pedal = false; | |||||
| rotateIndex = -1; | |||||
| heldNotes.clear(); | |||||
| } | |||||
| void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
| midi::Message msg; | midi::Message msg; | ||||
| while (midiInput.tryPop(&msg, args.frame)) { | while (midiInput.tryPop(&msg, args.frame)) { | ||||
| processMessage(msg); | |||||
| midiParser.processMessage(msg); | |||||
| } | } | ||||
| // Set pitch wheel and mod wheel | |||||
| int wheelChannels = (polyMode == MPE_MODE) ? 16 : 1; | |||||
| float pwValues[16] = {}; | |||||
| outputs[PW_OUTPUT].setChannels(wheelChannels); | |||||
| outputs[MOD_OUTPUT].setChannels(wheelChannels); | |||||
| for (int c = 0; c < wheelChannels; c++) { | |||||
| float pw = (int16_t(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; | |||||
| pwValues[c] = pw; | |||||
| outputs[PW_OUTPUT].setVoltage(pw * 5.f, c); | |||||
| 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[MOD_OUTPUT].setVoltage(mod * 10.f, c); | |||||
| } | |||||
| midiParser.processFilters(args.sampleTime); | |||||
| // Set note outputs | // Set note outputs | ||||
| 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++) { | |||||
| float pw = pwValues[(polyMode == MPE_MODE) ? c : 0]; | |||||
| float pitch = (notes[c] - 60.f + pw * pwRange) / 12.f; | |||||
| outputs[PITCH_OUTPUT].setVoltage(pitch, 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); | |||||
| outputs[PITCH_OUTPUT].setChannels(midiParser.channels); | |||||
| outputs[GATE_OUTPUT].setChannels(midiParser.channels); | |||||
| outputs[VELOCITY_OUTPUT].setChannels(midiParser.channels); | |||||
| outputs[AFTERTOUCH_OUTPUT].setChannels(midiParser.channels); | |||||
| outputs[RETRIGGER_OUTPUT].setChannels(midiParser.channels); | |||||
| for (uint8_t c = 0; c < midiParser.channels; c++) { | |||||
| outputs[PITCH_OUTPUT].setVoltage(midiParser.getPitchVoltage(c), c); | |||||
| outputs[GATE_OUTPUT].setVoltage(midiParser.gates[c] ? 10.f : 0.f, c); | |||||
| outputs[VELOCITY_OUTPUT].setVoltage(midiParser.velocities[c] / 127.f * 10.f, c); | |||||
| outputs[AFTERTOUCH_OUTPUT].setVoltage(midiParser.aftertouches[c] / 127.f * 10.f, c); | |||||
| outputs[RETRIGGER_OUTPUT].setVoltage(midiParser.retriggerPulses[c].isHigh() ? 10.f : 0.f, c); | |||||
| } | |||||
| // Pitch and mod wheel outputs | |||||
| uint8_t wheelChannels = midiParser.getWheelChannels(); | |||||
| outputs[PW_OUTPUT].setChannels(wheelChannels); | |||||
| outputs[MOD_OUTPUT].setChannels(wheelChannels); | |||||
| for (uint8_t c = 0; c < wheelChannels; c++) { | |||||
| outputs[PW_OUTPUT].setVoltage(midiParser.getPw(c) * 5.f, c); | |||||
| outputs[MOD_OUTPUT].setVoltage(midiParser.getMod(c) * 10.f, c); | |||||
| } | } | ||||
| // Set clock and transport outputs | // Set clock and transport outputs | ||||
| 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); | |||||
| } | |||||
| outputs[CLOCK_OUTPUT].setVoltage(midiParser.clockPulse.isHigh() ? 10.f : 0.f); | |||||
| outputs[CLOCK_DIV_OUTPUT].setVoltage(midiParser.clockDividerPulse.isHigh() ? 10.f : 0.f); | |||||
| outputs[START_OUTPUT].setVoltage(midiParser.startPulse.isHigh() ? 10.f : 0.f); | |||||
| outputs[STOP_OUTPUT].setVoltage(midiParser.stopPulse.isHigh() ? 10.f : 0.f); | |||||
| outputs[CONTINUE_OUTPUT].setVoltage(midiParser.continuePulse.isHigh() ? 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()) { | |||||
| // Song Position Pointer | |||||
| case 0x2: { | |||||
| int32_t pos = int32_t(msg.getNote()) | (int32_t(msg.getValue()) << 7); | |||||
| clock = pos * 6; | |||||
| } break; | |||||
| // 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); | |||||
| } 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()) { | |||||
| // Replace note with last held note | |||||
| uint8_t lastNote = heldNotes.back(); | |||||
| notes[0] = lastNote; | |||||
| } | |||||
| else { | |||||
| // Disable gate | |||||
| gates[0] = false; | |||||
| } | |||||
| } | |||||
| // Clear notes that are not held if polyphonic | |||||
| else { | |||||
| for (int c = 0; c < channels; c++) { | |||||
| if (!gates[c]) | |||||
| continue; | |||||
| // Disable all gates | |||||
| gates[c] = false; | |||||
| // Re-enable gate if channel's note is still held | |||||
| 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(); | |||||
| midiParser.processPulses(args.sampleTime); | |||||
| } | } | ||||
| json_t* dataToJson() override { | json_t* dataToJson() override { | ||||
| json_t* rootJ = json_object(); | |||||
| json_object_set_new(rootJ, "pwRange", json_real(pwRange)); | |||||
| 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(pws[0])); | |||||
| json_object_set_new(rootJ, "lastMod", json_integer(mods[0])); | |||||
| } | |||||
| json_t* rootJ = midiParser.toJson(); | |||||
| json_object_set_new(rootJ, "midi", midiInput.toJson()); | json_object_set_new(rootJ, "midi", midiInput.toJson()); | ||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| void dataFromJson(json_t* rootJ) override { | void dataFromJson(json_t* rootJ) override { | ||||
| json_t* pwRangeJ = json_object_get(rootJ, "pwRange"); | |||||
| if (pwRangeJ) | |||||
| pwRange = json_number_value(pwRangeJ); | |||||
| // For backwards compatibility, set to 0 if undefined in JSON. | // For backwards compatibility, set to 0 if undefined in JSON. | ||||
| else | |||||
| pwRange = 0; | |||||
| 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) | |||||
| pws[0] = json_integer_value(lastPitchJ); | |||||
| midiParser.pwRange = 0; | |||||
| json_t* lastModJ = json_object_get(rootJ, "lastMod"); | |||||
| if (lastModJ) | |||||
| mods[0] = json_integer_value(lastModJ); | |||||
| midiParser.fromJson(rootJ); | |||||
| json_t* midiJ = json_object_get(rootJ, "midi"); | json_t* midiJ = json_object_get(rootJ, "midi"); | ||||
| if (midiJ) | if (midiJ) | ||||
| @@ -543,35 +165,36 @@ struct MIDI_CVWidget : ModuleWidget { | |||||
| else | else | ||||
| return string::f("%g octave", pwRange / 12) + (pwRange / 12 == 1 ? "" : "s"); | return string::f("%g octave", pwRange / 12) + (pwRange / 12 == 1 ? "" : "s"); | ||||
| }; | }; | ||||
| menu->addChild(createSubmenuItem("Pitch bend range", getPwRangeLabel(module->pwRange), [=](Menu* menu) { | |||||
| menu->addChild(createSubmenuItem("Pitch bend range", getPwRangeLabel(module->midiParser.pwRange), [=](Menu* menu) { | |||||
| for (size_t i = 0; i < pwRanges.size(); i++) { | for (size_t i = 0; i < pwRanges.size(); i++) { | ||||
| menu->addChild(createCheckMenuItem(getPwRangeLabel(pwRanges[i]), "", | menu->addChild(createCheckMenuItem(getPwRangeLabel(pwRanges[i]), "", | ||||
| [=]() {return module->pwRange == pwRanges[i];}, | |||||
| [=]() {module->pwRange = pwRanges[i];} | |||||
| [=]() {return module->midiParser.pwRange == pwRanges[i];}, | |||||
| [=]() {module->midiParser.pwRange = pwRanges[i];} | |||||
| )); | )); | ||||
| } | } | ||||
| })); | })); | ||||
| menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->smooth)); | |||||
| menu->addChild(createBoolPtrMenuItem("Smooth pitch/mod wheel", "", &module->midiParser.smooth)); | |||||
| static const std::vector<int> clockDivisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1}; | |||||
| static const std::vector<uint32_t> clockDivisions = {24 * 4, 24 * 2, 24, 24 / 2, 24 / 4, 24 / 8, 2, 1}; | |||||
| static const std::vector<std::string> clockDivisionLabels = {"Whole", "Half", "Quarter", "8th", "16th", "32nd", "12 PPQN", "24 PPQN"}; | static const std::vector<std::string> clockDivisionLabels = {"Whole", "Half", "Quarter", "8th", "16th", "32nd", "12 PPQN", "24 PPQN"}; | ||||
| size_t clockDivisionIndex = std::find(clockDivisions.begin(), clockDivisions.end(), module->clockDivision) - clockDivisions.begin(); | |||||
| size_t clockDivisionIndex = std::find(clockDivisions.begin(), clockDivisions.end(), module->midiParser.clockDivision) - clockDivisions.begin(); | |||||
| std::string clockDivisionLabel = (clockDivisionIndex < clockDivisionLabels.size()) ? clockDivisionLabels[clockDivisionIndex] : ""; | std::string clockDivisionLabel = (clockDivisionIndex < clockDivisionLabels.size()) ? clockDivisionLabels[clockDivisionIndex] : ""; | ||||
| menu->addChild(createSubmenuItem("CLK/N divider", clockDivisionLabel, [=](Menu* menu) { | menu->addChild(createSubmenuItem("CLK/N divider", clockDivisionLabel, [=](Menu* menu) { | ||||
| for (size_t i = 0; i < clockDivisions.size(); i++) { | for (size_t i = 0; i < clockDivisions.size(); i++) { | ||||
| menu->addChild(createCheckMenuItem(clockDivisionLabels[i], "", | menu->addChild(createCheckMenuItem(clockDivisionLabels[i], "", | ||||
| [=]() {return module->clockDivision == clockDivisions[i];}, | |||||
| [=]() {module->clockDivision = clockDivisions[i];} | |||||
| [=]() {return module->midiParser.clockDivision == clockDivisions[i];}, | |||||
| [=]() {module->midiParser.clockDivision = clockDivisions[i];} | |||||
| )); | )); | ||||
| } | } | ||||
| })); | })); | ||||
| menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->channels), [=](Menu* menu) { | |||||
| menu->addChild(createSubmenuItem("Polyphony channels", string::f("%d", module->midiParser.channels), [=](Menu* menu) { | |||||
| for (int c = 1; c <= 16; c++) { | for (int c = 1; c <= 16; c++) { | ||||
| menu->addChild(createCheckMenuItem((c == 1) ? "Monophonic" : string::f("%d", c), "", | |||||
| [=]() {return module->channels == c;}, | |||||
| [=]() {module->setChannels(c);} | |||||
| std::string channelsLabel = (c == 1) ? "Monophonic" : string::f("%d", c); | |||||
| menu->addChild(createCheckMenuItem(channelsLabel, "", | |||||
| [=]() {return module->midiParser.channels == c;}, | |||||
| [=]() {module->midiParser.setChannels(c);} | |||||
| )); | )); | ||||
| } | } | ||||
| })); | })); | ||||
| @@ -581,10 +204,10 @@ struct MIDI_CVWidget : ModuleWidget { | |||||
| "Reuse", | "Reuse", | ||||
| "Reset", | "Reset", | ||||
| "MPE", | "MPE", | ||||
| }, &module->polyMode)); | |||||
| }, &module->midiParser.polyMode)); | |||||
| menu->addChild(createMenuItem("Panic", "", | menu->addChild(createMenuItem("Panic", "", | ||||
| [=]() {module->panic();} | |||||
| [=]() {module->midiParser.panic();} | |||||
| )); | )); | ||||
| // Example of using appendMidiMenu() | // Example of using appendMidiMenu() | ||||