| @@ -21,30 +21,39 @@ struct MIDI_CC : Module { | |||||
| }; | }; | ||||
| midi::InputQueue midiInput; | midi::InputQueue midiInput; | ||||
| int8_t values[128]; | |||||
| /** [cc][channel] */ | |||||
| int8_t values[128][16]; | |||||
| int learningId; | int learningId; | ||||
| int learnedCcs[16]; | int learnedCcs[16]; | ||||
| dsp::ExponentialFilter valueFilters[16]; | |||||
| /** [cell][channel] */ | |||||
| dsp::ExponentialFilter valueFilters[16][16]; | |||||
| bool mpeMode; | |||||
| MIDI_CC() { | MIDI_CC() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | ||||
| for (int i = 0; i < 16; i++) | for (int i = 0; i < 16; i++) | ||||
| configOutput(CC_OUTPUT + i, string::f("Cell %d", i + 1)); | configOutput(CC_OUTPUT + i, string::f("Cell %d", i + 1)); | ||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| valueFilters[i].setTau(1 / 30.f); | |||||
| for (int c = 0; c < 16; c++) { | |||||
| valueFilters[i][c].setTau(1 / 30.f); | |||||
| } | |||||
| } | } | ||||
| onReset(); | onReset(); | ||||
| } | } | ||||
| void onReset() override { | void onReset() override { | ||||
| for (int i = 0; i < 128; i++) { | for (int i = 0; i < 128; i++) { | ||||
| values[i] = 0; | |||||
| for (int c = 0; c < 16; c++) { | |||||
| values[i][c] = 0; | |||||
| } | |||||
| } | } | ||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| learnedCcs[i] = i; | learnedCcs[i] = i; | ||||
| } | } | ||||
| learningId = -1; | learningId = -1; | ||||
| midiInput.reset(); | midiInput.reset(); | ||||
| mpeMode = false; | |||||
| } | } | ||||
| void process(const ProcessArgs& args) override { | void process(const ProcessArgs& args) override { | ||||
| @@ -59,23 +68,28 @@ struct MIDI_CC : Module { | |||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| } | } | ||||
| int channels = mpeMode ? 16 : 1; | |||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| if (!outputs[CC_OUTPUT + i].isConnected()) | if (!outputs[CC_OUTPUT + i].isConnected()) | ||||
| continue; | continue; | ||||
| outputs[CC_OUTPUT + i].setChannels(channels); | |||||
| int cc = learnedCcs[i]; | int cc = learnedCcs[i]; | ||||
| float value = values[cc] / 127.f; | |||||
| // Detect behavior from MIDI buttons. | |||||
| if (std::fabs(valueFilters[i].out - value) >= 1.f) { | |||||
| // Jump value | |||||
| valueFilters[i].out = value; | |||||
| } | |||||
| else { | |||||
| // Smooth value with filter | |||||
| valueFilters[i].process(args.sampleTime, value); | |||||
| for (int c = 0; c < channels; c++) { | |||||
| float value = values[cc][c] / 127.f; | |||||
| // Detect behavior from MIDI buttons. | |||||
| if (std::fabs(valueFilters[i][c].out - value) >= 1.f) { | |||||
| // Jump value | |||||
| valueFilters[i][c].out = value; | |||||
| } | |||||
| else { | |||||
| // Smooth value with filter | |||||
| valueFilters[i][c].process(args.sampleTime, value); | |||||
| } | |||||
| outputs[CC_OUTPUT + i].setVoltage(valueFilters[i][c].out * 10.f, c); | |||||
| } | } | ||||
| outputs[CC_OUTPUT + i].setVoltage(valueFilters[i].out * 10.f); | |||||
| } | } | ||||
| } | } | ||||
| @@ -90,20 +104,21 @@ struct MIDI_CC : Module { | |||||
| } | } | ||||
| void processCC(const midi::Message &msg) { | void processCC(const midi::Message &msg) { | ||||
| uint8_t c = mpeMode ? msg.getChannel() : 0; | |||||
| uint8_t cc = msg.getNote(); | uint8_t cc = msg.getNote(); | ||||
| if (msg.bytes.size() < 2) | |||||
| return; | |||||
| // Allow CC to be negative if the 8th bit is set. | // Allow CC to be negative if the 8th bit is set. | ||||
| // The gamepad driver abuses this, for example. | // The gamepad driver abuses this, for example. | ||||
| // Cast uint8_t to int8_t | // Cast uint8_t to int8_t | ||||
| if (msg.bytes.size() < 2) | |||||
| return; | |||||
| int8_t value = msg.bytes[2]; | int8_t value = msg.bytes[2]; | ||||
| value = clamp(value, -127, 127); | value = clamp(value, -127, 127); | ||||
| // Learn | // Learn | ||||
| if (learningId >= 0 && values[cc] != value) { | |||||
| if (learningId >= 0 && values[cc][c] != value) { | |||||
| learnedCcs[learningId] = cc; | learnedCcs[learningId] = cc; | ||||
| learningId = -1; | learningId = -1; | ||||
| } | } | ||||
| values[cc] = value; | |||||
| values[cc][c] = value; | |||||
| } | } | ||||
| json_t* dataToJson() override { | json_t* dataToJson() override { | ||||
| @@ -118,11 +133,14 @@ struct MIDI_CC : Module { | |||||
| // Remember values so users don't have to touch MIDI controller knobs when restarting Rack | // Remember values so users don't have to touch MIDI controller knobs when restarting Rack | ||||
| json_t* valuesJ = json_array(); | json_t* valuesJ = json_array(); | ||||
| for (int i = 0; i < 128; i++) { | for (int i = 0; i < 128; i++) { | ||||
| json_array_append_new(valuesJ, json_integer(values[i])); | |||||
| // Note: Only save channel 0. Since MPE mode won't be commonly used, it's pointless to save all 16 channels. | |||||
| json_array_append_new(valuesJ, json_integer(values[i][0])); | |||||
| } | } | ||||
| json_object_set_new(rootJ, "values", valuesJ); | json_object_set_new(rootJ, "values", valuesJ); | ||||
| json_object_set_new(rootJ, "midi", midiInput.toJson()); | json_object_set_new(rootJ, "midi", midiInput.toJson()); | ||||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | |||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| @@ -141,7 +159,7 @@ struct MIDI_CC : Module { | |||||
| for (int i = 0; i < 128; i++) { | for (int i = 0; i < 128; i++) { | ||||
| json_t* valueJ = json_array_get(valuesJ, i); | json_t* valueJ = json_array_get(valuesJ, i); | ||||
| if (valueJ) { | if (valueJ) { | ||||
| values[i] = json_integer_value(valueJ); | |||||
| values[i][0] = json_integer_value(valueJ); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -149,6 +167,18 @@ struct MIDI_CC : Module { | |||||
| json_t* midiJ = json_object_get(rootJ, "midi"); | json_t* midiJ = json_object_get(rootJ, "midi"); | ||||
| if (midiJ) | if (midiJ) | ||||
| midiInput.fromJson(midiJ); | midiInput.fromJson(midiJ); | ||||
| json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); | |||||
| if (mpeModeJ) | |||||
| mpeMode = json_boolean_value(mpeModeJ); | |||||
| } | |||||
| }; | |||||
| struct MIDI_CCMpeModeItem : MenuItem { | |||||
| MIDI_CC* module; | |||||
| void onAction(const event::Action& e) override { | |||||
| module->mpeMode ^= true; | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -187,6 +217,18 @@ struct MIDI_CCWidget : ModuleWidget { | |||||
| midiWidget->setModule(module); | midiWidget->setModule(module); | ||||
| addChild(midiWidget); | addChild(midiWidget); | ||||
| } | } | ||||
| void appendContextMenu(Menu* menu) override { | |||||
| MIDI_CC* module = dynamic_cast<MIDI_CC*>(this->module); | |||||
| menu->addChild(new MenuSeparator); | |||||
| MIDI_CCMpeModeItem* mpeModeItem = new MIDI_CCMpeModeItem; | |||||
| mpeModeItem->text = "MPE mode"; | |||||
| mpeModeItem->rightText = CHECKMARK(module->mpeMode); | |||||
| mpeModeItem->module = module; | |||||
| menu->addChild(mpeModeItem); | |||||
| } | |||||
| }; | }; | ||||
| @@ -22,17 +22,22 @@ struct MIDI_Gate : Module { | |||||
| midi::InputQueue midiInput; | midi::InputQueue midiInput; | ||||
| bool gates[16]; | |||||
| float gateTimes[16]; | |||||
| uint8_t velocities[16]; | |||||
| /** [cell][c] */ | |||||
| bool gates[16][16]; | |||||
| /** [cell][c] */ | |||||
| float gateTimes[16][16]; | |||||
| /** [cell][c] */ | |||||
| uint8_t velocities[16][16]; | |||||
| int learningId = -1; | int learningId = -1; | ||||
| uint8_t learnedNotes[16] = {}; | uint8_t learnedNotes[16] = {}; | ||||
| bool velocityMode = false; | bool velocityMode = false; | ||||
| bool mpeMode = false; | |||||
| MIDI_Gate() { | MIDI_Gate() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | ||||
| for (int i = 0; i < 16; i++) | for (int i = 0; i < 16; i++) | ||||
| configOutput(TRIG_OUTPUT + i, string::f("Cell %d", i + 1)); | configOutput(TRIG_OUTPUT + i, string::f("Cell %d", i + 1)); | ||||
| onReset(); | onReset(); | ||||
| } | } | ||||
| @@ -45,12 +50,16 @@ struct MIDI_Gate : Module { | |||||
| learningId = -1; | learningId = -1; | ||||
| panic(); | panic(); | ||||
| midiInput.reset(); | midiInput.reset(); | ||||
| velocityMode = false; | |||||
| mpeMode = false; | |||||
| } | } | ||||
| void panic() { | void panic() { | ||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| gates[i] = false; | |||||
| gateTimes[i] = 0.f; | |||||
| for (int c = 0; c < 16; c++) { | |||||
| gates[i][c] = false; | |||||
| gateTimes[i][c] = 0.f; | |||||
| } | |||||
| } | } | ||||
| } | } | ||||
| @@ -66,17 +75,20 @@ struct MIDI_Gate : Module { | |||||
| midiInput.queue.pop(); | midiInput.queue.pop(); | ||||
| } | } | ||||
| int channels = mpeMode ? 16 : 1; | |||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| if (gateTimes[i] > 0.f) { | |||||
| outputs[TRIG_OUTPUT + i].setVoltage(velocityMode ? 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] -= args.sampleTime; | |||||
| outputs[TRIG_OUTPUT + i].setChannels(channels); | |||||
| for (int c = 0; c < channels; c++) { | |||||
| // Make sure all pulses last longer than 1ms | |||||
| if (gates[i][c] || gateTimes[i][c] > 0.f) { | |||||
| float velocity = velocityMode ? (velocities[i][c] / 127.f) : 1.f; | |||||
| outputs[TRIG_OUTPUT + i].setVoltage(velocity * 10.f, c); | |||||
| gateTimes[i][c] -= args.sampleTime; | |||||
| } | |||||
| else { | |||||
| outputs[TRIG_OUTPUT + i].setVoltage(0.f, c); | |||||
| } | } | ||||
| } | |||||
| else { | |||||
| outputs[TRIG_OUTPUT + i].setVoltage(0.f); | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -85,29 +97,24 @@ struct MIDI_Gate : Module { | |||||
| switch (msg.getStatus()) { | switch (msg.getStatus()) { | ||||
| // note off | // note off | ||||
| case 0x8: { | case 0x8: { | ||||
| releaseNote(msg.getNote()); | |||||
| releaseNote(msg.getChannel(), msg.getNote()); | |||||
| } break; | } break; | ||||
| // note on | // note on | ||||
| case 0x9: { | case 0x9: { | ||||
| if (msg.getValue() > 0) { | if (msg.getValue() > 0) { | ||||
| pressNote(msg.getNote(), msg.getValue()); | |||||
| pressNote(msg.getChannel(), msg.getNote(), msg.getValue()); | |||||
| } | } | ||||
| else { | else { | ||||
| // I don't know why, but many keyboards send a "note on" command with 0 velocity to mean "note release" | |||||
| releaseNote(msg.getNote()); | |||||
| } | |||||
| } break; | |||||
| // all notes off (panic) | |||||
| case 0x7b: { | |||||
| if (msg.getValue() == 0) { | |||||
| panic(); | |||||
| // Many stupid keyboards send a "note on" command with 0 velocity to mean "note release" | |||||
| releaseNote(msg.getChannel(), msg.getNote()); | |||||
| } | } | ||||
| } break; | } break; | ||||
| default: break; | default: break; | ||||
| } | } | ||||
| } | } | ||||
| void pressNote(uint8_t note, uint8_t vel) { | |||||
| void pressNote(uint8_t channel, uint8_t note, uint8_t vel) { | |||||
| int c = mpeMode ? channel : 0; | |||||
| // Learn | // Learn | ||||
| if (learningId >= 0) { | if (learningId >= 0) { | ||||
| learnedNotes[learningId] = note; | learnedNotes[learningId] = note; | ||||
| @@ -116,18 +123,19 @@ struct MIDI_Gate : Module { | |||||
| // Find id | // Find id | ||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| if (learnedNotes[i] == note) { | if (learnedNotes[i] == note) { | ||||
| gates[i] = true; | |||||
| gateTimes[i] = 1e-3f; | |||||
| velocities[i] = vel; | |||||
| gates[i][c] = true; | |||||
| gateTimes[i][c] = 1e-3f; | |||||
| velocities[i][c] = vel; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| void releaseNote(uint8_t note) { | |||||
| void releaseNote(uint8_t channel, uint8_t note) { | |||||
| int c = mpeMode ? channel : 0; | |||||
| // Find id | // Find id | ||||
| for (int i = 0; i < 16; i++) { | for (int i = 0; i < 16; i++) { | ||||
| if (learnedNotes[i] == note) { | if (learnedNotes[i] == note) { | ||||
| gates[i] = false; | |||||
| gates[i][c] = false; | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -145,6 +153,8 @@ struct MIDI_Gate : Module { | |||||
| json_object_set_new(rootJ, "velocity", json_boolean(velocityMode)); | json_object_set_new(rootJ, "velocity", json_boolean(velocityMode)); | ||||
| json_object_set_new(rootJ, "midi", midiInput.toJson()); | json_object_set_new(rootJ, "midi", midiInput.toJson()); | ||||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | |||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| @@ -165,6 +175,10 @@ struct MIDI_Gate : Module { | |||||
| json_t* midiJ = json_object_get(rootJ, "midi"); | json_t* midiJ = json_object_get(rootJ, "midi"); | ||||
| if (midiJ) | if (midiJ) | ||||
| midiInput.fromJson(midiJ); | midiInput.fromJson(midiJ); | ||||
| json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); | |||||
| if (mpeModeJ) | |||||
| mpeMode = json_boolean_value(mpeModeJ); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -177,6 +191,14 @@ struct MIDI_GateVelocityItem : MenuItem { | |||||
| }; | }; | ||||
| struct MIDI_GateMpeModeItem : MenuItem { | |||||
| MIDI_Gate* module; | |||||
| void onAction(const event::Action& e) override { | |||||
| module->mpeMode ^= true; | |||||
| } | |||||
| }; | |||||
| struct MIDI_GatePanicItem : MenuItem { | struct MIDI_GatePanicItem : MenuItem { | ||||
| MIDI_Gate* module; | MIDI_Gate* module; | ||||
| void onAction(const event::Action& e) override { | void onAction(const event::Action& e) override { | ||||
| @@ -228,6 +250,12 @@ struct MIDI_GateWidget : ModuleWidget { | |||||
| velocityItem->module = module; | velocityItem->module = module; | ||||
| menu->addChild(velocityItem); | menu->addChild(velocityItem); | ||||
| MIDI_GateMpeModeItem* mpeModeItem = new MIDI_GateMpeModeItem; | |||||
| mpeModeItem->text = "MPE mode"; | |||||
| mpeModeItem->rightText = CHECKMARK(module->mpeMode); | |||||
| mpeModeItem->module = module; | |||||
| menu->addChild(mpeModeItem); | |||||
| MIDI_GatePanicItem* panicItem = new MIDI_GatePanicItem; | MIDI_GatePanicItem* panicItem = new MIDI_GatePanicItem; | ||||
| panicItem->text = "Panic"; | panicItem->text = "Panic"; | ||||
| panicItem->module = module; | panicItem->module = module; | ||||