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.

329 lines
11KB

  1. #include <stdio.h>
  2. #include <sstream>
  3. #include <iomanip>
  4. #include "alikins.hpp"
  5. #include "dsp/digital.hpp"
  6. // #include "util.hpp"
  7. namespace rack_plugin_Alikins {
  8. /* IdleSwitch
  9. *
  10. * What:
  11. *
  12. * If no input events are seen at Input Source within the timeout period
  13. * emit a gate on Idle Gate Output that lasts until there are input events
  14. * again. Then reset the timeout period.
  15. *
  16. * Sort of metaphoricaly like an idle handler or timeout in event based
  17. * programming like GUI main loops.
  18. *
  19. * The timeout period is set by the value
  20. * of the 'Time before idle' param.
  21. *
  22. * If there is a 'Reset idle' source, when it gets an event, the timeout period
  23. * is reset. After a reset event, the Idle Gate Output will remain on until
  24. * an input event is seen at Input Source. When there is an input event, the Idle
  25. * Gate Output is turned off until the expiration of the 'Time before idle' or
  26. * the next 'Reset idle'.
  27. *
  28. * To use the eventloop/gui main loop analogy, a 'Reset idle' event is equilivent to
  29. * running an idle handler directly (or running a mainloop iteration with no non-idle
  30. * events pending).
  31. *
  32. * Why:
  33. *
  34. * Original intentional was to use in combo with a human player and midi/cv keyboard.
  35. * As long as the human is playing, the IdleSwitch output is 'off', but if they go
  36. * idle for some time period the output is turned on. For example, a patch may plain
  37. * loud drone when idle, but would turn the drone off or down when the human played
  38. * and then turn it back on when it stopped. Or maybe it could be used to start an
  39. * drum fill...
  40. *
  41. * The 'Reset idle' input allows this be kind of synced to a clock, beat, or sequence.
  42. * In the dronevexample above, the drone would then only come back in on a beat.
  43. *
  44. * And perhaps most importantly, it can be used to do almost random output and
  45. * make weird noises.
  46. */
  47. /* TODO
  48. * - is there a 'standard' for communicating lengths of time (like delay time)?
  49. * - idle start trigger
  50. * - idle end trigger
  51. * - switch for output to be high for idle or low for idle
  52. * - time display widget for timeout length
  53. * - Fine/Course params fors for timeout
  54. * - idle timeout countdown display for remaining time before timeout
  55. * - gui 'progress' widget?
  56. */
  57. struct IdleSwitch : Module {
  58. enum ParamIds {
  59. TIME_PARAM,
  60. NUM_PARAMS
  61. };
  62. enum InputIds {
  63. INPUT_SOURCE_INPUT,
  64. HEARTBEAT_INPUT,
  65. TIME_INPUT,
  66. PULSE_INPUT,
  67. SWITCHED_INPUT,
  68. NUM_INPUTS
  69. };
  70. enum OutputIds {
  71. IDLE_GATE_OUTPUT,
  72. TIME_OUTPUT,
  73. IDLE_START_OUTPUT,
  74. IDLE_END_OUTPUT,
  75. FRAME_COUNT_OUTPUT,
  76. ON_WHEN_IDLE_OUTPUT,
  77. OFF_WHEN_IDLE_OUTPUT,
  78. NUM_OUTPUTS
  79. };
  80. enum LightIds {
  81. NUM_LIGHTS
  82. };
  83. int idleTimeoutMS = 140;
  84. int idleTimeLeftMS = 0;
  85. SchmittTrigger inputTrigger;
  86. // FIXME: these names are confusing
  87. SchmittTrigger heartbeatTrigger;
  88. // clock mode stuff
  89. SchmittTrigger pulseTrigger;
  90. int pulseFrame = 0;
  91. bool waiting_for_pulse = false;
  92. bool pulse_mode = false;
  93. PulseGenerator idleStartPulse;
  94. PulseGenerator idleEndPulse;
  95. // FIXME: not really counts
  96. int frameCount = 0;
  97. int maxFrameCount = 0;
  98. float idleGateOutput = 0.0;
  99. float deltaTime = 0;
  100. bool is_idle = false;
  101. IdleSwitch() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {}
  102. void step() override;
  103. };
  104. void IdleSwitch::step() {
  105. bool pulse_seen = false;
  106. bool time_exceeded = false;
  107. pulse_mode = inputs[PULSE_INPUT].active;
  108. float sampleRate = engineGetSampleRate();
  109. // Compute the length of our idle time based on the knob + time cv
  110. // -or-
  111. // base it one the time since the last clock pulse
  112. if (pulse_mode) {
  113. if (inputTrigger.process(inputs[PULSE_INPUT].value)) {
  114. // keep track of which frame we got a pulse
  115. // FIXME: without a max time, frameCount can wrap?
  116. // update pulseFrame to point to current frame count
  117. pulseFrame = frameCount;
  118. waiting_for_pulse = true;
  119. pulse_seen = true;
  120. }
  121. deltaTime = fmax(frameCount - pulseFrame, 0) / sampleRate;
  122. // if we are waiting, maxframeCount is the time since last pulse and increasing
  123. maxFrameCount = frameCount;
  124. } else {
  125. deltaTime = params[TIME_PARAM].value;
  126. if (inputs[TIME_INPUT].active) {
  127. deltaTime += clamp(inputs[TIME_INPUT].value, 0.0f, 10.0f);
  128. }
  129. // TODO: refactor into submethods if not subclass
  130. maxFrameCount = (int)ceilf(deltaTime * sampleRate);
  131. }
  132. idleTimeoutMS = std::round(deltaTime*1000);
  133. // debug("is_idle: %d pulse_mode: %d pulse_frame: %d frameCount: %d maxFrameCount: %d ", is_idle, pulse_mode, pulseFrame, frameCount, maxFrameCount);
  134. // debug("is_idle: %d pulse_mode: %d w_f_pulse: %d pulse_seen: %d pulseFrame: %d frameCount: %d deltaTime: %f",
  135. // is_idle, pulse_mode, waiting_for_pulse, pulse_seen, pulseFrame, frameCount, deltaTime);
  136. if (inputs[HEARTBEAT_INPUT].active &&
  137. heartbeatTrigger.process(inputs[HEARTBEAT_INPUT].value)) {
  138. frameCount = 0;
  139. }
  140. // time_left_s is always 0 for pulse mode until we predict the future
  141. float frames_left = fmax(maxFrameCount - frameCount, 0);
  142. float time_left_s = frames_left / sampleRate;
  143. // TODO: simplify the start/end/gate on logic... really only a few states to check
  144. // the start of idle (not idle -> idle trans)
  145. if ((frameCount > maxFrameCount) || (waiting_for_pulse && pulse_seen)) {
  146. time_exceeded = true;
  147. if (!is_idle) {
  148. idleStartPulse.trigger(0.01);
  149. }
  150. }
  151. // stay idle once we start until there is an input event
  152. is_idle = (is_idle || time_exceeded);
  153. if (is_idle) {
  154. idleGateOutput = 10.0;
  155. outputs[ON_WHEN_IDLE_OUTPUT].value = inputs[SWITCHED_INPUT].value;
  156. outputs[OFF_WHEN_IDLE_OUTPUT].value = 0.0f;
  157. } else {
  158. idleGateOutput = 0.0;
  159. outputs[ON_WHEN_IDLE_OUTPUT].value = 0.0f;
  160. outputs[OFF_WHEN_IDLE_OUTPUT].value = inputs[SWITCHED_INPUT].value;
  161. is_idle = false;
  162. // if we arent idle yet, the idleTimeLeft is changing and we need to update time remaining display
  163. // update idletimeLeftMS which drives the digit display widget
  164. idleTimeLeftMS = time_left_s*1000;
  165. }
  166. frameCount++;
  167. if (inputs[INPUT_SOURCE_INPUT].active &&
  168. inputTrigger.process(inputs[INPUT_SOURCE_INPUT].value)) {
  169. // only end idle if we are already idle (idle->not idle transition)
  170. if (is_idle) {
  171. idleEndPulse.trigger(0.01);
  172. }
  173. is_idle = false;
  174. waiting_for_pulse = false;
  175. frameCount = 0;
  176. pulseFrame = 0;
  177. }
  178. // once clock input works, could add an output to indicate how long between clock
  179. // If in pulse mode, deltaTime can be larger than 10s internal, but the max output
  180. // to "Time output" is 10V. ie, after 10s the "Time Output" stops increasing.
  181. outputs[TIME_OUTPUT].value = clamp(deltaTime, 0.0f, 10.0f);
  182. outputs[IDLE_GATE_OUTPUT].value = idleGateOutput;
  183. outputs[IDLE_START_OUTPUT].value = idleStartPulse.process(1.0/engineGetSampleRate()) ? 10.0 : 0.0;
  184. outputs[IDLE_END_OUTPUT].value = idleEndPulse.process(1.0/engineGetSampleRate()) ? 10.0 : 0.0;
  185. }
  186. // From AS DelayPlus.cpp https://github.com/AScustomWorks/AS
  187. struct IdleSwitchMsDisplayWidget : TransparentWidget {
  188. int *value;
  189. std::shared_ptr<Font> font;
  190. IdleSwitchMsDisplayWidget() {
  191. font = Font::load(assetPlugin(plugin, "res/Segment7Standard.ttf"));
  192. }
  193. void draw(NVGcontext *vg) override {
  194. // Background
  195. // these go to...
  196. NVGcolor backgroundColor = nvgRGB(0x11, 0x11, 0x11);
  197. NVGcolor borderColor = nvgRGB(0xff, 0xff, 0xff);
  198. nvgBeginPath(vg);
  199. nvgRoundedRect(vg, 0.0, 0.0, box.size.x, box.size.y, 5.0);
  200. nvgFillColor(vg, backgroundColor);
  201. nvgFill(vg);
  202. nvgStrokeWidth(vg, 3.0);
  203. nvgStrokeColor(vg, borderColor);
  204. nvgStroke(vg);
  205. // text
  206. nvgFontSize(vg, 18);
  207. nvgFontFaceId(vg, font->handle);
  208. nvgTextLetterSpacing(vg, 2.5);
  209. std::stringstream to_display;
  210. to_display << std::right << std::setw(5) << *value;
  211. Vec textPos = Vec(0.5f, 19.0f);
  212. NVGcolor textColor = nvgRGB(0x65, 0xf6, 0x78);
  213. nvgFillColor(vg, textColor);
  214. nvgText(vg, textPos.x, textPos.y, to_display.str().c_str(), NULL);
  215. }
  216. };
  217. struct IdleSwitchWidget : ModuleWidget {
  218. IdleSwitchWidget(IdleSwitch *module);
  219. };
  220. IdleSwitchWidget::IdleSwitchWidget(IdleSwitch *module) : ModuleWidget(module) {
  221. setPanel(SVG::load(assetPlugin(plugin, "res/IdleSwitch.svg")));
  222. addChild(Widget::create<ScrewSilver>(Vec(5, 0)));
  223. addChild(Widget::create<ScrewSilver>(Vec(box.size.x - 20, 365)));
  224. addInput(Port::create<PJ301MPort>(Vec(37, 20.0), Port::INPUT, module, IdleSwitch::INPUT_SOURCE_INPUT));
  225. addInput(Port::create<PJ301MPort>(Vec(37, 60.0), Port::INPUT, module, IdleSwitch::HEARTBEAT_INPUT));
  226. addInput(Port::create<PJ301MPort>(Vec(70, 60.0), Port::INPUT, module, IdleSwitch::PULSE_INPUT));
  227. // idle time display
  228. // FIXME: handle large IdleTimeoutMs (> 99999ms) better
  229. IdleSwitchMsDisplayWidget *idle_time_display = new IdleSwitchMsDisplayWidget();
  230. idle_time_display->box.pos = Vec(20, 115);
  231. idle_time_display->box.size = Vec(70, 24);
  232. idle_time_display->value = &module->idleTimeoutMS;
  233. addChild(idle_time_display);
  234. addInput(Port::create<PJ301MPort>(Vec(10, 155.0), Port::INPUT, module, IdleSwitch::TIME_INPUT));
  235. addParam(ParamWidget::create<Davies1900hBlackKnob>(Vec(38.86, 150.0), module, IdleSwitch::TIME_PARAM, 0.0, 10.0, 0.25));
  236. addOutput(Port::create<PJ301MPort>(Vec(80, 155.0), Port::OUTPUT, module, IdleSwitch::TIME_OUTPUT));
  237. IdleSwitchMsDisplayWidget *time_remaining_display = new IdleSwitchMsDisplayWidget();
  238. time_remaining_display->box.pos = Vec(20, 225);
  239. time_remaining_display->box.size = Vec(70, 24);
  240. time_remaining_display->value = &module->idleTimeLeftMS;
  241. addChild(time_remaining_display);
  242. addOutput(Port::create<PJ301MPort>(Vec(10, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_START_OUTPUT));
  243. addOutput(Port::create<PJ301MPort>(Vec(47.5, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_GATE_OUTPUT));
  244. addOutput(Port::create<PJ301MPort>(Vec(85, 263.0), Port::OUTPUT, module, IdleSwitch::IDLE_END_OUTPUT));
  245. addInput(Port::create<PJ301MPort>(Vec(10.0f, 315.0f), Port::INPUT, module, IdleSwitch::SWITCHED_INPUT));
  246. addOutput(Port::create<PJ301MPort>(Vec(47.5f, 315.0f), Port::OUTPUT, module, IdleSwitch::ON_WHEN_IDLE_OUTPUT));
  247. addOutput(Port::create<PJ301MPort>(Vec(85.0f, 315.0f), Port::OUTPUT, module, IdleSwitch::OFF_WHEN_IDLE_OUTPUT));
  248. }
  249. } // namespace rack_plugin_Alikins
  250. using namespace rack_plugin_Alikins;
  251. RACK_PLUGIN_MODEL_INIT(Alikins, IdleSwitch) {
  252. Model *modelIdleSwitch = Model::create<IdleSwitch, IdleSwitchWidget>(
  253. "Alikins", "IdleSwitch", "Idle Switch", SWITCH_TAG , UTILITY_TAG);
  254. return modelIdleSwitch;
  255. }