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