Compile-time creation of tables

This is one of the features that has been in the TODO list since the creation of Vult. When generating code, now is possible to annotate a function and Vult will automatically create a table-based implementation which will be faster. This is specially useful when generating code for microcontrollers.

As an example, take the function used to convert from pitch (MIDI note number) to frequency.

Using that formula, we can calculate the rate of increment for a saw wave at a certain sampling frequency. For example, to produce a saw wave with pitch d at a sampling rate of 44100 Hz, you need to increment a counter at a rate to reach a value of 1.0. Here’s the formula:

This formula can be simplified (using Mathematica) to:

We can see that the formula uses an exponential. Calculating an exponential every sample can be quite heavy in a microntroller (for example when making modulations). To alleviate that problem, we can annotate the function in Vult as follows:

The tag @[table(size=128,min=0.0,max=127.0)] specifies that the function needs to be converted to a table of size 128, with a minimum value of 0.0 and a maximum of 127.0. In this case Vult will split the range of the function into 128 segments and for each segment a second order polynomial that fits the values is calculated. All the coefficients are precalculated. The above function will be replaced by the following code (for a size 8 table):

This new function is much faster that the one using the exponential. Thanks to this optimization I can run more complex code in the Teensy.

One cool thing is about this feature is that it can convert any function of one input and one output to a table. For example, in the following program the function wave_table is replaced by a table avoiding calculating three sine functions.

fun phasor(pitch){ mem phase; val rate = pitchToRate(pitch); phase = (phase + rate) % 1.0; return phase; }

fun wave_table(x) @[table(size=127,min=0.0,max=4.0)] { val pi = 3.14159265359; return (sin(2.0pix) + sin(2.0pix2.0) + sin(2.0pix4.0))/3.0; }

This feature works as well with fixed-point code. In one of the benchmarks that I ran I got the following results:

We can see that for fixed-point it really had a impact in the performance. The benchmark now runs 3 times faster.