@@ -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; | ||||