You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

535 lines
16KB

  1. #include <string.h>
  2. #include "plugin.hpp"
  3. static const int BUFFER_SIZE = 256;
  4. struct Scope : Module {
  5. enum ParamIds {
  6. X_SCALE_PARAM,
  7. X_POS_PARAM,
  8. Y_SCALE_PARAM,
  9. Y_POS_PARAM,
  10. TIME_PARAM,
  11. LISSAJOUS_PARAM,
  12. THRESH_PARAM,
  13. TRIG_PARAM,
  14. NUM_PARAMS
  15. };
  16. enum InputIds {
  17. X_INPUT,
  18. Y_INPUT,
  19. TRIG_INPUT,
  20. NUM_INPUTS
  21. };
  22. enum OutputIds {
  23. // new in 2.0
  24. X_OUTPUT,
  25. Y_OUTPUT,
  26. NUM_OUTPUTS
  27. };
  28. enum LightIds {
  29. LISSAJOUS_LIGHT,
  30. TRIG_LIGHT,
  31. NUM_LIGHTS
  32. };
  33. struct Point {
  34. float min = INFINITY;
  35. float max = -INFINITY;
  36. };
  37. Point pointBuffer[BUFFER_SIZE][2][PORT_MAX_CHANNELS];
  38. Point currentPoint[2][PORT_MAX_CHANNELS];
  39. int channelsX = 0;
  40. int channelsY = 0;
  41. int bufferIndex = 0;
  42. int frameIndex = 0;
  43. dsp::SchmittTrigger triggers[16];
  44. Scope() {
  45. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  46. configParam(X_SCALE_PARAM, 0.f, 8.f, 0.f, "Gain 1", " V/screen", 1 / 2.f, 20);
  47. getParamQuantity(X_SCALE_PARAM)->snapEnabled = true;
  48. configParam(X_POS_PARAM, -10.f, 10.f, 0.f, "Offset 1", " V");
  49. configParam(Y_SCALE_PARAM, 0.f, 8.f, 0.f, "Gain 2", " V/screen", 1 / 2.f, 20);
  50. getParamQuantity(Y_SCALE_PARAM)->snapEnabled = true;
  51. configParam(Y_POS_PARAM, -10.f, 10.f, 0.f, "Offset 2", " V");
  52. const float maxTime = -std::log2(5e1f);
  53. const float minTime = -std::log2(5e-3f);
  54. const float defaultTime = -std::log2(5e-1f);
  55. configParam(TIME_PARAM, maxTime, minTime, defaultTime, "Time", " ms/screen", 1 / 2.f, 1000);
  56. configSwitch(LISSAJOUS_PARAM, 0.f, 1.f, 0.f, "Scope mode", {"1 & 2", "1 x 2"});
  57. configParam(THRESH_PARAM, -10.f, 10.f, 0.f, "Trigger threshold", " V");
  58. configSwitch(TRIG_PARAM, 0.f, 1.f, 1.f, "Trigger", {"Enabled", "Disabled"});
  59. configInput(X_INPUT, "Ch 1");
  60. configInput(Y_INPUT, "Ch 2");
  61. configInput(TRIG_INPUT, "External trigger");
  62. configOutput(X_OUTPUT, "Ch 1");
  63. configOutput(Y_OUTPUT, "Ch 2");
  64. }
  65. void onReset() override {
  66. for (int i = 0; i < BUFFER_SIZE; i++) {
  67. for (int w = 0; w < 2; w++) {
  68. for (int c = 0; c < 16; c++) {
  69. pointBuffer[i][w][c] = Point();
  70. }
  71. }
  72. }
  73. }
  74. void process(const ProcessArgs& args) override {
  75. bool lissajous = params[LISSAJOUS_PARAM].getValue();
  76. lights[LISSAJOUS_LIGHT].setBrightness(lissajous);
  77. bool trig = !params[TRIG_PARAM].getValue();
  78. lights[TRIG_LIGHT].setBrightness(trig);
  79. // Detect trigger if no longer recording
  80. if (bufferIndex >= BUFFER_SIZE) {
  81. bool triggered = false;
  82. // Trigger immediately in Lissajous mode, or if trigger detection is disabled
  83. if (lissajous || !trig) {
  84. triggered = true;
  85. }
  86. else {
  87. // Reset if triggered
  88. float trigThreshold = params[THRESH_PARAM].getValue();
  89. Input& trigInput = inputs[TRIG_INPUT].isConnected() ? inputs[TRIG_INPUT] : inputs[X_INPUT];
  90. // This may be 0
  91. int trigChannels = trigInput.getChannels();
  92. for (int c = 0; c < trigChannels; c++) {
  93. float trigVoltage = trigInput.getVoltage(c);
  94. if (triggers[c].process(rescale(trigVoltage, trigThreshold, trigThreshold + 0.001f, 0.f, 1.f))) {
  95. triggered = true;
  96. }
  97. }
  98. }
  99. if (triggered) {
  100. for (int c = 0; c < 16; c++) {
  101. triggers[c].reset();
  102. }
  103. bufferIndex = 0;
  104. frameIndex = 0;
  105. }
  106. }
  107. // Set channels
  108. int channelsX = inputs[X_INPUT].getChannels();
  109. if (channelsX != this->channelsX) {
  110. // TODO
  111. // std::memset(bufferX, 0, sizeof(bufferX));
  112. this->channelsX = channelsX;
  113. }
  114. int channelsY = inputs[Y_INPUT].getChannels();
  115. if (channelsY != this->channelsY) {
  116. // TODO
  117. // std::memset(bufferY, 0, sizeof(bufferY));
  118. this->channelsY = channelsY;
  119. }
  120. // Copy inputs to outputs
  121. outputs[X_OUTPUT].setChannels(channelsX);
  122. outputs[X_OUTPUT].writeVoltages(inputs[X_INPUT].getVoltages());
  123. outputs[Y_OUTPUT].setChannels(channelsY);
  124. outputs[Y_OUTPUT].writeVoltages(inputs[Y_INPUT].getVoltages());
  125. // Add point to buffer if recording
  126. if (bufferIndex < BUFFER_SIZE) {
  127. // Compute time
  128. float deltaTime = dsp::exp2_taylor5(-params[TIME_PARAM].getValue()) / BUFFER_SIZE;
  129. int frameCount = (int) std::ceil(deltaTime * args.sampleRate);
  130. // Get input
  131. for (int c = 0; c < channelsX; c++) {
  132. float x = inputs[X_INPUT].getVoltage(c);
  133. currentPoint[0][c].min = std::min(currentPoint[0][c].min, x);
  134. currentPoint[0][c].max = std::max(currentPoint[0][c].max, x);
  135. }
  136. for (int c = 0; c < channelsY; c++) {
  137. float y = inputs[Y_INPUT].getVoltage(c);
  138. currentPoint[1][c].min = std::min(currentPoint[1][c].min, y);
  139. currentPoint[1][c].max = std::max(currentPoint[1][c].max, y);
  140. }
  141. if (++frameIndex >= frameCount) {
  142. frameIndex = 0;
  143. // Push current point
  144. for (int w = 0; w < 2; w++) {
  145. for (int c = 0; c < 16; c++) {
  146. pointBuffer[bufferIndex][w][c] = currentPoint[w][c];
  147. }
  148. }
  149. // Reset current point
  150. for (int w = 0; w < 2; w++) {
  151. for (int c = 0; c < 16; c++) {
  152. currentPoint[w][c] = Point();
  153. }
  154. }
  155. bufferIndex++;
  156. }
  157. }
  158. }
  159. bool isLissajous() {
  160. return params[LISSAJOUS_PARAM].getValue() > 0.f;
  161. }
  162. void dataFromJson(json_t* rootJ) override {
  163. // In <2.0, lissajous and external were class variables
  164. json_t* lissajousJ = json_object_get(rootJ, "lissajous");
  165. if (lissajousJ) {
  166. if (json_integer_value(lissajousJ))
  167. params[LISSAJOUS_PARAM].setValue(1.f);
  168. }
  169. json_t* externalJ = json_object_get(rootJ, "external");
  170. if (externalJ) {
  171. if (json_integer_value(externalJ))
  172. params[TRIG_PARAM].setValue(1.f);
  173. }
  174. }
  175. };
  176. Scope::Point DEMO_POINT_BUFFER[BUFFER_SIZE];
  177. void demoPointBufferInit() {
  178. static bool init = false;
  179. if (init)
  180. return;
  181. init = true;
  182. // Calculate demo point buffer
  183. for (size_t i = 0; i < BUFFER_SIZE; i++) {
  184. float phase = float(i) / BUFFER_SIZE;
  185. Scope::Point point;
  186. point.min = point.max = 4.f * std::sin(2 * M_PI * phase * 2.f);
  187. DEMO_POINT_BUFFER[i] = point;
  188. }
  189. }
  190. struct ScopeDisplay : LedDisplay {
  191. Scope* module;
  192. ModuleWidget* moduleWidget;
  193. int statsFrame = 0;
  194. std::string fontPath;
  195. struct Stats {
  196. float min = INFINITY;
  197. float max = -INFINITY;
  198. };
  199. Stats statsX;
  200. Stats statsY;
  201. ScopeDisplay() {
  202. fontPath = asset::system("res/fonts/ShareTechMono-Regular.ttf");
  203. demoPointBufferInit();
  204. }
  205. void calculateStats(Stats& stats, int wave, int channels) {
  206. if (!module) {
  207. stats.min = -5.f;
  208. stats.max = 5.f;
  209. return;
  210. }
  211. stats = Stats();
  212. for (int i = 0; i < BUFFER_SIZE; i++) {
  213. for (int c = 0; c < channels; c++) {
  214. Scope::Point point = module->pointBuffer[i][wave][c];
  215. stats.max = std::fmax(stats.max, point.max);
  216. stats.min = std::fmin(stats.min, point.min);
  217. }
  218. }
  219. }
  220. void drawWave(const DrawArgs& args, int wave, int channel, float offset, float gain) {
  221. Scope::Point pointBuffer[BUFFER_SIZE];
  222. for (int i = 0; i < BUFFER_SIZE; i++) {
  223. pointBuffer[i] = module ? module->pointBuffer[i][wave][channel] : DEMO_POINT_BUFFER[i];
  224. }
  225. nvgSave(args.vg);
  226. Rect b = box.zeroPos().shrink(Vec(0, 15));
  227. nvgScissor(args.vg, RECT_ARGS(b));
  228. nvgBeginPath(args.vg);
  229. // Draw max points on top
  230. for (int i = 0; i < BUFFER_SIZE; i++) {
  231. const Scope::Point& point = pointBuffer[i];
  232. float max = point.max;
  233. if (!std::isfinite(max))
  234. max = 0.f;
  235. Vec p;
  236. p.x = (float) i / (BUFFER_SIZE - 1);
  237. p.y = (max + offset) * gain * -0.5f + 0.5f;
  238. p = b.interpolate(p);
  239. p.y -= 1.0;
  240. if (i == 0)
  241. nvgMoveTo(args.vg, p.x, p.y);
  242. else
  243. nvgLineTo(args.vg, p.x, p.y);
  244. }
  245. // Draw min points on bottom
  246. for (int i = BUFFER_SIZE - 1; i >= 0; i--) {
  247. const Scope::Point& point = pointBuffer[i];
  248. float min = point.min;
  249. if (!std::isfinite(min))
  250. min = 0.f;
  251. Vec p;
  252. p.x = (float) i / (BUFFER_SIZE - 1);
  253. p.y = (min + offset) * gain * -0.5f + 0.5f;
  254. p = b.interpolate(p);
  255. p.y += 1.0;
  256. nvgLineTo(args.vg, p.x, p.y);
  257. }
  258. nvgClosePath(args.vg);
  259. // nvgLineCap(args.vg, NVG_ROUND);
  260. // nvgMiterLimit(args.vg, 2.f);
  261. nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER);
  262. nvgFill(args.vg);
  263. nvgResetScissor(args.vg);
  264. nvgRestore(args.vg);
  265. }
  266. void drawLissajous(const DrawArgs& args, int channel, float offsetX, float gainX, float offsetY, float gainY) {
  267. if (!module)
  268. return;
  269. Scope::Point pointBufferX[BUFFER_SIZE];
  270. Scope::Point pointBufferY[BUFFER_SIZE];
  271. for (int i = 0; i < BUFFER_SIZE; i++) {
  272. pointBufferX[i] = module->pointBuffer[i][0][channel];
  273. pointBufferY[i] = module->pointBuffer[i][1][channel];
  274. }
  275. nvgSave(args.vg);
  276. Rect b = box.zeroPos().shrink(Vec(0, 15));
  277. nvgScissor(args.vg, RECT_ARGS(b));
  278. nvgBeginPath(args.vg);
  279. int bufferIndex = module->bufferIndex;
  280. for (int i = 0; i < BUFFER_SIZE; i++) {
  281. // Get average point
  282. const Scope::Point& pointX = pointBufferX[(i + bufferIndex) % BUFFER_SIZE];
  283. const Scope::Point& pointY = pointBufferY[(i + bufferIndex) % BUFFER_SIZE];
  284. float avgX = (pointX.min + pointX.max) / 2;
  285. float avgY = (pointY.min + pointY.max) / 2;
  286. if (!std::isfinite(avgX) || !std::isfinite(avgY))
  287. continue;
  288. Vec p;
  289. p.x = (avgX + offsetX) * gainX * 0.5f + 0.5f;
  290. p.y = (avgY + offsetY) * gainY * -0.5f + 0.5f;
  291. p = b.interpolate(p);
  292. if (i == 0)
  293. nvgMoveTo(args.vg, p.x, p.y);
  294. else
  295. nvgLineTo(args.vg, p.x, p.y);
  296. }
  297. nvgLineCap(args.vg, NVG_ROUND);
  298. nvgMiterLimit(args.vg, 2.f);
  299. nvgStrokeWidth(args.vg, 1.5f);
  300. nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER);
  301. nvgStroke(args.vg);
  302. nvgResetScissor(args.vg);
  303. nvgRestore(args.vg);
  304. }
  305. void drawTrig(const DrawArgs& args, float value) {
  306. Rect b = Rect(Vec(0, 15), box.size.minus(Vec(0, 15 * 2)));
  307. nvgScissor(args.vg, b.pos.x, b.pos.y, b.size.x, b.size.y);
  308. value = value / 2.f + 0.5f;
  309. Vec p = Vec(box.size.x, b.pos.y + b.size.y * (1.f - value));
  310. // Draw line
  311. nvgStrokeColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x10));
  312. {
  313. nvgBeginPath(args.vg);
  314. nvgMoveTo(args.vg, p.x - 13, p.y);
  315. nvgLineTo(args.vg, 0, p.y);
  316. }
  317. nvgStroke(args.vg);
  318. // Draw indicator
  319. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x60));
  320. {
  321. nvgBeginPath(args.vg);
  322. nvgMoveTo(args.vg, p.x - 2, p.y - 4);
  323. nvgLineTo(args.vg, p.x - 9, p.y - 4);
  324. nvgLineTo(args.vg, p.x - 13, p.y);
  325. nvgLineTo(args.vg, p.x - 9, p.y + 4);
  326. nvgLineTo(args.vg, p.x - 2, p.y + 4);
  327. nvgClosePath(args.vg);
  328. }
  329. nvgFill(args.vg);
  330. std::shared_ptr<Font> font = APP->window->loadFont(fontPath);
  331. if (font) {
  332. nvgFontSize(args.vg, 9);
  333. nvgFontFaceId(args.vg, font->handle);
  334. nvgFillColor(args.vg, nvgRGBA(0x1e, 0x28, 0x2b, 0xff));
  335. nvgText(args.vg, p.x - 8, p.y + 3, "T", NULL);
  336. }
  337. nvgResetScissor(args.vg);
  338. }
  339. void drawStats(const DrawArgs& args, Vec pos, const char* title, const Stats& stats) {
  340. std::shared_ptr<Font> font = APP->window->loadFont(fontPath);
  341. if (!font)
  342. return;
  343. nvgFontSize(args.vg, 13);
  344. nvgFontFaceId(args.vg, font->handle);
  345. nvgTextLetterSpacing(args.vg, -2);
  346. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x40));
  347. nvgText(args.vg, pos.x + 6, pos.y + 11, title, NULL);
  348. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x80));
  349. pos = pos.plus(Vec(22, 11));
  350. std::string text;
  351. text = "pp ";
  352. float pp = stats.max - stats.min;
  353. text += isNear(pp, 0.f, 100.f) ? string::f("% 6.2f", pp) : " ---";
  354. nvgText(args.vg, pos.x, pos.y, text.c_str(), NULL);
  355. text = "max ";
  356. text += isNear(stats.max, 0.f, 100.f) ? string::f("% 6.2f", stats.max) : " ---";
  357. nvgText(args.vg, pos.x + 58 * 1, pos.y, text.c_str(), NULL);
  358. text = "min ";
  359. text += isNear(stats.min, 0.f, 100.f) ? string::f("% 6.2f", stats.min) : " ---";
  360. nvgText(args.vg, pos.x + 58 * 2, pos.y, text.c_str(), NULL);
  361. }
  362. void drawBackground(const DrawArgs& args) {
  363. Rect b = box.zeroPos().shrink(Vec(0, 15));
  364. nvgStrokeColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x10));
  365. for (int i = 0; i < 5; i++) {
  366. nvgBeginPath(args.vg);
  367. Vec p;
  368. p.x = 0.0;
  369. p.y = float(i) / (5 - 1);
  370. nvgMoveTo(args.vg, VEC_ARGS(b.interpolate(p)));
  371. p.x = 1.0;
  372. nvgLineTo(args.vg, VEC_ARGS(b.interpolate(p)));
  373. nvgStroke(args.vg);
  374. }
  375. }
  376. void drawLayer(const DrawArgs& args, int layer) override {
  377. if (layer != 1)
  378. return;
  379. // Background lines
  380. drawBackground(args);
  381. float gainX = module ? module->params[Scope::X_SCALE_PARAM].getValue() : 0.f;
  382. gainX = std::pow(2.f, std::round(gainX)) / 10.f;
  383. float gainY = module ? module->params[Scope::Y_SCALE_PARAM].getValue() : 0.f;
  384. gainY = std::pow(2.f, std::round(gainY)) / 10.f;
  385. float offsetX = module ? module->params[Scope::X_POS_PARAM].getValue() : 5.f;
  386. float offsetY = module ? module->params[Scope::Y_POS_PARAM].getValue() : -5.f;
  387. // Get input colors
  388. PortWidget* inputX = moduleWidget->getInput(Scope::X_INPUT);
  389. PortWidget* inputY = moduleWidget->getInput(Scope::Y_INPUT);
  390. CableWidget* inputXCable = APP->scene->rack->getTopCable(inputX);
  391. CableWidget* inputYCable = APP->scene->rack->getTopCable(inputY);
  392. NVGcolor inputXColor = inputXCable ? inputXCable->color : SCHEME_YELLOW;
  393. NVGcolor inputYColor = inputYCable ? inputYCable->color : SCHEME_YELLOW;
  394. // Draw waveforms
  395. int channelsY = module ? module->channelsY : 1;
  396. int channelsX = module ? module->channelsX : 1;
  397. if (module && module->isLissajous()) {
  398. // X x Y
  399. int lissajousChannels = std::min(channelsX, channelsY);
  400. for (int c = 0; c < lissajousChannels; c++) {
  401. nvgStrokeColor(args.vg, SCHEME_YELLOW);
  402. drawLissajous(args, c, offsetX, gainX, offsetY, gainY);
  403. }
  404. }
  405. else {
  406. // Y
  407. for (int c = 0; c < channelsY; c++) {
  408. nvgFillColor(args.vg, inputYColor);
  409. drawWave(args, 1, c, offsetY, gainY);
  410. }
  411. // X
  412. for (int c = 0; c < channelsX; c++) {
  413. nvgFillColor(args.vg, inputXColor);
  414. drawWave(args, 0, c, offsetX, gainX);
  415. }
  416. // Trigger
  417. float trigThreshold = module ? module->params[Scope::THRESH_PARAM].getValue() : 0.f;
  418. trigThreshold = (trigThreshold + offsetX) * gainX;
  419. drawTrig(args, trigThreshold);
  420. }
  421. // Calculate and draw stats
  422. if (statsFrame == 0) {
  423. calculateStats(statsX, 0, channelsX);
  424. calculateStats(statsY, 1, channelsY);
  425. }
  426. statsFrame = (statsFrame + 1) % 4;
  427. drawStats(args, Vec(0, 0 + 1), "1", statsX);
  428. drawStats(args, Vec(0, box.size.y - 15 - 1), "2", statsY);
  429. }
  430. };
  431. struct ScopeWidget : ModuleWidget {
  432. ScopeWidget(Scope* module) {
  433. setModule(module);
  434. setPanel(createPanel(asset::plugin(pluginInstance, "res/Scope.svg")));
  435. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
  436. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  437. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  438. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  439. addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(8.643, 80.603)), module, Scope::LISSAJOUS_PARAM, Scope::LISSAJOUS_LIGHT));
  440. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 80.551)), module, Scope::X_SCALE_PARAM));
  441. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 80.551)), module, Scope::Y_SCALE_PARAM));
  442. addParam(createLightParamCentered<VCVLightLatch<MediumSimpleLight<WhiteLight>>>(mm2px(Vec(57.397, 80.521)), module, Scope::TRIG_PARAM, Scope::TRIG_LIGHT));
  443. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(8.643, 96.819)), module, Scope::TIME_PARAM));
  444. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(24.897, 96.789)), module, Scope::X_POS_PARAM));
  445. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(41.147, 96.815)), module, Scope::Y_POS_PARAM));
  446. addParam(createParamCentered<RoundBlackKnob>(mm2px(Vec(57.397, 96.815)), module, Scope::THRESH_PARAM));
  447. addInput(createInputCentered<PJ301MPort>(mm2px(Vec(8.643, 113.115)), module, Scope::X_INPUT));
  448. addInput(createInputCentered<PJ301MPort>(mm2px(Vec(33.023, 113.115)), module, Scope::Y_INPUT));
  449. addInput(createInputCentered<PJ301MPort>(mm2px(Vec(57.397, 113.115)), module, Scope::TRIG_INPUT));
  450. addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(20.833, 113.115)), module, Scope::X_OUTPUT));
  451. addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(45.212, 113.115)), module, Scope::Y_OUTPUT));
  452. ScopeDisplay* display = createWidget<ScopeDisplay>(mm2px(Vec(0.0, 13.039)));
  453. display->box.size = mm2px(Vec(66.04, 55.88));
  454. display->module = module;
  455. display->moduleWidget = this;
  456. addChild(display);
  457. }
  458. };
  459. Model* modelScope = createModel<Scope, ScopeWidget>("Scope");