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.

375 lines
13KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2022 Bram Giesen
  4. * Copyright (C) 2022 Filipe Coelho <falktx@falktx.com>
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU General Public License as
  8. * published by the Free Software Foundation; either version 3 of
  9. * the License, or any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * For a full copy of the GNU General Public License see the LICENSE file.
  17. */
  18. #include "plugincontext.hpp"
  19. #include "ModuleWidgets.hpp"
  20. #include "Widgets.hpp"
  21. extern "C" {
  22. #include "aubio.h"
  23. }
  24. USE_NAMESPACE_DISTRHO;
  25. // --------------------------------------------------------------------------------------------------------------------
  26. // aubio setup values (tested under 48 kHz sample rate)
  27. static constexpr const uint32_t kAubioHopSize = 1;
  28. static constexpr const uint32_t kAubioBufferSize = (1024 + 256 + 128) / kAubioHopSize;
  29. // default values
  30. static constexpr const float kDefaultSensitivity = 50.f;
  31. static constexpr const float kDefaultTolerance = 6.25f;
  32. static constexpr const float kDefaultThreshold = 12.5f;
  33. // static checks
  34. static_assert(sizeof(smpl_t) == sizeof(float), "smpl_t is float");
  35. static_assert(kAubioBufferSize % kAubioHopSize == 0, "kAubioBufferSize / kAubioHopSize has no remainder");
  36. // --------------------------------------------------------------------------------------------------------------------
  37. struct AudioToCVPitch : Module {
  38. enum ParamIds {
  39. PARAM_SENSITIVITY,
  40. PARAM_CONFIDENCETHRESHOLD,
  41. PARAM_TOLERANCE,
  42. PARAM_OCTAVE,
  43. NUM_PARAMS
  44. };
  45. enum InputIds {
  46. AUDIO_INPUT,
  47. NUM_INPUTS
  48. };
  49. enum OutputIds {
  50. CV_PITCH,
  51. CV_GATE,
  52. NUM_OUTPUTS
  53. };
  54. enum LightIds {
  55. NUM_LIGHTS
  56. };
  57. bool holdOutputPitch = true;
  58. bool smooth = true;
  59. int octave = 0;
  60. float lastKnownPitchInHz = 0.f;
  61. float lastKnownPitchConfidence = 0.f;
  62. float lastUsedTolerance = kDefaultTolerance;
  63. float lastUsedOutputPitch = 0.f;
  64. float lastUsedOutputSignal = 0.f;
  65. fvec_t* const detectedPitch = new_fvec(1);
  66. fvec_t* const inputBuffer = new_fvec(kAubioBufferSize);
  67. uint32_t inputBufferPos = 0;
  68. aubio_pitch_t* pitchDetector = nullptr;
  69. dsp::SlewLimiter smoothOutputSignal;
  70. AudioToCVPitch()
  71. {
  72. config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS);
  73. configInput(AUDIO_INPUT, "Audio");
  74. configOutput(CV_PITCH, "Pitch");
  75. configOutput(CV_GATE, "Gate");
  76. configParam(PARAM_SENSITIVITY, 0.1f, 99.f, kDefaultSensitivity, "Sensitivity", " %");
  77. configParam(PARAM_CONFIDENCETHRESHOLD, 0.f, 99.f, kDefaultThreshold, "Confidence Threshold", " %");
  78. configParam(PARAM_TOLERANCE, 0.f, 99.f, kDefaultTolerance, "Tolerance", " %");
  79. }
  80. void process(const ProcessArgs& args) override
  81. {
  82. float cvPitch = lastUsedOutputPitch;
  83. float cvSignal = lastUsedOutputSignal;
  84. inputBuffer->data[inputBufferPos] = inputs[AUDIO_INPUT].getVoltage() * 0.1f
  85. * params[PARAM_SENSITIVITY].getValue();
  86. if (++inputBufferPos == kAubioBufferSize)
  87. {
  88. inputBufferPos = 0;
  89. const float tolerance = params[PARAM_TOLERANCE].getValue();
  90. if (d_isNotEqual(lastUsedTolerance, tolerance))
  91. {
  92. lastUsedTolerance = tolerance;
  93. aubio_pitch_set_tolerance(pitchDetector, tolerance * 0.01f);
  94. }
  95. aubio_pitch_do(pitchDetector, inputBuffer, detectedPitch);
  96. const float detectedPitchInHz = fvec_get_sample(detectedPitch, 0);
  97. const float pitchConfidence = aubio_pitch_get_confidence(pitchDetector);
  98. if (detectedPitchInHz > 0.f && pitchConfidence >= params[PARAM_CONFIDENCETHRESHOLD].getValue() * 0.01f)
  99. {
  100. const float linearPitch = 12.f * (log2f(detectedPitchInHz / 440.f) + octave - 5) + 69.f;
  101. cvPitch = std::max(-10.f, std::min(10.f, linearPitch * (1.f/12.f)));
  102. lastKnownPitchInHz = detectedPitchInHz;
  103. cvSignal = 10.f;
  104. }
  105. else
  106. {
  107. if (! holdOutputPitch)
  108. lastKnownPitchInHz = cvPitch = 0.0f;
  109. cvSignal = 0.f;
  110. }
  111. lastKnownPitchConfidence = pitchConfidence;
  112. lastUsedOutputPitch = cvPitch;
  113. lastUsedOutputSignal = cvSignal;
  114. }
  115. outputs[CV_PITCH].setVoltage(smooth ? smoothOutputSignal.process(args.sampleTime, cvPitch) : cvPitch);
  116. outputs[CV_GATE].setVoltage(cvSignal);
  117. }
  118. void onReset() override
  119. {
  120. inputBufferPos = 0;
  121. smooth = true;
  122. holdOutputPitch = true;
  123. octave = 0;
  124. }
  125. void onSampleRateChange(const SampleRateChangeEvent& e) override
  126. {
  127. float tolerance;
  128. if (pitchDetector != nullptr)
  129. {
  130. tolerance = aubio_pitch_get_tolerance(pitchDetector);
  131. del_aubio_pitch(pitchDetector);
  132. }
  133. else
  134. {
  135. tolerance = kDefaultTolerance * 0.01f;
  136. }
  137. pitchDetector = new_aubio_pitch("yinfast", kAubioBufferSize, kAubioHopSize, e.sampleRate);
  138. DISTRHO_SAFE_ASSERT_RETURN(pitchDetector != nullptr,);
  139. aubio_pitch_set_silence(pitchDetector, -30.0f);
  140. aubio_pitch_set_tolerance(pitchDetector, tolerance);
  141. aubio_pitch_set_unit(pitchDetector, "Hz");
  142. const double fall = 1.0 / (double(kAubioBufferSize) / e.sampleRate);
  143. smoothOutputSignal.reset();
  144. smoothOutputSignal.setRiseFall(fall, fall);
  145. }
  146. json_t* dataToJson() override
  147. {
  148. json_t* const rootJ = json_object();
  149. DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, nullptr);
  150. json_object_set_new(rootJ, "holdOutputPitch", json_boolean(holdOutputPitch));
  151. json_object_set_new(rootJ, "smooth", json_boolean(smooth));
  152. json_object_set_new(rootJ, "octave", json_integer(octave));
  153. return rootJ;
  154. }
  155. void dataFromJson(json_t* const rootJ) override
  156. {
  157. if (json_t* const holdOutputPitchJ = json_object_get(rootJ, "holdOutputPitch"))
  158. holdOutputPitch = json_boolean_value(holdOutputPitchJ);
  159. if (json_t* const smoothJ = json_object_get(rootJ, "smooth"))
  160. smooth = json_boolean_value(smoothJ);
  161. if (json_t* const octaveJ = json_object_get(rootJ, "octave"))
  162. octave = json_integer_value(octaveJ);
  163. }
  164. };
  165. #ifndef HEADLESS
  166. struct SmallPercentageNanoKnob : NanoKnob<2, 0> {
  167. SmallPercentageNanoKnob() {
  168. box.size = Vec(32, 32);
  169. displayLabel = "";
  170. }
  171. void onChange(const ChangeEvent&) override
  172. {
  173. engine::ParamQuantity* const pq = getParamQuantity();
  174. DISTRHO_SAFE_ASSERT_RETURN(pq != nullptr,);
  175. displayString = string::f("%.1f %%", pq->getDisplayValue());
  176. }
  177. };
  178. struct AudioToCVPitchWidget : ModuleWidgetWith9HP {
  179. static constexpr const float startX = 10.0f;
  180. static constexpr const float startY_top = 71.0f;
  181. static constexpr const float startY_cv1 = 115.0f;
  182. static constexpr const float startY_cv2 = 145.0f;
  183. static constexpr const float padding = 32.0f;
  184. AudioToCVPitch* const module;
  185. std::string monoFontPath;
  186. AudioToCVPitchWidget(AudioToCVPitch* const m)
  187. : module(m)
  188. {
  189. setModule(m);
  190. setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/AudioToCVPitch.svg")));
  191. monoFontPath = asset::system("res/fonts/ShareTechMono-Regular.ttf");
  192. createAndAddScrews();
  193. addInput(createInput<PJ301MPort>(Vec(startX, startY_cv1 + 0 * padding), m, AudioToCVPitch::AUDIO_INPUT));
  194. addOutput(createOutput<PJ301MPort>(Vec(startX, startY_cv2 + 0 * padding), m, AudioToCVPitch::CV_PITCH));
  195. addOutput(createOutput<PJ301MPort>(Vec(startX, startY_cv2 + 1 * padding), m, AudioToCVPitch::CV_GATE));
  196. SmallPercentageNanoKnob* knobSens = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 85.f),
  197. module, AudioToCVPitch::PARAM_SENSITIVITY);
  198. knobSens->displayString = "50 %";
  199. addChild(knobSens);
  200. SmallPercentageNanoKnob* knobTolerance = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 135.f),
  201. module, AudioToCVPitch::PARAM_TOLERANCE);
  202. knobTolerance->displayString = "6.25 %";
  203. addChild(knobTolerance);
  204. SmallPercentageNanoKnob* knobThres = createParamCentered<SmallPercentageNanoKnob>(Vec(box.size.x * 0.5f, startY_cv2 + 185.f),
  205. module, AudioToCVPitch::PARAM_CONFIDENCETHRESHOLD);
  206. knobThres->displayString = "12.5 %";
  207. addChild(knobThres);
  208. }
  209. void drawInputLine(NVGcontext* const vg, const uint offset, const char* const text)
  210. {
  211. const float y = startY_cv1 + offset * padding;
  212. nvgBeginPath(vg);
  213. nvgFillColor(vg, nvgRGB(0xd0, 0xd0, 0xd0));
  214. nvgText(vg, startX + 28, y + 16, text, nullptr);
  215. }
  216. void drawOutputLine(NVGcontext* const vg, const uint offset, const char* const text)
  217. {
  218. const float y = startY_cv2 + offset * padding;
  219. nvgBeginPath(vg);
  220. nvgRoundedRect(vg, startX - 1.f, y - 2.f, box.size.x - startX * 2 + 2.f, 28.f, 4);
  221. nvgFillColor(vg, nvgRGB(0xd0, 0xd0, 0xd0));
  222. nvgFill(vg);
  223. nvgBeginPath(vg);
  224. nvgFillColor(vg, color::BLACK);
  225. nvgText(vg, startX + 28, y + 16, text, nullptr);
  226. }
  227. void draw(const DrawArgs& args) override
  228. {
  229. drawBackground(args.vg);
  230. nvgFontFaceId(args.vg, 0);
  231. nvgFontSize(args.vg, 14);
  232. drawInputLine(args.vg, 0, "Input");
  233. drawOutputLine(args.vg, 0, "Pitch");
  234. drawOutputLine(args.vg, 1, "Gate");
  235. nvgFontSize(args.vg, 11);
  236. nvgBeginPath(args.vg);
  237. nvgFillColor(args.vg, nvgRGB(0xd0, 0xd0, 0xd0));
  238. nvgTextLineHeight(args.vg, 0.8f);
  239. nvgTextAlign(args.vg, NVG_ALIGN_CENTER);
  240. nvgTextBox(args.vg, startX + 6.f, startY_cv2 + 75.f, 11.f, "S\ne\nn\ns", nullptr);
  241. nvgTextBox(args.vg, box.size.x - startX - 16.f, startY_cv2 + 130.f, 11.f, "T\no\nl", nullptr);
  242. nvgTextBox(args.vg, startX + 6.f, startY_cv2 + 175.f, 11.f, "T\nh\nr\ne\ns", nullptr);
  243. nvgBeginPath(args.vg);
  244. nvgRoundedRect(args.vg, 10.0f, startY_top, box.size.x - 20.f, 38.0f, 4);
  245. nvgFillColor(args.vg, color::BLACK);
  246. nvgFill(args.vg);
  247. ModuleWidgetWith9HP::draw(args);
  248. }
  249. void drawLayer(const DrawArgs& args, int layer) override
  250. {
  251. if (layer == 1)
  252. {
  253. nvgFontSize(args.vg, 17);
  254. nvgFillColor(args.vg, nvgRGBf(0.76f, 0.11f, 0.22f));
  255. char pitchConfString[24];
  256. char pitchFreqString[24];
  257. std::shared_ptr<Font> monoFont = APP->window->loadFont(monoFontPath);
  258. if (module != nullptr && monoFont != nullptr)
  259. {
  260. nvgFontFaceId(args.vg, monoFont->handle);
  261. std::snprintf(pitchConfString, sizeof(pitchConfString), "%5.1f %%", module->lastKnownPitchConfidence * 100.f);
  262. std::snprintf(pitchFreqString, sizeof(pitchFreqString), "%5.0f Hz", module->lastKnownPitchInHz);
  263. }
  264. else
  265. {
  266. std::strcpy(pitchConfString, "0.0 %");
  267. std::strcpy(pitchFreqString, "0 Hz");
  268. }
  269. nvgTextAlign(args.vg, NVG_ALIGN_CENTER);
  270. nvgText(args.vg, box.size.x * 0.5f, startY_top + 15.0f, pitchConfString, nullptr);
  271. nvgText(args.vg, box.size.x * 0.5f, startY_top + 33.0f, pitchFreqString, nullptr);
  272. }
  273. ModuleWidgetWith9HP::drawLayer(args, layer);
  274. }
  275. void appendContextMenu(Menu* const menu) override
  276. {
  277. menu->addChild(new MenuSeparator);
  278. menu->addChild(createBoolPtrMenuItem("Hold Output Pitch", "", &module->holdOutputPitch));
  279. menu->addChild(createBoolPtrMenuItem("Smooth Output Pitch", "", &module->smooth));
  280. static const std::vector<int> octaves = {-4, -3, -2, -1, 0, 1, 2, 3, 4};
  281. menu->addChild(createSubmenuItem("Octave", string::f("%d", module->octave), [=](Menu* menu) {
  282. for (size_t i = 0; i < octaves.size(); i++) {
  283. menu->addChild(createCheckMenuItem(string::f("%d", octaves[i]), "",
  284. [=]() {return module->octave == octaves[i];},
  285. [=]() {module->octave = octaves[i];}
  286. ));
  287. }
  288. }));
  289. }
  290. };
  291. #else
  292. struct AudioToCVPitchWidget : ModuleWidget {
  293. AudioToCVPitchWidget(AudioToCVPitch* const module) {
  294. setModule(module);
  295. addInput(createInput<PJ301MPort>({}, module, AudioToCVPitch::AUDIO_INPUT));
  296. addOutput(createOutput<PJ301MPort>({}, module, AudioToCVPitch::CV_PITCH));
  297. addOutput(createOutput<PJ301MPort>({}, module, AudioToCVPitch::CV_GATE));
  298. }
  299. };
  300. #endif
  301. // --------------------------------------------------------------------------------------------------------------------
  302. Model* modelAudioToCVPitch = createModel<AudioToCVPitch, AudioToCVPitchWidget>("AudioToCVPitch");
  303. // --------------------------------------------------------------------------------------------------------------------