Browse Source

Replace ProcessBlock with direct access via ScriptEngine methods. Update readme.

tags/v1.1.0
Andrew Belt 5 years ago
parent
commit
1c6998ef80
5 changed files with 129 additions and 111 deletions
  1. +55
    -34
      README.md
  2. +3
    -5
      examples/hello.js
  3. +33
    -33
      src/DuktapeEngine.cpp
  4. +27
    -28
      src/Prototype.cpp
  5. +11
    -11
      src/ScriptEngine.hpp

+ 55
- 34
README.md View File

@@ -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.


+ 3
- 5
examples/hello.js View File

@@ -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!")

+ 33
- 33
src/DuktapeEngine.cpp View File

@@ -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) {


+ 27
- 28
src/Prototype.cpp View File

@@ -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 {


+ 11
- 11
src/ScriptEngine.hpp View File

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


Loading…
Cancel
Save