#include "Squinky.hpp" #ifdef _EV3 #include "ctrl/WaveformSelector.h" #include "ctrl/SqWidgets.h" #include "ctrl/SqMenuItem.h" #include "WidgetComposite.h" #include "EV3.h" #include struct EV3Module : Module { EV3Module(); void step() override; EV3 ev3; }; EV3Module::EV3Module() : Module(ev3.NUM_PARAMS, ev3.NUM_INPUTS, ev3.NUM_OUTPUTS, ev3.NUM_LIGHTS), ev3(this) { } void EV3Module::step() { ev3.step(); } /************************************************************/ class EV3PitchDisplay { public: EV3PitchDisplay(EV3Module * mod) : module(mod) { } void step(); /** * Labels must be added in order */ void addOctLabel(Label*); void addSemiLabel(Label*); private: EV3Module * const module; std::vector octLabels; std::vector semiLabels; std::vector semiX; int lastOctave[3] = {100, 100, 100}; int lastSemi[3] = {100, 100, 100}; bool lastPatched[3] = {false, false, false}; void updateAbsolute(int); void updateInterval(int); bool shouldUseInterval(int osc); }; void EV3PitchDisplay::step() { bool atLeastOneChanged = false; const int delta = EV3::OCTAVE2_PARAM - EV3::OCTAVE1_PARAM; for (int i = 0; i < 3; ++i) { const int octaveParam = EV3::OCTAVE1_PARAM + delta * i; const int semiParam = EV3::SEMI1_PARAM + delta * i; const int inputId = EV3::CV1_INPUT + i; const int oct = module->params[octaveParam].value; const int semi = module->params[semiParam].value; const bool patched = module->inputs[inputId].active; if (semi != lastSemi[i] || oct != lastOctave[i] || patched != lastPatched[i]) { atLeastOneChanged = true; lastSemi[i] = semi; lastOctave[i] = oct; lastPatched[i] = patched; } } if (atLeastOneChanged) { for (int i = 0; i < 3; ++i) { if (shouldUseInterval(i)) { updateInterval(i); } else { updateAbsolute(i); } } } } static const char* intervalNames[] = { "0", "m2nd", "2nd", "m3rd", "M3rd", "4th", "Dim5th", "5th", "m6th", "M6th", "m7th", "M7th", "oct" }; const static int offsetNatural = 10; const static int offsetAccidental = 7; static const int pitchOffsets[] = { offsetNatural, offsetAccidental, offsetNatural, // D offsetAccidental, offsetNatural, // E offsetNatural, // F offsetAccidental, offsetNatural, // g offsetAccidental, offsetNatural, offsetAccidental, offsetNatural, //b }; static const char* pitchNames[] = { "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" }; static const int intervalOffsets[] = { 11, -3, 3, // 2nd 0, 0, 4, // 4th -5, 4, // 5th 0, 0, 0, 2, 2 // M7 }; void EV3PitchDisplay::addOctLabel(Label* l) { octLabels.push_back(l); } void EV3PitchDisplay::addSemiLabel(Label* l) { semiLabels.push_back(l); semiX.push_back(l->box.pos.x); } bool EV3PitchDisplay::shouldUseInterval(int osc) { bool ret = false; // always safe to use abolute if (lastPatched[osc]) { ret = false; // if current one is patched, use absolute } else if ((osc > 0) && lastPatched[osc - 1]) { ret = true; // if prev patched and we are not, go for it } else if ((osc > 1) && lastPatched[osc - 2]) { ret = true; // if prev-prev patched and we are not, go for it } //printf("should use interval (%d) ret %d", osc, ret); return ret; } void EV3PitchDisplay::updateInterval(int osc) { int refSemi = 0; int refOctave = 0; int oct = 5 + lastOctave[osc]; int semi = lastSemi[osc]; assert(osc > 0); const bool prevPatched = lastPatched[osc - 1]; if (prevPatched) { refOctave = 5 + lastOctave[osc - 1]; refSemi = lastSemi[osc - 1]; // printf("got from prev %d (%d, %d)\n", osc-1, refOctave, refSemi); } else { assert(osc > 1); refOctave = 5 + lastOctave[osc - 2]; refSemi = lastSemi[osc - 2]; // printf("got from prev %d (%d, %d)\n", osc-2, refOctave, refSemi); } const int currentPitch = oct * 12 + semi; const int refPitch = refOctave * 12 + refSemi; const int relativePitch = currentPitch - refPitch; int adjustedOctave = 0; int adjustedSemi = 0; adjustedOctave = relativePitch / 12; adjustedSemi = relativePitch - (adjustedOctave * 12); if (adjustedSemi < 0) { adjustedOctave--; adjustedSemi += 12; } assert(adjustedSemi >= 0); assert(adjustedSemi < 12); std::stringstream so; so << adjustedOctave; octLabels[osc]->text = so.str(); semiLabels[osc]->text = intervalNames[adjustedSemi]; semiLabels[osc]->box.pos.x = semiX[osc] + intervalOffsets[adjustedSemi]; } void EV3PitchDisplay::updateAbsolute(int osc) { std::stringstream so; int oct = 5 + lastOctave[osc]; int semi = lastSemi[osc]; if (semi < 0) { --oct; semi += 12; } so << oct; octLabels[osc]->text = so.str(); semiLabels[osc]->text = pitchNames[semi]; semiLabels[osc]->box.pos.x = semiX[osc] + pitchOffsets[semi]; } struct EV3Widget : ModuleWidget { EV3Widget(EV3Module *); void makeSections(EV3Module *); void makeSection(EV3Module *, int index); void makeInputs(EV3Module *); void makeInput(EV3Module* module, int row, int col, int input, const char* name, float labelDeltaX); void makeOutputs(EV3Module *); Label* addLabel(const Vec& v, const char* str, const NVGcolor& color = COLOR_BLACK) { Label* label = new Label(); label->box.pos = v; label->text = str; label->color = color; addChild(label); return label; } void step() override; Menu* createContextMenu() override; EV3PitchDisplay pitchDisplay; EV3Module* const module; Label* plusOne = nullptr; Label* plusTwo = nullptr; bool wasNormalizing = false; }; inline Menu* EV3Widget::createContextMenu() { Menu* theMenu = ModuleWidget::createContextMenu(); ManualMenuItem* manual = new ManualMenuItem( "https://github.com/squinkylabs/SquinkyVCV/blob/master/docs/ev3.md"); theMenu->addChild(manual); return theMenu; } static const NVGcolor COLOR_GREEN2 = nvgRGB(0x90, 0xff, 0x3e); void EV3Widget::step() { ModuleWidget::step(); pitchDisplay.step(); bool norm = module->ev3.isLoweringVolume(); if (norm != wasNormalizing) { wasNormalizing = norm; auto color = norm ? COLOR_GREEN2 : COLOR_WHITE; plusOne->color = color; plusTwo->color = color; } } const int dy = -6; // apply to everything void EV3Widget::makeSection(EV3Module *module, int index) { const float x = (30 - 4) + index * (86 + 4); const float x2 = x + (36 + 2); const float y = 80 + dy; const float y2 = y + 56 + dy; const float y3 = y2 + 40 + dy; const int delta = EV3::OCTAVE2_PARAM - EV3::OCTAVE1_PARAM; pitchDisplay.addOctLabel( addLabel(Vec(x - 10, y - 32), "Oct")); pitchDisplay.addSemiLabel( addLabel(Vec(x2 - 22, y - 32), "Semi")); addParam(createParamCentered( Vec(x, y), module, EV3::OCTAVE1_PARAM + delta * index, -5.0f, 4.0f, 0.f)); addParam(createParamCentered( Vec(x2, y), module, EV3::SEMI1_PARAM + delta * index, -11.f, 11.0f, 0.f)); addParam(createParamCentered( Vec(x, y2), module, EV3::FINE1_PARAM + delta * index, -1.0f, 1.0f, 0)); addLabel(Vec(x - 20, y2 - 34), "Fine"); addParam(createParamCentered( Vec(x2, y2), module, EV3::FM1_PARAM + delta * index, 0.f, 1.f, 0)); addLabel(Vec(x2 - 19, y2 - 34), "Mod"); const float dy = 27; const float x0 = x; addParam(createParamCentered( Vec(x0, y3), module, EV3::PW1_PARAM + delta * index, -1.f, 1.f, 0)); if (index == 0) addLabel(Vec(x0 + 10, y3 - 8), "pw"); addParam(createParamCentered( Vec(x0, y3 + dy), module, EV3::PWM1_PARAM + delta * index, -1.0f, 1.0f, 0)); if (index == 0) addLabel(Vec(x0 + 10, y3 + dy - 8), "pwm"); // sync switches const float swx = x + 29; const float lbx = x + 19; if (index != 0) { addParam(ParamWidget::create( Vec(swx, y3), module, EV3::SYNC1_PARAM + delta * index, 0.0f, 1.0f, 0.0f)); addLabel(Vec(lbx - 3, y3 - 20), "sync"); addLabel(Vec(lbx + 2, y3 + 20), "off"); } const float y4 = y3 + 43; const float xx = x - 12; // include one extra wf - none const float numWaves = (float) EV3::Waves::END; const float defWave = (float) EV3::Waves::SIN; addParam(ParamWidget::create( Vec(xx, y4), module, EV3::WAVE1_PARAM + delta * index, 0.0f, numWaves, defWave)); } void EV3Widget::makeSections(EV3Module* module) { makeSection(module, 0); makeSection(module, 1); makeSection(module, 2); } const float row1Y = 280 + dy - 4; // -4 = move the last section up const float rowDY = 30; const float colDX = 45; void EV3Widget::makeInput(EV3Module* module, int row, int col, int inputNum, const char* name, float labelXDelta) { EV3::InputIds input = EV3::InputIds(inputNum); const float y = row1Y + row * rowDY; const float x = 14 + col * colDX; const float labelX = labelXDelta + x - 6; addInput(Port::create( Vec(x, y), Port::INPUT, module, input)); if (row == 0) addLabel(Vec(labelX, y - 20), name); } void EV3Widget::makeInputs(EV3Module* module) { // Row 0 = top row, 2 = bottom row auto row2Input = [](int row, EV3::InputIds baseInput) { // map inputs directly to rows return baseInput + row; }; for (int row = 0; row < 3; ++row) { makeInput(module, row, 0, row2Input(row, EV3::CV1_INPUT), "V/oct", -3); makeInput(module, row, 1, row2Input(row, EV3::FM1_INPUT), "Fm", 3); makeInput(module, row, 2, row2Input(row, EV3::PWM1_INPUT), "Pwm", -2); } } void EV3Widget::makeOutputs(EV3Module *) { const float x = 160; const float trimY = row1Y + 11; const float outX = x + 30; addParam(createParamCentered( Vec(x, trimY), module, EV3::MIX1_PARAM, 0.0f, 1.0f, 0)); addParam(createParamCentered( Vec(x, trimY + rowDY), module, EV3::MIX2_PARAM, 0.0f, 1.0f, 0)); addParam(createParamCentered( Vec(x, trimY + 2 * rowDY), module, EV3::MIX3_PARAM, 0.0f, 1.0f, 0)); addOutput(Port::create( Vec(outX, row1Y), Port::OUTPUT, module, EV3::VCO1_OUTPUT)); addLabel(Vec(outX + 20, row1Y + 0 * rowDY + 2), "1", COLOR_WHITE); addOutput(Port::create( Vec(outX, row1Y + rowDY), Port::OUTPUT, module, EV3::VCO2_OUTPUT)); addLabel(Vec(outX + 20, row1Y + 1 * rowDY + 2), "2", COLOR_WHITE); addOutput(Port::create( Vec(outX, row1Y + 2 * rowDY), Port::OUTPUT, module, EV3::VCO3_OUTPUT)); addLabel(Vec(outX + 20, row1Y + 2 * rowDY + 2), "3", COLOR_WHITE); addOutput(Port::create( Vec(outX + 41, row1Y + rowDY), Port::OUTPUT, module, EV3::MIX_OUTPUT)); plusOne = addLabel(Vec(outX + 41, row1Y + rowDY - 17), "+", COLOR_WHITE); plusTwo = addLabel(Vec(outX + 41, row1Y + rowDY + 20), "+", COLOR_WHITE); } /** * Widget constructor will describe my implementation structure and * provide meta-data. * This is not shared by all modules in the DLL, just one */ EV3Widget::EV3Widget(EV3Module *module) : ModuleWidget(module), pitchDisplay(module), module(module) { box.size = Vec(18 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT); { SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/ev3_panel.svg"))); addChild(panel); } makeSections(module); makeInputs(module); makeOutputs(module); // screws addChild(Widget::create(Vec(RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); addChild(Widget::create(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); addChild(Widget::create(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); } RACK_PLUGIN_MODEL_INIT(squinkylabs_plug1, EV3) { Model *modelEV3Module = Model::create("Squinky Labs", "squinkylabs-ev3", "EV3: Triple VCO with even waveform", OSCILLATOR_TAG); return modelEV3Module; } #endif