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.

391 lines
12KB

  1. #include <string.h>
  2. #include "plugin.hpp"
  3. static const int BUFFER_SIZE = 512;
  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. TRIG_PARAM,
  13. EXTERNAL_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. NUM_OUTPUTS
  24. };
  25. enum LightIds {
  26. PLOT_LIGHT,
  27. LISSAJOUS_LIGHT,
  28. INTERNAL_LIGHT,
  29. EXTERNAL_LIGHT,
  30. NUM_LIGHTS
  31. };
  32. float bufferX[16][BUFFER_SIZE] = {};
  33. float bufferY[16][BUFFER_SIZE] = {};
  34. int channelsX = 0;
  35. int channelsY = 0;
  36. int bufferIndex = 0;
  37. int frameIndex = 0;
  38. dsp::BooleanTrigger sumTrigger;
  39. dsp::BooleanTrigger extTrigger;
  40. bool lissajous = false;
  41. bool external = false;
  42. dsp::SchmittTrigger triggers[16];
  43. Scope() {
  44. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  45. configParam(X_SCALE_PARAM, -2.f, 8.f, 0.f, "X scale", " V/div", 1 / 2.f, 5);
  46. configParam(X_POS_PARAM, -10.f, 10.f, 0.f, "X position", " V");
  47. configParam(Y_SCALE_PARAM, -2.f, 8.f, 0.f, "Y scale", " V/div", 1 / 2.f, 5);
  48. configParam(Y_POS_PARAM, -10.f, 10.f, 0.f, "Y position", " V");
  49. const float timeBase = (float) BUFFER_SIZE / 6;
  50. configParam(TIME_PARAM, 6.f, 16.f, 14.f, "Time", " ms/div", 1 / 2.f, 1000 * timeBase);
  51. configButton(LISSAJOUS_PARAM, "Separate/Lissajous mode");
  52. configParam(TRIG_PARAM, -10.f, 10.f, 0.f, "Trigger position", " V");
  53. configButton(EXTERNAL_PARAM, "Internal/external trigger mode");
  54. configInput(X_INPUT, "X");
  55. configInput(Y_INPUT, "Y");
  56. configInput(TRIG_INPUT, "External trigger");
  57. }
  58. void onReset() override {
  59. lissajous = false;
  60. external = false;
  61. std::memset(bufferX, 0, sizeof(bufferX));
  62. std::memset(bufferY, 0, sizeof(bufferY));
  63. }
  64. void process(const ProcessArgs& args) override {
  65. // Modes
  66. if (sumTrigger.process(params[LISSAJOUS_PARAM].getValue() > 0.f)) {
  67. lissajous = !lissajous;
  68. }
  69. lights[PLOT_LIGHT].setBrightness(!lissajous);
  70. lights[LISSAJOUS_LIGHT].setBrightness(lissajous);
  71. if (extTrigger.process(params[EXTERNAL_PARAM].getValue() > 0.f)) {
  72. external = !external;
  73. }
  74. lights[INTERNAL_LIGHT].setBrightness(!external);
  75. lights[EXTERNAL_LIGHT].setBrightness(external);
  76. // Compute time
  77. float deltaTime = std::pow(2.f, -params[TIME_PARAM].getValue());
  78. int frameCount = (int) std::ceil(deltaTime * args.sampleRate);
  79. // Set channels
  80. int channelsX = inputs[X_INPUT].getChannels();
  81. if (channelsX != this->channelsX) {
  82. std::memset(bufferX, 0, sizeof(bufferX));
  83. this->channelsX = channelsX;
  84. }
  85. int channelsY = inputs[Y_INPUT].getChannels();
  86. if (channelsY != this->channelsY) {
  87. std::memset(bufferY, 0, sizeof(bufferY));
  88. this->channelsY = channelsY;
  89. }
  90. // Add frame to buffer
  91. if (bufferIndex < BUFFER_SIZE) {
  92. if (++frameIndex > frameCount) {
  93. frameIndex = 0;
  94. for (int c = 0; c < channelsX; c++) {
  95. bufferX[c][bufferIndex] = inputs[X_INPUT].getVoltage(c);
  96. }
  97. for (int c = 0; c < channelsY; c++) {
  98. bufferY[c][bufferIndex] = inputs[Y_INPUT].getVoltage(c);
  99. }
  100. bufferIndex++;
  101. }
  102. }
  103. // Don't wait for trigger if still filling buffer
  104. if (bufferIndex < BUFFER_SIZE) {
  105. return;
  106. }
  107. // Trigger immediately if external but nothing plugged in, or in Lissajous mode
  108. if (lissajous || (external && !inputs[TRIG_INPUT].isConnected())) {
  109. trigger();
  110. return;
  111. }
  112. frameIndex++;
  113. // Reset if triggered
  114. float trigThreshold = params[TRIG_PARAM].getValue();
  115. Input& trigInput = external ? inputs[TRIG_INPUT] : inputs[X_INPUT];
  116. // This may be 0
  117. int trigChannels = trigInput.getChannels();
  118. for (int c = 0; c < trigChannels; c++) {
  119. float trigVoltage = trigInput.getVoltage(c);
  120. if (triggers[c].process(rescale(trigVoltage, trigThreshold, trigThreshold + 0.001f, 0.f, 1.f))) {
  121. trigger();
  122. return;
  123. }
  124. }
  125. // Reset if we've been waiting for `holdTime`
  126. const float holdTime = 0.5f;
  127. if (frameIndex * args.sampleTime >= holdTime) {
  128. trigger();
  129. return;
  130. }
  131. }
  132. void trigger() {
  133. for (int c = 0; c < 16; c++) {
  134. triggers[c].reset();
  135. }
  136. bufferIndex = 0;
  137. frameIndex = 0;
  138. }
  139. json_t* dataToJson() override {
  140. json_t* rootJ = json_object();
  141. json_object_set_new(rootJ, "lissajous", json_integer((int) lissajous));
  142. json_object_set_new(rootJ, "external", json_integer((int) external));
  143. return rootJ;
  144. }
  145. void dataFromJson(json_t* rootJ) override {
  146. json_t* sumJ = json_object_get(rootJ, "lissajous");
  147. if (sumJ)
  148. lissajous = json_integer_value(sumJ);
  149. json_t* extJ = json_object_get(rootJ, "external");
  150. if (extJ)
  151. external = json_integer_value(extJ);
  152. }
  153. };
  154. struct ScopeDisplay : TransparentWidget {
  155. Scope* module;
  156. int statsFrame = 0;
  157. std::string fontPath;
  158. struct Stats {
  159. float vpp = 0.f;
  160. float vmin = 0.f;
  161. float vmax = 0.f;
  162. void calculate(float* buffer, int channels) {
  163. vmax = -INFINITY;
  164. vmin = INFINITY;
  165. for (int i = 0; i < BUFFER_SIZE * channels; i++) {
  166. float v = buffer[i];
  167. vmax = std::fmax(vmax, v);
  168. vmin = std::fmin(vmin, v);
  169. }
  170. vpp = vmax - vmin;
  171. }
  172. };
  173. Stats statsX, statsY;
  174. ScopeDisplay() {
  175. fontPath = asset::plugin(pluginInstance, "res/sudo/Sudo.ttf");
  176. }
  177. void drawWaveform(const DrawArgs& args, float* bufferX, float offsetX, float gainX, float* bufferY, float offsetY, float gainY) {
  178. assert(bufferY);
  179. nvgSave(args.vg);
  180. Rect b = Rect(Vec(0, 15), box.size.minus(Vec(0, 15 * 2)));
  181. nvgScissor(args.vg, b.pos.x, b.pos.y, b.size.x, b.size.y);
  182. nvgBeginPath(args.vg);
  183. for (int i = 0; i < BUFFER_SIZE; i++) {
  184. Vec v;
  185. if (bufferX)
  186. v.x = (bufferX[i] + offsetX) * gainX / 2.f + 0.5f;
  187. else
  188. v.x = (float) i / (BUFFER_SIZE - 1);
  189. v.y = (bufferY[i] + offsetY) * gainY / 2.f + 0.5f;
  190. Vec p;
  191. p.x = rescale(v.x, 0.f, 1.f, b.pos.x, b.pos.x + b.size.x);
  192. p.y = rescale(v.y, 0.f, 1.f, b.pos.y + b.size.y, b.pos.y);
  193. if (i == 0)
  194. nvgMoveTo(args.vg, p.x, p.y);
  195. else
  196. nvgLineTo(args.vg, p.x, p.y);
  197. }
  198. nvgLineCap(args.vg, NVG_ROUND);
  199. nvgMiterLimit(args.vg, 2.f);
  200. nvgStrokeWidth(args.vg, 1.5f);
  201. nvgGlobalCompositeOperation(args.vg, NVG_LIGHTER);
  202. nvgStroke(args.vg);
  203. nvgResetScissor(args.vg);
  204. nvgRestore(args.vg);
  205. }
  206. void drawTrig(const DrawArgs& args, float value) {
  207. Rect b = Rect(Vec(0, 15), box.size.minus(Vec(0, 15 * 2)));
  208. nvgScissor(args.vg, b.pos.x, b.pos.y, b.size.x, b.size.y);
  209. value = value / 2.f + 0.5f;
  210. Vec p = Vec(box.size.x, b.pos.y + b.size.y * (1.f - value));
  211. // Draw line
  212. nvgStrokeColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x10));
  213. {
  214. nvgBeginPath(args.vg);
  215. nvgMoveTo(args.vg, p.x - 13, p.y);
  216. nvgLineTo(args.vg, 0, p.y);
  217. nvgClosePath(args.vg);
  218. }
  219. nvgStroke(args.vg);
  220. // Draw indicator
  221. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x60));
  222. {
  223. nvgBeginPath(args.vg);
  224. nvgMoveTo(args.vg, p.x - 2, p.y - 4);
  225. nvgLineTo(args.vg, p.x - 9, p.y - 4);
  226. nvgLineTo(args.vg, p.x - 13, p.y);
  227. nvgLineTo(args.vg, p.x - 9, p.y + 4);
  228. nvgLineTo(args.vg, p.x - 2, p.y + 4);
  229. nvgClosePath(args.vg);
  230. }
  231. nvgFill(args.vg);
  232. std::shared_ptr<Font> font = APP->window->loadFont(fontPath);
  233. nvgFontSize(args.vg, 9);
  234. nvgFontFaceId(args.vg, font->handle);
  235. nvgFillColor(args.vg, nvgRGBA(0x1e, 0x28, 0x2b, 0xff));
  236. nvgText(args.vg, p.x - 8, p.y + 3, "T", NULL);
  237. nvgResetScissor(args.vg);
  238. }
  239. void drawStats(const DrawArgs& args, Vec pos, const char* title, Stats* stats) {
  240. std::shared_ptr<Font> font = APP->window->loadFont(fontPath);
  241. nvgFontSize(args.vg, 13);
  242. nvgFontFaceId(args.vg, font->handle);
  243. nvgTextLetterSpacing(args.vg, -2);
  244. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x40));
  245. nvgText(args.vg, pos.x + 6, pos.y + 11, title, NULL);
  246. nvgFillColor(args.vg, nvgRGBA(0xff, 0xff, 0xff, 0x80));
  247. pos = pos.plus(Vec(22, 11));
  248. std::string text;
  249. text = "pp ";
  250. text += isNear(stats->vpp, 0.f, 100.f) ? string::f("% 6.2f", stats->vpp) : " ---";
  251. nvgText(args.vg, pos.x, pos.y, text.c_str(), NULL);
  252. text = "max ";
  253. text += isNear(stats->vmax, 0.f, 100.f) ? string::f("% 6.2f", stats->vmax) : " ---";
  254. nvgText(args.vg, pos.x + 58 * 1, pos.y, text.c_str(), NULL);
  255. text = "min ";
  256. text += isNear(stats->vmin, 0.f, 100.f) ? string::f("% 6.2f", stats->vmin) : " ---";
  257. nvgText(args.vg, pos.x + 58 * 2, pos.y, text.c_str(), NULL);
  258. }
  259. void draw(const DrawArgs& args) override {
  260. if (!module)
  261. return;
  262. // Disable tinting when rack brightness is decreased
  263. nvgGlobalTint(args.vg, color::WHITE);
  264. float gainX = std::pow(2.f, std::round(module->params[Scope::X_SCALE_PARAM].getValue())) / 10.f;
  265. float gainY = std::pow(2.f, std::round(module->params[Scope::Y_SCALE_PARAM].getValue())) / 10.f;
  266. float offsetX = module->params[Scope::X_POS_PARAM].getValue();
  267. float offsetY = module->params[Scope::Y_POS_PARAM].getValue();
  268. // Draw waveforms
  269. if (module->lissajous) {
  270. // X x Y
  271. int lissajousChannels = std::max(module->channelsX, module->channelsY);
  272. for (int c = 0; c < lissajousChannels; c++) {
  273. nvgStrokeColor(args.vg, nvgRGBA(0x9f, 0xe4, 0x36, 0xc0));
  274. drawWaveform(args, module->bufferX[c], offsetX, gainX, module->bufferY[c], offsetY, gainY);
  275. }
  276. }
  277. else {
  278. // Y
  279. for (int c = 0; c < module->channelsY; c++) {
  280. nvgStrokeColor(args.vg, nvgRGBA(0xe1, 0x02, 0x78, 0xc0));
  281. drawWaveform(args, NULL, 0, 0, module->bufferY[c], offsetY, gainY);
  282. }
  283. // X
  284. for (int c = 0; c < module->channelsX; c++) {
  285. nvgStrokeColor(args.vg, nvgRGBA(0x28, 0xb0, 0xf3, 0xc0));
  286. drawWaveform(args, NULL, 0, 0, module->bufferX[c], offsetX, gainX);
  287. }
  288. float trigThreshold = module->params[Scope::TRIG_PARAM].getValue();
  289. trigThreshold = (trigThreshold + offsetX) * gainX;
  290. drawTrig(args, trigThreshold);
  291. }
  292. // Calculate and draw stats
  293. if (++statsFrame >= 4) {
  294. statsFrame = 0;
  295. statsX.calculate(module->bufferX[0], module->channelsX);
  296. statsY.calculate(module->bufferY[0], module->channelsY);
  297. }
  298. drawStats(args, Vec(0, 0), "X", &statsX);
  299. drawStats(args, Vec(0, box.size.y - 15), "Y", &statsY);
  300. }
  301. };
  302. struct ScopeWidget : ModuleWidget {
  303. ScopeWidget(Scope* module) {
  304. setModule(module);
  305. setPanel(createPanel(asset::plugin(pluginInstance, "res/Scope.svg")));
  306. addChild(createWidget<ScrewSilver>(Vec(15, 0)));
  307. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 0)));
  308. addChild(createWidget<ScrewSilver>(Vec(15, 365)));
  309. addChild(createWidget<ScrewSilver>(Vec(box.size.x - 30, 365)));
  310. {
  311. ScopeDisplay* display = new ScopeDisplay();
  312. display->module = module;
  313. display->box.pos = Vec(0, 44);
  314. display->box.size = Vec(box.size.x, 140);
  315. addChild(display);
  316. }
  317. addParam(createParam<RoundBlackSnapKnob>(Vec(15, 209), module, Scope::X_SCALE_PARAM));
  318. addParam(createParam<RoundBlackKnob>(Vec(15, 263), module, Scope::X_POS_PARAM));
  319. addParam(createParam<RoundBlackSnapKnob>(Vec(61, 209), module, Scope::Y_SCALE_PARAM));
  320. addParam(createParam<RoundBlackKnob>(Vec(61, 263), module, Scope::Y_POS_PARAM));
  321. addParam(createParam<RoundBlackKnob>(Vec(107, 209), module, Scope::TIME_PARAM));
  322. addParam(createParam<CKD6>(Vec(106, 262), module, Scope::LISSAJOUS_PARAM));
  323. addParam(createParam<RoundBlackKnob>(Vec(153, 209), module, Scope::TRIG_PARAM));
  324. addParam(createParam<CKD6>(Vec(152, 262), module, Scope::EXTERNAL_PARAM));
  325. addInput(createInput<PJ301MPort>(Vec(17, 319), module, Scope::X_INPUT));
  326. addInput(createInput<PJ301MPort>(Vec(63, 319), module, Scope::Y_INPUT));
  327. addInput(createInput<PJ301MPort>(Vec(154, 319), module, Scope::TRIG_INPUT));
  328. addChild(createLight<SmallLight<GreenLight>>(Vec(104, 251), module, Scope::PLOT_LIGHT));
  329. addChild(createLight<SmallLight<GreenLight>>(Vec(104, 296), module, Scope::LISSAJOUS_LIGHT));
  330. addChild(createLight<SmallLight<GreenLight>>(Vec(150, 251), module, Scope::INTERNAL_LIGHT));
  331. addChild(createLight<SmallLight<GreenLight>>(Vec(150, 296), module, Scope::EXTERNAL_LIGHT));
  332. }
  333. };
  334. Model* modelScope = createModel<Scope, ScopeWidget>("Scope");