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.

411 lines
13KB

  1. #include "ScriptEngine.hpp"
  2. #include "lang/SC_LanguageClient.h"
  3. #include "LangSource/SC_LanguageConfig.hpp"
  4. #include "LangSource/SCBase.h"
  5. #include "LangSource/VMGlobals.h"
  6. #include "LangSource/PyrObject.h"
  7. #include "LangSource/PyrKernel.h"
  8. #include <thread>
  9. #include <atomic>
  10. #include <numeric>
  11. #include <unistd.h> // getcwd
  12. // SuperCollider script engine for VCV-Prototype
  13. // Original author: Brian Heim <brianlheim@gmail.com>
  14. /* DESIGN
  15. *
  16. * The idea is that the user writes a script that defines a couple environment variables:
  17. *
  18. * ~vcv_frameDivider: Integer
  19. * ~vcv_bufferSize: Integer
  20. * ~vcv_process: Function (VcvPrototypeProcessBlock -> VcvPrototypeProcessBlock)
  21. *
  22. * ~vcv_process is invoked once per process block. Users should not manipulate
  23. * the block object in any way other than by writing directly to the arrays in `outputs`,
  24. * `knobs`, `lights`, and `switchLights`.
  25. */
  26. extern rack::plugin::Plugin* pluginInstance; // plugin's version of 'this'
  27. class SuperColliderEngine;
  28. class SC_VcvPrototypeClient final : public SC_LanguageClient {
  29. public:
  30. SC_VcvPrototypeClient(SuperColliderEngine* engine);
  31. ~SC_VcvPrototypeClient();
  32. // These will invoke the interpreter
  33. void interpretScript(const std::string& script) noexcept
  34. {
  35. // Insert our own environment variable in the script so we can check
  36. // later (in testCompile()) whether it compiled all right.
  37. auto modifiedScript = std::string(compileTestVariableName) + "=1;" + script;
  38. interpret(modifiedScript.c_str());
  39. testCompile();
  40. }
  41. void evaluateProcessBlock(ProcessBlock* block) noexcept;
  42. void setNumRows() noexcept {
  43. auto&& command = "VcvPrototypeProcessBlock.numRows = " + std::to_string(NUM_ROWS);
  44. interpret(command.c_str());
  45. }
  46. int getFrameDivider() noexcept {
  47. return getEnvVarAsPositiveInt("~vcv_frameDivider", "~vcv_frameDivider should be an Integer");
  48. }
  49. int getBufferSize() noexcept {
  50. return getEnvVarAsPositiveInt("~vcv_bufferSize", "~vcv_bufferSize should be an Integer");
  51. }
  52. bool isOk() const noexcept { return _ok; }
  53. void postText(const char* str, size_t len) override;
  54. // No concept of flushing or stdout vs stderr
  55. void postFlush(const char* str, size_t len) override { postText(str, len); }
  56. void postError(const char* str, size_t len) override { postText(str, len); }
  57. void flush() override {}
  58. private:
  59. static const char * compileTestVariableName;
  60. void interpret(const char * text) noexcept;
  61. void testCompile() noexcept { getEnvVarAsPositiveInt(compileTestVariableName, "Script failed to compile"); }
  62. // Called on unrecoverable error, will stop the plugin
  63. void fail(const std::string& msg) noexcept;
  64. const char* buildScProcessBlockString(const ProcessBlock* block) const noexcept;
  65. int getEnvVarAsPositiveInt(const char* envVarName, const char* errorMsg) noexcept;
  66. // converts top of stack back to ProcessBlock data
  67. void readScProcessBlockResult(ProcessBlock* block) noexcept;
  68. // helpers for copying SC info back into process block's arrays
  69. bool isVcvPrototypeProcessBlock(const PyrSlot* slot) const noexcept;
  70. bool copyFloatArray(const PyrSlot& inSlot, const char* context, const char* extraContext, float* outArray, int size) noexcept;
  71. template <typename Array>
  72. bool copyArrayOfFloatArrays(const PyrSlot& inSlot, const char* context, Array& array, int size) noexcept;
  73. SuperColliderEngine* _engine;
  74. PyrSymbol* _vcvPrototypeProcessBlockSym;
  75. bool _ok = true;
  76. };
  77. const char * SC_VcvPrototypeClient::compileTestVariableName = "~vcv_secretTestCompileSentinel";
  78. class SuperColliderEngine final : public ScriptEngine {
  79. public:
  80. ~SuperColliderEngine() noexcept {
  81. // Only join if client was started in the first place.
  82. if (_clientThread.joinable())
  83. _clientThread.join();
  84. engineRunning = false;
  85. }
  86. std::string getEngineName() override { return "SuperCollider"; }
  87. int run(const std::string& path, const std::string& script) override {
  88. if (engineRunning) {
  89. display("Only one SuperCollider engine may run at once");
  90. return 1;
  91. }
  92. engineRunning = true;
  93. if (!_clientThread.joinable()) {
  94. _clientThread = std::thread([this, script]() {
  95. _client.reset(new SC_VcvPrototypeClient(this));
  96. _client->setNumRows();
  97. _client->interpretScript(script);
  98. setFrameDivider(_client->getFrameDivider());
  99. setBufferSize(_client->getBufferSize());
  100. finishClientLoading();
  101. });
  102. }
  103. return 0;
  104. }
  105. int process() override {
  106. if (waitingOnClient())
  107. return 0;
  108. if (clientHasError())
  109. return 1;
  110. _client->evaluateProcessBlock(getProcessBlock());
  111. return clientHasError() ? 1 : 0;
  112. }
  113. private:
  114. // Used to limit the number of running SC instances to 1.
  115. static bool engineRunning;
  116. bool waitingOnClient() const noexcept { return !_clientRunning; }
  117. bool clientHasError() const noexcept { return !_client->isOk(); }
  118. void finishClientLoading() noexcept { _clientRunning = true; }
  119. std::unique_ptr<SC_VcvPrototypeClient> _client;
  120. std::thread _clientThread; // used only to start up client
  121. std::atomic_bool _clientRunning{false}; // set to true when client is ready to process data
  122. };
  123. bool SuperColliderEngine::engineRunning = false;
  124. SC_VcvPrototypeClient::SC_VcvPrototypeClient(SuperColliderEngine* engine)
  125. : SC_LanguageClient("SC VCV-Prototype client")
  126. , _engine(engine)
  127. {
  128. using Path = SC_LanguageConfig::Path;
  129. Path sc_lib_root = rack::asset::plugin(pluginInstance, "dep/supercollider/SCClassLibrary");
  130. Path sc_ext_root = rack::asset::plugin(pluginInstance, "support/supercollider_extensions");
  131. Path sc_yaml_path = rack::asset::plugin(pluginInstance, "dep/supercollider/sclang_vcv_config.yml");
  132. if (!SC_LanguageConfig::defaultLibraryConfig(/* isStandalone */ true))
  133. fail("Failed setting default library config");
  134. if (!gLanguageConfig->addIncludedDirectory(sc_lib_root))
  135. fail("Failed to add main include directory");
  136. if (!gLanguageConfig->addIncludedDirectory(sc_ext_root))
  137. fail("Failed to add extensions include directory");
  138. if (!SC_LanguageConfig::writeLibraryConfigYAML(sc_yaml_path))
  139. fail("Failed to write library config YAML file");
  140. SC_LanguageConfig::setConfigPath(sc_yaml_path);
  141. // TODO allow users to add extensions somehow?
  142. initRuntime();
  143. compileLibrary(/* isStandalone */ true);
  144. if (!isLibraryCompiled())
  145. fail("Error while compiling class library");
  146. _vcvPrototypeProcessBlockSym = getsym("VcvPrototypeProcessBlock");
  147. }
  148. SC_VcvPrototypeClient::~SC_VcvPrototypeClient() {
  149. shutdownLibrary();
  150. shutdownRuntime();
  151. }
  152. void SC_VcvPrototypeClient::interpret(const char* text) noexcept {
  153. setCmdLine(text);
  154. interpretCmdLine();
  155. }
  156. #ifdef SC_VCV_ENGINE_TIMING
  157. static long long int gmax = 0;
  158. static constexpr unsigned int nTimes = 1024;
  159. static long long int times[nTimes] = {};
  160. static unsigned int timesIndex = 0;
  161. #endif
  162. void SC_VcvPrototypeClient::evaluateProcessBlock(ProcessBlock* block) noexcept {
  163. #ifdef SC_VCV_ENGINE_TIMING
  164. auto start = std::chrono::high_resolution_clock::now();
  165. #endif
  166. auto* buf = buildScProcessBlockString(block);
  167. interpret(buf);
  168. readScProcessBlockResult(block);
  169. #ifdef SC_VCV_ENGINE_TIMING
  170. auto end = std::chrono::high_resolution_clock::now();
  171. auto ticks = (end - start).count();
  172. times[timesIndex] = ticks;
  173. timesIndex++;
  174. timesIndex %= nTimes;
  175. if (gmax < ticks)
  176. {
  177. gmax = ticks;
  178. std::printf("MAX TIME %lld\n", ticks);
  179. }
  180. if (timesIndex == 0)
  181. {
  182. std::printf("AVG TIME %lld\n", std::accumulate(std::begin(times), std::end(times), 0ull) / nTimes);
  183. }
  184. #endif
  185. }
  186. void SC_VcvPrototypeClient::postText(const char* str, size_t len) {
  187. // Ensure the last message logged (presumably an error) stays onscreen.
  188. if (_ok)
  189. _engine->display(std::string(str, len));
  190. }
  191. void SC_VcvPrototypeClient::fail(const std::string& msg) noexcept {
  192. // Ensure the last messaged logged in a previous failure stays onscreen.
  193. if (_ok)
  194. _engine->display(msg);
  195. _ok = false;
  196. }
  197. // This should be well above what we ever need to represent a process block.
  198. // Currently comes out to around 500 KB.
  199. constexpr unsigned buildPbOverhead = 1024;
  200. constexpr unsigned buildPbFloatSize = 10;
  201. constexpr unsigned buildPbInsOutsSize = MAX_BUFFER_SIZE * NUM_ROWS * 2 * buildPbFloatSize;
  202. constexpr unsigned buildPbOtherArraysSize = buildPbFloatSize * NUM_ROWS * 8;
  203. constexpr unsigned buildPbBufferSize = buildPbOverhead + buildPbInsOutsSize + buildPbOtherArraysSize;
  204. // Don't write initial string every time
  205. #define BUILD_PB_BEGIN_STRING "^~vcv_process.(VcvPrototypeProcessBlock.new("
  206. static char buildPbStringScratchBuf[buildPbBufferSize] = BUILD_PB_BEGIN_STRING;
  207. constexpr unsigned buildPbBeginStringOffset = sizeof(BUILD_PB_BEGIN_STRING);
  208. #undef BUILD_PB_BEGIN_STRING
  209. const char* SC_VcvPrototypeClient::buildScProcessBlockString(const ProcessBlock* block) const noexcept {
  210. auto* buf = buildPbStringScratchBuf + buildPbBeginStringOffset - 1;
  211. // Perhaps imprudently assuming snprintf never returns a negative code
  212. buf += std::sprintf(buf, "%.6f,%.6f,%d,", block->sampleRate, block->sampleTime, block->bufferSize);
  213. auto&& appendInOutArray = [&buf](const int bufferSize, const float (&data)[NUM_ROWS][MAX_BUFFER_SIZE]) {
  214. buf += std::sprintf(buf, "[");
  215. for (int i = 0; i < NUM_ROWS; ++i) {
  216. buf += std::sprintf(buf, "Signal[");
  217. for (int j = 0; j < bufferSize; ++j) {
  218. buf += std::sprintf(buf, "%g%c", data[i][j], j == bufferSize - 1 ? ' ' : ',');
  219. }
  220. buf += std::sprintf(buf, "]%c", i == NUM_ROWS - 1 ? ' ' : ',');
  221. }
  222. buf += std::sprintf(buf, "],");
  223. };
  224. appendInOutArray(block->bufferSize, block->inputs);
  225. appendInOutArray(block->bufferSize, block->outputs);
  226. // knobs
  227. buf += std::sprintf(buf, "FloatArray[");
  228. for (int i = 0; i < NUM_ROWS; ++i)
  229. buf += std::sprintf(buf, "%g%c", block->knobs[i], i == NUM_ROWS - 1 ? ' ' : ',');
  230. // switches
  231. buf += std::sprintf(buf, "],[");
  232. for (int i = 0; i < NUM_ROWS; ++i)
  233. buf += std::sprintf(buf, "%s%c", block->switches[i] ? "true" : "false", i == NUM_ROWS - 1 ? ' ' : ',');
  234. buf += std::sprintf(buf, "]");
  235. // lights, switchlights
  236. auto&& appendLightsArray = [&buf](const float (&array)[NUM_ROWS][3]) {
  237. buf += std::sprintf(buf, ",[");
  238. for (int i = 0; i < NUM_ROWS; ++i) {
  239. buf += std::sprintf(buf, "FloatArray[%g,%g,%g]%c", array[i][0], array[i][1], array[i][2],
  240. i == NUM_ROWS - 1 ? ' ' : ',');
  241. }
  242. buf += std::sprintf(buf, "]");
  243. };
  244. appendLightsArray(block->lights);
  245. appendLightsArray(block->switchLights);
  246. buf += std::sprintf(buf, "));");
  247. return buildPbStringScratchBuf;
  248. }
  249. int SC_VcvPrototypeClient::getEnvVarAsPositiveInt(const char* envVarName, const char* errorMsg) noexcept {
  250. auto command = std::string("^") + envVarName;
  251. interpret(command.c_str());
  252. auto* resultSlot = &scGlobals()->result;
  253. if (IsInt(resultSlot)) {
  254. auto intResult = slotRawInt(resultSlot);
  255. if (intResult > 0) {
  256. return intResult;
  257. } else {
  258. fail(std::string(envVarName) + " should be > 0");
  259. return -1;
  260. }
  261. } else {
  262. fail(errorMsg);
  263. return -1;
  264. }
  265. }
  266. void SC_VcvPrototypeClient::readScProcessBlockResult(ProcessBlock* block) noexcept {
  267. auto* resultSlot = &scGlobals()->result;
  268. if (!isVcvPrototypeProcessBlock(resultSlot)) {
  269. fail("Result of ~vcv_process must be an instance of VcvPrototypeProcessBlock");
  270. return;
  271. }
  272. PyrObject* object = slotRawObject(resultSlot);
  273. auto* rawSlots = static_cast<PyrSlot*>(object->slots);
  274. // See .sc object definition
  275. constexpr unsigned outputsSlotIndex = 4;
  276. constexpr unsigned knobsSlotIndex = 5;
  277. constexpr unsigned lightsSlotIndex = 7;
  278. constexpr unsigned switchLightsSlotIndex = 8;
  279. if (!copyArrayOfFloatArrays(rawSlots[outputsSlotIndex], "outputs", block->outputs, block->bufferSize))
  280. return;
  281. if (!copyArrayOfFloatArrays(rawSlots[lightsSlotIndex], "lights", block->lights, 3))
  282. return;
  283. if (!copyArrayOfFloatArrays(rawSlots[switchLightsSlotIndex], "switchLights", block->switchLights, 3))
  284. return;
  285. if (!copyFloatArray(rawSlots[knobsSlotIndex], "", "knobs", block->knobs, NUM_ROWS))
  286. return;
  287. }
  288. bool SC_VcvPrototypeClient::isVcvPrototypeProcessBlock(const PyrSlot* slot) const noexcept {
  289. if (NotObj(slot))
  290. return false;
  291. auto* klass = slotRawObject(slot)->classptr;
  292. auto* klassNameSymbol = slotRawSymbol(&klass->name);
  293. return klassNameSymbol == _vcvPrototypeProcessBlockSym;
  294. }
  295. // It's somewhat bad design that we pass two const char*s here, but this avoids an allocation while also providing
  296. // good context for errors.
  297. bool SC_VcvPrototypeClient::copyFloatArray(const PyrSlot& inSlot, const char* context, const char* extraContext, float* outArray, int size) noexcept
  298. {
  299. if (!isKindOfSlot(const_cast<PyrSlot*>(&inSlot), class_floatarray)) {
  300. fail(std::string(context) + extraContext + " must be a FloatArray");
  301. return false;
  302. }
  303. auto* floatArrayObj = slotRawObject(&inSlot);
  304. if (floatArrayObj->size != size) {
  305. fail(std::string(context) + extraContext + " must be of size " + std::to_string(size));
  306. return false;
  307. }
  308. auto* floatArray = reinterpret_cast<const PyrFloatArray*>(floatArrayObj);
  309. auto* rawArray = static_cast<const float*>(floatArray->f);
  310. std::memcpy(outArray, rawArray, size * sizeof(float));
  311. return true;
  312. }
  313. template <typename Array>
  314. bool SC_VcvPrototypeClient::copyArrayOfFloatArrays(const PyrSlot& inSlot, const char* context, Array& outArray, int size) noexcept
  315. {
  316. // OUTPUTS
  317. if (!isKindOfSlot(const_cast<PyrSlot*>(&inSlot), class_array)) {
  318. fail(std::string(context) + " must be a Array");
  319. return false;
  320. }
  321. auto* inObj = slotRawObject(&inSlot);
  322. if (inObj->size != NUM_ROWS) {
  323. fail(std::string(context) + " must be of size " + std::to_string(NUM_ROWS));
  324. return false;
  325. }
  326. for (int i = 0; i < NUM_ROWS; ++i) {
  327. if (!copyFloatArray(inObj->slots[i], "subarray of ", context, outArray[i], size)) {
  328. return false;
  329. }
  330. }
  331. return true;
  332. }
  333. __attribute__((constructor(1000)))
  334. static void constructor() {
  335. addScriptEngine<SuperColliderEngine>(".sc");
  336. addScriptEngine<SuperColliderEngine>(".scd");
  337. }