| @@ -21,30 +21,39 @@ struct MIDI_CC : Module { | |||
| }; | |||
| midi::InputQueue midiInput; | |||
| int8_t values[128]; | |||
| /** [cc][channel] */ | |||
| int8_t values[128][16]; | |||
| int learningId; | |||
| int learnedCcs[16]; | |||
| dsp::ExponentialFilter valueFilters[16]; | |||
| /** [cell][channel] */ | |||
| dsp::ExponentialFilter valueFilters[16][16]; | |||
| bool mpeMode; | |||
| MIDI_CC() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| for (int i = 0; i < 16; i++) | |||
| configOutput(CC_OUTPUT + i, string::f("Cell %d", i + 1)); | |||
| 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(); | |||
| } | |||
| void onReset() override { | |||
| 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++) { | |||
| learnedCcs[i] = i; | |||
| } | |||
| learningId = -1; | |||
| midiInput.reset(); | |||
| mpeMode = false; | |||
| } | |||
| void process(const ProcessArgs& args) override { | |||
| @@ -59,23 +68,28 @@ struct MIDI_CC : Module { | |||
| midiInput.queue.pop(); | |||
| } | |||
| int channels = mpeMode ? 16 : 1; | |||
| for (int i = 0; i < 16; i++) { | |||
| if (!outputs[CC_OUTPUT + i].isConnected()) | |||
| continue; | |||
| outputs[CC_OUTPUT + i].setChannels(channels); | |||
| 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) { | |||
| uint8_t c = mpeMode ? msg.getChannel() : 0; | |||
| uint8_t cc = msg.getNote(); | |||
| if (msg.bytes.size() < 2) | |||
| return; | |||
| // Allow CC to be negative if the 8th bit is set. | |||
| // The gamepad driver abuses this, for example. | |||
| // Cast uint8_t to int8_t | |||
| if (msg.bytes.size() < 2) | |||
| return; | |||
| int8_t value = msg.bytes[2]; | |||
| value = clamp(value, -127, 127); | |||
| // Learn | |||
| if (learningId >= 0 && values[cc] != value) { | |||
| if (learningId >= 0 && values[cc][c] != value) { | |||
| learnedCcs[learningId] = cc; | |||
| learningId = -1; | |||
| } | |||
| values[cc] = value; | |||
| values[cc][c] = value; | |||
| } | |||
| 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 | |||
| json_t* valuesJ = json_array(); | |||
| 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, "midi", midiInput.toJson()); | |||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | |||
| return rootJ; | |||
| } | |||
| @@ -141,7 +159,7 @@ struct MIDI_CC : Module { | |||
| for (int i = 0; i < 128; i++) { | |||
| json_t* valueJ = json_array_get(valuesJ, i); | |||
| 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"); | |||
| if (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); | |||
| 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; | |||
| 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; | |||
| uint8_t learnedNotes[16] = {}; | |||
| bool velocityMode = false; | |||
| bool mpeMode = false; | |||
| MIDI_Gate() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| for (int i = 0; i < 16; i++) | |||
| configOutput(TRIG_OUTPUT + i, string::f("Cell %d", i + 1)); | |||
| onReset(); | |||
| } | |||
| @@ -45,12 +50,16 @@ struct MIDI_Gate : Module { | |||
| learningId = -1; | |||
| panic(); | |||
| midiInput.reset(); | |||
| velocityMode = false; | |||
| mpeMode = false; | |||
| } | |||
| void panic() { | |||
| 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(); | |||
| } | |||
| int channels = mpeMode ? 16 : 1; | |||
| 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()) { | |||
| // note off | |||
| case 0x8: { | |||
| releaseNote(msg.getNote()); | |||
| releaseNote(msg.getChannel(), msg.getNote()); | |||
| } break; | |||
| // note on | |||
| case 0x9: { | |||
| if (msg.getValue() > 0) { | |||
| pressNote(msg.getNote(), msg.getValue()); | |||
| pressNote(msg.getChannel(), msg.getNote(), msg.getValue()); | |||
| } | |||
| 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; | |||
| 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 | |||
| if (learningId >= 0) { | |||
| learnedNotes[learningId] = note; | |||
| @@ -116,18 +123,19 @@ struct MIDI_Gate : Module { | |||
| // Find id | |||
| for (int i = 0; i < 16; i++) { | |||
| 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 | |||
| for (int i = 0; i < 16; i++) { | |||
| 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, "midi", midiInput.toJson()); | |||
| json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); | |||
| return rootJ; | |||
| } | |||
| @@ -165,6 +175,10 @@ struct MIDI_Gate : Module { | |||
| json_t* midiJ = json_object_get(rootJ, "midi"); | |||
| if (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 { | |||
| MIDI_Gate* module; | |||
| void onAction(const event::Action& e) override { | |||
| @@ -228,6 +250,12 @@ struct MIDI_GateWidget : ModuleWidget { | |||
| velocityItem->module = module; | |||
| 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; | |||
| panicItem->text = "Panic"; | |||
| panicItem->module = module; | |||