From 46cb271357357fb651c4d757599a6cdf0c1b55db Mon Sep 17 00:00:00 2001 From: Brian Heim Date: Fri, 13 Dec 2019 20:26:08 -0600 Subject: [PATCH] Add basic SuperCollider support --- Makefile | 21 +++++ examples/gain.scd | 7 ++ src/SuperColliderEngine.cpp | 167 ++++++++++++++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 examples/gain.scd create mode 100644 src/SuperColliderEngine.cpp diff --git a/Makefile b/Makefile index 42e4013..5ad946c 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ DUKTAPE ?= 0 QUICKJS ?= 1 LUAJIT ?= 1 PYTHON ?= 0 +SUPERCOLLIDER ?= 1 # Entropia File System Watcher efsw := dep/lib/libefsw-static-release.a @@ -73,6 +74,26 @@ $(luajit): cd dep/LuaJIT-2.0.5 && $(MAKE) BUILDMODE=static PREFIX="$(DEP_PATH)" install endif +# SuperCollider +ifeq ($(SUPERCOLLIDER), 1) +SOURCES += src/SuperColliderEngine.cpp +FLAGS += -Idep/supercollider/include -Idep/supercollider/include/common -Idep/supercollider/lang -Idep/supercollider/common -Idep/supercollider/include/plugin_interface +supercollider := dep/supercollider/build/lang/libsclang.a +OBJECTS += $(supercollider) +DEPS += $(supercollider) +SUPERCOLLIDER_CMAKE_FLAGS += -DSUPERNOVA=OFF -DSC_EL=OFF -DSC_VIM=OFF -DSC_ED=OFF -DSC_IDE=OFF -DSC_ABLETON_LINK=OFF -DSC_QT=OFF -DCMAKE_BUILD_TYPE=Release -DSCLANG_SERVER=OFF -DBUILD_TESTING=OFF +SUPERCOLLIDER_SUBMODULES += external_libraries/hidapi external_libraries/nova-simd external_libraries/nova-tt external_libraries/portaudio_sc_org external_libraries/yaml-cpp +SUPERCOLLIDER_BRANCH := topic/vcv-prototype-support +# TODO need some better way of getting link library names! +LDFLAGS += dep/supercollider/build/lang/../external_libraries/libtlsf.a /usr/lib/libpthread.dylib dep/supercollider/build/lang/../external_libraries/hidapi/mac/libhidapi.a dep/supercollider/build/lang/../external_libraries/hidapi/hidapi_parser/libhidapi_parser.a dep/supercollider/build/lang/../external_libraries/libboost_thread_lib.a dep/supercollider/build/lang/../external_libraries/libboost_system_lib.a dep/supercollider/build/lang/../external_libraries/libboost_regex_lib.a dep/supercollider/build/lang/../external_libraries/libboost_filesystem_lib.a /usr/local/opt/readline/lib/libreadline.dylib -framework Carbon -framework CoreAudio -framework CoreMIDI -framework CoreServices -framework IOKit -framework CoreFoundation /usr/local/opt/libsndfile/lib/libsndfile.dylib dep/supercollider/build/lang/../external_libraries/libyaml.a +$(supercollider): + cd dep && git clone "https://github.com/supercollider/supercollider" --branch $(SUPERCOLLIDER_BRANCH) --depth 5 + cd dep/supercollider && git submodule update --init -- $(SUPERCOLLIDER_SUBMODULES) + cd dep/supercollider && mkdir build && cd build + cd dep/supercollider/build && cmake .. -G "Unix Makefiles" $(SUPERCOLLIDER_CMAKE_FLAGS) + cd dep/supercollider/build && $(MAKE) libsclang +endif + # Python ifeq ($(PYTHON), 1) SOURCES += src/PythonEngine.cpp diff --git a/examples/gain.scd b/examples/gain.scd new file mode 100644 index 0000000..0da9f2c --- /dev/null +++ b/examples/gain.scd @@ -0,0 +1,7 @@ +// Simplest possible script using all variables, demonstrating buffering +// by Brian Heim + +~i = 0; +a = 0; +// ~process = {|x| a = max(a, bench { 12.do { 256.do { |i| sin(i)} }; post(x); }); post(a) } +~vcv_process = {post("test")} diff --git a/src/SuperColliderEngine.cpp b/src/SuperColliderEngine.cpp new file mode 100644 index 0000000..c6ba1eb --- /dev/null +++ b/src/SuperColliderEngine.cpp @@ -0,0 +1,167 @@ +#include "ScriptEngine.hpp" + +#include "lang/SC_LanguageClient.h" +#include "LangSource/SC_LanguageConfig.hpp" +#include "LangSource/SCBase.h" +#include "LangSource/VMGlobals.h" + +#include +#include +#include // getcwd + +// SuperCollider script engine for VCV-Prototype +// Original author: Brian Heim + +/* DESIGN + * + * This is currently a work in progress. The idea is that the user writes a script + * that defines a couple environment variables: + * + * ~vcv_frameDivider: Integer + * ~vcv_bufferSize: Integer + * ~vcv_process: Function (VcvPrototypeProcessBlock -> VcvPrototypeProcessBlock) + * + * ~vcv_process is invoked once per process block. Ideally, users should not manipulate + * the block object in any way other than by writing directly to the arrays in `outputs`, + * `knobs`, `lights`, and `switchLights`. + */ + +extern rack::plugin::Plugin* pluginInstance; // plugin's version of 'this' +class SuperColliderEngine; + +class SC_VcvPrototypeClient final : public SC_LanguageClient { +public: + SC_VcvPrototypeClient(SuperColliderEngine* engine); + ~SC_VcvPrototypeClient(); + + // These will invoke the interpreter + void interpret(const std::string& text) noexcept; + void evaluateProcessBlock(ProcessBlock* block) noexcept; + int getFrameDivider() noexcept { return 1; } // getResultAsInt("^~vcv_frameDivider"); } + int getBufferSize() noexcept { return 256; } // getResultAsInt("^~vcv_bufferSize"); } + + void postText(const char* str, size_t len) override; + + // No concept of flushing or stdout vs stderr + void postFlush(const char* str, size_t len) override { postText(str, len); } + void postError(const char* str, size_t len) override { postText(str, len); } + void flush() override {} + +private: + // TODO + int getResultAsInt(const char* text) noexcept; + + SuperColliderEngine* _engine; +}; + +class SuperColliderEngine final : public ScriptEngine { +public: + ~SuperColliderEngine() noexcept { _clientThread.join(); } + + std::string getEngineName() override { return "SuperCollider"; } + + int run(const std::string& path, const std::string& script) override { + if (!_clientThread.joinable()) { + _clientThread = std::thread([this, script]() { + _client.reset(new SC_VcvPrototypeClient(this)); + _client->interpret(script); + setFrameDivider(_client->getFrameDivider()); + setBufferSize(_client->getBufferSize()); + finishClientLoading(); + }); + } + + return 0; + } + + int process() override { + if (waitingOnClient()) + return 0; + + _client->evaluateProcessBlock(getProcessBlock()); + return 0; + } + +private: + bool waitingOnClient() const noexcept { return !_clientRunning; } + + // TODO handle failure conditions + void finishClientLoading() noexcept { _clientRunning = true; } + + std::unique_ptr _client; + std::thread _clientThread; // used only to start up client + std::atomic_bool _clientRunning{false}; // set to true when client is ready to process data +}; + +// TODO +#define FAIL(_msg_) _engine->display(_msg_) + +SC_VcvPrototypeClient::SC_VcvPrototypeClient(SuperColliderEngine* engine) + : SC_LanguageClient("SC VCV-Prototype client") + , _engine(engine) +{ + using Path = SC_LanguageConfig::Path; + Path sc_lib_root = rack::asset::plugin(pluginInstance, "dep/supercollider/SCClassLibrary"); + Path sc_ext_root = rack::asset::plugin(pluginInstance, "dep/supercollider_extensions"); + Path sc_yaml_path = rack::asset::plugin(pluginInstance, "dep/supercollider/sclang_vcv_config.yml"); + + if (!SC_LanguageConfig::defaultLibraryConfig(/* isStandalone */ true)) + FAIL("Failed setting default library config"); + if (!gLanguageConfig->addIncludedDirectory(sc_lib_root)) + FAIL("Failed to add main include directory"); + if (!gLanguageConfig->addIncludedDirectory(sc_ext_root)) + FAIL("Failed to add extensions include directory"); + if (!SC_LanguageConfig::writeLibraryConfigYAML(sc_yaml_path)) + FAIL("Failed to write library config YAML file"); + + SC_LanguageConfig::setConfigPath(sc_yaml_path); + + // TODO allow users to add extensions somehow? + + initRuntime(); + compileLibrary(/* isStandalone */ true); + // TODO better logging here? + if (!isLibraryCompiled()) + FAIL("Error while compiling class library"); +} + +SC_VcvPrototypeClient::~SC_VcvPrototypeClient() { + shutdownLibrary(); + shutdownRuntime(); +} + +void SC_VcvPrototypeClient::interpret(const std::string& text) noexcept { + setCmdLine(text.c_str()); + interpretCmdLine(); +} + +void SC_VcvPrototypeClient::postText(const char* str, size_t len) { + _engine->display(std::string(str, len)); +} + +// TODO test code +static long long int gmax = 0; + +void SC_VcvPrototypeClient::evaluateProcessBlock(ProcessBlock* block) noexcept { + std::ostringstream builder; + + builder << "~vcv_process.value(" << block->knobs[0] << ");\n"; + + // TIMING TODO test code + auto start = std::chrono::high_resolution_clock::now(); + interpret(builder.str()); + auto end = std::chrono::high_resolution_clock::now(); + auto ticks = (end - start).count(); + if (gmax < ticks) + { + gmax = ticks; + printf("MAX TIME %lld\n", ticks); + } + // END TIMING +} + +__attribute__((constructor(1000))) +static void constructor() { + addScriptEngine("sc"); + addScriptEngine("scd"); +}