Skip to content

About

Custom scripting language for alterants in the VST synth Spektral.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Latest commit

 

History

12 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 

Repository files navigation

Alterant Script

This is a scripting language specifically designed to be used in the VST synth Spektral. More specifically, this script can be used to create custom 'Alterants'. To read more about what alterants are, check Spektral's user manual.

Examples

Check the examples folder in this repository for some example scripts. This folder contains all of the built-in alterants in Spektral, recreated in alterant script.

How Does It Work

Alterants let you transform spectral data in some way. This scripting language allows you to do that efficiently. You can apply transformation on entire buffers in a single instruction. Here is a short example program that shows off some of the functionality:

@name Utility
@inplace true
@graph eq base = 0.5
@param 0 name="Gain"  default=1.0 transform=range[0,100]    format=percent
@param 1 name="Width" default=1.0 transform=range[0,100]    format=percent
@param 2 name="Pan"   default=0.5 transform=range[-100,100] format=pan 

gain  = param[0] * graph
width = param[1]
pan   = 2 * stereo(cos(param[2] * 0.5 * pi),
                   sin(param[2] * 0.5 * pi)) / sqrt2

mid = input.left + input.right
side = input.left - input.right

output = stereo(mid + side * width,
                mid - side * width) * pan * gain

Let's start at the top, here we define some configuration: we set our script name, tell it that the algorithm we are writing can be performed in-place, tell it what the default graph should be on initialization, and we give it information how many parameters we want, and how to display them in the UI.

After configuration we read and transform our parameters. Let's first look at the line gain = param[0] * graph. Here we set the gain variable to the first parameter multiplied by graph. graph is a global that's always available, and it represents the graph data. This global is a stereo buffer, and multiplying it with the scalar param[0] multiplies every value in the stereo buffer with param[0], resulting in a new stereo buffer. When assigning pan we use the function stereo(), this function takes 1 or 2 mono parameters, and turns it into a stereo value. This works for both mono scalar values, and mono buffers. After reading the parameters, we access input to get the mid and side channels. input is another global that is always available, and contains the spectral data that is fed into this alterant. Just like graph, this is a stereo buffer, which allows us to read .left and .right buffers. Doing input.left + input.right performs addition on all elements of the buffer at once, in a single instruction. Finally, we combine mid and side again using stereo() and our width parameter, and then we multiply the resulting stereo buffer with pan and gain.

Operations

As seen in the example, it is possible to do operations between different types. That behaviour is defined like this:

Type A Type B Result Type Behaviour
scalar scalar scalar a + b
scalar stereo stereo { a + b.left, a + b.right }
scalar buffer buffer [a + b[0], ...]
scalar stereo buffer stereo buffer [{ a + b[0].left, a + b[0].right }, ...]
stereo stereo stereo { a.left + b.left, a.right + b.right }
stereo buffer stereo buffer [{ a.left + b[0], a.right + b[0] }, ...]
stereo stereo buffer stereo buffer [{ a.left + b[0].left, a.right + b[0].right }, ...]
buffer buffer buffer [a[0] + b[0], a[1] + b[1], ...]
buffer stereo buffer stereo buffer [{ a[0] + b[0].left, a[0] + b[0].right }, ...]
stereo buffer stereo buffer stereo buffer [{ a[0].left + b[0].left, a[0].right + b[0].right }, ...]

Operators

This is a list of all supported operators. All of these support all types as arguments, unless otherwise specified. Mixing different types behaves the same as defined above.

All bit manipulation operators only work with integers. If the variables contain floating point numbers, they will be truncated to an integer before the bit operation.

Note how operators like += or ++ are not supported!

Operator Behaviour Operator Behaviour
a + b Addition. a > b Greater than.
a - b Subtraction. a >= b Greater than or equals.
a * b Multiplication. a == b Equals.
a / b Division. a != b Not equals.
a % b Modulo, this works with floating point numbers as well. +a Plus. This has no effect.
a << b Bitwise shift left. -a Minus.
a >> b Bitwise shift right. !a Logic not.
a && b Logic and. ~a Bitwise not.
a || b Logic or. a ? b : c Ternary conditional.
a & b Bitwise and. a = b Assignment.
a | b Bitwise or. a() Function call.
a ^ b Bitwise xor. a[] Indexing.
a < b Less than. a.left, a.right Accessing left and right channels of a stereo value.
a <= b Less than or equals. a, b Comma operator. Returns the last expression in the list.

Globals

Each script has access to some globals. Here is that list of globals:

Variable What does it mean
i Mono buffer containing indices: [0, 1, 2, 3, 4, ..., 511].
size The size of a buffer. This is always equal to 512.
sample_rate The rate at which this script is evaluated. Note that this is not equal to the actual sample rate of the audio, as alterants are not evaluated every sample on lower quality settings.
frequency The frequency in Hz of the voice that this alterant is influencing.
param The list of up to 5 parameter values.
trigger Boolean that is only true once when the voice that this alterant influences has been triggered. Can be used to initialize or reset values.
deterministic Boolean, corresponds with the 'fixed' toggle in the UI.
wrap Boolean, corresponds with the 'wrap' toggle in the UI.
cepstral Boolean, corresponds with the 'cepstral' toggle in the UI.
logarithmic Boolean, corresponds with the 'log' toggle in the UI.
synced Boolean, corresponds with the 'sync' toggle in the UI.
stereo Boolean, true when the left/right or mid/side graphs are not linked together.
input Stereo buffer containing the input spectral data.
inputs When multi-layer mode is active, this contains the 3 layers of data. The index must be a constant, i.e. inputs[0].
graph Stereo buffer containing the graph.
graphs Access any of the individual graphs in the graph list, index must be a constant, i.e. graphs[2].
graph_mode Integer corresponding with the currently selected graph mode, see @graph_modes
output Stereo buffer to write the result to.
bipolar Boolean that is true if the data coming into the alterant is bipolar. If any bipolar table feeds data into this alterant, it is set to true.
key_tracked Boolean that is true if the data coming into the alterant is key tracked. If all tables that feed into this alterant are key tracked, it is set to true.
phase_data Boolean that is true if the data coming into the alterant is phase data. If all tables that feed into this alterant are phase data, it is set to true.
interpolate Boolean that is true if the data coming into the alterant is interpolated. If any interpolated table feeds data into this alterant, it is set to true.
mid_side Boolean that is true if the data coming into the alterant is mid/side data. If any table that feeds into this alterant is mid/side, it is set to true.
nyquist The highest frequency represented in the spectral data. This is half the sample-rate of the audio that was analyzed to create this spectral data.
hop_size Distance in samples between consecutive frames from the incoming spectral data. If multiple tables feed into this alterant, the highest hop size is taken.
overlap The amount of overlap between consecutive frames from the incoming spectral data. Either 1, 2, 4, or 8. If multiple tables feed into this alterant, the highest overlap is taken.
current_layer When multi-layer mode is active, this is the current layer that is being processed. Meaning output corresponds to the output of this layer.
multi_layer True when multi-layered processing is enabled.
modulator Scalar value that can be used as a modulation source. Using this variable in your script enables the modulator. Right-click your custom alterant to link it to 1 of the 5 spectral modulation sources.

Note that these are treated as keywords, you cannot define any variable with these names.

Constants

Besides globals, there are also some constants: e, log2e, log10e, pi, two_pi, inv_pi, inv_two_pi, inv_sqrtpi, ln2, ln10, sqrt2, sqrt3, inv_sqrt3, egamma, phi.

Note that these are treated as keywords, you cannot define any variable with these names.

Functions

There are also some built-in functions. Custom functions are not supported. All of these functions support all types as arguments, unless otherwise specified. Mixing different types behaves the same as defined above.

Function What does it do
noise() Generate a buffer of random values between 0 and 1.
noise(a) Generate a buffer of random values between 0 and a. a must be a constant.
noise(a, b) Generate a buffer of random values between a and b. a and b must be a constant.
random() Generate a random value between 0 and 1.
random(a) Generate a random value between 0 and a. a must be a constant.
random(a, b) Generate a random value between a and b. a and b must be a constant.
stereo(a) Construct a stereo value from mono value a: { a, a }. Works for both mono scalar and mono buffer values.
stereo(a, b) Construct a stereo value from mono values a and b: { a, b }. Works for both mono scalar and mono buffer values.
floor(a) Rounds a towards negative infinity.
trunc(a) Rounds a towards 0.
ceil(a) Rounds a towards positive infinity.
round(a) Rounds a towards the nearest integer.
abs(a) Calculate the absolute value of a.
sqrt(a) Calculate the square root of a.
sin(a) Calculate sin of a. Uses a fast polynomial approximation.
cos(a) Calculate cos of a. Uses a fast polynomial approximation.
asin(a) Calculate asin of a.
acos(a) Calculate acos of a.
atan2(x, y) Calculate atan2 of x, y.
log(a) Calculate log of a.
exp(a) Calculate exp of a.
pow(a, b) Calculate a to the power b.
min(a, b) Returns the smallest between a and b. Equivalent to a < b ? a : b.
max(a, b) Returns the largest between a and b. Equivalent to a < b ? b : a.
clamp(a, b, c) Clamp a between b and c. Equivalent to min(max(a, b), c).
lerp(a, b, c) Linearly interpolate between b and c using a. Equivalent to b + a * (c - b).
time_to_samples(a, b, c) Convert a time parameter a in the range of b to c milliseconds to a number of samples. Takes into account the alterant tempo sync mode and sample rate.
time_to_coefficient(a, b, c) Convert a time parameter a in the range of b to c milliseconds to an envelope coefficient. Takes into account the alterant tempo sync mode and sample rate.
cepstral_lifter(a, b) Applies the cepstral lifter b to the data in a. Either returns a buffer or stereo buffer, depending on whether any arguments are stereo or not.
cepstral_combine(a, b, c) Recombines the modified liftered buffer a with the original buffer b, using the unmodified liftered signal c. For proper recombining, c must be the unaltered version of a.
reduce(a, operation) Reduce the buffer a using an operation: sum, product, min, max, absmin (smallest absolute value), absmax (biggest absolute value), argmin (index of the smallest number), argmax (index of the largest number), count (number of non-zero numbers), any (true if any non-zero numbers), all (true if only non-zero numbers).
scan(a, operation) Works the same as reduce, except it returns a buffer with all intermediate values. So for example scan(a, sum) returns the running sum of a.
reverse(a) Reverse buffer a.
sort(a) Sort buffer a.
argsort(a) Returns a buffer of indices that corresponds to the sorted permutation of a, such that sort(a) == a[argsort(a)].
convolve(a, b, size, pos, boundary) Apply a convolution over the buffer a using the kernel b, set the size of the kernel using size. pos decides the position of the kernel, must be one of: causal, centered, or forward. boundary determines how the boundary is handled, must be one of: zero, wrap, or clamp.
snap_frequency_to_scale(freq, scale) Snap the frequency to a frequency in the given scale. Use the value of a parameter with format=Scale as scale.
snap_note_to_scale(note, scale) Snap the note to a note in the given scale. Use the value of a parameter with format=Scale as scale.
note_in_scale(note, scale) Test whether the given note number is in the scale. Use the value of a parameter with format=Scale as scale.
scale_midi(scale) Test whether midi mode is enabled for the given scale. Use the value of a parameter with format=Scale as scale.

Indexing

Another powerful feature is the ability to index into buffers using expressions. And it is possible to configure how to handle boundaries, and whether to do any interpolation when the index is not a whole number.

There are 3 different boundary modes: wrap, clamp and zero.

  • wrap mode wraps the index around, so 512 becomes 0. Equivalent to data[index & 0x1FF].
  • clamp simply clamps the index between 0 and 511. Equivalent to data[min(max(index, 0), 511)].
  • zero returns 0 when reading out of bounds. Equivalent to index >= 0 && index < 512 ? data[index] : 0

There are 2 interpolation modes: none and linear.

  • none does no interpolation, this simply floors the floating point. Equivalent to data[floor(index)].
  • linear does linear interpolation. Equivalent to lerp(index - floor(index), data[floor(index)], data[floor(index) + 1]).

You can also set either one to fast, and it will use the fastest method, if you don't care about boundary mode. Right now, fast boundary is equivalent to wrap, and fast interpolation is none.

These indexing modes become incredibly powerful when combined with buffers, you can do things like:

output[i] = input[graph * size : linear wrap]

Note that output[i] is equivalent to just typing output.

It is also possible to write to scattered indices, however, this has some unexpected behaviour:

output[graph * size : linear wrap] = input[i]

This works to some degree, however, this is evaluated linearly, so if there are multiple writes to the same index, it will only maintain the latest value. This becomes even less intuitive when assigning to itself:

output[i] = 0
output[graph * size : linear wrap] = max(output[i], input[i])

You would expect this expression to keep the maximum value instead of the latest value. However, buffer operations are evaluated on the entire buffer at once, so it is actually evaluating max(output[i], input[i]) first on the whole buffer, before it starts assigning to output[graph * size : linear wrap]. So it is essentially equivalent to:

output[i] = 0
temp = max(output[i], input[i])
output[graph * size : linear wrap] = temp[i]

You can configure how to handle multiple writes using the scatter mode. There are 5 scatter modes:

  • last mode is the default, and just keeps the last written value.
  • min mode takes the value with the lowest magnitude. Keep in mind this includes the value already in the buffer, so if the buffer is filled with 0s it won't write anything else to the buffer in this mode.
  • max mode takes the value with the highest magnitude. Also includes the value already in the buffer.
  • add mode doesn't actually assign the new value, it imply adds it to the current value.
  • mult mode multiplies the current value.

You can also use fast here, which will just default to last.

So if we want to do what we tried to do before, and keep the maximum value from the input, we can write it like this:

output[i] = 0
output[graph * size : linear wrap max] = input[i]

You can also configure a global boundary, scatter mode, and interpolation mode using a config statement. If you do not specify a boundary or interpolation mode in the indexing operator, it will fall back to these. By default these are all set to fast.

@boundary clamp
@interpolation linear
@scatter max

Conditionals

Like in any programming language, it is also possible to write an if-statement.

if (wrap) {
  output = input[graph * size : wrap];
} else {
  output = input[graph * size : clamp];
}

You can only use a mono scalar value as a condition, you cannot use a stereo or buffer value. It is, however, possible to use both stereo and buffer values in ternary conditionals:

output = graph > 0.5 ? input : 0

Loops

Since most operations happen on entire buffers, you generally do not need loops. However, you still can write for-loops and while-loops if you need them. But keep in mind that these are extremely slow compared to simple buffer operations! But still, if you need them, you can still write them like this:

for (index = 0; index < size; index = index + 1) {
  output[index + 1] = output[index]
}

Or as a while-loop:

index = 0;
while (index < size) {
  output[index + 1] = output[index];
  index = index + 1;
}

These loops also support break and continue statements.

Persistent Data

For some algorithms you might need persistent data, like for an envelope follower for example. You can add any number of persistent variables to the script by defining them using the @data configuration. This persistent data will keep its value for as long as the voice that the alterant influences is active. The data is fully reset to 0 when a voice is retriggered. You can use the trigger global to initialize your persistent data to any value you want.

Here is an example:

@data stereo_buffer my_data
@data scalar counter

// Initialize the data on trigger
if (trigger) {
  my_data = noise()
}

if (counter == 100) {
  my_data = noise()
  counter = 0
}

output = input * my_data

// Update the data
delta_time = 1.0 / sample_rate
my_data = (my_data + delta_time) % 1
counter = counter + 1

Configuration

@name

The name that will be displayed in the browser view.

Usage: @name <string|identifier>

Examples:

@name "A Name"

@short_name

The name that will be displayed in the UI, if ommitted, @name will be used.

Usage: @short_name <string|identifier>

Examples:

@short_name "Name"

@author

The name of the author that created this alterant.

Usage: @author <string|identifier>

Examples:

@author "A Name"

@description

A description of what the alterant does.

Usage: @description <string|identifier>

Examples:

@description "This is an example alterant."

@inplace

Whether the algorithm works inplace, meaning it can use the same output buffer as the input buffer.

Usage: @inplace <true|false>

Defaults to: @inplace true

Examples:

@inplace true
@inplace false

@sync

Whether sync mode is enabled by default.

Usage: @sync <true|false>

Defaults to: @sync false

Examples:

@sync true
@sync false

@layers

In multi-layer mode, which layers this alterant processes by default.

Usage: @layers <0|1|2|level|frequency|phase|all> <0|1|2|level|frequency|phase> <0|1|2|level|frequency|phase>

Defaults to: @layers 0

Where level is the same as 0, frequency is the same as 1, and phase is the same as 2. all is equivalent to 0 1 2.

Examples:

@layers 0 1 2
@layers all
@layers level
@layers frequency

@graph

Configure the default graph when initializing this alterant.

Usage: @graph <line|eq|bar|none> [wrap=<true|false>] [log=<true|false>] [base=<number>] [points=<points>]

Where <points> is 1 or more [<number>, <number>] or [<number>, <number>, <number>] separated by spaces.

Defaults to: @graph none base=0.5

Examples:

@graph none
@graph eq base = 0.5 points = [0, 0, 2]
@graph bar base = 0
@graph line points = [0, 0] [1, 1]

@graph_modes

Add multiple graph modes. This gives you a way to have the graph mean different things. For example, a compressor alterant might want to have the graph mean 'mix' or 'threshold'. Adding these graph modes gives the user the ability to choose which one they want. Read the current selected graph mode with the global variable graph_mode.

Usage: @graph_modes <mode_names>

Where <mode_names> is 1 or more string or identifier defining the names of the different graph modes.

Examples:

@graph_modes Level Mix
@graph_modes "Threshold" "Smooth" "Mix"

@param

Configure parameters. Only parameters added as a config and have been given a name show up in the UI.

Usage: @param <0|1|2|3|4> [name=<string|identifier>] [default=<number>] [center=<number>] [transform=<transform>] [format=<format>]

Where <transform> is one of:

  • default
  • range[<number>,<number>]
  • integer[<number>,<number>]
  • power[<number>,<number>,<number>]
  • bipolar_power[<number>,<number>,<number>]
  • inverted_power[<number>,<number>,<number>]
  • bipolar_inverted_power[<number>,<number>,<number>]
  • decibels[<number>,<number>]

Where <format> is one of: default, integer, decibels, percent, frequency, pan, time, detune, multiplier, scale.

Examples:

@param 0 name="Mix" default=1 transform=range[0, 100] format=percent
@param 1 name=Gain transform=decibels[0, 1] format=decibels default=1
@param 2 name=Pan transform=range[-100, 100] default=0.5 format = pan
@param 3 name = "Attack" default = 0.0 transform = power[0, 10000, 2] format = time
@param 4 name = Scale default = 0.0 transform = range[1,10] format=multiplier

@data

Add persistent state.

Usage @data <scalar|stereo|buffer|stereo_buffer> <identifier>

Examples:

@data stereo_buffer position
@data scalar counter

@boundary

Set the default boundary handling.

Usage: @boundary <fast|wrap|clamp|zero>

Defaults to: @boundary fast

Examples:

@boundary fast
@boundary wrap

@interpolation

Set the default interpolation handling.

Usage: @interpolation <fast|none|linear>

Defaults to: @interpolation fast

Examples:

@interpolation none
@interpolation linear

@scatter

Set the default scatter mode.

Usage: @scatter <fast|last|min|max|add|mult>

Defaults to: @scatter fast

Examples:

@scatter fast
@scatter add

About

Custom scripting language for alterants in the VST synth Spektral.

Resources

Stars

2 stars

Watchers

1 watching

Forks

Contributors