HISE Docs

SNEX

The Scriptnode Expression Language (SNEX ) is a simplified subset of the C language family and is used throughout scriptnode for customization behaviour.

It's intended use is fast execution of expressions for signal processing algorithms. Unlike the HiseScript language, which is interpreted, the scriptnode expressions are JIT compiled and run in almost native speed (several orders of magnitude above HiseScript performance). When the scriptnode graph is exported as Cpp code, the expressions will be then compiled by the standard compiler (which is why it needs to be a strict subset of C / C++).

The JIT compiler uses the awesome asmjit library to emit the assembly instructions. Currently supported are macOS / Windows / Linux (32bit / 64bit). iOS support is not possible due to the security restriction that prevents allocation of executable memory.

There are two places where it can be used: in the core.jit node, which allows creating fully customizable nodes and at the connection between parameters (or modulation sources) via the Expression property which can be used to convert the value send to the connection target.

Getting started

The easiest way to get to know the language is to use the SNEX Playground which offers you a JIT compiler, a code editor with a predefined snippet and shows the assembly output that is fed directly to the CPU. It also uses the same callbacks as the core.jit node and offers various test signals that you can use to check your algorithm.

Language Reference

This is the complete language reference. Features that deviate from C / Cpp are emphasized

The SNEX syntax uses brackets for blocks and semicolons for statements. Comment lines start with // , multiline comments with /** Comment */)

/** This is a 
    multiline comment
*/
{
    x; // a statement
}

You can define variables in any scope (function scope or global scope), however there are no anonymous scopes .

Language structure

A valid SNEX code consists of definitions of variables and functions:

// some variables
type a = something;
type b = somethingElse;
...

// function definitions
type functionName()
{
    //... function body
}

There is no concept of classes or any other object oriented design principle. The rationale behind this is that a SNEX compiled object is already like a class, and the scope of a single SNEX code does not exceed the complexity of a single C++ class.

Variables

Variable names must be a valid identifier, and definitions must initialise the value :

type variableName = initialValue;

You can also define constant values by prepending const to the definition:

const type value = initialValue;

Doing so will speed up the compilation because it doesn't need to lookup the memory location.

Functions

Function definitions have this syntax:

ReturnType functionName(ArgumentType1 arg1, ArgumentType2 arg2)
{
   // body
}

Functions can be overloaded: despite having the same name, their argument amount and types can vary. However they can't differ only in the return type.

Types

Unlike HiseScript, SNEX is strictly typed. However there is a very limited set of available types :

There is no String type . Conversion between the types is done via a C-style cast:

// converts a float to an int
int x = (int)2.0f;

Type mismatches will be implicitely corrected by the compiler if possible (but it will produce a warning message so it's not recommended behaviour to just don't care about types).

Variable visibility

SNEX variables are visible inside their scope (= {...} block) or parent scopes. The inner scope has the highest priority and override variable names is possible:

void test()
{
    float x = 25.0f;
    
    {
        float x = 90.0f;
        Console.print(x); // 90.0f;
    }
    
    Console.print(x); // 25.0f;
}

However, since this is a common pitfall for bugs, it will produce a compiler warning.

Operators

Binary Operations

The usual binary operators are available:

a + b; // Add
a - b; // Subtract
a * b; // Multiply
a / b; // Divide
a % b; // Modulo

a++; // post-increment (no pre increment support!)

!a; // Logical inversion
a == b; // equality
a != b; // inequality
a > b; // greater than
a >= b; // greater or equal
// ...

The rules of operator precedence are similar to every other programming language on the planet. You can use parenthesis to change the order of execution:

(a + b) * c; // = a * c + b * c

Logical operators are short-circuited, which means that the second branch of a && operator will not be evaluated if the first branch is false:

int f1()
{
    Console.print(52.0f);
    return 0;
}

int f2()
{
    Console.print(12.0f);
    return 0;
}

void test()
{
    int c = f1() && f2() ? 2 : 1;
    Console.print(c);
}

// Will print 52 and 1 (f2 will not be executed)

Assignment

Assigning a value to a variable is done via the = operator. Other assignment types are supported:

int x = 12;
x += 3; // x == 15
x /= 3 // x == 5

You can access elements of a block via the [] operator:

block b;
b[12] = 12.0f;

There is an out-of-bounds check that prevents read access violations if you use an invalid array index. This comes with some performance overhead, which can be deactivated using a compiler flag.

Ternary Operator

Simple branching inside an expression can be done via the ternary operator:

a ? b : c

The false branch will not be evaluated.

Function calls

You can call other functions using this syntax: functionCall(parameter1, parameter2); Be aware that forward declaring is not supported so you can't call functions before defining them:

void f1()
{
    f2(); // won't work
}

void f1()
{
    doSomething();
}

void f3()
{
    f1(); // now you can call it
}

if / else-if branching

Conditional execution of entire code blocks is possible using the if / else keywords:

if(condition)
{
    // first case
}
else if (otherCondition)
{
    // second case
    return;
}
else
{
    // fallback code
}

// Some other code (will not be executed if otherCondition was true)

Return statement

Functions that have a return type need a return statement at the end of their function body:

void f1()
{
    // Do something
    return; // this is optional
}

int f2()
{
    return 42; // must return a int
}

Block Iterator

The only loop construct in SNEX is an iterator for a block using the range-based for loop syntax of C++:

double uptime = 0.0;

for(auto& sample: block)
{
    sample = (float)Math.sin(uptime);
    uptime += 0.002;
}

If you don't know C++, don't bother about the & symbol (it just means that it takes a reference to the value so you can actually change it). Be aware that the non-reference version:

for(auto sample: block)
    sample = 2.0f;

will not compile, since it would have no effect.

API classes

There are a few inbuilt API classes that offer additional helper functions.

The syntax for calling the API functions is the same as in HiseScript: Api.function() .

float x = Math.sin(2.0f);

The Math class contains overloaded functions for double and float , so be aware of implicit casts here.

Embedding the language

Embedding the language in a C++ project is pretty simple:

// Create a global scope that contains global variables.
snex::jit::GlobalScope pool;

// Create a compiler that turns a String into a function pointer.
snex::jit::Compiler compiler(pool);

// The SNEX code to be parsed - Check the language reference below
juce::String code = "float member = 8.0f; float square(float input){ member = input; return input * input; }";

// Compiles and returns a object that contains the function code and slots for class variables.
if (auto obj = compiler.compileJitObject(code))
{
    // Returns a wrapper around the function with the given name
    auto f = obj["square"];
    
    // Returns a reference to the variable slot `member`
    auto ptr = obj.getVariablePtr("member");

    DBG(ptr->toFloat()); // 8.0f

    // call the function - the return type has to be passed in via template.
    // It checks that the function signature matches 
    // and the JIT function was compiled correctly.
    auto returnValue = f.call(12.0f);
    
    DBG(returnValue); // 144.0f
    DBG(ptr->toFloat()); // 12.0f
}
else
{
    DBG(compiler.getErrorMessage());
}

Examples

These examples show some basic DSP algorithms and how they are implemented in SNEX. In order to use it, just load the given HISE Snippets into the latest version of HISE and play around.

Basic Sine Synthesiser

HISE automatically supports polyphony when

// we initialise it to a weird value, will get corrected later
double sr = 0.0;

// the counter for the signal generation
double uptime = 0.0;

// the increment value (will control the frequency)
double delta = 0.0;



void prepare(double sampleRate, int blockSize, int numChannels)
{
    // set the samplerate for the frequency calculation
    sr = sampleRate;
}

void reset()
{
    // When we start a new voice, we just need to reset the counter
    uptime = 0.0;
}

void handleEvent(event e)
{
    // get the frequency (in Hz) from the event
    const double cyclesPerSecond = e.getFrequency();

    // calculate the increment per sample
    const double cyclesPerSample = cyclesPerSecond / sr;
    
    // multiyply it with 2*PI to get the increment value
    delta = 2.0 * 3.14159265359 * cyclesPerSample;
}

void processFrame(block frame)
{
    // Calculate the signal
    frame[0] = (float)Math.sin(uptime);

    // Increment the value
    uptime += delta;

    // Copy the signal to the right channel
    frame[1] = frame[0];
}
/** Initialise the processing here. */
void prepare(double sampleRate, int blockSize, int numChannels)
{
    x.prepare(sampleRate, 500.0);
}

/** Reset the processing pipeline */
void reset()
{
    x.reset(0.0);
    x.set(1.0);
}

sdouble x = 1.0;

float processSample(float input)
{
    return (float)x.next();
}