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.

254 lines
8.5KB

  1. #include "21kHz.hpp"
  2. #include "dsp/digital.hpp"
  3. #include "dsp/math.hpp"
  4. #include <array>
  5. using std::array;
  6. namespace rack_plugin_21kHz {
  7. struct PalmLoop : Module {
  8. enum ParamIds {
  9. OCT_PARAM,
  10. COARSE_PARAM,
  11. FINE_PARAM,
  12. EXP_FM_PARAM,
  13. LIN_FM_PARAM,
  14. NUM_PARAMS
  15. };
  16. enum InputIds {
  17. RESET_INPUT,
  18. V_OCT_INPUT,
  19. EXP_FM_INPUT,
  20. LIN_FM_INPUT,
  21. NUM_INPUTS
  22. };
  23. enum OutputIds {
  24. SAW_OUTPUT,
  25. SQR_OUTPUT,
  26. TRI_OUTPUT,
  27. SIN_OUTPUT,
  28. SUB_OUTPUT,
  29. NUM_OUTPUTS
  30. };
  31. enum LightIds {
  32. NUM_LIGHTS
  33. };
  34. float phase = 0.0f;
  35. float oldPhase = 0.0f;
  36. float square = 1.0f;
  37. int discont = 0;
  38. int oldDiscont = 0;
  39. array<float, 4> sawBuffer;
  40. array<float, 4> sqrBuffer;
  41. array<float, 4> triBuffer;
  42. float log2sampleFreq = 15.4284f;
  43. SchmittTrigger resetTrigger;
  44. PalmLoop() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {}
  45. void step() override;
  46. void onSampleRateChange() override;
  47. // For more advanced Module features, read Rack's engine.hpp header file
  48. // - toJson, fromJson: serialization of internal data
  49. // - onSampleRateChange: event triggered by a change of sample rate
  50. // - onReset, onRandomize, onCreate, onDelete: implements special behavior when user clicks these from the context menu
  51. };
  52. void PalmLoop::onSampleRateChange() {
  53. log2sampleFreq = log2f(1 / engineGetSampleTime()) - 0.00009f;
  54. }
  55. // quick explanation: the whole thing is driven by a naive sawtooth, which writes to a four-sample buffer for each
  56. // (non-sine) waveform. the waves are calculated such that their discontinuities (or in the case of triangle, derivative
  57. // discontinuities) only occur each time the phasor exceeds a [0, 1) range. when we calculate the outputs, we look to see
  58. // if a discontinuity occured in the previous sample. if one did, we calculate the polyblep or polyblamp and add it to
  59. // each sample in the buffer. the output is the oldest buffer sample, which gets overwritten in the following step.
  60. void PalmLoop::step() {
  61. if (resetTrigger.process(inputs[RESET_INPUT].value)) {
  62. phase = 0.0f;
  63. }
  64. for (int i = 0; i <= 2; ++i) {
  65. sawBuffer[i] = sawBuffer[i + 1];
  66. sqrBuffer[i] = sqrBuffer[i + 1];
  67. triBuffer[i] = triBuffer[i + 1];
  68. }
  69. float freq = params[OCT_PARAM].value + 0.031360 + 0.083333 * params[COARSE_PARAM].value + params[FINE_PARAM].value + inputs[V_OCT_INPUT].value;
  70. if (inputs[EXP_FM_INPUT].active) {
  71. freq += params[EXP_FM_PARAM].value * inputs[EXP_FM_INPUT].value;
  72. if (freq >= log2sampleFreq) {
  73. freq = log2sampleFreq;
  74. }
  75. freq = powf(2.0f, freq);
  76. }
  77. else {
  78. if (freq >= log2sampleFreq) {
  79. freq = log2sampleFreq;
  80. }
  81. freq = powf(2.0f, freq);
  82. }
  83. float incr = 0.0f;
  84. if (inputs[LIN_FM_INPUT].active) {
  85. freq += params[LIN_FM_PARAM].value * params[LIN_FM_PARAM].value * inputs[LIN_FM_INPUT].value;
  86. incr = engineGetSampleTime() * freq;
  87. if (incr > 1.0f) {
  88. incr = 1.0f;
  89. }
  90. else if (incr < -1.0f) {
  91. incr = -1.0f;
  92. }
  93. }
  94. else {
  95. incr = engineGetSampleTime() * freq;
  96. }
  97. phase += incr;
  98. if (phase >= 0.0f && phase < 1.0f) {
  99. discont = 0;
  100. }
  101. else if (phase >= 1.0f) {
  102. discont = 1;
  103. --phase;
  104. square *= -1.0f;
  105. }
  106. else {
  107. discont = -1;
  108. ++phase;
  109. square *= -1.0f;
  110. }
  111. sawBuffer[3] = phase;
  112. sqrBuffer[3] = square;
  113. if (square >= 0.0f) {
  114. triBuffer[3] = phase;
  115. }
  116. else {
  117. triBuffer[3] = 1.0f - phase;
  118. }
  119. if (outputs[SAW_OUTPUT].active) {
  120. if (oldDiscont == 1) {
  121. polyblep4(sawBuffer, 1.0f - oldPhase / incr, 1.0f);
  122. }
  123. else if (oldDiscont == -1) {
  124. polyblep4(sawBuffer, 1.0f - (oldPhase - 1.0f) / incr, -1.0f);
  125. }
  126. outputs[SAW_OUTPUT].value = clampf(10.0f * (sawBuffer[0] - 0.5f), -5.0f, 5.0f);
  127. }
  128. if (outputs[SQR_OUTPUT].active) {
  129. // for some reason i don't understand, if discontinuities happen in two
  130. // adjacent samples, the first one must be inverted. otherwise the polyblep
  131. // is bad and causes aliasing. don't ask me how i managed to figure this out.
  132. if (discont == 0) {
  133. if (oldDiscont == 1) {
  134. polyblep4(sqrBuffer, 1.0f - oldPhase / incr, -2.0f * square);
  135. }
  136. else if (oldDiscont == -1) {
  137. polyblep4(sqrBuffer, 1.0f - (oldPhase - 1.0f) / incr, -2.0f * square);
  138. }
  139. }
  140. else {
  141. if (oldDiscont == 1) {
  142. polyblep4(sqrBuffer, 1.0f - oldPhase / incr, 2.0f * square);
  143. }
  144. else if (oldDiscont == -1) {
  145. polyblep4(sqrBuffer, 1.0f - (oldPhase - 1.0f) / incr, 2.0f * square);
  146. }
  147. }
  148. outputs[SQR_OUTPUT].value = clampf(4.9999f * sqrBuffer[0], -5.0f, 5.0f);
  149. }
  150. if (outputs[TRI_OUTPUT].active) {
  151. if (discont == 0) {
  152. if (oldDiscont == 1) {
  153. polyblamp4(triBuffer, 1.0f - oldPhase / incr, 2.0f * square * incr);
  154. }
  155. else if (oldDiscont == -1) {
  156. polyblamp4(triBuffer, 1.0f - (oldPhase - 1.0f) / incr, 2.0f * square * incr);
  157. }
  158. }
  159. else {
  160. if (oldDiscont == 1) {
  161. polyblamp4(triBuffer, 1.0f - oldPhase / incr, -2.0f * square * incr);
  162. }
  163. else if (oldDiscont == -1) {
  164. polyblamp4(triBuffer, 1.0f - (oldPhase - 1.0f) / incr, -2.0f * square * incr);
  165. }
  166. }
  167. outputs[TRI_OUTPUT].value = clampf(10.0f * (triBuffer[0] - 0.5f), -5.0f, 5.0f);
  168. }
  169. if (outputs[SIN_OUTPUT].active) {
  170. outputs[SIN_OUTPUT].value = 5.0f * sin_01(phase);
  171. }
  172. if (outputs[SUB_OUTPUT].active) {
  173. if (square >= 0.0f) {
  174. outputs[SUB_OUTPUT].value = 5.0f * sin_01(0.5f * phase);
  175. }
  176. else {
  177. outputs[SUB_OUTPUT].value = 5.0f * sin_01(0.5f * (1.0f - phase));
  178. }
  179. }
  180. oldPhase = phase;
  181. oldDiscont = discont;
  182. }
  183. struct PalmLoopWidget : ModuleWidget {
  184. PalmLoopWidget(PalmLoop *module) : ModuleWidget(module) {
  185. setPanel(SVG::load(assetPlugin(plugin, "res/Panels/PalmLoop.svg")));
  186. addChild(Widget::create<kHzScrew>(Vec(RACK_GRID_WIDTH, 0)));
  187. addChild(Widget::create<kHzScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0)));
  188. addChild(Widget::create<kHzScrew>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  189. addChild(Widget::create<kHzScrew>(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  190. addParam(ParamWidget::create<kHzKnobSnap>(Vec(36, 40), module, PalmLoop::OCT_PARAM, 4, 12, 8));
  191. addParam(ParamWidget::create<kHzKnobSmallSnap>(Vec(16, 112), module, PalmLoop::COARSE_PARAM, -7, 7, 0));
  192. addParam(ParamWidget::create<kHzKnobSmall>(Vec(72, 112), module, PalmLoop::FINE_PARAM, -0.083333, 0.083333, 0.0));
  193. addParam(ParamWidget::create<kHzKnobSmall>(Vec(16, 168), module, PalmLoop::EXP_FM_PARAM, -1.0, 1.0, 0.0));
  194. addParam(ParamWidget::create<kHzKnobSmall>(Vec(72, 168), module, PalmLoop::LIN_FM_PARAM, -40.0, 40.0, 0.0));
  195. addInput(Port::create<kHzPort>(Vec(10, 234), Port::INPUT, module, PalmLoop::EXP_FM_INPUT));
  196. addInput(Port::create<kHzPort>(Vec(47, 234), Port::INPUT, module, PalmLoop::V_OCT_INPUT));
  197. addInput(Port::create<kHzPort>(Vec(84, 234), Port::INPUT, module, PalmLoop::LIN_FM_INPUT));
  198. addInput(Port::create<kHzPort>(Vec(10, 276), Port::INPUT, module, PalmLoop::RESET_INPUT));
  199. addOutput(Port::create<kHzPort>(Vec(47, 276), Port::OUTPUT, module, PalmLoop::SAW_OUTPUT));
  200. addOutput(Port::create<kHzPort>(Vec(84, 276), Port::OUTPUT, module, PalmLoop::SIN_OUTPUT));
  201. addOutput(Port::create<kHzPort>(Vec(10, 318), Port::OUTPUT, module, PalmLoop::SQR_OUTPUT));
  202. addOutput(Port::create<kHzPort>(Vec(47, 318), Port::OUTPUT, module, PalmLoop::TRI_OUTPUT));
  203. addOutput(Port::create<kHzPort>(Vec(84, 318), Port::OUTPUT, module, PalmLoop::SUB_OUTPUT));
  204. }
  205. };
  206. } // namespace rack_plugin_21kHz
  207. using namespace rack_plugin_21kHz;
  208. RACK_PLUGIN_MODEL_INIT(21kHz, PalmLoop) {
  209. Model *modelPalmLoop = Model::create<PalmLoop, PalmLoopWidget>("21kHz", "kHzPalmLoop", "Palm Loop — basic VCO — 8hp", OSCILLATOR_TAG);
  210. return modelPalmLoop;
  211. }
  212. // history
  213. // 0.6.0
  214. // create
  215. // 0.6.1
  216. // minor optimizations
  217. // coarse goes -7 to +7
  218. // waveform labels & rearrangement on panel