From 411d83f84fe26cded790883a83131ef0f4f30435 Mon Sep 17 00:00:00 2001 From: Andrew Belt Date: Sun, 15 Sep 2019 14:51:01 -0400 Subject: [PATCH] Update readme. Add buffering to vco.js. --- README.md | 56 +++++++++++++++++++++++++------------------ examples/rainbow.js | 10 ++++---- examples/vco.js | 23 +++++++++++------- src/DuktapeEngine.cpp | 7 +++--- 4 files changed, 56 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 823cef1..34c60c3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Scripting language host for [VCV Rack](https://vcvrack.com/) containing: ## Scripting API 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]` vs `args.getInput(i)` vs `input(i)`), but the functionality should be similar. +Other script engines may vary in their syntax (e.g. `block.inputs[i]` vs `block.getInput(i)` vs `input(i)`), but the functionality should be similar. ```js /** Display message on LED display. @@ -22,50 +22,60 @@ display(message) /** 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 is recommended, but it will consume lots of CPU. +For audio generators and processors, 1 is recommended, but use `bufferSize` below. If this is too slow for your purposes, consider writing a C++ plugin, since native VCV Rack plugins have 10-100x better performance. */ config.frameDivider // 32 -/** Called when the next args is ready to be processed. +/** Instead of calling process() every sample frame, hold this many input/output voltages in a buffer and call process() when it is full. +This decreases CPU usage, since processing buffers is faster than processing one frame at a time. +The total latency of your script is `config.frameDivider * config.bufferSize * block.sampleTime`. */ -function process(args) { +config.bufferSize // 1 + +/** Called when the next block is ready to be processed. +*/ +function process(block) { /** Engine sample rate in Hz. Read-only. */ - args.sampleRate + block.sampleRate /** Engine sample timestep in seconds. Equal to `1 / sampleRate`. Read-only. - Note that the actual time between process() calls is `args.sampleTime * config.frameDivider`. + Note that the actual time between process() calls is `block.sampleTime * config.frameDivider`. + */ + block.sampleTime + + /** The actual size of the input/output buffers. */ - args.sampleTime + block.bufferSize - /** Voltage of the input port of row `i`. Read-only. + /** Voltage of the input port of row `rowIndex`. Read-only. */ - args.inputs[i] // 0.0 + block.inputs[rowIndex][bufferIndex] // 0.0 - /** Voltage of the output port of row `i`. Writable. + /** Voltage of the output port of row `rowIndex`. Writable. */ - args.outputs[i] // 0.0 + block.outputs[rowIndex][bufferIndex] // 0.0 - /** Value of the knob of row `i`. Between 0 and 1. Read-only. + /** Value of the knob of row `rowIndex`. Between 0 and 1. Read-only. */ - args.knobs[i] // 0.0 + block.knobs[rowIndex] // 0.0 - /** Pressed state of the switch of row `i`. Read-only. + /** Pressed state of the switch of row `rowIndex`. Read-only. */ - args.switches[i] // false + block.switches[rowIndex] // false - /** Brightness of the RGB LED of row `i`. Writable. + /** Brightness of the RGB LED of row `rowIndex`, between 0 and 1. Writable. */ - args.lights[i].r // 0.0 - args.lights[i].g // 0.0 - args.lights[i].b // 0.0 + block.lights[rowIndex][0] // 0.0 (red) + block.lights[rowIndex][1] // 0.0 (green) + block.lights[rowIndex][2] // 0.0 (blue) - /** Brightness of the switch RGB LED of row `i`. Writable. + /** Brightness of the switch RGB LED of row `rowIndex`. Writable. */ - args.switchLights[i].r // 0.0 - args.switchLights[i].g // 0.0 - args.switchLights[i].b // 0.0 + block.switchLights[rowIndex][0] // 0.0 (red) + block.switchLights[rowIndex][1] // 0.0 (green) + block.switchLights[rowIndex][2] // 0.0 (blue) } ``` diff --git a/examples/rainbow.js b/examples/rainbow.js index 399e83b..c9ac0f2 100644 --- a/examples/rainbow.js +++ b/examples/rainbow.js @@ -23,16 +23,16 @@ function hsvToRgb(h, s, v) { var phase = 0 -function process(args) { - phase += args.sampleTime * config.frameDivider * 0.5 +function process(block) { + phase += block.sampleTime * config.frameDivider * 0.5 phase %= 1 for (var i = 0; i < 6; i++) { var h = (1 - i / 6 + phase) % 1 var rgb = hsvToRgb(h, 1, 1) - args.lights[i] = rgb - args.switchLights[i] = rgb - args.outputs[i][0] = Math.sin(2 * Math.PI * h) * 5 + 5 + block.lights[i] = rgb + block.switchLights[i] = rgb + block.outputs[i][0] = Math.sin(2 * Math.PI * h) * 5 + 5 } } diff --git a/examples/vco.js b/examples/vco.js index b62a39d..f51d194 100644 --- a/examples/vco.js +++ b/examples/vco.js @@ -4,13 +4,14 @@ // JavaScript isn't ideal for audio generating and processing due to it being 10-100 less efficient than C++, but it's still an easy way to learn simple DSP. config.frameDivider = 1 +config.bufferSize = 16 var phase = 0 -function process(args) { +function process(block) { // Knob ranges from -5 to 5 octaves - var pitch = args.knobs[0] * 10 - 5 + var pitch = block.knobs[0] * 10 - 5 // Input follows 1V/oct standard - pitch += args.inputs[0][0] + pitch += block.inputs[0][0] // The relationship between 1V/oct pitch and frequency is `freq = 2^pitch`. // Default frequency is middle C (C4) in Hz. @@ -18,11 +19,15 @@ function process(args) { var freq = 261.6256 * Math.pow(2, pitch) display("Freq: " + freq.toFixed(3) + " Hz") - // Accumulate phase - phase += args.sampleTime * config.frameDivider * freq - // Wrap phase around range [0, 1] - phase %= 1 + // Set all output samples in block + var deltaPhase = block.sampleTime * config.frameDivider * freq + for (var i = 0; i < block.bufferSize; i++) { + // Accumulate phase + phase += deltaPhase + // Wrap phase around range [0, 1] + phase %= 1 - // Convert phase to sine output - args.outputs[0][0] = Math.sin(2 * Math.PI * phase) * 5 + // Convert phase to sine output + block.outputs[0][i] = Math.sin(2 * Math.PI * phase) * 5 + } } \ No newline at end of file diff --git a/src/DuktapeEngine.cpp b/src/DuktapeEngine.cpp index b1d52f5..d56412a 100644 --- a/src/DuktapeEngine.cpp +++ b/src/DuktapeEngine.cpp @@ -106,10 +106,7 @@ struct DuktapeEngine : ScriptEngine { // block (keep on stack) duk_idx_t blockIdx = duk_push_object(ctx); { - // bufferSize int bufferSize = getBufferSize(); - duk_push_int(ctx, bufferSize); - duk_put_prop_string(ctx, blockIdx, "bufferSize"); // inputs duk_idx_t inputsIdx = duk_push_array(ctx); @@ -191,6 +188,10 @@ struct DuktapeEngine : ScriptEngine { duk_push_number(ctx, block.sampleTime); duk_put_prop_string(ctx, blockIdx, "sampleTime"); + // bufferSize + duk_push_int(ctx, block.bufferSize); + duk_put_prop_string(ctx, blockIdx, "bufferSize"); + // inputs duk_get_prop_string(ctx, blockIdx, "inputs"); for (int i = 0; i < NUM_ROWS; i++) {