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.

1318 lines
45KB

  1. /*
  2. * DISTRHO Cardinal Plugin
  3. * Copyright (C) 2021-2022 Filipe Coelho <falktx@falktx.com>
  4. *
  5. * This program is free software; you can redistribute it and/or
  6. * modify it under the terms of the GNU General Public License as
  7. * published by the Free Software Foundation; either version 3 of
  8. * the License, or any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * For a full copy of the GNU General Public License see the LICENSE file.
  16. */
  17. #include <asset.hpp>
  18. #include <library.hpp>
  19. #include <midi.hpp>
  20. #include <patch.hpp>
  21. #include <plugin.hpp>
  22. #include <random.hpp>
  23. #include <settings.hpp>
  24. #include <system.hpp>
  25. #include <app/Browser.hpp>
  26. #include <app/Scene.hpp>
  27. #include <engine/Engine.hpp>
  28. #include <ui/common.hpp>
  29. #include <window/Window.hpp>
  30. #ifdef NDEBUG
  31. # undef DEBUG
  32. #endif
  33. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  34. # include <lo/lo.h>
  35. # include "extra/Thread.hpp"
  36. #endif
  37. #include <list>
  38. #include "CardinalCommon.hpp"
  39. #include "DistrhoPluginUtils.hpp"
  40. #include "PluginContext.hpp"
  41. #include "extra/Base64.hpp"
  42. #ifdef DISTRHO_OS_WASM
  43. # include <emscripten/emscripten.h>
  44. #else
  45. # include "extra/SharedResourcePointer.hpp"
  46. #endif
  47. #if CARDINAL_VARIANT_FX
  48. # define CARDINAL_FACTORY_TEMPLATE_NAME "template-fx.vcv"
  49. #elif CARDINAL_VARIANT_SYNTH
  50. # define CARDINAL_FACTORY_TEMPLATE_NAME "template-synth.vcv"
  51. #else
  52. # define CARDINAL_FACTORY_TEMPLATE_NAME "template.vcv"
  53. #endif
  54. #ifdef DISTRHO_OS_WASM
  55. # define CARDINAL_TEMPLATE_NAME "template-wasm.vcv"
  56. #else
  57. # define CARDINAL_TEMPLATE_NAME CARDINAL_FACTORY_TEMPLATE_NAME
  58. #endif
  59. static const constexpr uint kCardinalStateBaseCount = 3; // patch, screenshot, comment
  60. #ifndef HEADLESS
  61. # include "extra/ScopedValueSetter.hpp"
  62. # include "WindowParameters.hpp"
  63. static const constexpr uint kCardinalStateCount = kCardinalStateBaseCount + 2; // moduleInfos, windowSize
  64. #else
  65. # define kWindowParameterCount 0
  66. static const constexpr uint kCardinalStateCount = kCardinalStateBaseCount;
  67. #endif
  68. namespace rack {
  69. namespace engine {
  70. void Engine_setAboutToClose(Engine*);
  71. }
  72. namespace plugin {
  73. void initStaticPlugins();
  74. void destroyStaticPlugins();
  75. }
  76. #ifndef HEADLESS
  77. namespace window {
  78. void WindowInit(Window* window, DISTRHO_NAMESPACE::Plugin* plugin);
  79. }
  80. #endif
  81. }
  82. START_NAMESPACE_DISTRHO
  83. template<typename T>
  84. static inline
  85. bool d_isDiffHigherThanLimit(const T& v1, const T& v2, const T& limit)
  86. {
  87. return v1 != v2 ? (v1 > v2 ? v1 - v2 : v2 - v1) > limit : false;
  88. }
  89. // -----------------------------------------------------------------------------------------------------------
  90. #ifdef DISTRHO_OS_WASM
  91. static char* getPatchStorageSlug() {
  92. return static_cast<char*>(EM_ASM_PTR({
  93. var searchParams = new URLSearchParams(window.location.search);
  94. var patch = searchParams.get('patchstorage');
  95. if (!patch)
  96. return null;
  97. var length = lengthBytesUTF8(patch) + 1;
  98. var str = _malloc(length);
  99. stringToUTF8(patch, str, length);
  100. return str;
  101. }));
  102. };
  103. #endif
  104. // -----------------------------------------------------------------------------------------------------------
  105. struct Initializer
  106. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  107. : public Thread
  108. #endif
  109. {
  110. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  111. lo_server oscServer = nullptr;
  112. CardinalBasePlugin* oscPlugin = nullptr;
  113. #endif
  114. std::string templatePath;
  115. std::string factoryTemplatePath;
  116. Initializer(const CardinalBasePlugin* const plugin)
  117. {
  118. using namespace rack;
  119. #ifdef DISTRHO_OS_WASM
  120. settings::allowCursorLock = true;
  121. #else
  122. settings::allowCursorLock = false;
  123. #endif
  124. settings::autoCheckUpdates = false;
  125. settings::autosaveInterval = 0;
  126. settings::devMode = true;
  127. settings::isPlugin = true;
  128. settings::skipLoadOnLaunch = true;
  129. settings::showTipsOnLaunch = false;
  130. settings::windowPos = math::Vec(0, 0);
  131. #ifdef HEADLESS
  132. settings::headless = true;
  133. #endif
  134. // copied from https://community.vcvrack.com/t/16-colour-cable-palette/15951
  135. settings::cableColors = {
  136. color::fromHexString("#ff5252"),
  137. color::fromHexString("#ff9352"),
  138. color::fromHexString("#ffd452"),
  139. color::fromHexString("#e8ff52"),
  140. color::fromHexString("#a8ff52"),
  141. color::fromHexString("#67ff52"),
  142. color::fromHexString("#52ff7d"),
  143. color::fromHexString("#52ffbe"),
  144. color::fromHexString("#52ffff"),
  145. color::fromHexString("#52beff"),
  146. color::fromHexString("#527dff"),
  147. color::fromHexString("#6752ff"),
  148. color::fromHexString("#a852ff"),
  149. color::fromHexString("#e952ff"),
  150. color::fromHexString("#ff52d4"),
  151. color::fromHexString("#ff5293"),
  152. };
  153. system::init();
  154. logger::init();
  155. random::init();
  156. ui::init();
  157. if (asset::systemDir.empty())
  158. {
  159. if (const char* const bundlePath = plugin->getBundlePath())
  160. {
  161. if (const char* const resourcePath = getResourcePath(bundlePath))
  162. {
  163. asset::bundlePath = system::join(resourcePath, "PluginManifests");
  164. asset::systemDir = resourcePath;
  165. templatePath = system::join(asset::systemDir, CARDINAL_TEMPLATE_NAME);
  166. factoryTemplatePath = system::join(asset::systemDir, CARDINAL_FACTORY_TEMPLATE_NAME);
  167. }
  168. }
  169. if (asset::systemDir.empty() || ! system::exists(asset::systemDir))
  170. {
  171. #ifdef CARDINAL_PLUGIN_SOURCE_DIR
  172. // Make system dir point to source code location as fallback
  173. asset::systemDir = CARDINAL_PLUGIN_SOURCE_DIR DISTRHO_OS_SEP_STR "Rack";
  174. if (system::exists(system::join(asset::systemDir, "res")))
  175. {
  176. templatePath = CARDINAL_PLUGIN_SOURCE_DIR DISTRHO_OS_SEP_STR CARDINAL_TEMPLATE_NAME;
  177. factoryTemplatePath = CARDINAL_PLUGIN_SOURCE_DIR DISTRHO_OS_SEP_STR CARDINAL_FACTORY_TEMPLATE_NAME;
  178. }
  179. // If source code dir does not exist use install target prefix as system dir
  180. else
  181. #endif
  182. {
  183. #if defined(DISTRHO_OS_WASM)
  184. asset::systemDir = "/resources";
  185. #elif defined(ARCH_MAC)
  186. asset::systemDir = "/Library/Application Support/Cardinal";
  187. #elif defined(ARCH_WIN)
  188. const std::string commonprogfiles = getSpecialPath(kSpecialPathCommonProgramFiles);
  189. if (! commonprogfiles.empty())
  190. asset::systemDir = system::join(commonprogfiles, "Cardinal");
  191. #else
  192. asset::systemDir = CARDINAL_PLUGIN_PREFIX "/share/cardinal";
  193. #endif
  194. asset::bundlePath = system::join(asset::systemDir, "PluginManifests");
  195. templatePath = system::join(asset::systemDir, CARDINAL_TEMPLATE_NAME);
  196. factoryTemplatePath = system::join(asset::systemDir, CARDINAL_FACTORY_TEMPLATE_NAME);
  197. }
  198. }
  199. asset::userDir = asset::systemDir;
  200. }
  201. #ifdef DISTRHO_OS_WASM
  202. if ((patchStorageSlug = getPatchStorageSlug()) != nullptr)
  203. templatePath = CARDINAL_IMPORTED_TEMPLATE_FILENAME;
  204. #endif
  205. // Log environment
  206. INFO("%s %s v%s", APP_NAME.c_str(), APP_EDITION.c_str(), APP_VERSION.c_str());
  207. INFO("%s", system::getOperatingSystemInfo().c_str());
  208. INFO("Binary filename: %s", getBinaryFilename());
  209. INFO("Bundle path: %s", plugin->getBundlePath());
  210. INFO("System directory: %s", asset::systemDir.c_str());
  211. INFO("User directory: %s", asset::userDir.c_str());
  212. INFO("Template patch: %s", templatePath.c_str());
  213. INFO("System template patch: %s", factoryTemplatePath.c_str());
  214. // Report to user if something is wrong with the installation
  215. if (asset::systemDir.empty())
  216. {
  217. d_stderr2("Failed to locate Cardinal plugin bundle.\n"
  218. "Install Cardinal with its bundle folder intact and try again.");
  219. }
  220. else if (! system::exists(asset::systemDir))
  221. {
  222. d_stderr2("System directory \"%s\" does not exist.\n"
  223. "Make sure Cardinal was downloaded and installed correctly.", asset::systemDir.c_str());
  224. }
  225. INFO("Initializing plugins");
  226. plugin::initStaticPlugins();
  227. INFO("Initializing plugin browser DB");
  228. app::browserInit();
  229. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  230. INFO("Initializing OSC Remote control");
  231. oscServer = lo_server_new_with_proto(REMOTE_HOST_PORT, LO_UDP, osc_error_handler);
  232. DISTRHO_SAFE_ASSERT_RETURN(oscServer != nullptr,);
  233. lo_server_add_method(oscServer, "/hello", "", osc_hello_handler, this);
  234. lo_server_add_method(oscServer, "/load", "b", osc_load_handler, this);
  235. lo_server_add_method(oscServer, "/screenshot", "b", osc_screenshot_handler, this);
  236. lo_server_add_method(oscServer, nullptr, nullptr, osc_fallback_handler, nullptr);
  237. startThread();
  238. #elif defined(HEADLESS)
  239. INFO("OSC Remote control is not enabled in this build");
  240. #endif
  241. }
  242. ~Initializer()
  243. {
  244. using namespace rack;
  245. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  246. if (oscServer != nullptr)
  247. {
  248. stopThread(5000);
  249. lo_server_del_method(oscServer, nullptr, nullptr);
  250. lo_server_free(oscServer);
  251. oscServer = nullptr;
  252. }
  253. #endif
  254. INFO("Clearing asset paths");
  255. asset::bundlePath.clear();
  256. asset::systemDir.clear();
  257. asset::userDir.clear();
  258. INFO("Destroying plugins");
  259. plugin::destroyStaticPlugins();
  260. INFO("Destroying settings");
  261. settings::destroy();
  262. INFO("Destroying logger");
  263. logger::destroy();
  264. }
  265. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  266. void run() override
  267. {
  268. INFO("OSC Thread Listening for remote commands");
  269. while (! shouldThreadExit())
  270. {
  271. d_msleep(200);
  272. while (lo_server_recv_noblock(oscServer, 0) != 0) {}
  273. }
  274. INFO("OSC Thread Closed");
  275. }
  276. static void osc_error_handler(int num, const char* msg, const char* path)
  277. {
  278. d_stderr("Cardinal OSC Error: code: %i, msg: \"%s\", path: \"%s\")", num, msg, path);
  279. }
  280. static int osc_fallback_handler(const char* const path, const char* const types, lo_arg**, int, lo_message, void*)
  281. {
  282. d_stderr("Cardinal OSC unhandled message \"%s\" with types \"%s\"", path, types);
  283. return 0;
  284. }
  285. static int osc_hello_handler(const char*, const char*, lo_arg**, int, const lo_message m, void* const self)
  286. {
  287. d_stdout("osc_hello_handler()");
  288. const lo_address source = lo_message_get_source(m);
  289. lo_send_from(source, static_cast<Initializer*>(self)->oscServer, LO_TT_IMMEDIATE, "/resp", "ss", "hello", "ok");
  290. return 0;
  291. }
  292. static int osc_load_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self)
  293. {
  294. d_stdout("osc_load_handler()");
  295. DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0);
  296. DISTRHO_SAFE_ASSERT_RETURN(types != nullptr && types[0] == 'b', 0);
  297. const int32_t size = argv[0]->blob.size;
  298. DISTRHO_SAFE_ASSERT_RETURN(size > 4, 0);
  299. const uint8_t* const blob = (uint8_t*)(&argv[0]->blob.data);
  300. DISTRHO_SAFE_ASSERT_RETURN(blob != nullptr, 0);
  301. bool ok = false;
  302. if (CardinalBasePlugin* const plugin = static_cast<Initializer*>(self)->oscPlugin)
  303. {
  304. CardinalPluginContext* const context = plugin->context;
  305. std::vector<uint8_t> data(size);
  306. std::memcpy(data.data(), blob, size);
  307. rack::contextSet(context);
  308. rack::system::removeRecursively(context->patch->autosavePath);
  309. rack::system::createDirectories(context->patch->autosavePath);
  310. try {
  311. rack::system::unarchiveToDirectory(data, context->patch->autosavePath);
  312. context->patch->loadAutosave();
  313. ok = true;
  314. }
  315. catch (rack::Exception& e) {
  316. WARN("%s", e.what());
  317. }
  318. rack::contextSet(nullptr);
  319. }
  320. const lo_address source = lo_message_get_source(m);
  321. lo_send_from(source, static_cast<Initializer*>(self)->oscServer,
  322. LO_TT_IMMEDIATE, "/resp", "ss", "load", ok ? "ok" : "fail");
  323. return 0;
  324. }
  325. static int osc_screenshot_handler(const char*, const char* types, lo_arg** argv, int argc, const lo_message m, void* const self)
  326. {
  327. d_stdout("osc_screenshot_handler()");
  328. DISTRHO_SAFE_ASSERT_RETURN(argc == 1, 0);
  329. DISTRHO_SAFE_ASSERT_RETURN(types != nullptr && types[0] == 'b', 0);
  330. const int32_t size = argv[0]->blob.size;
  331. DISTRHO_SAFE_ASSERT_RETURN(size > 4, 0);
  332. const uint8_t* const blob = (uint8_t*)(&argv[0]->blob.data);
  333. DISTRHO_SAFE_ASSERT_RETURN(blob != nullptr, 0);
  334. bool ok = false;
  335. if (CardinalBasePlugin* const plugin = static_cast<Initializer*>(self)->oscPlugin)
  336. ok = plugin->updateStateValue("screenshot", String::asBase64(blob, size).buffer());
  337. const lo_address source = lo_message_get_source(m);
  338. lo_send_from(source, static_cast<Initializer*>(self)->oscServer,
  339. LO_TT_IMMEDIATE, "/resp", "ss", "screenshot", ok ? "ok" : "fail");
  340. return 0;
  341. }
  342. #endif
  343. };
  344. // -----------------------------------------------------------------------------------------------------------
  345. void CardinalPluginContext::writeMidiMessage(const rack::midi::Message& message, const uint8_t channel)
  346. {
  347. if (bypassed)
  348. return;
  349. const size_t size = message.bytes.size();
  350. DISTRHO_SAFE_ASSERT_RETURN(size > 0,);
  351. DISTRHO_SAFE_ASSERT_RETURN(message.frame >= 0,);
  352. MidiEvent event;
  353. event.frame = message.frame;
  354. switch (message.bytes[0] & 0xF0)
  355. {
  356. case 0x80:
  357. case 0x90:
  358. case 0xA0:
  359. case 0xB0:
  360. case 0xE0:
  361. event.size = 3;
  362. break;
  363. case 0xC0:
  364. case 0xD0:
  365. event.size = 2;
  366. break;
  367. case 0xF0:
  368. switch (message.bytes[0] & 0x0F)
  369. {
  370. case 0x0:
  371. case 0x4:
  372. case 0x5:
  373. case 0x7:
  374. case 0x9:
  375. case 0xD:
  376. // unsupported
  377. return;
  378. case 0x1:
  379. case 0x2:
  380. case 0x3:
  381. case 0xE:
  382. event.size = 3;
  383. break;
  384. case 0x6:
  385. case 0x8:
  386. case 0xA:
  387. case 0xB:
  388. case 0xC:
  389. case 0xF:
  390. event.size = 1;
  391. break;
  392. }
  393. break;
  394. default:
  395. // invalid
  396. return;
  397. }
  398. DISTRHO_SAFE_ASSERT_RETURN(size >= event.size,);
  399. std::memcpy(event.data, message.bytes.data(), event.size);
  400. if (channel != 0 && event.data[0] < 0xF0)
  401. event.data[0] |= channel & 0x0F;
  402. plugin->writeMidiEvent(event);
  403. }
  404. // -----------------------------------------------------------------------------------------------------------
  405. struct ScopedContext {
  406. ScopedContext(const CardinalBasePlugin* const plugin)
  407. {
  408. rack::contextSet(plugin->context);
  409. }
  410. ~ScopedContext()
  411. {
  412. rack::contextSet(nullptr);
  413. }
  414. };
  415. // -----------------------------------------------------------------------------------------------------------
  416. class CardinalPlugin : public CardinalBasePlugin
  417. {
  418. #ifdef DISTRHO_OS_WASM
  419. ScopedPointer<Initializer> fInitializer;
  420. #else
  421. SharedResourcePointer<Initializer> fInitializer;
  422. #endif
  423. #if DISTRHO_PLUGIN_NUM_INPUTS != 0
  424. /* If host audio ins == outs we can get issues for inplace processing.
  425. * So allocate a float array that will serve as safe copy for those cases.
  426. */
  427. float** fAudioBufferCopy;
  428. #endif
  429. std::string fAutosavePath;
  430. uint64_t fNextExpectedFrame;
  431. struct {
  432. String comment;
  433. String screenshot;
  434. #ifndef HEADLESS
  435. String windowSize;
  436. #endif
  437. } fState;
  438. // bypass handling
  439. bool fWasBypassed;
  440. MidiEvent bypassMidiEvents[16];
  441. #ifndef HEADLESS
  442. // real values, not VCV interpreted ones
  443. float fWindowParameters[kWindowParameterCount];
  444. #endif
  445. public:
  446. CardinalPlugin()
  447. : CardinalBasePlugin(kModuleParameters + kWindowParameterCount + 1, 0, kCardinalStateCount),
  448. #ifdef DISTRHO_OS_WASM
  449. fInitializer(new Initializer(this)),
  450. #else
  451. fInitializer(this),
  452. #endif
  453. #if DISTRHO_PLUGIN_NUM_INPUTS != 0
  454. fAudioBufferCopy(nullptr),
  455. #endif
  456. fNextExpectedFrame(0),
  457. fWasBypassed(false)
  458. {
  459. #ifndef HEADLESS
  460. fWindowParameters[kWindowParameterShowTooltips] = 1.0f;
  461. fWindowParameters[kWindowParameterCableOpacity] = 50.0f;
  462. fWindowParameters[kWindowParameterCableTension] = 75.0f;
  463. fWindowParameters[kWindowParameterRackBrightness] = 100.0f;
  464. fWindowParameters[kWindowParameterHaloBrightness] = 25.0f;
  465. fWindowParameters[kWindowParameterKnobMode] = 0.0f;
  466. fWindowParameters[kWindowParameterWheelKnobControl] = 0.0f;
  467. fWindowParameters[kWindowParameterWheelSensitivity] = 1.0f;
  468. fWindowParameters[kWindowParameterLockModulePositions] = 0.0f;
  469. fWindowParameters[kWindowParameterUpdateRateLimit] = 0.0f;
  470. fWindowParameters[kWindowParameterBrowserSort] = 3.0f;
  471. fWindowParameters[kWindowParameterBrowserZoom] = 50.0f;
  472. fWindowParameters[kWindowParameterInvertZoom] = 0.0f;
  473. fWindowParameters[kWindowParameterSqueezeModulePositions] = 1.0f;
  474. #endif
  475. // create unique temporary path for this instance
  476. try {
  477. char uidBuf[24];
  478. const std::string tmp = rack::system::getTempDirectory();
  479. for (int i=1;; ++i)
  480. {
  481. std::snprintf(uidBuf, sizeof(uidBuf), "Cardinal.%04d", i);
  482. const std::string trypath = rack::system::join(tmp, uidBuf);
  483. if (! rack::system::exists(trypath))
  484. {
  485. if (rack::system::createDirectories(trypath))
  486. fAutosavePath = trypath;
  487. break;
  488. }
  489. }
  490. } DISTRHO_SAFE_EXCEPTION("create unique temporary path");
  491. // initialize midi events used when entering bypassed state
  492. std::memset(bypassMidiEvents, 0, sizeof(bypassMidiEvents));
  493. for (uint8_t i=0; i<16; ++i)
  494. {
  495. bypassMidiEvents[i].size = 3;
  496. bypassMidiEvents[i].data[0] = 0xB0 + i;
  497. bypassMidiEvents[i].data[1] = 0x7B;
  498. }
  499. const float sampleRate = getSampleRate();
  500. rack::settings::sampleRate = sampleRate;
  501. context->bufferSize = getBufferSize();
  502. context->sampleRate = sampleRate;
  503. const ScopedContext sc(this);
  504. context->engine = new rack::engine::Engine;
  505. context->engine->setSampleRate(sampleRate);
  506. context->history = new rack::history::State;
  507. context->patch = new rack::patch::Manager;
  508. context->patch->autosavePath = fAutosavePath;
  509. context->patch->templatePath = fInitializer->templatePath;
  510. context->patch->factoryTemplatePath = fInitializer->factoryTemplatePath;
  511. context->event = new rack::widget::EventState;
  512. context->scene = new rack::app::Scene;
  513. context->event->rootWidget = context->scene;
  514. if (! isDummyInstance())
  515. context->window = new rack::window::Window;
  516. #ifdef DISTRHO_OS_WASM
  517. if (rack::patchStorageSlug == nullptr)
  518. #endif
  519. {
  520. context->patch->loadTemplate();
  521. context->scene->rackScroll->reset();
  522. }
  523. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  524. fInitializer->oscPlugin = this;
  525. #endif
  526. }
  527. ~CardinalPlugin() override
  528. {
  529. #if defined(HAVE_LIBLO) && defined(HEADLESS)
  530. fInitializer->oscPlugin = nullptr;
  531. #endif
  532. {
  533. const ScopedContext sc(this);
  534. context->patch->clear();
  535. // do a little dance to prevent context scene deletion from saving to temp dir
  536. #ifndef HEADLESS
  537. const ScopedValueSetter<bool> svs(rack::settings::headless, true);
  538. #endif
  539. Engine_setAboutToClose(context->engine);
  540. delete context;
  541. }
  542. if (! fAutosavePath.empty())
  543. rack::system::removeRecursively(fAutosavePath);
  544. }
  545. CardinalPluginContext* getRackContext() const noexcept
  546. {
  547. return context;
  548. }
  549. protected:
  550. /* --------------------------------------------------------------------------------------------------------
  551. * Information */
  552. const char* getLabel() const override
  553. {
  554. return DISTRHO_PLUGIN_LABEL;
  555. }
  556. const char* getDescription() const override
  557. {
  558. return ""
  559. "Cardinal is a free and open-source virtual modular synthesizer plugin.\n"
  560. "It is based on the popular VCV Rack but with a focus on being a fully self-contained plugin version.\n"
  561. "It is not an official VCV project, and it is not affiliated with it in any way.\n"
  562. "\n"
  563. "Cardinal contains Rack, some 3rd-party modules and a few internal utilities.\n"
  564. "It does not load external modules and does not connect to the official Rack library/store.\n";
  565. }
  566. const char* getMaker() const override
  567. {
  568. return "DISTRHO";
  569. }
  570. const char* getHomePage() const override
  571. {
  572. return "https://github.com/DISTRHO/Cardinal";
  573. }
  574. const char* getLicense() const override
  575. {
  576. return "GPLv3+";
  577. }
  578. uint32_t getVersion() const override
  579. {
  580. return d_version(0, 22, 6);
  581. }
  582. int64_t getUniqueId() const override
  583. {
  584. #if CARDINAL_VARIANT_MAIN
  585. return d_cconst('d', 'C', 'd', 'n');
  586. #elif CARDINAL_VARIANT_FX
  587. return d_cconst('d', 'C', 'n', 'F');
  588. #elif CARDINAL_VARIANT_SYNTH
  589. return d_cconst('d', 'C', 'n', 'S');
  590. #else
  591. #error cardinal variant not set
  592. #endif
  593. }
  594. /* --------------------------------------------------------------------------------------------------------
  595. * Init */
  596. void initAudioPort(const bool input, uint32_t index, AudioPort& port) override
  597. {
  598. #if CARDINAL_VARIANT_FX || CARDINAL_VARIANT_SYNTH
  599. if (index < 2)
  600. port.groupId = kPortGroupStereo;
  601. #endif
  602. if (index >= 8)
  603. {
  604. port.hints = kAudioPortIsCV | kCVPortHasPositiveUnipolarRange | kCVPortHasScaledRange;
  605. index -= 8;
  606. }
  607. CardinalBasePlugin::initAudioPort(input, index, port);
  608. }
  609. void initParameter(const uint32_t index, Parameter& parameter) override
  610. {
  611. if (index < kModuleParameters)
  612. {
  613. parameter.name = "Parameter ";
  614. parameter.name += String(index + 1);
  615. parameter.symbol = "param_";
  616. parameter.symbol += String(index + 1);
  617. parameter.unit = "v";
  618. parameter.hints = kParameterIsAutomatable;
  619. parameter.ranges.def = 0.0f;
  620. parameter.ranges.min = 0.0f;
  621. parameter.ranges.max = 10.0f;
  622. return;
  623. }
  624. if (index == kModuleParameters)
  625. {
  626. parameter.initDesignation(kParameterDesignationBypass);
  627. return;
  628. }
  629. #ifndef HEADLESS
  630. switch (index - kModuleParameters - 1)
  631. {
  632. case kWindowParameterShowTooltips:
  633. parameter.name = "Show tooltips";
  634. parameter.symbol = "tooltips";
  635. parameter.hints = kParameterIsAutomatable|kParameterIsInteger|kParameterIsBoolean;
  636. parameter.ranges.def = 1.0f;
  637. parameter.ranges.min = 0.0f;
  638. parameter.ranges.max = 1.0f;
  639. break;
  640. case kWindowParameterCableOpacity:
  641. parameter.name = "Cable opacity";
  642. parameter.symbol = "cableOpacity";
  643. parameter.unit = "%";
  644. parameter.hints = kParameterIsAutomatable;
  645. parameter.ranges.def = 50.0f;
  646. parameter.ranges.min = 0.0f;
  647. parameter.ranges.max = 100.0f;
  648. break;
  649. case kWindowParameterCableTension:
  650. parameter.name = "Cable tension";
  651. parameter.symbol = "cableTension";
  652. parameter.unit = "%";
  653. parameter.hints = kParameterIsAutomatable;
  654. parameter.ranges.def = 75.0f;
  655. parameter.ranges.min = 0.0f;
  656. parameter.ranges.max = 100.0f;
  657. break;
  658. case kWindowParameterRackBrightness:
  659. parameter.name = "Room brightness";
  660. parameter.symbol = "rackBrightness";
  661. parameter.unit = "%";
  662. parameter.hints = kParameterIsAutomatable;
  663. parameter.ranges.def = 100.0f;
  664. parameter.ranges.min = 0.0f;
  665. parameter.ranges.max = 100.0f;
  666. break;
  667. case kWindowParameterHaloBrightness:
  668. parameter.name = "Light Bloom";
  669. parameter.symbol = "haloBrightness";
  670. parameter.unit = "%";
  671. parameter.hints = kParameterIsAutomatable;
  672. parameter.ranges.def = 25.0f;
  673. parameter.ranges.min = 0.0f;
  674. parameter.ranges.max = 100.0f;
  675. break;
  676. case kWindowParameterKnobMode:
  677. parameter.name = "Knob mode";
  678. parameter.symbol = "knobMode";
  679. parameter.hints = kParameterIsAutomatable|kParameterIsInteger;
  680. parameter.ranges.def = 0.0f;
  681. parameter.ranges.min = 0.0f;
  682. parameter.ranges.max = 2.0f;
  683. parameter.enumValues.count = 3;
  684. parameter.enumValues.restrictedMode = true;
  685. parameter.enumValues.values = new ParameterEnumerationValue[3];
  686. parameter.enumValues.values[0].label = "Linear";
  687. parameter.enumValues.values[0].value = 0.0f;
  688. parameter.enumValues.values[1].label = "Absolute rotary";
  689. parameter.enumValues.values[1].value = 1.0f;
  690. parameter.enumValues.values[2].label = "Relative rotary";
  691. parameter.enumValues.values[2].value = 2.0f;
  692. break;
  693. case kWindowParameterWheelKnobControl:
  694. parameter.name = "Scroll wheel knob control";
  695. parameter.symbol = "knobScroll";
  696. parameter.hints = kParameterIsAutomatable|kParameterIsInteger|kParameterIsBoolean;
  697. parameter.ranges.def = 0.0f;
  698. parameter.ranges.min = 0.0f;
  699. parameter.ranges.max = 1.0f;
  700. break;
  701. case kWindowParameterWheelSensitivity:
  702. parameter.name = "Scroll wheel knob sensitivity";
  703. parameter.symbol = "knobScrollSensitivity";
  704. parameter.hints = kParameterIsAutomatable|kParameterIsLogarithmic;
  705. parameter.ranges.def = 1.0f;
  706. parameter.ranges.min = 0.1f;
  707. parameter.ranges.max = 10.0f;
  708. break;
  709. case kWindowParameterLockModulePositions:
  710. parameter.name = "Lock module positions";
  711. parameter.symbol = "lockModules";
  712. parameter.hints = kParameterIsAutomatable|kParameterIsInteger|kParameterIsBoolean;
  713. parameter.ranges.def = 0.0f;
  714. parameter.ranges.min = 0.0f;
  715. parameter.ranges.max = 1.0f;
  716. break;
  717. case kWindowParameterUpdateRateLimit:
  718. parameter.name = "Update rate limit";
  719. parameter.symbol = "rateLimit";
  720. parameter.hints = kParameterIsAutomatable|kParameterIsInteger;
  721. parameter.ranges.def = 0.0f;
  722. parameter.ranges.min = 0.0f;
  723. parameter.ranges.max = 2.0f;
  724. parameter.enumValues.count = 3;
  725. parameter.enumValues.restrictedMode = true;
  726. parameter.enumValues.values = new ParameterEnumerationValue[3];
  727. parameter.enumValues.values[0].label = "None";
  728. parameter.enumValues.values[0].value = 0.0f;
  729. parameter.enumValues.values[1].label = "2x";
  730. parameter.enumValues.values[1].value = 1.0f;
  731. parameter.enumValues.values[2].label = "4x";
  732. parameter.enumValues.values[2].value = 2.0f;
  733. break;
  734. case kWindowParameterBrowserSort:
  735. parameter.name = "Browser sort";
  736. parameter.symbol = "browserSort";
  737. parameter.hints = kParameterIsAutomatable|kParameterIsInteger;
  738. parameter.ranges.def = 3.0f;
  739. parameter.ranges.min = 0.0f;
  740. parameter.ranges.max = 5.0f;
  741. parameter.enumValues.count = 6;
  742. parameter.enumValues.restrictedMode = true;
  743. parameter.enumValues.values = new ParameterEnumerationValue[6];
  744. parameter.enumValues.values[0].label = "Updated";
  745. parameter.enumValues.values[0].value = 0.0f;
  746. parameter.enumValues.values[1].label = "Last used";
  747. parameter.enumValues.values[1].value = 1.0f;
  748. parameter.enumValues.values[2].label = "Most used";
  749. parameter.enumValues.values[2].value = 2.0f;
  750. parameter.enumValues.values[3].label = "Brand";
  751. parameter.enumValues.values[3].value = 3.0f;
  752. parameter.enumValues.values[4].label = "Name";
  753. parameter.enumValues.values[4].value = 4.0f;
  754. parameter.enumValues.values[5].label = "Random";
  755. parameter.enumValues.values[5].value = 5.0f;
  756. break;
  757. case kWindowParameterBrowserZoom:
  758. parameter.name = "Browser zoom";
  759. parameter.symbol = "browserZoom";
  760. parameter.hints = kParameterIsAutomatable;
  761. parameter.unit = "%";
  762. parameter.ranges.def = 50.0f;
  763. parameter.ranges.min = 25.0f;
  764. parameter.ranges.max = 200.0f;
  765. parameter.enumValues.count = 7;
  766. parameter.enumValues.restrictedMode = true;
  767. parameter.enumValues.values = new ParameterEnumerationValue[7];
  768. parameter.enumValues.values[0].label = "25";
  769. parameter.enumValues.values[0].value = 25.0f;
  770. parameter.enumValues.values[1].label = "35";
  771. parameter.enumValues.values[1].value = 35.0f;
  772. parameter.enumValues.values[2].label = "50";
  773. parameter.enumValues.values[2].value = 50.0f;
  774. parameter.enumValues.values[3].label = "71";
  775. parameter.enumValues.values[3].value = 71.0f;
  776. parameter.enumValues.values[4].label = "100";
  777. parameter.enumValues.values[4].value = 100.0f;
  778. parameter.enumValues.values[5].label = "141";
  779. parameter.enumValues.values[5].value = 141.0f;
  780. parameter.enumValues.values[6].label = "200";
  781. parameter.enumValues.values[6].value = 200.0f;
  782. break;
  783. case kWindowParameterInvertZoom:
  784. parameter.name = "Invert zoom";
  785. parameter.symbol = "invertZoom";
  786. parameter.hints = kParameterIsAutomatable|kParameterIsInteger|kParameterIsBoolean;
  787. parameter.ranges.def = 0.0f;
  788. parameter.ranges.min = 0.0f;
  789. parameter.ranges.max = 1.0f;
  790. break;
  791. case kWindowParameterSqueezeModulePositions:
  792. parameter.name = "Auto-squeeze module positions";
  793. parameter.symbol = "squeezeModules";
  794. parameter.hints = kParameterIsAutomatable|kParameterIsInteger|kParameterIsBoolean;
  795. parameter.ranges.def = 1.0f;
  796. parameter.ranges.min = 0.0f;
  797. parameter.ranges.max = 1.0f;
  798. break;
  799. }
  800. #endif
  801. }
  802. void initState(const uint32_t index, State& state) override
  803. {
  804. switch (index)
  805. {
  806. case 0:
  807. state.hints = kStateIsBase64Blob | kStateIsOnlyForDSP;
  808. state.key = "patch";
  809. state.label = "Patch";
  810. break;
  811. case 1:
  812. state.hints = kStateIsHostReadable | kStateIsBase64Blob;
  813. state.key = "screenshot";
  814. state.label = "Screenshot";
  815. break;
  816. case 2:
  817. state.hints = kStateIsHostWritable;
  818. state.key = "comment";
  819. state.label = "Comment";
  820. break;
  821. case 3:
  822. state.hints = kStateIsOnlyForUI;
  823. state.key = "moduleInfos";
  824. state.label = "moduleInfos";
  825. break;
  826. case 4:
  827. state.hints = kStateIsOnlyForUI;
  828. state.key = "windowSize";
  829. state.label = "Window size";
  830. break;
  831. }
  832. }
  833. /* --------------------------------------------------------------------------------------------------------
  834. * Internal data */
  835. float getParameterValue(uint32_t index) const override
  836. {
  837. // host mapped parameters
  838. if (index < kModuleParameters)
  839. return context->parameters[index];
  840. // bypass
  841. if (index == kModuleParameters)
  842. return context->bypassed ? 1.0f : 0.0f;
  843. #ifndef HEADLESS
  844. // window related parameters
  845. index -= kModuleParameters + 1;
  846. if (index < kWindowParameterCount)
  847. return fWindowParameters[index];
  848. #endif
  849. return 0.0f;
  850. }
  851. void setParameterValue(uint32_t index, float value) override
  852. {
  853. // host mapped parameters
  854. if (index < kModuleParameters)
  855. {
  856. context->parameters[index] = value;
  857. return;
  858. }
  859. // bypass
  860. if (index == kModuleParameters)
  861. {
  862. context->bypassed = value > 0.5f;
  863. return;
  864. }
  865. #ifndef HEADLESS
  866. // window related parameters
  867. index -= kModuleParameters + 1;
  868. if (index < kWindowParameterCount)
  869. {
  870. fWindowParameters[index] = value;
  871. return;
  872. }
  873. #endif
  874. }
  875. String getState(const char* const key) const override
  876. {
  877. #ifndef HEADLESS
  878. if (std::strcmp(key, "moduleInfos") == 0)
  879. {
  880. json_t* const rootJ = json_object();
  881. DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr, String());
  882. for (const auto& pluginPair : rack::settings::moduleInfos)
  883. {
  884. json_t* const pluginJ = json_object();
  885. DISTRHO_SAFE_ASSERT_CONTINUE(pluginJ != nullptr);
  886. for (const auto& modulePair : pluginPair.second)
  887. {
  888. json_t* const moduleJ = json_object();
  889. DISTRHO_SAFE_ASSERT_CONTINUE(moduleJ != nullptr);
  890. const rack::settings::ModuleInfo& m(modulePair.second);
  891. // To make setting.json smaller, only set properties if not default values.
  892. if (m.favorite)
  893. json_object_set_new(moduleJ, "favorite", json_boolean(m.favorite));
  894. if (m.added > 0)
  895. json_object_set_new(moduleJ, "added", json_integer(m.added));
  896. if (std::isfinite(m.lastAdded))
  897. json_object_set_new(moduleJ, "lastAdded", json_real(m.lastAdded));
  898. if (json_object_size(moduleJ))
  899. json_object_set_new(pluginJ, modulePair.first.c_str(), moduleJ);
  900. else
  901. json_decref(moduleJ);
  902. }
  903. if (json_object_size(pluginJ))
  904. json_object_set_new(rootJ, pluginPair.first.c_str(), pluginJ);
  905. else
  906. json_decref(pluginJ);
  907. }
  908. const String info(json_dumps(rootJ, JSON_COMPACT), false);
  909. json_decref(rootJ);
  910. return info;
  911. }
  912. if (std::strcmp(key, "windowSize") == 0)
  913. return fState.windowSize;
  914. #endif
  915. if (std::strcmp(key, "comment") == 0)
  916. return fState.comment;
  917. if (std::strcmp(key, "screenshot") == 0)
  918. return fState.screenshot;
  919. if (std::strcmp(key, "patch") != 0)
  920. return String();
  921. if (fAutosavePath.empty())
  922. return String();
  923. std::vector<uint8_t> data;
  924. {
  925. const ScopedContext sc(this);
  926. context->engine->prepareSave();
  927. context->patch->saveAutosave();
  928. context->patch->cleanAutosave();
  929. // context->history->setSaved();
  930. try {
  931. data = rack::system::archiveDirectory(fAutosavePath, 1);
  932. } DISTRHO_SAFE_EXCEPTION_RETURN("getState archiveDirectory", String());
  933. }
  934. return String::asBase64(data.data(), data.size());
  935. }
  936. void setState(const char* const key, const char* const value) override
  937. {
  938. #ifndef HEADLESS
  939. if (std::strcmp(key, "moduleInfos") == 0)
  940. {
  941. json_error_t error;
  942. json_t* const rootJ = json_loads(value, 0, &error);
  943. DISTRHO_SAFE_ASSERT_RETURN(rootJ != nullptr,);
  944. const char* pluginSlug;
  945. json_t* pluginJ;
  946. json_object_foreach(rootJ, pluginSlug, pluginJ)
  947. {
  948. const char* moduleSlug;
  949. json_t* moduleJ;
  950. json_object_foreach(pluginJ, moduleSlug, moduleJ)
  951. {
  952. rack::settings::ModuleInfo m;
  953. if (json_t* const favoriteJ = json_object_get(moduleJ, "favorite"))
  954. m.favorite = json_boolean_value(favoriteJ);
  955. if (json_t* const addedJ = json_object_get(moduleJ, "added"))
  956. m.added = json_integer_value(addedJ);
  957. if (json_t* const lastAddedJ = json_object_get(moduleJ, "lastAdded"))
  958. m.lastAdded = json_number_value(lastAddedJ);
  959. rack::settings::moduleInfos[pluginSlug][moduleSlug] = m;
  960. }
  961. }
  962. json_decref(rootJ);
  963. return;
  964. }
  965. if (std::strcmp(key, "windowSize") == 0)
  966. {
  967. fState.windowSize = value;
  968. return;
  969. }
  970. #endif
  971. if (std::strcmp(key, "comment") == 0)
  972. {
  973. fState.comment = value;
  974. return;
  975. }
  976. if (std::strcmp(key, "screenshot") == 0)
  977. {
  978. fState.screenshot = value;
  979. #if defined(HAVE_LIBLO) && !defined(HEADLESS)
  980. patchUtils::sendScreenshotToRemote(value);
  981. #endif
  982. return;
  983. }
  984. if (std::strcmp(key, "patch") != 0)
  985. return;
  986. if (fAutosavePath.empty())
  987. return;
  988. const std::vector<uint8_t> data(d_getChunkFromBase64String(value));
  989. const ScopedContext sc(this);
  990. rack::system::removeRecursively(fAutosavePath);
  991. rack::system::createDirectories(fAutosavePath);
  992. rack::system::unarchiveToDirectory(data, fAutosavePath);
  993. try {
  994. context->patch->loadAutosave();
  995. } DISTRHO_SAFE_EXCEPTION_RETURN("setState loadAutosave",);
  996. // context->history->setSaved();
  997. }
  998. /* --------------------------------------------------------------------------------------------------------
  999. * Process */
  1000. void activate() override
  1001. {
  1002. context->bufferSize = getBufferSize();
  1003. #if DISTRHO_PLUGIN_NUM_INPUTS != 0
  1004. fAudioBufferCopy = new float*[DISTRHO_PLUGIN_NUM_INPUTS];
  1005. for (int i=0; i<DISTRHO_PLUGIN_NUM_INPUTS; ++i)
  1006. fAudioBufferCopy[i] = new float[context->bufferSize];
  1007. #endif
  1008. fNextExpectedFrame = 0;
  1009. }
  1010. void deactivate() override
  1011. {
  1012. #if DISTRHO_PLUGIN_NUM_INPUTS != 0
  1013. if (fAudioBufferCopy != nullptr)
  1014. {
  1015. for (int i=0; i<DISTRHO_PLUGIN_NUM_INPUTS; ++i)
  1016. delete[] fAudioBufferCopy[i];
  1017. delete[] fAudioBufferCopy;
  1018. fAudioBufferCopy = nullptr;
  1019. }
  1020. #endif
  1021. }
  1022. void run(const float** const inputs, float** const outputs, const uint32_t frames,
  1023. const MidiEvent* const midiEvents, const uint32_t midiEventCount) override
  1024. {
  1025. rack::contextSet(context);
  1026. const bool bypassed = context->bypassed;
  1027. {
  1028. const TimePosition& timePos(getTimePosition());
  1029. const bool reset = timePos.playing && (timePos.frame == 0 || d_isDiffHigherThanLimit(fNextExpectedFrame, timePos.frame, (uint64_t)2));
  1030. context->playing = timePos.playing;
  1031. context->bbtValid = timePos.bbt.valid;
  1032. context->frame = timePos.frame;
  1033. if (timePos.bbt.valid)
  1034. {
  1035. const double samplesPerTick = 60.0 * getSampleRate()
  1036. / timePos.bbt.beatsPerMinute
  1037. / timePos.bbt.ticksPerBeat;
  1038. context->bar = timePos.bbt.bar;
  1039. context->beat = timePos.bbt.beat;
  1040. context->beatsPerBar = timePos.bbt.beatsPerBar;
  1041. context->beatType = timePos.bbt.beatType;
  1042. context->barStartTick = timePos.bbt.barStartTick;
  1043. context->beatsPerMinute = timePos.bbt.beatsPerMinute;
  1044. context->tick = timePos.bbt.tick;
  1045. context->ticksPerBeat = timePos.bbt.ticksPerBeat;
  1046. context->ticksPerClock = timePos.bbt.ticksPerBeat / timePos.bbt.beatType;
  1047. context->ticksPerFrame = 1.0 / samplesPerTick;
  1048. context->tickClock = std::fmod(timePos.bbt.tick, context->ticksPerClock);
  1049. }
  1050. context->reset = reset;
  1051. fNextExpectedFrame = timePos.playing ? timePos.frame + frames : 0;
  1052. }
  1053. // separate buffers, use them
  1054. if (inputs != outputs && (inputs == nullptr || inputs[0] != outputs[0]))
  1055. {
  1056. context->dataIns = inputs;
  1057. context->dataOuts = outputs;
  1058. }
  1059. // inline processing, use a safe copy
  1060. else
  1061. {
  1062. #if DISTRHO_PLUGIN_NUM_INPUTS != 0
  1063. for (int i=0; i<DISTRHO_PLUGIN_NUM_INPUTS; ++i)
  1064. {
  1065. #if CARDINAL_VARIANT_MAIN
  1066. // can be null on main variant
  1067. if (inputs[i] != nullptr)
  1068. #endif
  1069. std::memcpy(fAudioBufferCopy[i], inputs[i], sizeof(float)*frames);
  1070. }
  1071. context->dataIns = fAudioBufferCopy;
  1072. #else
  1073. context->dataIns = nullptr;
  1074. #endif
  1075. context->dataOuts = outputs;
  1076. }
  1077. for (int i=0; i<DISTRHO_PLUGIN_NUM_OUTPUTS; ++i)
  1078. {
  1079. #if CARDINAL_VARIANT_MAIN
  1080. // can be null on main variant
  1081. if (outputs[i] != nullptr)
  1082. #endif
  1083. std::memset(outputs[i], 0, sizeof(float)*frames);
  1084. }
  1085. if (bypassed)
  1086. {
  1087. if (fWasBypassed != bypassed)
  1088. {
  1089. context->midiEvents = bypassMidiEvents;
  1090. context->midiEventCount = 16;
  1091. }
  1092. else
  1093. {
  1094. context->midiEvents = nullptr;
  1095. context->midiEventCount = 0;
  1096. }
  1097. }
  1098. else
  1099. {
  1100. context->midiEvents = midiEvents;
  1101. context->midiEventCount = midiEventCount;
  1102. }
  1103. ++context->processCounter;
  1104. context->engine->stepBlock(frames);
  1105. fWasBypassed = bypassed;
  1106. }
  1107. void sampleRateChanged(const double newSampleRate) override
  1108. {
  1109. rack::contextSet(context);
  1110. rack::settings::sampleRate = newSampleRate;
  1111. context->sampleRate = newSampleRate;
  1112. context->engine->setSampleRate(newSampleRate);
  1113. }
  1114. // -------------------------------------------------------------------------------------------------------
  1115. private:
  1116. /**
  1117. Set our plugin class as non-copyable and add a leak detector just in case.
  1118. */
  1119. DISTRHO_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CardinalPlugin)
  1120. };
  1121. CardinalPluginContext* getRackContextFromPlugin(void* const ptr)
  1122. {
  1123. return static_cast<CardinalPlugin*>(ptr)->getRackContext();
  1124. }
  1125. /* ------------------------------------------------------------------------------------------------------------
  1126. * Plugin entry point, called by DPF to create a new plugin instance. */
  1127. Plugin* createPlugin()
  1128. {
  1129. return new CardinalPlugin();
  1130. }
  1131. // -----------------------------------------------------------------------------------------------------------
  1132. END_NAMESPACE_DISTRHO