Documents
Writing Docs Gigedit SFZ Instrument Scripts NKSP Language NKSP Reference

NKSP Language

This document intends to give you a compact introduction and overview to the NKSP real-time instrument script language, so you can start writing your own instrument scripts in short time. It concentrates on describing the script language. If you rather want to learn how to modify and attach scripts to your sounds, then please refer to the gigedit manual for how to manage instrument scripts with gigedit for Gigasampler/GigaStudio format sounds, or refer to the SFZ opcode script for attaching NKSP scripts with SFZ format sounds.

At a Glance

NKSP stands for "is Not KSP", which denotes its distinction to an existing proprietary language called KSP. NKSP is a script language specifically designed to write real-time capable software extensions to LinuxSampler's sampler engines that can be bundled individually with sounds by sound designers themselves. Instead of defining a completely new script language, NKSP is leaned on that mentioned properiatary script language. The biggest advantage is that sound designers and musicians can leverage the huge amount of existing KSP scripts which are already available for various purposes on the Internet, instead of being forced to write all scripts from scratch in a completely different language.

That also means however that there are some differences between those two languages. Some extensions have been added to the NKSP core language to make it a bit more convenient and less error prone to write scripts, and various new functions had to be added due to the large difference of the sampler engines and their underlying sampler format. Efforts have been made though to make NKSP as much compatible to KSP as possible. The NKSP documentation will emphasize individual differences in the two languages and function implementations wherever they may occur, to give you immediate hints where you need to take care of regarding compatibility issues when writing scripts that should be spawned on both platforms.

Please note that the current focus of NKSP is the sound controlling aspect of sounds. At this point there is no support for the graphical user interface function set of KSP in NKSP.

Event Handlers

NKSP is an event-driven language. That means you are writing so called event handlers which define what the sampler shall do on individual events that occur, while using the sound the script was bundled with. An event handler in general looks like this:

There are currently six events available:

Event Type Description
on note This event handler is executed when a new note was triggered, e.g. when hitting a key on a MIDI keyboard.
on release This event handler is executed when a note was released, e.g. when releasing a key on a MIDI keyboard.
on controller This event handler is executed when a MIDI control change event occurred. For instance when turning the modulation wheel of a MIDI keyboard.
on rpn This event handler is executed when a MIDI RPN event occurred.
on nrpn This event handler is executed when a MIDI NRPN event occurred.
on init Executed only once, as very first event handler, right after the script had been loaded. This code block is usually used to initialize variables in your script with some initial, useful data.

You are free to decide for which ones of those event types you are going to write an event handler for. You can write an event handler for only one event type or write event handlers for all of those event types. Also dependent on the respective event type, there are certain things you can do and things which you can't do. But more on that later.

Note Events

As a first example, the following tiny script will print a message to your terminal whenever you trigger a new note with your MIDI keyboard.

Probably you are also interested to see which note you triggered exactly. The sampler provides you a so called built-in variable called $EVENT_NOTE which reflects the note number (as value between 0 and 127) of the note that has just been triggered. Additionally the built-in variable $EVENT_VELOCITY provides you the velocity value (also between 0 and 127) of the note event.

The & character concatenates text strings with each other. In this case it is also automatically converting the note number into a text string.

The message() function is not appropriate for being used with your final production sounds, since it can lead to audio dropouts. You should only use the message() function to try out things, and to spot and debug problems with your scripts.

Release Events

As counter part to the note event handler, there is also the release event handler, which is executed when a note was released. This event handler can be used similarly:

Please note that you can hardly find MIDI keyboards which support release velocity. So with most keyboards this value will be 127.

Controller Events

Now let's extend the first script to not only show note-on and note-off events, but also to show a message whenever you use a MIDI controller (e.g. modulation wheel, sustain pedal, etc.).

It looks very similar to the note event handlers. $CC_NUM reflects the MIDI controller number of the MIDI controller that had been changed and %CC is a so called array variable , which not only contains a single number value, but instead it contains several values at the same time. The built-in %CC array variable contains the current controller values of all 127 MIDI controllers. So %CC[1] for example would give you the current controller value of the modulation wheel, and therefore %CC[$CC_NUM] reflects the new controller value of the controller that just had been changed.

There is some special aspect you need to be aware about: in contrast to the MIDI standard, monophonic aftertouch (a.k.a. channel pressure) and pitch bend wheel are handled by NKSP as if they were regular MIDI controllers. So a value change of one of those two triggers a regular controller event handler to be executed. To obtain the current aftertouch value you can use %CC[$VCC_MONO_AT], and to get the current pitch bend wheel value use %CC[$VCC_PITCH_BEND].

RPN / NRPN Events

There are also dedicated event handlers for MIDI RPN and NRPN events:

Since MIDI RPN and NRPN events are actually MIDI controller events, you might as well handle these with the previous controller event handler. But since RPN and NRPN messages are not just one MIDI message, but rather always handled by a set of individual MIDI messages, and since the precise set and sequence of actual MIDI commands sent varies between vendors and even among individual of their products, it highly makes sense to use these two specialized event handlers for these instead, because the sampler will already relief you from that burden to deal with all those low-level MIDI event processing issues and all their wrinkles involved when handling RPNs and NRPNs.

Even though there are two separate, dedicated event handlers for RPN and NRPN events, they both share the same built-in variable names as you can see in the example above.

So by reading $RPN_ADDRESS you get the RPN / NRPN parameter number that had been changed, and $RPN_VALUE represents the new value of that RPN / NRPN parameter. Note that these two built-in variables are a 14-bit representation of the parameter number and new value. So their possible value range is 0 .. 16383. If you rather want to use their (in MIDI world) more common separated two 7 bit values instead, then you can easily do that by wrapping them into either msb() or lsb() calls like also demonstrated above.

Script Load Event

As the last one of the six event types available with NKSP, the following is an example of an init event handler.

You might think, that this is probably a very exotic event. Because in fact, this "event" is only executed once for your script: exactly when the script was loaded by the sampler. This is not an unimportant event handler though. Because it is used to prepare your script for various purposes. We will get more about that later.

Comments

Let's face it: software code is sometimes hard to read, especially when you are not a professional software developer who deals with such kinds of things every day. To make it more easy for you to understand, what you had in mind when you wrote a certain script three years ago, and also if some other developer might need to continue working on your scripts one day, you should place as many comments into your scripts as possible. A comment in NKSP is everything that is nested into an opening and closing pair of curly braces.

{ This is a comment. }

You cannot only use this to leave some human readable explanations here and there, you might also use such curly braces to quickly disable parts of your scripts for a moment, e.g. when debugging certain things.

Variables

In order to be able to write more complex and more useful scripts, you also need to remember some data somewhere for being able to use that data at a later point. This can be done by using variables . We already came across some built-in variables, which are already defined by the sampler for you. To store your own data you need to declare your own user variables, which has the following form:

declare type variable-name := initial-value

The left hand side's variable-name is an arbitrary name you can choose for your variable. That name must consist of one or more characters of the English letters A to Z (lower and upper case), digits (0 to 9) the underscore character "_", and the name must not start with a digit.

Variable names must be unique within their individual variable name space. We will get more into detail about variable name spaces a bit later. For now just keep in mind that you can neither declare several variables with the same name, nor can you use a name for your variable that is already reserved by built-in variables.

The very first character in front of the variable name defines the data type of the variable. The supported data types are listed in the table below.

The right hand side's initial-value is simply the first value the variable should store right after it was created. You can also omit that initial value:

declare type variable-name

In this case the sampler will automatically assign an appropriate initial value, which depends on the variable's data type.

Variable Data Types

There are currently five different variable data types, which you can easily recognize upon their first character.

Variable Form Data Type Description
$variable-name Integer Scalar Stores one single integer number value.
%variable-name Integer Array Stores a certain amount of integer number values.
~variable-name Real Number Scalar Stores one single real (floating point) number value.
?variable-name Real Number Array Stores a certain amount of real (floating point) number values.
@variable-name String Stores one text string.

So the first character just before the actual variable name, always denotes the data type of the variable. Let's see what the differences between the individual variable data types are.

Integer Variables

The most simple variable data type in NKSP is the integer variable which is declared by adding a dollar sign just in front of the variable name:

declare $variable-name := initial-value

Or simply:

declare $variable-name

In the latter case the sampler will automatically assign the value 0 for you as the integer variable's initial value.

More specifically an integer variable declaration might look like this:

An integer variable can hold positive or negative integer numbers. Internally this type (meanwhile) has a size of 64-bit. That means a NKSP integer variable can hold any integer number in the range from -9223372036854775808 to +9223372036854775807.

This way we could for example count the total amount of notes triggered:

In the init event handler we create our own variable $numberOfNotes and assign 0 to it as its initial value. Like mentioned before, that initial assignment is optional. In the note event handler we then increase the $numberOfNotes variable by one, each time a new note was triggered and then print a message to the terminal with the current total amount of notes that have been triggered so far.

This variable data type is really limited to integer values only. So you cannot assign a value like 10.4 to this variable type as this would not be an integer. Trying to do so is treated as an error by the NKSP compiler. Also note when using integers in math formulas like in a division for example, as in many other programming languages, the result of the formula is an integer as well (if not type casted, but more about that later). So special care has to be taken in math formulas with integers.

Real Number Variables

To avoid the problems just described above with integer variables, you may instead use floating point arithmetics in NKSP, which can make calculations, that is mathematical formulas in your scripts, much easier and less error prone than with integer numbers. We call those floating point numbers real numbers and you can write them like you probably already expected; in simple dotted notation like 2.98. Accordingly there is a variable data type for real numbers.

The syntax to declare a real number variable is:

declare ~variable-name := initial-value

Or simply:

declare ~variable-name

In the latter case the sampler will automatically assign the value 0.0 for you as the real number variable's initial value.

More specifically a real number variable declaration might look like this:

So its declaration looks pretty much the same as with integer variables, just that you use "~" as prefix in front of the variable name instead of "$" that you would use for integer variables and the values must always contain a dot so that the parser can distinguish them clearly from integer numbers.

The main advantage of this data type is that it cannot only store a value like 1.0 or 2.0, but also any value in between those two (limited to a certain precision though), but may also hold values much larger or much smaller than integer variables. This makes this data type much more powerful than integers, and is therefore especially used in complex math formulas for that reason.

Number literals used with real number variables must always contain a dot with their value. So trying to assign 1 would be an error. You would use 1.0 instead. This is currently mandatory on language level to clearly distinguish between integers and real numbers. In future we might lift this restriction and rather just issue a warning instead. Keep in mind that this data type is still limited in precision, and calculated values may automatically be approximated according to that limited precision. And even though it can be useful to use this data type in certain situations, it is not always the best choice. For example when counting something, then you would better use an integer data type instead to avoid any floating point approximation issues.

Integer Array Variables

So far we have only seen scalar variables, that is a variable which can only hold exactly one single number. However sometimes it is useful to have a variable that can hold more than just one single number. For this purpose so called array variables exist. And the first array type we look at is the integer array variable. This is the common declaration form for creating an integer array variable:

declare %variable-name[array-size] := ( list-of-values )

The array-size defines the total amount of values this array variable can hold.

Similar to integer scalar variables, assigning some initial values with list-of-values is optional. The array declaration form without initial value assignment looks like this:

declare %variable-name[array-size]

When you omit that initial assignment, then all numbers of that array will automatically be initialized with the value 0 each.

With array variables, array-size is always mandatory though to be provided with an array variable declaration, so the sampler can create that array with the requested amount of values when the script is loaded. And in contrast to many other programming languages, changing that amount of values of an array variable is not possible after the variable had been declared. That's due to the fact that this language is dedicated to real-time applications, and changing the size of an array variable at runtime would harm real-time stability of the sampler and thus could lead to audio dropouts. For that reason NKSP does not allow you to do that.

So let's say you wanted to create an integer array variable with the first 12 prime numbers, then it might look like this.

Real Array Variables

Likewise there is also a real number array variable type which you can declare like this:

declare ?variable-name[array-size] := ( initial-values )

And, once again, assigning initial values is optional here as well:

declare ?variable-name[array-size]

If no initial-values are provided on declaration, then the array is automatically initialized with zero values, that is 0.0 for each element.

A more specifical example of declaring a real number array:

Once again, the only differences to integer array variables are that you use "?" as prefix in front of the array variable name instead of "%" that you would use for integer array variables, and that you must always use a dot for each value being assigned.

String Variables

You might also store text with variables. These are called text string variables , or short: string variables and are commonly declared like this:

declare @variable-name := initial-text

Or simply:

declare @variable-name

If no initial-text is assigned on declaration, then the string variable is automatically initialized as empty string "", that is a string of length 0. Unlike some low-level programming languages, in NKSP it is always safe to operate on string variables that have not been explicitly initialized or having turned into empty strings later on.

As an example of using string variables, let us modify a prior code example:

It behaves exactly like the prior example and shall just give you a first idea how to declare and use string variables.

Like with the message() function, you should not use string variables with your final production sounds, since it can lead to audio dropouts. You should only use string variables to try out things, and to spot and debug problems with your scripts.

Type Casting

Like KSP we are (currently) quite strict about your obligation to distinguish clearly between integer numbers and real numbers with NKSP. That means blindly mixing integers and real numbers e.g. in formulas, will currently cause a parser error like in this example:

~realVarA := (~realVarB + 1.9) / 24 { WRONG! }

In this example you would simply use 24.0 instead of 24 as number literal to fix the parser error:

~realVarA := (~realVarB + 1.9) / 24.0 { correct }

That's because, when you are mixing integer numbers and real numbers in mathematical operations, like the division in the latter example, what was your intended data type that you expected of the result of that division; a real number or an integer result? Because it would not only mean a different data type, it might certainly also mean a completely different result value accordingly.

That does not mean you were not allowed to mix real numbers with integers, it is just that you have to make your point clear to the parser what your intention is. For that purpose there are 2 built-in functions int_to_real() and real_to_int() which you may use for required type casts (data type conversions). So let's say you have a mathematical formula where you want to mix that formula with a real number variable and an integer variable, then you might e.g. type cast the integer variable like this:

~realVarA := ~realVarB / int_to_real($someIntVar)

And since this is a very common thing to do, we also have 2 short-hand forms of these 2 built-in functions in NKSP which are simply real() and int():

~realVarA := ~realVarB / real($someIntVar) { same as above, just shorter }

The short-hand functions real() and int() only exist in NKSP. They don't exist with KSP. In future we might lift this data type strictness and do it like many other programming languages handle this: just showing a parser warning (not an error) on mixed integer vs. real number expressions, and at the same time performing always an implied type cast of the respective integer to a real number type automatically by the compiler.

Variable Scope

For each variable declared you may also specify its variable scope by adding the respective qualifier keyword on variable declaration:

declare qualifier type variable-name

The specified variable scope defines from where the variable is accessible from, as well as its life-time and its amount of instances:

Variable Scope Qualifier Keyword Access Instances Life-Time
Global Variable none from entire script always exactly 1 as long as script/instrument is loaded
Local Variable local from current code block only separate, new instance for each execution as long as current code block is executed
Polyphonic Variable polyphonic from entire script separate, new instance for each note triggered as long as current note is playing

A variable must always be in exactly one of those 3 scopes. Therefore it would be an error to use both the keyword local and keyword polyphonic at the same time for a variable declaration. If you don't provide a specifier with your declaration, then that variable is automatically in the global variable scope.

Global Variables

By default, that is if you don't add one of the specifiers listed above on variable declaration, then that variable you declared with NKSP is a global variable .

declare $variable-name

With NKSP it does not matter much where exactly you declare a global variable. So far example:

is identical to:

and the same as:

and so on.

Unlike with KSP which in general only allows to declare variables inside of the init event handler, with NKSP though it does not matter where you declare global variables. You can declare them within the init event handler (like with KSP), inside of any other event handler block, inside of user functions, and outside of any event handler or user function.

A global variable starts to exist as soon as the script is loaded and continues to exist to the very end, that is until the script (or its using instrument that is) is unloaded. Furthermore, whenever you access a global variable, it is always the same variable instance. That means every event handler and function can access the data of such a global variable. And each instance of an event handler accesses the same data when it is referencing that variable. That's very simple and easy to understand, but can be a problem sometimes, which we will outline next with the following code example.

Let's assume you wanted to write an instrument script that shall resemble a simple delay effect. You could do that by writing a note event handler that automatically triggers several additional new notes for each note originally been triggered by the player on a MIDI keyboard. The following example demonstrates how that could be achieved.

NKSP's built-in function play_note() allows you to pass between one and four function arguments. For the function arguments you don't provide to a play_note() call, NKSP will automatically use default values. If you want your script to be compatible with KSP, then you should always pass four arguments to that function though.

In this example we used a new keyword const. This additional variable qualifier defines that we don't intend to change this variable after declaration. So if you know beforehand, that a certain variable should remain with a certain value, then you might use the const qualifier to avoid that you i.e. change the value accidently when you modify the script somewhere in future. It might also give the compiler the opportunity to perform additional runtime optimizations.

We also used the new keyword while which we will discuss in detail a bit later. For now only take it that the statements inside of while code block are executed several times in a loop until the specified while-loop condition (in round brackets) is true.

Now when you trigger one single note on your keyboard with that script, you will hear the additional notes being triggered. And also when you hit another note after a while, everything seems to be fine. However if you start playing quick successive notes, you will notice something goes wrong. The amount of notes being triggered by the script is now incorrect and also the volume of the individual notes triggered by the script is wrong. What's going on?

To understand the problem in the last example, let's consider what is happening exactly when executing that script: Each time you play a note on your keyboard, a new instance of the note event handler will be spawned and executed by the sampler. In all our examples so far our scripts were so simple, that in practice only one event handler instance was executed at a time. This is different in this case though. Because by calling the built-in wait() function, the respective handler execution instance is paused for a while and in total each handler instance will be executed for more than 2 seconds in this particular example. As a consequence, when you play multiple, successive notes on your keyboard in short time, you will have several instances of the note event handler running simultaniously. And that's where the problem starts. Because by default, as said, all variables are global variables. So the event handler instances which are now running in parallel, are all reading and modifying the same data. Thus the individual event handler instances will modify the $i and $velocity variables of each other, causing an undesired misbehavior.

Local Variables

As a logical consequence of the previously described data concurrency problem, it would be desirable to have each note event handler instance use its own variable instance, so that the individual handler instances stop interfering with each other. For this purpose the concept of so called local variables exist in programming languages. Therefor the local qualifier exists with NKSP. Declaring such a variable is identical to declaring any regular variable, just that you add the keyword local.

declare local type variable-name

A local variable is a temporary, usually short-lived variable of any data type, which is only accessible from within the same code block the local variabl was declared in. Each time an event handler execution instance is entering the respective code block, a new instance of the local variable is created and the local variable instance automatically stops to exist as soon as the event handler execution instance leaves that code block again.

That also means, even though accessing it by the same variable name, each event handler execution instance is actually accessing its own, separate variable data, even when executing (simultaniously) that same code block.

Unlike with global variables, with local variables it does matter where you declare the local variable. That's because a local variable only exists temporarily within the code block it was declared in.

So to fix the discussed bug in our previous example, we simply make the variables $i and $velocity local variables:

And that's it! The script works now as intended.

Local variables are a language extensions of NKSP. KSP only supports global and polyphonic variables.

Polyphonic Variables

A polyphonic variable is used to separate and share variable data on a per note level. Conceptually it is something between a global variable and a local variable.

declare polyphonic type variable-name

Like a global variable, a polyphonic variable is accessible from the entire script. However, for each note triggered by the player, a new instance of that polyphonic variable is created. The polyphonic variable instance stops to exist as soon as the respective note is no longer playing.

Unlike local variables though, a polyphonic variable can be accessed from all code blocks, and the same shared (note specific) variable data can be accessed even from different event handler instances. This can be used to easily transfer data from a note event handler to a release event handler for instance:

Note that if you were declaring $noteOnVelocity in this example as a global variable instead of a polyphonic one, then resulting behaviour would not be as intended. Because as soon as you were playing a chord, then the individual times the note event handler was executed, it would overwrite the global variable $noteOnVelocity with the latest note's velocity being played, and consequencely the subsequent executions of the release event handler would only print the velocity of the last note triggered, not the intended velocity of the note-on event corresponding to the processed note-off event.

Be aware that a polyphonic variable consumes a lot more memory than a global variable or a local variable. That's because for each polyphonic variable, the sampler has to allocate in advance (when the script is loaded) as many instances of that polyphonic variable as there are maximum events allowed with the sampler.

The polyphonic qualifier only exists for integer variables and real number variables (scalars). So you can neither declare polyphonic string variables, nor can you declare polyphonic array variables. Like in the previous explanation, this is due to the fact that it would consume a huge amount of memory for such variables. And with string variables and array variables, the required amount of memory would be much higher than with simple integer or real number variables.

Variable Name Spaces

Remember in the beginning, when we introduced variables, we mentioned that variable names must be unique within their respective variable name space . But what does that mean?

A variable is identified by its variable name. According to the variable name that you use in your script's code, the compiler performs a lookup of the actual variable instance that you want to access. The variable instance in turn is where the actual data of that variable is stored. Consequencely a variable name must be unique for the compiler to be able to always determine the correct variable instance for you. We call this unique set of variable names, where the compiler collects and remembers unique relations between a variable name and its actual variable instance, a variable name space.

However we have learned that there are different variable scopes in NKSP. Local variables do have a variable name space that's separate from global variables and polyphonic variables. That means you may declare a global variable (or polyphonic variable) and one or more local variables with the exact same variable name. Polyphonic variables however share the same variable name space with global variables. So you cannot declare a polyphonic variable while also declaring a global variable both with the exact same name.

You can even declare several local variables with the exact same name, as long as these local variables are declared in different code blocks:

Furthermore all variable data types share the same variable name space. That means you cannot declare a global variable with a name that has already been used to declare a global (or polyphonic) variable of another data type.

Multiple variable name spaces is a language extension of NKSP. In KSP there is only one single variable name space for all variables due to the fact that KSP only supports global and polyphonic variables.

Patch Variables

Instrument scripts give you the power and flexibility to achieve whatever exotic bahaviour you have in mind for your sounds.

Built-in patch parameters on the other hand, hard coded into the sampler engines and supported already by the underlying sampler file format, allow very quick and convenient parameter changes, e.g. by few simple clicks in the Gigedit instrument editor.

Why not having both?

This is what NKSP's patch variables are for:

declare patch type variable-name := initial-value

Or simply, as always:

declare patch type variable-name

Let's say you had some sort of tremolo effect script for your instruments and the speed of the tremelo effect can be altered in real-time by a MIDI controller. What you probably want to do is fine-tuning the speed range of the tremolo depending on the instrument, which would be the the desired fastest and slowest speed respectively when moving the controller to its upper and lower end.

And let's further allow to override the exact MIDI controller number for this as well, such that you could assign MIDI modulation wheel for instance for one instrument, while using the MIDI expression pedal for another instrument, simply because you might already have the modulation wheel assigned for another purpose with some instruments.

You could easily achieve this by declaring 3 patch variables:

By simply declaring an instrument script variable with the additional qualifier keyword patch you are publishing and exposing that variable as parameter in the instrument editor's patch parameters pane.

The additional const qualifier is not required. It just tells the compiler your intention that this variable shall not be altered during runtime, which makes sense in this example, but might be different in other examples.

Patch variables are shown and adjustable on a per instrument basis. So you can share the exact same script among multiple instruments and override certain script parameters to customize the script behaviour for your individual instruments if needed. Gigedit shows you the default values of each patch variable, that is the respective initial-value that you assigned on variable declaration. After changing a value of some of the variables in Gigedit's script tab, the respective variable turns into a bold font, so you can immediately see which script parameters you have adjusted for the specific instrument and which not. By selecting one of the overridden variables and hitting the Backspace (⌫) key you can easily revert those variables back to their default values.


By default, these variables act like any other variable. In our example for instance we assigned MIDI controller number 1 to $speedCC. So by default the modulation wheel (MIDI CC #1) is controlling our tremolo effect.

However you can now just select any instrument in Gigedit, click on the "Scripts" tab where it lists all patch variables, then click on the $speedCC variable's value and replace it with the value 11.

You have now assigned the MIDI expression pedal (MIDI CC #11) for this particular instrument, while your other instruments are still using the modulation wheel instead for controlling the speed of this tremolo effect.

We also wanted to fine-tune the tremolo's speed range. So let's say for this particular instrument we wanted to lower the maximum tremolo speed for not sounding too synthetically. So we just select $minTime variable's value in Gigedit's script pane and replace its default value 9000 by the new value 12000. And that's it! You can immediately hear the change of this with LinuxSampler without having to reload the instrument (provided you are running Gigedit not in stand-alone mode, but in live-mode with LinuxSampler).

Even though we just used and replaced simple scalar values in this example, the patch variables feature is not limited to simple values. You can assign any valid NKSP expression, both on script side for initial-value, as well as on instrument editor side when overriding it. So it might also be a complex math formula, one or more built-in function calls or user function calls, other variables, or any combination of these. And they might be completely different expressions on both ends. So think of this feature as a text replacement tool to whatever code was originally assigned to the variable.

Control Structures

A computer is more than a calculator that adds numbers and stores them somewhere. One of the biggest strength of a computer, which makes it such powerful, is the ability to do different things depending on various conditions. For example your computer might clean up your hard drive while you are not sitting in front of it, and it might immediately stop doing so when you need all its resources to cut your latest video which you just shot.

In order to do that for you, a computer program allows you to define conditions and a list of instructions the computer shall perform for you under those individual conditions. These kinds of software mechanisms are called Control Structures.

if Branches

The most fundamental control structure are if branches, which has the following general form.

The specified condition is evaluated each time script execution reaches this control block. The condition can for example be the value of a variable, some arithmetic expression, a function call or a combination of them. In all cases the sampler expects the condition expression to evaluate to some numeric (or boolean) value. If the evaluated number is exactly 0 then the condition is interpreted to be false and thus the list of statements is not executed. If the evaluated value is any other value than 0 then the condition is interpreted to be true and accordingly the list of statements will be executed.

Alternatively you might also specify a list of instructions which shall be executed when the condition is false.

In this case the first list of statements is executed when the condition evaluated to true, otherwise the second list of statements is executed instead.

Once again, let's get back to the example of counting triggered notes. You might have noticed that it did not output correct English for the first three notes. Let's correct this now.

We are now checking the value of $numberOfNotes before we print out a message. If $numberOfNotes equals one, then we assign the string "st" to the variable @postfix, if $numberOfNotes equals 2 instead we assign the string "nd" instead, if it equals 3 instead we assign "rd", in all other cases we assign the string "th". And finally we assemble the text message to be printed out to the terminal on line 23.

Select Case Branches

The previous example now outputs the numbers in correct English. But the script code looks a bit bloated, right? That's why there is a short hand form.

The provided expression is first evaluated to an integer value. Then this value is compared to the integer values of the nested case lines. So it first compares the evaluated value of expression with integer-1, then it compares it with integer-2, and so on. The first integer number that matches with the evaluated value of expression, will be interpreted as being the current valid condition. So if expression equals integer-1, then statements-1 will be executed, otherwise if expression equals integer-2, then statements-2 will be executed, and so on.

Using a select-case construct, our previous example would look like follows.

If you like, you can also put parentheses around the select expression, like select (expression). Some developers familiar with other programming languages might prefer this style. However if you want to keep compatibility with KSP, you should not use parentheses for select expressions.

The amount of case conditions you add to such select-case blocks is completely up to you. Just remember that the case conditions will be compared one by one, from top to down. The latter can be important when you define a case line that defines a value range. So for instance the following example will not do what was probably intended.

You probably get the idea what this script "should" do. For the 1st note it should print "First note was triggered!", for the 2nd note it should print "Second note was triggered!", for the 3rd note it should print "Third note was triggered!", for the 4th up to 99th note it should print "Less than 100 notes triggered so far", and starting from the 100th note and all following ones, it should print the precise note number according to line 23. However, it doesn't!

To correct this problem, you need to move the first case block to the end, like follows.

Or you could of course fix the questioned case range from case 1 to 99 to case 4 to 99. Both solutions will do.

We also used the built-in function exit() in the previous example. You can use it to stop execution at that point of your script. In the previous example it prevents multiple messages to be printed to the terminal.

The exit() function only stops execution of the current event handler instance! It does not stop execution of other instances of the same event handler, nor does it stop execution of other handlers of other event types, and especially it does not stop or prevent further or future execution of your entire script! In other words, you should rather see this function as a return statement, in case you are familiar with other programming languages already.

while Loops

Another fundamental control construct of program flow are loops. You can use so called while loops with NKSP.

A while loop is entered if the provided condition expression evaluates to true and will then continue to execute the given list of statements down to the end of the statements list. The condition is re-evaluated each time execution reached the end of the statements list and according to that latest evaluated condition value at that point, it will or will not repeat executing the statements again. If the condition turned false instead, it will leave the loop and continue executing statements that follow after the while loop block.

The next example will print the same message three times in a row to the terminal, right after the script had been loaded by the sampler.

When the while loop is reached for the first time in this example, the condition value is 3. And as we learned before, all integer values that are not 0 are interpreted as being a true condition. Accordingly the while loop is entered, the message is printed to the terminal and the variable $i is reduced by one. We reached the end of the loop's statements list, so it is now re-evaluating the condition, which is now the value 2 and thus the loop instructions are executed again. That is repeated until the loop was executed for the third time. The variable $i is now 0, so the loop condition turned finally to false and the loop is thus left at that point and the text message was printed three times in total.

User Functions

We already came across various built-in functions, which you may call by your scripts to perform certain tasks or behavior which is already provided for you by the sampler. NKSP however also allows you to write your own functions, which you then may call from various places of your script. These are called user functions and may be declared like this:

You can choose an arbitrary name for your own user functions (again limited to English letters, digits and underscore), as long as the name is not already reserved by a built-in function or by another user function that you have declared before.

Note that for calling a user function, you must always precede the actual user function name with the call keyword:

call function-name

Likewise you may however not use the call keyword for calling any built-in function. So that substantially differs calling built-in functions from calling user functions.

A very first simple example of declaring a user function and calling it:

When working on larger scripts, you may notice that you easily get to the point where you may have to duplicate portions of your script code, since there are certain things that you may have to do again and again in different parts of your script. Software developers usually try to avoid such code duplications to keep the overall amount of code as small as possible, since the overall amount of code would bloat quickly and would make the software very hard to maintain. One way for you to avoid such script code duplications with NKSP is to write and call User Functions.

Let's assume you wanted to create a simple stuttering effect. You may do so like in the following example.

This script will run an endless loop for each note being triggered. Every 200ms it will turn the volume alternatingly down and up to create the audible stuttering effect. After each wait() call it calls event_status($EVENT_ID) to check whether this note is still alive, and as soon as the note died, it will stop execution of the script instance by calling exit(). The latter is important in this example, because otherwise the script execution instances would continue to run in this endless loop forever, even after the respectives notes are gone. Which would let your CPU usage increase with every new note and would never decrease again. This behavior of the sampler is not a bug, it is intended, since there may also be cases where you want to do certain things by script even after the respective notes are dead and gone.

However as you can see, that script is using the same portions of script code twice.

To avoid that, you could also write the same script with a user function like this:

Previously we were using exit() to quit the script, whereas here we are using abort($NI_CALLBACK_ID). That's because the built-in exit() function acts like a return statement. So calling exit() from a user function would only cause to return execution back to where the user function was called at, and then continue execution from there.

The script became in this simple example only slightly smaller by introducing a user function, but the overall code also became easier to read and behaves identically to the previous solution. But your feeling is right, we can do better.

You may also declare function arguments and / or a result value for your user function:

So let's say you wanted to write a simple function that takes three arguments, calculates their sum, and returns that sum as result value:

which you could then use like this:

Or even shorter, like this:

Both function arguments and function's result value are handled by NKSP like local variables. That means their life-time is limited to the scope of the user function, i.e. they exist as long as the user function is executed. And like ordinary local variables they have their own variable name space. So you can declare function arguments and results with the exact same name like global variables. In this case, in the body of the user function, the function argument / result variable is used instead of the global variable.

Taking all this, we can now simplify our previous stuttering effect by adding two function arguments to the user function:

If you want, you may further introduce a second user function to make the "is the note dead?" check more readable:

Both user function arguments and user function return values do support optional default values:

For instance:

Which then would make it legal to call the user function just like this:

call myFn(10)

But also like this:

call myFn(10, 20)

Or like this:

call myFn(10, 20, 30)

All the arguments not supplied by caller would then automatically be initialized with their respective default value in that case.

Arguments and results for user functions are a language extension of NKSP. They are not supported by KSP.

Synchronized Blocks

When we introduced the local qualifier keyword previously, we learned that a script may automatically be suspended by the sampler at any time and then your script is thus sleeping for an arbitrary while. The sampler must do such auto suspensions under certain situations in cases where an instrument script may become a hazard for the sampler's overall real-time stability. If the sampler would not do so, then instrument scripts might easily cause audio dropouts, or at worst, buggy instrument scripts might even lock up the entire sampler in an endless loop. So auto suspension is an essential feature of the sampler's real-time instrument script engine.

Now the problem as a script author is that you don't really know beforehand why and when your script might get auto suspended by the sampler. And when you are working on more complex, sophisticated scripts, you will notice that this might indeed be a big problem in certain sections of your scripts. Because in practice, a sophisticated script often has at least one certain consecutive portion of statements which must be executed in strict consecutive order by the sampler, which might otherwise cause concurrency issues and thus misbehavior of your script if that sensible code section was auto suspended in between. A typical example of such concurrency sensible code sections are statements which are reading and conditionally modifying global variables. If your script gets auto suspended in such a code section, another script handler instance might then interfere and change those global variables in between.

To avoid that, you can place such a sensible code section at the very beginning of your event handler. For example consider you might be writing a custom glissando script starting like this:

Because the earlier statements are executed in an event handler, the higher the chance that they will never get auto suspended. And with those couple of lines in the latter example you might even be lucky that it won't ever get suspended in that sensible code section at least. However when it comes to live concerts you don't really want to depend on luck, and in practice such a sensible code section might be bigger than this one.

That's why we introduced synchronized code blocks for the NKSP language, which have the following form:

All statements which you put into such a synchronized code block are guaranteed that they will never get auto suspended by the sampler.

Such synchronized blocks are a language extension which is only available with NKSP. KSP does not support synchronized blocks.

So to make our previous example concurrency safe, we would change it like this:

If you are already familiar with some programming languages, then you might already have seen such synchronized code block concepts in languages like e.g. Java. This technique really provides an easy way to protect certain sections of your script against concurrency issues.

You must use such synchronized code blocks only with great care! If the amount of statements being executed in your synchronized block is too large, then you will get audio dropouts. If you even use loops in synchronized code blocks, then the entire sampler might even become unresponsive in case your script is buggy!

Operators

A programming language provides so called operators to perform certain kinds of transformations of data placed next to the operators. These are the operators available with NKSP.

Arithmetic Operators

These are the most basic mathematical operators, which allow to add, subtract, multiply and divide integer values with each other.

You may either use direct integer literal numbers like used in the upper example, or you can use integer number variables or integer array variables.

Boolean Operators

To perform logical transformations of boolean data, you may use the following logical operators:

Keep in mind that with logical operators shown above, all integer values other than 0 are interpreted as boolean true while an integer value of precisely 0 is interpreted as being boolean false.

So the logical operators shown above always look at numbers at a whole. Sometimes however you might rather need to process numbers bit by bit. For that purpose the following bitwise operators exist.

Bitwise operators work essentially like logical operators, with the difference that bitwise operators compare each bit independently. So a bitwise .and. operator for instance takes the 1st bit of the left hand's side value, the 1st bit of the right hand's side value, compares the two bits logically and then stores that result as 1st bit of the final result value, then it takes the 2nd bit of the left hand's side value and the 2nd bit of the right hand's side value, compares those two bits logically and then stores that result as 2nd bit of the final result value, and so on.

Comparison Operators

For branches in your program flow, it is often required to compare data with each other. This is done by using comparison operators, enumerated below.

All these operations yield in a boolean result which could then be used e.g. with if or while loop statements.

String Operators

Last but not least, there is exactly one operator for text string data; the string concatenation operator &, which combines two text strings with each other.

We have used it now frequently in various examples before.

Standard Measuring Units

If you are coming from KSP then you are eventually going to think next "WTF? What is this all about?". But hang with me, no matter how often you wrote instrument scripts before, you will most certainly regularly come into a situation like described next and we have a convenient fix for that.

Unit Literals

Let's consider you wanted to pause your script at a certain point for let's say 1 second. Ok, you remember from the back of your head that you need to use the built-in wait() function for that, but which value do you need to pass exactly to achieve that 1 second break? Would it be wait(1000) or probably wait(1000000)? Of course now you reach out for the reference documentation at this point and eventually find out that it would actually be wait(1000000). Not very intuitive. And the large amount of zeros required does not help to make your code necessarily more readable either, right?

So what about actually writing what we had in mind at first place:

wait(1s)

It couldn't be much clearer.

Or you want a break of 23 milliseconds instead? Then let's just write that!

wait(23ms)

Now let's consider another example: Say you wanted to reduce volume of some voices by 3.5 decibel. You remember that was something like change_vol(note, volume), but what would volume be exactly? Digging out the docs yet again you find out the correct call was change_vol($EVENT_ID, -3500).

We can do better than that:

change_vol($EVENT_ID, -3.5dB)

You rather want a slight volume increase by just 56 milli dB instead?

change_vol($EVENT_ID, +56mdB)

Or let's lower the tuning of a note by -24 Cents:

change_tune($EVENT_ID, -24c)

I'm sure you got the point. We are naturally using standard measuring units in our daily life without noticing their importance, but they actually help us a lot to give some otherwise purely abstract numbers an intuitive meaning to us. Hence it just made sense to add measuring units as core feature of the NKSP language, their built-in functions, variables and whatever you do with them.

Calculating with Units

Having said that, these examples above were just meant as warm up appetizer. Of course you can do much more with this feature than just passing them literally to some built-in function call as we did above so far. You can assign them to variables, too, like:

declare ~pitchLfoFrequency := 1.2kHz

You can use them in combinations with integers or real numbers, and of course you can do all mathematical calculations and comparisons that you would naturally be able to do in real life. For instance the following example

would print the text "Result of calculation is 988ms" to the terminal (notice that $a and $b actually used different units here).

Or the following example

would print the text "10.5mdB" to the terminal.

Or let's make this little value comparison check:

which will succeed of course (notice again that $foo and $bar used different units here as well).

So as you can see the units are not just eye candy for your code, they are actually interpreted actively by the script engine appropriately such that all your calculations, comparisons and function calls behave as you would expect them to do from your real-life experience.

Unit Components

In the examples above you might have noticed that the units' components were shown in different colors. That's not a glitch of the website, that's intentional and in fact NKSP code on this website is in general, automatically displayed in the same way as with e.g. Gigedit's instrument script editor. So what's the deal?

If you take the value 6.8mdB as an example, you have in front the numeric component = 6.8 of course, followed by the metric prefix = md for "milli deci" (which is always simply some kind of multiplication factor) and finally the fundamental unit type = B for "Bel" (which actually gives the number its final meaning).

So here's where language design comes into play. From language point of view both the numeric component and the optional metric prefix are runtime features which may change at any time, whereas the optional unit type is always a constant, "sticky", parse-time feature that you may never change at runtime. That means if you define a variable like e.g. declare $foo := 1s that variable $foo is now firmly tied to the unit type "seconds" for your entire script. You may change the variable's numeric component and metric prefix later on at any time like e.g. $foo := 8ms, but you must not change the variable ever to a different unit type later on like $foo := 8Hz. Trying to switch the variable to a different unit type that way will cause a parser error. Changing the fundamental unit type of a variable is not allowed, because it would change the semantical meaning of the variable.

What may look like a lousy limitation of the technical implementation is in fact an intentional language design decision and is actually a feature, called determinism. The price of this limitation of forcing unit types to be a constant parse time feature of variables and expressions comes with the profit of buying substantial error checks at parse time, and that in turn helps you to write more reliable instrument scripts in a shorter amount of time. For instance no matter how complex your mathematical formulas are in your scripts, the parser will always be able to check already at parse-time whether the final, evaluated results of your formulas and overall code that you pass to built-in functions, will finally be of correct unit type expected by the respective function that you are going to call with them as function arguments. Or in other words: the parser is able to check the correct meaning of your formulas at parse-time. So the parser will stop you immediately from doing such and similar mistakes by raising a parser error immediately while you are typing your script code. So you neither have to load the script into the sampler, nor do you have to run and test the code just to spot such kind of mistakes. You will always see them instantly in the code editor while you are typing your code.

So getting back and proceed with an early example, this code would be fine:

That's Ok because the built-in function change_vol() optionally accepts the unit B for its 2nd argument. Whereas the following would immediately raise a parser error:

That's because using the unit type Hertz for changing volume with the built-in function change_vol() does not make any sense, that built-in function expects a unit type suitable for volume changes, not a unit type for frequencies, and hence it is clearly a programming mistake. So getting this error in practice, you may have simply picked a wrong variable by accident for a certain function call for instance and the parser will immediately point you on that undesired circumstance.

As another example, you may now also use units with the built-in random number generating function like e.g. random(100Hz, 5kHz). The function would then return an arbitrary value between 100Hz and 5kHz each time you call it that way, so that makes sense. But trying e.g. random(100Hz, 5s) would not make any sense and consequently you would immediately get a parser error that you are attempting to pass two different unit types to the random() function, which is not accepted by this particular built-in function. And these kinds of parse-time errors are always detected, no matter whether you are literally passing constant values like in the simple example here, but also through every other means like variables and complex mathematical expressions.

The following tables list the unit types and metric prefixes currently supported by NKSP.

Unit Type Description Purpose
s short for "seconds" May be used for time durations.
Hz short for "Hertz" May be used for frequencies.
B short for "Bel" May be used for volume changes and other kinds of relative changes (e.g. depth of envelope generators).
Metric Prefix Description Equivalent Factor
u short for "micro" 10-6  =  0.000001
m short for "milli" 10-3  =  0.001
c short for "centi" 10-2  =  0.01
d short for "deci" 10-1  =  0.1
da short for "deca" 101  =  10
h short for "hecto" 102  =  100
k short for "kilo" 103  =  1000

Of course there are much more standard unit types and metric prefixes than those, but currently we only support those listed above. Simply because these listed ones were actually useful for instrument scripts.

When changing tuning, which is commonly expected by musicians in "Cents", like e.g.: change_tune($EVENT_ID, -24c), you might have noticed already from the markup color here, that this is actually not handled as a unit type by the NKSP language and that's why it is not listed as a unit type in the table above. So tuning changes in "Cents" is actually just a value with metric prefix "centi" and without any unit type, since tuning changes in "Cents" is really just a relative multiplication factor for changing the pitch of a note depending on the current base frequency of the note.

This might look a bit odd to you, it is semantically however absolutely correct to handle tuning changes in "Cents" that way by the language. You can still also use expressions like "milli Cents", e.g.: change_tune($EVENT_ID, +324mc), which is also valid since we (currently) allow a combination of up to 2 metric prefixes with NKSP.

The obvious advantage of not making "Cents" a dedicated unit type is that we can just use the character "c" in scripts both for tuning changes, as well as for conventional "centi" metric usage like 1cs ("one centi second"). The downside of this design decision (that is "Cents" being defined as metric prefix) on the other hand means that we loose the previously described parse-time stickyness feature that we would have with "real" unit types, and hence also loose some of the described error detection mechanisms that we have with "real" unit types at parse time.

In practice that means: you need to be a bit more cautious when doing calculations with tuning values in "Cents" compared to other tasks like volume changes, because with every calculation you do in your scripts, you might accidentally drop the "Cents" from your unit, which eventually will cause e.g. the change_tune() function to behave completely differently (since a value without any metric prefix will then be interpreted by change_tune() to be a value in "milli cents", exactly like this function did before introduction of units feature in NKSP).

Unit Conversion

Even though you are not allowed to change the unit type of a variable itself by assignment at runtime, that does not mean there was no way to get rid of units or that you were unable to convert values from one unit type to a different unit type. You can do that very easily actually with NKSP, exactly as you learned in school; i.e. by multiplications and divisions.

Let's say you have a variable $someFrequency that you use for controlling some LFO's frequency by script, and for some reason you really want to use the same value of that variable (for what reason ever) to change some volume with change_vol(), then all you have to do is dividing the existing unit type away, and multiplying it with the new unit type:

Which would convert the variable's original value 100Hz to 100mdB before passing it to the change_vol() function. So this actually did 3 things:

  1. the divsion (by / 1Hz) dropped the old unit type (Hertz),
  2. the multiplication (by * 1mdB) added the new unit type (Bel)
  3. and that multiplication also changed the metric prefix (to milli deci) before the result is finally passed to the change_vol() function.

And since change_vol() would now receive the value in correct unit type, this overall solution is hence legal and accepted by the parser without any complaint. And this type of unit conversion does not break any parse-time determinism and error detection features either, since it is not touching the variable's unit type directly (only the temporary value eventually being passed to the change_vol() function here), and so the result of the unit conversion expressions above can always reliably be evaluated by the parser at parse-time.

There are some intended limitations when performing unit type conversions though. For instance you are never allowed to multiply some unit type with another unit type in NKSP, neither different unit types like e.g. 100Hz * 1B, nor with the same unit type like e.g. 4s * 8s. That's because we don't have any practical usage for e.g. "square seconds" or other kinds of mixed unit types in instrument scripts. So trying to create a number or variable with more than one unit type will always raise a parser error. So keep that in mind and use common sense when writing calculations with units. And like always: the parser will always point you on misusage immediately.

Array Variables

And as we are at limitations regarding units: Currently unit types are not accepted for array variables yet. Metric prefixes are allowed though!

Main reason for that current limitation is that unlike with scalar variables, accessing array variables at runtime with an index by yet another (runtime changeable) variable might break the previously described parse-time determinism of unit types. That means if we just take the array variable %bar[] declared above and would access it in our scripts with another variable like:

%bar[$someVar]

then what would that unit type of that array access be? Notice that the array variable %bar[] was initialized with 3 different unit types for its individual elements. So the unit type of the array access would obviously depend on the precise value of variable $someVar, which most probably will change at runtime and hence the compiler would not know at parse-time yet.

This limitation will most probably be lifted later on by allowing exactly one unit type for an array variable, so that the array would be initialized with exactly the same unit type for all its elements to retain the parse-time determinism that we were talking about.

User Functions

You can also define user function arguments and user function return value to be of a specific unit type like Hz for frequencies or dB for loudness, etc., which provides a more intuitive approach to these variables:

So the default values in this case not only initialize those function arguments and result values, it also makes those arguments and the result value sticky to the specified unit type. Hence if the function was called with a value that does not contain any unit or another unit, then this would cause a compiler error.

So this measure also prevents many programming errors already on language level when using user functions.

Finalness

Here comes another core language feature of NKSP that you certainly don't know from KSP (as it does not exist there), and which definitely requires an elaborate explanation of what it is about: "finalizing" some value.

Default Relativity

When changing synthesis parameters, these are commonly relative changes, depending on other modulation sources. For instance let's say you are using change_vol() in your script to change the volume of voices of a note, then the actual, final volume value being applied to the voices is not just the value that you passed to change_vol(), but rather a combination of that value and values of other modulation sources of volume like a constant gain setting stored with the instrument patch, as well as a continuously changing value coming from an amplitude LFO that you might be using in your instrument patch, and you might most certainly also use an amplitude envelope generator which will also have its impact on the final volume of course. All these individual volume values are multiplied with each other in real-time by the sampler's engine core to eventually calculate the actual, final volume to be applied to the voices, like illustrated in the following schematic figure.

This relative handling of synthesis parameters is a good thing, because multiple modulation sources combined make up a vivid sound. However there are situations where this combined behaviour for synthesis parameters is not what you want. Sometimes you want to be able to just say in your script e.g. "Make the volume of those voices exactly -6dB. Period. I mean it!". And that's exactly what the introduced "final" operator ! does.

Final Operator

By prepending an exclamation mark ! in front of an expression as shown in the code above, you mark that value of that expression to be "final", wich means the value will bypass the values of all other modulation sources, so the sampler will ignore all other modulation sources that may exist, and will simply use your script's value exclusively for that synthesis parameter, as illustrated in the following figure:

You can of course revert back at any time to let the sampler process that synthesis parameter relatively again by calling change_vol() and just passing a value for volume without "finalness" (i.e. without ! operator) this time.

In the previous code example, the "finalness" was applied to the temporary value being passed to the change_vol() function, it did not change the information stored in variable $volume at all though. So this is different from:

In the latter code example the actual "finalness" is stored directly now to the $volume variable instead. Both approaches make sense depending on the actual use case. For instance if you only need "finalness" in rare situations, then you might use the prior solution by using the "final" operator just with the respective function call, whereas in use cases where you would always apply the $volume "finally" and probably need to pass it to several change_vol() function calls at several places in your script, then you might store the "finalness" information directly to the variable instead.

KSP is also using the exclamation mark in front of variable names of string arrays. Our usage of the exclamation mark character for this "finalness" feature does not cause a language conflict with that aspect though, because variable names (i.e. containing exclamation mark) are resolved by the language before our unary ! "final" operator is resolved in expressions.

Mixed Finalness

Like with the other language extensions described before, we also have some potential ambiguities that we need to deal with when applying "finalness". For instance consider this code:

Should the resulting, expected volume change of -4dB be applied as "final" value or as relative value instead? Because the problem here is that !-6dB obviously means "final", whereas + 2dB is actually a relative value to be added.

In the current version of the sampler the value to be applied in this case would be "final", so you will not get a parser error, however you will get a parser warning to make you aware about this ambiguity. So to fix the example above, that is to to get rid of that parser warning, you can simply add an exclamation mark in front of the other number as well like:

Also built-in functions will behave similarly as described above. Certain built-in functions accept finalness for all of their arguments, some functions accept finalness for only certain arguments and some functions won't accept finalness at all. Like with the other core language features always use common sense and quickly think about whether it would make sense if a certain function would accept finalness for its argument(s). Most of the time your guess will be right, and if not, then the parser will tell you immediately with either an error or warning, and the NKSP built-in functions reference will help you out with details in such rare cases where things might not be clear to you immediately.

Implied Finalness

The unit type "Bel" used in the examples for the "final" operator above is somewhat special, since the unit type "Bel" is in general used for relative quantities like i.e. volume changes. Tuning changes (i.e. in "Cents") are also relative quantities.

However other unit types like "seconds" or "Hertz" are absolute quantities. That means if you are using unit types "Hertz" or "seconds" in your scripts, then their values are automatically applied as implied "final" values, as if you were using the ! operator for them in your code. The parser will raise a parser warning though to point you on that circumstance.

The following table outlines this issue for the currently supported unit types.

Unit Type Relative Final Reason
None Yes, by default. Yes, if ! is used. If no unit type is used (which includes if only a metric prefix is used like e.g. change_tune($EVENT_ID, -23c)) then such values can be used both for relative, as well as for 'final' changes.
s No, never. Yes, always. This unit type is naturally for absolute values only, which implies its value to be always 'final'.
Hz No, never. Yes, always. This unit type is naturally for absolute values only, which implies its value to be always 'final'.
B Yes, by default. Yes, if ! is used. This unit type is naturally for relative changes. So this unit type can be used both for relative, as well as for 'final' changes.
Since unit types like seconds and Hertz are naturally always used for absolute values in real life, we might drop the mentioned parser warnings in future which currently occur if those units are used in scripts without having used the ! operator.

Array Variables

As with unit types, the same current restriction applies to "finalness" in conjunction with array variables at the moment: you may currently not apply "finalness" to the elements of array variables yet.

declare %foo[3] := ( !-6dB, -8dB, !2dB ) { WRONG - finalness not allowed for arrays (yet) ! }

The reason is also exactly the same, because finalness is a parse-time required information and an array access by using yet another variable like e.g. %foo[$someVar] might break that parse-time awareness of "finalness" for the compiler.

Likewise we might certainly lift that restriction later on by allowing finalness to be applied to arrays by initializing all members of an array to be all "final".

Preprocessor Statements

Similar to low-level programming languages like C, C++, Objective C and the like, NKSP supports a set of so called preprocessor statements. These are essentially "instructions" which are "executed" or rather processed, before (and only before) the script is executed by the sampler, and even before the script is parsed by the actual NKSP language parser. You can think of a preprocessor as a very primitive parser, which is the first one getting in touch with your script, it modifies the script code if requested by your preprocessor statements in the script, and then passes the (probably) modified script to the actual NKSP language parser.

When we discussed comments in NKSP scripts before, it was suggested that you might comment out certain code parts to disable them for a while during development of scripts. It was also suggested during this language tour that you should not use string variables or use the message() function with your final production sounds. However those are very handy things during development of your instrument scripts. You might even have a bunch of additional code in your scripts which only satisfies the purpose to make debugging of your scripts more easy, which however wastes on the other hand precious CPU time. So what do you do? Like suggested, you could comment out the respective code sections as soon as development of your script is completed. But then one day you might continue to improve your scripts, and the debugging code would be handy, so you would uncomment all the relevant code sections to get them back. When you think about this, that might be quite some work each time. Fortunately there is an alternative by using preprocessor statements.

Set a Condition

First you need to set a preprocessor condition in your script. You can do that like this:

SET_CONDITION(condition-name)

This preprocessor "condition" is just like some kind of boolean variable which is only available to the preprocessor and by using SET_CONDITION(condition-name), this is like setting this preprocessor condition to true. Like with regular script variables, a preprocessor condition name can be chosen quite arbitrarily by you. But again, there are some pre-defined preprocessor conditions defined by the sampler for you. So you can only set a condition name here which is not already reserved by a built-in preprocessor condition. Also you shall not set a condition in your script again if you have already set it before somewhere in your script. The NKSP preprocessor will ignore setting a condition a 2nd time and will just print a warning when the script is loaded, but you should take care of it, because it might be a cause for some bug.

Reset a Condition

To clear a condition in your script, you might reset the condition like so:

RESET_CONDITION(condition-name)

This is like setting that preprocessor condition back to false again. You should only reset a preprocessor condition that way if you did set it with SET_CONDITION(condition-name) before. Trying to reset a condition that has not been set before, or trying to reset a condition that has already been reset, will both be ignored by the sampler, but again you will get a warning, and you should take care about it.

Conditionally Using Code

Now what do you actually do with such preprocessor conditions? You can use them for the NKSP language parser to either

You can achieve that by wrapping NKSP code parts into a pair of either

preprocessor statements, or between

statements. In the first case, the NKSP code portion is used by the NKSP language parser if the given preprocessor condition-name is set (that is if condition is true). If the condition is not set, the NKSP code portion in between is completely ignored by the NKSP language parser.

In the second case, the NKSP code portion is used by the NKSP language parser if the given preprocessor condition-name is not set (or was reset) (that is if condition is false). If the condition is set, the NKSP code portion in between is completely ignored by the NKSP language parser.

Let's look at an example how to use that to define conditional debugging code.

The built-in function num_elements() used above, can be called to obtain the size of an array variable at runtime. As this script looks now, the debug messages will be printed out. However it requires you to just remove the first line, or to comment out the first line, in order to disable all debug code portions in just a second:

Now you might say, you could also achieve that by declaring and using a regular NKSP variable. That's correct, but there are two major advantages by using preprocessor statements.

  1. Saving Resources - The preprocessor conditions are only processed before the script is loaded into the NKSP parser. So in contrast to using NKSP variables, the preprocessor solution does not waste any CPU time or memory resources while executing the script. That also means that variable declarations can be disabled with the preprocessor this way and thus will also safe resources.
  2. Cross Platform Support - Since the code portions filtered out by the preprocessor never make it into the NKSP language parser, those filtered code portions might also contain code which would have lead to parser errors. For example you could use a built-in preprocessor condition to check whether your script was loaded into LinuxSampler or rather into another sampler. That way you could maintain one script for both platforms: NKSP and KSP. Accordingly you could also check a built-in variable to obtain the version of the sampler in order to enable or disable code portions of your script that might use some newer script features of the sampler which don't exist in older version of the sampler.

As a rule of thumb: if there are things that you could move from your NKSP executed programming code out to the preprocessor, then you should use the preprocessor instead for such things. And like stated above, there are certain things which you can only achieve with the preprocessor.

Disable Messages

Since it is quite common to switch a script between a development version and a production version, you actually don't need to wrap all your message() calls into preprocessor statements like in the previous example just to disable messages. There is actually a built-in preprocessor condition dedicated to perform that task much more conveniently for you. To disable all messages in your script, simply add SET_CONDITION(NKSP_NO_MESSAGE) e.g. at the very beginning of your script. So the previous example can be simplified to this:

You can then actually also add RESET_CONDITION(NKSP_NO_MESSAGE) at another section of your script, which will cause all subsequent message() calls to be processed again. So that way you can easily enable and disable message() calls of entire individual sections of your script, without having to wrap all message() calls into preprocessor statements.

What Next?

You have completed the introduction of the NKSP real-time instrument script language at this point. You can now dive into the details of the NKSP language by moving on to the NKSP reference documentation. Which provides you an overview and quick access to the details of all built-in functions, built-in variables and more.

Document Updated:  2026-02-17  |  Author:  Christian Schoenebeck