From 785b7b4c78883f069241c8083635a3e0b43ff008 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Sat, 14 Sep 2019 22:31:49 -0400 Subject: [PATCH] Initial commit --- .gitignore | 6 + LICENSE-dist.txt | 21 + LICENSE.txt | 7 + Makefile | 27 ++ README.md | 64 +++ examples/rainbow.js | 20 + plugin.json | 24 ++ res/Prototype.svg | 981 ++++++++++++++++++++++++++++++++++++++++++ src/DuktapeEngine.cpp | 480 +++++++++++++++++++++ src/Prototype.cpp | 346 +++++++++++++++ src/ScriptEngine.hpp | 64 +++ 11 files changed, 2040 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE-dist.txt create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 README.md create mode 100644 examples/rainbow.js create mode 100644 plugin.json create mode 100644 res/Prototype.svg create mode 100644 src/DuktapeEngine.cpp create mode 100644 src/Prototype.cpp create mode 100644 src/ScriptEngine.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..abde234 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/build +/dist +/plugin.so +/plugin.dylib +/plugin.dll +.DS_Store diff --git a/LICENSE-dist.txt b/LICENSE-dist.txt new file mode 100644 index 0000000..f74a115 --- /dev/null +++ b/LICENSE-dist.txt @@ -0,0 +1,21 @@ +# Duktape + +Copyright (c) 2013-2019 by Duktape authors (see AUTHORS.rst) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..77f55d4 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright (c) 2019 Andrew Belt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ce84e3a --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +RACK_DIR ?= ../.. + +FLAGS += +CFLAGS += +CXXFLAGS += + +LDFLAGS += + +SOURCES += $(wildcard src/*.cpp) + +DISTRIBUTABLES += res examples +DISTRIBUTABLES += $(wildcard LICENSE*) + + +# Duktape + +duktape := dep/duktape-2.4.0/src/duktape.c +DEPS += $(duktape) +SOURCES += $(duktape) +FLAGS += -Idep/duktape-2.4.0/src + +$(duktape): + cd dep && $(WGET) "https://duktape.org/duktape-2.4.0.tar.xz" + cd dep && $(UNTAR) duktape-2.4.0.tar.xz + + +include $(RACK_DIR)/plugin.mk diff --git a/README.md b/README.md new file mode 100644 index 0000000..30df8c2 --- /dev/null +++ b/README.md @@ -0,0 +1,64 @@ +# VCV Prototype + +Scripting language host for [VCV Rack](https://vcvrack.com/) containing: +- 6 inputs +- 6 outputs +- 6 knobs +- 6 lights (RGB LEDs) +- 6 switches with RGB LEDs + +### Scripting API + +This is a reference API for the `DuktapeEngine` (JavaScript). +Other script engines may vary in their syntax (e.g. `block.inputs[i][j]` vs `block.getInput(i, j)` vs `input(i, j)`). + +```js +// Display message on LED display. +display(message) +// Skip this many sample frames before running process(). +// For sequencers, 32 is reasonable since process() will be called every 0.7ms with a 44100kHz sample rate. +// For audio generators and processors, 1 is recommended. If this is too slow for your purposes, write a C++ plugin. +config.frameDivider = 1 +// Number of samples to store each block passed to process(). +// Latency introduced by buffers is `bufferSize * frameDivider * sampleTime`. +config.bufferSize = 1 + +// Called when the next block is ready to be processed. +function process(block) { + // Engine sample rate in Hz. Read-only. + block.sampleRate + // Equal to `1 / sampleRate`. Read-only. + block.sampleTime + // The actual buffer size, requested by `config.bufferSize`. Read-only. + block.bufferSize + // Voltage of the input port of row `i` and buffer index `j`. Read-only. + block.inputs[i][j] + // Voltage of the output port of row `i` and buffer index `j`. Writable. + block.outputs[i][j] + // Value of the knob of row `i`. Between 0 and 1. Read-only. + block.knobs[i] + // Pressed state of the switch of row `i`. Read-only. + block.switches[i] + // Brightness of the RGB LED of row `i` and color index `c`. Writable. + // `c=0` for red, `c=1` for green, `c=2` for blue. + block.lights[i][c] + // Brightness of the switch RGB LED of row `i` and color index `c`. Writable. + block.switchLights[i][c] +} +``` + +### Adding a script engine + +- Add your scripting language library to the build system so it builds with `make dep`, following the Duktape example in the `Makefile`. +- Create a `MyEngine.cpp` file in `src/` with a `ScriptEngine` subclass defining the virtual methods, following `src/DuktapeEngine.cpp` as an example. +- Add your engine to the "List of ScriptEngines" in `src/ScriptEngine.cpp`. +- Build and test VCV Prototype. +- Add a few example scripts and tests to `examples/`. These will be included in the plugin package for the user. +- Add your name to the Contributers list below. +- Send a pull request. Once merged, you will be added as a repo maintainer. Be sure to "watch" this repo to be notified of bugs in your engine. + +### Maintainers + +- [Wes Milholen](https://grayscale.info/): panel design +- [Andrew Belt](https://github.com/AndrewBelt): host code, `DuktapeEngine` (JavaScript) +- add your name here \ No newline at end of file diff --git a/examples/rainbow.js b/examples/rainbow.js new file mode 100644 index 0000000..29b3135 --- /dev/null +++ b/examples/rainbow.js @@ -0,0 +1,20 @@ +config.frameDivider = 1 +config.bufferSize = 1 + +var frame = 0 +function process(block) { + frame += 1 + for (var i = 0; i < 6; i++) { + for (var j = 0; j < block.bufferSize; j++) { + var v = block.inputs[i][j] + v *= block.knobs[i] + block.outputs[i][j] = v + } + // block.lights[i][2] = 1 + // block.switchLights[i][1] = 1 + } + + display(frame) +} + +display("Hello, world!") diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..ebae09b --- /dev/null +++ b/plugin.json @@ -0,0 +1,24 @@ +{ + "slug": "VCV-Prototype", + "name": "Prototype", + "version": "1.0.0", + "license": "BSD-3-Clause", + "brand": "VCV", + "author": "VCV", + "authorEmail": "contact@vcvrack.com", + "authorUrl": "https://vcvrack.com/", + "pluginUrl": "", + "manualUrl": "", + "sourceUrl": "", + "donateUrl": "", + "modules": [ + { + "slug": "Prototype", + "name": "Prototype", + "description": "", + "tags": [ + "External" + ] + } + ] +} \ No newline at end of file diff --git a/res/Prototype.svg b/res/Prototype.svg new file mode 100644 index 0000000..0e90acb --- /dev/null +++ b/res/Prototype.svg @@ -0,0 +1,981 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DuktapeEngine.cpp b/src/DuktapeEngine.cpp new file mode 100644 index 0000000..5260a1b --- /dev/null +++ b/src/DuktapeEngine.cpp @@ -0,0 +1,480 @@ +#include "ScriptEngine.hpp" +#include + + +struct DuktapeEngine : ScriptEngine { + duk_context* ctx; + + int initialize() override { + ctx = duk_create_heap_default(); + if (!ctx) { + setMessage("Could not create duktape context"); + return -1; + } + + // Initialize globals + // user pointer + duk_push_pointer(ctx, this); + duk_put_global_string(ctx, DUK_HIDDEN_SYMBOL("p")); + + // console + duk_idx_t consoleIdx = duk_push_object(ctx); + { + // log + duk_push_c_function(ctx, native_console_log, 1); + duk_put_prop_string(ctx, consoleIdx, "log"); + // info (alias for log) + duk_push_c_function(ctx, native_console_log, 1); + duk_put_prop_string(ctx, consoleIdx, "info"); + // debug + duk_push_c_function(ctx, native_console_debug, 1); + duk_put_prop_string(ctx, consoleIdx, "debug"); + // warn + duk_push_c_function(ctx, native_console_warn, 1); + duk_put_prop_string(ctx, consoleIdx, "warn"); + } + duk_put_global_string(ctx, "console"); + + // display + duk_push_c_function(ctx, native_display, 1); + duk_put_global_string(ctx, "display"); + + // config + duk_idx_t configIdx = duk_push_object(ctx); + { + // frameDivider + duk_push_int(ctx, getFrameDivider()); + duk_put_prop_string(ctx, configIdx, "frameDivider"); + // bufferSize + duk_push_int(ctx, getBufferSize()); + duk_put_prop_string(ctx, configIdx, "bufferSize"); + } + duk_put_global_string(ctx, "config"); + + // // block (put on stack) + // duk_idx_t blockIdx = duk_push_object(ctx); + // { + // // sampleRate + // duk_push_string(ctx, "sampleRate"); + // duk_push_c_function(ctx, native_block_sampleRate_get, 0); + // duk_def_prop(ctx, configIdx, DUK_DEFPROP_HAVE_GETTER); + + // // sampleTime + // duk_push_string(ctx, "sampleTime"); + // duk_push_c_function(ctx, native_block_sampleTime_get, 0); + // duk_def_prop(ctx, configIdx, DUK_DEFPROP_HAVE_GETTER); + + // // bufferSize + // duk_push_string(ctx, "bufferSize"); + // duk_push_c_function(ctx, native_block_bufferSize_get, 0); + // duk_def_prop(ctx, configIdx, DUK_DEFPROP_HAVE_GETTER); + + // // inputs + // duk_idx_t inputsIdx = duk_push_array(ctx); + // for (int i = 0; i < NUM_ROWS; i++) { + // duk_push_object(ctx); + // { + // duk_push_int(ctx, i); + // duk_put_prop_string(ctx, -2, "i"); + // } + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_input_get, 3); + // duk_put_prop_string(ctx, -2, "get"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_index(ctx, inputsIdx, i); + // } + // duk_put_prop_string(ctx, blockIdx, "inputs"); + + // // outputs + // duk_idx_t outputsIdx = duk_push_array(ctx); + // for (int i = 0; i < NUM_ROWS; i++) { + // duk_push_object(ctx); + // { + // duk_push_int(ctx, i); + // duk_put_prop_string(ctx, -2, "i"); + // } + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_output_set, 4); + // duk_put_prop_string(ctx, -2, "set"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_index(ctx, outputsIdx, i); + // } + // duk_put_prop_string(ctx, blockIdx, "outputs"); + + // // knobs + // duk_push_object(ctx); + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_knobs_get, 3); + // duk_put_prop_string(ctx, -2, "get"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_string(ctx, blockIdx, "knobs"); + + // // switches + // duk_push_object(ctx); + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_switches_get, 3); + // duk_put_prop_string(ctx, -2, "get"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_string(ctx, blockIdx, "switches"); + + // // lights + // duk_idx_t lightsIdx = duk_push_array(ctx); + // for (int i = 0; i < NUM_ROWS; i++) { + // duk_push_object(ctx); + // { + // duk_push_int(ctx, i); + // duk_put_prop_string(ctx, -2, "i"); + // } + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_light_set, 4); + // duk_put_prop_string(ctx, -2, "set"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_index(ctx, lightsIdx, i); + // } + // duk_put_prop_string(ctx, blockIdx, "lights"); + + // // switchLights + // duk_idx_t switchLightsIdx = duk_push_array(ctx); + // for (int i = 0; i < NUM_ROWS; i++) { + // duk_push_object(ctx); + // { + // duk_push_int(ctx, i); + // duk_put_prop_string(ctx, -2, "i"); + // } + // duk_push_object(ctx); + // { + // duk_push_c_function(ctx, native_block_switchLight_set, 4); + // duk_put_prop_string(ctx, -2, "set"); + // } + // duk_push_proxy(ctx, 0); + // duk_put_prop_index(ctx, switchLightsIdx, i); + // } + // duk_put_prop_string(ctx, blockIdx, "switchLights"); + // } + + // block (put on stack) + duk_idx_t blockIdx = duk_push_object(ctx); + { + // sampleRate + duk_push_undefined(ctx); + duk_put_prop_string(ctx, blockIdx, "sampleRate"); + + // sampleTime + duk_push_undefined(ctx); + duk_put_prop_string(ctx, blockIdx, "sampleTime"); + + // bufferSize + duk_push_undefined(ctx); + duk_put_prop_string(ctx, blockIdx, "bufferSize"); + + // inputs + duk_idx_t inputsIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, inputsIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "inputs"); + + // outputs + duk_idx_t outputsIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, outputsIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "outputs"); + + // knobs + duk_idx_t knobsIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, knobsIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "knobs"); + + // switches + duk_idx_t switchesIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, switchesIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "switches"); + + // lights + duk_idx_t lightsIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_idx_t lightIdx = duk_push_array(ctx); + for (int c = 0; c < 3; c++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, lightIdx, c); + } + duk_put_prop_index(ctx, lightsIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "lights"); + + // switchLights + duk_idx_t switchLightsIdx = duk_push_array(ctx); + for (int i = 0; i < NUM_ROWS; i++) { + duk_idx_t lightIdx = duk_push_array(ctx); + for (int c = 0; c < 3; c++) { + duk_push_undefined(ctx); + duk_put_prop_index(ctx, lightIdx, c); + } + duk_put_prop_index(ctx, switchLightsIdx, i); + } + duk_put_prop_string(ctx, blockIdx, "switchLights"); + } + + return 0; + } + + ~DuktapeEngine() { + duk_destroy_heap(ctx); + } + + std::string getEngineName() override { + return "JavaScript"; + } + + int run(const std::string& path, const std::string& script) override { + duk_push_string(ctx, path.c_str()); + if (duk_pcompile_lstring_filename(ctx, 0, script.c_str(), script.size()) != 0) { + const char* s = duk_safe_to_string(ctx, -1); + setMessage(s); + duk_pop(ctx); + return -1; + } + if (duk_pcall(ctx, 0)) { + const char* s = duk_safe_to_string(ctx, -1); + setMessage(s); + duk_pop(ctx); + return -1; + } + duk_pop(ctx); + + // Get config + duk_get_global_string(ctx, "config"); + { + // frameDivider + duk_get_prop_string(ctx, -1, "frameDivider"); + setFrameDivider(duk_get_int(ctx, -1)); + duk_pop(ctx); + // bufferSize + duk_get_prop_string(ctx, -1, "bufferSize"); + setBufferSize(duk_get_int(ctx, -1)); + duk_pop(ctx); + } + duk_pop(ctx); + + // Put process function on top of stack for faster calling + duk_get_global_string(ctx, "process"); + if (!duk_is_function(ctx, -1)) { + setMessage("No process() function"); + return -1; + } + return 0; + } + + int process(ProcessBlock& block) override { + currentBlock = █ + // Duplicate process function + duk_dup(ctx, -1); + // Duplicate block object + duk_dup(ctx, -3); + + // block + { + // sampleRate + duk_push_number(ctx, block.sampleRate); + duk_put_prop_string(ctx, -2, "sampleRate"); + + // sampleTime + duk_push_number(ctx, block.sampleTime); + duk_put_prop_string(ctx, -2, "sampleTime"); + + // bufferSize + duk_push_number(ctx, block.bufferSize); + duk_put_prop_string(ctx, -2, "bufferSize"); + + // inputs + duk_get_prop_string(ctx, -1, "inputs"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_number(ctx, block.inputs[i][0]); + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + + // outputs + duk_get_prop_string(ctx, -1, "outputs"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_number(ctx, block.outputs[i][0]); + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + + // knobs + duk_get_prop_string(ctx, -1, "knobs"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_number(ctx, block.knobs[i]); + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + + // switches + duk_get_prop_string(ctx, -1, "switches"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_push_boolean(ctx, block.switches[i]); + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + + // lights + duk_get_prop_string(ctx, -1, "lights"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_get_prop_index(ctx, -1, i); + for (int c = 0; c < 3; c++) { + duk_push_number(ctx, block.lights[i][c]); + duk_put_prop_index(ctx, -2, c); + } + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + + // switchLights + duk_get_prop_string(ctx, -1, "switchLights"); + for (int i = 0; i < NUM_ROWS; i++) { + duk_get_prop_index(ctx, -1, i); + for (int c = 0; c < 3; c++) { + duk_push_number(ctx, block.switchLights[i][c]); + duk_put_prop_index(ctx, -2, c); + } + duk_put_prop_index(ctx, -2, i); + } + duk_pop(ctx); + } + + // Call process function + if (duk_pcall(ctx, 1)) { + const char* s = duk_safe_to_string(ctx, -1); + setMessage(s); + duk_pop(ctx); + return -1; + } + // return value + duk_pop(ctx); + currentBlock = NULL; + return 0; + } + + static DuktapeEngine* getDuktapeEngine(duk_context* ctx) { + duk_get_global_string(ctx, DUK_HIDDEN_SYMBOL("p")); + DuktapeEngine* engine = (DuktapeEngine*) duk_get_pointer(ctx, -1); + duk_pop(ctx); + return engine; + } + + static duk_ret_t native_console_log(duk_context* ctx) { + const char* s = duk_safe_to_string(ctx, -1); + rack::INFO("Prototype: %s", s); + return 0; + } + static duk_ret_t native_console_debug(duk_context* ctx) { + const char* s = duk_safe_to_string(ctx, -1); + rack::DEBUG("Prototype: %s", s); + return 0; + } + static duk_ret_t native_console_warn(duk_context* ctx) { + const char* s = duk_safe_to_string(ctx, -1); + rack::WARN("Prototype: %s", s); + return 0; + } + static duk_ret_t native_display(duk_context* ctx) { + const char* s = duk_safe_to_string(ctx, -1); + getDuktapeEngine(ctx)->setMessage(s); + return 0; + } + + // Use thread_local variable instead of storing a user pointer in `ctx`, for performance. + static thread_local ProcessBlock* currentBlock; + + static duk_ret_t native_block_sampleRate_get(duk_context* ctx) { + float sampleRate = currentBlock->sampleRate; + duk_push_number(ctx, sampleRate); + return 1; + } + static duk_ret_t native_block_sampleTime_get(duk_context* ctx) { + float sampleTime = currentBlock->sampleTime; + duk_push_number(ctx, sampleTime); + return 1; + } + static duk_ret_t native_block_bufferSize_get(duk_context* ctx) { + int bufferSize = currentBlock->bufferSize; + duk_push_int(ctx, bufferSize); + return 1; + } + static duk_ret_t native_block_input_get(duk_context* ctx) { + duk_get_prop_string(ctx, -3, "i"); + int index = duk_get_int(ctx, -1); + duk_pop(ctx); + int bufferIndex = duk_get_int(ctx, -2); + float v = currentBlock->inputs[index][bufferIndex]; + duk_push_number(ctx, v); + return 1; + } + static duk_ret_t native_block_output_set(duk_context* ctx) { + duk_get_prop_string(ctx, -4, "i"); + int index = duk_get_int(ctx, -1); + duk_pop(ctx); + int bufferIndex = duk_get_int(ctx, -3); + float v = duk_get_number(ctx, -2); + currentBlock->outputs[index][bufferIndex] = v; + return 0; + } + static duk_ret_t native_block_knobs_get(duk_context* ctx) { + int index = duk_get_int(ctx, -2); + float v = currentBlock->knobs[index]; + duk_push_number(ctx, v); + return 1; + } + static duk_ret_t native_block_switches_get(duk_context* ctx) { + int index = duk_get_int(ctx, -2); + bool s = currentBlock->switches[index]; + duk_push_boolean(ctx, s); + return 1; + } + static duk_ret_t native_block_light_set(duk_context* ctx) { + duk_get_prop_string(ctx, -4, "i"); + int index = duk_get_int(ctx, -1); + duk_pop(ctx); + int c = duk_get_int(ctx, -3); + float v = duk_get_number(ctx, -2); + currentBlock->lights[index][c] = v; + return 0; + } + static duk_ret_t native_block_switchLight_set(duk_context* ctx) { + duk_get_prop_string(ctx, -4, "i"); + int index = duk_get_int(ctx, -1); + duk_pop(ctx); + int c = duk_get_int(ctx, -3); + float v = duk_get_number(ctx, -2); + currentBlock->switchLights[index][c] = v; + return 0; + } +}; + + +thread_local ScriptEngine::ProcessBlock* DuktapeEngine::currentBlock; + + +ScriptEngine* createDuktapeEngine() { + return new DuktapeEngine; +} \ No newline at end of file diff --git a/src/Prototype.cpp b/src/Prototype.cpp new file mode 100644 index 0000000..365cb51 --- /dev/null +++ b/src/Prototype.cpp @@ -0,0 +1,346 @@ +#include +#include +#include +#include +#include +#include +#include "ScriptEngine.hpp" + + +using namespace rack; +Plugin* pluginInstance; + + +struct Prototype : Module { + enum ParamIds { + ENUMS(KNOB_PARAMS, NUM_ROWS), + ENUMS(SWITCH_PARAMS, NUM_ROWS), + NUM_PARAMS + }; + enum InputIds { + ENUMS(IN_INPUTS, NUM_ROWS), + NUM_INPUTS + }; + enum OutputIds { + ENUMS(OUT_OUTPUTS, NUM_ROWS), + NUM_OUTPUTS + }; + enum LightIds { + ENUMS(LIGHT_LIGHTS, NUM_ROWS * 3), + ENUMS(SWITCH_LIGHTS, NUM_ROWS * 3), + NUM_LIGHTS + }; + + std::string path; + std::string script; + std::string engineName; + ScriptEngine* scriptEngine = NULL; + std::mutex scriptMutex; + std::string message; + int frame = 0; + int frameDivider; + ScriptEngine::ProcessBlock block; + int blockIndex = 0; + + Prototype() { + config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); + for (int i = 0; i < NUM_ROWS; i++) + configParam(KNOB_PARAMS + i, 0.f, 1.f, 0.f, string::f("Knob %d", i + 1)); + for (int i = 0; i < NUM_ROWS; i++) + configParam(SWITCH_PARAMS + i, 0.f, 1.f, 0.f, string::f("Switch %d", i + 1)); + + clearScriptEngine(); + } + + ~Prototype() { + std::lock_guard lock(scriptMutex); + clearScriptEngine(); + } + + void onReset() override { + setScriptString(path, script); + } + + void process(const ProcessArgs& args) override { + if (!scriptEngine) + return; + + // Frame divider for reducing sample rate + if (++frame < frameDivider) + return; + frame = 0; + + // Inputs + for (int i = 0; i < NUM_ROWS; i++) + block.inputs[i][blockIndex] = inputs[IN_INPUTS + i].getVoltage(); + // Outputs + for (int i = 0; i < NUM_ROWS; i++) + outputs[OUT_OUTPUTS + i].setVoltage(block.outputs[i][blockIndex]); + + // Block divider + if (++blockIndex < block.bufferSize) + return; + blockIndex = 0; + + // Params + for (int i = 0; i < NUM_ROWS; i++) + block.knobs[i] = params[KNOB_PARAMS + i].getValue(); + for (int i = 0; i < NUM_ROWS; i++) + block.switches[i] = (params[SWITCH_PARAMS + i].getValue() > 0.f); + + // Set other block parameters + block.sampleRate = args.sampleRate; + block.sampleTime = args.sampleTime; + + { + std::lock_guard lock(scriptMutex); + // Check for certain inside the mutex + if (scriptEngine) { + if (scriptEngine->process(block)) { + clearScriptEngine(); + return; + } + } + } + + // Lights + for (int i = 0; i < NUM_ROWS; i++) + for (int c = 0; c < 3; c++) + lights[LIGHT_LIGHTS + i * 3 + c].setBrightness(block.lights[i][c]); + for (int i = 0; i < NUM_ROWS; i++) + for (int c = 0; c < 3; c++) + lights[SWITCH_LIGHTS + i * 3 + c].setBrightness(block.switchLights[i][c]); + } + + void clearScriptEngine() { + if (scriptEngine) { + delete scriptEngine; + scriptEngine = NULL; + } + // Reset outputs because they might hold old values + for (int i = 0; i < NUM_ROWS; i++) + outputs[OUT_OUTPUTS + i].setVoltage(0.f); + for (int i = 0; i < NUM_ROWS; i++) + lights[LIGHT_LIGHTS + i].setBrightness(0.f); + for (int i = 0; i < NUM_ROWS; i++) + lights[SWITCH_LIGHTS + i].setBrightness(0.f); + std::memset(block.inputs, 0, sizeof(block.inputs)); + // Reset settings + frameDivider = 32; + block.bufferSize = 1; + } + + void setScriptString(std::string path, std::string script) { + std::lock_guard lock(scriptMutex); + message = ""; + this->path = ""; + this->script = ""; + this->engineName = ""; + clearScriptEngine(); + // Get ScriptEngine from path extension + if (path == "") { + return; + } + std::string ext = string::filenameExtension(string::filename(path)); + scriptEngine = createScriptEngine(ext); + if (!scriptEngine) { + message = string::f("No engine for .%s extension", ext.c_str()); + return; + } + this->path = path; + this->script = script; + this->engineName = scriptEngine->getEngineName(); + // Initialize ScriptEngine + scriptEngine->module = this; + if (scriptEngine->initialize()) { + // Error message should have been set by ScriptEngine + clearScriptEngine(); + return; + } + // Read file + std::ifstream file; + file.exceptions(std::ifstream::failbit | std::ifstream::badbit); + try { + file.open(this->path); + std::stringstream buffer; + buffer << file.rdbuf(); + this->script = buffer.str(); + } + catch (const std::runtime_error& err) { + } + // Run script + if (this->script == "") { + message = "Could not load script."; + clearScriptEngine(); + return; + } + if (scriptEngine->run(this->path, this->script)) { + // Error message should have been set by ScriptEngine + clearScriptEngine(); + return; + } + } + + json_t* dataToJson() override { + json_t* rootJ = json_object(); + + json_object_set_new(rootJ, "path", json_string(path.c_str())); + json_object_set_new(rootJ, "script", json_stringn(script.data(), script.size())); + + return rootJ; + } + + void dataFromJson(json_t* rootJ) override { + json_t* pathJ = json_object_get(rootJ, "path"); + json_t* scriptJ = json_object_get(rootJ, "script"); + if (pathJ && scriptJ) { + std::string path = json_string_value(pathJ); + std::string script = std::string(json_string_value(scriptJ), json_string_length(scriptJ)); + setScriptString(path, script); + } + } +}; + + +void ScriptEngine::setMessage(const std::string& message) { + module->message = message; +} +int ScriptEngine::getFrameDivider() { + return module->frameDivider; +} +void ScriptEngine::setFrameDivider(int frameDivider) { + module->frameDivider = frameDivider; +} +int ScriptEngine::getBufferSize() { + return module->block.bufferSize; +} +void ScriptEngine::setBufferSize(int bufferSize) { + module->block.bufferSize = bufferSize; +} + + +struct FileChoice : LedDisplayChoice { + Prototype* module; + + void step() override { + if (module && module->engineName != "") + text = module->engineName; + else + text = "Script"; + text += ": "; + if (module && module->path != "") + text += string::filename(module->path); + else + text += "(click to load)"; + } + + void onAction(const event::Action& e) override { + std::string dir = asset::user(""); + char* pathC = osdialog_file(OSDIALOG_OPEN, dir.c_str(), NULL, NULL); + if (!pathC) { + return; + } + std::string path = pathC; + std::free(pathC); + + module->setScriptString(path, ""); + } +}; + + +struct MessageChoice : LedDisplayChoice { + Prototype* module; + + void step() override { + text = module ? module->message : ""; + } +}; + + +struct PrototypeDisplay : LedDisplay { + PrototypeDisplay() { + box.size = mm2px(Vec(69.879, 27.335)); + } + + void setModule(Prototype* module) { + FileChoice* fileChoice = new FileChoice; + fileChoice->box.size.x = box.size.x; + fileChoice->module = module; + addChild(fileChoice); + + LedDisplaySeparator* fileSeparator = new LedDisplaySeparator; + fileSeparator->box.size.x = box.size.x; + fileSeparator->box.pos = fileChoice->box.getBottomLeft(); + addChild(fileSeparator); + + MessageChoice* messageChoice = new MessageChoice; + messageChoice->box.pos = fileChoice->box.getBottomLeft(); + messageChoice->box.size.x = box.size.x; + messageChoice->module = module; + addChild(messageChoice); + } +}; + + +struct PrototypeWidget : ModuleWidget { + PrototypeWidget(Prototype* module) { + setModule(module); + setPanel(APP->window->loadSvg(asset::plugin(pluginInstance, "res/Prototype.svg"))); + + addChild(createWidget(Vec(RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, 0))); + addChild(createWidget(Vec(RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + addChild(createWidget(Vec(box.size.x - 2 * RACK_GRID_WIDTH, RACK_GRID_HEIGHT - RACK_GRID_WIDTH))); + + addParam(createParamCentered(mm2px(Vec(8.099, 64.401)), module, Prototype::KNOB_PARAMS + 0)); + addParam(createParamCentered(mm2px(Vec(20.099, 64.401)), module, Prototype::KNOB_PARAMS + 1)); + addParam(createParamCentered(mm2px(Vec(32.099, 64.401)), module, Prototype::KNOB_PARAMS + 2)); + addParam(createParamCentered(mm2px(Vec(44.099, 64.401)), module, Prototype::KNOB_PARAMS + 3)); + addParam(createParamCentered(mm2px(Vec(56.099, 64.401)), module, Prototype::KNOB_PARAMS + 4)); + addParam(createParamCentered(mm2px(Vec(68.099, 64.401)), module, Prototype::KNOB_PARAMS + 5)); + addParam(createParamCentered(mm2px(Vec(8.099, 80.151)), module, Prototype::SWITCH_PARAMS + 0)); + addParam(createParamCentered(mm2px(Vec(20.099, 80.151)), module, Prototype::SWITCH_PARAMS + 1)); + addParam(createParamCentered(mm2px(Vec(32.099, 80.151)), module, Prototype::SWITCH_PARAMS + 2)); + addParam(createParamCentered(mm2px(Vec(44.099, 80.151)), module, Prototype::SWITCH_PARAMS + 3)); + addParam(createParamCentered(mm2px(Vec(56.099, 80.151)), module, Prototype::SWITCH_PARAMS + 4)); + addParam(createParamCentered(mm2px(Vec(68.099, 80.151)), module, Prototype::SWITCH_PARAMS + 5)); + + addInput(createInputCentered(mm2px(Vec(8.099, 96.025)), module, Prototype::IN_INPUTS + 0)); + addInput(createInputCentered(mm2px(Vec(20.099, 96.025)), module, Prototype::IN_INPUTS + 1)); + addInput(createInputCentered(mm2px(Vec(32.099, 96.025)), module, Prototype::IN_INPUTS + 2)); + addInput(createInputCentered(mm2px(Vec(44.099, 96.025)), module, Prototype::IN_INPUTS + 3)); + addInput(createInputCentered(mm2px(Vec(56.099, 96.025)), module, Prototype::IN_INPUTS + 4)); + addInput(createInputCentered(mm2px(Vec(68.099, 96.025)), module, Prototype::IN_INPUTS + 5)); + + addOutput(createOutputCentered(mm2px(Vec(8.099, 112.25)), module, Prototype::OUT_OUTPUTS + 0)); + addOutput(createOutputCentered(mm2px(Vec(20.099, 112.25)), module, Prototype::OUT_OUTPUTS + 1)); + addOutput(createOutputCentered(mm2px(Vec(32.099, 112.25)), module, Prototype::OUT_OUTPUTS + 2)); + addOutput(createOutputCentered(mm2px(Vec(44.099, 112.25)), module, Prototype::OUT_OUTPUTS + 3)); + addOutput(createOutputCentered(mm2px(Vec(56.099, 112.25)), module, Prototype::OUT_OUTPUTS + 4)); + addOutput(createOutputCentered(mm2px(Vec(68.099, 112.25)), module, Prototype::OUT_OUTPUTS + 5)); + + addChild(createLightCentered>(mm2px(Vec(8.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 0)); + addChild(createLightCentered>(mm2px(Vec(20.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 1)); + addChild(createLightCentered>(mm2px(Vec(32.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 2)); + addChild(createLightCentered>(mm2px(Vec(44.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 3)); + addChild(createLightCentered>(mm2px(Vec(56.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 4)); + addChild(createLightCentered>(mm2px(Vec(68.099, 51.4)), module, Prototype::LIGHT_LIGHTS + 3 * 5)); + addChild(createLightCentered>(mm2px(Vec(8.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 0)); + addChild(createLightCentered>(mm2px(Vec(20.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 1)); + addChild(createLightCentered>(mm2px(Vec(32.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 2)); + addChild(createLightCentered>(mm2px(Vec(44.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 3)); + addChild(createLightCentered>(mm2px(Vec(56.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 4)); + addChild(createLightCentered>(mm2px(Vec(68.099, 80.151)), module, Prototype::SWITCH_LIGHTS + 3 * 5)); + + PrototypeDisplay* display = createWidget(mm2px(Vec(3.16, 14.837))); + display->setModule(module); + addChild(display); + } +}; + + +void init(Plugin* p) { + pluginInstance = p; + + p->addModel(createModel("Prototype")); +} diff --git a/src/ScriptEngine.hpp b/src/ScriptEngine.hpp new file mode 100644 index 0000000..ea515f4 --- /dev/null +++ b/src/ScriptEngine.hpp @@ -0,0 +1,64 @@ +#pragma once +#include + + +static const int NUM_ROWS = 6; +static const int MAX_BUFFER_SIZE = 4096; + + +struct Prototype; + + +struct ScriptEngine { + // Virtual methods for subclasses + /** Constructor. + Return nonzero if failure, and set error message with setMessage(). + */ + virtual int initialize() {return 0;} + virtual ~ScriptEngine() {} + virtual std::string getEngineName() {return "";} + /** Executes the script. + Return nonzero if failure, and set error message with setMessage(). + Called only once per instance. + */ + virtual int run(const std::string& path, const std::string& script) {return 0;} + + struct ProcessBlock { + float sampleRate = 0.f; + float sampleTime = 0.f; + int bufferSize = 1; + float inputs[NUM_ROWS][MAX_BUFFER_SIZE] = {}; + float outputs[NUM_ROWS][MAX_BUFFER_SIZE] = {}; + float knobs[NUM_ROWS] = {}; + bool switches[NUM_ROWS] = {}; + float lights[NUM_ROWS][3] = {}; + float switchLights[NUM_ROWS][3] = {}; + }; + /** Calls the script's process() method. + Return nonzero if failure, and set error message with setMessage(). + */ + virtual int process(ProcessBlock& block) {return 0;} + + // Communication with Prototype module + void setMessage(const std::string& message); + int getFrameDivider(); + void setFrameDivider(int frameDivider); + int getBufferSize(); + void setBufferSize(int bufferSize); + // private + Prototype* module; +}; + + +// List of ScriptEngines + +// Add your createMyEngine() function here. +ScriptEngine* createDuktapeEngine(); + +inline ScriptEngine* createScriptEngine(std::string ext) { + ext = rack::string::lowercase(ext); + if (ext == "js") + return createDuktapeEngine(); + // Add your file extension check here. + return NULL; +}