diff --git a/src/Scope.cpp b/src/Scope.cpp index 04d0ef0..413f5dc 100644 --- a/src/Scope.cpp +++ b/src/Scope.cpp @@ -2,7 +2,7 @@ #include "plugin.hpp" -static const int BUFFER_SIZE = 512; +static const int BUFFER_SIZE = 256; struct Scope : Module { @@ -30,200 +30,277 @@ struct Scope : Module { NUM_OUTPUTS }; enum LightIds { - PLOT_LIGHT, LISSAJOUS_LIGHT, - INTERNAL_LIGHT, EXTERNAL_LIGHT, NUM_LIGHTS }; - float bufferX[16][BUFFER_SIZE] = {}; - float bufferY[16][BUFFER_SIZE] = {}; + struct Point { + float minX[16] = {}; + float maxX[16] = {}; + float minY[16] = {}; + float maxY[16] = {}; + + Point() { + for (int c = 0; c < 16; c++) { + minX[c] = INFINITY; + maxX[c] = -INFINITY; + minY[c] = INFINITY; + maxY[c] = -INFINITY; + } + } + }; + Point pointBuffer[BUFFER_SIZE]; int channelsX = 0; int channelsY = 0; int bufferIndex = 0; int frameIndex = 0; + Point currentPoint; - 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"); + configParam(X_SCALE_PARAM, 0.f, 8.f, 0.f, "Gain 1", " V/screen", 1 / 2.f, 20); + getParamQuantity(X_SCALE_PARAM)->snapEnabled = true; + configParam(X_POS_PARAM, -10.f, 10.f, 0.f, "Offset 1", " V"); + configParam(Y_SCALE_PARAM, 0.f, 8.f, 0.f, "Gain 2", " V/screen", 1 / 2.f, 20); + getParamQuantity(Y_SCALE_PARAM)->snapEnabled = true; + configParam(Y_POS_PARAM, -10.f, 10.f, 0.f, "Offset 2", " V"); + const float maxTime = -std::log2(5e1f); + const float minTime = -std::log2(5e-3f); + const float defaultTime = -std::log2(5e-1f); + configParam(TIME_PARAM, maxTime, minTime, defaultTime, "Time", " ms/screen", 1 / 2.f, 1000); + configSwitch(LISSAJOUS_PARAM, 0.f, 1.f, 0.f, "Scope mode", {"1 & 2", "1 x 2"}); + configParam(TRIG_PARAM, -10.f, 10.f, 0.f, "Trigger threshold", " V"); + configSwitch(EXTERNAL_PARAM, 0.f, 1.f, 1.f, "Trigger mode", {"Internal", "External"}); + + configInput(X_INPUT, "Ch 1"); + configInput(Y_INPUT, "Ch 2"); configInput(TRIG_INPUT, "External trigger"); + + configOutput(X_OUTPUT, "Ch 1"); + configOutput(Y_OUTPUT, "Ch 2"); } void onReset() override { - lissajous = false; - external = false; - std::memset(bufferX, 0, sizeof(bufferX)); - std::memset(bufferY, 0, sizeof(bufferY)); + for (int i = 0; i < BUFFER_SIZE; i++) { + pointBuffer[i] = Point(); + } } void process(const ProcessArgs& args) override { - // Modes - if (sumTrigger.process(params[LISSAJOUS_PARAM].getValue() > 0.f)) { - lissajous = !lissajous; - } - lights[PLOT_LIGHT].setBrightness(!lissajous); + bool lissajous = params[LISSAJOUS_PARAM].getValue() > 0.f; lights[LISSAJOUS_LIGHT].setBrightness(lissajous); - if (extTrigger.process(params[EXTERNAL_PARAM].getValue() > 0.f)) { - external = !external; - } - lights[INTERNAL_LIGHT].setBrightness(!external); + bool external = params[EXTERNAL_PARAM].getValue() > 0.f; lights[EXTERNAL_LIGHT].setBrightness(external); // Compute time - float deltaTime = std::pow(2.f, -params[TIME_PARAM].getValue()); + float deltaTime = std::pow(2.f, -params[TIME_PARAM].getValue()) / BUFFER_SIZE; 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)); + // TODO + // 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)); + // TODO + // std::memset(bufferY, 0, sizeof(bufferY)); this->channelsY = channelsY; } - // Add frame to buffer + // Copy inputs to outputs + outputs[X_OUTPUT].setChannels(channelsX); + outputs[X_OUTPUT].writeVoltages(inputs[X_INPUT].getVoltages()); + outputs[Y_OUTPUT].setChannels(channelsY); + outputs[Y_OUTPUT].writeVoltages(inputs[Y_INPUT].getVoltages()); + + // Add point to buffer if recording if (bufferIndex < BUFFER_SIZE) { - if (++frameIndex > frameCount) { + // Get input + for (int c = 0; c < channelsX; c++) { + float x = inputs[X_INPUT].getVoltage(c); + currentPoint.minX[c] = std::min(currentPoint.minX[c], x); + currentPoint.maxX[c] = std::max(currentPoint.maxX[c], x); + } + for (int c = 0; c < channelsY; c++) { + float y = inputs[Y_INPUT].getVoltage(c); + currentPoint.minY[c] = std::min(currentPoint.minY[c], y); + currentPoint.maxY[c] = std::max(currentPoint.maxY[c], y); + } + + 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); - } + // Push current point + pointBuffer[bufferIndex] = currentPoint; + // Reset current point + currentPoint = Point(); 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]; + // Wait for trigger if no longer recording + if (bufferIndex >= BUFFER_SIZE) { + auto trigger = [&]() { + for (int c = 0; c < 16; c++) { + triggers[c].reset(); + } + bufferIndex = 0; + frameIndex = 0; + }; - // 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 immediately if external but nothing plugged in, or in Lissajous mode + if (lissajous || (external && !inputs[TRIG_INPUT].isConnected())) { 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(); + // 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; + } + } } - 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; + bool isLissajous() { + return params[LISSAJOUS_PARAM].getValue() > 0.f; } void dataFromJson(json_t* rootJ) override { - json_t* sumJ = json_object_get(rootJ, "lissajous"); - if (sumJ) - lissajous = json_integer_value(sumJ); + // In <2.0, lissajous and external were class variables + json_t* lissajousJ = json_object_get(rootJ, "lissajous"); + if (lissajousJ) { + if (json_integer_value(lissajousJ)) + params[LISSAJOUS_PARAM].setValue(1.f); + } - json_t* extJ = json_object_get(rootJ, "external"); - if (extJ) - external = json_integer_value(extJ); + json_t* externalJ = json_object_get(rootJ, "external"); + if (externalJ) { + if (json_integer_value(externalJ)) + params[EXTERNAL_PARAM].setValue(1.f); + } } }; struct ScopeDisplay : LedDisplay { Scope* module; + ModuleWidget* moduleWidget; 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; - } + float min = INFINITY; + float max = -INFINITY; }; - - Stats statsX, statsY; + Stats statsX; + Stats statsY; ScopeDisplay() { fontPath = asset::system("res/fonts/ShareTechMono-Regular.ttf"); } - void drawWaveform(const DrawArgs& args, float* bufferX, float offsetX, float gainX, float* bufferY, float offsetY, float gainY) { - assert(bufferY); + void calculateStats(Stats& stats, int wave, int channels) { + if (!module) + return; + + stats = Stats(); + for (int i = 0; i < BUFFER_SIZE; i++) { + const Scope::Point& point = module->pointBuffer[i]; + for (int c = 0; c < channels; c++) { + float max = (wave == 0) ? point.maxX[c] : point.maxY[c]; + float min = (wave == 0) ? point.minX[c] : point.minY[c]; + + stats.max = std::fmax(stats.max, max); + stats.min = std::fmin(stats.min, min); + } + } + } + + void drawWave(const DrawArgs& args, int wave, int channel, float offset, float gain) { + if (!module) + return; + 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); + Rect b = box.zeroPos().shrink(Vec(0, 15)); + nvgScissor(args.vg, RECT_ARGS(b)); nvgBeginPath(args.vg); + // Draw max points on top for (int i = 0; i < BUFFER_SIZE; i++) { - Vec v; - if (bufferX) - v.x = (bufferX[i] + offsetX) * gainX / 2.f + 0.5f; + const Scope::Point& point = module->pointBuffer[i]; + float max = (wave == 0) ? point.maxX[channel] : point.maxY[channel]; + if (!std::isfinite(max)) + continue; + + Vec p; + p.x = (float) i / (BUFFER_SIZE - 1); + p.y = (max + offset) * gain * -0.5f + 0.5f; + p = b.interpolate(p); + p.y += 1.0; + if (i == 0) + nvgMoveTo(args.vg, p.x, p.y); else - v.x = (float) i / (BUFFER_SIZE - 1); - v.y = (bufferY[i] + offsetY) * gainY / 2.f + 0.5f; + nvgLineTo(args.vg, p.x, p.y); + } + // Draw min points on bottom + for (int i = BUFFER_SIZE - 1; i >= 0; i--) { + const Scope::Point& point = module->pointBuffer[i]; + float min = (wave == 0) ? point.minX[channel] : point.minY[channel]; + if (!std::isfinite(min)) + continue; + + Vec p; + p.x = (float) i / (BUFFER_SIZE - 1); + p.y = (min + offset) * gain * -0.5f + 0.5f; + p = b.interpolate(p); + p.y -= 1.0; + nvgLineTo(args.vg, p.x, p.y); + } + nvgClosePath(args.vg); + // nvgLineCap(args.vg, NVG_ROUND); + // nvgMiterLimit(args.vg, 2.f); + nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER); + nvgFill(args.vg); + nvgResetScissor(args.vg); + nvgRestore(args.vg); + } + + void drawLissajous(const DrawArgs& args, int channel, float offsetX, float gainX, float offsetY, float gainY) { + if (!module) + return; + + nvgSave(args.vg); + Rect b = box.zeroPos().shrink(Vec(0, 15)); + nvgScissor(args.vg, RECT_ARGS(b)); + nvgBeginPath(args.vg); + int bufferIndex = module->bufferIndex; + for (int i = 0; i < BUFFER_SIZE; i++) { + // Get average point + const Scope::Point& point = module->pointBuffer[(i + bufferIndex) % BUFFER_SIZE]; + float avgX = (point.minX[channel] + point.maxX[channel]) / 2; + float avgY = (point.minY[channel] + point.maxY[channel]) / 2; + if (!std::isfinite(avgX) || !std::isfinite(avgY)) + continue; + 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); + p.x = (avgX + offsetX) * gainX * -0.5f + 0.5f; + p.y = (avgY + offsetY) * gainY * -0.5f + 0.5f; + p = b.interpolate(p); if (i == 0) nvgMoveTo(args.vg, p.x, p.y); else @@ -251,7 +328,6 @@ struct ScopeDisplay : LedDisplay { nvgBeginPath(args.vg); nvgMoveTo(args.vg, p.x - 13, p.y); nvgLineTo(args.vg, 0, p.y); - nvgClosePath(args.vg); } nvgStroke(args.vg); @@ -278,7 +354,7 @@ struct ScopeDisplay : LedDisplay { nvgResetScissor(args.vg); } - void drawStats(const DrawArgs& args, Vec pos, const char* title, Stats* stats) { + void drawStats(const DrawArgs& args, Vec pos, const char* title, const Stats& stats) { std::shared_ptr font = APP->window->loadFont(fontPath); if (!font) return; @@ -294,16 +370,35 @@ struct ScopeDisplay : LedDisplay { std::string text; text = "pp "; - text += isNear(stats->vpp, 0.f, 100.f) ? string::f("% 6.2f", stats->vpp) : " ---"; + float pp = stats.max - stats.min; + text += isNear(pp, 0.f, 100.f) ? string::f("% 6.2f", pp) : " ---"; 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) : " ---"; + text += isNear(stats.max, 0.f, 100.f) ? string::f("% 6.2f", stats.max) : " ---"; 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) : " ---"; + text += isNear(stats.min, 0.f, 100.f) ? string::f("% 6.2f", stats.min) : " ---"; nvgText(args.vg, pos.x + 58 * 2, pos.y, text.c_str(), NULL); } + void drawBackground(const DrawArgs& args) { + Rect b = box.zeroPos().shrink(Vec(0, 15)); + + nvgStrokeColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x10)); + for (int i = 0; i < 5; i++) { + nvgBeginPath(args.vg); + + Vec p; + p.x = 0.0; + p.y = float(i) / (5 - 1); + nvgMoveTo(args.vg, VEC_ARGS(b.interpolate(p))); + + p.x = 1.0; + nvgLineTo(args.vg, VEC_ARGS(b.interpolate(p))); + nvgStroke(args.vg); + } + } + void drawLayer(const DrawArgs& args, int layer) override { if (layer != 1) return; @@ -316,41 +411,53 @@ struct ScopeDisplay : LedDisplay { float offsetX = module->params[Scope::X_POS_PARAM].getValue(); float offsetY = module->params[Scope::Y_POS_PARAM].getValue(); + // Get input colors + PortWidget* inputX = moduleWidget->getInput(Scope::X_INPUT); + PortWidget* inputY = moduleWidget->getInput(Scope::Y_INPUT); + CableWidget* inputXCable = APP->scene->rack->getTopCable(inputX); + CableWidget* inputYCable = APP->scene->rack->getTopCable(inputY); + NVGcolor inputXColor = inputXCable ? inputXCable->color : color::WHITE; + NVGcolor inputYColor = inputYCable ? inputYCable->color : color::WHITE; + // Draw waveforms - if (module->lissajous) { + if (module->isLissajous()) { // X x Y - int lissajousChannels = std::max(module->channelsX, module->channelsY); + int lissajousChannels = std::min(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); + nvgStrokeColor(args.vg, SCHEME_YELLOW); + drawLissajous(args, c, offsetX, gainX, 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); + nvgFillColor(args.vg, inputYColor); + drawWave(args, 1, 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); + nvgFillColor(args.vg, inputXColor); + drawWave(args, 0, c, offsetX, gainX); } + // Trigger float trigThreshold = module->params[Scope::TRIG_PARAM].getValue(); trigThreshold = (trigThreshold + offsetX) * gainX; drawTrig(args, trigThreshold); + + // Background lines + drawBackground(args); } // Calculate and draw stats if (++statsFrame >= 4) { statsFrame = 0; - statsX.calculate(module->bufferX[0], module->channelsX); - statsY.calculate(module->bufferY[0], module->channelsY); + calculateStats(statsX, 0, module->channelsX); + calculateStats(statsY, 1, module->channelsY); } - drawStats(args, Vec(0, 0), "X", &statsX); - drawStats(args, Vec(0, box.size.y - 15), "Y", &statsY); + drawStats(args, Vec(0, 0 + 1), "X", statsX); + drawStats(args, Vec(0, box.size.y - 15 - 1), "Y", statsY); } }; @@ -365,14 +472,14 @@ struct ScopeWidget : ModuleWidget { 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(createLightParamCentered>>(mm2px(Vec(8.643, 80.603)), module, Scope::LISSAJOUS_PARAM, Scope::LISSAJOUS_LIGHT)); 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(createLightParamCentered>>(mm2px(Vec(57.397, 80.521)), module, Scope::EXTERNAL_PARAM, Scope::EXTERNAL_LIGHT)); 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)); + addParam(createParamCentered(mm2px(Vec(57.397, 96.815)), module, Scope::TRIG_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)); @@ -381,9 +488,10 @@ struct ScopeWidget : ModuleWidget { 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); + ScopeDisplay* display = createWidget(mm2px(Vec(0.0, 13.039))); + display->box.size = mm2px(Vec(66.04, 55.88)); display->module = module; + display->moduleWidget = this; addChild(display); } };