From 1c6998ef80ddf1351dc5e8b1d626c1830a0dfd47 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Sun, 15 Sep 2019 00:49:41 -0400 Subject: [PATCH] Replace ProcessBlock with direct access via ScriptEngine methods. Update readme. --- README.md | 89 ++++++++++++++++++++++++++----------------- examples/hello.js | 8 ++-- src/DuktapeEngine.cpp | 66 ++++++++++++++++---------------- src/Prototype.cpp | 55 +++++++++++++------------- src/ScriptEngine.hpp | 22 +++++------ 5 files changed, 129 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index 30df8c2..b403cba 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/examples/hello.js b/examples/hello.js index 3f79a95..1ae5206 100644 --- a/examples/hello.js +++ b/examples/hello.js @@ -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!") diff --git a/src/DuktapeEngine.cpp b/src/DuktapeEngine.cpp index 58cb437..7ab7b84 100644 --- a/src/DuktapeEngine.cpp +++ b/src/DuktapeEngine.cpp @@ -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) { diff --git a/src/Prototype.cpp b/src/Prototype.cpp index 97f7471..1e20707 100644 --- a/src/Prototype.cpp +++ b/src/Prototype.cpp @@ -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 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 { diff --git a/src/ScriptEngine.hpp b/src/ScriptEngine.hpp index 0e64a39..a486e86 100644 --- a/src/ScriptEngine.hpp +++ b/src/ScriptEngine.hpp @@ -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; };