@@ -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; | |||
}; | |||