//============================================================================================================ //! //! \file Scope-G1.cpp //! //! \brief Scope-G1 is a... //! //============================================================================================================ #include #include "dsp/digital.hpp" #include "Gratrix.hpp" namespace rack_plugin_Gratrix { #define BUFFER_SIZE 512 struct Scope : Module { enum ParamIds { X_SCALE_PARAM, X_POS_PARAM, TIME_PARAM, TRIG_PARAM, EXTERNAL_PARAM, DISP_PARAM, NUM_PARAMS }; enum InputIds { X_INPUT, // N TRIG_INPUT, // N NUM_INPUTS }; enum OutputIds { NUM_OUTPUTS }; enum LightIds { NUM_LIGHTS }; struct Voice { bool active = false; float bufferX[BUFFER_SIZE] = {}; int bufferIndex = 0; float frameIndex = 0; SchmittTrigger resetTrigger; void step(bool external, int frameCount, const Param &trig_param, const Input &x_input, const Input &trig_input); }; bool external = false; Voice voice[GTX__N+1]; Scope() : Module(NUM_PARAMS, GTX__N * NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {} static constexpr std::size_t imap(std::size_t port, std::size_t bank) { return port + bank * NUM_INPUTS; } void step() override; }; void Scope::step() { // Modes external = params[EXTERNAL_PARAM].value <= 0.0f; // Compute time float deltaTime = powf(2.0f, params[TIME_PARAM].value); int frameCount = (int)ceilf(deltaTime * engineGetSampleRate()); Input x_sum, trig_sum; int count = 0; for (int k=0; k 0) { x_sum.value /= static_cast(count); } voice[GTX__N].step(external, frameCount, params[TRIG_PARAM], x_sum, trig_sum); } void Scope::Voice::step(bool external, int frameCount, const Param &trig_param, const Input &x_input, const Input &trig_input) { // Copy active state active = x_input.active; // Add frame to buffer if (bufferIndex < BUFFER_SIZE) { if (++frameIndex > frameCount) { frameIndex = 0; bufferX[bufferIndex] = x_input.value; bufferIndex++; } } // Are we waiting on the next trigger? if (bufferIndex >= BUFFER_SIZE) { // Trigger immediately if external but nothing plugged in if (external && !trig_input.active) { bufferIndex = 0; frameIndex = 0; return; } // Reset the Schmitt trigger so we don't trigger immediately if the input is high if (frameIndex == 0) { resetTrigger.reset(); } frameIndex++; // Must go below 0.1fV to trigger float gate = external ? trig_input.value : x_input.value; // Reset if triggered float holdTime = 0.1f; if (resetTrigger.process(rescale(gate, trig_param.value - 0.1f, trig_param.value, 0.f, 1.f)) || (frameIndex >= engineGetSampleRate() * holdTime)) { bufferIndex = 0; frameIndex = 0; return; } // Reset if we've waited too long if (frameIndex >= engineGetSampleRate() * holdTime) { bufferIndex = 0; frameIndex = 0; return; } } } struct Display : TransparentWidget { Scope *module; int frame = 0; std::shared_ptr font; struct Stats { float vrms, vpp, vmin, vmax; void calculate(float *values) { vrms = 0.0f; vmax = -INFINITY; vmin = INFINITY; for (int i = 0; i < BUFFER_SIZE; i++) { float v = values[i]; vrms += v*v; vmax = fmaxf(vmax, v); vmin = fminf(vmin, v); } vrms = sqrtf(vrms / BUFFER_SIZE); vpp = vmax - vmin; } }; Stats statsX[GTX__N + 1]; Display() { font = Font::load(assetPlugin(plugin, "res/fonts/Sudo.ttf")); } void drawWaveform(NVGcontext *vg, float *valuesX, const Rect &b) { if (!valuesX) return; nvgSave(vg); nvgScissor(vg, b.pos.x, b.pos.y, b.size.x, b.size.y); nvgBeginPath(vg); // Draw maximum display left to right for (int i = 0; i < BUFFER_SIZE; i++) { float x = (float)i / (BUFFER_SIZE - 1); float y = valuesX[i] / 2.0f + 0.5f; Vec p; p.x = b.pos.x + b.size.x * x; p.y = b.pos.y + b.size.y * (1.0f - y); if (i == 0) nvgMoveTo(vg, p.x, p.y); else nvgLineTo(vg, p.x, p.y); } nvgLineCap(vg, NVG_ROUND); nvgMiterLimit(vg, 2.0); nvgStrokeWidth(vg, 1.5); nvgGlobalCompositeOperation(vg, NVG_LIGHTER); nvgStroke(vg); nvgResetScissor(vg); nvgRestore(vg); } void drawTrig(NVGcontext *vg, float value, const Rect &b) { nvgScissor(vg, b.pos.x, b.pos.y, b.size.x, b.size.y); value = value / 2.0f + 0.5f; Vec p = Vec(b.pos.x + b.size.x, b.pos.y + b.size.y * (1.0f - value)); // Draw line nvgStrokeColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x10)); { nvgBeginPath(vg); nvgMoveTo(vg, p.x - 13, p.y); nvgLineTo(vg, 0, p.y); nvgClosePath(vg); } nvgStroke(vg); // Draw indicator nvgFillColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x60)); { nvgBeginPath(vg); nvgMoveTo(vg, p.x - 2, p.y - 4); nvgLineTo(vg, p.x - 9, p.y - 4); nvgLineTo(vg, p.x - 13, p.y); nvgLineTo(vg, p.x - 9, p.y + 4); nvgLineTo(vg, p.x - 2, p.y + 4); nvgClosePath(vg); } nvgFill(vg); nvgFontSize(vg, 9); nvgFontFaceId(vg, font->handle); nvgFillColor(vg, nvgRGBA(0x1e, 0x28, 0x2b, 0xff)); nvgText(vg, p.x - 8, p.y + 3, "T", NULL); nvgResetScissor(vg); } void drawStats(NVGcontext *vg, Vec pos, const char *title, const Stats &stats) { nvgFontSize(vg, 13); nvgFontFaceId(vg, font->handle); nvgTextLetterSpacing(vg, -2); nvgFillColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x80)); char text[128]; snprintf(text, sizeof(text), "%s. %4.1f [%+5.1f %+5.1f]", title, stats.vpp, stats.vmin, stats.vmax); nvgText(vg, pos.x + 6, pos.y + 11, text, NULL); } void draw(NVGcontext *vg) override { float gainX = powf(2.0, roundf(module->params[Scope::X_SCALE_PARAM].value)); float offsetX = module->params[Scope::X_POS_PARAM].value; int disp = static_cast(module->params[Scope::DISP_PARAM].value + 0.5f); int k0, k1, kM; if (disp == 0 ) { k0 = 0; k1 = GTX__N; kM = 0; } // six up else if (disp == GTX__N+1) { k0 = GTX__N; k1 = GTX__N+1; kM = 100; } // sum else { k0 = disp-1; k1 = disp ; kM = 100; } // individual static const char *stats_lab[GTX__N+1] = {"1", "2", "3", "4", "5", "6", "SUM"}; for (int k=k0; kvoice[k].bufferX[i] + offsetX) * gainX / 10.0; } // Draw waveforms if (module->voice[k].active) { if (k&1) nvgStrokeColor(vg, nvgRGBA(0xe1, 0x02, 0x78, 0xc0)); else nvgStrokeColor(vg, nvgRGBA(0x28, 0xb0, 0xf3, 0xc0)); drawWaveform(vg, valuesX, b); } float valueTrig = (module->params[Scope::TRIG_PARAM].value + offsetX) * gainX / 10.0; drawTrig(vg, valueTrig, b); // Calculate and draw stats if (frame == 0) { statsX[k].calculate(module->voice[k].bufferX); } Vec stats_pos = b.pos; if (k >= 3 && k < GTX__N) { stats_pos.y += b.size.y; } else { stats_pos.y -= 15; } drawStats(vg, stats_pos, stats_lab[k], statsX[k]); } if (++frame >= 4) { frame = 0; } } }; //============================================================================================================ //! \brief The widget. struct GtxWidget_Scope_G1 : ModuleWidget { GtxWidget_Scope_G1(Scope *module) : ModuleWidget(module) { GTX__WIDGET(); box.size = Vec(24*15, 380); auto screen_pos = Vec(0, 35); auto screen_size = Vec(box.size.x, 220); #if GTX__SAVE_SVG { PanelGen pg(assetPlugin(plugin, "build/res/Scope-G1.svg"), box.size, "SCOPE-G1"); pg.rect(screen_pos, screen_size, "fill:#222222;stroke:none"); pg.bus_in(0, 2, "IN"); pg.bus_in(2, 2, "EXT TRIG"); pg.nob_sml_raw(gx(1-0.22), gy(2-0.24), "SCALE"); pg.nob_sml_raw(gx(1-0.22), gy(2+0.22), "POS"); pg.nob_sml_raw(gx(1+0.22), gy(2-0.24), "TIME"); pg.nob_sml_raw(gx(1+0.22), gy(2+0.22), "TRIG"); pg.tog_raw (gx(3-0.22), gy(2-0.24), "INT", "EXT"); pg.nob_sml_raw(gx(3+0.22), gy(2-0.24), "DISP"); for (std::size_t i=0; i<=12; i++) { float x = screen_pos.x + screen_size.x * i / 12.0f; pg.line(Vec(x, screen_pos.y + 15), Vec(x, screen_pos.y + screen_size.y - 15), "fill:none;stroke:#666666;stroke-width:1"); } for (std::size_t i=0; i<=8; i++) { float y = screen_pos.y + 15 + (screen_size.y - 30) * i / 8.0f; pg.line(Vec(screen_pos.x, y), Vec(screen_pos.x + screen_size.x, y), "fill:none;stroke:#666666;stroke-width:1"); } } #endif setPanel(SVG::load(assetPlugin(plugin, "res/Scope-G1.svg"))); { Display *display = new Display(); display->module = module; display->box.pos = screen_pos; display->box.size = screen_size; addChild(display); } addParam(createParamGTX(Vec(gx(1-0.22), gy(2-0.24)), module, Scope::X_SCALE_PARAM, -2.0f, 8.0f, 0.0f)); addParam(createParamGTX(Vec(gx(1-0.22), gy(2+0.22)), module, Scope::X_POS_PARAM, -10.0f, 10.0f, 0.0f)); addParam(createParamGTX(Vec(gx(1+0.22), gy(2-0.24)), module, Scope::TIME_PARAM, -6.0f, -16.0f, -14.0f)); addParam(createParamGTX(Vec(gx(1+0.22), gy(2+0.22)), module, Scope::TRIG_PARAM, -10.0f, 10.0f, 0.0f)); addParam(ParamWidget::create (tog(gx(3-0.22), gy(2-0.24)), module, Scope::EXTERNAL_PARAM, 0.0f, 1.0f, 1.0f)); addParam(createParamGTX(Vec(gx(3+0.22), gy(2-0.24)), module, Scope::DISP_PARAM, 0.0f, GTX__N+1, 0.0f)); for (std::size_t i=0; i(Vec(px(0, i), py(2, i)), module, Scope::imap(Scope::X_INPUT, i))); addInput(createInputGTX(Vec(px(2, i), py(2, i)), module, Scope::imap(Scope::TRIG_INPUT, i))); } } }; } // namespace rack_plugin_Gratrix using namespace rack_plugin_Gratrix; RACK_PLUGIN_MODEL_INIT(Gratrix, Scope_G1) { Model *model = Model::create("Gratrix", "Scope-G1", "Scope-G1", VISUAL_TAG); return model; }