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:
- on event-name
- statements
- end on
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.
- on note
- message("A new note was triggered!")
- end on
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.
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
The & character concatenates text strings with each other.
In this case it is also automatically converting the note number into a
text string.
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:
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
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.).
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
- on controller
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- end on
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:
- on rpn
- message("RPN address msb=" & msb($RPN_ADDRESS) & ",lsb=" & lsb($RPN_ADDRESS) &
- "-> value msb=" & msb($RPN_VALUE) & ",lsb=" & lsb($RPN_VALUE))
- if ($RPN_ADDRESS = 2)
- message("Standard Coarse Tuning RPN received")
- end if
- end on
- on nrpn
- message("NRPN address msb=" & msb($RPN_ADDRESS) & ",lsb=" & lsb($RPN_ADDRESS) &
- "-> value msb=" & msb($RPN_VALUE) & ",lsb=" & lsb($RPN_VALUE))
- end on
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.
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.
- on init
- message("This script has been loaded and is ready now!")
- end on
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.
- on init
- { The following will be prompted to the terminal when the sampler loaded this script. }
- message("My script loaded.")
- { This code block is commented out, so these two messages will not be displayed }
- { message("Another text") message("And another one") }
- end on
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:
- on init
- declare $myVariable := 1000
- end on
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:
- on init
- declare $numberOfNotes := 0
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- message("This is the " & $numberOfNotes & "th note triggered so far.")
- end on
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.
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:
- on init
- declare ~myVariable := 3.142
- end on
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.
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.
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.
- on init
- declare %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- end on
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:
- on init
- declare ?myRealNumbers[4] := ( 0.1, 2.0, 46.238, 104.97 )
- end on
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:
- on init
- declare $numberOfNotes
- declare @firstText := "This is the "
- declare @secondText
- end on
- on note
- { Increase note counter by one. }
- $numberOfNotes := $numberOfNotes + 1
- { Assign some text to string variable @secondText }
- @secondText := "th note triggered so far."
- { Assemble and print the final text message. }
- message(@firstText & $numberOfNotes & @secondText)
- end on
It behaves exactly like the prior example and shall just give you a first idea how to declare and use string variables.
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 }
real() and int() only exist in
NKSP. They don't exist with KSP.
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:
- on init
- declare $someGlobalVar := 200
- { ... }
- end on
is identical to:
- on note
- declare $someGlobalVar := 200
- { ... }
- end on
- on init
- { ... }
- end on
and the same as:
- declare $someGlobalVar := 200
- on init
- { ... }
- end on
and so on.
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.
- on init
- { The amount of notes to play }
- declare const $delayNotes := 4
- { Tempo with which the new notes will follow the orignal note }
- declare const $bpm := 90
- { Convert BPM to microseconds (duration between the notes) }
- declare const $delayMicroSeconds := 60 * 1000000 / $bpm
- { Just a working variable for being used with the while loop below }
- declare $i
- { For each successive note we trigger, we will reduce the velocity a bit}
- declare $velocity
- end on
- on note
- { First initialize the variable $i with 4 each time we enter this event handler, because each time we executed this handler, the variable will be 0 }
- $i := $delayNotes
- { Loop which will be executed 4 times in a row }
- while ($i)
- { Calculate the velocity for the next note being triggered }
- $velocity := 127 * $i / ($delayNotes + 1)
- { Suspend this script for a short moment ... }
- wait($delayMicroSeconds)
- { ... and after that short break, trigger a new note. }
- play_note($EVENT_NOTE, $velocity)
- { Decrement loop counter $i by one }
- $i := $i - 1
- end while
- end on
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.
- on init
- { The local variable $foo is created as new instance here when this code block is entered. }
- declare local $foo
- other-statements
- { Local variable $foo (and its instance) stops to exist here when this code block is left. }
- end on
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.
So to fix the discussed bug in our previous example, we simply make the variables
$i and $velocity local variables:
- on init
- { The amount of notes to play }
- declare const $delayNotes := 4
- { Tempo with which the new notes will follow the orignal note }
- declare const $bpm := 90
- { Convert BPM to microseconds (duration between the notes) }
- declare const $delayMicroSeconds := 60 * 1000000 / $bpm
- end on
- on note
- { Just a working variable for being used with the while loop below }
- declare local $i { < --- NOW LOCAL VARIABLE !!! }
- { For each successive note we trigger, we will reduce the velocity a bit}
- declare local $velocity { < --- NOW LOCAL VARIABLE !!! }
- { First initialize the variable $i with 4 each time we enter this event handler, because each time we executed this handler, the variable will be 0 }
- $i := $delayNotes
- { Loop which will be executed 4 times in a row }
- while ($i)
- { Calculate the velocity for the next note being triggered }
- $velocity := 127 * $i / ($delayNotes + 1)
- { Suspend this script for a short moment ... }
- wait($delayMicroSeconds)
- { ... and after that short break, trigger a new note. }
- play_note($EVENT_NOTE, $velocity)
- { Decrement loop counter $i by one }
- $i := $i - 1
- end while
- end on
And that's it! The script works now as intended.
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:
- on init
- declare polyphonic $noteOnVelocity
- end on
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- { Transfer note-on velocity to release event handler. }
- $noteOnVelocity := $EVENT_VELOCITY
- end on
- on release
- message("Note " & $EVENT_NOTE & " was released with velocity " & $EVENT_VELOCITY)
- { This will print the note-on velocity corresponding to this note-off event. }
- message("The note was originally triggered with velocity " & $noteOnVelocity)
- end on
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.
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.
- { Declares a global variable "$foo". }
- declare $foo := 1
- { WRONG: This would cause a duplicate error: }
- { declare polyphonic $foo }
- on note
- { OK: Declares a local variable "$foo". }
- declare local $foo := 2
- { This will print "2". }
- message($foo)
- end on
- on init
- { This will print "1". }
- message($foo)
- end on
You can even declare several local variables with the exact same name, as long as these local variables are declared in different code blocks:
- on note
- { Declares a local variable "$foo". }
- declare local $foo
- { WRONG: There is already a local variable "$foo" in this code block. }
- { declare local $foo }
- end on
- on controller
- { OK: This declares a separate local variable "$foo" in another code block. }
- declare local $foo
- end on
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.
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:
- on init
- declare patch const $speedCC := 1
- declare patch const $minTime := 9000
- declare patch const $maxTime := 350000
- end on
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.
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).
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.
- if (condition)
- statements
- end if
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.
- if (condition)
- statements-when-true
- else
- statements-when-false
- end if
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.
- on init
- declare $numberOfNotes
- declare @postfix
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- if ($numberOfNotes == 1)
- @postfix := "st"
- else
- if ($numberOfNotes == 2)
- @postfix := "nd"
- else
- if ($numberOfNotes == 3)
- @postfix := "rd"
- else
- @postfix := "th"
- end if
- end if
- end if
- message("This is the " & $numberOfNotes & @postfix & " note triggered so far.")
- end on
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.
- select expression
- case integer-1
- statements-1
- case integer-2
- statements-2
- .
- .
- .
- end select
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.
- on init
- declare $numberOfNotes
- declare @postfix
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- @postfix := "th"
- select $numberOfNotes
- case 1
- @postfix := "st"
- case 2
- @postfix := "nd"
- case 3
- @postfix := "rd"
- end select
- message("This is the " & $numberOfNotes & @postfix & " note triggered so far.")
- end on
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.
- on init
- declare $numberOfNotes
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- select $numberOfNotes
- case 1 to 99
- message("Less than 100 notes triggered so far")
- exit
- case 1
- message("First note was triggered!") { Will never be printed ! }
- exit
- case 2
- message("Second note was triggered!") { Will never be printed ! }
- exit
- case 3
- message("Third note was triggered!") { Will never be printed ! }
- exit
- end select
- message("Wow, already the " & $numberOfNotes & "th note triggered.")
- end on
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.
- on init
- declare $numberOfNotes
- end on
- on note
- $numberOfNotes := $numberOfNotes + 1
- select $numberOfNotes
- case 1
- message("First note was triggered!")
- exit
- case 2
- message("Second note was triggered!")
- exit
- case 3
- message("Third note was triggered!")
- exit
- case 1 to 99
- message("Less than 100 notes triggered so far")
- exit
- end select
- message("Wow, already the " & $numberOfNotes & "th note triggered.")
- end on
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.
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.
- while (condition)
- statements
- end while
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.
- on init
- declare $i := 3
- while ($i)
- message("Print this three times.")
- $i := $i - 1
- end while
- end on
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:
- function function-name
- statements
- end function
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:
- function myUserFunction
- message("myUserFunction was called")
- end function
- on init
- { This will print: "myUserFunction was called". }
- call myUserFunction
- end on
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.
- on note
- while (1)
- { Sleep for 200ms. }
- wait(200000)
- { Back from sleep: is the note already dead? If yes quit. }
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- exit()
- end if
- { Reduce volume by 20 dB. }
- change_vol($EVENT_ID, -20000)
- { Sleep for 200ms. }
- wait(200000)
- { Back from sleep: is the note already dead? If yes quit. }
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- exit()
- end if
- { Increase volume to 0 dB. }
- change_vol($EVENT_ID, 0)
- end while
- end on
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:
- function pauseMyScript
- { Sleep for 200ms. }
- wait(200000)
- { Back from sleep: is the note already dead? If yes quit. }
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- abort($NI_CALLBACK_ID) { NOTE: not exit() call here, see description below. }
- end if
- end function
- on note
- while (1)
- call pauseMyScript
- { Reduce volume by 20 dB. }
- change_vol($EVENT_ID, -20000)
- call pauseMyScript
- { Increase volume back to 0 dB. }
- change_vol($EVENT_ID, 0)
- end while
- end on
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:
- function myFn(arguments) -> result
- statements
- end 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:
- function sumUp($a, $b, $c) -> $d
- { $a, $b, $c are arguments passed to this user function, whereas ... }
- $d := $a + $b + $c
- { ... $d is the result value to be returned to caller of this function }
- end function
which you could then use like this:
- on init
- declare local $value := call sumUp(2, 3, 5)
- message("The sum of 2, 3 and 5 is: " & $value)
- end on
Or even shorter, like this:
- on init
- message("The sum of 2, 3 and 5 is: " & call sumUp(2, 3, 5))
- end on
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.
- { Declares global variable $foo }
- declare $foo := 1
- { OK: Function argument $foo acts like a local variable. }
- function myFn($foo)
- message("$foo is: " & $foo)
- end function
- on init
- { This will print: "$foo is: 2". }
- call myFn(2)
- { This will print: "and here $foo is: 1". }
- message("and here $foo is: " & $foo)
- end on
Taking all this, we can now simplify our previous stuttering effect by adding two function arguments to the user function:
- function waitThenChangeVolume($time, $volume)
- { Sleep for the duration passed to this user function. }
- wait($time)
- { Back from sleep: is the note already dead? If yes quit. }
- if (not (event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE))
- abort($NI_CALLBACK_ID)
- end if
- { Change volume to the value passed to this user function. }
- change_vol($EVENT_ID, $volume)
- end function
- on note
- while (1)
- call waitThenChangeVolume(200000, -20000) { Wait 200ms, then reduce volume by 20 dB. }
- call waitThenChangeVolume(200000, 0) { Wait 200ms, then increase volume back to 0 dB. }
- end while
- end on
If you want, you may further introduce a second user function to make the "is the note dead?" check more readable:
- function checkNoteStillAlive() -> $res
- $res := event_status($EVENT_ID) .and. $EVENT_STATUS_NOTE_QUEUE
- end function
- function waitThenChangeVolume($time, $volume)
- { Sleep for the duration passed to this user function. }
- wait($time)
- { Back from sleep: is the note already dead? If yes quit. }
- if (not call checkNoteStillAlive())
- abort($NI_CALLBACK_ID)
- end if
- { Change volume to the value passed to this user function. }
- change_vol($EVENT_ID, $volume)
- end function
Both user function arguments and user function return values do support optional default values:
- function myFn($a, $b := default-value, $c := default-value) -> $res := default-value
- { ... }
- end function
For instance:
- function myFn($a, $b := 10, $c := $someGlobalVar) -> $res := 20
- { 1st argument ($a) is required to be passed by caller, whereas 2nd and 3rd arguments ($b and $c) are optional arguments. }
- end function
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.
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:
- on init
- declare $keysDown
- declare $firstNoteID
- declare $firstNoteNr
- declare $firstVelocity
- end on
- on note
- { The concurrency sensible code section for the "first active" note. }
- inc($keysDown)
- if ($keysDown = 1 or event_status($firstNoteID) = $EVENT_STATUS_INACTIVE)
- $firstNoteID = $EVENT_ID
- $firstNoteNr = $EVENT_NOTE
- $firstVelocity = $EVENT_VELOCITY
- exit { return from event handler here }
- end if
- { The non-sensible code for all other subsequent notes would go here. }
- end on
- on release
- dec($keysDown)
- end on
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:
- synchronized
- statements
- end synchronized
All statements which you put into such a synchronized
code block are guaranteed that they will never get auto suspended by
the sampler.
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:
- on init
- declare $keysDown
- declare $firstNoteID
- declare $firstNoteNr
- declare $firstVelocity
- end on
- on note
- { The concurrency sensible code section for the "first active" note. }
- synchronized
- inc($keysDown)
- if ($keysDown = 1 or event_status($firstNoteID) = $EVENT_STATUS_INACTIVE)
- $firstNoteID = $EVENT_ID
- $firstNoteNr = $EVENT_NOTE
- $firstVelocity = $EVENT_VELOCITY
- exit { return from event handler here }
- end if
- end synchronized
- { The non-sensible code for all other subsequent notes would go here. }
- end on
- on release
- dec($keysDown)
- end on
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.
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.
- on init
- message("4 + 3 is " & 4 + 3) { Add }
- message("4 - 3 is " & 4 - 3) { Subtract }
- message("4 * 3 is " & 4 * 3) { Multiply }
- message("35 / 5 is " & 35 / 5) { Divide }
- message("35 mod 5 is " & 35 mod 5) { Remainder of Division ("modulo") }
- end on
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:
- on init
- message("1 and 1 is " & 1 and 1) { logical "and" }
- message("1 and 0 is " & 1 and 0) { logical "and" }
- message("1 or 1 is " & 1 or 1) { logical "or" }
- message("1 or 0 is " & 1 or 0) { logical "or" }
- message("not 1 is " & not 1) { logical "not" }
- message("not 0 is " & not 0) { logical "not" }
- end on
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.
- on init
- message("1 .and. 1 is " & 1 .and. 1) { bitwise "and" }
- message("1 .and. 0 is " & 1 .and. 0) { bitwise "and" }
- message("1 .or. 1 is " & 1 .or. 1) { bitwise "or" }
- message("1 .or. 0 is " & 1 .or. 0) { bitwise "or" }
- message(".not. 1 is " & .not. 1) { bitwise "not" }
- message(".not. 0 is " & .not. 0) { bitwise "not" }
- end on
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.
- on init
- message("Relation 3 < 4 -> " & 3 < 4) { "smaller than" comparison }
- message("Relation 3 > 4 -> " & 3 > 4) { "greater than" comparison }
- message("Relation 3 <= 4 -> " & 3 <= 4) { "smaller or equal than" comparison}
- message("Relation 3 >= 4 -> " & 3 >= 4) { "greater or equal than" comparison}
- message("Relation 3 # 4 -> " & 3 # 4) { "not equal to" comparison}
- message("Relation 3 = 4 -> " & 3 = 4) { "is equal to" comparison}
- end on
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.
- on init
- declare @s := "foo" & " bar"
- message(@s)
- end on
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
- on init
- declare $a := 1s
- declare $b := 12ms
- declare $result := $a - $b
- message("Result of calculation is " & $result)
- end on
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
- on init
- declare ~a := 2.0mdB
- declare ~b := 3.2mdB
- message( 4.0 * ( ~a + ~b ) / 2.0 + 0.1mdB )
- end on
would print the text "10.5mdB" to the terminal.
Or let's make this little value comparison check:
- on init
- declare $foo := 999ms
- declare $bar := 1s
- if ($foo < $bar)
- message("Test succeeded")
- else
- message("Test failed")
- end fi
- end on
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.
So getting back and proceed with an early example, this code would be fine:
- on note
- declare ~reduction := -3.5dB { correct unit type }
- change_vol($EVENT_ID, ~reduction)
- end note
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:
- on note
- declare ~reduction := -3.5kHz { WRONG unit type for change_vol() call! }
- change_vol($EVENT_ID, ~reduction)
- end note
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.
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:
- on note
- declare $someFrequency := 100Hz
- change_vol($EVENT_ID, $someFrequency / 1Hz * 1mdB)
- end note
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:
- the divsion (by
/ 1Hz) dropped the old unit type (Hertz), - the multiplication (by
* 1mdB) added the new unit type (Bel) - 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.
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!
- declare %foo[4] := ( 800, 1m, 14c, 43) { OK - metric prefixes, but no unit types }
- declare %bar[4] := ( 800s, 1ms, 14kHz, 43mdB) { WRONG - unit types not allowed for arrays yet }
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.
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:
- function calcFrequency(~freq := 0.0Hz) -> ~resFreq := 0.0Hz
- ~resFreq := ~freq + 20.0Hz
- end function
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
- on note
- declare $volume := -6dB
- change_vol($EVENT_ID, !$volume) { '!' makes value read from variable $volume to become 'final' }
- end note
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:
- on note
- declare $volume := !-6dB { store 'finalness' directly to variable $volume }
- change_vol($EVENT_ID, $volume)
- end note
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.
! "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:
- on note
- declare $volume := !-6dB { store 'finalness' directly to variable $volume }
- change_vol($EVENT_ID, $volume + 2dB) { raises parser warning here ! }
- end note
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:
- on note
- declare $volume := !-6dB { store 'finalness' directly to variable $volume }
- change_vol($EVENT_ID, $volume + !2dB) { '!' fixes parser warning }
- end note
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. |
! 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.
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
- use certain parts of your code
- and / or to ignore certain parts of your code
You can achieve that by wrapping NKSP code parts into a pair of either
- USE_CODE_IF(condition-name)
- some-NKSP-code-goes-here
- END_USE_CODE
preprocessor statements, or between
- USE_CODE_IF_NOT(condition-name)
- some-NKSP-code-goes-here
- END_USE_CODE
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.
- SET_CONDITION(DEBUG_MODE)
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- USE_CODE_IF(DEBUG_MODE)
- message("This script has just been loaded.")
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- USE_CODE_IF(DEBUG_MODE)
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on release
- USE_CODE_IF(DEBUG_MODE)
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on controller
- USE_CODE_IF(DEBUG_MODE)
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- END_USE_CODE
- end on
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:
- { Setting the condition is commented out, so our DEBUG_MODE is disabled now. }
- { SET_CONDITION(DEBUG_MODE) }
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, so this entire block will be ignored now. }
- message("This script has just been loaded.")
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on release
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- END_USE_CODE
- end on
- on controller
- USE_CODE_IF(DEBUG_MODE) { Condition is not set, no message will be printed. }
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- END_USE_CODE
- end on
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.
- 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.
- 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:
- { Enable debug mode, so show all debug messages. }
- SET_CONDITION(DEBUG_MODE)
- { If our user declared condition "DEBUG_MODE" is not set ... }
- USE_CODE_IF_NOT(DEBUG_MODE)
- { ... then enable this built-in condition to disable all message() calls. }
- SET_CONDITION(NKSP_NO_MESSAGE)
- END_USE_CODE
- on init
- declare const %primes[12] := ( 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37 )
- declare $i
- message("This script has just been loaded.")
- USE_CODE_IF(DEBUG_MODE)
- $i := 0
- while ($i < num_elements(%primes))
- message("Prime " & $i & " is " & %primes[$i])
- $i := $i + 1
- end while
- END_USE_CODE
- end on
- on note
- message("Note " & $EVENT_NOTE & " was triggered with velocity " & $EVENT_VELOCITY)
- end on
- on release
- message("Note " & $EVENT_NOTE & " was released with release velocity " & $EVENT_VELOCITY)
- end on
- on controller
- message("MIDI Controller " & $CC_NUM " changed its value to " & %CC[$CC_NUM])
- end on
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.