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. NSKP 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 four events available:

Event Type Description
on note This event handler is executed when a new note was triggered, i.e. when hitting a key on a MIDI keyboard.
on release This event handler is executed when a new note was released, i.e. 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 at a MIDI keyboard.
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 (i.e. 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 beend 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].

Script Load Event

As the last one of the four 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 a 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, i.e. 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 $variable-name := initial-value

The left hand side's variable-name is an arbitrary name you can chose for your variable. That name might consist of English letters A to Z (lower and upper case) and the underscore character "_". Variable names must be unique. So you can neither declare several variables with the same name, nor can you use a name for your variable that is already been reserved by built-in variables. 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.

declare $variable-name

In that case the sampler will automatically assign 0 for you as the variable's initial value. 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.

NKSP allows you to declare variables in all event handlers, however if you want to keep compatibility with KSP, then you should only declare variables in init event handlers.

Variable Types

There are currently three different variable 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 String Stores one text string.

So the first character just before the actual variable name, always denotes the data type of the variable. Also note that all variable types share the same variable name space. That means you cannot declare a variable with a name that has already been used to declare a variable of another variable type.

Array Variables

We already used the first two variable types. However we have not seen yet how to declare such array variables. This is the common declaration form for creating your own array variables.

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

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

When you omit that initial assignment, then all numbers of that array will automatically be initialized with 0 each. With array variables however, it is always mandatory to provide array-size with an array variable declaration, so the sampler can create that array with the requested amount of values when the script is loaded. 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. So NKSP does not allow you to do that.

String Variables

You might also store text with variables. These are called text string variables, or short: string variables. Let's skip the common declaration form of string variables and let us modify a prior example to just use such kind of variable.

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.

Variable Scope

By default, all variables you declare with NKSP are global variables . That means every event handler can access the data of such a global variable. Furthermore, each instance of an event handler accesses the same data when it is referencing that variable. And the latter fact can be a problem sometimes, which we will outline next.

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

You need at least LinuxSampler 2.0.0.svn2 or higher for the following example to work as described and as expected. Refer to the notes of the wait() function reference documentation for more informations about this issue.

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.

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 when executing that script exactly: 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 handler instance was executed at a time. This is different in this case though. Because by calling the 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 handler instances which are now running in parallel, are all reading and modifying the same data. Thus the individual handler instances will modify the $i and $velocity variables of each other, causing an undesired misbehavior.

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.

Polyphonic Variables

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

declare polyphonic $variable-name

So to fix the bug in our previous example, we simply make the variables $i and $velocity polyphonic variables.

And that's it! The script works now as intended. Now you might wonder, why are variables not polyphonic by default? Isn't that more common and wouldn't that be more safer than using global variables by default? The reason is that a polyphonic variable consumes a lot more memory than a regular (global) 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. So that's a lot! Considering that today's computers have plenty of RAM this might be a theoretical aspect, but in the end: this default scope of variables was already like this with KSP so we are also doing it like this with NKSP for compatibility reasons.

Please note that the polyphonic qualifier only exists for integer variables. So you cannot 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 variables.

As summary, the following are guideline rules describing when you should use the polyphonic qualifier for a certain variable. You should declare a particular variable polyphonic if one (or even both) of the following two conditions apply to that variable.

  1. If you call the wait() function within your event handlers and the respective variable is modified and read before and after at least one of the individual wait() calls.
  2. If you have loops that might run for a very long time, while accessing the respective variable in between. That's because if your script is running consecutively for too long, the sampler will automatically suspend your script for a while to avoid your script becoming a real-time stability hazard for the sampler. Your script will then automatically be resumed after a short moment by the sampler, so effectively this is similar to something like an "automated" wait() function call by the sampler.

In all other cases you should rather use regular (global) variables instead. But keep in mind that you might need to re-assign a certain value for some global variables when you enter the respective event handler, just like we did with $i := $delayNotes right from the start during discussion of the previous example script.

There is another special aspect regarding the variable scope of polyphonic variables: note handlers and release handlers of the same script share the same polyphonic variable scope, that means you may pass data from a particular note's note handler to its release handler by using the same polyphonic variable name.

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 also allows you to write your own functions, which you then may call from various places of your script.

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 so called 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 to 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:

The script became in this simple example only slightly smaller, but it also became easier to read and behaves identically to the previous solution. And in practice, with a more complex script, you can reduce the overall amount of script code a lot this way. You can choose any name for your own user functions, as long as the name is not already reserved by a built-in function. Note that for calling a user function, you must always precede the actual user function name with the call keyword. 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.

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 of 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 by used i.e. 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.

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 samlper, 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.

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:  2017-02-15  |  Author:  Christian Schoenebeck