Browse Source

Add support for Vult scripts

tags/v1.3.0
Leonardo Laguna Ruiz Andrew Belt 5 years ago
parent
commit
165693b0de
6 changed files with 384 additions and 0 deletions
  1. +18
    -0
      Makefile
  2. +5
    -0
      README.md
  3. +83
    -0
      examples/basic.vult
  4. +80
    -0
      examples/synth.vult
  5. +45
    -0
      examples/vco.vult
  6. +153
    -0
      src/VultEngine.cpp

+ 18
- 0
Makefile View File

@@ -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

+ 5
- 0
README.md View File

@@ -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

+ 83
- 0
examples/basic.vult View File

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

}

+ 80
- 0
examples/synth.vult View File

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

+ 45
- 0
examples/vco.vult View File

@@ -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() {

}

+ 153
- 0
src/VultEngine.cpp View File

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

Loading…
Cancel
Save