#include "plugin.hpp" struct Octave : Module { enum ParamIds { OCTAVE_PARAM, NUM_PARAMS }; enum InputIds { PITCH_INPUT, OCTAVE_INPUT, NUM_INPUTS }; enum OutputIds { PITCH_OUTPUT, NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; int lastOctave = 0; Octave() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configParam(OCTAVE_PARAM, -4.f, 4.f, 0.f, "Shift", " oct"); getParamQuantity(OCTAVE_PARAM)->snapEnabled = true; configInput(PITCH_INPUT, "1V/octave pitch"); configInput(OCTAVE_INPUT, "Octave shift CV"); configOutput(PITCH_OUTPUT, "Pitch"); } void process(const ProcessArgs& args) override { int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1); int octaveParam = std::round(params[OCTAVE_PARAM].getValue()); for (int c = 0; c < channels; c++) { int octave = octaveParam + std::round(inputs[OCTAVE_INPUT].getPolyVoltage(c)); float pitch = inputs[PITCH_INPUT].getVoltage(c); pitch += octave; outputs[PITCH_OUTPUT].setVoltage(pitch, c); if (c == 0) lastOctave = octave; } outputs[PITCH_OUTPUT].setChannels(channels); } void dataFromJson(json_t* rootJ) override { // In Fundamental 1.1.1 and earlier, the octave param was internal data. json_t* octaveJ = json_object_get(rootJ, "octave"); if (octaveJ) { params[OCTAVE_PARAM].setValue(json_integer_value(octaveJ)); } } }; struct OctaveButton : Widget { int octave; void drawLayer(const DrawArgs& args, int layer) override { if (layer != 1) return; Vec c = box.size.div(2); int activeOctave = 0; int lastOctave = 0; ParamWidget* paramWidget = getAncestorOfType(); assert(paramWidget); engine::ParamQuantity* pq = paramWidget->getParamQuantity(); if (pq) { activeOctave = std::round(pq->getValue()); Octave* module = dynamic_cast(pq->module); if (module) lastOctave = module->lastOctave; } if (activeOctave == octave) { // Enabled nvgBeginPath(args.vg); nvgCircle(args.vg, c.x, c.y, mm2px(4.0 / 2)); if (octave == 0) nvgFillColor(args.vg, color::alpha(color::WHITE, 0.33)); else nvgFillColor(args.vg, SCHEME_YELLOW); nvgFill(args.vg); } else if (lastOctave == octave) { // Disabled but enabled by CV nvgBeginPath(args.vg); nvgCircle(args.vg, c.x, c.y, mm2px(4.0 / 2)); if (octave == 0) nvgFillColor(args.vg, color::alpha(color::WHITE, 0.5 * 0.33)); else nvgFillColor(args.vg, color::alpha(SCHEME_YELLOW, 0.5)); nvgFill(args.vg); } else { // Disabled nvgBeginPath(args.vg); nvgCircle(args.vg, c.x, c.y, mm2px(4.0 / 2)); nvgFillColor(args.vg, color::alpha(color::WHITE, 0.33)); nvgFill(args.vg); nvgBeginPath(args.vg); nvgCircle(args.vg, c.x, c.y, mm2px(3.0 / 2)); nvgFillColor(args.vg, nvgRGB(0x12, 0x12, 0x12)); nvgFill(args.vg); if (octave == 0) { nvgBeginPath(args.vg); nvgCircle(args.vg, c.x, c.y, mm2px(1.0 / 2)); nvgFillColor(args.vg, color::alpha(color::WHITE, 0.33)); nvgFill(args.vg); } } } void onDragHover(const event::DragHover& e) override { if (e.button == GLFW_MOUSE_BUTTON_LEFT) { e.consume(this); } Widget::onDragHover(e); } void onDragEnter(const event::DragEnter& e) override; }; struct OctaveParam : ParamWidget { OctaveParam() { box.size = mm2px(Vec(15.263, 55.88)); const int octaves = 9; const float margin = mm2px(2.0); float height = box.size.y - 2 * margin; for (int i = 0; i < octaves; i++) { OctaveButton* octaveButton = new OctaveButton(); octaveButton->box.pos = Vec(0, height / octaves * i + margin); octaveButton->box.size = Vec(box.size.x, height / octaves); octaveButton->octave = 4 - i; addChild(octaveButton); } } }; inline void OctaveButton::onDragEnter(const event::DragEnter& e) { if (e.button == GLFW_MOUSE_BUTTON_LEFT) { OctaveParam* origin = dynamic_cast(e.origin); if (origin) { ParamWidget* paramWidget = getAncestorOfType(); assert(paramWidget); engine::ParamQuantity* pq = paramWidget->getParamQuantity(); if (pq) { pq->setValue(octave); } } } Widget::onDragEnter(e); } struct OctaveDisplay : LedDisplay { void setModule(Octave* module) { addChild(createParam(mm2px(Vec(0.0, 0.0)), module, Octave::OCTAVE_PARAM)); } }; struct OctaveWidget : ModuleWidget { OctaveWidget(Octave* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/Octave.svg"))); addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addInput(createInputCentered(mm2px(Vec(7.62, 80.573)), module, Octave::OCTAVE_INPUT)); addInput(createInputCentered(mm2px(Vec(7.62, 96.859)), module, Octave::PITCH_INPUT)); addOutput(createOutputCentered(mm2px(Vec(7.62, 113.115)), module, Octave::PITCH_OUTPUT)); OctaveDisplay* display = createWidget(mm2px(Vec(0.0, 13.039))); display->box.size = mm2px(Vec(15.263, 55.88)); display->setModule(module); addChild(display); } }; Model* modelOctave = createModel("Octave");