| @@ -7,52 +7,73 @@ Scripting language host for [VCV Rack](https://vcvrack.com/) containing: | |||
| - 6 lights (RGB LEDs) | |||
| - 6 switches with RGB LEDs | |||
| [Discussion thread](https://community.vcvrack.com/t/vcv-prototype/3271/1) | |||
| ### 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)`). | |||
| This is the reference API for the JavaScript script engine, along with default property values. | |||
| Other script engines may vary in their syntax (e.g. `args.inputs[i][j]` vs `args.getInput(i, j)` vs `input(i, j)`), but the functionality should be similar. | |||
| ```js | |||
| // Display message on LED display. | |||
| /** 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] | |||
| /** Skip this many sample frames before running process(). | |||
| For CV generators and processors, 256 is reasonable. | |||
| For sequencers, 32 is reasonable since process() will be called every 0.7ms with a 44100kHz sample rate, which will capture 1ms-long triggers. | |||
| For audio generators and processors, 1-8 is recommended, but it will consume lots of CPU. | |||
| If this is too slow for your purposes, you should just write a C++ plugin. | |||
| */ | |||
| config.frameDivider // 32 | |||
| /** Called when the next args is ready to be processed. | |||
| */ | |||
| function process(args) { | |||
| /** Engine sample rate in Hz. Read-only. | |||
| */ | |||
| args.sampleRate | |||
| /** Engine sample timestep in seconds. Equal to `1 / sampleRate`. Read-only. | |||
| */ | |||
| args.sampleTime | |||
| /** Voltage of the input port of row `i`. Read-only. | |||
| */ | |||
| args.inputs[i] // 0.0 | |||
| /** Voltage of the output port of row `i`. Writable. | |||
| */ | |||
| args.outputs[i] // 0.0 | |||
| /** Value of the knob of row `i`. Between 0 and 1. Read-only. | |||
| */ | |||
| args.knobs[i] // 0.0 | |||
| /** Pressed state of the switch of row `i`. Read-only. | |||
| */ | |||
| args.switches[i] // false | |||
| /** Brightness of the RGB LED of row `i`. Writable. | |||
| */ | |||
| args.lights[i].r // 0.0 | |||
| args.lights[i].g // 0.0 | |||
| args.lights[i].b // 0.0 | |||
| /** Brightness of the switch RGB LED of row `i`. Writable. | |||
| */ | |||
| args.switchLights[i].r // 0.0 | |||
| args.switchLights[i].g // 0.0 | |||
| args.switchLights[i].b // 0.0 | |||
| } | |||
| ``` | |||
| ### 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. | |||
| - Create a `MyEngine.cpp` file (for example) 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. | |||
| - Build and test the plugin. | |||
| - 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. | |||
| @@ -1,5 +1,5 @@ | |||
| // Call process() every 128 audio samples | |||
| config.frameDivider = 128 | |||
| // Call process() every 256 audio samples | |||
| config.frameDivider = 256 | |||
| // From https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB | |||
| @@ -25,14 +25,12 @@ function process(args) { | |||
| phase %= 1 | |||
| for (var i = 0; i < 6; i++) { | |||
| var h = (i / 6 + phase) % 1 | |||
| var h = (1 - i / 6 + phase) % 1 | |||
| var rgb = hsvToRgb(h, 1, 1) | |||
| args.lights[i] = rgb | |||
| args.switchLights[i] = rgb | |||
| args.outputs[i] = Math.sin(2 * Math.PI * h) * 10 | |||
| } | |||
| display(phase) | |||
| } | |||
| display("Hello, world!") | |||
| @@ -94,8 +94,8 @@ struct DuktapeEngine : ScriptEngine { | |||
| return -1; | |||
| } | |||
| // block (keep on stack) | |||
| duk_idx_t blockIdx = duk_push_object(ctx); | |||
| // args (keep on stack) | |||
| duk_idx_t argsIdx = duk_push_object(ctx); | |||
| { | |||
| // inputs | |||
| duk_idx_t inputsIdx = duk_push_array(ctx); | |||
| @@ -103,7 +103,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_push_number(ctx, 0.0); | |||
| duk_put_prop_index(ctx, inputsIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "inputs"); | |||
| duk_put_prop_string(ctx, argsIdx, "inputs"); | |||
| // outputs | |||
| duk_idx_t outputsIdx = duk_push_array(ctx); | |||
| @@ -111,7 +111,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_push_number(ctx, 0.0); | |||
| duk_put_prop_index(ctx, outputsIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "outputs"); | |||
| duk_put_prop_string(ctx, argsIdx, "outputs"); | |||
| // knobs | |||
| duk_idx_t knobsIdx = duk_push_array(ctx); | |||
| @@ -119,7 +119,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_push_number(ctx, 0.0); | |||
| duk_put_prop_index(ctx, knobsIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "knobs"); | |||
| duk_put_prop_string(ctx, argsIdx, "knobs"); | |||
| // switches | |||
| duk_idx_t switchesIdx = duk_push_array(ctx); | |||
| @@ -127,7 +127,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_push_number(ctx, 0.0); | |||
| duk_put_prop_index(ctx, switchesIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "switches"); | |||
| duk_put_prop_string(ctx, argsIdx, "switches"); | |||
| // lights | |||
| duk_idx_t lightsIdx = duk_push_array(ctx); | |||
| @@ -143,7 +143,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| } | |||
| duk_put_prop_index(ctx, lightsIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "lights"); | |||
| duk_put_prop_string(ctx, argsIdx, "lights"); | |||
| // switchLights | |||
| duk_idx_t switchLightsIdx = duk_push_array(ctx); | |||
| @@ -159,44 +159,44 @@ struct DuktapeEngine : ScriptEngine { | |||
| } | |||
| duk_put_prop_index(ctx, switchLightsIdx, i); | |||
| } | |||
| duk_put_prop_string(ctx, blockIdx, "switchLights"); | |||
| duk_put_prop_string(ctx, argsIdx, "switchLights"); | |||
| } | |||
| return 0; | |||
| } | |||
| int process(ProcessBlock& block) override { | |||
| // block | |||
| duk_idx_t blockIdx = duk_get_top(ctx) - 1; | |||
| int process(ProcessArgs& args) override { | |||
| // args | |||
| duk_idx_t argsIdx = duk_get_top(ctx) - 1; | |||
| { | |||
| // sampleRate | |||
| duk_push_number(ctx, block.sampleRate); | |||
| duk_put_prop_string(ctx, blockIdx, "sampleRate"); | |||
| duk_push_number(ctx, args.sampleRate); | |||
| duk_put_prop_string(ctx, argsIdx, "sampleRate"); | |||
| // sampleTime | |||
| duk_push_number(ctx, block.sampleTime); | |||
| duk_put_prop_string(ctx, blockIdx, "sampleTime"); | |||
| duk_push_number(ctx, args.sampleTime); | |||
| duk_put_prop_string(ctx, argsIdx, "sampleTime"); | |||
| // inputs | |||
| duk_get_prop_string(ctx, blockIdx, "inputs"); | |||
| duk_get_prop_string(ctx, argsIdx, "inputs"); | |||
| for (int i = 0; i < NUM_ROWS; i++) { | |||
| duk_push_number(ctx, block.inputs[i]); | |||
| duk_push_number(ctx, getInput(i)); | |||
| duk_put_prop_index(ctx, -2, i); | |||
| } | |||
| duk_pop(ctx); | |||
| // knobs | |||
| duk_get_prop_string(ctx, blockIdx, "knobs"); | |||
| duk_get_prop_string(ctx, argsIdx, "knobs"); | |||
| for (int i = 0; i < NUM_ROWS; i++) { | |||
| duk_push_number(ctx, block.knobs[i]); | |||
| duk_push_number(ctx, getKnob(i)); | |||
| duk_put_prop_index(ctx, -2, i); | |||
| } | |||
| duk_pop(ctx); | |||
| // switches | |||
| duk_get_prop_string(ctx, blockIdx, "switches"); | |||
| duk_get_prop_string(ctx, argsIdx, "switches"); | |||
| for (int i = 0; i < NUM_ROWS; i++) { | |||
| duk_push_boolean(ctx, block.switches[i]); | |||
| duk_push_boolean(ctx, getSwitch(i)); | |||
| duk_put_prop_index(ctx, -2, i); | |||
| } | |||
| duk_pop(ctx); | |||
| @@ -204,7 +204,7 @@ struct DuktapeEngine : ScriptEngine { | |||
| // Duplicate process function | |||
| duk_dup(ctx, -2); | |||
| // Duplicate block object | |||
| // Duplicate args object | |||
| duk_dup(ctx, -2); | |||
| // Call process function | |||
| if (duk_pcall(ctx, 1)) { | |||
| @@ -216,13 +216,13 @@ struct DuktapeEngine : ScriptEngine { | |||
| // return value | |||
| duk_pop(ctx); | |||
| // block | |||
| // args | |||
| { | |||
| // outputs | |||
| duk_get_prop_string(ctx, -1, "outputs"); | |||
| for (int i = 0; i < NUM_ROWS; i++) { | |||
| duk_get_prop_index(ctx, -1, i); | |||
| block.outputs[i] = duk_get_number(ctx, -1); | |||
| setOutput(i, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| } | |||
| duk_pop(ctx); | |||
| @@ -233,13 +233,13 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_get_prop_index(ctx, -1, i); | |||
| { | |||
| duk_get_prop_string(ctx, -1, "r"); | |||
| block.lights[i][0] = duk_get_number(ctx, -1); | |||
| setLight(i, 0, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| duk_get_prop_string(ctx, -1, "g"); | |||
| block.lights[i][1] = duk_get_number(ctx, -1); | |||
| setLight(i, 1, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| duk_get_prop_string(ctx, -1, "b"); | |||
| block.lights[i][2] = duk_get_number(ctx, -1); | |||
| setLight(i, 2, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| } | |||
| duk_pop(ctx); | |||
| @@ -252,13 +252,13 @@ struct DuktapeEngine : ScriptEngine { | |||
| duk_get_prop_index(ctx, -1, i); | |||
| { | |||
| duk_get_prop_string(ctx, -1, "r"); | |||
| block.switchLights[i][0] = duk_get_number(ctx, -1); | |||
| setSwitchLight(i, 0, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| duk_get_prop_string(ctx, -1, "g"); | |||
| block.switchLights[i][1] = duk_get_number(ctx, -1); | |||
| setSwitchLight(i, 1, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| duk_get_prop_string(ctx, -1, "b"); | |||
| block.switchLights[i][2] = duk_get_number(ctx, -1); | |||
| setSwitchLight(i, 2, duk_get_number(ctx, -1)); | |||
| duk_pop(ctx); | |||
| } | |||
| duk_pop(ctx); | |||
| @@ -278,17 +278,17 @@ struct DuktapeEngine : ScriptEngine { | |||
| static duk_ret_t native_console_log(duk_context* ctx) { | |||
| const char* s = duk_safe_to_string(ctx, -1); | |||
| rack::INFO("Prototype: %s", s); | |||
| rack::INFO("VCV 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); | |||
| rack::DEBUG("VCV 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); | |||
| rack::WARN("VCV Prototype: %s", s); | |||
| return 0; | |||
| } | |||
| static duk_ret_t native_display(duk_context* ctx) { | |||
| @@ -31,15 +31,14 @@ struct Prototype : Module { | |||
| NUM_LIGHTS | |||
| }; | |||
| std::string message; | |||
| std::string path; | |||
| std::string script; | |||
| std::string engineName; | |||
| ScriptEngine* scriptEngine = NULL; | |||
| std::mutex scriptMutex; | |||
| std::string message; | |||
| ScriptEngine* scriptEngine = NULL; | |||
| int frame = 0; | |||
| int frameDivider; | |||
| ScriptEngine::ProcessBlock block; | |||
| Prototype() { | |||
| config(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS); | |||
| @@ -65,40 +64,20 @@ struct Prototype : Module { | |||
| return; | |||
| frame = 0; | |||
| // Inputs | |||
| for (int i = 0; i < NUM_ROWS; i++) | |||
| block.inputs[i] = inputs[IN_INPUTS + i].getVoltage(); | |||
| // 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; | |||
| ScriptEngine::ProcessArgs scriptArgs; | |||
| scriptArgs.sampleRate = args.sampleRate; | |||
| scriptArgs.sampleTime = args.sampleTime; | |||
| { | |||
| std::lock_guard<std::mutex> lock(scriptMutex); | |||
| // Check for certain inside the mutex | |||
| if (scriptEngine) { | |||
| if (scriptEngine->process(block)) { | |||
| if (scriptEngine->process(scriptArgs)) { | |||
| clearScriptEngine(); | |||
| return; | |||
| } | |||
| } | |||
| } | |||
| // Outputs | |||
| for (int i = 0; i < NUM_ROWS; i++) | |||
| outputs[OUT_OUTPUTS + i].setVoltage(block.outputs[i]); | |||
| // 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() { | |||
| @@ -115,9 +94,9 @@ struct Prototype : Module { | |||
| for (int i = 0; i < NUM_ROWS; i++) | |||
| for (int c = 0; c < 3; c++) | |||
| lights[SWITCH_LIGHTS + i * 3 + c].setBrightness(0.f); | |||
| std::memset(block.inputs, 0, sizeof(block.inputs)); | |||
| // Reset settings | |||
| frameDivider = 32; | |||
| frame = 0; | |||
| } | |||
| void setScriptString(std::string path, std::string script) { | |||
| @@ -131,6 +110,7 @@ struct Prototype : Module { | |||
| if (path == "") { | |||
| return; | |||
| } | |||
| INFO("Loading script %s", path.c_str()); | |||
| std::string ext = string::filenameExtension(string::filename(path)); | |||
| scriptEngine = createScriptEngine(ext); | |||
| if (!scriptEngine) { | |||
| @@ -164,6 +144,7 @@ struct Prototype : Module { | |||
| clearScriptEngine(); | |||
| return; | |||
| } | |||
| INFO("Successfully ran script %s", this->path.c_str()); | |||
| } | |||
| json_t* dataToJson() override { | |||
| @@ -200,6 +181,24 @@ int ScriptEngine::getFrameDivider() { | |||
| void ScriptEngine::setFrameDivider(int frameDivider) { | |||
| module->frameDivider = frameDivider; | |||
| } | |||
| float ScriptEngine::getInput(int index) { | |||
| return module->inputs[Prototype::IN_INPUTS + index].getVoltage(); | |||
| } | |||
| void ScriptEngine::setOutput(int index, float voltage) { | |||
| module->outputs[Prototype::OUT_OUTPUTS + index].setVoltage(voltage); | |||
| } | |||
| float ScriptEngine::getKnob(int index) { | |||
| return module->params[Prototype::KNOB_PARAMS + index].getValue(); | |||
| } | |||
| bool ScriptEngine::getSwitch(int index) { | |||
| return module->params[Prototype::SWITCH_PARAMS + index].getValue() > 0.f; | |||
| } | |||
| void ScriptEngine::setLight(int index, int color, float brightness) { | |||
| module->lights[Prototype::LIGHT_LIGHTS + index * 3 + color].setBrightness(brightness); | |||
| } | |||
| void ScriptEngine::setSwitchLight(int index, int color, float brightness) { | |||
| module->lights[Prototype::SWITCH_LIGHTS + index * 3 + color].setBrightness(brightness); | |||
| } | |||
| struct FileChoice : LedDisplayChoice { | |||
| @@ -18,26 +18,26 @@ struct ScriptEngine { | |||
| */ | |||
| virtual int run(const std::string& path, const std::string& script) {return 0;} | |||
| struct ProcessBlock { | |||
| float sampleRate = 0.f; | |||
| float sampleTime = 0.f; | |||
| float inputs[NUM_ROWS] = {}; | |||
| float outputs[NUM_ROWS] = {}; | |||
| float knobs[NUM_ROWS] = {}; | |||
| bool switches[NUM_ROWS] = {}; | |||
| float lights[NUM_ROWS][3] = {}; | |||
| float switchLights[NUM_ROWS][3] = {}; | |||
| struct ProcessArgs { | |||
| float sampleRate; | |||
| float sampleTime; | |||
| }; | |||
| /** Calls the script's process() method. | |||
| Return nonzero if failure, and set error message with setMessage(). | |||
| */ | |||
| virtual int process(ProcessBlock& block) {return 0;} | |||
| virtual int process(ProcessArgs& block) {return 0;} | |||
| // Communication with Prototype module. | |||
| // These cannot be called from the constructor, so initialize in the run() method. | |||
| // These cannot be called from your constructor, so initialize your engine in the run() method. | |||
| void setMessage(const std::string& message); | |||
| int getFrameDivider(); | |||
| void setFrameDivider(int frameDivider); | |||
| float getInput(int index); | |||
| void setOutput(int index, float voltage); | |||
| float getKnob(int index); | |||
| bool getSwitch(int index); | |||
| void setLight(int index, int color, float brightness); | |||
| void setSwitchLight(int index, int color, float brightness); | |||
| // private | |||
| Prototype* module; | |||
| }; | |||