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.

372 lines
12KB

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