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.

202 lines
5.2KB

  1. #include "plugin.hpp"
  2. struct Quantizer : Module {
  3. enum ParamIds {
  4. NUM_PARAMS
  5. };
  6. enum InputIds {
  7. PITCH_INPUT,
  8. NUM_INPUTS
  9. };
  10. enum OutputIds {
  11. PITCH_OUTPUT,
  12. NUM_OUTPUTS
  13. };
  14. enum LightIds {
  15. NUM_LIGHTS
  16. };
  17. bool enabledNotes[12];
  18. // Intervals [i / 24, (i+1) / 24) V mapping to the closest enabled note
  19. int ranges[24];
  20. bool playingNotes[12];
  21. Quantizer() {
  22. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  23. onReset();
  24. }
  25. void onReset() override {
  26. for (int i = 0; i < 12; i++) {
  27. enabledNotes[i] = true;
  28. }
  29. updateRanges();
  30. }
  31. void process(const ProcessArgs &args) override {
  32. bool playingNotes[12] = {};
  33. int channels = std::max(inputs[PITCH_INPUT].getChannels(), 1);
  34. for (int c = 0; c < channels; c++) {
  35. float pitch = inputs[PITCH_INPUT].getVoltage(c);
  36. int range = std::floor(pitch * 24);
  37. int octave = eucDiv(range, 24);
  38. range -= octave * 24;
  39. int note = ranges[range] + octave * 12;
  40. playingNotes[eucMod(note, 12)] = true;
  41. pitch = float(note) / 12;
  42. outputs[PITCH_OUTPUT].setVoltage(pitch, c);
  43. }
  44. outputs[PITCH_OUTPUT].setChannels(channels);
  45. std::memcpy(this->playingNotes, playingNotes, sizeof(playingNotes));
  46. }
  47. void updateRanges() {
  48. // Check if no notes are enabled
  49. bool anyEnabled = false;
  50. for (int note = 0; note < 12; note++) {
  51. if (enabledNotes[note]) {
  52. anyEnabled = true;
  53. break;
  54. }
  55. }
  56. // Find closest notes for each range
  57. for (int i = 0; i < 24; i++) {
  58. int closestNote = 0;
  59. int closestDist = INT_MAX;
  60. for (int note = -12; note <= 24; note++) {
  61. int dist = std::abs((i + 1) / 2 - note);
  62. // Ignore enabled state if no notes are enabled
  63. if (anyEnabled && !enabledNotes[eucMod(note, 12)]) {
  64. continue;
  65. }
  66. if (dist < closestDist) {
  67. closestNote = note;
  68. closestDist = dist;
  69. }
  70. else {
  71. // If dist increases, we won't find a better one.
  72. break;
  73. }
  74. }
  75. ranges[i] = closestNote;
  76. }
  77. }
  78. json_t *dataToJson() override {
  79. json_t *rootJ = json_object();
  80. json_t *enabledNotesJ = json_array();
  81. for (int i = 0; i < 12; i++) {
  82. json_array_insert_new(enabledNotesJ, i, json_boolean(enabledNotes[i]));
  83. }
  84. json_object_set_new(rootJ, "enabledNotes", enabledNotesJ);
  85. return rootJ;
  86. }
  87. void dataFromJson(json_t *rootJ) override {
  88. json_t *enabledNotesJ = json_object_get(rootJ, "enabledNotes");
  89. if (enabledNotesJ) {
  90. for (int i = 0; i < 12; i++) {
  91. json_t *enabledNoteJ = json_array_get(enabledNotesJ, i);
  92. if (enabledNoteJ)
  93. enabledNotes[i] = json_boolean_value(enabledNoteJ);
  94. }
  95. }
  96. updateRanges();
  97. }
  98. };
  99. struct QuantizerButton : OpaqueWidget {
  100. int note;
  101. Quantizer *module;
  102. void draw(const DrawArgs &args) override {
  103. const float margin = mm2px(1.5);
  104. Rect r = box.zeroPos().grow(Vec(margin, margin / 2).neg());
  105. nvgBeginPath(args.vg);
  106. nvgRect(args.vg, RECT_ARGS(r));
  107. if (module ? module->playingNotes[note] : (note == 0)) {
  108. nvgFillColor(args.vg, nvgRGB(0xff, 0xd7, 0x14));
  109. }
  110. else if (module ? module->enabledNotes[note] : true) {
  111. nvgFillColor(args.vg, nvgRGB(0x7f, 0x6b, 0x0a));
  112. }
  113. else {
  114. nvgFillColor(args.vg, nvgRGB(0x40, 0x36, 0x05));
  115. }
  116. nvgFill(args.vg);
  117. }
  118. void onDragStart(const event::DragStart &e) override {
  119. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  120. module->enabledNotes[note] ^= true;
  121. module->updateRanges();
  122. }
  123. OpaqueWidget::onDragStart(e);
  124. }
  125. void onDragEnter(const event::DragEnter &e) override {
  126. if (e.button == GLFW_MOUSE_BUTTON_LEFT) {
  127. QuantizerButton *origin = dynamic_cast<QuantizerButton*>(e.origin);
  128. if (origin) {
  129. module->enabledNotes[note] = module->enabledNotes[origin->note];;
  130. module->updateRanges();
  131. }
  132. }
  133. OpaqueWidget::onDragEnter(e);
  134. }
  135. };
  136. struct QuantizerDisplay : OpaqueWidget {
  137. void setModule(Quantizer *module) {
  138. const float margin = mm2px(1.5) / 2;
  139. box.size = mm2px(Vec(15.24, 72.0));
  140. const int notes = 12;
  141. const float height = box.size.y - 2 * margin;
  142. for (int note = 0; note < notes; note++) {
  143. QuantizerButton *quantizerButton = new QuantizerButton();
  144. quantizerButton->box.pos = Vec(0, margin + height / notes * note);
  145. quantizerButton->box.size = Vec(box.size.x, height / notes);
  146. quantizerButton->module = module;
  147. quantizerButton->note = note;
  148. addChild(quantizerButton);
  149. }
  150. }
  151. void draw(const DrawArgs &args) override {
  152. // Background
  153. nvgBeginPath(args.vg);
  154. nvgRect(args.vg, 0, 0, box.size.x, box.size.y);
  155. nvgFillColor(args.vg, nvgRGB(0, 0, 0));
  156. nvgFill(args.vg);
  157. OpaqueWidget::draw(args);
  158. }
  159. };
  160. struct QuantizerWidget : ModuleWidget {
  161. QuantizerWidget(Quantizer *module) {
  162. setModule(module);
  163. setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Quantizer.svg")));
  164. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, 0)));
  165. addChild(createWidget<ScrewSilver>(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH)));
  166. addInput(createInputCentered<PJ301MPort>(mm2px(Vec(7.62, 97.253)), module, Quantizer::PITCH_INPUT));
  167. addOutput(createOutputCentered<PJ301MPort>(mm2px(Vec(7.62, 112.253)), module, Quantizer::PITCH_OUTPUT));
  168. QuantizerDisplay *quantizerDisplay = createWidget<QuantizerDisplay>(mm2px(Vec(0.0, 14.585)));
  169. quantizerDisplay->setModule(module);
  170. addChild(quantizerDisplay);
  171. }
  172. };
  173. Model *modelQuantizer = createModel<Quantizer, QuantizerWidget>("Quantizer");