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.

388 lines
10KB

  1. //============================================================================================================
  2. //!
  3. //! \file Scope-G1.cpp
  4. //!
  5. //! \brief Scope-G1 is a...
  6. //!
  7. //============================================================================================================
  8. #include <string.h>
  9. #include "dsp/digital.hpp"
  10. #include "Gratrix.hpp"
  11. namespace rack_plugin_Gratrix {
  12. #define BUFFER_SIZE 512
  13. struct Scope : Module {
  14. enum ParamIds {
  15. X_SCALE_PARAM,
  16. X_POS_PARAM,
  17. TIME_PARAM,
  18. TRIG_PARAM,
  19. EXTERNAL_PARAM,
  20. DISP_PARAM,
  21. NUM_PARAMS
  22. };
  23. enum InputIds {
  24. X_INPUT, // N
  25. TRIG_INPUT, // N
  26. NUM_INPUTS
  27. };
  28. enum OutputIds {
  29. NUM_OUTPUTS
  30. };
  31. enum LightIds {
  32. NUM_LIGHTS
  33. };
  34. struct Voice {
  35. bool active = false;
  36. float bufferX[BUFFER_SIZE] = {};
  37. int bufferIndex = 0;
  38. float frameIndex = 0;
  39. SchmittTrigger resetTrigger;
  40. void step(bool external, int frameCount, const Param &trig_param, const Input &x_input, const Input &trig_input);
  41. };
  42. bool external = false;
  43. Voice voice[GTX__N+1];
  44. Scope() : Module(NUM_PARAMS, GTX__N * NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {}
  45. static constexpr std::size_t imap(std::size_t port, std::size_t bank)
  46. {
  47. return port + bank * NUM_INPUTS;
  48. }
  49. void step() override;
  50. };
  51. void Scope::step() {
  52. // Modes
  53. external = params[EXTERNAL_PARAM].value <= 0.0f;
  54. // Compute time
  55. float deltaTime = powf(2.0f, params[TIME_PARAM].value);
  56. int frameCount = (int)ceilf(deltaTime * engineGetSampleRate());
  57. Input x_sum, trig_sum;
  58. int count = 0;
  59. for (int k=0; k<GTX__N; ++k)
  60. {
  61. voice[k].step(external, frameCount, params[TRIG_PARAM], inputs[imap(X_INPUT, k)], inputs[imap(TRIG_INPUT, k)]);
  62. if (inputs[imap(X_INPUT, k)].active)
  63. {
  64. x_sum.active = true;
  65. x_sum.value += inputs[imap(X_INPUT, k)].value;
  66. count++;
  67. }
  68. if (inputs[imap(TRIG_INPUT, k)].active) // GTX TODO - may need better logic here
  69. {
  70. trig_sum.active = true;
  71. trig_sum.value += inputs[imap(TRIG_INPUT, k)].value;
  72. }
  73. }
  74. if (count > 0)
  75. {
  76. x_sum.value /= static_cast<float>(count);
  77. }
  78. voice[GTX__N].step(external, frameCount, params[TRIG_PARAM], x_sum, trig_sum);
  79. }
  80. void Scope::Voice::step(bool external, int frameCount, const Param &trig_param, const Input &x_input, const Input &trig_input) {
  81. // Copy active state
  82. active = x_input.active;
  83. // Add frame to buffer
  84. if (bufferIndex < BUFFER_SIZE) {
  85. if (++frameIndex > frameCount) {
  86. frameIndex = 0;
  87. bufferX[bufferIndex] = x_input.value;
  88. bufferIndex++;
  89. }
  90. }
  91. // Are we waiting on the next trigger?
  92. if (bufferIndex >= BUFFER_SIZE) {
  93. // Trigger immediately if external but nothing plugged in
  94. if (external && !trig_input.active) {
  95. bufferIndex = 0;
  96. frameIndex = 0;
  97. return;
  98. }
  99. // Reset the Schmitt trigger so we don't trigger immediately if the input is high
  100. if (frameIndex == 0) {
  101. resetTrigger.reset();
  102. }
  103. frameIndex++;
  104. // Must go below 0.1fV to trigger
  105. float gate = external ? trig_input.value : x_input.value;
  106. // Reset if triggered
  107. float holdTime = 0.1f;
  108. if (resetTrigger.process(rescale(gate, trig_param.value - 0.1f, trig_param.value, 0.f, 1.f)) || (frameIndex >= engineGetSampleRate() * holdTime)) {
  109. bufferIndex = 0; frameIndex = 0; return;
  110. }
  111. // Reset if we've waited too long
  112. if (frameIndex >= engineGetSampleRate() * holdTime) {
  113. bufferIndex = 0; frameIndex = 0; return;
  114. }
  115. }
  116. }
  117. struct Display : TransparentWidget {
  118. Scope *module;
  119. int frame = 0;
  120. std::shared_ptr<Font> font;
  121. struct Stats {
  122. float vrms, vpp, vmin, vmax;
  123. void calculate(float *values) {
  124. vrms = 0.0f;
  125. vmax = -INFINITY;
  126. vmin = INFINITY;
  127. for (int i = 0; i < BUFFER_SIZE; i++) {
  128. float v = values[i];
  129. vrms += v*v;
  130. vmax = fmaxf(vmax, v);
  131. vmin = fminf(vmin, v);
  132. }
  133. vrms = sqrtf(vrms / BUFFER_SIZE);
  134. vpp = vmax - vmin;
  135. }
  136. };
  137. Stats statsX[GTX__N + 1];
  138. Display() {
  139. font = Font::load(assetPlugin(plugin, "res/fonts/Sudo.ttf"));
  140. }
  141. void drawWaveform(NVGcontext *vg, float *valuesX, const Rect &b) {
  142. if (!valuesX)
  143. return;
  144. nvgSave(vg);
  145. nvgScissor(vg, b.pos.x, b.pos.y, b.size.x, b.size.y);
  146. nvgBeginPath(vg);
  147. // Draw maximum display left to right
  148. for (int i = 0; i < BUFFER_SIZE; i++) {
  149. float x = (float)i / (BUFFER_SIZE - 1);
  150. float y = valuesX[i] / 2.0f + 0.5f;
  151. Vec p;
  152. p.x = b.pos.x + b.size.x * x;
  153. p.y = b.pos.y + b.size.y * (1.0f - y);
  154. if (i == 0)
  155. nvgMoveTo(vg, p.x, p.y);
  156. else
  157. nvgLineTo(vg, p.x, p.y);
  158. }
  159. nvgLineCap(vg, NVG_ROUND);
  160. nvgMiterLimit(vg, 2.0);
  161. nvgStrokeWidth(vg, 1.5);
  162. nvgGlobalCompositeOperation(vg, NVG_LIGHTER);
  163. nvgStroke(vg);
  164. nvgResetScissor(vg);
  165. nvgRestore(vg);
  166. }
  167. void drawTrig(NVGcontext *vg, float value, const Rect &b) {
  168. nvgScissor(vg, b.pos.x, b.pos.y, b.size.x, b.size.y);
  169. value = value / 2.0f + 0.5f;
  170. Vec p = Vec(b.pos.x + b.size.x, b.pos.y + b.size.y * (1.0f - value));
  171. // Draw line
  172. nvgStrokeColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x10));
  173. {
  174. nvgBeginPath(vg);
  175. nvgMoveTo(vg, p.x - 13, p.y);
  176. nvgLineTo(vg, 0, p.y);
  177. nvgClosePath(vg);
  178. }
  179. nvgStroke(vg);
  180. // Draw indicator
  181. nvgFillColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x60));
  182. {
  183. nvgBeginPath(vg);
  184. nvgMoveTo(vg, p.x - 2, p.y - 4);
  185. nvgLineTo(vg, p.x - 9, p.y - 4);
  186. nvgLineTo(vg, p.x - 13, p.y);
  187. nvgLineTo(vg, p.x - 9, p.y + 4);
  188. nvgLineTo(vg, p.x - 2, p.y + 4);
  189. nvgClosePath(vg);
  190. }
  191. nvgFill(vg);
  192. nvgFontSize(vg, 9);
  193. nvgFontFaceId(vg, font->handle);
  194. nvgFillColor(vg, nvgRGBA(0x1e, 0x28, 0x2b, 0xff));
  195. nvgText(vg, p.x - 8, p.y + 3, "T", NULL);
  196. nvgResetScissor(vg);
  197. }
  198. void drawStats(NVGcontext *vg, Vec pos, const char *title, const Stats &stats) {
  199. nvgFontSize(vg, 13);
  200. nvgFontFaceId(vg, font->handle);
  201. nvgTextLetterSpacing(vg, -2);
  202. nvgFillColor(vg, nvgRGBA(0xff, 0xff, 0xff, 0x80));
  203. char text[128];
  204. snprintf(text, sizeof(text), "%s. %4.1f [%+5.1f %+5.1f]", title, stats.vpp, stats.vmin, stats.vmax);
  205. nvgText(vg, pos.x + 6, pos.y + 11, text, NULL);
  206. }
  207. void draw(NVGcontext *vg) override {
  208. float gainX = powf(2.0, roundf(module->params[Scope::X_SCALE_PARAM].value));
  209. float offsetX = module->params[Scope::X_POS_PARAM].value;
  210. int disp = static_cast<int>(module->params[Scope::DISP_PARAM].value + 0.5f);
  211. int k0, k1, kM;
  212. if (disp == 0 ) { k0 = 0; k1 = GTX__N; kM = 0; } // six up
  213. else if (disp == GTX__N+1) { k0 = GTX__N; k1 = GTX__N+1; kM = 100; } // sum
  214. else { k0 = disp-1; k1 = disp ; kM = 100; } // individual
  215. static const char *stats_lab[GTX__N+1] = {"1", "2", "3", "4", "5", "6", "SUM"};
  216. for (int k=k0; k<k1; ++k)
  217. {
  218. Rect a = Rect(Vec(0, 15), box.size.minus(Vec(0, 15*2)));
  219. Rect b = a;
  220. // GTX TODO - imporve
  221. if (kM < GTX__N)
  222. {
  223. b.size.x /= (GTX__N/2);
  224. b.size.y /= (GTX__N/3);
  225. b.pos.x += (k%3) * b.size.x;
  226. b.pos.y += (k/3) * b.size.y;
  227. }
  228. float valuesX[BUFFER_SIZE];
  229. for (int i = 0; i < BUFFER_SIZE; i++) {
  230. valuesX[i] = (module->voice[k].bufferX[i] + offsetX) * gainX / 10.0;
  231. }
  232. // Draw waveforms
  233. if (module->voice[k].active) {
  234. if (k&1) nvgStrokeColor(vg, nvgRGBA(0xe1, 0x02, 0x78, 0xc0));
  235. else nvgStrokeColor(vg, nvgRGBA(0x28, 0xb0, 0xf3, 0xc0));
  236. drawWaveform(vg, valuesX, b);
  237. }
  238. float valueTrig = (module->params[Scope::TRIG_PARAM].value + offsetX) * gainX / 10.0;
  239. drawTrig(vg, valueTrig, b);
  240. // Calculate and draw stats
  241. if (frame == 0) {
  242. statsX[k].calculate(module->voice[k].bufferX);
  243. }
  244. Vec stats_pos = b.pos;
  245. if (k >= 3 && k < GTX__N)
  246. {
  247. stats_pos.y += b.size.y;
  248. }
  249. else
  250. {
  251. stats_pos.y -= 15;
  252. }
  253. drawStats(vg, stats_pos, stats_lab[k], statsX[k]);
  254. }
  255. if (++frame >= 4)
  256. {
  257. frame = 0;
  258. }
  259. }
  260. };
  261. //============================================================================================================
  262. //! \brief The widget.
  263. struct GtxWidget_Scope_G1 : ModuleWidget
  264. {
  265. GtxWidget_Scope_G1(Scope *module) : ModuleWidget(module)
  266. {
  267. GTX__WIDGET();
  268. box.size = Vec(24*15, 380);
  269. auto screen_pos = Vec(0, 35);
  270. auto screen_size = Vec(box.size.x, 220);
  271. #if GTX__SAVE_SVG
  272. {
  273. PanelGen pg(assetPlugin(plugin, "build/res/Scope-G1.svg"), box.size, "SCOPE-G1");
  274. pg.rect(screen_pos, screen_size, "fill:#222222;stroke:none");
  275. pg.bus_in(0, 2, "IN");
  276. pg.bus_in(2, 2, "EXT TRIG");
  277. pg.nob_sml_raw(gx(1-0.22), gy(2-0.24), "SCALE");
  278. pg.nob_sml_raw(gx(1-0.22), gy(2+0.22), "POS");
  279. pg.nob_sml_raw(gx(1+0.22), gy(2-0.24), "TIME");
  280. pg.nob_sml_raw(gx(1+0.22), gy(2+0.22), "TRIG");
  281. pg.tog_raw (gx(3-0.22), gy(2-0.24), "INT", "EXT");
  282. pg.nob_sml_raw(gx(3+0.22), gy(2-0.24), "DISP");
  283. for (std::size_t i=0; i<=12; i++)
  284. {
  285. float x = screen_pos.x + screen_size.x * i / 12.0f;
  286. pg.line(Vec(x, screen_pos.y + 15), Vec(x, screen_pos.y + screen_size.y - 15), "fill:none;stroke:#666666;stroke-width:1");
  287. }
  288. for (std::size_t i=0; i<=8; i++)
  289. {
  290. float y = screen_pos.y + 15 + (screen_size.y - 30) * i / 8.0f;
  291. pg.line(Vec(screen_pos.x, y), Vec(screen_pos.x + screen_size.x, y), "fill:none;stroke:#666666;stroke-width:1");
  292. }
  293. }
  294. #endif
  295. setPanel(SVG::load(assetPlugin(plugin, "res/Scope-G1.svg")));
  296. {
  297. Display *display = new Display();
  298. display->module = module;
  299. display->box.pos = screen_pos;
  300. display->box.size = screen_size;
  301. addChild(display);
  302. }
  303. addParam(createParamGTX<KnobSnapSml>(Vec(gx(1-0.22), gy(2-0.24)), module, Scope::X_SCALE_PARAM, -2.0f, 8.0f, 0.0f));
  304. addParam(createParamGTX<KnobFreeSml>(Vec(gx(1-0.22), gy(2+0.22)), module, Scope::X_POS_PARAM, -10.0f, 10.0f, 0.0f));
  305. addParam(createParamGTX<KnobFreeSml>(Vec(gx(1+0.22), gy(2-0.24)), module, Scope::TIME_PARAM, -6.0f, -16.0f, -14.0f));
  306. addParam(createParamGTX<KnobFreeSml>(Vec(gx(1+0.22), gy(2+0.22)), module, Scope::TRIG_PARAM, -10.0f, 10.0f, 0.0f));
  307. addParam(ParamWidget::create<CKSS> (tog(gx(3-0.22), gy(2-0.24)), module, Scope::EXTERNAL_PARAM, 0.0f, 1.0f, 1.0f));
  308. addParam(createParamGTX<KnobSnapSml>(Vec(gx(3+0.22), gy(2-0.24)), module, Scope::DISP_PARAM, 0.0f, GTX__N+1, 0.0f));
  309. for (std::size_t i=0; i<GTX__N; ++i)
  310. {
  311. addInput(createInputGTX<PortInMed>(Vec(px(0, i), py(2, i)), module, Scope::imap(Scope::X_INPUT, i)));
  312. addInput(createInputGTX<PortInMed>(Vec(px(2, i), py(2, i)), module, Scope::imap(Scope::TRIG_INPUT, i)));
  313. }
  314. }
  315. };
  316. } // namespace rack_plugin_Gratrix
  317. using namespace rack_plugin_Gratrix;
  318. RACK_PLUGIN_MODEL_INIT(Gratrix, Scope_G1) {
  319. Model *model = Model::create<Scope, GtxWidget_Scope_G1>("Gratrix", "Scope-G1", "Scope-G1", VISUAL_TAG);
  320. return model;
  321. }