diff --git a/CHANGELOG.md b/CHANGELOG.md index f5bc5cca..ee28add7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ In this document, Mod is Ctrl on Windows/Linux and Cmd on Mac. - Use MIDI timestamps in MIDI-CV, MIDI-CC, MIDI-Gate, and MIDI-Map to improve overall timing and drastically reduce clock jitter. - Add red clip lights to VCV Audio-8/16 when signal reaches beyond ±10V. - Reset notes in MIDI-CV and MIDI-Gate if an "all notes off" MIDI message is received. + - Allow disabling smoothing for MIDI-CV (pitch and mod wheel), MIDI-CC, and MIDI-Map. - API - Add `Module::configInput()` and `Module::configOutput()` for adding names to ports. - Replace `ParamWidget::paramQuantity` with `ParamWidget::getParamQuantity()`. diff --git a/src/core/MIDI_CC.cpp b/src/core/MIDI_CC.cpp index d218df08..8517d0e5 100644 --- a/src/core/MIDI_CC.cpp +++ b/src/core/MIDI_CC.cpp @@ -31,6 +31,7 @@ struct MIDI_CC : Module { int learnedCcs[16]; /** [cell][channel] */ dsp::ExponentialFilter valueFilters[16][16]; + bool smooth; bool mpeMode; bool lsbEnabled; @@ -63,6 +64,7 @@ struct MIDI_CC : Module { learnedCcs[i] = i; } midiInput.reset(); + smooth = true; mpeMode = false; lsbEnabled = false; } @@ -88,23 +90,23 @@ struct MIDI_CC : Module { int cc = learnedCcs[i]; for (int c = 0; c < channels; c++) { - int16_t cellValue = ccValues[cc][c] * 128; + int16_t cellValue = int16_t(ccValues[cc][c]) * 128; if (lsbEnabled && cc < 32) cellValue += ccValues[cc + 32][c]; - // Maximum value for 14-bit CC should be MSB=127 LSB=0, not MSB=127 LSB=127. - float value = cellValue / float(128 * 127); + // Maximum value for 14-bit CC should be MSB=127 LSB=0, not MSB=127 LSB=127, because this is the maximum value that 7-bit controllers can send. + float value = float(cellValue) / (128 * 127); // Support negative values because the gamepad MIDI driver generates nonstandard 8-bit CC values. value = clamp(value, -1.f, 1.f); // Detect behavior from MIDI buttons. - if (std::fabs(valueFilters[i][c].out - value) >= 1.f) { - // Jump value - valueFilters[i][c].out = value; - } - else { + if (smooth && std::fabs(valueFilters[i][c].out - value) < 1.f) { // Smooth value with filter valueFilters[i][c].process(args.sampleTime, value); } + else { + // Jump value + valueFilters[i][c].out = value; + } outputs[CC_OUTPUT + i].setVoltage(valueFilters[i][c].out * 10.f, c); } } @@ -168,6 +170,7 @@ struct MIDI_CC : Module { json_object_set_new(rootJ, "midi", midiInput.toJson()); + json_object_set_new(rootJ, "smooth", json_boolean(smooth)); json_object_set_new(rootJ, "mpeMode", json_boolean(mpeMode)); json_object_set_new(rootJ, "lsbEnabled", json_boolean(lsbEnabled)); return rootJ; @@ -197,6 +200,10 @@ struct MIDI_CC : Module { if (midiJ) midiInput.fromJson(midiJ); + json_t* smoothJ = json_object_get(rootJ, "smooth"); + if (smoothJ) + smooth = json_boolean_value(smoothJ); + json_t* mpeModeJ = json_object_get(rootJ, "mpeMode"); if (mpeModeJ) mpeMode = json_boolean_value(mpeModeJ); @@ -248,6 +255,19 @@ struct MIDI_CCWidget : ModuleWidget { menu->addChild(new MenuSeparator); + struct SmoothItem : MenuItem { + MIDI_CC* module; + void onAction(const event::Action& e) override { + module->smooth ^= true; + } + }; + + SmoothItem* smoothItem = new SmoothItem; + smoothItem->text = "Smooth CC"; + smoothItem->rightText = CHECKMARK(module->smooth); + smoothItem->module = module; + menu->addChild(smoothItem); + struct MpeModeItem : MenuItem { MIDI_CC* module; void onAction(const event::Action& e) override { diff --git a/src/core/MIDI_CV.cpp b/src/core/MIDI_CV.cpp index 2ffe6c19..52840c7a 100644 --- a/src/core/MIDI_CV.cpp +++ b/src/core/MIDI_CV.cpp @@ -35,6 +35,7 @@ struct MIDI_CV : Module { midi::InputQueue midiInput; + bool smooth; int channels; enum PolyMode { ROTATE_MODE, @@ -94,6 +95,7 @@ struct MIDI_CV : Module { } void onReset() override { + smooth = true; channels = 1; polyMode = ROTATE_MODE; clockDivision = 24; @@ -143,19 +145,38 @@ struct MIDI_CV : Module { 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++) { - outputs[PITCH_OUTPUT].setChannels(channels); - outputs[MOD_OUTPUT].setChannels(channels); - outputs[PITCH_OUTPUT].setVoltage(pitchFilters[c].process(args.sampleTime, rescale(pitches[c], 0, 1 << 14, -5.f, 5.f)), c); - outputs[MOD_OUTPUT].setVoltage(modFilters[c].process(args.sampleTime, rescale(mods[c], 0, 127, 0.f, 10.f)), 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[PITCH_OUTPUT].setVoltage(pitchFilters[0].process(args.sampleTime, rescale(pitches[0], 0, 1 << 14, -5.f, 5.f))); - outputs[MOD_OUTPUT].setVoltage(modFilters[0].process(args.sampleTime, rescale(mods[0], 0, 127, 0.f, 10.f))); } outputs[CLOCK_OUTPUT].setVoltage(clockPulse.process(args.sampleTime) ? 10.f : 0.f); @@ -415,6 +436,7 @@ struct MIDI_CV : Module { 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)); @@ -428,6 +450,10 @@ struct MIDI_CV : Module { } 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)); @@ -455,104 +481,6 @@ struct MIDI_CV : Module { }; -struct ClockDivisionValueItem : MenuItem { - MIDI_CV* module; - int clockDivision; - void onAction(const event::Action& 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; - } -}; - - -struct ChannelValueItem : MenuItem { - MIDI_CV* module; - int channels; - void onAction(const event::Action& 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; - } -}; - - -struct PolyModeValueItem : MenuItem { - MIDI_CV* module; - MIDI_CV::PolyMode polyMode; - void onAction(const event::Action& 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; - } -}; - - -struct MIDI_CVPanicItem : MenuItem { - MIDI_CV* module; - void onAction(const event::Action& e) override { - module->panic(); - } -}; - - struct MIDI_CVWidget : ModuleWidget { MIDI_CVWidget(MIDI_CV* module) { setModule(module); @@ -587,25 +515,129 @@ struct MIDI_CVWidget : ModuleWidget { menu->addChild(new MenuSeparator); + struct SmoothItem : MenuItem { + MIDI_CV* module; + void onAction(const event::Action& 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 event::Action& 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 event::Action& 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 event::Action& 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); - MIDI_CVPanicItem* panicItem = new MIDI_CVPanicItem; + struct PanicItem : MenuItem { + MIDI_CV* module; + void onAction(const event::Action& e) override { + module->panic(); + } + }; + + PanicItem* panicItem = new PanicItem; panicItem->text = "Panic"; panicItem->module = module; menu->addChild(panicItem); diff --git a/src/core/MIDI_Map.cpp b/src/core/MIDI_Map.cpp index de4175df..755fb2e7 100644 --- a/src/core/MIDI_Map.cpp +++ b/src/core/MIDI_Map.cpp @@ -24,6 +24,7 @@ struct MIDI_Map : Module { midi::InputQueue midiInput; + bool smooth; /** Number of maps */ int mapLen = 0; /** The mapped CC number of each channel */ @@ -65,6 +66,7 @@ struct MIDI_Map : Module { } void onReset() override { + smooth = true; learningId = -1; learnedCc = false; learnedParam = false; @@ -116,14 +118,14 @@ struct MIDI_Map : Module { continue; float value = values[cc] / 127.f; // Detect behavior from MIDI buttons. - if (std::fabs(valueFilters[id].out - value) >= 1.f) { - // Jump value - valueFilters[id].out = value; - } - else { + if (smooth && std::fabs(valueFilters[id].out - value) < 1.f) { // Smooth value with filter valueFilters[id].process(args.sampleTime * divider.getDivision(), value); } + else { + // Jump value + valueFilters[id].out = value; + } paramQuantity->setScaledValue(valueFilters[id].out); } } @@ -153,6 +155,9 @@ struct MIDI_Map : Module { updateMapLen(); refreshParamHandleText(learningId); } + // Ignore negative values generated using the nonstandard 8-bit MIDI extension from the gamepad driver + if (values[cc] < 0) + return; values[cc] = value; } @@ -250,13 +255,13 @@ struct MIDI_Map : Module { } json_object_set_new(rootJ, "maps", mapsJ); + json_object_set_new(rootJ, "smooth", json_boolean(smooth)); json_object_set_new(rootJ, "midi", midiInput.toJson()); return rootJ; } void dataFromJson(json_t* rootJ) override { clearMaps(); - json_t* mapsJ = json_object_get(rootJ, "maps"); if (mapsJ) { json_t* mapJ; @@ -277,6 +282,10 @@ struct MIDI_Map : Module { updateMapLen(); + json_t* smoothJ = json_object_get(rootJ, "smooth"); + if (smoothJ) + smooth = json_boolean_value(smoothJ); + json_t* midiJ = json_object_get(rootJ, "midi"); if (midiJ) midiInput.fromJson(midiJ); @@ -482,6 +491,25 @@ struct MIDI_MapWidget : ModuleWidget { midiWidget->setModule(module); addChild(midiWidget); } + + void appendContextMenu(Menu* menu) override { + MIDI_Map* module = dynamic_cast(this->module); + + menu->addChild(new MenuSeparator); + + struct SmoothItem : MenuItem { + MIDI_Map* module; + void onAction(const event::Action& e) override { + module->smooth ^= true; + } + }; + + SmoothItem* smoothItem = new SmoothItem; + smoothItem->text = "Smooth CC"; + smoothItem->rightText = CHECKMARK(module->smooth); + smoothItem->module = module; + menu->addChild(smoothItem); + } };