|
@@ -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); |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|