#include #include "plugin.hpp" static const int BUFFER_SIZE = 512; struct Scope : Module { enum ParamIds { X_SCALE_PARAM, X_POS_PARAM, Y_SCALE_PARAM, Y_POS_PARAM, TIME_PARAM, LISSAJOUS_PARAM, TRIG_PARAM, EXTERNAL_PARAM, NUM_PARAMS }; enum InputIds { X_INPUT, Y_INPUT, TRIG_INPUT, NUM_INPUTS }; enum OutputIds { // new in 2.0 X_OUTPUT, Y_OUTPUT, NUM_OUTPUTS }; enum LightIds { PLOT_LIGHT, LISSAJOUS_LIGHT, INTERNAL_LIGHT, EXTERNAL_LIGHT, NUM_LIGHTS }; float bufferX[16][BUFFER_SIZE] = {}; float bufferY[16][BUFFER_SIZE] = {}; int channelsX = 0; int channelsY = 0; int bufferIndex = 0; int frameIndex = 0; dsp::BooleanTrigger sumTrigger; dsp::BooleanTrigger extTrigger; bool lissajous = false; bool external = false; dsp::SchmittTrigger triggers[16]; Scope() { config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); configParam(X_SCALE_PARAM, -2.f, 8.f, 0.f, "X scale", " V/div", 1 / 2.f, 5); configParam(X_POS_PARAM, -10.f, 10.f, 0.f, "X position", " V"); configParam(Y_SCALE_PARAM, -2.f, 8.f, 0.f, "Y scale", " V/div", 1 / 2.f, 5); configParam(Y_POS_PARAM, -10.f, 10.f, 0.f, "Y position", " V"); const float timeBase = (float) BUFFER_SIZE / 6; configParam(TIME_PARAM, 6.f, 16.f, 14.f, "Time", " ms/div", 1 / 2.f, 1000 * timeBase); configButton(LISSAJOUS_PARAM, "Separate/Lissajous mode"); configParam(TRIG_PARAM, -10.f, 10.f, 0.f, "Trigger position", " V"); configButton(EXTERNAL_PARAM, "Internal/external trigger mode"); configInput(X_INPUT, "X"); configInput(Y_INPUT, "Y"); configInput(TRIG_INPUT, "External trigger"); } void onReset() override { lissajous = false; external = false; std::memset(bufferX, 0, sizeof(bufferX)); std::memset(bufferY, 0, sizeof(bufferY)); } void process(const ProcessArgs& args) override { // Modes if (sumTrigger.process(params[LISSAJOUS_PARAM].getValue() > 0.f)) { lissajous = !lissajous; } lights[PLOT_LIGHT].setBrightness(!lissajous); lights[LISSAJOUS_LIGHT].setBrightness(lissajous); if (extTrigger.process(params[EXTERNAL_PARAM].getValue() > 0.f)) { external = !external; } lights[INTERNAL_LIGHT].setBrightness(!external); lights[EXTERNAL_LIGHT].setBrightness(external); // Compute time float deltaTime = std::pow(2.f, -params[TIME_PARAM].getValue()); int frameCount = (int) std::ceil(deltaTime * args.sampleRate); // Set channels int channelsX = inputs[X_INPUT].getChannels(); if (channelsX != this->channelsX) { std::memset(bufferX, 0, sizeof(bufferX)); this->channelsX = channelsX; } int channelsY = inputs[Y_INPUT].getChannels(); if (channelsY != this->channelsY) { std::memset(bufferY, 0, sizeof(bufferY)); this->channelsY = channelsY; } // Add frame to buffer if (bufferIndex < BUFFER_SIZE) { if (++frameIndex > frameCount) { frameIndex = 0; for (int c = 0; c < channelsX; c++) { bufferX[c][bufferIndex] = inputs[X_INPUT].getVoltage(c); } for (int c = 0; c < channelsY; c++) { bufferY[c][bufferIndex] = inputs[Y_INPUT].getVoltage(c); } bufferIndex++; } } // Don't wait for trigger if still filling buffer if (bufferIndex < BUFFER_SIZE) { return; } // Trigger immediately if external but nothing plugged in, or in Lissajous mode if (lissajous || (external && !inputs[TRIG_INPUT].isConnected())) { trigger(); return; } frameIndex++; // Reset if triggered float trigThreshold = params[TRIG_PARAM].getValue(); Input& trigInput = external ? inputs[TRIG_INPUT] : inputs[X_INPUT]; // This may be 0 int trigChannels = trigInput.getChannels(); for (int c = 0; c < trigChannels; c++) { float trigVoltage = trigInput.getVoltage(c); if (triggers[c].process(rescale(trigVoltage, trigThreshold, trigThreshold + 0.001f, 0.f, 1.f))) { trigger(); return; } } // Reset if we've been waiting for `holdTime` const float holdTime = 0.5f; if (frameIndex * args.sampleTime >= holdTime) { trigger(); return; } } void trigger() { for (int c = 0; c < 16; c++) { triggers[c].reset(); } bufferIndex = 0; frameIndex = 0; } json_t* dataToJson() override { json_t* rootJ = json_object(); json_object_set_new(rootJ, "lissajous", json_integer((int) lissajous)); json_object_set_new(rootJ, "external", json_integer((int) external)); return rootJ; } void dataFromJson(json_t* rootJ) override { json_t* sumJ = json_object_get(rootJ, "lissajous"); if (sumJ) lissajous = json_integer_value(sumJ); json_t* extJ = json_object_get(rootJ, "external"); if (extJ) external = json_integer_value(extJ); } }; struct ScopeDisplay : LedDisplay { Scope* module; int statsFrame = 0; std::string fontPath; struct Stats { float vpp = 0.f; float vmin = 0.f; float vmax = 0.f; void calculate(float* buffer, int channels) { vmax = -INFINITY; vmin = INFINITY; for (int i = 0; i < BUFFER_SIZE * channels; i++) { float v = buffer[i]; vmax = std::fmax(vmax, v); vmin = std::fmin(vmin, v); } vpp = vmax - vmin; } }; Stats statsX, statsY; ScopeDisplay() { fontPath = asset::plugin(pluginInstance, "res/sudo/Sudo.ttf"); } void drawWaveform(const DrawArgs& args, float* bufferX, float offsetX, float gainX, float* bufferY, float offsetY, float gainY) { assert(bufferY); nvgSave(args.vg); Rect b = Rect(Vec(0, 15), box.size.minus(Vec(0, 15 * 2))); nvgScissor(args.vg, b.pos.x, b.pos.y, b.size.x, b.size.y); nvgBeginPath(args.vg); for (int i = 0; i < BUFFER_SIZE; i++) { Vec v; if (bufferX) v.x = (bufferX[i] + offsetX) * gainX / 2.f + 0.5f; else v.x = (float) i / (BUFFER_SIZE - 1); v.y = (bufferY[i] + offsetY) * gainY / 2.f + 0.5f; Vec p; p.x = rescale(v.x, 0.f, 1.f, b.pos.x, b.pos.x + b.size.x); p.y = rescale(v.y, 0.f, 1.f, b.pos.y + b.size.y, b.pos.y); if (i == 0) nvgMoveTo(args.vg, p.x, p.y); else nvgLineTo(args.vg, p.x, p.y); } nvgLineCap(args.vg, NVG_ROUND); nvgMiterLimit(args.vg, 2.f); nvgStrokeWidth(args.vg, 1.5f); nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER); nvgStroke(args.vg); nvgResetScissor(args.vg); nvgRestore(args.vg); } void drawTrig(const DrawArgs& args, float value) { Rect b = Rect(Vec(0, 15), box.size.minus(Vec(0, 15 * 2))); nvgScissor(args.vg, b.pos.x, b.pos.y, b.size.x, b.size.y); value = value / 2.f + 0.5f; Vec p = Vec(box.size.x, b.pos.y + b.size.y * (1.f - value)); // Draw line nvgStrokeColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x10)); { nvgBeginPath(args.vg); nvgMoveTo(args.vg, p.x - 13, p.y); nvgLineTo(args.vg, 0, p.y); nvgClosePath(args.vg); } nvgStroke(args.vg); // Draw indicator nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x60)); { nvgBeginPath(args.vg); nvgMoveTo(args.vg, p.x - 2, p.y - 4); nvgLineTo(args.vg, p.x - 9, p.y - 4); nvgLineTo(args.vg, p.x - 13, p.y); nvgLineTo(args.vg, p.x - 9, p.y + 4); nvgLineTo(args.vg, p.x - 2, p.y + 4); nvgClosePath(args.vg); } nvgFill(args.vg); std::shared_ptr font = APP->window->loadFont(fontPath); nvgFontSize(args.vg, 9); nvgFontFaceId(args.vg, font->handle); nvgFillColor(args.vg, nvgRGBA(0x1e, 0x28, 0x2b, 0xff)); nvgText(args.vg, p.x - 8, p.y + 3, "T", NULL); nvgResetScissor(args.vg); } void drawStats(const DrawArgs& args, Vec pos, const char* title, Stats* stats) { std::shared_ptr font = APP->window->loadFont(fontPath); nvgFontSize(args.vg, 13); nvgFontFaceId(args.vg, font->handle); nvgTextLetterSpacing(args.vg, -2); nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x40)); nvgText(args.vg, pos.x + 6, pos.y + 11, title, NULL); nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x80)); pos = pos.plus(Vec(22, 11)); std::string text; text = "pp "; text += isNear(stats->vpp, 0.f, 100.f) ? string::f("% 6.2f", stats->vpp) : " ---"; nvgText(args.vg, pos.x, pos.y, text.c_str(), NULL); text = "max "; text += isNear(stats->vmax, 0.f, 100.f) ? string::f("% 6.2f", stats->vmax) : " ---"; nvgText(args.vg, pos.x + 58 * 1, pos.y, text.c_str(), NULL); text = "min "; text += isNear(stats->vmin, 0.f, 100.f) ? string::f("% 6.2f", stats->vmin) : " ---"; nvgText(args.vg, pos.x + 58 * 2, pos.y, text.c_str(), NULL); } void drawLayer(const DrawArgs& args, int layer) override { if (layer != 1) return; if (!module) return; float gainX = std::pow(2.f, std::round(module->params[Scope::X_SCALE_PARAM].getValue())) / 10.f; float gainY = std::pow(2.f, std::round(module->params[Scope::Y_SCALE_PARAM].getValue())) / 10.f; float offsetX = module->params[Scope::X_POS_PARAM].getValue(); float offsetY = module->params[Scope::Y_POS_PARAM].getValue(); // Draw waveforms if (module->lissajous) { // X x Y int lissajousChannels = std::max(module->channelsX, module->channelsY); for (int c = 0; c < lissajousChannels; c++) { nvgStrokeColor(args.vg, nvgRGBA(0x9f, 0xe4, 0x36, 0xc0)); drawWaveform(args, module->bufferX[c], offsetX, gainX, module->bufferY[c], offsetY, gainY); } } else { // Y for (int c = 0; c < module->channelsY; c++) { nvgStrokeColor(args.vg, nvgRGBA(0xe1, 0x02, 0x78, 0xc0)); drawWaveform(args, NULL, 0, 0, module->bufferY[c], offsetY, gainY); } // X for (int c = 0; c < module->channelsX; c++) { nvgStrokeColor(args.vg, nvgRGBA(0x28, 0xb0, 0xf3, 0xc0)); drawWaveform(args, NULL, 0, 0, module->bufferX[c], offsetX, gainX); } float trigThreshold = module->params[Scope::TRIG_PARAM].getValue(); trigThreshold = (trigThreshold + offsetX) * gainX; drawTrig(args, trigThreshold); } // Calculate and draw stats if (++statsFrame >= 4) { statsFrame = 0; statsX.calculate(module->bufferX[0], module->channelsX); statsY.calculate(module->bufferY[0], module->channelsY); } drawStats(args, Vec(0, 0), "X", &statsX); drawStats(args, Vec(0, box.size.y - 15), "Y", &statsY); } }; struct ScopeWidget : ModuleWidget { ScopeWidget(Scope* module) { setModule(module); setPanel(createPanel(asset::plugin(pluginInstance, "res/Scope.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))); // addParam(createParamCentered(mm2px(Vec(8.643, 80.603)), module, Scope::_1X2_PARAM)); addParam(createParamCentered(mm2px(Vec(24.897, 80.551)), module, Scope::X_SCALE_PARAM)); addParam(createParamCentered(mm2px(Vec(41.147, 80.551)), module, Scope::Y_SCALE_PARAM)); // addParam(createParamCentered(mm2px(Vec(57.397, 80.521)), module, Scope::TRIG_PARAM)); addParam(createParamCentered(mm2px(Vec(8.643, 96.819)), module, Scope::TIME_PARAM)); addParam(createParamCentered(mm2px(Vec(24.897, 96.789)), module, Scope::X_POS_PARAM)); addParam(createParamCentered(mm2px(Vec(41.147, 96.815)), module, Scope::Y_POS_PARAM)); // addParam(createParamCentered(mm2px(Vec(57.397, 96.815)), module, Scope::THERS_PARAM)); addInput(createInputCentered(mm2px(Vec(8.643, 113.115)), module, Scope::X_INPUT)); addInput(createInputCentered(mm2px(Vec(33.023, 113.115)), module, Scope::Y_INPUT)); addInput(createInputCentered(mm2px(Vec(57.397, 113.115)), module, Scope::TRIG_INPUT)); addOutput(createOutputCentered(mm2px(Vec(20.833, 113.115)), module, Scope::X_OUTPUT)); addOutput(createOutputCentered(mm2px(Vec(45.212, 113.115)), module, Scope::Y_OUTPUT)); ScopeDisplay* display = createWidget(Vec(0, 44)); display->box.size = Vec(box.size.x, 140); display->module = module; addChild(display); } }; Model* modelScope = createModel("Scope");