@@ -18,6 +18,13 @@ QUICKJS ?= 1 | |||
LUAJIT ?= 1 | |||
PYTHON ?= 0 | |||
SUPERCOLLIDER ?= 0 | |||
VULT ?= 1 | |||
# Vult depends on both LuaJIT and QuickJS | |||
ifeq ($(VULT), 1) | |||
QUICKJS = 1 | |||
LUAJIT = 1 | |||
endif | |||
# Entropia File System Watcher | |||
efsw := dep/lib/libefsw-static-release.a | |||
@@ -186,5 +193,16 @@ endif | |||
# cd dep/llvm-8.0.1.src/build && $(MAKE) | |||
# cd dep/llvm-8.0.1.src/build && $(MAKE) install | |||
# Vult | |||
ifeq ($(VULT), 1) | |||
SOURCES += src/VultEngine.cpp | |||
vult := dep/vult/vultc.h | |||
$(vult): | |||
cd dep && mkdir -p vult | |||
cd dep/vult && $(WGET) "https://github.com/modlfo/vult/releases/download/v0.4.9/vultc.h" | |||
$(SHA256) $(vult) 73f53e7595d515ae87fe1c89925e17cd86c2ac75b73b48fa502a2fb0fd1d4847 | |||
FLAGS += -Idep/vult | |||
DEPS += $(vult) | |||
endif | |||
include $(RACK_DIR)/plugin.mk |
@@ -9,6 +9,8 @@ Scripting language host for [VCV Rack](https://vcvrack.com/) containing: | |||
Supported scripting languages: | |||
- JavaScript (.js) | |||
- Lua (.lua) | |||
- [Vult](https://github.com/modlfo/vult) (.vult) | |||
- [Add your own below](#adding-a-script-engine) | |||
[Discussion thread](https://community.vcvrack.com/t/vcv-prototype/3271) | |||
@@ -89,6 +91,8 @@ function process(block) { | |||
} | |||
``` | |||
The Vult API is slightly different from the JavaScript version. Check the Vult examples included with the plugin to learn how to use the language with the VCV-Prototype plugin. | |||
## Build dependencies | |||
### Windows | |||
@@ -125,4 +129,5 @@ sudo pacman -S premake | |||
- [Wes Milholen](https://grayscale.info/): panel design | |||
- [Andrew Belt](https://github.com/AndrewBelt): host code, Duktape (JavaScript, disabled), LuaJIT (Lua), Python (in development) | |||
- [Jerry Sievert](https://github.com/JerrySievert): QuickJS (JavaScript) | |||
- [Leonardo Laguna Ruiz](https://github.com/modlfo): Vult | |||
- add your name here |
@@ -0,0 +1,83 @@ | |||
/* | |||
Vult API documentation. | |||
Author: Leonardo Laguna Ruiz - leonardo@vult-dsp.com | |||
The main difference of the Vult API compared to the JavaScript and Lua is that all interactions | |||
happen through functions rather than accessing to the block arrays. | |||
A Vult script requires the following two functions: | |||
fun process() { } | |||
and update() { } | |||
The 'process' function is called every audio sample. As inputs, it will receive the values from | |||
the input jacks but normalized to 1.0. This means that a value of 10.0 V in VCV Rack is received | |||
as 1.0. Similarly, when you return a value of 1.0 it will be output by the prototype as 10.0V. | |||
You can use the input and output jacks by adding or removing arguments to the function. For example, | |||
to pass all the inputs to the outputs you can declare the function as follows: | |||
fun process(i1, i2, i3, i4, i5, i6) { | |||
return i1, i2, i3, i4, i5, i6; | |||
} | |||
The 'update' function is called once every 32 samples. You can use this function to perform actions | |||
that do not require audio rate speed e.g. setting light colors or displying characters in the screen. | |||
The function 'update' do not takes or returns any value. | |||
Important: Notice that the 'update' function is declared with the keyword 'and'. In Vult language, | |||
this means that they share context. At the moment, declaring them differently could have an undefined | |||
behavior. | |||
To interact with knobs, switches, lights the following builtin functions are provided. | |||
NOTE: the knobs, switches and lights are numbered from 1 to 6 | |||
getKnob(n:int) : real // value of the nth knob range: 0.0-1.0 | |||
getSwitch(n:int) : bool // value of the nth switch: true/false | |||
setLight(n:int, r:real, g:real, b:real) // r, g, b range: 0.0-1.0 | |||
setSwitchLight(n:int, r:real, g:real, b:real) // r, g, b range: 0.0-1.0 | |||
samplerate() : real // current sample rate | |||
sampletime() : real // current time step (1.0 / samplerate()) | |||
display(text:string) // display text in the screen | |||
*/ | |||
// Returns the r,g,b values for a given voltage | |||
fun getRGB(v) { | |||
if (v > 0.0) | |||
return v, 0.0, 0.0; | |||
else | |||
return 0.0, -v, 0.0; | |||
} | |||
// Takes two inputs and returns the result of different operations on them | |||
fun process(in1, in2) { | |||
// theses are declared as 'mem' so we can remember them and use them in 'update' | |||
mem sum = clip(in1 + in2, -1.0, 1.0); // use 'clip' to keep the signals in the specified range | |||
mem sub = clip(in1 - in2, -1.0, 1.0); | |||
mem mul = clip(in1 * in2, -1.0, 1.0); | |||
return sum, sub, mul; | |||
} | |||
and update() { | |||
_ = display("Add two LFO to IN1 and IN2"); | |||
val r, g, b; | |||
// Set the light no 1 with the 'sum' value | |||
r, g, b = getRGB(sum); | |||
_ = setLight(1, r, g, b); | |||
_ = setSwitchLight(1, r, g, b); | |||
// Set the light no 2 with the 'sub' value | |||
r, g, b = getRGB(sub); | |||
_ = setLight(2, r, g, b); | |||
_ = setSwitchLight(2, r, g, b); | |||
// Set the light no 2 with the 'mul' value | |||
r, g, b = getRGB(mul); | |||
_ = setLight(3, r, g, b); | |||
_ = setSwitchLight(3, r, g, b); | |||
} |
@@ -0,0 +1,80 @@ | |||
/* | |||
Simple synthesizer with one oscillator, LFO and envelope. | |||
Author: Leonardo Laguna Ruiz - leonardo@vult-dsp.com | |||
Check the API documentation in the basic.vult example | |||
*/ | |||
fun env(gate) { | |||
mem x; | |||
val k = if gate > x then 0.05 else 0.0002; | |||
x = x + (gate - x) * k; | |||
return x; | |||
} | |||
fun edge(x):bool { | |||
mem pre_x; | |||
val v:bool = (pre_x <> x) && (pre_x == false); | |||
pre_x = x; | |||
return v; | |||
} | |||
fun pitchToFreq(cv) { | |||
return 261.6256 * exp(cv * 0.69314718056); | |||
} | |||
fun phasor(pitch, reset){ | |||
mem phase; | |||
val rate = pitchToFreq(pitch) * sampletime(); | |||
phase = phase + rate; | |||
if(phase > 1.0) | |||
phase = phase - 1.0; | |||
if(reset) | |||
phase = 0.0; | |||
return phase; | |||
} | |||
fun oscillator(pitch, mod) { | |||
mem pre_phase1; | |||
// Implements the resonant filter simulation as shown in | |||
// http://en.wikipedia.org/wiki/Phase_distortion_synthesis | |||
val phase1 = phasor(pitch, false); | |||
val comp = 1.0 - phase1; | |||
val reset = edge((pre_phase1 - phase1) > 0.5); | |||
pre_phase1 = phase1; | |||
val phase2 = phasor(pitch + mod, reset); | |||
val sine = sin(2.0 * pi() * phase2); | |||
return sine * comp; | |||
} | |||
fun lfo(f, gate){ | |||
mem phase; | |||
val rate = f * 10.0 * sampletime(); | |||
if(edge(gate > 0.0)) | |||
phase = 0.0; | |||
phase = phase + rate; | |||
if(phase > 1.0) | |||
phase = phase - 1.0; | |||
return sin(phase * 2.0 * pi()) - 0.5; | |||
} | |||
// Main processing function | |||
fun process(cv, gate){ | |||
// LFO | |||
val lfo_rate = getKnob(3); | |||
val lfo_amt = getKnob(4); | |||
val lfo_val = lfo(lfo_rate, gate) * lfo_amt; | |||
// Oscillator | |||
val pitch = getKnob(1) + 10.0 * cv - 2.0; | |||
val mod = getKnob(2) * 2.0 + lfo_val; | |||
val o = oscillator(pitch, mod); | |||
// Envelope | |||
val e = env(gate); | |||
return o * e; | |||
} | |||
and update() { | |||
_ = display("IN1: CV, IN2: GATE"); | |||
} |
@@ -0,0 +1,45 @@ | |||
/* | |||
Simple wave table VCO. | |||
Author: Leonardo Laguna Ruiz - leonardo@vult-dsp.com | |||
Check the API documentation in the basic.vult example | |||
*/ | |||
fun pitchToFreq(cv) @[table(size=128, min=-10.0, max=10.0)] { | |||
return 261.6256 * exp(cv * 0.69314718056); | |||
} | |||
// Generates (at compile time) a wave table based on the provided harmonics | |||
// To change the wave table, change the 'harmonics' array | |||
fun wavetable(phase) @[table(size=128, min=0.0, max=1.0)] { | |||
val harmonics = [0.0, 1.0, 0.7, 0.5, 0.3]; | |||
val n = size(harmonics); | |||
val acc = 0.0; | |||
val i = 0; | |||
while(i < n) { | |||
acc = acc + harmonics[i] * sin(2.0 * pi() * real(i) * phase); | |||
i = i + 1; | |||
} | |||
return acc / sqrt(real(n)); | |||
} | |||
fun process(input_cv:real) { | |||
mem phase; | |||
val knob_cv = getKnob(1) - 0.5; | |||
val cv = 10.0 * (input_cv + knob_cv); | |||
val freq = pitchToFreq(cv); | |||
val delta = sampletime() * freq; | |||
phase = phase + delta; | |||
if(phase > 1.0) { | |||
phase = phase - 1.0; | |||
} | |||
return wavetable(phase); | |||
} | |||
and update() { | |||
} |
@@ -0,0 +1,153 @@ | |||
#include "ScriptEngine.hpp" | |||
#include "vultc.h" | |||
#include <quickjs/quickjs.h> | |||
/* The Vult engine relies on both QuickJS and LuaJIT. | |||
* | |||
* The compiler is written in OCaml but converted to JavaScript. The JavaScript | |||
* code is embedded as a string and executed by the QuickJs engine. The Vult | |||
* compiler generates Lua code that is executed by the LuaJIT engine. | |||
*/ | |||
extern std::map<std::string, ScriptEngineFactory *> scriptEngineFactories; | |||
// Special version of createScriptEngine that only creates Lua engines | |||
ScriptEngine *createLuaEngine() { | |||
auto it = scriptEngineFactories.find("lua"); | |||
if (it == scriptEngineFactories.end()) | |||
return NULL; | |||
return it->second->createScriptEngine(); | |||
} | |||
struct VultEngine : ScriptEngine { | |||
// used to run the lua generated code | |||
ScriptEngine *luaEngine; | |||
// used to run the Vult compiler | |||
JSRuntime *rt = NULL; | |||
JSContext *ctx = NULL; | |||
VultEngine() { | |||
rt = JS_NewRuntime(); | |||
// Create QuickJS context | |||
ctx = JS_NewContext(rt); | |||
if (!ctx) { | |||
display("Could not create QuickJS context"); | |||
return; | |||
} | |||
JSValue global_obj = JS_GetGlobalObject(ctx); | |||
// Load the Vult compiler code | |||
JSValue val = | |||
JS_Eval(ctx, (const char *)vultc_h, vultc_h_size, "vultc.js", 0); | |||
if (JS_IsException(val)) { | |||
display("Error loading the Vult compiler"); | |||
JS_FreeValue(ctx, val); | |||
JS_FreeValue(ctx, global_obj); | |||
return; | |||
} | |||
} | |||
~VultEngine() { | |||
if (ctx) { | |||
JS_FreeContext(ctx); | |||
} | |||
if (rt) { | |||
JS_FreeRuntime(rt); | |||
} | |||
} | |||
std::string getEngineName() override { return "Vult"; } | |||
int run(const std::string &path, const std::string &script) override { | |||
display("Loading..."); | |||
JSValue global_obj = JS_GetGlobalObject(ctx); | |||
// Put the script text in the 'code' variable | |||
JSValue code = JS_NewString(ctx, script.c_str()); | |||
JS_SetPropertyStr(ctx, global_obj, "code", code); | |||
// Put the script path in 'file' variable | |||
JSValue file = JS_NewString(ctx, path.c_str()); | |||
JS_SetPropertyStr(ctx, global_obj, "file", file); | |||
display("Compiling..."); | |||
// Call the Vult compiler to generate Lua code | |||
static const std::string testVult = R"( | |||
var result = vult.generateLua([{ file:file, code:code}],{ output:'Engine', template:'vcv-prototype'});)"; | |||
JSValue compile = | |||
JS_Eval(ctx, testVult.c_str(), testVult.size(), "Compile", 0); | |||
JS_FreeValue(ctx, code); | |||
JS_FreeValue(ctx, file); | |||
// If there are any internal errors, the execution could fail | |||
if (JS_IsException(compile)) { | |||
display("Fatal error in the Vult compiler"); | |||
JS_FreeValue(ctx, global_obj); | |||
return -1; | |||
} | |||
// Retrive the variable 'result' | |||
JSValue result = JS_GetPropertyStr(ctx, global_obj, "result"); | |||
// Get the first element of the 'result' array | |||
JSValue first = JS_GetPropertyUint32(ctx, result, 0); | |||
// Try to get the 'msg' field which is only present in error messages | |||
JSValue msg = JS_GetPropertyStr(ctx, first, "msg"); | |||
// Display the error if any | |||
if (!JS_IsUndefined(msg)) { | |||
const char *text = JS_ToCString(ctx, msg); | |||
const char *row = | |||
JS_ToCString(ctx, JS_GetPropertyStr(ctx, first, "line")); | |||
const char *col = JS_ToCString(ctx, JS_GetPropertyStr(ctx, first, "col")); | |||
// Compose the error message | |||
std::stringstream error; | |||
error << "line:" << row << ":" << col << ": " << text; | |||
WARN("Vult Error: %s", error.str().c_str()); | |||
display(error.str().c_str()); | |||
JS_FreeValue(ctx, result); | |||
JS_FreeValue(ctx, first); | |||
JS_FreeValue(ctx, msg); | |||
return -1; | |||
} | |||
// In case of no error, retrieve the generated code | |||
JSValue luacode = JS_GetPropertyStr(ctx, first, "code"); | |||
std::string luacode_str(JS_ToCString(ctx, luacode)); | |||
//WARN("Generated Code: %s", luacode_str.c_str()); | |||
luaEngine = createLuaEngine(); | |||
if (!luaEngine) { | |||
WARN("Could not create a Lua script engine"); | |||
return -1; | |||
} | |||
luaEngine->module = this->module; | |||
display("Running..."); | |||
JS_FreeValue(ctx, luacode); | |||
JS_FreeValue(ctx, first); | |||
JS_FreeValue(ctx, msg); | |||
JS_FreeValue(ctx, msg); | |||
JS_FreeValue(ctx, global_obj); | |||
return luaEngine->run(path, luacode_str); | |||
} | |||
int process() override { | |||
if (luaEngine) | |||
return luaEngine->process(); | |||
else | |||
return 0; | |||
} | |||
}; | |||
__attribute__((constructor(1000))) static void constructor() { | |||
addScriptEngine<VultEngine>("vult"); | |||
} |