| @@ -2,7 +2,7 @@ | |||||
| #include "plugin.hpp" | #include "plugin.hpp" | ||||
| static const int BUFFER_SIZE = 512; | |||||
| static const int BUFFER_SIZE = 256; | |||||
| struct Scope : Module { | struct Scope : Module { | ||||
| @@ -30,200 +30,277 @@ struct Scope : Module { | |||||
| NUM_OUTPUTS | NUM_OUTPUTS | ||||
| }; | }; | ||||
| enum LightIds { | enum LightIds { | ||||
| PLOT_LIGHT, | |||||
| LISSAJOUS_LIGHT, | LISSAJOUS_LIGHT, | ||||
| INTERNAL_LIGHT, | |||||
| EXTERNAL_LIGHT, | EXTERNAL_LIGHT, | ||||
| NUM_LIGHTS | 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 channelsX = 0; | ||||
| int channelsY = 0; | int channelsY = 0; | ||||
| int bufferIndex = 0; | int bufferIndex = 0; | ||||
| int frameIndex = 0; | int frameIndex = 0; | ||||
| Point currentPoint; | |||||
| dsp::BooleanTrigger sumTrigger; | |||||
| dsp::BooleanTrigger extTrigger; | |||||
| bool lissajous = false; | |||||
| bool external = false; | |||||
| dsp::SchmittTrigger triggers[16]; | dsp::SchmittTrigger triggers[16]; | ||||
| Scope() { | Scope() { | ||||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | 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"); | configInput(TRIG_INPUT, "External trigger"); | ||||
| configOutput(X_OUTPUT, "Ch 1"); | |||||
| configOutput(Y_OUTPUT, "Ch 2"); | |||||
| } | } | ||||
| void onReset() override { | 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 { | 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); | 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); | lights[EXTERNAL_LIGHT].setBrightness(external); | ||||
| // Compute time | // 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); | int frameCount = (int) std::ceil(deltaTime * args.sampleRate); | ||||
| // Set channels | // Set channels | ||||
| int channelsX = inputs[X_INPUT].getChannels(); | int channelsX = inputs[X_INPUT].getChannels(); | ||||
| if (channelsX != this->channelsX) { | if (channelsX != this->channelsX) { | ||||
| std::memset(bufferX, 0, sizeof(bufferX)); | |||||
| // TODO | |||||
| // std::memset(bufferX, 0, sizeof(bufferX)); | |||||
| this->channelsX = channelsX; | this->channelsX = channelsX; | ||||
| } | } | ||||
| int channelsY = inputs[Y_INPUT].getChannels(); | int channelsY = inputs[Y_INPUT].getChannels(); | ||||
| if (channelsY != this->channelsY) { | if (channelsY != this->channelsY) { | ||||
| std::memset(bufferY, 0, sizeof(bufferY)); | |||||
| // TODO | |||||
| // std::memset(bufferY, 0, sizeof(bufferY)); | |||||
| this->channelsY = channelsY; | 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 (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; | 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++; | 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(); | trigger(); | ||||
| return; | 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 { | 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 { | struct ScopeDisplay : LedDisplay { | ||||
| Scope* module; | Scope* module; | ||||
| ModuleWidget* moduleWidget; | |||||
| int statsFrame = 0; | int statsFrame = 0; | ||||
| std::string fontPath; | std::string fontPath; | ||||
| struct Stats { | 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() { | ScopeDisplay() { | ||||
| fontPath = asset::system("res/fonts/ShareTechMono-Regular.ttf"); | 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); | 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); | nvgBeginPath(args.vg); | ||||
| // Draw max points on top | |||||
| for (int i = 0; i < BUFFER_SIZE; i++) { | 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 | 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; | 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) | if (i == 0) | ||||
| nvgMoveTo(args.vg, p.x, p.y); | nvgMoveTo(args.vg, p.x, p.y); | ||||
| else | else | ||||
| @@ -251,7 +328,6 @@ struct ScopeDisplay : LedDisplay { | |||||
| nvgBeginPath(args.vg); | nvgBeginPath(args.vg); | ||||
| nvgMoveTo(args.vg, p.x - 13, p.y); | nvgMoveTo(args.vg, p.x - 13, p.y); | ||||
| nvgLineTo(args.vg, 0, p.y); | nvgLineTo(args.vg, 0, p.y); | ||||
| nvgClosePath(args.vg); | |||||
| } | } | ||||
| nvgStroke(args.vg); | nvgStroke(args.vg); | ||||
| @@ -278,7 +354,7 @@ struct ScopeDisplay : LedDisplay { | |||||
| nvgResetScissor(args.vg); | 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> font = APP->window->loadFont(fontPath); | std::shared_ptr<Font> font = APP->window->loadFont(fontPath); | ||||
| if (!font) | if (!font) | ||||
| return; | return; | ||||
| @@ -294,16 +370,35 @@ struct ScopeDisplay : LedDisplay { | |||||
| std::string text; | std::string text; | ||||
| text = "pp "; | 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); | nvgText(args.vg, pos.x, pos.y, text.c_str(), NULL); | ||||
| text = "max "; | 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); | nvgText(args.vg, pos.x + 58 * 1, pos.y, text.c_str(), NULL); | ||||
| text = "min "; | 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); | 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 { | void drawLayer(const DrawArgs& args, int layer) override { | ||||
| if (layer != 1) | if (layer != 1) | ||||
| return; | return; | ||||
| @@ -316,41 +411,53 @@ struct ScopeDisplay : LedDisplay { | |||||
| float offsetX = module->params[Scope::X_POS_PARAM].getValue(); | float offsetX = module->params[Scope::X_POS_PARAM].getValue(); | ||||
| float offsetY = module->params[Scope::Y_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 | // Draw waveforms | ||||
| if (module->lissajous) { | |||||
| if (module->isLissajous()) { | |||||
| // X x Y | // 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++) { | 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 { | else { | ||||
| // Y | // Y | ||||
| for (int c = 0; c < module->channelsY; c++) { | 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 | // X | ||||
| for (int c = 0; c < module->channelsX; c++) { | 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(); | float trigThreshold = module->params[Scope::TRIG_PARAM].getValue(); | ||||
| trigThreshold = (trigThreshold + offsetX) * gainX; | trigThreshold = (trigThreshold + offsetX) * gainX; | ||||
| drawTrig(args, trigThreshold); | drawTrig(args, trigThreshold); | ||||
| // Background lines | |||||
| drawBackground(args); | |||||
| } | } | ||||
| // Calculate and draw stats | // Calculate and draw stats | ||||
| if (++statsFrame >= 4) { | if (++statsFrame >= 4) { | ||||
| statsFrame = 0; | 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<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | ||||
| addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); | ||||
| // addParam(createParamCentered<VCVButton>(mm2px(Vec(8.643, 80.603)), module, Scope::_1X2_PARAM)); | |||||
| addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(8.643, 80.603)), module, Scope::LISSAJOUS_PARAM, Scope::LISSAJOUS_LIGHT)); | |||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 80.551)), module, Scope::X_SCALE_PARAM)); | addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 80.551)), module, Scope::X_SCALE_PARAM)); | ||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 80.551)), module, Scope::Y_SCALE_PARAM)); | addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 80.551)), module, Scope::Y_SCALE_PARAM)); | ||||
| // addParam(createParamCentered<VCVButton>(mm2px(Vec(57.397, 80.521)), module, Scope::TRIG_PARAM)); | |||||
| addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(57.397, 80.521)), module, Scope::EXTERNAL_PARAM, Scope::EXTERNAL_LIGHT)); | |||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(8.643, 96.819)), module, Scope::TIME_PARAM)); | addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(8.643, 96.819)), module, Scope::TIME_PARAM)); | ||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 96.789)), module, Scope::X_POS_PARAM)); | addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 96.789)), module, Scope::X_POS_PARAM)); | ||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 96.815)), module, Scope::Y_POS_PARAM)); | addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 96.815)), module, Scope::Y_POS_PARAM)); | ||||
| // addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(57.397, 96.815)), module, Scope::THERS_PARAM)); | |||||
| addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(57.397, 96.815)), module, Scope::TRIG_PARAM)); | |||||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.643, 113.115)), module, Scope::X_INPUT)); | addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.643, 113.115)), module, Scope::X_INPUT)); | ||||
| addInput(createInputCentered<PJ301MPort>(mm2px(Vec(33.023, 113.115)), module, Scope::Y_INPUT)); | addInput(createInputCentered<PJ301MPort>(mm2px(Vec(33.023, 113.115)), module, Scope::Y_INPUT)); | ||||
| @@ -381,9 +488,10 @@ struct ScopeWidget : ModuleWidget { | |||||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(20.833, 113.115)), module, Scope::X_OUTPUT)); | addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(20.833, 113.115)), module, Scope::X_OUTPUT)); | ||||
| addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(45.212, 113.115)), module, Scope::Y_OUTPUT)); | addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(45.212, 113.115)), module, Scope::Y_OUTPUT)); | ||||
| ScopeDisplay* display = createWidget<ScopeDisplay>(Vec(0, 44)); | |||||
| display->box.size = Vec(box.size.x, 140); | |||||
| ScopeDisplay* display = createWidget<ScopeDisplay>(mm2px(Vec(0.0, 13.039))); | |||||
| display->box.size = mm2px(Vec(66.04, 55.88)); | |||||
| display->module = module; | display->module = module; | ||||
| display->moduleWidget = this; | |||||
| addChild(display); | addChild(display); | ||||
| } | } | ||||
| }; | }; | ||||