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