@@ -18,6 +18,13 @@ QUICKJS ?= 1 | |||||
LUAJIT ?= 1 | LUAJIT ?= 1 | ||||
PYTHON ?= 0 | PYTHON ?= 0 | ||||
SUPERCOLLIDER ?= 0 | SUPERCOLLIDER ?= 0 | ||||
VULT ?= 1 | |||||
# Vult depends on both LuaJIT and QuickJS | |||||
ifeq ($(VULT), 1) | |||||
QUICKJS = 1 | |||||
LUAJIT = 1 | |||||
endif | |||||
# Entropia File System Watcher | # Entropia File System Watcher | ||||
efsw := dep/lib/libefsw-static-release.a | 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) | ||||
# cd dep/llvm-8.0.1.src/build && $(MAKE) install | # 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 | include $(RACK_DIR)/plugin.mk |
@@ -9,6 +9,8 @@ Scripting language host for [VCV Rack](https://vcvrack.com/) containing: | |||||
Supported scripting languages: | Supported scripting languages: | ||||
- JavaScript (.js) | - JavaScript (.js) | ||||
- Lua (.lua) | |||||
- [Vult](https://github.com/modlfo/vult) (.vult) | |||||
- [Add your own below](#adding-a-script-engine) | - [Add your own below](#adding-a-script-engine) | ||||
[Discussion thread](https://community.vcvrack.com/t/vcv-prototype/3271) | [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 | ## Build dependencies | ||||
### Windows | ### Windows | ||||
@@ -125,4 +129,5 @@ sudo pacman -S premake | |||||
- [Wes Milholen](https://grayscale.info/): panel design | - [Wes Milholen](https://grayscale.info/): panel design | ||||
- [Andrew Belt](https://github.com/AndrewBelt): host code, Duktape (JavaScript, disabled), LuaJIT (Lua), Python (in development) | - [Andrew Belt](https://github.com/AndrewBelt): host code, Duktape (JavaScript, disabled), LuaJIT (Lua), Python (in development) | ||||
- [Jerry Sievert](https://github.com/JerrySievert): QuickJS (JavaScript) | - [Jerry Sievert](https://github.com/JerrySievert): QuickJS (JavaScript) | ||||
- [Leonardo Laguna Ruiz](https://github.com/modlfo): Vult | |||||
- add your name here | - 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"); | |||||
} |