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.

552 lines
15KB

  1. #include "Noobhour.hpp"
  2. #include "dsp/digital.hpp"
  3. #include <vector>
  4. namespace rack_plugin_noobhour {
  5. // TODO LATER
  6. // scale file loader - Latif Fital
  7. // CustomScale Pro
  8. // - paging? different pages, back/forth, CV for index selection (interpolate between non-empty pages, lights to indicate?)
  9. // - quad
  10. // - button back for randomization
  11. // TODO
  12. // IN PROGRESS
  13. // bus
  14. // DONE
  15. // bsl1r
  16. // context menu switch between 0-10 and -5..5 - (was attenuator - Pyer Cllrd)
  17. // Normalizing the gate input to all following inputs (with nothing plugged in them) would be amazing (instead of 4 copies of the same cable) Patrick McIlveen
  18. // Normalized HIGH and LOW inputs as well
  19. // Normalizing all of the outputs to the last one (a la AS 4ch baby mixer and Audible) Patrick McIlveen
  20. // Performance imporvement CustomScale
  21. // random subset (randomize activity of individual tones) - Pyer Cllrd
  22. // ended up with lines on I, IV, V, was: change rings around lights for black/white distinction - steve baker
  23. struct GreenBlueYellowLight : GrayModuleLightWidget {
  24. GreenBlueYellowLight() {
  25. addBaseColor(COLOR_GREEN);
  26. addBaseColor(COLOR_BLUE);
  27. addBaseColor(COLOR_YELLOW);
  28. }
  29. };
  30. template <typename BASE>
  31. struct ToneLight : BASE {
  32. ToneLight() {
  33. this->box.size = mm2px(Vec(6.0f, 6.0f));
  34. }
  35. };
  36. /*
  37. struct LEDBezelGray : SVGSwitch, MomentarySwitch {
  38. LEDBezelGray() {
  39. addFrame(SVG::load(assetPlugin(plugin, "res/LEDBezelGray.svg")));
  40. }
  41. };
  42. struct LEDBezelDark : SVGSwitch, MomentarySwitch {
  43. LEDBezelDark() {
  44. addFrame(SVG::load(assetPlugin(plugin, "res/LEDBezelDark.svg")));
  45. }
  46. };
  47. struct LighterGrayModuleLightWidget : ModuleLightWidget {
  48. LighterGrayModuleLightWidget() {
  49. bgColor = nvgRGB(0x9a, 0x9a, 0x9a);
  50. borderColor = nvgRGBA(0, 0, 0, 0x60);
  51. }
  52. };
  53. struct LighterGreenBlueYellowLight : LighterGrayModuleLightWidget {
  54. LighterGreenBlueYellowLight() {
  55. addBaseColor(COLOR_GREEN);
  56. addBaseColor(COLOR_BLUE);
  57. addBaseColor(COLOR_YELLOW);
  58. }
  59. };
  60. */
  61. struct Customscaler : Module {
  62. static const int NUM_OCTAVES = 5;
  63. static const int BASE_OCTAVE = 2;
  64. static const int NUM_TONES = NUM_OCTAVES * 12;
  65. enum InputIds {
  66. SIGNAL_INPUT,
  67. TONE_INPUT,
  68. TOGGLE_TRIGGER_INPUT,
  69. RESET_TRIGGER_INPUT,
  70. RANDOMIZE_TRIGGER_INPUT,
  71. P_INPUT,
  72. BASE_INPUT,
  73. NUM_INPUTS
  74. };
  75. enum OutputIds {
  76. OUT_OUTPUT,
  77. CHANGEGATE_OUTPUT,
  78. NUM_OUTPUTS
  79. };
  80. enum ParamIds {
  81. ENUMS(TONE1_PARAM, NUM_TONES),
  82. RANGE_PARAM,
  83. P_PARAM,
  84. // RANDOMIZE_BUTTON_PARAM,
  85. RESET_BUTTON_PARAM,
  86. BASE_PARAM,
  87. MODE_PARAM,
  88. NUM_PARAMS
  89. };
  90. enum LightIds {
  91. ENUMS(TONE1_LIGHT, NUM_TONES * 3),
  92. NUM_LIGHTS
  93. };
  94. SchmittTrigger gateTrigger;
  95. SchmittTrigger randomizeTrigger;
  96. SchmittTrigger resetTrigger;
  97. SchmittTrigger resetButtonTrigger;
  98. SchmittTrigger paramTrigger[NUM_TONES];
  99. PulseGenerator changePulse;
  100. bool state[NUM_TONES];
  101. bool candidate[NUM_TONES];
  102. int lastFinalTone = NUM_TONES;
  103. int lastStartTone = NUM_TONES;
  104. int lastSelectedTone = NUM_TONES;
  105. std::vector<int> activeTones;
  106. bool activeTonesDirty = true;
  107. bool bipolarInput = false;
  108. Customscaler() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) {
  109. activeTones.reserve(NUM_TONES);
  110. onReset();
  111. }
  112. void step() override;
  113. float getVOct(int toneIndex) const {
  114. int octave = toneIndex / 12;
  115. int tone = toneIndex % 12;
  116. return tone/12.f + octave - BASE_OCTAVE;
  117. }
  118. int getTone(float vOct) const {
  119. return static_cast<int>(vOct * 12.f) + 12 * BASE_OCTAVE;
  120. }
  121. void onReset() override {
  122. for (int i = 0; i < NUM_TONES; i++) {
  123. state[i] = false;
  124. candidate[i] = false;
  125. }
  126. activeTonesDirty = true;
  127. }
  128. float getP() {
  129. float p_input = 0;
  130. if (inputs[P_INPUT].active)
  131. p_input = clamp(inputs[P_INPUT].value / 10.f, -10.f, 10.f);
  132. return clamp(p_input + params[P_PARAM].value, 0.0f, 1.0f);
  133. }
  134. void onRandomize() override {
  135. randomizeTones(getP());
  136. }
  137. void randomizeTones(float p) {
  138. for (int i = 0; i < NUM_TONES; i++) {
  139. state[i] = (randomUniform() < p);
  140. candidate[i] = false;
  141. }
  142. activeTonesDirty = true;
  143. }
  144. void randomSubset(float p) {
  145. int activeTones = 0;
  146. int candidates = 0;
  147. bool toggle = params[MODE_PARAM].value < 0.5f;
  148. for (int i = 0; i < NUM_TONES; i++) {
  149. if (state[i] || candidate[i]) {
  150. candidates++;
  151. if (toggle) {
  152. if (randomUniform() < p) {
  153. state[i] ^= true;
  154. }
  155. if (state[i]) {
  156. activeTones++;
  157. candidate[i] = false;
  158. } else {
  159. candidate[i] = true;
  160. }
  161. } else {
  162. if (randomUniform() < p) {
  163. activeTones++;
  164. state[i] = true;
  165. candidate[i] = false;
  166. } else {
  167. state[i] = false;
  168. candidate[i] = true;
  169. }
  170. }
  171. }
  172. }
  173. // if random subset is called without active or candidate tones,
  174. // let it behave like the normal randomisation: everything is a
  175. // candidate, retry
  176. if (candidates == 0) {
  177. for (int i = 0; i < NUM_TONES; i++) {
  178. candidate[i] = true;
  179. }
  180. randomSubset(p);
  181. return;
  182. }
  183. // make sure at least one tone is active so we don't return 0 = C4
  184. // which may be not be a candidate
  185. if (activeTones == 0) {
  186. for (int i = 0; i < NUM_TONES; i++) {
  187. if (candidate[i]) {
  188. state[i] = true;
  189. candidate[i] = false;
  190. break;
  191. }
  192. }
  193. }
  194. activeTonesDirty = true;
  195. }
  196. json_t *toJson() override {
  197. json_t *rootJ = json_object();
  198. json_t *statesJ = json_array();
  199. for (int i = 0; i < NUM_TONES; i++) {
  200. json_t *stateJ = json_boolean(state[i]);
  201. json_array_append_new(statesJ, stateJ);
  202. }
  203. json_object_set_new(rootJ, "states", statesJ);
  204. json_t *candidatesJ = json_array();
  205. for (int i = 0; i < NUM_TONES; i++) {
  206. json_t *candidateJ = json_boolean(candidate[i]);
  207. json_array_append_new(candidatesJ, candidateJ);
  208. }
  209. json_object_set_new(rootJ, "candidates", candidatesJ);
  210. json_t *bipolarInputJ = json_boolean(bipolarInput);
  211. json_object_set_new(rootJ, "bipolarInput", bipolarInputJ);
  212. return rootJ;
  213. }
  214. void fromJson(json_t *rootJ) override {
  215. json_t *statesJ = json_object_get(rootJ, "states");
  216. if (statesJ) {
  217. for (int i = 0; i < NUM_TONES; i++) {
  218. json_t *stateJ = json_array_get(statesJ, i);
  219. if (stateJ)
  220. state[i] = json_boolean_value(stateJ);
  221. }
  222. }
  223. json_t *candidatesJ = json_object_get(rootJ, "candidates");
  224. if (candidatesJ) {
  225. for (int i = 0; i < NUM_TONES; i++) {
  226. json_t *candidateJ = json_array_get(candidatesJ, i);
  227. if (candidateJ)
  228. candidate[i] = json_boolean_value(candidateJ);
  229. }
  230. }
  231. json_t *bipolarInputJ = json_object_get(rootJ, "bipolarInput");
  232. bipolarInput = json_boolean_value(bipolarInputJ);
  233. activeTonesDirty = true;
  234. }
  235. };
  236. void Customscaler::step() {
  237. // RESET
  238. if (inputs[RESET_TRIGGER_INPUT].active) {
  239. if (resetTrigger.process(rescale(inputs[RESET_TRIGGER_INPUT].value, 0.1f, 2.f, 0.f, 1.f))) {
  240. onReset();
  241. }
  242. }
  243. if (resetButtonTrigger.process(params[RESET_BUTTON_PARAM].value)) {
  244. onReset();
  245. }
  246. // RANDOMIZE
  247. if (inputs[RANDOMIZE_TRIGGER_INPUT].active) {
  248. if (randomizeTrigger.process(rescale(inputs[RANDOMIZE_TRIGGER_INPUT].value, 0.1f, 2.f, 0.f, 1.f))) {
  249. randomSubset(getP());
  250. }
  251. }
  252. /*
  253. if (randomizeButtonTrigger.process(params[RANDOMIZE_BUTTON_PARAM].value)) {
  254. randomizeTones(getP());
  255. }
  256. */
  257. // TOGGLE
  258. if (inputs[TONE_INPUT].active) {
  259. float gate = 0.0;
  260. if (inputs[TOGGLE_TRIGGER_INPUT].active)
  261. gate = inputs[TOGGLE_TRIGGER_INPUT].value;
  262. if (gateTrigger.process(rescale(gate, 0.1f, 2.f, 0.f, 1.f))) {
  263. int toneIndex = getTone(inputs[TONE_INPUT].value);
  264. if (toneIndex >= 0 && toneIndex < NUM_TONES) {
  265. state[toneIndex] ^= true;
  266. candidate[toneIndex] = false;
  267. activeTonesDirty = true;
  268. }
  269. }
  270. }
  271. // OCTAVE RANGE
  272. int startTone = 0;
  273. int endTone = 0;
  274. if (params[RANGE_PARAM].value < 0.5f) {
  275. startTone = 0;
  276. endTone = NUM_TONES - 1;
  277. } else if (params[RANGE_PARAM].value < 1.5f) {
  278. startTone = 12;
  279. endTone = NUM_TONES - 13;
  280. } else {
  281. startTone = 24;
  282. endTone = NUM_TONES - 25;
  283. }
  284. if (startTone != lastStartTone) {
  285. activeTonesDirty = true;
  286. lastStartTone = startTone;
  287. }
  288. // CHECK TONE TOGGLES
  289. for (int i = 0; i < NUM_TONES; i++) {
  290. if (paramTrigger[i].process(params[i].value)) {
  291. state[i] ^= true;
  292. candidate[i] = false;
  293. activeTonesDirty = true;
  294. }
  295. }
  296. // GATHER CANDIDATES
  297. if (activeTonesDirty) {
  298. activeTones.clear();
  299. for (int i = 0; i < NUM_TONES; i++) {
  300. if (state[i] && i >= startTone && i <= endTone) {
  301. activeTones.push_back(i);
  302. }
  303. }
  304. }
  305. // FETCH BASE TONE
  306. float baseTone = params[BASE_PARAM].value;
  307. if (inputs[BASE_INPUT].active) {
  308. baseTone += inputs[BASE_INPUT].value / 10.f * 11.f;
  309. }
  310. int baseToneDiscrete = static_cast<int>(clamp(baseTone, 0.f, 11.f));
  311. // SELECT TONE
  312. float output = 0;
  313. int selectedTone = NUM_TONES;
  314. int finalTone = NUM_TONES;
  315. if (inputs[SIGNAL_INPUT].active && activeTones.size() > 0) {
  316. float inp = inputs[SIGNAL_INPUT].value;
  317. if (bipolarInput)
  318. inp += 5.f;
  319. unsigned int selectedIndex = static_cast<int>(activeTones.size() * (clamp(inp, 0.f, 10.f)) / 10.f);
  320. if (selectedIndex == activeTones.size())
  321. selectedIndex--;
  322. selectedTone = activeTones[selectedIndex];
  323. finalTone = selectedTone + baseToneDiscrete;
  324. output = getVOct(finalTone);
  325. }
  326. // DETECT TONE CHANGE
  327. if (finalTone != lastFinalTone) {
  328. changePulse.trigger(0.001f);
  329. lastFinalTone = finalTone;
  330. }
  331. // LIGHTS
  332. if (activeTonesDirty || selectedTone != lastSelectedTone) {
  333. for (int i = 0; i < NUM_TONES; i++) {
  334. float green = 0.f;
  335. float blue = 0.f;
  336. float yellow = 0.f;
  337. if (state[i]) {
  338. if (i==selectedTone) {
  339. blue = 0.9f;
  340. } else {
  341. if (i >= startTone && i <= endTone) {
  342. green = 0.9f; // active tone but not selected
  343. } else {
  344. green = 0.1f; // active but in inactive octave
  345. }
  346. }
  347. } else {
  348. if (candidate[i]) {
  349. if (i >= startTone && i <= endTone) {
  350. yellow = 0.3f; // candidate
  351. } else {
  352. yellow = 0.1f; // candidate but in inactive octave
  353. }
  354. }
  355. }
  356. lights[i * 3].setBrightness(green);
  357. lights[i * 3 + 1].setBrightness(blue);
  358. lights[i * 3 + 2].setBrightness(yellow);
  359. }
  360. }
  361. activeTonesDirty = false; // only reset after check for lights has been done
  362. lastSelectedTone = selectedTone;
  363. // OUTPUT
  364. outputs[OUT_OUTPUT].value = output;
  365. outputs[CHANGEGATE_OUTPUT].value = (changePulse.process(1.0f / engineGetSampleRate()) ? 10.0f : 0.0f);
  366. }
  367. struct CustomscalerWidget : ModuleWidget {
  368. // generate controls
  369. const int yStart = 25;
  370. const int yRange = 40;
  371. const int ySeparator = 5;
  372. const float x = 11.5f;
  373. const float x2 = 46.5f;
  374. const float lastY = 329;
  375. // static const float wKnob = 30.23437f;
  376. const float wInput = 31.58030f;
  377. const float wSwitch = 17.94267f;
  378. const float offsetKnob = -2.1;
  379. const float offsetSwitch = (wInput - wSwitch) / 2.0f - 1.5; // no idea why 1.5, not centered otherwise
  380. const int offsetTL1005 = 4;
  381. const bool whiteKey[12] = {true, false, true, false, true, true, false, true, false, true, false, true};
  382. CustomscalerWidget(Customscaler *module) : ModuleWidget(module) {
  383. setPanel(SVG::load(assetPlugin(plugin, "res/Customscaler.svg")));
  384. addChild(Widget::create<ScrewSilver>(Vec(15, 0)));
  385. addChild(Widget::create<ScrewSilver>(Vec(15, 365)));
  386. addChild(Widget::create<ScrewSilver>(Vec(box.size.x-30, 0)));
  387. addChild(Widget::create<ScrewSilver>(Vec(box.size.x-30, 365)));
  388. // upper panel
  389. addInput(Port::create<PJ301MPort>(Vec(x, yStart + 0 * yRange + 0 * ySeparator), Port::INPUT, module, Customscaler::SIGNAL_INPUT));
  390. addParam(ParamWidget::create<CKSSThree>(Vec(x2 + offsetSwitch, yStart + 0 * yRange + 0 * ySeparator), module, Customscaler::RANGE_PARAM, 0.f, 2.f, 0.f));
  391. addInput(Port::create<PJ301MPort>(Vec(x, yStart + 1 * yRange + 1 * ySeparator), Port::INPUT, module, Customscaler::BASE_INPUT));
  392. addParam(ParamWidget::create<RoundBlackSnapKnob>(Vec(x2 + offsetKnob, yStart + 1 * yRange + 1 * ySeparator + offsetKnob), module, Customscaler::BASE_PARAM, 0.f, 11.f, 0.f));
  393. addOutput(Port::create<PJ301MPort>(Vec(x, yStart + 2 * yRange + 2 * ySeparator), Port::OUTPUT, module, Customscaler::OUT_OUTPUT));
  394. addOutput(Port::create<PJ301MPort>(Vec(x2, yStart + 2 * yRange + 2 * ySeparator), Port::OUTPUT, module, Customscaler::CHANGEGATE_OUTPUT));
  395. // lower panel
  396. addInput(Port::create<PJ301MPort>(Vec(x, lastY - (3 * yRange + 2 * ySeparator)), Port::INPUT, module, Customscaler::TONE_INPUT));
  397. addInput(Port::create<PJ301MPort>(Vec(x2, lastY - (3 * yRange + 2 * ySeparator)), Port::INPUT, module, Customscaler::TOGGLE_TRIGGER_INPUT));
  398. // addInput(Port::create<PJ301MPort>(Vec(x, lastY - (1 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::RANDOM_SUBSET_TRIGGER_INPUT));
  399. // addParam(ParamWidget::create<TL1105>(Vec(x2 + offsetTL1005, lastY - (3 * yRange + 1 * ySeparator - offsetTL1005)), module, Customscaler::RANDOMIZE_BUTTON_PARAM, 0.0f, 1.0f, 0.0f));
  400. addParam(ParamWidget::create<RoundBlackKnob>(Vec(x2 + offsetKnob, lastY - (2 * yRange + 1 * ySeparator - offsetKnob)), module, Customscaler::P_PARAM, 0.f, 1.f, 0.5f));
  401. addInput(Port::create<PJ301MPort>(Vec(x, lastY - (2 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::P_INPUT));
  402. addInput(Port::create<PJ301MPort>(Vec(x, lastY - (1 * yRange + 1 * ySeparator)), Port::INPUT, module, Customscaler::RANDOMIZE_TRIGGER_INPUT));
  403. addParam(ParamWidget::create<CKSS>(Vec(x2 + offsetSwitch, lastY - (1 * yRange + 1 * ySeparator)), module, Customscaler::MODE_PARAM, 0.f, 1.f, 1.f));
  404. addInput(Port::create<PJ301MPort>(Vec(x, lastY), Port::INPUT, module, Customscaler::RESET_TRIGGER_INPUT));
  405. addParam(ParamWidget::create<TL1105>(Vec(x2 + offsetTL1005, lastY + offsetTL1005), module, Customscaler::RESET_BUTTON_PARAM, 0.0f, 1.0f, 0.0f));
  406. // generate lights
  407. float offsetX = mm2px(Vec(17.32, 18.915)).x - mm2px(Vec(16.57, 18.165)).x; // from Mutes
  408. float offsetY = mm2px(Vec(17.32, 18.915)).y - mm2px(Vec(16.57, 18.165)).y;
  409. for (int octave=0; octave<Customscaler::NUM_OCTAVES; octave++) {
  410. float x = 88 + octave * 27;
  411. for (int tone=0; tone<12; tone++) {
  412. float y = -5 + 28 * (12 - tone);
  413. int index = octave * 12 + tone;
  414. addParam(ParamWidget::create<LEDBezel>(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f));
  415. addChild(ModuleLightWidget::create<ToneLight<GreenBlueYellowLight>>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3));
  416. /*
  417. if (whiteKey[tone]) {
  418. addParam(ParamWidget::create<LEDBezelGray>(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f));
  419. addChild(ModuleLightWidget::create<ToneLight<GreenBlueYellowLight>>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3));
  420. } else {
  421. addParam(ParamWidget::create<LEDBezelDark>(Vec(x, y), module, Customscaler::TONE1_PARAM + index, 0.0f, 1.0f, 0.0f));
  422. addChild(ModuleLightWidget::create<ToneLight<GreenBlueYellowLight>>(Vec(x + offsetX, y + offsetY), module, Customscaler::TONE1_PARAM + index * 3));
  423. }
  424. */
  425. }
  426. }
  427. };
  428. void appendContextMenu(Menu *menu) override {
  429. Customscaler *customscaler = dynamic_cast<Customscaler*>(module);
  430. assert(customscaler);
  431. struct UniBiItem : MenuItem {
  432. Customscaler *customscaler;
  433. void onAction(EventAction &e) override {
  434. customscaler->bipolarInput ^= true;;
  435. }
  436. void step() override {
  437. rightText = customscaler->bipolarInput ? "-5V..5V" : "0V..10V";
  438. MenuItem::step();
  439. }
  440. };
  441. menu->addChild(construct<MenuLabel>());
  442. menu->addChild(construct<UniBiItem>(&MenuItem::text, "Signal input", &UniBiItem::customscaler, customscaler));
  443. };
  444. };
  445. } // namespace rack_plugin_noobhour
  446. using namespace rack_plugin_noobhour;
  447. RACK_PLUGIN_MODEL_INIT(noobhour, Customscaler) {
  448. Model *modelCustomscaler = Model::create<Customscaler, CustomscalerWidget>("noobhour", "customscale", "Customscaler", QUANTIZER_TAG, RANDOM_TAG);
  449. return modelCustomscaler;
  450. }