diff --git a/Makefile b/Makefile index 8cd8e9f..d8646a5 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index d206ef3..625daaf 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/basic.vult b/examples/basic.vult new file mode 100644 index 0000000..f77c865 --- /dev/null +++ b/examples/basic.vult @@ -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); + +} \ No newline at end of file diff --git a/examples/synth.vult b/examples/synth.vult new file mode 100644 index 0000000..0042632 --- /dev/null +++ b/examples/synth.vult @@ -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"); +} \ No newline at end of file diff --git a/examples/vco.vult b/examples/vco.vult new file mode 100644 index 0000000..9b8155e --- /dev/null +++ b/examples/vco.vult @@ -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() { + +} diff --git a/src/VultEngine.cpp b/src/VultEngine.cpp new file mode 100644 index 0000000..c454b0d --- /dev/null +++ b/src/VultEngine.cpp @@ -0,0 +1,153 @@ +#include "ScriptEngine.hpp" +#include "vultc.h" +#include + +/* 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 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("vult"); +}