#include #include "JWModules.hpp" #include "dsp/digital.hpp" namespace rack_plugin_JW_Modules { struct XYPad : Module { enum ParamIds { X_POS_PARAM, Y_POS_PARAM, GATE_PARAM, OFFSET_X_VOLTS_PARAM, OFFSET_Y_VOLTS_PARAM, SCALE_X_PARAM, SCALE_Y_PARAM, AUTO_PLAY_PARAM, PLAY_SPEED_PARAM, SPEED_MULT_PARAM, RND_SHAPES_PARAM, RND_VARIATION_PARAM, NUM_PARAMS }; enum InputIds { PLAY_GATE_INPUT, PLAY_SPEED_INPUT, NUM_INPUTS }; enum OutputIds { X_OUTPUT, Y_OUTPUT, X_INV_OUTPUT, Y_INV_OUTPUT, GATE_OUTPUT, NUM_OUTPUTS }; enum State { STATE_IDLE, STATE_RECORDING, STATE_AUTO_PLAYING, STATE_GATE_PLAYING }; enum LightIds { AUTO_LIGHT, NUM_LIGHTS }; enum Shapes { RND_SINE, RND_SQUARE, RND_RAMP, RND_LINE, RND_NOISE, RND_SINE_MOD, RND_SPIRAL, RND_STEPS, NUM_SHAPES }; enum PlayModes { FWD_LOOP, BWD_LOOP, FWD_ONE_SHOT, BWD_ONE_SHOT, FWD_BWD_LOOP, BWD_FWD_LOOP, NUM_PLAY_MODES }; float minX = 0, minY = 0, maxX = 0, maxY = 0; float displayWidth = 0, displayHeight = 0; float ballRadius = 10; float ballStrokeWidth = 2; float minVolt = -5, maxVolt = 5; float recordPhase = 0.0; float playbackPhase = 0.0; bool autoPlayOn = false; bool playingFwd = true; int state = STATE_IDLE; int curPlayMode = FWD_LOOP; int lastRandomShape = RND_STEPS; SchmittTrigger autoBtnTrigger; std::vector points; long curPointIdx = 0; XYPad() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} void step() override; void reset() override { setState(STATE_IDLE); points.clear(); defaultPos(); } void randomize() override { randomizeShape(); } void randomizeShape(){ makeShape((lastRandomShape+1)%NUM_SHAPES); } void makeShape(int shape){ lastRandomShape = shape; int stateBefore = state; setState(STATE_IDLE); points.clear(); switch(shape){ case RND_SINE: { float twoPi = 2.0*M_PI; float cycles = 1 + int(randomUniform() * 13); bool inside = true; for(float i=0; i minX){ x--; } else if(squSt == ST_UP && y > minY){ y--; } else if(squSt == ST_DOWN && y < maxY){ y++; } if(i % stepsBeforeStateChange == 0){ squSt = int(randomUniform() * 4); } inside = isInView(x, y); if(inside)addPoint(x, y); } } break; } setCurrentPos(points[0].x, points[0].y); setState(stateBefore); } json_t *toJson() override { json_t *rootJ = json_object(); json_object_set_new(rootJ, "lastRandomShape", json_integer(lastRandomShape)); json_object_set_new(rootJ, "curPlayMode", json_integer(curPlayMode)); json_object_set_new(rootJ, "autoPlayOn", json_boolean(autoPlayOn)); json_object_set_new(rootJ, "xPos", json_real(params[X_POS_PARAM].value)); json_object_set_new(rootJ, "yPos", json_real(params[Y_POS_PARAM].value)); json_t *pointsArr = json_array(); for(Vec pt : points){ json_t *posArr = json_array(); json_array_append(posArr, json_real(pt.x)); json_array_append(posArr, json_real(pt.y)); json_array_append(pointsArr, posArr); } json_object_set_new(rootJ, "points", pointsArr); return rootJ; } void fromJson(json_t *rootJ) override { lastRandomShape = json_integer_value(json_object_get(rootJ, "lastRandomShape")); curPlayMode = json_integer_value(json_object_get(rootJ, "curPlayMode")); json_t *xPosJ = json_object_get(rootJ, "xPos"); json_t *yPosJ = json_object_get(rootJ, "yPos"); setCurrentPos(json_real_value(xPosJ), json_real_value(yPosJ)); json_t *array = json_object_get(rootJ, "points"); if(array){ size_t index; json_t *value; json_array_foreach(array, index, value) { float x = json_real_value(json_array_get(value, 0)); float y = json_real_value(json_array_get(value, 1)); addPoint(x, y); } } json_t *autoPlayOnJ = json_object_get(rootJ, "autoPlayOn"); if (autoPlayOnJ){ autoPlayOn = json_is_true(autoPlayOnJ); } lights[AUTO_LIGHT].value = autoPlayOn ? 1.0 : 0.0; params[AUTO_PLAY_PARAM].value = autoPlayOn ? 1 : 0; if(autoPlayOn){setState(STATE_AUTO_PLAYING);} } void defaultPos() { params[XYPad::X_POS_PARAM].value = displayWidth / 2.0; params[XYPad::Y_POS_PARAM].value = displayHeight / 2.0; } void setMouseDown(const Vec &pos, bool down){ if(down){ setCurrentPos(pos.x, pos.y); setState(STATE_RECORDING); } else { if(autoPlayOn && !inputs[PLAY_GATE_INPUT].active){ //no auto play if wire connected to play in setState(STATE_AUTO_PLAYING); } else { setState(STATE_IDLE); } } } void setCurrentPos(float x, float y){ params[X_POS_PARAM].value = clampfjw(x, minX, maxX); params[Y_POS_PARAM].value = clampfjw(y, minY, maxY); } bool isInView(float x, float y){ return x >= minX && x <= maxX && y >= minY && y <= maxY; } void addPoint(float x, float y){ points.push_back(Vec(x, y)); } void updateMinMax(){ float distToMid = ballRadius + ballStrokeWidth; minX = distToMid; minY = distToMid; maxX = displayWidth - distToMid; maxY = displayHeight - distToMid; } bool isStatePlaying() { return state == STATE_GATE_PLAYING || state == STATE_AUTO_PLAYING; } void playback(){ if(isStatePlaying() && points.size() > 0){ params[X_POS_PARAM].value = points[curPointIdx].x; params[Y_POS_PARAM].value = points[curPointIdx].y; if(curPlayMode == FWD_LOOP || curPlayMode == FWD_ONE_SHOT){ playingFwd = true; } else if(curPlayMode == BWD_LOOP || curPlayMode == BWD_ONE_SHOT){ playingFwd = false; } curPointIdx += playingFwd ? 1 : -1; if(curPointIdx >= 0 && curPointIdx < long(points.size())){ params[GATE_PARAM].value = true; //keep gate on } else { params[GATE_PARAM].value = false; if(curPlayMode == FWD_LOOP){ curPointIdx = 0; } else if(curPlayMode == BWD_LOOP){ curPointIdx = points.size() - 1; } else if(curPlayMode == FWD_ONE_SHOT || curPlayMode == BWD_ONE_SHOT){ setState(STATE_IDLE);//done playing curPointIdx = playingFwd ? points.size() - 1 : 0; } else if(curPlayMode == FWD_BWD_LOOP || curPlayMode == BWD_FWD_LOOP){ playingFwd = !playingFwd; //go the other way now curPointIdx = playingFwd ? 0 : points.size() - 1; } } } } void setState(int newState){ switch(newState){ case STATE_IDLE: curPointIdx = 0; params[GATE_PARAM].value = false; break; case STATE_RECORDING: points.clear(); curPointIdx = 0; params[GATE_PARAM].value = true; break; case STATE_AUTO_PLAYING: params[GATE_PARAM].value = true; break; case STATE_GATE_PLAYING: params[GATE_PARAM].value = true; break; } if(isStatePlaying()){ if(curPlayMode == FWD_LOOP || curPlayMode == FWD_ONE_SHOT){ curPointIdx = 0; } else if(curPlayMode == BWD_LOOP || curPlayMode == BWD_ONE_SHOT){ curPointIdx = points.size() - 1; } } state = newState; } }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// void XYPad::step() { if (autoBtnTrigger.process(params[AUTO_PLAY_PARAM].value)) { autoPlayOn = !autoPlayOn; if(autoPlayOn){ if(!isStatePlaying()){ setState(STATE_AUTO_PLAYING); } } else { //stop when auto turned off setState(STATE_IDLE); } } lights[AUTO_LIGHT].value = autoPlayOn ? 1.0 : 0.0; if (inputs[PLAY_GATE_INPUT].active) { params[AUTO_PLAY_PARAM].value = 0; //disable autoplay if wire connected to play gate autoPlayOn = false; //disable autoplay if wire connected to play gate if (inputs[PLAY_GATE_INPUT].value >= 1.0) { if(!isStatePlaying() && state != STATE_RECORDING){ setState(STATE_GATE_PLAYING); } } else { if(isStatePlaying()){ setState(STATE_IDLE); } } } else if(state == STATE_GATE_PLAYING){//wire removed while playing setState(STATE_IDLE); } if(state == STATE_RECORDING){ float recordClockTime = 50; recordPhase += recordClockTime / engineGetSampleRate(); if (recordPhase >= 1.0) { recordPhase -= 1.0; addPoint(params[X_POS_PARAM].value, params[Y_POS_PARAM].value); } } else if(isStatePlaying()){ float playSpeedTotal = clampfjw(inputs[PLAY_SPEED_INPUT].value + params[PLAY_SPEED_PARAM].value, 0, 20); float playbackClockTime = rescalefjw(playSpeedTotal, 0, 20, 1, 500 * params[SPEED_MULT_PARAM].value); playbackPhase += playbackClockTime / engineGetSampleRate(); if (playbackPhase >= 1.0) { playbackPhase -= 1.0; playback(); } } float xOut = rescalefjw(params[X_POS_PARAM].value, minX, maxX, minVolt, maxVolt); float yOut = rescalefjw(params[Y_POS_PARAM].value, minY, maxY, maxVolt, minVolt); //y is inverted because gui coords outputs[X_OUTPUT].value = (xOut + params[OFFSET_X_VOLTS_PARAM].value) * params[SCALE_X_PARAM].value; outputs[Y_OUTPUT].value = (yOut + params[OFFSET_Y_VOLTS_PARAM].value) * params[SCALE_Y_PARAM].value; float xInvOut = rescalefjw(params[X_POS_PARAM].value, minX, maxX, maxVolt, minVolt); float yInvOut = rescalefjw(params[Y_POS_PARAM].value, minY, maxY, minVolt, maxVolt); //y is inverted because gui coords outputs[X_INV_OUTPUT].value = (xInvOut + params[OFFSET_X_VOLTS_PARAM].value) * params[SCALE_X_PARAM].value; outputs[Y_INV_OUTPUT].value = (yInvOut + params[OFFSET_Y_VOLTS_PARAM].value) * params[SCALE_Y_PARAM].value; outputs[GATE_OUTPUT].value = params[GATE_PARAM].value * 10; } ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// struct XYPadDisplay : Widget { XYPad *module; XYPadDisplay() {} float initX = 0; float initY = 0; float dragX = 0; float dragY = 0; void onMouseDown(EventMouseDown &e) override { if (e.button == 0) { e.consumed = true; e.target = this; initX = e.pos.x; initY = e.pos.y; module->setMouseDown(e.pos, true); } } void onMouseMove(EventMouseMove &e) override { } void onMouseUp(EventMouseUp &e) override { if(e.button==0)module->setMouseDown(e.pos, false); } void onDragStart(EventDragStart &e) override { dragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x; dragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y; } void onDragEnd(EventDragEnd &e) override { module->setMouseDown(Vec(0,0), false); RACK_PLUGIN_UI_DRAGGED_WIDGET_SET(NULL); } void onDragMove(EventDragMove &e) override { if(module->state == XYPad::STATE_RECORDING){ float newDragX = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.x; float newDragY = RACK_PLUGIN_UI_RACKWIDGET->lastMousePos.y; module->setCurrentPos(initX+(newDragX-dragX), initY+(newDragY-dragY)); } } void draw(NVGcontext *vg) override { float ballX = module->params[XYPad::X_POS_PARAM].value; float ballY = module->params[XYPad::Y_POS_PARAM].value; float invBallX = module->displayWidth-ballX; float invBallY = module->displayHeight-ballY; //background nvgFillColor(vg, nvgRGB(20, 30, 33)); nvgBeginPath(vg); nvgRect(vg, 0, 0, box.size.x, box.size.y); nvgFill(vg); //INVERTED/////////////////////////////////// NVGcolor invertedColor = nvgRGB(20, 50, 53); NVGcolor ballColor = nvgRGB(25, 150, 252); //horizontal line nvgStrokeColor(vg, invertedColor); nvgBeginPath(vg); nvgMoveTo(vg, 0, invBallY); nvgLineTo(vg, box.size.x, invBallY); nvgStroke(vg); //vertical line nvgStrokeColor(vg, invertedColor); nvgBeginPath(vg); nvgMoveTo(vg, invBallX, 0); nvgLineTo(vg, invBallX, box.size.y); nvgStroke(vg); //inv ball nvgFillColor(vg, invertedColor); nvgStrokeColor(vg, invertedColor); nvgStrokeWidth(vg, module->ballStrokeWidth); nvgBeginPath(vg); nvgCircle(vg, module->displayWidth-ballX, module->displayHeight-ballY, module->ballRadius); if(module->params[XYPad::GATE_PARAM].value)nvgFill(vg); nvgStroke(vg); //POINTS/////////////////////////////////// if(module->points.size() > 0){ nvgStrokeColor(vg, ballColor); nvgStrokeWidth(vg, 2); nvgBeginPath(vg); long lastI = module->points.size() - 1; for (long i = lastI; i>=0 && ipoints.size()); i--) { if(i == lastI){ nvgMoveTo(vg, module->points[i].x, module->points[i].y); } else { nvgLineTo(vg, module->points[i].x, module->points[i].y); } } nvgStroke(vg); } //MAIN/////////////////////////////////// //horizontal line nvgStrokeColor(vg, nvgRGB(255, 255, 255)); nvgBeginPath(vg); nvgMoveTo(vg, 0, ballY); nvgLineTo(vg, box.size.x, ballY); nvgStroke(vg); //vertical line nvgStrokeColor(vg, nvgRGB(255, 255, 255)); nvgBeginPath(vg); nvgMoveTo(vg, ballX, 0); nvgLineTo(vg, ballX, box.size.y); nvgStroke(vg); //ball nvgFillColor(vg, ballColor); nvgStrokeColor(vg, ballColor); nvgStrokeWidth(vg, module->ballStrokeWidth); nvgBeginPath(vg); nvgCircle(vg, ballX, ballY, module->ballRadius); if(module->params[XYPad::GATE_PARAM].value)nvgFill(vg); nvgStroke(vg); } }; ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// struct XYPadWidget : ModuleWidget { XYPadWidget(XYPad *module); Menu *createContextMenu() override; }; struct RandomShapeButton : TinyButton { void onMouseDown(EventMouseDown &e) override { TinyButton::onMouseDown(e); XYPadWidget *xyw = this->getAncestorOfType(); XYPad *xyPad = dynamic_cast(xyw->module); xyPad->randomizeShape(); } }; struct RandomVariationButton : TinyButton { void onMouseDown(EventMouseDown &e) override { TinyButton::onMouseDown(e); XYPadWidget *xyw = this->getAncestorOfType(); XYPad *xyPad = dynamic_cast(xyw->module); xyPad->makeShape(xyPad->lastRandomShape); } }; XYPadWidget::XYPadWidget(XYPad *module) : ModuleWidget(module) { box.size = Vec(RACK_GRID_WIDTH*24, RACK_GRID_HEIGHT); SVGPanel *panel = new SVGPanel(); panel->box.size = box.size; panel->setBackground(SVG::load(assetPlugin(plugin, "res/XYPad.svg"))); addChild(panel); XYPadDisplay *display = new XYPadDisplay(); display->module = module; display->box.pos = Vec(2, 40); display->box.size = Vec(box.size.x - 4, RACK_GRID_HEIGHT - 80); addChild(display); module->displayWidth = display->box.size.x; module->displayHeight = display->box.size.y; module->updateMinMax(); module->defaultPos(); addChild(Widget::create(Vec(40, 20))); addChild(Widget::create(Vec(55, 20))); addParam(ParamWidget::create(Vec(90, 20), module, XYPad::RND_SHAPES_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(105, 20), module, XYPad::RND_VARIATION_PARAM, 0.0, 1.0, 0.0)); addParam(ParamWidget::create(Vec(140, 20), module, XYPad::SCALE_X_PARAM, 0.01, 1.0, 0.5)); addParam(ParamWidget::create(Vec(200, 20), module, XYPad::SCALE_Y_PARAM, 0.01, 1.0, 0.5)); addParam(ParamWidget::create(Vec(260, 20), module, XYPad::OFFSET_X_VOLTS_PARAM, -5.0, 5.0, 5.0)); addParam(ParamWidget::create(Vec(320, 20), module, XYPad::OFFSET_Y_VOLTS_PARAM, -5.0, 5.0, 5.0)); //////////////////////////////////////////////////////////// addInput(Port::create(Vec(25, 360), Port::INPUT, module, XYPad::PLAY_GATE_INPUT)); addParam(ParamWidget::create(Vec(71, 360), module, XYPad::AUTO_PLAY_PARAM, 0.0, 1.0, 0.0)); addChild(ModuleLightWidget::create>(Vec(71+3.75, 360+3.75), module, XYPad::AUTO_LIGHT)); addInput(Port::create(Vec(110, 360), Port::INPUT, module, XYPad::PLAY_SPEED_INPUT)); addParam(ParamWidget::create(Vec(130, 360), module, XYPad::PLAY_SPEED_PARAM, 0.0, 10.0, 1.0)); addParam(ParamWidget::create(Vec(157, 360), module, XYPad::SPEED_MULT_PARAM, 1.0, 100.0, 1.0)); addOutput(Port::create(Vec(195, 360), Port::OUTPUT, module, XYPad::X_OUTPUT)); addOutput(Port::create(Vec(220, 360), Port::OUTPUT, module, XYPad::Y_OUTPUT)); addOutput(Port::create(Vec(255, 360), Port::OUTPUT, module, XYPad::X_INV_OUTPUT)); addOutput(Port::create(Vec(280, 360), Port::OUTPUT, module, XYPad::Y_INV_OUTPUT)); addOutput(Port::create(Vec(320, 360), Port::OUTPUT, module, XYPad::GATE_OUTPUT)); } struct PlayModeItem : MenuItem { XYPad *xyPad; int mode; void onAction(EventAction &e) override { xyPad->curPlayMode = mode; xyPad->setState(XYPad::STATE_AUTO_PLAYING); } void step() override { rightText = (xyPad->curPlayMode == mode) ? "✔" : ""; } }; struct ShapeMenuItem : MenuItem { XYPad *xyPad; int shape = -1; void onAction(EventAction &e) override { xyPad->makeShape(shape); } }; Menu *XYPadWidget::createContextMenu() { Menu *menu = ModuleWidget::createContextMenu(); { MenuLabel *spacerLabel = new MenuLabel(); menu->addChild(spacerLabel); } XYPad *xyPad = dynamic_cast(module); assert(xyPad); { PlayModeItem *item = new PlayModeItem(); item->text = "Forward Loop"; item->xyPad = xyPad; item->mode = XYPad::FWD_LOOP; menu->addChild(item); } { PlayModeItem *item = new PlayModeItem(); item->text = "Backward Loop"; item->xyPad = xyPad; item->mode = XYPad::BWD_LOOP; menu->addChild(item); } { PlayModeItem *item = new PlayModeItem(); item->text = "Forward One-Shot"; item->xyPad = xyPad; item->mode = XYPad::FWD_ONE_SHOT; menu->addChild(item); } { PlayModeItem *item = new PlayModeItem(); item->text = "Backward One-Shot"; item->xyPad = xyPad; item->mode = XYPad::BWD_ONE_SHOT; menu->addChild(item); } { PlayModeItem *item = new PlayModeItem(); item->text = "Forward-Backward Loop"; item->xyPad = xyPad; item->mode = XYPad::FWD_BWD_LOOP; menu->addChild(item); } { PlayModeItem *item = new PlayModeItem(); item->text = "Backward-Forward Loop"; item->xyPad = xyPad; item->mode = XYPad::BWD_FWD_LOOP; menu->addChild(item); } return menu; } } // namespace rack_plugin_JW_Modules using namespace rack_plugin_JW_Modules; RACK_PLUGIN_MODEL_INIT(JW_Modules, XYPad) { Model *modelXYPad = Model::create("JW-Modules", "XYPad", "XY Pad", LFO_TAG, ENVELOPE_GENERATOR_TAG, RANDOM_TAG, OSCILLATOR_TAG, SAMPLE_AND_HOLD_TAG); return modelXYPad; }