diff --git a/plugin.json b/plugin.json
index eee2012..f55877a 100644
--- a/plugin.json
+++ b/plugin.json
@@ -1,7 +1,7 @@
{
"slug": "cvly",
"name": "cvly",
- "version": "1.0.1",
+ "version": "1.0.2",
"license": "GPL-3.0-or-later",
"brand": "cvly",
"author": "Benja Appel",
@@ -11,8 +11,19 @@
"manualUrl": "https://github.com/Lyqst/cvly-modules/wiki",
"sourceUrl": "https://github.com/Lyqst/cvly-modules",
"donateUrl": "https://www.buymeacoffee.com/cvly",
- "changelogUrl": "",
+ "changelogUrl": "https://github.com/Lyqst/cvly-modules/releases",
"modules": [
+ {
+ "slug": "crcl",
+ "name": "crcl",
+ "description": "Quantizer/sequencer based on the circle of fifths",
+ "tags": [
+ "quantizer",
+ "poly",
+ "sample and hold",
+ "sequencer"
+ ]
+ },
{
"slug": "ntrvlc",
"name": "ntrvlc",
diff --git a/res/crcl.svg b/res/crcl.svg
new file mode 100644
index 0000000..77fbffc
--- /dev/null
+++ b/res/crcl.svg
@@ -0,0 +1,640 @@
+
+
diff --git a/src/crcl.cpp b/src/crcl.cpp
new file mode 100644
index 0000000..0f3c8c7
--- /dev/null
+++ b/src/crcl.cpp
@@ -0,0 +1,301 @@
+#include "plugin.hpp"
+#include "scales.hpp"
+
+struct Crcl : Module
+{
+ enum ParamIds
+ {
+ MODE_PARAM,
+ ENUMS(CIRCLE_PARAM, 12),
+ NUM_PARAMS
+ };
+
+ enum InputIds
+ {
+ CV_INPUT,
+ TRIGGER_INPUT,
+ RESET_INPUT,
+ ROOT_INPUT,
+ NUM_INPUTS
+ };
+
+ enum OutputIds
+ {
+ CV_OUTPUT,
+ TRIGGER_OUTPUT,
+ NUM_OUTPUTS
+ };
+
+ enum LightIds
+ {
+ ENUMS(MODE_LIGHT, 3),
+ ENUMS(CIRCLE_LIGHT, 12),
+ NUM_LIGHTS
+ };
+
+ dsp::SchmittTrigger modeTrigger;
+ int mode = 0;
+
+ json_t *dataToJson() override
+ {
+ json_t *rootJ = json_object();
+
+ // mode
+ json_object_set_new(rootJ, "mode", json_integer(mode));
+
+ return rootJ;
+ }
+
+ void dataFromJson(json_t *rootJ) override
+ {
+ // mode
+ json_t *modeJ = json_object_get(rootJ, "mode");
+ if (modeJ)
+ mode = json_integer_value(modeJ);
+ }
+
+ int notes = 12;
+ int input_scale[12];
+ float scale[13];
+ int normalMapping[13] = {0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5, 0};
+ int negativeMapping[13] = {7, 6, 5, 4, 3, 2, 1, 0, 11, 10, 9, 8, 7};
+ dsp::SchmittTrigger inputTrigger[16];
+ dsp::SchmittTrigger resetTrigger;
+ dsp::PulseGenerator pulseGenerator[16];
+ float prev_out_cv[16] = {};
+ float in_cv[16] = {};
+ float out_cv[16] = {};
+ float out_trigger[16] = {};
+ float out_count = 0;
+ int step = 0;
+
+ Crcl()
+ {
+ config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
+ }
+
+ void process(const ProcessArgs &args) override
+ {
+ if (modeTrigger.process(params[MODE_PARAM].getValue()))
+ {
+ mode++;
+ if (mode > 2)
+ mode = 0;
+ }
+
+ int cv_channels = inputs[CV_INPUT].getChannels();
+ int tg_channels = inputs[TRIGGER_INPUT].getChannels();
+ int channels = std::max(cv_channels, tg_channels);
+
+ if (mode == 1) // no polyphony in sequencer mode
+ channels = 1;
+
+ bool trigger_bool[16];
+ for (int c = 0; c < tg_channels; c++)
+ {
+ trigger_bool[c] = inputTrigger[c].process(inputs[TRIGGER_INPUT].getVoltage(c));
+ }
+
+ bool light_bool[12] = {false};
+
+ input_scale[0] = params[CIRCLE_PARAM].getValue();
+ input_scale[1] = params[CIRCLE_PARAM + 7].getValue(); // m2
+ input_scale[2] = params[CIRCLE_PARAM + 2].getValue(); // M2
+ input_scale[3] = params[CIRCLE_PARAM + 9].getValue(); // m3
+ input_scale[4] = params[CIRCLE_PARAM + 4].getValue(); // M3
+ input_scale[5] = params[CIRCLE_PARAM + 11].getValue(); // P4
+ input_scale[6] = params[CIRCLE_PARAM + 6].getValue(); // TT
+ input_scale[7] = params[CIRCLE_PARAM + 1].getValue(); // P5
+ input_scale[8] = params[CIRCLE_PARAM + 8].getValue(); // m6
+ input_scale[9] = params[CIRCLE_PARAM + 3].getValue(); // M6
+ input_scale[10] = params[CIRCLE_PARAM + 10].getValue(); // m7
+ input_scale[11] = params[CIRCLE_PARAM + 5].getValue(); // M7
+
+ int n = 0;
+
+ for (int i = 0; i < 12; i++)
+ {
+ if (input_scale[mode == 1 ? normalMapping[i] : i] > 0.1)
+ scale[n++] = (mode == 1 ? normalMapping[i] : (mode == 2 ? negativeMapping[i] : i)) / 12.f;
+ }
+
+ if (n == 0)
+ {
+ n = 1;
+ scale[0] = 0;
+ }
+
+ scale[n] = scale[0] + 1;
+ notes = n;
+
+ if (mode == 0 || mode == 2)
+ {
+ for (int c = 0; c < channels; c++)
+ {
+
+ if (tg_channels == 0 || trigger_bool[c >= tg_channels ? tg_channels - 1 : c])
+ {
+ in_cv[c] = inputs[CV_INPUT].getVoltage(c >= cv_channels ? cv_channels - 1 : c);
+ }
+
+ float root = inputs[ROOT_INPUT].getVoltage() - floor(inputs[ROOT_INPUT].getVoltage());
+
+ int oct = floor(in_cv[c]);
+ float diff = in_cv[c] - oct - root;
+
+ if (diff < 0)
+ {
+ diff = 1 + diff;
+ oct--;
+ }
+
+ int note = floor(diff * notes + 0.5);
+
+ out_cv[c] = oct + root + scale[note];
+
+ light_bool[normalMapping[(int)floor(scale[note] * 12)]] = true;
+ }
+ }
+ else if (mode == 1)
+ {
+ if (inputs[TRIGGER_INPUT].isConnected() && trigger_bool[0])
+ {
+ if (inputs[CV_INPUT].getVoltage() >= 0 || !inputs[CV_INPUT].isConnected())
+ {
+ if (++step >= notes)
+ step = 0;
+ }
+ else
+ {
+ if (--step < 0)
+ step = notes - 1;
+ }
+ }
+
+ if (resetTrigger.process(inputs[RESET_INPUT].getVoltage()))
+ {
+ step = 0;
+ }
+
+ out_cv[0] = inputs[ROOT_INPUT].getVoltage() + scale[step];
+
+ light_bool[normalMapping[(int)floor(scale[step] * 12)]] = true;
+ }
+
+ for (int i = 0; i < 3; i++)
+ {
+ lights[MODE_LIGHT + i].setBrightness(mode == i ? 0.7f : 0);
+ }
+
+ for (int i = 0; i < 12; i++)
+ {
+ lights[CIRCLE_LIGHT + i].setBrightness(light_bool[i] ? 0.7f : 0);
+ }
+
+ for (int i = 0; i < channels; i++)
+ {
+ if (out_cv[i] != prev_out_cv[i])
+ {
+ prev_out_cv[i] = out_cv[i];
+ pulseGenerator[i].trigger(1e-3f);
+ }
+ outputs[CV_OUTPUT].setVoltage(out_cv[i], i);
+ bool pulse = pulseGenerator[i].process(args.sampleTime);
+ outputs[TRIGGER_OUTPUT].setVoltage(pulse ? 10.0 : 0.0, i);
+ }
+
+ outputs[CV_OUTPUT].setChannels(channels);
+ outputs[TRIGGER_OUTPUT].setChannels(channels);
+ }
+};
+
+struct CrclWidget : ModuleWidget
+{
+ CrclWidget(Crcl *module)
+ {
+ setModule(module);
+ setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/crcl.svg")));
+
+ addChild(createWidget(Vec(RACK_GRID_WIDTH * 4, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH * 15, 0)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+ addChild(createWidget(Vec(RACK_GRID_WIDTH * 18, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
+
+ addInput(createInputCentered(Vec(28, 38), module, Crcl::CV_INPUT));
+ addInput(createInputCentered(Vec(28, 90), module, Crcl::TRIGGER_INPUT));
+ addInput(createInputCentered(Vec(28, 142), module, Crcl::RESET_INPUT));
+
+ addParam(createParamCentered(Vec(150, 37), module, Crcl::MODE_PARAM));
+ addChild(createLightCentered>(Vec(130, 57), module, Crcl::MODE_LIGHT));
+ addChild(createLightCentered>(Vec(130, 68), module, Crcl::MODE_LIGHT + 1));
+ addChild(createLightCentered>(Vec(130, 79), module, Crcl::MODE_LIGHT + 2));
+
+ addOutput(createOutputCentered(Vec(267, 38), module, Crcl::CV_OUTPUT));
+ addOutput(createOutputCentered(Vec(267, 90), module, Crcl::TRIGGER_OUTPUT));
+
+ Vec center = Vec(150, 213);
+ addInput(createInputCentered(center, module, Crcl::ROOT_INPUT));
+ float r = 92;
+ float r2 = 109;
+ for (int i = 0; i < 12; i++)
+ {
+ addParam(createParamCentered(Vec(center.x + r * sin(M_PI * i / 6), center.y - r * cos(M_PI * i / 6)), module, Crcl::CIRCLE_PARAM + i));
+ addChild(createLightCentered>(Vec(center.x + r2 * sin(M_PI * i / 6), center.y - r2 * cos(M_PI * i / 6)), module, Crcl::CIRCLE_LIGHT + i));
+ }
+ }
+
+ void appendContextMenu(Menu *menu) override
+ {
+ Crcl *module = dynamic_cast(this->module);
+
+ menu->addChild(new MenuEntry);
+ menu->addChild(createMenuLabel("Mode"));
+
+ struct ModeItem : MenuItem
+ {
+ Crcl *module;
+ int mode;
+ void onAction(const event::Action &e) override
+ {
+ module->mode = mode;
+ }
+ };
+
+ std::string labels[5] = {"Quant + SH", "Sequencer", "Negative harmony"};
+ int modes[3] = {0, 1, 2};
+ for (int i = 0; i < 3; i++)
+ {
+ ModeItem *modeItem = createMenuItem(labels[i]);
+ modeItem->rightText = CHECKMARK(module->mode == modes[i]);
+ modeItem->module = module;
+ modeItem->mode = modes[i];
+ menu->addChild(modeItem);
+ }
+
+ menu->addChild(new MenuEntry);
+ menu->addChild(createMenuLabel("Load Scale:"));
+
+ struct ScaleItem : MenuItem
+ {
+ Crcl *module;
+ const int *scale;
+ void onAction(const event::Action &e) override
+ {
+ for (int i = 0; i < 12; i++)
+ {
+ module->params[module->CIRCLE_PARAM + module->normalMapping[i]].setValue(scale[i]);
+ }
+ }
+ };
+
+ for (int i = 0; i < scales::getNumberOfScales(); i++)
+ {
+ ScaleItem *scaleItem = createMenuItem(scales::getScaleName(i));
+ scaleItem->module = module;
+ scaleItem->scale = scales::getScale(i);
+ menu->addChild(scaleItem);
+ }
+ }
+};
+
+Model *modelCrcl = createModel("crcl");
\ No newline at end of file
diff --git a/src/plugin.cpp b/src/plugin.cpp
index 839fd7c..861e25b 100644
--- a/src/plugin.cpp
+++ b/src/plugin.cpp
@@ -9,6 +9,7 @@ void init(Plugin* p) {
// Add modules here
// p->addModel(modelMyModule);
+ p->addModel(modelCrcl);
p->addModel(modelNtrvlc);
p->addModel(modelNtrvlx);
p->addModel(modelStpr);
diff --git a/src/plugin.hpp b/src/plugin.hpp
index 4a62105..386f37b 100644
--- a/src/plugin.hpp
+++ b/src/plugin.hpp
@@ -9,6 +9,7 @@ extern Plugin* pluginInstance;
// Declare each Model, defined in each module source file
// extern Model* modelMyModule;
+extern Model* modelCrcl;
extern Model* modelNtrvlc;
extern Model* modelNtrvlx;
extern Model* modelStpr;
diff --git a/src/scales.cpp b/src/scales.cpp
new file mode 100644
index 0000000..13b1af6
--- /dev/null
+++ b/src/scales.cpp
@@ -0,0 +1,16 @@
+#include "scales.hpp"
+
+int scales::getNumberOfScales()
+{
+ return sizeof(notes) / sizeof(notes[0]);
+}
+
+const int *scales::getScale(int i)
+{
+ return notes[i];
+}
+
+const std::string scales::getScaleName(int i)
+{
+ return name[i];
+}
diff --git a/src/scales.hpp b/src/scales.hpp
new file mode 100644
index 0000000..7b1b86a
--- /dev/null
+++ b/src/scales.hpp
@@ -0,0 +1,38 @@
+#include
+
+namespace scales
+{
+ const std::string name[12] = {
+ "Ionian/Major",
+ "Dorian",
+ "Phrygian",
+ "Lydian",
+ "Mixolydian",
+ "Aeolian/Minor",
+ "Locrian",
+ "Harmonic Minor",
+ "Whole Tone",
+ "Major Pentatonic",
+ "Minor Pentatonic",
+ "Blues Scale"};
+
+ const int notes[][12] = {
+ {1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0, 1},
+ {1, 0, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0},
+ {1, 1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0},
+ {1, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1},
+ {1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0},
+ {1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 0},
+ {1, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1, 0},
+ {1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1},
+ {1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0},
+ {1, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0},
+ {1, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0},
+ {1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0}};
+
+ int getNumberOfScales();
+
+ const int* getScale(int i);
+
+ const std::string getScaleName(int i);
+}
\ No newline at end of file