/dbRackFormulaOne

the excellent live coding/prototyping VCV Rack module (forked from docB)

Primary LanguageC++GNU General Public License v3.0GPL-3.0

Formula One

A script/formula evaluation module for VCVRack. This module can do almost anything, and it is quite fast.

It is based on the exprtk expression library which has very good benchmarks.

It is not intended for replacing native modules because they still will be faster i.e. consume less CPU and provide more usability. It is for experimenting,learning and making special things.

Examples and Usecases

The examples are available as factory presets.

Simple polyphonic oscillator

var freq := 261.626 * pow(2,w);
var p:= bufr(chn);
var o:=a*4*(
   sin(2*pi*p)+
   sin(6*pi*p)/3+
   sin(10*pi*p)/5+
   sin(14*pi*p)/7
);
var phs:= p+stim*freq;
phs-=trunc(phs);
bufw(chn,phs);
var out:=o; 

Step by Step

var freq := 261.626 * pow(2,w); Declare a variable freq and assign the computed frequency from the input w (V/Oct).

var p:= bufr(chn); Read the current phase for chn from the buffer into the variable p. The script is evaluated for every channel which is detected in the w input and provides the value of the input w for the current channel via the global variable w. The current channel number is set in the global variable chn.

var o:=a*4*(sin(2*pi*p)+sin(6*pi*p)/3+sin(10*pi*p)/5+sin(14*pi*p)/7); compute the output sample from the current phase. the global variable a holds the current value of the knob a;

var phs:= p+stim*freq; phs-=trunc(phs); compute the next phase using the global variable stim which holds the current sample time (1/sampleRate).

bufw(chn,phs); write the new phase into the buffer.

var out:=o; the last expression value in the script is returned to the module and written into the output out.

External Phase Controlled Oscillator

a*5*(
   sin(2*pi*t)+
   sin(6*pi*t)/3+
   sin(10*pi*t)/5+
   sin(14*pi*t)/7
)

This example does the same as the one above but uses the special input t as phase. The values of t are normalized from -5V/5V to 0V/1V.

Wave Folder

sin((c*5)*t*2*pi+b*5)*a*5

The same principle can be used to make a wave folder. The knob c controls the depth and the knob b controls the offset.

Simple Polyphonic Filter

This example shows how to implement a simple LP filter. The algorithm is directly taken from the VCV Rack source

var f := clamp(2^(c*8),0.001,0.5);
var fc := 2/f;
var out:= (x + bufr(chn) - buf1r(chn) * (1 - fc)) /(1 + fc);
bufw(chn,x); 
buf1w(chn,out);
dcb(chn,out);

Step by Step

var f := clamp(2^(c*8),0.001,0.5); var fc := 2/f; Compute the cutoff frequency from the knob c.

var out:= (x + bufr(chn) - buf1r(chn) * (1 - fc)) /(1 + fc);
bufw(chn,x); 
buf1w(chn,out);

Compute the next filter sample. The two needed state variables are stored per channel in the buffer (0) and buffer 1. There are 4 buffers of size 4096 available which can be read via bufr,buf1r,buf2r,buf3r and written via bufw,buf1w,buf2w,buf3w

dcb(chn,out); output the dc blocked sample.

Polyphonic Comb Filter (Chorus,Flanger or whatever)

var delay_ms:= 2.1;
var len:= delay_ms/1000*sr;
rblen(chn,len);
var c0 := clamp((c+1)/2+y/10*a,0,0.99);
var x0 := rbget(chn,c0*len); 
var nx := x/2 + x0 * d;
rbpush(chn,nx);
var out:=x0+nx;
var x1 := rbget(chn,c0*len/2);
out1:=dcb2(chn,x1+nx);
dcb(chn,out);

This example shows the use of the provided ring buffers.

Step by Step

var delay_ms:= 2.1; var len:= delay_ms/1000*sr; Define the delay length and compute the buffer size in samples using the global variabel sr (sample rate).

rblen(chn,len); set the ring buffer length for each channel. There are 16 ring buffers available with maximum length of 48000.

var c0 := clamp((c+1)/2+y/10*a,0,0.99);. Compute the relative read position of the buffer using the knob value c and the input y attenuated with the knob value a.

var x0 := rbget(chn,c0*len); read the buffer value at the computed index.

var nx := x/2 + x0 * d; rbpush(chn,nx); mix with the input, add feedback attenuated with knob dand write it into the ring buffer.

var out:=x0+nx; store the output in the variable out.

var x1 := rbget(chn,c0*len/2); read a second value from the buffer for a stereo effect.

out1:=dcb2(chn,x1+nx); output the 'right' stereo channel in output out1 via setting the global variable out1. There are two dc blockers available per channel.

dcb(chn,out); output the left channel in out. (The value of the last expression is always written into the output out).

Stereo Delay

var delayR:= 0.375;
var delayL:= 0.750;
var lenR:= delayR*sr;
var lenL:= delayL*sr;
rblen(0,lenL);
rblen(1,lenR);

var xL := rbget(0,0);
var xR := rbget(1,0);
rbpush(0,x+a*xL);
rbpush(1,x+a*xR);
out1:=dcb(0,(xR*b)+(x*(1-b)));
dcb(1,xL*b+x*(1-b));

Similar to the last example. The knob a controls the feedback and the knob b dry/wet. The position in the call rbget is relative to the last write position + 1. With position 0 it returns the sample which was written length samples before. rbget(0,-1) would return the last written sample.

Polyphonic Random and Hold

if(st(chn,z)>0) {
  bufw(chn,rnd());
}
var out := bufr(chn)*10*a;

There are 4 Schmitt Triggers available per channel (st,st1,st2,st3). The buffer is used to hold the value until the next trigger arrives. NB the polyphony is determined by the input channels in the following way: If the input t is connected the channels of t is used, otherwise the maximum of the channels of the inputs w,x,y,z. The image above shows the case with one channel.

Simple Sequencer

var seq[8] := { 0,3/12,5/12,
             7/12,10/12,7/12,
             5/12,3/12};

if(st(0,z)>0) { 
 v1:=v1+1;
 if(v1>=8) v1:=0;
} 
if(st1(0,y)>0) v1:=0;
var out := seq[v1];

This example shows the use of one of the 8 global variables v1-v8. v1 is used as the current position of the sequence which is advanced on arrival on a trigger on input z. The input y is used for resetting the sequence.

Chord Sequencer

if(v3==0) {
  var n := -1;
  for(var i:=0;i<36;i+=1) {
    bufw(i,n);
    n+=1/12;
  };
  v3:=1;
};
var chord[16]:={9,12,16,19, 9,12,14,18,
                7,11,14,18, 7,11,12,16};
if(st(0,z)>0) {v1+=1; if(v1>=4) v1:=0;}
var out: = bufr(chord[chn+v1*4]);

All variables and buffers are cleared if the script is compiled due to a change. Here the variable v3 is used to fill a buffer with note values (V/Oct) only once. (otherwise it costs much CPU).

Similar to the RndH example, a trigger on the input z advances the sequence. The notes of a single chord are distributed in 4 channels.

Gate Sequencer

var data[16] := { 
1,0,0,0,1,0,0,0,1,0,0,0,1,0,0,1};
var dur := 10; var gate:=0;
if(st(chn,z)>0) {
    if(data[v1]>0) {
      bufw(chn,dur);
    } else {
      bufw(chn,0);
    } 
};
if(bufr(chn)>0) { 
  gate:=10; bufw(chn,bufr(chn)-1);
};
if(st1(0,z)>0) {
  v1+=1;
  if(v1>=16) v1:=0;
};
var out:= gate;

This example shows how to make pulses. The duration is defined in samples. When a trigger arrives and the current position (v1) has a one, a state variable is set to the duration and decremented on each run until it is zero. While greater than zero the output is set to 10V.

A Line Segment Envelope Generator

var vals[6]:={0,5,3,2,2,0};
var durs[5]:={0.3,0.2,0.5,0.3,0.2};
if(st(0,w)>0) { v1:=0; v2:=0; v3:=0; }
var end:=0;
if(v1>=v2+durs[v3]) { // advance
  if(v3>=5) end:=1;
  else { v2+=durs[v3]; v3+=1; }
};
var o;var pct;
if(end==1) {
  o:=vals[5]; 
} else {
  pct:= (v1-v2)/durs[v3];
  o:=vals[v3]+(vals[v3+1]-vals[v3])*pct;  
};
v1+=stim;
o;

Outputs line segments according to the values and duration arrays. It is triggered via the w input. As this is a common usecase the example can be simplified as so:

if(st(0,w)>0) { v1:=0; }
var o:=lseg(v1,0,0.3,5,0.2,3,0.5,2,0.3,
        2,0.2,0);
v1+=stim;
o;

The function lseg takes as shown the phase as first parameter then alternating value and duration. If the last value is missing the first value is used instead.

Additionally there is a function eseg providing exponential segments

var o:=eseg(v1, 0,0.3,-5, 5,0.5,-4,
               2,0.3,0, 2,0.5,-3, 0);
if(st(0,w)>0) v1:=0;

v1+=stim;
o;

The eseg function takes three alternating parameters value, duration, bending. The sign of the bending parameter determines if the curve is fast decaying/raising (<0) or slow decaying/raising (>0).

Waveshaping

This example shows the definition and use of a user defined function.

function sign(f) {
   f<0?-1:(f>0?1:0);
}
var p:= scl1(-a,0.01,10);
// waveshaping function
sign(x)*(1-p/(abs(x)+p))*5;

Functions must be defined always on top, before the normal code starts. The return value of a function is always the value of the last expression.

There are some convenient functions provided for scaling:

scl(input,minInput,maxInput,minOutput,maxOutput)

this function scales the input expected in the range minInput,maxInput to the target range minOutput,maxOutput.

scl1 as shown in the example is a shortcut for

scl(input,-1,1,minOutput,maxOutput)

-- suited for the knob inputs.

Wavshaping with exp segemnts

eseg(t,-1,0.5,a*10,1,0.5,b*10,-1)*5

This example shows the use of the eseg function (see above) for waveshaping. Play around with knob a and b.

CZ-Series PD Filter sweep

var freq := 261.626 * pow(2,w);
var p:= bufr(chn);
var phs:= p+stim*freq;
if(phs>=1) phs:=0;
bufw(chn,phs);
var inv:=1-p;
out1:=p;
var freq2 := 261.626 * pow(2,a*5);
var p2:= buf2r(chn);
var o:= sin(2*pi*p2)*inv;
var phs2:= p2+stim*freq2;
phs2-=trunc(phs2);
if(phs==0) phs2:=0;
buf2w(chn,phs2);

var out:=o*5;

This script implements the Resonant Filter Simulation from the Casio CZ Series. The knob a controls the resonant frequency.